@mmerterden/multi-agent-pipeline 20.2.1 → 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 +912 -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 -35
- 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 +79 -8
- 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 -65
- 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,160 @@
|
|
|
1
|
+
// Third-party attribution for pipeline/skills/shared/external.
|
|
2
|
+
//
|
|
3
|
+
// A vendored skill carries `metadata.source: "<upstream> (<license>)"` in its
|
|
4
|
+
// frontmatter, and its upstream's NOTICE-*.md lists it in a `## Skills (N)`
|
|
5
|
+
// section. The licenses involved (MIT, Apache 2.0, PolyForm Perimeter) require
|
|
6
|
+
// the notice to travel with every copy, and build-stack-plugins.mjs ships a
|
|
7
|
+
// NOTICE only beside the skills that section lists, so the two records are
|
|
8
|
+
// checked against each other here: lint-skills fails on a mismatch and the
|
|
9
|
+
// builder refuses to publish one.
|
|
10
|
+
//
|
|
11
|
+
// A skill whose header says only `metadata.source: community` came from a
|
|
12
|
+
// community collection whose repository and license are not known. Content of
|
|
13
|
+
// unknown license cannot ship verbatim, so such a skill is an attribution
|
|
14
|
+
// problem whether or not a NOTICE lists it.
|
|
15
|
+
|
|
16
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
17
|
+
import { join } from "node:path";
|
|
18
|
+
|
|
19
|
+
export const NOTICE_FILE = /^NOTICE-.*\.md$/;
|
|
20
|
+
|
|
21
|
+
// A URL in the parentheses names a location, not a license.
|
|
22
|
+
const LICENSED = /^"?(?<upstream>[^"(]+?)\s*\((?<license>(?!https?:)[^)]+)\)"?\s*$/;
|
|
23
|
+
|
|
24
|
+
// Written on a skill authored in this repository.
|
|
25
|
+
export const AUTHORED_SOURCE = "multi-agent-pipeline";
|
|
26
|
+
|
|
27
|
+
// Written on a skill taken from a community collection with no named upstream.
|
|
28
|
+
export const COMMUNITY_SOURCE = "community";
|
|
29
|
+
|
|
30
|
+
/** `{ declared, skills }` from a NOTICE's `## Skills (N)` section; `declared` is null without one. */
|
|
31
|
+
export function parseNoticeSkills(text) {
|
|
32
|
+
const src = String(text).replace(/\r\n?/g, "\n");
|
|
33
|
+
const head = src.match(/^##[ \t]+Skills[ \t]+\((\d+)\)[ \t]*$/m);
|
|
34
|
+
if (!head) return { declared: null, skills: [] };
|
|
35
|
+
const rest = src.slice(head.index + head[0].length);
|
|
36
|
+
const next = rest.search(/^##[ \t]/m);
|
|
37
|
+
const section = next === -1 ? rest : rest.slice(0, next);
|
|
38
|
+
return {
|
|
39
|
+
declared: Number(head[1]),
|
|
40
|
+
skills: [...section.matchAll(/`([a-z0-9][a-z0-9_-]*)`/g)].map((m) => m[1]),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** The raw `metadata.source` value of a SKILL.md, unquoted, or "". */
|
|
45
|
+
export function metadataSource(skillMd) {
|
|
46
|
+
const src = String(skillMd)
|
|
47
|
+
.replace(/^\uFEFF/, "")
|
|
48
|
+
.replace(/\r\n?/g, "\n");
|
|
49
|
+
const fm = src.match(/^---\n([\s\S]*?)\n---/);
|
|
50
|
+
if (!fm) return "";
|
|
51
|
+
let inside = false;
|
|
52
|
+
for (const line of fm[1].split("\n")) {
|
|
53
|
+
if (/^\S/.test(line)) {
|
|
54
|
+
inside = /^metadata:\s*(#.*)?$/.test(line);
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const m = inside && line.match(/^\s+source:\s*(.*?)\s*$/);
|
|
58
|
+
if (m) return m[1].replace(/^(["'])(.*)\1$/, "$2");
|
|
59
|
+
}
|
|
60
|
+
return "";
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `{ upstream, license }` when the skill is vendored from a licensed upstream, else null. */
|
|
64
|
+
export function licensedSource(skillMd) {
|
|
65
|
+
const m = metadataSource(skillMd).match(LICENSED);
|
|
66
|
+
return m ? { upstream: m.groups.upstream.trim(), license: m.groups.license.trim() } : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const isDir = (p) => {
|
|
70
|
+
try {
|
|
71
|
+
return statSync(p).isDirectory();
|
|
72
|
+
} catch {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
function skillDirs(externalDir) {
|
|
78
|
+
return readdirSync(externalDir)
|
|
79
|
+
.filter((n) => isDir(join(externalDir, n)) && existsSync(join(externalDir, n, "SKILL.md")))
|
|
80
|
+
.sort();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Every NOTICE-*.md in the directory with its bytes and parsed Skills section. */
|
|
84
|
+
export function readNotices(externalDir) {
|
|
85
|
+
if (!existsSync(externalDir)) return [];
|
|
86
|
+
return readdirSync(externalDir)
|
|
87
|
+
.filter((n) => NOTICE_FILE.test(n))
|
|
88
|
+
.sort()
|
|
89
|
+
.map((file) => {
|
|
90
|
+
const bytes = readFileSync(join(externalDir, file));
|
|
91
|
+
const { declared, skills } = parseNoticeSkills(bytes.toString("utf8"));
|
|
92
|
+
return {
|
|
93
|
+
file,
|
|
94
|
+
bytes,
|
|
95
|
+
text: bytes.toString("utf8"),
|
|
96
|
+
declared,
|
|
97
|
+
skills: new Set(skills),
|
|
98
|
+
listed: skills,
|
|
99
|
+
};
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Why the NOTICE files and the skills' `metadata.source` lines disagree; empty when they agree. */
|
|
104
|
+
export function noticeProblems(externalDir) {
|
|
105
|
+
const problems = [];
|
|
106
|
+
const skills = new Set(skillDirs(externalDir));
|
|
107
|
+
const owner = new Map();
|
|
108
|
+
const notices = readNotices(externalDir);
|
|
109
|
+
for (const n of notices) {
|
|
110
|
+
if (n.declared === null) {
|
|
111
|
+
problems.push(`${n.file}: no '## Skills (N)' section listing the skills it covers`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (n.declared !== n.listed.length) {
|
|
115
|
+
problems.push(`${n.file}: declares ${n.declared} skills but lists ${n.listed.length}`);
|
|
116
|
+
}
|
|
117
|
+
for (const s of n.listed) {
|
|
118
|
+
if (!skills.has(s)) problems.push(`${n.file}: lists '${s}', which is not a skill directory`);
|
|
119
|
+
if (owner.has(s)) problems.push(`${s}: listed by both ${owner.get(s)} and ${n.file}`);
|
|
120
|
+
else owner.set(s, n.file);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const byFile = new Map(notices.map((n) => [n.file, n]));
|
|
124
|
+
for (const s of skills) {
|
|
125
|
+
const md = readFileSync(join(externalDir, s, "SKILL.md"), "utf8");
|
|
126
|
+
const src = licensedSource(md);
|
|
127
|
+
const file = owner.get(s);
|
|
128
|
+
if (metadataSource(md) === COMMUNITY_SOURCE) {
|
|
129
|
+
problems.push(
|
|
130
|
+
`${s}: metadata.source '${COMMUNITY_SOURCE}' names no upstream or license, and content of unknown license cannot ship`,
|
|
131
|
+
);
|
|
132
|
+
} else if (src && !file) {
|
|
133
|
+
problems.push(
|
|
134
|
+
`${s}: vendored from ${src.upstream} (${src.license}) but no NOTICE-*.md lists it`,
|
|
135
|
+
);
|
|
136
|
+
} else if (src && !byFile.get(file).text.includes(src.upstream)) {
|
|
137
|
+
problems.push(`${s}: names upstream '${src.upstream}', which ${file} does not mention`);
|
|
138
|
+
} else if (!src && file) {
|
|
139
|
+
problems.push(
|
|
140
|
+
`${s}: listed by ${file} but carries no 'metadata.source: "<upstream> (<license>)"'`,
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return problems;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Skills whose `metadata.source` is set but names no licensed upstream and is not
|
|
149
|
+
* the authored marker: attribution that cannot be checked against a NOTICE.
|
|
150
|
+
*/
|
|
151
|
+
export function unnamedUpstreams(externalDir) {
|
|
152
|
+
const out = [];
|
|
153
|
+
for (const s of skillDirs(externalDir)) {
|
|
154
|
+
const md = readFileSync(join(externalDir, s, "SKILL.md"), "utf8");
|
|
155
|
+
const source = metadataSource(md);
|
|
156
|
+
if (!source || source === AUTHORED_SOURCE || licensedSource(md)) continue;
|
|
157
|
+
out.push({ skill: s, source });
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
@@ -0,0 +1,485 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file _phone-auth.mjs - device signatures for the phone routes of
|
|
3
|
+
* contract-server.mjs.
|
|
4
|
+
*
|
|
5
|
+
* A phone reaches the server through a forwarder (a private network or a
|
|
6
|
+
* relay; features/phone-api.md), so it cannot read the bearer token file and
|
|
7
|
+
* must not be handed a copy: a token is a bearer secret, valid from anywhere
|
|
8
|
+
* for as long as the server runs. A phone instead holds an Ed25519 private key
|
|
9
|
+
* that never leaves it, and signs every request. This machine keeps only the
|
|
10
|
+
* public halves, in a registry the enrolment CLI (phone-devices.mjs) writes.
|
|
11
|
+
*
|
|
12
|
+
* The signed text is canonical: seven fields joined by "\n", each one refused
|
|
13
|
+
* if it could carry a newline, so two different requests can never produce the
|
|
14
|
+
* same text.
|
|
15
|
+
*
|
|
16
|
+
* MA-PHONE-SIG-1
|
|
17
|
+
* <method> GET or POST, as sent
|
|
18
|
+
* <target> the request target exactly as sent: path plus query
|
|
19
|
+
* <body sha256 hex> of the raw body bytes; the empty body has a digest too
|
|
20
|
+
* <device id>
|
|
21
|
+
* <timestamp> Unix seconds
|
|
22
|
+
* <nonce> 16 to 64 base64url characters, fresh per request
|
|
23
|
+
*
|
|
24
|
+
* A request is refused, in this order, when it is unsigned, names a device the
|
|
25
|
+
* registry does not hold, carries a timestamp more than WINDOW_SEC behind this
|
|
26
|
+
* machine's clock, more than FUTURE_SKEW_SEC ahead of it, or earlier than the
|
|
27
|
+
* server's own start, fails the signature, comes from a revoked device, or
|
|
28
|
+
* repeats a nonce inside the window.
|
|
29
|
+
*
|
|
30
|
+
* Seen nonces are kept on disk (nonces.json, 0600, bounded, pruned as their
|
|
31
|
+
* window closes), not only in memory, so a request accepted by one server
|
|
32
|
+
* process is still a replay to the next one. The start-time floor alone cannot
|
|
33
|
+
* do that: a timestamp ahead of the clock stays later than a restart for as
|
|
34
|
+
* long as it is ahead. The small future skew bounds that lead, and the floor
|
|
35
|
+
* still refuses anything signed before the process started, which covers a
|
|
36
|
+
* nonce file that was lost or could not be read. Only a hash of each nonce is
|
|
37
|
+
* stored.
|
|
38
|
+
*
|
|
39
|
+
* The registry is read on every request, so a revocation or a launch toggle
|
|
40
|
+
* takes effect on the next request. A registry that other users can write, or
|
|
41
|
+
* that another user owns, is treated as empty.
|
|
42
|
+
*
|
|
43
|
+
* The audit log records one line per phone request whose signature verified,
|
|
44
|
+
* accepted or not: time, device id, method, verb, status and outcome. Never the
|
|
45
|
+
* signature, the nonce, the query or the body. It keeps AUDIT_GENERATIONS
|
|
46
|
+
* rotated files. A refusal decided before a signature verified (unsigned,
|
|
47
|
+
* unknown device, stale timestamp, bad signature, unusable registry) costs an
|
|
48
|
+
* anonymous sender nothing to produce, so it goes to a separate refusals log,
|
|
49
|
+
* rate-limited per process, where a flood cannot rotate away the rows that
|
|
50
|
+
* record what an enrolled device did. A device id alone proves nothing: it
|
|
51
|
+
* travels in clear on every request.
|
|
52
|
+
*
|
|
53
|
+
* Zero runtime dependencies (ADR-0004): node:crypto and node:fs.
|
|
54
|
+
*
|
|
55
|
+
* @module pipeline/scripts/_phone-auth
|
|
56
|
+
*/
|
|
57
|
+
|
|
58
|
+
import { Buffer } from "node:buffer";
|
|
59
|
+
import { createHash, createPublicKey, randomBytes, sign, verify } from "node:crypto";
|
|
60
|
+
import {
|
|
61
|
+
appendFileSync,
|
|
62
|
+
chmodSync,
|
|
63
|
+
existsSync,
|
|
64
|
+
mkdirSync,
|
|
65
|
+
readFileSync,
|
|
66
|
+
renameSync,
|
|
67
|
+
rmSync,
|
|
68
|
+
statSync,
|
|
69
|
+
writeFileSync,
|
|
70
|
+
} from "node:fs";
|
|
71
|
+
import { dirname, join } from "node:path";
|
|
72
|
+
import { autopilotRoot } from "../lib/pr-request-location.mjs";
|
|
73
|
+
|
|
74
|
+
export const PHONE_PREFIX = "/v1/phone/";
|
|
75
|
+
export const SIGNATURE_VERSION = "MA-PHONE-SIG-1";
|
|
76
|
+
export const WINDOW_SEC = 60;
|
|
77
|
+
export const FUTURE_SKEW_SEC = 5;
|
|
78
|
+
export const SCOPES = ["read", "answer", "launch"];
|
|
79
|
+
export const REGISTRY_VERSION = "1.0.0";
|
|
80
|
+
export const NONCE_STORE_MAX = 4096;
|
|
81
|
+
export const HEADERS = {
|
|
82
|
+
device: "x-ma-device",
|
|
83
|
+
timestamp: "x-ma-timestamp",
|
|
84
|
+
nonce: "x-ma-nonce",
|
|
85
|
+
signature: "x-ma-signature",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
export const DEVICE_ID = /^d-[0-9a-f]{16}$/;
|
|
89
|
+
const TIMESTAMP = /^[0-9]{1,12}$/;
|
|
90
|
+
const NONCE = /^[A-Za-z0-9_-]{16,64}$/;
|
|
91
|
+
const SIGNATURE = /^[A-Za-z0-9_-]{86}$/;
|
|
92
|
+
const METHOD = /^[A-Z]{1,10}$/;
|
|
93
|
+
const TARGET = /^\/[\x21-\x7e]{0,4095}$/;
|
|
94
|
+
const DIGEST = /^[0-9a-f]{64}$/;
|
|
95
|
+
const VERB = /^[a-z0-9-]{1,32}$/;
|
|
96
|
+
const SPKI_ED25519_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
97
|
+
const AUDIT_MAX_BYTES = 256 * 1024;
|
|
98
|
+
export const AUDIT_GENERATIONS = 5;
|
|
99
|
+
export const REFUSAL_ROWS_PER_MINUTE = 30;
|
|
100
|
+
/** Outcomes decided before any signature verified; they go to the refusals log. */
|
|
101
|
+
export const UNAUTHENTICATED_OUTCOMES = [
|
|
102
|
+
"unsigned",
|
|
103
|
+
"unknown-device",
|
|
104
|
+
"stale-request",
|
|
105
|
+
"bad-signature",
|
|
106
|
+
"registry-insecure",
|
|
107
|
+
"registry-unreadable",
|
|
108
|
+
];
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Where the phone files live: under the autopilot root, which an unattended
|
|
112
|
+
* run may not write (unattended-policy.json, protectedHostEntries).
|
|
113
|
+
*/
|
|
114
|
+
export function phoneDir(env = process.env) {
|
|
115
|
+
return join(autopilotRoot(env), "phone");
|
|
116
|
+
}
|
|
117
|
+
export const registryPath = (env = process.env) => join(phoneDir(env), "devices.json");
|
|
118
|
+
export const auditPath = (env = process.env) => join(phoneDir(env), "audit.jsonl");
|
|
119
|
+
export const refusalsPath = (env = process.env) => join(phoneDir(env), "refusals.jsonl");
|
|
120
|
+
export const noncesPath = (env = process.env) => join(phoneDir(env), "nonces.json");
|
|
121
|
+
|
|
122
|
+
/** SHA-256 of the raw body bytes, lowercase hex. */
|
|
123
|
+
export const bodyDigest = (buf) =>
|
|
124
|
+
createHash("sha256")
|
|
125
|
+
.update(buf ?? Buffer.alloc(0))
|
|
126
|
+
.digest("hex");
|
|
127
|
+
|
|
128
|
+
/** The text a device signs. Throws on any field that could break the framing. */
|
|
129
|
+
export function canonicalPayload({
|
|
130
|
+
method,
|
|
131
|
+
target,
|
|
132
|
+
bodyDigest: digest,
|
|
133
|
+
deviceId,
|
|
134
|
+
timestamp,
|
|
135
|
+
nonce,
|
|
136
|
+
}) {
|
|
137
|
+
const checks = [
|
|
138
|
+
[METHOD, method, "method"],
|
|
139
|
+
[TARGET, target, "target"],
|
|
140
|
+
[DIGEST, digest, "body digest"],
|
|
141
|
+
[DEVICE_ID, deviceId, "device id"],
|
|
142
|
+
[TIMESTAMP, timestamp, "timestamp"],
|
|
143
|
+
[NONCE, nonce, "nonce"],
|
|
144
|
+
];
|
|
145
|
+
for (const [re, v, name] of checks) {
|
|
146
|
+
if (typeof v !== "string" || !re.test(v)) throw new Error(`${name} is not canonical`);
|
|
147
|
+
}
|
|
148
|
+
return [SIGNATURE_VERSION, method, target, digest, deviceId, timestamp, nonce].join("\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** A raw 32-byte Ed25519 public key (base64url) as a KeyObject. */
|
|
152
|
+
export function publicKeyObject(raw) {
|
|
153
|
+
const bytes = Buffer.from(String(raw), "base64url");
|
|
154
|
+
if (bytes.length !== 32) throw new Error("an Ed25519 public key is 32 bytes");
|
|
155
|
+
return createPublicKey({
|
|
156
|
+
key: Buffer.concat([SPKI_ED25519_PREFIX, bytes]),
|
|
157
|
+
format: "der",
|
|
158
|
+
type: "spki",
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Normalize what a person pastes at enrolment: a raw base64url key or an SPKI
|
|
164
|
+
* PEM block. Returns the raw key as base64url; throws on anything that is not
|
|
165
|
+
* an Ed25519 public key.
|
|
166
|
+
*/
|
|
167
|
+
export function parsePublicKey(input) {
|
|
168
|
+
const text = String(input ?? "").trim();
|
|
169
|
+
if (/PRIVATE KEY/.test(text)) throw new Error("that is a private key; paste the public half");
|
|
170
|
+
let key;
|
|
171
|
+
if (text.startsWith("-----BEGIN")) {
|
|
172
|
+
key = createPublicKey({ key: text, format: "pem" });
|
|
173
|
+
} else {
|
|
174
|
+
if (!/^[A-Za-z0-9_-]{43}=?$/.test(text)) throw new Error("not a base64url Ed25519 key");
|
|
175
|
+
key = publicKeyObject(text.replace(/=$/, ""));
|
|
176
|
+
}
|
|
177
|
+
if (key.asymmetricKeyType !== "ed25519") throw new Error("the key is not Ed25519");
|
|
178
|
+
const der = key.export({ format: "der", type: "spki" });
|
|
179
|
+
return der.subarray(SPKI_ED25519_PREFIX.length).toString("base64url");
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Short, stable identifier of a public key, for listing and the audit trail. */
|
|
183
|
+
export const fingerprint = (raw) =>
|
|
184
|
+
createHash("sha256").update(Buffer.from(raw, "base64url")).digest("hex").slice(0, 16);
|
|
185
|
+
|
|
186
|
+
export const newDeviceId = () => `d-${randomBytes(8).toString("hex")}`;
|
|
187
|
+
|
|
188
|
+
const emptyRegistry = () => ({
|
|
189
|
+
schemaVersion: REGISTRY_VERSION,
|
|
190
|
+
launchEnabled: false,
|
|
191
|
+
devices: [],
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The registry, or an empty one when it is missing, unreadable, or writable by
|
|
196
|
+
* anyone but its owner. `insecure` says which of those it was.
|
|
197
|
+
*/
|
|
198
|
+
export function readRegistry(env = process.env) {
|
|
199
|
+
const file = registryPath(env);
|
|
200
|
+
if (!existsSync(file)) return { ...emptyRegistry(), insecure: null };
|
|
201
|
+
try {
|
|
202
|
+
const st = statSync(file);
|
|
203
|
+
const uid = typeof process.getuid === "function" ? process.getuid() : st.uid;
|
|
204
|
+
if ((st.mode & 0o022) !== 0 || st.uid !== uid) {
|
|
205
|
+
return { ...emptyRegistry(), insecure: "registry-insecure" };
|
|
206
|
+
}
|
|
207
|
+
const doc = JSON.parse(readFileSync(file, "utf8"));
|
|
208
|
+
if (!doc || !Array.isArray(doc.devices))
|
|
209
|
+
return { ...emptyRegistry(), insecure: "registry-unreadable" };
|
|
210
|
+
return {
|
|
211
|
+
schemaVersion: doc.schemaVersion ?? REGISTRY_VERSION,
|
|
212
|
+
launchEnabled: doc.launchEnabled === true,
|
|
213
|
+
devices: doc.devices,
|
|
214
|
+
insecure: null,
|
|
215
|
+
};
|
|
216
|
+
} catch {
|
|
217
|
+
return { ...emptyRegistry(), insecure: "registry-unreadable" };
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Replace the registry: a 0600 file written beside it and renamed over it. */
|
|
222
|
+
export function writeRegistry(reg, env = process.env) {
|
|
223
|
+
const dir = phoneDir(env);
|
|
224
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
225
|
+
chmodSync(dir, 0o700);
|
|
226
|
+
const file = registryPath(env);
|
|
227
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
228
|
+
const doc = {
|
|
229
|
+
schemaVersion: REGISTRY_VERSION,
|
|
230
|
+
launchEnabled: reg.launchEnabled === true,
|
|
231
|
+
devices: reg.devices,
|
|
232
|
+
};
|
|
233
|
+
rmSync(tmp, { force: true });
|
|
234
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2) + "\n", { mode: 0o600, flag: "wx" });
|
|
235
|
+
renameSync(tmp, file);
|
|
236
|
+
chmodSync(file, 0o600);
|
|
237
|
+
return file;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function readNonceFile(file) {
|
|
241
|
+
try {
|
|
242
|
+
const doc = JSON.parse(readFileSync(file, "utf8"));
|
|
243
|
+
return doc && typeof doc.seen === "object" && doc.seen !== null ? doc.seen : {};
|
|
244
|
+
} catch {
|
|
245
|
+
return {};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function writeNonceFile(file, seen) {
|
|
250
|
+
try {
|
|
251
|
+
const dir = dirname(file);
|
|
252
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
253
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
254
|
+
rmSync(tmp, { force: true });
|
|
255
|
+
writeFileSync(tmp, JSON.stringify({ seen: Object.fromEntries(seen) }) + "\n", {
|
|
256
|
+
mode: 0o600,
|
|
257
|
+
flag: "wx",
|
|
258
|
+
});
|
|
259
|
+
renameSync(tmp, file);
|
|
260
|
+
chmodSync(file, 0o600);
|
|
261
|
+
} catch {
|
|
262
|
+
// The in-memory copy still refuses a replay to this process.
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const nonceKey = (key) => createHash("sha256").update(String(key)).digest("hex").slice(0, 32);
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Nonces seen inside their window. Bounded: when full after dropping expired
|
|
270
|
+
* entries it answers "full" instead of forgetting a live nonce, because
|
|
271
|
+
* eviction is exactly what a replay would need. With `file`, every check first
|
|
272
|
+
* merges what other processes recorded there and every acceptance is written
|
|
273
|
+
* back, so the store outlives a restart.
|
|
274
|
+
*/
|
|
275
|
+
export function createNonceStore({ max = NONCE_STORE_MAX, file = null } = {}) {
|
|
276
|
+
const seen = new Map();
|
|
277
|
+
const prune = (now) => {
|
|
278
|
+
for (const [k, exp] of seen) if (exp < now) seen.delete(k);
|
|
279
|
+
};
|
|
280
|
+
const merge = () => {
|
|
281
|
+
if (!file) return;
|
|
282
|
+
for (const [k, exp] of Object.entries(readNonceFile(file))) {
|
|
283
|
+
if (typeof exp !== "number") continue;
|
|
284
|
+
if (!(seen.get(k) >= exp)) seen.set(k, exp);
|
|
285
|
+
}
|
|
286
|
+
};
|
|
287
|
+
return {
|
|
288
|
+
remember(key, expiresAt, now) {
|
|
289
|
+
const k = file ? nonceKey(key) : key;
|
|
290
|
+
merge();
|
|
291
|
+
const exp = seen.get(k);
|
|
292
|
+
if (exp !== undefined && exp >= now) return "replayed";
|
|
293
|
+
if (exp !== undefined) seen.delete(k);
|
|
294
|
+
if (seen.size >= max) prune(now);
|
|
295
|
+
if (seen.size >= max) return "full";
|
|
296
|
+
seen.set(k, expiresAt);
|
|
297
|
+
if (file) {
|
|
298
|
+
prune(now);
|
|
299
|
+
writeNonceFile(file, seen);
|
|
300
|
+
}
|
|
301
|
+
return "ok";
|
|
302
|
+
},
|
|
303
|
+
size: () => seen.size,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const header = (headers, name) => {
|
|
308
|
+
const v = headers[name];
|
|
309
|
+
return Array.isArray(v) ? null : (v ?? null);
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
const refuse = (status, code, deviceId = null) => ({ ok: false, status, code, deviceId });
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Decide whether a phone request is authentic. Pure apart from the nonce store
|
|
316
|
+
* it is handed.
|
|
317
|
+
*
|
|
318
|
+
* @returns {{ok: true, device: object} | {ok: false, status: number, code: string, deviceId: string|null}}
|
|
319
|
+
*/
|
|
320
|
+
export function verifyRequest({
|
|
321
|
+
method,
|
|
322
|
+
target,
|
|
323
|
+
headers,
|
|
324
|
+
body,
|
|
325
|
+
registry,
|
|
326
|
+
nonces,
|
|
327
|
+
nowSec,
|
|
328
|
+
notBeforeSec,
|
|
329
|
+
}) {
|
|
330
|
+
const deviceId = header(headers, HEADERS.device);
|
|
331
|
+
const ts = header(headers, HEADERS.timestamp);
|
|
332
|
+
const nonce = header(headers, HEADERS.nonce);
|
|
333
|
+
const sig = header(headers, HEADERS.signature);
|
|
334
|
+
if (
|
|
335
|
+
!DEVICE_ID.test(deviceId ?? "") ||
|
|
336
|
+
!TIMESTAMP.test(ts ?? "") ||
|
|
337
|
+
!NONCE.test(nonce ?? "") ||
|
|
338
|
+
!SIGNATURE.test(sig ?? "")
|
|
339
|
+
) {
|
|
340
|
+
return refuse(401, "unsigned", DEVICE_ID.test(deviceId ?? "") ? deviceId : null);
|
|
341
|
+
}
|
|
342
|
+
const device = registry.devices.find((d) => d && d.id === deviceId);
|
|
343
|
+
if (!device) return refuse(401, "unknown-device", deviceId);
|
|
344
|
+
const t = Number(ts);
|
|
345
|
+
if (nowSec - t > WINDOW_SEC || t - nowSec > FUTURE_SKEW_SEC || t < notBeforeSec) {
|
|
346
|
+
return refuse(401, "stale-request", deviceId);
|
|
347
|
+
}
|
|
348
|
+
let text;
|
|
349
|
+
try {
|
|
350
|
+
text = canonicalPayload({
|
|
351
|
+
method,
|
|
352
|
+
target,
|
|
353
|
+
bodyDigest: bodyDigest(body),
|
|
354
|
+
deviceId,
|
|
355
|
+
timestamp: ts,
|
|
356
|
+
nonce,
|
|
357
|
+
});
|
|
358
|
+
} catch {
|
|
359
|
+
return refuse(401, "bad-signature", deviceId);
|
|
360
|
+
}
|
|
361
|
+
let good;
|
|
362
|
+
try {
|
|
363
|
+
good = verify(
|
|
364
|
+
null,
|
|
365
|
+
Buffer.from(text, "utf8"),
|
|
366
|
+
publicKeyObject(device.publicKey),
|
|
367
|
+
Buffer.from(sig, "base64url"),
|
|
368
|
+
);
|
|
369
|
+
} catch {
|
|
370
|
+
good = false;
|
|
371
|
+
}
|
|
372
|
+
if (!good) return refuse(401, "bad-signature", deviceId);
|
|
373
|
+
if (device.revokedAt) return refuse(401, "revoked-device", deviceId);
|
|
374
|
+
const seen = nonces.remember(`${deviceId}:${nonce}`, t + WINDOW_SEC, nowSec);
|
|
375
|
+
if (seen === "replayed") return refuse(401, "replayed", deviceId);
|
|
376
|
+
if (seen === "full") return refuse(503, "replay-store-full", deviceId);
|
|
377
|
+
return { ok: true, device };
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The headers a device sends. The reference for a client implementation, and
|
|
382
|
+
* what the tests and smokes sign with.
|
|
383
|
+
*/
|
|
384
|
+
export function signRequest({
|
|
385
|
+
method,
|
|
386
|
+
target,
|
|
387
|
+
body = Buffer.alloc(0),
|
|
388
|
+
deviceId,
|
|
389
|
+
privateKey,
|
|
390
|
+
timestamp,
|
|
391
|
+
nonce,
|
|
392
|
+
}) {
|
|
393
|
+
const ts = String(timestamp ?? Math.floor(Date.now() / 1000));
|
|
394
|
+
const n = nonce ?? randomBytes(18).toString("base64url");
|
|
395
|
+
const text = canonicalPayload({
|
|
396
|
+
method,
|
|
397
|
+
target,
|
|
398
|
+
bodyDigest: bodyDigest(Buffer.isBuffer(body) ? body : Buffer.from(String(body), "utf8")),
|
|
399
|
+
deviceId,
|
|
400
|
+
timestamp: ts,
|
|
401
|
+
nonce: n,
|
|
402
|
+
});
|
|
403
|
+
return {
|
|
404
|
+
"X-MA-Device": deviceId,
|
|
405
|
+
"X-MA-Timestamp": ts,
|
|
406
|
+
"X-MA-Nonce": n,
|
|
407
|
+
"X-MA-Signature": sign(null, Buffer.from(text, "utf8"), privateKey).toString("base64url"),
|
|
408
|
+
};
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/** The verb a phone path names, or "unknown" when it is not a plain word. */
|
|
412
|
+
export function verbOf(pathname) {
|
|
413
|
+
const seg = String(pathname).slice(PHONE_PREFIX.length).split("/")[0];
|
|
414
|
+
return VERB.test(seg) ? seg : "unknown";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function rotate(file, generations) {
|
|
418
|
+
if (!existsSync(file) || statSync(file).size <= AUDIT_MAX_BYTES) return;
|
|
419
|
+
for (let i = generations - 1; i >= 1; i--) {
|
|
420
|
+
if (existsSync(`${file}.${i}`)) renameSync(`${file}.${i}`, `${file}.${i + 1}`);
|
|
421
|
+
}
|
|
422
|
+
renameSync(file, `${file}.1`);
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** Per-process budget for the refusals log: rows written and dropped this minute. */
|
|
426
|
+
const refusalBudget = { minute: -1, written: 0, dropped: 0 };
|
|
427
|
+
|
|
428
|
+
/** Reset the refusals budget; for tests that call appendAudit directly. */
|
|
429
|
+
export function resetRefusalBudget() {
|
|
430
|
+
refusalBudget.minute = -1;
|
|
431
|
+
refusalBudget.written = 0;
|
|
432
|
+
refusalBudget.dropped = 0;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* One audit line. Only fixed-shape fields: nothing a caller controls reaches
|
|
437
|
+
* the file unless it matched a strict pattern first. A refusal decided before
|
|
438
|
+
* a signature verified goes to the refusals log instead, at most
|
|
439
|
+
* REFUSAL_ROWS_PER_MINUTE rows a minute; the first row written after rows were
|
|
440
|
+
* dropped carries `suppressed`, their count. Best effort: a log that cannot be
|
|
441
|
+
* written never turns a refusal into an acceptance or the reverse.
|
|
442
|
+
*/
|
|
443
|
+
export function appendAudit(
|
|
444
|
+
{ deviceId, method, verb, status, outcome },
|
|
445
|
+
env = process.env,
|
|
446
|
+
nowMs = Date.now(),
|
|
447
|
+
) {
|
|
448
|
+
try {
|
|
449
|
+
const dir = phoneDir(env);
|
|
450
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
451
|
+
const row = {
|
|
452
|
+
at: new Date(nowMs).toISOString(),
|
|
453
|
+
deviceId: DEVICE_ID.test(deviceId ?? "") ? deviceId : null,
|
|
454
|
+
method: METHOD.test(method ?? "") ? method : "?",
|
|
455
|
+
verb: VERB.test(verb ?? "") ? verb : "unknown",
|
|
456
|
+
status,
|
|
457
|
+
outcome: VERB.test(outcome ?? "") ? outcome : "unknown",
|
|
458
|
+
};
|
|
459
|
+
let file = auditPath(env);
|
|
460
|
+
let generations = AUDIT_GENERATIONS;
|
|
461
|
+
if (UNAUTHENTICATED_OUTCOMES.includes(row.outcome)) {
|
|
462
|
+
const minute = Math.floor(nowMs / 60000);
|
|
463
|
+
if (minute !== refusalBudget.minute) {
|
|
464
|
+
refusalBudget.minute = minute;
|
|
465
|
+
refusalBudget.written = 0;
|
|
466
|
+
}
|
|
467
|
+
if (refusalBudget.written >= REFUSAL_ROWS_PER_MINUTE) {
|
|
468
|
+
refusalBudget.dropped++;
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
refusalBudget.written++;
|
|
472
|
+
if (refusalBudget.dropped) {
|
|
473
|
+
row.suppressed = refusalBudget.dropped;
|
|
474
|
+
refusalBudget.dropped = 0;
|
|
475
|
+
}
|
|
476
|
+
file = refusalsPath(env);
|
|
477
|
+
generations = 1;
|
|
478
|
+
}
|
|
479
|
+
rotate(file, generations);
|
|
480
|
+
appendFileSync(file, JSON.stringify(row) + "\n", { mode: 0o600 });
|
|
481
|
+
chmodSync(file, 0o600);
|
|
482
|
+
} catch {
|
|
483
|
+
// The request's answer does not depend on the log.
|
|
484
|
+
}
|
|
485
|
+
}
|