@geraldmaron/construct 1.4.1 → 1.5.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/.env.example +36 -0
- package/README.md +41 -7
- package/bin/construct +1029 -65
- package/bin/construct-postinstall.mjs +27 -2
- package/config/tag-vocabulary.json +264 -0
- package/examples/distribution/sources/deck-one-pager.md +1 -1
- package/lib/acp/server.mjs +21 -7
- package/lib/adapters-sync.mjs +2 -1
- package/lib/audit-trail.mjs +185 -2
- package/lib/beads/auto-close.mjs +2 -1
- package/lib/beads/drift.mjs +2 -1
- package/lib/bridges/copilot-proxy.mjs +20 -5
- package/lib/certification/skill-inventory.mjs +10 -1
- package/lib/cli/approvals.mjs +147 -0
- package/lib/cli-commands.mjs +105 -14
- package/lib/comment-lint.mjs +127 -8
- package/lib/config/schema.mjs +7 -30
- package/lib/config/source-target-registry.mjs +70 -0
- package/lib/config/source-targets.mjs +179 -205
- package/lib/contracts/coverage.mjs +77 -0
- package/lib/contracts/validate.mjs +102 -13
- package/lib/contracts/violation-log.mjs +50 -4
- package/lib/db/migrate.mjs +69 -0
- package/lib/db/migrations/001_orchestration_runs.sql +9 -0
- package/lib/db/migrations/002_queue_provider.sql +50 -0
- package/lib/db/migrations/003_worker_registry.sql +17 -0
- package/lib/db/migrations/004_trace_events.sql +16 -0
- package/lib/db/migrations/005_shared_memory.sql +15 -0
- package/lib/db/migrations/006_orchestration_runs_tenant.sql +5 -0
- package/lib/decisions/enforced-baseline.json +0 -2
- package/lib/decisions/registry.mjs +3 -2
- package/lib/deployment/parity-contract.mjs +1 -1
- package/lib/deployment-mode.mjs +78 -2
- package/lib/diagram-export.mjs +10 -1
- package/lib/distill.mjs +5 -2
- package/lib/docs-verify.mjs +2 -1
- package/lib/doctor/cli.mjs +1 -0
- package/lib/doctor/command-on-path.mjs +24 -0
- package/lib/doctor/diagnosis.mjs +109 -0
- package/lib/doctor/embedding-health.mjs +50 -0
- package/lib/doctor/engine-health.mjs +93 -0
- package/lib/doctor/graph-validate.mjs +47 -0
- package/lib/doctor/index.mjs +23 -4
- package/lib/doctor/sidecar-providers.mjs +56 -0
- package/lib/doctor/watchers/consistency.mjs +93 -1
- package/lib/doctor/watchers/cx-budget.mjs +1 -1
- package/lib/doctor/watchers/graph-staleness.mjs +1 -1
- package/lib/doctor/watchers/mcp-protocol.mjs +17 -0
- package/lib/doctor/watchers/oracle-liveness.mjs +92 -0
- package/lib/doctor/watchers/orchestration-runs.mjs +108 -0
- package/lib/doctor/watchers/provider-breaker.mjs +78 -0
- package/lib/document-export.mjs +52 -27
- package/lib/document-extract/docling-client.mjs +9 -5
- package/lib/document-extract/docling-sidecar.py +30 -0
- package/lib/document-extract.mjs +1 -1
- package/lib/document-ingest.mjs +9 -0
- package/lib/embed/approval-queue.mjs +184 -88
- package/lib/embed/authority-guard.mjs +65 -2
- package/lib/embed/capability-jobs.mjs +365 -0
- package/lib/embed/capability-lifecycle.mjs +219 -0
- package/lib/embed/capability-loader.mjs +303 -0
- package/lib/embed/capability-runtime.mjs +58 -0
- package/lib/embed/cli.mjs +185 -8
- package/lib/embed/config.mjs +4 -0
- package/lib/embed/daemon.mjs +125 -6
- package/lib/embed/demand-fetch.mjs +190 -54
- package/lib/embed/inbox.mjs +12 -10
- package/lib/embed/presets/ops-triage.mjs +236 -0
- package/lib/embed/presets/pm-feedback.mjs +341 -0
- package/lib/embed/presets/tpm.mjs +401 -0
- package/lib/embed/providers/jira.mjs +72 -1
- package/lib/embed/providers/registry.mjs +140 -33
- package/lib/embed/worker.mjs +5 -0
- package/lib/embedded-contract/capability.mjs +4 -0
- package/lib/embedded-contract/execution.mjs +1 -1
- package/lib/embedded-contract/model-resolve.mjs +53 -3
- package/lib/embedded-contract/workflow-defs.mjs +48 -73
- package/lib/embedded-contract/workflow-invoke.mjs +144 -4
- package/lib/embedded-contract/workflows/architecture-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/data-structure.manifest.json +14 -0
- package/lib/embedded-contract/workflows/evidence-ingest.manifest.json +14 -0
- package/lib/embedded-contract/workflows/memo-draft.manifest.json +14 -0
- package/lib/embedded-contract/workflows/operations-triage.manifest.json +18 -0
- package/lib/embedded-contract/workflows/operations.manifest.json +18 -0
- package/lib/embedded-contract/workflows/pm-feedback.manifest.json +18 -0
- package/lib/embedded-contract/workflows/prd-draft.manifest.json +15 -0
- package/lib/embedded-contract/workflows/proposal-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/research-synthesis.manifest.json +14 -0
- package/lib/embedded-contract/workflows/risk-review.manifest.json +15 -0
- package/lib/embedded-contract/workflows/structure-notes.manifest.json +14 -0
- package/lib/embedded-contract/workflows/transcript-process.manifest.json +14 -0
- package/lib/embedded-contract/workflows/triage.manifest.json +14 -0
- package/lib/env-config.mjs +60 -11
- package/lib/export-validate.mjs +34 -2
- package/lib/extensions/index.mjs +27 -0
- package/lib/extensions/loader.mjs +120 -0
- package/lib/extensions/manifest-schema.mjs +141 -0
- package/lib/extensions/manifests/anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-confluence.manifest.json +12 -0
- package/lib/extensions/manifests/atlassian-jira.manifest.json +53 -0
- package/lib/extensions/manifests/directory.manifest.json +12 -0
- package/lib/extensions/manifests/docling.manifest.json +37 -0
- package/lib/extensions/manifests/echo.manifest.json +8 -0
- package/lib/extensions/manifests/feedback.manifest.json +12 -0
- package/lib/extensions/manifests/github-copilot.manifest.json +12 -0
- package/lib/extensions/manifests/github.manifest.json +52 -0
- package/lib/extensions/manifests/linear.manifest.json +50 -0
- package/lib/extensions/manifests/local.manifest.json +12 -0
- package/lib/extensions/manifests/ollama.manifest.json +12 -0
- package/lib/extensions/manifests/openai.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-anthropic.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-deepseek.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-google.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-llama.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter-qwen.manifest.json +12 -0
- package/lib/extensions/manifests/openrouter.manifest.json +12 -0
- package/lib/extensions/manifests/postgres.manifest.json +12 -0
- package/lib/extensions/manifests/salesforce.manifest.json +12 -0
- package/lib/extensions/manifests/slack.manifest.json +58 -0
- package/lib/extensions/manifests/whisper.manifest.json +36 -0
- package/lib/extensions/validate.mjs +175 -0
- package/lib/features.mjs +13 -9
- package/lib/flows/checkpoint.mjs +184 -0
- package/lib/flows/constants.mjs +27 -0
- package/lib/flows/define.mjs +146 -0
- package/lib/flows/engine.mjs +249 -0
- package/lib/flows/errors.mjs +19 -0
- package/lib/flows/index.mjs +27 -0
- package/lib/flows/joins.mjs +18 -0
- package/lib/flows/schema.mjs +90 -0
- package/lib/flows/state.mjs +31 -0
- package/lib/frameworks/loader.mjs +99 -0
- package/lib/frameworks/schema.mjs +157 -0
- package/lib/graph/build-from-corpus.mjs +67 -0
- package/lib/graph/build-from-embed.mjs +82 -0
- package/lib/graph/build-from-registry.mjs +164 -3
- package/lib/graph/cli.mjs +456 -12
- package/lib/graph/gap-queries.mjs +156 -0
- package/lib/graph/gaps.mjs +41 -0
- package/lib/graph/impacted.mjs +129 -0
- package/lib/graph/runtime-evidence.mjs +177 -0
- package/lib/graph/security-coverage.mjs +113 -0
- package/lib/graph/staleness.mjs +241 -10
- package/lib/graph/store.mjs +24 -7
- package/lib/graph/validate.mjs +161 -0
- package/lib/headhunt.mjs +9 -8
- package/lib/health-check.mjs +15 -7
- package/lib/hook-health.mjs +1 -1
- package/lib/hooks/agent-tracker.mjs +10 -4
- package/lib/hooks/edit-guard.mjs +3 -3
- package/lib/hooks/graph-impact-advisory.mjs +2 -2
- package/lib/hooks/guard-bash.mjs +41 -15
- package/lib/hooks/mcp-health-check.mjs +11 -4
- package/lib/hooks/model-fallback.mjs +50 -6
- package/lib/hooks/orchestration-dispatch-guard.mjs +14 -3
- package/lib/hooks/pre-compact.mjs +181 -173
- package/lib/hooks/rule-verifier.mjs +2 -1
- package/lib/hooks/session-reflect.mjs +2 -1
- package/lib/hooks/session-start.mjs +3 -3
- package/lib/host/readiness.mjs +109 -0
- package/lib/host-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +29 -8
- package/lib/identity.mjs +92 -0
- package/lib/ingest/degraded-extract.mjs +96 -0
- package/lib/ingest/docling-remote.mjs +2 -1
- package/lib/ingest/provider-extract.mjs +7 -19
- package/lib/ingest/sidecar-providers.mjs +196 -0
- package/lib/ingest-tooling.mjs +11 -5
- package/lib/init-unified.mjs +55 -38
- package/lib/init-update.mjs +2 -1
- package/lib/install/legacy-global-cleanup.mjs +25 -0
- package/lib/install/stage-project.mjs +41 -4
- package/lib/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- package/lib/intake/prepare.mjs +2 -0
- package/lib/intake/queue-registry.mjs +50 -0
- package/lib/intake/queue.mjs +133 -17
- package/lib/intake/session-prelude.mjs +57 -8
- package/lib/integrations/intake-integrations.mjs +61 -111
- package/lib/intent-classifier.mjs +43 -5
- package/lib/libreoffice-export.mjs +8 -8
- package/lib/logging/rotate.mjs +5 -4
- package/lib/mcp/broker.mjs +332 -17
- package/lib/mcp/denied-store.mjs +95 -0
- package/lib/mcp/destructive-approval.mjs +57 -0
- package/lib/mcp/destructive-gate.mjs +30 -0
- package/lib/mcp/dispatch-envelope.mjs +199 -0
- package/lib/mcp/memory-bridge.mjs +2 -1
- package/lib/mcp/server.mjs +185 -1268
- package/lib/mcp/tool-definitions-memory.mjs +376 -0
- package/lib/mcp/tool-definitions-project.mjs +271 -0
- package/lib/mcp/tool-definitions-skills.mjs +311 -0
- package/lib/mcp/tool-definitions-workflow.mjs +398 -0
- package/lib/mcp/tool-definitions.mjs +25 -0
- package/lib/mcp/tool-rate-limit.mjs +47 -0
- package/lib/mcp/tool-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +95 -0
- package/lib/mcp/tool-surface-parity.mjs +60 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +197 -19
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- package/lib/mcp/tools/project.mjs +25 -8
- package/lib/mcp/tools/provider-write.mjs +187 -0
- package/lib/mcp/tools/scope.mjs +0 -3
- package/lib/mcp/tools/skills.mjs +1 -1
- package/lib/mcp/tools/storage.mjs +2 -2
- package/lib/mcp/tools/web-search-governance.mjs +96 -0
- package/lib/mcp/tools/web-search.mjs +5 -76
- package/lib/mcp/transport/auth.mjs +238 -0
- package/lib/mcp/transport/http.mjs +136 -0
- package/lib/mcp/transport/mode.mjs +31 -0
- package/lib/mcp/transport/stdio.mjs +22 -0
- package/lib/mcp-catalog.json +4 -4
- package/lib/mcp-manager.mjs +34 -23
- package/lib/mcp-platform-config.mjs +53 -20
- package/lib/mode-capabilities.mjs +109 -0
- package/lib/model-router.mjs +42 -9
- package/lib/models/catalog.mjs +40 -1
- package/lib/net-guard.mjs +247 -0
- package/lib/observation-store.mjs +6 -2
- package/lib/ollama/provision-context.mjs +2 -1
- package/lib/opencode-config.mjs +22 -3
- package/lib/opencode-runtime-plugin.mjs +120 -2
- package/lib/oracle/actions.mjs +204 -10
- package/lib/oracle/cli.mjs +24 -3
- package/lib/oracle/daemon-entry.mjs +6 -0
- package/lib/oracle/dispatch.mjs +2 -1
- package/lib/oracle/execute.mjs +2 -2
- package/lib/oracle/gaps.mjs +3 -3
- package/lib/oracle/heartbeat.mjs +53 -0
- package/lib/oracle/read-model.mjs +69 -13
- package/lib/oracle/reconcile.mjs +3 -46
- package/lib/oracle/routing.mjs +37 -31
- package/lib/oracle/synthesize.mjs +1 -1
- package/lib/orchestration/classification.mjs +434 -0
- package/lib/orchestration/delegation-flow.mjs +132 -0
- package/lib/orchestration/flow-selection.mjs +418 -0
- package/lib/orchestration/gates.mjs +244 -0
- package/lib/orchestration/host-sampling.mjs +121 -0
- package/lib/orchestration/policy-constants.mjs +48 -0
- package/lib/orchestration/provider-outcome.mjs +197 -0
- package/lib/orchestration/readiness.mjs +114 -13
- package/lib/orchestration/run-store-postgres.mjs +32 -26
- package/lib/orchestration/run-store-sqlite.mjs +8 -14
- package/lib/orchestration/run-store.mjs +25 -12
- package/lib/orchestration/runtime.mjs +508 -62
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +60 -0
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +763 -80
- package/lib/orchestration-policy.mjs +67 -1111
- package/lib/output-quality.mjs +61 -2
- package/lib/packs/cli.mjs +144 -0
- package/lib/packs/core-pack.mjs +112 -0
- package/lib/packs/enablement.mjs +187 -0
- package/lib/packs/index.mjs +23 -0
- package/lib/packs/loader.mjs +176 -0
- package/lib/packs/manifest-schema.mjs +58 -0
- package/lib/packs/prompts.mjs +120 -0
- package/lib/packs/validate.mjs +208 -0
- package/lib/path-policy.mjs +56 -0
- package/lib/plugin-registry.mjs +4 -5
- package/lib/policy/audit-gate.mjs +42 -0
- package/lib/policy/consumption-budget.mjs +149 -0
- package/lib/policy/engine.mjs +81 -6
- package/lib/policy/role-authority.mjs +89 -0
- package/lib/prompt-composer.js +19 -3
- package/lib/providers/contract/adapters/confluence/governed-write.mjs +215 -0
- package/lib/providers/contract/adapters/confluence/manifest.json +17 -0
- package/lib/providers/contract/adapters/confluence/transport.mjs +135 -0
- package/lib/providers/contract/adapters/github/governed-write.mjs +121 -0
- package/lib/providers/contract/adapters/github/index.mjs +38 -3
- package/lib/providers/contract/adapters/jira/adf.mjs +151 -0
- package/lib/providers/contract/adapters/jira/createmeta.mjs +143 -0
- package/lib/providers/contract/adapters/jira/governed-write.mjs +182 -0
- package/lib/providers/contract/adapters/jira/manifest.json +17 -0
- package/lib/providers/contract/adapters/jira/transport.mjs +119 -0
- package/lib/providers/contract.mjs +207 -0
- package/lib/providers/credential-bootstrap.mjs +7 -4
- package/lib/providers/credential-sources.mjs +14 -2
- package/lib/providers/directory/index.mjs +261 -0
- package/lib/providers/feedback/index.mjs +392 -0
- package/lib/providers/filter-audit.mjs +68 -0
- package/lib/providers/filter-schema.mjs +54 -0
- package/lib/providers/github/index.mjs +31 -0
- package/lib/providers/instance-config.mjs +215 -0
- package/lib/providers/op-locate.mjs +96 -0
- package/lib/providers/op-run.mjs +64 -9
- package/lib/providers/registry.mjs +26 -3
- package/lib/providers/secret-audit-wiring.mjs +41 -18
- package/lib/providers/secret-resolver.mjs +263 -31
- package/lib/publish-template.mjs +6 -3
- package/lib/publish-tooling.mjs +4 -0
- package/lib/publish.mjs +32 -4
- package/lib/queue/pg-queue.mjs +395 -0
- package/lib/reflect.mjs +2 -1
- package/lib/registry/assemble.mjs +28 -2
- package/lib/registry/catalog.mjs +6 -0
- package/lib/registry/consolidation.mjs +8 -1
- package/lib/registry/custom-scaffold.mjs +200 -0
- package/lib/registry/custom-schema.mjs +137 -0
- package/lib/registry/loader.mjs +15 -4
- package/lib/registry/manifests/format-engines.default.json +15 -0
- package/lib/registry/manifests/surface-map.default.json +46 -0
- package/lib/registry/retired-paths.mjs +1 -0
- package/lib/registry/surface-map.mjs +100 -50
- package/lib/registry/validate.mjs +3 -3
- package/lib/render-visual-check.mjs +240 -0
- package/lib/resources/budget.mjs +40 -17
- package/lib/roles/flavor-bindings.mjs +36 -14
- package/lib/roles/gateway.mjs +15 -8
- package/lib/roots.mjs +107 -0
- package/lib/runtime/uv-bootstrap.mjs +32 -6
- package/lib/runtime/whisper-bootstrap.mjs +11 -3
- package/lib/runtime-env.mjs +5 -2
- package/lib/sandbox.mjs +1 -1
- package/lib/scheduler/index.mjs +8 -20
- package/lib/schema-infer.mjs +31 -2
- package/lib/scopes/lifecycle.mjs +29 -25
- package/lib/scopes/loader.mjs +49 -12
- package/lib/scopes/teams.mjs +1 -1
- package/lib/security/ingest-boundary.mjs +80 -0
- package/lib/security/recall-wrapper.mjs +99 -0
- package/lib/security/trust.mjs +132 -0
- package/lib/service-manager.mjs +93 -24
- package/lib/setup.mjs +41 -23
- package/lib/skills-apply.mjs +4 -2
- package/lib/specialists/postconditions.mjs +2 -2
- package/lib/state-root.mjs +147 -0
- package/lib/status.mjs +597 -6
- package/lib/storage/admin.mjs +84 -7
- package/lib/storage/backend-registry.mjs +73 -0
- package/lib/storage/backend.mjs +63 -9
- package/lib/storage/embeddings-openai.mjs +12 -2
- package/lib/storage/hybrid-query.mjs +11 -3
- package/lib/storage/retrieval-hardening.mjs +384 -0
- package/lib/storage/shared-memory.mjs +158 -0
- package/lib/storage/vector-client.mjs +69 -2
- package/lib/team/health.mjs +72 -0
- package/lib/telemetry/backends/local.mjs +9 -3
- package/lib/telemetry/client.mjs +3 -7
- package/lib/template-registry.mjs +10 -12
- package/lib/tenant/context.mjs +82 -0
- package/lib/tenant/isolation.mjs +129 -0
- package/lib/test-corpus-inventory.mjs +103 -2
- package/lib/uninstall/uninstall.mjs +78 -12
- package/lib/validator.mjs +4 -6
- package/lib/worker/entrypoint.mjs +2 -1
- package/lib/worker/run.mjs +2 -2
- package/lib/worker/trace.mjs +5 -11
- package/lib/workflow-state.mjs +52 -8
- package/lib/workflows/liveness.mjs +203 -0
- package/lib/workflows/loader.mjs +177 -0
- package/lib/workflows/manifest-schema.mjs +71 -0
- package/lib/workflows/surface-parity.mjs +118 -0
- package/lib/workflows/validate.mjs +127 -0
- package/lib/writes/control-plane.mjs +140 -0
- package/lib/writes/envelope.mjs +206 -0
- package/lib/writes/sent-log.mjs +86 -0
- package/lib/writes/write-intent.mjs +109 -0
- package/package.json +20 -7
- package/personas/construct.md +12 -3
- package/registry/agent-manifest.json +117 -0
- package/registry/capabilities.json +1987 -0
- package/rules/common/comments.md +1 -0
- package/schemas/brand-voice.schema.json +24 -0
- package/schemas/capability-registry.schema.json +72 -0
- package/schemas/certification-run.schema.json +130 -0
- package/schemas/demo-recording.schema.json +46 -0
- package/schemas/eval-dataset.schema.json +79 -0
- package/schemas/execution-capability-profile.schema.json +46 -0
- package/schemas/execution-policy.schema.json +114 -0
- package/schemas/improvement-proposal.schema.json +65 -0
- package/schemas/mcp-tool-output.schema.json +61 -0
- package/schemas/platform-capabilities.schema.json +83 -0
- package/schemas/project-config.schema.json +215 -0
- package/schemas/project-demo.schema.json +60 -0
- package/schemas/provider-behavior-matrix.schema.json +91 -0
- package/schemas/scope.schema.json +197 -0
- package/schemas/specialist-trace.schema.json +107 -0
- package/schemas/team.schema.json +99 -0
- package/schemas/unified-registry.schema.json +546 -0
- package/scripts/sync-specialists.mjs +336 -106
- package/skills/docs/adr-workflow.md +1 -1
- package/skills/docs/codebase-research-workflow.md +3 -3
- package/skills/docs/prd-workflow.md +5 -6
- package/skills/docs/runbook-workflow.md +2 -2
- package/skills/docs/user-research-workflow.md +3 -3
- package/skills/operating/fleet-health-routing.md +77 -0
- package/skills/roles/ai-engineer.md +1 -1
- package/skills/roles/architect.md +0 -1
- package/skills/roles/business-strategist.md +1 -1
- package/skills/roles/data-analyst.telemetry.md +1 -1
- package/skills/roles/data-engineer.md +1 -1
- package/skills/roles/data-engineer.pipeline.md +1 -1
- package/skills/roles/data-engineer.vector-retrieval.md +1 -2
- package/skills/roles/data-engineer.warehouse.md +1 -1
- package/skills/roles/designer.accessibility.md +1 -1
- package/skills/roles/designer.md +0 -1
- package/skills/roles/devil-advocate.md +1 -1
- package/skills/roles/docs-keeper.md +1 -1
- package/skills/roles/evaluator.md +1 -1
- package/skills/roles/explorer.md +1 -1
- package/skills/roles/platform-engineer.md +1 -1
- package/skills/roles/qa.ai-eval.md +1 -2
- package/skills/roles/qa.api-contract.md +0 -1
- package/skills/roles/qa.data-pipeline.md +0 -1
- package/skills/roles/qa.md +0 -1
- package/skills/roles/qa.web-ui.md +0 -1
- package/skills/roles/release-manager.md +1 -1
- package/skills/roles/researcher.md +0 -2
- package/skills/roles/reviewer.md +0 -3
- package/skills/roles/security.ai.md +1 -1
- package/skills/roles/security.legal-compliance.md +1 -1
- package/skills/roles/security.md +0 -1
- package/skills/roles/security.privacy.md +0 -1
- package/skills/roles/security.supply-chain.md +1 -1
- package/skills/roles/sre.md +1 -1
- package/skills/roles/test-automation.md +1 -1
- package/skills/roles/trace-reviewer.md +1 -1
- package/skills/roles/ux-researcher.md +1 -1
- package/specialists/artifact-manifest.json +36 -36
- package/specialists/org/contracts/accessibility-to-qa.json +11 -3
- package/specialists/org/contracts/any-to-business-strategist.json +19 -7
- package/specialists/org/contracts/any-to-debugger.json +7 -1
- package/specialists/org/contracts/any-to-designer.json +7 -1
- package/specialists/org/contracts/any-to-docs-keeper.json +18 -4
- package/specialists/org/contracts/any-to-explorer.json +13 -4
- package/specialists/org/contracts/any-to-sre-incident.json +6 -2
- package/specialists/org/contracts/any-to-trace-reviewer.json +16 -4
- package/specialists/org/contracts/architect-to-ai-engineer.json +6 -2
- package/specialists/org/contracts/architect-to-data-engineer.json +17 -5
- package/specialists/org/contracts/architect-to-devil-advocate.json +11 -3
- package/specialists/org/contracts/architect-to-engineer.json +20 -4
- package/specialists/org/contracts/architect-to-evaluator.json +15 -5
- package/specialists/org/contracts/architect-to-legal-compliance.json +15 -5
- package/specialists/org/contracts/architect-to-operations.json +17 -4
- package/specialists/org/contracts/architect-to-platform-engineer.json +20 -4
- package/specialists/org/contracts/construct-to-orchestrator.json +10 -2
- package/specialists/org/contracts/data-analyst-to-product-manager.json +11 -2
- package/specialists/org/contracts/engineer-to-qa.json +20 -4
- package/specialists/org/contracts/engineer-to-reviewer.json +29 -5
- package/specialists/org/contracts/explorer-to-engineer.json +11 -3
- package/specialists/org/contracts/legal-compliance-to-release-manager.json +12 -4
- package/specialists/org/contracts/operations-to-user.json +53 -0
- package/specialists/org/contracts/operations-triage-output.json +53 -0
- package/specialists/org/contracts/pm-requirements-candidates.json +53 -0
- package/specialists/org/contracts/product-manager-to-architect.json +30 -10
- package/specialists/org/contracts/product-manager-to-data-analyst.json +13 -3
- package/specialists/org/contracts/product-manager-to-ux-researcher.json +15 -5
- package/specialists/org/contracts/qa-to-release-manager.json +11 -3
- package/specialists/org/contracts/researcher-to-architect.json +10 -2
- package/specialists/org/contracts/researcher-to-product-manager.json +16 -5
- package/specialists/org/contracts/reviewer-to-security.json +17 -3
- package/specialists/org/contracts/test-automation-to-engineer.json +11 -3
- package/specialists/org/contracts/trace-reviewer-to-sre.json +12 -4
- package/specialists/org/contracts/user-to-construct.json +11 -4
- package/specialists/org/frameworks/cx-architect-constraint-option-failure.md +66 -0
- package/specialists/org/frameworks/cx-engineer-feasibility-blast-radius.md +66 -0
- package/specialists/org/frameworks/cx-ops-dependency-sequencing.md +74 -0
- package/specialists/org/frameworks/cx-pm-value-tradeoff.md +66 -0
- package/specialists/org/frameworks/cx-qa-risk-based-coverage.md +69 -0
- package/specialists/org/groups/engineering-group.json +2 -6
- package/specialists/org/groups/governance-group.json +2 -3
- package/specialists/org/groups/operations-group.json +5 -8
- package/specialists/org/groups/product-group.json +4 -8
- package/specialists/org/groups/quality-group.json +3 -7
- package/specialists/org/groups/strategy-group.json +6 -9
- package/specialists/org/models.json.example +8 -0
- package/specialists/org/scopes/creative.json +6 -1
- package/specialists/org/scopes/operations.json +7 -1
- package/specialists/org/scopes/research.json +6 -1
- package/specialists/org/scopes/rnd.json +7 -1
- package/specialists/org/specialists/cx-architect.json +27 -8
- package/specialists/org/specialists/cx-designer.json +22 -8
- package/specialists/org/specialists/cx-engineer.json +71 -7
- package/specialists/org/specialists/cx-operations.json +89 -15
- package/specialists/org/specialists/cx-orchestrator.json +16 -4
- package/specialists/org/specialists/cx-product-manager.json +28 -9
- package/specialists/org/specialists/cx-qa.json +16 -6
- package/specialists/org/specialists/cx-researcher.json +41 -7
- package/specialists/org/specialists/cx-reviewer.json +61 -11
- package/specialists/org/specialists/cx-security.json +30 -10
- package/specialists/org/teams/design-team.json +3 -4
- package/specialists/org/teams/engineering-team.json +3 -10
- package/specialists/org/teams/governance-team.json +3 -5
- package/specialists/org/teams/operations-team.json +6 -12
- package/specialists/org/teams/product-management-team.json +2 -3
- package/specialists/org/teams/quality-team.json +4 -12
- package/specialists/org/teams/research-team.json +3 -3
- package/specialists/org/teams/strategy-team.json +7 -14
- package/specialists/prompts/cx-architect.md +20 -1
- package/specialists/prompts/cx-data-analyst.md +1 -1
- package/specialists/prompts/cx-designer.md +12 -4
- package/specialists/prompts/cx-engineer.md +15 -1
- package/specialists/prompts/cx-operations.md +14 -2
- package/specialists/prompts/cx-orchestrator.md +9 -5
- package/specialists/prompts/cx-product-manager.md +5 -1
- package/specialists/prompts/cx-qa.md +6 -2
- package/specialists/prompts/cx-researcher.md +27 -31
- package/specialists/prompts/cx-reviewer.md +19 -1
- package/specialists/prompts/cx-security.md +5 -1
- package/templates/distribution/construct-brand.typ +123 -59
- package/templates/distribution/construct-decision.typ +6 -3
- package/templates/distribution/construct-pdf.typ +11 -4
- package/templates/distribution/construct-prd.typ +6 -3
- package/templates/distribution/construct-research.typ +7 -4
- package/vendor/pandoc-ext/README.md +3 -0
- package/vendor/pandoc-ext/diagram.lua +687 -0
- package/lib/providers/contract/adapters/git/index.mjs +0 -115
- package/lib/providers/contract/adapters/slack/index.mjs +0 -175
- package/specialists/org/contracts/business-strategist-to-product-manager.json +0 -39
- package/specialists/org/contracts/construct-to-rd-lead.json +0 -53
- package/specialists/org/contracts/data-engineer-to-platform-engineer.json +0 -36
- package/specialists/org/contracts/designer-to-accessibility.json +0 -36
- package/specialists/org/contracts/platform-engineer-to-engineer.json +0 -31
- package/specialists/org/contracts/qa-to-test-automation.json +0 -42
- package/specialists/org/contracts/rd-lead-to-architect.json +0 -38
- package/specialists/org/contracts/sre-to-release-manager.json +0 -36
- package/specialists/org/specialists/cx-accessibility.json +0 -69
- package/specialists/org/specialists/cx-ai-engineer.json +0 -75
- package/specialists/org/specialists/cx-business-strategist.json +0 -72
- package/specialists/org/specialists/cx-data-engineer.json +0 -71
- package/specialists/org/specialists/cx-devil-advocate.json +0 -71
- package/specialists/org/specialists/cx-docs-keeper.json +0 -82
- package/specialists/org/specialists/cx-evaluator.json +0 -69
- package/specialists/org/specialists/cx-explorer.json +0 -69
- package/specialists/org/specialists/cx-legal-compliance.json +0 -74
- package/specialists/org/specialists/cx-oracle.json +0 -46
- package/specialists/org/specialists/cx-platform-engineer.json +0 -80
- package/specialists/org/specialists/cx-rd-lead.json +0 -73
- package/specialists/org/specialists/cx-release-manager.json +0 -77
- package/specialists/org/specialists/cx-sre.json +0 -94
- package/specialists/org/specialists/cx-test-automation.json +0 -69
- package/specialists/org/specialists/cx-trace-reviewer.json +0 -69
- package/specialists/org/specialists/cx-ux-researcher.json +0 -68
- package/specialists/org/teams/accessibility-team.json +0 -39
- package/specialists/org/teams/ux-research-team.json +0 -39
- package/specialists/prompts/cx-accessibility.md +0 -55
- package/specialists/prompts/cx-ai-engineer.md +0 -124
- package/specialists/prompts/cx-business-strategist.md +0 -58
- package/specialists/prompts/cx-data-engineer.md +0 -55
- package/specialists/prompts/cx-devil-advocate.md +0 -59
- package/specialists/prompts/cx-docs-keeper.md +0 -164
- package/specialists/prompts/cx-evaluator.md +0 -49
- package/specialists/prompts/cx-explorer.md +0 -71
- package/specialists/prompts/cx-legal-compliance.md +0 -58
- package/specialists/prompts/cx-oracle.md +0 -98
- package/specialists/prompts/cx-platform-engineer.md +0 -97
- package/specialists/prompts/cx-rd-lead.md +0 -59
- package/specialists/prompts/cx-release-manager.md +0 -54
- package/specialists/prompts/cx-sre.md +0 -111
- package/specialists/prompts/cx-test-automation.md +0 -55
- package/specialists/prompts/cx-trace-reviewer.md +0 -101
- package/specialists/prompts/cx-ux-researcher.md +0 -53
|
@@ -15,26 +15,26 @@ const SOFFICE_CANDIDATES = [
|
|
|
15
15
|
'/Applications/LibreOffice.app/Contents/MacOS/soffice',
|
|
16
16
|
];
|
|
17
17
|
|
|
18
|
-
function whichBin(name) {
|
|
18
|
+
function whichBin(name, env) {
|
|
19
19
|
const cmd = process.platform === 'win32' ? 'where' : 'which';
|
|
20
|
-
const result = spawnSync(cmd, [name], { encoding: 'utf8' });
|
|
20
|
+
const result = spawnSync(cmd, [name], { encoding: 'utf8', env });
|
|
21
21
|
if (result.status !== 0) return null;
|
|
22
22
|
return (result.stdout || '').trim().split('\n')[0] || null;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function resolveLibreOfficeBin(env = process.env) {
|
|
25
|
+
export function resolveLibreOfficeBin(env = process.env, { existsSyncFn = fs.existsSync } = {}) {
|
|
26
26
|
const fromEnv = (env.CONSTRUCT_LIBREOFFICE_BIN || env.SOFFICE_BIN || '').trim();
|
|
27
|
-
if (fromEnv &&
|
|
27
|
+
if (fromEnv && existsSyncFn(fromEnv)) return fromEnv;
|
|
28
28
|
for (const candidate of SOFFICE_CANDIDATES) {
|
|
29
|
-
if (candidate.includes('/') &&
|
|
30
|
-
const found = whichBin(candidate);
|
|
29
|
+
if (candidate.includes('/') && existsSyncFn(candidate)) return candidate;
|
|
30
|
+
const found = whichBin(candidate, env);
|
|
31
31
|
if (found) return found;
|
|
32
32
|
}
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
export function libreOfficePresent(env = process.env) {
|
|
37
|
-
return Boolean(resolveLibreOfficeBin(env));
|
|
36
|
+
export function libreOfficePresent(env = process.env, opts) {
|
|
37
|
+
return Boolean(resolveLibreOfficeBin(env, opts));
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
export function libreOfficeInstallHint() {
|
package/lib/logging/rotate.mjs
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
* lib/logging/rotate.mjs — shared file-rotation primitive.
|
|
3
3
|
*
|
|
4
4
|
* Two consumers need bounded log files: the trace writer
|
|
5
|
-
* (
|
|
6
|
-
*
|
|
5
|
+
* (state-root `traces/<date>.jsonl` — ADR-0066, machine-scoped, not
|
|
6
|
+
* project-local — capped to avoid >100MB single-file commits that GitHub
|
|
7
|
+
* would reject if ever committed) and the embed daemon stdout log
|
|
7
8
|
* (`~/.cx/runtime/embed-daemon.log`, capped so a stuck "Telemetry skipped"
|
|
8
9
|
* message can't fill the disk).
|
|
9
10
|
*
|
|
@@ -183,8 +184,8 @@ export function appendWithRotationSync(filePath, line, { maxBytes, maxSegments =
|
|
|
183
184
|
* background daemons).
|
|
184
185
|
*/
|
|
185
186
|
export const LIMITS = {
|
|
186
|
-
// Trace shards under
|
|
187
|
-
// 100 MB single-file ceiling. Bead construct-1vv5.
|
|
187
|
+
// Trace shards under the state-root's traces/<date>.jsonl — capped below
|
|
188
|
+
// GitHub's 100 MB single-file ceiling. Bead construct-1vv5.
|
|
188
189
|
|
|
189
190
|
trace: {
|
|
190
191
|
maxBytes: 100 * 1024 * 1024,
|
package/lib/mcp/broker.mjs
CHANGED
|
@@ -8,27 +8,139 @@
|
|
|
8
8
|
* `Broker.invoke({...})`, every denial emits a typed error rather than
|
|
9
9
|
* a silent fallthrough, and every brokered call appends a `tool.called`
|
|
10
10
|
* trace event tagged with the policy decision.
|
|
11
|
+
*
|
|
12
|
+
* LMCP-I3: every decision (allow/deny/approval_required) is also recorded
|
|
13
|
+
* as durable evidence under one schema — {decisionId, actor, tenant,
|
|
14
|
+
* project, tool, target, risk, outcome, correlationId, ts} — via
|
|
15
|
+
* `_recordDecision`. Denied decisions additionally persist to
|
|
16
|
+
* lib/mcp/denied-store.mjs so a PolicyDenied throw is never the only trace
|
|
17
|
+
* of the denial. `correlationId` reuses an inbound `traceId` when the
|
|
18
|
+
* caller has one, or mints a fresh trace-shaped id, so the same value ties
|
|
19
|
+
* the broker call to its `tool.called` trace event and to any run/task/
|
|
20
|
+
* external write layered on top by the caller.
|
|
11
21
|
*/
|
|
12
22
|
|
|
23
|
+
import fs from 'node:fs';
|
|
24
|
+
import path from 'node:path';
|
|
25
|
+
import crypto from 'node:crypto';
|
|
13
26
|
import { policyDecision } from '../policy/engine.mjs';
|
|
14
|
-
import { emitTraceEvent } from '../worker/trace.mjs';
|
|
27
|
+
import { emitTraceEvent, newTraceId } from '../worker/trace.mjs';
|
|
28
|
+
import { getDeploymentMode } from '../deployment-mode.mjs';
|
|
29
|
+
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
30
|
+
import { ApprovalQueue } from '../embed/approval-queue.mjs';
|
|
31
|
+
import { resolveTenantContext } from '../tenant/context.mjs';
|
|
32
|
+
import { appendAuditRecord } from '../audit-trail.mjs';
|
|
33
|
+
import { DeniedStore } from './denied-store.mjs';
|
|
34
|
+
import { ConsumptionBudgetStore, BudgetExceeded } from '../policy/consumption-budget.mjs';
|
|
15
35
|
|
|
16
36
|
const DEFAULT_RATE_WINDOW_MS = 60_000;
|
|
17
37
|
const DEFAULT_RATE_BUDGET = 30;
|
|
18
38
|
|
|
39
|
+
// Every broker decision — allow, deny, or approval-required — is recorded
|
|
40
|
+
// under this one schema so the durable denied-store and the audit-trail
|
|
41
|
+
// entry for the same decision can be cross-referenced by decisionId, and a
|
|
42
|
+
// correlationId ties the decision back to the trace event and, downstream,
|
|
43
|
+
// to whatever run/task/external write the tool call was part of.
|
|
44
|
+
|
|
45
|
+
function newDecisionId() {
|
|
46
|
+
return `decision-${crypto.randomBytes(8).toString('hex')}`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function buildDecisionRecord({ decision, outcome, actor, tenant, project, tool, target, risk, correlationId, ts }) {
|
|
50
|
+
return {
|
|
51
|
+
decisionId: newDecisionId(),
|
|
52
|
+
actor: actor ?? 'unknown',
|
|
53
|
+
tenant: tenant ?? 'unknown',
|
|
54
|
+
project: project ?? null,
|
|
55
|
+
tool: tool ?? 'unknown',
|
|
56
|
+
target: target ?? null,
|
|
57
|
+
risk: risk || 'low',
|
|
58
|
+
outcome,
|
|
59
|
+
correlationId,
|
|
60
|
+
reason: decision?.reason ?? null,
|
|
61
|
+
source: decision?.source ?? null,
|
|
62
|
+
ts,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* BrokerStore — file-backed durable storage for broker rate-limit state.
|
|
68
|
+
*
|
|
69
|
+
* Persists call timestamps to `<rootDir>/.cx/broker-state.json` so that
|
|
70
|
+
* rate-limit windows survive broker instance recreation and server restarts.
|
|
71
|
+
* In-memory `_data` is the authoritative write buffer; `load`/`save` sync
|
|
72
|
+
* against disk. All disk I/O is best-effort: a disk failure leaves in-memory
|
|
73
|
+
* state intact without crashing the broker.
|
|
74
|
+
* Rate-limit windows are keyed by actor+tool+windowStart (epoch ms).
|
|
75
|
+
*/
|
|
76
|
+
export class BrokerStore {
|
|
77
|
+
constructor() {
|
|
78
|
+
// key → timestamp[] (ms since epoch), trimmed to current window on read
|
|
79
|
+
this._data = {};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Load persisted state from `storePath`. No-op if the file is absent or unreadable. */
|
|
83
|
+
load(storePath) {
|
|
84
|
+
try {
|
|
85
|
+
const raw = fs.readFileSync(storePath, 'utf8');
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
88
|
+
this._data = parsed;
|
|
89
|
+
}
|
|
90
|
+
} catch { /* absent or corrupt file — start fresh */ }
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Persist current state to `storePath`. Best-effort. */
|
|
94
|
+
save(storePath) {
|
|
95
|
+
try {
|
|
96
|
+
fs.mkdirSync(path.dirname(storePath), { recursive: true });
|
|
97
|
+
fs.writeFileSync(storePath, JSON.stringify(this._data), 'utf8');
|
|
98
|
+
} catch { /* best-effort */ }
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* getRateLimitState(actor, tool, windowMs) → timestamps[] within the window.
|
|
103
|
+
* Trims stale entries before returning.
|
|
104
|
+
*/
|
|
105
|
+
getRateLimitState(actor, tool, windowMs) {
|
|
106
|
+
const key = `${actor}::${tool}`;
|
|
107
|
+
const now = Date.now();
|
|
108
|
+
const all = this._data[key] || [];
|
|
109
|
+
const fresh = all.filter((ts) => now - ts < windowMs);
|
|
110
|
+
this._data[key] = fresh;
|
|
111
|
+
return fresh;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* incrementRateLimitState(actor, tool, windowMs) → updated timestamps[].
|
|
116
|
+
* Appends `Date.now()` and trims stale entries.
|
|
117
|
+
*/
|
|
118
|
+
incrementRateLimitState(actor, tool, windowMs) {
|
|
119
|
+
const key = `${actor}::${tool}`;
|
|
120
|
+
const now = Date.now();
|
|
121
|
+
const all = this._data[key] || [];
|
|
122
|
+
const fresh = all.filter((ts) => now - ts < windowMs);
|
|
123
|
+
fresh.push(now);
|
|
124
|
+
this._data[key] = fresh;
|
|
125
|
+
return fresh;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
19
129
|
export class PolicyDenied extends Error {
|
|
20
|
-
constructor(decision) {
|
|
130
|
+
constructor(decision, correlationId = null) {
|
|
21
131
|
super(`policy denied: ${decision.reason}`);
|
|
22
132
|
this.name = 'PolicyDenied';
|
|
23
133
|
this.decision = decision;
|
|
134
|
+
this.correlationId = correlationId;
|
|
24
135
|
}
|
|
25
136
|
}
|
|
26
137
|
|
|
27
138
|
export class ApprovalRequired extends Error {
|
|
28
|
-
constructor(decision) {
|
|
139
|
+
constructor(decision, correlationId = null) {
|
|
29
140
|
super(`approval required: ${decision.reason}`);
|
|
30
141
|
this.name = 'ApprovalRequired';
|
|
31
142
|
this.decision = decision;
|
|
143
|
+
this.correlationId = correlationId;
|
|
32
144
|
}
|
|
33
145
|
}
|
|
34
146
|
|
|
@@ -49,6 +161,13 @@ export class Broker {
|
|
|
49
161
|
rateBudget = DEFAULT_RATE_BUDGET,
|
|
50
162
|
rateWindowMs = DEFAULT_RATE_WINDOW_MS,
|
|
51
163
|
now = () => Date.now(),
|
|
164
|
+
store,
|
|
165
|
+
approvalQueue,
|
|
166
|
+
deniedStore,
|
|
167
|
+
consumptionBudgets,
|
|
168
|
+
tenantContext,
|
|
169
|
+
auditRecorder = appendAuditRecord,
|
|
170
|
+
env = process.env,
|
|
52
171
|
} = {}) {
|
|
53
172
|
if (!rootDir) throw new Error('Broker: rootDir is required');
|
|
54
173
|
this.rootDir = rootDir;
|
|
@@ -57,42 +176,170 @@ export class Broker {
|
|
|
57
176
|
this.rateBudget = rateBudget;
|
|
58
177
|
this.rateWindowMs = rateWindowMs;
|
|
59
178
|
this.now = now;
|
|
179
|
+
// Durable store for rate-limit state.
|
|
180
|
+
this._storePath = path.join(rootDir, '.cx', 'broker-state.json');
|
|
181
|
+
this._store = store instanceof BrokerStore ? store : new BrokerStore();
|
|
182
|
+
if (!(store instanceof BrokerStore)) {
|
|
183
|
+
this._store.load(this._storePath);
|
|
184
|
+
}
|
|
185
|
+
// Approval queue for durable awaiting_approval state (per ADR-0056).
|
|
186
|
+
this.approvalQueue = approvalQueue || null;
|
|
187
|
+
// Durable store for denied decisions (LMCP-I3) — every PolicyDenied
|
|
188
|
+
// throw also lands a row here, keyed by decisionId, before the error
|
|
189
|
+
// leaves invoke().
|
|
190
|
+
this._deniedStore = deniedStore instanceof DeniedStore ? deniedStore : new DeniedStore({ rootDir });
|
|
191
|
+
// Durable per-actor/run consumption budgets (LMCP-N5) — checked pre-
|
|
192
|
+
// execution when a caller passes `runId`; a caller that never passes
|
|
193
|
+
// one sees no budget behavior at all (backward compatible).
|
|
194
|
+
this._consumptionBudgets = consumptionBudgets instanceof ConsumptionBudgetStore
|
|
195
|
+
? consumptionBudgets
|
|
196
|
+
: new ConsumptionBudgetStore({ rootDir, env });
|
|
197
|
+
// Audit recorder is injectable so tests can capture records without
|
|
198
|
+
// touching the real ~/.cx/audit-trail.jsonl file.
|
|
199
|
+
this._auditRecorder = auditRecorder;
|
|
200
|
+
// Tenant resolved once per broker instance (env/config are stable for
|
|
201
|
+
// the process lifetime); reused for every decision record.
|
|
202
|
+
this._tenant = tenantContext || resolveTenantContext({ env, mode: getDeploymentMode(env, { cwd: rootDir }) }).tenantId;
|
|
203
|
+
// Keep this.calls as an in-memory Map for callers that read it directly
|
|
60
204
|
this.calls = new Map();
|
|
61
205
|
}
|
|
62
206
|
|
|
207
|
+
/**
|
|
208
|
+
* Attach an ApprovalQueue instance to this broker after construction.
|
|
209
|
+
*/
|
|
210
|
+
setApprovalQueue(queue) {
|
|
211
|
+
this.approvalQueue = queue;
|
|
212
|
+
}
|
|
213
|
+
|
|
63
214
|
_checkRate(role, tool) {
|
|
64
|
-
const
|
|
65
|
-
const ts = this.now();
|
|
66
|
-
const window = this.calls.get(key) || [];
|
|
67
|
-
const fresh = window.filter((t) => ts - t < this.rateWindowMs);
|
|
215
|
+
const fresh = this._store.getRateLimitState(role, tool, this.rateWindowMs);
|
|
68
216
|
if (fresh.length >= this.rateBudget) throw new RateLimited(role, tool, this.rateBudget);
|
|
69
|
-
|
|
70
|
-
|
|
217
|
+
this._store.incrementRateLimitState(role, tool, this.rateWindowMs);
|
|
218
|
+
// Persist after every update so the state is durable across restarts.
|
|
219
|
+
this._store.save(this._storePath);
|
|
220
|
+
// Keep the legacy Map in sync for any callers inspecting it directly.
|
|
221
|
+
this.calls.set(`${role}::${tool}`, this._store.getRateLimitState(role, tool, this.rateWindowMs));
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// requestedBy already carries the H2 identity record (identityToRecord()
|
|
225
|
+
// shape: userId/serviceId/role/...) at every call site observed in
|
|
226
|
+
// lib/mcp/server.mjs and lib/writes/envelope.mjs. Fall back to role, then
|
|
227
|
+
// to 'unknown' rather than fabricating an identity that was never resolved.
|
|
228
|
+
|
|
229
|
+
_resolveActor(role, requestedBy) {
|
|
230
|
+
if (requestedBy && typeof requestedBy === 'object') {
|
|
231
|
+
const id = requestedBy.userId || requestedBy.serviceId;
|
|
232
|
+
if (id) return id;
|
|
233
|
+
}
|
|
234
|
+
return role || 'unknown';
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Record a broker decision as durable evidence: a denied-store row when
|
|
239
|
+
* the outcome is 'denied' or 'budget_denied' (LMCP-N5 — a budget refusal
|
|
240
|
+
* is a denial under the same schema, just from a different gate than the
|
|
241
|
+
* policy engine), and an audit-trail entry for every outcome (allow/deny/
|
|
242
|
+
* approval/budget_denied) under the same schema. Both writes are
|
|
243
|
+
* best-effort — a disk failure here must never break tool dispatch, the
|
|
244
|
+
* same guarantee lib/mcp/server.mjs already gives its own audit call.
|
|
245
|
+
*/
|
|
246
|
+
_recordDecision({ decision, outcome, role, tool, action, risk, project, requestedBy, correlationId }) {
|
|
247
|
+
const record = buildDecisionRecord({
|
|
248
|
+
decision,
|
|
249
|
+
outcome,
|
|
250
|
+
actor: this._resolveActor(role, requestedBy),
|
|
251
|
+
tenant: this._tenant,
|
|
252
|
+
project,
|
|
253
|
+
tool,
|
|
254
|
+
target: action ?? null,
|
|
255
|
+
risk,
|
|
256
|
+
correlationId,
|
|
257
|
+
ts: new Date(this.now()).toISOString(),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
if (outcome === 'denied' || outcome === 'budget_denied') {
|
|
261
|
+
try { this._deniedStore.append(record); } catch { /* best-effort, mirrors BrokerStore.save */ }
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
try { this._auditRecorder({ agent: 'mcp-broker', ...record }); } catch { /* audit trail unavailable must not fail the call */ }
|
|
265
|
+
|
|
266
|
+
return record;
|
|
71
267
|
}
|
|
72
268
|
|
|
73
269
|
/**
|
|
74
270
|
* Invoke a tool through the broker. The `execute` function does the
|
|
75
271
|
* actual work; the broker decides whether to call it.
|
|
76
272
|
*
|
|
273
|
+
* When approval is required, returns a structured `{ status, approvalId,
|
|
274
|
+
* resumeToken, expiresAt }` instead of throwing. Callers that retry with
|
|
275
|
+
* the same tool+args will match the existing approval record. After the
|
|
276
|
+
* record is approved (via CLI or API), a call with the same args executes
|
|
277
|
+
* the tool. Pass `resumeToken` explicitly to bypass argsHash dedup.
|
|
278
|
+
*
|
|
77
279
|
* @param {object} args
|
|
78
280
|
* @param {string} args.role
|
|
79
281
|
* @param {string} args.tool
|
|
80
282
|
* @param {string} args.action
|
|
283
|
+
* @param {object} [args.toolArgs] - Raw tool arguments for argsHash dedup
|
|
81
284
|
* @param {string} [args.risk]
|
|
82
285
|
* @param {string} [args.project]
|
|
83
286
|
* @param {string} [args.traceId]
|
|
287
|
+
* @param {string} [args.resumeToken] - Explicit resume token (bypasses dedup)
|
|
288
|
+
* @param {object} [args.requestedBy] - Actor identity (identityToRecord() shape)
|
|
289
|
+
* @param {string} [args.runId] - LMCP-N5: attributes consumption to a run; omitted means no budget check at all
|
|
290
|
+
* @param {number} [args.tokenEstimate] - LMCP-N5: tokens this call is expected to consume, counted toward the run's token budget
|
|
291
|
+
* @param {object} [args.budget] - LMCP-N5: explicit budget override (defaults to the deployment-mode default)
|
|
84
292
|
* @param {Function} args.execute
|
|
85
|
-
* @returns {Promise<{result, decision}>}
|
|
293
|
+
* @returns {Promise<{result, decision, correlationId}|{status, approvalId, resumeToken, expiresAt, correlationId}>}
|
|
86
294
|
*/
|
|
87
|
-
async invoke({ role, tool, action, risk, project, traceId, execute }) {
|
|
295
|
+
async invoke({ role, tool, action, toolArgs, risk, project, traceId, resumeToken, requestedBy, runId, tokenEstimate = 0, budget, execute }) {
|
|
88
296
|
if (typeof execute !== 'function') throw new Error('Broker.invoke: execute function is required');
|
|
89
297
|
|
|
298
|
+
// If a resumeToken is provided, look up the approval record directly.
|
|
299
|
+
if (resumeToken && this.approvalQueue) {
|
|
300
|
+
const record = this.approvalQueue.getByResumeToken(resumeToken);
|
|
301
|
+
if (!record) return { status: 'not_found', resumeToken };
|
|
302
|
+
if (record.state === 'approved') {
|
|
303
|
+
const result = await execute();
|
|
304
|
+
return { result, decision: { allowed: true, reason: 'resumed after approval', approvalRequired: false, source: 'approval-queue' } };
|
|
305
|
+
}
|
|
306
|
+
if (record.state === 'denied') return { status: 'denied', approvalId: record.approvalId, reason: record.reason };
|
|
307
|
+
if (record.state === 'expired') return { status: 'expired', approvalId: record.approvalId };
|
|
308
|
+
return { status: 'awaiting_approval', approvalId: record.approvalId, resumeToken: record.resumeToken, expiresAt: record.expiresAt };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Compute argsHash for dedup with the approval queue.
|
|
312
|
+
const argsHash = this.approvalQueue ? ApprovalQueue.hashToolCall(tool, toolArgs ?? {}) : null;
|
|
313
|
+
|
|
314
|
+
// Check if there is already an approval record for this exact tool call.
|
|
315
|
+
if (this.approvalQueue && argsHash) {
|
|
316
|
+
this.approvalQueue.expireStale();
|
|
317
|
+
const existing = this.approvalQueue.findByToolArgs(tool, argsHash);
|
|
318
|
+
if (existing) {
|
|
319
|
+
if (existing.state === 'approved') {
|
|
320
|
+
const result = await execute();
|
|
321
|
+
return { result, decision: { allowed: true, reason: 'approved via queue', approvalRequired: false, source: 'approval-queue' } };
|
|
322
|
+
}
|
|
323
|
+
if (existing.state === 'denied') return { status: 'denied', approvalId: existing.approvalId, reason: existing.reason };
|
|
324
|
+
if (existing.state === 'expired') return { status: 'expired', approvalId: existing.approvalId };
|
|
325
|
+
return { status: 'awaiting_approval', approvalId: existing.approvalId, resumeToken: existing.resumeToken, expiresAt: existing.expiresAt };
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
90
329
|
const decision = this.policy({ role, project, tool, action, risk });
|
|
91
330
|
|
|
331
|
+
// correlationId ties this broker decision to the tool.called trace event
|
|
332
|
+
// (same value as traceId), and downstream to whatever run/task/external
|
|
333
|
+
// write the caller layers on top — reuse an inbound traceId so a caller
|
|
334
|
+
// that already has one keeps a single id across the whole chain, and
|
|
335
|
+
// mint a trace-shaped id only when the caller supplied none.
|
|
336
|
+
|
|
337
|
+
const correlationId = traceId || newTraceId();
|
|
338
|
+
|
|
92
339
|
this.emit({
|
|
93
340
|
rootDir: this.rootDir,
|
|
94
341
|
eventType: 'tool.called',
|
|
95
|
-
traceId,
|
|
342
|
+
traceId: correlationId,
|
|
96
343
|
project,
|
|
97
344
|
role,
|
|
98
345
|
metadata: {
|
|
@@ -106,19 +353,87 @@ export class Broker {
|
|
|
106
353
|
},
|
|
107
354
|
});
|
|
108
355
|
|
|
109
|
-
if (!decision.allowed)
|
|
110
|
-
|
|
356
|
+
if (!decision.allowed) {
|
|
357
|
+
this._recordDecision({ decision, outcome: 'denied', role, tool, action, risk, project, requestedBy, correlationId });
|
|
358
|
+
throw new PolicyDenied(decision, correlationId);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
if (decision.approvalRequired) {
|
|
362
|
+
this._recordDecision({ decision, outcome: 'approval_required', role, tool, action, risk, project, requestedBy, correlationId });
|
|
363
|
+
if (this.approvalQueue) {
|
|
364
|
+
const record = this.approvalQueue.enqueue({
|
|
365
|
+
tool,
|
|
366
|
+
args: toolArgs ?? {},
|
|
367
|
+
surface: 'mcp',
|
|
368
|
+
argsHash,
|
|
369
|
+
requestedBy: requestedBy ?? { role },
|
|
370
|
+
});
|
|
371
|
+
return {
|
|
372
|
+
status: 'awaiting_approval',
|
|
373
|
+
approvalId: record.approvalId,
|
|
374
|
+
resumeToken: record.resumeToken,
|
|
375
|
+
expiresAt: record.expiresAt,
|
|
376
|
+
correlationId,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// No queue configured — fall back to throwing for backward compat.
|
|
380
|
+
throw new ApprovalRequired(decision, correlationId);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
this._recordDecision({ decision, outcome: 'allowed', role, tool, action, risk, project, requestedBy, correlationId });
|
|
384
|
+
|
|
385
|
+
// LMCP-N5: a caller with no runId gets no budget behavior at all — only
|
|
386
|
+
// an attributed run accrues durable consumption, so this is additive to
|
|
387
|
+
// every call site that predates runId.
|
|
388
|
+
if (runId) {
|
|
389
|
+
const actor = this._resolveActor(role, requestedBy);
|
|
390
|
+
const toolCallCheck = this._consumptionBudgets.check(actor, runId, 'toolCalls', 1, { budget });
|
|
391
|
+
const tokenCheck = tokenEstimate > 0
|
|
392
|
+
? this._consumptionBudgets.check(actor, runId, 'tokens', tokenEstimate, { budget })
|
|
393
|
+
: { allowed: true };
|
|
394
|
+
const failed = !toolCallCheck.allowed ? { kind: 'toolCalls', check: toolCallCheck } : (!tokenCheck.allowed ? { kind: 'tokens', check: tokenCheck } : null);
|
|
395
|
+
if (failed) {
|
|
396
|
+
const budgetDecision = {
|
|
397
|
+
allowed: false,
|
|
398
|
+
approvalRequired: false,
|
|
399
|
+
reason: `consumption budget exceeded: ${failed.kind} would reach ${failed.check.projected} against a budget of ${failed.check.cap}`,
|
|
400
|
+
source: 'consumption-budget',
|
|
401
|
+
};
|
|
402
|
+
this._recordDecision({ decision: budgetDecision, outcome: 'budget_denied', role, tool, action, risk, project, requestedBy, correlationId });
|
|
403
|
+
throw new BudgetExceeded({ actor, runId, kind: failed.kind, cap: failed.check.cap, projected: failed.check.projected });
|
|
404
|
+
}
|
|
405
|
+
}
|
|
111
406
|
|
|
112
407
|
this._checkRate(role, tool);
|
|
113
408
|
const result = await execute();
|
|
114
|
-
|
|
409
|
+
|
|
410
|
+
if (runId) {
|
|
411
|
+
const actor = this._resolveActor(role, requestedBy);
|
|
412
|
+
this._consumptionBudgets.record(actor, runId, 'toolCalls', 1);
|
|
413
|
+
if (tokenEstimate > 0) this._consumptionBudgets.record(actor, runId, 'tokens', tokenEstimate);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
return { result, decision, correlationId };
|
|
115
417
|
}
|
|
116
418
|
}
|
|
117
419
|
|
|
118
|
-
export
|
|
420
|
+
export { BudgetExceeded };
|
|
421
|
+
|
|
422
|
+
export function isBrokered(env = process.env, { cwd } = {}) {
|
|
119
423
|
const override = env?.CONSTRUCT_MCP_BROKER;
|
|
120
424
|
if (override === 'on') return true;
|
|
121
425
|
if (override === 'off') return false;
|
|
122
|
-
|
|
426
|
+
|
|
427
|
+
// Same env > config > default precedence as getDeploymentMode: 'auto' (or an
|
|
428
|
+
// absent key) falls through to the deployment-mode default below.
|
|
429
|
+
|
|
430
|
+
try {
|
|
431
|
+
const { config } = loadProjectConfig(cwd, env);
|
|
432
|
+
const fromConfig = config?.deployment?.mcpBroker;
|
|
433
|
+
if (fromConfig === 'on') return true;
|
|
434
|
+
if (fromConfig === 'off') return false;
|
|
435
|
+
} catch { /* loader is best-effort — fall through to deployment-mode default */ }
|
|
436
|
+
|
|
437
|
+
const mode = getDeploymentMode(env, { cwd });
|
|
123
438
|
return mode === 'team' || mode === 'enterprise';
|
|
124
439
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/denied-store.mjs — durable store for denied policy decisions.
|
|
3
|
+
*
|
|
4
|
+
* LMCP-I3: a PolicyDenied throw is a transient exception unless the decision
|
|
5
|
+
* that produced it is also written somewhere a reviewer can re-open later.
|
|
6
|
+
* DeniedStore appends one JSONL line per denied decision to
|
|
7
|
+
* `<rootDir>/.cx/denied-decisions.jsonl` — append-only, never rewritten, so a
|
|
8
|
+
* repeated-denial pattern for one actor+tool is visible as a policy-tuning
|
|
9
|
+
* signal without replaying the audit-trail chain. Every record uses the same
|
|
10
|
+
* schema as the audit-trail entry for the same decision — {decisionId, actor,
|
|
11
|
+
* tenant, project, tool, target, risk, outcome, correlationId, ts} — so the
|
|
12
|
+
* two stores can be cross-referenced by decisionId. Disk I/O is best-effort:
|
|
13
|
+
* a write failure never breaks the broker call it originated from.
|
|
14
|
+
*
|
|
15
|
+
* readAllForTenant (LMCP-H5) composes readAll with
|
|
16
|
+
* lib/tenant/isolation.mjs#readTenantScoped so any caller reading denied
|
|
17
|
+
* decisions for a specific tenant gets a fail-closed, tenant-scoped view —
|
|
18
|
+
* a record from another tenant is excluded, and an unresolved tenantId
|
|
19
|
+
* throws rather than silently returning the unfiltered store.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
|
|
25
|
+
import { readTenantScoped } from '../tenant/isolation.mjs';
|
|
26
|
+
|
|
27
|
+
const DENIED_STORE_SUBPATH = path.join('.cx', 'denied-decisions.jsonl');
|
|
28
|
+
|
|
29
|
+
export function deniedStorePath(rootDir) {
|
|
30
|
+
return path.join(rootDir, DENIED_STORE_SUBPATH);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class DeniedStore {
|
|
34
|
+
constructor({ rootDir, file } = {}) {
|
|
35
|
+
if (!rootDir && !file) throw new Error('DeniedStore: rootDir or file is required');
|
|
36
|
+
this.file = file || deniedStorePath(rootDir);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Append a denied-decision record. Returns the record on success, or null
|
|
41
|
+
* if the write failed (disk full, permissions) — best-effort, never throws.
|
|
42
|
+
*
|
|
43
|
+
* @param {object} record - {decisionId, actor, tenant, project, tool, target, risk, outcome, correlationId, ts}
|
|
44
|
+
*/
|
|
45
|
+
append(record) {
|
|
46
|
+
try {
|
|
47
|
+
fs.mkdirSync(path.dirname(this.file), { recursive: true });
|
|
48
|
+
fs.appendFileSync(this.file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
49
|
+
return record;
|
|
50
|
+
} catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Read all durable denied records. Returns [] if the store is absent or
|
|
57
|
+
* unreadable rather than throwing — a missing store means zero denials so
|
|
58
|
+
* far, not a hard error.
|
|
59
|
+
*/
|
|
60
|
+
readAll() {
|
|
61
|
+
try {
|
|
62
|
+
const raw = fs.readFileSync(this.file, 'utf8');
|
|
63
|
+
return raw
|
|
64
|
+
.split('\n')
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((line) => {
|
|
67
|
+
try { return JSON.parse(line); } catch { return null; }
|
|
68
|
+
})
|
|
69
|
+
.filter(Boolean);
|
|
70
|
+
} catch {
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** Find denied records matching decisionId. Linear scan — store is low-volume by design. */
|
|
76
|
+
findByDecisionId(decisionId) {
|
|
77
|
+
return this.readAll().filter((r) => r.decisionId === decisionId);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** Find denied records matching correlationId, tying a denial back to its originating call. */
|
|
81
|
+
findByCorrelationId(correlationId) {
|
|
82
|
+
return this.readAll().filter((r) => r.correlationId === correlationId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Tenant-scoped read: same records as readAll(), filtered to exactly the
|
|
87
|
+
* given tenantId. Fail-closed — throws TenantIsolationViolation rather
|
|
88
|
+
* than returning the unfiltered store when tenantId is missing/blank.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} tenantId
|
|
91
|
+
*/
|
|
92
|
+
readAllForTenant(tenantId) {
|
|
93
|
+
return readTenantScoped(() => this.readAll(), tenantId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/destructive-approval.mjs — out-of-band approval tokens for destructive MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* storage_reset and delete_ingested_artifacts are reachable only through the model's
|
|
5
|
+
* argument channel, so a confirm flag the model itself supplies cannot authorize
|
|
6
|
+
* irreversible deletion. Tokens are issued out-of-band by an operator action (never the
|
|
7
|
+
* model), persisted with 0600 under the user state dir — outside any project root, so a
|
|
8
|
+
* root-contained file tool cannot read them — and consumed one-time. consumeApprovalToken
|
|
9
|
+
* returns false unless a live, unexpired token for the scope matches, and never writes
|
|
10
|
+
* when no token is supplied.
|
|
11
|
+
*/
|
|
12
|
+
import { randomBytes } from 'node:crypto';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import { doctorRoot } from '../config/xdg.mjs';
|
|
16
|
+
|
|
17
|
+
const TOKEN_TTL_MS = 5 * 60 * 1000;
|
|
18
|
+
|
|
19
|
+
function storePath(env) {
|
|
20
|
+
return path.join(doctorRoot(undefined, env), 'destructive-approvals.json');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readStore(file) {
|
|
24
|
+
try {
|
|
25
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
26
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
27
|
+
} catch {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function writeStore(file, tokens) {
|
|
33
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
34
|
+
fs.writeFileSync(file, JSON.stringify(tokens), { mode: 0o600 });
|
|
35
|
+
try { fs.chmodSync(file, 0o600); } catch { /* non-posix filesystems */ }
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function issueApprovalToken(scope, { env = process.env, now = Date.now() } = {}) {
|
|
39
|
+
if (typeof scope !== 'string' || !scope) throw new Error('scope required');
|
|
40
|
+
const file = storePath(env);
|
|
41
|
+
const token = randomBytes(24).toString('hex');
|
|
42
|
+
const live = readStore(file).filter((t) => t.expiresAt > now);
|
|
43
|
+
live.push({ token, scope, expiresAt: now + TOKEN_TTL_MS });
|
|
44
|
+
writeStore(file, live);
|
|
45
|
+
return token;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function consumeApprovalToken(scope, token, { env = process.env, now = Date.now() } = {}) {
|
|
49
|
+
if (typeof token !== 'string' || !token) return false;
|
|
50
|
+
const file = storePath(env);
|
|
51
|
+
const tokens = readStore(file);
|
|
52
|
+
const idx = tokens.findIndex((t) => t.scope === scope && t.token === token && t.expiresAt > now);
|
|
53
|
+
if (idx === -1) return false;
|
|
54
|
+
tokens.splice(idx, 1);
|
|
55
|
+
writeStore(file, tokens.filter((t) => t.expiresAt > now));
|
|
56
|
+
return true;
|
|
57
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/mcp/destructive-gate.mjs — unified gate for destructive-class MCP tools.
|
|
3
|
+
*
|
|
4
|
+
* Replaces per-tool confirm:true booleans: a tool classified destructive in
|
|
5
|
+
* TOOL_SAFETY requires a valid out-of-band approval token (consumeApprovalToken)
|
|
6
|
+
* before the broker will dispatch it. Single choke point for all destructive tools.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { TOOL_SAFETY } from './tool-safety.mjs';
|
|
10
|
+
import { consumeApprovalToken } from './destructive-approval.mjs';
|
|
11
|
+
|
|
12
|
+
const DESTRUCTIVE_CLASS = 'destructive';
|
|
13
|
+
|
|
14
|
+
export function checkDestructiveGate(toolName, toolArgs, opts = {}) {
|
|
15
|
+
const safety = TOOL_SAFETY[toolName];
|
|
16
|
+
if (!safety || safety.class !== DESTRUCTIVE_CLASS) {
|
|
17
|
+
return { gated: false, allowed: true };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const token = toolArgs?.approval_token;
|
|
21
|
+
if (!consumeApprovalToken(toolName, token)) {
|
|
22
|
+
return {
|
|
23
|
+
gated: true,
|
|
24
|
+
allowed: false,
|
|
25
|
+
reason: `${toolName} requires a valid out-of-band approval token (destructive tool; confirm=true alone is not authorization). Use 'construct tokens issue ${toolName}' or set approval_token.`,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return { gated: true, allowed: true };
|
|
30
|
+
}
|