Skip to content

fix(workflows): give a workflow-dispatched agent the same run identity a run has - #891

Open
arantespp wants to merge 1 commit into
mainfrom
claude/next-steps-fq672a
Open

fix(workflows): give a workflow-dispatched agent the same run identity a run has#891
arantespp wants to merge 1 commit into
mainfrom
claude/next-steps-fq672a

Conversation

@arantespp

Copy link
Copy Markdown
Member

Closes #884. First item of #889.

Problem

runDispatch called createGeneration with no authHeader, so a workflow state with dispatch.kind: "agent" whose agent holds a soat tool had no credential for its platform self-calls — the call reached the loopback unauthenticated and came back 401. #879 fixed exactly this for the orchestration branch of the same dispatch and left the agent branch out.

What the red test showed

The failing assertion, before the fix:

Expected substring: "soat-agent-list-tool"
Received string:    "\"HttpToolError: SOAT action 'list-tools' failed\""

Worth knowing, because it changes how this reads in production: the AI SDK converts the thrown tool error into a tool-error part fed back to the model, so the dispatch still reported completed while the agent had actually failed. The task looked fine; only the answer was wrong. The tests therefore assert on the tool result the model was handed, which is the one place that is visible.

Fix

The agent branch re-mints the same run-as token from the dispatch principal, keyed to the task. taskPublicId is threaded through tasksAutomationRetry.ts, which already had it.

The token still asserts identity only — authorization is re-evaluated per call against current policies — so a chain started by a scoped API key stays bounded by that key's own policies rather than widening to its owning user. A trigger- or OAuth-started chain records no principal and behaves exactly as before, self-calls included.

signRunToken / buildRunAuthHeader now take workPublicId instead of runPublicId. The orn claim's presence is what marks a run-as token (auth.ts, authScopedBoundary.ts); its value is never an authorization input, so it now names whichever durable work the token acts for — an orchestration run, or the task whose automation dispatched an agent.

Tests

Both in rest/soatSelfCall.test.ts, which already binds a real HTTP listener for the loopback path. A local HTTP stub stands in for the AI provider and forces the soat tool call, so ai.generateText drives a real tool call over real HTTP — nothing internal is mocked.

  • a workflow agent dispatch whose soat tool must return the real listing;
  • the escalation mirror of the run test above it: a read-only key's read reaches 200, its write is refused 403, and no row is created.

Verified the escalation test is load-bearing by mutating the mint to drop apiKeyPublicId — the existing orchestration escalation test and the new agent one go red together.

Checks

  • pnpm typecheck, pnpm eslint --fix — clean; no as any / as unknown added.
  • pnpm test — 4946 passed, 0 failed.
  • pnpm run docs-lint — OK.
  • Smoke tests not run: no Docker in this environment. No smoke steps added either, for the reason [feature] Smoke coverage for an orchestration transitioning its own task via a soat tool (tests) #886 already records — an unexecuted end-to-end assertion is worse than none. The unit tests here exercise the real loopback and a real tool call, which is the closest verifiable equivalent.

No OpenAPI, SDK, CLI, permission, or formation surface changed — the fix is internal.

Docs

  • workflows.md — the return-edge paragraph now covers both dispatch kinds.
  • orchestrations.md §Run identity — a workflow-dispatched agent re-mints the same token, under the same four rules.

Open questions, resolved

Q: Reuse the orchestration run-as token for an agent dispatch, or mint a third token kind?
A: Reuse — resolved by pareto; checked: the `orn` claim is only a marker
   (`auth.ts:249`, `authScopedBoundary.ts:54`) and its value is never an
   authorization input, so keying it to the task adds no middleware path and no
   second boundary rule to keep in sync. Renamed the parameter to `workPublicId`
   rather than pass a task id under a name that says "run".

Q: Should the generation persist the principal for its own async continuations?
A: Not here — resolved by long-term; checked: `submitToolOutputs`
   (`agentGeneration.ts`) already takes an `authHeader` from its REST caller, so
   `requires_action` is not request-less. See the follow-up below for the one
   that is.

Follow-up this surfaced

fireContinuation (agentToolApprovalContinuation.ts:141) calls createGeneration with no authHeader. An approved tool call's continuation generation is genuinely request-less — the same shape as this bug, one layer down — so an agent resuming after approval has no credential for its own soat tools.

Not fixed here on purpose: closing it needs a durable principal on Generation (an approval can resolve days later, from a different request), and that is the same surface #887 changes when it decides API-key-vs-user attribution. Writing those semantics now risks a migration. Happy to file it as a sub-issue of #889.


Generated by Claude Code

…y a run has

A workflow state with `dispatch.kind: "agent"` called `createGeneration` with no
`authHeader`, so an agent holding a `soat` tool had no credential for its
platform self-calls: the call reached the loopback unauthenticated and came back
401. #879 fixed exactly this for the orchestration branch of the same dispatch
and left the agent branch out.

The agent branch now re-mints the same run-as token from the dispatch principal,
keyed to the task. The token still asserts identity only — authorization is
re-evaluated per call — so a chain started by a scoped API key stays bounded by
that key's own policies rather than widening to its owning user.

`signRunToken`/`buildRunAuthHeader` take `workPublicId` instead of
`runPublicId`: the `orn` claim's *presence* is what marks a run-as token
(`auth.ts`, `authScopedBoundary.ts`), and its value is a provenance breadcrumb
that is never an authorization input, so it now names whichever durable work the
token acts for — a run, or the task whose automation dispatched an agent.

Worth knowing about the old behaviour: the AI SDK turns the thrown tool error
into a `tool-error` part fed back to the model, so the dispatch still reported
`completed` while the agent had actually failed. The red test asserts on the
tool result the model was handed, which is where that is visible.

Tests: a workflow agent dispatch whose `soat` tool must return the real listing,
and the escalation mirror of `rest/soatSelfCall.test.ts` — a read-only key's
read reaches 200, its write is refused 403, and no row is created.

Refs #884
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Deploy Outputs

Package Stack Output Key Output Value
@soat/website SoatWebsite-claude-next-steps-fq672a BucketWebsiteURL http://soatwebsite-claude-next-steps-fq672a-staticbucket-utnayvpmt6lx.s3-website-us-east-1.amazonaws.com

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.

[bug] Workflow-dispatched agent generations carry no principal, so their soat tools cannot authenticate (workflows)

2 participants