Skip to content

Workflow deploy: close the bearer-auth gap for agent-authored workflows - #258

Merged
TheGreatAxios merged 3 commits into
mainfrom
cl-workflow-deploy-bearer
Aug 21, 2026
Merged

TheGreatAxios merged 3 commits into
mainfrom
cl-workflow-deploy-bearer

Conversation

@TheGreatAxios

Copy link
Copy Markdown
Contributor

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/deployments was mounted under the
tenant-session pipeline only (browser cookie via getSession) — every other
workflow-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-api is already
a vendored, locally-modified tree (see VENDORED.md) — the file the
previous 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: createResolveTenant short-circuits once
principal/tenant are set on the context, and the git-token/asset routes
already 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 workflowRunAuthenticator is supplied) ahead of
createResolveTenant, resolving a sidecar bearer token + run address to the
caller's own tenant/principal.

The existing deploy route (routes/workflows.ts) is untouched. A
bearer-authenticated request now reaches the exact same handler a human
session does — same requireGrant("workflow:*", "create") gate, same asset
tenant-scoping, same install/probe/gate/freeze call into
sessionService.deployWorkflowFromSource. No deploy logic is duplicated or
weakened.

Grant

Gates on the existing workflow:*/create grant — already in
SEED_GRANTS, already what the session route checks. No new grant was
needed to land this. (See note below on tightening this.)

Security

  • Tenant scoping: the resolved run's own tenant/principal are set
    unconditionally; the :tenantId path segment is never trusted, so a
    caller cannot widen its own scope by putting a different tenant in the
    URL.
  • Foreign-tenant asset: unchanged behavior — the existing asset lookup
    already scopes by tenant.id and returns not_found (never a confirming
    403) for an asset owned by another tenant. Covered by a new test.
  • A request with no bearer credential falls through unchanged to the
    session-cookie path — this is additive, not a replacement.
  • A present-but-invalid bearer credential fails closed with 401.

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):

  • a workflow-run principal holding workflow:*/create deploys its own
    tenant's asset (201, and the fake sessionService.deployWorkflowFromSource
    is called exactly once, scoped to the caller's own tenant)
  • without the grant: refused before the deploy call (403)
  • an asset in another tenant: not_found (404), never a confirming 403
  • an unrecognized bearer token: refused (401) before any tenant/grant
    resolution

Vendoring

No new vendored path — vendor/intx/hub-api already had a ledger row and
kill date (2026-09-19). Updated its VENDORED.md delta description and
refreshed its check:killdates tree hash in
scripts/checks/kill-dates.txt in the same change (verified bun run scripts/checks/killdates.ts passes).

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:*/
create grant was the fastest safe path to land in the window — it's
already 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's
source.assetId rather than a URL param. Flagging rather than rushing it
into this window.

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.
@TheGreatAxios
TheGreatAxios merged commit fdc4658 into main Aug 21, 2026
5 checks passed
@TheGreatAxios
TheGreatAxios deleted the cl-workflow-deploy-bearer branch August 25, 2026 15:29
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