@webpieces/ai-hook-rules 0.4.710 → 0.4.712
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 +5 -4
- package/src/adapters/detect-ai.js +5 -4
- package/src/adapters/detect-ai.js.map +1 -1
- package/src/adapters/hook-core.js +9 -3
- package/src/adapters/hook-core.js.map +1 -1
- package/src/bin/codex-guard-presence.d.ts +86 -0
- package/src/bin/codex-guard-presence.js +150 -0
- package/src/bin/codex-guard-presence.js.map +1 -0
- package/src/bin/codex-trust.d.ts +99 -0
- package/src/bin/codex-trust.js +199 -0
- package/src/bin/codex-trust.js.map +1 -0
- package/src/bin/hook-registration.d.ts +200 -43
- package/src/bin/hook-registration.js +218 -89
- package/src/bin/hook-registration.js.map +1 -1
- package/src/bin/l0-allowlist.d.ts +0 -30
- package/src/bin/l0-allowlist.js +9 -67
- package/src/bin/l0-allowlist.js.map +1 -1
- package/src/bin/l0-decide.d.ts +43 -0
- package/src/bin/l0-decide.js +90 -0
- package/src/bin/l0-decide.js.map +1 -0
- package/src/bin/l0-ignored-tools.d.ts +38 -0
- package/src/bin/l0-ignored-tools.js +44 -0
- package/src/bin/l0-ignored-tools.js.map +1 -0
- package/src/bin/setup-config.d.ts +19 -0
- package/src/bin/setup-config.js +370 -0
- package/src/bin/setup-config.js.map +1 -0
- package/src/bin/setup.d.ts +56 -21
- package/src/bin/setup.js +125 -370
- package/src/bin/setup.js.map +1 -1
- package/src/bin/shim-audit-log.js +10 -1
- package/src/bin/shim-audit-log.js.map +1 -1
- package/src/bin/shim-deny-reason.js +4 -4
- package/src/bin/shim-deny-reason.js.map +1 -1
- package/src/bin/shim.d.ts +2 -0
- package/src/bin/shim.js +21 -1
- package/src/bin/shim.js.map +1 -1
- package/src/bin/upgrade-shim.d.ts +1 -1
- package/src/bin/upgrade-shim.js +12 -8
- package/src/bin/upgrade-shim.js.map +1 -1
- package/src/core/agent-event.d.ts +16 -0
- package/src/core/agent-event.js +17 -1
- package/src/core/agent-event.js.map +1 -1
- package/src/core/ai-type-context.d.ts +36 -0
- package/src/core/ai-type-context.js +45 -0
- package/src/core/ai-type-context.js.map +1 -0
- package/src/core/decision-log.js +9 -0
- package/src/core/decision-log.js.map +1 -1
- package/src/core/l0-matrix.js +14 -6
- package/src/core/l0-matrix.js.map +1 -1
- package/src/core/l0-tooling-doc.d.ts +3 -2
- package/src/core/l0-tooling-doc.js +22 -10
- package/src/core/l0-tooling-doc.js.map +1 -1
- package/src/core/rejection-log.js +5 -1
- package/src/core/rejection-log.js.map +1 -1
- package/src/index.d.ts +1 -1
- package/src/index.js +4 -1
- package/src/index.js.map +1 -1
- package/templates/ai-hook.sh +15 -1
package/src/bin/setup.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { allRuleNames, seedEntryForRule, schemaFieldNames, sectionForRule, isHookGuard, DEFAULT_MATCH_RULES, DEFAULT_BUILD_COMMAND, RETIRED_CONFIG_KEYS, RETIRED_SCOPE_RULE, RepoRootFinder, writeTemplate, writeTemplateIfMissing, CONFIG_POLICY_DOC } from '@webpieces/rules-config';\n\nimport { toError } from '../core/to-error';\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\nimport {\n ClaudeSettings, HookCommand, HookEntry, HookRegistrationEntry, GUARDS_BIN, LEGACY_GUARANTEE_ROOT_MARKER,\n GUARDS_MATCHER, RULES_BIN, RULES_MATCHER, addHookEntry, applyManagedEnv, readSettings, shimCommand,\n writeSettings,\n} from './hook-registration';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\n\nconst CONFIG_FILENAME = 'webpieces.config.json';\n// The seeded buildCommand comes from @webpieces/rules-config, NOT from a copy here. This file used to\n// hold its own — with `--base=origin/main`, a DIFFERENT base from the one the gate documents — so a\n// freshly set-up repo was seeded with a command that rebuilds projects touched by other people's\n// merged PRs, and whole-repo-build-guard then quoted that command back in its refusals.\nconst DEFAULT_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nconst DEFAULT_MERGE_COMPLETE = 'pnpm wp-finish-upsert-pr';\n\n// ---------------------------------------------------------------------------\n// The two independently-installable GUARD hooks. Each can land in a different settings file (see\n// InstallTarget) so a team can ship the guards while a developer keeps the code-style rules local\n// while iterating. Both are registered ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every\n// tree. There used to be a third — the L-1 hook, which existed only to keep a RELATIVE registration\n// resolvable; it is retired, and purgeRetiredGuaranteeRoot() removes anything left of it.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly matcher: string,\n readonly bin: string,\n ) {}\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project targets get the ABSOLUTE shim command, `$CLAUDE_PROJECT_DIR/.claude/webpieces/ai-hook.sh`\n // (see hook-registration.ts for why): it resolves from ANY cwd, so a hook can never fail to launch —\n // which per the hooks reference would be exit 127, a SILENT UNGUARDED ALLOW rather than a block.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return shimCommand(this.bin);\n }\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// A managed .sh is shared by the project hooks — only safe to delete once no project settings file\n// references it anymore (i.e. the other hook was moved to global or uninstalled too).\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction markerReferenced(targets: InstallTarget[], marker: string): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(marker)));\n });\n}\n\n/**\n * Remove every trace of the RETIRED L-1 hook (`guarantee-root.sh`) — the file and any PreToolUse entry\n * still pointing at it. REMOVAL ONLY: nothing here can ever write one back.\n *\n * L-1 existed to guarantee a RELATIVE guard-hook path resolved, by refusing any `cd` that would park the\n * shell in a subdirectory. The guard hooks are ABSOLUTE now (`$CLAUDE_PROJECT_DIR/...`), so they resolve\n * from any cwd and the guarantee is structural — there is nothing left to police, and the subdirectory\n * denial that used to pay for it is gone with it.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction purgeRetiredGuaranteeRoot(targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n if (removeHookByMarker(settings, LEGACY_GUARANTEE_ROOT_MARKER)) {\n writeSettings(target.settingsPath, settings);\n console.log(` 🗑️ removed the retired L-1 guarantee-root hook from ${target.label}`);\n }\n }\n const legacyFile = path.join(projectRoot, LEGACY_GUARANTEE_ROOT_MARKER);\n if (fs.existsSync(legacyFile)) {\n fs.rmSync(legacyFile, { force: true });\n console.log(' 🗑️ deleted the retired .claude/webpieces/guarantee-root.sh');\n }\n}\n\nexport class InstallTarget {\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n ) {}\n}\n\n// The matchers and bin names come from ./hook-registration, which is also what the drift check and\n// wp-upgrade-shim compare against — one spelling of the registration, or the installer and the\n// validator can disagree about what \"installed\" means.\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', RULES_MATCHER, RULES_BIN);\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', GUARDS_MATCHER, GUARDS_BIN);\n\n// `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n// ~/.claude/settings.json (a unit test must never write the user's actual global settings).\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// webpieces.config.json seeding + migration to the rules / hookGuards / commands layout.\n// ---------------------------------------------------------------------------\n// webpieces-disable no-any-unknown -- webpieces.config.json / settings.json are opaque consumer JSON\ntype Json = Record<string, unknown>;\ntype RuleEntry = Json;\ntype Section = Record<string, RuleEntry>;\n\ninterface ConfigFile {\n extends?: string;\n rules: Section;\n hookGuards: Section;\n commands: Json;\n excludePaths: string[];\n 'match-rules': Json[];\n rulesDir: string[];\n}\n\ninterface MigrateResult {\n config: ConfigFile;\n changes: string[];\n}\n\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedRule(ruleName: string): RuleEntry {\n // Both escape hatches are seeded (and REQUIRED) so every rule block shows them: 0 = active,\n // null = no branch scoping. A human/AI edits these to time-box or branch-scope a rule off.\n //\n // The ENTIRE entry comes from rules-config's seedEntryForRule() — the same module that owns the\n // schema the loader validates against, so the installer can never emit an entry the loader\n // rejects. It supplies: the recommended mode (the SAME recommendation the validator prints in its\n // copy-paste snippet, so seed and advice cannot disagree), both hatches, and a default for every\n // other schema-REQUIRED field. Seeding used to be a flat 'OFF' plus the two hatches, which was\n // wrong twice over: adopters got nothing enforced, AND the entry was missing required fields\n // (e.g. branch-creation-guard.autoReapMergedBranches), so the config failed to load on first run.\n return seedEntryForRule(ruleName);\n}\n\n// The guard-hint command strings live under `guardHints`. The flat `upsertPr`/`mergeComplete` keys this\n// used to seed are RETIRED and now fail validation — seeding them meant every freshly installed repo was\n// born on a shape the validator rejects.\nfunction seedCommands(): Json {\n return {\n 'pr-gate': { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] },\n guardHints: { prCreationOrPush: DEFAULT_UPSERT_PR, mergeInProgress: DEFAULT_MERGE_COMPLETE },\n };\n}\n\n// Required excludePaths block: ONE glob list suppressing hook enforcement per file path. Seeded empty\n// (enforce everywhere) — a client adds paths (e.g. \"repositories/**\") to exempt vendored trees.\n//\n// Deliberately NOT seeded with webpieces' own `.webpieces/` state dir. That exemption lives in CODE\n// (`isWebpiecesStateDir`, consulted by `filterByExcludedPaths` ahead of this list and regardless of it),\n// and a glob here would be a second, weaker spelling of it — weaker because `.webpieces/**` compiles to\n// an anchored regex that misses the bare directory the predicate matches, and because a config entry\n// invites a consumer to delete it and believe the exemption went with it.\n// webpieces-disable no-function-outside-class -- sibling of the other seed* helpers; this module is config-shape builders by design\nfunction seedExcludePaths(): string[] {\n return [];\n}\n\n// Bring an existing `excludePaths` forward to the single-list shape. Already a list → untouched.\n// Legacy `{ rules, guards }` → unioned (order preserved, duplicates dropped) and recorded as a change\n// so `wp-install-ai-hooks` is the migration path rather than a hand-edit. Anything else → seeded [].\n// webpieces-disable no-any-unknown -- `raw` is opaque consumer JSON until narrowed here\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateExcludePaths(raw: unknown, changes: string[]): string[] {\n if (Array.isArray(raw)) return (raw as string[]).filter(p => typeof p === 'string');\n if (typeof raw === 'object' && raw !== null) {\n // webpieces-disable no-any-unknown -- narrowing the opaque legacy block from consumer JSON\n const legacy = raw as Record<string, unknown>;\n const rules = Array.isArray(legacy['rules']) ? (legacy['rules'] as string[]) : [];\n const guards = Array.isArray(legacy['guards']) ? (legacy['guards'] as string[]) : [];\n const merged = [...new Set([...rules, ...guards].filter(p => typeof p === 'string'))];\n changes.push(`migrated excludePaths {rules,guards} -> one list (${merged.length} path(s))`);\n return merged;\n }\n changes.push('added excludePaths ([])');\n return seedExcludePaths();\n}\n\n/** One retired flat command string and the guardHints field it becomes. Data-only (per CLAUDE.md). */\nclass GuardHintMove {\n retiredKey: string;\n hintKey: string;\n fallback: string;\n\n constructor(retiredKey: string, hintKey: string, fallback: string) {\n this.retiredKey = retiredKey;\n this.hintKey = hintKey;\n this.fallback = fallback;\n }\n}\n\n/**\n * Bring `commands` forward to the `guardHints` shape, moving the RETIRED flat `upsertPr`/`mergeComplete`\n * strings and DELETING them. Deleting is the point: the validator now rejects them, so leaving them behind\n * would keep the config failing after a \"successful\" sync.\n *\n * The consumer's own value wins over the default — a repo that renamed its gated command keeps that name.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateGuardHints(commands: Json, changes: string[]): void {\n const hints: Json = (typeof commands['guardHints'] === 'object' && commands['guardHints'] !== null)\n ? (commands['guardHints'] as Json) : {};\n const moves: readonly GuardHintMove[] = [\n new GuardHintMove('upsertPr', 'prCreationOrPush', DEFAULT_UPSERT_PR),\n new GuardHintMove('mergeComplete', 'mergeInProgress', DEFAULT_MERGE_COMPLETE),\n ];\n for (const move of moves) {\n const retiredKey = move.retiredKey;\n const hintKey = move.hintKey;\n const fallback = move.fallback;\n const carried = commands[retiredKey];\n if (carried !== undefined) {\n delete commands[retiredKey];\n if (hints[hintKey] === undefined) hints[hintKey] = carried;\n changes.push(`moved retired commands.${retiredKey} -> commands.guardHints.${hintKey}`);\n }\n if (hints[hintKey] === undefined) {\n hints[hintKey] = fallback;\n changes.push(`added commands.guardHints.${hintKey}`);\n }\n }\n commands['guardHints'] = hints;\n}\n\n/**\n * Apply the RETIRED rule/guard retirements in place. These used to be rewritten silently at load time, so\n * a consumer's file kept the dead name forever; the loader now rejects it, which makes this the one\n * command that can fix the file. Skips a rename when the new name is already configured, so an explicit\n * entry is never clobbered by a stale one.\n *\n * NOT EVERY RETIREMENT IS A RENAME, and treating them all as one produced garbage. `whole-repo-build-guard`\n * moved OUT of webpieces.config.json entirely — its `movedTo` is the PROSE destination\n * `~/.webpieces/config.json → experimental.whole-repo-build-guard`, not a sibling key — so the rename\n * branch below would have created a hookGuards entry literally named that whole sentence, which no\n * validator knows and which the next run reports as another unknown rule. `prunable` is the discriminator:\n * when the entry says deleting is the whole fix, DELETE it, exactly as `ConfigPruner` does.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction migrateRetiredRuleNames(section: Section, changes: string[]): void {\n for (const entry of RETIRED_CONFIG_KEYS) {\n if (entry.scope !== RETIRED_SCOPE_RULE) continue;\n if (!(entry.key in section)) continue;\n if (entry.prunable) {\n delete section[entry.key];\n changes.push(`deleted retired \"${entry.key}\" (it moved to ${entry.movedTo})`);\n continue;\n }\n mergeIntoDestination(section, entry.key, entry.movedTo, changes);\n }\n fillRequiredFields(section, changes);\n}\n\n/**\n * Fold one retired key's entry into its destination, whether the destination exists yet or not.\n *\n * THIS IS N→1, NOT 1:1, and the difference is the whole reason this helper exists. Four retired keys\n * now point at ONE destination (`branch-state-guard`, `pr-lifecycle-guard`). The previous code renamed\n * the first key it met and then, finding the destination already present, DELETED each of the other\n * three outright — so which guard's settings survived depended on RETIRED_CONFIG_KEYS declaration\n * order rather than on the consumer's file, and the survivor carried only that one guard's fields, so\n * it was missing required fields of the merged schema. `wp-install-ai-hooks` is the command advertised\n * as the migration path; half-migrating every consumer into an invalid config is not an option.\n *\n * UNION, first writer wins per field. Earlier-declared keys are the more specific ones (only\n * feature-branch-guard carries `branchNamingConvention`), and a field already present on the\n * destination — because the consumer wrote it, or an earlier key contributed it — is never overwritten.\n * Fields the merged schema does not know are dropped by the same pass, since carrying a deleted field\n * across (`upsertPrCommand`) would produce a config the validator immediately rejects.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction mergeIntoDestination(section: Section, key: string, destination: string, changes: string[]): void {\n const source = asSection(section[key]);\n delete section[key];\n const fields = schemaFieldNames(destination);\n const target = asSection(section[destination]);\n const existed = destination in section;\n const carried: string[] = [];\n const dropped: string[] = [];\n for (const field of Object.keys(source)) {\n if (fields !== null && !fields.includes(field)) { dropped.push(field); continue; }\n if (field in target) continue;\n target[field] = source[field];\n carried.push(field);\n }\n section[destination] = target;\n const verb = existed ? 'merged' : 'renamed';\n const droppedNote = dropped.length > 0 ? `; dropped deleted field(s) ${dropped.join(', ')}` : '';\n changes.push(`${verb} retired \"${key}\" -> \"${destination}\" (carried ${carried.join(', ') || 'nothing new'}${droppedNote})`);\n}\n\n/**\n * Fill any schema-REQUIRED field a migrated entry ended up without.\n *\n * A union of four partial entries is not guaranteed to satisfy the destination's schema — the merged\n * `branch-state-guard` needs `mode` and both escape hatches, and a consumer whose four old entries\n * predate one of them would land short. Seeding the gap from the SAME source the installer and the\n * validator use (seedEntryForRule) is what makes the install command a complete instruction\n * rather than a first step. Only ever ADDS; a value the consumer stated is never touched.\n */\n// webpieces-disable no-function-outside-class -- sibling of the other seed*/migrate* helpers; this module is config-shape builders by design\nfunction fillRequiredFields(section: Section, changes: string[]): void {\n for (const name of Object.keys(section)) {\n if (schemaFieldNames(name) === null) continue;\n // A rule ENTRY is a flat bag of scalars, so it is read as Json here rather than through\n // asSection (whose values are whole entries). Same object either way; only the view differs.\n const entry: Json = asSection(section[name]);\n const seed = seedEntryForRule(name);\n const added: string[] = [];\n for (const field of Object.keys(seed)) {\n if (field in entry) continue;\n entry[field] = seed[field];\n added.push(field);\n }\n if (added.length === 0) continue;\n section[name] = entry;\n changes.push(`filled required field(s) on \"${name}\": ${added.join(', ')}`);\n }\n}\n\n// Deep-copy the framework's default match-rules (the no-fetch guard) into plain JSON for the config\n// file. Round-tripping through JSON turns the MatchRuleConfig instances into plain objects.\nfunction seedMatchRules(): Json[] {\n return JSON.parse(JSON.stringify(DEFAULT_MATCH_RULES)) as Json[];\n}\n\nfunction buildSeedConfig(): ConfigFile {\n const rules: Section = {};\n const hookGuards: Section = {};\n for (const name of allRuleNames()) {\n if (sectionForRule(name) === 'hookGuards') hookGuards[name] = seedRule(name);\n else rules[name] = seedRule(name);\n }\n return {\n rules, hookGuards, commands: seedCommands(), excludePaths: seedExcludePaths(),\n // Seed the required match-rules array with the framework's default no-fetch guard. A fresh\n // project gets contract-first enforcement out of the box; clients edit it and add more entries.\n 'match-rules': seedMatchRules(),\n rulesDir: [],\n };\n}\n\nfunction writeConfig(configPath: string, config: ConfigFile): void {\n fs.writeFileSync(configPath, JSON.stringify(config, null, 4) + '\\n');\n}\n\nfunction readConfig(configPath: string): Json {\n const raw = fs.readFileSync(configPath, 'utf8');\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n return JSON.parse(raw) as Json;\n } catch (err: unknown) {\n const error = toError(err);\n throw new Error(`${CONFIG_FILENAME} has invalid JSON — fix it, then retry: ${error.message}`, { cause: error });\n }\n}\n\nfunction asSection(value: Json[string]): Section {\n return (typeof value === 'object' && value !== null && !Array.isArray(value)) ? (value as Section) : {};\n}\n\n// Migrate an existing config to the rules / hookGuards / commands layout and add any missing rules.\n// Returns a human-readable list of what changed (empty = already up to date).\nexport function migrate(existing: Json): MigrateResult {\n const changes: string[] = [];\n const rules: Section = asSection(existing['rules']);\n const hookGuards: Section = asSection(existing['hookGuards']);\n const commands: Json = (typeof existing['commands'] === 'object' && existing['commands'] !== null)\n ? (existing['commands'] as Json) : {};\n\n // Move a deprecated top-level pr-gate block under commands.\n if (existing['pr-gate'] !== undefined && commands['pr-gate'] === undefined) {\n commands['pr-gate'] = existing['pr-gate'];\n changes.push('moved top-level \"pr-gate\" → commands[\"pr-gate\"]');\n }\n // Apply retired RENAMES first, so a renamed guard is placed and presence-checked under its new name\n // rather than being treated as unknown and re-added alongside its own stale entry.\n migrateRetiredRuleNames(rules, changes);\n migrateRetiredRuleNames(hookGuards, changes);\n\n // Move guards mistakenly left in rules into hookGuards.\n for (const name of Object.keys(rules)) {\n if (isHookGuard(name)) {\n hookGuards[name] = rules[name];\n delete rules[name];\n changes.push(`moved \"${name}\" from rules → hookGuards`);\n }\n }\n // Move code rules mistakenly placed in hookGuards back into rules.\n for (const name of Object.keys(hookGuards)) {\n if (!isHookGuard(name) && allRuleNames().includes(name)) {\n rules[name] = hookGuards[name];\n delete hookGuards[name];\n changes.push(`moved \"${name}\" from hookGuards → rules`);\n }\n }\n // Add any missing built-in into its correct section, ENFORCING at its recommended mode (not OFF).\n for (const name of allRuleNames()) {\n const target = sectionForRule(name) === 'hookGuards' ? hookGuards : rules;\n if (!(name in target)) {\n const entry = seedRule(name);\n target[name] = entry;\n changes.push(`added \"${name}\" (${String(entry['mode'])}) to ${sectionForRule(name)}`);\n }\n }\n // Fill command defaults.\n if (commands['pr-gate'] === undefined) {\n commands['pr-gate'] = { mode: 'OFF', buildCommand: DEFAULT_BUILD_COMMAND, gates: [] };\n changes.push('added commands[\"pr-gate\"] (OFF)');\n }\n migrateGuardHints(commands, changes);\n\n // Seed the now-required excludePaths list (empty = enforce everywhere) if the config predates it,\n // and MIGRATE the legacy `{ rules: [], guards: [] }` object to the single list by unioning them.\n // The union is behaviour-preserving for every config we have seen (both lists set identically), and\n // widening is the safe direction anyway: a path either side excluded stays excluded.\n const excludePaths: string[] = migrateExcludePaths(existing['excludePaths'], changes);\n\n // Seed the now-required match-rules array (with the default no-fetch guard) if the config predates\n // it. A client that has already customized it keeps their array untouched.\n let matchRules: Json[];\n if (Array.isArray(existing['match-rules'])) {\n matchRules = existing['match-rules'] as Json[];\n } else {\n matchRules = seedMatchRules();\n changes.push('added \"match-rules\" (seeded with the no-fetch guard)');\n }\n\n const rulesDir: string[] = Array.isArray(existing['rulesDir']) ? (existing['rulesDir'] as string[]) : [];\n const config: ConfigFile = { rules, hookGuards, commands, excludePaths, 'match-rules': matchRules, rulesDir };\n if (typeof existing['extends'] === 'string') config.extends = existing['extends'];\n return { config, changes };\n}\n\n// Seed the config when it is missing, migrate it when it is not. ONE behaviour, always — there is no\n// \"migrate but stop here\" mode any more. The flag that used to select it was never NECESSARY (the validator prints\n// the exact edit for every error at once, and editing webpieces.config.json is always allowed through the\n// guard — the documented primary cure), it REFUSED to act when the config was missing (useless in the one\n// case automation would have helped), and it gave deny messages a second competing path when they are\n// supposed to end in exactly one action. Readers also mistook it for the shim-repair command, which it\n// never was — `wp-upgrade-shim` is that.\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction seedOrSyncConfig(projectRoot: string): void {\n const configPath = path.join(projectRoot, CONFIG_FILENAME);\n if (!fs.existsSync(configPath)) {\n writeConfig(configPath, buildSeedConfig());\n console.log(` [ai-hooks] Created ${CONFIG_FILENAME} (rules / hookGuards / commands); each rule seeded at its recommended mode — gradual where supported, so only code you change is enforced.`);\n console.log(' Enable the ones you want by changing \"mode\".');\n return;\n }\n const result = migrate(readConfig(configPath));\n if (result.changes.length === 0) {\n console.log(` [ai-hooks] ${CONFIG_FILENAME} already uses the rules / hookGuards / commands layout — no changes.`);\n return;\n }\n writeConfig(configPath, result.config);\n console.log(` [ai-hooks] Migrated ${CONFIG_FILENAME}:`);\n for (const change of result.changes) console.log(` - ${change}`);\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command containing `marker` (a bin name, or a managed .sh path); returns true\n// if anything was removed. REMOVE-then-ADD is what keeps an upgrade from leaving a superseded spelling\n// (a relative command, or the retired L-1 entry) beside the current one — two spellings of one\n// registration is the compatibility shim the backwards-compat reviewer rejects.\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction removeHookByMarker(settings: ClaudeSettings, marker: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(marker));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHookByMarker(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHookEntry(settings, new HookRegistrationEntry(hook.matcher, hook.commandFor(target, projectRoot)));\n // The managed `env` entry goes into the SAME file the hooks go into, on every path that\n // writes hooks — interactive or `--target=`. It pins the Bash cwd to the project root, so a\n // guard's answer depends on the command rather than on where an earlier `cd` left the shell,\n // and settings `env` is inherited, so every subagent gets the identical cwd and therefore the\n // identical guard verdict. (It no longer has a RESOLUTION job — the hooks are absolute.) See\n // hook-registration.ts for the full argument; `wp-upgrade-shim` self-heals it afterwards.\n if (applyManagedEnv(settings)) {\n console.log(` ✅ env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} → ${target.label} (pins the Bash cwd to the project root, for this session and every subagent)`);\n }\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!markerReferenced(targets, SHIM_MARKER)) {\n removeShim(projectRoot);\n }\n // The RETIRED L-1 hook rode with the GUARDS hook, so its removal does too. Doing it here rather than\n // at a separate call site means every existing caller of applyHook — the installer's interactive and\n // --target paths, and every test — converges on the two-hook absolute form with no second step.\n if (hook.bin === GUARDS_BIN) purgeRetiredGuaranteeRoot(targets, projectRoot);\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label} [matcher: ${hook.matcher}]`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n for (const target of targets) console.log(` ${target.choice}) ${target.label}`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n const chosen = targets.find((t: InstallTarget) => t.choice === answer) ?? null;\n applyHook(hook, chosen, targets, projectRoot);\n}\n\n/**\n * Scaffold the SERVER-SIDE PR gate: the CI workflow plus the doc explaining how to turn it on.\n *\n * This lives in the installer, not the PR flow. `wp-start-upsert-pr` used to do it — printing\n * copy-to-`.github` and branch-protection instructions on EVERY run, at an agent doing feature work\n * that could not act on them anyway (marking a check required needs a repo admin). Setup is a\n * one-time, admin-shaped act, so it belongs with the other one-time setup.\n *\n * Written UNCONDITIONALLY, unlike the old version which required a `gateSalt` to already be set: the\n * whole point of the doc is to tell you to set one, so gating it on the thing it teaches meant the\n * instructions only appeared to repos that no longer needed them.\n *\n * Both land in gitignored `.webpieces/instruct-ai/`, never `.github/` directly — writing there would\n * dirty the tree, and copying it is the human's decision. `IfMissing` for the yml so a repo that has\n * customized its workflow never gets it clobbered; the doc itself is refreshed so it cannot go stale.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction scaffoldCiGate(projectRoot: string): void {\n writeTemplateIfMissing(projectRoot, 'webpieces-pr-gate.yml');\n writeTemplate(projectRoot, 'webpieces.ci-gate-setup.md');\n console.log('');\n console.log('ℹ️ Optional: server-side PR gate (stops an UNHOOKED teammate opening a PR in the web UI).');\n console.log(' It is OFF until you set a gateSalt. Three steps, one of which needs a repo admin:');\n console.log(' .webpieces/instruct-ai/webpieces.ci-gate-setup.md');\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n // Anchor the install at the repo root (git toplevel — webpieces.config.json may not exist yet on\n // a first install), never a subdir cwd, so `.webpieces`/hooks/config all land at the root.\n const projectRoot = new RepoRootFinder().resolveRepoRoot(process.cwd());\n\n seedOrSyncConfig(projectRoot);\n // Always refreshed: it explains why a retired key is rejected rather than accepted, and what to do\n // about it — which is exactly what an agent needs on the run where a migration just moved keys out\n // from under its config.\n writeTemplate(projectRoot, CONFIG_POLICY_DOC);\n\n scaffoldCiGate(projectRoot);\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n const chosen = targets.find((t: InstallTarget): boolean => t.choice === choice) ?? null;\n applyHook(RULES_HOOK, chosen, targets, projectRoot);\n applyHook(GUARDS_HOOK, chosen, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n console.log('');\n console.log('Done. Re-run `pnpm wp-install-ai-hooks` anytime to move or uninstall a hook.');\n console.log('(Non-interactive: pnpm wp-install-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"setup.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/setup.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { homedir } from 'os';\nimport { createInterface } from 'readline';\n\nimport { RepoRootFinder, writeTemplate, writeTemplateIfMissing, CONFIG_POLICY_DOC } from '@webpieces/rules-config';\n\nimport { SHIM_MARKER, shimPath, renderShim } from './shim';\nimport {\n ClaudeSettings, HookCommand, HookEntry, HookRegistrationEntry, GUARDS_BIN, LEGACY_GUARANTEE_ROOT_MARKER,\n RULES_BIN, addHookEntry, applyManagedEnv, readSettings, writeSettings,\n HarnessRegistration, CLAUDE_REGISTRATION, CODEX_REGISTRATION,\n} from './hook-registration';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { CodexTrustProbe } from './codex-trust';\n// The config half of the installer — seeding and migrating webpieces.config.json. Split for size along\n// the seam that was already there: this module is hook WIRING, that one is CONFIG SHAPE.\nimport { seedOrSyncConfig } from './setup-config';\n\n\n// ---------------------------------------------------------------------------\n// The two independently-installable GUARD hooks. Each can land in a different settings file (see\n// InstallTarget) so a team can ship the guards while a developer keeps the code-style rules local\n// while iterating. Both are registered ABSOLUTE via $CLAUDE_PROJECT_DIR, so the MAIN tree governs every\n// tree. There used to be a third — the L-1 hook, which existed only to keep a RELATIVE registration\n// resolvable; it is retired, and purgeRetiredGuaranteeRoot() removes anything left of it.\n// ---------------------------------------------------------------------------\nclass HookSpec {\n constructor(\n readonly key: string,\n readonly label: string,\n readonly bin: string,\n ) {}\n\n /**\n * WHICH TOOL NAMES this hook must see, in the harness the target belongs to.\n *\n * It is a lookup on the target rather than a field on the spec because the answer is not a property\n * of the hook: the rules hook matches `Write|Edit|MultiEdit` under Claude Code and `apply_patch`\n * under Codex, and a single stored matcher is exactly how `.codex/hooks.json` came to be registered\n * against tool names Codex never emits.\n */\n matcherFor(target: InstallTarget): string {\n return target.harness.matcherFor(this.bin);\n }\n\n // Absolute targets (global) need the exact path to this repo's bin — no ~/.webpieces bridge.\n // Project targets get the ABSOLUTE shim command, anchored on the harness's own project-root variable\n // (see HarnessRegistration.shimCommand): it resolves from ANY cwd, so a hook can never fail to\n // launch — which per the hooks reference would be exit 127, a SILENT UNGUARDED ALLOW, not a block.\n commandFor(target: InstallTarget, projectRoot: string): string {\n if (target.absolute) {\n return `node ${path.join(projectRoot, 'node_modules', '.bin', this.bin)}`;\n }\n return target.harness.shimCommand(this.bin);\n }\n}\n\n// Idempotent: re-running the installer overwrites the managed shim in place.\nfunction writeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n fs.mkdirSync(path.dirname(target), { recursive: true });\n fs.writeFileSync(target, renderShim(), { mode: 0o755 });\n // writeFileSync's mode is only applied when creating the file; force it on overwrite too.\n fs.chmodSync(target, 0o755);\n}\n\nfunction removeShim(projectRoot: string): void {\n const target = shimPath(projectRoot);\n if (fs.existsSync(target)) fs.rmSync(target);\n}\n\n// A managed .sh is shared by the project hooks — only safe to delete once no project settings file\n// references it anymore (i.e. the other hook was moved to global or uninstalled too).\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction markerReferenced(targets: InstallTarget[], marker: string): boolean {\n return targets.some((t: InstallTarget) => {\n const entries = readSettings(t.settingsPath).hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(marker)));\n });\n}\n\n/**\n * Remove every trace of the RETIRED L-1 hook (`guarantee-root.sh`) — the file and any PreToolUse entry\n * still pointing at it. REMOVAL ONLY: nothing here can ever write one back.\n *\n * L-1 existed to guarantee a RELATIVE guard-hook path resolved, by refusing any `cd` that would park the\n * shell in a subdirectory. The guard hooks are ABSOLUTE now (`$CLAUDE_PROJECT_DIR/...`), so they resolve\n * from any cwd and the guarantee is structural — there is nothing left to police, and the subdirectory\n * denial that used to pay for it is gone with it.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction purgeRetiredGuaranteeRoot(targets: InstallTarget[], projectRoot: string): void {\n for (const target of targets) {\n const settings = readSettings(target.settingsPath);\n if (removeHookByMarker(settings, LEGACY_GUARANTEE_ROOT_MARKER)) {\n writeSettings(target.settingsPath, settings);\n console.log(` 🗑️ removed the retired L-1 guarantee-root hook from ${target.label}`);\n }\n }\n const legacyFile = path.join(projectRoot, LEGACY_GUARANTEE_ROOT_MARKER);\n if (fs.existsSync(legacyFile)) {\n fs.rmSync(legacyFile, { force: true });\n console.log(' 🗑️ deleted the retired .claude/webpieces/guarantee-root.sh');\n }\n}\n\nexport class InstallTarget {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly choice: string,\n readonly label: string,\n readonly settingsPath: string,\n readonly absolute: boolean,\n /**\n * WHICH HARNESS this file arms — it decides the matcher and the shim anchor written into it.\n *\n * REQUIRED, with no default, and the absence of one is the point. A default of\n * CLAUDE_REGISTRATION would make \"omit the harness\" mean \"Claude Code\" — a widening that is an\n * ABSENCE rather than a token, so a target built for Codex without it would silently carry\n * `Write|Edit|MultiEdit` and `$CLAUDE_PROJECT_DIR`, which is EXACTLY the silently-unguarded\n * state this class exists to end, and ungreppable besides.\n */\n readonly harness: HarnessRegistration,\n ) {}\n}\n\n// The bin names come from ./hook-registration, which is also what the drift check and wp-upgrade-shim\n// compare against — one spelling of the registration, or the installer and the validator can disagree\n// about what \"installed\" means. The MATCHER is not here: it belongs to the harness, not the hook (see\n// HookSpec.matcherFor).\nexport const RULES_HOOK = new HookSpec('rules', 'Rules hook (code-style validation)', RULES_BIN);\nexport const GUARDS_HOOK = new HookSpec('guards', 'Guards hook (git/PR/branch protection)', GUARDS_BIN);\n\n/**\n * Every file the installer can write, keyed by the CHOICE a human makes.\n *\n * TWO TARGETS SHARE CHOICE `1`, and that is the design rather than an oversight: \"the project, committed,\n * for the team\" is ONE intention, and a repo that is worked on by both harnesses needs both files armed\n * to mean it. Splitting it into two questions would let a human answer them differently and end up with a\n * repo where Codex is silently unguarded — which is the state this whole change exists to end. Every\n * caller selects by choice id (`targets.filter(t => t.choice === answer)`), never by index, so adding a\n * harness adds a row and changes no numbering.\n *\n * `2` (personal) and `3` (global) stay Claude-only because neither has a Codex counterpart: Codex reads\n * one repo-local `hooks.json` and has no personal or home-scoped hook file.\n *\n * `homeDir` is injectable so tests can point the global target at a temp dir instead of the real\n * ~/.claude/settings.json (a unit test must never write the user's actual global settings).\n */\nexport function installTargets(projectRoot: string, homeDir: string = homedir()): InstallTarget[] {\n return [\n new InstallTarget('1', 'project (.claude/settings.json — committed, for the team)',\n path.join(projectRoot, '.claude', 'settings.json'), false, CLAUDE_REGISTRATION),\n new InstallTarget('2', 'project for you (.claude/settings.local.json — personal)',\n path.join(projectRoot, '.claude', 'settings.local.json'), false, CLAUDE_REGISTRATION),\n new InstallTarget('3', 'global (~/.claude/settings.json — exact path, this repo only)',\n path.join(homeDir, '.claude', 'settings.json'), true, CLAUDE_REGISTRATION),\n // Given choice `1` so one answer arms the whole project — see the docblock. Its position in the\n // array carries no meaning: every caller selects by choice id.\n new InstallTarget('1', 'project, for Codex too (.codex/hooks.json — committed, for the team)',\n path.join(projectRoot, ...CODEX_REGISTRATION.settingsFiles[0].split('/')), false, CODEX_REGISTRATION),\n ];\n}\n\n// ---------------------------------------------------------------------------\n// Claude Code settings.json hook wiring.\n// ---------------------------------------------------------------------------\nexport function hasHook(settings: ClaudeSettings, bin: string): boolean {\n const entries = settings.hooks?.PreToolUse ?? [];\n return entries.some((e: HookEntry) => e.hooks.some((h: HookCommand) => h.command.includes(bin)));\n}\n\n// Drop every PreToolUse command containing `marker` (a bin name, or a managed .sh path); returns true\n// if anything was removed. REMOVE-then-ADD is what keeps an upgrade from leaving a superseded spelling\n// (a relative command, or the retired L-1 entry) beside the current one — two spellings of one\n// registration is the compatibility shim the backwards-compat reviewer rejects.\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction removeHookByMarker(settings: ClaudeSettings, marker: string): boolean {\n const entries = settings.hooks?.PreToolUse;\n if (!entries) return false;\n let changed = false;\n const kept: HookEntry[] = [];\n for (const entry of entries) {\n const hooks = entry.hooks.filter((h: HookCommand) => !h.command.includes(marker));\n if (hooks.length !== entry.hooks.length) changed = true;\n if (hooks.length > 0) kept.push({ matcher: entry.matcher, hooks });\n }\n if (changed) settings.hooks!.PreToolUse = kept;\n return changed;\n}\n\n// Apply the chosen install for one hook: remove it from every target file, then add it back to the\n// chosen one (or nowhere, for uninstall). Writes only the files that changed.\nexport function applyHook(hook: HookSpec, chosen: InstallTarget | null, targets: InstallTarget[], projectRoot: string): void {\n // SCOPED TO THE CHOSEN HARNESS, and this is what makes one call per target composable. Installing\n // the Claude hook must not strip the Codex one, so \"remove it from everywhere else\" means everywhere\n // else THIS HARNESS could live. Uninstall (chosen === null) is the one case that means every\n // harness, because \"not installed\" has to be true everywhere or the hook is still armed somewhere.\n const scope = chosen === null ? targets : targets.filter((t: InstallTarget) => t.harness === chosen.harness);\n for (const target of scope) {\n const settings = readSettings(target.settingsPath);\n const removed = removeHookByMarker(settings, hook.bin);\n const isChosen = chosen !== null && chosen.settingsPath === target.settingsPath;\n if (isChosen) {\n addHookEntry(settings, new HookRegistrationEntry(hook.matcherFor(target), hook.commandFor(target, projectRoot)));\n // The managed `env` entry goes into the SAME file the hooks go into, on every path that\n // writes hooks — interactive or `--target=`. It pins the Bash cwd to the project root, so a\n // guard's answer depends on the command rather than on where an earlier `cd` left the shell,\n // and settings `env` is inherited, so every subagent gets the identical cwd and therefore the\n // identical guard verdict. (It no longer has a RESOLUTION job — the hooks are absolute.) See\n // hook-registration.ts for the full argument; `wp-upgrade-shim` self-heals it afterwards.\n // …in the harness that HAS that surface. Codex has no settings `env`, and needs none: its\n // cwd is MEASURED not to drift, which is the whole thing this entry buys under Claude Code.\n if (target.harness.managesEnv && applyManagedEnv(settings)) {\n console.log(` ✅ env.${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE} → ${target.label} (pins the Bash cwd to the project root, for this session and every subagent)`);\n }\n writeSettings(target.settingsPath, settings);\n console.log(` ✅ ${hook.label} → ${target.label}`);\n } else if (removed) {\n writeSettings(target.settingsPath, settings);\n }\n }\n // Manage the shared checked-in shim: (re)write it whenever a project (relative) install exists,\n // otherwise clean it up once neither hook references it anymore.\n if (chosen !== null && !chosen.absolute) {\n writeShim(projectRoot);\n } else if (!markerReferenced(targets, SHIM_MARKER)) {\n removeShim(projectRoot);\n }\n // The RETIRED L-1 hook rode with the GUARDS hook, so its removal does too. Doing it here rather than\n // at a separate call site means every existing caller of applyHook — the installer's interactive and\n // --target paths, and every test — converges on the two-hook absolute form with no second step.\n if (hook.bin === GUARDS_BIN) purgeRetiredGuaranteeRoot(targets, projectRoot);\n if (chosen === null) console.log(` ⛔ ${hook.label} not installed (removed from all locations).`);\n}\n\n/**\n * Say whether Codex will actually RUN what we just registered — the one thing the installer cannot do\n * anything about and therefore must not leave silent.\n *\n * Codex trusts a hook entry TOFU and re-prompts whenever its bytes change, and the prompt's third option\n * is \"Continue without trusting (hooks won't run)\". So a perfectly successful install can be followed by\n * a fully unguarded session, and the only honest thing to print is what is true plus the one action a\n * HUMAN has to take. Nothing here writes `~/.codex/config.toml` — see codex-trust.ts for why forging a\n * `trusted_hash` is not on the table.\n *\n * Silent when this repo did not arm Codex, so a Claude-only install gains no noise.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction reportCodexTrust(projectRoot: string, targets: InstallTarget[], choice: string): void {\n const armed = targets.some((t: InstallTarget): boolean => t.choice === choice && t.harness === CODEX_REGISTRATION);\n if (!armed) return;\n const lines = new CodexTrustProbe().read(projectRoot).lines();\n if (lines.length === 0) return;\n console.log('');\n for (const line of lines) console.log(line);\n}\n\nfunction currentLocation(hook: HookSpec, targets: InstallTarget[]): string {\n const here = targets.filter((t: InstallTarget) => hasHook(readSettings(t.settingsPath), hook.bin));\n return here.length === 0 ? 'none' : here.map((t: InstallTarget) => t.label.split(' (')[0]).join(', ');\n}\n\nfunction prompt(question: string): Promise<string> {\n return new Promise((resolve: (answer: string) => void) => {\n const rl = createInterface({ input: process.stdin, output: process.stdout });\n rl.question(question, (answer: string) => { rl.close(); resolve(answer.trim()); });\n });\n}\n\n// Map a friendly `--target` name to an InstallTarget choice id (see installTargets). Returns null\n// for an unknown name so the caller can error out. Kept separate + exported for unit testing.\nexport function resolveTargetChoice(name: string): string | null {\n switch (name) {\n case 'project': return '1';\n case 'project-personal':\n case 'projectpersonal':\n case 'local': return '2';\n case 'global': return '3';\n case 'none':\n case 'uninstall': return '4';\n default: return null;\n }\n}\n\n// Extract the value of `--target=<name>` from argv (null if the flag is absent).\nexport function parseTargetArg(args: string[]): string | null {\n const flag = args.find((a: string): boolean => a.startsWith('--target='));\n return flag ? flag.slice('--target='.length) : null;\n}\n\n/**\n * Apply ONE choice for one hook: every target that answer selects, or uninstall when it selects none.\n *\n * The loop is what lets one choice arm several harnesses (choice `1` writes both the Claude settings\n * file and the Codex hooks file — see installTargets). `applyHook` scopes its removals to the chosen\n * target's harness, which is what keeps these calls from undoing one another.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nexport function applyChoice(hook: HookSpec, choice: string, targets: InstallTarget[], projectRoot: string): void {\n const chosen = targets.filter((t: InstallTarget): boolean => t.choice === choice);\n if (chosen.length === 0) {\n applyHook(hook, null, targets, projectRoot);\n return;\n }\n for (const target of chosen) applyHook(hook, target, targets, projectRoot);\n}\n\nasync function wireHook(hook: HookSpec, targets: InstallTarget[], projectRoot: string): Promise<void> {\n console.log('');\n console.log(`${hook.label}`);\n console.log(` currently installed in: ${currentLocation(hook, targets)}`);\n // The matcher is printed PER TARGET now, because it differs per harness — Codex's file tool is\n // `apply_patch`, Claude's are Write|Edit|MultiEdit — and one matcher printed above the list would be\n // wrong for whichever harness it did not describe.\n for (const target of targets) console.log(` ${target.choice}) ${target.label} [matcher: ${hook.matcherFor(target)}]`);\n console.log(' 4) none / uninstall');\n const answer = await prompt(' Where should it live? [1/2/3/4, default 4]: ');\n applyChoice(hook, answer, targets, projectRoot);\n}\n\n/**\n * Scaffold the SERVER-SIDE PR gate: the CI workflow plus the doc explaining how to turn it on.\n *\n * This lives in the installer, not the PR flow. `wp-start-upsert-pr` used to do it — printing\n * copy-to-`.github` and branch-protection instructions on EVERY run, at an agent doing feature work\n * that could not act on them anyway (marking a check required needs a repo admin). Setup is a\n * one-time, admin-shaped act, so it belongs with the other one-time setup.\n *\n * Written UNCONDITIONALLY, unlike the old version which required a `gateSalt` to already be set: the\n * whole point of the doc is to tell you to set one, so gating it on the thing it teaches meant the\n * instructions only appeared to repos that no longer needed them.\n *\n * Both land in gitignored `.webpieces/instruct-ai/`, never `.github/` directly — writing there would\n * dirty the tree, and copying it is the human's decision. `IfMissing` for the yml so a repo that has\n * customized its workflow never gets it clobbered; the doc itself is refreshed so it cannot go stale.\n */\n// webpieces-disable no-function-outside-class -- setup.ts is deliberately DI-free (it must run on a half-written node_modules; see install-entry.ts), so every function here is module-scope\nfunction scaffoldCiGate(projectRoot: string): void {\n writeTemplateIfMissing(projectRoot, 'webpieces-pr-gate.yml');\n writeTemplate(projectRoot, 'webpieces.ci-gate-setup.md');\n console.log('');\n console.log('ℹ️ Optional: server-side PR gate (stops an UNHOOKED teammate opening a PR in the web UI).');\n console.log(' It is OFF until you set a gateSalt. Three steps, one of which needs a repo admin:');\n console.log(' .webpieces/instruct-ai/webpieces.ci-gate-setup.md');\n}\n\nexport async function main(): Promise<void> {\n const args = process.argv.slice(2);\n // Anchor the install at the repo root (git toplevel — webpieces.config.json may not exist yet on\n // a first install), never a subdir cwd, so `.webpieces`/hooks/config all land at the root.\n const projectRoot = new RepoRootFinder().resolveRepoRoot(process.cwd());\n\n seedOrSyncConfig(projectRoot);\n // Always refreshed: it explains why a retired key is rejected rather than accepted, and what to do\n // about it — which is exactly what an agent needs on the run where a migration just moved keys out\n // from under its config.\n writeTemplate(projectRoot, CONFIG_POLICY_DOC);\n\n scaffoldCiGate(projectRoot);\n\n const targets = installTargets(projectRoot);\n\n // Non-interactive: `--target=project|project-personal|global|none` installs BOTH hooks at that\n // location without prompting, so an agent or CI can run the installer unattended (e.g. after a\n // @webpieces upgrade that changed the hook entry). Omit the flag for the interactive per-hook chooser.\n const targetName = parseTargetArg(args);\n if (targetName !== null) {\n const choice = resolveTargetChoice(targetName);\n if (choice === null) {\n console.error(`❌ Unknown --target '${targetName}'. Use one of: project | project-personal | global | none`);\n process.exitCode = 1;\n return;\n }\n applyChoice(RULES_HOOK, choice, targets, projectRoot);\n applyChoice(GUARDS_HOOK, choice, targets, projectRoot);\n console.log(`\\nDone. Both hooks set to: ${targetName}.`);\n reportCodexTrust(projectRoot, targets, choice);\n return;\n }\n\n console.log('');\n console.log('Two webpieces hooks can be installed independently — choose a location for each:');\n await wireHook(RULES_HOOK, targets, projectRoot);\n await wireHook(GUARDS_HOOK, targets, projectRoot);\n // Whichever choices were made above, report Codex trust for the project choice — an interactive run\n // that armed Codex needs the same warning the --target path prints.\n reportCodexTrust(projectRoot, targets, '1');\n console.log('');\n console.log('Done. Re-run `pnpm wp-install-ai-hooks` anytime to move or uninstall a hook.');\n console.log('(Non-interactive: pnpm wp-install-ai-hooks --target=project|project-personal|global|none)');\n}\n\nif (require.main === module) {\n void main();\n}\n"]}
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.WP_LOG_SH = exports.RESOLVE_LOG_DIR_SH = exports.SHIM_LOG_PRINTF = exports.SHIM_LOG_FIELDS = exports.ShimLogField = exports.SHIM_LOG_FAULTS = exports.SHIM_LOG_VERDICTS = exports.ShimLogVerdict = exports.SHIM_LOG_MAX_BYTES = void 0;
|
|
4
4
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
5
5
|
const log_streams_1 = require("../core/log-streams");
|
|
6
|
+
const agent_event_1 = require("../core/agent-event");
|
|
6
7
|
const l0_fault_codes_1 = require("../core/l0-fault-codes");
|
|
7
8
|
// ---------------------------------------------------------------------------
|
|
8
9
|
// THE L0 AUDIT LOG, in POSIX sh — the shim half of
|
|
@@ -66,6 +67,7 @@ exports.SHIM_LOG_VERDICTS = [
|
|
|
66
67
|
new ShimLogVerdict('PASS-BIN-ALLOW', 'no sh-side fault; the bin ran and exited 0 — matrix row 1, handed down to L1'),
|
|
67
68
|
new ShimLogVerdict('PASS-BIN-BLOCK', 'no sh-side fault; the bin ran and exited 2 — matrix row 1, a LATER layer blocked'),
|
|
68
69
|
new ShimLogVerdict('ALLOW-READ', 'allowlist entry 1 (any Read) — PASS, but terminal here (the bin never ran)'),
|
|
70
|
+
new ShimLogVerdict('ALLOW-IGNORED', 'a Codex tool with nothing to judge (L0_IGNORED_TOOLS) — PASS, terminal here'),
|
|
69
71
|
new ShimLogVerdict('ALLOW-CONFIG', 'allowlist entry 2 (a Write/Edit of webpieces.config.json) — PASS, terminal here'),
|
|
70
72
|
new ShimLogVerdict('ALLOW-MANIFEST', 'allowlist entry 3 (a Write/Edit of pnpm-workspace.yaml or package.json) — PASS, terminal here'),
|
|
71
73
|
new ShimLogVerdict('ALLOW-CURE', 'a Bash entry of the allowlist matched — ALLOW'),
|
|
@@ -120,7 +122,14 @@ exports.ShimLogField = ShimLogField;
|
|
|
120
122
|
exports.SHIM_LOG_FIELDS = [
|
|
121
123
|
new ShimLogField('<iso-ts>', `"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)"`, 'when the shim judged the call, local time with offset'),
|
|
122
124
|
new ShimLogField('<bin-name>', '"$BIN_NAME"', 'WHICH hook ran - wp-ai-guards-hook or wp-ai-rules-hook; Claude Code runs them in parallel'),
|
|
123
|
-
new ShimLogField('<tool>', '"$TOOL"', 'the PreToolUse tool name (Bash, Read, Write, Edit, …)'),
|
|
125
|
+
new ShimLogField('<tool>', '"$TOOL"', 'the PreToolUse tool name (Bash, Read, Write, Edit, apply_patch, …)'),
|
|
126
|
+
// WHICH HARNESS. Inserted MID-LINE rather than appended, which is this format's house style and is
|
|
127
|
+
// deliberate: a positional reader that has not been updated fails loudly here instead of silently
|
|
128
|
+
// reading the wrong column forever (see this array's own header). The values are the `AiType`
|
|
129
|
+
// union's, produced by AI_TYPE_SH — one vocabulary across all five streams. A row written before
|
|
130
|
+
// this field existed simply has no `ai=`, which reads as `unknown`; that is a real value, not a
|
|
131
|
+
// compatibility shim.
|
|
132
|
+
new ShimLogField(`ai=<${agent_event_1.AI_TYPES.join('|')}>`, '"ai=$AI"', 'WHICH coding agent made the call, from the one turn_id discriminator (adapters/detect-ai.ts)'),
|
|
124
133
|
new ShimLogField('tree=<name|primary>', '"tree=$WP_TREE"', 'git\'s own name for the worktree the CALL was made in, derived from the payload\'s cwd'),
|
|
125
134
|
new ShimLogField(`layer=${l0_fault_codes_1.L0_LAYER}`, `"layer=${l0_fault_codes_1.L0_LAYER}"`, 'the layer that judged it — constant here, and the first half of the join key a deny cites'),
|
|
126
135
|
new ShimLogField(`row=<${l0_fault_codes_1.L0_ROW_HANDED_DOWN}|${l0_fault_codes_1.L0_ROW_ALLOWLISTED}|${l0_fault_codes_1.L0_ROW_BLOCKED}>`, '"row=$_wp_row"', 'WHICH row of the three-row matrix this call took, read off the verdict (hand-down / allowlisted / blocked)'),
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-audit-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-audit-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { LOGS_STATE_DIR, WORKTREE_STATE_DIR, WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\nimport { L0_SHIM_STREAM } from '../core/log-streams';\n\nimport {\n L0_FAULT_NONE, L0_LAYER, L0_SH_FAULT_CODES, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, L0_ROW_HANDED_DOWN,\n} from '../core/l0-fault-codes';\n\n// ---------------------------------------------------------------------------\n// THE L0 AUDIT LOG, in POSIX sh — the shim half of\n// `.webpieces/**/logs/L0-shim/<session>-<agent|coordinator>-<binName>.log`. The writer key is\n// the sh twin of ai-hook-rules' LogStream.writerFile(): wp-ai-guards-hook and wp-ai-rules-hook are run\n// IN PARALLEL by Claude Code on every file edit, so an unsplit name means two writers, one file, and\n// torn appends above PIPE_BUF. A payload with no session_id renders 'unknown', never a bare name —\n// there is no un-prefixed spelling on either side.\n//\n// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices\n// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must\n// stay dependency-light: the shim it renders has to work on a tree too broken to load the rule engine.\n//\n// ─── What changed, and why it is not just a bigger log ─────────────────────────────────────────────\n// This log used to be a FAULT log wearing an audit log's name. `wp_log` fired only on the fail-closed\n// path (ALLOW-READ / ALLOW-CONFIG / ALLOW-CURE / DENY*), so a HEALTHY call — the overwhelming majority\n// — exec'd the bin and recorded nothing at all. You could therefore never answer \"what did L0 do to\n// this tool call?\", only \"what did L0 do on the calls where L0 was already broken\". Absence of a line\n// meant either \"healthy\" or \"the shim never ran\", and those are the two answers you most need to tell\n// apart. Every path now logs exactly one line, including the pass-through, so the file can be diffed\n// against the documented matrix in guards/L0-tooling.md rather than merely spot-checked.\n//\n// Two more defects went with it:\n// • it wrote to a hardcoded `$ROOT/.webpieces/logs`, so every worktree's lines landed in one flat\n// file (or, worse, in whichever tree happened to hold the shim) instead of the per-tree namespace\n// the L1 binary has used since the state-dir split;\n// • it had NO rotation, on a file now written on EVERY tool call.\n// ---------------------------------------------------------------------------\n\n/**\n * Rotation threshold, in bytes — 512 KB, the SAME number and the same `.1.log` naming as\n * decision-log.ts / rejection-log.ts / main-sync-log.ts. Deliberately identical rather than merely\n * similar: two log families in one directory with two different retention rules is a trap for whoever\n * later tries to reason about how much history they still have.\n */\nexport const SHIM_LOG_MAX_BYTES = 512 * 1024;\n\n/**\n * One verdict label the shim can record, WITH what it means. Data-only → a class, per CLAUDE.md.\n *\n * The meaning travels with the label because guards/L0-tooling.md renders this table rather than\n * restating it: a bare `string[]` left the meanings in prose, and the prose is what went stale (the\n * hand-written doc documented `DENY-UNDECLARED` for releases while this array did not list it at all).\n */\nexport class ShimLogVerdict {\n constructor(\n readonly label: string,\n readonly means: string,\n ) {}\n}\n\n/**\n * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.\n *\n * The ALLOW-* and DENY-* labels are the ones this log has always used and are kept verbatim, so\n * anything already grepping them keeps working. `PASS-BIN-*` is the healthy case the log used to be\n * silent about, and `DENY-UNDECLARED` is fault U's — emitted by the shim since U existed, but missing\n * from this array until the generated doc started reading it.\n */\nexport const SHIM_LOG_VERDICTS: readonly ShimLogVerdict[] = [\n new ShimLogVerdict('PASS-BIN-ALLOW', 'no sh-side fault; the bin ran and exited 0 — matrix row 1, handed down to L1'),\n new ShimLogVerdict('PASS-BIN-BLOCK', 'no sh-side fault; the bin ran and exited 2 — matrix row 1, a LATER layer blocked'),\n new ShimLogVerdict('ALLOW-READ', 'allowlist entry 1 (any Read) — PASS, but terminal here (the bin never ran)'),\n new ShimLogVerdict('ALLOW-CONFIG', 'allowlist entry 2 (a Write/Edit of webpieces.config.json) — PASS, terminal here'),\n new ShimLogVerdict('ALLOW-MANIFEST', 'allowlist entry 3 (a Write/Edit of pnpm-workspace.yaml or package.json) — PASS, terminal here'),\n new ShimLogVerdict('ALLOW-CURE', 'a Bash entry of the allowlist matched — ALLOW'),\n new ShimLogVerdict('DENY', 'fault X, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-UNDECLARED', 'fault U, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-STALE', 'fault D, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-BROKEN', 'fault K, not on the allowlist — BLOCK_AI_CURE'),\n];\n\n/**\n * The sh-side L0 fault codes, IMPORTED from the one codebook (../core/l0-fault-codes) rather than\n * retyped here — the letters in this file and the letters in `L0_FAULTS` have to be the same letters or\n * the log cannot be reconciled against the matrix. `-` means \"no sh-side fault\": the shim cannot\n * classify S / C / Y, which the BINARY detects and stamps onto its OWN streams with the same `fault=`\n * field, so a `-` here is a statement about this layer only, never a claim that nothing was wrong.\n */\nexport const SHIM_LOG_FAULTS = [...L0_SH_FAULT_CODES, L0_FAULT_NONE] as const;\n\n/**\n * One FIELD of the audit line: how it reads on disk, the sh expression that produces it, and what it\n * answers. Data-only → a class, per CLAUDE.md.\n */\nexport class ShimLogField {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly label: string,\n /** The sh word spliced into the printf below — the ONE place this field's value is spelled. */\n readonly shValue: string,\n readonly means: string,\n /**\n * True for a field that is printed only SOMETIMES (`bin=`, and only when it differs from\n * `shim=`). Such a field carries its OWN trailing tab in its sh value and therefore renders\n * with NO separator of its own — `%s%s` glues it to the next field, so an empty value leaves\n * the line one field shorter rather than leaving a stray tab behind.\n */\n readonly optional: boolean = false,\n ) {}\n}\n\n/**\n * THE LINE, as data. The printf below is BUILT from this array and guards/L0-tooling.md RENDERS it, so\n * a field cannot be added, dropped or reordered without both the shim and the doc changing with it.\n *\n * That is not decoration: `shim=`/`bin=` were inserted mid-line (deliberately breaking positional\n * readers rather than appending where a stale parser keeps working), then `layer=`/`row=` joined them,\n * and the hand-written doc went on describing a 7-field line with no `U` in its fault set the whole time.\n */\nexport const SHIM_LOG_FIELDS: readonly ShimLogField[] = [\n new ShimLogField('<iso-ts>', `\"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)\"`,\n 'when the shim judged the call, local time with offset'),\n new ShimLogField('<bin-name>', '\"$BIN_NAME\"',\n 'WHICH hook ran - wp-ai-guards-hook or wp-ai-rules-hook; Claude Code runs them in parallel'),\n new ShimLogField('<tool>', '\"$TOOL\"', 'the PreToolUse tool name (Bash, Read, Write, Edit, …)'),\n new ShimLogField('tree=<name|primary>', '\"tree=$WP_TREE\"',\n 'git\\'s own name for the worktree the CALL was made in, derived from the payload\\'s cwd'),\n new ShimLogField(`layer=${L0_LAYER}`, `\"layer=${L0_LAYER}\"`,\n 'the layer that judged it — constant here, and the first half of the join key a deny cites'),\n new ShimLogField(`row=<${L0_ROW_HANDED_DOWN}|${L0_ROW_ALLOWLISTED}|${L0_ROW_BLOCKED}>`, '\"row=$_wp_row\"',\n 'WHICH row of the three-row matrix this call took, read off the verdict (hand-down / allowlisted / blocked)'),\n new ShimLogField('shim=<root>', '\"shim=$ROOT\"',\n 'WHICH COPY of ai-hook.sh ran, resolved from $0 — against tree= it is the straddle detector'),\n new ShimLogField('bin=<root>', '\"$_wp_bin\"',\n 'WHICH TREE supplied the binary — printed ONLY when it differs from shim=, so its presence IS the borrow',\n true),\n new ShimLogField(`fault=<${SHIM_LOG_FAULTS.join('|')}>`, '\"fault=$1\"',\n 'the sh-side L0 fault, or `-`; S/C/Y are the binary\\'s and are stamped on ITS streams'),\n new ShimLogField('<VERDICT>', '\"$2\"', 'one of the verdict labels below — kept adjacent to the command'),\n new ShimLogField('<command>', '\"$CMD_LOG\"',\n 'the command PREFIX (the audit spelling; the DECISION reads $CMD, which fails closed on a quote)'),\n];\n\n/**\n * The writer's `printf`, assembled from SHIM_LOG_FIELDS — one `%s` per field, in the same order, and a\n * tab after every field EXCEPT an optional one (which carries its own). Retyping either half is what\n * let the format and its documentation disagree, so neither half is retyped anywhere.\n */\nexport const SHIM_LOG_PRINTF =\n `printf '${SHIM_LOG_FIELDS.map(\n (f: ShimLogField, i: number): string => '%s' + (i === SHIM_LOG_FIELDS.length - 1 ? '' : (f.optional ? '' : '\\\\t')),\n ).join('')}\\\\n' `\n + `${SHIM_LOG_FIELDS.map((f: ShimLogField): string => f.shValue).join(' ')} >> \"$_wp_f\"`;\n\n/**\n * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +\n * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.\n *\n * sh cannot import TypeScript, so this derivation is duplicated by necessity; the mitigation is\n * `shim-audit-log.spec.ts`, which runs THIS function through a real /bin/sh in real git worktrees and\n * asserts it returns exactly what `dotWebpieces.worktreeName()` returns. If the two ever disagree the\n * lock goes red rather than the logs quietly splitting in half.\n *\n * It asks git the SAME question the TS side asks — `--git-dir` vs `--git-common-dir`, which differ if\n * and only if this is a linked worktree — but in ONE `rev-parse` (it accepts both flags and prints a\n * line each) rather than two, because this runs on the blocking path of every tool call.\n *\n * The tree is derived from the PAYLOAD's `cwd` (Claude Code documents it as the working directory the\n * hook was invoked from), not from `$ROOT`. `$ROOT` is where the shim FILE lives and stays the anchor\n * for what the drift guard MEASURES — this fragment changes only where the log is WRITTEN.\n *\n * Fails soft, exactly like the TS side: when git cannot answer, the log collapses to\n * `<cwd>/.webpieces/logs`, which is the pre-change behaviour.\n */\nexport const RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {\n _wp_rp=\"$(git -C \"$WP_CWD\" rev-parse --git-dir --git-common-dir 2>/dev/null)\"\n _wp_gd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 1p)\"\n _wp_cd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 2p)\"\n if [ -z \"$_wp_gd\" ] || [ -z \"$_wp_cd\" ]; then\n WP_TREE=primary; WP_LOG_DIR=\"$WP_CWD/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n WP_PRIMARY_LOG_DIR=\"$WP_LOG_DIR\"; return 0\n fi\n # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS\n # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.\n case \"$_wp_gd\" in /*) : ;; *) _wp_gd=\"$WP_CWD/$_wp_gd\" ;; esac\n case \"$_wp_cd\" in /*) : ;; *) _wp_cd=\"$WP_CWD/$_wp_cd\" ;; esac\n # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared\n # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().\n _wp_primary=\"$WP_CWD\"\n case \"$_wp_cd\" in\n */.git) [ -d \"\\${_wp_cd%/*}\" ] && _wp_primary=\"\\${_wp_cd%/*}\" ;;\n esac\n # The PRIMARY clone's log dir, resolved on both branches. A deny that has to tell a human WHERE the\n # audit trail is (the inverse-drift escalation in shim.ts) must be able to name both the tree it is\n # standing in and the primary — a subagent has no reach into the second one, so the deny has to quote\n # that path rather than send anyone to go and look.\n WP_PRIMARY_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n if [ \"$_wp_gd\" = \"$_wp_cd\" ]; then\n WP_TREE=primary\n WP_LOG_DIR=\"$WP_PRIMARY_LOG_DIR\"\n else\n # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the\n # directory's basename — two worktrees under different parents may share a directory name.\n WP_TREE=\"\\${_wp_gd##*/}\"\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/$WP_TREE/${LOGS_STATE_DIR}\"\n fi\n}`;\n\n/**\n * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.\n *\n * FORMAT: SHIM_LOG_FIELDS, tab-separated, append-only — that array IS the format, and SHIM_LOG_PRINTF\n * is built from it, so neither this docblock nor guards/L0-tooling.md can describe a line the shim does\n * not write.\n *\n * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:\n * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise\n * unreadable), the second says which of the sh-side faults the shim detected. The verdict\n * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\\\t'` still\n * finds what it always found.\n *\n * NEVER breaks or blocks the hook: the whole body is wrapped so a failure of any kind — unwritable\n * directory, read-only filesystem, missing `git` — is swallowed, and nothing is ever written to\n * stdout (stdout is the PreToolUse decision channel; a stray byte there corrupts allow/deny).\n *\n * The log dir is resolved LAZILY on first use so a call that never logs never pays for the git probe.\n */\nexport const WP_LOG_SH = `WP_TREE=\"\"\nWP_LOG_DIR=\"\"\nWP_PRIMARY_LOG_DIR=\"\"\nWP_TAB=\"$(printf '\\\\t')\" # one real tab, so the OPTIONAL bin= field can carry its own separator\n${RESOLVE_LOG_DIR_SH}\nwp_clean() { # one path segment from an UNTRUSTED payload id — twin of LogStream's segment()\n printf '%s' \"$1\" | tr -c 'A-Za-z0-9._-' '_' | sed -e 's/\\\\.\\\\{2,\\\\}/_/g' -e 's/^\\\\.\\\\{1,\\\\}/_/' | cut -c1-64\n}\nwp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label\n {\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n # The LAYER is the directory and the WRITER is the file — same layout the TS writers use, spelled\n # from the same constant so the two halves cannot drift apart.\n _wp_sd=\"$WP_LOG_DIR/${L0_SHIM_STREAM}\"\n mkdir -p \"$_wp_sd\" 2>/dev/null || return 0\n # Same writer key as LogStream.writerFile(): <session>-<agent|coordinator>-<hook>.log. $BIN_NAME\n # IS the hook discriminator here (wp-ai-guards-hook vs wp-ai-rules-hook), and Claude Code runs those\n # two IN PARALLEL on every file edit — without this prefix they append to ONE file and tear above\n # PIPE_BUF. An empty session id renders 'unknown' — this has no bare-name branch, matching\n # LogStream.writerFile(), which has none either.\n # ALWAYS prefixed - a missing session_id renders as 'unknown', never as the shared bare name.\n # Gating this on a non-empty id would drop both parallel hooks back onto one file, which is the\n # torn-append case this exists to remove. Twin of LogStream.writerFile(), which has no bare branch.\n _wp_pfx=\"$(wp_clean \"\\${WP_SID:-unknown}\")-$(wp_clean \"\\${WP_AID:-coordinator}\")-$BIN_NAME\"\n _wp_f=\"$_wp_sd/\\${_wp_pfx}.log\"\n # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs\n # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).\n _wp_sz=\"$(wc -c < \"$_wp_f\" 2>/dev/null | tr -d ' ')\"\n case \"$_wp_sz\" in ''|*[!0-9]*) _wp_sz=0 ;; esac\n [ \"$_wp_sz\" -gt ${String(SHIM_LOG_MAX_BYTES)} ] && mv -f \"$_wp_f\" \"$_wp_sd/\\${_wp_pfx}.1.log\" 2>/dev/null\n # shim= and bin= are the two facts this log could not previously answer, and they are the ones that\n # decide whether a tree was governed by its OWN release or a borrowed one:\n # shim= WHICH COPY OF ai-hook.sh RAN — $ROOT, resolved from $0. The file is TRACKED, so every\n # worktree carries the version at ITS commit; settings.json registers it ABSOLUTE, so the copy\n # that runs is the SESSION ROOT's. Logged rather than assumed, on EVERY line: compared against\n # tree= it is the STRADDLE detector (tree=agent-X shim=<repo> = standing in one tree, judged by\n # another), and that pair varies constantly.\n # bin= WHICH TREE SUPPLIED THE BINARY — $BIN_ROOT, the upward walk's answer.\n #\n # bin= IS PRINTED ONLY WHEN IT DIFFERS FROM shim=, so its mere PRESENCE is the diagnostic (\"the binary\n # came from a different tree than the shim\") instead of ~50 bytes repeated on every line. Measured\n # across 549 logged lines: it differed on 39, every one a worktree agent's first few calls before it\n # ran pnpm install — after that they matched for the rest of that agent's life. And since the hooks\n # went ABSOLUTE, shim= is always the MAIN tree, so the two can now only differ when the main tree\n # itself has no node_modules (a fresh clone before install). ~7% of lines then, near 0% going forward.\n # A unit test asserts the field appears if and only if the roots differ, so it cannot quietly become\n # unconditional noise again.\n _wp_bin=\"\"\n [ \"$BIN_ROOT\" != \"$ROOT\" ] && _wp_bin=\"bin=$BIN_ROOT$WP_TAB\"\n # layer= and row= are the JOIN KEYS, and they are here so the join is REAL rather than promised.\n # Every L0 deny now opens '[<guard>] (layer=L0 fault=<code> row=<n>)' and cites \"the same coordinates\n # the audit line carries\" — which was true of the JS half (MATRIX_L0_BLOCK, via decision-log) and\n # FALSE of this one, which carried 'fault=' alone. Fixing the message instead of the line would have\n # left 'grep 'layer=L0 row=3'' finding one half of L0 and silently missing the other four faults.\n #\n # 'row=' is NOT a constant: it is the row of the three-row matrix this call actually took, read off\n # the verdict — hand-down, allowlisted, or blocked — exactly as L1 logs 'row=' from L1_ROWS. That is\n # what distinguishes it from the ~50 constant bytes 'bin=' used to spend above.\n _wp_row=${L0_ROW_HANDED_DOWN}\n case \"$2\" in ALLOW*) _wp_row=${L0_ROW_ALLOWLISTED} ;; DENY*) _wp_row=${L0_ROW_BLOCKED} ;; esac\n ${SHIM_LOG_PRINTF}\n } 2>/dev/null || true\n}`;\n"]}
|
|
1
|
+
{"version":3,"file":"shim-audit-log.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-audit-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { LOGS_STATE_DIR, WORKTREE_STATE_DIR, WEBPIECES_TMP_DIR } from '@webpieces/rules-config';\nimport { L0_SHIM_STREAM } from '../core/log-streams';\nimport { AI_TYPES } from '../core/agent-event';\n\nimport {\n L0_FAULT_NONE, L0_LAYER, L0_SH_FAULT_CODES, L0_ROW_ALLOWLISTED, L0_ROW_BLOCKED, L0_ROW_HANDED_DOWN,\n} from '../core/l0-fault-codes';\n\n// ---------------------------------------------------------------------------\n// THE L0 AUDIT LOG, in POSIX sh — the shim half of\n// `.webpieces/**/logs/L0-shim/<session>-<agent|coordinator>-<binName>.log`. The writer key is\n// the sh twin of ai-hook-rules' LogStream.writerFile(): wp-ai-guards-hook and wp-ai-rules-hook are run\n// IN PARALLEL by Claude Code on every file edit, so an unsplit name means two writers, one file, and\n// torn appends above PIPE_BUF. A payload with no session_id renders 'unknown', never a bare name —\n// there is no un-prefixed spelling on either side.\n//\n// Split out of ./shim.ts (which renders the shim body) purely so both stay readable; shim.ts splices\n// these fragments in verbatim and re-exports the constants. Like l0-allowlist.ts, this module must\n// stay dependency-light: the shim it renders has to work on a tree too broken to load the rule engine.\n//\n// ─── What changed, and why it is not just a bigger log ─────────────────────────────────────────────\n// This log used to be a FAULT log wearing an audit log's name. `wp_log` fired only on the fail-closed\n// path (ALLOW-READ / ALLOW-CONFIG / ALLOW-CURE / DENY*), so a HEALTHY call — the overwhelming majority\n// — exec'd the bin and recorded nothing at all. You could therefore never answer \"what did L0 do to\n// this tool call?\", only \"what did L0 do on the calls where L0 was already broken\". Absence of a line\n// meant either \"healthy\" or \"the shim never ran\", and those are the two answers you most need to tell\n// apart. Every path now logs exactly one line, including the pass-through, so the file can be diffed\n// against the documented matrix in guards/L0-tooling.md rather than merely spot-checked.\n//\n// Two more defects went with it:\n// • it wrote to a hardcoded `$ROOT/.webpieces/logs`, so every worktree's lines landed in one flat\n// file (or, worse, in whichever tree happened to hold the shim) instead of the per-tree namespace\n// the L1 binary has used since the state-dir split;\n// • it had NO rotation, on a file now written on EVERY tool call.\n// ---------------------------------------------------------------------------\n\n/**\n * Rotation threshold, in bytes — 512 KB, the SAME number and the same `.1.log` naming as\n * decision-log.ts / rejection-log.ts / main-sync-log.ts. Deliberately identical rather than merely\n * similar: two log families in one directory with two different retention rules is a trap for whoever\n * later tries to reason about how much history they still have.\n */\nexport const SHIM_LOG_MAX_BYTES = 512 * 1024;\n\n/**\n * One verdict label the shim can record, WITH what it means. Data-only → a class, per CLAUDE.md.\n *\n * The meaning travels with the label because guards/L0-tooling.md renders this table rather than\n * restating it: a bare `string[]` left the meanings in prose, and the prose is what went stale (the\n * hand-written doc documented `DENY-UNDECLARED` for releases while this array did not list it at all).\n */\nexport class ShimLogVerdict {\n constructor(\n readonly label: string,\n readonly means: string,\n ) {}\n}\n\n/**\n * The verdict vocabulary one shim invocation can record, and how each maps to guards/L0-tooling.md.\n *\n * The ALLOW-* and DENY-* labels are the ones this log has always used and are kept verbatim, so\n * anything already grepping them keeps working. `PASS-BIN-*` is the healthy case the log used to be\n * silent about, and `DENY-UNDECLARED` is fault U's — emitted by the shim since U existed, but missing\n * from this array until the generated doc started reading it.\n */\nexport const SHIM_LOG_VERDICTS: readonly ShimLogVerdict[] = [\n new ShimLogVerdict('PASS-BIN-ALLOW', 'no sh-side fault; the bin ran and exited 0 — matrix row 1, handed down to L1'),\n new ShimLogVerdict('PASS-BIN-BLOCK', 'no sh-side fault; the bin ran and exited 2 — matrix row 1, a LATER layer blocked'),\n new ShimLogVerdict('ALLOW-READ', 'allowlist entry 1 (any Read) — PASS, but terminal here (the bin never ran)'),\n new ShimLogVerdict('ALLOW-IGNORED', 'a Codex tool with nothing to judge (L0_IGNORED_TOOLS) — PASS, terminal here'),\n new ShimLogVerdict('ALLOW-CONFIG', 'allowlist entry 2 (a Write/Edit of webpieces.config.json) — PASS, terminal here'),\n new ShimLogVerdict('ALLOW-MANIFEST', 'allowlist entry 3 (a Write/Edit of pnpm-workspace.yaml or package.json) — PASS, terminal here'),\n new ShimLogVerdict('ALLOW-CURE', 'a Bash entry of the allowlist matched — ALLOW'),\n new ShimLogVerdict('DENY', 'fault X, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-UNDECLARED', 'fault U, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-STALE', 'fault D, not on the allowlist — BLOCK_AI_CURE'),\n new ShimLogVerdict('DENY-BROKEN', 'fault K, not on the allowlist — BLOCK_AI_CURE'),\n];\n\n/**\n * The sh-side L0 fault codes, IMPORTED from the one codebook (../core/l0-fault-codes) rather than\n * retyped here — the letters in this file and the letters in `L0_FAULTS` have to be the same letters or\n * the log cannot be reconciled against the matrix. `-` means \"no sh-side fault\": the shim cannot\n * classify S / C / Y, which the BINARY detects and stamps onto its OWN streams with the same `fault=`\n * field, so a `-` here is a statement about this layer only, never a claim that nothing was wrong.\n */\nexport const SHIM_LOG_FAULTS = [...L0_SH_FAULT_CODES, L0_FAULT_NONE] as const;\n\n/**\n * One FIELD of the audit line: how it reads on disk, the sh expression that produces it, and what it\n * answers. Data-only → a class, per CLAUDE.md.\n */\nexport class ShimLogField {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n readonly label: string,\n /** The sh word spliced into the printf below — the ONE place this field's value is spelled. */\n readonly shValue: string,\n readonly means: string,\n /**\n * True for a field that is printed only SOMETIMES (`bin=`, and only when it differs from\n * `shim=`). Such a field carries its OWN trailing tab in its sh value and therefore renders\n * with NO separator of its own — `%s%s` glues it to the next field, so an empty value leaves\n * the line one field shorter rather than leaving a stray tab behind.\n */\n readonly optional: boolean = false,\n ) {}\n}\n\n/**\n * THE LINE, as data. The printf below is BUILT from this array and guards/L0-tooling.md RENDERS it, so\n * a field cannot be added, dropped or reordered without both the shim and the doc changing with it.\n *\n * That is not decoration: `shim=`/`bin=` were inserted mid-line (deliberately breaking positional\n * readers rather than appending where a stale parser keeps working), then `layer=`/`row=` joined them,\n * and the hand-written doc went on describing a 7-field line with no `U` in its fault set the whole time.\n */\nexport const SHIM_LOG_FIELDS: readonly ShimLogField[] = [\n new ShimLogField('<iso-ts>', `\"$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)\"`,\n 'when the shim judged the call, local time with offset'),\n new ShimLogField('<bin-name>', '\"$BIN_NAME\"',\n 'WHICH hook ran - wp-ai-guards-hook or wp-ai-rules-hook; Claude Code runs them in parallel'),\n new ShimLogField('<tool>', '\"$TOOL\"', 'the PreToolUse tool name (Bash, Read, Write, Edit, apply_patch, …)'),\n // WHICH HARNESS. Inserted MID-LINE rather than appended, which is this format's house style and is\n // deliberate: a positional reader that has not been updated fails loudly here instead of silently\n // reading the wrong column forever (see this array's own header). The values are the `AiType`\n // union's, produced by AI_TYPE_SH — one vocabulary across all five streams. A row written before\n // this field existed simply has no `ai=`, which reads as `unknown`; that is a real value, not a\n // compatibility shim.\n new ShimLogField(`ai=<${AI_TYPES.join('|')}>`, '\"ai=$AI\"',\n 'WHICH coding agent made the call, from the one turn_id discriminator (adapters/detect-ai.ts)'),\n new ShimLogField('tree=<name|primary>', '\"tree=$WP_TREE\"',\n 'git\\'s own name for the worktree the CALL was made in, derived from the payload\\'s cwd'),\n new ShimLogField(`layer=${L0_LAYER}`, `\"layer=${L0_LAYER}\"`,\n 'the layer that judged it — constant here, and the first half of the join key a deny cites'),\n new ShimLogField(`row=<${L0_ROW_HANDED_DOWN}|${L0_ROW_ALLOWLISTED}|${L0_ROW_BLOCKED}>`, '\"row=$_wp_row\"',\n 'WHICH row of the three-row matrix this call took, read off the verdict (hand-down / allowlisted / blocked)'),\n new ShimLogField('shim=<root>', '\"shim=$ROOT\"',\n 'WHICH COPY of ai-hook.sh ran, resolved from $0 — against tree= it is the straddle detector'),\n new ShimLogField('bin=<root>', '\"$_wp_bin\"',\n 'WHICH TREE supplied the binary — printed ONLY when it differs from shim=, so its presence IS the borrow',\n true),\n new ShimLogField(`fault=<${SHIM_LOG_FAULTS.join('|')}>`, '\"fault=$1\"',\n 'the sh-side L0 fault, or `-`; S/C/Y are the binary\\'s and are stamped on ITS streams'),\n new ShimLogField('<VERDICT>', '\"$2\"', 'one of the verdict labels below — kept adjacent to the command'),\n new ShimLogField('<command>', '\"$CMD_LOG\"',\n 'the command PREFIX (the audit spelling; the DECISION reads $CMD, which fails closed on a quote)'),\n];\n\n/**\n * The writer's `printf`, assembled from SHIM_LOG_FIELDS — one `%s` per field, in the same order, and a\n * tab after every field EXCEPT an optional one (which carries its own). Retyping either half is what\n * let the format and its documentation disagree, so neither half is retyped anywhere.\n */\nexport const SHIM_LOG_PRINTF =\n `printf '${SHIM_LOG_FIELDS.map(\n (f: ShimLogField, i: number): string => '%s' + (i === SHIM_LOG_FIELDS.length - 1 ? '' : (f.optional ? '' : '\\\\t')),\n ).join('')}\\\\n' `\n + `${SHIM_LOG_FIELDS.map((f: ShimLogField): string => f.shValue).join(' ')} >> \"$_wp_f\"`;\n\n/**\n * Shell fragment: derive WHERE this call's log belongs — the sh TWIN of `DotWebpieces.local()` +\n * `worktreeName()` + `primaryRoot()` in @webpieces/rules-config.\n *\n * sh cannot import TypeScript, so this derivation is duplicated by necessity; the mitigation is\n * `shim-audit-log.spec.ts`, which runs THIS function through a real /bin/sh in real git worktrees and\n * asserts it returns exactly what `dotWebpieces.worktreeName()` returns. If the two ever disagree the\n * lock goes red rather than the logs quietly splitting in half.\n *\n * It asks git the SAME question the TS side asks — `--git-dir` vs `--git-common-dir`, which differ if\n * and only if this is a linked worktree — but in ONE `rev-parse` (it accepts both flags and prints a\n * line each) rather than two, because this runs on the blocking path of every tool call.\n *\n * The tree is derived from the PAYLOAD's `cwd` (Claude Code documents it as the working directory the\n * hook was invoked from), not from `$ROOT`. `$ROOT` is where the shim FILE lives and stays the anchor\n * for what the drift guard MEASURES — this fragment changes only where the log is WRITTEN.\n *\n * Fails soft, exactly like the TS side: when git cannot answer, the log collapses to\n * `<cwd>/.webpieces/logs`, which is the pre-change behaviour.\n */\nexport const RESOLVE_LOG_DIR_SH = `wp_resolve_log_dir() {\n _wp_rp=\"$(git -C \"$WP_CWD\" rev-parse --git-dir --git-common-dir 2>/dev/null)\"\n _wp_gd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 1p)\"\n _wp_cd=\"$(printf '%s\\\\n' \"$_wp_rp\" | sed -n 2p)\"\n if [ -z \"$_wp_gd\" ] || [ -z \"$_wp_cd\" ]; then\n WP_TREE=primary; WP_LOG_DIR=\"$WP_CWD/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n WP_PRIMARY_LOG_DIR=\"$WP_LOG_DIR\"; return 0\n fi\n # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS\n # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.\n case \"$_wp_gd\" in /*) : ;; *) _wp_gd=\"$WP_CWD/$_wp_gd\" ;; esac\n case \"$_wp_cd\" in /*) : ;; *) _wp_cd=\"$WP_CWD/$_wp_cd\" ;; esac\n # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared\n # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().\n _wp_primary=\"$WP_CWD\"\n case \"$_wp_cd\" in\n */.git) [ -d \"\\${_wp_cd%/*}\" ] && _wp_primary=\"\\${_wp_cd%/*}\" ;;\n esac\n # The PRIMARY clone's log dir, resolved on both branches. A deny that has to tell a human WHERE the\n # audit trail is (the inverse-drift escalation in shim.ts) must be able to name both the tree it is\n # standing in and the primary — a subagent has no reach into the second one, so the deny has to quote\n # that path rather than send anyone to go and look.\n WP_PRIMARY_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${LOGS_STATE_DIR}\"\n if [ \"$_wp_gd\" = \"$_wp_cd\" ]; then\n WP_TREE=primary\n WP_LOG_DIR=\"$WP_PRIMARY_LOG_DIR\"\n else\n # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the\n # directory's basename — two worktrees under different parents may share a directory name.\n WP_TREE=\"\\${_wp_gd##*/}\"\n WP_LOG_DIR=\"$_wp_primary/${WEBPIECES_TMP_DIR}/${WORKTREE_STATE_DIR}/$WP_TREE/${LOGS_STATE_DIR}\"\n fi\n}`;\n\n/**\n * Shell fragment: the audit-log writer itself — `wp_log <fault> <verdict>`, one tab-separated line.\n *\n * FORMAT: SHIM_LOG_FIELDS, tab-separated, append-only — that array IS the format, and SHIM_LOG_PRINTF\n * is built from it, so neither this docblock nor guards/L0-tooling.md can describe a line the shim does\n * not write.\n *\n * `tree=` and `fault=` are the two fields that make the file reconcilable against guards/L0-tooling.md:\n * the first says WHICH checkout produced the line (a shared log across seven worktrees is otherwise\n * unreadable), the second says which of the sh-side faults the shim detected. The verdict\n * keeps its historical spelling and stays adjacent to the command, so `grep 'DENY-STALE\\\\t'` still\n * finds what it always found.\n *\n * NEVER breaks or blocks the hook: the whole body is wrapped so a failure of any kind — unwritable\n * directory, read-only filesystem, missing `git` — is swallowed, and nothing is ever written to\n * stdout (stdout is the PreToolUse decision channel; a stray byte there corrupts allow/deny).\n *\n * The log dir is resolved LAZILY on first use so a call that never logs never pays for the git probe.\n */\nexport const WP_LOG_SH = `WP_TREE=\"\"\nWP_LOG_DIR=\"\"\nWP_PRIMARY_LOG_DIR=\"\"\nWP_TAB=\"$(printf '\\\\t')\" # one real tab, so the OPTIONAL bin= field can carry its own separator\n${RESOLVE_LOG_DIR_SH}\nwp_clean() { # one path segment from an UNTRUSTED payload id — twin of LogStream's segment()\n printf '%s' \"$1\" | tr -c 'A-Za-z0-9._-' '_' | sed -e 's/\\\\.\\\\{2,\\\\}/_/g' -e 's/^\\\\.\\\\{1,\\\\}/_/' | cut -c1-64\n}\nwp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label\n {\n [ -n \"$WP_LOG_DIR\" ] || wp_resolve_log_dir\n # The LAYER is the directory and the WRITER is the file — same layout the TS writers use, spelled\n # from the same constant so the two halves cannot drift apart.\n _wp_sd=\"$WP_LOG_DIR/${L0_SHIM_STREAM}\"\n mkdir -p \"$_wp_sd\" 2>/dev/null || return 0\n # Same writer key as LogStream.writerFile(): <session>-<agent|coordinator>-<hook>.log. $BIN_NAME\n # IS the hook discriminator here (wp-ai-guards-hook vs wp-ai-rules-hook), and Claude Code runs those\n # two IN PARALLEL on every file edit — without this prefix they append to ONE file and tear above\n # PIPE_BUF. An empty session id renders 'unknown' — this has no bare-name branch, matching\n # LogStream.writerFile(), which has none either.\n # ALWAYS prefixed - a missing session_id renders as 'unknown', never as the shared bare name.\n # Gating this on a non-empty id would drop both parallel hooks back onto one file, which is the\n # torn-append case this exists to remove. Twin of LogStream.writerFile(), which has no bare branch.\n _wp_pfx=\"$(wp_clean \"\\${WP_SID:-unknown}\")-$(wp_clean \"\\${WP_AID:-coordinator}\")-$BIN_NAME\"\n _wp_f=\"$_wp_sd/\\${_wp_pfx}.log\"\n # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs\n # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).\n _wp_sz=\"$(wc -c < \"$_wp_f\" 2>/dev/null | tr -d ' ')\"\n case \"$_wp_sz\" in ''|*[!0-9]*) _wp_sz=0 ;; esac\n [ \"$_wp_sz\" -gt ${String(SHIM_LOG_MAX_BYTES)} ] && mv -f \"$_wp_f\" \"$_wp_sd/\\${_wp_pfx}.1.log\" 2>/dev/null\n # shim= and bin= are the two facts this log could not previously answer, and they are the ones that\n # decide whether a tree was governed by its OWN release or a borrowed one:\n # shim= WHICH COPY OF ai-hook.sh RAN — $ROOT, resolved from $0. The file is TRACKED, so every\n # worktree carries the version at ITS commit; settings.json registers it ABSOLUTE, so the copy\n # that runs is the SESSION ROOT's. Logged rather than assumed, on EVERY line: compared against\n # tree= it is the STRADDLE detector (tree=agent-X shim=<repo> = standing in one tree, judged by\n # another), and that pair varies constantly.\n # bin= WHICH TREE SUPPLIED THE BINARY — $BIN_ROOT, the upward walk's answer.\n #\n # bin= IS PRINTED ONLY WHEN IT DIFFERS FROM shim=, so its mere PRESENCE is the diagnostic (\"the binary\n # came from a different tree than the shim\") instead of ~50 bytes repeated on every line. Measured\n # across 549 logged lines: it differed on 39, every one a worktree agent's first few calls before it\n # ran pnpm install — after that they matched for the rest of that agent's life. And since the hooks\n # went ABSOLUTE, shim= is always the MAIN tree, so the two can now only differ when the main tree\n # itself has no node_modules (a fresh clone before install). ~7% of lines then, near 0% going forward.\n # A unit test asserts the field appears if and only if the roots differ, so it cannot quietly become\n # unconditional noise again.\n _wp_bin=\"\"\n [ \"$BIN_ROOT\" != \"$ROOT\" ] && _wp_bin=\"bin=$BIN_ROOT$WP_TAB\"\n # layer= and row= are the JOIN KEYS, and they are here so the join is REAL rather than promised.\n # Every L0 deny now opens '[<guard>] (layer=L0 fault=<code> row=<n>)' and cites \"the same coordinates\n # the audit line carries\" — which was true of the JS half (MATRIX_L0_BLOCK, via decision-log) and\n # FALSE of this one, which carried 'fault=' alone. Fixing the message instead of the line would have\n # left 'grep 'layer=L0 row=3'' finding one half of L0 and silently missing the other four faults.\n #\n # 'row=' is NOT a constant: it is the row of the three-row matrix this call actually took, read off\n # the verdict — hand-down, allowlisted, or blocked — exactly as L1 logs 'row=' from L1_ROWS. That is\n # what distinguishes it from the ~50 constant bytes 'bin=' used to spend above.\n _wp_row=${L0_ROW_HANDED_DOWN}\n case \"$2\" in ALLOW*) _wp_row=${L0_ROW_ALLOWLISTED} ;; DENY*) _wp_row=${L0_ROW_BLOCKED} ;; esac\n ${SHIM_LOG_PRINTF}\n } 2>/dev/null || true\n}`;\n"]}
|
|
@@ -70,7 +70,7 @@ class ShimStaleDeny {
|
|
|
70
70
|
/**
|
|
71
71
|
* `[managed-hook-surface]` and the surfaces that drifted, one per line.
|
|
72
72
|
*
|
|
73
|
-
* `drifted` names WHICH of the
|
|
73
|
+
* `drifted` names WHICH of the managed things moved — .claude/webpieces/ai-hook.sh, each harness's
|
|
74
74
|
* .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It
|
|
75
75
|
* is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a
|
|
76
76
|
* caller silently keep emitting the one-file text after the surface grew — the "two spellings of one
|
|
@@ -172,13 +172,13 @@ class ShimStaleDeny {
|
|
|
172
172
|
' - any Read',
|
|
173
173
|
' - any Write/Edit whose target is webpieces.config.json',
|
|
174
174
|
' - every command on the L0 allowlist, including both Fix Options below',
|
|
175
|
-
' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until
|
|
175
|
+
' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until every managed surface matches again.',
|
|
176
176
|
'',
|
|
177
177
|
];
|
|
178
178
|
}
|
|
179
179
|
/**
|
|
180
180
|
* The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only
|
|
181
|
-
* cure that repairs
|
|
181
|
+
* cure that repairs every managed surface, it touches no config and imports only fs/path, so it
|
|
182
182
|
* runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.
|
|
183
183
|
*
|
|
184
184
|
* Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED
|
|
@@ -190,7 +190,7 @@ class ShimStaleDeny {
|
|
|
190
190
|
fixOptions() {
|
|
191
191
|
const anchor = (cmd) => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);
|
|
192
192
|
return [
|
|
193
|
-
' Fix Option 1: (preferred) the only cure that repairs
|
|
193
|
+
' Fix Option 1: (preferred) the only cure that repairs every managed surface, and it runs on a broken tree',
|
|
194
194
|
` run EXACTLY: '${anchor(l0_allowlist_1.UPGRADE_SHIM_CMD)}'`,
|
|
195
195
|
` Fix Option 2: PARTIAL - repairs ${shim_1.SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,
|
|
196
196
|
` run EXACTLY: '${anchor(l0_allowlist_1.RESTORE_SHIM_CMD)}'`,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"shim-deny-reason.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-deny-reason.ts"],"names":[],"mappings":";;AA0NA,kDAEC;AA5ND,0DAAoD;AAEpD,2DAA8F;AAC9F,iDAAuF;AACvF,+CAAqE;AACrE,iCAAuD;AAEvD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,aAAa;IACE,gBAAgB,CAAS;IAC1C,8FAA8F;IAC7E,QAAQ,CAAS;IAClC,yEAAyE;IACxD,UAAU,CAAS;IACnB,OAAO,CAAoB;IAC3B,UAAU,CAAU;IACrC;;;;;OAKG;IACc,IAAI,CAAU;IAE/B,YAAY,gBAAwB,EAAE,IAAY,EAAE,OAA0B,EAAE,UAAmB;QAC/F,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;QACzC,IAAI,CAAC,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAC3C,IAAI,CAAC,UAAU,GAAG,wBAAS,CAAC,gBAAgB,EAAE,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QACrE,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,KAAK,EAAE,IAAI,uBAAuB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAClE,CAAC;IAED,MAAM;QACF,OAAO;YACH,GAAG,IAAI,CAAC,MAAM,EAAE;YAChB,GAAG,IAAI,CAAC,QAAQ,EAAE;YAClB,GAAG,IAAI,CAAC,MAAM,EAAE;YAChB,GAAG,IAAI,CAAC,YAAY,EAAE;YACtB,GAAG,IAAI,CAAC,UAAU,EAAE;YACpB,GAAG,IAAI,CAAC,MAAM,EAAE;SACnB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACjB,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,MAAM;QACV,MAAM,OAAO,GAAG,IAAI,CAAC,gBAAgB,CAAC,CAAC,CAAC,uBAAuB,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7F,MAAM,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QAC9B,MAAM,KAAK,GAAG,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,GAAG,MAAM,CAAC,CAAC,CAAC,mBAAmB,CAAC;QAC9E,OAAO;YACH,wHAAwH;YACxH,EAAE;YACF,IAAA,8BAAa,EAAC,oCAAmB,EAAE,KAAK,CAAC;YACzC,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,OAAe,EAAU,EAAE,CAAC,KAAK,OAAO,EAAE,CAAC;YAChE,6GAA6G,OAAO,0FAA0F,8BAAgB,IAAI,gCAAkB,kHAAkH;YACtW,SAAS,IAAA,iCAAgB,EAAC,oCAAmB,CAAC,EAAE;YAChD,EAAE;SACL,CAAC;IACN,CAAC;IAED;;;;;;;;;;OAUG;IACK,QAAQ;QACZ,IAAI,IAAI,CAAC,QAAQ,KAAK,EAAE;YAAE,OAAO,EAAE,CAAC;QACpC,OAAO;YACH,0BAA0B;YAC1B,UAAU,IAAI,CAAC,QAAQ,4DAA4D;YACnF,gBAAgB,IAAI,CAAC,UAAU,oFAAoF;YACnH,SAAS,IAAI,CAAC,OAAO,EAAE,EAAE;YACzB,EAAE;SACL,CAAC;IACN,CAAC;IAEO,OAAO;QACX,OAAO,IAAI,CAAC,iBAAiB,EAAE;YAC3B,CAAC,CAAC,+FAA+F;YACjG,CAAC,CAAC,8IAA8I,CAAC;IACzJ,CAAC;IAED,yGAAyG;IACjG,iBAAiB;QACrB,OAAO,IAAI,CAAC,QAAQ,KAAK,IAAI,CAAC,UAAU,CAAC;IAC7C,CAAC;IAED;;;;;;;;;;;;;;;;;;;;;;;;;;;OA2BG;IACK,MAAM;QACV,8FAA8F;QAC9F,iGAAiG;QACjG,IAAI,IAAI,CAAC,QAAQ,KAAK,EAAE,IAAI,CAAC,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,iBAAiB,EAAE;YAAE,OAAO,EAAE,CAAC;QACpF,OAAO;YACH,4FAA4F;YAC5F,qPAAqP;YACrP,0EAA0E,IAAI,CAAC,QAAQ,+HAA+H,IAAI,CAAC,QAAQ,mCAAmC;YACtQ,EAAE;SACL,CAAC;IACN,CAAC;IAEO,YAAY;QAChB,OAAO;YACH,uCAAuC;YACvC,cAAc;YACd,0DAA0D;YAC1D,yEAAyE;YACzE,kMAAkM;YAClM,EAAE;SACL,CAAC;IACN,CAAC;IAED;;;;;;;;;;OAUG;IACK,UAAU;QACd,MAAM,MAAM,GAAG,CAAC,GAAW,EAAU,EAAE,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,QAAQ,OAAO,GAAG,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC;QAC5F,OAAO;YACH,+GAA+G;YAC/G,qBAAqB,MAAM,CAAC,+BAAgB,CAAC,GAAG;YAChD,qCAAqC,kBAAW,kKAAkK;YAClN,qBAAqB,MAAM,CAAC,+BAAgB,CAAC,GAAG;YAChD,yCAAyC,gCAAiB,mHAAmH;YAC7K,EAAE;SACL,CAAC;IACN,CAAC;IAEO,MAAM;QACV,OAAO;YACH,uBAAgB;YAChB,sHAAsH,kBAAW,GAAG;SACvI,CAAC;IACN,CAAC;CACJ;AAED,8FAA8F;AAC9F,kGAAkG;AAClG,gNAAgN;AAChN,SAAgB,mBAAmB,CAAC,gBAAwB,EAAE,IAAY,EAAE,OAA0B,EAAE,UAAmB;IACvH,OAAO,IAAI,aAAa,CAAC,gBAAgB,EAAE,IAAI,EAAE,OAAO,EAAE,UAAU,CAAC,CAAC,MAAM,EAAE,CAAC;AACnF,CAAC","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { L0_FAULT_SHIM_STALE, l0GuardHeader, l0MatrixCitation } from '../core/l0-fault-codes';\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NO_CHAINING_RULE, SHIM_MARKER } from './shim';\n\n/**\n * THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because\n * shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and\n * nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.\n *\n * IT IS RENDERED IN THE HOUSE FORMAT, the same skeleton core/report.ts (formatReport) gives every L1\n * and L2 deny: a header naming what was blocked, a `[guard-name]` block listing the offenders with a\n * one-line `→ why`, what is still allowed, then numbered `Fix Option N:` lines each with its command\n * on its own line. L0 used to be the ONLY layer in webpieces that answered in one unbroken paragraph —\n * ~3,300 characters of it here — so the two commands that matter were buried in prose and there was no\n * guard name to grep for. Nothing about the DECISION changed; only the shape of the words.\n *\n * SHORTER IS NOT THE GOAL, SCANNABLE IS. The budget below still exists (a paragraph regrows when every\n * new finding argues its case here), but a section that earns a line gets a line.\n *\n * CONSTRAINT: the returned string must contain no `\"` and no `\\` — it is JSON-serialized by denyJson()\n * (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an\n * INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a\n * Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit\n * tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).\n *\n * NEWLINES ARE SAFE HERE AND NEEDED NO NEW MECHANISM. denyJson() runs JSON.stringify, which escapes a\n * real newline to the two-character `\\n` on the wire, and Claude Code's parser turns it back. This is\n * already proven in production — every L1 deny is formatReport()'s multi-line string down this exact\n * path. The sh half of L0 (faults D/X/U/K in renderShim) cannot do this: it printf's REASON into a JSON\n * string literal, so it spells its newlines `${NL}` the same way it spells the ANSI escape `${ESC}`.\n * A real newline is neither a quote nor a backslash, so the JSON-safety assertions above are untouched.\n */\nclass ShimStaleDeny {\n private readonly installedVersion: string;\n /** The governing root, STRIPPED of `\"` and `\\` for display. '' when there is none to name. */\n private readonly safeRoot: string;\n /** CLAUDE_PROJECT_DIR as this process sees it, stripped the same way. */\n private readonly projectDir: string;\n private readonly drifted: readonly string[];\n private readonly inSubagent: boolean;\n /**\n * Whether the RAW root can carry a leading `cd <root> &&`. Tested against the raw root, never the\n * stripped one: stripping is a display-safety measure, and cd-anchoring to a path we just mangled\n * would prescribe a cd into a directory that does not exist. A root CD_PREFIX cannot express is\n * simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n */\n private readonly cdOk: boolean;\n\n constructor(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean) {\n this.installedVersion = installedVersion;\n this.safeRoot = root.replace(/[\"\\\\]/g, '');\n this.projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n this.drifted = drifted;\n this.inSubagent = inSubagent;\n this.cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n }\n\n render(): string {\n return [\n ...this.header(),\n ...this.measured(),\n ...this.caller(),\n ...this.stillAllowed(),\n ...this.fixOptions(),\n ...this.footer(),\n ].join('\\n');\n }\n\n /**\n * `[managed-hook-surface]` and the surfaces that drifted, one per line.\n *\n * `drifted` names WHICH of the three managed things moved — .claude/webpieces/ai-hook.sh, the\n * .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It\n * is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a\n * caller silently keep emitting the one-file text after the surface grew — the \"two spellings of one\n * thing\" shape the compatibility policy rejects. It was FOUR; guarantee-root.sh (L-1) is gone,\n * because the guard hooks are registered ABSOLUTE now and there is no second .sh to keep byte-locked.\n *\n * THE CAUSE IS A LIST. It used to assert flatly \"(it was reverted or hand-edited)\", which is\n * frequently FALSE — the common case is a shim whose logic simply predates this binary — and that\n * false certainty sent a real agent hunting for a tamper that never happened.\n */\n private header(): string[] {\n const verNote = this.installedVersion ? ` (installed version ${this.installedVersion})` : '';\n const n = this.drifted.length;\n const label = n === 1 ? '1 surface drifted' : `${String(n)} surfaces drifted`;\n return [\n '❌ webpieces ai-hooks blocked this call: a webpieces-managed hook surface no longer matches the installed guard binary.',\n '',\n l0GuardHeader(L0_FAULT_SHIM_STALE, label),\n ...this.drifted.map((surface: string): string => ` ${surface}`),\n ` → webpieces manages those THREE things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE}, which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited.`,\n ` → ${l0MatrixCitation(L0_FAULT_SHIM_STALE)}`,\n '',\n ];\n }\n\n /**\n * WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and\n * `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose\n * `<unset>` token keeps \"variable absent\" distinguishable from \"set to empty\"). The log is forensics\n * AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly\n * these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field\n * names on purpose, so the deny text and the log lines grep together.\n *\n * Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this\n * guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.\n */\n private measured(): string[] {\n if (this.safeRoot === '') return [];\n return [\n 'Where this was measured:',\n ` root=${this.safeRoot} - the tree whose shim was compared, and the one to repair`,\n ` projectDir=${this.projectDir} - CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty`,\n ` → ${this.verdict()}`,\n '',\n ];\n }\n\n private verdict(): string {\n return this.callerIsInTheTree()\n ? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'\n : 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';\n }\n\n /** True when the tree needing repair is the caller's own tree — the input the caller branch gates on. */\n private callerIsInTheTree(): boolean {\n return this.safeRoot === this.projectDir;\n }\n\n /**\n * THE CALLER-GATED BRANCH. It changes the WORDS ONLY — never the block/allow decision, never which\n * command is printed, and never which tree anything acts on (that is decided from the path, which is\n * why the deleted AgentIdentity class is NOT coming back for anything but message shape; agent\n * identity was measured untrustworthy as a location signal when a worktree agent resumed on the\n * primary clone after its tree was reaped).\n *\n * Two inputs, both already on hand:\n * 1. `inSubagent` — from the payload's `agent_id`, which Claude Code populates ONLY off the main\n * loop (main falls back to the session id, so the field is absent there). `agent_type` is NOT\n * usable: it is always populated and discriminates nothing. REQUIRED for the same reason\n * `drifted` is — an optional flag would let a caller keep emitting the main-loop text.\n * 2. root= vs projectDir= — different means the caller is not standing in the tree to repair.\n *\n * A main agent, and a subagent whose cwd IS the tree, get the cure and nothing else: they can run it,\n * see the result and commit it. A WORKTREE-ISOLATED subagent gets one extra step, and only because it\n * is true — MEASURED 2026-08-11: such an agent CAN run `cd <main> && pnpm exec wp-upgrade-shim` and it\n * works (the harness refuses cross-tree GIT operations, not this), but it can neither verify nor\n * commit the result, because `git -C <main>` is refused. So the escalation is of the COMMIT, not of\n * the repair, and the text must never tell it a local cure cannot work — the older wording asserted\n * exactly that and was false in the window where it fired (measured 2026-08-10: a worktree subagent\n * cured in place and the block lifted, with the deny's own root= naming that worktree).\n *\n * WHAT IS DELIBERATELY GONE: the \"ask the coordinator to run pnpm install so both trees are on the\n * same @webpieces version\" clause. Both hooks are registered ABSOLUTE, so every tree is already\n * judged by MAIN's shim and MAIN's binary — there is no version alignment left to ask for, and\n * asking sends an agent after a non-problem.\n */\n private caller(): string[] {\n // No governing root to name means no `Where this was measured` section either, so there is no\n // root= for this text to point at and nothing to escalate ABOUT. Silence beats a dangling field.\n if (this.safeRoot === '' || !this.inSubagent || this.callerIsInTheTree()) return [];\n return [\n 'You are a SUBAGENT and root= is not the tree you are standing in, so this takes TWO steps:',\n ' 1. Run Fix Option 1 below exactly as printed. It is already anchored to the tree that must change, and a worktree-isolated subagent CAN run it against another tree - that was measured and it works, so never conclude a local cure cannot work.',\n ` 2. Then ESCALATE THE COMMIT, which is the part you cannot do: git -C ${this.safeRoot} is refused here, so you can neither verify nor commit what the cure regenerated. Tell the coordinator to run git status in ${this.safeRoot} and commit the regenerated shim.`,\n '',\n ];\n }\n\n private stillAllowed(): string[] {\n return [\n 'Still allowed while this block is up:',\n ' - any Read',\n ' - any Write/Edit whose target is webpieces.config.json',\n ' - every command on the L0 allowlist, including both Fix Options below',\n ' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until all three match again.',\n '',\n ];\n }\n\n /**\n * The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only\n * cure that repairs all three managed surfaces, it touches no config and imports only fs/path, so it\n * runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.\n *\n * Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED\n * tolerates exactly that one prefix (locked by unit test), and it is what keeps the cure curable when\n * the AI's cwd is a DIFFERENT tree than the one being judged. OPTION 2 is a relative-path `cp`, so it\n * is even MORE cwd-sensitive than OPTION 1 — it is anchored too. Never a SECOND `cd … &&`: the\n * allowlist matches the whole command and three segments are denied.\n */\n private fixOptions(): string[] {\n const anchor = (cmd: string): string => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);\n return [\n ' Fix Option 1: (preferred) the only cure that repairs all three managed things, and it runs on a broken tree',\n ` run EXACTLY: '${anchor(UPGRADE_SHIM_CMD)}'`,\n ` Fix Option 2: PARTIAL - repairs ${SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,\n ` run EXACTLY: '${anchor(RESTORE_SHIM_CMD)}'`,\n ` NOT an option: do NOT use the bare '${INSTALL_HOOKS_CMD}' here - it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session.`,\n '',\n ];\n }\n\n private footer(): string[] {\n return [\n NO_CHAINING_RULE,\n `If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting ${SHIM_MARKER}.`,\n ];\n }\n}\n\n// The ONE entry point its two call sites (hook-core's fault-S deny, and the L0 fault table in\n// l0-matrix) import. The rendering lives on ShimStaleDeny above, per CLAUDE.md; this is the seam.\n// webpieces-disable no-function-outside-class -- one-line constructor+render seam for ShimStaleDeny, in the dependency-free shim module (it must stay callable from a tree too broken to build a DI container).\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n return new ShimStaleDeny(installedVersion, root, drifted, inSubagent).render();\n}\n"]}
|
|
1
|
+
{"version":3,"file":"shim-deny-reason.js","sourceRoot":"","sources":["../../../../../../packages/tooling/ai-hook-rules/src/bin/shim-deny-reason.ts"],"names":[],"mappings":"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","sourcesContent":["import { claudeEnv } from '@webpieces/rules-config';\n\nimport { L0_FAULT_SHIM_STALE, l0GuardHeader, l0MatrixCitation } from '../core/l0-fault-codes';\nimport { INSTALL_HOOKS_CMD, RESTORE_SHIM_CMD, UPGRADE_SHIM_CMD } from './l0-allowlist';\nimport { BASH_CWD_ENV_KEY, BASH_CWD_ENV_VALUE } from './managed-env';\nimport { NO_CHAINING_RULE, SHIM_MARKER } from './shim';\n\n/**\n * THE FAIL-CLOSED DENY TEXT for a drifted managed hook surface (L0 fault S) — its own module because\n * shim.ts is at its line cap and this is one cohesive unit: the words a blocked agent reads, and\n * nothing else. It imports FROM shim.ts and is never imported BY it, so the graph stays acyclic.\n *\n * IT IS RENDERED IN THE HOUSE FORMAT, the same skeleton core/report.ts (formatReport) gives every L1\n * and L2 deny: a header naming what was blocked, a `[guard-name]` block listing the offenders with a\n * one-line `→ why`, what is still allowed, then numbered `Fix Option N:` lines each with its command\n * on its own line. L0 used to be the ONLY layer in webpieces that answered in one unbroken paragraph —\n * ~3,300 characters of it here — so the two commands that matter were buried in prose and there was no\n * guard name to grep for. Nothing about the DECISION changed; only the shape of the words.\n *\n * SHORTER IS NOT THE GOAL, SCANNABLE IS. The budget below still exists (a paragraph regrows when every\n * new finding argues its case here), but a section that earns a line gets a line.\n *\n * CONSTRAINT: the returned string must contain no `\"` and no `\\` — it is JSON-serialized by denyJson()\n * (a stray quote/backslash would corrupt the PreToolUse decision payload, not just the text). That is an\n * INVARIANT, not a hope, so every interpolated path is STRIPPED of both rather than trusted — a\n * Windows-style path or an odd directory name must not be able to corrupt the decision. Locked by unit\n * tests. An unusual root is also dropped from the `cd` cure rather than quoted (CD_PREFIX would reject it).\n *\n * NEWLINES ARE SAFE HERE AND NEEDED NO NEW MECHANISM. denyJson() runs JSON.stringify, which escapes a\n * real newline to the two-character `\\n` on the wire, and Claude Code's parser turns it back. This is\n * already proven in production — every L1 deny is formatReport()'s multi-line string down this exact\n * path. The sh half of L0 (faults D/X/U/K in renderShim) cannot do this: it printf's REASON into a JSON\n * string literal, so it spells its newlines `${NL}` the same way it spells the ANSI escape `${ESC}`.\n * A real newline is neither a quote nor a backslash, so the JSON-safety assertions above are untouched.\n */\nclass ShimStaleDeny {\n private readonly installedVersion: string;\n /** The governing root, STRIPPED of `\"` and `\\` for display. '' when there is none to name. */\n private readonly safeRoot: string;\n /** CLAUDE_PROJECT_DIR as this process sees it, stripped the same way. */\n private readonly projectDir: string;\n private readonly drifted: readonly string[];\n private readonly inSubagent: boolean;\n /**\n * Whether the RAW root can carry a leading `cd <root> &&`. Tested against the raw root, never the\n * stripped one: stripping is a display-safety measure, and cd-anchoring to a path we just mangled\n * would prescribe a cd into a directory that does not exist. A root CD_PREFIX cannot express is\n * simply not offered as a `cd` (raw ok ⇒ safeRoot === root).\n */\n private readonly cdOk: boolean;\n\n constructor(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean) {\n this.installedVersion = installedVersion;\n this.safeRoot = root.replace(/[\"\\\\]/g, '');\n this.projectDir = claudeEnv.projectDirForLog().replace(/[\"\\\\]/g, '');\n this.drifted = drifted;\n this.inSubagent = inSubagent;\n this.cdOk = root !== '' && /^[A-Za-z0-9._/@~+-]+$/.test(root);\n }\n\n render(): string {\n return [\n ...this.header(),\n ...this.measured(),\n ...this.caller(),\n ...this.stillAllowed(),\n ...this.fixOptions(),\n ...this.footer(),\n ].join('\\n');\n }\n\n /**\n * `[managed-hook-surface]` and the surfaces that drifted, one per line.\n *\n * `drifted` names WHICH of the managed things moved — .claude/webpieces/ai-hook.sh, each harness's\n * .claude/settings.json hook registration, and its managed env entry (see hook-registration.ts). It\n * is REQUIRED, not optional: this used to be a shim-only message, and an optional list would let a\n * caller silently keep emitting the one-file text after the surface grew — the \"two spellings of one\n * thing\" shape the compatibility policy rejects. It was FOUR; guarantee-root.sh (L-1) is gone,\n * because the guard hooks are registered ABSOLUTE now and there is no second .sh to keep byte-locked.\n *\n * THE CAUSE IS A LIST. It used to assert flatly \"(it was reverted or hand-edited)\", which is\n * frequently FALSE — the common case is a shim whose logic simply predates this binary — and that\n * false certainty sent a real agent hunting for a tamper that never happened.\n */\n private header(): string[] {\n const verNote = this.installedVersion ? ` (installed version ${this.installedVersion})` : '';\n const n = this.drifted.length;\n const label = n === 1 ? '1 surface drifted' : `${String(n)} surfaces drifted`;\n return [\n '❌ webpieces ai-hooks blocked this call: a webpieces-managed hook surface no longer matches the installed guard binary.',\n '',\n l0GuardHeader(L0_FAULT_SHIM_STALE, label),\n ...this.drifted.map((surface: string): string => ` ${surface}`),\n ` → webpieces manages those THREE things as ONE set, GENERATED by the INSTALLED @webpieces/ai-hook-rules${verNote}; what is on disk is reverted, hand-edited, or predating this binary. The env entry is ${BASH_CWD_ENV_KEY}=${BASH_CWD_ENV_VALUE}, which pins the Bash cwd to the project root, identically for every subagent because settings env is inherited.`,\n ` → ${l0MatrixCitation(L0_FAULT_SHIM_STALE)}`,\n '',\n ];\n }\n\n /**\n * WHERE IT WAS MEASURED, in the deny itself and not only in the logs. #574 put `root=` and\n * `projectDir=` on every L1 invocation line (see decision-log / ClaudeEnv.projectDirForLog, whose\n * `<unset>` token keeps \"variable absent\" distinguishable from \"set to empty\"). The log is forensics\n * AFTER the fact; the deny is what a blocked agent reads IN the moment, and the absence of exactly\n * these two fields is what sent a real agent chasing the wrong mechanism for four cures. Same field\n * names on purpose, so the deny text and the log lines grep together.\n *\n * Agreement is the routine case; DISAGREEMENT is the signature of the session-root-vs-cwd split this\n * guard was rewritten to make unconstructible, so it gets said out loud rather than left to inference.\n */\n private measured(): string[] {\n if (this.safeRoot === '') return [];\n return [\n 'Where this was measured:',\n ` root=${this.safeRoot} - the tree whose shim was compared, and the one to repair`,\n ` projectDir=${this.projectDir} - CLAUDE_PROJECT_DIR as this process sees it; <unset> = absent, not set-but-empty`,\n ` → ${this.verdict()}`,\n '',\n ];\n }\n\n private verdict(): string {\n return this.callerIsInTheTree()\n ? 'These two AGREE, so this is the ordinary case - the tree you are in is the tree being judged.'\n : 'These two DISAGREE - the tree being judged is NOT the one CLAUDE_PROJECT_DIR names, so cure the root= tree and do not assume your cwd is it.';\n }\n\n /** True when the tree needing repair is the caller's own tree — the input the caller branch gates on. */\n private callerIsInTheTree(): boolean {\n return this.safeRoot === this.projectDir;\n }\n\n /**\n * THE CALLER-GATED BRANCH. It changes the WORDS ONLY — never the block/allow decision, never which\n * command is printed, and never which tree anything acts on (that is decided from the path, which is\n * why the deleted AgentIdentity class is NOT coming back for anything but message shape; agent\n * identity was measured untrustworthy as a location signal when a worktree agent resumed on the\n * primary clone after its tree was reaped).\n *\n * Two inputs, both already on hand:\n * 1. `inSubagent` — from the payload's `agent_id`, which Claude Code populates ONLY off the main\n * loop (main falls back to the session id, so the field is absent there). `agent_type` is NOT\n * usable: it is always populated and discriminates nothing. REQUIRED for the same reason\n * `drifted` is — an optional flag would let a caller keep emitting the main-loop text.\n * 2. root= vs projectDir= — different means the caller is not standing in the tree to repair.\n *\n * A main agent, and a subagent whose cwd IS the tree, get the cure and nothing else: they can run it,\n * see the result and commit it. A WORKTREE-ISOLATED subagent gets one extra step, and only because it\n * is true — MEASURED 2026-08-11: such an agent CAN run `cd <main> && pnpm exec wp-upgrade-shim` and it\n * works (the harness refuses cross-tree GIT operations, not this), but it can neither verify nor\n * commit the result, because `git -C <main>` is refused. So the escalation is of the COMMIT, not of\n * the repair, and the text must never tell it a local cure cannot work — the older wording asserted\n * exactly that and was false in the window where it fired (measured 2026-08-10: a worktree subagent\n * cured in place and the block lifted, with the deny's own root= naming that worktree).\n *\n * WHAT IS DELIBERATELY GONE: the \"ask the coordinator to run pnpm install so both trees are on the\n * same @webpieces version\" clause. Both hooks are registered ABSOLUTE, so every tree is already\n * judged by MAIN's shim and MAIN's binary — there is no version alignment left to ask for, and\n * asking sends an agent after a non-problem.\n */\n private caller(): string[] {\n // No governing root to name means no `Where this was measured` section either, so there is no\n // root= for this text to point at and nothing to escalate ABOUT. Silence beats a dangling field.\n if (this.safeRoot === '' || !this.inSubagent || this.callerIsInTheTree()) return [];\n return [\n 'You are a SUBAGENT and root= is not the tree you are standing in, so this takes TWO steps:',\n ' 1. Run Fix Option 1 below exactly as printed. It is already anchored to the tree that must change, and a worktree-isolated subagent CAN run it against another tree - that was measured and it works, so never conclude a local cure cannot work.',\n ` 2. Then ESCALATE THE COMMIT, which is the part you cannot do: git -C ${this.safeRoot} is refused here, so you can neither verify nor commit what the cure regenerated. Tell the coordinator to run git status in ${this.safeRoot} and commit the regenerated shim.`,\n '',\n ];\n }\n\n private stillAllowed(): string[] {\n return [\n 'Still allowed while this block is up:',\n ' - any Read',\n ' - any Write/Edit whose target is webpieces.config.json',\n ' - every command on the L0 allowlist, including both Fix Options below',\n ' THIS IS NOT A DEADLOCK: both options are explicitly ALLOWED through, so run one YOURSELF now - do not hand it back to the human. Every OTHER tool call is blocked until every managed surface matches again.',\n '',\n ];\n }\n\n /**\n * The two cures, house-numbered. ORDER IS LOAD-BEARING: wp-upgrade-shim LEADS because it is the only\n * cure that repairs every managed surface, it touches no config and imports only fs/path, so it\n * runs on a tree too broken to load the rule engine. The `cp` stays last as the pre-0.4.408 fallback.\n *\n * Both are anchored with a leading `cd <root> &&` when the root allows it — CD_PREFIX_*_ANCHORED\n * tolerates exactly that one prefix (locked by unit test), and it is what keeps the cure curable when\n * the AI's cwd is a DIFFERENT tree than the one being judged. OPTION 2 is a relative-path `cp`, so it\n * is even MORE cwd-sensitive than OPTION 1 — it is anchored too. Never a SECOND `cd … &&`: the\n * allowlist matches the whole command and three segments are denied.\n */\n private fixOptions(): string[] {\n const anchor = (cmd: string): string => (this.cdOk ? `cd ${this.safeRoot} && ${cmd}` : cmd);\n return [\n ' Fix Option 1: (preferred) the only cure that repairs every managed surface, and it runs on a broken tree',\n ` run EXACTLY: '${anchor(UPGRADE_SHIM_CMD)}'`,\n ` Fix Option 2: PARTIAL - repairs ${SHIM_MARKER} only. Pick it ONLY when the installed @webpieces/ai-hook-rules is older than 0.4.408, where Fix Option 1 does not exist yet; then upgrade and run Fix Option 1.`,\n ` run EXACTLY: '${anchor(RESTORE_SHIM_CMD)}'`,\n ` NOT an option: do NOT use the bare '${INSTALL_HOOKS_CMD}' here - it also migrates your config and PROMPTS for a hook target twice, which hangs a non-interactive session.`,\n '',\n ];\n }\n\n private footer(): string[] {\n return [\n NO_CHAINING_RULE,\n `If you meant to remove @webpieces/ai-hook-rules, delete its hooks from .claude/settings.json rather than reverting ${SHIM_MARKER}.`,\n ];\n }\n}\n\n// The ONE entry point its two call sites (hook-core's fault-S deny, and the L0 fault table in\n// l0-matrix) import. The rendering lives on ShimStaleDeny above, per CLAUDE.md; this is the seam.\n// webpieces-disable no-function-outside-class -- one-line constructor+render seam for ShimStaleDeny, in the dependency-free shim module (it must stay callable from a tree too broken to build a DI container).\nexport function shimStaleDenyReason(installedVersion: string, root: string, drifted: readonly string[], inSubagent: boolean): string {\n return new ShimStaleDeny(installedVersion, root, drifted, inSubagent).render();\n}\n"]}
|
package/src/bin/shim.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from './l0-allowlist';
|
|
2
|
+
export * from './l0-decide';
|
|
3
|
+
export * from './l0-ignored-tools';
|
|
2
4
|
export * from './shim-audit-log';
|
|
3
5
|
export declare const SHIM_MARKER = ".claude/webpieces/ai-hook.sh";
|
|
4
6
|
export declare function shimPath(projectRoot: string): string;
|
package/src/bin/shim.js
CHANGED
|
@@ -16,12 +16,18 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
16
16
|
const l0_fault_codes_1 = require("../core/l0-fault-codes");
|
|
17
17
|
const to_error_1 = require("../core/to-error");
|
|
18
18
|
const l0_allowlist_1 = require("./l0-allowlist");
|
|
19
|
+
const l0_ignored_tools_1 = require("./l0-ignored-tools");
|
|
20
|
+
const detect_ai_1 = require("../adapters/detect-ai");
|
|
19
21
|
const shim_audit_log_1 = require("./shim-audit-log");
|
|
20
22
|
const shim_drift_fix_1 = require("./shim-drift-fix");
|
|
21
23
|
const shim_version_drift_1 = require("./shim-version-drift");
|
|
22
24
|
// The allowlist moved to ./l0-allowlist (this module was over the file-size limit); re-exported here so
|
|
23
25
|
// every existing `from './shim'` import keeps working and there is still ONE name to import L0 by.
|
|
24
26
|
tslib_1.__exportStar(require("./l0-allowlist"), exports);
|
|
27
|
+
// …and its two siblings, split out for the same size reason: the decision (isAllowed / isRootManifest /
|
|
28
|
+
// READ_TOOLS) and the tool-shaped set it consults. ONE name to import L0 by, still.
|
|
29
|
+
tslib_1.__exportStar(require("./l0-decide"), exports);
|
|
30
|
+
tslib_1.__exportStar(require("./l0-ignored-tools"), exports);
|
|
25
31
|
// Same treatment for the audit-log fragment: one name to import the whole rendered shim by.
|
|
26
32
|
tslib_1.__exportStar(require("./shim-audit-log"), exports);
|
|
27
33
|
// ---------------------------------------------------------------------------
|
|
@@ -212,7 +218,13 @@ WP_SID="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"session_id"[[:space:]]*:[[:spa
|
|
|
212
218
|
WP_AID="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"agent_id"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
213
219
|
FILE="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
214
220
|
WP_CWD="\$(printf '%s' "\$PAYLOAD" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\\([^"\\\\]*\\)".*/\\1/p')"
|
|
215
|
-
[ -n "\$WP_CWD" ] || WP_CWD="\$ROOT" # no cwd in the payload (older client, or a hand-run) → the shim's own tree
|
|
221
|
+
[ -n "\$WP_CWD" ] || WP_CWD="\$ROOT" # no cwd in the payload (older client, or a hand-run) → the shim's own tree
|
|
222
|
+
# WHICH HARNESS sent this call — the ONE discriminator, imported from ../adapters/detect-ai so the sh
|
|
223
|
+
# half of L0 and the JS half answer the identical question from one definition (the same twin pattern
|
|
224
|
+
# L0_ALLOW_ERE_SH / L0_ALLOW_JS uses). One \`case\`, no JSON parser: consistent with how every field
|
|
225
|
+
# above is \`sed\`-scraped, and the values are the AiType union's own strings so the twin-agreement spec
|
|
226
|
+
# compares them byte for byte instead of translating between two vocabularies.
|
|
227
|
+
${detect_ai_1.AI_TYPE_SH}`;
|
|
216
228
|
// Shell fragment: the guards are DOWN (missing | stale | crashed). Classify the fault, then let THE L0
|
|
217
229
|
// ALLOWLIST through — everything else falls to the deny below.
|
|
218
230
|
//
|
|
@@ -240,6 +252,14 @@ if [ "\$TOOL" = "Read" ]; then
|
|
|
240
252
|
wp_log "\$WP_FAULT" ALLOW-READ # you must be able to read to work out how to fix this
|
|
241
253
|
exit 0
|
|
242
254
|
fi
|
|
255
|
+
case "\$TOOL" in
|
|
256
|
+
${l0_ignored_tools_1.L0_IGNORED_TOOLS_SH})
|
|
257
|
+
# Nothing to judge — the Codex tools that are neither a shell command nor a file edit. sh twin of
|
|
258
|
+
# isAllowed()'s L0_IGNORED_TOOLS branch; see there for why the list is EXPLICIT and why apply_patch
|
|
259
|
+
# (Codex's only WRITE) is not on it.
|
|
260
|
+
wp_log "\$WP_FAULT" ALLOW-IGNORED
|
|
261
|
+
exit 0 ;;
|
|
262
|
+
esac
|
|
243
263
|
case "\$FILE" in
|
|
244
264
|
*/${rules_config_1.CONFIG_FILENAME}|${rules_config_1.CONFIG_FILENAME})
|
|
245
265
|
wp_log "\$WP_FAULT" ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
|