@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,719 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — BaseAdapter (cross-cutting channel logic)
|
|
3
|
+
*
|
|
4
|
+
* WS2 (design §5) factors the logic every real-time channel shares into one
|
|
5
|
+
* base class so concrete adapters implement only the platform-specific hooks:
|
|
6
|
+
*
|
|
7
|
+
* abstract _connect({ onRaw, signal }) open the live stream; call onRaw(raw)
|
|
8
|
+
* per provider frame.
|
|
9
|
+
* abstract _toEvent(raw) → MessageEvent|null translate a raw frame to a
|
|
10
|
+
* contract MessageEvent (null to drop).
|
|
11
|
+
* abstract _send(out) → SendResult send an outbound message.
|
|
12
|
+
* optional _react(out) → SendResult add/remove a reaction.
|
|
13
|
+
* optional _setTyping(source, on) set/clear a typing indicator.
|
|
14
|
+
*
|
|
15
|
+
* BaseAdapter provides:
|
|
16
|
+
* - `start({ onInbound, signal })` — wires _connect → _toEvent → (pairing
|
|
17
|
+
* gate) → onInbound, stamping `ev.channel`. The pairing gate runs in the
|
|
18
|
+
* onInbound wrapper so every channel inherits default-deny DMs + pair-code
|
|
19
|
+
* interception (lib/channels/pairing.mjs). Pairing/gate failures are
|
|
20
|
+
* fail-open-logged, never thrown into the live loop.
|
|
21
|
+
* - `send(out)` — thin wrapper over `_send` with uniform error shaping.
|
|
22
|
+
* - `startTypingHeartbeat(source)` / `stopTypingHeartbeat(source)` — a
|
|
23
|
+
* ~4s setInterval that calls `_setTyping(source, true)`, timeout-guarded so
|
|
24
|
+
* a hung provider call can't wedge the loop. The dispatcher starts the
|
|
25
|
+
* heartbeat on session spawn and stops it on session close.
|
|
26
|
+
* - `react(out)`, `setTyping(...)`, `stop()`, `healthCheck()`.
|
|
27
|
+
*
|
|
28
|
+
* The base is transport-agnostic and hermetic: it never imports a provider SDK.
|
|
29
|
+
* Concrete adapters are dynamic-import-gated (like baileys/grammy).
|
|
30
|
+
*
|
|
31
|
+
* @module lib/channels/base-adapter
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
"use strict";
|
|
35
|
+
|
|
36
|
+
import { resolve } from "node:path";
|
|
37
|
+
import { checkAllowed, startPairing, redeemPairing } from "./pairing.mjs";
|
|
38
|
+
import { screenOutbound as defaultScreenOutbound } from "../comms/send-gate.mjs";
|
|
39
|
+
import { getHookBus } from "../hooks/bus.mjs";
|
|
40
|
+
import { superviseConnection as defaultSupervise, CONN_STATE } from "../util/reconnect.mjs";
|
|
41
|
+
import { bump as defaultBump } from "../diagnostics/counters.mjs";
|
|
42
|
+
|
|
43
|
+
const DEFAULT_TYPING_INTERVAL_MS = 4_000;
|
|
44
|
+
const DEFAULT_TYPING_CALL_TIMEOUT_MS = 3_000;
|
|
45
|
+
|
|
46
|
+
/* ───────────────────── bot-loop / reply-storm guard knobs ─────────────────── */
|
|
47
|
+
// Two agents sharing one channel can ping-pong forever ("ok" → "ok" → …). The
|
|
48
|
+
// pair guard is a per-conversation sliding window: if more than
|
|
49
|
+
// PAIR_GUARD_MAX outbound replies land inside PAIR_GUARD_WINDOW_MS for the same
|
|
50
|
+
// conversation, further sends are suppressed until the window drains. This is a
|
|
51
|
+
// last-ditch circuit breaker BELOW the ownership filter (which already drops a
|
|
52
|
+
// known peer-agent inbound) — it catches the storm even when the other party
|
|
53
|
+
// isn't a *registered* peer agent. Cheap, in-memory, fail-open.
|
|
54
|
+
const PAIR_GUARD_WINDOW_MS = 60_000;
|
|
55
|
+
const PAIR_GUARD_MAX = 6;
|
|
56
|
+
|
|
57
|
+
/** Diagnostic counter names (gaps-product-quality P0-6: count every fail-open). */
|
|
58
|
+
export const CHANNEL_COUNTERS = Object.freeze({
|
|
59
|
+
SEND_GATE_BLOCKED: "channel.send_gate.blocked",
|
|
60
|
+
SEND_GATE_FAILOPEN: "channel.send_gate.fail_open",
|
|
61
|
+
PAIR_GUARD_TRIPPED: "channel.pair_guard.tripped",
|
|
62
|
+
PEER_AGENT_DROPPED: "channel.peer_agent.dropped",
|
|
63
|
+
RECONNECT_DOWN: "channel.reconnect.down",
|
|
64
|
+
RECONNECT_GAVE_UP: "channel.reconnect.gave_up",
|
|
65
|
+
RECONNECT_RECOVERED: "channel.reconnect.recovered",
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export class BaseAdapter {
|
|
69
|
+
/**
|
|
70
|
+
* @param {object} opts
|
|
71
|
+
* @param {string} opts.name channel name (e.g. "telegram")
|
|
72
|
+
* @param {string} [opts.agentRoot]
|
|
73
|
+
* @param {string[]} [opts.capabilities]
|
|
74
|
+
* @param {object} [opts.config] loaded config/<name>.yaml
|
|
75
|
+
* @param {(lvl:string,msg:string)=>void} [opts.log]
|
|
76
|
+
* @param {string} [opts.ownId] the agent's own peer id on this channel
|
|
77
|
+
* @param {Set<string>|string[]} [opts.peerAgentIds]
|
|
78
|
+
* @param {number} [opts.typingIntervalMs]
|
|
79
|
+
* @param {(source:object)=>Promise<void>|void} [opts.onPairingPrompt] one-time
|
|
80
|
+
* reply to an unknown DM sender (e.g. "DM me `pair <code>`"). Optional.
|
|
81
|
+
* @param {"deny"|"open"} [opts.pairingMode="deny"] "deny" = default-deny
|
|
82
|
+
* unknown DM senders (new channels: Telegram/WhatsApp). "open" = skip
|
|
83
|
+
* the pairing gate entirely because channel membership/identity is
|
|
84
|
+
* already governed upstream (Slack: shouldKeepEvent + directed-gate).
|
|
85
|
+
* Behaviour-preserving for the live Slack path.
|
|
86
|
+
* @param {boolean} [opts.sendGate=true] run the shared outbound send-gate
|
|
87
|
+
* (lib/comms/send-gate + hook-bus beforeSend) before every _send. Set
|
|
88
|
+
* false only for a path already gated upstream.
|
|
89
|
+
* @param {boolean} [opts.supervise=true] wrap _connect in the bounded-backoff
|
|
90
|
+
* reconnect supervisor (lib/util/reconnect) so a dropped channel
|
|
91
|
+
* reconnects and a dead channel turns healthCheck() red.
|
|
92
|
+
* @param {object} [opts.screenOutbound] inject the send-gate fn (tests).
|
|
93
|
+
* @param {object} [opts.hookBus] inject a HookBus (tests); default = the
|
|
94
|
+
* process-wide singleton getHookBus().
|
|
95
|
+
* @param {Function} [opts.superviseConnection] inject the reconnect supervisor.
|
|
96
|
+
* @param {Function} [opts.bump] inject the diagnostics counter (tests).
|
|
97
|
+
* @param {string} [opts.jurisdiction] recipient jurisdiction for disclosure.
|
|
98
|
+
* @param {string[]} [opts.internalDomains] email domains treated as internal.
|
|
99
|
+
* @param {() => number} [opts.rng] jitter RNG for the reconnect supervisor.
|
|
100
|
+
*/
|
|
101
|
+
constructor(opts = {}) {
|
|
102
|
+
this.name = opts.name || "channel";
|
|
103
|
+
this.agentRoot = resolve(opts.agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
|
|
104
|
+
this.capabilities = Array.isArray(opts.capabilities) ? opts.capabilities.slice() : [];
|
|
105
|
+
this.config = opts.config || {};
|
|
106
|
+
this.log = opts.log || ((lvl, m) => process.stdout.write(`[${this.name}:${lvl}] ${m}\n`));
|
|
107
|
+
this.ownId = opts.ownId || "";
|
|
108
|
+
this.peerAgentIds = toPeerSet(opts.peerAgentIds);
|
|
109
|
+
this.onPairingPrompt = typeof opts.onPairingPrompt === "function" ? opts.onPairingPrompt : null;
|
|
110
|
+
this.pairingMode = opts.pairingMode === "open" ? "open" : "deny";
|
|
111
|
+
|
|
112
|
+
this._typingIntervalMs = opts.typingIntervalMs || DEFAULT_TYPING_INTERVAL_MS;
|
|
113
|
+
this._setTimeout = opts.setTimeout || globalThis.setTimeout;
|
|
114
|
+
this._clearTimeout = opts.clearTimeout || globalThis.clearTimeout;
|
|
115
|
+
this._setInterval = opts.setInterval || globalThis.setInterval;
|
|
116
|
+
this._clearInterval = opts.clearInterval || globalThis.clearInterval;
|
|
117
|
+
|
|
118
|
+
// -- outbound governance (P0-3): the shared send-gate runs before _send. --
|
|
119
|
+
this._sendGateEnabled = opts.sendGate !== false;
|
|
120
|
+
this._screenOutbound =
|
|
121
|
+
typeof opts.screenOutbound === "function" ? opts.screenOutbound : defaultScreenOutbound;
|
|
122
|
+
this._hookBus = opts.hookBus || null; // resolved lazily so the singleton is shared
|
|
123
|
+
this._jurisdiction = opts.jurisdiction || undefined;
|
|
124
|
+
this._internalDomains = Array.isArray(opts.internalDomains) ? opts.internalDomains.slice() : [];
|
|
125
|
+
|
|
126
|
+
// -- reconnect supervision (P0-5/P0-6). --
|
|
127
|
+
this._superviseEnabled = opts.supervise !== false;
|
|
128
|
+
this._superviseConnection =
|
|
129
|
+
typeof opts.superviseConnection === "function" ? opts.superviseConnection : defaultSupervise;
|
|
130
|
+
// Health-poll interval for the reconnect supervisor: without it the poll is
|
|
131
|
+
// off and a SILENT drop (transport unhealthy but _connect never rejected)
|
|
132
|
+
// goes undetected. Default 30s; 0 disables (tests/adapters with push-based
|
|
133
|
+
// drop signalling via notifyDown).
|
|
134
|
+
this._healthIntervalMs = Number.isFinite(opts.healthIntervalMs) ? opts.healthIntervalMs : 30_000;
|
|
135
|
+
this._supervisor = null;
|
|
136
|
+
this._connState = CONN_STATE.CONNECTING;
|
|
137
|
+
this._rng = typeof opts.rng === "function" ? opts.rng : undefined;
|
|
138
|
+
|
|
139
|
+
// -- diagnostics counter (P0-6): every fail-open is counted somewhere. --
|
|
140
|
+
this._bump = typeof opts.bump === "function" ? opts.bump : defaultBump;
|
|
141
|
+
|
|
142
|
+
this._onInbound = null;
|
|
143
|
+
this._signal = null;
|
|
144
|
+
this._stopping = false;
|
|
145
|
+
/** @type {Map<string, any>} sessionKey → interval handle */
|
|
146
|
+
this._typingTimers = new Map();
|
|
147
|
+
// De-dupe one-time pairing prompts per unknown sender so we don't spam.
|
|
148
|
+
this._promptedPeers = new Set();
|
|
149
|
+
// Sliding-window reply timestamps per conversation key (bot-loop guard).
|
|
150
|
+
/** @type {Map<string, number[]>} */
|
|
151
|
+
this._replyWindow = new Map();
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Resolve the HookBus (the shared singleton unless one was injected). */
|
|
155
|
+
_bus() {
|
|
156
|
+
if (this._hookBus) return this._hookBus;
|
|
157
|
+
try {
|
|
158
|
+
this._hookBus = getHookBus();
|
|
159
|
+
} catch {
|
|
160
|
+
this._hookBus = null; // never let bus construction wedge a send
|
|
161
|
+
}
|
|
162
|
+
return this._hookBus;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Count a guard event without ever throwing into the observed path. */
|
|
166
|
+
_count(name, attrs) {
|
|
167
|
+
try {
|
|
168
|
+
this._bump(name, { channel: this.name, ...(attrs || {}) }, { agentRoot: this.agentRoot });
|
|
169
|
+
} catch {
|
|
170
|
+
/* a counter must never crash the path it observes */
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// -- Abstract hooks (subclasses must override _connect/_toEvent/_send) -----
|
|
175
|
+
|
|
176
|
+
/* eslint-disable no-unused-vars */
|
|
177
|
+
async _connect({ onRaw, signal }) {
|
|
178
|
+
throw new Error(`${this.name}: _connect() not implemented`);
|
|
179
|
+
}
|
|
180
|
+
_toEvent(raw) {
|
|
181
|
+
throw new Error(`${this.name}: _toEvent() not implemented`);
|
|
182
|
+
}
|
|
183
|
+
async _send(out) {
|
|
184
|
+
throw new Error(`${this.name}: _send() not implemented`);
|
|
185
|
+
}
|
|
186
|
+
async _react(out) {
|
|
187
|
+
// Optional — default no-op for channels without reactions.
|
|
188
|
+
return { ok: false, error: "react-not-supported" };
|
|
189
|
+
}
|
|
190
|
+
async _setTyping(source, on) {
|
|
191
|
+
// Optional — default no-op for channels without typing (SMS/Gmail).
|
|
192
|
+
}
|
|
193
|
+
/* eslint-enable no-unused-vars */
|
|
194
|
+
|
|
195
|
+
// -- start: connect → translate → gate → onInbound ------------------------
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* @param {object} args
|
|
199
|
+
* @param {(item:object)=>void} args.onInbound called per accepted MessageEvent
|
|
200
|
+
* @param {AbortSignal} [args.signal]
|
|
201
|
+
*/
|
|
202
|
+
async start({ onInbound, signal }) {
|
|
203
|
+
if (typeof onInbound !== "function") throw new Error(`${this.name} start(): onInbound required`);
|
|
204
|
+
this._onInbound = onInbound;
|
|
205
|
+
this._signal = signal || null;
|
|
206
|
+
this._stopping = false;
|
|
207
|
+
if (signal) signal.addEventListener?.("abort", () => { this._stopping = true; });
|
|
208
|
+
|
|
209
|
+
const onRaw = (raw) => {
|
|
210
|
+
// Translation + gating must never throw into the provider's frame loop.
|
|
211
|
+
let ev;
|
|
212
|
+
try {
|
|
213
|
+
ev = this._toEvent(raw);
|
|
214
|
+
} catch (err) {
|
|
215
|
+
this.log("error", `_toEvent threw: ${err.message}`);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (!ev) return; // adapter chose to drop this frame
|
|
219
|
+
if (!ev.channel) ev.channel = this.name;
|
|
220
|
+
this._handleEvent(ev);
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
// P0-5/P0-6: a live channel that drops must reconnect (bounded backoff) and
|
|
224
|
+
// a dead one must turn healthCheck() red — today nothing supervises this.
|
|
225
|
+
// The supervisor owns the connect loop: each (re)connect attempt calls
|
|
226
|
+
// _connect, and every state transition is surfaced + counted so the channel
|
|
227
|
+
// can never silently die. We still AWAIT the first connect so callers that
|
|
228
|
+
// act immediately after start() (e.g. driving onRaw) see a live adapter.
|
|
229
|
+
if (this._superviseEnabled && typeof this._superviseConnection === "function") {
|
|
230
|
+
this._supervisor = this._superviseConnection({
|
|
231
|
+
connect: () => this._connect({ onRaw, signal }),
|
|
232
|
+
isHealthy: () => this._connectionHealthy(),
|
|
233
|
+
onState: (state, info) => this._onConnState(state, info),
|
|
234
|
+
healthIntervalMs: this._healthIntervalMs,
|
|
235
|
+
setTimeout: this._setTimeout,
|
|
236
|
+
clearTimeout: this._clearTimeout,
|
|
237
|
+
rng: this._rng,
|
|
238
|
+
});
|
|
239
|
+
if (signal) {
|
|
240
|
+
signal.addEventListener?.("abort", () => {
|
|
241
|
+
try { this._supervisor?.stop?.("aborted"); } catch { /* */ }
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
await this._supervisor.started; // first connect attempt
|
|
246
|
+
} catch {
|
|
247
|
+
/* a failed first connect is handled by the supervisor's backoff loop */
|
|
248
|
+
}
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Unsupervised path (supervise:false): connect once, no reconnect.
|
|
253
|
+
await this._connect({ onRaw, signal });
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Per-adapter liveness probe the reconnect supervisor polls (and the daemon
|
|
258
|
+
* health sweep can reuse via healthCheck). Default: live unless the supervisor
|
|
259
|
+
* has reported the connection down/giving-up. Adapters with a cheap socket
|
|
260
|
+
* probe can override. Never throws — a thrown probe is an unhealthy probe.
|
|
261
|
+
* @returns {Promise<boolean>|boolean}
|
|
262
|
+
*/
|
|
263
|
+
_connectionHealthy() {
|
|
264
|
+
return this._connState === CONN_STATE.UP || this._connState === CONN_STATE.CONNECTING;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reconnect-supervisor state sink. Records the current state (so healthCheck
|
|
269
|
+
* reflects it) and counts every transition so a drop/giving-up is never
|
|
270
|
+
* silent (P0-6). Fail-open: a throwing counter can't break the supervisor.
|
|
271
|
+
*/
|
|
272
|
+
_onConnState(state, info = {}) {
|
|
273
|
+
this._connState = state;
|
|
274
|
+
if (state === CONN_STATE.UP) {
|
|
275
|
+
// A reconnection (reconnected:true) is the noteworthy recovery; the very
|
|
276
|
+
// first connect (reconnected:false) is just normal startup.
|
|
277
|
+
if (info.reconnected) {
|
|
278
|
+
this._count(CHANNEL_COUNTERS.RECONNECT_RECOVERED, { attempt: info.attempt });
|
|
279
|
+
this.log("info", `${this.name}: reconnected after a drop`);
|
|
280
|
+
}
|
|
281
|
+
} else if (state === CONN_STATE.DOWN) {
|
|
282
|
+
this._count(CHANNEL_COUNTERS.RECONNECT_DOWN, { reason: info.reason, delayMs: info.delayMs });
|
|
283
|
+
this.log("warn", `${this.name}: connection down (${info.reason || "?"}) — retrying in ${info.delayMs ?? "?"}ms`);
|
|
284
|
+
} else if (state === CONN_STATE.GIVING_UP) {
|
|
285
|
+
this._count(CHANNEL_COUNTERS.RECONNECT_GAVE_UP, { attempts: info.attempts, reason: info.reason });
|
|
286
|
+
this.log("error", `${this.name}: gave up reconnecting after ${info.attempts ?? "?"} attempts — channel is DEAD`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Report an observed connection drop to the supervisor so it reconnects with
|
|
292
|
+
* bounded backoff. Adapters call this from their provider's close/error
|
|
293
|
+
* listener (the channel-adapter path). No-op when unsupervised.
|
|
294
|
+
* @param {string} [reason]
|
|
295
|
+
*/
|
|
296
|
+
notifyConnectionDown(reason) {
|
|
297
|
+
try { this._supervisor?.notifyDown?.(reason); } catch { /* */ }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Run the pairing gate, intercept `pair <code>`, then forward to onInbound.
|
|
302
|
+
* Fail-open: any error here is logged and the event is dropped rather than
|
|
303
|
+
* crashing the loop (a single bad event must not take the channel down).
|
|
304
|
+
*/
|
|
305
|
+
_handleEvent(ev) {
|
|
306
|
+
// Ownership / bot-loop backstop (P0-5): an inbound from the agent's OWN id
|
|
307
|
+
// or a KNOWN peer-agent id is never auto-replied — two collaborating agents
|
|
308
|
+
// in one channel would otherwise reply-storm. This runs in BOTH pairing
|
|
309
|
+
// modes, including "open" (Slack), where the bus factory now wires ownId /
|
|
310
|
+
// peerAgentIds but the directed-gate alone wouldn't stop a peer loop.
|
|
311
|
+
if (this._isOwnOrPeer(ev)) {
|
|
312
|
+
this._count(CHANNEL_COUNTERS.PEER_AGENT_DROPPED, { from: this._eventSenderId(ev) });
|
|
313
|
+
this.log("debug", `dropping own/peer-agent inbound (no auto-reply)`);
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// "open" channels (Slack) bypass the pairing gate — their membership and
|
|
318
|
+
// self/peer filtering already happened upstream (shouldKeepEvent) and the
|
|
319
|
+
// daemon's directed-gate governs the rest. Forwarding straight through
|
|
320
|
+
// keeps the live path byte-for-byte unchanged.
|
|
321
|
+
if (this.pairingMode === "open") {
|
|
322
|
+
try { this._onInbound(ev); }
|
|
323
|
+
catch (err) { this.log("error", `onInbound threw: ${err.message}`); }
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
let gate;
|
|
328
|
+
try {
|
|
329
|
+
gate = checkAllowed(ev, {
|
|
330
|
+
agentRoot: this.agentRoot,
|
|
331
|
+
ownId: this.ownId,
|
|
332
|
+
peerAgentIds: this.peerAgentIds,
|
|
333
|
+
});
|
|
334
|
+
} catch (err) {
|
|
335
|
+
this.log("error", `pairing gate threw, dropping event: ${err.message}`);
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// `pair <code>` is intercepted BEFORE the classifier — it never reaches
|
|
340
|
+
// the inbox. Redeem it and (best-effort) acknowledge.
|
|
341
|
+
if (gate.isPairingCommand && gate.code) {
|
|
342
|
+
this._redeemAndAck(ev, gate.code);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (gate.allowed) {
|
|
347
|
+
try {
|
|
348
|
+
this._onInbound(ev);
|
|
349
|
+
} catch (err) {
|
|
350
|
+
this.log("error", `onInbound threw: ${err.message}`);
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Denied. For an unknown DM sender, emit a single pairing prompt (once)
|
|
356
|
+
// so a legitimate human knows how to pair. Other reasons (self/peer) are
|
|
357
|
+
// silent drops.
|
|
358
|
+
if (gate.reason === "not-paired") {
|
|
359
|
+
this._maybePromptPairing(ev);
|
|
360
|
+
}
|
|
361
|
+
this.log("debug", `event dropped: ${gate.reason}`);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
_redeemAndAck(ev, code) {
|
|
365
|
+
const source = ev.source || {};
|
|
366
|
+
const channel = source.channel || ev.channel || this.name;
|
|
367
|
+
const peerId = String(ev.from?.id ?? source.peerId ?? "");
|
|
368
|
+
let res = { ok: false, reason: "no-peer" };
|
|
369
|
+
try {
|
|
370
|
+
res = redeemPairing(channel, peerId, code, { agentRoot: this.agentRoot });
|
|
371
|
+
} catch (err) {
|
|
372
|
+
this.log("error", `redeemPairing threw: ${err.message}`);
|
|
373
|
+
}
|
|
374
|
+
// Best-effort acknowledgement back to the sender. This is a fixed,
|
|
375
|
+
// framework-authored control string (not model output and not user-facing
|
|
376
|
+
// content under disclosure/barrier policy), so it bypasses the outbound
|
|
377
|
+
// policy gate — otherwise the fail-closed external default would silently
|
|
378
|
+
// swallow the pairing handshake itself.
|
|
379
|
+
const text = res.ok
|
|
380
|
+
? "Paired. You can message me here now."
|
|
381
|
+
: `That pairing code didn't work (${res.reason}). Ask for a fresh one.`;
|
|
382
|
+
this._systemSend({ to: source.chatId || peerId, text, source }).catch(() => {});
|
|
383
|
+
this.log("info", `pair attempt from ${peerId}: ${res.ok ? "ok" : res.reason}`);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
_maybePromptPairing(ev) {
|
|
387
|
+
const peerId = String(ev.from?.id ?? ev.source?.peerId ?? "");
|
|
388
|
+
if (!peerId || this._promptedPeers.has(peerId)) return;
|
|
389
|
+
this._promptedPeers.add(peerId);
|
|
390
|
+
if (!this.onPairingPrompt) return;
|
|
391
|
+
Promise.resolve()
|
|
392
|
+
.then(() => this.onPairingPrompt(ev.source || { channel: this.name, chatId: peerId }))
|
|
393
|
+
.catch((err) => this.log("warn", `pairing prompt failed: ${err.message}`));
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// -- ownership / bot-loop guard (P0-5) ------------------------------------
|
|
397
|
+
|
|
398
|
+
/** The sender peer id of an inbound event (best-effort, stringified). */
|
|
399
|
+
_eventSenderId(ev) {
|
|
400
|
+
return String(ev?.from?.id ?? ev?.source?.peerId ?? "");
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Is this inbound from the agent's own id or a registered peer agent? Used to
|
|
405
|
+
* suppress self-echo and agent-to-agent reply storms in every pairing mode.
|
|
406
|
+
* @returns {boolean}
|
|
407
|
+
*/
|
|
408
|
+
_isOwnOrPeer(ev) {
|
|
409
|
+
const id = this._eventSenderId(ev);
|
|
410
|
+
if (!id) return false;
|
|
411
|
+
if (this.ownId && id === String(this.ownId)) return true;
|
|
412
|
+
return this.peerAgentIds instanceof Set && this.peerAgentIds.has(id);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* Sliding-window reply-storm guard. Records that we're about to send to a
|
|
417
|
+
* conversation and returns true if the per-conversation reply rate is over
|
|
418
|
+
* the ceiling inside the window — meaning we should suppress this send to
|
|
419
|
+
* break a ping-pong. Pruning is lazy (drop timestamps older than the window).
|
|
420
|
+
* Fail-open: any internal slip allows the send.
|
|
421
|
+
*
|
|
422
|
+
* @param {string} convKey conversation key (channel:chatId:threadRef)
|
|
423
|
+
* @returns {boolean} true ⇒ suppress (storm); false ⇒ allow
|
|
424
|
+
*/
|
|
425
|
+
_pairGuardTrips(convKey) {
|
|
426
|
+
try {
|
|
427
|
+
const now = Date.now();
|
|
428
|
+
const cutoff = now - PAIR_GUARD_WINDOW_MS;
|
|
429
|
+
const arr = (this._replyWindow.get(convKey) || []).filter((t) => t >= cutoff);
|
|
430
|
+
if (arr.length >= PAIR_GUARD_MAX) {
|
|
431
|
+
this._replyWindow.set(convKey, arr); // keep the pruned window
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
arr.push(now);
|
|
435
|
+
this._replyWindow.set(convKey, arr);
|
|
436
|
+
return false;
|
|
437
|
+
} catch {
|
|
438
|
+
return false; // fail-open: never let the guard itself block a send
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// -- send / react ---------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Send an outbound message through the ONE shared outbound perimeter, then the
|
|
446
|
+
* adapter's _send.
|
|
447
|
+
*
|
|
448
|
+
* Order (gaps-product-quality P0-3/P0-5):
|
|
449
|
+
* 1. send-gate — the hook-bus `beforeSend` cancellable chain AND the shared
|
|
450
|
+
* lib/comms/send-gate.screenOutbound. EITHER vetoing blocks the send.
|
|
451
|
+
* Now EVERY adapter send passes the gate, not just Gmail.
|
|
452
|
+
* 2. pair guard — a per-conversation sliding window so two agents in one
|
|
453
|
+
* channel can't reply-storm.
|
|
454
|
+
* 3. _send — the adapter transport.
|
|
455
|
+
*
|
|
456
|
+
* A blocked send returns a uniform SendResult `{ ok:false, blocked:true,
|
|
457
|
+
* reason }` (NOT thrown) so callers can branch on `res.blocked`.
|
|
458
|
+
*
|
|
459
|
+
* @param {object} out
|
|
460
|
+
* @returns {Promise<import("./contract.mjs").SendResult>}
|
|
461
|
+
*/
|
|
462
|
+
async send(out) {
|
|
463
|
+
// 1. Outbound governance — the shared send-gate (hook-bus + screenOutbound).
|
|
464
|
+
if (this._sendGateEnabled) {
|
|
465
|
+
const gate = await this._screenSend(out);
|
|
466
|
+
if (!gate.allow) {
|
|
467
|
+
this._count(CHANNEL_COUNTERS.SEND_GATE_BLOCKED, { reason: gate.reason });
|
|
468
|
+
this.log("warn", `send blocked by gate: ${gate.reason}`);
|
|
469
|
+
return { ok: false, blocked: true, reason: gate.reason };
|
|
470
|
+
}
|
|
471
|
+
// The gate may rewrite the body (redaction); honour it.
|
|
472
|
+
if (typeof gate.text === "string" && gate.text !== out.text) {
|
|
473
|
+
out = { ...out, text: gate.text };
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// 2. Bot-loop / reply-storm guard (per conversation).
|
|
478
|
+
const convKey = this._sendConvKey(out);
|
|
479
|
+
if (convKey && this._pairGuardTrips(convKey)) {
|
|
480
|
+
this._count(CHANNEL_COUNTERS.PAIR_GUARD_TRIPPED, { conversation: convKey });
|
|
481
|
+
this.log("warn", `send suppressed: reply-storm guard tripped for ${convKey}`);
|
|
482
|
+
return { ok: false, blocked: true, reason: "reply-storm-guard" };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
// 3. Adapter transport.
|
|
486
|
+
try {
|
|
487
|
+
const res = await this._send(out);
|
|
488
|
+
// Normalise into a SendResult.
|
|
489
|
+
if (res && typeof res === "object" && "ok" in res) return res;
|
|
490
|
+
return { ok: true, message_id: res?.message_id, raw: res };
|
|
491
|
+
} catch (err) {
|
|
492
|
+
return { ok: false, error: err.message };
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Send a fixed, framework-authored control message (pairing acks, prompts)
|
|
498
|
+
* that bypasses the outbound policy gate and the reply-storm guard. These are
|
|
499
|
+
* NOT model output and NOT user-facing content under disclosure/barrier
|
|
500
|
+
* policy, so gating them would only let the fail-closed external default
|
|
501
|
+
* swallow the pairing handshake. Errors are still shaped into a SendResult.
|
|
502
|
+
* @param {object} out
|
|
503
|
+
* @returns {Promise<import("./contract.mjs").SendResult>}
|
|
504
|
+
*/
|
|
505
|
+
async _systemSend(out) {
|
|
506
|
+
try {
|
|
507
|
+
const res = await this._send(out);
|
|
508
|
+
if (res && typeof res === "object" && "ok" in res) return res;
|
|
509
|
+
return { ok: true, message_id: res?.message_id, raw: res };
|
|
510
|
+
} catch (err) {
|
|
511
|
+
return { ok: false, error: err.message };
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
/** Conversation key for the pair guard, derived from the outbound shape. */
|
|
516
|
+
_sendConvKey(out) {
|
|
517
|
+
const channel = out?.source?.channel || this.name;
|
|
518
|
+
const chatId = out?.to ?? out?.source?.chatId ?? "";
|
|
519
|
+
const threadRef = out?.thread_ref ?? out?.source?.threadRef ?? "";
|
|
520
|
+
if (chatId === "" || chatId == null) return "";
|
|
521
|
+
return `${channel}:${chatId}:${threadRef}`;
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* Run the shared outbound send-gate: the hook-bus `beforeSend` cancellable
|
|
526
|
+
* chain (so any subscriber — disclosure, custom policy — can veto) AND
|
|
527
|
+
* lib/comms/send-gate.screenOutbound (banned phrases, AI-disclosure,
|
|
528
|
+
* information barriers, allowlist). EITHER veto blocks.
|
|
529
|
+
*
|
|
530
|
+
* Fail posture: screenOutbound itself fails CLOSED for external recipients and
|
|
531
|
+
* OPEN for internal (it owns that policy). If our wiring around it throws
|
|
532
|
+
* unexpectedly we count a fail-open and allow — a buggy gate must never
|
|
533
|
+
* silently wedge all outbound traffic (house idiom), but the fail-open is
|
|
534
|
+
* LOUD (counted) so doctor/a human sees it.
|
|
535
|
+
*
|
|
536
|
+
* @returns {Promise<{allow:boolean, reason:string, text?:string}>}
|
|
537
|
+
*/
|
|
538
|
+
async _screenSend(out) {
|
|
539
|
+
const channel = out?.source?.channel || this.name;
|
|
540
|
+
const recipient = String(out?.to ?? out?.source?.chatId ?? "");
|
|
541
|
+
const text = typeof out?.text === "string" ? out.text : "";
|
|
542
|
+
|
|
543
|
+
// (a) hook-bus beforeSend chain — only `beforeSend` is cancellable.
|
|
544
|
+
try {
|
|
545
|
+
const bus = this._bus();
|
|
546
|
+
if (bus) {
|
|
547
|
+
const res = await bus.emit("beforeSend", {
|
|
548
|
+
tool: `${this.name}_send`,
|
|
549
|
+
channel,
|
|
550
|
+
recipient,
|
|
551
|
+
text,
|
|
552
|
+
source: out?.source || null,
|
|
553
|
+
});
|
|
554
|
+
if (res && res.cancelled) {
|
|
555
|
+
return { allow: false, reason: res.reason || "beforeSend-cancelled" };
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
} catch (err) {
|
|
559
|
+
// A bus fault is fail-open (counted) — the screenOutbound layer below is
|
|
560
|
+
// the authoritative policy gate and runs regardless.
|
|
561
|
+
this._count(CHANNEL_COUNTERS.SEND_GATE_FAILOPEN, { stage: "hookbus", error: err && err.message });
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// (b) the shared policy gate (the authoritative chokepoint).
|
|
565
|
+
try {
|
|
566
|
+
const verdict = await this._screenOutbound({
|
|
567
|
+
channel,
|
|
568
|
+
recipient,
|
|
569
|
+
text,
|
|
570
|
+
agentRoot: this.agentRoot,
|
|
571
|
+
jurisdiction: this._jurisdiction,
|
|
572
|
+
firstContact: out?.firstContact !== false,
|
|
573
|
+
internalDomains: this._internalDomains,
|
|
574
|
+
allowlist: Array.isArray(out?.allowlist) ? out.allowlist : undefined,
|
|
575
|
+
});
|
|
576
|
+
if (verdict && verdict.allow === false) {
|
|
577
|
+
return { allow: false, reason: verdict.reason || "blocked" };
|
|
578
|
+
}
|
|
579
|
+
return { allow: true, reason: "", text: verdict?.redactedText };
|
|
580
|
+
} catch (err) {
|
|
581
|
+
// screenOutbound is itself never supposed to throw (it has its own
|
|
582
|
+
// fail-closed/open policy). If our call site does, count the fail-open and
|
|
583
|
+
// allow rather than wedge every send — but loudly.
|
|
584
|
+
this._count(CHANNEL_COUNTERS.SEND_GATE_FAILOPEN, { stage: "screen", error: err && err.message });
|
|
585
|
+
this.log("error", `send-gate threw (fail-open): ${err && err.message}`);
|
|
586
|
+
return { allow: true, reason: "" };
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
async react(out) {
|
|
591
|
+
try {
|
|
592
|
+
return await this._react(out);
|
|
593
|
+
} catch (err) {
|
|
594
|
+
return { ok: false, error: err.message };
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
async setTyping(source, on) {
|
|
599
|
+
try {
|
|
600
|
+
await this._setTyping(source, on);
|
|
601
|
+
return { ok: true };
|
|
602
|
+
} catch (err) {
|
|
603
|
+
return { ok: false, error: err.message };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// -- typing heartbeat -----------------------------------------------------
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* Start a typing heartbeat for a conversation. Calls `_setTyping(source,
|
|
611
|
+
* true)` immediately and then every ~4s until stopped. Each call is
|
|
612
|
+
* timeout-guarded so a hung provider can't wedge the interval. Keyed by the
|
|
613
|
+
* source's chat/thread so concurrent sessions don't collide.
|
|
614
|
+
*
|
|
615
|
+
* @param {import("./contract.mjs").SessionSource} source
|
|
616
|
+
* @returns {string} the heartbeat key (pass to stopTypingHeartbeat)
|
|
617
|
+
*/
|
|
618
|
+
startTypingHeartbeat(source) {
|
|
619
|
+
if (!source) return "";
|
|
620
|
+
const key = this._typingKey(source);
|
|
621
|
+
if (this._typingTimers.has(key)) return key; // already beating
|
|
622
|
+
|
|
623
|
+
const beat = () => {
|
|
624
|
+
this._guardedTyping(source, true);
|
|
625
|
+
};
|
|
626
|
+
// Fire once immediately so the indicator shows without waiting a full tick.
|
|
627
|
+
beat();
|
|
628
|
+
const handle = this._setInterval(beat, this._typingIntervalMs);
|
|
629
|
+
if (handle && typeof handle.unref === "function") handle.unref();
|
|
630
|
+
this._typingTimers.set(key, handle);
|
|
631
|
+
return key;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
/**
|
|
635
|
+
* Stop the typing heartbeat for a conversation and clear the indicator.
|
|
636
|
+
* @param {import("./contract.mjs").SessionSource|string} sourceOrKey
|
|
637
|
+
*/
|
|
638
|
+
stopTypingHeartbeat(sourceOrKey) {
|
|
639
|
+
const key = typeof sourceOrKey === "string" ? sourceOrKey : this._typingKey(sourceOrKey);
|
|
640
|
+
const handle = this._typingTimers.get(key);
|
|
641
|
+
if (handle != null) {
|
|
642
|
+
this._clearInterval(handle);
|
|
643
|
+
this._typingTimers.delete(key);
|
|
644
|
+
}
|
|
645
|
+
// Best-effort clear of the indicator (channels where typing auto-expires
|
|
646
|
+
// can no-op _setTyping(false)).
|
|
647
|
+
if (typeof sourceOrKey === "object" && sourceOrKey) this._guardedTyping(sourceOrKey, false);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/** Stop every active typing heartbeat (called on stop()). */
|
|
651
|
+
stopAllTyping() {
|
|
652
|
+
for (const handle of this._typingTimers.values()) this._clearInterval(handle);
|
|
653
|
+
this._typingTimers.clear();
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
_typingKey(source) {
|
|
657
|
+
return `${source.channel || this.name}:${source.chatId || ""}:${source.threadRef || ""}`;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
_guardedTyping(source, on) {
|
|
661
|
+
// Run _setTyping but never let it run longer than the guard window or
|
|
662
|
+
// reject into the interval; swallow + log.
|
|
663
|
+
let settled = false;
|
|
664
|
+
const timer = this._setTimeout(() => {
|
|
665
|
+
if (settled) return;
|
|
666
|
+
settled = true;
|
|
667
|
+
this.log("debug", "typing call timed out");
|
|
668
|
+
}, DEFAULT_TYPING_CALL_TIMEOUT_MS);
|
|
669
|
+
if (timer && typeof timer.unref === "function") timer.unref();
|
|
670
|
+
Promise.resolve()
|
|
671
|
+
.then(() => this._setTyping(source, on))
|
|
672
|
+
.catch((err) => this.log("debug", `_setTyping failed: ${err.message}`))
|
|
673
|
+
.finally(() => {
|
|
674
|
+
settled = true;
|
|
675
|
+
this._clearTimeout(timer);
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// -- lifecycle ------------------------------------------------------------
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* Liveness for the daemon health sweep. Reflects the reconnect supervisor's
|
|
683
|
+
* state so a dead channel reports RED instead of the old hardcoded green
|
|
684
|
+
* (P0-6: "nothing anywhere calls healthCheck; dead channels report healthy").
|
|
685
|
+
* A channel that has given up reconnecting is unhealthy; one that is down but
|
|
686
|
+
* still retrying is degraded-but-not-yet-dead (reported ok:false too so the
|
|
687
|
+
* sweep notices). Unsupervised adapters keep the prior always-ok behaviour.
|
|
688
|
+
*/
|
|
689
|
+
async healthCheck() {
|
|
690
|
+
if (!this._superviseEnabled || !this._supervisor) {
|
|
691
|
+
return { ok: true, detail: `${this.name} adapter` };
|
|
692
|
+
}
|
|
693
|
+
const state = this._connState;
|
|
694
|
+
const ok = state === CONN_STATE.UP;
|
|
695
|
+
return {
|
|
696
|
+
ok,
|
|
697
|
+
state,
|
|
698
|
+
detail: ok
|
|
699
|
+
? `${this.name} adapter (${state})`
|
|
700
|
+
: `${this.name} adapter ${state === CONN_STATE.GIVING_UP ? "DEAD" : "degraded"} (${state})`,
|
|
701
|
+
};
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
async stop() {
|
|
705
|
+
this._stopping = true;
|
|
706
|
+
try { this._supervisor?.stop?.("adapter-stop"); } catch { /* */ }
|
|
707
|
+
this.stopAllTyping();
|
|
708
|
+
this._replyWindow.clear();
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
/** Coerce peerAgentIds (Set | array | undefined) to a Set<string>. */
|
|
713
|
+
function toPeerSet(v) {
|
|
714
|
+
if (v instanceof Set) return new Set([...v].map(String));
|
|
715
|
+
if (Array.isArray(v)) return new Set(v.map(String));
|
|
716
|
+
return new Set();
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
export default BaseAdapter;
|