@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,348 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ui-parity.test.mjs — Agent UI-parity helpers (full human-action mirror).
|
|
3
|
+
*
|
|
4
|
+
* The 328 wrappers in ui-parity.mjs are each a one-liner over call() from
|
|
5
|
+
* client.mjs, so client.test.mjs already proves the transport contract (headers,
|
|
6
|
+
* idempotency, fail-open, frame normalisation). Here we just prove a representative
|
|
7
|
+
* slice across families ROUTES to the correct method name, POSTs the params body,
|
|
8
|
+
* and returns the res frame — using the same injected fetchImpl (no network).
|
|
9
|
+
*
|
|
10
|
+
* Run: node --test lib/org/ui-parity.test.mjs
|
|
11
|
+
*/
|
|
12
|
+
"use strict";
|
|
13
|
+
|
|
14
|
+
import { test } from "node:test";
|
|
15
|
+
import assert from "node:assert/strict";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
charterUpdateClauses,
|
|
19
|
+
charterGet,
|
|
20
|
+
memberCreate,
|
|
21
|
+
channelCreate,
|
|
22
|
+
sopCreate,
|
|
23
|
+
decisionSign,
|
|
24
|
+
taskUpdate,
|
|
25
|
+
settingsMintApiKey,
|
|
26
|
+
filePin,
|
|
27
|
+
emailSend,
|
|
28
|
+
emailMarkRead,
|
|
29
|
+
emailInbox,
|
|
30
|
+
emailMessage,
|
|
31
|
+
emailThread,
|
|
32
|
+
artifactCreate,
|
|
33
|
+
artifactAct,
|
|
34
|
+
artifactPatch,
|
|
35
|
+
artifactGet,
|
|
36
|
+
artifactList,
|
|
37
|
+
artifactCatalog,
|
|
38
|
+
} from "./ui-parity.mjs";
|
|
39
|
+
import { PROTOCOL_VERSION } from "./protocol.mjs";
|
|
40
|
+
|
|
41
|
+
const BASE = "https://org.cohortapp.com";
|
|
42
|
+
const TOKEN = "tok-abc";
|
|
43
|
+
|
|
44
|
+
// Ambient COHORT_* tokens would leak into header assertions; clear them.
|
|
45
|
+
delete process.env.COHORT_API_TOKEN;
|
|
46
|
+
delete process.env.COHORT_TOKEN;
|
|
47
|
+
|
|
48
|
+
/** A fake fetch that records calls and returns a canned res frame. */
|
|
49
|
+
function fakeFetch(responder) {
|
|
50
|
+
const calls = [];
|
|
51
|
+
const impl = async (url, init) => {
|
|
52
|
+
calls.push({ url, init, headers: (init && init.headers) || {} });
|
|
53
|
+
const r = responder ? responder(url, init, calls.length - 1) : null;
|
|
54
|
+
const status = r && typeof r.status === "number" ? r.status : 200;
|
|
55
|
+
const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
|
|
56
|
+
const body = r ? r.body : { ok: true, result: null };
|
|
57
|
+
return {
|
|
58
|
+
ok,
|
|
59
|
+
status,
|
|
60
|
+
headers: { get: (k) => (r && r.respHeaders ? r.respHeaders[k.toLowerCase()] : undefined) },
|
|
61
|
+
json: async () => body,
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
impl.calls = calls;
|
|
65
|
+
return impl;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- representative wrappers route to the right /v1 method -------------------
|
|
69
|
+
|
|
70
|
+
test("ui-parity helpers route to their /v1 methods (one per family)", async () => {
|
|
71
|
+
const seen = [];
|
|
72
|
+
const f = fakeFetch((url) => { seen.push(url); return { body: { ok: true, result: null } }; });
|
|
73
|
+
const o = { base: BASE, token: TOKEN, idempotencyKey: "k", fetchImpl: f };
|
|
74
|
+
await charterUpdateClauses({ channelId: "c", clauses: [] }, o);
|
|
75
|
+
await charterGet({ memberId: "m" }, o);
|
|
76
|
+
await memberCreate({ kind: "AI", displayName: "Robin", roleTitle: "Eng", roleLevel: 4, teamId: "t" }, o);
|
|
77
|
+
await channelCreate({ slug: "ops", name: "Ops", kind: "TEAM" }, o);
|
|
78
|
+
await sopCreate({ channelId: "c", title: "Runbook" }, o);
|
|
79
|
+
await decisionSign({ decisionId: "d", memberSlug: "robin" }, o);
|
|
80
|
+
await taskUpdate({ taskId: "tk", priority: "P1" }, o);
|
|
81
|
+
await settingsMintApiKey({ name: "ci" }, o);
|
|
82
|
+
await filePin({ channelId: "c", name: "f.pdf", dataUrl: "data:," }, o);
|
|
83
|
+
assert.deepEqual(seen, [
|
|
84
|
+
`${BASE}/api/v1/charter.updateClauses`,
|
|
85
|
+
`${BASE}/api/v1/charter.get`,
|
|
86
|
+
`${BASE}/api/v1/member.create`,
|
|
87
|
+
`${BASE}/api/v1/channel.create`,
|
|
88
|
+
`${BASE}/api/v1/sop.create`,
|
|
89
|
+
`${BASE}/api/v1/decision.sign`,
|
|
90
|
+
`${BASE}/api/v1/board.updateTask`,
|
|
91
|
+
`${BASE}/api/v1/settings.mintApiKey`,
|
|
92
|
+
`${BASE}/api/v1/file.pin`,
|
|
93
|
+
]);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
test("ui-parity wrapper POSTs params + Bearer + x-org-protocol, returns the frame", async () => {
|
|
97
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { id: "ch-1" } } }));
|
|
98
|
+
const frame = await channelCreate(
|
|
99
|
+
{ slug: "ops", name: "Ops", kind: "TEAM" },
|
|
100
|
+
{ base: BASE, token: TOKEN, idempotencyKey: "chan-ops", fetchImpl: f },
|
|
101
|
+
);
|
|
102
|
+
assert.deepEqual(frame, { ok: true, result: { id: "ch-1" } });
|
|
103
|
+
assert.equal(f.calls[0].init.method, "POST");
|
|
104
|
+
assert.equal(f.calls[0].url, `${BASE}/api/v1/channel.create`);
|
|
105
|
+
assert.equal(f.calls[0].headers.authorization, `Bearer ${TOKEN}`);
|
|
106
|
+
assert.equal(f.calls[0].headers["x-org-protocol"], String(PROTOCOL_VERSION));
|
|
107
|
+
assert.deepEqual(JSON.parse(f.calls[0].init.body), { slug: "ops", name: "Ops", kind: "TEAM" });
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("idempotent wrapper forwards a caller-supplied x-idempotency-key", async () => {
|
|
111
|
+
// member.create is idempotent but carries no natural id at creation time, so the
|
|
112
|
+
// caller SHOULD supply a stable key — verify it reaches the wire.
|
|
113
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
114
|
+
await memberCreate(
|
|
115
|
+
{ kind: "AI", displayName: "Robin", roleTitle: "Eng", roleLevel: 4, teamId: "t" },
|
|
116
|
+
{ base: BASE, token: TOKEN, idempotencyKey: "seed-robin", fetchImpl: f },
|
|
117
|
+
);
|
|
118
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "seed-robin");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("read-style method (charter.get) is a POST RPC, sends NO idempotency key", async () => {
|
|
122
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { memberId: "m" } } }));
|
|
123
|
+
const frame = await charterGet({ memberId: "m" }, { base: BASE, token: TOKEN, fetchImpl: f });
|
|
124
|
+
assert.equal(f.calls[0].init.method, "POST");
|
|
125
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], undefined);
|
|
126
|
+
assert.equal(frame.result.memberId, "m");
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test("ui-parity wrappers fail open on transport throw (no throw, INTERNAL frame)", async () => {
|
|
130
|
+
const thrower = fakeFetch(() => { throw new Error("network"); });
|
|
131
|
+
const o = { base: BASE, token: TOKEN, fetchImpl: thrower };
|
|
132
|
+
for (const frame of [
|
|
133
|
+
await taskUpdate({ taskId: "tk" }, o),
|
|
134
|
+
await decisionSign({ decisionId: "d", memberSlug: "x" }, o),
|
|
135
|
+
await filePin({ channelId: "c", name: "f", dataUrl: "data:," }, o),
|
|
136
|
+
]) {
|
|
137
|
+
assert.equal(frame.ok, false);
|
|
138
|
+
assert.equal(frame.error.code, "INTERNAL");
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test("ui-parity wrapper surfaces a server error frame verbatim (FORBIDDEN_SCOPE 403)", async () => {
|
|
143
|
+
const f = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "admin only" } } }));
|
|
144
|
+
const frame = await settingsMintApiKey({ name: "ci" }, { base: BASE, token: TOKEN, idempotencyKey: "k", fetchImpl: f });
|
|
145
|
+
assert.equal(frame.ok, false);
|
|
146
|
+
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// --- email family (companion spec §8 / §9.1) --------------------------------
|
|
150
|
+
|
|
151
|
+
test("email wrappers route to their /v1 email.* methods and POST the params", async () => {
|
|
152
|
+
const seen = [];
|
|
153
|
+
const f = fakeFetch((url) => { seen.push(url); return { body: { ok: true, result: null } }; });
|
|
154
|
+
const o = { base: BASE, token: TOKEN, idempotencyKey: "k", fetchImpl: f };
|
|
155
|
+
await emailSend({ threadId: "t1", text: "re: hi", idempotencyId: "biz-1" }, o);
|
|
156
|
+
await emailMarkRead({ messageId: "m1" }, o);
|
|
157
|
+
await emailInbox({ unreadOnly: true, limit: 50 }, o);
|
|
158
|
+
await emailMessage({ id: "m1" }, o);
|
|
159
|
+
await emailThread({ threadId: "t1" }, o);
|
|
160
|
+
assert.deepEqual(seen, [
|
|
161
|
+
`${BASE}/api/v1/email.send`,
|
|
162
|
+
`${BASE}/api/v1/email.markRead`,
|
|
163
|
+
`${BASE}/api/v1/email.inbox`,
|
|
164
|
+
`${BASE}/api/v1/email.message`,
|
|
165
|
+
`${BASE}/api/v1/email.thread`,
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("email mutating wrappers carry x-idempotency-key; reads do not", async () => {
|
|
170
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
171
|
+
await emailSend({ threadId: "t1", text: "x", idempotencyId: "biz-1" }, { base: BASE, token: TOKEN, idempotencyKey: "send-1", fetchImpl: f });
|
|
172
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], "send-1");
|
|
173
|
+
await emailInbox({ limit: 1 }, { base: BASE, token: TOKEN, fetchImpl: f });
|
|
174
|
+
assert.equal(f.calls[1].headers["x-idempotency-key"], undefined, "sideEffecting:false reads send no key");
|
|
175
|
+
assert.equal(f.calls[1].init.method, "POST", "email reads are POST METHODS, not GET reads");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("emailSend surfaces the approval_required result as a NORMAL frame", async () => {
|
|
179
|
+
const approval = { actionClass: "external_comms", payloadHash: "abc", intent: { to: ["x@y.z"] } };
|
|
180
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { status: "approval_required", approval, firstContactRecipients: ["x@y.z"] } } }));
|
|
181
|
+
const frame = await emailSend({ to: ["x@y.z"], subject: "s", text: "b", idempotencyId: "biz-2" }, { base: BASE, token: TOKEN, idempotencyKey: "k2", fetchImpl: f });
|
|
182
|
+
assert.equal(frame.ok, true, "approval_required is a result, not an error");
|
|
183
|
+
assert.equal(frame.result.status, "approval_required");
|
|
184
|
+
assert.deepEqual(frame.result.approval, approval);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// --- artifact family (GU program 2026-07) -----------------------------------
|
|
188
|
+
|
|
189
|
+
test("artifact wrappers route to their /v1 artifact.* methods and POST the params", async () => {
|
|
190
|
+
const seen = [];
|
|
191
|
+
const f = fakeFetch((url) => { seen.push(url); return { body: { ok: true, result: null } }; });
|
|
192
|
+
const o = { base: BASE, token: TOKEN, fetchImpl: f };
|
|
193
|
+
const envelope = { genui: "v2", artifact: { class: "ops.status", version: 1, state: "complete", source: { kind: "native" } }, actions: [], root: { type: "card", tone: "neutral", blocks: [] } };
|
|
194
|
+
await artifactCreate({ channelId: "c1", envelope, clientMsgId: "cm-1" }, o);
|
|
195
|
+
await artifactAct({ artifactId: "a1", action: "approve", idempotencyKey: "k-1" }, o);
|
|
196
|
+
await artifactPatch({ artifactId: "a1", toState: "complete", envelope }, o);
|
|
197
|
+
await artifactGet({ artifactId: "a1" }, o);
|
|
198
|
+
await artifactList({ channelId: "c1", state: "complete" }, o);
|
|
199
|
+
await artifactCatalog({}, o);
|
|
200
|
+
assert.deepEqual(seen, [
|
|
201
|
+
`${BASE}/api/v1/artifact.create`,
|
|
202
|
+
`${BASE}/api/v1/artifact.act`,
|
|
203
|
+
`${BASE}/api/v1/artifact.patch`,
|
|
204
|
+
`${BASE}/api/v1/artifact.get`,
|
|
205
|
+
`${BASE}/api/v1/artifact.list`,
|
|
206
|
+
`${BASE}/api/v1/artifact.catalog`,
|
|
207
|
+
]);
|
|
208
|
+
assert.deepEqual(JSON.parse(f.calls[0].init.body).envelope, envelope, "envelope POSTed verbatim");
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("artifact writes are dispatcher-neutral: business ids in the BODY, no x-idempotency-key header", async () => {
|
|
212
|
+
// sideEffecting:false at the dispatcher (the hq artifact service self-appends
|
|
213
|
+
// its ledger) → call() attaches no header even when o.idempotencyKey is set;
|
|
214
|
+
// dedup rides params.clientMsgId / params.idempotencyKey instead.
|
|
215
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { artifactId: "a1", messageId: "m1" } } }));
|
|
216
|
+
await artifactCreate({ channelId: "c1", envelope: { genui: "v1", root: {} }, clientMsgId: "cm-9" }, { base: BASE, token: TOKEN, idempotencyKey: "ignored", fetchImpl: f });
|
|
217
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], undefined);
|
|
218
|
+
assert.equal(JSON.parse(f.calls[0].init.body).clientMsgId, "cm-9");
|
|
219
|
+
const held = fakeFetch(() => ({ body: { ok: true, result: { held: true, approvalId: "ap-1" } } }));
|
|
220
|
+
const frame = await artifactAct({ artifactId: "a1", action: "pay", idempotencyKey: "pay-a1" }, { base: BASE, token: TOKEN, fetchImpl: held });
|
|
221
|
+
assert.equal(held.calls[0].headers["x-idempotency-key"], undefined);
|
|
222
|
+
assert.equal(JSON.parse(held.calls[0].init.body).idempotencyKey, "pay-a1");
|
|
223
|
+
assert.equal(frame.ok, true, "approval-held is a result, not an error");
|
|
224
|
+
assert.equal(frame.result.held, true);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
// --- agent desks (books / calendar / crm / files / email Inbox app / meetings) --
|
|
228
|
+
|
|
229
|
+
test("desk wrappers route to their /v1 methods and POST the params", async () => {
|
|
230
|
+
const {
|
|
231
|
+
emailMailboxes, emailThreads, emailTriage, emailDraftSend,
|
|
232
|
+
filesList, filesDocWriteRevision,
|
|
233
|
+
calendarFindATime, calendarRsvp,
|
|
234
|
+
crmListDeals, crmMoveStage,
|
|
235
|
+
booksReports, booksInvoiceDraft, booksAsk,
|
|
236
|
+
meetingsRecapFile,
|
|
237
|
+
} = await import("./ui-parity.mjs");
|
|
238
|
+
const seen = [];
|
|
239
|
+
const f = fakeFetch((url) => { seen.push(url); return { body: { ok: true, result: null } }; });
|
|
240
|
+
const o = { base: BASE, token: TOKEN, idempotencyKey: "k", fetchImpl: f };
|
|
241
|
+
await emailMailboxes({}, o);
|
|
242
|
+
await emailThreads({ mailboxId: "mb1", folder: "inbox" }, o);
|
|
243
|
+
await emailTriage({ threadId: "t1", priority: "P1" }, o);
|
|
244
|
+
await emailDraftSend({ mailboxId: "mb1", to: ["a@b.co"], bodyText: "hi" }, o);
|
|
245
|
+
await filesList({ q: "plan" }, o);
|
|
246
|
+
await filesDocWriteRevision({ fileId: "f1", html: "<p>x</p>" }, o);
|
|
247
|
+
await calendarFindATime({ attendeeMemberIds: ["m1"], durationMinutes: 30, fromDay: "2026-07-27" }, o);
|
|
248
|
+
await calendarRsvp({ eventId: "e1", rsvp: "ACCEPTED" }, o);
|
|
249
|
+
await crmListDeals({ openOnly: true }, o);
|
|
250
|
+
await crmMoveStage({ dealId: "d1", stage: "PROPOSAL" }, o);
|
|
251
|
+
await booksReports({ report: "pl" }, o);
|
|
252
|
+
await booksInvoiceDraft({ payeeName: "Acme", description: "Retainer", totalCents: 120000 }, o);
|
|
253
|
+
await booksAsk({ question: "What is overdue?" }, o);
|
|
254
|
+
await meetingsRecapFile({ meetingId: "mt1", recap: { bullets: ["done"] } }, o);
|
|
255
|
+
assert.deepEqual(seen, [
|
|
256
|
+
`${BASE}/api/v1/email.mailboxes`,
|
|
257
|
+
`${BASE}/api/v1/email.threads`,
|
|
258
|
+
`${BASE}/api/v1/email.triage`,
|
|
259
|
+
`${BASE}/api/v1/email.draftSend`,
|
|
260
|
+
`${BASE}/api/v1/files.list`,
|
|
261
|
+
`${BASE}/api/v1/files.docWriteRevision`,
|
|
262
|
+
`${BASE}/api/v1/calendar.findATime`,
|
|
263
|
+
`${BASE}/api/v1/calendar.rsvp`,
|
|
264
|
+
`${BASE}/api/v1/crm.listDeals`,
|
|
265
|
+
`${BASE}/api/v1/crm.moveStage`,
|
|
266
|
+
`${BASE}/api/v1/books.reports`,
|
|
267
|
+
`${BASE}/api/v1/books.invoiceDraft`,
|
|
268
|
+
`${BASE}/api/v1/books.ask`,
|
|
269
|
+
`${BASE}/api/v1/meetings.recapFile`,
|
|
270
|
+
]);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// --- directory desk (2026-07) -----------------------------------------------
|
|
274
|
+
|
|
275
|
+
test("directory wrappers route to their /v1 methods and POST the params", async () => {
|
|
276
|
+
const {
|
|
277
|
+
directorySearch, directoryListPeople, directoryGetPerson, directoryAsk,
|
|
278
|
+
directoryAddChannel, directoryProposeCapture, directoryProposeMerge,
|
|
279
|
+
directoryRequestAwareness, directoryCreateList,
|
|
280
|
+
} = await import("./ui-parity.mjs");
|
|
281
|
+
const seen = [];
|
|
282
|
+
const f = fakeFetch((url) => { seen.push(url); return { body: { ok: true, result: null } }; });
|
|
283
|
+
const o = { base: BASE, token: TOKEN, idempotencyKey: "k", fetchImpl: f };
|
|
284
|
+
await directorySearch({ q: "hartmann" }, o);
|
|
285
|
+
await directoryListPeople({ relationship: "client", sort: "touch" }, o);
|
|
286
|
+
await directoryGetPerson({ contactId: "c1" }, o);
|
|
287
|
+
await directoryAsk({ question: "Who owns Fieldstone?" }, o);
|
|
288
|
+
await directoryAddChannel({ subjectType: "PERSON", subjectId: "c1", type: "EMAIL", value: "a@b.co", verifiedFrom: "SIGNATURE" }, o);
|
|
289
|
+
await directoryProposeCapture({ kind: "PERSON", payload: { person: { displayName: "Lena" } }, source: "MAIL", sourceFingerprint: "sig:1" }, o);
|
|
290
|
+
await directoryProposeMerge({ aRef: "contact:c1", bRef: "contact:c2" }, o);
|
|
291
|
+
await directoryRequestAwareness({ contactId: "c9", reason: "renewal paper" }, o);
|
|
292
|
+
await directoryCreateList({ name: "Fieldstone renewal" }, o);
|
|
293
|
+
assert.deepEqual(seen, [
|
|
294
|
+
`${BASE}/api/v1/directory.search`,
|
|
295
|
+
`${BASE}/api/v1/directory.listPeople`,
|
|
296
|
+
`${BASE}/api/v1/directory.getPerson`,
|
|
297
|
+
`${BASE}/api/v1/directory.ask`,
|
|
298
|
+
`${BASE}/api/v1/directory.addChannel`,
|
|
299
|
+
`${BASE}/api/v1/directory.proposeCapture`,
|
|
300
|
+
`${BASE}/api/v1/directory.proposeMerge`,
|
|
301
|
+
`${BASE}/api/v1/directory.requestAwareness`,
|
|
302
|
+
`${BASE}/api/v1/directory.createList`,
|
|
303
|
+
]);
|
|
304
|
+
assert.equal(JSON.parse(f.calls[5].init.body).sourceFingerprint, "sig:1", "the dedup key rides the body");
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("directory reads are POST RPCs with no idempotency key; idempotent writes forward the caller's", async () => {
|
|
308
|
+
const { directoryListPeople, directoryCreatePerson } = await import("./ui-parity.mjs");
|
|
309
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: { people: [] } } }));
|
|
310
|
+
await directoryListPeople({ scope: "external" }, { base: BASE, token: TOKEN, fetchImpl: f });
|
|
311
|
+
assert.equal(f.calls[0].init.method, "POST", "directory reads are POST METHODS, not GET reads");
|
|
312
|
+
assert.equal(f.calls[0].headers["x-idempotency-key"], undefined);
|
|
313
|
+
await directoryCreatePerson({ displayName: "Lena Hartmann" }, { base: BASE, token: TOKEN, idempotencyKey: "seed-lena", fetchImpl: f });
|
|
314
|
+
assert.equal(f.calls[1].headers["x-idempotency-key"], "seed-lena");
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("a human-gated directory verb surfaces the server's refusal verbatim, never a throw", async () => {
|
|
318
|
+
// executeMerge / setAwareness / archive* are human-gated in the hq domain
|
|
319
|
+
// (DECISIONS D-7). An agent seat gets a FORBIDDEN_SCOPE frame — that refusal is
|
|
320
|
+
// correct behaviour and must reach the caller intact.
|
|
321
|
+
const { directoryExecuteMerge } = await import("./ui-parity.mjs");
|
|
322
|
+
const f = fakeFetch(() => ({ ok: false, status: 403, body: { ok: false, error: { code: "FORBIDDEN_SCOPE", message: "a human resolves merges" } } }));
|
|
323
|
+
const frame = await directoryExecuteMerge({ candidateId: "m1", survivorRef: "contact:c1" }, { base: BASE, token: TOKEN, fetchImpl: f });
|
|
324
|
+
assert.equal(frame.ok, false);
|
|
325
|
+
assert.equal(frame.error.code, "FORBIDDEN_SCOPE");
|
|
326
|
+
assert.match(frame.error.message, /human/);
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
test("every desk protocol method has exactly one ui-parity wrapper (196 desks / 328 total)", async () => {
|
|
330
|
+
const fs = await import("node:fs");
|
|
331
|
+
const path = await import("node:path");
|
|
332
|
+
const url = await import("node:url");
|
|
333
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
334
|
+
const src = fs.readFileSync(path.join(here, "ui-parity.mjs"), "utf8");
|
|
335
|
+
const called = [...src.matchAll(/return call\("([a-z]+\.[A-Za-z]+)"/g)].map((x) => x[1]);
|
|
336
|
+
// 2026-08 mobile-parity delta: +16 desk methods (email 5, files 9,
|
|
337
|
+
// calendar.ask, crm.ask) → 312 → 328.
|
|
338
|
+
assert.equal(called.length, 328, "one call() site per wrapper");
|
|
339
|
+
assert.equal(new Set(called).size, called.length, "no duplicate method bindings");
|
|
340
|
+
const p = await import("./protocol.mjs");
|
|
341
|
+
const deskFams = new Set(["books", "calendar", "crm", "directory", "files"]);
|
|
342
|
+
const deskMethods = Object.entries(p.METHODS)
|
|
343
|
+
.filter(([n, d]) => deskFams.has(d.family) || n.startsWith("email.") || n === "meetings.recapFile")
|
|
344
|
+
.map(([n]) => n);
|
|
345
|
+
const bound = new Set(called);
|
|
346
|
+
const missing = deskMethods.filter((m) => !bound.has(m));
|
|
347
|
+
assert.deepEqual(missing, [], "every desk method is wrapped");
|
|
348
|
+
});
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/verify.mjs — the detective layer (SPEC-org-server §G3).
|
|
3
|
+
*
|
|
4
|
+
* The org server is a convenient coordinator, NOT a trusted authority. The
|
|
5
|
+
* threat model: a compromised or malicious server may *withhold* state or *lie*,
|
|
6
|
+
* but it must never be able to forge state the agent *accepts* as authoritative.
|
|
7
|
+
* So every authoritative read is independently re-checked here:
|
|
8
|
+
*
|
|
9
|
+
* - verifySnapshot(snapshot, trustedSignersPem[]) — the server signs the
|
|
10
|
+
* canonicalized snapshot with its org-signing key; we verify that signature
|
|
11
|
+
* against a PINNED allow-list of public keys baked into the agent's config.
|
|
12
|
+
* The signer list is NEVER taken from the snapshot itself (that would let
|
|
13
|
+
* the server name its own key) — it is supplied by the caller from pinned
|
|
14
|
+
* trust. An unsigned, wrong-signer, or tampered snapshot is rejected.
|
|
15
|
+
*
|
|
16
|
+
* - verifyEvents(events, startPrev) — re-derives the append-only hash chain via
|
|
17
|
+
* protocol.verifyChain (the SAME computeRowHash the server wrote with), so a
|
|
18
|
+
* rewritten or reordered log is caught at the first broken link.
|
|
19
|
+
*
|
|
20
|
+
* Helpers return a `{ok, reason}` quarantine verdict rather than throwing: a
|
|
21
|
+
* compromised server can DoS us (we refuse its state) but cannot crash us or
|
|
22
|
+
* slip forged state past. The caller quarantines and falls back to last-good /
|
|
23
|
+
* file-backed state on `ok:false`.
|
|
24
|
+
*
|
|
25
|
+
* Pure: node:crypto via keys.mjs + protocol.mjs. No I/O, no network.
|
|
26
|
+
*
|
|
27
|
+
* @module lib/org/verify
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
"use strict";
|
|
31
|
+
|
|
32
|
+
import { canonicalize, verifyChain } from "./protocol.mjs";
|
|
33
|
+
import { verifySignature } from "./keys.mjs";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build the canonical signing material for a snapshot: the snapshot payload with
|
|
37
|
+
* its own signature envelope stripped (the server signs the body, not the
|
|
38
|
+
* signature). We delete `sig`/`signature`/`signer` before canonicalizing so the
|
|
39
|
+
* bytes match what the server signed.
|
|
40
|
+
* @param {object} snapshot
|
|
41
|
+
* @returns {string}
|
|
42
|
+
*/
|
|
43
|
+
function snapshotSigningMaterial(snapshot) {
|
|
44
|
+
const { sig, signature, signer, ...body } = snapshot || {};
|
|
45
|
+
void sig; void signature; void signer; // signature envelope is excluded from material
|
|
46
|
+
return canonicalize(body);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Verify a server snapshot against a PINNED list of trusted signer public keys.
|
|
51
|
+
*
|
|
52
|
+
* The snapshot carries a detached signature in `sig` (base64) — and optionally a
|
|
53
|
+
* `signer` hint — over the canonicalized snapshot body (envelope fields removed).
|
|
54
|
+
* We try every pinned signer and accept iff one verifies. The pinned list is the
|
|
55
|
+
* root of trust; an empty list means "nothing is trusted" → reject.
|
|
56
|
+
*
|
|
57
|
+
* @param {object} snapshot the server snapshot payload (must contain `sig`)
|
|
58
|
+
* @param {string[]} trustedSignersPem PINNED SPKI public-key PEMs (from config)
|
|
59
|
+
* @returns {{ok:true, signer:string}|{ok:false, reason:string}}
|
|
60
|
+
*/
|
|
61
|
+
export function verifySnapshot(snapshot, trustedSignersPem) {
|
|
62
|
+
if (!snapshot || typeof snapshot !== "object") {
|
|
63
|
+
return { ok: false, reason: "snapshot not an object" };
|
|
64
|
+
}
|
|
65
|
+
const sig = snapshot.sig || snapshot.signature;
|
|
66
|
+
if (!sig || typeof sig !== "string") {
|
|
67
|
+
return { ok: false, reason: "snapshot missing signature" };
|
|
68
|
+
}
|
|
69
|
+
const signers = Array.isArray(trustedSignersPem) ? trustedSignersPem.filter(Boolean) : [];
|
|
70
|
+
if (signers.length === 0) {
|
|
71
|
+
return { ok: false, reason: "no pinned signers (default-deny)" };
|
|
72
|
+
}
|
|
73
|
+
const material = Buffer.from(snapshotSigningMaterial(snapshot));
|
|
74
|
+
for (const pem of signers) {
|
|
75
|
+
if (verifySignature(pem, material, sig)) {
|
|
76
|
+
return { ok: true, signer: pem };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { ok: false, reason: "signature not from any pinned signer" };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// ---------------------------------------------------------------------------
|
|
83
|
+
// SSH allowed-signers → SPKI PEM (the SPEC-format trust root loader)
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
/** Fixed DER prefix for an Ed25519 SubjectPublicKeyInfo (12 bytes, then 32 raw). */
|
|
87
|
+
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Convert an OpenSSH ed25519 public key (the base64 blob from a `ssh-ed25519
|
|
91
|
+
* AAAA… [comment]` one-liner, OR the whole one-liner) into a SubjectPublicKeyInfo
|
|
92
|
+
* PEM that node:crypto / the verify layer can pin. The trust root SPEC uses the
|
|
93
|
+
* OpenSSH allowed-signers format (`principal namespaces? ssh-ed25519 base64key`),
|
|
94
|
+
* so we accept either the bare base64 blob or a full allowed-signers / authorized
|
|
95
|
+
* line and pull the `ssh-ed25519` token out.
|
|
96
|
+
*
|
|
97
|
+
* The OpenSSH wire blob is `string("ssh-ed25519") string(<32-byte raw key>)`; the
|
|
98
|
+
* SPKI DER is the fixed Ed25519 prefix followed by the same 32 raw bytes — so the
|
|
99
|
+
* conversion is a parse + re-wrap (round-trips exactly with the SPKI PEM
|
|
100
|
+
* node:crypto exports for the same key). Returns null on any malformed input
|
|
101
|
+
* (the detective layer treats "can't parse" as "don't trust"). Pure, never throws.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} input a base64 ssh-ed25519 blob or a full allowed-signers line
|
|
104
|
+
* @returns {string|null} SPKI PEM, or null
|
|
105
|
+
*/
|
|
106
|
+
export function sshEd25519ToSpkiPem(input) {
|
|
107
|
+
try {
|
|
108
|
+
if (!input || typeof input !== "string") return null;
|
|
109
|
+
let b64 = input.trim();
|
|
110
|
+
if (/\s/.test(b64)) {
|
|
111
|
+
// A full line (allowed-signers or authorized_keys): take the field that
|
|
112
|
+
// follows the `ssh-ed25519` token, else the longest base64-looking token.
|
|
113
|
+
const toks = b64.split(/\s+/);
|
|
114
|
+
const idx = toks.indexOf("ssh-ed25519");
|
|
115
|
+
b64 = idx >= 0 && toks[idx + 1] ? toks[idx + 1]
|
|
116
|
+
: toks.filter((t) => /^[A-Za-z0-9+/=]+$/.test(t) && t.length >= 64).sort((a, b) => b.length - a.length)[0] || "";
|
|
117
|
+
}
|
|
118
|
+
if (!b64) return null;
|
|
119
|
+
const blob = Buffer.from(b64, "base64");
|
|
120
|
+
// Parse the SSH wire format: 4-byte BE length-prefixed strings.
|
|
121
|
+
let off = 0;
|
|
122
|
+
const readStr = () => {
|
|
123
|
+
if (off + 4 > blob.length) throw new Error("truncated");
|
|
124
|
+
const n = blob.readUInt32BE(off); off += 4;
|
|
125
|
+
if (off + n > blob.length) throw new Error("truncated");
|
|
126
|
+
const s = blob.subarray(off, off + n); off += n;
|
|
127
|
+
return s;
|
|
128
|
+
};
|
|
129
|
+
const algo = readStr().toString("utf8");
|
|
130
|
+
const raw = readStr();
|
|
131
|
+
if (algo !== "ssh-ed25519" || raw.length !== 32) return null;
|
|
132
|
+
const der = Buffer.concat([ED25519_SPKI_PREFIX, raw]);
|
|
133
|
+
const body = der.toString("base64").replace(/(.{64})/g, "$1\n").replace(/\n$/, "");
|
|
134
|
+
return `-----BEGIN PUBLIC KEY-----\n${body}\n-----END PUBLIC KEY-----\n`;
|
|
135
|
+
} catch {
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Verify a contiguous slice of the event chain. Delegates to the protocol's
|
|
142
|
+
* `verifyChain` (which uses the load-bearing computeRowHash). Returns a
|
|
143
|
+
* quarantine verdict instead of the raw break object.
|
|
144
|
+
*
|
|
145
|
+
* @param {Array<object>} events ordered by seq ascending
|
|
146
|
+
* @param {string} [startPrev=""] row_hash of the event BEFORE events[0]
|
|
147
|
+
* @returns {{ok:true}|{ok:false, reason:string, index:number}}
|
|
148
|
+
*/
|
|
149
|
+
export function verifyEvents(events, startPrev = "") {
|
|
150
|
+
if (!Array.isArray(events)) {
|
|
151
|
+
return { ok: false, reason: "events not an array", index: -1 };
|
|
152
|
+
}
|
|
153
|
+
const broken = verifyChain(events, startPrev);
|
|
154
|
+
if (broken) {
|
|
155
|
+
return { ok: false, reason: broken.reason, index: broken.index };
|
|
156
|
+
}
|
|
157
|
+
return { ok: true };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Quarantine wrapper: run a verifier and normalise any thrown error into a
|
|
162
|
+
* `{ok:false, reason}` verdict, so a malformed payload from a hostile server can
|
|
163
|
+
* never crash the caller. Accepts a thunk that returns a `{ok,...}` verdict.
|
|
164
|
+
*
|
|
165
|
+
* @param {() => ({ok:boolean, reason?:string})} fn
|
|
166
|
+
* @returns {{ok:boolean, reason?:string}}
|
|
167
|
+
*/
|
|
168
|
+
export function quarantine(fn) {
|
|
169
|
+
try {
|
|
170
|
+
const v = fn();
|
|
171
|
+
if (v && typeof v.ok === "boolean") return v;
|
|
172
|
+
return { ok: false, reason: "verifier returned no verdict" };
|
|
173
|
+
} catch (err) {
|
|
174
|
+
return { ok: false, reason: `verify threw: ${err && err.message ? err.message : String(err)}` };
|
|
175
|
+
}
|
|
176
|
+
}
|