diff --git a/src/main/agent-hooks/server-ingest-remote.test.ts b/src/main/agent-hooks/server-ingest-remote.test.ts index 726a9d2d50e..673c3757579 100644 --- a/src/main/agent-hooks/server-ingest-remote.test.ts +++ b/src/main/agent-hooks/server-ingest-remote.test.ts @@ -653,10 +653,10 @@ describe('AgentHookServer ingestRemote', () => { paneKey: PANE, tabId: 'tab-1', worktreeId: 'wt-1', + advertisedAgentStatusCapabilities: [], payload: { state: 'working', prompt: 'unsupported peer', agentType: 'claude' } }, - 'conn-1', - [] + 'conn-1' ) const olderPeerRow = server.getStatusSnapshot()[0] @@ -665,10 +665,10 @@ describe('AgentHookServer ingestRemote', () => { paneKey: PANE, tabId: 'tab-1', worktreeId: 'wt-1', + advertisedAgentStatusCapabilities: [AGENT_STATUS_RUNS_RUNTIME_CAPABILITY], payload: { state: 'done', prompt: 'capable peer', agentType: 'claude' } }, - 'conn-1', - [AGENT_STATUS_RUNS_RUNTIME_CAPABILITY] + 'conn-1' ) expect(server.getStatusSnapshot()).toEqual([olderPeerRow]) diff --git a/src/main/agent-hooks/server/server-ingest-remote.ts b/src/main/agent-hooks/server/server-ingest-remote.ts index 91edeb4a947..d14714ae09b 100644 --- a/src/main/agent-hooks/server/server-ingest-remote.ts +++ b/src/main/agent-hooks/server/server-ingest-remote.ts @@ -52,15 +52,19 @@ export abstract class AgentHookServerIngestRemote extends AgentHookServerIngestS /** Payload fields the relay dropped to fit an oversized frame; validated below. */ shedFields?: unknown claudeRunningNonAgentTask?: unknown + /** The producing peer's advertised run-capability set — a property of the peer/connection that built this envelope, not an orthogonal call parameter. Absent (older relay/HTTP paths) defaults to the unadvertised-legacy-peer set. */ + advertisedAgentStatusCapabilities?: readonly string[] payload: unknown }, - connectionId: string | null, - advertisedAgentStatusCapabilities: readonly string[] = AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES + connectionId: string | null ): void { if ( !canAdmitLegacyAgentStatus( 'main-status-update', - olderPeerAgentStatusLegacyMode(advertisedAgentStatusCapabilities) + olderPeerAgentStatusLegacyMode( + envelope?.advertisedAgentStatusCapabilities ?? + AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES + ) ) ) { return diff --git a/src/main/agent-hooks/wsl-hook-relay-deps.ts b/src/main/agent-hooks/wsl-hook-relay-deps.ts index dea27a880a9..cd9c1929853 100644 --- a/src/main/agent-hooks/wsl-hook-relay-deps.ts +++ b/src/main/agent-hooks/wsl-hook-relay-deps.ts @@ -100,12 +100,17 @@ export const defaultWslHookRelayDeps: WslHookRelayManagerDeps = { spawnRelay: spawnWslRelayProcess, runInstall: runWslInstallProcess, waitForSentinel: waitForWslRelaySentinel, - ingest: (envelope, connectionId) => - agentHookServer.ingestRemote( - envelope as Parameters[0], - connectionId, - AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES - ), + // Why: the WSL relay protocol advertises no run-serving capability; stamped onto a copy so the + // wire-deserialized notification object itself is never mutated. + ingest: (envelope, connectionId) => { + const capped = { + ...envelope, + advertisedAgentStatusCapabilities: AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES + } + type IngestEnvelope = Parameters[0] + // oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: envelope is the wire-deserialized notification; ingestRemote independently re-validates paneKey's type before trusting anything here. + return agentHookServer.ingestRemote(capped as IngestEnvelope, connectionId) + }, installHooks: installRemoteManagedAgentHooks, installCodex: (runtimeHomePath, distro) => codexHookService.installForRuntimeHomeSerialized(runtimeHomePath, { diff --git a/src/main/ssh/ssh-relay-session.ts b/src/main/ssh/ssh-relay-session.ts index 70095196f58..fd8579610ae 100644 --- a/src/main/ssh/ssh-relay-session.ts +++ b/src/main/ssh/ssh-relay-session.ts @@ -1607,10 +1607,11 @@ export class SshRelaySession { typeof envelope.claudeRunningNonAgentTask === 'boolean' ? envelope.claudeRunningNonAgentTask : undefined, + // Why: the SSH relay protocol advertises no run-serving capability. + advertisedAgentStatusCapabilities: AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES, payload: envelope.payload }, - this.targetId, - AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES + this.targetId ) }) diff --git a/src/shared/agent-status-legacy-adapter.ts b/src/shared/agent-status-legacy-adapter.ts index 42089779a07..cbed5778a78 100644 --- a/src/shared/agent-status-legacy-adapter.ts +++ b/src/shared/agent-status-legacy-adapter.ts @@ -33,7 +33,7 @@ export const AGENT_STATUS_PERSISTED_HYDRATION_MODE: AgentStatusLegacyAdmissionMo kind: 'persisted-hydration' }) -/** Existing relay protocols advertise no run-serving capability. Production call sites pass this explicitly. */ +/** Existing relay protocols advertise no run-serving capability. Production ingress call sites stamp this onto the envelope explicitly. */ export const AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES: readonly string[] = Object.freeze( [] ) diff --git a/src/shared/agent-status-legacy-ingress-ratchet.test.ts b/src/shared/agent-status-legacy-ingress-ratchet.test.ts index 70dec4e0bdf..648adda1d61 100644 --- a/src/shared/agent-status-legacy-ingress-ratchet.test.ts +++ b/src/shared/agent-status-legacy-ingress-ratchet.test.ts @@ -116,8 +116,12 @@ describe('legacy agent-status ingress ratchet', () => { 'main/agent-hooks/wsl-hook-relay-deps.ts', 'main/ssh/ssh-relay-session.ts' ]) + // Why: a bare import of the constant (unused elsewhere) would pass a substring check + // without ever stamping it onto the envelope — require the actual key:value binding. for (const caller of callers) { - expect(caller.source).toContain('AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES') + expect(stripComments(caller.source)).toMatch( + /advertisedAgentStatusCapabilities\s*:\s*AGENT_STATUS_LEGACY_UNADVERTISED_PEER_CAPABILITIES\b/ + ) } })