@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,448 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/org/approvals.mjs — agent-mediated approval delivery + the action-class gate.
|
|
3
|
+
*
|
|
4
|
+
* The agent half of the phase-3 approval flow (SPEC-org-server §4.6 / §5.4). The
|
|
5
|
+
* Cohort server holds NO Slack/WhatsApp credentials; an `approval.request`
|
|
6
|
+
* merely records the pending row and emits an `approval.pending` event. THIS
|
|
7
|
+
* module is the side that (a) decides whether an action even needs approval (the
|
|
8
|
+
* action-class gate), (b) sha256-binds the exact payload the human will approve,
|
|
9
|
+
* (c) posts `approval.request`, and (d) — agent-mediated — delivers the
|
|
10
|
+
* approve/deny ask to a human over the configured channel (default Slack). The
|
|
11
|
+
* human's reply routes back through the channel adapter → `approval.resolve` (the
|
|
12
|
+
* approver side; not this module), and the requester wakes via the
|
|
13
|
+
* `approval.wait` long-poll for a <60s end-to-end round-trip.
|
|
14
|
+
*
|
|
15
|
+
* G16 (zero-LLM-in-the-coordination-loop): NOTHING here makes an LLM call. The
|
|
16
|
+
* classification is a pure table lookup over policies/action-classification.yaml;
|
|
17
|
+
* delivery is a deterministic channel send; the wait is a bounded HTTP long-poll.
|
|
18
|
+
* If this module ever needs a model to decide whether to gate, the invariant is
|
|
19
|
+
* broken — keep it deterministic.
|
|
20
|
+
*
|
|
21
|
+
* G5 (requester != approver): enforced at the storage + method layer server-side
|
|
22
|
+
* (the approvals-table CHECK constraint and approval.resolve actor check). This
|
|
23
|
+
* module never resolves an approval it requested; it only requests + waits.
|
|
24
|
+
*
|
|
25
|
+
* Pure gate/hash/classify logic (unit-tested) + a thin, injectable, fail-open
|
|
26
|
+
* channel transport (default: scripts/slack-send.sh) and the long-poll. Node
|
|
27
|
+
* builtins only beyond the optional js-yaml policy parse (loaded via createRequire,
|
|
28
|
+
* same as lib/voice/tool-bridge.mjs — absent it the gate fails CLOSED).
|
|
29
|
+
*
|
|
30
|
+
* @module lib/org/approvals
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
"use strict";
|
|
34
|
+
|
|
35
|
+
import { spawnSync } from "node:child_process";
|
|
36
|
+
import { createHash } from "node:crypto";
|
|
37
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
38
|
+
import { join, resolve } from "node:path";
|
|
39
|
+
import { createRequire } from "node:module";
|
|
40
|
+
|
|
41
|
+
import { isEnabled, configFromAgent, approvalRequest, approvalWait } from "./client.mjs";
|
|
42
|
+
|
|
43
|
+
const require = createRequire(import.meta.url);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* The gated action classes (SPEC §4.6 + the SHARED CONTRACT). An action that maps
|
|
47
|
+
* to ANY of these MUST be approved before it executes. Keyed off the action
|
|
48
|
+
* classification — `irreversible` / `external` / `financial`.
|
|
49
|
+
* @type {readonly string[]}
|
|
50
|
+
*/
|
|
51
|
+
export const GATED_CLASSES = Object.freeze(["irreversible", "external", "financial"]);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Map an action-classification level (the `risk`/`level`/`approval` triple from
|
|
55
|
+
* policies/action-classification.yaml) onto the gated action classes. Pure.
|
|
56
|
+
*
|
|
57
|
+
* The default/template policy speaks in `action_levels` (observe / draft /
|
|
58
|
+
* execute_autonomous / escalate_to_principal) and per-action `risk` + `approval`.
|
|
59
|
+
* The mapping below is conservative — anything that escalates to the principal,
|
|
60
|
+
* is approval-gated, or is explicitly tagged high/critical risk is treated as a
|
|
61
|
+
* gated class. We also honour an explicit `classes:[…]` / `action_class` field
|
|
62
|
+
* when the archetype factory emits one (the per-action map it generates).
|
|
63
|
+
*
|
|
64
|
+
* @param {object} def normalised classification entry for the action
|
|
65
|
+
* @returns {string[]} the subset of GATED_CLASSES this action falls under
|
|
66
|
+
*/
|
|
67
|
+
export function classesForAction(def) {
|
|
68
|
+
if (!def || typeof def !== "object") return [];
|
|
69
|
+
const out = new Set();
|
|
70
|
+
// 1. Explicit class tags win (archetype-generated `actions` map may carry these).
|
|
71
|
+
const explicit = []
|
|
72
|
+
.concat(def.classes || [])
|
|
73
|
+
.concat(def.action_class ? [def.action_class] : [])
|
|
74
|
+
.concat(def.action_classes || []);
|
|
75
|
+
for (const c of explicit) {
|
|
76
|
+
const k = String(c || "").toLowerCase();
|
|
77
|
+
if (GATED_CLASSES.includes(k)) out.add(k);
|
|
78
|
+
}
|
|
79
|
+
if (out.size) return [...out];
|
|
80
|
+
|
|
81
|
+
// 2. Derive from risk + approval + level when no explicit tag is present.
|
|
82
|
+
const risk = String(def.risk || def.level || "").toLowerCase();
|
|
83
|
+
const approval = String(def.approval || def.gate || "").toLowerCase();
|
|
84
|
+
const level = String(def.action_level || def.level || "").toLowerCase();
|
|
85
|
+
const highRisk = risk.includes("high") || risk.includes("critical");
|
|
86
|
+
const gated =
|
|
87
|
+
highRisk ||
|
|
88
|
+
approval.includes("escalat") ||
|
|
89
|
+
approval.includes("principal") ||
|
|
90
|
+
approval.includes("approval") ||
|
|
91
|
+
level.includes("escalate");
|
|
92
|
+
if (!gated) return [];
|
|
93
|
+
|
|
94
|
+
// Heuristic class assignment from the action's notes/keywords (deterministic).
|
|
95
|
+
const hay = `${def.notes || ""} ${def.description || ""} ${def.name || ""}`.toLowerCase();
|
|
96
|
+
if (/spend|payment|invoice|wire|financ|budget|purchase|pricing|deal terms/.test(hay)) out.add("financial");
|
|
97
|
+
if (/external|public|press|partner|customer|regulat|submission|filing/.test(hay)) out.add("external");
|
|
98
|
+
if (/delete|irreversible|destroy|prod|merge|binding|commit/.test(hay)) out.add("irreversible");
|
|
99
|
+
// A gated action with no keyword signal still needs approval — default to the
|
|
100
|
+
// most generic gated class so the gate never silently passes it.
|
|
101
|
+
if (!out.size) out.add("irreversible");
|
|
102
|
+
return [...out];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Load + normalise the action-classification policy (YAML or JSON), mirroring
|
|
107
|
+
* lib/voice/tool-bridge.loadActionClassification: a map of
|
|
108
|
+
* `{ actionName: { risk, approval, level, classes?, notes } }`. Absent js-yaml
|
|
109
|
+
* (and no JSON), the gate fails CLOSED (returns {} → unknown actions gate by the
|
|
110
|
+
* caller's `defaultGate`). Never throws.
|
|
111
|
+
*
|
|
112
|
+
* @param {string} agentRoot
|
|
113
|
+
* @returns {Record<string, object>}
|
|
114
|
+
*/
|
|
115
|
+
export function loadActionClassification(agentRoot) {
|
|
116
|
+
const root = resolve(agentRoot || process.env.AGENT_ROOT || process.env.AGENT_DIR || process.cwd());
|
|
117
|
+
const candidates = [
|
|
118
|
+
join(root, "policies/action-classification.json"),
|
|
119
|
+
join(root, "policies/action-classification.yaml"),
|
|
120
|
+
];
|
|
121
|
+
for (const path of candidates) {
|
|
122
|
+
if (!existsSync(path)) continue;
|
|
123
|
+
let raw;
|
|
124
|
+
try { raw = readFileSync(path, "utf-8"); } catch { continue; }
|
|
125
|
+
try {
|
|
126
|
+
const parsed = path.endsWith(".json") ? JSON.parse(raw) : require("js-yaml").load(raw);
|
|
127
|
+
return normaliseClassification(parsed);
|
|
128
|
+
} catch {
|
|
129
|
+
// Misconfigured/absent parser → empty map; the gate fails closed.
|
|
130
|
+
return {};
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return {};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normaliseClassification(raw) {
|
|
137
|
+
if (!raw || typeof raw !== "object") return {};
|
|
138
|
+
const out = {};
|
|
139
|
+
const actions = raw.actions || raw.action_levels || raw;
|
|
140
|
+
for (const [name, def] of Object.entries(actions)) {
|
|
141
|
+
if (!def || typeof def !== "object") continue;
|
|
142
|
+
out[name] = {
|
|
143
|
+
name,
|
|
144
|
+
risk: def.risk || def.level || "low",
|
|
145
|
+
approval: def.approval || def.gate || "none",
|
|
146
|
+
action_level: def.action_level || def.level || "",
|
|
147
|
+
classes: def.classes || def.action_classes || (def.action_class ? [def.action_class] : []),
|
|
148
|
+
notes: def.notes || def.description || "",
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Classify an action by name against the loaded policy → its gated classes. When
|
|
156
|
+
* the caller already knows the classification (e.g. a tool that self-declares
|
|
157
|
+
* `irreversible`), pass `classification` directly and skip the lookup. Pure.
|
|
158
|
+
*
|
|
159
|
+
* @param {object} o - { action, classification?, policy?, agentRoot? }
|
|
160
|
+
* @returns {{ gated:boolean, classes:string[], source:string }}
|
|
161
|
+
*/
|
|
162
|
+
export function classifyAction(o = {}) {
|
|
163
|
+
// Caller-supplied classification short-circuits the policy lookup.
|
|
164
|
+
if (o.classification) {
|
|
165
|
+
const list = []
|
|
166
|
+
.concat(o.classification)
|
|
167
|
+
.map((c) => String(c || "").toLowerCase())
|
|
168
|
+
.filter((c) => GATED_CLASSES.includes(c));
|
|
169
|
+
return { gated: list.length > 0, classes: list, source: "explicit" };
|
|
170
|
+
}
|
|
171
|
+
const policy = o.policy || loadActionClassification(o.agentRoot);
|
|
172
|
+
const def = policy[o.action];
|
|
173
|
+
if (!def) {
|
|
174
|
+
// Unknown action: fail closed — treat as gated unless caller opts out.
|
|
175
|
+
return { gated: true, classes: ["irreversible"], source: "unknown-fail-closed" };
|
|
176
|
+
}
|
|
177
|
+
const classes = classesForAction(def);
|
|
178
|
+
return { gated: classes.length > 0, classes, source: "policy" };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* sha256-bind a payload: the canonical "the human approved exactly this" hash.
|
|
183
|
+
* Stable across re-serialization (keys sorted) so the executor can recompute it
|
|
184
|
+
* and the server can reject a resolve whose executing payload hash differs. Pure.
|
|
185
|
+
*
|
|
186
|
+
* @param {unknown} payload
|
|
187
|
+
* @returns {string} lowercase hex sha-256 over the canonical JSON
|
|
188
|
+
*/
|
|
189
|
+
export function hashPayload(payload) {
|
|
190
|
+
return createHash("sha256").update(canonicalJson(payload)).digest("hex");
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Deterministic JSON: object keys sorted recursively. */
|
|
194
|
+
function canonicalJson(value) {
|
|
195
|
+
return JSON.stringify(sortDeep(value));
|
|
196
|
+
}
|
|
197
|
+
function sortDeep(v) {
|
|
198
|
+
if (Array.isArray(v)) return v.map(sortDeep);
|
|
199
|
+
if (v && typeof v === "object") {
|
|
200
|
+
const out = {};
|
|
201
|
+
for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
return v;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
// Agent-mediated delivery (default transport: scripts/slack-send.sh)
|
|
209
|
+
// ---------------------------------------------------------------------------
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Build the human-facing approval ask (a plain-text message the delivery agent
|
|
213
|
+
* posts to the approver's channel). Deterministic; no LLM. The approver replies
|
|
214
|
+
* over the same channel → the channel adapter routes the reply to
|
|
215
|
+
* `approval.resolve`. Pure.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} o - { id, action, classes, subject?, requester? }
|
|
218
|
+
* @returns {string}
|
|
219
|
+
*/
|
|
220
|
+
export function buildApprovalAsk(o = {}) {
|
|
221
|
+
const classes = Array.isArray(o.classes) && o.classes.length ? o.classes.join(", ") : "gated";
|
|
222
|
+
const who = o.requester ? ` (requested by ${o.requester})` : "";
|
|
223
|
+
const subject = o.subject ? `\n${String(o.subject).slice(0, 800)}` : "";
|
|
224
|
+
return (
|
|
225
|
+
`Approval needed${who}: ${o.action || "an action"} [${classes}]` +
|
|
226
|
+
subject +
|
|
227
|
+
`\nReply "approve ${o.id}" or "deny ${o.id}".`
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Default delivery transport: scripts/slack-send.sh to the approver. Fail-open. */
|
|
232
|
+
export function defaultDeliver({ agentRoot, approver, text }) {
|
|
233
|
+
try {
|
|
234
|
+
const dest = approver && (approver.slackId || approver.channel || approver.id);
|
|
235
|
+
if (!dest) return { ok: false, channel: null, reason: "no-approver-coordinates" };
|
|
236
|
+
const res = spawnSync(join(agentRoot, "scripts", "slack-send.sh"), [String(dest), String(text)], {
|
|
237
|
+
cwd: agentRoot,
|
|
238
|
+
env: { ...process.env, AGENT_DIR: agentRoot },
|
|
239
|
+
encoding: "utf8",
|
|
240
|
+
timeout: 30_000,
|
|
241
|
+
});
|
|
242
|
+
return { ok: res.status === 0, channel: `slack:${dest}`, reason: res.status === 0 ? "delivered" : `exit-${res.status}` };
|
|
243
|
+
} catch (e) {
|
|
244
|
+
return { ok: false, channel: null, reason: e.message };
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// ---------------------------------------------------------------------------
|
|
249
|
+
// requestApproval — classify, bind, request, deliver
|
|
250
|
+
// ---------------------------------------------------------------------------
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Request approval for a classified action and deliver the ask to a human.
|
|
254
|
+
*
|
|
255
|
+
* Steps (all deterministic; G16):
|
|
256
|
+
* 1. Classify the action (irreversible/external/financial) via the policy (or
|
|
257
|
+
* the caller-supplied `classification`). If NOT gated, returns immediately
|
|
258
|
+
* with `{ gated:false }` — no server call, no delivery.
|
|
259
|
+
* 2. sha256-bind the EXACT payload (`payload_hash`) so the human approves this
|
|
260
|
+
* and only this; the executor re-checks the hash before acting.
|
|
261
|
+
* 3. POST approval.request (idempotent on the derived id) — the server records
|
|
262
|
+
* the pending row + emits approval.pending.
|
|
263
|
+
* 4. Agent-mediated delivery: post the approve/deny ask to the approver's
|
|
264
|
+
* channel (default Slack). Delivery failure is recorded, not fatal — the
|
|
265
|
+
* pending row + /ops dashboard remain the fallback surface.
|
|
266
|
+
*
|
|
267
|
+
* Always resolves (fail-open).
|
|
268
|
+
*
|
|
269
|
+
* @param {object} o - {
|
|
270
|
+
* cfg, agentRoot, action, payload, classification?, policy?,
|
|
271
|
+
* subject?, kind?, itemId?, requester?, approver?, channel?,
|
|
272
|
+
* expiresInMs?, id?, deliverFn?, fetchImpl?, nowMs?
|
|
273
|
+
* }
|
|
274
|
+
* @returns {Promise<{
|
|
275
|
+
* gated:boolean, classes:string[], id:string|null, payloadHash:string,
|
|
276
|
+
* requested:boolean, frame:object|null, delivery:object|null
|
|
277
|
+
* }>}
|
|
278
|
+
*/
|
|
279
|
+
export async function requestApproval(o = {}) {
|
|
280
|
+
const { gated, classes } = classifyAction({
|
|
281
|
+
action: o.action,
|
|
282
|
+
classification: o.classification,
|
|
283
|
+
policy: o.policy,
|
|
284
|
+
agentRoot: o.agentRoot,
|
|
285
|
+
});
|
|
286
|
+
const payloadHash = hashPayload(o.payload ?? null);
|
|
287
|
+
|
|
288
|
+
if (!gated) {
|
|
289
|
+
return { gated: false, classes: [], id: null, payloadHash, requested: false, frame: null, delivery: null };
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// Stable approval id: caller-supplied else derived from action+payload so a
|
|
293
|
+
// retried request (same action, same payload) reuses the same approval row.
|
|
294
|
+
const id = o.id || `ap-${payloadHash.slice(0, 16)}`;
|
|
295
|
+
const kind = o.kind || classToKind(classes);
|
|
296
|
+
const subject = o.subject || `${o.action || "action"} (${classes.join(", ")})`;
|
|
297
|
+
|
|
298
|
+
if (!isEnabled(o.cfg)) {
|
|
299
|
+
// Org binding disabled: surface the gate decision + hash but make no call.
|
|
300
|
+
return { gated: true, classes, id, payloadHash, requested: false, frame: null, delivery: null };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const c = configFromAgent(o.cfg);
|
|
304
|
+
const params = {
|
|
305
|
+
id,
|
|
306
|
+
kind,
|
|
307
|
+
subject,
|
|
308
|
+
payload_hash: payloadHash,
|
|
309
|
+
requester: o.requester || c.orgId || undefined,
|
|
310
|
+
};
|
|
311
|
+
if (o.itemId) params.itemId = String(o.itemId);
|
|
312
|
+
if (Number.isFinite(o.expiresInMs)) params.expiresInMs = o.expiresInMs;
|
|
313
|
+
|
|
314
|
+
const frame = await approvalRequest(params, {
|
|
315
|
+
base: c.base,
|
|
316
|
+
token: c.token,
|
|
317
|
+
idempotencyKey: `approval.request:${id}`,
|
|
318
|
+
fetchImpl: o.fetchImpl,
|
|
319
|
+
});
|
|
320
|
+
const requested = !!(frame && frame.ok);
|
|
321
|
+
|
|
322
|
+
// Agent-mediated delivery (only when the request landed).
|
|
323
|
+
let delivery = null;
|
|
324
|
+
if (requested && o.approver) {
|
|
325
|
+
const text = buildApprovalAsk({ id, action: o.action, classes, subject: o.subject, requester: o.requester });
|
|
326
|
+
const deliver = o.deliverFn || defaultDeliver;
|
|
327
|
+
try {
|
|
328
|
+
delivery = await deliver({ agentRoot: o.agentRoot, approver: o.approver, channel: o.channel, text, id });
|
|
329
|
+
} catch (e) {
|
|
330
|
+
delivery = { ok: false, reason: e.message };
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return { gated: true, classes, id, payloadHash, requested, frame, delivery };
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/** Pick the approvals.kind enum value (§4.6) from the derived classes. */
|
|
338
|
+
function classToKind(classes) {
|
|
339
|
+
if (classes.includes("financial")) return "spend";
|
|
340
|
+
if (classes.includes("external")) return "external_comms";
|
|
341
|
+
if (classes.includes("irreversible")) return "exec";
|
|
342
|
+
return "exec";
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ---------------------------------------------------------------------------
|
|
346
|
+
// awaitDecision — bounded long-poll for the outcome
|
|
347
|
+
// ---------------------------------------------------------------------------
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* Wait for an approval to be decided via the bounded long-poll (approval.wait),
|
|
351
|
+
* with optional re-polling until an overall deadline so the caller can wait
|
|
352
|
+
* longer than one 55s window without holding the connection open. Fail-open.
|
|
353
|
+
*
|
|
354
|
+
* @param {string} id approval id
|
|
355
|
+
* @param {number} timeoutMs overall deadline (each poll bounded to <=55s)
|
|
356
|
+
* @param {object} o - { cfg, fetchImpl?, nowMs?, pollMs? }
|
|
357
|
+
* @returns {Promise<{ decided:boolean, status:string, payload:object|null }>}
|
|
358
|
+
*/
|
|
359
|
+
export async function awaitDecision(id, timeoutMs = 55000, o = {}) {
|
|
360
|
+
if (!id) return { decided: false, status: "unknown", payload: null };
|
|
361
|
+
if (!isEnabled(o.cfg)) return { decided: false, status: "unknown", payload: null };
|
|
362
|
+
const c = configFromAgent(o.cfg);
|
|
363
|
+
const now = typeof o.nowMs === "function" ? o.nowMs : Date.now;
|
|
364
|
+
const deadline = now() + (Number.isFinite(timeoutMs) && timeoutMs > 0 ? timeoutMs : 55000);
|
|
365
|
+
|
|
366
|
+
let last = null;
|
|
367
|
+
// Each iteration is one bounded long-poll; loop until decided or the deadline.
|
|
368
|
+
// The first poll is given the full remaining budget (clamped to 55s by the
|
|
369
|
+
// client), so a single call covers the common <60s case in one round-trip.
|
|
370
|
+
do {
|
|
371
|
+
const remaining = deadline - now();
|
|
372
|
+
const pollMs = Math.max(1, Math.min(55000, remaining));
|
|
373
|
+
const r = await approvalWait({ id, timeoutMs: pollMs }, { base: c.base, token: c.token, fetchImpl: o.fetchImpl });
|
|
374
|
+
last = r && r.ok ? r.payload : last;
|
|
375
|
+
const status = decisionStatus(last);
|
|
376
|
+
if (isDecided(status)) return { decided: true, status, payload: last };
|
|
377
|
+
} while (now() < deadline);
|
|
378
|
+
|
|
379
|
+
return { decided: false, status: decisionStatus(last) || "pending", payload: last };
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
function decisionStatus(payload) {
|
|
383
|
+
if (!payload || typeof payload !== "object") return "";
|
|
384
|
+
return String(payload.status || payload.state || "").toLowerCase();
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function isDecided(status) {
|
|
388
|
+
return status === "approved" || status === "denied" || status === "expired";
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ---------------------------------------------------------------------------
|
|
392
|
+
// gateActionClass — the blocking helper the action-executor calls
|
|
393
|
+
// ---------------------------------------------------------------------------
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Block an action-class until it is approved. The helper the action-executor
|
|
397
|
+
* wraps a gated tool with: classify → (if gated) request + deliver + wait →
|
|
398
|
+
* return `{ approved, reason }`. Targets <60s end-to-end via the long-poll.
|
|
399
|
+
*
|
|
400
|
+
* Non-gated actions return `{ approved:true, reason:"not-gated" }` immediately
|
|
401
|
+
* (no server round-trip). A disabled org binding returns
|
|
402
|
+
* `{ approved:false, reason:"org-disabled", ... }` so the executor can decide its
|
|
403
|
+
* own fallback (the gate never silently passes a gated action when it cannot
|
|
404
|
+
* obtain an approval). The returned `payloadHash` lets the executor verify it
|
|
405
|
+
* runs exactly the approved payload.
|
|
406
|
+
*
|
|
407
|
+
* @param {object} o - all of requestApproval's params + { timeoutMs?, pollMs? }
|
|
408
|
+
* @returns {Promise<{
|
|
409
|
+
* approved:boolean, reason:string, id:string|null, classes:string[],
|
|
410
|
+
* payloadHash:string, status?:string
|
|
411
|
+
* }>}
|
|
412
|
+
*/
|
|
413
|
+
export async function gateActionClass(o = {}) {
|
|
414
|
+
const req = await requestApproval(o);
|
|
415
|
+
if (!req.gated) {
|
|
416
|
+
return { approved: true, reason: "not-gated", id: null, classes: [], payloadHash: req.payloadHash };
|
|
417
|
+
}
|
|
418
|
+
if (!req.requested) {
|
|
419
|
+
const reason = isEnabled(o.cfg) ? "request-failed" : "org-disabled";
|
|
420
|
+
return { approved: false, reason, id: req.id, classes: req.classes, payloadHash: req.payloadHash };
|
|
421
|
+
}
|
|
422
|
+
const dec = await awaitDecision(req.id, Number.isFinite(o.timeoutMs) ? o.timeoutMs : 55000, {
|
|
423
|
+
cfg: o.cfg,
|
|
424
|
+
fetchImpl: o.fetchImpl,
|
|
425
|
+
nowMs: o.nowMs,
|
|
426
|
+
pollMs: o.pollMs,
|
|
427
|
+
});
|
|
428
|
+
if (dec.status === "approved") {
|
|
429
|
+
return { approved: true, reason: "approved", id: req.id, classes: req.classes, payloadHash: req.payloadHash, status: dec.status };
|
|
430
|
+
}
|
|
431
|
+
const reason = dec.decided ? dec.status : "timeout";
|
|
432
|
+
return { approved: false, reason, id: req.id, classes: req.classes, payloadHash: req.payloadHash, status: dec.status };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
export const _internals = { canonicalJson, classToKind, decisionStatus, isDecided, normaliseClassification };
|
|
436
|
+
|
|
437
|
+
export default {
|
|
438
|
+
GATED_CLASSES,
|
|
439
|
+
classesForAction,
|
|
440
|
+
loadActionClassification,
|
|
441
|
+
classifyAction,
|
|
442
|
+
hashPayload,
|
|
443
|
+
buildApprovalAsk,
|
|
444
|
+
defaultDeliver,
|
|
445
|
+
requestApproval,
|
|
446
|
+
awaitDecision,
|
|
447
|
+
gateActionClass,
|
|
448
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* approvals.test.mjs — action-class gate, sha256 payload binding, agent-mediated
|
|
3
|
+
* delivery, and the bounded long-poll awaitDecision / gateActionClass blocking
|
|
4
|
+
* helper. Injected fetchImpl + deliverFn; no network, no real channels.
|
|
5
|
+
* Run: node --test lib/org/approvals.test.mjs
|
|
6
|
+
*/
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
import { test } from "node:test";
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
GATED_CLASSES,
|
|
15
|
+
classesForAction,
|
|
16
|
+
classifyAction,
|
|
17
|
+
hashPayload,
|
|
18
|
+
buildApprovalAsk,
|
|
19
|
+
requestApproval,
|
|
20
|
+
awaitDecision,
|
|
21
|
+
gateActionClass,
|
|
22
|
+
} from "./approvals.mjs";
|
|
23
|
+
|
|
24
|
+
const BASE = "https://org.cohortapp.com";
|
|
25
|
+
const CFG = { org: { cohort: { enabled: true, base: BASE, token: "tok", orgId: "alex-ai" } } };
|
|
26
|
+
const OFF = { org: { cohort: { enabled: false } } };
|
|
27
|
+
|
|
28
|
+
delete process.env.COHORT_API_TOKEN;
|
|
29
|
+
delete process.env.COHORT_TOKEN;
|
|
30
|
+
|
|
31
|
+
/** Fake fetch capturing POST bodies and returning canned res frames / read payloads. */
|
|
32
|
+
function fakeFetch(responder) {
|
|
33
|
+
const calls = [];
|
|
34
|
+
const impl = async (url, init) => {
|
|
35
|
+
calls.push({ url, init, headers: (init && init.headers) || {}, body: init && init.body ? JSON.parse(init.body) : null });
|
|
36
|
+
const r = responder ? responder(url, init, calls.length - 1) : null;
|
|
37
|
+
const status = r && typeof r.status === "number" ? r.status : 200;
|
|
38
|
+
const ok = r && typeof r.ok === "boolean" ? r.ok : status >= 200 && status < 300;
|
|
39
|
+
const body = r ? r.body : { ok: true, result: null };
|
|
40
|
+
return { ok, status, headers: { get: () => undefined }, json: async () => body };
|
|
41
|
+
};
|
|
42
|
+
impl.calls = calls;
|
|
43
|
+
return impl;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// --- classification --------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
test("classesForAction: explicit class tags win; gated-by-risk derivation; non-gated empty", () => {
|
|
49
|
+
assert.deepEqual(classesForAction({ classes: ["financial"] }), ["financial"]);
|
|
50
|
+
assert.deepEqual(classesForAction({ action_class: "external" }), ["external"]);
|
|
51
|
+
// high-risk + escalation, no explicit tag, keyword "delete prod" → irreversible
|
|
52
|
+
const d = classesForAction({ risk: "high", approval: "principal_escalation", notes: "delete production data" });
|
|
53
|
+
assert.ok(d.includes("irreversible"));
|
|
54
|
+
// low-risk autonomous action is not gated
|
|
55
|
+
assert.deepEqual(classesForAction({ risk: "low", approval: "autonomous" }), []);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("classifyAction: explicit classification short-circuits; unknown fails closed", () => {
|
|
59
|
+
assert.deepEqual(classifyAction({ action: "x", classification: "financial" }), { gated: true, classes: ["financial"], source: "explicit" });
|
|
60
|
+
const unknown = classifyAction({ action: "never-seen", policy: {} });
|
|
61
|
+
assert.equal(unknown.gated, true);
|
|
62
|
+
assert.equal(unknown.source, "unknown-fail-closed");
|
|
63
|
+
const seen = classifyAction({ action: "send_slack", policy: { send_slack: { risk: "low", approval: "autonomous" } } });
|
|
64
|
+
assert.equal(seen.gated, false);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("GATED_CLASSES are the three action classes", () => {
|
|
68
|
+
assert.deepEqual([...GATED_CLASSES].sort(), ["external", "financial", "irreversible"]);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
// --- payload binding -------------------------------------------------------
|
|
72
|
+
|
|
73
|
+
test("hashPayload: stable sha256 over canonical JSON (key order independent)", () => {
|
|
74
|
+
const a = hashPayload({ amount: 100, to: "bob" });
|
|
75
|
+
const b = hashPayload({ to: "bob", amount: 100 });
|
|
76
|
+
assert.equal(a, b);
|
|
77
|
+
assert.equal(a, createHash("sha256").update(JSON.stringify({ amount: 100, to: "bob" })).digest("hex"));
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// --- requestApproval: binds the hash + posts approval.request ---------------
|
|
81
|
+
|
|
82
|
+
test("requestApproval: binds payload hash and POSTs approval.request with it", async () => {
|
|
83
|
+
const payload = { action: "wire", amount: 5000, to: "vendor" };
|
|
84
|
+
const expected = hashPayload(payload);
|
|
85
|
+
const f = fakeFetch((url) => {
|
|
86
|
+
if (url.endsWith("/v1/approval.request")) return { body: { ok: true, result: { id: "ap-x", status: "pending" } } };
|
|
87
|
+
return { body: { ok: true, result: null } };
|
|
88
|
+
});
|
|
89
|
+
const r = await requestApproval({
|
|
90
|
+
cfg: CFG,
|
|
91
|
+
action: "wire_funds",
|
|
92
|
+
classification: "financial",
|
|
93
|
+
payload,
|
|
94
|
+
approver: { slackId: "U123" },
|
|
95
|
+
deliverFn: async () => ({ ok: true, channel: "slack:U123", reason: "delivered" }),
|
|
96
|
+
fetchImpl: f,
|
|
97
|
+
});
|
|
98
|
+
assert.equal(r.gated, true);
|
|
99
|
+
assert.deepEqual(r.classes, ["financial"]);
|
|
100
|
+
assert.equal(r.requested, true);
|
|
101
|
+
assert.equal(r.payloadHash, expected);
|
|
102
|
+
// the request body carried the exact payload_hash ("approved exactly this")
|
|
103
|
+
const reqCall = f.calls.find((c) => c.url.endsWith("/v1/approval.request"));
|
|
104
|
+
assert.equal(reqCall.body.payload_hash, expected);
|
|
105
|
+
assert.equal(reqCall.body.kind, "spend");
|
|
106
|
+
// agent-mediated delivery fired
|
|
107
|
+
assert.equal(r.delivery.ok, true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
test("requestApproval: NON-gated action makes no server call and no delivery", async () => {
|
|
111
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
112
|
+
const r = await requestApproval({
|
|
113
|
+
cfg: CFG,
|
|
114
|
+
action: "send_slack",
|
|
115
|
+
classification: [], // explicitly not a gated class
|
|
116
|
+
payload: { msg: "hi" },
|
|
117
|
+
fetchImpl: f,
|
|
118
|
+
});
|
|
119
|
+
assert.equal(r.gated, false);
|
|
120
|
+
assert.equal(r.requested, false);
|
|
121
|
+
assert.equal(f.calls.length, 0);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
test("requestApproval: org disabled → gated decision + hash, but no call", async () => {
|
|
125
|
+
const r = await requestApproval({ cfg: OFF, action: "x", classification: "irreversible", payload: { a: 1 } });
|
|
126
|
+
assert.equal(r.gated, true);
|
|
127
|
+
assert.equal(r.requested, false);
|
|
128
|
+
assert.ok(r.payloadHash);
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("buildApprovalAsk: human-readable approve/deny affordance referencing the id", () => {
|
|
132
|
+
const ask = buildApprovalAsk({ id: "ap-1", action: "wire_funds", classes: ["financial"], requester: "alex" });
|
|
133
|
+
assert.match(ask, /Approval needed/);
|
|
134
|
+
assert.match(ask, /approve ap-1/);
|
|
135
|
+
assert.match(ask, /deny ap-1/);
|
|
136
|
+
assert.match(ask, /financial/);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// --- awaitDecision: resolves on approval -----------------------------------
|
|
140
|
+
|
|
141
|
+
test("awaitDecision: resolves decided=true when approval.wait returns approved", async () => {
|
|
142
|
+
const f = fakeFetch(() => ({ body: { id: "ap-1", status: "approved", resolved_by_agent: "human-1" } }));
|
|
143
|
+
const dec = await awaitDecision("ap-1", 10000, { cfg: CFG, fetchImpl: f });
|
|
144
|
+
assert.equal(dec.decided, true);
|
|
145
|
+
assert.equal(dec.status, "approved");
|
|
146
|
+
assert.match(f.calls[0].url, /\/v1\/approval\.wait\?id=ap-1/);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test("awaitDecision: denied is decided; pending past deadline is not decided", async () => {
|
|
150
|
+
const denied = fakeFetch(() => ({ body: { id: "ap-1", status: "denied" } }));
|
|
151
|
+
const d1 = await awaitDecision("ap-1", 5000, { cfg: CFG, fetchImpl: denied });
|
|
152
|
+
assert.equal(d1.decided, true);
|
|
153
|
+
assert.equal(d1.status, "denied");
|
|
154
|
+
|
|
155
|
+
// pending forever: a fixed clock makes now() === deadline after the first poll,
|
|
156
|
+
// so the do/while runs exactly once and then exits not-decided.
|
|
157
|
+
let t = 1000;
|
|
158
|
+
const clock = () => t;
|
|
159
|
+
const pending = fakeFetch(() => { t += 100000; return { body: { id: "ap-1", status: "pending" } }; });
|
|
160
|
+
const d2 = await awaitDecision("ap-1", 5000, { cfg: CFG, fetchImpl: pending, nowMs: clock });
|
|
161
|
+
assert.equal(d2.decided, false);
|
|
162
|
+
assert.equal(d2.status, "pending");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// --- gateActionClass: blocks then unblocks ---------------------------------
|
|
166
|
+
|
|
167
|
+
test("gateActionClass: non-gated → approved immediately, no server call", async () => {
|
|
168
|
+
const f = fakeFetch(() => ({ body: { ok: true, result: null } }));
|
|
169
|
+
const g = await gateActionClass({ cfg: CFG, action: "send_slack", classification: [], payload: {}, fetchImpl: f });
|
|
170
|
+
assert.equal(g.approved, true);
|
|
171
|
+
assert.equal(g.reason, "not-gated");
|
|
172
|
+
assert.equal(f.calls.length, 0);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("gateActionClass: gated action BLOCKS then UNBLOCKS on human approval", async () => {
|
|
176
|
+
const payload = { action: "merge", repo: "core", pr: 42 };
|
|
177
|
+
const expectHash = hashPayload(payload);
|
|
178
|
+
// request → pending; wait → approved (the human approved exactly this payload).
|
|
179
|
+
const f = fakeFetch((url) => {
|
|
180
|
+
if (url.includes("/v1/approval.request")) return { body: { ok: true, result: { id: "ap-m", status: "pending" } } };
|
|
181
|
+
if (url.includes("/v1/approval.wait")) return { body: { id: "ap-m", status: "approved" } };
|
|
182
|
+
return { body: { ok: true, result: null } };
|
|
183
|
+
});
|
|
184
|
+
const g = await gateActionClass({
|
|
185
|
+
cfg: CFG,
|
|
186
|
+
action: "merge_pr",
|
|
187
|
+
classification: "irreversible",
|
|
188
|
+
payload,
|
|
189
|
+
approver: { slackId: "U1" },
|
|
190
|
+
deliverFn: async () => ({ ok: true }),
|
|
191
|
+
timeoutMs: 10000,
|
|
192
|
+
fetchImpl: f,
|
|
193
|
+
});
|
|
194
|
+
assert.equal(g.approved, true);
|
|
195
|
+
assert.equal(g.reason, "approved");
|
|
196
|
+
assert.deepEqual(g.classes, ["irreversible"]);
|
|
197
|
+
// the executor can verify it runs EXACTLY the approved payload
|
|
198
|
+
assert.equal(g.payloadHash, expectHash);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
test("gateActionClass: denied → not approved with reason 'denied'", async () => {
|
|
202
|
+
const f = fakeFetch((url) => {
|
|
203
|
+
if (url.includes("/v1/approval.request")) return { body: { ok: true, result: { id: "ap-d", status: "pending" } } };
|
|
204
|
+
if (url.includes("/v1/approval.wait")) return { body: { id: "ap-d", status: "denied" } };
|
|
205
|
+
return { body: { ok: true, result: null } };
|
|
206
|
+
});
|
|
207
|
+
const g = await gateActionClass({ cfg: CFG, action: "x", classification: "external", payload: { a: 1 }, timeoutMs: 5000, fetchImpl: f });
|
|
208
|
+
assert.equal(g.approved, false);
|
|
209
|
+
assert.equal(g.reason, "denied");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("gateActionClass: org disabled → not approved, reason org-disabled (never silently passes)", async () => {
|
|
213
|
+
const g = await gateActionClass({ cfg: OFF, action: "x", classification: "financial", payload: { a: 1 } });
|
|
214
|
+
assert.equal(g.approved, false);
|
|
215
|
+
assert.equal(g.reason, "org-disabled");
|
|
216
|
+
});
|