@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,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* messaging.test.mjs — the SP10 org messaging seam: send-gate enforcement,
|
|
3
|
+
* wire shape + idempotency header, afterSend hook + source="messaging"
|
|
4
|
+
* attribution, calling, and the inbound projection. Injected client/fetch +
|
|
5
|
+
* send-gate + hook bus + ledger, so no network and no disk writes leak.
|
|
6
|
+
* Run: node --test lib/org/messaging.test.mjs
|
|
7
|
+
*/
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
sendMessage,
|
|
15
|
+
reactMessage,
|
|
16
|
+
fetchHistory,
|
|
17
|
+
listChannels,
|
|
18
|
+
initiateCall,
|
|
19
|
+
joinCall,
|
|
20
|
+
pullInbound,
|
|
21
|
+
isDirectedAtAgent,
|
|
22
|
+
toMessageEvent,
|
|
23
|
+
} from "./messaging.mjs";
|
|
24
|
+
import { PROTOCOL_VERSION } from "./protocol.mjs";
|
|
25
|
+
|
|
26
|
+
const BASE = "https://org.cohortapp.com";
|
|
27
|
+
const TOKEN = "tok-msg";
|
|
28
|
+
const CFG = { org: { cohort: { enabled: true, base: BASE, token: TOKEN, agentId: "agent-1" } } };
|
|
29
|
+
|
|
30
|
+
delete process.env.COHORT_API_TOKEN;
|
|
31
|
+
delete process.env.COHORT_TOKEN;
|
|
32
|
+
delete process.env.MESSAGING_INBOUND_ESCALATE;
|
|
33
|
+
|
|
34
|
+
/** A fake fetch that records calls and returns a canned res frame (mirrors client.test). */
|
|
35
|
+
function fakeFetch(responder) {
|
|
36
|
+
const calls = [];
|
|
37
|
+
const impl = async (url, init) => {
|
|
38
|
+
calls.push({ url, init, headers: (init && init.headers) || {}, body: init && init.body ? JSON.parse(init.body) : null });
|
|
39
|
+
const r = responder ? responder(url, init, calls.length - 1) : null;
|
|
40
|
+
const status = r && typeof r.status === "number" ? r.status : 200;
|
|
41
|
+
const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
|
|
42
|
+
const body = r ? r.body : { ok: true, result: null };
|
|
43
|
+
return { ok, status, headers: { get: () => undefined }, json: async () => body };
|
|
44
|
+
};
|
|
45
|
+
impl.calls = calls;
|
|
46
|
+
return impl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** Allow-all send-gate (the default screen would also allow benign text). */
|
|
50
|
+
const allowGate = async () => ({ allow: true, reason: "" });
|
|
51
|
+
/** A capturing ledger so we can assert source="messaging" without touching disk. */
|
|
52
|
+
function captureLedger() {
|
|
53
|
+
const rows = [];
|
|
54
|
+
const writeLedgerRow = (_catalog, entry) => { rows.push(entry); return { row: entry, written: true, path: "" }; };
|
|
55
|
+
writeLedgerRow.rows = rows;
|
|
56
|
+
return writeLedgerRow;
|
|
57
|
+
}
|
|
58
|
+
/** A hook bus stub that records afterSend emits. */
|
|
59
|
+
function captureBus() {
|
|
60
|
+
const emits = [];
|
|
61
|
+
return { emits, emit: async (ev, ctx) => { emits.push({ ev, ctx }); return { cancelled: false }; } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// --- send-gate enforcement (the load-bearing invariant) --------------------
|
|
65
|
+
|
|
66
|
+
test("sendMessage: a banned-phrase message is BLOCKED by the send-gate and NEVER sent", async () => {
|
|
67
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "m1" } } }));
|
|
68
|
+
// Real send-gate (no gateImpl): "I'm here to help" is a banned AI-tell.
|
|
69
|
+
const frame = await sendMessage(
|
|
70
|
+
{ channel: "C-eng", body: "Hi — I'm here to help with the deploy." },
|
|
71
|
+
{ cfg: CFG, fetchImpl: f, ledgerImpl: null }
|
|
72
|
+
);
|
|
73
|
+
assert.equal(frame.ok, false, "blocked → not ok");
|
|
74
|
+
assert.equal(frame.blocked, true, "marked blocked (policy refusal, not transport)");
|
|
75
|
+
assert.match(frame.error.message, /banned/i);
|
|
76
|
+
assert.equal(f.calls.length, 0, "NO messaging.send was issued for a blocked message");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test("sendMessage: a clean message posts to /v1/messaging.send with Bearer + protocol + idempotency", async () => {
|
|
80
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "m2", ts: "t" } } }));
|
|
81
|
+
const ledger = captureLedger();
|
|
82
|
+
const bus = captureBus();
|
|
83
|
+
const frame = await sendMessage(
|
|
84
|
+
{ channel: "C-eng", body: "Deploy is green on staging.", mentions: ["agent-2"], idempotencyId: "msg-xyz" },
|
|
85
|
+
{ cfg: CFG, fetchImpl: f, gateImpl: allowGate, hookBus: bus, ledgerImpl: ledger }
|
|
86
|
+
);
|
|
87
|
+
assert.equal(frame.ok, true);
|
|
88
|
+
assert.equal(f.calls.length, 1);
|
|
89
|
+
assert.match(f.calls[0].url, /\/v1\/messaging\.send$/);
|
|
90
|
+
assert.equal(f.calls[0].headers.authorization, `Bearer ${TOKEN}`);
|
|
91
|
+
assert.equal(f.calls[0].headers["x-org-protocol"], String(PROTOCOL_VERSION));
|
|
92
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "msg-xyz", "idempotent on the client message id");
|
|
93
|
+
// wire shape
|
|
94
|
+
assert.equal(f.calls[0].body.channel, "C-eng");
|
|
95
|
+
assert.equal(f.calls[0].body.body, "Deploy is green on staging.");
|
|
96
|
+
assert.deepEqual(f.calls[0].body.mentions, ["agent-2"]);
|
|
97
|
+
assert.equal(f.calls[0].body.id, "msg-xyz");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("sendMessage: emits afterSend + attributes a source='messaging' ledger row on success", async () => {
|
|
101
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "m3" } } }));
|
|
102
|
+
const ledger = captureLedger();
|
|
103
|
+
const bus = captureBus();
|
|
104
|
+
await sendMessage(
|
|
105
|
+
{ channel: "C-ops", body: "Status nominal." },
|
|
106
|
+
{ cfg: CFG, agentRoot: "/tmp/agent", fetchImpl: f, gateImpl: allowGate, hookBus: bus, ledgerImpl: ledger }
|
|
107
|
+
);
|
|
108
|
+
assert.equal(bus.emits.length, 1, "one afterSend emit");
|
|
109
|
+
assert.equal(bus.emits[0].ev, "afterSend");
|
|
110
|
+
assert.equal(bus.emits[0].ctx.source, "messaging");
|
|
111
|
+
assert.equal(ledger.rows.length, 1, "one ledger row");
|
|
112
|
+
assert.equal(ledger.rows[0].source, "messaging");
|
|
113
|
+
assert.equal(ledger.rows[0].inputTokens, 0, "zero-token attribution row (not LLM spend)");
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("sendMessage: redactedText from the gate is what hits the wire", async () => {
|
|
117
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "m4" } } }));
|
|
118
|
+
const redactGate = async () => ({ allow: true, reason: "", redactedText: "REDACTED" });
|
|
119
|
+
await sendMessage({ channel: "C", body: "secret" }, { cfg: CFG, fetchImpl: f, gateImpl: redactGate, ledgerImpl: null });
|
|
120
|
+
assert.equal(f.calls[0].body.body, "REDACTED");
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("sendMessage: missing channel/body → BAD_REQUEST and never touches the gate or wire", async () => {
|
|
124
|
+
const f = fakeFetch();
|
|
125
|
+
assert.equal((await sendMessage({ body: "x" }, { cfg: CFG, fetchImpl: f })).error.code, "BAD_REQUEST");
|
|
126
|
+
assert.equal((await sendMessage({ channel: "C" }, { cfg: CFG, fetchImpl: f })).error.code, "BAD_REQUEST");
|
|
127
|
+
assert.equal(f.calls.length, 0);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test("sendMessage: org disabled → fail-open error frame, gate ran but no wire", async () => {
|
|
131
|
+
const f = fakeFetch();
|
|
132
|
+
const frame = await sendMessage({ channel: "C", body: "hello team" }, { cfg: { org: { cohort: { enabled: false } } }, fetchImpl: f, gateImpl: allowGate, ledgerImpl: null });
|
|
133
|
+
assert.equal(frame.ok, false);
|
|
134
|
+
assert.equal(f.calls.length, 0, "no wire when org is disabled");
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("sendMessage: a gate that throws fails CLOSED (no send)", async () => {
|
|
138
|
+
const f = fakeFetch();
|
|
139
|
+
const boom = async () => { throw new Error("gate down"); };
|
|
140
|
+
const frame = await sendMessage({ channel: "C", body: "hello" }, { cfg: CFG, fetchImpl: f, gateImpl: boom, ledgerImpl: null });
|
|
141
|
+
assert.equal(frame.ok, false);
|
|
142
|
+
assert.equal(frame.blocked, true);
|
|
143
|
+
assert.equal(f.calls.length, 0);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
// --- react / history / channels --------------------------------------------
|
|
147
|
+
|
|
148
|
+
test("reactMessage: posts messaging.react and attributes the action", async () => {
|
|
149
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
150
|
+
const ledger = captureLedger();
|
|
151
|
+
const frame = await reactMessage({ channel: "C", messageId: "m1", emoji: "eyes" }, { cfg: CFG, fetchImpl: f, ledgerImpl: ledger });
|
|
152
|
+
assert.equal(frame.ok, true);
|
|
153
|
+
assert.match(f.calls[0].url, /messaging\.react$/);
|
|
154
|
+
assert.equal(ledger.rows[0].source, "messaging");
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("fetchHistory: returns the messages array (fail-open [] on error)", async () => {
|
|
158
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { messages: [{ id: "a" }, { id: "b" }] } } }));
|
|
159
|
+
const msgs = await fetchHistory({ channel: "C", limit: 10 }, { cfg: CFG, fetchImpl: f });
|
|
160
|
+
assert.equal(msgs.length, 2);
|
|
161
|
+
assert.equal(f.calls[0].body.limit, 10);
|
|
162
|
+
const bad = fakeFetch(() => ({ ok: false, status: 500, body: { ok: false, error: { code: "INTERNAL" } } }));
|
|
163
|
+
assert.deepEqual(await fetchHistory({ channel: "C" }, { cfg: CFG, fetchImpl: bad }), []);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
test("listChannels: returns the channels array (fail-open [])", async () => {
|
|
167
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { channels: [{ id: "C1" }] } } }));
|
|
168
|
+
assert.deepEqual(await listChannels({ cfg: CFG, fetchImpl: f }), [{ id: "C1" }]);
|
|
169
|
+
assert.deepEqual(await listChannels({ cfg: { org: { cohort: { enabled: false } } } }), []);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// --- calling ----------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
test("initiateCall: posts calling.start with invitees + attributes the action", async () => {
|
|
175
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { callId: "call-1" } } }));
|
|
176
|
+
const ledger = captureLedger();
|
|
177
|
+
const frame = await initiateCall({ channel: "C", invitees: ["agent-2"], topic: "sync", idempotencyId: "c1" }, { cfg: CFG, fetchImpl: f, ledgerImpl: ledger });
|
|
178
|
+
assert.equal(frame.ok, true);
|
|
179
|
+
assert.match(f.calls[0].url, /calling\.start$/);
|
|
180
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "c1");
|
|
181
|
+
assert.deepEqual(f.calls[0].body.invitees, ["agent-2"]);
|
|
182
|
+
assert.equal(ledger.rows[0].source, "messaging");
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
test("joinCall: posts calling.join (missing callId → BAD_REQUEST)", async () => {
|
|
186
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
187
|
+
assert.equal((await joinCall({}, { cfg: CFG, fetchImpl: f })).error.code, "BAD_REQUEST");
|
|
188
|
+
const frame = await joinCall({ callId: "call-1" }, { cfg: CFG, fetchImpl: f, ledgerImpl: null });
|
|
189
|
+
assert.equal(frame.ok, true);
|
|
190
|
+
assert.match(f.calls[0].url, /calling\.join$/);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// --- inbound projection -----------------------------------------------------
|
|
194
|
+
|
|
195
|
+
test("isDirectedAtAgent: @mention / DM-to / invitee = directed; own echo / ambient = not", () => {
|
|
196
|
+
assert.equal(isDirectedAtAgent({ family: "messaging", payload: { from: "x", mentions: ["me"], body: "hi" } }, "me"), true);
|
|
197
|
+
assert.equal(isDirectedAtAgent({ family: "messaging", payload: { from: "x", to: ["me"], kind: "dm", body: "hi" } }, "me"), true);
|
|
198
|
+
assert.equal(isDirectedAtAgent({ family: "calling", payload: { from: "x", invitees: ["me"], topic: "t" } }, "me"), true);
|
|
199
|
+
assert.equal(isDirectedAtAgent({ family: "messaging", payload: { from: "me", mentions: ["me"], body: "echo" } }, "me"), false, "own echo ignored");
|
|
200
|
+
assert.equal(isDirectedAtAgent({ family: "messaging", payload: { from: "x", body: "ambient channel chatter" } }, "me"), false);
|
|
201
|
+
assert.equal(isDirectedAtAgent({ family: "board", payload: {} }, "me"), false, "non-messaging family ignored");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
test("toMessageEvent: projects a messaging event onto the MessageEvent contract", () => {
|
|
205
|
+
const ev = toMessageEvent({ family: "messaging", seq: 7, at: "2026-06-12T00:00:00Z", payload: { id: "m9", channel: "C-eng", from: "agent-2", fromName: "Robin", body: "ping", mentions: ["me"] } }, "me");
|
|
206
|
+
assert.equal(ev.channel, "cohort");
|
|
207
|
+
assert.equal(ev.message_id, "m9");
|
|
208
|
+
assert.equal(ev.text, "ping");
|
|
209
|
+
assert.equal(ev.channel_id, "C-eng");
|
|
210
|
+
assert.equal(ev.from.id, "agent-2");
|
|
211
|
+
assert.equal(ev.kind, "message");
|
|
212
|
+
assert.equal(ev.priority_signals.mentions_agent, true);
|
|
213
|
+
assert.equal(ev.ingest_source, "cohort");
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test("pullInbound: filters to directed events and advances the cursor to max seq", async () => {
|
|
217
|
+
const f = fakeFetch(() => ({
|
|
218
|
+
body: {
|
|
219
|
+
events: [
|
|
220
|
+
{ family: "messaging", seq: 10, payload: { id: "a", channel: "C", from: "x", body: "ambient" } },
|
|
221
|
+
{ family: "messaging", seq: 11, payload: { id: "b", channel: "C", from: "x", mentions: ["agent-1"], body: "@me hi" } },
|
|
222
|
+
{ family: "calling", seq: 12, payload: { id: "c", callId: "call-9", from: "x", invitees: ["agent-1"], topic: "sync" } },
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
}));
|
|
226
|
+
const { events, nextCursor } = await pullInbound({ cfg: CFG, agentId: "agent-1", cursor: 9, fetchImpl: f });
|
|
227
|
+
assert.equal(events.length, 2, "only the @mention + the call-invite are directed");
|
|
228
|
+
assert.equal(nextCursor, 12, "cursor advanced to the highest seq seen (incl. the skipped ambient)");
|
|
229
|
+
// the events query carried the family filter + cursor
|
|
230
|
+
assert.match(f.calls[0].url, /events\?cursor=9/);
|
|
231
|
+
assert.match(f.calls[0].url, /family=messaging,calling/);
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
test("pullInbound: org disabled → empty, cursor preserved (fail-open)", async () => {
|
|
235
|
+
const { events, nextCursor } = await pullInbound({ cfg: { org: { cohort: { enabled: false } } }, cursor: 5 });
|
|
236
|
+
assert.deepEqual(events, []);
|
|
237
|
+
assert.equal(nextCursor, 5);
|
|
238
|
+
});
|
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/policy.mjs — fetch, verify, cache, and stale-check the signed policy
|
|
3
|
+
* bundle (SPEC-org-server §6 governance-as-code + §G3 detective layer).
|
|
4
|
+
*
|
|
5
|
+
* Policy bundles (communication-style, model-allowlist, action-classification,
|
|
6
|
+
* org/unit budgets) are authored in the protected `org-governance` git repo and
|
|
7
|
+
* distributed by Cohort as SIGNED bundles with a ring rollout. Because the org
|
|
8
|
+
* server is a convenient coordinator and NOT a trusted authority (§G3), every
|
|
9
|
+
* fetched bundle is independently verified here against a PINNED trust root
|
|
10
|
+
* (`config/org-trust/allowed-signers`) that Cohort NEVER serves — a compromised
|
|
11
|
+
* server can withhold or DoS, but cannot forge a bundle the agent will apply.
|
|
12
|
+
*
|
|
13
|
+
* Flow:
|
|
14
|
+
* 1. policyFetch (lib/org/client) GETs /v1/policy → the raw signed bundle.
|
|
15
|
+
* 2. verifyBundle re-checks `content_sig` over the canonical bundle body against
|
|
16
|
+
* the pinned signer PEMs (lib/org/verify.verifySignature). A bad/unsigned/
|
|
17
|
+
* wrong-signer bundle is QUARANTINED (never cached, never applied).
|
|
18
|
+
* 3. A good bundle is cached to `config/cache/policy/<kind>.json` with its
|
|
19
|
+
* verified-at timestamp; isStale() flags a cache older than the freshness
|
|
20
|
+
* window so the daemon can refresh / degrade.
|
|
21
|
+
* 4. handlePolicyStale() is the presence.beat `policy_stale` directive handler:
|
|
22
|
+
* the beat response nudges the agent that a newer bundle exists; this
|
|
23
|
+
* re-fetches + re-verifies and returns the applied/quarantined verdict.
|
|
24
|
+
*
|
|
25
|
+
* G16 (zero-LLM-in-the-coordination-loop): pure fetch + signature verify + file
|
|
26
|
+
* cache. ZERO LLM calls.
|
|
27
|
+
*
|
|
28
|
+
* Node builtins only (node:crypto via verify.mjs, node:fs). Fail-open /
|
|
29
|
+
* quarantine-on-mismatch — never throws past the verdict. `fetchImpl`, `now`, and
|
|
30
|
+
* `cacheDir`/`agentRoot` injectable. ESM.
|
|
31
|
+
*
|
|
32
|
+
* @module lib/org/policy
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
"use strict";
|
|
36
|
+
|
|
37
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
38
|
+
import { join, resolve } from "node:path";
|
|
39
|
+
|
|
40
|
+
import { canonicalize } from "./protocol.mjs";
|
|
41
|
+
import { verifySignature } from "./keys.mjs";
|
|
42
|
+
import { sshEd25519ToSpkiPem } from "./verify.mjs";
|
|
43
|
+
import { configFromAgent, policyFetch } from "./client.mjs";
|
|
44
|
+
import { writeJsonAtomic } from "../fs-atomic.mjs";
|
|
45
|
+
|
|
46
|
+
/** Default freshness window: a bundle older than this is "stale" (24h). */
|
|
47
|
+
export const DEFAULT_STALE_MS = 24 * 60 * 60 * 1000;
|
|
48
|
+
|
|
49
|
+
// ---------------------------------------------------------------------------
|
|
50
|
+
// Trust root + cache locations
|
|
51
|
+
// ---------------------------------------------------------------------------
|
|
52
|
+
|
|
53
|
+
function agentRootOf(o = {}) {
|
|
54
|
+
return resolve(o.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** The PINNED trust root path (Cohort NEVER serves this). */
|
|
58
|
+
function trustRootPath(o = {}) {
|
|
59
|
+
return o.trustRootPath || join(agentRootOf(o), "config", "org-trust", "allowed-signers");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Where verified bundles are cached. */
|
|
63
|
+
function cacheDir(o = {}) {
|
|
64
|
+
return o.cacheDir ? resolve(o.cacheDir) : join(agentRootOf(o), "config", "cache", "policy");
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function cachePath(kind, o = {}) {
|
|
68
|
+
return join(cacheDir(o), `${sanitizeKind(kind)}.json`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function sanitizeKind(kind) {
|
|
72
|
+
return String(kind || "bundle").replace(/[^a-z0-9._-]/gi, "_");
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Load the pinned signer PEMs for verification. Precedence:
|
|
77
|
+
* 1. explicit `o.trustedSignersPem` (e.g. from configFromAgent)
|
|
78
|
+
* 2. `o.cfg` → configFromAgent(cfg).trustedSignersPem
|
|
79
|
+
* 3. the allowed-signers trust-root FILE (OpenSSH allowed-signers format:
|
|
80
|
+
* `principal namespaces? keytype base64key` per line; we extract PEM-able
|
|
81
|
+
* keys, tolerating both raw PEM blocks and SSH one-liners we can wrap).
|
|
82
|
+
* An EMPTY result means "nothing trusted" → every bundle is quarantined
|
|
83
|
+
* (default-deny). Never throws.
|
|
84
|
+
*
|
|
85
|
+
* @param {object} o - { trustedSignersPem?, cfg?, trustRootPath?, agentRoot? }
|
|
86
|
+
* @returns {string[]} signer PEM strings
|
|
87
|
+
*/
|
|
88
|
+
export function loadTrustedSigners(o = {}) {
|
|
89
|
+
if (Array.isArray(o.trustedSignersPem) && o.trustedSignersPem.length) {
|
|
90
|
+
return o.trustedSignersPem.filter((s) => typeof s === "string" && s);
|
|
91
|
+
}
|
|
92
|
+
if (o.cfg) {
|
|
93
|
+
const c = configFromAgent(o.cfg);
|
|
94
|
+
if (Array.isArray(c.trustedSignersPem) && c.trustedSignersPem.length) return c.trustedSignersPem;
|
|
95
|
+
}
|
|
96
|
+
return parseAllowedSigners(trustRootPath(o));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Parse the trust-root file into signer PEMs. Tolerant — accepts BOTH formats the
|
|
101
|
+
* trust root may carry:
|
|
102
|
+
* 1. raw `-----BEGIN PUBLIC KEY-----` SPKI blocks (returned verbatim), and
|
|
103
|
+
* 2. OpenSSH allowed-signers one-liners (`principal namespaces? ssh-ed25519
|
|
104
|
+
* base64key comment`), converted to SPKI PEM via verify.sshEd25519ToSpkiPem
|
|
105
|
+
* so the SPEC-format (allowed-signers) trust root loads directly.
|
|
106
|
+
* Blank lines and `#` comments are ignored; an unparseable line is skipped (not
|
|
107
|
+
* fatal). Returns [] when the file is missing/unreadable (default-deny). Never throws.
|
|
108
|
+
*/
|
|
109
|
+
export function parseAllowedSigners(path) {
|
|
110
|
+
if (!path || !existsSync(path)) return [];
|
|
111
|
+
let raw;
|
|
112
|
+
try { raw = readFileSync(path, "utf-8"); } catch { return []; }
|
|
113
|
+
const pems = [];
|
|
114
|
+
// First, lift any inline SPKI PEM blocks verbatim and blank them out so the
|
|
115
|
+
// line scan below doesn't re-parse their base64 bodies as SSH keys.
|
|
116
|
+
const re = /-----BEGIN [A-Z ]*PUBLIC KEY-----[\s\S]*?-----END [A-Z ]*PUBLIC KEY-----/g;
|
|
117
|
+
let m;
|
|
118
|
+
while ((m = re.exec(raw)) !== null) pems.push(m[0]);
|
|
119
|
+
const withoutPem = raw.replace(re, "");
|
|
120
|
+
// Then scan the remaining lines for OpenSSH allowed-signers entries.
|
|
121
|
+
for (const line of withoutPem.split(/\r?\n/)) {
|
|
122
|
+
const trimmed = line.trim();
|
|
123
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
124
|
+
if (!trimmed.includes("ssh-ed25519")) continue;
|
|
125
|
+
const pem = sshEd25519ToSpkiPem(trimmed);
|
|
126
|
+
if (pem) pems.push(pem);
|
|
127
|
+
}
|
|
128
|
+
return pems;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ---------------------------------------------------------------------------
|
|
132
|
+
// Verify a fetched bundle against the pinned trust root
|
|
133
|
+
// ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Build the canonical signing material for a bundle: the governance content with
|
|
137
|
+
* the envelope stripped. The envelope is everything the server signs OVER, not
|
|
138
|
+
* the signature itself NOR the publish-time metadata — so we drop the signature
|
|
139
|
+
* fields (`content_sig`/`sig`/`signature`/`signer`) AND the publisher identity +
|
|
140
|
+
* timestamp (`publishedBy`/`publishedAt`, snake-case variants too), which the
|
|
141
|
+
* server stamps at insert time and therefore cannot include in the signature it
|
|
142
|
+
* computes before the row exists. What remains — name, version, body, kind,
|
|
143
|
+
* source provenance — is exactly what the server's asymmetric signer signs over
|
|
144
|
+
* (services/cohort policy.publish signBundle), so a fetched bundle verifies
|
|
145
|
+
* end-to-end against the PINNED root. Matches the envelope convention in
|
|
146
|
+
* lib/org/verify.mjs. Pure.
|
|
147
|
+
*/
|
|
148
|
+
function bundleSigningMaterial(bundle) {
|
|
149
|
+
const {
|
|
150
|
+
content_sig, sig, signature, signer,
|
|
151
|
+
publishedBy, publishedAt, published_by, published_at,
|
|
152
|
+
...body
|
|
153
|
+
} = bundle || {};
|
|
154
|
+
void content_sig; void sig; void signature; void signer;
|
|
155
|
+
void publishedBy; void publishedAt; void published_by; void published_at;
|
|
156
|
+
return canonicalize(body);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Verify a policy bundle against the PINNED signer PEMs. Tries every pinned
|
|
161
|
+
* signer; accepts iff one verifies the `content_sig` over the canonical body. An
|
|
162
|
+
* unsigned, wrong-signer, or tampered bundle is rejected. An EMPTY pinned list
|
|
163
|
+
* means "nothing trusted" → reject (default-deny). Never throws.
|
|
164
|
+
*
|
|
165
|
+
* @param {object} bundle the fetched bundle (must carry content_sig / sig)
|
|
166
|
+
* @param {string[]} trustedSignersPem pinned SPKI PEMs
|
|
167
|
+
* @returns {{ok:true, signer:string}|{ok:false, reason:string}}
|
|
168
|
+
*/
|
|
169
|
+
export function verifyBundle(bundle, trustedSignersPem) {
|
|
170
|
+
if (!bundle || typeof bundle !== "object") return { ok: false, reason: "bundle not an object" };
|
|
171
|
+
const sig = bundle.content_sig || bundle.sig || bundle.signature;
|
|
172
|
+
if (!sig || typeof sig !== "string") return { ok: false, reason: "bundle missing signature" };
|
|
173
|
+
const signers = Array.isArray(trustedSignersPem) ? trustedSignersPem.filter(Boolean) : [];
|
|
174
|
+
if (signers.length === 0) return { ok: false, reason: "no pinned signers (default-deny)" };
|
|
175
|
+
const material = Buffer.from(bundleSigningMaterial(bundle));
|
|
176
|
+
for (const pem of signers) {
|
|
177
|
+
if (verifySignature(pem, material, sig)) return { ok: true, signer: pem };
|
|
178
|
+
}
|
|
179
|
+
return { ok: false, reason: "signature not from any pinned signer" };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Cache read/write
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Read a cached, previously-verified bundle for a kind. Returns the cache record
|
|
188
|
+
* `{ bundle, verifiedAt, signer }` or null. Never throws.
|
|
189
|
+
* @param {string} kind
|
|
190
|
+
* @param {object} o - { cacheDir?, agentRoot? }
|
|
191
|
+
*/
|
|
192
|
+
export function readCachedBundle(kind, o = {}) {
|
|
193
|
+
const path = cachePath(kind, o);
|
|
194
|
+
if (!existsSync(path)) return null;
|
|
195
|
+
try { return JSON.parse(readFileSync(path, "utf-8")); } catch { return null; }
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Cache a verified bundle. Atomic write. Never throws. Returns the cache record.
|
|
200
|
+
* @param {string} kind @param {object} bundle @param {object} o - { now?, cacheDir?, agentRoot?, signer? }
|
|
201
|
+
*/
|
|
202
|
+
export function cacheBundle(kind, bundle, o = {}) {
|
|
203
|
+
const now = (typeof o.now === "function" ? o.now : Date.now)();
|
|
204
|
+
const record = {
|
|
205
|
+
kind: sanitizeKind(kind),
|
|
206
|
+
bundle,
|
|
207
|
+
version: bundle && bundle.version != null ? bundle.version : null,
|
|
208
|
+
source_commit: bundle && bundle.source_commit ? bundle.source_commit : null,
|
|
209
|
+
verifiedAt: new Date(now).toISOString(),
|
|
210
|
+
verifiedAtMs: now,
|
|
211
|
+
signer: o.signer || null,
|
|
212
|
+
};
|
|
213
|
+
try { writeJsonAtomic(cachePath(kind, o), record); } catch { /* fail-open */ }
|
|
214
|
+
return record;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Is the cached bundle for a kind stale (older than the freshness window, or
|
|
219
|
+
* absent)? Absent ⇒ stale (we have nothing fresh). Never throws.
|
|
220
|
+
* @param {string} kind
|
|
221
|
+
* @param {object} o - { now?, staleMs?, cacheDir?, agentRoot? }
|
|
222
|
+
* @returns {boolean}
|
|
223
|
+
*/
|
|
224
|
+
export function isStale(kind, o = {}) {
|
|
225
|
+
const record = readCachedBundle(kind, o);
|
|
226
|
+
if (!record) return true;
|
|
227
|
+
const now = (typeof o.now === "function" ? o.now : Date.now)();
|
|
228
|
+
const staleMs = Number.isFinite(o.staleMs) && o.staleMs > 0 ? o.staleMs : DEFAULT_STALE_MS;
|
|
229
|
+
const at = Number.isFinite(record.verifiedAtMs)
|
|
230
|
+
? record.verifiedAtMs
|
|
231
|
+
: (record.verifiedAt ? Date.parse(record.verifiedAt) : NaN);
|
|
232
|
+
if (!Number.isFinite(at)) return true;
|
|
233
|
+
return now - at > staleMs;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ---------------------------------------------------------------------------
|
|
237
|
+
// Fetch → verify → cache
|
|
238
|
+
// ---------------------------------------------------------------------------
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Fetch the current policy bundle, verify it against the PINNED trust root, and
|
|
242
|
+
* (on success) cache it. The single entry point the daemon/setup uses.
|
|
243
|
+
*
|
|
244
|
+
* On a GOOD signature: caches the bundle and returns
|
|
245
|
+
* `{ ok:true, applied:true, bundle, signer, record }`.
|
|
246
|
+
* On a BAD/unsigned/wrong-signer bundle: QUARANTINES — does NOT cache or apply —
|
|
247
|
+
* and returns `{ ok:false, quarantined:true, reason }`. The caller falls back to
|
|
248
|
+
* the last-good cached bundle (readCachedBundle).
|
|
249
|
+
* On a fetch failure (disabled/unreachable): `{ ok:false, reason:"no-bundle" }`.
|
|
250
|
+
*
|
|
251
|
+
* Always resolves (fail-open). G16: no LLM call.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} o - { cfg, kind?, trustedSignersPem?, trustRootPath?, agentRoot?, cacheDir?, now?, fetchImpl? }
|
|
254
|
+
* @returns {Promise<{
|
|
255
|
+
* ok:boolean, applied?:boolean, quarantined?:boolean, reason?:string,
|
|
256
|
+
* bundle?:object, signer?:string, record?:object
|
|
257
|
+
* }>}
|
|
258
|
+
*/
|
|
259
|
+
export async function fetchAndApply(o = {}) {
|
|
260
|
+
const c = configFromAgent(o.cfg || {});
|
|
261
|
+
const bundle = await policyFetch({ base: c.base, token: c.token, kind: o.kind, fetchImpl: o.fetchImpl });
|
|
262
|
+
if (!bundle) return { ok: false, reason: "no-bundle" };
|
|
263
|
+
|
|
264
|
+
const signers = loadTrustedSigners(o);
|
|
265
|
+
const verdict = verifyBundle(bundle, signers);
|
|
266
|
+
if (!verdict.ok) {
|
|
267
|
+
// Quarantine: never cache or apply an unverifiable bundle.
|
|
268
|
+
return { ok: false, quarantined: true, reason: verdict.reason, bundle };
|
|
269
|
+
}
|
|
270
|
+
const kind = o.kind || (bundle && bundle.kind) || "bundle";
|
|
271
|
+
const record = cacheBundle(kind, bundle, { ...o, signer: verdict.signer });
|
|
272
|
+
return { ok: true, applied: true, bundle, signer: verdict.signer, record };
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Handle the presence.beat `policy_stale` directive: a beat response carrying
|
|
277
|
+
* `policy_stale` is the rollout nudge that a newer signed bundle exists. Re-fetch
|
|
278
|
+
* + re-verify (+ cache on success). Returns the fetchAndApply verdict per kind.
|
|
279
|
+
*
|
|
280
|
+
* Accepts either a single kind, an array of kinds, or (when none given) refreshes
|
|
281
|
+
* every kind we already have cached so the whole policy set converges on the nudge.
|
|
282
|
+
*
|
|
283
|
+
* Fail-open: a quarantined or unreachable refresh leaves the last-good cache in
|
|
284
|
+
* place and returns the verdict so the daemon can surface it.
|
|
285
|
+
*
|
|
286
|
+
* @param {object} o - { cfg, kinds?|kind?, trustedSignersPem?, agentRoot?, cacheDir?, now?, fetchImpl? }
|
|
287
|
+
* @returns {Promise<{ refreshed:number, results:Array<{kind:string} & object> }>}
|
|
288
|
+
*/
|
|
289
|
+
export async function handlePolicyStale(o = {}) {
|
|
290
|
+
let kinds = [];
|
|
291
|
+
if (Array.isArray(o.kinds)) kinds = o.kinds.slice();
|
|
292
|
+
else if (o.kind) kinds = [o.kind];
|
|
293
|
+
else kinds = listCachedKinds(o);
|
|
294
|
+
if (kinds.length === 0) kinds = [undefined]; // let the server return the default bundle
|
|
295
|
+
|
|
296
|
+
const results = [];
|
|
297
|
+
let refreshed = 0;
|
|
298
|
+
for (const kind of kinds) {
|
|
299
|
+
const r = await fetchAndApply({ ...o, kind });
|
|
300
|
+
results.push({ kind: kind || (r.bundle && r.bundle.kind) || "bundle", ...r });
|
|
301
|
+
if (r.ok && r.applied) refreshed += 1;
|
|
302
|
+
}
|
|
303
|
+
return { refreshed, results };
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/** List the kinds we currently have cached (by cache filenames). Never throws. */
|
|
307
|
+
export function listCachedKinds(o = {}) {
|
|
308
|
+
const dir = cacheDir(o);
|
|
309
|
+
if (!existsSync(dir)) return [];
|
|
310
|
+
try {
|
|
311
|
+
return readdirSync(dir)
|
|
312
|
+
.filter((f) => f.endsWith(".json"))
|
|
313
|
+
.map((f) => f.slice(0, -5));
|
|
314
|
+
} catch {
|
|
315
|
+
return [];
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* The canonical asymmetric signing material for a policy bundle. Exported so the
|
|
321
|
+
* SERVER's signer (services/cohort policy.publish) signs over the EXACT bytes
|
|
322
|
+
* the client reconstructs at verify time — the single source of truth for the
|
|
323
|
+
* material, so the two halves can never drift. Pure.
|
|
324
|
+
* @param {object} bundle a bundle (or projected bundle) — envelope fields ignored
|
|
325
|
+
* @returns {string} canonical JSON of the signed content
|
|
326
|
+
*/
|
|
327
|
+
export function policyBundleSigningMaterial(bundle) {
|
|
328
|
+
return bundleSigningMaterial(bundle);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
export const _internals = { bundleSigningMaterial, cachePath, sanitizeKind, trustRootPath, cacheDir };
|
|
332
|
+
|
|
333
|
+
export default {
|
|
334
|
+
DEFAULT_STALE_MS,
|
|
335
|
+
loadTrustedSigners,
|
|
336
|
+
parseAllowedSigners,
|
|
337
|
+
policyBundleSigningMaterial,
|
|
338
|
+
verifyBundle,
|
|
339
|
+
readCachedBundle,
|
|
340
|
+
cacheBundle,
|
|
341
|
+
isStale,
|
|
342
|
+
fetchAndApply,
|
|
343
|
+
handlePolicyStale,
|
|
344
|
+
listCachedKinds,
|
|
345
|
+
};
|