@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,441 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/security/redact.test.mjs
|
|
3
|
+
*
|
|
4
|
+
* node:test coverage for redact.mjs:
|
|
5
|
+
* - user:pass@ userinfo is stripped
|
|
6
|
+
* - sensitive query params (token/key/secret/password/apikey/...) are masked
|
|
7
|
+
* - benign URLs are untouched
|
|
8
|
+
* - malformed URLs do not throw
|
|
9
|
+
* - private vs public IP classification, incl. IPv4-mapped-IPv6 bypass
|
|
10
|
+
* (::ffff:10.0.0.1) and loopback 127.0.0.1
|
|
11
|
+
*
|
|
12
|
+
* Run: cd /Users/wong/maestro && node --test lib/security/redact.test.mjs
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
redactSensitiveUrl,
|
|
20
|
+
isPrivateOrReservedIp,
|
|
21
|
+
isSensitiveParamName,
|
|
22
|
+
REDACTED,
|
|
23
|
+
maskText,
|
|
24
|
+
maskTextString,
|
|
25
|
+
redactObject,
|
|
26
|
+
REDACTED_LABEL_PREFIX,
|
|
27
|
+
} from "./redact.mjs";
|
|
28
|
+
|
|
29
|
+
/* ----------------------------- userinfo ---------------------------------- */
|
|
30
|
+
|
|
31
|
+
test("strips user:pass@ userinfo from a URL", () => {
|
|
32
|
+
const out = redactSensitiveUrl("https://alice:hunter2@example.com/path?x=1");
|
|
33
|
+
assert.ok(!out.includes("alice"), `username leaked: ${out}`);
|
|
34
|
+
assert.ok(!out.includes("hunter2"), `password leaked: ${out}`);
|
|
35
|
+
assert.ok(!out.includes("@example.com"), `@ host leaked: ${out}`);
|
|
36
|
+
assert.ok(out.startsWith("https://example.com/"), `host changed: ${out}`);
|
|
37
|
+
// Non-sensitive param preserved.
|
|
38
|
+
assert.ok(out.includes("x=1"), `benign param dropped: ${out}`);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test("strips userinfo with username only (no password)", () => {
|
|
42
|
+
const out = redactSensitiveUrl("https://tokenuser@api.example.com/");
|
|
43
|
+
assert.ok(!out.includes("tokenuser"), `username leaked: ${out}`);
|
|
44
|
+
assert.ok(out.startsWith("https://api.example.com/"), out);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
/* --------------------------- secret params ------------------------------- */
|
|
48
|
+
|
|
49
|
+
test("masks sensitive query params, keeps benign ones", () => {
|
|
50
|
+
const out = redactSensitiveUrl(
|
|
51
|
+
"https://example.com/api?token=abc123&page=2&api_key=zzz&q=hello"
|
|
52
|
+
);
|
|
53
|
+
assert.ok(out.includes(`token=${encodeURIComponent(REDACTED)}`) || out.includes("token=***"), out);
|
|
54
|
+
assert.ok(!out.includes("abc123"), `token value leaked: ${out}`);
|
|
55
|
+
assert.ok(!out.includes("zzz"), `api_key value leaked: ${out}`);
|
|
56
|
+
// Benign params untouched.
|
|
57
|
+
assert.ok(out.includes("page=2"), out);
|
|
58
|
+
assert.ok(out.includes("q=hello"), out);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("masks each sensitive keyword variant", () => {
|
|
62
|
+
const cases = [
|
|
63
|
+
"token",
|
|
64
|
+
"access_token",
|
|
65
|
+
"refresh_token",
|
|
66
|
+
"key",
|
|
67
|
+
"apiKey",
|
|
68
|
+
"api_key",
|
|
69
|
+
"secret",
|
|
70
|
+
"client_secret",
|
|
71
|
+
"password",
|
|
72
|
+
"passwd",
|
|
73
|
+
"pwd",
|
|
74
|
+
"auth",
|
|
75
|
+
"signature",
|
|
76
|
+
"sig",
|
|
77
|
+
];
|
|
78
|
+
for (const k of cases) {
|
|
79
|
+
const out = redactSensitiveUrl(`https://e.com/?${k}=SUPERSECRETVALUE`);
|
|
80
|
+
assert.ok(
|
|
81
|
+
!out.includes("SUPERSECRETVALUE"),
|
|
82
|
+
`param "${k}" not masked: ${out}`
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("masks repeated sensitive params (multiplicity preserved)", () => {
|
|
88
|
+
const out = redactSensitiveUrl("https://e.com/?key=a&key=b&n=1");
|
|
89
|
+
assert.ok(!out.includes("=a"), out);
|
|
90
|
+
assert.ok(!out.includes("=b"), out);
|
|
91
|
+
// two key= occurrences remain
|
|
92
|
+
const count = (out.match(/key=/g) || []).length;
|
|
93
|
+
assert.equal(count, 2, `expected 2 key= occurrences: ${out}`);
|
|
94
|
+
assert.ok(out.includes("n=1"), out);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
/* ----------------------------- benign ------------------------------------ */
|
|
98
|
+
|
|
99
|
+
test("leaves a benign URL effectively untouched", () => {
|
|
100
|
+
const url = "https://example.com/docs/page?lang=en&page=2";
|
|
101
|
+
const out = redactSensitiveUrl(url);
|
|
102
|
+
assert.ok(out.includes("example.com/docs/page"), out);
|
|
103
|
+
assert.ok(out.includes("lang=en"), out);
|
|
104
|
+
assert.ok(out.includes("page=2"), out);
|
|
105
|
+
assert.ok(!out.includes(REDACTED), `nothing should be masked: ${out}`);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("benign URL with no query is unchanged", () => {
|
|
109
|
+
const url = "https://example.com/";
|
|
110
|
+
assert.equal(redactSensitiveUrl(url), url);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
/* ---------------------------- malformed ---------------------------------- */
|
|
114
|
+
|
|
115
|
+
test("malformed / non-URL inputs do not throw", () => {
|
|
116
|
+
const inputs = [
|
|
117
|
+
"",
|
|
118
|
+
"not a url",
|
|
119
|
+
"http://",
|
|
120
|
+
"://missing-scheme",
|
|
121
|
+
"ht!tp://bad",
|
|
122
|
+
"https://alice:pw@", // truncated authority
|
|
123
|
+
"token=secret&password=hunter2", // bare query-ish string
|
|
124
|
+
"javascript:alert(1)",
|
|
125
|
+
" ",
|
|
126
|
+
"%%%not-encoded%%%",
|
|
127
|
+
];
|
|
128
|
+
for (const inp of inputs) {
|
|
129
|
+
assert.doesNotThrow(() => redactSensitiveUrl(inp), `threw on: ${JSON.stringify(inp)}`);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
test("non-string inputs are handled without throwing", () => {
|
|
134
|
+
assert.doesNotThrow(() => redactSensitiveUrl(null));
|
|
135
|
+
assert.doesNotThrow(() => redactSensitiveUrl(undefined));
|
|
136
|
+
assert.doesNotThrow(() => redactSensitiveUrl(12345));
|
|
137
|
+
assert.equal(redactSensitiveUrl(null), "");
|
|
138
|
+
assert.equal(redactSensitiveUrl(undefined), "");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("regex fallback masks secrets in unparseable query strings", () => {
|
|
142
|
+
const out = redactSensitiveUrl("token=abc123&page=2&password=hunter2");
|
|
143
|
+
assert.ok(!out.includes("abc123"), out);
|
|
144
|
+
assert.ok(!out.includes("hunter2"), out);
|
|
145
|
+
assert.ok(out.includes("page=2"), out);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
/* ------------------------ isSensitiveParamName --------------------------- */
|
|
149
|
+
|
|
150
|
+
test("isSensitiveParamName matches case-insensitively", () => {
|
|
151
|
+
assert.equal(isSensitiveParamName("ApiKey"), true);
|
|
152
|
+
assert.equal(isSensitiveParamName("X-Auth-Token"), true);
|
|
153
|
+
assert.equal(isSensitiveParamName("PASSWORD"), true);
|
|
154
|
+
assert.equal(isSensitiveParamName("page"), false);
|
|
155
|
+
assert.equal(isSensitiveParamName("q"), false);
|
|
156
|
+
assert.equal(isSensitiveParamName(""), false);
|
|
157
|
+
assert.equal(isSensitiveParamName(null), false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
/* --------------------------- IP: private --------------------------------- */
|
|
161
|
+
|
|
162
|
+
test("classifies private IPv4 ranges as private/reserved", () => {
|
|
163
|
+
const priv = [
|
|
164
|
+
"10.0.0.1",
|
|
165
|
+
"10.255.255.255",
|
|
166
|
+
"172.16.0.1",
|
|
167
|
+
"172.31.255.255",
|
|
168
|
+
"192.168.0.1",
|
|
169
|
+
"192.168.1.1",
|
|
170
|
+
"127.0.0.1", // loopback
|
|
171
|
+
"127.1.2.3",
|
|
172
|
+
"169.254.0.1", // link-local
|
|
173
|
+
"0.0.0.0", // unspecified
|
|
174
|
+
"100.64.0.1", // CGNAT
|
|
175
|
+
"255.255.255.255", // broadcast / reserved
|
|
176
|
+
"224.0.0.1", // multicast
|
|
177
|
+
"198.18.0.1", // benchmarking
|
|
178
|
+
"192.0.2.5", // TEST-NET-1
|
|
179
|
+
"203.0.113.9", // TEST-NET-3
|
|
180
|
+
];
|
|
181
|
+
for (const ip of priv) {
|
|
182
|
+
assert.equal(isPrivateOrReservedIp(ip), true, `${ip} should be private/reserved`);
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("127.0.0.1 loopback is private/reserved", () => {
|
|
187
|
+
assert.equal(isPrivateOrReservedIp("127.0.0.1"), true);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
/* ---------------------------- IP: public --------------------------------- */
|
|
191
|
+
|
|
192
|
+
test("classifies public IPv4 as NOT private/reserved", () => {
|
|
193
|
+
const pub = ["8.8.8.8", "1.1.1.1", "93.184.216.34", "172.15.0.1", "172.32.0.1", "11.0.0.1"];
|
|
194
|
+
for (const ip of pub) {
|
|
195
|
+
assert.equal(isPrivateOrReservedIp(ip), false, `${ip} should be public`);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
/* --------------------------- IP: IPv6 ------------------------------------ */
|
|
200
|
+
|
|
201
|
+
test("IPv4-mapped-IPv6 private addresses are caught (bypass protection)", () => {
|
|
202
|
+
assert.equal(isPrivateOrReservedIp("::ffff:10.0.0.1"), true);
|
|
203
|
+
assert.equal(isPrivateOrReservedIp("::ffff:127.0.0.1"), true);
|
|
204
|
+
assert.equal(isPrivateOrReservedIp("::ffff:192.168.1.1"), true);
|
|
205
|
+
assert.equal(isPrivateOrReservedIp("[::ffff:10.0.0.1]"), true);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("IPv4-mapped-IPv6 public address stays public", () => {
|
|
209
|
+
assert.equal(isPrivateOrReservedIp("::ffff:8.8.8.8"), false);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
test("classifies IPv6 loopback / ULA / link-local / multicast as private", () => {
|
|
213
|
+
assert.equal(isPrivateOrReservedIp("::1"), true); // loopback
|
|
214
|
+
assert.equal(isPrivateOrReservedIp("[::1]"), true);
|
|
215
|
+
assert.equal(isPrivateOrReservedIp("::"), true); // unspecified
|
|
216
|
+
assert.equal(isPrivateOrReservedIp("fc00::1"), true); // ULA
|
|
217
|
+
assert.equal(isPrivateOrReservedIp("fd12:3456:789a::1"), true); // ULA
|
|
218
|
+
assert.equal(isPrivateOrReservedIp("fe80::1"), true); // link-local
|
|
219
|
+
assert.equal(isPrivateOrReservedIp("fe80::1%eth0"), true); // link-local + zone
|
|
220
|
+
assert.equal(isPrivateOrReservedIp("ff02::1"), true); // multicast
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
test("classifies public IPv6 as NOT private", () => {
|
|
224
|
+
assert.equal(isPrivateOrReservedIp("2606:4700:4700::1111"), false); // cloudflare
|
|
225
|
+
assert.equal(isPrivateOrReservedIp("2001:4860:4860::8888"), false); // google
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
/* --------------------------- IP: edges ----------------------------------- */
|
|
229
|
+
|
|
230
|
+
test("non-IP hosts and junk return false (classifies addresses, not names)", () => {
|
|
231
|
+
assert.equal(isPrivateOrReservedIp("example.com"), false);
|
|
232
|
+
assert.equal(isPrivateOrReservedIp("localhost"), false);
|
|
233
|
+
assert.equal(isPrivateOrReservedIp(""), false);
|
|
234
|
+
assert.equal(isPrivateOrReservedIp(" "), false);
|
|
235
|
+
assert.equal(isPrivateOrReservedIp("999.999.999.999"), false);
|
|
236
|
+
assert.equal(isPrivateOrReservedIp("not-an-ip"), false);
|
|
237
|
+
assert.equal(isPrivateOrReservedIp(null), false);
|
|
238
|
+
assert.equal(isPrivateOrReservedIp(undefined), false);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
test("isPrivateOrReservedIp never throws on hostile input", () => {
|
|
242
|
+
for (const v of ["", "::::", "[", "]", "1.2.3", "::ffff:", "fe80::%", null, 42, {}]) {
|
|
243
|
+
assert.doesNotThrow(() => isPrivateOrReservedIp(/** @type {any} */ (v)));
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
/* ========================================================================= *
|
|
248
|
+
* maskText — free-text PII / secret masking
|
|
249
|
+
* ========================================================================= */
|
|
250
|
+
|
|
251
|
+
/** Assert a single secret class is fully masked and counted. */
|
|
252
|
+
function assertMasked(input, secret, label) {
|
|
253
|
+
const { text, count } = maskText(input);
|
|
254
|
+
assert.ok(!text.includes(secret), `${label} value leaked: ${text}`);
|
|
255
|
+
assert.ok(count >= 1, `${label} not counted: ${JSON.stringify({ text, count })}`);
|
|
256
|
+
assert.ok(
|
|
257
|
+
text.includes(`${REDACTED_LABEL_PREFIX}`),
|
|
258
|
+
`${label} placeholder missing: ${text}`
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
test("maskText redacts Anthropic keys (sk-ant-) without leaking sk- generic", () => {
|
|
263
|
+
const key = "sk-ant-api03-AbCdEf0123456789_GhIjKlMnOpQrStUvWx";
|
|
264
|
+
const { text, count } = maskText(`my key is ${key} ok`);
|
|
265
|
+
assert.ok(!text.includes(key), text);
|
|
266
|
+
assert.equal(count, 1);
|
|
267
|
+
assert.ok(text.includes("[REDACTED:ANTHROPIC_KEY]"), text);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("maskText redacts OpenAI-style sk- and sk-proj- keys", () => {
|
|
271
|
+
assertMasked("token sk-proj-0123456789ABCDEFabcdef0123 here", "sk-proj-0123456789ABCDEFabcdef0123", "API_KEY");
|
|
272
|
+
assertMasked("token sk-0123456789ABCDEFabcdef0123 here", "sk-0123456789ABCDEFabcdef0123", "API_KEY");
|
|
273
|
+
});
|
|
274
|
+
|
|
275
|
+
test("maskText redacts AWS access key ids (AKIA/ASIA)", () => {
|
|
276
|
+
assertMasked("creds AKIAIOSFODNN7EXAMPLE done", "AKIAIOSFODNN7EXAMPLE", "AWS_KEY");
|
|
277
|
+
assertMasked("creds ASIAIOSFODNN7EXAMPLE done", "ASIAIOSFODNN7EXAMPLE", "AWS_KEY");
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
test("maskText redacts GitHub tokens (ghp_ / github_pat_)", () => {
|
|
281
|
+
assertMasked(
|
|
282
|
+
"auth ghp_0123456789abcdefABCDEF0123456789abcdef tail",
|
|
283
|
+
"ghp_0123456789abcdefABCDEF0123456789abcdef",
|
|
284
|
+
"GITHUB_TOKEN"
|
|
285
|
+
);
|
|
286
|
+
assertMasked(
|
|
287
|
+
"auth github_pat_0123456789abcdefABCD_extra tail",
|
|
288
|
+
"github_pat_0123456789abcdefABCD_extra",
|
|
289
|
+
"GITHUB_TOKEN"
|
|
290
|
+
);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
test("maskText redacts bearer tokens", () => {
|
|
294
|
+
const { text, count } = maskText("Authorization: Bearer abc.DEF-123_xyz=+/ rest");
|
|
295
|
+
assert.ok(!text.includes("abc.DEF-123_xyz"), text);
|
|
296
|
+
assert.ok(count >= 1, `count=${count}`);
|
|
297
|
+
assert.ok(text.includes("[REDACTED:BEARER_TOKEN]"), text);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
test("maskText redacts JWTs", () => {
|
|
301
|
+
const jwt =
|
|
302
|
+
"eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIn0.dozjgNryP4J3jVmNHl0w5N_XgL0n3I9PlFUP0THsR8U";
|
|
303
|
+
const { text, count } = maskText(`session=${jwt};`);
|
|
304
|
+
assert.ok(!text.includes(jwt), text);
|
|
305
|
+
assert.ok(count >= 1);
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
test("maskText redacts PEM private key blocks", () => {
|
|
309
|
+
const pem =
|
|
310
|
+
"-----BEGIN RSA PRIVATE KEY-----\nMIIBVAIBADANBgkqh\nkiG9w0BAQEFA\n-----END RSA PRIVATE KEY-----";
|
|
311
|
+
const { text, count } = maskText(`key:\n${pem}\nend`);
|
|
312
|
+
assert.ok(!text.includes("MIIBVAIBADAN"), text);
|
|
313
|
+
assert.ok(count >= 1);
|
|
314
|
+
assert.ok(text.includes("[REDACTED:PRIVATE_KEY]"), text);
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
test("maskText redacts email addresses", () => {
|
|
318
|
+
assertMasked("contact alice.smith+tag@example.co.uk please", "alice.smith+tag@example.co.uk", "EMAIL");
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
test("maskText redacts phone numbers", () => {
|
|
322
|
+
const { text, count } = maskText("call me at +1 (415) 555-0132 tomorrow");
|
|
323
|
+
assert.ok(!text.includes("555-0132"), text);
|
|
324
|
+
assert.ok(count >= 1);
|
|
325
|
+
assert.ok(text.includes("[REDACTED:PHONE]"), text);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
test("maskText redacts long hex secrets", () => {
|
|
329
|
+
const hex = "deadbeefcafef00d0123456789abcdef0123456789abcdef"; // 48 hex chars
|
|
330
|
+
assertMasked(`hash=${hex} ok`, hex, "HEX_SECRET");
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("maskText redacts long base64-shaped secrets", () => {
|
|
334
|
+
const b64 = "QWxhZGRpbjpvcGVuIHNlc2FtZQ123456789abcDEF_xyz-987654321";
|
|
335
|
+
const { text, count } = maskText(`blob ${b64} end`);
|
|
336
|
+
assert.ok(!text.includes(b64), text);
|
|
337
|
+
assert.ok(count >= 1);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
test("maskText leaves benign prose untouched and counts zero", () => {
|
|
341
|
+
const benign =
|
|
342
|
+
"The quarterly report is due Friday. Page 2 covers revenue; total was 1234 units across 3 regions.";
|
|
343
|
+
const { text, count } = maskText(benign);
|
|
344
|
+
assert.equal(text, benign);
|
|
345
|
+
assert.equal(count, 0);
|
|
346
|
+
assert.ok(!text.includes(REDACTED_LABEL_PREFIX), text);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
test("maskText counts multiple distinct secrets in one string", () => {
|
|
350
|
+
const input =
|
|
351
|
+
"email bob@corp.com key sk-ant-api03-ZZZZZZZZ_aaaaaaaa11112222 and aws AKIAIOSFODNN7EXAMPLE";
|
|
352
|
+
const { text, count } = maskText(input);
|
|
353
|
+
assert.ok(!text.includes("bob@corp.com"), text);
|
|
354
|
+
assert.ok(!text.includes("AKIAIOSFODNN7EXAMPLE"), text);
|
|
355
|
+
assert.equal(count, 3, `expected 3 redactions, got ${count}: ${text}`);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
test("maskText never throws and coerces non-strings", () => {
|
|
359
|
+
assert.deepEqual(maskText(null), { text: "", count: 0 });
|
|
360
|
+
assert.deepEqual(maskText(undefined), { text: "", count: 0 });
|
|
361
|
+
assert.equal(maskText(12345).count, 0);
|
|
362
|
+
assert.doesNotThrow(() => maskText({}));
|
|
363
|
+
assert.deepEqual(maskText(""), { text: "", count: 0 });
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
test("maskText honors a custom placeholder factory", () => {
|
|
367
|
+
const { text } = maskText("mail me at x@y.com", {
|
|
368
|
+
placeholder: (label) => `<${label}>`,
|
|
369
|
+
});
|
|
370
|
+
assert.ok(text.includes("<EMAIL>"), text);
|
|
371
|
+
assert.ok(!text.includes("x@y.com"), text);
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
test("maskTextString returns only the cleaned string (masker-hook shape)", () => {
|
|
375
|
+
const out = maskTextString("token sk-0123456789ABCDEFabcdef0123 done");
|
|
376
|
+
assert.equal(typeof out, "string");
|
|
377
|
+
assert.ok(!out.includes("sk-0123456789ABCDEFabcdef0123"), out);
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
/* ========================================================================= *
|
|
381
|
+
* redactObject — deep masking
|
|
382
|
+
* ========================================================================= */
|
|
383
|
+
|
|
384
|
+
test("redactObject deep-masks nested string values, preserving structure", () => {
|
|
385
|
+
const input = {
|
|
386
|
+
user: "alice@example.com",
|
|
387
|
+
meta: {
|
|
388
|
+
note: "all good",
|
|
389
|
+
creds: ["AKIAIOSFODNN7EXAMPLE", "harmless"],
|
|
390
|
+
depth: { token: "sk-ant-api03-QQQQQQQQ_bbbbbbbb33334444" },
|
|
391
|
+
},
|
|
392
|
+
count: 7,
|
|
393
|
+
enabled: true,
|
|
394
|
+
nothing: null,
|
|
395
|
+
};
|
|
396
|
+
const { value, count } = redactObject(input);
|
|
397
|
+
|
|
398
|
+
assert.ok(!value.user.includes("alice@example.com"), value.user);
|
|
399
|
+
assert.equal(value.meta.note, "all good"); // benign untouched
|
|
400
|
+
assert.ok(!value.meta.creds[0].includes("AKIAIOSFODNN7EXAMPLE"), value.meta.creds[0]);
|
|
401
|
+
assert.equal(value.meta.creds[1], "harmless");
|
|
402
|
+
assert.ok(!value.meta.depth.token.includes("sk-ant-"), value.meta.depth.token);
|
|
403
|
+
// Non-strings pass through unchanged.
|
|
404
|
+
assert.equal(value.count, 7);
|
|
405
|
+
assert.equal(value.enabled, true);
|
|
406
|
+
assert.equal(value.nothing, null);
|
|
407
|
+
// Keys preserved.
|
|
408
|
+
assert.deepEqual(Object.keys(value).sort(), ["count", "enabled", "meta", "nothing", "user"]);
|
|
409
|
+
assert.equal(count, 3);
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
test("redactObject does not mutate the input", () => {
|
|
413
|
+
const input = { a: "ping me at z@z.com" };
|
|
414
|
+
const before = input.a;
|
|
415
|
+
redactObject(input);
|
|
416
|
+
assert.equal(input.a, before, "input was mutated");
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
test("redactObject masks keys' values only, not the keys themselves", () => {
|
|
420
|
+
const input = { "token-field": "sk-0123456789ABCDEFabcdef0123" };
|
|
421
|
+
const { value } = redactObject(input);
|
|
422
|
+
assert.ok(Object.prototype.hasOwnProperty.call(value, "token-field"), "key dropped");
|
|
423
|
+
assert.ok(!value["token-field"].includes("sk-0123456789"), value["token-field"]);
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test("redactObject handles cycles and never throws", () => {
|
|
427
|
+
const a = { name: "leak: secret@host.com" };
|
|
428
|
+
a.self = a; // cycle
|
|
429
|
+
let result;
|
|
430
|
+
assert.doesNotThrow(() => {
|
|
431
|
+
result = redactObject(a);
|
|
432
|
+
});
|
|
433
|
+
assert.ok(!result.value.name.includes("secret@host.com"), result.value.name);
|
|
434
|
+
assert.equal(result.count, 1);
|
|
435
|
+
});
|
|
436
|
+
|
|
437
|
+
test("redactObject passes scalars through untouched", () => {
|
|
438
|
+
assert.deepEqual(redactObject(42), { value: 42, count: 0 });
|
|
439
|
+
assert.deepEqual(redactObject(true), { value: true, count: 0 });
|
|
440
|
+
assert.deepEqual(redactObject(null), { value: null, count: 0 });
|
|
441
|
+
});
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secret-equal.mjs — constant-time comparison of secrets (tokens, signatures,
|
|
3
|
+
* HMACs, shared keys) without leaking length or content through timing.
|
|
4
|
+
*
|
|
5
|
+
* A naive `a === b` short-circuits on the first differing byte and on length
|
|
6
|
+
* mismatch, which gives an attacker a timing oracle. node:crypto's
|
|
7
|
+
* timingSafeEqual fixes the byte-by-byte leak but THROWS when the two buffers
|
|
8
|
+
* differ in length — so calling it directly re-introduces an early-return path.
|
|
9
|
+
*
|
|
10
|
+
* safeEqualSecret() closes both holes: it always runs the constant-time
|
|
11
|
+
* compare over equal-length buffers (padded to the longer of the two) and only
|
|
12
|
+
* afterwards folds in the original length equality, so a length mismatch costs
|
|
13
|
+
* the same as a content mismatch. Non-string inputs return false instead of
|
|
14
|
+
* throwing.
|
|
15
|
+
*
|
|
16
|
+
* Ported and adapted from OpenClaw (src/security/secret-equal.ts) for the
|
|
17
|
+
* maestro autonomy model: any place that checks an inbound webhook signature,
|
|
18
|
+
* relay token, or shared secret should compare it with safeEqualSecret().
|
|
19
|
+
*
|
|
20
|
+
* Pure function; the only dependency is node:crypto.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { timingSafeEqual } from "node:crypto";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Copy `bytes` into a fresh buffer of exactly `length` bytes, zero-padding the
|
|
27
|
+
* tail when the input is shorter. Returns the input untouched when it already
|
|
28
|
+
* matches `length`.
|
|
29
|
+
* @param {Buffer} bytes
|
|
30
|
+
* @param {number} length
|
|
31
|
+
* @returns {Buffer}
|
|
32
|
+
*/
|
|
33
|
+
function padSecretBytes(bytes, length) {
|
|
34
|
+
if (bytes.length === length) {
|
|
35
|
+
return bytes;
|
|
36
|
+
}
|
|
37
|
+
const padded = Buffer.alloc(length);
|
|
38
|
+
bytes.copy(padded);
|
|
39
|
+
return padded;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Timing-safe secret comparison.
|
|
44
|
+
*
|
|
45
|
+
* Returns true iff `provided` and `expected` are both strings with identical
|
|
46
|
+
* UTF-8 bytes. The comparison runs in time independent of where the values
|
|
47
|
+
* differ and of their lengths, and never throws on length mismatch or
|
|
48
|
+
* non-string input.
|
|
49
|
+
*
|
|
50
|
+
* @param {string|undefined|null} provided
|
|
51
|
+
* @param {string|undefined|null} expected
|
|
52
|
+
* @returns {boolean}
|
|
53
|
+
*/
|
|
54
|
+
export function safeEqualSecret(provided, expected) {
|
|
55
|
+
if (typeof provided !== "string" || typeof expected !== "string") {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const providedBytes = Buffer.from(provided, "utf8");
|
|
59
|
+
const expectedBytes = Buffer.from(expected, "utf8");
|
|
60
|
+
const byteLength = Math.max(providedBytes.length, expectedBytes.length);
|
|
61
|
+
if (byteLength === 0) {
|
|
62
|
+
// Both strings are empty: equal, and there is nothing to compare.
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
// Always perform the constant-time compare, then fold in the length check so
|
|
66
|
+
// a length mismatch is indistinguishable (timing-wise) from a content one.
|
|
67
|
+
return (
|
|
68
|
+
timingSafeEqual(
|
|
69
|
+
padSecretBytes(providedBytes, byteLength),
|
|
70
|
+
padSecretBytes(expectedBytes, byteLength),
|
|
71
|
+
) && providedBytes.length === expectedBytes.length
|
|
72
|
+
);
|
|
73
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* secret-equal.test.mjs — node:test coverage for safeEqualSecret().
|
|
3
|
+
*
|
|
4
|
+
* Verifies: equal strings, differing strings, length mismatch (no throw),
|
|
5
|
+
* non-string inputs (no throw), and empty-string handling.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import test from "node:test";
|
|
9
|
+
import assert from "node:assert/strict";
|
|
10
|
+
|
|
11
|
+
import { safeEqualSecret } from "./secret-equal.mjs";
|
|
12
|
+
|
|
13
|
+
test("equal strings return true", () => {
|
|
14
|
+
assert.equal(safeEqualSecret("s3cr3t-token", "s3cr3t-token"), true);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("different strings of same length return false", () => {
|
|
18
|
+
assert.equal(safeEqualSecret("abcdef", "abcxef"), false);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("different lengths return false without throwing", () => {
|
|
22
|
+
assert.doesNotThrow(() => safeEqualSecret("short", "shorter-secret"));
|
|
23
|
+
assert.equal(safeEqualSecret("short", "shorter-secret"), false);
|
|
24
|
+
assert.equal(safeEqualSecret("shorter-secret", "short"), false);
|
|
25
|
+
// A string that is a strict prefix of the other must not compare equal.
|
|
26
|
+
assert.equal(safeEqualSecret("abc", "abcd"), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test("non-string inputs return false without throwing", () => {
|
|
30
|
+
assert.doesNotThrow(() => safeEqualSecret(undefined, "x"));
|
|
31
|
+
assert.equal(safeEqualSecret(undefined, "x"), false);
|
|
32
|
+
assert.equal(safeEqualSecret("x", undefined), false);
|
|
33
|
+
assert.equal(safeEqualSecret(null, "x"), false);
|
|
34
|
+
assert.equal(safeEqualSecret("x", null), false);
|
|
35
|
+
assert.equal(safeEqualSecret(123, "123"), false);
|
|
36
|
+
assert.equal(safeEqualSecret("123", 123), false);
|
|
37
|
+
assert.equal(safeEqualSecret(Buffer.from("x"), "x"), false);
|
|
38
|
+
assert.equal(safeEqualSecret({}, {}), false);
|
|
39
|
+
assert.equal(safeEqualSecret(undefined, undefined), false);
|
|
40
|
+
assert.equal(safeEqualSecret(null, null), false);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test("two empty strings are equal", () => {
|
|
44
|
+
assert.equal(safeEqualSecret("", ""), true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("empty string vs non-empty string returns false", () => {
|
|
48
|
+
assert.equal(safeEqualSecret("", "x"), false);
|
|
49
|
+
assert.equal(safeEqualSecret("x", ""), false);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test("multibyte/unicode strings compare on UTF-8 bytes", () => {
|
|
53
|
+
assert.equal(safeEqualSecret("séçret-🔑", "séçret-🔑"), true);
|
|
54
|
+
assert.equal(safeEqualSecret("séçret-🔑", "secret-key"), false);
|
|
55
|
+
});
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Maestro — Session permission scoping (audit H1).
|
|
3
|
+
*
|
|
4
|
+
* Every autonomous sub-session the daemon spawns (cadence ticks, and — by
|
|
5
|
+
* extension — reactive dispatcher/responder/classifier paths) historically ran
|
|
6
|
+
* `claude --print` with `--dangerously-skip-permissions`. That means an
|
|
7
|
+
* inbound message containing a prompt-injection payload can drive ARBITRARY
|
|
8
|
+
* tool use with no human in the loop, which on an unattended 24/7 Mac mini is
|
|
9
|
+
* the single highest-impact realistic compromise path (audit/AUDIT-REPORT.md
|
|
10
|
+
* finding H1).
|
|
11
|
+
*
|
|
12
|
+
* This module centralizes the decision of WHICH permission CLI args a spawned
|
|
13
|
+
* session gets, so the dangerous default becomes explicit and operators get a
|
|
14
|
+
* real gate. It is deliberately OPT-IN: with no env flag set, behaviour is
|
|
15
|
+
* BYTE-FOR-BYTE UNCHANGED (still `--dangerously-skip-permissions`). Operators
|
|
16
|
+
* who want the scoped behaviour set `MAESTRO_SCOPED_PERMISSIONS=1`.
|
|
17
|
+
*
|
|
18
|
+
* Pure function; zero dependencies beyond the cadence registry. Returns ONLY
|
|
19
|
+
* the permission flags — callers keep appending `daemonClaudeArgs()`
|
|
20
|
+
* (`--strict-mcp-config` / `--bare`) exactly as before, so `--strict-mcp-config`
|
|
21
|
+
* is preserved in BOTH modes.
|
|
22
|
+
*
|
|
23
|
+
* Caller sites (today): scripts/daemon/cadence-consumer.mjs#realSpawnSession.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { getCadenceDef } from "../scripts/daemon/cadence-handlers.mjs";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Conservative fallback allowlist used in scoped mode when a cadence has no
|
|
30
|
+
* explicit `allowedTools` field in the registry. Deliberately READ-ONLY /
|
|
31
|
+
* search-only: an unknown or untrusted cadence should never get a write,
|
|
32
|
+
* shell, send, or control-plane tool by default. Tightening here is safe;
|
|
33
|
+
* widening should be a per-cadence opt-in via the registry `allowedTools`.
|
|
34
|
+
*
|
|
35
|
+
* Frozen so a caller can't accidentally mutate the shared default.
|
|
36
|
+
* @type {ReadonlyArray<string>}
|
|
37
|
+
*/
|
|
38
|
+
export const CONSERVATIVE_DEFAULT_ALLOWLIST = Object.freeze([
|
|
39
|
+
"Read",
|
|
40
|
+
"Grep",
|
|
41
|
+
"Glob",
|
|
42
|
+
"LS",
|
|
43
|
+
]);
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the effective allowlist for a cadence in scoped mode. Prefers an
|
|
47
|
+
* explicit `allowedTools` array on the cadence registry entry; otherwise falls
|
|
48
|
+
* back to {@link CONSERVATIVE_DEFAULT_ALLOWLIST}. A registry entry whose
|
|
49
|
+
* `allowedTools` is absent, non-array, or empty also falls back, so a
|
|
50
|
+
* mis-specified entry fails CLOSED (to the conservative default) rather than
|
|
51
|
+
* open.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} [cadence] cadence name (registry key)
|
|
54
|
+
* @returns {string[]} non-empty list of allowed tool names
|
|
55
|
+
*/
|
|
56
|
+
function resolveAllowlist(cadence) {
|
|
57
|
+
const def = cadence ? getCadenceDef(cadence) : null;
|
|
58
|
+
const allow = def && def.allowedTools;
|
|
59
|
+
if (Array.isArray(allow) && allow.length > 0) {
|
|
60
|
+
// Drop any non-string / blank entries defensively.
|
|
61
|
+
const cleaned = allow.filter((t) => typeof t === "string" && t.trim());
|
|
62
|
+
if (cleaned.length > 0) return cleaned;
|
|
63
|
+
}
|
|
64
|
+
return [...CONSERVATIVE_DEFAULT_ALLOWLIST];
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Build the `claude` CLI permission args for a spawned sub-session.
|
|
69
|
+
*
|
|
70
|
+
* Behaviour is gated on the `MAESTRO_SCOPED_PERMISSIONS` env var:
|
|
71
|
+
*
|
|
72
|
+
* - UNSET (or any value other than "1") — back-compat / default. Returns
|
|
73
|
+
* `["--dangerously-skip-permissions"]`, identical to the pre-H1 hardcoded
|
|
74
|
+
* literal. NOTHING changes for operators who have not opted in.
|
|
75
|
+
*
|
|
76
|
+
* - "1" — scoped mode. Returns `["--allowedTools", "<comma-list>"]`, where
|
|
77
|
+
* the list is the cadence's registry `allowedTools` field if present, else
|
|
78
|
+
* {@link CONSERVATIVE_DEFAULT_ALLOWLIST}. `--strict-mcp-config` is NOT
|
|
79
|
+
* added here — it stays single-sourced in `daemonClaudeArgs()` which the
|
|
80
|
+
* caller appends.
|
|
81
|
+
*
|
|
82
|
+
* The flag is read at call time (not import time) so an operator can flip it
|
|
83
|
+
* via `.env` and have a long-lived daemon pick it up on the next spawn without
|
|
84
|
+
* a code change.
|
|
85
|
+
*
|
|
86
|
+
* @param {Object} [opts]
|
|
87
|
+
* @param {string} [opts.cadence] cadence name; used to look up a per-cadence allowlist in scoped mode.
|
|
88
|
+
* @param {string} [opts.mode] cadence mode ("inline"|"guarded"|"escalate"); accepted for forward-compat (future per-mode tightening), currently unused.
|
|
89
|
+
* @returns {string[]} permission CLI args to splice into the `claude --print` argv.
|
|
90
|
+
*/
|
|
91
|
+
export function sessionPermissionArgs({ cadence, mode } = {}) {
|
|
92
|
+
// `mode` is reserved for future per-mode tightening; reference it so linters
|
|
93
|
+
// don't flag it as unused while keeping the signature stable for callers.
|
|
94
|
+
void mode;
|
|
95
|
+
if (process.env.MAESTRO_SCOPED_PERMISSIONS !== "1") {
|
|
96
|
+
// Default / back-compat: UNCHANGED behaviour.
|
|
97
|
+
return ["--dangerously-skip-permissions"];
|
|
98
|
+
}
|
|
99
|
+
const allow = resolveAllowlist(cadence);
|
|
100
|
+
return ["--allowedTools", allow.join(",")];
|
|
101
|
+
}
|