Skip to content

feat(agents): add Agent Auth blueprints, tokens, instances and sessions - #104

Merged
gjtorikian merged 5 commits into
mainfrom
devin/1788906573-agent-blueprints
Sep 9, 2026
Merged

feat(agents): add Agent Auth blueprints, tokens, instances and sessions#104
gjtorikian merged 5 commits into
mainfrom
devin/1788906573-agent-blueprints

Conversation

@m0tzy

@m0tzy m0tzy commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds Agent Auth to the emulator: the 13 endpoints under agents.blueprints, agents.blueprints.tokens, agents.instances and agents.sessions, the seven agent.* events (constants already existed in generated/events.ts), and an agentBlueprints seed key. SUPPORTED.md goes from Agents 0/16 to Agent Auth 13/13; Agent Registration (PATCH /agents/claims/attempts, POST /agents/credentials/validate) is intentionally left unimplemented and listed as its own row.

Behaviour was modelled on the API's agent-blueprints-token.controller.ts / minter service and the OpenAPI spec; the non-obvious parts:

Token grants (POST /agents/blueprints/:id/tokens, type discriminator):

user_delegated  verify user JWT with the emulator JWTManager → sid must be a live ws.sessions row
                → active membership in org_id (403 user_not_member_of_organization)
                → org invocable (403 organization_not_invocable) → role invocable (403 role_not_invocable)
                → user session created_at + max_age_seconds > now (400 max_age_exceeded)
                permissions = blueprint.permissions ∩ current role permissions (claims on the user token are not trusted)
autonomous      organization_id required + invocable; permissions = blueprint ceiling; instance reused per (blueprint, org)
agent_delegated presented token must be ai_agent on the *same* blueprint/instance (else invalid_agent_access_token);
                new session on the same instance, parent_session_id set; depth ≤ 32 (chain_depth_exceeded)
refresh         single-use: row.refresh_token is rotated via updateSilent (no session events); replay → invalid_refresh_token

Every session expires_at = min(now + refresh_token_ttl, root.created_at + max_age), so neither chaining nor refreshing can outlive the chain root's max-age window. Delegated permissions and act are recomputed from the member's current role on every mint/refresh; a delegated chain also fails with user_session_ended once the backing user session is gone, and revoking a user session (/user_management/sessions/revoke, logout, user delete) cascades to its agent sessions.

JWT: JWTManager.sign gains an optional typ header so agent tokens are typ: at+jwt (default stays JWT), plus sub_profile, act, intent claim types. Agent tokens are signed with the environment key so JWKS validation works. aud is the workos-emulate placeholder — production mints environment.clientId, but nothing at the API-key-authenticated token endpoint names a client; documented in README/SUPPORTED.

Session model: WorkOSAgentInstanceSession keeps refresh_token, parent_session_id, user_session_id, permissions, intent internally; status is derived (revoked / expired / active), never stored. Revoke cascades through descendants and is idempotent (200 with the existing revoked_at). Deleting an instance revokes-then-deletes its sessions; deleting a blueprint does the same for its instances, so agent.instance.session.revoked / agent.instance.deleted / agent.blueprint.deleted fire in that order.

Seed: agentBlueprints[] with invocable_by.organizations as org names (resolved to ids like feature-flag targets); validator checks referenced permissions/roles/orgs, duplicate names/ids and session-setting ranges (max_age ≤ 31536000, access_token_ttl ≤ 3600, refresh_token_ttl ≤ 5184000). Instances and sessions are never seeded.

Testing

bun run lint && bun run fmt:check && bun run typecheck && bun run build   # all pass
bun run gen:supported                                                     # 179/250 endpoints (71.6%)
bun run test                                                              # 1163 pass, 0 fail (68 files)

New: src/workos/routes/agents.spec.ts (36 tests: CRUD/pagination/validation, all four grants, chain depth, root max-age anchoring, refresh rotation + replay, validate, cascades, event payloads) and src/workos/seed-agent-blueprints.spec.ts (6).

Link to Devin session: https://app.devin.ai/sessions/598441040892444abdd5d09e78b2379c
Open in Devin Desktop: https://app.devin.ai/desktop/session/598441040892444abdd5d09e78b2379c?variant=devin
Requested by: @m0tzy

Implements the 13 Agent Auth endpoints (blueprint CRUD, all four token
grants plus validation, instance and session listing/lookup/deletion/
revocation), the seven agent.* lifecycle events, and an agentBlueprints
seed key. Agent Registration is intentionally left unimplemented.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

🤖 Devin AI Engineer

I'll be helping with this pull request! Here's what you should know:

✅ I will automatically:

  • Address comments on this PR. Add '(aside)' to your comment to have me ignore it.
  • Look at CI failures and help fix them

Note: I can only respond to comments from users who have write access to this repository.

⚙️ Control Options:

  • Disable automatic comment, CI, and merge conflict monitoring

Original prompt from madison.packer

What would it take to do this? https://work-os.slack.com/archives/C0BDDTXSBGC/p1788905811357599

@greptile-apps

greptile-apps Bot commented Sep 8, 2026

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

The PR appears safe to merge; the previous findings are resolved and no new actionable defects remain.

Summary

  • Implements blueprint CRUD and four token grant flows.
  • Adds agent instances, sessions, validation, revocation, and lifecycle cascades.
  • Extends JWT claims and headers for agent access tokens.
  • Adds seed configuration, validation, endpoint coverage documentation, and tests.
  • Changes since the previous review complete seed-validation parity with blueprint creation.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    BP[Agent Blueprint] --> M{Token grant}
    U[User access token] -->|user_delegated| M
    O[Organization] -->|autonomous| M
    AT[Agent access token] -->|agent_delegated| M
    RT[Refresh token] -->|refresh| M
    M --> I[Agent Instance]
    I --> S[Agent Session]
    S --> JWT[Agent access token]
    S --> RT
    S -->|parent_session_id| CS[Child Session]
    US[User Session] -->|backs delegated chain| S
    R[Revoke or delete] --> S
    R --> CS
Loading

greptile-apps[bot]

This comment was marked as resolved.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Devin Review found 5 potential issues.

Devin Review

Comment thread src/workos/routes/agents.ts
Comment thread src/workos/routes/agents.ts
Comment thread src/workos/routes/agents.ts
Comment thread src/workos/routes/agents.ts
Comment thread src/workos/agent-sessions.ts
Match production's AgentInstancesDeleter: deleting an organization or a
membership deletes the agent instances that reference it (firing their
deleted and session-revoked events), deactivating a membership revokes
its delegated sessions, and deleting a permission drops the slug from
every blueprint so no later mint can grant it. A chained session whose
parent row is missing now reads as revoked instead of posing as a root.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@m0tzy
m0tzy requested a review from gjtorikian September 9, 2026 00:15
@gjtorikian

Copy link
Copy Markdown
Collaborator

I found four API-parity cases worth addressing:

1. Accept user tokens with an explicit sub_profile: "user"

At src/workos/routes/agents.ts:503, the check rejects every explicit sub_profile, including "user"--but it looks like production accepts this?

Could we accept either an absent profile or USER_SUBJECT_PROFILE, while continuing to reject other token families, and add a regression test?

2. Cascade agent teardown when deleting a user

The new teardown call in src/workos/routes/memberships.ts:106–110 only covers the membership endpoint. DELETE /user_management/users/:id deletes memberships directly, bypassing it. The user-session hook revokes delegated agent sessions, but their rows and instances remain accessible, referencing a deleted membership, and no agent.instance.deleted event fires.

Could we apply deleteAgentInstancesForMembership during user deletion too, or move the cascade into the shared membership-deletion hook? An end-to-end user-delete test would cover this path.

3. Leave already-expired sessions unchanged during revocation

At src/workos/agent-sessions.ts:112–114, the walk checks revoked_at but not expiry. Revoking an expired session consequently changes its status to revoked and emits a revocation event. The revoke endpoint's OpenAPI v0.80.0 contract explicitly says an already-expired session should remain expired with revoked_at: null.

Could we update only unrevoked, unexpired rows, while still traversing their descendants? This should also apply to the user-session and membership revocation cascades.

4. Preserve the distinction between create and update validation

POST /agents/blueprints shares PATCH's validation, so both of these requests return 201 locally:

{ "name": "Example A", "description": null }
{ "name": "Example B", "session_settings": { "access_token_ttl_seconds": 60 } }

Production's create schema rejects both: null descriptions are update-only, and supplying session_settings requires all three settings. Could we add create-specific checks and regression tests while keeping PATCH's nullable description and partial settings?

- Accept user access tokens that carry an explicit `sub_profile: 'user'` for the user_delegated grant; other subject profiles are still rejected.
- Delete the instances delegated from a user's memberships when the user is deleted, the way membership and organization deletion already do.
- Leave already-expired sessions untouched in revoke cascades so they stay `expired` with a null `revoked_at`, while still walking their descendants.
- Reject `description: null` and partial `session_settings` on blueprint create; both remain valid on PATCH.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@gjtorikian re #104 (comment) — thanks, all four were right and each is fixed in 3973335 with a regression test:

  1. Explicit sub_profile: "user"user_delegated now accepts an absent profile or USER_SUBJECT_PROFILE and still rejects any other family (matches isUserAccessTokenPayload in api-services). Test: accepts a user token that names its subject profile explicitly (also checks a widget profile is still rejected).
  2. User deletionDELETE /user_management/users/:id now calls deleteAgentInstancesForMembership for each membership before deleting it, the same order production's UserlandUserOrganizationMembershipDeleter.deleteByUser uses so agent.instance.deleted / session-revoked events fire before the rows go. Test: deleting a user deletes the instances delegated from its memberships (an autonomous instance in the same org survives).
  3. Expired sessions in revoke cascadesrevokeAgentSessionTree only updates rows that are unrevoked and unexpired, but still walks descendants, matching AgentInstanceSessionRevocationService. An expired root answers 200 with status: expired, revoked_at: null and emits no event. Test: leaves an already-expired session expired while still revoking its live descendants.
  4. Create vs update validationvalidateBlueprintBody now takes a 'create' | 'update' mode: create rejects description: null and requires all three session_settings keys when the object is present (as CreateAgentBlueprintBody does), while PATCH keeps nullable description and partial settings. Test: rejects on create what only update may send: a null description and partial settings (covers both bad bodies plus the positive PATCH). Four existing tests that seeded blueprints with partial session_settings had their setup completed; their assertions are unchanged.

Note the agentBlueprints seed key still accepts partial session_settings (filled from defaults) since it's emulator config rather than the API surface — happy to tighten that too if you'd prefer parity there.

@gjtorikian

Copy link
Copy Markdown
Collaborator

tighten that too, of course

The agentBlueprints seed key now rejects a null description and requires all three session_settings values when the object is present, matching POST /agents/blueprints. Omitting session_settings still applies production's defaults.

Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
@devin-ai-integration

Copy link
Copy Markdown
Contributor

@gjtorikian done in c8e0dcd — the agentBlueprints seed key now validates like POST /agents/blueprints: session_settings, when present, needs all three values (omitting the object still applies the 3600/300/3600 defaults), and description: null is rejected (omit it instead). The WorkOSSeedAgentBlueprint type and the README section were updated to match, with the seed spec covering both rejections.

@gjtorikian
gjtorikian merged commit 9a2f973 into main Sep 9, 2026
9 checks passed
@gjtorikian
gjtorikian deleted the devin/1788906573-agent-blueprints branch September 9, 2026 15:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants