@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,363 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Cross-channel DM pairing gate (default-DENY)
|
|
3
|
+
*
|
|
4
|
+
* WS2 (design §5) makes every channel default-deny for *unknown DM senders*:
|
|
5
|
+
* a stranger who DMs the agent on Telegram/WhatsApp/Slack is dropped (with a
|
|
6
|
+
* one-time pairing prompt) unless their peer id is on the allowlist. Pairing
|
|
7
|
+
* is the bootstrap: the operator runs `maestro pair` (or the agent emits a
|
|
8
|
+
* code), the human replies `pair <code>` in the DM, and that peer id is
|
|
9
|
+
* appended to `config/allowlist.yaml` for the channel.
|
|
10
|
+
*
|
|
11
|
+
* This module is enforced by `BaseAdapter.start`'s onInbound wrapper, so every
|
|
12
|
+
* channel inherits the same policy. Three concerns live here so the existing
|
|
13
|
+
* Slack self-message / peer-agent drops fold into one place:
|
|
14
|
+
*
|
|
15
|
+
* - `checkAllowed(ev, { agentRoot })` → { allowed, reason, isPairingCommand }
|
|
16
|
+
* The gate. Returns allowed=false for unknown DM senders (reason
|
|
17
|
+
* "not-paired"), self-messages ("self-message"), peer agents
|
|
18
|
+
* ("peer-agent"). Non-DM/group traffic bypasses pairing (membership +
|
|
19
|
+
* directed-gate govern it instead) → allowed=true, reason "non-dm".
|
|
20
|
+
* `^pair <code>$` is flagged isPairingCommand so the caller intercepts
|
|
21
|
+
* it before the classifier ever sees it.
|
|
22
|
+
* - `startPairing(channel, { agentRoot, ttlMs })` → { code, expiresAt }
|
|
23
|
+
* Mints a short unambiguous code, persists it under
|
|
24
|
+
* state/channels/pairing/<code>.json (chmod 0600) with a TTL.
|
|
25
|
+
* - `redeemPairing(channel, peerId, code, { agentRoot })` → { ok, reason? }
|
|
26
|
+
* Validates an unexpired code and appends the peer to the allowlist.
|
|
27
|
+
*
|
|
28
|
+
* Allowlist format (config/allowlist.yaml) — intentionally a tiny, explicit
|
|
29
|
+
* YAML map parsed/written by a built-in mini-parser so the hot
|
|
30
|
+
* `checkAllowed` path needs no dynamic js-yaml import and the module stays
|
|
31
|
+
* fully hermetic:
|
|
32
|
+
*
|
|
33
|
+
* slack:
|
|
34
|
+
* - U0123ABC
|
|
35
|
+
* telegram:
|
|
36
|
+
* - "987654321" # paired 2026-06-09 via code 7Q4K
|
|
37
|
+
*
|
|
38
|
+
* @module lib/channels/pairing
|
|
39
|
+
*/
|
|
40
|
+
|
|
41
|
+
"use strict";
|
|
42
|
+
|
|
43
|
+
import { join } from "node:path";
|
|
44
|
+
import {
|
|
45
|
+
existsSync,
|
|
46
|
+
readFileSync,
|
|
47
|
+
writeFileSync,
|
|
48
|
+
mkdirSync,
|
|
49
|
+
readdirSync,
|
|
50
|
+
unlinkSync,
|
|
51
|
+
rmSync,
|
|
52
|
+
chmodSync,
|
|
53
|
+
} from "node:fs";
|
|
54
|
+
import { randomInt } from "node:crypto";
|
|
55
|
+
|
|
56
|
+
const PAIRING_DIR = "state/channels/pairing";
|
|
57
|
+
const ALLOWLIST_FILE = "config/allowlist.yaml";
|
|
58
|
+
const DEFAULT_TTL_MS = 15 * 60 * 1000; // 15 minutes
|
|
59
|
+
|
|
60
|
+
// Unambiguous alphabet: no 0/O, 1/I/L — avoids transcription errors when a
|
|
61
|
+
// human reads a code off a screen and types it into a chat.
|
|
62
|
+
const CODE_ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
|
|
63
|
+
const CODE_LENGTH = 6;
|
|
64
|
+
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Allowlist (config/allowlist.yaml) — tiny, dependency-free YAML map.
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Parse the minimal allowlist YAML into `{ channel: Set<peerId> }`. Tolerant:
|
|
71
|
+
* unknown lines are ignored, ids may be quoted, inline `#` comments stripped.
|
|
72
|
+
* Returns an empty map when the file is missing or malformed (default-deny).
|
|
73
|
+
*
|
|
74
|
+
* @param {string} agentRoot
|
|
75
|
+
* @returns {Record<string, Set<string>>}
|
|
76
|
+
*/
|
|
77
|
+
export function readAllowlist(agentRoot) {
|
|
78
|
+
const path = join(agentRoot, ALLOWLIST_FILE);
|
|
79
|
+
const out = {};
|
|
80
|
+
if (!existsSync(path)) return out;
|
|
81
|
+
let body;
|
|
82
|
+
try {
|
|
83
|
+
body = readFileSync(path, "utf-8");
|
|
84
|
+
} catch {
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
let currentChannel = null;
|
|
88
|
+
for (const raw of body.split("\n")) {
|
|
89
|
+
const line = raw.replace(/\t/g, " ");
|
|
90
|
+
if (!line.trim() || line.trim().startsWith("#")) continue;
|
|
91
|
+
// Top-level key: "<channel>:" (no leading whitespace).
|
|
92
|
+
const keyMatch = line.match(/^([A-Za-z][\w-]*):\s*(.*)$/);
|
|
93
|
+
if (keyMatch && !/^\s/.test(line)) {
|
|
94
|
+
currentChannel = keyMatch[1];
|
|
95
|
+
if (!out[currentChannel]) out[currentChannel] = new Set();
|
|
96
|
+
// Inline `[a, b]` flow list support (rare, but cheap).
|
|
97
|
+
const inline = keyMatch[2].trim();
|
|
98
|
+
if (inline.startsWith("[") && inline.endsWith("]")) {
|
|
99
|
+
for (const tok of inline.slice(1, -1).split(",")) {
|
|
100
|
+
const id = stripIdToken(tok);
|
|
101
|
+
if (id) out[currentChannel].add(id);
|
|
102
|
+
}
|
|
103
|
+
currentChannel = null; // inline list closes the key
|
|
104
|
+
}
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
// List item: "- <id>" under the current channel.
|
|
108
|
+
const itemMatch = line.match(/^\s*-\s*(.+)$/);
|
|
109
|
+
if (itemMatch && currentChannel) {
|
|
110
|
+
const id = stripIdToken(itemMatch[1]);
|
|
111
|
+
if (id) out[currentChannel].add(id);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function stripIdToken(tok) {
|
|
118
|
+
let t = String(tok).trim();
|
|
119
|
+
// Quoted value (possibly followed by a trailing comment): capture the quoted
|
|
120
|
+
// span and discard everything after the closing quote.
|
|
121
|
+
const q = t.match(/^(['"])(.*?)\1/);
|
|
122
|
+
if (q) return q[2].trim();
|
|
123
|
+
// Unquoted: a `#` starts an inline comment.
|
|
124
|
+
const hash = t.indexOf("#");
|
|
125
|
+
if (hash >= 0) t = t.slice(0, hash).trim();
|
|
126
|
+
return t.trim();
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Append a peer id to a channel's allowlist (idempotent). Creates the file +
|
|
131
|
+
* the channel key if absent. Returns true when a new id was added, false when
|
|
132
|
+
* it was already present.
|
|
133
|
+
*
|
|
134
|
+
* @param {string} agentRoot
|
|
135
|
+
* @param {string} channel
|
|
136
|
+
* @param {string} peerId
|
|
137
|
+
* @returns {boolean} whether the id was newly added
|
|
138
|
+
*/
|
|
139
|
+
export function appendToAllowlist(agentRoot, channel, peerId, note) {
|
|
140
|
+
const path = join(agentRoot, ALLOWLIST_FILE);
|
|
141
|
+
const current = readAllowlist(agentRoot);
|
|
142
|
+
if (current[channel] && current[channel].has(String(peerId))) return false;
|
|
143
|
+
|
|
144
|
+
mkdirSync(join(agentRoot, "config"), { recursive: true });
|
|
145
|
+
let body = existsSync(path) ? readFileSync(path, "utf-8") : "";
|
|
146
|
+
if (body && !body.endsWith("\n")) body += "\n";
|
|
147
|
+
|
|
148
|
+
const quoted = /^\d+$/.test(String(peerId)) ? `"${peerId}"` : String(peerId);
|
|
149
|
+
const comment = note ? ` # ${note}` : "";
|
|
150
|
+
|
|
151
|
+
// If the channel key already exists, insert the item right after it;
|
|
152
|
+
// otherwise append a new key block at the end.
|
|
153
|
+
const keyLine = new RegExp(`^${escapeRegExp(channel)}:\\s*$`, "m");
|
|
154
|
+
if (keyLine.test(body)) {
|
|
155
|
+
body = body.replace(keyLine, (m) => `${m}\n - ${quoted}${comment}`);
|
|
156
|
+
} else {
|
|
157
|
+
body += `${channel}:\n - ${quoted}${comment}\n`;
|
|
158
|
+
}
|
|
159
|
+
writeFileSync(path, body);
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function escapeRegExp(s) {
|
|
164
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// ---------------------------------------------------------------------------
|
|
168
|
+
// Pairing-command detection.
|
|
169
|
+
// ---------------------------------------------------------------------------
|
|
170
|
+
|
|
171
|
+
const PAIR_COMMAND_RE = /^\s*pair\s+([A-Za-z0-9]{4,12})\s*$/i;
|
|
172
|
+
|
|
173
|
+
/** Extract a pairing code from a message body, or null. */
|
|
174
|
+
export function parsePairCommand(text) {
|
|
175
|
+
if (typeof text !== "string") return null;
|
|
176
|
+
const m = text.match(PAIR_COMMAND_RE);
|
|
177
|
+
return m ? m[1].toUpperCase() : null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
// checkAllowed — the gate.
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* @param {import("./contract.mjs").MessageEvent & Record<string,any>} ev
|
|
186
|
+
* @param {object} opts
|
|
187
|
+
* @param {string} opts.agentRoot
|
|
188
|
+
* @param {string} [opts.ownId] the agent's own peer id on this channel (drop self)
|
|
189
|
+
* @param {Set<string>|string[]} [opts.peerAgentIds] peer-agent ids to drop
|
|
190
|
+
* @returns {{ allowed: boolean, reason: string, isPairingCommand: boolean, code?: string }}
|
|
191
|
+
*/
|
|
192
|
+
export function checkAllowed(ev, opts = {}) {
|
|
193
|
+
const agentRoot = opts.agentRoot;
|
|
194
|
+
const source = ev?.source || {};
|
|
195
|
+
const scope = source.userScope || (isDmEvent(ev) ? "dm" : "group");
|
|
196
|
+
const senderId = String(ev?.from?.id ?? source.peerId ?? "");
|
|
197
|
+
|
|
198
|
+
// A pairing command is always recognised (even from an unknown sender) so
|
|
199
|
+
// the adapter can intercept + redeem it before the classifier.
|
|
200
|
+
const code = parsePairCommand(ev?.text);
|
|
201
|
+
const isPairingCommand = !!code;
|
|
202
|
+
|
|
203
|
+
// Self-message: the agent's own outbound echoed back.
|
|
204
|
+
if (opts.ownId && senderId && senderId === String(opts.ownId)) {
|
|
205
|
+
return { allowed: false, reason: "self-message", isPairingCommand, code: code || undefined };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Peer agent: another maestro agent — never converse (loops two daemons).
|
|
209
|
+
const peers = toSet(opts.peerAgentIds);
|
|
210
|
+
if (senderId && peers.has(senderId)) {
|
|
211
|
+
return { allowed: false, reason: "peer-agent", isPairingCommand, code: code || undefined };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Non-DM (group/channel) traffic bypasses pairing — membership + the
|
|
215
|
+
// directed-gate decide there.
|
|
216
|
+
if (scope !== "dm") {
|
|
217
|
+
return { allowed: true, reason: "non-dm", isPairingCommand, code: code || undefined };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// DM path: default-DENY unless the sender is on the allowlist.
|
|
221
|
+
const allow = readAllowlist(agentRoot || ".");
|
|
222
|
+
const channel = source.channel || ev?.channel || "unknown";
|
|
223
|
+
const paired = !!(allow[channel] && allow[channel].has(senderId));
|
|
224
|
+
if (paired) {
|
|
225
|
+
return { allowed: true, reason: "paired", isPairingCommand, code: code || undefined };
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Unknown DM sender. The command itself is allowed to *reach the adapter*
|
|
229
|
+
// (so it can redeem), but it is NOT a normal allowed message.
|
|
230
|
+
return { allowed: false, reason: "not-paired", isPairingCommand, code: code || undefined };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function isDmEvent(ev) {
|
|
234
|
+
const ch = ev?.channel_label || ev?.channel || "";
|
|
235
|
+
const cid = ev?.channel_id || ev?.source?.chatId || "";
|
|
236
|
+
return (typeof ch === "string" && ch.startsWith("dm/")) || (typeof cid === "string" && cid.startsWith("D"));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function toSet(v) {
|
|
240
|
+
if (v instanceof Set) return v;
|
|
241
|
+
if (Array.isArray(v)) return new Set(v.map(String));
|
|
242
|
+
return new Set();
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// ---------------------------------------------------------------------------
|
|
246
|
+
// startPairing / redeemPairing.
|
|
247
|
+
// ---------------------------------------------------------------------------
|
|
248
|
+
|
|
249
|
+
/** Mint a random unambiguous pairing code. */
|
|
250
|
+
export function mintCode() {
|
|
251
|
+
let s = "";
|
|
252
|
+
for (let i = 0; i < CODE_LENGTH; i++) s += CODE_ALPHABET[randomInt(CODE_ALPHABET.length)];
|
|
253
|
+
return s;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Start a pairing for `channel`: mint a code, persist it (0600) with a TTL.
|
|
258
|
+
*
|
|
259
|
+
* @param {string} channel
|
|
260
|
+
* @param {object} opts
|
|
261
|
+
* @param {string} opts.agentRoot
|
|
262
|
+
* @param {number} [opts.ttlMs]
|
|
263
|
+
* @param {()=>number} [opts.now]
|
|
264
|
+
* @returns {{ code: string, expiresAt: number }}
|
|
265
|
+
*/
|
|
266
|
+
export function startPairing(channel, opts = {}) {
|
|
267
|
+
const agentRoot = opts.agentRoot || ".";
|
|
268
|
+
const ttlMs = typeof opts.ttlMs === "number" && opts.ttlMs > 0 ? opts.ttlMs : DEFAULT_TTL_MS;
|
|
269
|
+
const now = (opts.now || Date.now)();
|
|
270
|
+
const dir = join(agentRoot, PAIRING_DIR);
|
|
271
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
272
|
+
|
|
273
|
+
const code = mintCode();
|
|
274
|
+
const expiresAt = now + ttlMs;
|
|
275
|
+
const file = join(dir, `${code}.json`);
|
|
276
|
+
const payload = JSON.stringify({ channel, code, createdAt: now, expiresAt }, null, 2) + "\n";
|
|
277
|
+
// Write then chmod 0600 (the code is a short-lived shared secret). The mode
|
|
278
|
+
// arg on writeFileSync only applies on create; chmod enforces it even when
|
|
279
|
+
// the file pre-existed or umask widened it.
|
|
280
|
+
writeFileSync(file, payload, { mode: 0o600 });
|
|
281
|
+
try { chmodSync(file, 0o600); } catch { /* best-effort */ }
|
|
282
|
+
return { code, expiresAt };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Redeem a pairing code for a peer id on a channel. Validates the code exists,
|
|
287
|
+
* is unexpired, and (when stored) matches the channel; then appends the peer to
|
|
288
|
+
* the allowlist and consumes the code file.
|
|
289
|
+
*
|
|
290
|
+
* @param {string} channel
|
|
291
|
+
* @param {string} peerId
|
|
292
|
+
* @param {string} code
|
|
293
|
+
* @param {object} opts
|
|
294
|
+
* @param {string} opts.agentRoot
|
|
295
|
+
* @param {()=>number} [opts.now]
|
|
296
|
+
* @returns {{ ok: boolean, reason?: string, added?: boolean }}
|
|
297
|
+
*/
|
|
298
|
+
export function redeemPairing(channel, peerId, code, opts = {}) {
|
|
299
|
+
const agentRoot = opts.agentRoot || ".";
|
|
300
|
+
const now = (opts.now || Date.now)();
|
|
301
|
+
const normalized = String(code || "").toUpperCase();
|
|
302
|
+
if (!normalized) return { ok: false, reason: "no-code" };
|
|
303
|
+
if (!peerId) return { ok: false, reason: "no-peer" };
|
|
304
|
+
|
|
305
|
+
const file = join(agentRoot, PAIRING_DIR, `${normalized}.json`);
|
|
306
|
+
if (!existsSync(file)) return { ok: false, reason: "unknown-code" };
|
|
307
|
+
|
|
308
|
+
let rec;
|
|
309
|
+
try {
|
|
310
|
+
rec = JSON.parse(readFileSync(file, "utf-8"));
|
|
311
|
+
} catch {
|
|
312
|
+
return { ok: false, reason: "corrupt-code" };
|
|
313
|
+
}
|
|
314
|
+
if (typeof rec.expiresAt === "number" && now > rec.expiresAt) {
|
|
315
|
+
try { unlinkSync(file); } catch { /* */ }
|
|
316
|
+
return { ok: false, reason: "expired" };
|
|
317
|
+
}
|
|
318
|
+
if (rec.channel && rec.channel !== channel) {
|
|
319
|
+
return { ok: false, reason: "channel-mismatch" };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const added = appendToAllowlist(agentRoot, channel, peerId, `paired ${new Date(now).toISOString().slice(0, 10)} via code ${normalized}`);
|
|
323
|
+
// Consume the code (single-use).
|
|
324
|
+
try { unlinkSync(file); } catch { /* */ }
|
|
325
|
+
return { ok: true, added };
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Sweep expired pairing codes. Returns the number removed. Best-effort.
|
|
330
|
+
* @param {string} agentRoot
|
|
331
|
+
* @param {()=>number} [now]
|
|
332
|
+
*/
|
|
333
|
+
export function sweepExpiredCodes(agentRoot, now = Date.now) {
|
|
334
|
+
const dir = join(agentRoot, PAIRING_DIR);
|
|
335
|
+
if (!existsSync(dir)) return 0;
|
|
336
|
+
let removed = 0;
|
|
337
|
+
const t = now();
|
|
338
|
+
for (const f of readdirSync(dir)) {
|
|
339
|
+
if (!f.endsWith(".json")) continue;
|
|
340
|
+
const file = join(dir, f);
|
|
341
|
+
try {
|
|
342
|
+
const rec = JSON.parse(readFileSync(file, "utf-8"));
|
|
343
|
+
if (typeof rec.expiresAt === "number" && t > rec.expiresAt) {
|
|
344
|
+
rmSync(file, { force: true });
|
|
345
|
+
removed++;
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
/* leave unreadable files for the operator */
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
return removed;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
export default {
|
|
355
|
+
readAllowlist,
|
|
356
|
+
appendToAllowlist,
|
|
357
|
+
parsePairCommand,
|
|
358
|
+
checkAllowed,
|
|
359
|
+
mintCode,
|
|
360
|
+
startPairing,
|
|
361
|
+
redeemPairing,
|
|
362
|
+
sweepExpiredCodes,
|
|
363
|
+
};
|
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* pairing.test.mjs — node:test coverage for lib/channels/pairing.mjs
|
|
3
|
+
*
|
|
4
|
+
* Covers: default-deny for unknown DM senders, self/peer drops, non-DM bypass,
|
|
5
|
+
* pair-command interception, startPairing TTL + 0600 perms, redeemPairing
|
|
6
|
+
* (happy path, expired, unknown, channel-mismatch, single-use), allowlist
|
|
7
|
+
* read/append round-trip. Hermetic — uses an injectable `now` and tmpdirs.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { test } from "node:test";
|
|
11
|
+
import assert from "node:assert/strict";
|
|
12
|
+
import { promises as fsp } from "node:fs";
|
|
13
|
+
import { statSync, existsSync } from "node:fs";
|
|
14
|
+
import { tmpdir } from "node:os";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
checkAllowed,
|
|
19
|
+
startPairing,
|
|
20
|
+
redeemPairing,
|
|
21
|
+
parsePairCommand,
|
|
22
|
+
readAllowlist,
|
|
23
|
+
appendToAllowlist,
|
|
24
|
+
sweepExpiredCodes,
|
|
25
|
+
mintCode,
|
|
26
|
+
} from "./pairing.mjs";
|
|
27
|
+
|
|
28
|
+
async function makeRoot() {
|
|
29
|
+
const p = join(tmpdir(), `pairing-test-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`);
|
|
30
|
+
await fsp.mkdir(p, { recursive: true });
|
|
31
|
+
return p;
|
|
32
|
+
}
|
|
33
|
+
async function rmRoot(p) { try { await fsp.rm(p, { recursive: true, force: true }); } catch { /* */ } }
|
|
34
|
+
|
|
35
|
+
function dmEvent(channel, senderId, text = "hello") {
|
|
36
|
+
return {
|
|
37
|
+
channel,
|
|
38
|
+
text,
|
|
39
|
+
from: { id: senderId },
|
|
40
|
+
source: { channel, chatId: senderId, userScope: "dm", peerId: senderId },
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function groupEvent(channel, senderId, text = "hello team") {
|
|
44
|
+
return {
|
|
45
|
+
channel,
|
|
46
|
+
text,
|
|
47
|
+
from: { id: senderId },
|
|
48
|
+
source: { channel, chatId: "G1", userScope: "group", peerId: senderId },
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// checkAllowed — default-DENY
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
test("unknown DM sender is denied with reason not-paired", async () => {
|
|
57
|
+
const root = await makeRoot();
|
|
58
|
+
try {
|
|
59
|
+
const res = checkAllowed(dmEvent("telegram", "987"), { agentRoot: root });
|
|
60
|
+
assert.equal(res.allowed, false);
|
|
61
|
+
assert.equal(res.reason, "not-paired");
|
|
62
|
+
assert.equal(res.isPairingCommand, false);
|
|
63
|
+
} finally { await rmRoot(root); }
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("paired DM sender is allowed", async () => {
|
|
67
|
+
const root = await makeRoot();
|
|
68
|
+
try {
|
|
69
|
+
appendToAllowlist(root, "telegram", "987");
|
|
70
|
+
const res = checkAllowed(dmEvent("telegram", "987"), { agentRoot: root });
|
|
71
|
+
assert.equal(res.allowed, true);
|
|
72
|
+
assert.equal(res.reason, "paired");
|
|
73
|
+
} finally { await rmRoot(root); }
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("self-message is dropped", async () => {
|
|
77
|
+
const root = await makeRoot();
|
|
78
|
+
try {
|
|
79
|
+
const res = checkAllowed(dmEvent("slack", "UAGENT"), { agentRoot: root, ownId: "UAGENT" });
|
|
80
|
+
assert.equal(res.allowed, false);
|
|
81
|
+
assert.equal(res.reason, "self-message");
|
|
82
|
+
} finally { await rmRoot(root); }
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("peer-agent message is dropped", async () => {
|
|
86
|
+
const root = await makeRoot();
|
|
87
|
+
try {
|
|
88
|
+
const res = checkAllowed(dmEvent("slack", "UPEER"), { agentRoot: root, peerAgentIds: ["UPEER", "UPEER2"] });
|
|
89
|
+
assert.equal(res.allowed, false);
|
|
90
|
+
assert.equal(res.reason, "peer-agent");
|
|
91
|
+
} finally { await rmRoot(root); }
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
test("non-DM (group) traffic bypasses pairing", async () => {
|
|
95
|
+
const root = await makeRoot();
|
|
96
|
+
try {
|
|
97
|
+
// Unknown sender, but in a group → allowed (membership + directed-gate govern).
|
|
98
|
+
const res = checkAllowed(groupEvent("slack", "USTRANGER"), { agentRoot: root });
|
|
99
|
+
assert.equal(res.allowed, true);
|
|
100
|
+
assert.equal(res.reason, "non-dm");
|
|
101
|
+
} finally { await rmRoot(root); }
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
test("pair command is flagged isPairingCommand even from an unknown DM sender", async () => {
|
|
105
|
+
const root = await makeRoot();
|
|
106
|
+
try {
|
|
107
|
+
const res = checkAllowed(dmEvent("telegram", "987", "pair 7Q4KMN"), { agentRoot: root });
|
|
108
|
+
assert.equal(res.isPairingCommand, true);
|
|
109
|
+
assert.equal(res.code, "7Q4KMN");
|
|
110
|
+
// Still not a normal allowed message until redeemed.
|
|
111
|
+
assert.equal(res.allowed, false);
|
|
112
|
+
assert.equal(res.reason, "not-paired");
|
|
113
|
+
} finally { await rmRoot(root); }
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("parsePairCommand matches ^pair <code>$ only", () => {
|
|
117
|
+
assert.equal(parsePairCommand("pair ABCD"), "ABCD");
|
|
118
|
+
assert.equal(parsePairCommand("PAIR abcd23"), "ABCD23");
|
|
119
|
+
assert.equal(parsePairCommand(" pair 7q4k "), "7Q4K");
|
|
120
|
+
assert.equal(parsePairCommand("please pair ABCD now"), null);
|
|
121
|
+
assert.equal(parsePairCommand("pairing"), null);
|
|
122
|
+
assert.equal(parsePairCommand(""), null);
|
|
123
|
+
assert.equal(parsePairCommand(undefined), null);
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// ---------------------------------------------------------------------------
|
|
127
|
+
// startPairing — code + TTL + 0600
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
|
|
130
|
+
test("startPairing mints a code, persists it 0600, with a TTL", async () => {
|
|
131
|
+
const root = await makeRoot();
|
|
132
|
+
try {
|
|
133
|
+
const t0 = 1_000_000;
|
|
134
|
+
const { code, expiresAt } = startPairing("telegram", { agentRoot: root, ttlMs: 60_000, now: () => t0 });
|
|
135
|
+
assert.match(code, /^[A-Z0-9]{6}$/);
|
|
136
|
+
assert.equal(expiresAt, t0 + 60_000);
|
|
137
|
+
const file = join(root, "state/channels/pairing", `${code}.json`);
|
|
138
|
+
assert.ok(existsSync(file));
|
|
139
|
+
// POSIX perms: 0600 (owner rw only). Mask to the permission bits.
|
|
140
|
+
const mode = statSync(file).mode & 0o777;
|
|
141
|
+
assert.equal(mode, 0o600, `expected 0600, got ${mode.toString(8)}`);
|
|
142
|
+
} finally { await rmRoot(root); }
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
test("mintCode uses an unambiguous alphabet (no 0/O/1/I/L)", () => {
|
|
146
|
+
for (let i = 0; i < 50; i++) {
|
|
147
|
+
const c = mintCode();
|
|
148
|
+
assert.doesNotMatch(c, /[01OIL]/, `code ${c} contains an ambiguous char`);
|
|
149
|
+
}
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ---------------------------------------------------------------------------
|
|
153
|
+
// redeemPairing
|
|
154
|
+
// ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
test("redeemPairing: happy path adds the peer and consumes the code", async () => {
|
|
157
|
+
const root = await makeRoot();
|
|
158
|
+
try {
|
|
159
|
+
const t0 = 5_000;
|
|
160
|
+
const { code } = startPairing("telegram", { agentRoot: root, ttlMs: 60_000, now: () => t0 });
|
|
161
|
+
const res = redeemPairing("telegram", "987", code, { agentRoot: root, now: () => t0 + 1000 });
|
|
162
|
+
assert.equal(res.ok, true);
|
|
163
|
+
assert.equal(res.added, true);
|
|
164
|
+
// Peer is now on the allowlist.
|
|
165
|
+
const allow = readAllowlist(root);
|
|
166
|
+
assert.ok(allow.telegram.has("987"));
|
|
167
|
+
// Code file consumed (single-use).
|
|
168
|
+
assert.ok(!existsSync(join(root, "state/channels/pairing", `${code}.json`)));
|
|
169
|
+
// A now-allowed DM passes the gate.
|
|
170
|
+
const gate = checkAllowed(dmEvent("telegram", "987"), { agentRoot: root });
|
|
171
|
+
assert.equal(gate.allowed, true);
|
|
172
|
+
} finally { await rmRoot(root); }
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("redeemPairing: expired code is rejected and cleaned up", async () => {
|
|
176
|
+
const root = await makeRoot();
|
|
177
|
+
try {
|
|
178
|
+
const t0 = 10_000;
|
|
179
|
+
const { code } = startPairing("telegram", { agentRoot: root, ttlMs: 1000, now: () => t0 });
|
|
180
|
+
const res = redeemPairing("telegram", "987", code, { agentRoot: root, now: () => t0 + 5000 });
|
|
181
|
+
assert.equal(res.ok, false);
|
|
182
|
+
assert.equal(res.reason, "expired");
|
|
183
|
+
assert.ok(!existsSync(join(root, "state/channels/pairing", `${code}.json`)));
|
|
184
|
+
// Not added.
|
|
185
|
+
assert.equal(readAllowlist(root).telegram, undefined);
|
|
186
|
+
} finally { await rmRoot(root); }
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("redeemPairing: unknown code rejected", async () => {
|
|
190
|
+
const root = await makeRoot();
|
|
191
|
+
try {
|
|
192
|
+
const res = redeemPairing("telegram", "987", "ZZZZZZ", { agentRoot: root });
|
|
193
|
+
assert.equal(res.ok, false);
|
|
194
|
+
assert.equal(res.reason, "unknown-code");
|
|
195
|
+
} finally { await rmRoot(root); }
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test("redeemPairing: channel mismatch rejected, code not consumed", async () => {
|
|
199
|
+
const root = await makeRoot();
|
|
200
|
+
try {
|
|
201
|
+
const t0 = 1000;
|
|
202
|
+
const { code } = startPairing("telegram", { agentRoot: root, ttlMs: 60_000, now: () => t0 });
|
|
203
|
+
const res = redeemPairing("slack", "987", code, { agentRoot: root, now: () => t0 + 100 });
|
|
204
|
+
assert.equal(res.ok, false);
|
|
205
|
+
assert.equal(res.reason, "channel-mismatch");
|
|
206
|
+
// Still present (so the right channel can redeem it).
|
|
207
|
+
assert.ok(existsSync(join(root, "state/channels/pairing", `${code}.json`)));
|
|
208
|
+
} finally { await rmRoot(root); }
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
test("redeemPairing is single-use: a second redeem fails", async () => {
|
|
212
|
+
const root = await makeRoot();
|
|
213
|
+
try {
|
|
214
|
+
const t0 = 1000;
|
|
215
|
+
const { code } = startPairing("telegram", { agentRoot: root, ttlMs: 60_000, now: () => t0 });
|
|
216
|
+
assert.equal(redeemPairing("telegram", "987", code, { agentRoot: root, now: () => t0 + 1 }).ok, true);
|
|
217
|
+
const second = redeemPairing("telegram", "654", code, { agentRoot: root, now: () => t0 + 2 });
|
|
218
|
+
assert.equal(second.ok, false);
|
|
219
|
+
assert.equal(second.reason, "unknown-code");
|
|
220
|
+
} finally { await rmRoot(root); }
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
// allowlist read/append + sweep
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
|
|
227
|
+
test("appendToAllowlist is idempotent and groups under the channel key", async () => {
|
|
228
|
+
const root = await makeRoot();
|
|
229
|
+
try {
|
|
230
|
+
assert.equal(appendToAllowlist(root, "slack", "U1"), true);
|
|
231
|
+
assert.equal(appendToAllowlist(root, "slack", "U1"), false, "duplicate should not re-add");
|
|
232
|
+
assert.equal(appendToAllowlist(root, "slack", "U2"), true);
|
|
233
|
+
assert.equal(appendToAllowlist(root, "telegram", "987"), true);
|
|
234
|
+
const allow = readAllowlist(root);
|
|
235
|
+
assert.deepEqual([...allow.slack].sort(), ["U1", "U2"]);
|
|
236
|
+
assert.ok(allow.telegram.has("987"));
|
|
237
|
+
} finally { await rmRoot(root); }
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
test("readAllowlist tolerates comments, quotes, and inline flow lists", async () => {
|
|
241
|
+
const root = await makeRoot();
|
|
242
|
+
try {
|
|
243
|
+
await fsp.mkdir(join(root, "config"), { recursive: true });
|
|
244
|
+
await fsp.writeFile(join(root, "config/allowlist.yaml"),
|
|
245
|
+
[
|
|
246
|
+
"# cross-channel DM allowlist",
|
|
247
|
+
"slack:",
|
|
248
|
+
" - U1 # casey",
|
|
249
|
+
' - "U2"',
|
|
250
|
+
"telegram: [ \"987\", 654 ]",
|
|
251
|
+
"",
|
|
252
|
+
].join("\n"));
|
|
253
|
+
const allow = readAllowlist(root);
|
|
254
|
+
assert.deepEqual([...allow.slack].sort(), ["U1", "U2"]);
|
|
255
|
+
assert.deepEqual([...allow.telegram].sort(), ["654", "987"]);
|
|
256
|
+
} finally { await rmRoot(root); }
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
test("sweepExpiredCodes removes only expired codes", async () => {
|
|
260
|
+
const root = await makeRoot();
|
|
261
|
+
try {
|
|
262
|
+
const t0 = 1000;
|
|
263
|
+
const fresh = startPairing("telegram", { agentRoot: root, ttlMs: 100_000, now: () => t0 });
|
|
264
|
+
const stale = startPairing("slack", { agentRoot: root, ttlMs: 10, now: () => t0 });
|
|
265
|
+
const removed = sweepExpiredCodes(root, () => t0 + 5000);
|
|
266
|
+
assert.equal(removed, 1);
|
|
267
|
+
assert.ok(existsSync(join(root, "state/channels/pairing", `${fresh.code}.json`)));
|
|
268
|
+
assert.ok(!existsSync(join(root, "state/channels/pairing", `${stale.code}.json`)));
|
|
269
|
+
} finally { await rmRoot(root); }
|
|
270
|
+
});
|