agent-blocked 0.1.5 → 0.1.7

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/README.md CHANGED
@@ -16,7 +16,9 @@ Report a blocker manually:
16
16
  npx agent-blocked@latest report --event=needs_direction --severity=medium --reason="Need human direction"
17
17
  ```
18
18
 
19
- The installer adds a small `.agent-blocked/` directory plus tool-specific config or instruction files for the selected agent tools. Those files tell the agent when to escalate during goal mode, autonomous mode, background execution, hook-driven automation, CI-style runs, or another CLI-specific automated mode. They also provide a local reporter command that sends blocker events to your Agent Blocked alert profile.
19
+ The installer adds a small `.agent-blocked/` directory plus tool-specific config, hooks, or instruction files for the selected agent tools. Those files tell the agent when to escalate during goal mode, autonomous mode, background execution, hook-driven automation, CI-style runs, or another CLI-specific automated mode. They also provide a local reporter command that sends blocker events to your Agent Blocked alert profile.
20
+
21
+ For tools with native hooks, such as Claude Code and Codex, installing the adapter turns the hook on for that project. Make sure `AGENT_BLOCKED_AGENT_TOKEN` is available in the shell or secret store used by the agent.
20
22
 
21
23
  Agents should not notify during normal interactive paired work when the human is already present and responding in the current conversation. In automated modes, agents should notify when continuing would waste time, create risk, or require a decision only the human can make:
22
24
 
@@ -39,7 +41,9 @@ Agents can report through the installed local reporter:
39
41
  node .agent-blocked/report.mjs --event=needs_credentials --severity=critical --reason="Missing deploy credentials"
40
42
  ```
41
43
 
42
- Codex can also be run through the wrapper so hard stops are reported automatically:
44
+ Codex hook reporting is installed by `npx agent-blocked@latest install --tool=codex`. Restart Codex or start a new Codex session after installation so the hook config is loaded.
45
+
46
+ Codex can also be run through the wrapper when you want hard process exits reported:
43
47
 
44
48
  ```bash
45
49
  npx agent-blocked@latest codex -- codex "implement the requested change"
@@ -154,6 +154,119 @@ try {
154
154
  }
155
155
  `;
156
156
 
157
+ const codexHookScript = `#!/usr/bin/env node
158
+
159
+ let input = "";
160
+ process.stdin.setEncoding("utf8");
161
+ for await (const chunk of process.stdin) input += chunk;
162
+
163
+ let event = {};
164
+ try {
165
+ event = input ? JSON.parse(input) : {};
166
+ } catch {
167
+ event = {};
168
+ }
169
+
170
+ const webhookUrl = process.env.AGENT_BLOCKED_WEBHOOK_URL || "https://agentblocked.com/api/agent-blocked";
171
+ const token = process.env.AGENT_BLOCKED_AGENT_TOKEN;
172
+ const agentId = process.env.AGENT_NAME || "codex";
173
+
174
+ if (!token) process.exit(0);
175
+
176
+ function pick(...values) {
177
+ return values.find((value) => value !== undefined && value !== null && value !== "");
178
+ }
179
+
180
+ function hookEventName(payload) {
181
+ return pick(payload.hook_event_name, payload.hookEventName, payload.event_name, payload.eventName, payload.event, payload.type, "");
182
+ }
183
+
184
+ function toolName(payload) {
185
+ return pick(payload.tool_name, payload.toolName, payload.tool, payload.tool_call?.name, payload.toolCall?.name, "");
186
+ }
187
+
188
+ function exitCode(payload) {
189
+ return pick(
190
+ payload.exit_code,
191
+ payload.exitCode,
192
+ payload.result?.exit_code,
193
+ payload.result?.exitCode,
194
+ payload.tool_result?.exit_code,
195
+ payload.toolResult?.exitCode
196
+ );
197
+ }
198
+
199
+ function failed(payload) {
200
+ if (payload.success === false || payload.ok === false) return true;
201
+ if (payload.error || payload.error_message || payload.errorMessage) return true;
202
+ const code = exitCode(payload);
203
+ return code !== undefined && code !== null && Number(code) !== 0;
204
+ }
205
+
206
+ function commandText(payload) {
207
+ return pick(
208
+ payload.command,
209
+ payload.cmd,
210
+ payload.tool_input?.cmd,
211
+ payload.toolInput?.cmd,
212
+ payload.tool_input?.command,
213
+ payload.toolInput?.command,
214
+ ""
215
+ );
216
+ }
217
+
218
+ const eventName = hookEventName(event);
219
+ const tool = toolName(event);
220
+ let payload = null;
221
+
222
+ if (eventName === "PermissionRequest" || /permission/i.test(eventName)) {
223
+ payload = {
224
+ eventType: "approval_required",
225
+ severity: "medium",
226
+ reason: \`Codex requested approval\${tool ? \` for \${tool}\` : ""}.\`
227
+ };
228
+ } else if ((eventName === "PostToolUse" || /post.*tool/i.test(eventName)) && failed(event)) {
229
+ const code = exitCode(event);
230
+ payload = {
231
+ eventType: "tool_failure",
232
+ severity: "medium",
233
+ reason: \`Codex tool failed\${tool ? \` during \${tool}\` : ""}\${code !== undefined ? \` with exit code \${code}\` : ""}.\`
234
+ };
235
+ }
236
+
237
+ if (!payload) process.exit(0);
238
+
239
+ const details = {
240
+ hookEventName: eventName || "unknown",
241
+ toolName: tool || undefined,
242
+ command: commandText(event) || undefined,
243
+ cwd: event.cwd,
244
+ sessionId: event.session_id || event.sessionId,
245
+ threadId: event.thread_id || event.threadId,
246
+ exitCode: exitCode(event)
247
+ };
248
+
249
+ try {
250
+ await fetch(webhookUrl, {
251
+ method: "POST",
252
+ headers: {
253
+ authorization: \`Bearer \${token}\`,
254
+ "content-type": "application/json"
255
+ },
256
+ body: JSON.stringify({
257
+ agentId,
258
+ ...payload,
259
+ details: JSON.stringify(details),
260
+ provider: "openai",
261
+ model: "codex",
262
+ runId: details.sessionId || details.threadId
263
+ })
264
+ });
265
+ } catch {
266
+ // Hooks should not break Codex when notification delivery fails.
267
+ }
268
+ `;
269
+
157
270
  const instructions = `# Agent Blocked instructions for coding agents
158
271
 
159
272
  Use Agent Blocked only when this agent is running without an actively engaged human in the loop, such as goal mode, autonomous mode, background execution, hook-driven automation, CI-style runs, or another CLI-specific automated mode.
@@ -176,6 +289,14 @@ Environment expected by the report command:
176
289
  export AGENT_BLOCKED_AGENT_TOKEN="your-scoped-token"
177
290
  \`\`\`
178
291
 
292
+ For Codex hook-based automatic reporting, install the Codex adapter and restart Codex or start a new Codex session so the hook config is loaded:
293
+
294
+ \`\`\`bash
295
+ npx agent-blocked@latest install --tool=codex
296
+ \`\`\`
297
+
298
+ The hook only reports matching hook events such as approval requests and failed tool calls.
299
+
179
300
  Event types:
180
301
 
181
302
  - hard_blocked: no productive path remains without human input.
@@ -250,6 +371,37 @@ function mergeClaudeSettings(path, settings) {
250
371
  console.log(`Updated ${path} with Agent Blocked hooks.`);
251
372
  }
252
373
 
374
+ function mergeCodexHooks(path, config) {
375
+ let currentConfig = {};
376
+ let currentHooks = {};
377
+
378
+ if (existsSync(path)) {
379
+ const existingRaw = readFileSync(path, "utf8");
380
+ try {
381
+ const parsed = JSON.parse(existingRaw);
382
+ currentConfig = parsed && typeof parsed === "object" ? parsed : {};
383
+ currentHooks = currentConfig.hooks && typeof currentConfig.hooks === "object" ? currentConfig.hooks : {};
384
+ } catch {
385
+ console.log(`Could not parse ${path}. Wrote example file for manual merge instead.`);
386
+ writeFileIfMissing(join(root, ".codex", "hooks.agent-blocked.example.json"), `${JSON.stringify(config, null, 2)}\n`);
387
+ return;
388
+ }
389
+ }
390
+
391
+ const mergedHooks = { ...currentHooks };
392
+ for (const [eventName, hooks] of Object.entries(config.hooks || {})) {
393
+ mergedHooks[eventName] = mergeHookEntries(
394
+ Array.isArray(mergedHooks[eventName]) ? mergedHooks[eventName] : [],
395
+ hooks || []
396
+ );
397
+ }
398
+
399
+ currentConfig.hooks = mergedHooks;
400
+ mkdirSync(dirname(path), { recursive: true });
401
+ writeFileSync(path, `${JSON.stringify(currentConfig, null, 2)}\n`);
402
+ console.log(`Updated ${path} with Agent Blocked hooks.`);
403
+ }
404
+
253
405
  function upsertMarkedSection(path, title) {
254
406
  const section = `${markerStart}
255
407
  ## ${title}
@@ -295,6 +447,26 @@ function installClaude() {
295
447
  }
296
448
 
297
449
  function installCodex() {
450
+ writeFileIfMissing(join(root, ".agent-blocked", "codex-hook.mjs"), codexHookScript);
451
+ const hooks = {
452
+ hooks: {
453
+ PermissionRequest: [
454
+ {
455
+ matcher: "*",
456
+ hooks: [{ type: "command", command: "node \"$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agent-blocked/codex-hook.mjs\"" }]
457
+ }
458
+ ],
459
+ PostToolUse: [
460
+ {
461
+ matcher: "*",
462
+ hooks: [{ type: "command", command: "node \"$(git rev-parse --show-toplevel 2>/dev/null || pwd)/.agent-blocked/codex-hook.mjs\"" }]
463
+ }
464
+ ]
465
+ }
466
+ };
467
+
468
+ writeFileIfMissing(join(root, ".codex", "hooks.agent-blocked.example.json"), `${JSON.stringify(hooks, null, 2)}\n`);
469
+ mergeCodexHooks(join(root, ".codex", "hooks.json"), hooks);
298
470
  upsertMarkedSection(join(root, "AGENTS.md"), "Codex Agent Blocked setup");
299
471
  }
300
472
 
@@ -328,6 +500,9 @@ function install() {
328
500
  }
329
501
 
330
502
  console.log("\nNext: set AGENT_BLOCKED_AGENT_TOKEN in the shell or secret store where your agent runs.");
503
+ if (selected.includes("codex")) {
504
+ console.log("Codex hook reporting is installed. Restart Codex or start a new Codex session if it was already running.");
505
+ }
331
506
  console.log("Manual report command: node .agent-blocked/report.mjs --event=needs_direction --severity=medium --reason=\"Need human direction\"");
332
507
  }
333
508
 
@@ -374,7 +549,11 @@ async function codex() {
374
549
  const command = args[0] || "codex";
375
550
  const commandArgs = args[0] ? args.slice(1) : [];
376
551
 
377
- const child = spawn(command, commandArgs, { stdio: "inherit", shell: false });
552
+ const child = spawn(command, commandArgs, {
553
+ stdio: "inherit",
554
+ shell: false,
555
+ env: process.env
556
+ });
378
557
  const code = await new Promise((resolve) => child.on("close", resolve));
379
558
 
380
559
  if (code && process.env.AGENT_BLOCKED_AGENT_TOKEN) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-blocked",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "CLI installer and reporter for Agent Blocked AI agent escalation alerts.",
5
5
  "type": "module",
6
6
  "bin": {