mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 16:03:20 +00:00
* feat(agent-hooks): introduce relay wire envelope + connectionId stamping Adds the shared `agent-hook-relay.ts` module with the `agent.hook` JSON-RPC notification envelope, the `agent_hook.requestReplay` / `agent_hook.installPlugins` method names, and the `ORCA_FEATURE_REMOTE_AGENT_HOOKS` flag helper. Promotes `AgentHookSource` to `shared/` so the relay can import it without dragging Electron in. Threads a `connectionId: string | null` field through `AgentHookEventPayload`, the `agentStatus:set` IPC contract, and the renderer-bound preload listener. Local hook posts stamp `null`; the relay-forwarded path will stamp from `mux` identity in a later commit. Renderer uses the stamp for stale-event filtering when an SSH connection tears down with notifications still in flight. See docs/design/agent-status-over-ssh.md §1, §5, §8 (commit #1). Co-authored-by: Orca <help@stably.ai> * refactor(agent-hooks): extract shared listener; add relay-side adapter Extracts the listener internals (request parsing, payload normalization, endpoint-file writing, per-CLI extractors, warn-once Sets, slowloris timer helper, request size cap, paneKey caches) from `src/main/agent-hooks/server.ts` into a new transport-agnostic `src/shared/agent-hook-listener.ts`. The shared module uses only Node builtins (no Electron) so it is safe to import from `src/relay/`. Adds `src/relay/agent-hook-server.ts` — a thin HTTP-loopback adapter that wires the shared listener to a `forward(envelope)` callback so `relay.ts` can re-emit each parsed payload as an `agent.hook` JSON-RPC notification on the existing SshChannelMultiplexer. The adapter owns: - 127.0.0.1:0 socket + bearer-token auth, identical shape to the local server - per-paneKey last-payload cache + replayCachedPayloadsForPanes() for the request-driven replay path used after `--connect` reattach (see §5 Path 3) - clearPaneState(paneKey) for PTY-exit eviction (symmetric with local server) - buildPtyEnv() / endpoint-file writing for relay-spawned PTYs Orca's `AgentHookServer` is now a ~200-LoC adapter over the shared listener that owns the IPC fanout, listener replay, and `ingestRemote(envelope, connId)` entry point that bypasses the HTTP path for relay-forwarded events. See docs/design/agent-status-over-ssh.md §3, §8 (commit #2). Co-authored-by: Orca <help@stably.ai> * fix(preload): expose connectionId on agentStatus.onSet type src/preload/index.ts already passes through `connectionId?: string | null` from main, but the PreloadApi declaration in api-types.ts was missing the field. Align the type with the runtime contract so renderer call sites can read connectionId without an `as` cast. Co-authored-by: Orca <help@stably.ai> * fix(agent-hooks): harden ingestRemote + relay replay; review-driven cleanup - ingestRemote: re-run normalizeAgentStatusPayload at trust boundary; trim+validate connectionId/paneKey/tabId/worktreeId - relay: preserve source/env/version through replay via sidecar map; drop sourceFromAgentType fallback that mis-tagged unknown agents - shared listener: exhaustive switch+never on AgentHookSource dispatch chains; extractPromptText returns trimmed values; export MAX_PANE_KEY_LEN - preload: tighten connectionId from optional to required (always sent) - main IPC: reorder spread so explicit envelope fields win on collision Co-authored-by: Orca <help@stably.ai> * chore(docs): drop agent-status-over-ssh design doc from PR The design RFC was useful for authoring this PR series but doesn't belong in-tree — keeping it here would freeze line-number references and design prose against future churn. Folding it into the PR description instead. Co-authored-by: Orca <help@stably.ai> * chore(agent-hooks): widen ingestRemote type for env/version (PR2 prep) Declares `env?: string` and `version?: string` on the `ingestRemote` envelope parameter so PR2 only needs to add the `warnOnHookEnvOrVersionMismatch` callsite, not also widen the type. The fields are forwarded verbatim from the agent CLI POST body on the remote and let Orca's warn-once cross-build / dev-vs-prod diagnostics fire identically on remote-sourced events. Type-only addition; no runtime consumer in this PR. Co-authored-by: Orca <help@stably.ai> --------- Co-authored-by: Orca <help@stably.ai>
84 lines
4.1 KiB
TypeScript
84 lines
4.1 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` on the relay side; Orca's
|
|
// ingestRemote re-runs the canonical normalizer at the trust boundary
|
|
// (defense-in-depth) before feeding the event into the same `onAgentStatus`
|
|
// fanout the local HTTP path uses.
|
|
// - 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 verbatim from the agent
|
|
// CLI's POST body so Orca's existing warn-once cross-build / dev-vs-prod
|
|
// diagnostics still fire on remote-sourced events.
|
|
|
|
import type { ParsedAgentStatusPayload } from './agent-status-types'
|
|
|
|
// 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' | 'opencode' | 'cursor' | 'pi'
|
|
|
|
/** 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
|
|
/** Forwarded verbatim from the agent CLI POST body (e.g. 'production',
|
|
* 'development'). Lets Orca's warn-once env-mismatch diagnostic fire on
|
|
* remote events the same as on local. */
|
|
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` re-validates via `normalizeAgentStatusPayload` at
|
|
* the trust boundary as defense-in-depth. */
|
|
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.
|
|
* Absent / empty / "0" = off; anything else = on. See §8 of the design doc
|
|
* for the gate locations. */
|
|
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 false
|
|
}
|
|
const trimmed = raw.trim()
|
|
if (trimmed.length === 0 || trimmed === '0') {
|
|
return false
|
|
}
|
|
return true
|
|
}
|