@mmerterden/multi-agent-pipeline 20.2.0 → 20.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +923 -1
- package/README.md +104 -81
- package/README.tr.md +103 -62
- package/docs/FIGMA_PIPELINE.md +35 -35
- package/docs/adr/0006-skills-core-external-split.md +1 -1
- package/docs/adr/0007-multi-tool-adapter-framework.md +6 -0
- package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +7 -0
- package/docs/architecture.md +50 -14
- package/docs/best-practices.md +1 -1
- package/docs/ecosystem.md +56 -32
- package/docs/facts.json +10 -10
- package/docs/features.md +97 -5
- package/docs/recovery-guide.md +7 -14
- package/docs/server-readiness.md +31 -24
- package/index.js +1 -1
- package/install/_common.mjs +3 -5
- package/install/_platform-filter.mjs +23 -1
- package/install/_unattended-profile.mjs +321 -75
- package/install/claude.mjs +51 -10
- package/install/codex.mjs +2 -0
- package/install/copilot.mjs +2 -0
- package/install/index.mjs +30 -17
- package/install/templates/claude-hooks.json +16 -5
- package/install/templates/copilot-instructions.md +1 -1
- package/install/templates/multi-agent-autopilot-awake.plist.template +48 -0
- package/install/templates/multi-agent-autopilot.plist.template +12 -5
- package/install/unattended-profile-legacy.json +80 -0
- package/manifest.json +616 -483
- package/package.json +8 -3
- package/pipeline/agents/code-reviewer.md +10 -0
- package/pipeline/agents/plan-critic.md +98 -0
- package/pipeline/agents/security-auditor.md +10 -0
- package/pipeline/agents/task-clarifier.md +10 -0
- package/pipeline/commands/multi-agent/SKILL.md +2 -2
- package/pipeline/commands/multi-agent/analysis/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/analysis-jira/SKILL.md +12 -1
- package/pipeline/commands/multi-agent/analysis-resolve/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/autopilot/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/autopilot-off/SKILL.md +36 -6
- package/pipeline/commands/multi-agent/autopilot-on/SKILL.md +77 -12
- package/pipeline/commands/multi-agent/autopilot-status/SKILL.md +29 -8
- package/pipeline/commands/multi-agent/build-optimize/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/channels/SKILL.md +9 -9
- package/pipeline/commands/multi-agent/complaint-analysis/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/create-jira/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/design-check/SKILL.md +16 -11
- package/pipeline/commands/multi-agent/diff-explain/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/doctor/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/feedback/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/forget/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/garbage-collect/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/graph/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/help/SKILL.md +37 -39
- package/pipeline/commands/multi-agent/ios-coding-standard/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/issue/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/jira/SKILL.md +7 -1
- package/pipeline/commands/multi-agent/kill/SKILL.md +9 -3
- package/pipeline/commands/multi-agent/language/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/log/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/manual-test/SKILL.md +5 -0
- package/pipeline/commands/multi-agent/model/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/prune-logs/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/prune-prompts/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/purge/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/refactor/SKILL.md +5 -0
- package/pipeline/commands/multi-agent/research/SKILL.md +49 -0
- package/pipeline/commands/multi-agent/resume/SKILL.md +29 -6
- package/pipeline/commands/multi-agent/review/SKILL.md +28 -9
- package/pipeline/commands/multi-agent/review-analysis/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/review-issue/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/review-jira/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/route-off/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/route-on/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/route-status/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/routines/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/save/SKILL.md +6 -2
- package/pipeline/commands/multi-agent/scaffold/SKILL.md +47 -0
- package/pipeline/commands/multi-agent/scan/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/search/SKILL.md +12 -8
- package/pipeline/commands/multi-agent/security-review/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/serve/SKILL.md +62 -0
- package/pipeline/commands/multi-agent/setup/SKILL.md +27 -36
- package/pipeline/commands/multi-agent/stack/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/status/SKILL.md +10 -9
- package/pipeline/commands/multi-agent/steer/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/store-ready/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/sync/SKILL.md +13 -13
- package/pipeline/commands/multi-agent/test/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/test-accessibility/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/test-dark-mode/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/test-dynamic-type/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/test-screenshots/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/testflight-validation/SKILL.md +4 -0
- package/pipeline/commands/multi-agent/uninstall/SKILL.md +5 -1
- package/pipeline/commands/multi-agent/update/SKILL.md +4 -0
- package/pipeline/contract/CHANGELOG.md +74 -0
- package/pipeline/contract/README.md +126 -0
- package/pipeline/contract/build.mjs +427 -0
- package/pipeline/contract/fixtures/answer-result.json +11 -0
- package/pipeline/contract/fixtures/error-invalid-request.json +8 -0
- package/pipeline/contract/fixtures/error-unauthorized.json +5 -0
- package/pipeline/contract/fixtures/error-unsigned.json +5 -0
- package/pipeline/contract/fixtures/issues-empty.json +18 -0
- package/pipeline/contract/fixtures/launch-plan.json +31 -0
- package/pipeline/contract/fixtures/runs-awaiting-question.json +70 -0
- package/pipeline/contract/fixtures/runs-empty.json +6 -0
- package/pipeline/contract/fixtures/runs-failed.json +84 -0
- package/pipeline/contract/fixtures/runs-old-schema.json +70 -0
- package/pipeline/contract/fixtures/runs-pr-opened-redacted.json +101 -0
- package/pipeline/contract/fixtures/runs-pr-opened.json +106 -0
- package/pipeline/contract/fixtures/runs-running.json +84 -0
- package/pipeline/contract/fixtures/worktrees-empty.json +4 -0
- package/pipeline/contract/frozen/toolbox.json +107 -0
- package/pipeline/contract/manifest.json +263 -0
- package/pipeline/contract/types/index.d.ts +343 -0
- package/pipeline/lib/_jira-auth.sh +6 -2
- package/pipeline/lib/account-resolver.sh +1 -1
- package/pipeline/lib/autopilot-state.sh +19 -0
- package/pipeline/lib/context-link-extractor.sh +12 -5
- package/pipeline/lib/credential-inventory.sh +12 -5
- package/pipeline/lib/credential-store.sh +116 -185
- package/pipeline/lib/fetch-confluence.sh +44 -3
- package/pipeline/lib/fetch-document.sh +3 -4
- package/pipeline/lib/fetch-fortify.sh +1 -1
- package/pipeline/lib/figma-mcp-refresh.sh +2 -2
- package/pipeline/lib/figma-token.sh +5 -1
- package/pipeline/lib/issue-fetcher.sh +233 -16
- package/pipeline/lib/json-file-lock.mjs +172 -0
- package/pipeline/lib/model-dispatch.sh +21 -12
- package/pipeline/lib/model-rung.sh +6 -1
- package/pipeline/lib/multi-repo-pipeline.sh +1 -1
- package/pipeline/lib/outbound-gate.mjs +46 -16
- package/pipeline/lib/parse-complaints.sh +14 -7
- package/pipeline/lib/plan-todos.sh +3 -3
- package/pipeline/lib/post-pr-review.sh +9 -9
- package/pipeline/lib/pr-request-location.mjs +85 -0
- package/pipeline/lib/regular-file.mjs +153 -0
- package/pipeline/lib/repo-hygiene.sh +17 -0
- package/pipeline/lib/route-state.sh +5 -1
- package/pipeline/lib/run-paths.sh +3 -2
- package/pipeline/lib/stack-detect.sh +19 -1
- package/pipeline/lib/unattended-profile-check.mjs +178 -0
- package/pipeline/lib/unattended-settings-location.mjs +28 -0
- package/pipeline/lib/unattended.mjs +76 -0
- package/pipeline/lib/unattended.sh +32 -0
- package/pipeline/lib/untrusted.mjs +76 -0
- package/pipeline/lib/user-facing.mjs +82 -0
- package/pipeline/lib/user-facing.sh +58 -0
- package/pipeline/multi-agent-refs/_dev-context.md +1 -1
- package/pipeline/multi-agent-refs/analysis/evidence.md +4 -0
- package/pipeline/multi-agent-refs/analysis/locked.md +2 -2
- package/pipeline/multi-agent-refs/analysis/render.md +4 -3
- package/pipeline/multi-agent-refs/analysis/resolve.md +3 -1
- package/pipeline/multi-agent-refs/analysis/synthesis.md +4 -5
- package/pipeline/multi-agent-refs/analysis-template.md +10 -17
- package/pipeline/multi-agent-refs/channels/jira.md +1 -1
- package/pipeline/multi-agent-refs/component-dispatch.md +3 -3
- package/pipeline/multi-agent-refs/conventions-defaults.md +32 -32
- package/pipeline/multi-agent-refs/cross-cli-contract.md +10 -17
- package/pipeline/multi-agent-refs/features/autopilot-circuit-breaker.md +37 -9
- package/pipeline/multi-agent-refs/features/autopilot-operations.md +277 -0
- package/pipeline/multi-agent-refs/features/constitution.md +196 -0
- package/pipeline/multi-agent-refs/features/doctor.md +6 -3
- package/pipeline/multi-agent-refs/features/external-context-injection.md +6 -1
- package/pipeline/multi-agent-refs/features/jira-context.md +1 -1
- package/pipeline/multi-agent-refs/features/maturity-followup.md +43 -23
- package/pipeline/multi-agent-refs/features/model-fallback.md +9 -0
- package/pipeline/multi-agent-refs/features/phone-api.md +306 -0
- package/pipeline/multi-agent-refs/features/plan-critic.md +159 -0
- package/pipeline/multi-agent-refs/features/research.md +150 -0
- package/pipeline/multi-agent-refs/features/review-decision.md +185 -0
- package/pipeline/multi-agent-refs/features/scaffold.md +160 -0
- package/pipeline/multi-agent-refs/features/security-audit.md +6 -2
- package/pipeline/multi-agent-refs/features/skill-conformance.md +4 -1
- package/pipeline/multi-agent-refs/features/stack-adapters.md +116 -0
- package/pipeline/multi-agent-refs/features/unattended-gates.md +316 -0
- package/pipeline/multi-agent-refs/features/unattended-security.md +731 -0
- package/pipeline/multi-agent-refs/features/url-enrichment.md +2 -2
- package/pipeline/multi-agent-refs/features/usage-reporting.md +127 -31
- package/pipeline/multi-agent-refs/features/verify-by-test.md +1 -1
- package/pipeline/multi-agent-refs/features/visual-evidence.md +7 -7
- package/pipeline/multi-agent-refs/keychain.md +6 -11
- package/pipeline/multi-agent-refs/payload-contracts.md +2 -2
- package/pipeline/multi-agent-refs/phases/log-format.md +2 -3
- package/pipeline/multi-agent-refs/phases/modes.md +10 -12
- package/pipeline/multi-agent-refs/phases/operations.md +11 -5
- package/pipeline/multi-agent-refs/phases/phase-0-init.md +73 -81
- package/pipeline/multi-agent-refs/phases/phase-1-plan.md +22 -22
- package/pipeline/multi-agent-refs/phases/phase-2-dev.md +42 -66
- package/pipeline/multi-agent-refs/phases/phase-3-review.md +45 -79
- package/pipeline/multi-agent-refs/phases/phase-4-commit.md +28 -38
- package/pipeline/multi-agent-refs/phases/phase-5-report.md +27 -25
- package/pipeline/multi-agent-refs/phases.md +1 -1
- package/pipeline/multi-agent-refs/picker-contract.md +1 -1
- package/pipeline/multi-agent-refs/progress-contract.md +13 -16
- package/pipeline/multi-agent-refs/readiness-review.md +1 -1
- package/pipeline/multi-agent-refs/research/engine.md +91 -0
- package/pipeline/multi-agent-refs/rules.md +6 -4
- package/pipeline/multi-agent-refs/setup/repo-discovery.md +2 -2
- package/pipeline/multi-agent-refs/tracker-contract.md +2 -2
- package/pipeline/multi-agent-refs/unattended-contract.md +68 -16
- package/pipeline/rules/figma-pipeline.md +12 -12
- package/pipeline/schemas/agent-state.schema.json +480 -18
- package/pipeline/schemas/analysis-spec.schema.json +4 -4
- package/pipeline/schemas/answer-request.schema.json +24 -0
- package/pipeline/schemas/answer-result.schema.json +28 -0
- package/pipeline/schemas/autopilot-config.schema.json +111 -13
- package/pipeline/schemas/command-parameters.schema.json +99 -0
- package/pipeline/schemas/constitution.schema.json +56 -0
- package/pipeline/schemas/contract-error.schema.json +52 -0
- package/pipeline/schemas/design-check-config.schema.json +5 -1
- package/pipeline/schemas/issues.schema.json +61 -0
- package/pipeline/schemas/launch-plan.schema.json +46 -0
- package/pipeline/schemas/launch-request.schema.json +45 -0
- package/pipeline/schemas/launch.json +61 -0
- package/pipeline/schemas/launch.schema.json +84 -0
- package/pipeline/schemas/phases.json +2 -2
- package/pipeline/schemas/phases.schema.json +68 -0
- package/pipeline/schemas/phone-devices.schema.json +61 -0
- package/pipeline/schemas/phone-signed-request.schema.json +67 -0
- package/pipeline/schemas/plan-critique.schema.json +99 -0
- package/pipeline/schemas/plan-todos.schema.json +7 -7
- package/pipeline/schemas/planning-output.schema.json +5 -0
- package/pipeline/schemas/pr-request.schema.json +46 -0
- package/pipeline/schemas/prefs.schema.json +82 -7
- package/pipeline/schemas/research-output.schema.json +118 -0
- package/pipeline/schemas/review-file-exclusions.schema.json +25 -0
- package/pipeline/schemas/reviewer-output.schema.json +40 -4
- package/pipeline/schemas/run-questions.json +392 -0
- package/pipeline/schemas/run-questions.schema.json +118 -0
- package/pipeline/schemas/runs-index.schema.json +189 -0
- package/pipeline/schemas/scaffold-manifest.schema.json +43 -0
- package/pipeline/schemas/secret-patterns.schema.json +28 -0
- package/pipeline/schemas/stack-adapters.json +527 -0
- package/pipeline/schemas/stack-adapters.schema.json +184 -0
- package/pipeline/schemas/token-budget.json +1 -1
- package/pipeline/schemas/token-budget.schema.json +26 -0
- package/pipeline/schemas/triage-output.schema.json +64 -3
- package/pipeline/schemas/unattended-policy.json +139 -0
- package/pipeline/schemas/unattended-policy.schema.json +73 -0
- package/pipeline/schemas/unattended-profile.json +248 -0
- package/pipeline/schemas/unattended-profile.schema.json +198 -0
- package/pipeline/schemas/worktrees.schema.json +51 -0
- package/pipeline/scripts/README.md +1 -0
- package/pipeline/scripts/_autopilot-config.mjs +130 -0
- package/pipeline/scripts/_autopilot-ops.mjs +567 -0
- package/pipeline/scripts/_autopilot-outcomes.mjs +174 -0
- package/pipeline/scripts/_command-contract.mjs +384 -0
- package/pipeline/scripts/_cost.mjs +40 -0
- package/pipeline/scripts/_notices.mjs +160 -0
- package/pipeline/scripts/_phone-auth.mjs +485 -0
- package/pipeline/scripts/_pre-existing.mjs +294 -0
- package/pipeline/scripts/_redact.mjs +77 -0
- package/pipeline/scripts/_run-paths.mjs +4 -2
- package/pipeline/scripts/_stack-adapter.mjs +678 -0
- package/pipeline/scripts/_stack-routing.mjs +1 -1
- package/pipeline/scripts/agent-guard.py +348 -37
- package/pipeline/scripts/agent-guard.sh +41 -13
- package/pipeline/scripts/analysis-story-tree.mjs +79 -3
- package/pipeline/scripts/answer-question.mjs +181 -0
- package/pipeline/scripts/audit-log-rotate.sh +1 -4
- package/pipeline/scripts/audit-log.sh +4 -4
- package/pipeline/scripts/autopilot-arming.mjs +389 -21
- package/pipeline/scripts/autopilot-awake.mjs +255 -0
- package/pipeline/scripts/autopilot-intake.mjs +137 -36
- package/pipeline/scripts/autopilot-menubar.swift +156 -44
- package/pipeline/scripts/autopilot-publish.mjs +1625 -0
- package/pipeline/scripts/autopilot-runner.mjs +1678 -222
- package/pipeline/scripts/autopilot-status.sh +198 -33
- package/pipeline/scripts/build-lock.sh +120 -0
- package/pipeline/scripts/build-references.mjs +4 -1
- package/pipeline/scripts/build-stack-plugins.mjs +59 -22
- package/pipeline/scripts/capture-flush.sh +1 -1
- package/pipeline/scripts/capture-resume.sh +13 -9
- package/pipeline/scripts/check-derived-drift.mjs +52 -11
- package/pipeline/scripts/commands.mjs +88 -0
- package/pipeline/scripts/constitution.mjs +362 -0
- package/pipeline/scripts/contract-server.mjs +776 -0
- package/pipeline/scripts/cost-analyze.mjs +89 -39
- package/pipeline/scripts/diff-explain.mjs +12 -1
- package/pipeline/scripts/doctor.mjs +77 -28
- package/pipeline/scripts/evidence-gate.mjs +192 -12
- package/pipeline/scripts/feedback-send.mjs +4 -2
- package/pipeline/scripts/gate-ledger.mjs +449 -0
- package/pipeline/scripts/gc-abandoned.sh +132 -13
- package/pipeline/scripts/gen-facts.mjs +31 -15
- package/pipeline/scripts/gen-mode-dispatch.mjs +3 -3
- package/pipeline/scripts/github-ssh-setup.sh +140 -29
- package/pipeline/scripts/graph-mermaid.mjs +4 -1
- package/pipeline/scripts/issues.mjs +236 -0
- package/pipeline/scripts/jira-attach.sh +6 -2
- package/pipeline/scripts/jira-search.sh +4 -3
- package/pipeline/scripts/keychain-save.sh +125 -24
- package/pipeline/scripts/keychain.py +63 -93
- package/pipeline/scripts/launch-request.mjs +747 -0
- package/pipeline/scripts/localize-commands.mjs +4 -10
- package/pipeline/scripts/log-metric.sh +6 -5
- package/pipeline/scripts/maturity-followup.mjs +13 -4
- package/pipeline/scripts/memory-save.sh +25 -0
- package/pipeline/scripts/migrate-prefs.mjs +4 -3
- package/pipeline/scripts/open-questions-gate.mjs +276 -0
- package/pipeline/scripts/phase-tracker.sh +41 -27
- package/pipeline/scripts/phase0-exit-gate.mjs +22 -4
- package/pipeline/scripts/phone-devices.mjs +224 -0
- package/pipeline/scripts/plan-coverage-gate.mjs +200 -66
- package/pipeline/scripts/plan-critique-gate.mjs +591 -0
- package/pipeline/scripts/pr-request.mjs +188 -0
- package/pipeline/scripts/pre-commit-check.sh +115 -4
- package/pipeline/scripts/probe-evidence-capability.sh +44 -5
- package/pipeline/scripts/record-phase.mjs +71 -0
- package/pipeline/scripts/render-agent-log-cost.sh +17 -2
- package/pipeline/scripts/render-cost-summary.sh +1 -1
- package/pipeline/scripts/render-work-summary.sh +1 -1
- package/pipeline/scripts/require-supported-version.sh +4 -1
- package/pipeline/scripts/research-gate.mjs +704 -0
- package/pipeline/scripts/review-decision-gate.mjs +403 -0
- package/pipeline/scripts/routine-registry.mjs +5 -2
- package/pipeline/scripts/runs-index.mjs +135 -27
- package/pipeline/scripts/scaffold-gate.mjs +393 -0
- package/pipeline/scripts/skill-conformance.mjs +25 -8
- package/pipeline/scripts/skill-siblings.mjs +2 -1
- package/pipeline/scripts/smoke-cross-cli-behavior.sh +32 -6
- package/pipeline/scripts/smoke-schema-validation.sh +6 -2
- package/pipeline/scripts/spec-consistency-gate.mjs +469 -0
- package/pipeline/scripts/symbol-existence-gate.mjs +450 -0
- package/pipeline/scripts/test-gap-scan.mjs +40 -2
- package/pipeline/scripts/test-integrity-gate.mjs +20 -4
- package/pipeline/scripts/test-strength.mjs +484 -0
- package/pipeline/scripts/test-summary.mjs +651 -0
- package/pipeline/scripts/triage-memory.mjs +49 -9
- package/pipeline/scripts/unattended_policy.py +2786 -0
- package/pipeline/scripts/uninstall.mjs +10 -10
- package/pipeline/scripts/update-issue-progress.sh +1 -1
- package/pipeline/scripts/usage-identity.mjs +288 -0
- package/pipeline/scripts/usage-register.mjs +185 -63
- package/pipeline/scripts/usage-report.mjs +230 -66
- package/pipeline/scripts/validate-complaint-doc.mjs +28 -10
- package/pipeline/scripts/validate-planning.mjs +6 -0
- package/pipeline/scripts/verify-citations.mjs +151 -38
- package/pipeline/scripts/verify.mjs +58 -18
- package/pipeline/scripts/worktree-prepare.sh +126 -0
- package/pipeline/scripts/worktrees.mjs +124 -0
- package/pipeline/scripts/write-state.mjs +48 -17
- package/pipeline/skills/.skill-manifest.json +222 -226
- package/pipeline/skills/.skills-index.json +77 -88
- package/pipeline/skills/shared/README.md +44 -45
- package/pipeline/skills/shared/core/apple-archive-compliance/SKILL.md +3 -2
- package/pipeline/skills/shared/core/google-play-compliance/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent/SKILL.md +10 -9
- package/pipeline/skills/shared/core/multi-agent-analysis/SKILL.md +6 -5
- package/pipeline/skills/shared/core/multi-agent-analysis-jira/SKILL.md +11 -3
- package/pipeline/skills/shared/core/multi-agent-analysis-resolve/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-autopilot/SKILL.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-autopilot-off/SKILL.md +32 -6
- package/pipeline/skills/shared/core/multi-agent-autopilot-on/SKILL.md +66 -8
- package/pipeline/skills/shared/core/multi-agent-autopilot-status/SKILL.md +27 -9
- package/pipeline/skills/shared/core/multi-agent-build-optimize/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-channels/SKILL.md +7 -4
- package/pipeline/skills/shared/core/multi-agent-complaint-analysis/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent-create-jira/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-design-check/SKILL.md +7 -5
- package/pipeline/skills/shared/core/multi-agent-diff-explain/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-doctor/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent-feedback/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-forget/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-garbage-collect/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-graph/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-help/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-ios-coding-standard/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-issue/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent-jira/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-kill/SKILL.md +7 -4
- package/pipeline/skills/shared/core/multi-agent-language/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-log/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-manual-test/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-model/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-prune-logs/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-prune-prompts/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-purge/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-refactor/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-research/SKILL.md +35 -0
- package/pipeline/skills/shared/core/multi-agent-resume/SKILL.md +7 -3
- package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +7 -5
- package/pipeline/skills/shared/core/multi-agent-review-analysis/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-review-issue/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent-review-jira/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-route-off/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-route-on/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-route-status/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-routines/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-save/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent-scaffold/SKILL.md +30 -0
- package/pipeline/skills/shared/core/multi-agent-scan/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-search/SKILL.md +4 -3
- package/pipeline/skills/shared/core/multi-agent-security-review/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-serve/SKILL.md +60 -0
- package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +8 -8
- package/pipeline/skills/shared/core/multi-agent-stack/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-status/SKILL.md +7 -8
- package/pipeline/skills/shared/core/multi-agent-steer/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-store-ready/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +10 -10
- package/pipeline/skills/shared/core/multi-agent-test/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-test-accessibility/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-test-dark-mode/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-test-dynamic-type/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-test-screenshots/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-testflight-validation/SKILL.md +2 -1
- package/pipeline/skills/shared/core/multi-agent-uninstall/SKILL.md +3 -2
- package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +2 -1
- package/pipeline/skills/shared/external/NOTICE-avdlee-swiftui-agent-skill.md +46 -0
- package/pipeline/skills/shared/external/NOTICE-dimillian-skills.md +9 -3
- package/pipeline/skills/shared/external/NOTICE-paul-hudson-skills.md +54 -0
- package/pipeline/skills/shared/external/NOTICE-swift-ios-skills.md +9 -11
- package/pipeline/skills/shared/external/NOTICE-vibeship-spawner-skills.md +203 -0
- package/pipeline/skills/shared/external/NOTICE-xcode-build-skills.md +10 -3
- package/pipeline/skills/shared/external/accessibility-compliance-accessibility-audit/SKILL.md +131 -25
- package/pipeline/skills/shared/external/agent-introspection-debugging/SKILL.md +4 -3
- package/pipeline/skills/shared/external/alarmkit/SKILL.md +2 -1
- package/pipeline/skills/shared/external/android-architecture/SKILL.md +85 -76
- package/pipeline/skills/shared/external/android-build-quality-gates/SKILL.md +51 -3
- package/pipeline/skills/shared/external/android-build-quality-gates/references/patterns.md +9 -0
- package/pipeline/skills/shared/external/android-datastore/SKILL.md +4 -3
- package/pipeline/skills/shared/external/android-design-tokens-codegen/SKILL.md +4 -3
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/SKILL.md +106 -181
- package/pipeline/skills/shared/external/android-jetpack-compose-expert/references/patterns.md +147 -0
- package/pipeline/skills/shared/external/android-mvi-viewmodel/SKILL.md +45 -4
- package/pipeline/skills/shared/external/android-performance/SKILL.md +27 -2
- package/pipeline/skills/shared/external/android-performance/references/patterns.md +37 -37
- package/pipeline/skills/shared/external/api-patterns/SKILL.md +111 -67
- package/pipeline/skills/shared/external/api-patterns/references/contract-details.md +128 -0
- package/pipeline/skills/shared/external/api-security-best-practices/SKILL.md +136 -186
- package/pipeline/skills/shared/external/api-security-best-practices/references/abuse-controls.md +140 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/identity-and-access.md +207 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/operations.md +66 -0
- package/pipeline/skills/shared/external/api-security-best-practices/references/request-handling.md +175 -0
- package/pipeline/skills/shared/external/app-clips/SKILL.md +2 -0
- package/pipeline/skills/shared/external/app-intents/SKILL.md +2 -0
- package/pipeline/skills/shared/external/app-store-changelog/SKILL.md +4 -3
- package/pipeline/skills/shared/external/app-store-optimization/SKILL.md +2 -0
- package/pipeline/skills/shared/external/app-store-review/SKILL.md +2 -0
- package/pipeline/skills/shared/external/apple-on-device-ai/SKILL.md +2 -0
- package/pipeline/skills/shared/external/architecture/SKILL.md +106 -39
- package/pipeline/skills/shared/external/architecture/references/adr-and-review.md +76 -0
- package/pipeline/skills/shared/external/authentication/SKILL.md +2 -0
- package/pipeline/skills/shared/external/avkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/background-processing/SKILL.md +2 -1
- package/pipeline/skills/shared/external/backlog/SKILL.md +3 -2
- package/pipeline/skills/shared/external/callkit-voip/SKILL.md +2 -0
- package/pipeline/skills/shared/external/callkit-voip/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/ci-cd-pipelines/SKILL.md +3 -2
- package/pipeline/skills/shared/external/clean-code/SKILL.md +192 -90
- package/pipeline/skills/shared/external/cloudkit-sync/SKILL.md +2 -1
- package/pipeline/skills/shared/external/cloudkit-sync/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/compose-components/SKILL.md +4 -4
- package/pipeline/skills/shared/external/compose-navigation/SKILL.md +22 -22
- package/pipeline/skills/shared/external/compose-navigation/references/patterns.md +10 -10
- package/pipeline/skills/shared/external/compose-testing/SKILL.md +13 -6
- package/pipeline/skills/shared/external/compose-testing/references/patterns.md +59 -59
- package/pipeline/skills/shared/external/contacts-framework/SKILL.md +2 -0
- package/pipeline/skills/shared/external/context-compression/SKILL.md +118 -250
- package/pipeline/skills/shared/external/core-bluetooth/SKILL.md +2 -0
- package/pipeline/skills/shared/external/core-data/SKILL.md +2 -0
- package/pipeline/skills/shared/external/core-motion/SKILL.md +2 -0
- package/pipeline/skills/shared/external/core-nfc/SKILL.md +2 -0
- package/pipeline/skills/shared/external/coreml/SKILL.md +2 -1
- package/pipeline/skills/shared/external/council/SKILL.md +2 -1
- package/pipeline/skills/shared/external/cryptokit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/css-modern/SKILL.md +3 -2
- package/pipeline/skills/shared/external/database-patterns/SKILL.md +3 -2
- package/pipeline/skills/shared/external/debugging-instruments/SKILL.md +2 -0
- package/pipeline/skills/shared/external/debugging-strategies/SKILL.md +130 -20
- package/pipeline/skills/shared/external/debugging-strategies/references/hard-cases.md +54 -0
- package/pipeline/skills/shared/external/device-integrity/SKILL.md +2 -0
- package/pipeline/skills/shared/external/docker-expert/SKILL.md +137 -381
- package/pipeline/skills/shared/external/docker-expert/references/patterns.md +98 -0
- package/pipeline/skills/shared/external/energykit/SKILL.md +2 -1
- package/pipeline/skills/shared/external/eventkit-calendar/SKILL.md +3 -1
- package/pipeline/skills/shared/external/eventkit-calendar/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/fastapi-pro/SKILL.md +116 -185
- package/pipeline/skills/shared/external/fastapi-pro/references/app-structure.md +235 -0
- package/pipeline/skills/shared/external/fastapi-pro/references/testing-and-deployment.md +69 -0
- package/pipeline/skills/shared/external/firebase/SKILL.md +4 -3
- package/pipeline/skills/shared/external/github-actions-templates/SKILL.md +151 -293
- package/pipeline/skills/shared/external/github-actions-templates/references/patterns.md +96 -0
- package/pipeline/skills/shared/external/healthkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/hig-components-content/SKILL.md +69 -72
- package/pipeline/skills/shared/external/hig-components-content/references/content-views.md +111 -0
- package/pipeline/skills/shared/external/hig-components-layout/SKILL.md +77 -86
- package/pipeline/skills/shared/external/hig-components-layout/references/containers.md +98 -0
- package/pipeline/skills/shared/external/hig-components-status/SKILL.md +150 -78
- package/pipeline/skills/shared/external/hig-components-system/SKILL.md +76 -97
- package/pipeline/skills/shared/external/hig-components-system/references/surfaces.md +98 -0
- package/pipeline/skills/shared/external/hig-foundations/SKILL.md +48 -76
- package/pipeline/skills/shared/external/hig-foundations/references/foundations-detail.md +132 -0
- package/pipeline/skills/shared/external/hig-inputs/SKILL.md +147 -106
- package/pipeline/skills/shared/external/hig-patterns/SKILL.md +61 -77
- package/pipeline/skills/shared/external/hig-patterns/references/patterns.md +92 -0
- package/pipeline/skills/shared/external/hig-platforms/SKILL.md +147 -77
- package/pipeline/skills/shared/external/hig-technologies/SKILL.md +61 -121
- package/pipeline/skills/shared/external/hig-technologies/references/technologies.md +108 -0
- package/pipeline/skills/shared/external/homekit-matter/SKILL.md +2 -0
- package/pipeline/skills/shared/external/homekit-matter/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/html-semantic/SKILL.md +3 -2
- package/pipeline/skills/shared/external/humanizer/SKILL.md +2 -1
- package/pipeline/skills/shared/external/ios-accessibility/SKILL.md +2 -0
- package/pipeline/skills/shared/external/ios-coding-standard/SKILL.md +2 -1
- package/pipeline/skills/shared/external/ios-coding-standard/references/STANDARD.md +14 -2
- package/pipeline/skills/shared/external/ios-coding-standard/references/rules.yml +2 -2
- package/pipeline/skills/shared/external/ios-debugger-agent/SKILL.md +4 -3
- package/pipeline/skills/shared/external/ios-localization/SKILL.md +8 -6
- package/pipeline/skills/shared/external/ios-localization/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/ios-localization/references/string-catalogs.md +11 -11
- package/pipeline/skills/shared/external/ios-module-structure/SKILL.md +2 -1
- package/pipeline/skills/shared/external/ios-networking/SKILL.md +2 -0
- package/pipeline/skills/shared/external/ios-simulator/SKILL.md +2 -0
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/SKILL.md +102 -193
- package/pipeline/skills/shared/external/kotlin-coroutines-expert/references/patterns.md +103 -0
- package/pipeline/skills/shared/external/live-activities/SKILL.md +4 -2
- package/pipeline/skills/shared/external/live-activities/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/localization-reuse-map/SKILL.md +7 -9
- package/pipeline/skills/shared/external/localization-reuse-map/reference/sources-and-recipes.md +3 -3
- package/pipeline/skills/shared/external/localization-reuse-map/scripts/resolve-new-values.py +2 -2
- package/pipeline/skills/shared/external/macos-menubar-tuist-app/SKILL.md +4 -3
- package/pipeline/skills/shared/external/macos-spm-app-packaging/SKILL.md +5 -3
- package/pipeline/skills/shared/external/mapkit-location/SKILL.md +2 -0
- package/pipeline/skills/shared/external/mapkit-location/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/metrickit-diagnostics/SKILL.md +2 -0
- package/pipeline/skills/shared/external/metrickit-diagnostics/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/monorepo-architect/SKILL.md +149 -46
- package/pipeline/skills/shared/external/monorepo-architect/references/patterns.md +70 -0
- package/pipeline/skills/shared/external/musickit-audio/SKILL.md +2 -0
- package/pipeline/skills/shared/external/musickit-audio/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/natural-language/SKILL.md +2 -0
- package/pipeline/skills/shared/external/nextjs-app-router/SKILL.md +3 -2
- package/pipeline/skills/shared/external/nodejs-backend-patterns/SKILL.md +91 -21
- package/pipeline/skills/shared/external/nodejs-backend-patterns/references/runtime-patterns.md +247 -0
- package/pipeline/skills/shared/external/nodejs-backend-patterns/references/testing-and-frameworks.md +57 -0
- package/pipeline/skills/shared/external/observability-engineer/SKILL.md +155 -232
- package/pipeline/skills/shared/external/observability-engineer/references/patterns.md +75 -0
- package/pipeline/skills/shared/external/passkit-wallet/SKILL.md +2 -0
- package/pipeline/skills/shared/external/passkit-wallet/evals/evals.json +2 -2
- package/pipeline/skills/shared/external/passkit-wallet/references/wallet-passes.md +18 -18
- package/pipeline/skills/shared/external/pdfkit/SKILL.md +2 -1
- package/pipeline/skills/shared/external/pencilkit-drawing/SKILL.md +2 -0
- package/pipeline/skills/shared/external/pencilkit-drawing/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/permissionkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/photos-camera-media/SKILL.md +2 -1
- package/pipeline/skills/shared/external/push-notifications/SKILL.md +2 -0
- package/pipeline/skills/shared/external/python-patterns/SKILL.md +3 -2
- package/pipeline/skills/shared/external/react-best-practices/SKILL.md +3 -2
- package/pipeline/skills/shared/external/realitykit-ar/SKILL.md +2 -2
- package/pipeline/skills/shared/external/realitykit-ar/evals/evals.json +1 -1
- package/pipeline/skills/shared/external/rest-api-design/SKILL.md +3 -2
- package/pipeline/skills/shared/external/retrofit-networking/SKILL.md +7 -7
- package/pipeline/skills/shared/external/retrofit-networking/references/patterns.md +69 -69
- package/pipeline/skills/shared/external/room-database/references/patterns.md +126 -126
- package/pipeline/skills/shared/external/search-first/SKILL.md +2 -1
- package/pipeline/skills/shared/external/security-review/SKILL.md +4 -3
- package/pipeline/skills/shared/external/shareplay-activities/SKILL.md +2 -2
- package/pipeline/skills/shared/external/signal-community/SKILL.md +1 -1
- package/pipeline/skills/shared/external/skill-creator/SKILL.md +2 -1
- package/pipeline/skills/shared/external/speech-recognition/SKILL.md +2 -1
- package/pipeline/skills/shared/external/spm-build-analysis/SKILL.md +2 -0
- package/pipeline/skills/shared/external/storekit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swift-api-design-guidelines/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swift-architecture/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swift-charts/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swift-codable/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swift-concurrency/SKILL.md +3 -1
- package/pipeline/skills/shared/external/swift-concurrency-expert/SKILL.md +5 -4
- package/pipeline/skills/shared/external/swift-concurrency-pro/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swift-formatstyle/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swift-language/SKILL.md +3 -1
- package/pipeline/skills/shared/external/swift-security/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swift-testing/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swift-testing-pro/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swift-testing-pro/references/new-features.md +10 -0
- package/pipeline/skills/shared/external/swiftdata/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftdata-pro/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swiftlint/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-animation/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-expert-skill/SKILL.md +3 -1
- package/pipeline/skills/shared/external/swiftui-gestures/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-layout-components/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-liquid-glass/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-navigation/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-patterns/SKILL.md +3 -1
- package/pipeline/skills/shared/external/swiftui-performance/SKILL.md +3 -1
- package/pipeline/skills/shared/external/swiftui-performance-audit/SKILL.md +5 -4
- package/pipeline/skills/shared/external/swiftui-pro/SKILL.md +2 -1
- package/pipeline/skills/shared/external/swiftui-ui-patterns/SKILL.md +5 -4
- package/pipeline/skills/shared/external/swiftui-uikit-interop/SKILL.md +2 -0
- package/pipeline/skills/shared/external/swiftui-view-refactor/SKILL.md +4 -3
- package/pipeline/skills/shared/external/swiftui-webkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/tailwind-css/SKILL.md +3 -2
- package/pipeline/skills/shared/external/testing-backend/SKILL.md +3 -2
- package/pipeline/skills/shared/external/tipkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/typescript-patterns/SKILL.md +3 -2
- package/pipeline/skills/shared/external/vision-framework/SKILL.md +2 -0
- package/pipeline/skills/shared/external/vue-composition/SKILL.md +3 -2
- package/pipeline/skills/shared/external/weatherkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/web-accessibility/SKILL.md +3 -2
- package/pipeline/skills/shared/external/web-performance/SKILL.md +3 -2
- package/pipeline/skills/shared/external/web-testing/SKILL.md +3 -2
- package/pipeline/skills/shared/external/widgetkit/SKILL.md +2 -0
- package/pipeline/skills/shared/external/widgetkit/references/widgetkit-advanced.md +1 -1
- package/pipeline/skills/shared/external/xcode-build-benchmark/SKILL.md +2 -0
- package/pipeline/skills/shared/external/xcode-build-fixer/SKILL.md +2 -0
- package/pipeline/skills/shared/external/xcode-build-orchestrator/SKILL.md +2 -0
- package/pipeline/skills/shared/external/xcode-compilation-analyzer/SKILL.md +2 -0
- package/pipeline/skills/shared/external/xcode-project-analyzer/SKILL.md +2 -0
- package/pipeline/skills/skills-index.md +40 -41
- package/docs/token-budget-history.md +0 -24
- package/pipeline/skills/shared/external/agentflow/SKILL.md +0 -199
- package/pipeline/skills/shared/external/android-ui-verification/SKILL.md +0 -66
- package/pipeline/skills/shared/external/api-security-best-practices/references/auth.md +0 -299
- package/pipeline/skills/shared/external/api-security-best-practices/references/input-validation.md +0 -255
- package/pipeline/skills/shared/external/api-security-best-practices/references/rate-limiting.md +0 -167
- package/pipeline/skills/shared/external/closed-loop-delivery/SKILL.md +0 -116
- package/pipeline/skills/shared/external/ios-developer/SKILL.md +0 -216
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @file contract-server.mjs - the pipeline's client contract over HTTP, on
|
|
4
|
+
* this machine only, for as long as someone is using it.
|
|
5
|
+
*
|
|
6
|
+
* The contract has two carriers. The first is the CLI: runs-index.mjs,
|
|
7
|
+
* commands.mjs, issues.mjs, worktrees.mjs, launch-request.mjs and
|
|
8
|
+
* answer-question.mjs print JSON a client decodes. This is the second, for a
|
|
9
|
+
* client that would rather hold a socket than spawn a process per read. It
|
|
10
|
+
* adds no behaviour: every route calls the function the matching CLI calls
|
|
11
|
+
* and serializes it the same way, so for the same input the body is the same
|
|
12
|
+
* bytes the CLI prints (test/contract-parity.test.mjs holds that).
|
|
13
|
+
*
|
|
14
|
+
* Routes (pipeline/contract/manifest.json lists them with their schemas):
|
|
15
|
+
*
|
|
16
|
+
* GET /v1/runs runs-index.mjs --json [--group G] [--redact]
|
|
17
|
+
* GET /v1/runs/{id} runs-index.mjs --json --task-id {id} [--redact]
|
|
18
|
+
* GET /v1/commands commands.mjs --json
|
|
19
|
+
* GET /v1/questions launch-request.mjs questions
|
|
20
|
+
* GET /v1/issues issues.mjs --json [--source S] [--project K]... [--max N]
|
|
21
|
+
* GET /v1/worktrees worktrees.mjs --json [--measure] [--redact]
|
|
22
|
+
* GET /v1/launch-spec launch-request.mjs spec
|
|
23
|
+
* POST /v1/launch?repo=P write a launch request, return launch-request.mjs plan
|
|
24
|
+
* POST /v1/runs/{id}/answer answer-question.mjs --question Q --answer IDS
|
|
25
|
+
*
|
|
26
|
+
* Phone routes, the same handlers behind a device signature instead of the
|
|
27
|
+
* bearer token, always redacted (features/phone-api.md):
|
|
28
|
+
*
|
|
29
|
+
* GET /v1/phone/runs scope read
|
|
30
|
+
* GET /v1/phone/runs/{id} scope read
|
|
31
|
+
* POST /v1/phone/runs/{id}/answer scope answer
|
|
32
|
+
* POST /v1/phone/launch?repo=P scope launch, and only after `phone-devices.mjs launch on`
|
|
33
|
+
*
|
|
34
|
+
* There is deliberately no route that merges, marks ready, runs a command or
|
|
35
|
+
* starts a host. POST /v1/launch writes the request file and returns the argv
|
|
36
|
+
* and environment launch.json declares; the client starts the process, and
|
|
37
|
+
* nothing here ever spawns `claude`.
|
|
38
|
+
*
|
|
39
|
+
* Security model. The listener binds 127.0.0.1 and nothing else: any other
|
|
40
|
+
* --host, 0.0.0.0 and :: included, is refused at startup. Each start mints a
|
|
41
|
+
* random token (32 bytes from crypto.randomBytes) and writes it to a 0600 file
|
|
42
|
+
* under ~/.claude/logs/multi-agent/contract-server/ (0700); --print-token also
|
|
43
|
+
* puts it on the ready line for a parent that spawned the server itself. Every
|
|
44
|
+
* request is checked in this order:
|
|
45
|
+
*
|
|
46
|
+
* 1. Host must be 127.0.0.1:<port> or localhost:<port>, which is what stops
|
|
47
|
+
* a DNS-rebound page from reaching the socket under another name (403);
|
|
48
|
+
* 2. an Origin header must be one of --allow-origin; without the flag no
|
|
49
|
+
* Origin is accepted and no CORS header is ever sent (403);
|
|
50
|
+
* 3. an allowed preflight is answered, before authentication, since a
|
|
51
|
+
* browser sends no credentials on one (204);
|
|
52
|
+
* 4. a path under /v1/phone/ leaves here for the device-signature checks in
|
|
53
|
+
* _phone-auth.mjs (401 per refusal, then 404 unknown-verb, 403
|
|
54
|
+
* forbidden-scope); the bearer token is not consulted there, and a
|
|
55
|
+
* signature is not consulted anywhere else;
|
|
56
|
+
* 5. `Authorization: Bearer <token>`, compared in constant time over SHA-256
|
|
57
|
+
* digests so neither content nor length leaks through timing (401);
|
|
58
|
+
* 6. the route, its method and its query parameters, each validated (404,
|
|
59
|
+
* 405, 400); a POST body must be application/json (415), at most 64 KiB,
|
|
60
|
+
* and valid against the route's schema (400).
|
|
61
|
+
*
|
|
62
|
+
* `?redact=1` is `--redact`. A request that carries an Origin is a browser
|
|
63
|
+
* page, which can relay what it reads anywhere, so it is always served
|
|
64
|
+
* redacted whatever the query says.
|
|
65
|
+
*
|
|
66
|
+
* Lifetime. The server exits after --idle-minutes without a request (15 by
|
|
67
|
+
* default) and on SIGTERM, SIGINT or SIGHUP, removing its token file either
|
|
68
|
+
* way. It is started on demand by `/multi-agent:serve` or by a client, and is
|
|
69
|
+
* never installed as a service.
|
|
70
|
+
*
|
|
71
|
+
* Zero runtime dependencies (ADR-0004): node:http, node:crypto and the
|
|
72
|
+
* producers' own modules.
|
|
73
|
+
*
|
|
74
|
+
* Usage:
|
|
75
|
+
* node contract-server.mjs [--port N] [--idle-minutes N] [--allow-origin <origin>]... [--print-token]
|
|
76
|
+
*
|
|
77
|
+
* Stdout: one JSON line once listening - {contractVersion, url, port, pid,
|
|
78
|
+
* tokenFile, idleMinutes[, token]}.
|
|
79
|
+
* Exit: 0 stopped (idle or signal), 1 could not listen, 2 usage error.
|
|
80
|
+
*
|
|
81
|
+
* @module pipeline/scripts/contract-server
|
|
82
|
+
*/
|
|
83
|
+
|
|
84
|
+
import { Buffer } from "node:buffer";
|
|
85
|
+
import { createHash, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
|
|
86
|
+
import { chmodSync, existsSync, mkdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
87
|
+
import { createServer } from "node:http";
|
|
88
|
+
import { homedir } from "node:os";
|
|
89
|
+
import { isAbsolute, join } from "node:path";
|
|
90
|
+
import { clearTimeout, setTimeout } from "node:timers";
|
|
91
|
+
import { answerStateFile } from "./answer-question.mjs";
|
|
92
|
+
import { catalog } from "./commands.mjs";
|
|
93
|
+
import { collect } from "./issues.mjs";
|
|
94
|
+
import {
|
|
95
|
+
launchRepoAllowed,
|
|
96
|
+
launchSpec,
|
|
97
|
+
planLaunch,
|
|
98
|
+
questionsDocument,
|
|
99
|
+
requestsDir,
|
|
100
|
+
workingTreeState,
|
|
101
|
+
writeRequestFile,
|
|
102
|
+
} from "./launch-request.mjs";
|
|
103
|
+
import { GROUP_NAMES, envelope, selectRuns } from "./runs-index.mjs";
|
|
104
|
+
import {
|
|
105
|
+
PHONE_PREFIX,
|
|
106
|
+
appendAudit,
|
|
107
|
+
createNonceStore,
|
|
108
|
+
noncesPath,
|
|
109
|
+
readRegistry,
|
|
110
|
+
verbOf,
|
|
111
|
+
verifyRequest,
|
|
112
|
+
} from "./_phone-auth.mjs";
|
|
113
|
+
import { buildList } from "./worktrees.mjs";
|
|
114
|
+
import { runMain } from "../lib/fatal.mjs";
|
|
115
|
+
import { invokedDirectly } from "../lib/invoked-directly.mjs";
|
|
116
|
+
|
|
117
|
+
/** The contract as a whole; pipeline/contract/manifest.json carries the same. */
|
|
118
|
+
export const CONTRACT_VERSION = "1.2.0";
|
|
119
|
+
/** The majors `?v=` accepts. A new major keeps the previous one here for one minor. */
|
|
120
|
+
export const SUPPORTED_MAJORS = [1];
|
|
121
|
+
export const BIND_HOST = "127.0.0.1";
|
|
122
|
+
export const MAX_BODY_BYTES = 64 * 1024;
|
|
123
|
+
/** How much of an oversized body is drained before the socket is dropped. */
|
|
124
|
+
const MAX_DRAIN_BYTES = 1024 * 1024;
|
|
125
|
+
const DEFAULT_IDLE_MINUTES = 15;
|
|
126
|
+
const MAX_IDLE_MINUTES = 24 * 60;
|
|
127
|
+
const RUN_ID = /^[A-Za-z0-9#][A-Za-z0-9#._-]{0,127}$/;
|
|
128
|
+
const PROJECT_KEY = /^[A-Z][A-Z0-9_]*$/;
|
|
129
|
+
const QUESTION_ID = /^[a-z][a-z0-9-]*$/;
|
|
130
|
+
const OPTION_ID = /^[a-z0-9][a-z0-9+_-]*$/;
|
|
131
|
+
|
|
132
|
+
/** Where token files live. Fixed under HOME, never under LOGS_ROOT. */
|
|
133
|
+
export function tokenDir() {
|
|
134
|
+
return join(homedir(), ".claude", "logs", "multi-agent", "contract-server");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The exact bytes a CLI prints for the same object. */
|
|
138
|
+
export const serialize = (obj) => JSON.stringify(obj, null, 2) + "\n";
|
|
139
|
+
|
|
140
|
+
class HttpError extends Error {
|
|
141
|
+
constructor(status, code, message, extra = {}) {
|
|
142
|
+
super(message);
|
|
143
|
+
this.status = status;
|
|
144
|
+
this.code = code;
|
|
145
|
+
this.extra = extra;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const flag = (v) => v === "1";
|
|
150
|
+
const one = (q, name) => (q[name] === undefined ? null : q[name][0]);
|
|
151
|
+
|
|
152
|
+
/** Query-parameter rules: name -> {repeat?, check(value) -> ok}. */
|
|
153
|
+
const Q = {
|
|
154
|
+
redact: { check: (v) => v === "0" || v === "1" },
|
|
155
|
+
group: { check: (v) => GROUP_NAMES.includes(v) },
|
|
156
|
+
measure: { check: (v) => v === "0" || v === "1" },
|
|
157
|
+
source: { check: (v) => ["jira", "github", "all"].includes(v) },
|
|
158
|
+
project: { repeat: true, check: (v) => PROJECT_KEY.test(v) },
|
|
159
|
+
max: { check: (v) => /^[0-9]{1,3}$/.test(v) && Number(v) >= 1 && Number(v) <= 200 },
|
|
160
|
+
repo: { check: (v) => v.length <= 4096 && isAbsolute(v) },
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
function runRecordFor(id) {
|
|
164
|
+
if (!RUN_ID.test(id)) throw new HttpError(404, "not-found", "no such run");
|
|
165
|
+
const { found, records } = selectRuns({ taskId: id });
|
|
166
|
+
if (!found) throw new HttpError(404, "not-found", "no such run");
|
|
167
|
+
return records;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function stateFileOf(record) {
|
|
171
|
+
for (const p of [
|
|
172
|
+
join(record.dir, "agent-state.json"),
|
|
173
|
+
join(record.dir, "artifacts", "agent-state.json"),
|
|
174
|
+
]) {
|
|
175
|
+
if (existsSync(p)) return p;
|
|
176
|
+
}
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/** answer-request.schema.json, checked by hand (ADR-0004). */
|
|
181
|
+
export function answerBodyErrors(body) {
|
|
182
|
+
if (!body || typeof body !== "object" || Array.isArray(body))
|
|
183
|
+
return ["body is not a JSON object"];
|
|
184
|
+
const errors = [];
|
|
185
|
+
for (const k of Object.keys(body)) {
|
|
186
|
+
if (k !== "questionId" && k !== "optionIds") errors.push(`unknown field ${k}`);
|
|
187
|
+
}
|
|
188
|
+
if (
|
|
189
|
+
typeof body.questionId !== "string" ||
|
|
190
|
+
body.questionId.length > 100 ||
|
|
191
|
+
!QUESTION_ID.test(body.questionId)
|
|
192
|
+
) {
|
|
193
|
+
errors.push("questionId must be a question id");
|
|
194
|
+
}
|
|
195
|
+
const ids = body.optionIds;
|
|
196
|
+
if (!Array.isArray(ids) || ids.length < 1 || ids.length > 50) {
|
|
197
|
+
errors.push("optionIds must be an array of 1 to 50 option ids");
|
|
198
|
+
} else if (!ids.every((id) => typeof id === "string" && id.length <= 100 && OPTION_ID.test(id))) {
|
|
199
|
+
errors.push("every optionIds entry must be an option id");
|
|
200
|
+
}
|
|
201
|
+
return errors;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const PHONE_REFUSAL = {
|
|
205
|
+
unsigned: "a device signature is required",
|
|
206
|
+
"unknown-device": "this device is not enrolled here",
|
|
207
|
+
"stale-request": "the timestamp is outside the accepted window",
|
|
208
|
+
"bad-signature": "the signature does not match the request",
|
|
209
|
+
"revoked-device": "this device was revoked",
|
|
210
|
+
replayed: "this request was already received",
|
|
211
|
+
"replay-store-full": "too many requests in the window; retry shortly",
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
const ANSWER_REFUSAL = {
|
|
215
|
+
"no-state": [409, "not-waiting"],
|
|
216
|
+
"not-waiting": [409, "not-waiting"],
|
|
217
|
+
"wrong-question": [409, "wrong-question"],
|
|
218
|
+
"state-moved": [409, "state-moved"],
|
|
219
|
+
"no-answer": [422, "wrong-arity"],
|
|
220
|
+
arity: [422, "wrong-arity"],
|
|
221
|
+
"not-an-option": [422, "not-an-option"],
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* POST /v1/launch and /v1/phone/launch. `channel` decides what the input may
|
|
226
|
+
* be (launch-request.mjs inputErrors): the phone takes structured references
|
|
227
|
+
* only. Either way the repo must be a configured or registered one.
|
|
228
|
+
*/
|
|
229
|
+
function launchRun({ q, body, redact, channel = "desktop" }) {
|
|
230
|
+
const repoPath = one(q, "repo");
|
|
231
|
+
if (!repoPath) throw new HttpError(400, "invalid-query", "repo is required");
|
|
232
|
+
if (!existsSync(repoPath) || !statSync(repoPath).isDirectory()) {
|
|
233
|
+
throw new HttpError(400, "invalid-query", "repo is not a directory");
|
|
234
|
+
}
|
|
235
|
+
if (!launchRepoAllowed(repoPath)) {
|
|
236
|
+
throw new HttpError(
|
|
237
|
+
403,
|
|
238
|
+
"repo-not-allowed",
|
|
239
|
+
"repo is neither a configured autopilot repo nor one registered with launch-request.mjs register-repo",
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const sessionId = randomUUID();
|
|
243
|
+
const planned = join(requestsDir(), `${sessionId}.json`);
|
|
244
|
+
const args = {
|
|
245
|
+
request: body,
|
|
246
|
+
requestFile: planned,
|
|
247
|
+
repoPath,
|
|
248
|
+
sessionId,
|
|
249
|
+
workingTree: workingTreeState(repoPath),
|
|
250
|
+
channel,
|
|
251
|
+
};
|
|
252
|
+
const dry = planLaunch(args);
|
|
253
|
+
if (dry.code !== 0) {
|
|
254
|
+
const status = dry.reason === "no-launch-shape" ? 422 : 400;
|
|
255
|
+
throw new HttpError(status, dry.reason, "the launch request was refused", {
|
|
256
|
+
errors: dry.out.errors,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
const written = writeRequestFile(body, sessionId);
|
|
260
|
+
return planLaunch({ ...args, requestFile: written, redact }).out;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function answerRun({ params, body }) {
|
|
264
|
+
const errors = answerBodyErrors(body);
|
|
265
|
+
if (errors.length) {
|
|
266
|
+
throw new HttpError(
|
|
267
|
+
400,
|
|
268
|
+
"invalid-body",
|
|
269
|
+
"the answer does not match answer-request.schema.json",
|
|
270
|
+
{
|
|
271
|
+
errors,
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
}
|
|
275
|
+
const [record] = runRecordFor(params.id);
|
|
276
|
+
const file = stateFileOf(record);
|
|
277
|
+
if (!file) throw new HttpError(409, "not-waiting", "this run is not waiting on a question");
|
|
278
|
+
const r = answerStateFile(file, body.optionIds, { questionId: body.questionId });
|
|
279
|
+
if (r.code === 0) return r.out;
|
|
280
|
+
const [status, code] = ANSWER_REFUSAL[r.reason] ?? [500, "internal"];
|
|
281
|
+
throw new HttpError(status, code, status === 500 ? "the answer could not be written" : r.error);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const listRuns = ({ q, redact }) =>
|
|
285
|
+
envelope(selectRuns({ group: one(q, "group") }).records, { redact });
|
|
286
|
+
const oneRun = ({ params, redact }) => envelope(runRecordFor(params.id), { redact });
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Every route. `query` names the parameters beyond `v` and `redact` that a
|
|
290
|
+
* route accepts; `body` marks a route that reads a JSON body. A route with
|
|
291
|
+
* `scope` is a phone route: it is reached only with a device signature, never
|
|
292
|
+
* with the bearer token, and is always served redacted.
|
|
293
|
+
*/
|
|
294
|
+
export const ROUTES = [
|
|
295
|
+
{ method: "GET", path: "/v1/runs", query: ["group"], handle: listRuns },
|
|
296
|
+
{ method: "GET", path: "/v1/runs/{id}", query: [], handle: oneRun },
|
|
297
|
+
{
|
|
298
|
+
method: "GET",
|
|
299
|
+
path: "/v1/commands",
|
|
300
|
+
query: [],
|
|
301
|
+
handle: () => {
|
|
302
|
+
const out = catalog();
|
|
303
|
+
if (!out) throw new HttpError(500, "internal", "no command tree found");
|
|
304
|
+
return out;
|
|
305
|
+
},
|
|
306
|
+
},
|
|
307
|
+
{ method: "GET", path: "/v1/questions", query: [], handle: () => questionsDocument() },
|
|
308
|
+
{
|
|
309
|
+
method: "GET",
|
|
310
|
+
path: "/v1/issues",
|
|
311
|
+
query: ["source", "project", "max"],
|
|
312
|
+
handle: ({ q }) =>
|
|
313
|
+
collect({
|
|
314
|
+
source: one(q, "source") ?? "all",
|
|
315
|
+
projects: q.project ?? [],
|
|
316
|
+
max: Number(one(q, "max") ?? 50),
|
|
317
|
+
}),
|
|
318
|
+
},
|
|
319
|
+
{
|
|
320
|
+
method: "GET",
|
|
321
|
+
path: "/v1/worktrees",
|
|
322
|
+
query: ["measure"],
|
|
323
|
+
handle: ({ q, redact }) => buildList({ doMeasure: flag(one(q, "measure")), redact }),
|
|
324
|
+
},
|
|
325
|
+
{ method: "GET", path: "/v1/launch-spec", query: [], handle: () => launchSpec() },
|
|
326
|
+
{ method: "POST", path: "/v1/launch", query: ["repo"], body: true, handle: launchRun },
|
|
327
|
+
{ method: "POST", path: "/v1/runs/{id}/answer", query: [], body: true, handle: answerRun },
|
|
328
|
+
{ method: "GET", path: "/v1/phone/runs", query: ["group"], scope: "read", handle: listRuns },
|
|
329
|
+
{ method: "GET", path: "/v1/phone/runs/{id}", query: [], scope: "read", handle: oneRun },
|
|
330
|
+
{
|
|
331
|
+
method: "POST",
|
|
332
|
+
path: "/v1/phone/runs/{id}/answer",
|
|
333
|
+
query: [],
|
|
334
|
+
body: true,
|
|
335
|
+
scope: "answer",
|
|
336
|
+
handle: answerRun,
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
method: "POST",
|
|
340
|
+
path: "/v1/phone/launch",
|
|
341
|
+
query: ["repo"],
|
|
342
|
+
body: true,
|
|
343
|
+
scope: "launch",
|
|
344
|
+
handle: (ctx) => {
|
|
345
|
+
if (!ctx.registry.launchEnabled) {
|
|
346
|
+
throw new HttpError(403, "launch-disabled", "launch from a phone is not enabled here");
|
|
347
|
+
}
|
|
348
|
+
return launchRun({ ...ctx, channel: "phone" });
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
].map((r) => ({
|
|
352
|
+
...r,
|
|
353
|
+
phone: r.path.startsWith(PHONE_PREFIX),
|
|
354
|
+
pattern: new RegExp(`^${r.path.replace("{id}", "(?<id>[^/]+)")}$`),
|
|
355
|
+
}));
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* The route for a path and method, or the HttpError that says why not. A phone
|
|
359
|
+
* path is looked up among phone routes only and a desktop path among desktop
|
|
360
|
+
* routes only, so neither credential can reach the other family.
|
|
361
|
+
*/
|
|
362
|
+
export function match(method, pathname) {
|
|
363
|
+
const phone = isPhonePath(pathname);
|
|
364
|
+
const hits = [];
|
|
365
|
+
for (const r of ROUTES) {
|
|
366
|
+
if (r.phone !== phone) continue;
|
|
367
|
+
const m = r.pattern.exec(pathname);
|
|
368
|
+
if (m) hits.push({ route: r, params: m.groups ?? {} });
|
|
369
|
+
}
|
|
370
|
+
if (!hits.length) {
|
|
371
|
+
if (phone) throw new HttpError(404, "unknown-verb", "the phone API has no such verb");
|
|
372
|
+
throw new HttpError(404, "not-found", "no such route");
|
|
373
|
+
}
|
|
374
|
+
const hit = hits.find((h) => h.route.method === method);
|
|
375
|
+
if (!hit) {
|
|
376
|
+
throw new HttpError(
|
|
377
|
+
405,
|
|
378
|
+
"method-not-allowed",
|
|
379
|
+
`use ${hits.map((h) => h.route.method).join(", ")}`,
|
|
380
|
+
{
|
|
381
|
+
allow: hits.map((h) => h.route.method).join(", "),
|
|
382
|
+
},
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
if (hit.params.id !== undefined) {
|
|
386
|
+
try {
|
|
387
|
+
hit.params.id = decodeURIComponent(hit.params.id);
|
|
388
|
+
} catch {
|
|
389
|
+
throw new HttpError(404, "not-found", "no such run");
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
return hit;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** Query parameters as {name: [values]}, after every rule a route declares. */
|
|
396
|
+
export function parseQuery(route, searchParams) {
|
|
397
|
+
const q = {};
|
|
398
|
+
for (const [k, v] of searchParams) {
|
|
399
|
+
if (k !== "v" && k !== "redact" && !route.query.includes(k)) {
|
|
400
|
+
throw new HttpError(400, "invalid-query", `unknown query parameter ${k}`);
|
|
401
|
+
}
|
|
402
|
+
(q[k] ??= []).push(v);
|
|
403
|
+
}
|
|
404
|
+
for (const [k, values] of Object.entries(q)) {
|
|
405
|
+
if (k === "v") {
|
|
406
|
+
if (
|
|
407
|
+
values.length !== 1 ||
|
|
408
|
+
!SUPPORTED_MAJORS.includes(Number(values[0])) ||
|
|
409
|
+
!/^[0-9]+$/.test(values[0])
|
|
410
|
+
) {
|
|
411
|
+
throw new HttpError(400, "unsupported-version", "this server does not serve that major", {
|
|
412
|
+
supported: SUPPORTED_MAJORS,
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
continue;
|
|
416
|
+
}
|
|
417
|
+
const rule = Q[k];
|
|
418
|
+
if (values.length > 1 && !rule.repeat)
|
|
419
|
+
throw new HttpError(400, "invalid-query", `${k} is given twice`);
|
|
420
|
+
for (const v of values) {
|
|
421
|
+
if (!rule.check(v)) throw new HttpError(400, "invalid-query", `${k} has an invalid value`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
return q;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const sha = (s) => createHash("sha256").update(s).digest();
|
|
428
|
+
|
|
429
|
+
/** Does this path belong to the phone family? */
|
|
430
|
+
export const isPhonePath = (pathname) =>
|
|
431
|
+
pathname === PHONE_PREFIX.slice(0, -1) || pathname.startsWith(PHONE_PREFIX);
|
|
432
|
+
|
|
433
|
+
/** Constant-time bearer check: equal-length digests, whatever the input. */
|
|
434
|
+
export function authorized(header, tokenDigest) {
|
|
435
|
+
const m = /^Bearer ([A-Za-z0-9_-]{1,512})$/.exec(String(header ?? ""));
|
|
436
|
+
return timingSafeEqual(sha(m ? m[1] : ""), tokenDigest) && Boolean(m);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
function readRawBody(req) {
|
|
440
|
+
return new Promise((resolve, reject) => {
|
|
441
|
+
const chunks = [];
|
|
442
|
+
let size = 0;
|
|
443
|
+
let over = false;
|
|
444
|
+
req.on("data", (chunk) => {
|
|
445
|
+
size += chunk.length;
|
|
446
|
+
if (size > MAX_BODY_BYTES) over = true;
|
|
447
|
+
if (size > MAX_DRAIN_BYTES) req.destroy();
|
|
448
|
+
if (!over) chunks.push(chunk);
|
|
449
|
+
});
|
|
450
|
+
req.on("end", () => {
|
|
451
|
+
if (over) {
|
|
452
|
+
reject(new HttpError(400, "invalid-body", `the body is over ${MAX_BODY_BYTES} bytes`));
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
resolve(Buffer.concat(chunks));
|
|
456
|
+
});
|
|
457
|
+
req.on("close", () => {
|
|
458
|
+
if (!req.complete) reject(new HttpError(400, "invalid-body", "the body was cut off"));
|
|
459
|
+
});
|
|
460
|
+
req.on("error", reject);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function parseJson(raw) {
|
|
465
|
+
try {
|
|
466
|
+
return JSON.parse(raw.toString("utf8"));
|
|
467
|
+
} catch {
|
|
468
|
+
throw new HttpError(400, "invalid-body", "the body is not JSON");
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
const readBody = async (req) => parseJson(await readRawBody(req));
|
|
473
|
+
|
|
474
|
+
function drain(req) {
|
|
475
|
+
let size = 0;
|
|
476
|
+
req.on("data", (chunk) => {
|
|
477
|
+
size += chunk.length;
|
|
478
|
+
if (size > MAX_DRAIN_BYTES) req.destroy();
|
|
479
|
+
});
|
|
480
|
+
req.resume();
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function parseArgs(argv) {
|
|
484
|
+
const opts = { port: 0, idleMinutes: DEFAULT_IDLE_MINUTES, origins: [], printToken: false };
|
|
485
|
+
for (let i = 0; i < argv.length; i++) {
|
|
486
|
+
const a = argv[i];
|
|
487
|
+
const value = () => {
|
|
488
|
+
const v = argv[++i];
|
|
489
|
+
if (v === undefined) throw new Error(`${a} needs a value`);
|
|
490
|
+
return v;
|
|
491
|
+
};
|
|
492
|
+
if (a === "--host") {
|
|
493
|
+
const h = value();
|
|
494
|
+
if (h !== BIND_HOST) throw new Error(`binds only ${BIND_HOST}; --host ${h} is refused`);
|
|
495
|
+
} else if (a === "--port") {
|
|
496
|
+
const p = value();
|
|
497
|
+
if (!/^[0-9]{1,5}$/.test(p) || Number(p) > 65535) throw new Error("--port is 0 to 65535");
|
|
498
|
+
opts.port = Number(p);
|
|
499
|
+
} else if (a === "--idle-minutes") {
|
|
500
|
+
const n = Number(value());
|
|
501
|
+
if (!Number.isFinite(n) || n <= 0 || n > MAX_IDLE_MINUTES) {
|
|
502
|
+
throw new Error(`--idle-minutes is more than 0 and at most ${MAX_IDLE_MINUTES}`);
|
|
503
|
+
}
|
|
504
|
+
opts.idleMinutes = n;
|
|
505
|
+
} else if (a === "--allow-origin") {
|
|
506
|
+
const o = value();
|
|
507
|
+
let parsed;
|
|
508
|
+
try {
|
|
509
|
+
parsed = new URL(o);
|
|
510
|
+
} catch {
|
|
511
|
+
parsed = null;
|
|
512
|
+
}
|
|
513
|
+
if (!parsed || !/^https?:$/.test(parsed.protocol) || parsed.origin !== o) {
|
|
514
|
+
throw new Error(`--allow-origin takes an origin like http://127.0.0.1:5173, not ${o}`);
|
|
515
|
+
}
|
|
516
|
+
opts.origins.push(o);
|
|
517
|
+
} else if (a === "--print-token") opts.printToken = true;
|
|
518
|
+
else if (a === "-h" || a === "--help") opts.help = true;
|
|
519
|
+
else throw new Error(`unknown argument ${a}`);
|
|
520
|
+
}
|
|
521
|
+
return opts;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/** The token file this process wrote, removed on every way out. */
|
|
525
|
+
let liveTokenFile = null;
|
|
526
|
+
const removeTokenFile = () => {
|
|
527
|
+
if (liveTokenFile) rmSync(liveTokenFile, { force: true });
|
|
528
|
+
liveTokenFile = null;
|
|
529
|
+
};
|
|
530
|
+
|
|
531
|
+
function writeTokenFile(record) {
|
|
532
|
+
const dir = tokenDir();
|
|
533
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
534
|
+
chmodSync(dir, 0o700);
|
|
535
|
+
const file = join(dir, `${process.pid}.json`);
|
|
536
|
+
rmSync(file, { force: true });
|
|
537
|
+
writeFileSync(file, JSON.stringify(record, null, 2) + "\n", { mode: 0o600, flag: "wx" });
|
|
538
|
+
liveTokenFile = file;
|
|
539
|
+
return file;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
/**
|
|
543
|
+
* Start listening. Resolves with the ready record once the port is known.
|
|
544
|
+
*
|
|
545
|
+
* @param {{port?: number, idleMinutes?: number, origins?: string[], printToken?: boolean}} opts
|
|
546
|
+
*/
|
|
547
|
+
export function serve(opts) {
|
|
548
|
+
const token = randomBytes(32).toString("base64url");
|
|
549
|
+
const tokenDigest = sha(token);
|
|
550
|
+
const origins = new Set(opts.origins ?? []);
|
|
551
|
+
const idleMs = Math.round((opts.idleMinutes ?? DEFAULT_IDLE_MINUTES) * 60000);
|
|
552
|
+
let hosts = new Set();
|
|
553
|
+
let startSec = Infinity;
|
|
554
|
+
const nonces = createNonceStore({ file: noncesPath() });
|
|
555
|
+
let tokenFile = null;
|
|
556
|
+
let idleTimer = null;
|
|
557
|
+
let stopping = false;
|
|
558
|
+
|
|
559
|
+
// requireHostHeader off: a request without Host reaches the Host check below
|
|
560
|
+
// and is refused there with a contract error body, like any other wrong Host.
|
|
561
|
+
const server = createServer({
|
|
562
|
+
requestTimeout: 120000,
|
|
563
|
+
headersTimeout: 10000,
|
|
564
|
+
requireHostHeader: false,
|
|
565
|
+
});
|
|
566
|
+
server.maxHeadersCount = 50;
|
|
567
|
+
|
|
568
|
+
const send = (res, status, obj, extra = {}) => {
|
|
569
|
+
const body = serialize(obj);
|
|
570
|
+
res.writeHead(status, {
|
|
571
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
572
|
+
"Content-Length": Buffer.byteLength(body),
|
|
573
|
+
"Cache-Control": "no-store",
|
|
574
|
+
"X-Content-Type-Options": "nosniff",
|
|
575
|
+
"X-Contract-Version": CONTRACT_VERSION,
|
|
576
|
+
...extra,
|
|
577
|
+
});
|
|
578
|
+
res.end(body);
|
|
579
|
+
};
|
|
580
|
+
const fail = (res, err, extra) => {
|
|
581
|
+
const body = { contractVersion: CONTRACT_VERSION, error: err.code, message: err.message };
|
|
582
|
+
if (err.extra?.errors) body.errors = err.extra.errors;
|
|
583
|
+
if (err.extra?.supported) body.supported = err.extra.supported;
|
|
584
|
+
const headers = { ...extra };
|
|
585
|
+
if (err.extra?.allow) headers.Allow = err.extra.allow;
|
|
586
|
+
if (err.status === 401) headers["WWW-Authenticate"] = err.extra?.scheme ?? "Bearer";
|
|
587
|
+
send(res, err.status, body, headers);
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
const stop = (why) => {
|
|
591
|
+
if (stopping) return;
|
|
592
|
+
stopping = true;
|
|
593
|
+
clearTimeout(idleTimer);
|
|
594
|
+
removeTokenFile();
|
|
595
|
+
process.stderr.write(`contract-server: stopping (${why})\n`);
|
|
596
|
+
server.close(() => process.exit(0));
|
|
597
|
+
server.closeAllConnections();
|
|
598
|
+
setTimeout(() => process.exit(0), 2000).unref();
|
|
599
|
+
};
|
|
600
|
+
const arm = () => {
|
|
601
|
+
clearTimeout(idleTimer);
|
|
602
|
+
idleTimer = setTimeout(() => stop("idle"), idleMs);
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* A phone request: the signature decides who is asking, before the route is
|
|
607
|
+
* even looked up, and the device's scopes decide what it may ask for. The
|
|
608
|
+
* bearer token plays no part in either direction. Every outcome is audited.
|
|
609
|
+
*/
|
|
610
|
+
const servePhone = async (req, res, url, cors) => {
|
|
611
|
+
const audit = { deviceId: null, method: req.method, verb: verbOf(url.pathname) };
|
|
612
|
+
let insecure = null;
|
|
613
|
+
try {
|
|
614
|
+
const raw = await readRawBody(req);
|
|
615
|
+
const registry = readRegistry();
|
|
616
|
+
insecure = registry.insecure;
|
|
617
|
+
const v = verifyRequest({
|
|
618
|
+
method: req.method,
|
|
619
|
+
target: req.url ?? "",
|
|
620
|
+
headers: req.headers,
|
|
621
|
+
body: raw,
|
|
622
|
+
registry,
|
|
623
|
+
nonces,
|
|
624
|
+
nowSec: Math.floor(Date.now() / 1000),
|
|
625
|
+
notBeforeSec: startSec,
|
|
626
|
+
});
|
|
627
|
+
audit.deviceId = v.ok ? v.device.id : v.deviceId;
|
|
628
|
+
if (!v.ok) {
|
|
629
|
+
throw new HttpError(v.status, v.code, PHONE_REFUSAL[v.code], {
|
|
630
|
+
scheme: "MA-Phone-Signature",
|
|
631
|
+
});
|
|
632
|
+
}
|
|
633
|
+
const { route, params } = match(req.method, url.pathname);
|
|
634
|
+
if (!Array.isArray(v.device.scopes) || !v.device.scopes.includes(route.scope)) {
|
|
635
|
+
throw new HttpError(403, "forbidden-scope", `this device has no ${route.scope} scope`);
|
|
636
|
+
}
|
|
637
|
+
const q = parseQuery(route, url.searchParams);
|
|
638
|
+
if (q.redact && one(q, "redact") !== "1") {
|
|
639
|
+
throw new HttpError(400, "invalid-query", "phone responses are always redacted");
|
|
640
|
+
}
|
|
641
|
+
let body = null;
|
|
642
|
+
if (route.body) {
|
|
643
|
+
const type = String(req.headers["content-type"] ?? "");
|
|
644
|
+
if (!/^application\/json(\s*;.*)?$/i.test(type)) {
|
|
645
|
+
throw new HttpError(415, "unsupported-media-type", "the body must be application/json");
|
|
646
|
+
}
|
|
647
|
+
body = parseJson(raw);
|
|
648
|
+
}
|
|
649
|
+
send(res, 200, route.handle({ q, params, body, redact: true, registry }), cors);
|
|
650
|
+
appendAudit({ ...audit, status: 200, outcome: "accepted" });
|
|
651
|
+
} catch (err) {
|
|
652
|
+
const e = err instanceof HttpError ? err : new HttpError(500, "internal", "internal error");
|
|
653
|
+
if (!(err instanceof HttpError))
|
|
654
|
+
process.stderr.write(`contract-server: ${err?.message ?? err}\n`);
|
|
655
|
+
fail(res, e, cors);
|
|
656
|
+
const outcome = e.code === "unknown-device" && insecure ? insecure : e.code;
|
|
657
|
+
appendAudit({ ...audit, status: e.status, outcome });
|
|
658
|
+
}
|
|
659
|
+
};
|
|
660
|
+
|
|
661
|
+
server.on("request", async (req, res) => {
|
|
662
|
+
arm();
|
|
663
|
+
res.on("finish", arm);
|
|
664
|
+
const origin = req.headers.origin;
|
|
665
|
+
const cors =
|
|
666
|
+
origin && origins.has(origin)
|
|
667
|
+
? { "Access-Control-Allow-Origin": origin, Vary: "Origin" }
|
|
668
|
+
: {};
|
|
669
|
+
try {
|
|
670
|
+
if (!hosts.has(String(req.headers.host ?? "").toLowerCase())) {
|
|
671
|
+
throw new HttpError(403, "forbidden-host", "the Host header does not name this server");
|
|
672
|
+
}
|
|
673
|
+
if (origin !== undefined && !origins.has(origin)) {
|
|
674
|
+
throw new HttpError(403, "forbidden-origin", "this origin is not allowed");
|
|
675
|
+
}
|
|
676
|
+
if (req.method === "OPTIONS" && origin !== undefined) {
|
|
677
|
+
drain(req);
|
|
678
|
+
res.writeHead(204, {
|
|
679
|
+
...cors,
|
|
680
|
+
"Access-Control-Allow-Methods": "GET, POST",
|
|
681
|
+
"Access-Control-Allow-Headers": "Authorization, Content-Type",
|
|
682
|
+
"Access-Control-Max-Age": "600",
|
|
683
|
+
"Cache-Control": "no-store",
|
|
684
|
+
});
|
|
685
|
+
res.end();
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
const url = new URL(req.url ?? "/", `http://${BIND_HOST}`);
|
|
689
|
+
if (isPhonePath(url.pathname)) {
|
|
690
|
+
await servePhone(req, res, url, cors);
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (!authorized(req.headers.authorization, tokenDigest)) {
|
|
694
|
+
throw new HttpError(401, "unauthorized", "a bearer token is required");
|
|
695
|
+
}
|
|
696
|
+
const { route, params } = match(req.method, url.pathname);
|
|
697
|
+
const q = parseQuery(route, url.searchParams);
|
|
698
|
+
const redact = flag(one(q, "redact")) || origin !== undefined;
|
|
699
|
+
let body = null;
|
|
700
|
+
if (route.body) {
|
|
701
|
+
const type = String(req.headers["content-type"] ?? "");
|
|
702
|
+
if (!/^application\/json(\s*;.*)?$/i.test(type)) {
|
|
703
|
+
drain(req);
|
|
704
|
+
throw new HttpError(415, "unsupported-media-type", "the body must be application/json");
|
|
705
|
+
}
|
|
706
|
+
body = await readBody(req);
|
|
707
|
+
} else {
|
|
708
|
+
drain(req);
|
|
709
|
+
}
|
|
710
|
+
send(res, 200, route.handle({ q, params, body, redact }), cors);
|
|
711
|
+
} catch (err) {
|
|
712
|
+
if (err instanceof HttpError) {
|
|
713
|
+
fail(res, err, cors);
|
|
714
|
+
return;
|
|
715
|
+
}
|
|
716
|
+
process.stderr.write(`contract-server: ${err?.message ?? err}\n`);
|
|
717
|
+
fail(res, new HttpError(500, "internal", "internal error"), cors);
|
|
718
|
+
}
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
return new Promise((resolve, reject) => {
|
|
722
|
+
server.once("error", reject);
|
|
723
|
+
server.listen(opts.port ?? 0, BIND_HOST, () => {
|
|
724
|
+
const { port } = server.address();
|
|
725
|
+
hosts = new Set([`${BIND_HOST}:${port}`, `localhost:${port}`]);
|
|
726
|
+
const record = {
|
|
727
|
+
contractVersion: CONTRACT_VERSION,
|
|
728
|
+
url: `http://${BIND_HOST}:${port}`,
|
|
729
|
+
port,
|
|
730
|
+
pid: process.pid,
|
|
731
|
+
idleMinutes: opts.idleMinutes ?? DEFAULT_IDLE_MINUTES,
|
|
732
|
+
};
|
|
733
|
+
const started = new Date();
|
|
734
|
+
startSec = Math.floor(started.getTime() / 1000);
|
|
735
|
+
tokenFile = writeTokenFile({ ...record, token, startedAt: started.toISOString() });
|
|
736
|
+
for (const sig of ["SIGTERM", "SIGINT", "SIGHUP"]) process.on(sig, () => stop(sig));
|
|
737
|
+
arm();
|
|
738
|
+
resolve({ ...record, tokenFile, ...(opts.printToken ? { token } : {}) });
|
|
739
|
+
});
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
async function main(argv) {
|
|
744
|
+
let opts;
|
|
745
|
+
try {
|
|
746
|
+
opts = parseArgs(argv);
|
|
747
|
+
} catch (err) {
|
|
748
|
+
process.stderr.write(`contract-server: ${err.message}\n`);
|
|
749
|
+
return 2;
|
|
750
|
+
}
|
|
751
|
+
if (opts.help) {
|
|
752
|
+
process.stdout.write(
|
|
753
|
+
"Usage: contract-server.mjs [--port N] [--idle-minutes N] [--allow-origin <origin>]... [--print-token]\n",
|
|
754
|
+
);
|
|
755
|
+
return 0;
|
|
756
|
+
}
|
|
757
|
+
try {
|
|
758
|
+
const ready = await serve(opts);
|
|
759
|
+
process.stdout.write(JSON.stringify(ready) + "\n");
|
|
760
|
+
} catch (err) {
|
|
761
|
+
process.stderr.write(`contract-server: could not listen (${err.code ?? err.message})\n`);
|
|
762
|
+
return 1;
|
|
763
|
+
}
|
|
764
|
+
return undefined;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
if (invokedDirectly(import.meta.url)) {
|
|
768
|
+
runMain(
|
|
769
|
+
"contract-server",
|
|
770
|
+
async () => {
|
|
771
|
+
const code = await main(process.argv.slice(2));
|
|
772
|
+
if (code !== undefined) process.exitCode = code;
|
|
773
|
+
},
|
|
774
|
+
{ cleanup: removeTokenFile },
|
|
775
|
+
);
|
|
776
|
+
}
|