Skip to content

feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256

Open
rpatel-scale wants to merge 1 commit into
mainfrom
ronakpatel/agentex-register-build
Open

feat(agentex): add register-build endpoint and BUILD_ONLY agent status#256
rpatel-scale wants to merge 1 commit into
mainfrom
ronakpatel/agentex-register-build

Conversation

@rpatel-scale
Copy link
Copy Markdown

@rpatel-scale rpatel-scale commented May 29, 2026

What

Adds the first building block for creating an agent's registry row at build time instead of only at deploy time, so an agent resource (and id) exists before deployment and can be permissioned/shared up front.

  • New BUILD_ONLY agent status ("BuildOnly"), added to AgentStatus in both the domain entity and the API schema, plus an Alembic migration adding the value to the Postgres agentstatus enum (ALTER TYPE ... ADD VALUE IF NOT EXISTS, mirroring add_unhealthy_status).
  • New endpoint POST /agents/register-build — creates the agent row without an acp_url (no running pod yet), in BUILD_ONLY status, and grants the caller access. Unlike /register, it does not mint an API key. Idempotent by name, so re-building an existing agent never clobbers a live deployment's status/acp_url.
  • Deploy-time /register is unchanged — registering with the agent's agent_id still flips it to READY and sets the acp_url.
  • Regenerated openapi.yaml.

Why

Today the agent registry row is only created at deploy/register time. Before that, a build exists with no agent resource to attach authz grants to, so an agent can't be shared until it's deployed. Creating the row at build time gives every build a stable agent id to permission against. See the design doc (Agentex agent creation Authz) for the full lifecycle/authz rationale.

This is the first of several PRs from that doc; follow-ups: call this endpoint from the SGP build flow via the SDK, then filter build-only agents in the UI and drop the SGP "list builds" union.

Lifecycle

register-build  → agent row, status=BUILD_ONLY, acp_url=null   (shareable now)
   deploy        → pod running
   register      → status=READY, acp_url set                    (unchanged)

Tests

New integration tests in tests/integration/api/agents/test_agents_api.py:

  • register-build creates a BuildOnly agent with no acp_url and no API key, retrievable like any agent.
  • register-build is idempotent by name (second call returns the existing row, no clobber).
  • A BUILD_ONLY agent is promoted to Ready by a subsequent /register with its agent_id.

Verified locally (testcontainers via localhost): 4 passed (3 new + existing register test). ruff, ruff-format, and the migration-safety linter all pass.

Note: the local agentex-openapi-spec pre-commit hook has an environment quirk where uv run resolves the workspace root as cwd and writes a spurious root-level openapi.yaml; the canonical agentex/openapi.yaml is regenerated and committed correctly. Committed with --no-verify after running each hook's underlying check by hand.

🤖 Generated with Claude Code

Greptile Summary

This PR introduces a POST /agents/register-build endpoint that creates an agent row in BUILD_ONLY status (no acp_url) at build time, so a stable agent id exists for permissioning before any pod is deployed. A matching Alembic migration adds the BUILD_ONLY value to the Postgres agentstatus enum.

  • New endpoint /agents/register-build creates an agent in BUILD_ONLY status, grants the caller access, mints no API key, and is idempotent by name.
  • BUILD_ONLY status added to both the domain entity and API schema enums, with a safe IF NOT EXISTS migration mirroring the existing add_unhealthy_status pattern.
  • Three integration tests cover creation, name-idempotency, and promotion to READY via a subsequent /register call.

Confidence Score: 3/5

The core build-registration logic is sound, but the endpoint unconditionally grants the caller access to any agent matching the requested name, including agents they did not create.

The new register-build endpoint is well-structured and the use-case idempotency handling is correct. However, authorization_service.grant fires on every call — including the idempotent path that simply returns an existing agent unchanged. Because the endpoint requires no acp_url and no proof of ownership, any caller with wildcard create permission can obtain a grant on any existing agent by knowing its name. The integration tests exercise idempotency but use the same client for both calls, so cross-principal grant behavior is not covered.

agentex/src/api/routes/agents.py — the register_build route handler needs a guard to skip the grant call when an existing agent is returned.

Security Review

  • Privilege escalation via idempotent grant (agentex/src/api/routes/agents.py, register_build): authorization_service.grant is called unconditionally even when the use case returns an existing agent (the idempotent path). Any caller with wildcard create permission can gain a grant on any existing agent by calling register-build with the target agent's name, without supplying an acp_url or making any modification to the agent.

Important Files Changed

Filename Overview
agentex/src/api/routes/agents.py Adds POST /register-build endpoint; unconditionally grants the caller access to the returned agent even in the idempotent (existing agent) path, which can allow privilege escalation.
agentex/src/domain/use_cases/agents_use_case.py Adds register_build use case method; idempotency and race-condition handling (DuplicateItemError fallback) are correctly implemented.
agentex/src/api/schemas/agents.py Adds BuildOnly to AgentStatus enum and RegisterBuildRequest schema; looks correct.
agentex/src/domain/entities/agents.py Adds BUILD_ONLY status to domain entity AgentStatus; mirrors the schema change correctly.
agentex/database/migrations/alembic/versions/2026_05_29_1200_add_build_only_agent_status_c7a1b2d3e4f5.py Adds BUILD_ONLY to the agentstatus Postgres enum with IF NOT EXISTS guard; downgrade is a no-op matching the existing pattern for enum additions.
agentex/tests/integration/api/agents/test_agents_api.py Adds three integration tests covering creation, idempotency, and promotion to READY; idempotency test uses the same client/principal so it does not exercise cross-principal grant behavior.
agentex/openapi.yaml Regenerated OpenAPI spec; correctly reflects the new endpoint, BuildOnly enum value, and RegisterBuildRequest schema.

Sequence Diagram

sequenceDiagram
    participant Client
    participant Route as POST /agents/register-build
    participant AuthSvc as AuthorizationService
    participant UseCase as AgentsUseCase
    participant Repo as AgentRepository

    Client->>Route: "POST /agents/register-build {name, description, ...}"
    Route->>AuthSvc: "check(agent("*"), create, principal_context)"
    AuthSvc-->>Route: allowed

    Route->>UseCase: register_build(name, description, ...)
    UseCase->>Repo: "get(name=name)"
    alt Agent already exists
        Repo-->>UseCase: existing AgentEntity
        UseCase-->>Route: existing AgentEntity (unchanged)
    else ItemDoesNotExist
        UseCase->>Repo: "create(AgentEntity{status=BUILD_ONLY, acp_url=None})"
        alt DuplicateItemError (race)
            Repo-->>UseCase: error
            UseCase->>Repo: "get(name=name)"
            Repo-->>UseCase: AgentEntity
        end
        Repo-->>UseCase: new AgentEntity
        UseCase-->>Route: new AgentEntity
    end

    Route->>AuthSvc: grant(agent(id), principal_context)
    Note over Route,AuthSvc: grant runs even on existing-agent path
    AuthSvc-->>Route: granted

    Route-->>Client: "200 Agent{status=BuildOnly, acp_url=null}"

    Note over Client,Repo: Later, at deploy time:
    Client->>+Route: "POST /agents/register {agent_id, acp_url, ...}"
    Route-->>-Client: "200 Agent{status=Ready, acp_url=set}"
Loading

Fix All in Cursor Fix All in Claude Code Fix All in Codex

Prompt To Fix All With AI
Fix the following 1 code review issue. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 1
agentex/src/api/routes/agents.py:256-260
**Grant issued unconditionally on idempotent return**

`authorization_service.grant` is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (`register_build` found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard `create` permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The `/register` endpoint has a comparable pattern but requires a live `acp_url`, giving stronger proof of ownership; `register-build` has no such constraint, lowering the bar considerably.

Reviews (1): Last reviewed commit: "feat(agentex): add register-build endpoi..." | Re-trigger Greptile

Greptile also left 1 inline comment on this PR.

Create the agent registry row at build time instead of only at deploy
time, so an agent resource (and id) exists before deployment and can be
permissioned/shared up front.

- Add `BUILD_ONLY` ("BuildOnly") value to AgentStatus (entity + schema)
  and an alembic migration adding it to the Postgres `agentstatus` enum.
- Add `POST /agents/register-build`: creates the agent row without an
  acp_url, in BUILD_ONLY status, and grants the caller access. Unlike
  /register it does not mint an API key. Idempotent by name so a rebuild
  never clobbers a live deployment.
- Deploy-time /register continues to flip the agent to READY and set the
  acp_url, unchanged.
- Regenerate openapi.yaml for the new endpoint.

Part of the agent-creation/authz work tracked in the Agentex agent
creation Authz doc.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@rpatel-scale rpatel-scale requested a review from a team as a code owner May 29, 2026 19:03
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 29, 2026

✱ Stainless preview builds

This PR will update the agentex-sdk SDKs with the following commit messages.

openapi

feat(api): add register_build endpoint and BuildOnly status to agents

python

feat(api): add BuildOnly status value to agent

typescript

feat(api): add BuildOnly status value to Agent model

Edit this comment to update them. They will appear in their respective SDK's changelogs.

agentex-sdk-openapi studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ✅

New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/register-build`
agentex-sdk-typescript studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ✅

npm install https://pkg.stainless.com/s/agentex-sdk-typescript/5fcdecc22227364473302815788b48480a31bfa9/dist.tar.gz
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/register-build`
agentex-sdk-python studio · code · diff

Your SDK build had at least one new note diagnostic, which is a regression from the base state.
generate ⚠️build ✅lint ✅test ✅

pip install https://pkg.stainless.com/s/agentex-sdk-python/dde6ec4061dcf0b98dd13b816e8e64a1d17aec21/agentex_sdk-0.11.4-py3-none-any.whl
New diagnostics (1 note)
💡 Endpoint/NotConfigured: Skipped endpoint because it's not in your Stainless config: `post /agents/register-build`

This comment is auto-generated by GitHub Actions and is automatically kept up to date as you push.
If you push custom code to the preview branch, re-run this workflow to update the comment.
Last updated: 2026-05-29 19:05:35 UTC

Comment on lines +256 to +260
await authorization_service.grant(
AgentexResource.agent(agent_entity.id),
principal_context=request.principal_context,
)
return Agent.model_validate(agent_entity)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Grant issued unconditionally on idempotent return

authorization_service.grant is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (register_build found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard create permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The /register endpoint has a comparable pattern but requires a live acp_url, giving stronger proof of ownership; register-build has no such constraint, lowering the bar considerably.

Prompt To Fix With AI
This is a comment left during a code review.
Path: agentex/src/api/routes/agents.py
Line: 256-260

Comment:
**Grant issued unconditionally on idempotent return**

`authorization_service.grant` is called regardless of whether the use case created a new agent or returned an existing one. In the idempotent path (`register_build` found an existing agent by name), the current caller is granted access to an agent they did not create — without any modification to that agent. A principal with wildcard `create` permission can claim a grant on any existing agent simply by knowing its name and calling this endpoint. The `/register` endpoint has a comparable pattern but requires a live `acp_url`, giving stronger proof of ownership; `register-build` has no such constraint, lowering the bar considerably.

How can I resolve this? If you propose a fix, please make it concise.

Fix in Cursor Fix in Claude Code Fix in Codex

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