Skip to content

fix(boundaries): harden DNS pinning, GraphQL cost limits, and CLI/provider validation (salvage #764, #774, #778) - #788

Merged
unohee merged 1 commit into
mainfrom
salvage/boundaries
Sep 28, 2026
Merged

unohee merged 1 commit into
mainfrom
salvage/boundaries

Conversation

@unohee

@unohee unohee commented Sep 28, 2026

Copy link
Copy Markdown
Collaborator

Summary

Rebuilds the network-boundary work from three abandoned draft PRs as one reviewable change on current main.

Salvaged from: #764, #774, #778.

Kept

#764 — remote redirects, callbacks, schemas, GraphQL execution

  • support/outboundUrl.ts: resolvePublicHttpUrl now returns the validated addresses alongside the URL, and createPinnedPublicLookup answers the connect.lookup hook from that same set — a second DNS answer can no longer steer the socket after the check. publicFetch installs a per-request pinned dispatcher instead of a shared, unpinned agent. assertPublicHttpUrl stays as a thin wrapper.
  • issues/graphql/server.ts: GRAPHQL_MAX_DEPTH/FIELD_COUNT/ALIAS_COUNT/COST + createGraphQLCostRule (depth-weighted cost) installed via a typed Yoga plugin; isGraphQLRequest now matches the exact /graphql path (with query/hash) instead of startsWith.
  • mcp/mcpClient.ts: MAX_INPUT_SCHEMA_BYTES/MAX_INPUT_SCHEMA_PROPERTIES + countSchemaProperties (walks nested objects, composed schemas, arrays, additionalProperties).
  • auth/oauthPkce.ts: isLoopbackRemote guard on the callback server; re-exported from auth/index.ts.
  • verify/runner.ts: buildVerifyToolchainPath replaces the inherited sandbox PATH with project bins + an explicit read-only allowlist.
  • adapters/webTools.ts: cancel the redirect body before the next hop.

#774 — GraphQL costing

  • issues/graphql/costAnalysis.ts: registry CRUD costs (registerEntity 100, updateEntity/removeEntity 80, addEntityRelation/removeEntityRelation 60) so aliasing them multiplies past the limit.
  • issues/graphql/server.ts: applyCors returns early when no Origin is present.
  • Tests for bulk-register alias/fragment multiplication.

#778 — provider/CLI boundary data

  • adapters/rateLimitError.ts: parseRetryAfterSeconds (RFC 7231 delta-seconds or HTTP-date); classifyLimitResponse uses it and falls back to the codex reset epoch.
  • cli/mcpCommand.ts: preset/url/command validated before the registry is written.
  • cli/prCreate.ts: fail closed on a dirty tree or a branch with no upstream.
  • adapters/webTools.ts: refuse non-http(s) redirect destinations.

Fixed the failing Tests check from #778

  • Added the missing 'rate limit exceeded' signature and the local-server overload entry, so matchesRateLimitMessage('Rate limit exceeded') / detectRateLimit are truthy.
  • The helper used {attempt, maxAttempts} while ThrottleState is {attempts}; the tests now use the real shape.

Dropped

  • Junk/scratch: _trigger_vitest.txt, run-agt3442-vitest-temp.sh, cli-permissions-override.json, cursor-*.json, hooks.json + hooks/, ls, run_diag.sh, scripts/agt3473-bootstrap.sh, scripts/run-cost-analysis-smoke.mjs, _cursor_dir_probe/, git-index-copy.txt, agt3473-probe.txt.
  • Already byte-identical in main (verified by hash): deletedTestGuard.{ts,test.ts}, publicationReviewHook.{ts,test.ts}, publishOnPark.test.ts, recallStatus.test.ts, and the publishOnPark/runnerExecution parked-publication hook wiring.
  • package.json/lock version churn — main is at 0.24.3; fix(graphql-costing): account for aliased expensive mutations — enforce query-cost limits against resolver multiplication #774 would have reverted it to 0.24.0.

Conflicts resolved

  • issues/graphql/server.ts + costAnalysis.ts (764 × 774): kept main's onValidate cost plugin (onParse/replaceParseResult is measured to return 500) and main's AUTO_LINK_MEMORIES_COST, merged 764's shape rule and exact-path match and 774's registry-cost table and CORS guard.
  • adapters/webTools.ts (764 × 778): both hunks kept — redirect-body cancellation plus the non-http(s) refusal, ordered so the protocol check runs before the origin check.
  • verify/runner.ts: kept main's clone-timeout (CLONE_TIMEOUT_MS/gitTimeoutMsFor) and test-resource-budget code; only the PATH construction changed.
  • auth/oauthPkce.ts: rejected fix(boundaries): harden remote redirects, callbacks, schemas, and GraphQL execution — constrain untrusted network inputs #764's listen('localhost') — measured on macOS it resolves once and binds ::1 only, so 127.0.0.1 clients get ECONNREFUSED. Main's deterministic loopback bind is kept; isLoopbackRemote still accepts the v4, v6 and v4-mapped forms.

Two deliberate tightenings found while verifying:

  • 'overloaded' is matched as 'server is overloaded', not as a bare word: Anthropic/OpenRouter report a 529 capacity blip with that single word, and matching it would re-bucket every overload ahead of isInfraError into a scheduler pause.
  • buildVerifyToolchainPath places the running interpreter's own bin directory directly after the project bins — on this host /usr/local/bin/node is a different major version, and the sandbox must run the interpreter the daemon runs.

Verification

  • npx tsc --noEmit — clean.
  • Affected tests (10 files): 263 passed.

Smoke-tested against real surfaces, not just mocks:

  • DNS pinning: pinned lookup answers (addresses) / (address, family) from the pinned set, refuses a set containing a private address, and a real publicFetch to a public origin streams the full body.
  • GraphQL: aliased bulkRegisterEntities → 400 GRAPHQL_COST_LIMIT_EXCEEDED; registerEntity priced below bulk; /graphqlfoo, /graphql/admin, /graphql/ → 404 while /graphql?query= → 200; OPTIONS with no Origin is not answered by the CORS layer, a bad origin gets 403, localhost gets 204; GraphiQL still renders and full introspection fits the caps (237/250).
  • prCreate: refuses clean-no-upstream, clean-in-sync and dirty trees; publishes only when clean and ahead of @{u}.
  • webTools: a real public redirector returning file:/javascript: is refused and never followed; a legitimate hop lands with the body intact.
  • mcpCommand: malformed URL / unknown preset / blank name refused with nothing written to the registry.
  • buildVerifyToolchainPath: 45 host entries → 13, still resolving node/npm/git.

Refs #764, #774, #778.

…d CLI/provider validation

Salvages the boundary work from draft PRs #764, #774 and #778 onto current main.

From #764:
- support/outboundUrl: resolvePublicHttpUrl returns the validated addresses,
  createPinnedPublicLookup answers the connect hook from that same set (no
  second DNS round-trip), and publicFetch installs a per-request pinned
  dispatcher instead of a shared unpinned agent.
- issues/graphql/server: GRAPHQL_MAX_DEPTH/FIELD_COUNT/ALIAS_COUNT/COST plus
  createGraphQLCostRule, and exact-path '/graphql' matching.
- mcp/mcpClient: MAX_INPUT_SCHEMA_BYTES/PROPERTIES with countSchemaProperties.
- auth/oauthPkce: isLoopbackRemote guard on the callback server (+ re-export).
- verify/runner: buildVerifyToolchainPath replaces the inherited sandbox PATH.
- adapters/webTools: cancel the redirect body before the next hop.

From #774:
- issues/graphql/costAnalysis: registry CRUD costs (registerEntity 100,
  updateEntity/removeEntity 80, addEntityRelation/removeEntityRelation 60).
- issues/graphql/server: applyCors returns early without an Origin header.
- bulkRegisterEntities alias/fragment-multiplication tests.

From #778:
- adapters/rateLimitError: parseRetryAfterSeconds handles HTTP-date
  Retry-After; classifyLimitResponse falls back to the codex reset epoch.
- cli/mcpCommand: preset/url/command validation before persisting the registry.
- cli/prCreate: fail closed on a dirty tree or a branch with no upstream.
- adapters/webTools: refuse non-http(s) redirect destinations.

Kept main's newer clone-timeout/resource-budget code in verify/runner and its
onValidate cost plugin; dropped the already-in-main #774 files and all scratch
probe files.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant