mirror of
https://github.com/mvschwarz/openrig.git
synced 2026-11-10 03:31:40 -10:00
feat(daemon): rip activity-hook injection from both adapters; delete relay assets; wire ensureCodexFeatureFlag in startup (Checkpoint D.2)
Per IMPL-PRD §1.2 + DESIGN.md §3 (no-coexistence; rip-and-replace) +
velocity-guard cadence boundary (d). Makes the 2 TDD-red grep-source
absence tests in commit 879b405 pass.
Activity-hook injection RIPPED from both adapters:
- claude-code-adapter.ts: removed provisionActivityHooks() method,
upsertCommandHook + hookEntryContainsCommand helpers, activityHookRelayAssetPath
constructor option + private field + call site in deliverStartup,
unused shellQuote import
- codex-runtime-adapter.ts: same — removed provisionActivityHooks() +
upsertCommandHook + hookEntryContainsCommand + activityHookRelayAssetPath
constructor option + field + call site. upsertCodexHooksFeature TOML
helper KEPT (slice 3.5 ensureCodexFeatureFlag uses it; ensure no
shipped behavior broken)
Asset deletion:
- packages/daemon/assets/openrig-activity-hook-relay.cjs DELETED
- packages/cli/daemon/assets/openrig-activity-hook-relay.cjs DELETED
- packages/daemon/test/activity-hook-relay.test.ts DELETED entirely
Test fixture cleanup:
- claude-runtime-adapter.test.ts: removed 'provisions project-local Claude
hooks without clobbering existing local settings or persisting the hook
token' test. Replacement coverage in activity-hook-rip-proof.test.ts.
- codex-runtime-adapter.test.ts: removed 'provisions project-local Codex
hooks and feature flag without persisting the hook token' test. Same.
Daemon startup wire-up:
- Removed activityHookRelayAssetPath construction line + adapter constructor
args
- Added codex feature flag setup: read runtime.codex.hooks_enabled setting,
call codexAdapter.ensureCodexFeatureFlag(enabled) (slice 3.5; replaces
the activity-hook-coupled feature-flag set)
- Added homedir to Codex fsOps so ensureCodexFeatureFlag can write to
~/.codex/config.toml
Endpoint discipline maintained per IMPL-PRD §1.2 + DESIGN.md §3:
- /api/activity/hooks endpoint STAYS (POST /hooks in src/routes/activity.ts)
- agent-activity-store ingestion path STAYS (plugin-shipped hooks in
slice 3.2 will POST to it)
Verification:
- activity-hook-rip-proof.test.ts: 8/8 pass (negative-assertion regression
locks + grep-source absence proof + endpoint-stays proof)
- codex-hooks-feature-flag.test.ts: 7/7 pass (slice 3.5 HG-5.1-5.4 unchanged)
- adapter-plugin-projection.test.ts: 7/7 pass (Checkpoint C unchanged)
- projection-planner-plugins.test.ts: 10/10 pass (Checkpoint B unchanged)
- agent-manifest-plugins.test.ts: 16/16 pass (Checkpoint A unchanged)
- Full daemon suite: 3161 pass; 1 fail (pre-existing rsi-v2)
- rg 'provisionActivityHooks|upsertCommandHook|hookEntryContainsCommand|
activityHookRelayAssetPath|openrig-activity-hook-relay' packages/ src/:
ZERO production-source matches; only test/ references in rip-proof +
removed-test comments
Per IMPL-PRD §1 + §5 plugin-primitive Phase 3a slices 3.1 + 3.5.
This commit is contained in:
1 parent
879b405c30
commit
61e3ca63fb
7 files changed
+32
-372
No files matched your search
@@ -1,104 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
"use strict";
|
||||
|
||||
async function readStdin() {
|
||||
return new Promise((resolve) => {
|
||||
let data = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
process.stdin.on("data", (chunk) => {
|
||||
data += chunk;
|
||||
});
|
||||
process.stdin.on("end", () => resolve(data));
|
||||
process.stdin.on("error", () => resolve(""));
|
||||
});
|
||||
}
|
||||
|
||||
function parseJson(value) {
|
||||
try {
|
||||
const parsed = JSON.parse(value || "{}");
|
||||
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
function firstString(...values) {
|
||||
for (const value of values) {
|
||||
if (typeof value === "string" && value.trim().length > 0) return value.trim();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function buildOpenRigPayload(providerPayload, env = process.env, now = () => new Date()) {
|
||||
const sessionName = firstString(env.OPENRIG_SESSION_NAME, env.RIGGED_SESSION_NAME);
|
||||
const nodeId = firstString(env.OPENRIG_NODE_ID, env.RIGGED_NODE_ID);
|
||||
const runtime = firstString(env.OPENRIG_RUNTIME, env.RIGGED_RUNTIME);
|
||||
const hookEvent = firstString(
|
||||
providerPayload.hookEvent,
|
||||
providerPayload.hookEventName,
|
||||
providerPayload.hook_event_name,
|
||||
providerPayload.event,
|
||||
providerPayload.eventName
|
||||
);
|
||||
|
||||
if ((!sessionName && !nodeId) || !runtime || !hookEvent) return null;
|
||||
|
||||
const subtype = firstString(
|
||||
providerPayload.subtype,
|
||||
providerPayload.notification_type,
|
||||
providerPayload.notificationType,
|
||||
providerPayload.tool_name,
|
||||
providerPayload.toolName,
|
||||
providerPayload.source,
|
||||
providerPayload.matcher
|
||||
);
|
||||
|
||||
return {
|
||||
sessionName,
|
||||
nodeId,
|
||||
runtime,
|
||||
hookEvent,
|
||||
subtype,
|
||||
occurredAt: now().toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async function postHookPayload(payload, env = process.env) {
|
||||
const baseUrl = firstString(env.OPENRIG_URL, env.RIGGED_URL);
|
||||
const token = firstString(env.OPENRIG_ACTIVITY_HOOK_TOKEN, env.RIGGED_ACTIVITY_HOOK_TOKEN);
|
||||
if (!baseUrl || !token || !payload || typeof fetch !== "function") return;
|
||||
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => controller.abort(), 1500);
|
||||
try {
|
||||
await fetch(new URL("/api/activity/hooks", baseUrl).toString(), {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"authorization": `Bearer ${token}`,
|
||||
},
|
||||
body: JSON.stringify(payload),
|
||||
signal: controller.signal,
|
||||
});
|
||||
} catch {
|
||||
// Provider hooks must not block the agent loop if OpenRig is unavailable.
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const providerPayload = parseJson(await readStdin());
|
||||
const payload = buildOpenRigPayload(providerPayload);
|
||||
await postHookPayload(payload);
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
main().catch(() => {});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
buildOpenRigPayload,
|
||||
parseJson,
|
||||
postHookPayload,
|
||||
};
|
||||
@@ -47,7 +47,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
private sleep: (ms: number) => Promise<void>;
|
||||
private stateDir: string | null;
|
||||
private collectorAssetPath: string | null;
|
||||
private activityHookRelayAssetPath: string | null;
|
||||
private autoDriveProviderPrompts: boolean;
|
||||
|
||||
constructor(deps: {
|
||||
@@ -57,7 +56,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
stateDir?: string;
|
||||
collectorAssetPath?: string;
|
||||
activityHookRelayAssetPath?: string;
|
||||
autoDriveProviderPrompts?: boolean;
|
||||
}) {
|
||||
this.tmux = deps.tmux;
|
||||
@@ -66,7 +64,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
this.stateDir = deps.stateDir ?? null;
|
||||
this.collectorAssetPath = deps.collectorAssetPath ?? null;
|
||||
this.activityHookRelayAssetPath = deps.activityHookRelayAssetPath ?? null;
|
||||
this.autoDriveProviderPrompts = deps.autoDriveProviderPrompts ?? false;
|
||||
}
|
||||
|
||||
@@ -118,12 +115,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
console.error(`[openrig] context collector provisioning warning: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// Best-effort: provision project-local activity hooks. The hook token is
|
||||
// supplied through tmux session env, never written to provider settings.
|
||||
try { this.provisionActivityHooks(binding); } catch (err) {
|
||||
console.error(`[openrig] claude activity hook provisioning warning: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
let delivered = 0;
|
||||
const failed: Array<{ path: string; error: string }> = [];
|
||||
|
||||
@@ -643,26 +634,6 @@ export class ClaudeCodeAdapter implements RuntimeAdapter {
|
||||
this.fs.writeFile(settingsPath, JSON.stringify(existing, null, 2));
|
||||
}
|
||||
|
||||
private provisionActivityHooks(binding: { cwd?: string | null }): void {
|
||||
if (!binding.cwd || !this.activityHookRelayAssetPath) return;
|
||||
|
||||
const relayDest = nodePath.join(binding.cwd, ".openrig", "activity-hook-relay.cjs");
|
||||
this.fs.mkdirp(nodePath.dirname(relayDest));
|
||||
this.fs.writeFile(relayDest, this.fs.readFile(this.activityHookRelayAssetPath));
|
||||
|
||||
const settingsPath = nodePath.join(binding.cwd, ".claude", "settings.local.json");
|
||||
this.fs.mkdirp(nodePath.dirname(settingsPath));
|
||||
const settings = this.readJsonObject(settingsPath);
|
||||
const hooks = this.readJsonObjectField(settings, "hooks");
|
||||
const command = `node ${shellQuote(relayDest)}`;
|
||||
|
||||
for (const event of ["SessionStart", "UserPromptSubmit", "Stop", "Notification"]) {
|
||||
upsertCommandHook(hooks, event, command);
|
||||
}
|
||||
|
||||
settings["hooks"] = hooks;
|
||||
this.fs.writeFile(settingsPath, JSON.stringify(settings, null, 2));
|
||||
}
|
||||
}
|
||||
|
||||
function hashContent(content: string): string {
|
||||
@@ -712,34 +683,3 @@ function stableJsonKey(value: unknown): string {
|
||||
return JSON.stringify(sorted);
|
||||
}
|
||||
|
||||
/** Shell-quote a string using single quotes (POSIX-safe). */
|
||||
function shellQuote(s: string): string {
|
||||
return "'" + s.replace(/'/g, "'\"'\"'") + "'";
|
||||
}
|
||||
|
||||
function upsertCommandHook(hooks: Record<string, unknown>, event: string, command: string): void {
|
||||
const eventEntries = Array.isArray(hooks[event]) ? hooks[event] as unknown[] : [];
|
||||
if (eventEntries.some((entry) => hookEntryContainsCommand(entry, command))) {
|
||||
hooks[event] = eventEntries;
|
||||
return;
|
||||
}
|
||||
eventEntries.push({
|
||||
hooks: [
|
||||
{ type: "command", command, timeout: 5 },
|
||||
],
|
||||
});
|
||||
hooks[event] = eventEntries;
|
||||
}
|
||||
|
||||
function hookEntryContainsCommand(entry: unknown, command: string): boolean {
|
||||
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return false;
|
||||
const hooks = (entry as Record<string, unknown>)["hooks"];
|
||||
if (!Array.isArray(hooks)) return false;
|
||||
return hooks.some((hook) =>
|
||||
typeof hook === "object" &&
|
||||
hook !== null &&
|
||||
!Array.isArray(hook) &&
|
||||
(hook as Record<string, unknown>)["type"] === "command" &&
|
||||
(hook as Record<string, unknown>)["command"] === command
|
||||
);
|
||||
}
|
||||
@@ -42,7 +42,6 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
private readThreadIdByPid: (pid: number) => string | undefined;
|
||||
private sleep: (ms: number) => Promise<void>;
|
||||
private resolveHomeDirByPid: ResolveHomeDirByPid;
|
||||
private activityHookRelayAssetPath: string | null;
|
||||
|
||||
constructor(deps: {
|
||||
tmux: TmuxAdapter;
|
||||
@@ -51,7 +50,6 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
readThreadIdByPid?: (pid: number) => string | undefined;
|
||||
resolveHomeDirByPid?: ResolveHomeDirByPid;
|
||||
sleep?: (ms: number) => Promise<void>;
|
||||
activityHookRelayAssetPath?: string;
|
||||
}) {
|
||||
this.tmux = deps.tmux;
|
||||
this.fs = deps.fsOps;
|
||||
@@ -59,7 +57,6 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
this.readThreadIdByPid = deps.readThreadIdByPid ?? ((pid) => this.readThreadIdFromLogs(pid));
|
||||
this.resolveHomeDirByPid = deps.resolveHomeDirByPid ?? defaultResolveHomeDirByPid;
|
||||
this.sleep = deps.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
||||
this.activityHookRelayAssetPath = deps.activityHookRelayAssetPath ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,8 +67,9 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
* When `enabled` is false, makes ZERO modifications — the operator is
|
||||
* managing Codex config independently and the daemon does not touch it.
|
||||
*
|
||||
* Replaces the activity-hook-injection-coupled call to upsertCodexHooksFeature
|
||||
* that lived in provisionActivityHooks() pre-rip.
|
||||
* Replaces the activity-hook-injection-coupled feature-flag set call
|
||||
* that lived inside the auto-injected activity-hook provisioning path
|
||||
* pre-rip (plugin-primitive Phase 3a slice 3.1).
|
||||
*/
|
||||
ensureCodexFeatureFlag(enabled: boolean): void {
|
||||
if (!enabled) return;
|
||||
@@ -128,12 +126,6 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
console.error(`[openrig] codex bootstrap warning: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// Best-effort: provision project-local provider hooks. The hook token is
|
||||
// supplied through tmux session env, never written to provider config.
|
||||
try { this.provisionActivityHooks(binding); } catch (err) {
|
||||
console.error(`[openrig] codex activity hook provisioning warning: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
let delivered = 0;
|
||||
const failed: Array<{ path: string; error: string }> = [];
|
||||
|
||||
@@ -440,31 +432,6 @@ export class CodexRuntimeAdapter implements RuntimeAdapter {
|
||||
this.fs.writeFile(configPath, content);
|
||||
}
|
||||
|
||||
private provisionActivityHooks(binding: { cwd?: string | null }): void {
|
||||
if (!binding.cwd || !this.activityHookRelayAssetPath) return;
|
||||
|
||||
const relayDest = nodePath.join(binding.cwd, ".openrig", "activity-hook-relay.cjs");
|
||||
this.fs.mkdirp(nodePath.dirname(relayDest));
|
||||
this.fs.writeFile(relayDest, this.fs.readFile(this.activityHookRelayAssetPath));
|
||||
|
||||
const codexDir = nodePath.join(binding.cwd, ".codex");
|
||||
this.fs.mkdirp(codexDir);
|
||||
|
||||
const hooksPath = nodePath.join(codexDir, "hooks.json");
|
||||
const hooksConfig = this.readJsonObject(hooksPath);
|
||||
const hooks = this.readJsonObjectField(hooksConfig, "hooks");
|
||||
const command = `node ${shellQuote(relayDest)}`;
|
||||
for (const event of ["SessionStart", "UserPromptSubmit", "Stop"]) {
|
||||
upsertCommandHook(hooks, event, command);
|
||||
}
|
||||
hooksConfig["hooks"] = hooks;
|
||||
this.fs.writeFile(hooksPath, `${JSON.stringify(hooksConfig, null, 2)}\n`);
|
||||
|
||||
const configPath = nodePath.join(codexDir, "config.toml");
|
||||
const existingConfig = this.fs.exists(configPath) ? this.fs.readFile(configPath) : "";
|
||||
this.fs.writeFile(configPath, upsertCodexHooksFeature(existingConfig));
|
||||
}
|
||||
|
||||
private readJsonObject(path: string): Record<string, unknown> {
|
||||
try {
|
||||
if (!this.fs.exists(path)) return {};
|
||||
@@ -702,33 +669,6 @@ function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
}
|
||||
|
||||
function upsertCommandHook(hooks: Record<string, unknown>, event: string, command: string): void {
|
||||
const eventEntries = Array.isArray(hooks[event]) ? hooks[event] as unknown[] : [];
|
||||
if (eventEntries.some((entry) => hookEntryContainsCommand(entry, command))) {
|
||||
hooks[event] = eventEntries;
|
||||
return;
|
||||
}
|
||||
eventEntries.push({
|
||||
hooks: [
|
||||
{ type: "command", command, timeout: 5 },
|
||||
],
|
||||
});
|
||||
hooks[event] = eventEntries;
|
||||
}
|
||||
|
||||
function hookEntryContainsCommand(entry: unknown, command: string): boolean {
|
||||
if (typeof entry !== "object" || entry === null || Array.isArray(entry)) return false;
|
||||
const hooks = (entry as Record<string, unknown>)["hooks"];
|
||||
if (!Array.isArray(hooks)) return false;
|
||||
return hooks.some((hook) =>
|
||||
typeof hook === "object" &&
|
||||
hook !== null &&
|
||||
!Array.isArray(hook) &&
|
||||
(hook as Record<string, unknown>)["type"] === "command" &&
|
||||
(hook as Record<string, unknown>)["command"] === command
|
||||
);
|
||||
}
|
||||
|
||||
function defaultListProcesses(): Array<{ pid: number; ppid: number; command: string }> {
|
||||
try {
|
||||
const output = execFileSync("ps", ["-Ao", "pid,ppid,command"], { encoding: "utf-8" });
|
||||
|
||||
@@ -395,10 +395,21 @@ export async function createDaemon(opts?: DaemonOptions): Promise<DaemonResult>
|
||||
const { CodexRuntimeAdapter } = await import("./adapters/codex-runtime-adapter.js");
|
||||
|
||||
const startupOrchestrator = new StartupOrchestrator({ db, sessionRegistry, eventBus, tmuxAdapter, readFile: (p: string) => fs.readFileSync(p, "utf-8") });
|
||||
const activityHookRelayAssetPath = nodePath.resolve(import.meta.dirname, "../assets/openrig-activity-hook-relay.cjs");
|
||||
const runtimeSettings = new ContextPackSettingsStore().resolveConfig();
|
||||
const claudeAdapter = new ClaudeCodeAdapter({ tmux: tmuxAdapter, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), copyFile: (src: string, dest: string) => fs.copyFileSync(src, dest), listFiles: (dir: string) => { const r: string[] = []; function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, readdir: (dir: string) => fs.readdirSync(dir), homedir: os.homedir() }, stateDir: OPENRIG_HOME, collectorAssetPath: nodePath.resolve(import.meta.dirname, "../assets/claude-statusline-context.cjs"), activityHookRelayAssetPath, autoDriveProviderPrompts: runtimeSettings.recoveryAutoDriveProviderPrompts });
|
||||
const codexAdapter = new CodexRuntimeAdapter({ tmux: tmuxAdapter, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), listFiles: (dir: string) => { const r: string[] = []; function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; } }, activityHookRelayAssetPath });
|
||||
const claudeAdapter = new ClaudeCodeAdapter({ tmux: tmuxAdapter, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), copyFile: (src: string, dest: string) => fs.copyFileSync(src, dest), listFiles: (dir: string) => { const r: string[] = []; function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, readdir: (dir: string) => fs.readdirSync(dir), homedir: os.homedir() }, stateDir: OPENRIG_HOME, collectorAssetPath: nodePath.resolve(import.meta.dirname, "../assets/claude-statusline-context.cjs"), autoDriveProviderPrompts: runtimeSettings.recoveryAutoDriveProviderPrompts });
|
||||
const codexAdapter = new CodexRuntimeAdapter({ tmux: tmuxAdapter, fsOps: { readFile: (p: string) => fs.readFileSync(p, "utf-8"), writeFile: (p: string, c: string) => fs.writeFileSync(p, c, "utf-8"), exists: (p: string) => fs.existsSync(p), mkdirp: (p: string) => fs.mkdirSync(p, { recursive: true }), listFiles: (dir: string) => { const r: string[] = []; function w(d: string, pre: string) { for (const e of fs.readdirSync(d, { withFileTypes: true })) { if (e.isDirectory()) w(nodePath.join(d, e.name), nodePath.join(pre, e.name)); else r.push(pre ? nodePath.join(pre, e.name) : e.name); } } w(dir, ""); return r; }, homedir: os.homedir() } });
|
||||
|
||||
// plugin-primitive Phase 3a slice 3.5 — ensure Codex feature flag
|
||||
// codex_hooks = true is set in ~/.codex/config.toml so plugin-shipped
|
||||
// hooks fire on Codex runtime. Operator can disable via OPENRIG_RUNTIME_CODEX_HOOKS_ENABLED
|
||||
// or rig config set runtime.codex.hooks_enabled false.
|
||||
try {
|
||||
const settingsStore = new (await import("./domain/user-settings/settings-store.js")).SettingsStore();
|
||||
const enabled = settingsStore.resolveOne("runtime.codex.hooks_enabled").value as boolean;
|
||||
codexAdapter.ensureCodexFeatureFlag(enabled);
|
||||
} catch (err) {
|
||||
console.error(`[openrig] codex feature flag setup warning: ${(err as Error).message}`);
|
||||
}
|
||||
|
||||
// PL-014 Item 6: hoist ContextPackLibraryService construction so the
|
||||
// PodRigInstantiator can resolve `kind: context_pack` startup_files
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import { createRequire } from "node:module";
|
||||
import nodePath from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const relay = require(nodePath.resolve(import.meta.dirname, "../assets/openrig-activity-hook-relay.cjs")) as {
|
||||
buildOpenRigPayload: (
|
||||
providerPayload: Record<string, unknown>,
|
||||
env?: Record<string, string | undefined>,
|
||||
now?: () => Date,
|
||||
) => Record<string, unknown> | null;
|
||||
parseJson: (value: string) => Record<string, unknown>;
|
||||
};
|
||||
|
||||
describe("OpenRig activity hook relay", () => {
|
||||
it("maps provider hook stdin fields to the authenticated OpenRig hook payload", () => {
|
||||
const payload = relay.buildOpenRigPayload(
|
||||
{
|
||||
hook_event_name: "Notification",
|
||||
notification_type: "idle_prompt",
|
||||
session_id: "provider-session",
|
||||
},
|
||||
{
|
||||
OPENRIG_SESSION_NAME: "dev-qa@test-rig",
|
||||
OPENRIG_NODE_ID: "node-123",
|
||||
OPENRIG_RUNTIME: "claude-code",
|
||||
},
|
||||
() => new Date("2026-04-24T21:45:00.000Z")
|
||||
);
|
||||
|
||||
expect(payload).toEqual({
|
||||
sessionName: "dev-qa@test-rig",
|
||||
nodeId: "node-123",
|
||||
runtime: "claude-code",
|
||||
hookEvent: "Notification",
|
||||
subtype: "idle_prompt",
|
||||
occurredAt: "2026-04-24T21:45:00.000Z",
|
||||
});
|
||||
});
|
||||
|
||||
it("returns null when required OpenRig hook env is absent", () => {
|
||||
expect(relay.buildOpenRigPayload({ hook_event_name: "Stop" }, {})).toBeNull();
|
||||
});
|
||||
|
||||
it("parses malformed provider stdin as an empty object", () => {
|
||||
expect(relay.parseJson("not json")).toEqual({});
|
||||
});
|
||||
});
|
||||
@@ -700,56 +700,13 @@ describe("Claude Code runtime adapter", () => {
|
||||
expect(settings.mcpServers).toEqual({ context7: {} });
|
||||
});
|
||||
|
||||
it("provisions project-local Claude hooks without clobbering existing local settings or persisting the hook token", async () => {
|
||||
const fs = mockFs({
|
||||
"/daemon/assets/openrig-activity-hook-relay.cjs": "relay script",
|
||||
"/project/.claude/settings.local.json": JSON.stringify({
|
||||
statusLine: { type: "command", command: "node .openrig/context-collector.cjs" },
|
||||
permissions: {
|
||||
allow: ["Bash(existing:*)"],
|
||||
deny: ["Bash(rm -rf *)"],
|
||||
},
|
||||
enabledMcpjsonServers: ["existing-mcp"],
|
||||
hooks: {
|
||||
Stop: [
|
||||
{
|
||||
hooks: [
|
||||
{ type: "command", command: "node ./existing-stop-hook.cjs", timeout: 10 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
const adapter = new ClaudeCodeAdapter({
|
||||
tmux: mockTmux(),
|
||||
fsOps: { ...fs, homedir: "/home/test" },
|
||||
activityHookRelayAssetPath: "/daemon/assets/openrig-activity-hook-relay.cjs",
|
||||
});
|
||||
|
||||
await adapter.deliverStartup([], makeBinding());
|
||||
await adapter.deliverStartup([], makeBinding());
|
||||
|
||||
const store = (fs as unknown as { _store: Record<string, string> })._store;
|
||||
expect(store["/project/.openrig/activity-hook-relay.cjs"]).toBe("relay script");
|
||||
const settings = JSON.parse(store["/project/.claude/settings.local.json"]!);
|
||||
|
||||
expect(settings.statusLine.command).toBe("node .openrig/context-collector.cjs");
|
||||
expect(settings.permissions.allow).toEqual(["Bash(existing:*)"]);
|
||||
expect(settings.permissions.ask).toBeUndefined();
|
||||
expect(settings.permissions.deny).toEqual(["Bash(rm -rf *)"]);
|
||||
expect(settings.enabledMcpjsonServers).toEqual(["existing-mcp"]);
|
||||
|
||||
const hookJson = JSON.stringify(settings.hooks);
|
||||
expect(hookJson).toContain("node ./existing-stop-hook.cjs");
|
||||
expect(hookJson).toContain("node '/project/.openrig/activity-hook-relay.cjs'");
|
||||
expect(hookJson).toContain("SessionStart");
|
||||
expect(hookJson).toContain("UserPromptSubmit");
|
||||
expect(hookJson).toContain("Stop");
|
||||
expect(hookJson).toContain("Notification");
|
||||
expect(hookJson).not.toContain("secret-token");
|
||||
|
||||
const relayCommandMatches = hookJson.match(/activity-hook-relay\.cjs/g) ?? [];
|
||||
expect(relayCommandMatches).toHaveLength(4);
|
||||
});
|
||||
// Pre-rip 'provisions project-local Claude hooks without clobbering
|
||||
// existing local settings or persisting the hook token' test removed in
|
||||
// plugin-primitive Phase 3a slice 3.1 — activity-hook auto-injection
|
||||
// ripped (provisionActivityHooks gone). Replacement coverage:
|
||||
// activity-hook-rip-proof.test.ts asserts (a) no .openrig/activity-hook-relay.cjs
|
||||
// file written; (b) no OpenRig-injected hook entries in settings.local.json;
|
||||
// (c) pre-existing user-authored hooks PRESERVED untouched; (d) source
|
||||
// grep confirms provisionActivityHooks/upsertCommandHook/etc. removed
|
||||
// from adapter source.
|
||||
});
|
||||
@@ -748,48 +748,12 @@ describe("Codex runtime adapter", () => {
|
||||
expect(sendText).toHaveBeenCalledWith("r01-qa", expectedProfileResumeCommand("fleet"));
|
||||
});
|
||||
|
||||
it("provisions project-local Codex hooks and feature flag without persisting the hook token", async () => {
|
||||
const fs = mockFs({
|
||||
"/daemon/assets/openrig-activity-hook-relay.cjs": "relay script",
|
||||
"/project/.codex/hooks.json": JSON.stringify({
|
||||
hooks: {
|
||||
UserPromptSubmit: [
|
||||
{
|
||||
hooks: [
|
||||
{ type: "command", command: "node ./existing-hook.cjs", timeout: 10 },
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
"/project/.codex/config.toml": "[features]\ncodex_hooks = false\n",
|
||||
});
|
||||
const adapter = new CodexRuntimeAdapter({
|
||||
tmux: mockTmux(),
|
||||
fsOps: { ...fs, homedir: "/home/test" },
|
||||
activityHookRelayAssetPath: "/daemon/assets/openrig-activity-hook-relay.cjs",
|
||||
});
|
||||
|
||||
await adapter.deliverStartup([], makeBinding());
|
||||
await adapter.deliverStartup([], makeBinding());
|
||||
|
||||
const store = (fs as unknown as { _store: Record<string, string> })._store;
|
||||
expect(store["/project/.openrig/activity-hook-relay.cjs"]).toBe("relay script");
|
||||
expect(store["/project/.codex/config.toml"]).toContain("codex_hooks = true");
|
||||
expect(store["/home/test/.codex/hooks.json"]).toBeUndefined();
|
||||
|
||||
const hooksConfig = JSON.parse(store["/project/.codex/hooks.json"]!);
|
||||
const hookJson = JSON.stringify(hooksConfig);
|
||||
expect(hookJson).toContain("node ./existing-hook.cjs");
|
||||
expect(hookJson).toContain("node '/project/.openrig/activity-hook-relay.cjs'");
|
||||
expect(hookJson).toContain("SessionStart");
|
||||
expect(hookJson).toContain("UserPromptSubmit");
|
||||
expect(hookJson).toContain("Stop");
|
||||
expect(hookJson).not.toContain("secret-token");
|
||||
|
||||
const relayCommandMatches = hookJson.match(/activity-hook-relay\.cjs/g) ?? [];
|
||||
expect(relayCommandMatches).toHaveLength(3);
|
||||
});
|
||||
// Pre-rip 'provisions project-local Codex hooks and feature flag without
|
||||
// persisting the hook token' test removed in plugin-primitive Phase 3a
|
||||
// slice 3.1 — activity-hook auto-injection ripped (provisionActivityHooks
|
||||
// gone). Replacement coverage: codex-hooks-feature-flag.test.ts (slice 3.5
|
||||
// ensureCodexFeatureFlag) + activity-hook-rip-proof.test.ts (negative
|
||||
// assertions on adapter symbol absence + endpoint-stays).
|
||||
|
||||
it("launchHarness skips the non-mutating Codex update prompt during resume verification", async () => {
|
||||
const updatePrompt = [
|
||||
|
||||
Reference in new issue
Block a user