holo-codex 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/plugins/marketplace.json +20 -0
- package/CONTRIBUTING.md +54 -0
- package/LICENSE +21 -0
- package/README.md +215 -0
- package/README.zh-CN.md +215 -0
- package/SECURITY.md +39 -0
- package/assets/brand/README.md +35 -0
- package/assets/brand/holo-codex-icon.svg +28 -0
- package/assets/brand/holo-codex-lockup.svg +49 -0
- package/assets/brand/holo-codex-mark.svg +33 -0
- package/assets/brand/holo-codex-plugin-card.png +0 -0
- package/assets/brand/holo-codex-plugin-card.svg +81 -0
- package/assets/brand/holo-codex-readme-hero.png +0 -0
- package/assets/brand/holo-codex-readme-hero.svg +140 -0
- package/assets/brand/holo-codex-social-preview.png +0 -0
- package/assets/brand/holo-codex-social-preview.svg +130 -0
- package/assets/brand/holo-codex-wordmark-options.svg +52 -0
- package/docs/checklists/agent-loop-first-delivery-audit.md +129 -0
- package/docs/examples/generic-loop-repo-hygiene.md +168 -0
- package/docs/install.md +190 -0
- package/docs/local-release-readiness.md +206 -0
- package/docs/release-checklist.md +144 -0
- package/docs/self-bootstrap.md +150 -0
- package/docs/trust-and-safety.md +45 -0
- package/package.json +83 -0
- package/plugins/autonomous-pr-loop/.codex-plugin/plugin.json +17 -0
- package/plugins/autonomous-pr-loop/.mcp.json +13 -0
- package/plugins/autonomous-pr-loop/bin/agent-loop.mjs +31 -0
- package/plugins/autonomous-pr-loop/core/artifacts.ts +164 -0
- package/plugins/autonomous-pr-loop/core/autonomy-policy.ts +206 -0
- package/plugins/autonomous-pr-loop/core/ci.ts +131 -0
- package/plugins/autonomous-pr-loop/core/cli-i18n.ts +123 -0
- package/plugins/autonomous-pr-loop/core/cli.ts +1413 -0
- package/plugins/autonomous-pr-loop/core/command-runner.ts +446 -0
- package/plugins/autonomous-pr-loop/core/command.ts +47 -0
- package/plugins/autonomous-pr-loop/core/config-editor.ts +140 -0
- package/plugins/autonomous-pr-loop/core/config.ts +293 -0
- package/plugins/autonomous-pr-loop/core/controller-host.ts +19 -0
- package/plugins/autonomous-pr-loop/core/dashboard-server.ts +536 -0
- package/plugins/autonomous-pr-loop/core/delivery-work-item.ts +217 -0
- package/plugins/autonomous-pr-loop/core/doctor.ts +335 -0
- package/plugins/autonomous-pr-loop/core/errors.ts +82 -0
- package/plugins/autonomous-pr-loop/core/gate-recovery.ts +176 -0
- package/plugins/autonomous-pr-loop/core/gates.ts +26 -0
- package/plugins/autonomous-pr-loop/core/generic-lifecycle.ts +399 -0
- package/plugins/autonomous-pr-loop/core/git.ts +213 -0
- package/plugins/autonomous-pr-loop/core/github.ts +269 -0
- package/plugins/autonomous-pr-loop/core/gitnexus.ts +90 -0
- package/plugins/autonomous-pr-loop/core/happy.ts +42 -0
- package/plugins/autonomous-pr-loop/core/hook-capture.ts +115 -0
- package/plugins/autonomous-pr-loop/core/hook-events.ts +22 -0
- package/plugins/autonomous-pr-loop/core/hook-installation.ts +85 -0
- package/plugins/autonomous-pr-loop/core/hook-observer.ts +84 -0
- package/plugins/autonomous-pr-loop/core/hook-policy.ts +423 -0
- package/plugins/autonomous-pr-loop/core/hook-router.ts +452 -0
- package/plugins/autonomous-pr-loop/core/index.ts +32 -0
- package/plugins/autonomous-pr-loop/core/local-install.ts +778 -0
- package/plugins/autonomous-pr-loop/core/locale.ts +60 -0
- package/plugins/autonomous-pr-loop/core/loop-shapes.ts +190 -0
- package/plugins/autonomous-pr-loop/core/mcp-controller.ts +1479 -0
- package/plugins/autonomous-pr-loop/core/notification-feed.ts +263 -0
- package/plugins/autonomous-pr-loop/core/plan-parser.ts +206 -0
- package/plugins/autonomous-pr-loop/core/plugin-paths.ts +32 -0
- package/plugins/autonomous-pr-loop/core/policy.ts +65 -0
- package/plugins/autonomous-pr-loop/core/pr-lifecycle.ts +464 -0
- package/plugins/autonomous-pr-loop/core/pr-selector.ts +284 -0
- package/plugins/autonomous-pr-loop/core/profiles.ts +439 -0
- package/plugins/autonomous-pr-loop/core/redaction.ts +17 -0
- package/plugins/autonomous-pr-loop/core/repo-root.ts +22 -0
- package/plugins/autonomous-pr-loop/core/review-comments.ts +77 -0
- package/plugins/autonomous-pr-loop/core/scope-guard.ts +179 -0
- package/plugins/autonomous-pr-loop/core/state-machine.ts +828 -0
- package/plugins/autonomous-pr-loop/core/state-types.ts +130 -0
- package/plugins/autonomous-pr-loop/core/storage.ts +2527 -0
- package/plugins/autonomous-pr-loop/core/types.ts +567 -0
- package/plugins/autonomous-pr-loop/core/worker-events.ts +412 -0
- package/plugins/autonomous-pr-loop/core/worker-policy.ts +72 -0
- package/plugins/autonomous-pr-loop/core/worker-prompts.ts +182 -0
- package/plugins/autonomous-pr-loop/core/worker.ts +809 -0
- package/plugins/autonomous-pr-loop/core/workflow-board.ts +1515 -0
- package/plugins/autonomous-pr-loop/hooks/dist/permission-request.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/post-tool-use.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-compact.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/pre-tool-use.js +3460 -0
- package/plugins/autonomous-pr-loop/hooks/dist/session-start.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/stop.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/dist/user-prompt-submit.js +2462 -0
- package/plugins/autonomous-pr-loop/hooks/hooks.json +106 -0
- package/plugins/autonomous-pr-loop/hooks/observe-runner.ts +25 -0
- package/plugins/autonomous-pr-loop/hooks/permission-request.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/post-tool-use.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-compact.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/pre-tool-use.ts +44 -0
- package/plugins/autonomous-pr-loop/hooks/session-start.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/stop.ts +4 -0
- package/plugins/autonomous-pr-loop/hooks/user-prompt-submit.ts +4 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/index.ts +87 -0
- package/plugins/autonomous-pr-loop/mcp-server/src/tools.ts +205 -0
- package/plugins/autonomous-pr-loop/package.json +9 -0
- package/plugins/autonomous-pr-loop/schemas/config.schema.json +74 -0
- package/plugins/autonomous-pr-loop/schemas/marketplace.schema.json +46 -0
- package/plugins/autonomous-pr-loop/schemas/plugin.schema.json +32 -0
- package/plugins/autonomous-pr-loop/schemas/state.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-event.schema.json +19 -0
- package/plugins/autonomous-pr-loop/schemas/worker-result.schema.json +58 -0
- package/plugins/autonomous-pr-loop/scripts/agent-loop.ts +44 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/SKILL.md +26 -0
- package/plugins/autonomous-pr-loop/skills/autonomous-pr-loop/agents/openai.yaml +6 -0
- package/plugins/autonomous-pr-loop/ui/index.html +26 -0
- package/plugins/autonomous-pr-loop/ui/public/favicon.svg +7 -0
- package/plugins/autonomous-pr-loop/ui/src/api.ts +639 -0
- package/plugins/autonomous-pr-loop/ui/src/app.tsx +238 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ActivityBadge.tsx +31 -0
- package/plugins/autonomous-pr-loop/ui/src/components/BrandMark.tsx +36 -0
- package/plugins/autonomous-pr-loop/ui/src/components/Collapsible.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/CommandPreview.tsx +15 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ConfigEditor.tsx +389 -0
- package/plugins/autonomous-pr-loop/ui/src/components/EmptyState.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ErrorState.tsx +12 -0
- package/plugins/autonomous-pr-loop/ui/src/components/List.tsx +7 -0
- package/plugins/autonomous-pr-loop/ui/src/components/MetricRow.tsx +6 -0
- package/plugins/autonomous-pr-loop/ui/src/components/ResponsiveTable.tsx +65 -0
- package/plugins/autonomous-pr-loop/ui/src/components/RiskBadge.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/components/StatusBadge.tsx +29 -0
- package/plugins/autonomous-pr-loop/ui/src/components/TopMetric.tsx +10 -0
- package/plugins/autonomous-pr-loop/ui/src/fixtures.ts +1152 -0
- package/plugins/autonomous-pr-loop/ui/src/i18n.ts +1105 -0
- package/plugins/autonomous-pr-loop/ui/src/main.tsx +14 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenter.tsx +470 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/CommandCenterParts.tsx +276 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/agent-timeline/AgentTimelineView.tsx +73 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/artifact-viewer/ArtifactViewer.tsx +44 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/dry-run-preview/DryRunPreview.tsx +66 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/event-ledger/EventLedger.tsx +17 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/gate-center/GateCenter.tsx +34 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/MissionControl.tsx +104 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/mission-control/WorkflowBoard.tsx +577 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/notifications/NotificationsView.tsx +30 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/plan-navigator/PlanNavigator.tsx +19 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/policy-config/PolicyConfig.tsx +22 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/pr-inbox/PrInbox.tsx +26 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/recovery-center/RecoveryCenter.tsx +125 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/scope-guard/ScopeGuard.tsx +16 -0
- package/plugins/autonomous-pr-loop/ui/src/pages/worker-runs/WorkerRuns.tsx +39 -0
- package/plugins/autonomous-pr-loop/ui/src/styles.css +2673 -0
- package/plugins/autonomous-pr-loop/ui/src/theme.ts +57 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { statePath } from "./config.js";
|
|
3
|
+
import { codexHomePath, listHookBindings, type HookBinding } from "./hook-router.js";
|
|
4
|
+
import { SqliteAgentLoopStorage } from "./storage.js";
|
|
5
|
+
|
|
6
|
+
export type HookCaptureStatus = "captured" | "not_seen" | "stale" | "ambiguous" | "unavailable";
|
|
7
|
+
|
|
8
|
+
export interface HookCaptureReport {
|
|
9
|
+
status: HookCaptureStatus;
|
|
10
|
+
reason: string;
|
|
11
|
+
currentRepoBindings?: number;
|
|
12
|
+
sessionScopedBindings?: number;
|
|
13
|
+
activeBindings?: number;
|
|
14
|
+
lastSeenAt?: string;
|
|
15
|
+
latestHookEventAt?: string;
|
|
16
|
+
latestHookEventKind?: string;
|
|
17
|
+
runId?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const RECENT_CAPTURE_MS = 5 * 60 * 1000;
|
|
21
|
+
|
|
22
|
+
export function inspectHookCapture(repoRoot: string, codexHome = codexHomePath()): HookCaptureReport {
|
|
23
|
+
let bindings: HookBinding[];
|
|
24
|
+
try {
|
|
25
|
+
bindings = listHookBindings(codexHome);
|
|
26
|
+
} catch (error) {
|
|
27
|
+
return unavailable(`Hook binding registry could not be read: ${errorMessage(error)}`);
|
|
28
|
+
}
|
|
29
|
+
const active = bindings.filter((binding) => binding.status === "active");
|
|
30
|
+
const current = active.filter((binding) => binding.repoRoot === repoRoot);
|
|
31
|
+
if (current.length === 0) {
|
|
32
|
+
return {
|
|
33
|
+
status: "unavailable",
|
|
34
|
+
reason: "No active hook binding exists for this repo.",
|
|
35
|
+
currentRepoBindings: 0,
|
|
36
|
+
sessionScopedBindings: 0,
|
|
37
|
+
activeBindings: active.length
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (current.length > 1) {
|
|
41
|
+
return {
|
|
42
|
+
status: "ambiguous",
|
|
43
|
+
reason: "Multiple active hook bindings exist for this repo.",
|
|
44
|
+
currentRepoBindings: current.length,
|
|
45
|
+
sessionScopedBindings: current.filter((binding) => binding.sessionIdHash).length,
|
|
46
|
+
activeBindings: active.length
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
const binding = current[0]!;
|
|
50
|
+
const hookEvent = latestHookEvent(repoRoot, binding.runId);
|
|
51
|
+
const hookEventRecent = hookEvent ? Date.now() - Date.parse(hookEvent.createdAt) <= RECENT_CAPTURE_MS : false;
|
|
52
|
+
const base = {
|
|
53
|
+
currentRepoBindings: current.length,
|
|
54
|
+
sessionScopedBindings: current.filter((item) => item.sessionIdHash).length,
|
|
55
|
+
activeBindings: active.length,
|
|
56
|
+
...(binding.lastSeenAt ? { lastSeenAt: binding.lastSeenAt } : {}),
|
|
57
|
+
...(hookEvent ? { latestHookEventAt: hookEvent.createdAt, latestHookEventKind: hookEvent.kind } : {}),
|
|
58
|
+
...(binding.runId ? { runId: binding.runId } : {})
|
|
59
|
+
};
|
|
60
|
+
if (hookEventRecent) {
|
|
61
|
+
return {
|
|
62
|
+
status: "captured",
|
|
63
|
+
reason: "Recent hook event was captured for this repo.",
|
|
64
|
+
...base
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
if (hookEvent) {
|
|
68
|
+
return {
|
|
69
|
+
status: "stale",
|
|
70
|
+
reason: "Hook events were captured before, but not recently.",
|
|
71
|
+
...base
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (binding.lastSeenAt) {
|
|
75
|
+
return {
|
|
76
|
+
status: "not_seen",
|
|
77
|
+
reason: "Hook routing matched this repo, but no hook event has been captured.",
|
|
78
|
+
...base
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
status: "not_seen",
|
|
83
|
+
reason: "Hook router is installed, but this repo binding has not observed the current Codex session.",
|
|
84
|
+
...base
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function latestHookEvent(repoRoot: string, runId: string | undefined): { kind: string; createdAt: string } | undefined {
|
|
89
|
+
const path = statePath(repoRoot);
|
|
90
|
+
if (!existsSync(path)) return undefined;
|
|
91
|
+
const storage = new SqliteAgentLoopStorage(path, { mode: "ro" });
|
|
92
|
+
try {
|
|
93
|
+
return storage
|
|
94
|
+
.listEvents(1000)
|
|
95
|
+
.filter((event) => event.kind.startsWith("hook_") && (!runId || event.runId === runId))
|
|
96
|
+
.map((event) => ({ kind: event.kind, createdAt: event.createdAt }))
|
|
97
|
+
.sort((left, right) => Date.parse(right.createdAt) - Date.parse(left.createdAt))[0];
|
|
98
|
+
} finally {
|
|
99
|
+
storage.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function unavailable(reason: string): HookCaptureReport {
|
|
104
|
+
return {
|
|
105
|
+
status: "unavailable",
|
|
106
|
+
reason,
|
|
107
|
+
currentRepoBindings: 0,
|
|
108
|
+
sessionScopedBindings: 0,
|
|
109
|
+
activeBindings: 0
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function errorMessage(error: unknown): string {
|
|
114
|
+
return error instanceof Error ? error.message : String(error);
|
|
115
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export const CODEX_HOOK_EVENTS = [
|
|
2
|
+
"PreToolUse",
|
|
3
|
+
"PostToolUse",
|
|
4
|
+
"UserPromptSubmit",
|
|
5
|
+
"Stop",
|
|
6
|
+
"SessionStart",
|
|
7
|
+
"PreCompact",
|
|
8
|
+
"PostCompact",
|
|
9
|
+
"PermissionRequest"
|
|
10
|
+
] as const;
|
|
11
|
+
|
|
12
|
+
export type CodexHookEvent = typeof CODEX_HOOK_EVENTS[number];
|
|
13
|
+
|
|
14
|
+
export const OBSERVE_ONLY_HOOK_EVENTS = CODEX_HOOK_EVENTS.filter((event) => event !== "PreToolUse") as Exclude<CodexHookEvent, "PreToolUse">[];
|
|
15
|
+
|
|
16
|
+
export function hookScriptName(event: CodexHookEvent): string {
|
|
17
|
+
return `${event.replaceAll(/([a-z])([A-Z])/g, "$1-$2").toLowerCase()}.js`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function hookEventKind(event: CodexHookEvent): string {
|
|
21
|
+
return `hook_${event.replaceAll(/([a-z])([A-Z])/g, "$1_$2").toLowerCase()}`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { join } from "node:path";
|
|
2
|
+
import { CODEX_HOOK_EVENTS, hookScriptName, type CodexHookEvent } from "./hook-events.js";
|
|
3
|
+
import { defaultPackageRoot, hookDistRoot } from "./plugin-paths.js";
|
|
4
|
+
|
|
5
|
+
/** Return the installed command used by Codex to invoke one agent-loop hook. */
|
|
6
|
+
export function agentLoopHookCommand(repoRoot: string, event: CodexHookEvent, packageRoot = defaultPackageRoot()): string {
|
|
7
|
+
return `AGENT_LOOP_REPO_ROOT=${shellQuote(repoRoot)} node ${shellQuote(join(hookDistRoot(packageRoot), hookScriptName(event)))}`;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Return the installed router command used by Codex to invoke one agent-loop hook. */
|
|
11
|
+
export function agentLoopRouterHookCommand(event: CodexHookEvent, packageRoot = defaultPackageRoot()): string {
|
|
12
|
+
return `node ${shellQuote(join(hookDistRoot(packageRoot), hookScriptName(event)))}`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** Return the legacy per-repo Codex hook entries installed before router isolation. */
|
|
16
|
+
export function agentLoopHookEntries(repoRoot: string, packageRoot = defaultPackageRoot()): Record<string, unknown[]> {
|
|
17
|
+
return Object.fromEntries(CODEX_HOOK_EVENTS.map((event) => [
|
|
18
|
+
event,
|
|
19
|
+
[{
|
|
20
|
+
matcher: "*",
|
|
21
|
+
hooks: [{
|
|
22
|
+
type: "command",
|
|
23
|
+
command: agentLoopHookCommand(repoRoot, event, packageRoot),
|
|
24
|
+
timeout: event === "PreToolUse" ? 1000 : 500,
|
|
25
|
+
statusMessage: hookStatusMessage(event)
|
|
26
|
+
}]
|
|
27
|
+
}]
|
|
28
|
+
]));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Return canonical router entries for `hooks.json` under the root `hooks` key. */
|
|
32
|
+
export function agentLoopRouterHookEntries(packageRoot = defaultPackageRoot()): Record<string, unknown[]> {
|
|
33
|
+
return Object.fromEntries(CODEX_HOOK_EVENTS.map((event) => [
|
|
34
|
+
event,
|
|
35
|
+
[{
|
|
36
|
+
matcher: "*",
|
|
37
|
+
hooks: [{
|
|
38
|
+
type: "command",
|
|
39
|
+
command: agentLoopRouterHookCommand(event, packageRoot),
|
|
40
|
+
timeout: event === "PreToolUse" ? 1000 : 500,
|
|
41
|
+
statusMessage: hookStatusMessage(event)
|
|
42
|
+
}]
|
|
43
|
+
}]
|
|
44
|
+
]));
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** True for agent-loop hook commands managed by this plugin. */
|
|
48
|
+
export function isAgentLoopHookCommand(command: string): boolean {
|
|
49
|
+
return command.includes("autonomous-pr-loop/hooks/dist/") && CODEX_HOOK_EVENTS.some((event) => command.includes(hookScriptName(event)));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** True for old per-repo agent-loop hook commands that used `AGENT_LOOP_REPO_ROOT`. */
|
|
53
|
+
export function isLegacyAgentLoopHookCommand(command: string): boolean {
|
|
54
|
+
return isAgentLoopHookCommand(command) && command.includes("AGENT_LOOP_REPO_ROOT=");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Recursively collect command hook strings from supported Codex hooks config shapes. */
|
|
58
|
+
export function collectHookCommands(value: unknown): string[] {
|
|
59
|
+
if (Array.isArray(value)) {
|
|
60
|
+
return value.flatMap(collectHookCommands);
|
|
61
|
+
}
|
|
62
|
+
if (typeof value !== "object" || value === null) {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
const record = value as Record<string, unknown>;
|
|
66
|
+
const direct = Array.isArray(record.hooks)
|
|
67
|
+
? record.hooks.flatMap((hook) => typeof hook === "object" && hook !== null && typeof (hook as { command?: unknown }).command === "string"
|
|
68
|
+
? [(hook as { command: string }).command]
|
|
69
|
+
: [])
|
|
70
|
+
: [];
|
|
71
|
+
return [
|
|
72
|
+
...direct,
|
|
73
|
+
...Object.values(record).flatMap(collectHookCommands)
|
|
74
|
+
];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function hookStatusMessage(event: CodexHookEvent): string {
|
|
78
|
+
return event === "PreToolUse"
|
|
79
|
+
? "Checking agent-loop command policy"
|
|
80
|
+
: `Recording agent-loop ${event} event`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shellQuote(value: string): string {
|
|
84
|
+
return `'${value.replaceAll("'", "'\\''")}'`;
|
|
85
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { isRecord, statePath } from "./config.js";
|
|
3
|
+
import { hookEventKind, type CodexHookEvent } from "./hook-events.js";
|
|
4
|
+
import { resolveHookRoute } from "./hook-router.js";
|
|
5
|
+
import { redactSecrets } from "./redaction.js";
|
|
6
|
+
import { SqliteAgentLoopStorage } from "./storage.js";
|
|
7
|
+
|
|
8
|
+
export interface ObserveHookResult {
|
|
9
|
+
continue: true;
|
|
10
|
+
observed: boolean;
|
|
11
|
+
error?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Persist a lightweight observe-only Codex hook event without affecting tool execution. */
|
|
15
|
+
export function observeCodexHook(event: CodexHookEvent, payload: unknown, repoRoot?: string): ObserveHookResult {
|
|
16
|
+
try {
|
|
17
|
+
const route = resolveHookRoute(payload, { legacyRepoRoot: repoRoot });
|
|
18
|
+
if (route.status === "no_match") {
|
|
19
|
+
return { continue: true, observed: false };
|
|
20
|
+
}
|
|
21
|
+
if (route.status === "ambiguous") {
|
|
22
|
+
return { continue: true, observed: false, error: route.reason };
|
|
23
|
+
}
|
|
24
|
+
if (route.status === "route_error") {
|
|
25
|
+
return { continue: true, observed: false, error: route.reason };
|
|
26
|
+
}
|
|
27
|
+
const storage = new SqliteAgentLoopStorage(statePath(route.binding.repoRoot));
|
|
28
|
+
try {
|
|
29
|
+
const run = route.binding.runId ? storage.listRuns(200).find((item) => item.id === route.binding.runId) : storage.getCurrentRun();
|
|
30
|
+
storage.appendEvent({
|
|
31
|
+
...(run ? { runId: run.id } : {}),
|
|
32
|
+
kind: hookEventKind(event),
|
|
33
|
+
message: `Codex ${event} hook observed.`,
|
|
34
|
+
payload: {
|
|
35
|
+
...normalizeHookPayload(event, payload),
|
|
36
|
+
hookRouting: route.legacy ? "legacy" : "binding",
|
|
37
|
+
worktreeRoot: route.context.worktreeRoot
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
} finally {
|
|
41
|
+
storage.close();
|
|
42
|
+
}
|
|
43
|
+
return { continue: true, observed: true };
|
|
44
|
+
} catch (error) {
|
|
45
|
+
return { continue: true, observed: false, error: error instanceof Error ? error.message : String(error) };
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function normalizeHookPayload(event: CodexHookEvent, payload: unknown): Record<string, unknown> {
|
|
50
|
+
const text = JSON.stringify(payload ?? {});
|
|
51
|
+
const base = {
|
|
52
|
+
event,
|
|
53
|
+
payloadLength: text.length,
|
|
54
|
+
payloadSha256: createHash("sha256").update(text).digest("hex")
|
|
55
|
+
};
|
|
56
|
+
if (event === "UserPromptSubmit" || event === "PermissionRequest") {
|
|
57
|
+
return { ...base, redacted: true };
|
|
58
|
+
}
|
|
59
|
+
if (!isRecord(payload)) {
|
|
60
|
+
return base;
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
...base,
|
|
64
|
+
redacted: true,
|
|
65
|
+
toolName: stringValue(payload.tool_name) ?? stringValue(payload.toolName) ?? stringValue(payload.tool),
|
|
66
|
+
matcher: stringValue(payload.matcher),
|
|
67
|
+
sessionIdHash: hashOptional(stringValue(payload.session_id) ?? stringValue(payload.sessionId)),
|
|
68
|
+
command: summarizeCommand(payload)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function summarizeCommand(payload: Record<string, unknown>): string | undefined {
|
|
73
|
+
const toolInput = isRecord(payload.tool_input) ? payload.tool_input : payload;
|
|
74
|
+
const command = stringValue(toolInput.command) ?? stringValue(toolInput.cmd) ?? stringValue(toolInput.input);
|
|
75
|
+
return command ? redactSecrets(command.slice(0, 500)) : undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function stringValue(value: unknown): string | undefined {
|
|
79
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function hashOptional(value: string | undefined): string | undefined {
|
|
83
|
+
return value ? createHash("sha256").update(value).digest("hex") : undefined;
|
|
84
|
+
}
|