@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,116 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Health Check — Verifies all Maestro agent subsystems are operational
|
|
3
|
+
# Usage: ./scripts/healthcheck.sh
|
|
4
|
+
# Exit codes: 0 = healthy, 1 = degraded, 2 = critical
|
|
5
|
+
|
|
6
|
+
set -e
|
|
7
|
+
|
|
8
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
9
|
+
SOPHIE_AI_DIR="$(dirname "$SCRIPT_DIR")"
|
|
10
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
11
|
+
HEALTHY=0
|
|
12
|
+
WARNINGS=0
|
|
13
|
+
ERRORS=0
|
|
14
|
+
|
|
15
|
+
echo "Maestro Health Check — $TIMESTAMP"
|
|
16
|
+
echo "======================================="
|
|
17
|
+
|
|
18
|
+
# Check 1: Emergency stop flag
|
|
19
|
+
if [ -f "$SOPHIE_AI_DIR/.emergency-stop" ]; then
|
|
20
|
+
echo "[STOP] Emergency stop flag is active"
|
|
21
|
+
ERRORS=$((ERRORS + 1))
|
|
22
|
+
else
|
|
23
|
+
echo "[OK] No emergency stop flag"
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Check 2: Anthropic auth — either ANTHROPIC_API_KEY set OR subscription
|
|
27
|
+
# auth enabled. Daemons launched under launchd don't inherit the agent's
|
|
28
|
+
# shell env, so a "missing" ANTHROPIC_API_KEY is normal when the operator
|
|
29
|
+
# opted into Claude Code subscription auth (MAESTRO_PREFER_SUBSCRIPTION_AUTH=1
|
|
30
|
+
# in .env strips the var and falls back to Keychain OAuth).
|
|
31
|
+
if [ -n "${ANTHROPIC_API_KEY:-}" ]; then
|
|
32
|
+
echo "[OK] Environment variable: ANTHROPIC_API_KEY"
|
|
33
|
+
elif [ -f "$SOPHIE_AI_DIR/.env" ] && grep -qE "^MAESTRO_PREFER_SUBSCRIPTION_AUTH=(1|true|yes)$" "$SOPHIE_AI_DIR/.env"; then
|
|
34
|
+
echo "[OK] Anthropic auth: subscription mode (.env: MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)"
|
|
35
|
+
elif [ -f "$SOPHIE_AI_DIR/.env" ] && grep -qE "^ANTHROPIC_API_KEY=." "$SOPHIE_AI_DIR/.env"; then
|
|
36
|
+
echo "[OK] Anthropic auth: ANTHROPIC_API_KEY in .env"
|
|
37
|
+
else
|
|
38
|
+
echo "[FAIL] Anthropic auth not configured (.env needs either ANTHROPIC_API_KEY or MAESTRO_PREFER_SUBSCRIPTION_AUTH=1)"
|
|
39
|
+
ERRORS=$((ERRORS + 1))
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
# Check 3: Key directories exist and are writable
|
|
43
|
+
for dir in logs outputs knowledge/memory config; do
|
|
44
|
+
if [ -d "$SOPHIE_AI_DIR/$dir" ] && [ -w "$SOPHIE_AI_DIR/$dir" ]; then
|
|
45
|
+
echo "[OK] Directory writable: $dir"
|
|
46
|
+
else
|
|
47
|
+
echo "[FAIL] Directory missing or not writable: $dir"
|
|
48
|
+
ERRORS=$((ERRORS + 1))
|
|
49
|
+
fi
|
|
50
|
+
done
|
|
51
|
+
|
|
52
|
+
# Check 4: Source repositories accessible
|
|
53
|
+
for repo in company-web innovation-lab partner-framework company-legal regulatory-submission strategic-initiative; do
|
|
54
|
+
if [ -d "$HOME/$repo" ]; then
|
|
55
|
+
echo "[OK] Source repo accessible: ~/$repo"
|
|
56
|
+
else
|
|
57
|
+
echo "[WARN] Source repo not found: ~/$repo"
|
|
58
|
+
WARNINGS=$((WARNINGS + 1))
|
|
59
|
+
fi
|
|
60
|
+
done
|
|
61
|
+
|
|
62
|
+
# Check 5: Key applications available
|
|
63
|
+
for app in Slack WhatsApp Safari; do
|
|
64
|
+
if [ -d "/Applications/$app.app" ] || [ -d "$HOME/Applications/$app.app" ]; then
|
|
65
|
+
echo "[OK] Application installed: $app"
|
|
66
|
+
else
|
|
67
|
+
echo "[WARN] Application not found: $app"
|
|
68
|
+
WARNINGS=$((WARNINGS + 1))
|
|
69
|
+
fi
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
# Check 6: Network connectivity
|
|
73
|
+
if curl -s --max-time 5 https://api.anthropic.com > /dev/null 2>&1; then
|
|
74
|
+
echo "[OK] Network: Anthropic API reachable"
|
|
75
|
+
else
|
|
76
|
+
echo "[FAIL] Network: Cannot reach Anthropic API"
|
|
77
|
+
ERRORS=$((ERRORS + 1))
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Check 7: Disk space
|
|
81
|
+
DISK_FREE=$(df -h "$SOPHIE_AI_DIR" | awk 'NR==2{print $5}' | tr -d '%')
|
|
82
|
+
if [ "$DISK_FREE" -gt 90 ]; then
|
|
83
|
+
echo "[WARN] Disk usage: ${DISK_FREE}% — consider cleanup"
|
|
84
|
+
WARNINGS=$((WARNINGS + 1))
|
|
85
|
+
elif [ "$DISK_FREE" -gt 95 ]; then
|
|
86
|
+
echo "[FAIL] Disk usage: ${DISK_FREE}% — critically low"
|
|
87
|
+
ERRORS=$((ERRORS + 1))
|
|
88
|
+
else
|
|
89
|
+
echo "[OK] Disk usage: ${DISK_FREE}%"
|
|
90
|
+
fi
|
|
91
|
+
|
|
92
|
+
# Check 8: Config files present
|
|
93
|
+
for config in priorities.yaml environment.yaml contacts.yaml; do
|
|
94
|
+
if [ -f "$SOPHIE_AI_DIR/config/$config" ]; then
|
|
95
|
+
echo "[OK] Config present: $config"
|
|
96
|
+
else
|
|
97
|
+
echo "[FAIL] Config missing: $config"
|
|
98
|
+
ERRORS=$((ERRORS + 1))
|
|
99
|
+
fi
|
|
100
|
+
done
|
|
101
|
+
|
|
102
|
+
# Summary
|
|
103
|
+
echo ""
|
|
104
|
+
echo "======================================="
|
|
105
|
+
echo "Results: $ERRORS errors, $WARNINGS warnings"
|
|
106
|
+
|
|
107
|
+
if [ "$ERRORS" -gt 0 ]; then
|
|
108
|
+
echo "Status: CRITICAL — fix errors before operating"
|
|
109
|
+
exit 2
|
|
110
|
+
elif [ "$WARNINGS" -gt 0 ]; then
|
|
111
|
+
echo "Status: DEGRADED — operational with limitations"
|
|
112
|
+
exit 1
|
|
113
|
+
else
|
|
114
|
+
echo "Status: HEALTHY — all systems operational"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Block org-MCP outbound writes in agent-repo sessions.
|
|
3
|
+
#
|
|
4
|
+
# The cohort MCP server (scaffold/.mcp.json) exposes the org tool surface to
|
|
5
|
+
# interactive Claude sessions. READS are welcome; the outbound WRITES
|
|
6
|
+
# (messaging_send, email_send, email_draft_send) and the org_rpc escape hatch
|
|
7
|
+
# must go through the
|
|
8
|
+
# sanctioned CLI lane instead — scripts/slack-send.sh / scripts/org/send-orgmail.mjs
|
|
9
|
+
# — which carries the full dedup + reply-storm + governance perimeter that the
|
|
10
|
+
# MCP tool's content screen alone does not. Same pattern as
|
|
11
|
+
# block-mcp-slack-send.sh (CEO directive si-010).
|
|
12
|
+
#
|
|
13
|
+
# Exit 2 = blocked (message shown to Claude as rejection)
|
|
14
|
+
echo "BLOCKED: org-MCP outbound writes are session-blocked. Reply via the sanctioned CLI lane instead: org messaging → the daemon's messaging path / scripts/slack-send.sh for Slack; workspace email → node scripts/org/send-orgmail.mjs --thread \"<thread_id>\" --body-file <path>. Read/board/decision org tools remain available." >&2
|
|
15
|
+
exit 2
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Block MCP Slack sends — per CEO directive si-010
|
|
3
|
+
# All Slack sending must go through User OAuth Token (xoxp-) via slack-send.sh
|
|
4
|
+
# MCP Slack adds "Sent using @Claude" label — unacceptable for the agent's identity
|
|
5
|
+
# Exit 2 = blocked (message shown to Claude as rejection)
|
|
6
|
+
echo "BLOCKED: Per CEO directive si-010 — do NOT use MCP Slack for sending. Use scripts/slack-send.sh with SLACK_USER_TOKEN instead." >&2
|
|
7
|
+
exit 2
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Post-action log hook — logs every tool completion to the audit trail.
|
|
3
|
+
# Called by Claude Code's PostToolUse hook after every tool execution.
|
|
4
|
+
#
|
|
5
|
+
# Stdin: Claude Code PostToolUse JSON payload (same contract the sibling
|
|
6
|
+
# pre-send-audit.sh relies on):
|
|
7
|
+
# { "tool_name": "...", "tool_input": { ... }, "tool_response": ...,
|
|
8
|
+
# "session_id": "..." }
|
|
9
|
+
#
|
|
10
|
+
# We parse that payload (jq when available, grep/sed fallback otherwise) and
|
|
11
|
+
# append one row:
|
|
12
|
+
# {"timestamp":ISO8601Z,"tool":<tool_name>,"status":<"error"|"completed">,
|
|
13
|
+
# "target":<best-effort recipient/path/url/command>,"session_id":<id|null>}
|
|
14
|
+
#
|
|
15
|
+
# Design note: PostToolUse runs AFTER the tool has executed, so this hook can
|
|
16
|
+
# never block or undo anything. Every path therefore fails OPEN and the script
|
|
17
|
+
# always exits 0 — a parse error must still produce a row (with whatever is
|
|
18
|
+
# parseable and status "unknown-parse"), never crash the session.
|
|
19
|
+
|
|
20
|
+
AGENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
21
|
+
ACTION_LOG="$AGENT_DIR/logs/audit/$(date +%Y-%m-%d)-actions.jsonl"
|
|
22
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
23
|
+
|
|
24
|
+
# Read the full hook payload from stdin (do NOT discard it — it carries the
|
|
25
|
+
# real tool name, inputs and response that the old hook threw away).
|
|
26
|
+
PAYLOAD=$(cat 2>/dev/null)
|
|
27
|
+
|
|
28
|
+
mkdir -p "$(dirname "$ACTION_LOG")" 2>/dev/null
|
|
29
|
+
|
|
30
|
+
# Emit a row and exit. Args: tool status target session_id (each already a
|
|
31
|
+
# bare string; jq -n quotes/escapes them safely; nulls passed as empty string
|
|
32
|
+
# become JSON null).
|
|
33
|
+
emit_row() {
|
|
34
|
+
local tool="$1" status="$2" target="$3" sid="$4"
|
|
35
|
+
if command -v jq >/dev/null 2>&1; then
|
|
36
|
+
jq -cn \
|
|
37
|
+
--arg ts "$TIMESTAMP" \
|
|
38
|
+
--arg tool "$tool" \
|
|
39
|
+
--arg status "$status" \
|
|
40
|
+
--arg target "$target" \
|
|
41
|
+
--arg sid "$sid" \
|
|
42
|
+
'{timestamp:$ts, tool:$tool, status:$status,
|
|
43
|
+
target:(if $target=="" then null else $target end),
|
|
44
|
+
session_id:(if $sid=="" then null else $sid end)}' \
|
|
45
|
+
>> "$ACTION_LOG" 2>/dev/null
|
|
46
|
+
else
|
|
47
|
+
# jq missing: hand-escape the few fields we control (backslash first,
|
|
48
|
+
# then double-quote, then strip control chars/newlines) so the row is
|
|
49
|
+
# still valid JSONL.
|
|
50
|
+
local esc_tool esc_status esc_target target_json sid_json
|
|
51
|
+
esc_tool=$(printf '%s' "$tool" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')
|
|
52
|
+
esc_status=$(printf '%s' "$status" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')
|
|
53
|
+
if [ -n "$target" ]; then
|
|
54
|
+
esc_target=$(printf '%s' "$target" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')
|
|
55
|
+
target_json="\"$esc_target\""
|
|
56
|
+
else
|
|
57
|
+
target_json="null"
|
|
58
|
+
fi
|
|
59
|
+
if [ -n "$sid" ]; then
|
|
60
|
+
sid_json="\"$(printf '%s' "$sid" | sed 's/\\/\\\\/g; s/"/\\"/g' | tr -d '\000-\037')\""
|
|
61
|
+
else
|
|
62
|
+
sid_json="null"
|
|
63
|
+
fi
|
|
64
|
+
printf '{"timestamp":"%s","tool":"%s","status":"%s","target":%s,"session_id":%s}\n' \
|
|
65
|
+
"$TIMESTAMP" "$esc_tool" "$esc_status" "$target_json" "$sid_json" \
|
|
66
|
+
>> "$ACTION_LOG" 2>/dev/null
|
|
67
|
+
fi
|
|
68
|
+
exit 0
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
# --- jq path (preferred) ----------------------------------------------------
|
|
72
|
+
if command -v jq >/dev/null 2>&1 && printf '%s' "$PAYLOAD" | jq -e . >/dev/null 2>&1; then
|
|
73
|
+
TOOL_NAME=$(printf '%s' "$PAYLOAD" | jq -r '.tool_name // "unknown"' 2>/dev/null)
|
|
74
|
+
SESSION_ID=$(printf '%s' "$PAYLOAD" | jq -r '.session_id // empty' 2>/dev/null)
|
|
75
|
+
|
|
76
|
+
# Status: "error" when the tool response signals failure in any of the
|
|
77
|
+
# shapes Claude Code uses (string "error", {error:...}, {is_error:true},
|
|
78
|
+
# {success:false}, or a non-zero numeric exit/code); else "completed".
|
|
79
|
+
STATUS=$(printf '%s' "$PAYLOAD" | jq -r '
|
|
80
|
+
.tool_response as $r
|
|
81
|
+
| if ($r == null) then "completed"
|
|
82
|
+
elif ($r | type) == "string" then
|
|
83
|
+
(if ($r | ascii_downcase | test("error")) then "error" else "completed" end)
|
|
84
|
+
elif ($r | type) == "object" then
|
|
85
|
+
(if ($r.is_error == true)
|
|
86
|
+
or ($r.error != null and $r.error != false)
|
|
87
|
+
or ($r.success == false)
|
|
88
|
+
or (($r.exit_code // $r.exitCode // $r.code // 0) | tonumber? // 0) != 0
|
|
89
|
+
then "error" else "completed" end)
|
|
90
|
+
else "completed" end
|
|
91
|
+
' 2>/dev/null)
|
|
92
|
+
[ -n "$STATUS" ] || STATUS="completed"
|
|
93
|
+
|
|
94
|
+
# Target: best-effort, first present of file_path / command / url / to /
|
|
95
|
+
# channel / recipient from tool_input. Command is truncated to 200 chars.
|
|
96
|
+
TARGET=$(printf '%s' "$PAYLOAD" | jq -r '
|
|
97
|
+
(.tool_input // {}) as $ti
|
|
98
|
+
| ( $ti.file_path?
|
|
99
|
+
// ( $ti.command? | if . == null then null else (.[0:200]) end )
|
|
100
|
+
// $ti.url?
|
|
101
|
+
// $ti.to?
|
|
102
|
+
// $ti.channel?
|
|
103
|
+
// $ti.recipient?
|
|
104
|
+
// empty )
|
|
105
|
+
| if . == null then "" else (. | tostring) end
|
|
106
|
+
' 2>/dev/null)
|
|
107
|
+
|
|
108
|
+
emit_row "$TOOL_NAME" "$STATUS" "$TARGET" "$SESSION_ID"
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
# --- fallback path: jq missing or payload not valid JSON --------------------
|
|
112
|
+
# Pull the tool name out of the raw payload with a minimal grep/sed extraction
|
|
113
|
+
# rather than writing "unknown". If we get a name we still record a useful row;
|
|
114
|
+
# status is "unknown-parse" because we cannot reliably inspect the response.
|
|
115
|
+
TOOL_NAME=$(printf '%s' "$PAYLOAD" \
|
|
116
|
+
| grep -o '"tool_name"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
117
|
+
| head -1 \
|
|
118
|
+
| sed 's/.*"tool_name"[[:space:]]*:[[:space:]]*"//; s/"$//')
|
|
119
|
+
[ -n "$TOOL_NAME" ] || TOOL_NAME="unknown"
|
|
120
|
+
|
|
121
|
+
SESSION_ID=$(printf '%s' "$PAYLOAD" \
|
|
122
|
+
| grep -o '"session_id"[[:space:]]*:[[:space:]]*"[^"]*"' \
|
|
123
|
+
| head -1 \
|
|
124
|
+
| sed 's/.*"session_id"[[:space:]]*:[[:space:]]*"//; s/"$//')
|
|
125
|
+
|
|
126
|
+
emit_row "$TOOL_NAME" "unknown-parse" "" "$SESSION_ID"
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Pre-send audit hook — gates outbound communications
|
|
3
|
+
# Called by Claude Code PreToolUse hook before Slack/Gmail/WhatsApp sends.
|
|
4
|
+
# Reads the PreToolUse payload from stdin, screens the outbound CONTENT
|
|
5
|
+
# against the banned-phrase list (policies/communication-style.md), runs a
|
|
6
|
+
# recipient allowlist sanity check, and enforces hourly + daily rate caps.
|
|
7
|
+
#
|
|
8
|
+
# Usage: pre-send-audit.sh <channel_type>
|
|
9
|
+
# channel_type: slack | slack-draft | gmail | whatsapp
|
|
10
|
+
#
|
|
11
|
+
# Stdin: Claude Code PreToolUse JSON payload:
|
|
12
|
+
# { "tool_name": "...", "tool_input": { "text"|"message"|"body": "...",
|
|
13
|
+
# "to"|"recipient"|"channel": "..." }, ... }
|
|
14
|
+
#
|
|
15
|
+
# Exit codes:
|
|
16
|
+
# 0 = allowed (stdout message shown to Claude)
|
|
17
|
+
# 2 = blocked / any parse|IO error (stderr reason shown as rejection).
|
|
18
|
+
# Claude Code blocks the tool call on any non-zero exit, so EVERY
|
|
19
|
+
# failure path below intentionally fails CLOSED with exit 2.
|
|
20
|
+
#
|
|
21
|
+
# Design note: this script does NOT use `set -e`. A non-zero from a grep
|
|
22
|
+
# (no match) or an arithmetic expansion must not silently abort the script
|
|
23
|
+
# into an implicit success/fail-open state. Instead every gate is checked
|
|
24
|
+
# explicitly and the script ends with `exit 0` only after all checks pass.
|
|
25
|
+
|
|
26
|
+
set -uo pipefail
|
|
27
|
+
|
|
28
|
+
# --- fail-closed helper -----------------------------------------------------
|
|
29
|
+
deny() {
|
|
30
|
+
# $1 = human-readable reason
|
|
31
|
+
echo "BLOCKED: ${1:-pre-send audit failed}" >&2
|
|
32
|
+
exit 2
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
CHANNEL_TYPE="${1:-}"
|
|
36
|
+
[ -n "$CHANNEL_TYPE" ] || deny "Channel type required (slack|slack-draft|gmail|whatsapp)"
|
|
37
|
+
|
|
38
|
+
# jq is mandatory — without it we cannot inspect content, so refuse to send.
|
|
39
|
+
command -v jq >/dev/null 2>&1 || deny "jq not available — cannot screen outbound content (fail closed)"
|
|
40
|
+
|
|
41
|
+
# --- read stdin payload -----------------------------------------------------
|
|
42
|
+
TOOL_INPUT=$(cat)
|
|
43
|
+
[ -n "$TOOL_INPUT" ] || deny "Empty hook payload — nothing to audit (fail closed)"
|
|
44
|
+
|
|
45
|
+
# Validate that the payload is well-formed JSON before doing anything else.
|
|
46
|
+
if ! printf '%s' "$TOOL_INPUT" | jq -e . >/dev/null 2>&1; then
|
|
47
|
+
deny "Malformed hook payload (not valid JSON) — fail closed"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
# Extract the outbound text. Claude Code wraps the tool args under
|
|
51
|
+
# tool_input; for robustness we also look at the top level. We concatenate
|
|
52
|
+
# every plausible content field so a banned phrase anywhere is caught.
|
|
53
|
+
CONTENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
|
|
54
|
+
[ .tool_input // {} , . ]
|
|
55
|
+
| map(
|
|
56
|
+
( .text? // empty ),
|
|
57
|
+
( .message? // empty ),
|
|
58
|
+
( .body? // empty ),
|
|
59
|
+
( .content? // empty ),
|
|
60
|
+
( .blocks? | if type=="array" or type=="object" then tojson else empty end )
|
|
61
|
+
)
|
|
62
|
+
| flatten
|
|
63
|
+
| map(select(. != null and . != ""))
|
|
64
|
+
| join("\n")
|
|
65
|
+
' 2>/dev/null) || deny "Failed to parse outbound content (fail closed)"
|
|
66
|
+
|
|
67
|
+
# Extract the recipient for the allowlist sanity check (optional field).
|
|
68
|
+
RECIPIENT=$(printf '%s' "$TOOL_INPUT" | jq -r '
|
|
69
|
+
( .tool_input // {} ) as $ti
|
|
70
|
+
| ( $ti.to? // $ti.recipient? // $ti.channel? // $ti.email? // $ti.thread_ts? // .to? // .recipient? // empty )
|
|
71
|
+
| if . == null then "" else (. | tostring) end
|
|
72
|
+
' 2>/dev/null) || deny "Failed to parse recipient (fail closed)"
|
|
73
|
+
|
|
74
|
+
# --- (a) banned-phrase screen ----------------------------------------------
|
|
75
|
+
# Source of truth: policies/communication-style.md (kept in sync manually).
|
|
76
|
+
# Matching is case-insensitive and substring-based. Opener phrases
|
|
77
|
+
# ("Certainly!" / "Absolutely!") are common in legitimate prose mid-sentence,
|
|
78
|
+
# so we anchor those to the start of the content / a line.
|
|
79
|
+
BANNED_SUBSTRINGS=(
|
|
80
|
+
"I'm here to help"
|
|
81
|
+
"I am here to help"
|
|
82
|
+
"Let me know if you need anything else"
|
|
83
|
+
"I hope this helps"
|
|
84
|
+
"Feel free to ask if you have any questions"
|
|
85
|
+
"As an AI"
|
|
86
|
+
"As a language model"
|
|
87
|
+
"I should clarify that I am"
|
|
88
|
+
"Great question!"
|
|
89
|
+
"Here's a comprehensive overview of"
|
|
90
|
+
"Here is a comprehensive overview of"
|
|
91
|
+
"I'd be happy to"
|
|
92
|
+
"I would be happy to"
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
# Lowercased copy for case-insensitive substring matching.
|
|
96
|
+
CONTENT_LC=$(printf '%s' "$CONTENT" | tr '[:upper:]' '[:lower:]')
|
|
97
|
+
|
|
98
|
+
for phrase in "${BANNED_SUBSTRINGS[@]}"; do
|
|
99
|
+
phrase_lc=$(printf '%s' "$phrase" | tr '[:upper:]' '[:lower:]')
|
|
100
|
+
case "$CONTENT_LC" in
|
|
101
|
+
*"$phrase_lc"*)
|
|
102
|
+
deny "Banned AI-tell phrase in outbound message: \"$phrase\" (see policies/communication-style.md)"
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
105
|
+
done
|
|
106
|
+
|
|
107
|
+
# Opener-only phrases: block when they begin the message or any line.
|
|
108
|
+
# (Mid-sentence "absolutely" is fine; "Absolutely!" as a line opener is not.)
|
|
109
|
+
if printf '%s' "$CONTENT" | grep -Eiq '(^|[[:cntrl:]])[[:space:]]*(Certainly!|Absolutely!|Great question!)'; then
|
|
110
|
+
deny "Banned filler opener (Certainly!/Absolutely!/Great question!) — see policies/communication-style.md"
|
|
111
|
+
fi
|
|
112
|
+
|
|
113
|
+
# --- (b) recipient allowlist sanity check ----------------------------------
|
|
114
|
+
# Only enforced when a recipient is present AND an allowlist file exists.
|
|
115
|
+
# Allowlist file: one entry (channel id, email, handle) per line, '#' comments.
|
|
116
|
+
ALLOWLIST_FILE="${MAESTRO_SEND_ALLOWLIST:-}"
|
|
117
|
+
if [ -n "$RECIPIENT" ] && [ -n "$ALLOWLIST_FILE" ] && [ -f "$ALLOWLIST_FILE" ]; then
|
|
118
|
+
if ! grep -Eq -- "^[[:space:]]*$(printf '%s' "$RECIPIENT" | sed 's/[.[\*^$()+?{|]/\\&/g')[[:space:]]*(#.*)?$" "$ALLOWLIST_FILE" 2>/dev/null; then
|
|
119
|
+
deny "Recipient \"$RECIPIENT\" not on send allowlist ($ALLOWLIST_FILE)"
|
|
120
|
+
fi
|
|
121
|
+
fi
|
|
122
|
+
|
|
123
|
+
# --- (c) rate caps (hourly + daily) ----------------------------------------
|
|
124
|
+
RUNTIME_DIR="${MAESTRO_RUNTIME_DIR:-$HOME/.maestro/runtime}/audit"
|
|
125
|
+
if ! mkdir -p "$RUNTIME_DIR" 2>/dev/null; then
|
|
126
|
+
deny "Cannot create audit runtime dir $RUNTIME_DIR (fail closed)"
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
COUNTER_FILE="$RUNTIME_DIR/send-counter.tsv"
|
|
130
|
+
TODAY=$(date +%Y-%m-%d)
|
|
131
|
+
HOUR=$(date +%H)
|
|
132
|
+
TIMESTAMP=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
|
|
133
|
+
SEND_LOG="$RUNTIME_DIR/$TODAY-sends.jsonl"
|
|
134
|
+
|
|
135
|
+
PER_HOUR_CAP="${MAESTRO_SEND_PER_HOUR:-3000}"
|
|
136
|
+
PER_DAY_CAP="${MAESTRO_SEND_PER_DAY:-20000}"
|
|
137
|
+
|
|
138
|
+
# Counter format (TSV): date<TAB>hour<TAB>hour_count<TAB>day_count
|
|
139
|
+
# Reset whenever the date rolls over; reset the hour bucket when hour rolls.
|
|
140
|
+
PREV_DATE=""; PREV_HOUR=""; HOUR_COUNT=0; DAY_COUNT=0
|
|
141
|
+
if [ -f "$COUNTER_FILE" ]; then
|
|
142
|
+
IFS=$'\t' read -r PREV_DATE PREV_HOUR HOUR_COUNT DAY_COUNT < "$COUNTER_FILE" 2>/dev/null || true
|
|
143
|
+
fi
|
|
144
|
+
# Sanitise to integers (fail closed to 0 rather than aborting).
|
|
145
|
+
case "$HOUR_COUNT" in (''|*[!0-9]*) HOUR_COUNT=0 ;; esac
|
|
146
|
+
case "$DAY_COUNT" in (''|*[!0-9]*) DAY_COUNT=0 ;; esac
|
|
147
|
+
|
|
148
|
+
if [ "$PREV_DATE" != "$TODAY" ]; then
|
|
149
|
+
DAY_COUNT=0
|
|
150
|
+
HOUR_COUNT=0
|
|
151
|
+
elif [ "$PREV_HOUR" != "$HOUR" ]; then
|
|
152
|
+
HOUR_COUNT=0
|
|
153
|
+
fi
|
|
154
|
+
|
|
155
|
+
if [ "$DAY_COUNT" -ge "$PER_DAY_CAP" ]; then
|
|
156
|
+
deny "Daily send limit ($PER_DAY_CAP) reached. Sends resume tomorrow."
|
|
157
|
+
fi
|
|
158
|
+
if [ "$HOUR_COUNT" -ge "$PER_HOUR_CAP" ]; then
|
|
159
|
+
deny "Hourly send limit ($PER_HOUR_CAP) reached. Sends resume next hour."
|
|
160
|
+
fi
|
|
161
|
+
|
|
162
|
+
# --- commit: increment counters + log allowed send -------------------------
|
|
163
|
+
HOUR_COUNT=$((HOUR_COUNT + 1))
|
|
164
|
+
DAY_COUNT=$((DAY_COUNT + 1))
|
|
165
|
+
if ! printf '%s\t%s\t%s\t%s\n' "$TODAY" "$HOUR" "$HOUR_COUNT" "$DAY_COUNT" > "$COUNTER_FILE" 2>/dev/null; then
|
|
166
|
+
deny "Cannot write send counter (fail closed)"
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
RECIPIENT_JSON=$(printf '%s' "$RECIPIENT" | jq -Rs . 2>/dev/null || printf '""')
|
|
170
|
+
printf '{"timestamp":"%s","channel":"%s","recipient":%s,"status":"allowed"}\n' \
|
|
171
|
+
"$TIMESTAMP" "$CHANNEL_TYPE" "$RECIPIENT_JSON" >> "$SEND_LOG" 2>/dev/null || true
|
|
172
|
+
|
|
173
|
+
echo "Audit: $CHANNEL_TYPE send screened and logged at $TIMESTAMP (hour: $HOUR_COUNT/$PER_HOUR_CAP, day: $DAY_COUNT/$PER_DAY_CAP)"
|
|
174
|
+
exit 0
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
// node:test for scripts/hooks/pre-send-audit.sh (audit M1)
|
|
2
|
+
//
|
|
3
|
+
// The hook is a Claude Code PreToolUse gate that must SCREEN outbound
|
|
4
|
+
// content and FAIL CLOSED (exit 2) on any banned phrase, allowlist miss,
|
|
5
|
+
// rate-cap breach, or parse/IO error. These tests execFile the real shell
|
|
6
|
+
// script with crafted stdin JSON and assert the exit code.
|
|
7
|
+
//
|
|
8
|
+
// jq is required by the hook; if it is not on PATH we skip rather than
|
|
9
|
+
// report a false failure (but jq is available in CI/dev here).
|
|
10
|
+
|
|
11
|
+
import { test } from "node:test";
|
|
12
|
+
import assert from "node:assert/strict";
|
|
13
|
+
import { execFile, execFileSync } from "node:child_process";
|
|
14
|
+
import { fileURLToPath } from "node:url";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
17
|
+
import { tmpdir } from "node:os";
|
|
18
|
+
|
|
19
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
20
|
+
const HOOK = join(__dirname, "pre-send-audit.sh");
|
|
21
|
+
|
|
22
|
+
function jqAvailable() {
|
|
23
|
+
try {
|
|
24
|
+
execFileSync("jq", ["--version"], { stdio: "ignore" });
|
|
25
|
+
return true;
|
|
26
|
+
} catch {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
const JQ = jqAvailable();
|
|
31
|
+
|
|
32
|
+
// Run the hook with the given stdin, returning { code, stdout, stderr }.
|
|
33
|
+
// Each call gets its own runtime dir so the rate counters never leak
|
|
34
|
+
// between cases. The shell env is reset to a known baseline so a stray
|
|
35
|
+
// MAESTRO_SEND_* in the test runner's environment cannot skew caps.
|
|
36
|
+
function runHook(stdin, { channel = "slack", env = {} } = {}) {
|
|
37
|
+
const runtimeDir = mkdtempSync(join(tmpdir(), "presend-"));
|
|
38
|
+
return new Promise((resolve) => {
|
|
39
|
+
const child = execFile(
|
|
40
|
+
"bash",
|
|
41
|
+
[HOOK, channel],
|
|
42
|
+
{
|
|
43
|
+
env: {
|
|
44
|
+
PATH: process.env.PATH,
|
|
45
|
+
HOME: runtimeDir,
|
|
46
|
+
MAESTRO_RUNTIME_DIR: runtimeDir,
|
|
47
|
+
...env,
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
(err, stdout, stderr) => {
|
|
51
|
+
const code = err && typeof err.code === "number" ? err.code : 0;
|
|
52
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
53
|
+
resolve({ code, stdout, stderr });
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
child.stdin.end(stdin);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
test("clean message passes (exit 0)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
61
|
+
const payload = JSON.stringify({
|
|
62
|
+
tool_name: "send",
|
|
63
|
+
tool_input: { text: "In progress — only blocker is DBA approval, expected EOD.", to: "C123" },
|
|
64
|
+
});
|
|
65
|
+
const { code, stdout } = await runHook(payload);
|
|
66
|
+
assert.equal(code, 0, `expected exit 0, got ${code}`);
|
|
67
|
+
assert.match(stdout, /screened and logged/);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("banned phrase 'Let me know if you need anything else' blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
71
|
+
const payload = JSON.stringify({ tool_input: { text: "Done. Let me know if you need anything else!" } });
|
|
72
|
+
const { code, stderr } = await runHook(payload);
|
|
73
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
74
|
+
assert.match(stderr, /BLOCKED/);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("banned phrase 'As an AI' blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
78
|
+
const payload = JSON.stringify({ tool_input: { body: "As an AI, I cannot make that commitment." } });
|
|
79
|
+
const { code } = await runHook(payload, { channel: "gmail" });
|
|
80
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test("banned phrase \"I'd be happy to\" blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
84
|
+
const payload = JSON.stringify({ tool_input: { message: "I'd be happy to set that up for you." } });
|
|
85
|
+
const { code } = await runHook(payload);
|
|
86
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("filler opener 'Absolutely!' blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
90
|
+
const payload = JSON.stringify({ tool_input: { text: "Absolutely! I can pull that together." } });
|
|
91
|
+
const { code } = await runHook(payload);
|
|
92
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("mid-sentence 'absolutely' (no opener) passes (exit 0)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
96
|
+
const payload = JSON.stringify({ tool_input: { text: "I am absolutely certain the deploy is fine." } });
|
|
97
|
+
const { code } = await runHook(payload);
|
|
98
|
+
assert.equal(code, 0, `expected exit 0, got ${code}`);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
test("banned phrase inside Slack blocks array blocks (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
102
|
+
const payload = JSON.stringify({
|
|
103
|
+
tool_input: { blocks: [{ type: "section", text: { type: "mrkdwn", text: "As an AI I will help with that." } }] },
|
|
104
|
+
});
|
|
105
|
+
const { code } = await runHook(payload);
|
|
106
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("malformed JSON fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
110
|
+
const { code, stderr } = await runHook("not json {{{");
|
|
111
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
112
|
+
assert.match(stderr, /Malformed|fail closed/i);
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
test("empty stdin fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
116
|
+
const { code } = await runHook("");
|
|
117
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("missing channel arg fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
121
|
+
// Invoke with no channel argument.
|
|
122
|
+
const runtimeDir = mkdtempSync(join(tmpdir(), "presend-"));
|
|
123
|
+
const code = await new Promise((resolve) => {
|
|
124
|
+
const child = execFile(
|
|
125
|
+
"bash",
|
|
126
|
+
[HOOK],
|
|
127
|
+
{ env: { PATH: process.env.PATH, HOME: runtimeDir, MAESTRO_RUNTIME_DIR: runtimeDir } },
|
|
128
|
+
(err) => {
|
|
129
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
130
|
+
resolve(err && typeof err.code === "number" ? err.code : 0);
|
|
131
|
+
},
|
|
132
|
+
);
|
|
133
|
+
child.stdin.end(JSON.stringify({ tool_input: { text: "hi" } }));
|
|
134
|
+
});
|
|
135
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("recipient on allowlist passes (exit 0)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
139
|
+
const dir = mkdtempSync(join(tmpdir(), "presend-allow-"));
|
|
140
|
+
const allow = join(dir, "allow.txt");
|
|
141
|
+
writeFileSync(allow, "C123\ngood@example.com\n# a comment\n");
|
|
142
|
+
const payload = JSON.stringify({ tool_input: { text: "hello", to: "C123" } });
|
|
143
|
+
const { code } = await runHook(payload, { env: { MAESTRO_SEND_ALLOWLIST: allow } });
|
|
144
|
+
rmSync(dir, { recursive: true, force: true });
|
|
145
|
+
assert.equal(code, 0, `expected exit 0, got ${code}`);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("recipient NOT on allowlist fails closed (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
149
|
+
const dir = mkdtempSync(join(tmpdir(), "presend-allow-"));
|
|
150
|
+
const allow = join(dir, "allow.txt");
|
|
151
|
+
writeFileSync(allow, "C123\ngood@example.com\n");
|
|
152
|
+
const payload = JSON.stringify({ tool_input: { text: "hello", to: "C999" } });
|
|
153
|
+
const { code, stderr } = await runHook(payload, { env: { MAESTRO_SEND_ALLOWLIST: allow } });
|
|
154
|
+
rmSync(dir, { recursive: true, force: true });
|
|
155
|
+
assert.equal(code, 2, `expected exit 2, got ${code}`);
|
|
156
|
+
assert.match(stderr, /allowlist/i);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("hourly cap blocks once exceeded (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
160
|
+
// Share one runtime dir across three sends with PER_HOUR=2.
|
|
161
|
+
const runtimeDir = mkdtempSync(join(tmpdir(), "presend-hour-"));
|
|
162
|
+
const payload = JSON.stringify({ tool_input: { text: "hi" } });
|
|
163
|
+
const send = () =>
|
|
164
|
+
new Promise((resolve) => {
|
|
165
|
+
const child = execFile(
|
|
166
|
+
"bash",
|
|
167
|
+
[HOOK, "slack"],
|
|
168
|
+
{
|
|
169
|
+
env: {
|
|
170
|
+
PATH: process.env.PATH,
|
|
171
|
+
HOME: runtimeDir,
|
|
172
|
+
MAESTRO_RUNTIME_DIR: runtimeDir,
|
|
173
|
+
MAESTRO_SEND_PER_HOUR: "2",
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
(err) => resolve(err && typeof err.code === "number" ? err.code : 0),
|
|
177
|
+
);
|
|
178
|
+
child.stdin.end(payload);
|
|
179
|
+
});
|
|
180
|
+
const c1 = await send();
|
|
181
|
+
const c2 = await send();
|
|
182
|
+
const c3 = await send();
|
|
183
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
184
|
+
assert.equal(c1, 0, `send 1 expected 0, got ${c1}`);
|
|
185
|
+
assert.equal(c2, 0, `send 2 expected 0, got ${c2}`);
|
|
186
|
+
assert.equal(c3, 2, `send 3 expected 2 (hourly cap), got ${c3}`);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
test("daily cap blocks once exceeded (exit 2)", { skip: JQ ? false : "jq not on PATH" }, async () => {
|
|
190
|
+
const runtimeDir = mkdtempSync(join(tmpdir(), "presend-day-"));
|
|
191
|
+
const payload = JSON.stringify({ tool_input: { text: "hi" } });
|
|
192
|
+
const send = () =>
|
|
193
|
+
new Promise((resolve) => {
|
|
194
|
+
const child = execFile(
|
|
195
|
+
"bash",
|
|
196
|
+
[HOOK, "slack"],
|
|
197
|
+
{
|
|
198
|
+
env: {
|
|
199
|
+
PATH: process.env.PATH,
|
|
200
|
+
HOME: runtimeDir,
|
|
201
|
+
MAESTRO_RUNTIME_DIR: runtimeDir,
|
|
202
|
+
MAESTRO_SEND_PER_DAY: "1",
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
(err, stdout, stderr) => resolve({ code: err && typeof err.code === "number" ? err.code : 0, stderr }),
|
|
206
|
+
);
|
|
207
|
+
child.stdin.end(payload);
|
|
208
|
+
});
|
|
209
|
+
const r1 = await send();
|
|
210
|
+
const r2 = await send();
|
|
211
|
+
rmSync(runtimeDir, { recursive: true, force: true });
|
|
212
|
+
assert.equal(r1.code, 0, `send 1 expected 0, got ${r1.code}`);
|
|
213
|
+
assert.equal(r2.code, 2, `send 2 expected 2 (daily cap), got ${r2.code}`);
|
|
214
|
+
assert.match(r2.stderr, /Daily send limit/);
|
|
215
|
+
});
|