From fc32a4179caa6071e6dde0de672a21f63f7c492d Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 20 Sep 2026 23:16:03 -0700 Subject: [PATCH 1/6] fix(cli): use workspace auth for provider subscription lifecycle Session-Id: 01a0c27d-bf21-78d0-a18b-062250fa617a Session-Id: 01a0cac4-0bbe-7a11-bb46-2500bfdac947 --- CHANGELOG.md | 2 ++ docs/evidence/subscription-wake-provider.json | 11 ++++++ docs/evidence/subscription-workspace-auth.md | 36 +++++++++++++++++++ .../commands/integration-subscribe.test.ts | 27 ++++++++++++-- packages/cli/src/cli/commands/integration.ts | 24 ++++++++++--- .../src/cli/commands/relaycast-groups.test.ts | 10 +++--- 6 files changed, 99 insertions(+), 11 deletions(-) create mode 100644 docs/evidence/subscription-wake-provider.json create mode 100644 docs/evidence/subscription-workspace-auth.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f8707793..0b4a70b9a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Integration subscription setup, listing and retirement use workspace authentication even when a spawned worker also has an agent token, preventing misleading “Workspace key required” failures. + - Broker `manual_flush` recovery now replays a missing cumulative-ACK predecessor without duplicating an already-completed PTY injection, restores it ahead of parked successors, and reports the head/ACK/received sequence gap plus the reconciliation action in `message flush` and `message auto` results. ## [12.4.1] - 2026-09-22 diff --git a/docs/evidence/subscription-wake-provider.json b/docs/evidence/subscription-wake-provider.json new file mode 100644 index 000000000..8df230972 --- /dev/null +++ b/docs/evidence/subscription-wake-provider.json @@ -0,0 +1,11 @@ +{ + "id": "3843931114454384640", + "guid": "89e65110-b580-11f1-8407-405863f40b35", + "delivered_at": "2026-09-21T05:51:51.082Z", + "event": "issue_comment", + "action": "created", + "status_code": 200, + "duration": 0.32, + "comment_id": "5755994597", + "repository": "AgentWorkforce/cloud" +} diff --git a/docs/evidence/subscription-workspace-auth.md b/docs/evidence/subscription-workspace-auth.md new file mode 100644 index 000000000..eeba0c8c4 --- /dev/null +++ b/docs/evidence/subscription-workspace-auth.md @@ -0,0 +1,36 @@ +# Provider subscription workspace authentication and wake proof + +The resumed worker had real RELAY_WORKSPACE_KEY and RELAY_AGENT_TOKEN values. +A temporary fetch diagnostic recorded GET https://cast.agentrelay.com/v1/agents +returning 401, followed by "Workspace key required (rk_live_...)". Removing only +RELAY_AGENT_TOKEN made the same request return 200. This was client credential +selection, not missing worker environment, a masked key, or node configuration. +No credential values were logged. Temporary diagnostics were removed. + +Integration subscribe/list/unsubscribe (including owned-binding retirement) +require workspace-owner endpoints. They now use createWorkspaceRelay with the +same selected workspace as inbound-target and subscription-channel provisioning. +Explicit --token is rejected with guidance. General agent messaging and other +agent-scoped operations retain createAgentRelay and its ambient-token rules. + +Validation: 154 focused CLI/auth tests and CLI typecheck pass. Regression tests +fail before the fix for ambient agent-token setup and explicit-token rejection. +Running the patched source CLI with the original workspace key AND agent token +successfully subscribed webhook-subscription-closeout-r2 to +/github/repos/AgentWorkforce/relayfile/pulls/515/**. The local Relayfile client +used a 120s request budget for this live proof; the deployed 30s default can +still time out on overloaded control-plane operations (separate from auth). + +The earlier same-repository Cloud #3896 probe provides the full provider lane: +GitHub comment 5755994597 -> delivery GUID 89e65110-b580-11f1-8407-405863f40b35 +-> Relayfile evt_4923952/rev_5368829 -> Relay message 227668215640440832 +-> this live agent's next input turn. GitHub delivered at 05:51:51.082Z with +HTTP 200; Relay received at 05:52:12Z (20.918s). Reader confirmation identifies +this owner at 05:56:16Z. The labeled temporary comment was deleted and its +GitHub API returned 404. The provider-side IDs are stored as strings in +subscription-wake-provider.json to preserve integer precision. + +This proves issue_comment.created provider subscription delivery and agent +input injection. It does not prove production check_run.completed, fork checks, +or the separate hosted Babysitter flow-listener lane. No merge or deployment +was performed. diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index efeedc6c5..a571d66c8 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -192,6 +192,8 @@ function harness( ) ) ); + const createWorkspaceRelay = vi.fn(() => relay as never); + const createAgentRelay = vi.fn(() => relay as never); const log = vi.fn(); const error = vi.fn(); const exit = vi.fn(); @@ -199,7 +201,8 @@ function harness( program.exitOverride(); registerIntegrationCommands(program, { ...opts.recipientDeps, - createAgentRelay: () => relay as never, + createAgentRelay, + createWorkspaceRelay, relayfile: relayfile as never, cleanupJournal: journal, resolveLocalRelayOptions: @@ -209,7 +212,7 @@ function harness( error, exit: exit as never, } satisfies Partial); - return { program, relay, relayfile, journal, log, error, exit }; + return { program, relay, relayfile, journal, log, error, exit, createWorkspaceRelay, createAgentRelay }; } const RESOURCE = '/slack/channels/C0/**'; @@ -251,6 +254,26 @@ describe('integration subscribe', () => { }); }); + it('uses workspace auth for subscription management even in an agent-token worker', async () => { + vi.stubEnv('RELAY_WORKSPACE_KEY', 'rk_live_worker_workspace'); + vi.stubEnv('RELAY_AGENT_TOKEN', 'at_worker_token'); + const { program, createWorkspaceRelay, createAgentRelay, error } = harness({ + resolveLocalRelayOptions: async () => undefined, + }); + await program.parseAsync(ARGS(), { from: 'user' }); + expect(error).not.toHaveBeenCalled(); + expect(createWorkspaceRelay).toHaveBeenCalledTimes(1); + expect(createAgentRelay).not.toHaveBeenCalled(); + }); + + it('rejects an explicit agent token for workspace-only subscription operations', async () => { + const { program, relay, error, createWorkspaceRelay } = harness(); + await program.parseAsync(ARGS(['--token', 'at_explicit']), { from: 'user' }); + expect(error).toHaveBeenCalledWith(expect.stringContaining('requires a workspace key')); + expect(createWorkspaceRelay).not.toHaveBeenCalled(); + expect(relay.webhooks.createInbound).not.toHaveBeenCalled(); + }); + it('resolves provider-native resources before binding and replacement lookup', async () => { const resolved = '/slack/channels/C123__watchdog-test/**'; const relayfile = createRelayfileMock([], { diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index 4773b4628..5e262a4fd 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -1528,6 +1528,21 @@ async function runSubscribe( } } +/** Subscription provisioning and retirement are workspace-owner operations, like + * their inbound-target and subscription-channel HTTP calls. Do not let the + * worker's ambient participant token override the selected workspace here. */ +function createSubscriptionRelay( + deps: IntegrationCommandDependencies, + options: SdkClientOptions +): AgentRelayAgent { + if (options.token?.trim()) { + throw new Error( + 'Integration subscription management requires a workspace key; use --workspace-key instead of --token.' + ); + } + return deps.createWorkspaceRelay(options); +} + async function runSubscribeSetup( deps: IntegrationCommandDependencies, providerArg: string | undefined, @@ -1539,7 +1554,8 @@ async function runSubscribeSetup( if (opts.list) { const local = await deps.resolveLocalRelayOptions(); const relayOptions = sdkOptionsFromOpts(opts); - const relay = deps.createAgentRelay( + const relay = createSubscriptionRelay( + deps, local && !explicitWorkspaceKey(opts) ? localRetryOptions(relayOptions, local) : relayOptions ); const [bindings, webhooks, subscriptions] = await Promise.all([ @@ -1572,7 +1588,7 @@ async function runSubscribeSetup( const relayOptions = sdkOptionsFromOpts(opts); const effectiveRelayOptions = local && !explicitWorkspaceKey(opts) ? localRetryOptions(relayOptions, local) : relayOptions; - const relay = deps.createAgentRelay(effectiveRelayOptions); + const relay = createSubscriptionRelay(deps, effectiveRelayOptions); const recipientName = agentName(to); if (opts.spawn && !recipientName) throw new Error('--spawn requires an explicit @agent recipient'); if (recipientName && typeof opts.spawn === 'string') { @@ -1866,7 +1882,7 @@ async function runUnsubscribeOwnedBy( const relayOptions = sdkOptionsFromOpts(opts); const effectiveRelayOptions = local && !explicitWorkspaceKey(opts) ? localRetryOptions(relayOptions, local) : relayOptions; - const relay = deps.createAgentRelay(effectiveRelayOptions); + const relay = createSubscriptionRelay(deps, effectiveRelayOptions); const agents = await relay.agents.list(); const agent = agents.find((item) => item.name === owner || `@${item.name}` === owner); if (!agent) { @@ -1926,7 +1942,7 @@ async function runUnsubscribe( const relayOptions = sdkOptionsFromOpts(opts); const effectiveRelayOptions = local && !explicitWorkspaceKey(opts) ? localRetryOptions(relayOptions, local) : relayOptions; - const relay = deps.createAgentRelay(effectiveRelayOptions); + const relay = createSubscriptionRelay(deps, effectiveRelayOptions); const relayScope = relayCleanupScope(effectiveRelayOptions); const relayfileScope = relayfileCleanupScope(); diff --git a/packages/cli/src/cli/commands/relaycast-groups.test.ts b/packages/cli/src/cli/commands/relaycast-groups.test.ts index 71822b42b..633bcb3c1 100644 --- a/packages/cli/src/cli/commands/relaycast-groups.test.ts +++ b/packages/cli/src/cli/commands/relaycast-groups.test.ts @@ -529,7 +529,7 @@ describe('SDK-backed CLI groups', () => { const program = new Command(); program.exitOverride(); registerIntegrationCommands(program, { - createAgentRelay: () => relay as never, + createWorkspaceRelay: () => relay as never, log, error, exit: exit as never, @@ -617,7 +617,7 @@ describe('SDK-backed CLI groups', () => { const program = new Command(); program.exitOverride(); registerIntegrationCommands(program, { - createAgentRelay: () => relay as never, + createWorkspaceRelay: () => relay as never, log: vi.fn(), error: vi.fn(), exit: vi.fn() as never, @@ -676,7 +676,7 @@ describe('SDK-backed CLI groups', () => { const program = new Command(); program.exitOverride(); registerIntegrationCommands(program, { - createAgentRelay: () => relay as never, + createWorkspaceRelay: () => relay as never, log, error, exit: exit as never, @@ -728,7 +728,7 @@ describe('SDK-backed CLI groups', () => { const program = new Command(); program.exitOverride(); registerIntegrationCommands(program, { - createAgentRelay: () => relay as never, + createWorkspaceRelay: () => relay as never, log, error, exit: exit as never, @@ -793,7 +793,7 @@ describe('SDK-backed CLI groups', () => { const program = new Command(); program.exitOverride(); registerIntegrationCommands(program, { - createAgentRelay: () => relay as never, + createWorkspaceRelay: () => relay as never, log, error, exit: exit as never, From 59c40397b72a9ded33063efe84bf65ce17592bfd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 21 Sep 2026 06:18:09 +0000 Subject: [PATCH 2/6] style: auto-format with Prettier Session-Id: 01a0cac4-0bbe-7a11-bb46-2500bfdac947 --- docs/evidence/subscription-workspace-auth.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/evidence/subscription-workspace-auth.md b/docs/evidence/subscription-workspace-auth.md index eeba0c8c4..eb7309932 100644 --- a/docs/evidence/subscription-workspace-auth.md +++ b/docs/evidence/subscription-workspace-auth.md @@ -1,8 +1,8 @@ # Provider subscription workspace authentication and wake proof -The resumed worker had real RELAY_WORKSPACE_KEY and RELAY_AGENT_TOKEN values. +The resumed worker had real RELAY*WORKSPACE_KEY and RELAY_AGENT_TOKEN values. A temporary fetch diagnostic recorded GET https://cast.agentrelay.com/v1/agents -returning 401, followed by "Workspace key required (rk_live_...)". Removing only +returning 401, followed by "Workspace key required (rk_live*...)". Removing only RELAY_AGENT_TOKEN made the same request return 200. This was client credential selection, not missing worker environment, a masked key, or node configuration. No credential values were logged. Temporary diagnostics were removed. @@ -17,7 +17,7 @@ Validation: 154 focused CLI/auth tests and CLI typecheck pass. Regression tests fail before the fix for ambient agent-token setup and explicit-token rejection. Running the patched source CLI with the original workspace key AND agent token successfully subscribed webhook-subscription-closeout-r2 to -/github/repos/AgentWorkforce/relayfile/pulls/515/**. The local Relayfile client +/github/repos/AgentWorkforce/relayfile/pulls/515/\*\*. The local Relayfile client used a 120s request budget for this live proof; the deployed 30s default can still time out on overloaded control-plane operations (separate from auth). From 5ca0ccfa064c5849fe8d913a77b7f73452985126 Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 20 Sep 2026 23:19:21 -0700 Subject: [PATCH 3/6] docs: record exact live PR owner subscription bindings Session-Id: 01a0c27d-bf21-78d0-a18b-062250fa617a Session-Id: 01a0c27d-bf21-78d0-a18b-062250fa617a Session-Id: 01a0cac4-0bbe-7a11-bb46-2500bfdac947 --- docs/evidence/subscription-live-bindings.json | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 docs/evidence/subscription-live-bindings.json diff --git a/docs/evidence/subscription-live-bindings.json b/docs/evidence/subscription-live-bindings.json new file mode 100644 index 000000000..76bab454d --- /dev/null +++ b/docs/evidence/subscription-live-bindings.json @@ -0,0 +1,38 @@ +[ + { + "provider": "github", + "resource": "/github/repos/AgentWorkforce/cloud/pulls/3896/**", + "channel": "agent-events-227666016525594624", + "webhookId": "wh_227668032397963264", + "subscriptionId": "sub_227668035883429888", + "webhookSubscriptionId": "whsub_9171d549-7f7a-4ae5-a340-bc1c01dba11b", + "webhookSubscriptionWorkspaceId": "rw_7ccfea89" + }, + { + "provider": "github", + "resource": "/github/repos/AgentWorkforce/relay/pulls/1834/**", + "channel": "agent-events-227666016525594624", + "webhookId": "wh_227674523242795008", + "subscriptionId": "sub_227674526870867968", + "webhookSubscriptionId": "whsub_74bb3bec-bf6e-4953-86af-4c3e1ef7d0b8", + "webhookSubscriptionWorkspaceId": "rw_7ccfea89" + }, + { + "provider": "github", + "resource": "/github/repos/AgentWorkforce/relayfile-cloud/pulls/238/**", + "channel": "agent-events-227666016525594624", + "webhookId": "wh_227669931394633728", + "subscriptionId": "sub_227669934972375040", + "webhookSubscriptionId": "whsub_d1a74cda-3a57-45cf-882d-70e24b4d9fb0", + "webhookSubscriptionWorkspaceId": "rw_7ccfea89" + }, + { + "provider": "github", + "resource": "/github/repos/AgentWorkforce/relayfile/pulls/515/**", + "channel": "agent-events-227666016525594624", + "webhookId": "wh_227673937327886336", + "subscriptionId": "sub_227673941069205504", + "webhookSubscriptionId": "whsub_45079f6f-a330-46d9-b9b7-b167834cf253", + "webhookSubscriptionWorkspaceId": "rw_7ccfea89" + } +] From 813d489d0d0ac8ddaac120145eb013c38f7288ac Mon Sep 17 00:00:00 2001 From: khaliqgant Date: Sun, 20 Sep 2026 23:21:14 -0700 Subject: [PATCH 4/6] fix(cli): reject subscription token options before provider work Session-Id: 01a0c27d-bf21-78d0-a18b-062250fa617a Session-Id: 01a0cac4-0bbe-7a11-bb46-2500bfdac947 --- .../cli/commands/integration-subscribe.test.ts | 15 +++++++++++++++ packages/cli/src/cli/commands/integration.ts | 15 +++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/cli/commands/integration-subscribe.test.ts b/packages/cli/src/cli/commands/integration-subscribe.test.ts index a571d66c8..b28d090a5 100644 --- a/packages/cli/src/cli/commands/integration-subscribe.test.ts +++ b/packages/cli/src/cli/commands/integration-subscribe.test.ts @@ -274,6 +274,21 @@ describe('integration subscribe', () => { expect(relay.webhooks.createInbound).not.toHaveBeenCalled(); }); + it.each([ + ['subscribe', 'slack', '--resource', RESOURCE, '--to', '#general'], + ['subscribe', '--list'], + ['unsubscribe', 'slack', '--resource', RESOURCE], + ['unsubscribe', 'slack', '--owned-by', '@lead'], + ])('rejects explicit token before provider/control-plane work: %j', async (...args) => { + const relayfile = createRelayfileMock([], { isConnected: vi.fn(async () => false) }); + const { program, error } = harness({ relayfile }); + await program.parseAsync(['integration', ...args, '--token', 'at_explicit'], { from: 'user' }); + expect(error).toHaveBeenCalledWith(expect.stringContaining('requires a workspace key')); + expect(relayfile.ensureCompatible).not.toHaveBeenCalled(); + expect(relayfile.isConnected).not.toHaveBeenCalled(); + expect(relayfile.connect).not.toHaveBeenCalled(); + }); + it('resolves provider-native resources before binding and replacement lookup', async () => { const resolved = '/slack/channels/C123__watchdog-test/**'; const relayfile = createRelayfileMock([], { diff --git a/packages/cli/src/cli/commands/integration.ts b/packages/cli/src/cli/commands/integration.ts index 5e262a4fd..28a5c485f 100644 --- a/packages/cli/src/cli/commands/integration.ts +++ b/packages/cli/src/cli/commands/integration.ts @@ -1531,15 +1531,19 @@ async function runSubscribe( /** Subscription provisioning and retirement are workspace-owner operations, like * their inbound-target and subscription-channel HTTP calls. Do not let the * worker's ambient participant token override the selected workspace here. */ -function createSubscriptionRelay( - deps: IntegrationCommandDependencies, - options: SdkClientOptions -): AgentRelayAgent { +function validateSubscriptionCredentials(options: SdkClientOptions): void { if (options.token?.trim()) { throw new Error( 'Integration subscription management requires a workspace key; use --workspace-key instead of --token.' ); } +} + +function createSubscriptionRelay( + deps: IntegrationCommandDependencies, + options: SdkClientOptions +): AgentRelayAgent { + validateSubscriptionCredentials(options); return deps.createWorkspaceRelay(options); } @@ -1549,6 +1553,7 @@ async function runSubscribeSetup( opts: Record, recipient: { launch?: RecipientLaunch; committed?: boolean } ): Promise { + validateSubscriptionCredentials(sdkOptionsFromOpts(opts)); await deps.relayfile.ensureCompatible(); if (opts.list) { @@ -1877,6 +1882,7 @@ async function runUnsubscribeOwnedBy( owner: string, opts: Record ): Promise { + validateSubscriptionCredentials(sdkOptionsFromOpts(opts)); await deps.relayfile.ensureCompatible(); const local = await deps.resolveLocalRelayOptions(); const relayOptions = sdkOptionsFromOpts(opts); @@ -1923,6 +1929,7 @@ async function runUnsubscribe( provider: string, opts: Record ): Promise { + validateSubscriptionCredentials(sdkOptionsFromOpts(opts)); const ownedBy = typeof opts.ownedBy === 'string' ? opts.ownedBy.trim().replace(/^@/, '') : ''; if (ownedBy) { await runUnsubscribeOwnedBy(deps, provider, ownedBy, opts); From a98704aaf4a451b02f26433b3f45f3f9f1c15f16 Mon Sep 17 00:00:00 2001 From: kjgbot Date: Tue, 22 Sep 2026 23:22:39 -0700 Subject: [PATCH 5/6] docs: correct workspace key evidence name Session-Id: 01a0cac4-0bbe-7a11-bb46-2500bfdac947 --- docs/evidence/subscription-workspace-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/evidence/subscription-workspace-auth.md b/docs/evidence/subscription-workspace-auth.md index eb7309932..84db1f2f8 100644 --- a/docs/evidence/subscription-workspace-auth.md +++ b/docs/evidence/subscription-workspace-auth.md @@ -1,6 +1,6 @@ # Provider subscription workspace authentication and wake proof -The resumed worker had real RELAY*WORKSPACE_KEY and RELAY_AGENT_TOKEN values. +The resumed worker had real RELAY_WORKSPACE_KEY and RELAY_AGENT_TOKEN values. A temporary fetch diagnostic recorded GET https://cast.agentrelay.com/v1/agents returning 401, followed by "Workspace key required (rk_live*...)". Removing only RELAY_AGENT_TOKEN made the same request return 200. This was client credential From bcf59994ce296a8fcb155e9457ac096071af0caa Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Wed, 23 Sep 2026 06:24:07 +0000 Subject: [PATCH 6/6] style: auto-format with Prettier --- docs/evidence/subscription-workspace-auth.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/evidence/subscription-workspace-auth.md b/docs/evidence/subscription-workspace-auth.md index 84db1f2f8..f0be365b3 100644 --- a/docs/evidence/subscription-workspace-auth.md +++ b/docs/evidence/subscription-workspace-auth.md @@ -2,7 +2,7 @@ The resumed worker had real RELAY_WORKSPACE_KEY and RELAY_AGENT_TOKEN values. A temporary fetch diagnostic recorded GET https://cast.agentrelay.com/v1/agents -returning 401, followed by "Workspace key required (rk_live*...)". Removing only +returning 401, followed by "Workspace key required (rk_live\*...)". Removing only RELAY_AGENT_TOKEN made the same request return 200. This was client credential selection, not missing worker environment, a masked key, or node configuration. No credential values were logged. Temporary diagnostics were removed.