Agent identity: session tokens, Bearer-only API, honest history attribution - #178
Agent identity: session tokens, Bearer-only API, honest history attribution#178HamptonMakes wants to merge 2 commits into
Conversation
…bution Agent edits in production were showing up in the history tab as edits the user made themselves. Root cause: hook-authenticated API calls (the production proxy authenticates curl automatically) derived actor_type from the auth mechanism, so an agent arriving on the user's credentials wrote actor_type "human" — and the history partials hid actor_type whenever actor_id resolved to a user anyway. Three changes, one story — every API caller now has a real identity: - Session tokens: each agent run mints its own short-lived token (POST /api/v1/tokens) carrying an agent_name, either as a child of a long-lived settings-UI root token (revoking the root cascades) or bootstrapped straight from the host's request auth. DELETE /api/v1/tokens/current lets a run revoke itself on exit. - Bearer-only API: every other API call now requires the token. Hook auth proves which human is behind the wire, not which agent is acting, so it is good for exactly one thing — minting. - Attribution matching comments: versions and events store the human in actor_id (never the token id) plus the acting agent in a new agent_name column, and the history tab renders "Claude (via Hampton)" the way comment authors already do. Comments themselves now fall back to the token's agent_name instead of rejecting agent posts that omitted the param. The api_tokens migration is guarded with column_exists? because main's db/schema.rb has carried these columns (and the agent-collaboration tables) without backing migrations since the schema regeneration in #175 — schema-loaded databases already have them, production's migrated database does not. Existing production rows written as "human" cannot be re-attributed; the fix is forward-looking. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 91bcb32255
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| def api_user_id | ||
| current_user&.id |
There was a problem hiding this comment.
Propagate agent identity into the initial plan version
When a Bearer-authenticated agent calls POST /api/v1/plans, PlansController#create still invokes Plans::Create with only user, and that service hardcodes version 1 as actor_type: "human" without an agent_name. Consequently, every agent-created plan permanently attributes its initial content to the human, contradicting the new identity model and the documented promise that every version is stamped; pass api_author_type, api_user_id, and api_agent_name through the creation path as well.
AGENTS.md reference: AGENTS.md:L5-L5
Useful? React with 👍 / 👎.
Identity facts beyond a display name — harness, harness version, model —
now travel with the token, not the attribution rows. Minting accepts a
schemaless `metadata` JSON object (capped at 4 KB, non-hash input coerced
to {} rather than failing the mint), and every version, event, and
comment an agent writes records `api_token_id` as a provenance join back
to the token that produced it. actor/author stays the human; agent_name
stays the denormalized display string the history UI already renders.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The bug
Agent edits made in production showed on the history tab as edits the human made themselves. Two compounding causes:
curlautomatically (the API config even says so), so a local agent's requests arrive on the user's credentials with no Bearer token.api_author_typederived actor type from the auth mechanism (token →local_agent, hook →human), so every agent edit was persisted asactor_type: "human"with the user's id.actor_idresolved to a user, and only showed the actor-type badge in the else branch. Even a correctly-typed agent version rendered as the plain human.The fix: every API caller has a real identity
Session tokens (
POST /api/v1/tokens): each agent run mints its own short-lived token (default 12h, cap 7d) carrying anagent_name— either as a child of a long-lived root token from the settings UI (revoking the root revokes everything it minted; children can't mint, so the tree stays one level deep), or bootstrapped directly from the host's request auth for zero-setup production use.DELETE /api/v1/tokens/currentlets a run revoke itself on exit instead of leaving a live credential until the TTL.Bearer-only API: every other
/api/v1call now requires the token, with a 403 that tells the caller exactly how to mint. Hook auth proves which human is behind the wire, never which agent is acting — so it's accepted for exactly one thing: minting.Attribution matching comments:
coplan_plan_versionsandcoplan_plan_eventsgainagent_name; API writes store the human inactor_id(never the token id — a token id names nobody in a history tab) and the acting agent inagent_name, resolved per-request → token'sagent_name→ token's name. The history tab renders "Claude (via Hampton)", exactly the convention comment authors already use. The expired-session auto-commit job resolves identity the same way. API comments now fall back to the token's agent name instead of 422ing whenagent_nameis omitted.Token metadata + provenance join (second commit): identity is more than a display name — harness, harness version, model. Rather than growing columns on three tables, minting accepts a schemaless
metadataJSON object ({"harness": "claude-code", "harness_version": "2.1.3", "model": "claude-fable-5"}— keys are convention, like a User-Agent; 4 KB cap; non-hash input coerced to{}so a malformed field never costs an agent its identity), and every version, event, and comment recordsapi_token_id(indexed, FK'd) pointing back at the token that wrote it. The history UI stays one string; anything richer joins through the token. No UI surfacing yet — that's deliberate; a tooltip/detail view can come later without touching the data model.The agent instructions document the mint-first flow (they're re-read every session, so agents adopt it automatically), including the
metadataparam.Notes
column_exists?: main'sdb/schema.rbhas carried these columns — plus the unreleased agent-collaboration tables — without backing migrations since the schema regeneration in Voice commenting: push-to-talk dictation pinned to what you were reading #175. Schema-loaded databases already have the columns; production's migrated database doesn't. The straycoplan_agent_sessions/coplan_agent_eventstables in schema.rb are left for the agent-collaboration PR that owns them."human"can't be re-attributed — nothing distinguishes them. The fix is forward-looking.agent_auth_instructions("Authentication is automatic with curl") should be reworded to describe the mint-first flow — I'll include that in the bump PR.Testing
{}, over-budget 422,api_token_idrecorded on versions and comments. Models + services + API + jobs: 937 examples, 0 failures.🤖 Generated with Claude Code