@cohortapp/agent-sdk 2.16.0 → 2.18.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.json +18 -0
- package/.env.example +23 -7
- package/README.md +1 -0
- package/bin/maestro.mjs +62 -0
- package/docs/guides/billing-console-keys.md +60 -0
- package/docs/guides/front-door-session.md +54 -9
- package/docs/guides/mac-mini.md +20 -25
- package/docs/guides/poller-daemon-setup.md +4 -1
- package/docs/guides/setup-wizard.md +1 -1
- package/docs/runbooks/fleet-rollout.md +156 -0
- package/docs/runbooks/mac-mini-bootstrap.md +12 -14
- package/lib/action-executor.js +19 -3
- package/lib/budget-guard.mjs +279 -3
- package/lib/channels/base-adapter.mjs +3 -1
- package/lib/channels/contract.mjs +2 -1
- package/lib/channels/inbox-item.mjs +8 -0
- package/lib/claude-bin.mjs +5 -6
- package/lib/cli/doctor-checks.mjs +141 -10
- package/lib/cli/global-setup-extras.mjs +5 -1
- package/lib/cli/inbox.mjs +100 -15
- package/lib/cli/seat-auth.mjs +463 -0
- package/lib/cli/session.mjs +80 -12
- package/lib/collective/capture.mjs +8 -6
- package/lib/collective/global-config.mjs +63 -1
- package/lib/collective/presence.mjs +142 -5
- package/lib/comms/send-gate.mjs +559 -1
- package/lib/context/budget.mjs +327 -0
- package/lib/context/history-scope.mjs +138 -0
- package/lib/diagnostics/alerts.mjs +49 -0
- package/lib/diagnostics/cadence-output-freshness.mjs +288 -0
- package/lib/engine/agents/definitions.mjs +343 -0
- package/lib/engine/agents/persist.mjs +275 -0
- package/lib/engine/agents/runtime.mjs +748 -0
- package/lib/engine/agents/usage.mjs +95 -0
- package/lib/engine/auth-status.mjs +139 -0
- package/lib/engine/budget.mjs +194 -0
- package/lib/engine/cli.mjs +1204 -0
- package/lib/engine/commands/index.mjs +269 -0
- package/lib/engine/context/budget.mjs +219 -0
- package/lib/engine/context/cache.mjs +125 -0
- package/lib/engine/context/child-env.mjs +215 -0
- package/lib/engine/context/compaction.mjs +342 -0
- package/lib/engine/context/images.mjs +90 -0
- package/lib/engine/context/instructions.mjs +327 -0
- package/lib/engine/context/lazy-instructions.mjs +169 -0
- package/lib/engine/context/manager.mjs +182 -0
- package/lib/engine/context/real-path.mjs +91 -0
- package/lib/engine/context/secret-values.mjs +163 -0
- package/lib/engine/context/settings.mjs +274 -0
- package/lib/engine/context/stream-input.mjs +159 -0
- package/lib/engine/guard.mjs +152 -0
- package/lib/engine/hooks.mjs +713 -0
- package/lib/engine/loop.mjs +560 -0
- package/lib/engine/mcp/client.mjs +254 -0
- package/lib/engine/mcp/config.mjs +301 -0
- package/lib/engine/mcp/http.mjs +201 -0
- package/lib/engine/mcp/index.mjs +146 -0
- package/lib/engine/mcp/jsonrpc.mjs +147 -0
- package/lib/engine/mcp/naming.mjs +66 -0
- package/lib/engine/mcp/resources.mjs +89 -0
- package/lib/engine/mcp/results.mjs +133 -0
- package/lib/engine/mcp/stdio.mjs +137 -0
- package/lib/engine/mcp/supervisor.mjs +116 -0
- package/lib/engine/messages.mjs +104 -0
- package/lib/engine/output/json.mjs +164 -0
- package/lib/engine/output/stream-json.mjs +266 -0
- package/lib/engine/permissions.mjs +845 -0
- package/lib/engine/process-identity.mjs +164 -0
- package/lib/engine/process-tree.mjs +551 -0
- package/lib/engine/prompt.mjs +60 -0
- package/lib/engine/session/store.mjs +299 -0
- package/lib/engine/session-runtime/args.mjs +97 -0
- package/lib/engine/session-runtime/host.mjs +143 -0
- package/lib/engine/session-runtime/inbox.mjs +122 -0
- package/lib/engine/session-runtime/notifications.mjs +129 -0
- package/lib/engine/session-runtime/registry.mjs +328 -0
- package/lib/engine/session-runtime/runner.mjs +344 -0
- package/lib/engine/session-runtime/socket.mjs +212 -0
- package/lib/engine/session-runtime/wakeup.mjs +115 -0
- package/lib/engine/skills/index.mjs +321 -0
- package/lib/engine/tools/bash-background.mjs +533 -0
- package/lib/engine/tools/bash.mjs +216 -0
- package/lib/engine/tools/edit.mjs +97 -0
- package/lib/engine/tools/glob.mjs +81 -0
- package/lib/engine/tools/grep.mjs +224 -0
- package/lib/engine/tools/index.mjs +84 -0
- package/lib/engine/tools/list-agents.mjs +32 -0
- package/lib/engine/tools/ls.mjs +127 -0
- package/lib/engine/tools/monitor.mjs +82 -0
- package/lib/engine/tools/notebook-edit.mjs +218 -0
- package/lib/engine/tools/read.mjs +103 -0
- package/lib/engine/tools/schedule-wakeup.mjs +45 -0
- package/lib/engine/tools/schema.mjs +144 -0
- package/lib/engine/tools/send-message.mjs +77 -0
- package/lib/engine/tools/session.mjs +70 -0
- package/lib/engine/tools/todo.mjs +144 -0
- package/lib/engine/tools/toolsearch.mjs +217 -0
- package/lib/engine/tools/walk.mjs +193 -0
- package/lib/engine/tools/web-switch.mjs +31 -0
- package/lib/engine/tools/webfetch-html.mjs +387 -0
- package/lib/engine/tools/webfetch-net.mjs +340 -0
- package/lib/engine/tools/webfetch.mjs +198 -0
- package/lib/engine/tools/websearch.mjs +91 -0
- package/lib/engine/tools/workflow.mjs +95 -0
- package/lib/engine/tools/write.mjs +76 -0
- package/lib/engine/tui/line-editor.mjs +137 -0
- package/lib/engine/tui/render.mjs +86 -0
- package/lib/engine/tui/tui.mjs +274 -0
- package/lib/engine/wire/anthropic-messages.mjs +263 -0
- package/lib/engine/wire/effort.mjs +36 -0
- package/lib/engine/wire/errors.mjs +496 -0
- package/lib/engine/wire/http.mjs +441 -0
- package/lib/engine/wire/index.mjs +76 -0
- package/lib/engine/wire/openai-chat.mjs +332 -0
- package/lib/engine/wire/prompt-cache.mjs +79 -0
- package/lib/engine/wire/search.mjs +140 -0
- package/lib/engine/wire/sse.mjs +114 -0
- package/lib/engine/wire/stall.mjs +349 -0
- package/lib/engine/wire/token-provider.mjs +175 -0
- package/lib/engine/wire/usage.mjs +192 -0
- package/lib/engine/workflow/host.mjs +524 -0
- package/lib/engine/workflow/journal.mjs +188 -0
- package/lib/engine/workflow/json-schema.mjs +171 -0
- package/lib/engine/workflow/meta.mjs +329 -0
- package/lib/engine/workflow/notifications.mjs +52 -0
- package/lib/engine/workflow/runtime.mjs +447 -0
- package/lib/engine/workflow/sandbox.mjs +534 -0
- package/lib/engine/workflow/worker.mjs +141 -0
- package/lib/engine/workflow/worktree.mjs +74 -0
- package/lib/execution/disposition.mjs +1 -1
- package/lib/execution/intake.mjs +10 -0
- package/lib/execution/surface-policy.mjs +15 -0
- package/lib/learning/curator.mjs +8 -6
- package/lib/learning/reflect.mjs +8 -6
- package/lib/model-router/catalog/cohort.yaml +137 -0
- package/lib/model-router/catalog.mjs +118 -1
- package/lib/model-router/economics.mjs +9 -0
- package/lib/model-router/failover.mjs +67 -16
- package/lib/model-router/llm-task.mjs +39 -3
- package/lib/model-router/resolve.mjs +95 -3
- package/lib/model-router/spawn.mjs +46 -47
- package/lib/model-router/taxonomy.mjs +126 -4
- package/lib/org/cost-sync.mjs +141 -11
- package/lib/org/inbound/broadcast.mjs +289 -0
- package/lib/org/inbound/collective.mjs +375 -0
- package/lib/org/inbound/directedness.mjs +96 -8
- package/lib/org/inbound/facts.mjs +82 -4
- package/lib/org/inbound/hydrate.mjs +555 -51
- package/lib/org/inbound/project.mjs +22 -0
- package/lib/org/inbound/surfaces.mjs +14 -0
- package/lib/org/llm-token.mjs +879 -0
- package/lib/org/mesh.mjs +61 -0
- package/lib/org/messaging.mjs +3 -1
- package/lib/org/protocol.checksum +1 -1
- package/lib/org/protocol.mjs +15 -0
- package/lib/org/quota.mjs +520 -0
- package/lib/org/tool-surface.mjs +104 -16
- package/lib/org/ui-parity.mjs +16 -1
- package/lib/org/work-ledger.mjs +37 -6
- package/lib/rate-guard.mjs +114 -1
- package/lib/resource-governor.mjs +41 -6
- package/lib/runtime/adapter.mjs +823 -0
- package/lib/runtime/child-env.mjs +191 -0
- package/lib/runtime/legacy-shell-guard.mjs +97 -0
- package/lib/runtime/seat-engine.mjs +162 -0
- package/lib/session/ask-ledger.mjs +271 -0
- package/lib/session/current-work.mjs +676 -0
- package/lib/session/feed-core.mjs +40 -3
- package/lib/session/launch-args.mjs +56 -4
- package/lib/session/status-summary.mjs +26 -9
- package/lib/session/upgrade-notice.mjs +42 -0
- package/lib/setup/claude-probe.mjs +117 -13
- package/lib/setup/enrich.mjs +13 -10
- package/lib/setup/sections/model.mjs +39 -13
- package/lib/telemetry/collect.mjs +208 -9
- package/lib/upgrade/ignored-drift.mjs +105 -0
- package/lib/voice/post-call-brief.mjs +30 -17
- package/package.json +15 -3
- package/plugins/maestro-skills/skills/board-work.md +5 -0
- package/plugins/maestro-skills/skills/inbound-triage.md +56 -15
- package/plugins/maestro-skills/skills/main-session.md +18 -7
- package/scripts/ci/check-tarball-fidelity.mjs +126 -2
- package/scripts/ci/run-tests.mjs +47 -19
- package/scripts/cohort-llm/api-key-helper.mjs +92 -0
- package/scripts/collective/hook-runner.mjs +29 -2
- package/scripts/continuous-monitor.sh +13 -0
- package/scripts/cost/track-claude-usage.mjs +15 -0
- package/scripts/daemon/agent-daemon.mjs +408 -20
- package/scripts/daemon/assurance.mjs +48 -12
- package/scripts/daemon/cadence-consumer.mjs +218 -68
- package/scripts/daemon/cadence-handlers.mjs +73 -4
- package/scripts/daemon/classifier.mjs +75 -26
- package/scripts/daemon/context-compiler.mjs +104 -59
- package/scripts/daemon/deliver.mjs +30 -1
- package/scripts/daemon/dispatcher.mjs +804 -157
- package/scripts/daemon/health.mjs +14 -1
- package/scripts/daemon/lib/session-router.mjs +310 -42
- package/scripts/daemon/maestro-daemon.mjs +11 -0
- package/scripts/daemon/prompt-builder.mjs +121 -12
- package/scripts/daemon/responder.mjs +315 -146
- package/scripts/daemon/sdk-version.mjs +98 -16
- package/scripts/eval/probe-gateway.mjs +635 -0
- package/scripts/eval/replay/extract.mjs +270 -0
- package/scripts/eval/replay/grade.mjs +260 -0
- package/scripts/eval/replay/lib/config.mjs +50 -0
- package/scripts/eval/replay/lib/effects.mjs +65 -0
- package/scripts/eval/replay/lib/fixture.mjs +188 -0
- package/scripts/eval/replay/lib/judge.mjs +72 -0
- package/scripts/eval/replay/lib/redact.mjs +136 -0
- package/scripts/eval/replay/lib/sandbox.mjs +170 -0
- package/scripts/eval/replay/lib/schema-check.mjs +63 -0
- package/scripts/eval/replay/lib/transcript.mjs +76 -0
- package/scripts/eval/replay/mcp-replay-stub.mjs +101 -0
- package/scripts/eval/replay/report.mjs +185 -0
- package/scripts/eval/replay/run.mjs +404 -0
- package/scripts/fleet/rollout.mjs +1094 -0
- package/scripts/hooks/pre-send-audit.sh +36 -245
- package/scripts/hooks/pre-write-yaml-validate.mjs +275 -0
- package/scripts/hooks/validate-state-yaml.sh +190 -0
- package/scripts/huddle/huddle-llm.mjs +361 -0
- package/scripts/huddle/huddle-server.mjs +46 -121
- package/scripts/local-triggers/autoupdate.sh +448 -78
- package/scripts/local-triggers/run-trigger.sh +13 -0
- package/scripts/maintenance/pin-integrity.mjs +364 -0
- package/scripts/poll-slack-events.sh +41 -9
- package/scripts/poller/slack-socket-mode.mjs +28 -3
- package/scripts/session/supervisor.mjs +80 -13
- package/scripts/spawn-session.sh +13 -0
- package/bin/maestro.test.mjs +0 -1574
- package/lib/action-executor.test.mjs +0 -871
- package/lib/archetype.test.mjs +0 -132
- package/lib/assurance/plan-note.test.mjs +0 -234
- package/lib/assurance/room-budget.test.mjs +0 -486
- package/lib/assurance/tier.test.mjs +0 -174
- package/lib/autonomy.test.mjs +0 -66
- package/lib/backlog.test.mjs +0 -302
- package/lib/backup/policy.test.mjs +0 -305
- package/lib/budget-escalate.test.mjs +0 -232
- package/lib/budget-guard.envelope.test.mjs +0 -476
- package/lib/budget-guard.test.mjs +0 -427
- package/lib/cadence-bus-requeue.test.mjs +0 -83
- package/lib/cadence-bus-schedule.test.mjs +0 -194
- package/lib/cadence-bus.test.mjs +0 -720
- package/lib/cadences.test.mjs +0 -230
- package/lib/capability/inventory.test.mjs +0 -232
- package/lib/capability.test.mjs +0 -78
- package/lib/channels/base-adapter.test.mjs +0 -590
- package/lib/channels/channels.test.mjs +0 -371
- package/lib/channels/contract.test.mjs +0 -162
- package/lib/channels/inbox-item.test.mjs +0 -368
- package/lib/channels/orgmail/adapter.test.mjs +0 -448
- package/lib/channels/pairing.test.mjs +0 -270
- package/lib/channels/repeat-suppressor.test.mjs +0 -134
- package/lib/channels/slack-adapter.test.mjs +0 -212
- package/lib/channels/telegram-adapter.test.mjs +0 -306
- package/lib/channels/voice/adapter.test.mjs +0 -278
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +0 -359
- package/lib/channels/whatsapp/baileys-typing.test.mjs +0 -154
- package/lib/charter.test.mjs +0 -89
- package/lib/claude-bin.test.mjs +0 -131
- package/lib/cli/board.test.mjs +0 -227
- package/lib/cli/design.test.mjs +0 -270
- package/lib/cli/doctor-checks.test.mjs +0 -336
- package/lib/cli/global-setup-extras.test.mjs +0 -462
- package/lib/cli/inbox.test.mjs +0 -230
- package/lib/cli/session-ack.test.mjs +0 -63
- package/lib/cli/session.test.mjs +0 -613
- package/lib/collective/capture.test.mjs +0 -121
- package/lib/collective/cards.test.mjs +0 -114
- package/lib/collective/config.test.mjs +0 -123
- package/lib/collective/global-config.test.mjs +0 -220
- package/lib/collective/global-skills.test.mjs +0 -126
- package/lib/collective/presence.test.mjs +0 -95
- package/lib/collective/recall.test.mjs +0 -116
- package/lib/collective/vendor-skills.test.mjs +0 -306
- package/lib/comms/send-gate.test.mjs +0 -770
- package/lib/comms.test.mjs +0 -41
- package/lib/cost/ledger-row.test.mjs +0 -183
- package/lib/design/design-md.test.mjs +0 -318
- package/lib/design/fixtures/DESIGN.golden.md +0 -238
- package/lib/design/fixtures/PRODUCT.golden.md +0 -67
- package/lib/design/fixtures/foundation.json +0 -133
- package/lib/design/refresh-gate.test.mjs +0 -144
- package/lib/design/write.test.mjs +0 -241
- package/lib/diagnostics/alerts.test.mjs +0 -318
- package/lib/diagnostics/backup-freshness.test.mjs +0 -185
- package/lib/diagnostics/counters.test.mjs +0 -206
- package/lib/diagnostics/events.test.mjs +0 -290
- package/lib/diagnostics/otel.test.mjs +0 -196
- package/lib/diagnostics/trace.test.mjs +0 -251
- package/lib/env-compat.test.mjs +0 -104
- package/lib/execution/disposition.test.mjs +0 -553
- package/lib/execution/drive.test.mjs +0 -270
- package/lib/execution/effects.test.mjs +0 -344
- package/lib/execution/intake.test.mjs +0 -389
- package/lib/execution/journal.test.mjs +0 -261
- package/lib/execution/match.test.mjs +0 -235
- package/lib/execution/pipeline.test.mjs +0 -392
- package/lib/execution/route.test.mjs +0 -186
- package/lib/execution/surface-policy.test.mjs +0 -162
- package/lib/fs-atomic.test.mjs +0 -72
- package/lib/fs-ownership.test.mjs +0 -158
- package/lib/goals/admission.test.mjs +0 -164
- package/lib/goals/classify.test.mjs +0 -167
- package/lib/goals/collaborate.test.mjs +0 -336
- package/lib/goals/gaps.test.mjs +0 -284
- package/lib/goals/loop.test.mjs +0 -845
- package/lib/hooks/bus.test.mjs +0 -387
- package/lib/identity/persona.test.mjs +0 -142
- package/lib/kpi-sensors.test.mjs +0 -278
- package/lib/kpi.test.mjs +0 -244
- package/lib/learning/config.test.mjs +0 -75
- package/lib/learning/counters.test.mjs +0 -69
- package/lib/learning/curator-consolidate.test.mjs +0 -238
- package/lib/learning/curator.test.mjs +0 -106
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.test.mjs +0 -125
- package/lib/learning/skill-writer.test.mjs +0 -210
- package/lib/mandate/audit.test.mjs +0 -195
- package/lib/mandate/contract.test.mjs +0 -185
- package/lib/mandate/derive.test.mjs +0 -274
- package/lib/mandate/model.test.mjs +0 -164
- package/lib/mandate/refresh.test.mjs +0 -389
- package/lib/mcp/server.test.mjs +0 -426
- package/lib/model-router/auth-profiles.test.mjs +0 -580
- package/lib/model-router/catalog.test.mjs +0 -385
- package/lib/model-router/economics.test.mjs +0 -438
- package/lib/model-router/failover.test.mjs +0 -439
- package/lib/model-router/health.test.mjs +0 -338
- package/lib/model-router/integration-coverage.test.mjs +0 -831
- package/lib/model-router/integration.test.mjs +0 -564
- package/lib/model-router/ledger.test.mjs +0 -415
- package/lib/model-router/llm-task.test.mjs +0 -392
- package/lib/model-router/org-credentials.test.mjs +0 -265
- package/lib/model-router/pricing-refresh.test.mjs +0 -286
- package/lib/model-router/reconcile.test.mjs +0 -316
- package/lib/model-router/repair.test.mjs +0 -180
- package/lib/model-router/spawn.test.mjs +0 -446
- package/lib/model-router/taxonomy.test.mjs +0 -410
- package/lib/model-router.test.mjs +0 -1207
- package/lib/org/activity.test.mjs +0 -134
- package/lib/org/approvals.test.mjs +0 -216
- package/lib/org/awareness.test.mjs +0 -159
- package/lib/org/board-mine-cache.test.mjs +0 -53
- package/lib/org/board.test.mjs +0 -187
- package/lib/org/bootstrap-context.test.mjs +0 -153
- package/lib/org/client.test.mjs +0 -1206
- package/lib/org/cohort-client.test.mjs +0 -126
- package/lib/org/cost-sync.test.mjs +0 -153
- package/lib/org/doctor.test.mjs +0 -346
- package/lib/org/engagement-ledger.test.mjs +0 -112
- package/lib/org/engagement.test.mjs +0 -739
- package/lib/org/handoff.test.mjs +0 -269
- package/lib/org/inbound/directedness.test.mjs +0 -668
- package/lib/org/inbound/facts.test.mjs +0 -471
- package/lib/org/inbound/hydrate.test.mjs +0 -453
- package/lib/org/inbound/index.test.mjs +0 -429
- package/lib/org/inbound/project.test.mjs +0 -287
- package/lib/org/integration-tools.test.mjs +0 -160
- package/lib/org/keys.test.mjs +0 -92
- package/lib/org/knowledge.test.mjs +0 -326
- package/lib/org/leases.test.mjs +0 -235
- package/lib/org/mesh-directives.test.mjs +0 -110
- package/lib/org/mesh-integration.test.mjs +0 -127
- package/lib/org/mesh.test.mjs +0 -400
- package/lib/org/messaging.test.mjs +0 -471
- package/lib/org/param-contract.test.mjs +0 -477
- package/lib/org/policy.test.mjs +0 -237
- package/lib/org/protocol.checksum.test.mjs +0 -90
- package/lib/org/protocol.test.mjs +0 -323
- package/lib/org/push.test.mjs +0 -792
- package/lib/org/registry.test.mjs +0 -100
- package/lib/org/resource-tools.test.mjs +0 -361
- package/lib/org/tool-access.test.mjs +0 -144
- package/lib/org/tool-surface-integration.test.mjs +0 -120
- package/lib/org/tool-surface.test.mjs +0 -1268
- package/lib/org/typing.test.mjs +0 -291
- package/lib/org/ui-parity.test.mjs +0 -560
- package/lib/org/verify.test.mjs +0 -194
- package/lib/org/work-ledger.test.mjs +0 -273
- package/lib/plan/adoption-e2e.test.mjs +0 -366
- package/lib/plan/budget-enforcement.test.mjs +0 -400
- package/lib/plan/compile.test.mjs +0 -382
- package/lib/plan/emit.test.mjs +0 -269
- package/lib/plan/explain.test.mjs +0 -188
- package/lib/prompts/parallelism.test.mjs +0 -177
- package/lib/rag/rag.test.mjs +0 -505
- package/lib/rate-guard.test.mjs +0 -272
- package/lib/reactive-gate.test.mjs +0 -57
- package/lib/render.test.mjs +0 -68
- package/lib/resource-governor.test.mjs +0 -488
- package/lib/scheduling/dynamic-jobs.test.mjs +0 -344
- package/lib/scheduling/jitter.test.mjs +0 -140
- package/lib/secrets/broker.test.mjs +0 -280
- package/lib/secrets/providers.test.mjs +0 -274
- package/lib/security/audit-engine.test.mjs +0 -424
- package/lib/security/coerce-args.test.mjs +0 -281
- package/lib/security/dangerous-tools.test.mjs +0 -68
- package/lib/security/external-content.test.mjs +0 -84
- package/lib/security/redact.test.mjs +0 -441
- package/lib/security/secret-equal.test.mjs +0 -55
- package/lib/session/config.test.mjs +0 -92
- package/lib/session/feed-core.test.mjs +0 -198
- package/lib/session/first-run.test.mjs +0 -121
- package/lib/session/frontdoor.test.mjs +0 -205
- package/lib/session/handoffs.test.mjs +0 -183
- package/lib/session/identity.test.mjs +0 -180
- package/lib/session/inbox-claims.test.mjs +0 -286
- package/lib/session/launch-args.test.mjs +0 -157
- package/lib/session/liveness.test.mjs +0 -100
- package/lib/session/status-summary.test.mjs +0 -118
- package/lib/session-permissions.test.mjs +0 -120
- package/lib/setup/claude-probe.test.mjs +0 -187
- package/lib/setup/completeness.test.mjs +0 -110
- package/lib/setup/context-pack.test.mjs +0 -89
- package/lib/setup/enrich.test.mjs +0 -115
- package/lib/setup/enroll-from-cohort.test.mjs +0 -300
- package/lib/setup/integration.test.mjs +0 -162
- package/lib/setup/io.test.mjs +0 -77
- package/lib/setup/runner.test.mjs +0 -132
- package/lib/setup/sections/identity.test.mjs +0 -234
- package/lib/setup/sections/inventory.test.mjs +0 -198
- package/lib/setup/sections/learning.test.mjs +0 -81
- package/lib/setup/sections/mandate.test.mjs +0 -388
- package/lib/setup/sections/messaging.test.mjs +0 -127
- package/lib/setup/sections/model.test.mjs +0 -240
- package/lib/setup/sections/org.test.mjs +0 -346
- package/lib/setup/sections/orgmail.test.mjs +0 -118
- package/lib/setup/sections/recovery.test.mjs +0 -98
- package/lib/setup/sections/subagents.test.mjs +0 -429
- package/lib/setup/sections/verify.test.mjs +0 -175
- package/lib/setup/sot.test.mjs +0 -81
- package/lib/setup/state.test.mjs +0 -115
- package/lib/singleton.test.mjs +0 -151
- package/lib/subagents/cli.test.mjs +0 -389
- package/lib/subagents/client.test.mjs +0 -309
- package/lib/subagents/gap.test.mjs +0 -234
- package/lib/subagents/lock.test.mjs +0 -248
- package/lib/subagents/manifest.test.mjs +0 -175
- package/lib/subagents/refs.test.mjs +0 -204
- package/lib/subagents/resolve.test.mjs +0 -422
- package/lib/subagents/schema.test.mjs +0 -328
- package/lib/telemetry/alerts.test.mjs +0 -109
- package/lib/telemetry/collect.test.mjs +0 -1274
- package/lib/tool-definitions-integration.test.mjs +0 -83
- package/lib/tool-definitions.test.mjs +0 -437
- package/lib/upgrade/global-refresh.test.mjs +0 -65
- package/lib/upgrade/launchd-reconcile.test.mjs +0 -272
- package/lib/upgrade/post-steps.test.mjs +0 -200
- package/lib/upgrade/verify.test.mjs +0 -164
- package/lib/util/fetch-timeout.test.mjs +0 -202
- package/lib/util/reconnect.test.mjs +0 -369
- package/lib/util/unhandled.test.mjs +0 -216
- package/lib/voice/outbound.test.mjs +0 -69
- package/lib/voice/session-rotation.test.mjs +0 -114
- package/lib/voice/stt.test.mjs +0 -226
- package/lib/voice/voice.test.mjs +0 -990
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +0 -187
- package/scripts/ci/check-docs-accuracy.test.mjs +0 -409
- package/scripts/ci/check-durable-write-seam.test.mjs +0 -90
- package/scripts/ci/check-no-build-artifacts.test.mjs +0 -71
- package/scripts/ci/check-no-residual-identity.test.mjs +0 -202
- package/scripts/ci/check-skill-packs.test.mjs +0 -495
- package/scripts/ci/check-subagent-frontmatter.test.mjs +0 -124
- package/scripts/ci/check.test.mjs +0 -194
- package/scripts/ci/conformance-org-api.test.mjs +0 -425
- package/scripts/cloud-relay/voice/relay-identity.test.mjs +0 -96
- package/scripts/collective/hook-runner.test.mjs +0 -173
- package/scripts/cost/fleet-digest.test.mjs +0 -207
- package/scripts/cost/track-claude-usage-pricing.test.mjs +0 -183
- package/scripts/cost/track-claude-usage.test.mjs +0 -148
- package/scripts/daemon/agent-daemon-board-mine.test.mjs +0 -96
- package/scripts/daemon/agent-daemon-design.test.mjs +0 -238
- package/scripts/daemon/agent-daemon-frontdoor.test.mjs +0 -60
- package/scripts/daemon/agent-daemon.test.mjs +0 -995
- package/scripts/daemon/assurance-e2e.test.mjs +0 -613
- package/scripts/daemon/assurance.test.mjs +0 -1791
- package/scripts/daemon/board-mirror.test.mjs +0 -165
- package/scripts/daemon/cadence-consumer-frontdoor.test.mjs +0 -393
- package/scripts/daemon/cadence-consumer-governance.test.mjs +0 -276
- package/scripts/daemon/cadence-consumer.test.mjs +0 -776
- package/scripts/daemon/cadence-handlers.test.mjs +0 -837
- package/scripts/daemon/classifier-identity.test.mjs +0 -137
- package/scripts/daemon/classifier.test.mjs +0 -266
- package/scripts/daemon/classify-kind.test.mjs +0 -40
- package/scripts/daemon/context-compiler.test.mjs +0 -300
- package/scripts/daemon/deliver.test.mjs +0 -564
- package/scripts/daemon/dispatcher-cooldown.test.mjs +0 -122
- package/scripts/daemon/dispatcher-governance.test.mjs +0 -1013
- package/scripts/daemon/dispatcher-resume.test.mjs +0 -166
- package/scripts/daemon/execution-ladder.test.mjs +0 -470
- package/scripts/daemon/goal-steward-cadence.test.mjs +0 -312
- package/scripts/daemon/inbox-deferral-session.test.mjs +0 -49
- package/scripts/daemon/inbox-deferral.test.mjs +0 -336
- package/scripts/daemon/inbox-wake.test.mjs +0 -199
- package/scripts/daemon/integration.test.mjs +0 -149
- package/scripts/daemon/lib/self-echo.test.mjs +0 -153
- package/scripts/daemon/lib/session-router.test.mjs +0 -295
- package/scripts/daemon/prompt-builder-preamble.test.mjs +0 -210
- package/scripts/daemon/prompt-builder.test.mjs +0 -344
- package/scripts/daemon/responder-cost.test.mjs +0 -68
- package/scripts/daemon/responder-history.test.mjs +0 -185
- package/scripts/daemon/sdk-version.test.mjs +0 -31
- package/scripts/daemon/session-lock.test.mjs +0 -252
- package/scripts/daemon/session-outcomes.test.mjs +0 -533
- package/scripts/daemon/typing-registry.test.mjs +0 -102
- package/scripts/hooks/pre-send-audit.test.mjs +0 -354
- package/scripts/huddle/huddle-prompt.test.mjs +0 -176
- package/scripts/local-triggers/autoupdate.test.mjs +0 -518
- package/scripts/local-triggers/generate-plists.test.mjs +0 -456
- package/scripts/media-generation/brand-clause.test.mjs +0 -135
- package/scripts/org/send-orgmail.first-contact.test.mjs +0 -102
- package/scripts/poller/inbox-privilege-injection.test.mjs +0 -167
- package/scripts/poller/inbox-scan-poller.test.mjs +0 -295
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +0 -133
- package/scripts/poller/slack-socket-mode.test.mjs +0 -805
- package/scripts/poller-launchd/install.test.mjs +0 -243
- package/scripts/restore-from-backup.test.mjs +0 -181
- package/scripts/session/feed.test.mjs +0 -196
- package/scripts/session/supervisor-sh.test.mjs +0 -218
- package/scripts/session/supervisor.test.mjs +0 -482
- package/scripts/setup/configure-macos.test.mjs +0 -306
- package/scripts/setup/gen-subagent-manifest.test.mjs +0 -124
- package/scripts/setup/generate-agent-package-json.test.mjs +0 -143
- package/scripts/setup/generate-capability.test.mjs +0 -134
- package/scripts/setup/init-agent.test.mjs +0 -370
- package/scripts/setup/init-skill-marketplace.test.mjs +0 -193
- package/scripts/vendor/sync-skill-packs.test.mjs +0 -103
- package/scripts/watchdog/memory-watchdog.test.mjs +0 -64
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/mcp/client.mjs — connect to one MCP server and use its tools and resources.
|
|
3
|
+
*
|
|
4
|
+
* Lifecycle (MCP specification):
|
|
5
|
+
* 1. `initialize` offering our protocol version (2025-06-18), our client
|
|
6
|
+
* info, and no optional capabilities;
|
|
7
|
+
* 2. the server answers with the version it will speak — accepted if it is
|
|
8
|
+
* one we support, otherwise the connection is closed with an error;
|
|
9
|
+
* 3. `notifications/initialized`; on streamable HTTP the optional GET
|
|
10
|
+
* stream is then opened for server-initiated messages (a server that
|
|
11
|
+
* does not offer it answers 405, which is fine);
|
|
12
|
+
* 4. `tools/list`, following `nextCursor` pages.
|
|
13
|
+
*
|
|
14
|
+
* A `notifications/tools/list_changed` from the server (on stdio, or the
|
|
15
|
+
* HTTP GET stream) sets `toolsChanged`; the run re-lists at its next turn
|
|
16
|
+
* boundary (`refreshTools`, via mcp/index.mjs), never mid-turn.
|
|
17
|
+
*
|
|
18
|
+
* Resources: when the server declares the `resources` capability,
|
|
19
|
+
* `listResources` (paginated `resources/list`) and `readResource`
|
|
20
|
+
* (`resources/read`) back the ListMcpResources / ReadMcpResource tools.
|
|
21
|
+
*
|
|
22
|
+
* `connectMcpServer` never throws for an expected failure (a missing binary,
|
|
23
|
+
* a server that exits, a bad version, a timeout): it returns `{ok:false}`.
|
|
24
|
+
* `callTool` likewise returns an engine tool result; a crashed server becomes
|
|
25
|
+
* an error result naming the server and its last stderr, and the run goes on
|
|
26
|
+
* (mcp/supervisor.mjs restarts a crashed stdio server once per run). Image
|
|
27
|
+
* content is attached to the result (`images`) for the wire to send.
|
|
28
|
+
*
|
|
29
|
+
* A stdio server inherits the run's environment minus every credential-shaped
|
|
30
|
+
* variable, the org credential included (context/child-env.mjs, CF-22): a
|
|
31
|
+
* credential reaches it only when its entry's `inheritEnv` names it, or the
|
|
32
|
+
* run's `envPassthrough` names an extended-shape one. Its config `env` is
|
|
33
|
+
* applied on top, so a server that needs a value gets it by naming it. Every error string
|
|
34
|
+
* this module returns — it reaches stderr, the JSON result and the transcript —
|
|
35
|
+
* has credential values, the server's header and env values, and URL secrets
|
|
36
|
+
* removed.
|
|
37
|
+
*
|
|
38
|
+
* @module lib/engine/mcp/client
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
import { configuredCommandEnv, redactSecrets } from "../context/child-env.mjs";
|
|
42
|
+
import { createRpcPeer } from "./jsonrpc.mjs";
|
|
43
|
+
import { createStdioTransport } from "./stdio.mjs";
|
|
44
|
+
import { createHttpTransport } from "./http.mjs";
|
|
45
|
+
import { mapCallToolResult, mapReadResourceResult } from "./results.mjs";
|
|
46
|
+
|
|
47
|
+
export const CLIENT_PROTOCOL_VERSION = "2025-06-18";
|
|
48
|
+
export const SUPPORTED_PROTOCOL_VERSIONS = Object.freeze(["2025-06-18", "2025-03-26", "2024-11-05"]);
|
|
49
|
+
export const DEFAULT_STARTUP_TIMEOUT_MS = 30_000;
|
|
50
|
+
export const DEFAULT_TOOL_TIMEOUT_MS = 10 * 60_000;
|
|
51
|
+
const MAX_LIST_PAGES = 100;
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @typedef {{ok:true, content:string, images?:Array<{mediaType:string, data:string}>}|{ok:false, error:string}} ToolOutcome
|
|
55
|
+
* @typedef {Object} McpConnection
|
|
56
|
+
* @property {string} name
|
|
57
|
+
* @property {string} protocolVersion
|
|
58
|
+
* @property {{name?:string, version?:string}|null} serverInfo
|
|
59
|
+
* @property {string|null} instructions
|
|
60
|
+
* @property {Record<string, any>} capabilities
|
|
61
|
+
* @property {Array<{name:string, description?:string, inputSchema?:object, annotations?:{readOnlyHint?:boolean}}>} tools
|
|
62
|
+
* @property {boolean} toolsChanged
|
|
63
|
+
* @property {() => Promise<{ok:true, tools:any[]}|{ok:false, error:string}>} refreshTools
|
|
64
|
+
* @property {(tool:string, args:object, o?:{signal?:AbortSignal, timeoutMs?:number}) => Promise<ToolOutcome>} callTool
|
|
65
|
+
* @property {(o?:{signal?:AbortSignal}) => Promise<{ok:true, resources:any[]}|{ok:false, error:string}>} listResources
|
|
66
|
+
* @property {(uri:string, o?:{signal?:AbortSignal}) => Promise<ToolOutcome>} readResource
|
|
67
|
+
* @property {() => boolean} isAlive
|
|
68
|
+
* @property {() => Promise<void>} close
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* @param {import('./config.mjs').McpServerConfig} config
|
|
73
|
+
* @param {object} [deps]
|
|
74
|
+
* @param {{name:string, version:string}} [deps.clientInfo]
|
|
75
|
+
* @param {number} [deps.startupTimeoutMs]
|
|
76
|
+
* @param {number} [deps.toolTimeoutMs]
|
|
77
|
+
* @param {Record<string,string|undefined>} [deps.env] base environment for stdio servers
|
|
78
|
+
* @param {string[]} [deps.envPassthrough] extended-shape secrets every stdio server may inherit (CF-22)
|
|
79
|
+
* @param {string} [deps.cwd]
|
|
80
|
+
* @param {typeof import('node:child_process').spawn} [deps.spawnImpl]
|
|
81
|
+
* @param {typeof fetch} [deps.fetchImpl]
|
|
82
|
+
* @param {boolean} [deps.eventStream] open the HTTP GET stream (default true)
|
|
83
|
+
* @returns {Promise<{ok:true, connection:McpConnection} | {ok:false, error:string}>}
|
|
84
|
+
*/
|
|
85
|
+
export async function connectMcpServer(config, deps = {}) {
|
|
86
|
+
const baseEnv = deps.env ?? process.env;
|
|
87
|
+
// Credential-named config env values, every header value, every URL query value.
|
|
88
|
+
const redact = (/** @type string */ text) =>
|
|
89
|
+
redactSecrets(text, {
|
|
90
|
+
env: { ...baseEnv, ...(config.transport === "stdio" ? config.env : {}) },
|
|
91
|
+
values: config.transport === "stdio" ? [] : [...Object.values(config.headers ?? {}), ...new URL(config.url).searchParams.values()],
|
|
92
|
+
});
|
|
93
|
+
const r = await connect(config, deps, baseEnv);
|
|
94
|
+
if (!r.ok) return { ok: false, error: redact(r.error) };
|
|
95
|
+
const c = r.connection;
|
|
96
|
+
const clean = (/** @type {any} */ res) => (res.ok ? res : { ...res, error: redact(res.error) });
|
|
97
|
+
const call = c.callTool;
|
|
98
|
+
c.callTool = async (tool, args, o) => clean(await call(tool, args, o));
|
|
99
|
+
const read = c.readResource;
|
|
100
|
+
c.readResource = async (uri, o) => clean(await read(uri, o));
|
|
101
|
+
const list = c.listResources;
|
|
102
|
+
c.listResources = async (o) => clean(await list(o));
|
|
103
|
+
const refresh = c.refreshTools;
|
|
104
|
+
c.refreshTools = async () => clean(await refresh());
|
|
105
|
+
return r;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* @param {import('./config.mjs').McpServerConfig} config
|
|
110
|
+
* @param {Parameters<typeof connectMcpServer>[1] & {}} deps
|
|
111
|
+
* @param {Record<string,string|undefined>} baseEnv
|
|
112
|
+
* @returns {Promise<{ok:true, connection:McpConnection} | {ok:false, error:string}>}
|
|
113
|
+
*/
|
|
114
|
+
async function connect(config, deps, baseEnv) {
|
|
115
|
+
const startupTimeoutMs = deps.startupTimeoutMs ?? DEFAULT_STARTUP_TIMEOUT_MS;
|
|
116
|
+
const toolTimeoutMs = deps.toolTimeoutMs ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
117
|
+
const name = config.name;
|
|
118
|
+
|
|
119
|
+
const transport =
|
|
120
|
+
config.transport === "stdio"
|
|
121
|
+
? createStdioTransport({
|
|
122
|
+
command: config.command,
|
|
123
|
+
args: config.args,
|
|
124
|
+
env: { ...configuredCommandEnv(baseEnv, { inheritEnv: config.inheritEnv ?? [], passthrough: deps.envPassthrough ?? [] }), ...config.env },
|
|
125
|
+
cwd: config.cwd ?? deps.cwd,
|
|
126
|
+
spawnImpl: deps.spawnImpl,
|
|
127
|
+
})
|
|
128
|
+
: createHttpTransport({ url: config.url, headers: config.headers, fetchImpl: deps.fetchImpl });
|
|
129
|
+
|
|
130
|
+
/** @type {string|null} */
|
|
131
|
+
let closeReason = null;
|
|
132
|
+
let toolsChanged = false;
|
|
133
|
+
const peer = createRpcPeer({
|
|
134
|
+
send: (m, o) => transport.send(m, o),
|
|
135
|
+
onNotification: (m) => {
|
|
136
|
+
if (m.method === "notifications/tools/list_changed") toolsChanged = true;
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
const started = transport.start(
|
|
140
|
+
(msg) => peer.receive(msg),
|
|
141
|
+
(reason) => {
|
|
142
|
+
closeReason = reason;
|
|
143
|
+
peer.close(reason);
|
|
144
|
+
},
|
|
145
|
+
);
|
|
146
|
+
const describe = (/** @type string */ why) => {
|
|
147
|
+
const tail = transport.stderrTail();
|
|
148
|
+
return `MCP server "${name}": ${why}${tail ? `\n--- server stderr (tail) ---\n${tail}` : ""}`;
|
|
149
|
+
};
|
|
150
|
+
if (!started.ok) return { ok: false, error: describe(started.error) };
|
|
151
|
+
|
|
152
|
+
const fail = async (/** @type string */ why) => {
|
|
153
|
+
await transport.close();
|
|
154
|
+
return { ok: /** @type {false} */ (false), error: describe(why) };
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const init = await peer.request(
|
|
158
|
+
"initialize",
|
|
159
|
+
{ protocolVersion: CLIENT_PROTOCOL_VERSION, capabilities: {}, clientInfo: deps.clientInfo ?? { name: "cohort-engine", version: "0" } },
|
|
160
|
+
{ timeoutMs: startupTimeoutMs },
|
|
161
|
+
);
|
|
162
|
+
if (!init.ok) return fail(`initialize failed: ${init.error.message}`);
|
|
163
|
+
const version = init.result?.protocolVersion;
|
|
164
|
+
if (!SUPPORTED_PROTOCOL_VERSIONS.includes(version)) {
|
|
165
|
+
return fail(`the server speaks protocol version ${JSON.stringify(version)}; supported: ${SUPPORTED_PROTOCOL_VERSIONS.join(", ")}`);
|
|
166
|
+
}
|
|
167
|
+
if ("setProtocolVersion" in transport) transport.setProtocolVersion(version);
|
|
168
|
+
peer.notify("notifications/initialized");
|
|
169
|
+
if ("openEventStream" in transport && deps.eventStream !== false) void transport.openEventStream();
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Every page of a list method. A server without the capability (-32601) has none.
|
|
173
|
+
* @param {string} method @param {string} key @param {{signal?:AbortSignal, timeoutMs:number}} o
|
|
174
|
+
* @returns {Promise<{ok:true, items:any[]}|{ok:false, error:string}>}
|
|
175
|
+
*/
|
|
176
|
+
const listAll = async (method, key, o) => {
|
|
177
|
+
const items = [];
|
|
178
|
+
let cursor;
|
|
179
|
+
for (let page = 0; page < MAX_LIST_PAGES; page++) {
|
|
180
|
+
const listed = await peer.request(method, cursor ? { cursor } : {}, o);
|
|
181
|
+
if (!listed.ok) {
|
|
182
|
+
if (listed.error.kind === "rpc" && listed.error.code === -32601 && page === 0) break;
|
|
183
|
+
return { ok: false, error: `${method} failed: ${listed.error.message}` };
|
|
184
|
+
}
|
|
185
|
+
const list = Array.isArray(listed.result?.[key]) ? listed.result[key] : [];
|
|
186
|
+
items.push(...list);
|
|
187
|
+
cursor = listed.result?.nextCursor;
|
|
188
|
+
if (!cursor) break;
|
|
189
|
+
}
|
|
190
|
+
return { ok: true, items };
|
|
191
|
+
};
|
|
192
|
+
|
|
193
|
+
const listed = await listAll("tools/list", "tools", { timeoutMs: startupTimeoutMs });
|
|
194
|
+
if (!listed.ok) return fail(listed.error);
|
|
195
|
+
const validTools = (/** @type any[] */ items) => items.filter((t) => t && typeof t.name === "string");
|
|
196
|
+
|
|
197
|
+
/** @type {McpConnection} */
|
|
198
|
+
const connection = {
|
|
199
|
+
name,
|
|
200
|
+
protocolVersion: version,
|
|
201
|
+
serverInfo: init.result?.serverInfo ?? null,
|
|
202
|
+
instructions: typeof init.result?.instructions === "string" ? init.result.instructions : null,
|
|
203
|
+
capabilities: init.result?.capabilities && typeof init.result.capabilities === "object" ? init.result.capabilities : {},
|
|
204
|
+
tools: validTools(listed.items),
|
|
205
|
+
get toolsChanged() {
|
|
206
|
+
return toolsChanged;
|
|
207
|
+
},
|
|
208
|
+
isAlive: () => closeReason === null && !peer.closed,
|
|
209
|
+
async refreshTools() {
|
|
210
|
+
if (!connection.isAlive()) return { ok: false, error: describe(`is not running (${closeReason ?? "closed"}); its tools could not be re-listed`) };
|
|
211
|
+
toolsChanged = false;
|
|
212
|
+
const again = await listAll("tools/list", "tools", { timeoutMs: startupTimeoutMs });
|
|
213
|
+
if (!again.ok) return { ok: false, error: describe(again.error) };
|
|
214
|
+
connection.tools = validTools(again.items);
|
|
215
|
+
return { ok: true, tools: connection.tools };
|
|
216
|
+
},
|
|
217
|
+
async callTool(tool, args, o = {}) {
|
|
218
|
+
if (!connection.isAlive()) return { ok: false, error: describe(`is not running (${closeReason ?? "closed"}); ${tool} was not called`) };
|
|
219
|
+
const r = await peer.request("tools/call", { name: tool, arguments: args ?? {} }, { signal: o.signal, timeoutMs: o.timeoutMs ?? toolTimeoutMs });
|
|
220
|
+
if (r.ok) return mapCallToolResult(r.result, { attachImages: true });
|
|
221
|
+
return rpcFailure(r.error, tool);
|
|
222
|
+
},
|
|
223
|
+
async listResources(o = {}) {
|
|
224
|
+
if (!connection.isAlive()) return { ok: false, error: describe(`is not running (${closeReason ?? "closed"})`) };
|
|
225
|
+
const r = await listAll("resources/list", "resources", { signal: o.signal, timeoutMs: startupTimeoutMs });
|
|
226
|
+
return r.ok ? { ok: true, resources: r.items.filter((x) => x && typeof x.uri === "string") } : { ok: false, error: describe(r.error) };
|
|
227
|
+
},
|
|
228
|
+
async readResource(uri, o = {}) {
|
|
229
|
+
if (!connection.isAlive()) return { ok: false, error: describe(`is not running (${closeReason ?? "closed"}); ${uri} was not read`) };
|
|
230
|
+
const r = await peer.request("resources/read", { uri }, { signal: o.signal, timeoutMs: toolTimeoutMs });
|
|
231
|
+
if (r.ok) return mapReadResourceResult(r.result, { attachImages: true });
|
|
232
|
+
return rpcFailure(r.error, `resources/read ${uri}`);
|
|
233
|
+
},
|
|
234
|
+
async close() {
|
|
235
|
+
peer.close("closed by the engine");
|
|
236
|
+
await transport.close();
|
|
237
|
+
},
|
|
238
|
+
};
|
|
239
|
+
|
|
240
|
+
/** @param {import('./jsonrpc.mjs').RpcError} error @param {string} what @returns {{ok:false, error:string}} */
|
|
241
|
+
function rpcFailure(error, what) {
|
|
242
|
+
switch (error.kind) {
|
|
243
|
+
case "rpc":
|
|
244
|
+
return { ok: false, error: `MCP server "${name}" refused ${what}: ${error.message} (code ${error.code})` };
|
|
245
|
+
case "aborted":
|
|
246
|
+
return { ok: false, error: `${what} was cancelled.` };
|
|
247
|
+
case "timeout":
|
|
248
|
+
return { ok: false, error: `MCP server "${name}" did not answer ${what}: ${error.message}` };
|
|
249
|
+
default:
|
|
250
|
+
return { ok: false, error: describe(`failed during ${what}: ${error.message}`) };
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
return { ok: true, connection };
|
|
254
|
+
}
|
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/engine/mcp/config.mjs — which MCP servers a run connects to.
|
|
3
|
+
*
|
|
4
|
+
* Config shape (the one maestro's `.mcp.json` and `~/.claude.json` already use):
|
|
5
|
+
*
|
|
6
|
+
* { "mcpServers": {
|
|
7
|
+
* "<name>": { "command": "cohort-mcp", "args": [], "env": {} }, // stdio
|
|
8
|
+
* "<name>": { "type": "http", "url": "https://…/mcp", "headers": {} } } } // streamable HTTP
|
|
9
|
+
*
|
|
10
|
+
* `type` may be omitted or "stdio" for a command server; "http" (alias
|
|
11
|
+
* "streamable-http") for a URL server. The legacy HTTP+SSE transport ("sse")
|
|
12
|
+
* is refused with a clear error rather than half-supported.
|
|
13
|
+
*
|
|
14
|
+
* `${VAR}` and `${VAR:-default}` are expanded from the environment in
|
|
15
|
+
* command, args, env values, url and headers. An unset variable with no default
|
|
16
|
+
* is an error for that server (it would otherwise connect with an empty token).
|
|
17
|
+
*
|
|
18
|
+
* Layers, lowest to highest precedence (a later layer's server with the same
|
|
19
|
+
* name replaces an earlier one):
|
|
20
|
+
*
|
|
21
|
+
* user ~/.claude.json mcpServers
|
|
22
|
+
* project <cwd>/.mcp.json mcpServers
|
|
23
|
+
* local ~/.claude.json projects[<cwd>].mcpServers
|
|
24
|
+
* cli --mcp-config <file|json> (repeatable; later flags win)
|
|
25
|
+
*
|
|
26
|
+
* `--strict-mcp-config` uses ONLY the cli layer.
|
|
27
|
+
*
|
|
28
|
+
* The project layer is the repository's word, so it is trusted less:
|
|
29
|
+
* · a project server starts only when approved (`filterProjectServers`):
|
|
30
|
+
* `enableAllProjectMcpServers: true` or its name in `enabledMcpjsonServers`,
|
|
31
|
+
* from user, managed or cli settings or from ~/.claude.json's
|
|
32
|
+
* projects[<cwd>] entry; a name in `disabledMcpjsonServers` never starts;
|
|
33
|
+
* · its `${VAR}` placeholders may not name a credential-shaped variable
|
|
34
|
+
* (context/child-env.mjs): a repository cannot send the gateway token, a
|
|
35
|
+
* provider key or the org token to a URL it chose;
|
|
36
|
+
* · its `inheritEnv` may not list one either (CF-22);
|
|
37
|
+
* · nor, for either, a variable whose VALUE looks like a credential, whatever
|
|
38
|
+
* its name (`DATABASE_URL` with a password: context/secret-values.mjs, CF-118).
|
|
39
|
+
*
|
|
40
|
+
* `inheritEnv: ["NAME", …]` on a stdio entry is the credential opt-in: a stdio
|
|
41
|
+
* server inherits no credential-shaped variable unless its entry names it
|
|
42
|
+
* (context/child-env.mjs).
|
|
43
|
+
*
|
|
44
|
+
* Parsing and layering are pure (`parseMcpConfigText`, `resolveMcpServers`);
|
|
45
|
+
* the two readers take an injected `readFile`.
|
|
46
|
+
*
|
|
47
|
+
* @module lib/engine/mcp/config
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import path from "node:path";
|
|
51
|
+
import { validateServerName } from "./naming.mjs";
|
|
52
|
+
import { isCredentialEnv, parseEnvNameList } from "../context/child-env.mjs";
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* @typedef {{name:string, source:string, transport:'stdio', command:string, args:string[], env:Record<string,string>, inheritEnv?:string[], cwd?:string}} StdioServerConfig
|
|
56
|
+
* @typedef {{name:string, source:string, transport:'http', url:string, headers:Record<string,string>}} HttpServerConfig
|
|
57
|
+
* @typedef {StdioServerConfig | HttpServerConfig} McpServerConfig
|
|
58
|
+
*/
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Expand `${VAR}` / `${VAR:-default}`.
|
|
62
|
+
* @param {string} s
|
|
63
|
+
* @param {Record<string,string|undefined>} env
|
|
64
|
+
* @param {{refuse?:(name:string)=>boolean}} [o] names that may not be expanded here
|
|
65
|
+
* @returns {{ok:true, value:string} | {ok:false, error:string}}
|
|
66
|
+
*/
|
|
67
|
+
export function expandEnvVars(s, env, o = {}) {
|
|
68
|
+
let missing = null;
|
|
69
|
+
let refused = null;
|
|
70
|
+
const value = String(s).replace(/\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-([^}]*))?\}/g, (_, name, def) => {
|
|
71
|
+
if (o.refuse?.(name)) {
|
|
72
|
+
refused ??= name;
|
|
73
|
+
return "";
|
|
74
|
+
}
|
|
75
|
+
const v = env[name];
|
|
76
|
+
if (v !== undefined && v !== "") return v;
|
|
77
|
+
if (def !== undefined) return def;
|
|
78
|
+
missing ??= name;
|
|
79
|
+
return "";
|
|
80
|
+
});
|
|
81
|
+
if (refused) return { ok: false, error: `environment variable ${refused} is a credential and may not be expanded in a project .mcp.json` };
|
|
82
|
+
return missing ? { ok: false, error: `environment variable ${missing} is not set` } : { ok: true, value };
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* @param {string} name
|
|
87
|
+
* @param {unknown} raw
|
|
88
|
+
* @param {string} source
|
|
89
|
+
* @param {Record<string,string|undefined>} env
|
|
90
|
+
* @param {{refuse?:(name:string)=>boolean}} [expandOpts]
|
|
91
|
+
* @returns {{ok:true, server:McpServerConfig} | {ok:false, error:string}}
|
|
92
|
+
*/
|
|
93
|
+
export function parseServerEntry(name, raw, source, env = {}, expandOpts = {}) {
|
|
94
|
+
const fail = (/** @type string */ msg) => ({ ok: /** @type {false} */ (false), error: `MCP server "${name}" (${source}): ${msg}` });
|
|
95
|
+
const nameOk = validateServerName(name);
|
|
96
|
+
if (!nameOk.ok) return { ok: false, error: `${nameOk.error} (${source})` };
|
|
97
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) return fail("entry must be an object");
|
|
98
|
+
const r = /** @type {any} */ (raw);
|
|
99
|
+
const type = r.type ?? (r.url !== undefined && r.command === undefined ? "http" : "stdio");
|
|
100
|
+
const expand = (/** @type {unknown} */ v, /** @type string */ what) => {
|
|
101
|
+
if (typeof v !== "string") return { ok: false, error: `${what} must be a string` };
|
|
102
|
+
return expandEnvVars(v, env, expandOpts);
|
|
103
|
+
};
|
|
104
|
+
const expandMap = (/** @type {unknown} */ m, /** @type string */ what) => {
|
|
105
|
+
if (m === undefined || m === null) return { ok: true, value: {} };
|
|
106
|
+
if (typeof m !== "object" || Array.isArray(m)) return { ok: false, error: `${what} must be an object` };
|
|
107
|
+
/** @type {Record<string,string>} */
|
|
108
|
+
const out = {};
|
|
109
|
+
for (const [k, v] of Object.entries(m)) {
|
|
110
|
+
const e = expand(v, `${what}.${k}`);
|
|
111
|
+
if (!e.ok) return { ok: false, error: `${what}.${k}: ${e.error}` };
|
|
112
|
+
out[k] = e.value;
|
|
113
|
+
}
|
|
114
|
+
return { ok: true, value: out };
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
if (type === "stdio") {
|
|
118
|
+
const cmd = expand(r.command, "command");
|
|
119
|
+
if (!cmd.ok) return fail(cmd.error);
|
|
120
|
+
if (cmd.value.trim() === "") return fail("command is empty");
|
|
121
|
+
const args = [];
|
|
122
|
+
if (r.args !== undefined) {
|
|
123
|
+
if (!Array.isArray(r.args)) return fail("args must be an array of strings");
|
|
124
|
+
for (const [i, a] of r.args.entries()) {
|
|
125
|
+
const e = expand(a, `args[${i}]`);
|
|
126
|
+
if (!e.ok) return fail(`args[${i}]: ${e.error}`);
|
|
127
|
+
args.push(e.value);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const envMap = expandMap(r.env, "env");
|
|
131
|
+
if (!envMap.ok) return fail(envMap.error);
|
|
132
|
+
// CF-22: the credentials this server inherits, by name. A project .mcp.json may not list one.
|
|
133
|
+
const inherit = parseEnvNameList(r.inheritEnv);
|
|
134
|
+
if (!inherit.ok) return fail(`inheritEnv ${inherit.error}`);
|
|
135
|
+
const refusedInherit = expandOpts.refuse ? inherit.names.find((n) => expandOpts.refuse?.(n)) : undefined;
|
|
136
|
+
if (refusedInherit) return fail(`inheritEnv names ${refusedInherit}, a credential, which a project .mcp.json may not pass to a server`);
|
|
137
|
+
/** @type {StdioServerConfig} */
|
|
138
|
+
const server = { name, source, transport: "stdio", command: cmd.value, args, env: envMap.value };
|
|
139
|
+
if (inherit.names.length > 0) server.inheritEnv = inherit.names;
|
|
140
|
+
if (typeof r.cwd === "string") server.cwd = r.cwd;
|
|
141
|
+
return { ok: true, server };
|
|
142
|
+
}
|
|
143
|
+
if (type === "http" || type === "streamable-http") {
|
|
144
|
+
const url = expand(r.url, "url");
|
|
145
|
+
if (!url.ok) return fail(url.error);
|
|
146
|
+
try {
|
|
147
|
+
const u = new URL(url.value);
|
|
148
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return fail(`url must be http(s), got ${u.protocol}`);
|
|
149
|
+
} catch {
|
|
150
|
+
return fail(`url "${url.value}" is not a valid URL`);
|
|
151
|
+
}
|
|
152
|
+
const headers = expandMap(r.headers, "headers");
|
|
153
|
+
if (!headers.ok) return fail(headers.error);
|
|
154
|
+
return { ok: true, server: { name, source, transport: "http", url: url.value, headers: headers.value } };
|
|
155
|
+
}
|
|
156
|
+
if (type === "sse") return fail('the legacy "sse" transport is not supported; use a streamable HTTP endpoint ("type": "http")');
|
|
157
|
+
return fail(`unknown transport type "${String(type)}"`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* @param {unknown} doc a parsed config document ({mcpServers:{…}})
|
|
162
|
+
* @param {string} source
|
|
163
|
+
* @param {Record<string,string|undefined>} env
|
|
164
|
+
* @param {{refuse?:(name:string)=>boolean}} [expandOpts]
|
|
165
|
+
* @returns {{servers: McpServerConfig[], errors: string[]}}
|
|
166
|
+
*/
|
|
167
|
+
export function serversFromDocument(doc, source, env = {}, expandOpts = {}) {
|
|
168
|
+
/** @type {McpServerConfig[]} */
|
|
169
|
+
const servers = [];
|
|
170
|
+
const errors = [];
|
|
171
|
+
if (doc === undefined || doc === null) return { servers, errors };
|
|
172
|
+
if (typeof doc !== "object" || Array.isArray(doc)) return { servers, errors: [`${source}: MCP config must be a JSON object`] };
|
|
173
|
+
const map = /** @type {any} */ (doc).mcpServers;
|
|
174
|
+
if (map === undefined) return { servers, errors };
|
|
175
|
+
if (!map || typeof map !== "object" || Array.isArray(map)) return { servers, errors: [`${source}: "mcpServers" must be an object`] };
|
|
176
|
+
for (const [name, raw] of Object.entries(map)) {
|
|
177
|
+
const r = parseServerEntry(name, raw, source, env, expandOpts);
|
|
178
|
+
if (r.ok) servers.push(r.server);
|
|
179
|
+
else errors.push(r.error);
|
|
180
|
+
}
|
|
181
|
+
return { servers, errors };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {string} text
|
|
186
|
+
* @param {string} source
|
|
187
|
+
* @param {Record<string,string|undefined>} env
|
|
188
|
+
* @param {{refuse?:(name:string)=>boolean}} [expandOpts]
|
|
189
|
+
* @returns {{servers: McpServerConfig[], errors: string[]}}
|
|
190
|
+
*/
|
|
191
|
+
export function parseMcpConfigText(text, source, env = {}, expandOpts = {}) {
|
|
192
|
+
let doc;
|
|
193
|
+
try {
|
|
194
|
+
doc = JSON.parse(text);
|
|
195
|
+
} catch (e) {
|
|
196
|
+
return { servers: [], errors: [`${source}: not valid JSON (${e instanceof Error ? e.message : e})`] };
|
|
197
|
+
}
|
|
198
|
+
return serversFromDocument(doc, source, env, expandOpts);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* One `--mcp-config` value: inline JSON when it starts with "{", else a path.
|
|
203
|
+
* @param {string} value
|
|
204
|
+
* @param {{cwd:string, readFile:(p:string)=>string, env?:Record<string,string|undefined>}} deps
|
|
205
|
+
*/
|
|
206
|
+
export function readMcpConfigArg(value, { cwd, readFile, env = {} }) {
|
|
207
|
+
const v = String(value).trim();
|
|
208
|
+
if (v.startsWith("{")) return parseMcpConfigText(v, "--mcp-config (inline)", env);
|
|
209
|
+
const file = path.resolve(cwd, v);
|
|
210
|
+
let text;
|
|
211
|
+
try {
|
|
212
|
+
text = readFile(file);
|
|
213
|
+
} catch (e) {
|
|
214
|
+
return { servers: [], errors: [`--mcp-config ${file}: cannot read (${e instanceof Error ? e.message : e})`] };
|
|
215
|
+
}
|
|
216
|
+
return parseMcpConfigText(text, `--mcp-config ${file}`, env);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* The user, project and local layers from disk. A missing file is an empty
|
|
221
|
+
* layer; an unreadable or malformed one is an error, never a silent skip.
|
|
222
|
+
* @param {{homedir:string|null, cwd:string, readFile:(p:string)=>string, exists:(p:string)=>boolean, env?:Record<string,string|undefined>}} deps
|
|
223
|
+
*/
|
|
224
|
+
export function loadMcpLayers({ homedir, cwd, readFile, exists, env = {} }) {
|
|
225
|
+
const errors = [];
|
|
226
|
+
/** @type {{user:McpServerConfig[], project:McpServerConfig[], local:McpServerConfig[]}} */
|
|
227
|
+
const layers = { user: [], project: [], local: [] };
|
|
228
|
+
/** Approval recorded in ~/.claude.json projects[<cwd>] (a user-owned file). */
|
|
229
|
+
const approval = { enableAll: /** @type {boolean|null} */ (null), enabled: /** @type {string[]} */ ([]), disabled: /** @type {string[]} */ ([]) };
|
|
230
|
+
|
|
231
|
+
if (homedir) {
|
|
232
|
+
const file = path.join(homedir, ".claude.json");
|
|
233
|
+
if (exists(file)) {
|
|
234
|
+
try {
|
|
235
|
+
const doc = JSON.parse(readFile(file));
|
|
236
|
+
const u = serversFromDocument({ mcpServers: doc?.mcpServers }, `user ${file}`, env);
|
|
237
|
+
layers.user = u.servers;
|
|
238
|
+
errors.push(...u.errors);
|
|
239
|
+
const proj = doc?.projects && typeof doc.projects === "object" ? doc.projects[cwd] : undefined;
|
|
240
|
+
if (proj && typeof proj.enableAllProjectMcpServers === "boolean") approval.enableAll = proj.enableAllProjectMcpServers;
|
|
241
|
+
if (proj && Array.isArray(proj.enabledMcpjsonServers)) approval.enabled.push(...proj.enabledMcpjsonServers.map(String));
|
|
242
|
+
if (proj && Array.isArray(proj.disabledMcpjsonServers)) approval.disabled.push(...proj.disabledMcpjsonServers.map(String));
|
|
243
|
+
if (proj && proj.mcpServers) {
|
|
244
|
+
const l = serversFromDocument({ mcpServers: proj.mcpServers }, `local ${file} (projects["${cwd}"])`, env);
|
|
245
|
+
layers.local = l.servers;
|
|
246
|
+
errors.push(...l.errors);
|
|
247
|
+
}
|
|
248
|
+
} catch (e) {
|
|
249
|
+
errors.push(`user ${file}: cannot read MCP servers (${e instanceof Error ? e.message : e})`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const projectFile = path.join(cwd, ".mcp.json");
|
|
254
|
+
if (exists(projectFile)) {
|
|
255
|
+
try {
|
|
256
|
+
const p = parseMcpConfigText(readFile(projectFile), `project ${projectFile}`, env, { refuse: (n) => isCredentialEnv(n, env[n]) });
|
|
257
|
+
layers.project = p.servers;
|
|
258
|
+
errors.push(...p.errors);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
errors.push(`project ${projectFile}: cannot read (${e instanceof Error ? e.message : e})`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { layers, errors, approval };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Keep only the project servers that are approved (pure).
|
|
268
|
+
* @param {McpServerConfig[]} servers the project layer
|
|
269
|
+
* @param {Array<{enableAll?:boolean|null, enabled?:string[], disabled?:string[]}>} approvals lowest precedence first
|
|
270
|
+
* @returns {{servers: McpServerConfig[], unapproved: McpServerConfig[], disabled: McpServerConfig[]}}
|
|
271
|
+
*/
|
|
272
|
+
export function filterProjectServers(servers, approvals) {
|
|
273
|
+
let enableAll = false;
|
|
274
|
+
const enabled = new Set();
|
|
275
|
+
const disabled = new Set();
|
|
276
|
+
for (const a of approvals) {
|
|
277
|
+
if (typeof a.enableAll === "boolean") enableAll = a.enableAll;
|
|
278
|
+
for (const n of a.enabled ?? []) enabled.add(n);
|
|
279
|
+
for (const n of a.disabled ?? []) disabled.add(n);
|
|
280
|
+
}
|
|
281
|
+
const out = { servers: /** @type {McpServerConfig[]} */ ([]), unapproved: /** @type {McpServerConfig[]} */ ([]), disabled: /** @type {McpServerConfig[]} */ ([]) };
|
|
282
|
+
for (const s of servers) {
|
|
283
|
+
if (disabled.has(s.name)) out.disabled.push(s);
|
|
284
|
+
else if (enableAll || enabled.has(s.name)) out.servers.push(s);
|
|
285
|
+
else out.unapproved.push(s);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Merge layers into the final server list (pure).
|
|
292
|
+
* @param {{cli: McpServerConfig[], strict: boolean, user?: McpServerConfig[], project?: McpServerConfig[], local?: McpServerConfig[]}} p
|
|
293
|
+
* @returns {McpServerConfig[]} in first-seen order, later layers replacing earlier entries
|
|
294
|
+
*/
|
|
295
|
+
export function resolveMcpServers({ cli, strict, user = [], project = [], local = [] }) {
|
|
296
|
+
const ordered = strict ? [cli] : [user, project, local, cli];
|
|
297
|
+
/** @type {Map<string, McpServerConfig>} */
|
|
298
|
+
const byName = new Map();
|
|
299
|
+
for (const layer of ordered) for (const s of layer) byName.set(s.name, s);
|
|
300
|
+
return [...byName.values()];
|
|
301
|
+
}
|