@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,758 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/model-router/auth-profiles.mjs — the OpenClaw auth-profile rotation store.
|
|
3
|
+
*
|
|
4
|
+
* A 40-60 agent org runs MULTIPLE keys per provider (several Anthropic / DeepSeek
|
|
5
|
+
* accounts) for throughput isolation. This module turns that pile of keys into a
|
|
6
|
+
* self-healing POOLED quota with per-credential circuit-breaker semantics:
|
|
7
|
+
*
|
|
8
|
+
* loadAuthProfiles(cfg) → per-provider key lists, drawn from
|
|
9
|
+
* config/auth-profiles.yaml AND from
|
|
10
|
+
* env lists (ANTHROPIC_API_KEY,
|
|
11
|
+
* ANTHROPIC_API_KEY_2, …).
|
|
12
|
+
* nextCredential(provider, {now,...}) → round-robin pick that SKIPS keys
|
|
13
|
+
* currently cooling down.
|
|
14
|
+
* markCredentialCoolingDown(provider,key,…) → a 429 / auth error cools THAT key,
|
|
15
|
+
* NOT the whole provider (the
|
|
16
|
+
* provider-level breaker is
|
|
17
|
+
* rate-guard.mjs / health.mjs's job).
|
|
18
|
+
* available(provider, {now}) → how many keys can serve right now.
|
|
19
|
+
*
|
|
20
|
+
* This is the per-CREDENTIAL layer; it composes UNDER the per-BACKEND breaker
|
|
21
|
+
* (health.mjs) and the shared 429 breaker (rate-guard.mjs). One Anthropic key
|
|
22
|
+
* hitting a 429 cools only that key — the pool keeps serving on the siblings, and
|
|
23
|
+
* only when EVERY key in the pool is cooling does the backend look exhausted.
|
|
24
|
+
*
|
|
25
|
+
* State lives at `state/rate-limits/auth-profiles.json` (same dir as the breakers
|
|
26
|
+
* so `maestro doctor` dumps them together):
|
|
27
|
+
* {
|
|
28
|
+
* "<provider>": {
|
|
29
|
+
* "rrIndex": <number — round-robin cursor>,
|
|
30
|
+
* "creds": {
|
|
31
|
+
* "<fingerprint>": { "coolUntil": <epoch ms>, "reason": <str>, "strikes": <n>, "updatedAt": <epoch ms> }
|
|
32
|
+
* }
|
|
33
|
+
* }
|
|
34
|
+
* }
|
|
35
|
+
*
|
|
36
|
+
* Credentials are keyed by a STABLE FINGERPRINT (sha256[:16] of the plaintext),
|
|
37
|
+
* never the plaintext itself — the cooldown ledger NEVER persists a secret to
|
|
38
|
+
* disk. The fingerprint is deterministic across processes so two agents (or two
|
|
39
|
+
* lanes) cooling the same key converge on the same ledger entry.
|
|
40
|
+
*
|
|
41
|
+
* Concurrency: read-modify-write goes through the same O_EXCL lock discipline as
|
|
42
|
+
* health.mjs (openSync(..,"wx"), spin-with-stale-break, atomic tmp+rename). Two
|
|
43
|
+
* concurrent marks on the same key converge to ONE coherent file with monotonic
|
|
44
|
+
* `strikes` instead of clobbering each other.
|
|
45
|
+
*
|
|
46
|
+
* House style (CLAUDE.md): ESM .mjs, Node 20, near-zero-dep (js-yaml is the one
|
|
47
|
+
* parse dep, imported defensively). EVERYTHING injectable so tests are hermetic —
|
|
48
|
+
* `deps.now`, `deps.stateDir`, `deps.agentRoot`, `deps.env` all bypass the real
|
|
49
|
+
* clock/fs/env. NEVER throws on the read path: a missing/corrupt ledger degrades
|
|
50
|
+
* to "no cooldowns" (fail-open-for-work — better one extra request on a key we
|
|
51
|
+
* can't prove is cooling than blocking the whole pool).
|
|
52
|
+
*
|
|
53
|
+
* @module lib/model-router/auth-profiles
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
import {
|
|
57
|
+
existsSync,
|
|
58
|
+
mkdirSync,
|
|
59
|
+
readFileSync,
|
|
60
|
+
writeFileSync,
|
|
61
|
+
renameSync,
|
|
62
|
+
unlinkSync,
|
|
63
|
+
openSync,
|
|
64
|
+
closeSync,
|
|
65
|
+
} from "node:fs";
|
|
66
|
+
import { join, resolve, dirname } from "node:path";
|
|
67
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
68
|
+
|
|
69
|
+
import { sanitizeProvider } from "../rate-guard.mjs";
|
|
70
|
+
|
|
71
|
+
// js-yaml is a hard dep of @cohortapp/agent-sdk, imported defensively so the read
|
|
72
|
+
// path degrades rather than crashes if the dep tree is broken on a machine.
|
|
73
|
+
let yamlParse = null;
|
|
74
|
+
try {
|
|
75
|
+
const y = await import("js-yaml");
|
|
76
|
+
yamlParse = (text) => y.load(text);
|
|
77
|
+
} catch {
|
|
78
|
+
yamlParse = null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const SECOND = 1000;
|
|
82
|
+
const MINUTE = 60 * SECOND;
|
|
83
|
+
|
|
84
|
+
/** Stepped cooldown ladder for a cooled credential: 30s → 1m → 5m, then sticks. */
|
|
85
|
+
const COOLDOWN_LADDER_MS = Object.freeze([30 * SECOND, MINUTE, 5 * MINUTE]);
|
|
86
|
+
/** Default cooldown when a caller passes no explicit ms (uses the ladder). */
|
|
87
|
+
function ladderMsForStrike(strike) {
|
|
88
|
+
const i = Math.max(0, Math.min(COOLDOWN_LADDER_MS.length - 1, strike));
|
|
89
|
+
return COOLDOWN_LADDER_MS[i];
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** How long we busy-spin trying to grab the O_EXCL lock before giving up. */
|
|
93
|
+
const LOCK_TIMEOUT_MS = 2 * SECOND;
|
|
94
|
+
const LOCK_SPIN_MS = 5;
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// Injected primitives + paths
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function clock(deps) {
|
|
101
|
+
return deps && typeof deps.now === "function" ? deps.now : Date.now;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function envOf(deps) {
|
|
105
|
+
return (deps && deps.env) || process.env || {};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function resolveStateDir(deps) {
|
|
109
|
+
if (deps && deps.stateDir) return resolve(deps.stateDir);
|
|
110
|
+
const root =
|
|
111
|
+
(deps && deps.agentRoot) ||
|
|
112
|
+
process.env.AGENT_ROOT ||
|
|
113
|
+
process.env.AGENT_DIR ||
|
|
114
|
+
process.cwd();
|
|
115
|
+
return join(resolve(root), "state", "rate-limits");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function ledgerPath(deps) {
|
|
119
|
+
return join(resolveStateDir(deps), "auth-profiles.json");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function agentRootOf(deps) {
|
|
123
|
+
return resolve(
|
|
124
|
+
(deps && deps.agentRoot) ||
|
|
125
|
+
process.env.AGENT_ROOT ||
|
|
126
|
+
process.env.AGENT_DIR ||
|
|
127
|
+
process.cwd()
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function authProfilesConfigPath(root) {
|
|
132
|
+
return join(root, "config", "auth-profiles.yaml");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---------------------------------------------------------------------------
|
|
136
|
+
// Fingerprinting (never persist plaintext)
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Stable, deterministic fingerprint of a credential value. We hash the plaintext
|
|
141
|
+
* so the cooldown ledger references a key WITHOUT storing it, and so two
|
|
142
|
+
* processes cooling the same key converge on the same ledger entry. 16 hex chars
|
|
143
|
+
* (64 bits) is collision-resistant for a per-provider key pool.
|
|
144
|
+
*
|
|
145
|
+
* @param {string} value
|
|
146
|
+
* @returns {string}
|
|
147
|
+
*/
|
|
148
|
+
export function fingerprint(value) {
|
|
149
|
+
if (typeof value !== "string" || value === "") return "";
|
|
150
|
+
return createHash("sha256").update(value, "utf8").digest("hex").slice(0, 16);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// loadAuthProfiles — per-provider key list from config + env
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
function readTextSafe(path) {
|
|
158
|
+
try {
|
|
159
|
+
if (!existsSync(path)) return null;
|
|
160
|
+
return readFileSync(path, "utf-8");
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Parse the config/auth-profiles.yaml document into a provider → key[] map. The
|
|
168
|
+
* file shape is intentionally simple and forgiving:
|
|
169
|
+
*
|
|
170
|
+
* providers:
|
|
171
|
+
* deepseek:
|
|
172
|
+
* keys: ["sk-a", "sk-b"] # explicit pool
|
|
173
|
+
* anthropic:
|
|
174
|
+
* env: [ANTHROPIC_API_KEY, ANTHROPIC_API_KEY_2] # names of env vars to pool
|
|
175
|
+
*
|
|
176
|
+
* Either `keys` (literal values) or `env` (env var NAMES to read) may appear; both
|
|
177
|
+
* merge. Bare `provider: [k1, k2]` and `provider: "k1"` are also accepted.
|
|
178
|
+
*
|
|
179
|
+
* @param {object|string|null} doc parsed YAML doc (or raw string, or null)
|
|
180
|
+
* @param {object} env
|
|
181
|
+
* @returns {Map<string,string[]>}
|
|
182
|
+
*/
|
|
183
|
+
function profilesFromConfigDoc(doc, env) {
|
|
184
|
+
const out = new Map();
|
|
185
|
+
if (typeof doc === "string") {
|
|
186
|
+
if (!yamlParse) return out;
|
|
187
|
+
try {
|
|
188
|
+
doc = yamlParse(doc);
|
|
189
|
+
} catch {
|
|
190
|
+
return out;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (!doc || typeof doc !== "object") return out;
|
|
194
|
+
|
|
195
|
+
const providers = doc.providers && typeof doc.providers === "object" ? doc.providers : doc;
|
|
196
|
+
if (!providers || typeof providers !== "object") return out;
|
|
197
|
+
|
|
198
|
+
for (const [rawName, spec] of Object.entries(providers)) {
|
|
199
|
+
const provider = sanitizeProvider(rawName);
|
|
200
|
+
const keys = [];
|
|
201
|
+
if (Array.isArray(spec)) {
|
|
202
|
+
for (const v of spec) if (typeof v === "string" && v.trim()) keys.push(v.trim());
|
|
203
|
+
} else if (typeof spec === "string") {
|
|
204
|
+
if (spec.trim()) keys.push(spec.trim());
|
|
205
|
+
} else if (spec && typeof spec === "object") {
|
|
206
|
+
const kk = spec.keys;
|
|
207
|
+
if (Array.isArray(kk)) {
|
|
208
|
+
for (const v of kk) if (typeof v === "string" && v.trim()) keys.push(v.trim());
|
|
209
|
+
} else if (typeof kk === "string" && kk.trim()) {
|
|
210
|
+
keys.push(kk.trim());
|
|
211
|
+
}
|
|
212
|
+
const ee = spec.env;
|
|
213
|
+
const envNames = Array.isArray(ee) ? ee : typeof ee === "string" ? [ee] : [];
|
|
214
|
+
for (const name of envNames) {
|
|
215
|
+
if (typeof name !== "string") continue;
|
|
216
|
+
const v = env[name];
|
|
217
|
+
if (typeof v === "string" && v.trim()) keys.push(v.trim());
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (keys.length) mergeKeys(out, provider, keys);
|
|
221
|
+
}
|
|
222
|
+
return out;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Pool keys from env-var lists. Convention (OpenClaw api-key-rotation): a base
|
|
227
|
+
* var plus numbered siblings — ANTHROPIC_API_KEY, ANTHROPIC_API_KEY_2,
|
|
228
|
+
* ANTHROPIC_API_KEY_3, … and also ANTHROPIC_API_KEYS="k1,k2,k3" (comma list).
|
|
229
|
+
*
|
|
230
|
+
* The mapping provider → base env var name comes from the catalog's auth_env
|
|
231
|
+
* (passed via opts.authEnvByProvider) so we don't hardcode provider names. We
|
|
232
|
+
* also accept an explicit opts.envBases map. As a convenience, the well-known
|
|
233
|
+
* defaults below are always probed.
|
|
234
|
+
*
|
|
235
|
+
* @param {object} env
|
|
236
|
+
* @param {Map<string,string>} authEnvByProvider provider -> base env var name
|
|
237
|
+
* @returns {Map<string,string[]>}
|
|
238
|
+
*/
|
|
239
|
+
function profilesFromEnv(env, authEnvByProvider) {
|
|
240
|
+
const out = new Map();
|
|
241
|
+
for (const [rawProvider, baseVar] of authEnvByProvider) {
|
|
242
|
+
if (typeof baseVar !== "string" || !baseVar) continue;
|
|
243
|
+
const provider = sanitizeProvider(rawProvider);
|
|
244
|
+
const keys = [];
|
|
245
|
+
|
|
246
|
+
// Comma-list form: <BASE>S="k1,k2" (e.g. ANTHROPIC_API_KEYS).
|
|
247
|
+
const listVar = env[`${baseVar}S`] ?? env[`${baseVar}_LIST`];
|
|
248
|
+
if (typeof listVar === "string" && listVar.includes(",")) {
|
|
249
|
+
for (const part of listVar.split(",")) {
|
|
250
|
+
const v = part.trim();
|
|
251
|
+
if (v) keys.push(v);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// Base var.
|
|
256
|
+
const base = env[baseVar];
|
|
257
|
+
if (typeof base === "string" && base.trim()) keys.push(base.trim());
|
|
258
|
+
|
|
259
|
+
// Numbered siblings: <BASE>_2, <BASE>_3, … (stop at the first gap, cap 32).
|
|
260
|
+
for (let n = 2; n <= 32; n++) {
|
|
261
|
+
const v = env[`${baseVar}_${n}`];
|
|
262
|
+
if (typeof v === "string" && v.trim()) keys.push(v.trim());
|
|
263
|
+
else break;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
if (keys.length) mergeKeys(out, provider, keys);
|
|
267
|
+
}
|
|
268
|
+
return out;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** Append keys to a provider's pool, de-duped, preserving first-seen order. */
|
|
272
|
+
function mergeKeys(map, provider, keys) {
|
|
273
|
+
const list = map.get(provider) || [];
|
|
274
|
+
const seen = new Set(list);
|
|
275
|
+
for (const k of keys) {
|
|
276
|
+
if (typeof k !== "string" || !k.trim()) continue;
|
|
277
|
+
const v = k.trim();
|
|
278
|
+
if (seen.has(v)) continue;
|
|
279
|
+
seen.add(v);
|
|
280
|
+
list.push(v);
|
|
281
|
+
}
|
|
282
|
+
map.set(provider, list);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* @typedef {object} AuthProfiles
|
|
287
|
+
* @property {Map<string,string[]>} byProvider provider -> ordered key pool (plaintext)
|
|
288
|
+
* @property {(provider:string)=>string[]} keysFor
|
|
289
|
+
* @property {(provider:string)=>boolean} isPooled true ⇒ >1 key (rotation matters)
|
|
290
|
+
* @property {string[]} providers
|
|
291
|
+
*/
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Load the auth-profile key pools. Merges config/auth-profiles.yaml with env-var
|
|
295
|
+
* lists. NEVER throws — a missing config or env yields an empty pool map.
|
|
296
|
+
*
|
|
297
|
+
* @param {object} [cfg] optional agent config (currently informational; the
|
|
298
|
+
* pools come from the dedicated auth-profiles.yaml + env)
|
|
299
|
+
* @param {object} [opts]
|
|
300
|
+
* @param {object} [opts.env] env to read (default process.env)
|
|
301
|
+
* @param {string} [opts.agentRoot]
|
|
302
|
+
* @param {object|string} [opts.configDoc] bypass-FS config doc (tests)
|
|
303
|
+
* @param {Map<string,string>|object} [opts.authEnvByProvider] provider -> base env var
|
|
304
|
+
* @returns {AuthProfiles}
|
|
305
|
+
*/
|
|
306
|
+
export function loadAuthProfiles(cfg, opts = {}) {
|
|
307
|
+
const env = envOf(opts);
|
|
308
|
+
const root = agentRootOf(opts);
|
|
309
|
+
|
|
310
|
+
// 1. config/auth-profiles.yaml (or injected configDoc).
|
|
311
|
+
let configDoc = opts.configDoc;
|
|
312
|
+
if (configDoc === undefined) {
|
|
313
|
+
const text = readTextSafe(authProfilesConfigPath(root));
|
|
314
|
+
configDoc = text;
|
|
315
|
+
}
|
|
316
|
+
const fromConfig = profilesFromConfigDoc(configDoc, env);
|
|
317
|
+
|
|
318
|
+
// 2. env-var lists, keyed by the catalog's provider → auth_env mapping.
|
|
319
|
+
const authEnvByProvider = normaliseAuthEnvMap(opts.authEnvByProvider);
|
|
320
|
+
const fromEnv = profilesFromEnv(env, authEnvByProvider);
|
|
321
|
+
|
|
322
|
+
// Merge: config keys first (explicit intent wins ordering), then env siblings.
|
|
323
|
+
const byProvider = new Map();
|
|
324
|
+
for (const [provider, keys] of fromConfig) mergeKeys(byProvider, provider, keys);
|
|
325
|
+
for (const [provider, keys] of fromEnv) mergeKeys(byProvider, provider, keys);
|
|
326
|
+
|
|
327
|
+
return {
|
|
328
|
+
byProvider,
|
|
329
|
+
keysFor(provider) {
|
|
330
|
+
return byProvider.get(sanitizeProvider(provider)) || [];
|
|
331
|
+
},
|
|
332
|
+
isPooled(provider) {
|
|
333
|
+
const k = byProvider.get(sanitizeProvider(provider));
|
|
334
|
+
return Array.isArray(k) && k.length > 1;
|
|
335
|
+
},
|
|
336
|
+
get providers() {
|
|
337
|
+
return [...byProvider.keys()];
|
|
338
|
+
},
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/** Coerce a Map | plain object | undefined into a Map<provider, baseVar>. */
|
|
343
|
+
function normaliseAuthEnvMap(m) {
|
|
344
|
+
const out = new Map();
|
|
345
|
+
if (m instanceof Map) {
|
|
346
|
+
for (const [p, v] of m) if (typeof v === "string") out.set(sanitizeProvider(p), v);
|
|
347
|
+
} else if (m && typeof m === "object") {
|
|
348
|
+
for (const [p, v] of Object.entries(m)) if (typeof v === "string") out.set(sanitizeProvider(p), v);
|
|
349
|
+
}
|
|
350
|
+
return out;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Convenience: build the provider → auth_env map loadAuthProfiles wants from a
|
|
355
|
+
* loaded catalog (its `providers` Map / `models` array each carry auth_env).
|
|
356
|
+
* NEVER throws.
|
|
357
|
+
*
|
|
358
|
+
* @param {object} catalog loadCatalog result OR bare catalog
|
|
359
|
+
* @returns {Map<string,string>}
|
|
360
|
+
*/
|
|
361
|
+
export function authEnvMapFromCatalog(catalog) {
|
|
362
|
+
const out = new Map();
|
|
363
|
+
let cat = catalog;
|
|
364
|
+
if (catalog && typeof catalog === "object" && !catalog.providers && !Array.isArray(catalog.models) && catalog.catalog) {
|
|
365
|
+
cat = catalog.catalog;
|
|
366
|
+
}
|
|
367
|
+
const consider = (provider, authEnv) => {
|
|
368
|
+
const p = typeof provider === "string" ? provider.trim() : "";
|
|
369
|
+
const a = typeof authEnv === "string" ? authEnv.trim() : "";
|
|
370
|
+
if (!p || !a) return;
|
|
371
|
+
if (!out.has(sanitizeProvider(p))) out.set(sanitizeProvider(p), a);
|
|
372
|
+
};
|
|
373
|
+
const providers = cat && cat.providers;
|
|
374
|
+
if (providers && typeof providers.forEach === "function" && !(providers instanceof Array)) {
|
|
375
|
+
providers.forEach((meta, provider) => consider(provider, meta && meta.auth_env));
|
|
376
|
+
} else if (Array.isArray(providers)) {
|
|
377
|
+
for (const meta of providers) consider(meta && meta.provider, meta && meta.auth_env);
|
|
378
|
+
}
|
|
379
|
+
const models = (cat && cat.models) || [];
|
|
380
|
+
if (Array.isArray(models)) {
|
|
381
|
+
for (const row of models) consider(row && row.provider, row && row.auth_env);
|
|
382
|
+
}
|
|
383
|
+
return out;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// ---------------------------------------------------------------------------
|
|
387
|
+
// Cooldown ledger (read/write under O_EXCL lock)
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
|
|
390
|
+
function readLedgerSafe(deps) {
|
|
391
|
+
const p = ledgerPath(deps);
|
|
392
|
+
if (!existsSync(p)) return {};
|
|
393
|
+
try {
|
|
394
|
+
const raw = JSON.parse(readFileSync(p, "utf-8"));
|
|
395
|
+
return raw && typeof raw === "object" ? raw : {};
|
|
396
|
+
} catch {
|
|
397
|
+
return {};
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function providerEntry(ledger, provider) {
|
|
402
|
+
const e = ledger[provider];
|
|
403
|
+
if (e && typeof e === "object") {
|
|
404
|
+
return {
|
|
405
|
+
rrIndex: Number.isFinite(Number(e.rrIndex)) ? Number(e.rrIndex) : 0,
|
|
406
|
+
creds: e.creds && typeof e.creds === "object" ? e.creds : {},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
return { rrIndex: 0, creds: {} };
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function credState(entry, fp) {
|
|
413
|
+
const c = entry.creds[fp];
|
|
414
|
+
if (c && typeof c === "object") {
|
|
415
|
+
return {
|
|
416
|
+
coolUntil: Number.isFinite(Number(c.coolUntil)) ? Number(c.coolUntil) : 0,
|
|
417
|
+
reason: typeof c.reason === "string" ? c.reason : null,
|
|
418
|
+
strikes: Number.isFinite(Number(c.strikes)) ? Number(c.strikes) : 0,
|
|
419
|
+
updatedAt: Number.isFinite(Number(c.updatedAt)) ? Number(c.updatedAt) : 0,
|
|
420
|
+
};
|
|
421
|
+
}
|
|
422
|
+
return { coolUntil: 0, reason: null, strikes: 0, updatedAt: 0 };
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
function isCooling(entry, fp, now) {
|
|
426
|
+
return credState(entry, fp).coolUntil > now;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// O_EXCL lock — mirrors health.mjs's withLock so concurrent marks converge.
|
|
430
|
+
|
|
431
|
+
function acquireLock(lockPath, deps) {
|
|
432
|
+
const now = clock(deps);
|
|
433
|
+
const deadline = now() + LOCK_TIMEOUT_MS;
|
|
434
|
+
for (;;) {
|
|
435
|
+
try {
|
|
436
|
+
return openSync(lockPath, "wx"); // O_CREAT | O_EXCL | O_WRONLY
|
|
437
|
+
} catch (err) {
|
|
438
|
+
if (err && err.code === "EEXIST") {
|
|
439
|
+
try {
|
|
440
|
+
const raw = readFileSync(lockPath, "utf-8");
|
|
441
|
+
const heldAt = Number(JSON.parse(raw).at);
|
|
442
|
+
if (Number.isFinite(heldAt) && now() - heldAt > LOCK_TIMEOUT_MS) {
|
|
443
|
+
try { unlinkSync(lockPath); } catch { /* */ }
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
} catch { /* unreadable lock — spin/timeout */ }
|
|
447
|
+
if (now() >= deadline) return null;
|
|
448
|
+
busyWaitMs(LOCK_SPIN_MS);
|
|
449
|
+
continue;
|
|
450
|
+
}
|
|
451
|
+
return undefined; // e.g. ENOENT on missing dir — caller mkdir+retries
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
function busyWaitMs(ms) {
|
|
457
|
+
const end = Date.now() + ms;
|
|
458
|
+
while (Date.now() < end) { /* spin */ }
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
/**
|
|
462
|
+
* Run `mutator(ledger) → ledger` under an exclusive lock over the whole ledger
|
|
463
|
+
* file and atomically persist it. Returns the persisted ledger. On unrecoverable
|
|
464
|
+
* I/O failure applies the mutation in-memory and returns it (fail-open).
|
|
465
|
+
*/
|
|
466
|
+
function withLock(deps, mutator) {
|
|
467
|
+
const file = ledgerPath(deps);
|
|
468
|
+
const lockPath = `${file}.lock`;
|
|
469
|
+
const now = clock(deps);
|
|
470
|
+
try { mkdirSync(dirname(file), { recursive: true }); } catch { /* */ }
|
|
471
|
+
|
|
472
|
+
let fd = acquireLock(lockPath, deps);
|
|
473
|
+
if (fd === undefined) {
|
|
474
|
+
try { mkdirSync(dirname(file), { recursive: true }); } catch { /* */ }
|
|
475
|
+
fd = acquireLock(lockPath, deps);
|
|
476
|
+
}
|
|
477
|
+
if (fd == null) {
|
|
478
|
+
const cur = readLedgerSafe(deps);
|
|
479
|
+
const next = mutator(cur) || cur;
|
|
480
|
+
bestEffortWrite(file, next, deps);
|
|
481
|
+
return next;
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
try { writeFileSync(fd, JSON.stringify({ at: now(), pid: process.pid })); } catch { /* */ }
|
|
485
|
+
const cur = readLedgerSafe(deps);
|
|
486
|
+
const next = mutator(cur) || cur;
|
|
487
|
+
bestEffortWrite(file, next, deps);
|
|
488
|
+
return next;
|
|
489
|
+
} finally {
|
|
490
|
+
try { closeSync(fd); } catch { /* */ }
|
|
491
|
+
try { unlinkSync(lockPath); } catch { /* */ }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function bestEffortWrite(file, ledger, deps) {
|
|
496
|
+
try {
|
|
497
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
498
|
+
const tmp = `${file}.tmp.${process.pid}.${Date.now()}.${randomBytes(2).toString("hex")}`;
|
|
499
|
+
writeFileSync(tmp, JSON.stringify(ledger, null, 2) + "\n");
|
|
500
|
+
try {
|
|
501
|
+
renameSync(tmp, file);
|
|
502
|
+
} catch (err) {
|
|
503
|
+
try { unlinkSync(tmp); } catch { /* */ }
|
|
504
|
+
throw err;
|
|
505
|
+
}
|
|
506
|
+
} catch {
|
|
507
|
+
/* fail-open: in-memory only */
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
// ---------------------------------------------------------------------------
|
|
512
|
+
// Public API: nextCredential / markCredentialCoolingDown / available
|
|
513
|
+
// ---------------------------------------------------------------------------
|
|
514
|
+
|
|
515
|
+
/**
|
|
516
|
+
* Pick the next credential for `provider` round-robin, SKIPPING any key whose
|
|
517
|
+
* cooldown has not expired. Advances and persists the round-robin cursor under
|
|
518
|
+
* the lock so concurrent pickers fairly share the pool. NEVER throws.
|
|
519
|
+
*
|
|
520
|
+
* The key pool comes from `opts.profiles` (a loadAuthProfiles result) or
|
|
521
|
+
* `opts.keys` (an explicit string[]). When the pool has 0 or 1 usable key, the
|
|
522
|
+
* single-key behavior is preserved exactly (returns that key, or null).
|
|
523
|
+
*
|
|
524
|
+
* @param {string} provider
|
|
525
|
+
* @param {object} [opts]
|
|
526
|
+
* @param {AuthProfiles} [opts.profiles] from loadAuthProfiles
|
|
527
|
+
* @param {string[]} [opts.keys] explicit pool (overrides profiles)
|
|
528
|
+
* @param {number} [opts.now]
|
|
529
|
+
* @param {object} [opts.deps] { now?, stateDir?, agentRoot? } for the ledger
|
|
530
|
+
* @returns {{ key:string|null, fingerprint:string|null, index:number,
|
|
531
|
+
* poolSize:number, available:number, allCooling:boolean,
|
|
532
|
+
* soonestCoolUntil:number|null }}
|
|
533
|
+
*/
|
|
534
|
+
export function nextCredential(provider, opts = {}) {
|
|
535
|
+
const prov = sanitizeProvider(provider);
|
|
536
|
+
const deps = opts.deps || opts; // allow flat opts.now / opts.stateDir too
|
|
537
|
+
const now = Number.isFinite(opts.now) ? opts.now : clock(deps)();
|
|
538
|
+
|
|
539
|
+
const keys = poolKeys(prov, opts);
|
|
540
|
+
const poolSize = keys.length;
|
|
541
|
+
|
|
542
|
+
// Single-key (or empty) pool: preserve today's behavior exactly. No ledger
|
|
543
|
+
// write, no rotation — just return the one key (respecting its cooldown only
|
|
544
|
+
// insofar as we still surface allCooling so the caller can skip if it wants).
|
|
545
|
+
if (poolSize === 0) {
|
|
546
|
+
return { key: null, fingerprint: null, index: -1, poolSize: 0, available: 0, allCooling: false, soonestCoolUntil: null };
|
|
547
|
+
}
|
|
548
|
+
if (poolSize === 1) {
|
|
549
|
+
const key = keys[0];
|
|
550
|
+
const fp = fingerprint(key);
|
|
551
|
+
const led = readLedgerSafe(deps);
|
|
552
|
+
const entry = providerEntry(led, prov);
|
|
553
|
+
const cooling = isCooling(entry, fp, now);
|
|
554
|
+
const cs = credState(entry, fp);
|
|
555
|
+
return {
|
|
556
|
+
key,
|
|
557
|
+
fingerprint: fp,
|
|
558
|
+
index: 0,
|
|
559
|
+
poolSize: 1,
|
|
560
|
+
available: cooling ? 0 : 1,
|
|
561
|
+
allCooling: cooling,
|
|
562
|
+
soonestCoolUntil: cooling ? cs.coolUntil : null,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Multi-key pool: round-robin over the keys, skipping cooled ones. The cursor
|
|
567
|
+
// is advanced + persisted under the lock so two concurrent pickers don't both
|
|
568
|
+
// hand out the same key.
|
|
569
|
+
let chosenIdx = -1;
|
|
570
|
+
let chosenKey = null;
|
|
571
|
+
let chosenFp = null;
|
|
572
|
+
let availableCount = 0;
|
|
573
|
+
let soonest = null;
|
|
574
|
+
|
|
575
|
+
withLock(deps, (ledger) => {
|
|
576
|
+
const entry = providerEntry(ledger, prov);
|
|
577
|
+
const start = ((entry.rrIndex % poolSize) + poolSize) % poolSize;
|
|
578
|
+
// First pass: find the next non-cooling key starting at the cursor.
|
|
579
|
+
for (let step = 0; step < poolSize; step++) {
|
|
580
|
+
const idx = (start + step) % poolSize;
|
|
581
|
+
const key = keys[idx];
|
|
582
|
+
const fp = fingerprint(key);
|
|
583
|
+
if (!isCooling(entry, fp, now)) {
|
|
584
|
+
if (chosenIdx === -1) {
|
|
585
|
+
chosenIdx = idx;
|
|
586
|
+
chosenKey = key;
|
|
587
|
+
chosenFp = fp;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
// Count availability + soonest cooldown across the whole pool (for the caller).
|
|
592
|
+
for (let i = 0; i < poolSize; i++) {
|
|
593
|
+
const fp = fingerprint(keys[i]);
|
|
594
|
+
const cs = credState(entry, fp);
|
|
595
|
+
if (cs.coolUntil > now) {
|
|
596
|
+
if (soonest == null || cs.coolUntil < soonest) soonest = cs.coolUntil;
|
|
597
|
+
} else {
|
|
598
|
+
availableCount++;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
// Advance the cursor to just past the chosen key so the NEXT call rotates on.
|
|
602
|
+
// If everything is cooling, advance by one so we don't pin a single key.
|
|
603
|
+
const advanceTo = chosenIdx === -1 ? (start + 1) % poolSize : (chosenIdx + 1) % poolSize;
|
|
604
|
+
ledger[prov] = { ...entry, rrIndex: advanceTo };
|
|
605
|
+
return ledger;
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
return {
|
|
609
|
+
key: chosenKey,
|
|
610
|
+
fingerprint: chosenFp,
|
|
611
|
+
index: chosenIdx,
|
|
612
|
+
poolSize,
|
|
613
|
+
available: availableCount,
|
|
614
|
+
allCooling: chosenIdx === -1,
|
|
615
|
+
soonestCoolUntil: soonest,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Cool down ONE credential (the one that just 429'd / auth-failed), NOT the whole
|
|
621
|
+
* provider. Stepped backoff via the ladder unless an explicit `ms` is given.
|
|
622
|
+
* Locked read-modify-write so concurrent marks converge with monotonic strikes
|
|
623
|
+
* and the longer cooldown wins. NEVER throws.
|
|
624
|
+
*
|
|
625
|
+
* The credential may be passed as the plaintext `key` OR a precomputed
|
|
626
|
+
* `{ fingerprint }`; either way only the fingerprint is persisted.
|
|
627
|
+
*
|
|
628
|
+
* @param {string} provider
|
|
629
|
+
* @param {string} key the plaintext credential (fingerprinted internally)
|
|
630
|
+
* @param {string} [reason] FailoverReason-ish label (e.g. "rate_limit", "auth")
|
|
631
|
+
* @param {number|null} [ms] explicit cooldown ms; null/undefined → ladder
|
|
632
|
+
* @param {object} [opts]
|
|
633
|
+
* @param {number} [opts.now]
|
|
634
|
+
* @param {object} [opts.deps]
|
|
635
|
+
* @returns {{ fingerprint:string, coolUntil:number, strikes:number, reason:string|null }}
|
|
636
|
+
*/
|
|
637
|
+
export function markCredentialCoolingDown(provider, key, reason, ms, opts = {}) {
|
|
638
|
+
const prov = sanitizeProvider(provider);
|
|
639
|
+
const deps = opts.deps || opts;
|
|
640
|
+
const now = Number.isFinite(opts.now) ? opts.now : clock(deps)();
|
|
641
|
+
const fp = typeof key === "string" ? fingerprint(key) : (key && key.fingerprint) || "";
|
|
642
|
+
if (!fp) {
|
|
643
|
+
return { fingerprint: "", coolUntil: 0, strikes: 0, reason: null };
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
let result = { fingerprint: fp, coolUntil: 0, strikes: 0, reason: reason || null };
|
|
647
|
+
withLock(deps, (ledger) => {
|
|
648
|
+
const entry = providerEntry(ledger, prov);
|
|
649
|
+
const cur = credState(entry, fp);
|
|
650
|
+
const strikes = cur.strikes + 1;
|
|
651
|
+
// strike-1 indexes the ladder from 0 (first hit → 30s).
|
|
652
|
+
const cooldownMs = ms == null ? ladderMsForStrike(strikes - 1) : Math.max(0, Number(ms) || 0);
|
|
653
|
+
const until = cooldownMs === Infinity ? Infinity : now + cooldownMs;
|
|
654
|
+
// Never shorten an existing longer cooldown (a racing shorter mark can't undo it).
|
|
655
|
+
const coolUntil =
|
|
656
|
+
cur.coolUntil === Infinity ? Infinity : Math.max(cur.coolUntil, until);
|
|
657
|
+
const next = {
|
|
658
|
+
coolUntil,
|
|
659
|
+
reason: reason || cur.reason || null,
|
|
660
|
+
strikes,
|
|
661
|
+
updatedAt: now,
|
|
662
|
+
};
|
|
663
|
+
entry.creds[fp] = next;
|
|
664
|
+
ledger[prov] = entry;
|
|
665
|
+
result = { fingerprint: fp, coolUntil, strikes, reason: next.reason };
|
|
666
|
+
return ledger;
|
|
667
|
+
});
|
|
668
|
+
return result;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/**
|
|
672
|
+
* Clear a credential's cooldown (clean success). Idempotent + best-effort.
|
|
673
|
+
* NEVER throws.
|
|
674
|
+
*
|
|
675
|
+
* @param {string} provider
|
|
676
|
+
* @param {string} key
|
|
677
|
+
* @param {object} [opts]
|
|
678
|
+
*/
|
|
679
|
+
export function markCredentialHealthy(provider, key, opts = {}) {
|
|
680
|
+
const prov = sanitizeProvider(provider);
|
|
681
|
+
const deps = opts.deps || opts;
|
|
682
|
+
const fp = typeof key === "string" ? fingerprint(key) : (key && key.fingerprint) || "";
|
|
683
|
+
if (!fp) return;
|
|
684
|
+
const led = readLedgerSafe(deps);
|
|
685
|
+
const entry = providerEntry(led, prov);
|
|
686
|
+
// No-op if there's nothing to clear (don't churn the file on every success).
|
|
687
|
+
if (!entry.creds[fp]) return;
|
|
688
|
+
withLock(deps, (ledger) => {
|
|
689
|
+
const e = providerEntry(ledger, prov);
|
|
690
|
+
if (e.creds[fp]) delete e.creds[fp];
|
|
691
|
+
ledger[prov] = e;
|
|
692
|
+
return ledger;
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/**
|
|
697
|
+
* How many credentials in `provider`'s pool can serve right now (not cooling).
|
|
698
|
+
* NEVER throws.
|
|
699
|
+
*
|
|
700
|
+
* @param {string} provider
|
|
701
|
+
* @param {object} [opts] same pool/clock/deps shape as nextCredential
|
|
702
|
+
* @returns {{ count:number, poolSize:number, soonestCoolUntil:number|null }}
|
|
703
|
+
*/
|
|
704
|
+
export function available(provider, opts = {}) {
|
|
705
|
+
const prov = sanitizeProvider(provider);
|
|
706
|
+
const deps = opts.deps || opts;
|
|
707
|
+
const now = Number.isFinite(opts.now) ? opts.now : clock(deps)();
|
|
708
|
+
const keys = poolKeys(prov, opts);
|
|
709
|
+
const poolSize = keys.length;
|
|
710
|
+
if (poolSize === 0) return { count: 0, poolSize: 0, soonestCoolUntil: null };
|
|
711
|
+
const led = readLedgerSafe(deps);
|
|
712
|
+
const entry = providerEntry(led, prov);
|
|
713
|
+
let count = 0;
|
|
714
|
+
let soonest = null;
|
|
715
|
+
for (const k of keys) {
|
|
716
|
+
const cs = credState(entry, fingerprint(k));
|
|
717
|
+
if (cs.coolUntil > now) {
|
|
718
|
+
if (soonest == null || cs.coolUntil < soonest) soonest = cs.coolUntil;
|
|
719
|
+
} else {
|
|
720
|
+
count++;
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return { count, poolSize, soonestCoolUntil: soonest };
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/** Resolve the candidate key pool for a provider from opts.keys | opts.profiles. */
|
|
727
|
+
function poolKeys(provider, opts) {
|
|
728
|
+
if (Array.isArray(opts.keys)) {
|
|
729
|
+
return opts.keys.filter((k) => typeof k === "string" && k.trim()).map((k) => k.trim());
|
|
730
|
+
}
|
|
731
|
+
if (opts.profiles && typeof opts.profiles.keysFor === "function") {
|
|
732
|
+
return opts.profiles.keysFor(provider);
|
|
733
|
+
}
|
|
734
|
+
return [];
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
export const _internals = {
|
|
738
|
+
profilesFromConfigDoc,
|
|
739
|
+
profilesFromEnv,
|
|
740
|
+
mergeKeys,
|
|
741
|
+
ladderMsForStrike,
|
|
742
|
+
COOLDOWN_LADDER_MS,
|
|
743
|
+
readLedgerSafe,
|
|
744
|
+
providerEntry,
|
|
745
|
+
credState,
|
|
746
|
+
poolKeys,
|
|
747
|
+
ledgerPath,
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
export default {
|
|
751
|
+
loadAuthProfiles,
|
|
752
|
+
nextCredential,
|
|
753
|
+
markCredentialCoolingDown,
|
|
754
|
+
markCredentialHealthy,
|
|
755
|
+
available,
|
|
756
|
+
fingerprint,
|
|
757
|
+
authEnvMapFromCatalog,
|
|
758
|
+
};
|