orca/src/shared/agent-hook-relay.ts

119 lines
5.5 KiB
TypeScript

// Why: defines the wire shape carried by the JSON-RPC `agent.hook` notification
// the relay sends to Orca. Consumed by `src/relay/agent-hook-server.ts` (which
// produces it after the shared listener parses an HTTP POST) and by
// `src/main/agent-hooks/server.ts` (which ingests it via `ingestRemote`).
//
// Lives in `shared/` because the relay deliberately has no Electron dependency
// (cf. `src/relay/protocol.ts` header). `agent-hook-types.ts` is reserved for
// the renderer-bound IPC + installer contract; this module is the wire envelope
// between Orca's main process and the remote relay.
//
// Per the design doc:
// - The relay normalizes; Orca routes. The envelope's `payload` field has
// already been through `normalizeHookPayload` (which calls
// `parseAgentStatusPayload` → `normalizeAgentStatusObject`) on the relay
// side. Orca's `ingestRemote` re-runs the canonical payload normalizer at
// the SSH trust boundary before caching or persisting, so relay skew or a
// buggy remote process cannot poison main-process state.
// - The wire `connectionId` is **always `null`**: a `connectionId` is Orca's
// local handle on an `ssh2` connection, not a wire identity. Orca stamps the
// real value on receive from `mux` identity inside `ingestRemote`.
// - The wire `version` and `env` fields are forwarded from the agent CLI's
// POST body so Orca's warn-once protocol diagnostics still fire. The relay
// default env is `remote`, a location marker ignored by dev-vs-prod checks.
import type { ParsedAgentStatusPayload } from './agent-status-types'
import type { AgentProviderSessionMetadata } from './agent-session-resume'
// Why: the local hook server knows the discriminator from URL pathname routing
// (`/hook/<source>`); the relay equally must tag each forwarded notification
// with the same value so Orca can attribute the event back to the right CLI.
// Promoted from `src/main/agent-hooks/server.ts` so the relay can import it
// without dragging Electron in (the shared listener module is the only place
// that consumes it from the relay side).
export type AgentHookSource =
| 'claude'
| 'codex'
| 'gemini'
| 'antigravity'
| 'amp'
| 'opencode'
| 'cursor'
| 'pi'
| 'omp'
| 'droid'
| 'command-code'
| 'grok'
| 'copilot'
| 'hermes'
/** Env marker used by the remote relay. It is a transport/location marker, not
* a dev-vs-prod build tag, so main-process env mismatch diagnostics ignore it. */
export const REMOTE_AGENT_HOOK_ENV = 'remote' as const
/** Wire envelope for a single hook event flowing relay → Orca. */
export type AgentHookRelayEnvelope = {
source: AgentHookSource
paneKey: string
tabId?: string
worktreeId?: string
/** Always `null` on the wire — relay does not know Orca's local connectionId. */
connectionId: null
/** Preserved from the relay-side normalized hook event so Orca can
* distinguish a true same-prompt retry from a cached-prompt tool ping. */
hasExplicitPrompt?: boolean
/** Optional stable per-turn key from the relay-side listener. Used only for
* in-memory dedupe; never included in product telemetry payloads. */
promptInteractionKey?: string
/** Hook discriminator preserved for main-process transition rules. */
hookEventName?: string
/** Claude tool execution id, when the source hook provides one. */
toolUseId?: string
/** Claude subagent identity, when the source hook provides one. */
toolAgentId?: string
/** Claude agent type, used only as a lower-confidence identity fallback. */
toolAgentType?: string
/** Provider-owned conversation/session id needed to resume a sleeping agent. */
providerSession?: AgentProviderSessionMetadata
/** True when the relay is replaying its cache after Orca reconnects. */
isReplay?: boolean
/** Forwarded from the agent CLI POST body. The relay default is `remote`,
* which marks transport location rather than dev/prod build env. */
env?: string
/** Forwarded verbatim from the agent CLI POST body. Lets Orca's warn-once
* protocol-version diagnostic fire on remote events the same as on local. */
version?: string
/** Pre-normalized status payload from the relay's `normalizeHookPayload`.
* Orca's `ingestRemote` validates it again at the SSH trust boundary. */
payload: ParsedAgentStatusPayload
}
/** JSON-RPC notification method name carried over the relay control channel. */
export const AGENT_HOOK_NOTIFICATION_METHOD = 'agent.hook' as const
/** JSON-RPC request method Orca issues after `--connect` reattach to ask the
* relay to replay its per-paneKey last-payload cache. See §5 Path 3 of the
* design doc for the race that ruled out push-on-`setWrite`. */
export const AGENT_HOOK_REQUEST_REPLAY_METHOD = 'agent_hook.requestReplay' as const
/** JSON-RPC request method Orca issues at session-ready to ship the
* OpenCode/Pi plugin source files to the relay so it can materialize the
* per-PTY overlay dirs on the remote. */
export const AGENT_HOOK_INSTALL_PLUGINS_METHOD = 'agent_hook.installPlugins' as const
/** Feature-flag env var. Read once at process start by Orca and the relay.
* Remote agent hooks ship as the default SSH behavior; set "0" to opt out. */
export const ORCA_FEATURE_REMOTE_AGENT_HOOKS_ENV = 'ORCA_FEATURE_REMOTE_AGENT_HOOKS' as const
export function isRemoteAgentHooksEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
const raw = env[ORCA_FEATURE_REMOTE_AGENT_HOOKS_ENV]
if (raw === undefined) {
return true
}
const trimmed = raw.trim()
if (trimmed.length === 0 || trimmed === '0') {
return false
}
return true
}