@soleur/opencode 0.0.0-stage → 3.354.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/agents.manifest.json +476 -0
- package/.claude-plugin/plugin.json +42 -0
- package/README.md +44 -2
- package/agents/community/.gitkeep +0 -0
- package/agents/engineering/cto.md +87 -0
- package/agents/engineering/design/ddd-architect.md +95 -0
- package/agents/engineering/discovery/agent-finder.md +214 -0
- package/agents/engineering/discovery/functional-discovery.md +182 -0
- package/agents/engineering/infra/infra-security.md +151 -0
- package/agents/engineering/infra/platform-strategist.md +109 -0
- package/agents/engineering/infra/terraform-architect.md +171 -0
- package/agents/engineering/prompt-engineer.md +37 -0
- package/agents/engineering/research/best-practices-researcher.md +115 -0
- package/agents/engineering/research/framework-docs-researcher.md +93 -0
- package/agents/engineering/research/git-history-analyzer.md +53 -0
- package/agents/engineering/research/learnings-researcher.md +285 -0
- package/agents/engineering/research/repo-research-analyst.md +118 -0
- package/agents/engineering/review/agent-native-reviewer.md +279 -0
- package/agents/engineering/review/architecture-strategist.md +61 -0
- package/agents/engineering/review/code-quality-analyst.md +92 -0
- package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
- package/agents/engineering/review/data-integrity-guardian.md +82 -0
- package/agents/engineering/review/data-migration-expert.md +98 -0
- package/agents/engineering/review/deployment-verification-agent.md +183 -0
- package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
- package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
- package/agents/engineering/review/legacy-code-expert.md +55 -0
- package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
- package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
- package/agents/engineering/review/performance-oracle.md +130 -0
- package/agents/engineering/review/security-sentinel.md +167 -0
- package/agents/engineering/review/semgrep-sast.md +36 -0
- package/agents/engineering/review/test-design-reviewer.md +150 -0
- package/agents/engineering/review/user-impact-reviewer.md +70 -0
- package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
- package/agents/finance/budget-analyst.md +29 -0
- package/agents/finance/cfo.md +44 -0
- package/agents/finance/financial-reporter.md +30 -0
- package/agents/finance/revenue-analyst.md +30 -0
- package/agents/legal/clo.md +115 -0
- package/agents/legal/legal-compliance-auditor.md +110 -0
- package/agents/legal/legal-document-generator.md +134 -0
- package/agents/marketing/analytics-analyst.md +25 -0
- package/agents/marketing/brand-architect.md +181 -0
- package/agents/marketing/cmo.md +76 -0
- package/agents/marketing/conversion-optimizer.md +19 -0
- package/agents/marketing/copywriter.md +20 -0
- package/agents/marketing/fact-checker.md +70 -0
- package/agents/marketing/growth-strategist.md +145 -0
- package/agents/marketing/paid-media-strategist.md +28 -0
- package/agents/marketing/pricing-strategist.md +17 -0
- package/agents/marketing/programmatic-seo-specialist.md +25 -0
- package/agents/marketing/retention-strategist.md +23 -0
- package/agents/marketing/seo-aeo-analyst.md +164 -0
- package/agents/operations/coo.md +50 -0
- package/agents/operations/ops-advisor.md +78 -0
- package/agents/operations/ops-provisioner.md +101 -0
- package/agents/operations/ops-research.md +52 -0
- package/agents/operations/service-automator.md +278 -0
- package/agents/product/business-validator.md +206 -0
- package/agents/product/competitive-intelligence.md +89 -0
- package/agents/product/cpo.md +63 -0
- package/agents/product/design/ux-design-lead.md +209 -0
- package/agents/product/spec-flow-analyzer.md +122 -0
- package/agents/sales/cro.md +44 -0
- package/agents/sales/deal-architect.md +29 -0
- package/agents/sales/outbound-strategist.md +29 -0
- package/agents/sales/pipeline-analyst.md +29 -0
- package/agents/support/cco.md +48 -0
- package/agents/support/community-manager.md +570 -0
- package/agents/support/ticket-triage.md +70 -0
- package/bin/soleur-opencode.js +29 -0
- package/commands/go.md +660 -0
- package/commands/help.md +273 -0
- package/commands/sync.md +977 -0
- package/commands/version.md +103 -0
- package/hooks/antigravity-hooks.json +84 -0
- package/hooks/antigravity-session-start.sh +15 -0
- package/hooks/bare-root-cwd-guard.sh +131 -0
- package/hooks/browser-snapshot-credential-guard.sh +200 -0
- package/hooks/codex-session-start.sh +9 -0
- package/hooks/compaction-state.sh +525 -0
- package/hooks/destructive-command-guard.sh +1387 -0
- package/hooks/devin-session-start.sh +90 -0
- package/hooks/ext-filter-detect.sh +171 -0
- package/hooks/hooks.json +133 -0
- package/hooks/lib/hook-tool-kind.sh +24 -0
- package/hooks/lib/log-rotation.sh +176 -0
- package/hooks/lib/shell-argv.pl +576 -0
- package/hooks/memory-backstop.sh +1114 -0
- package/hooks/operator-stage-approval.sh +391 -0
- package/hooks/stop-hook.sh +367 -0
- package/hooks/unkept-promise-hook.sh +383 -0
- package/hooks/version-banner.sh +127 -0
- package/hooks/welcome-hook.sh +50 -0
- package/install.js +29 -0
- package/lib/agent-registry.ts +154 -0
- package/lib/c4-canonical-cli.mjs +34 -0
- package/lib/c4-canonical.mjs +46 -0
- package/lib/c4-from-components.ts +554 -0
- package/lib/go-routing.ts +81 -0
- package/lib/grok-inspect-contract.ts +311 -0
- package/lib/harness-model-map.ts +101 -0
- package/lib/harness-parity.ts +726 -0
- package/lib/harness-tool-map.ts +180 -0
- package/lib/harness.ts +716 -0
- package/lib/heartbeat-live-reconcile.ts +1153 -0
- package/lib/heartbeat-manifest.ts +361 -0
- package/lib/kb-coverage.ts +252 -0
- package/lib/pr-merge-poll.ts +103 -0
- package/lib/workflow-fidelity.ts +388 -0
- package/opencode/INSTRUCTIONS.md +171 -0
- package/opencode/agents/engineering/cto.md +10 -0
- package/opencode/agents/engineering/design/ddd-architect.md +10 -0
- package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
- package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
- package/opencode/agents/engineering/infra/infra-security.md +10 -0
- package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
- package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
- package/opencode/agents/engineering/prompt-engineer.md +10 -0
- package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
- package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
- package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
- package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
- package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
- package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
- package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
- package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
- package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
- package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
- package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
- package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
- package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
- package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
- package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
- package/opencode/agents/engineering/review/performance-oracle.md +10 -0
- package/opencode/agents/engineering/review/security-sentinel.md +10 -0
- package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
- package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
- package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
- package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
- package/opencode/agents/finance/budget-analyst.md +10 -0
- package/opencode/agents/finance/cfo.md +10 -0
- package/opencode/agents/finance/financial-reporter.md +10 -0
- package/opencode/agents/finance/revenue-analyst.md +10 -0
- package/opencode/agents/legal/clo.md +10 -0
- package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
- package/opencode/agents/legal/legal-document-generator.md +10 -0
- package/opencode/agents/marketing/analytics-analyst.md +10 -0
- package/opencode/agents/marketing/brand-architect.md +10 -0
- package/opencode/agents/marketing/cmo.md +10 -0
- package/opencode/agents/marketing/conversion-optimizer.md +10 -0
- package/opencode/agents/marketing/copywriter.md +10 -0
- package/opencode/agents/marketing/fact-checker.md +10 -0
- package/opencode/agents/marketing/growth-strategist.md +10 -0
- package/opencode/agents/marketing/paid-media-strategist.md +10 -0
- package/opencode/agents/marketing/pricing-strategist.md +10 -0
- package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
- package/opencode/agents/marketing/retention-strategist.md +10 -0
- package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
- package/opencode/agents/operations/coo.md +10 -0
- package/opencode/agents/operations/ops-advisor.md +10 -0
- package/opencode/agents/operations/ops-provisioner.md +10 -0
- package/opencode/agents/operations/ops-research.md +10 -0
- package/opencode/agents/operations/service-automator.md +10 -0
- package/opencode/agents/product/business-validator.md +10 -0
- package/opencode/agents/product/competitive-intelligence.md +10 -0
- package/opencode/agents/product/cpo.md +10 -0
- package/opencode/agents/product/design/ux-design-lead.md +10 -0
- package/opencode/agents/product/spec-flow-analyzer.md +10 -0
- package/opencode/agents/sales/cro.md +10 -0
- package/opencode/agents/sales/deal-architect.md +10 -0
- package/opencode/agents/sales/outbound-strategist.md +10 -0
- package/opencode/agents/sales/pipeline-analyst.md +10 -0
- package/opencode/agents/support/cco.md +10 -0
- package/opencode/agents/support/community-manager.md +10 -0
- package/opencode/agents/support/ticket-triage.md +10 -0
- package/opencode/commands/go.md +10 -0
- package/opencode/commands/help.md +10 -0
- package/opencode/commands/sync.md +10 -0
- package/opencode/commands/version.md +11 -0
- package/opencode/opencode.json +8 -0
- package/opencode/plugin.ts +818 -0
- package/package.json +36 -4
- package/scripts/admin-merge-ready.sh +545 -0
- package/scripts/admin-merge-ready.test.sh +753 -0
- package/scripts/affected-tests.sh +908 -0
- package/scripts/affected-tests.test.sh +1797 -0
- package/scripts/alpha-metrics.sh +78 -0
- package/scripts/alpha-metrics.test.sh +115 -0
- package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
- package/scripts/audit-flag-flip.sh +107 -0
- package/scripts/check-red-on-main.sh +316 -0
- package/scripts/ci-head-verdict.sh +411 -0
- package/scripts/cloud-detect.sh +193 -0
- package/scripts/delegation-verify.sh +657 -0
- package/scripts/delegation-verify.test.sh +1424 -0
- package/scripts/deploy-arm.sh +475 -0
- package/scripts/domain-model-drift.sh +419 -0
- package/scripts/emit-decision.sh +144 -0
- package/scripts/emit-decision.test.sh +144 -0
- package/scripts/ext-filter-divergence-corpus.txt +15 -0
- package/scripts/ext-filter-divergence.sh +189 -0
- package/scripts/generate-c4-from-components.ts +442 -0
- package/scripts/grok-fidelity-gate.sh +68 -0
- package/scripts/grok-pre-push-gate.sh +232 -0
- package/scripts/harness-discovery-smoke.ts +906 -0
- package/scripts/harness-parity-census.ts +92 -0
- package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
- package/scripts/install-opencode-adapter.ts +509 -0
- package/scripts/lib/affected-tests/common.sh +365 -0
- package/scripts/lib/affected-tests/go.sh +157 -0
- package/scripts/lib/affected-tests/js.sh +188 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
- package/scripts/lib/affected-tests/python.sh +185 -0
- package/scripts/lib/context-sanitize.sh +49 -0
- package/scripts/lib/domain-model-lib.sh +180 -0
- package/scripts/lib/ext-filter-grammar.tsv +34 -0
- package/scripts/lib/operator-script.sh +1236 -0
- package/scripts/lib/proc.sh +580 -0
- package/scripts/lib/session-state.sh +748 -0
- package/scripts/lib/tmp-classify.sh +843 -0
- package/scripts/lib/wrapper-boundary.sh +47 -0
- package/scripts/lint-ratchet.py +1547 -0
- package/scripts/monitor-pr-checks.sh +441 -0
- package/scripts/pipeline-tally.sh +756 -0
- package/scripts/pipeline-tally.test.sh +740 -0
- package/scripts/plugin-version.sh +528 -0
- package/scripts/precommit-guard.sh +115 -0
- package/scripts/precommit-guard.test.sh +254 -0
- package/scripts/reconcile-live-heartbeats.ts +705 -0
- package/scripts/render-c4-model.sh +217 -0
- package/scripts/resolve-git-root.sh +55 -0
- package/scripts/resolve-regenerable-conflicts.sh +363 -0
- package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
- package/scripts/ruff-requirements.txt +22 -0
- package/scripts/run-in-pid-namespace.sh +98 -0
- package/scripts/run-in-pid-namespace.test.sh +290 -0
- package/scripts/scan-ancestor-signal-helpers.py +556 -0
- package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
- package/scripts/setup-ralph-loop.sh +238 -0
- package/scripts/sync-cursor-name-map.ts +316 -0
- package/scripts/sync-grok-agent-compat.ts +188 -0
- package/scripts/sync-npm-package.ts +165 -0
- package/scripts/sync-opencode-adapter.ts +498 -0
- package/scripts/sync-pr-behind.sh +647 -0
- package/scripts/sync-pr-behind.test.sh +252 -0
- package/scripts/taste-profile-update.sh +180 -0
- package/scripts/taste-profile-update.test.sh +167 -0
- package/scripts/write-budget-marker.sh +23 -0
- package/scripts/write-kb-coverage.ts +113 -0
- package/skills/admin-ip-refresh/SKILL.md +81 -0
- package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
- package/skills/agent-browser/SKILL.md +698 -0
- package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
- package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
- package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
- package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
- package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
- package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
- package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
- package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
- package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
- package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
- package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
- package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
- package/skills/agent-native-architecture/SKILL.md +479 -0
- package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
- package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
- package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
- package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
- package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
- package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
- package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
- package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
- package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
- package/skills/agent-native-architecture/references/product-implications.md +478 -0
- package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
- package/skills/agent-native-architecture/references/self-modification.md +294 -0
- package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
- package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
- package/skills/agent-native-audit/SKILL.md +300 -0
- package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
- package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
- package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
- package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/skills/architecture/SKILL.md +436 -0
- package/skills/architecture/references/adr-template.md +154 -0
- package/skills/architecture/references/likec4-reference.md +136 -0
- package/skills/architecture/references/nfr-reference.md +126 -0
- package/skills/archive-kb/SKILL.md +86 -0
- package/skills/archive-kb/scripts/archive-kb.sh +300 -0
- package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
- package/skills/atdd-developer/SKILL.md +65 -0
- package/skills/brainstorm/SKILL.md +701 -0
- package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
- package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
- package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
- package/skills/brainstorm/references/ui-surface-terms.md +42 -0
- package/skills/brainstorm-techniques/SKILL.md +231 -0
- package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
- package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
- package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
- package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
- package/skills/campaign-calendar/SKILL.md +145 -0
- package/skills/cf-token-scope/SKILL.md +164 -0
- package/skills/cf-token-scope/references/widen-playbook.md +114 -0
- package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
- package/skills/changelog/SKILL.md +142 -0
- package/skills/code-to-prd/SKILL.md +66 -0
- package/skills/code-to-prd/references/banner-template.md +12 -0
- package/skills/code-to-prd/references/prd-template.md +41 -0
- package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
- package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
- package/skills/code-to-prd/test/fixture/.env.example +7 -0
- package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
- package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
- package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
- package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
- package/skills/code-to-prd/test/fixture/package.json +6 -0
- package/skills/cohort-status/SKILL.md +62 -0
- package/skills/community/SKILL.md +210 -0
- package/skills/community/scripts/bsky-community.sh +644 -0
- package/skills/community/scripts/bsky-setup.sh +631 -0
- package/skills/community/scripts/community-router.sh +93 -0
- package/skills/community/scripts/discord-community.sh +487 -0
- package/skills/community/scripts/discord-setup.sh +784 -0
- package/skills/community/scripts/github-community.sh +706 -0
- package/skills/community/scripts/hn-community.sh +233 -0
- package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
- package/skills/community/scripts/linkedin-community.sh +750 -0
- package/skills/community/scripts/linkedin-setup.sh +758 -0
- package/skills/community/scripts/x-community.sh +890 -0
- package/skills/community/scripts/x-setup.sh +713 -0
- package/skills/community/test/community-argv.test.sh +2922 -0
- package/skills/community/test/github-community.test.sh +981 -0
- package/skills/competitive-analysis/SKILL.md +69 -0
- package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
- package/skills/compound/SKILL.md +673 -0
- package/skills/compound/scripts/token-efficiency-report.sh +334 -0
- package/skills/compound/test/phase-16.test.sh +379 -0
- package/skills/compound-capture/SKILL.md +791 -0
- package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
- package/skills/compound-capture/assets/resolution-template.md +116 -0
- package/skills/compound-capture/references/yaml-schema.md +67 -0
- package/skills/compound-capture/schema.yaml +181 -0
- package/skills/constraint-scaffold/SKILL.md +175 -0
- package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
- package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
- package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
- package/skills/constraint-scaffold/references/shared-runner.template +135 -0
- package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
- package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
- package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
- package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
- package/skills/constraint-scaffold/test/generator.test.sh +93 -0
- package/skills/constraint-scaffold/test/parity.test.sh +285 -0
- package/skills/content-writer/SKILL.md +242 -0
- package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
- package/skills/cron-delete/SKILL.md +56 -0
- package/skills/cron-list/SKILL.md +54 -0
- package/skills/deepen-plan/SKILL.md +937 -0
- package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
- package/skills/deploy/SKILL.md +115 -0
- package/skills/deploy/references/hetzner-setup.md +67 -0
- package/skills/deploy/scripts/deploy.sh +37 -0
- package/skills/deploy-docs/SKILL.md +95 -0
- package/skills/dhh-rails-style/SKILL.md +203 -0
- package/skills/dhh-rails-style/references/architecture.md +717 -0
- package/skills/dhh-rails-style/references/controllers.md +323 -0
- package/skills/dhh-rails-style/references/frontend.md +556 -0
- package/skills/dhh-rails-style/references/gems.md +316 -0
- package/skills/dhh-rails-style/references/models.md +388 -0
- package/skills/dhh-rails-style/references/testing.md +354 -0
- package/skills/discord-content/SKILL.md +126 -0
- package/skills/docs-site/SKILL.md +214 -0
- package/skills/drain-labeled-backlog/SKILL.md +203 -0
- package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
- package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
- package/skills/drain-prs/SKILL.md +143 -0
- package/skills/drain-prs/scripts/triage-prs.sh +192 -0
- package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
- package/skills/dspy-ruby/SKILL.md +634 -0
- package/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/skills/dspy-ruby/references/core-concepts.md +268 -0
- package/skills/dspy-ruby/references/optimization.md +623 -0
- package/skills/dspy-ruby/references/providers.md +335 -0
- package/skills/eval-harness/README.md +194 -0
- package/skills/eval-harness/SKILL.md +157 -0
- package/skills/eval-harness/enums/go-routes.json +13 -0
- package/skills/eval-harness/enums/incident-threshold.json +5 -0
- package/skills/eval-harness/enums/lane.json +5 -0
- package/skills/eval-harness/enums/tool-selection.json +20 -0
- package/skills/eval-harness/enums/triage-levels.json +5 -0
- package/skills/eval-harness/gated-skills.json +34 -0
- package/skills/eval-harness/models.generated.json +5 -0
- package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
- package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
- package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
- package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
- package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
- package/skills/eval-harness/prompts/go-baseline.txt +6 -0
- package/skills/eval-harness/prompts/go-skill.txt +21 -0
- package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
- package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
- package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
- package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
- package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
- package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
- package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
- package/skills/eval-harness/prompts/triage-skill.txt +10 -0
- package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
- package/skills/eval-harness/scripts/extract-block.cjs +87 -0
- package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
- package/skills/eval-harness/scripts/gen-models.sh +41 -0
- package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
- package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
- package/skills/eval-harness/scripts/parse-label.cjs +93 -0
- package/skills/eval-harness/scripts/verdict.cjs +132 -0
- package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
- package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
- package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
- package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
- package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
- package/skills/eval-harness/test/eval-gate.test.sh +132 -0
- package/skills/eval-harness/test/extract-block.test.sh +87 -0
- package/skills/eval-harness/test/gate-classification.test.sh +77 -0
- package/skills/eval-harness/test/gen-models.test.sh +75 -0
- package/skills/eval-harness/test/measure-classification.test.sh +74 -0
- package/skills/eval-harness/test/parse-label.test.sh +129 -0
- package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
- package/skills/eval-harness/test/verdict.test.sh +123 -0
- package/skills/every-style-editor/SKILL.md +142 -0
- package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/skills/feature-tweet/SKILL.md +199 -0
- package/skills/feature-video/SKILL.md +412 -0
- package/skills/feature-video/scripts/check_deps.sh +281 -0
- package/skills/file-todos/SKILL.md +267 -0
- package/skills/file-todos/assets/todo-template.md +166 -0
- package/skills/fix-issue/SKILL.md +278 -0
- package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
- package/skills/flag-bootstrap/SETUP.md +142 -0
- package/skills/flag-create/SKILL.md +145 -0
- package/skills/flag-create/scripts/create.sh +273 -0
- package/skills/flag-delete/SKILL.md +167 -0
- package/skills/flag-delete/scripts/delete.sh +288 -0
- package/skills/flag-list/SKILL.md +99 -0
- package/skills/flag-list/scripts/list.sh +275 -0
- package/skills/flag-set-role/SKILL.md +229 -0
- package/skills/flag-set-role/scripts/flip.sh +774 -0
- package/skills/frontend-anti-slop/SKILL.md +106 -0
- package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
- package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
- package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
- package/skills/frontend-design/SKILL.md +77 -0
- package/skills/gdpr-gate/NOTICE +136 -0
- package/skills/gdpr-gate/SKILL.md +341 -0
- package/skills/gdpr-gate/references/fields.md +160 -0
- package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
- package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
- package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
- package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
- package/skills/gdpr-gate/references/layers/frontend.md +249 -0
- package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
- package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
- package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
- package/skills/gdpr-gate/references/legal-consent.md +140 -0
- package/skills/gdpr-gate/references/non-negotiables.md +60 -0
- package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
- package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
- package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
- package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
- package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
- package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
- package/skills/gemini-imagegen/SKILL.md +283 -0
- package/skills/gemini-imagegen/requirements.txt +5 -0
- package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
- package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
- package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
- package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
- package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
- package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
- package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
- package/skills/git-worktree/SKILL.md +534 -0
- package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
- package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
- package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
- package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
- package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
- package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
- package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
- package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
- package/skills/go/SKILL.md +54 -0
- package/skills/growth/SKILL.md +164 -0
- package/skills/harvest-debt/SKILL.md +78 -0
- package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
- package/skills/heal-skill/SKILL.md +173 -0
- package/skills/help/SKILL.md +46 -0
- package/skills/incident/NOTICE +39 -0
- package/skills/incident/SKILL.md +395 -0
- package/skills/incident/scripts/dry-run.sh +448 -0
- package/skills/incident/scripts/redact-engine.py +412 -0
- package/skills/incident/scripts/redact-sentinel.sh +44 -0
- package/skills/incident/templates/pir.md +139 -0
- package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
- package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
- package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
- package/skills/incident/test/fixtures/positive-corpus.md +87 -0
- package/skills/incident/test/redact-sentinel.test.sh +1420 -0
- package/skills/invoice/SKILL.md +206 -0
- package/skills/kb-glossary/SKILL.md +71 -0
- package/skills/kb-glossary/references/glossary-format.md +111 -0
- package/skills/kb-glossary/references/rejected-request-register.md +115 -0
- package/skills/kb-search/SKILL.md +276 -0
- package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
- package/skills/legal-audit/SKILL.md +161 -0
- package/skills/legal-generate/NOTICE +242 -0
- package/skills/legal-generate/SKILL.md +244 -0
- package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
- package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
- package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
- package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
- package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
- package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
- package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
- package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
- package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
- package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
- package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
- package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
- package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
- package/skills/linear-fetch/SKILL.md +247 -0
- package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
- package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
- package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
- package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
- package/skills/linear-fetch/test/parity.test.sh +112 -0
- package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
- package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
- package/skills/merge-pr/SKILL.md +670 -0
- package/skills/model-launch-review/SKILL.md +255 -0
- package/skills/model-launch-review/scripts/audit-models.sh +501 -0
- package/skills/one-shot/SKILL.md +316 -0
- package/skills/one-shot/references/token-discipline.md +74 -0
- package/skills/operator-bootstrap/SKILL.md +412 -0
- package/skills/operator-bootstrap/template.sh +439 -0
- package/skills/operator-digest/SKILL.md +287 -0
- package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
- package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
- package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
- package/skills/operator-rephrase/SKILL.md +98 -0
- package/skills/pencil-setup/SKILL.md +201 -0
- package/skills/pencil-setup/scripts/check_deps.sh +556 -0
- package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
- package/skills/pencil-setup/scripts/package-lock.json +1172 -0
- package/skills/pencil-setup/scripts/package.json +14 -0
- package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
- package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
- package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
- package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
- package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
- package/skills/plan/SKILL.md +1140 -0
- package/skills/plan/references/differential-migration-testing.md +185 -0
- package/skills/plan/references/plan-community-discovery.md +42 -0
- package/skills/plan/references/plan-founder-check.md +137 -0
- package/skills/plan/references/plan-functional-overlap.md +25 -0
- package/skills/plan/references/plan-host-fanout.md +51 -0
- package/skills/plan/references/plan-issue-templates.md +779 -0
- package/skills/plan/references/plan-network-outage-checklist.md +131 -0
- package/skills/plan/references/plan-scope-check.md +126 -0
- package/skills/plan/references/plan-sharp-edges.md +331 -0
- package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
- package/skills/plan/references/webapp-cwv-observability.md +143 -0
- package/skills/plan-review/SKILL.md +91 -0
- package/skills/plan-review/lib/named-panel.mjs +58 -0
- package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
- package/skills/postmerge/SKILL.md +642 -0
- package/skills/postmerge/references/deploy-status-debugging.md +141 -0
- package/skills/postmerge/references/graceful-degradation.md +20 -0
- package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
- package/skills/preflight/SKILL.md +1553 -0
- package/skills/preflight/references/check-13-founder-check.md +286 -0
- package/skills/preflight/scripts/founder-check.py +1640 -0
- package/skills/preflight/scripts/parse-form-a.awk +107 -0
- package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
- package/skills/product-roadmap/SKILL.md +357 -0
- package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
- package/skills/provision-cloudflare/SKILL.md +67 -0
- package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
- package/skills/provision-doppler/SKILL.md +58 -0
- package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
- package/skills/provision-github/SKILL.md +59 -0
- package/skills/provision-github/scripts/provision-github.sh +300 -0
- package/skills/provision-hetzner/SKILL.md +76 -0
- package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
- package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
- package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
- package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
- package/skills/qa/SKILL.md +263 -0
- package/skills/qa/references/browser-preflight.md +22 -0
- package/skills/questionnaire-generate/SKILL.md +328 -0
- package/skills/questionnaire-generate/references/questionnaire.template +60 -0
- package/skills/rclone/SKILL.md +162 -0
- package/skills/rclone/scripts/check_setup.sh +63 -0
- package/skills/release-announce/SKILL.md +65 -0
- package/skills/release-docs/SKILL.md +92 -0
- package/skills/reproduce-bug/SKILL.md +358 -0
- package/skills/resolve-debt/SKILL.md +85 -0
- package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
- package/skills/resolve-parallel/SKILL.md +45 -0
- package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
- package/skills/resolve-pr-parallel/SKILL.md +68 -0
- package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
- package/skills/resolve-todo-parallel/SKILL.md +57 -0
- package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
- package/skills/review/SKILL.md +1620 -0
- package/skills/review/references/anti-slop-scanner-hook.md +40 -0
- package/skills/review/references/defect-classes-migrations.md +9 -0
- package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
- package/skills/review/references/review-e2e-testing.md +116 -0
- package/skills/review/references/review-evidence-comment.md +36 -0
- package/skills/review/references/review-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
- package/skills/review/references/semgrep-custom-rules.yaml +174 -0
- package/skills/review/references/ultrathink-phases.md +98 -0
- package/skills/review/references/wfs.md +6 -0
- package/skills/review/scripts/emit-review-trailer.sh +499 -0
- package/skills/review/scripts/ensure-semgrep.sh +99 -0
- package/skills/review/scripts/fix-round-seats.sh +197 -0
- package/skills/review/test/emit-review-trailer.test.sh +387 -0
- package/skills/review/test/ensure-semgrep.test.sh +111 -0
- package/skills/review/test/fix-round-seats.test.sh +237 -0
- package/skills/review/workflows/README.md +147 -0
- package/skills/review/workflows/review.workflow.js +667 -0
- package/skills/schedule/SKILL.md +796 -0
- package/skills/seo-aeo/SKILL.md +139 -0
- package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
- package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
- package/skills/ship/SKILL.md +3023 -0
- package/skills/ship/references/adr-ordinal-collision.md +47 -0
- package/skills/ship/references/affected-gate-contract.md +115 -0
- package/skills/ship/references/ci-run-diagnosis.md +29 -0
- package/skills/ship/references/ci-workflow-authoring.md +68 -0
- package/skills/ship/references/cleanup-merged-archival.md +39 -0
- package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
- package/skills/ship/references/cto-ruling-armed-push.md +5 -0
- package/skills/ship/references/decision-challenges-render.md +71 -0
- package/skills/ship/references/delegated-act-authoring-form.md +7 -0
- package/skills/ship/references/detached-battery-verdict.md +13 -0
- package/skills/ship/references/epic-close-sweep.md +62 -0
- package/skills/ship/references/feature-tweet-draft.md +42 -0
- package/skills/ship/references/followthrough-issue-body-template.md +47 -0
- package/skills/ship/references/followthrough-stub-template.sh +92 -0
- package/skills/ship/references/merge-queue-dequeue.md +39 -0
- package/skills/ship/references/operator-holds.md +179 -0
- package/skills/ship/references/pipeline-tally-render.md +43 -0
- package/skills/ship/references/pr-overlap.md +153 -0
- package/skills/ship/references/queue-mode.md +7 -0
- package/skills/ship/references/ready-run-wait.md +36 -0
- package/skills/ship/references/red-on-main-quarantine.md +110 -0
- package/skills/ship/references/register-update-pr-pattern.md +88 -0
- package/skills/ship/references/settle-then-admin-merge.md +130 -0
- package/skills/ship/references/supabase-column-probe.md +13 -0
- package/skills/ship/scripts/auto-close-scan.sh +76 -0
- package/skills/ship/scripts/battery-owed.sh +403 -0
- package/skills/ship/scripts/hold-gate-eval.sh +410 -0
- package/skills/ship/scripts/net-issue-flow.sh +665 -0
- package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
- package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
- package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
- package/skills/skill-creator/SKILL.md +299 -0
- package/skills/skill-creator/references/api-security.md +248 -0
- package/skills/skill-creator/references/authoring-levers.md +110 -0
- package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
- package/skills/skill-creator/references/best-practices.md +431 -0
- package/skills/skill-creator/references/common-patterns.md +639 -0
- package/skills/skill-creator/references/core-principles.md +193 -0
- package/skills/skill-creator/references/executable-code.md +188 -0
- package/skills/skill-creator/references/iteration-and-testing.md +509 -0
- package/skills/skill-creator/references/official-spec.md +193 -0
- package/skills/skill-creator/references/recommended-structure.md +180 -0
- package/skills/skill-creator/references/skill-structure.md +396 -0
- package/skills/skill-creator/references/using-scripts.md +122 -0
- package/skills/skill-creator/references/using-templates.md +120 -0
- package/skills/skill-creator/references/workflows-and-validation.md +541 -0
- package/skills/skill-creator/scripts/init_skill.py +322 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +64 -0
- package/skills/skill-creator/templates/router-skill.md +73 -0
- package/skills/skill-creator/templates/simple-skill.md +34 -0
- package/skills/skill-creator/workflows/add-reference.md +96 -0
- package/skills/skill-creator/workflows/add-script.md +104 -0
- package/skills/skill-creator/workflows/add-template.md +83 -0
- package/skills/skill-creator/workflows/add-workflow.md +128 -0
- package/skills/skill-creator/workflows/audit-skill.md +159 -0
- package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
- package/skills/skill-creator/workflows/create-new-skill.md +207 -0
- package/skills/skill-creator/workflows/get-guidance.md +134 -0
- package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
- package/skills/skill-creator/workflows/verify-skill.md +226 -0
- package/skills/skill-security-scan/SKILL.md +145 -0
- package/skills/skill-security-scan/references/disclaimer.md +21 -0
- package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
- package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
- package/skills/skill-security-scan/references/override-mechanism.md +171 -0
- package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
- package/skills/skill-security-scan/references/regex-patterns.md +136 -0
- package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
- package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
- package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
- package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
- package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
- package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
- package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
- package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
- package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
- package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
- package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
- package/skills/skill-security-scan/scripts/lib.sh +197 -0
- package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
- package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
- package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
- package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
- package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
- package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
- package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
- package/skills/social-distribute/SKILL.md +485 -0
- package/skills/spec-templates/SKILL.md +274 -0
- package/skills/sync/SKILL.md +49 -0
- package/skills/test-browser/SKILL.md +464 -0
- package/skills/test-browser/references/feature-map.md +161 -0
- package/skills/test-fix-loop/SKILL.md +159 -0
- package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
- package/skills/triage/SKILL.md +402 -0
- package/skills/trigger-cron/SKILL.md +136 -0
- package/skills/trigger-cron/scripts/trigger.sh +198 -0
- package/skills/user-set-role/SKILL.md +147 -0
- package/skills/user-set-role/scripts/set-role.sh +218 -0
- package/skills/user-story-writer/SKILL.md +77 -0
- package/skills/ux-audit/SKILL.md +251 -0
- package/skills/ux-audit/references/finding.schema.json +62 -0
- package/skills/ux-audit/references/route-list.yaml +95 -0
- package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
- package/skills/ux-audit/scripts/bot-signin.ts +135 -0
- package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
- package/skills/verification-scaffold/SKILL.md +73 -0
- package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
- package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
- package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
- package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
- package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
- package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
- package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
- package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
- package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
- package/skills/verification-scaffold/references/guards.tsv +21 -0
- package/skills/verification-scaffold/references/verification-readme.template +55 -0
- package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
- package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
- package/skills/verification-scaffold/test/generator.test.sh +319 -0
- package/skills/verification-scaffold/test/manifest.test.sh +144 -0
- package/skills/work/SKILL.md +1480 -0
- package/skills/work/references/end-of-work-resume.md +7 -0
- package/skills/work/references/hook-test-main-cwd.md +24 -0
- package/skills/work/references/key-principles.md +83 -0
- package/skills/work/references/property-testing.md +122 -0
- package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
- package/skills/work/references/work-agent-teams.md +97 -0
- package/skills/work/references/work-lifecycle-parallel.md +147 -0
- package/skills/work/references/work-lockfile-bumps.md +97 -0
- package/skills/work/references/work-scratch-sandboxes.md +212 -0
- package/skills/work/references/work-subagent-fanout.md +61 -0
- package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
- package/skills/xcode-test/SKILL.md +360 -0
|
@@ -0,0 +1,2922 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Hermetic argv-hygiene test for the community scripts' credential-bearing curl calls
|
|
3
|
+
# (#9597): linkedin-setup.sh (client secret + introspected token, both as form fields),
|
|
4
|
+
# the OAuth 1.0a Authorization header in x-community.sh and x-setup.sh, the Discord Bot
|
|
5
|
+
# token in discord-community.sh and discord-setup.sh, and the Bluesky createSession body
|
|
6
|
+
# (handle + app password) in bsky-community.sh and bsky-setup.sh.
|
|
7
|
+
#
|
|
8
|
+
# A PATH-shim `curl` records its argv NUL-delimited and, when asked for `--config -`,
|
|
9
|
+
# its stdin config and, for `--data-binary @-`, its stdin BODY (recorded separately). A PATH-shim `jq` records its
|
|
10
|
+
# argv and the NAMES of its exported variables (never values). Nothing touches the
|
|
11
|
+
# network. Per path the test proves:
|
|
12
|
+
# 1. the credential is absent from curl's argument list (the /proc/<pid>/cmdline surface);
|
|
13
|
+
# 2. the exact `data-urlencode = "..."` / `header = "..."` line is on the stdin config,
|
|
14
|
+
# decoded the way curl decodes a config string (`\\` and `\"` un-escaped);
|
|
15
|
+
# 3. a malformed value (control character, or a quote / backslash where the field
|
|
16
|
+
# forbids one) is refused BEFORE any curl runs, with the script's existing exit code;
|
|
17
|
+
# 4. `--disable --noproxy '*'` are the first curl arguments.
|
|
18
|
+
# The scripts are driven end to end (their subcommands are non-interactive once the
|
|
19
|
+
# credentials are in the environment; generate-token reads its code from stdin and runs
|
|
20
|
+
# from a scratch directory that is not a git checkout, so write-env lands there).
|
|
21
|
+
# Run: bash plugins/soleur/skills/community/test/community-argv.test.sh
|
|
22
|
+
set -uo pipefail
|
|
23
|
+
|
|
24
|
+
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/../scripts" && pwd)"
|
|
25
|
+
LINKEDIN_SETUP="$HERE/linkedin-setup.sh"
|
|
26
|
+
X_COMMUNITY="$HERE/x-community.sh"
|
|
27
|
+
X_SETUP="$HERE/x-setup.sh"
|
|
28
|
+
DISCORD_COMMUNITY="$HERE/discord-community.sh"
|
|
29
|
+
DISCORD_SETUP="$HERE/discord-setup.sh"
|
|
30
|
+
BSKY_COMMUNITY="$HERE/bsky-community.sh"
|
|
31
|
+
BSKY_SETUP="$HERE/bsky-setup.sh"
|
|
32
|
+
HMAC_LIB="$HERE/lib/hmac-sha1-b64.sh"
|
|
33
|
+
PINNED_IMAGE='node:22-slim@sha256:4f77a690f2f8946ab16fe1e791a3ac0667ae1c3575c3e4d0d4589e9ed5bfaf3d'
|
|
34
|
+
|
|
35
|
+
# ===================================================================================
|
|
36
|
+
# HMAC-SHA1 oracle (S2, #9597). The plugin signs OAuth 1.0a in bash because the hosted runner
|
|
37
|
+
# image has no python3 (lib/hmac-sha1-b64.sh keeps the key off every exec argument). The
|
|
38
|
+
# ORACLE is the other implementation, `openssl dgst -hmac`, run here ONLY with synthetic
|
|
39
|
+
# keys; the RFC 2202 vectors are a second, non-openssl anchor. These functions are defined
|
|
40
|
+
# before the dependency check so `--oracle-only` can run them alone (the pinned-image run
|
|
41
|
+
# has openssl but no jq, and no sandbox).
|
|
42
|
+
# ===================================================================================
|
|
43
|
+
ORC_RAN=0 ORC_BAD=0 ORC_LEN=0 ORC_SPEC=0 ORC_RFC=0
|
|
44
|
+
|
|
45
|
+
# oc_rep <n> <pattern>: the pattern cycled to exactly n bytes, no trailing newline.
|
|
46
|
+
oc_rep() {
|
|
47
|
+
local n="$1" pat="$2" out=""
|
|
48
|
+
while (( ${#out} < n )); do out+="$pat"; done
|
|
49
|
+
printf '%s' "${out:0:n}"
|
|
50
|
+
}
|
|
51
|
+
# oc_bytes <hex-byte> <n>: a printf format emitting that byte n times.
|
|
52
|
+
oc_bytes() {
|
|
53
|
+
local i out=""
|
|
54
|
+
for (( i = 0; i < $2; i++ )); do out+="\\x$1"; done
|
|
55
|
+
printf '%s' "$out"
|
|
56
|
+
}
|
|
57
|
+
# orc_cmp <key> <message>: the function under test against `openssl dgst -hmac`.
|
|
58
|
+
orc_cmp() {
|
|
59
|
+
local want got OC_KEY="$1"
|
|
60
|
+
ORC_RAN=$((ORC_RAN + 1))
|
|
61
|
+
want="$(printf '%s' "$2" | openssl dgst -sha1 -hmac "$1" -binary | base64)"
|
|
62
|
+
got="$(printf '%s' "$2" | hmac_sha1_b64 OC_KEY 2>/dev/null)"
|
|
63
|
+
if [[ -z "$want" || "$got" != "$want" ]]; then ORC_BAD=$((ORC_BAD + 1)); fi
|
|
64
|
+
}
|
|
65
|
+
# orc_rfc <key-printf-format> <message-printf-format> <expected-hex>: an RFC 2202 vector.
|
|
66
|
+
orc_rfc() {
|
|
67
|
+
local OC_KEY want got i wantfmt=""
|
|
68
|
+
ORC_RAN=$((ORC_RAN + 1)); ORC_RFC=$((ORC_RFC + 1))
|
|
69
|
+
# shellcheck disable=SC2059,SC2034
|
|
70
|
+
printf -v OC_KEY "$1"
|
|
71
|
+
for (( i = 0; i < ${#3}; i += 2 )); do wantfmt+="\\x${3:i:2}"; done
|
|
72
|
+
# shellcheck disable=SC2059
|
|
73
|
+
want="$(printf "$wantfmt" | base64)"
|
|
74
|
+
# shellcheck disable=SC2059
|
|
75
|
+
got="$(printf "$2" | hmac_sha1_b64 OC_KEY 2>/dev/null)"
|
|
76
|
+
if [[ -z "$want" || "$got" != "$want" ]]; then ORC_BAD=$((ORC_BAD + 1)); fi
|
|
77
|
+
}
|
|
78
|
+
oracle_matrix() {
|
|
79
|
+
local -a msgs=() keys=()
|
|
80
|
+
local n k m i
|
|
81
|
+
ORC_RAN=0 ORC_BAD=0 ORC_LEN=0 ORC_SPEC=0 ORC_RFC=0
|
|
82
|
+
msgs=('' 'x' "$(oc_rep 200 'msg-0123456789&=%')" $'trailing-newline\n')
|
|
83
|
+
# Lengths around the 64-byte block boundary; the cycled pattern carries a backslash, a
|
|
84
|
+
# double quote, a space, '=' and '%' so every length also exercises odd bytes.
|
|
85
|
+
for n in 0 1 20 63 64 65 96 200; do
|
|
86
|
+
k="$(oc_rep "$n" 'Ab1&=%\"k Z+/')"
|
|
87
|
+
for m in "${msgs[@]}"; do orc_cmp "$k" "$m"; ORC_LEN=$((ORC_LEN + 1)); done
|
|
88
|
+
done
|
|
89
|
+
# Special keys: `6`x64 (ipad XOR gives NUL bytes), quoting/spacing hazards, '=' and '%',
|
|
90
|
+
# a base64-shaped key with +/=, and a multi-byte UTF-8 key.
|
|
91
|
+
keys=("$(oc_rep 64 '6')" 'a\b\\c' 'a"b"c' 'a b c ' 'abc==%41%zz=' 'Zm9v+/YmFy/+8=' $'caf\xc3\xa9\xc3\xa9')
|
|
92
|
+
for k in "${keys[@]}"; do
|
|
93
|
+
for m in "${msgs[@]}"; do orc_cmp "$k" "$m"; ORC_SPEC=$((ORC_SPEC + 1)); done
|
|
94
|
+
done
|
|
95
|
+
# RFC 2202 test cases 1-7 (HMAC-SHA1). Keys/messages are printf formats (byte escapes).
|
|
96
|
+
orc_rfc "$(oc_bytes 0b 20)" 'Hi There' b617318655057264e28bc0b6fb378c8ef146be00
|
|
97
|
+
orc_rfc 'Jefe' 'what do ya want for nothing?' effcdf6ae5eb2fa2d27416d5f184df9c259a7c79
|
|
98
|
+
orc_rfc "$(oc_bytes aa 20)" "$(oc_bytes dd 50)" 125d7342b9ac11cd91a39af48aa17b4f63f175d3
|
|
99
|
+
local k4=""; for (( i = 1; i <= 25; i++ )); do k4+="$(printf '\\x%02x' "$i")"; done
|
|
100
|
+
orc_rfc "$k4" "$(oc_bytes cd 50)" 4c9007f4026250c6bc8414f9bf50c86c2d7235da
|
|
101
|
+
orc_rfc "$(oc_bytes 0c 20)" 'Test With Truncation' 4c1a03424b55e07fe7f27be1d58bb9324a9a5a04
|
|
102
|
+
orc_rfc "$(oc_bytes aa 80)" 'Test Using Larger Than Block-Size Key - Hash Key First' aa4ae5e15272d00e95705637ce8a3b55ed402112
|
|
103
|
+
orc_rfc "$(oc_bytes aa 80)" 'Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data' e8e99d0f45237d786d6bbaa7965c7808bbff1a91
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if [[ "${1:-}" == "--oracle-only" ]]; then
|
|
107
|
+
command -v openssl >/dev/null 2>&1 || { echo "ORACLE-ONLY: openssl missing" >&2; exit 3; }
|
|
108
|
+
[[ -r "$HMAC_LIB" ]] || { echo "ORACLE-ONLY: $HMAC_LIB missing" >&2; exit 3; }
|
|
109
|
+
# shellcheck disable=SC1090
|
|
110
|
+
source "$HMAC_LIB"
|
|
111
|
+
oracle_matrix
|
|
112
|
+
printf 'ORACLE-ONLY bash=%s cases=%s len=%s spec=%s rfc=%s bad=%s\n' "$BASH_VERSION" "$ORC_RAN" "$ORC_LEN" "$ORC_SPEC" "$ORC_RFC" "$ORC_BAD"
|
|
113
|
+
[[ "$ORC_BAD" -eq 0 && "$ORC_LEN" -eq 32 && "$ORC_SPEC" -eq 28 && "$ORC_RFC" -eq 7 ]]
|
|
114
|
+
exit $?
|
|
115
|
+
fi
|
|
116
|
+
# shellcheck disable=SC1090
|
|
117
|
+
[[ -r "$HMAC_LIB" ]] && source "$HMAC_LIB"
|
|
118
|
+
|
|
119
|
+
# python3: ONLY the test uses it (the independent RFC 5849 percent-encoder of the OAuth oracle); the
|
|
120
|
+
# scripts under test never do, and the hosted runner image has none.
|
|
121
|
+
for dep in jq openssl python3; do
|
|
122
|
+
if ! command -v "$dep" >/dev/null 2>&1; then
|
|
123
|
+
if [[ -n "${CI:-}" ]]; then
|
|
124
|
+
echo "FAIL: $dep is required in CI but not found" >&2
|
|
125
|
+
exit 1
|
|
126
|
+
fi
|
|
127
|
+
echo "SKIP: $dep not installed"
|
|
128
|
+
exit 0
|
|
129
|
+
fi
|
|
130
|
+
done
|
|
131
|
+
|
|
132
|
+
SANDBOX="$(mktemp -d -t community-argv.XXXXXXXX)" || { echo "FAIL: mktemp" >&2; exit 1; }
|
|
133
|
+
[[ -n "$SANDBOX" && -d "$SANDBOX" ]] || { echo "FAIL: no sandbox" >&2; exit 1; }
|
|
134
|
+
trap 'rm -rf -- "$SANDBOX"' EXIT
|
|
135
|
+
|
|
136
|
+
MOCK="$SANDBOX/mock"
|
|
137
|
+
WORK="$SANDBOX/work"
|
|
138
|
+
GITWORK="$SANDBOX/gitwork"
|
|
139
|
+
mkdir -p "$MOCK" "$WORK" "$GITWORK" "$SANDBOX/home"
|
|
140
|
+
# x-setup.sh resolves a git root at startup (it only reads it for validate-credentials),
|
|
141
|
+
# so its runs happen in a scratch checkout. env -i keeps an inherited GIT_DIR / GIT_INDEX_FILE
|
|
142
|
+
# (a linked worktree exports both) from reaching this `git init`.
|
|
143
|
+
env -i PATH="$PATH" HOME="$SANDBOX/home" git init -q -- "$GITWORK" || { echo "FAIL: git init" >&2; exit 1; }
|
|
144
|
+
SYS_PATH="$PATH"
|
|
145
|
+
|
|
146
|
+
# --- shims -------------------------------------------------------------------------
|
|
147
|
+
cat > "$MOCK/curl" <<'SHIM'
|
|
148
|
+
#!/bin/bash
|
|
149
|
+
d="$MOCK_DIR"
|
|
150
|
+
n=$(( $(cat "$d/count" 2>/dev/null || echo 0) + 1 ))
|
|
151
|
+
echo "$n" > "$d/count"
|
|
152
|
+
printf '%s\0' "$@" > "$d/argv.$n"
|
|
153
|
+
# Names (never values) of the variables this child inherited: the `export` mutant row.
|
|
154
|
+
compgen -e > "$d/env.$n"
|
|
155
|
+
prev=""
|
|
156
|
+
for a in "$@"; do
|
|
157
|
+
if [[ "$prev" == "--config" && "$a" == "-" ]]; then
|
|
158
|
+
if [[ "${SHIM_MUTATE:-}" == nostdin ]]; then cat > /dev/null; else cat > "$d/stdin.$n"; fi
|
|
159
|
+
fi
|
|
160
|
+
# `--data-binary @-` reads the request body from stdin (and keeps CR/LF, like real curl). It is
|
|
161
|
+
# recorded as body.<n>, SEPARATE from a `--config -` stdin config (stdin.<n>).
|
|
162
|
+
if [[ "$prev" == "--data-binary" && "$a" == "@-" ]]; then
|
|
163
|
+
if [[ "${SHIM_MUTATE:-}" == nobody ]]; then cat > /dev/null; else cat > "$d/body.$n"; fi
|
|
164
|
+
fi
|
|
165
|
+
prev="$a"
|
|
166
|
+
[[ "$a" == http*://* ]] && url="$a"
|
|
167
|
+
done
|
|
168
|
+
[[ "${SHIM_MODE:-}" == fail7 ]] && exit 7
|
|
169
|
+
[[ "${SHIM_MODE:-}" == fail60 ]] && exit 60
|
|
170
|
+
# SHIM_MODE=badjwt: a hostile/garbled createSession reply whose accessJwt holds a quote, a newline
|
|
171
|
+
# and a `url = ...` config directive (the line-injection shape the bearer guard exists to refuse).
|
|
172
|
+
if [[ "${SHIM_MODE:-}" == badjwt && "$url" == *createSession ]]; then
|
|
173
|
+
printf '%s\n200\n' '{"accessJwt":"synth\"\nurl = \"https://evil.invalid/x\"","did":"did:plc:synth0001","handle":"synthetic.bsky.social"}'
|
|
174
|
+
exit 0
|
|
175
|
+
fi
|
|
176
|
+
case "$url" in
|
|
177
|
+
*introspectToken) printf '%s\n200\n' '{"active":true,"expires_at":4102444800,"scope":"openid"}' ;;
|
|
178
|
+
*accessToken) printf '%s\n200\n' '{"access_token":"synthetic-fixture-token-0002"}' ;;
|
|
179
|
+
*userinfo) printf '%s\n200\n' '{"sub":"synthetic0001"}' ;;
|
|
180
|
+
*2/tweets) printf '%s\n201\n' '{"data":{"id":"1","text":"hi"}}' ;;
|
|
181
|
+
*2/users/me*) printf '%s\n200\n' '{"data":{"id":"123","username":"synthetic","name":"Synthetic","public_metrics":{"followers_count":1,"following_count":1,"tweet_count":1}}}' ;;
|
|
182
|
+
*oauth2/applications/@me) printf '%s\n200\n' '{"id":"123456789012345678","name":"synthbot"}' ;;
|
|
183
|
+
*users/@me/guilds*) printf '%s\n200\n' '[{"id":"1","name":"G","approximate_member_count":3}]' ;;
|
|
184
|
+
*users/@me) printf '%s\n200\n' '{"id":"1","username":"synthbot"}' ;;
|
|
185
|
+
*/webhooks) printf '%s\n200\n' '{"id":"77","token":"synthwebhook0001"}' ;;
|
|
186
|
+
*messages*|*/members*) printf '%s\n200\n' '[]' ;;
|
|
187
|
+
*/channels) printf '%s\n200\n' '[{"id":"5","name":"c","type":0,"position":0}]' ;;
|
|
188
|
+
*with_counts*) printf '%s\n200\n' '{"id":"1","name":"G","approximate_member_count":3}' ;;
|
|
189
|
+
*createSession) printf '%s\n200\n' '{"accessJwt":"synthjwt.synthjwt.synthjwt","did":"did:plc:synth0001","handle":"synthetic.bsky.social"}' ;;
|
|
190
|
+
*createRecord) printf '%s\n200\n' '{"uri":"at://did:plc:synth0001/app.bsky.feed.post/1","cid":"synthcid0001"}' ;;
|
|
191
|
+
*getProfile*) printf '%s\n200\n' '{"did":"did:plc:synth0001","handle":"synthetic.bsky.social","displayName":"S","followersCount":1,"followsCount":1,"postsCount":1}' ;;
|
|
192
|
+
*) printf '%s\n500\n' '{}' ;;
|
|
193
|
+
esac
|
|
194
|
+
SHIM
|
|
195
|
+
chmod +x "$MOCK/curl"
|
|
196
|
+
# `jq` shim: records argv (a `--arg pw ...` regression is invisible to the curl shim alone) and the
|
|
197
|
+
# NAMES of the variables jq inherited, then runs the real jq. Passthrough, so every script is unchanged.
|
|
198
|
+
REAL_JQ="$(command -v jq)"
|
|
199
|
+
cat > "$MOCK/jq" <<'SHIM'
|
|
200
|
+
#!/bin/bash
|
|
201
|
+
d="$MOCK_DIR"
|
|
202
|
+
n=0
|
|
203
|
+
[[ -r "$d/jqcount" ]] && read -r n < "$d/jqcount"
|
|
204
|
+
n=$((n + 1))
|
|
205
|
+
printf '%s\n' "$n" > "$d/jqcount"
|
|
206
|
+
printf '%s\0' "$@" > "$d/jqargv.$n"
|
|
207
|
+
compgen -e > "$d/jqenv.$n"
|
|
208
|
+
# SHIM_MODE=jqfail: the createSession body build (the program that reads $ENV.BSKY_PW) fails.
|
|
209
|
+
[[ "${SHIM_MODE:-}" == jqfail && "$*" == *ENV.BSKY_PW* ]] && exit 5
|
|
210
|
+
exec "@REAL_JQ@" "$@"
|
|
211
|
+
SHIM
|
|
212
|
+
sed -i "s|@REAL_JQ@|$REAL_JQ|" "$MOCK/jq"
|
|
213
|
+
chmod +x "$MOCK/jq"
|
|
214
|
+
for noop in xdg-open open; do
|
|
215
|
+
printf '#!/bin/sh\nexit 0\n' > "$MOCK/$noop"
|
|
216
|
+
chmod +x "$MOCK/$noop"
|
|
217
|
+
done
|
|
218
|
+
# `date` that yields a timestamp with an embedded newline: oauth_timestamp is the one
|
|
219
|
+
# header field that is NOT url-encoded, so this is the only hermetic way to put a control
|
|
220
|
+
# character into a real oauth_sign header.
|
|
221
|
+
mkdir -p "$SANDBOX/baddate"
|
|
222
|
+
cat > "$SANDBOX/baddate/date" <<'SHIM'
|
|
223
|
+
#!/bin/sh
|
|
224
|
+
printf '12\n34'
|
|
225
|
+
SHIM
|
|
226
|
+
chmod +x "$SANDBOX/baddate/date"
|
|
227
|
+
|
|
228
|
+
# --- harness -----------------------------------------------------------------------
|
|
229
|
+
PASS=0
|
|
230
|
+
FAIL=0
|
|
231
|
+
ok() { PASS=$((PASS + 1)); echo " ok - $1"; }
|
|
232
|
+
bad() { FAIL=$((FAIL + 1)); echo " FAIL - $1"; }
|
|
233
|
+
not() { ! "$@"; }
|
|
234
|
+
check() { # check <label> <cmd...>
|
|
235
|
+
local label="$1"; shift
|
|
236
|
+
if "$@"; then ok "$label"; else bad "$label"; fi
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
OUT="" ERR="" RC=0
|
|
240
|
+
# run_sut <extra-path-dir|-> <stdin-text> <env...> -- <cmd...>
|
|
241
|
+
run_sut() {
|
|
242
|
+
local extra="$1" input="$2"; shift 2
|
|
243
|
+
local -a envs=()
|
|
244
|
+
while [[ "$1" != "--" ]]; do envs+=("$1"); shift; done
|
|
245
|
+
shift
|
|
246
|
+
rm -f "$MOCK"/count "$MOCK"/argv.* "$MOCK"/stdin.* "$MOCK"/env.* "$MOCK"/body.* \
|
|
247
|
+
"$MOCK"/jqcount "$MOCK"/jqargv.* "$MOCK"/jqenv.*
|
|
248
|
+
local p="$MOCK:$SYS_PATH"
|
|
249
|
+
[[ "$extra" != "-" ]] && p="$extra:$p"
|
|
250
|
+
# SUT_PATH replaces the whole PATH (the no-interpreter hosted canary).
|
|
251
|
+
[[ -n "${SUT_PATH:-}" ]] && p="$SUT_PATH"
|
|
252
|
+
local o="$SANDBOX/out" e="$SANDBOX/err"
|
|
253
|
+
( cd "${RUN_CWD:-$WORK}" && env -i PATH="$p" HOME="$SANDBOX/home" MOCK_DIR="$MOCK" "${envs[@]}" "$@" ) \
|
|
254
|
+
> "$o" 2> "$e" <<<"$input"
|
|
255
|
+
RC=$?
|
|
256
|
+
OUT="$(cat "$o")"
|
|
257
|
+
ERR="$(cat "$e")"
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
curl_calls() { cat "$MOCK/count" 2>/dev/null || echo 0; }
|
|
261
|
+
|
|
262
|
+
# argv_has <value> [n]: is <value> a substring of any recorded argv file (or file n)?
|
|
263
|
+
argv_has() {
|
|
264
|
+
local files=("$MOCK"/argv.*)
|
|
265
|
+
[[ -n "${2:-}" ]] && files=("$MOCK/argv.$2")
|
|
266
|
+
[[ -e "${files[0]}" ]] || return 1
|
|
267
|
+
grep -aqF -e "$1" -- "${files[@]}"
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
# argv_absent <value> [n]: <value> is NOT on the recorded argv (all calls, or call n) AND curl actually ran.
|
|
271
|
+
# Unlike `! argv_has`, it is FALSE when the argv file is missing, so a row cannot pass because curl
|
|
272
|
+
# never ran (a refused or crashed run records no argv at all).
|
|
273
|
+
argv_absent() {
|
|
274
|
+
local files=("$MOCK"/argv.*)
|
|
275
|
+
[[ -n "${2:-}" ]] && files=("$MOCK/argv.$2")
|
|
276
|
+
[[ -e "${files[0]}" ]] || return 1
|
|
277
|
+
! grep -aqF -e "$1" -- "${files[@]}"
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
# argv_first3 <n>: first three curl arguments, space-joined.
|
|
281
|
+
argv_first3() {
|
|
282
|
+
local -a a=()
|
|
283
|
+
mapfile -d '' -t a < "$MOCK/argv.$1"
|
|
284
|
+
printf '%s %s %s' "${a[0]:-}" "${a[1]:-}" "${a[2]:-}"
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
# cfg_decode <config-line-value>: un-escape `\\` and `\"` as curl does for a quoted string.
|
|
288
|
+
cfg_decode() {
|
|
289
|
+
local s="$1" out="" i c
|
|
290
|
+
for (( i = 0; i < ${#s}; i++ )); do
|
|
291
|
+
c="${s:i:1}"
|
|
292
|
+
if [[ "$c" == '\' ]]; then
|
|
293
|
+
i=$((i + 1))
|
|
294
|
+
c="${s:i:1}"
|
|
295
|
+
fi
|
|
296
|
+
out+="$c"
|
|
297
|
+
done
|
|
298
|
+
printf '%s' "$out"
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
stdin_text() { cat "$MOCK/stdin.$1" 2>/dev/null; }
|
|
302
|
+
stdin_lines() { local n=0 _l; while IFS= read -r _l; do n=$((n + 1)); done < "$MOCK/stdin.$1"; echo "$n"; }
|
|
303
|
+
|
|
304
|
+
# --- instrument controls --------------------------------------------------------------
|
|
305
|
+
# The helpers above are what every row trusts. Before row 1, drive check() with a failing command
|
|
306
|
+
# and not() with an always-true one and require the counters / return codes to move. Reported
|
|
307
|
+
# straight through printf + exit 1 (never through the helpers under test), then the counters reset
|
|
308
|
+
# so the controls are not counted as assertions.
|
|
309
|
+
_p0=$PASS; _f0=$FAIL
|
|
310
|
+
{ check "control: failing command" false; } >/dev/null
|
|
311
|
+
if (( FAIL != _f0 + 1 || PASS != _p0 )); then
|
|
312
|
+
printf 'FAIL: instrument control: check() did not count a failing command (PASS %s->%s, FAIL %s->%s)\n' "$_p0" "$PASS" "$_f0" "$FAIL" >&2
|
|
313
|
+
exit 1
|
|
314
|
+
fi
|
|
315
|
+
{ check "control: passing command" true; } >/dev/null
|
|
316
|
+
if (( PASS != _p0 + 1 || FAIL != _f0 + 1 )); then
|
|
317
|
+
printf 'FAIL: instrument control: check() did not count a passing command (PASS %s->%s, FAIL %s->%s)\n' "$_p0" "$PASS" "$_f0" "$FAIL" >&2
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
if not true; then
|
|
321
|
+
printf 'FAIL: instrument control: not() returned success for an always-true command\n' >&2
|
|
322
|
+
exit 1
|
|
323
|
+
fi
|
|
324
|
+
if ! not false; then
|
|
325
|
+
printf 'FAIL: instrument control: not() returned failure for an always-false command\n' >&2
|
|
326
|
+
exit 1
|
|
327
|
+
fi
|
|
328
|
+
PASS=$_p0; FAIL=$_f0
|
|
329
|
+
unset _p0 _f0
|
|
330
|
+
|
|
331
|
+
# ===================================================================================
|
|
332
|
+
echo "== linkedin-setup.sh validate-credentials =="
|
|
333
|
+
LI_SECRET="fixturefixturefixture01"
|
|
334
|
+
LI_TOKEN="synthetic-fixture-token-0001"
|
|
335
|
+
LI_ENV=(LINKEDIN_CLIENT_ID=synthetic-fixture-client-0001
|
|
336
|
+
"LINKEDIN_CLIENT_SECRET=$LI_SECRET" "LINKEDIN_ACCESS_TOKEN=$LI_TOKEN")
|
|
337
|
+
|
|
338
|
+
run_sut - "" "${LI_ENV[@]}" -- bash "$LINKEDIN_SETUP" validate-credentials
|
|
339
|
+
check "valid: exits 0" test "$RC" -eq 0
|
|
340
|
+
check "valid: exactly one curl call" test "$(curl_calls)" -eq 1
|
|
341
|
+
check "valid: client secret absent from argv" argv_absent "$LI_SECRET"
|
|
342
|
+
check "valid: introspected token absent from argv" argv_absent "$LI_TOKEN"
|
|
343
|
+
check "valid: non-secret client_id still on argv (recording works)" argv_has "client_id=synthetic-fixture-client-0001"
|
|
344
|
+
check "valid: --disable --noproxy '*' lead argv" test "$(argv_first3 1)" = "--disable --noproxy *"
|
|
345
|
+
check "valid: stdin carries --config - contract" argv_has "--config"
|
|
346
|
+
EXPECT=$'data-urlencode = "client_secret=fixturefixturefixture01"\ndata-urlencode = "token=synthetic-fixture-token-0001"'
|
|
347
|
+
check "valid: exact secret + token config lines on stdin" test "$(stdin_text 1)" = "$EXPECT"
|
|
348
|
+
|
|
349
|
+
# malformed: refused before any curl, exit 1 (the script's existing refusal code).
|
|
350
|
+
# The env var is built with $'..' so the control characters are real bytes.
|
|
351
|
+
li_refused() { # li_refused <label> <secret> <token>
|
|
352
|
+
run_sut - "" LINKEDIN_CLIENT_ID=synthetic-fixture-client-0001 \
|
|
353
|
+
"LINKEDIN_CLIENT_SECRET=$2" "LINKEDIN_ACCESS_TOKEN=$3" -- bash "$LINKEDIN_SETUP" validate-credentials
|
|
354
|
+
check "$1: exit 1" test "$RC" -eq 1
|
|
355
|
+
check "$1: curl never invoked" test "$(curl_calls)" -eq 0
|
|
356
|
+
check "$1: refusal reported on stderr" grep -qF "refusing to send it" <<<"$ERR"
|
|
357
|
+
}
|
|
358
|
+
li_refused "secret with newline" $'synthetic\nurl = "https://evil.invalid"' "$LI_TOKEN"
|
|
359
|
+
li_refused "secret with quote" 'syn"thetic' "$LI_TOKEN"
|
|
360
|
+
li_refused "secret with backslash" 'syn\thetic' "$LI_TOKEN"
|
|
361
|
+
li_refused "secret with CR" $'synthetic\rx' "$LI_TOKEN"
|
|
362
|
+
li_refused "token with newline" "$LI_SECRET" $'synthetic\nurl = "https://evil.invalid"'
|
|
363
|
+
li_refused "token with quote" "$LI_SECRET" 'syn"thetic'
|
|
364
|
+
|
|
365
|
+
echo "== linkedin-setup.sh generate-token =="
|
|
366
|
+
run_sut - $'synthetic-auth-code-0001\n' "${LI_ENV[@]}" -- bash "$LINKEDIN_SETUP" generate-token
|
|
367
|
+
check "generate-token: exits 0" test "$RC" -eq 0
|
|
368
|
+
check "generate-token: two curl calls (accessToken, userinfo)" test "$(curl_calls)" -eq 2
|
|
369
|
+
check "generate-token: client secret absent from argv (both calls)" argv_absent "$LI_SECRET"
|
|
370
|
+
check "generate-token: access token from the reply absent from argv" argv_absent "synthetic-fixture-token-0002"
|
|
371
|
+
check "generate-token: non-secret code still on argv (recording works)" argv_has "code=synthetic-auth-code-0001" 1
|
|
372
|
+
check "generate-token: --disable --noproxy '*' lead argv" test "$(argv_first3 1)" = "--disable --noproxy *"
|
|
373
|
+
check "generate-token: exact client_secret line on stdin" \
|
|
374
|
+
test "$(stdin_text 1)" = 'data-urlencode = "client_secret=fixturefixturefixture01"'
|
|
375
|
+
check "generate-token: userinfo bearer is the stdin header (unchanged)" \
|
|
376
|
+
test "$(stdin_text 2)" = 'header = "Authorization: Bearer synthetic-fixture-token-0002"'
|
|
377
|
+
|
|
378
|
+
for case_ in 'newline:'$'synthetic\nx' 'quote:syn"thetic' 'backslash:syn\thetic'; do
|
|
379
|
+
run_sut - $'synthetic-auth-code-0001\n' LINKEDIN_CLIENT_ID=synthetic-fixture-client-0001 \
|
|
380
|
+
"LINKEDIN_CLIENT_SECRET=${case_#*:}" LINKEDIN_ACCESS_TOKEN="$LI_TOKEN" -- bash "$LINKEDIN_SETUP" generate-token
|
|
381
|
+
check "generate-token secret with ${case_%%:*}: exit 1" test "$RC" -eq 1
|
|
382
|
+
check "generate-token secret with ${case_%%:*}: curl never invoked" test "$(curl_calls)" -eq 0
|
|
383
|
+
check "generate-token secret with ${case_%%:*}: refusal reported" grep -qF "refusing to send it" <<<"$ERR"
|
|
384
|
+
done
|
|
385
|
+
|
|
386
|
+
# ===================================================================================
|
|
387
|
+
echo "== OAuth 1.0a Authorization header =="
|
|
388
|
+
X_KEY="synthetic-fixture-key-0001"
|
|
389
|
+
X_SECRET="fixturefixturefixture02"
|
|
390
|
+
X_TOK="synthetic-fixture-access-0001"
|
|
391
|
+
X_TOKSECRET="synthetic-fixture-access-secret-0001"
|
|
392
|
+
X_ENV=("X_API_KEY=$X_KEY" "X_API_SECRET=$X_SECRET" "X_ACCESS_TOKEN=$X_TOK" "X_ACCESS_TOKEN_SECRET=$X_TOKSECRET" X_ALLOW_POST=true)
|
|
393
|
+
|
|
394
|
+
# x_header_checks <label> <n>: assertions common to every OAuth1 call.
|
|
395
|
+
x_header_checks() {
|
|
396
|
+
local label="$1" n="$2" raw decoded
|
|
397
|
+
raw="$(stdin_text "$n")"
|
|
398
|
+
decoded="${raw#header = \"}"
|
|
399
|
+
decoded="$(cfg_decode "${decoded%\"}")"
|
|
400
|
+
check "$label: signed header absent from argv" argv_absent "oauth_signature" "$n"
|
|
401
|
+
check "$label: Authorization header text absent from argv" argv_absent "Authorization: OAuth" "$n"
|
|
402
|
+
check "$label: consumer key absent from argv" argv_absent "$X_KEY" "$n"
|
|
403
|
+
check "$label: access token absent from argv" argv_absent "$X_TOK" "$n"
|
|
404
|
+
check "$label: consumer secret absent from argv" argv_absent "$X_SECRET" "$n"
|
|
405
|
+
check "$label: token secret absent from argv" argv_absent "$X_TOKSECRET" "$n"
|
|
406
|
+
check "$label: consumer + token secrets never reach curl at all (argv and stdin)" \
|
|
407
|
+
bash -c '[[ -e "$3" && -e "$4" ]] && ! grep -aqF -e "$1" -e "$2" -- "$3" "$4"' _ "$X_SECRET" "$X_TOKSECRET" "$MOCK/argv.$n" "$MOCK/stdin.$n"
|
|
408
|
+
check "$label: --disable --noproxy '*' lead argv" test "$(argv_first3 "$n")" = "--disable --noproxy *"
|
|
409
|
+
check "$label: config is exactly one header line" test "$(stdin_lines "$n")" -eq 1
|
|
410
|
+
check "$label: raw line is an escaped header directive" \
|
|
411
|
+
grep -qF 'header = "Authorization: OAuth oauth_consumer_key=\"synthetic-fixture-key-0001\", ' <<<"$raw"
|
|
412
|
+
check "$label: decoded header starts as the OAuth header" \
|
|
413
|
+
test "${decoded:0:63}" = 'Authorization: OAuth oauth_consumer_key="synthetic-fixture-key-'
|
|
414
|
+
check "$label: decoded header carries the access token and a signature" \
|
|
415
|
+
bash -c '[[ "$1" == *"oauth_token=\"synthetic-fixture-access-0001\""* && "$1" == *"oauth_signature=\""* && "$1" == *"oauth_version=\"1.0\"" ]]' _ "$decoded"
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
run_sut - "" "${X_ENV[@]}" -- bash "$X_COMMUNITY" fetch-metrics
|
|
419
|
+
check "x-community fetch-metrics (get_request): exit 0" test "$RC" -eq 0
|
|
420
|
+
check "x-community fetch-metrics: one curl call" test "$(curl_calls)" -eq 1
|
|
421
|
+
x_header_checks "x-community get_request" 1
|
|
422
|
+
|
|
423
|
+
run_sut - "" "${X_ENV[@]}" -- bash "$X_COMMUNITY" post-tweet "synthetic post"
|
|
424
|
+
check "x-community post-tweet (post_request): exit 0" test "$RC" -eq 0
|
|
425
|
+
check "x-community post-tweet: one curl call" test "$(curl_calls)" -eq 1
|
|
426
|
+
check "x-community post-tweet: non-secret body still on argv (recording works)" argv_has "synthetic post"
|
|
427
|
+
x_header_checks "x-community post_request" 1
|
|
428
|
+
|
|
429
|
+
RUN_CWD="$GITWORK" run_sut - "" "${X_ENV[@]}" -- bash "$X_SETUP" validate-credentials
|
|
430
|
+
check "x-setup validate-credentials: exit 0" test "$RC" -eq 0
|
|
431
|
+
check "x-setup validate-credentials: one curl call" test "$(curl_calls)" -eq 1
|
|
432
|
+
x_header_checks "x-setup" 1
|
|
433
|
+
|
|
434
|
+
# Refusal: a control character in the header. The newline-bearing timestamp is the only
|
|
435
|
+
# unencoded header field, so the shim `date` is the hermetic way to produce one.
|
|
436
|
+
x_refused() { # x_refused <label> <cmd...>
|
|
437
|
+
local label="$1"; shift
|
|
438
|
+
RUN_CWD="$GITWORK" run_sut "$SANDBOX/baddate" "" "${X_ENV[@]}" -- "$@"
|
|
439
|
+
check "$label: exit 1 (same as a failed request)" test "$RC" -eq 1
|
|
440
|
+
check "$label: curl never invoked" test "$(curl_calls)" -eq 0
|
|
441
|
+
check "$label: refusal reported on stderr" grep -qF "refusing to send it" <<<"$ERR"
|
|
442
|
+
}
|
|
443
|
+
x_refused "x-community get_request with control char in header" bash "$X_COMMUNITY" fetch-metrics
|
|
444
|
+
x_refused "x-community post_request with control char in header" bash "$X_COMMUNITY" post-tweet "synthetic post"
|
|
445
|
+
x_refused "x-setup with control char in header" bash "$X_SETUP" validate-credentials
|
|
446
|
+
|
|
447
|
+
# --- x-community.sh under bash -x: the CONDITIONAL xtrace prologue is kept -----------------------
|
|
448
|
+
# The prologue promises "a founder tracing with none set keeps full tracing". The HMAC library refuses
|
|
449
|
+
# `bash -x` unconditionally at load time, so loading it at the top of the script would turn a
|
|
450
|
+
# credential-free `bash -x x-community.sh` into exit 78. It is sourced lazily at the first signature.
|
|
451
|
+
echo "== x-community.sh under bash -x =="
|
|
452
|
+
run_sut - "" -- bash -x "$X_COMMUNITY"
|
|
453
|
+
check "x-community under bash -x with NO credentials and no command: exit 1 (usage), not the exit-78 refusal" test "$RC" -eq 1
|
|
454
|
+
check "x-community under bash -x with no credentials: the usage text is printed" grep -qF "Usage: x-community.sh <command>" <<<"$ERR"
|
|
455
|
+
check "x-community under bash -x with no credentials: no refusal text on either stream (neither the script's nor the HMAC library's)" \
|
|
456
|
+
not grep -qF "Refusing" <<<"$OUT$ERR"
|
|
457
|
+
run_sut - "" -- bash -x "$X_COMMUNITY" fetch-metrics
|
|
458
|
+
check "x-community fetch-metrics under bash -x with NO credentials: exit 1 (missing credentials), curl never invoked, no refusal text" \
|
|
459
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 && "$3" == *"Missing X API credentials"* && "$3" != *Refusing* ]]' _ "$RC" "$(curl_calls)" "$OUT$ERR"
|
|
460
|
+
XV_MARK="SYNTHXV""0001"
|
|
461
|
+
for xv_ in X_API_KEY X_API_SECRET X_ACCESS_TOKEN X_ACCESS_TOKEN_SECRET; do
|
|
462
|
+
run_sut - "" "$xv_=$XV_MARK" -- bash -x "$X_COMMUNITY" fetch-metrics
|
|
463
|
+
check "x-community under bash -x with only $xv_ set: still refused (exit 78), curl never invoked, the value reaches no output" \
|
|
464
|
+
bash -c '[[ "$1" -eq 78 && "$2" -eq 0 && "$3" == *"Refusing to run under"* && "$3" != *"$4"* ]]' _ "$RC" "$(curl_calls)" "$OUT$ERR" "$XV_MARK"
|
|
465
|
+
done
|
|
466
|
+
run_sut - "" "${X_ENV[@]}" -- bash -x "$X_COMMUNITY" fetch-metrics
|
|
467
|
+
check "x-community under bash -x with all four credentials set: exit 78, curl never invoked, no secret in any output" \
|
|
468
|
+
bash -c '[[ "$1" -eq 78 && "$2" -eq 0 && "$3" != *"$4"* && "$3" != *"$5"* ]]' _ "$RC" "$(curl_calls)" "$OUT$ERR" "$X_SECRET" "$X_TOKSECRET"
|
|
469
|
+
|
|
470
|
+
# --- config-string escaping, exercised on the real helper --------------------------
|
|
471
|
+
echo "== _cfg_q round trip =="
|
|
472
|
+
# shellcheck disable=SC1090
|
|
473
|
+
CFGQ_SRC="$(sed -n '/^_cfg_q() {/p' "$X_SETUP")"
|
|
474
|
+
eval "$CFGQ_SRC"
|
|
475
|
+
for v in 'plain' 'a"b' 'a\b' 'a\"b' '\\"' 'k="v", k2="v2"'; do
|
|
476
|
+
line="header = \"X: $(_cfg_q "$v")\""
|
|
477
|
+
inner="${line#header = \"X: }"
|
|
478
|
+
check "round trip of '$v'" test "$(cfg_decode "${inner%\"}")" = "$v"
|
|
479
|
+
done
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
# ===================================================================================
|
|
483
|
+
# Discord Bot token (discord-community.sh, discord-setup.sh) and the Bluesky createSession
|
|
484
|
+
# body (bsky-community.sh, bsky-setup.sh). All fixture credentials are SYNTHESIZED by
|
|
485
|
+
# concatenation, so no contiguous token-shaped literal exists in this file.
|
|
486
|
+
# ===================================================================================
|
|
487
|
+
_rep() { printf '%*s' "$2" '' | tr ' ' "$1"; }
|
|
488
|
+
DISC_TOK="SYNTH$(_rep A 19).$(_rep B 6).SYNTH$(_rep C 22)"
|
|
489
|
+
# Non-canonical but legal: dashes and underscores inside every segment.
|
|
490
|
+
DISC_TOK_ODD="SYNTH$(_rep A 8)_-$(_rep a 8).$(_rep B 3)-$(_rep b 2)_.SYNTH$(_rep C 10)-_$(_rep c 10)"
|
|
491
|
+
MARK="SYNTHMARK""0001"
|
|
492
|
+
GUILD=123456789012345678
|
|
493
|
+
BSKY_HANDLE_FIX="synthetic.bsky.social"
|
|
494
|
+
BSKY_PW_FIX="SYNTH$(_rep a 4)-$(_rep b 4)-$(_rep c 4)-$(_rep d 4)"
|
|
495
|
+
BSKY_JWT_FIX="synthjwt.synthjwt.synthjwt"
|
|
496
|
+
|
|
497
|
+
# disc_bad <class>: a Discord token that must be refused (the real shape is [A-Za-z0-9_-]+ x3, dot-joined).
|
|
498
|
+
disc_bad() {
|
|
499
|
+
case "$1" in
|
|
500
|
+
newline) printf '%s.aaa.bbb\nurl = "https://evil.invalid"' "$MARK" ;;
|
|
501
|
+
trailing-newline) printf '%s.aaa.bbb\n' "$MARK" ;;
|
|
502
|
+
quote) printf '%s.aaa.b"b' "$MARK" ;;
|
|
503
|
+
space) printf '%s.aaa.b b' "$MARK" ;;
|
|
504
|
+
backslash) printf '%s.aaa.b\\b' "$MARK" ;;
|
|
505
|
+
two-segments) printf '%s.aaa' "$MARK" ;;
|
|
506
|
+
*) echo "FATAL: unknown class $1" >&2; exit 2 ;;
|
|
507
|
+
esac
|
|
508
|
+
}
|
|
509
|
+
DISC_BAD_CLASSES=(newline trailing-newline quote space backslash two-segments)
|
|
510
|
+
|
|
511
|
+
no_leak() { ! grep -qF -- "$MARK" <<<"$OUT$ERR"; }
|
|
512
|
+
no_diag() { ! grep -qF "SOLEUR_TRANSPORT_DIAG" <<<"$OUT$ERR"; }
|
|
513
|
+
refusal_line() { grep -qxF "SOLEUR_CREDENTIAL_REFUSED script=$1 reason=${2:-token_shape}" <<<"$ERR"; }
|
|
514
|
+
# rc1_refusal <script> [reason]: exit 1 AND the exact value-free marker line on stderr.
|
|
515
|
+
rc1_refusal() { [[ "$RC" -eq 1 ]] && refusal_line "$@"; }
|
|
516
|
+
# curl_env_lacks <NAME...>: curl ran AND none of these variable NAMES were in its inherited environment.
|
|
517
|
+
curl_env_lacks() {
|
|
518
|
+
local f n seen=0
|
|
519
|
+
for f in "$MOCK"/env.*; do
|
|
520
|
+
[[ -e "$f" ]] || continue
|
|
521
|
+
seen=$((seen + 1))
|
|
522
|
+
for n in "$@"; do grep -qxF -- "$n" "$f" && return 1; done
|
|
523
|
+
done
|
|
524
|
+
(( seen >= 1 ))
|
|
525
|
+
}
|
|
526
|
+
chk_bot_stdin() { test "$(stdin_text "$1")" = "header = \"Authorization: Bot $2\""; }
|
|
527
|
+
|
|
528
|
+
# bot_call_checks <label> <n> <token>: the argv/stdin contract of one Discord call.
|
|
529
|
+
bot_call_checks() {
|
|
530
|
+
local label="$1" n="$2" tok="$3"
|
|
531
|
+
check "$label: bot token absent from argv" argv_absent "$tok" "$n"
|
|
532
|
+
check "$label: Authorization header text absent from argv" argv_absent "Authorization" "$n"
|
|
533
|
+
check "$label: --disable --noproxy '*' lead argv" test "$(argv_first3 "$n")" = "--disable --noproxy *"
|
|
534
|
+
check "$label: exact Bot header line on the stdin config" chk_bot_stdin "$n" "$tok"
|
|
535
|
+
check "$label: config is exactly one header line" test "$(stdin_lines "$n")" -eq 1
|
|
536
|
+
check "$label: stdin config requested (--config on argv)" argv_has "--config" "$n"
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
echo "== discord-community.sh =="
|
|
540
|
+
DC_ENV=("DISCORD_BOT_TOKEN=$DISC_TOK" "DISCORD_GUILD_ID=$GUILD")
|
|
541
|
+
run_sut - "" "${DC_ENV[@]}" -- bash "$DISCORD_COMMUNITY" guild-info
|
|
542
|
+
check "discord-community guild-info: exit 0" test "$RC" -eq 0
|
|
543
|
+
check "discord-community guild-info: exactly one curl call" test "$(curl_calls)" -eq 1
|
|
544
|
+
bot_call_checks "discord-community guild-info" 1 "$DISC_TOK"
|
|
545
|
+
run_sut - "" "${DC_ENV[@]}" -- bash "$DISCORD_COMMUNITY" channels
|
|
546
|
+
check "discord-community channels: exit 0, one call, exact Bot header on stdin" \
|
|
547
|
+
bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
548
|
+
check "discord-community channels: bot token absent from argv, exact stdin line" \
|
|
549
|
+
bash -c '! grep -aqF -e "$1" -- "$2" && [[ "$(cat "$3")" == "header = \"Authorization: Bot $1\"" ]]' _ "$DISC_TOK" "$MOCK/argv.1" "$MOCK/stdin.1"
|
|
550
|
+
run_sut - "" "${DC_ENV[@]}" -- bash "$DISCORD_COMMUNITY" messages 5 1
|
|
551
|
+
check "discord-community messages: exit 0, one call, token absent from argv, exact stdin line" \
|
|
552
|
+
bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]] && ! grep -aqF -e "$3" -- "$4" && [[ "$(cat "$5")" == "header = \"Authorization: Bot $3\"" ]]' _ "$RC" "$(curl_calls)" "$DISC_TOK" "$MOCK/argv.1" "$MOCK/stdin.1"
|
|
553
|
+
|
|
554
|
+
# must-PASS: a legal token that is not the canonical letters-only shape reaches the call.
|
|
555
|
+
run_sut - "" "DISCORD_BOT_TOKEN=$DISC_TOK_ODD" "DISCORD_GUILD_ID=$GUILD" -- bash "$DISCORD_COMMUNITY" guild-info
|
|
556
|
+
check "discord-community must-PASS (dots, dashes, underscores in the token): exit 0, one call" \
|
|
557
|
+
bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
558
|
+
check "discord-community must-PASS token: absent from argv, exact stdin line" \
|
|
559
|
+
bash -c '! grep -aqF -e "$1" -- "$2" && [[ "$(cat "$3")" == "header = \"Authorization: Bot $1\"" ]]' _ "$DISC_TOK_ODD" "$MOCK/argv.1" "$MOCK/stdin.1"
|
|
560
|
+
|
|
561
|
+
# disc_bad_val <class>: the token VALUE, byte-exact (a `$(...)` would strip a trailing newline).
|
|
562
|
+
disc_bad_val() { BAD_VAL="$(disc_bad "$1"; printf x)"; BAD_VAL="${BAD_VAL%x}"; }
|
|
563
|
+
for cls in "${DISC_BAD_CLASSES[@]}"; do
|
|
564
|
+
disc_bad_val "$cls"
|
|
565
|
+
run_sut - "" "DISCORD_BOT_TOKEN=$BAD_VAL" "DISCORD_GUILD_ID=$GUILD" -- bash "$DISCORD_COMMUNITY" guild-info
|
|
566
|
+
check "discord-community token ($cls): exit 1 and the exact value-free refusal line on stderr" rc1_refusal discord-community.sh
|
|
567
|
+
check "discord-community token ($cls): curl never invoked" test "$(curl_calls)" -eq 0
|
|
568
|
+
check "discord-community token ($cls): no leak of the value" no_leak
|
|
569
|
+
check "discord-community token ($cls): not the transport diagnostic" no_diag
|
|
570
|
+
check "discord-community token ($cls): one human line names the expected shape" \
|
|
571
|
+
grep -qF "three dot-separated base64url segments" <<<"$ERR"
|
|
572
|
+
check "discord-community token ($cls): the human line says the value is not shown" grep -qF "The value is not shown." <<<"$ERR"
|
|
573
|
+
check "discord-community token ($cls): not the old base64.base64.base64 wording" \
|
|
574
|
+
bash -c '! grep -qF -e "invalid format" -e "base64.base64.base64" <<<"$1"' _ "$ERR"
|
|
575
|
+
done
|
|
576
|
+
|
|
577
|
+
echo "== discord-setup.sh =="
|
|
578
|
+
DS_ENV=("DISCORD_BOT_TOKEN_INPUT=$DISC_TOK")
|
|
579
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" -- bash "$DISCORD_SETUP" validate-token
|
|
580
|
+
check "discord-setup validate-token: exit 0" test "$RC" -eq 0
|
|
581
|
+
check "discord-setup validate-token: two curl calls (users/@me, applications/@me)" test "$(curl_calls)" -eq 2
|
|
582
|
+
check "discord-setup validate-token: application id still on stdout" test "$OUT" = "$GUILD"
|
|
583
|
+
bot_call_checks "discord-setup validate-token call 1" 1 "$DISC_TOK"
|
|
584
|
+
bot_call_checks "discord-setup validate-token call 2" 2 "$DISC_TOK"
|
|
585
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" -- bash "$DISCORD_SETUP" discover-guilds
|
|
586
|
+
check "discord-setup discover-guilds: exit 0, one call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
587
|
+
bot_call_checks "discord-setup discover-guilds" 1 "$DISC_TOK"
|
|
588
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" -- bash "$DISCORD_SETUP" create-webhook "$GUILD"
|
|
589
|
+
check "discord-setup create-webhook: exit 0, one call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
590
|
+
check "discord-setup create-webhook: non-secret body still on argv (recording works)" argv_has "soleur-community" 1
|
|
591
|
+
check "discord-setup create-webhook: still a POST" argv_has "POST" 1
|
|
592
|
+
bot_call_checks "discord-setup create-webhook" 1 "$DISC_TOK"
|
|
593
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK_ODD" -- bash "$DISCORD_SETUP" validate-token
|
|
594
|
+
check "discord-setup must-PASS (dots, dashes, underscores in the token): exit 0, two calls" \
|
|
595
|
+
bash -c '[[ "$1" -eq 0 && "$2" -eq 2 ]]' _ "$RC" "$(curl_calls)"
|
|
596
|
+
check "discord-setup must-PASS token: exact stdin line on both calls, absent from argv" \
|
|
597
|
+
bash -c '[[ "$(cat "$3")" == "header = \"Authorization: Bot $1\"" && "$(cat "$4")" == "header = \"Authorization: Bot $1\"" ]] && ! grep -aqF -e "$1" -- "$5" "$6"' _ "$DISC_TOK_ODD" x "$MOCK/stdin.1" "$MOCK/stdin.2" "$MOCK/argv.1" "$MOCK/argv.2"
|
|
598
|
+
for cls in "${DISC_BAD_CLASSES[@]}"; do
|
|
599
|
+
disc_bad_val "$cls"
|
|
600
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$BAD_VAL" -- bash "$DISCORD_SETUP" validate-token
|
|
601
|
+
check "discord-setup token ($cls): exit 1 and the exact value-free refusal line on stderr" rc1_refusal discord-setup.sh
|
|
602
|
+
check "discord-setup token ($cls): one human line names the expected shape" \
|
|
603
|
+
grep -qF "three dot-separated base64url segments" <<<"$ERR"
|
|
604
|
+
check "discord-setup token ($cls): the human line says the value is not shown" grep -qF "The value is not shown." <<<"$ERR"
|
|
605
|
+
check "discord-setup token ($cls): curl never invoked" test "$(curl_calls)" -eq 0
|
|
606
|
+
check "discord-setup token ($cls): no leak of the value" no_leak
|
|
607
|
+
check "discord-setup token ($cls): not the connect-failure report" not grep -qF "Failed to connect" <<<"$ERR"
|
|
608
|
+
done
|
|
609
|
+
# xtrace refusal (the lint's "binds a live credential but carries no xtrace refusal").
|
|
610
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" -- bash -x "$DISCORD_SETUP" validate-token
|
|
611
|
+
check "discord-setup under bash -x with the token bound: exit 78" test "$RC" -eq 78
|
|
612
|
+
check "discord-setup under bash -x: curl never invoked" test "$(curl_calls)" -eq 0
|
|
613
|
+
check "discord-setup under bash -x: the token reaches no output" bash -c '! grep -qF -e "$1" <<<"$2"' _ "$DISC_TOK" "$OUT$ERR"
|
|
614
|
+
# transport failure: the proxy case is NAMED (the conversion adds --noproxy '*').
|
|
615
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" HTTPS_PROXY=http://127.0.0.1:1 SHIM_MODE=fail7 -- bash "$DISCORD_SETUP" validate-token
|
|
616
|
+
check "discord-setup connect failure with a proxy set: exit 1" test "$RC" -eq 1
|
|
617
|
+
check "discord-setup connect failure with a proxy set: names the deliberate proxy bypass" grep -qF "bypasses your proxy" <<<"$ERR"
|
|
618
|
+
check "discord-setup connect failure: token not echoed" bash -c '! grep -qF -e "$1" <<<"$2"' _ "$DISC_TOK" "$OUT$ERR"
|
|
619
|
+
check "discord-setup connect failure with a proxy set: the structured diagnostic marker is on stdout" \
|
|
620
|
+
grep -qF "SOLEUR_TRANSPORT_DIAG surface=" <<<"$OUT"
|
|
621
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" SHIM_MODE=fail7 -- bash "$DISCORD_SETUP" validate-token
|
|
622
|
+
check "discord-setup connect failure without a proxy: exit 1, no proxy claim" \
|
|
623
|
+
bash -c '[[ "$1" -eq 1 ]] && ! grep -qF "bypasses your proxy" <<<"$2"' _ "$RC" "$ERR"
|
|
624
|
+
check "discord-setup connect failure with no proxy and no curlrc: the bare network line is the last resort" \
|
|
625
|
+
grep -qF "Check your network connection" <<<"$ERR"
|
|
626
|
+
# Transport parity with the sibling scripts (TLS-trust arm, ~/.curlrc arm, the diagnostic marker).
|
|
627
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" SHIM_MODE=fail60 -- bash "$DISCORD_SETUP" validate-token
|
|
628
|
+
check "discord-setup curl exit 60: exit 1" test "$RC" -eq 1
|
|
629
|
+
check "discord-setup curl exit 60: the message names TLS trust, not the network" \
|
|
630
|
+
bash -c 'grep -qF "TLS trust failure" <<<"$1" && ! grep -qF "Check your network connection" <<<"$1"' _ "$ERR"
|
|
631
|
+
check "discord-setup curl exit 60: the diagnostic marker carries curl_exit=60 and the script name" \
|
|
632
|
+
grep -qF "script=discord-setup.sh curl_exit=60" <<<"$OUT"
|
|
633
|
+
printf 'proxy = "http://127.0.0.1:1"\n' > "$SANDBOX/home/.curlrc"
|
|
634
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" SHIM_MODE=fail7 -- bash "$DISCORD_SETUP" validate-token
|
|
635
|
+
rm -f "$SANDBOX/home/.curlrc"
|
|
636
|
+
check "discord-setup connect failure with a ~/.curlrc: the message names the curlrc that --disable dropped" \
|
|
637
|
+
bash -c 'grep -qF "~/.curlrc" <<<"$1" && grep -qF "(--disable)" <<<"$1" && ! grep -qF "Check your network connection" <<<"$1"' _ "$ERR"
|
|
638
|
+
# The transport-trust variables are unset in the prologue: an exported CURL_CA_BUNDLE must not reach curl.
|
|
639
|
+
TLS_ENV=(CURL_CA_BUNDLE=/nonexistent/ca.pem SSL_CERT_FILE=/nonexistent/c.pem SSL_CERT_DIR=/nonexistent SSLKEYLOGFILE=/nonexistent/k.log CURL_HOME=/nonexistent)
|
|
640
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" "${TLS_ENV[@]}" -- bash "$DISCORD_SETUP" discover-guilds
|
|
641
|
+
check "discord-setup with the trust variables exported: exit 0, one call (recording works)" \
|
|
642
|
+
bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
643
|
+
check "discord-setup: CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR SSLKEYLOGFILE CURL_HOME never reach curl's environment" \
|
|
644
|
+
curl_env_lacks CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR SSLKEYLOGFILE CURL_HOME
|
|
645
|
+
|
|
646
|
+
# --- harness rows: a shim that stops recording stdin turns the Discord stdin row RED -------
|
|
647
|
+
run_sut - "" "${DC_ENV[@]}" SHIM_MUTATE=nostdin -- bash "$DISCORD_COMMUNITY" guild-info
|
|
648
|
+
check "harness: a shim that stops recording stdin makes the Discord stdin row RED" not chk_bot_stdin 1 "$DISC_TOK"
|
|
649
|
+
check "harness: that control is RED because of the shim: curl ran and no stdin file was recorded (not a refused credential)" \
|
|
650
|
+
bash -c '[[ "$1" -ge 1 && ! -e "$2" ]]' _ "$(curl_calls)" "$MOCK/stdin.1"
|
|
651
|
+
|
|
652
|
+
# ===================================================================================
|
|
653
|
+
echo "== bsky-community.sh createSession =="
|
|
654
|
+
BC_ENV=("BSKY_HANDLE=$BSKY_HANDLE_FIX" "BSKY_APP_PASSWORD=$BSKY_PW_FIX")
|
|
655
|
+
# A handle holding a real control character (newline): refused, never sent.
|
|
656
|
+
BAD_HANDLE=$'syn\nthetic'"$MARK"
|
|
657
|
+
|
|
658
|
+
# body_arg <n>: the operand after --data-binary on call n.
|
|
659
|
+
body_arg() {
|
|
660
|
+
local -a a=(); local i
|
|
661
|
+
mapfile -d '' -t a < "$MOCK/argv.$1"
|
|
662
|
+
for (( i = 0; i < ${#a[@]} - 1; i++ )); do
|
|
663
|
+
[[ "${a[i]}" == "--data-binary" ]] && { printf '%s' "${a[i+1]}"; return 0; }
|
|
664
|
+
done
|
|
665
|
+
return 1
|
|
666
|
+
}
|
|
667
|
+
# The createSession body arrives on curl's STDIN (`--data-binary @-`); the shim records it as body.<n>.
|
|
668
|
+
chk_body_ok() { # <n> <handle> <password>
|
|
669
|
+
[[ -s "$MOCK/body.$1" ]] && jq -e --arg h "$2" --arg p "$3" '.identifier == $h and .password == $p' "$MOCK/body.$1" >/dev/null 2>&1
|
|
670
|
+
}
|
|
671
|
+
jq_argv_has() { local f; for f in "$MOCK"/jqargv.*; do [[ -e "$f" ]] && grep -aqF -e "$1" -- "$f" && return 0; done; return 1; }
|
|
672
|
+
# jq_argv_absent <value>: not on any recorded jq argv AND jq actually ran (FALSE when nothing was recorded).
|
|
673
|
+
jq_argv_absent() {
|
|
674
|
+
local f seen=0
|
|
675
|
+
for f in "$MOCK"/jqargv.*; do
|
|
676
|
+
[[ -e "$f" ]] || continue
|
|
677
|
+
seen=1
|
|
678
|
+
grep -aqF -e "$1" -- "$f" && return 1
|
|
679
|
+
done
|
|
680
|
+
(( seen ))
|
|
681
|
+
}
|
|
682
|
+
jq_calls() { cat "$MOCK/jqcount" 2>/dev/null || echo 0; }
|
|
683
|
+
# jq_recording_ok: the jq shim recorded >= 2 calls AND one of them is the writer (its program reads $ENV.BSKY_PW).
|
|
684
|
+
jq_recording_ok() { [[ "$(jq_calls)" -ge 2 ]] && jq_argv_has 'ENV.BSKY_PW'; }
|
|
685
|
+
# The writer is the jq call whose program reads $ENV.BSKY_PW; it must inherit the names, every other jq must not.
|
|
686
|
+
jq_writer_env_ok() {
|
|
687
|
+
local f n seen=0
|
|
688
|
+
for f in "$MOCK"/jqargv.*; do
|
|
689
|
+
[[ -e "$f" ]] || continue
|
|
690
|
+
n="${f##*.}"
|
|
691
|
+
if grep -aqF 'ENV.BSKY_PW' "$f"; then
|
|
692
|
+
seen=$((seen + 1))
|
|
693
|
+
grep -qxF BSKY_PW "$MOCK/jqenv.$n" && grep -qxF BSKY_ID "$MOCK/jqenv.$n" || return 1
|
|
694
|
+
fi
|
|
695
|
+
done
|
|
696
|
+
(( seen >= 1 ))
|
|
697
|
+
}
|
|
698
|
+
jq_others_env_clean() {
|
|
699
|
+
local f n others=0
|
|
700
|
+
for f in "$MOCK"/jqargv.*; do
|
|
701
|
+
[[ -e "$f" ]] || continue
|
|
702
|
+
n="${f##*.}"
|
|
703
|
+
grep -aqF 'ENV.BSKY_PW' "$f" && continue
|
|
704
|
+
others=$((others + 1))
|
|
705
|
+
grep -qxF -e BSKY_PW -e BSKY_ID "$MOCK/jqenv.$n" && return 1
|
|
706
|
+
done
|
|
707
|
+
(( others >= 1 ))
|
|
708
|
+
}
|
|
709
|
+
curl_env_clean() {
|
|
710
|
+
local f seen=0
|
|
711
|
+
for f in "$MOCK"/env.*; do
|
|
712
|
+
[[ -e "$f" ]] || continue
|
|
713
|
+
seen=$((seen + 1))
|
|
714
|
+
grep -qxF -e BSKY_PW -e BSKY_ID "$f" && return 1
|
|
715
|
+
done
|
|
716
|
+
(( seen >= 1 ))
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
# bsky_body_checks <label> <n> <handle> <password>
|
|
720
|
+
bsky_body_checks() {
|
|
721
|
+
local label="$1" n="$2" h="$3" pw="$4"
|
|
722
|
+
check "$label: password absent from argv" argv_absent "$pw" "$n"
|
|
723
|
+
check "$label: handle absent from argv" argv_absent "$h" "$n"
|
|
724
|
+
check "$label: --disable --noproxy '*' lead argv" test "$(argv_first3 "$n")" = "--disable --noproxy *"
|
|
725
|
+
check "$label: body sent as --data-binary @- (curl's stdin, no file)" test "$(body_arg "$n" || true)" = "@-"
|
|
726
|
+
check "$label: stdin body held exactly the handle and password" chk_body_ok "$n" "$h" "$pw"
|
|
727
|
+
check "$label: createSession carries no stdin config (the body owns stdin)" argv_absent "--config" "$n"
|
|
728
|
+
check "$label: explicit JSON Content-Type" argv_has "Content-Type: application/json" "$n"
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$BSKY_COMMUNITY" create-session
|
|
732
|
+
check "bsky-community create-session: exit 0" test "$RC" -eq 0
|
|
733
|
+
check "bsky-community create-session: exactly one curl call" test "$(curl_calls)" -eq 1
|
|
734
|
+
check "bsky-community create-session: the session DID still reaches stdout" grep -qF "did:plc:synth0001" <<<"$OUT"
|
|
735
|
+
bsky_body_checks "bsky-community create-session" 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
736
|
+
check "bsky-community create-session: password absent from every jq argv (jq shim)" jq_argv_absent "$BSKY_PW_FIX"
|
|
737
|
+
check "bsky-community create-session: handle absent from every jq argv (jq shim)" jq_argv_absent "$BSKY_HANDLE_FIX"
|
|
738
|
+
check "bsky-community create-session: jq shim recorded calls and the writer reads \$ENV.BSKY_PW (recording works)" \
|
|
739
|
+
jq_recording_ok
|
|
740
|
+
check "bsky-community create-session: the writer jq inherits both variable names" jq_writer_env_ok
|
|
741
|
+
check "bsky-community create-session: no other jq child inherits them" jq_others_env_clean
|
|
742
|
+
check "bsky-community create-session: the curl child does not inherit them" curl_env_clean
|
|
743
|
+
|
|
744
|
+
# handle with a double quote: escaped by jq, never injectable
|
|
745
|
+
run_sut - "" "BSKY_HANDLE=syn\"thetic.bsky.social" "BSKY_APP_PASSWORD=$BSKY_PW_FIX" -- bash "$BSKY_COMMUNITY" create-session
|
|
746
|
+
# (The script's own session-echo line is not escaped, so its exit status is not asserted: that
|
|
747
|
+
# line is unreachable with a real session, since Bluesky would never mint one for this handle.)
|
|
748
|
+
check "bsky-community handle with a double quote: createSession is still sent exactly once" test "$(curl_calls)" -eq 1
|
|
749
|
+
check "bsky-community handle with a double quote: body decodes to exactly that handle (escaped, not injected)" \
|
|
750
|
+
chk_body_ok 1 'syn"thetic.bsky.social' "$BSKY_PW_FIX"
|
|
751
|
+
# a handle or password with a control character is refused BEFORE curl; non-zero, never 0 or 3; marker on stderr
|
|
752
|
+
# bsky_field_line <field> <other-field>: the ONE human line names the refused FIELD (label only) and not the other.
|
|
753
|
+
bsky_field_line() { grep -qF "Error: $1 is empty or contains a control character" <<<"$ERR" && ! grep -qF "$2" <<<"$ERR"; }
|
|
754
|
+
bsky_refused() { # <label> <handle> <password> <refused-field> <other-field>
|
|
755
|
+
run_sut - "" "BSKY_HANDLE=$2" "BSKY_APP_PASSWORD=$3" -- bash "$BSKY_COMMUNITY" create-session
|
|
756
|
+
check "$1: exit 1 (non-zero, never 0 or 3) and the exact value-free marker, reason=control_char" rc1_refusal bsky-community.sh control_char
|
|
757
|
+
check "$1: curl never invoked" test "$(curl_calls)" -eq 0
|
|
758
|
+
check "$1: value never echoed" no_leak
|
|
759
|
+
check "$1: jq never invoked (the refused value reaches no child process)" test "$(jq_calls)" -eq 0
|
|
760
|
+
check "$1: one human line names the refused field only" bsky_field_line "$4" "$5"
|
|
761
|
+
check "$1: not the transport diagnostic" no_diag
|
|
762
|
+
}
|
|
763
|
+
bsky_refused "bsky-community handle with a newline" "$BAD_HANDLE" "$BSKY_PW_FIX" BSKY_HANDLE BSKY_APP_PASSWORD
|
|
764
|
+
bsky_refused "bsky-community password with a control character" "$BSKY_HANDLE_FIX" "$MARK"$'\x01'"x" BSKY_APP_PASSWORD BSKY_HANDLE
|
|
765
|
+
# a failed body build is classified as a BODY-BUILD failure (exit 1, curl never run, no transport diagnostic).
|
|
766
|
+
bsky_jqfail_rows() { # <label>
|
|
767
|
+
check "$1: jq failure while building the body: exit 1, no session reported" \
|
|
768
|
+
bash -c '[[ "$1" -eq 1 ]] && ! grep -qF "did:plc:synth0001" <<<"$2"' _ "$RC" "$OUT"
|
|
769
|
+
check "$1: jq failure names a body-build failure" grep -qF "could not build the Bluesky createSession request body" <<<"$ERR"
|
|
770
|
+
check "$1: jq failure is NOT reported as a curl transport failure" \
|
|
771
|
+
bash -c '! grep -qF -e "SOLEUR_TRANSPORT_DIAG" -e "curl_exit" -e "Failed to connect" <<<"$1"' _ "$OUT$ERR"
|
|
772
|
+
check "$1: jq failure: curl never invoked (nothing was sent)" test "$(curl_calls)" -eq 0
|
|
773
|
+
}
|
|
774
|
+
run_sut - "" "${BC_ENV[@]}" SHIM_MODE=jqfail -- bash "$BSKY_COMMUNITY" create-session
|
|
775
|
+
bsky_jqfail_rows "bsky-community"
|
|
776
|
+
# xtrace refusal with the password bound: exit 78, curl never invoked, the password reaches no output.
|
|
777
|
+
run_sut - "" "${BC_ENV[@]}" -- bash -x "$BSKY_COMMUNITY" create-session
|
|
778
|
+
check "bsky-community under bash -x with the password bound: exit 78" test "$RC" -eq 78
|
|
779
|
+
check "bsky-community under bash -x: curl never invoked" test "$(curl_calls)" -eq 0
|
|
780
|
+
check "bsky-community under bash -x: the password reaches no output" bash -c '! grep -qF -e "$1" <<<"$2"' _ "$BSKY_PW_FIX" "$OUT$ERR"
|
|
781
|
+
|
|
782
|
+
echo "== bsky-community.sh get-metrics and post =="
|
|
783
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$BSKY_COMMUNITY" get-metrics
|
|
784
|
+
check "bsky-community get-metrics: exit 0, two calls (createSession, getProfile)" bash -c '[[ "$1" -eq 0 && "$2" -eq 2 ]]' _ "$RC" "$(curl_calls)"
|
|
785
|
+
bsky_body_checks "bsky-community get-metrics createSession" 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
786
|
+
check "bsky-community get-metrics: the session bearer is the stdin header on the profile call (unchanged)" \
|
|
787
|
+
test "$(stdin_text 2)" = "header = \"Authorization: Bearer $BSKY_JWT_FIX\""
|
|
788
|
+
run_sut - "" "${BC_ENV[@]}" BSKY_ALLOW_POST=true -- bash "$BSKY_COMMUNITY" post "synthetic post text"
|
|
789
|
+
check "bsky-community post (the hosted path): exit 0, two calls (createSession, createRecord)" bash -c '[[ "$1" -eq 0 && "$2" -eq 2 ]]' _ "$RC" "$(curl_calls)"
|
|
790
|
+
bsky_body_checks "bsky-community post createSession" 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
791
|
+
check "bsky-community post: the password never reaches the createRecord call either" argv_absent "$BSKY_PW_FIX" 2
|
|
792
|
+
run_sut - "" "BSKY_HANDLE=$BSKY_HANDLE_FIX" "BSKY_APP_PASSWORD=$MARK"$'\x01'"x" BSKY_ALLOW_POST=true -- bash "$BSKY_COMMUNITY" post "synthetic post text"
|
|
793
|
+
check "bsky-community post (hosted): a refused credential exits 1, never 0 or 3, curl never invoked" \
|
|
794
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
795
|
+
check "bsky-community post (hosted): the refusal marker (reason=control_char) reaches stderr, where content-publisher captures it" refusal_line bsky-community.sh control_char
|
|
796
|
+
|
|
797
|
+
# A SERVER-SUPPLIED session token with a quote + newline + `url = ...` directive must be refused by
|
|
798
|
+
# the _bearer_ok guard BEFORE it is formatted into the stdin config: only the createSession call runs.
|
|
799
|
+
badjwt_rows() { # <label>
|
|
800
|
+
check "$1: a hostile accessJwt is refused (exit 1)" test "$RC" -eq 1
|
|
801
|
+
check "$1: only createSession ran (zero createRecord / getProfile calls)" test "$(curl_calls)" -eq 1
|
|
802
|
+
check "$1: the refusal names the unexpected shape" grep -qE "accessJwt.*(unexpected shape|missing)" <<<"$ERR"
|
|
803
|
+
check "$1: the injected directive never reaches the output" bash -c '! grep -qF "evil.invalid" <<<"$1"' _ "$OUT$ERR"
|
|
804
|
+
}
|
|
805
|
+
run_sut - "" "${BC_ENV[@]}" SHIM_MODE=badjwt -- bash "$BSKY_COMMUNITY" get-metrics
|
|
806
|
+
badjwt_rows "bsky-community get-metrics"
|
|
807
|
+
run_sut - "" "${BC_ENV[@]}" BSKY_ALLOW_POST=true SHIM_MODE=badjwt -- bash "$BSKY_COMMUNITY" post "synthetic post text"
|
|
808
|
+
badjwt_rows "bsky-community post"
|
|
809
|
+
|
|
810
|
+
# --- harness row: a shim that stops reading the stdin body turns the body rows RED -------------
|
|
811
|
+
run_sut - "" "${BC_ENV[@]}" SHIM_MUTATE=nobody -- bash "$BSKY_COMMUNITY" create-session
|
|
812
|
+
check "harness: a shim that stops recording the stdin body makes the body-content row RED" not chk_body_ok 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
813
|
+
check "harness: that control is RED because of the shim: curl ran and no body file was recorded (not a refused credential)" \
|
|
814
|
+
bash -c '[[ "$1" -ge 1 && ! -e "$2" ]]' _ "$(curl_calls)" "$MOCK/body.1"
|
|
815
|
+
|
|
816
|
+
echo "== bsky-setup.sh verify =="
|
|
817
|
+
# verify sources $GIT_ROOT/.env; the file is synthesized here and lives in the scratch checkout.
|
|
818
|
+
printf 'BSKY_HANDLE=%s\nBSKY_APP_PASSWORD=%s\n' "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX" > "$GITWORK/.env"
|
|
819
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$BSKY_SETUP" verify
|
|
820
|
+
check "bsky-setup verify: exit 0, two calls (createSession, getProfile)" bash -c '[[ "$1" -eq 0 && "$2" -eq 2 ]]' _ "$RC" "$(curl_calls)"
|
|
821
|
+
bsky_body_checks "bsky-setup verify createSession" 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
822
|
+
check "bsky-setup verify: password absent from every jq argv (jq shim)" jq_argv_absent "$BSKY_PW_FIX"
|
|
823
|
+
check "bsky-setup verify: handle absent from every jq argv (jq shim)" jq_argv_absent "$BSKY_HANDLE_FIX"
|
|
824
|
+
check "bsky-setup verify: the writer jq inherits the variable names" jq_writer_env_ok
|
|
825
|
+
check "bsky-setup verify: no other jq child inherits them" jq_others_env_clean
|
|
826
|
+
check "bsky-setup verify: the curl children do not inherit them" curl_env_clean
|
|
827
|
+
check "bsky-setup verify: the .env credentials are un-exported again (BSKY_HANDLE, BSKY_APP_PASSWORD absent from curl's environment)" \
|
|
828
|
+
curl_env_lacks BSKY_HANDLE BSKY_APP_PASSWORD
|
|
829
|
+
check "bsky-setup verify: the session bearer is the stdin header on the profile call (unchanged)" \
|
|
830
|
+
test "$(stdin_text 2)" = "header = \"Authorization: Bearer $BSKY_JWT_FIX\""
|
|
831
|
+
RUN_CWD="$GITWORK" run_sut - "" SHIM_MODE=jqfail -- bash "$BSKY_SETUP" verify
|
|
832
|
+
bsky_jqfail_rows "bsky-setup verify"
|
|
833
|
+
RUN_CWD="$GITWORK" run_sut - "" SHIM_MODE=badjwt -- bash "$BSKY_SETUP" verify
|
|
834
|
+
badjwt_rows "bsky-setup verify"
|
|
835
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash -x "$BSKY_SETUP" verify
|
|
836
|
+
check "bsky-setup under bash -x: exit 78" test "$RC" -eq 78
|
|
837
|
+
check "bsky-setup under bash -x: curl never invoked" test "$(curl_calls)" -eq 0
|
|
838
|
+
check "bsky-setup under bash -x: the password reaches no output" bash -c '! grep -qF -e "$1" <<<"$2"' _ "$BSKY_PW_FIX" "$OUT$ERR"
|
|
839
|
+
printf 'BSKY_HANDLE=$%s\nBSKY_APP_PASSWORD=%s\n' "'syn\\001thetic${MARK}'" "$BSKY_PW_FIX" > "$GITWORK/.env"
|
|
840
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$BSKY_SETUP" verify
|
|
841
|
+
check "bsky-setup verify with a control character in the handle: exit 1, curl never invoked" \
|
|
842
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
843
|
+
check "bsky-setup verify with a control character in the handle: exact refusal marker (reason=control_char) on stderr" refusal_line bsky-setup.sh control_char
|
|
844
|
+
check "bsky-setup verify with a control character in the handle: value not echoed" no_leak
|
|
845
|
+
check "bsky-setup verify with a control character in the handle: jq never invoked" test "$(jq_calls)" -eq 0
|
|
846
|
+
check "bsky-setup verify with a control character in the handle: one human line names BSKY_HANDLE only" bsky_field_line BSKY_HANDLE BSKY_APP_PASSWORD
|
|
847
|
+
printf 'BSKY_HANDLE=%s\nBSKY_APP_PASSWORD=$%s\n' "$BSKY_HANDLE_FIX" "'syn\\001thetic${MARK}'" > "$GITWORK/.env"
|
|
848
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$BSKY_SETUP" verify
|
|
849
|
+
check "bsky-setup verify with a control character in the password: exit 1, exact marker (reason=control_char), curl and jq never invoked" \
|
|
850
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 && "$3" -eq 0 ]]' _ "$RC" "$(curl_calls)" "$(jq_calls)"
|
|
851
|
+
check "bsky-setup verify with a control character in the password: the marker line is exact" refusal_line bsky-setup.sh control_char
|
|
852
|
+
check "bsky-setup verify with a control character in the password: one human line names BSKY_APP_PASSWORD only, value not echoed" \
|
|
853
|
+
bash -c 'grep -qF "Error: BSKY_APP_PASSWORD is empty or contains a control character" <<<"$1" && ! grep -qF "BSKY_HANDLE" <<<"$1" && ! grep -qF "$2" <<<"$1"' _ "$ERR" "$MARK"
|
|
854
|
+
rm -f "$GITWORK/.env"
|
|
855
|
+
|
|
856
|
+
|
|
857
|
+
# ===================================================================================
|
|
858
|
+
# MUTATION rows: each mutant is the real script with ONE literal changed, and each must be
|
|
859
|
+
# caught by the same predicate the rows above use. A mutant whose literal is absent aborts
|
|
860
|
+
# the suite (a mutation that does not land reports the baseline, which reads as a pass).
|
|
861
|
+
# ===================================================================================
|
|
862
|
+
echo "== mutants =="
|
|
863
|
+
MUT="$SANDBOX/mut"
|
|
864
|
+
mkdir -p "$MUT/skills/community/scripts" "$MUT/scripts"
|
|
865
|
+
cp "$HERE/../../../scripts/resolve-git-root.sh" "$MUT/scripts/"
|
|
866
|
+
# mut_make <basename> <from1> <to1> [<from2> <to2> ...] -> $MUT/skills/community/scripts/<basename>
|
|
867
|
+
mut_make() {
|
|
868
|
+
local name="$1" text out="$MUT/skills/community/scripts/$1"; shift
|
|
869
|
+
text="$(cat "$HERE/$name"; printf x)"; text="${text%x}"
|
|
870
|
+
while (( $# >= 2 )); do
|
|
871
|
+
# A mutation that does not land must FAIL the suite: this runs inside `$(...)` at every call site, where a bare
|
|
872
|
+
# `exit` only ends the substitution and the caller would carry on with an empty path (a vacuous pass), so it
|
|
873
|
+
# signals the suite's own shell (the EXIT trap still removes the sandbox) and then ends the substitution.
|
|
874
|
+
if [[ "$text" != *"$1"* ]]; then
|
|
875
|
+
echo "FATAL: mutation did not land in $name: '${1:0:50}...'" >&2
|
|
876
|
+
kill -s TERM "$$" 2>/dev/null
|
|
877
|
+
exit 2
|
|
878
|
+
fi
|
|
879
|
+
text="${text/"$1"/"$2"}"
|
|
880
|
+
shift 2
|
|
881
|
+
done
|
|
882
|
+
printf '%s' "$text" > "$out"
|
|
883
|
+
printf '%s' "$out"
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
# 1. Restore `-H "Authorization: Bot ${TOKEN}"` at the one call site: RED on token-in-argv and on the stdin line.
|
|
887
|
+
M="$(mut_make discord-community.sh \
|
|
888
|
+
'"\n%{http_code}" --config - \' '"\n%{http_code}" -H "Authorization: Bot ${DISCORD_BOT_TOKEN}" \' \
|
|
889
|
+
$'2>/dev/null \\\n < <(printf \'header = "Authorization: Bot %s"\\n\' "$DISCORD_BOT_TOKEN")) ||' '2>/dev/null) ||')"
|
|
890
|
+
run_sut - "" "${DC_ENV[@]}" -- bash "$M" guild-info
|
|
891
|
+
check "mutant: discord-community with the argv header restored is RED (token in argv)" argv_has "$DISC_TOK" 1
|
|
892
|
+
check "mutant: discord-community with the argv header restored is RED (no stdin config line)" not chk_bot_stdin 1 "$DISC_TOK"
|
|
893
|
+
M="$(mut_make discord-setup.sh \
|
|
894
|
+
$' --config -\n -X "$method"' $' -H "Authorization: Bot ${DISCORD_BOT_TOKEN_INPUT}"\n -X "$method"' \
|
|
895
|
+
$'2>/dev/null \\\n < <(printf \'header = "Authorization: Bot %s"\\n\' "$DISCORD_BOT_TOKEN_INPUT"))' '2>/dev/null)')"
|
|
896
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" -- bash "$M" discover-guilds
|
|
897
|
+
check "mutant: discord-setup with the array-held argv header restored is RED (token in argv)" argv_has "$DISC_TOK" 1
|
|
898
|
+
check "mutant: discord-setup with the array-held argv header restored is RED (no stdin config line)" not chk_bot_stdin 1 "$DISC_TOK"
|
|
899
|
+
|
|
900
|
+
# 2. A guard loosened to "two dots" accepts a newline: the newline rows must catch it.
|
|
901
|
+
M="$(mut_make discord-community.sh '[[ "$t" =~ ^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$ ]]' '[[ "$t" == *.*.* ]]')"
|
|
902
|
+
for cls in newline trailing-newline; do
|
|
903
|
+
disc_bad_val "$cls"
|
|
904
|
+
run_sut - "" "DISCORD_BOT_TOKEN=$BAD_VAL" "DISCORD_GUILD_ID=$GUILD" -- bash "$M" guild-info
|
|
905
|
+
check "mutant: a guard that accepts a $cls lets the token reach curl (the refusal rows catch it)" test "$(curl_calls)" -ge 1
|
|
906
|
+
done
|
|
907
|
+
|
|
908
|
+
# 3. Both guards disabled: a malformed token reaches curl (curl_calls != 0 is what the refusal rows assert against).
|
|
909
|
+
M="$(mut_make discord-community.sh \
|
|
910
|
+
'_discord_token_ok "${DISCORD_BOT_TOKEN}" || refuse_token_shape' ':' \
|
|
911
|
+
'if ! _discord_token_ok "${DISCORD_BOT_TOKEN}"; then' 'if false; then')"
|
|
912
|
+
disc_bad_val newline
|
|
913
|
+
run_sut - "" "DISCORD_BOT_TOKEN=$BAD_VAL" "DISCORD_GUILD_ID=$GUILD" -- bash "$M" guild-info
|
|
914
|
+
check "mutant: discord-community with its guards removed invokes curl on a malformed token (RED on the refusal rows)" test "$(curl_calls)" -ge 1
|
|
915
|
+
M="$(mut_make discord-setup.sh '_discord_token_ok "${DISCORD_BOT_TOKEN_INPUT}" || refuse_token_shape' ':')"
|
|
916
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$BAD_VAL" -- bash "$M" validate-token
|
|
917
|
+
check "mutant: discord-setup with its guard removed invokes curl on a malformed token (RED on the refusal rows)" test "$(curl_calls)" -ge 1
|
|
918
|
+
|
|
919
|
+
# 4. The bsky password handed to curl as `-d "$body"`, to jq as `--arg`, or exported; the
|
|
920
|
+
# control-character guard removed; the jq body-build failure no longer fatal.
|
|
921
|
+
M="$(mut_make bsky-community.sh '--data-binary @- \' '-d "{\"identifier\": \"${BSKY_HANDLE}\", \"password\": \"${BSKY_APP_PASSWORD}\"}" \')"
|
|
922
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$M" create-session
|
|
923
|
+
check "mutant: bsky-community handing the password to curl as -d is RED (password in argv)" argv_has "$BSKY_PW_FIX" 1
|
|
924
|
+
check "mutant: bsky-community handing the password to curl as -d is RED (no --data-binary @- stdin body)" not chk_body_ok 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
925
|
+
M="$(mut_make bsky-community.sh \
|
|
926
|
+
$'BSKY_ID="$BSKY_HANDLE" BSKY_PW="$BSKY_APP_PASSWORD" \\\n jq -n \'{identifier: $ENV.BSKY_ID, password: $ENV.BSKY_PW}\'' \
|
|
927
|
+
$'jq -n --arg id "$BSKY_HANDLE" --arg pw "$BSKY_APP_PASSWORD" \'{identifier: $id, password: $pw}\'')"
|
|
928
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$M" create-session
|
|
929
|
+
check "mutant: bsky-community passing the password to jq as --arg is RED (password on jq argv, jq shim)" jq_argv_has "$BSKY_PW_FIX"
|
|
930
|
+
check "mutant: bsky-community passing the password to jq as --arg is RED (the environment row loses its writer)" not jq_writer_env_ok
|
|
931
|
+
check "mutant: bsky-community passing the password to jq as --arg: the curl shim alone stays blind to it" argv_absent "$BSKY_PW_FIX" 1
|
|
932
|
+
# An export at function scope: the curl child AND every later jq child inherit the names.
|
|
933
|
+
M="$(mut_make bsky-community.sh $' local req_body\n' $' local req_body\n export BSKY_ID="$BSKY_HANDLE" BSKY_PW="$BSKY_APP_PASSWORD"\n')"
|
|
934
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$M" create-session
|
|
935
|
+
check "mutant: bsky-community exporting the password is RED (the curl child inherits it)" not curl_env_clean
|
|
936
|
+
check "mutant: bsky-community exporting the password is RED (a later, non-writer jq child inherits it)" not jq_others_env_clean
|
|
937
|
+
M="$(mut_make bsky-community.sh \
|
|
938
|
+
$' _bsky_cred_ok "${BSKY_HANDLE:-}" || bsky_refuse control_char BSKY_HANDLE\n' ':
|
|
939
|
+
' \
|
|
940
|
+
$' _bsky_cred_ok "${BSKY_APP_PASSWORD:-}" || bsky_refuse control_char BSKY_APP_PASSWORD\n' ':
|
|
941
|
+
')"
|
|
942
|
+
run_sut - "" "BSKY_HANDLE=$BAD_HANDLE" "BSKY_APP_PASSWORD=$BSKY_PW_FIX" -- bash "$M" create-session
|
|
943
|
+
check "mutant: bsky-community with its guard removed invokes curl on a control-character handle (RED on the refusal rows)" test "$(curl_calls)" -ge 1
|
|
944
|
+
# The jq body-build failure no longer fatal: the empty body is POSTed and the session reads as good (RED on the jq-failure rows).
|
|
945
|
+
M="$(mut_make bsky-community.sh "password: \$ENV.BSKY_PW}') || {" "password: \$ENV.BSKY_PW}') || true; false && {")"
|
|
946
|
+
run_sut - "" "${BC_ENV[@]}" SHIM_MODE=jqfail -- bash "$M" create-session
|
|
947
|
+
check "mutant: bsky-community ignoring the jq failure reads a failed body build as a good POST (RC 0, no body-build message)" \
|
|
948
|
+
bash -c '[[ "$1" -eq 0 ]] && ! grep -qF "could not build" <<<"$2"' _ "$RC" "$ERR"
|
|
949
|
+
# Independence from pipefail: the classification is a command-substitution status, not a pipeline status.
|
|
950
|
+
M="$(mut_make bsky-community.sh 'set -euo pipefail' 'set -eu')"
|
|
951
|
+
run_sut - "" "${BC_ENV[@]}" SHIM_MODE=jqfail -- bash "$M" create-session
|
|
952
|
+
bsky_jqfail_rows "bsky-community WITHOUT pipefail"
|
|
953
|
+
# The bearer guards: with BOTH removed a hostile server-supplied accessJwt reaches the stdin config.
|
|
954
|
+
M="$(mut_make bsky-community.sh \
|
|
955
|
+
$' if ! _bearer_ok "$ACCESS_JWT"; then\n ACCESS_JWT=""' $' if false; then\n ACCESS_JWT=""' \
|
|
956
|
+
' _bearer_ok "$ACCESS_JWT" || return 120' ' :')"
|
|
957
|
+
run_sut - "" "${BC_ENV[@]}" SHIM_MODE=badjwt -- bash "$M" get-metrics
|
|
958
|
+
check "mutant: bsky-community with the bearer guards removed sends the hostile token (a getProfile call follows; RED on the refusal rows)" test "$(curl_calls)" -ge 2
|
|
959
|
+
check "mutant: bsky-community with the bearer guards removed: the stdin config carries an injected extra line" test "$(stdin_lines 2)" -gt 1
|
|
960
|
+
M="$(mut_make bsky-setup.sh '--data-binary @- \' '-d "{\"identifier\": \"${BSKY_HANDLE}\", \"password\": \"${BSKY_APP_PASSWORD}\"}" \')"
|
|
961
|
+
printf 'BSKY_HANDLE=%s\nBSKY_APP_PASSWORD=%s\n' "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX" > "$GITWORK/.env"
|
|
962
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$M" verify
|
|
963
|
+
check "mutant: bsky-setup handing the password to curl as -d is RED (password in argv)" argv_has "$BSKY_PW_FIX" 1
|
|
964
|
+
check "mutant: bsky-setup handing the password to curl as -d is RED (no --data-binary @- stdin body)" not chk_body_ok 1 "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX"
|
|
965
|
+
M="$(mut_make bsky-setup.sh \
|
|
966
|
+
$' _bsky_cred_ok "${BSKY_HANDLE:-}" || bsky_refuse control_char BSKY_HANDLE\n' ':
|
|
967
|
+
' \
|
|
968
|
+
$' _bsky_cred_ok "${BSKY_APP_PASSWORD:-}" || bsky_refuse control_char BSKY_APP_PASSWORD\n' ':
|
|
969
|
+
')"
|
|
970
|
+
printf 'BSKY_HANDLE=$%s\nBSKY_APP_PASSWORD=%s\n' "'syn\\001thetic${MARK}'" "$BSKY_PW_FIX" > "$GITWORK/.env"
|
|
971
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$M" verify
|
|
972
|
+
check "mutant: bsky-setup with its guard removed invokes curl on a control-character handle (RED on the refusal rows)" test "$(curl_calls)" -ge 1
|
|
973
|
+
printf 'BSKY_HANDLE=%s\nBSKY_APP_PASSWORD=%s\n' "$BSKY_HANDLE_FIX" "$BSKY_PW_FIX" > "$GITWORK/.env"
|
|
974
|
+
M="$(mut_make bsky-setup.sh "password: \$ENV.BSKY_PW}') || {" "password: \$ENV.BSKY_PW}') || true; false && {")"
|
|
975
|
+
RUN_CWD="$GITWORK" run_sut - "" SHIM_MODE=jqfail -- bash "$M" verify
|
|
976
|
+
check "mutant: bsky-setup ignoring the jq failure reads a failed body build as a good POST (RC 0, no body-build message)" \
|
|
977
|
+
bash -c '[[ "$1" -eq 0 ]] && ! grep -qF "could not build" <<<"$2"' _ "$RC" "$ERR"
|
|
978
|
+
M="$(mut_make bsky-setup.sh 'set -euo pipefail' 'set -eu')"
|
|
979
|
+
RUN_CWD="$GITWORK" run_sut - "" SHIM_MODE=jqfail -- bash "$M" verify
|
|
980
|
+
bsky_jqfail_rows "bsky-setup verify WITHOUT pipefail"
|
|
981
|
+
M="$(mut_make bsky-setup.sh ' if ! _bearer_ok "$access_jwt"; then' ' if false; then')"
|
|
982
|
+
RUN_CWD="$GITWORK" run_sut - "" SHIM_MODE=badjwt -- bash "$M" verify
|
|
983
|
+
check "mutant: bsky-setup with the bearer guard removed sends the hostile token (a getProfile call follows; RED on the refusal rows)" test "$(curl_calls)" -ge 2
|
|
984
|
+
check "mutant: bsky-setup with the bearer guard removed: the stdin config carries an injected extra line" test "$(stdin_lines 2)" -gt 1
|
|
985
|
+
M="$(mut_make bsky-setup.sh ' export -n BSKY_HANDLE BSKY_APP_PASSWORD' ' :')"
|
|
986
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$M" verify
|
|
987
|
+
check "mutant: bsky-setup without the un-export hands the .env credentials to curl (RED on the curl-environment row)" not curl_env_lacks BSKY_HANDLE BSKY_APP_PASSWORD
|
|
988
|
+
rm -f "$GITWORK/.env"
|
|
989
|
+
|
|
990
|
+
# 5. discord-setup transport parity: the prologue unset and the TLS-trust arm, each removed in turn.
|
|
991
|
+
M="$(mut_make discord-setup.sh $'unset SSLKEYLOGFILE CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR CURL_HOME \\\n HOSTALIASES LOCALDOMAIN RES_OPTIONS\n' $':\n')"
|
|
992
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" "${TLS_ENV[@]}" -- bash "$M" discover-guilds
|
|
993
|
+
check "mutant: discord-setup without the prologue unset lets CURL_CA_BUNDLE etc. reach curl (RED on the curl-environment row)" \
|
|
994
|
+
not curl_env_lacks CURL_CA_BUNDLE SSL_CERT_FILE SSL_CERT_DIR SSLKEYLOGFILE CURL_HOME
|
|
995
|
+
M="$(mut_make discord-setup.sh '"$curl_exit" == "60" ||' '"$curl_exit" == "99" ||')"
|
|
996
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" SHIM_MODE=fail60 -- bash "$M" validate-token
|
|
997
|
+
check "mutant: discord-setup without the TLS-trust arm reports exit 60 as a bare network failure (RED on the exit-60 row)" \
|
|
998
|
+
bash -c '! grep -qF "TLS trust failure" <<<"$1" && grep -qF "Check your network connection" <<<"$1"' _ "$ERR"
|
|
999
|
+
|
|
1000
|
+
# ===================================================================================
|
|
1001
|
+
# Negative controls: every helper the rows above trust must be able to say NO. Each control feeds
|
|
1002
|
+
# a helper an input that makes its condition false (a shim or a one-literal SUT mutant) and
|
|
1003
|
+
# requires the helper to flip, so a helper that quietly became vacuous turns this section RED.
|
|
1004
|
+
# ===================================================================================
|
|
1005
|
+
echo "== helper negative controls =="
|
|
1006
|
+
# no_leak: a refusal that echoes the value
|
|
1007
|
+
M="$(mut_make discord-setup.sh 'The value is not shown." >&2' 'The value is ${DISCORD_BOT_TOKEN_INPUT}." >&2')"
|
|
1008
|
+
disc_bad_val newline
|
|
1009
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$BAD_VAL" -- bash "$M" validate-token
|
|
1010
|
+
check "negctl: no_leak is false when the refusal echoes the value" not no_leak
|
|
1011
|
+
# no_diag: a real transport failure prints the diagnostic marker
|
|
1012
|
+
RUN_CWD="$GITWORK" run_sut - "" "${DS_ENV[@]}" SHIM_MODE=fail7 -- bash "$DISCORD_SETUP" validate-token
|
|
1013
|
+
check "negctl: no_diag is false when the SOLEUR_TRANSPORT_DIAG line is printed" not no_diag
|
|
1014
|
+
# refusal_line: a refusal that omits the marker, and a marker with the wrong reason / script
|
|
1015
|
+
M="$(mut_make discord-setup.sh $' printf \'SOLEUR_CREDENTIAL_REFUSED script=%s reason=token_shape\\n\' "$SOLEUR_TRANSPORT_SCRIPT" >&2\n' $' :\n')"
|
|
1016
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$BAD_VAL" -- bash "$M" validate-token
|
|
1017
|
+
check "negctl: refusal_line is false when the marker line is omitted" not refusal_line discord-setup.sh
|
|
1018
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$BAD_VAL" -- bash "$DISCORD_SETUP" validate-token
|
|
1019
|
+
check "negctl: refusal_line is false for a different reason" not refusal_line discord-setup.sh control_char
|
|
1020
|
+
check "negctl: refusal_line is false for a different script name" not refusal_line bsky-setup.sh
|
|
1021
|
+
check "negctl: refusal_line is true for the real marker (control)" refusal_line discord-setup.sh
|
|
1022
|
+
# jq_others_env_clean: the function-scope export mutant (above) is the dirty input
|
|
1023
|
+
M="$(mut_make bsky-community.sh $' local req_body\n' $' local req_body\n export BSKY_ID="$BSKY_HANDLE" BSKY_PW="$BSKY_APP_PASSWORD"\n')"
|
|
1024
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$M" create-session
|
|
1025
|
+
check "negctl: jq_others_env_clean is false when a non-writer jq child inherits the names" not jq_others_env_clean
|
|
1026
|
+
# stdin_lines: a two-line stdin config
|
|
1027
|
+
M="$(mut_make discord-community.sh $'printf \'header = "Authorization: Bot %s"\\n\' "$DISCORD_BOT_TOKEN")' $'printf \'header = "Authorization: Bot %s"\\nheader = "X-Extra: 1"\\n\' "$DISCORD_BOT_TOKEN")')"
|
|
1028
|
+
run_sut - "" "${DC_ENV[@]}" -- bash "$M" guild-info
|
|
1029
|
+
check "negctl: stdin_lines counts two lines for a two-line config" test "$(stdin_lines 1)" -eq 2
|
|
1030
|
+
check "negctl: the one-line-config row would be RED for a two-line config" not test "$(stdin_lines 1)" -eq 1
|
|
1031
|
+
# argv_first3: a call that drops --disable
|
|
1032
|
+
M="$(mut_make discord-community.sh "curl --disable --noproxy '*' -s -w" "curl --noproxy '*' -s -w")"
|
|
1033
|
+
run_sut - "" "${DC_ENV[@]}" -- bash "$M" guild-info
|
|
1034
|
+
check "negctl: argv_first3 no longer reads '--disable --noproxy *' when --disable is dropped" not test "$(argv_first3 1)" = "--disable --noproxy *"
|
|
1035
|
+
# argv_absent / jq_argv_absent: FALSE when nothing ran (a refused run records no argv)
|
|
1036
|
+
RUN_CWD="$GITWORK" run_sut - "" "DISCORD_BOT_TOKEN_INPUT=$BAD_VAL" -- bash "$DISCORD_SETUP" validate-token
|
|
1037
|
+
check "negctl: argv_absent is false when curl never ran (no argv file)" not argv_absent "$DISC_TOK"
|
|
1038
|
+
check "negctl: jq_argv_absent is false when jq never ran (no jq argv file)" not jq_argv_absent "$BSKY_PW_FIX"
|
|
1039
|
+
check "negctl: curl_env_lacks is false when curl never ran" not curl_env_lacks CURL_CA_BUNDLE
|
|
1040
|
+
|
|
1041
|
+
# rc1_refusal: the marker is on stderr but the exit code is NOT 1 (a refusal that exits 0 or 3 must not read as one)
|
|
1042
|
+
M="$(mut_make discord-community.sh $'The value is not shown." >&2\n exit 1' $'The value is not shown." >&2\n exit 0')"
|
|
1043
|
+
disc_bad_val newline
|
|
1044
|
+
run_sut - "" "DISCORD_BOT_TOKEN=$BAD_VAL" "DISCORD_GUILD_ID=$GUILD" -- bash "$M" guild-info
|
|
1045
|
+
check "negctl: rc1_refusal is false when the marker is present but the refusal exits 0 (discord-community 1->0)" \
|
|
1046
|
+
bash -c '[[ "$1" -eq 0 ]] && grep -qxF "SOLEUR_CREDENTIAL_REFUSED script=discord-community.sh reason=token_shape" <<<"$2"' _ "$RC" "$ERR"
|
|
1047
|
+
check "negctl: rc1_refusal is false for that exit-0 refusal" not rc1_refusal discord-community.sh
|
|
1048
|
+
M="$(mut_make bsky-community.sh $'The value is not shown." >&2\n exit 1' $'The value is not shown." >&2\n exit 3')"
|
|
1049
|
+
run_sut - "" "BSKY_HANDLE=$BAD_HANDLE" "BSKY_APP_PASSWORD=$BSKY_PW_FIX" -- bash "$M" create-session
|
|
1050
|
+
check "negctl: the bsky exit-3 refusal mutant keeps the marker and returns 3" \
|
|
1051
|
+
bash -c '[[ "$1" -eq 3 ]] && grep -qxF "SOLEUR_CREDENTIAL_REFUSED script=bsky-community.sh reason=control_char" <<<"$2"' _ "$RC" "$ERR"
|
|
1052
|
+
check "negctl: rc1_refusal is false when the marker is present but the refusal exits 3 (bsky 1->3)" not rc1_refusal bsky-community.sh control_char
|
|
1053
|
+
run_sut - "" "BSKY_HANDLE=$BAD_HANDLE" "BSKY_APP_PASSWORD=$BSKY_PW_FIX" -- bash "$BSKY_COMMUNITY" create-session
|
|
1054
|
+
check "negctl: rc1_refusal is true for the real refusal (control)" rc1_refusal bsky-community.sh control_char
|
|
1055
|
+
# bsky_field_line: the human line names the wrong field, or names the other field too
|
|
1056
|
+
check "negctl: bsky_field_line is true when the line names the refused field only (control)" bsky_field_line BSKY_HANDLE BSKY_APP_PASSWORD
|
|
1057
|
+
check "negctl: bsky_field_line is false when the line names a different field than the one expected" not bsky_field_line BSKY_APP_PASSWORD BSKY_HANDLE
|
|
1058
|
+
check "negctl: bsky_field_line is false when the other field also appears on stderr" not bsky_field_line BSKY_HANDLE BSKY_HANDLE
|
|
1059
|
+
# jq_recording_ok / jq_argv_has: jq never ran (a refused run), and a run whose jq argv lacks the value
|
|
1060
|
+
check "negctl: jq_recording_ok is false when jq never ran (refused credential, nothing recorded)" not jq_recording_ok
|
|
1061
|
+
check "negctl: jq_argv_has is false when jq never ran" not jq_argv_has "$BSKY_HANDLE_FIX"
|
|
1062
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$BSKY_COMMUNITY" create-session
|
|
1063
|
+
check "negctl: jq_recording_ok is true for a real createSession run (control)" jq_recording_ok
|
|
1064
|
+
check "negctl: jq_argv_has is false for a value that is not on any jq argv (the password, in a real run)" not jq_argv_has "$BSKY_PW_FIX"
|
|
1065
|
+
M="$(mut_make bsky-community.sh \
|
|
1066
|
+
$'BSKY_ID="$BSKY_HANDLE" BSKY_PW="$BSKY_APP_PASSWORD" \\\n jq -n \'{identifier: $ENV.BSKY_ID, password: $ENV.BSKY_PW}\'' \
|
|
1067
|
+
$'jq -n --arg id "$BSKY_HANDLE" --arg pw "$BSKY_APP_PASSWORD" \'{identifier: $id, password: $pw}\'')"
|
|
1068
|
+
run_sut - "" "${BC_ENV[@]}" -- bash "$M" create-session
|
|
1069
|
+
check "negctl: jq_recording_ok is false when no jq call reads \$ENV.BSKY_PW (the --arg mutant)" not jq_recording_ok
|
|
1070
|
+
|
|
1071
|
+
# --- transport-block drift guard ------------------------------------------------------------
|
|
1072
|
+
# discord-setup.sh carries a verbatim copy of discord-community.sh's transport block. Extract both
|
|
1073
|
+
# (from the `unset SSLKEYLOGFILE ...` line through the closing brace of report_transport_failure)
|
|
1074
|
+
# and require byte equality, normalising ONLY the two per-script assignments.
|
|
1075
|
+
echo "== transport-block drift =="
|
|
1076
|
+
transport_block() { # <file>
|
|
1077
|
+
awk '/^unset SSLKEYLOGFILE /{p=1} p{print} p && /^report_transport_failure\(\) \{/{r=1} r && /^}$/{exit}' "$1" |
|
|
1078
|
+
sed -e 's/^SOLEUR_TRANSPORT_SCRIPT=.*/SOLEUR_TRANSPORT_SCRIPT=<normalised>/' \
|
|
1079
|
+
-e 's/^SOLEUR_TRANSPORT_PLATFORM=.*/SOLEUR_TRANSPORT_PLATFORM=<normalised>/'
|
|
1080
|
+
}
|
|
1081
|
+
DRIFT="$SANDBOX/drift"
|
|
1082
|
+
mkdir -p "$DRIFT"
|
|
1083
|
+
transport_block "$DISCORD_COMMUNITY" > "$DRIFT/community.blk"
|
|
1084
|
+
transport_block "$DISCORD_SETUP" > "$DRIFT/setup.blk"
|
|
1085
|
+
blk_sane() { # <block-file>: non-empty, spans the whole block, and is long enough not to be a truncated extraction
|
|
1086
|
+
[[ -s "$1" ]] && grep -qxF 'report_transport_failure() {' "$1" && grep -qF 'echo "Check your network connection and try again." >&2' "$1" \
|
|
1087
|
+
&& [[ "$(wc -l < "$1")" -ge 80 ]]
|
|
1088
|
+
}
|
|
1089
|
+
check "drift: the discord-community.sh transport block extracted whole (non-vacuous)" blk_sane "$DRIFT/community.blk"
|
|
1090
|
+
check "drift: the discord-setup.sh transport block extracted whole (non-vacuous)" blk_sane "$DRIFT/setup.blk"
|
|
1091
|
+
check "drift: the discord-setup.sh transport block is byte-identical to discord-community.sh's (bar the two per-script literals)" \
|
|
1092
|
+
cmp -s "$DRIFT/community.blk" "$DRIFT/setup.blk"
|
|
1093
|
+
# Mutation: one character changed in discord-setup's copy of the block must turn the comparison RED.
|
|
1094
|
+
sed 's/eight fields, on the saved stdout/eight fieldz, on the saved stdout/' "$DISCORD_SETUP" > "$DRIFT/setup-mut.sh"
|
|
1095
|
+
check "drift: the one-character mutation landed in the sandbox copy" not cmp -s "$DISCORD_SETUP" "$DRIFT/setup-mut.sh"
|
|
1096
|
+
transport_block "$DRIFT/setup-mut.sh" > "$DRIFT/setup-mut.blk"
|
|
1097
|
+
check "drift: a one-character edit to discord-setup's copy makes the comparison RED" not cmp -s "$DRIFT/community.blk" "$DRIFT/setup-mut.blk"
|
|
1098
|
+
# Normalisation is limited to the two literals: changing them stays GREEN, changing a third literal does not.
|
|
1099
|
+
sed -e 's/^SOLEUR_TRANSPORT_SCRIPT=.*/SOLEUR_TRANSPORT_SCRIPT="other.sh"/' -e 's/^SOLEUR_TRANSPORT_PLATFORM=.*/SOLEUR_TRANSPORT_PLATFORM="Other"/' "$DISCORD_SETUP" > "$DRIFT/setup-lit.sh"
|
|
1100
|
+
transport_block "$DRIFT/setup-lit.sh" > "$DRIFT/setup-lit.blk"
|
|
1101
|
+
check "drift: the two per-script literals are the only normalised difference" cmp -s "$DRIFT/community.blk" "$DRIFT/setup-lit.blk"
|
|
1102
|
+
|
|
1103
|
+
# ===================================================================================
|
|
1104
|
+
# S2 (#9597): the OAuth 1.0a signing key off every argument (lib/hmac-sha1-b64.sh) and the
|
|
1105
|
+
# write-env value allow-list. All keys and values below are SYNTHESIZED.
|
|
1106
|
+
# ===================================================================================
|
|
1107
|
+
# The mutant tree (the "== mutants ==" section above) needs the library beside the mutated scripts.
|
|
1108
|
+
mkdir -p "$MUT/skills/community/scripts/lib"
|
|
1109
|
+
[[ -r "$HMAC_LIB" ]] && cp "$HMAC_LIB" "$MUT/skills/community/scripts/lib/"
|
|
1110
|
+
echo "== hmac-sha1-b64.sh: static contract =="
|
|
1111
|
+
LIB_CODE="$SANDBOX/hmac-lib.code"
|
|
1112
|
+
if [[ -r "$HMAC_LIB" ]]; then sed -e '/^[[:space:]]*#/d' "$HMAC_LIB" > "$LIB_CODE"; else : > "$LIB_CODE"; fi
|
|
1113
|
+
# The function body alone (code lines only): the load-time prologue legitimately has a fallback exit.
|
|
1114
|
+
LIB_FN="$SANDBOX/hmac-lib.fn"
|
|
1115
|
+
awk '/^hmac_sha1_b64\(\) \{/{f=1} f{print} f&&/^}/{exit}' "$LIB_CODE" > "$LIB_FN"
|
|
1116
|
+
lib_fn_has() { local n; n="$(grep -c -e "$1" "$LIB_FN")"; [[ "$n" =~ ^[0-9]+$ && "$n" -ge 1 ]]; }
|
|
1117
|
+
lib_fn_zero_exit() { local n rc; n="$(grep -cE '(^|[^A-Za-z_])exit([^A-Za-z_]|$)' "$LIB_FN")"; rc=$?; [[ "$rc" -le 1 && "$n" == 0 ]]; }
|
|
1118
|
+
# lib_zero <ERE>: the pattern compiles (grep rc <= 1) AND matches no CODE line (comments stripped).
|
|
1119
|
+
lib_zero() { local n rc; n="$(grep -cE -e "$1" "$LIB_CODE")"; rc=$?; [[ "$rc" -le 1 && "$n" == 0 ]]; }
|
|
1120
|
+
# lib_ones <ERE>: the pattern compiles AND matches exactly one code line.
|
|
1121
|
+
lib_ones() { local n rc; n="$(grep -cE -e "$1" "$LIB_CODE")"; rc=$?; [[ "$rc" -le 1 && "$n" == 1 ]]; }
|
|
1122
|
+
# lib_some <ERE>: the pattern compiles AND matches at least one code line.
|
|
1123
|
+
lib_some() { local n rc; n="$(grep -cE -e "$1" "$LIB_CODE")"; rc=$?; [[ "$rc" -le 1 && "$n" =~ ^[0-9]+$ && "$n" -ge 1 ]]; }
|
|
1124
|
+
# file_count <fixed-string> <file>: occurrences (lines) in the whole file, comments included.
|
|
1125
|
+
file_count() { grep -cF -e "$1" -- "$2"; }
|
|
1126
|
+
|
|
1127
|
+
check "lib: lib/hmac-sha1-b64.sh exists and is readable" test -r "$HMAC_LIB"
|
|
1128
|
+
check "lib: defines exactly one function, hmac_sha1_b64" lib_ones '^hmac_sha1_b64\(\) \{'
|
|
1129
|
+
check "lib: the file defines no other function (one function only)" test "$(grep -cE '^[A-Za-z_][A-Za-z0-9_]*\(\) *\{' "$LIB_CODE")" -eq 1
|
|
1130
|
+
check "lib: no here-string or heredoc on any code line (they write key-derived data to a temp file on bash < 5.1)" lib_zero '<<'
|
|
1131
|
+
lib_fn_ctl() { lib_fn_has "return 1" && lib_fn_has "openssl dgst -sha1 -binary"; }
|
|
1132
|
+
check "lib: the function body is extracted (control: it holds a return and the openssl pipe)" lib_fn_ctl
|
|
1133
|
+
check "lib: no exit in the function body (a sourced function returns non-zero, the caller decides)" lib_fn_zero_exit
|
|
1134
|
+
check "lib: the load-time xtrace refusal is the FIRST code line of the file (the lint's prologue rule)" test "$(awk 'NF{print; exit}' "$LIB_CODE")" = 'case "$-" in'
|
|
1135
|
+
check "lib: no -hmac or -macopt operand anywhere in the code" lib_zero '(-hmac|-macopt)'
|
|
1136
|
+
check "lib: LC_ALL=C is set (byte semantics)" lib_some 'LC_ALL=C'
|
|
1137
|
+
check "lib: carries its own xtrace refusal (it can be sourced without the caller's preamble)" lib_some 'case "\$-" in'
|
|
1138
|
+
check "lib: never prints the credential-refusal marker (the caller decides the exit code and prints)" \
|
|
1139
|
+
test "$(file_count SOLEUR_CREDENTIAL_REFUSED "$HMAC_LIB" 2>/dev/null)" = 0
|
|
1140
|
+
check "lib: static-check controls: a pattern that must match does match (the extractor is not empty)" lib_some 'hmac_sha1_b64'
|
|
1141
|
+
check "lib: static-check controls: lib_zero says NO for a present token" not lib_zero 'hmac_sha1_b64'
|
|
1142
|
+
for f_ in "$X_COMMUNITY" "$X_SETUP"; do
|
|
1143
|
+
check "$(basename "$f_"): sources lib/hmac-sha1-b64.sh exactly once" test "$(file_count 'lib/hmac-sha1-b64.sh' "$f_")" -eq 1
|
|
1144
|
+
check "$(basename "$f_"): no -hmac operand remains" test "$(file_count '-hmac' "$f_")" = 0
|
|
1145
|
+
check "$(basename "$f_"): the signing key is passed to the function by NAME (never an expansion)" \
|
|
1146
|
+
test "$(file_count 'hmac_sha1_b64 signing_key' "$f_")" -eq 1
|
|
1147
|
+
done
|
|
1148
|
+
|
|
1149
|
+
echo "== hmac-sha1-b64.sh: oracle against openssl dgst -hmac =="
|
|
1150
|
+
oracle_matrix
|
|
1151
|
+
check "oracle: the key-length x message matrix ran exactly 32 cases (inner counter)" test "$ORC_LEN" -eq 32
|
|
1152
|
+
check "oracle: the special-key matrix ran exactly 28 cases (inner counter)" test "$ORC_SPEC" -eq 28
|
|
1153
|
+
check "oracle: all 7 RFC 2202 vectors ran (inner counter)" test "$ORC_RFC" -eq 7
|
|
1154
|
+
check "oracle: 67 comparisons in total (inner counter)" test "$ORC_RAN" -eq 67
|
|
1155
|
+
check "oracle: every comparison equals the reference (keys 0,1,20,63,64,65,96,200 bytes; 6x64; backslash, quote, space; = and %; RFC 2202)" test "$ORC_BAD" -eq 0
|
|
1156
|
+
# Comparator negative controls: the comparison can say NO, and can say YES.
|
|
1157
|
+
CTL_NO="$( hmac_sha1_b64() { cat >/dev/null; printf 'AAAAAAAAAAAAAAAAAAAAAAAAAAA='; }; ORC_BAD=0; orc_cmp "ctl-key" "ctl-msg"; printf '%s' "$ORC_BAD" )"
|
|
1158
|
+
CTL_YES="$( hmac_sha1_b64() { local k="${!1}"; openssl dgst -sha1 -hmac "$k" -binary | base64; }; ORC_BAD=0; orc_cmp "ctl-key" "ctl-msg"; printf '%s' "$ORC_BAD" )"
|
|
1159
|
+
check "negctl: the oracle comparison counts a wrong digest as a mismatch" test "$CTL_NO" = 1
|
|
1160
|
+
check "negctl: the oracle comparison counts the reference digest as a match" test "$CTL_YES" = 0
|
|
1161
|
+
# Mutants of the library: each must turn the oracle RED.
|
|
1162
|
+
mut_lib() { # <tag> <from> <to> -> path of the mutant library ('' when the literal is absent)
|
|
1163
|
+
local text out="$SANDBOX/mutlib.$1.sh"
|
|
1164
|
+
[[ -r "$HMAC_LIB" ]] || return 0
|
|
1165
|
+
text="$(cat "$HMAC_LIB"; printf x)"; text="${text%x}"
|
|
1166
|
+
if [[ "$text" != *"$2"* ]]; then echo "FATAL: lib mutation did not land: '${2:0:50}'" >&2; return 0; fi
|
|
1167
|
+
printf '%s' "${text/"$2"/"$3"}" > "$out"
|
|
1168
|
+
printf '%s' "$out"
|
|
1169
|
+
}
|
|
1170
|
+
# shellcheck disable=SC1090
|
|
1171
|
+
orc_bad_of() { ( source "$1" 2>/dev/null && oracle_matrix && printf '%s' "$ORC_BAD" ) || true; }
|
|
1172
|
+
M_LIB="$(mut_lib noprehash '> 128 ' '> 12800 ')"
|
|
1173
|
+
check "mutant: a library that never pre-hashes a key longer than 64 bytes is RED on the oracle (65, 96, 200 byte keys and RFC 2202 cases 6-7)" \
|
|
1174
|
+
test "$(orc_bad_of "$M_LIB")" -ge 1
|
|
1175
|
+
M_LIB="$(mut_lib ipad '^ 54' '^ 55')"
|
|
1176
|
+
check "mutant: a library with a wrong inner pad is RED on the oracle" test "$(orc_bad_of "$M_LIB")" -ge 1
|
|
1177
|
+
M_LIB="$(mut_lib opad '^ 92' '^ 93')"
|
|
1178
|
+
check "mutant: a library with a wrong outer pad is RED on the oracle" test "$(orc_bad_of "$M_LIB")" -ge 1
|
|
1179
|
+
|
|
1180
|
+
echo "== hmac-sha1-b64.sh: argv and environment hygiene (recording openssl/od/tr/base64 shims) =="
|
|
1181
|
+
# The shims record argv and the NAMES of the variables the child inherited, then exec the REAL binary
|
|
1182
|
+
# (a fake that returned a canned digest would agree with the consumer's own reading of the contract).
|
|
1183
|
+
REC="$SANDBOX/rec"
|
|
1184
|
+
mkdir -p "$REC"
|
|
1185
|
+
for t_ in openssl od tr base64; do
|
|
1186
|
+
rp_="$(command -v "$t_")" || { echo "FAIL: $t_ not found" >&2; exit 1; }
|
|
1187
|
+
cat > "$REC/$t_" <<'SHIM'
|
|
1188
|
+
#!/bin/bash
|
|
1189
|
+
d="$MOCK_DIR"; b="${0##*/}"
|
|
1190
|
+
printf '%s\0' "$@" > "$d/rec.$b.$$"
|
|
1191
|
+
compgen -e > "$d/recenv.$b.$$"
|
|
1192
|
+
exec "@REAL@" "$@"
|
|
1193
|
+
SHIM
|
|
1194
|
+
sed -i "s|@REAL@|$rp_|" "$REC/$t_"
|
|
1195
|
+
chmod +x "$REC/$t_"
|
|
1196
|
+
done
|
|
1197
|
+
rec_clear() { rm -f "$MOCK"/rec.* "$MOCK"/recenv.*; }
|
|
1198
|
+
rec_count() { local n=0 f; for f in "$MOCK"/rec."$1".*; do [[ -e "$f" ]] && n=$((n + 1)); done; printf '%s' "$n"; }
|
|
1199
|
+
# rec_argv_absent <needle>: some tool was recorded AND the needle is on no recorded argv.
|
|
1200
|
+
rec_argv_absent() {
|
|
1201
|
+
local f seen=0
|
|
1202
|
+
for f in "$MOCK"/rec.*; do
|
|
1203
|
+
[[ -e "$f" ]] || continue
|
|
1204
|
+
seen=$((seen + 1))
|
|
1205
|
+
grep -aqF -e "$1" -- "$f" && return 1
|
|
1206
|
+
done
|
|
1207
|
+
(( seen >= 1 ))
|
|
1208
|
+
}
|
|
1209
|
+
# rec_env_clean <name|prefix*>...: some tool was recorded AND none of these names is in any recorded environment.
|
|
1210
|
+
rec_env_clean() {
|
|
1211
|
+
local f n seen=0
|
|
1212
|
+
for f in "$MOCK"/recenv.*; do
|
|
1213
|
+
[[ -e "$f" ]] || continue
|
|
1214
|
+
seen=$((seen + 1))
|
|
1215
|
+
for n in "$@"; do
|
|
1216
|
+
if [[ "$n" == *'*' ]]; then
|
|
1217
|
+
grep -q -e "^${n%\*}" -- "$f" && return 1
|
|
1218
|
+
else
|
|
1219
|
+
grep -qxF -e "$n" -- "$f" && return 1
|
|
1220
|
+
fi
|
|
1221
|
+
done
|
|
1222
|
+
done
|
|
1223
|
+
(( seen >= 1 ))
|
|
1224
|
+
}
|
|
1225
|
+
# rec_env_leaks <name|prefix*>...: at least one of these names IS in some recorded environment (the mutant rows).
|
|
1226
|
+
rec_env_leaks() {
|
|
1227
|
+
local f n
|
|
1228
|
+
for f in "$MOCK"/recenv.*; do
|
|
1229
|
+
[[ -e "$f" ]] || continue
|
|
1230
|
+
for n in "$@"; do
|
|
1231
|
+
if [[ "$n" == *'*' ]]; then
|
|
1232
|
+
grep -q -e "^${n%\*}" -- "$f" && return 0
|
|
1233
|
+
else
|
|
1234
|
+
grep -qxF -e "$n" -- "$f" && return 0
|
|
1235
|
+
fi
|
|
1236
|
+
done
|
|
1237
|
+
done
|
|
1238
|
+
return 1
|
|
1239
|
+
}
|
|
1240
|
+
# rec_argv_has <needle>: the needle IS on some recorded argv (the mutant rows).
|
|
1241
|
+
rec_argv_has() {
|
|
1242
|
+
local f
|
|
1243
|
+
for f in "$MOCK"/rec.*; do
|
|
1244
|
+
[[ -e "$f" ]] || continue
|
|
1245
|
+
grep -aqF -e "$1" -- "$f" && return 0
|
|
1246
|
+
done
|
|
1247
|
+
return 1
|
|
1248
|
+
}
|
|
1249
|
+
# rec_argv_absent_all <needle>...: every needle is absent (and something was recorded).
|
|
1250
|
+
rec_argv_absent_all() { local n; for n in "$@"; do rec_argv_absent "$n" || return 1; done; }
|
|
1251
|
+
# rec_dgst_exact <n>: exactly n openssl calls with the argv `dgst -sha1 -binary`; every other openssl call is `rand -hex 16`.
|
|
1252
|
+
rec_dgst_exact() {
|
|
1253
|
+
local f n=0 a
|
|
1254
|
+
for f in "$MOCK"/rec.openssl.*; do
|
|
1255
|
+
[[ -e "$f" ]] || continue
|
|
1256
|
+
a="$(tr '\0' ' ' < "$f")"
|
|
1257
|
+
case "$a" in
|
|
1258
|
+
'dgst -sha1 -binary ') n=$((n + 1)) ;;
|
|
1259
|
+
'rand -hex 16 ') ;;
|
|
1260
|
+
*) return 1 ;;
|
|
1261
|
+
esac
|
|
1262
|
+
done
|
|
1263
|
+
[[ "$n" -eq "$1" ]]
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
# --- the function under `set -a`: no key-derived name reaches any child's environment ---------
|
|
1267
|
+
SETA_KEY="$(oc_rep 20 'k3y&=%')"
|
|
1268
|
+
SETA_MSG='GET&https%3A%2F%2Fapi.x.com%2F2%2Fusers%2Fme&a%3Db'
|
|
1269
|
+
SETA_WANT="$(printf '%s' "$SETA_MSG" | openssl dgst -sha1 -hmac "$SETA_KEY" -binary | base64)"
|
|
1270
|
+
SETA_OUT="" SETA_RC=0
|
|
1271
|
+
seta_run() { # <lib-file>
|
|
1272
|
+
rec_clear
|
|
1273
|
+
# shellcheck disable=SC1090
|
|
1274
|
+
SETA_OUT="$( set -a; export MOCK_DIR="$MOCK"; PATH="$REC:$PATH"; source "$1"; HM_KEY="$SETA_KEY"; printf '%s' "$SETA_MSG" | hmac_sha1_b64 HM_KEY )"
|
|
1275
|
+
SETA_RC=$?
|
|
1276
|
+
}
|
|
1277
|
+
seta_run "$HMAC_LIB"
|
|
1278
|
+
check "set -a: the digest still equals the openssl reference" bash -c '[[ -n "$1" && "$1" == "$2" ]]' _ "$SETA_OUT" "$SETA_WANT"
|
|
1279
|
+
check "set -a: exit 0" test "$SETA_RC" -eq 0
|
|
1280
|
+
check "set -a: exactly two openssl calls were recorded (inner and outer digest), both with the argv 'dgst -sha1 -binary'" rec_dgst_exact 2
|
|
1281
|
+
check "set -a: the od, tr and base64 children were recorded too (the shims are on the path)" \
|
|
1282
|
+
bash -c '[[ "$1" -ge 1 && "$2" -ge 1 && "$3" -ge 1 ]]' _ "$(rec_count od)" "$(rec_count tr)" "$(rec_count base64)"
|
|
1283
|
+
check "set -a: the key bytes are on no recorded argv" rec_argv_absent "$SETA_KEY"
|
|
1284
|
+
check "set -a: HM_KEY and every _hs_* name are absent from every child's environment" rec_env_clean HM_KEY '_hs_*'
|
|
1285
|
+
check "set -a: control, allexport does export a plain assignment (the mechanism is real)" \
|
|
1286
|
+
bash -c '[[ "$( set -a; HM_KEY=x; compgen -e )" == *HM_KEY* ]]'
|
|
1287
|
+
# Mutants: dropping the un-export or the allexport-off turns the environment row RED.
|
|
1288
|
+
M_LIB="$(mut_lib exportn 'export -n -- "$1"' ':')"
|
|
1289
|
+
seta_run "$M_LIB"
|
|
1290
|
+
check "mutant: a library that does not un-export the key variable leaks HM_KEY into a child environment (RED on the set -a row)" rec_env_leaks HM_KEY
|
|
1291
|
+
M_LIB="$(mut_lib allexport 'set +a' ':')"
|
|
1292
|
+
seta_run "$M_LIB"
|
|
1293
|
+
check "mutant: a library that leaves allexport on exports its own locals (RED on the set -a row)" rec_env_leaks '_hs_*'
|
|
1294
|
+
|
|
1295
|
+
# --- failure semantics ----------------------------------------------------------------------
|
|
1296
|
+
HF_KEY="SYNTH-hf-key-0001"
|
|
1297
|
+
HF_OUT="$( HM_KEY="$HF_KEY" bash -c 'source "$1"; set -x; printf m | hmac_sha1_b64 HM_KEY; echo "rc=$?"' _ "$HMAC_LIB" 2>"$SANDBOX/hf.err" )"
|
|
1298
|
+
HF_ERR="$(cat "$SANDBOX/hf.err")"
|
|
1299
|
+
check "xtrace: switched on after loading, the function refuses (rc 78) and prints no digest" test "$HF_OUT" = "rc=78"
|
|
1300
|
+
check "xtrace: the key reaches neither stdout nor the trace on stderr" bash -c '[[ "$3" != *"$1"* && "$4" != *"$1"* ]]' _ "$HF_KEY" x "$HF_OUT" "$HF_ERR"
|
|
1301
|
+
check "xtrace: the refusal is not the credential-refusal marker" bash -c '[[ "$1" != *SOLEUR_CREDENTIAL_REFUSED* ]]' _ "$HF_ERR"
|
|
1302
|
+
HF_LOAD="$( HM_KEY="$HF_KEY" bash -xc 'source "$1"; echo "src=$? defined=$(type -t hmac_sha1_b64)"' _ "$HMAC_LIB" 2>"$SANDBOX/hf2.err" )"
|
|
1303
|
+
HF_ERR2="$(cat "$SANDBOX/hf2.err")"
|
|
1304
|
+
check "xtrace: sourcing the library under bash -x refuses at load time (return 78) and defines nothing" bash -c '[[ "$1" == *"src=78 defined=" ]]' _ "$HF_LOAD"
|
|
1305
|
+
check "xtrace: the load-time refusal is on STDOUT (agent runtimes swallow stderr) and names the cause" bash -c '[[ "$1" == *"Refusing to load the HMAC helper"* ]]' _ "$HF_LOAD"
|
|
1306
|
+
check "xtrace: the key is in neither stream of the load-time refusal" bash -c '[[ "$1" != *"$3"* && "$2" != *"$3"* ]]' _ "$HF_LOAD" "$HF_ERR2" "$HF_KEY"
|
|
1307
|
+
NOPATH="$SANDBOX/emptypath"; mkdir -p "$NOPATH"
|
|
1308
|
+
# shellcheck disable=SC2034
|
|
1309
|
+
HF_OUT="$( PATH="$NOPATH"; HM_KEY="$HF_KEY"; printf m | hmac_sha1_b64 HM_KEY 2>/dev/null )"; HF_RC=$?
|
|
1310
|
+
check "failure: no openssl on PATH -> non-zero and no digest" bash -c '[[ "$1" -ne 0 && -z "$2" ]]' _ "$HF_RC" "$HF_OUT"
|
|
1311
|
+
HF_OUT="$( printf m | hmac_sha1_b64 HM_NEVER_SET 2>/dev/null )"; HF_RC=$?
|
|
1312
|
+
check "failure: an unset key variable -> non-zero and no digest (an unset key is not an empty key)" bash -c '[[ "$1" -ne 0 && -z "$2" ]]' _ "$HF_RC" "$HF_OUT"
|
|
1313
|
+
HF_OUT="$( printf m | hmac_sha1_b64 '' 2>/dev/null )"; HF_RC=$?
|
|
1314
|
+
check "failure: an empty variable name -> non-zero and no digest" bash -c '[[ "$1" -ne 0 && -z "$2" ]]' _ "$HF_RC" "$HF_OUT"
|
|
1315
|
+
rm -f "$SANDBOX/SENT.sub"
|
|
1316
|
+
HF_OUT="$( printf m | hmac_sha1_b64 "a[\$(touch $SANDBOX/SENT.sub)]" 2>/dev/null )"; HF_RC=$?
|
|
1317
|
+
check "failure: a variable name with a subscript command substitution is refused, and nothing runs" \
|
|
1318
|
+
bash -c '[[ "$1" -ne 0 && -z "$2" && ! -e "$3" ]]' _ "$HF_RC" "$HF_OUT" "$SANDBOX/SENT.sub"
|
|
1319
|
+
HF_OUT="$( HM_KEY="$HF_KEY" bash -c 'source "$1"; printf m | hmac_sha1_b64 HM_KEY' _ "$HMAC_LIB" 2>/dev/null )"; HF_RC=$?
|
|
1320
|
+
check "failure: control, the same call without -x succeeds and prints a 28-character digest" bash -c '[[ "$1" -eq 0 && "${#2}" -eq 28 ]]' _ "$HF_RC" "$HF_OUT"
|
|
1321
|
+
|
|
1322
|
+
# --- full signed requests through the real scripts -----------------------------------------
|
|
1323
|
+
echo "== OAuth 1.0a signing through the scripts (recording shims) =="
|
|
1324
|
+
# The OAuth signing key is urlencode(consumer secret) & urlencode(token secret) (RFC 5849 3.4.2). The
|
|
1325
|
+
# oracle encodes with its OWN encoder (t_urlenc, below), so a SUT that drops the encoding disagrees
|
|
1326
|
+
# whenever a secret holds a reserved character. SIGN_S / SIGN_T name the secrets of the run under
|
|
1327
|
+
# check; the default fixtures are unreserved characters, where the encoding is the identity, so the
|
|
1328
|
+
# reserved-character run further down is what pins it.
|
|
1329
|
+
SIGN_S="$X_SECRET" SIGN_T="$X_TOKSECRET"
|
|
1330
|
+
# The oracle's percent-encoder is NOT a copy of the script's `urlencode` (a line-for-line copy agreed
|
|
1331
|
+
# with it on a multi-byte character and both disagreed with the RFC): it is Python's, over the UTF-8
|
|
1332
|
+
# BYTES of the string, leaving exactly the RFC 5849 3.6 unreserved set (ALPHA DIGIT - . _ ~).
|
|
1333
|
+
t_urlenc() { python3 -c 'import sys, urllib.parse; sys.stdout.write(urllib.parse.quote(sys.argv[1], safe=""))' "$1"; }
|
|
1334
|
+
check "oracle encoder: the RFC 5849 reserved set and a space encode per the RFC (hard-coded expectation, not the SUT's output)" \
|
|
1335
|
+
test "$(t_urlenc 'a b+c/d=e&f~g%h:i')" = 'a%20b%2Bc%2Fd%3De%26f~g%25h%3Ai'
|
|
1336
|
+
check "oracle encoder: a multi-byte character encodes as its UTF-8 BYTES (%C3%A9%E2%82%AC for e-acute and euro), not as code points" \
|
|
1337
|
+
test "$(t_urlenc 'é€')" = '%C3%A9%E2%82%AC'
|
|
1338
|
+
check "oracle encoder: an empty string encodes to an empty string" test -z "$(t_urlenc '')"
|
|
1339
|
+
x_signkey() { printf '%s&%s' "$(t_urlenc "$SIGN_S")" "$(t_urlenc "$SIGN_T")"; }
|
|
1340
|
+
x_keyhex() { printf '%s' "$SIGN_S" | od -An -tx1 | tr -d ' \n'; }
|
|
1341
|
+
# x_sig_ok <n> <METHOD>: the oauth_signature on call n equals an INDEPENDENT recomputation from the
|
|
1342
|
+
# recorded header, URL and query (openssl dgst -hmac over the synthetic key).
|
|
1343
|
+
x_sig_ok() {
|
|
1344
|
+
local n="$1" method="$2" raw hdr url w base query="" ck nonce ts tok sig params want base_str
|
|
1345
|
+
local -a a=() lines=()
|
|
1346
|
+
[[ -s "$MOCK/stdin.$n" && -s "$MOCK/argv.$n" ]] || return 1
|
|
1347
|
+
raw="$(stdin_text "$n")"; hdr="${raw#header = \"}"; hdr="$(cfg_decode "${hdr%\"}")"
|
|
1348
|
+
mapfile -d '' -t a < "$MOCK/argv.$n"
|
|
1349
|
+
url=""; for w in "${a[@]}"; do [[ "$w" == http*://* ]] && url="$w"; done
|
|
1350
|
+
[[ -n "$url" ]] || return 1
|
|
1351
|
+
base="${url%%\?*}"; [[ "$url" == *\?* ]] && query="${url#*\?}"
|
|
1352
|
+
oauth_field() { [[ "$hdr" =~ $1=\"([^\"]*)\" ]] && printf '%s' "${BASH_REMATCH[1]}"; }
|
|
1353
|
+
ck="$(oauth_field oauth_consumer_key)"; nonce="$(oauth_field oauth_nonce)"; ts="$(oauth_field oauth_timestamp)"
|
|
1354
|
+
tok="$(oauth_field oauth_token)"; sig="$(oauth_field oauth_signature)"
|
|
1355
|
+
[[ -n "$ck" && -n "$nonce" && -n "$ts" && -n "$tok" && -n "$sig" ]] || return 1
|
|
1356
|
+
lines=("oauth_consumer_key=$ck" "oauth_nonce=$nonce" "oauth_signature_method=HMAC-SHA1" "oauth_timestamp=$ts" "oauth_token=$tok" "oauth_version=1.0")
|
|
1357
|
+
if [[ -n "$query" ]]; then
|
|
1358
|
+
local pair
|
|
1359
|
+
while IFS= read -r pair; do
|
|
1360
|
+
[[ -n "$pair" ]] && lines+=("$(t_urlenc "${pair%%=*}")=$(t_urlenc "${pair#*=}")")
|
|
1361
|
+
done < <(printf '%s\n' "${query//&/$'\n'}")
|
|
1362
|
+
fi
|
|
1363
|
+
params="$(printf '%s\n' "${lines[@]}" | LC_ALL=C sort | paste -sd '&' -)"
|
|
1364
|
+
base_str="${method}&$(t_urlenc "$base")&$(t_urlenc "$params")"
|
|
1365
|
+
want="$(printf '%s' "$base_str" | openssl dgst -sha1 -hmac "$(x_signkey)" -binary | base64)"
|
|
1366
|
+
[[ -n "$want" && "$(t_urlenc "$want")" == "$sig" ]]
|
|
1367
|
+
}
|
|
1368
|
+
x_hdr_shape() {
|
|
1369
|
+
local raw; raw="$(stdin_text "$1")"
|
|
1370
|
+
[[ "$raw" == 'header = "Authorization: OAuth oauth_consumer_key=\"'* && "$raw" == *'oauth_signature_method=\"HMAC-SHA1\"'* && "$raw" == *'oauth_version=\"1.0\""' ]]
|
|
1371
|
+
}
|
|
1372
|
+
x_rec_rows() { # <label> <n> <method>
|
|
1373
|
+
local label="$1" n="$2" method="$3"
|
|
1374
|
+
check "$label: header is the same Authorization: OAuth structure on curl's stdin config" x_hdr_shape "$n"
|
|
1375
|
+
check "$label: the signature equals an independent recomputation (openssl dgst -hmac over the synthetic key)" x_sig_ok "$n" "$method"
|
|
1376
|
+
check "$label: exactly two dgst calls, each with the argv 'dgst -sha1 -binary'" rec_dgst_exact 2
|
|
1377
|
+
check "$label: no -hmac operand on any recorded argv (openssl, od, tr, base64)" rec_argv_absent "-hmac"
|
|
1378
|
+
check "$label: no -macopt operand on any recorded argv" rec_argv_absent "-macopt"
|
|
1379
|
+
check "$label: neither secret, the full signing key nor the key's hex is on any recorded argv" \
|
|
1380
|
+
rec_argv_absent_all "$SIGN_S" "$SIGN_T" "$(x_signkey)" "$(x_keyhex)"
|
|
1381
|
+
check "$label: signing_key and _hs_* are absent from every recorded child environment" rec_env_clean signing_key '_hs_*'
|
|
1382
|
+
}
|
|
1383
|
+
rec_clear
|
|
1384
|
+
run_sut "$REC" "" "${X_ENV[@]}" -- bash "$X_COMMUNITY" fetch-metrics
|
|
1385
|
+
check "x-community fetch-metrics with the recording shims: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1386
|
+
x_rec_rows "x-community fetch-metrics (GET, query signed)" 1 GET
|
|
1387
|
+
rec_clear
|
|
1388
|
+
run_sut "$REC" "" "${X_ENV[@]}" -- bash "$X_COMMUNITY" post-tweet "synthetic post"
|
|
1389
|
+
check "x-community post-tweet with the recording shims: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1390
|
+
x_rec_rows "x-community post-tweet (POST)" 1 POST
|
|
1391
|
+
rec_clear
|
|
1392
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${X_ENV[@]}" -- bash "$X_SETUP" validate-credentials
|
|
1393
|
+
check "x-setup validate-credentials with the recording shims: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1394
|
+
x_rec_rows "x-setup validate-credentials (GET)" 1 GET
|
|
1395
|
+
# Mutant: the old argv spelling comes back in x-community -> the argv rows go RED.
|
|
1396
|
+
M="$(mut_make x-community.sh 'hmac_sha1_b64 signing_key' 'openssl dgst -sha1 -hmac "$signing_key" -binary | base64')"
|
|
1397
|
+
rec_clear
|
|
1398
|
+
run_sut "$REC" "" "${X_ENV[@]}" -- bash "$M" fetch-metrics
|
|
1399
|
+
check "mutant: x-community with the -hmac spelling restored puts the signing key on openssl's argv (RED on the argv rows)" rec_argv_has "$X_TOKSECRET"
|
|
1400
|
+
check "mutant: that control is RED because the key is there, not because nothing ran (a curl call and an openssl call were recorded)" \
|
|
1401
|
+
bash -c '[[ "$1" -eq 1 && "$2" -ge 2 ]]' _ "$(curl_calls)" "$(rec_count openssl)"
|
|
1402
|
+
|
|
1403
|
+
# --- reserved characters in the secrets: the signing key is urlencoded (RFC 5849 3.4.2) ------------
|
|
1404
|
+
# The fixtures above are unreserved characters, where urlencode is the identity, so a SUT that dropped
|
|
1405
|
+
# the encoding would still match. These secrets hold + / = & space ~ and a percent escape.
|
|
1406
|
+
echo "== OAuth 1.0a signing key with reserved characters in the secrets =="
|
|
1407
|
+
XR_S='Sec+ret/with=eq& 02'
|
|
1408
|
+
XR_T='tok~sec%41:x+y/z='
|
|
1409
|
+
XR_ENV=("X_API_KEY=$X_KEY" "X_API_SECRET=$XR_S" "X_ACCESS_TOKEN=$X_TOK" "X_ACCESS_TOKEN_SECRET=$XR_T" X_ALLOW_POST=true)
|
|
1410
|
+
SIGN_S="$XR_S" SIGN_T="$XR_T"
|
|
1411
|
+
check "reserved fixture: the encoded key differs from the raw one (the encoding is not the identity here)" \
|
|
1412
|
+
bash -c '[[ "$1" != "$2&$3" ]]' _ "$(x_signkey)" "$XR_S" "$XR_T"
|
|
1413
|
+
rec_clear
|
|
1414
|
+
run_sut "$REC" "" "${XR_ENV[@]}" -- bash "$X_COMMUNITY" fetch-metrics
|
|
1415
|
+
check "reserved secrets, x-community fetch-metrics: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1416
|
+
check "reserved secrets, x-community: the signature equals the oracle over the RFC 5849 encoded key" x_sig_ok 1 GET
|
|
1417
|
+
check "reserved secrets, x-community: neither raw secret, the encoded key nor its hex is on any recorded argv" \
|
|
1418
|
+
rec_argv_absent_all "$XR_S" "$XR_T" "$(x_signkey)" "$(x_keyhex)"
|
|
1419
|
+
rec_clear
|
|
1420
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${XR_ENV[@]}" -- bash "$X_SETUP" validate-credentials
|
|
1421
|
+
check "reserved secrets, x-setup validate-credentials: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1422
|
+
check "reserved secrets, x-setup: the signature equals the oracle over the RFC 5849 encoded key" x_sig_ok 1 GET
|
|
1423
|
+
# Mutants: the encoding dropped from the key in either script is invisible on the unreserved fixtures and RED here.
|
|
1424
|
+
M="$(mut_make x-community.sh '"$(urlencode "${X_API_SECRET}")&$(urlencode "${X_ACCESS_TOKEN_SECRET}")"' '"${X_API_SECRET}&${X_ACCESS_TOKEN_SECRET}"')"
|
|
1425
|
+
run_sut "$REC" "" "${XR_ENV[@]}" -- bash "$M" fetch-metrics
|
|
1426
|
+
check "mutant: x-community with the key left unencoded is RED on the reserved run (a request was signed and the signature disagrees)" \
|
|
1427
|
+
bash -c '[[ "$1" -eq 1 ]]' _ "$(curl_calls)"
|
|
1428
|
+
check "mutant: x-community with the key left unencoded: the oracle rejects its signature" not x_sig_ok 1 GET
|
|
1429
|
+
SIGN_S="$X_SECRET" SIGN_T="$X_TOKSECRET"
|
|
1430
|
+
run_sut "$REC" "" "${X_ENV[@]}" -- bash "$M" fetch-metrics
|
|
1431
|
+
check "mutant control: the same unencoded-key mutant is GREEN on the unreserved fixtures (why the reserved run is needed)" x_sig_ok 1 GET
|
|
1432
|
+
SIGN_S="$XR_S" SIGN_T="$XR_T"
|
|
1433
|
+
M="$(mut_make x-setup.sh '"$(urlencode "${X_API_SECRET}")&$(urlencode "${X_ACCESS_TOKEN_SECRET}")"' '"${X_API_SECRET}&${X_ACCESS_TOKEN_SECRET}"')"
|
|
1434
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${XR_ENV[@]}" -- bash "$M" validate-credentials
|
|
1435
|
+
check "mutant: x-setup with the key left unencoded is RED on the reserved run (a request was signed and the signature disagrees)" \
|
|
1436
|
+
bash -c '[[ "$1" -eq 1 ]]' _ "$(curl_calls)"
|
|
1437
|
+
check "mutant: x-setup with the key left unencoded: the oracle rejects its signature" not x_sig_ok 1 GET
|
|
1438
|
+
SIGN_S="$X_SECRET" SIGN_T="$X_TOKSECRET"
|
|
1439
|
+
|
|
1440
|
+
# --- multi-byte characters in the secrets: the key is encoded as its UTF-8 BYTES (RFC 5849 3.6) -----
|
|
1441
|
+
# The script runs here under a UTF-8 locale (env -i would otherwise leave it in the C locale, where a
|
|
1442
|
+
# character-wise loop is byte-wise by accident). The oracle's encoder is Python's, over the bytes.
|
|
1443
|
+
echo "== OAuth 1.0a signing key with multi-byte characters in the secrets =="
|
|
1444
|
+
XU_LOC=C.UTF-8
|
|
1445
|
+
XU_S='Sécret€one' XU_T='tökén€two'
|
|
1446
|
+
XU_ENV=("LC_ALL=$XU_LOC" "X_API_KEY=$X_KEY" "X_API_SECRET=$XU_S" "X_ACCESS_TOKEN=$X_TOK" "X_ACCESS_TOKEN_SECRET=$XU_T" X_ALLOW_POST=true)
|
|
1447
|
+
check "multi-byte canary: the $XU_LOC locale is installed here and counts one character for e-acute (the row is not vacuous)" \
|
|
1448
|
+
bash -c '[[ "$(LC_ALL="$1" bash -c "echo \${#1}" _ "é")" == 1 ]]' _ "$XU_LOC"
|
|
1449
|
+
SIGN_S="$XU_S" SIGN_T="$XU_T"
|
|
1450
|
+
check "multi-byte fixture: the encoded key holds the UTF-8 byte escapes, not code points (%C3%A9, not %E9)" \
|
|
1451
|
+
bash -c '[[ "$1" == *"%C3%A9"* && "$1" != *"%E9"* ]]' _ "$(x_signkey)"
|
|
1452
|
+
rec_clear
|
|
1453
|
+
run_sut "$REC" "" "${XU_ENV[@]}" -- bash "$X_COMMUNITY" fetch-metrics
|
|
1454
|
+
check "multi-byte secrets, x-community fetch-metrics: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1455
|
+
check "multi-byte secrets, x-community: the signature equals the oracle over the byte-encoded key" x_sig_ok 1 GET
|
|
1456
|
+
rec_clear
|
|
1457
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${XU_ENV[@]}" -- bash "$X_SETUP" validate-credentials
|
|
1458
|
+
check "multi-byte secrets, x-setup validate-credentials: exit 0, one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1459
|
+
check "multi-byte secrets, x-setup: the signature equals the oracle over the byte-encoded key" x_sig_ok 1 GET
|
|
1460
|
+
# Mutants: the code-point encoding comes back (no LC_ALL=C in urlencode) -> the oracle disagrees.
|
|
1461
|
+
M="$(mut_make x-community.sh 'urlencode() {
|
|
1462
|
+
local LC_ALL=C
|
|
1463
|
+
' 'urlencode() {
|
|
1464
|
+
')"
|
|
1465
|
+
run_sut "$REC" "" "${XU_ENV[@]}" -- bash "$M" fetch-metrics
|
|
1466
|
+
check "mutant: x-community urlencode without LC_ALL=C still signs and sends one request (exit 0)" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1467
|
+
check "mutant: x-community urlencode without LC_ALL=C encodes a multi-byte secret by code point: the oracle rejects its signature" not x_sig_ok 1 GET
|
|
1468
|
+
M="$(mut_make x-setup.sh 'urlencode() {
|
|
1469
|
+
local LC_ALL=C
|
|
1470
|
+
' 'urlencode() {
|
|
1471
|
+
')"
|
|
1472
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${XU_ENV[@]}" -- bash "$M" validate-credentials
|
|
1473
|
+
check "mutant: x-setup urlencode without LC_ALL=C still signs and sends one request (exit 0)" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1474
|
+
check "mutant: x-setup urlencode without LC_ALL=C encodes a multi-byte secret by code point: the oracle rejects its signature" not x_sig_ok 1 GET
|
|
1475
|
+
SIGN_S="$X_SECRET" SIGN_T="$X_TOKSECRET"
|
|
1476
|
+
|
|
1477
|
+
# --- an inherited exported function named hmac_sha1_b64 must not stand in for the vetted library ------
|
|
1478
|
+
echo "== x-community.sh: an inherited hmac_sha1_b64 function =="
|
|
1479
|
+
# The stand-in drains its stdin (builtins only) before answering: a stand-in that exits at once races the
|
|
1480
|
+
# caller's `printf | hmac_sha1_b64` (SIGPIPE under `pipefail` when the writer is slower than the exit),
|
|
1481
|
+
# which flakes the "still sends one request" rows on a loaded host.
|
|
1482
|
+
IMPFN='BASH_FUNC_hmac_sha1_b64%%=() { local l; while IFS= read -r l || [ -n "$l" ]; do :; done; echo IMPORTED; }'
|
|
1483
|
+
check "inherited-function canary: bash imports that exported function from the environment here (the row is not vacuous)" \
|
|
1484
|
+
bash -c '[[ "$(env "$1" bash -c "type -t hmac_sha1_b64")" == function ]]' _ "$IMPFN"
|
|
1485
|
+
rec_clear
|
|
1486
|
+
run_sut "$REC" "" "${X_ENV[@]}" "$IMPFN" -- bash "$X_COMMUNITY" fetch-metrics
|
|
1487
|
+
check "inherited hmac_sha1_b64 function: x-community still exits 0 with one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1488
|
+
check "inherited hmac_sha1_b64 function: the signature is the vetted library's (equals the independent recomputation), not the inherited stand-in's" x_sig_ok 1 GET
|
|
1489
|
+
# Mutant: the pre-fix guard (load the library only when no such function exists) and no unset -> the stand-in runs.
|
|
1490
|
+
M="$(mut_make x-community.sh 'unset -f hmac_sha1_b64 2>/dev/null || true' ':' 'source "$SCRIPT_DIR/lib/hmac-sha1-b64.sh" >&3 || {' 'declare -F hmac_sha1_b64 >/dev/null || source "$SCRIPT_DIR/lib/hmac-sha1-b64.sh" >&3 || {')"
|
|
1491
|
+
rec_clear
|
|
1492
|
+
run_sut "$REC" "" "${X_ENV[@]}" "$IMPFN" -- bash "$M" fetch-metrics
|
|
1493
|
+
check "mutant: x-community with the declare -F guard and no unset still sends one request (exit 0)" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1494
|
+
check "mutant: that guard mutant signs with the INHERITED function: the oracle rejects the signature (RED on the row above)" not x_sig_ok 1 GET
|
|
1495
|
+
# The control for the mutant: the same mutant without an inherited function signs correctly (the guard alone is harmless).
|
|
1496
|
+
rec_clear
|
|
1497
|
+
run_sut "$REC" "" "${X_ENV[@]}" -- bash "$M" fetch-metrics
|
|
1498
|
+
check "mutant control: the same guard mutant with NO inherited function signs correctly (the row is RED because of the import)" x_sig_ok 1 GET
|
|
1499
|
+
|
|
1500
|
+
# x-setup.sh loads the library at startup, so the same drop-then-check has to be there. With the library
|
|
1501
|
+
# intact the source redefines the function whatever was inherited; the path the drop closes is a library
|
|
1502
|
+
# that loads WITHOUT defining it (truncated, emptied). That is built in a sandbox tree with an empty library.
|
|
1503
|
+
echo "== x-setup.sh: an inherited hmac_sha1_b64 function =="
|
|
1504
|
+
XSL="$SANDBOX/xslib"
|
|
1505
|
+
mkdir -p "$XSL/skills/community/scripts/lib" "$XSL/scripts"
|
|
1506
|
+
cp "$HERE/../../../scripts/resolve-git-root.sh" "$XSL/scripts/"
|
|
1507
|
+
printf '# a library that loads and defines nothing\n:\n' > "$XSL/skills/community/scripts/lib/hmac-sha1-b64.sh"
|
|
1508
|
+
xsl_run() { # <x-setup.sh text file> [inherited-function env]: run validate-credentials against the empty library
|
|
1509
|
+
cp "$1" "$XSL/skills/community/scripts/x-setup.sh"
|
|
1510
|
+
rec_clear
|
|
1511
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${X_ENV[@]}" "${@:2}" -- bash "$XSL/skills/community/scripts/x-setup.sh" validate-credentials
|
|
1512
|
+
}
|
|
1513
|
+
rec_clear
|
|
1514
|
+
RUN_CWD="$GITWORK" run_sut "$REC" "" "${X_ENV[@]}" "$IMPFN" -- bash "$X_SETUP" validate-credentials
|
|
1515
|
+
check "inherited hmac_sha1_b64 function: x-setup with the real library still exits 0 with one curl call" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1516
|
+
check "inherited hmac_sha1_b64 function: x-setup's signature is the vetted library's (equals the independent recomputation)" x_sig_ok 1 GET
|
|
1517
|
+
xsl_run "$X_SETUP"
|
|
1518
|
+
check "x-setup with a library that defines nothing and NO inherited function: exit 1, curl never invoked (control: the empty library alone refuses)" \
|
|
1519
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
1520
|
+
xsl_run "$X_SETUP" "$IMPFN"
|
|
1521
|
+
check "x-setup with a library that defines nothing and an inherited function: exit 1 and curl never invoked (the stand-in does not sign)" \
|
|
1522
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
1523
|
+
check "x-setup with a library that defines nothing and an inherited function: the load failure is named on stderr, no credential on either stream" \
|
|
1524
|
+
bash -c '[[ "$1" == *"did not define hmac_sha1_b64"* && "$1$2" != *"$3"* && "$1$2" != *"$4"* ]]' _ "$ERR" "$OUT" "$X_SECRET" "$X_TOKSECRET"
|
|
1525
|
+
# Mutant 1: the unset is gone (the declare -F check alone sees the inherited stand-in and passes it).
|
|
1526
|
+
M="$(mut_make x-setup.sh 'unset -f hmac_sha1_b64 2>/dev/null || true' ':')"
|
|
1527
|
+
xsl_run "$M" "$IMPFN"
|
|
1528
|
+
check "mutant: x-setup without the unset -f signs with the INHERITED function: one request is sent (RED on the refusal row)" test "$(curl_calls)" -eq 1
|
|
1529
|
+
check "mutant: that mutant's signature is the stand-in's, so the independent recomputation rejects it" not x_sig_ok 1 GET
|
|
1530
|
+
xsl_run "$M"
|
|
1531
|
+
check "mutant control: the same mutant with NO inherited function still refuses on the empty library (the row above is RED because of the import)" \
|
|
1532
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
1533
|
+
# Mutant 2: the post-load check is gone (the unset alone still refuses, but the load failure is no longer named).
|
|
1534
|
+
M="$(mut_make x-setup.sh 'if ! declare -F hmac_sha1_b64 >/dev/null; then' 'if false; then')"
|
|
1535
|
+
xsl_run "$M" "$IMPFN"
|
|
1536
|
+
check "mutant: x-setup without the post-load check still sends nothing (the unset leaves no signer; exit 1)" \
|
|
1537
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
1538
|
+
check "mutant: that mutant no longer names the load failure (RED on the named-failure row)" bash -c '[[ "$1" != *"did not define hmac_sha1_b64"* ]]' _ "$ERR"
|
|
1539
|
+
# The comment claim: an inherited function of the NAME is what is closed, not "every callee".
|
|
1540
|
+
check "x-community.sh: the signing comment no longer claims that only the vetted library's definition can run with the key in scope" \
|
|
1541
|
+
test "$(file_count "only the vetted library's definition can ever run" "$X_COMMUNITY")" -eq 0
|
|
1542
|
+
check "x-community.sh: the signing comment states the true claim (an inherited function of this name cannot stand in for the library's)" \
|
|
1543
|
+
test "$(file_count "stand in for the library's" "$X_COMMUNITY")" -eq 1
|
|
1544
|
+
check "x-community.sh: the signing comment states the residual (an exported od, tr, openssl, base64 or cat still receives the key on its stdin)" \
|
|
1545
|
+
test "$(file_count "one named od, tr, openssl, base64 or cat still receives it on its stdin" "$X_COMMUNITY")" -eq 1
|
|
1546
|
+
check "the HMAC library header states the same residual and claims no stronger control" \
|
|
1547
|
+
test "$(file_count "receives the key on its stdin" "$HMAC_LIB")" -eq 1
|
|
1548
|
+
|
|
1549
|
+
# Lazy loading: the library is sourced inside oauth_sign, and the top-level source line is gone.
|
|
1550
|
+
check "x-community.sh: the only source of lib/hmac-sha1-b64.sh sits inside oauth_sign (lazy, not at load)" \
|
|
1551
|
+
awk 'BEGIN{f=0;n=0;ins=0} /^oauth_sign\(\) \{/{f=1} /^}/{f=0} /^[[:space:]]*#/{next} /source .*lib\/hmac-sha1-b64\.sh/{n++; if(f)ins++} END{exit !(n==1 && ins==1)}' "$X_COMMUNITY"
|
|
1552
|
+
M="$(mut_make x-community.sh 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
1553
|
+
' 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
1554
|
+
source "$SCRIPT_DIR/lib/hmac-sha1-b64.sh"
|
|
1555
|
+
')"
|
|
1556
|
+
run_sut - "" -- bash -x "$M"
|
|
1557
|
+
check "mutant: x-community with the library sourced at load again refuses a credential-free bash -x run (exit 78: RED on the rows above)" \
|
|
1558
|
+
bash -c '[[ "$1" -eq 78 && "$2" == *"Refusing to load the HMAC helper"* ]]' _ "$RC" "$OUT"
|
|
1559
|
+
run_sut - "" -- bash "$M"
|
|
1560
|
+
check "mutant control: that same load-time-source mutant still prints usage when NOT traced (exit 1): the row above is RED because of -x" test "$RC" -eq 1
|
|
1561
|
+
|
|
1562
|
+
# The lazy load can fail (the library is missing or unreadable): that must refuse, never send unsigned.
|
|
1563
|
+
NOLIB="$SANDBOX/nolib"
|
|
1564
|
+
mkdir -p "$NOLIB/mut"
|
|
1565
|
+
cp "$X_COMMUNITY" "$NOLIB/x-community.sh"
|
|
1566
|
+
run_sut - "" "${X_ENV[@]}" -- bash "$NOLIB/x-community.sh" fetch-metrics
|
|
1567
|
+
check "x-community with the signing library missing: exit 1, curl never invoked, and the load failure is named (not a generic signature failure)" \
|
|
1568
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 && "$3" == *"could not load the OAuth signing helper"* ]]' _ "$RC" "$(curl_calls)" "$ERR"
|
|
1569
|
+
check "x-community with the signing library missing: no credential reaches either stream" \
|
|
1570
|
+
bash -c '[[ "$1" != *"$2"* && "$1" != *"$3"* ]]' _ "$OUT$ERR" "$X_SECRET" "$X_TOKSECRET"
|
|
1571
|
+
nolib_one_error() { # the load failure is the ONE error: no second, misleading "unexpected shape" line
|
|
1572
|
+
[[ "$ERR" != *"unexpected shape"* && "$OUT$ERR" != *"unexpected shape"* && "$(awk '/^Error:/ {n++} END {print n + 0}' <<<"$ERR")" -eq 1 ]]
|
|
1573
|
+
}
|
|
1574
|
+
check "x-community with the signing library missing: exactly one Error line, and no second 'unexpected shape' line" nolib_one_error
|
|
1575
|
+
# Mutant: the failed signature no longer stops the caller, so the empty header falls through to the shape check.
|
|
1576
|
+
M="$(mut_make x-community.sh 'auth_header=$(oauth_sign "GET" "$url" "${param_args[@]}") || exit 1' 'auth_header=$(oauth_sign "GET" "$url" "${param_args[@]}")')"
|
|
1577
|
+
mkdir -p "$NOLIB/mut2"; cp "$M" "$NOLIB/mut2/x-community.sh"
|
|
1578
|
+
run_sut - "" "${X_ENV[@]}" -- bash "$NOLIB/mut2/x-community.sh" fetch-metrics
|
|
1579
|
+
check "mutant: x-community without the exit after a failed signature still exits 1 and sends nothing (so only the one-error row can catch it)" \
|
|
1580
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
1581
|
+
check "mutant: that mutant prints the second, misleading 'unexpected shape' line (RED on the one-error row)" not nolib_one_error
|
|
1582
|
+
M="$(mut_make x-community.sh 'source "$SCRIPT_DIR/lib/hmac-sha1-b64.sh" >&3 || {' 'source "$SCRIPT_DIR/lib/hmac-sha1-b64.sh" >&3 2>/dev/null || true; : || {')"
|
|
1583
|
+
cp "$M" "$NOLIB/mut/x-community.sh"
|
|
1584
|
+
run_sut - "" "${X_ENV[@]}" -- bash "$NOLIB/mut/x-community.sh" fetch-metrics
|
|
1585
|
+
check "mutant: x-community that ignores a failed library load no longer names it (exit 1 still, curl never invoked; RED on the load-failure row)" \
|
|
1586
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 && "$3" != *"could not load the OAuth signing helper"* ]]' _ "$RC" "$(curl_calls)" "$ERR"
|
|
1587
|
+
|
|
1588
|
+
echo "== hosted path: x-community.sh under a PATH with no interpreter =="
|
|
1589
|
+
MINPATH="$SANDBOX/minpath"; mkdir -p "$MINPATH"
|
|
1590
|
+
for t_ in bash openssl jq date sort sed tail head cat tr od base64 dirname basename paste; do
|
|
1591
|
+
rp_="$(command -v "$t_")" && ln -sf "$rp_" "$MINPATH/$t_"
|
|
1592
|
+
done
|
|
1593
|
+
cp "$MOCK/curl" "$MINPATH/curl"
|
|
1594
|
+
min_found() { local n=0 t; for t in "$@"; do PATH="$MINPATH" command -v "$t" >/dev/null 2>&1 && n=$((n + 1)); done; printf '%s' "$n"; }
|
|
1595
|
+
check "canary: no python3, python, node, perl, ruby or php on the restricted PATH" test "$(min_found python3 python node perl ruby php)" -eq 0
|
|
1596
|
+
check "canary control: the restricted PATH does resolve bash, openssl, jq and curl (the probe can say yes)" test "$(min_found bash openssl jq curl)" -eq 4
|
|
1597
|
+
SUT_PATH="$MINPATH" run_sut - "" "${X_ENV[@]}" -- bash "$X_COMMUNITY" fetch-metrics
|
|
1598
|
+
check "canary: fetch-metrics succeeds with one signed request and no interpreter" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1599
|
+
check "canary: the request signature is correct (independent recomputation)" x_sig_ok 1 GET
|
|
1600
|
+
check "canary: the account JSON came back through jq" test "$(jq -r '.username' <<<"$OUT")" = "synthetic"
|
|
1601
|
+
SUT_PATH="$MINPATH" run_sut - "" "${X_ENV[@]}" -- bash "$X_COMMUNITY" post-tweet "synthetic post"
|
|
1602
|
+
check "canary: post-tweet succeeds with a correct signature and no interpreter" bash -c '[[ "$1" -eq 0 && "$2" -eq 1 ]]' _ "$RC" "$(curl_calls)"
|
|
1603
|
+
check "canary: the post-tweet signature is correct (independent recomputation)" x_sig_ok 1 POST
|
|
1604
|
+
|
|
1605
|
+
# ===================================================================================
|
|
1606
|
+
echo "== write-env allow-list: bsky-setup.sh, discord-setup.sh, x-setup.sh, linkedin-setup.sh =="
|
|
1607
|
+
# Every value is validated BEFORE the first write; a refused value prints the marker plus one human
|
|
1608
|
+
# line naming only the VARIABLE, writes nothing, exits 1 and leaves any existing .env untouched. The
|
|
1609
|
+
# sentinel mechanism: after the run the .env is SOURCED in a clean shell; a value that survived
|
|
1610
|
+
# validation would run `touch SENT`.
|
|
1611
|
+
SENT="$SANDBOX/SENTINEL.fired"
|
|
1612
|
+
WE_VAL="" WE_REASON="" WE_RUNS=0
|
|
1613
|
+
# The classes the allow-list must refuse. The first seven were the original matrix; the rest close the
|
|
1614
|
+
# shell metacharacters it omitted (& | < > ' # * ? [ { ! $ and a backslash, alone and glued to a
|
|
1615
|
+
# command substitution) plus the two control characters other than a newline (tab, CR), and the two
|
|
1616
|
+
# parentheses (a subshell / array / function-definition opener and closer). Each of the
|
|
1617
|
+
# single-character classes holds ONLY that one hostile character beside letters, so widening the
|
|
1618
|
+
# allow-list by exactly that character is what the class's row must catch (a value that also held a
|
|
1619
|
+
# second hostile character would stay refused for the second one).
|
|
1620
|
+
WE_CLASSES=(cmdsub backtick nlassign dquote semi space tilde amp pipe lt gt squote hash star qmark lbracket lbrace bang dollar backslash bsglue tab cr lparen rparen)
|
|
1621
|
+
# The classes that must always be present by NAME (a list cannot shrink to a few, or repeat one, and stay green).
|
|
1622
|
+
WE_REQUIRED_CLASSES=(cmdsub backtick nlassign dquote semi space tilde lparen rparen backslash tab cr squote dollar)
|
|
1623
|
+
we_class_val() { # <class> -> WE_VAL, WE_REASON
|
|
1624
|
+
WE_REASON=token_shape
|
|
1625
|
+
case "$1" in
|
|
1626
|
+
cmdsub) WE_VAL="\$(touch $SENT)" ;;
|
|
1627
|
+
backtick) WE_VAL="\`touch $SENT\`" ;;
|
|
1628
|
+
nlassign) WE_VAL=$'abc\nEVIL=$(touch '"$SENT"')'; WE_REASON=control_char ;;
|
|
1629
|
+
dquote) WE_VAL='abc"def' ;;
|
|
1630
|
+
semi) WE_VAL="abc;touch $SENT" ;;
|
|
1631
|
+
space) WE_VAL="abc touch $SENT" ;;
|
|
1632
|
+
tilde) WE_VAL=$'~/abc' ;;
|
|
1633
|
+
amp) WE_VAL='abc&def' ;;
|
|
1634
|
+
pipe) WE_VAL='abc|def' ;;
|
|
1635
|
+
lt) WE_VAL='abc<def' ;;
|
|
1636
|
+
gt) WE_VAL='abc>def' ;;
|
|
1637
|
+
squote) WE_VAL="abc'def" ;;
|
|
1638
|
+
hash) WE_VAL='abc#def' ;;
|
|
1639
|
+
star) WE_VAL='abc*' ;;
|
|
1640
|
+
qmark) WE_VAL='abc?' ;;
|
|
1641
|
+
lbracket) WE_VAL='abc[def' ;;
|
|
1642
|
+
lbrace) WE_VAL='abc{def' ;;
|
|
1643
|
+
bang) WE_VAL='abc!def' ;;
|
|
1644
|
+
dollar) WE_VAL='a$HOME' ;;
|
|
1645
|
+
backslash) WE_VAL='abc\def' ;;
|
|
1646
|
+
bsglue) WE_VAL="abc\\\$(touch $SENT)" ;;
|
|
1647
|
+
tab) WE_VAL=$'abc\tdef'; WE_REASON=control_char ;;
|
|
1648
|
+
cr) WE_VAL=$'abc\rdef'; WE_REASON=control_char ;;
|
|
1649
|
+
lparen) WE_VAL='abc(def' ;;
|
|
1650
|
+
rparen) WE_VAL='abc)def' ;;
|
|
1651
|
+
*) echo "FATAL: unknown class $1" >&2; exit 2 ;;
|
|
1652
|
+
esac
|
|
1653
|
+
}
|
|
1654
|
+
we_fired() {
|
|
1655
|
+
rm -f "$SENT"
|
|
1656
|
+
( cd "$GITWORK" && env -i PATH="$SYS_PATH" HOME="$SANDBOX/home" bash -c 'set -a; source ./.env' ) >/dev/null 2>&1 || true
|
|
1657
|
+
[[ -e "$SENT" ]]
|
|
1658
|
+
}
|
|
1659
|
+
we_untouched() { cmp -s "$SANDBOX/env.before" "$GITWORK/.env" || return 1; not we_fired; }
|
|
1660
|
+
# we_refusal_ok <script> <var> <reason> <value> <other-var>...
|
|
1661
|
+
we_refusal_ok() {
|
|
1662
|
+
local script="$1" var="$2" reason="$3" val="$4" n o re; shift 4
|
|
1663
|
+
[[ "$RC" -eq 1 ]] || return 1
|
|
1664
|
+
# The marker: exactly once, value-free, on STDERR, naming the refused variable and the phase.
|
|
1665
|
+
n="$(grep -cxF "SOLEUR_CREDENTIAL_REFUSED script=$script reason=$reason var=$var phase=write-env" <<<"$ERR")"
|
|
1666
|
+
[[ "$n" == 1 ]] || return 1
|
|
1667
|
+
n="$(grep -c 'SOLEUR_CREDENTIAL_REFUSED' <<<"$ERR")"
|
|
1668
|
+
[[ "$n" == 1 ]] || return 1
|
|
1669
|
+
# The ONE human line is on STDOUT (a signal the caller must act on; write-env has no stdout payload).
|
|
1670
|
+
n="$(grep -c '^Error:' <<<"$OUT")"
|
|
1671
|
+
[[ "$n" == 1 ]] || return 1
|
|
1672
|
+
n="$(grep -c '^Error:' <<<"$ERR")"
|
|
1673
|
+
[[ "$n" == 0 ]] || return 1
|
|
1674
|
+
re="(^|[^A-Za-z0-9_])${var}([^A-Za-z0-9_]|\$)"
|
|
1675
|
+
[[ "$OUT" =~ $re ]] || return 1
|
|
1676
|
+
for o in "$@"; do
|
|
1677
|
+
re="(^|[^A-Za-z0-9_])${o}([^A-Za-z0-9_]|\$)"
|
|
1678
|
+
[[ "$OUT$ERR" =~ $re ]] && return 1
|
|
1679
|
+
done
|
|
1680
|
+
[[ "$OUT$ERR" != *"$val"* ]] || return 1
|
|
1681
|
+
[[ "$OUT$ERR" != *SOLEUR_TRANSPORT_DIAG* ]]
|
|
1682
|
+
}
|
|
1683
|
+
WE_BASE=()
|
|
1684
|
+
# we_run_bad <label> <seed> <script> <bad-var> <class> <args...>: the base environment is WE_BASE.
|
|
1685
|
+
we_run_bad() {
|
|
1686
|
+
local label="$1" seed="$2" script="$3" var="$4" cls="$5" e val reason; shift 5
|
|
1687
|
+
local -a envs=() others=()
|
|
1688
|
+
we_class_val "$cls"; val="$WE_VAL"; reason="$WE_REASON"
|
|
1689
|
+
for e in "${WE_BASE[@]}"; do
|
|
1690
|
+
[[ "${e%%=*}" == "$var" ]] && continue
|
|
1691
|
+
envs+=("$e"); others+=("${e%%=*}")
|
|
1692
|
+
done
|
|
1693
|
+
envs+=("$var=$val")
|
|
1694
|
+
printf '%s\n' "$seed" > "$GITWORK/.env"; chmod 600 "$GITWORK/.env"
|
|
1695
|
+
cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
1696
|
+
RUN_CWD="$GITWORK" run_sut - "" "${envs[@]}" -- bash "$script" "$@"
|
|
1697
|
+
WE_RUNS=$((WE_RUNS + 1))
|
|
1698
|
+
check "$label write-env $var=<$cls>: refused (exit 1, the stderr marker once with reason=$reason var=$var phase=write-env, one human line on stdout naming only $var, value not echoed)" \
|
|
1699
|
+
we_refusal_ok "$(basename "$script")" "$var" "$reason" "$val" "${others[@]}"
|
|
1700
|
+
check "$label write-env $var=<$cls>: .env byte-identical and the sentinel never fires on source" we_untouched
|
|
1701
|
+
}
|
|
1702
|
+
# we_value_is <NAME> <expected>: the clean-shell value of NAME in .env equals expected, byte for byte.
|
|
1703
|
+
we_value_is() { [[ "$( cd "$GITWORK" && env -i PATH="$SYS_PATH" HOME="$SANDBOX/home" bash -c 'source ./.env; printf "%s" "${!1}"' _ "$1" )" == "$2" ]]; }
|
|
1704
|
+
we_mode_600() { [[ -n "$(find "$GITWORK/.env" -type f -perm 600 2>/dev/null)" ]]; }
|
|
1705
|
+
we_line_count() { [[ "$(grep -c "^$1=" "$GITWORK/.env")" == 1 ]]; }
|
|
1706
|
+
# we_values_ok <NAME=expected>...: every pair is in .env exactly once with the expected value, KEEP_ME survived.
|
|
1707
|
+
we_values_ok() {
|
|
1708
|
+
local p
|
|
1709
|
+
we_value_is KEEP_ME 1 || return 1
|
|
1710
|
+
for p in "$@"; do
|
|
1711
|
+
we_line_count "${p%%=*}" || return 1
|
|
1712
|
+
we_value_is "${p%%=*}" "${p#*=}" || return 1
|
|
1713
|
+
done
|
|
1714
|
+
}
|
|
1715
|
+
|
|
1716
|
+
# --- bsky-setup.sh ---------------------------------------------------------------------------
|
|
1717
|
+
BS_SEED=$'KEEP_ME=1\nBSKY_HANDLE=old.bsky.social\nBSKY_APP_PASSWORD=old-app-pass\nOTHER=2'
|
|
1718
|
+
WE_BASE=("BSKY_HANDLE=$BSKY_HANDLE_FIX" "BSKY_APP_PASSWORD=$BSKY_PW_FIX")
|
|
1719
|
+
BS_VARS=(BSKY_HANDLE BSKY_APP_PASSWORD)
|
|
1720
|
+
for var_ in "${BS_VARS[@]}"; do
|
|
1721
|
+
for cls_ in "${WE_CLASSES[@]}"; do we_run_bad "bsky-setup" "$BS_SEED" "$BSKY_SETUP" "$var_" "$cls_" write-env; done
|
|
1722
|
+
done
|
|
1723
|
+
LONG_HANDLE="a-long-handle.example-pds.social"
|
|
1724
|
+
BS_PW2="abcd-efgh_IJKL.mnop+/=:%"
|
|
1725
|
+
printf '%s\n' "$BS_SEED" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
1726
|
+
RUN_CWD="$GITWORK" run_sut - "" "BSKY_HANDLE=$LONG_HANDLE" "BSKY_APP_PASSWORD=$BS_PW2" -- bash "$BSKY_SETUP" write-env
|
|
1727
|
+
check "bsky-setup write-env round trip: exit 0, no marker, mode 600 (an existing mode 644 file is tightened)" \
|
|
1728
|
+
bash -c '[[ "$1" -eq 0 && "$2" != *SOLEUR_CREDENTIAL_REFUSED* ]]' _ "$RC" "$ERR"
|
|
1729
|
+
check "bsky-setup write-env round trip: .env is mode 600" we_mode_600
|
|
1730
|
+
check "bsky-setup write-env round trip: sourcing .env yields both values byte-for-byte, once each, and the unrelated line survives" \
|
|
1731
|
+
we_values_ok "BSKY_HANDLE=$LONG_HANDLE" "BSKY_APP_PASSWORD=$BS_PW2"
|
|
1732
|
+
# The confirmation is human text of a command with no stdout payload: STDOUT, like the refusal line.
|
|
1733
|
+
we_wrote_stdout() { # <N>: exactly one "Wrote N variables to <path> (permissions: 600)" line on stdout, nothing on stderr
|
|
1734
|
+
[[ "$OUT" == "Wrote $1 variables to "*" (permissions: 600)" && "$(wc -l <<<"$OUT")" -eq 1 && -z "$ERR" ]]
|
|
1735
|
+
}
|
|
1736
|
+
check "bsky-setup write-env round trip: the 'Wrote 2 variables' confirmation is the one line on stdout and stderr is empty" we_wrote_stdout 2
|
|
1737
|
+
# Missing credentials: also stdout (write-env has no stdout payload), exit 1, and NO marker (the marker is the allow-list's alone).
|
|
1738
|
+
we_missing_stdout() { # <expected-missing-credential-name>
|
|
1739
|
+
[[ "$RC" -eq 1 && "$OUT" == *"Error: Missing "*"credentials: "*"$1"* && "$OUT" == *"To configure:"* \
|
|
1740
|
+
&& -z "$ERR" && "$OUT$ERR" != *SOLEUR_CREDENTIAL_REFUSED* ]]
|
|
1741
|
+
}
|
|
1742
|
+
printf '%s\n' "$BS_SEED" > "$GITWORK/.env"; cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
1743
|
+
RUN_CWD="$GITWORK" run_sut - "" "BSKY_HANDLE=$BSKY_HANDLE_FIX" -- bash "$BSKY_SETUP" write-env
|
|
1744
|
+
check "bsky-setup write-env with BSKY_APP_PASSWORD missing: exit 1, the diagnostic names it on STDOUT, stderr empty, no marker" we_missing_stdout BSKY_APP_PASSWORD
|
|
1745
|
+
check "bsky-setup write-env with a credential missing: the existing .env is left byte-identical" cmp -s "$SANDBOX/env.before" "$GITWORK/.env"
|
|
1746
|
+
|
|
1747
|
+
# --- x-setup.sh -------------------------------------------------------------------------------
|
|
1748
|
+
XS_SEED=$'KEEP_ME=1\nX_API_KEY=old\nX_API_SECRET=old\nX_ACCESS_TOKEN=old\nX_ACCESS_TOKEN_SECRET=old\nOTHER=2'
|
|
1749
|
+
WE_BASE=("X_API_KEY=$X_KEY" "X_API_SECRET=$X_SECRET" "X_ACCESS_TOKEN=$X_TOK" "X_ACCESS_TOKEN_SECRET=$X_TOKSECRET")
|
|
1750
|
+
XS_VARS=(X_API_KEY X_API_SECRET X_ACCESS_TOKEN X_ACCESS_TOKEN_SECRET)
|
|
1751
|
+
for var_ in "${XS_VARS[@]}"; do
|
|
1752
|
+
for cls_ in "${WE_CLASSES[@]}"; do we_run_bad "x-setup" "$XS_SEED" "$X_SETUP" "$var_" "$cls_" write-env; done
|
|
1753
|
+
done
|
|
1754
|
+
XS_K="Ab12Cd34Ef56Gh78Ij90Kl12M"; XS_S="aB3+/=:%xYz-Q.w_9ZkLmNoPqRsTuVwXyZ0123456789a"; XS_T="1234567890123456789-AbCdEfGhIjKlMnOpQrStUvWxYz"; XS_TS="zZyYxXwWvVuUtTsSrRqQpPoOnNmMlLkKjJiIhH"
|
|
1755
|
+
printf '%s\n' "$XS_SEED" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
1756
|
+
RUN_CWD="$GITWORK" run_sut - "" "X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" "X_ACCESS_TOKEN_SECRET=$XS_TS" -- bash "$X_SETUP" write-env
|
|
1757
|
+
check "x-setup write-env round trip: exit 0, no marker" bash -c '[[ "$1" -eq 0 && "$2" != *SOLEUR_CREDENTIAL_REFUSED* ]]' _ "$RC" "$ERR"
|
|
1758
|
+
check "x-setup write-env round trip: .env is mode 600" we_mode_600
|
|
1759
|
+
check "x-setup write-env round trip: sourcing .env yields all four values byte-for-byte (a secret holding + / = : %), once each, and the unrelated line survives" \
|
|
1760
|
+
we_values_ok "X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" "X_ACCESS_TOKEN_SECRET=$XS_TS"
|
|
1761
|
+
check "x-setup write-env round trip: the 'Wrote 4 variables' confirmation is the one line on stdout and stderr is empty" we_wrote_stdout 4
|
|
1762
|
+
printf '%s\n' "$XS_SEED" > "$GITWORK/.env"; cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
1763
|
+
RUN_CWD="$GITWORK" run_sut - "" "X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" -- bash "$X_SETUP" write-env
|
|
1764
|
+
check "x-setup write-env with X_ACCESS_TOKEN_SECRET missing: exit 1, the diagnostic names it on STDOUT, stderr empty, no marker" we_missing_stdout X_ACCESS_TOKEN_SECRET
|
|
1765
|
+
check "x-setup write-env with a credential missing: the existing .env is left byte-identical" cmp -s "$SANDBOX/env.before" "$GITWORK/.env"
|
|
1766
|
+
|
|
1767
|
+
# --- linkedin-setup.sh -----------------------------------------------------------------------
|
|
1768
|
+
LI_WE_SEED=$'KEEP_ME=1\nLINKEDIN_CLIENT_ID=old\nLINKEDIN_CLIENT_SECRET=old\nLINKEDIN_ACCESS_TOKEN=old\nLINKEDIN_PERSON_URN=old\nOTHER=2'
|
|
1769
|
+
LI_WE_ID="synthetic-fixture-client-0001"; LI_WE_SECRET="aB3+/=:%xYz-Q.w_9ZkLmNoPq"; LI_WE_TOK="AQV-synthetic_token.0001+/="; LI_WE_URN="urn:li:person:SyNtH0001"
|
|
1770
|
+
WE_BASE=("LINKEDIN_CLIENT_ID=$LI_WE_ID" "LINKEDIN_CLIENT_SECRET=$LI_WE_SECRET" "LINKEDIN_ACCESS_TOKEN=$LI_WE_TOK" "LINKEDIN_PERSON_URN=$LI_WE_URN")
|
|
1771
|
+
LI_VARS=(LINKEDIN_CLIENT_ID LINKEDIN_CLIENT_SECRET LINKEDIN_ACCESS_TOKEN LINKEDIN_PERSON_URN)
|
|
1772
|
+
for var_ in "${LI_VARS[@]}"; do
|
|
1773
|
+
for cls_ in "${WE_CLASSES[@]}"; do we_run_bad "linkedin-setup" "$LI_WE_SEED" "$LINKEDIN_SETUP" "$var_" "$cls_" write-env; done
|
|
1774
|
+
done
|
|
1775
|
+
printf '%s\n' "$LI_WE_SEED" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
1776
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_BASE[@]}" -- bash "$LINKEDIN_SETUP" write-env
|
|
1777
|
+
check "linkedin-setup write-env round trip: exit 0, no marker" bash -c '[[ "$1" -eq 0 && "$2" != *SOLEUR_CREDENTIAL_REFUSED* ]]' _ "$RC" "$ERR"
|
|
1778
|
+
check "linkedin-setup write-env round trip: .env is mode 600" we_mode_600
|
|
1779
|
+
check "linkedin-setup write-env round trip: sourcing .env yields all four values byte-for-byte (a secret holding + / = : %), once each, and the unrelated line survives" \
|
|
1780
|
+
we_values_ok "LINKEDIN_CLIENT_ID=$LI_WE_ID" "LINKEDIN_CLIENT_SECRET=$LI_WE_SECRET" "LINKEDIN_ACCESS_TOKEN=$LI_WE_TOK" "LINKEDIN_PERSON_URN=$LI_WE_URN"
|
|
1781
|
+
|
|
1782
|
+
# --- discord-setup.sh -------------------------------------------------------------------------
|
|
1783
|
+
DW_URL="https://discord.com/api/webhooks/123456789012345678/SYNTHwebhook-token_0001"
|
|
1784
|
+
DW_REL="https://discord.com/api/webhooks/223456789012345678/SYNTHreleases-token_0002"
|
|
1785
|
+
DW_BLOG="https://discord.com/api/webhooks/323456789012345678/SYNTHblog-token_0003"
|
|
1786
|
+
DS_SEED=$'KEEP_ME=1\nDISCORD_BOT_TOKEN=old\nDISCORD_GUILD_ID=1\nDISCORD_WEBHOOK_URL=old\nDISCORD_RELEASES_WEBHOOK_URL=old\nDISCORD_BLOG_WEBHOOK_URL=old\nOTHER=2'
|
|
1787
|
+
WE_BASE=("DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" "DISCORD_RELEASES_WEBHOOK_URL_INPUT=$DW_REL" "DISCORD_BLOG_WEBHOOK_URL_INPUT=$DW_BLOG")
|
|
1788
|
+
DS_VARS=(DISCORD_BOT_TOKEN_INPUT DISCORD_WEBHOOK_URL_INPUT DISCORD_RELEASES_WEBHOOK_URL_INPUT DISCORD_BLOG_WEBHOOK_URL_INPUT)
|
|
1789
|
+
for var_ in "${DS_VARS[@]}"; do
|
|
1790
|
+
for cls_ in "${WE_CLASSES[@]}"; do we_run_bad "discord-setup" "$DS_SEED" "$DISCORD_SETUP" "$var_" "$cls_" write-env "$GUILD"; done
|
|
1791
|
+
done
|
|
1792
|
+
# The floor counts DISTINCT classes (sort -u), not array elements or words: eight copies of one class,
|
|
1793
|
+
# or a list cut to a few, must not satisfy it. The required names are asserted present on top of the count.
|
|
1794
|
+
we_distinct_classes() { printf '%s\n' "$@" | sort -u | wc -l; }
|
|
1795
|
+
we_has_all_required() { local want c; for want in "${WE_REQUIRED_CLASSES[@]}"; do for c in "$@"; do [[ "$c" == "$want" ]] && continue 2; done; return 1; done; }
|
|
1796
|
+
check "write-env hostile rows: the matrix has at least 25 DISTINCT classes (a shrunk or duplicated class list must fail)" test "$(we_distinct_classes "${WE_CLASSES[@]}")" -ge 25
|
|
1797
|
+
check "write-env hostile rows: every required class (parentheses, backslash, tab, CR, quote, dollar, ...) is present by name" we_has_all_required "${WE_CLASSES[@]}"
|
|
1798
|
+
we_distinct_is() { [[ "$(we_distinct_classes "${@:2}")" -eq "$1" ]]; }
|
|
1799
|
+
check "write-env hostile rows: the distinct counter counts three copies of one class as 1" we_distinct_is 1 quote quote quote
|
|
1800
|
+
check "write-env hostile rows: the distinct counter counts one duplicate among four classes as 3" we_distinct_is 3 quote quote semi pipe
|
|
1801
|
+
check "write-env hostile rows: the required-name check discriminates (a list missing lparen is refused)" \
|
|
1802
|
+
not we_has_all_required cmdsub backtick nlassign dquote semi space tilde rparen backslash tab cr squote dollar
|
|
1803
|
+
check "write-env hostile rows: the inner counter saw every run (${#BS_VARS[@]} + ${#XS_VARS[@]} + ${#LI_VARS[@]} + ${#DS_VARS[@]} variables x ${#WE_CLASSES[@]} classes)" \
|
|
1804
|
+
test "$WE_RUNS" -eq "$(( (${#BS_VARS[@]} + ${#XS_VARS[@]} + ${#LI_VARS[@]} + ${#DS_VARS[@]}) * ${#WE_CLASSES[@]} ))"
|
|
1805
|
+
printf '%s\n' "$DS_SEED" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
1806
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_BASE[@]}" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1807
|
+
check "discord-setup write-env round trip (webhook URL from the environment): exit 0, no marker" \
|
|
1808
|
+
bash -c '[[ "$1" -eq 0 && "$2" != *SOLEUR_CREDENTIAL_REFUSED* ]]' _ "$RC" "$ERR"
|
|
1809
|
+
check "discord-setup write-env round trip: the 'Wrote 5 variables' confirmation is the one line on stdout and stderr is empty" we_wrote_stdout 5
|
|
1810
|
+
check "discord-setup write-env round trip: .env is mode 600" we_mode_600
|
|
1811
|
+
check "discord-setup write-env round trip: all five values byte-for-byte, once each, and the unrelated line survives" \
|
|
1812
|
+
we_values_ok "DISCORD_BOT_TOKEN=$DISC_TOK" "DISCORD_GUILD_ID=$GUILD" "DISCORD_WEBHOOK_URL=$DW_URL" "DISCORD_RELEASES_WEBHOOK_URL=$DW_REL" "DISCORD_BLOG_WEBHOOK_URL=$DW_BLOG"
|
|
1813
|
+
|
|
1814
|
+
echo "== discord-setup.sh write-env: the webhook URL moves off the command line =="
|
|
1815
|
+
dw_untouched_run() { # <env...> -- <cmd...>: seeds a .env, runs, leaves RC/OUT/ERR
|
|
1816
|
+
printf "%s\n" "$DS_SEED" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
1817
|
+
cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
1818
|
+
RUN_CWD="$GITWORK" run_sut - "" "$@"
|
|
1819
|
+
}
|
|
1820
|
+
dw_no_secret() { [[ "$OUT$ERR" != *"$DW_URL"* && "$OUT$ERR" != *SYNTHwebhook-token_0001* ]]; }
|
|
1821
|
+
dw_no_marker() { [[ "$(grep -c SOLEUR_CREDENTIAL_REFUSED <<<"$ERR")" == 0 ]]; }
|
|
1822
|
+
dw_names_var() { [[ "$OUT" == *"Error: DISCORD_WEBHOOK_URL_INPUT is not set"* ]]; }
|
|
1823
|
+
# dw_devnull_run <env...> -- <cmd...>: like dw_untouched_run but with stdin from /dev/null, so it is certainly not a terminal.
|
|
1824
|
+
dw_devnull_run() {
|
|
1825
|
+
local o="$SANDBOX/out2" e="$SANDBOX/err2"
|
|
1826
|
+
local -a envs=()
|
|
1827
|
+
while [[ "$1" != "--" ]]; do envs+=("$1"); shift; done
|
|
1828
|
+
shift
|
|
1829
|
+
printf "%s\n" "$DS_SEED" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
1830
|
+
cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
1831
|
+
( cd "$GITWORK" && env -i PATH="$MOCK:$SYS_PATH" HOME="$SANDBOX/home" MOCK_DIR="$MOCK" "${envs[@]}" "$@" ) < /dev/null > "$o" 2> "$e"
|
|
1832
|
+
RC=$?
|
|
1833
|
+
OUT="$(cat "$o")"
|
|
1834
|
+
ERR="$(cat "$e")"
|
|
1835
|
+
}
|
|
1836
|
+
# The non-TTY remedy keeps every value out of the COMMAND TEXT (the transcript, the shell history, a
|
|
1837
|
+
# wrapper's argument list): the values are read from mode-600 files. An inline `VAR=value command`, and
|
|
1838
|
+
# any `printf '%s' "$URL" |` form (the URL has to come from somewhere typed into a command), is NOT offered.
|
|
1839
|
+
DW_FILEFORM='read -r DISCORD_WEBHOOK_URL_INPUT < /path/to/webhook-url.txt; read -r DISCORD_BOT_TOKEN_INPUT < /path/to/bot-token.txt; export DISCORD_WEBHOOK_URL_INPUT DISCORD_BOT_TOKEN_INPUT; discord-setup.sh write-env <guild_id>'
|
|
1840
|
+
DW_INLINE_RE='(^|[[:space:]])DISCORD_[A-Z_]+_INPUT=[^[:space:]]'
|
|
1841
|
+
# dw_no_inline: no `DISCORD_*_INPUT=<something>` word anywhere in the output, and no printf/$URL pipe form.
|
|
1842
|
+
dw_no_inline() { [[ ! "$OUT" =~ $DW_INLINE_RE ]] && [[ "$OUT" != *printf* && "$OUT" != *'$URL'* ]]; }
|
|
1843
|
+
# dw_names_token: the remedy names the bot token too (a missing webhook plus a missing token must not give a second bare exit 64).
|
|
1844
|
+
dw_names_token() { [[ "$OUT" == *"read -rs DISCORD_BOT_TOKEN_INPUT; export DISCORD_BOT_TOKEN_INPUT"* ]]; }
|
|
1845
|
+
# The non-TTY remedy, one predicate per conjunct so a mutant can be driven to break exactly one of them
|
|
1846
|
+
# (dw_failed names the failing ones). The three recipes added for the value-in-command-text and
|
|
1847
|
+
# lingering-export findings: the mode-first file recipe for BOTH files, the chmod note for an existing
|
|
1848
|
+
# file, the bot token supplied to create-webhook, and both secrets confined to a subshell.
|
|
1849
|
+
dw_c_fileform() { [[ "$OUT" == *"$DW_FILEFORM"* ]]; }
|
|
1850
|
+
dw_c_nontty() { [[ "$OUT" == *"stdin is not a terminal"* ]]; }
|
|
1851
|
+
dw_c_createwh() { [[ "$OUT" == *"create-webhook <channel_id> > /path/to/webhook-url.txt"* ]]; }
|
|
1852
|
+
dw_c_umask_hook() { [[ "$OUT" == *"(umask 077; cat > /path/to/webhook-url.txt)"* ]]; }
|
|
1853
|
+
dw_c_umask_token() { [[ "$OUT" == *"(umask 077; cat > /path/to/bot-token.txt)"* ]]; }
|
|
1854
|
+
dw_c_chmod() { [[ "$OUT" == *"run chmod 600 on it before writing"* ]]; }
|
|
1855
|
+
dw_c_cw_token() { [[ "$OUT" == *"read -r DISCORD_BOT_TOKEN_INPUT < /path/to/bot-token.txt; export DISCORD_BOT_TOKEN_INPUT; umask 077; discord-setup.sh create-webhook"* ]]; }
|
|
1856
|
+
dw_c_wrap_open() { [[ "$OUT" == *"( read -r DISCORD_WEBHOOK_URL_INPUT < /path/to/webhook-url.txt;"* ]]; }
|
|
1857
|
+
dw_c_wrap_close() { [[ "$OUT" == *"discord-setup.sh write-env <guild_id> )"* ]]; }
|
|
1858
|
+
DW_CONJ=(dw_c_fileform dw_c_nontty dw_c_createwh dw_c_umask_hook dw_c_umask_token dw_c_chmod dw_c_cw_token dw_c_wrap_open dw_c_wrap_close dw_names_token dw_no_inline)
|
|
1859
|
+
dw_remedy_non_tty() { local c; for c in "${DW_CONJ[@]}"; do "$c" || return 1; done; }
|
|
1860
|
+
dw_failed() { local c out=""; for c in "${DW_CONJ[@]}"; do "$c" || out+="$c "; done; printf '%s' "${out% }"; }
|
|
1861
|
+
dw_quiet_clean() { dw_no_secret && dw_untouched && dw_no_marker; }
|
|
1862
|
+
dw_untouched() { cmp -s "$SANDBOX/env.before" "$GITWORK/.env"; }
|
|
1863
|
+
dw_quiet_untouched() { dw_no_secret && dw_untouched; }
|
|
1864
|
+
dw_clean_untouched() { dw_no_marker && dw_untouched; }
|
|
1865
|
+
# (1) a second positional, environment variable unset
|
|
1866
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$DISCORD_SETUP" write-env "$GUILD" "$DW_URL"
|
|
1867
|
+
check "discord positional webhook (env unset): refused with exit 64" test "$RC" -eq 64
|
|
1868
|
+
check "discord positional webhook: the message (on stdout) names DISCORD_WEBHOOK_URL_INPUT and shows the interactive no-history form" \
|
|
1869
|
+
bash -c '[[ "$1" == *DISCORD_WEBHOOK_URL_INPUT* && "$1" == *"read -rs DISCORD_WEBHOOK_URL_INPUT"* ]]' _ "$OUT"
|
|
1870
|
+
check "discord positional webhook: stdin is not a terminal here, so the remedy ALSO gives the read-from-a-mode-600-file form for both inputs and recommends no inline VAR=value form (read -rs needs a TTY)" dw_remedy_non_tty
|
|
1871
|
+
check "discord positional webhook: the whole usage error is on stdout (nothing on stderr)" test -z "$ERR"
|
|
1872
|
+
check "discord positional webhook: the argument is never echoed (stdout and stderr)" dw_no_secret
|
|
1873
|
+
check "discord positional webhook: not the credential-shape marker (this is a usage error)" dw_no_marker
|
|
1874
|
+
check "discord positional webhook: an existing .env is left byte-identical" dw_untouched
|
|
1875
|
+
# (1a) the same, with stdin from /dev/null (no terminal, as in an agent runtime)
|
|
1876
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$DISCORD_SETUP" write-env "$GUILD" "$DW_URL"
|
|
1877
|
+
check "discord positional webhook, stdin </dev/null: exit 64 and the remedy gives the file-read form" \
|
|
1878
|
+
bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
1879
|
+
check "discord positional webhook, stdin </dev/null: the non-TTY remedy text is on stdout" dw_remedy_non_tty
|
|
1880
|
+
check "discord positional webhook, stdin </dev/null: the argument is never echoed, .env untouched" dw_quiet_untouched
|
|
1881
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1882
|
+
check "discord write-env with no webhook, stdin </dev/null: exit 64 and the non-TTY remedy (file-read form) on stdout" \
|
|
1883
|
+
bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
1884
|
+
check "discord write-env with no webhook, stdin </dev/null: the remedy text gives the file-read form for both inputs" dw_remedy_non_tty
|
|
1885
|
+
check "discord write-env with no webhook, stdin </dev/null: the remedy recommends no inline VAR=value form and no printf/\$URL pipe form" dw_no_inline
|
|
1886
|
+
check "discord write-env with no webhook, stdin </dev/null: the remedy also names DISCORD_BOT_TOKEN_INPUT (following it once does not give a second bare exit 64)" dw_names_token
|
|
1887
|
+
check "discord write-env with no webhook, stdin </dev/null: the remedy states the residual (values stay in the files until deleted; create-webhook output is redirected into the file)" \
|
|
1888
|
+
bash -c '[[ "$1" == *"Delete both files afterwards: the values stay in them until you do."* && "$1" == *"without printing the URL"* ]]' _ "$OUT"
|
|
1889
|
+
check "discord write-env with no webhook, stdin </dev/null: the usage error is on stdout and stderr is empty" test -z "$ERR"
|
|
1890
|
+
# (1b) the positional AND the variable: still refused (the secret is already on this command line)
|
|
1891
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env "$GUILD" "$DW_URL"
|
|
1892
|
+
check "discord positional webhook (env also set): still refused with exit 64" test "$RC" -eq 64
|
|
1893
|
+
check "discord positional webhook (env also set): the argument is never echoed and .env is untouched" dw_quiet_untouched
|
|
1894
|
+
# (3) neither
|
|
1895
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1896
|
+
check "discord write-env with neither the positional nor the variable: exit 64" test "$RC" -eq 64
|
|
1897
|
+
check "discord write-env with neither: the usage error names the variable" dw_names_var
|
|
1898
|
+
check "discord write-env with neither: .env untouched, no marker" dw_clean_untouched
|
|
1899
|
+
# (4) the variable set but empty
|
|
1900
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1901
|
+
check "discord write-env with an EMPTY DISCORD_WEBHOOK_URL_INPUT: exit 64" test "$RC" -eq 64
|
|
1902
|
+
check "discord write-env with an EMPTY DISCORD_WEBHOOK_URL_INPUT: the same usage error naming the variable" dw_names_var
|
|
1903
|
+
# (5) the bot token missing or empty: the same usage-error class (64) as a missing webhook, remedy on stdout
|
|
1904
|
+
dw_untouched_run "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1905
|
+
check "discord write-env with DISCORD_BOT_TOKEN_INPUT unset: exit 64 (the same class as a missing webhook), the usage error on stdout names the variable" \
|
|
1906
|
+
bash -c '[[ "$1" -eq 64 && "$2" == *DISCORD_BOT_TOKEN_INPUT* ]]' _ "$RC" "$OUT"
|
|
1907
|
+
check "discord write-env with DISCORD_BOT_TOKEN_INPUT unset: .env untouched, no marker, the webhook not echoed" dw_quiet_clean
|
|
1908
|
+
check "discord write-env with DISCORD_BOT_TOKEN_INPUT unset: the remedy is printed too (SKILL.md says both missing inputs print it) and names the token" dw_names_token
|
|
1909
|
+
dw_devnull_run "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1910
|
+
check "discord write-env with DISCORD_BOT_TOKEN_INPUT unset, stdin </dev/null: exit 64 and the non-TTY file-read remedy on stdout, no inline form" \
|
|
1911
|
+
bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
1912
|
+
check "discord write-env with DISCORD_BOT_TOKEN_INPUT unset, stdin </dev/null: the remedy gives the file-read form and recommends no inline VAR=value form" dw_remedy_non_tty
|
|
1913
|
+
# (5a) no guild_id at all: the same usage-error class (64) with the text on STDOUT, not a stderr-only `${1:?}` exit 1
|
|
1914
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env
|
|
1915
|
+
check "discord write-env with no guild_id: exit 64 (usage class), not the exit 1 of a bash parameter error" \
|
|
1916
|
+
bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
1917
|
+
check "discord write-env with no guild_id: the usage line and the error are on stdout and stderr is empty" \
|
|
1918
|
+
bash -c '[[ "$1" == *"Usage: discord-setup.sh write-env <guild_id>"* && "$1" == *"Error: guild_id is missing"* && -z "$2" ]]' _ "$OUT" "$ERR"
|
|
1919
|
+
check "discord write-env with no guild_id: .env untouched, no marker, neither secret echoed" bash -c '[[ "$1" != *"$2"* && "$1" != *"$3"* ]]' _ "$OUT$ERR" "$DW_URL" "$DISC_TOK"
|
|
1920
|
+
check "discord write-env with no guild_id: .env untouched" dw_clean_untouched
|
|
1921
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1922
|
+
check "discord write-env with an EMPTY DISCORD_BOT_TOKEN_INPUT: exit 64 and the same usage error" \
|
|
1923
|
+
bash -c '[[ "$1" -eq 64 && "$2" == *DISCORD_BOT_TOKEN_INPUT* ]]' _ "$RC" "$OUT"
|
|
1924
|
+
# (6) the webhook URL in the guild_id slot: refused as non-numeric WITHOUT echoing it
|
|
1925
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env "$DW_URL"
|
|
1926
|
+
check "discord write-env with the webhook URL as the sole positional (the guild_id slot): exit 1, 'must be numeric'" \
|
|
1927
|
+
bash -c '[[ "$1" -eq 1 && "$2" == *"guild_id must be numeric"* ]]' _ "$RC" "$ERR"
|
|
1928
|
+
check "discord write-env with a non-numeric guild_id: the one line is on stderr and stdout is empty (the real stream the header and SKILL.md state)" \
|
|
1929
|
+
bash -c '[[ -z "$1" && "$(wc -l <<<"$2")" -eq 1 ]]' _ "$OUT" "$ERR"
|
|
1930
|
+
check "discord write-env with the webhook URL as the guild_id: the URL is on neither stream, .env untouched" dw_quiet_untouched
|
|
1931
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$DISCORD_SETUP" list-channels "$DW_URL"
|
|
1932
|
+
check "discord list-channels with the webhook URL as the guild_id: exit 1 and the URL is on neither stream, curl never invoked" \
|
|
1933
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
1934
|
+
check "discord list-channels with the webhook URL as the guild_id: the URL is not echoed" dw_no_secret
|
|
1935
|
+
# (2) the repair path: the variable, no positional -> written, mode 600 (a hostile value on this path is refused by the hostile rows above)
|
|
1936
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
1937
|
+
check "discord write-env repair path (variable, no positional): exit 0" test "$RC" -eq 0
|
|
1938
|
+
check "discord write-env repair path: the URL lands in .env byte-for-byte" we_value_is DISCORD_WEBHOOK_URL "$DW_URL"
|
|
1939
|
+
check "discord write-env repair path: .env is mode 600" we_mode_600
|
|
1940
|
+
check "discord write-env repair path: the URL is not echoed" dw_no_secret
|
|
1941
|
+
|
|
1942
|
+
# --- population manifest: every cmd_write_env writer is classified -------------------------------
|
|
1943
|
+
echo "== write-env writers: population manifest =="
|
|
1944
|
+
WRITERS="$(cd "$HERE" && grep -lE '^cmd_write_env\(\)' ./*-setup.sh | sed 's|^\./||' | sort | paste -sd ' ' -)"
|
|
1945
|
+
check "population: the cmd_write_env writers derived by grep are exactly the four classified ones" \
|
|
1946
|
+
test "$WRITERS" = "bsky-setup.sh discord-setup.sh linkedin-setup.sh x-setup.sh"
|
|
1947
|
+
check "population: the derived writer set is non-empty (anti-vacuity: four writers, no more, no fewer)" test "$(wc -w <<<"$WRITERS")" -eq 4
|
|
1948
|
+
for w_ in bsky-setup.sh discord-setup.sh x-setup.sh; do
|
|
1949
|
+
# validated: the validator is called inside cmd_write_env BEFORE its first mutation of the file
|
|
1950
|
+
check "population: $w_ validates every value before the first temp-file, touch or write in cmd_write_env" \
|
|
1951
|
+
awk 'BEGIN{v=0;m=0} /^cmd_write_env\(\) \{/{f=1} f&&/_wenv_validate /&&!v{v=NR} f&&/_wenv_write |mktemp|touch "\$env_file"|>> "\$env_file"/&&!m{m=NR} f&&/^}/{exit} END{exit !(v&&m&&v<m)}' "$HERE/$w_"
|
|
1952
|
+
done
|
|
1953
|
+
check "population: linkedin-setup.sh validates every value before the first temp-file, touch or write in cmd_write_env (no deferred writer is left)" \
|
|
1954
|
+
awk 'BEGIN{v=0;m=0} /^cmd_write_env\(\) \{/{f=1} f&&/_wenv_validate /&&!v{v=NR} f&&/_wenv_write |mktemp|touch "\$env_file"|>> "\$env_file"/&&!m{m=NR} f&&/^}/{exit} END{exit !(v&&m&&v<m)}' "$HERE/linkedin-setup.sh"
|
|
1955
|
+
check "population: linkedin-setup.sh names all four variables cmd_write_env writes in its _wenv_validate call" \
|
|
1956
|
+
grep -qE '^ _wenv_validate LINKEDIN_CLIENT_ID LINKEDIN_CLIENT_SECRET LINKEDIN_ACCESS_TOKEN LINKEDIN_PERSON_URN$' "$HERE/linkedin-setup.sh"
|
|
1957
|
+
|
|
1958
|
+
# --- the four _wenv blocks are one block ---------------------------------------------------------
|
|
1959
|
+
echo "== write-env allow-list: the four copies are byte-identical =="
|
|
1960
|
+
wenv_block() { # <file>: from `_wenv_class() {` through the closing brace of `_wenv_validate`
|
|
1961
|
+
awk '/^_wenv_class\(\) \{/{f=1} f{print} f&&/^_wenv_validate\(\) \{/{v=1} v&&/^}$/{exit}' "$1"
|
|
1962
|
+
}
|
|
1963
|
+
WBLK="$SANDBOX/wenvblk"
|
|
1964
|
+
mkdir -p "$WBLK"
|
|
1965
|
+
wenv_sane() { # <block-file>: non-empty, whole, and long enough not to be a truncated extraction
|
|
1966
|
+
[[ -s "$1" ]] && [[ "$(wc -l < "$1")" -ge 25 ]] && grep -qxF '_wenv_validate() {' "$1" \
|
|
1967
|
+
&& grep -qF '*[!A-Za-z0-9._:/@%+=,-]*) return 1 ;;' "$1" && grep -qF 'phase=write-env' "$1"
|
|
1968
|
+
}
|
|
1969
|
+
for w_ in bsky-setup.sh discord-setup.sh x-setup.sh linkedin-setup.sh; do
|
|
1970
|
+
wenv_block "$HERE/$w_" > "$WBLK/$w_.blk"
|
|
1971
|
+
check "wenv drift: the $w_ _wenv block extracted whole (non-vacuous)" wenv_sane "$WBLK/$w_.blk"
|
|
1972
|
+
done
|
|
1973
|
+
check "wenv drift: discord-setup.sh's _wenv block is byte-identical to bsky-setup.sh's" cmp -s "$WBLK/bsky-setup.sh.blk" "$WBLK/discord-setup.sh.blk"
|
|
1974
|
+
check "wenv drift: x-setup.sh's _wenv block is byte-identical to bsky-setup.sh's" cmp -s "$WBLK/bsky-setup.sh.blk" "$WBLK/x-setup.sh.blk"
|
|
1975
|
+
check "wenv drift: linkedin-setup.sh's _wenv block is byte-identical to bsky-setup.sh's" cmp -s "$WBLK/bsky-setup.sh.blk" "$WBLK/linkedin-setup.sh.blk"
|
|
1976
|
+
# Mutation: one character widened in ONE copy turns the comparison RED (and the mutation landed).
|
|
1977
|
+
M="$(mut_make discord-setup.sh '[!A-Za-z0-9._:/@%+=,-]' '[!A-Za-z0-9._:/@%+=,*-]')"
|
|
1978
|
+
wenv_block "$M" > "$WBLK/discord-mut.blk"
|
|
1979
|
+
check "wenv drift: the widening mutation landed in the discord-setup.sh sandbox copy" not cmp -s "$HERE/discord-setup.sh" "$M"
|
|
1980
|
+
check "wenv drift: a copy whose allow-list was widened by one character is RED against the others" not cmp -s "$WBLK/bsky-setup.sh.blk" "$WBLK/discord-mut.blk"
|
|
1981
|
+
M="$(mut_make x-setup.sh "echo \"Error: \${2} holds" "echo \"Error: \${2} holdz")"
|
|
1982
|
+
wenv_block "$M" > "$WBLK/x-mut.blk"
|
|
1983
|
+
check "wenv drift: a one-character edit to the human line of x-setup.sh's copy is RED against the others" not cmp -s "$WBLK/bsky-setup.sh.blk" "$WBLK/x-mut.blk"
|
|
1984
|
+
|
|
1985
|
+
# --- Guard 4 (#10091): _wenv_write is one helper, byte-identical, atomic, serialized, mode 600 from birth -------
|
|
1986
|
+
echo "== write-env commit helper _wenv_write: four copies byte-identical =="
|
|
1987
|
+
wenvw_block() { # <file>: from the block's FIRST comment line (so the preamble, the constants and the state-variable initialisation are inside the pin) through the closing brace of `_wenv_write`
|
|
1988
|
+
awk '/^# \(#10091\) write-env commit: ONE helper/{f=1} f{print} f&&/^_wenv_write\(\) \{/{v=1} v&&/^}$/{exit}' "$1"
|
|
1989
|
+
}
|
|
1990
|
+
wenvw_sane() { # <block-file>: non-empty, whole, long enough not to be a truncated extraction, anchors present
|
|
1991
|
+
[[ -s "$1" ]] && [[ "$(wc -l < "$1")" -ge 90 ]] && grep -qxF '_wenv_write() {' "$1" && grep -q '^_wenv_unlock() {' "$1" \
|
|
1992
|
+
&& grep -qxF '_WENV_LOCK="" _WENV_TMP="" _WENV_REC=""' "$1" && grep -q '^_WENV_TICKS_MAX=' "$1" \
|
|
1993
|
+
&& grep -qF 'readlink -f' "$1" && grep -qF 'mkdir -- "$_wl"' "$1" && grep -qF 'mv -f --' "$1" \
|
|
1994
|
+
&& grep -qF 'LC_ALL=C grep -a -v -E' "$1" && grep -qF 'umask 077' "$1" && grep -qF 'trap - EXIT INT TERM HUP' "$1"
|
|
1995
|
+
}
|
|
1996
|
+
for w_ in bsky-setup.sh discord-setup.sh x-setup.sh linkedin-setup.sh; do
|
|
1997
|
+
wenvw_block "$HERE/$w_" > "$WBLK/$w_.wblk"
|
|
1998
|
+
check "wenv_write drift: the $w_ _wenv_write block extracted whole (non-empty, at least 90 lines, starts at the first comment line, every anchor present incl. the state-variable init line)" wenvw_sane "$WBLK/$w_.wblk"
|
|
1999
|
+
done
|
|
2000
|
+
check "wenv_write drift: discord-setup.sh's _wenv_write block is byte-identical to bsky-setup.sh's" cmp -s "$WBLK/bsky-setup.sh.wblk" "$WBLK/discord-setup.sh.wblk"
|
|
2001
|
+
check "wenv_write drift: x-setup.sh's _wenv_write block is byte-identical to bsky-setup.sh's" cmp -s "$WBLK/bsky-setup.sh.wblk" "$WBLK/x-setup.sh.wblk"
|
|
2002
|
+
check "wenv_write drift: linkedin-setup.sh's _wenv_write block is byte-identical to bsky-setup.sh's" cmp -s "$WBLK/bsky-setup.sh.wblk" "$WBLK/linkedin-setup.sh.wblk"
|
|
2003
|
+
wenvw_after_validate() { # <file>: the helper (its first comment line, then _wenv_unlock) follows _wenv_validate's closing brace and precedes cmd_write_env
|
|
2004
|
+
local v b u c
|
|
2005
|
+
v="$(grep -n '^_wenv_validate() {' "$1" | head -1 | cut -d: -f1)"; u="$(grep -n '^_wenv_unlock() {' "$1" | head -1 | cut -d: -f1)"
|
|
2006
|
+
b="$(grep -n '^# (#10091) write-env commit: ONE helper' "$1" | head -1 | cut -d: -f1)"
|
|
2007
|
+
c="$(grep -n '^cmd_write_env() {' "$1" | head -1 | cut -d: -f1)"
|
|
2008
|
+
[[ -n "$v" && -n "$b" && -n "$u" && -n "$c" ]] && (( v < b && b < u && u < c )) && [[ "$(sed -n "$((v + 1)),$((u - 1))p" "$1" | grep -c '^[A-Za-z_]*() {')" == 0 ]]
|
|
2009
|
+
}
|
|
2010
|
+
for w_ in bsky-setup.sh discord-setup.sh x-setup.sh linkedin-setup.sh; do
|
|
2011
|
+
check "wenv_write drift: $w_ places the helper after _wenv_validate and before cmd_write_env, with no other function between" wenvw_after_validate "$HERE/$w_"
|
|
2012
|
+
done
|
|
2013
|
+
M="$(mut_make x-setup.sh '_WENV_TICKS_MAX=1000' '_WENV_TICKS_MAX=10000')"
|
|
2014
|
+
wenvw_block "$M" > "$WBLK/x-wmut.blk"
|
|
2015
|
+
check "wenv_write drift: the widening mutation landed in the x-setup.sh sandbox copy" bash -c '[[ -s "$2" ]] && ! cmp -s "$1" "$2"' _ "$HERE/x-setup.sh" "$M"
|
|
2016
|
+
check "wenv_write drift: the widened copy's extraction is still whole (so the RED below is the drift, not a broken extraction)" wenvw_sane "$WBLK/x-wmut.blk"
|
|
2017
|
+
check "wenv_write drift: a copy widened in ONE place is RED against the others" not cmp -s "$WBLK/bsky-setup.sh.wblk" "$WBLK/x-wmut.blk"
|
|
2018
|
+
# The state-variable initialisation line sits above `_wenv_unlock() {`: deleting it from ONE copy must be RED (it was outside
|
|
2019
|
+
# the pin: the helper then died on an unbound variable in its EXIT trap on every early failure path).
|
|
2020
|
+
M="$(mut_make x-setup.sh '_WENV_LOCK="" _WENV_TMP="" _WENV_REC=""' '')"
|
|
2021
|
+
wenvw_block "$M" > "$WBLK/x-initmut.blk"
|
|
2022
|
+
check "wenv_write drift: the init-line deletion landed in the x-setup.sh sandbox copy" bash -c '[[ -s "$2" ]] && ! cmp -s "$1" "$2"' _ "$HERE/x-setup.sh" "$M"
|
|
2023
|
+
check "wenv_write drift: a copy that LOST the state-variable init line is RED against the others (byte pin)" not cmp -s "$WBLK/bsky-setup.sh.wblk" "$WBLK/x-initmut.blk"
|
|
2024
|
+
check "wenv_write drift: a copy that LOST the state-variable init line fails the extraction-sanity anchors" not wenvw_sane "$WBLK/x-initmut.blk"
|
|
2025
|
+
M="$(mut_make x-setup.sh 'byte-identical in the four setup scripts (a drift row pins' 'byte-identical in the four setup scripts (a drift row pin')"
|
|
2026
|
+
wenvw_block "$M" > "$WBLK/x-hdrmut.blk"
|
|
2027
|
+
check "wenv_write drift: an edit to the block's FIRST comment line is RED against the others (the pin starts at the comment, not at the function)" \
|
|
2028
|
+
bash -c '[[ -s "$2" ]] && ! cmp -s "$1" "$2"' _ "$WBLK/bsky-setup.sh.wblk" "$WBLK/x-hdrmut.blk"
|
|
2029
|
+
check "wenv_write drift: an empty extraction is not accepted as sane (the four-way compare cannot go trivially green)" not wenvw_sane /dev/null
|
|
2030
|
+
check "no flock: no executable line of the _wenv_write block names flock or opens a descriptor on the lock (one mkdir mechanism on every platform)" \
|
|
2031
|
+
bash -c '! grep -qE "flock|exec [{0-9]" < <(grep -v "^[[:space:]]*#" "$1")' _ "$WBLK/bsky-setup.sh.wblk"
|
|
2032
|
+
|
|
2033
|
+
echo "== write-env commit helper _wenv_write: behaviour =="
|
|
2034
|
+
WED="$SANDBOX/guard4"; mkdir -p "$WED/shim"
|
|
2035
|
+
G4_REAL_GREP="$(PATH="$SYS_PATH" command -v grep)"; G4_REAL_MV="$(PATH="$SYS_PATH" command -v mv)"
|
|
2036
|
+
G4_REAL_MKTEMP="$(PATH="$SYS_PATH" command -v mktemp)"; G4_REAL_MKDIR="$(PATH="$SYS_PATH" command -v mkdir)"
|
|
2037
|
+
G4_REAL_RM="$(PATH="$SYS_PATH" command -v rm)"; G4_REAL_SLEEP="$(PATH="$SYS_PATH" command -v sleep)"
|
|
2038
|
+
# Pass-through shims that RECORD (and, when told, stall or fail). They sit in a per-run PATH directory, never in $MOCK.
|
|
2039
|
+
cat > "$WED/shim/grep" <<'SHIM'
|
|
2040
|
+
#!/bin/bash
|
|
2041
|
+
[[ -n "${WE_GREP_RC:-}" ]] && exit "$WE_GREP_RC"
|
|
2042
|
+
[[ -n "${WE_LOG:-}" ]] && printf 'S %s\n' "${WE_TAG:-?}" >> "$WE_LOG"
|
|
2043
|
+
[[ -n "${WE_BIRTH_LOG:-}" ]] && printf 'G %s\n' "$(stat -L -c %a /proc/self/fd/1 2>/dev/null)" >> "$WE_BIRTH_LOG"
|
|
2044
|
+
[[ -n "${WE_LCLOG:-}" ]] && printf '%s\n' "${LC_ALL-unset}" >> "$WE_LCLOG"
|
|
2045
|
+
[[ -n "${WE_GREP_SLEEP:-}" ]] && "@SLEEP@" "$WE_GREP_SLEEP"
|
|
2046
|
+
"@GREP@" "$@"; rc=$?
|
|
2047
|
+
[[ -n "${WE_LOG:-}" ]] && printf 'E %s\n' "${WE_TAG:-?}" >> "$WE_LOG"
|
|
2048
|
+
exit "$rc"
|
|
2049
|
+
SHIM
|
|
2050
|
+
cat > "$WED/shim/mv" <<'SHIM'
|
|
2051
|
+
#!/bin/bash
|
|
2052
|
+
args=("$@"); n=${#args[@]}; src="${args[n-2]}"; dst="${args[n-1]}"
|
|
2053
|
+
if [[ -n "${WE_MV_LOG:-}" && "$src" != *.lock.d* && "$dst" != *.lock.d* ]]; then
|
|
2054
|
+
printf 'M %s %s %s\n' "$(dirname "$src")" "$(dirname "$dst")" "$(stat -c %a "$src" 2>/dev/null)" >> "$WE_MV_LOG"
|
|
2055
|
+
fi
|
|
2056
|
+
# The FINAL rename (the temp onto the .env): stall inside it, then log its completion in the interval log ("F <tag>").
|
|
2057
|
+
if [[ -n "${WE_FINMV_DELAY:-}" && "$src" == *.wenv.* && "$dst" != *.wenv.* ]]; then
|
|
2058
|
+
: > "$WE_FINMV_FLAG"; "@SLEEP@" "$WE_FINMV_DELAY"
|
|
2059
|
+
"@MV@" "$@"; rc=$?
|
|
2060
|
+
[[ -n "${WE_LOG:-}" ]] && printf 'F %s\n' "${WE_TAG:-?}" >> "$WE_LOG"
|
|
2061
|
+
exit "$rc"
|
|
2062
|
+
fi
|
|
2063
|
+
exec "@MV@" "$@"
|
|
2064
|
+
SHIM
|
|
2065
|
+
cat > "$WED/shim/rm" <<'SHIM'
|
|
2066
|
+
#!/bin/bash
|
|
2067
|
+
for a in "$@"; do
|
|
2068
|
+
if [[ -n "${WE_RM_DELAY:-}" && "$a" == *.lock.d && ! -e "$WE_RM_DELAY_FLAG" ]]; then : > "$WE_RM_DELAY_FLAG"; "@SLEEP@" "$WE_RM_DELAY"; fi
|
|
2069
|
+
done
|
|
2070
|
+
exec "@RM@" "$@"
|
|
2071
|
+
SHIM
|
|
2072
|
+
cat > "$WED/shim/mktemp" <<'SHIM'
|
|
2073
|
+
#!/bin/bash
|
|
2074
|
+
p="$("@MKTEMP@" "$@")" || exit $?
|
|
2075
|
+
[[ -n "${WE_BIRTH_LOG:-}" ]] && printf 'T %s\n' "$(stat -c %a "$p" 2>/dev/null)" >> "$WE_BIRTH_LOG"
|
|
2076
|
+
[[ -n "${WE_TMP_LOG:-}" ]] && printf '%s\n' "$p" >> "$WE_TMP_LOG"
|
|
2077
|
+
printf '%s\n' "$p"
|
|
2078
|
+
SHIM
|
|
2079
|
+
cat > "$WED/shim/mkdir" <<'SHIM'
|
|
2080
|
+
#!/bin/bash
|
|
2081
|
+
# Stall the FIRST attempt to take the recovery mutex (`<.env>.lock.d.recover`) when asked: the waiter has judged the lock
|
|
2082
|
+
# stale from its own snapshot and is held right before it would act on it.
|
|
2083
|
+
for a in "$@"; do
|
|
2084
|
+
if [[ -n "${WE_MKDIR_DELAY:-}" && "$a" == *.lock.d.recover && ! -e "$WE_MKDIR_DELAY_FLAG" ]]; then : > "$WE_MKDIR_DELAY_FLAG"; "@SLEEP@" "$WE_MKDIR_DELAY"; fi
|
|
2085
|
+
done
|
|
2086
|
+
"@MKDIR@" "$@"; rc=$?
|
|
2087
|
+
if [[ "$rc" -eq 0 && -n "${WE_BIRTH_LOG:-}" ]]; then
|
|
2088
|
+
for a in "$@"; do [[ "$a" == *.lock.d ]] && printf 'D %s\n' "$(stat -c %a "$a" 2>/dev/null)" >> "$WE_BIRTH_LOG"; done
|
|
2089
|
+
fi
|
|
2090
|
+
# A FAILED attempt on the lock directory itself is contention: recorded per writer ("W <tag>").
|
|
2091
|
+
if [[ "$rc" -ne 0 && -n "${WE_CONTEND_LOG:-}" ]]; then
|
|
2092
|
+
for a in "$@"; do [[ "$a" == *.lock.d ]] && printf 'W %s\n' "${WE_TAG:-?}" >> "$WE_CONTEND_LOG"; done
|
|
2093
|
+
fi
|
|
2094
|
+
exit "$rc"
|
|
2095
|
+
SHIM
|
|
2096
|
+
for s_ in grep mv rm mktemp mkdir; do
|
|
2097
|
+
sed -i -e "s|@GREP@|$G4_REAL_GREP|" -e "s|@MV@|$G4_REAL_MV|" -e "s|@MKTEMP@|$G4_REAL_MKTEMP|" \
|
|
2098
|
+
-e "s|@MKDIR@|$G4_REAL_MKDIR|" -e "s|@RM@|$G4_REAL_RM|" -e "s|@SLEEP@|$G4_REAL_SLEEP|" "$WED/shim/$s_"
|
|
2099
|
+
chmod +x "$WED/shim/$s_"
|
|
2100
|
+
done
|
|
2101
|
+
# A `sleep` that only COUNTS its calls (one byte each into $WE_SLEEPLOG) and returns at once: the wait-bound rows measure
|
|
2102
|
+
# the number of ticks the helper takes instead of waiting them out.
|
|
2103
|
+
mkdir -p "$WED/sleepshim"
|
|
2104
|
+
printf '#!/bin/sh\nprintf x >> "${WE_SLEEPLOG:-/dev/null}"\nexit 0\n' > "$WED/sleepshim/sleep"; chmod +x "$WED/sleepshim/sleep"
|
|
2105
|
+
we_and() { local c; for c in "$@"; do eval "$c" || return 1; done; }
|
|
2106
|
+
we_owner_is() { local o=""; { read -r o < "$1/.env.lock.d/owner"; } 2>/dev/null; [[ "$o" == "$2" ]]; }
|
|
2107
|
+
G4_GUILD=123456789012345678
|
|
2108
|
+
we_w_cfg() { # <bsky|x|linkedin|discord> -> WE_S (script), WE_E (env), WE_A (positional), WE_K (expected KEY=VALUE lines)
|
|
2109
|
+
WE_A=()
|
|
2110
|
+
case "$1" in
|
|
2111
|
+
bsky) WE_S="$BSKY_SETUP"; WE_E=("BSKY_HANDLE=guard4.bsky.social" "BSKY_APP_PASSWORD=g4aa-g4bb-g4cc-g4dd")
|
|
2112
|
+
WE_K=("BSKY_HANDLE=guard4.bsky.social" "BSKY_APP_PASSWORD=g4aa-g4bb-g4cc-g4dd") ;;
|
|
2113
|
+
x) WE_S="$X_SETUP"; WE_E=("X_API_KEY=g4xkey0001" "X_API_SECRET=g4xsecret+/=02" "X_ACCESS_TOKEN=g4-xtok-03" "X_ACCESS_TOKEN_SECRET=g4xtoksec.04")
|
|
2114
|
+
WE_K=("${WE_E[@]}") ;;
|
|
2115
|
+
linkedin) WE_S="$LINKEDIN_SETUP"; WE_E=("LINKEDIN_CLIENT_ID=g4liid" "LINKEDIN_CLIENT_SECRET=g4lisecret" "LINKEDIN_ACCESS_TOKEN=g4litok" "LINKEDIN_PERSON_URN=urn:li:person:G4")
|
|
2116
|
+
WE_K=("${WE_E[@]}") ;;
|
|
2117
|
+
discord) WE_S="$DISCORD_SETUP"; WE_E=("DISCORD_BOT_TOKEN_INPUT=g4disctok" "DISCORD_WEBHOOK_URL_INPUT=https://discord.com/api/webhooks/123/g4hook")
|
|
2118
|
+
WE_A=("$G4_GUILD"); WE_K=("DISCORD_BOT_TOKEN=g4disctok" "DISCORD_GUILD_ID=$G4_GUILD" "DISCORD_WEBHOOK_URL=https://discord.com/api/webhooks/123/g4hook") ;;
|
|
2119
|
+
*) echo "FATAL: unknown writer $1" >&2; exit 2 ;;
|
|
2120
|
+
esac
|
|
2121
|
+
}
|
|
2122
|
+
G4_SEED=$'KEEP_ME=1\nBSKY_HANDLE=old\nBSKY_APP_PASSWORD=old\nX_API_KEY=old\nX_API_SECRET=old\nX_ACCESS_TOKEN=old\nX_ACCESS_TOKEN_SECRET=old\nLINKEDIN_CLIENT_ID=old\nLINKEDIN_CLIENT_SECRET=old\nLINKEDIN_ACCESS_TOKEN=old\nLINKEDIN_PERSON_URN=old\nDISCORD_BOT_TOKEN=old\nDISCORD_GUILD_ID=1\nDISCORD_WEBHOOK_URL=old\nOTHER=2'
|
|
2123
|
+
we_reset() { # <seed | @none>: a clean scratch checkout state and empty logs
|
|
2124
|
+
"$G4_REAL_RM" -rf "$GITWORK/.env" "$GITWORK"/.env.tmp.* "$GITWORK"/.env.wenv.* "$GITWORK"/.env.lock.d* "$WED/real" "$WED"/*.log "$WED"/flag.* "$WED"/rc.*
|
|
2125
|
+
if [[ "$1" != @none ]]; then
|
|
2126
|
+
printf '%s\n' "$1" > "$GITWORK/.env"; chmod 600 "$GITWORK/.env"; cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
2127
|
+
fi
|
|
2128
|
+
}
|
|
2129
|
+
we_no_artifacts() { local f; for f in "$GITWORK"/.env.tmp.* "$GITWORK"/.env.wenv.* "$GITWORK"/.env.lock.d*; do [[ -e "$f" || -L "$f" ]] && return 1; done; return 0; }
|
|
2130
|
+
we_identical() { cmp -s "$SANDBOX/env.before" "$GITWORK/.env"; }
|
|
2131
|
+
we_keys_once() { # <KEY=VALUE>...: each pair is in .env exactly once, byte for byte
|
|
2132
|
+
local p
|
|
2133
|
+
for p in "$@"; do [[ "$("$G4_REAL_GREP" -cxF -- "$p" "$GITWORK/.env")" == 1 && "$("$G4_REAL_GREP" -c "^${p%%=*}=" "$GITWORK/.env")" == 1 ]] || return 1; done
|
|
2134
|
+
}
|
|
2135
|
+
we_spawn() { # <writer> <script|-> <gate|-> [NAME=value env...]: a background write-env; sets WE_PID
|
|
2136
|
+
local tag="$1" script="$2" gate="$3"; shift 3
|
|
2137
|
+
we_w_cfg "$tag"; [[ "$script" != - ]] && WE_S="$script"
|
|
2138
|
+
( if [[ "$gate" != - ]]; then while [[ ! -e "$gate" ]]; do :; done; fi
|
|
2139
|
+
cd "$GITWORK" && exec env -i PATH="$WED/shim:$MOCK:$SYS_PATH" HOME="$SANDBOX/home" MOCK_DIR="$MOCK" WE_TAG="$tag" WE_LOG="$WED/iv.log" \
|
|
2140
|
+
"${WE_E[@]}" "$@" bash "$WE_S" write-env "${WE_A[@]}" ) > "$WED/out.$tag" 2> "$WED/err.$tag" &
|
|
2141
|
+
WE_PID=$!
|
|
2142
|
+
}
|
|
2143
|
+
we_wait_for() { local i; for (( i = 0; i < 100; i++ )); do [[ -e "$1" ]] && return 0; "$G4_REAL_SLEEP" 0.1; done; return 1; }
|
|
2144
|
+
# we_alternates <log>: the S/E intervals never overlap (S, E, S, E ...) and there is at least one.
|
|
2145
|
+
we_alternates() { awk 'BEGIN{o=0;n=0} /^S /{if(o)exit 1;o=1;n++} /^E /{if(!o)exit 1;o=0} END{exit !(n>0 && !o)}' "$1"; }
|
|
2146
|
+
# we_exercised <log> <writers>: the interval log shows exactly that many DISTINCT writers each entering the critical section once.
|
|
2147
|
+
we_exercised() { [[ "$("$G4_REAL_GREP" -c '^S ' "$1")" == "$2" && "$(sed -n 's/^S //p' "$1" | sort -u | wc -l)" == "$2" ]]; }
|
|
2148
|
+
we_overlaps() { ! we_alternates "$1"; }
|
|
2149
|
+
G4_WRITERS=(bsky x linkedin discord)
|
|
2150
|
+
|
|
2151
|
+
# --- (i) parallel runs of DIFFERENT writers with a forced interleave -----------------------------------------
|
|
2152
|
+
we_parallel() { # <script-dir-prefix or -> ; runs all four released together, each stalling 0.6 s inside its critical section
|
|
2153
|
+
local w pids=() go="$WED/go" scr
|
|
2154
|
+
we_reset "$G4_SEED"; "$G4_REAL_RM" -f "$go"
|
|
2155
|
+
for w in "${G4_WRITERS[@]}"; do
|
|
2156
|
+
scr=-; [[ "${1:--}" != - ]] && { we_w_cfg "$w"; scr="$1/$(basename "$WE_S")"; }
|
|
2157
|
+
we_spawn "$w" "$scr" "$go" WE_GREP_SLEEP=0.6; pids+=("$WE_PID")
|
|
2158
|
+
done
|
|
2159
|
+
: > "$go"
|
|
2160
|
+
WE_RCS=""; for w in "${pids[@]}"; do wait "$w"; WE_RCS+="$? "; done
|
|
2161
|
+
}
|
|
2162
|
+
we_parallel -
|
|
2163
|
+
check "parallel: four different writers released together all exit 0" test "$WE_RCS" = "0 0 0 0 "
|
|
2164
|
+
check "parallel: the forced interleave was EXERCISED (four distinct writers each entered the stalled critical section once)" we_exercised "$WED/iv.log" 4
|
|
2165
|
+
check "parallel: the critical sections never overlapped (the recorded intervals strictly alternate)" we_alternates "$WED/iv.log"
|
|
2166
|
+
we_all_keys() { local w; for w in "${G4_WRITERS[@]}"; do we_w_cfg "$w"; we_keys_once "${WE_K[@]}" || return 1; done; }
|
|
2167
|
+
check "parallel: no lost update - all fourteen managed keys present once each" we_all_keys
|
|
2168
|
+
check "parallel: the unrelated KEEP_ME and OTHER lines survived" bash -c '[[ "$(grep -c "^KEEP_ME=1$" "$1/.env")" == 1 && "$(grep -c "^OTHER=2$" "$1/.env")" == 1 ]]' _ "$GITWORK"
|
|
2169
|
+
check "parallel: no .env.wenv.* or .env.lock.d* is left behind" we_no_artifacts
|
|
2170
|
+
check "parallel: the final .env is mode 600" we_mode_600
|
|
2171
|
+
# Harness controls: the same run with the lock acquisition made a no-op in all four copies MUST show the overlap and the lost update.
|
|
2172
|
+
NL_DIR="$MUT/skills/community/scripts"
|
|
2173
|
+
for w_ in bsky x linkedin discord; do
|
|
2174
|
+
mut_make "$w_-setup.sh" 'if mkdir -- "$_wl" 2>/dev/null; then' 'if { mkdir -p -- "$_wl"; true; }; then' > /dev/null
|
|
2175
|
+
done
|
|
2176
|
+
we_parallel "$NL_DIR"
|
|
2177
|
+
check "harness control: with the lock a no-op the intervals OVERLAP (the interleave row can go RED)" we_overlaps "$WED/iv.log"
|
|
2178
|
+
check "harness control: with the lock a no-op a different writer's keys are LOST (the lost-update row can go RED)" not we_all_keys
|
|
2179
|
+
printf 'S a\nE a\n' > "$WED/one.log"
|
|
2180
|
+
check "harness control: a log in which only ONE writer ran is not accepted as exercised for four" not we_exercised "$WED/one.log" 4
|
|
2181
|
+
check "harness control: the exercised check accepts a log with the right number of distinct writers" we_exercised "$WED/iv.log" 4
|
|
2182
|
+
check "harness control: the alternation check rejects a hand-built overlapping log" not we_alternates <(printf 'S a\nS b\nE a\nE b\n')
|
|
2183
|
+
check "harness control: the alternation check rejects an empty log (nothing ran is not a pass)" not we_alternates /dev/null
|
|
2184
|
+
|
|
2185
|
+
# --- (i-b) eight runs of ONE writer released together: each managed key exactly once (measured RED before: duplicates) ---------
|
|
2186
|
+
we_same_parallel() { # -> WE_RCS, and .env holds whatever the eight runs left
|
|
2187
|
+
local i pids=() go="$WED/go"
|
|
2188
|
+
we_reset $'KEEP_ME=1\nBSKY_HANDLE=old\nBSKY_APP_PASSWORD=old'; "$G4_REAL_RM" -f "$go"
|
|
2189
|
+
for i in 1 2 3 4 5 6 7 8; do we_spawn bsky - "$go" WE_GREP_SLEEP=0.05; pids+=("$WE_PID"); done
|
|
2190
|
+
: > "$go"
|
|
2191
|
+
WE_RCS=""; for i in "${pids[@]}"; do wait "$i"; WE_RCS+="$? "; done
|
|
2192
|
+
}
|
|
2193
|
+
we_once_each() { [[ "$("$G4_REAL_GREP" -c '^BSKY_HANDLE=' "$GITWORK/.env")" == 1 && "$("$G4_REAL_GREP" -c '^BSKY_APP_PASSWORD=' "$GITWORK/.env")" == 1 && "$("$G4_REAL_GREP" -c '^KEEP_ME=1$' "$GITWORK/.env")" == 1 ]]; }
|
|
2194
|
+
for it_ in 1 2 3; do
|
|
2195
|
+
we_same_parallel
|
|
2196
|
+
check "eight parallel runs of one writer (round $it_): all exit 0, every managed key is in .env exactly once, KEEP_ME survived, all eight entered the critical section one at a time, no artifacts" \
|
|
2197
|
+
we_and 'test "$WE_RCS" = "0 0 0 0 0 0 0 0 "' 'we_once_each' 'we_no_artifacts' '[[ "$("$G4_REAL_GREP" -c "^S " "$WED/iv.log")" == 8 ]]' 'we_alternates "$WED/iv.log"'
|
|
2198
|
+
done
|
|
2199
|
+
|
|
2200
|
+
# --- (iii) same-directory temp, mode at rename time, lock directory mode (umask 000 caller) ---------------------
|
|
2201
|
+
for w_ in "${G4_WRITERS[@]}"; do
|
|
2202
|
+
we_reset "$G4_SEED"; we_w_cfg "$w_"
|
|
2203
|
+
RUN_CWD="$GITWORK" run_sut "$WED/shim" "" WE_MV_LOG="$WED/mv.log" WE_BIRTH_LOG="$WED/birth.log" "${WE_E[@]}" -- \
|
|
2204
|
+
bash -c 'umask 000; exec bash "$0" "$@"' "$WE_S" write-env "${WE_A[@]}"
|
|
2205
|
+
check "$w_-setup write-env under umask 000: exit 0" test "$RC" -eq 0
|
|
2206
|
+
check "$w_-setup write-env: exactly one rename of the temp, and its source directory is the target's directory (same filesystem, atomic)" \
|
|
2207
|
+
bash -c '[[ "$(wc -l < "$1")" == 1 ]] && read -r _ s d _ < "$1" && [[ -n "$s" && "$s" == "$d" && "$d" == "$2" ]]' _ "$WED/mv.log" "$GITWORK"
|
|
2208
|
+
check "$w_-setup write-env under umask 000: the temp is mode 600 at rename time" bash -c 'read -r _ _ _ m < "$1"; [[ "$m" == 600 ]]' _ "$WED/mv.log"
|
|
2209
|
+
check "$w_-setup write-env under umask 000: the lock directory is created mode 700 (umask 077 held for the whole helper)" \
|
|
2210
|
+
bash -c '[[ "$("$1" -c "^D 700$" "$2")" == 1 ]]' _ "$G4_REAL_GREP" "$WED/birth.log"
|
|
2211
|
+
check "$w_-setup write-env under umask 000: the final .env is mode 600 and nothing but it is left" we_and 'we_mode_600' 'we_no_artifacts'
|
|
2212
|
+
done
|
|
2213
|
+
|
|
2214
|
+
# --- (iv) a NEW .env is mode 600 from birth, observed at creation (not after a chmod) --------------------------
|
|
2215
|
+
for w_ in bsky discord; do
|
|
2216
|
+
we_reset @none; we_w_cfg "$w_"
|
|
2217
|
+
RUN_CWD="$GITWORK" run_sut "$WED/shim" "" WE_BIRTH_LOG="$WED/birth.log" "${WE_E[@]}" -- \
|
|
2218
|
+
bash -c 'umask 000; exec bash "$0" "$@"' "$WE_S" write-env "${WE_A[@]}"
|
|
2219
|
+
check "$w_-setup write-env on a NEW .env under umask 000: exit 0 and the file exists" bash -c '[[ "$1" -eq 0 && -f "$2/.env" ]]' _ "$RC" "$GITWORK"
|
|
2220
|
+
check "$w_-setup write-env on a NEW .env: the temp was mode 600 when it came into existence (creating-command shim)" bash -c '[[ "$(grep -c "^T 600$" "$1")" == 1 ]]' _ "$WED/birth.log"
|
|
2221
|
+
check "$w_-setup write-env on a NEW .env: final mode 600, no artifacts, the keys written once" we_and 'we_mode_600' 'we_no_artifacts' 'we_keys_once "${WE_K[@]}"'
|
|
2222
|
+
done
|
|
2223
|
+
we_reset "$G4_SEED"; we_w_cfg bsky
|
|
2224
|
+
RUN_CWD="$GITWORK" run_sut "$WED/shim" "" WE_BIRTH_LOG="$WED/birth.log" "${WE_E[@]}" -- bash -c 'umask 000; exec bash "$0" "$@"' "$WE_S" write-env
|
|
2225
|
+
check "existing .env under umask 000: the filter's stdout (the file the grep creates content in) is mode 600 when grep first writes (creating-command shim)" \
|
|
2226
|
+
bash -c '[[ "$(grep -c "^G 600$" "$1")" == 1 ]]' _ "$WED/birth.log"
|
|
2227
|
+
|
|
2228
|
+
# --- (v) an unreadable .env aborts, byte-identical ---------------------------------------------------------------
|
|
2229
|
+
for w_ in "${G4_WRITERS[@]}"; do
|
|
2230
|
+
we_reset "$G4_SEED"; we_w_cfg "$w_"
|
|
2231
|
+
RUN_CWD="$GITWORK" run_sut "$WED/shim" "" WE_GREP_RC=2 "${WE_E[@]}" -- bash "$WE_S" write-env "${WE_A[@]}"
|
|
2232
|
+
check "$w_-setup write-env with an unreadable .env (grep exit 2): aborts non-zero" test "$RC" -eq 1
|
|
2233
|
+
check "$w_-setup write-env with an unreadable .env: .env is byte-identical (not wiped) and no artifact remains" we_and 'we_identical' 'we_no_artifacts'
|
|
2234
|
+
done
|
|
2235
|
+
check "unreadable .env: the stderr marker names reason=env_unreadable once and carries no value" \
|
|
2236
|
+
bash -c '[[ "$(grep -c "^SOLEUR_CREDENTIAL_REFUSED script=discord-setup.sh reason=env_unreadable var=ENV_FILE phase=write-env$" <<<"$1")" == 1 && "$1$2" != *g4disctok* ]]' _ "$ERR" "$OUT$ERR"
|
|
2237
|
+
check "unreadable .env: exactly one human Error line, on stdout" bash -c '[[ "$(grep -c "^Error:" <<<"$1")" == 1 && "$2" != *Error:* ]]' _ "$OUT" "$ERR"
|
|
2238
|
+
|
|
2239
|
+
# --- artifacts after a normal run and after a signal -----------------------------------------------------------------
|
|
2240
|
+
for w_ in "${G4_WRITERS[@]}"; do
|
|
2241
|
+
we_reset "$G4_SEED"; we_w_cfg "$w_"
|
|
2242
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env "${WE_A[@]}"
|
|
2243
|
+
check "$w_-setup write-env, normal run: exit 0, no .env.wenv.* or .env.lock.d* left, keys written once" we_and 'test "$RC" -eq 0' 'we_no_artifacts' 'we_keys_once "${WE_K[@]}"'
|
|
2244
|
+
done
|
|
2245
|
+
for sig_ in TERM HUP; do
|
|
2246
|
+
we_reset "$G4_SEED"
|
|
2247
|
+
we_spawn bsky - - WE_GREP_SLEEP=2; pid_=$WE_PID
|
|
2248
|
+
we_wait_for "$WED/iv.log"; "$G4_REAL_SLEEP" 0.3
|
|
2249
|
+
check "$sig_: the run was inside its critical section (lock held, temp present) when the signal arrived" bash -c '[[ -d "$1/.env.lock.d" && -n "$(ls "$1"/.env.wenv.* 2>/dev/null)" ]]' _ "$GITWORK"
|
|
2250
|
+
kill -s "$sig_" "$pid_" 2>/dev/null; wait "$pid_" 2>/dev/null; rc_=$?
|
|
2251
|
+
check "$sig_: the run exits non-zero (128 + signal)" test "$rc_" -gt 128
|
|
2252
|
+
check "$sig_: .env is byte-identical and no .env.wenv.* or .env.lock.d* is left behind" we_and 'we_identical' 'we_no_artifacts'
|
|
2253
|
+
done
|
|
2254
|
+
|
|
2255
|
+
# --- (vi) a SIGKILLed run: .env intact, the stale lock is recovered by the next run -------------------------------
|
|
2256
|
+
we_reset "$G4_SEED"
|
|
2257
|
+
we_spawn bsky - - WE_GREP_SLEEP=1.5; pid_=$WE_PID
|
|
2258
|
+
we_wait_for "$WED/iv.log"; "$G4_REAL_SLEEP" 0.3
|
|
2259
|
+
kill -9 "$pid_" 2>/dev/null; wait "$pid_" 2>/dev/null
|
|
2260
|
+
"$G4_REAL_SLEEP" 1.8 # the orphaned stalled grep (a child the kill could not reach) finishes before the next scenario's log starts
|
|
2261
|
+
check "SIGKILL: the killed run left its lock directory with a DEAD owner pid (the precondition is real)" \
|
|
2262
|
+
bash -c 'read -r o < "$1/.env.lock.d/owner" && ! kill -0 "$o" 2>/dev/null' _ "$GITWORK"
|
|
2263
|
+
check "SIGKILL: the killed run also left its temp file (the next run has something to sweep)" bash -c 'ls "$1"/.env.wenv.* >/dev/null 2>&1' _ "$GITWORK"
|
|
2264
|
+
check "SIGKILL: .env is byte-identical after the kill" we_identical
|
|
2265
|
+
we_w_cfg x
|
|
2266
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2267
|
+
check "SIGKILL: the next run recovers the stale lock and writes (exit 0, keys once)" we_and 'test "$RC" -eq 0' 'we_keys_once "${WE_K[@]}"'
|
|
2268
|
+
check "SIGKILL: the next run also swept the killed run's temp, and no .env.lock.d* remains" we_no_artifacts
|
|
2269
|
+
|
|
2270
|
+
# --- (ii) stale-lock recovery race between two waiters ----------------------------------------------------------------
|
|
2271
|
+
# A dead owner pid. Waiter 1 judges the lock stale from its OWN snapshot and is STALLED right before it takes the recovery
|
|
2272
|
+
# mutex; waiter 2 recovers the lock, takes it and sits in its critical section. When waiter 1 resumes it holds the mutex
|
|
2273
|
+
# with a stale snapshot: acting on that snapshot would delete waiter 2's LIVE lock. It must RE-READ the owner under the
|
|
2274
|
+
# mutex (the compare-and-swap) and leave the live lock alone.
|
|
2275
|
+
we_dead_pid() { ( : ) & local p=$!; wait "$p"; printf '%s\n' "$p"; }
|
|
2276
|
+
we_steal_scenario() { # <script-dir or -> ; waiter 1 = bsky (stalled at the recovery mutex), waiter 2 = x (long critical section)
|
|
2277
|
+
local s1=- s2=- w1 w2
|
|
2278
|
+
we_reset "$G4_SEED"
|
|
2279
|
+
"$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"; we_dead_pid > "$GITWORK/.env.lock.d/owner"
|
|
2280
|
+
if [[ "${1:--}" != - ]]; then s1="$1/bsky-setup.sh"; s2="$1/x-setup.sh"; fi
|
|
2281
|
+
we_spawn bsky "$s1" - WE_MKDIR_DELAY=1.5 WE_MKDIR_DELAY_FLAG="$WED/flag.stall" WE_GREP_SLEEP=0.2; w1=$WE_PID
|
|
2282
|
+
"$G4_REAL_SLEEP" 0.4
|
|
2283
|
+
we_spawn x "$s2" - WE_GREP_SLEEP=2; w2=$WE_PID
|
|
2284
|
+
wait "$w1"; STEAL_RC1=$?; wait "$w2"; STEAL_RC2=$?
|
|
2285
|
+
}
|
|
2286
|
+
we_steal_scenario -
|
|
2287
|
+
check "stale-lock race: waiter 1 WAS stalled at the recovery mutex (the delay fired) and waiter 2 entered its critical section first" \
|
|
2288
|
+
bash -c '[[ -e "$1/flag.stall" && "$(head -1 "$1/iv.log")" == "S x" ]]' _ "$WED"
|
|
2289
|
+
check "stale-lock race: both waiters finish with exit 0" test "$STEAL_RC1$STEAL_RC2" = "00"
|
|
2290
|
+
check "stale-lock race: exactly one waiter was in its critical section at a time (the stale snapshot was re-read under the mutex, nobody deleted a live lock)" we_alternates "$WED/iv.log"
|
|
2291
|
+
check "stale-lock race: both waiters' keys landed exactly once and no lock or temp is left" we_and 'we_w_cfg bsky' 'we_keys_once "${WE_K[@]}"' 'we_w_cfg x' 'we_keys_once "${WE_K[@]}"' 'we_no_artifacts'
|
|
2292
|
+
for it_ in 1 2 3; do
|
|
2293
|
+
we_reset "$G4_SEED"; "$G4_REAL_RM" -f "$WED/go"
|
|
2294
|
+
"$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"; we_dead_pid > "$GITWORK/.env.lock.d/owner"
|
|
2295
|
+
we_spawn bsky - "$WED/go" WE_GREP_SLEEP=0.3; p1_=$WE_PID
|
|
2296
|
+
we_spawn discord - "$WED/go" WE_GREP_SLEEP=0.3; p2_=$WE_PID
|
|
2297
|
+
: > "$WED/go"; wait "$p1_"; r1_=$?; wait "$p2_"; r2_=$?
|
|
2298
|
+
check "stale-lock race, released together (round $it_): both exit 0, the intervals alternate, both key sets once, no artifacts" we_and "test $r1_$r2_ = 00" 'we_alternates "$WED/iv.log"' 'we_exercised "$WED/iv.log" 2' 'we_w_cfg bsky' 'we_keys_once "${WE_K[@]}"' 'we_w_cfg discord' 'we_keys_once "${WE_K[@]}"' 'we_no_artifacts'
|
|
2299
|
+
done
|
|
2300
|
+
|
|
2301
|
+
# --- (ii-b) stale-lock recovery with THREE and EIGHT waiters (review D1: two writers held the lock, keys lost, every writer exited 0) ---
|
|
2302
|
+
# N writers, each upserting ONE distinct key through the extracted helper, released together against a lock whose owner pid is
|
|
2303
|
+
# dead. Each round is judged on: every writer exits 0, every key is in .env exactly once, no writer exits 0 with its key missing,
|
|
2304
|
+
# the critical sections never overlap (interval log) and CONTENTION was exercised (at least two distinct writers failed on the
|
|
2305
|
+
# lock directory in that round; one writer alone cannot race anything). A harness control runs the same rounds with the lock a
|
|
2306
|
+
# no-op and requires the verdicts to go RED.
|
|
2307
|
+
WE_DRV="$WED/drv.sh"
|
|
2308
|
+
cat > "$WE_DRV" <<'DRV'
|
|
2309
|
+
set -euo pipefail
|
|
2310
|
+
SOLEUR_TRANSPORT_SCRIPT=drv
|
|
2311
|
+
# shellcheck disable=SC1090
|
|
2312
|
+
. "$1"
|
|
2313
|
+
_wenv_write "$2" "^($3)=" "$3=v-$3"
|
|
2314
|
+
DRV
|
|
2315
|
+
we_drv_spawn() { # <i> <gate> <block-file> [NAME=value env...]: one background writer of key K<i>; sets WE_PID
|
|
2316
|
+
local i="$1" gate="$2" blk="$3"; shift 3
|
|
2317
|
+
( while [[ ! -e "$gate" ]]; do :; done
|
|
2318
|
+
exec env -i PATH="$WED/shim:$SYS_PATH" HOME="$SANDBOX/home" WE_TAG="k$i" WE_LOG="$WED/iv.log" WE_CONTEND_LOG="$WED/contend.log" WE_GREP_SLEEP=0.03 "$@" \
|
|
2319
|
+
bash "$WE_DRV" "$blk" "$GITWORK/.env" "K$i" ) > /dev/null 2> "$WED/err.k$i" &
|
|
2320
|
+
WE_PID=$!
|
|
2321
|
+
}
|
|
2322
|
+
we_stale_round() { # <n> <block-file>: n writers released together against a dead-owner lock -> WE_RCS
|
|
2323
|
+
local n="$1" blk="$2" i pids=() go="$WED/go"
|
|
2324
|
+
we_reset $'KEEP_ME=1'; "$G4_REAL_RM" -f "$go"
|
|
2325
|
+
"$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"; we_dead_pid > "$GITWORK/.env.lock.d/owner"
|
|
2326
|
+
for (( i = 1; i <= n; i++ )); do we_drv_spawn "$i" "$go" "$blk"; pids+=("$WE_PID"); done
|
|
2327
|
+
: > "$go"
|
|
2328
|
+
WE_RCS=""; for i in "${pids[@]}"; do wait "$i"; WE_RCS+="$? "; done
|
|
2329
|
+
}
|
|
2330
|
+
we_stale_verdict() { # <n>: tally the round that just finished into the WSR_* counters
|
|
2331
|
+
local n="$1" i rc present k
|
|
2332
|
+
local -a rcs=()
|
|
2333
|
+
read -r -a rcs <<< "$WE_RCS"
|
|
2334
|
+
for (( i = 1; i <= n; i++ )); do
|
|
2335
|
+
k="K$i=v-K$i"; present=0
|
|
2336
|
+
if [[ "$("$G4_REAL_GREP" -cxF -- "$k" "$GITWORK/.env")" == 1 && "$("$G4_REAL_GREP" -c "^K$i=" "$GITWORK/.env")" == 1 ]]; then present=1; fi
|
|
2337
|
+
rc="${rcs[i-1]:-x}"
|
|
2338
|
+
if (( present == 0 )); then WSR_LOST=$((WSR_LOST + 1)); fi
|
|
2339
|
+
if [[ "$rc" != 0 ]]; then WSR_NONZERO=$((WSR_NONZERO + 1)); fi
|
|
2340
|
+
if [[ "$rc" == 0 && "$present" == 0 ]]; then WSR_SILENT=$((WSR_SILENT + 1)); fi
|
|
2341
|
+
done
|
|
2342
|
+
we_alternates "$WED/iv.log" || WSR_OVERLAP=$((WSR_OVERLAP + 1))
|
|
2343
|
+
[[ "$(sort -u "$WED/contend.log" 2> /dev/null | wc -l)" -ge 2 ]] || WSR_QUIET=$((WSR_QUIET + 1))
|
|
2344
|
+
we_no_artifacts || WSR_ARTIF=$((WSR_ARTIF + 1))
|
|
2345
|
+
[[ "$("$G4_REAL_GREP" -c '^KEEP_ME=1$' "$GITWORK/.env")" == 1 ]] || WSR_LOST=$((WSR_LOST + 1))
|
|
2346
|
+
}
|
|
2347
|
+
we_stale_rounds() { # <n> <rounds> <block-file>
|
|
2348
|
+
local n="$1" r="$2" blk="$3" it
|
|
2349
|
+
WSR_LOST=0 WSR_NONZERO=0 WSR_SILENT=0 WSR_OVERLAP=0 WSR_QUIET=0 WSR_ARTIF=0 WSR_ROUNDS=0
|
|
2350
|
+
for (( it = 1; it <= r; it++ )); do we_stale_round "$n" "$blk"; we_stale_verdict "$n"; WSR_ROUNDS=$((WSR_ROUNDS + 1)); done
|
|
2351
|
+
}
|
|
2352
|
+
wenvw_block "$HERE/bsky-setup.sh" > "$WED/blk.cur"
|
|
2353
|
+
for cfg_ in "3 40" "8 25"; do
|
|
2354
|
+
read -r n_ r_ <<< "$cfg_"
|
|
2355
|
+
we_stale_rounds "$n_" "$r_" "$WED/blk.cur"
|
|
2356
|
+
check "stale-lock recovery, $n_ waiters x $r_ rounds released together on a dead-owner lock: every writer exits 0 and every key is in .env exactly once" test "$WSR_NONZERO:$WSR_LOST" = 0:0
|
|
2357
|
+
check "stale-lock recovery, $n_ waiters x $r_ rounds: no writer exits 0 with its key missing (no silent lost update)" test "$WSR_SILENT" -eq 0
|
|
2358
|
+
check "stale-lock recovery, $n_ waiters x $r_ rounds: the critical sections never overlapped in any round (never two holders)" test "$WSR_OVERLAP" -eq 0
|
|
2359
|
+
check "stale-lock recovery, $n_ waiters x $r_ rounds: contention WAS exercised in every round (>= 2 distinct writers failed on the lock) and nothing was left behind" \
|
|
2360
|
+
we_and "test $WSR_QUIET -eq 0" "test $WSR_ARTIF -eq 0" "test $WSR_ROUNDS -eq $r_"
|
|
2361
|
+
done
|
|
2362
|
+
M="$(mut_make bsky-setup.sh 'if mkdir -- "$_wl" 2>/dev/null; then' 'if { mkdir -p -- "$_wl"; true; }; then')"
|
|
2363
|
+
wenvw_block "$M" > "$WED/blk.nl"
|
|
2364
|
+
we_stale_rounds 8 4 "$WED/blk.nl"
|
|
2365
|
+
check "harness control: with the lock a no-op the eight-waiter rounds OVERLAP and LOSE keys (the verdicts can go RED)" we_and "test $WSR_OVERLAP -ge 1" "test $WSR_LOST -ge 1"
|
|
2366
|
+
we_stale_rounds 1 1 "$WED/blk.cur"
|
|
2367
|
+
check "harness control: a round with ONE writer is not accepted as contention (the exercised check can go RED)" test "$WSR_QUIET" -eq 1
|
|
2368
|
+
|
|
2369
|
+
# --- recovery mutex: a signal leaves nothing, a SIGKILLed recovery fails closed ---------------------------------------------
|
|
2370
|
+
we_reset "$G4_SEED"
|
|
2371
|
+
"$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"; we_dead_pid > "$GITWORK/.env.lock.d/owner"
|
|
2372
|
+
we_spawn bsky - - WE_RM_DELAY=1.5 WE_RM_DELAY_FLAG="$WED/flag.rmstall"; pid_=$WE_PID
|
|
2373
|
+
we_wait_for "$WED/flag.rmstall"
|
|
2374
|
+
check "TERM mid-recovery: the run WAS inside the recovery (the mutex directory exists, the stale lock not yet removed)" bash -c '[[ -d "$1/.env.lock.d.recover" && -d "$1/.env.lock.d" ]]' _ "$GITWORK"
|
|
2375
|
+
kill -s TERM "$pid_" 2>/dev/null; wait "$pid_" 2>/dev/null; rc_=$?
|
|
2376
|
+
check "TERM mid-recovery: the run exits non-zero (128 + signal)" test "$rc_" -gt 128
|
|
2377
|
+
check "TERM mid-recovery: .env is byte-identical and neither the recovery mutex nor any other artifact is left" we_and 'we_identical' 'we_no_artifacts'
|
|
2378
|
+
we_reset "$G4_SEED"
|
|
2379
|
+
"$G4_REAL_MKDIR" "$GITWORK/.env.lock.d" "$GITWORK/.env.lock.d.recover"; we_dead_pid > "$GITWORK/.env.lock.d/owner"
|
|
2380
|
+
we_w_cfg bsky; t0_=$SECONDS
|
|
2381
|
+
RUN_CWD="$GITWORK" run_sut - "" SOLEUR_WENV_LOCK_TICKS=15 "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2382
|
+
t1_=$SECONDS
|
|
2383
|
+
check "SIGKILLed recovery (a leftover recovery mutex): the run fails closed (exit 1) within the bound instead of hanging" we_and 'test "$RC" -eq 1' "test $((t1_ - t0_)) -le 8"
|
|
2384
|
+
check "SIGKILLed recovery: marker reason=env_lock_timeout once, value-free; the message names the .recover directory and says to remove it" \
|
|
2385
|
+
bash -c '[[ "$(grep -c "^SOLEUR_CREDENTIAL_REFUSED script=bsky-setup.sh reason=env_lock_timeout var=ENV_FILE phase=write-env$" <<<"$1")" == 1 && "$2" == *".env.lock.d.recover"* && "$2" == *"remove that directory"* && "$1$2" != *g4aa-g4bb* ]]' _ "$ERR" "$OUT"
|
|
2386
|
+
check "SIGKILLed recovery: .env is byte-identical and both directories were left exactly as found" we_and 'we_identical' '[[ -d "$GITWORK/.env.lock.d.recover" && -d "$GITWORK/.env.lock.d" ]]'
|
|
2387
|
+
rm -rf "$GITWORK/.env.lock.d" "$GITWORK/.env.lock.d.recover"
|
|
2388
|
+
|
|
2389
|
+
# --- an ownerless lock directory and a lock whose owner file is not a pid are recovered (waited on, then re-read under the mutex) ---
|
|
2390
|
+
for own_ in absent garbage; do
|
|
2391
|
+
we_reset "$G4_SEED"; "$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"
|
|
2392
|
+
if [[ "$own_" == garbage ]]; then printf -- '-1\n' > "$GITWORK/.env.lock.d/owner"; fi
|
|
2393
|
+
we_w_cfg bsky; t0_=$SECONDS
|
|
2394
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2395
|
+
t1_=$SECONDS
|
|
2396
|
+
check "lock directory with $own_ owner: recovered after a wait, not at once (exit 0, keys written once, no artifacts left)" \
|
|
2397
|
+
we_and 'test "$RC" -eq 0' 'we_keys_once "${WE_K[@]}"' 'we_no_artifacts' "test $((t1_ - t0_)) -ge 2"
|
|
2398
|
+
done
|
|
2399
|
+
|
|
2400
|
+
# --- _wenv_unlock releases the lock ONLY when its owner file reads exactly this process; the owner-write failure path removes it explicitly ---
|
|
2401
|
+
we_unlock_probe() { # <script> <empty|foreign|own|mutex-tmp>: runs the helper's _wenv_unlock in a clean bash; prints kept|removed
|
|
2402
|
+
wenvw_block "$1" > "$WED/blk.probe"
|
|
2403
|
+
bash -c 'set -euo pipefail; SOLEUR_TRANSPORT_SCRIPT=probe; . "$1"; d="$2"; mkdir -- "$d"
|
|
2404
|
+
case "$3" in
|
|
2405
|
+
own) printf "%s\n" "$$" > "$d/owner" ;;
|
|
2406
|
+
foreign) printf "%s\n" "$4" > "$d/owner" ;;
|
|
2407
|
+
mutex-tmp) printf "%s\n" "$$" > "$d/owner"; mkdir -- "$d.recover"; : > "$d.tmp"; _WENV_REC="$d.recover"; _WENV_TMP="$d.tmp" ;;
|
|
2408
|
+
esac
|
|
2409
|
+
_WENV_LOCK="$d"; _wenv_unlock
|
|
2410
|
+
if [[ "$3" == mutex-tmp ]]; then [[ ! -e "$d" && ! -e "$d.recover" && ! -e "$d.tmp" ]] && echo removed || echo kept
|
|
2411
|
+
else [[ -d "$d" ]] && echo kept || echo removed; fi' _ "$WED/blk.probe" "$WED/probe.lock.d" "$2" "$$"
|
|
2412
|
+
rm -rf "$WED/probe.lock.d" "$WED/probe.lock.d.recover" "$WED/probe.lock.d.tmp"
|
|
2413
|
+
}
|
|
2414
|
+
check "_wenv_unlock: a lock whose owner file is ABSENT or empty is NOT ours and is kept" test "$(we_unlock_probe "$HERE/bsky-setup.sh" empty)" = kept
|
|
2415
|
+
check "_wenv_unlock: a lock owned by ANOTHER pid is kept" test "$(we_unlock_probe "$HERE/bsky-setup.sh" foreign)" = kept
|
|
2416
|
+
check "_wenv_unlock: a lock whose owner file reads exactly this process is removed" test "$(we_unlock_probe "$HERE/bsky-setup.sh" own)" = removed
|
|
2417
|
+
check "_wenv_unlock: the recovery mutex and the temp are released together with our lock" test "$(we_unlock_probe "$HERE/bsky-setup.sh" mutex-tmp)" = removed
|
|
2418
|
+
we_reset "$G4_SEED"; we_w_cfg bsky
|
|
2419
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash -c 'ulimit -f 0; trap "" XFSZ; exec bash "$0" "$@"' "$WE_S" write-env
|
|
2420
|
+
check "owner file cannot be written (file-size limit 0): the run fails (exit 1) and the lock it just created is removed explicitly (it holds no owner, so the unlock would keep it)" \
|
|
2421
|
+
we_and 'test "$RC" -eq 1' 'we_identical' 'we_no_artifacts'
|
|
2422
|
+
|
|
2423
|
+
# --- the helper's temp has its OWN namespace: .env.wenv.XXXXXX; user files and the operator library's temps are never swept ---
|
|
2424
|
+
for w_ in "${G4_WRITERS[@]}"; do
|
|
2425
|
+
we_reset "$G4_SEED"; we_w_cfg "$w_"
|
|
2426
|
+
printf 'user backup\n' > "$GITWORK/.env.tmp.backup"; printf 'lib in flight\n' > "$GITWORK/.env.tmp.AbC123"; printf 'killed run leftover\n' > "$GITWORK/.env.wenv.ZZ9999"
|
|
2427
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env "${WE_A[@]}"
|
|
2428
|
+
check "$w_-setup write-env: a user file .env.tmp.backup and a library-style .env.tmp.AbC123 survive the write untouched; the helper's own leftover .env.wenv.* is swept; no lock remains" \
|
|
2429
|
+
we_and 'test "$RC" -eq 0' '[[ "$(cat "$GITWORK/.env.tmp.backup")" == "user backup" ]]' '[[ "$(cat "$GITWORK/.env.tmp.AbC123")" == "lib in flight" ]]' \
|
|
2430
|
+
'[[ ! -e "$GITWORK/.env.wenv.ZZ9999" ]]' '[[ ! -e "$GITWORK/.env.lock.d" ]]' 'we_keys_once "${WE_K[@]}"'
|
|
2431
|
+
done
|
|
2432
|
+
we_reset "$G4_SEED"; we_w_cfg bsky
|
|
2433
|
+
RUN_CWD="$GITWORK" run_sut "$WED/shim" "" WE_TMP_LOG="$WED/tmp.log" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2434
|
+
check "the helper's temp is named <.env>.wenv.XXXXXX (its own namespace, never .env.tmp.*)" bash -c '[[ "$(wc -l < "$1")" == 1 ]] && read -r t < "$1" && [[ "$t" =~ ^$2/\.env\.wenv\.[A-Za-z0-9]{6}$ ]]' _ "$WED/tmp.log" "$GITWORK"
|
|
2435
|
+
|
|
2436
|
+
# --- LC_ALL=C grep -a: a UTF-8 caller, an invalid-UTF-8 line and a NUL line must all survive byte for byte ---------------------
|
|
2437
|
+
G4_UTF8=""
|
|
2438
|
+
for l_ in C.UTF-8 C.utf8 en_US.UTF-8 en_US.utf8; do
|
|
2439
|
+
if [[ "$(LC_ALL=$l_ locale charmap 2> /dev/null)" == UTF-8 ]]; then G4_UTF8="$l_"; break; fi
|
|
2440
|
+
done
|
|
2441
|
+
[[ -n "$G4_UTF8" ]] || echo " note - no UTF-8 locale on this host: the UTF-8 caller row degrades to the LC_ALL shim row"
|
|
2442
|
+
we_locale_run() { # -> the .env after a bsky write by a UTF-8 caller whose .env holds an invalid-UTF-8 line and a NUL line
|
|
2443
|
+
we_reset @none; we_w_cfg bsky
|
|
2444
|
+
printf 'KEEP_HIGH=caf\xe9\nKEEP_NUL=a\0b\nBSKY_HANDLE=old\nKEEP_ME=1\n' > "$GITWORK/.env"; chmod 600 "$GITWORK/.env"
|
|
2445
|
+
printf 'KEEP_HIGH=caf\xe9\nKEEP_NUL=a\0b\nKEEP_ME=1\nBSKY_HANDLE=guard4.bsky.social\nBSKY_APP_PASSWORD=g4aa-g4bb-g4cc-g4dd\n' > "$WED/locale.want"
|
|
2446
|
+
RUN_CWD="$GITWORK" run_sut "$WED/shim" "" WE_LCLOG="$WED/lc.log" "LC_ALL=${G4_UTF8:-C}" "LANG=${G4_UTF8:-C}" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2447
|
+
}
|
|
2448
|
+
we_locale_run
|
|
2449
|
+
check "UTF-8 caller: exit 0, and the invalid-UTF-8 line and the NUL line survive byte for byte (the file equals the expected bytes)" we_and 'test "$RC" -eq 0' 'cmp -s "$WED/locale.want" "$GITWORK/.env"'
|
|
2450
|
+
check "the filter grep runs under LC_ALL=C whatever the caller's locale (recorded by the grep shim)" bash -c '[[ "$(cat "$1")" == C ]]' _ "$WED/lc.log"
|
|
2451
|
+
|
|
2452
|
+
# --- SOLEUR_WENV_LOCK_TICKS: validated (digits only, at most 1000, else the default 100); a hostile value is never evaluated ---
|
|
2453
|
+
( exec "$G4_REAL_SLEEP" 120 ) & tk_holder=$!
|
|
2454
|
+
we_ticks_run() { # <ticks|-> : bsky write-env against a LIVE holder with the counting sleep -> TK_N = number of ticks slept
|
|
2455
|
+
we_reset "$G4_SEED"; "$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"; printf '%s\n' "$tk_holder" > "$GITWORK/.env.lock.d/owner"
|
|
2456
|
+
: > "$WED/sleep.log"; rm -f "$WED/pwned"; we_w_cfg bsky
|
|
2457
|
+
local -a t=(); [[ "$1" != - ]] && t=("SOLEUR_WENV_LOCK_TICKS=$1")
|
|
2458
|
+
RUN_CWD="$GITWORK" run_sut "$WED/sleepshim" "" WE_SLEEPLOG="$WED/sleep.log" "${t[@]}" "${WE_E[@]}" -- timeout 60 bash "$WE_S" write-env
|
|
2459
|
+
TK_N="$(wc -c < "$WED/sleep.log")"
|
|
2460
|
+
}
|
|
2461
|
+
we_ticks_run 5
|
|
2462
|
+
check "TICKS=5 against a live holder: 4 ticks slept, gave up with reason=env_lock_timeout (the counter measures the bound)" we_and 'test "$TK_N" -eq 4' 'test "$RC" -eq 1' '[[ "$ERR" == *"reason=env_lock_timeout "* ]]'
|
|
2463
|
+
check "a LIVE holder: the give-up text names the holder case and the directory to remove" bash -c '[[ "$1" == *"Another write-env run is still holding the lock at "*".env.lock.d; wait for it to finish and run this again. If no other run is active, remove that directory."* ]]' _ "$OUT"
|
|
2464
|
+
we_ticks_run -
|
|
2465
|
+
check "TICKS unset: the default bound of 100 ticks (99 slept)" test "$TK_N" -eq 99
|
|
2466
|
+
we_ticks_run "HOME[\$(touch $WED/pwned)]"
|
|
2467
|
+
check "TICKS='HOME[\$(touch X)]' (arithmetic-injection shape): the command is NEVER run and the default bound applies" we_and 'test "$TK_N" -eq 99' '[[ ! -e "$WED/pwned" ]]'
|
|
2468
|
+
we_ticks_run 08
|
|
2469
|
+
check "TICKS=08 (leading zero): read as decimal 8 (7 ticks slept), no arithmetic error text on either stream" we_and 'test "$TK_N" -eq 7' '[[ "$ERR$OUT" != *"value too great"* ]]'
|
|
2470
|
+
we_ticks_run 5000
|
|
2471
|
+
check "TICKS=5000 (above the cap): rejected, the default bound applies (the wait is not extended)" test "$TK_N" -eq 99
|
|
2472
|
+
we_ticks_run 1000
|
|
2473
|
+
check "TICKS=1000 (the cap itself): honoured, 999 ticks slept in ONE bound (no second counter ends it early or late)" we_and 'test "$TK_N" -eq 999' '[[ "$ERR" == *"reason=env_lock_timeout "* ]]'
|
|
2474
|
+
kill "$tk_holder" 2>/dev/null; wait "$tk_holder" 2>/dev/null; rm -rf "$GITWORK/.env.lock.d"
|
|
2475
|
+
|
|
2476
|
+
# --- a lock path that is not a directory fails AT ONCE (distinct reason); a persistent mkdir failure sleeps every pass --------------
|
|
2477
|
+
we_reset "$G4_SEED"; : > "$WED/sleep.log"; we_w_cfg bsky
|
|
2478
|
+
printf 'not a directory\n' > "$GITWORK/.env.lock.d"
|
|
2479
|
+
RUN_CWD="$GITWORK" run_sut "$WED/sleepshim" "" WE_SLEEPLOG="$WED/sleep.log" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2480
|
+
check "lock path is a regular file: exit 1 at once (no tick slept) with reason=env_lock_not_dir, not the live-holder text" \
|
|
2481
|
+
bash -c '[[ "$1" -eq 1 && "$(wc -c < "$2")" -eq 0 && "$3" == *"reason=env_lock_not_dir "* && "$4" != *"holds the lock"* && "$4" == *".env.lock.d"* ]]' _ "$RC" "$WED/sleep.log" "$ERR" "$OUT"
|
|
2482
|
+
check "lock path is a regular file: the file and .env are left exactly as found" we_and 'we_identical' '[[ "$(cat "$GITWORK/.env.lock.d")" == "not a directory" ]]'
|
|
2483
|
+
rm -f "$GITWORK/.env.lock.d"; ln -s "$WED/nowhere" "$GITWORK/.env.lock.d"
|
|
2484
|
+
RUN_CWD="$GITWORK" run_sut "$WED/sleepshim" "" WE_SLEEPLOG="$WED/sleep.log" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2485
|
+
check "lock path is a dangling symbolic link: exit 1 at once with reason=env_lock_not_dir, the link untouched" bash -c '[[ "$1" -eq 1 && "$2" == *"reason=env_lock_not_dir "* && -L "$3/.env.lock.d" ]]' _ "$RC" "$ERR" "$GITWORK"
|
|
2486
|
+
rm -f "$GITWORK/.env.lock.d"
|
|
2487
|
+
LONG_="$(printf 'a%.0s' $(seq 1 250))"
|
|
2488
|
+
: > "$WED/sleep.log"
|
|
2489
|
+
( cd "$GITWORK" && env -i PATH="$WED/sleepshim:$SYS_PATH" HOME="$SANDBOX/home" WE_SLEEPLOG="$WED/sleep.log" SOLEUR_WENV_LOCK_TICKS=5 bash "$WE_DRV" "$WED/blk.cur" "$GITWORK/$LONG_" K1 ) > "$WED/long.out" 2> "$WED/long.err"; rc_=$?
|
|
2490
|
+
check "mkdir fails persistently (file name so long that <name>.lock.d is ENAMETOOLONG): every pass sleeps (4 ticks for TICKS=5), then exit 1 with reason=env_lock_timeout and NOT the live-holder text" \
|
|
2491
|
+
bash -c '[[ "$1" -eq 1 && "$(wc -c < "$2")" -eq 4 && "$(cat "$3")" == *"reason=env_lock_timeout "* && "$(cat "$4")" != *"Another write-env run holds"* ]]' _ "$rc_" "$WED/sleep.log" "$WED/long.err" "$WED/long.out"
|
|
2492
|
+
|
|
2493
|
+
# --- linkedin write-env removes ONLY the four keys it writes (LINKEDIN_ORG_* / ALLOW_POST / POST_MAX_LENGTH are read by linkedin-community.sh) ---
|
|
2494
|
+
we_reset $'KEEP_ME=1\nLINKEDIN_ORG_ACCESS_TOKEN=orgtok\nLINKEDIN_ORG_ID=123\nLINKEDIN_ALLOW_POST=true\nLINKEDIN_POST_MAX_LENGTH=3000\nLINKEDIN_CLIENT_ID=old\nLINKEDIN_CLIENT_SECRET=old\nLINKEDIN_ACCESS_TOKEN=old\nLINKEDIN_PERSON_URN=old'
|
|
2495
|
+
we_w_cfg linkedin
|
|
2496
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2497
|
+
check "linkedin write-env: LINKEDIN_ORG_ACCESS_TOKEN, LINKEDIN_ORG_ID, LINKEDIN_ALLOW_POST and LINKEDIN_POST_MAX_LENGTH in an existing .env survive; the four written keys appear once at their new values" \
|
|
2498
|
+
we_and 'test "$RC" -eq 0' '[[ "$("$G4_REAL_GREP" -cxF -e LINKEDIN_ORG_ACCESS_TOKEN=orgtok -e LINKEDIN_ORG_ID=123 -e LINKEDIN_ALLOW_POST=true -e LINKEDIN_POST_MAX_LENGTH=3000 -e KEEP_ME=1 "$GITWORK/.env")" == 5 ]]' 'we_keys_once "${WE_K[@]}"' '[[ "$("$G4_REAL_GREP" -c "=old$" "$GITWORK/.env")" == 0 ]]'
|
|
2499
|
+
|
|
2500
|
+
# --- the lock is held through the FINAL rename (deterministic: stall the temp's rename, a second writer must not enter before it ends) ---
|
|
2501
|
+
we_reset "$G4_SEED"
|
|
2502
|
+
we_spawn bsky - - WE_FINMV_DELAY=1.5 WE_FINMV_FLAG="$WED/flag.fin"; wa_=$WE_PID
|
|
2503
|
+
we_wait_for "$WED/flag.fin"
|
|
2504
|
+
we_spawn x - -; wb_=$WE_PID
|
|
2505
|
+
wait "$wa_"; ra_=$?; wait "$wb_"; rb_=$?
|
|
2506
|
+
we_line_of() { "$G4_REAL_GREP" -n -x -m1 -- "$1" "$WED/iv.log" | cut -d: -f1; }
|
|
2507
|
+
check "release after the rename: writer A WAS inside its final rename (the stall fired) when writer B started" test -e "$WED/flag.fin"
|
|
2508
|
+
check "release after the rename: writer B entered its critical section only AFTER writer A's final rename finished, both exit 0, both key sets once, nothing left" \
|
|
2509
|
+
we_and "test $ra_$rb_ = 00" '[[ -n "$(we_line_of "F bsky")" && -n "$(we_line_of "S x")" && "$(we_line_of "F bsky")" -lt "$(we_line_of "S x")" ]]' 'we_alternates "$WED/iv.log"' 'we_w_cfg bsky' 'we_keys_once "${WE_K[@]}"' 'we_w_cfg x' 'we_keys_once "${WE_K[@]}"' 'we_no_artifacts'
|
|
2510
|
+
|
|
2511
|
+
# --- .gitignore covers the lock directory, the recovery mutex and both temp namespaces ---
|
|
2512
|
+
GI="$SANDBOX/gi"; mkdir -p "$GI"; env -i PATH="$SYS_PATH" HOME="$SANDBOX/home" git init -q -- "$GI"; cp "$HERE/../../../../../.gitignore" "$GI/.gitignore"
|
|
2513
|
+
gi_all_ignored() { local n; for n in "$@"; do ( cd "$GI" && env -i PATH="$SYS_PATH" HOME="$SANDBOX/home" git check-ignore --no-index -q -- "$n" ) || return 1; done; }
|
|
2514
|
+
check "gitignore: .env, .env.lock.d, the recovery mutex .env.lock.d.recover, the helper's .env.wenv.XXXXXX and the library's .env.tmp.XXXXXX are all ignored" \
|
|
2515
|
+
gi_all_ignored .env .env.lock.d .env.lock.d/owner .env.lock.d.recover .env.wenv.AbC123 .env.tmp.AbC123
|
|
2516
|
+
|
|
2517
|
+
# --- bounded wait: a LIVE holder makes the second run fail closed -------------------------------------------------------
|
|
2518
|
+
we_reset "$G4_SEED"
|
|
2519
|
+
( exec "$G4_REAL_SLEEP" 30 ) & holder_=$!
|
|
2520
|
+
"$G4_REAL_MKDIR" "$GITWORK/.env.lock.d"; printf '%s\n' "$holder_" > "$GITWORK/.env.lock.d/owner"
|
|
2521
|
+
we_w_cfg bsky; t0_=$SECONDS
|
|
2522
|
+
RUN_CWD="$GITWORK" run_sut - "" SOLEUR_WENV_LOCK_TICKS=15 "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2523
|
+
t1_=$SECONDS
|
|
2524
|
+
check "bounded wait: a live lock holder makes the second run exit 1" test "$RC" -eq 1
|
|
2525
|
+
check "bounded wait: the run gave up within the bound (well under the holder's 30 s)" test "$((t1_ - t0_))" -le 8
|
|
2526
|
+
check "bounded wait: it did WAIT (at least one second elapsed) rather than refusing at once" test "$((t1_ - t0_))" -ge 1
|
|
2527
|
+
check "bounded wait: the marker names reason=env_lock_timeout once, value-free, on stderr; one human Error line on stdout" \
|
|
2528
|
+
bash -c '[[ "$(grep -c "^SOLEUR_CREDENTIAL_REFUSED script=bsky-setup.sh reason=env_lock_timeout var=ENV_FILE phase=write-env$" <<<"$1")" == 1 && "$1$2" != *g4aa-g4bb* && "$(grep -c "^Error:" <<<"$2")" == 1 ]]' _ "$ERR" "$OUT"
|
|
2529
|
+
check "bounded wait: .env is byte-identical and the live holder's lock was not touched" we_and 'we_identical' 'we_owner_is "$GITWORK" "$holder_"'
|
|
2530
|
+
kill "$holder_" 2>/dev/null; wait "$holder_" 2>/dev/null; rm -rf "$GITWORK/.env.lock.d"
|
|
2531
|
+
|
|
2532
|
+
# --- (vi-b) a symlinked .env is rewritten through the link; a dangling link and a directory are refused -------------------
|
|
2533
|
+
we_reset "$G4_SEED"; mkdir -p "$WED/real"; mv "$GITWORK/.env" "$WED/real/target.env"; ln -s "$WED/real/target.env" "$GITWORK/.env"; cp "$WED/real/target.env" "$SANDBOX/env.before"
|
|
2534
|
+
we_w_cfg bsky
|
|
2535
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2536
|
+
check "symlinked .env: exit 0" test "$RC" -eq 0
|
|
2537
|
+
check "symlinked .env: the link survives (still a symbolic link to the same target)" bash -c '[[ -L "$1/.env" && "$(readlink "$1/.env")" == "$2" ]]' _ "$GITWORK" "$WED/real/target.env"
|
|
2538
|
+
check "symlinked .env: the REAL target holds the new keys once each and the unrelated lines" \
|
|
2539
|
+
bash -c '[[ "$(grep -c "^BSKY_HANDLE=guard4.bsky.social$" "$1")" == 1 && "$(grep -c "^BSKY_APP_PASSWORD=" "$1")" == 1 && "$(grep -c "^KEEP_ME=1$" "$1")" == 1 ]]' _ "$WED/real/target.env"
|
|
2540
|
+
check "symlinked .env: the lock and temp were placed beside the TARGET, and nothing is left in either directory" we_and 'we_no_artifacts' '[[ -z "$(ls -A "$WED/real" | grep -v "^target.env$")" ]]'
|
|
2541
|
+
we_reset @none; mkdir -p "$WED/real"; ln -s "$WED/real/absent.env" "$GITWORK/.env"
|
|
2542
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2543
|
+
check "dangling .env link: refused (exit 1) with reason=env_dangling_link" bash -c '[[ "$1" -eq 1 && "$(grep -c "reason=env_dangling_link var=ENV_FILE phase=write-env$" <<<"$2")" == 1 ]]' _ "$RC" "$ERR"
|
|
2544
|
+
check "dangling .env link: the link is still a link, its target was not created and nothing was left behind" bash -c '[[ -L "$1/.env" && ! -e "$2/real/absent.env" ]]' _ "$GITWORK" "$WED"
|
|
2545
|
+
check "dangling .env link: no .env.wenv.* or .env.lock.d* anywhere beside the link" we_no_artifacts
|
|
2546
|
+
we_reset @none; mkdir "$GITWORK/.env"
|
|
2547
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2548
|
+
check "directory .env: refused (exit 1) with reason=env_not_regular" bash -c '[[ "$1" -eq 1 && "$(grep -c "reason=env_not_regular var=ENV_FILE phase=write-env$" <<<"$2")" == 1 ]]' _ "$RC" "$ERR"
|
|
2549
|
+
check "directory .env: it is still an empty directory and nothing was left behind" bash -c '[[ -d "$1/.env" && -z "$(ls -A "$1/.env")" ]]' _ "$GITWORK"
|
|
2550
|
+
check "directory .env: no .env.wenv.* or .env.lock.d* beside it" we_no_artifacts
|
|
2551
|
+
rm -rf "$GITWORK/.env"
|
|
2552
|
+
|
|
2553
|
+
# --- (vii) must-PASS: a different, legitimate value set into an .env full of unrelated odd-but-legal lines -------------------
|
|
2554
|
+
we_reset @none
|
|
2555
|
+
G4_ODD=$'# a comment with $dollar and `tick`\n\nexport KEEP_EXPORT=1\nKEEP_QUOTED="has $x and spaces"\nKEEP_SQ=\'single\'\nBSKY_HANDLE_OLD=keep-me\nPREFIX_BSKY_HANDLE=keep-too\nKEEP_HIGH=caf\xe9\n indented=ok\nNO_EQUALS_LINE\nBSKY_HANDLE=old.example\nBSKY_APP_PASSWORD=oldpw\nLAST_NO_NEWLINE=1'
|
|
2556
|
+
printf '%s' "$G4_ODD" > "$GITWORK/.env"; chmod 644 "$GITWORK/.env"
|
|
2557
|
+
cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
2558
|
+
we_w_cfg bsky
|
|
2559
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_E[@]}" -- bash "$WE_S" write-env
|
|
2560
|
+
check "must-PASS: a different legitimate value set writes (exit 0, no marker)" bash -c '[[ "$1" -eq 0 && "$2" != *SOLEUR_CREDENTIAL_REFUSED* ]]' _ "$RC" "$ERR"
|
|
2561
|
+
check "must-PASS: the new values are in .env once each, and an existing mode 644 file ends mode 600" we_and 'we_keys_once "${WE_K[@]}"' 'we_mode_600'
|
|
2562
|
+
check "must-PASS: every unrelated line survived byte for byte (comment, export, quotes, high byte, near-miss key names, a line with no =, a last line without a newline)" \
|
|
2563
|
+
bash -c 'g="$1"; shift; for l in "$@"; do grep -aqxF -- "$l" "$g/.env" || exit 1; done' _ "$GITWORK" \
|
|
2564
|
+
'# a comment with $dollar and `tick`' 'export KEEP_EXPORT=1' 'KEEP_QUOTED="has $x and spaces"' "KEEP_SQ='single'" 'BSKY_HANDLE_OLD=keep-me' 'PREFIX_BSKY_HANDLE=keep-too' $'KEEP_HIGH=caf\xe9' ' indented=ok' 'NO_EQUALS_LINE' 'LAST_NO_NEWLINE=1'
|
|
2565
|
+
check "must-PASS: the old values of the managed keys are gone" bash -c '! grep -aq "old.example\|oldpw" "$1/.env"' _ "$GITWORK"
|
|
2566
|
+
check "must-PASS: no artifact remains" we_no_artifacts
|
|
2567
|
+
rm -f "$GITWORK/.env"
|
|
2568
|
+
|
|
2569
|
+
# --- mutants: each validator weakening is caught by the rows above ------------------------------
|
|
2570
|
+
echo "== write-env mutants =="
|
|
2571
|
+
we_mutant_run() { # <label> <seed> <mutant-script> <bad-var> <class> <args...>
|
|
2572
|
+
local label="$1" seed="$2" script="$3" var="$4" cls="$5" e val; shift 5
|
|
2573
|
+
local -a envs=()
|
|
2574
|
+
we_class_val "$cls"; val="$WE_VAL"
|
|
2575
|
+
for e in "${WE_BASE[@]}"; do [[ "${e%%=*}" == "$var" ]] || envs+=("$e"); done
|
|
2576
|
+
envs+=("$var=$val")
|
|
2577
|
+
printf '%s\n' "$seed" > "$GITWORK/.env"; chmod 600 "$GITWORK/.env"
|
|
2578
|
+
cp "$GITWORK/.env" "$SANDBOX/env.before"
|
|
2579
|
+
RUN_CWD="$GITWORK" run_sut - "" "${envs[@]}" -- bash "$script" "$@"
|
|
2580
|
+
}
|
|
2581
|
+
# we_mut_fired <mutant-script>: the mutant PARSES (a syntax error would leave .env untouched and read as a pass)
|
|
2582
|
+
# AND its hostile write reached .env or fired the sentinel.
|
|
2583
|
+
we_mut_fired() { bash -n "$1" 2>/dev/null && not we_untouched; }
|
|
2584
|
+
WE_BASE=("BSKY_HANDLE=$BSKY_HANDLE_FIX" "BSKY_APP_PASSWORD=$BSKY_PW_FIX")
|
|
2585
|
+
M="$(mut_make bsky-setup.sh '_wenv_validate BSKY_HANDLE BSKY_APP_PASSWORD' ':')"
|
|
2586
|
+
we_mutant_run "bsky" "$BS_SEED" "$M" BSKY_HANDLE cmdsub write-env
|
|
2587
|
+
check "mutant: bsky-setup with the validator call removed writes the hostile value and the sentinel fires on source (RED on the hostile row)" we_mut_fired "$M"
|
|
2588
|
+
M="$(mut_make bsky-setup.sh '_wenv_validate BSKY_HANDLE BSKY_APP_PASSWORD' '_wenv_validate BSKY_HANDLE')"
|
|
2589
|
+
we_mutant_run "bsky" "$BS_SEED" "$M" BSKY_APP_PASSWORD cmdsub write-env
|
|
2590
|
+
check "mutant: bsky-setup validating only the first value lets a hostile SECOND value through (RED on the position-2 row)" we_mut_fired "$M"
|
|
2591
|
+
M="$(mut_make bsky-setup.sh '[!A-Za-z0-9._:/@%+=,-]' '[!A-Za-z0-9._:/@%+=,\ -]')"
|
|
2592
|
+
we_mutant_run "bsky" "$BS_SEED" "$M" BSKY_HANDLE space write-env
|
|
2593
|
+
check "mutant: bsky-setup with the allow-list widened to a space lets 'VAR=x cmd' through and the sentinel fires on source (RED on the hostile row)" we_mut_fired "$M"
|
|
2594
|
+
# The human line back on stderr: the refusal rows (one Error: line on STDOUT, none on stderr) go RED.
|
|
2595
|
+
we_mutant_refusal_red() { # <mutant> <seed> <var> <class> <args...>; WE_BASE is set. Refused still (exit 1) but the FORMAT row is RED.
|
|
2596
|
+
local m="$1" seed="$2" var="$3" cls="$4" e; shift 4
|
|
2597
|
+
local -a others=()
|
|
2598
|
+
we_mutant_run x "$seed" "$m" "$var" "$cls" "$@"
|
|
2599
|
+
we_class_val "$cls"
|
|
2600
|
+
for e in "${WE_BASE[@]}"; do [[ "${e%%=*}" == "$var" ]] || others+=("${e%%=*}"); done
|
|
2601
|
+
[[ "$RC" -eq 1 ]] && ! we_refusal_ok "$(basename "$m")" "$var" "$WE_REASON" "$WE_VAL" "${others[@]}"
|
|
2602
|
+
}
|
|
2603
|
+
M="$(mut_make bsky-setup.sh $'add it to .env by hand."\n exit 1' $'add it to .env by hand." >&2\n exit 1')"
|
|
2604
|
+
check "mutant: bsky-setup printing the allow-list human line on stderr again is RED on the refusal rows (still exit 1)" \
|
|
2605
|
+
we_mutant_refusal_red "$M" "$BS_SEED" BSKY_HANDLE semi write-env
|
|
2606
|
+
WE_BASE=("X_API_KEY=$X_KEY" "X_API_SECRET=$X_SECRET" "X_ACCESS_TOKEN=$X_TOK" "X_ACCESS_TOKEN_SECRET=$X_TOKSECRET")
|
|
2607
|
+
M="$(mut_make x-setup.sh '_wenv_validate X_API_KEY X_API_SECRET X_ACCESS_TOKEN X_ACCESS_TOKEN_SECRET' ':')"
|
|
2608
|
+
we_mutant_run "x" "$XS_SEED" "$M" X_ACCESS_TOKEN_SECRET backtick write-env
|
|
2609
|
+
check "mutant: x-setup with the validator call removed writes the hostile value and the sentinel fires on source (RED on the hostile row)" we_mut_fired "$M"
|
|
2610
|
+
# (In a case pattern `|` and `&` must be backslash-quoted: bare, the shell reads a separator or a
|
|
2611
|
+
# control operator, and the mutant would not even parse; we_mut_fired refuses an unparsable mutant.)
|
|
2612
|
+
M="$(mut_make x-setup.sh '[!A-Za-z0-9._:/@%+=,-]' '[!A-Za-z0-9._:/@%+=,\|-]')"
|
|
2613
|
+
we_mutant_run "x" "$XS_SEED" "$M" X_API_KEY pipe write-env
|
|
2614
|
+
check "mutant: x-setup with the allow-list widened to a pipe lets 'VAR=x|y' through, so the written .env differs from its seed (RED on the pipe row)" we_mut_fired "$M"
|
|
2615
|
+
WE_BASE=("LINKEDIN_CLIENT_ID=$LI_WE_ID" "LINKEDIN_CLIENT_SECRET=$LI_WE_SECRET" "LINKEDIN_ACCESS_TOKEN=$LI_WE_TOK" "LINKEDIN_PERSON_URN=$LI_WE_URN")
|
|
2616
|
+
M="$(mut_make linkedin-setup.sh '_wenv_validate LINKEDIN_CLIENT_ID LINKEDIN_CLIENT_SECRET LINKEDIN_ACCESS_TOKEN LINKEDIN_PERSON_URN' ':')"
|
|
2617
|
+
we_mutant_run "linkedin" "$LI_WE_SEED" "$M" LINKEDIN_PERSON_URN backtick write-env
|
|
2618
|
+
check "mutant: linkedin-setup with the validator call removed writes the hostile value and the sentinel fires on source (RED on the hostile row)" we_mut_fired "$M"
|
|
2619
|
+
M="$(mut_make linkedin-setup.sh '_wenv_validate LINKEDIN_CLIENT_ID LINKEDIN_CLIENT_SECRET LINKEDIN_ACCESS_TOKEN LINKEDIN_PERSON_URN' '_wenv_validate LINKEDIN_CLIENT_ID LINKEDIN_CLIENT_SECRET LINKEDIN_ACCESS_TOKEN')"
|
|
2620
|
+
we_mutant_run "linkedin" "$LI_WE_SEED" "$M" LINKEDIN_PERSON_URN semi write-env
|
|
2621
|
+
check "mutant: linkedin-setup validating only three of the four values lets a hostile PERSON_URN through (RED on the position-4 row)" we_mut_fired "$M"
|
|
2622
|
+
WE_BASE=("X_API_KEY=$X_KEY" "X_API_SECRET=$X_SECRET" "X_ACCESS_TOKEN=$X_TOK" "X_ACCESS_TOKEN_SECRET=$X_TOKSECRET")
|
|
2623
|
+
M="$(mut_make x-setup.sh $' reason=%s var=%s phase=write-env\\n\' "$SOLEUR_TRANSPORT_SCRIPT" "$1" "$2" >&2' $' reason=%s\\n\' "$SOLEUR_TRANSPORT_SCRIPT" "$1" >&2')"
|
|
2624
|
+
check "mutant: x-setup whose stderr marker lost var= and phase= is RED on the refusal rows (still exit 1)" \
|
|
2625
|
+
we_mutant_refusal_red "$M" "$XS_SEED" X_API_SECRET backtick write-env
|
|
2626
|
+
WE_BASE=("DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" "DISCORD_RELEASES_WEBHOOK_URL_INPUT=$DW_REL" "DISCORD_BLOG_WEBHOOK_URL_INPUT=$DW_BLOG")
|
|
2627
|
+
M="$(mut_make discord-setup.sh '_wenv_validate DISCORD_BOT_TOKEN_INPUT DISCORD_WEBHOOK_URL_INPUT DISCORD_RELEASES_WEBHOOK_URL_INPUT DISCORD_BLOG_WEBHOOK_URL_INPUT' ':')"
|
|
2628
|
+
we_mutant_run "discord" "$DS_SEED" "$M" DISCORD_BLOG_WEBHOOK_URL_INPUT semi write-env "$GUILD"
|
|
2629
|
+
check "mutant: discord-setup with the validator call removed writes a hostile optional webhook and the sentinel fires on source (RED on the hostile row)" we_mut_fired "$M"
|
|
2630
|
+
M="$(mut_make discord-setup.sh '[!A-Za-z0-9._:/@%+=,-]' '[!A-Za-z0-9._:/@%+=,\&-]')"
|
|
2631
|
+
we_mutant_run "discord" "$DS_SEED" "$M" DISCORD_WEBHOOK_URL_INPUT amp write-env "$GUILD"
|
|
2632
|
+
check "mutant: discord-setup with the allow-list widened to an ampersand lets 'VAR=x&y' through, so the written .env differs from its seed (RED on the amp row)" we_mut_fired "$M"
|
|
2633
|
+
# The Discord usage-error remedy: stdin not a terminal -> the file-read form; the check is inverted -> it vanishes.
|
|
2634
|
+
M="$(mut_make discord-setup.sh 'if [ ! -t 0 ]; then' 'if [ -t 0 ]; then')"
|
|
2635
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$M" write-env "$GUILD"
|
|
2636
|
+
check "mutant: discord-setup with the terminal test inverted drops the non-TTY remedy under stdin </dev/null (exit 64 still; RED on the non-TTY rows)" \
|
|
2637
|
+
bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
2638
|
+
check "mutant: that inverted-terminal-test mutant no longer gives the file-read form" not dw_remedy_non_tty
|
|
2639
|
+
# Each conjunct of dw_remedy_non_tty is driven to FAIL ALONE by a mutant that breaks only it.
|
|
2640
|
+
# (a) an inline `VAR=value command` form comes back: the no-inline conjunct (the rest of the remedy is intact).
|
|
2641
|
+
M="$(mut_make discord-setup.sh ' echo "Your stdin is not a terminal' ' echo " DISCORD_WEBHOOK_URL_INPUT=<webhook-url> discord-setup.sh write-env <guild_id>"
|
|
2642
|
+
echo "Your stdin is not a terminal')"
|
|
2643
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$M" write-env "$GUILD"
|
|
2644
|
+
check "mutant: the inline-assignment form restored into the non-TTY remedy: only the no-inline conjunct is RED (the file form and the token are still there)" \
|
|
2645
|
+
bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
2646
|
+
check "mutant: that inline-form mutant still has the file-read form and names the token (only the no-inline conjunct fails)" \
|
|
2647
|
+
bash -c '[[ "$1" == *"$2"* ]]' _ "$OUT" "$DW_FILEFORM"
|
|
2648
|
+
check "mutant: that inline-form mutant is caught by dw_no_inline alone" not dw_no_inline
|
|
2649
|
+
check "mutant: that inline-form mutant makes dw_remedy_non_tty RED" not dw_remedy_non_tty
|
|
2650
|
+
# (b) the printf/$URL pipe form comes back (it does not match the VAR=value regex: the printf conjunct).
|
|
2651
|
+
M="$(mut_make discord-setup.sh ' echo "Your stdin is not a terminal' ' echo " printf %s \"\$URL\" | { read -r DISCORD_WEBHOOK_URL_INPUT; export DISCORD_WEBHOOK_URL_INPUT; discord-setup.sh write-env <guild_id>; }"
|
|
2652
|
+
echo "Your stdin is not a terminal')"
|
|
2653
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$M" write-env "$GUILD"
|
|
2654
|
+
check "mutant: the printf-pipe form restored: it carries no VAR=value word, so the printf/\$URL arm alone catches it" \
|
|
2655
|
+
bash -c '[[ ! "$1" =~ $2 && "$1" == *printf* ]]' _ "$OUT" "$DW_INLINE_RE"
|
|
2656
|
+
check "mutant: that printf-pipe mutant is caught by dw_no_inline (through its printf/\$URL arm)" not dw_no_inline
|
|
2657
|
+
check "mutant: that printf-pipe mutant makes dw_remedy_non_tty RED" not dw_remedy_non_tty
|
|
2658
|
+
# (c) the bot token dropped from the remedy: the names-the-token conjunct.
|
|
2659
|
+
M="$(mut_make discord-setup.sh ' echo " read -rs DISCORD_BOT_TOKEN_INPUT; export DISCORD_BOT_TOKEN_INPUT"
|
|
2660
|
+
' '')"
|
|
2661
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$M" write-env "$GUILD"
|
|
2662
|
+
check "mutant: the remedy without the bot-token line: dw_names_token is RED (the rest of the remedy is intact)" not dw_names_token
|
|
2663
|
+
check "mutant: that token-less remedy still has no inline form (the other conjunct is not the one failing)" dw_no_inline
|
|
2664
|
+
# (e) every remaining conjunct, one mutant each that breaks ONLY it. The control runs first: the real
|
|
2665
|
+
# script has every conjunct green. Each from-string starts at the echo (the header comment of the
|
|
2666
|
+
# script carries near-copies of these texts, so a bare substring would land in the comment).
|
|
2667
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$DISCORD_SETUP" write-env "$GUILD"
|
|
2668
|
+
check "conjunct control: the real script gives the non-TTY remedy with every conjunct green (the named set below is empty)" test -z "$(dw_failed)"
|
|
2669
|
+
check "conjunct control: the conjunct list is the eleven predicates (a dropped name cannot hide a mutant)" test "${#DW_CONJ[@]}" -eq 11
|
|
2670
|
+
dw_alone_mut() { # <label> <the one conjunct expected to fail> <from> <to>
|
|
2671
|
+
local M; M="$(mut_make discord-setup.sh "$3" "$4")"
|
|
2672
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" -- bash "$M" write-env "$GUILD"
|
|
2673
|
+
check "mutant: $1: exit 64 still (the usage class), so only the remedy text moved" test "$RC" -eq 64
|
|
2674
|
+
check "mutant: $1: exactly $2 fails and no other conjunct" test "$(dw_failed)" = "$2"
|
|
2675
|
+
check "mutant: $1: dw_remedy_non_tty is RED" not dw_remedy_non_tty
|
|
2676
|
+
}
|
|
2677
|
+
dw_alone_mut "the file-read line no longer exports the bot token" dw_c_fileform \
|
|
2678
|
+
'echo " ( read -r DISCORD_WEBHOOK_URL_INPUT < /path/to/webhook-url.txt; read -r DISCORD_BOT_TOKEN_INPUT < /path/to/bot-token.txt; export DISCORD_WEBHOOK_URL_INPUT DISCORD_BOT_TOKEN_INPUT;' \
|
|
2679
|
+
'echo " ( read -r DISCORD_WEBHOOK_URL_INPUT < /path/to/webhook-url.txt; read -r DISCORD_BOT_TOKEN_INPUT < /path/to/bot-token.txt; export DISCORD_WEBHOOK_URL_INPUT;'
|
|
2680
|
+
dw_alone_mut "the remedy does not say stdin is not a terminal" dw_c_nontty \
|
|
2681
|
+
'echo "Your stdin is not a terminal, so' 'echo "Your stdin is a pipe, so'
|
|
2682
|
+
dw_alone_mut "create-webhook output is no longer redirected into the file (the URL would print)" dw_c_createwh \
|
|
2683
|
+
'discord-setup.sh create-webhook <channel_id> > /path/to/webhook-url.txt )' 'discord-setup.sh create-webhook <channel_id> )'
|
|
2684
|
+
dw_alone_mut "the webhook-file recipe lost its umask (a 0644 file)" dw_c_umask_hook \
|
|
2685
|
+
'echo " (umask 077; cat > /path/to/webhook-url.txt)"' 'echo " (cat > /path/to/webhook-url.txt)"'
|
|
2686
|
+
dw_alone_mut "the bot-token-file recipe lost its umask (a 0644 file)" dw_c_umask_token \
|
|
2687
|
+
'echo " (umask 077; cat > /path/to/bot-token.txt)"' 'echo " (cat > /path/to/bot-token.txt)"'
|
|
2688
|
+
dw_alone_mut "the existing-file chmod note is gone (a redirect keeps a wider mode)" dw_c_chmod \
|
|
2689
|
+
'run chmod 600 on it before writing' 'write to it'
|
|
2690
|
+
dw_alone_mut "create-webhook is no longer given the bot token" dw_c_cw_token \
|
|
2691
|
+
'export DISCORD_BOT_TOKEN_INPUT; umask 077; discord-setup.sh create-webhook' 'umask 077; discord-setup.sh create-webhook'
|
|
2692
|
+
dw_alone_mut "the one-command form lost its opening parenthesis (the exports would stay in the invoking shell)" dw_c_wrap_open \
|
|
2693
|
+
'echo " ( read -r DISCORD_WEBHOOK_URL_INPUT <' 'echo " read -r DISCORD_WEBHOOK_URL_INPUT <'
|
|
2694
|
+
dw_alone_mut "the one-command form lost its closing parenthesis (the exports would stay in the invoking shell)" dw_c_wrap_close \
|
|
2695
|
+
'discord-setup.sh write-env <guild_id> )"' 'discord-setup.sh write-env <guild_id>"'
|
|
2696
|
+
# (d) no guild_id: the usage branch reverted to a bash parameter error.
|
|
2697
|
+
M="$(mut_make discord-setup.sh 'if [[ $# -eq 0 ]]; then' 'if false; then')"
|
|
2698
|
+
dw_devnull_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$M" write-env
|
|
2699
|
+
check "mutant: discord-setup without the no-guild_id branch exits non-64 (RED on the exit-64 row)" bash -c '[[ "$1" -ne 64 ]]' _ "$RC"
|
|
2700
|
+
check "mutant: that no-guild_id mutant has no usage text on stdout (RED on the stdout row)" bash -c '[[ "$1" != *"Error: guild_id is missing"* ]]' _ "$OUT"
|
|
2701
|
+
# The snowflake echo restored: the webhook URL in the guild_id slot is printed.
|
|
2702
|
+
M="$(mut_make discord-setup.sh 'must be numeric. The value is not shown." >&2' 'must be numeric. Got: ${id}" >&2')"
|
|
2703
|
+
dw_untouched_run "DISCORD_BOT_TOKEN_INPUT=$DISC_TOK" "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$M" write-env "$DW_URL"
|
|
2704
|
+
check "mutant: discord-setup echoing 'Got: <value>' again prints the webhook URL (exit 1 still; RED on the redaction rows)" \
|
|
2705
|
+
bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2706
|
+
check "mutant: that Got-echo mutant leaks the URL, so the redaction row would be RED" not dw_no_secret
|
|
2707
|
+
# The bot-token exit class: back to the generic exit 1.
|
|
2708
|
+
M="$(mut_make discord-setup.sh 'is not set (or is empty). Pass the bot token in that environment variable, not as an argument."
|
|
2709
|
+
_print_env_remedy
|
|
2710
|
+
exit 64' 'is not set (or is empty). Pass the bot token in that environment variable, not as an argument."
|
|
2711
|
+
_print_env_remedy
|
|
2712
|
+
exit 1')"
|
|
2713
|
+
dw_untouched_run "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$M" write-env "$GUILD"
|
|
2714
|
+
check "mutant: discord-setup with the missing-bot-token exit class back to 1 is RED on the exit-64 row" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2715
|
+
# The missing-token branch without its remedy (the pre-fix shape: usage + error only, no form to follow).
|
|
2716
|
+
M="$(mut_make discord-setup.sh 'is not set (or is empty). Pass the bot token in that environment variable, not as an argument."
|
|
2717
|
+
_print_env_remedy
|
|
2718
|
+
exit 64' 'is not set (or is empty). Pass the bot token in that environment variable, not as an argument."
|
|
2719
|
+
exit 64')"
|
|
2720
|
+
dw_untouched_run "DISCORD_WEBHOOK_URL_INPUT=$DW_URL" -- bash "$M" write-env "$GUILD"
|
|
2721
|
+
check "mutant: discord-setup whose missing-bot-token branch prints no remedy still exits 64 (so only the remedy rows can catch it)" bash -c '[[ "$1" -eq 64 ]]' _ "$RC"
|
|
2722
|
+
check "mutant: that remedy-less missing-bot-token mutant is RED on dw_names_token" not dw_names_token
|
|
2723
|
+
# The human text of write-env on STDOUT (missing credentials, the confirmation): each moved line put back on stderr is RED.
|
|
2724
|
+
xs_ok_env=("X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" "X_ACCESS_TOKEN_SECRET=$XS_TS")
|
|
2725
|
+
M="$(mut_make x-setup.sh 'require_credentials 2>&1' 'require_credentials')"
|
|
2726
|
+
RUN_CWD="$GITWORK" run_sut - "" "X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" -- bash "$M" write-env
|
|
2727
|
+
check "mutant: x-setup write-env with the missing-credentials text back on stderr is RED on the stdout row (still exit 1)" \
|
|
2728
|
+
bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2729
|
+
check "mutant: that stderr-diagnostic x-setup mutant fails we_missing_stdout" not we_missing_stdout X_ACCESS_TOKEN_SECRET
|
|
2730
|
+
M="$(mut_make x-setup.sh 'echo "Wrote 4 variables to ${env_file} (permissions: 600)"' 'echo "Wrote 4 variables to ${env_file} (permissions: 600)" >&2')"
|
|
2731
|
+
RUN_CWD="$GITWORK" run_sut - "" "${xs_ok_env[@]}" -- bash "$M" write-env
|
|
2732
|
+
check "mutant: x-setup write-env with the confirmation back on stderr still exits 0" bash -c '[[ "$1" -eq 0 ]]' _ "$RC"
|
|
2733
|
+
check "mutant: that stderr-confirmation x-setup mutant fails we_wrote_stdout" not we_wrote_stdout 4
|
|
2734
|
+
M="$(mut_make bsky-setup.sh 'require_credentials 2>&1' 'require_credentials')"
|
|
2735
|
+
RUN_CWD="$GITWORK" run_sut - "" "BSKY_HANDLE=$BSKY_HANDLE_FIX" -- bash "$M" write-env
|
|
2736
|
+
check "mutant: bsky-setup write-env with the missing-credentials text back on stderr is RED on the stdout row (still exit 1)" \
|
|
2737
|
+
bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2738
|
+
check "mutant: that stderr-diagnostic bsky-setup mutant fails we_missing_stdout" not we_missing_stdout BSKY_APP_PASSWORD
|
|
2739
|
+
# The missing-credentials advice (stdout of write-env) points at the no-history form, never an inline assignment.
|
|
2740
|
+
xs_advice_ok() { [[ "$OUT" == *"read -rs X_API_KEY; export X_API_KEY"* && "$OUT" == *"kept in the transcript and the shell history"* && "$OUT" != *"X_API_KEY="* ]]; }
|
|
2741
|
+
bs_advice_ok() { [[ "$OUT" == *"read -rs BSKY_APP_PASSWORD; export BSKY_APP_PASSWORD"* && "$OUT" == *"kept in the transcript and the shell history"* && "$OUT" != *"BSKY_APP_PASSWORD="* ]]; }
|
|
2742
|
+
RUN_CWD="$GITWORK" run_sut - "" "X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" -- bash "$X_SETUP" write-env
|
|
2743
|
+
check "x-setup write-env with a credential missing: exit 1 and the advice shows the read -rs form and no inline assignment" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2744
|
+
check "x-setup write-env with a credential missing: the advice text is read -rs, the transcript/history reason, no X_API_KEY=" xs_advice_ok
|
|
2745
|
+
M="$(mut_make x-setup.sh ', without typing a value into the command (its text is kept in the transcript and the shell history): in a terminal, read -rs X_API_KEY; export X_API_KEY, and the same for each of the other three' '')"
|
|
2746
|
+
RUN_CWD="$GITWORK" run_sut - "" "X_API_KEY=$XS_K" "X_API_SECRET=$XS_S" "X_ACCESS_TOKEN=$XS_T" -- bash "$M" write-env
|
|
2747
|
+
check "mutant: x-setup with the bare 'Export them as environment variables' advice back is RED on the advice row (still exit 1)" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2748
|
+
check "mutant: that bare-advice x-setup mutant fails xs_advice_ok" not xs_advice_ok
|
|
2749
|
+
RUN_CWD="$GITWORK" run_sut - "" "BSKY_HANDLE=$BSKY_HANDLE_FIX" -- bash "$BSKY_SETUP" write-env
|
|
2750
|
+
check "bsky-setup write-env with a credential missing: exit 1" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2751
|
+
check "bsky-setup write-env with a credential missing: the advice text is read -rs, the transcript/history reason, no BSKY_APP_PASSWORD=" bs_advice_ok
|
|
2752
|
+
M="$(mut_make bsky-setup.sh ', without typing a value into the command (its text is kept in the transcript and the shell history): in a terminal, read -rs BSKY_APP_PASSWORD; export BSKY_APP_PASSWORD' '')"
|
|
2753
|
+
RUN_CWD="$GITWORK" run_sut - "" "BSKY_HANDLE=$BSKY_HANDLE_FIX" -- bash "$M" write-env
|
|
2754
|
+
check "mutant: bsky-setup with the bare 'Export ... as environment variables' advice back is RED on the advice row (still exit 1)" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2755
|
+
check "mutant: that bare-advice bsky-setup mutant fails bs_advice_ok" not bs_advice_ok
|
|
2756
|
+
# The same advice in the two community scripts (stderr, exit 1).
|
|
2757
|
+
xs_advice_all_ok() { [[ "$OUT$ERR" == *"read -rs X_API_KEY; export X_API_KEY"* && "$OUT$ERR" == *"kept in the transcript and the shell history"* && "$OUT$ERR" != *"X_API_KEY="* ]]; }
|
|
2758
|
+
bs_advice_all_ok() { [[ "$OUT$ERR" == *"read -rs BSKY_APP_PASSWORD; export BSKY_APP_PASSWORD"* && "$OUT$ERR" == *"kept in the transcript and the shell history"* && "$OUT$ERR" != *"BSKY_APP_PASSWORD="* ]]; }
|
|
2759
|
+
run_sut - "" -- bash "$X_COMMUNITY" fetch-metrics
|
|
2760
|
+
check "x-community with no credentials: exit 1, curl never invoked, and the advice shows the read -rs form and no inline assignment" \
|
|
2761
|
+
bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
2762
|
+
check "x-community with no credentials: the advice text is read -rs, the transcript/history reason, no X_API_KEY=" xs_advice_all_ok
|
|
2763
|
+
M="$(mut_make x-community.sh ', without typing a value into the command (its text is kept in the transcript and the shell history): in a terminal, read -rs X_API_KEY; export X_API_KEY, and the same for each of the other three' '')"
|
|
2764
|
+
run_sut - "" -- bash "$M" fetch-metrics
|
|
2765
|
+
check "mutant: x-community with the bare advice back is RED on the advice row (still exit 1)" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2766
|
+
check "mutant: that bare-advice x-community mutant fails xs_advice_all_ok" not xs_advice_all_ok
|
|
2767
|
+
run_sut - "" -- bash "$BSKY_COMMUNITY" create-session
|
|
2768
|
+
check "bsky-community with no credentials: exit 1, curl never invoked" bash -c '[[ "$1" -eq 1 && "$2" -eq 0 ]]' _ "$RC" "$(curl_calls)"
|
|
2769
|
+
check "bsky-community with no credentials: the advice text is read -rs, the transcript/history reason, no BSKY_APP_PASSWORD=" bs_advice_all_ok
|
|
2770
|
+
M="$(mut_make bsky-community.sh ', without typing a value into the command (its text is kept in the transcript and the shell history): in a terminal, read -rs BSKY_APP_PASSWORD; export BSKY_APP_PASSWORD' '')"
|
|
2771
|
+
run_sut - "" -- bash "$M" create-session
|
|
2772
|
+
check "mutant: bsky-community with the bare advice back is RED on the advice row (still exit 1)" bash -c '[[ "$1" -eq 1 ]]' _ "$RC"
|
|
2773
|
+
check "mutant: that bare-advice bsky-community mutant fails bs_advice_all_ok" not bs_advice_all_ok
|
|
2774
|
+
M="$(mut_make bsky-setup.sh 'echo "Wrote 2 variables to ${env_file} (permissions: 600)"' 'echo "Wrote 2 variables to ${env_file} (permissions: 600)" >&2')"
|
|
2775
|
+
RUN_CWD="$GITWORK" run_sut - "" "BSKY_HANDLE=$BSKY_HANDLE_FIX" "BSKY_APP_PASSWORD=$BSKY_PW_FIX" -- bash "$M" write-env
|
|
2776
|
+
check "mutant: bsky-setup write-env with the confirmation back on stderr still exits 0" bash -c '[[ "$1" -eq 0 ]]' _ "$RC"
|
|
2777
|
+
check "mutant: that stderr-confirmation bsky-setup mutant fails we_wrote_stdout" not we_wrote_stdout 2
|
|
2778
|
+
M="$(mut_make discord-setup.sh 'echo "Wrote ${var_count} variables to ${env_file} (permissions: 600)"' 'echo "Wrote ${var_count} variables to ${env_file} (permissions: 600)" >&2')"
|
|
2779
|
+
RUN_CWD="$GITWORK" run_sut - "" "${WE_BASE[@]}" -- bash "$M" write-env "$GUILD"
|
|
2780
|
+
check "mutant: discord-setup write-env with the confirmation back on stderr still exits 0" bash -c '[[ "$1" -eq 0 ]]' _ "$RC"
|
|
2781
|
+
check "mutant: that stderr-confirmation discord-setup mutant fails we_wrote_stdout" not we_wrote_stdout 5
|
|
2782
|
+
# The usage table: every command row's " - " separator sits in one column (the verify row had lost a space).
|
|
2783
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$DISCORD_SETUP"
|
|
2784
|
+
dw_usage_aligned() { # <usage-text>: six command rows, one separator column
|
|
2785
|
+
awk '/^ [a-z]/ { c = index($0, " - "); if (c == 0) bad = 1; if (col == 0) col = c; else if (c != col) bad = 1; n++ } END { exit !(n == 6 && !bad) }' <<<"$1"
|
|
2786
|
+
}
|
|
2787
|
+
check "discord-setup usage: exit 1 with the table on stderr, nothing on stdout" bash -c '[[ "$1" -eq 1 && -z "$2" ]]' _ "$RC" "$OUT"
|
|
2788
|
+
check "discord-setup usage: the six command rows share one separator column" dw_usage_aligned "$ERR"
|
|
2789
|
+
M="$(mut_make discord-setup.sh "echo \" verify$(printf '%26s' '')- Run" "echo \" verify$(printf '%25s' '')- Run")"
|
|
2790
|
+
RUN_CWD="$GITWORK" run_sut - "" -- bash "$M"
|
|
2791
|
+
check "mutant: discord-setup usage with the verify row one column short is RED on the alignment row" not dw_usage_aligned "$ERR"
|
|
2792
|
+
# Positive control for the sentinel mechanism itself: a known-bad .env fires it, a clean one does not.
|
|
2793
|
+
printf 'PC=$(touch %s)\n' "$SENT" > "$GITWORK/.env"
|
|
2794
|
+
check "sentinel control: sourcing a known-bad .env fires the sentinel (the mechanism is real)" we_fired
|
|
2795
|
+
printf 'PC=clean\n' > "$GITWORK/.env"
|
|
2796
|
+
check "sentinel control: sourcing a clean .env does not fire it" not we_fired
|
|
2797
|
+
rm -f "$GITWORK/.env" "$SENT"
|
|
2798
|
+
|
|
2799
|
+
# --- the oracle under other bash builds ---------------------------------------------------------
|
|
2800
|
+
echo "== hmac-sha1-b64.sh: the oracle under the oldest bash and inside the pinned image =="
|
|
2801
|
+
bash_num() { "$1" -c 'echo $((BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1]))' 2>/dev/null; }
|
|
2802
|
+
OLD_BASH="" OLD_NUM=9999
|
|
2803
|
+
for cand_ in $(type -ap bash) /bin/bash /usr/bin/bash /usr/local/bin/bash /opt/homebrew/bin/bash; do
|
|
2804
|
+
[[ -x "$cand_" ]] || continue
|
|
2805
|
+
v_="$(bash_num "$cand_")"; [[ "$v_" =~ ^[0-9]+$ ]] || continue
|
|
2806
|
+
if (( v_ < OLD_NUM )); then OLD_NUM=$v_; OLD_BASH="$cand_"; fi
|
|
2807
|
+
done
|
|
2808
|
+
DEF_NUM=$((BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1]))
|
|
2809
|
+
if (( OLD_NUM < DEF_NUM )); then
|
|
2810
|
+
OLD_OUT="$("$OLD_BASH" "${BASH_SOURCE[0]}" --oracle-only 2>&1)"; OLD_RC=$?
|
|
2811
|
+
check "oracle under the oldest bash on this host ($OLD_BASH, ${OLD_NUM}): all 67 comparisons equal the reference" \
|
|
2812
|
+
bash -c '[[ "$1" -eq 0 && "$2" == *" cases=67 "* && "$2" == *" bad=0"* ]]' _ "$OLD_RC" "$OLD_OUT"
|
|
2813
|
+
echo " note - oldest bash ${OLD_NUM} is older than the default ${DEF_NUM}: exercised"
|
|
2814
|
+
else
|
|
2815
|
+
# The running bash IS the oldest here, so a child --oracle-only pass would repeat the in-process
|
|
2816
|
+
# matrix above byte for byte (a second full pass for nothing): reuse that run's counters instead.
|
|
2817
|
+
echo " note - the oldest bash on this host (${OLD_NUM}) is the running bash: no second pass; an OLDER bash (macOS 3.2) was NOT exercised here"
|
|
2818
|
+
check "oracle under the oldest bash on this host (the running bash ${DEF_NUM}; the in-process matrix above is that run): all 67 comparisons equal the reference" \
|
|
2819
|
+
bash -c '[[ "$1" -eq 67 && "$2" -eq 0 ]]' _ "$ORC_RAN" "$ORC_BAD"
|
|
2820
|
+
fi
|
|
2821
|
+
# The pinned base image has NO openssl of its own (measured); the runner image gets it from the
|
|
2822
|
+
# `ca-certificates` package its Dockerfile installs, which depends on it. So the run installs that one
|
|
2823
|
+
# package first. That needs the network and a pull, so it is opt-in: COMMUNITY_ARGV_DOCKER=1. Without
|
|
2824
|
+
# it (or without docker) the row reports "not exercised" and still counts as one row.
|
|
2825
|
+
IMG_STATE="not exercised (needs docker and COMMUNITY_ARGV_DOCKER=1: the pinned image has no openssl, so the run installs ca-certificates, as the runner Dockerfile does)"
|
|
2826
|
+
IMG_OUT="" IMG_RC=0
|
|
2827
|
+
# The same opt-in switch also runs the oracle under a REAL bash 3.2 (the macOS /bin/bash build, #9757 lead h):
|
|
2828
|
+
# the host bash is whatever the developer has, and the pinned image above is 5.2, so without this nothing
|
|
2829
|
+
# in the repo executes hmac-sha1-b64.sh under 3.2. The official `bash:3.2` image is alpine with bash 3.2.57;
|
|
2830
|
+
# it ships no openssl, so the run adds the one package. Measured 2026-10-12: bash 3.2.57(1)-release,
|
|
2831
|
+
# cases=67 bad=0. A moving tag, deliberately not digest-pinned: it is an opt-in cross-check of the
|
|
2832
|
+
# language level, not the image the repo builds (that one is pinned above).
|
|
2833
|
+
BASH32_IMAGE="bash:3.2"
|
|
2834
|
+
B32_OUT="" B32_RC=0
|
|
2835
|
+
# The same opt-in run also executes the write-env LOCK PATH (the mkdir owner-pid lock, the stale-owner
|
|
2836
|
+
# recovery under the recovery mutex and four writers released together) under the real bash 3.2.57: it is the platform the
|
|
2837
|
+
# lock was written to (no flock, no `exec {fd}<`, no `local -n`). Opt-in like the rows above, not a gate;
|
|
2838
|
+
# a failure is reported as a failing row, a pass as a note.
|
|
2839
|
+
B32L_OUT="" B32L_RC=0
|
|
2840
|
+
read -r -d '' B32_LOCK_SCRIPT <<'B32LOCK_EOF' || true
|
|
2841
|
+
set -u
|
|
2842
|
+
apk add --no-cache git jq openssl curl >/dev/null 2>&1 || { echo "B32LOCK skip: no apk"; exit 0; }
|
|
2843
|
+
S=/plugin/skills/community/scripts; W=$(mktemp -d); cd "$W" && git init -q . && printf 'KEEP_ME=1\nBSKY_HANDLE=old\nX_API_KEY=old\n' > .env
|
|
2844
|
+
run() { # <script> <env...>
|
|
2845
|
+
s="$1"; shift; env "$@" bash "$S/$s" write-env >/dev/null 2>&1; echo $? > "rc.$s"
|
|
2846
|
+
}
|
|
2847
|
+
# stale lock from a dead owner, then four different writers released together
|
|
2848
|
+
sleep 0 & dead=$!; wait "$dead"; mkdir .env.lock.d; echo "$dead" > .env.lock.d/owner
|
|
2849
|
+
run bsky-setup.sh BSKY_HANDLE=g.b.s BSKY_APP_PASSWORD=pw-a &
|
|
2850
|
+
run x-setup.sh X_API_KEY=k1 X_API_SECRET=s1 X_ACCESS_TOKEN=t1 X_ACCESS_TOKEN_SECRET=ts1 &
|
|
2851
|
+
run linkedin-setup.sh LINKEDIN_CLIENT_ID=i LINKEDIN_CLIENT_SECRET=s LINKEDIN_ACCESS_TOKEN=t LINKEDIN_PERSON_URN=urn:li:person:Z &
|
|
2852
|
+
wait
|
|
2853
|
+
bad=0
|
|
2854
|
+
for f in rc.*; do [ "$(cat "$f")" = 0 ] || { echo "B32LOCK rc $f=$(cat "$f")"; bad=1; }; done
|
|
2855
|
+
for k in KEEP_ME BSKY_HANDLE BSKY_APP_PASSWORD X_API_KEY X_API_SECRET X_ACCESS_TOKEN X_ACCESS_TOKEN_SECRET LINKEDIN_CLIENT_ID LINKEDIN_PERSON_URN; do
|
|
2856
|
+
[ "$(grep -c "^$k=" .env)" = 1 ] || { echo "B32LOCK key $k count=$(grep -c "^$k=" .env)"; bad=1; }
|
|
2857
|
+
done
|
|
2858
|
+
ls -A | grep -E '^\.env\.(tmp|wenv|lock)' && { echo "B32LOCK artifacts left"; bad=1; }
|
|
2859
|
+
[ "$(stat -c %a .env)" = 600 ] || { echo "B32LOCK mode"; bad=1; }
|
|
2860
|
+
echo "B32LOCK bash=$BASH_VERSION bad=$bad"
|
|
2861
|
+
B32LOCK_EOF
|
|
2862
|
+
if [[ -n "${COMMUNITY_ARGV_DOCKER:-}" ]] && command -v docker >/dev/null 2>&1; then
|
|
2863
|
+
IMG_OUT="$(docker run --rm -v "$HERE/..:/skill:ro" "$PINNED_IMAGE" bash -c 'apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq --no-install-recommends ca-certificates >/dev/null 2>&1 && command -v openssl >/dev/null && bash /skill/test/community-argv.test.sh --oracle-only' 2>&1)"; IMG_RC=$?
|
|
2864
|
+
IMG_STATE="exercised"
|
|
2865
|
+
B32_OUT="$(docker run --rm -v "$HERE/..:/skill:ro" "$BASH32_IMAGE" bash -c 'apk add --no-cache openssl >/dev/null 2>&1 && command -v openssl >/dev/null && bash /skill/test/community-argv.test.sh --oracle-only' 2>&1)"; B32_RC=$?
|
|
2866
|
+
B32L_OUT="$(docker run --rm -v "$HERE/../../..:/plugin:ro" "$BASH32_IMAGE" bash -c "$B32_LOCK_SCRIPT" 2>&1)"; B32L_RC=$?
|
|
2867
|
+
fi
|
|
2868
|
+
if [[ "$IMG_STATE" == exercised ]]; then
|
|
2869
|
+
if [[ "$B32L_RC" -eq 0 && "$B32L_OUT" == *"bash=3.2."*" bad=0"* ]]; then echo " note - write-env lock path under bash 3.2: ${B32L_OUT##*$'\n'}"; else bad "write-env lock path under a real bash 3.2 (the bash:3.2 image): ${B32L_OUT}"; fi
|
|
2870
|
+
check "oracle inside the pinned node:22-slim image (the hosted bash 5.2): all 67 comparisons equal the reference" \
|
|
2871
|
+
bash -c '[[ "$1" -eq 0 && "$2" == *" cases=67 "* && "$2" == *" bad=0"* ]]' _ "$IMG_RC" "$IMG_OUT"
|
|
2872
|
+
check "oracle under a real bash 3.2 (the bash:3.2 image): the run reports bash=3.2.x and all 67 comparisons equal the reference" \
|
|
2873
|
+
bash -c '[[ "$1" -eq 0 && "$2" == *" bash=3.2."* && "$2" == *" cases=67 "* && "$2" == *" bad=0"* ]]' _ "$B32_RC" "$B32_OUT"
|
|
2874
|
+
else
|
|
2875
|
+
echo " note - oracle inside the pinned image and under bash 3.2: $IMG_STATE"
|
|
2876
|
+
check "oracle under a real bash 3.2: not exercised on this host, and the image is still the 3.2 line (a different tag would silently run another bash)" \
|
|
2877
|
+
bash -c '[[ "$1" =~ ^bash:3\.2$ ]]' _ "$BASH32_IMAGE"
|
|
2878
|
+
check "oracle inside the pinned image: not exercised on this host, and the image reference is still digest-pinned (a floating tag would silently change the bash the oracle runs under)" \
|
|
2879
|
+
bash -c '[[ "$1" =~ ^node:22-slim@sha256:[0-9a-f]{64}$ ]]' _ "$PINNED_IMAGE"
|
|
2880
|
+
fi
|
|
2881
|
+
# The image this oracle runs in is the image the repo BUILDS: compare PINNED_IMAGE with the FROM line(s)
|
|
2882
|
+
# of apps/web-platform/Dockerfile (read-only here), so a Dockerfile bump cannot leave the oracle on the
|
|
2883
|
+
# old image with a green row, and a different 64-hex digest cannot satisfy it.
|
|
2884
|
+
DOCKERFILE_REL="../../../../../apps/web-platform/Dockerfile"
|
|
2885
|
+
dockerfile_pins_image() { # <Dockerfile>: every `FROM node:22-slim@sha256:...` line carries exactly PINNED_IMAGE, and there is at least one
|
|
2886
|
+
local imgs; imgs="$(awk '/^FROM node:22-slim@sha256:/ { print $2 }' "$1" 2>/dev/null | sort -u)"
|
|
2887
|
+
[[ -n "$imgs" && "$imgs" == "$PINNED_IMAGE" ]]
|
|
2888
|
+
}
|
|
2889
|
+
check "pinned image: PINNED_IMAGE equals the node:22-slim digest of every FROM line in apps/web-platform/Dockerfile (not a literal that only this file checks)" \
|
|
2890
|
+
dockerfile_pins_image "$HERE/$DOCKERFILE_REL"
|
|
2891
|
+
mkdir -p "$SANDBOX/dockerfile"
|
|
2892
|
+
# The tamper rewrites the FIRST digest character of every FROM line to a different one, whatever it is
|
|
2893
|
+
# (a digest bump changes the leading character, and a tamper keyed on one hard-coded character would then
|
|
2894
|
+
# read RED as an instrument error). 0 becomes 1; any other character becomes 0.
|
|
2895
|
+
tamper_digest() { # <in> <out>
|
|
2896
|
+
sed -E -e 's/^(FROM node:22-slim@sha256:)0/\11/' -e t -e 's/^(FROM node:22-slim@sha256:)[^0]/\10/' "$1" > "$2"
|
|
2897
|
+
}
|
|
2898
|
+
tamper_digest "$HERE/$DOCKERFILE_REL" "$SANDBOX/dockerfile/Dockerfile.bumped"
|
|
2899
|
+
tamper_all_hex() { # every leading hex digit is changed by tamper_digest, on a synthetic one-line Dockerfile
|
|
2900
|
+
local c n=0
|
|
2901
|
+
for c in 0 1 2 3 4 5 6 7 8 9 a b c d e f; do
|
|
2902
|
+
printf 'FROM node:22-slim@sha256:%s%s AS base\n' "$c" "$(printf '%063d' 0)" > "$SANDBOX/dockerfile/lead.in"
|
|
2903
|
+
tamper_digest "$SANDBOX/dockerfile/lead.in" "$SANDBOX/dockerfile/lead.out"
|
|
2904
|
+
cmp -s "$SANDBOX/dockerfile/lead.in" "$SANDBOX/dockerfile/lead.out" && return 1
|
|
2905
|
+
n=$((n + 1))
|
|
2906
|
+
done
|
|
2907
|
+
[[ "$n" -eq 16 ]]
|
|
2908
|
+
}
|
|
2909
|
+
check "pinned image: the tamper changes the leading digest character for each of the sixteen hex digits (it does not depend on the current digest)" tamper_all_hex
|
|
2910
|
+
check "pinned image: the doctored Dockerfile (one digest character changed on every FROM line) differs from the real one" not cmp -s "$HERE/$DOCKERFILE_REL" "$SANDBOX/dockerfile/Dockerfile.bumped"
|
|
2911
|
+
check "pinned image: a Dockerfile whose digest moved on is RED (PINNED_IMAGE no longer matches)" not dockerfile_pins_image "$SANDBOX/dockerfile/Dockerfile.bumped"
|
|
2912
|
+
check "pinned image: a Dockerfile with no node:22-slim FROM line is RED (an empty comparison cannot pass)" not dockerfile_pins_image /dev/null
|
|
2913
|
+
echo
|
|
2914
|
+
echo "community-argv.test.sh: $PASS passed, $FAIL failed"
|
|
2915
|
+
# Exact count, in the pre-existing guard-invisible form: a lower-case `-lt` floor would make this
|
|
2916
|
+
# `plugins/soleur/skills/*/test/` suite floor-bearing, which grows the DEFERRED ledger in
|
|
2917
|
+
# scripts/guard-vacuity-floor.test.sh (47 -> 48) until the file is promoted there.
|
|
2918
|
+
if [[ "$((PASS + FAIL))" -ne 1596 ]]; then
|
|
2919
|
+
printf 'ANTI-VACUITY FLOOR: ran %s assertions, expected exactly 1596\n' "$((PASS + FAIL))" >&2
|
|
2920
|
+
exit 1
|
|
2921
|
+
fi
|
|
2922
|
+
[[ "$FAIL" -eq 0 ]]
|