@raishin/vanguard-frontier-agentic 3.15.0 → 3.17.0
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +3 -1
- package/.cursor-plugin/plugin.json +3 -1
- package/.github/plugin/marketplace.json +1 -1
- package/README.md +11 -11
- package/agents/AGENTS.md +1 -1
- package/agents/README.md +1 -1
- package/agents/cross-functional/agentic-security-review-agent/AGENT.md +168 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/claude-code.agent.md +158 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/codex.toml +165 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/copilot.agent.md +163 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/cursor.agent.md +158 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/gemini.agent.md +164 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/cross-functional/agentic-security-review-agent/harnesses/kiro-ide.agent.md +157 -0
- package/agents/cross-functional/agentic-security-review-agent/metadata.json +45 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/AGENT.md +157 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/claude-code.agent.md +147 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/codex.toml +154 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/copilot.agent.md +152 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/cursor.agent.md +147 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/gemini.agent.md +153 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-cli.agent.json +5 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/harnesses/kiro-ide.agent.md +146 -0
- package/agents/cross-functional/agentic-system-architecture-review-agent/metadata.json +46 -0
- package/agents/typescript/typescript-maestro-agent/AGENT.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/claude-code.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/codex.toml +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/copilot.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/cursor.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/gemini.agent.md +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/kiro-cli.agent.json +1 -1
- package/agents/typescript/typescript-maestro-agent/harnesses/kiro-ide.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/AGENT.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/claude-code.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/copilot.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/cursor.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/gemini.agent.md +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-cli.agent.json +1 -1
- package/agents/typescript/typescript-mcp-tool-contract-agent/harnesses/kiro-ide.agent.md +1 -1
- package/catalog/AGENTS.md +2 -1
- package/catalog/agents.json +71 -0
- package/catalog/asset-integrity.json +1639 -92
- package/catalog/index.json +3 -2
- package/catalog/install-roles.json +24 -3
- package/catalog/model-assignments.json +66 -0
- package/catalog/mods.json +131 -0
- package/catalog/skill-manifest.json +134 -8
- package/catalog/skills.json +89 -0
- package/mods/README.md +220 -0
- package/mods/audit-trail/README.md +70 -0
- package/mods/audit-trail/bin/hook.mjs +65 -0
- package/mods/audit-trail/claude-code/UNSUPPORTED.md +11 -0
- package/mods/audit-trail/claude-code/settings.hooks.json +14 -0
- package/mods/audit-trail/codex/hooks.json +14 -0
- package/mods/audit-trail/kiro/UNSUPPORTED.md +7 -0
- package/mods/audit-trail/kiro/audit-trail-post-tool-use-hook.json +14 -0
- package/mods/audit-trail/lib/common.mjs +213 -0
- package/mods/audit-trail/lib/core.mjs +50 -0
- package/mods/audit-trail/lib/runtime.mjs +140 -0
- package/mods/audit-trail/mod.toml +37 -0
- package/mods/blast-radius-guard/.claude-plugin/plugin.json +5 -0
- package/mods/blast-radius-guard/README.md +53 -0
- package/mods/blast-radius-guard/bin/hook.mjs +18 -0
- package/mods/blast-radius-guard/claude-code/settings.hooks.json +14 -0
- package/mods/blast-radius-guard/codex/UNSUPPORTED.md +7 -0
- package/mods/blast-radius-guard/codex/hooks.json +14 -0
- package/mods/blast-radius-guard/hooks/hooks.json +5 -0
- package/mods/blast-radius-guard/hooks/register.test.ts +97 -0
- package/mods/blast-radius-guard/hooks/register.ts +13 -0
- package/mods/blast-radius-guard/kiro/UNSUPPORTED.md +7 -0
- package/mods/blast-radius-guard/kiro/blast-radius-guard-pre-tool-use-hook.json +14 -0
- package/mods/blast-radius-guard/lib/common.mjs +213 -0
- package/mods/blast-radius-guard/lib/core.mjs +66 -0
- package/mods/blast-radius-guard/lib/patterns.mjs +102 -0
- package/mods/blast-radius-guard/lib/runtime.mjs +140 -0
- package/mods/blast-radius-guard/mod.toml +34 -0
- package/mods/blast-radius-guard/patterns.json +101 -0
- package/mods/context-burn/.claude-plugin/plugin.json +5 -0
- package/mods/context-burn/README.md +66 -0
- package/mods/context-burn/bin/hook.mjs +38 -0
- package/mods/context-burn/claude-code/UNSUPPORTED.md +7 -0
- package/mods/context-burn/claude-code/settings.hooks.json +24 -0
- package/mods/context-burn/codex/UNSUPPORTED.md +7 -0
- package/mods/context-burn/codex/hooks.json +24 -0
- package/mods/context-burn/hooks/hooks.json +5 -0
- package/mods/context-burn/hooks/register.test.ts +151 -0
- package/mods/context-burn/hooks/register.ts +35 -0
- package/mods/context-burn/kiro/UNSUPPORTED.md +7 -0
- package/mods/context-burn/kiro/context-burn-post-tool-use-hook.json +14 -0
- package/mods/context-burn/kiro/context-burn-session-start-hook.json +14 -0
- package/mods/context-burn/lib/common.mjs +213 -0
- package/mods/context-burn/lib/core.mjs +79 -0
- package/mods/context-burn/lib/runtime.mjs +140 -0
- package/mods/context-burn/mod.toml +37 -0
- package/mods/live-guard-gate/.claude-plugin/plugin.json +5 -0
- package/mods/live-guard-gate/README.md +43 -0
- package/mods/live-guard-gate/bin/hook.mjs +12 -0
- package/mods/live-guard-gate/claude-code/settings.hooks.json +14 -0
- package/mods/live-guard-gate/codex/UNSUPPORTED.md +7 -0
- package/mods/live-guard-gate/codex/hooks.json +14 -0
- package/mods/live-guard-gate/hooks/hooks.json +5 -0
- package/mods/live-guard-gate/hooks/register.test.ts +59 -0
- package/mods/live-guard-gate/hooks/register.ts +11 -0
- package/mods/live-guard-gate/kiro/UNSUPPORTED.md +7 -0
- package/mods/live-guard-gate/kiro/live-guard-gate-pre-tool-use-hook.json +14 -0
- package/mods/live-guard-gate/lib/common.mjs +213 -0
- package/mods/live-guard-gate/lib/core.mjs +24 -0
- package/mods/live-guard-gate/lib/live-agents.mjs +65 -0
- package/mods/live-guard-gate/lib/patterns.mjs +523 -0
- package/mods/live-guard-gate/lib/runtime.mjs +140 -0
- package/mods/live-guard-gate/mod.toml +34 -0
- package/mods/live-guard-gate/patterns.json +522 -0
- package/mods/secret-redactor/.claude-plugin/plugin.json +5 -0
- package/mods/secret-redactor/README.md +86 -0
- package/mods/secret-redactor/bin/hook.mjs +35 -0
- package/mods/secret-redactor/claude-code/UNSUPPORTED.md +7 -0
- package/mods/secret-redactor/claude-code/settings.hooks.json +24 -0
- package/mods/secret-redactor/codex/UNSUPPORTED.md +11 -0
- package/mods/secret-redactor/codex/hooks.json +14 -0
- package/mods/secret-redactor/hooks/hooks.json +5 -0
- package/mods/secret-redactor/hooks/register.test.ts +150 -0
- package/mods/secret-redactor/hooks/register.ts +19 -0
- package/mods/secret-redactor/kiro/UNSUPPORTED.md +11 -0
- package/mods/secret-redactor/kiro/secret-redactor-pre-tool-use-hook.json +14 -0
- package/mods/secret-redactor/lib/common.mjs +213 -0
- package/mods/secret-redactor/lib/core.mjs +61 -0
- package/mods/secret-redactor/lib/patterns.mjs +75 -0
- package/mods/secret-redactor/lib/runtime.mjs +140 -0
- package/mods/secret-redactor/mod.toml +43 -0
- package/mods/secret-redactor/patterns.json +74 -0
- package/package.json +9 -4
- package/plugins/vanguard-frontier-agentic/.codex-plugin/plugin.json +1 -1
- package/powers/vanguard-generic/POWER.md +6 -6
- package/powers/vanguard-kubernetes/POWER.md +1 -1
- package/powers/vanguard-multi-cloud/POWER.md +1 -1
- package/schemas/agentic-handoff.schema.json +443 -0
- package/scripts/generate-kiro-powers.mjs +17 -6
- package/scripts/generate-mods.mjs +784 -0
- package/scripts/mods/common.mjs +213 -0
- package/scripts/mods/runtime.mjs +140 -0
- package/scripts/typescript_data/agents/00-typescript-maestro-agent.json +2 -2
- package/scripts/typescript_data/agents/11-typescript-mcp-tool-contract-agent.json +4 -4
- package/skills/cross-functional/agentic-board-protocol/SKILL.md +85 -0
- package/skills/cross-functional/agentic-board-protocol/metadata.json +28 -0
- package/skills/cross-functional/agentic-board-protocol/references/decision-record.md +134 -0
- package/skills/cross-functional/agentic-board-protocol/references/handoff-envelope.md +208 -0
- package/skills/cross-functional/agentic-board-protocol/references/risk-tier-crosswalk.md +88 -0
- package/skills/cross-functional/agentic-board-protocol/references/stop-conditions.md +122 -0
- package/skills/cross-functional/agentic-board-protocol/references/value-case.md +159 -0
- package/skills/cross-functional/agentic-security-review/SKILL.md +78 -0
- package/skills/cross-functional/agentic-security-review/metadata.json +30 -0
- package/skills/cross-functional/agentic-security-review/references/identity-and-delegated-authorization.md +144 -0
- package/skills/cross-functional/agentic-security-review/references/supply-chain-and-provenance.md +111 -0
- package/skills/cross-functional/agentic-security-review/references/threat-model-owasp-asi.md +55 -0
- package/skills/cross-functional/agentic-security-review/references/tools-memory-and-data-reach.md +125 -0
- package/skills/cross-functional/agentic-security-review/references/workflow-and-output.md +210 -0
- package/skills/cross-functional/agentic-system-architecture-review/SKILL.md +74 -0
- package/skills/cross-functional/agentic-system-architecture-review/metadata.json +31 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/delegation-and-handoff-review.md +107 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/pattern-selection.md +81 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/reliability-and-containment.md +86 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/state-memory-and-durability.md +102 -0
- package/skills/cross-functional/agentic-system-architecture-review/references/workflow-and-output.md +135 -0
- package/skills/typescript/typescript-maestro/SKILL.md +1 -1
- package/skills/typescript/typescript-maestro/references/routing-taxonomy.md +1 -1
- package/skills/typescript/typescript-mcp-tool-contract/SKILL.md +1 -1
- package/tests/agentic-board-suite/_build_fixtures.py +1751 -0
- package/tests/agentic-board-suite/_build_harnesses.py +123 -0
- package/tests/agentic-board-suite/agentic_board_rules.py +857 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--challenge-names-a-later-envelope.json +244 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-challenge-beside-a-request.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-PROPOSAL-LINK--dangling-proposal.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--round-two-declared-as-one.json +481 -0
- package/tests/agentic-board-suite/fixtures/invalid/CH-ROUND-COUNTED--self-declared-round.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ALTERNATIVES--no-alternatives.json +340 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-behind-by-proposer-swap.json +532 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-left-unattached.json +480 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--hard-gate-on-root-left-unattached.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--late-challenge-before-record.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-COMPLETE--superseded-request-missed-earlier-challenge.json +565 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-attached-before-it-was-sent.json +483 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-PRECEDES--withdrawal-descends-from-the-record.json +547 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r1-flag-without-challenge.json +349 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r2-without-challenge.json +339 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-REQUIRED--r3-no-flags-without-challenge.json +339 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-RESOLVES--dangling-challenge.json +347 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CHALLENGE-TARGETS-PROPOSAL--challenge-of-requester-message.json +415 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-no-class.json +345 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-CLASS-FROM-ROOT--root-declares-other-class.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--designated-challenger-only-on-the-framing.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--investment-without-finance.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-DESIGNATED-CHALLENGER--stopped-designated-challenge.json +348 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--conditional-over-hard-gate.json +357 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-HARD-GATE--unresolved-hard-gate.json +357 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-a-stopped-challenge.json +413 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-NO-APPROVE-OVER-DISSENT--approve-over-dissent.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--re-ask-without-supersedes.json +426 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--request-superseded-twice.json +581 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-challenge.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-dangling-id.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersedes-a-later-request.json +500 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--superseding-request-from-another-proposer.json +621 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-ONE-REQUEST--supersession-cycle.json +501 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--agent-opens-decision-path.json +480 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--owner-is-the-requester.json +273 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-later-sorting-non-owner.json +273 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--request-to-non-owner.json +273 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-OWNER-FROM-ROOT--root-names-no-owner.json +341 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-IS-SENDER--challenger-assembles-request.json +337 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-PROPOSER-NOT-CHALLENGER--self-challenge.json +411 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--concurrent-higher-tier-envelope.json +403 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--r5-decided-through-lower-tier-request.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--request-drops-materiality-flags.json +479 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-RISK-CARRIED--risk-raised-after-request-before-decision.json +403 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-TO-PRINCIPAL--request-to-agent.json +272 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--dissent-dropped.json +341 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--stopped-challenge-recorded-withdrawn.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-UNRESOLVED-DISSENT--unresolved-without-dissent.json +341 -0
- package/tests/agentic-board-suite/fixtures/invalid/DR-VALUE-CASE-REQUIRED--investment-without-value-case.json +353 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--decided-before-request.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-AFTER-REQUEST--request-descends-from-its-record.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-APPROVAL-STATE--state-contradicts-outcome.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-approves-a-redesign.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-contradicts-outcome.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-does-not-descend-from-its-record.json +499 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-is-the-parent-of-its-record.json +500 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-names-another-approver.json +438 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-nobody-asked-for.json +421 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGN-BINDING--co-sign-sent-before-the-record.json +499 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--decision-in-force-not-co-signed.json +591 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--later-sorting-second-approver-never-signs.json +359 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-CO-SIGNED--named-second-approver-never-signs.json +359 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-COMPLETED--record-left-dispatched.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--decided-by-non-addressee.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-ADDRESSEE--earlier-sorting-non-addressee-decides.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-DECIDER-IS-SENDER--relayed-decision.json +408 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--override-without-acceptance.json +356 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--same-round-withdrawal.json +423 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--stopped-over-cap-withdrawal.json +623 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--unrelated-withdrawal-does-not-clear-gate.json +482 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-names-an-undelivered-envelope.json +482 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-HARD-GATE-ACCEPTANCE--withdrawal-on-another-proposers-revision.json +600 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-NOT-REQUESTER--second-approver-is-requester.json +433 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--contradicting-records.json +416 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-ONE-RECORD--duplicate-co-signature.json +507 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-PRINCIPAL-SOURCE--agent-writes-decision.json +345 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--points-at-challenge.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--record-names-a-missing-request.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-REQUEST-BINDING--request-edited-after-decision.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-request-decided-on-lower-tier-branch.json +418 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--r5-single-approver.json +354 -0
- package/tests/agentic-board-suite/fixtures/invalid/DRC-SECOND-APPROVER--second-approver-is-decider.json +359 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-AGENT-NO-APPROVAL--agent-claims-approval.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-APPROVAL-ONLY-ON-RECORD--principal-delegate-claims-approval.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-ACTIONS--child-widens-actions.json +345 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--child-raises-limit.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ATTEN-BUDGET--currency-changed.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-BLOCK-AS-CHALLENGE--block-sent-as-return.json +184 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--parent-sender-continues-unstopped.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-CUSTODY--reply-from-outside-chain.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-PARENT--hop-skips.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-CHAIN-TIME--sent-before-parent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-live-guard.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-DESTINATION-TIER--delegate-to-untiered-agent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalate-to-agent.json +131 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ESCALATE-TO-PRINCIPAL--escalated-delegate-to-agent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-CLASS--classification-lowered.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-DEADLINE--deadline-extended.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MATERIALITY--flag-dropped.json +434 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-MAX-HOPS--max-hops-raised.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-PROHIBITIONS--prohibition-dropped.json +131 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--one-counter-decreases.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-SPENT--spent-counter-reset.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-MONO-TIER--tier-lowered.json +346 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-NO-EXECUTION--deploy-authorized.json +133 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-NO-SELF-ADDRESS--challenge-to-self.json +187 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-CORRELATION--two-correlations.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--second-root-beside-a-decision.json +410 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ONE-ROOT--two-roots.json +190 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-CHALLENGE--challenge-without-payload.json +180 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-RECORD--record-without-payload.json +331 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-DECISION-REQUEST--request-without-payload.json +324 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-ESCALATE--escalate-without-payload.json +128 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-EXCLUSIVE--return-with-escalation.json +136 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-PAYLOAD-RETURN--return-without-verdict.json +124 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-REASON-HAS-STATUS--reason-on-completed.json +134 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-on-inference-only.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-over-blocking-gap.json +137 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-EVIDENCE--pass-with-no-evidence.json +127 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-RETURN-TO-SENDER--return-skips-the-delegator.json +185 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-ROOT-PARENT--root-with-parent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-SCHEMA--unknown-credential-field.json +133 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-STOP-NOTE--conflict-without-note.json +133 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-STOPPED-HAS-REASON--stopped-without-reason.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/HO-UNIQUE-TASK-ID--duplicate-task-id.json +198 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--budget-undeclared.json +123 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-BUDGET--return-overspent.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--revisions-carried-in-rebuttal-challenges.json +461 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-round.json +454 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-CHALLENGE-ROUNDS--third-version-by-return-naming-the-original.json +461 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-DEADLINE--sent-after-deadline.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-DEPTH--depth-undeclared.json +182 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-INSTRUCTION-BAIT--claimed-approval-dispatched.json +132 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-LIVE-GUARD--mutation-dispatched.json +130 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--blocking-gap-dispatched.json +137 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-MISSING-EVIDENCE--decision-request-blocking-gap.json +351 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--exchanges-on-the-original-envelope.json +381 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--loop-undeclared.json +182 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--revisit-to-challenger-of-another-party.json +256 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-ROUTING-LOOP--second-revisit-to-challenger.json +540 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--challenge-answers-stopped-delegate.json +247 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-escalated-request.json +353 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--decision-on-stopped-request.json +422 -0
- package/tests/agentic-board-suite/fixtures/invalid/STOP-TERMINAL--reply-to-stopped-delegate.json +244 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-BENEFIT-CEILING--benefit-above-baseline.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--negative-risk-loss.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COST-SIGN--risk-loss-low-minus-one.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--single-agent-counterfactual-missing.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-COUNTERFACTUALS--workflow-counterfactual-missing.json +427 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-NAV-ARITHMETIC--nav-off-by-one.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-ORDER--inverted-range.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-OVERSIGHT--free-oversight.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--measured-but-not-incremental.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-PROMOTION--promoted-on-estimate.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-RECOMMEND-INCREMENTAL--recommend-non-incremental.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-RUNS-BOUND--more-successes-than-runs.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-SYNTHETIC--synthetic-claims-measured.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--no-successes-nonzero-cost.json +436 -0
- package/tests/agentic-board-suite/fixtures/invalid/VC-UNIT-COST--unit-cost-mismatch.json +436 -0
- package/tests/agentic-board-suite/fixtures/valid/v01-r0-single-review.json +129 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-on-inference.json +125 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-block-return-to-principal.json +125 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-documentation-evidence.json +129 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-mixed-evidence.json +133 -0
- package/tests/agentic-board-suite/fixtures/valid/v01x-pass-return-nonblocking-gap.json +134 -0
- package/tests/agentic-board-suite/fixtures/valid/v02-r2-architecture-dissent.json +343 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-decision.json +407 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-challenge-after-record-same-second.json +464 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-redispatch-after-cured-stop.json +534 -0
- package/tests/agentic-board-suite/fixtures/valid/v02x-security-proposal-returned-to-its-delegator.json +407 -0
- package/tests/agentic-board-suite/fixtures/valid/v03-investment-loses-to-workflow.json +433 -0
- package/tests/agentic-board-suite/fixtures/valid/v04-r3-hard-gate-risk-acceptance.json +354 -0
- package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-escalate.json +354 -0
- package/tests/agentic-board-suite/fixtures/valid/v04x-live-hard-gate-recommend-reject.json +354 -0
- package/tests/agentic-board-suite/fixtures/valid/v05-live-guard-escalation.json +129 -0
- package/tests/agentic-board-suite/fixtures/valid/v06-r5-dual-approval.json +430 -0
- package/tests/agentic-board-suite/fixtures/valid/v06x-co-sign-same-second.json +430 -0
- package/tests/agentic-board-suite/fixtures/valid/v06x-re-asked-r5-decision-co-signed-again.json +662 -0
- package/tests/agentic-board-suite/fixtures/valid/v06x-unsigned-approve-superseded-by-co-signed-reject.json +588 -0
- package/tests/agentic-board-suite/fixtures/valid/v07-depth-and-routing-loop.json +180 -0
- package/tests/agentic-board-suite/fixtures/valid/v08-budget-exhausted.json +186 -0
- package/tests/agentic-board-suite/fixtures/valid/v09-instruction-bait.json +131 -0
- package/tests/agentic-board-suite/fixtures/valid/v09x-requester-restates-after-bait.json +251 -0
- package/tests/agentic-board-suite/fixtures/valid/v10-r1-optional-challenge.json +184 -0
- package/tests/agentic-board-suite/fixtures/valid/v10x-report-up-after-challenge.json +246 -0
- package/tests/agentic-board-suite/fixtures/valid/v11-two-rounds-hard-gate-withdrawn.json +478 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-designated-challenge-first-answer-over-cap.json +598 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-interleaved-challenger-round-two.json +604 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-proposal-handed-to-the-challenger-capped.json +860 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-round-three-escalation-path.json +747 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-same-second-round-two.json +478 -0
- package/tests/agentic-board-suite/fixtures/valid/v11x-two-findings-one-round.json +549 -0
- package/tests/agentic-board-suite/fixtures/valid/v12-superseded-request.json +497 -0
- package/tests/agentic-board-suite/fixtures/valid/v12x-challenge-after-the-first-decision.json +566 -0
- package/tests/agentic-board-suite/fixtures/valid/v12x-late-challenge-cured-by-supersede.json +566 -0
- package/tests/agentic-board-suite/fixtures/valid/v12x-supersede-in-the-same-second.json +497 -0
- package/tests/agentic-board-suite/test_agentic_board.py +502 -0
- package/tests/fixtures/README.md +1 -1
- package/tests/fuzz-properties.test.mjs +254 -0
- package/tests/mods.test.mjs +768 -0
- package/tests/validate-agent-tool-tiers.py +7 -5
- package/tests/validate-asset-integrity.py +1 -0
- package/tests/validate-mods.py +821 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# context-burn
|
|
2
|
+
|
|
3
|
+
Counts tool calls per session in a small status file. Where the harness reports token use,
|
|
4
|
+
it also warns once at 70% and once at 90% of the context window. It only observes: it never
|
|
5
|
+
blocks a tool and never changes a tool result.
|
|
6
|
+
|
|
7
|
+
## Reach and declared footprint
|
|
8
|
+
|
|
9
|
+
**L1 (file writes).** The manifest declares default writes to
|
|
10
|
+
`.claude/context-burn-status/*.json`, `.kiro/context-burn-status/*.json` and
|
|
11
|
+
`.codex/context-burn-status/*.json`, and reads `VANGUARD_CONTEXT_BURN_STATUS_PATH`.
|
|
12
|
+
It declares no subprocess spawns or network egress. This is capability disclosure,
|
|
13
|
+
not isolation; the status-path override can write outside these defaults.
|
|
14
|
+
|
|
15
|
+
## Status file
|
|
16
|
+
|
|
17
|
+
Fields (pretty-printed JSON, rewritten in full on every event):
|
|
18
|
+
|
|
19
|
+
- `session_id`: the session the counts belong to.
|
|
20
|
+
- `tool_call_count`: tool calls seen in that session.
|
|
21
|
+
- `last_updated`: ISO 8601 time of the last event.
|
|
22
|
+
- `threshold_events`: `{ threshold, tokens, window, at }` for each threshold crossed, at most
|
|
23
|
+
one per threshold per session. Empty where no token data exists.
|
|
24
|
+
|
|
25
|
+
Default paths, relative to the project:
|
|
26
|
+
|
|
27
|
+
| Harness | Path |
|
|
28
|
+
|---|---|
|
|
29
|
+
| Claude Code | `.claude/context-burn-status/<session-hash>.json` |
|
|
30
|
+
| Kiro | `.kiro/context-burn-status/<session-hash>.json` |
|
|
31
|
+
| Codex | `.codex/context-burn-status/<session-hash>.json` |
|
|
32
|
+
|
|
33
|
+
`<session-hash>` is the full lowercase SHA-256 of the UTF-8 `session_id`; raw session IDs
|
|
34
|
+
never appear in the path. Missing or empty Node session IDs use `unsessioned`, so those
|
|
35
|
+
events share one fallback file rather than isolated session state.
|
|
36
|
+
|
|
37
|
+
`VANGUARD_CONTEXT_BURN_STATUS_PATH` (when non-empty) is an exact path override. Using the
|
|
38
|
+
same override across sessions opts out of default isolation. It can point outside the
|
|
39
|
+
declared `file_writes` globs. Old single-file state is not migrated automatically.
|
|
40
|
+
|
|
41
|
+
## Per harness
|
|
42
|
+
|
|
43
|
+
| Harness | Behavior |
|
|
44
|
+
|---|---|
|
|
45
|
+
| Claude Code native | `session.start` resets the file. After each tool call it reads context use from the session, toasts the 70% warning and the 90% critical message once each, and updates the file. |
|
|
46
|
+
| Claude Code classic | Counter only. No token data, so no warnings. |
|
|
47
|
+
| Kiro | Counter only, as above. See `kiro/UNSUPPORTED.md`. |
|
|
48
|
+
| Codex | Counter only, as above. See `codex/UNSUPPORTED.md`. |
|
|
49
|
+
|
|
50
|
+
Messages: `[context-burn] WARNING: 70% context used (<tokens> / <window> tokens)` and
|
|
51
|
+
`[context-burn] CRITICAL: 90% context used (<tokens> / <window> tokens)`.
|
|
52
|
+
|
|
53
|
+
## Limitations
|
|
54
|
+
|
|
55
|
+
- Install either the native or the classic surface, not both: they share
|
|
56
|
+
the same session status file and would double count.
|
|
57
|
+
- Updates are unlocked read-modify-write operations, so same-session parallel hooks can
|
|
58
|
+
lose increments. There is no automatic cleanup of old status files.
|
|
59
|
+
- Classic PostToolUse fires only after a tool succeeds, so failed calls are not counted.
|
|
60
|
+
- An unreadable or invalid status file, including malformed threshold event objects or
|
|
61
|
+
fields, starts a fresh count; the Node hook says so on stderr.
|
|
62
|
+
- The native API was verified against Claude Code 2.1.296. The Node hooks need Node 22+.
|
|
63
|
+
|
|
64
|
+
## Install
|
|
65
|
+
|
|
66
|
+
See `mods/README.md`.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// Command hook for classic Claude Code, Kiro and Codex: counter only, no token data.
|
|
2
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
3
|
+
import { dirname } from 'node:path'
|
|
4
|
+
import { defaultStatusPath, freshStatus, parseStatus, recordCall, serializeStatus } from '../lib/core.mjs'
|
|
5
|
+
import { projectPath, runHook } from '../lib/runtime.mjs'
|
|
6
|
+
|
|
7
|
+
const target = async (ctx, sessionId) =>
|
|
8
|
+
projectPath(process.env.VANGUARD_CONTEXT_BURN_STATUS_PATH || await defaultStatusPath(ctx.harness, sessionId), ctx.harness, import.meta.dirname)
|
|
9
|
+
const sessionOf = (p) => (typeof p.session_id === 'string' && p.session_id !== '' ? p.session_id : 'unsessioned')
|
|
10
|
+
|
|
11
|
+
function save(file, status) {
|
|
12
|
+
mkdirSync(dirname(file), { recursive: true })
|
|
13
|
+
writeFileSync(file, serializeStatus(status))
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const handlers = {
|
|
17
|
+
async SessionStart(payload, ctx) {
|
|
18
|
+
const sessionId = sessionOf(payload)
|
|
19
|
+
save(await target(ctx, sessionId), freshStatus(sessionId, new Date().toISOString()))
|
|
20
|
+
},
|
|
21
|
+
// ponytail: parallel hooks can lose an increment (read-modify-write, no lock); add the
|
|
22
|
+
// audit-trail lock if exact counts matter.
|
|
23
|
+
async PostToolUse(payload, ctx) {
|
|
24
|
+
const sessionId = sessionOf(payload)
|
|
25
|
+
const file = await target(ctx, sessionId)
|
|
26
|
+
const log = []
|
|
27
|
+
let prev
|
|
28
|
+
try {
|
|
29
|
+
prev = parseStatus(readFileSync(file, 'utf8'))
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err?.code !== 'ENOENT') log.push(`[context-burn] status file unreadable; starting a fresh count: ${err.message}`)
|
|
32
|
+
}
|
|
33
|
+
save(file, recordCall(prev, sessionId, new Date().toISOString(), undefined).status)
|
|
34
|
+
return log.length ? { decision: 'allow', log } : undefined
|
|
35
|
+
},
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
process.exitCode = await runHook({ name: 'context-burn', handlers })
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Context Burn on Claude Code
|
|
2
|
+
|
|
3
|
+
<!-- Generated by scripts/generate-mods.mjs from mod.toml. Do not edit. -->
|
|
4
|
+
|
|
5
|
+
## Degraded
|
|
6
|
+
|
|
7
|
+
- `classic`: Classic hooks receive no token counts, so only the tool-call counter is kept and no threshold warnings are emitted; PostToolUse fires only after a tool succeeds, so failed calls are not counted.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/vanguard-mods/context-burn/bin/hook.mjs\" --harness claude-code --event PostToolUse"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"SessionStart": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node \"$CLAUDE_PROJECT_DIR/.claude/vanguard-mods/context-burn/bin/hook.mjs\" --harness claude-code --event SessionStart"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"hooks": {
|
|
3
|
+
"PostToolUse": [
|
|
4
|
+
{
|
|
5
|
+
"hooks": [
|
|
6
|
+
{
|
|
7
|
+
"type": "command",
|
|
8
|
+
"command": "node .codex/vanguard-mods/context-burn/bin/hook.mjs --harness codex --event PostToolUse"
|
|
9
|
+
}
|
|
10
|
+
]
|
|
11
|
+
}
|
|
12
|
+
],
|
|
13
|
+
"SessionStart": [
|
|
14
|
+
{
|
|
15
|
+
"hooks": [
|
|
16
|
+
{
|
|
17
|
+
"type": "command",
|
|
18
|
+
"command": "node .codex/vanguard-mods/context-burn/bin/hook.mjs --harness codex --event SessionStart"
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { test, expect, mock } from 'claude-code/testing'
|
|
2
|
+
|
|
3
|
+
const ENV = 'VANGUARD_CONTEXT_BURN_STATUS_PATH'
|
|
4
|
+
const DEFAULT = '.claude/context-burn-status/e8bc163c82eee18733288c7d4ac636db3a6deb013ef2d37b68322be20edc45cc.json'
|
|
5
|
+
const RESULT = { result: { stdout: 'x', stderr: '', interrupted: false } }
|
|
6
|
+
const call = { tool: 'Bash', command: 'echo x' }
|
|
7
|
+
const status = (over: object = {}) => ({
|
|
8
|
+
session_id: 's1',
|
|
9
|
+
tool_call_count: 4,
|
|
10
|
+
last_updated: '1970-01-01T00:00:00.000Z',
|
|
11
|
+
threshold_events: [],
|
|
12
|
+
...over,
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
// Wires every `$` noun a tool.call touches. Returns what the mod wrote and toasted.
|
|
16
|
+
function rig(on: any, o: { env?: object; prev?: object | null; readFails?: boolean; writeFails?: boolean; context?: object; session?: { id: string }; files?: Map<string, string> } = {}) {
|
|
17
|
+
const writes: { path: string; text: string }[] = []
|
|
18
|
+
const toasts: string[] = []
|
|
19
|
+
mock.env(on, o.env ?? {})
|
|
20
|
+
mock.clock(on, { now: 0 })
|
|
21
|
+
on('session.id', () => ({ value: o.session?.id ?? 's1' }))
|
|
22
|
+
on('session.usage', () => ({
|
|
23
|
+
value: { startedAt: 0, context: o.context ?? { window: 10000 }, rateLimits: [] },
|
|
24
|
+
}))
|
|
25
|
+
on('fs.read', ($: any, e: any) => {
|
|
26
|
+
if (o.files) {
|
|
27
|
+
if (!o.files.has(e.path)) throw new Error('ENOENT')
|
|
28
|
+
return { value: o.files.get(e.path) }
|
|
29
|
+
}
|
|
30
|
+
if (o.readFails || !o.prev) throw new Error('ENOENT')
|
|
31
|
+
return { value: JSON.stringify(o.prev) }
|
|
32
|
+
})
|
|
33
|
+
on('fs.write', ($: any, e: any) => {
|
|
34
|
+
if (o.writeFails) throw new Error('disk full')
|
|
35
|
+
writes.push({ path: e.path, text: e.text })
|
|
36
|
+
o.files?.set(e.path, e.text)
|
|
37
|
+
return { value: undefined }
|
|
38
|
+
})
|
|
39
|
+
on('ui.toast', ($: any, e: any) => {
|
|
40
|
+
toasts.push(e.text)
|
|
41
|
+
return { value: undefined }
|
|
42
|
+
})
|
|
43
|
+
on('tool.call', () => RESULT)
|
|
44
|
+
on('session.start', ($: any, e: any) => ({ cwd: e.cwd }))
|
|
45
|
+
return { writes, toasts, last: () => JSON.parse(writes[writes.length - 1].text) }
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
test('session.start writes a fresh status', async ($, on) => {
|
|
49
|
+
const r = rig(on)
|
|
50
|
+
await $.session.start({ cwd: '/w', surface: 'terminal', isInteractive: true })
|
|
51
|
+
expect(r.writes[0].path.endsWith(DEFAULT)).toBe(true)
|
|
52
|
+
expect(r.last()).toEqual({ session_id: 's1', tool_call_count: 0, last_updated: '1970-01-01T00:00:00.000Z', threshold_events: [] })
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
test('session.start honors the env override', async ($, on) => {
|
|
56
|
+
const r = rig(on, { env: { [ENV]: 'custom/out.json' } })
|
|
57
|
+
await $.session.start({ cwd: '/w', surface: 'terminal', isInteractive: true })
|
|
58
|
+
expect(r.writes[0].path.endsWith('custom/out.json')).toBe(true)
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
test('tool.call passes the result through and increments the count', async ($, on) => {
|
|
62
|
+
const r = rig(on, { prev: status() })
|
|
63
|
+
expect(await $.tool.call(call)).toEqual(RESULT)
|
|
64
|
+
expect(r.last().tool_call_count).toBe(5)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('a status for another session restarts at 1', async ($, on) => {
|
|
68
|
+
const r = rig(on, { prev: status({ session_id: 'other' }) })
|
|
69
|
+
await $.tool.call(call)
|
|
70
|
+
expect(r.last().session_id).toBe('s1')
|
|
71
|
+
expect(r.last().tool_call_count).toBe(1)
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
test('75 percent toasts one warning and records the event', async ($, on) => {
|
|
75
|
+
const r = rig(on, { prev: status(), context: { tokens: 7500, window: 10000, percent: 75 } })
|
|
76
|
+
await $.tool.call(call)
|
|
77
|
+
expect(r.toasts).toEqual(['[context-burn] WARNING: 70% context used (7500 / 10000 tokens)'])
|
|
78
|
+
expect(r.last().threshold_events.length).toBe(1)
|
|
79
|
+
expect(r.last().threshold_events[0].threshold).toBe(70)
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
test('an already recorded 70 event does not toast again', async ($, on) => {
|
|
83
|
+
const prev = status({ threshold_events: [{ threshold: 70, tokens: 7500, window: 10000, at: 'x' }] })
|
|
84
|
+
const r = rig(on, { prev, context: { tokens: 7600, window: 10000, percent: 76 } })
|
|
85
|
+
await $.tool.call(call)
|
|
86
|
+
expect(r.toasts).toEqual([])
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
test('95 percent toasts warning then critical', async ($, on) => {
|
|
90
|
+
const r = rig(on, { prev: status(), context: { tokens: 9500, window: 10000, percent: 95 } })
|
|
91
|
+
await $.tool.call(call)
|
|
92
|
+
expect(r.toasts).toEqual([
|
|
93
|
+
'[context-burn] WARNING: 70% context used (9500 / 10000 tokens)',
|
|
94
|
+
'[context-burn] CRITICAL: 90% context used (9500 / 10000 tokens)',
|
|
95
|
+
])
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
test('percent without tokens computes and rounds tokens', async ($, on) => {
|
|
99
|
+
const r = rig(on, { prev: status(), context: { window: 3333, percent: 71.5 } })
|
|
100
|
+
await $.tool.call(call)
|
|
101
|
+
expect(r.toasts).toEqual(['[context-burn] WARNING: 70% context used (2383 / 3333 tokens)'])
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
test('no token data means no toast but the count still rises', async ($, on) => {
|
|
105
|
+
const r = rig(on, { prev: status(), context: { window: 10000 } })
|
|
106
|
+
await $.tool.call(call)
|
|
107
|
+
expect(r.toasts).toEqual([])
|
|
108
|
+
expect(r.last().tool_call_count).toBe(5)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
test('a failing read starts a fresh count', async ($, on) => {
|
|
112
|
+
const r = rig(on, { readFails: true })
|
|
113
|
+
await $.tool.call(call)
|
|
114
|
+
expect(r.last().tool_call_count).toBe(1)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('malformed threshold events restart counting and still toast the threshold', async ($, on) => {
|
|
118
|
+
const r = rig(on, { prev: status({ threshold_events: [null] }), context: { tokens: 7500, window: 10000 } })
|
|
119
|
+
expect(await $.tool.call(call)).toEqual(RESULT)
|
|
120
|
+
expect(r.last().tool_call_count).toBe(1)
|
|
121
|
+
expect(r.last().threshold_events.length).toBe(1)
|
|
122
|
+
expect(r.toasts.length).toBe(1)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
test('interleaved sessions retain independent counts and warn only once per session', async ($, on) => {
|
|
126
|
+
const session = { id: '../first' }
|
|
127
|
+
const files = new Map<string, string>()
|
|
128
|
+
const r = rig(on, { session, files, context: { tokens: 7500, window: 10000 } })
|
|
129
|
+
for (const id of ['../first', '..\\second']) {
|
|
130
|
+
session.id = id
|
|
131
|
+
await $.session.start({ cwd: '/w', surface: 'terminal', isInteractive: true })
|
|
132
|
+
expect(await $.tool.call(call)).toEqual(RESULT)
|
|
133
|
+
}
|
|
134
|
+
for (const id of ['../first', '..\\second']) {
|
|
135
|
+
session.id = id
|
|
136
|
+
expect(await $.tool.call(call)).toEqual(RESULT)
|
|
137
|
+
}
|
|
138
|
+
expect(files.size).toBe(2)
|
|
139
|
+
for (const [path, text] of files) {
|
|
140
|
+
expect(/\/context-burn-status\/[a-f0-9]{64}\.json$/.test(path)).toBe(true)
|
|
141
|
+
const saved = JSON.parse(text)
|
|
142
|
+
expect(saved.tool_call_count).toBe(2)
|
|
143
|
+
expect(saved.threshold_events.length).toBe(1)
|
|
144
|
+
}
|
|
145
|
+
expect(r.toasts.length).toBe(2)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('a failing write still returns the tool result', async ($, on) => {
|
|
149
|
+
rig(on, { prev: status(), writeFails: true })
|
|
150
|
+
expect(await $.tool.call(call)).toEqual(RESULT)
|
|
151
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Register } from 'claude-code'
|
|
2
|
+
import { defaultStatusPath, freshStatus, parseStatus, recordCall, serializeStatus } from '../lib/core.mjs'
|
|
3
|
+
|
|
4
|
+
async function statusPath($: any, sessionId: string): Promise<string> {
|
|
5
|
+
return (await $.env.get('VANGUARD_CONTEXT_BURN_STATUS_PATH')) || await defaultStatusPath('claude-code', sessionId)
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export const register: Register = (on) => {
|
|
9
|
+
// A new session resets the file; a failure must never block startup.
|
|
10
|
+
on('session.start', async ($, e, next) => {
|
|
11
|
+
const at = new Date(await $.clock.now()).toISOString()
|
|
12
|
+
const sessionId = await $.session.id()
|
|
13
|
+
await $.fs.write(await statusPath($, sessionId), serializeStatus(freshStatus(sessionId, at)))
|
|
14
|
+
return next(e)
|
|
15
|
+
}).catch(($, e, next) => next(e))
|
|
16
|
+
|
|
17
|
+
// Observer: the tool runs first and its result is returned untouched, whatever happens here.
|
|
18
|
+
on('tool.call', async ($, e, next) => {
|
|
19
|
+
const r = await next(e)
|
|
20
|
+
const sessionId = await $.session.id()
|
|
21
|
+
const path = await statusPath($, sessionId)
|
|
22
|
+
let prev
|
|
23
|
+
try {
|
|
24
|
+
prev = parseStatus(await $.fs.read(path))
|
|
25
|
+
} catch {
|
|
26
|
+
prev = undefined
|
|
27
|
+
}
|
|
28
|
+
const at = new Date(await $.clock.now()).toISOString()
|
|
29
|
+
const { context } = await $.session.usage()
|
|
30
|
+
const { status, messages } = recordCall(prev, sessionId, at, context)
|
|
31
|
+
for (const m of messages) await $.ui.toast(m)
|
|
32
|
+
await $.fs.write(path, serializeStatus(status))
|
|
33
|
+
return r
|
|
34
|
+
}).catch(($, e, next) => next(e))
|
|
35
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "v1",
|
|
3
|
+
"hooks": [
|
|
4
|
+
{
|
|
5
|
+
"name": "context-burn:PostToolUse",
|
|
6
|
+
"description": "Tracks context use per session in a status file and warns at 70% and 90% where the harness reports token use.",
|
|
7
|
+
"trigger": "PostToolUse",
|
|
8
|
+
"action": {
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node .kiro/vanguard-mods/context-burn/bin/hook.mjs --harness kiro --event PostToolUse"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "v1",
|
|
3
|
+
"hooks": [
|
|
4
|
+
{
|
|
5
|
+
"name": "context-burn:SessionStart",
|
|
6
|
+
"description": "Tracks context use per session in a status file and warns at 70% and 90% where the harness reports token use.",
|
|
7
|
+
"trigger": "SessionStart",
|
|
8
|
+
"action": {
|
|
9
|
+
"type": "command",
|
|
10
|
+
"command": "node .kiro/vanguard-mods/context-burn/bin/hook.mjs --harness kiro --event SessionStart"
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
// Shared helpers for the mods under mods/. scripts/generate-mods.mjs copies this file
|
|
2
|
+
// byte-for-byte to mods/<id>/lib/common.mjs, so edit it here and run `npm run mods:write`.
|
|
3
|
+
//
|
|
4
|
+
// Pure on purpose: no imports, no environment, no I/O. Claude Code's native mod runtime
|
|
5
|
+
// imports it and has no Node, and every harness must reach the same verdict on the same
|
|
6
|
+
// input. Regexes compile without the `u` flag so Node and Python `re.ASCII`
|
|
7
|
+
// (tests/validate-mods.py) read them the same way.
|
|
8
|
+
|
|
9
|
+
export const LIMITS = Object.freeze({
|
|
10
|
+
// A redaction that still changes after this many passes is withheld as a whole.
|
|
11
|
+
maxPasses: 8,
|
|
12
|
+
// A guard candidate longer than this is not evaluated and counts as a hit.
|
|
13
|
+
maxCandidateChars: 65536,
|
|
14
|
+
// Guard candidates are not evaluated past this combined length; the call counts as a hit.
|
|
15
|
+
maxTotalChars: 262144,
|
|
16
|
+
// Only the tail of a leaf's context (key or previous sibling) is prefixed to it.
|
|
17
|
+
maxContextChars: 256,
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
const MARKER_PREFIX = '[REDACTED:'
|
|
21
|
+
export const OVERFLOW_MARKER = '[REDACTED:overflow]'
|
|
22
|
+
const OVERFLOW_HIT = Object.freeze({ id: 'overflow', name: 'redaction did not converge' })
|
|
23
|
+
export const TOO_LARGE_HIT = Object.freeze({ id: 'too-large', name: 'input too large to evaluate' })
|
|
24
|
+
|
|
25
|
+
const ID_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/
|
|
26
|
+
const PATTERN_KEYS = new Set(['id', 'name', 'description', 'regex', 'flags', 'context', 'severity', 'agents'])
|
|
27
|
+
const isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string' && s !== '')
|
|
28
|
+
|
|
29
|
+
// Parser diagnostics can quote credential-bearing data; never expose it in hook logs.
|
|
30
|
+
export function parseJson(text, source) {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(text)
|
|
33
|
+
} catch {
|
|
34
|
+
throw new Error(`${source}: invalid JSON`)
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Validates a patterns.json-shaped list and compiles it. Throws on anything malformed so a
|
|
39
|
+
// bad extension file fails closed in the caller instead of silently matching nothing.
|
|
40
|
+
export function compilePatterns(list, source = 'patterns') {
|
|
41
|
+
if (!Array.isArray(list)) throw new Error(`${source}: expected an array of patterns`)
|
|
42
|
+
const seen = new Set()
|
|
43
|
+
return list.map((p, i) => {
|
|
44
|
+
const where = `${source}[${i}]`
|
|
45
|
+
if (p === null || typeof p !== 'object' || Array.isArray(p)) throw new Error(`${where}: expected an object`)
|
|
46
|
+
for (const k of Object.keys(p)) if (!PATTERN_KEYS.has(k)) throw new Error(`${where}: unknown key ${k}`)
|
|
47
|
+
if (typeof p.id !== 'string' || !ID_RE.test(p.id)) throw new Error(`${where}: id must be kebab-case`)
|
|
48
|
+
if (seen.has(p.id)) throw new Error(`${where}: duplicate id ${p.id}`)
|
|
49
|
+
seen.add(p.id)
|
|
50
|
+
if (typeof p.name !== 'string' || p.name === '') throw new Error(`${where}: name must be a non-empty string`)
|
|
51
|
+
if (typeof p.regex !== 'string' || p.regex === '') throw new Error(`${where}: regex must be a non-empty string`)
|
|
52
|
+
if (p.flags !== '' && p.flags !== 'i') throw new Error(`${where}: flags must be "" or "i"`)
|
|
53
|
+
if (!isStringArray(p.context) || p.context.length === 0) throw new Error(`${where}: context must be a non-empty string array`)
|
|
54
|
+
for (const k of ['description', 'severity']) {
|
|
55
|
+
if (p[k] !== undefined && typeof p[k] !== 'string') throw new Error(`${where}: ${k} must be a string`)
|
|
56
|
+
}
|
|
57
|
+
if (p.agents !== undefined && !isStringArray(p.agents)) throw new Error(`${where}: agents must be a string array`)
|
|
58
|
+
let rx, rxg
|
|
59
|
+
try {
|
|
60
|
+
rx = new RegExp(p.regex, p.flags)
|
|
61
|
+
rxg = new RegExp(p.regex, 'g' + p.flags)
|
|
62
|
+
} catch {
|
|
63
|
+
throw new Error(`${where}: regex does not compile`)
|
|
64
|
+
}
|
|
65
|
+
return Object.freeze({
|
|
66
|
+
id: p.id,
|
|
67
|
+
name: p.name,
|
|
68
|
+
severity: p.severity,
|
|
69
|
+
context: Object.freeze([...p.context]),
|
|
70
|
+
agents: p.agents ? Object.freeze([...p.agents]) : undefined,
|
|
71
|
+
rx,
|
|
72
|
+
rxg,
|
|
73
|
+
marker: `${MARKER_PREFIX}${p.id}]`,
|
|
74
|
+
})
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Every string value (not key) in depth-first order. The order follows the input, so the
|
|
79
|
+
// same payload always yields the same list.
|
|
80
|
+
export function stringLeaves(value, out = []) {
|
|
81
|
+
if (typeof value === 'string') out.push(value)
|
|
82
|
+
else if (Array.isArray(value)) for (const v of value) stringLeaves(v, out)
|
|
83
|
+
else if (value !== null && typeof value === 'object') for (const v of Object.values(value)) stringLeaves(v, out)
|
|
84
|
+
return out
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Guard candidates: every string leaf, plus every all-string array joined with spaces so
|
|
88
|
+
// an argv-style command (["rm", "-rf", "/"]) reads like the shell line it runs.
|
|
89
|
+
export function candidates(value) {
|
|
90
|
+
const out = stringLeaves(value)
|
|
91
|
+
const joinArrays = (v) => {
|
|
92
|
+
if (Array.isArray(v)) {
|
|
93
|
+
if (v.length > 1 && v.every((s) => typeof s === 'string')) out.push(v.join(' '))
|
|
94
|
+
v.forEach(joinArrays)
|
|
95
|
+
} else if (v !== null && typeof v === 'object') {
|
|
96
|
+
Object.values(v).forEach(joinArrays)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
joinArrays(value)
|
|
100
|
+
return out
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// The patterns tagged with `context` that match any text, in catalog order. Oversized input
|
|
104
|
+
// is not evaluated (regex cost is unbounded there) and reports TOO_LARGE_HIT instead.
|
|
105
|
+
export function matchPatterns(patterns, texts, context) {
|
|
106
|
+
let total = 0
|
|
107
|
+
for (const t of texts) {
|
|
108
|
+
total += t.length
|
|
109
|
+
if (t.length > LIMITS.maxCandidateChars || total > LIMITS.maxTotalChars) return [TOO_LARGE_HIT]
|
|
110
|
+
}
|
|
111
|
+
return patterns.filter((p) => p.context.includes(context) && texts.some((t) => p.rx.test(t)))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// One pattern pass after another until the text stops changing. Each pass can expose a new
|
|
115
|
+
// match (password-in-url can swallow an earlier marker), so a single pass is not enough.
|
|
116
|
+
function fixedPoint(text, patterns, hits) {
|
|
117
|
+
let x = text
|
|
118
|
+
for (let pass = 0; pass <= LIMITS.maxPasses; pass++) {
|
|
119
|
+
let y = x
|
|
120
|
+
for (const p of patterns) {
|
|
121
|
+
y = y.replace(p.rxg, (m) => {
|
|
122
|
+
if (m === '') return m
|
|
123
|
+
hits.add(p)
|
|
124
|
+
return p.marker
|
|
125
|
+
})
|
|
126
|
+
}
|
|
127
|
+
if (y === x) return x
|
|
128
|
+
x = y
|
|
129
|
+
}
|
|
130
|
+
hits.add(OVERFLOW_HIT)
|
|
131
|
+
return OVERFLOW_MARKER
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Redacts `s` with `ctx` in front so a secret split across a key and its value, or across
|
|
135
|
+
// argv elements ("Bearer", "<token>"), still matches; then strips the context back off.
|
|
136
|
+
function redactLeaf(s, ctx, patterns, hits) {
|
|
137
|
+
if (ctx === '') return fixedPoint(s, patterns, hits)
|
|
138
|
+
const pre = ctx.slice(-LIMITS.maxContextChars) + ' '
|
|
139
|
+
const y = fixedPoint(pre + s, patterns, hits)
|
|
140
|
+
if (y === OVERFLOW_MARKER) return y
|
|
141
|
+
const r = fixedPoint(pre, patterns, new Set())
|
|
142
|
+
if (y.startsWith(r)) return fixedPoint(y.slice(r.length), patterns, hits)
|
|
143
|
+
// A match spans the boundary: keep from the marker that covers it. Slicing changes
|
|
144
|
+
// anchors and boundaries, so also check the standalone leaf after contextual matching.
|
|
145
|
+
let d = 0
|
|
146
|
+
while (d < r.length && y[d] === r[d]) d++
|
|
147
|
+
const i = y.lastIndexOf(MARKER_PREFIX, d)
|
|
148
|
+
return fixedPoint(i >= 0 ? y.slice(i) : y, patterns, hits)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function walk(v, ctx, patterns, hits) {
|
|
152
|
+
if (typeof v === 'string') {
|
|
153
|
+
const local = new Set()
|
|
154
|
+
const out = redactLeaf(v, ctx, patterns, local)
|
|
155
|
+
if (out !== v) for (const h of local) hits.add(h)
|
|
156
|
+
return out
|
|
157
|
+
}
|
|
158
|
+
if (Array.isArray(v)) {
|
|
159
|
+
let prev = null
|
|
160
|
+
return v.map((el) => {
|
|
161
|
+
const out = walk(el, typeof prev === 'string' ? prev : ctx, patterns, hits)
|
|
162
|
+
prev = el
|
|
163
|
+
return out
|
|
164
|
+
})
|
|
165
|
+
}
|
|
166
|
+
if (v !== null && typeof v === 'object') {
|
|
167
|
+
const named = typeof v.name === 'string' ? v.name : typeof v.key === 'string' ? v.key : null
|
|
168
|
+
// fromEntries, not assignment: `out[k] = v` would drop an own "__proto__" key.
|
|
169
|
+
return Object.fromEntries(
|
|
170
|
+
Object.entries(v).map(([k, x]) => [
|
|
171
|
+
walk(k, '', patterns, hits),
|
|
172
|
+
walk(x, k === 'value' && named !== null ? named : k, patterns, hits),
|
|
173
|
+
]),
|
|
174
|
+
)
|
|
175
|
+
}
|
|
176
|
+
return v
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Returns a copy of `value` with every match replaced by `[REDACTED:<id>]`, plus the
|
|
180
|
+
// patterns that fired (catalog order, unique). Keys are redacted too; two keys that redact
|
|
181
|
+
// to the same marker collapse into one, which loses data but never leaks it.
|
|
182
|
+
export function redactValue(value, patterns) {
|
|
183
|
+
const hits = new Set()
|
|
184
|
+
const out = walk(value, '', patterns, hits)
|
|
185
|
+
const order = [...patterns, OVERFLOW_HIT]
|
|
186
|
+
return { value: out, hits: order.filter((p) => hits.has(p)) }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Same shape as `value`, every string replaced by `text`. The fallback when redaction
|
|
190
|
+
// itself failed: withhold everything rather than pass anything through.
|
|
191
|
+
export function maskValue(value, text) {
|
|
192
|
+
if (typeof value === 'string') return text
|
|
193
|
+
if (Array.isArray(value)) return value.map((v) => maskValue(v, text))
|
|
194
|
+
if (value !== null && typeof value === 'object') {
|
|
195
|
+
return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, maskValue(v, text)]))
|
|
196
|
+
}
|
|
197
|
+
return value
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// JSON with object keys sorted at every level, so equal values hash equally.
|
|
201
|
+
export function canonicalJson(value) {
|
|
202
|
+
if (Array.isArray(value)) return '[' + value.map((v) => (v === undefined ? 'null' : canonicalJson(v))).join(',') + ']'
|
|
203
|
+
if (value !== null && typeof value === 'object') {
|
|
204
|
+
const keys = Object.keys(value).filter((k) => value[k] !== undefined).sort()
|
|
205
|
+
return '{' + keys.map((k) => JSON.stringify(k) + ':' + canonicalJson(value[k])).join(',') + '}'
|
|
206
|
+
}
|
|
207
|
+
return JSON.stringify(value) ?? 'null'
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export async function sha256Hex(text) {
|
|
211
|
+
const digest = await globalThis.crypto.subtle.digest('SHA-256', new TextEncoder().encode(text))
|
|
212
|
+
return Array.from(new Uint8Array(digest), (b) => b.toString(16).padStart(2, '0')).join('')
|
|
213
|
+
}
|