@soleur/opencode 0.0.0-stage → 3.354.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/agents.manifest.json +476 -0
- package/.claude-plugin/plugin.json +42 -0
- package/README.md +44 -2
- package/agents/community/.gitkeep +0 -0
- package/agents/engineering/cto.md +87 -0
- package/agents/engineering/design/ddd-architect.md +95 -0
- package/agents/engineering/discovery/agent-finder.md +214 -0
- package/agents/engineering/discovery/functional-discovery.md +182 -0
- package/agents/engineering/infra/infra-security.md +151 -0
- package/agents/engineering/infra/platform-strategist.md +109 -0
- package/agents/engineering/infra/terraform-architect.md +171 -0
- package/agents/engineering/prompt-engineer.md +37 -0
- package/agents/engineering/research/best-practices-researcher.md +115 -0
- package/agents/engineering/research/framework-docs-researcher.md +93 -0
- package/agents/engineering/research/git-history-analyzer.md +53 -0
- package/agents/engineering/research/learnings-researcher.md +285 -0
- package/agents/engineering/research/repo-research-analyst.md +118 -0
- package/agents/engineering/review/agent-native-reviewer.md +279 -0
- package/agents/engineering/review/architecture-strategist.md +61 -0
- package/agents/engineering/review/code-quality-analyst.md +92 -0
- package/agents/engineering/review/code-simplicity-reviewer.md +109 -0
- package/agents/engineering/review/data-integrity-guardian.md +82 -0
- package/agents/engineering/review/data-migration-expert.md +98 -0
- package/agents/engineering/review/deployment-verification-agent.md +183 -0
- package/agents/engineering/review/dhh-rails-reviewer.md +46 -0
- package/agents/engineering/review/kieran-rails-reviewer.md +87 -0
- package/agents/engineering/review/legacy-code-expert.md +55 -0
- package/agents/engineering/review/observability-coverage-reviewer.md +147 -0
- package/agents/engineering/review/pattern-recognition-specialist.md +59 -0
- package/agents/engineering/review/performance-oracle.md +130 -0
- package/agents/engineering/review/security-sentinel.md +167 -0
- package/agents/engineering/review/semgrep-sast.md +36 -0
- package/agents/engineering/review/test-design-reviewer.md +150 -0
- package/agents/engineering/review/user-impact-reviewer.md +70 -0
- package/agents/engineering/workflow/pr-comment-resolver.md +69 -0
- package/agents/finance/budget-analyst.md +29 -0
- package/agents/finance/cfo.md +44 -0
- package/agents/finance/financial-reporter.md +30 -0
- package/agents/finance/revenue-analyst.md +30 -0
- package/agents/legal/clo.md +115 -0
- package/agents/legal/legal-compliance-auditor.md +110 -0
- package/agents/legal/legal-document-generator.md +134 -0
- package/agents/marketing/analytics-analyst.md +25 -0
- package/agents/marketing/brand-architect.md +181 -0
- package/agents/marketing/cmo.md +76 -0
- package/agents/marketing/conversion-optimizer.md +19 -0
- package/agents/marketing/copywriter.md +20 -0
- package/agents/marketing/fact-checker.md +70 -0
- package/agents/marketing/growth-strategist.md +145 -0
- package/agents/marketing/paid-media-strategist.md +28 -0
- package/agents/marketing/pricing-strategist.md +17 -0
- package/agents/marketing/programmatic-seo-specialist.md +25 -0
- package/agents/marketing/retention-strategist.md +23 -0
- package/agents/marketing/seo-aeo-analyst.md +164 -0
- package/agents/operations/coo.md +50 -0
- package/agents/operations/ops-advisor.md +78 -0
- package/agents/operations/ops-provisioner.md +101 -0
- package/agents/operations/ops-research.md +52 -0
- package/agents/operations/service-automator.md +278 -0
- package/agents/product/business-validator.md +206 -0
- package/agents/product/competitive-intelligence.md +89 -0
- package/agents/product/cpo.md +63 -0
- package/agents/product/design/ux-design-lead.md +209 -0
- package/agents/product/spec-flow-analyzer.md +122 -0
- package/agents/sales/cro.md +44 -0
- package/agents/sales/deal-architect.md +29 -0
- package/agents/sales/outbound-strategist.md +29 -0
- package/agents/sales/pipeline-analyst.md +29 -0
- package/agents/support/cco.md +48 -0
- package/agents/support/community-manager.md +570 -0
- package/agents/support/ticket-triage.md +70 -0
- package/bin/soleur-opencode.js +29 -0
- package/commands/go.md +660 -0
- package/commands/help.md +273 -0
- package/commands/sync.md +977 -0
- package/commands/version.md +103 -0
- package/hooks/antigravity-hooks.json +84 -0
- package/hooks/antigravity-session-start.sh +15 -0
- package/hooks/bare-root-cwd-guard.sh +131 -0
- package/hooks/browser-snapshot-credential-guard.sh +200 -0
- package/hooks/codex-session-start.sh +9 -0
- package/hooks/compaction-state.sh +525 -0
- package/hooks/destructive-command-guard.sh +1387 -0
- package/hooks/devin-session-start.sh +90 -0
- package/hooks/ext-filter-detect.sh +171 -0
- package/hooks/hooks.json +133 -0
- package/hooks/lib/hook-tool-kind.sh +24 -0
- package/hooks/lib/log-rotation.sh +176 -0
- package/hooks/lib/shell-argv.pl +576 -0
- package/hooks/memory-backstop.sh +1114 -0
- package/hooks/operator-stage-approval.sh +391 -0
- package/hooks/stop-hook.sh +367 -0
- package/hooks/unkept-promise-hook.sh +383 -0
- package/hooks/version-banner.sh +127 -0
- package/hooks/welcome-hook.sh +50 -0
- package/install.js +29 -0
- package/lib/agent-registry.ts +154 -0
- package/lib/c4-canonical-cli.mjs +34 -0
- package/lib/c4-canonical.mjs +46 -0
- package/lib/c4-from-components.ts +554 -0
- package/lib/go-routing.ts +81 -0
- package/lib/grok-inspect-contract.ts +311 -0
- package/lib/harness-model-map.ts +101 -0
- package/lib/harness-parity.ts +726 -0
- package/lib/harness-tool-map.ts +180 -0
- package/lib/harness.ts +716 -0
- package/lib/heartbeat-live-reconcile.ts +1153 -0
- package/lib/heartbeat-manifest.ts +361 -0
- package/lib/kb-coverage.ts +252 -0
- package/lib/pr-merge-poll.ts +103 -0
- package/lib/workflow-fidelity.ts +388 -0
- package/opencode/INSTRUCTIONS.md +171 -0
- package/opencode/agents/engineering/cto.md +10 -0
- package/opencode/agents/engineering/design/ddd-architect.md +10 -0
- package/opencode/agents/engineering/discovery/agent-finder.md +10 -0
- package/opencode/agents/engineering/discovery/functional-discovery.md +10 -0
- package/opencode/agents/engineering/infra/infra-security.md +10 -0
- package/opencode/agents/engineering/infra/platform-strategist.md +10 -0
- package/opencode/agents/engineering/infra/terraform-architect.md +10 -0
- package/opencode/agents/engineering/prompt-engineer.md +10 -0
- package/opencode/agents/engineering/research/best-practices-researcher.md +10 -0
- package/opencode/agents/engineering/research/framework-docs-researcher.md +10 -0
- package/opencode/agents/engineering/research/git-history-analyzer.md +10 -0
- package/opencode/agents/engineering/research/learnings-researcher.md +10 -0
- package/opencode/agents/engineering/research/repo-research-analyst.md +10 -0
- package/opencode/agents/engineering/review/agent-native-reviewer.md +10 -0
- package/opencode/agents/engineering/review/architecture-strategist.md +10 -0
- package/opencode/agents/engineering/review/code-quality-analyst.md +10 -0
- package/opencode/agents/engineering/review/code-simplicity-reviewer.md +10 -0
- package/opencode/agents/engineering/review/data-integrity-guardian.md +10 -0
- package/opencode/agents/engineering/review/data-migration-expert.md +10 -0
- package/opencode/agents/engineering/review/deployment-verification-agent.md +10 -0
- package/opencode/agents/engineering/review/dhh-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/kieran-rails-reviewer.md +10 -0
- package/opencode/agents/engineering/review/legacy-code-expert.md +10 -0
- package/opencode/agents/engineering/review/observability-coverage-reviewer.md +10 -0
- package/opencode/agents/engineering/review/pattern-recognition-specialist.md +10 -0
- package/opencode/agents/engineering/review/performance-oracle.md +10 -0
- package/opencode/agents/engineering/review/security-sentinel.md +10 -0
- package/opencode/agents/engineering/review/semgrep-sast.md +10 -0
- package/opencode/agents/engineering/review/test-design-reviewer.md +10 -0
- package/opencode/agents/engineering/review/user-impact-reviewer.md +10 -0
- package/opencode/agents/engineering/workflow/pr-comment-resolver.md +10 -0
- package/opencode/agents/finance/budget-analyst.md +10 -0
- package/opencode/agents/finance/cfo.md +10 -0
- package/opencode/agents/finance/financial-reporter.md +10 -0
- package/opencode/agents/finance/revenue-analyst.md +10 -0
- package/opencode/agents/legal/clo.md +10 -0
- package/opencode/agents/legal/legal-compliance-auditor.md +10 -0
- package/opencode/agents/legal/legal-document-generator.md +10 -0
- package/opencode/agents/marketing/analytics-analyst.md +10 -0
- package/opencode/agents/marketing/brand-architect.md +10 -0
- package/opencode/agents/marketing/cmo.md +10 -0
- package/opencode/agents/marketing/conversion-optimizer.md +10 -0
- package/opencode/agents/marketing/copywriter.md +10 -0
- package/opencode/agents/marketing/fact-checker.md +10 -0
- package/opencode/agents/marketing/growth-strategist.md +10 -0
- package/opencode/agents/marketing/paid-media-strategist.md +10 -0
- package/opencode/agents/marketing/pricing-strategist.md +10 -0
- package/opencode/agents/marketing/programmatic-seo-specialist.md +10 -0
- package/opencode/agents/marketing/retention-strategist.md +10 -0
- package/opencode/agents/marketing/seo-aeo-analyst.md +10 -0
- package/opencode/agents/operations/coo.md +10 -0
- package/opencode/agents/operations/ops-advisor.md +10 -0
- package/opencode/agents/operations/ops-provisioner.md +10 -0
- package/opencode/agents/operations/ops-research.md +10 -0
- package/opencode/agents/operations/service-automator.md +10 -0
- package/opencode/agents/product/business-validator.md +10 -0
- package/opencode/agents/product/competitive-intelligence.md +10 -0
- package/opencode/agents/product/cpo.md +10 -0
- package/opencode/agents/product/design/ux-design-lead.md +10 -0
- package/opencode/agents/product/spec-flow-analyzer.md +10 -0
- package/opencode/agents/sales/cro.md +10 -0
- package/opencode/agents/sales/deal-architect.md +10 -0
- package/opencode/agents/sales/outbound-strategist.md +10 -0
- package/opencode/agents/sales/pipeline-analyst.md +10 -0
- package/opencode/agents/support/cco.md +10 -0
- package/opencode/agents/support/community-manager.md +10 -0
- package/opencode/agents/support/ticket-triage.md +10 -0
- package/opencode/commands/go.md +10 -0
- package/opencode/commands/help.md +10 -0
- package/opencode/commands/sync.md +10 -0
- package/opencode/commands/version.md +11 -0
- package/opencode/opencode.json +8 -0
- package/opencode/plugin.ts +818 -0
- package/package.json +36 -4
- package/scripts/admin-merge-ready.sh +545 -0
- package/scripts/admin-merge-ready.test.sh +753 -0
- package/scripts/affected-tests.sh +908 -0
- package/scripts/affected-tests.test.sh +1797 -0
- package/scripts/alpha-metrics.sh +78 -0
- package/scripts/alpha-metrics.test.sh +115 -0
- package/scripts/ancestor-signal-helpers.baseline.txt +16 -0
- package/scripts/audit-flag-flip.sh +107 -0
- package/scripts/check-red-on-main.sh +316 -0
- package/scripts/ci-head-verdict.sh +411 -0
- package/scripts/cloud-detect.sh +193 -0
- package/scripts/delegation-verify.sh +657 -0
- package/scripts/delegation-verify.test.sh +1424 -0
- package/scripts/deploy-arm.sh +475 -0
- package/scripts/domain-model-drift.sh +419 -0
- package/scripts/emit-decision.sh +144 -0
- package/scripts/emit-decision.test.sh +144 -0
- package/scripts/ext-filter-divergence-corpus.txt +15 -0
- package/scripts/ext-filter-divergence.sh +189 -0
- package/scripts/generate-c4-from-components.ts +442 -0
- package/scripts/grok-fidelity-gate.sh +68 -0
- package/scripts/grok-pre-push-gate.sh +232 -0
- package/scripts/harness-discovery-smoke.ts +906 -0
- package/scripts/harness-parity-census.ts +92 -0
- package/scripts/inngest-luks-alert-armed-probe.ts +29 -0
- package/scripts/install-opencode-adapter.ts +509 -0
- package/scripts/lib/affected-tests/common.sh +365 -0
- package/scripts/lib/affected-tests/go.sh +157 -0
- package/scripts/lib/affected-tests/js.sh +188 -0
- package/scripts/lib/affected-tests/nx-turbo.sh +98 -0
- package/scripts/lib/affected-tests/python.sh +185 -0
- package/scripts/lib/context-sanitize.sh +49 -0
- package/scripts/lib/domain-model-lib.sh +180 -0
- package/scripts/lib/ext-filter-grammar.tsv +34 -0
- package/scripts/lib/operator-script.sh +1236 -0
- package/scripts/lib/proc.sh +580 -0
- package/scripts/lib/session-state.sh +748 -0
- package/scripts/lib/tmp-classify.sh +843 -0
- package/scripts/lib/wrapper-boundary.sh +47 -0
- package/scripts/lint-ratchet.py +1547 -0
- package/scripts/monitor-pr-checks.sh +441 -0
- package/scripts/pipeline-tally.sh +756 -0
- package/scripts/pipeline-tally.test.sh +740 -0
- package/scripts/plugin-version.sh +528 -0
- package/scripts/precommit-guard.sh +115 -0
- package/scripts/precommit-guard.test.sh +254 -0
- package/scripts/reconcile-live-heartbeats.ts +705 -0
- package/scripts/render-c4-model.sh +217 -0
- package/scripts/resolve-git-root.sh +55 -0
- package/scripts/resolve-regenerable-conflicts.sh +363 -0
- package/scripts/resolve-regenerable-conflicts.test.sh +780 -0
- package/scripts/ruff-requirements.txt +22 -0
- package/scripts/run-in-pid-namespace.sh +98 -0
- package/scripts/run-in-pid-namespace.test.sh +290 -0
- package/scripts/scan-ancestor-signal-helpers.py +556 -0
- package/scripts/scan-ancestor-signal-helpers.test.sh +670 -0
- package/scripts/setup-ralph-loop.sh +238 -0
- package/scripts/sync-cursor-name-map.ts +316 -0
- package/scripts/sync-grok-agent-compat.ts +188 -0
- package/scripts/sync-npm-package.ts +165 -0
- package/scripts/sync-opencode-adapter.ts +498 -0
- package/scripts/sync-pr-behind.sh +647 -0
- package/scripts/sync-pr-behind.test.sh +252 -0
- package/scripts/taste-profile-update.sh +180 -0
- package/scripts/taste-profile-update.test.sh +167 -0
- package/scripts/write-budget-marker.sh +23 -0
- package/scripts/write-kb-coverage.ts +113 -0
- package/skills/admin-ip-refresh/SKILL.md +81 -0
- package/skills/admin-ip-refresh/references/admin-ip-refresh-procedure.md +201 -0
- package/skills/agent-browser/SKILL.md +698 -0
- package/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh +304 -0
- package/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py +964 -0
- package/skills/agent-browser/scripts/redact-a11y-snapshot.py +443 -0
- package/skills/agent-browser/test/fixtures/capture-playwright-mcp-fixtures.py +217 -0
- package/skills/agent-browser/test/fixtures/fake-passthrough-proxy.py +24 -0
- package/skills/agent-browser/test/fixtures/fake-playwright-mcp.py +131 -0
- package/skills/agent-browser/test/fixtures/make-proxy-mutant.py +48 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/dialog-link-injection.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/empty-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-and-result.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-bool.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-code-string.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-message-tree.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-no-data.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-string.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/error-with-data.json +9 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/evaluate-escaped-object.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/invalid-utf8.json +4 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/link-result.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/list-two-results.json +6 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/meta-in-result.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/no-content.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/quoted-key.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/resource-block.json +18 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-and-method.json +11 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/result-string.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped-one-row.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/run-code-escaped.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/structured-content.json +13 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tools-list-no-tools.json +5 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/tree-no-credential.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/two-text-blocks.json +14 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/unparsable-line.json +3 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-brace.json +10 -0
- package/skills/agent-browser/test/fixtures/odd-shapes/value-contains-link-substring.json +10 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/close.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/evaluate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/find.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/framing.json +6 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/initialize.json +14 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate-link.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/navigate.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/roots-list-request.json +5 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/screenshot.json +17 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-filename.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-json.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot-meta-raw.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/snapshot.json +12 -0
- package/skills/agent-browser/test/fixtures/playwright-mcp-0.0.78/tools-list.json +827 -0
- package/skills/agent-browser/test/fixtures/proxy-session-driver.py +138 -0
- package/skills/agent-browser/test/fixtures/session-facts.py +102 -0
- package/skills/agent-browser/test/playwright-mcp-lifetime.test.sh +1620 -0
- package/skills/agent-browser/test/playwright-mcp-redact-proxy.test.sh +1599 -0
- package/skills/agent-browser/test/redact-a11y-snapshot.test.sh +527 -0
- package/skills/agent-native-architecture/SKILL.md +479 -0
- package/skills/agent-native-architecture/references/action-parity-discipline.md +430 -0
- package/skills/agent-native-architecture/references/agent-execution-patterns.md +483 -0
- package/skills/agent-native-architecture/references/agent-native-testing.md +605 -0
- package/skills/agent-native-architecture/references/architecture-patterns.md +493 -0
- package/skills/agent-native-architecture/references/dynamic-context-injection.md +358 -0
- package/skills/agent-native-architecture/references/files-universal-interface.md +314 -0
- package/skills/agent-native-architecture/references/from-primitives-to-domain-tools.md +381 -0
- package/skills/agent-native-architecture/references/mcp-tool-design.md +524 -0
- package/skills/agent-native-architecture/references/mobile-patterns.md +897 -0
- package/skills/agent-native-architecture/references/product-implications.md +478 -0
- package/skills/agent-native-architecture/references/refactoring-to-prompt-native.md +342 -0
- package/skills/agent-native-architecture/references/self-modification.md +294 -0
- package/skills/agent-native-architecture/references/shared-workspace-architecture.md +700 -0
- package/skills/agent-native-architecture/references/system-prompt-design.md +263 -0
- package/skills/agent-native-audit/SKILL.md +300 -0
- package/skills/agent-native-audit/workflows/agent-native-audit.workflow.js +423 -0
- package/skills/andrew-kane-gem-writer/SKILL.md +189 -0
- package/skills/andrew-kane-gem-writer/references/database-adapters.md +231 -0
- package/skills/andrew-kane-gem-writer/references/module-organization.md +121 -0
- package/skills/andrew-kane-gem-writer/references/rails-integration.md +183 -0
- package/skills/andrew-kane-gem-writer/references/resources.md +126 -0
- package/skills/andrew-kane-gem-writer/references/testing-patterns.md +261 -0
- package/skills/architecture/SKILL.md +436 -0
- package/skills/architecture/references/adr-template.md +154 -0
- package/skills/architecture/references/likec4-reference.md +136 -0
- package/skills/architecture/references/nfr-reference.md +126 -0
- package/skills/archive-kb/SKILL.md +86 -0
- package/skills/archive-kb/scripts/archive-kb.sh +300 -0
- package/skills/archive-kb/test/archive-kb-partial-run.test.sh +276 -0
- package/skills/atdd-developer/SKILL.md +65 -0
- package/skills/brainstorm/SKILL.md +701 -0
- package/skills/brainstorm/references/brainstorm-brand-workshop.md +153 -0
- package/skills/brainstorm/references/brainstorm-domain-config.md +63 -0
- package/skills/brainstorm/references/brainstorm-validation-workshop.md +65 -0
- package/skills/brainstorm/references/ui-surface-terms.md +42 -0
- package/skills/brainstorm-techniques/SKILL.md +231 -0
- package/skills/brainstorm-techniques/references/decision-principles.md +150 -0
- package/skills/brainstorm-techniques/references/phase-boundaries.md +103 -0
- package/skills/brainstorm-techniques/references/standing-delegation.md +407 -0
- package/skills/brainstorm-techniques/references/technical-ruling-delegation.md +174 -0
- package/skills/campaign-calendar/SKILL.md +145 -0
- package/skills/cf-token-scope/SKILL.md +164 -0
- package/skills/cf-token-scope/references/widen-playbook.md +114 -0
- package/skills/cf-token-scope/scripts/cf-token-scope.sh +253 -0
- package/skills/changelog/SKILL.md +142 -0
- package/skills/code-to-prd/SKILL.md +66 -0
- package/skills/code-to-prd/references/banner-template.md +12 -0
- package/skills/code-to-prd/references/prd-template.md +41 -0
- package/skills/code-to-prd/scripts/code-to-prd.sh +634 -0
- package/skills/code-to-prd/test/code-to-prd.test.sh +447 -0
- package/skills/code-to-prd/test/fixture/.env.example +7 -0
- package/skills/code-to-prd/test/fixture/app/about/page.tsx +11 -0
- package/skills/code-to-prd/test/fixture/app/api/health/route.ts +10 -0
- package/skills/code-to-prd/test/fixture/app/page.tsx +14 -0
- package/skills/code-to-prd/test/fixture/next.config.mjs +3 -0
- package/skills/code-to-prd/test/fixture/package.json +6 -0
- package/skills/cohort-status/SKILL.md +62 -0
- package/skills/community/SKILL.md +210 -0
- package/skills/community/scripts/bsky-community.sh +644 -0
- package/skills/community/scripts/bsky-setup.sh +631 -0
- package/skills/community/scripts/community-router.sh +93 -0
- package/skills/community/scripts/discord-community.sh +487 -0
- package/skills/community/scripts/discord-setup.sh +784 -0
- package/skills/community/scripts/github-community.sh +706 -0
- package/skills/community/scripts/hn-community.sh +233 -0
- package/skills/community/scripts/lib/hmac-sha1-b64.sh +126 -0
- package/skills/community/scripts/linkedin-community.sh +750 -0
- package/skills/community/scripts/linkedin-setup.sh +758 -0
- package/skills/community/scripts/x-community.sh +890 -0
- package/skills/community/scripts/x-setup.sh +713 -0
- package/skills/community/test/community-argv.test.sh +2922 -0
- package/skills/community/test/github-community.test.sh +981 -0
- package/skills/competitive-analysis/SKILL.md +69 -0
- package/skills/competitive-analysis/references/peer-plugin-audit.md +218 -0
- package/skills/compound/SKILL.md +673 -0
- package/skills/compound/scripts/token-efficiency-report.sh +334 -0
- package/skills/compound/test/phase-16.test.sh +379 -0
- package/skills/compound-capture/SKILL.md +791 -0
- package/skills/compound-capture/assets/critical-pattern-template.md +37 -0
- package/skills/compound-capture/assets/resolution-template.md +116 -0
- package/skills/compound-capture/references/yaml-schema.md +67 -0
- package/skills/compound-capture/schema.yaml +181 -0
- package/skills/constraint-scaffold/SKILL.md +175 -0
- package/skills/constraint-scaffold/references/boundary-readme.template +35 -0
- package/skills/constraint-scaffold/references/constraint-gates-workflow.template +69 -0
- package/skills/constraint-scaffold/references/depcruise-config.template +230 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-a.template +240 -0
- package/skills/constraint-scaffold/references/fix-constraints-stage-b.template +313 -0
- package/skills/constraint-scaffold/references/shared-runner.template +135 -0
- package/skills/constraint-scaffold/scripts/constraint-scaffold.sh +589 -0
- package/skills/constraint-scaffold/test/bite-proof.test.sh +1338 -0
- package/skills/constraint-scaffold/test/boundary.test.sh +734 -0
- package/skills/constraint-scaffold/test/emit-fix-constraints.test.sh +193 -0
- package/skills/constraint-scaffold/test/generator.test.sh +93 -0
- package/skills/constraint-scaffold/test/parity.test.sh +285 -0
- package/skills/content-writer/SKILL.md +242 -0
- package/skills/content-writer/scripts/blog-jargon-scan.sh +42 -0
- package/skills/cron-delete/SKILL.md +56 -0
- package/skills/cron-list/SKILL.md +54 -0
- package/skills/deepen-plan/SKILL.md +937 -0
- package/skills/deepen-plan/workflows/deepen-plan.workflow.js +488 -0
- package/skills/deploy/SKILL.md +115 -0
- package/skills/deploy/references/hetzner-setup.md +67 -0
- package/skills/deploy/scripts/deploy.sh +37 -0
- package/skills/deploy-docs/SKILL.md +95 -0
- package/skills/dhh-rails-style/SKILL.md +203 -0
- package/skills/dhh-rails-style/references/architecture.md +717 -0
- package/skills/dhh-rails-style/references/controllers.md +323 -0
- package/skills/dhh-rails-style/references/frontend.md +556 -0
- package/skills/dhh-rails-style/references/gems.md +316 -0
- package/skills/dhh-rails-style/references/models.md +388 -0
- package/skills/dhh-rails-style/references/testing.md +354 -0
- package/skills/discord-content/SKILL.md +126 -0
- package/skills/docs-site/SKILL.md +214 -0
- package/skills/drain-labeled-backlog/SKILL.md +203 -0
- package/skills/drain-labeled-backlog/scripts/group-by-area.sh +208 -0
- package/skills/drain-labeled-backlog/workflows/drain-labeled-backlog.workflow.js +433 -0
- package/skills/drain-prs/SKILL.md +143 -0
- package/skills/drain-prs/scripts/triage-prs.sh +192 -0
- package/skills/drain-prs/test/argv-ceiling.test.sh +183 -0
- package/skills/dspy-ruby/SKILL.md +634 -0
- package/skills/dspy-ruby/assets/config-template.rb +359 -0
- package/skills/dspy-ruby/assets/module-template.rb +326 -0
- package/skills/dspy-ruby/assets/signature-template.rb +143 -0
- package/skills/dspy-ruby/references/core-concepts.md +268 -0
- package/skills/dspy-ruby/references/optimization.md +623 -0
- package/skills/dspy-ruby/references/providers.md +335 -0
- package/skills/eval-harness/README.md +194 -0
- package/skills/eval-harness/SKILL.md +157 -0
- package/skills/eval-harness/enums/go-routes.json +13 -0
- package/skills/eval-harness/enums/incident-threshold.json +5 -0
- package/skills/eval-harness/enums/lane.json +5 -0
- package/skills/eval-harness/enums/tool-selection.json +20 -0
- package/skills/eval-harness/enums/triage-levels.json +5 -0
- package/skills/eval-harness/gated-skills.json +34 -0
- package/skills/eval-harness/models.generated.json +5 -0
- package/skills/eval-harness/promptfooconfig-go-routing.yaml +35 -0
- package/skills/eval-harness/promptfooconfig-incident-threshold.yaml +32 -0
- package/skills/eval-harness/promptfooconfig-lane-inference.yaml +33 -0
- package/skills/eval-harness/promptfooconfig-ticket-triage.yaml +34 -0
- package/skills/eval-harness/promptfooconfig-tool-selection.yaml +33 -0
- package/skills/eval-harness/prompts/go-baseline.txt +6 -0
- package/skills/eval-harness/prompts/go-skill.txt +21 -0
- package/skills/eval-harness/prompts/incident-threshold-baseline.txt +6 -0
- package/skills/eval-harness/prompts/incident-threshold-skill.txt +12 -0
- package/skills/eval-harness/prompts/lane-inference-baseline.txt +6 -0
- package/skills/eval-harness/prompts/lane-inference-skill.txt +15 -0
- package/skills/eval-harness/prompts/tool-selection-baseline.txt +19 -0
- package/skills/eval-harness/prompts/tool-selection-skill.txt +22 -0
- package/skills/eval-harness/prompts/triage-baseline.txt +7 -0
- package/skills/eval-harness/prompts/triage-skill.txt +10 -0
- package/skills/eval-harness/scripts/eval-gate.cjs +318 -0
- package/skills/eval-harness/scripts/extract-block.cjs +87 -0
- package/skills/eval-harness/scripts/gate-classification.cjs +28 -0
- package/skills/eval-harness/scripts/gen-models.sh +41 -0
- package/skills/eval-harness/scripts/gen-skill-prompt.cjs +177 -0
- package/skills/eval-harness/scripts/measure-classification.cjs +30 -0
- package/skills/eval-harness/scripts/parse-label.cjs +93 -0
- package/skills/eval-harness/scripts/verdict.cjs +132 -0
- package/skills/eval-harness/tasks/go-routing.jsonl +8 -0
- package/skills/eval-harness/tasks/incident-threshold.jsonl +7 -0
- package/skills/eval-harness/tasks/lane-inference.jsonl +7 -0
- package/skills/eval-harness/tasks/ticket-triage.jsonl +6 -0
- package/skills/eval-harness/tasks/tool-selection.jsonl +15 -0
- package/skills/eval-harness/test/eval-gate.test.sh +132 -0
- package/skills/eval-harness/test/extract-block.test.sh +87 -0
- package/skills/eval-harness/test/gate-classification.test.sh +77 -0
- package/skills/eval-harness/test/gen-models.test.sh +75 -0
- package/skills/eval-harness/test/measure-classification.test.sh +74 -0
- package/skills/eval-harness/test/parse-label.test.sh +129 -0
- package/skills/eval-harness/test/registry-completeness.test.sh +109 -0
- package/skills/eval-harness/test/verdict.test.sh +123 -0
- package/skills/every-style-editor/SKILL.md +142 -0
- package/skills/every-style-editor/references/EVERY_WRITE_STYLE.md +529 -0
- package/skills/feature-tweet/SKILL.md +199 -0
- package/skills/feature-video/SKILL.md +412 -0
- package/skills/feature-video/scripts/check_deps.sh +281 -0
- package/skills/file-todos/SKILL.md +267 -0
- package/skills/file-todos/assets/todo-template.md +166 -0
- package/skills/fix-issue/SKILL.md +278 -0
- package/skills/fix-issue/references/agent-authored-exclusion.md +120 -0
- package/skills/fix-issue/references/exclude-label-jq-snippet.md +97 -0
- package/skills/flag-bootstrap/SETUP.md +142 -0
- package/skills/flag-create/SKILL.md +145 -0
- package/skills/flag-create/scripts/create.sh +273 -0
- package/skills/flag-delete/SKILL.md +167 -0
- package/skills/flag-delete/scripts/delete.sh +288 -0
- package/skills/flag-list/SKILL.md +99 -0
- package/skills/flag-list/scripts/list.sh +275 -0
- package/skills/flag-set-role/SKILL.md +229 -0
- package/skills/flag-set-role/scripts/flip.sh +774 -0
- package/skills/frontend-anti-slop/SKILL.md +106 -0
- package/skills/frontend-anti-slop/references/anti-patterns.md +199 -0
- package/skills/frontend-anti-slop/references/slop-rules.md +65 -0
- package/skills/frontend-anti-slop/scripts/tier1-scan.ts +525 -0
- package/skills/frontend-design/SKILL.md +77 -0
- package/skills/gdpr-gate/NOTICE +136 -0
- package/skills/gdpr-gate/SKILL.md +341 -0
- package/skills/gdpr-gate/references/fields.md +160 -0
- package/skills/gdpr-gate/references/layers/api-layer.md +355 -0
- package/skills/gdpr-gate/references/layers/auth-sessions.md +290 -0
- package/skills/gdpr-gate/references/layers/data-in-transit.md +331 -0
- package/skills/gdpr-gate/references/layers/data-lifecycle.md +341 -0
- package/skills/gdpr-gate/references/layers/frontend.md +249 -0
- package/skills/gdpr-gate/references/layers/testing-seeding.md +256 -0
- package/skills/gdpr-gate/references/leakage-vectors.md +248 -0
- package/skills/gdpr-gate/references/legacy/legal-consent-v1-prose.md +48 -0
- package/skills/gdpr-gate/references/legal-consent.md +140 -0
- package/skills/gdpr-gate/references/non-negotiables.md +60 -0
- package/skills/gdpr-gate/scripts/gdpr-gate.sh +271 -0
- package/skills/gdpr-gate/scripts/notice-frontmatter.sh +337 -0
- package/skills/gdpr-gate/scripts/path-denylist.txt +39 -0
- package/skills/gdpr-gate/scripts/repo-scan.sh +158 -0
- package/skills/gdpr-gate/scripts/vendor-drift-classify.sh +120 -0
- package/skills/gdpr-gate/scripts/vendor-pin-integrity.sh +237 -0
- package/skills/gemini-imagegen/SKILL.md +283 -0
- package/skills/gemini-imagegen/requirements.txt +5 -0
- package/skills/gemini-imagegen/scripts/_error_handling.py +146 -0
- package/skills/gemini-imagegen/scripts/compose_images.py +150 -0
- package/skills/gemini-imagegen/scripts/edit_image.py +137 -0
- package/skills/gemini-imagegen/scripts/gemini_images.py +256 -0
- package/skills/gemini-imagegen/scripts/generate_image.py +147 -0
- package/skills/gemini-imagegen/scripts/multi_turn_chat.py +223 -0
- package/skills/gemini-imagegen/scripts/test_error_handling.py +179 -0
- package/skills/git-worktree/SKILL.md +534 -0
- package/skills/git-worktree/scripts/git-repo-readiness-diag.sh +98 -0
- package/skills/git-worktree/scripts/worktree-manager.sh +4773 -0
- package/skills/git-worktree/test/create-from-origin-main.test.sh +116 -0
- package/skills/git-worktree/test/lease-protects-active.test.sh +1056 -0
- package/skills/git-worktree/test/no-repo-fail-loud.test.sh +88 -0
- package/skills/git-worktree/test/orphan-reaper-honest-count.test.sh +379 -0
- package/skills/git-worktree/test/reap-archive-persistence.test.sh +725 -0
- package/skills/git-worktree/test/stale-lock-sweep.test.sh +176 -0
- package/skills/go/SKILL.md +54 -0
- package/skills/growth/SKILL.md +164 -0
- package/skills/harvest-debt/SKILL.md +78 -0
- package/skills/harvest-debt/scripts/harvest-debt.sh +114 -0
- package/skills/heal-skill/SKILL.md +173 -0
- package/skills/help/SKILL.md +46 -0
- package/skills/incident/NOTICE +39 -0
- package/skills/incident/SKILL.md +395 -0
- package/skills/incident/scripts/dry-run.sh +448 -0
- package/skills/incident/scripts/redact-engine.py +412 -0
- package/skills/incident/scripts/redact-sentinel.sh +44 -0
- package/skills/incident/templates/pir.md +139 -0
- package/skills/incident/test/fixtures/dry-run-incident.json +26 -0
- package/skills/incident/test/fixtures/dry-run-secret-leak.json +26 -0
- package/skills/incident/test/fixtures/legacy-bash-scanner.sh +82 -0
- package/skills/incident/test/fixtures/positive-corpus.md +87 -0
- package/skills/incident/test/redact-sentinel.test.sh +1420 -0
- package/skills/invoice/SKILL.md +206 -0
- package/skills/kb-glossary/SKILL.md +71 -0
- package/skills/kb-glossary/references/glossary-format.md +111 -0
- package/skills/kb-glossary/references/rejected-request-register.md +115 -0
- package/skills/kb-search/SKILL.md +276 -0
- package/skills/kb-search/scripts/kb-search-cache.sh +123 -0
- package/skills/legal-audit/SKILL.md +161 -0
- package/skills/legal-generate/NOTICE +242 -0
- package/skills/legal-generate/SKILL.md +244 -0
- package/skills/legal-generate/references/templates/advisor-agreement/template.md +12 -0
- package/skills/legal-generate/references/templates/business-associate-agreement/template.md +112 -0
- package/skills/legal-generate/references/templates/cookie-notice/template.md +82 -0
- package/skills/legal-generate/references/templates/dpa-global/template.md +439 -0
- package/skills/legal-generate/references/templates/dpa-us/template.md +222 -0
- package/skills/legal-generate/references/templates/employee-offer-letter/template.md +75 -0
- package/skills/legal-generate/references/templates/master-services-agreement/template.md +252 -0
- package/skills/legal-generate/references/templates/mutual-nda/template.md +66 -0
- package/skills/legal-generate/references/templates/one-way-nda/template.md +38 -0
- package/skills/legal-generate/references/templates/privacy-policy-gdpr/template.md +427 -0
- package/skills/legal-generate/references/templates/privacy-policy-us/template.md +313 -0
- package/skills/legal-generate/references/templates/terms-of-use/template.md +75 -0
- package/skills/legal-generate/scripts/strip-vendor-credit.sh +96 -0
- package/skills/linear-fetch/SKILL.md +247 -0
- package/skills/linear-fetch/scripts/assert-no-linear-telemetry.sh +66 -0
- package/skills/linear-fetch/scripts/redact-linear-urls.sh +51 -0
- package/skills/linear-fetch/scripts/render-caller-template.sh +36 -0
- package/skills/linear-fetch/test/assert-no-linear-telemetry.test.sh +113 -0
- package/skills/linear-fetch/test/parity.test.sh +112 -0
- package/skills/linear-fetch/test/persist-safe-integration.test.sh +191 -0
- package/skills/linear-fetch/test/redact-linear-urls.test.sh +226 -0
- package/skills/merge-pr/SKILL.md +670 -0
- package/skills/model-launch-review/SKILL.md +255 -0
- package/skills/model-launch-review/scripts/audit-models.sh +501 -0
- package/skills/one-shot/SKILL.md +316 -0
- package/skills/one-shot/references/token-discipline.md +74 -0
- package/skills/operator-bootstrap/SKILL.md +412 -0
- package/skills/operator-bootstrap/template.sh +439 -0
- package/skills/operator-digest/SKILL.md +287 -0
- package/skills/operator-digest/assets/operator-digest.workflow.yml +175 -0
- package/skills/operator-digest/scripts/digest-scrub.sh +119 -0
- package/skills/operator-digest/scripts/provision-operator-digest-repo.sh +147 -0
- package/skills/operator-rephrase/SKILL.md +98 -0
- package/skills/pencil-setup/SKILL.md +201 -0
- package/skills/pencil-setup/scripts/check_deps.sh +556 -0
- package/skills/pencil-setup/scripts/copy_adapter.sh +80 -0
- package/skills/pencil-setup/scripts/package-lock.json +1172 -0
- package/skills/pencil-setup/scripts/package.json +14 -0
- package/skills/pencil-setup/scripts/pencil-error-enrichment.mjs +21 -0
- package/skills/pencil-setup/scripts/pencil-mcp-adapter.mjs +733 -0
- package/skills/pencil-setup/scripts/pencil-response-classify.mjs +38 -0
- package/skills/pencil-setup/scripts/pencil-save-gate.mjs +26 -0
- package/skills/pencil-setup/scripts/sanitize-filename.mjs +25 -0
- package/skills/plan/SKILL.md +1140 -0
- package/skills/plan/references/differential-migration-testing.md +185 -0
- package/skills/plan/references/plan-community-discovery.md +42 -0
- package/skills/plan/references/plan-founder-check.md +137 -0
- package/skills/plan/references/plan-functional-overlap.md +25 -0
- package/skills/plan/references/plan-host-fanout.md +51 -0
- package/skills/plan/references/plan-issue-templates.md +779 -0
- package/skills/plan/references/plan-network-outage-checklist.md +131 -0
- package/skills/plan/references/plan-scope-check.md +126 -0
- package/skills/plan/references/plan-sharp-edges.md +331 -0
- package/skills/plan/references/plan-signoff-lifecycle.md +12 -0
- package/skills/plan/references/webapp-cwv-observability.md +143 -0
- package/skills/plan-review/SKILL.md +91 -0
- package/skills/plan-review/lib/named-panel.mjs +58 -0
- package/skills/plan-review/workflows/plan-review.workflow.js +477 -0
- package/skills/postmerge/SKILL.md +642 -0
- package/skills/postmerge/references/deploy-status-debugging.md +141 -0
- package/skills/postmerge/references/graceful-degradation.md +20 -0
- package/skills/postmerge/references/sentry-error-count-delta.md +137 -0
- package/skills/preflight/SKILL.md +1553 -0
- package/skills/preflight/references/check-13-founder-check.md +286 -0
- package/skills/preflight/scripts/founder-check.py +1640 -0
- package/skills/preflight/scripts/parse-form-a.awk +107 -0
- package/skills/preflight/scripts/probe-verb-gate.sh +108 -0
- package/skills/product-roadmap/SKILL.md +357 -0
- package/skills/product-roadmap/scripts/roadmap-reconcile.sh +327 -0
- package/skills/provision-cloudflare/SKILL.md +67 -0
- package/skills/provision-cloudflare/scripts/provision-cloudflare.sh +271 -0
- package/skills/provision-doppler/SKILL.md +58 -0
- package/skills/provision-doppler/scripts/provision-doppler.sh +358 -0
- package/skills/provision-github/SKILL.md +59 -0
- package/skills/provision-github/scripts/provision-github.sh +300 -0
- package/skills/provision-hetzner/SKILL.md +76 -0
- package/skills/provision-hetzner/scripts/provision-hetzner.sh +262 -0
- package/skills/provision-hetzner/test/fixture/knowledge-base/legal/tenant-dpa-register.md +26 -0
- package/skills/provision-hetzner/test/fixture/provision-hetzner-dry-run.golden +34 -0
- package/skills/provision-hetzner/test/provision-hetzner-characterization.test.sh +227 -0
- package/skills/qa/SKILL.md +263 -0
- package/skills/qa/references/browser-preflight.md +22 -0
- package/skills/questionnaire-generate/SKILL.md +328 -0
- package/skills/questionnaire-generate/references/questionnaire.template +60 -0
- package/skills/rclone/SKILL.md +162 -0
- package/skills/rclone/scripts/check_setup.sh +63 -0
- package/skills/release-announce/SKILL.md +65 -0
- package/skills/release-docs/SKILL.md +92 -0
- package/skills/reproduce-bug/SKILL.md +358 -0
- package/skills/resolve-debt/SKILL.md +85 -0
- package/skills/resolve-debt/scripts/resolve-debt.py +584 -0
- package/skills/resolve-parallel/SKILL.md +45 -0
- package/skills/resolve-parallel/workflows/resolve-parallel.workflow.js +415 -0
- package/skills/resolve-pr-parallel/SKILL.md +68 -0
- package/skills/resolve-pr-parallel/workflows/resolve-pr-parallel.workflow.js +419 -0
- package/skills/resolve-todo-parallel/SKILL.md +57 -0
- package/skills/resolve-todo-parallel/workflows/resolve-todo-parallel.workflow.js +398 -0
- package/skills/review/SKILL.md +1620 -0
- package/skills/review/references/anti-slop-scanner-hook.md +40 -0
- package/skills/review/references/defect-classes-migrations.md +9 -0
- package/skills/review/references/generated-input-oracle-surfaces.md +15 -0
- package/skills/review/references/review-e2e-testing.md +116 -0
- package/skills/review/references/review-evidence-comment.md +36 -0
- package/skills/review/references/review-todo-structure.md +165 -0
- package/skills/review/references/risk-tier-and-fix-rounds.md +97 -0
- package/skills/review/references/semgrep-custom-rules.yaml +174 -0
- package/skills/review/references/ultrathink-phases.md +98 -0
- package/skills/review/references/wfs.md +6 -0
- package/skills/review/scripts/emit-review-trailer.sh +499 -0
- package/skills/review/scripts/ensure-semgrep.sh +99 -0
- package/skills/review/scripts/fix-round-seats.sh +197 -0
- package/skills/review/test/emit-review-trailer.test.sh +387 -0
- package/skills/review/test/ensure-semgrep.test.sh +111 -0
- package/skills/review/test/fix-round-seats.test.sh +237 -0
- package/skills/review/workflows/README.md +147 -0
- package/skills/review/workflows/review.workflow.js +667 -0
- package/skills/schedule/SKILL.md +796 -0
- package/skills/seo-aeo/SKILL.md +139 -0
- package/skills/seo-aeo/scripts/validate-csp.sh +233 -0
- package/skills/seo-aeo/scripts/validate-seo.sh +262 -0
- package/skills/ship/SKILL.md +3023 -0
- package/skills/ship/references/adr-ordinal-collision.md +47 -0
- package/skills/ship/references/affected-gate-contract.md +115 -0
- package/skills/ship/references/ci-run-diagnosis.md +29 -0
- package/skills/ship/references/ci-workflow-authoring.md +68 -0
- package/skills/ship/references/cleanup-merged-archival.md +39 -0
- package/skills/ship/references/counsel-review-clo-attestation.md +38 -0
- package/skills/ship/references/cto-ruling-armed-push.md +5 -0
- package/skills/ship/references/decision-challenges-render.md +71 -0
- package/skills/ship/references/delegated-act-authoring-form.md +7 -0
- package/skills/ship/references/detached-battery-verdict.md +13 -0
- package/skills/ship/references/epic-close-sweep.md +62 -0
- package/skills/ship/references/feature-tweet-draft.md +42 -0
- package/skills/ship/references/followthrough-issue-body-template.md +47 -0
- package/skills/ship/references/followthrough-stub-template.sh +92 -0
- package/skills/ship/references/merge-queue-dequeue.md +39 -0
- package/skills/ship/references/operator-holds.md +179 -0
- package/skills/ship/references/pipeline-tally-render.md +43 -0
- package/skills/ship/references/pr-overlap.md +153 -0
- package/skills/ship/references/queue-mode.md +7 -0
- package/skills/ship/references/ready-run-wait.md +36 -0
- package/skills/ship/references/red-on-main-quarantine.md +110 -0
- package/skills/ship/references/register-update-pr-pattern.md +88 -0
- package/skills/ship/references/settle-then-admin-merge.md +130 -0
- package/skills/ship/references/supabase-column-probe.md +13 -0
- package/skills/ship/scripts/auto-close-scan.sh +76 -0
- package/skills/ship/scripts/battery-owed.sh +403 -0
- package/skills/ship/scripts/hold-gate-eval.sh +410 -0
- package/skills/ship/scripts/net-issue-flow.sh +665 -0
- package/skills/ship/scripts/pr-overlap-preflight.sh +483 -0
- package/skills/ship/scripts/ship-pir-action-items-gate.sh +246 -0
- package/skills/ship/scripts/technical-ruling-gate.sh +444 -0
- package/skills/skill-creator/SKILL.md +299 -0
- package/skills/skill-creator/references/api-security.md +248 -0
- package/skills/skill-creator/references/authoring-levers.md +110 -0
- package/skills/skill-creator/references/be-clear-and-direct.md +587 -0
- package/skills/skill-creator/references/best-practices.md +431 -0
- package/skills/skill-creator/references/common-patterns.md +639 -0
- package/skills/skill-creator/references/core-principles.md +193 -0
- package/skills/skill-creator/references/executable-code.md +188 -0
- package/skills/skill-creator/references/iteration-and-testing.md +509 -0
- package/skills/skill-creator/references/official-spec.md +193 -0
- package/skills/skill-creator/references/recommended-structure.md +180 -0
- package/skills/skill-creator/references/skill-structure.md +396 -0
- package/skills/skill-creator/references/using-scripts.md +122 -0
- package/skills/skill-creator/references/using-templates.md +120 -0
- package/skills/skill-creator/references/workflows-and-validation.md +541 -0
- package/skills/skill-creator/scripts/init_skill.py +322 -0
- package/skills/skill-creator/scripts/package_skill.py +110 -0
- package/skills/skill-creator/scripts/quick_validate.py +64 -0
- package/skills/skill-creator/templates/router-skill.md +73 -0
- package/skills/skill-creator/templates/simple-skill.md +34 -0
- package/skills/skill-creator/workflows/add-reference.md +96 -0
- package/skills/skill-creator/workflows/add-script.md +104 -0
- package/skills/skill-creator/workflows/add-template.md +83 -0
- package/skills/skill-creator/workflows/add-workflow.md +128 -0
- package/skills/skill-creator/workflows/audit-skill.md +159 -0
- package/skills/skill-creator/workflows/create-domain-expertise-skill.md +623 -0
- package/skills/skill-creator/workflows/create-new-skill.md +207 -0
- package/skills/skill-creator/workflows/get-guidance.md +134 -0
- package/skills/skill-creator/workflows/upgrade-to-router.md +170 -0
- package/skills/skill-creator/workflows/verify-skill.md +226 -0
- package/skills/skill-security-scan/SKILL.md +145 -0
- package/skills/skill-security-scan/references/disclaimer.md +21 -0
- package/skills/skill-security-scan/references/first-party-allowlist.yaml +38 -0
- package/skills/skill-security-scan/references/override-artifact-schema.json +68 -0
- package/skills/skill-security-scan/references/override-mechanism.md +171 -0
- package/skills/skill-security-scan/references/redirect-domains.yaml +27 -0
- package/skills/skill-security-scan/references/regex-patterns.md +136 -0
- package/skills/skill-security-scan/references/rules/code-exec.yaml +70 -0
- package/skills/skill-security-scan/references/rules/filesystem-boundary.yaml +29 -0
- package/skills/skill-security-scan/references/rules/manifest.yaml +44 -0
- package/skills/skill-security-scan/references/rules/supply-chain.yaml +36 -0
- package/skills/skill-security-scan/references/scan-meta-schema.json +63 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-soleur-style.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/clean-third-party.skill.md +26 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-codeexec.skill.md +23 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-curl-pipe-bash.skill.md +69 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-prompt-injection.skill.md +17 -0
- package/skills/skill-security-scan/references/test-fixtures/malicious-telemetry-beacon.skill.md +23 -0
- package/skills/skill-security-scan/references/typosquat-targets.yaml +65 -0
- package/skills/skill-security-scan/scripts/check-codeexec.sh +41 -0
- package/skills/skill-security-scan/scripts/check-filesystem-boundary.sh +19 -0
- package/skills/skill-security-scan/scripts/check-prompt-injection.sh +95 -0
- package/skills/skill-security-scan/scripts/check-supply-chain.sh +219 -0
- package/skills/skill-security-scan/scripts/check-telemetry-surface.sh +126 -0
- package/skills/skill-security-scan/scripts/lib.sh +197 -0
- package/skills/skill-security-scan/scripts/parse-override.sh +231 -0
- package/skills/skill-security-scan/scripts/run-scan.sh +342 -0
- package/skills/skill-security-scan/scripts/run-self-test.sh +95 -0
- package/skills/skill-security-scan/test/argv-ceiling.test.sh +180 -0
- package/skills/skill-security-scan/test/meta-dir-reap.test.sh +411 -0
- package/skills/skill-security-scan/test/parse-override.test.sh +607 -0
- package/skills/skill-security-scan/test/redact-pii-ghs-shape.test.sh +251 -0
- package/skills/social-distribute/SKILL.md +485 -0
- package/skills/spec-templates/SKILL.md +274 -0
- package/skills/sync/SKILL.md +49 -0
- package/skills/test-browser/SKILL.md +464 -0
- package/skills/test-browser/references/feature-map.md +161 -0
- package/skills/test-fix-loop/SKILL.md +159 -0
- package/skills/test-fix-loop/references/determinism-checklist.md +154 -0
- package/skills/triage/SKILL.md +402 -0
- package/skills/trigger-cron/SKILL.md +136 -0
- package/skills/trigger-cron/scripts/trigger.sh +198 -0
- package/skills/user-set-role/SKILL.md +147 -0
- package/skills/user-set-role/scripts/set-role.sh +218 -0
- package/skills/user-story-writer/SKILL.md +77 -0
- package/skills/ux-audit/SKILL.md +251 -0
- package/skills/ux-audit/references/finding.schema.json +62 -0
- package/skills/ux-audit/references/route-list.yaml +95 -0
- package/skills/ux-audit/scripts/bot-fixture.ts +270 -0
- package/skills/ux-audit/scripts/bot-signin.ts +135 -0
- package/skills/ux-audit/scripts/dedup-hash.ts +46 -0
- package/skills/verification-scaffold/SKILL.md +73 -0
- package/skills/verification-scaffold/references/gitleaks.toml.template +13 -0
- package/skills/verification-scaffold/references/guardrail-audit.md +156 -0
- package/skills/verification-scaffold/references/guards/block-stash-hook.sh.template +72 -0
- package/skills/verification-scaffold/references/guards/commit-on-main-hook.sh.template +82 -0
- package/skills/verification-scaffold/references/guards/orphan-test-check.sh.template +102 -0
- package/skills/verification-scaffold/references/guards/precommit-guard.sh.template +115 -0
- package/skills/verification-scaffold/references/guards/secret-scan-hook.sh.template +176 -0
- package/skills/verification-scaffold/references/guards/verification-gates.yml.template +49 -0
- package/skills/verification-scaffold/references/guards/wrapper-boundary.sh.template +47 -0
- package/skills/verification-scaffold/references/guards.tsv +21 -0
- package/skills/verification-scaffold/references/verification-readme.template +55 -0
- package/skills/verification-scaffold/scripts/verification-scaffold.sh +785 -0
- package/skills/verification-scaffold/test/bite-proof.test.sh +159 -0
- package/skills/verification-scaffold/test/generator.test.sh +319 -0
- package/skills/verification-scaffold/test/manifest.test.sh +144 -0
- package/skills/work/SKILL.md +1480 -0
- package/skills/work/references/end-of-work-resume.md +7 -0
- package/skills/work/references/hook-test-main-cwd.md +24 -0
- package/skills/work/references/key-principles.md +83 -0
- package/skills/work/references/property-testing.md +122 -0
- package/skills/work/references/sql-security-definer-rpc-scaffold.sql +78 -0
- package/skills/work/references/work-agent-teams.md +97 -0
- package/skills/work/references/work-lifecycle-parallel.md +147 -0
- package/skills/work/references/work-lockfile-bumps.md +97 -0
- package/skills/work/references/work-scratch-sandboxes.md +212 -0
- package/skills/work/references/work-subagent-fanout.md +61 -0
- package/skills/work/references/work-test-harness-sharp-edges.md +73 -0
- package/skills/xcode-test/SKILL.md +360 -0
|
@@ -0,0 +1,1599 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Guard-contract suite for playwright-mcp-redact-proxy.py (#7980).
|
|
3
|
+
#
|
|
4
|
+
# Mirrors redact-a11y-snapshot.test.sh: ok/bad helpers, an instrument
|
|
5
|
+
# self-test, helper controls that must REJECT, then the rows, then a verdict
|
|
6
|
+
# reported with printf + exit (never through the helpers it backstops) and a
|
|
7
|
+
# MIN_ASSERTIONS floor bound ADJACENT to the floor block.
|
|
8
|
+
#
|
|
9
|
+
# Every session is driven by fixtures/proxy-session-driver.py against the
|
|
10
|
+
# table-driven stub fixtures/fake-playwright-mcp.py, fed the capture directory
|
|
11
|
+
# DERIVED from the .mcp.json pin; every process assertion is scoped to the pgid
|
|
12
|
+
# the proxy logs (never a machine-wide name match). Mutants are standalone copies
|
|
13
|
+
# written by fixtures/make-proxy-mutant.py (one edit, asserted landed by `diff -q`
|
|
14
|
+
# AND inside the named function's ast range), counted in THIS shell so a mutant
|
|
15
|
+
# that stops landing reds the run instead of silently deleting its row.
|
|
16
|
+
# An "absent" observable is only ever asserted together with a positive proof
|
|
17
|
+
# that the thing which would have produced it ran. `grep -q` reads herestrings or
|
|
18
|
+
# files only; a producer pipe is drained, `grep -c ... >/dev/null` (SIGPIPE under pipefail is a false negative).
|
|
19
|
+
#
|
|
20
|
+
# Three proxy paths: PROXY_SHIPPED (the file under test, and the source of every
|
|
21
|
+
# mutant), PROXY (what the rows drive; PROXY_UNDER_TEST points it at
|
|
22
|
+
# fixtures/fake-passthrough-proxy.py to observe the RED rows RED, QG5), and the
|
|
23
|
+
# per-row variant copies that sit beside a modified redactor.
|
|
24
|
+
set -Eeuo pipefail
|
|
25
|
+
# An abort names its line: under `set -e` a silent exit 1 is indistinguishable from a verdict.
|
|
26
|
+
trap 'printf "[ABORT] line %s: %s (rc=%s)\n" "$LINENO" "$BASH_COMMAND" "$?" >&2' ERR
|
|
27
|
+
export TMPDIR="${TMPDIR:-/var/tmp}"
|
|
28
|
+
export PLAYWRIGHT_MCP_PROXY_GRACE_S=1
|
|
29
|
+
# An ambient value would change what the fallback/env rows observe; every row that needs one sets it explicitly.
|
|
30
|
+
unset PLAYWRIGHT_MCP_PING_TIMEOUT_MS PLAYWRIGHT_MCP_BROWSER PLAYWRIGHT_MCP_PROXY_CHROME_PATHS
|
|
31
|
+
|
|
32
|
+
# Refuses an empty, relative, root or synthetic-fs fixture dir (byte-identical copy; the
|
|
33
|
+
# fixture-dir-operand-assert suite pins every tracked copy).
|
|
34
|
+
assert_fixture_dir() {
|
|
35
|
+
case "${1-}" in
|
|
36
|
+
"") printf 'FATAL: fixture dir is EMPTY; git -C "" would operate on %s\n' "$PWD" >&2; exit 2 ;;
|
|
37
|
+
*/../*|*/..) printf 'FATAL: fixture dir %s contains ..; refusing\n' "$1" >&2; exit 2 ;;
|
|
38
|
+
/proc/*|/sys/*|/dev/*) printf 'FATAL: fixture dir %s is a synthetic-fs path; refusing\n' "$1" >&2; exit 2 ;;
|
|
39
|
+
/|//|/.) printf 'FATAL: fixture dir resolves to the filesystem root; refusing\n' >&2; exit 2 ;;
|
|
40
|
+
/*) : ;;
|
|
41
|
+
*) printf 'FATAL: fixture dir %s is RELATIVE; refusing\n' "$1" >&2; exit 2 ;;
|
|
42
|
+
esac
|
|
43
|
+
}
|
|
44
|
+
REPO_ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)"
|
|
45
|
+
assert_fixture_dir "$REPO_ROOT"
|
|
46
|
+
SKILL="$REPO_ROOT/plugins/soleur/skills/agent-browser"
|
|
47
|
+
PROXY_SHIPPED="$SKILL/scripts/playwright-mcp-redact-proxy.py"
|
|
48
|
+
PROXY="${PROXY_UNDER_TEST:-$PROXY_SHIPPED}"
|
|
49
|
+
REDACTOR="$SKILL/scripts/redact-a11y-snapshot.py"
|
|
50
|
+
FIX="$SKILL/test/fixtures"
|
|
51
|
+
STUB="$FIX/fake-playwright-mcp.py"
|
|
52
|
+
PASSTHROUGH="$FIX/fake-passthrough-proxy.py"
|
|
53
|
+
DRV="$FIX/proxy-session-driver.py"
|
|
54
|
+
export FACTS="$FIX/session-facts.py"
|
|
55
|
+
MUTATE="$FIX/make-proxy-mutant.py"
|
|
56
|
+
ODD="$FIX/odd-shapes"
|
|
57
|
+
# The capture directory is DERIVED from the .mcp.json pin and handed to the stub
|
|
58
|
+
# on every session: a bump that forgets to re-capture reds here.
|
|
59
|
+
PIN="$(python3 -c "import json,re,sys; s=json.load(open(sys.argv[1]))['mcpServers']['playwright']['args'][1]; print(re.search(r'@playwright/mcp@([0-9.]+)', s).group(1))" "$REPO_ROOT/.mcp.json")"
|
|
60
|
+
CAPTURES="$FIX/playwright-mcp-$PIN"
|
|
61
|
+
SENTINEL='ZZQP-SENTINEL-7980'
|
|
62
|
+
BENIGN='ZZQP-BENIGN-7980'
|
|
63
|
+
WORK="$(mktemp -d -t proxy-suite.XXXXXXXX)"
|
|
64
|
+
assert_fixture_dir "$WORK"
|
|
65
|
+
MUT="$WORK/mutants"; mkdir -p "$MUT"; cp "$REDACTOR" "$MUT/" # every mutant loads the REAL predicate beside it
|
|
66
|
+
# Every child group a session logged. A FAKE_PW_HOLD stub ignores SIGTERM, so a row whose proxy
|
|
67
|
+
# does no teardown (the passthrough, a teardown mutant) leaves it alive; reap_all SIGKILLs each
|
|
68
|
+
# recorded group that still holds a stub process, at the hygiene row and again on EXIT.
|
|
69
|
+
PGIDS="$WORK/pgids"
|
|
70
|
+
stub_groups() { local pg; [[ -s "$PGIDS" ]] || return 0; while read -r pg; do if [[ -n "$pg" ]] && pgrep -g "$pg" -a 2>/dev/null | grep -cF >/dev/null -e "$STUB" -e 'sleep 300'; then printf '%s\n' "$pg"; fi; done < <(sort -u "$PGIDS"); }
|
|
71
|
+
reap_all() { local pg; while read -r pg; do pkill -KILL -g "$pg" 2>/dev/null || true; done < <(stub_groups); }
|
|
72
|
+
trap 'reap_all; rm -rf "$WORK"' EXIT
|
|
73
|
+
|
|
74
|
+
INIT='{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{"roots":{}},"clientInfo":{"name":"suite","version":"0"}}}'
|
|
75
|
+
LIST='{"jsonrpc":"2.0","id":2,"method":"tools/list"}'
|
|
76
|
+
SNAP='{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}'
|
|
77
|
+
SNAP4='{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}'
|
|
78
|
+
NAV='{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"browser_navigate","arguments":{"url":"http://127.0.0.1:1/"}}}'
|
|
79
|
+
FIND='{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"browser_find","arguments":{"text":"Token"}}}'
|
|
80
|
+
EVAL='{"jsonrpc":"2.0","id":7,"method":"tools/call","params":{"name":"browser_evaluate","arguments":{"function":"() => 1"}}}'
|
|
81
|
+
SHOT='{"jsonrpc":"2.0","id":8,"method":"tools/call","params":{"name":"browser_take_screenshot","arguments":{}}}'
|
|
82
|
+
CLOSE='{"jsonrpc":"2.0","id":9,"method":"tools/call","params":{"name":"browser_close","arguments":{}}}'
|
|
83
|
+
NOTIF='{"jsonrpc":"2.0","method":"notifications/initialized"}'
|
|
84
|
+
FILENAME_REQ() { printf '{"jsonrpc":"2.0","id":%s,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{"filename":"%s"}}}' "$1" "$2"; }
|
|
85
|
+
|
|
86
|
+
pass=0; fail=0; cases=0; mutants_declared=0; red_rows=0
|
|
87
|
+
ok() { printf 'ok - %s\n' "$1"; pass=$((pass + 1)); }
|
|
88
|
+
bad() { printf 'FAIL - %s\n' "$1"; fail=$((fail + 1)); }
|
|
89
|
+
|
|
90
|
+
# ---------------------------------------------------------------------------
|
|
91
|
+
# Instrument self-test (runs BEFORE any real row): both verdict helpers must
|
|
92
|
+
# move their counters or the suite refuses to continue.
|
|
93
|
+
# ---------------------------------------------------------------------------
|
|
94
|
+
_p0=$pass; _f0=$fail
|
|
95
|
+
ok "instrument self-test: ok() increments"
|
|
96
|
+
bad "instrument self-test: bad() increments (EXPECTED, not a real failure)"
|
|
97
|
+
if [[ $pass -ne $((_p0 + 1)) || $fail -ne $((_f0 + 1)) ]]; then
|
|
98
|
+
printf 'INSTRUMENT BROKEN: ok()/bad() did not both move\n' >&2; exit 1
|
|
99
|
+
fi
|
|
100
|
+
pass=0; fail=0; cases=0
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------------
|
|
103
|
+
# Session + fact helpers (exported at definition: `bash -c` rows use them)
|
|
104
|
+
# ---------------------------------------------------------------------------
|
|
105
|
+
# session <name> <proxy> [driver args...] ; prints the out dir. The stub is the
|
|
106
|
+
# server unless the caller passes --server; the pin-derived capture dir is the
|
|
107
|
+
# stub's fixture dir unless the caller passes its own FAKE_PW_FIXTURE_DIR.
|
|
108
|
+
session() {
|
|
109
|
+
local name="$1" proxy="$2"; shift 2
|
|
110
|
+
local out="$WORK/s-$name"; rm -rf "$out"
|
|
111
|
+
local args=(--env "FAKE_PW_FIXTURE_DIR=$CAPTURES" "$@")
|
|
112
|
+
[[ " $* " == *" --server "* ]] || args+=(--server python3 "$STUB")
|
|
113
|
+
python3 "$DRV" --out "$out" --proxy "$proxy" "${args[@]}" >/dev/null 2>&1 || true
|
|
114
|
+
printf '%s\n' "$(cat "$out/child_pgid" 2>/dev/null || true)" >> "$PGIDS"
|
|
115
|
+
printf '%s' "$out"
|
|
116
|
+
}
|
|
117
|
+
fact() { python3 "$FACTS" "$@" 2>/dev/null || printf 'ERR'; }
|
|
118
|
+
rcof() { cat "$1/rc" 2>/dev/null || printf 'none'; }
|
|
119
|
+
stderr_has() { [[ -f "$1/stderr.txt" ]] && grep -qF -- "$2" "$1/stderr.txt"; } # file, never a pipe
|
|
120
|
+
stdout_has() { [[ -f "$1/stdout.bin" ]] && grep -qaF -- "$2" "$1/stdout.bin"; }
|
|
121
|
+
# leaks <out> <id> — a response for <id> was DELIVERED and carries the sentinel
|
|
122
|
+
leaks() { [[ "$(fact "$1" "$2" found)" == "1" && "$(fact "$1" "$2" sentinel)" != "0" ]]; }
|
|
123
|
+
# delivered_ok <out> <id> — a RESULT for <id> was delivered: not a JSON-RPC error, not an isError result
|
|
124
|
+
delivered_ok() { [[ "$(fact "$1" "$2" found)" == "1" && "$(fact "$1" "$2" jsonrpc_error)" == "0" && "$(fact "$1" "$2" isError)" != "true" ]]; }
|
|
125
|
+
# started <out> — the proxy got past startup: not a refusal, and it logged the child
|
|
126
|
+
started() { [[ "$(rcof "$1")" != "2" && "$(rcof "$1")" != "none" ]] && stderr_has "$1" 'child pgid'; }
|
|
127
|
+
text_has() { [[ "$(fact "$1" "$2" text)" == *"$3"* ]]; }
|
|
128
|
+
# `A && B` as a function's last list trips `set -e` when B fails, and pkill on an already-empty group fails
|
|
129
|
+
reap_group() { local pg; pg="$(cat "$1/child_pgid" 2>/dev/null || true)"; if [[ -n "$pg" ]]; then pkill -KILL -g "$pg" 2>/dev/null || true; fi; }
|
|
130
|
+
export -f fact rcof stderr_has stdout_has leaks delivered_ok started text_has
|
|
131
|
+
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
# Verdict-owning helpers. Each increments `cases`; each is driven with an input
|
|
134
|
+
# it MUST reject in the helper control below.
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
# assert_redacted_result <label> <out> <id-json> <benign-count>
|
|
137
|
+
# The NEGATIVE half (sentinel count 0) is load-bearing; the benign count
|
|
138
|
+
# proves the helper can discriminate an over-aggressive rewrite from a leak.
|
|
139
|
+
assert_redacted_result() {
|
|
140
|
+
local label="$1" out="$2" id="$3" want_benign="$4"
|
|
141
|
+
cases=$((cases + 1))
|
|
142
|
+
local found sent ben err
|
|
143
|
+
found="$(fact "$out" "$id" found)"; sent="$(fact "$out" "$id" sentinel)"; ben="$(fact "$out" "$id" benign)"; err="$(fact "$out" "$id" isError)"
|
|
144
|
+
if [[ "$found" != "1" ]]; then bad "$label — no response for id $id was delivered"
|
|
145
|
+
elif [[ "$err" == "true" ]]; then bad "$label — result was withheld, not redacted"
|
|
146
|
+
elif [[ "$sent" != "0" ]]; then bad "$label — sentinel survived ($sent occurrences)"
|
|
147
|
+
elif [[ "$ben" != "$want_benign" ]]; then bad "$label — benign count $ben, expected $want_benign (over- or under-rewrite)"
|
|
148
|
+
else ok "$label"; fi
|
|
149
|
+
}
|
|
150
|
+
# assert_byte_identical <label> <out> <id-json> <fixture.json>
|
|
151
|
+
assert_byte_identical() {
|
|
152
|
+
local label="$1" out="$2" id="$3" fixture="$4"
|
|
153
|
+
cases=$((cases + 1))
|
|
154
|
+
local got want
|
|
155
|
+
got="$(fact "$out" "$id" rawline)"; want="$(fact "$out" "$id" expected_line "$fixture")"
|
|
156
|
+
if [[ -z "$got" ]]; then bad "$label — no response for id $id"
|
|
157
|
+
elif [[ "$got" != "$want" ]]; then bad "$label — bytes differ from the stub's line"
|
|
158
|
+
else ok "$label"; fi
|
|
159
|
+
}
|
|
160
|
+
# assert_withheld <label> <out> <id-json> <reason-needle> — refused OR withheld result
|
|
161
|
+
assert_withheld() {
|
|
162
|
+
local label="$1" out="$2" id="$3" needle="$4"
|
|
163
|
+
cases=$((cases + 1))
|
|
164
|
+
local found err sent text
|
|
165
|
+
found="$(fact "$out" "$id" found)"; err="$(fact "$out" "$id" isError)"; sent="$(fact "$out" "$id" sentinel)"; text="$(fact "$out" "$id" text)"
|
|
166
|
+
if [[ "$found" != "1" ]]; then bad "$label — no response for id $id (a dropped result is as RED as a raw one)"
|
|
167
|
+
elif [[ "$err" != "true" ]]; then bad "$label — not withheld (isError=$err)"
|
|
168
|
+
elif [[ "$sent" != "0" ]]; then bad "$label — sentinel present in the withheld result"
|
|
169
|
+
elif [[ "$text" != *"$needle"* ]]; then bad "$label — reason lacks '$needle'"
|
|
170
|
+
elif [[ "$text" != *"withheld by playwright-mcp-redact-proxy:"* && "$text" != *"refused by playwright-mcp-redact-proxy:"* ]]; then bad "$label — text lacks the pinned proxy prefix"
|
|
171
|
+
elif [[ "$text" != *"call browser_snapshot with no filename"* ]]; then bad "$label — next-step line missing"
|
|
172
|
+
else ok "$label"; fi
|
|
173
|
+
}
|
|
174
|
+
# assert_refused_start <label> <out> <reason-needle>
|
|
175
|
+
assert_refused_start() {
|
|
176
|
+
local label="$1" out="$2" needle="$3"
|
|
177
|
+
cases=$((cases + 1))
|
|
178
|
+
local rc lines pg
|
|
179
|
+
rc="$(rcof "$out")"; lines="$(fact "$out" 0 lines)"; pg="$(cat "$out/child_pgid" 2>/dev/null || true)"
|
|
180
|
+
if [[ "$rc" != "2" ]]; then bad "$label — exit $rc, expected 2"
|
|
181
|
+
elif ! stderr_has "$out" "playwright-mcp-redact-proxy: refusing to start:"; then bad "$label — no 'playwright-mcp-redact-proxy: refusing to start:' line"
|
|
182
|
+
elif ! stderr_has "$out" "$needle"; then bad "$label — reason lacks '$needle'"
|
|
183
|
+
elif [[ "$lines" != "0" ]]; then bad "$label — wrote $lines stdout line(s) before refusing"
|
|
184
|
+
elif [[ -n "$pg" ]]; then bad "$label — a child was spawned (pgid $pg)"
|
|
185
|
+
else ok "$label"; fi
|
|
186
|
+
}
|
|
187
|
+
# assert_stderr_marker <label> <out> <needle> <count>
|
|
188
|
+
assert_stderr_marker() {
|
|
189
|
+
local label="$1" out="$2" needle="$3" want="$4"
|
|
190
|
+
cases=$((cases + 1))
|
|
191
|
+
local n; n="$(fact "$out" 0 "stderr_count:$needle")"
|
|
192
|
+
if [[ "$n" == "$want" ]]; then ok "$label"; else bad "$label — '$needle' appears $n time(s), expected $want"; fi
|
|
193
|
+
}
|
|
194
|
+
# assert_group_empty <label> <out>
|
|
195
|
+
assert_group_empty() {
|
|
196
|
+
local label="$1" out="$2"
|
|
197
|
+
cases=$((cases + 1))
|
|
198
|
+
local pg n; pg="$(cat "$out/child_pgid" 2>/dev/null || true)"; n="$(cat "$out/group_after" 2>/dev/null || echo -1)"
|
|
199
|
+
if [[ -z "$pg" ]]; then bad "$label — proxy never logged the child pgid"
|
|
200
|
+
elif [[ "$n" != "0" ]]; then bad "$label — group $pg still has $n member(s)"
|
|
201
|
+
else ok "$label"; fi
|
|
202
|
+
}
|
|
203
|
+
# assert_rc <label> <out> <expected>
|
|
204
|
+
assert_rc() {
|
|
205
|
+
local label="$1" out="$2" want="$3"
|
|
206
|
+
cases=$((cases + 1))
|
|
207
|
+
local rc; rc="$(rcof "$out")"
|
|
208
|
+
if [[ "$rc" == "$want" ]]; then ok "$label"; else bad "$label — exit $rc, expected $want"; fi
|
|
209
|
+
}
|
|
210
|
+
# assert_true <label> <shell-test...> — for one-off observables
|
|
211
|
+
assert_true() {
|
|
212
|
+
local label="$1"; shift
|
|
213
|
+
cases=$((cases + 1))
|
|
214
|
+
if "$@"; then ok "$label"; else bad "$label"; fi
|
|
215
|
+
}
|
|
216
|
+
# mutant <name> <function> <old> <new> — writes $MUT/<name>.py and sets MUTANT_PATH
|
|
217
|
+
# (empty when the edit did not land). Runs in THIS shell: its verdict and its
|
|
218
|
+
# count survive, so a mutant that stops landing is a FAIL, never a vanished row.
|
|
219
|
+
MUTANT_PATH=""
|
|
220
|
+
mutant() {
|
|
221
|
+
local name="$1" func="$2" old="$3" new="$4"
|
|
222
|
+
local out="$MUT/$name.py"
|
|
223
|
+
cases=$((cases + 1)); mutants_declared=$((mutants_declared + 1)); MUTANT_PATH=""
|
|
224
|
+
if python3 "$MUTATE" "$PROXY_SHIPPED" "$out" "$func" "$old" "$new" >/dev/null 2>"$MUT/$name.err" && ! diff -q "$PROXY_SHIPPED" "$out" >/dev/null; then
|
|
225
|
+
ok "mutant $name landed inside $func"; MUTANT_PATH="$out"
|
|
226
|
+
else
|
|
227
|
+
bad "mutant $name did NOT land: $(tr '\n' ' ' < "$MUT/$name.err")"
|
|
228
|
+
fi
|
|
229
|
+
}
|
|
230
|
+
# red <label> <shell-test...> — a mutation row is ok when the DEFECT is observable
|
|
231
|
+
red() { local label="$1"; shift; cases=$((cases + 1)); red_rows=$((red_rows + 1)); if "$@"; then ok "$label"; else bad "$label — mutant survived"; fi; }
|
|
232
|
+
|
|
233
|
+
# ---------------------------------------------------------------------------
|
|
234
|
+
# Helper control: every verdict-owning helper driven with an input it MUST
|
|
235
|
+
# reject. Counters are unwound afterwards; the suite refuses to continue if any
|
|
236
|
+
# helper failed to reject.
|
|
237
|
+
# ---------------------------------------------------------------------------
|
|
238
|
+
_helper_control() {
|
|
239
|
+
local _p=$pass _f=$fail _c=$cases _m=$mutants_declared _r=$red_rows
|
|
240
|
+
local leak; leak="$(session hc-leak "$PASSTHROUGH" --send "$INIT" --send "$SNAP" --end eof)"
|
|
241
|
+
local clean="$WORK/s-hc-clean"; mkdir -p "$clean"
|
|
242
|
+
printf '%s\n' '{"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"- textbox \"Token\" [ref=e1]: <redacted>"}]}}' > "$clean/stdout.bin"
|
|
243
|
+
printf 'playwright-mcp-redact-proxy: refusing to start: synthetic\n' > "$clean/stderr.txt"; printf '2\n' > "$clean/rc"; printf '\n' > "$clean/child_pgid"; printf '3\n' > "$clean/group_after"
|
|
244
|
+
assert_redacted_result 'helper control: assert_redacted_result must REJECT a leak (EXPECTED)' "$leak" 3 1
|
|
245
|
+
assert_withheld 'helper control: assert_withheld must REJECT a raw forwarded result (EXPECTED)' "$leak" 3 'x'
|
|
246
|
+
assert_byte_identical 'helper control: assert_byte_identical must REJECT a differing line (EXPECTED)' "$leak" 3 "$CAPTURES/navigate.json"
|
|
247
|
+
assert_refused_start 'helper control: assert_refused_start must REJECT a started proxy (EXPECTED)' "$leak" 'x'
|
|
248
|
+
assert_stderr_marker 'helper control: assert_stderr_marker must REJECT a wrong count (EXPECTED)' "$leak" 'child pgid' 7
|
|
249
|
+
assert_group_empty 'helper control: assert_group_empty must REJECT a missing pgid (EXPECTED)' "$clean"
|
|
250
|
+
assert_rc 'helper control: assert_rc must REJECT a wrong exit (EXPECTED)' "$clean" 9
|
|
251
|
+
assert_true 'helper control: assert_true must REJECT false (EXPECTED)' false
|
|
252
|
+
red 'helper control: red must REJECT an unobservable defect (EXPECTED)' false
|
|
253
|
+
assert_true 'helper control: leaks must be FALSE on a redacted response (EXPECTED)' leaks "$clean" 3
|
|
254
|
+
assert_true 'helper control: leaks must be TRUE on the passthrough (EXPECTED)' bash -c '! leaks "$1" 3' _ "$leak"
|
|
255
|
+
assert_true 'helper control: started must be FALSE on a refused start (EXPECTED)' started "$clean"
|
|
256
|
+
assert_true 'helper control: delivered_ok must be FALSE on a missing id (EXPECTED)' delivered_ok "$clean" 99
|
|
257
|
+
mutant hc-nonunique-EXPECTED rewrite_result 'self' 'this' # <old> is not unique, so it must not land
|
|
258
|
+
if [[ $fail -ne $((_f + 14)) ]]; then
|
|
259
|
+
printf 'HELPER CONTROL BROKEN: expected 14 rejections, fail %d->%d\n' "$_f" "$fail" >&2; exit 1
|
|
260
|
+
fi
|
|
261
|
+
pass=$_p; fail=$_f; cases=$_c; mutants_declared=$_m; red_rows=$_r
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if [[ ! -f "$PROXY" ]]; then
|
|
265
|
+
printf 'FAIL - proxy missing at %s\n\nRED: the proxy does not exist yet (expected pre-implementation state).\n' "$PROXY"; exit 1
|
|
266
|
+
fi
|
|
267
|
+
if [[ ! -d "$CAPTURES" ]]; then
|
|
268
|
+
printf 'FAIL - capture directory %s missing: the .mcp.json pin is %s; re-run fixtures/capture-playwright-mcp-fixtures.py\n' "$CAPTURES" "$PIN"; exit 1
|
|
269
|
+
fi
|
|
270
|
+
_helper_control
|
|
271
|
+
|
|
272
|
+
# ---------------------------------------------------------------------------
|
|
273
|
+
# Instrument rows: the stub, driven through the PASSTHROUGH (no rewrite), must
|
|
274
|
+
# produce the artefacts the matrix rows assert on.
|
|
275
|
+
# ---------------------------------------------------------------------------
|
|
276
|
+
out="$(session inst "$PASSTHROUGH" --env FAKE_PW_ARGV_OUT="$WORK/inst-argv" --env FAKE_PW_REQUEST_LOG="$WORK/inst-req" \
|
|
277
|
+
--send "$INIT" --send "$(FILENAME_REQ 3 "$WORK/inst-raw.yml")" --send "$SNAP4" --end eof)"
|
|
278
|
+
assert_true 'instrument: stub WRITES the raw tree for a filename call' test -s "$WORK/inst-raw.yml"
|
|
279
|
+
assert_true 'instrument: the written file carries the sentinel' grep -qF "$SENTINEL" "$WORK/inst-raw.yml"
|
|
280
|
+
assert_true 'instrument: stub request log records the call' grep -qF '"name": "browser_snapshot"' "$WORK/inst-req"
|
|
281
|
+
assert_true 'instrument: stub records its argv' test -f "$WORK/inst-argv"
|
|
282
|
+
assert_true 'instrument: passthrough forwards the raw tree (known negative)' leaks "$out" 4
|
|
283
|
+
assert_true 'instrument: the stub refuses to run without FAKE_PW_FIXTURE_DIR' bash -c 'rc=0; env -u FAKE_PW_FIXTURE_DIR python3 "$1" </dev/null >/dev/null 2>&1 || rc=$?; [[ $rc == 64 ]]' _ "$STUB"
|
|
284
|
+
out="$(session inst-notify "$PASSTHROUGH" --env FAKE_PW_NOTIFY=1 --env FAKE_PW_SERVER_REQUEST=1 --send "$INIT" --send "$SNAP" --end eof)"
|
|
285
|
+
assert_true 'instrument: FAKE_PW_NOTIFY / FAKE_PW_SERVER_REQUEST put a tree on the wire (known negative)' bash -c '[[ "$(grep -caF "notifications/message" "$1/stdout.bin")" == "1" && "$(grep -caF "sampling/createMessage" "$1/stdout.bin")" == "1" ]] && stdout_has "$1" ZZQP-SENTINEL-7980' _ "$out"
|
|
286
|
+
out="$(session inst-cancel "$PASSTHROUGH" --env FAKE_PW_CANCEL=1 --send "$INIT" --send "$SNAP" --end eof)"
|
|
287
|
+
assert_true 'instrument: FAKE_PW_CANCEL puts a tree in a relayed-set reason, _meta and requestId (known negative)' bash -c '[[ "$(grep -caF "notifications/cancelled" "$1/stdout.bin")" == "2" ]] && [[ "$(grep -caF "\"_meta\"" "$1/stdout.bin")" == "1" ]] && stdout_has "$1" ZZQP-SENTINEL-7980' _ "$out"
|
|
288
|
+
hold="$(session inst-hold "$PASSTHROUGH" --env FAKE_PW_HOLD=1 --send "$INIT" --end eof --timeout 3)"
|
|
289
|
+
assert_true 'instrument: FAKE_PW_HOLD keeps the group alive past stdin EOF (passthrough does no teardown)' test "$(cat "$hold/group_after")" -gt 0
|
|
290
|
+
reap_group "$hold"
|
|
291
|
+
gc="$(session inst-grandchild "$PASSTHROUGH" --env FAKE_PW_GRANDCHILD=1 --send "$INIT" --end eof --timeout 3)"
|
|
292
|
+
assert_true 'instrument: FAKE_PW_GRANDCHILD — the SIGTERM-ignoring grandchild holds the group past stdin EOF' bash -c '[[ "$(cat "$1/group_after")" -gt 0 ]]' _ "$gc"
|
|
293
|
+
reap_group "$gc"
|
|
294
|
+
|
|
295
|
+
# ---------------------------------------------------------------------------
|
|
296
|
+
# FR1/FR2/FR2b/FR10/FR10b/FR11 — the shipped path, one canonical session
|
|
297
|
+
# ---------------------------------------------------------------------------
|
|
298
|
+
main="$(session main "$PROXY" --send "$INIT" --send "$NOTIF" --send "$LIST" --send "$SNAP" --send "$NAV" --send "$FIND" --send "$EVAL" --send "$SHOT" --send "$CLOSE" --end eof)"
|
|
299
|
+
assert_byte_identical 'FR1/FR11: initialize result forwarded byte-identical' "$main" 1 "$CAPTURES/initialize.json"
|
|
300
|
+
assert_redacted_result 'FR2: bare browser_snapshot — password/Token redacted, Notes intact (P3)' "$main" 3 1
|
|
301
|
+
assert_true 'FR2: <redacted> marker present on the credential rows' bash -c 'text_has "$1" 3 "\"Enter your password\" [ref=e4]: <redacted>" && text_has "$1" 3 "\"Token\" [ref=e6]: <redacted>"' _ "$main"
|
|
302
|
+
assert_true 'FR2: Email address survives in clear (must-PASS)' text_has "$main" 3 'probe-user@example.invalid'
|
|
303
|
+
assert_redacted_result 'FR2b/P7: browser_find result redacted by SHAPE (no tool-name allowlist)' "$main" 6 0
|
|
304
|
+
assert_true 'FR10b: tree-carrying result ends with the trailer block' test "$(fact "$main" 3 trailer)" = "1"
|
|
305
|
+
assert_true 'FR10b: browser_find result ends with the trailer block' test "$(fact "$main" 6 trailer)" = "1"
|
|
306
|
+
assert_true 'FR10b: browser_navigate (no tree) carries NO trailer' bash -c 'delivered_ok "$1" 5 && [[ "$(fact "$1" 5 trailer)" == "0" ]]' _ "$main"
|
|
307
|
+
assert_byte_identical 'FR11/P1: browser_navigate forwarded byte-identical' "$main" 5 "$CAPTURES/navigate.json"
|
|
308
|
+
assert_byte_identical 'FR11/P2: browser_evaluate object result forwarded byte-identical' "$main" 7 "$CAPTURES/evaluate.json"
|
|
309
|
+
assert_byte_identical 'FR11/P4: screenshot (text + image) forwarded byte-identical' "$main" 8 "$CAPTURES/screenshot.json"
|
|
310
|
+
assert_byte_identical 'FR11/P5: browser_close result (no isClose on the wire) forwarded' "$main" 9 "$CAPTURES/close.json"
|
|
311
|
+
assert_true 'FR10: tools/list — browser_snapshot description ends with the marker' bash -c 'd="$(fact "$1" 2 description:browser_snapshot)"; [[ "$d" == *"redacted in flight"* && "$d" == *"filename is refused — call browser_snapshot with no filename.]" ]]' _ "$main"
|
|
312
|
+
assert_true 'FR10: other tool descriptions untouched' python3 - "$main" "$CAPTURES/tools-list.json" <<'PY'
|
|
313
|
+
import json, sys
|
|
314
|
+
fx = {t["name"]: t["description"] for t in json.load(open(sys.argv[2]))["result"]["tools"]}
|
|
315
|
+
for o in json.load(open(sys.argv[1] + "/responses.json")):
|
|
316
|
+
if isinstance(o, dict) and o.get("id") == 2:
|
|
317
|
+
got = {t["name"]: t["description"] for t in o["result"]["tools"]}
|
|
318
|
+
sys.exit(0 if len(got) == len(fx) and all(got[n] == fx[n] for n in fx if n != "browser_snapshot") else 1)
|
|
319
|
+
sys.exit(1)
|
|
320
|
+
PY
|
|
321
|
+
assert_stderr_marker 'B7: initialize logs what was wrapped' "$main" 'wrapping Playwright' 1
|
|
322
|
+
assert_stderr_marker 'B4: child pgid logged once' "$main" 'child pgid' 1
|
|
323
|
+
assert_stderr_marker 'B10: teardown line on stdin EOF' "$main" 'child exited rc=0' 1
|
|
324
|
+
assert_stderr_marker 'PR2: a clean EOF session emits no swallowed marker (characterization)' "$main" 'swallowed' 0
|
|
325
|
+
assert_group_empty 'FR14: stdin EOF → child group empty' "$main"
|
|
326
|
+
assert_rc 'FR14: clean child exit → proxy exit 0' "$main" 0
|
|
327
|
+
assert_true 'NFR2: every stdout line of a full session is a JSON-RPC object' test "$(fact "$main" 0 alljson)" = "1"
|
|
328
|
+
|
|
329
|
+
# roots/list from the server (P5): rebuilt to method + id, map untouched
|
|
330
|
+
roots="$(session roots "$PROXY" --env FAKE_PW_ROOTS_COLLIDE=1 --send "$INIT" --send "$SNAP" --end eof)"
|
|
331
|
+
assert_true 'FR11/P5: roots/list server request forwarded (with a COLLIDING id)' stdout_has "$roots" '{"jsonrpc":"2.0","id":3,"method":"roots/list"}'
|
|
332
|
+
assert_redacted_result 'FR11b: colliding roots/list id — result still redacted and delivered' "$roots" 3 1
|
|
333
|
+
|
|
334
|
+
# ---------------------------------------------------------------------------
|
|
335
|
+
# FR3 — refusals before the server sees the call (filename; ""; _meta)
|
|
336
|
+
# ---------------------------------------------------------------------------
|
|
337
|
+
ref="$(session refuse "$PROXY" --env FAKE_PW_REQUEST_LOG="$WORK/ref-req" --send "$INIT" \
|
|
338
|
+
--send "$(FILENAME_REQ 10 "$WORK/ref-raw.yml")" \
|
|
339
|
+
--send '{"jsonrpc":"2.0","id":"11","method":"tools/call","params":{"name":"browser_snapshot","arguments":{"filename":""}}}' \
|
|
340
|
+
--send '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{"_meta":{"json":true}}}}' \
|
|
341
|
+
--send '{"jsonrpc":"2.0","id":13,"method":"tools/call","params":{"name":"browser_navigate","arguments":{"_meta":{"raw":true},"url":"x"}}}' \
|
|
342
|
+
--send "$SNAP" --end eof)"
|
|
343
|
+
assert_withheld 'FR3: browser_snapshot + filename refused (id type int preserved)' "$ref" 10 'filename'
|
|
344
|
+
assert_withheld 'FR3: browser_snapshot + filename:"" refused on KEY presence (id type str preserved)' "$ref" '"11"' 'filename'
|
|
345
|
+
assert_withheld 'FR3: tools/call with arguments._meta refused' "$ref" 12 '_meta'
|
|
346
|
+
assert_withheld 'FR3: _meta refused on ANY tool, not only browser_snapshot' "$ref" 13 '_meta'
|
|
347
|
+
assert_true 'FR3: refusal text carries the proxy-unique token (the S2 signal)' text_has "$ref" 10 'refused by playwright-mcp-redact-proxy: filename writes the raw tree to disk'
|
|
348
|
+
assert_true 'FR3: the refused calls never reached the stub, which did receive the session' bash -c 'grep -qF "\"method\": \"initialize\"" "$1" && ! grep -qF "filename" "$1" && ! grep -qF "_meta" "$1"' _ "$WORK/ref-req"
|
|
349
|
+
assert_true 'FR3: no raw file was written' test ! -e "$WORK/ref-raw.yml"
|
|
350
|
+
assert_redacted_result 'FR3: the bare call after the refusals is still served' "$ref" 3 1
|
|
351
|
+
assert_stderr_marker 'FR7b: one pinned stderr line per refusal' "$ref" 'refused tool=' 4
|
|
352
|
+
assert_true 'FR7b/B9: stderr refusal lines never quote the input path' bash -c 'stderr_has "$1" "refused tool=" && ! grep -qF "ref-raw.yml" "$1/stderr.txt"' _ "$ref"
|
|
353
|
+
|
|
354
|
+
# ---------------------------------------------------------------------------
|
|
355
|
+
# FR4 — the injected flag
|
|
356
|
+
# ---------------------------------------------------------------------------
|
|
357
|
+
session argv "$PROXY" --env FAKE_PW_ARGV_OUT="$WORK/argv-out" --send "$INIT" --end eof >/dev/null
|
|
358
|
+
assert_true 'FR4: child argv ends with --snapshot-mode none' bash -c 'test -f "$1" && [[ "$(tail -n 2 "$1" | tr "\n" " ")" == "--snapshot-mode none " ]]' _ "$WORK/argv-out"
|
|
359
|
+
|
|
360
|
+
# ---------------------------------------------------------------------------
|
|
361
|
+
# FR15 — --user-data-dir-name: profile-dir injection and refusals (#8156)
|
|
362
|
+
# ---------------------------------------------------------------------------
|
|
363
|
+
UDD=(--proxy-arg --user-data-dir-name --proxy-arg soleur-test-profile)
|
|
364
|
+
session argv-udd "$PROXY" "${UDD[@]}" --env "XDG_CACHE_HOME=$WORK/xdg" --env FAKE_PW_ARGV_OUT="$WORK/argv-udd" --send "$INIT" --end eof >/dev/null
|
|
365
|
+
assert_true 'FR15: --user-data-dir=<XDG_CACHE_HOME>/<name> injected once, before --snapshot-mode' bash -c 'test -f "$1" && [[ "$(grep -c "^--user-data-dir=" "$1")" == "1" ]] && grep -qxF -- "--user-data-dir=$2/soleur-test-profile" "$1" && [[ "$(tail -n 2 "$1" | tr "\n" " ")" == "--snapshot-mode none " ]]' _ "$WORK/argv-udd" "$WORK/xdg"
|
|
366
|
+
session argv-udd-home "$PROXY" "${UDD[@]}" --env "XDG_CACHE_HOME=" --env "HOME=$WORK/home" --env FAKE_PW_ARGV_OUT="$WORK/argv-udd-home" --send "$INIT" --end eof >/dev/null
|
|
367
|
+
assert_true 'FR15: XDG_CACHE_HOME empty → profile resolves under $HOME/.cache' bash -c 'test -f "$1" && grep -qxF -- "--user-data-dir=$2/.cache/soleur-test-profile" "$1"' _ "$WORK/argv-udd-home" "$WORK/home"
|
|
368
|
+
assert_true 'FR15: flag absent → child argv carries no --user-data-dir (unchanged behaviour)' bash -c 'test -f "$1" && ! grep -q -- "--user-data-dir" "$1"' _ "$WORK/argv-out"
|
|
369
|
+
r="$(session udd-dotdot "$PROXY" --proxy-arg --user-data-dir-name --proxy-arg '..' --send "$INIT" --end eof --timeout 3)"
|
|
370
|
+
assert_refused_start 'FR15: a .. basename refuses to start' "$r" '..'
|
|
371
|
+
r="$(session udd-sep "$PROXY" --proxy-arg --user-data-dir-name --proxy-arg 'a/b' --send "$INIT" --end eof --timeout 3)"
|
|
372
|
+
assert_refused_start 'FR15: a basename containing / refuses to start' "$r" 'separator'
|
|
373
|
+
r="$(session udd-sep-bs "$PROXY" --proxy-arg --user-data-dir-name --proxy-arg 'a\b' --send "$INIT" --end eof --timeout 3)"
|
|
374
|
+
assert_refused_start 'FR15: a basename containing a backslash refuses to start' "$r" 'separator'
|
|
375
|
+
r="$(session udd-conflict "$PROXY" "${UDD[@]}" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --user-data-dir=/x)"
|
|
376
|
+
assert_refused_start 'FR15: the flag combined with an explicit --user-data-dir refuses to start' "$r" 'explicit'
|
|
377
|
+
r="$(session udd-relxdg "$PROXY" "${UDD[@]}" --env XDG_CACHE_HOME=relative/cache --send "$INIT" --end eof --timeout 3)"
|
|
378
|
+
assert_refused_start 'FR15: a relative XDG_CACHE_HOME refuses to start' "$r" 'absolute'
|
|
379
|
+
r="$(session udd-nohome "$PROXY" "${UDD[@]}" --env XDG_CACHE_HOME= --env HOME= --send "$INIT" --end eof --timeout 3)"
|
|
380
|
+
assert_refused_start 'FR15: XDG_CACHE_HOME and HOME both unset refuses to start' "$r" 'HOME is set'
|
|
381
|
+
r="$(session udd-missing "$PROXY" --proxy-arg --user-data-dir-name --send "$INIT" --end eof --timeout 3)"
|
|
382
|
+
assert_refused_start 'FR15: --user-data-dir-name with no argument refuses to start' "$r" 'basename argument'
|
|
383
|
+
r="$(session udd-unknown "$PROXY" --proxy-arg --bogus-flag --send "$INIT" --end eof --timeout 3)"
|
|
384
|
+
assert_refused_start 'FR15: an unrecognised proxy flag refuses to start' "$r" 'unrecognised'
|
|
385
|
+
r="$(session udd-dup "$PROXY" --proxy-arg --user-data-dir-name --proxy-arg a --proxy-arg --user-data-dir-name=b --send "$INIT" --end eof --timeout 3)"
|
|
386
|
+
assert_refused_start 'FR15: the flag given twice refuses to start' "$r" 'twice'
|
|
387
|
+
r="$(session udd-dash "$PROXY" --proxy-arg --user-data-dir-name --proxy-arg '--bogus' --send "$INIT" --end eof --timeout 3)"
|
|
388
|
+
assert_refused_start 'FR15: a basename beginning with - refuses to start' "$r" "leading '-'"
|
|
389
|
+
# the = spelling, and a root that only resolves after realpath normalisation
|
|
390
|
+
UDDEQ=(--proxy-arg --user-data-dir-name=eq-profile)
|
|
391
|
+
session argv-udd-eq "$PROXY" "${UDDEQ[@]}" --env "XDG_CACHE_HOME=$WORK/xdg" --env FAKE_PW_ARGV_OUT="$WORK/argv-udd-eq" --send "$INIT" --end eof >/dev/null
|
|
392
|
+
assert_true 'FR15: --user-data-dir-name=<name> injects the same profile dir' bash -c 'test -f "$1" && grep -qxF -- "--user-data-dir=$2/eq-profile" "$1"' _ "$WORK/argv-udd-eq" "$WORK/xdg"
|
|
393
|
+
r="$(session udd-eqempty "$PROXY" --proxy-arg '--user-data-dir-name=' --send "$INIT" --end eof --timeout 3)"
|
|
394
|
+
assert_refused_start 'FR15: --user-data-dir-name= (empty value) refuses to start' "$r" 'basename'
|
|
395
|
+
mkdir -p "$WORK/sub" "$WORK/xdg-real"; ln -sfn "$WORK/xdg-real" "$WORK/xdg-link"
|
|
396
|
+
session argv-udd-dotroot "$PROXY" "${UDD[@]}" --env "XDG_CACHE_HOME=$WORK/sub/.." --env FAKE_PW_ARGV_OUT="$WORK/argv-udd-dotroot" --send "$INIT" --end eof >/dev/null
|
|
397
|
+
assert_true 'FR15: a .. inside the profile ROOT is realpath-normalised — the child argv never sees it' bash -c 'test -f "$1" && grep -qxF -- "--user-data-dir=$2/soleur-test-profile" "$1" && ! grep -qF ".." "$1"' _ "$WORK/argv-udd-dotroot" "$WORK"
|
|
398
|
+
session argv-udd-sym "$PROXY" "${UDD[@]}" --env "XDG_CACHE_HOME=$WORK/xdg-link" --env FAKE_PW_ARGV_OUT="$WORK/argv-udd-sym" --send "$INIT" --end eof >/dev/null
|
|
399
|
+
assert_true 'FR15: a symlinked profile ROOT resolves to its target before the basename joins' bash -c 'test -f "$1" && grep -qxF -- "--user-data-dir=$2/soleur-test-profile" "$1" && ! grep -qF "xdg-link" "$1"' _ "$WORK/argv-udd-sym" "$WORK/xdg-real"
|
|
400
|
+
|
|
401
|
+
# ---------------------------------------------------------------------------
|
|
402
|
+
# FR16 — --chromium-fallback: `--browser chromium` only when the server would default to an absent Google Chrome.
|
|
403
|
+
# FR17 — PLAYWRIGHT_MCP_PING_TIMEOUT_MS reaches the server child (the proxy has no child-env allowlist).
|
|
404
|
+
# ---------------------------------------------------------------------------
|
|
405
|
+
CHR="$WORK/chr"; mkdir -p "$CHR"
|
|
406
|
+
printf '#!/bin/sh\nexit 0\n' > "$CHR/chrome-present"; chmod +x "$CHR/chrome-present"
|
|
407
|
+
printf 'not executable\n' > "$CHR/chrome-noexec"; chmod -x "$CHR/chrome-noexec"
|
|
408
|
+
CFB=(--proxy-arg --chromium-fallback)
|
|
409
|
+
NOCHROME="PLAYWRIGHT_MCP_PROXY_CHROME_PATHS=$CHR/chrome-absent"
|
|
410
|
+
HASCHROME="PLAYWRIGHT_MCP_PROXY_CHROME_PATHS=$CHR/chrome-present"
|
|
411
|
+
# The platform seam: the proxy reads sys.platform, so a shim that sets it and runs the proxy by path models another OS.
|
|
412
|
+
printf 'import os, runpy, sys\nsys.platform = os.environ["SHIM_PLATFORM"]\nrunpy.run_path(os.environ["SHIM_TARGET"], run_name="__main__")\n' > "$WORK/platform-shim.py"
|
|
413
|
+
# The path-list seam: the REAL GOOGLE_CHROME_PATHS constant (no PLAYWRIGHT_MCP_PROXY_CHROME_PATHS), with sys.platform
|
|
414
|
+
# and the two filesystem probes patched. Only the two documented Chrome paths (the literals below, passed in SHIM_DOC) are
|
|
415
|
+
# intercepted: isfile/access answer true for SHIM_PRESENT alone there. Every other path keeps its real answer, so the
|
|
416
|
+
# verdict never depends on a Chrome installed on the host running the suite, the shim itself carries no copy of the shipped
|
|
417
|
+
# list, and a checkout whose path happens to contain "chrome" (the proxy script, $WORK, the config files) is not hijacked.
|
|
418
|
+
# The documented Google Chrome locations are literals HERE (not read back from the proxy): a wrong default in the
|
|
419
|
+
# shipped constant must fail a row. A file at the documented path means Chrome is installed; one character off, it is not.
|
|
420
|
+
LINUX_CHROME='/opt/google/chrome/chrome'
|
|
421
|
+
DARWIN_CHROME='/Applications/Google Chrome.app/Contents/MacOS/Google Chrome'
|
|
422
|
+
printf 'import os, runpy, sys\nsys.platform = os.environ["SHIM_PLATFORM"]\npresent = os.environ["SHIM_PRESENT"]\ndoc = os.environ["SHIM_DOC"].split(os.pathsep)\n_isfile, _access = os.path.isfile, os.access\nos.path.isfile = lambda p: p == present if str(p) in doc else _isfile(p)\nos.access = lambda p, m, **k: p == present if str(p) in doc else _access(p, m, **k)\nrunpy.run_path(os.environ["SHIM_TARGET"], run_name="__main__")\n' > "$WORK/chrome-fs-shim.py"
|
|
423
|
+
FBCHK="$WORK/fb-check.py"
|
|
424
|
+
cat > "$FBCHK" <<'PY'
|
|
425
|
+
import sys
|
|
426
|
+
path, mode = sys.argv[1], sys.argv[2]
|
|
427
|
+
lines = open(path).read().splitlines()
|
|
428
|
+
n = lines.count("--browser")
|
|
429
|
+
idx = lines.index("--browser") if n else -1
|
|
430
|
+
tail = lines[-2:] == ["--snapshot-mode", "none"]
|
|
431
|
+
checks = {
|
|
432
|
+
# exactly one `--browser chromium --sandbox` triple, ahead of the proxy's own flags, chromium is never the last arg,
|
|
433
|
+
# and the bundled build never gets --no-sandbox from the proxy
|
|
434
|
+
"once": n == 1 and lines[idx + 1] == "chromium" and lines[idx + 2] == "--sandbox" and lines.count("chromium") == 1 and lines.count("--sandbox") == 1 and "--no-sandbox" not in lines and tail and idx + 1 != len(lines) - 1,
|
|
435
|
+
"none": n == 0 and "chromium" not in lines and "--sandbox" not in lines and tail,
|
|
436
|
+
"twice": n == 2 and lines.count("chromium") == 2 and lines.count("--sandbox") == 2,
|
|
437
|
+
"second-pair": n == 2 and lines.count("chromium") == 1 and "firefox" in lines,
|
|
438
|
+
"only-explicit": n == 1 and "chromium" not in lines and "--sandbox" not in lines and lines[idx + 1] == "firefox",
|
|
439
|
+
"eq-explicit": n == 0 and "--browser=firefox" in lines and "chromium" not in lines and "--sandbox" not in lines,
|
|
440
|
+
"udd-order": n == 1 and lines[idx + 1] == "chromium" and lines[idx + 2] == "--sandbox" and lines[idx + 3:] == ["--user-data-dir=" + (sys.argv[3] if len(sys.argv) > 3 else ""), "--snapshot-mode", "none"],
|
|
441
|
+
# the fallback fired but dropped the sandbox flag: the bundled Chromium would run unsandboxed on Linux
|
|
442
|
+
"no-sandbox-flag": n == 1 and lines[idx + 1] == "chromium" and "--sandbox" not in lines,
|
|
443
|
+
}
|
|
444
|
+
sys.exit(0 if checks[mode] else 1)
|
|
445
|
+
PY
|
|
446
|
+
out="$(session fb-absent "$PROXY" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-absent" --send "$INIT" --end eof)"
|
|
447
|
+
assert_true 'FR16: --chromium-fallback with no Chrome at the (seam) path appends --browser chromium --sandbox exactly once, ahead of --snapshot-mode, chromium is never the last arg, and no --no-sandbox appears' python3 "$FBCHK" "$WORK/fb-absent" once
|
|
448
|
+
assert_stderr_marker 'FR16: the fallback is logged once' "$out" 'adding --browser chromium' 1
|
|
449
|
+
assert_stderr_marker 'FR16: the fallback line says the sandbox is kept on' "$out" 'adding --browser chromium --sandbox' 1
|
|
450
|
+
out="$(session fb-noflag "$PROXY" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-noflag" --send "$INIT" --end eof)"
|
|
451
|
+
assert_true 'FR16: flag absent (and no Chrome) → child argv byte-identical to the unflagged baseline of FR4' bash -c 'test -f "$1" && test -f "$2" && cmp -s "$1" "$2"' _ "$WORK/fb-noflag" "$WORK/argv-out"
|
|
452
|
+
assert_stderr_marker 'FR16: flag absent → no fallback line' "$out" 'adding --browser chromium' 0
|
|
453
|
+
session fb-present "$PROXY" "${CFB[@]}" --env "$HASCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-present" --send "$INIT" --end eof >/dev/null
|
|
454
|
+
assert_true 'FR16: flag + an executable Chrome at the (seam) path → nothing appended' python3 "$FBCHK" "$WORK/fb-present" none
|
|
455
|
+
session fb-noexec "$PROXY" "${CFB[@]}" --env "PLAYWRIGHT_MCP_PROXY_CHROME_PATHS=$CHR/chrome-noexec" --env FAKE_PW_ARGV_OUT="$WORK/fb-noexec" --send "$INIT" --end eof >/dev/null
|
|
456
|
+
assert_true 'FR16: a present but non-executable file is not a Chrome → appended' python3 "$FBCHK" "$WORK/fb-noexec" once
|
|
457
|
+
session fb-list "$PROXY" "${CFB[@]}" --env "PLAYWRIGHT_MCP_PROXY_CHROME_PATHS=$CHR/chrome-absent:$CHR/chrome-present" --env FAKE_PW_ARGV_OUT="$WORK/fb-list" --send "$INIT" --end eof >/dev/null
|
|
458
|
+
assert_true 'FR16: the seam is an os.pathsep list — one present entry anywhere in it means Chrome is installed' python3 "$FBCHK" "$WORK/fb-list" none
|
|
459
|
+
session fb-explicit "$PROXY" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-explicit" --send "$INIT" --end eof --server python3 "$STUB" --browser firefox >/dev/null
|
|
460
|
+
assert_true 'FR16: an explicit --browser <v> in the server argv → nothing appended (one --browser, no chromium)' python3 "$FBCHK" "$WORK/fb-explicit" only-explicit
|
|
461
|
+
session fb-explicit-eq "$PROXY" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-explicit-eq" --send "$INIT" --end eof --server python3 "$STUB" --browser=firefox >/dev/null
|
|
462
|
+
assert_true 'FR16: an explicit --browser=<v> (the = spelling) → nothing appended' python3 "$FBCHK" "$WORK/fb-explicit-eq" eq-explicit
|
|
463
|
+
session fb-env "$PROXY" "${CFB[@]}" --env "$NOCHROME" --env PLAYWRIGHT_MCP_BROWSER=firefox --env FAKE_PW_ARGV_OUT="$WORK/fb-env" --send "$INIT" --end eof >/dev/null
|
|
464
|
+
assert_true 'FR16: PLAYWRIGHT_MCP_BROWSER set → nothing appended' python3 "$FBCHK" "$WORK/fb-env" none
|
|
465
|
+
session fb-linux "$WORK/platform-shim.py" "${CFB[@]}" --env SHIM_TARGET="$PROXY" --env SHIM_PLATFORM=linux --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-linux" --send "$INIT" --end eof >/dev/null
|
|
466
|
+
assert_true 'FR16 platform seam (positive proof): the shim runs the proxy as linux and the fallback applies' python3 "$FBCHK" "$WORK/fb-linux" once
|
|
467
|
+
session fb-darwin "$WORK/platform-shim.py" "${CFB[@]}" --env SHIM_TARGET="$PROXY" --env SHIM_PLATFORM=darwin --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-darwin" --send "$INIT" --end eof >/dev/null
|
|
468
|
+
assert_true 'FR16: macOS is modelled — the fallback applies there too' python3 "$FBCHK" "$WORK/fb-darwin" once
|
|
469
|
+
session fb-win "$WORK/platform-shim.py" "${CFB[@]}" --env SHIM_TARGET="$PROXY" --env SHIM_PLATFORM=win32 --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/fb-win" --send "$INIT" --end eof >/dev/null
|
|
470
|
+
assert_true 'FR16: any other platform is a no-op — nothing appended (the proxy still started and ran the child)' python3 "$FBCHK" "$WORK/fb-win" none
|
|
471
|
+
fs_session() { # <tag> <platform> <present-path> [extra --env args...] — the real path list under the filesystem shim
|
|
472
|
+
local tag="$1" plat="$2" present="$3"; shift 3
|
|
473
|
+
session "$tag" "$WORK/chrome-fs-shim.py" "${CFB[@]}" --env SHIM_TARGET="$PROXY" --env SHIM_PLATFORM="$plat" --env SHIM_PRESENT="$present" --env SHIM_DOC="$LINUX_CHROME:$DARWIN_CHROME" --env FAKE_PW_ARGV_OUT="$WORK/$tag" --send "$INIT" --end eof "$@" >/dev/null
|
|
474
|
+
}
|
|
475
|
+
fs_session fb-real-linux linux "$LINUX_CHROME"
|
|
476
|
+
assert_true 'FR16 shipped path list: an executable file at /opt/google/chrome/chrome on Linux → Chrome is installed, nothing appended' python3 "$FBCHK" "$WORK/fb-real-linux" none
|
|
477
|
+
fs_session fb-real-linux-miss linux "${LINUX_CHROME}2"
|
|
478
|
+
assert_true 'FR16 shipped path list: a near-miss path on Linux is not Chrome → the fallback applies' python3 "$FBCHK" "$WORK/fb-real-linux-miss" once
|
|
479
|
+
fs_session fb-real-darwin darwin "$DARWIN_CHROME"
|
|
480
|
+
assert_true 'FR16 shipped path list: an executable file at the Google Chrome.app binary on macOS → nothing appended' python3 "$FBCHK" "$WORK/fb-real-darwin" none
|
|
481
|
+
fs_session fb-real-darwin-miss darwin "${DARWIN_CHROME}2"
|
|
482
|
+
assert_true 'FR16 shipped path list: a near-miss path on macOS is not Chrome → the fallback applies' python3 "$FBCHK" "$WORK/fb-real-darwin-miss" once
|
|
483
|
+
fs_session fb-real-cross linux "$DARWIN_CHROME"
|
|
484
|
+
assert_true 'FR16 shipped path list: the macOS path does not count as Chrome on Linux (one table per platform)' python3 "$FBCHK" "$WORK/fb-real-cross" once
|
|
485
|
+
mkdir -p "$CHR/chrome-dir"
|
|
486
|
+
session fb-dir "$PROXY" "${CFB[@]}" --env "PLAYWRIGHT_MCP_PROXY_CHROME_PATHS=$CHR/chrome-dir" --env FAKE_PW_ARGV_OUT="$WORK/fb-dir" --send "$INIT" --end eof >/dev/null
|
|
487
|
+
assert_true 'FR16: a directory (searchable, so os.access passes) at a listed path is not a Chrome → appended' python3 "$FBCHK" "$WORK/fb-dir" once
|
|
488
|
+
fs_session fb-empty-seam linux "$LINUX_CHROME" --env PLAYWRIGHT_MCP_PROXY_CHROME_PATHS=
|
|
489
|
+
assert_true 'FR16: an empty path-list seam REPLACES the list (even with Chrome at the documented path) → appended' python3 "$FBCHK" "$WORK/fb-empty-seam" once
|
|
490
|
+
assert_true 'FR16: the Google Chrome paths in the shipped constant are the ones the module docstring documents (whitespace-normalised)' python3 - "$PROXY_SHIPPED" <<'PY'
|
|
491
|
+
import ast, sys
|
|
492
|
+
tree = ast.parse(open(sys.argv[1]).read())
|
|
493
|
+
doc = " ".join((ast.get_docstring(tree) or "").split())
|
|
494
|
+
paths = []
|
|
495
|
+
for node in tree.body:
|
|
496
|
+
if isinstance(node, ast.Assign) and any(getattr(t, "id", "") == "GOOGLE_CHROME_PATHS" for t in node.targets):
|
|
497
|
+
paths = [p for v in ast.literal_eval(node.value).values() for p in v]
|
|
498
|
+
sys.exit(0 if len(paths) == 2 and all(p in doc for p in paths) else 1)
|
|
499
|
+
PY
|
|
500
|
+
# The filesystem shim answers only for the two literals above. A path added to the shipped list is answered by the REAL filesystem, so
|
|
501
|
+
# the miss rows would silently read the host's Chrome again: the shipped constant must equal exactly these two literals, per platform.
|
|
502
|
+
assert_true 'FR16 shipped path list: GOOGLE_CHROME_PATHS is exactly {linux: [the Linux literal], darwin: [the macOS literal]}, the two paths the filesystem shim answers for (a widened list turns this row red)' python3 - "$PROXY_SHIPPED" "$LINUX_CHROME" "$DARWIN_CHROME" <<'PY'
|
|
503
|
+
import ast, sys
|
|
504
|
+
tree = ast.parse(open(sys.argv[1]).read())
|
|
505
|
+
found = None
|
|
506
|
+
for node in tree.body:
|
|
507
|
+
if isinstance(node, ast.Assign) and any(getattr(t, "id", "") == "GOOGLE_CHROME_PATHS" for t in node.targets):
|
|
508
|
+
found = ast.literal_eval(node.value)
|
|
509
|
+
sys.exit(0 if found == {"linux": [sys.argv[2]], "darwin": [sys.argv[3]]} else 1)
|
|
510
|
+
PY
|
|
511
|
+
r="$(session fb-twice "$PROXY" --proxy-arg --chromium-fallback --proxy-arg --chromium-fallback --send "$INIT" --end eof --timeout 3)"
|
|
512
|
+
assert_refused_start 'FR16: --chromium-fallback given twice refuses to start' "$r" 'twice'
|
|
513
|
+
r="$(session fb-eq "$PROXY" --proxy-arg --chromium-fallback=1 --send "$INIT" --end eof --timeout 3)"
|
|
514
|
+
assert_refused_start 'FR16: --chromium-fallback=<v> is not a recognised spelling (the flag is a boolean) and refuses to start' "$r" 'unrecognised'
|
|
515
|
+
r="$(session fb-trailing "$PROXY" "${CFB[@]}" --env "$NOCHROME" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --browser)"
|
|
516
|
+
assert_refused_start 'FR16: a trailing --browser would bind the appended flag as its value — refuses even with the fallback on' "$r" 'expects a value'
|
|
517
|
+
r="$(session fb-name-swallow "$PROXY" --proxy-arg --user-data-dir-name --proxy-arg --chromium-fallback --send "$INIT" --end eof --timeout 3)"
|
|
518
|
+
assert_refused_start 'FR16: --user-data-dir-name never swallows the fallback flag as its basename (a leading - refuses)' "$r" "leading '-'"
|
|
519
|
+
# The sandbox refusals hold with the fallback ON: the fallback adds --sandbox, it never opens a bypass for the forbidden spelling.
|
|
520
|
+
r="$(session fb-nosandbox "$PROXY" "${CFB[@]}" --env "$NOCHROME" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --no-sandbox)"
|
|
521
|
+
assert_refused_start 'FR16: --no-sandbox in the server argv still refuses to start with the fallback on' "$r" '--no-sandbox'
|
|
522
|
+
r="$(session fb-nosandbox-env "$PROXY" "${CFB[@]}" --env "$NOCHROME" --env PLAYWRIGHT_MCP_SANDBOX=1 --send "$INIT" --end eof --timeout 3)"
|
|
523
|
+
assert_refused_start 'FR16: PLAYWRIGHT_MCP_SANDBOX in the env still refuses to start with the fallback on' "$r" 'PLAYWRIGHT_MCP_SANDBOX'
|
|
524
|
+
session fb-udd "$PROXY" "${CFB[@]}" "${UDD[@]}" --env "$NOCHROME" --env "XDG_CACHE_HOME=$WORK/xdg" --env FAKE_PW_ARGV_OUT="$WORK/fb-udd" --send "$INIT" --end eof >/dev/null
|
|
525
|
+
assert_true 'FR16: with a profile flag too, the appended flags arrive as --browser chromium --sandbox, --user-data-dir=<dir>, --snapshot-mode none, in that order' python3 "$FBCHK" "$WORK/fb-udd" udd-order "$WORK/xdg/soleur-test-profile"
|
|
526
|
+
out="$(session pingenv "$PROXY" --env PLAYWRIGHT_MCP_PING_TIMEOUT_MS=0 --env FAKE_PW_ENV_ECHO=1 --send "$INIT" --end eof)"
|
|
527
|
+
assert_true 'FR17: PLAYWRIGHT_MCP_PING_TIMEOUT_MS=0 reaches the server child (the proxy has no child-env allowlist and it is not a refused sink)' bash -c 'started "$1" && stderr_has "$1" "env PLAYWRIGHT_MCP_PING_TIMEOUT_MS=0"' _ "$out"
|
|
528
|
+
out="$(session pingenv-unset "$PROXY" --env FAKE_PW_ENV_ECHO=1 --send "$INIT" --end eof)"
|
|
529
|
+
assert_true 'FR17 control: the stub reports <unset> when the variable is absent (it logs what it received, not a constant)' bash -c 'started "$1" && stderr_has "$1" "PLAYWRIGHT_MCP_PING_TIMEOUT_MS=<unset>"' _ "$out"
|
|
530
|
+
|
|
531
|
+
# ---------------------------------------------------------------------------
|
|
532
|
+
# FR5 — refuse to start (no child spawned)
|
|
533
|
+
# ---------------------------------------------------------------------------
|
|
534
|
+
NOSIB="$WORK/nosib"; mkdir -p "$NOSIB"; cp "$PROXY" "$NOSIB/proxy.py"
|
|
535
|
+
r="$(session nosib "$NOSIB/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
536
|
+
assert_refused_start 'FR5: proxy beside no redactor refuses to start' "$r" 'missing'
|
|
537
|
+
r="$(session savesess "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --save-session)"
|
|
538
|
+
assert_refused_start 'FR5: --save-session in the wrapped argv refuses to start' "$r" 'save-session'
|
|
539
|
+
printf '{"saveSession": true}\n' > "$WORK/cfg-save.json"
|
|
540
|
+
r="$(session savecfg "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config "$WORK/cfg-save.json")"
|
|
541
|
+
assert_refused_start 'FR5: config saveSession (via --config) refuses to start' "$r" 'saveSession'
|
|
542
|
+
r="$(session savecfgenv "$PROXY" --env PLAYWRIGHT_MCP_CONFIG="$WORK/cfg-save.json" --send "$INIT" --end eof --timeout 3)"
|
|
543
|
+
assert_refused_start 'FR5: config saveSession (via $PLAYWRIGHT_MCP_CONFIG) refuses to start' "$r" 'saveSession'
|
|
544
|
+
printf 'saveSession = true\n' > "$WORK/cfg-save.ini"
|
|
545
|
+
r="$(session saveini "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-save.ini")"
|
|
546
|
+
assert_refused_start 'FR5: a config the proxy cannot parse as JSON (the server reads INI) refuses to start' "$r" 'not JSON'
|
|
547
|
+
r="$(session cfgmissing "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/nope.json")"
|
|
548
|
+
assert_refused_start 'FR5: a --config that does not exist refuses to start with a named reason' "$r" 'does not exist'
|
|
549
|
+
printf '{"saveVideo": {"width": 800, "height": 600}}\n' > "$WORK/cfg-video.json"
|
|
550
|
+
r="$(session savevideo "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-video.json")"
|
|
551
|
+
assert_refused_start 'FR5: config saveVideo refuses to start' "$r" 'saveVideo'
|
|
552
|
+
printf '{"capabilities": ["devtools"]}\n' > "$WORK/cfg-caps.json"
|
|
553
|
+
r="$(session cfgcaps "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-caps.json")"
|
|
554
|
+
assert_refused_start 'FR5: config capabilities other than vision refuses to start' "$r" 'capabilities'
|
|
555
|
+
printf '{"server": {"port": 8931}}\n' > "$WORK/cfg-port.json"
|
|
556
|
+
r="$(session cfgport "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-port.json")"
|
|
557
|
+
assert_refused_start 'FR5: config server.port refuses to start' "$r" 'server.port'
|
|
558
|
+
r="$(session port "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --port 8931)"
|
|
559
|
+
assert_refused_start 'FR5: --port (HTTP transport around the relay) refuses to start' "$r" '--port'
|
|
560
|
+
r="$(session portenv "$PROXY" --env PLAYWRIGHT_MCP_PORT=8931 --send "$INIT" --end eof --timeout 3)"
|
|
561
|
+
assert_refused_start 'FR5: PLAYWRIGHT_MCP_PORT refuses to start' "$r" '--port'
|
|
562
|
+
r="$(session host "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --host=0.0.0.0)"
|
|
563
|
+
assert_refused_start 'FR5: --host refuses to start' "$r" '--host'
|
|
564
|
+
r="$(session caps "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --caps=vision,devtools)"
|
|
565
|
+
assert_refused_start 'FR5: --caps with devtools refuses to start' "$r" '--caps'
|
|
566
|
+
r="$(session capsenv "$PROXY" --env PLAYWRIGHT_MCP_CAPS=storage --send "$INIT" --end eof --timeout 3)"
|
|
567
|
+
assert_refused_start 'FR5: PLAYWRIGHT_MCP_CAPS=storage refuses to start' "$r" '--caps'
|
|
568
|
+
r="$(session outmode "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --output-mode file)"
|
|
569
|
+
assert_refused_start 'FR5: --output-mode file refuses to start' "$r" 'output-mode'
|
|
570
|
+
printf '{"snapshot": {"mode": "full"}, "capabilities": ["vision"]}\n' > "$WORK/cfg-ok.json"
|
|
571
|
+
r="$(session cfgok "$PROXY" --send "$INIT" --send "$SNAP" --end eof --server python3 "$STUB" --config="$WORK/cfg-ok.json" --caps vision)"
|
|
572
|
+
assert_redacted_result 'FR5 companion: a benign JSON --config=<file> and --caps vision start and serve' "$r" 3 1
|
|
573
|
+
for dbg in 'pw:mcp:server:response' '*' 'pw:*' 'pw:api *' '*:response' 'express:router, *' 'p*'; do
|
|
574
|
+
r="$(session "debug-$(printf '%s' "$dbg" | md5sum | cut -c1-8)" "$PROXY" --env "DEBUG=$dbg" --send "$INIT" --end eof --timeout 3)"
|
|
575
|
+
assert_refused_start "FR5: DEBUG='$dbg' (enables a pw:* logger) refuses to start" "$r" 'DEBUG'
|
|
576
|
+
done
|
|
577
|
+
assert_true 'FR5: the DEBUG reason never quotes the value' bash -c 'stderr_has "$1" "refusing to start" && ! grep -qF "p*" "$1/stderr.txt"' _ "$r"
|
|
578
|
+
r="$(session debugfile "$PROXY" --env DEBUG_FILE="$WORK/dbg" --send "$INIT" --end eof --timeout 3)"
|
|
579
|
+
assert_refused_start 'FR5: DEBUG_FILE refuses to start' "$r" 'DEBUG_FILE'
|
|
580
|
+
r="$(session debugother "$PROXY" --env 'DEBUG=express:router,-pw:*' --send "$INIT" --send "$SNAP" --end eof)"
|
|
581
|
+
assert_redacted_result 'FR5 companion: an unrelated DEBUG namespace (plus a pw exclusion) starts and serves' "$r" 3 1
|
|
582
|
+
# FR5 (sink table): every argv flag that re-opens a raw sink, swaps the browser
|
|
583
|
+
# the proxy launched, or injects page material — in both spellings, plus the
|
|
584
|
+
# PLAYWRIGHT_MCP_* env twin (the child inherits the session env wholesale).
|
|
585
|
+
for pair in \
|
|
586
|
+
'--storage-state:PLAYWRIGHT_MCP_STORAGE_STATE' \
|
|
587
|
+
'--secrets:PLAYWRIGHT_MCP_SECRETS_FILE' \
|
|
588
|
+
'--output-dir:PLAYWRIGHT_MCP_OUTPUT_DIR' \
|
|
589
|
+
'--init-script:PLAYWRIGHT_MCP_INIT_SCRIPT' \
|
|
590
|
+
'--init-page:PLAYWRIGHT_MCP_INIT_PAGE' \
|
|
591
|
+
'--cdp-endpoint:PLAYWRIGHT_MCP_CDP_ENDPOINT' \
|
|
592
|
+
'--endpoint:PLAYWRIGHT_MCP_ENDPOINT' \
|
|
593
|
+
'--extension:PLAYWRIGHT_MCP_EXTENSION' \
|
|
594
|
+
'--executable-path:PLAYWRIGHT_MCP_EXECUTABLE_PATH' \
|
|
595
|
+
'--grant-permissions:PLAYWRIGHT_MCP_GRANT_PERMISSIONS' \
|
|
596
|
+
'--save-trace:PLAYWRIGHT_MCP_SAVE_TRACE' \
|
|
597
|
+
'--save-video:PLAYWRIGHT_MCP_SAVE_VIDEO' \
|
|
598
|
+
'--ignore-https-errors:PLAYWRIGHT_MCP_IGNORE_HTTPS_ERRORS' \
|
|
599
|
+
'--no-sandbox:PLAYWRIGHT_MCP_SANDBOX' \
|
|
600
|
+
'--daemon:PLAYWRIGHT_MCP_DAEMON'; do
|
|
601
|
+
flag="${pair%%:*}"; envvar="${pair##*:}"
|
|
602
|
+
r="$(session "sink-$flag" "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" "$flag=/tmp/x")"
|
|
603
|
+
assert_refused_start "FR5: $flag=<v> in the wrapped argv refuses to start" "$r" "$flag"
|
|
604
|
+
r="$(session "sinkenv-${envvar#PLAYWRIGHT_MCP_}" "$PROXY" --env "$envvar=/tmp/x" --send "$INIT" --end eof --timeout 3)"
|
|
605
|
+
assert_refused_start "FR5: $envvar refuses to start" "$r" "$envvar"
|
|
606
|
+
done
|
|
607
|
+
r="$(session sink-aufa "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --allow-unrestricted-file-access)"
|
|
608
|
+
assert_refused_start 'FR5: bare --allow-unrestricted-file-access refuses to start' "$r" 'allow-unrestricted-file-access'
|
|
609
|
+
r="$(session sinkenv-aufa "$PROXY" --env PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS=1 --send "$INIT" --end eof --timeout 3)"
|
|
610
|
+
assert_refused_start 'FR5: PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS refuses to start' "$r" 'PLAYWRIGHT_MCP_ALLOW_UNRESTRICTED_FILE_ACCESS'
|
|
611
|
+
r="$(session sink-savesess-eq "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --save-session=/x)"
|
|
612
|
+
assert_refused_start 'FR5: --save-session=<v> (the = spelling) refuses to start' "$r" 'save-session'
|
|
613
|
+
r="$(session sinkenv-savesess "$PROXY" --env PLAYWRIGHT_MCP_SAVE_SESSION=1 --send "$INIT" --end eof --timeout 3)"
|
|
614
|
+
assert_refused_start 'FR5: PLAYWRIGHT_MCP_SAVE_SESSION refuses to start' "$r" 'PLAYWRIGHT_MCP_SAVE_SESSION'
|
|
615
|
+
r="$(session sinkenv-udd "$PROXY" --env PLAYWRIGHT_MCP_USER_DATA_DIR=/tmp/p --send "$INIT" --end eof --timeout 3)"
|
|
616
|
+
assert_refused_start 'FR5: PLAYWRIGHT_MCP_USER_DATA_DIR refuses to start (the env form of an audited argv setting)' "$r" 'PLAYWRIGHT_MCP_USER_DATA_DIR'
|
|
617
|
+
r="$(session sinkenv-snap "$PROXY" --env PLAYWRIGHT_MCP_SNAPSHOT_MODE=full --send "$INIT" --end eof --timeout 3)"
|
|
618
|
+
assert_refused_start 'FR5: PLAYWRIGHT_MCP_SNAPSHOT_MODE refuses to start (the proxy owns the flag)' "$r" 'PLAYWRIGHT_MCP_SNAPSHOT_MODE'
|
|
619
|
+
# a bare valued flag as the LAST server arg would bind an appended
|
|
620
|
+
# `--user-data-dir=`/`--snapshot-mode` token as its value — refused, whatever
|
|
621
|
+
# the flag
|
|
622
|
+
r="$(session trailcfg "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config)"
|
|
623
|
+
assert_refused_start 'FR5: a trailing --config would swallow an appended flag into its value — refuses' "$r" 'expects a value'
|
|
624
|
+
r="$(session trailua "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --user-agent)"
|
|
625
|
+
assert_refused_start 'FR5: a trailing --user-agent (valued, not a sink) refuses the same way' "$r" 'expects a value'
|
|
626
|
+
r="$(session midua "$PROXY" --send "$INIT" --send "$SNAP" --end eof --server python3 "$STUB" --user-agent suite-ua)"
|
|
627
|
+
assert_redacted_result 'FR5 companion: a valued flag with its value mid-argv starts and serves' "$r" 3 1
|
|
628
|
+
# the config-file forms of the same sinks (audited argv stays allowed)
|
|
629
|
+
printf '{"saveTrace": true}\n' > "$WORK/cfg-trace.json"
|
|
630
|
+
r="$(session cfgtrace "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-trace.json")"
|
|
631
|
+
assert_refused_start 'FR5: config saveTrace refuses to start' "$r" 'saveTrace'
|
|
632
|
+
printf '{"secrets": "/tmp/s.json"}\n' > "$WORK/cfg-secrets.json"
|
|
633
|
+
r="$(session cfgsecrets "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-secrets.json")"
|
|
634
|
+
assert_refused_start 'FR5: config secrets refuses to start' "$r" 'secrets'
|
|
635
|
+
printf '{"outputDir": "/tmp/o"}\n' > "$WORK/cfg-outdir.json"
|
|
636
|
+
r="$(session cfgoutdir "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-outdir.json")"
|
|
637
|
+
assert_refused_start 'FR5: config outputDir refuses to start' "$r" 'outputDir'
|
|
638
|
+
printf '{"allowUnrestrictedFileAccess": true}\n' > "$WORK/cfg-aufa.json"
|
|
639
|
+
r="$(session cfgaufa "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-aufa.json")"
|
|
640
|
+
assert_refused_start 'FR5: config allowUnrestrictedFileAccess refuses to start' "$r" 'allowUnrestrictedFileAccess'
|
|
641
|
+
printf '{"extension": true}\n' > "$WORK/cfg-ext.json"
|
|
642
|
+
r="$(session cfgext "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-ext.json")"
|
|
643
|
+
assert_refused_start 'FR5: config extension refuses to start' "$r" 'extension'
|
|
644
|
+
printf '{"browser": {"cdpEndpoint": "ws://127.0.0.1:1"}}\n' > "$WORK/cfg-cdp.json"
|
|
645
|
+
r="$(session cfgcdp "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-cdp.json")"
|
|
646
|
+
assert_refused_start 'FR5: config browser.cdpEndpoint refuses to start' "$r" 'cdpEndpoint'
|
|
647
|
+
printf '{"browser": {"initScript": "/tmp/x.js"}}\n' > "$WORK/cfg-initscript.json"
|
|
648
|
+
r="$(session cfginitscript "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-initscript.json")"
|
|
649
|
+
assert_refused_start 'FR5: config browser.initScript refuses to start' "$r" 'initScript'
|
|
650
|
+
printf '{"browser": {"launchOptions": {"executablePath": "/tmp/x"}}}\n' > "$WORK/cfg-execpath.json"
|
|
651
|
+
r="$(session cfgexecpath "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-execpath.json")"
|
|
652
|
+
assert_refused_start 'FR5: config browser.launchOptions.executablePath refuses to start' "$r" 'executablePath'
|
|
653
|
+
printf '{"browser": {"launchOptions": {"args": ["--remote-debugging-port=9222"]}}}\n' > "$WORK/cfg-rdbg.json"
|
|
654
|
+
r="$(session cfgrdbg "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-rdbg.json")"
|
|
655
|
+
assert_refused_start 'FR5: config launchOptions.args with a remote debugging flag refuses to start' "$r" 'remote debugging'
|
|
656
|
+
printf '{"browser": {"userDataDir": "/tmp/p"}}\n' > "$WORK/cfg-udd.json"
|
|
657
|
+
r="$(session cfgudd "$PROXY" "${UDD[@]}" --env "XDG_CACHE_HOME=$WORK/xdg" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-udd.json")"
|
|
658
|
+
assert_refused_start 'FR5: config browser.userDataDir conflicts with --user-data-dir-name' "$r" 'userDataDir'
|
|
659
|
+
r="$(session cfgudd-ok "$PROXY" --send "$INIT" --send "$SNAP" --end eof --server python3 "$STUB" --config="$WORK/cfg-udd.json")"
|
|
660
|
+
assert_redacted_result 'FR5 companion: config browser.userDataDir WITHOUT the profile flag is the audited form — starts and serves' "$r" 3 1
|
|
661
|
+
printf '{"browser": {"contextOptions": {"storageState": "/tmp/s.json"}}}\n' > "$WORK/cfg-ctxstorage.json"
|
|
662
|
+
r="$(session cfgctxstorage "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-ctxstorage.json")"
|
|
663
|
+
assert_refused_start 'FR5: config browser.contextOptions.storageState refuses to start' "$r" 'storageState'
|
|
664
|
+
printf '{"browser": {"contextOptions": {"permissions": ["clipboard-read"]}}}\n' > "$WORK/cfg-ctxperms.json"
|
|
665
|
+
r="$(session cfgctxperms "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-ctxperms.json")"
|
|
666
|
+
assert_refused_start 'FR5: config browser.contextOptions.permissions refuses to start' "$r" 'permissions'
|
|
667
|
+
printf '{"browser": {"contextOptions": {"ignoreHTTPSErrors": true}}}\n' > "$WORK/cfg-ctxhttps.json"
|
|
668
|
+
r="$(session cfgctxhttps "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-ctxhttps.json")"
|
|
669
|
+
assert_refused_start 'FR5: config browser.contextOptions.ignoreHTTPSErrors refuses to start' "$r" 'ignoreHTTPSErrors'
|
|
670
|
+
printf '{"browser": {"launchOptions": {"chromiumSandbox": false}}}\n' > "$WORK/cfg-sandbox.json"
|
|
671
|
+
r="$(session cfgsandbox "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-sandbox.json")"
|
|
672
|
+
assert_refused_start 'FR5: config browser.launchOptions.chromiumSandbox refuses to start' "$r" 'chromiumSandbox'
|
|
673
|
+
r="$(session traildaemon "$PROXY" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --daemon)"
|
|
674
|
+
assert_refused_start 'FR5: a trailing --daemon (hidden valued option) would swallow an appended flag — refuses' "$r" 'expects a value'
|
|
675
|
+
usage_rc=0; python3 "$PROXY" -- </dev/null >"$WORK/usage.out" 2>"$WORK/usage.err" || usage_rc=$?
|
|
676
|
+
assert_true 'B1: empty server argv → exit 2, usage on stderr, nothing on stdout' bash -c '[[ "$1" == "2" ]] && grep -qF "refusing to start: usage:" "$2" && [[ ! -s "$3" ]]' _ "$usage_rc" "$WORK/usage.err" "$WORK/usage.out"
|
|
677
|
+
usage_rc=0; python3 "$PROXY" python3 "$STUB" </dev/null >"$WORK/usage2.out" 2>"$WORK/usage2.err" || usage_rc=$?
|
|
678
|
+
assert_true 'B1: missing -- separator → exit 2 (the server argv is never guessed)' bash -c '[[ "$1" == "2" ]] && grep -qF "no \`--\` separator" "$2"' _ "$usage_rc" "$WORK/usage2.err"
|
|
679
|
+
|
|
680
|
+
# ---------------------------------------------------------------------------
|
|
681
|
+
# FR6 — redactor variants make the proxy refuse (self-test), FR7 — raising predicate
|
|
682
|
+
# ---------------------------------------------------------------------------
|
|
683
|
+
mk_variant() { # <dir> <proxy> <python-replace-old> <python-replace-new>
|
|
684
|
+
mkdir -p "$1"; cp "$2" "$1/proxy.py"
|
|
685
|
+
python3 - "$REDACTOR" "$1/redact-a11y-snapshot.py" "$3" "$4" <<'PY'
|
|
686
|
+
import sys
|
|
687
|
+
src = open(sys.argv[1]).read(); old, new = sys.argv[3], sys.argv[4].encode().decode("unicode_escape")
|
|
688
|
+
assert src.count(old) == 1, (old, src.count(old))
|
|
689
|
+
open(sys.argv[2], "w").write(src.replace(old, new))
|
|
690
|
+
PY
|
|
691
|
+
}
|
|
692
|
+
mk_variant "$WORK/v-identity" "$PROXY" 'def redact_text(text: str) -> str:' 'def redact_text(text: str) -> str:\n return text\n\ndef _unused_redact_text(text: str) -> str:'
|
|
693
|
+
r="$(session v-identity "$WORK/v-identity/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
694
|
+
assert_refused_start 'FR6: a redactor returning its input unchanged → refuse to start' "$r" 'self-test'
|
|
695
|
+
mk_variant "$WORK/v-append" "$PROXY" 'def redact_text(text: str) -> str:' 'def redact_text(text: str) -> str:\n return text + " <redacted>"\n\ndef _unused_redact_text(text: str) -> str:'
|
|
696
|
+
r="$(session v-append "$WORK/v-append/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
697
|
+
assert_refused_start 'FR6/row 9 input: a redactor appending <redacted> without removing the value → refuse' "$r" 'self-test'
|
|
698
|
+
mk_variant "$WORK/v-false" "$PROXY" 'def looks_like_a11y_tree(value: str) -> bool:' 'def looks_like_a11y_tree(value: str) -> bool:\n return False\n\ndef _unused_looks(value: str) -> bool:'
|
|
699
|
+
r="$(session v-false "$WORK/v-false/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
700
|
+
assert_refused_start 'FR6/row 29 input: constant-False looks_like_a11y_tree → refuse' "$r" 'looks_like_a11y_tree'
|
|
701
|
+
# raises only for long text, so the startup self-test passes (row 34's input)
|
|
702
|
+
mk_variant "$WORK/v-raise" "$PROXY" 'def redact_text(text: str) -> str:' 'def redact_text(text: str) -> str:\n if len(text) > 200:\n raise RuntimeError("boom")\n return _orig_redact_text(text)\n\ndef _orig_redact_text(text: str) -> str:'
|
|
703
|
+
r="$(session v-raise "$WORK/v-raise/proxy.py" --send "$INIT" --send "$SNAP" --end eof)"
|
|
704
|
+
assert_withheld 'FR7: redact_text raising → withheld, no sentinel, session continues' "$r" 3 'raised'
|
|
705
|
+
assert_true 'FR7: the withhold says the action may already have run' text_has "$r" 3 'its result was withheld, not its action'
|
|
706
|
+
assert_stderr_marker 'FR7b: one withheld line for the raise' "$r" 'withheld tool=browser_snapshot' 1
|
|
707
|
+
assert_rc 'FR7: the session ended cleanly after the withhold' "$r" 0
|
|
708
|
+
# the four-name contract: a redactor missing one name → refuse, not a per-result withhold
|
|
709
|
+
mk_variant "$WORK/v-rename" "$PROXY" 'def main() -> None:' 'del looks_like_a11y_tree\n\n\ndef main() -> None:'
|
|
710
|
+
r="$(session v-rename "$WORK/v-rename/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
711
|
+
assert_refused_start 'B2: a redactor missing one of the four names → refuse (not a per-result withhold)' "$r" 'does not export looks_like_a11y_tree'
|
|
712
|
+
# Startup refusal (slice 1 of #10331): the startup handlers catch (Exception, SystemExit, KeyboardInterrupt), not BaseException. Each excluded
|
|
713
|
+
# class must still refuse with rc 2 and the unchanged reason line, at every member: the import, the redact_text call and the
|
|
714
|
+
# looks_like_a11y_tree call in the self-test (a case for one member does not cover the other).
|
|
715
|
+
for cls in SystemExit KeyboardInterrupt; do
|
|
716
|
+
mk_variant "$WORK/v-load-$cls" "$PROXY" 'def main() -> None:' "raise $cls()\n\n\ndef main() -> None:"
|
|
717
|
+
r="$(session "v-load-$cls" "$WORK/v-load-$cls/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
718
|
+
assert_refused_start "G1: a redactor raising $cls at import → refuse to start (rc 2), class named" "$r" "cannot load redact-a11y-snapshot.py: $cls"
|
|
719
|
+
done
|
|
720
|
+
mk_variant "$WORK/v-st-redact" "$PROXY" 'def redact_text(text: str) -> str:' 'def redact_text(text: str) -> str:\n raise KeyboardInterrupt()\n\ndef _unused_redact_text(text: str) -> str:'
|
|
721
|
+
r="$(session v-st-redact "$WORK/v-st-redact/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
722
|
+
assert_refused_start 'G1: redact_text raising KeyboardInterrupt in the self-test → refuse (rc 2), class named' "$r" 'redactor self-test raised KeyboardInterrupt'
|
|
723
|
+
mk_variant "$WORK/v-st-redact-exit" "$PROXY" 'def redact_text(text: str) -> str:' 'def redact_text(text: str) -> str:\n raise SystemExit(3)\n\ndef _unused_redact_text(text: str) -> str:'
|
|
724
|
+
r="$(session v-st-redact-exit "$WORK/v-st-redact-exit/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
725
|
+
assert_refused_start 'G1: redact_text raising SystemExit in the self-test → refuse (rc 2), class named' "$r" 'redactor self-test raised SystemExit'
|
|
726
|
+
mk_variant "$WORK/v-st-looks" "$PROXY" 'def looks_like_a11y_tree(value: str) -> bool:' 'def looks_like_a11y_tree(value: str) -> bool:\n raise KeyboardInterrupt()\n\ndef _unused_looks(value: str) -> bool:'
|
|
727
|
+
r="$(session v-st-looks "$WORK/v-st-looks/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
728
|
+
assert_refused_start 'G1: looks_like_a11y_tree raising KeyboardInterrupt in the self-test → refuse (rc 2), class named' "$r" 'looks_like_a11y_tree raised KeyboardInterrupt'
|
|
729
|
+
mk_variant "$WORK/v-st-looks-exit" "$PROXY" 'def looks_like_a11y_tree(value: str) -> bool:' 'def looks_like_a11y_tree(value: str) -> bool:\n raise SystemExit(3)\n\ndef _unused_looks(value: str) -> bool:'
|
|
730
|
+
r="$(session v-st-looks-exit "$WORK/v-st-looks-exit/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
731
|
+
assert_refused_start 'G1: looks_like_a11y_tree raising SystemExit in the self-test → refuse (rc 2), class named' "$r" 'looks_like_a11y_tree raised SystemExit'
|
|
732
|
+
# The narrowing is deliberate: a BaseException subclass outside (Exception, SystemExit, KeyboardInterrupt) is not caught, so the proxy
|
|
733
|
+
# dies with rc 1 and a traceback. Still fail-closed (no stdout line, no child) -- pinned so widening back to BaseException (rc 2) is a decision.
|
|
734
|
+
mk_variant "$WORK/v-load-outside" "$PROXY" 'def main() -> None:' "class _OutsideBase(BaseException):\n pass\n\n\nraise _OutsideBase()\n\n\ndef main() -> None:"
|
|
735
|
+
r="$(session v-load-outside "$WORK/v-load-outside/proxy.py" --send "$INIT" --end eof --timeout 3)"
|
|
736
|
+
assert_true 'G1: a BaseException subclass outside the narrowed tuple at import → rc 1, nothing relayed, no child spawned' bash -c '[[ "$(rcof "$1")" == "1" && "$(fact "$1" 0 lines)" == "0" && -z "$(cat "$1/child_pgid" 2>/dev/null)" ]] && stderr_has "$1" "_OutsideBase"' _ "$r"
|
|
737
|
+
# FR13 behavioural: a SPY redactor tags every string it is handed. Every text block
|
|
738
|
+
# the proxy delivers must carry the tag — a proxy-side predicate that skips some
|
|
739
|
+
# text (a literal, a tool-name allowlist, any spelling of either) cannot hide.
|
|
740
|
+
mk_variant "$WORK/v-spy" "$PROXY" 'def redact_text(text: str) -> str:' 'def redact_text(text: str) -> str:\n return _orig_redact_text(text) + "\\n[[SPY]]"\n\ndef _orig_redact_text(text: str) -> str:'
|
|
741
|
+
spy_check() { # <out> — every non-trailer text block of ids 3,5,6,7 ends with the spy tag
|
|
742
|
+
python3 - "$1" <<'PY'
|
|
743
|
+
import json, sys
|
|
744
|
+
seen = 0
|
|
745
|
+
for o in json.load(open(sys.argv[1] + "/responses.json")):
|
|
746
|
+
if isinstance(o, dict) and o.get("id") in (3, 5, 6, 7) and isinstance(o.get("result"), dict):
|
|
747
|
+
for b in o["result"]["content"]:
|
|
748
|
+
if b.get("type") == "text" and not b["text"].startswith("[Soleur:"):
|
|
749
|
+
seen += 1
|
|
750
|
+
if not b["text"].endswith("[[SPY]]"):
|
|
751
|
+
sys.exit(1)
|
|
752
|
+
sys.exit(0 if seen >= 4 else 2)
|
|
753
|
+
PY
|
|
754
|
+
}
|
|
755
|
+
export -f spy_check
|
|
756
|
+
r="$(session v-spy "$WORK/v-spy/proxy.py" --send "$INIT" --send "$SNAP" --send "$NAV" --send "$FIND" --send "$EVAL" --end eof)"
|
|
757
|
+
assert_true 'FR13 behavioural: every delivered text block passed through redact_text (spy)' spy_check "$r"
|
|
758
|
+
|
|
759
|
+
# ---------------------------------------------------------------------------
|
|
760
|
+
# FR8/FR9/FR11b — odd shapes through the shipped proxy
|
|
761
|
+
# ---------------------------------------------------------------------------
|
|
762
|
+
odd() { session "odd-$1" "$PROXY" --env FAKE_PW_RESULT_FILE="$ODD/$1.json" "${@:2}" --send "$INIT" --send "$SNAP" --end eof; }
|
|
763
|
+
r="$(odd two-text-blocks)"; assert_redacted_result 'row 2 input: two text blocks both redacted (second-member)' "$r" 3 2
|
|
764
|
+
r="$(odd link-result)"
|
|
765
|
+
assert_true 'FR9: a - [Snapshot]( file link is replaced by the do-not-read notice, result delivered' bash -c 'delivered_ok "$1" 3 && text_has "$1" 3 "[Snapshot withheld by playwright-mcp-redact-proxy: the server wrote a raw tree file"' _ "$r"
|
|
766
|
+
assert_true 'FR9: the server-emitted path is echoed nowhere (stdout, stderr)' bash -c 'stderr_has "$1" "replaced a snapshot file link" && ! grep -qaF "page-2026" "$1/stdout.bin" && ! grep -qF "page-2026" "$1/stderr.txt"' _ "$r"
|
|
767
|
+
r="$(odd dialog-link-injection)"
|
|
768
|
+
assert_redacted_result 'FR9: a page-controlled dialog line shaped like the link cannot withhold the result' "$r" 3 1
|
|
769
|
+
r="$(odd structured-content)"; assert_withheld 'FR11b: structuredContent → withheld' "$r" 3 'unrecognised result shape'
|
|
770
|
+
r="$(odd resource-block)"; assert_withheld 'FR11b: resource block → withheld' "$r" 3 'unrecognised result shape'
|
|
771
|
+
r="$(odd meta-in-result)"; assert_withheld 'FR11b: _meta in the result → withheld' "$r" 3 'unrecognised result shape'
|
|
772
|
+
r="$(odd result-and-method)"; assert_withheld 'FR11b: result AND method → withheld' "$r" 3 'unrecognised result shape'
|
|
773
|
+
r="$(odd no-content)"; assert_withheld 'FR11b: no content key → withheld' "$r" 3 'unrecognised result shape'
|
|
774
|
+
r="$(odd result-string)"; assert_withheld 'FR11b: result is a string → withheld' "$r" 3 'unrecognised result shape'
|
|
775
|
+
for f in evaluate-escaped-object; do
|
|
776
|
+
r="$(odd "$f")"
|
|
777
|
+
assert_true "PR2: non-JSON candidate lines ($f) emit no swallowed marker (characterization)" bash -c 'stderr_has "$1" "child pgid" && ! stderr_has "$1" "swallowed"' _ "$r"
|
|
778
|
+
done
|
|
779
|
+
r="$(odd empty-content)"; assert_byte_identical 'edge: content: [] forwarded unchanged' "$r" 3 "$ODD/empty-content.json"
|
|
780
|
+
r="$(odd error-with-data)"; assert_withheld 'row 31: error with structured data → withheld' "$r" 3 'unrecognised shape'
|
|
781
|
+
r="$(odd error-and-result)"; assert_withheld 'row 31: an error frame that also carries a result → withheld' "$r" 3 'unrecognised shape'
|
|
782
|
+
r="$(odd error-string)"; assert_withheld 'row 31: a non-object error → withheld' "$r" 3 'unrecognised shape'
|
|
783
|
+
r="$(odd error-message-tree)"; assert_withheld 'row 46: an error message carrying tree rows → withheld' "$r" 3 'tree-shaped'
|
|
784
|
+
r="$(odd error-no-data)"; assert_true 'row 31 companion: a data-less prose error forwarded byte-identical' bash -c '[[ "$(fact "$1" 3 rawline | base64 -d)" == "{\"jsonrpc\":\"2.0\",\"id\":3,\"error\":{\"code\":-32000,\"message\":\"boom\"}}" ]]' _ "$r"
|
|
785
|
+
r="$(odd error-code-string)"; assert_withheld 'B9: a non-integer error code → withheld' "$r" 3 'unrecognised shape'
|
|
786
|
+
r="$(odd error-code-bool)"; assert_withheld 'B9: a boolean error code → withheld (bool is not an int code)' "$r" 3 'unrecognised shape'
|
|
787
|
+
# the error frame is REBUILT from {id, code, message}: a key hung on the
|
|
788
|
+
# top-level frame never reaches the client even when it carries a sentinel
|
|
789
|
+
python3 - "$WORK/error-top-extra.json" <<'PY'
|
|
790
|
+
import base64, json, sys
|
|
791
|
+
line = '{"jsonrpc":"2.0","id":{id},"error":{"code":-32000,"message":"boom"},"stolen":"ZZQP-SENTINEL-7980"}'
|
|
792
|
+
json.dump({"raw_b64": base64.b64encode(line.encode()).decode()}, open(sys.argv[1], "w"))
|
|
793
|
+
PY
|
|
794
|
+
r="$(session errtopextra "$PROXY" --env FAKE_PW_RESULT_FILE="$WORK/error-top-extra.json" --send "$INIT" --send "$SNAP" --end eof)"
|
|
795
|
+
assert_true 'B9: error frame rebuilt — a stolen top-level key never reaches the client' bash -c '[[ "$(fact "$1" 3 found)" == "1" && "$(fact "$1" 3 jsonrpc_error)" == "1" ]] && ! stdout_has "$1" stolen && ! stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"
|
|
796
|
+
# vet_other_result: a matched non-tools/call result forwards only when no
|
|
797
|
+
# string anywhere in the frame is tree-shaped or redactable (the
|
|
798
|
+
# resources/read-class bypass the result-shape whitelist)
|
|
799
|
+
INITRES="$WORK/initres"; mkdir -p "$INITRES"; cp "$CAPTURES/"*.json "$INITRES/"
|
|
800
|
+
python3 - "$INITRES/initialize.json" <<'PY'
|
|
801
|
+
import json, sys
|
|
802
|
+
d = json.load(open(sys.argv[1]))
|
|
803
|
+
d["result"]["instructions"] = '- textbox "Token" [ref=e1]: ZZQP-SENTINEL-7980\n- button "Go" [ref=e2]'
|
|
804
|
+
json.dump(d, open(sys.argv[1], "w"))
|
|
805
|
+
PY
|
|
806
|
+
r="$(session initres "$PROXY" --env FAKE_PW_FIXTURE_DIR="$INITRES" --send "$INIT" --end eof)"
|
|
807
|
+
assert_withheld 'B9: a tree string inside an initialize result is withheld, not forwarded' "$r" 1 'non-tools/call'
|
|
808
|
+
PING='{"jsonrpc":"2.0","id":40,"method":"ping"}'
|
|
809
|
+
r="$(session ping "$PROXY" --send "$INIT" --send "$PING" --end eof)"
|
|
810
|
+
assert_true 'B9: a clean non-tools/call result (ping) is forwarded' delivered_ok "$r" 40
|
|
811
|
+
printf '{"result": {"data": "- textbox \\"Token\\" [ref=e1]: ZZQP-SENTINEL-7980\\n- button \\"Go\\" [ref=e2]"}}\n' > "$WORK/ping-tree.json"
|
|
812
|
+
r="$(session pingtree "$PROXY" --env FAKE_PW_PING_RESULT_FILE="$WORK/ping-tree.json" --send "$INIT" --send "$PING" --end eof)"
|
|
813
|
+
assert_withheld 'B9: a tree in a ping result (a method the proxy does not know) is withheld' "$r" 40 'non-tools/call'
|
|
814
|
+
# log-scrub: client- and server-controlled strings reaching stderr can never
|
|
815
|
+
# forge a line in the persisted MCP logs
|
|
816
|
+
r="$(session logsan "$PROXY" --send "$INIT" --send '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"browser_snapshot\nFORGED-LINE","arguments":{"_meta":{}}}}' --end eof)"
|
|
817
|
+
assert_withheld 'log-scrub: the _meta refusal is still delivered for a newline-bearing tool name' "$r" 12 '_meta'
|
|
818
|
+
assert_true 'log-scrub: a newline in the tool name is scrubbed to one stderr line, never forging a second' bash -c 'stderr_has "$1" "refused tool=browser_snapshot FORGED-LINE" && ! grep -q "^FORGED-LINE" "$1/stderr.txt"' _ "$r"
|
|
819
|
+
INITSAN="$WORK/initsan"; mkdir -p "$INITSAN"; cp "$CAPTURES/"*.json "$INITSAN/"
|
|
820
|
+
python3 - "$INITSAN/initialize.json" <<'PY'
|
|
821
|
+
import json, sys
|
|
822
|
+
d = json.load(open(sys.argv[1]))
|
|
823
|
+
d["result"]["serverInfo"]["name"] = "pw\nFORGED-INIT"
|
|
824
|
+
json.dump(d, open(sys.argv[1], "w"))
|
|
825
|
+
PY
|
|
826
|
+
r="$(session initsan "$PROXY" --env FAKE_PW_FIXTURE_DIR="$INITSAN" --send "$INIT" --send "$SNAP" --end eof)"
|
|
827
|
+
assert_true 'log-scrub: a newline-bearing serverInfo name cannot forge a stderr line either' bash -c 'stderr_has "$1" "wrapping pw FORGED-INIT" && ! grep -q "^FORGED-INIT" "$1/stderr.txt"' _ "$r"
|
|
828
|
+
assert_redacted_result 'log-scrub: the session still serves after the scrubbed serverInfo' "$r" 3 1
|
|
829
|
+
r="$(odd run-code-escaped)"; assert_withheld 'row 49: a JSON-escaped tree (browser_run_code_unsafe shape) → withheld' "$r" 3 'JSON-escaped'
|
|
830
|
+
r="$(odd evaluate-escaped-object)"; assert_withheld 'row 49: a tree inside a pretty-printed JSON object → withheld' "$r" 3 'JSON-escaped'
|
|
831
|
+
r="$(odd run-code-escaped-one-row)"; assert_withheld 'row 56: a ONE-ROW escaped tree (single-locator ariaSnapshot) → withheld' "$r" 3 'JSON-escaped'
|
|
832
|
+
r="$(odd quoted-key)"; assert_redacted_result 'quoted YAML key ("API Key #1") redacted through the proxy; "Notes #2" intact' "$r" 3 1
|
|
833
|
+
r="$(odd value-contains-link-substring)"; assert_true 'row 36: a VALUE containing the link substring outside ### Snapshot is redacted, not replaced' bash -c 'delivered_ok "$1" 3 && text_has "$1" 3 "\"Token\" [ref=e1]: <redacted>"' _ "$r"
|
|
834
|
+
r="$(odd value-contains-brace)"; assert_redacted_result 'P6 canary: a value containing { is delivered redacted (no envelope sniff)' "$r" 3 1
|
|
835
|
+
r="$(odd tree-no-credential)"; assert_true 'row 18: a tree with no credential-named field still gets the trailer' bash -c 'delivered_ok "$1" 3 && [[ "$(fact "$1" 3 trailer)" == "1" ]]' _ "$r"
|
|
836
|
+
r="$(odd invalid-utf8)"; assert_redacted_result 'row 14: one invalid UTF-8 byte in the text → decoded with replacement, still redacted' "$r" 3 1
|
|
837
|
+
r="$(odd unparsable-line)"; assert_true 'edge: unparsable server line dropped with a stderr line; session continues' bash -c 'stderr_has "$1" "dropped unparsable server line" && [[ "$(cat "$1/rc")" == "0" ]]' _ "$r"
|
|
838
|
+
r="$(session odd-list "$PROXY" --env FAKE_PW_RESULT_FILE="$ODD/list-two-results.json" --send "$INIT" --send "$SNAP" --send "$SNAP4" --end eof)"
|
|
839
|
+
assert_withheld 'row 21: list-shaped server line — first pending id answered by error_result' "$r" 3 'list line'
|
|
840
|
+
assert_withheld 'row 21: list-shaped server line — second pending id answered' "$r" 4 'list line'
|
|
841
|
+
assert_true 'row 21: the list line itself was not forwarded' bash -c 'stderr_has "$1" "dropped list line from server" && ! grep -qa "^\[" "$1/stdout.bin"' _ "$r"
|
|
842
|
+
# tools/list without a tools key: fed from a scratch capture dir
|
|
843
|
+
NOTOOLS="$WORK/notools"
|
|
844
|
+
assert_fixture_dir "$NOTOOLS"
|
|
845
|
+
mkdir -p "$NOTOOLS"
|
|
846
|
+
assert_fixture_dir "$CAPTURES"
|
|
847
|
+
cp "$CAPTURES/initialize.json" "$CAPTURES/snapshot.json" "$NOTOOLS/"
|
|
848
|
+
cp "$ODD/tools-list-no-tools.json" "$NOTOOLS/tools-list.json"
|
|
849
|
+
r="$(session notools "$PROXY" --env FAKE_PW_FIXTURE_DIR="$NOTOOLS" --send "$INIT" --send "$LIST" --send "$SNAP" --end eof)"
|
|
850
|
+
assert_byte_identical 'FR10/row 25: tools/list without a tools key forwarded unchanged (fail-safe)' "$r" 2 "$ODD/tools-list-no-tools.json"
|
|
851
|
+
assert_stderr_marker 'FR10/row 25: annotate failure noted on stderr' "$r" 'annotate failed' 1
|
|
852
|
+
assert_redacted_result 'FR10/row 25: the session continues after the annotate failure' "$r" 3 1
|
|
853
|
+
# an initialize answered with a content-bearing tree: routed by SHAPE, not by method
|
|
854
|
+
INITTREE="$WORK/inittree"; mkdir -p "$INITTREE"; cp "$CAPTURES/snapshot.json" "$INITTREE/initialize.json"; cp "$CAPTURES/snapshot.json" "$INITTREE/"
|
|
855
|
+
r="$(session inittree "$PROXY" --env FAKE_PW_FIXTURE_DIR="$INITTREE" --send "$INIT" --end eof)"
|
|
856
|
+
assert_redacted_result 'row 48: a content-bearing result answering initialize is rewritten (routed by shape)' "$r" 1 1
|
|
857
|
+
# unknown-id response dropped (row 23 shipped side)
|
|
858
|
+
r="$(session unprompted "$PROXY" --env FAKE_PW_UNPROMPTED=1 --send "$INIT" --send "$SNAP" --end eof)"
|
|
859
|
+
assert_true 'row 23: an unprompted result (id never sent) is dropped; no sentinel anywhere on stdout' bash -c 'delivered_ok "$1" 3 && [[ "$(fact "$1" 999 found)" == "0" ]] && ! stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"
|
|
860
|
+
assert_stderr_marker 'row 23: dropped unknown-id response noted' "$r" 'dropped unknown-id response' 1
|
|
861
|
+
# server notifications and requests outside the relayed set
|
|
862
|
+
r="$(session notify "$PROXY" --env FAKE_PW_NOTIFY=1 --send "$INIT" --send "$SNAP" --end eof)"
|
|
863
|
+
assert_true 'row 44: a notifications/message carrying a tree is dropped; the result still redacted' bash -c 'delivered_ok "$1" 3 && stderr_has "$1" "dropped a server notification" && ! stdout_has "$1" ZZQP-SENTINEL-7980 && ! stdout_has "$1" notifications/message' _ "$r"
|
|
864
|
+
r="$(session srvreq "$PROXY" --env FAKE_PW_SERVER_REQUEST=1 --env FAKE_PW_REQUEST_LOG="$WORK/srvreq-req" --send "$INIT" --send "$SNAP" --end eof)"
|
|
865
|
+
assert_true 'row 45: a sampling/createMessage server request is dropped, never reaching the client' bash -c 'delivered_ok "$1" 3 && stderr_has "$1" "dropped a server request" && ! stdout_has "$1" sampling/createMessage && ! stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"
|
|
866
|
+
assert_true 'row 45: the dropped server request is answered with an error on the server side' grep -qF '"response_id": "srv-1", "error": true' "$WORK/srvreq-req"
|
|
867
|
+
r="$(session cancel "$PROXY" --env FAKE_PW_CANCEL=1 --send "$INIT" --send "$SNAP" --end eof)"
|
|
868
|
+
assert_true 'row 54: relayed-set notifications are rebuilt — method and plain requestId arrive, reason/_meta/tree requestId do not' bash -c 'delivered_ok "$1" 3 && stdout_has "$1" "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/cancelled\",\"params\":{\"requestId\":3}}" && stdout_has "$1" "{\"jsonrpc\":\"2.0\",\"method\":\"notifications/tools/list_changed\"}" && stderr_has "$1" "dropped a cancellation whose requestId is not a plain id" && ! stdout_has "$1" ZZQP-SENTINEL-7980 && ! stdout_has "$1" _meta' _ "$r"
|
|
869
|
+
# FR8 — over the cap
|
|
870
|
+
python3 - "$WORK/big.json" "$SENTINEL" "$BENIGN" <<'PY'
|
|
871
|
+
import json, sys
|
|
872
|
+
big = f'- textbox "Token" [ref=e1]: {sys.argv[2]}\n' + (f'- textbox "Notes" [ref=e2]: {sys.argv[3]}\n' * (4 * 1024 * 1024 // 44 + 2))
|
|
873
|
+
assert len(big.encode()) > 4 * 1024 * 1024
|
|
874
|
+
json.dump({"result": {"content": [{"type": "text", "text": big}]}}, open(sys.argv[1], "w"))
|
|
875
|
+
PY
|
|
876
|
+
r="$(session big "$PROXY" --env FAKE_PW_RESULT_FILE="$WORK/big.json" --send "$INIT" --send "$SNAP" --end eof --timeout 20)"
|
|
877
|
+
assert_withheld 'FR8: text over MAX_INPUT_BYTES → withheld' "$r" 3 'size cap'
|
|
878
|
+
assert_stderr_marker 'FR8: withheld line on stderr' "$r" 'withheld tool=browser_snapshot' 1
|
|
879
|
+
# row 30 — ids keyed by their JSON text
|
|
880
|
+
r="$(session idtype "$PROXY" --send "$INIT" --send "$LIST" --send '{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}' --end eof)"
|
|
881
|
+
assert_true 'row 30: id 2 (tools/list) and id "2" (tools/call) are classified by their own requests' bash -c '[[ "$(fact "$1" 2 description:browser_snapshot)" == *"redacted in flight"* ]] && delivered_ok "$1" "\"2\"" && [[ "$(fact "$1" "\"2\"" sentinel)" == "0" ]]' _ "$r"
|
|
882
|
+
# row 47 — a request reusing a pending id is refused, never overwrites the entry
|
|
883
|
+
r="$(session reuse "$PROXY" --send "$INIT" --send "$SNAP" --send '{"jsonrpc":"2.0","id":3,"method":"tools/list"}' --end eof)"
|
|
884
|
+
assert_true 'row 47: a request reusing a pending id is refused; the original result is still redacted' bash -c '[[ "$(fact "$1" 3 count)" == "2" && "$(fact "$1" 3 jsonrpc_error)" == "1" ]] && ! stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"
|
|
885
|
+
assert_stderr_marker 'row 47: the reuse refusal is noted' "$r" 'refused a request reusing a pending id' 1
|
|
886
|
+
# an id of any JSON shape is keyed; one odd line never strands the lines behind it
|
|
887
|
+
r="$(session listid "$PROXY" --send "$INIT" --send '{"jsonrpc":"2.0","id":[1],"method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}' --send "$SNAP" --end eof)"
|
|
888
|
+
assert_true 'edge: a list-shaped id is keyed and answered, and the next request is served' bash -c 'delivered_ok "$1" "[1]" && delivered_ok "$1" 3 && ! stderr_has "$1" "pump error"' _ "$r"
|
|
889
|
+
# row 26 — unparsable client lines are dropped, never forwarded unvetted
|
|
890
|
+
bigint="$(python3 -c 'print("9" * 5000)')"
|
|
891
|
+
r="$(session badclient "$PROXY" --env FAKE_PW_REQUEST_LOG="$WORK/badclient-req" --send "$INIT" --send 'not json' \
|
|
892
|
+
--send "{\"jsonrpc\":\"2.0\",\"id\":10,\"method\":\"tools/call\",\"params\":{\"name\":\"browser_snapshot\",\"arguments\":{\"filename\":\"$WORK/badclient.yml\",\"_meta\":{\"n\":$bigint}}}}" \
|
|
893
|
+
--send "$SNAP" --end eof)"
|
|
894
|
+
assert_stderr_marker 'row 26: two unparsable client lines (text; an integer past the digit limit) dropped with a note' "$r" 'dropped unparsable client line' 2
|
|
895
|
+
assert_true 'row 26: neither reached the stub, which did receive the session; no raw file' bash -c 'grep -qF "\"method\": \"initialize\"" "$1" && ! grep -qF filename "$1" && [[ ! -e "$2" ]]' _ "$WORK/badclient-req" "$WORK/badclient.yml"
|
|
896
|
+
assert_redacted_result 'row 26: the next request is still relayed' "$r" 3 1
|
|
897
|
+
# row 38 — client list line dropped
|
|
898
|
+
r="$(session listclient "$PROXY" --send "$INIT" --send '[{"jsonrpc":"2.0","id":40,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}]' --send "$SNAP" --end eof)"
|
|
899
|
+
assert_stderr_marker 'row 38: client list line dropped with a stderr note' "$r" 'dropped list line from client' 1
|
|
900
|
+
assert_redacted_result 'row 38: the bare call after the dropped list line is still served' "$r" 3 1
|
|
901
|
+
# row 37 — a 65 MiB line: one clear MiB past MAX_LINE_BYTES (a line of exactly 64 MiB parses and is caught by the 4 MiB cap instead)
|
|
902
|
+
r="$(session oversize "$PROXY" --env FAKE_PW_OVERSIZE=$((65*1024*1024)) --send "$INIT" --send "$SNAP" --end eof --timeout 40)"
|
|
903
|
+
assert_withheld 'row 37: a line over MAX_LINE_BYTES is discarded and the pending call answered oversize' "$r" 3 'oversize'
|
|
904
|
+
assert_stderr_marker 'row 37: discard noted with the byte count only' "$r" 'discarding oversize server line' 1
|
|
905
|
+
|
|
906
|
+
# ---------------------------------------------------------------------------
|
|
907
|
+
# FR14 — lifecycle against the stub's pgid (HOLD and GRANDCHILD model Chrome)
|
|
908
|
+
# ---------------------------------------------------------------------------
|
|
909
|
+
r="$(session hold-eof "$PROXY" --env FAKE_PW_HOLD=1 --send "$INIT" --end eof)"
|
|
910
|
+
assert_group_empty 'FR14/row 12: stdin EOF with a holding child → group empty (SIGKILL after grace)' "$r"
|
|
911
|
+
assert_stderr_marker 'FR14: SIGKILL-after-grace line' "$r" 'survived SIGTERM; SIGKILL sent' 1
|
|
912
|
+
assert_stderr_marker 'PR2: the SIGTERM-ignoring child needing SIGKILL emits no swallowed marker (characterization)' "$r" 'swallowed' 0
|
|
913
|
+
r="$(session grandchild "$PROXY" --env FAKE_PW_GRANDCHILD=1 --send "$INIT" --end eof)"
|
|
914
|
+
assert_group_empty 'FR14/row 50: direct child exits, a SIGTERM-ignoring grandchild holds → group still emptied' "$r"
|
|
915
|
+
assert_stderr_marker 'FR14/row 50: the grandchild needed the SIGKILL' "$r" 'survived SIGTERM; SIGKILL sent' 1
|
|
916
|
+
assert_stderr_marker 'PR2: the grandchild needing SIGKILL emits no swallowed marker (characterization)' "$r" 'swallowed' 0
|
|
917
|
+
r="$(session hold-term "$PROXY" --env FAKE_PW_HOLD=1 --send "$INIT" --end sigterm)"
|
|
918
|
+
assert_group_empty 'FR14/row 27: SIGTERM to the proxy → group empty' "$r"
|
|
919
|
+
r="$(session exit3 "$PROXY" --env FAKE_PW_EXIT_CODE=3 --send "$INIT" --send "$SNAP" --end none --timeout 5)"
|
|
920
|
+
assert_rc 'FR14/row 28: stub exit 3 → proxy exit 3' "$r" 3
|
|
921
|
+
assert_stderr_marker 'FR14: child exit logged' "$r" 'child exited rc=3' 1
|
|
922
|
+
r="$(session killchild "$PROXY" --send "$INIT" --end killchild --timeout 5)"
|
|
923
|
+
assert_rc 'FR14/row 28: stub group killed by SIGTERM → proxy exit 143 (clamped), never 241 or 0' "$r" 143
|
|
924
|
+
assert_stderr_marker 'FR14: the signal is named on stderr' "$r" 'child exited rc=-15 signal=15' 1
|
|
925
|
+
|
|
926
|
+
# ---------------------------------------------------------------------------
|
|
927
|
+
# FR13 / NFR1 / NFR3 — structure, with the redactor as oracle
|
|
928
|
+
# ---------------------------------------------------------------------------
|
|
929
|
+
fr13_literals() { # <proxy> — exit 0 when no credential-shaped literal sits outside self_test's `row =` or the refusal tables
|
|
930
|
+
python3 - "$1" "$REDACTOR" <<'PY'
|
|
931
|
+
import ast, importlib.util, sys
|
|
932
|
+
t = ast.parse(open(sys.argv[1]).read())
|
|
933
|
+
spec = importlib.util.spec_from_file_location("r", sys.argv[2]); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m)
|
|
934
|
+
doc = ast.get_docstring(t, clean=False)
|
|
935
|
+
funcs = {n.name: n for n in ast.walk(t) if isinstance(n, ast.FunctionDef)}
|
|
936
|
+
st = funcs["self_test"]
|
|
937
|
+
rows = [a.value for a in ast.walk(st) if isinstance(a, ast.Assign) and any(isinstance(x, ast.Name) and x.id == "row" for x in a.targets)]
|
|
938
|
+
exempt = {id(c) for r in rows for c in ast.walk(r)}
|
|
939
|
+
# The refusal surface legitimately NAMES the settings it refuses ("secrets",
|
|
940
|
+
# "--secrets", PLAYWRIGHT_MCP_SECRETS_FILE): those constants are labels and
|
|
941
|
+
# setting names, never payload text, so the sink tables and the whole
|
|
942
|
+
# refuse_argv_and_env body are exempt by the same mechanism as self_test's row.
|
|
943
|
+
tables = [n for n in ast.walk(t) if isinstance(n, ast.Assign) and any(isinstance(x, ast.Name) and x.id in ("SINK_FLAGS", "SINK_ENV_ONLY", "VALUE_FLAGS") for x in n.targets)]
|
|
944
|
+
exempt |= {id(c) for a in tables for c in ast.walk(a) if isinstance(c, ast.Constant)}
|
|
945
|
+
if funcs.get("refuse_argv_and_env") is not None:
|
|
946
|
+
exempt |= {id(c) for c in ast.walk(funcs["refuse_argv_and_env"]) if isinstance(c, ast.Constant)}
|
|
947
|
+
consts = [n for n in ast.walk(t) if isinstance(n, ast.Constant) and isinstance(n.value, str)]
|
|
948
|
+
bad = [n.value for n in consts if n.value != doc and id(n) not in exempt and m._is_credential_name(n.value)]
|
|
949
|
+
shaped_row = [c.value for r in rows for c in ast.walk(r) if isinstance(c, ast.Constant) and isinstance(c.value, str) and m._is_credential_name(c.value)]
|
|
950
|
+
sys.exit(0 if not bad and shaped_row else 1)
|
|
951
|
+
PY
|
|
952
|
+
}
|
|
953
|
+
assert_true 'FR13: no re.* call, no def redact_text, exactly one spec_from_file_location' python3 - "$PROXY" <<'PY'
|
|
954
|
+
import ast, sys
|
|
955
|
+
t = ast.parse(open(sys.argv[1]).read())
|
|
956
|
+
re_calls = [n for n in ast.walk(t) if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and isinstance(n.func.value, ast.Name) and n.func.value.id == "re"]
|
|
957
|
+
defs = [n.name for n in ast.walk(t) if isinstance(n, ast.FunctionDef)]
|
|
958
|
+
sffl = sum(1 for n in ast.walk(t) if isinstance(n, ast.Call) and isinstance(n.func, ast.Attribute) and n.func.attr == "spec_from_file_location")
|
|
959
|
+
sys.exit(0 if not re_calls and "redact_text" not in defs and sffl == 1 else 1)
|
|
960
|
+
PY
|
|
961
|
+
assert_true 'FR13: the only credential-shaped literal is the self-test sentinel row' fr13_literals "$PROXY"
|
|
962
|
+
assert_true 'NFR1/NFR3: stdlib-only imports, no threading' python3 - "$PROXY" <<'PY'
|
|
963
|
+
import ast, sys
|
|
964
|
+
t = ast.parse(open(sys.argv[1]).read())
|
|
965
|
+
mods = {(n.names[0].name if isinstance(n, ast.Import) else n.module).split(".")[0] for n in ast.walk(t) if isinstance(n, (ast.Import, ast.ImportFrom))}
|
|
966
|
+
allowed = {"__future__", "contextlib", "importlib", "json", "os", "selectors", "signal", "subprocess", "sys", "time", "typing"}
|
|
967
|
+
sys.exit(0 if mods <= allowed and "threading" not in mods else 1)
|
|
968
|
+
PY
|
|
969
|
+
|
|
970
|
+
# ---------------------------------------------------------------------------
|
|
971
|
+
# P6 — parity with the CLI on every sentinel-bearing tree fixture (P-ONE)
|
|
972
|
+
# ---------------------------------------------------------------------------
|
|
973
|
+
parity_n=0
|
|
974
|
+
for f in "$CAPTURES"/*.json "$ODD"/*.json; do
|
|
975
|
+
b="$(basename "$f" .json)"
|
|
976
|
+
[[ "$b" == "snapshot-meta-json" ]] && continue # the CLI's JSON arm redacts {"snapshot": …}; in-process redact_text does not — refused upstream by B5
|
|
977
|
+
text="$(python3 -c 'import json,sys; o=json.load(open(sys.argv[1])); r=o.get("result"); c=r.get("content") if isinstance(r,dict) else None; print("\n".join(b.get("text","") for b in c if isinstance(b,dict) and b.get("type")=="text") if isinstance(c,list) else "")' "$f" 2>/dev/null || true)"
|
|
978
|
+
[[ "$text" == *"$SENTINEL"* ]] || continue
|
|
979
|
+
grep -q '^- \[Snapshot\](' <<<"$text" && continue # the drift arm rewrites these lines by design (FR9)
|
|
980
|
+
python3 -c 'import importlib.util,sys; s=importlib.util.spec_from_file_location("r",sys.argv[1]); m=importlib.util.module_from_spec(s); s.loader.exec_module(m); sys.exit(0 if m.looks_like_a11y_tree(sys.stdin.read()) else 1)' "$REDACTOR" <<<"$text" || continue
|
|
981
|
+
p="$(session "par-$b" "$PROXY" --env FAKE_PW_RESULT_FILE="$f" --send "$INIT" --send "$SNAP" --end eof)"
|
|
982
|
+
[[ "$(fact "$p" 3 isError)" == "true" ]] && continue # withheld shapes are asserted by their own rows above
|
|
983
|
+
parity_n=$((parity_n + 1))
|
|
984
|
+
cases=$((cases + 1))
|
|
985
|
+
if python3 - "$FACTS" "$p" "$REDACTOR" "$f" <<'PYPAR'
|
|
986
|
+
import json, subprocess, sys
|
|
987
|
+
facts, out, redactor, fixture = sys.argv[1:]
|
|
988
|
+
got = subprocess.run([sys.executable, facts, out, "3", "text"], capture_output=True, text=True).stdout
|
|
989
|
+
trailer = "\n[Soleur: redacted in flight by playwright-mcp-redact-proxy]"
|
|
990
|
+
if not got.endswith(trailer): sys.exit(2)
|
|
991
|
+
got = got[: -len(trailer)]
|
|
992
|
+
text = "\n".join(b.get("text", "") for b in json.load(open(fixture))["result"]["content"] if isinstance(b, dict) and b.get("type") == "text")
|
|
993
|
+
cli = subprocess.run([sys.executable, redactor], input=text.encode(), capture_output=True)
|
|
994
|
+
if cli.returncode != 0: sys.exit(3)
|
|
995
|
+
sys.exit(0 if got.rstrip("\n") == cli.stdout.decode().rstrip("\n") else 1)
|
|
996
|
+
PYPAR
|
|
997
|
+
then ok "P6 parity: proxy output == CLI output on $b"; else bad "P6 parity: proxy output differs from the CLI on $b (2 = no trailer, 3 = CLI failed)"; fi
|
|
998
|
+
done
|
|
999
|
+
assert_true "P6 parity population floor: $parity_n sentinel-bearing tree fixtures (>= 5)" test "$parity_n" -ge 5
|
|
1000
|
+
|
|
1001
|
+
# ---------------------------------------------------------------------------
|
|
1002
|
+
# Guard 1 mutation matrix — standalone copies of PROXY_SHIPPED, one edit each
|
|
1003
|
+
# ---------------------------------------------------------------------------
|
|
1004
|
+
beside() { # <dir> <redactor-variant-dir> — copy the current mutant next to a variant redactor
|
|
1005
|
+
mkdir -p "$1"; cp "$MUTANT_PATH" "$1/proxy.py"; cp "$2/redact-a11y-snapshot.py" "$1/"
|
|
1006
|
+
}
|
|
1007
|
+
RD='new_text = self.redact_text(relinked)'
|
|
1008
|
+
mutant 01-no-redact rewrite_result "$RD" 'new_text = relinked'
|
|
1009
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m01 "$MUTANT_PATH" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 1: redaction skipped → sentinel in stdout' leaks "$r" 3; fi
|
|
1010
|
+
mutant 02-first-block-only rewrite_result "$RD" 'new_text = self.redact_text(relinked) if block is content[0] else relinked'
|
|
1011
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m02 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/two-text-blocks.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 2: only content[0] processed → second block leaks' leaks "$r" 3; fi
|
|
1012
|
+
mutant 03-dispatch pump_client_to_server 'if "method" in req and "id" in req:' 'if req.get("method") == "tools/cal" and "id" in req:'
|
|
1013
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m03 "$MUTANT_PATH" --send "$INIT" --send "$SNAP" --end eof --timeout 3)"; red 'row 3: dispatch broken → the redacted result is never delivered (own-dispatch)' bash -c 'started "$1" && [[ "$(fact "$1" 3 found)" == "0" ]]' _ "$r"; fi
|
|
1014
|
+
mutant 05-swallow-load load_redactor ' refuse_start(f"cannot load {REDACTOR_BASENAME}: {type(exc).__name__}")' ' return (lambda t: t.replace("ZZQP-SENTINEL-7980", "<redacted>"), lambda t: t.startswith("- "), 4194304, "<redacted>")'
|
|
1015
|
+
if [[ -n "$MUTANT_PATH" ]]; then BROKEN="$WORK/broken"; mkdir -p "$BROKEN"; cp "$MUTANT_PATH" "$BROKEN/proxy.py"; printf 'def (\n' > "$BROKEN/redact-a11y-snapshot.py"; r="$(session m05 "$BROKEN/proxy.py" --send "$INIT" --end eof --timeout 3)"; red 'row 5: load error swallowed → proxy starts and answers initialize' bash -c 'started "$1" && [[ "$(fact "$1" 1 found)" == "1" ]]' _ "$r"; fi
|
|
1016
|
+
mutant 06-no-flag __init__ 'argv += ["--snapshot-mode", "none"]' 'pass'
|
|
1017
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m06 "$MUTANT_PATH" --env FAKE_PW_ARGV_OUT="$WORK/m06-argv" --send "$INIT" --end eof >/dev/null; red 'row 6: flag not appended → the stub ran and its argv lacks it' bash -c 'test -f "$1" && ! grep -qx -- "--snapshot-mode" "$1"' _ "$WORK/m06-argv"; fi
|
|
1018
|
+
mutant 07-forward-filename refuse_request 'if name == "browser_snapshot" and "filename" in args:' 'if False and "filename" in args:'
|
|
1019
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m07 "$MUTANT_PATH" --env FAKE_PW_REQUEST_LOG="$WORK/m07-req" --send "$INIT" --send "$(FILENAME_REQ 10 "$WORK/m07-raw.yml")" --end eof >/dev/null; red 'row 7: filename forwarded → stub writes the raw file and logs the call' bash -c 'test -s "$1" && grep -qF filename "$2"' _ "$WORK/m07-raw.yml" "$WORK/m07-req"; fi
|
|
1020
|
+
mutant 08-no-link-arm rewrite_result 'elif in_snapshot and tline.startswith("- [Snapshot]("):' 'elif False:'
|
|
1021
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m08 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/link-result.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 8: link arm removed → the raw-file path reaches the client' bash -c 'delivered_ok "$1" 3 && grep -qaF "page-2026" "$1/stdout.bin"' _ "$r"; fi
|
|
1022
|
+
mutant 09-weak-selftest self_test 'if redacted not in out or "ZZQP-SENTINEL-7980" in out:' 'if redacted not in out:'
|
|
1023
|
+
if [[ -n "$MUTANT_PATH" ]]; then beside "$WORK/m09" "$WORK/v-append"; r="$(session m09 "$WORK/m09/proxy.py" --send "$INIT" --end eof --timeout 3)"; red 'row 9: self-test checks only <redacted> present → append-only redactor starts the proxy' started "$r"; fi
|
|
1024
|
+
mutant 10-own-predicate rewrite_result "$RD" 'new_text = self.redact_text(relinked) if "password" in relinked else relinked'
|
|
1025
|
+
if [[ -n "$MUTANT_PATH" ]]; then beside "$WORK/m10" "$WORK/v-spy"; r="$(session m10 "$WORK/m10/proxy.py" --send "$INIT" --send "$SNAP" --send "$NAV" --send "$FIND" --send "$EVAL" --end eof)"; red 'row 10: a proxy-side predicate skips some text → the spy tag is missing' bash -c 'started "$1" && ! spy_check "$1"' _ "$r"; fi
|
|
1026
|
+
mutant 11-no-annotate pump_server_to_client ' write_line(self.out, self.annotate_tools_list(msg, line))' ' write_line(self.out, line)'
|
|
1027
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m11 "$MUTANT_PATH" --send "$INIT" --send "$LIST" --end eof)"; red 'row 11: annotate skipped → tools/list lacks the marker' bash -c '[[ "$(fact "$1" 2 found)" == "1" ]] && ! stdout_has "$1" "redacted in flight"' _ "$r"; fi
|
|
1028
|
+
mutant 12-no-eof-teardown run 'self.teardown("stdin EOF" if side == "client" else "child stdout EOF")' 'os._exit(0)'
|
|
1029
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m12 "$MUTANT_PATH" --env FAKE_PW_HOLD=1 --send "$INIT" --end eof --timeout 4)"; red 'row 12: EOF teardown removed → holding child survives' bash -c 'started "$1" && [[ "$(cat "$1/group_after")" -gt 0 ]]' _ "$r"; reap_group "$r"; fi
|
|
1030
|
+
mutant 13-classify-pending-only pump_server_to_client ' if "result" not in msg and "error" not in msg:' ' if False:'
|
|
1031
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m13 "$MUTANT_PATH" --env FAKE_PW_ROOTS_COLLIDE=1 --send "$INIT" --send "$SNAP" --end eof --timeout 3)"; red 'row 13: responses keyed on pending alone → a colliding roots/list breaks delivery' bash -c 'started "$1" && ! delivered_ok "$1" 3' _ "$r"; fi
|
|
1032
|
+
mutant 14-strict-decoder pump_server_to_client 'msg = json.loads(line.decode("utf-8", errors="replace"))' 'msg = json.loads(line.decode("utf-8", errors="strict"))'
|
|
1033
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m14 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/invalid-utf8.json" --send "$INIT" --send "$SNAP" --end eof --timeout 3)"; red 'row 14: strict decoder → the invalid byte drops or leaks the result' bash -c 'started "$1" && { [[ "$(fact "$1" 3 found)" == "0" ]] || leaks "$1" 3; }' _ "$r"; fi
|
|
1034
|
+
mutant 15-no-refuse-argv refuse_argv_and_env ' if flag_present(server, flag):' ' if False:'
|
|
1035
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m15 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --save-session)"; red 'row 15: the argv sink-flag check removed → --save-session starts' started "$r"; reap_group "$r"; fi
|
|
1036
|
+
mutant 16-wide-whitelist rewrite_result 'if not isinstance(result, dict) or not set(result.keys()) <= RESULT_KEYS:' 'if not isinstance(result, dict):'
|
|
1037
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m16 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/structured-content.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 16: whitelist widened → structuredContent tree leaks' leaks "$r" 3; fi
|
|
1038
|
+
mutant 17-resource-ignored rewrite_result ' else:
|
|
1039
|
+
return error_result(rid, tool, UNRECOGNISED)
|
|
1040
|
+
new_content.append(block)' ' else:
|
|
1041
|
+
pass
|
|
1042
|
+
new_content.append(block)'
|
|
1043
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m17 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/resource-block.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 17: resource block ignored → leaks' leaks "$r" 3; fi
|
|
1044
|
+
mutant 18-trailer-on-change rewrite_result 'if tree_seen:
|
|
1045
|
+
new_content.append' 'if tree_seen and changed:
|
|
1046
|
+
new_content.append'
|
|
1047
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m18 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/tree-no-credential.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 18: trailer only on change → credential-free tree lacks it' bash -c 'delivered_ok "$1" 3 && [[ "$(fact "$1" 3 trailer)" == "0" ]]' _ "$r"; fi
|
|
1048
|
+
mutant 19-filename-truthy refuse_request 'if name == "browser_snapshot" and "filename" in args:' 'if name == "browser_snapshot" and args.get("filename"):'
|
|
1049
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m19 "$MUTANT_PATH" --env FAKE_PW_REQUEST_LOG="$WORK/m19-req" --send "$INIT" --send '{"jsonrpc":"2.0","id":11,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{"filename":""}}}' --end eof >/dev/null; red 'row 19: filename keyed on truthiness → filename:"" reaches the stub' grep -qF '"filename": ""' "$WORK/m19-req"; fi
|
|
1050
|
+
mutant 20-no-meta-check refuse_request 'if "_meta" in args:' 'if False:'
|
|
1051
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m20 "$MUTANT_PATH" --env FAKE_PW_REQUEST_LOG="$WORK/m20-req" --send "$INIT" --send '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{"_meta":{"json":true}}}}' --end eof >/dev/null; red 'row 20: _meta not refused → the undocumented hook reaches the server' grep -qF '_meta' "$WORK/m20-req"; fi
|
|
1052
|
+
mutant 21-list-unhandled pump_server_to_client 'if isinstance(msg, list):' 'if False:'
|
|
1053
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m21 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/list-two-results.json" --send "$INIT" --send "$SNAP" --send "$SNAP4" --end eof --timeout 3)"; red 'row 21: list line unhandled → pending ids left hanging' bash -c 'started "$1" && [[ "$(fact "$1" 3 found)" == "0" ]]' _ "$r"; fi
|
|
1054
|
+
mutant 22a-handbuilt-refusal refuse_request ' return error_result(req.get("id"), name, "filename writes the raw tree to disk; call browser_snapshot with no filename", refused=True)' ' return json.dumps({"jsonrpc": "2.0", "id": req.get("id"), "result": {"content": [{"type": "text", "text": "refused"}]}}).encode()'
|
|
1055
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m22a "$MUTANT_PATH" --send "$INIT" --send "$(FILENAME_REQ 10 x)" --end eof)"; red 'row 22a: a refusal arm bypassing error_result → no isError, no proxy token' bash -c '[[ "$(fact "$1" 10 found)" == "1" && "$(fact "$1" 10 isError)" != "true" ]]' _ "$r"; fi
|
|
1056
|
+
mutant 22b-error-result-field error_result '"isError": True}}' '}}'
|
|
1057
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m22b "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/structured-content.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 22b: a field dropped from error_result → every withhold row reds at once' bash -c '[[ "$(fact "$1" 3 found)" == "1" && "$(fact "$1" 3 isError)" != "true" ]]' _ "$r"; fi
|
|
1058
|
+
mutant 23-forward-unknown pump_server_to_client ' log(f"dropped unknown-id response ({len(line)} bytes)")
|
|
1059
|
+
return' ' write_line(self.out, line)
|
|
1060
|
+
return'
|
|
1061
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m23 "$MUTANT_PATH" --env FAKE_PW_UNPROMPTED=1 --send "$INIT" --send "$SNAP" --end eof)"; red 'row 23: unknown-id response forwarded raw → sentinel leaks' leaks "$r" 999; fi
|
|
1062
|
+
mutant 24-result-and-method pump_server_to_client ' if "method" in msg:
|
|
1063
|
+
write_line(self.out, error_result(rid, tool, UNRECOGNISED' ' if False:
|
|
1064
|
+
write_line(self.out, error_result(rid, tool, UNRECOGNISED'
|
|
1065
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m24 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/result-and-method.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 24: result+method treated as a normal result → not withheld' delivered_ok "$r" 3; fi
|
|
1066
|
+
mutant 25-annotate-unwrapped annotate_tools_list ' log(f"annotate failed: {type(exc).__name__}")
|
|
1067
|
+
return line' ' raise'
|
|
1068
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m25 "$MUTANT_PATH" --env FAKE_PW_FIXTURE_DIR="$NOTOOLS" --send "$INIT" --send "$LIST" --send "$SNAP" --end eof --timeout 3)"; red 'row 25: annotate not fail-safe → tools/list result lost' bash -c 'started "$1" && [[ "$(fact "$1" 2 found)" == "0" ]]' _ "$r"; fi
|
|
1069
|
+
mutant 26-forward-unparsable-client pump_client_to_server ' log(f"dropped unparsable client line ({len(line)} bytes)")
|
|
1070
|
+
return' ' write_line(self.child.stdin, line)
|
|
1071
|
+
return'
|
|
1072
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m26 "$MUTANT_PATH" --env FAKE_PW_REQUEST_LOG="$WORK/m26-req" --send "$INIT" --send 'not json' --send "$SNAP" --end eof)"; red 'row 26: unparsable client line forwarded → no drop note, and the server answered it' bash -c 'started "$1" && ! stderr_has "$1" "dropped unparsable client line" && stderr_has "$1" "dropped unknown-id response"' _ "$r"; fi
|
|
1073
|
+
mutant 27-no-sigterm-handler __init__ 'signal.signal(signal.SIGTERM, self.on_signal)' 'signal.signal(signal.SIGTERM, signal.SIG_DFL)'
|
|
1074
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m27 "$MUTANT_PATH" --env FAKE_PW_HOLD=1 --send "$INIT" --end sigterm --timeout 4)"; red 'row 27: SIGTERM handler removed → holding child survives' bash -c 'stderr_has "$1" "child pgid" && [[ "$(cat "$1/group_after")" -gt 0 ]]' _ "$r"; reap_group "$r"; fi
|
|
1075
|
+
mutant 28-no-clamp teardown 'code = rc if rc >= 0 else 128 - rc' 'code = rc'
|
|
1076
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m28 "$MUTANT_PATH" --send "$INIT" --end killchild --timeout 5)"; red 'row 28: clamp removed → proxy exits 241 (raw -15) instead of 143' bash -c '[[ "$(cat "$1/rc")" == "241" ]]' _ "$r"; fi
|
|
1077
|
+
mutant 29-weak-tree-selftest self_test 'if not tree_true or prose_false:' 'if False:'
|
|
1078
|
+
if [[ -n "$MUTANT_PATH" ]]; then beside "$WORK/m29" "$WORK/v-false"; r="$(session m29 "$WORK/m29/proxy.py" --send "$INIT" --send "$SNAP" --end eof --timeout 3)"; red 'row 29: tree self-test dropped → constant-False redactor starts and never appends the trailer' bash -c 'started "$1" && [[ "$(fact "$1" 3 trailer)" == "0" ]]' _ "$r"; fi
|
|
1079
|
+
mutant 30-bare-id id_key 'return json.dumps(rid, sort_keys=True)' 'return json.dumps(str(rid))'
|
|
1080
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m30 "$MUTANT_PATH" --send "$INIT" --send "$LIST" --send '{"jsonrpc":"2.0","id":"2","method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}' --end eof --timeout 3)"; red 'row 30: ids keyed by their string form → 2 and "2" collide' bash -c 'started "$1" && ! delivered_ok "$1" "\"2\""' _ "$r"; fi
|
|
1081
|
+
mutant 31-error-shape-forwarded vet_error 'if "result" in msg or not isinstance(err, dict) or not set(err.keys()) <= ERROR_KEYS:' 'if False:'
|
|
1082
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m31 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/error-with-data.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 31: the error-shape bound removed → the malformed frame is RELAYED (rebuilt, data dropped) instead of withheld' bash -c '[[ "$(fact "$1" 3 found)" == "1" && "$(fact "$1" 3 jsonrpc_error)" == "1" ]]' _ "$r"; fi
|
|
1083
|
+
mutant 32-no-cap rewrite_result 'if len(text.encode("utf-8", "surrogatepass")) > self.max_input_bytes:' 'if False:'
|
|
1084
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m32 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$WORK/big.json" --send "$INIT" --send "$SNAP" --end eof --timeout 30)"; red 'row 32: size cap removed → oversized text forwarded (not withheld)' delivered_ok "$r" 3; fi
|
|
1085
|
+
mutant 33-quotes-input rewrite_result 'return error_result(rid, tool, "result exceeds the redactor'"'"'s size cap")' 'return error_result(rid, tool, "cap: " + text[:100])'
|
|
1086
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m33 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$WORK/big.json" --send "$INIT" --send "$SNAP" --end eof --timeout 30)"; red 'row 33: reason quotes the input → sentinel in the isError text or stderr' bash -c 'started "$1" && { stdout_has "$1" ZZQP-SENTINEL-7980 || grep -qF ZZQP-SENTINEL-7980 "$1/stderr.txt"; }' _ "$r"; fi
|
|
1087
|
+
mutant 34-no-except-arm rewrite_result ' return error_result(rid, tool, f"redaction raised {type(exc).__name__}")' ' return line'
|
|
1088
|
+
if [[ -n "$MUTANT_PATH" ]]; then beside "$WORK/m34" "$WORK/v-raise"; r="$(session m34 "$WORK/m34/proxy.py" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 34: except arm forwards raw → raising redactor leaks' leaks "$r" 3; fi
|
|
1089
|
+
mutant 35-ignore-debug refuse_argv_and_env 'patterns = env.get("DEBUG", "").replace(",", " ").split()' 'patterns = []'
|
|
1090
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m35 "$MUTANT_PATH" --env DEBUG=pw:mcp:server:response --send "$INIT" --end eof --timeout 3)"; red 'row 35: DEBUG ignored → proxy starts under the response logger' started "$r"; fi
|
|
1091
|
+
mutant 36-substring-drift rewrite_result 'elif in_snapshot and tline.startswith("- [Snapshot]("):' 'elif "- [Snapshot](" in tline:'
|
|
1092
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m36 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/value-contains-link-substring.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 36: drift arm keyed on substring → a page value rewrites the credential row away' bash -c 'delivered_ok "$1" 3 && ! text_has "$1" 3 "\"Token\" [ref=e1]: <redacted>"' _ "$r"; fi
|
|
1093
|
+
mutant 37-no-line-cap run 'if len(buf) > MAX_LINE_BYTES and not discarding[side]:' 'if False:'
|
|
1094
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m37 "$MUTANT_PATH" --env FAKE_PW_OVERSIZE=$((65*1024*1024)) --send "$INIT" --send "$SNAP" --end eof --timeout 40)"; red 'row 37: line cap removed → the 65 MiB line is buffered whole (no discard, reason lacks oversize)' bash -c 'started "$1" && [[ "$(fact "$1" 3 found)" == "1" ]] && ! text_has "$1" 3 oversize && ! stderr_has "$1" "discarding oversize server line"' _ "$r"; fi
|
|
1095
|
+
mutant 38-forward-client-list pump_client_to_server ' if isinstance(req, list):
|
|
1096
|
+
log(f"dropped list line from client ({len(line)} bytes)")
|
|
1097
|
+
return' ' if False:
|
|
1098
|
+
return'
|
|
1099
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m38 "$MUTANT_PATH" --send "$INIT" --send '[{"jsonrpc":"2.0","id":40,"method":"tools/call","params":{"name":"browser_snapshot","arguments":{}}}]' --send "$SNAP" --end eof)"; red 'row 38: the client list arm removed → the drop note disappears' bash -c 'started "$1" && ! stderr_has "$1" "dropped list line from client"' _ "$r"; fi
|
|
1100
|
+
mutant 39-name-allowlist rewrite_result "$RD" 'new_text = self.redact_text(relinked) if tool == "browser_snapshot" else relinked'
|
|
1101
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m39 "$MUTANT_PATH" --send "$INIT" --send "$FIND" --end eof)"; red 'row 39: tool-name allowlist → browser_find leaks (Q1 by shape)' leaks "$r" 6; fi
|
|
1102
|
+
mutant 40-debug-wildcard debug_pattern_enables_pw ' if ch == "*":
|
|
1103
|
+
return True' ' if False:
|
|
1104
|
+
return True'
|
|
1105
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m40 "$MUTANT_PATH" --env 'DEBUG=*:response' --send "$INIT" --end eof --timeout 3)"; red 'row 40: debug wildcard not modelled → DEBUG=*:response starts' started "$r"; fi
|
|
1106
|
+
mutant 41-ini-config-skipped refuse_argv_and_env ' refuse_start("the config file is not JSON, so its raw-sink settings cannot be checked")' ' cfg = {}'
|
|
1107
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m41 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-save.ini")"; red 'row 41: a non-JSON config skipped → an INI saveSession starts' started "$r"; fi
|
|
1108
|
+
mutant 42-port refuse_argv_and_env 'if argv_values(server, "--port") or flag_present(server, "--port") or env.get("PLAYWRIGHT_MCP_PORT"):' 'if False:'
|
|
1109
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m42 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --port 8931)"; red 'row 42: --port check removed → proxy starts' started "$r"; fi
|
|
1110
|
+
mutant 43-caps refuse_argv_and_env 'if caps_open_sinks(argv_values(server, "--caps")) or caps_open_sinks([env.get("PLAYWRIGHT_MCP_CAPS", "")]):' 'if False:'
|
|
1111
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m43 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --caps=devtools)"; red 'row 43: --caps check removed → devtools starts' started "$r"; fi
|
|
1112
|
+
mutant 44-relay-notifications relay_server_message ' if method in PASSTHROUGH_NOTIFICATIONS:' ' if True:'
|
|
1113
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m44 "$MUTANT_PATH" --env FAKE_PW_NOTIFY=1 --send "$INIT" --send "$SNAP" --end eof)"; red 'row 44: every notification relayed → a notifications/message reaches the client' bash -c 'started "$1" && stdout_has "$1" notifications/message' _ "$r"; fi
|
|
1114
|
+
mutant 45-relay-requests relay_server_message ' if method in PASSTHROUGH_REQUESTS and self.plain_id(msg.get("id")):' ' if True:'
|
|
1115
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m45 "$MUTANT_PATH" --env FAKE_PW_SERVER_REQUEST=1 --send "$INIT" --send "$SNAP" --end eof)"; red 'row 45: every server request relayed → a sampling/createMessage reaches the client' bash -c 'started "$1" && stdout_has "$1" sampling/createMessage' _ "$r"; fi
|
|
1116
|
+
mutant 46-error-message-unchecked vet_error 'if not isinstance(message, str) or self.looks_like_a11y_tree(message) or self.redact_text(message) != message:' 'if False:'
|
|
1117
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m46 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/error-message-tree.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 46: error message not vetted → tree row leaks' bash -c 'started "$1" && stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"; fi
|
|
1118
|
+
mutant 47-reuse-overwrites pump_client_to_server ' if key in self.pending:' ' if False:'
|
|
1119
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m47 "$MUTANT_PATH" --send "$INIT" --send "$SNAP" --send '{"jsonrpc":"2.0","id":3,"method":"tools/list"}' --end eof)"; red 'row 47: reuse not refused → no refusal, one answer for two requests' bash -c 'started "$1" && [[ "$(fact "$1" 3 count)" == "1" ]] && ! stderr_has "$1" "reusing a pending id"' _ "$r"; fi
|
|
1120
|
+
mutant 48-route-by-method-only pump_server_to_client 'if method == "tools/call" or (isinstance(result, dict) and "content" in result):' 'if method == "tools/call":'
|
|
1121
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m48 "$MUTANT_PATH" --env FAKE_PW_FIXTURE_DIR="$INITTREE" --send "$INIT" --end eof)"; red 'row 48: routed by method only → a content-bearing initialize result is withheld by vet_other_result instead of redacted and delivered' bash -c '[[ "$(fact "$1" 1 found)" == "1" && "$(fact "$1" 1 isError)" == "true" ]]' _ "$r"; fi
|
|
1122
|
+
mutant 49-no-escaped-check rewrite_result 'if self.escaped_tree_in(text):' 'if False:'
|
|
1123
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m49 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/run-code-escaped.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 49: escaped-tree check removed → the escaped tree leaks' leaks "$r" 3; fi
|
|
1124
|
+
mutant 50-kill-only-live-child teardown ' if not self.wait_group_empty(self.grace):' ' if child.poll() is None and not self.wait_group_empty(self.grace):'
|
|
1125
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m50 "$MUTANT_PATH" --env FAKE_PW_GRANDCHILD=1 --send "$INIT" --end eof --timeout 4)"; red 'row 50: escalation keyed on the direct child → the grandchild survives' bash -c 'started "$1" && [[ "$(cat "$1/group_after")" -gt 0 ]]' _ "$r"; reap_group "$r"; fi
|
|
1126
|
+
mutant 51-save-video refuse_argv_and_env ' if cfg.get("saveVideo"):' ' if False:'
|
|
1127
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m51 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-video.json")"; red 'row 51: saveVideo check removed → proxy starts' started "$r"; fi
|
|
1128
|
+
mutant 52-output-mode refuse_argv_and_env 'if any(v != "stdout" for v in argv_values(server, "--output-mode") + [env.get("PLAYWRIGHT_MCP_OUTPUT_MODE", "stdout")]):' 'if False:'
|
|
1129
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m52 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --output-mode file)"; red 'row 52: --output-mode check removed → proxy starts' started "$r"; fi
|
|
1130
|
+
mutant 53-config-server refuse_argv_and_env 'if isinstance(srv, dict) and (srv.get("port") is not None or srv.get("host") is not None):' 'if False:'
|
|
1131
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m53 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-port.json")"; red 'row 53: config server.port check removed → proxy starts' started "$r"; fi
|
|
1132
|
+
mutant 54-relay-verbatim relay_server_message ' write_line(self.out, json.dumps(rebuilt, separators=(",", ":")).encode())' ' write_line(self.out, json.dumps(msg, separators=(",", ":")).encode())'
|
|
1133
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m54 "$MUTANT_PATH" --env FAKE_PW_CANCEL=1 --send "$INIT" --send "$SNAP" --end eof)"; red 'row 54: relayed notifications not rebuilt → the reason/_meta tree reaches the client' bash -c 'started "$1" && stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"; fi
|
|
1134
|
+
mutant 55-any-id-plain plain_id ' return isinstance(value, str) and not self.looks_like_a11y_tree(value) and self.redact_text(value) == value' ' return True'
|
|
1135
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m55 "$MUTANT_PATH" --env FAKE_PW_CANCEL=1 --send "$INIT" --send "$SNAP" --end eof)"; red 'row 55: requestId not vetted → a tree row rides a cancellation to the client' bash -c 'started "$1" && stdout_has "$1" ZZQP-SENTINEL-7980' _ "$r"; fi
|
|
1136
|
+
mutant 56-escaped-needs-newline escaped_tree_in ' if any(self.looks_like_a11y_tree(s) for s in json_strings(parsed)):' ' if any("\n" in s and self.looks_like_a11y_tree(s) for s in json_strings(parsed)):'
|
|
1137
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m56 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/run-code-escaped-one-row.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 56: escaped check requires a newline → a one-row tree leaks' leaks "$r" 3; fi
|
|
1138
|
+
mutant 57-udd-no-inject __init__ ' if profile_dir is not None:' ' if False:'
|
|
1139
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m57 "$MUTANT_PATH" "${UDD[@]}" --env "XDG_CACHE_HOME=$WORK/xdg" --env FAKE_PW_ARGV_OUT="$WORK/m57-argv" --send "$INIT" --end eof >/dev/null; red 'row 57: profile flag parsed but never injected → stub argv lacks --user-data-dir' bash -c 'test -f "$1" && ! grep -q -- "--user-data-dir" "$1"' _ "$WORK/m57-argv"; fi
|
|
1140
|
+
mutant 58-udd-dotdot-ok profile_dir_from_args ' if not name or name == "." or ".." in name:' ' if False:'
|
|
1141
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m58 "$MUTANT_PATH" --proxy-arg --user-data-dir-name --proxy-arg '..' --send "$INIT" --end eof --timeout 3)"; red 'row 58: the .. check removed → a traversal basename starts the proxy' started "$r"; fi
|
|
1142
|
+
mutant 59-udd-sep-ok profile_dir_from_args ' if "/" in name or "\\" in name:' ' if False:'
|
|
1143
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m59 "$MUTANT_PATH" --proxy-arg --user-data-dir-name --proxy-arg 'a/b' --send "$INIT" --end eof --timeout 3)"; red 'row 59: the separator check removed → a nested basename starts the proxy' started "$r"; fi
|
|
1144
|
+
mutant 60-udd-relxdg-ok profile_dir_from_args ' if not os.path.isabs(root):' ' if False:'
|
|
1145
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m60 "$MUTANT_PATH" "${UDD[@]}" --env XDG_CACHE_HOME=relative/cache --send "$INIT" --end eof --timeout 3)"; red 'row 60: the absolute-root check removed → a relative XDG_CACHE_HOME starts the proxy' started "$r"; fi
|
|
1146
|
+
mutant 61-udd-conflict-ok refuse_argv_and_env ' if profile_dir is not None and (argv_values(server, "--user-data-dir") or flag_present(server, "--user-data-dir")):' ' if False:'
|
|
1147
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m61 "$MUTANT_PATH" "${UDD[@]}" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --user-data-dir=/x)"; red 'row 61: the conflict check removed → flag + explicit --user-data-dir starts the proxy' started "$r"; fi
|
|
1148
|
+
mutant 62-udd-nohome-ok profile_dir_from_args ' if not home:' ' if False:'
|
|
1149
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m62 "$MUTANT_PATH" "${UDD[@]}" --env XDG_CACHE_HOME= --env HOME= --send "$INIT" --end eof --timeout 3)"; red 'row 62: the HOME check removed → the refusal no longer names the setting' bash -c 'started "$1" || ! stderr_has "$1" "HOME is set"' _ "$r"; fi
|
|
1150
|
+
mutant 63-udd-missing-ok profile_dir_from_args ' if i + 1 >= len(args):' ' if False:'
|
|
1151
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m63 "$MUTANT_PATH" --proxy-arg --user-data-dir-name --send "$INIT" --end eof --timeout 3)"; red 'row 63: the missing-argument check removed → a bare flag crashes instead of refusing cleanly' bash -c '[[ "$(rcof "$1")" != "2" ]] || ! stderr_has "$1" "refusing to start"' _ "$r"; fi
|
|
1152
|
+
mutant 64-udd-dup-ok profile_dir_from_args ' if name is not None:' ' if False:'
|
|
1153
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m64 "$MUTANT_PATH" --proxy-arg --user-data-dir-name --proxy-arg a --proxy-arg --user-data-dir-name=b --send "$INIT" --end eof --timeout 3)"; red 'row 64: the duplicate-flag check removed → the second value silently wins' started "$r"; fi
|
|
1154
|
+
mutant 65-udd-dash-ok profile_dir_from_args ' if name.startswith("-"):' ' if False:'
|
|
1155
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m65 "$MUTANT_PATH" --proxy-arg --user-data-dir-name --proxy-arg '--bogus' --send "$INIT" --end eof --timeout 3)"; red 'row 65: the leading-dash check removed → a flag-like value becomes a profile name' started "$r"; fi
|
|
1156
|
+
mutant 66-sink-flags-off refuse_argv_and_env ' for flag, envvar, why in SINK_FLAGS:' ' for flag, envvar, why in []:'
|
|
1157
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m66 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --storage-state=/x)"; red 'row 66: the sink-flag table dropped → --storage-state starts' started "$r"; fi
|
|
1158
|
+
mutant 67-sink-env-off refuse_argv_and_env ' for envvar, why in SINK_ENV_ONLY:' ' for envvar, why in []:'
|
|
1159
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m67 "$MUTANT_PATH" --env PLAYWRIGHT_MCP_USER_DATA_DIR=/tmp/p --send "$INIT" --end eof --timeout 3)"; red 'row 67: the env-only sink table dropped → PLAYWRIGHT_MCP_USER_DATA_DIR starts' started "$r"; fi
|
|
1160
|
+
mutant 68-trailing-ok refuse_argv_and_env ' if server and server[-1] in VALUE_FLAGS:' ' if False:'
|
|
1161
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m68 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config)"; red 'row 68: the trailing-valued-flag check dropped → a bare --config starts' started "$r"; fi
|
|
1162
|
+
mutant 69-eq-form-missed flag_present ' return any(a == flag or a.startswith(flag + "=") for a in server)' ' return any(a == flag for a in server)'
|
|
1163
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m69 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --storage-state=/x)"; red 'row 69: flag_present blind to the = spelling → --storage-state=/x starts' started "$r"; fi
|
|
1164
|
+
mutant 70-cfg-browser-off refuse_argv_and_env ' if isinstance(browser, dict):' ' if False:'
|
|
1165
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m70 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-cdp.json")"; red 'row 70: the config browser block dropped → browser.cdpEndpoint starts' started "$r"; fi
|
|
1166
|
+
mutant 71-cfg-secrets-ok refuse_argv_and_env ' if cfg.get("secrets"):' ' if False:'
|
|
1167
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m71 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-secrets.json")"; red 'row 71: the config secrets check dropped → secrets starts' started "$r"; fi
|
|
1168
|
+
mutant 72-realpath-off profile_dir_from_args ' return os.path.join(os.path.realpath(root), name)' ' return os.path.join(root, name)'
|
|
1169
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m72 "$MUTANT_PATH" "${UDD[@]}" --env "XDG_CACHE_HOME=$WORK/sub/.." --env FAKE_PW_ARGV_OUT="$WORK/m72-argv" --send "$INIT" --end eof >/dev/null; red 'row 72: realpath removed → the .. reaches the child argv verbatim' bash -c 'test -f "$1" && grep -qF ".." "$1"' _ "$WORK/m72-argv"; fi
|
|
1170
|
+
mutant 73-log-scrub-off log ' sys.stderr.write(f"{PREFIX} {msg.translate(_LOG_SCRUB)}\n")' ' sys.stderr.write(f"{PREFIX} {msg}\n")'
|
|
1171
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m73 "$MUTANT_PATH" --send "$INIT" --send '{"jsonrpc":"2.0","id":12,"method":"tools/call","params":{"name":"x\nFORGED-LINE","arguments":{"_meta":{}}}}' --end eof)"; red 'row 73: log scrub removed → the tool name forges a second stderr line' bash -c 'grep -q "^FORGED-LINE" "$1/stderr.txt"' _ "$r"; fi
|
|
1172
|
+
mutant 74-err-code-any vet_error ' if not isinstance(err.get("code"), int) or isinstance(err.get("code"), bool):' ' if False:'
|
|
1173
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m74 "$MUTANT_PATH" --env FAKE_PW_RESULT_FILE="$ODD/error-code-bool.json" --send "$INIT" --send "$SNAP" --end eof)"; red 'row 74: the code-type check dropped → a boolean error code is relayed verbatim' bash -c '[[ "$(fact "$1" 3 found)" == "1" && "$(fact "$1" 3 jsonrpc_error)" == "1" ]]' _ "$r"; fi
|
|
1174
|
+
mutant 75-other-unvetted vet_other_result ' if any(self.looks_like_a11y_tree(s) or self.redact_text(s) != s for s in json_strings(msg)):' ' if False:'
|
|
1175
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m75 "$MUTANT_PATH" --env FAKE_PW_PING_RESULT_FILE="$WORK/ping-tree.json" --send "$INIT" --send "$PING" --end eof)"; red 'row 75: the non-tools/call scan dropped → a ping result carries the tree to the client' leaks "$r" 40; fi
|
|
1176
|
+
mutant 76-cfg-ctx-off refuse_argv_and_env ' ctx = browser.get("contextOptions")
|
|
1177
|
+
if isinstance(ctx, dict):' ' ctx = browser.get("contextOptions")
|
|
1178
|
+
if False:'
|
|
1179
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m76 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-ctxstorage.json")"; red 'row 76: the contextOptions arm dropped → a config storageState starts' started "$r"; fi
|
|
1180
|
+
mutant 77-cfg-sandbox-ok refuse_argv_and_env ' if lo.get("chromiumSandbox") is False:' ' if False:'
|
|
1181
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m77 "$MUTANT_PATH" --send "$INIT" --end eof --timeout 3 --server python3 "$STUB" --config="$WORK/cfg-sandbox.json")"; red 'row 77: the chromiumSandbox check dropped → a config disabling the sandbox starts' started "$r"; fi
|
|
1182
|
+
mutant 78-fallback-ignores-explicit-browser chromium_fallback_applies ' if flag_present(server, "--browser") or env.get("PLAYWRIGHT_MCP_BROWSER"):' ' if False:'
|
|
1183
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m78 "$MUTANT_PATH" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m78-argv" --send "$INIT" --end eof --server python3 "$STUB" --browser firefox >/dev/null; red 'row 78: an explicit --browser ignored → a second --browser chromium is appended' python3 "$FBCHK" "$WORK/m78-argv" second-pair; fi
|
|
1184
|
+
mutant 79-fallback-ignores-chrome chromium_fallback_applies ' return not any(p and os.path.isfile(p) and os.access(p, os.X_OK) for p in paths)' ' return True'
|
|
1185
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m79 "$MUTANT_PATH" "${CFB[@]}" --env "$HASCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m79-argv" --send "$INIT" --end eof >/dev/null; red 'row 79: an installed Chrome ignored → chromium is appended on a Chrome host' python3 "$FBCHK" "$WORK/m79-argv" once; fi
|
|
1186
|
+
mutant 80-fallback-ignores-flag __init__ ' if chromium_fallback and chromium_fallback_applies(server, dict(os.environ)):' ' if chromium_fallback_applies(server, dict(os.environ)):'
|
|
1187
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m80 "$MUTANT_PATH" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m80-argv" --send "$INIT" --end eof >/dev/null; red 'row 80: the opt-in flag ignored → chromium is appended with the flag absent' python3 "$FBCHK" "$WORK/m80-argv" once; fi
|
|
1188
|
+
mutant 81-fallback-any-platform chromium_fallback_applies ' if plat not in GOOGLE_CHROME_PATHS:
|
|
1189
|
+
return False' ' if False:
|
|
1190
|
+
return False'
|
|
1191
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m81 "$WORK/platform-shim.py" "${CFB[@]}" --env SHIM_TARGET="$MUTANT_PATH" --env SHIM_PLATFORM=win32 --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m81-argv" --send "$INIT" --end eof >/dev/null; red 'row 81: the platform gate removed → chromium is appended on an unmodelled platform' python3 "$FBCHK" "$WORK/m81-argv" once; fi
|
|
1192
|
+
mutant 82-fallback-twice __init__ ' argv += ["--browser", "chromium", "--sandbox"]' ' argv += ["--browser", "chromium", "--sandbox", "--browser", "chromium", "--sandbox"]'
|
|
1193
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m82 "$MUTANT_PATH" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m82-argv" --send "$INIT" --end eof >/dev/null; red 'row 82: the triple appended twice → the argv carries two --browser chromium --sandbox' python3 "$FBCHK" "$WORK/m82-argv" twice; fi
|
|
1194
|
+
mutant 83-fallback-seam-ignored chromium_fallback_applies ' seam = env.get("PLAYWRIGHT_MCP_PROXY_CHROME_PATHS")' ' seam = None'
|
|
1195
|
+
if [[ -n "$MUTANT_PATH" ]]; then
|
|
1196
|
+
session m83p "$MUTANT_PATH" "${CFB[@]}" --env "$HASCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m83-present" --send "$INIT" --end eof >/dev/null
|
|
1197
|
+
session m83a "$MUTANT_PATH" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m83-absent" --send "$INIT" --end eof >/dev/null
|
|
1198
|
+
m83_diverges() { python3 "$FBCHK" "$WORK/m83-present" once || python3 "$FBCHK" "$WORK/m83-absent" none; } # a host with Chrome diverges on the absent row, one without on the present row
|
|
1199
|
+
red 'row 83: the path-list seam ignored → the known-path list decides, so one of the two seam rows diverges' m83_diverges
|
|
1200
|
+
fi
|
|
1201
|
+
mutant 84-fallback-twice-allowed take_chromium_fallback ' if found:' ' if False:'
|
|
1202
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m84 "$MUTANT_PATH" --proxy-arg --chromium-fallback --proxy-arg --chromium-fallback --send "$INIT" --end eof --timeout 3)"; red 'row 84: the duplicate-flag check removed → --chromium-fallback twice starts' started "$r"; fi
|
|
1203
|
+
mutant 85-ping-env-stripped __init__ 'stderr=None, bufsize=0, start_new_session=True)' 'stderr=None, bufsize=0, start_new_session=True, env={k: v for k, v in os.environ.items() if k != "PLAYWRIGHT_MCP_PING_TIMEOUT_MS"})'
|
|
1204
|
+
if [[ -n "$MUTANT_PATH" ]]; then r="$(session m85 "$MUTANT_PATH" --env PLAYWRIGHT_MCP_PING_TIMEOUT_MS=0 --env FAKE_PW_ENV_ECHO=1 --send "$INIT" --end eof)"; red 'row 85: a child-env allowlist that drops the ping variable → the server starts without it' bash -c 'started "$1" && stderr_has "$1" "PLAYWRIGHT_MCP_PING_TIMEOUT_MS=<unset>"' _ "$r"; fi
|
|
1205
|
+
mutant 86-fallback-drops-sandbox __init__ ' argv += ["--browser", "chromium", "--sandbox"]' ' argv += ["--browser", "chromium"]'
|
|
1206
|
+
if [[ -n "$MUTANT_PATH" ]]; then session m86 "$MUTANT_PATH" "${CFB[@]}" --env "$NOCHROME" --env FAKE_PW_ARGV_OUT="$WORK/m86-argv" --send "$INIT" --end eof >/dev/null; red 'row 86: --sandbox dropped → the bundled Chromium runs with no sandbox on Linux' python3 "$FBCHK" "$WORK/m86-argv" no-sandbox-flag; fi
|
|
1207
|
+
EXPECTED_MUTANTS=86 # rows 1-56 without row 4, with 22a/22b; 57-65 for --user-data-dir-name; 66-77 for the sink/env/config/trailing-flag/realpath/log-scrub/vetting hardening; 78-86 for --chromium-fallback (incl. its --sandbox) and the ping-env passthrough; exactly one mutation row each
|
|
1208
|
+
EXPECTED_RED_ROWS=86
|
|
1209
|
+
|
|
1210
|
+
# ---------------------------------------------------------------------------
|
|
1211
|
+
# Guard 2 — .mcp.json routing, EXECUTABLE (scratch HOME, npx shim on PATH)
|
|
1212
|
+
# ---------------------------------------------------------------------------
|
|
1213
|
+
G2="$WORK/g2"; mkdir -p "$G2/home" "$G2/bin" "$G2/stale"; printf '{}\n' > "$G2/second-config.json"
|
|
1214
|
+
cp "$PASSTHROUGH" "$G2/stale/playwright-mcp-redact-proxy.py"
|
|
1215
|
+
# The npx shim: one JSON record per call, APPENDED — the argv, and the parent resolved
|
|
1216
|
+
# by pid. (Two files rather than a nested heredoc, which line-based heredoc scanners
|
|
1217
|
+
# such as guard-vacuity-floor's cannot see the end of.)
|
|
1218
|
+
cat > "$G2/npx-record.py" <<'PY'
|
|
1219
|
+
import json, os, sys
|
|
1220
|
+
ppid = sys.argv[1]
|
|
1221
|
+
cmd = [c.decode() for c in open(f"/proc/{ppid}/cmdline", "rb").read().split(b"\0") if c]
|
|
1222
|
+
cwd = os.readlink(f"/proc/{ppid}/cwd")
|
|
1223
|
+
script = os.path.realpath(os.path.join(cwd, cmd[1])) if len(cmd) > 1 else ""
|
|
1224
|
+
print(json.dumps({"argv": sys.argv[2:], "parent_comm": open(f"/proc/{ppid}/comm").read().strip(), "parent_script": script}))
|
|
1225
|
+
PY
|
|
1226
|
+
printf '#!/usr/bin/env bash\npython3 "%s" "$PPID" "$@" %s "$SHIM_OUT"\n' "$G2/npx-record.py" '>>' > "$G2/bin/npx"
|
|
1227
|
+
chmod +x "$G2/bin/npx"
|
|
1228
|
+
# run_mcp_args <args1-string> <tag>; the wrapper sources the slot script, which resolves $prof under the scratch HOME
|
|
1229
|
+
run_mcp_args() {
|
|
1230
|
+
local cmd="$1" tag="$2"
|
|
1231
|
+
: > "$G2/$tag.jsonl"
|
|
1232
|
+
( cd "$REPO_ROOT" && export SHIM_OUT="$G2/$tag.jsonl" HOME="$G2/home" PATH="$G2/bin:$PATH" && sleep 5 | timeout 20 bash -c "$cmd" >/dev/null 2>"$G2/$tag.err" ) || true
|
|
1233
|
+
}
|
|
1234
|
+
# g2_prop <jsonl> <property> — exit 0 when the property holds; 3 when the shim never ran
|
|
1235
|
+
g2_prop() {
|
|
1236
|
+
python3 - "$1" "$2" "$PROXY_SHIPPED" "$PIN" "$G2/home/.cache/playwright-mcp-profile" <<'PY'
|
|
1237
|
+
import json, os, sys
|
|
1238
|
+
path, prop, shipped, pin, prof = sys.argv[1:]
|
|
1239
|
+
calls = [json.loads(l) for l in open(path) if l.strip()]
|
|
1240
|
+
if not calls:
|
|
1241
|
+
sys.exit(3)
|
|
1242
|
+
argv = calls[0]["argv"]
|
|
1243
|
+
checks = {
|
|
1244
|
+
"one-call": len(calls) == 1,
|
|
1245
|
+
"parent-is-shipped-proxy": len(calls) == 1 and calls[0]["parent_comm"] == "python3" and calls[0]["parent_script"] == os.path.realpath(shipped),
|
|
1246
|
+
"pin-once": argv.count(f"@playwright/mcp@{pin}") == 1,
|
|
1247
|
+
"profile-once": argv.count(f"--user-data-dir={prof}") == 1 and sum(a.startswith("--user-data-dir") for a in argv) == 1,
|
|
1248
|
+
"config-once": argv.count("--config=.claude/playwright-mcp.config.json") == 1 and sum(a.startswith("--config") for a in argv) == 1,
|
|
1249
|
+
"flag-appended-once": argv[-2:] == ["--snapshot-mode", "none"] and argv.count("--snapshot-mode") == 1,
|
|
1250
|
+
}
|
|
1251
|
+
sys.exit(0 if checks[prop] else 1)
|
|
1252
|
+
PY
|
|
1253
|
+
}
|
|
1254
|
+
export G2 PROXY_SHIPPED PIN
|
|
1255
|
+
MCP_ARGS="$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['mcpServers']['playwright']['args'][1])" "$REPO_ROOT/.mcp.json")"
|
|
1256
|
+
MCP_ARG0="$(python3 -c "import json,sys; print(json.load(open(sys.argv[1]))['mcpServers']['playwright']['args'][0])" "$REPO_ROOT/.mcp.json")"
|
|
1257
|
+
assert_true 'Guard 2: .mcp.json playwright command is bash -c <string>' test "$MCP_ARG0" = "-c"
|
|
1258
|
+
assert_true 'Guard 2: the .mcp.json string itself carries no --snapshot-mode (only the proxy may append it)' bash -c '[[ "$1" != *"--snapshot-mode"* ]]' _ "$MCP_ARGS"
|
|
1259
|
+
run_mcp_args "$MCP_ARGS" g2
|
|
1260
|
+
for prop in one-call parent-is-shipped-proxy pin-once profile-once config-once flag-appended-once; do
|
|
1261
|
+
assert_true "Guard 2: $prop" g2_prop "$G2/g2.jsonl" "$prop"
|
|
1262
|
+
done
|
|
1263
|
+
# Guard 2 mutants of the args string: each must fail its property while the shim DID run
|
|
1264
|
+
g2_mut() { # <label> <mutated-args> <tag> <property>
|
|
1265
|
+
local label="$1" mutated="$2" tag="$3" prop="$4"
|
|
1266
|
+
cases=$((cases + 1))
|
|
1267
|
+
if [[ "$mutated" == "$MCP_ARGS" ]]; then bad "$label — mutation did not land"; return 0; fi
|
|
1268
|
+
run_mcp_args "$mutated" "$tag"
|
|
1269
|
+
local rc=0; g2_prop "$G2/$tag.jsonl" "$prop" || rc=$?
|
|
1270
|
+
if [[ $rc == 1 ]]; then ok "$label"; elif [[ $rc == 3 ]]; then bad "$label — the shim never ran"; else bad "$label — mutant survived"; fi
|
|
1271
|
+
}
|
|
1272
|
+
PROXY_REL='python3 plugins/soleur/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py -- '
|
|
1273
|
+
g2_mut 'Guard 2 mutant 1: proxy removed → the parent is not the shipped proxy' "${MCP_ARGS/"$PROXY_REL"/}" g2m1 parent-is-shipped-proxy
|
|
1274
|
+
M2="$(python3 -c 'import sys; s,p=sys.argv[1],sys.argv[2]; i=s.index(p); s=s[:i]+s[i+len(p):]; print(s.rstrip()+" -- "+p.strip(" -"))' "$MCP_ARGS" "$PROXY_REL")"
|
|
1275
|
+
g2_mut 'Guard 2 mutant 2: proxy AFTER the server → the parent is not the shipped proxy' "$M2" g2m2 parent-is-shipped-proxy
|
|
1276
|
+
g2_mut 'Guard 2 mutant 3: --config dropped → argv lacks it' "${MCP_ARGS// --config=.claude\/playwright-mcp.config.json/}" g2m3 config-once
|
|
1277
|
+
g2_mut "Guard 2 mutant 4: version bumped in .mcp.json alone → argv no longer carries @playwright/mcp@$PIN" "${MCP_ARGS//@playwright\/mcp@$PIN/@playwright\/mcp@9.9.9}" g2m4 pin-once
|
|
1278
|
+
g2_mut 'Guard 2 mutant 6: a passthrough saved under the proxy'"'"'s filename → not the shipped proxy' "${MCP_ARGS/plugins\/soleur\/skills\/agent-browser\/scripts\/playwright-mcp-redact-proxy.py/$G2/stale/playwright-mcp-redact-proxy.py}" g2m6 parent-is-shipped-proxy
|
|
1279
|
+
g2_mut 'Guard 2 mutant 7: an unwrapped npx run before the wrapped one → two calls' "${MCP_ARGS/exec env/npx @playwright/mcp@$PIN --probe; exec env}" g2m7 one-call
|
|
1280
|
+
g2_mut 'Guard 2 mutant 8: a second --config later in the args → config not once' "${MCP_ARGS/--config=.claude\/playwright-mcp.config.json/--config=.claude/playwright-mcp.config.json --config=$G2/second-config.json}" g2m8 config-once
|
|
1281
|
+
g2_mut 'Guard 2 mutant 9: a second --user-data-dir → profile not once' "${MCP_ARGS/--user-data-dir=\"\$prof\"/--user-data-dir=\"\$prof\" --user-data-dir=/tmp/other}" g2m9 profile-once
|
|
1282
|
+
g2_mut 'Guard 2 mutant 10: --snapshot-mode full in the args → flag not appended once' "${MCP_ARGS/--user-data-dir=\"\$prof\"/--snapshot-mode full --user-data-dir=\"\$prof\"}" g2m10 flag-appended-once
|
|
1283
|
+
assert_true 'Guard 2 mutant 5: renaming the server key fails the lookup loudly' bash -c '! python3 -c "import json,sys; d=json.load(open(sys.argv[1])); d[\"mcpServers\"][\"playwrite\"]=d[\"mcpServers\"].pop(\"playwright\"); print(d[\"mcpServers\"][\"playwright\"][\"args\"][1])" "$1" 2>/dev/null' _ "$REPO_ROOT/.mcp.json"
|
|
1284
|
+
# The wrapper no longer reaps: no pattern kill of the proxy, the server or Chrome, and the slot script is sourced
|
|
1285
|
+
# BEFORE the exec (the lifetime suite executes the lease itself). Positive proof beside each absence: the string
|
|
1286
|
+
# still carries the exec env and the proxy, so a vacuous or truncated string cannot satisfy them.
|
|
1287
|
+
SLOT_REL='plugins/soleur/skills/agent-browser/scripts/playwright-mcp-profile-slot.sh'
|
|
1288
|
+
assert_true 'Guard 2: the launch string kills nothing by pattern (no pkill/killall/pgrep), with the exec env and the shipped proxy still present' python3 - "$MCP_ARGS" "$PROXY_REL" <<'PY'
|
|
1289
|
+
import re, sys
|
|
1290
|
+
s, proxy = sys.argv[1], sys.argv[2].strip(" -")
|
|
1291
|
+
sys.exit(0 if "exec env" in s and proxy in s and re.search(r"\b(pkill|killall|pgrep)\b", s) is None else 1)
|
|
1292
|
+
PY
|
|
1293
|
+
assert_true 'Guard 2: the slot script is sourced (with || exit 1) before the exec' python3 - "$MCP_ARGS" "$SLOT_REL" <<'PY'
|
|
1294
|
+
import sys
|
|
1295
|
+
s, slot = sys.argv[1], sys.argv[2]
|
|
1296
|
+
src = f'. {slot} || exit 1; '
|
|
1297
|
+
i, j = s.find(src), s.find("exec env")
|
|
1298
|
+
sys.exit(0 if i == 0 and j == len(src) else 1)
|
|
1299
|
+
PY
|
|
1300
|
+
# The whole launch string, pinned: the slot is sourced with no unreachable $prof clause, and the profile flag is quoted
|
|
1301
|
+
# so a $HOME with a space cannot split it into extra server flags.
|
|
1302
|
+
assert_true 'Guard 2: the .mcp.json launch string is exactly the pinned one (slot sourced, X11 env, proxy, pinned server, quoted profile flag)' python3 - "$MCP_ARGS" "$SLOT_REL" "$PIN" <<'PY'
|
|
1303
|
+
import sys
|
|
1304
|
+
s, slot, pin = sys.argv[1:]
|
|
1305
|
+
want = (f". {slot} || exit 1; exec env -u WAYLAND_DISPLAY XDG_SESSION_TYPE=x11 GDK_BACKEND=x11 DISPLAY=${{DISPLAY:-:0}} "
|
|
1306
|
+
"python3 plugins/soleur/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py -- "
|
|
1307
|
+
f'npx @playwright/mcp@{pin} --user-data-dir="$prof" --config=.claude/playwright-mcp.config.json')
|
|
1308
|
+
sys.exit(0 if s == want else 1)
|
|
1309
|
+
PY
|
|
1310
|
+
|
|
1311
|
+
# ---------------------------------------------------------------------------
|
|
1312
|
+
# Guard 3 — plugins/soleur/.mcp.json ships the proxy-wrapped registration (#8156)
|
|
1313
|
+
# ---------------------------------------------------------------------------
|
|
1314
|
+
PLUGIN_MCP="$REPO_ROOT/plugins/soleur/.mcp.json"
|
|
1315
|
+
g3_shape() { # <mcp.json> — exit 0 when playwright is the ONLY key and is the required wrapped registration
|
|
1316
|
+
python3 - "$1" "$PIN" <<'PY'
|
|
1317
|
+
import json, sys
|
|
1318
|
+
path, pin = sys.argv[1:]
|
|
1319
|
+
d = json.load(open(path))
|
|
1320
|
+
if set(d.get("mcpServers", {})) != {"playwright"}:
|
|
1321
|
+
sys.exit(1)
|
|
1322
|
+
e = d["mcpServers"]["playwright"]
|
|
1323
|
+
expected = [
|
|
1324
|
+
"${CLAUDE_PLUGIN_ROOT}/skills/agent-browser/scripts/playwright-mcp-redact-proxy.py",
|
|
1325
|
+
"--user-data-dir-name",
|
|
1326
|
+
"soleur-playwright-mcp-profile",
|
|
1327
|
+
"--chromium-fallback",
|
|
1328
|
+
"--",
|
|
1329
|
+
"npx",
|
|
1330
|
+
f"@playwright/mcp@{pin}",
|
|
1331
|
+
]
|
|
1332
|
+
sys.exit(0 if e["command"] == "python3" and e["args"] == expected else 1)
|
|
1333
|
+
PY
|
|
1334
|
+
}
|
|
1335
|
+
assert_true 'Guard 3: plugins/soleur/.mcp.json playwright entry is the wrapped registration (python3, ${CLAUDE_PLUGIN_ROOT} proxy, profile flag, --chromium-fallback, pin parity, sole mcpServers key)' g3_shape "$PLUGIN_MCP"
|
|
1336
|
+
# The rest of the manifest contract — never `bash`, never `--config`, never a
|
|
1337
|
+
# literal `--user-data-dir` — is carried by g3_shape's exact argv equality; a
|
|
1338
|
+
# separate literal grep cannot fail while it passes, so it is not an assertion.
|
|
1339
|
+
# The plugin's playwright server lives in .mcp.json ALONE: a manifest
|
|
1340
|
+
# mcpServers entry would register a SECOND, unwrapped server. Asserted absent
|
|
1341
|
+
# against the positive proof that plugin.json DOES carry mcpServers.
|
|
1342
|
+
assert_true 'Guard 3: plugin.json carries mcpServers but NO playwright entry (the plugin server is .mcp.json-only)' python3 - "$REPO_ROOT/plugins/soleur/.claude-plugin/plugin.json" <<'PY'
|
|
1343
|
+
import json, sys
|
|
1344
|
+
d = json.load(open(sys.argv[1]))
|
|
1345
|
+
sys.exit(0 if d.get("mcpServers") and "playwright" not in d["mcpServers"] else 1)
|
|
1346
|
+
PY
|
|
1347
|
+
# The vendored plugin tree must NOT carry .mcp.json at all: the hosted
|
|
1348
|
+
# agent-runner loads the vendored root via plugins:[{type:"local"}] and the
|
|
1349
|
+
# prod image has no python3 and no @playwright/mcp@0.0.78 — a vendored
|
|
1350
|
+
# registration is a guaranteed-failed server every session. Both vendor steps
|
|
1351
|
+
# exclude the file; these rows pin the exclusion as LIVE lines (a commented-out
|
|
1352
|
+
# rm cannot satisfy them) ordered AFTER the cp -a that produced the tree.
|
|
1353
|
+
for wf in ci.yml reusable-release.yml; do
|
|
1354
|
+
assert_true "Guard 3: $wf vendored plugin tree excludes plugins/soleur/.mcp.json (live line, ordered after the cp -a)" python3 - "$REPO_ROOT/.github/workflows/$wf" <<'PY'
|
|
1355
|
+
import sys
|
|
1356
|
+
lines = open(sys.argv[1]).read().splitlines()
|
|
1357
|
+
cp = [i for i, l in enumerate(lines) if "cp -a --no-dereference plugins/soleur" in l and not l.lstrip().startswith("#")]
|
|
1358
|
+
rm = [i for i, l in enumerate(lines) if l.strip() == 'rm -f "$DEST/.mcp.json"']
|
|
1359
|
+
sys.exit(0 if cp and rm and min(rm) > max(cp) else 1)
|
|
1360
|
+
PY
|
|
1361
|
+
done
|
|
1362
|
+
# In-context backstop (the vendor rm runs in the workflow, not the Dockerfile):
|
|
1363
|
+
# the `!_plugin-vendored/**/*.json` bang would otherwise re-include .mcp.json.
|
|
1364
|
+
# Last-match-wins — the exclude must come AFTER the bang.
|
|
1365
|
+
assert_true 'Guard 3: .dockerignore re-excludes _plugin-vendored/.mcp.json after the **/*.json re-include' python3 - "$REPO_ROOT/apps/web-platform/.dockerignore" <<'PY'
|
|
1366
|
+
import sys
|
|
1367
|
+
lines = [l.strip() for l in open(sys.argv[1])]
|
|
1368
|
+
inc = [i for i, l in enumerate(lines) if l == "!_plugin-vendored/**/*.json"]
|
|
1369
|
+
exc = [i for i, l in enumerate(lines) if l == "_plugin-vendored/.mcp.json"]
|
|
1370
|
+
sys.exit(0 if inc and exc and max(exc) > max(inc) else 1)
|
|
1371
|
+
PY
|
|
1372
|
+
g3_mut() { # <label> <python-mutation-src> — the mutated manifest must fail g3_shape
|
|
1373
|
+
cases=$((cases + 1))
|
|
1374
|
+
local f="$WORK/g3-mut.json"
|
|
1375
|
+
MUTATION="$2" python3 - "$PLUGIN_MCP" "$f" <<'PY'
|
|
1376
|
+
import json, os, sys
|
|
1377
|
+
d = json.load(open(sys.argv[1]))
|
|
1378
|
+
exec(os.environ["MUTATION"])
|
|
1379
|
+
json.dump(d, open(sys.argv[2], "w"))
|
|
1380
|
+
PY
|
|
1381
|
+
if g3_shape "$f"; then bad "$1 — mutant survived"; else ok "$1"; fi
|
|
1382
|
+
}
|
|
1383
|
+
g3_mut 'Guard 3 mutant 1: command bash instead of python3' 'd["mcpServers"]["playwright"]["command"] = "bash"'
|
|
1384
|
+
g3_mut 'Guard 3 mutant 2: a literal --user-data-dir replaces the name flag' 'd["mcpServers"]["playwright"]["args"][1:3] = ["--user-data-dir=/tmp/x"]'
|
|
1385
|
+
g3_mut 'Guard 3 mutant 3: the pin drifts from .mcp.json' 'd["mcpServers"]["playwright"]["args"][-1] = "@playwright/mcp@9.9.9"'
|
|
1386
|
+
g3_mut 'Guard 3 mutant 4: the proxy dropped → an unwrapped npx registration' 'd["mcpServers"]["playwright"]["args"] = ["--", "npx", "@playwright/mcp@0.0.78"]'
|
|
1387
|
+
g3_mut 'Guard 3 mutant 6: a second mcpServers key → not the pinned key set' 'd["mcpServers"]["other"] = {"command": "npx", "args": ["x"]}'
|
|
1388
|
+
g3_mut 'Guard 3 mutant 7: --chromium-fallback dropped → a Chrome-less host gets no browser' 'd["mcpServers"]["playwright"]["args"].remove("--chromium-fallback")'
|
|
1389
|
+
assert_true 'Guard 3 mutant 5: renaming the server key fails the lookup loudly' bash -c '! python3 -c "import json,sys; d=json.load(open(sys.argv[1])); d[\"mcpServers\"][\"playwrite\"]=d[\"mcpServers\"].pop(\"playwright\"); print(d[\"mcpServers\"][\"playwright\"][\"args\"][0])" "$1" 2>/dev/null' _ "$PLUGIN_MCP"
|
|
1390
|
+
|
|
1391
|
+
# PR2 (#10331): the swallowed-error note carries a class name and a literal site label only, never raises,
|
|
1392
|
+
# and runs after the group kill. The probe text is shared with the other suites except its census and the teardown case.
|
|
1393
|
+
# $PROXY, not $PROXY_SHIPPED: the probe must exercise the proxy under test so a mutated copy (PROXY_UNDER_TEST) is actually driven.
|
|
1394
|
+
note_probe="$(python3 -I - "$PROXY" reply-unrelayed-request,teardown-stdin-close,teardown-sigkill,teardown-final-wait,teardown-flush 2>&1 <<'PY' || true
|
|
1395
|
+
import ast, importlib.util, io, os, signal, sys
|
|
1396
|
+
spec = importlib.util.spec_from_file_location("note_target", sys.argv[1])
|
|
1397
|
+
mod = importlib.util.module_from_spec(spec)
|
|
1398
|
+
spec.loader.exec_module(mod)
|
|
1399
|
+
|
|
1400
|
+
def say(ok, name):
|
|
1401
|
+
print(("OK " if ok else "NOT-OK ") + name)
|
|
1402
|
+
|
|
1403
|
+
class Boom(OSError):
|
|
1404
|
+
pass
|
|
1405
|
+
|
|
1406
|
+
def run_note(exc, stream):
|
|
1407
|
+
real, sys.stderr = sys.stderr, stream
|
|
1408
|
+
try:
|
|
1409
|
+
mod._note(exc, "probe")
|
|
1410
|
+
finally:
|
|
1411
|
+
sys.stderr = real
|
|
1412
|
+
|
|
1413
|
+
buf = io.StringIO()
|
|
1414
|
+
run_note(Boom("CANARY-7f3a"), buf)
|
|
1415
|
+
line = buf.getvalue()
|
|
1416
|
+
say(line.endswith(" swallowed Boom at probe\n") and line.count("\n") == 1, "line-shape")
|
|
1417
|
+
say("CANARY" not in line, "canary-absent")
|
|
1418
|
+
|
|
1419
|
+
class Failing:
|
|
1420
|
+
def __init__(self, exc):
|
|
1421
|
+
self.exc = exc
|
|
1422
|
+
def write(self, s):
|
|
1423
|
+
raise self.exc
|
|
1424
|
+
def flush(self):
|
|
1425
|
+
raise self.exc
|
|
1426
|
+
|
|
1427
|
+
for name, exc in (("oserror-stderr", OSError("w")), ("valueerror-stderr", ValueError("w")), ("runtimeerror-stderr", RuntimeError("w"))):
|
|
1428
|
+
try:
|
|
1429
|
+
run_note(Boom("x"), Failing(exc))
|
|
1430
|
+
say(True, name)
|
|
1431
|
+
except BaseException:
|
|
1432
|
+
say(False, name)
|
|
1433
|
+
|
|
1434
|
+
try:
|
|
1435
|
+
run_note(Boom("x"), None)
|
|
1436
|
+
say(True, "none-stderr")
|
|
1437
|
+
except BaseException:
|
|
1438
|
+
say(False, "none-stderr")
|
|
1439
|
+
|
|
1440
|
+
closed = open(os.devnull, "w")
|
|
1441
|
+
closed.close()
|
|
1442
|
+
try:
|
|
1443
|
+
run_note(Boom("x"), closed)
|
|
1444
|
+
say(True, "closed-stderr")
|
|
1445
|
+
except BaseException:
|
|
1446
|
+
say(False, "closed-stderr")
|
|
1447
|
+
|
|
1448
|
+
def literal(node):
|
|
1449
|
+
return isinstance(node, ast.Constant) and isinstance(node.value, str)
|
|
1450
|
+
|
|
1451
|
+
def census(src):
|
|
1452
|
+
# the proxy collects (exc, literal-label) pairs in `pending` inside teardown and emits them from _flush_notes,
|
|
1453
|
+
# so the loop's own _note(exc, label) call is exempt and every pending.append tuple is checked instead
|
|
1454
|
+
calls, bad, labels = 0, [], set()
|
|
1455
|
+
tree = ast.parse(src)
|
|
1456
|
+
exempt = set()
|
|
1457
|
+
for fn in ast.walk(tree):
|
|
1458
|
+
if isinstance(fn, ast.FunctionDef) and fn.name == "_flush_notes":
|
|
1459
|
+
exempt.update(id(n) for n in ast.walk(fn))
|
|
1460
|
+
for node in ast.walk(tree):
|
|
1461
|
+
if not isinstance(node, ast.Call) or id(node) in exempt:
|
|
1462
|
+
continue
|
|
1463
|
+
if isinstance(node.func, ast.Name) and node.func.id == "_note":
|
|
1464
|
+
calls += 1
|
|
1465
|
+
if len(node.args) == 2 and not node.keywords and isinstance(node.args[0], ast.Name) and literal(node.args[1]):
|
|
1466
|
+
labels.add(node.args[1].value)
|
|
1467
|
+
else:
|
|
1468
|
+
bad.append(node.lineno)
|
|
1469
|
+
elif isinstance(node.func, ast.Attribute) and node.func.attr == "append" and isinstance(node.func.value, ast.Name) and node.func.value.id == "pending":
|
|
1470
|
+
calls += 1
|
|
1471
|
+
tup = node.args[0] if len(node.args) == 1 else None
|
|
1472
|
+
if isinstance(tup, ast.Tuple) and len(tup.elts) == 2 and isinstance(tup.elts[0], ast.Name) and literal(tup.elts[1]):
|
|
1473
|
+
labels.add(tup.elts[1].value)
|
|
1474
|
+
else:
|
|
1475
|
+
bad.append(node.lineno)
|
|
1476
|
+
return calls, bad, labels
|
|
1477
|
+
|
|
1478
|
+
calls, bad, labels = census(open(sys.argv[1], encoding="utf-8").read())
|
|
1479
|
+
say(calls >= 1 and not bad, "census-literal-labels")
|
|
1480
|
+
say(labels == set(sys.argv[2].split(",")), "census-label-set")
|
|
1481
|
+
say(census("_note(exc, site='x')")[1] != [], "census-rejects-keyword")
|
|
1482
|
+
say(census("_note(str(exc), 'a')")[1] != [], "census-rejects-computed")
|
|
1483
|
+
say(census("_note(exc, 'a')")[:2] == (1, []), "census-accepts-literal")
|
|
1484
|
+
say(census("pending.append((exc, 'a'))")[:2] == (1, []), "census-accepts-pending-literal")
|
|
1485
|
+
say(census("pending.append((exc, str(exc)))")[1] != [], "census-rejects-pending-computed")
|
|
1486
|
+
|
|
1487
|
+
class FakeStdin:
|
|
1488
|
+
closed = False
|
|
1489
|
+
def close(self):
|
|
1490
|
+
raise OSError("stdin")
|
|
1491
|
+
|
|
1492
|
+
class FakeChild:
|
|
1493
|
+
stdin = FakeStdin()
|
|
1494
|
+
returncode = 0
|
|
1495
|
+
def wait(self, timeout=None):
|
|
1496
|
+
return 0
|
|
1497
|
+
def poll(self):
|
|
1498
|
+
return 0
|
|
1499
|
+
|
|
1500
|
+
class Exit(BaseException):
|
|
1501
|
+
def __init__(self, code):
|
|
1502
|
+
self.code = code
|
|
1503
|
+
|
|
1504
|
+
def fake_exit(code):
|
|
1505
|
+
raise Exit(code)
|
|
1506
|
+
|
|
1507
|
+
kills = []
|
|
1508
|
+
def fake_killpg(pg, sig):
|
|
1509
|
+
if sig == 0:
|
|
1510
|
+
raise ProcessLookupError
|
|
1511
|
+
kills.append(sig)
|
|
1512
|
+
|
|
1513
|
+
attempts = []
|
|
1514
|
+
texts = []
|
|
1515
|
+
class FlakyErr(io.StringIO):
|
|
1516
|
+
def write(self, s):
|
|
1517
|
+
if "swallowed" in s:
|
|
1518
|
+
attempts.append(list(kills))
|
|
1519
|
+
texts.append(s)
|
|
1520
|
+
raise RuntimeError("reentrant write")
|
|
1521
|
+
return super().write(s)
|
|
1522
|
+
|
|
1523
|
+
proxy = mod.Proxy.__new__(mod.Proxy)
|
|
1524
|
+
proxy.out = io.BytesIO()
|
|
1525
|
+
proxy.tearing_down = False
|
|
1526
|
+
proxy.grace = 0.2
|
|
1527
|
+
proxy.pgid = 4242424
|
|
1528
|
+
proxy.child = FakeChild()
|
|
1529
|
+
real_exit, real_killpg, real_err = os._exit, os.killpg, sys.stderr
|
|
1530
|
+
os._exit, os.killpg, sys.stderr = fake_exit, fake_killpg, FlakyErr()
|
|
1531
|
+
code = None
|
|
1532
|
+
try:
|
|
1533
|
+
proxy.teardown("probe")
|
|
1534
|
+
except Exit as stop:
|
|
1535
|
+
code = stop.code
|
|
1536
|
+
except BaseException:
|
|
1537
|
+
code = "raised"
|
|
1538
|
+
finally:
|
|
1539
|
+
captured = sys.stderr.getvalue()
|
|
1540
|
+
os._exit, os.killpg, sys.stderr = real_exit, real_killpg, real_err
|
|
1541
|
+
say(code == 0 and attempts == [[signal.SIGTERM]] and "child exited rc=0" in captured, "teardown-note-after-kill")
|
|
1542
|
+
say(texts == [mod.PREFIX + " swallowed OSError at teardown-stdin-close\n"], "teardown-note-text")
|
|
1543
|
+
|
|
1544
|
+
# Teardown narrowness (slice 1 of #10331): the SIGTERM step suppresses ProcessLookupError ONLY. A PermissionError from os.killpg is not
|
|
1545
|
+
# "the group is already empty" and must surface; widening the suppress to Exception would swallow it.
|
|
1546
|
+
def fake_killpg_denied(pg, sig):
|
|
1547
|
+
if sig == 0:
|
|
1548
|
+
raise ProcessLookupError
|
|
1549
|
+
raise PermissionError("denied")
|
|
1550
|
+
|
|
1551
|
+
proxy2 = mod.Proxy.__new__(mod.Proxy)
|
|
1552
|
+
proxy2.out = io.BytesIO()
|
|
1553
|
+
proxy2.tearing_down = False
|
|
1554
|
+
proxy2.grace = 0.2
|
|
1555
|
+
proxy2.pgid = 4242425
|
|
1556
|
+
proxy2.child = FakeChild()
|
|
1557
|
+
real_exit, real_killpg, real_err = os._exit, os.killpg, sys.stderr
|
|
1558
|
+
os._exit, os.killpg, sys.stderr = fake_exit, fake_killpg_denied, io.StringIO()
|
|
1559
|
+
outcome = "returned"
|
|
1560
|
+
try:
|
|
1561
|
+
proxy2.teardown("probe-denied")
|
|
1562
|
+
except Exit:
|
|
1563
|
+
outcome = "exit"
|
|
1564
|
+
except PermissionError:
|
|
1565
|
+
outcome = "PermissionError"
|
|
1566
|
+
except BaseException:
|
|
1567
|
+
outcome = "other"
|
|
1568
|
+
finally:
|
|
1569
|
+
os._exit, os.killpg, sys.stderr = real_exit, real_killpg, real_err
|
|
1570
|
+
say(outcome == "PermissionError", "teardown-permerror-surfaces")
|
|
1571
|
+
PY
|
|
1572
|
+
)"
|
|
1573
|
+
for tok in line-shape canary-absent oserror-stderr valueerror-stderr runtimeerror-stderr none-stderr closed-stderr census-literal-labels census-label-set census-rejects-keyword census-rejects-computed census-accepts-literal census-accepts-pending-literal census-rejects-pending-computed teardown-note-after-kill teardown-note-text teardown-permerror-surfaces; do
|
|
1574
|
+
assert_true "PR2 note probe: $tok" grep -qxF "OK $tok" <<<"$note_probe"
|
|
1575
|
+
done
|
|
1576
|
+
|
|
1577
|
+
reap_all
|
|
1578
|
+
assert_true 'hygiene: sessions recorded their child groups, and none still holding a stub outlives the reap' bash -c '[[ "$1" -gt 0 && -z "$2" ]]' _ "$(grep -c . "$PGIDS" || true)" "$(stub_groups)"
|
|
1579
|
+
|
|
1580
|
+
# ---------------------------------------------------------------------------
|
|
1581
|
+
# Verdict. Reported with printf + exit, never through ok()/bad() (ADR-193).
|
|
1582
|
+
# ---------------------------------------------------------------------------
|
|
1583
|
+
printf '\n%d passed, %d failed, %d cases (%d mutants, %d mutation rows)\n' "$pass" "$fail" "$cases" "$mutants_declared" "$red_rows"
|
|
1584
|
+
|
|
1585
|
+
if [[ $((pass + fail)) -ne $cases ]]; then
|
|
1586
|
+
printf '[FATAL] vacuity accounting: pass+fail (%d) != cases (%d) — a row did not report\n' "$((pass + fail))" "$cases" >&2
|
|
1587
|
+
exit 1
|
|
1588
|
+
fi
|
|
1589
|
+
if [[ $mutants_declared -ne $EXPECTED_MUTANTS || $red_rows -ne $EXPECTED_RED_ROWS ]]; then
|
|
1590
|
+
printf '[FATAL] mutation matrix: %d mutants / %d mutation rows ran, expected %d / %d — a row vanished\n' "$mutants_declared" "$red_rows" "$EXPECTED_MUTANTS" "$EXPECTED_RED_ROWS" >&2
|
|
1591
|
+
exit 1
|
|
1592
|
+
fi
|
|
1593
|
+
MIN_ASSERTIONS=499
|
|
1594
|
+
if [[ $cases -lt $MIN_ASSERTIONS ]]; then
|
|
1595
|
+
printf '[FATAL] vacuity floor: only %d cases executed, expected at least %d\n' "$cases" "$MIN_ASSERTIONS" >&2
|
|
1596
|
+
exit 1
|
|
1597
|
+
fi
|
|
1598
|
+
[[ $fail -eq 0 ]] || exit 1
|
|
1599
|
+
exit 0
|