@webpieces/ai-hook-rules 0.4.713 → 0.4.715
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/package.json +2 -2
- package/src/adapters/detect-ai.d.ts +39 -6
- package/src/adapters/detect-ai.js +40 -6
- package/src/adapters/detect-ai.js.map +1 -1
- package/src/adapters/hook-core.d.ts +2 -1
- package/src/adapters/hook-core.js +13 -4
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/l0-allowlist.d.ts +24 -1
- package/src/bin/l0-allowlist.js +58 -18
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/l0-codex-read.d.ts +21 -0
- package/src/bin/l0-codex-read.js +109 -0
- package/src/bin/l0-codex-read.js.map +1 -0
- package/src/bin/l0-decide.d.ts +8 -1
- package/src/bin/l0-decide.js +14 -1
- package/src/bin/l0-decide.js.map +1 -1
- package/src/bin/shim-audit-log.js +2 -0
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.js +2 -1
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim.d.ts +1 -0
- package/src/bin/shim.js +16 -1
- package/src/bin/shim.js.map +1 -1
- package/src/core/l0-matrix.js +11 -4
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l0-tooling-doc.js +8 -2
- package/src/core/l0-tooling-doc.js.map +1 -1
- package/src/core/runner.d.ts +2 -1
- package/src/core/runner.js +15 -17
- package/src/core/runner.js.map +1 -1
- package/src/core/shell-read-parity.d.ts +34 -0
- package/src/core/shell-read-parity.js +15 -5
- package/src/core/shell-read-parity.js.map +1 -1
- package/templates/ai-hook.sh +14 -2
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shell-read-parity.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/shell-read-parity.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,aAAa,GAAwB,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC,CAAC;AAEnG,sGAAsG;AACtG,MAAM,eAAe,GAAG,eAAe,CAAC;AAExC,iGAAiG;AACjG,MAAM,WAAW,GAAwB,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC;AAEjF,kFAAkF;AAClF,MAAM,SAAS,GAAG,eAAe,CAAC;AAElC,MAAa,eAAe;IACxB;;;OAGG;IACH,WAAW,CAAC,OAAe,EAAE,GAAW,EAAE,IAAY;QAClD,MAAM,OAAO,GAAG,OAAO,CAAC,IAAI,EAAE,CAAC;QAC/B,IAAI,OAAO,KAAK,EAAE,IAAI,eAAe,CAAC,IAAI,CAAC,OAAO,CAAC;YAAE,OAAO,EAAE,CAAC;QAC/D,MAAM,MAAM,GAAG,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACtC,IAAI,MAAM,KAAK,IAAI,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,EAAE,CAAC;QAEpD,MAAM,KAAK,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;QACxB,MAAM,IAAI,GAAG,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACvH,IAAI,IAAI,KAAK,IAAI;YAAE,OAAO,EAAE,CAAC;QAE7B,MAAM,KAAK,GAAa,EAAE,CAAC;QAC3B,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,OAAO,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC;YACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC;YAC1B,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,KAAK,GAAG,EAAE,CAAC;gBACzC,IAAI,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC;oBAAE,KAAK,IAAI,CAAC,CAAC;gBACvC,KAAK,IAAI,CAAC,CAAC;gBACX,SAAS;YACb,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;YAC1C,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,CAAC;gBAAE,OAAO,EAAE,CAAC;YAClD,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;YACrB,KAAK,IAAI,CAAC,CAAC;QACf,CAAC;QACD,OAAO,KAAK,CAAC;IACjB,CAAC;IAED;;;;OAIG;IACK,WAAW,CAAC,IAAuB;QACvC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACtC,MAAM,OAAO,GAAG,IAAI,CAAC,MAAM,CAAC,CAAC,CAAS,EAAW,EAAE,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QACxE,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC;YAAE,OAAO,IAAI,CAAC;QACpC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QAC7C,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IAEO,YAAY,CAAC,QAAgB,EAAE,IAAY;QAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC;QACrE,IAAI,CAAC,QAAQ,CAAC,UAAU,CAAC,WAAW,CAAC;YAAE,OAAO,KAAK,CAAC;QACpD,8DAA8D;QAC9D,IAAI,CAAC;YACD,OAAO,EAAE,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,MAAM,EAAE,CAAC;QAC1C,CAAC;QAAC,OAAO,GAAY,EAAE,CAAC;YACpB,6FAA6F;YAC7F,4FAA4F;YAC5F,6FAA6F;YAC7F,kFAAkF;YAClF,6BAA6B;YAC7B,OAAO,KAAK,CAAC;QACjB,CAAC;IACL,CAAC;IAED;;;;;;OAMG;IACK,QAAQ,CAAC,OAAe;QAC5B,MAAM,MAAM,GAAa,EAAE,CAAC;QAC5B,IAAI,OAAO,GAAG,EAAE,CAAC;QACjB,IAAI,KAAK,GAAG,EAAE,CAAC;QACf,IAAI,OAAO,GAAG,KAAK,CAAC;QACpB,KAAK,MAAM,EAAE,IAAI,OAAO,EAAE,CAAC;YACvB,IAAI,KAAK,KAAK,EAAE,EAAE,CAAC;gBACf,IAAI,EAAE,KAAK,KAAK;oBAAE,KAAK,GAAG,EAAE,CAAC;;oBACxB,OAAO,IAAI,EAAE,CAAC;gBACnB,SAAS;YACb,CAAC;YACD,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,GAAG,EAAE,CAAC;gBAAC,KAAK,GAAG,EAAE,CAAC;gBAAC,OAAO,GAAG,IAAI,CAAC;gBAAC,SAAS;YAAC,CAAC;YACxE,IAAI,EAAE,KAAK,GAAG,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,IAAI,EAAE,CAAC;gBAC3C,IAAI,OAAO;oBAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBAClC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,GAAG,KAAK,CAAC;gBAChB,SAAS;YACb,CAAC;YACD,OAAO,IAAI,EAAE,CAAC;YACd,OAAO,GAAG,IAAI,CAAC;QACnB,CAAC;QACD,IAAI,KAAK,KAAK,EAAE;YAAE,OAAO,IAAI,CAAC;QAC9B,IAAI,OAAO;YAAE,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAClC,OAAO,MAAM,CAAC;IAClB,CAAC;CACJ;AA7FD,0CA6FC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n/**\n * READ PARITY, and it is a CODEX-ONLY surface.\n *\n * Claude Code has a first-class `Read` tool, so the read-scoped guard (read-stale-guard) and the\n * `calls/` audit trail see every file the agent opens. Codex has no such tool: a file read arrives as\n * `Bash` running `sed -n '1,240p' <path>` (measured). Without this, every Codex read is invisible to\n * the read guard and to the audit log, and the two harnesses cannot be compared at all.\n *\n * The caller gates this on `aiType === 'codex'`. Nothing here is reachable from a Claude Code payload,\n * and a spec pins that.\n *\n * DELIBERATELY CONSERVATIVE, because a false positive here is not a missed read — it is a Read GUARD\n * verdict applied to a command that was never a read, i.e. a new way to block work:\n *\n * - ONE command only. Any `;` `&&` `||` `|` backtick `$(` or redirect and the answer is \"not a read\";\n * the command still runs the ordinary bash guards, exactly as today.\n * - `argv[0]` must be one of the six pure pagers, or `sed -n '<range>p'`.\n * - EVERY non-flag argument must resolve to a file that exists inside the tree. `cat /etc/passwd` is\n * not a read of this repo, and neither is a heredoc.\n *\n * A match does NOT replace the bash guards — the caller runs BOTH, and either may deny.\n */\nconst READ_COMMANDS: ReadonlySet<string> = new Set(['cat', 'head', 'tail', 'less', 'more', 'bat']);\n\n/** Shell syntax that makes a command more than one command, or redirects it. Any hit ⇒ not a read. */\nconst NOT_ONE_COMMAND = /[;|&`<>]|\\$\\(/;\n\n/** Flags that consume the FOLLOWING token as their value, so it is never mistaken for a file. */\nconst VALUE_FLAGS: ReadonlySet<string> = new Set(['-n', '-c', '-b', '-e', '-f']);\n\n/** The only `sed` script shape that is a read: a line range printed with `-n`. */\nconst SED_RANGE = /^\\d+(,\\d+)?p$/;\n\nexport class ShellReadParity {\n /**\n * The absolute paths this command reads, or an empty list when it is not a read-shaped command.\n * `root` is the tree the read must fall inside; a path outside it is not this repo's read.\n */\n readTargets(command: string, cwd: string, root: string): readonly string[] {\n const trimmed = command.trim();\n if (trimmed === '' || NOT_ONE_COMMAND.test(trimmed)) return [];\n const tokens = this.tokenize(trimmed);\n if (tokens === null || tokens.length < 2) return [];\n\n const argv0 = tokens[0];\n const rest = argv0 === 'sed' ? this.sedOperands(tokens.slice(1)) : (READ_COMMANDS.has(argv0) ? tokens.slice(1) : null);\n if (rest === null) return [];\n\n const files: string[] = [];\n let index = 0;\n while (index < rest.length) {\n const token = rest[index];\n if (token.startsWith('-') && token !== '-') {\n if (VALUE_FLAGS.has(token)) index += 1;\n index += 1;\n continue;\n }\n const resolved = path.resolve(cwd, token);\n if (!this.isFileInTree(resolved, root)) return [];\n files.push(resolved);\n index += 1;\n }\n return files;\n }\n\n /**\n * `sed`'s operands, or null when this `sed` invocation is not a plain range print. `-n` and a\n * single `<range>p` script are BOTH required: without `-n` sed echoes and edits, and any other\n * script is a transformation, not a read.\n */\n private sedOperands(args: readonly string[]): readonly string[] | null {\n if (!args.includes('-n')) return null;\n const scripts = args.filter((a: string): boolean => !a.startsWith('-'));\n if (scripts.length < 2) return null;\n if (!SED_RANGE.test(scripts[0])) return null;\n return scripts.slice(1);\n }\n\n private isFileInTree(resolved: string, root: string): boolean {\n const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;\n if (!resolved.startsWith(rootWithSep)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(resolved).isFile();\n } catch (err: unknown) {\n // Nothing is swallowed here: a token we cannot stat is simply NOT a read target, which is an\n // ANSWER, and the command still runs the ordinary bash guards either way. The commented-out\n // toError() call below is the form `catch-error-pattern` requires for a deliberately ignored\n // error — it is the rule's spelling of \"ignored on purpose\", not a disabled line.\n //const error = toError(err);\n return false;\n }\n }\n\n /**\n * Split on whitespace, honouring single and double quotes. Returns null on an unterminated quote —\n * a command we cannot read confidently is never a read.\n *\n * There is no escape or expansion handling here on purpose: `NOT_ONE_COMMAND` has already refused\n * everything carrying shell syntax, so what reaches this is a literal argv.\n */\n private tokenize(command: string): readonly string[] | null {\n const tokens: string[] = [];\n let current = '';\n let quote = '';\n let started = false;\n for (const ch of command) {\n if (quote !== '') {\n if (ch === quote) quote = '';\n else current += ch;\n continue;\n }\n if (ch === '\\'' || ch === '\"') { quote = ch; started = true; continue; }\n if (ch === ' ' || ch === '\\t' || ch === '\\n') {\n if (started) tokens.push(current);\n current = '';\n started = false;\n continue;\n }\n current += ch;\n started = true;\n }\n if (quote !== '') return null;\n if (started) tokens.push(current);\n return tokens;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shell-read-parity.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/core/shell-read-parity.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\n/**\n * READ PARITY, and it is a CODEX-ONLY surface.\n *\n * Claude Code has a first-class `Read` tool, so the read-scoped guard (read-stale-guard) and the\n * `calls/` audit trail see every file the agent opens. Codex has no such tool: a file read arrives as\n * `Bash` running `sed -n '1,240p' <path>` (measured). Without this, every Codex read is invisible to\n * the read guard and to the audit log, and the two harnesses cannot be compared at all.\n *\n * The caller gates this on `aiType === 'codex'`. Nothing here is reachable from a Claude Code payload,\n * and a spec pins that.\n *\n * DELIBERATELY CONSERVATIVE, because a false positive here is not a missed read — it is a Read GUARD\n * verdict applied to a command that was never a read, i.e. a new way to block work:\n *\n * - ONE command only. Any `;` `&&` `||` `|` backtick `$(` or redirect and the answer is \"not a read\";\n * the command still runs the ordinary bash guards, exactly as today.\n * - `argv[0]` must be one of the six pure pagers, or `sed -n '<range>p'`.\n * - EVERY non-flag argument must resolve to a file that exists inside the tree. `cat /etc/passwd` is\n * not a read of this repo, and neither is a heredoc.\n *\n * A match does NOT replace the bash guards — the caller runs BOTH, and either may deny.\n */\nexport const READ_COMMANDS: ReadonlySet<string> = new Set(['cat', 'head', 'tail', 'less', 'more', 'bat']);\n\n/** Shell syntax that makes a command more than one command, or redirects it. Any hit ⇒ not a read. */\nconst NOT_ONE_COMMAND = /[;|&`<>]|\\$\\(/;\n\n/** Flags that consume the FOLLOWING token as their value, so it is never mistaken for a file. */\nconst VALUE_FLAGS: ReadonlySet<string> = new Set(['-n', '-c', '-b', '-e', '-f']);\n\n/**\n * The only `sed` script shape that is a read: a line range printed with `-n` — as a regex BODY, so the\n * L0 allowlist's ERE twin can be built from the same characters instead of retyping them.\n *\n * EXPORTED for `bin/l0-allowlist.ts`. The L0 entry that lets a Codex session read its way out of an L0\n * fault has to mean the same thing as this module or the two definitions of \"read-shaped\" drift, and a\n * drifted L0 entry is either a hole or a deadlock. It cannot literally CALL this module — L0's sh half\n * has no JS at all — so the shared thing is the vocabulary, and `codex-l0-read.spec.ts` asserts the two\n * agree over a corpus.\n */\nexport const SED_RANGE_BODY = '[0-9]+(,[0-9]+)?p';\n\nconst SED_RANGE = new RegExp('^' + SED_RANGE_BODY + '$');\n\nexport class ShellReadParity {\n /**\n * The absolute paths this command reads, or an empty list when it is not a read-shaped command.\n * `root` is the tree the read must fall inside; a path outside it is not this repo's read.\n */\n readTargets(command: string, cwd: string, root: string): readonly string[] {\n const trimmed = command.trim();\n if (trimmed === '' || NOT_ONE_COMMAND.test(trimmed)) return [];\n const tokens = this.tokenize(trimmed);\n if (tokens === null || tokens.length < 2) return [];\n\n const argv0 = tokens[0];\n const rest = argv0 === 'sed' ? this.sedOperands(tokens.slice(1)) : (READ_COMMANDS.has(argv0) ? tokens.slice(1) : null);\n if (rest === null) return [];\n\n const files: string[] = [];\n let index = 0;\n while (index < rest.length) {\n const token = rest[index];\n if (token.startsWith('-') && token !== '-') {\n if (VALUE_FLAGS.has(token)) index += 1;\n index += 1;\n continue;\n }\n const resolved = path.resolve(cwd, token);\n if (!this.isFileInTree(resolved, root)) return [];\n files.push(resolved);\n index += 1;\n }\n return files;\n }\n\n /**\n * `sed`'s operands, or null when this `sed` invocation is not a plain range print. `-n` and a\n * single `<range>p` script are BOTH required: without `-n` sed echoes and edits, and any other\n * script is a transformation, not a read.\n */\n private sedOperands(args: readonly string[]): readonly string[] | null {\n if (!args.includes('-n')) return null;\n const scripts = args.filter((a: string): boolean => !a.startsWith('-'));\n if (scripts.length < 2) return null;\n if (!SED_RANGE.test(scripts[0])) return null;\n return scripts.slice(1);\n }\n\n private isFileInTree(resolved: string, root: string): boolean {\n const rootWithSep = root.endsWith(path.sep) ? root : root + path.sep;\n if (!resolved.startsWith(rootWithSep)) return false;\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return fs.statSync(resolved).isFile();\n } catch (err: unknown) {\n // Nothing is swallowed here: a token we cannot stat is simply NOT a read target, which is an\n // ANSWER, and the command still runs the ordinary bash guards either way. The commented-out\n // toError() call below is the form `catch-error-pattern` requires for a deliberately ignored\n // error — it is the rule's spelling of \"ignored on purpose\", not a disabled line.\n //const error = toError(err);\n return false;\n }\n }\n\n /**\n * Split on whitespace, honouring single and double quotes. Returns null on an unterminated quote —\n * a command we cannot read confidently is never a read.\n *\n * There is no escape or expansion handling here on purpose: `NOT_ONE_COMMAND` has already refused\n * everything carrying shell syntax, so what reaches this is a literal argv.\n */\n private tokenize(command: string): readonly string[] | null {\n const tokens: string[] = [];\n let current = '';\n let quote = '';\n let started = false;\n for (const ch of command) {\n if (quote !== '') {\n if (ch === quote) quote = '';\n else current += ch;\n continue;\n }\n if (ch === '\\'' || ch === '\"') { quote = ch; started = true; continue; }\n if (ch === ' ' || ch === '\\t' || ch === '\\n') {\n if (started) tokens.push(current);\n current = '';\n started = false;\n continue;\n }\n current += ch;\n started = true;\n }\n if (quote !== '') return null;\n if (started) tokens.push(current);\n return tokens;\n }\n}\n"]}
|
package/templates/ai-hook.sh
CHANGED
|
@@ -19,7 +19,7 @@ ROOT="$(CDPATH= cd -- "$(dirname -- "$0")/../.." && pwd)"
|
|
|
19
19
|
BS='\' # one literal backslash, so no \u001b / \n escape sits in this source
|
|
20
20
|
ESC="${BS}u001b" # the 6 chars: backslash u 0 0 1 b — Claude Code parses \u001b → ESC
|
|
21
21
|
NL="${BS}n" # the 2 chars: backslash n — parsed as a real newline inside the JSON string
|
|
22
|
-
WP_STILL_ALLOWED="Still allowed while this block is up:${NL} - any Read${NL} - any Write/Edit whose target is webpieces.config.json, pnpm-workspace.yaml or package.json${NL} - every command on the L0 allowlist, including the Fix Options below${NL} THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human."
|
|
22
|
+
WP_STILL_ALLOWED="Still allowed while this block is up:${NL} - any Read, and on CODEX (no Read tool): a bare read command - cat/head/tail/less/more/bat <file>, or sed -n '1,240p' package.json - with nothing piped, redirected or chained onto it${NL} - any Write/Edit whose target is webpieces.config.json, pnpm-workspace.yaml or package.json${NL} - every command on the L0 allowlist, including the Fix Options below${NL} THIS IS NOT A DEADLOCK - run one YOURSELF now; do not hand it back to the human."
|
|
23
23
|
# The BIN is resolved by walking UP from ROOT (as Node does), and BIN_ROOT records which tree supplied
|
|
24
24
|
# it — the version-drift guard below compares THIS tree's pin against THAT tree's installed version.
|
|
25
25
|
BIN_ROOT="$ROOT"
|
|
@@ -200,7 +200,8 @@ WP_CWD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\(
|
|
|
200
200
|
# L0_ALLOW_ERE_SH / L0_ALLOW_JS uses). One `case`, no JSON parser: consistent with how every field
|
|
201
201
|
# above is `sed`-scraped, and the values are the AiType union's own strings so the twin-agreement spec
|
|
202
202
|
# compares them byte for byte instead of translating between two vocabularies.
|
|
203
|
-
|
|
203
|
+
WP_AI_ENV="$(printf '%s' "$PAYLOAD" | tr -d ' \t\n\r')"
|
|
204
|
+
case "$WP_AI_ENV" in *[{,]'"turn_id":'*) AI=codex ;; *) AI=claude-code ;; esac
|
|
204
205
|
# Best-effort AUDIT TRAIL of what L0 did with this call — every call, not just the broken ones. One
|
|
205
206
|
# tab-separated line per invocation into this TREE's own
|
|
206
207
|
# logs/L0-shim/<session>-<agent|coordinator>-<binName>.log (gitignored), so the
|
|
@@ -381,6 +382,17 @@ if printf '%s' "$CMD" | grep -Eq '^(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'\''[^'\'
|
|
|
381
382
|
wp_log "$WP_FAULT" ALLOW-CURE # record the self-heal we let through (re-enables the guards)
|
|
382
383
|
exit 0 # allow the cure so the assistant can break the deadlock
|
|
383
384
|
fi
|
|
385
|
+
# THE HARNESS-GATED TAIL OF THE SAME LIST, and the ONLY place $AI changes a decision. Under Claude Code
|
|
386
|
+
# the guard is false and the next line is the deny — byte for byte the path a Claude payload took before
|
|
387
|
+
# this branch existed. Under Codex it is the twin of the ALLOW-READ arm above: Codex has NO Read tool, so
|
|
388
|
+
# a read arrives as this Bash command, and without this every L0 fault denies a Codex session the very
|
|
389
|
+
# reads the deny is telling it to perform. The pattern is anchored on its own (no cd prefix, no capture
|
|
390
|
+
# tail) — see L0_CODEX_ALLOW_ERE. TERMINAL, exactly like the Read arm, and for the same reason: on this
|
|
391
|
+
# path the bin never runs, so there is nothing to fall through to.
|
|
392
|
+
if [ "$AI" = codex ] && printf '%s' "$CMD" | grep -Eq '^(((cat|head|tail|less|more|bat)([[:space:]]+((-[0-9]+|-{1,2}[A-Za-z][A-Za-z0-9=._-]*)|([A-Za-z0-9._/@~+,:][A-Za-z0-9._/@~+,:-]*|'\''[^'\'']+'\'')))*[[:space:]]+([A-Za-z0-9._/@~+,:][A-Za-z0-9._/@~+,:-]*|'\''[^'\'']+'\'')([[:space:]]+((-[0-9]+|-{1,2}[A-Za-z][A-Za-z0-9=._-]*)|([A-Za-z0-9._/@~+,:][A-Za-z0-9._/@~+,:-]*|'\''[^'\'']+'\'')))*|sed[[:space:]]+-n[[:space:]]+([0-9]+(,[0-9]+)?p|'\''[0-9]+(,[0-9]+)?p'\'')([[:space:]]+((-[0-9]+|-{1,2}[A-Za-z][A-Za-z0-9=._-]*)|([A-Za-z0-9._/@~+,:][A-Za-z0-9._/@~+,:-]*|'\''[^'\'']+'\'')))*[[:space:]]+([A-Za-z0-9._/@~+,:][A-Za-z0-9._/@~+,:-]*|'\''[^'\'']+'\'')([[:space:]]+((-[0-9]+|-{1,2}[A-Za-z][A-Za-z0-9=._-]*)|([A-Za-z0-9._/@~+,:][A-Za-z0-9._/@~+,:-]*|'\''[^'\'']+'\'')))*))[[:space:]]*$'; then
|
|
393
|
+
wp_log "$WP_FAULT" ALLOW-CODEX-READ
|
|
394
|
+
exit 0
|
|
395
|
+
fi
|
|
384
396
|
wp_log "$WP_FAULT" "$DENY_LABEL" # every fail-closed block, with the fault that caused it
|
|
385
397
|
if [ -n "$BROKEN_BIN" ]; then
|
|
386
398
|
# Report (do NOT auto-clean) the orphaned pnpm staging dirs — a package pnpm was mid-way through
|