fix(agent-hooks): renderer ownership gate treats wsl:* connection ids as local (STA-1515)

Round-2 rig finding: with the link fixed, WSL hook envelopes reached
ingestRemote and the durable cache, but useIpcEvents.applyAgentStatus
drops any status whose stamped connectionId differs from the owning
repo's — 'wsl:<distro>' !== null for a local repo, so every WSL-relayed
status died before setAgentStatus and notifications.

wsl:* ids are transport provenance, not ownership: the gate now
normalizes them to local via isWslHookRelayConnectionId (shared
contract, also used by the relay link when stamping), while still
rejecting WSL-stamped events against SSH-owned repos. Provenance stays
stamped — it is what made this drop diagnosable.

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
brennanb2025
2026-07-09 14:50:43 -07:00
co-authored by Orca
parent 9245852d5a
commit 4556f2f1a2
5 changed files with 170 additions and 6 deletions
+10 -1
View File
@@ -221,7 +221,16 @@ two fixes:
can pinpoint any residual drop. The full host chain is pinned by a live integration
test (real bundle over real child stdio through the real manager into a real
`ingestRemote`).
2. **Codex reads a redirected home.** Orca launches WSL Codex with `CODEX_HOME` pointed at
2. **(Round 2) The renderer's SSH-era ownership gate dropped `wsl:*` events.** With the
link fixed, envelopes reached `ingestRemote` and the durable cache, but
`useIpcEvents.applyAgentStatus` compares the stamped connectionId against the owning
repo's — `"wsl:<distro>" !== null` for a local repo, so every WSL-relayed status died
before `setAgentStatus`/notifications. Fix: `wsl:*` ids are transport provenance, not
ownership — the gate normalizes them to local (null) via
`isWslHookRelayConnectionId`, while still rejecting WSL-stamped events against
SSH-owned repos. Provenance stays stamped (it made the drop diagnosable in the first
place).
3. **Codex reads a redirected home.** Orca launches WSL Codex with `CODEX_HOME` pointed at
the managed runtime home (`~/.local/share/orca/codex-runtime-home/home`), so installing
hooks to `~/.codex` left Codex dark. The installers now accept an explicit codex home;
the trust write into `config.toml` is deferred while that file doesn't exist (the
+2 -1
View File
@@ -6,6 +6,7 @@ import type { ChildProcessWithoutNullStreams } from 'node:child_process'
import type { SshChannelMultiplexer } from '../ssh/ssh-channel-multiplexer'
import { AGENT_HOOK_NOTIFICATION_METHOD } from '../../shared/agent-hook-relay'
import { wslHookRelayConnectionId } from '../../shared/wsl-hook-relay-contract'
export type WslRelayLinkOptions = {
mux: SshChannelMultiplexer
@@ -22,7 +23,7 @@ export type WslRelayLinkOptions = {
export function wireWslRelayLink(options: WslRelayLinkOptions): void {
const { mux, child, distro, ingest, warn, onDead } = options
const connectionId = `wsl:${distro}`
const connectionId = wslHookRelayConnectionId(distro)
mux.onNotification((method, params) => {
if (method !== AGENT_HOOK_NOTIFICATION_METHOD) {
+132
View File
@@ -5999,6 +5999,138 @@ describe('useIpcEvents agent status snapshot integration', () => {
)
})
it('accepts WSL-relayed status events for a local repo (wsl:* is transport provenance, not ownership)', async () => {
const setAgentStatus = vi.fn()
const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = {
current: null
}
const storeState: StoreLike = buildStoreState({
setAgentStatus,
workspaceSessionReady: true,
tabsByWorktree: {
'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'WSL Tab' }]
},
terminalLayoutsByTabId: {
'tab-future': {
root: { type: 'leaf', leafId: FUTURE_LEAF_ID },
activeLeafId: FUTURE_LEAF_ID,
expandedLeafId: null
}
},
repos: [{ id: 'repo-1', connectionId: null }],
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }
})
stubReactSyncEffect()
vi.doMock('../store', () => ({
useAppStore: {
subscribe: vi.fn(() => () => {}),
getState: () => storeState
}
}))
stubAuxiliaryModules()
vi.stubGlobal(
'window',
buildWindowApi({
onSet: (cb) => {
onSetListenerRef.current = cb
return () => {}
}
})
)
const { useIpcEvents } = await import('./useIpcEvents')
useIpcEvents()
await Promise.resolve()
if (typeof onSetListenerRef.current !== 'function') {
throw new Error('Expected agentStatus.onSet listener to be registered')
}
onSetListenerRef.current({
paneKey: FUTURE_PANE_KEY,
state: 'working',
prompt: 'wsl p',
agentType: 'claude',
worktreeId: 'wt-1',
connectionId: 'wsl:Ubuntu',
receivedAt: 1_700_000_000_000,
stateStartedAt: 1_699_999_999_000
})
expect(setAgentStatus).toHaveBeenCalledTimes(1)
expect(setAgentStatus).toHaveBeenCalledWith(
FUTURE_PANE_KEY,
expect.objectContaining({ state: 'working', prompt: 'wsl p', agentType: 'claude' }),
'WSL Tab',
{ updatedAt: 1_700_000_000_000, stateStartedAt: 1_699_999_999_000 },
expectWorktreeRouting('wt-1'),
undefined
)
})
it('still rejects WSL-relayed status events against an SSH-owned repo', async () => {
const setAgentStatus = vi.fn()
const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = {
current: null
}
const storeState: StoreLike = buildStoreState({
setAgentStatus,
workspaceSessionReady: true,
tabsByWorktree: {
'wt-1': [{ id: 'tab-future', ptyId: 'pty-1', worktreeId: 'wt-1', title: 'SSH Tab' }]
},
terminalLayoutsByTabId: {
'tab-future': {
root: { type: 'leaf', leafId: FUTURE_LEAF_ID },
activeLeafId: FUTURE_LEAF_ID,
expandedLeafId: null
}
},
repos: [{ id: 'repo-1', connectionId: 'ssh-1' }],
worktreesByRepo: { 'repo-1': [{ id: 'wt-1', repoId: 'repo-1' }] }
})
stubReactSyncEffect()
vi.doMock('../store', () => ({
useAppStore: {
subscribe: vi.fn(() => () => {}),
getState: () => storeState
}
}))
stubAuxiliaryModules()
vi.stubGlobal(
'window',
buildWindowApi({
onSet: (cb) => {
onSetListenerRef.current = cb
return () => {}
}
})
)
const { useIpcEvents } = await import('./useIpcEvents')
useIpcEvents()
await Promise.resolve()
if (typeof onSetListenerRef.current !== 'function') {
throw new Error('Expected agentStatus.onSet listener to be registered')
}
onSetListenerRef.current({
paneKey: FUTURE_PANE_KEY,
state: 'working',
prompt: 'wsl p',
agentType: 'claude',
worktreeId: 'wt-1',
connectionId: 'wsl:Ubuntu',
receivedAt: 1_700_000_000_000,
stateStartedAt: 1_699_999_999_000
})
expect(setAgentStatus).not.toHaveBeenCalled()
})
it('still rejects remote status events once the pane resolves to a local repo', async () => {
const setAgentStatus = vi.fn()
const onSetListenerRef: { current: ((data: AgentStatusSetData) => void) | null } = {
+13 -4
View File
@@ -31,6 +31,7 @@ import type {
} from '../../../shared/remote-workspace-types'
import type { RateLimitState } from '../../../shared/rate-limit-types'
import type { SshConnectionState } from '../../../shared/ssh-types'
import { isWslHookRelayConnectionId } from '../../../shared/wsl-hook-relay-contract'
import type {
RuntimeBrowserDriverState,
RuntimeTerminalPresentation,
@@ -2984,15 +2985,23 @@ export function useIpcEvents(): void {
// matches that tab's worktree, accept the status until repo ownership
// becomes available; once ownership is resolved, keep the strict
// connectionId check below.
// Why: the WSL hook relay stamps a transport-provenance connectionId
// (`wsl:<distro>`), but the pane is a LOCAL pane on a local repo —
// ownership-wise it is null. Without this normalization the strict
// check below drops every WSL-relayed status for a local repo (while
// still rejecting WSL-stamped events against SSH-owned repos).
const ownershipConnectionId = isWslHookRelayConnectionId(data.connectionId)
? null
: data.connectionId
const canAcceptPendingRemoteOwnership =
data.connectionId !== undefined &&
data.connectionId !== null &&
ownershipConnectionId !== undefined &&
ownershipConnectionId !== null &&
!repoConnectionResolved &&
data.worktreeId !== undefined &&
data.worktreeId === owningWorktreeId
if (
data.connectionId !== undefined &&
data.connectionId !== repoConnectionId &&
ownershipConnectionId !== undefined &&
ownershipConnectionId !== repoConnectionId &&
!canAcceptPendingRemoteOwnership
) {
return 'dropped'
+13
View File
@@ -54,6 +54,19 @@ export function wslHookRelayEndpointDir(guestHome: string, windowsPort: number):
* `endpoint.cmd`. Matches the POSIX branch of that helper. */
export const WSL_HOOK_RELAY_ENDPOINT_FILE = 'endpoint.env'
/** connectionId stamped on WSL-relayed hook envelopes. Transport provenance
* only: the pane is a LOCAL pane on a local repo, so ownership checks must
* treat these ids as local (null), not as a remote connection. */
export const WSL_HOOK_RELAY_CONNECTION_PREFIX = 'wsl:'
export function wslHookRelayConnectionId(distro: string): string {
return `${WSL_HOOK_RELAY_CONNECTION_PREFIX}${distro}`
}
export function isWslHookRelayConnectionId(value: string | null | undefined): boolean {
return typeof value === 'string' && value.startsWith(WSL_HOOK_RELAY_CONNECTION_PREFIX)
}
export function wslHookRelayEndpointFilePath(guestHome: string, windowsPort: number): string {
return `${wslHookRelayEndpointDir(guestHome, windowsPort)}/${WSL_HOOK_RELAY_ENDPOINT_FILE}`
}