Skip to content

Split the Core/Web boundary: official /v1, one Core-key /core/v1 tree, Core-key executor credentials - #128

Merged
SaladDay merged 32 commits into
mainfrom
codex/core-web-boundary
Sep 25, 2026
Merged

SaladDay merged 32 commits into
mainfrom
codex/core-web-boundary

Conversation

@SaladDay

@SaladDay SaladDay commented Sep 25, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Completes the Core/Web API boundary as a single paired release (backend P2b and P4, plus the Web P2b from #127):

  • /v1 is exactly the pinned official Agents API. It serves only the pinned SDK route set. Non-official request and response fields live only under x_agents_core (harness, model_provider). The eight extension routes and sandbox_node_id are gone. Route-set and field-placement tests hold this in place.
  • /core/v1 is one Core-key tree for the Web and operator scripts. The /admin segment is dropped: projects/**, summary, audit-log, metrics, and sandbox/{deployment,enrollment-tokens,nodes,runtime-observations}. Authentication runs before routing, so unknown paths and 405 responses also require the Core key. A Project key gets 401 everywhere under /core/v1, and the Core key gets 401 on /v1 and /api/v1.
  • Executor credentials are issued with the Core key. The routes are GET/POST/DELETE /core/v1/projects/{p}/environments/{e}/executor-credentials[/{key_id}].
    • Error order: body 400 → environment 404 → archived Project 409 project_archived → existing key 409 executor_credential_exists / rotating an unknown key 404.
    • The archive check is repeated inside the write transaction (FOR SHARE against the archive's FOR UPDATE).
    • The audit entry commits in the same transaction and records only the key ID.
    • Responses carry Cache-Control: no-store.
    • Revocation is idempotent, including in an archived Project.
  • The console forwards by prefix. services/core-console forwards every signed-in, same-origin /core/v1/* request with the Core key. It no longer uses per-route allowlists.
    • safePath rejects encoded %, dot segments, empty segments and backslashes.
    • Upgrade, CONNECT and TRACE requests are rejected.
    • The browser's Authorization, Cookie, Origin and Referer headers are stripped, and the actor header is overwritten to console.
  • Client. The Core clients in packages/agents-client have their own /core/v1 request layer and no longer inherit the /v1 client. The package exports CoreProjectReader and the executor credential types, and CoreMetricsClient.baseUrl is now a prefix.
  • Contracts. The /core/v1 contract is generated as contracts/agents-api/core.openapi.yaml. openapi.yaml covers only the pinned official routes.
  • Web (Read the console through Core's /core/v1 routes and manage executor credentials #127). The console reads through the new routes. A self-hosted Session can issue, rotate and revoke its executor credentials. The fixtures match Core's rules, and the docs describe prefix forwarding.
  • Docs. They cover the three namespaces (/v1, /core/v1, /api/v1), and the release guide now issues executor credentials with the Core key. The database command is kept only as break-glass.

Review

  • P4: independent blind review, with the fixes reviewed before it formed the integration base.
  • P2b (backend): independent blind review of the full diff from main, focused on authentication, Project scoping, executor credentials, the console proxy, the client and docs. It found no P0 or P1. Four P3s were fixed in 158f6b9, 09070b8, 7dba5e7 and f2ab0ac. Those fixes were verified by focused tests and the gate below, without a re-review, following the small-fix rule. The one P2 (stale docs/web allowlist text) was fixed on the Web side in 5c93243.
  • Web P2b (Read the console through Core's /core/v1 routes and manage executor credentials #127): independent blind review with no P0 or P1. Three P2s and some nits (retained key_id reuse after rotate or revoke, recovery of a non-live key on 409, docs out of step with Core, a fixture 500 on invalid JSON, UI copy) were fixed in d766039, 05aa4d7 and f9b145c. Those fixes were verified by tests, a new regression e2e and the gate below, without a re-review.

Verification

  • Full local server gate on f9b145c6 (the exact head of this PR): passed. It covers make check plus typecheck, core-doctor, the Web unit tests and the Web build.
  • The Playwright browser acceptance cases were skipped on the server (Chrome is unavailable there; the skip is approved). The frontend ran the full e2e suite locally: 17/17 passed.
  • Regenerating openapi.yaml, core.openapi.yaml and runtime.openapi.yaml produces no diff.
  • A real end-to-end run on P2b: the new /core/v1 paths serve, the old paths return 404, a Core-key-issued executor credential lets a real daemon connect, and after revocation the daemon is refused.
  • GitHub CI is not a merge requirement for this batch. The known agents-api job timeout also occurs on main and is tracked for a follow-up.

Compatibility

  • This is a breaking change for any caller of the removed /v1 extensions or the old /core/v1/admin/** paths. No product code depends on them yet.
  • The Web and Core must be deployed together.
  • The node transport stays at /api/v1/sandbox-node/* (Move node transport to /api/v1 and stop forwarding it through Web #119). A demo drain from a pre-spec deployment must still use a pre-P3 build.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.

/v1 now has the 58 method and path pairs of the pinned OpenAI Python SDK
3.13.0. The eight Core extension routes are removed from /v1: startup
configuration, Runtime observations (list and per Session), Runtime
history (capabilities and per Session), Session execution configuration,
Session sandbox placement and the sandbox node directory. Their
administrator equivalents under /core/v1/admin keep the same handlers.

Placement is automatic only: x_agents_core.sandbox_node_id is removed and
is now rejected as an unknown member (400). The store no longer selects an
explicit node, and the unused per-Session placement read is gone.

The shared client drops SandboxProjectClient and the public /v1 readers
for the removed routes; AdminClient keeps its /core equivalents.

scripts/extract-agents-api-upstream.py writes upstream-routes.json and
upstream-fields.json from the pinned SDK. Contract tests require the
public OpenAPI to have exactly those routes and to keep every
non-official field inside x_agents_core, with three shared-schema
mismatches listed for a decision.
Port the ordering, concurrency-limit and whole-page failure tests of the
removed /v1 list to GET /core/v1/admin/runtime-observations, including its
Project labels and tenant binding.
The field audit now accepts x_agents_core only on the Agent, saved-Agent
and Session definitions, with harness and model_provider as its only
members, and compares query parameters with the SDK's query params types,
which the extractor now records. A test proves the audit catches
sandbox_node_id in the Session or Agent extension and x_agents_core on a
Vault, and the extracted operations must equal the pinned route set.

The router walk compares the served non-HEAD /v1 routes with the pinned
set. Administrator Runtime tests cover the unconfigured 503 and HEAD 405,
and the client tests cover the observation page invariants.

Docs no longer mention explicit node selection or Project node reads, say
when a Session carries agent.x_agents_core, list executor credential
issuance in the API index and drop the removed /v1 Runtime reads from the
HEAD exceptions.
…ry-p2b

# Conflicts:
#	contracts/agents-api/admin-api.md
#	contracts/agents-api/sandbox-manager.openapi.yaml
#	services/agents-api/internal/api/admin_resources.go
…thout a route

P1 removed their /core/v1/admin routes and P4 removed their /v1 routes, so
after the merge the handlers, Core types, server composition, client
projections and tests had no caller.
/core/v1/admin/** moves to /core/v1/**: projects, summary, audit-log,
metrics (was core-metrics) and sandbox/runtime-observations. One
Core-key router serves the whole tree, so every /core/v1 request,
including an unknown path, is authenticated before routing, and old
/core/v1/admin paths return 404.

Executor credentials move from the Project-key route
/core/v1/environments/{id}/executor-credentials to
/core/v1/projects/{project_id}/environments/{environment_id}/executor-credentials
with the Core key: GET lists metadata, POST issues or rotates, DELETE
revokes. The target must be a self_hosted Environment of the Project whose
Session exists. The Project's principal remains the execution principal,
the scope stays daemon enrollment and connection for that Environment, and
each write records an administrator audit entry in its transaction.
…re key

The per-route allowlists in admin_routes.go and sandbox_admin.go and the
/core/v1/environments special case are replaced by one rule: after console
sign-in and the existing origin and CSRF checks, a /core/v1/* request is
forwarded with the Core key and Core decides whether the route exists.
/v1 and /api/v1 still return 404, and the path checks that keep dot
segments and encoded separators from leaving /core/v1 are unchanged.
AdminClient, SandboxAdminClient and CoreMetricsClient keep their names and
methods but no longer extend the /v1 OpenAIAgentsClient. They share an
internal Core request layer that reuses AdminClient's error projection and
AgentCoreError, never sends OpenAI-Beta and sends a bearer only when a Core
key is given. Defaults are /core/v1, /core/v1/sandbox and /core/v1/metrics;
listRuntimeObservations reads /core/v1/sandbox/runtime-observations.

Export CoreProjectReader as agreed with Web, tighten listSkills,
listSkillVersions and listSourceFiles to SkillList, SkillVersionList and
SourceFileList so a project-bound AdminClient satisfies it without a cast,
and add listExecutorCredentials, issueExecutorCredential and
revokeExecutorCredential with ExecutorCredential,
IssueExecutorCredentialInput and IssuedExecutorCredential.
SandboxNode.diagnostic is optional because Core omits it when ready.
sandbox-manager.openapi.yaml held every /core/v1 route, not only sandbox
administration. make openapi now splits the generated document by
namespace into openapi.yaml (/v1), core.openapi.yaml (/core/v1) and
runtime.openapi.yaml (/api/v1); the public openapi.yaml is unchanged.
A table over the served composition shows a Project API key reaches only
/v1, the Core key only /core/v1, and node and executor credentials only
their own /api/v1 machine connection routes; every other cell is 401.
AGENTS.md is replaced with the agreed draft adjusted to the final facts.
docs/api/README.md becomes the /v1, /core/v1 and /api/v1 table with a
/core/v1 route-group index; CONTRIBUTING.md, design principles, the Web
and public API guides and getting-started describe Web as a /core/v1-only
client behind a prefix proxy. Contracts move from /core/v1/admin to
/core/v1, the executor credential contract and self-hosted installation
steps describe Core-key issuance by the operator, and the removed startup
configuration and history capability reads are noted.
…125)

Keep #124's Core-key sign-in, fixed console actor label and configuration
names; the console still forwards every signed-in /core/v1 request by the
prefix rule instead of the removed allowlists. Installer, console tests and
docs take #124's wording with /core/v1 paths in place of /core/v1/admin.
#123's allocation test uses automatic placement, since P4 removed explicit
node selection.
A key_id that is not a canonical lowercase, non-nil UUID is 400 before any
store call, and rotate:true for a key_id that was never issued is 404.
Issue and rotate return the existing 409 project_archived. The check
share-locks the Project inside the issuing transaction, before the write and
its audit entry, so a concurrent archive either follows the issuance or is
seen by it. Listing and revocation stay available, because revoking must
always work.
The generated Core contract described its operations as deployment
administrator only; it now names the Core key, as the docs do, and says the
Project ID, not a key, selects the target space. openapi.yaml is unchanged.
Issue and rotate now validate the request body (400), then the target
Environment (404), then an archived Project (409 project_archived), and only
then the key: 409 executor_credential_exists without rotate, or 404 for
rotating a key_id that was never issued. Previously the Project was
resolved before the body, and rotation checked the key before the archive.
The in-transaction archive check stays for concurrent archives.
The Core-key POST under /core/v1/projects/{p}/environments/{e} is now the
instruction; keys.json no longer exists. agents-api-environment-key remains
only as a marked break-glass database path that skips the archived-Project
check and writes no administrator audit entry.
The default is now /core/v1 and the client requests /metrics?range=..., so
new CoreMetricsClient() still reads /core/v1/metrics. Option names are
unchanged; each option has a one-line doc comment.
An unconsumed enrollment token reads node configuration and gets 401 on
/v1, /core/v1, node identity and both agent-daemon routes. The matrix now
also covers daemon enrollment, which only the executor credential passes.
It was the last Core operation tagged "Core extensions"; it is now
"Execution configuration", beside "Runtime observations" and "Runtime
history". Project resource summaries say "in a Project" instead of the
stale "in a managed key space". openapi.yaml is unchanged.
Rotating a key_id never issued for the environment returns 404, key IDs must be lowercase non-nil canonical UUIDs, and not-found responses use Core's not_found_error code.
Core refuses issue and rotate on an archived project with 409 project_archived while list and revoke still work. The fixture returns that 409, and the Session page hides Issue and Rotate with a note; a write that meets project_archived shows the same note as its error and rereads the projects instead of entering rotate recovery.
A kept key ID from an issuance with an unknown outcome is no longer sent
again once it is listed, and a successful rotation or revocation of it
forgets it, so the next Issue starts a new credential instead of offering
to rotate one that was just rotated or revoked. An existing key ID (409)
opens rotation only after a fresh read shows it active in an active
project; otherwise the issuance is reported as rejected.

Dismissing the one-time credential dialog now keeps the credential on the
page until Done, as the API-key flow does. The list's date column is
"Created", and the empty state no longer repeats the Issue button.
Core accepts the self_hosted environment of any existing Session in the project and returns 404 otherwise; its only conflicts are project_archived and executor_credential_exists, and rotation restores a revoked key, which the console hides by choice. Record the archived-project view, the recovery rules and the Session page's credential management.
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