Workflow deploy: close the bearer-auth gap for agent-authored workflows - #258
Merged
Merged
Conversation
Covers the gap: a workflow-run principal holding workflow:*/create can deploy an asset in its own tenant; without the grant it is refused; an asset in another tenant reads as not-found; and the deploy call reaches the same probe/gate/freeze path a session-authenticated deploy does.
Every other workflow-run write surface (skills, capabilities, routines,
agent-directory) has its own bearer-authenticated route; workflow deploy
had none, so an agent could author a workflow asset but never deploy it
without a human browser session.
Adds workflow-run-deploy-auth, an optional middleware mounted ahead of
createResolveTenant (which already short-circuits once principal/tenant
are set, the same seam the git-token/asset bearer routes use). It
resolves a sidecar bearer token + run address to the caller's own
tenant/principal and hands off to the EXISTING POST/GET
.../workflows/deployments route unchanged: same requireGrant("workflow:*",
"create") gate (already seeded, so no grant changes needed), same asset
tenant-scoping, same install/probe/gate/freeze call into
sessionService.deployWorkflowFromSource. No deploy logic is duplicated.
A request with no bearer credential falls through unchanged to the
session-cookie path.
…elta Records the new middleware in VENDORED.md's hub-api row and refreshes the vendored tree's kill-date hash so check:killdates stays green.
2 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes the last gap in the author→deploy→redeploy loop for agent-authored
workflows (the sibling authoring lane,
cl-agent-authored-workflows,already lets an agent publish a
kind:"workflow"asset).POST/GET /api/tenants/:tenantId/workflows/deploymentswas mounted under thetenant-session pipeline only (browser cookie via
getSession) — every otherworkflow-run write surface (skills, capabilities, routines, agent-directory)
already has its own bearer-authenticated mirror route; deploy had none.
Seam found, no new vendoring required.
vendor/intx/hub-apiis alreadya vendored, locally-modified tree (see
VENDORED.md) — the file theprevious lane read as "off-limits" is actually ours to edit through normal
review; only the upstream repo is untouched. The app already has the exact
seam this needs:
createResolveTenantshort-circuits onceprincipal/tenantare set on the context, and the git-token/asset routesalready exploit that by mounting their own bearer middleware ahead of it.
This PR adds one more:
middleware/workflow-run-deploy-auth.ts, mounted(only when a
workflowRunAuthenticatoris supplied) ahead ofcreateResolveTenant, resolving a sidecar bearer token + run address to thecaller's own tenant/principal.
The existing deploy route (
routes/workflows.ts) is untouched. Abearer-authenticated request now reaches the exact same handler a human
session does — same
requireGrant("workflow:*", "create")gate, same assettenant-scoping, same install/probe/gate/freeze call into
sessionService.deployWorkflowFromSource. No deploy logic is duplicated orweakened.
Grant
Gates on the existing
workflow:*/creategrant — already inSEED_GRANTS, already what the session route checks. No new grant wasneeded to land this. (See note below on tightening this.)
Security
unconditionally; the
:tenantIdpath segment is never trusted, so acaller cannot widen its own scope by putting a different tenant in the
URL.
already scopes by
tenant.idand returnsnot_found(never a confirming403) for an asset owned by another tenant. Covered by a new test.
session-cookie path — this is additive, not a replacement.
Tests
vendor/intx/hub-api/src/middleware/workflow-run-deploy-auth.test.ts(DB-gated, real Postgres, same convention as
packages/approvals/test/needs-you.test.ts):workflow:*/createdeploys its owntenant's asset (201, and the fake
sessionService.deployWorkflowFromSourceis called exactly once, scoped to the caller's own tenant)
not_found(404), never a confirming 403resolution
Vendoring
No new vendored path —
vendor/intx/hub-apialready had a ledger row andkill date (2026-09-19). Updated its
VENDORED.mddelta description andrefreshed its
check:killdatestree hash inscripts/checks/kill-dates.txtin the same change (verifiedbun run scripts/checks/killdates.tspasses).What's left
The loop closes: author (sibling PR) → deploy (this PR, once both land)
→ redeploy (republish + re-deploy, same two routes) all now have a
bearer path.
On grant granularity: reusing the existing tenant-wide
workflow:*/creategrant was the fastest safe path to land in the window — it'salready seeded, already what the session route checks, so this is at
least as strict as the human path today. A tighter, per-asset grant
(mirroring how the authoring half scopes republish to
asset:<id>/write)is a real improvement worth a fast follow-up: check a per-asset grant
before
requireGrant("workflow:*", "create")runs, using the body'ssource.assetIdrather than a URL param. Flagging rather than rushing itinto this window.