@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.
Files changed (832) hide show
  1. package/.claude-plugin/agents.manifest.json +476 -0
  2. package/.claude-plugin/plugin.json +42 -0
  3. package/README.md +44 -2
  4. package/agents/community/.gitkeep +0 -0
  5. package/agents/engineering/cto.md +87 -0
  6. package/agents/engineering/design/ddd-architect.md +95 -0
  7. package/agents/engineering/discovery/agent-finder.md +214 -0
  8. package/agents/engineering/discovery/functional-discovery.md +182 -0
  9. package/agents/engineering/infra/infra-security.md +151 -0
  10. package/agents/engineering/infra/platform-strategist.md +109 -0
  11. package/agents/engineering/infra/terraform-architect.md +171 -0
  12. package/agents/engineering/prompt-engineer.md +37 -0
  13. package/agents/engineering/research/best-practices-researcher.md +115 -0
  14. package/agents/engineering/research/framework-docs-researcher.md +93 -0
  15. package/agents/engineering/research/git-history-analyzer.md +53 -0
  16. package/agents/engineering/research/learnings-researcher.md +285 -0
  17. package/agents/engineering/research/repo-research-analyst.md +118 -0
  18. package/agents/engineering/review/agent-native-reviewer.md +279 -0
  19. package/agents/engineering/review/architecture-strategist.md +61 -0
  20. package/agents/engineering/review/code-quality-analyst.md +92 -0
  21. package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
  22. package/agents/engineering/review/data-integrity-guardian.md +82 -0
  23. package/agents/engineering/review/data-migration-expert.md +98 -0
  24. package/agents/engineering/review/deployment-verification-agent.md +183 -0
  25. package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
  26. package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
  27. package/agents/engineering/review/legacy-code-expert.md +55 -0
  28. package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
  29. package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
  30. package/agents/engineering/review/performance-oracle.md +130 -0
  31. package/agents/engineering/review/security-sentinel.md +167 -0
  32. package/agents/engineering/review/semgrep-sast.md +36 -0
  33. package/agents/engineering/review/test-design-reviewer.md +150 -0
  34. package/agents/engineering/review/user-impact-reviewer.md +70 -0
  35. package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
  36. package/agents/finance/budget-analyst.md +29 -0
  37. package/agents/finance/cfo.md +44 -0
  38. package/agents/finance/financial-reporter.md +30 -0
  39. package/agents/finance/revenue-analyst.md +30 -0
  40. package/agents/legal/clo.md +115 -0
  41. package/agents/legal/legal-compliance-auditor.md +110 -0
  42. package/agents/legal/legal-document-generator.md +134 -0
  43. package/agents/marketing/analytics-analyst.md +25 -0
  44. package/agents/marketing/brand-architect.md +181 -0
  45. package/agents/marketing/cmo.md +76 -0
  46. package/agents/marketing/conversion-optimizer.md +19 -0
  47. package/agents/marketing/copywriter.md +20 -0
  48. package/agents/marketing/fact-checker.md +70 -0
  49. package/agents/marketing/growth-strategist.md +145 -0
  50. package/agents/marketing/paid-media-strategist.md +28 -0
  51. package/agents/marketing/pricing-strategist.md +17 -0
  52. package/agents/marketing/programmatic-seo-specialist.md +25 -0
  53. package/agents/marketing/retention-strategist.md +23 -0
  54. package/agents/marketing/seo-aeo-analyst.md +164 -0
  55. package/agents/operations/coo.md +50 -0
  56. package/agents/operations/ops-advisor.md +78 -0
  57. package/agents/operations/ops-provisioner.md +101 -0
  58. package/agents/operations/ops-research.md +52 -0
  59. package/agents/operations/service-automator.md +278 -0
  60. package/agents/product/business-validator.md +206 -0
  61. package/agents/product/competitive-intelligence.md +89 -0
  62. package/agents/product/cpo.md +63 -0
  63. package/agents/product/design/ux-design-lead.md +209 -0
  64. package/agents/product/spec-flow-analyzer.md +122 -0
  65. package/agents/sales/cro.md +44 -0
  66. package/agents/sales/deal-architect.md +29 -0
  67. package/agents/sales/outbound-strategist.md +29 -0
  68. package/agents/sales/pipeline-analyst.md +29 -0
  69. package/agents/support/cco.md +48 -0
  70. package/agents/support/community-manager.md +570 -0
  71. package/agents/support/ticket-triage.md +70 -0
  72. package/bin/soleur-opencode.js +29 -0
  73. package/commands/go.md +660 -0
  74. package/commands/help.md +273 -0
  75. package/commands/sync.md +977 -0
  76. package/commands/version.md +103 -0
  77. package/hooks/antigravity-hooks.json +84 -0
  78. package/hooks/antigravity-session-start.sh +15 -0
  79. package/hooks/bare-root-cwd-guard.sh +131 -0
  80. package/hooks/browser-snapshot-credential-guard.sh +200 -0
  81. package/hooks/codex-session-start.sh +9 -0
  82. package/hooks/compaction-state.sh +525 -0
  83. package/hooks/destructive-command-guard.sh +1387 -0
  84. package/hooks/devin-session-start.sh +90 -0
  85. package/hooks/ext-filter-detect.sh +171 -0
  86. package/hooks/hooks.json +133 -0
  87. package/hooks/lib/hook-tool-kind.sh +24 -0
  88. package/hooks/lib/log-rotation.sh +176 -0
  89. package/hooks/lib/shell-argv.pl +576 -0
  90. package/hooks/memory-backstop.sh +1114 -0
  91. package/hooks/operator-stage-approval.sh +391 -0
  92. package/hooks/stop-hook.sh +367 -0
  93. package/hooks/unkept-promise-hook.sh +383 -0
  94. package/hooks/version-banner.sh +127 -0
  95. package/hooks/welcome-hook.sh +50 -0
  96. package/install.js +29 -0
  97. package/lib/agent-registry.ts +154 -0
  98. package/lib/c4-canonical-cli.mjs +34 -0
  99. package/lib/c4-canonical.mjs +46 -0
  100. package/lib/c4-from-components.ts +554 -0
  101. package/lib/go-routing.ts +81 -0
  102. package/lib/grok-inspect-contract.ts +311 -0
  103. package/lib/harness-model-map.ts +101 -0
  104. package/lib/harness-parity.ts +726 -0
  105. package/lib/harness-tool-map.ts +180 -0
  106. package/lib/harness.ts +716 -0
  107. package/lib/heartbeat-live-reconcile.ts +1153 -0
  108. package/lib/heartbeat-manifest.ts +361 -0
  109. package/lib/kb-coverage.ts +252 -0
  110. package/lib/pr-merge-poll.ts +103 -0
  111. package/lib/workflow-fidelity.ts +388 -0
  112. package/opencode/INSTRUCTIONS.md +171 -0
  113. package/opencode/agents/engineering/cto.md +10 -0
  114. package/opencode/agents/engineering/design/ddd-architect.md +10 -0
  115. package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
  116. package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
  117. package/opencode/agents/engineering/infra/infra-security.md +10 -0
  118. package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
  119. package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
  120. package/opencode/agents/engineering/prompt-engineer.md +10 -0
  121. package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
  122. package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
  123. package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
  124. package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
  125. package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
  126. package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
  127. package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
  128. package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
  129. package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
  130. package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
  131. package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
  132. package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
  133. package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
  134. package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
  135. package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
  136. package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
  137. package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
  138. package/opencode/agents/engineering/review/performance-oracle.md +10 -0
  139. package/opencode/agents/engineering/review/security-sentinel.md +10 -0
  140. package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
  141. package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
  142. package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
  143. package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
  144. package/opencode/agents/finance/budget-analyst.md +10 -0
  145. package/opencode/agents/finance/cfo.md +10 -0
  146. package/opencode/agents/finance/financial-reporter.md +10 -0
  147. package/opencode/agents/finance/revenue-analyst.md +10 -0
  148. package/opencode/agents/legal/clo.md +10 -0
  149. package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
  150. package/opencode/agents/legal/legal-document-generator.md +10 -0
  151. package/opencode/agents/marketing/analytics-analyst.md +10 -0
  152. package/opencode/agents/marketing/brand-architect.md +10 -0
  153. package/opencode/agents/marketing/cmo.md +10 -0
  154. package/opencode/agents/marketing/conversion-optimizer.md +10 -0
  155. package/opencode/agents/marketing/copywriter.md +10 -0
  156. package/opencode/agents/marketing/fact-checker.md +10 -0
  157. package/opencode/agents/marketing/growth-strategist.md +10 -0
  158. package/opencode/agents/marketing/paid-media-strategist.md +10 -0
  159. package/opencode/agents/marketing/pricing-strategist.md +10 -0
  160. package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
  161. package/opencode/agents/marketing/retention-strategist.md +10 -0
  162. package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
  163. package/opencode/agents/operations/coo.md +10 -0
  164. package/opencode/agents/operations/ops-advisor.md +10 -0
  165. package/opencode/agents/operations/ops-provisioner.md +10 -0
  166. package/opencode/agents/operations/ops-research.md +10 -0
  167. package/opencode/agents/operations/service-automator.md +10 -0
  168. package/opencode/agents/product/business-validator.md +10 -0
  169. package/opencode/agents/product/competitive-intelligence.md +10 -0
  170. package/opencode/agents/product/cpo.md +10 -0
  171. package/opencode/agents/product/design/ux-design-lead.md +10 -0
  172. package/opencode/agents/product/spec-flow-analyzer.md +10 -0
  173. package/opencode/agents/sales/cro.md +10 -0
  174. package/opencode/agents/sales/deal-architect.md +10 -0
  175. package/opencode/agents/sales/outbound-strategist.md +10 -0
  176. package/opencode/agents/sales/pipeline-analyst.md +10 -0
  177. package/opencode/agents/support/cco.md +10 -0
  178. package/opencode/agents/support/community-manager.md +10 -0
  179. package/opencode/agents/support/ticket-triage.md +10 -0
  180. package/opencode/commands/go.md +10 -0
  181. package/opencode/commands/help.md +10 -0
  182. package/opencode/commands/sync.md +10 -0
  183. package/opencode/commands/version.md +11 -0
  184. package/opencode/opencode.json +8 -0
  185. package/opencode/plugin.ts +818 -0
  186. package/package.json +36 -4
  187. package/scripts/admin-merge-ready.sh +545 -0
  188. package/scripts/admin-merge-ready.test.sh +753 -0
  189. package/scripts/affected-tests.sh +908 -0
  190. package/scripts/affected-tests.test.sh +1797 -0
  191. package/scripts/alpha-metrics.sh +78 -0
  192. package/scripts/alpha-metrics.test.sh +115 -0
  193. package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
  194. package/scripts/audit-flag-flip.sh +107 -0
  195. package/scripts/check-red-on-main.sh +316 -0
  196. package/scripts/ci-head-verdict.sh +411 -0
  197. package/scripts/cloud-detect.sh +193 -0
  198. package/scripts/delegation-verify.sh +657 -0
  199. package/scripts/delegation-verify.test.sh +1424 -0
  200. package/scripts/deploy-arm.sh +475 -0
  201. package/scripts/domain-model-drift.sh +419 -0
  202. package/scripts/emit-decision.sh +144 -0
  203. package/scripts/emit-decision.test.sh +144 -0
  204. package/scripts/ext-filter-divergence-corpus.txt +15 -0
  205. package/scripts/ext-filter-divergence.sh +189 -0
  206. package/scripts/generate-c4-from-components.ts +442 -0
  207. package/scripts/grok-fidelity-gate.sh +68 -0
  208. package/scripts/grok-pre-push-gate.sh +232 -0
  209. package/scripts/harness-discovery-smoke.ts +906 -0
  210. package/scripts/harness-parity-census.ts +92 -0
  211. package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
  212. package/scripts/install-opencode-adapter.ts +509 -0
  213. package/scripts/lib/affected-tests/common.sh +365 -0
  214. package/scripts/lib/affected-tests/go.sh +157 -0
  215. package/scripts/lib/affected-tests/js.sh +188 -0
  216. package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
  217. package/scripts/lib/affected-tests/python.sh +185 -0
  218. package/scripts/lib/context-sanitize.sh +49 -0
  219. package/scripts/lib/domain-model-lib.sh +180 -0
  220. package/scripts/lib/ext-filter-grammar.tsv +34 -0
  221. package/scripts/lib/operator-script.sh +1236 -0
  222. package/scripts/lib/proc.sh +580 -0
  223. package/scripts/lib/session-state.sh +748 -0
  224. package/scripts/lib/tmp-classify.sh +843 -0
  225. package/scripts/lib/wrapper-boundary.sh +47 -0
  226. package/scripts/lint-ratchet.py +1547 -0
  227. package/scripts/monitor-pr-checks.sh +441 -0
  228. package/scripts/pipeline-tally.sh +756 -0
  229. package/scripts/pipeline-tally.test.sh +740 -0
  230. package/scripts/plugin-version.sh +528 -0
  231. package/scripts/precommit-guard.sh +115 -0
  232. package/scripts/precommit-guard.test.sh +254 -0
  233. package/scripts/reconcile-live-heartbeats.ts +705 -0
  234. package/scripts/render-c4-model.sh +217 -0
  235. package/scripts/resolve-git-root.sh +55 -0
  236. package/scripts/resolve-regenerable-conflicts.sh +363 -0
  237. package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
  238. package/scripts/ruff-requirements.txt +22 -0
  239. package/scripts/run-in-pid-namespace.sh +98 -0
  240. package/scripts/run-in-pid-namespace.test.sh +290 -0
  241. package/scripts/scan-ancestor-signal-helpers.py +556 -0
  242. package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
  243. package/scripts/setup-ralph-loop.sh +238 -0
  244. package/scripts/sync-cursor-name-map.ts +316 -0
  245. package/scripts/sync-grok-agent-compat.ts +188 -0
  246. package/scripts/sync-npm-package.ts +165 -0
  247. package/scripts/sync-opencode-adapter.ts +498 -0
  248. package/scripts/sync-pr-behind.sh +647 -0
  249. package/scripts/sync-pr-behind.test.sh +252 -0
  250. package/scripts/taste-profile-update.sh +180 -0
  251. package/scripts/taste-profile-update.test.sh +167 -0
  252. package/scripts/write-budget-marker.sh +23 -0
  253. package/scripts/write-kb-coverage.ts +113 -0
  254. package/skills/admin-ip-refresh/SKILL.md +81 -0
  255. package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
  256. package/skills/agent-browser/SKILL.md +698 -0
  257. package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
  258. package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
  259. package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
  260. package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
  261. package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
  262. package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
  263. package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
  264. package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
  265. package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
  266. package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
  267. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
  268. package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
  269. package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
  270. package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
  271. package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
  272. package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
  273. package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
  274. package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
  275. package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
  276. package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
  277. package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
  278. package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
  279. package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
  280. package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
  281. package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
  282. package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
  283. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
  284. package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
  285. package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
  286. package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
  287. package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
  288. package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
  289. package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
  290. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
  291. package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
  292. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
  293. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
  294. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
  295. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
  296. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
  297. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
  298. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
  299. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
  300. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
  301. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
  302. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
  303. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
  304. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
  305. package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
  306. package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
  307. package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
  308. package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
  309. package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
  310. package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
  311. package/skills/agent-native-architecture/SKILL.md +479 -0
  312. package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
  313. package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
  314. package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
  315. package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
  316. package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
  317. package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
  318. package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
  319. package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
  320. package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
  321. package/skills/agent-native-architecture/references/product-implications.md +478 -0
  322. package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
  323. package/skills/agent-native-architecture/references/self-modification.md +294 -0
  324. package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
  325. package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
  326. package/skills/agent-native-audit/SKILL.md +300 -0
  327. package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
  328. package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
  329. package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
  330. package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
  331. package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
  332. package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
  333. package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
  334. package/skills/architecture/SKILL.md +436 -0
  335. package/skills/architecture/references/adr-template.md +154 -0
  336. package/skills/architecture/references/likec4-reference.md +136 -0
  337. package/skills/architecture/references/nfr-reference.md +126 -0
  338. package/skills/archive-kb/SKILL.md +86 -0
  339. package/skills/archive-kb/scripts/archive-kb.sh +300 -0
  340. package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
  341. package/skills/atdd-developer/SKILL.md +65 -0
  342. package/skills/brainstorm/SKILL.md +701 -0
  343. package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
  344. package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
  345. package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
  346. package/skills/brainstorm/references/ui-surface-terms.md +42 -0
  347. package/skills/brainstorm-techniques/SKILL.md +231 -0
  348. package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
  349. package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
  350. package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
  351. package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
  352. package/skills/campaign-calendar/SKILL.md +145 -0
  353. package/skills/cf-token-scope/SKILL.md +164 -0
  354. package/skills/cf-token-scope/references/widen-playbook.md +114 -0
  355. package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
  356. package/skills/changelog/SKILL.md +142 -0
  357. package/skills/code-to-prd/SKILL.md +66 -0
  358. package/skills/code-to-prd/references/banner-template.md +12 -0
  359. package/skills/code-to-prd/references/prd-template.md +41 -0
  360. package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
  361. package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
  362. package/skills/code-to-prd/test/fixture/.env.example +7 -0
  363. package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
  364. package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
  365. package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
  366. package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
  367. package/skills/code-to-prd/test/fixture/package.json +6 -0
  368. package/skills/cohort-status/SKILL.md +62 -0
  369. package/skills/community/SKILL.md +210 -0
  370. package/skills/community/scripts/bsky-community.sh +644 -0
  371. package/skills/community/scripts/bsky-setup.sh +631 -0
  372. package/skills/community/scripts/community-router.sh +93 -0
  373. package/skills/community/scripts/discord-community.sh +487 -0
  374. package/skills/community/scripts/discord-setup.sh +784 -0
  375. package/skills/community/scripts/github-community.sh +706 -0
  376. package/skills/community/scripts/hn-community.sh +233 -0
  377. package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
  378. package/skills/community/scripts/linkedin-community.sh +750 -0
  379. package/skills/community/scripts/linkedin-setup.sh +758 -0
  380. package/skills/community/scripts/x-community.sh +890 -0
  381. package/skills/community/scripts/x-setup.sh +713 -0
  382. package/skills/community/test/community-argv.test.sh +2922 -0
  383. package/skills/community/test/github-community.test.sh +981 -0
  384. package/skills/competitive-analysis/SKILL.md +69 -0
  385. package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
  386. package/skills/compound/SKILL.md +673 -0
  387. package/skills/compound/scripts/token-efficiency-report.sh +334 -0
  388. package/skills/compound/test/phase-16.test.sh +379 -0
  389. package/skills/compound-capture/SKILL.md +791 -0
  390. package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
  391. package/skills/compound-capture/assets/resolution-template.md +116 -0
  392. package/skills/compound-capture/references/yaml-schema.md +67 -0
  393. package/skills/compound-capture/schema.yaml +181 -0
  394. package/skills/constraint-scaffold/SKILL.md +175 -0
  395. package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
  396. package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
  397. package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
  398. package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
  399. package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
  400. package/skills/constraint-scaffold/references/shared-runner.template +135 -0
  401. package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
  402. package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
  403. package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
  404. package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
  405. package/skills/constraint-scaffold/test/generator.test.sh +93 -0
  406. package/skills/constraint-scaffold/test/parity.test.sh +285 -0
  407. package/skills/content-writer/SKILL.md +242 -0
  408. package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
  409. package/skills/cron-delete/SKILL.md +56 -0
  410. package/skills/cron-list/SKILL.md +54 -0
  411. package/skills/deepen-plan/SKILL.md +937 -0
  412. package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
  413. package/skills/deploy/SKILL.md +115 -0
  414. package/skills/deploy/references/hetzner-setup.md +67 -0
  415. package/skills/deploy/scripts/deploy.sh +37 -0
  416. package/skills/deploy-docs/SKILL.md +95 -0
  417. package/skills/dhh-rails-style/SKILL.md +203 -0
  418. package/skills/dhh-rails-style/references/architecture.md +717 -0
  419. package/skills/dhh-rails-style/references/controllers.md +323 -0
  420. package/skills/dhh-rails-style/references/frontend.md +556 -0
  421. package/skills/dhh-rails-style/references/gems.md +316 -0
  422. package/skills/dhh-rails-style/references/models.md +388 -0
  423. package/skills/dhh-rails-style/references/testing.md +354 -0
  424. package/skills/discord-content/SKILL.md +126 -0
  425. package/skills/docs-site/SKILL.md +214 -0
  426. package/skills/drain-labeled-backlog/SKILL.md +203 -0
  427. package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
  428. package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
  429. package/skills/drain-prs/SKILL.md +143 -0
  430. package/skills/drain-prs/scripts/triage-prs.sh +192 -0
  431. package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
  432. package/skills/dspy-ruby/SKILL.md +634 -0
  433. package/skills/dspy-ruby/assets/config-template.rb +359 -0
  434. package/skills/dspy-ruby/assets/module-template.rb +326 -0
  435. package/skills/dspy-ruby/assets/signature-template.rb +143 -0
  436. package/skills/dspy-ruby/references/core-concepts.md +268 -0
  437. package/skills/dspy-ruby/references/optimization.md +623 -0
  438. package/skills/dspy-ruby/references/providers.md +335 -0
  439. package/skills/eval-harness/README.md +194 -0
  440. package/skills/eval-harness/SKILL.md +157 -0
  441. package/skills/eval-harness/enums/go-routes.json +13 -0
  442. package/skills/eval-harness/enums/incident-threshold.json +5 -0
  443. package/skills/eval-harness/enums/lane.json +5 -0
  444. package/skills/eval-harness/enums/tool-selection.json +20 -0
  445. package/skills/eval-harness/enums/triage-levels.json +5 -0
  446. package/skills/eval-harness/gated-skills.json +34 -0
  447. package/skills/eval-harness/models.generated.json +5 -0
  448. package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
  449. package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
  450. package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
  451. package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
  452. package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
  453. package/skills/eval-harness/prompts/go-baseline.txt +6 -0
  454. package/skills/eval-harness/prompts/go-skill.txt +21 -0
  455. package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
  456. package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
  457. package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
  458. package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
  459. package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
  460. package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
  461. package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
  462. package/skills/eval-harness/prompts/triage-skill.txt +10 -0
  463. package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
  464. package/skills/eval-harness/scripts/extract-block.cjs +87 -0
  465. package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
  466. package/skills/eval-harness/scripts/gen-models.sh +41 -0
  467. package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
  468. package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
  469. package/skills/eval-harness/scripts/parse-label.cjs +93 -0
  470. package/skills/eval-harness/scripts/verdict.cjs +132 -0
  471. package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
  472. package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
  473. package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
  474. package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
  475. package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
  476. package/skills/eval-harness/test/eval-gate.test.sh +132 -0
  477. package/skills/eval-harness/test/extract-block.test.sh +87 -0
  478. package/skills/eval-harness/test/gate-classification.test.sh +77 -0
  479. package/skills/eval-harness/test/gen-models.test.sh +75 -0
  480. package/skills/eval-harness/test/measure-classification.test.sh +74 -0
  481. package/skills/eval-harness/test/parse-label.test.sh +129 -0
  482. package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
  483. package/skills/eval-harness/test/verdict.test.sh +123 -0
  484. package/skills/every-style-editor/SKILL.md +142 -0
  485. package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
  486. package/skills/feature-tweet/SKILL.md +199 -0
  487. package/skills/feature-video/SKILL.md +412 -0
  488. package/skills/feature-video/scripts/check_deps.sh +281 -0
  489. package/skills/file-todos/SKILL.md +267 -0
  490. package/skills/file-todos/assets/todo-template.md +166 -0
  491. package/skills/fix-issue/SKILL.md +278 -0
  492. package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
  493. package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
  494. package/skills/flag-bootstrap/SETUP.md +142 -0
  495. package/skills/flag-create/SKILL.md +145 -0
  496. package/skills/flag-create/scripts/create.sh +273 -0
  497. package/skills/flag-delete/SKILL.md +167 -0
  498. package/skills/flag-delete/scripts/delete.sh +288 -0
  499. package/skills/flag-list/SKILL.md +99 -0
  500. package/skills/flag-list/scripts/list.sh +275 -0
  501. package/skills/flag-set-role/SKILL.md +229 -0
  502. package/skills/flag-set-role/scripts/flip.sh +774 -0
  503. package/skills/frontend-anti-slop/SKILL.md +106 -0
  504. package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
  505. package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
  506. package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
  507. package/skills/frontend-design/SKILL.md +77 -0
  508. package/skills/gdpr-gate/NOTICE +136 -0
  509. package/skills/gdpr-gate/SKILL.md +341 -0
  510. package/skills/gdpr-gate/references/fields.md +160 -0
  511. package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
  512. package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
  513. package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
  514. package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
  515. package/skills/gdpr-gate/references/layers/frontend.md +249 -0
  516. package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
  517. package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
  518. package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
  519. package/skills/gdpr-gate/references/legal-consent.md +140 -0
  520. package/skills/gdpr-gate/references/non-negotiables.md +60 -0
  521. package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
  522. package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
  523. package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
  524. package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
  525. package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
  526. package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
  527. package/skills/gemini-imagegen/SKILL.md +283 -0
  528. package/skills/gemini-imagegen/requirements.txt +5 -0
  529. package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
  530. package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
  531. package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
  532. package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
  533. package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
  534. package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
  535. package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
  536. package/skills/git-worktree/SKILL.md +534 -0
  537. package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
  538. package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
  539. package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
  540. package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
  541. package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
  542. package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
  543. package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
  544. package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
  545. package/skills/go/SKILL.md +54 -0
  546. package/skills/growth/SKILL.md +164 -0
  547. package/skills/harvest-debt/SKILL.md +78 -0
  548. package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
  549. package/skills/heal-skill/SKILL.md +173 -0
  550. package/skills/help/SKILL.md +46 -0
  551. package/skills/incident/NOTICE +39 -0
  552. package/skills/incident/SKILL.md +395 -0
  553. package/skills/incident/scripts/dry-run.sh +448 -0
  554. package/skills/incident/scripts/redact-engine.py +412 -0
  555. package/skills/incident/scripts/redact-sentinel.sh +44 -0
  556. package/skills/incident/templates/pir.md +139 -0
  557. package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
  558. package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
  559. package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
  560. package/skills/incident/test/fixtures/positive-corpus.md +87 -0
  561. package/skills/incident/test/redact-sentinel.test.sh +1420 -0
  562. package/skills/invoice/SKILL.md +206 -0
  563. package/skills/kb-glossary/SKILL.md +71 -0
  564. package/skills/kb-glossary/references/glossary-format.md +111 -0
  565. package/skills/kb-glossary/references/rejected-request-register.md +115 -0
  566. package/skills/kb-search/SKILL.md +276 -0
  567. package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
  568. package/skills/legal-audit/SKILL.md +161 -0
  569. package/skills/legal-generate/NOTICE +242 -0
  570. package/skills/legal-generate/SKILL.md +244 -0
  571. package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
  572. package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
  573. package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
  574. package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
  575. package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
  576. package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
  577. package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
  578. package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
  579. package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
  580. package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
  581. package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
  582. package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
  583. package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
  584. package/skills/linear-fetch/SKILL.md +247 -0
  585. package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
  586. package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
  587. package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
  588. package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
  589. package/skills/linear-fetch/test/parity.test.sh +112 -0
  590. package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
  591. package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
  592. package/skills/merge-pr/SKILL.md +670 -0
  593. package/skills/model-launch-review/SKILL.md +255 -0
  594. package/skills/model-launch-review/scripts/audit-models.sh +501 -0
  595. package/skills/one-shot/SKILL.md +316 -0
  596. package/skills/one-shot/references/token-discipline.md +74 -0
  597. package/skills/operator-bootstrap/SKILL.md +412 -0
  598. package/skills/operator-bootstrap/template.sh +439 -0
  599. package/skills/operator-digest/SKILL.md +287 -0
  600. package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
  601. package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
  602. package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
  603. package/skills/operator-rephrase/SKILL.md +98 -0
  604. package/skills/pencil-setup/SKILL.md +201 -0
  605. package/skills/pencil-setup/scripts/check_deps.sh +556 -0
  606. package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
  607. package/skills/pencil-setup/scripts/package-lock.json +1172 -0
  608. package/skills/pencil-setup/scripts/package.json +14 -0
  609. package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
  610. package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
  611. package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
  612. package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
  613. package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
  614. package/skills/plan/SKILL.md +1140 -0
  615. package/skills/plan/references/differential-migration-testing.md +185 -0
  616. package/skills/plan/references/plan-community-discovery.md +42 -0
  617. package/skills/plan/references/plan-founder-check.md +137 -0
  618. package/skills/plan/references/plan-functional-overlap.md +25 -0
  619. package/skills/plan/references/plan-host-fanout.md +51 -0
  620. package/skills/plan/references/plan-issue-templates.md +779 -0
  621. package/skills/plan/references/plan-network-outage-checklist.md +131 -0
  622. package/skills/plan/references/plan-scope-check.md +126 -0
  623. package/skills/plan/references/plan-sharp-edges.md +331 -0
  624. package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
  625. package/skills/plan/references/webapp-cwv-observability.md +143 -0
  626. package/skills/plan-review/SKILL.md +91 -0
  627. package/skills/plan-review/lib/named-panel.mjs +58 -0
  628. package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
  629. package/skills/postmerge/SKILL.md +642 -0
  630. package/skills/postmerge/references/deploy-status-debugging.md +141 -0
  631. package/skills/postmerge/references/graceful-degradation.md +20 -0
  632. package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
  633. package/skills/preflight/SKILL.md +1553 -0
  634. package/skills/preflight/references/check-13-founder-check.md +286 -0
  635. package/skills/preflight/scripts/founder-check.py +1640 -0
  636. package/skills/preflight/scripts/parse-form-a.awk +107 -0
  637. package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
  638. package/skills/product-roadmap/SKILL.md +357 -0
  639. package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
  640. package/skills/provision-cloudflare/SKILL.md +67 -0
  641. package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
  642. package/skills/provision-doppler/SKILL.md +58 -0
  643. package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
  644. package/skills/provision-github/SKILL.md +59 -0
  645. package/skills/provision-github/scripts/provision-github.sh +300 -0
  646. package/skills/provision-hetzner/SKILL.md +76 -0
  647. package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
  648. package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
  649. package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
  650. package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
  651. package/skills/qa/SKILL.md +263 -0
  652. package/skills/qa/references/browser-preflight.md +22 -0
  653. package/skills/questionnaire-generate/SKILL.md +328 -0
  654. package/skills/questionnaire-generate/references/questionnaire.template +60 -0
  655. package/skills/rclone/SKILL.md +162 -0
  656. package/skills/rclone/scripts/check_setup.sh +63 -0
  657. package/skills/release-announce/SKILL.md +65 -0
  658. package/skills/release-docs/SKILL.md +92 -0
  659. package/skills/reproduce-bug/SKILL.md +358 -0
  660. package/skills/resolve-debt/SKILL.md +85 -0
  661. package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
  662. package/skills/resolve-parallel/SKILL.md +45 -0
  663. package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
  664. package/skills/resolve-pr-parallel/SKILL.md +68 -0
  665. package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
  666. package/skills/resolve-todo-parallel/SKILL.md +57 -0
  667. package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
  668. package/skills/review/SKILL.md +1620 -0
  669. package/skills/review/references/anti-slop-scanner-hook.md +40 -0
  670. package/skills/review/references/defect-classes-migrations.md +9 -0
  671. package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
  672. package/skills/review/references/review-e2e-testing.md +116 -0
  673. package/skills/review/references/review-evidence-comment.md +36 -0
  674. package/skills/review/references/review-todo-structure.md +165 -0
  675. package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
  676. package/skills/review/references/semgrep-custom-rules.yaml +174 -0
  677. package/skills/review/references/ultrathink-phases.md +98 -0
  678. package/skills/review/references/wfs.md +6 -0
  679. package/skills/review/scripts/emit-review-trailer.sh +499 -0
  680. package/skills/review/scripts/ensure-semgrep.sh +99 -0
  681. package/skills/review/scripts/fix-round-seats.sh +197 -0
  682. package/skills/review/test/emit-review-trailer.test.sh +387 -0
  683. package/skills/review/test/ensure-semgrep.test.sh +111 -0
  684. package/skills/review/test/fix-round-seats.test.sh +237 -0
  685. package/skills/review/workflows/README.md +147 -0
  686. package/skills/review/workflows/review.workflow.js +667 -0
  687. package/skills/schedule/SKILL.md +796 -0
  688. package/skills/seo-aeo/SKILL.md +139 -0
  689. package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
  690. package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
  691. package/skills/ship/SKILL.md +3023 -0
  692. package/skills/ship/references/adr-ordinal-collision.md +47 -0
  693. package/skills/ship/references/affected-gate-contract.md +115 -0
  694. package/skills/ship/references/ci-run-diagnosis.md +29 -0
  695. package/skills/ship/references/ci-workflow-authoring.md +68 -0
  696. package/skills/ship/references/cleanup-merged-archival.md +39 -0
  697. package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
  698. package/skills/ship/references/cto-ruling-armed-push.md +5 -0
  699. package/skills/ship/references/decision-challenges-render.md +71 -0
  700. package/skills/ship/references/delegated-act-authoring-form.md +7 -0
  701. package/skills/ship/references/detached-battery-verdict.md +13 -0
  702. package/skills/ship/references/epic-close-sweep.md +62 -0
  703. package/skills/ship/references/feature-tweet-draft.md +42 -0
  704. package/skills/ship/references/followthrough-issue-body-template.md +47 -0
  705. package/skills/ship/references/followthrough-stub-template.sh +92 -0
  706. package/skills/ship/references/merge-queue-dequeue.md +39 -0
  707. package/skills/ship/references/operator-holds.md +179 -0
  708. package/skills/ship/references/pipeline-tally-render.md +43 -0
  709. package/skills/ship/references/pr-overlap.md +153 -0
  710. package/skills/ship/references/queue-mode.md +7 -0
  711. package/skills/ship/references/ready-run-wait.md +36 -0
  712. package/skills/ship/references/red-on-main-quarantine.md +110 -0
  713. package/skills/ship/references/register-update-pr-pattern.md +88 -0
  714. package/skills/ship/references/settle-then-admin-merge.md +130 -0
  715. package/skills/ship/references/supabase-column-probe.md +13 -0
  716. package/skills/ship/scripts/auto-close-scan.sh +76 -0
  717. package/skills/ship/scripts/battery-owed.sh +403 -0
  718. package/skills/ship/scripts/hold-gate-eval.sh +410 -0
  719. package/skills/ship/scripts/net-issue-flow.sh +665 -0
  720. package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
  721. package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
  722. package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
  723. package/skills/skill-creator/SKILL.md +299 -0
  724. package/skills/skill-creator/references/api-security.md +248 -0
  725. package/skills/skill-creator/references/authoring-levers.md +110 -0
  726. package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
  727. package/skills/skill-creator/references/best-practices.md +431 -0
  728. package/skills/skill-creator/references/common-patterns.md +639 -0
  729. package/skills/skill-creator/references/core-principles.md +193 -0
  730. package/skills/skill-creator/references/executable-code.md +188 -0
  731. package/skills/skill-creator/references/iteration-and-testing.md +509 -0
  732. package/skills/skill-creator/references/official-spec.md +193 -0
  733. package/skills/skill-creator/references/recommended-structure.md +180 -0
  734. package/skills/skill-creator/references/skill-structure.md +396 -0
  735. package/skills/skill-creator/references/using-scripts.md +122 -0
  736. package/skills/skill-creator/references/using-templates.md +120 -0
  737. package/skills/skill-creator/references/workflows-and-validation.md +541 -0
  738. package/skills/skill-creator/scripts/init_skill.py +322 -0
  739. package/skills/skill-creator/scripts/package_skill.py +110 -0
  740. package/skills/skill-creator/scripts/quick_validate.py +64 -0
  741. package/skills/skill-creator/templates/router-skill.md +73 -0
  742. package/skills/skill-creator/templates/simple-skill.md +34 -0
  743. package/skills/skill-creator/workflows/add-reference.md +96 -0
  744. package/skills/skill-creator/workflows/add-script.md +104 -0
  745. package/skills/skill-creator/workflows/add-template.md +83 -0
  746. package/skills/skill-creator/workflows/add-workflow.md +128 -0
  747. package/skills/skill-creator/workflows/audit-skill.md +159 -0
  748. package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
  749. package/skills/skill-creator/workflows/create-new-skill.md +207 -0
  750. package/skills/skill-creator/workflows/get-guidance.md +134 -0
  751. package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
  752. package/skills/skill-creator/workflows/verify-skill.md +226 -0
  753. package/skills/skill-security-scan/SKILL.md +145 -0
  754. package/skills/skill-security-scan/references/disclaimer.md +21 -0
  755. package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
  756. package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
  757. package/skills/skill-security-scan/references/override-mechanism.md +171 -0
  758. package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
  759. package/skills/skill-security-scan/references/regex-patterns.md +136 -0
  760. package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
  761. package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
  762. package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
  763. package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
  764. package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
  765. package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
  766. package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
  767. package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
  768. package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
  769. package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
  770. package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
  771. package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
  772. package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
  773. package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
  774. package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
  775. package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
  776. package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
  777. package/skills/skill-security-scan/scripts/lib.sh +197 -0
  778. package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
  779. package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
  780. package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
  781. package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
  782. package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
  783. package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
  784. package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
  785. package/skills/social-distribute/SKILL.md +485 -0
  786. package/skills/spec-templates/SKILL.md +274 -0
  787. package/skills/sync/SKILL.md +49 -0
  788. package/skills/test-browser/SKILL.md +464 -0
  789. package/skills/test-browser/references/feature-map.md +161 -0
  790. package/skills/test-fix-loop/SKILL.md +159 -0
  791. package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
  792. package/skills/triage/SKILL.md +402 -0
  793. package/skills/trigger-cron/SKILL.md +136 -0
  794. package/skills/trigger-cron/scripts/trigger.sh +198 -0
  795. package/skills/user-set-role/SKILL.md +147 -0
  796. package/skills/user-set-role/scripts/set-role.sh +218 -0
  797. package/skills/user-story-writer/SKILL.md +77 -0
  798. package/skills/ux-audit/SKILL.md +251 -0
  799. package/skills/ux-audit/references/finding.schema.json +62 -0
  800. package/skills/ux-audit/references/route-list.yaml +95 -0
  801. package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
  802. package/skills/ux-audit/scripts/bot-signin.ts +135 -0
  803. package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
  804. package/skills/verification-scaffold/SKILL.md +73 -0
  805. package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
  806. package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
  807. package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
  808. package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
  809. package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
  810. package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
  811. package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
  812. package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
  813. package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
  814. package/skills/verification-scaffold/references/guards.tsv +21 -0
  815. package/skills/verification-scaffold/references/verification-readme.template +55 -0
  816. package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
  817. package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
  818. package/skills/verification-scaffold/test/generator.test.sh +319 -0
  819. package/skills/verification-scaffold/test/manifest.test.sh +144 -0
  820. package/skills/work/SKILL.md +1480 -0
  821. package/skills/work/references/end-of-work-resume.md +7 -0
  822. package/skills/work/references/hook-test-main-cwd.md +24 -0
  823. package/skills/work/references/key-principles.md +83 -0
  824. package/skills/work/references/property-testing.md +122 -0
  825. package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
  826. package/skills/work/references/work-agent-teams.md +97 -0
  827. package/skills/work/references/work-lifecycle-parallel.md +147 -0
  828. package/skills/work/references/work-lockfile-bumps.md +97 -0
  829. package/skills/work/references/work-scratch-sandboxes.md +212 -0
  830. package/skills/work/references/work-subagent-fanout.md +61 -0
  831. package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
  832. 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&amp 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 ]]