@yemi33/minions 0.1.2448 → 0.1.2449
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/bin/cli-api-client.js +1 -1
- package/bin/install-internal-minions.js +1382 -44
- package/bin/install-layout.js +150 -0
- package/bin/minions.js +460 -167
- package/dashboard/docs/typography.md +65 -12
- package/dashboard/js/command-center.js +58 -4
- package/dashboard/js/detail-panel.js +36 -0
- package/dashboard/js/memory-panel.js +59 -12
- package/dashboard/js/qa.js +179 -20
- package/dashboard/js/refresh.js +148 -12
- package/dashboard/js/render-dispatch.js +3 -4
- package/dashboard/js/render-inbox.js +2 -2
- package/dashboard/js/render-other.js +3 -3
- package/dashboard/js/render-pipelines.js +14 -0
- package/dashboard/js/render-plans.js +57 -9
- package/dashboard/js/render-prd.js +132 -23
- package/dashboard/js/render-prs.js +195 -166
- package/dashboard/js/render-schedules.js +63 -3
- package/dashboard/js/render-utils.js +3 -3
- package/dashboard/js/render-watches.js +19 -3
- package/dashboard/js/render-work-items.js +238 -30
- package/dashboard/js/settings.js +205 -54
- package/dashboard/js/utils.js +51 -1
- package/dashboard/pages/home.html +1 -1
- package/dashboard/pages/qa.html +1 -16
- package/dashboard/pages/work.html +40 -0
- package/dashboard/shared/cc-limits.js +79 -0
- package/dashboard/shared/pr-filters.js +21 -38
- package/dashboard/shared/project-git-summary.js +1 -1
- package/dashboard/shared/record-filters.js +169 -0
- package/dashboard/shared/watches-source.js +1 -1
- package/dashboard/shared/welcome-popup.js +1 -1
- package/dashboard/shared/wi-filters.js +302 -0
- package/dashboard/slim/body.html +1 -0
- package/dashboard/slim/js/command-send.js +26 -0
- package/dashboard/slim/js/modals-tiles.js +380 -39
- package/dashboard/slim/js/status.js +13 -21
- package/dashboard/slim/layout.html +1 -0
- package/dashboard/slim/panel-bootstrap.js +6 -2
- package/dashboard/slim/styles.css +38 -0
- package/dashboard/styles.css +159 -55
- package/dashboard-build.js +13 -2
- package/dashboard.js +956 -423
- package/docs/README.md +11 -6
- package/docs/api-errors.md +2 -2
- package/docs/architecture-review-2026-07-09.md +1 -1
- package/docs/architecture.excalidraw +2 -2
- package/docs/auto-discovery.md +18 -9
- package/docs/branch-derivation.md +4 -4
- package/docs/capture-demos.js +39 -2
- package/docs/ci-runner-canary.md +123 -0
- package/docs/claude-md-propagation.md +2 -2
- package/docs/cloud-agent-dispatch.md +204 -0
- package/docs/command-center.md +7 -7
- package/docs/completion-reports.md +43 -20
- package/docs/constants.md +10 -3
- package/docs/constellation-bridge.md +134 -6
- package/docs/constellation-style-telemetry.md +4 -4
- package/docs/contracts/capability-protocol.v1.json +165 -0
- package/docs/cooldown-merge-semantics.md +12 -12
- package/docs/copilot-cli-schema.md +7 -7
- package/docs/cross-repo-plans.md +10 -10
- package/docs/dead-code-audit-retractions.md +5 -5
- package/docs/default-branch-ci.md +173 -0
- package/docs/deprecated.json +31 -31
- package/docs/design-inbox-entries-schema.md +3 -3
- package/docs/design-language.md +1051 -0
- package/docs/design-state-storage.md +11 -11
- package/docs/diagnostics-crash-reports.md +9 -9
- package/docs/diagnostics-memory.md +5 -5
- package/docs/documentation-audit-2026-07-09.md +7 -7
- package/docs/engine-restart.md +90 -5
- package/docs/harness-mode.md +1 -1
- package/docs/internal-install.md +338 -39
- package/docs/kb-dedup-duplicate-pair-investigation.md +5 -5
- package/docs/kb-pr3223-cascade-archiving.md +1 -1
- package/docs/kb-pr696-merge-conflict-docs.md +6 -6
- package/docs/kb-sweep.md +35 -35
- package/docs/keep-processes.md +1 -1
- package/docs/live-checkout-mode.md +30 -30
- package/docs/managed-spawn.md +18 -14
- package/docs/named-agents.md +7 -7
- package/docs/plan-lifecycle.md +69 -2
- package/docs/pr-author-identity.md +114 -0
- package/docs/pr-auto-fix-dispatch.md +19 -4
- package/docs/pr-comment-followup.md +6 -6
- package/docs/pr-review-fix-loop.md +59 -10
- package/docs/process-termination.md +40 -0
- package/docs/proposals/repo-pool-for-live-checkout.md +13 -13
- package/docs/qa-runbook-lifecycle.md +367 -17
- package/docs/qa-runbooks.md +3 -3
- package/docs/rfc-completion-json.md +18 -18
- package/docs/runtime-adapters.md +26 -21
- package/docs/security.md +6 -6
- package/docs/self-improvement.md +4 -4
- package/docs/shared-lifecycle-module-map.md +473 -472
- package/docs/skills.md +52 -3
- package/docs/slim-ux/concepts.md +121 -116
- package/docs/specs/agent-configurability.md +18 -18
- package/docs/specs/agent-rename.md +18 -18
- package/docs/team-memory.md +38 -21
- package/docs/timeouts-and-liveness.md +118 -10
- package/docs/tutorials/01-install-and-connect.md +1 -1
- package/docs/watches.md +40 -39
- package/docs/workspace-manifests.md +4 -4
- package/docs/worktree-lifecycle.md +293 -14
- package/engine/README.md +46 -0
- package/engine/{ado-comment.js → ado/comment.js} +8 -8
- package/engine/{ado-git-auth.js → ado/git-auth.js} +4 -4
- package/engine/{ado.js → ado/index.js} +417 -63
- package/engine/{ado-status.js → ado/status.js} +6 -8
- package/engine/{ado-token.js → ado/token.js} +1 -1
- package/engine/{acp-transport.js → agents/acp-transport.js} +62 -22
- package/engine/{agent-worker-pool.js → agents/agent-worker-pool.js} +17 -8
- package/engine/{cc-worker-pool.js → agents/cc-worker-pool.js} +16 -6
- package/engine/{claude-md-context.js → agents/claude-md-context.js} +5 -5
- package/engine/{harness-context.js → agents/harness-context.js} +5 -5
- package/engine/{harness.js → agents/harness.js} +3 -3
- package/engine/{llm.js → agents/llm.js} +18 -14
- package/engine/{model-discovery.js → agents/model-discovery.js} +2 -2
- package/engine/{playbook.js → agents/playbook.js} +155 -22
- package/engine/{pooled-agent-process.js → agents/pooled-agent-process.js} +14 -12
- package/engine/{preflight.js → agents/preflight.js} +29 -10
- package/engine/{spawn-agent.js → agents/spawn-agent.js} +25 -14
- package/engine/{spawn-phase-watchdog.js → agents/spawn-phase-watchdog.js} +16 -7
- package/engine/{steering.js → agents/steering.js} +5 -5
- package/engine/{tools-inventory.js → agents/tools-inventory.js} +2 -2
- package/engine/{agent-api-validation.js → api/agent-api-validation.js} +2 -2
- package/engine/{api-validation.js → api/api-validation.js} +1 -1
- package/engine/api/bridge.js +787 -0
- package/engine/{cc-api-validation.js → api/cc-api-validation.js} +1 -1
- package/engine/api/companion.js +560 -0
- package/engine/{content-api-validation.js → api/content-api-validation.js} +2 -2
- package/engine/{pr-issue-validation.js → api/pr-issue-validation.js} +33 -6
- package/engine/{settings-validation.js → api/settings-validation.js} +32 -4
- package/engine/api-contracts/agent-content.js +4 -4
- package/engine/api-contracts/capability-manifest.js +236 -0
- package/engine/api-contracts/capability-protocol.js +333 -0
- package/engine/api-contracts/cc-ops.js +1 -1
- package/engine/api-contracts/config-runtime.js +5 -0
- package/engine/api-contracts/core.js +28 -1
- package/engine/api-contracts/index.js +100 -0
- package/engine/api-contracts/orchestration.js +18 -5
- package/engine/api-contracts/pull-requests.js +37 -6
- package/engine/api-contracts/qa-process.js +29 -6
- package/engine/api-contracts/work-plan-prd.js +21 -1
- package/engine/cloud/contract.js +212 -0
- package/engine/cloud/index.js +159 -0
- package/engine/{execution-model.js → core/execution-model.js} +1 -1
- package/engine/{features.js → core/features.js} +4 -4
- package/engine/{operator-identity.js → core/operator-identity.js} +1 -1
- package/engine/{queries.js → core/queries.js} +201 -36
- package/engine/{safe-expr.js → core/safe-expr.js} +1 -1
- package/engine/{shared.js → core/shared.js} +1637 -175
- package/engine/{stdio-timestamps.js → core/stdio-timestamps.js} +1 -1
- package/engine/{untrusted-fence.js → core/untrusted-fence.js} +3 -3
- package/engine/db/index.js +11 -2
- package/engine/db/migrations/002-dispatches.js +3 -3
- package/engine/db/migrations/003-work-items.js +1 -1
- package/engine/db/migrations/004-pull-requests.js +1 -1
- package/engine/db/migrations/006-metrics.js +1 -1
- package/engine/db/migrations/007-watches.js +2 -2
- package/engine/db/migrations/008-small-state.js +1 -1
- package/engine/db/migrations/009-qa.js +1 -1
- package/engine/db/migrations/010-pr-links.js +1 -1
- package/engine/db/migrations/011-remaining-state.js +1 -1
- package/engine/db/migrations/012-steering-deliveries.js +2 -2
- package/engine/db/migrations/013-backfill-broken-note-links.js +1 -1
- package/engine/db/migrations/014-pr-fix-target-prefs.js +2 -2
- package/engine/db/migrations/015-plans-prds.js +0 -0
- package/engine/db/migrations/018-sql-only-cutover.js +2 -2
- package/engine/db/migrations/021-archived-work-items.js +1 -1
- package/engine/db/migrations/022-global-cc-session.js +1 -1
- package/engine/db/migrations/023-engine-state.js +1 -1
- package/engine/db/migrations/025-malformed-work-item-phantoms.js +1 -1
- package/engine/db/migrations/027-review-learning-lifecycle.js +1 -1
- package/engine/db/migrations/029-repair-reused-versions.js +20 -0
- package/engine/db/migrations/031-pr-author-identity.js +137 -0
- package/engine/{consolidation.js → memory/consolidation.js} +6 -6
- package/engine/{kb-sweep-runner.js → memory/kb-sweep-runner.js} +2 -2
- package/engine/{kb-sweep.js → memory/kb-sweep.js} +9 -7
- package/engine/{memory-retrieval.js → memory/memory-retrieval.js} +46 -4
- package/engine/{memory-store.js → memory/memory-store.js} +3 -3
- package/engine/{promotion.js → memory/promotion.js} +3 -3
- package/engine/{review-learning-backfill.js → memory/review-learning-backfill.js} +6 -6
- package/engine/{review-learning.js → memory/review-learning.js} +10 -5
- package/engine/{diagnostics-memory.js → observability/diagnostics-memory.js} +1 -1
- package/engine/{logs-store.js → observability/logs-store.js} +5 -5
- package/engine/{metrics-store.js → observability/metrics-store.js} +4 -4
- package/engine/{check-status.js → operations/check-status.js} +3 -3
- package/engine/{cli.js → operations/cli.js} +271 -113
- package/engine/{distribution.js → operations/distribution.js} +5 -6
- package/engine/{cleanup.js → orchestration/cleanup.js} +72 -45
- package/engine/{cooldown.js → orchestration/cooldown.js} +5 -5
- package/engine/{dispatch-events.js → orchestration/dispatch-events.js} +2 -2
- package/engine/{dispatch.js → orchestration/dispatch.js} +129 -36
- package/engine/orchestration/failed-scheduled-cleanup.js +274 -0
- package/engine/{lifecycle.js → orchestration/lifecycle.js} +198 -90
- package/engine/{meeting.js → orchestration/meeting.js} +6 -16
- package/engine/{pipeline.js → orchestration/pipeline.js} +12 -12
- package/engine/{pre-dispatch-eval.js → orchestration/pre-dispatch-eval.js} +10 -9
- package/engine/{routing.js → orchestration/routing.js} +3 -3
- package/engine/{schedule-bootstrap.js → orchestration/schedule-bootstrap.js} +4 -4
- package/engine/{scheduler.js → orchestration/scheduler.js} +38 -8
- package/engine/{timeout.js → orchestration/timeout.js} +158 -109
- package/engine/{db-events.js → persistence/db-events.js} +2 -2
- package/engine/{dispatch-store.js → persistence/dispatch-store.js} +7 -7
- package/engine/{inbox-store.js → persistence/inbox-store.js} +2 -2
- package/engine/{note-link-backfill.js → persistence/note-link-backfill.js} +4 -4
- package/engine/{pr-fix-target-store.js → persistence/pr-fix-target-store.js} +8 -8
- package/engine/{pull-requests-store.js → persistence/pull-requests-store.js} +21 -7
- package/engine/{small-state-store.js → persistence/small-state-store.js} +31 -31
- package/engine/persistence/state-operations.js +350 -0
- package/engine/{steering-store.js → persistence/steering-store.js} +6 -6
- package/engine/{issues.js → planning/issues.js} +2 -2
- package/engine/{plan-prd-validation.js → planning/plan-prd-validation.js} +8 -2
- package/engine/planning/prd-result-sidecar.js +190 -0
- package/engine/{prd-store.js → planning/prd-store.js} +17 -17
- package/engine/{project-discovery.js → planning/project-discovery.js} +5 -5
- package/engine/{projects.js → planning/projects.js} +10 -10
- package/engine/{resolve-area.js → planning/resolve-area.js} +1 -1
- package/engine/{work-item-validation.js → planning/work-item-validation.js} +39 -3
- package/engine/{work-items-store.js → planning/work-items-store.js} +29 -21
- package/engine/{keep-process-sweep.js → processes/keep-process-sweep.js} +57 -17
- package/engine/{managed-spawn-launcher.js → processes/managed-spawn-launcher.js} +3 -3
- package/engine/{managed-spawn.js → processes/managed-spawn.js} +97 -46
- package/engine/{process-utils.js → processes/process-utils.js} +599 -55
- package/engine/{abandoned-pr-reconciliation.js → providers/abandoned-pr-reconciliation.js} +17 -7
- package/engine/{comment-classifier.js → providers/comment-classifier.js} +85 -17
- package/engine/{comment-format.js → providers/comment-format.js} +5 -5
- package/engine/{gh-comment.js → providers/gh-comment.js} +15 -15
- package/engine/{gh-token.js → providers/gh-token.js} +4 -4
- package/engine/{github.js → providers/github.js} +131 -54
- package/engine/{pr-action.js → providers/pr-action.js} +13 -12
- package/engine/{pr-clone-keep.js → providers/pr-clone-keep.js} +7 -7
- package/engine/{pr-devbox.js → providers/pr-devbox.js} +6 -6
- package/engine/{pr-fix-target.js → providers/pr-fix-target.js} +13 -13
- package/engine/{pr-remote-patch.js → providers/pr-remote-patch.js} +4 -4
- package/engine/{pr-resolve.js → providers/pr-resolve.js} +7 -7
- package/engine/{pr-temp-clone.js → providers/pr-temp-clone.js} +5 -5
- package/engine/{pr-track.js → providers/pr-track.js} +11 -13
- package/engine/{shared-branch-pr-reconcile.js → providers/shared-branch-pr-reconcile.js} +4 -4
- package/engine/qa/auto-prd-qa.js +313 -0
- package/engine/{qa-from-prd.js → qa/from-prd.js} +42 -12
- package/engine/qa/prd-session.js +240 -0
- package/engine/{qa-process-validation.js → qa/process-validation.js} +14 -9
- package/engine/{qa-runbooks.js → qa/runbooks.js} +1 -1
- package/engine/{qa-runs.js → qa/runs.js} +286 -15
- package/engine/{qa-sessions.js → qa/sessions.js} +595 -49
- package/engine/qa/visual-journey.js +654 -0
- package/engine/{qa-runners.js → qa-runners/index.js} +7 -7
- package/engine/qa-runners/maestro.js +3 -3
- package/engine/qa-runners/playwright.js +2 -2
- package/engine/{restart-health.js → recovery/restart-health.js} +48 -4
- package/engine/recovery/stop-stack.js +607 -0
- package/engine/{supervisor.js → recovery/supervisor.js} +105 -175
- package/engine/{watchdog.js → recovery/watchdog.js} +136 -13
- package/engine/runtimes/claude.js +14 -12
- package/engine/runtimes/codex.js +8 -6
- package/engine/runtimes/copilot.js +17 -16
- package/engine/{watch-actions.js → watches/actions.js} +13 -13
- package/engine/{watches.js → watches/index.js} +43 -32
- package/engine/{watches-store.js → watches/store.js} +4 -4
- package/engine/{create-pr-worktree.js → worktrees/create-pr.js} +1 -1
- package/engine/{worktree-gc.js → worktrees/gc.js} +70 -22
- package/engine/worktrees/inventory.js +671 -0
- package/engine/{live-checkout.js → worktrees/live-checkout.js} +4 -4
- package/engine/{worktree-pool.js → worktrees/pool.js} +2 -2
- package/engine/{worktree-preflight.js → worktrees/preflight.js} +1 -0
- package/engine/worktrees/quarantine-refs.js +173 -0
- package/engine.js +1137 -208
- package/minions.js +147 -77
- package/package.json +10 -6
- package/playbooks/_pr-description-audit.md +110 -78
- package/playbooks/build-fix-complex.md +2 -0
- package/playbooks/fix.md +16 -12
- package/playbooks/implement-shared.md +2 -0
- package/playbooks/implement.md +19 -20
- package/playbooks/plan-to-prd.md +18 -3
- package/playbooks/qa-session-draft.md +136 -1
- package/playbooks/qa-session-execute.md +80 -2
- package/playbooks/qa-session-setup.md +17 -1
- package/playbooks/qa-validate.md +1 -1
- package/playbooks/setup.md +2 -0
- package/playbooks/shared-rules.md +25 -32
- package/playbooks/templates/followup-dispatch.md +4 -3
- package/playbooks/verify.md +1 -1
- package/prompts/cc-system.md +19 -27
- package/watch-plugins/README.md +92 -0
- package/watch-plugins/ado-author-prs.js +336 -0
- package/watch-plugins/gh-author-prs.js +375 -0
- package/watch-plugins/http.js +474 -0
- package/watch-plugins/teams-channel.js +869 -0
- package/docs/dev-composite-workflow.md +0 -101
- package/docs/pr-screenshots/pr-886/after-single-header.png +0 -0
- package/docs/pr-screenshots/pr-886/before-duplicate-header.png +0 -0
- package/docs/pr-screenshots/pr-895/01-cancellation-reason-detail.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-899/worker-pool-worktrees-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-default.png +0 -0
- package/docs/pr-screenshots/pr-901/projects-tab-fmf-selected.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE-crop.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-916/model-picker-dropdown-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-979/auto-fix-pane-BEFORE.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-AFTER.png +0 -0
- package/docs/pr-screenshots/pr-985/pr-column-em-dash-BEFORE.png +0 -0
- package/docs/visual-evidence-ci.md +0 -103
- package/engine/bridge.js +0 -379
- package/engine/quarantine-refs.js +0 -103
- package/engine/state-operations.js +0 -178
- /package/engine/{steering-constraints.js → agents/steering-constraints.js} +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const apiValidation = require('./api-validation');
|
|
4
|
-
const features = require('
|
|
5
|
-
const modelDiscovery = require('
|
|
6
|
-
const runtimes = require('
|
|
7
|
-
const
|
|
4
|
+
const features = require('../core/features');
|
|
5
|
+
const modelDiscovery = require('../agents/model-discovery');
|
|
6
|
+
const runtimes = require('../runtimes');
|
|
7
|
+
const cloudProviders = require('../cloud');
|
|
8
|
+
const shared = require('../core/shared');
|
|
8
9
|
|
|
9
10
|
const MAX_SETTINGS_ERRORS = 25;
|
|
10
11
|
const SETTINGS_TOP_LEVEL_FIELDS = new Set(['engine', 'claude', 'agents', 'projects', 'features']);
|
|
@@ -57,6 +58,9 @@ const ENGINE_NUMERIC_RANGES = Object.freeze({
|
|
|
57
58
|
worktreeCreateTimeout: { min: 60000 },
|
|
58
59
|
worktreeCreateRetries: { min: 0, max: 3 },
|
|
59
60
|
worktreeMinFreeSpaceBytes: { min: 0, max: 10 * 1024 * 1024 * 1024 * 1024 },
|
|
61
|
+
// W-ms5tb6ha016fda10 — 0 disables (keep protected quarantine dirs forever);
|
|
62
|
+
// cap at 365 days so a typo cannot silently mean "never".
|
|
63
|
+
worktreeProtectedQuarantineRetentionMs: { min: 0, max: 365 * 24 * 60 * 60 * 1000 },
|
|
60
64
|
assertCleanStatusTimeoutMs: { min: 1000, max: 120000 },
|
|
61
65
|
restartHealthTimeoutMs: { min: 15000, max: 300000 },
|
|
62
66
|
orphanHolderScanTimeoutMs: { min: 1000, max: 30000 },
|
|
@@ -72,9 +76,17 @@ const ENGINE_NUMERIC_RANGES = Object.freeze({
|
|
|
72
76
|
versionCheckInterval: { min: 60000 },
|
|
73
77
|
prPollStatusEvery: { min: 1 },
|
|
74
78
|
prPollCommentsEvery: { min: 1 },
|
|
79
|
+
// W-msav5edk00g81710 — bounded re-probe window for `abandoned` PR records so a
|
|
80
|
+
// reopened PR can leave the terminal stamp. 0 disables re-probing entirely;
|
|
81
|
+
// cap at 90 days so a typo cannot turn a bounded window into a permanent one.
|
|
82
|
+
abandonedPrRepollWindowMs: { min: 0, max: 90 * 24 * 60 * 60 * 1000 },
|
|
75
83
|
agentBusyReassignMs: { min: 0 },
|
|
76
84
|
maxRetriesPerAgent: { min: 1, max: 20 },
|
|
77
85
|
ccTurnTimeoutMs: { min: 10000, max: 3600000 },
|
|
86
|
+
// Issue #1105 — cold ACP worker handshake budget. Bounds are single-sourced
|
|
87
|
+
// from engine/shared.js so the validator and resolveAcpHandshakeTimeoutMs()
|
|
88
|
+
// can never disagree about the legal range.
|
|
89
|
+
acpHandshakeTimeoutMs: { min: shared.ACP_HANDSHAKE_TIMEOUT_MIN_MS, max: shared.ACP_HANDSHAKE_TIMEOUT_MAX_MS },
|
|
78
90
|
preDispatchEvalConcurrency: { min: 1, max: 20 },
|
|
79
91
|
memoryRetrievalTopK: { min: 1, max: 30 },
|
|
80
92
|
memoryRetrievalMaxBytes: { min: 1024, max: 65536 },
|
|
@@ -110,6 +122,7 @@ const ENGINE_SPECIAL_FIELDS = new Set([
|
|
|
110
122
|
'maxTurnsByType',
|
|
111
123
|
'evalMaxCost',
|
|
112
124
|
'ignoredCommentAuthors',
|
|
125
|
+
'cloudProvider',
|
|
113
126
|
]);
|
|
114
127
|
const ENGINE_ALLOWED_FIELDS = new Set([
|
|
115
128
|
...Object.keys(ENGINE_NUMERIC_RANGES),
|
|
@@ -337,6 +350,21 @@ function applyEngineSettings(candidate, bodyEngine, errors) {
|
|
|
337
350
|
if (!CLEAR_SENTINELS.has(bodyEngine[key]) && value === undefined) continue;
|
|
338
351
|
setOrDelete(engine, key, value);
|
|
339
352
|
}
|
|
353
|
+
// Cloud dispatch default provider. Validated against the REGISTERED provider
|
|
354
|
+
// list rather than accepted as free text: an id that resolves to nothing
|
|
355
|
+
// would produce a work item that dispatches nowhere, discovered only when
|
|
356
|
+
// the engine refuses it. Fail-closed here instead. (While no providers are
|
|
357
|
+
// registered, the only accepted value is the clear sentinel — which is the
|
|
358
|
+
// honest answer to "configure cloud dispatch" on a build that has none.)
|
|
359
|
+
if (hasOwn(bodyEngine, 'cloudProvider')) {
|
|
360
|
+
if (CLEAR_SENTINELS.has(bodyEngine.cloudProvider)) {
|
|
361
|
+
delete engine.cloudProvider;
|
|
362
|
+
} else {
|
|
363
|
+
const registered = cloudProviders.listCloudProviders();
|
|
364
|
+
const value = parseEnum(bodyEngine.cloudProvider, 'engine.cloudProvider', registered, errors);
|
|
365
|
+
if (value !== undefined) engine.cloudProvider = value;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
340
368
|
if (hasOwn(bodyEngine, 'copilotStreamMode')) {
|
|
341
369
|
if (CLEAR_SENTINELS.has(bodyEngine.copilotStreamMode)) {
|
|
342
370
|
delete engine.copilotStreamMode;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const agentApiValidation = require('../agent-api-validation');
|
|
4
|
-
const steeringConstraints = require('../steering-constraints');
|
|
5
|
-
const steeringStore = require('../steering-store');
|
|
6
|
-
const { LIMITS, PIN_LEVELS } = require('../content-api-validation');
|
|
3
|
+
const agentApiValidation = require('../api/agent-api-validation');
|
|
4
|
+
const steeringConstraints = require('../agents/steering-constraints');
|
|
5
|
+
const steeringStore = require('../persistence/steering-store');
|
|
6
|
+
const { LIMITS, PIN_LEVELS } = require('../api/content-api-validation');
|
|
7
7
|
|
|
8
8
|
function rejectsInvalid(...fields) {
|
|
9
9
|
return [{
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* engine/api-contracts/capability-manifest.js — the PRODUCER half of the
|
|
5
|
+
* cross-repo capability protocol (P-c4a1f7b2).
|
|
6
|
+
*
|
|
7
|
+
* Minions publishes a typed capability manifest; Constellation consumes it.
|
|
8
|
+
* This module owns the BUILDER and nothing else: every piece of vocabulary
|
|
9
|
+
* (scope list, confirmation enum, parameter types, capability-entry field list,
|
|
10
|
+
* envelope field list, protocol name/version) is imported from
|
|
11
|
+
* ./capability-protocol, which is the single owner of all of it.
|
|
12
|
+
*
|
|
13
|
+
* Two properties make the manifest safe to hand to a remote caller:
|
|
14
|
+
*
|
|
15
|
+
* 1. OPT-IN, FAIL-CLOSED. A route reaches the manifest only when its owning
|
|
16
|
+
* contract module declares an explicit `capability` block. A route that
|
|
17
|
+
* declares nothing is invisible here — there is no heuristic, no
|
|
18
|
+
* derived-by-default entry, and no partial entry.
|
|
19
|
+
* 2. NO INFERRED AUTHORIZATION. `destructive` and `confirm` are read straight
|
|
20
|
+
* from that declaration. They are deliberately NOT derived from the route
|
|
21
|
+
* registry's `_routeDestructive()` classification in dashboard.js, which
|
|
22
|
+
* is a fail-open regex over the route path AND its free-text description:
|
|
23
|
+
* rewording a description would otherwise silently reclassify a
|
|
24
|
+
* capability's safety posture for a remote caller.
|
|
25
|
+
*
|
|
26
|
+
* Parameters are projected from the audited route contract (the same
|
|
27
|
+
* `request.{path,query,body,headers}.fields` that GET /api/routes exposes
|
|
28
|
+
* through `_routeContractParameters`), so a published parameter type cannot
|
|
29
|
+
* drift from the contract the route actually validates against.
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
const {
|
|
33
|
+
CAPABILITY_PROTOCOL_NAME,
|
|
34
|
+
CAPABILITY_PROTOCOL_VERSION,
|
|
35
|
+
CAPABILITY_PARAM_TYPES,
|
|
36
|
+
CAPABILITY_ENTRY_FIELDS,
|
|
37
|
+
CAPABILITY_PARAM_FIELDS,
|
|
38
|
+
CAPABILITY_MANIFEST_FIELDS,
|
|
39
|
+
validateCapabilityManifest,
|
|
40
|
+
} = require('./capability-protocol');
|
|
41
|
+
|
|
42
|
+
const { listRouteContracts } = require('./index');
|
|
43
|
+
|
|
44
|
+
// Route contracts speak a slightly wider type vocabulary than the protocol
|
|
45
|
+
// (`identifier`, `basename` and `path` are all string refinements). This is a
|
|
46
|
+
// TRANSLATION table, not a second copy of the protocol's type set: every target
|
|
47
|
+
// below is verified against CAPABILITY_PARAM_TYPES at module load, and an
|
|
48
|
+
// unmapped contract type throws rather than degrading to a guessed type.
|
|
49
|
+
const PARAM_TYPE_FROM_CONTRACT = Object.freeze({
|
|
50
|
+
string: 'string',
|
|
51
|
+
identifier: 'string',
|
|
52
|
+
basename: 'string',
|
|
53
|
+
path: 'string',
|
|
54
|
+
number: 'number',
|
|
55
|
+
integer: 'integer',
|
|
56
|
+
boolean: 'boolean',
|
|
57
|
+
array: 'array',
|
|
58
|
+
object: 'object',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
for (const [contractType, protocolType] of Object.entries(PARAM_TYPE_FROM_CONTRACT)) {
|
|
62
|
+
if (!CAPABILITY_PARAM_TYPES.includes(protocolType)) {
|
|
63
|
+
throw new Error(`capability-manifest: contract type "${contractType}" maps to "${protocolType}", which is not in the protocol parameter-type set`);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Resolve a route-contract field type to a protocol parameter type.
|
|
69
|
+
*
|
|
70
|
+
* `array<string>` / `array<object>` normalize to `array`: the protocol has no
|
|
71
|
+
* way to express an item type, so dropping the parameterization loses nothing a
|
|
72
|
+
* consumer could have used. A UNION (`string|array<string>`) is a different
|
|
73
|
+
* matter — there is no honest single type for it, so it throws. That is a real
|
|
74
|
+
* signal, not an obstacle: a capability published to a remote caller has to
|
|
75
|
+
* have an unambiguous parameter type, and the route contract is where the
|
|
76
|
+
* ambiguity should be resolved.
|
|
77
|
+
*/
|
|
78
|
+
function protocolParamType(contractType) {
|
|
79
|
+
const raw = String(contractType || '').trim();
|
|
80
|
+
if (!raw) return null;
|
|
81
|
+
if (raw.includes('|')) return null;
|
|
82
|
+
const normalized = raw.replace(/<.*>$/, '');
|
|
83
|
+
return PARAM_TYPE_FROM_CONTRACT[normalized] || null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Same locations, in the same order, that dashboard.js#_routeContractParameters
|
|
87
|
+
// walks when it projects contract metadata onto GET /api/routes.
|
|
88
|
+
const PARAM_LOCATIONS = Object.freeze(['path', 'query', 'body', 'headers']);
|
|
89
|
+
|
|
90
|
+
const PARAM_FIELD_NAMES = Object.freeze(CAPABILITY_PARAM_FIELDS.map(entryField => entryField.name));
|
|
91
|
+
|
|
92
|
+
function fail(message) {
|
|
93
|
+
throw new Error(`capability-manifest: ${message}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Project one contract request field onto a protocol parameter descriptor.
|
|
98
|
+
* Only fields the protocol declares are copied — contract-only annotations such
|
|
99
|
+
* as `allowedValuesSource` are producer bookkeeping and would be rejected by
|
|
100
|
+
* the protocol's strict unknown-field check.
|
|
101
|
+
*/
|
|
102
|
+
function capabilityParam(contractField, contract, location) {
|
|
103
|
+
const name = String(contractField?.name || '').trim();
|
|
104
|
+
if (!name) fail(`${contract.key} declares an unnamed ${location} field`);
|
|
105
|
+
const type = protocolParamType(contractField.type);
|
|
106
|
+
if (!type) {
|
|
107
|
+
fail(`${contract.key} ${location} field "${name}" has contract type "${contractField.type}", which has no unambiguous protocol parameter type`);
|
|
108
|
+
}
|
|
109
|
+
const param = { name, type, required: contractField.required === true };
|
|
110
|
+
if (typeof contractField.description === 'string' && contractField.description.trim()) {
|
|
111
|
+
param.description = contractField.description;
|
|
112
|
+
}
|
|
113
|
+
if (Array.isArray(contractField.allowedValues)) {
|
|
114
|
+
param.allowedValues = [...contractField.allowedValues];
|
|
115
|
+
}
|
|
116
|
+
if (Number.isInteger(contractField.maxLength)) {
|
|
117
|
+
param.maxLength = contractField.maxLength;
|
|
118
|
+
}
|
|
119
|
+
for (const key of Object.keys(param)) {
|
|
120
|
+
if (!PARAM_FIELD_NAMES.includes(key)) fail(`${contract.key} produced an out-of-contract parameter field "${key}"`);
|
|
121
|
+
}
|
|
122
|
+
return param;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Typed parameters for one capability, sourced from the merged route contract.
|
|
127
|
+
* The protocol's parameter descriptor has no location discriminator, so a name
|
|
128
|
+
* declared in two locations collapses to its first occurrence — path first,
|
|
129
|
+
* which is the canonical selector for every route that has one.
|
|
130
|
+
*/
|
|
131
|
+
function capabilityParams(contract) {
|
|
132
|
+
const params = [];
|
|
133
|
+
const seen = new Set();
|
|
134
|
+
for (const location of PARAM_LOCATIONS) {
|
|
135
|
+
const fields = contract?.request?.[location]?.fields;
|
|
136
|
+
if (!Array.isArray(fields)) continue;
|
|
137
|
+
for (const contractField of fields) {
|
|
138
|
+
const param = capabilityParam(contractField, contract, location);
|
|
139
|
+
if (seen.has(param.name)) continue;
|
|
140
|
+
seen.add(param.name);
|
|
141
|
+
params.push(param);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return params;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Assemble one capability entry. The emitted key set is generated by walking
|
|
149
|
+
* CAPABILITY_ENTRY_FIELDS, so it is the protocol's field list by construction
|
|
150
|
+
* and can never become a hand-maintained copy that drifts from it.
|
|
151
|
+
*/
|
|
152
|
+
function buildCapabilityEntry(contract) {
|
|
153
|
+
const declared = contract.capability;
|
|
154
|
+
const derived = {
|
|
155
|
+
method: contract.method,
|
|
156
|
+
path: contract.path,
|
|
157
|
+
params: capabilityParams(contract),
|
|
158
|
+
};
|
|
159
|
+
const entry = {};
|
|
160
|
+
for (const entryField of CAPABILITY_ENTRY_FIELDS) {
|
|
161
|
+
const value = Object.hasOwn(derived, entryField.name) ? derived[entryField.name] : declared[entryField.name];
|
|
162
|
+
if (value === undefined) {
|
|
163
|
+
if (entryField.required) fail(`${contract.key} capability is missing required field "${entryField.name}"`);
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
entry[entryField.name] = value;
|
|
167
|
+
}
|
|
168
|
+
return entry;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function resolveMinionsVersion() {
|
|
172
|
+
try {
|
|
173
|
+
const version = require('../../package.json').version;
|
|
174
|
+
return typeof version === 'string' && version.trim() ? version : null;
|
|
175
|
+
} catch {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Build the capability manifest.
|
|
182
|
+
*
|
|
183
|
+
* THROWS on any contract violation — an out-of-vocabulary scope, an unknown
|
|
184
|
+
* confirmation mode, an unmapped parameter type or a duplicate capability id is
|
|
185
|
+
* a producer bug, never a warning that ships a manifest the consumer cannot
|
|
186
|
+
* generate against.
|
|
187
|
+
*
|
|
188
|
+
* @param {object} [options]
|
|
189
|
+
* @param {function} [options.listContracts] contract source (tests inject one)
|
|
190
|
+
* @param {string} [options.generatedAt] ISO timestamp override
|
|
191
|
+
* @param {string} [options.minionsVersion] producer version override
|
|
192
|
+
*/
|
|
193
|
+
function buildCapabilityManifest(options = {}) {
|
|
194
|
+
const contracts = typeof options.listContracts === 'function'
|
|
195
|
+
? options.listContracts()
|
|
196
|
+
: listRouteContracts();
|
|
197
|
+
if (!Array.isArray(contracts)) fail('contract source must return an array');
|
|
198
|
+
|
|
199
|
+
const capabilities = contracts
|
|
200
|
+
.filter(contract => contract && contract.capability)
|
|
201
|
+
.map(buildCapabilityEntry)
|
|
202
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
203
|
+
|
|
204
|
+
const minionsVersion = options.minionsVersion === undefined
|
|
205
|
+
? resolveMinionsVersion()
|
|
206
|
+
: options.minionsVersion;
|
|
207
|
+
|
|
208
|
+
const values = {
|
|
209
|
+
protocol: CAPABILITY_PROTOCOL_NAME,
|
|
210
|
+
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
211
|
+
generatedAt: options.generatedAt || new Date().toISOString(),
|
|
212
|
+
minionsVersion,
|
|
213
|
+
capabilities,
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const manifest = {};
|
|
217
|
+
for (const envelopeField of CAPABILITY_MANIFEST_FIELDS) {
|
|
218
|
+
const value = values[envelopeField.name];
|
|
219
|
+
if (value === undefined || value === null) {
|
|
220
|
+
if (envelopeField.required) fail(`manifest is missing required envelope field "${envelopeField.name}"`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
manifest[envelopeField.name] = value;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
validateCapabilityManifest(manifest);
|
|
227
|
+
return manifest;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
module.exports = {
|
|
231
|
+
buildCapabilityManifest,
|
|
232
|
+
// exported for testing
|
|
233
|
+
PARAM_TYPE_FROM_CONTRACT,
|
|
234
|
+
PARAM_LOCATIONS,
|
|
235
|
+
protocolParamType,
|
|
236
|
+
};
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* engine/api-contracts/capability-protocol.js — the canonical, versioned
|
|
5
|
+
* cross-repo capability protocol contract.
|
|
6
|
+
*
|
|
7
|
+
* Minions is the PRODUCER of the capability manifest and Constellation is the
|
|
8
|
+
* CONSUMER. Without a single owned vocabulary the two repos independently
|
|
9
|
+
* invent the scope strings, the confirmation enum, the parameter types and the
|
|
10
|
+
* version rules, and drift silently. This module is that single owned
|
|
11
|
+
* vocabulary: the scope list, confirmation enum, parameter-type set,
|
|
12
|
+
* capability-entry/envelope field lists and the version-compatibility rule are
|
|
13
|
+
* defined HERE and nowhere else in this repo.
|
|
14
|
+
*
|
|
15
|
+
* Constraints that shape the design:
|
|
16
|
+
*
|
|
17
|
+
* 1. Minions has zero runtime dependencies (package.json declares none; the
|
|
18
|
+
* engine is built entirely on Node built-ins). So the contract is a plain
|
|
19
|
+
* CommonJS module — no schema library, no build step, no transpile.
|
|
20
|
+
* 2. Constellation cannot import CommonJS from this repo, so the contract is
|
|
21
|
+
* also emitted as a machine-readable artifact:
|
|
22
|
+
*
|
|
23
|
+
* node engine/api-contracts/capability-protocol.js --emit
|
|
24
|
+
*
|
|
25
|
+
* writes docs/contracts/capability-protocol.v1.json, which is checked in
|
|
26
|
+
* and guarded by a drift test. Consumers GENERATE code from that artifact
|
|
27
|
+
* (pinning `protocolVersion` + `checksum`); they never transcribe it.
|
|
28
|
+
* 3. The artifact is content-addressed, not time-stamped: regenerating it
|
|
29
|
+
* from unchanged sources must reproduce the same bytes, so the drift test
|
|
30
|
+
* can compare byte-for-byte.
|
|
31
|
+
*
|
|
32
|
+
* The module intentionally requires only Node built-ins so a generator in
|
|
33
|
+
* another repo can load it standalone without dragging in the engine.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const crypto = require('node:crypto');
|
|
37
|
+
const fs = require('node:fs');
|
|
38
|
+
const path = require('node:path');
|
|
39
|
+
|
|
40
|
+
const CAPABILITY_PROTOCOL_NAME = 'minions.capability.manifest';
|
|
41
|
+
const CAPABILITY_PROTOCOL_VERSION = 1;
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Stated ONCE, and deliberately byte-identical to the rule already published
|
|
45
|
+
* for the bridge snapshot in docs/constellation-bridge.md. Restating it in
|
|
46
|
+
* different words is exactly the drift this contract exists to prevent, so a
|
|
47
|
+
* unit test asserts this string appears verbatim in that document.
|
|
48
|
+
*/
|
|
49
|
+
const CAPABILITY_VERSION_RULE = '`protocolVersion` changes only for a breaking schema change. Additive optional fields do not require a version bump.';
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The complete scope vocabulary. A capability declares the scopes it consumes;
|
|
53
|
+
* a consumer grants or withholds them. Adding a scope is additive (no version
|
|
54
|
+
* bump); removing or repurposing one is breaking (version bump).
|
|
55
|
+
*/
|
|
56
|
+
const CAPABILITY_SCOPES = Object.freeze([
|
|
57
|
+
'work:read',
|
|
58
|
+
'work:write',
|
|
59
|
+
'plan:read',
|
|
60
|
+
'plan:write',
|
|
61
|
+
'pr:read',
|
|
62
|
+
'notes:write',
|
|
63
|
+
'watch:write',
|
|
64
|
+
'engine:control',
|
|
65
|
+
'cc:converse',
|
|
66
|
+
'docchat:write',
|
|
67
|
+
'qa:read',
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Confirmation policy for invoking a capability.
|
|
72
|
+
* none — the consumer may invoke without asking the operator.
|
|
73
|
+
* per-call — the consumer must obtain operator confirmation for every call.
|
|
74
|
+
*/
|
|
75
|
+
const CAPABILITY_CONFIRM_MODES = Object.freeze(['none', 'per-call']);
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Parameter types. Deliberately a small JSON-native set drawn from the
|
|
79
|
+
* vocabulary already used by the route contracts in this directory, so a
|
|
80
|
+
* generated consumer type maps onto it without a translation table. Value
|
|
81
|
+
* constraints are expressed with `allowedValues` / `maxLength`, not with more
|
|
82
|
+
* types.
|
|
83
|
+
*/
|
|
84
|
+
const CAPABILITY_PARAM_TYPES = Object.freeze([
|
|
85
|
+
'string',
|
|
86
|
+
'number',
|
|
87
|
+
'integer',
|
|
88
|
+
'boolean',
|
|
89
|
+
'array',
|
|
90
|
+
'object',
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Symbolic accessors DERIVED from the vocabularies above — never a second copy.
|
|
95
|
+
* A producer that has to name one scope or confirmation mode writes
|
|
96
|
+
* `CAPABILITY_SCOPE.WORK_READ` rather than transcribing `'work:read'`, so the
|
|
97
|
+
* "defined exactly once" property survives contact with the route declarations
|
|
98
|
+
* that consume it. The repo-wide scan in test/unit/capability-protocol.test.js
|
|
99
|
+
* treats a transcribed scope literal anywhere else as drift, and these
|
|
100
|
+
* accessors are what make declaring a capability possible without tripping it.
|
|
101
|
+
*/
|
|
102
|
+
function symbolicIndex(values) {
|
|
103
|
+
return Object.freeze(Object.fromEntries(
|
|
104
|
+
values.map(value => [value.replace(/[^A-Za-z0-9]+/g, '_').toUpperCase(), value]),
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const CAPABILITY_SCOPE = symbolicIndex(CAPABILITY_SCOPES);
|
|
109
|
+
const CAPABILITY_CONFIRM = symbolicIndex(CAPABILITY_CONFIRM_MODES);
|
|
110
|
+
|
|
111
|
+
function field(name, type, required, options) {
|
|
112
|
+
return Object.freeze({ name, type, required, ...(options || {}) });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** Field list for one capability entry. */
|
|
116
|
+
const CAPABILITY_ENTRY_FIELDS = Object.freeze([
|
|
117
|
+
field('id', 'string', true, { description: 'Stable capability identifier, unique within a manifest.' }),
|
|
118
|
+
field('title', 'string', true, { description: 'Short human-readable label.' }),
|
|
119
|
+
field('description', 'string', true, { description: 'One-line description of the effect.' }),
|
|
120
|
+
field('method', 'string', true, { description: 'HTTP method of the backing Minions route.' }),
|
|
121
|
+
field('path', 'string', true, { description: 'Canonical route path template of the backing Minions route.' }),
|
|
122
|
+
field('scopes', 'array', true, { itemsOf: 'scopes', description: 'Scopes consumed, drawn from the scope vocabulary.' }),
|
|
123
|
+
field('destructive', 'boolean', true, { description: 'Whether invoking the capability destroys or irreversibly mutates state. A consumer authorization input, so it is always declared explicitly and never inferred from the route name, method, or description.' }),
|
|
124
|
+
field('confirm', 'string', true, { allowedValuesOf: 'confirmModes', description: 'Confirmation policy for invocation.' }),
|
|
125
|
+
field('params', 'array', true, { itemsOf: 'capabilityParamFields', description: 'Parameter descriptors; may be empty.' }),
|
|
126
|
+
]);
|
|
127
|
+
|
|
128
|
+
/** Field list for one parameter descriptor inside a capability entry. */
|
|
129
|
+
const CAPABILITY_PARAM_FIELDS = Object.freeze([
|
|
130
|
+
field('name', 'string', true, { description: 'Parameter name as sent to the route.' }),
|
|
131
|
+
field('type', 'string', true, { allowedValuesOf: 'paramTypes', description: 'Parameter type from the parameter-type set.' }),
|
|
132
|
+
field('required', 'boolean', true, { description: 'Whether the caller must supply the parameter.' }),
|
|
133
|
+
field('description', 'string', false, { description: 'Optional one-line parameter description.' }),
|
|
134
|
+
field('allowedValues', 'array', false, { description: 'Optional closed value set.' }),
|
|
135
|
+
field('maxLength', 'integer', false, { description: 'Optional maximum length for string parameters.' }),
|
|
136
|
+
]);
|
|
137
|
+
|
|
138
|
+
/** Field list for the manifest envelope that wraps the capability entries. */
|
|
139
|
+
const CAPABILITY_MANIFEST_FIELDS = Object.freeze([
|
|
140
|
+
field('protocol', 'string', true, { constant: CAPABILITY_PROTOCOL_NAME, description: 'Protocol identifier.' }),
|
|
141
|
+
field('protocolVersion', 'integer', true, { constant: CAPABILITY_PROTOCOL_VERSION, description: 'Protocol version; see versionRule.' }),
|
|
142
|
+
field('generatedAt', 'string', true, { description: 'ISO-8601 UTC timestamp of manifest generation.' }),
|
|
143
|
+
field('minionsVersion', 'string', false, { description: 'Optional Minions package version of the producer.' }),
|
|
144
|
+
field('capabilities', 'array', true, { itemsOf: 'capabilityEntryFields', description: 'Capability entries.' }),
|
|
145
|
+
]);
|
|
146
|
+
|
|
147
|
+
const SCOPE_SET = new Set(CAPABILITY_SCOPES);
|
|
148
|
+
const CONFIRM_SET = new Set(CAPABILITY_CONFIRM_MODES);
|
|
149
|
+
const PARAM_TYPE_SET = new Set(CAPABILITY_PARAM_TYPES);
|
|
150
|
+
|
|
151
|
+
const ARTIFACT_RELATIVE_PATH = `docs/contracts/capability-protocol.v${CAPABILITY_PROTOCOL_VERSION}.json`;
|
|
152
|
+
const GENERATOR_RELATIVE_PATH = 'engine/api-contracts/capability-protocol.js';
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* The version-compatibility rule, executable. A manifest is compatible only
|
|
156
|
+
* when its `protocolVersion` equals ours: per CAPABILITY_VERSION_RULE the
|
|
157
|
+
* number moves solely on a breaking schema change, so an exact match is the
|
|
158
|
+
* whole rule — and additive optional fields keep the number (and therefore
|
|
159
|
+
* compatibility) unchanged.
|
|
160
|
+
*/
|
|
161
|
+
function isCompatibleProtocolVersion(version) {
|
|
162
|
+
return version === CAPABILITY_PROTOCOL_VERSION;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function fail(message) {
|
|
166
|
+
throw new Error(`capability-protocol: ${message}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function typeMatches(type, value) {
|
|
170
|
+
switch (type) {
|
|
171
|
+
case 'string': return typeof value === 'string';
|
|
172
|
+
case 'number': return typeof value === 'number' && Number.isFinite(value);
|
|
173
|
+
case 'integer': return Number.isInteger(value);
|
|
174
|
+
case 'boolean': return typeof value === 'boolean';
|
|
175
|
+
case 'array': return Array.isArray(value);
|
|
176
|
+
case 'object': return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
177
|
+
default: return false;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function assertFields(target, fields, label, itemLabel) {
|
|
182
|
+
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
|
183
|
+
fail(`${label} must be an object`);
|
|
184
|
+
}
|
|
185
|
+
const known = new Set(fields.map(entry => entry.name));
|
|
186
|
+
for (const key of Object.keys(target)) {
|
|
187
|
+
if (!known.has(key)) fail(`unknown ${itemLabel} field: ${key}`);
|
|
188
|
+
}
|
|
189
|
+
for (const entry of fields) {
|
|
190
|
+
const value = target[entry.name];
|
|
191
|
+
if (value === undefined) {
|
|
192
|
+
if (entry.required) fail(`missing required ${itemLabel} field: ${entry.name}`);
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
if (!typeMatches(entry.type, value)) {
|
|
196
|
+
fail(`${itemLabel} field ${entry.name} must be of type ${entry.type}`);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function validateCapabilityParam(param, capabilityId) {
|
|
202
|
+
assertFields(param, CAPABILITY_PARAM_FIELDS, `capability ${capabilityId} parameter`, 'parameter');
|
|
203
|
+
if (!PARAM_TYPE_SET.has(param.type)) {
|
|
204
|
+
fail(`unknown parameter type "${param.type}" on capability ${capabilityId}; allowed: ${CAPABILITY_PARAM_TYPES.join(', ')}`);
|
|
205
|
+
}
|
|
206
|
+
if (!param.name.trim()) fail(`capability ${capabilityId} has a parameter with an empty name`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function validateCapabilityEntry(entry, index) {
|
|
210
|
+
assertFields(entry, CAPABILITY_ENTRY_FIELDS, `capability at index ${index}`, 'capability');
|
|
211
|
+
const id = entry.id;
|
|
212
|
+
if (!id.trim()) fail(`capability at index ${index} has an empty id`);
|
|
213
|
+
if (!entry.scopes.length) fail(`capability ${id} must declare at least one scope`);
|
|
214
|
+
for (const scope of entry.scopes) {
|
|
215
|
+
if (!SCOPE_SET.has(scope)) {
|
|
216
|
+
fail(`unknown capability scope "${scope}" on capability ${id}; allowed: ${CAPABILITY_SCOPES.join(', ')}`);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
if (!CONFIRM_SET.has(entry.confirm)) {
|
|
220
|
+
fail(`unknown confirm mode "${entry.confirm}" on capability ${id}; allowed: ${CAPABILITY_CONFIRM_MODES.join(', ')}`);
|
|
221
|
+
}
|
|
222
|
+
for (const param of entry.params) validateCapabilityParam(param, id);
|
|
223
|
+
const paramNames = entry.params.map(param => param.name);
|
|
224
|
+
if (new Set(paramNames).size !== paramNames.length) {
|
|
225
|
+
fail(`capability ${id} declares duplicate parameter names`);
|
|
226
|
+
}
|
|
227
|
+
return id;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Validate a capability manifest against this contract. THROWS on any
|
|
232
|
+
* violation — an out-of-contract scope, confirmation mode or parameter type is
|
|
233
|
+
* a build-time failure for the producer, never a warning that ships a manifest
|
|
234
|
+
* the consumer cannot generate against. Returns `true` on success so callers
|
|
235
|
+
* can assert on it.
|
|
236
|
+
*/
|
|
237
|
+
function validateCapabilityManifest(manifest) {
|
|
238
|
+
assertFields(manifest, CAPABILITY_MANIFEST_FIELDS, 'manifest', 'manifest');
|
|
239
|
+
if (manifest.protocol !== CAPABILITY_PROTOCOL_NAME) {
|
|
240
|
+
fail(`manifest protocol must be "${CAPABILITY_PROTOCOL_NAME}", got "${manifest.protocol}"`);
|
|
241
|
+
}
|
|
242
|
+
if (!isCompatibleProtocolVersion(manifest.protocolVersion)) {
|
|
243
|
+
fail(`incompatible protocolVersion ${manifest.protocolVersion}; this contract emits ${CAPABILITY_PROTOCOL_VERSION}`);
|
|
244
|
+
}
|
|
245
|
+
const ids = [];
|
|
246
|
+
manifest.capabilities.forEach((entry, index) => { ids.push(validateCapabilityEntry(entry, index)); });
|
|
247
|
+
if (new Set(ids).size !== ids.length) fail('manifest declares duplicate capability ids');
|
|
248
|
+
return true;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Recursively sort object keys so the checksum is independent of key order and
|
|
252
|
+
// reproducible from any language that can sort strings.
|
|
253
|
+
function canonicalize(value) {
|
|
254
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
255
|
+
if (value && typeof value === 'object') {
|
|
256
|
+
const out = {};
|
|
257
|
+
for (const key of Object.keys(value).sort()) out[key] = canonicalize(value[key]);
|
|
258
|
+
return out;
|
|
259
|
+
}
|
|
260
|
+
return value;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Content checksum over every field except `checksum` itself, so a consumer can
|
|
265
|
+
* pin the artifact it generated from and detect a swapped or edited copy.
|
|
266
|
+
*/
|
|
267
|
+
function computeArtifactChecksum(artifact) {
|
|
268
|
+
const { checksum, ...rest } = artifact || {};
|
|
269
|
+
void checksum;
|
|
270
|
+
const digest = crypto.createHash('sha256').update(JSON.stringify(canonicalize(rest))).digest('hex');
|
|
271
|
+
return `sha256:${digest}`;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Build the machine-readable artifact from this module. Deterministic. */
|
|
275
|
+
function buildProtocolArtifact() {
|
|
276
|
+
const body = {
|
|
277
|
+
protocol: CAPABILITY_PROTOCOL_NAME,
|
|
278
|
+
protocolVersion: CAPABILITY_PROTOCOL_VERSION,
|
|
279
|
+
generator: GENERATOR_RELATIVE_PATH,
|
|
280
|
+
versionRule: CAPABILITY_VERSION_RULE,
|
|
281
|
+
scopes: [...CAPABILITY_SCOPES],
|
|
282
|
+
confirmModes: [...CAPABILITY_CONFIRM_MODES],
|
|
283
|
+
paramTypes: [...CAPABILITY_PARAM_TYPES],
|
|
284
|
+
manifestEnvelopeFields: CAPABILITY_MANIFEST_FIELDS.map(entry => ({ ...entry })),
|
|
285
|
+
capabilityEntryFields: CAPABILITY_ENTRY_FIELDS.map(entry => ({ ...entry })),
|
|
286
|
+
capabilityParamFields: CAPABILITY_PARAM_FIELDS.map(entry => ({ ...entry })),
|
|
287
|
+
};
|
|
288
|
+
return { ...body, checksum: computeArtifactChecksum(body) };
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Exact bytes of the checked-in artifact (LF, trailing newline). */
|
|
292
|
+
function serializeProtocolArtifact() {
|
|
293
|
+
return `${JSON.stringify(buildProtocolArtifact(), null, 2)}\n`;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Write the artifact next to this module's repo root. Returns the path written. */
|
|
297
|
+
function emitProtocolArtifact(repoRoot) {
|
|
298
|
+
const root = repoRoot || path.resolve(__dirname, '..', '..');
|
|
299
|
+
const target = path.join(root, ...ARTIFACT_RELATIVE_PATH.split('/'));
|
|
300
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
301
|
+
fs.writeFileSync(target, serializeProtocolArtifact());
|
|
302
|
+
return target;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (require.main === module) {
|
|
306
|
+
if (process.argv.includes('--emit')) {
|
|
307
|
+
console.log(emitProtocolArtifact());
|
|
308
|
+
} else {
|
|
309
|
+
process.stdout.write(serializeProtocolArtifact());
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = Object.freeze({
|
|
314
|
+
CAPABILITY_PROTOCOL_NAME,
|
|
315
|
+
CAPABILITY_PROTOCOL_VERSION,
|
|
316
|
+
CAPABILITY_VERSION_RULE,
|
|
317
|
+
CAPABILITY_SCOPES,
|
|
318
|
+
CAPABILITY_CONFIRM_MODES,
|
|
319
|
+
CAPABILITY_PARAM_TYPES,
|
|
320
|
+
CAPABILITY_SCOPE,
|
|
321
|
+
CAPABILITY_CONFIRM,
|
|
322
|
+
CAPABILITY_ENTRY_FIELDS,
|
|
323
|
+
CAPABILITY_PARAM_FIELDS,
|
|
324
|
+
CAPABILITY_MANIFEST_FIELDS,
|
|
325
|
+
ARTIFACT_RELATIVE_PATH,
|
|
326
|
+
GENERATOR_RELATIVE_PATH,
|
|
327
|
+
isCompatibleProtocolVersion,
|
|
328
|
+
validateCapabilityManifest,
|
|
329
|
+
computeArtifactChecksum,
|
|
330
|
+
buildProtocolArtifact,
|
|
331
|
+
serializeProtocolArtifact,
|
|
332
|
+
emitProtocolArtifact,
|
|
333
|
+
});
|
|
@@ -10,6 +10,7 @@ module.exports = {
|
|
|
10
10
|
'POST /api/projects/add',
|
|
11
11
|
'POST /api/projects/remove',
|
|
12
12
|
'GET /api/runtimes',
|
|
13
|
+
'GET /api/cloud-providers',
|
|
13
14
|
'POST /api/runtimes/<runtime>/models/refresh',
|
|
14
15
|
'GET /api/runtimes/<runtime>/models',
|
|
15
16
|
'GET /api/settings',
|
|
@@ -136,6 +137,10 @@ module.exports = {
|
|
|
136
137
|
audit: 'audited',
|
|
137
138
|
noInputReason: 'Lists the canonical runtime registry and accepts no caller input.',
|
|
138
139
|
},
|
|
140
|
+
'GET /api/cloud-providers': {
|
|
141
|
+
audit: 'audited',
|
|
142
|
+
noInputReason: 'Lists the canonical cloud dispatch provider registry and accepts no caller input.',
|
|
143
|
+
},
|
|
139
144
|
'POST /api/runtimes/<runtime>/models/refresh': {
|
|
140
145
|
audit: 'audited',
|
|
141
146
|
path: {
|