mirror of
https://github.com/stablyai/orca.git
synced 2026-10-17 08:03:14 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 } = {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user