@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,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/integration-tools.mjs — the daemon's versioned cache of the workspace's
|
|
3
|
+
* GRANTED integration (plugin) tools, and its persistence to disk (SP5).
|
|
4
|
+
*
|
|
5
|
+
* THE GAP this closes: SP1 gave hq the `integration.toolsetVersion` (cheap ETag
|
|
6
|
+
* poll) + `integration.listAgentTools` (the authoritative granted-tool manifest,
|
|
7
|
+
* SCHEMAS ONLY — never plugin code or secrets) reads. This module is the agent
|
|
8
|
+
* half: it polls the version, refetches the manifest only when it changed, maps
|
|
9
|
+
* each granted tool into the canonical tool-surface descriptor, and — so per
|
|
10
|
+
* session `cohort-mcp` subprocesses and the native tool snapshot pick up the
|
|
11
|
+
* current set WITHOUT a daemon restart — persists the versioned set to
|
|
12
|
+
* `state/org/toolset.json`.
|
|
13
|
+
*
|
|
14
|
+
* Both SP1 endpoints are protocol METHODS (POST) not GET reads, so BOTH ride the
|
|
15
|
+
* client's `call(method, params, o)` — which returns a res frame `{ok, result}`.
|
|
16
|
+
* `call` is injected (deps) so tests never touch the network.
|
|
17
|
+
*
|
|
18
|
+
* Tool name uses the SP3/SP4 convention (`plugin__<slug>__<tool>`) so a tool is
|
|
19
|
+
* named IDENTICALLY across the in-app responder, the hq MCP server, and here.
|
|
20
|
+
* Execution proxies back to hq through `integration.invokeTool` (SP5) — the
|
|
21
|
+
* descriptor carries `{integration:{slug,tool}}` so `executeOrgTool` can send the
|
|
22
|
+
* right params.
|
|
23
|
+
*
|
|
24
|
+
* FAIL-OPEN like the rest of the org client: any transport/parse hiccup returns
|
|
25
|
+
* the last-good cache; a refresh never throws past the boundary.
|
|
26
|
+
*
|
|
27
|
+
* Node builtins only. ESM.
|
|
28
|
+
*
|
|
29
|
+
* @module lib/org/integration-tools
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
"use strict";
|
|
33
|
+
|
|
34
|
+
import { existsSync, mkdirSync, writeFileSync, renameSync } from "node:fs";
|
|
35
|
+
import { join, resolve } from "node:path";
|
|
36
|
+
|
|
37
|
+
/** In-memory cache: the current version + mapped tools + when we last fetched. */
|
|
38
|
+
let cache = { version: 0, tools: [], fetchedAt: 0 };
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Resolve the agent root the same env-first way `resolveOrgToolConfig` /
|
|
42
|
+
* lib/agent-root do, so the long-lived daemon and a per-session subprocess agree
|
|
43
|
+
* on the `state/org/toolset.json` location. Fail-open: worst case is cwd.
|
|
44
|
+
* @param {string} [agentRoot]
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function resolveAgentRoot(agentRoot) {
|
|
48
|
+
return resolve(
|
|
49
|
+
agentRoot ||
|
|
50
|
+
process.env.COHORT_AGENT_ROOT ||
|
|
51
|
+
process.env.AGENT_ROOT ||
|
|
52
|
+
process.env.AGENT_DIR ||
|
|
53
|
+
process.cwd(),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The canonical cross-process cache path: `<agentRoot>/state/org/toolset.json`.
|
|
59
|
+
* The daemon WRITES it on refresh (Task 4); `getOrgTools()` + the native plane
|
|
60
|
+
* READ it (tool-surface.loadIntegrationToolsFromDisk). Shape:
|
|
61
|
+
* `{ version:number, tools:SurfaceTool[], fetchedAt:number }`.
|
|
62
|
+
* @param {string} [agentRoot]
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function integrationToolsetPath(agentRoot) {
|
|
66
|
+
return join(resolveAgentRoot(agentRoot), "state", "org", "toolset.json");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Build the SP3/SP4 tool name; sanitized + capped at 64 chars. */
|
|
70
|
+
function mcpName(slug, tool) {
|
|
71
|
+
return `plugin__${String(slug).replace(/[^a-z0-9]/gi, "_")}__${tool}`.slice(
|
|
72
|
+
0,
|
|
73
|
+
64,
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Map a `listAgentTools` manifest entry → the canonical tool-surface descriptor
|
|
79
|
+
* (the SAME shape ORG_TOOLS uses), carrying an `integration:{slug,tool}` hint so
|
|
80
|
+
* `executeOrgTool` can proxy to `integration.invokeTool`.
|
|
81
|
+
* @param {object} t
|
|
82
|
+
* @returns {object}
|
|
83
|
+
*/
|
|
84
|
+
export function toSurfaceTool(t) {
|
|
85
|
+
return {
|
|
86
|
+
name: mcpName(t.slug, t.name),
|
|
87
|
+
title: t.name,
|
|
88
|
+
description: `${t.slug}: ${t.description ?? ""}`.trim(),
|
|
89
|
+
input_schema:
|
|
90
|
+
t.inputSchema && typeof t.inputSchema === "object"
|
|
91
|
+
? t.inputSchema
|
|
92
|
+
: { type: "object" },
|
|
93
|
+
access: "write",
|
|
94
|
+
binding: { kind: "rpc", method: "integration.invokeTool" },
|
|
95
|
+
// Carried so executeOrgTool passes {slug, tool} to invokeTool (Task 3).
|
|
96
|
+
integration: { slug: t.slug, tool: t.name },
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Cheap version poll + conditional refetch. `deps` inject `call` (+ optional
|
|
102
|
+
* `agentMemberId`, `now`) for tests. Both SP1 endpoints are protocol METHODS, so
|
|
103
|
+
* both go through `call` (res frame `{ok, result}`); a transport failure or a
|
|
104
|
+
* bad shape returns the last-good cache. Returns the (possibly unchanged) cache.
|
|
105
|
+
*
|
|
106
|
+
* @param {object} deps - { call, agentMemberId?, now? }
|
|
107
|
+
* @returns {Promise<{version:number, tools:object[], fetchedAt:number}>}
|
|
108
|
+
*/
|
|
109
|
+
export async function maybeRefresh({ call, agentMemberId, now } = {}) {
|
|
110
|
+
if (typeof call !== "function") return cache;
|
|
111
|
+
const vres = await call("integration.toolsetVersion", {}).catch(() => null);
|
|
112
|
+
const version = vres && vres.result ? vres.result.version : undefined;
|
|
113
|
+
if (typeof version !== "number") return cache;
|
|
114
|
+
// A matching version with a populated set is a no-op (the cheap poll's payoff).
|
|
115
|
+
if (version === cache.version && cache.tools.length) return cache;
|
|
116
|
+
|
|
117
|
+
const tres = await call(
|
|
118
|
+
"integration.listAgentTools",
|
|
119
|
+
agentMemberId ? { agentMemberId } : {},
|
|
120
|
+
).catch(() => null);
|
|
121
|
+
const tools = tres && tres.result ? tres.result.tools : undefined;
|
|
122
|
+
if (!Array.isArray(tools)) return cache;
|
|
123
|
+
|
|
124
|
+
cache = {
|
|
125
|
+
version,
|
|
126
|
+
tools: tools.map(toSurfaceTool),
|
|
127
|
+
fetchedAt: typeof now === "function" ? now() : Date.now(),
|
|
128
|
+
};
|
|
129
|
+
return cache;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** The current mapped integration tools (surface-shaped). */
|
|
133
|
+
export function getIntegrationTools() {
|
|
134
|
+
return cache.tools;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** The current cached toolset version (0 before the first successful refresh). */
|
|
138
|
+
export function currentToolsetVersion() {
|
|
139
|
+
return cache.version;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Persist the versioned tool set to `state/org/toolset.json` (atomic write via a
|
|
144
|
+
* temp file + rename) so per-session subprocesses + the native snapshot read the
|
|
145
|
+
* current set. Fail-open: never throws (a persistence hiccup must not break the
|
|
146
|
+
* beat that calls it).
|
|
147
|
+
* @param {string} agentRoot
|
|
148
|
+
* @param {{version:number, tools:object[]}} snapshot
|
|
149
|
+
* @returns {boolean} true when written
|
|
150
|
+
*/
|
|
151
|
+
export function writeIntegrationToolsToDisk(agentRoot, snapshot) {
|
|
152
|
+
try {
|
|
153
|
+
const path = integrationToolsetPath(agentRoot);
|
|
154
|
+
const dir = join(resolveAgentRoot(agentRoot), "state", "org");
|
|
155
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
156
|
+
const doc = {
|
|
157
|
+
version: Number(snapshot && snapshot.version) || 0,
|
|
158
|
+
tools: Array.isArray(snapshot && snapshot.tools) ? snapshot.tools : [],
|
|
159
|
+
fetchedAt: Date.now(),
|
|
160
|
+
};
|
|
161
|
+
const tmp = `${path}.${process.pid}.tmp`;
|
|
162
|
+
writeFileSync(tmp, JSON.stringify(doc, null, 2));
|
|
163
|
+
renameSync(tmp, path);
|
|
164
|
+
return true;
|
|
165
|
+
} catch {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Reset the in-memory cache — test-only. */
|
|
171
|
+
export function _resetForTest() {
|
|
172
|
+
cache = { version: 0, tools: [], fetchedAt: 0 };
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export default {
|
|
176
|
+
maybeRefresh,
|
|
177
|
+
getIntegrationTools,
|
|
178
|
+
currentToolsetVersion,
|
|
179
|
+
writeIntegrationToolsToDisk,
|
|
180
|
+
integrationToolsetPath,
|
|
181
|
+
toSurfaceTool,
|
|
182
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* integration-tools.test.mjs — the versioned integration-tools cache (SP5).
|
|
3
|
+
*
|
|
4
|
+
* Hermetic: the hq fetch rides an injected `call` (no network). Disk tests write
|
|
5
|
+
* into a fresh mkdtemp root. Run: node --test lib/org/integration-tools.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
import { test, beforeEach, after } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { mkdtempSync, rmSync, readFileSync, existsSync } from "node:fs";
|
|
13
|
+
import { tmpdir } from "node:os";
|
|
14
|
+
import { join } from "node:path";
|
|
15
|
+
|
|
16
|
+
import {
|
|
17
|
+
maybeRefresh,
|
|
18
|
+
getIntegrationTools,
|
|
19
|
+
currentToolsetVersion,
|
|
20
|
+
writeIntegrationToolsToDisk,
|
|
21
|
+
integrationToolsetPath,
|
|
22
|
+
_resetForTest,
|
|
23
|
+
} from "./integration-tools.mjs";
|
|
24
|
+
|
|
25
|
+
/** A fake `call` that answers the two SP1 read-methods from a canned set. */
|
|
26
|
+
function makeCall({ version, tools }) {
|
|
27
|
+
const calls = [];
|
|
28
|
+
const call = async (method, params) => {
|
|
29
|
+
calls.push({ method, params });
|
|
30
|
+
if (method === "integration.toolsetVersion") {
|
|
31
|
+
return { ok: true, result: { version, updatedAt: null } };
|
|
32
|
+
}
|
|
33
|
+
if (method === "integration.listAgentTools") {
|
|
34
|
+
return { ok: true, result: { version, tools } };
|
|
35
|
+
}
|
|
36
|
+
return { ok: false, error: { code: "NOT_FOUND", message: "?" } };
|
|
37
|
+
};
|
|
38
|
+
return { call, calls };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const TOOLS_V5 = [
|
|
42
|
+
{
|
|
43
|
+
slug: "open-meteo",
|
|
44
|
+
name: "forecast",
|
|
45
|
+
description: "Get the forecast",
|
|
46
|
+
inputSchema: { type: "object", properties: { city: { type: "string" } } },
|
|
47
|
+
outputSchema: { type: "object" },
|
|
48
|
+
},
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
const roots = [];
|
|
52
|
+
function freshRoot() {
|
|
53
|
+
const r = mkdtempSync(join(tmpdir(), "int-tools-"));
|
|
54
|
+
roots.push(r);
|
|
55
|
+
return r;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
beforeEach(() => _resetForTest());
|
|
59
|
+
after(() => {
|
|
60
|
+
for (const r of roots) {
|
|
61
|
+
try {
|
|
62
|
+
rmSync(r, { recursive: true, force: true });
|
|
63
|
+
} catch {
|
|
64
|
+
/* best-effort */
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("first refresh populates the cache from version + tools", async () => {
|
|
70
|
+
const { call, calls } = makeCall({ version: 5, tools: TOOLS_V5 });
|
|
71
|
+
const cache = await maybeRefresh({ call });
|
|
72
|
+
assert.equal(cache.version, 5);
|
|
73
|
+
assert.equal(currentToolsetVersion(), 5);
|
|
74
|
+
const got = getIntegrationTools();
|
|
75
|
+
assert.equal(got.length, 1);
|
|
76
|
+
const t = got[0];
|
|
77
|
+
assert.equal(t.name, "plugin__open_meteo__forecast");
|
|
78
|
+
assert.equal(t.access, "write");
|
|
79
|
+
assert.equal(t.binding.kind, "rpc");
|
|
80
|
+
assert.equal(t.binding.method, "integration.invokeTool");
|
|
81
|
+
assert.deepEqual(t.integration, { slug: "open-meteo", tool: "forecast" });
|
|
82
|
+
assert.deepEqual(t.input_schema, TOOLS_V5[0].inputSchema);
|
|
83
|
+
// Both reads happened on the first refresh.
|
|
84
|
+
assert.deepEqual(
|
|
85
|
+
calls.map((c) => c.method),
|
|
86
|
+
["integration.toolsetVersion", "integration.listAgentTools"],
|
|
87
|
+
);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test("same version → no refetch of listAgentTools", async () => {
|
|
91
|
+
const first = makeCall({ version: 5, tools: TOOLS_V5 });
|
|
92
|
+
await maybeRefresh({ call: first.call });
|
|
93
|
+
const second = makeCall({ version: 5, tools: TOOLS_V5 });
|
|
94
|
+
await maybeRefresh({ call: second.call });
|
|
95
|
+
assert.deepEqual(
|
|
96
|
+
second.calls.map((c) => c.method),
|
|
97
|
+
["integration.toolsetVersion"],
|
|
98
|
+
"a matching version polls the cheap version read only",
|
|
99
|
+
);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test("a bumped version refetches + swaps in the new tools", async () => {
|
|
103
|
+
await maybeRefresh(makeCall({ version: 5, tools: TOOLS_V5 }));
|
|
104
|
+
const v6Tools = [
|
|
105
|
+
{ slug: "stripe", name: "list_charges", description: "List charges", inputSchema: {} },
|
|
106
|
+
];
|
|
107
|
+
await maybeRefresh(makeCall({ version: 6, tools: v6Tools }));
|
|
108
|
+
assert.equal(currentToolsetVersion(), 6);
|
|
109
|
+
const names = getIntegrationTools().map((t) => t.name);
|
|
110
|
+
assert.deepEqual(names, ["plugin__stripe__list_charges"]);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("passes agentMemberId through to listAgentTools when given", async () => {
|
|
114
|
+
const { call, calls } = makeCall({ version: 1, tools: TOOLS_V5 });
|
|
115
|
+
await maybeRefresh({ call, agentMemberId: "wm_agent" });
|
|
116
|
+
const list = calls.find((c) => c.method === "integration.listAgentTools");
|
|
117
|
+
assert.deepEqual(list.params, { agentMemberId: "wm_agent" });
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("a non-numeric version leaves the cache untouched (fail-open)", async () => {
|
|
121
|
+
const call = async () => ({ ok: true, result: {} }); // no version
|
|
122
|
+
const cache = await maybeRefresh({ call });
|
|
123
|
+
assert.equal(cache.version, 0);
|
|
124
|
+
assert.equal(getIntegrationTools().length, 0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test("a throwing call fails open to the existing cache", async () => {
|
|
128
|
+
await maybeRefresh(makeCall({ version: 2, tools: TOOLS_V5 }));
|
|
129
|
+
const boom = async () => {
|
|
130
|
+
throw new Error("network down");
|
|
131
|
+
};
|
|
132
|
+
const cache = await maybeRefresh({ call: boom });
|
|
133
|
+
assert.equal(cache.version, 2, "kept the last-good cache");
|
|
134
|
+
assert.equal(getIntegrationTools().length, 1);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
test("the mcp name sanitizes non-alnum slug chars and caps at 64", async () => {
|
|
138
|
+
const tools = [
|
|
139
|
+
{ slug: "My.Weird-Slug", name: "do_thing", description: "d", inputSchema: {} },
|
|
140
|
+
];
|
|
141
|
+
await maybeRefresh(makeCall({ version: 1, tools }));
|
|
142
|
+
const [t] = getIntegrationTools();
|
|
143
|
+
assert.equal(t.name, "plugin__My_Weird_Slug__do_thing");
|
|
144
|
+
assert.ok(t.name.length <= 64);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
test("writeIntegrationToolsToDisk persists {version,tools} atomically", async () => {
|
|
148
|
+
const root = freshRoot();
|
|
149
|
+
await maybeRefresh(makeCall({ version: 9, tools: TOOLS_V5 }));
|
|
150
|
+
writeIntegrationToolsToDisk(root, {
|
|
151
|
+
version: currentToolsetVersion(),
|
|
152
|
+
tools: getIntegrationTools(),
|
|
153
|
+
});
|
|
154
|
+
const p = integrationToolsetPath(root);
|
|
155
|
+
assert.ok(existsSync(p), "toolset.json exists");
|
|
156
|
+
const doc = JSON.parse(readFileSync(p, "utf8"));
|
|
157
|
+
assert.equal(doc.version, 9);
|
|
158
|
+
assert.equal(doc.tools.length, 1);
|
|
159
|
+
assert.equal(doc.tools[0].name, "plugin__open_meteo__forecast");
|
|
160
|
+
});
|
package/lib/org/keys.mjs
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/keys.mjs — device keypair for the Cohort org binding.
|
|
3
|
+
*
|
|
4
|
+
* Every paired agent owns one long-lived Ed25519 device key. The PUBLIC key is
|
|
5
|
+
* presented in the pairing handshake (`pairing.request { pubkey }`) and pinned
|
|
6
|
+
* by peers / the verify layer; the PRIVATE key never leaves the machine and is
|
|
7
|
+
* used to sign outbound material (and, symmetrically, to let the detective layer
|
|
8
|
+
* verify signatures the server makes over snapshots — see lib/org/verify.mjs).
|
|
9
|
+
*
|
|
10
|
+
* The private key is stored 0600 under `state/org/` and loaded-or-created on
|
|
11
|
+
* first use, so a freshly-provisioned agent self-bootstraps a stable identity
|
|
12
|
+
* that survives restarts. Keys are SubjectPublicKeyInfo / PKCS#8 PEM so they are
|
|
13
|
+
* portable across `node:crypto`, OpenSSL, and any Ed25519 verifier.
|
|
14
|
+
*
|
|
15
|
+
* Pure node:crypto + node:fs (atomic write via lib/fs-atomic). Fully testable:
|
|
16
|
+
* `ensureDeviceKey` is the only fs-touching entry point; `verifySignature` and
|
|
17
|
+
* the returned `sign` are pure over their inputs.
|
|
18
|
+
*
|
|
19
|
+
* @module lib/org/keys
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
"use strict";
|
|
23
|
+
|
|
24
|
+
import {
|
|
25
|
+
generateKeyPairSync,
|
|
26
|
+
createPrivateKey,
|
|
27
|
+
createPublicKey,
|
|
28
|
+
sign as edSign,
|
|
29
|
+
verify as edVerify,
|
|
30
|
+
} from "node:crypto";
|
|
31
|
+
import { readFileSync, existsSync, chmodSync } from "node:fs";
|
|
32
|
+
import { join } from "node:path";
|
|
33
|
+
|
|
34
|
+
import { writeFileAtomic } from "../fs-atomic.mjs";
|
|
35
|
+
|
|
36
|
+
/** Relative location of the device private key under the agent root. */
|
|
37
|
+
const KEY_DIR = join("state", "org");
|
|
38
|
+
const PRIV_FILE = "device-key.pem";
|
|
39
|
+
|
|
40
|
+
/** Resolve the on-disk path of the private key PEM for an agent root. */
|
|
41
|
+
function privKeyPath(agentRoot) {
|
|
42
|
+
return join(String(agentRoot || "."), KEY_DIR, PRIV_FILE);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Derive the public-key PEM (SPKI) from a private-key PEM (PKCS#8). Pure.
|
|
47
|
+
* @param {string} privateKeyPem
|
|
48
|
+
* @returns {string}
|
|
49
|
+
*/
|
|
50
|
+
function publicPemFromPrivate(privateKeyPem) {
|
|
51
|
+
const pub = createPublicKey(createPrivateKey(privateKeyPem));
|
|
52
|
+
return pub.export({ type: "spki", format: "pem" }).toString();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a fresh Ed25519 keypair as PEM strings. Pure.
|
|
57
|
+
* @returns {{publicKeyPem:string, privateKeyPem:string}}
|
|
58
|
+
*/
|
|
59
|
+
export function generateDeviceKey() {
|
|
60
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
61
|
+
return {
|
|
62
|
+
publicKeyPem: publicKey.export({ type: "spki", format: "pem" }).toString(),
|
|
63
|
+
privateKeyPem: privateKey.export({ type: "pkcs8", format: "pem" }).toString(),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Sign a buffer with an Ed25519 private-key PEM. Ed25519 takes no digest
|
|
69
|
+
* algorithm (the second arg to `crypto.sign` is null). Returns base64. Pure.
|
|
70
|
+
* @param {string} privateKeyPem PKCS#8 PEM
|
|
71
|
+
* @param {Buffer|Uint8Array|string} data
|
|
72
|
+
* @returns {string} base64 signature
|
|
73
|
+
*/
|
|
74
|
+
export function signWith(privateKeyPem, data) {
|
|
75
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
76
|
+
const key = createPrivateKey(privateKeyPem);
|
|
77
|
+
return edSign(null, buf, key).toString("base64");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Verify an Ed25519 signature over `data` against a public-key PEM. Never
|
|
82
|
+
* throws — a malformed key/signature returns false (the detective layer treats
|
|
83
|
+
* "can't verify" as "don't trust"). Pure.
|
|
84
|
+
* @param {string} publicKeyPem SPKI PEM
|
|
85
|
+
* @param {Buffer|Uint8Array|string} data
|
|
86
|
+
* @param {string} sigB64 base64 signature
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
export function verifySignature(publicKeyPem, data, sigB64) {
|
|
90
|
+
try {
|
|
91
|
+
const buf = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
92
|
+
const sig = Buffer.from(String(sigB64 || ""), "base64");
|
|
93
|
+
if (sig.length === 0) return false;
|
|
94
|
+
const key = createPublicKey(publicKeyPem);
|
|
95
|
+
return edVerify(null, buf, key, sig);
|
|
96
|
+
} catch {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Load the agent's device key, creating it on first use. The private key is
|
|
103
|
+
* stored 0600 under `state/org/device-key.pem`; only the public key and a bound
|
|
104
|
+
* `sign(buf)->base64` closure are returned (the private PEM never escapes).
|
|
105
|
+
*
|
|
106
|
+
* Idempotent and crash-safe: the create path writes atomically (temp→fsync→
|
|
107
|
+
* rename) with mode 0600, then re-chmods defensively in case a pre-existing key
|
|
108
|
+
* had looser perms.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} agentRoot absolute path to the agent instance root
|
|
111
|
+
* @returns {{publicKeyPem:string, sign:(data:(Buffer|Uint8Array|string))=>string}}
|
|
112
|
+
*/
|
|
113
|
+
export function ensureDeviceKey(agentRoot) {
|
|
114
|
+
const path = privKeyPath(agentRoot);
|
|
115
|
+
let privateKeyPem;
|
|
116
|
+
if (existsSync(path)) {
|
|
117
|
+
privateKeyPem = readFileSync(path, "utf8");
|
|
118
|
+
// Defensive: tighten perms if a previous writer left them loose.
|
|
119
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
120
|
+
} else {
|
|
121
|
+
const kp = generateDeviceKey();
|
|
122
|
+
privateKeyPem = kp.privateKeyPem;
|
|
123
|
+
writeFileAtomic(path, privateKeyPem, { mode: 0o600 });
|
|
124
|
+
try { chmodSync(path, 0o600); } catch { /* best-effort */ }
|
|
125
|
+
}
|
|
126
|
+
const publicKeyPem = publicPemFromPrivate(privateKeyPem);
|
|
127
|
+
return {
|
|
128
|
+
publicKeyPem,
|
|
129
|
+
sign: (data) => signWith(privateKeyPem, data),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* keys.test.mjs — device keypair crypto round-trips + tamper rejection.
|
|
3
|
+
* Run: node --test lib/org/keys.test.mjs
|
|
4
|
+
*/
|
|
5
|
+
"use strict";
|
|
6
|
+
|
|
7
|
+
import { test } from "node:test";
|
|
8
|
+
import assert from "node:assert/strict";
|
|
9
|
+
import { mkdtempSync, existsSync, statSync, rmSync, readFileSync } from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import { join } from "node:path";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
generateDeviceKey,
|
|
15
|
+
signWith,
|
|
16
|
+
verifySignature,
|
|
17
|
+
ensureDeviceKey,
|
|
18
|
+
} from "./keys.mjs";
|
|
19
|
+
|
|
20
|
+
function tmpRoot() {
|
|
21
|
+
return mkdtempSync(join(tmpdir(), "cohort-keys-"));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test("generateDeviceKey: emits SPKI public + PKCS#8 private PEM", () => {
|
|
25
|
+
const { publicKeyPem, privateKeyPem } = generateDeviceKey();
|
|
26
|
+
assert.match(publicKeyPem, /-----BEGIN PUBLIC KEY-----/);
|
|
27
|
+
assert.match(privateKeyPem, /-----BEGIN PRIVATE KEY-----/);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("sign/verify: round-trip succeeds", () => {
|
|
31
|
+
const { publicKeyPem, privateKeyPem } = generateDeviceKey();
|
|
32
|
+
const msg = Buffer.from("the org snapshot bytes");
|
|
33
|
+
const sig = signWith(privateKeyPem, msg);
|
|
34
|
+
assert.equal(typeof sig, "string");
|
|
35
|
+
assert.equal(verifySignature(publicKeyPem, msg, sig), true);
|
|
36
|
+
// string payload accepted too
|
|
37
|
+
assert.equal(verifySignature(publicKeyPem, "the org snapshot bytes", sig), true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("verify: rejects tampered message", () => {
|
|
41
|
+
const { publicKeyPem, privateKeyPem } = generateDeviceKey();
|
|
42
|
+
const sig = signWith(privateKeyPem, Buffer.from("original"));
|
|
43
|
+
assert.equal(verifySignature(publicKeyPem, Buffer.from("tampered"), sig), false);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("verify: rejects signature from a different key", () => {
|
|
47
|
+
const a = generateDeviceKey();
|
|
48
|
+
const b = generateDeviceKey();
|
|
49
|
+
const msg = Buffer.from("payload");
|
|
50
|
+
const sig = signWith(a.privateKeyPem, msg);
|
|
51
|
+
assert.equal(verifySignature(b.publicKeyPem, msg, sig), false);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("verify: never throws on garbage inputs", () => {
|
|
55
|
+
assert.equal(verifySignature("not a pem", Buffer.from("x"), "AAAA"), false);
|
|
56
|
+
assert.equal(verifySignature(generateDeviceKey().publicKeyPem, Buffer.from("x"), ""), false);
|
|
57
|
+
assert.equal(verifySignature(generateDeviceKey().publicKeyPem, Buffer.from("x"), "!!!notb64"), false);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("ensureDeviceKey: creates 0600 key under state/org and returns signer", () => {
|
|
61
|
+
const root = tmpRoot();
|
|
62
|
+
try {
|
|
63
|
+
const { publicKeyPem, sign } = ensureDeviceKey(root);
|
|
64
|
+
assert.match(publicKeyPem, /BEGIN PUBLIC KEY/);
|
|
65
|
+
const path = join(root, "state", "org", "device-key.pem");
|
|
66
|
+
assert.equal(existsSync(path), true);
|
|
67
|
+
// perms must be owner-only (0600)
|
|
68
|
+
const mode = statSync(path).mode & 0o777;
|
|
69
|
+
assert.equal(mode, 0o600);
|
|
70
|
+
// the returned signer signs verifiably against the returned public key
|
|
71
|
+
const sig = sign(Buffer.from("hello"));
|
|
72
|
+
assert.equal(verifySignature(publicKeyPem, Buffer.from("hello"), sig), true);
|
|
73
|
+
// private PEM is on disk but never returned
|
|
74
|
+
assert.match(readFileSync(path, "utf8"), /BEGIN PRIVATE KEY/);
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(root, { recursive: true, force: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("ensureDeviceKey: idempotent — reload returns the SAME identity", () => {
|
|
81
|
+
const root = tmpRoot();
|
|
82
|
+
try {
|
|
83
|
+
const first = ensureDeviceKey(root);
|
|
84
|
+
const second = ensureDeviceKey(root);
|
|
85
|
+
assert.equal(first.publicKeyPem, second.publicKeyPem);
|
|
86
|
+
// a signature from the first loader verifies under the second's public key
|
|
87
|
+
const sig = first.sign(Buffer.from("cross"));
|
|
88
|
+
assert.equal(verifySignature(second.publicKeyPem, Buffer.from("cross"), sig), true);
|
|
89
|
+
} finally {
|
|
90
|
+
rmSync(root, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
});
|