@webpieces/hook-runtime 0.0.1

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.
Files changed (103) hide show
  1. package/package.json +37 -0
  2. package/src/adapters/agent-adapters.d.ts +13 -0
  3. package/src/adapters/agent-adapters.js +23 -0
  4. package/src/adapters/agent-adapters.js.map +1 -0
  5. package/src/adapters/agent-payload.d.ts +48 -0
  6. package/src/adapters/agent-payload.js +30 -0
  7. package/src/adapters/agent-payload.js.map +1 -0
  8. package/src/adapters/claude-code-adapter.d.ts +26 -0
  9. package/src/adapters/claude-code-adapter.js +69 -0
  10. package/src/adapters/claude-code-adapter.js.map +1 -0
  11. package/src/adapters/codex-adapter.d.ts +19 -0
  12. package/src/adapters/codex-adapter.js +48 -0
  13. package/src/adapters/codex-adapter.js.map +1 -0
  14. package/src/adapters/detect-ai.d.ts +70 -0
  15. package/src/adapters/detect-ai.js +82 -0
  16. package/src/adapters/detect-ai.js.map +1 -0
  17. package/src/core/apply-patch-parse.d.ts +36 -0
  18. package/src/core/apply-patch-parse.js +155 -0
  19. package/src/core/apply-patch-parse.js.map +1 -0
  20. package/src/core/build-context.d.ts +11 -0
  21. package/src/core/build-context.js +69 -0
  22. package/src/core/build-context.js.map +1 -0
  23. package/src/core/command-scan.d.ts +122 -0
  24. package/src/core/command-scan.js +282 -0
  25. package/src/core/command-scan.js.map +1 -0
  26. package/src/core/custom-rule-adapter.d.ts +23 -0
  27. package/src/core/custom-rule-adapter.js +50 -0
  28. package/src/core/custom-rule-adapter.js.map +1 -0
  29. package/src/core/delete-scoped-rules.d.ts +9 -0
  30. package/src/core/delete-scoped-rules.js +31 -0
  31. package/src/core/delete-scoped-rules.js.map +1 -0
  32. package/src/core/disable-directives.d.ts +8 -0
  33. package/src/core/disable-directives.js +86 -0
  34. package/src/core/disable-directives.js.map +1 -0
  35. package/src/core/effective-tree.d.ts +197 -0
  36. package/src/core/effective-tree.js +269 -0
  37. package/src/core/effective-tree.js.map +1 -0
  38. package/src/core/excluded-paths.d.ts +23 -0
  39. package/src/core/excluded-paths.js +54 -0
  40. package/src/core/excluded-paths.js.map +1 -0
  41. package/src/core/file-evaluation.d.ts +11 -0
  42. package/src/core/file-evaluation.js +45 -0
  43. package/src/core/file-evaluation.js.map +1 -0
  44. package/src/core/fix-hint.d.ts +40 -0
  45. package/src/core/fix-hint.js +53 -0
  46. package/src/core/fix-hint.js.map +1 -0
  47. package/src/core/glob.d.ts +1 -0
  48. package/src/core/glob.js +42 -0
  49. package/src/core/glob.js.map +1 -0
  50. package/src/core/report.d.ts +21 -0
  51. package/src/core/report.js +74 -0
  52. package/src/core/report.js.map +1 -0
  53. package/src/core/root-manifest.d.ts +1 -0
  54. package/src/core/root-manifest.js +21 -0
  55. package/src/core/root-manifest.js.map +1 -0
  56. package/src/core/rule-base.d.ts +40 -0
  57. package/src/core/rule-base.js +37 -0
  58. package/src/core/rule-base.js.map +1 -0
  59. package/src/core/rule-evaluation.d.ts +18 -0
  60. package/src/core/rule-evaluation.js +179 -0
  61. package/src/core/rule-evaluation.js.map +1 -0
  62. package/src/core/rules/shell-segment-scan.d.ts +64 -0
  63. package/src/core/rules/shell-segment-scan.js +88 -0
  64. package/src/core/rules/shell-segment-scan.js.map +1 -0
  65. package/src/core/shell-read-parity.d.ts +34 -0
  66. package/src/core/shell-read-parity.js +157 -0
  67. package/src/core/shell-read-parity.js.map +1 -0
  68. package/src/core/strip-ts-noise.d.ts +1 -0
  69. package/src/core/strip-ts-noise.js +191 -0
  70. package/src/core/strip-ts-noise.js.map +1 -0
  71. package/src/core/target-tree.d.ts +84 -0
  72. package/src/core/target-tree.js +148 -0
  73. package/src/core/target-tree.js.map +1 -0
  74. package/src/core/types.d.ts +149 -0
  75. package/src/core/types.js +190 -0
  76. package/src/core/types.js.map +1 -0
  77. package/src/harness-vocabulary.d.ts +2 -0
  78. package/src/harness-vocabulary.js +10 -0
  79. package/src/harness-vocabulary.js.map +1 -0
  80. package/src/hook-app.d.ts +15 -0
  81. package/src/hook-app.js +58 -0
  82. package/src/hook-app.js.map +1 -0
  83. package/src/hook-evaluator.d.ts +5 -0
  84. package/src/hook-evaluator.js +13 -0
  85. package/src/hook-evaluator.js.map +1 -0
  86. package/src/hook-ports.d.ts +9 -0
  87. package/src/hook-ports.js +40 -0
  88. package/src/hook-ports.js.map +1 -0
  89. package/src/index.d.ts +31 -0
  90. package/src/index.js +59 -0
  91. package/src/index.js.map +1 -0
  92. package/src/outcome.d.ts +14 -0
  93. package/src/outcome.js +20 -0
  94. package/src/outcome.js.map +1 -0
  95. package/src/protocol.d.ts +40 -0
  96. package/src/protocol.js +55 -0
  97. package/src/protocol.js.map +1 -0
  98. package/src/response.d.ts +2 -0
  99. package/src/response.js +21 -0
  100. package/src/response.js.map +1 -0
  101. package/src/to-error.d.ts +1 -0
  102. package/src/to-error.js +9 -0
  103. package/src/to-error.js.map +1 -0
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@webpieces/hook-runtime",
3
+ "version": "0.0.1",
4
+ "description": "Agent hook protocol, normalized events, and fail-closed application boundary.",
5
+ "type": "commonjs",
6
+ "main": "./src/index.js",
7
+ "exports": {
8
+ ".": "./src/index.js",
9
+ "./package.json": "./package.json",
10
+ "./harness-vocabulary": "./src/harness-vocabulary.js"
11
+ },
12
+ "files": [
13
+ "src/**/*"
14
+ ],
15
+ "author": "Dean Hiller",
16
+ "license": "Apache-2.0",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/deanhiller/webpieces-ts.git",
20
+ "directory": "packages/tooling/hook-runtime"
21
+ },
22
+ "dependencies": {
23
+ "inversify": "7.10.4",
24
+ "reflect-metadata": "0.2.2",
25
+ "@webpieces/rules-config": "0.4.845",
26
+ "@webpieces/tooling-common": "0.0.1",
27
+ "tslib": "2.8.1",
28
+ "@webpieces/rules-sdk": "0.0.1"
29
+ },
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "devDependencies": {
34
+ "@webpieces/tooling-testkit": "0.0.1"
35
+ },
36
+ "types": "./src/index.d.ts"
37
+ }
@@ -0,0 +1,13 @@
1
+ import { AgentPayload } from './agent-payload';
2
+ import { AgentHookEvent } from '../protocol';
3
+ /**
4
+ * The ONE place a payload is routed to its harness's adapter. Every other module takes an
5
+ * `AgentHookEvent` and never asks which agent produced it — except the two codex-only surfaces
6
+ * (read parity, the shared-tree subagent guard), which check `aiType` explicitly and say so.
7
+ */
8
+ export declare class AgentAdapters {
9
+ private readonly claude;
10
+ private readonly codex;
11
+ envelope(payload: AgentPayload): AgentHookEvent;
12
+ toEvent(payload: AgentPayload, cwd: string): AgentHookEvent;
13
+ }
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentAdapters = void 0;
4
+ const detect_ai_1 = require("./detect-ai");
5
+ const claude_code_adapter_1 = require("./claude-code-adapter");
6
+ const codex_adapter_1 = require("./codex-adapter");
7
+ /**
8
+ * The ONE place a payload is routed to its harness's adapter. Every other module takes an
9
+ * `AgentHookEvent` and never asks which agent produced it — except the two codex-only surfaces
10
+ * (read parity, the shared-tree subagent guard), which check `aiType` explicitly and say so.
11
+ */
12
+ class AgentAdapters {
13
+ claude = new claude_code_adapter_1.ClaudeCodeAdapter();
14
+ codex = new codex_adapter_1.CodexAdapter();
15
+ envelope(payload) {
16
+ return (0, detect_ai_1.detectAiType)(payload) === 'codex' ? this.codex.envelope(payload) : this.claude.envelope(payload);
17
+ }
18
+ toEvent(payload, cwd) {
19
+ return (0, detect_ai_1.detectAiType)(payload) === 'codex' ? this.codex.toEvent(payload, cwd) : this.claude.toEvent(payload, cwd);
20
+ }
21
+ }
22
+ exports.AgentAdapters = AgentAdapters;
23
+ //# sourceMappingURL=agent-adapters.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-adapters.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/adapters/agent-adapters.ts"],"names":[],"mappings":";;;AAEA,2CAA2C;AAC3C,+DAA0D;AAC1D,mDAA+C;AAE/C;;;;GAIG;AACH,MAAa,aAAa;IACL,MAAM,GAAG,IAAI,uCAAiB,EAAE,CAAC;IACjC,KAAK,GAAG,IAAI,4BAAY,EAAE,CAAC;IAE5C,QAAQ,CAAC,OAAqB;QAC1B,OAAO,IAAA,wBAAY,EAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;IAC5G,CAAC;IAED,OAAO,CAAC,OAAqB,EAAE,GAAW;QACtC,OAAO,IAAA,wBAAY,EAAC,OAAO,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;IACpH,CAAC;CACJ;AAXD,sCAWC","sourcesContent":["import { AgentPayload } from './agent-payload';\nimport { AgentHookEvent } from '../protocol';\nimport { detectAiType } from './detect-ai';\nimport { ClaudeCodeAdapter } from './claude-code-adapter';\nimport { CodexAdapter } from './codex-adapter';\n\n/**\n * The ONE place a payload is routed to its harness's adapter. Every other module takes an\n * `AgentHookEvent` and never asks which agent produced it — except the two codex-only surfaces\n * (read parity, the shared-tree subagent guard), which check `aiType` explicitly and say so.\n */\nexport class AgentAdapters {\n private readonly claude = new ClaudeCodeAdapter();\n private readonly codex = new CodexAdapter();\n\n envelope(payload: AgentPayload): AgentHookEvent {\n return detectAiType(payload) === 'codex' ? this.codex.envelope(payload) : this.claude.envelope(payload);\n }\n\n toEvent(payload: AgentPayload, cwd: string): AgentHookEvent {\n return detectAiType(payload) === 'codex' ? this.codex.toEvent(payload, cwd) : this.claude.toEvent(payload, cwd);\n }\n}\n"]}
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The RAW PreToolUse wire envelope, as it arrives on stdin.
3
+ *
4
+ * ONE shape for both harnesses, because that is what was measured: Codex uses the SAME key names as
5
+ * Claude Code (`hook_event_name`, `tool_name`, `tool_input`, `cwd`, `session_id`, `transcript_path`,
6
+ * `agent_id`, `agent_type`) and merely ADDS `model`, `turn_id` and `tool_use_id`. A second payload
7
+ * type for the second harness would be two spellings of one thing.
8
+ *
9
+ * Interfaces, not classes, and deliberately so: nothing in this codebase ever CONSTRUCTS one of these.
10
+ * They describe bytes somebody else wrote, which `JSON.parse` hands back as a plain object — the same
11
+ * reason the shape this replaces was an interface.
12
+ */
13
+ export interface AgentPayload {
14
+ tool_name: string;
15
+ tool_input: AgentToolInput;
16
+ /** The session's current working directory. Used to scope guards to the tree the agent is in. */
17
+ cwd?: string;
18
+ session_id?: string;
19
+ /** Empty/absent ⇒ the coordinator, populated ⇒ a subagent. MEASURED identical in both harnesses. */
20
+ agent_id?: string;
21
+ agent_type?: string;
22
+ /** Codex-only, and REQUIRED there. The one discriminator — see ./detect-ai.ts. */
23
+ turn_id?: string;
24
+ }
25
+ export interface AgentToolInput {
26
+ file_path?: string;
27
+ content?: string;
28
+ old_string?: string;
29
+ new_string?: string;
30
+ edits?: AgentEditEntry[];
31
+ command?: string;
32
+ }
33
+ export interface AgentEditEntry {
34
+ old_string?: string;
35
+ new_string?: string;
36
+ }
37
+ export declare class AgentPayloadParser {
38
+ /**
39
+ * Returns null for empty stdin (nothing to judge); throws InformAiError on unparseable bytes.
40
+ *
41
+ * The message still names Claude Code even though this path is shared, and that is DELIBERATE and
42
+ * temporary: the overriding constraint on this change is that no Claude Code behaviour moves, and a
43
+ * user-visible string is behaviour. Renaming it is a one-line follow-up once the Codex path is
44
+ * actually armed (it cannot mislead anyone before then — no Codex session reaches this code until
45
+ * the installer lands).
46
+ */
47
+ parse(raw: string): AgentPayload | null;
48
+ }
@@ -0,0 +1,30 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AgentPayloadParser = void 0;
4
+ const tooling_common_1 = require("@webpieces/tooling-common");
5
+ const to_error_1 = require("@webpieces/tooling-common/to-error");
6
+ class AgentPayloadParser {
7
+ /**
8
+ * Returns null for empty stdin (nothing to judge); throws InformAiError on unparseable bytes.
9
+ *
10
+ * The message still names Claude Code even though this path is shared, and that is DELIBERATE and
11
+ * temporary: the overriding constraint on this change is that no Claude Code behaviour moves, and a
12
+ * user-visible string is behaviour. Renaming it is a one-line follow-up once the Codex path is
13
+ * actually armed (it cannot mislead anyone before then — no Codex session reaches this code until
14
+ * the installer lands).
15
+ */
16
+ parse(raw) {
17
+ if (!raw || raw.trim() === '')
18
+ return null;
19
+ // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
20
+ try {
21
+ return JSON.parse(raw);
22
+ }
23
+ catch (err) {
24
+ const error = (0, to_error_1.toError)(err);
25
+ throw new tooling_common_1.InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });
26
+ }
27
+ }
28
+ }
29
+ exports.AgentPayloadParser = AgentPayloadParser;
30
+ //# sourceMappingURL=agent-payload.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"agent-payload.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/adapters/agent-payload.ts"],"names":[],"mappings":";;;AAAA,8DAA0D;AAC1D,iEAA6D;AAyC7D,MAAa,kBAAkB;IAC3B;;;;;;;;OAQG;IACH,KAAK,CAAC,GAAW;QACb,IAAI,CAAC,GAAG,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC3C,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAiB,CAAC;QAC3C,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,IAAA,kBAAO,EAAC,GAAG,CAAC,CAAC;YAC3B,MAAM,IAAI,8BAAa,CAAC,gDAAgD,KAAK,CAAC,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC;QAC/G,CAAC;IACL,CAAC;CACJ;AApBD,gDAoBC","sourcesContent":["import { InformAiError } from '@webpieces/tooling-common';\nimport { toError } from '@webpieces/tooling-common/to-error';\n\n/**\n * The RAW PreToolUse wire envelope, as it arrives on stdin.\n *\n * ONE shape for both harnesses, because that is what was measured: Codex uses the SAME key names as\n * Claude Code (`hook_event_name`, `tool_name`, `tool_input`, `cwd`, `session_id`, `transcript_path`,\n * `agent_id`, `agent_type`) and merely ADDS `model`, `turn_id` and `tool_use_id`. A second payload\n * type for the second harness would be two spellings of one thing.\n *\n * Interfaces, not classes, and deliberately so: nothing in this codebase ever CONSTRUCTS one of these.\n * They describe bytes somebody else wrote, which `JSON.parse` hands back as a plain object — the same\n * reason the shape this replaces was an interface.\n */\nexport interface AgentPayload {\n tool_name: string;\n tool_input: AgentToolInput;\n /** The session's current working directory. Used to scope guards to the tree the agent is in. */\n cwd?: string;\n session_id?: string;\n /** Empty/absent ⇒ the coordinator, populated ⇒ a subagent. MEASURED identical in both harnesses. */\n agent_id?: string;\n agent_type?: string;\n /** Codex-only, and REQUIRED there. The one discriminator — see ./detect-ai.ts. */\n turn_id?: string;\n}\n\nexport interface AgentToolInput {\n file_path?: string;\n content?: string;\n old_string?: string;\n new_string?: string;\n edits?: AgentEditEntry[];\n command?: string;\n}\n\nexport interface AgentEditEntry {\n old_string?: string;\n new_string?: string;\n}\n\nexport class AgentPayloadParser {\n /**\n * Returns null for empty stdin (nothing to judge); throws InformAiError on unparseable bytes.\n *\n * The message still names Claude Code even though this path is shared, and that is DELIBERATE and\n * temporary: the overriding constraint on this change is that no Claude Code behaviour moves, and a\n * user-visible string is behaviour. Renaming it is a one-line follow-up once the Codex path is\n * actually armed (it cannot mislead anyone before then — no Codex session reaches this code until\n * the installer lands).\n */\n parse(raw: string): AgentPayload | null {\n if (!raw || raw.trim() === '') return null;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as AgentPayload;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(`Malformed hook input from Claude Code stdin: ${error.message}`, { cause: error });\n }\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ import { AgentPayload } from './agent-payload';
2
+ import { AgentHookEvent } from '../protocol';
3
+ /**
4
+ * Morphs a Claude Code PreToolUse payload into the one normalized `AgentHookEvent`.
5
+ *
6
+ * This is the SAME `normalizeToolKind` / `normalizeToolInput` logic that used to live inline in
7
+ * hook-core.ts, moved out unchanged so hook-core is written once for every harness. Claude Code is the
8
+ * harness every developer uses today, so nothing about the mapping is allowed to move: `Write` still
9
+ * becomes one edit of `content` against '', `Edit` one edit of `old_string`→`new_string`, `MultiEdit`
10
+ * one per entry, and a file tool with no `file_path` still ends up allowed (kind `Ignored`).
11
+ */
12
+ export declare class ClaudeCodeAdapter {
13
+ /**
14
+ * What is known from the ENVELOPE ALONE, touching nothing but `tool_name` and the identity fields.
15
+ *
16
+ * Not a second spelling of `toEvent` — a different question, asked at a moment when the answer to
17
+ * the other one may not exist. `toEvent` reads `tool_input`, and a payload whose `tool_input` is
18
+ * missing makes it throw; the crash then still has to be DENIED, and the deny still has to know
19
+ * whether it is decorating a Bash block (which needs the red `systemMessage`) or a file block
20
+ * (which does not). This is the shape that answers that, and it cannot fail.
21
+ */
22
+ envelope(payload: AgentPayload): AgentHookEvent;
23
+ toEvent(payload: AgentPayload, cwd: string): AgentHookEvent;
24
+ private kindOf;
25
+ private fileOperations;
26
+ }
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ClaudeCodeAdapter = void 0;
4
+ const protocol_1 = require("../protocol");
5
+ const protocol_2 = require("../protocol");
6
+ /**
7
+ * The Claude Code tools that enter the file/edit rule pipeline. `Read` is deliberately NOT here — it
8
+ * has its own fast path and only one guard may see it.
9
+ */
10
+ const HANDLED_FILE_TOOLS = new Set(['Write', 'Edit', 'MultiEdit']);
11
+ /**
12
+ * Morphs a Claude Code PreToolUse payload into the one normalized `AgentHookEvent`.
13
+ *
14
+ * This is the SAME `normalizeToolKind` / `normalizeToolInput` logic that used to live inline in
15
+ * hook-core.ts, moved out unchanged so hook-core is written once for every harness. Claude Code is the
16
+ * harness every developer uses today, so nothing about the mapping is allowed to move: `Write` still
17
+ * becomes one edit of `content` against '', `Edit` one edit of `old_string`→`new_string`, `MultiEdit`
18
+ * one per entry, and a file tool with no `file_path` still ends up allowed (kind `Ignored`).
19
+ */
20
+ class ClaudeCodeAdapter {
21
+ /**
22
+ * What is known from the ENVELOPE ALONE, touching nothing but `tool_name` and the identity fields.
23
+ *
24
+ * Not a second spelling of `toEvent` — a different question, asked at a moment when the answer to
25
+ * the other one may not exist. `toEvent` reads `tool_input`, and a payload whose `tool_input` is
26
+ * missing makes it throw; the crash then still has to be DENIED, and the deny still has to know
27
+ * whether it is decorating a Bash block (which needs the red `systemMessage`) or a file block
28
+ * (which does not). This is the shape that answers that, and it cannot fail.
29
+ */
30
+ envelope(payload) {
31
+ return new protocol_1.AgentHookEvent('claude-code', this.kindOf(payload.tool_name), payload.tool_name, payload.cwd ?? '', payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '', [], null, []);
32
+ }
33
+ toEvent(payload, cwd) {
34
+ const kind = this.kindOf(payload.tool_name);
35
+ const toolInput = payload.tool_input;
36
+ const bash = kind === 'Bash' ? new protocol_2.NormalizedBashInput(toolInput.command ?? '') : null;
37
+ const reads = kind === 'Read' ? [toolInput.file_path ?? ''] : [];
38
+ const files = kind === 'File' ? this.fileOperations(payload.tool_name, toolInput) : [];
39
+ // A file tool that named no file has nothing to judge; fall back to Ignored so the hook allows
40
+ // it, exactly as the old `if (!input) emitAllow()` did.
41
+ const effective = kind === 'File' && files.length === 0 ? 'Ignored' : kind;
42
+ return new protocol_1.AgentHookEvent('claude-code', effective, payload.tool_name, cwd, payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '', files, bash, reads);
43
+ }
44
+ kindOf(toolName) {
45
+ if (toolName === 'Bash')
46
+ return 'Bash';
47
+ if (toolName === 'Read')
48
+ return 'Read';
49
+ if (HANDLED_FILE_TOOLS.has(toolName))
50
+ return 'File';
51
+ return 'Ignored';
52
+ }
53
+ fileOperations(toolKind, toolInput) {
54
+ const filePath = toolInput.file_path;
55
+ if (!filePath)
56
+ return [];
57
+ if (toolKind === 'Write') {
58
+ return [new protocol_1.FileOperation(toolKind, new protocol_2.NormalizedToolInput(filePath, [new protocol_2.NormalizedEdit('', toolInput.content || '')]))];
59
+ }
60
+ if (toolKind === 'Edit') {
61
+ return [new protocol_1.FileOperation(toolKind, new protocol_2.NormalizedToolInput(filePath, [new protocol_2.NormalizedEdit(toolInput.old_string || '', toolInput.new_string || '')]))];
62
+ }
63
+ const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];
64
+ const edits = raw.map((e) => new protocol_2.NormalizedEdit(e.old_string || '', e.new_string || ''));
65
+ return [new protocol_1.FileOperation(toolKind, new protocol_2.NormalizedToolInput(filePath, edits))];
66
+ }
67
+ }
68
+ exports.ClaudeCodeAdapter = ClaudeCodeAdapter;
69
+ //# sourceMappingURL=claude-code-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"claude-code-adapter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/adapters/claude-code-adapter.ts"],"names":[],"mappings":"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","sourcesContent":["import { AgentPayload, AgentToolInput, AgentEditEntry } from './agent-payload';\nimport { AgentHookEvent, AgentEventKind, FileOperation } from '../protocol';\nimport { NormalizedBashInput, NormalizedEdit, NormalizedToolInput, ToolKind } from '../protocol';\n\n/**\n * The Claude Code tools that enter the file/edit rule pipeline. `Read` is deliberately NOT here — it\n * has its own fast path and only one guard may see it.\n */\nconst HANDLED_FILE_TOOLS: ReadonlySet<string> = new Set(['Write', 'Edit', 'MultiEdit']);\n\n/**\n * Morphs a Claude Code PreToolUse payload into the one normalized `AgentHookEvent`.\n *\n * This is the SAME `normalizeToolKind` / `normalizeToolInput` logic that used to live inline in\n * hook-core.ts, moved out unchanged so hook-core is written once for every harness. Claude Code is the\n * harness every developer uses today, so nothing about the mapping is allowed to move: `Write` still\n * becomes one edit of `content` against '', `Edit` one edit of `old_string`→`new_string`, `MultiEdit`\n * one per entry, and a file tool with no `file_path` still ends up allowed (kind `Ignored`).\n */\nexport class ClaudeCodeAdapter {\n /**\n * What is known from the ENVELOPE ALONE, touching nothing but `tool_name` and the identity fields.\n *\n * Not a second spelling of `toEvent` — a different question, asked at a moment when the answer to\n * the other one may not exist. `toEvent` reads `tool_input`, and a payload whose `tool_input` is\n * missing makes it throw; the crash then still has to be DENIED, and the deny still has to know\n * whether it is decorating a Bash block (which needs the red `systemMessage`) or a file block\n * (which does not). This is the shape that answers that, and it cannot fail.\n */\n envelope(payload: AgentPayload): AgentHookEvent {\n return new AgentHookEvent(\n 'claude-code', this.kindOf(payload.tool_name), payload.tool_name,\n payload.cwd ?? '', payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '',\n [], null, [],\n );\n }\n\n toEvent(payload: AgentPayload, cwd: string): AgentHookEvent {\n const kind = this.kindOf(payload.tool_name);\n const toolInput = payload.tool_input;\n const bash = kind === 'Bash' ? new NormalizedBashInput(toolInput.command ?? '') : null;\n const reads = kind === 'Read' ? [toolInput.file_path ?? ''] : [];\n const files = kind === 'File' ? this.fileOperations(payload.tool_name as ToolKind, toolInput) : [];\n // A file tool that named no file has nothing to judge; fall back to Ignored so the hook allows\n // it, exactly as the old `if (!input) emitAllow()` did.\n const effective: AgentEventKind = kind === 'File' && files.length === 0 ? 'Ignored' : kind;\n return new AgentHookEvent(\n 'claude-code', effective, payload.tool_name,\n cwd, payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '',\n files, bash, reads,\n );\n }\n\n private kindOf(toolName: string): AgentEventKind {\n if (toolName === 'Bash') return 'Bash';\n if (toolName === 'Read') return 'Read';\n if (HANDLED_FILE_TOOLS.has(toolName)) return 'File';\n return 'Ignored';\n }\n\n private fileOperations(toolKind: ToolKind, toolInput: AgentToolInput): readonly FileOperation[] {\n const filePath = toolInput.file_path;\n if (!filePath) return [];\n if (toolKind === 'Write') {\n return [new FileOperation(toolKind, new NormalizedToolInput(filePath, [new NormalizedEdit('', toolInput.content || '')]))];\n }\n if (toolKind === 'Edit') {\n return [new FileOperation(toolKind, new NormalizedToolInput(filePath, [new NormalizedEdit(toolInput.old_string || '', toolInput.new_string || '')]))];\n }\n const raw = Array.isArray(toolInput.edits) ? toolInput.edits : [];\n const edits = raw.map((e: AgentEditEntry): NormalizedEdit => new NormalizedEdit(e.old_string || '', e.new_string || ''));\n return [new FileOperation(toolKind, new NormalizedToolInput(filePath, edits))];\n }\n}\n"]}
@@ -0,0 +1,19 @@
1
+ import { AgentPayload } from './agent-payload';
2
+ import { AgentHookEvent } from '../protocol';
3
+ /**
4
+ * Morphs a Codex PreToolUse payload into the one normalized `AgentHookEvent`.
5
+ *
6
+ * Codex exposes exactly two tools this hook has anything to say about. Everything else measured in a
7
+ * live session — `webrun`, `collaborationspawn_agent`, `collaborationwait_agent`, `view_image`,
8
+ * `update_plan` — and every tool not yet seen maps to `Ignored` and is allowed immediately. That
9
+ * default is chosen on purpose: an unknown tool is one we cannot judge, and inventing a mapping for it
10
+ * would apply file rules to bytes that are not a file edit.
11
+ */
12
+ export declare class CodexAdapter {
13
+ private readonly patchParser;
14
+ private readonly readParity;
15
+ /** See ClaudeCodeAdapter.envelope — the pre-normalization shape the crash deny needs. */
16
+ envelope(payload: AgentPayload): AgentHookEvent;
17
+ toEvent(payload: AgentPayload, cwd: string): AgentHookEvent;
18
+ private kindOf;
19
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CodexAdapter = void 0;
4
+ const rules_config_1 = require("@webpieces/rules-config");
5
+ const protocol_1 = require("../protocol");
6
+ const protocol_2 = require("../protocol");
7
+ const apply_patch_parse_1 = require("../core/apply-patch-parse");
8
+ const shell_read_parity_1 = require("../core/shell-read-parity");
9
+ /** Codex's shell tool. MEASURED: it reuses Claude's name — it is `Bash`, NOT `shell`. */
10
+ const CODEX_BASH = 'Bash';
11
+ /** Codex's ONLY file-editing tool. One envelope, many files, mixed operations. */
12
+ const CODEX_APPLY_PATCH = 'apply_patch';
13
+ /**
14
+ * Morphs a Codex PreToolUse payload into the one normalized `AgentHookEvent`.
15
+ *
16
+ * Codex exposes exactly two tools this hook has anything to say about. Everything else measured in a
17
+ * live session — `webrun`, `collaborationspawn_agent`, `collaborationwait_agent`, `view_image`,
18
+ * `update_plan` — and every tool not yet seen maps to `Ignored` and is allowed immediately. That
19
+ * default is chosen on purpose: an unknown tool is one we cannot judge, and inventing a mapping for it
20
+ * would apply file rules to bytes that are not a file edit.
21
+ */
22
+ class CodexAdapter {
23
+ patchParser = new apply_patch_parse_1.ApplyPatchParser();
24
+ readParity = new shell_read_parity_1.ShellReadParity();
25
+ /** See ClaudeCodeAdapter.envelope — the pre-normalization shape the crash deny needs. */
26
+ envelope(payload) {
27
+ return new protocol_1.AgentHookEvent('codex', this.kindOf(payload.tool_name), payload.tool_name, payload.cwd ?? '', payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '', [], null, []);
28
+ }
29
+ toEvent(payload, cwd) {
30
+ const kind = this.kindOf(payload.tool_name);
31
+ const command = payload.tool_input.command ?? '';
32
+ const bash = kind === 'Bash' ? new protocol_2.NormalizedBashInput(command) : null;
33
+ // Read parity: Codex has no Read tool, so a read arrives as `Bash` running a pager. Synthesized
34
+ // reads run the read guard IN ADDITION to the bash guards — see ../core/shell-read-parity.ts.
35
+ const reads = kind === 'Bash' ? this.readParity.readTargets(command, cwd, new rules_config_1.RepoRootFinder().resolveRepoRoot(cwd)) : [];
36
+ const files = kind === 'File' ? this.patchParser.parse(command, cwd) : [];
37
+ return new protocol_1.AgentHookEvent('codex', kind, payload.tool_name, cwd, payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '', files, bash, reads);
38
+ }
39
+ kindOf(toolName) {
40
+ if (toolName === CODEX_BASH)
41
+ return 'Bash';
42
+ if (toolName === CODEX_APPLY_PATCH)
43
+ return 'File';
44
+ return 'Ignored';
45
+ }
46
+ }
47
+ exports.CodexAdapter = CodexAdapter;
48
+ //# sourceMappingURL=codex-adapter.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-adapter.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/adapters/codex-adapter.ts"],"names":[],"mappings":";;;AAAA,0DAAyD;AAGzD,0CAA6D;AAC7D,0CAAkD;AAClD,iEAA6D;AAC7D,iEAA4D;AAE5D,yFAAyF;AACzF,MAAM,UAAU,GAAG,MAAM,CAAC;AAC1B,kFAAkF;AAClF,MAAM,iBAAiB,GAAG,aAAa,CAAC;AAExC;;;;;;;;GAQG;AACH,MAAa,YAAY;IACJ,WAAW,GAAG,IAAI,oCAAgB,EAAE,CAAC;IACrC,UAAU,GAAG,IAAI,mCAAe,EAAE,CAAC;IAEpD,yFAAyF;IACzF,QAAQ,CAAC,OAAqB;QAC1B,OAAO,IAAI,yBAAc,CACrB,OAAO,EAAE,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,EAAE,OAAO,CAAC,SAAS,EAC1D,OAAO,CAAC,GAAG,IAAI,EAAE,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,EAC7F,EAAE,EAAE,IAAI,EAAE,EAAE,CACf,CAAC;IACN,CAAC;IAED,OAAO,CAAC,OAAqB,EAAE,GAAW;QACtC,MAAM,IAAI,GAAG,IAAI,CAAC,MAAM,CAAC,OAAO,CAAC,SAAS,CAAC,CAAC;QAC5C,MAAM,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,IAAI,EAAE,CAAC;QACjD,MAAM,IAAI,GAAG,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,8BAAmB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACvE,gGAAgG;QAChG,8FAA8F;QAC9F,MAAM,KAAK,GAAG,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,6BAAc,EAAE,CAAC,eAAe,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1H,MAAM,KAAK,GAAG,IAAI,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,OAAO,IAAI,yBAAc,CACrB,OAAO,EAAE,IAAI,EAAE,OAAO,CAAC,SAAS,EAChC,GAAG,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,EAAE,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,OAAO,CAAC,UAAU,IAAI,EAAE,EAC/E,KAAK,EAAE,IAAI,EAAE,KAAK,CACrB,CAAC;IACN,CAAC;IAEO,MAAM,CAAC,QAAgB;QAC3B,IAAI,QAAQ,KAAK,UAAU;YAAE,OAAO,MAAM,CAAC;QAC3C,IAAI,QAAQ,KAAK,iBAAiB;YAAE,OAAO,MAAM,CAAC;QAClD,OAAO,SAAS,CAAC;IACrB,CAAC;CACJ;AAjCD,oCAiCC","sourcesContent":["import { RepoRootFinder } from '@webpieces/rules-config';\n\nimport { AgentPayload } from './agent-payload';\nimport { AgentHookEvent, AgentEventKind } from '../protocol';\nimport { NormalizedBashInput } from '../protocol';\nimport { ApplyPatchParser } from '../core/apply-patch-parse';\nimport { ShellReadParity } from '../core/shell-read-parity';\n\n/** Codex's shell tool. MEASURED: it reuses Claude's name — it is `Bash`, NOT `shell`. */\nconst CODEX_BASH = 'Bash';\n/** Codex's ONLY file-editing tool. One envelope, many files, mixed operations. */\nconst CODEX_APPLY_PATCH = 'apply_patch';\n\n/**\n * Morphs a Codex PreToolUse payload into the one normalized `AgentHookEvent`.\n *\n * Codex exposes exactly two tools this hook has anything to say about. Everything else measured in a\n * live session — `webrun`, `collaborationspawn_agent`, `collaborationwait_agent`, `view_image`,\n * `update_plan` — and every tool not yet seen maps to `Ignored` and is allowed immediately. That\n * default is chosen on purpose: an unknown tool is one we cannot judge, and inventing a mapping for it\n * would apply file rules to bytes that are not a file edit.\n */\nexport class CodexAdapter {\n private readonly patchParser = new ApplyPatchParser();\n private readonly readParity = new ShellReadParity();\n\n /** See ClaudeCodeAdapter.envelope — the pre-normalization shape the crash deny needs. */\n envelope(payload: AgentPayload): AgentHookEvent {\n return new AgentHookEvent(\n 'codex', this.kindOf(payload.tool_name), payload.tool_name,\n payload.cwd ?? '', payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '',\n [], null, [],\n );\n }\n\n toEvent(payload: AgentPayload, cwd: string): AgentHookEvent {\n const kind = this.kindOf(payload.tool_name);\n const command = payload.tool_input.command ?? '';\n const bash = kind === 'Bash' ? new NormalizedBashInput(command) : null;\n // Read parity: Codex has no Read tool, so a read arrives as `Bash` running a pager. Synthesized\n // reads run the read guard IN ADDITION to the bash guards — see ../core/shell-read-parity.ts.\n const reads = kind === 'Bash' ? this.readParity.readTargets(command, cwd, new RepoRootFinder().resolveRepoRoot(cwd)) : [];\n const files = kind === 'File' ? this.patchParser.parse(command, cwd) : [];\n return new AgentHookEvent(\n 'codex', kind, payload.tool_name,\n cwd, payload.session_id ?? '', payload.agent_id ?? '', payload.agent_type ?? '',\n files, bash, reads,\n );\n }\n\n private kindOf(toolName: string): AgentEventKind {\n if (toolName === CODEX_BASH) return 'Bash';\n if (toolName === CODEX_APPLY_PATCH) return 'File';\n return 'Ignored';\n }\n}\n"]}
@@ -0,0 +1,70 @@
1
+ import { AiType } from '../protocol';
2
+ /**
3
+ * THE discriminator, and the only one. Codex's PreToolUse envelope carries a REQUIRED `turn_id`;
4
+ * Claude Code's has no such key. Everything else in the two envelopes is the same key names
5
+ * (`hook_event_name`, `tool_name`, `tool_input`, `cwd`, `session_id`, `transcript_path`), which is
6
+ * exactly why one positive key is the whole test rather than a shape heuristic.
7
+ *
8
+ * Exported as a TWIN — an sh fragment and a JS predicate — because L0 has two halves that must
9
+ * answer the identical question: the rendered POSIX-sh shim (which has no JSON parser and scrapes
10
+ * text) and this binary (which has the parsed object). That is the same pattern
11
+ * ../bin/l0-allowlist.ts already uses for `L0_ALLOW_ERE_SH` / `L0_ALLOW_JS`, and detect-ai.spec.ts
12
+ * asserts the two agree over a corpus the same way.
13
+ *
14
+ * ─── THE SH HALF IS AN APPROXIMATION, AND SINCE L0 BRANCHES ON IT, HERE IS EXACTLY HOW FAR OFF ──────
15
+ *
16
+ * The JS half asks the precise question (`turn_id` is a key of the TOP-LEVEL envelope). The sh half has
17
+ * no JSON parser, so it asks a structural approximation of it: after deleting all whitespace, does the
18
+ * payload contain `{` or `,` immediately followed by `"turn_id":`? That is strictly narrower than the
19
+ * bare substring test it replaced, and the tightening is not cosmetic — `bin/l0-allowlist.ts` now gates
20
+ * an allowlist entry on this answer, so a wrong answer is a grant.
21
+ *
22
+ * WHAT IT CAN AND CANNOT DISTINGUISH:
23
+ *
24
+ * - A Claude payload MENTIONING the key in a command — `grep '"turn_id":' x.json` — is answered
25
+ * `claude-code`, and not by luck: JSON escapes every `"` inside a string value, so the bytes on the
26
+ * wire are `\"turn_id\":`, which contains no `"turn_id":` at all. There is no spelling of a shell
27
+ * command that puts the RAW token into the payload.
28
+ * - A NESTED `turn_id` key — an MCP tool_input that happens to carry one — IS misread as `codex`.
29
+ * This is the residual gap and sh cannot close it. Do not write a JSON parser in sh to try.
30
+ *
31
+ * WHY THE RESIDUAL GAP IS NOT A PRIVILEGE ESCALATION, stated as the property to preserve rather than
32
+ * as a reassurance: the ONLY thing the harness answer gates at L0 is `L0_CODEX_ALLOW_ERE` (built from
33
+ * `CODEX_READ_BODY_ERE` in ../bin/l0-codex-read.ts), which
34
+ * admits nothing but a single, unredirected, unchained READ (`cat`/`head`/`tail`/`less`/`more`/`bat`,
35
+ * or `sed -n '<range>p'`). Allowlist entry 1 — "any Read", with no path restriction — already grants
36
+ * Claude Code the identical capability under every L0 fault. So a misclassification hands a Claude
37
+ * session another SPELLING of a read it could already do, and never a capability it lacked. Anything
38
+ * added to the aiType-gated set later must be checked against that property, because it is what makes
39
+ * the approximation tolerable. `codex-l0-read.spec.ts` pins both halves of this.
40
+ *
41
+ * WHENEVER THE JS HALF IS RUNNING IT IS AUTHORITATIVE: the binary calls `detectAiType()` on the parsed
42
+ * envelope (see `enforceCommittedShim`), so the approximation decides only faults D/X/U/K, where the
43
+ * binary never runs at all.
44
+ *
45
+ * WIRED INTO THE RENDERED SHIM (`PARSE_PAYLOAD_SH` in ../bin/shim.ts), which is what makes the L0 audit
46
+ * line's `ai=` field possible. Note the release ordering that governs the ARTIFACT rather than this
47
+ * constant: `committedShimStale()` compares the committed `.claude/webpieces/ai-hook.sh` against
48
+ * `renderShim()` of the INSTALLED release, so the committed shim is NOT regenerated in the same PR that
49
+ * changes the renderer — it is regenerated after that release publishes, or by `wp-upgrade-shim`.
50
+ */
51
+ export declare const AI_TYPE_TOKEN_SH = "\"turn_id\":";
52
+ /**
53
+ * Sets `AI` to the literal `AiType` value — `codex` or `claude-code` — from `$PAYLOAD`. The values
54
+ * are the SAME strings the TypeScript union carries, so the twin test can compare them byte for byte
55
+ * instead of translating between two vocabularies (translation is where twins drift).
56
+ *
57
+ * Two steps, and both are load-bearing:
58
+ * 1. `tr -d` deletes every whitespace byte into `WP_AI_ENV`, so the structural test below does not
59
+ * have to spell "optional whitespace" — which a POSIX `case` glob cannot express — and a
60
+ * pretty-printed envelope is read exactly like a compact one. The stripped copy is used for THIS
61
+ * test only; `$PAYLOAD` itself is untouched and stays the input every `sed` scrape reads.
62
+ * 2. the glob requires `{` or `,` IMMEDIATELY before the quoted key, i.e. the structural context a
63
+ * real JSON key has and a mention inside a value does not.
64
+ */
65
+ export declare const AI_TYPE_SH = "WP_AI_ENV=\"$(printf '%s' \"$PAYLOAD\" | tr -d ' \\t\\n\\r')\"\ncase \"$WP_AI_ENV\" in *[{,]'\"turn_id\":'*) AI=codex ;; *) AI=claude-code ;; esac";
66
+ /**
67
+ * JS twin of AI_TYPE_SH. Asks the precise question the sh half approximates: is `turn_id` a key of
68
+ * the top-level envelope?
69
+ */
70
+ export declare function detectAiType(payload: unknown): AiType;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AI_TYPE_SH = exports.AI_TYPE_TOKEN_SH = void 0;
4
+ exports.detectAiType = detectAiType;
5
+ /**
6
+ * THE discriminator, and the only one. Codex's PreToolUse envelope carries a REQUIRED `turn_id`;
7
+ * Claude Code's has no such key. Everything else in the two envelopes is the same key names
8
+ * (`hook_event_name`, `tool_name`, `tool_input`, `cwd`, `session_id`, `transcript_path`), which is
9
+ * exactly why one positive key is the whole test rather than a shape heuristic.
10
+ *
11
+ * Exported as a TWIN — an sh fragment and a JS predicate — because L0 has two halves that must
12
+ * answer the identical question: the rendered POSIX-sh shim (which has no JSON parser and scrapes
13
+ * text) and this binary (which has the parsed object). That is the same pattern
14
+ * ../bin/l0-allowlist.ts already uses for `L0_ALLOW_ERE_SH` / `L0_ALLOW_JS`, and detect-ai.spec.ts
15
+ * asserts the two agree over a corpus the same way.
16
+ *
17
+ * ─── THE SH HALF IS AN APPROXIMATION, AND SINCE L0 BRANCHES ON IT, HERE IS EXACTLY HOW FAR OFF ──────
18
+ *
19
+ * The JS half asks the precise question (`turn_id` is a key of the TOP-LEVEL envelope). The sh half has
20
+ * no JSON parser, so it asks a structural approximation of it: after deleting all whitespace, does the
21
+ * payload contain `{` or `,` immediately followed by `"turn_id":`? That is strictly narrower than the
22
+ * bare substring test it replaced, and the tightening is not cosmetic — `bin/l0-allowlist.ts` now gates
23
+ * an allowlist entry on this answer, so a wrong answer is a grant.
24
+ *
25
+ * WHAT IT CAN AND CANNOT DISTINGUISH:
26
+ *
27
+ * - A Claude payload MENTIONING the key in a command — `grep '"turn_id":' x.json` — is answered
28
+ * `claude-code`, and not by luck: JSON escapes every `"` inside a string value, so the bytes on the
29
+ * wire are `\"turn_id\":`, which contains no `"turn_id":` at all. There is no spelling of a shell
30
+ * command that puts the RAW token into the payload.
31
+ * - A NESTED `turn_id` key — an MCP tool_input that happens to carry one — IS misread as `codex`.
32
+ * This is the residual gap and sh cannot close it. Do not write a JSON parser in sh to try.
33
+ *
34
+ * WHY THE RESIDUAL GAP IS NOT A PRIVILEGE ESCALATION, stated as the property to preserve rather than
35
+ * as a reassurance: the ONLY thing the harness answer gates at L0 is `L0_CODEX_ALLOW_ERE` (built from
36
+ * `CODEX_READ_BODY_ERE` in ../bin/l0-codex-read.ts), which
37
+ * admits nothing but a single, unredirected, unchained READ (`cat`/`head`/`tail`/`less`/`more`/`bat`,
38
+ * or `sed -n '<range>p'`). Allowlist entry 1 — "any Read", with no path restriction — already grants
39
+ * Claude Code the identical capability under every L0 fault. So a misclassification hands a Claude
40
+ * session another SPELLING of a read it could already do, and never a capability it lacked. Anything
41
+ * added to the aiType-gated set later must be checked against that property, because it is what makes
42
+ * the approximation tolerable. `codex-l0-read.spec.ts` pins both halves of this.
43
+ *
44
+ * WHENEVER THE JS HALF IS RUNNING IT IS AUTHORITATIVE: the binary calls `detectAiType()` on the parsed
45
+ * envelope (see `enforceCommittedShim`), so the approximation decides only faults D/X/U/K, where the
46
+ * binary never runs at all.
47
+ *
48
+ * WIRED INTO THE RENDERED SHIM (`PARSE_PAYLOAD_SH` in ../bin/shim.ts), which is what makes the L0 audit
49
+ * line's `ai=` field possible. Note the release ordering that governs the ARTIFACT rather than this
50
+ * constant: `committedShimStale()` compares the committed `.claude/webpieces/ai-hook.sh` against
51
+ * `renderShim()` of the INSTALLED release, so the committed shim is NOT regenerated in the same PR that
52
+ * changes the renderer — it is regenerated after that release publishes, or by `wp-upgrade-shim`.
53
+ */
54
+ exports.AI_TYPE_TOKEN_SH = '"turn_id":';
55
+ /**
56
+ * Sets `AI` to the literal `AiType` value — `codex` or `claude-code` — from `$PAYLOAD`. The values
57
+ * are the SAME strings the TypeScript union carries, so the twin test can compare them byte for byte
58
+ * instead of translating between two vocabularies (translation is where twins drift).
59
+ *
60
+ * Two steps, and both are load-bearing:
61
+ * 1. `tr -d` deletes every whitespace byte into `WP_AI_ENV`, so the structural test below does not
62
+ * have to spell "optional whitespace" — which a POSIX `case` glob cannot express — and a
63
+ * pretty-printed envelope is read exactly like a compact one. The stripped copy is used for THIS
64
+ * test only; `$PAYLOAD` itself is untouched and stays the input every `sed` scrape reads.
65
+ * 2. the glob requires `{` or `,` IMMEDIATELY before the quoted key, i.e. the structural context a
66
+ * real JSON key has and a mention inside a value does not.
67
+ */
68
+ exports.AI_TYPE_SH = `WP_AI_ENV="$(printf '%s' "$PAYLOAD" | tr -d ' \\t\\n\\r')"
69
+ case "$WP_AI_ENV" in *[{,]'${exports.AI_TYPE_TOKEN_SH}'*) AI=codex ;; *) AI=claude-code ;; esac`;
70
+ /**
71
+ * JS twin of AI_TYPE_SH. Asks the precise question the sh half approximates: is `turn_id` a key of
72
+ * the top-level envelope?
73
+ */
74
+ // webpieces-disable no-any-unknown -- the argument IS unparsed JSON from another process's stdout; naming a type here would assert a shape we have not yet established, which is the question this function exists to answer
75
+ // webpieces-disable no-function-outside-class -- twin of an sh fragment in the dependency-free adapter layer; it must stay callable on a tree too broken to build a DI container, exactly like isAllowed()
76
+ function detectAiType(payload) {
77
+ if (payload === null || typeof payload !== 'object')
78
+ return 'claude-code';
79
+ // webpieces-disable no-any-unknown -- narrowing the same unparsed JSON; the index signature is the widest true statement about it
80
+ return 'turn_id' in payload ? 'codex' : 'claude-code';
81
+ }
82
+ //# sourceMappingURL=detect-ai.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detect-ai.js","sourceRoot":"","sources":["../../../../../../packages/tooling/hook-runtime/src/adapters/detect-ai.ts"],"names":[],"mappings":";;;AA2EA,oCAIC;AA7ED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgDG;AACU,QAAA,gBAAgB,GAAG,YAAY,CAAC;AAE7C;;;;;;;;;;;;GAYG;AACU,QAAA,UAAU,GAAG;6BACG,wBAAgB,2CAA2C,CAAC;AAEzF;;;GAGG;AACH,6NAA6N;AAC7N,2MAA2M;AAC3M,SAAgB,YAAY,CAAC,OAAgB;IACzC,IAAI,OAAO,KAAK,IAAI,IAAI,OAAO,OAAO,KAAK,QAAQ;QAAE,OAAO,aAAa,CAAC;IAC1E,kIAAkI;IAClI,OAAO,SAAS,IAAK,OAAmC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,aAAa,CAAC;AACvF,CAAC","sourcesContent":["import { AiType } from '../protocol';\n\n/**\n * THE discriminator, and the only one. Codex's PreToolUse envelope carries a REQUIRED `turn_id`;\n * Claude Code's has no such key. Everything else in the two envelopes is the same key names\n * (`hook_event_name`, `tool_name`, `tool_input`, `cwd`, `session_id`, `transcript_path`), which is\n * exactly why one positive key is the whole test rather than a shape heuristic.\n *\n * Exported as a TWIN — an sh fragment and a JS predicate — because L0 has two halves that must\n * answer the identical question: the rendered POSIX-sh shim (which has no JSON parser and scrapes\n * text) and this binary (which has the parsed object). That is the same pattern\n * ../bin/l0-allowlist.ts already uses for `L0_ALLOW_ERE_SH` / `L0_ALLOW_JS`, and detect-ai.spec.ts\n * asserts the two agree over a corpus the same way.\n *\n * ─── THE SH HALF IS AN APPROXIMATION, AND SINCE L0 BRANCHES ON IT, HERE IS EXACTLY HOW FAR OFF ──────\n *\n * The JS half asks the precise question (`turn_id` is a key of the TOP-LEVEL envelope). The sh half has\n * no JSON parser, so it asks a structural approximation of it: after deleting all whitespace, does the\n * payload contain `{` or `,` immediately followed by `\"turn_id\":`? That is strictly narrower than the\n * bare substring test it replaced, and the tightening is not cosmetic — `bin/l0-allowlist.ts` now gates\n * an allowlist entry on this answer, so a wrong answer is a grant.\n *\n * WHAT IT CAN AND CANNOT DISTINGUISH:\n *\n * - A Claude payload MENTIONING the key in a command — `grep '\"turn_id\":' x.json` — is answered\n * `claude-code`, and not by luck: JSON escapes every `\"` inside a string value, so the bytes on the\n * wire are `\\\"turn_id\\\":`, which contains no `\"turn_id\":` at all. There is no spelling of a shell\n * command that puts the RAW token into the payload.\n * - A NESTED `turn_id` key — an MCP tool_input that happens to carry one — IS misread as `codex`.\n * This is the residual gap and sh cannot close it. Do not write a JSON parser in sh to try.\n *\n * WHY THE RESIDUAL GAP IS NOT A PRIVILEGE ESCALATION, stated as the property to preserve rather than\n * as a reassurance: the ONLY thing the harness answer gates at L0 is `L0_CODEX_ALLOW_ERE` (built from\n * `CODEX_READ_BODY_ERE` in ../bin/l0-codex-read.ts), which\n * admits nothing but a single, unredirected, unchained READ (`cat`/`head`/`tail`/`less`/`more`/`bat`,\n * or `sed -n '<range>p'`). Allowlist entry 1 — \"any Read\", with no path restriction — already grants\n * Claude Code the identical capability under every L0 fault. So a misclassification hands a Claude\n * session another SPELLING of a read it could already do, and never a capability it lacked. Anything\n * added to the aiType-gated set later must be checked against that property, because it is what makes\n * the approximation tolerable. `codex-l0-read.spec.ts` pins both halves of this.\n *\n * WHENEVER THE JS HALF IS RUNNING IT IS AUTHORITATIVE: the binary calls `detectAiType()` on the parsed\n * envelope (see `enforceCommittedShim`), so the approximation decides only faults D/X/U/K, where the\n * binary never runs at all.\n *\n * WIRED INTO THE RENDERED SHIM (`PARSE_PAYLOAD_SH` in ../bin/shim.ts), which is what makes the L0 audit\n * line's `ai=` field possible. Note the release ordering that governs the ARTIFACT rather than this\n * constant: `committedShimStale()` compares the committed `.claude/webpieces/ai-hook.sh` against\n * `renderShim()` of the INSTALLED release, so the committed shim is NOT regenerated in the same PR that\n * changes the renderer — it is regenerated after that release publishes, or by `wp-upgrade-shim`.\n */\nexport const AI_TYPE_TOKEN_SH = '\"turn_id\":';\n\n/**\n * Sets `AI` to the literal `AiType` value — `codex` or `claude-code` — from `$PAYLOAD`. The values\n * are the SAME strings the TypeScript union carries, so the twin test can compare them byte for byte\n * instead of translating between two vocabularies (translation is where twins drift).\n *\n * Two steps, and both are load-bearing:\n * 1. `tr -d` deletes every whitespace byte into `WP_AI_ENV`, so the structural test below does not\n * have to spell \"optional whitespace\" — which a POSIX `case` glob cannot express — and a\n * pretty-printed envelope is read exactly like a compact one. The stripped copy is used for THIS\n * test only; `$PAYLOAD` itself is untouched and stays the input every `sed` scrape reads.\n * 2. the glob requires `{` or `,` IMMEDIATELY before the quoted key, i.e. the structural context a\n * real JSON key has and a mention inside a value does not.\n */\nexport const AI_TYPE_SH = `WP_AI_ENV=\"$(printf '%s' \"$PAYLOAD\" | tr -d ' \\\\t\\\\n\\\\r')\"\ncase \"$WP_AI_ENV\" in *[{,]'${AI_TYPE_TOKEN_SH}'*) AI=codex ;; *) AI=claude-code ;; esac`;\n\n/**\n * JS twin of AI_TYPE_SH. Asks the precise question the sh half approximates: is `turn_id` a key of\n * the top-level envelope?\n */\n// webpieces-disable no-any-unknown -- the argument IS unparsed JSON from another process's stdout; naming a type here would assert a shape we have not yet established, which is the question this function exists to answer\n// webpieces-disable no-function-outside-class -- twin of an sh fragment in the dependency-free adapter layer; it must stay callable on a tree too broken to build a DI container, exactly like isAllowed()\nexport function detectAiType(payload: unknown): AiType {\n if (payload === null || typeof payload !== 'object') return 'claude-code';\n // webpieces-disable no-any-unknown -- narrowing the same unparsed JSON; the index signature is the widest true statement about it\n return 'turn_id' in (payload as Record<string, unknown>) ? 'codex' : 'claude-code';\n}\n"]}
@@ -0,0 +1,36 @@
1
+ import { FileOperation } from '../protocol';
2
+ /**
3
+ * Parses Codex's `apply_patch` envelope into the same `FileOperation[]` the rest of the hook already
4
+ * understands, so a Codex edit is judged by the identical rules a Claude edit is.
5
+ *
6
+ * The grammar is MEASURED from a live codex-cli 0.151.0 session, not inferred from a diff format it
7
+ * resembles. Two properties of it drive every design choice here:
8
+ *
9
+ * - ONE envelope carries MANY files with MIXED operations, which is why the result is a LIST of
10
+ * `FileOperation` (each with its own kind) rather than one kind for the call.
11
+ * - Hunk headers are a BARE `@@` with NO line numbers. A unified-diff `@@ -1,3 +1,4 @@` is therefore
12
+ * not a stricter spelling of the same thing — it is an envelope we have never seen, and guessing at
13
+ * it is how a guard silently judges the wrong bytes. It is rejected.
14
+ *
15
+ * Paths are sometimes relative and sometimes absolute (measured: subagents used relative, the
16
+ * coordinator absolute), so a relative path is resolved against the payload's `cwd`.
17
+ *
18
+ * FAIL CLOSED. Every malformed envelope THROWS `InformAiError`, which the hook's one top-level handler
19
+ * turns into a deny naming the offending line. A parser that "did its best" on an envelope it did not
20
+ * understand would hand the rule engine a subset of the real edit and allow the rest unjudged, which is
21
+ * strictly worse than refusing the call.
22
+ */
23
+ export declare class ApplyPatchParser {
24
+ parse(command: string, cwd: string): readonly FileOperation[];
25
+ /** `*** Add File: <p>` plus a body of `+` lines → a Write of the joined body. */
26
+ private parseAdd;
27
+ /**
28
+ * `*** Update File: <p>`, an optional `*** Move to: <q>`, then one or more bare-`@@` hunks.
29
+ *
30
+ * A `Move to` makes the DESTINATION the judged path. That is the file whose bytes will exist after
31
+ * the call, so it is the one a path-scoped rule must see; judging the source would let a rename
32
+ * into a guarded directory land unjudged.
33
+ */
34
+ private parseUpdate;
35
+ private resolve;
36
+ }