@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,315 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/secrets/broker.mjs — the secret lifecycle broker (gaps-enterprise-ops
|
|
3
|
+
* G12 + G16 DR-config).
|
|
4
|
+
*
|
|
5
|
+
* Sits above lib/secrets/providers.mjs and turns "where does this secret live"
|
|
6
|
+
* into a resolution policy plus two fleet operations: pull (sync) and rotate.
|
|
7
|
+
*
|
|
8
|
+
* • resolveSecret(name, {cfg, …})
|
|
9
|
+
* Chain precedence: explicit override > broker > file > env.
|
|
10
|
+
* The first provider that returns a non-null value wins; every read in
|
|
11
|
+
* the chain fails OPEN (a provider error is treated as "not here, keep
|
|
12
|
+
* going"), matching the project audit's fail-open posture.
|
|
13
|
+
*
|
|
14
|
+
* • syncSecrets({cfg, names, …})
|
|
15
|
+
* Pull a set of secrets from the broker into the local provider (the
|
|
16
|
+
* backing store of `maestro secrets sync`). Each pull is a `set` on the
|
|
17
|
+
* local provider, so it fails CLOSED per secret — a secret that the
|
|
18
|
+
* broker can't supply is reported, not silently skipped.
|
|
19
|
+
*
|
|
20
|
+
* • rotateSecret(name, newValue, {cfg, …})
|
|
21
|
+
* Write the new value to the AUTHORITATIVE provider (broker if a broker
|
|
22
|
+
* is configured, else the local provider) and record names-only rotation
|
|
23
|
+
* metadata. Fail-closed on the write.
|
|
24
|
+
*
|
|
25
|
+
* Audit: every rotation appends one line to logs/audit/secrets.jsonl recording
|
|
26
|
+
* WHO (the agent identity), WHAT (the secret name + a name fingerprint), WHEN,
|
|
27
|
+
* and the target provider — and NEVER the value. One identity per agent: the
|
|
28
|
+
* agent token, surfaced via getAgentIdentity().
|
|
29
|
+
*
|
|
30
|
+
* Pure node:* + the providers module. The broker's network identity (fetch,
|
|
31
|
+
* baseUrl, token) is injected through cfg/ctx so the whole module is testable
|
|
32
|
+
* with a stub fetch and no live calls.
|
|
33
|
+
*
|
|
34
|
+
* @module lib/secrets/broker
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
"use strict";
|
|
38
|
+
|
|
39
|
+
import { join } from "node:path";
|
|
40
|
+
|
|
41
|
+
import { appendJsonl } from "../fs-atomic.mjs";
|
|
42
|
+
import {
|
|
43
|
+
selectProvider,
|
|
44
|
+
BrokerProvider,
|
|
45
|
+
nameFingerprint,
|
|
46
|
+
} from "./providers.mjs";
|
|
47
|
+
|
|
48
|
+
/** Relative path of the names-only rotation audit log under the agent root. */
|
|
49
|
+
const AUDIT_REL = join("logs", "audit", "secrets.jsonl");
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* The single agent identity used for broker auth + audit attribution. One
|
|
53
|
+
* identity per agent (the agent token). Resolution order, all injectable:
|
|
54
|
+
* ctx.identity > ctx.env.MAESTRO_AGENT_ID > cfg.broker.identity > "agent".
|
|
55
|
+
*
|
|
56
|
+
* @param {object} cfg
|
|
57
|
+
* @param {object} ctx
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
export function getAgentIdentity(cfg, ctx = {}) {
|
|
61
|
+
const env = ctx.env || process.env;
|
|
62
|
+
return (
|
|
63
|
+
ctx.identity ||
|
|
64
|
+
env.MAESTRO_AGENT_ID ||
|
|
65
|
+
(cfg && cfg.broker && cfg.broker.identity) ||
|
|
66
|
+
"agent"
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The agent token used as the broker Bearer credential. Injectable so tests
|
|
72
|
+
* never read a real secret: ctx.token > ctx.env.MAESTRO_AGENT_TOKEN >
|
|
73
|
+
* cfg.broker.token_env-named env var.
|
|
74
|
+
*
|
|
75
|
+
* @param {object} cfg
|
|
76
|
+
* @param {object} ctx
|
|
77
|
+
* @returns {string}
|
|
78
|
+
*/
|
|
79
|
+
export function getAgentToken(cfg, ctx = {}) {
|
|
80
|
+
const env = ctx.env || process.env;
|
|
81
|
+
if (ctx.token) return ctx.token;
|
|
82
|
+
const tokenEnv = cfg && cfg.broker && cfg.broker.token_env;
|
|
83
|
+
if (tokenEnv && env[tokenEnv]) return env[tokenEnv];
|
|
84
|
+
return env.MAESTRO_AGENT_TOKEN || "";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Construct the broker provider from config, or null when no broker is
|
|
89
|
+
* configured / no fetch is available. The fetch is INJECTED (ctx.fetch) — we
|
|
90
|
+
* never reach for the global so tests stay offline by construction.
|
|
91
|
+
*
|
|
92
|
+
* @param {object} cfg parsed config/secrets.yaml
|
|
93
|
+
* @param {object} ctx @param {typeof fetch} [ctx.fetch] @param {string} [ctx.token]
|
|
94
|
+
* @returns {BrokerProvider|null}
|
|
95
|
+
*/
|
|
96
|
+
export function makeBroker(cfg, ctx = {}) {
|
|
97
|
+
const bcfg = (cfg && cfg.broker) || {};
|
|
98
|
+
const enabled = bcfg.enabled !== false; // default-on when a broker block exists
|
|
99
|
+
const baseUrl = ctx.baseUrl || bcfg.base_url || bcfg.baseUrl;
|
|
100
|
+
const fetchImpl = ctx.fetch; // required — no implicit global fetch
|
|
101
|
+
if (!enabled || !baseUrl || !fetchImpl) return null;
|
|
102
|
+
return new BrokerProvider({
|
|
103
|
+
baseUrl,
|
|
104
|
+
token: getAgentToken(cfg, ctx),
|
|
105
|
+
fetch: fetchImpl,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Whether a broker is the authoritative writer for this agent. A broker is
|
|
111
|
+
* authoritative when one is configured AND constructible (fetch present).
|
|
112
|
+
*
|
|
113
|
+
* @param {object} cfg
|
|
114
|
+
* @param {object} ctx
|
|
115
|
+
* @returns {boolean}
|
|
116
|
+
*/
|
|
117
|
+
export function brokerIsAuthoritative(cfg, ctx = {}) {
|
|
118
|
+
return makeBroker(cfg, ctx) != null;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Resolve one secret by walking the provider chain in precedence order:
|
|
123
|
+
* explicit override > broker > file (local provider) > env.
|
|
124
|
+
*
|
|
125
|
+
* `explicit` lets a caller pin one provider (used by tooling that already
|
|
126
|
+
* knows where a value lives). Every read fails open: a provider that throws
|
|
127
|
+
* or returns null is skipped and the chain continues. Returns the first
|
|
128
|
+
* non-null value, or null if nothing in the chain has it.
|
|
129
|
+
*
|
|
130
|
+
* @param {string} name
|
|
131
|
+
* @param {object} args
|
|
132
|
+
* @param {object} args.cfg parsed config/secrets.yaml
|
|
133
|
+
* @param {string} [args.agentRoot]
|
|
134
|
+
* @param {Record<string,string>} [args.env]
|
|
135
|
+
* @param {typeof fetch} [args.fetch] injected fetch for the broker link
|
|
136
|
+
* @param {string} [args.token] injected agent token
|
|
137
|
+
* @param {{get:(n:string)=>any}} [args.explicit] an explicit provider to try first
|
|
138
|
+
* @returns {Promise<string|null>}
|
|
139
|
+
*/
|
|
140
|
+
export async function resolveSecret(name, args = {}) {
|
|
141
|
+
const { cfg, agentRoot = ".", env, explicit } = args;
|
|
142
|
+
const ctx = { agentRoot, env, fetch: args.fetch, token: args.token, baseUrl: args.baseUrl };
|
|
143
|
+
|
|
144
|
+
const chain = [];
|
|
145
|
+
if (explicit) chain.push(explicit);
|
|
146
|
+
|
|
147
|
+
const broker = makeBroker(cfg, ctx);
|
|
148
|
+
if (broker) chain.push(broker);
|
|
149
|
+
|
|
150
|
+
// Local provider (file by default). If config explicitly selects `env`, the
|
|
151
|
+
// local provider IS env — we still add an env provider below, but a duplicate
|
|
152
|
+
// read is harmless (both fail open) and keeps the precedence comment honest.
|
|
153
|
+
const local = selectProvider(cfg, { agentRoot, env });
|
|
154
|
+
chain.push(local);
|
|
155
|
+
|
|
156
|
+
// Env is the final fallback so process-injected secrets always resolve even
|
|
157
|
+
// when nothing else is configured.
|
|
158
|
+
if (local.name !== "env") {
|
|
159
|
+
const { EnvProvider } = await import("./providers.mjs");
|
|
160
|
+
chain.push(new EnvProvider({ env }));
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
for (const provider of chain) {
|
|
164
|
+
try {
|
|
165
|
+
const v = await provider.get(name);
|
|
166
|
+
if (v != null && v !== "") return v;
|
|
167
|
+
} catch {
|
|
168
|
+
// fail open — try the next link
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Pull a set of secrets from the broker into the local provider. This is the
|
|
176
|
+
* engine behind `maestro secrets sync`: it makes the local encrypted store the
|
|
177
|
+
* cached, offline-readable mirror of the org's broker.
|
|
178
|
+
*
|
|
179
|
+
* Per-secret semantics: fetch from the broker (fail-open read), then `set` on
|
|
180
|
+
* the local provider (fail-CLOSED write). A secret the broker doesn't have is
|
|
181
|
+
* recorded as `missing`; a local write that throws is recorded as `failed`.
|
|
182
|
+
* The caller (CLI) decides how loudly to surface those.
|
|
183
|
+
*
|
|
184
|
+
* @param {object} args
|
|
185
|
+
* @param {object} args.cfg
|
|
186
|
+
* @param {string[]} args.names secret names to pull (required)
|
|
187
|
+
* @param {string} [args.agentRoot]
|
|
188
|
+
* @param {Record<string,string>} [args.env]
|
|
189
|
+
* @param {typeof fetch} args.fetch injected broker fetch (required for a real pull)
|
|
190
|
+
* @param {string} [args.token]
|
|
191
|
+
* @returns {Promise<{synced:string[], missing:string[], failed:{name:string,error:string}[]}>}
|
|
192
|
+
*/
|
|
193
|
+
export async function syncSecrets(args = {}) {
|
|
194
|
+
const { cfg, agentRoot = ".", env, names } = args;
|
|
195
|
+
if (!Array.isArray(names)) {
|
|
196
|
+
throw new Error("syncSecrets: { names: string[] } is required");
|
|
197
|
+
}
|
|
198
|
+
const ctx = { agentRoot, env, fetch: args.fetch, token: args.token, baseUrl: args.baseUrl };
|
|
199
|
+
const broker = makeBroker(cfg, ctx);
|
|
200
|
+
if (!broker) {
|
|
201
|
+
throw new Error("syncSecrets: no broker configured (need config.broker + an injected fetch)");
|
|
202
|
+
}
|
|
203
|
+
const local = selectProvider(cfg, { agentRoot, env });
|
|
204
|
+
if (local.readonly) {
|
|
205
|
+
throw new Error(`syncSecrets: local provider "${local.name}" is read-only — cannot cache pulled secrets`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = { synced: [], missing: [], failed: [] };
|
|
209
|
+
for (const name of names) {
|
|
210
|
+
let value = null;
|
|
211
|
+
try {
|
|
212
|
+
value = await broker.get(name); // fail-open read
|
|
213
|
+
} catch {
|
|
214
|
+
value = null;
|
|
215
|
+
}
|
|
216
|
+
if (value == null) {
|
|
217
|
+
result.missing.push(name);
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
try {
|
|
221
|
+
await local.set(name, value); // fail-closed write
|
|
222
|
+
result.synced.push(name);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
result.failed.push({ name, error: err.message });
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return result;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Rotate a secret: write `newValue` to the authoritative provider and record
|
|
232
|
+
* names-only rotation metadata. Authoritative = broker when configured, else
|
|
233
|
+
* the local provider. The write is fail-CLOSED (a throw propagates) so a
|
|
234
|
+
* botched rotation never looks like success.
|
|
235
|
+
*
|
|
236
|
+
* The audit line records who/what/when/where — never the value. On a broker
|
|
237
|
+
* rotation we also `set` the local mirror best-effort so the agent can keep
|
|
238
|
+
* reading the new value offline; a mirror failure does NOT fail the rotation
|
|
239
|
+
* (the authoritative write already succeeded) but is noted in the return.
|
|
240
|
+
*
|
|
241
|
+
* @param {string} name
|
|
242
|
+
* @param {string} newValue
|
|
243
|
+
* @param {object} args
|
|
244
|
+
* @param {object} args.cfg
|
|
245
|
+
* @param {string} [args.agentRoot]
|
|
246
|
+
* @param {Record<string,string>} [args.env]
|
|
247
|
+
* @param {typeof fetch} [args.fetch]
|
|
248
|
+
* @param {string} [args.token]
|
|
249
|
+
* @param {string} [args.identity] override the audited agent identity
|
|
250
|
+
* @param {(p:string,r:object)=>boolean} [args.appendAudit] injected audit sink (tests)
|
|
251
|
+
* @returns {Promise<{name:string, provider:string, identity:string, at:string, mirrored:boolean}>}
|
|
252
|
+
*/
|
|
253
|
+
export async function rotateSecret(name, newValue, args = {}) {
|
|
254
|
+
const { cfg, agentRoot = ".", env } = args;
|
|
255
|
+
if (!name || typeof name !== "string") {
|
|
256
|
+
throw new Error("rotateSecret: a secret name is required");
|
|
257
|
+
}
|
|
258
|
+
if (newValue == null || newValue === "") {
|
|
259
|
+
throw new Error(`rotateSecret: a non-empty value is required for "${name}"`);
|
|
260
|
+
}
|
|
261
|
+
const ctx = { agentRoot, env, fetch: args.fetch, token: args.token, baseUrl: args.baseUrl };
|
|
262
|
+
const broker = makeBroker(cfg, ctx);
|
|
263
|
+
const local = selectProvider(cfg, { agentRoot, env });
|
|
264
|
+
|
|
265
|
+
let authoritative;
|
|
266
|
+
if (broker) authoritative = broker;
|
|
267
|
+
else if (!local.readonly) authoritative = local;
|
|
268
|
+
else {
|
|
269
|
+
throw new Error(
|
|
270
|
+
`rotateSecret: no writable authoritative provider (local "${local.name}" is read-only and no broker is configured)`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Fail-closed authoritative write.
|
|
275
|
+
await authoritative.set(name, newValue);
|
|
276
|
+
|
|
277
|
+
// Best-effort local mirror when the broker is authoritative so reads stay
|
|
278
|
+
// offline-fast. Never overrides the success of the authoritative write.
|
|
279
|
+
let mirrored = false;
|
|
280
|
+
if (authoritative === broker && !local.readonly) {
|
|
281
|
+
try {
|
|
282
|
+
await local.set(name, newValue);
|
|
283
|
+
mirrored = true;
|
|
284
|
+
} catch {
|
|
285
|
+
mirrored = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const identity = getAgentIdentity(cfg, { ...ctx, identity: args.identity });
|
|
290
|
+
const at = new Date().toISOString();
|
|
291
|
+
const record = {
|
|
292
|
+
event: "secret-rotated",
|
|
293
|
+
at,
|
|
294
|
+
identity,
|
|
295
|
+
name, // the NAME is operational metadata; the VALUE is never logged
|
|
296
|
+
name_fingerprint: nameFingerprint(name),
|
|
297
|
+
provider: authoritative.name,
|
|
298
|
+
mirrored,
|
|
299
|
+
};
|
|
300
|
+
const sink = args.appendAudit || appendJsonl;
|
|
301
|
+
sink(join(String(agentRoot), AUDIT_REL), record);
|
|
302
|
+
|
|
303
|
+
return { name, provider: authoritative.name, identity, at, mirrored };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Resolve the path of the names-only rotation audit log for an agent root.
|
|
308
|
+
* Exported so the CLI / doctor can point at it without duplicating the const.
|
|
309
|
+
*
|
|
310
|
+
* @param {string} agentRoot
|
|
311
|
+
* @returns {string}
|
|
312
|
+
*/
|
|
313
|
+
export function auditLogPath(agentRoot) {
|
|
314
|
+
return join(String(agentRoot || "."), AUDIT_REL);
|
|
315
|
+
}
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/secrets/broker.test.mjs — resolution chain, sync, rotate, names-only audit.
|
|
3
|
+
*
|
|
4
|
+
* Covers:
|
|
5
|
+
* • resolveSecret chain precedence: explicit > broker > file > env
|
|
6
|
+
* • resolveSecret fails open through a throwing provider
|
|
7
|
+
* • syncSecrets pulls a set from an INJECTED broker fetch into the local store
|
|
8
|
+
* • syncSecrets reports missing + write failures
|
|
9
|
+
* • rotateSecret writes to the authoritative provider (broker > local)
|
|
10
|
+
* • rotateSecret audits names-only (never the value), with identity + fingerprint
|
|
11
|
+
* • getAgentIdentity / getAgentToken resolution
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
import { promises as fsp, readFileSync, existsSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
|
|
20
|
+
import {
|
|
21
|
+
resolveSecret,
|
|
22
|
+
syncSecrets,
|
|
23
|
+
rotateSecret,
|
|
24
|
+
getAgentIdentity,
|
|
25
|
+
getAgentToken,
|
|
26
|
+
makeBroker,
|
|
27
|
+
brokerIsAuthoritative,
|
|
28
|
+
auditLogPath,
|
|
29
|
+
} from "./broker.mjs";
|
|
30
|
+
import { FileProvider, nameFingerprint } from "./providers.mjs";
|
|
31
|
+
|
|
32
|
+
async function tmpRoot(prefix = "secrets-broker") {
|
|
33
|
+
const path = join(tmpdir(), `${prefix}-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
34
|
+
await fsp.mkdir(path, { recursive: true });
|
|
35
|
+
return path;
|
|
36
|
+
}
|
|
37
|
+
async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
|
|
38
|
+
|
|
39
|
+
/** A fetch stub backed by an in-memory secrets object (offline). */
|
|
40
|
+
function brokerFetchStub(secrets) {
|
|
41
|
+
return async (url, opts = {}) => {
|
|
42
|
+
const u = new URL(url);
|
|
43
|
+
const m = u.pathname.match(/\/secrets\/(.+)$/);
|
|
44
|
+
if (opts.method === "GET" && u.pathname === "/secrets") {
|
|
45
|
+
return { ok: true, status: 200, json: async () => ({ secrets: Object.keys(secrets).map((name) => ({ name })) }) };
|
|
46
|
+
}
|
|
47
|
+
if (opts.method === "GET" && m) {
|
|
48
|
+
const name = decodeURIComponent(m[1]);
|
|
49
|
+
if (!(name in secrets)) return { ok: false, status: 404, json: async () => ({}) };
|
|
50
|
+
return { ok: true, status: 200, json: async () => ({ value: secrets[name] }) };
|
|
51
|
+
}
|
|
52
|
+
if (opts.method === "PUT" && m) {
|
|
53
|
+
secrets[decodeURIComponent(m[1])] = JSON.parse(opts.body).value;
|
|
54
|
+
return { ok: true, status: 200, json: async () => ({}) };
|
|
55
|
+
}
|
|
56
|
+
return { ok: false, status: 400, json: async () => ({}) };
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BROKER_CFG = { provider: "file", broker: { base_url: "https://broker.example", token_env: "MAESTRO_AGENT_TOKEN" } };
|
|
61
|
+
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
// identity + token
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
|
|
66
|
+
test("getAgentIdentity / getAgentToken resolve from ctx then env then cfg", () => {
|
|
67
|
+
assert.equal(getAgentIdentity({}, { identity: "explicit" }), "explicit");
|
|
68
|
+
assert.equal(getAgentIdentity({}, { env: { MAESTRO_AGENT_ID: "robin" } }), "robin");
|
|
69
|
+
assert.equal(getAgentIdentity({ broker: { identity: "cfg-id" } }, { env: {} }), "cfg-id");
|
|
70
|
+
assert.equal(getAgentIdentity({}, { env: {} }), "agent");
|
|
71
|
+
|
|
72
|
+
assert.equal(getAgentToken({}, { token: "tok" }), "tok");
|
|
73
|
+
assert.equal(getAgentToken(BROKER_CFG, { env: { MAESTRO_AGENT_TOKEN: "from-env" } }), "from-env");
|
|
74
|
+
assert.equal(getAgentToken({}, { env: {} }), "");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("makeBroker / brokerIsAuthoritative require config + an injected fetch", () => {
|
|
78
|
+
assert.equal(makeBroker({}, {}), null, "no broker block → null");
|
|
79
|
+
assert.equal(makeBroker(BROKER_CFG, {}), null, "no fetch → null (stay offline)");
|
|
80
|
+
const b = makeBroker(BROKER_CFG, { fetch: async () => ({ ok: true, status: 200, json: async () => ({}) }), token: "t" });
|
|
81
|
+
assert.ok(b && b.name === "broker");
|
|
82
|
+
assert.equal(brokerIsAuthoritative(BROKER_CFG, { fetch: b._fetch }), true);
|
|
83
|
+
assert.equal(brokerIsAuthoritative({}, {}), false);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
// resolveSecret — chain precedence
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
|
|
90
|
+
test("resolveSecret precedence: explicit > broker > file > env", async () => {
|
|
91
|
+
const root = await tmpRoot();
|
|
92
|
+
try {
|
|
93
|
+
// Seed the file store with FILE_KEY; broker has BROKER_KEY + a shadowed key.
|
|
94
|
+
const file = new FileProvider({ agentRoot: root });
|
|
95
|
+
file.set("FILE_KEY", "from-file");
|
|
96
|
+
file.set("SHADOW", "file-shadow");
|
|
97
|
+
const secrets = { BROKER_KEY: "from-broker", SHADOW: "broker-shadow" };
|
|
98
|
+
const fetch = brokerFetchStub(secrets);
|
|
99
|
+
const env = { ENV_KEY: "from-env", SHADOW: "env-shadow" };
|
|
100
|
+
|
|
101
|
+
const base = { cfg: BROKER_CFG, agentRoot: root, env, fetch, token: "t" };
|
|
102
|
+
|
|
103
|
+
// explicit wins over everything
|
|
104
|
+
const explicit = { get: () => "from-explicit" };
|
|
105
|
+
assert.equal(await resolveSecret("SHADOW", { ...base, explicit }), "from-explicit");
|
|
106
|
+
|
|
107
|
+
// broker beats file beats env on the same name
|
|
108
|
+
assert.equal(await resolveSecret("SHADOW", base), "broker-shadow");
|
|
109
|
+
|
|
110
|
+
// broker-only key resolves from the broker
|
|
111
|
+
assert.equal(await resolveSecret("BROKER_KEY", base), "from-broker");
|
|
112
|
+
// file-only key resolves from the file store
|
|
113
|
+
assert.equal(await resolveSecret("FILE_KEY", base), "from-file");
|
|
114
|
+
// env-only key resolves from env (final fallback)
|
|
115
|
+
assert.equal(await resolveSecret("ENV_KEY", base), "from-env");
|
|
116
|
+
// nothing has it
|
|
117
|
+
assert.equal(await resolveSecret("ABSENT", base), null);
|
|
118
|
+
} finally { await rmRoot(root); }
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("resolveSecret fails open through a throwing provider in the chain", async () => {
|
|
122
|
+
const root = await tmpRoot();
|
|
123
|
+
try {
|
|
124
|
+
const file = new FileProvider({ agentRoot: root });
|
|
125
|
+
file.set("K", "from-file");
|
|
126
|
+
const explicit = { get: () => { throw new Error("boom"); } };
|
|
127
|
+
// Explicit throws → chain continues to the file store.
|
|
128
|
+
assert.equal(await resolveSecret("K", { cfg: { provider: "file" }, agentRoot: root, env: {}, explicit }), "from-file");
|
|
129
|
+
} finally { await rmRoot(root); }
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test("resolveSecret with no broker config still reads file then env", async () => {
|
|
133
|
+
const root = await tmpRoot();
|
|
134
|
+
try {
|
|
135
|
+
const file = new FileProvider({ agentRoot: root });
|
|
136
|
+
file.set("ONLY_FILE", "f");
|
|
137
|
+
const args = { cfg: { provider: "file" }, agentRoot: root, env: { ONLY_ENV: "e" } };
|
|
138
|
+
assert.equal(await resolveSecret("ONLY_FILE", args), "f");
|
|
139
|
+
assert.equal(await resolveSecret("ONLY_ENV", args), "e");
|
|
140
|
+
} finally { await rmRoot(root); }
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// ---------------------------------------------------------------------------
|
|
144
|
+
// syncSecrets — pull from injected broker into local store
|
|
145
|
+
// ---------------------------------------------------------------------------
|
|
146
|
+
|
|
147
|
+
test("syncSecrets pulls a set from the broker into the local file store", async () => {
|
|
148
|
+
const root = await tmpRoot();
|
|
149
|
+
try {
|
|
150
|
+
const secrets = { A: "alpha", B: "bravo", C: "charlie" };
|
|
151
|
+
const fetch = brokerFetchStub(secrets);
|
|
152
|
+
const res = await syncSecrets({
|
|
153
|
+
cfg: BROKER_CFG, agentRoot: root, env: {}, fetch, token: "t",
|
|
154
|
+
names: ["A", "B", "MISSING"],
|
|
155
|
+
});
|
|
156
|
+
assert.deepEqual(res.synced.sort(), ["A", "B"]);
|
|
157
|
+
assert.deepEqual(res.missing, ["MISSING"]);
|
|
158
|
+
assert.deepEqual(res.failed, []);
|
|
159
|
+
|
|
160
|
+
// The local store now holds the pulled values, sealed.
|
|
161
|
+
const file = new FileProvider({ agentRoot: root });
|
|
162
|
+
assert.equal(file.get("A"), "alpha");
|
|
163
|
+
assert.equal(file.get("B"), "bravo");
|
|
164
|
+
assert.equal(file.get("MISSING"), null);
|
|
165
|
+
|
|
166
|
+
// And the value never appears in plaintext on disk.
|
|
167
|
+
const raw = readFileSync(join(root, "state/secrets/.secrets.json"), "utf-8");
|
|
168
|
+
assert.ok(!raw.includes("alpha") && !raw.includes("bravo"), "sealed at rest");
|
|
169
|
+
} finally { await rmRoot(root); }
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
test("syncSecrets throws when no broker is configured", async () => {
|
|
173
|
+
const root = await tmpRoot();
|
|
174
|
+
try {
|
|
175
|
+
await assert.rejects(
|
|
176
|
+
() => syncSecrets({ cfg: { provider: "file" }, agentRoot: root, names: ["A"] }),
|
|
177
|
+
/no broker configured/
|
|
178
|
+
);
|
|
179
|
+
} finally { await rmRoot(root); }
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test("syncSecrets requires names array", async () => {
|
|
183
|
+
await assert.rejects(() => syncSecrets({ cfg: BROKER_CFG }), /names: string\[\] \} is required/);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// ---------------------------------------------------------------------------
|
|
187
|
+
// rotateSecret — authoritative write + names-only audit
|
|
188
|
+
// ---------------------------------------------------------------------------
|
|
189
|
+
|
|
190
|
+
test("rotateSecret writes to the local provider and audits names-only", async () => {
|
|
191
|
+
const root = await tmpRoot();
|
|
192
|
+
try {
|
|
193
|
+
const res = await rotateSecret("DB_PASSWORD", "p@ss-NEW", {
|
|
194
|
+
cfg: { provider: "file" }, agentRoot: root, env: { MAESTRO_AGENT_ID: "alex" },
|
|
195
|
+
});
|
|
196
|
+
assert.equal(res.provider, "file");
|
|
197
|
+
assert.equal(res.identity, "alex");
|
|
198
|
+
assert.match(res.at, /^\d{4}-\d{2}-\d{2}T/);
|
|
199
|
+
|
|
200
|
+
// The new value is readable.
|
|
201
|
+
const file = new FileProvider({ agentRoot: root });
|
|
202
|
+
assert.equal(file.get("DB_PASSWORD"), "p@ss-NEW");
|
|
203
|
+
|
|
204
|
+
// Audit line: names + identity + fingerprint, NEVER the value.
|
|
205
|
+
const logPath = auditLogPath(root);
|
|
206
|
+
assert.ok(existsSync(logPath), "audit log written");
|
|
207
|
+
const raw = readFileSync(logPath, "utf-8").trim();
|
|
208
|
+
assert.ok(!raw.includes("p@ss-NEW"), "audit must never contain the secret value");
|
|
209
|
+
const rec = JSON.parse(raw.split("\n").pop());
|
|
210
|
+
assert.equal(rec.event, "secret-rotated");
|
|
211
|
+
assert.equal(rec.name, "DB_PASSWORD");
|
|
212
|
+
assert.equal(rec.identity, "alex");
|
|
213
|
+
assert.equal(rec.provider, "file");
|
|
214
|
+
assert.equal(rec.name_fingerprint, nameFingerprint("DB_PASSWORD"));
|
|
215
|
+
} finally { await rmRoot(root); }
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("rotateSecret writes to the broker (authoritative) and mirrors locally", async () => {
|
|
219
|
+
const root = await tmpRoot();
|
|
220
|
+
try {
|
|
221
|
+
const secrets = {};
|
|
222
|
+
const fetch = brokerFetchStub(secrets);
|
|
223
|
+
const res = await rotateSecret("API_TOKEN", "tok-rotated", {
|
|
224
|
+
cfg: BROKER_CFG, agentRoot: root, env: {}, fetch, token: "agent-tok", identity: "robin",
|
|
225
|
+
});
|
|
226
|
+
assert.equal(res.provider, "broker", "broker is authoritative when configured");
|
|
227
|
+
assert.equal(res.mirrored, true, "local mirror written for offline reads");
|
|
228
|
+
assert.equal(secrets.API_TOKEN, "tok-rotated", "broker received the rotation");
|
|
229
|
+
// Local mirror holds it too.
|
|
230
|
+
assert.equal(new FileProvider({ agentRoot: root }).get("API_TOKEN"), "tok-rotated");
|
|
231
|
+
} finally { await rmRoot(root); }
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("rotateSecret fails closed on an empty value or missing name", async () => {
|
|
235
|
+
const root = await tmpRoot();
|
|
236
|
+
try {
|
|
237
|
+
await assert.rejects(() => rotateSecret("", "v", { cfg: { provider: "file" }, agentRoot: root }), /name is required/);
|
|
238
|
+
await assert.rejects(() => rotateSecret("K", "", { cfg: { provider: "file" }, agentRoot: root }), /non-empty value/);
|
|
239
|
+
} finally { await rmRoot(root); }
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
test("rotateSecret fails closed when the broker write rejects", async () => {
|
|
243
|
+
const root = await tmpRoot();
|
|
244
|
+
try {
|
|
245
|
+
const fetch = async (url, opts) => {
|
|
246
|
+
if (opts.method === "PUT") return { ok: false, status: 503, json: async () => ({}) };
|
|
247
|
+
return { ok: false, status: 404, json: async () => ({}) };
|
|
248
|
+
};
|
|
249
|
+
await assert.rejects(
|
|
250
|
+
() => rotateSecret("K", "v", { cfg: BROKER_CFG, agentRoot: root, env: {}, fetch, token: "t" }),
|
|
251
|
+
/failed \(status 503\)/
|
|
252
|
+
);
|
|
253
|
+
// No audit line on a failed rotation.
|
|
254
|
+
assert.ok(!existsSync(auditLogPath(root)), "no audit on failed authoritative write");
|
|
255
|
+
} finally { await rmRoot(root); }
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
test("rotateSecret fails closed when the local provider is read-only (env) and no broker", async () => {
|
|
259
|
+
const root = await tmpRoot();
|
|
260
|
+
try {
|
|
261
|
+
await assert.rejects(
|
|
262
|
+
() => rotateSecret("K", "v", { cfg: { provider: "env" }, agentRoot: root, env: {} }),
|
|
263
|
+
/no writable authoritative provider/
|
|
264
|
+
);
|
|
265
|
+
} finally { await rmRoot(root); }
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
test("rotateSecret uses an injected audit sink when provided", async () => {
|
|
269
|
+
const root = await tmpRoot();
|
|
270
|
+
try {
|
|
271
|
+
const lines = [];
|
|
272
|
+
await rotateSecret("K", "v", {
|
|
273
|
+
cfg: { provider: "file" }, agentRoot: root, env: {},
|
|
274
|
+
appendAudit: (p, rec) => { lines.push({ p, rec }); return true; },
|
|
275
|
+
});
|
|
276
|
+
assert.equal(lines.length, 1);
|
|
277
|
+
assert.equal(lines[0].rec.name, "K");
|
|
278
|
+
assert.ok(lines[0].p.endsWith("logs/audit/secrets.jsonl"));
|
|
279
|
+
} finally { await rmRoot(root); }
|
|
280
|
+
});
|