mirror of
https://github.com/stablyai/orca.git
synced 2026-09-29 08:03:20 +00:00
* Split speech session lifecycle * Split terminal output scheduler pipeline * Split mobile browser pane modules * Prune resolved max-lines suppressions * Split pane tree equalization logic * Extract mobile troubleshoot screen styles * Split external automation manager * Split main window service attachments * Split hosted review creation checks * Split automation dispatch event handling * Split settings navigation metadata * Split daemon initialization lifecycle * Split GitLab item dialog * Split relay dispatcher layers * Split mobile host screen * Retarget mobile view settings source test * Split runtime file client layers * Split ports panel layers * Split runtime environments pane layers * Split local PTY provider responsibilities * Split CDP bridge responsibilities * Split relay Git handler responsibilities * Track moved relay Git fetch audit * Split Linear item drawer responsibilities * Split telemetry event schema responsibilities * Split resource usage status responsibilities * Split remote terminal multiplexer responsibilities * Split Git worktree responsibilities * Split Codex hook service responsibilities * Keep mirrored hook trust type private * Fix F3-speech for #17123 * Fix F1-cycle for #17131 * Fix F4-navtest for #17157 * Fix F2-allowlist for #17161
144 lines
5.4 KiB
TypeScript
144 lines
5.4 KiB
TypeScript
import type { SFTPWrapper } from 'ssh2'
|
|
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
|
|
import {
|
|
buildManagedCommandHook,
|
|
createManagedCommandMatcher,
|
|
MANAGED_HOOK_TIMEOUT_SECONDS,
|
|
removeManagedCommands,
|
|
wrapPosixHookCommand,
|
|
type HookDefinition
|
|
} from '../agent-hooks/installer-utils'
|
|
import {
|
|
readHooksJsonRemote,
|
|
readTextFileRemote,
|
|
writeHooksJsonRemote,
|
|
writeManagedScriptRemote,
|
|
writeTextFileRemoteAtomic
|
|
} from '../agent-hooks/installer-utils-remote'
|
|
import { upsertHookTrustEntriesInContent, type CodexTrustEntry } from './config-toml-trust'
|
|
import {
|
|
CODEX_EVENTS,
|
|
CODEX_EVENT_LABEL,
|
|
wrapReadablePosixHookCommand
|
|
} from './codex-hook-definition'
|
|
import { getManagedScript } from './codex-hook-script'
|
|
|
|
export async function installCodexHooksRemote(
|
|
sftp: SFTPWrapper,
|
|
remoteHome: string,
|
|
options?: { codexHomeDir?: string; deferTrustUntilConfigToml?: boolean }
|
|
): Promise<AgentHookInstallStatus> {
|
|
const codexHomeBase =
|
|
options?.codexHomeDir?.replace(/\/$/, '') ?? `${remoteHome.replace(/\/$/, '')}/.codex`
|
|
const remoteConfigPath = `${codexHomeBase}/hooks.json`
|
|
const remoteTomlPath = `${codexHomeBase}/config.toml`
|
|
// Redirected WSL homes must use the same script location and command shape
|
|
// as the runtime installer; two representations of one hooks.json race
|
|
// into stale trust keys. Plain SSH keeps its guest-home script contract.
|
|
const redirectedCodexHome = options?.codexHomeDir?.replace(/\/$/, '')
|
|
const remoteScriptPath = redirectedCodexHome
|
|
? `${redirectedCodexHome}/.orca/agent-hooks/codex-hook.sh`
|
|
: `${remoteHome.replace(/\/$/, '')}/.orca/agent-hooks/codex-hook.sh`
|
|
try {
|
|
const config = await readHooksJsonRemote(sftp, remoteConfigPath)
|
|
if (!config) {
|
|
return {
|
|
agent: 'codex',
|
|
state: 'error',
|
|
configPath: remoteConfigPath,
|
|
managedHooksPresent: false,
|
|
detail: 'Could not parse remote Codex hooks.json'
|
|
}
|
|
}
|
|
|
|
const command = redirectedCodexHome
|
|
? wrapReadablePosixHookCommand(remoteScriptPath)
|
|
: wrapPosixHookCommand(remoteScriptPath)
|
|
const nextHooks = { ...config.hooks }
|
|
const managedEvents = new Set<string>(CODEX_EVENTS)
|
|
const isManagedCommand = createManagedCommandMatcher('codex-hook.sh')
|
|
|
|
for (const [eventName, definitions] of Object.entries(nextHooks)) {
|
|
if (managedEvents.has(eventName) || !Array.isArray(definitions)) {
|
|
continue
|
|
}
|
|
const cleaned = removeManagedCommands(definitions, isManagedCommand)
|
|
if (cleaned.length === 0) {
|
|
delete nextHooks[eventName]
|
|
} else {
|
|
nextHooks[eventName] = cleaned
|
|
}
|
|
}
|
|
|
|
const trustEntries: CodexTrustEntry[] = []
|
|
for (const eventName of CODEX_EVENTS) {
|
|
const current = Array.isArray(nextHooks[eventName]) ? nextHooks[eventName] : []
|
|
const cleaned = removeManagedCommands(current, isManagedCommand)
|
|
const definition: HookDefinition = {
|
|
hooks: [buildManagedCommandHook(command)]
|
|
}
|
|
nextHooks[eventName] = redirectedCodexHome
|
|
? [definition, ...cleaned]
|
|
: [...cleaned, definition]
|
|
trustEntries.push({
|
|
sourcePath: remoteConfigPath,
|
|
eventLabel: CODEX_EVENT_LABEL[eventName],
|
|
groupIndex: redirectedCodexHome ? 0 : cleaned.length,
|
|
handlerIndex: 0,
|
|
command,
|
|
timeoutSec: MANAGED_HOOK_TIMEOUT_SECONDS
|
|
})
|
|
}
|
|
|
|
config.hooks = nextHooks
|
|
// Why: write script/settings before trust TOML; a partial trust write leaves Codex asking approval instead of running a missing script.
|
|
// Why: SSH remotes use POSIX `.sh` paths even when Orca runs on Windows; never derive remote script syntax from local OS.
|
|
await writeManagedScriptRemote(sftp, remoteScriptPath, getManagedScript('posix'))
|
|
// Why: SSH edits the user's remote ~/.codex/hooks.json directly, so preserve non-Orca top-level metadata while replacing the hooks tree.
|
|
await writeHooksJsonRemote(sftp, remoteConfigPath, { ...config, hooks: nextHooks })
|
|
try {
|
|
const existingTomlRaw = await readTextFileRemote(sftp, remoteTomlPath)
|
|
if (existingTomlRaw === null && options?.deferTrustUntilConfigToml === true) {
|
|
return {
|
|
agent: 'codex',
|
|
state: 'installed',
|
|
configPath: remoteConfigPath,
|
|
managedHooksPresent: true,
|
|
detail: 'Trust entries deferred until config.toml is seeded by the launch path'
|
|
}
|
|
}
|
|
const existingToml = existingTomlRaw ?? ''
|
|
const updatedToml = upsertHookTrustEntriesInContent(existingToml, trustEntries)
|
|
if (updatedToml !== existingToml) {
|
|
await writeTextFileRemoteAtomic(sftp, remoteTomlPath, updatedToml)
|
|
}
|
|
} catch (error) {
|
|
return {
|
|
agent: 'codex',
|
|
state: 'error',
|
|
configPath: remoteConfigPath,
|
|
managedHooksPresent: true,
|
|
detail: `Hooks installed but trust entries could not be written: ${
|
|
error instanceof Error ? error.message : String(error)
|
|
}. Run /hooks in Codex on the remote host to approve.`
|
|
}
|
|
}
|
|
|
|
return {
|
|
agent: 'codex',
|
|
state: 'installed',
|
|
configPath: remoteConfigPath,
|
|
managedHooksPresent: true,
|
|
detail: null
|
|
}
|
|
} catch (err) {
|
|
return {
|
|
agent: 'codex',
|
|
state: 'error',
|
|
configPath: remoteConfigPath,
|
|
managedHooksPresent: false,
|
|
detail: err instanceof Error ? err.message : String(err)
|
|
}
|
|
}
|
|
}
|