@cohortapp/agent-sdk 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/init-agent.md +104 -0
- package/.claude/commands/init-maestro.md +1187 -0
- package/.claude/settings.json +161 -0
- package/.env.example +216 -0
- package/README.md +632 -0
- package/agents/browser-operator/agent.md +52 -0
- package/agents/calendar-ops/agent.md +50 -0
- package/agents/communications/agent.md +96 -0
- package/agents/decision-log/agent.md +65 -0
- package/agents/desktop-operator/agent.md +59 -0
- package/agents/gmail-operator/agent.md +62 -0
- package/agents/inbound-dispatcher/agent.md +66 -0
- package/agents/inbox-processor/agent.md +39 -0
- package/agents/pmo-execution/agent.md +60 -0
- package/agents/session-spawner/agent.md +64 -0
- package/agents/slack-operator/agent.md +60 -0
- package/agents/whatsapp-operator/agent.md +60 -0
- package/agents/workflow-automation/agent.md +61 -0
- package/archetypes/altitudes/c-suite.yaml +58 -0
- package/archetypes/altitudes/founder.yaml +68 -0
- package/archetypes/altitudes/senior-manager.yaml +63 -0
- package/archetypes/altitudes/svp.yaml +60 -0
- package/archetypes/altitudes/vp.yaml +50 -0
- package/archetypes/archetype.schema.json +77 -0
- package/archetypes/base.yaml +47 -0
- package/archetypes/capabilities/commercial-leader.yaml +159 -0
- package/archetypes/capabilities/compliance-officer.yaml +159 -0
- package/archetypes/capabilities/executive-operator.yaml +169 -0
- package/archetypes/capabilities/finance-leader.yaml +162 -0
- package/archetypes/capabilities/operations-leader.yaml +154 -0
- package/archetypes/capabilities/product-leader.yaml +148 -0
- package/archetypes/capabilities/technical-leader.yaml +146 -0
- package/archetypes/functions/commercial-leader.yaml +62 -0
- package/archetypes/functions/compliance-officer.yaml +64 -0
- package/archetypes/functions/executive-operator.yaml +70 -0
- package/archetypes/functions/finance-leader.yaml +70 -0
- package/archetypes/functions/operations-leader.yaml +62 -0
- package/archetypes/functions/product-leader.yaml +61 -0
- package/archetypes/functions/technical-leader.yaml +57 -0
- package/bin/cohort-mcp.mjs +81 -0
- package/bin/maestro.mjs +3516 -0
- package/bin/maestro.test.mjs +1015 -0
- package/desktop-control/README.md +56 -0
- package/desktop-control/app-profiles/gmail.yaml +120 -0
- package/desktop-control/app-profiles/slack.yaml +315 -0
- package/desktop-control/app-profiles/whatsapp.yaml +107 -0
- package/docs/architecture/agent-topology.md +2239 -0
- package/docs/architecture/archetype-agent-factory.md +110 -0
- package/docs/architecture/collective-memory-and-org-mesh.md +115 -0
- package/docs/architecture/continuous-monitoring.md +221 -0
- package/docs/architecture/mcp-capability-map.md +585 -0
- package/docs/architecture/system-architecture.md +1272 -0
- package/docs/company-context/README.md +40 -0
- package/docs/guides/agent-persona-setup.md +600 -0
- package/docs/guides/agents-observe-setup.md +64 -0
- package/docs/guides/billing-console-keys.md +88 -0
- package/docs/guides/ccxray-diagnostics.md +65 -0
- package/docs/guides/channel-bus.md +127 -0
- package/docs/guides/claude-mem-setup.md +79 -0
- package/docs/guides/claude-pace-setup.md +56 -0
- package/docs/guides/claudraband-sessions.md +98 -0
- package/docs/guides/clawteam-swarm.md +116 -0
- package/docs/guides/code-review-graph-setup.md +86 -0
- package/docs/guides/email-setup.md +431 -0
- package/docs/guides/mac-mini.md +119 -0
- package/docs/guides/media-generation-setup.md +349 -0
- package/docs/guides/model-routing.md +162 -0
- package/docs/guides/observability-otel.md +265 -0
- package/docs/guides/org-onboarding.md +132 -0
- package/docs/guides/outbound-governance-setup.md +437 -0
- package/docs/guides/pdf-generation-setup.md +315 -0
- package/docs/guides/poller-daemon-setup.md +563 -0
- package/docs/guides/rag-context-setup.md +459 -0
- package/docs/guides/self-optimization-pattern.md +82 -0
- package/docs/guides/setup-wizard.md +178 -0
- package/docs/guides/slack-setup.md +350 -0
- package/docs/guides/telegram-setup.md +227 -0
- package/docs/guides/twilio-subaccounts-setup.md +223 -0
- package/docs/guides/verification.md +128 -0
- package/docs/guides/voice-mode.md +188 -0
- package/docs/guides/voice-sms-setup.md +698 -0
- package/docs/guides/webhook-relay-setup.md +349 -0
- package/docs/guides/whatsapp-setup.md +288 -0
- package/docs/prompts/board-pack-cover-template.md +36 -0
- package/docs/prompts/decision-recommendation-template.md +88 -0
- package/docs/prompts/followup-message-template.md +141 -0
- package/docs/prompts/investor-letter-template.md +52 -0
- package/docs/prompts/morning-brief-template.md +82 -0
- package/docs/prompts/presentation-template.md +58 -0
- package/docs/prompts/weekly-strategic-memo-template.md +104 -0
- package/docs/research/hallucinated-tool-output-investigation.md +151 -0
- package/docs/runbooks/backup-restore.md +205 -0
- package/docs/runbooks/cohort-cutover.md +129 -0
- package/docs/runbooks/fleet-operations.md +200 -0
- package/docs/runbooks/incident-response.md +226 -0
- package/docs/runbooks/mac-mini-bootstrap.md +431 -0
- package/docs/runbooks/perpetual-operations.md +509 -0
- package/docs/runbooks/recovery-and-failover.md +260 -0
- package/framework-features.json +267 -0
- package/ingest/README.md +87 -0
- package/lib/action-executor.js +689 -0
- package/lib/action-executor.test.mjs +871 -0
- package/lib/agent-root.mjs +37 -0
- package/lib/archetype.mjs +236 -0
- package/lib/archetype.test.mjs +132 -0
- package/lib/autonomy.mjs +114 -0
- package/lib/autonomy.test.mjs +66 -0
- package/lib/backlog.mjs +358 -0
- package/lib/backlog.test.mjs +266 -0
- package/lib/budget-guard.mjs +279 -0
- package/lib/budget-guard.test.mjs +291 -0
- package/lib/cadence-bus-schedule.test.mjs +194 -0
- package/lib/cadence-bus.mjs +1120 -0
- package/lib/cadence-bus.test.mjs +720 -0
- package/lib/cadences.mjs +205 -0
- package/lib/cadences.test.mjs +125 -0
- package/lib/capability.mjs +154 -0
- package/lib/capability.test.mjs +78 -0
- package/lib/channels/base-adapter.mjs +719 -0
- package/lib/channels/base-adapter.test.mjs +590 -0
- package/lib/channels/channel.mjs +128 -0
- package/lib/channels/channels.test.mjs +371 -0
- package/lib/channels/contract.mjs +215 -0
- package/lib/channels/contract.test.mjs +137 -0
- package/lib/channels/conversation-resolver.mjs +95 -0
- package/lib/channels/gmail/adapter.mjs +87 -0
- package/lib/channels/inbox-item.mjs +255 -0
- package/lib/channels/inbox-item.test.mjs +335 -0
- package/lib/channels/index.mjs +94 -0
- package/lib/channels/orgmail/adapter.mjs +353 -0
- package/lib/channels/orgmail/adapter.test.mjs +311 -0
- package/lib/channels/pairing.mjs +363 -0
- package/lib/channels/pairing.test.mjs +270 -0
- package/lib/channels/registry.mjs +164 -0
- package/lib/channels/slack/adapter.mjs +317 -0
- package/lib/channels/slack-adapter.test.mjs +212 -0
- package/lib/channels/sms/adapter.mjs +43 -0
- package/lib/channels/telegram/adapter.mjs +432 -0
- package/lib/channels/telegram-adapter.test.mjs +306 -0
- package/lib/channels/voice/adapter.mjs +301 -0
- package/lib/channels/voice/adapter.test.mjs +278 -0
- package/lib/channels/whatsapp/adapter-baileys.mjs +587 -0
- package/lib/channels/whatsapp/adapter-baileys.test.mjs +359 -0
- package/lib/channels/whatsapp/adapter-twilio.mjs +65 -0
- package/lib/channels/whatsapp/baileys-typing.test.mjs +154 -0
- package/lib/charter.mjs +256 -0
- package/lib/charter.test.mjs +89 -0
- package/lib/claude-bin.mjs +134 -0
- package/lib/claude-bin.test.mjs +75 -0
- package/lib/collective/capture.mjs +185 -0
- package/lib/collective/capture.test.mjs +121 -0
- package/lib/collective/cards.mjs +201 -0
- package/lib/collective/cards.test.mjs +114 -0
- package/lib/collective/config.mjs +186 -0
- package/lib/collective/config.test.mjs +123 -0
- package/lib/collective/global-config.mjs +113 -0
- package/lib/collective/global-config.test.mjs +75 -0
- package/lib/collective/presence.mjs +201 -0
- package/lib/collective/presence.test.mjs +95 -0
- package/lib/collective/recall.mjs +215 -0
- package/lib/collective/recall.test.mjs +116 -0
- package/lib/comms/send-gate.mjs +554 -0
- package/lib/comms/send-gate.test.mjs +577 -0
- package/lib/comms.mjs +67 -0
- package/lib/comms.test.mjs +41 -0
- package/lib/diagnostics/alerts.mjs +424 -0
- package/lib/diagnostics/alerts.test.mjs +318 -0
- package/lib/diagnostics/backup-freshness.mjs +188 -0
- package/lib/diagnostics/backup-freshness.test.mjs +185 -0
- package/lib/diagnostics/counters.mjs +269 -0
- package/lib/diagnostics/counters.test.mjs +206 -0
- package/lib/diagnostics/events.mjs +188 -0
- package/lib/diagnostics/events.test.mjs +290 -0
- package/lib/diagnostics/otel.mjs +237 -0
- package/lib/diagnostics/otel.test.mjs +196 -0
- package/lib/diagnostics/trace.mjs +216 -0
- package/lib/diagnostics/trace.test.mjs +251 -0
- package/lib/env-compat.mjs +74 -0
- package/lib/env-compat.test.mjs +104 -0
- package/lib/feature-init.mjs +331 -0
- package/lib/fs-atomic.mjs +112 -0
- package/lib/fs-atomic.test.mjs +72 -0
- package/lib/fs-ownership.mjs +111 -0
- package/lib/fs-ownership.test.mjs +158 -0
- package/lib/hooks/bus.mjs +347 -0
- package/lib/hooks/bus.test.mjs +387 -0
- package/lib/index.js +16 -0
- package/lib/learning/config.mjs +106 -0
- package/lib/learning/config.test.mjs +75 -0
- package/lib/learning/counters.mjs +156 -0
- package/lib/learning/counters.test.mjs +69 -0
- package/lib/learning/curator-consolidate.test.mjs +238 -0
- package/lib/learning/curator.mjs +453 -0
- package/lib/learning/curator.test.mjs +106 -0
- package/lib/learning/log.mjs +40 -0
- package/lib/learning/reflect.mjs +534 -0
- package/lib/learning/reflect.test.mjs +0 -0
- package/lib/learning/session-index.mjs +352 -0
- package/lib/learning/session-index.test.mjs +125 -0
- package/lib/learning/skill-writer.mjs +474 -0
- package/lib/learning/skill-writer.test.mjs +210 -0
- package/lib/mcp/server.mjs +328 -0
- package/lib/mcp/server.test.mjs +400 -0
- package/lib/model-router/auth-profiles.mjs +758 -0
- package/lib/model-router/auth-profiles.test.mjs +580 -0
- package/lib/model-router/catalog/anthropic.yaml +153 -0
- package/lib/model-router/catalog/deepseek.yaml +86 -0
- package/lib/model-router/catalog/moonshot.yaml +81 -0
- package/lib/model-router/catalog/qwen.yaml +114 -0
- package/lib/model-router/catalog.mjs +925 -0
- package/lib/model-router/catalog.test.mjs +385 -0
- package/lib/model-router/economics.mjs +564 -0
- package/lib/model-router/economics.test.mjs +344 -0
- package/lib/model-router/failover.mjs +298 -0
- package/lib/model-router/failover.test.mjs +439 -0
- package/lib/model-router/health.mjs +453 -0
- package/lib/model-router/health.test.mjs +338 -0
- package/lib/model-router/integration-coverage.test.mjs +829 -0
- package/lib/model-router/integration.test.mjs +564 -0
- package/lib/model-router/ledger.mjs +402 -0
- package/lib/model-router/ledger.test.mjs +382 -0
- package/lib/model-router/llm-task.mjs +515 -0
- package/lib/model-router/llm-task.test.mjs +392 -0
- package/lib/model-router/org-credentials.mjs +260 -0
- package/lib/model-router/org-credentials.test.mjs +265 -0
- package/lib/model-router/pricing-refresh.mjs +463 -0
- package/lib/model-router/pricing-refresh.test.mjs +286 -0
- package/lib/model-router/reconcile.mjs +429 -0
- package/lib/model-router/reconcile.test.mjs +316 -0
- package/lib/model-router/repair.mjs +471 -0
- package/lib/model-router/repair.test.mjs +180 -0
- package/lib/model-router/resolve.mjs +1206 -0
- package/lib/model-router/spawn.mjs +497 -0
- package/lib/model-router/spawn.test.mjs +425 -0
- package/lib/model-router/taxonomy.mjs +893 -0
- package/lib/model-router/taxonomy.test.mjs +410 -0
- package/lib/model-router.mjs +677 -0
- package/lib/model-router.test.mjs +907 -0
- package/lib/org/activity.mjs +211 -0
- package/lib/org/activity.test.mjs +134 -0
- package/lib/org/approvals.mjs +448 -0
- package/lib/org/approvals.test.mjs +216 -0
- package/lib/org/awareness.mjs +222 -0
- package/lib/org/awareness.test.mjs +159 -0
- package/lib/org/board.mjs +229 -0
- package/lib/org/board.test.mjs +177 -0
- package/lib/org/bootstrap-context.mjs +169 -0
- package/lib/org/bootstrap-context.test.mjs +153 -0
- package/lib/org/client.mjs +1628 -0
- package/lib/org/client.test.mjs +1107 -0
- package/lib/org/cohort-client.mjs +67 -0
- package/lib/org/cohort-client.test.mjs +126 -0
- package/lib/org/cost-sync.mjs +227 -0
- package/lib/org/cost-sync.test.mjs +153 -0
- package/lib/org/doctor.mjs +212 -0
- package/lib/org/doctor.test.mjs +212 -0
- package/lib/org/handoff.mjs +293 -0
- package/lib/org/handoff.test.mjs +269 -0
- package/lib/org/integration-tools.mjs +182 -0
- package/lib/org/integration-tools.test.mjs +160 -0
- package/lib/org/keys.mjs +131 -0
- package/lib/org/keys.test.mjs +92 -0
- package/lib/org/knowledge.mjs +463 -0
- package/lib/org/knowledge.test.mjs +319 -0
- package/lib/org/leases.mjs +335 -0
- package/lib/org/leases.test.mjs +235 -0
- package/lib/org/mesh-integration.test.mjs +127 -0
- package/lib/org/mesh.mjs +459 -0
- package/lib/org/mesh.test.mjs +345 -0
- package/lib/org/messaging.mjs +503 -0
- package/lib/org/messaging.test.mjs +238 -0
- package/lib/org/policy.mjs +345 -0
- package/lib/org/policy.test.mjs +237 -0
- package/lib/org/protocol.checksum +1 -0
- package/lib/org/protocol.checksum.test.mjs +90 -0
- package/lib/org/protocol.mjs +967 -0
- package/lib/org/protocol.test.mjs +264 -0
- package/lib/org/registry.mjs +194 -0
- package/lib/org/registry.test.mjs +100 -0
- package/lib/org/tool-surface-integration.test.mjs +120 -0
- package/lib/org/tool-surface.mjs +2535 -0
- package/lib/org/tool-surface.test.mjs +589 -0
- package/lib/org/ui-parity.mjs +3236 -0
- package/lib/org/ui-parity.test.mjs +348 -0
- package/lib/org/verify.mjs +176 -0
- package/lib/org/verify.test.mjs +194 -0
- package/lib/rag/embed.mjs +188 -0
- package/lib/rag/indexer.mjs +425 -0
- package/lib/rag/rag.test.mjs +505 -0
- package/lib/rag/search.mjs +475 -0
- package/lib/rate-guard.mjs +246 -0
- package/lib/rate-guard.test.mjs +201 -0
- package/lib/render.mjs +112 -0
- package/lib/render.test.mjs +68 -0
- package/lib/resource-governor.mjs +297 -0
- package/lib/resource-governor.test.mjs +262 -0
- package/lib/scheduling/dynamic-jobs.mjs +675 -0
- package/lib/scheduling/dynamic-jobs.test.mjs +344 -0
- package/lib/scheduling/jitter.mjs +0 -0
- package/lib/scheduling/jitter.test.mjs +140 -0
- package/lib/secrets/broker.mjs +315 -0
- package/lib/secrets/broker.test.mjs +280 -0
- package/lib/secrets/providers.mjs +461 -0
- package/lib/secrets/providers.test.mjs +274 -0
- package/lib/security/audit-engine.mjs +684 -0
- package/lib/security/audit-engine.test.mjs +389 -0
- package/lib/security/coerce-args.mjs +552 -0
- package/lib/security/coerce-args.test.mjs +281 -0
- package/lib/security/dangerous-tools.mjs +97 -0
- package/lib/security/dangerous-tools.test.mjs +68 -0
- package/lib/security/external-content.mjs +145 -0
- package/lib/security/external-content.test.mjs +67 -0
- package/lib/security/redact.mjs +592 -0
- package/lib/security/redact.test.mjs +441 -0
- package/lib/security/secret-equal.mjs +73 -0
- package/lib/security/secret-equal.test.mjs +55 -0
- package/lib/session-permissions.mjs +101 -0
- package/lib/session-permissions.test.mjs +100 -0
- package/lib/setup/claude-probe.mjs +74 -0
- package/lib/setup/completeness.mjs +175 -0
- package/lib/setup/completeness.test.mjs +110 -0
- package/lib/setup/context-pack.mjs +173 -0
- package/lib/setup/context-pack.test.mjs +89 -0
- package/lib/setup/enrich.mjs +277 -0
- package/lib/setup/enrich.test.mjs +115 -0
- package/lib/setup/enroll-from-cohort.mjs +441 -0
- package/lib/setup/enroll-from-cohort.test.mjs +233 -0
- package/lib/setup/integration.test.mjs +162 -0
- package/lib/setup/io.mjs +360 -0
- package/lib/setup/io.test.mjs +77 -0
- package/lib/setup/run-generator.mjs +81 -0
- package/lib/setup/runner.mjs +244 -0
- package/lib/setup/runner.test.mjs +132 -0
- package/lib/setup/sections/comms.mjs +173 -0
- package/lib/setup/sections/company.mjs +120 -0
- package/lib/setup/sections/enrich.mjs +138 -0
- package/lib/setup/sections/identity.mjs +182 -0
- package/lib/setup/sections/identity.test.mjs +140 -0
- package/lib/setup/sections/learning.mjs +153 -0
- package/lib/setup/sections/learning.test.mjs +81 -0
- package/lib/setup/sections/messaging.mjs +219 -0
- package/lib/setup/sections/messaging.test.mjs +127 -0
- package/lib/setup/sections/model.mjs +102 -0
- package/lib/setup/sections/operating-model.mjs +78 -0
- package/lib/setup/sections/org.mjs +475 -0
- package/lib/setup/sections/org.test.mjs +313 -0
- package/lib/setup/sections/orgmail.mjs +173 -0
- package/lib/setup/sections/orgmail.test.mjs +118 -0
- package/lib/setup/sections/recovery.mjs +159 -0
- package/lib/setup/sections/recovery.test.mjs +98 -0
- package/lib/setup/sections/tools.mjs +132 -0
- package/lib/setup/sections/verify.mjs +97 -0
- package/lib/setup/sot.mjs +205 -0
- package/lib/setup/sot.test.mjs +81 -0
- package/lib/setup/state.mjs +151 -0
- package/lib/setup/state.test.mjs +92 -0
- package/lib/singleton.js +229 -0
- package/lib/singleton.test.mjs +135 -0
- package/lib/telemetry/alerts.mjs +216 -0
- package/lib/telemetry/alerts.test.mjs +109 -0
- package/lib/telemetry/collect.mjs +512 -0
- package/lib/telemetry/collect.test.mjs +202 -0
- package/lib/tool-definitions-integration.test.mjs +83 -0
- package/lib/tool-definitions.js +738 -0
- package/lib/tool-definitions.test.mjs +437 -0
- package/lib/util/fetch-timeout.mjs +136 -0
- package/lib/util/fetch-timeout.test.mjs +202 -0
- package/lib/util/reconnect.mjs +343 -0
- package/lib/util/reconnect.test.mjs +369 -0
- package/lib/util/unhandled.mjs +205 -0
- package/lib/util/unhandled.test.mjs +216 -0
- package/lib/voice/context-loader.mjs +466 -0
- package/lib/voice/index.mjs +100 -0
- package/lib/voice/openai-realtime.mjs +510 -0
- package/lib/voice/outbound.mjs +542 -0
- package/lib/voice/outbound.test.mjs +69 -0
- package/lib/voice/post-call-brief.mjs +428 -0
- package/lib/voice/provider.mjs +52 -0
- package/lib/voice/session-rotation.mjs +257 -0
- package/lib/voice/stt.mjs +161 -0
- package/lib/voice/stt.test.mjs +226 -0
- package/lib/voice/tool-bridge.mjs +370 -0
- package/lib/voice/tts.mjs +104 -0
- package/lib/voice/twilio-sip-bridge.mjs +288 -0
- package/lib/voice/voice.test.mjs +990 -0
- package/mcp/README.md +80 -0
- package/package.json +151 -0
- package/plugins/maestro-skills/plugin.json +139 -0
- package/plugins/maestro-skills/skills/agents-observe.md +110 -0
- package/plugins/maestro-skills/skills/board-deck.md +68 -0
- package/plugins/maestro-skills/skills/books-close.md +77 -0
- package/plugins/maestro-skills/skills/brand-steward.md +121 -0
- package/plugins/maestro-skills/skills/calendar-plan.md +57 -0
- package/plugins/maestro-skills/skills/call-working-sessions.md +124 -0
- package/plugins/maestro-skills/skills/ccxray-diagnostics.md +91 -0
- package/plugins/maestro-skills/skills/claude-pace.md +61 -0
- package/plugins/maestro-skills/skills/code-review-graph.md +99 -0
- package/plugins/maestro-skills/skills/crm-pipeline.md +65 -0
- package/plugins/maestro-skills/skills/decision-brief.md +89 -0
- package/plugins/maestro-skills/skills/directory-hygiene.md +125 -0
- package/plugins/maestro-skills/skills/draft-comms.md +84 -0
- package/plugins/maestro-skills/skills/evening-wrap.md +53 -0
- package/plugins/maestro-skills/skills/files-find.md +65 -0
- package/plugins/maestro-skills/skills/generative-ui.md +228 -0
- package/plugins/maestro-skills/skills/hiring-triage.md +74 -0
- package/plugins/maestro-skills/skills/inbox-triage.md +61 -0
- package/plugins/maestro-skills/skills/mail-triage.md +86 -0
- package/plugins/maestro-skills/skills/morning-brief.md +54 -0
- package/plugins/maestro-skills/skills/native-artifacts.md +157 -0
- package/plugins/maestro-skills/skills/org-board.md +133 -0
- package/plugins/maestro-skills/skills/org-credential.md +68 -0
- package/plugins/maestro-skills/skills/org-recall.md +81 -0
- package/plugins/maestro-skills/skills/pipeline-review.md +76 -0
- package/plugins/maestro-skills/skills/regulatory-status.md +81 -0
- package/plugins/maestro-skills/skills/router-why.md +78 -0
- package/plugins/maestro-skills/skills/schedule-meeting.md +91 -0
- package/plugins/maestro-skills/skills/session-search.md +71 -0
- package/plugins/maestro-skills/skills/set-reminder.md +93 -0
- package/plugins/maestro-skills/skills/slack-followup.md +64 -0
- package/plugins/maestro-skills/skills/team-activity.md +86 -0
- package/plugins/maestro-skills/skills/weekly-memo.md +70 -0
- package/policies/action-classification.yaml +114 -0
- package/policies/ai-disclosure.yaml +294 -0
- package/policies/communication-style.md +139 -0
- package/policies/information-barriers.yaml +118 -0
- package/policies/prompt-injection-defence.yaml +138 -0
- package/public/assets/icon-dark.png +0 -0
- package/public/assets/icon-dark.svg +9 -0
- package/public/assets/icon-light.svg +9 -0
- package/public/assets/logo-dark.svg +15 -0
- package/public/assets/logo-light.svg +15 -0
- package/scaffold/.mcp.json +7 -0
- package/scaffold/CLAUDE.md +368 -0
- package/scaffold/config/agent.json +55 -0
- package/scaffold/config/agent.ts +76 -0
- package/scaffold/config/agent.ts.example +89 -0
- package/scaffold/config/alerts.yaml +23 -0
- package/scaffold/config/allowlist.yaml.example +25 -0
- package/scaffold/config/caller-id-map.yaml +46 -0
- package/scaffold/config/collective.yaml +49 -0
- package/scaffold/config/company.json +20 -0
- package/scaffold/config/known-agents.json +6 -0
- package/scaffold/config/learning.yaml +55 -0
- package/scaffold/config/model-routing.yaml.example +104 -0
- package/scaffold/config/org.yaml +25 -0
- package/scaffold/config/orgmail.yaml.example +19 -0
- package/scaffold/config/recovery.yaml +72 -0
- package/scaffold/config/secrets.yaml +27 -0
- package/scaffold/config/slack.yaml.example +35 -0
- package/scaffold/config/telegram.yaml.example +38 -0
- package/scaffold/config/voice.yaml.example +89 -0
- package/scaffold/config/whatsapp.yaml.example +39 -0
- package/schedules/README.md +49 -0
- package/schedules/triggers/backlog-executor.md +102 -0
- package/schedules/triggers/brand-steward.md +72 -0
- package/schedules/triggers/daily-evening-wrap.md +159 -0
- package/schedules/triggers/daily-midday-sweep.md +58 -0
- package/schedules/triggers/daily-morning-brief.md +55 -0
- package/schedules/triggers/directory-hygiene.md +81 -0
- package/schedules/triggers/dynamic-jobs.md +40 -0
- package/schedules/triggers/inbox-processor.md +115 -0
- package/schedules/triggers/meeting-action-capture.md +60 -0
- package/schedules/triggers/meeting-prep.md +69 -0
- package/schedules/triggers/messaging-inbound.md +50 -0
- package/schedules/triggers/org-pulse.md +24 -0
- package/schedules/triggers/quarterly-self-assessment.md +54 -0
- package/schedules/triggers/weekly-engineering-health.md +37 -0
- package/schedules/triggers/weekly-execution.md +65 -0
- package/schedules/triggers/weekly-hiring.md +53 -0
- package/schedules/triggers/weekly-priorities.md +38 -0
- package/schedules/triggers/weekly-strategic-memo.md +124 -0
- package/scripts/archive-email.sh +55 -0
- package/scripts/cadence/cadence-status.mjs +36 -0
- package/scripts/cadence/enqueue-cadence-tick.mjs +174 -0
- package/scripts/cadence/enqueue-cadence-tick.test.mjs +187 -0
- package/scripts/cadence/launchd-cadence-wrapper.sh +85 -0
- package/scripts/cadence/launchd-cloud-relay-wrapper.sh +95 -0
- package/scripts/cadence/launchd-socket-mode-wrapper.sh +95 -0
- package/scripts/ci/check-docs-accuracy.mjs +493 -0
- package/scripts/ci/check-docs-accuracy.test.mjs +409 -0
- package/scripts/ci/check-exports-exist.mjs +140 -0
- package/scripts/ci/check-files-exist.mjs +107 -0
- package/scripts/ci/check-no-build-artifacts.mjs +111 -0
- package/scripts/ci/check-no-build-artifacts.test.mjs +71 -0
- package/scripts/ci/check-no-confidential.mjs +198 -0
- package/scripts/ci/check-no-conflict-markers.mjs +169 -0
- package/scripts/ci/check-no-residual-identity.mjs +163 -0
- package/scripts/ci/check-no-residual-identity.test.mjs +89 -0
- package/scripts/ci/check-tarball-fidelity.mjs +205 -0
- package/scripts/ci/check-unresolved-tokens.mjs +83 -0
- package/scripts/ci/check.mjs +109 -0
- package/scripts/ci/check.test.mjs +194 -0
- package/scripts/ci/run-coverage.mjs +82 -0
- package/scripts/ci/run-tests.mjs +71 -0
- package/scripts/cloud-relay/README.md +59 -0
- package/scripts/cloud-relay/index.mjs +233 -0
- package/scripts/cloud-relay/package.json +15 -0
- package/scripts/cloud-relay/railway.json +13 -0
- package/scripts/cloud-relay/voice/README.md +94 -0
- package/scripts/cloud-relay/voice/package-lock.json +39 -0
- package/scripts/cloud-relay/voice/package.json +16 -0
- package/scripts/cloud-relay/voice/railway.json +13 -0
- package/scripts/cloud-relay/voice/server.mjs +532 -0
- package/scripts/collective/hook-runner.mjs +211 -0
- package/scripts/collective/hook-runner.test.mjs +90 -0
- package/scripts/collective/org-pulse.mjs +72 -0
- package/scripts/collective/org-sync.mjs +61 -0
- package/scripts/collective/recall.mjs +45 -0
- package/scripts/collective/who.mjs +30 -0
- package/scripts/comms-monitor.sh +288 -0
- package/scripts/configure-whatsapp-sandbox.sh +201 -0
- package/scripts/continuous-monitor.sh +91 -0
- package/scripts/cost/fleet-digest.mjs +407 -0
- package/scripts/cost/fleet-digest.test.mjs +207 -0
- package/scripts/cost/track-claude-usage.mjs +169 -0
- package/scripts/daemon/agent-daemon.mjs +989 -0
- package/scripts/daemon/agent-daemon.test.mjs +525 -0
- package/scripts/daemon/cadence-consumer-governance.test.mjs +220 -0
- package/scripts/daemon/cadence-consumer.mjs +1080 -0
- package/scripts/daemon/cadence-consumer.test.mjs +770 -0
- package/scripts/daemon/cadence-handlers.mjs +1121 -0
- package/scripts/daemon/cadence-handlers.test.mjs +617 -0
- package/scripts/daemon/classifier.mjs +704 -0
- package/scripts/daemon/classifier.test.mjs +238 -0
- package/scripts/daemon/classify-kind.mjs +54 -0
- package/scripts/daemon/classify-kind.test.mjs +40 -0
- package/scripts/daemon/context-compiler.mjs +605 -0
- package/scripts/daemon/context-compiler.test.mjs +300 -0
- package/scripts/daemon/dispatcher-cooldown.test.mjs +122 -0
- package/scripts/daemon/dispatcher-governance.test.mjs +886 -0
- package/scripts/daemon/dispatcher.mjs +1516 -0
- package/scripts/daemon/health.mjs +72 -0
- package/scripts/daemon/inbox-deferral.mjs +210 -0
- package/scripts/daemon/inbox-deferral.test.mjs +242 -0
- package/scripts/daemon/integration.test.mjs +149 -0
- package/scripts/daemon/launchd-wrapper-generic.sh +96 -0
- package/scripts/daemon/launchd-wrapper-slack-events.sh +37 -0
- package/scripts/daemon/launchd-wrapper.sh +91 -0
- package/scripts/daemon/lib/session-router.mjs +274 -0
- package/scripts/daemon/lib/session-router.test.mjs +295 -0
- package/scripts/daemon/maestro-daemon.mjs +275 -0
- package/scripts/daemon/prompt-builder.mjs +685 -0
- package/scripts/daemon/prompt-builder.test.mjs +213 -0
- package/scripts/daemon/responder.mjs +854 -0
- package/scripts/daemon/session-lock.mjs +721 -0
- package/scripts/daemon/session-lock.test.mjs +252 -0
- package/scripts/daemon/session-outcomes.mjs +640 -0
- package/scripts/daemon/session-outcomes.test.mjs +533 -0
- package/scripts/daemon/typing-registry.mjs +90 -0
- package/scripts/daemon/typing-registry.test.mjs +77 -0
- package/scripts/daemon/voice-webhook-server.mjs +804 -0
- package/scripts/decisions/capture-decision.mjs +116 -0
- package/scripts/disclosure_assessment.py +873 -0
- package/scripts/disclosure_boundaries.py +562 -0
- package/scripts/email-signature-principal.html +52 -0
- package/scripts/email-signature.html +60 -0
- package/scripts/email_quote_thread.py +167 -0
- package/scripts/email_thread_dedup.py +362 -0
- package/scripts/emergency-stop.sh +81 -0
- package/scripts/healthcheck.sh +116 -0
- package/scripts/hooks/block-mcp-cohort-send.sh +15 -0
- package/scripts/hooks/block-mcp-slack-send.sh +7 -0
- package/scripts/hooks/post-action-log.sh +126 -0
- package/scripts/hooks/pre-send-audit.sh +174 -0
- package/scripts/hooks/pre-send-audit.test.mjs +215 -0
- package/scripts/hooks/session-end-log.sh +27 -0
- package/scripts/hooks/session-start-banner.sh +115 -0
- package/scripts/huddle/audio-bridge.mjs +664 -0
- package/scripts/huddle/boot-slack-cdp.sh +102 -0
- package/scripts/huddle/huddle-controller.mjs +942 -0
- package/scripts/huddle/huddle-server.mjs +1229 -0
- package/scripts/huddle/launch-slack.sh +232 -0
- package/scripts/huddle/openai-realtime-bridge.mjs +462 -0
- package/scripts/huddle/package-lock.json +62 -0
- package/scripts/huddle/package.json +22 -0
- package/scripts/huddle/setup-audio.sh +239 -0
- package/scripts/huddle/start-call.mjs +318 -0
- package/scripts/huddle/test-pipeline.mjs +263 -0
- package/scripts/learning/consolidate-skills.mjs +72 -0
- package/scripts/learning/session-search.mjs +125 -0
- package/scripts/llm_email_dedup.py +442 -0
- package/scripts/local-triggers/generate-plists.sh +432 -0
- package/scripts/local-triggers/generate-plists.test.mjs +413 -0
- package/scripts/local-triggers/install-all.sh +49 -0
- package/scripts/local-triggers/plists/.gitkeep +0 -0
- package/scripts/local-triggers/run-trigger.sh +63 -0
- package/scripts/local-triggers/templates/rag-reindex.plist.template +47 -0
- package/scripts/local-triggers/templates/voice-relay-poller.plist.template +54 -0
- package/scripts/local-triggers/templates/voice-tunnel.plist.template +55 -0
- package/scripts/local-triggers/templates/voice-webhook.plist.template +51 -0
- package/scripts/maintenance/backup-to-cloud.sh +124 -0
- package/scripts/maintenance/health-check.sh +377 -0
- package/scripts/media-generation/README.md +105 -0
- package/scripts/media-generation/gemini-image-client.mjs +173 -0
- package/scripts/media-generation/generate-assets.mjs +289 -0
- package/scripts/media-generation/veo-video-client.mjs +219 -0
- package/scripts/org/send-orgmail.mjs +227 -0
- package/scripts/outbound-dedup-cleanup.sh +43 -0
- package/scripts/outbound-dedup.sh +477 -0
- package/scripts/outbound_dedup.py +115 -0
- package/scripts/parse-voice-transcript.mjs +481 -0
- package/scripts/pdf-generation/README.md +63 -0
- package/scripts/pdf-generation/build-document.mjs +247 -0
- package/scripts/pdf-generation/templates/board-pack.latex +136 -0
- package/scripts/pdf-generation/templates/corporate-letter.latex +126 -0
- package/scripts/pdf-generation/templates/memo.latex +114 -0
- package/scripts/poll-slack-events.sh +35 -0
- package/scripts/poller/calendar-poller.mjs +12 -0
- package/scripts/poller/gmail-poller.mjs +192 -0
- package/scripts/poller/imap-client.mjs +289 -0
- package/scripts/poller/inbox-scan-poller.mjs +156 -0
- package/scripts/poller/inbox-scan-poller.test.mjs +231 -0
- package/scripts/poller/index.mjs +73 -0
- package/scripts/poller/intra-session-check.mjs +285 -0
- package/scripts/poller/lib/cloud-relay-dedup.mjs +88 -0
- package/scripts/poller/lib/cloud-relay-dedup.test.mjs +133 -0
- package/scripts/poller/lib/slash-command-handlers.mjs +177 -0
- package/scripts/poller/secondary-gmail-poller.mjs +132 -0
- package/scripts/poller/slack-cloud-relay-client.mjs +368 -0
- package/scripts/poller/slack-poller.mjs +854 -0
- package/scripts/poller/slack-socket-mode.mjs +917 -0
- package/scripts/poller/slack-socket-mode.test.mjs +753 -0
- package/scripts/poller/trigger.mjs +75 -0
- package/scripts/poller/utils.mjs +371 -0
- package/scripts/poller/voice-cloud-relay-client.mjs +179 -0
- package/scripts/poller/voice-poller.mjs +236 -0
- package/scripts/poller-launchd/install.sh +66 -0
- package/scripts/poller-launchd/poller.plist.template +40 -0
- package/scripts/poller-launchd/whatsapp-handler.plist.template +39 -0
- package/scripts/post-interaction-indexer.py +1598 -0
- package/scripts/pre-draft-context.py +994 -0
- package/scripts/pre_draft_lookup.py +258 -0
- package/scripts/rag/build-index.mjs +47 -0
- package/scripts/rag/ingest.mjs +111 -0
- package/scripts/rag/search.mjs +119 -0
- package/scripts/rag-indexer.py +629 -0
- package/scripts/restore-from-backup.sh +248 -0
- package/scripts/restore-from-backup.test.mjs +178 -0
- package/scripts/resume-operations.sh +80 -0
- package/scripts/search-secondary-inbox.py +181 -0
- package/scripts/secondary-inbox-poller.py +437 -0
- package/scripts/self-optimization/compute-metrics.py +398 -0
- package/scripts/send-email-as-principal.py +369 -0
- package/scripts/send-email-threaded.py +392 -0
- package/scripts/send-email-with-attachment.py +377 -0
- package/scripts/send-email.sh +131 -0
- package/scripts/send-sms.sh +175 -0
- package/scripts/send-whatsapp.sh +292 -0
- package/scripts/session-start.sh +106 -0
- package/scripts/setup/boot-claude-session.sh +94 -0
- package/scripts/setup/configure-macos.sh +674 -0
- package/scripts/setup/configure-twilio-sip-trunk.mjs +207 -0
- package/scripts/setup/configure-voice-tunnel.mjs +182 -0
- package/scripts/setup/generate-agent-env.mjs +92 -0
- package/scripts/setup/generate-agent-package-json.mjs +222 -0
- package/scripts/setup/generate-agent-package-json.test.mjs +143 -0
- package/scripts/setup/generate-autonomy.mjs +60 -0
- package/scripts/setup/generate-backlog.mjs +101 -0
- package/scripts/setup/generate-cadences.mjs +92 -0
- package/scripts/setup/generate-capability.mjs +162 -0
- package/scripts/setup/generate-charter.mjs +76 -0
- package/scripts/setup/generate-comms.mjs +59 -0
- package/scripts/setup/generate-company.mjs +92 -0
- package/scripts/setup/init-agent.sh +547 -0
- package/scripts/setup/init-agent.test.mjs +151 -0
- package/scripts/setup/init-archetype.mjs +74 -0
- package/scripts/setup/init-backup.mjs +54 -0
- package/scripts/setup/init-cadence-bus.mjs +60 -0
- package/scripts/setup/init-channel-bus.mjs +46 -0
- package/scripts/setup/init-cost-tracking.mjs +45 -0
- package/scripts/setup/init-decision-capture.mjs +66 -0
- package/scripts/setup/init-known-agents.mjs +57 -0
- package/scripts/setup/init-learning.mjs +70 -0
- package/scripts/setup/init-memory-executive.mjs +45 -0
- package/scripts/setup/init-model-router.mjs +124 -0
- package/scripts/setup/init-rag.mjs +174 -0
- package/scripts/setup/init-session-router.mjs +38 -0
- package/scripts/setup/init-slack-socket-mode.mjs +260 -0
- package/scripts/setup/init-telegram.mjs +165 -0
- package/scripts/setup/init-voice-realtime.mjs +204 -0
- package/scripts/setup/init-whatsapp-baileys.mjs +77 -0
- package/scripts/setup/install-dev-tools.sh +150 -0
- package/scripts/setup/lib/install-plist.mjs +95 -0
- package/scripts/setup/migrate-agent-to-sot.mjs +192 -0
- package/scripts/setup/render-environment-yaml.mjs +133 -0
- package/scripts/slack-events-ctl.sh +177 -0
- package/scripts/slack-events-server.mjs +1045 -0
- package/scripts/slack-react.mjs +89 -0
- package/scripts/slack-responded.sh +232 -0
- package/scripts/slack-send.sh +287 -0
- package/scripts/slack-typing.mjs +196 -0
- package/scripts/slack-upload-v2.py +95 -0
- package/scripts/sms-handler.mjs +450 -0
- package/scripts/spawn-session.sh +120 -0
- package/scripts/sync-protocol.mjs +217 -0
- package/scripts/system-verify.sh +184 -0
- package/scripts/test-email-thread-dedup.py +239 -0
- package/scripts/test-information-barriers.py +484 -0
- package/scripts/test-llm-email-dedup.py +251 -0
- package/scripts/test-pre-draft-integration.py +203 -0
- package/scripts/test-rag-phase2.sh +442 -0
- package/scripts/test-rag-search.sh +251 -0
- package/scripts/test-voice-parser.mjs +316 -0
- package/scripts/user-context-search.py +659 -0
- package/scripts/validate_outbound.py +1504 -0
- package/scripts/watchdog/ai.maestro.memory-watchdog.plist +41 -0
- package/scripts/watchdog/force-reboot.sh +157 -0
- package/scripts/watchdog/memory-watchdog.sh +473 -0
- package/scripts/whatsapp-handler.mjs +538 -0
- package/teams/desktop-operations.yaml +34 -0
- package/teams/executive-office.yaml +27 -0
- package/teams/legal-and-regulatory.yaml +24 -0
- package/teams/platform-and-engineering.yaml +23 -0
- package/teams/strategy-and-growth.yaml +29 -0
- package/workflows/continuous/backlog-executor.yaml +141 -0
- package/workflows/continuous/inbound-monitor.yaml +168 -0
- package/workflows/daily/applicant-triage.yaml +197 -0
- package/workflows/daily/comms-triage.yaml +80 -0
- package/workflows/daily/evening-wrap.yaml +105 -0
- package/workflows/daily/morning-brief.yaml +164 -0
- package/workflows/daily/slack-followup-sweep.yaml +87 -0
- package/workflows/event-driven/README.md +50 -0
- package/workflows/event-driven/agent-failure-investigation.yaml +137 -0
- package/workflows/event-driven/pr-review.yaml +107 -0
- package/workflows/monthly/board-readiness.yaml +76 -0
- package/workflows/quarterly/strategic-scenario-analysis.yaml +85 -0
- package/workflows/session-protocol.md +171 -0
- package/workflows/weekly/engineering-health.yaml +154 -0
- package/workflows/weekly/hiring-review.yaml +169 -0
- package/workflows/weekly/rollup-pipeline-review.yaml +76 -0
- package/workflows/weekly/strategic-memo.yaml +79 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-router/llm-task.test.mjs — the governed cheap one-shot path.
|
|
3
|
+
*
|
|
4
|
+
* Drives llmTask with an injected fetchImpl (no network) over a hermetic catalog,
|
|
5
|
+
* asserting the component-brief contract:
|
|
6
|
+
* - routes to the CHEAPEST direct-harness model meeting minGrade;
|
|
7
|
+
* - writes ONE ledger row (real writeLedgerRow into a tmp dir);
|
|
8
|
+
* - tool-less by construction (no tools[] in the request body);
|
|
9
|
+
* - non-Anthropic payloads pass through the injected PII mask (§7.4);
|
|
10
|
+
* - HTTP/transport faults classify onto the taxonomy and fail-open (no throw);
|
|
11
|
+
* - the kill switch forces an Anthropic direct row;
|
|
12
|
+
* - missing credential / open breaker / grade floor are honored.
|
|
13
|
+
*
|
|
14
|
+
* House style: node:test, node:assert/strict, hermetic.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { test } from "node:test";
|
|
18
|
+
import assert from "node:assert/strict";
|
|
19
|
+
import { mkdtempSync, rmSync, existsSync, readFileSync, mkdirSync } from "node:fs";
|
|
20
|
+
import { tmpdir } from "node:os";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
import { llmTask, pickDirectModel, _internals } from "./llm-task.mjs";
|
|
24
|
+
import { loadCatalog } from "./catalog.mjs";
|
|
25
|
+
import { sumLedgerToday } from "./ledger.mjs";
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// A hermetic catalog: a cheap grade-B direct row, a cheaper grade-C row, and an
|
|
29
|
+
// anthropic direct row. We inject raw YAML via catalogText so no FS is touched.
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
|
|
32
|
+
const CHEAP_YAML = `
|
|
33
|
+
provider: cheapco
|
|
34
|
+
auth_env: CHEAPCO_API_KEY
|
|
35
|
+
endpoints:
|
|
36
|
+
openai: https://api.cheapco.test
|
|
37
|
+
models:
|
|
38
|
+
- id: cheap-b
|
|
39
|
+
status: available
|
|
40
|
+
context_tokens: 200000
|
|
41
|
+
cost: { input: 0.20, output: 0.40 }
|
|
42
|
+
cost_provenance: { source: official, fetched: "2026-06-10", volatile: false }
|
|
43
|
+
compat: { cache_control: none, max_tokens_field: max_tokens }
|
|
44
|
+
tool_reliability: B
|
|
45
|
+
harness: { session: false, direct: true, batch: false }
|
|
46
|
+
- id: cheap-c
|
|
47
|
+
status: available
|
|
48
|
+
context_tokens: 100000
|
|
49
|
+
cost: { input: 0.05, output: 0.10 }
|
|
50
|
+
cost_provenance: { source: official, fetched: "2026-06-10", volatile: false }
|
|
51
|
+
compat: { cache_control: none, max_tokens_field: max_tokens }
|
|
52
|
+
tool_reliability: C
|
|
53
|
+
harness: { session: false, direct: true, batch: false }
|
|
54
|
+
`;
|
|
55
|
+
|
|
56
|
+
const ANTH_YAML = `
|
|
57
|
+
provider: anthropic
|
|
58
|
+
auth_env: ANTHROPIC_API_KEY
|
|
59
|
+
endpoints:
|
|
60
|
+
anthropic: https://api.anthropic.com
|
|
61
|
+
models:
|
|
62
|
+
- id: claude-haiku-4-5
|
|
63
|
+
status: available
|
|
64
|
+
context_tokens: 200000
|
|
65
|
+
cost: { input: 1.0, output: 5.0, cache_read: 0.1, cache_write: 1.25 }
|
|
66
|
+
cost_provenance: { source: official, fetched: "2026-06-10", volatile: false }
|
|
67
|
+
compat: { cache_control: anthropic }
|
|
68
|
+
tool_reliability: A
|
|
69
|
+
harness: { session: true, direct: true, batch: true }
|
|
70
|
+
`;
|
|
71
|
+
|
|
72
|
+
function hermeticCatalog() {
|
|
73
|
+
const res = loadCatalog("/tmp/nope", {
|
|
74
|
+
catalogText: { cheapco: CHEAP_YAML, anthropic: ANTH_YAML },
|
|
75
|
+
});
|
|
76
|
+
assert.ok(res.byRef.size >= 3, "catalog parsed three rows");
|
|
77
|
+
return res;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** A config that permits cheapco for public data (otherwise deny-default). */
|
|
81
|
+
function publicConfig() {
|
|
82
|
+
return {
|
|
83
|
+
schema_version: 2,
|
|
84
|
+
backends: {
|
|
85
|
+
anthropic: { allowed_data_classes: ["public", "internal", "sensitive"] },
|
|
86
|
+
cheapco: { allowed_data_classes: ["public"] },
|
|
87
|
+
},
|
|
88
|
+
defaults: { data_class: "public" },
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** A fake OpenAI-compatible response. */
|
|
93
|
+
function openaiResponse(text, usage = {}) {
|
|
94
|
+
return {
|
|
95
|
+
ok: true,
|
|
96
|
+
status: 200,
|
|
97
|
+
async json() {
|
|
98
|
+
return {
|
|
99
|
+
choices: [{ message: { role: "assistant", content: text } }],
|
|
100
|
+
usage: { prompt_tokens: usage.in ?? 100, completion_tokens: usage.out ?? 20 },
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/** A fake anthropic-messages response. */
|
|
107
|
+
function anthropicResponse(text, usage = {}) {
|
|
108
|
+
return {
|
|
109
|
+
ok: true,
|
|
110
|
+
status: 200,
|
|
111
|
+
async json() {
|
|
112
|
+
return {
|
|
113
|
+
content: [{ type: "text", text }],
|
|
114
|
+
usage: { input_tokens: usage.in ?? 100, output_tokens: usage.out ?? 20 },
|
|
115
|
+
};
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const baseDeps = (over = {}) => ({
|
|
121
|
+
catalog: hermeticCatalog(),
|
|
122
|
+
config: publicConfig(),
|
|
123
|
+
env: { CHEAPCO_API_KEY: "cheap-key", ANTHROPIC_API_KEY: "anth-key" },
|
|
124
|
+
now: () => 1_700_000_000_000,
|
|
125
|
+
isOpen: () => ({ open: false, until: 0, reason: null, strikes: 0 }),
|
|
126
|
+
...over,
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// ---------------------------------------------------------------------------
|
|
130
|
+
// pickDirectModel
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
|
|
133
|
+
test("pickDirectModel returns the cheapest qualifying direct row for minGrade C", () => {
|
|
134
|
+
const catalog = hermeticCatalog();
|
|
135
|
+
const r = pickDirectModel(catalog, { data_class: "public" }, { ...baseDeps({ catalog }), minGrade: "C" });
|
|
136
|
+
assert.equal(r.row.ref, "cheapco/cheap-c", "cheapest (C) wins at minGrade C");
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("pickDirectModel skips below-grade rows for minGrade B", () => {
|
|
140
|
+
const catalog = hermeticCatalog();
|
|
141
|
+
const r = pickDirectModel(catalog, { data_class: "public" }, { ...baseDeps({ catalog }), minGrade: "B" });
|
|
142
|
+
assert.equal(r.row.ref, "cheapco/cheap-b", "cheap-c is grade C, excluded; cheap-b wins");
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("pickDirectModel denies cheapco for non-public data_class (deny-default)", () => {
|
|
146
|
+
const catalog = hermeticCatalog();
|
|
147
|
+
const r = pickDirectModel(
|
|
148
|
+
catalog,
|
|
149
|
+
{ data_class: "sensitive" },
|
|
150
|
+
{ ...baseDeps({ catalog }), minGrade: "C" }
|
|
151
|
+
);
|
|
152
|
+
// cheapco rows denied → anthropic haiku is the only allowed direct row.
|
|
153
|
+
assert.equal(r.row.ref, "anthropic/claude-haiku-4-5");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test("pickDirectModel skips a row whose credential is absent", () => {
|
|
157
|
+
const catalog = hermeticCatalog();
|
|
158
|
+
const r = pickDirectModel(
|
|
159
|
+
catalog,
|
|
160
|
+
{ data_class: "public" },
|
|
161
|
+
{ ...baseDeps({ catalog, env: { ANTHROPIC_API_KEY: "anth-key" } }), minGrade: "C" }
|
|
162
|
+
);
|
|
163
|
+
// No CHEAPCO_API_KEY → cheapco skipped → anthropic haiku.
|
|
164
|
+
assert.equal(r.row.ref, "anthropic/claude-haiku-4-5");
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
test("pickDirectModel skips a row with an open breaker", () => {
|
|
168
|
+
const catalog = hermeticCatalog();
|
|
169
|
+
const r = pickDirectModel(catalog, { data_class: "public" }, {
|
|
170
|
+
...baseDeps({ catalog }),
|
|
171
|
+
minGrade: "C",
|
|
172
|
+
isOpen: (key) => ({ open: key.startsWith("cheapco"), until: 9e15, reason: "rate_limit", strikes: 1 }),
|
|
173
|
+
});
|
|
174
|
+
assert.equal(r.row.ref, "anthropic/claude-haiku-4-5", "cheapco breaker open → anthropic");
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// llmTask happy path
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
test("routes to the cheapest direct model, returns text + usage + estCostUSD, writes a ledger row", async () => {
|
|
182
|
+
const rows = [];
|
|
183
|
+
const out = await llmTask("classify this", {
|
|
184
|
+
...baseDeps(),
|
|
185
|
+
minGrade: "C",
|
|
186
|
+
request: { task_class: "classify.inbox", data_class: "public", agent: "alex" },
|
|
187
|
+
fetchImpl: async (url, init) => {
|
|
188
|
+
// tool-less invariant: no tools[] ever sent.
|
|
189
|
+
const body = JSON.parse(init.body);
|
|
190
|
+
assert.equal(body.tools, undefined, "no tools[] in the request");
|
|
191
|
+
assert.match(url, /cheapco/, "cheapest cheapco model selected");
|
|
192
|
+
return openaiResponse("spam", { in: 80, out: 4 });
|
|
193
|
+
},
|
|
194
|
+
writeLedger: (catalog, entry) => rows.push(entry),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
assert.equal(out.ok, true);
|
|
198
|
+
assert.equal(out.ref, "cheapco/cheap-c");
|
|
199
|
+
assert.equal(out.text, "spam");
|
|
200
|
+
assert.equal(out.usage.inputTokens, 80);
|
|
201
|
+
assert.equal(out.usage.outputTokens, 4);
|
|
202
|
+
assert.ok(out.estCostUSD != null);
|
|
203
|
+
// Exactly one ledger row, joined to the task + harness + exit reason.
|
|
204
|
+
assert.equal(rows.length, 1);
|
|
205
|
+
assert.equal(rows[0].ref, "cheapco/cheap-c");
|
|
206
|
+
assert.equal(rows[0].harness, "direct");
|
|
207
|
+
assert.equal(rows[0].task_class, "classify.inbox");
|
|
208
|
+
assert.equal(rows[0].exitReason, "ok");
|
|
209
|
+
assert.equal(rows[0].agent, "alex");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("writes a REAL ledger row to state/cost-tracking via writeLedgerRow", async () => {
|
|
213
|
+
const root = mkdtempSync(join(tmpdir(), "maestro-llmtask-"));
|
|
214
|
+
mkdirSync(join(root, "state", "cost-tracking"), { recursive: true });
|
|
215
|
+
try {
|
|
216
|
+
const out = await llmTask("hi", {
|
|
217
|
+
...baseDeps(),
|
|
218
|
+
minGrade: "C",
|
|
219
|
+
request: { task_class: "lookup.gmail", data_class: "public" },
|
|
220
|
+
ledgerDeps: { agentRoot: root, now: () => 1_700_000_000_000 },
|
|
221
|
+
fetchImpl: async () => openaiResponse("ok", { in: 50, out: 10 }),
|
|
222
|
+
});
|
|
223
|
+
assert.equal(out.ok, true);
|
|
224
|
+
const sum = sumLedgerToday({ agentRoot: root, now: () => 1_700_000_000_000 });
|
|
225
|
+
assert.equal(sum.sessions, 1, "one ledger row written to disk");
|
|
226
|
+
assert.ok(sum.spentUSD >= 0);
|
|
227
|
+
} finally {
|
|
228
|
+
rmSync(root, { recursive: true, force: true });
|
|
229
|
+
}
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
test("non-Anthropic payload passes through the injected PII mask (§7.4)", async () => {
|
|
233
|
+
let masked = false;
|
|
234
|
+
let sentBody = null;
|
|
235
|
+
const out = await llmTask("email me at a@b.com", {
|
|
236
|
+
...baseDeps(),
|
|
237
|
+
minGrade: "C",
|
|
238
|
+
request: { data_class: "public" },
|
|
239
|
+
maskInput: (s) => {
|
|
240
|
+
masked = true;
|
|
241
|
+
return s.replace(/[\w.]+@[\w.]+/g, "***");
|
|
242
|
+
},
|
|
243
|
+
fetchImpl: async (url, init) => {
|
|
244
|
+
sentBody = JSON.parse(init.body);
|
|
245
|
+
return openaiResponse("done");
|
|
246
|
+
},
|
|
247
|
+
writeLedger: () => {},
|
|
248
|
+
});
|
|
249
|
+
assert.equal(out.ok, true);
|
|
250
|
+
assert.equal(masked, true, "mask invoked for the cheapco (non-anthropic) route");
|
|
251
|
+
assert.match(sentBody.messages.at(-1).content, /\*\*\*/, "PII masked in the sent prompt");
|
|
252
|
+
assert.ok(!/a@b\.com/.test(sentBody.messages.at(-1).content));
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("kill switch forces an Anthropic direct row (mask NOT applied)", async () => {
|
|
256
|
+
let masked = false;
|
|
257
|
+
let url = null;
|
|
258
|
+
const out = await llmTask("anything", {
|
|
259
|
+
...baseDeps({ env: { CHEAPCO_API_KEY: "k", ANTHROPIC_API_KEY: "a", MAESTRO_ROUTER_FORCE_ANTHROPIC: "1" } }),
|
|
260
|
+
minGrade: "C",
|
|
261
|
+
request: { data_class: "public" },
|
|
262
|
+
maskInput: () => {
|
|
263
|
+
masked = true;
|
|
264
|
+
return "x";
|
|
265
|
+
},
|
|
266
|
+
fetchImpl: async (u) => {
|
|
267
|
+
url = u;
|
|
268
|
+
return anthropicResponse("hello");
|
|
269
|
+
},
|
|
270
|
+
writeLedger: () => {},
|
|
271
|
+
});
|
|
272
|
+
assert.equal(out.ok, true);
|
|
273
|
+
assert.equal(out.provider, "anthropic");
|
|
274
|
+
assert.match(url, /anthropic/);
|
|
275
|
+
assert.equal(masked, false, "anthropic route is not PII-masked");
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// ---------------------------------------------------------------------------
|
|
279
|
+
// fault classification (fail-open, never throws)
|
|
280
|
+
// ---------------------------------------------------------------------------
|
|
281
|
+
|
|
282
|
+
test("HTTP 429 → rate_limit reason, fail-open (no throw), ledger row written", async () => {
|
|
283
|
+
const rows = [];
|
|
284
|
+
const out = await llmTask("x", {
|
|
285
|
+
...baseDeps(),
|
|
286
|
+
minGrade: "C",
|
|
287
|
+
request: { data_class: "public" },
|
|
288
|
+
fetchImpl: async () => ({
|
|
289
|
+
ok: false,
|
|
290
|
+
status: 429,
|
|
291
|
+
async json() {
|
|
292
|
+
return { error: { message: "rate limit exceeded" } };
|
|
293
|
+
},
|
|
294
|
+
}),
|
|
295
|
+
writeLedger: (c, e) => rows.push(e),
|
|
296
|
+
});
|
|
297
|
+
assert.equal(out.ok, false);
|
|
298
|
+
assert.equal(out.reason, "rate_limit");
|
|
299
|
+
assert.equal(rows.length, 1);
|
|
300
|
+
assert.equal(rows[0].exitReason, "rate_limit");
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
test("transport throw → classified network, fail-open", async () => {
|
|
304
|
+
const out = await llmTask("x", {
|
|
305
|
+
...baseDeps(),
|
|
306
|
+
minGrade: "C",
|
|
307
|
+
request: { data_class: "public" },
|
|
308
|
+
fetchImpl: async () => {
|
|
309
|
+
const e = new Error("getaddrinfo ENOTFOUND api.cheapco.test");
|
|
310
|
+
e.code = "ENOTFOUND";
|
|
311
|
+
throw e;
|
|
312
|
+
},
|
|
313
|
+
writeLedger: () => {},
|
|
314
|
+
});
|
|
315
|
+
assert.equal(out.ok, false);
|
|
316
|
+
assert.equal(out.reason, "network");
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
test("2xx with empty content → empty_response soft-fail", async () => {
|
|
320
|
+
const out = await llmTask("x", {
|
|
321
|
+
...baseDeps(),
|
|
322
|
+
minGrade: "C",
|
|
323
|
+
request: { data_class: "public" },
|
|
324
|
+
fetchImpl: async () => openaiResponse(" "),
|
|
325
|
+
writeLedger: () => {},
|
|
326
|
+
});
|
|
327
|
+
assert.equal(out.ok, false);
|
|
328
|
+
assert.equal(out.reason, "empty_response");
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
test("no fetchImpl → no_fetch_impl, does not throw", async () => {
|
|
332
|
+
const out = await llmTask("x", { ...baseDeps(), request: { data_class: "public" } });
|
|
333
|
+
assert.equal(out.ok, false);
|
|
334
|
+
assert.equal(out.reason, "no_fetch_impl");
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
test("no qualifying candidate (no creds anywhere) → no_direct_candidate", async () => {
|
|
338
|
+
const out = await llmTask("x", {
|
|
339
|
+
...baseDeps({ env: {} }),
|
|
340
|
+
minGrade: "C",
|
|
341
|
+
request: { data_class: "public" },
|
|
342
|
+
fetchImpl: async () => openaiResponse("y"),
|
|
343
|
+
});
|
|
344
|
+
assert.equal(out.ok, false);
|
|
345
|
+
assert.ok(["missing_credential", "no_direct_candidate"].includes(out.reason));
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
test("opts.repair promotes a leaked tool call in the response text", async () => {
|
|
349
|
+
const out = await llmTask("call the tool", {
|
|
350
|
+
...baseDeps(),
|
|
351
|
+
minGrade: "C",
|
|
352
|
+
request: { data_class: "public" },
|
|
353
|
+
repair: true,
|
|
354
|
+
repairOpts: { idFor: (n) => `id_${n}` },
|
|
355
|
+
fetchImpl: async () => openaiResponse('```json\n{"name":"do","input":{"k":1}}\n```'),
|
|
356
|
+
writeLedger: () => {},
|
|
357
|
+
});
|
|
358
|
+
assert.equal(out.ok, true);
|
|
359
|
+
assert.equal(out.repaired, true);
|
|
360
|
+
assert.equal(out.toolCalls[0].name, "do");
|
|
361
|
+
assert.deepEqual(out.toolCalls[0].input, { k: 1 });
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test("anthropic wire sends x-api-key + anthropic-version, parses content[]", async () => {
|
|
365
|
+
let headers = null;
|
|
366
|
+
const out = await llmTask("x", {
|
|
367
|
+
...baseDeps(),
|
|
368
|
+
minGrade: "A", // forces the anthropic grade-A row
|
|
369
|
+
request: { data_class: "public" },
|
|
370
|
+
fetchImpl: async (url, init) => {
|
|
371
|
+
headers = init.headers;
|
|
372
|
+
assert.match(url, /\/v1\/messages$/);
|
|
373
|
+
return anthropicResponse("hi", { in: 10, out: 2 });
|
|
374
|
+
},
|
|
375
|
+
writeLedger: () => {},
|
|
376
|
+
});
|
|
377
|
+
assert.equal(out.ok, true);
|
|
378
|
+
assert.equal(out.provider, "anthropic");
|
|
379
|
+
assert.equal(headers["x-api-key"], "anth-key");
|
|
380
|
+
assert.equal(headers["anthropic-version"], "2023-06-01");
|
|
381
|
+
assert.equal(out.text, "hi");
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
test("_internals.wireFor + endpointFor pick the right wire per provider", () => {
|
|
385
|
+
const catalog = hermeticCatalog();
|
|
386
|
+
const cheap = catalog.byRef.get("cheapco/cheap-c");
|
|
387
|
+
const anth = catalog.byRef.get("anthropic/claude-haiku-4-5");
|
|
388
|
+
assert.equal(_internals.wireFor(cheap), "openai-completions");
|
|
389
|
+
assert.equal(_internals.wireFor(anth), "anthropic-messages");
|
|
390
|
+
assert.match(_internals.endpointFor(cheap, "openai-completions"), /cheapco.*chat\/completions/);
|
|
391
|
+
assert.match(_internals.endpointFor(anth, "anthropic-messages"), /\/v1\/messages$/);
|
|
392
|
+
});
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-router/org-credentials.mjs — make the router CONSUME org credentials.
|
|
3
|
+
*
|
|
4
|
+
* THE single-source-of-truth payoff. An org runs 50-100 agents on separate
|
|
5
|
+
* machines; provisioning a DeepSeek/Kimi/OpenAI key onto every one is
|
|
6
|
+
* unmanageable and a breach surface. Instead an admin sets each provider key
|
|
7
|
+
* ONCE in Cohort (credential.put); this module LEASES it short-lived,
|
|
8
|
+
* role-scoped, and audited (credential.lease) at spawn time — the org key never
|
|
9
|
+
* persists on the agent machine.
|
|
10
|
+
*
|
|
11
|
+
* resolveOrgCredentialEnv({catalog, cfg, roles, fetchImpl, now}) walks the
|
|
12
|
+
* router catalog's third-party providers; for each one whose `auth_env` is NOT
|
|
13
|
+
* already set in the local env AND which the org has configured, it leases the
|
|
14
|
+
* value from Cohort and maps provider → auth_env → value into an `envOverlay`.
|
|
15
|
+
* The daemon merges that overlay into the spawn child env BEFORE resolveChain
|
|
16
|
+
* (see the INTEGRATION POINT note below), so the router sees the leased key the
|
|
17
|
+
* same way it would a locally-provisioned one — and a genuinely missing key still
|
|
18
|
+
* surfaces as the router's existing `missing_credential`, never as a crash.
|
|
19
|
+
*
|
|
20
|
+
* DESIGN INVARIANTS:
|
|
21
|
+
* - LOCAL WINS. If `env[auth_env]` is already a non-empty string, we never
|
|
22
|
+
* lease that provider (the agent's own key beats the org broker).
|
|
23
|
+
* - FAIL-OPEN. Cohort unreachable / disabled / errors ⇒ we return whatever
|
|
24
|
+
* local env already has. We NEVER throw and NEVER block a spawn.
|
|
25
|
+
* - NEVER PERSIST. Leased plaintext lives ONLY in this process's in-memory
|
|
26
|
+
* cache (keyed by provider, expiring at the lease TTL) and in the returned
|
|
27
|
+
* overlay the caller merges into the child env. It is NEVER written to disk
|
|
28
|
+
* and NEVER logged.
|
|
29
|
+
* - CACHE. A successful lease is cached for its TTL; a second resolve within
|
|
30
|
+
* the TTL reuses the cached value WITHOUT a second lease call. Expiry
|
|
31
|
+
* triggers a re-lease.
|
|
32
|
+
*
|
|
33
|
+
* INTEGRATION POINT (one-line follow-up, OUT of this lane's files): the spawn
|
|
34
|
+
* site (scripts/daemon/* / lib/model-router/spawn.mjs) must, BEFORE calling
|
|
35
|
+
* resolveChain, do roughly:
|
|
36
|
+
* const { envOverlay } = await resolveOrgCredentialEnv({ catalog, cfg, roles });
|
|
37
|
+
* childEnv = { ...childEnv, ...envOverlay }; // leased keys, local-wins already applied
|
|
38
|
+
* resolveChain then reads childEnv[row.auth_env] exactly as today.
|
|
39
|
+
*
|
|
40
|
+
* Node builtins only. ESM. Pure-core + injectable IO (fetchImpl, env, now, a
|
|
41
|
+
* private cache) so tests are deterministic and never touch the network.
|
|
42
|
+
*
|
|
43
|
+
* @module lib/model-router/org-credentials
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
"use strict";
|
|
47
|
+
|
|
48
|
+
import { credentialLease, isEnabled, configFromAgent } from "../org/client.mjs";
|
|
49
|
+
|
|
50
|
+
/** Providers that ride the stock claude CLI / keychain OAuth — never brokered. */
|
|
51
|
+
const NEVER_BROKER = new Set(["anthropic"]);
|
|
52
|
+
|
|
53
|
+
/** Default lease TTL (ms) when the server's response omits one. 15 minutes. */
|
|
54
|
+
const DEFAULT_LEASE_TTL_MS = 15 * 60 * 1000;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Process-lifetime in-memory cache of leased credentials, keyed by provider.
|
|
58
|
+
* Value: { authEnv, value, expiresAtMs }. NEVER serialised to disk. A module
|
|
59
|
+
* singleton so successive spawns within a TTL share one lease; tests inject
|
|
60
|
+
* their own cache via opts.cache for isolation.
|
|
61
|
+
* @type {Map<string,{authEnv:string,value:string,expiresAtMs:number}>}
|
|
62
|
+
*/
|
|
63
|
+
const GLOBAL_CACHE = new Map();
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Enumerate the catalog's third-party providers and their auth_env. Accepts the
|
|
67
|
+
* loadCatalog result OR a bare catalog ({providers} Map / providers array /
|
|
68
|
+
* models array) — tolerant because the caller passes whatever it has on hand.
|
|
69
|
+
* Returns a de-duped list of { provider, authEnv }; providers we never broker
|
|
70
|
+
* (anthropic) and rows without an auth_env are dropped.
|
|
71
|
+
* @param {object} catalog
|
|
72
|
+
* @returns {{provider:string, authEnv:string}[]}
|
|
73
|
+
*/
|
|
74
|
+
function thirdPartyProviders(catalog) {
|
|
75
|
+
// Unwrap a loadCatalog result ({catalog, models, byRef}) to the bare catalog:
|
|
76
|
+
// prefer a top-level that actually carries a providers Map or non-empty models,
|
|
77
|
+
// else fall through to the nested `.catalog`. (An empty `models:[]` on the
|
|
78
|
+
// wrapper must NOT mask the real catalog underneath.)
|
|
79
|
+
let cat = catalog;
|
|
80
|
+
if (catalog && typeof catalog === "object") {
|
|
81
|
+
const topHasProviders = !!catalog.providers;
|
|
82
|
+
const topHasModels = Array.isArray(catalog.models) && catalog.models.length > 0;
|
|
83
|
+
if (!topHasProviders && !topHasModels && catalog.catalog) cat = catalog.catalog;
|
|
84
|
+
}
|
|
85
|
+
const out = new Map(); // provider -> authEnv (first non-null wins)
|
|
86
|
+
|
|
87
|
+
const consider = (provider, authEnv) => {
|
|
88
|
+
const p = typeof provider === "string" ? provider.trim() : "";
|
|
89
|
+
const a = typeof authEnv === "string" ? authEnv.trim() : "";
|
|
90
|
+
if (!p || !a) return;
|
|
91
|
+
if (NEVER_BROKER.has(p)) return;
|
|
92
|
+
if (!out.has(p)) out.set(p, a);
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
// Preferred source: the providers map (provider -> { auth_env, ... }).
|
|
96
|
+
const providers = cat && cat.providers;
|
|
97
|
+
if (providers && typeof providers.forEach === "function" && !(providers instanceof Array)) {
|
|
98
|
+
// Map<string, {auth_env}>
|
|
99
|
+
providers.forEach((meta, provider) => consider(provider, meta && meta.auth_env));
|
|
100
|
+
} else if (Array.isArray(providers)) {
|
|
101
|
+
for (const meta of providers) consider(meta && meta.provider, meta && meta.auth_env);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Fallback / supplement: model rows each carry provider + auth_env.
|
|
105
|
+
const models = (cat && cat.models) || [];
|
|
106
|
+
if (Array.isArray(models)) {
|
|
107
|
+
for (const row of models) consider(row && row.provider, row && row.auth_env);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return [...out.entries()].map(([provider, authEnv]) => ({ provider, authEnv }));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Is a local env var already a usable (non-empty) credential? Local wins, so
|
|
115
|
+
* such a provider is NEVER leased.
|
|
116
|
+
* @param {object} env @param {string} authEnv @returns {boolean}
|
|
117
|
+
*/
|
|
118
|
+
function localHas(env, authEnv) {
|
|
119
|
+
const v = env && env[authEnv];
|
|
120
|
+
return typeof v === "string" && v.trim() !== "";
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extract the leased value + TTL from a credential.lease res frame, tolerant of
|
|
125
|
+
* where the server lands the fields (result.value | result.credential.value;
|
|
126
|
+
* ttlMs | ttl_ms | expiresAt). Returns null when there's no usable plaintext.
|
|
127
|
+
* @param {object} frame res frame {ok,result}
|
|
128
|
+
* @param {number} nowMs
|
|
129
|
+
* @returns {{value:string, expiresAtMs:number}|null}
|
|
130
|
+
*/
|
|
131
|
+
function parseLease(frame, nowMs) {
|
|
132
|
+
if (!frame || frame.ok !== true) return null;
|
|
133
|
+
const r = frame.result || {};
|
|
134
|
+
const inner = (r.credential && typeof r.credential === "object") ? r.credential : r;
|
|
135
|
+
const value = inner.value;
|
|
136
|
+
if (typeof value !== "string" || value === "") return null;
|
|
137
|
+
|
|
138
|
+
// Derive expiry: explicit expiresAt wins; else now + ttlMs; else default.
|
|
139
|
+
let expiresAtMs;
|
|
140
|
+
const exp = inner.expiresAt != null ? inner.expiresAt : r.expiresAt;
|
|
141
|
+
const ttl = pickTtlMs(inner, r);
|
|
142
|
+
const parsed = exp != null ? Date.parse(String(exp)) : NaN;
|
|
143
|
+
if (Number.isFinite(parsed)) expiresAtMs = parsed;
|
|
144
|
+
else if (Number.isFinite(ttl) && ttl > 0) expiresAtMs = nowMs + ttl;
|
|
145
|
+
else expiresAtMs = nowMs + DEFAULT_LEASE_TTL_MS;
|
|
146
|
+
|
|
147
|
+
return { value, expiresAtMs };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pickTtlMs(inner, r) {
|
|
151
|
+
for (const v of [inner.ttlMs, inner.ttl_ms, r.ttlMs, r.ttl_ms]) {
|
|
152
|
+
const n = Number(v);
|
|
153
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
154
|
+
}
|
|
155
|
+
return NaN;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Resolve org-brokered third-party credentials into an env overlay the spawn
|
|
160
|
+
* site merges into the child env BEFORE resolveChain.
|
|
161
|
+
*
|
|
162
|
+
* For each third-party provider in the catalog whose `auth_env` is NOT already
|
|
163
|
+
* set in the local env: serve it from the in-memory cache if a non-expired lease
|
|
164
|
+
* exists, else lease it from Cohort (credential.lease) and cache the value at
|
|
165
|
+
* its TTL. Providers the org hasn't configured (lease ⇒ NOT_FOUND / error frame)
|
|
166
|
+
* are recorded in `errors` and simply omitted from the overlay — the router then
|
|
167
|
+
* skips that backend with its existing `missing_credential`, exactly as if no
|
|
168
|
+
* key were provisioned. Local-set providers are never leased (local wins).
|
|
169
|
+
*
|
|
170
|
+
* Fail-open everywhere: a disabled org plane or an unreachable Cohort yields an
|
|
171
|
+
* empty overlay (or whatever the cache already holds) and never throws.
|
|
172
|
+
*
|
|
173
|
+
* @param {object} args
|
|
174
|
+
* @param {object} args.catalog loadCatalog result OR a bare catalog
|
|
175
|
+
* @param {object} args.cfg agent config (org.cohort.* drives the client)
|
|
176
|
+
* @param {string[]} [args.roles] the agent's roles (informational; scope is
|
|
177
|
+
* enforced server-side from the verified token)
|
|
178
|
+
* @param {object} [args.env] env to read local keys from (default process.env)
|
|
179
|
+
* @param {function} [args.fetchImpl] injected fetch (tests / determinism)
|
|
180
|
+
* @param {()=>number} [args.now] clock (default Date.now)
|
|
181
|
+
* @param {Map} [args.cache] injectable cache (default the module singleton)
|
|
182
|
+
* @returns {Promise<{envOverlay:Object<string,string>, leased:string[], errors:Array<{provider:string,reason:string}>}>}
|
|
183
|
+
*/
|
|
184
|
+
export async function resolveOrgCredentialEnv(args = {}) {
|
|
185
|
+
const catalog = args.catalog;
|
|
186
|
+
const cfg = args.cfg;
|
|
187
|
+
const env = args.env || process.env;
|
|
188
|
+
const fetchImpl = args.fetchImpl;
|
|
189
|
+
const nowFn = typeof args.now === "function" ? args.now : Date.now;
|
|
190
|
+
const cache = args.cache instanceof Map ? args.cache : GLOBAL_CACHE;
|
|
191
|
+
|
|
192
|
+
const envOverlay = {};
|
|
193
|
+
const leased = [];
|
|
194
|
+
const errors = [];
|
|
195
|
+
|
|
196
|
+
// Fail-open: org plane disabled or unconfigured ⇒ nothing to broker, return
|
|
197
|
+
// an empty overlay so the router falls through to whatever local env has.
|
|
198
|
+
if (!isEnabled(cfg)) {
|
|
199
|
+
return { envOverlay, leased, errors };
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const conn = configFromAgent(cfg);
|
|
203
|
+
const o = { base: conn.base, token: conn.token, fetchImpl };
|
|
204
|
+
|
|
205
|
+
const providers = thirdPartyProviders(catalog);
|
|
206
|
+
if (providers.length === 0) {
|
|
207
|
+
return { envOverlay, leased, errors };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const nowMs = nowFn();
|
|
211
|
+
|
|
212
|
+
for (const { provider, authEnv } of providers) {
|
|
213
|
+
// LOCAL WINS — never lease a provider whose key the agent already holds.
|
|
214
|
+
if (localHas(env, authEnv)) continue;
|
|
215
|
+
|
|
216
|
+
// CACHE — reuse a non-expired lease without a second credential.lease call.
|
|
217
|
+
const cached = cache.get(provider);
|
|
218
|
+
if (cached && cached.value && cached.expiresAtMs > nowMs) {
|
|
219
|
+
envOverlay[cached.authEnv || authEnv] = cached.value;
|
|
220
|
+
leased.push(provider);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (cached && cached.expiresAtMs <= nowMs) cache.delete(provider); // expired
|
|
224
|
+
|
|
225
|
+
// LEASE — short-lived, role-scoped, audited. Fail-open per provider: an
|
|
226
|
+
// error frame (provider not configured / Cohort down / FORBIDDEN_SCOPE)
|
|
227
|
+
// is recorded and the provider is left out of the overlay.
|
|
228
|
+
let frame;
|
|
229
|
+
try {
|
|
230
|
+
frame = await credentialLease({ provider }, o);
|
|
231
|
+
} catch {
|
|
232
|
+
// The client is fail-open and shouldn't throw, but belt-and-braces: a
|
|
233
|
+
// thrown fetchImpl must never crash the spawn path.
|
|
234
|
+
frame = null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const parsed = parseLease(frame, nowMs);
|
|
238
|
+
if (!parsed) {
|
|
239
|
+
const reason =
|
|
240
|
+
(frame && frame.error && frame.error.code) ||
|
|
241
|
+
(frame && frame.ok === false ? "lease_failed" : "unreachable");
|
|
242
|
+
errors.push({ provider, reason: String(reason) });
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
cache.set(provider, { authEnv, value: parsed.value, expiresAtMs: parsed.expiresAtMs });
|
|
247
|
+
envOverlay[authEnv] = parsed.value;
|
|
248
|
+
leased.push(provider);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
return { envOverlay, leased, errors };
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Clear the module-singleton lease cache (test hygiene / forced re-lease, e.g.
|
|
256
|
+
* after a credential.revoke). Does NOT touch an injected cache.
|
|
257
|
+
*/
|
|
258
|
+
export function clearOrgCredentialCache() {
|
|
259
|
+
GLOBAL_CACHE.clear();
|
|
260
|
+
}
|