@soleur/opencode 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/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 +659 -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 +108 -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 +342 -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 +152 -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 +493 -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 +363 -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 +520 -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 +113 -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 +178 -0
- package/scripts/sync-opencode-adapter.ts +479 -0
- package/scripts/sync-pr-behind.sh +534 -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 +65 -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 +142 -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 +118 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +96 -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 +668 -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 +325 -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 +138 -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 +1621 -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-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +95 -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 +3022 -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/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/detached-battery-verdict.md +13 -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 +149 -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/work/SKILL.md +1479 -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,1387 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# shellcheck disable=SC2016,SC2088 # the single-quoted `$HOME`/`~` strings are LITERAL patterns matched against lexer text, never expanded
|
|
3
|
+
# PreToolUse hook on Bash: a narrow destructive-command guard for the Soleur plugin (W2, #9601, ADR-277).
|
|
4
|
+
# The kill switch below is the first executable statement; the rest follows the order under MECHANISM.
|
|
5
|
+
[[ "${SOLEUR_DISABLE_DESTRUCTIVE_GUARD-}" == "1" ]] && exit 0
|
|
6
|
+
#
|
|
7
|
+
# PROPERTY. A Bash tool call whose command, after lexing and wrapper unwrapping, runs a command in the
|
|
8
|
+
# decision set below receives `ask` (the harness shows the person the reason and waits) or `deny`, never an
|
|
9
|
+
# implicit allow, in every spelling bash reads as that command (within the scope stated under NOT DECIDED);
|
|
10
|
+
# a command in the same family outside the set receives no decision (no output, exit 0); an envelope the
|
|
11
|
+
# hook cannot read receives `ask`. The hook is a seatbelt against an unambiguous infrastructure destroy, a
|
|
12
|
+
# rewrite or deletion of a default branch on a remote, and a recursive delete of / or the home directory.
|
|
13
|
+
# It is not a security boundary.
|
|
14
|
+
#
|
|
15
|
+
# WHAT IT DECIDES (Bash tool calls only). Each simple command is judged after lexing: a command inside
|
|
16
|
+
# `bash|sh|zsh|dash|ksh -c`, `eval`, `$(...)`, backticks, `<(...)` and every list operator counts as its own
|
|
17
|
+
# simple command, and the recorded argv, never the raw text, is what is judged (so `echo "terraform
|
|
18
|
+
# destroy"`, a heredoc body and `git commit -m "rm -rf /"` are not commands).
|
|
19
|
+
# deny rm with a recursive flag (-r, -R, -rf, --recursive, any cluster or spelling, flags before or after
|
|
20
|
+
# the targets) whose target is /, an ancestor of the home directory, the home directory (~, $HOME,
|
|
21
|
+
# ${HOME}, a relative path resolving there) or the contents of those: /*, ~/*, and any target whose
|
|
22
|
+
# every component after the glob-free root is only glob syntax (/** /*/* /*/ /? /[a-z]* ~/**), and a
|
|
23
|
+
# bare * ** or ./* when the working directory is home or an ancestor. A component that mixes literal
|
|
24
|
+
# text with a glob (~/*/node_modules, /home/*/x, /*.log, ~/.*) is another set of files and is not
|
|
25
|
+
# decided. `~+` is the working directory; with HOME unset or empty `~` is the passwd entry's home
|
|
26
|
+
# (the same lookup bash makes) and `$HOME/` is `/`. A trailing slash or a glob suffix follows a
|
|
27
|
+
# symlink; a bare symlink name does not (`rm -rf link` only unlinks it).
|
|
28
|
+
# ask rm with a recursive flag whose target is the working directory or an ancestor of it (.. , ../.. ,
|
|
29
|
+
# an absolute path above it; `.` alone is not asked because rm refuses it). The working directory is
|
|
30
|
+
# BOTH the envelope's cwd and the one a literal cd/pushd earlier in the command moved to.
|
|
31
|
+
# ask terraform|tofu destroy, and terraform|tofu apply -destroy (global options such as -chdir= skipped;
|
|
32
|
+
# -destroy=<value> is a destroy unless the value is one of 0 f F false FALSE False, the Go bool "off").
|
|
33
|
+
# ask git push that force-pushes (-f, --force, --force-with-lease[=..], --force-if-includes, a +refspec,
|
|
34
|
+
# in any short-flag cluster, --force with --all/--mirror or their unique abbreviations --al, --m...) or
|
|
35
|
+
# deletes (--delete, -d, :ref) a default branch; a destination spelled heads/main or refs/heads/main, a
|
|
36
|
+
# glob destination (refs/heads/*:refs/heads/*) and the matching refspec `:` count under a force flag.
|
|
37
|
+
# Default branches are the union of `git symbolic-ref --short
|
|
38
|
+
# refs/remotes/<named remote>/HEAD` (read LOCALLY, no network), `main` and `master`; the value-taking
|
|
39
|
+
# flags -o, --push-option, --repo, --receive-pack, `git -C <dir>` and the global options that take a
|
|
40
|
+
# separate word (-c, --config-env, --git-dir, --work-tree, --namespace, --attr-source) are parsed so
|
|
41
|
+
# positions and the repository path are right. A `git -c alias.p=push p ...` alias is NOT DECIDED.
|
|
42
|
+
# ask an unresolvable `cd`/`pushd` (a variable, `-`) followed in the same command by a recursive rm or
|
|
43
|
+
# by a force/delete git push: the working directory the later command sees is unknown.
|
|
44
|
+
# A literal `cd`/`pushd` earlier in the same command (also behind `command` or `builtin`) moves the
|
|
45
|
+
# simulated working directory for the later commands (`cd ~ && rm -rf ./*` is a delete of home).
|
|
46
|
+
# Wrappers are unwrapped with a small option table: sudo doas env command (not -v/-V) nohup time timeout
|
|
47
|
+
# nice rtk; xargs is NOT unwrapped. `rtk` (#10230) is a wrapper on every invocation shape: a filtered
|
|
48
|
+
# subcommand (`rtk git status` runs git), an exec subcommand (run|err|test|proxy|bypass|summary, matched
|
|
49
|
+
# case-sensitively — clap rejects `rtk RUN`, which then lands as argv0), or the run_fallback raw exec of
|
|
50
|
+
# any word not in rtk's meta-command set (`rtk rm -rf ~` runs rm; rtk task/pipeline names that resolve to
|
|
51
|
+
# TOML-defined commands land as their name, the same unanalyzable-script class every wrapper table
|
|
52
|
+
# accepts). Globals -v/-vv/-vvv/--verbose and --skip-env (legal before AND after the exec word) are
|
|
53
|
+
# consumed; any other option word, and -c/--command/--shell or a payload word carrying whitespace or
|
|
54
|
+
# shell metacharacters (err|test|summary may join it to `sh -c`), asks unparsed-wrapper — never a silent
|
|
55
|
+
# allow. Grammar verified 2026-10-13 against src/lib.rs + src/core/constants.rs on develop; the exec set
|
|
56
|
+
# is version-pinned here, so a new rtk subcommand is a one-line checklist edit and an unrecognized word
|
|
57
|
+
# simply lands as argv0 in the fail-open direction that run_fallback also picks. `time` is a wrapper as a command word (/usr/bin/time, `command time`, `env
|
|
58
|
+
# time`); as a reserved word it is the lexer's, which drops it, and the first word `-p` then recovers
|
|
59
|
+
# `time -p rm ...` (a pseudo-wrapper). A short-option cluster whose last letter takes a value shifts one more
|
|
60
|
+
# word (sudo -nu root, env -iu X, timeout -vk 5 10). The rule table is retried on the words after a `--`,
|
|
61
|
+
# which covers `doppler run --`, `aws-vault exec <profile> --` and `op run --`. An absolute-path binary
|
|
62
|
+
# matches by basename (/bin/rm) and command names are compared in lower case (RM, Git, Terraform; the fold
|
|
63
|
+
# starts no process). A wrapper's chdir option (env -C DIR, env --chdir=DIR, sudo -D DIR, sudo --chdir=DIR, and the abbreviations getopt_long accepts: --chd)
|
|
64
|
+
# moves the simulated working directory for the command it runs, and for that command only; a directory the
|
|
65
|
+
# guard cannot resolve (a variable) is an unresolved cd. A chdir wrapper around a shell or eval (`env -C D bash
|
|
66
|
+
# -c '...'`) makes the directory unresolved for the commands after it, because the inner string is a record of
|
|
67
|
+
# its own that the guard cannot place in D (a later recursive delete asks; it never under-asks).
|
|
68
|
+
# ask env -S, --split-string (any abbreviation) or a cluster with S (rule id `unparsed-wrapper`): the string
|
|
69
|
+
# it splits into a command is not analysed.
|
|
70
|
+
# ask more than 8 nested wrappers or `--` separators (rule id `wrapper-depth`): what they run cannot be checked.
|
|
71
|
+
# ask a command too large to check (rule id `bound`): a tool call over 256 KiB (checked before the prefilter or
|
|
72
|
+
# any parser reads it), more than MAX_RECORDS simple commands, more than MAX_WORDS words, a word over
|
|
73
|
+
# MAX_WORD_BYTES where the rule table would expand it (a command name, a path, a wrapper's option, a dash word of git or
|
|
74
|
+
# terraform: bash's expansions on one word are quadratic in its length; long text such as a PR body is not asked), a target path
|
|
75
|
+
# with more than MAX_PATH_COMPONENTS components, a segment over 16 KiB (or 64 KiB of segments in all) in a degraded scan, or the DEADLINE_S wall
|
|
76
|
+
# clock reached.
|
|
77
|
+
# What was read before the limit is still judged: a deny wins; an ask-class match keeps its own reason
|
|
78
|
+
# with the bound sentence appended; with nothing matched the ask is the bare `bound` one.
|
|
79
|
+
# ask the lexer returned no command for text that names something the guard decides on (rule id `lexer-empty`:
|
|
80
|
+
# a lexer that silently dropped a command it should have judged). Judged on the text, full-line comments
|
|
81
|
+
# (first non-blank character #) skipped and a later # not treated as a comment: (1) a whole-word token,
|
|
82
|
+
# with the line split at blanks and shell metacharacters, that is rm, destroy, push, terraform, tofu, git
|
|
83
|
+
# or eval in any case; (2) when the line has a quote, backslash, backtick or a $ that is not a plain
|
|
84
|
+
# variable name, the line with those characters removed (r""m, 'r'm, ev""al) has such a token. Both read the
|
|
85
|
+
# last path component of a token (/bin/rm is rm). A blank or comment-only command, and a bare redirect to a
|
|
86
|
+
# file that merely contains one (`> terraform.log`, `> "out/terraform.log"`, `> out.log`), is allowed.
|
|
87
|
+
# A command that is only a heredoc or a here-string (`<<EOF ... EOF`, `<<<"rm -rf /"`) has no command word, so
|
|
88
|
+
# the lexer returns no record; its body names a keyword, so it asks lexer-empty (a body is not a command).
|
|
89
|
+
#
|
|
90
|
+
# NOT DECIDED (stated, not implied). Obfuscation: a variable-built command name, glob or brace expansion of
|
|
91
|
+
# a command name (r[m], r{m,}), brace expansion of a target (`/{bin,usr}`), `xargs rm`, `find -delete` and
|
|
92
|
+
# `find -exec rm`, `rsync --delete`, other interpreters and tools (python shutil.rmtree), zsh-only expansions
|
|
93
|
+
# (=rm); wrappers outside the table (exec, builtin, setsid, ionice, stdbuf, flock, nsenter, chroot, `su -c`,
|
|
94
|
+
# `sudo -s '...'`, coproc); strings run later (trap strings, function bodies, aliases including `git -c
|
|
95
|
+
# alias.x=push`); shells fed on stdin (heredocs, here-strings, a pipe into bash or sh) and `source <(...)`; a
|
|
96
|
+
# runner name in another case (`BASH -c ...`, the lexer is case-sensitive there); a script written and then run;
|
|
97
|
+
# a piped SQL string; `drop database`/`dropdb`; MCP delete tools; any non-Bash tool (including Devin's
|
|
98
|
+
# `exec`: the raw tool_name must be Bash); Doppler secret writes and deletes; a plain `terraform apply`;
|
|
99
|
+
# `kubectl delete`; `pulumi destroy`; `terragrunt destroy`; `git push --mirror` without --force; and an
|
|
100
|
+
# unresolvable $VAR target.
|
|
101
|
+
#
|
|
102
|
+
# USER-FACING STATEMENT (the plugin README carries this sentence, word for word; the suite pins both copies):
|
|
103
|
+
# User-facing statement: The guard does not cover a plain `terraform apply`, secret writes, SQL, non-Bash tools,
|
|
104
|
+
# `terragrunt` or `pulumi` destroy, or indirect command forms (scripts or heredocs fed to a shell, wrappers it
|
|
105
|
+
# does not unwrap, obfuscated command names), and is not a substitute for scoped credentials.
|
|
106
|
+
#
|
|
107
|
+
# RESIDUALS of the shared lexer: it lexes an identical `bash -c`/`eval` string once, so the working-directory
|
|
108
|
+
# simulation cannot tell two identical inner strings under different `cd`s apart; a `cd` inside a subshell or a $(...) is treated as
|
|
109
|
+
# sticky for the later commands (over-asks, never under-asks); a quoted fragment inside a tilde word
|
|
110
|
+
# (`~/"x"`) carries the quoted flag, so the word is read as literal unless it is exactly `~/` or `~/*`
|
|
111
|
+
# (those two are read as home even when quoted: a directory literally named `~` is the cost).
|
|
112
|
+
#
|
|
113
|
+
# DECISION PRECEDENCE across hooks (ADR-264, measured): deny > defer > ask > allow, so another hook's allow
|
|
114
|
+
# cannot turn this hook's ask into a silent run. The guard judges the ORIGINAL command it receives, not
|
|
115
|
+
# another hook's `updatedInput` (PreToolUse hooks of one event run independently on the same input).
|
|
116
|
+
#
|
|
117
|
+
# ASK AND DENY SEMANTICS (measured, phase-0-measurements.md 1.2): `ask` holds under bypassPermissions and
|
|
118
|
+
# against an allow rule; under `claude -p` it blocks the call with the reason shown, so a headless run
|
|
119
|
+
# degrades to a block, never an allow. On a `deny` the person sees only a collapsed "Ran 1 shell command"
|
|
120
|
+
# unless a top-level `systemMessage` is set, so every deny also carries `systemMessage` with the same full
|
|
121
|
+
# reason (the escape hatch and the issues URL included); an ask does not need it. The prompt of an ask shows the
|
|
122
|
+
# whole reason to the person, so an ask opens with a sentence written for that person (see REASONS below).
|
|
123
|
+
#
|
|
124
|
+
# FAILURE POSTURE (D6; a stated narrowing of ADR-157's `.claude` row, with reasons). An envelope jq rejects, a
|
|
125
|
+
# non-string .tool_input.command, empty stdin, a lexer parse failure (exit 2: unbalanced quote, unterminated
|
|
126
|
+
# substitution, NUL byte) and a lexer bound trip (exit 3: depth, budget, alarm, crash) all ASK; the parse
|
|
127
|
+
# reasons say "could not parse this command; it was not recognised as destructive" and never quote a command
|
|
128
|
+
# the hook did not match; a command too large to check (`bound`) and a lexer that returns nothing for real text
|
|
129
|
+
# (`lexer-empty`) ask too.
|
|
130
|
+
#
|
|
131
|
+
# REASONS. An ask opens with a sentence for the person at the prompt ("The guard paused this command and is
|
|
132
|
+
# asking you. It has not run yet and runs only if you approve."), then the rule id, a one-sentence lead and the
|
|
133
|
+
# quoted command, then the agent's instructions under "If you are the agent: This command was NOT run." A deny
|
|
134
|
+
# opens with "This command was NOT run." (the person reads a block, not a prompt). The agent's tail follows the
|
|
135
|
+
# cause: a matched destructive command says stop, do not retry, do not rephrase; a lexer parse failure (exit 2)
|
|
136
|
+
# says fix the quoting or heredoc and send it again; a lexer that gave out (depth, budget, alarm, crash) and a
|
|
137
|
+
# command too large to check say simplify or split; an unreadable envelope says the fault is in the tool call, so
|
|
138
|
+
# stop and tell the person; a lexer that produced no result or malformed output says the parser is not working on this machine, so stop
|
|
139
|
+
# and tell the person (splitting cannot help); env -S and too many wrappers say write the command out so the guard can check it; an
|
|
140
|
+
# unresolvable cd says put a literal directory in the cd.
|
|
141
|
+
# Every ask and deny carries the escape hatch and the issues URL. The quoted command has credentials masked:
|
|
142
|
+
# NAME=value, --name=value and -var name=value with a key, tok, secret, pass, pw, cred, auth or bearer name; the
|
|
143
|
+
# word after --token, --password, --passwd, --secret, --api-key, --auth or --bearer (not a command word, a path starting with / or ~ or
|
|
144
|
+
# $HOME, or origin/main/master: the quote must still say what was about to run); the text after
|
|
145
|
+
# `Authorization:` or `Bearer `; URL userinfo (not the `git@` of an ssh remote). That is a coverage choice, not a boundary: a
|
|
146
|
+
# name that merely contains a cue (AUTHOR=, KEYBOARD=, -var key_name=, passenger_count=) is masked too, and the person reads the
|
|
147
|
+
# original command at the Bash prompt. When jq cannot build the
|
|
148
|
+
# output (emit_fallback) the decision and the rule id are kept: a plain body goes out as it is, a body that quotes
|
|
149
|
+
# the command gets a fixed `guard-output-fallback` text, and a deny stays a deny.
|
|
150
|
+
#
|
|
151
|
+
# DEGRADED MODES. A missing or unusable `jq` does not ask on every call (that would make the plugin
|
|
152
|
+
# unusable without jq): the hook scans the RAW envelope for its own narrow patterns (recursive rm of / ~ or
|
|
153
|
+
# $HOME, `destroy`, `push` with a force flag or +), tolerating JSON-escaped whitespace; a hit asks (output
|
|
154
|
+
# hand-built with a fixed reason naming jq, plus a stderr notice naming jq), a miss exits 0. A missing or
|
|
155
|
+
# broken `perl` with a working jq scans the jq-decoded command the same way (stderr notice naming perl). Both
|
|
156
|
+
# scans stop with a `bound` ask at a segment over 16 KiB, at 64 KiB of segments in all, or at the deadline. It never DENIES on a dependency
|
|
157
|
+
# failure (the repair is itself a Bash call). A miss on the raw scan is a stated, tested fail-open (the
|
|
158
|
+
# raw-scan-miss residual ADR-165 accepted for its `.openhands` row, a mirror ADR-245 has since retired; ADR-277
|
|
159
|
+
# records why this hook asks on a hit where that row denied, and why its `.claude` row, which asks on every
|
|
160
|
+
# call, is narrowed).
|
|
161
|
+
#
|
|
162
|
+
# KILL SWITCH (D5). SOLEUR_DISABLE_DESTRUCTIVE_GUARD=1 (exactly 1; empty, 0 and anything else leave the
|
|
163
|
+
# guard on) exits 0 with no output, before any dependency probe. It is read from the harness process
|
|
164
|
+
# environment, so setting it needs a session restart. It can ALSO be set from a settings-level `env` block
|
|
165
|
+
# (settings.json "env"), which no Bash guard sees and which an agent that can edit a settings file can use:
|
|
166
|
+
# this is a seatbelt, not a boundary. Hosted sessions disable it through AGENT_ENV_OVERRIDES (D8).
|
|
167
|
+
#
|
|
168
|
+
# MECHANISM AND ORDER. Kill switch; read stdin; a tool call over 256 KiB asks `bound` before the prefilter or any parser; the zero-spawn
|
|
169
|
+
# prefilter; the raw tool_name (it must be exactly Bash, so Devin's `exec` is not decided); dependency probes by
|
|
170
|
+
# RESULT, not by `command -v` (a jq that is present and exits non-zero fails like an absent one; probed only on
|
|
171
|
+
# failure, so the happy path pays nothing); jq extraction (the command goes through `jq -j` straight into the lexer so
|
|
172
|
+
# NULs and newlines survive; the small fields use a separate jq call); lib/shell-argv.pl; the rule table; the
|
|
173
|
+
# decision. The prefilter skips the lexer only when the envelope holds exactly one `"command"` text, no backslash
|
|
174
|
+
# followed by u (a key spelled `"\u0063ommand"` is the key command to jq and not the text `"command"`), and that
|
|
175
|
+
# command's raw JSON text has NONE of the keywords rm (also as RM, Rm, rM), destroy, push, eval and none of
|
|
176
|
+
# backslash, single quote, double quote, $, a backtick, `<(`, `>(` or `<<`. It reads the command string only (the
|
|
177
|
+
# text after "command":" up to the first double quote), because the envelope's other fields can spell a keyword
|
|
178
|
+
# (permission_mode, a cwd of .../web-platform), and a double quote inside the command is always JSON-escaped with a
|
|
179
|
+
# backslash, so the backslash test is what sees it. It also requires a `{...}`-shaped envelope with a string
|
|
180
|
+
# "command" so garbage and a non-string command reach the jq path and ask. WHAT THE SKIP IS AND IS NOT: it is not
|
|
181
|
+
# "everything skipped is also allowed by the lexer path" (an unterminated `<(` or backtick is skipped by a
|
|
182
|
+
# keyword-only test yet asks on the lexer path, which is why those are boundary characters and why `<(`/`>(`
|
|
183
|
+
# were added after a differential fuzz found 79 such commands, all unterminated process substitutions). It is
|
|
184
|
+
# "nothing skipped can be a D1 command": every command in the decision set spells rm, destroy or push as a plain
|
|
185
|
+
# substring, and every other spelling of those (a quote, escape, expansion or runner) carries a boundary
|
|
186
|
+
# character; so the lexer path could only ever add a parse-failure or wrapper-depth ask to a skipped command, never a
|
|
187
|
+
# destructive-command decision.
|
|
188
|
+
# Every boundary character has a must-ASK row.
|
|
189
|
+
#
|
|
190
|
+
# DEPENDENCIES. bash (3.2 or later), jq, perl >= 5.10 with core pragmas only (the lexer), git (read-only,
|
|
191
|
+
# local `symbolic-ref`, only for a force/delete push), POSIX utilities. No `eval`, ever (ADR-156: hook stdin
|
|
192
|
+
# is model-controlled). The hook reads stdin and writes stdout and stderr; it writes no file (no here-string,
|
|
193
|
+
# which bash before 5.1 backs with a temporary file) and sends nothing off the machine.
|
|
194
|
+
#
|
|
195
|
+
# PORTABILITY (bash 3.2 and POSIX only). No bash-4 builtin, associative array or case-conversion expansion,
|
|
196
|
+
# and no GNU-only flag of the path, stream-edit, date or stat utilities. Lexer frames are read with
|
|
197
|
+
# `read -d ''` (a $(...) would strip the NULs); the decision is made from the `OK\0` terminator, never from
|
|
198
|
+
# an exit status lost across a process substitution. Empty-array expansions under `set -u` are guarded
|
|
199
|
+
# (`${x[@]+"${x[@]}"}`) wherever an array can be empty; the unguarded ones (`"${AV[@]}"`, `"${DA_T[@]}"`,
|
|
200
|
+
# `"${keep_t[@]}"`, `"${t[@]:...}"`, `"${defaults[@]}"`) are non-empty by invariant: a lexer record has argc >= 1
|
|
201
|
+
# and every slice or copy is taken only after an index < n check. What was run on bash 3.2.57 itself (docker bash:3.2
|
|
202
|
+
# with the host's jq, perl and git): the whole hook over a 30-command corpus (wrapper chains and case folding, env -C and
|
|
203
|
+
# sudo -D, lexer-empty redirects, redaction shapes, env -S, wrapper depth, a bound ask, a deep path), and the decisions and
|
|
204
|
+
# reason text were identical to bash 5.3. Not run on 3.2: the clock-trip and partial-record paths, the degraded (jq- or
|
|
205
|
+
# perl-less) scans and the output fallback. The suites run under the host bash.
|
|
206
|
+
# Path resolution has no canonicalising utility: one subshell finds the longest existing prefix of a directory
|
|
207
|
+
# (`cd -P`, then `pwd -P`) and the rest is normalized lexically, with the literal basename; HOME is compared in both its literal and physical forms. Inherited GIT_* variables are stripped
|
|
208
|
+
# BY PREFIX before any git call (a lefthook-exported GIT_DIR must not redirect it). The working directory is
|
|
209
|
+
# the envelope's .cwd, then CLAUDE_PROJECT_DIR, then PWD.
|
|
210
|
+
set -uo pipefail
|
|
211
|
+
# No pathname expansion: unquoted splits below must never glob.
|
|
212
|
+
set -f
|
|
213
|
+
export LC_ALL=C
|
|
214
|
+
unset CDPATH
|
|
215
|
+
|
|
216
|
+
# BOUNDS on the bash side (the harness kills this hook at hooks.json `timeout: 10`, and a killed hook is not a
|
|
217
|
+
# decision). The wall clock is `SECONDS`, reset here: it ticks on whole-second boundaries, so DEADLINE_S=6 trips
|
|
218
|
+
# between 5 and 6 s after this line, comfortably inside the 10 s budget. MAX_RECORDS caps the simple commands the
|
|
219
|
+
# rule table judges. MAX_WORDS caps the words of the whole command; its job is to bound the TIME one record takes to read
|
|
220
|
+
# (the frame-read loop costs about 120 us a word under load), not what the rule table can judge. MAX_WORD_BYTES caps one word
|
|
221
|
+
# (the frame reader swaps a longer one for a placeholder): bash's own expansions on a word, ${p##*/} and a per-character loop,
|
|
222
|
+
# are quadratic in its length and no clock check can interrupt one, while the 256 KiB and 128-component caps bound neither.
|
|
223
|
+
# MAX_PATH_COMPONENTS caps the components of a path the guard resolves, MAX_ENVELOPE the tool call, SCAN_MAX_SEG a degraded-scan segment and
|
|
224
|
+
# SCAN_MAX_TOTAL all the segments of one degraded scan. A command
|
|
225
|
+
# beyond any of them, or one that runs out of time, asks with rule id `bound` unless a deny was found in what was read
|
|
226
|
+
# (BOUND_SOFT: a trip while reading or resolving does not stop the judging of the rest). The lexer has its own 2 s alarm;
|
|
227
|
+
# these bound everything after it.
|
|
228
|
+
SECONDS=0
|
|
229
|
+
DEADLINE_S=6
|
|
230
|
+
LIMIT_S="$DEADLINE_S" # the limit the messages name: DEADLINE_S itself moves (a read-time trip grants the judging 2 more seconds)
|
|
231
|
+
MAX_RECORDS=2000
|
|
232
|
+
MAX_WORDS=20000
|
|
233
|
+
MAX_WORD_BYTES=4096 # PATH_MAX: a longer word is not judged. bash's own expansions on one word (${p##*/}, a per-character loop over a dash word) are quadratic in its length
|
|
234
|
+
MAX_PATH_COMPONENTS=128
|
|
235
|
+
MAX_WRAP=8 # nested wrappers (and -- hops) the rule table unwraps; the ninth asks `wrapper-depth`
|
|
236
|
+
QUOTE_MAX=200 # characters of the matched command a reason quotes
|
|
237
|
+
MAX_ENVELOPE=262144 # 256 KiB: a larger tool call asks `bound` before the prefilter or any parser reads it (the prefilter and the raw scans are not linear)
|
|
238
|
+
SCAN_MAX_SEG=16384 # 16 KiB: the longest segment the raw/decoded scans will try their patterns on (their cost is quadratic in it: about 0.3 s at the cap)
|
|
239
|
+
SCAN_MAX_TOTAL=65536 # 64 KiB: the bytes of all the segments of one command the scans will try (the clock is read only between segments)
|
|
240
|
+
BOUND_WHY=""
|
|
241
|
+
BOUND_SOFT=""
|
|
242
|
+
|
|
243
|
+
ISSUES_URL='https://github.com/jikig-ai/soleur/issues'
|
|
244
|
+
# A reason has two readers: the person at the prompt (an ask) and the agent. A body written as `<head><AMARK><tail>` is composed by
|
|
245
|
+
# compose() below: the tail is the agent's instructions. An ask opens with a sentence for the person and puts the agent's part under
|
|
246
|
+
# "If you are the agent:"; a deny (and its systemMessage) opens with the not-run sentence, as the person only reads a block.
|
|
247
|
+
AMARK=$'\001'
|
|
248
|
+
ASK_LEAD="The guard paused this command and is asking you. It has not run yet and runs only if you approve."
|
|
249
|
+
PERSON_TAIL=" The person can run the command themselves in their own terminal, outside the agent, or start the session with SOLEUR_DISABLE_DESTRUCTIVE_GUARD=1 set in their own shell. If this was flagged wrongly, report it at ${ISSUES_URL}"
|
|
250
|
+
# a matched destructive command: stop, do not retry, do not rephrase
|
|
251
|
+
REASON_TAIL="${AMARK} Stop and tell the person what you were about to run and why. Do not retry this command and do not rephrase it to get around the guard. If no person is available to answer, end the task and report it as blocked.${PERSON_TAIL}"
|
|
252
|
+
# a command too large to check, or a lexer that gave out (depth, budget, alarm, crash): simplify or split
|
|
253
|
+
BOUND_TAIL="${AMARK} Split it into smaller commands and send them one at a time; if you cannot, stop and report the task as blocked.${PERSON_TAIL}"
|
|
254
|
+
# a quoting or heredoc problem in the command (lexer exit 2) is repaired and resent
|
|
255
|
+
PARSE_TAIL="${AMARK} If this is a valid command you meant to run, fix its quoting or heredoc and send it again; if you cannot, stop and report the task as blocked.${PERSON_TAIL}"
|
|
256
|
+
# an envelope the hook cannot read is a fault in the tool call, not in the command
|
|
257
|
+
ENVELOPE_TAIL="${AMARK} This is a fault in the tool call the harness sent, not a problem with the command: stop and tell the person what happened instead of rewriting the command.${PERSON_TAIL}"
|
|
258
|
+
# env -S and too many wrappers hide the command: write it out so it can be checked
|
|
259
|
+
WRAPPER_TAIL="${AMARK} If this is a command you meant to run, write it out without env -S (or with fewer nested wrappers) so the guard can check it, and send it again; if you cannot, stop and tell the person what you were about to run.${PERSON_TAIL}"
|
|
260
|
+
# a lexer that produced nothing or garbage is a fault on this machine: no piece of the command can be checked either, so splitting does not help
|
|
261
|
+
LEXER_TAIL="${AMARK} The guard's parser is not working on this machine, so no command can be checked here and splitting this one will not help: stop and tell the person what happened instead of rewriting the command.${PERSON_TAIL}"
|
|
262
|
+
# an unresolvable cd: the repair is a literal directory
|
|
263
|
+
CD_TAIL="${AMARK} If this is a command you meant to run, put a literal directory in the cd (not a variable or -) so the guard can check where the command runs (for a directory option such as env -C DIR in front of sh -c, do the cd inside the inner command instead), and send it again; if you cannot, stop and tell the person what you were about to run.${PERSON_TAIL}"
|
|
264
|
+
|
|
265
|
+
# ---- output ---------------------------------------------------------------------------------------
|
|
266
|
+
# compose <ask|deny> <body> -> COMPOSED: the reason text (see AMARK).
|
|
267
|
+
COMPOSED=""
|
|
268
|
+
compose() {
|
|
269
|
+
local head="${2%%"$AMARK"*}" tail=""
|
|
270
|
+
[[ "$2" == *"$AMARK"* ]] && tail="${2#*"$AMARK"}"
|
|
271
|
+
if [[ "$1" == deny ]]; then COMPOSED="This command was NOT run. ${head}${tail}"
|
|
272
|
+
else COMPOSED="${ASK_LEAD} ${head} If you are the agent: This command was NOT run.${tail}"; fi
|
|
273
|
+
}
|
|
274
|
+
# emit <ask|deny> <body>: the full envelope (a bare decision without hookEventName is silently ignored), built with jq; when jq
|
|
275
|
+
# cannot build it, emit_fallback. A deny also carries systemMessage.
|
|
276
|
+
emit() {
|
|
277
|
+
local out="" reason
|
|
278
|
+
compose "$1" "$2"; reason="$COMPOSED"
|
|
279
|
+
out="$(jq -nc --arg d "$1" --arg r "$reason" \
|
|
280
|
+
'{hookSpecificOutput: {hookEventName: "PreToolUse", permissionDecision: $d, permissionDecisionReason: $r}}
|
|
281
|
+
+ (if $d == "deny" then {systemMessage: $r} else {} end)' 2>/dev/null)" || out=""
|
|
282
|
+
if [[ -z "$out" ]]; then emit_fallback "$1" "$2"; return; fi
|
|
283
|
+
printf '%s\n' "$out"
|
|
284
|
+
}
|
|
285
|
+
# emit_fixed <ask|deny> <body without quotes, backslashes or control characters>: no jq, no interpolation of input text.
|
|
286
|
+
emit_fixed() {
|
|
287
|
+
compose "$1" "$2"
|
|
288
|
+
if [[ "$1" == deny ]]; then
|
|
289
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"},"systemMessage":"%s"}\n' "$COMPOSED" "$COMPOSED"
|
|
290
|
+
else
|
|
291
|
+
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"ask","permissionDecisionReason":"%s"}}\n' "$COMPOSED"
|
|
292
|
+
fi
|
|
293
|
+
}
|
|
294
|
+
# emit_fallback <ask|deny> <body>: jq could not build the output. A body of our own text (printable ASCII, no quote or backslash) goes
|
|
295
|
+
# out as it is, so the rule id, the lead and the tail survive. A body that quotes the command (it holds a quote, a backslash or a
|
|
296
|
+
# control character) gets a fixed text that keeps the rule id and the decision: a deny stays a deny and says it is blocked.
|
|
297
|
+
emit_fallback() {
|
|
298
|
+
local fid="${2%%:*}"
|
|
299
|
+
case "$fid" in ""|*[!a-z0-9-]*) fid=unknown ;; esac
|
|
300
|
+
compose "$1" "$2"
|
|
301
|
+
case "$COMPOSED" in
|
|
302
|
+
*[![:print:]]*|*[\"\\]*)
|
|
303
|
+
if [[ "$1" == deny ]]; then
|
|
304
|
+
emit_fixed deny "guard-output-fallback: the destructive-command guard matched rule ${fid} and blocked this command, but jq could not build the full message that quotes it.${AMARK} Stop and tell the person. Do not retry this command. If no person is available, end the task and report it as blocked. The person can run it in their own terminal or start the session with SOLEUR_DISABLE_DESTRUCTIVE_GUARD=1. Report a wrong flag at ${ISSUES_URL}"
|
|
305
|
+
else
|
|
306
|
+
emit_fixed ask "guard-output-fallback: the destructive-command guard matched rule ${fid}, but jq could not build the full message that quotes the command, so it is asking instead of allowing.${AMARK} Stop and tell the person. Do not retry this command. If no person is available, end the task and report it as blocked. The person can run it in their own terminal or start the session with SOLEUR_DISABLE_DESTRUCTIVE_GUARD=1. Report a wrong flag at ${ISSUES_URL}"
|
|
307
|
+
fi ;;
|
|
308
|
+
*) emit_fixed "$1" "$2" ;;
|
|
309
|
+
esac
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
# lead_for <rule id>: the one-sentence statement of what the rule matched.
|
|
313
|
+
lead_for() {
|
|
314
|
+
case "$1" in
|
|
315
|
+
recursive-delete-home) LEAD="this command recursively deletes the filesystem root, an ancestor of the home directory, the home directory, or everything inside one of them." ;;
|
|
316
|
+
recursive-delete-workdir) LEAD="this command recursively deletes the working directory or one of its ancestors." ;;
|
|
317
|
+
infra-destroy) LEAD="this command runs terraform or tofu destroy (or apply -destroy), which tears down infrastructure." ;;
|
|
318
|
+
default-branch-force-push) LEAD="this command force-pushes over, or deletes, a default branch on a remote." ;;
|
|
319
|
+
wrapper-depth) LEAD="this command nests more than ${MAX_WRAP} wrappers (sudo, env, timeout, ...) or -- separators, more than the guard unwraps, so the command it finally runs cannot be checked." ;;
|
|
320
|
+
unparsed-wrapper) LEAD="this command runs env -S (--split-string), which splits a string into the command to run, and the guard does not analyse that string." ;;
|
|
321
|
+
unresolved-cd-before-destructive) LEAD="a cd, pushd or wrapper directory option (env -C, sudo -D) that cannot be resolved (a variable, -, a path too deep to resolve, or a directory option in front of a shell command, whose inner commands the guard cannot place in it) comes before a recursive delete or a force push in the same command, so the guard cannot tell which directory the later command runs in." ;;
|
|
322
|
+
*) LEAD="this command matched a destructive-command rule." ;;
|
|
323
|
+
esac
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
# ---- redaction: the matched command goes into the reason, the transcript and (on a deny) the systemMessage -------
|
|
327
|
+
# A word that carries a credential is masked BEFORE the text is joined and cut to 200 characters (so a cut can never
|
|
328
|
+
# leave half of one). Masked: (1) NAME=value, --name=value and -var name=value where NAME holds key, tok, secret, pass, pw (so pwd
|
|
329
|
+
# too), cred, auth or bearer in any case (the whole value, spaces included, because the lexer's word is one argument); (2) the word
|
|
330
|
+
# AFTER --token, --password, --passwd, --secret, --api-key, --auth or --bearer (unless it is itself a flag); (3) the text after
|
|
331
|
+
# `Authorization:` or `Bearer ` inside a word (an -H header, `-c http.extraheader=Authorization: Basic x`); (4) URL userinfo
|
|
332
|
+
# (`://user:pass@` keeps the user, a password may hold / or @; `://token@` masks it). Only the emit path calls this, one word at a
|
|
333
|
+
# time with bash regexes under nocasematch, so it costs no process. The names are a coverage choice, not a boundary: a
|
|
334
|
+
# credential in a word with none of these names (or a bare value) is not masked.
|
|
335
|
+
RE_SECRET_ASSIGN='^(([A-Za-z0-9_.-]*=)?-{0,2}[A-Za-z0-9_.-]*(key|tok|secret|pass|pw|cred|auth|bearer)[A-Za-z0-9_.-]*=)'
|
|
336
|
+
RE_URL_PW='^(.*://[^/@[:space:]:]*:)[^[:space:]]*(@[^@[:space:]]*)$'
|
|
337
|
+
RE_URL_USER='^(.*://)[^/@[:space:]:]+(@.*)$'
|
|
338
|
+
RE_AUTH_HDR='^(.*authorization:[[:space:]]*)(.+)$'
|
|
339
|
+
RE_BEARER='^(.*bearer[[:space:]]+)(.+)$'
|
|
340
|
+
RE_FLAG_NEXT='^--(token|password|passwd|secret|api-key|auth|bearer)$'
|
|
341
|
+
RW=""; RW_NEXT=0
|
|
342
|
+
redact_word() { # <word> -> RW; RW_NEXT (1 = the word after a credential flag) carries across words: the caller resets it per command
|
|
343
|
+
local w="${1//"$AMARK"/}" had=0 mask=0 # (the marker byte never reaches the quote: compose() splits the reason at it)
|
|
344
|
+
if (( RW_NEXT )) && [[ "$w" != -* ]]; then
|
|
345
|
+
case "$w" in
|
|
346
|
+
# a command word or a destructive operand is not a credential: the person reads the quote to see WHAT was about to run
|
|
347
|
+
/|'/*'|'~'|'~/'|'~/*'|'$HOME'|'${HOME}'|'$HOME/*'|'${HOME}/*'|rm|git|terraform|tofu|sudo|doas|env|command|nohup|time|timeout|nice|rtk|origin|main|master) : ;;
|
|
348
|
+
*) RW="<redacted>"; RW_NEXT=0; return ;;
|
|
349
|
+
esac
|
|
350
|
+
fi
|
|
351
|
+
shopt -q nocasematch && had=1
|
|
352
|
+
shopt -s nocasematch
|
|
353
|
+
if [[ "$w" =~ $RE_SECRET_ASSIGN ]]; then w="${BASH_REMATCH[1]}<redacted>"
|
|
354
|
+
elif [[ "$w" =~ $RE_URL_PW ]]; then w="${BASH_REMATCH[1]}<redacted>${BASH_REMATCH[2]}"
|
|
355
|
+
elif [[ "$w" == *://git@* ]]; then : # the service account of an ssh remote (git@github.com) is not a credential
|
|
356
|
+
elif [[ "$w" =~ $RE_URL_USER ]]; then w="${BASH_REMATCH[1]}<redacted>${BASH_REMATCH[2]}"
|
|
357
|
+
elif [[ "$w" =~ $RE_AUTH_HDR ]]; then w="${BASH_REMATCH[1]}<redacted>"
|
|
358
|
+
elif [[ "$w" =~ $RE_BEARER ]]; then w="${BASH_REMATCH[1]}<redacted>"
|
|
359
|
+
elif [[ "$w" =~ $RE_FLAG_NEXT ]]; then mask=1
|
|
360
|
+
fi
|
|
361
|
+
(( had )) || shopt -u nocasematch
|
|
362
|
+
RW_NEXT="$mask"
|
|
363
|
+
RW="$w"
|
|
364
|
+
}
|
|
365
|
+
# redact_text <text> -> RTXT: the same, over the blank-separated words of raw text (the perl-less scan has text, not words). A word
|
|
366
|
+
# that opens with a quote is taken with the words after it up to the one that closes it, and the quotes are dropped, so a quoted
|
|
367
|
+
# name=value is read as the word the shell would have made of it. Whole words only, and it stops after the 200 shown characters.
|
|
368
|
+
RTXT=""
|
|
369
|
+
redact_text() {
|
|
370
|
+
local IFS=$' \t\n' w q i=0 n out=""
|
|
371
|
+
local -a ws
|
|
372
|
+
# shellcheck disable=SC2206 # the split on blanks is the point (no pathname expansion: set -f)
|
|
373
|
+
ws=($1); n=${#ws[@]}
|
|
374
|
+
while (( i < n )); do
|
|
375
|
+
(( ${#out} > QUOTE_MAX )) && break
|
|
376
|
+
w="${ws[$i]}"; i=$((i + 1)); q=""
|
|
377
|
+
case "$w" in \'*|\"*) q="${w:0:1}"; w="${w#"$q"}" ;; esac
|
|
378
|
+
if [[ -n "$q" ]]; then
|
|
379
|
+
while [[ "$w" != *"$q" ]] && (( i < n && ${#w} <= QUOTE_MAX )); do w="$w ${ws[$i]}"; i=$((i + 1)); done
|
|
380
|
+
w="${w%"$q"}"
|
|
381
|
+
fi
|
|
382
|
+
redact_word "$w"; out="${out:+$out }$RW"
|
|
383
|
+
done
|
|
384
|
+
RTXT="$out"
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
ask_parse() { # <cause text>: a command the lexer could not read. Only a parse failure (exit 2) is repaired by fixing the quoting;
|
|
388
|
+
# a lexer that gave out (depth, budget, alarm, crash, no result, malformed output) wants a simpler or smaller command.
|
|
389
|
+
local tail="$BOUND_TAIL"
|
|
390
|
+
case "$1" in
|
|
391
|
+
"lexer exit2") tail="$PARSE_TAIL" ;;
|
|
392
|
+
"lexer depth"|"lexer budget"|"lexer alarm"|"lexer crash") : ;;
|
|
393
|
+
*) tail="$LEXER_TAIL" ;; # no result, malformed output, a cause the hook does not know
|
|
394
|
+
esac
|
|
395
|
+
emit ask "command-not-parsed: the destructive-command guard could not parse this command; it was not recognised as destructive, and the guard asks rather than guess (${1}).${tail}"
|
|
396
|
+
exit 0
|
|
397
|
+
}
|
|
398
|
+
ask_lexer_empty() {
|
|
399
|
+
emit ask "lexer-empty: the destructive-command guard's lexer returned no command for this input, which mentions a word the guard decides on (rm, destroy, push, terraform, tofu, git or eval); it was not recognised as destructive, and the guard asks rather than guess.${PARSE_TAIL}"
|
|
400
|
+
exit 0
|
|
401
|
+
}
|
|
402
|
+
ask_bound() { # <why>: a command too large to check in full
|
|
403
|
+
emit ask "bound: the destructive-command guard stopped checking this command because it is too large to check in full (${1}); it was not recognised as destructive, and the guard asks rather than guess.${BOUND_TAIL}"
|
|
404
|
+
exit 0
|
|
405
|
+
}
|
|
406
|
+
ask_envelope() { # <cause text>
|
|
407
|
+
emit ask "envelope-unreadable: the destructive-command guard could not read this tool call (${1}), and it asks rather than allow what it cannot read.${ENVELOPE_TAIL}"
|
|
408
|
+
exit 0
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
# ---- the narrow raw/decoded scan used by the degraded paths (D6) -----------------------------------
|
|
412
|
+
# One anchored pattern per case, each tested on ONE segment (a list is split on ; & | and newlines first),
|
|
413
|
+
# never independent greps ANDed over the whole text. A blank between two words of terraform, tofu or git is a RUN of blanks
|
|
414
|
+
# (`[[:space:]]+`): `terraform destroy` is `terraform destroy` to a shell; the scan already reads a JSON tab as a blank.
|
|
415
|
+
SC_B='(^|[^[:alnum:]_.-])'
|
|
416
|
+
RE_RM_WORD="${SC_B}rm[[:space:]]"
|
|
417
|
+
RE_RM_REC='[[:space:]](-[a-zA-Z]*[rR][a-zA-Z]*|--recursive)([^[:alnum:]_-]|$)'
|
|
418
|
+
RE_RM_TGT='(^|[^[:alnum:]_./~$-])((/+\*?)|((~|\$HOME|\$\{HOME\})(/+\*?)?))([^[:alnum:]_./~$*-]|$)'
|
|
419
|
+
RE_TF_DESTROY="${SC_B}(terraform|tofu)[[:space:]]+([^[:space:]].*[[:space:]])?destroy([^[:alnum:]_.-]|\$)"
|
|
420
|
+
RE_TF_APPLY="${SC_B}(terraform|tofu)[[:space:]]+([^[:space:]].*[[:space:]])?apply[[:space:]]+([^[:space:]].*[[:space:]])?-{1,2}destroy([^[:alnum:]_.-]|\$)"
|
|
421
|
+
RE_GIT_PUSH="${SC_B}git[[:space:]]+([^[:space:]].*[[:space:]])?push[[:space:]]+([^[:space:]].*[[:space:]])?(-[a-zA-Z]*f[a-zA-Z]*|--force[^[:space:]]*|\\+[^[:space:]+])"
|
|
422
|
+
SCAN_SEG=""
|
|
423
|
+
# scan_narrow <text>: 0 on a hit (SCAN_SEG = the matched segment), 1 on a miss, 2 when a bound tripped (SCAN_WHY): a segment
|
|
424
|
+
# longer than SCAN_MAX_SEG or more than SCAN_MAX_TOTAL bytes of segments (the patterns are not linear in a segment's length, and
|
|
425
|
+
# the clock is read only between segments) or the clock reached DEADLINE_S.
|
|
426
|
+
SCAN_WHY=""
|
|
427
|
+
scan_narrow() {
|
|
428
|
+
local seg IFS=$';&|\n' total=0
|
|
429
|
+
for seg in $1; do
|
|
430
|
+
total=$((total + ${#seg}))
|
|
431
|
+
if (( ${#seg} > SCAN_MAX_SEG )); then SCAN_WHY="a command segment longer than $((SCAN_MAX_SEG / 1024)) KiB"; return 2; fi
|
|
432
|
+
if (( total > SCAN_MAX_TOTAL )); then SCAN_WHY="more than $((SCAN_MAX_TOTAL / 1024)) KiB of command text to scan"; return 2; fi
|
|
433
|
+
if (( SECONDS >= DEADLINE_S )); then SCAN_WHY="the ${LIMIT_S} s time limit was reached while scanning"; return 2; fi
|
|
434
|
+
if [[ "$seg" =~ $RE_RM_WORD && "$seg" =~ $RE_RM_REC && "$seg" =~ $RE_RM_TGT ]]; then SCAN_SEG="$seg"; return 0; fi
|
|
435
|
+
if [[ "$seg" =~ $RE_TF_DESTROY || "$seg" =~ $RE_TF_APPLY ]]; then SCAN_SEG="$seg"; return 0; fi
|
|
436
|
+
if [[ "$seg" =~ $RE_GIT_PUSH ]]; then SCAN_SEG="$seg"; return 0; fi
|
|
437
|
+
done
|
|
438
|
+
return 1
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
# ---- 1. read stdin ---------------------------------------------------------------------------------
|
|
442
|
+
INPUT="$(cat 2>/dev/null)" || INPUT=""
|
|
443
|
+
case "$INPUT" in
|
|
444
|
+
*[![:space:]]*) : ;;
|
|
445
|
+
*) ask_envelope "stdin was empty" ;;
|
|
446
|
+
esac
|
|
447
|
+
|
|
448
|
+
# An oversize tool call asks before anything reads it (the prefilter below and the raw scans are not linear in its size).
|
|
449
|
+
if (( ${#INPUT} > MAX_ENVELOPE )); then ask_bound "the tool call is larger than $((MAX_ENVELOPE / 1024)) KiB"; fi
|
|
450
|
+
|
|
451
|
+
# ---- 2. the zero-spawn prefilter (D7) --------------------------------------------------------------
|
|
452
|
+
# Skip the lexer only for a `{...}` envelope with a STRING command that has no keyword and no boundary
|
|
453
|
+
# character. See the header: only the command's own raw JSON text is scanned.
|
|
454
|
+
PF_LEAD="${INPUT#"${INPUT%%[![:space:]]*}"}"
|
|
455
|
+
PF_TAIL="${INPUT%"${INPUT##*[![:space:]]}"}"
|
|
456
|
+
PF_CMD_RE='"command"[[:space:]]*:[[:space:]]*"'
|
|
457
|
+
# Exactly one `"command"` text in the whole envelope, or the prefilter does not decide: it reads the FIRST key
|
|
458
|
+
# while jq reads .tool_input.command, so a decoy key before the real one (nested, in an array, at the top level, or
|
|
459
|
+
# a duplicate) must go to jq. Nine characters are the text `"command"`. A key can also be spelled with a \u escape
|
|
460
|
+
# (`"\u0063ommand"` is the key command to jq and not the text `"command"` to the count), so an envelope that holds a
|
|
461
|
+
# backslash followed by u anywhere goes to jq.
|
|
462
|
+
PF_ONCE="${INPUT//\"command\"/}"
|
|
463
|
+
if [[ "${PF_LEAD:0:1}" == "{" && "${PF_TAIL: -1}" == "}" && $(( ${#INPUT} - ${#PF_ONCE} )) -eq 9 && "$INPUT" != *'\u'* && "$INPUT" =~ $PF_CMD_RE ]]; then
|
|
464
|
+
PF_MARK="${BASH_REMATCH[0]}"
|
|
465
|
+
PF_REST="${INPUT#*"$PF_MARK"}"
|
|
466
|
+
PF_CMD="${PF_REST%%\"*}"
|
|
467
|
+
case "$PF_CMD" in
|
|
468
|
+
*rm*|*destroy*|*push*|*eval*|*Rm*|*RM*|*rM*|*'<('*|*'>('*|*'<<'*|*[\\\'\"\$\`]*) : ;;
|
|
469
|
+
*) exit 0 ;;
|
|
470
|
+
esac
|
|
471
|
+
fi
|
|
472
|
+
|
|
473
|
+
# ---- 3. the lexer path -----------------------------------------------------------------------------
|
|
474
|
+
HOOK_SRC="${BASH_SOURCE[0]}"
|
|
475
|
+
HOOK_DIR="${HOOK_SRC%/*}"; [[ "$HOOK_DIR" == "$HOOK_SRC" ]] && HOOK_DIR=.
|
|
476
|
+
LEXER="$HOOK_DIR/lib/shell-argv.pl"
|
|
477
|
+
|
|
478
|
+
# (no regex function: a jq built without regex support must still read the envelope; newlines and carriage returns become blanks so the
|
|
479
|
+
# three fields stay three lines)
|
|
480
|
+
JQ_FIELDS='if type != "object" then "invalid" else
|
|
481
|
+
((.tool_name // "" | if type == "string" then . else "" end | split("\n") | join(" ") | split("\r") | join(" ")),
|
|
482
|
+
(.cwd // "" | if type == "string" then . else "" end | split("\n") | join(" ") | split("\r") | join(" ")),
|
|
483
|
+
(try (.tool_input.command | type) catch "invalid"))
|
|
484
|
+
end'
|
|
485
|
+
|
|
486
|
+
degrade_jq() {
|
|
487
|
+
echo "soleur destructive-command-guard: jq is missing or unusable on this machine; scanning the raw tool input with the guard's own narrow patterns instead of parsing it (install jq for full coverage)" >&2
|
|
488
|
+
local raw="$INPUT"
|
|
489
|
+
raw="${raw//\\n/;}"; raw="${raw//\\u000[aA]/;}"; raw="${raw//\\u000[dD]/;}"
|
|
490
|
+
raw="${raw//\\t/ }"; raw="${raw//\\r/ }"; raw="${raw//\\u0009/ }"; raw="${raw//\\u0020/ }"
|
|
491
|
+
scan_narrow "$raw"; SCAN_RC=$?
|
|
492
|
+
[[ "$SCAN_RC" -eq 2 ]] && ask_bound "$SCAN_WHY"
|
|
493
|
+
if [[ "$SCAN_RC" -eq 0 ]]; then
|
|
494
|
+
emit_fixed ask "guard-degraded-jq-missing: jq is missing or unusable on this machine, so the destructive-command guard scanned the raw tool input instead of parsing it, and this call looks destructive (a recursive delete of / or home, a destroy, or a force push).${AMARK} Stop and tell the person. Do not retry this command or rephrase it. If no person is available, end the task and report it as blocked. The person can run it in their own terminal, or start the session with SOLEUR_DISABLE_DESTRUCTIVE_GUARD=1, or install jq. Report a wrong flag at ${ISSUES_URL}"
|
|
495
|
+
fi
|
|
496
|
+
exit 0
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
JQ_RC=0
|
|
500
|
+
FIELDS="$(printf '%s' "$INPUT" | jq -r "$JQ_FIELDS" 2>/dev/null)" || JQ_RC=$?
|
|
501
|
+
if [[ "$JQ_RC" -ne 0 ]]; then
|
|
502
|
+
# Probe by RESULT: a working jq that rejected this envelope is a bad envelope (ask); anything else is a
|
|
503
|
+
# missing or unusable jq (raw scan).
|
|
504
|
+
JQ_PROBE="$(printf '{"a":1}' | jq -r '.a' 2>/dev/null)" || JQ_PROBE=""
|
|
505
|
+
[[ "$JQ_PROBE" == 1 ]] && ask_envelope "the envelope is not valid JSON"
|
|
506
|
+
degrade_jq
|
|
507
|
+
fi
|
|
508
|
+
[[ "$FIELDS" == invalid ]] && ask_envelope "the envelope is not a JSON object"
|
|
509
|
+
RAW_TOOL="${FIELDS%%$'\n'*}"; FIELDS_REST="${FIELDS#*$'\n'}"
|
|
510
|
+
CWD_IN="${FIELDS_REST%%$'\n'*}"; CMD_TYPE="${FIELDS_REST#*$'\n'}"
|
|
511
|
+
# Only a Bash call is decided, by the RAW name (Devin's `exec` is not decided: D2). A missing tool_name is read as Bash (fail toward deciding); the registered
|
|
512
|
+
# matcher is ^Bash$, so a real call always carries it.
|
|
513
|
+
if [[ -n "$RAW_TOOL" ]]; then
|
|
514
|
+
[[ "$RAW_TOOL" == Bash ]] || exit 0
|
|
515
|
+
fi
|
|
516
|
+
[[ "$CMD_TYPE" == string ]] || ask_envelope "tool_input.command is not a string"
|
|
517
|
+
|
|
518
|
+
# long_word_judged <word>: 0 when a word over MAX_WORD_BYTES sits where the rule table would EXPAND it, so it is a bound ask. Those are
|
|
519
|
+
# the command-name word itself, any word of a record whose command is rm, cd, pushd, popd, builtin or a wrapper (their words are paths and
|
|
520
|
+
# options), and a dash word of git, terraform or tofu (the option loops). A long argument that is only text (a PR body, a commit message,
|
|
521
|
+
# an echo argument, an assignment value) is never expanded by anything, so it is neither judged nor asked about. Reads the words of the
|
|
522
|
+
# current record already read (W_TXT from REC_OFF); the word being tested is not yet in W_TXT.
|
|
523
|
+
long_word_judged() {
|
|
524
|
+
local w="$1" k name had=0 hit=1
|
|
525
|
+
for ((k = REC_OFF[${#REC_OFF[@]} - 1]; k < NW; k++)); do
|
|
526
|
+
[[ "${W_TXT[$k]}" =~ ^[A-Za-z_][A-Za-z0-9_]*\+?= ]] || break
|
|
527
|
+
done
|
|
528
|
+
if (( k >= NW )); then # nothing but assignments so far: this word is another assignment (not judged) or the command name (judged)
|
|
529
|
+
[[ "$w" =~ ^[A-Za-z_][A-Za-z0-9_]*\+?= ]] && return 1
|
|
530
|
+
return 0
|
|
531
|
+
fi
|
|
532
|
+
name="${W_TXT[$k]##*/}"
|
|
533
|
+
shopt -q nocasematch && had=1
|
|
534
|
+
shopt -s nocasematch
|
|
535
|
+
case "$name" in
|
|
536
|
+
rm|cd|pushd|popd|builtin|sudo|doas|env|command|nohup|time|timeout|nice|rtk|-p) hit=0 ;;
|
|
537
|
+
git|terraform|tofu) [[ "$w" == -* ]] && hit=0 ;;
|
|
538
|
+
esac
|
|
539
|
+
(( had )) || shopt -u nocasematch
|
|
540
|
+
return "$hit"
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
# ---- 4. lex ----------------------------------------------------------------------------------------
|
|
544
|
+
# Frames: C \0 ctx \0 argc \0 (flags \0 arg \0){argc} ... then OK \0 (or E \0 cause \0 on failure).
|
|
545
|
+
W_TXT=(); W_FLG=(); REC_OFF=(); REC_N=()
|
|
546
|
+
SAW_OK=0; SAW_E=""; LEX_BAD=0; BOUND_READ=0
|
|
547
|
+
LEX_ST=0; LEX_LEFT=0; LEX_KIND=0; NW=0; FRN=0
|
|
548
|
+
while IFS= read -r -d '' F; do
|
|
549
|
+
# a clock check every 1024 frames, so one huge record is bounded too (the per-record check below sees only record starts)
|
|
550
|
+
FRN=$((FRN + 1))
|
|
551
|
+
if (( (FRN & 1023) == 0 && SECONDS >= DEADLINE_S )); then
|
|
552
|
+
BOUND_SOFT="the ${LIMIT_S} s time limit was reached while reading the lexer output"; BOUND_READ=1
|
|
553
|
+
if (( LEX_ST == 3 )); then
|
|
554
|
+
# a record cut mid-way keeps the words it has: they are judged; with none complete it is dropped (its words are not all there)
|
|
555
|
+
LEX_DONE=$((NW - REC_OFF[${#REC_OFF[@]} - 1]))
|
|
556
|
+
if (( LEX_DONE > 0 )); then REC_N[${#REC_N[@]} - 1]="$LEX_DONE"
|
|
557
|
+
else unset 'REC_OFF[${#REC_OFF[@]} - 1]' 'REC_N[${#REC_N[@]} - 1]'; fi
|
|
558
|
+
fi
|
|
559
|
+
break
|
|
560
|
+
fi
|
|
561
|
+
case "$LEX_ST" in
|
|
562
|
+
0) case "$F" in C) LEX_ST=1 ;; OK) SAW_OK=1 ;; E) LEX_ST=9 ;; *) LEX_BAD=1 ;; esac ;;
|
|
563
|
+
1) LEX_ST=2 ;; # the ctx field
|
|
564
|
+
2) if [[ "$F" =~ ^[1-9][0-9]{0,6}$ ]]; then
|
|
565
|
+
if (( ${#REC_N[@]} >= MAX_RECORDS )); then BOUND_SOFT="more than ${MAX_RECORDS} simple commands"; break; fi
|
|
566
|
+
if (( NW + F > MAX_WORDS )); then BOUND_SOFT="more than ${MAX_WORDS} words"; break; fi
|
|
567
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_SOFT="the ${LIMIT_S} s time limit was reached while reading the lexer output"; BOUND_READ=1; break; fi
|
|
568
|
+
REC_OFF[${#REC_OFF[@]}]="$NW"; REC_N[${#REC_N[@]}]="$F"; LEX_LEFT=$((F * 2)); LEX_KIND=0; LEX_ST=3
|
|
569
|
+
else LEX_BAD=1; LEX_ST=0; fi ;;
|
|
570
|
+
3) if [[ "$LEX_KIND" -eq 0 ]]; then W_FLG[${#W_FLG[@]}]="$F"; LEX_KIND=1
|
|
571
|
+
else
|
|
572
|
+
# a word over MAX_WORD_BYTES is replaced by a placeholder and the command asks `bound` unless something denies: the rule table's
|
|
573
|
+
# expansions on it are quadratic, the lexer's 2 s alarm does not cover them, and the clock is read only between records
|
|
574
|
+
if (( ${#F} > MAX_WORD_BYTES )); then
|
|
575
|
+
long_word_judged "$F" && BOUND_SOFT="a word longer than ${MAX_WORD_BYTES} bytes"
|
|
576
|
+
F="(overlong word)"
|
|
577
|
+
fi
|
|
578
|
+
W_TXT[${#W_TXT[@]}]="$F"; LEX_KIND=0; NW=$((NW + 1)); fi
|
|
579
|
+
LEX_LEFT=$((LEX_LEFT - 1)); [[ "$LEX_LEFT" -le 0 ]] && LEX_ST=0 ;;
|
|
580
|
+
9) SAW_E="$F"; LEX_ST=0 ;;
|
|
581
|
+
esac
|
|
582
|
+
done < <({ printf '%s' "$INPUT" | jq -j '.tool_input.command' | perl "$LEXER"; } 2>/dev/null)
|
|
583
|
+
|
|
584
|
+
# A read-time trip (a record or word cap, or the clock: BOUND_SOFT) does not discard the records already read: the rule table
|
|
585
|
+
# judges them, and the decision below is a deny if one matched, else a bound ask. After a clock trip the judging gets 2 more
|
|
586
|
+
# seconds counted from the trip (the harness kills the hook at 10 s; a trip detected late must not leave the judging no time at all).
|
|
587
|
+
if [[ "$BOUND_READ" -eq 1 ]]; then
|
|
588
|
+
DEADLINE_S=$((SECONDS + 2))
|
|
589
|
+
(( DEADLINE_S > 9 )) && DEADLINE_S=9 # inside the harness's 10 s kill: a trip detected at 8 s or later leaves the judging one second or none
|
|
590
|
+
fi
|
|
591
|
+
if [[ "$SAW_OK" -ne 1 && -z "$BOUND_SOFT" ]]; then
|
|
592
|
+
if [[ -n "$SAW_E" ]]; then ask_parse "lexer ${SAW_E}"; fi
|
|
593
|
+
# No OK and no E: the lexer was killed, crashed, or perl is missing/unusable. Probe by RESULT.
|
|
594
|
+
PERL_PROBE="$(perl -e 'print "ok"' 2>/dev/null)" || PERL_PROBE=""
|
|
595
|
+
if [[ "$PERL_PROBE" != ok || ! -r "$LEXER" ]]; then
|
|
596
|
+
echo "soleur destructive-command-guard: perl (or its lexer) is missing or unusable on this machine; scanning the decoded command with the guard's own narrow patterns instead of lexing it (install perl for full coverage)" >&2
|
|
597
|
+
DEC_CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command' 2>/dev/null)" || DEC_CMD=""
|
|
598
|
+
scan_narrow "$DEC_CMD"; SCAN_RC=$?
|
|
599
|
+
[[ "$SCAN_RC" -eq 2 ]] && ask_bound "$SCAN_WHY"
|
|
600
|
+
if [[ "$SCAN_RC" -eq 0 ]]; then
|
|
601
|
+
redact_text "$SCAN_SEG"
|
|
602
|
+
emit ask "guard-degraded-perl-missing: perl is missing or unusable on this machine, so the destructive-command guard scanned the decoded command with its narrow patterns instead of lexing it, and this call looks destructive. Matched segment: ${RTXT:0:QUOTE_MAX}.${REASON_TAIL}"
|
|
603
|
+
fi
|
|
604
|
+
exit 0
|
|
605
|
+
fi
|
|
606
|
+
ask_parse "the lexer produced no result"
|
|
607
|
+
fi
|
|
608
|
+
[[ "$LEX_BAD" -eq 1 ]] && ask_parse "the lexer output was malformed"
|
|
609
|
+
# OK with no record is the right answer for a blank or comment-only command and for a command whose text names nothing the
|
|
610
|
+
# guard decides on (a bare redirect, `> out.log`): there is nothing here for the guard to judge. A lexer that returned nothing
|
|
611
|
+
# for text that DOES name something the guard decides on has silently dropped its input, and asks. The test is on the text, in
|
|
612
|
+
# two parts, both a WORD test on the last path component of a token (so /bin/rm and ./rm count, `git/err` does not): (1) a token (the
|
|
613
|
+
# line split on blanks and shell metacharacters) that is rm, destroy, push, terraform, tofu, git or eval in any case, so a file NAME
|
|
614
|
+
# that merely contains one (terraform.log, format.log, rm.txt), quoted or not, is not a hit; and (2) when the line has a quote, a
|
|
615
|
+
# backslash, a backtick or a $ that is not a plain variable name, the line read again with those characters removed (r""m, 'r'm,
|
|
616
|
+
# r\m, $'r''m', ev""al) has such a token. A line whose first non-blank character is # is a comment and is skipped; a # later in a
|
|
617
|
+
# line is not a comment ("a #b" is a quoted string), so nothing after it is dropped. (Pass 1 does not split at a backslash, pass 2
|
|
618
|
+
# also splits at { and }: ${IFS}rm is IFS and rm.) The text is split into lines once (a loop that
|
|
619
|
+
# re-sliced the rest per line was quadratic) and the clock is read every 1024 lines. No process and no here-string (bash before 5.1
|
|
620
|
+
# writes a temporary file for <<<).
|
|
621
|
+
RE_DOLLAR_X='\$([^A-Za-z0-9_]|$)'
|
|
622
|
+
# le_words <text>: 0 when a blank-separated token of the text, taken by its last path component, is a keyword (the rule table
|
|
623
|
+
# compares the basename too, so /bin/rm and ./rm are rm). Reads under set -f and the caller's nocasematch; a token over MAX_WORD_BYTES is
|
|
624
|
+
# compared whole (a basename strip is quadratic in its length).
|
|
625
|
+
le_words() {
|
|
626
|
+
local w
|
|
627
|
+
for w in $1; do
|
|
628
|
+
(( ${#w} > MAX_WORD_BYTES )) || w="${w##*/}"
|
|
629
|
+
case "$w" in rm|destroy|push|terraform|tofu|git|eval) return 0 ;; esac
|
|
630
|
+
done
|
|
631
|
+
return 1
|
|
632
|
+
}
|
|
633
|
+
LE_CLOCK=0
|
|
634
|
+
lexer_empty_hit() { # <command text>: 0 when the text mentions what the guard decides on; LE_CLOCK=1 when the clock ran out first
|
|
635
|
+
local IFS=$'\n' ln j had=0 hit=1 nl=0
|
|
636
|
+
local -a lines
|
|
637
|
+
LE_CLOCK=0
|
|
638
|
+
shopt -q nocasematch && had=1
|
|
639
|
+
shopt -s nocasematch
|
|
640
|
+
# shellcheck disable=SC2206 # the split on newlines is the point (IFS is a newline, no pathname expansion: set -f)
|
|
641
|
+
lines=($1) # the text is split ONCE (set -f is on; blank lines vanish, which is harmless: a blank line is skipped anyway)
|
|
642
|
+
IFS=$' \t\n'
|
|
643
|
+
for ln in ${lines[@]+"${lines[@]}"}; do
|
|
644
|
+
nl=$((nl + 1))
|
|
645
|
+
if (( (nl & 1023) == 0 && SECONDS >= DEADLINE_S )); then LE_CLOCK=1; hit=0; break; fi
|
|
646
|
+
ln="${ln#"${ln%%[![:space:]]*}"}"
|
|
647
|
+
[[ -n "$ln" && "$ln" != '#'* ]] || continue
|
|
648
|
+
j="${ln//[;&|()<>\$\"\'\`]/ }" # (not the backslash: terraform\.log is a file name; pass 2 removes the backslash)
|
|
649
|
+
if le_words "$j"; then hit=0; break; fi
|
|
650
|
+
if [[ "$ln" == *[\'\"\\\`]* || "$ln" =~ $RE_DOLLAR_X ]]; then
|
|
651
|
+
j="${ln//[\'\"\\\`\$]/}"; j="${j//[;&|()<>{\}]/ }"
|
|
652
|
+
if le_words "$j"; then hit=0; break; fi
|
|
653
|
+
fi
|
|
654
|
+
done
|
|
655
|
+
(( had )) || shopt -u nocasematch
|
|
656
|
+
return "$hit"
|
|
657
|
+
}
|
|
658
|
+
if [[ "${#REC_N[@]}" -eq 0 && -z "$BOUND_SOFT" ]]; then
|
|
659
|
+
LX_CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command' 2>/dev/null)" || ask_lexer_empty
|
|
660
|
+
if lexer_empty_hit "$LX_CMD"; then
|
|
661
|
+
[[ "$LE_CLOCK" -eq 1 ]] && ask_bound "the ${LIMIT_S} s time limit was reached while checking whether the lexer dropped a command"
|
|
662
|
+
ask_lexer_empty
|
|
663
|
+
fi
|
|
664
|
+
fi
|
|
665
|
+
|
|
666
|
+
# ---- 5. the rule table -----------------------------------------------------------------------------
|
|
667
|
+
BEST_RANK=0; BEST_RULE=""; BEST_QUOTE=""
|
|
668
|
+
REC_RANK=0; REC_RULE=""
|
|
669
|
+
note() { # <rank 1|2> <rule id>
|
|
670
|
+
if (( $1 > REC_RANK )); then REC_RANK="$1"; REC_RULE="$2"; fi
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
SIMCWD=""; ORIGCWD=""; CWD_READY=0; UNRES=0; EH=""; EH_READY=0
|
|
674
|
+
HL=""; HP=""; HOME_READY=0
|
|
675
|
+
GIT_SCRUBBED=0
|
|
676
|
+
AV=(); AF=(); DA_T=(); DA_F=()
|
|
677
|
+
EW=""; EW_Q=0; RP=""; LN=""; WJ=-1
|
|
678
|
+
|
|
679
|
+
# lex_norm <abs path> -> LN: collapse // . .. lexically.
|
|
680
|
+
lex_norm() {
|
|
681
|
+
local IFS=/ part i n=0
|
|
682
|
+
local -a stack=()
|
|
683
|
+
for part in $1; do
|
|
684
|
+
case "$part" in
|
|
685
|
+
''|.) : ;;
|
|
686
|
+
..) if (( n > 0 )); then n=$((n - 1)); unset "stack[$n]"; fi ;;
|
|
687
|
+
*) stack[n]="$part"; n=$((n + 1)) ;;
|
|
688
|
+
esac
|
|
689
|
+
done
|
|
690
|
+
LN=""
|
|
691
|
+
for ((i = 0; i < n; i++)); do LN="$LN/${stack[$i]}"; done
|
|
692
|
+
[[ -n "$LN" ]] || LN=/
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
# resolve_phys <abs path> <follow 0|1> -> RP (empty = no decision: rm refuses `.`)
|
|
696
|
+
# follow=1: the path itself is resolved through symlinks (a trailing slash or a glob suffix); follow=0: a
|
|
697
|
+
# bare final name stays literal (`rm -rf link` only unlinks the link). A path with more than MAX_PATH_COMPONENTS components
|
|
698
|
+
# sets BOUND_SOFT and returns 1 (nothing about that path was resolved); the rest of the command is still judged, so a
|
|
699
|
+
# deny elsewhere in it wins, and the decision asks `bound` when nothing denied.
|
|
700
|
+
#
|
|
701
|
+
# phys_walk <abs dir> -> WK WP: ONE subshell finds the longest existing prefix of the directory (WP, physical) and how
|
|
702
|
+
# many trailing components it had to drop to get there (WK; 0 = the directory itself exists). The answer is cached per
|
|
703
|
+
# directory (a long target list shares its parents) in a table capped at PW_MAX entries, so a lookup is a short scan;
|
|
704
|
+
# only this final per-directory answer is cached, never a probe of a prefix.
|
|
705
|
+
PW_K=(); PW_V=()
|
|
706
|
+
PW_MAX=256
|
|
707
|
+
phys_walk() {
|
|
708
|
+
local k n=${#PW_K[@]} out
|
|
709
|
+
for ((k = 0; k < n; k++)); do
|
|
710
|
+
if [[ "${PW_K[$k]}" == "$1" ]]; then out="${PW_V[$k]}"; WK="${out%%$'\n'*}"; WP="${out#*$'\n'}"; return 0; fi
|
|
711
|
+
done
|
|
712
|
+
out="$( cd / && q="$1" && c=0 && while ! cd -P -- "$q" 2>/dev/null; do c=$((c + 1)); q="${q%/*}"; [[ -z "$q" ]] && q=/; (( c > MAX_PATH_COMPONENTS + 2 )) && break; done; printf '%s\n' "$c"; pwd -P )" || out=""
|
|
713
|
+
WK="${out%%$'\n'*}"; WP="${out#*$'\n'}"
|
|
714
|
+
[[ "$WK" =~ ^[0-9]+$ ]] || { WK=0; WP=""; }
|
|
715
|
+
if (( n < PW_MAX )); then PW_K[n]="$1"; PW_V[n]="$WK"$'\n'"$WP"; fi
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
resolve_phys() {
|
|
719
|
+
local p="$1" follow="$2" last dir rest probe slashes k
|
|
720
|
+
RP=""
|
|
721
|
+
if (( ${#p} > MAX_WORD_BYTES )); then BOUND_SOFT="a path longer than ${MAX_WORD_BYTES} bytes"; return 1; fi
|
|
722
|
+
while [[ "$p" == */ && "$p" != / ]]; do p="${p%/}"; done
|
|
723
|
+
if [[ "$p" == / ]]; then RP=/; return 0; fi
|
|
724
|
+
slashes="${p//[!\/]/}"
|
|
725
|
+
if (( ${#slashes} > MAX_PATH_COMPONENTS )); then BOUND_SOFT="a path with more than ${MAX_PATH_COMPONENTS} components"; return 1; fi
|
|
726
|
+
last="${p##*/}"
|
|
727
|
+
if [[ "$follow" == 0 && "$last" == . ]]; then return 1; fi
|
|
728
|
+
dir="${p%/*}"; [[ -z "$dir" ]] && dir=/
|
|
729
|
+
if [[ "$follow" == 1 || "$last" == .. ]]; then
|
|
730
|
+
# the path itself first; when it is missing the same walk already knows its parent (one component fewer)
|
|
731
|
+
phys_walk "$p"
|
|
732
|
+
if (( WK == 0 )) && [[ -n "$WP" ]]; then RP="$WP"; return 0; fi
|
|
733
|
+
k=$((WK - 1)); [[ "$k" -lt 0 ]] && k=0
|
|
734
|
+
else
|
|
735
|
+
phys_walk "$dir"
|
|
736
|
+
k="$WK"
|
|
737
|
+
fi
|
|
738
|
+
[[ -n "$WP" ]] || { WP=/; }
|
|
739
|
+
rest="$last"; probe="$dir"
|
|
740
|
+
while (( k > 0 )); do
|
|
741
|
+
rest="${probe##*/}/$rest"; probe="${probe%/*}"; [[ -z "$probe" ]] && probe=/
|
|
742
|
+
k=$((k - 1))
|
|
743
|
+
done
|
|
744
|
+
lex_norm "${WP%/}/$rest"; RP="$LN"
|
|
745
|
+
return 0
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
anc_or_eq() { # <a> <b>: a is /, equals b, or is an ancestor of b
|
|
749
|
+
local a="$1" b="$2"
|
|
750
|
+
while [[ "$a" == */ && "$a" != / ]]; do a="${a%/}"; done
|
|
751
|
+
while [[ "$b" == */ && "$b" != / ]]; do b="${b%/}"; done
|
|
752
|
+
[[ -z "$a" ]] && a=/
|
|
753
|
+
[[ -z "$b" ]] && b=/
|
|
754
|
+
[[ "$a" == / || "$b" == "$a" || "$b" == "$a"/* ]]
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
cwd_ready() {
|
|
758
|
+
[[ "$CWD_READY" -eq 1 ]] && return 0
|
|
759
|
+
CWD_READY=1
|
|
760
|
+
local base="${CWD_IN:-${CLAUDE_PROJECT_DIR:-${PWD:-/}}}"
|
|
761
|
+
[[ "$base" == /* ]] || base="${PWD:-/}/$base"
|
|
762
|
+
resolve_phys "$base" 1 || RP="$base"
|
|
763
|
+
SIMCWD="$RP"; ORIGCWD="$RP"
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
# eff_home -> EH: HOME when it is set, else the directory bash itself would use for ~ (the passwd entry), else
|
|
767
|
+
# empty. The lookup is one subshell, and only when HOME is empty or unset.
|
|
768
|
+
eff_home() {
|
|
769
|
+
[[ "$EH_READY" -eq 1 ]] && return 0
|
|
770
|
+
EH_READY=1
|
|
771
|
+
if [[ -n "${HOME:-}" ]]; then EH="$HOME"; else EH="$( ( unset HOME; cd ~ 2>/dev/null && pwd -P ) 2>/dev/null )"; fi
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
home_ready() {
|
|
775
|
+
[[ "$HOME_READY" -eq 1 ]] && return 0
|
|
776
|
+
HOME_READY=1
|
|
777
|
+
eff_home
|
|
778
|
+
local h="$EH"
|
|
779
|
+
if [[ "$h" != /* ]]; then HL="/nonexistent-home-unset"; HP="$HL"; return 0; fi
|
|
780
|
+
lex_norm "$h"; HL="$LN"
|
|
781
|
+
resolve_phys "$h" 1 || RP="$HL"
|
|
782
|
+
HP="$RP"
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
scrub_git_env() {
|
|
786
|
+
[[ "$GIT_SCRUBBED" -eq 1 ]] && return 0
|
|
787
|
+
GIT_SCRUBBED=1
|
|
788
|
+
local v
|
|
789
|
+
while IFS= read -r v; do
|
|
790
|
+
case "$v" in GIT_*) unset "$v" ;; esac
|
|
791
|
+
done < <(compgen -e)
|
|
792
|
+
export GIT_TERMINAL_PROMPT=0
|
|
793
|
+
}
|
|
794
|
+
git_ro() { # <repo dir> <args...>: read-only, local only
|
|
795
|
+
local repo="$1"; shift
|
|
796
|
+
scrub_git_env
|
|
797
|
+
git -C "$repo" "$@" 2>/dev/null </dev/null
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
is_assign() { [[ "$1" =~ ^[A-Za-z_][A-Za-z0-9_]*\+?= ]]; }
|
|
801
|
+
|
|
802
|
+
# expand_word <text> <flags> -> EW, EW_Q; returns 1 when the word cannot be resolved (a variable target is
|
|
803
|
+
# no decision). Tilde: only an unquoted leading ~ or ~/ (plus the quoted `~/` and `~/*` forms, see header);
|
|
804
|
+
# $HOME ${HOME} $PWD ${PWD} when expanded; any other expansion is unresolvable.
|
|
805
|
+
expand_word() {
|
|
806
|
+
local t="$1" fl="$2" xd=0 qd=0 P
|
|
807
|
+
EW=""; EW_Q=0
|
|
808
|
+
case "$fl" in *q*) qd=1 ;; esac
|
|
809
|
+
case "$fl" in *x*) xd=1 ;; esac
|
|
810
|
+
EW_Q="$qd"
|
|
811
|
+
if (( qd == 0 )) || [[ "$t" == '~/' || "$t" == '~/*' ]]; then
|
|
812
|
+
case "$t" in
|
|
813
|
+
'~'|'~/'*) eff_home; [[ -n "$EH" ]] || return 1; P='~'; t="$EH${t#"$P"}" ;;
|
|
814
|
+
'~+'|'~+/'*) cwd_ready; P='~+'; t="$SIMCWD${t#"$P"}" ;;
|
|
815
|
+
'~'*) return 1 ;;
|
|
816
|
+
esac
|
|
817
|
+
fi
|
|
818
|
+
if (( xd )); then
|
|
819
|
+
case "$t" in
|
|
820
|
+
'$HOME'|'$HOME/'*) P='$HOME'; t="${HOME:-}${t#"$P"}" ;; # an unset HOME expands to nothing: $HOME/* is /*
|
|
821
|
+
'${HOME}'|'${HOME}/'*) P='${HOME}'; t="${HOME:-}${t#"$P"}" ;;
|
|
822
|
+
'$PWD'|'$PWD/'*) cwd_ready; P='$PWD'; t="$SIMCWD${t#"$P"}" ;;
|
|
823
|
+
'${PWD}'|'${PWD}/'*) cwd_ready; P='${PWD}'; t="$SIMCWD${t#"$P"}" ;;
|
|
824
|
+
esac
|
|
825
|
+
case "$t" in *'$'*|*'`'*|*'<('*|*'>('*) return 1 ;; esac
|
|
826
|
+
fi
|
|
827
|
+
[[ -n "$t" ]] || return 1
|
|
828
|
+
EW="$t"
|
|
829
|
+
return 0
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
# glob_contents <text> <quoted 0|1> -> GD: 0 when the target names the CONTENTS of a directory, i.e. every path
|
|
833
|
+
# component after the glob-free root is only glob syntax (`*`, `**`, `?`, `[a-z]`, in any run): /** /*/* /*/ /?
|
|
834
|
+
# /[a-z]* ~/** and a bare * or ./*. A component that mixes literal text with a glob (`*.log`, `.*`, `node_modules`
|
|
835
|
+
# after a `*`) is a different set of files and is not matched. A quoted target is a glob only in the old
|
|
836
|
+
# trailing-/* spelling (see the header: `~/*` is read as home even when quoted).
|
|
837
|
+
RE_GLOB_ONLY='^(\*|\?|\[[^]]+\])+$'
|
|
838
|
+
glob_contents() {
|
|
839
|
+
local t="$1" qd="$2" comp="" root="" seen=0 IFS=/ lead=""
|
|
840
|
+
GD=""
|
|
841
|
+
if (( qd )); then
|
|
842
|
+
if [[ "$t" == */'*' ]]; then GD="${t%/\*}"; [[ -z "$GD" ]] && GD=/; return 0; fi
|
|
843
|
+
return 1
|
|
844
|
+
fi
|
|
845
|
+
[[ "$t" == /* ]] && lead=/
|
|
846
|
+
for comp in $t; do
|
|
847
|
+
if (( seen )); then
|
|
848
|
+
[[ -z "$comp" ]] && continue
|
|
849
|
+
[[ "$comp" =~ $RE_GLOB_ONLY ]] || return 1
|
|
850
|
+
elif [[ "$comp" =~ $RE_GLOB_ONLY ]]; then seen=1
|
|
851
|
+
else
|
|
852
|
+
case "$comp" in *'*'*|*'?'*|*'['*) return 1 ;; esac
|
|
853
|
+
[[ -n "$comp" ]] && root="${root:+$root/}$comp"
|
|
854
|
+
fi
|
|
855
|
+
done
|
|
856
|
+
(( seen )) || return 1
|
|
857
|
+
if [[ -n "$lead" ]]; then GD="/$root"; else GD="${root:-.}"; fi
|
|
858
|
+
return 0
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
rule_rm() {
|
|
862
|
+
local -a a=("${AV[@]}") fl=("${AF[@]}") tix=()
|
|
863
|
+
local n=${#a[@]} k x name rec=0 end=0 glob follow T D
|
|
864
|
+
for ((k = 1; k < n; k++)); do
|
|
865
|
+
x="${a[$k]}"
|
|
866
|
+
if (( end )); then tix[${#tix[@]}]="$k"
|
|
867
|
+
elif [[ "$x" == -- ]]; then end=1
|
|
868
|
+
elif [[ "$x" == --* ]]; then
|
|
869
|
+
name="${x#--}"
|
|
870
|
+
if [[ -n "$name" && "recursive" == "$name"* ]]; then rec=1; fi
|
|
871
|
+
elif [[ "$x" == -?* ]]; then
|
|
872
|
+
case "${x:1}" in *[rR]*) rec=1 ;; esac
|
|
873
|
+
else tix[${#tix[@]}]="$k"; fi
|
|
874
|
+
done
|
|
875
|
+
(( rec )) || return 0
|
|
876
|
+
cwd_ready; home_ready
|
|
877
|
+
if (( UNRES )); then note 1 unresolved-cd-before-destructive; fi
|
|
878
|
+
for k in ${tix[@]+"${tix[@]}"}; do
|
|
879
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking the targets of rm"; return 0; fi
|
|
880
|
+
expand_word "${a[$k]}" "${fl[$k]}" || continue
|
|
881
|
+
T="$EW"; glob=0; follow=0
|
|
882
|
+
if glob_contents "$T" "$EW_Q"; then glob=1; follow=1; D="$GD"
|
|
883
|
+
elif [[ "$T" == */'*' ]]; then glob=1; follow=1; D="${T%/\*}"; [[ -z "$D" ]] && D=/
|
|
884
|
+
elif [[ "$T" == */ ]]; then
|
|
885
|
+
follow=1; D="$T"
|
|
886
|
+
x="$D"; while [[ "$x" == */ && "$x" != / ]]; do x="${x%/}"; done
|
|
887
|
+
[[ "$x" != / && "${x##*/}" == . ]] && continue
|
|
888
|
+
else D="$T"; fi
|
|
889
|
+
[[ "$D" == /* ]] || D="$SIMCWD/$D"
|
|
890
|
+
resolve_phys "$D" "$follow" || continue
|
|
891
|
+
[[ -n "$RP" ]] || continue
|
|
892
|
+
if anc_or_eq "$RP" "$HL" || anc_or_eq "$RP" "$HP"; then note 2 recursive-delete-home
|
|
893
|
+
elif (( glob == 0 )) && { anc_or_eq "$RP" "$SIMCWD" || anc_or_eq "$RP" "$ORIGCWD"; }; then note 1 recursive-delete-workdir; fi
|
|
894
|
+
done
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
rule_tf() {
|
|
898
|
+
local -a a=("${AV[@]}")
|
|
899
|
+
local n=${#a[@]} i=1 sub x
|
|
900
|
+
while (( i < n )) && [[ "${a[$i]}" == -* ]]; do i=$((i + 1)); done
|
|
901
|
+
(( i < n )) || return 0
|
|
902
|
+
sub="${a[$i]}"
|
|
903
|
+
if [[ "$sub" == destroy ]]; then note 1 infra-destroy; return 0; fi
|
|
904
|
+
if [[ "$sub" == apply ]]; then
|
|
905
|
+
for ((i = i + 1; i < n; i++)); do
|
|
906
|
+
x="${a[$i]}"
|
|
907
|
+
case "$x" in
|
|
908
|
+
-destroy|--destroy) note 1 infra-destroy; return 0 ;;
|
|
909
|
+
-destroy=*|--destroy=*) # a Go bool: 0 f F false FALSE False turn it off, every other value is on (or an error)
|
|
910
|
+
case "${x#*=}" in 0|f|F|false|FALSE|False) : ;; *) note 1 infra-destroy; return 0 ;; esac ;;
|
|
911
|
+
esac
|
|
912
|
+
done
|
|
913
|
+
fi
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
rule_git() {
|
|
917
|
+
local -a a=("${AV[@]}") pos=() refs=() D_DST=() D_F=() D_DEL=()
|
|
918
|
+
local n=${#a[@]} i=1 j k c x name end=0 repo cur head remote named
|
|
919
|
+
local force=0 del=0 allf=0 mirror=0 repoopt=0 risk=0 r f dele src dst mt=0 mtf=0
|
|
920
|
+
cwd_ready; repo="$SIMCWD"
|
|
921
|
+
while (( i < n )); do
|
|
922
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking the options of git"; return 0; fi
|
|
923
|
+
case "${a[$i]}" in
|
|
924
|
+
-C) if (( i + 1 < n )); then
|
|
925
|
+
case "${a[$((i + 1))]}" in /*) repo="${a[$((i + 1))]}" ;; *) repo="$repo/${a[$((i + 1))]}" ;; esac
|
|
926
|
+
fi
|
|
927
|
+
i=$((i + 2)) ;;
|
|
928
|
+
# the separate-argument forms take the next word; `--opt=value` and every other flag take none
|
|
929
|
+
-c|--config-env|--git-dir|--work-tree|--namespace|--super-prefix|--attr-source) i=$((i + 2)) ;;
|
|
930
|
+
-*) i=$((i + 1)) ;;
|
|
931
|
+
*) break ;;
|
|
932
|
+
esac
|
|
933
|
+
done
|
|
934
|
+
[[ "${a[$i]:-}" == push ]] || return 0
|
|
935
|
+
j=$((i + 1))
|
|
936
|
+
while (( j < n )); do
|
|
937
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking the flags of git push"; return 0; fi
|
|
938
|
+
x="${a[$j]}"
|
|
939
|
+
if (( end )); then pos[${#pos[@]}]="$x"
|
|
940
|
+
elif [[ "$x" == -- ]]; then end=1
|
|
941
|
+
elif [[ "$x" == --* ]]; then
|
|
942
|
+
name="${x%%=*}"
|
|
943
|
+
case "$name" in
|
|
944
|
+
--force*) force=1 ;;
|
|
945
|
+
--de|--del|--dele|--delet|--delete) del=1 ;;
|
|
946
|
+
--al|--all) allf=1 ;; # --al is the shortest unique abbreviation (--a is ambiguous)
|
|
947
|
+
--m|--mi|--mir|--mirr|--mirro|--mirror) mirror=1 ;;
|
|
948
|
+
--repo) repoopt=1; [[ "$x" == *=* ]] || j=$((j + 1)) ;;
|
|
949
|
+
--push-option|--receive-pack|--exec) [[ "$x" == *=* ]] || j=$((j + 1)) ;;
|
|
950
|
+
esac
|
|
951
|
+
elif [[ "$x" == -?* ]]; then
|
|
952
|
+
for ((k = 1; k < ${#x}; k++)); do
|
|
953
|
+
c="${x:$k:1}"
|
|
954
|
+
case "$c" in
|
|
955
|
+
f) force=1 ;;
|
|
956
|
+
d) del=1 ;;
|
|
957
|
+
o) [[ "$k" -eq $((${#x} - 1)) ]] && j=$((j + 1)); break ;;
|
|
958
|
+
esac
|
|
959
|
+
done
|
|
960
|
+
else pos[${#pos[@]}]="$x"; fi
|
|
961
|
+
j=$((j + 1))
|
|
962
|
+
done
|
|
963
|
+
remote=""
|
|
964
|
+
if (( repoopt )); then
|
|
965
|
+
for x in ${pos[@]+"${pos[@]}"}; do refs[${#refs[@]}]="$x"; done
|
|
966
|
+
else
|
|
967
|
+
if (( ${#pos[@]} > 0 )); then remote="${pos[0]}"; fi
|
|
968
|
+
for ((k = 1; k < ${#pos[@]}; k++)); do refs[${#refs[@]}]="${pos[$k]}"; done
|
|
969
|
+
fi
|
|
970
|
+
named="${remote:-origin}"
|
|
971
|
+
# The destinations, flagged: +refspec / +dst (force) and :dst (delete) are risky on their own.
|
|
972
|
+
for r in ${refs[@]+"${refs[@]}"}; do
|
|
973
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking the refs of git push"; return 0; fi
|
|
974
|
+
f=0; dele=0
|
|
975
|
+
if [[ "$r" == +* ]]; then f=1; r="${r#+}"; fi
|
|
976
|
+
if [[ "$r" == : ]]; then
|
|
977
|
+
mt=1; (( f )) && mtf=1 # the matching refspec: every branch both sides have
|
|
978
|
+
dst=""
|
|
979
|
+
elif [[ "$r" == *:* ]]; then
|
|
980
|
+
src="${r%%:*}"; dst="${r#*:}"
|
|
981
|
+
if [[ "$dst" == +* ]]; then f=1; dst="${dst#+}"; fi
|
|
982
|
+
[[ -z "$src" ]] && dele=1
|
|
983
|
+
else
|
|
984
|
+
dst="$r"
|
|
985
|
+
fi
|
|
986
|
+
(( f || dele )) && risk=1
|
|
987
|
+
D_DST[${#D_DST[@]}]="$dst"; D_F[${#D_F[@]}]="$f"; D_DEL[${#D_DEL[@]}]="$dele"
|
|
988
|
+
done
|
|
989
|
+
(( force || del )) && risk=1
|
|
990
|
+
(( risk )) || return 0
|
|
991
|
+
# The directory the push acts on depends on the working directory: an unresolvable cd decides it here.
|
|
992
|
+
if (( UNRES )); then note 1 unresolved-cd-before-destructive; return 0; fi
|
|
993
|
+
if (( (allf || mirror) && force )); then note 1 default-branch-force-push; return 0; fi
|
|
994
|
+
if (( mt && (force || mtf) )); then note 1 default-branch-force-push; return 0; fi
|
|
995
|
+
head=""; cur=""; local have_head=0 have_cur=0
|
|
996
|
+
local -a defaults=(main master)
|
|
997
|
+
for ((k = 0; k < ${#D_DST[@]}; k++)); do
|
|
998
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking the destinations of git push"; return 0; fi
|
|
999
|
+
dst="${D_DST[$k]}"
|
|
1000
|
+
if [[ "$dst" == HEAD ]]; then
|
|
1001
|
+
if (( ! have_cur )); then cur="$(git_ro "$repo" symbolic-ref --short HEAD)"; have_cur=1; fi
|
|
1002
|
+
dst="$cur"
|
|
1003
|
+
fi
|
|
1004
|
+
dst="${dst#refs/heads/}"; dst="${dst#heads/}"
|
|
1005
|
+
[[ -n "$dst" ]] || continue
|
|
1006
|
+
if (( force || D_F[k] || del || D_DEL[k] )); then
|
|
1007
|
+
# a destination with glob syntax can name a default branch (refs/heads/*:refs/heads/*)
|
|
1008
|
+
case "$dst" in *'*'*|*'?'*|*'['*) note 1 default-branch-force-push; return 0 ;; esac
|
|
1009
|
+
if [[ "$dst" == main || "$dst" == master ]]; then note 1 default-branch-force-push; return 0; fi
|
|
1010
|
+
if (( ! have_head )); then
|
|
1011
|
+
head="$(git_ro "$repo" symbolic-ref --short "refs/remotes/$named/HEAD")"; have_head=1
|
|
1012
|
+
head="${head#"$named"/}"
|
|
1013
|
+
[[ -n "$head" ]] && defaults[${#defaults[@]}]="$head"
|
|
1014
|
+
fi
|
|
1015
|
+
for x in "${defaults[@]}"; do
|
|
1016
|
+
if [[ "$dst" == "$x" ]]; then note 1 default-branch-force-push; return 0; fi
|
|
1017
|
+
done
|
|
1018
|
+
fi
|
|
1019
|
+
done
|
|
1020
|
+
# No refspec at all: the push goes to the current branch (the upstream default).
|
|
1021
|
+
if (( ${#D_DST[@]} == 0 && allf == 0 && mirror == 0 && force )); then
|
|
1022
|
+
cur="$(git_ro "$repo" symbolic-ref --short HEAD)"
|
|
1023
|
+
if [[ -n "$cur" ]]; then
|
|
1024
|
+
head="$(git_ro "$repo" symbolic-ref --short "refs/remotes/$named/HEAD")"; head="${head#"$named"/}"
|
|
1025
|
+
if [[ "$cur" == main || "$cur" == master || ( -n "$head" && "$cur" == "$head" ) ]]; then
|
|
1026
|
+
note 1 default-branch-force-push; return 0
|
|
1027
|
+
fi
|
|
1028
|
+
fi
|
|
1029
|
+
fi
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
# short_val <value-taking letters> <option word>: 0 when a short-option cluster (-nHu) ENDS in a value-taking
|
|
1033
|
+
# letter, so that option's value is the NEXT word. A value-taking letter earlier in the cluster takes the rest of
|
|
1034
|
+
# the word as its value (-uroot), which needs no extra word.
|
|
1035
|
+
short_val() {
|
|
1036
|
+
local letters="$1" w="$2" k c
|
|
1037
|
+
for ((k = 1; k < ${#w}; k++)); do
|
|
1038
|
+
c="${w:$k:1}"
|
|
1039
|
+
case "$letters" in *"$c"*) (( k == ${#w} - 1 )); return ;; esac
|
|
1040
|
+
done
|
|
1041
|
+
return 1
|
|
1042
|
+
}
|
|
1043
|
+
|
|
1044
|
+
# short_first_val <value-taking letters> <option word> -> SFV SFV_REST: the first value-taking letter of a short-option cluster and
|
|
1045
|
+
# what follows it in the word (empty = the value is the NEXT word; non-empty = the value is attached: -D/tmp).
|
|
1046
|
+
SFV=""; SFV_REST=""
|
|
1047
|
+
short_first_val() {
|
|
1048
|
+
local letters="$1" w="$2" k c
|
|
1049
|
+
SFV=""; SFV_REST=""
|
|
1050
|
+
for ((k = 1; k < ${#w}; k++)); do
|
|
1051
|
+
c="${w:$k:1}"
|
|
1052
|
+
case "$letters" in *"$c"*) SFV="$c"; SFV_REST="${w:$((k + 1))}"; return 0 ;; esac
|
|
1053
|
+
done
|
|
1054
|
+
return 1
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
# A wrapper's chdir option (env -C DIR, --chdir DIR; sudo -D DIR, --chdir DIR) is reported in WCD (the directory word), WCDF (its
|
|
1058
|
+
# lexer flags) and WCD_SET: the wrapped command runs there, and decide_walk judges it with the simulated working directory moved.
|
|
1059
|
+
WCD=""; WCDF="-"; WCD_SET=0
|
|
1060
|
+
WCD_RUNNER=0
|
|
1061
|
+
|
|
1062
|
+
# wrap_skip <name> <index of name in t> -> WJ: the index of the wrapped command's first word, or -1 when
|
|
1063
|
+
# the wrapper is a look-up only (`command -v`) or its payload is a string the guard does not analyse (env -S).
|
|
1064
|
+
# Reads t[] and n of the caller (dynamic scope, by design).
|
|
1065
|
+
wrap_skip() {
|
|
1066
|
+
local name="$1" j=$(($2 + 1)) a lname k c eat split opt
|
|
1067
|
+
WJ=-1
|
|
1068
|
+
case "$name" in
|
|
1069
|
+
sudo)
|
|
1070
|
+
while (( j < n )) && [[ "${t[$j]}" == -* && "${t[$j]}" != - ]]; do
|
|
1071
|
+
a="${t[$j]}"; k=$j; j=$((j + 1))
|
|
1072
|
+
[[ "$a" == -- ]] && break
|
|
1073
|
+
case "$a" in
|
|
1074
|
+
--*) # sudo (getopt_long) takes any unique abbreviation of a long option: --us root is --user root, --chd D is --chdir D
|
|
1075
|
+
lname="${a%%=*}"; lname="${lname#--}"
|
|
1076
|
+
if [[ -n "$lname" && "chdir" == "$lname"* ]]; then
|
|
1077
|
+
if [[ "$a" == *=* ]]; then WCD="${a#*=}"; WCDF="${f[$k]:--}"; WCD_SET=1
|
|
1078
|
+
else WCD="${t[$j]:-}"; WCDF="${f[$j]:--}"; WCD_SET=1; j=$((j + 1)); fi
|
|
1079
|
+
elif [[ "$a" != *=* && -n "$lname" ]]; then
|
|
1080
|
+
for opt in user group host prompt chroot role type close-from command-timeout other-user; do
|
|
1081
|
+
if [[ "$opt" == "$lname"* ]]; then j=$((j + 1)); break; fi
|
|
1082
|
+
done
|
|
1083
|
+
fi ;;
|
|
1084
|
+
*) if short_first_val ughpCTUDRrt "$a"; then
|
|
1085
|
+
if [[ -z "$SFV_REST" ]]; then
|
|
1086
|
+
[[ "$SFV" == D ]] && { WCD="${t[$j]:-}"; WCDF="${f[$j]:--}"; WCD_SET=1; }
|
|
1087
|
+
j=$((j + 1))
|
|
1088
|
+
else
|
|
1089
|
+
[[ "$SFV" == D ]] && { WCD="$SFV_REST"; WCDF="${f[$k]:--}"; WCD_SET=1; }
|
|
1090
|
+
fi
|
|
1091
|
+
fi ;;
|
|
1092
|
+
esac
|
|
1093
|
+
done
|
|
1094
|
+
while (( j < n )) && is_assign "${t[$j]}"; do j=$((j + 1)); done ;;
|
|
1095
|
+
doas)
|
|
1096
|
+
while (( j < n )) && [[ "${t[$j]}" == -* && "${t[$j]}" != - ]]; do
|
|
1097
|
+
a="${t[$j]}"; j=$((j + 1))
|
|
1098
|
+
[[ "$a" == -- ]] && break
|
|
1099
|
+
if short_val uC "$a"; then j=$((j + 1)); fi
|
|
1100
|
+
done ;;
|
|
1101
|
+
env)
|
|
1102
|
+
while (( j < n )); do
|
|
1103
|
+
a="${t[$j]}"
|
|
1104
|
+
if [[ "$a" == -- ]]; then j=$((j + 1)); break
|
|
1105
|
+
elif [[ "$a" == - ]]; then j=$((j + 1))
|
|
1106
|
+
elif [[ "$a" == --* ]]; then
|
|
1107
|
+
lname="${a%%=*}"; lname="${lname#--}"
|
|
1108
|
+
if [[ -n "$lname" && "split-string" == "$lname"* ]]; then note 1 unparsed-wrapper; return 0; fi
|
|
1109
|
+
if [[ -n "$lname" && "chdir" == "$lname"* ]]; then
|
|
1110
|
+
if [[ "$a" == *=* ]]; then WCD="${a#*=}"; WCDF="${f[$j]:--}"; WCD_SET=1
|
|
1111
|
+
else WCD="${t[$((j + 1))]:-}"; WCDF="${f[$((j + 1))]:--}"; WCD_SET=1; fi
|
|
1112
|
+
fi
|
|
1113
|
+
j=$((j + 1))
|
|
1114
|
+
# a value-taking long option with its value in the next word: --unset X, --chdir D, --argv0 A
|
|
1115
|
+
if [[ "$a" != *=* && -n "$lname" ]] && { [[ "unset" == "$lname"* ]] || [[ "chdir" == "$lname"* ]] || [[ "argv0" == "$lname"* ]]; }; then j=$((j + 1)); fi
|
|
1116
|
+
elif [[ "$a" == -?* ]]; then
|
|
1117
|
+
eat=0; split=0
|
|
1118
|
+
for ((k = 1; k < ${#a}; k++)); do
|
|
1119
|
+
c="${a:$k:1}"
|
|
1120
|
+
case "$c" in
|
|
1121
|
+
S) split=1; break ;;
|
|
1122
|
+
u|C|P|a)
|
|
1123
|
+
if (( k == ${#a} - 1 )); then
|
|
1124
|
+
eat=1
|
|
1125
|
+
[[ "$c" == C ]] && { WCD="${t[$((j + 1))]:-}"; WCDF="${f[$((j + 1))]:--}"; WCD_SET=1; }
|
|
1126
|
+
else
|
|
1127
|
+
[[ "$c" == C ]] && { WCD="${a:$((k + 1))}"; WCDF="${f[$j]:--}"; WCD_SET=1; }
|
|
1128
|
+
fi
|
|
1129
|
+
break ;;
|
|
1130
|
+
esac
|
|
1131
|
+
done
|
|
1132
|
+
if (( split )); then note 1 unparsed-wrapper; return 0; fi
|
|
1133
|
+
j=$((j + 1 + eat))
|
|
1134
|
+
elif is_assign "$a"; then j=$((j + 1))
|
|
1135
|
+
else break; fi
|
|
1136
|
+
done ;;
|
|
1137
|
+
timeout)
|
|
1138
|
+
while (( j < n )) && [[ "${t[$j]}" == -* ]]; do
|
|
1139
|
+
a="${t[$j]}"; j=$((j + 1))
|
|
1140
|
+
case "$a" in
|
|
1141
|
+
--*) # getopt_long takes a unique abbreviation: --sig KILL is --signal KILL
|
|
1142
|
+
lname="${a%%=*}"; lname="${lname#--}"
|
|
1143
|
+
if [[ "$a" != *=* && -n "$lname" ]] && { [[ signal == "$lname"* ]] || [[ kill-after == "$lname"* ]]; }; then j=$((j + 1)); fi ;;
|
|
1144
|
+
*) if short_val sk "$a"; then j=$((j + 1)); fi ;;
|
|
1145
|
+
esac
|
|
1146
|
+
done
|
|
1147
|
+
j=$((j + 1)) ;;
|
|
1148
|
+
nice)
|
|
1149
|
+
while (( j < n )) && [[ "${t[$j]}" == -* ]]; do
|
|
1150
|
+
a="${t[$j]}"; j=$((j + 1))
|
|
1151
|
+
case "$a" in
|
|
1152
|
+
--*)
|
|
1153
|
+
lname="${a%%=*}"; lname="${lname#--}"
|
|
1154
|
+
if [[ "$a" != *=* && -n "$lname" && adjustment == "$lname"* ]]; then j=$((j + 1)); fi ;;
|
|
1155
|
+
*) if short_val n "$a"; then j=$((j + 1)); fi ;;
|
|
1156
|
+
esac
|
|
1157
|
+
done ;;
|
|
1158
|
+
time) # the external time(1) (/usr/bin/time, `command time`, `env time`); the reserved word is the lexer's
|
|
1159
|
+
while (( j < n )) && [[ "${t[$j]}" == -* && "${t[$j]}" != - ]]; do
|
|
1160
|
+
a="${t[$j]}"; j=$((j + 1))
|
|
1161
|
+
[[ "$a" == -- ]] && break
|
|
1162
|
+
case "$a" in
|
|
1163
|
+
--*)
|
|
1164
|
+
lname="${a%%=*}"; lname="${lname#--}"
|
|
1165
|
+
if [[ "$a" != *=* && -n "$lname" ]] && { [[ format == "$lname"* ]] || [[ output == "$lname"* ]]; }; then j=$((j + 1)); fi ;;
|
|
1166
|
+
*) if short_val fo "$a"; then j=$((j + 1)); fi ;;
|
|
1167
|
+
esac
|
|
1168
|
+
done ;;
|
|
1169
|
+
command)
|
|
1170
|
+
while (( j < n )) && [[ "${t[$j]}" == -* ]]; do
|
|
1171
|
+
a="${t[$j]}"; j=$((j + 1))
|
|
1172
|
+
case "$a" in --) break ;; -v|-V|-[a-zA-Z]*[vV]*) return 0 ;; esac
|
|
1173
|
+
done ;;
|
|
1174
|
+
rtk) # grammar per the header prose; an ask here is unparsed-wrapper, never a silent allow
|
|
1175
|
+
while (( j < n )) && [[ "${t[$j]}" == -* && "${t[$j]}" != - ]]; do
|
|
1176
|
+
a="${t[$j]}"
|
|
1177
|
+
case "$a" in
|
|
1178
|
+
--) j=$((j + 1)); break ;;
|
|
1179
|
+
-v|-vv|-vvv|--verbose|--skip-env|--skip-env=*) j=$((j + 1)) ;;
|
|
1180
|
+
*) note 1 unparsed-wrapper; return 0 ;;
|
|
1181
|
+
esac
|
|
1182
|
+
done
|
|
1183
|
+
if (( j < n )); then
|
|
1184
|
+
case "${t[$j]}" in
|
|
1185
|
+
run|err|test|proxy|bypass|summary)
|
|
1186
|
+
j=$((j + 1))
|
|
1187
|
+
while (( j < n )) && [[ "${t[$j]}" == -* && "${t[$j]}" != - ]]; do
|
|
1188
|
+
a="${t[$j]}"
|
|
1189
|
+
case "$a" in
|
|
1190
|
+
--) j=$((j + 1)); break ;;
|
|
1191
|
+
# globals are legal post-subcommand too (clap global=true, verified in
|
|
1192
|
+
# lib.rs): -v/-vv/-vvv/--verbose + --skip-env
|
|
1193
|
+
-v|-vv|-vvv|--verbose|--skip-env|--skip-env=*) j=$((j + 1)) ;;
|
|
1194
|
+
*) note 1 unparsed-wrapper; return 0 ;;
|
|
1195
|
+
esac
|
|
1196
|
+
done
|
|
1197
|
+
# any payload word carrying whitespace or a shell metacharacter is a string payload:
|
|
1198
|
+
# err|test|summary may join it to `sh -c` (-c/--command/--shell were already asked on
|
|
1199
|
+
# the -* sweep above, so only word content remains to check)
|
|
1200
|
+
for ((k = j; k < n; k++)); do
|
|
1201
|
+
case "${t[$k]}" in
|
|
1202
|
+
*[[:space:]]*|*\;*|*\|*|*\&*|*\(*|*\)*|*\{*|*\}*|*\$*|*\`*|*"'"*|*'"'*|*\<*|*\>*)
|
|
1203
|
+
note 1 unparsed-wrapper; return 0 ;;
|
|
1204
|
+
esac
|
|
1205
|
+
done ;;
|
|
1206
|
+
esac
|
|
1207
|
+
fi ;;
|
|
1208
|
+
nohup|-p) : ;; # `-p` is `time -p`: the lexer drops the reserved word `time` and leaves -p as the first word
|
|
1209
|
+
esac
|
|
1210
|
+
WJ="$j"
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
# fold_name <name> -> FN: the command name in lower case when it is one the table knows, else the name as it is. No process is
|
|
1214
|
+
# started (a `tr` per record cost 3 ms each and, with no `tr` on the PATH, silently allowed RM): the case statement runs under
|
|
1215
|
+
# nocasematch, which is restored.
|
|
1216
|
+
FN=""
|
|
1217
|
+
fold_name() {
|
|
1218
|
+
local had=0
|
|
1219
|
+
FN="$1"
|
|
1220
|
+
shopt -q nocasematch && had=1
|
|
1221
|
+
shopt -s nocasematch
|
|
1222
|
+
case "$1" in
|
|
1223
|
+
rm) FN="rm" ;; terraform) FN="terraform" ;; tofu) FN="tofu" ;; git) FN="git" ;;
|
|
1224
|
+
sudo) FN="sudo" ;; doas) FN="doas" ;; env) FN="env" ;; command) FN="command" ;; nohup) FN="nohup" ;;
|
|
1225
|
+
time) FN="time" ;; timeout) FN="timeout" ;; nice) FN="nice" ;; rtk) FN="rtk" ;;
|
|
1226
|
+
esac
|
|
1227
|
+
(( had )) || shopt -u nocasematch
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
# walk_in_dir <dir> <flags> <depth>: decide_walk with the simulated working directory moved to a wrapper's chdir directory, for the
|
|
1231
|
+
# wrapped command only (the directory and the unresolved-cd state are put back). A directory that cannot be resolved (a variable, an
|
|
1232
|
+
# empty word) is an unresolved cd, as for a literal `cd "$X"`.
|
|
1233
|
+
walk_in_dir() {
|
|
1234
|
+
local d="$1" dflag="$2" saved_cwd saved_unres="$UNRES" ok=0 abs=0
|
|
1235
|
+
cwd_ready; saved_cwd="$SIMCWD"
|
|
1236
|
+
if [[ -n "$d" ]] && expand_word "$d" "$dflag"; then
|
|
1237
|
+
d="$EW"; if [[ "$d" == /* ]]; then abs=1; else d="$SIMCWD/$d"; fi
|
|
1238
|
+
if resolve_phys "$d" 1 && [[ -n "$RP" ]]; then SIMCWD="$RP"; ok=1; fi
|
|
1239
|
+
fi
|
|
1240
|
+
# an ABSOLUTE directory that resolved makes the working directory known again, whatever an earlier unresolvable cd left (a relative one
|
|
1241
|
+
# is joined to the unknown directory and stays unknown); the unresolved state is put back after the wrapped command
|
|
1242
|
+
if (( ok )); then (( abs )) && UNRES=0; else UNRES=1; fi
|
|
1243
|
+
decide_walk "$3"
|
|
1244
|
+
SIMCWD="$saved_cwd"; UNRES="$saved_unres"
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
# decide_walk <depth>: the rule table over DA_T/DA_F (the words of one simple command), retried on the
|
|
1248
|
+
# command a wrapper hides and on the words after the first `--`. It hands the words it recurses on to itself
|
|
1249
|
+
# through DA_T/DA_F, so it CLOBBERS them: callers use decide_argv, which puts them back.
|
|
1250
|
+
decide_walk() {
|
|
1251
|
+
local depth="$1" n i k name wcd wcdf wset
|
|
1252
|
+
if (( depth > MAX_WRAP )); then note 1 wrapper-depth; return 0; fi
|
|
1253
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking a command"; return 0; fi
|
|
1254
|
+
local -a t=("${DA_T[@]}") f=("${DA_F[@]}")
|
|
1255
|
+
n=${#t[@]}; i=0
|
|
1256
|
+
while (( i < n )) && is_assign "${t[$i]}"; do i=$((i + 1)); done
|
|
1257
|
+
if (( i < n )); then
|
|
1258
|
+
name="${t[$i]##*/}"
|
|
1259
|
+
# command names are compared in lower case: a case-insensitive filesystem runs RM as rm
|
|
1260
|
+
case "$name" in *[A-Z]*) fold_name "$name"; name="$FN" ;; esac
|
|
1261
|
+
AV=("${t[@]:$i}"); AF=("${f[@]:$i}")
|
|
1262
|
+
case "$name" in
|
|
1263
|
+
rm) rule_rm ;;
|
|
1264
|
+
terraform|tofu) rule_tf ;;
|
|
1265
|
+
git) rule_git ;;
|
|
1266
|
+
sudo|doas|env|command|nohup|time|timeout|nice|rtk|-p) # `-p`: see wrap_skip
|
|
1267
|
+
WCD_SET=0; WCD=""
|
|
1268
|
+
wrap_skip "$name" "$i"
|
|
1269
|
+
wcd="$WCD"; wcdf="$WCDF"; wset="$WCD_SET"
|
|
1270
|
+
if (( WJ >= 0 && WJ < n )); then
|
|
1271
|
+
DA_T=("${t[@]:$WJ}"); DA_F=("${f[@]:$WJ}")
|
|
1272
|
+
if (( wset )); then
|
|
1273
|
+
# a chdir wrapper around a shell or eval: the lexer makes the inner string its OWN record, which the main loop judges with the
|
|
1274
|
+
# unmoved directory, so the loop treats the directory as unresolved from here on (WCD_RUNNER; an over-ask, never an under-ask)
|
|
1275
|
+
# (a shell can sit behind any wrapper: env -C D sudo bash -c, env -C D nice -n 5 sh -c, busybox sh -c, so every wrapped word is read)
|
|
1276
|
+
for ((k = WJ; k < n; k++)); do
|
|
1277
|
+
case "${t[$k]##*/}" in sh|bash|zsh|dash|ksh|eval|busybox) WCD_RUNNER=1; break ;; esac
|
|
1278
|
+
done
|
|
1279
|
+
walk_in_dir "$wcd" "$wcdf" $((depth + 1))
|
|
1280
|
+
else decide_walk $((depth + 1)); fi
|
|
1281
|
+
fi ;;
|
|
1282
|
+
esac
|
|
1283
|
+
fi
|
|
1284
|
+
for ((k = 0; k + 1 < n; k++)); do
|
|
1285
|
+
if [[ "${t[$k]}" == -- ]]; then
|
|
1286
|
+
DA_T=("${t[@]:$((k + 1))}"); DA_F=("${f[@]:$((k + 1))}")
|
|
1287
|
+
decide_walk $((depth + 1))
|
|
1288
|
+
break # the recursion retries every later `--` itself; looping on would repeat that work exponentially
|
|
1289
|
+
fi
|
|
1290
|
+
done
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
# decide_argv: judge the words in DA_T/DA_F and leave both exactly as found: the main loop reads them again for
|
|
1294
|
+
# the quoted text and for the cd effect. A record always has at least one word (the lexer's argc is >= 1), so the
|
|
1295
|
+
# copies below are never empty-array expansions.
|
|
1296
|
+
decide_argv() {
|
|
1297
|
+
local -a keep_t=("${DA_T[@]}") keep_f=("${DA_F[@]}")
|
|
1298
|
+
decide_walk "$1"
|
|
1299
|
+
DA_T=("${keep_t[@]}"); DA_F=("${keep_f[@]}")
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
# cd_index: CI = the index in DA_T of a cd/pushd/popd that IS the command (after assignments and a leading
|
|
1303
|
+
# `command` or `builtin`, which do not change what cd does), else -1.
|
|
1304
|
+
cd_index() {
|
|
1305
|
+
local n=${#DA_T[@]}
|
|
1306
|
+
CI=0
|
|
1307
|
+
while (( CI < n )) && is_assign "${DA_T[$CI]}"; do CI=$((CI + 1)); done
|
|
1308
|
+
while (( CI < n )); do
|
|
1309
|
+
case "${DA_T[$CI]}" in
|
|
1310
|
+
command|builtin) CI=$((CI + 1)); while [[ "${DA_T[$CI]:-}" == -p || "${DA_T[$CI]:-}" == -- ]]; do CI=$((CI + 1)); done ;;
|
|
1311
|
+
*) break ;;
|
|
1312
|
+
esac
|
|
1313
|
+
done
|
|
1314
|
+
if (( CI < n )); then
|
|
1315
|
+
case "${DA_T[$CI]}" in cd|pushd|popd) return 0 ;; esac
|
|
1316
|
+
fi
|
|
1317
|
+
CI=-1
|
|
1318
|
+
return 1
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
# apply_cd: a literal cd/pushd moves the simulated working directory; an unresolvable one sets UNRES.
|
|
1322
|
+
apply_cd() { # reads DA_T/DA_F of the current record; CI (from cd_index) is the index of its cd/pushd/popd word
|
|
1323
|
+
local -a t=("${DA_T[@]}") f=("${DA_F[@]}")
|
|
1324
|
+
local n=${#t[@]} i=$CI j target="" tf="-" have=0
|
|
1325
|
+
cwd_ready
|
|
1326
|
+
if [[ "${t[$i]}" == popd ]]; then UNRES=1; return 0; fi
|
|
1327
|
+
j=$((i + 1))
|
|
1328
|
+
while (( j < n )); do
|
|
1329
|
+
if [[ "${t[$j]}" == -- ]]; then j=$((j + 1)); break; fi
|
|
1330
|
+
[[ "${t[$j]}" == -* && "${t[$j]}" != - ]] || break
|
|
1331
|
+
j=$((j + 1))
|
|
1332
|
+
done
|
|
1333
|
+
if (( j < n )); then target="${t[$j]}"; tf="${f[$j]}"; have=1; fi
|
|
1334
|
+
if (( ! have )); then
|
|
1335
|
+
if [[ -n "${HOME:-}" ]]; then target="$HOME"; else UNRES=1; return 0; fi
|
|
1336
|
+
tf="-"
|
|
1337
|
+
fi
|
|
1338
|
+
if [[ "$target" == - ]]; then UNRES=1; return 0; fi
|
|
1339
|
+
if ! expand_word "$target" "$tf"; then UNRES=1; return 0; fi
|
|
1340
|
+
target="$EW"
|
|
1341
|
+
[[ "$target" == /* ]] || target="$SIMCWD/$target"
|
|
1342
|
+
if resolve_phys "$target" 1 && [[ -n "$RP" ]]; then SIMCWD="$RP"; UNRES=0; else UNRES=1; fi
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
NREC=${#REC_N[@]}
|
|
1346
|
+
for ((r = 0; r < NREC; r++)); do
|
|
1347
|
+
if (( SECONDS >= DEADLINE_S )); then BOUND_WHY="the ${LIMIT_S} s time limit was reached while checking the commands"; break; fi
|
|
1348
|
+
# one record's words by index: an array slice is O(offset) in bash and made the loop quadratic
|
|
1349
|
+
RO="${REC_OFF[$r]}"; RC="${REC_N[$r]}"; DA_T=(); DA_F=()
|
|
1350
|
+
for ((RQ = 0; RQ < RC; RQ++)); do DA_T[RQ]="${W_TXT[RO + RQ]}"; DA_F[RQ]="${W_FLG[RO + RQ]}"; done
|
|
1351
|
+
REC_RANK=0; REC_RULE=""
|
|
1352
|
+
decide_argv 0
|
|
1353
|
+
if (( WCD_RUNNER )); then UNRES=1; WCD_RUNNER=0; fi
|
|
1354
|
+
if (( REC_RANK > BEST_RANK )); then
|
|
1355
|
+
BEST_RANK="$REC_RANK"; BEST_RULE="$REC_RULE"
|
|
1356
|
+
REC_TXT=""; RW_NEXT=0
|
|
1357
|
+
for ((RQ = 0; RQ < ${#DA_T[@]} && ${#REC_TXT} <= QUOTE_MAX; RQ++)); do redact_word "${DA_T[RQ]}"; REC_TXT="${REC_TXT:+$REC_TXT }$RW"; done
|
|
1358
|
+
BEST_QUOTE="${REC_TXT:0:QUOTE_MAX}"
|
|
1359
|
+
(( ${#REC_TXT} > QUOTE_MAX )) && BEST_QUOTE="${BEST_QUOTE}..."
|
|
1360
|
+
(( BEST_RANK == 2 )) && break
|
|
1361
|
+
fi
|
|
1362
|
+
[[ -n "$BOUND_WHY" ]] && break
|
|
1363
|
+
# the cd effect of this record, for the commands after it
|
|
1364
|
+
if cd_index; then apply_cd; fi
|
|
1365
|
+
done
|
|
1366
|
+
|
|
1367
|
+
# ---- 6. the decision -------------------------------------------------------------------------------
|
|
1368
|
+
# a bound trip (record or word cap, or the deadline) is an ask unless a deny was already found
|
|
1369
|
+
[[ -z "$BOUND_WHY" ]] && BOUND_WHY="$BOUND_SOFT"
|
|
1370
|
+
# A bound with nothing matched is a bare `bound` ask. A bound AFTER an ask-class match keeps that rule's reason (the command WAS
|
|
1371
|
+
# recognised) with the bound sentence appended: the person sees what matched and that the rest was not checked.
|
|
1372
|
+
BOUND_NOTE=""
|
|
1373
|
+
if [[ -n "$BOUND_WHY" && "$BEST_RANK" -lt 2 ]]; then
|
|
1374
|
+
(( BEST_RANK == 0 )) && ask_bound "$BOUND_WHY"
|
|
1375
|
+
BOUND_NOTE=" The guard also stopped checking the rest of this command because it is too large to check in full (${BOUND_WHY}), so other parts of it were not checked."
|
|
1376
|
+
fi
|
|
1377
|
+
(( BEST_RANK == 0 )) && exit 0
|
|
1378
|
+
lead_for "$BEST_RULE"
|
|
1379
|
+
# env -S and too many wrappers hide the command (write it out) and an unresolvable cd hides the directory (use a literal one); every other match is a stop
|
|
1380
|
+
TAIL="$REASON_TAIL"
|
|
1381
|
+
case "$BEST_RULE" in
|
|
1382
|
+
unparsed-wrapper|wrapper-depth) TAIL="$WRAPPER_TAIL" ;;
|
|
1383
|
+
unresolved-cd-before-destructive) TAIL="$CD_TAIL" ;;
|
|
1384
|
+
esac
|
|
1385
|
+
REASON="${BEST_RULE}: ${LEAD} Matched command: [${BEST_QUOTE}].${BOUND_NOTE}${TAIL}"
|
|
1386
|
+
if (( BEST_RANK == 2 )); then emit deny "$REASON"; else emit ask "$REASON"; fi
|
|
1387
|
+
exit 0
|