@soleur/opencode 0.0.0-stage → 3.354.2
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/agents.manifest.json +476 -0
- package/.claude-plugin/plugin.json +42 -0
- package/README.md +44 -2
- package/agents/community/.gitkeep +0 -0
- package/agents/engineering/cto.md +87 -0
- package/agents/engineering/design/ddd-architect.md +95 -0
- package/agents/engineering/discovery/agent-finder.md +214 -0
- package/agents/engineering/discovery/functional-discovery.md +182 -0
- package/agents/engineering/infra/infra-security.md +151 -0
- package/agents/engineering/infra/platform-strategist.md +109 -0
- package/agents/engineering/infra/terraform-architect.md +171 -0
- package/agents/engineering/prompt-engineer.md +37 -0
- package/agents/engineering/research/best-practices-researcher.md +115 -0
- package/agents/engineering/research/framework-docs-researcher.md +93 -0
- package/agents/engineering/research/git-history-analyzer.md +53 -0
- package/agents/engineering/research/learnings-researcher.md +285 -0
- package/agents/engineering/research/repo-research-analyst.md +118 -0
- package/agents/engineering/review/agent-native-reviewer.md +279 -0
- package/agents/engineering/review/architecture-strategist.md +61 -0
- package/agents/engineering/review/code-quality-analyst.md +92 -0
- package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
- package/agents/engineering/review/data-integrity-guardian.md +82 -0
- package/agents/engineering/review/data-migration-expert.md +98 -0
- package/agents/engineering/review/deployment-verification-agent.md +183 -0
- package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
- package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
- package/agents/engineering/review/legacy-code-expert.md +55 -0
- package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
- package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
- package/agents/engineering/review/performance-oracle.md +130 -0
- package/agents/engineering/review/security-sentinel.md +167 -0
- package/agents/engineering/review/semgrep-sast.md +36 -0
- package/agents/engineering/review/test-design-reviewer.md +150 -0
- package/agents/engineering/review/user-impact-reviewer.md +70 -0
- package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
- package/agents/finance/budget-analyst.md +29 -0
- package/agents/finance/cfo.md +44 -0
- package/agents/finance/financial-reporter.md +30 -0
- package/agents/finance/revenue-analyst.md +30 -0
- package/agents/legal/clo.md +115 -0
- package/agents/legal/legal-compliance-auditor.md +110 -0
- package/agents/legal/legal-document-generator.md +134 -0
- package/agents/marketing/analytics-analyst.md +25 -0
- package/agents/marketing/brand-architect.md +181 -0
- package/agents/marketing/cmo.md +76 -0
- package/agents/marketing/conversion-optimizer.md +19 -0
- package/agents/marketing/copywriter.md +20 -0
- package/agents/marketing/fact-checker.md +70 -0
- package/agents/marketing/growth-strategist.md +145 -0
- package/agents/marketing/paid-media-strategist.md +28 -0
- package/agents/marketing/pricing-strategist.md +17 -0
- package/agents/marketing/programmatic-seo-specialist.md +25 -0
- package/agents/marketing/retention-strategist.md +23 -0
- package/agents/marketing/seo-aeo-analyst.md +164 -0
- package/agents/operations/coo.md +50 -0
- package/agents/operations/ops-advisor.md +78 -0
- package/agents/operations/ops-provisioner.md +101 -0
- package/agents/operations/ops-research.md +52 -0
- package/agents/operations/service-automator.md +278 -0
- package/agents/product/business-validator.md +206 -0
- package/agents/product/competitive-intelligence.md +89 -0
- package/agents/product/cpo.md +63 -0
- package/agents/product/design/ux-design-lead.md +209 -0
- package/agents/product/spec-flow-analyzer.md +122 -0
- package/agents/sales/cro.md +44 -0
- package/agents/sales/deal-architect.md +29 -0
- package/agents/sales/outbound-strategist.md +29 -0
- package/agents/sales/pipeline-analyst.md +29 -0
- package/agents/support/cco.md +48 -0
- package/agents/support/community-manager.md +570 -0
- package/agents/support/ticket-triage.md +70 -0
- package/bin/soleur-opencode.js +29 -0
- package/commands/go.md +660 -0
- package/commands/help.md +273 -0
- package/commands/sync.md +977 -0
- package/commands/version.md +103 -0
- package/hooks/antigravity-hooks.json +84 -0
- package/hooks/antigravity-session-start.sh +15 -0
- package/hooks/bare-root-cwd-guard.sh +131 -0
- package/hooks/browser-snapshot-credential-guard.sh +200 -0
- package/hooks/codex-session-start.sh +9 -0
- package/hooks/compaction-state.sh +525 -0
- package/hooks/destructive-command-guard.sh +1387 -0
- package/hooks/devin-session-start.sh +90 -0
- package/hooks/ext-filter-detect.sh +171 -0
- package/hooks/hooks.json +133 -0
- package/hooks/lib/hook-tool-kind.sh +24 -0
- package/hooks/lib/log-rotation.sh +176 -0
- package/hooks/lib/shell-argv.pl +576 -0
- package/hooks/memory-backstop.sh +1114 -0
- package/hooks/operator-stage-approval.sh +391 -0
- package/hooks/stop-hook.sh +367 -0
- package/hooks/unkept-promise-hook.sh +383 -0
- package/hooks/version-banner.sh +127 -0
- package/hooks/welcome-hook.sh +50 -0
- package/install.js +29 -0
- package/lib/agent-registry.ts +154 -0
- package/lib/c4-canonical-cli.mjs +34 -0
- package/lib/c4-canonical.mjs +46 -0
- package/lib/c4-from-components.ts +554 -0
- package/lib/go-routing.ts +81 -0
- package/lib/grok-inspect-contract.ts +311 -0
- package/lib/harness-model-map.ts +101 -0
- package/lib/harness-parity.ts +726 -0
- package/lib/harness-tool-map.ts +180 -0
- package/lib/harness.ts +716 -0
- package/lib/heartbeat-live-reconcile.ts +1153 -0
- package/lib/heartbeat-manifest.ts +361 -0
- package/lib/kb-coverage.ts +252 -0
- package/lib/pr-merge-poll.ts +103 -0
- package/lib/workflow-fidelity.ts +388 -0
- package/opencode/INSTRUCTIONS.md +171 -0
- package/opencode/agents/engineering/cto.md +10 -0
- package/opencode/agents/engineering/design/ddd-architect.md +10 -0
- package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
- package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
- package/opencode/agents/engineering/infra/infra-security.md +10 -0
- package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
- package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
- package/opencode/agents/engineering/prompt-engineer.md +10 -0
- package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
- package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
- package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
- package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
- package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
- package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
- package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
- package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
- package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
- package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
- package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
- package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
- package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
- package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
- package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
- package/opencode/agents/engineering/review/performance-oracle.md +10 -0
- package/opencode/agents/engineering/review/security-sentinel.md +10 -0
- package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
- package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
- package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
- package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
- package/opencode/agents/finance/budget-analyst.md +10 -0
- package/opencode/agents/finance/cfo.md +10 -0
- package/opencode/agents/finance/financial-reporter.md +10 -0
- package/opencode/agents/finance/revenue-analyst.md +10 -0
- package/opencode/agents/legal/clo.md +10 -0
- package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
- package/opencode/agents/legal/legal-document-generator.md +10 -0
- package/opencode/agents/marketing/analytics-analyst.md +10 -0
- package/opencode/agents/marketing/brand-architect.md +10 -0
- package/opencode/agents/marketing/cmo.md +10 -0
- package/opencode/agents/marketing/conversion-optimizer.md +10 -0
- package/opencode/agents/marketing/copywriter.md +10 -0
- package/opencode/agents/marketing/fact-checker.md +10 -0
- package/opencode/agents/marketing/growth-strategist.md +10 -0
- package/opencode/agents/marketing/paid-media-strategist.md +10 -0
- package/opencode/agents/marketing/pricing-strategist.md +10 -0
- package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
- package/opencode/agents/marketing/retention-strategist.md +10 -0
- package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
- package/opencode/agents/operations/coo.md +10 -0
- package/opencode/agents/operations/ops-advisor.md +10 -0
- package/opencode/agents/operations/ops-provisioner.md +10 -0
- package/opencode/agents/operations/ops-research.md +10 -0
- package/opencode/agents/operations/service-automator.md +10 -0
- package/opencode/agents/product/business-validator.md +10 -0
- package/opencode/agents/product/competitive-intelligence.md +10 -0
- package/opencode/agents/product/cpo.md +10 -0
- package/opencode/agents/product/design/ux-design-lead.md +10 -0
- package/opencode/agents/product/spec-flow-analyzer.md +10 -0
- package/opencode/agents/sales/cro.md +10 -0
- package/opencode/agents/sales/deal-architect.md +10 -0
- package/opencode/agents/sales/outbound-strategist.md +10 -0
- package/opencode/agents/sales/pipeline-analyst.md +10 -0
- package/opencode/agents/support/cco.md +10 -0
- package/opencode/agents/support/community-manager.md +10 -0
- package/opencode/agents/support/ticket-triage.md +10 -0
- package/opencode/commands/go.md +10 -0
- package/opencode/commands/help.md +10 -0
- package/opencode/commands/sync.md +10 -0
- package/opencode/commands/version.md +11 -0
- package/opencode/opencode.json +8 -0
- package/opencode/plugin.ts +818 -0
- package/package.json +36 -4
- package/scripts/admin-merge-ready.sh +545 -0
- package/scripts/admin-merge-ready.test.sh +753 -0
- package/scripts/affected-tests.sh +908 -0
- package/scripts/affected-tests.test.sh +1797 -0
- package/scripts/alpha-metrics.sh +78 -0
- package/scripts/alpha-metrics.test.sh +115 -0
- package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
- package/scripts/audit-flag-flip.sh +107 -0
- package/scripts/check-red-on-main.sh +316 -0
- package/scripts/ci-head-verdict.sh +411 -0
- package/scripts/cloud-detect.sh +193 -0
- package/scripts/delegation-verify.sh +657 -0
- package/scripts/delegation-verify.test.sh +1424 -0
- package/scripts/deploy-arm.sh +475 -0
- package/scripts/domain-model-drift.sh +419 -0
- package/scripts/emit-decision.sh +144 -0
- package/scripts/emit-decision.test.sh +144 -0
- package/scripts/ext-filter-divergence-corpus.txt +15 -0
- package/scripts/ext-filter-divergence.sh +189 -0
- package/scripts/generate-c4-from-components.ts +442 -0
- package/scripts/grok-fidelity-gate.sh +68 -0
- package/scripts/grok-pre-push-gate.sh +232 -0
- package/scripts/harness-discovery-smoke.ts +906 -0
- package/scripts/harness-parity-census.ts +92 -0
- package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
- package/scripts/install-opencode-adapter.ts +509 -0
- package/scripts/lib/affected-tests/common.sh +365 -0
- package/scripts/lib/affected-tests/go.sh +157 -0
- package/scripts/lib/affected-tests/js.sh +188 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
- package/scripts/lib/affected-tests/python.sh +185 -0
- package/scripts/lib/context-sanitize.sh +49 -0
- package/scripts/lib/domain-model-lib.sh +180 -0
- package/scripts/lib/ext-filter-grammar.tsv +34 -0
- package/scripts/lib/operator-script.sh +1236 -0
- package/scripts/lib/proc.sh +580 -0
- package/scripts/lib/session-state.sh +748 -0
- package/scripts/lib/tmp-classify.sh +843 -0
- package/scripts/lib/wrapper-boundary.sh +47 -0
- package/scripts/lint-ratchet.py +1547 -0
- package/scripts/monitor-pr-checks.sh +441 -0
- package/scripts/pipeline-tally.sh +756 -0
- package/scripts/pipeline-tally.test.sh +740 -0
- package/scripts/plugin-version.sh +528 -0
- package/scripts/precommit-guard.sh +115 -0
- package/scripts/precommit-guard.test.sh +254 -0
- package/scripts/reconcile-live-heartbeats.ts +705 -0
- package/scripts/render-c4-model.sh +217 -0
- package/scripts/resolve-git-root.sh +55 -0
- package/scripts/resolve-regenerable-conflicts.sh +363 -0
- package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
- package/scripts/ruff-requirements.txt +22 -0
- package/scripts/run-in-pid-namespace.sh +98 -0
- package/scripts/run-in-pid-namespace.test.sh +290 -0
- package/scripts/scan-ancestor-signal-helpers.py +556 -0
- package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
- package/scripts/setup-ralph-loop.sh +238 -0
- package/scripts/sync-cursor-name-map.ts +316 -0
- package/scripts/sync-grok-agent-compat.ts +188 -0
- package/scripts/sync-npm-package.ts +165 -0
- package/scripts/sync-opencode-adapter.ts +498 -0
- package/scripts/sync-pr-behind.sh +647 -0
- package/scripts/sync-pr-behind.test.sh +252 -0
- package/scripts/taste-profile-update.sh +180 -0
- package/scripts/taste-profile-update.test.sh +167 -0
- package/scripts/write-budget-marker.sh +23 -0
- package/scripts/write-kb-coverage.ts +113 -0
- package/skills/admin-ip-refresh/SKILL.md +81 -0
- package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
- package/skills/agent-browser/SKILL.md +698 -0
- package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
- package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
- package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
- package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
- package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
- package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
- package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
- package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
- package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
- package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
- package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
- package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
- package/skills/agent-native-architecture/SKILL.md +479 -0
- package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
- package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
- package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
- package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
- package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
- package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
- package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
- package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
- package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
- package/skills/agent-native-architecture/references/product-implications.md +478 -0
- package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
- package/skills/agent-native-architecture/references/self-modification.md +294 -0
- package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
- package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
- package/skills/agent-native-audit/SKILL.md +300 -0
- package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
- package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
- package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
- package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/skills/architecture/SKILL.md +436 -0
- package/skills/architecture/references/adr-template.md +154 -0
- package/skills/architecture/references/likec4-reference.md +136 -0
- package/skills/architecture/references/nfr-reference.md +126 -0
- package/skills/archive-kb/SKILL.md +86 -0
- package/skills/archive-kb/scripts/archive-kb.sh +300 -0
- package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
- package/skills/atdd-developer/SKILL.md +65 -0
- package/skills/brainstorm/SKILL.md +701 -0
- package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
- package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
- package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
- package/skills/brainstorm/references/ui-surface-terms.md +42 -0
- package/skills/brainstorm-techniques/SKILL.md +231 -0
- package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
- package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
- package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
- package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
- package/skills/campaign-calendar/SKILL.md +145 -0
- package/skills/cf-token-scope/SKILL.md +164 -0
- package/skills/cf-token-scope/references/widen-playbook.md +114 -0
- package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
- package/skills/changelog/SKILL.md +142 -0
- package/skills/code-to-prd/SKILL.md +66 -0
- package/skills/code-to-prd/references/banner-template.md +12 -0
- package/skills/code-to-prd/references/prd-template.md +41 -0
- package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
- package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
- package/skills/code-to-prd/test/fixture/.env.example +7 -0
- package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
- package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
- package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
- package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
- package/skills/code-to-prd/test/fixture/package.json +6 -0
- package/skills/cohort-status/SKILL.md +62 -0
- package/skills/community/SKILL.md +210 -0
- package/skills/community/scripts/bsky-community.sh +644 -0
- package/skills/community/scripts/bsky-setup.sh +631 -0
- package/skills/community/scripts/community-router.sh +93 -0
- package/skills/community/scripts/discord-community.sh +487 -0
- package/skills/community/scripts/discord-setup.sh +784 -0
- package/skills/community/scripts/github-community.sh +706 -0
- package/skills/community/scripts/hn-community.sh +233 -0
- package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
- package/skills/community/scripts/linkedin-community.sh +750 -0
- package/skills/community/scripts/linkedin-setup.sh +758 -0
- package/skills/community/scripts/x-community.sh +890 -0
- package/skills/community/scripts/x-setup.sh +713 -0
- package/skills/community/test/community-argv.test.sh +2922 -0
- package/skills/community/test/github-community.test.sh +981 -0
- package/skills/competitive-analysis/SKILL.md +69 -0
- package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
- package/skills/compound/SKILL.md +673 -0
- package/skills/compound/scripts/token-efficiency-report.sh +334 -0
- package/skills/compound/test/phase-16.test.sh +379 -0
- package/skills/compound-capture/SKILL.md +791 -0
- package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
- package/skills/compound-capture/assets/resolution-template.md +116 -0
- package/skills/compound-capture/references/yaml-schema.md +67 -0
- package/skills/compound-capture/schema.yaml +181 -0
- package/skills/constraint-scaffold/SKILL.md +175 -0
- package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
- package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
- package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
- package/skills/constraint-scaffold/references/shared-runner.template +135 -0
- package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
- package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
- package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
- package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
- package/skills/constraint-scaffold/test/generator.test.sh +93 -0
- package/skills/constraint-scaffold/test/parity.test.sh +285 -0
- package/skills/content-writer/SKILL.md +242 -0
- package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
- package/skills/cron-delete/SKILL.md +56 -0
- package/skills/cron-list/SKILL.md +54 -0
- package/skills/deepen-plan/SKILL.md +937 -0
- package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
- package/skills/deploy/SKILL.md +115 -0
- package/skills/deploy/references/hetzner-setup.md +67 -0
- package/skills/deploy/scripts/deploy.sh +37 -0
- package/skills/deploy-docs/SKILL.md +95 -0
- package/skills/dhh-rails-style/SKILL.md +203 -0
- package/skills/dhh-rails-style/references/architecture.md +717 -0
- package/skills/dhh-rails-style/references/controllers.md +323 -0
- package/skills/dhh-rails-style/references/frontend.md +556 -0
- package/skills/dhh-rails-style/references/gems.md +316 -0
- package/skills/dhh-rails-style/references/models.md +388 -0
- package/skills/dhh-rails-style/references/testing.md +354 -0
- package/skills/discord-content/SKILL.md +126 -0
- package/skills/docs-site/SKILL.md +214 -0
- package/skills/drain-labeled-backlog/SKILL.md +203 -0
- package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
- package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
- package/skills/drain-prs/SKILL.md +143 -0
- package/skills/drain-prs/scripts/triage-prs.sh +192 -0
- package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
- package/skills/dspy-ruby/SKILL.md +634 -0
- package/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/skills/dspy-ruby/references/core-concepts.md +268 -0
- package/skills/dspy-ruby/references/optimization.md +623 -0
- package/skills/dspy-ruby/references/providers.md +335 -0
- package/skills/eval-harness/README.md +194 -0
- package/skills/eval-harness/SKILL.md +157 -0
- package/skills/eval-harness/enums/go-routes.json +13 -0
- package/skills/eval-harness/enums/incident-threshold.json +5 -0
- package/skills/eval-harness/enums/lane.json +5 -0
- package/skills/eval-harness/enums/tool-selection.json +20 -0
- package/skills/eval-harness/enums/triage-levels.json +5 -0
- package/skills/eval-harness/gated-skills.json +34 -0
- package/skills/eval-harness/models.generated.json +5 -0
- package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
- package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
- package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
- package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
- package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
- package/skills/eval-harness/prompts/go-baseline.txt +6 -0
- package/skills/eval-harness/prompts/go-skill.txt +21 -0
- package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
- package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
- package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
- package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
- package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
- package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
- package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
- package/skills/eval-harness/prompts/triage-skill.txt +10 -0
- package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
- package/skills/eval-harness/scripts/extract-block.cjs +87 -0
- package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
- package/skills/eval-harness/scripts/gen-models.sh +41 -0
- package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
- package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
- package/skills/eval-harness/scripts/parse-label.cjs +93 -0
- package/skills/eval-harness/scripts/verdict.cjs +132 -0
- package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
- package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
- package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
- package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
- package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
- package/skills/eval-harness/test/eval-gate.test.sh +132 -0
- package/skills/eval-harness/test/extract-block.test.sh +87 -0
- package/skills/eval-harness/test/gate-classification.test.sh +77 -0
- package/skills/eval-harness/test/gen-models.test.sh +75 -0
- package/skills/eval-harness/test/measure-classification.test.sh +74 -0
- package/skills/eval-harness/test/parse-label.test.sh +129 -0
- package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
- package/skills/eval-harness/test/verdict.test.sh +123 -0
- package/skills/every-style-editor/SKILL.md +142 -0
- package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/skills/feature-tweet/SKILL.md +199 -0
- package/skills/feature-video/SKILL.md +412 -0
- package/skills/feature-video/scripts/check_deps.sh +281 -0
- package/skills/file-todos/SKILL.md +267 -0
- package/skills/file-todos/assets/todo-template.md +166 -0
- package/skills/fix-issue/SKILL.md +278 -0
- package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
- package/skills/flag-bootstrap/SETUP.md +142 -0
- package/skills/flag-create/SKILL.md +145 -0
- package/skills/flag-create/scripts/create.sh +273 -0
- package/skills/flag-delete/SKILL.md +167 -0
- package/skills/flag-delete/scripts/delete.sh +288 -0
- package/skills/flag-list/SKILL.md +99 -0
- package/skills/flag-list/scripts/list.sh +275 -0
- package/skills/flag-set-role/SKILL.md +229 -0
- package/skills/flag-set-role/scripts/flip.sh +774 -0
- package/skills/frontend-anti-slop/SKILL.md +106 -0
- package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
- package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
- package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
- package/skills/frontend-design/SKILL.md +77 -0
- package/skills/gdpr-gate/NOTICE +136 -0
- package/skills/gdpr-gate/SKILL.md +341 -0
- package/skills/gdpr-gate/references/fields.md +160 -0
- package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
- package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
- package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
- package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
- package/skills/gdpr-gate/references/layers/frontend.md +249 -0
- package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
- package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
- package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
- package/skills/gdpr-gate/references/legal-consent.md +140 -0
- package/skills/gdpr-gate/references/non-negotiables.md +60 -0
- package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
- package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
- package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
- package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
- package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
- package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
- package/skills/gemini-imagegen/SKILL.md +283 -0
- package/skills/gemini-imagegen/requirements.txt +5 -0
- package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
- package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
- package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
- package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
- package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
- package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
- package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
- package/skills/git-worktree/SKILL.md +534 -0
- package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
- package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
- package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
- package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
- package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
- package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
- package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
- package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
- package/skills/go/SKILL.md +54 -0
- package/skills/growth/SKILL.md +164 -0
- package/skills/harvest-debt/SKILL.md +78 -0
- package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
- package/skills/heal-skill/SKILL.md +173 -0
- package/skills/help/SKILL.md +46 -0
- package/skills/incident/NOTICE +39 -0
- package/skills/incident/SKILL.md +395 -0
- package/skills/incident/scripts/dry-run.sh +448 -0
- package/skills/incident/scripts/redact-engine.py +412 -0
- package/skills/incident/scripts/redact-sentinel.sh +44 -0
- package/skills/incident/templates/pir.md +139 -0
- package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
- package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
- package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
- package/skills/incident/test/fixtures/positive-corpus.md +87 -0
- package/skills/incident/test/redact-sentinel.test.sh +1420 -0
- package/skills/invoice/SKILL.md +206 -0
- package/skills/kb-glossary/SKILL.md +71 -0
- package/skills/kb-glossary/references/glossary-format.md +111 -0
- package/skills/kb-glossary/references/rejected-request-register.md +115 -0
- package/skills/kb-search/SKILL.md +276 -0
- package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
- package/skills/legal-audit/SKILL.md +161 -0
- package/skills/legal-generate/NOTICE +242 -0
- package/skills/legal-generate/SKILL.md +244 -0
- package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
- package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
- package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
- package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
- package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
- package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
- package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
- package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
- package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
- package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
- package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
- package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
- package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
- package/skills/linear-fetch/SKILL.md +247 -0
- package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
- package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
- package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
- package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
- package/skills/linear-fetch/test/parity.test.sh +112 -0
- package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
- package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
- package/skills/merge-pr/SKILL.md +670 -0
- package/skills/model-launch-review/SKILL.md +255 -0
- package/skills/model-launch-review/scripts/audit-models.sh +501 -0
- package/skills/one-shot/SKILL.md +316 -0
- package/skills/one-shot/references/token-discipline.md +74 -0
- package/skills/operator-bootstrap/SKILL.md +412 -0
- package/skills/operator-bootstrap/template.sh +439 -0
- package/skills/operator-digest/SKILL.md +287 -0
- package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
- package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
- package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
- package/skills/operator-rephrase/SKILL.md +98 -0
- package/skills/pencil-setup/SKILL.md +201 -0
- package/skills/pencil-setup/scripts/check_deps.sh +556 -0
- package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
- package/skills/pencil-setup/scripts/package-lock.json +1172 -0
- package/skills/pencil-setup/scripts/package.json +14 -0
- package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
- package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
- package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
- package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
- package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
- package/skills/plan/SKILL.md +1140 -0
- package/skills/plan/references/differential-migration-testing.md +185 -0
- package/skills/plan/references/plan-community-discovery.md +42 -0
- package/skills/plan/references/plan-founder-check.md +137 -0
- package/skills/plan/references/plan-functional-overlap.md +25 -0
- package/skills/plan/references/plan-host-fanout.md +51 -0
- package/skills/plan/references/plan-issue-templates.md +779 -0
- package/skills/plan/references/plan-network-outage-checklist.md +131 -0
- package/skills/plan/references/plan-scope-check.md +126 -0
- package/skills/plan/references/plan-sharp-edges.md +331 -0
- package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
- package/skills/plan/references/webapp-cwv-observability.md +143 -0
- package/skills/plan-review/SKILL.md +91 -0
- package/skills/plan-review/lib/named-panel.mjs +58 -0
- package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
- package/skills/postmerge/SKILL.md +642 -0
- package/skills/postmerge/references/deploy-status-debugging.md +141 -0
- package/skills/postmerge/references/graceful-degradation.md +20 -0
- package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
- package/skills/preflight/SKILL.md +1553 -0
- package/skills/preflight/references/check-13-founder-check.md +286 -0
- package/skills/preflight/scripts/founder-check.py +1640 -0
- package/skills/preflight/scripts/parse-form-a.awk +107 -0
- package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
- package/skills/product-roadmap/SKILL.md +357 -0
- package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
- package/skills/provision-cloudflare/SKILL.md +67 -0
- package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
- package/skills/provision-doppler/SKILL.md +58 -0
- package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
- package/skills/provision-github/SKILL.md +59 -0
- package/skills/provision-github/scripts/provision-github.sh +300 -0
- package/skills/provision-hetzner/SKILL.md +76 -0
- package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
- package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
- package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
- package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
- package/skills/qa/SKILL.md +263 -0
- package/skills/qa/references/browser-preflight.md +22 -0
- package/skills/questionnaire-generate/SKILL.md +328 -0
- package/skills/questionnaire-generate/references/questionnaire.template +60 -0
- package/skills/rclone/SKILL.md +162 -0
- package/skills/rclone/scripts/check_setup.sh +63 -0
- package/skills/release-announce/SKILL.md +65 -0
- package/skills/release-docs/SKILL.md +92 -0
- package/skills/reproduce-bug/SKILL.md +358 -0
- package/skills/resolve-debt/SKILL.md +85 -0
- package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
- package/skills/resolve-parallel/SKILL.md +45 -0
- package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
- package/skills/resolve-pr-parallel/SKILL.md +68 -0
- package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
- package/skills/resolve-todo-parallel/SKILL.md +57 -0
- package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
- package/skills/review/SKILL.md +1620 -0
- package/skills/review/references/anti-slop-scanner-hook.md +40 -0
- package/skills/review/references/defect-classes-migrations.md +9 -0
- package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
- package/skills/review/references/review-e2e-testing.md +116 -0
- package/skills/review/references/review-evidence-comment.md +36 -0
- package/skills/review/references/review-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
- package/skills/review/references/semgrep-custom-rules.yaml +174 -0
- package/skills/review/references/ultrathink-phases.md +98 -0
- package/skills/review/references/wfs.md +6 -0
- package/skills/review/scripts/emit-review-trailer.sh +499 -0
- package/skills/review/scripts/ensure-semgrep.sh +99 -0
- package/skills/review/scripts/fix-round-seats.sh +197 -0
- package/skills/review/test/emit-review-trailer.test.sh +387 -0
- package/skills/review/test/ensure-semgrep.test.sh +111 -0
- package/skills/review/test/fix-round-seats.test.sh +237 -0
- package/skills/review/workflows/README.md +147 -0
- package/skills/review/workflows/review.workflow.js +667 -0
- package/skills/schedule/SKILL.md +796 -0
- package/skills/seo-aeo/SKILL.md +139 -0
- package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
- package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
- package/skills/ship/SKILL.md +3023 -0
- package/skills/ship/references/adr-ordinal-collision.md +47 -0
- package/skills/ship/references/affected-gate-contract.md +115 -0
- package/skills/ship/references/ci-run-diagnosis.md +29 -0
- package/skills/ship/references/ci-workflow-authoring.md +68 -0
- package/skills/ship/references/cleanup-merged-archival.md +39 -0
- package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
- package/skills/ship/references/cto-ruling-armed-push.md +5 -0
- package/skills/ship/references/decision-challenges-render.md +71 -0
- package/skills/ship/references/delegated-act-authoring-form.md +7 -0
- package/skills/ship/references/detached-battery-verdict.md +13 -0
- package/skills/ship/references/epic-close-sweep.md +62 -0
- package/skills/ship/references/feature-tweet-draft.md +42 -0
- package/skills/ship/references/followthrough-issue-body-template.md +47 -0
- package/skills/ship/references/followthrough-stub-template.sh +92 -0
- package/skills/ship/references/merge-queue-dequeue.md +39 -0
- package/skills/ship/references/operator-holds.md +179 -0
- package/skills/ship/references/pipeline-tally-render.md +43 -0
- package/skills/ship/references/pr-overlap.md +153 -0
- package/skills/ship/references/queue-mode.md +7 -0
- package/skills/ship/references/ready-run-wait.md +36 -0
- package/skills/ship/references/red-on-main-quarantine.md +110 -0
- package/skills/ship/references/register-update-pr-pattern.md +88 -0
- package/skills/ship/references/settle-then-admin-merge.md +130 -0
- package/skills/ship/references/supabase-column-probe.md +13 -0
- package/skills/ship/scripts/auto-close-scan.sh +76 -0
- package/skills/ship/scripts/battery-owed.sh +403 -0
- package/skills/ship/scripts/hold-gate-eval.sh +410 -0
- package/skills/ship/scripts/net-issue-flow.sh +665 -0
- package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
- package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
- package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
- package/skills/skill-creator/SKILL.md +299 -0
- package/skills/skill-creator/references/api-security.md +248 -0
- package/skills/skill-creator/references/authoring-levers.md +110 -0
- package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
- package/skills/skill-creator/references/best-practices.md +431 -0
- package/skills/skill-creator/references/common-patterns.md +639 -0
- package/skills/skill-creator/references/core-principles.md +193 -0
- package/skills/skill-creator/references/executable-code.md +188 -0
- package/skills/skill-creator/references/iteration-and-testing.md +509 -0
- package/skills/skill-creator/references/official-spec.md +193 -0
- package/skills/skill-creator/references/recommended-structure.md +180 -0
- package/skills/skill-creator/references/skill-structure.md +396 -0
- package/skills/skill-creator/references/using-scripts.md +122 -0
- package/skills/skill-creator/references/using-templates.md +120 -0
- package/skills/skill-creator/references/workflows-and-validation.md +541 -0
- package/skills/skill-creator/scripts/init_skill.py +322 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +64 -0
- package/skills/skill-creator/templates/router-skill.md +73 -0
- package/skills/skill-creator/templates/simple-skill.md +34 -0
- package/skills/skill-creator/workflows/add-reference.md +96 -0
- package/skills/skill-creator/workflows/add-script.md +104 -0
- package/skills/skill-creator/workflows/add-template.md +83 -0
- package/skills/skill-creator/workflows/add-workflow.md +128 -0
- package/skills/skill-creator/workflows/audit-skill.md +159 -0
- package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
- package/skills/skill-creator/workflows/create-new-skill.md +207 -0
- package/skills/skill-creator/workflows/get-guidance.md +134 -0
- package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
- package/skills/skill-creator/workflows/verify-skill.md +226 -0
- package/skills/skill-security-scan/SKILL.md +145 -0
- package/skills/skill-security-scan/references/disclaimer.md +21 -0
- package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
- package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
- package/skills/skill-security-scan/references/override-mechanism.md +171 -0
- package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
- package/skills/skill-security-scan/references/regex-patterns.md +136 -0
- package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
- package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
- package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
- package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
- package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
- package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
- package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
- package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
- package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
- package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
- package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
- package/skills/skill-security-scan/scripts/lib.sh +197 -0
- package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
- package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
- package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
- package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
- package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
- package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
- package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
- package/skills/social-distribute/SKILL.md +485 -0
- package/skills/spec-templates/SKILL.md +274 -0
- package/skills/sync/SKILL.md +49 -0
- package/skills/test-browser/SKILL.md +464 -0
- package/skills/test-browser/references/feature-map.md +161 -0
- package/skills/test-fix-loop/SKILL.md +159 -0
- package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
- package/skills/triage/SKILL.md +402 -0
- package/skills/trigger-cron/SKILL.md +136 -0
- package/skills/trigger-cron/scripts/trigger.sh +198 -0
- package/skills/user-set-role/SKILL.md +147 -0
- package/skills/user-set-role/scripts/set-role.sh +218 -0
- package/skills/user-story-writer/SKILL.md +77 -0
- package/skills/ux-audit/SKILL.md +251 -0
- package/skills/ux-audit/references/finding.schema.json +62 -0
- package/skills/ux-audit/references/route-list.yaml +95 -0
- package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
- package/skills/ux-audit/scripts/bot-signin.ts +135 -0
- package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
- package/skills/verification-scaffold/SKILL.md +73 -0
- package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
- package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
- package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
- package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
- package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
- package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
- package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
- package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
- package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
- package/skills/verification-scaffold/references/guards.tsv +21 -0
- package/skills/verification-scaffold/references/verification-readme.template +55 -0
- package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
- package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
- package/skills/verification-scaffold/test/generator.test.sh +319 -0
- package/skills/verification-scaffold/test/manifest.test.sh +144 -0
- package/skills/work/SKILL.md +1480 -0
- package/skills/work/references/end-of-work-resume.md +7 -0
- package/skills/work/references/hook-test-main-cwd.md +24 -0
- package/skills/work/references/key-principles.md +83 -0
- package/skills/work/references/property-testing.md +122 -0
- package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
- package/skills/work/references/work-agent-teams.md +97 -0
- package/skills/work/references/work-lifecycle-parallel.md +147 -0
- package/skills/work/references/work-lockfile-bumps.md +97 -0
- package/skills/work/references/work-scratch-sandboxes.md +212 -0
- package/skills/work/references/work-subagent-fanout.md +61 -0
- package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
- package/skills/xcode-test/SKILL.md +360 -0
|
@@ -0,0 +1,1420 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Test harness for plugins/soleur/skills/incident/scripts/redact-sentinel.sh
|
|
3
|
+
#
|
|
4
|
+
# Pattern mirrors plugins/soleur/skills/git-worktree/test/lease-protects-active.test.sh:
|
|
5
|
+
# - set -uo pipefail (NOT -e — single test failure must not abort the suite)
|
|
6
|
+
# - PASS/FAIL counter
|
|
7
|
+
# - trap-based cleanup
|
|
8
|
+
#
|
|
9
|
+
# Tests 1-4 are the ORIGINAL contract (redact-sentinel #2725 FR3).
|
|
10
|
+
# Tests 5-12 are the redaction-hardening suite (#5987): NFKC + zero-width strip
|
|
11
|
+
# before matching, ReDoS-safe fail-closed input cap, fail-closed on no-python3,
|
|
12
|
+
# ERE->re golden parity, and the legal-generate gate.
|
|
13
|
+
#
|
|
14
|
+
# All confusable / oversize / invalid-byte inputs are generated AT RUNTIME via
|
|
15
|
+
# `python3 -c` with chr(0xXXXX) — NEVER committed as literal invisibles
|
|
16
|
+
# (`cq-regex-unicode-separators-escape-only`; AC6 enforces this). All tokens are
|
|
17
|
+
# synthesized from format specs (`cq-test-fixtures-synthesized-only`).
|
|
18
|
+
set -uo pipefail
|
|
19
|
+
|
|
20
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
21
|
+
SKILL_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
|
|
22
|
+
REPO_ROOT="$(cd "${SKILL_DIR}/../../../.." && pwd)"
|
|
23
|
+
|
|
24
|
+
SENTINEL="${SKILL_DIR}/scripts/redact-sentinel.sh"
|
|
25
|
+
POSITIVE_CORPUS="${SCRIPT_DIR}/fixtures/positive-corpus.md"
|
|
26
|
+
NEGATIVE_BASELINE="${REPO_ROOT}/knowledge-base/engineering/operations/post-mortems/dashboard-error-postmortem.md"
|
|
27
|
+
LEGAL_SKILL="${REPO_ROOT}/plugins/soleur/skills/legal-generate/SKILL.md"
|
|
28
|
+
|
|
29
|
+
PASS=0
|
|
30
|
+
FAIL=0
|
|
31
|
+
TMP_DIR="$(mktemp -d)"
|
|
32
|
+
trap 'rm -rf "${TMP_DIR}"' EXIT
|
|
33
|
+
|
|
34
|
+
assert_exit() {
|
|
35
|
+
local label="$1" expected="$2" actual="$3"
|
|
36
|
+
if [[ "${expected}" == "${actual}" ]]; then
|
|
37
|
+
echo "PASS: ${label} (exit=${actual})"
|
|
38
|
+
PASS=$((PASS + 1))
|
|
39
|
+
else
|
|
40
|
+
echo "FAIL: ${label} (expected exit=${expected}, got ${actual})"
|
|
41
|
+
FAIL=$((FAIL + 1))
|
|
42
|
+
fi
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
assert_grep() {
|
|
46
|
+
local label="$1" pattern="$2" file="$3"
|
|
47
|
+
if grep -qE "${pattern}" "${file}"; then
|
|
48
|
+
echo "PASS: ${label} (pattern matched in ${file##*/})"
|
|
49
|
+
PASS=$((PASS + 1))
|
|
50
|
+
else
|
|
51
|
+
echo "FAIL: ${label} (pattern '${pattern}' not found in ${file##*/})"
|
|
52
|
+
FAIL=$((FAIL + 1))
|
|
53
|
+
fi
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Test 1 — negative-baseline (clean hand-redacted PIR exits 0)
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
out="${TMP_DIR}/test1.out"
|
|
60
|
+
bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >"${out}" 2>&1
|
|
61
|
+
assert_exit "Test 1: negative-baseline exits 0 on hand-redacted PIR" 0 $?
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Test 2 — positive-corpus: every regex class triggers >=1
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
out="${TMP_DIR}/test2.out"
|
|
67
|
+
bash "${SENTINEL}" "${POSITIVE_CORPUS}" >"${out}" 2>&1
|
|
68
|
+
rc=$?
|
|
69
|
+
assert_exit "Test 2: positive-corpus exits non-zero" 1 "${rc}"
|
|
70
|
+
|
|
71
|
+
for class in JWT email UUID stripe_key stripe_whsec stripe_acct stripe_cust_pi_seti_sub_in IPv4 env_var \
|
|
72
|
+
github_token anthropic_key openai_key supabase_pat pem_private_key doppler_token slack_token \
|
|
73
|
+
cloudflare_token; do
|
|
74
|
+
assert_grep "Test 2.${class}: pattern '${class}' present" "matched pattern ${class}" "${out}"
|
|
75
|
+
done
|
|
76
|
+
|
|
77
|
+
# ---------------------------------------------------------------------------
|
|
78
|
+
# Test 3 — invalid arg -> exit 2
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
out="${TMP_DIR}/test3.out"
|
|
81
|
+
bash "${SENTINEL}" /nonexistent/path/to/file.md >"${out}" 2>&1
|
|
82
|
+
assert_exit "Test 3: nonexistent file exits 2" 2 $?
|
|
83
|
+
|
|
84
|
+
bash "${SENTINEL}" >"${out}" 2>&1
|
|
85
|
+
assert_exit "Test 3: missing arg exits 2" 2 $?
|
|
86
|
+
|
|
87
|
+
# ---------------------------------------------------------------------------
|
|
88
|
+
# Test 4 — output format (TIGHTENED, #5987): capped reveal, never a full token.
|
|
89
|
+
# Was `.{8}\*\*\*.{8}` (leaked ~50% of a fixed-prefix key). Now <=4 + *** + <=4.
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
out="${TMP_DIR}/test4.out"
|
|
92
|
+
bash "${SENTINEL}" "${POSITIVE_CORPUS}" >"${out}" 2>&1 || true
|
|
93
|
+
assert_grep "Test 4: capped output format 'at offset N: <=4-prefix***<=4-suffix matched pattern X'" \
|
|
94
|
+
'at offset [0-9]+: .{0,4}\*\*\*(.{0,4})? matched pattern [A-Za-z_]+' "${out}"
|
|
95
|
+
|
|
96
|
+
# Test 4b — NO full-token reveal: no finding line may reveal >4 prefix chars.
|
|
97
|
+
if grep -qE 'at offset [0-9]+: [^*]{5,}\*\*\*' "${out}"; then
|
|
98
|
+
echo "FAIL: Test 4b: a finding revealed >4 prefix chars (entropy leak regression)"
|
|
99
|
+
FAIL=$((FAIL + 1))
|
|
100
|
+
else
|
|
101
|
+
echo "PASS: Test 4b: no finding reveals >4 prefix chars (meta-redaction tightened)"
|
|
102
|
+
PASS=$((PASS + 1))
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# ===========================================================================
|
|
106
|
+
# #5987 — redaction hardening suite (Tests 5-12)
|
|
107
|
+
# ===========================================================================
|
|
108
|
+
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
# Test 5 — compatibility-confusable + invisible-splitter evasion (AC1).
|
|
111
|
+
# A JWT split by ZWSP (U+200B), soft-hyphen (U+00AD), and line-sep (U+2028),
|
|
112
|
+
# plus a fullwidth Stripe key. (a) OLD raw-byte engine MISSES; (b) new engine CATCHES.
|
|
113
|
+
# Invisibles built via chr(0xXXXX) so no literal invisibles land in this file.
|
|
114
|
+
# ---------------------------------------------------------------------------
|
|
115
|
+
t5_file="${TMP_DIR}/t5.txt"
|
|
116
|
+
python3 -c "
|
|
117
|
+
import sys
|
|
118
|
+
def fw(s): # ASCII printable -> fullwidth (NFKC folds back)
|
|
119
|
+
return ''.join(chr(ord(c)+0xFEE0) if 0x21<=ord(c)<=0x7e else c for c in s)
|
|
120
|
+
jwt = 'eyJ'+'A'*12+chr(0x200b)+'.'+'B'*10+chr(0x00ad)+'BB'+'.'+'C'*10+chr(0x2028)+'CC'
|
|
121
|
+
stripe = fw('sk_live_0000000000000000')
|
|
122
|
+
sys.stdout.write(jwt+'\n'+stripe+'\n')
|
|
123
|
+
" > "${t5_file}"
|
|
124
|
+
|
|
125
|
+
# (a) OLD bash engine MISSES the evasive tokens. Baseline is a FROZEN copy of the pre-#5987
|
|
126
|
+
# grep scanner committed at fixtures/legacy-bash-scanner.sh — NOT `git show main:redact-sentinel.sh`,
|
|
127
|
+
# which becomes the new shim post-merge (references redact-engine.py absent from a temp dir -> exit 2,
|
|
128
|
+
# a merge-time time-bomb that would turn main red for the next contributor). The frozen copy is a
|
|
129
|
+
# self-contained pure-bash scanner and is stable across merges.
|
|
130
|
+
OLD_ENGINE="${SCRIPT_DIR}/fixtures/legacy-bash-scanner.sh"
|
|
131
|
+
bash "${OLD_ENGINE}" "${t5_file}" >/dev/null 2>&1
|
|
132
|
+
assert_exit "Test 5a: OLD raw-byte engine MISSES confusable/invisible-split tokens" 0 $?
|
|
133
|
+
|
|
134
|
+
# (b) new engine catches after strip + NFKC
|
|
135
|
+
t5_out="${TMP_DIR}/t5.out"
|
|
136
|
+
bash "${SENTINEL}" "${t5_file}" >"${t5_out}" 2>&1
|
|
137
|
+
assert_exit "Test 5b: engine catches confusable/invisible-split tokens" 1 $?
|
|
138
|
+
assert_grep "Test 5b.JWT: JWT class tripped after strip" "matched pattern JWT" "${t5_out}"
|
|
139
|
+
assert_grep "Test 5b.stripe: fullwidth Stripe key tripped after NFKC" "matched pattern stripe_key" "${t5_out}"
|
|
140
|
+
|
|
141
|
+
# Test 5c — category-based STRIP: invisible/control/format families NFKC leaves intact must all be
|
|
142
|
+
# stripped, not just a hand-picked list. One secret per invisible class; each must be CAUGHT (exit 1).
|
|
143
|
+
# NUL(Cc), DEL(Cc), variation-selector U+FE0F(Mn), Tags-block U+E0020(Cf), combining grapheme joiner
|
|
144
|
+
# U+034F(Mn). Splice is placed mid-token so a raw-byte matcher would miss it.
|
|
145
|
+
for probe in "NUL:0x00" "DEL:0x7f" "VS16:0xfe0f" "TAGSPACE:0xe0020" "CGJ:0x34f"; do
|
|
146
|
+
name="${probe%%:*}"; cp="${probe##*:}"
|
|
147
|
+
f="${TMP_DIR}/t5c-${name}.txt"
|
|
148
|
+
python3 -c "import sys; sys.stdout.write('sk_live_0000'+chr(${cp})+'000000000000\n')" > "${f}"
|
|
149
|
+
bash "${SENTINEL}" "${f}" >/dev/null 2>&1
|
|
150
|
+
assert_exit "Test 5c.${name}: invisible-splice (${cp}) stripped by category, secret caught" 1 $?
|
|
151
|
+
done
|
|
152
|
+
|
|
153
|
+
# Test 5d — second-strip necessity (U+FFA0 halfwidth Hangul filler): NFKC folds U+FFA0 -> U+1160,
|
|
154
|
+
# a strippable char NOT present in the raw input. Only the post-NFKC second strip catches it. This
|
|
155
|
+
# pins the double-strip so a future "simplification" removing it fails loudly.
|
|
156
|
+
t5d_file="${TMP_DIR}/t5d.txt"
|
|
157
|
+
python3 -c "import sys; sys.stdout.write('sk_live_0000'+chr(0xFFA0)+'000000000000\n')" > "${t5d_file}"
|
|
158
|
+
bash "${SENTINEL}" "${t5d_file}" >/dev/null 2>&1
|
|
159
|
+
assert_exit "Test 5d: U+FFA0->U+1160 NFKC fold caught by the second strip" 1 $?
|
|
160
|
+
|
|
161
|
+
# ---------------------------------------------------------------------------
|
|
162
|
+
# Test 6 — oversize -> synthetic HIGH (AC2). Cap lowered via env for speed.
|
|
163
|
+
# ---------------------------------------------------------------------------
|
|
164
|
+
t6_file="${TMP_DIR}/t6.txt"
|
|
165
|
+
head -c 2048 /dev/zero | tr '\0' 'a' > "${t6_file}" # 2048 bytes of 'a', no secret
|
|
166
|
+
t6_out="${TMP_DIR}/t6.out"
|
|
167
|
+
REDACT_MAX_INPUT_BYTES=1024 bash "${SENTINEL}" "${t6_file}" >"${t6_out}" 2>&1
|
|
168
|
+
assert_exit "Test 6: raw-oversize input exits 1 (synthetic HIGH)" 1 $?
|
|
169
|
+
assert_grep "Test 6: synthetic-HIGH marker present" "SYNTHETIC HIGH" "${t6_out}"
|
|
170
|
+
if grep -qE 'matched pattern' "${t6_out}"; then
|
|
171
|
+
echo "FAIL: Test 6: per-class matching ran on oversize input (should short-circuit)"
|
|
172
|
+
FAIL=$((FAIL + 1))
|
|
173
|
+
else
|
|
174
|
+
echo "PASS: Test 6: no per-class matching on oversize input (fail-fast)"
|
|
175
|
+
PASS=$((PASS + 1))
|
|
176
|
+
fi
|
|
177
|
+
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
# Test 6b — expansion bomb (AC2): raw < cap but NFKC-expanded > cap.
|
|
180
|
+
# U+FDFA NFKC-expands to 18 codepoints. Built via chr(0xFDFA).
|
|
181
|
+
# ---------------------------------------------------------------------------
|
|
182
|
+
t6b_file="${TMP_DIR}/t6b.txt"
|
|
183
|
+
python3 -c "import sys; sys.stdout.write(chr(0xFDFA)*40)" > "${t6b_file}" # raw ~120 bytes
|
|
184
|
+
t6b_raw=$(wc -c < "${t6b_file}")
|
|
185
|
+
t6b_out="${TMP_DIR}/t6b.out"
|
|
186
|
+
REDACT_MAX_INPUT_BYTES=200 bash "${SENTINEL}" "${t6b_file}" >"${t6b_out}" 2>&1
|
|
187
|
+
t6b_rc=$?
|
|
188
|
+
echo " (Test 6b: raw=${t6b_raw} bytes < cap=200; NFKC-expanded > cap)"
|
|
189
|
+
assert_exit "Test 6b: NFKC-expansion-oversize exits 1 (post-NFKC re-check)" 1 "${t6b_rc}"
|
|
190
|
+
assert_grep "Test 6b: synthetic-HIGH marker present" "SYNTHETIC HIGH" "${t6b_out}"
|
|
191
|
+
|
|
192
|
+
# ---------------------------------------------------------------------------
|
|
193
|
+
# Test 7 — invalid-UTF-8 splice (AC1b): a secret with an invalid byte spliced in
|
|
194
|
+
# (-> U+FFFD) is caught after the strip.
|
|
195
|
+
# ---------------------------------------------------------------------------
|
|
196
|
+
t7_file="${TMP_DIR}/t7.txt"
|
|
197
|
+
python3 -c "
|
|
198
|
+
import sys
|
|
199
|
+
sys.stdout.buffer.write(b'sk_live_0000' + b'\x80' + b'0000000000000000\n')
|
|
200
|
+
" > "${t7_file}"
|
|
201
|
+
t7_out="${TMP_DIR}/t7.out"
|
|
202
|
+
bash "${SENTINEL}" "${t7_file}" >"${t7_out}" 2>&1
|
|
203
|
+
assert_exit "Test 7: invalid-UTF-8-spliced secret caught after strip" 1 $?
|
|
204
|
+
assert_grep "Test 7: stripe_key tripped post-strip" "matched pattern stripe_key" "${t7_out}"
|
|
205
|
+
|
|
206
|
+
# ---------------------------------------------------------------------------
|
|
207
|
+
# Test 8 — no false positives: clean baselines still exit 0 after normalization.
|
|
208
|
+
# ---------------------------------------------------------------------------
|
|
209
|
+
bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >/dev/null 2>&1
|
|
210
|
+
assert_exit "Test 8a: normalization does not manufacture matches on clean PIR" 0 $?
|
|
211
|
+
t8_file="${TMP_DIR}/t8.txt"
|
|
212
|
+
python3 -c "import sys; sys.stdout.write('Privet clean prose ' + chr(0x043f)+chr(0x0440)+' no secrets.\n')" > "${t8_file}"
|
|
213
|
+
bash "${SENTINEL}" "${t8_file}" >/dev/null 2>&1
|
|
214
|
+
assert_exit "Test 8b: Cyrillic prose without secrets exits 0 (fold != fabricate)" 0 $?
|
|
215
|
+
|
|
216
|
+
# ---------------------------------------------------------------------------
|
|
217
|
+
# Test 9 — golden ERE<->re parity (AC3): every class the OLD bash engine catches
|
|
218
|
+
# on the corpus, the NEW engine also catches (no class narrowed by the port).
|
|
219
|
+
# New additive classes (doppler/slack) are a superset and do not break parity.
|
|
220
|
+
# ---------------------------------------------------------------------------
|
|
221
|
+
old_hits="${TMP_DIR}/old_hits.txt"
|
|
222
|
+
new_hits="${TMP_DIR}/new_hits.txt"
|
|
223
|
+
# LC_ALL=C on BOTH sort and comm: comm requires byte-collation order; a locale sort (en_US) collates
|
|
224
|
+
# case-insensitively so comm sees "unsorted input" and its diff is undefined — the parity guard would
|
|
225
|
+
# run blind. Byte-sort both streams and byte-compare.
|
|
226
|
+
bash "${OLD_ENGINE}" "${POSITIVE_CORPUS}" 2>/dev/null | grep -oE 'matched pattern [A-Za-z_]+' | LC_ALL=C sort -u > "${old_hits}"
|
|
227
|
+
bash "${SENTINEL}" "${POSITIVE_CORPUS}" 2>/dev/null | grep -oE 'matched pattern [A-Za-z_]+' | LC_ALL=C sort -u > "${new_hits}"
|
|
228
|
+
missing=$(LC_ALL=C comm -23 "${old_hits}" "${new_hits}")
|
|
229
|
+
if [[ -z "${missing}" ]]; then
|
|
230
|
+
echo "PASS: Test 9: golden parity — new engine catches every class the old engine did"
|
|
231
|
+
PASS=$((PASS + 1))
|
|
232
|
+
else
|
|
233
|
+
echo "FAIL: Test 9: new engine NARROWED these classes vs old: ${missing//$'\n'/, }"
|
|
234
|
+
FAIL=$((FAIL + 1))
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
# Test 10 — fail-closed on no-python3 (AC4): shim exits 2 (not 0, not 1) with
|
|
239
|
+
# python3 shadowed off PATH. `dirname` symlinked so the shim can still resolve DIR.
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
nopy="${TMP_DIR}/nopy"
|
|
242
|
+
mkdir -p "${nopy}"
|
|
243
|
+
ln -s "$(command -v dirname)" "${nopy}/dirname"
|
|
244
|
+
BASH_BIN="$(command -v bash)" # absolute path — parent must NOT PATH-search under the stripped PATH
|
|
245
|
+
t10_file="${TMP_DIR}/t10.txt"
|
|
246
|
+
printf 'sk_test_0000000000000000\n' > "${t10_file}"
|
|
247
|
+
PATH="${nopy}" "${BASH_BIN}" "${SENTINEL}" "${t10_file}" >/dev/null 2>&1
|
|
248
|
+
assert_exit "Test 10: no-python3 fails closed with exit 2" 2 $?
|
|
249
|
+
|
|
250
|
+
# ---------------------------------------------------------------------------
|
|
251
|
+
# Test 11 — legal-generate gate (AC5): a synthesized secret in a legal draft
|
|
252
|
+
# mktemp makes the sentinel exit non-zero; and the SKILL.md wires the gate
|
|
253
|
+
# BEFORE the inline presentation step.
|
|
254
|
+
# ---------------------------------------------------------------------------
|
|
255
|
+
t11_file="${TMP_DIR}/legal-draft.md"
|
|
256
|
+
printf '# Privacy Policy (DRAFT)\n\nContact: STRIPE_SECRET_KEY=sk_live_0000000000000000\n' > "${t11_file}"
|
|
257
|
+
bash "${SENTINEL}" "${t11_file}" >/dev/null 2>&1
|
|
258
|
+
assert_exit "Test 11a: legal draft with secret trips the sentinel (non-zero)" 1 $?
|
|
259
|
+
assert_grep "Test 11b: legal-generate SKILL.md wires redact-sentinel.sh" \
|
|
260
|
+
'redact-sentinel\.sh' "${LEGAL_SKILL}"
|
|
261
|
+
# The gate block must appear BEFORE the presentation step. Anchor on the stable structural heading
|
|
262
|
+
# `## Phase 3` (where the Accept/Edit/Reject presentation lives) rather than brittle label prose —
|
|
263
|
+
# a reworded decision-gate label must not silently empty the check.
|
|
264
|
+
gate_line=$(grep -nE 'redact-sentinel\.sh' "${LEGAL_SKILL}" 2>/dev/null | head -1 | cut -d: -f1)
|
|
265
|
+
present_line=$(grep -nE '^## Phase 3' "${LEGAL_SKILL}" 2>/dev/null | head -1 | cut -d: -f1)
|
|
266
|
+
if [[ -n "${gate_line}" && -n "${present_line}" && "${gate_line}" -lt "${present_line}" ]]; then
|
|
267
|
+
echo "PASS: Test 11c: redaction gate precedes inline presentation (Phase 3) in legal-generate SKILL.md"
|
|
268
|
+
PASS=$((PASS + 1))
|
|
269
|
+
else
|
|
270
|
+
echo "FAIL: Test 11c: redaction gate must precede Phase 3 presentation (gate=${gate_line:-none}, phase3=${present_line:-none})"
|
|
271
|
+
FAIL=$((FAIL + 1))
|
|
272
|
+
fi
|
|
273
|
+
|
|
274
|
+
# ---------------------------------------------------------------------------
|
|
275
|
+
# Test 12 — cross-script homoglyph honesty (AC1): a CONFUSABLE_MAP-covered prefix
|
|
276
|
+
# (Cyrillic s U+0455) is caught; an UNMAPPED lookalike (Cyrillic t U+0442) is a
|
|
277
|
+
# version-controlled KNOWN GAP (current behavior = exit 0, not claimed covered).
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
t12_cov="${TMP_DIR}/t12-covered.txt"
|
|
280
|
+
python3 -c "import sys; sys.stdout.write(chr(0x0455)+'k-ant-' + 'A'*32 + '\n')" > "${t12_cov}" # Cyrillic s -> s
|
|
281
|
+
bash "${SENTINEL}" "${t12_cov}" >/dev/null 2>&1
|
|
282
|
+
assert_exit "Test 12a: CONFUSABLE_MAP-covered homoglyph prefix is caught (exit 1)" 1 $?
|
|
283
|
+
|
|
284
|
+
t12_gap="${TMP_DIR}/t12-gap.txt"
|
|
285
|
+
python3 -c "import sys; sys.stdout.write('sk-an'+chr(0x0442)+'-' + 'A'*32 + '\n')" > "${t12_gap}" # Cyrillic t -> UNMAPPED
|
|
286
|
+
bash "${SENTINEL}" "${t12_gap}" >/dev/null 2>&1
|
|
287
|
+
assert_exit "Test 12b: UNMAPPED homoglyph is a version-controlled known gap (exit 0)" 0 $?
|
|
288
|
+
|
|
289
|
+
# ---------------------------------------------------------------------------
|
|
290
|
+
# AC6 — no literal invisibles committed anywhere in the two touched skills.
|
|
291
|
+
# Exclusion: skills/legal-generate/references/templates/ is upstream-vendored
|
|
292
|
+
# corpus whose bytes are pinned to upstream blobs in the skill NOTICE — the
|
|
293
|
+
# blob pin is a strictly stronger integrity control than this grep (any local
|
|
294
|
+
# edit, invisibles included, fails vendor-pin-integrity), and verbatim byte
|
|
295
|
+
# fidelity is what keeps drift detection meaningful.
|
|
296
|
+
# ---------------------------------------------------------------------------
|
|
297
|
+
if grep -rlP '[\x{200b}\x{200c}\x{200d}\x{2060}\x{feff}\x{202a}-\x{202e}\x{2028}\x{2029}\x{00ad}\x{fffd}]' \
|
|
298
|
+
"${REPO_ROOT}/plugins/soleur/skills/incident" \
|
|
299
|
+
"${REPO_ROOT}/plugins/soleur/skills/legal-generate" 2>/dev/null \
|
|
300
|
+
| grep -v '/skills/legal-generate/references/templates/' | grep -c >/dev/null .; then
|
|
301
|
+
echo "FAIL: AC6: literal invisibles committed (must be chr()/escapes only)"
|
|
302
|
+
grep -rlP '[\x{200b}\x{200c}\x{200d}\x{2060}\x{feff}\x{202a}-\x{202e}\x{2028}\x{2029}\x{00ad}\x{fffd}]' \
|
|
303
|
+
"${REPO_ROOT}/plugins/soleur/skills/incident" \
|
|
304
|
+
"${REPO_ROOT}/plugins/soleur/skills/legal-generate" 2>/dev/null \
|
|
305
|
+
| grep -v '/skills/legal-generate/references/templates/'
|
|
306
|
+
FAIL=$((FAIL + 1))
|
|
307
|
+
else
|
|
308
|
+
echo "PASS: AC6: no literal invisibles committed in incident/legal-generate skills"
|
|
309
|
+
PASS=$((PASS + 1))
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
# ===========================================================================
|
|
313
|
+
# #6045 PR-B — item 1 (whitespace/newline reflow re-scan) + item 6 (Cloudflare token).
|
|
314
|
+
# All fixtures synthesized at runtime; invisibles/newlines via chr() (AC6).
|
|
315
|
+
# ===========================================================================
|
|
316
|
+
|
|
317
|
+
# Test 13 — reflow two-engine (item 1, AC1 shape): a stripe key split by a NEWLINE.
|
|
318
|
+
# (a) OLD raw-byte engine MISSES the split token (exit 0); (b) the new engine's base pass also
|
|
319
|
+
# cannot match across the kept newline — only _scan_reflow catches it after whitespace-rejoin.
|
|
320
|
+
# stripe_key is a pre-#5987 class the OLD engine already carries, so 13a isolates the SPLIT-evasion.
|
|
321
|
+
t13_file="${TMP_DIR}/t13.txt"
|
|
322
|
+
python3 -c "import sys; sys.stdout.write('key sk_live_0000ABCD'+chr(0x0a)+'1234000000000000 end'+chr(0x0a))" > "${t13_file}"
|
|
323
|
+
bash "${OLD_ENGINE}" "${t13_file}" >/dev/null 2>&1
|
|
324
|
+
assert_exit "Test 13a: OLD raw-byte engine MISSES newline-split stripe key" 0 $?
|
|
325
|
+
t13_out="${TMP_DIR}/t13.out"
|
|
326
|
+
bash "${SENTINEL}" "${t13_file}" >"${t13_out}" 2>&1
|
|
327
|
+
assert_exit "Test 13b: reflow catches newline-split stripe key" 1 $?
|
|
328
|
+
assert_grep "Test 13b: stripe_key via whitespace-rejoin" "matched pattern stripe_key" "${t13_out}"
|
|
329
|
+
|
|
330
|
+
# Test 13c — reflow with a SPACE split (distinct whitespace kind from 13b's newline). A doppler
|
|
331
|
+
# token split by an ASCII space (0x20 survives normalization). Must be caught.
|
|
332
|
+
t13c_file="${TMP_DIR}/t13c.txt"
|
|
333
|
+
python3 -c "import sys; sys.stdout.write('tok dp.st.ABCD1234 EFGH5678IJKLMNOP done'+chr(0x0a))" > "${t13c_file}"
|
|
334
|
+
bash "${SENTINEL}" "${t13c_file}" >/dev/null 2>&1
|
|
335
|
+
assert_exit "Test 13c: reflow catches space-split doppler token" 1 $?
|
|
336
|
+
|
|
337
|
+
# Test 13d — reflow NO-FALSE-POSITIVE on an INCLUDED prefix (spec-flow highest-value guard):
|
|
338
|
+
# an included distinctive prefix (dp.st.) followed by DESPACEABLE PROSE must NOT manufacture a
|
|
339
|
+
# match. Naive whitespace-strip would glue the prose into a 16+ run; the bounded-split guard
|
|
340
|
+
# (rejoin across few runs only; prose has a run every few chars) rejects it. Must exit 0.
|
|
341
|
+
t13d_file="${TMP_DIR}/t13d.txt"
|
|
342
|
+
python3 -c "import sys; sys.stdout.write('note dp.st. the quick brown fox ran far away today ok'+chr(0x0a))" > "${t13d_file}"
|
|
343
|
+
bash "${SENTINEL}" "${t13d_file}" >/dev/null 2>&1
|
|
344
|
+
assert_exit "Test 13d: reflow does NOT manufacture a match from prose after an included prefix" 0 $?
|
|
345
|
+
|
|
346
|
+
# Test 13e — negative baseline still clean after the reflow pass (no manufactured matches).
|
|
347
|
+
bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >/dev/null 2>&1
|
|
348
|
+
assert_exit "Test 13e: reflow pass does not manufacture matches on the clean PIR" 0 $?
|
|
349
|
+
|
|
350
|
+
# Test 13f — Test-4b invariant (meta-redaction) holds on a REFLOW-caught finding (new emit path).
|
|
351
|
+
t13f_out="${TMP_DIR}/t13f.out"
|
|
352
|
+
bash "${SENTINEL}" "${t13_file}" >"${t13f_out}" 2>&1 || true
|
|
353
|
+
if grep -qE 'matched pattern stripe_key \(whitespace-rejoin\)' "${t13f_out}" && \
|
|
354
|
+
! grep -qE 'at offset [0-9]+: [^*]{5,}\*\*\*' "${t13f_out}"; then
|
|
355
|
+
echo "PASS: Test 13f: reflow finding is tagged and meta-redacted (<=4-char reveal)"
|
|
356
|
+
PASS=$((PASS + 1))
|
|
357
|
+
else
|
|
358
|
+
echo "FAIL: Test 13f: reflow finding missing tag or leaked >4 prefix chars"
|
|
359
|
+
cat "${t13f_out}"
|
|
360
|
+
FAIL=$((FAIL + 1))
|
|
361
|
+
fi
|
|
362
|
+
|
|
363
|
+
# Test 14 — Cloudflare token (item 6): a 40-char [A-Za-z0-9_-] token containing BOTH an uppercase
|
|
364
|
+
# letter AND a digit trips cloudflare_token. Synthesized (no real token).
|
|
365
|
+
t14_file="${TMP_DIR}/t14.txt"
|
|
366
|
+
python3 -c "import sys; sys.stdout.write('CF_TOKEN v1.0 Ab3'+'x'*37+' end'+chr(0x0a))" > "${t14_file}"
|
|
367
|
+
t14_out="${TMP_DIR}/t14.out"
|
|
368
|
+
bash "${SENTINEL}" "${t14_file}" >"${t14_out}" 2>&1
|
|
369
|
+
assert_exit "Test 14: 40-char upper+digit token trips cloudflare_token" 1 $?
|
|
370
|
+
assert_grep "Test 14: cloudflare_token matched" "matched pattern cloudflare_token" "${t14_out}"
|
|
371
|
+
|
|
372
|
+
# Test 14b — anti-SHA NO-FALSE-POSITIVE: a 40-char lowercase-hex git SHA (digit, NO uppercase)
|
|
373
|
+
# must NOT trip. Incident PIRs cite commit SHAs constantly.
|
|
374
|
+
t14b_file="${TMP_DIR}/t14b.txt"
|
|
375
|
+
python3 -c "import sys; sys.stdout.write('commit '+'a1b2c3d4'*5+' shipped'+chr(0x0a))" > "${t14b_file}"
|
|
376
|
+
t14b_out="${TMP_DIR}/t14b.out"
|
|
377
|
+
bash "${SENTINEL}" "${t14b_file}" >"${t14b_out}" 2>&1
|
|
378
|
+
if grep -qE 'matched pattern cloudflare_token' "${t14b_out}"; then
|
|
379
|
+
echo "FAIL: Test 14b: a 40-char git SHA falsely tripped cloudflare_token"
|
|
380
|
+
FAIL=$((FAIL + 1))
|
|
381
|
+
else
|
|
382
|
+
echo "PASS: Test 14b: 40-char lowercase-hex git SHA does NOT trip cloudflare_token (anti-SHA predicate)"
|
|
383
|
+
PASS=$((PASS + 1))
|
|
384
|
+
fi
|
|
385
|
+
|
|
386
|
+
# Test 14c — anti-prose NO-FALSE-POSITIVE: a 40-char kebab-case identifier span (NO uppercase, NO
|
|
387
|
+
# digit) must NOT trip.
|
|
388
|
+
t14c_file="${TMP_DIR}/t14c.txt"
|
|
389
|
+
python3 -c "import sys; sys.stdout.write('rule cq-'+'-'.join(['abc']*9)+' xx'+chr(0x0a))" > "${t14c_file}"
|
|
390
|
+
t14c_out="${TMP_DIR}/t14c.out"
|
|
391
|
+
bash "${SENTINEL}" "${t14c_file}" >"${t14c_out}" 2>&1
|
|
392
|
+
if grep -qE 'matched pattern cloudflare_token' "${t14c_out}"; then
|
|
393
|
+
echo "FAIL: Test 14c: a 40-char kebab span falsely tripped cloudflare_token"
|
|
394
|
+
FAIL=$((FAIL + 1))
|
|
395
|
+
else
|
|
396
|
+
echo "PASS: Test 14c: 40-char kebab prose does NOT trip cloudflare_token"
|
|
397
|
+
PASS=$((PASS + 1))
|
|
398
|
+
fi
|
|
399
|
+
|
|
400
|
+
# Test 14d — negative baseline still clean with cloudflare_token added (cites SHAs).
|
|
401
|
+
bash "${SENTINEL}" "${NEGATIVE_BASELINE}" >/dev/null 2>&1
|
|
402
|
+
assert_exit "Test 14d: cloudflare_token class does not manufacture matches on the clean PIR" 0 $?
|
|
403
|
+
|
|
404
|
+
# ===========================================================================
|
|
405
|
+
# #6045 PR-C — item 2 (base64/hex/percent decode-and-rescan) + item 3 (headerless-PEM
|
|
406
|
+
# private-key DER discriminator). DER fixtures are SYNTHESIZED from the ASN.1 spec (no
|
|
407
|
+
# key material) so they exercise the discriminator branches directly.
|
|
408
|
+
# ===========================================================================
|
|
409
|
+
|
|
410
|
+
# DER shape helpers (Python): seq() wraps content in a SEQUENCE with correct short/long-form length.
|
|
411
|
+
_DER_PY='
|
|
412
|
+
import base64, sys
|
|
413
|
+
def seq(content):
|
|
414
|
+
n = len(content)
|
|
415
|
+
if n < 0x80: length = bytes([n])
|
|
416
|
+
elif n < 0x100: length = bytes([0x81, n])
|
|
417
|
+
else: length = bytes([0x82, n >> 8, n & 0xFF])
|
|
418
|
+
return bytes([0x30]) + length + content
|
|
419
|
+
def b64(b): return base64.b64encode(b).decode()
|
|
420
|
+
# private-key-shaped, SHORT-form length (EC/Ed25519 size): SEQUENCE{ INTEGER 1, OCTET STRING(44) }
|
|
421
|
+
priv_short = seq(bytes([0x02,0x01,0x01]) + bytes([0x04,0x2C]) + b"\x00"*44)
|
|
422
|
+
# private-key-shaped, LONG-form length (RSA size): SEQUENCE{ INTEGER 0, OCTET STRING(300) }
|
|
423
|
+
priv_long = seq(bytes([0x02,0x01,0x00]) + bytes([0x04,0x82,0x01,0x2C]) + b"\x00"*300)
|
|
424
|
+
# cert/SPKI/encrypted-PKCS#8 shape: SEQUENCE{ SEQUENCE(44) } — first inner is SEQUENCE, must REJECT
|
|
425
|
+
cert_like = seq(seq(b"\x00"*44))
|
|
426
|
+
# PNG magic (0x89) — not a SEQUENCE, must REJECT
|
|
427
|
+
png = bytes([0x89,0x50,0x4E,0x47,0x0D,0x0A,0x1A,0x0A]) + b"\x00"*56
|
|
428
|
+
'
|
|
429
|
+
|
|
430
|
+
# Test 15 — item 3 private-key DER discriminator POSITIVES (short + long form).
|
|
431
|
+
t15a="${TMP_DIR}/t15a.txt"
|
|
432
|
+
python3 -c "${_DER_PY}
|
|
433
|
+
sys.stdout.write('k8s data: ' + b64(priv_short) + '\n')" > "${t15a}"
|
|
434
|
+
bash "${SENTINEL}" "${t15a}" >${TMP_DIR}/t15a.out 2>&1
|
|
435
|
+
assert_exit "Test 15a: short-form (EC/Ed25519-size) headerless private-key DER caught" 1 $?
|
|
436
|
+
assert_grep "Test 15a: pem_key_body" "matched pattern pem_key_body" ${TMP_DIR}/t15a.out
|
|
437
|
+
|
|
438
|
+
t15b="${TMP_DIR}/t15b.txt"
|
|
439
|
+
python3 -c "${_DER_PY}
|
|
440
|
+
sys.stdout.write('blob ' + b64(priv_long) + '\n')" > "${t15b}"
|
|
441
|
+
bash "${SENTINEL}" "${t15b}" >/dev/null 2>&1
|
|
442
|
+
assert_exit "Test 15b: long-form (RSA-size) headerless private-key DER caught" 1 $?
|
|
443
|
+
|
|
444
|
+
# Test 15c — item 3 with a 64-char-WRAPPED body (real PEM bodies are line-wrapped): block assembly
|
|
445
|
+
# must join consecutive base64 lines before decoding.
|
|
446
|
+
t15c="${TMP_DIR}/t15c.txt"
|
|
447
|
+
python3 -c "${_DER_PY}
|
|
448
|
+
b = b64(priv_long)
|
|
449
|
+
wrapped = '\n'.join(b[i:i+64] for i in range(0, len(b), 64))
|
|
450
|
+
sys.stdout.write('-- body --\n' + wrapped + '\n-- end --\n')" > "${t15c}"
|
|
451
|
+
bash "${SENTINEL}" "${t15c}" >/dev/null 2>&1
|
|
452
|
+
assert_exit "Test 15c: 64-char-wrapped headerless private-key body caught via block assembly" 1 $?
|
|
453
|
+
|
|
454
|
+
# Test 15d — item 3 NO-FALSE-POSITIVE: cert/SPKI/encrypted-PKCS#8 shape (inner SEQUENCE) must NOT
|
|
455
|
+
# trip pem_key_body — a public cert pasted into a legal draft is legitimate; flagging it fail-closes.
|
|
456
|
+
t15d="${TMP_DIR}/t15d.txt"
|
|
457
|
+
python3 -c "${_DER_PY}
|
|
458
|
+
sys.stdout.write('cert ' + b64(cert_like) + '\n')" > "${t15d}"
|
|
459
|
+
bash "${SENTINEL}" "${t15d}" >${TMP_DIR}/t15d.out 2>&1
|
|
460
|
+
if grep -qE 'matched pattern pem_key_body' ${TMP_DIR}/t15d.out; then
|
|
461
|
+
echo "FAIL: Test 15d: a public-cert-shaped DER (inner SEQUENCE) falsely tripped pem_key_body"; FAIL=$((FAIL + 1))
|
|
462
|
+
else
|
|
463
|
+
echo "PASS: Test 15d: cert/SPKI/encrypted-PKCS#8 shape does NOT trip pem_key_body (private-key discriminator)"; PASS=$((PASS + 1))
|
|
464
|
+
fi
|
|
465
|
+
|
|
466
|
+
# Test 15e — item 3 NO-FALSE-POSITIVE: a PNG image body (0x89 magic) must NOT trip pem_key_body.
|
|
467
|
+
t15e="${TMP_DIR}/t15e.txt"
|
|
468
|
+
python3 -c "${_DER_PY}
|
|
469
|
+
sys.stdout.write('img ' + b64(png) + '\n')" > "${t15e}"
|
|
470
|
+
bash "${SENTINEL}" "${t15e}" >${TMP_DIR}/t15e.out 2>&1
|
|
471
|
+
if grep -qE 'matched pattern pem_key_body' ${TMP_DIR}/t15e.out; then
|
|
472
|
+
echo "FAIL: Test 15e: a PNG body falsely tripped pem_key_body"; FAIL=$((FAIL + 1))
|
|
473
|
+
else
|
|
474
|
+
echo "PASS: Test 15e: PNG image body does NOT trip pem_key_body"; PASS=$((PASS + 1))
|
|
475
|
+
fi
|
|
476
|
+
|
|
477
|
+
# Test 16 — item 2 decode-and-rescan POSITIVES (base64, base64url, hex, percent) of a known secret.
|
|
478
|
+
_SECRET_PY='
|
|
479
|
+
import base64, urllib.parse, binascii, sys
|
|
480
|
+
# Split across concatenation so no contiguous sk_live_<24+> literal exists in source (GitHub push
|
|
481
|
+
# protection scans the raw file; cq-test-fixtures-synthesized-only). 38B body -> base64 is 52 chars
|
|
482
|
+
# (not 40 -> no cloudflare_token collision in the decode test).
|
|
483
|
+
s = b"sk_" + b"live_0000ABCD1234567890abcdefXYZ789"
|
|
484
|
+
b64s = base64.b64encode(s).decode()
|
|
485
|
+
b64url = base64.urlsafe_b64encode(s).decode()
|
|
486
|
+
hexs = s.hex()
|
|
487
|
+
pcts = urllib.parse.quote(s.decode())
|
|
488
|
+
'
|
|
489
|
+
for enc in b64s b64url hexs pcts; do
|
|
490
|
+
f="${TMP_DIR}/t16-${enc}.txt"
|
|
491
|
+
python3 -c "${_SECRET_PY}
|
|
492
|
+
sys.stdout.write('payload: ' + ${enc} + ' trailer\n')" > "${f}"
|
|
493
|
+
bash "${SENTINEL}" "${f}" >${TMP_DIR}/t16.out 2>&1
|
|
494
|
+
assert_exit "Test 16.${enc}: encoded stripe key decoded and caught" 1 $?
|
|
495
|
+
assert_grep "Test 16.${enc}: stripe_key via decode" "matched pattern stripe_key" ${TMP_DIR}/t16.out
|
|
496
|
+
done
|
|
497
|
+
|
|
498
|
+
# Test 16b — decode NO-FALSE-POSITIVE corpus: innocent encoded content must NOT manufacture a match.
|
|
499
|
+
# (i) base64 of a PNG image; (ii) an SRI sha512- hash; (iii) a git SHA / sha256 hex; (iv) a base64
|
|
500
|
+
# JWT-payload-shaped JSON containing an email (email is decode-skipped → must stay clean); (v) a
|
|
501
|
+
# percent-encoded URL.
|
|
502
|
+
t16b="${TMP_DIR}/t16b.txt"
|
|
503
|
+
python3 -c "
|
|
504
|
+
import base64, sys
|
|
505
|
+
png = bytes([0x89,0x50,0x4E,0x47]) + bytes(range(60))
|
|
506
|
+
sri = 'sha512-' + base64.b64encode(bytes(range(64))).decode()
|
|
507
|
+
jwtjson = base64.urlsafe_b64encode(b'{\"sub\":\"1\",\"email\":\"a@b.com\"}').decode()
|
|
508
|
+
sys.stdout.write('image ' + base64.b64encode(png).decode() + '\n')
|
|
509
|
+
sys.stdout.write('integrity ' + sri + '\n')
|
|
510
|
+
sys.stdout.write('commit ' + 'ab'*20 + ' sha256 ' + 'cd'*32 + '\n')
|
|
511
|
+
sys.stdout.write('token ' + jwtjson + '\n')
|
|
512
|
+
sys.stdout.write('url https://x.example/a%2Fb%2Fc?q=1 done\n')
|
|
513
|
+
" > "${t16b}"
|
|
514
|
+
bash "${SENTINEL}" "${t16b}" >${TMP_DIR}/t16b.out 2>&1
|
|
515
|
+
if [[ $? -eq 0 ]]; then
|
|
516
|
+
echo "PASS: Test 16b: innocent encoded content (image/SRI/SHA/JWT-JSON-email/percent-URL) does NOT manufacture a match"; PASS=$((PASS + 1))
|
|
517
|
+
else
|
|
518
|
+
echo "FAIL: Test 16b: decode pass manufactured a false positive on innocent encoded content"; cat ${TMP_DIR}/t16b.out; FAIL=$((FAIL + 1))
|
|
519
|
+
fi
|
|
520
|
+
|
|
521
|
+
# Test 16c — malformed base64 candidate must NOT bubble to the exit-2 catch-all (per-candidate guard).
|
|
522
|
+
# A real secret alongside a malformed blob must still be caught (exit 1), not fail-closed to exit 2.
|
|
523
|
+
t16c="${TMP_DIR}/t16c.txt"
|
|
524
|
+
python3 -c "
|
|
525
|
+
import base64, sys
|
|
526
|
+
good = base64.b64encode(b'sk_' + b'live_0000ABCD1234567890abXY').decode() # split literal: gitleaks stripe rule (cq-test-fixtures-synthesized-only)
|
|
527
|
+
sys.stdout.write('bad !!!====notbase64==== ' + good + ' end\n')
|
|
528
|
+
" > "${t16c}"
|
|
529
|
+
bash "${SENTINEL}" "${t16c}" >/dev/null 2>&1
|
|
530
|
+
assert_exit "Test 16c: malformed base64 candidate skipped per-candidate (real secret still caught, exit 1 not 2)" 1 $?
|
|
531
|
+
|
|
532
|
+
# Test 16d — Test-4b invariant on a DECODE-caught finding (new emit path).
|
|
533
|
+
bash "${SENTINEL}" "${TMP_DIR}/t16-b64s.txt" >${TMP_DIR}/t16d.out 2>&1 || true
|
|
534
|
+
if grep -qE 'matched pattern stripe_key \(decoded' ${TMP_DIR}/t16d.out && \
|
|
535
|
+
! grep -qE 'at offset [0-9]+: [^*]{5,}\*\*\*' ${TMP_DIR}/t16d.out; then
|
|
536
|
+
echo "PASS: Test 16d: decode finding is tagged and meta-redacted (<=4-char reveal)"; PASS=$((PASS + 1))
|
|
537
|
+
else
|
|
538
|
+
echo "FAIL: Test 16d: decode finding missing tag or leaked >4 prefix chars"; cat ${TMP_DIR}/t16d.out; FAIL=$((FAIL + 1))
|
|
539
|
+
fi
|
|
540
|
+
|
|
541
|
+
# Test 16e — behavioral fan-out bound: a base64-run-flooded input completes without hanging and
|
|
542
|
+
# exits cleanly (no manufactured matches); pins _MAX_ENCODED_CANDIDATES against a future cap removal.
|
|
543
|
+
t16e="${TMP_DIR}/t16e.txt"
|
|
544
|
+
python3 -c "import sys; sys.stdout.write((('QUJDREVGR0hJSktMTU5PUFFSU1Q ')*5000))" > "${t16e}"
|
|
545
|
+
timeout 30 bash "${SENTINEL}" "${t16e}" >/dev/null 2>&1
|
|
546
|
+
rc=$?
|
|
547
|
+
if [[ $rc -eq 0 || $rc -eq 1 ]]; then
|
|
548
|
+
echo "PASS: Test 16e: base64-flooded input completes within bound (rc=${rc}, not a timeout)"; PASS=$((PASS + 1))
|
|
549
|
+
else
|
|
550
|
+
echo "FAIL: Test 16e: base64-flooded input did not complete (rc=${rc} — possible unbounded fan-out)"; FAIL=$((FAIL + 1))
|
|
551
|
+
fi
|
|
552
|
+
|
|
553
|
+
# Test 15f — item 3 with an INDENTED, 32-col-wrapped body (the real k8s Secret / Helm `data:` shape):
|
|
554
|
+
# block assembly must .strip() each line so indented base64 assembles. (#6045 review F2)
|
|
555
|
+
t15f="${TMP_DIR}/t15f.txt"
|
|
556
|
+
python3 -c "${_DER_PY}
|
|
557
|
+
b = b64(priv_long)
|
|
558
|
+
wrapped = '\n'.join(' ' + b[i:i+32] for i in range(0, len(b), 32)) # 4-space indent, 32-col
|
|
559
|
+
sys.stdout.write('data:\n tls.key: |\n' + wrapped + '\n')" > "${t15f}"
|
|
560
|
+
bash "${SENTINEL}" "${t15f}" >/dev/null 2>&1
|
|
561
|
+
assert_exit "Test 15f: indented 32-col-wrapped k8s Secret private-key body caught via block assembly" 1 $?
|
|
562
|
+
|
|
563
|
+
# Test 16f — decode NO-FALSE-POSITIVE on a BINARY asset: a base64'd mostly-non-printable blob that
|
|
564
|
+
# carries an INCIDENTAL sk- run must NOT fail-close (the printable-text gate skips binary). (#6045 review F4)
|
|
565
|
+
t16f="${TMP_DIR}/t16f.txt"
|
|
566
|
+
python3 -c "
|
|
567
|
+
import base64, sys
|
|
568
|
+
# 200 high bytes (a font/wasm/image shape) with an incidental 'sk-...' run spliced in.
|
|
569
|
+
blob = bytes(range(0x80, 0x100)) * 2 + b'sk-A1b2C3d4E5f6G7h8I9j0' + bytes(range(0x80, 0x100))
|
|
570
|
+
sys.stdout.write('embedded asset: ' + base64.b64encode(blob).decode() + ' end\n')
|
|
571
|
+
" > "${t16f}"
|
|
572
|
+
bash "${SENTINEL}" "${t16f}" >${TMP_DIR}/t16f.out 2>&1
|
|
573
|
+
if [[ $? -eq 0 ]]; then
|
|
574
|
+
echo "PASS: Test 16f: a base64 binary asset with an incidental sk- run does NOT fail-close (printable-text gate)"; PASS=$((PASS + 1))
|
|
575
|
+
else
|
|
576
|
+
echo "FAIL: Test 16f: decode pass fail-closed a legitimate binary asset (over-redaction FP)"; cat ${TMP_DIR}/t16f.out; FAIL=$((FAIL + 1))
|
|
577
|
+
fi
|
|
578
|
+
|
|
579
|
+
# Test 17 — email-class ReDoS bound (#6045 security review P1): a large run of email-class chars with
|
|
580
|
+
# NO '@' must complete quickly (bounded quantifier → linear), not hang the fail-closed gate. 256 KiB of
|
|
581
|
+
# 'a' at the pre-fix O(n^2) rate timed out (>2 min); bounded it returns in well under the 20s ceiling.
|
|
582
|
+
t17="${TMP_DIR}/t17.txt"
|
|
583
|
+
python3 -c "import sys; sys.stdout.write('a'*262144)" > "${t17}"
|
|
584
|
+
timeout 20 bash "${SENTINEL}" "${t17}" >/dev/null 2>&1
|
|
585
|
+
rc=$?
|
|
586
|
+
if [[ $rc -eq 0 || $rc -eq 1 ]]; then
|
|
587
|
+
echo "PASS: Test 17: 256 KiB email-class flood completes within 20s (email quantifier bounded — no O(n^2) ReDoS)"; PASS=$((PASS + 1))
|
|
588
|
+
else
|
|
589
|
+
echo "FAIL: Test 17: email-class flood did not complete (rc=${rc} — O(n^2) ReDoS regression)"; FAIL=$((FAIL + 1))
|
|
590
|
+
fi
|
|
591
|
+
|
|
592
|
+
# Test 18 — deployed-anchor coupling, RETARGETED and STRENGTHENED (#7450; was #6156 review P2).
|
|
593
|
+
#
|
|
594
|
+
# The original pinned the two redact-sentinel call sites byte-identical to each other, which is
|
|
595
|
+
# necessary and was never sufficient: two files can agree perfectly on an anchor that resolves
|
|
596
|
+
# into the reviewed party's tree. A naive `ANCHOR` swap would have preserved only that weak
|
|
597
|
+
# property and left the corpus unguarded, so this now asserts THREE things
|
|
598
|
+
# (`cq-assert-anchor-not-bare-token`).
|
|
599
|
+
INCIDENT_SKILL="${SKILL_DIR}/SKILL.md"
|
|
600
|
+
LINEAR_SKILL="${REPO_ROOT}/plugins/soleur/skills/linear-fetch/SKILL.md"
|
|
601
|
+
# The 4th gate ADR-179's negative branch names. It carries the preflight but nothing pinned
|
|
602
|
+
# it, and its halts carried no marker — so C10 covered 3 of the 4 skills the ADR credits.
|
|
603
|
+
TRIGGER_SKILL="${REPO_ROOT}/plugins/soleur/skills/trigger-cron/SKILL.md"
|
|
604
|
+
|
|
605
|
+
# 18a — coupling (preserved): the bare literal appears exactly 1x in each site.
|
|
606
|
+
ANCHOR='${CLAUDE_PLUGIN_ROOT}/skills/incident/scripts/redact-sentinel.sh'
|
|
607
|
+
lg_anchor=$(grep -Fc "${ANCHOR}" "${LEGAL_SKILL}" 2>/dev/null || true)
|
|
608
|
+
inc_anchor=$(grep -Fc "${ANCHOR}" "${INCIDENT_SKILL}" 2>/dev/null || true)
|
|
609
|
+
if [[ "${lg_anchor}" == "1" && "${inc_anchor}" == "1" ]]; then
|
|
610
|
+
echo "PASS: Test 18a: bare deployed-anchor redact-sentinel literal is byte-identical (1×) in both legal-generate & incident SKILL.md"
|
|
611
|
+
PASS=$((PASS + 1))
|
|
612
|
+
else
|
|
613
|
+
echo "FAIL: Test 18a: deployed-anchor drift — expected the ADR-179 bare anchored redact-sentinel literal exactly once in each (legal-generate=${lg_anchor}, incident=${inc_anchor})"
|
|
614
|
+
FAIL=$((FAIL + 1))
|
|
615
|
+
fi
|
|
616
|
+
|
|
617
|
+
# 18b — the ADR-179 decision-2 identity preflight is present at every secret-gate site.
|
|
618
|
+
# Anchored on the manifest name check, which a prose sentence cannot produce; a bare token like
|
|
619
|
+
# "preflight" or "CLAUDE_PLUGIN_ROOT" would be satisfied by a comment ABOUT the guard.
|
|
620
|
+
# `[[ -r ]]` does NOT satisfy this: ADR-179 §(a) measured a shape check passing while an
|
|
621
|
+
# attacker-chosen payload executed.
|
|
622
|
+
PREFLIGHT_NEEDLE='grep -q '"'"'"name"[[:space:]]*:[[:space:]]*"soleur"'"'"''
|
|
623
|
+
missing_preflight=""
|
|
624
|
+
unbound_halt=""
|
|
625
|
+
|
|
626
|
+
# STRENGTHENED, not cut (#7450 review). Guard 1 asserts the same property in
|
|
627
|
+
# TypeScript, but `scripts/test-all.sh` shards the two suites separately — Guard 1
|
|
628
|
+
# with the web-platform group, this file with the scripts group — so they do not
|
|
629
|
+
# always run together and neither is redundant with the other.
|
|
630
|
+
#
|
|
631
|
+
# Presence of the manifest check was never the property worth asserting: every gate
|
|
632
|
+
# carries a SECOND fail-closed check (`[[ -r "$SENTINEL" ]] || { …; exit 2; }`)
|
|
633
|
+
# immediately below, so "the file contains a preflight AND contains an exit 2" is
|
|
634
|
+
# satisfied with the preflight's own arm converted to a no-op. This now requires the
|
|
635
|
+
# halt to sit inside the preflight's OWN brace group, tracked by brace depth.
|
|
636
|
+
preflight_halts_in_own_arm() {
|
|
637
|
+
awk '
|
|
638
|
+
index($0, "\"${CLAUDE_PLUGIN_ROOT}/.claude-plugin/plugin.json\"") && !started { started = 1 }
|
|
639
|
+
started {
|
|
640
|
+
stmt = stmt $0 "\n"
|
|
641
|
+
n = gsub(/\{/, "{"); depth += n
|
|
642
|
+
n = gsub(/\}/, "}"); depth -= n
|
|
643
|
+
# Statement ends when braces balance and no continuation is pending.
|
|
644
|
+
if (depth <= 0 && $0 !~ /\\[[:space:]]*$/) {
|
|
645
|
+
print (stmt ~ /exit 2/) ? "BOUND" : "UNBOUND"
|
|
646
|
+
exit
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
END { if (!started) print "ABSENT" }
|
|
650
|
+
' "$1"
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
for gate_file in "${INCIDENT_SKILL}" "${LEGAL_SKILL}" "${LINEAR_SKILL}"; do
|
|
654
|
+
n=$(grep -Fc "${PREFLIGHT_NEEDLE}" "${gate_file}" 2>/dev/null || true)
|
|
655
|
+
if [[ "${n}" -lt 1 ]]; then
|
|
656
|
+
missing_preflight="${missing_preflight} ${gate_file##*/skills/}"
|
|
657
|
+
continue
|
|
658
|
+
fi
|
|
659
|
+
verdict="$(preflight_halts_in_own_arm "${gate_file}")"
|
|
660
|
+
[[ "${verdict}" == "BOUND" ]] || unbound_halt="${unbound_halt} ${gate_file##*/skills/}(${verdict})"
|
|
661
|
+
done
|
|
662
|
+
|
|
663
|
+
if [[ -z "${missing_preflight}" && -z "${unbound_halt}" ]]; then
|
|
664
|
+
echo "PASS: Test 18b: identity preflight present at all 3 secret-gate sites, and each HALTS IN ITS OWN ARM"
|
|
665
|
+
PASS=$((PASS + 1))
|
|
666
|
+
else
|
|
667
|
+
[[ -n "${missing_preflight}" ]] && echo "FAIL: Test 18b: identity preflight missing at:${missing_preflight} — the bare anchor is NOT safe by construction; the preflight is what carries it (ADR-179 decision 2)"
|
|
668
|
+
[[ -n "${unbound_halt}" ]] && echo "FAIL: Test 18b: identity preflight present but its own arm does not exit 2 at:${unbound_halt} — a sibling halt elsewhere in the fence does not make THIS check fail-closed"
|
|
669
|
+
FAIL=$((FAIL + 1))
|
|
670
|
+
fi
|
|
671
|
+
|
|
672
|
+
# 18c — corpus-wide negative. This single assertion mechanically closes ADR-179 §R5 and stops the
|
|
673
|
+
# rejected form reappearing ANYWHERE under the payload, which per-file counts structurally cannot
|
|
674
|
+
# do. The needle is built by concatenation so this file does not match its own assertion — the
|
|
675
|
+
# same reason the suite splits token-shaped fixtures.
|
|
676
|
+
#
|
|
677
|
+
# The split point sits INSIDE the rev-parse argument, not at the variable-name boundary. Splitting
|
|
678
|
+
# at the boundary leaves the shorter default-arm substring contiguous, and that shorter form is the
|
|
679
|
+
# needle this PR's own AC1 sweep greps for — so the file would report ITSELF as an unmigrated site
|
|
680
|
+
# while its own assertion passed. Do not restore the literal to this comment for readability: a
|
|
681
|
+
# body-grep cannot tell an explanation from an occurrence.
|
|
682
|
+
FORBIDDEN='${CLAUDE_PLUGIN_ROOT:-$(git rev-parse ''--show-toplevel)'
|
|
683
|
+
SEARCH_ROOT="${REPO_ROOT}/plugins/soleur/"
|
|
684
|
+
|
|
685
|
+
# ANTI-VACUITY FLOOR (#7450 review-finding A12). `grep -r` on a nonexistent root exits
|
|
686
|
+
# 2, `|| true` swallows it, `residual` is empty and the assertion reports a clean
|
|
687
|
+
# corpus — a PASS that means "the scan could not run", which is the one outcome a
|
|
688
|
+
# corpus-wide negative must never render as good. Two positive controls: the root must
|
|
689
|
+
# exist, and a literal KNOWN to be present must actually be found. If the apparatus is
|
|
690
|
+
# broken, the control fails loudly instead of the negative passing quietly.
|
|
691
|
+
control_hits=$(grep -rlF '${CLAUDE_PLUGIN_ROOT}' "${SEARCH_ROOT}" 2>/dev/null | grep -c . || true)
|
|
692
|
+
if [[ ! -d "${SEARCH_ROOT}" ]]; then
|
|
693
|
+
echo "FAIL: Test 18c: search root ${SEARCH_ROOT} does not exist — the corpus-wide negative cannot be evaluated (a vacuous PASS is not a clean corpus)"
|
|
694
|
+
FAIL=$((FAIL + 1))
|
|
695
|
+
elif [[ "${control_hits}" -lt 10 ]]; then
|
|
696
|
+
echo "FAIL: Test 18c: positive control found only ${control_hits} files containing the bare anchor under ${SEARCH_ROOT} — expected >=10; the scan apparatus is broken, so its empty result proves nothing"
|
|
697
|
+
FAIL=$((FAIL + 1))
|
|
698
|
+
else
|
|
699
|
+
residual=$(grep -rlF "${FORBIDDEN}" "${SEARCH_ROOT}" 2>/dev/null || true)
|
|
700
|
+
if [[ -z "${residual}" ]]; then
|
|
701
|
+
echo "PASS: Test 18c: zero git-root-defaulted plugin-root anchors remain anywhere under plugins/soleur/ (control: ${control_hits} files scanned and matched)"
|
|
702
|
+
PASS=$((PASS + 1))
|
|
703
|
+
else
|
|
704
|
+
echo "FAIL: Test 18c: the rejected git-root-default form survives at:"
|
|
705
|
+
echo "${residual}" | sed 's/^/ /'
|
|
706
|
+
FAIL=$((FAIL + 1))
|
|
707
|
+
fi
|
|
708
|
+
fi
|
|
709
|
+
|
|
710
|
+
# ---------------------------------------------------------------------------
|
|
711
|
+
# Test 19 — DECOY POSITIVE CONTROL for the untrusted-anchor vector (#7450, ADR-179).
|
|
712
|
+
#
|
|
713
|
+
# Test 18 above pins the two call sites to each OTHER. Nothing pinned either of them to a
|
|
714
|
+
# path the reviewed party cannot control — which is the actual defect: `review/SKILL.md`
|
|
715
|
+
# instructs `gh pr checkout`, so on the review path `$(git rev-parse --show-toplevel)` IS the
|
|
716
|
+
# contributor's tree, and the gate whose exit code decides whether secrets are emitted resolves
|
|
717
|
+
# its own scanner from there.
|
|
718
|
+
#
|
|
719
|
+
# The oracle is the anchor expression AS COMMITTED in incident/SKILL.md, extracted at runtime.
|
|
720
|
+
# It is deliberately NOT duplicated into this file: a guard whose expected value is a literal
|
|
721
|
+
# copied from the artifact it guards is a set-difference over one producer, and goes vacuous the
|
|
722
|
+
# moment someone edits both together
|
|
723
|
+
# (learnings/2026-07-19-a-self-graded-mutation-battery-went-vacuous-twice-in-one-pr...).
|
|
724
|
+
#
|
|
725
|
+
# 19a proves the decoy is a LIVE hazard rather than an inert file — the real sentinel flags the
|
|
726
|
+
# same fixture that the decoy waves through. A positive control that could not have passed
|
|
727
|
+
# proves nothing.
|
|
728
|
+
# ---------------------------------------------------------------------------
|
|
729
|
+
t19_tree="${TMP_DIR}/t19-contributor-tree"
|
|
730
|
+
mkdir -p "${t19_tree}/plugins/soleur/skills/incident/scripts" || {
|
|
731
|
+
echo "FAIL: Test 19: could not build the contributor-tree sandbox — a harness that cannot SET UP must abort, not report a verdict about the SUT" >&2
|
|
732
|
+
exit 2
|
|
733
|
+
}
|
|
734
|
+
git -C "${t19_tree}" init -q >/dev/null 2>&1
|
|
735
|
+
# Compare physical paths: mktemp may hand back a symlinked prefix while
|
|
736
|
+
# `git rev-parse --show-toplevel` always reports the resolved one, which would make the
|
|
737
|
+
# containment test below silently false-PASS.
|
|
738
|
+
t19_tree="$(cd "${t19_tree}" && pwd -P)"
|
|
739
|
+
t19_decoy="${t19_tree}/plugins/soleur/skills/incident/scripts/redact-sentinel.sh"
|
|
740
|
+
printf '#!/usr/bin/env bash\nexit 0\n' > "${t19_decoy}"
|
|
741
|
+
chmod 0755 "${t19_decoy}"
|
|
742
|
+
|
|
743
|
+
# 19a — the decoy is a live hazard: it passes a file the real sentinel rejects.
|
|
744
|
+
t19_secret="${TMP_DIR}/t19-secret.md"
|
|
745
|
+
python3 -c "import sys; sys.stdout.write('anthropic_key: ' + 'sk-' + 'ant-api03-' + 'A'*32 + '\n')" > "${t19_secret}"
|
|
746
|
+
bash "${SENTINEL}" "${t19_secret}" >/dev/null 2>&1
|
|
747
|
+
t19_real_rc=$?
|
|
748
|
+
bash "${t19_decoy}" "${t19_secret}" >/dev/null 2>&1
|
|
749
|
+
t19_decoy_rc=$?
|
|
750
|
+
if [[ "${t19_real_rc}" -eq 1 && "${t19_decoy_rc}" -eq 0 ]]; then
|
|
751
|
+
echo "PASS: Test 19a: decoy is a live hazard (real sentinel exit=1 on the fixture, decoy exit=0)"
|
|
752
|
+
PASS=$((PASS + 1))
|
|
753
|
+
else
|
|
754
|
+
echo "FAIL: Test 19a: decoy is not a valid positive control (real=${t19_real_rc} expected 1, decoy=${t19_decoy_rc} expected 0)"
|
|
755
|
+
FAIL=$((FAIL + 1))
|
|
756
|
+
fi
|
|
757
|
+
|
|
758
|
+
# 19b — extract the anchor from the committed producer, and refuse to run vacuously.
|
|
759
|
+
#
|
|
760
|
+
# THREE hardenings over the original (#7450 review):
|
|
761
|
+
#
|
|
762
|
+
# (1) BASH FENCES ONLY, and exactly one assignment per file. The old extractor was a
|
|
763
|
+
# whole-file `sed … | head -1`, so an EARLIER ```text doc example carrying a
|
|
764
|
+
# well-formed assignment became the oracle and silently retargeted 19b/19c away
|
|
765
|
+
# from the live gate — oracle shadowing, measured. `head -1` is what made it
|
|
766
|
+
# silent: a second match was not a conflict, it was simply discarded.
|
|
767
|
+
# (2) (SENTINEL|SCRUBBER), not SENTINEL alone. The decoy control never reached
|
|
768
|
+
# linear-fetch, which the PR body calls the highest residual risk, because its
|
|
769
|
+
# variable is named SCRUBBER.
|
|
770
|
+
# (3) Both gate files are driven, not just incident.
|
|
771
|
+
extract_gate_anchor() {
|
|
772
|
+
awk '
|
|
773
|
+
/^[[:space:]]*```bash[[:space:]]*$/ { infence = 1; next }
|
|
774
|
+
/^[[:space:]]*```/ { infence = 0; next }
|
|
775
|
+
infence && /^[[:space:]]*(SENTINEL|SCRUBBER)="[^"]*"[[:space:]]*$/ {
|
|
776
|
+
line = $0
|
|
777
|
+
sub(/^[[:space:]]*(SENTINEL|SCRUBBER)="/, "", line)
|
|
778
|
+
sub(/"[[:space:]]*$/, "", line)
|
|
779
|
+
print line
|
|
780
|
+
}
|
|
781
|
+
' "$1"
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
for t19_gate in "${INCIDENT_SKILL}" "${LINEAR_SKILL}"; do
|
|
785
|
+
t19_name="${t19_gate##*/skills/}"
|
|
786
|
+
t19_anchors="$(extract_gate_anchor "${t19_gate}")"
|
|
787
|
+
t19_n=$(printf '%s\n' "${t19_anchors}" | grep -c . || true)
|
|
788
|
+
# DISTINCT values, not occurrences. The hazard this pins is a doc example or a second,
|
|
789
|
+
# DIFFERING assignment shadowing the real anchor — so the property is "the file names
|
|
790
|
+
# exactly one anchor value", not "it names it exactly once".
|
|
791
|
+
#
|
|
792
|
+
# Requiring a single occurrence was a proxy, and it was wrong in the fail-CLOSED direction:
|
|
793
|
+
# `linear-fetch` must legitimately re-derive `SCRUBBER` in its Phase D fence, because each
|
|
794
|
+
# fenced block is a separate Bash call and shell state does not persist across them. The
|
|
795
|
+
# single-occurrence form rejected the correct code (and, before it was fixed, accepted the
|
|
796
|
+
# bricked version that defined the variable in one fence and used it in another).
|
|
797
|
+
# Two byte-identical assignments cannot shadow each other; two differing ones can, and
|
|
798
|
+
# still fail here.
|
|
799
|
+
t19_distinct=$(printf '%s\n' "${t19_anchors}" | grep . | sort -u | grep -c . || true)
|
|
800
|
+
|
|
801
|
+
if [[ "${t19_n}" -lt 1 || "${t19_distinct}" -ne 1 ]]; then
|
|
802
|
+
echo "FAIL: Test 19b[${t19_name}]: expected exactly ONE DISTINCT gate-anchor value in bash fences, found ${t19_distinct} distinct across ${t19_n} assignment(s) — a differing second assignment can shadow the real anchor, and an extractor that takes the first of several would not notice"
|
|
803
|
+
FAIL=$((FAIL + 1))
|
|
804
|
+
continue
|
|
805
|
+
fi
|
|
806
|
+
# The UNIQUE value — asserted to be the only distinct one above. Taking the raw multi-line
|
|
807
|
+
# capture would leave `grep -F` treating the embedded newline as a pattern separator, so a
|
|
808
|
+
# match on either line would satisfy it; that happens to work while the values are identical
|
|
809
|
+
# and stops working the moment they are not, which is exactly the case being guarded.
|
|
810
|
+
t19_expr="$(printf '%s\n' "${t19_anchors}" | grep . | sort -u)"
|
|
811
|
+
|
|
812
|
+
if grep -Fq "${t19_expr}" "${t19_gate}"; then
|
|
813
|
+
echo "PASS: Test 19b[${t19_name}]: anchor expression extracted from the committed SKILL.md"
|
|
814
|
+
PASS=$((PASS + 1))
|
|
815
|
+
else
|
|
816
|
+
echo "FAIL: Test 19b[${t19_name}]: anchor extraction is vacuous — extracted text absent from the file"
|
|
817
|
+
FAIL=$((FAIL + 1))
|
|
818
|
+
continue
|
|
819
|
+
fi
|
|
820
|
+
|
|
821
|
+
# 19c — resolve the committed expression the way a reviewer's session would: plugin
|
|
822
|
+
# root UNRESOLVED, CWD inside the checked-out contributor tree.
|
|
823
|
+
#
|
|
824
|
+
# (a) NEVER bash-expand this text. On the review path the SKILL.md is the CONTRIBUTOR's
|
|
825
|
+
# file, so the previous form — writing the expression into a script and running it
|
|
826
|
+
# — handed arbitrary command substitution from a hostile PR straight to bash inside
|
|
827
|
+
# the reviewer's own test run. The guard for the untrusted-anchor vector was itself
|
|
828
|
+
# an untrusted-input execution sink. Metacharacters are now a REFUSAL, not input.
|
|
829
|
+
#
|
|
830
|
+
# (b) RESOLVE BEFORE CONTAINMENT. The old `case` compared an UNRESOLVED string against
|
|
831
|
+
# an absolute `${t19_tree}/*` prefix, so a bare RELATIVE path never matched the
|
|
832
|
+
# prefix and therefore always "passed" — while resolving, by definition, against
|
|
833
|
+
# the caller's CWD, which is the contributor tree. The relative case is the
|
|
834
|
+
# failure, so it must be tested as one.
|
|
835
|
+
case "${t19_expr}" in
|
|
836
|
+
*'$('*|*'`'*|*';'*|*'|'*|*'&'*|*'>'*|*'<'*)
|
|
837
|
+
echo "FAIL: Test 19c[${t19_name}]: anchor expression carries shell metacharacters (${t19_expr}) — refusing to expand contributor-controlled text"
|
|
838
|
+
FAIL=$((FAIL + 1))
|
|
839
|
+
continue ;;
|
|
840
|
+
*'${CLAUDE_PLUGIN_ROOT:'*)
|
|
841
|
+
echo "FAIL: Test 19c[${t19_name}]: anchor uses a :- / :? default (${t19_expr}) — ADR-179 rejects both; the default arm is the vector"
|
|
842
|
+
FAIL=$((FAIL + 1))
|
|
843
|
+
continue ;;
|
|
844
|
+
esac
|
|
845
|
+
|
|
846
|
+
# Pure textual substitution of the unresolved root — no shell evaluation.
|
|
847
|
+
t19_resolved="${t19_expr//\$\{CLAUDE_PLUGIN_ROOT\}/}"
|
|
848
|
+
case "${t19_resolved}" in
|
|
849
|
+
/*) case "${t19_resolved}/" in
|
|
850
|
+
"${t19_tree}"/*) t19_inside=1 ;;
|
|
851
|
+
*) t19_inside=0 ;;
|
|
852
|
+
esac ;;
|
|
853
|
+
# Relative: resolves against the caller's CWD, i.e. INTO the contributor tree.
|
|
854
|
+
*) t19_inside=1 ;;
|
|
855
|
+
esac
|
|
856
|
+
|
|
857
|
+
if [[ -n "${t19_resolved}" && "${t19_inside}" -eq 0 ]]; then
|
|
858
|
+
echo "PASS: Test 19c[${t19_name}]: committed anchor resolves outside the contributor tree (${t19_resolved})"
|
|
859
|
+
PASS=$((PASS + 1))
|
|
860
|
+
else
|
|
861
|
+
echo "FAIL: Test 19c[${t19_name}]: committed anchor resolves INTO the reviewed party's tree — a hostile PR's gate script would run as the gate (resolved=${t19_resolved:-<empty>}, tree=${t19_tree})"
|
|
862
|
+
FAIL=$((FAIL + 1))
|
|
863
|
+
fi
|
|
864
|
+
done
|
|
865
|
+
|
|
866
|
+
# ---------------------------------------------------------------------------
|
|
867
|
+
# Test 20 — CROSS-FILE anti-vacuity floor for the Guard 1 skills-axis block.
|
|
868
|
+
#
|
|
869
|
+
# Guard 1's own floor is closure-scoped to its `describe`, so DELETING THE WHOLE
|
|
870
|
+
# #7450 BLOCK was measured green at 9 tests: the floor died with the thing it
|
|
871
|
+
# counted. An in-file floor structurally cannot survive the deletion of its own
|
|
872
|
+
# file or block, so the floor that can must live somewhere else.
|
|
873
|
+
#
|
|
874
|
+
# Here specifically, and not in a sibling vitest file: `scripts/test-all.sh` shards
|
|
875
|
+
# these two suites differently (Guard 1 under the web-platform group, this one under
|
|
876
|
+
# the scripts group), so they do not always run together. A floor in the same shard
|
|
877
|
+
# would be reaped by the same skip that reaped the thing it guards.
|
|
878
|
+
# ---------------------------------------------------------------------------
|
|
879
|
+
T20_GUARD="${REPO_ROOT}/apps/web-platform/test/plugin-root-anchoring.test.ts"
|
|
880
|
+
T20_FLOOR=18
|
|
881
|
+
|
|
882
|
+
if [[ ! -f "${T20_GUARD}" ]]; then
|
|
883
|
+
echo "FAIL: Test 20: Guard 1 is GONE (${T20_GUARD}) — the skills-axis anchoring assertions no longer exist"
|
|
884
|
+
FAIL=$((FAIL + 1))
|
|
885
|
+
elif ! grep -Fq 'skills secret-gate subset (#7450)' "${T20_GUARD}"; then
|
|
886
|
+
echo "FAIL: Test 20: Guard 1 no longer contains the #7450 skills secret-gate describe block — deleting it is exactly what its own in-file floor cannot detect"
|
|
887
|
+
FAIL=$((FAIL + 1))
|
|
888
|
+
else
|
|
889
|
+
# SCOPED to the #7450 describe, not `| tail -1` over the whole file.
|
|
890
|
+
#
|
|
891
|
+
# The old form took the LAST `expect(assertions).toBe(N)` anywhere in the guard as a proxy
|
|
892
|
+
# for "Guard 1's floor". That is correct only while #7450 is the final block in the file.
|
|
893
|
+
# PR #8570 appended a third describe (the #7453 skills-ratchet axis, floor 8) AFTER it, so
|
|
894
|
+
# `tail -1` silently started reading the new block's floor and this test failed claiming the
|
|
895
|
+
# #7450 floor had been "lowered" to 8 — naming a cause that had not happened, on a file whose
|
|
896
|
+
# #7450 block was untouched. A positional proxy for a named thing breaks the moment the
|
|
897
|
+
# corpus grows, which is the exact class this suite exists to catch elsewhere.
|
|
898
|
+
#
|
|
899
|
+
# Range ends at the next line beginning `describe(`; sed searches the end pattern from the
|
|
900
|
+
# line AFTER the start, so the opening describe cannot close its own range. `head -1` takes
|
|
901
|
+
# the block's own floor.
|
|
902
|
+
t20_declared=$(sed -n '/^describe(.*#7450/,/^describe(/{ s/^[[:space:]]*expect(assertions)\.toBe(\([0-9]\+\));[[:space:]]*$/\1/p }' "${T20_GUARD}" | head -1)
|
|
903
|
+
if [[ "${t20_declared}" != "${T20_FLOOR}" ]]; then
|
|
904
|
+
echo "FAIL: Test 20: Guard 1's #7450 assertion floor is '${t20_declared:-<none>}', expected ${T20_FLOOR} — a floor lowered in the same commit that removes assertions is the failure mode this pins"
|
|
905
|
+
FAIL=$((FAIL + 1))
|
|
906
|
+
else
|
|
907
|
+
echo "PASS: Test 20: Guard 1's #7450 block is present and still declares ${T20_FLOOR} decided assertions (cross-file, cross-shard floor)"
|
|
908
|
+
PASS=$((PASS + 1))
|
|
909
|
+
fi
|
|
910
|
+
fi
|
|
911
|
+
|
|
912
|
+
# ---------------------------------------------------------------------------
|
|
913
|
+
# SHARED EXECUTABLE-LINE EXTRACTOR for Tests 21, 22 and 23.
|
|
914
|
+
#
|
|
915
|
+
# THE RECURRING DEFECT THIS CLOSES. Tests 22, 23 and 24 were written in the same two commits
|
|
916
|
+
# as Test 21 and asserted against the file's BYTES, not its executable content — so each was
|
|
917
|
+
# defeated by a COMMENT. Test 21 alone built an extractor, and its own comment argues at
|
|
918
|
+
# length why that scoping is load-bearing. Measured at round-2 review: deleting the real
|
|
919
|
+
# `[ -n "$PERSIST_SAFE" ]` arm and leaving a three-line comment quoting it kept BOTH Test 22
|
|
920
|
+
# and Test 23 green.
|
|
921
|
+
#
|
|
922
|
+
# So the extractor is shared rather than duplicated: a future assertion inherits the immunity
|
|
923
|
+
# instead of having to remember it.
|
|
924
|
+
#
|
|
925
|
+
# FENCE HANDLING, every arm added because a mutation walked through it:
|
|
926
|
+
# V1 ```sh / ```shell / ```zsh — an agent executes those exactly as it executes ```bash.
|
|
927
|
+
# This was the cheapest evasion of all: one word.
|
|
928
|
+
# V2 ~~~ fences, which CommonMark treats identically to ```.
|
|
929
|
+
# P2 a fence that is opened and never closed, and a stray ``` INSIDE a fence (a heredoc
|
|
930
|
+
# emitting markdown) closing it early — so the close marker must match the opening
|
|
931
|
+
# run length, and an unclosed fence is reported rather than silently swallowing the
|
|
932
|
+
# rest of the file.
|
|
933
|
+
# Info strings (```bash title="x") and CRLF were already handled and are kept.
|
|
934
|
+
# ---------------------------------------------------------------------------
|
|
935
|
+
exec_lines() {
|
|
936
|
+
awk '
|
|
937
|
+
# opening fence: ``` or ~~~ (>=3), a shell-ish language, optional info string
|
|
938
|
+
!infence && match($0, /^[[:space:]]*(`{3,}|~{3,})[[:space:]]*(bash|sh|shell|zsh)([[:space:]]|$)/) {
|
|
939
|
+
line = $0
|
|
940
|
+
sub(/^[[:space:]]*/, "", line)
|
|
941
|
+
run = 0; ch = substr(line, 1, 1)
|
|
942
|
+
while (substr(line, run + 1, 1) == ch) run++
|
|
943
|
+
infence = 1; fch = ch; frun = run; next
|
|
944
|
+
}
|
|
945
|
+
# a non-shell fence still OPENS a fence — otherwise its contents leak into the scan
|
|
946
|
+
!infence && match($0, /^[[:space:]]*(`{3,}|~{3,})/) {
|
|
947
|
+
line = $0; sub(/^[[:space:]]*/, "", line)
|
|
948
|
+
run = 0; ch = substr(line, 1, 1)
|
|
949
|
+
while (substr(line, run + 1, 1) == ch) run++
|
|
950
|
+
infence = 2; fch = ch; frun = run; next
|
|
951
|
+
}
|
|
952
|
+
# closing fence: same char, at least the opening run length, nothing but the marker
|
|
953
|
+
infence && match($0, /^[[:space:]]*(`{3,}|~{3,})[[:space:]]*\r?$/) {
|
|
954
|
+
line = $0; sub(/^[[:space:]]*/, "", line)
|
|
955
|
+
run = 0; ch = substr(line, 1, 1)
|
|
956
|
+
while (substr(line, run + 1, 1) == ch) run++
|
|
957
|
+
if (ch == fch && run >= frun) { infence = 0 }
|
|
958
|
+
next
|
|
959
|
+
}
|
|
960
|
+
# SHELL COMMENTS INSIDE THE FENCE ARE NOT EXECUTABLE EITHER.
|
|
961
|
+
# Fence-scoping alone is not enough and this was measured: with the fence scoping in
|
|
962
|
+
# place, deleting the real `[ -n "$PERSIST_SAFE" ]` arm and leaving a comment quoting it
|
|
963
|
+
# still passed Test 23, because the comment sits inside the ```bash fence. "Executable
|
|
964
|
+
# content" has to mean executable, not merely fenced.
|
|
965
|
+
infence == 1 && /^[[:space:]]*#/ { next }
|
|
966
|
+
infence == 1 { print }
|
|
967
|
+
END { if (infence) exit 3 }
|
|
968
|
+
' "$1"
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
# ---------------------------------------------------------------------------
|
|
972
|
+
# Test 21 — B1: PIN THE ABSENCE of any git-root resolution at the three secret gates.
|
|
973
|
+
#
|
|
974
|
+
# Review finding §B1 prescribed adding, at each gate,
|
|
975
|
+
# case "$(cd "${CLAUDE_PLUGIN_ROOT}" && pwd -P)/" in "$(git rev-parse --show-toplevel)/"*) halt;;
|
|
976
|
+
# That was measured and REJECTED (specs/.../b1-disposition.md): the loader substitutes the bare
|
|
977
|
+
# token at delivery, so the operand is a literal the adversary cannot reach — while the
|
|
978
|
+
# assertion breaks dogfooding on any plain clone (there the plugin root IS inside the working
|
|
979
|
+
# tree) and puts `git rev-parse --show-toplevel` back into the exact three files this PR
|
|
980
|
+
# removes it from.
|
|
981
|
+
#
|
|
982
|
+
# A disposition that is merely SATISFIED is not guarded, so this pins it. Deliberately broader
|
|
983
|
+
# than 18c, which is keyed to the `${CLAUDE_PLUGIN_ROOT:-...}` SYNTAX and so cannot see a bare
|
|
984
|
+
# `$(git rev-parse --show-toplevel)` — precisely the shape §B1 proposes.
|
|
985
|
+
# ---------------------------------------------------------------------------
|
|
986
|
+
t21_offenders=""
|
|
987
|
+
t21_unclosed=""
|
|
988
|
+
for gate_file in "${INCIDENT_SKILL}" "${LEGAL_SKILL}" "${LINEAR_SKILL}"; do
|
|
989
|
+
if ! exec_lines "${gate_file}" >/dev/null 2>&1; then
|
|
990
|
+
t21_unclosed="${t21_unclosed} ${gate_file##*/skills/}"
|
|
991
|
+
continue
|
|
992
|
+
fi
|
|
993
|
+
n=$(exec_lines "${gate_file}" | grep -c 'git rev-parse' || true)
|
|
994
|
+
[[ "${n}" -ne 0 ]] && t21_offenders="${t21_offenders} ${gate_file##*/skills/}(${n})"
|
|
995
|
+
done
|
|
996
|
+
|
|
997
|
+
# ANTI-VACUITY: PER-FILE required membership, never a total count.
|
|
998
|
+
#
|
|
999
|
+
# A `>= N` total fails OPEN on shrinkage — measured, the previous floor was `>= 3` against a
|
|
1000
|
+
# real population of 15 (5 anchor lines per gate), so a one-character regression that blinded
|
|
1001
|
+
# every INDENTED fence dropped `legal-generate` to 0 of its 5 while the other two still
|
|
1002
|
+
# supplied 10, and B1's `case` planted in the blinded file stayed GREEN. That is finding A10
|
|
1003
|
+
# reintroduced — and required-membership was the fix already applied to Test 24's floor in the
|
|
1004
|
+
# same commit, and not carried across to its sibling.
|
|
1005
|
+
t21_blind=""
|
|
1006
|
+
for gate_file in "${INCIDENT_SKILL}" "${LEGAL_SKILL}" "${LINEAR_SKILL}"; do
|
|
1007
|
+
c=$(exec_lines "${gate_file}" 2>/dev/null | grep -c 'CLAUDE_PLUGIN_ROOT' || true)
|
|
1008
|
+
[[ "${c}" -ge 1 ]] || t21_blind="${t21_blind} ${gate_file##*/skills/}"
|
|
1009
|
+
done
|
|
1010
|
+
|
|
1011
|
+
if [[ -n "${t21_unclosed}" ]]; then
|
|
1012
|
+
echo "FAIL: Test 21: unterminated bash fence in:${t21_unclosed} — the extractor cannot bound the executable region, so every verdict over this file would be unreliable"
|
|
1013
|
+
FAIL=$((FAIL + 1))
|
|
1014
|
+
elif [[ -n "${t21_blind}" ]]; then
|
|
1015
|
+
echo "FAIL: Test 21: the fence extractor sees ZERO anchor lines in:${t21_blind} — it is blind to that gate's executable content, so a zero-offender verdict there is vacuous (per-file floor, not a total: a total lets one gate go dark while its siblings carry the count)"
|
|
1016
|
+
FAIL=$((FAIL + 1))
|
|
1017
|
+
elif [[ -z "${t21_offenders}" ]]; then
|
|
1018
|
+
echo "PASS: Test 21: all 3 secret gates carry ZERO git-root resolution in executable positions, and the extractor sees every gate (the B1 invariant is pinned, not merely satisfied)"
|
|
1019
|
+
PASS=$((PASS + 1))
|
|
1020
|
+
else
|
|
1021
|
+
echo "FAIL: Test 21: git-root resolution reappeared in an executable fence at:${t21_offenders} — see specs/feat-one-shot-7450-git-root-anchor-untrusted/b1-disposition.md. If this is a deliberate reversal, that document must be superseded FIRST; a gate that resolves the git root is the #7450 vector itself"
|
|
1022
|
+
FAIL=$((FAIL + 1))
|
|
1023
|
+
fi
|
|
1024
|
+
|
|
1025
|
+
# ---------------------------------------------------------------------------
|
|
1026
|
+
# Test 22 — C10: every fail-closed halt on the anchor path emits a SOLEUR_* marker,
|
|
1027
|
+
# ON STDOUT, FROM AN EXECUTABLE LINE.
|
|
1028
|
+
#
|
|
1029
|
+
# `go.md` and `sync.md` already emit one and `go.md` documents why: a refusal nobody can see
|
|
1030
|
+
# in telemetry is indistinguishable from a run that never happened.
|
|
1031
|
+
#
|
|
1032
|
+
# THREE evasions this now closes, all measured green against the previous version:
|
|
1033
|
+
# W1 the marker present only in PROSE while the real echo was silenced — the whole point
|
|
1034
|
+
# of `exec_lines`.
|
|
1035
|
+
# W2 `exit 2; } >&2` — the redirect on the BRACE GROUP rather than the marker line, so a
|
|
1036
|
+
# substring test on the marker line alone sees nothing.
|
|
1037
|
+
# W5 `>&"2"` and `1>&2` spellings.
|
|
1038
|
+
# And W3, the growth gap: the reason table is hardcoded, so a NEW fail-closed halt added with
|
|
1039
|
+
# no marker passed silently. The cardinality check below closes that — the number of halts and
|
|
1040
|
+
# the number of markers must agree per file.
|
|
1041
|
+
# ---------------------------------------------------------------------------
|
|
1042
|
+
t22_missing=""
|
|
1043
|
+
t22_on_stderr=""
|
|
1044
|
+
t22_uncovered=""
|
|
1045
|
+
t22_specs="${INCIDENT_SKILL}:SOLEUR_INCIDENT_HALT:plugin-root-unverified,sentinel-unreadable,draft-alloc-failed,draft-empty,invalid-calendar-date,mttr-transposed,mttd-transposed
|
|
1046
|
+
${LEGAL_SKILL}:SOLEUR_LEGAL_GENERATE_HALT:plugin-root-unverified,sentinel-unreadable,draft-alloc-failed,draft-empty
|
|
1047
|
+
${LINEAR_SKILL}:SOLEUR_LINEAR_FETCH_HALT:plugin-root-unverified,scrubber-unreadable,scrubber-nonzero-exit,redaction-empty-output,redaction-ineffective
|
|
1048
|
+
${TRIGGER_SKILL}:SOLEUR_TRIGGER_CRON_HALT:plugin-root-unverified,producer-missing"
|
|
1049
|
+
|
|
1050
|
+
while IFS= read -r t22_spec; do
|
|
1051
|
+
[[ -n "${t22_spec}" ]] || continue
|
|
1052
|
+
t22_file="${t22_spec%%:*}"
|
|
1053
|
+
t22_rest="${t22_spec#*:}"
|
|
1054
|
+
t22_prefix="${t22_rest%%:*}"
|
|
1055
|
+
t22_reasons="${t22_rest#*:}"
|
|
1056
|
+
t22_exec="$(exec_lines "${t22_file}" 2>/dev/null)"
|
|
1057
|
+
|
|
1058
|
+
while IFS= read -r t22_reason; do
|
|
1059
|
+
[[ -n "${t22_reason}" ]] || continue
|
|
1060
|
+
# EXECUTABLE lines only, and the marker must be EMITTED (an `echo`/`printf` argument),
|
|
1061
|
+
# not merely mentioned.
|
|
1062
|
+
t22_line="$(printf '%s\n' "${t22_exec}" \
|
|
1063
|
+
| grep -E "(echo|printf)[^|]*${t22_prefix} reason=${t22_reason}" || true)"
|
|
1064
|
+
if [[ -z "${t22_line}" ]]; then
|
|
1065
|
+
t22_missing="${t22_missing} ${t22_file##*/skills/}:${t22_reason}"
|
|
1066
|
+
else
|
|
1067
|
+
# Inspect the MARKER'S OWN command segment, not the whole line. A single-line brace
|
|
1068
|
+
# group puts the marker echo and the human-guidance echo (`… >&2`) on one line, so a
|
|
1069
|
+
# line-wide test reports every correctly-written marker as stderr-bound — measured, it
|
|
1070
|
+
# flagged all three date halts the moment they were marked. Cut from the marker's echo
|
|
1071
|
+
# to the next `;`, which is where that command ends.
|
|
1072
|
+
t22_seg="$(printf '%s' "${t22_line}" \
|
|
1073
|
+
| sed -E "s/.*((echo|printf)[^;]*${t22_prefix} reason=${t22_reason}[^;]*).*/\\1/")"
|
|
1074
|
+
# ...AND the enclosing brace group. `{ echo "<marker>"; …; exit 2; } >&2` redirects the
|
|
1075
|
+
# WHOLE arm, including the marker, while leaving the marker's own segment clean — so a
|
|
1076
|
+
# per-command test alone reports it as compliant. Measured: that mutation landed and the
|
|
1077
|
+
# suite stayed green until this arm existed. Walk from the marker to the point where
|
|
1078
|
+
# brace depth returns to zero and inspect the closing line.
|
|
1079
|
+
t22_arm_redir="$(printf '%s\n' "${t22_exec}" | awk -v mark="${t22_prefix} reason=${t22_reason}" '
|
|
1080
|
+
index($0, mark) { armed = 1 }
|
|
1081
|
+
armed {
|
|
1082
|
+
n = gsub(/\{/, "{"); m = gsub(/\}/, "}")
|
|
1083
|
+
depth += n - m
|
|
1084
|
+
if (depth <= 0 && /\}/) {
|
|
1085
|
+
if ($0 ~ /\}[[:space:]]*[0-9]*>&[[:space:]]*"?2"?/) print "REDIR"
|
|
1086
|
+
armed = 0
|
|
1087
|
+
}
|
|
1088
|
+
}')"
|
|
1089
|
+
if printf '%s' "${t22_seg}" | grep -cE >/dev/null '>&[[:space:]]*"?2"?|[0-9]+>&' \
|
|
1090
|
+
|| [[ -n "${t22_arm_redir}" ]]; then
|
|
1091
|
+
t22_on_stderr="${t22_on_stderr} ${t22_file##*/skills/}:${t22_reason}"
|
|
1092
|
+
fi
|
|
1093
|
+
fi
|
|
1094
|
+
done < <(printf '%s\n' "${t22_reasons}" | tr ',' '\n')
|
|
1095
|
+
|
|
1096
|
+
# GROWTH FLOOR (W3): a new fail-closed halt must arrive with a marker. Count `exit 2`
|
|
1097
|
+
# dispatches and distinct markers in the executable region and require agreement.
|
|
1098
|
+
t22_exits=$(printf '%s\n' "${t22_exec}" | grep -c 'exit 2' || true)
|
|
1099
|
+
t22_marks=$(printf '%s\n' "${t22_exec}" | grep -oE "${t22_prefix} reason=[a-z-]+" | sort -u | grep -c . || true)
|
|
1100
|
+
if [[ "${t22_exits}" -gt "${t22_marks}" ]]; then
|
|
1101
|
+
t22_uncovered="${t22_uncovered} ${t22_file##*/skills/}(${t22_exits} halts vs ${t22_marks} markers)"
|
|
1102
|
+
fi
|
|
1103
|
+
done < <(printf '%s\n' "${t22_specs}")
|
|
1104
|
+
|
|
1105
|
+
if [[ -z "${t22_missing}" && -z "${t22_on_stderr}" && -z "${t22_uncovered}" ]]; then
|
|
1106
|
+
echo "PASS: Test 22: all 18 fail-closed halts across the 4 gates (incident 7, legal-generate 4, linear-fetch 5, trigger-cron 2) emit a distinct SOLEUR_*_HALT reason= marker, on STDOUT, from an executable line — and each gate's halt count is covered by its marker count"
|
|
1107
|
+
PASS=$((PASS + 1))
|
|
1108
|
+
else
|
|
1109
|
+
[[ -n "${t22_missing}" ]] && echo "FAIL: Test 22: fail-closed halt with no SOLEUR_* marker EMITTED from an executable line at:${t22_missing} — a marker that appears only in prose or a comment is documentation, and a halt nobody can see in telemetry is indistinguishable from a run that never happened"
|
|
1110
|
+
[[ -n "${t22_on_stderr}" ]] && echo "FAIL: Test 22: SOLEUR_* marker redirected to stderr at:${t22_on_stderr} — the human guidance belongs on stderr, but the MARKER is the machine-readable half and must go to stdout (check the brace group too: \`exit 2; } >&2\` redirects the whole arm)"
|
|
1111
|
+
[[ -n "${t22_uncovered}" ]] && echo "FAIL: Test 22: a gate has more fail-closed halts than distinct markers at:${t22_uncovered} — a new halt was added without one, and the reason table above cannot notice that on its own"
|
|
1112
|
+
FAIL=$((FAIL + 1))
|
|
1113
|
+
fi
|
|
1114
|
+
|
|
1115
|
+
# ---------------------------------------------------------------------------
|
|
1116
|
+
# Test 23 — C12/AC5d: the linear-fetch non-empty check is ASSERTED, not merely present.
|
|
1117
|
+
#
|
|
1118
|
+
# AC5d claimed "asserted by a test, not by inspection" and no test asserted it: deleting
|
|
1119
|
+
# `[ -n "$PERSIST_SAFE" ]` left the whole suite green.
|
|
1120
|
+
#
|
|
1121
|
+
# The first fix used `grep -A6` for the `exit 2`, and review defeated it two ways: a COMMENT
|
|
1122
|
+
# quoting the old arm satisfied the presence half (X1b), and `[ -n … ] || true` next to an
|
|
1123
|
+
# UNRELATED guard's `exit 2` satisfied the dispatch half (X2) — finding A2 reintroduced inside
|
|
1124
|
+
# the assertion whose own comment cites A2. A window cannot tell whose `exit 2` it found.
|
|
1125
|
+
#
|
|
1126
|
+
# So this reuses `preflight_halts_in_own_arm`'s technique, which already exists in this file
|
|
1127
|
+
# and solved exactly this: require the halt inside the check's OWN brace group, by brace depth.
|
|
1128
|
+
# ---------------------------------------------------------------------------
|
|
1129
|
+
t23_needle='[ -n "$PERSIST_SAFE" ]'
|
|
1130
|
+
t23_exec="$(exec_lines "${LINEAR_SKILL}" 2>/dev/null)"
|
|
1131
|
+
t23_n=$(printf '%s\n' "${t23_exec}" | grep -Fc "${t23_needle}" || true)
|
|
1132
|
+
t23_bound=$(printf '%s\n' "${t23_exec}" | awk -v needle="${t23_needle}" '
|
|
1133
|
+
index($0, needle) { armed = 1 }
|
|
1134
|
+
armed {
|
|
1135
|
+
n = gsub(/\{/, "{"); m = gsub(/\}/, "}")
|
|
1136
|
+
depth += n - m
|
|
1137
|
+
if ($0 ~ /exit[[:space:]]+2/) { found = 1 }
|
|
1138
|
+
if (armed && depth <= 0 && /\}/) { armed = 0 }
|
|
1139
|
+
}
|
|
1140
|
+
END { print (found ? 1 : 0) }
|
|
1141
|
+
')
|
|
1142
|
+
|
|
1143
|
+
if [[ "${t23_n}" -ge 1 && "${t23_bound}" -eq 1 ]]; then
|
|
1144
|
+
echo "PASS: Test 23: linear-fetch asserts non-empty redaction output on an executable line AND halts inside that check's OWN arm (AC5d is test-enforced)"
|
|
1145
|
+
PASS=$((PASS + 1))
|
|
1146
|
+
else
|
|
1147
|
+
echo "FAIL: Test 23: the linear-fetch non-empty guard is missing from executable lines or does not halt in its own arm (present=${t23_n}, halts-in-own-arm=${t23_bound}) — empty scrubber output would be persisted as persist_safe_summary and the callers' absent-artifact contract, which is what stops them substituting the bearer-URL-bearing agent_context, would never fire"
|
|
1148
|
+
FAIL=$((FAIL + 1))
|
|
1149
|
+
fi
|
|
1150
|
+
|
|
1151
|
+
# ---------------------------------------------------------------------------
|
|
1152
|
+
# Test 24 — STAKES-KEYED discovery, INVARIANT-keyed violation, NO allowlist.
|
|
1153
|
+
# (CTO ruling item 2, rewritten at #7450 round-2 review.)
|
|
1154
|
+
#
|
|
1155
|
+
# WHY THIS IS NOT KEYED ON SYNTAX. Test 18c is keyed to one literal,
|
|
1156
|
+
# `${CLAUDE_PLUGIN_ROOT:-$(git rev-parse …)}`. That is what let finding B2 hide: `trigger-cron`
|
|
1157
|
+
# invoked a script running `doppler secrets get … -c prd --plain` through the `./`-relative
|
|
1158
|
+
# `:-` form, so 18c truthfully printed "zero … remain anywhere under plugins/soleur/" while a
|
|
1159
|
+
# cheaper-to-exploit secret site sat one syntax away. A needle shaped like yesterday's bug
|
|
1160
|
+
# cannot see tomorrow's.
|
|
1161
|
+
#
|
|
1162
|
+
# The first version of THIS test then made the same mistake one level up: it keyed the
|
|
1163
|
+
# violation on `CLAUDE_PLUGIN_ROOT:-`, so it could not see the 16 credential scripts invoked
|
|
1164
|
+
# through a BARE repo-relative path — strictly worse, because a bare path is CWD-relative
|
|
1165
|
+
# unconditionally. So the assertion below is written as the INVARIANT:
|
|
1166
|
+
#
|
|
1167
|
+
# no credential-ACQUIRING script is reachable through a path that resolves relative to CWD
|
|
1168
|
+
#
|
|
1169
|
+
# `:-`, bare `plugins/soleur/…` and `./plugins/soleur/…` are three instances of one class. A
|
|
1170
|
+
# fourth spelling needs no new clause.
|
|
1171
|
+
#
|
|
1172
|
+
# WHY ACQUISITION, NOT ADJACENCY. The ruling says "reads a secret". Keying on credential-shaped
|
|
1173
|
+
# NAMES instead enrols every redaction engine, because token names are their needles — measured:
|
|
1174
|
+
# a name-keyed predicate matched `$SESSION_TOKENS` (LLM tokens), `$TRAILER_KEY` (a map key) and
|
|
1175
|
+
# `${LINEAR_CDN_PATTERNS}` (via `_PAT`), inflating the population by 9 with scripts that acquire
|
|
1176
|
+
# nothing. It also drags in `worktree-manager.sh`, which copies `.env*` files but acquires no
|
|
1177
|
+
# named credential — and which #7453 owns.
|
|
1178
|
+
#
|
|
1179
|
+
# The boundary, as a DEFINITION rather than an allowlist (so a future member is excluded by
|
|
1180
|
+
# reasoning, not by having been omitted from a list): a script that copies files which may
|
|
1181
|
+
# contain secrets, on a machine that has already granted it execution, adds no marginal exposure
|
|
1182
|
+
# beyond the generic arbitrary-code-execution that #7453 addresses for all ~105 deferred sites.
|
|
1183
|
+
# A script whose purpose is to FETCH a named production credential and act with it is a direct
|
|
1184
|
+
# exfiltration primitive when substituted.
|
|
1185
|
+
# ---------------------------------------------------------------------------
|
|
1186
|
+
|
|
1187
|
+
# A credential token inside a COMMENT is a mention, not an acquisition. Without this a
|
|
1188
|
+
# `# Never echo the API_KEY` line enrols a script that handles no credential, and the resulting
|
|
1189
|
+
# CI failure asserts something untrue about it.
|
|
1190
|
+
t24_strip_comments() {
|
|
1191
|
+
case "$1" in
|
|
1192
|
+
*.ts|*.mjs|*.cjs) sed -E 's,//.*,,' "$1" | grep -vE '^[[:space:]]*\*' ;;
|
|
1193
|
+
*) grep -vE '^[[:space:]]*#' "$1" ;;
|
|
1194
|
+
esac
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
# Keyed on the ACT of obtaining or presenting a credential. `read -[a-z]*s` (not `read -s`)
|
|
1198
|
+
# because the real sites write `read -rs`; that exact miss cost a false-clean during the ruling.
|
|
1199
|
+
t24_acq_re='doppler secrets (get|download)|op read |vault kv get|gh auth token|read -[a-z]*s |Authorization: *[A-Za-z]|-H .Authorization|process\.env\.[A-Z_]*(TOKEN|_KEY|SECRET|PASSWORD)|env\("[A-Z_]*(TOKEN|_KEY|SECRET|PASSWORD)'
|
|
1200
|
+
|
|
1201
|
+
t24_acquirers=()
|
|
1202
|
+
while IFS= read -r t24_f; do
|
|
1203
|
+
t24_strip_comments "${t24_f}" | grep -cE >/dev/null "${t24_acq_re}" \
|
|
1204
|
+
&& t24_acquirers+=("${t24_f#"${REPO_ROOT}"/plugins/soleur/}")
|
|
1205
|
+
done < <(find "${REPO_ROOT}/plugins/soleur" -path '*/scripts/*' -type f \
|
|
1206
|
+
\( -name '*.sh' -o -name '*.py' -o -name '*.ts' -o -name '*.mjs' -o -name '*.cjs' \) \
|
|
1207
|
+
! -name '*.test.sh' 2>/dev/null | sort)
|
|
1208
|
+
|
|
1209
|
+
# ANTI-VACUITY: required MEMBERSHIP, never a count. A bare `>= N` floor fails OPEN on
|
|
1210
|
+
# shrinkage — measured, an early draft used `>= 5` against a population of 21 and a mutation
|
|
1211
|
+
# that gutted the predicate down to 9 members SURVIVED it. That is finding A10's shape.
|
|
1212
|
+
# These three are pinned because each is a DISTINCT acquisition mechanism; a predicate that
|
|
1213
|
+
# stops seeing any one of them has stopped doing its job whatever its total.
|
|
1214
|
+
# trigger.sh — secrets-manager read (`doppler secrets get … -c prd --plain`)
|
|
1215
|
+
# discord-setup.sh — presents a bot token (`Authorization: Bot` on curl's stdin config)
|
|
1216
|
+
# provision-hetzner.sh — silent interactive prompt (`read -rs`)
|
|
1217
|
+
# `community-router.sh` is deliberately NOT pinned: under an acquisition predicate it correctly
|
|
1218
|
+
# drops out, because its only credential mention is inside a routing-table string.
|
|
1219
|
+
t24_required=(
|
|
1220
|
+
"skills/trigger-cron/scripts/trigger.sh"
|
|
1221
|
+
"skills/community/scripts/discord-setup.sh"
|
|
1222
|
+
"skills/provision-hetzner/scripts/provision-hetzner.sh"
|
|
1223
|
+
)
|
|
1224
|
+
t24_missing_required=""
|
|
1225
|
+
for t24_req in "${t24_required[@]}"; do
|
|
1226
|
+
t24_found=0
|
|
1227
|
+
for t24_have in ${t24_acquirers[@]+"${t24_acquirers[@]}"}; do
|
|
1228
|
+
[[ "${t24_have}" == "${t24_req}" ]] && { t24_found=1; break; }
|
|
1229
|
+
done
|
|
1230
|
+
[[ "${t24_found}" -eq 1 ]] || t24_missing_required="${t24_missing_required} ${t24_req}"
|
|
1231
|
+
done
|
|
1232
|
+
|
|
1233
|
+
if [[ -n "${t24_missing_required}" ]]; then
|
|
1234
|
+
echo "FAIL: Test 24: credential-acquisition discovery no longer sees:${t24_missing_required} — each is a DISTINCT acquisition mechanism (secrets-manager read / presented bot token / silent prompt), so the predicate is broken and the zero-violation verdict below would be vacuous. Population is ${#t24_acquirers[@]}."
|
|
1235
|
+
FAIL=$((FAIL + 1))
|
|
1236
|
+
else
|
|
1237
|
+
t24_violations=""
|
|
1238
|
+
for t24_rel in ${t24_acquirers[@]+"${t24_acquirers[@]}"}; do
|
|
1239
|
+
# Keyed on the PAYLOAD-RELATIVE PATH, never the basename: `flip.sh` is a suffix of
|
|
1240
|
+
# `audit-flag-flip.sh`, and a bare basename also collides with markdown link targets and
|
|
1241
|
+
# `# Usage:` lines. The full path is the script's unambiguous identity.
|
|
1242
|
+
t24_rel_re="$(printf '%s' "${t24_rel}" | sed 's/[.[\*^$]/\\&/g')"
|
|
1243
|
+
while IFS= read -r t24_hit; do
|
|
1244
|
+
[[ -n "${t24_hit}" ]] || continue
|
|
1245
|
+
t24_file="${t24_hit%%:*}"; t24_rest="${t24_hit#*:}"
|
|
1246
|
+
t24_lno="${t24_rest%%:*}"; t24_line="${t24_rest#*:}"
|
|
1247
|
+
|
|
1248
|
+
# A script naming its OWN path (a usage header, a self-referential constant) is not a
|
|
1249
|
+
# call to itself.
|
|
1250
|
+
[[ "${t24_file}" == *"${t24_rel}" ]] && continue
|
|
1251
|
+
|
|
1252
|
+
# A path inside a COMMENT is documentation, not an invocation — `audit-flag-flip.sh`
|
|
1253
|
+
# lists its four consumers in a comment block, which is a manifest, not a call.
|
|
1254
|
+
case "${t24_file}" in
|
|
1255
|
+
*.sh|*.py) printf '%s' "${t24_line}" | grep -cE >/dev/null '^[[:space:]]*#' && continue ;;
|
|
1256
|
+
*.ts|*.mjs|*.cjs) printf '%s' "${t24_line}" | grep -cE >/dev/null '^[[:space:]]*(//|\*)' && continue ;;
|
|
1257
|
+
esac
|
|
1258
|
+
|
|
1259
|
+
# EXECUTION context only. The invariant is about reachability FOR EXECUTION, so a path
|
|
1260
|
+
# merely read or written as CONTENT is a data root and correct as-is (the ruling's
|
|
1261
|
+
# classification table). Measured: `flag-delete/scripts/delete.sh` names `flip.sh` twice
|
|
1262
|
+
# to edit a map entry inside it — a source edit, not a call — and flagging that would
|
|
1263
|
+
# teach the next reader that data roots need anchoring, which inverts the rule.
|
|
1264
|
+
t24_is_exec=0
|
|
1265
|
+
# (1) an explicit verb, or a prose "Run <path>" instruction
|
|
1266
|
+
printf '%s' "${t24_line}" \
|
|
1267
|
+
| grep -cE >/dev/null "(^|[[:space:];&|(])(bash|sh|bun|node|python3|python|exec|source|\.)[[:space:]]+[^[:space:]]*${t24_rel_re}|[Rr]un[[:space:]]+[^[:space:]]*${t24_rel_re}" \
|
|
1268
|
+
&& t24_is_exec=1
|
|
1269
|
+
# (2) a BARE quoted-path invocation — `"${CLAUDE_PLUGIN_ROOT}/…/trigger.sh" --list`
|
|
1270
|
+
# relies on the file's exec bit and carries no verb at all.
|
|
1271
|
+
printf '%s' "${t24_line}" | grep -cE >/dev/null "^[[:space:]]*\"?[^[:space:]]*${t24_rel_re}\"?[[:space:]]" \
|
|
1272
|
+
&& t24_is_exec=1
|
|
1273
|
+
# (3) ASSIGN-THEN-INVOKE. `TRIGGER="…/trigger.sh"` then `bash "$TRIGGER"` on another
|
|
1274
|
+
# line is THE dominant shape in this corpus — it is why Guard 1 needs G2 at all ("the
|
|
1275
|
+
# anchor lives in an assignment, so an operand rule certifies the invocation while the
|
|
1276
|
+
# assignment points anywhere"). Requiring a verb on the same line missed every one of
|
|
1277
|
+
# them: measured, reverting trigger-cron's anchor to the `:-` form SURVIVED, because
|
|
1278
|
+
# all three of its sites are an assignment and two bare invocations.
|
|
1279
|
+
# The variable must actually be INVOKED somewhere in the file — otherwise this would
|
|
1280
|
+
# re-flag `flag-delete`'s `FLIP_SH`, which is only ever read as content (a data root).
|
|
1281
|
+
# `t24_rel_re` is a PAYLOAD-RELATIVE PATH, so it always contains `/` — which is this
|
|
1282
|
+
# `s///`'s own delimiter. Interpolating it raw ended the expression early and sed died
|
|
1283
|
+
# with `unknown option to 's'` for EVERY acquirer, leaving `t24_var` empty and this whole
|
|
1284
|
+
# arm dead while the suite still read 96/0. That is the vacuity class this file exists to
|
|
1285
|
+
# catch, in the arm whose comment says the mutation it guards already SURVIVED once.
|
|
1286
|
+
# Escape the delimiter for sed only: `t24_rel_re` is reused verbatim in the `grep -E`
|
|
1287
|
+
# arms above, where `\/` is not portable.
|
|
1288
|
+
t24_rel_sed="${t24_rel_re//\//\\/}"
|
|
1289
|
+
t24_var="$(printf '%s' "${t24_line}" | sed -nE "s/^[[:space:]]*(readonly[[:space:]]+)?([A-Za-z_][A-Za-z0-9_]*)=.*${t24_rel_sed}.*/\\2/p")"
|
|
1290
|
+
if [[ -n "${t24_var}" ]] \
|
|
1291
|
+
&& grep -qE "(^|[[:space:];&|(])(bash|sh|bun|node|python3|python|exec|source|\.)[[:space:]]+\"?\\\$\{?${t24_var}\}?\"?|^[[:space:]]*\"\\\$\{?${t24_var}\}?\"[[:space:]]" "${t24_file}"; then
|
|
1292
|
+
t24_is_exec=1
|
|
1293
|
+
fi
|
|
1294
|
+
[[ "${t24_is_exec}" -eq 1 ]] || continue
|
|
1295
|
+
|
|
1296
|
+
# POSITIVE requirement — the invariant, not a list of bad spellings. TWO prefixes satisfy
|
|
1297
|
+
# it, and both must be accepted or the guard flags correct code (and gets weakened by
|
|
1298
|
+
# whoever hits that next):
|
|
1299
|
+
# ${CLAUDE_PLUGIN_ROOT}/… loader-substituted at delivery (ADR-179 decision 1)
|
|
1300
|
+
# $SCRIPT_DIR / $BASH_SOURCE layout-invariant per ADR-178, likewise not CWD-derived
|
|
1301
|
+
printf '%s' "${t24_line}" | grep -cE >/dev/null '\$\{CLAUDE_PLUGIN_ROOT\}/' && continue
|
|
1302
|
+
# The operator-terminal handoff shape of ADR-249 (#8486): a skill tells the agent to PRINT
|
|
1303
|
+
# `cd <WORKTREE> && bash <WORKTREE>/…` with <WORKTREE> replaced by the absolute worktree path,
|
|
1304
|
+
# for the operator to run in their own terminal. Like ${CLAUDE_PLUGIN_ROOT} (substituted
|
|
1305
|
+
# absolute by the loader), the placeholder is substituted absolute by the agent, and the `cd`
|
|
1306
|
+
# into the same absolute root means no working directory chooses the script. Anchored on the
|
|
1307
|
+
# WHOLE shape at line start, not on the token, so a bare `bash <WORKTREE>/…` elsewhere still
|
|
1308
|
+
# counts. ${CLAUDE_PLUGIN_ROOT} cannot serve here: it is unset in the operator's terminal.
|
|
1309
|
+
printf '%s' "${t24_line}" | grep -cE >/dev/null '^[[:space:]]*cd <WORKTREE> && bash <WORKTREE>/' && continue
|
|
1310
|
+
printf '%s' "${t24_line}" | grep -cE >/dev/null '\$\{?(SCRIPT_DIR|BASH_SOURCE)' && continue
|
|
1311
|
+
|
|
1312
|
+
t24_violations="${t24_violations}
|
|
1313
|
+
${t24_file#"${REPO_ROOT}"/}:${t24_lno}"
|
|
1314
|
+
done < <(grep -rn -F "${t24_rel}" "${REPO_ROOT}/plugins/soleur" \
|
|
1315
|
+
--include='*.md' --include='*.sh' --include='*.json' --include='*.ts' 2>/dev/null \
|
|
1316
|
+
| grep -v '/test/' | grep -v '\.test\.' || true)
|
|
1317
|
+
done
|
|
1318
|
+
|
|
1319
|
+
if [[ -z "${t24_violations}" ]]; then
|
|
1320
|
+
echo "PASS: Test 24: none of the ${#t24_acquirers[@]} discovered credential-acquiring scripts is reachable through a CWD-relative path (stakes-keyed discovery, invariant-keyed violation, no allowlist)"
|
|
1321
|
+
PASS=$((PASS + 1))
|
|
1322
|
+
else
|
|
1323
|
+
echo "FAIL: Test 24: a credential-ACQUIRING script is invoked through a path that resolves relative to CWD, so the session's own working directory decides which script runs:${t24_violations}"
|
|
1324
|
+
echo " Fix: anchor the invocation on the bare quoted \${CLAUDE_PLUGIN_ROOT}, or resolve it \$BASH_SOURCE-relative if the target is a sibling in the payload."
|
|
1325
|
+
echo " This assertion has NO allowlist by ruling — deferring to #7453 is for non-gate sites, not for sites that hand over a credential."
|
|
1326
|
+
FAIL=$((FAIL + 1))
|
|
1327
|
+
fi
|
|
1328
|
+
fi
|
|
1329
|
+
|
|
1330
|
+
|
|
1331
|
+
echo
|
|
1332
|
+
# ---------------------------------------------------------------------------
|
|
1333
|
+
# Test 25 — an engine crash is reported by class name and the engine's own frame only (PR2 of #10331): the exception text
|
|
1334
|
+
# can carry scanned content, so the fail-closed arm must not print it.
|
|
1335
|
+
# ---------------------------------------------------------------------------
|
|
1336
|
+
t25_out="${TMP_DIR}/test25.out"
|
|
1337
|
+
python3 -I - "${SKILL_DIR}/scripts/redact-engine.py" >"${t25_out}" 2>&1 <<'PY'
|
|
1338
|
+
import importlib.util, sys
|
|
1339
|
+
spec = importlib.util.spec_from_file_location("redact_engine_under_test", sys.argv[1])
|
|
1340
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1341
|
+
spec.loader.exec_module(mod)
|
|
1342
|
+
def boom(path):
|
|
1343
|
+
raise ValueError("CANARY-7f3a")
|
|
1344
|
+
mod.scan = boom
|
|
1345
|
+
sys.argv = ["redact-engine.py", "x"]
|
|
1346
|
+
print("RC=%s" % mod.main())
|
|
1347
|
+
PY
|
|
1348
|
+
assert_grep "Test 25: a crashing engine exits 2" "^RC=2$" "${t25_out}"
|
|
1349
|
+
assert_grep "Test 25: the crash line names the class" "redact-engine: internal error: ValueError at main:[0-9]+$" "${t25_out}"
|
|
1350
|
+
if grep -q "CANARY" "${t25_out}"; then
|
|
1351
|
+
echo "FAIL: Test 25: the exception text reached the output"
|
|
1352
|
+
FAIL=$((FAIL + 1))
|
|
1353
|
+
else
|
|
1354
|
+
echo "PASS: Test 25: the exception text is absent from the output"
|
|
1355
|
+
PASS=$((PASS + 1))
|
|
1356
|
+
fi
|
|
1357
|
+
|
|
1358
|
+
# Test 25b — with stderr closed (sys.stderr is None) the fail-closed arm still returns 2, never an uncaught crash.
|
|
1359
|
+
t25b_out="${TMP_DIR}/test25b.out"
|
|
1360
|
+
python3 -I - "${SKILL_DIR}/scripts/redact-engine.py" >"${t25b_out}" 2>&1 <<'PY'
|
|
1361
|
+
import importlib.util, sys
|
|
1362
|
+
spec = importlib.util.spec_from_file_location("redact_engine_under_test", sys.argv[1])
|
|
1363
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1364
|
+
spec.loader.exec_module(mod)
|
|
1365
|
+
def boom(path):
|
|
1366
|
+
raise ValueError("CANARY-7f3a")
|
|
1367
|
+
mod.scan = boom
|
|
1368
|
+
sys.argv = ["redact-engine.py", "x"]
|
|
1369
|
+
real, sys.stderr = sys.stderr, None
|
|
1370
|
+
try:
|
|
1371
|
+
rc = mod.main()
|
|
1372
|
+
finally:
|
|
1373
|
+
sys.stderr = real
|
|
1374
|
+
print("RC=%s" % rc)
|
|
1375
|
+
PY
|
|
1376
|
+
assert_grep "Test 25b: a crashing engine with stderr closed still exits 2" "^RC=2$" "${t25b_out}"
|
|
1377
|
+
|
|
1378
|
+
# Test 26 — _b64_decode contract (slice 1 of #10331): a non-base64 candidate returns None (never raises), a valid one decodes,
|
|
1379
|
+
# the url-safe alphabet is tried second. Guards the sentinel form that replaced the per-candidate `continue`.
|
|
1380
|
+
t26_out="${TMP_DIR}/test26.out"
|
|
1381
|
+
python3 -I - "${SKILL_DIR}/scripts/redact-engine.py" >"${t26_out}" 2>&1 <<'PY'
|
|
1382
|
+
import importlib.util, sys
|
|
1383
|
+
spec = importlib.util.spec_from_file_location("redact_engine_under_test", sys.argv[1])
|
|
1384
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1385
|
+
spec.loader.exec_module(mod)
|
|
1386
|
+
print("NONB64=%r" % (mod._b64_decode("not base64 !!"),))
|
|
1387
|
+
print("STD=%r" % (mod._b64_decode("aGVsbG8="),))
|
|
1388
|
+
print("URL=%r" % (mod._b64_decode("--8_") == bytes([0xFB, 0xEF, 0x3F]),))
|
|
1389
|
+
PY
|
|
1390
|
+
assert_grep "Test 26: a non-base64 candidate returns None" "^NONB64=None$" "${t26_out}"
|
|
1391
|
+
assert_grep "Test 26: a standard-alphabet candidate decodes" "^STD=b'hello'$" "${t26_out}"
|
|
1392
|
+
assert_grep "Test 26: a url-safe candidate falls through to the second alphabet" "^URL=True$" "${t26_out}"
|
|
1393
|
+
|
|
1394
|
+
echo "Total: ${PASS} pass, ${FAIL} fail"
|
|
1395
|
+
|
|
1396
|
+
# ---------------------------------------------------------------------------
|
|
1397
|
+
# ANTI-VACUITY FLOOR ON THIS HARNESS'S OWN DISPATCH.
|
|
1398
|
+
#
|
|
1399
|
+
# `[[ "${FAIL}" -eq 0 ]]` alone is satisfied by a run that asserted NOTHING: delete every
|
|
1400
|
+
# assertion between the setup block and here and the file exits 0 at `Total: 0 pass, 0 fail`,
|
|
1401
|
+
# which `scripts/test-all.sh` reads as a passing suite. Measured — that is exactly what
|
|
1402
|
+
# happened, in the file whose whole purpose is proving OTHER files are not deletable at green.
|
|
1403
|
+
# Test 20 gives Guard 1 a cross-file floor and nothing reciprocated for Guard 2.
|
|
1404
|
+
#
|
|
1405
|
+
# This is not an invented convention: `scripts/lint-guard-contract.test.sh` already ships the
|
|
1406
|
+
# identical `EXPECTED_MIN` floor, and it was not reused here.
|
|
1407
|
+
#
|
|
1408
|
+
# A FLOOR, deliberately, not `-eq`: the count is developer-incremented, so equality turns every
|
|
1409
|
+
# added assertion into a spurious failure and the natural fix is to stop adding them. Derive the
|
|
1410
|
+
# value from a green run and ratchet it UP when assertions are added — never down to make a
|
|
1411
|
+
# failure go away, which is how a floor becomes the thing it was built to prevent.
|
|
1412
|
+
# ---------------------------------------------------------------------------
|
|
1413
|
+
EXPECTED_MIN=103
|
|
1414
|
+
TOTAL_DISPATCHED=$(( PASS + FAIL ))
|
|
1415
|
+
if [[ "${TOTAL_DISPATCHED}" -lt "${EXPECTED_MIN}" ]]; then
|
|
1416
|
+
echo "FAIL: harness dispatched only ${TOTAL_DISPATCHED} assertions (expected >= ${EXPECTED_MIN}) — a vacuous run. Assertions were removed or the dispatch was neutered; a suite that asserts nothing exits 0 and reads as PASS." >&2
|
|
1417
|
+
exit 1
|
|
1418
|
+
fi
|
|
1419
|
+
|
|
1420
|
+
[[ "${FAIL}" -eq 0 ]]
|