Restore agent hook opt-out controls (#2778)

This commit is contained in:
Neil
2026-05-25 10:37:22 -07:00
committed by GitHub
parent aaa6ade586
commit d4703bd1ab
50 changed files with 1627 additions and 275 deletions
+2
View File
@@ -98,8 +98,10 @@ export function isCommandGroup(commandPath: string[]): boolean {
'storage',
'orchestration',
'computer',
'agent',
'environment'
].includes(commandPath[0])) ||
(commandPath.length === 2 && commandPath[0] === 'agent' && commandPath[1] === 'hooks') ||
(commandPath.length === 2 &&
commandPath[0] === 'storage' &&
['local', 'session'].includes(commandPath[1]))
+2
View File
@@ -16,6 +16,7 @@ import { BROWSER_STORAGE_HANDLERS } from './handlers/browser-storage'
import { ORCHESTRATION_HANDLERS } from './handlers/orchestration'
import { COMPUTER_HANDLERS } from './handlers/computer'
import { ENVIRONMENT_HANDLERS } from './handlers/environment'
import { AGENT_HOOK_HANDLERS } from './handlers/agent-hooks'
export type HandlerContext = {
flags: Map<string, string | boolean>
@@ -44,6 +45,7 @@ function buildHandlers(): Map<string, CommandHandler> {
BROWSER_STORAGE_HANDLERS,
ORCHESTRATION_HANDLERS,
COMPUTER_HANDLERS,
AGENT_HOOK_HANDLERS,
ENVIRONMENT_HANDLERS
]
for (const group of groups) {
+163
View File
@@ -0,0 +1,163 @@
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'fs'
import { homedir } from 'os'
import { dirname, join } from 'path'
import { randomUUID } from 'crypto'
import type { CommandHandler } from '../dispatch'
import { printResult } from '../format'
import { RuntimeClientError, type RuntimeClient, type RuntimeRpcSuccess } from '../runtime-client'
import type { AgentHookInstallStatus } from '../../shared/agent-hook-types'
import { getDefaultPersistedState } from '../../shared/constants'
import type { PersistedState } from '../../shared/types'
import {
applyAgentStatusHooksEnabled,
getManagedAgentHookStatuses
} from '../../main/agent-hooks/managed-agent-hook-controls'
import { getDefaultUserDataPath } from '../runtime-client'
type AgentHookCommandResult = {
enabled: boolean
settingsPath: string
appliedBy: 'runtime' | 'offline'
statuses: AgentHookInstallStatus[]
}
function getDataPath(): string {
return join(getDefaultUserDataPath(), 'orca-data.json')
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}
function readPersistedState(dataPath: string): PersistedState {
if (!existsSync(dataPath)) {
return getDefaultPersistedState(homedir())
}
try {
const parsed = JSON.parse(readFileSync(dataPath, 'utf-8'))
if (!isRecord(parsed)) {
throw new Error('file does not contain a JSON object')
}
return parsed as PersistedState
} catch (error) {
throw new RuntimeClientError(
'runtime_error',
`Could not read ${dataPath}: ${error instanceof Error ? error.message : String(error)}`
)
}
}
function writePersistedState(dataPath: string, state: PersistedState): void {
mkdirSync(dirname(dataPath), { recursive: true })
const tmpPath = join(dirname(dataPath), `.${Date.now()}-${randomUUID()}.tmp`)
let renamed = false
try {
writeFileSync(tmpPath, `${JSON.stringify(state, null, 2)}\n`, 'utf-8')
renameSync(tmpPath, dataPath)
renamed = true
} finally {
if (!renamed && existsSync(tmpPath)) {
try {
unlinkSync(tmpPath)
} catch {
// best effort
}
}
}
}
function readEnabledFromDisk(): boolean {
const state = readPersistedState(getDataPath())
return state.settings?.agentStatusHooksEnabled !== false
}
function updateEnabledOnDisk(enabled: boolean): string {
const dataPath = getDataPath()
const state = readPersistedState(dataPath)
state.settings = {
...getDefaultPersistedState(homedir()).settings,
...state.settings,
agentStatusHooksEnabled: enabled
}
writePersistedState(dataPath, state)
return dataPath
}
async function updateRunningRuntime(client: RuntimeClient, enabled: boolean): Promise<boolean> {
try {
const status = await client.getCliStatus()
if (!status.result.runtime.reachable) {
return false
}
await client.call(
'settings.update',
{ agentStatusHooksEnabled: enabled },
{ timeoutMs: 10_000 }
)
return true
} catch {
return false
}
}
function localSuccess<TResult>(result: TResult): RuntimeRpcSuccess<TResult> {
return {
id: 'local',
ok: true,
result,
_meta: {
runtimeId: 'local'
}
}
}
function formatAgentHookCommandResult(result: AgentHookCommandResult): string {
const statusSummary = result.statuses
.map((status) => `${status.agent}: ${status.state}`)
.join('\n')
return [
`agentStatusHooksEnabled: ${result.enabled}`,
`appliedBy: ${result.appliedBy}`,
`settingsPath: ${result.settingsPath}`,
statusSummary
]
.filter(Boolean)
.join('\n')
}
async function setAgentHooksEnabled(
client: RuntimeClient,
enabled: boolean
): Promise<AgentHookCommandResult> {
const updatedRuntime = await updateRunningRuntime(client, enabled)
const settingsPath = updatedRuntime ? getDataPath() : updateEnabledOnDisk(enabled)
const statuses = updatedRuntime
? getManagedAgentHookStatuses()
: applyAgentStatusHooksEnabled(enabled)
return {
enabled,
settingsPath,
appliedBy: updatedRuntime ? 'runtime' : 'offline',
statuses
}
}
export const AGENT_HOOK_HANDLERS: Record<string, CommandHandler> = {
'agent hooks status': async ({ json }) => {
const result: AgentHookCommandResult = {
enabled: readEnabledFromDisk(),
settingsPath: getDataPath(),
appliedBy: 'offline',
statuses: getManagedAgentHookStatuses()
}
printResult(localSuccess(result), json, formatAgentHookCommandResult)
},
'agent hooks off': async ({ client, json }) => {
const result = await setAgentHooksEnabled(client, false)
printResult(localSuccess(result), json, formatAgentHookCommandResult)
},
'agent hooks on': async ({ client, json }) => {
const result = await setAgentHooksEnabled(client, true)
printResult(localSuccess(result), json, formatAgentHookCommandResult)
}
}
+3 -1
View File
@@ -17,7 +17,9 @@ export { COMMAND_SPECS } from './specs'
export { buildCurrentWorktreeSelector, normalizeWorktreeSelector } from './selectors'
function shouldIgnoreRemoteSelection(commandPath: string[]): boolean {
return commandPath[0] === 'environment' || commandPath[0] === 'serve'
return (
commandPath[0] === 'environment' || commandPath[0] === 'serve' || commandPath[0] === 'agent'
)
}
export async function main(argv = process.argv.slice(2), cwd = process.cwd()): Promise<void> {
+26
View File
@@ -0,0 +1,26 @@
import type { CommandSpec } from '../args'
import { GLOBAL_FLAGS } from '../args'
export const AGENT_HOOK_COMMAND_SPECS: CommandSpec[] = [
{
path: ['agent', 'hooks', 'status'],
summary: 'Show whether Orca-managed agent status hooks are enabled',
usage: 'orca agent hooks status [--json]',
allowedFlags: [...GLOBAL_FLAGS],
examples: ['orca agent hooks status', 'orca agent hooks status --json']
},
{
path: ['agent', 'hooks', 'off'],
summary: 'Disable Orca-managed agent status hooks and remove local hook entries',
usage: 'orca agent hooks off [--json]',
allowedFlags: [...GLOBAL_FLAGS],
examples: ['orca agent hooks off']
},
{
path: ['agent', 'hooks', 'on'],
summary: 'Enable Orca-managed agent status hooks',
usage: 'orca agent hooks on [--json]',
allowedFlags: [...GLOBAL_FLAGS],
examples: ['orca agent hooks on']
}
]
+2
View File
@@ -6,6 +6,7 @@ import { CORE_COMMAND_SPECS } from './core'
import { ORCHESTRATION_COMMAND_SPECS } from './orchestration'
import { COMPUTER_COMMAND_SPECS } from './computer'
import { ENVIRONMENT_COMMAND_SPECS } from './environment'
import { AGENT_HOOK_COMMAND_SPECS } from './agent-hooks'
export const COMMAND_SPECS: CommandSpec[] = [
...CORE_COMMAND_SPECS,
@@ -14,5 +15,6 @@ export const COMMAND_SPECS: CommandSpec[] = [
...BROWSER_ADVANCED_COMMAND_SPECS,
...ORCHESTRATION_COMMAND_SPECS,
...COMPUTER_COMMAND_SPECS,
...AGENT_HOOK_COMMAND_SPECS,
...ENVIRONMENT_COMMAND_SPECS
]