@geraldmaron/construct 1.4.2 → 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 +1027 -64
- 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 +6 -29
- 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 +89 -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 +18 -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/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 +50 -9
- 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-capabilities.mjs +13 -2
- package/lib/host-disposition.mjs +19 -7
- 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/intake/daemon.mjs +99 -8
- package/lib/intake/git-queue.mjs +110 -38
- 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-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 +154 -1411
- 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-registry.mjs +107 -0
- package/lib/mcp/tool-safety.mjs +1 -0
- package/lib/mcp/tools/orchestration-delegation-next.tool.mjs +68 -0
- package/lib/mcp/tools/orchestration-run.mjs +165 -25
- package/lib/mcp/tools/orchestration-task-result.tool.mjs +77 -0
- 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 +0 -8
- 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 +31 -7
- 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/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 +446 -39
- package/lib/orchestration/store.mjs +87 -12
- package/lib/orchestration/trace-store.mjs +125 -0
- package/lib/orchestration/web-capability.mjs +3 -2
- package/lib/orchestration/worker-runtime.mjs +162 -0
- package/lib/orchestration/worker.mjs +528 -89
- package/lib/orchestration-policy.mjs +67 -1111
- 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/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 +6 -0
- package/lib/providers/secret-resolver.mjs +240 -23
- 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/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 +8 -3
- 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/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/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 +38 -19
- 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 +77 -5
- 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 +16 -8
- package/personas/construct.md +12 -3
- package/registry/capabilities.json +143 -36
- package/rules/common/comments.md +1 -0
- package/schemas/project-config.schema.json +0 -12
- package/schemas/unified-registry.schema.json +2 -4
- package/scripts/sync-specialists.mjs +284 -81
- 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 +40 -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 +25 -30
- 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/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
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment-mode capability registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps each deployment mode (solo / team / enterprise) to its declared capabilities
|
|
5
|
+
* and their current implementation status. This is the single authoritative source
|
|
6
|
+
* for what a mode promises versus what is actually wired up at runtime.
|
|
7
|
+
*
|
|
8
|
+
* Status values:
|
|
9
|
+
* 'implemented' — fully wired; production-safe.
|
|
10
|
+
* 'stub' — code path exists but returns null / falls back silently.
|
|
11
|
+
* 'not-implemented' — no code path exists yet; will throw or degrade if invoked.
|
|
12
|
+
*
|
|
13
|
+
* How to add a new capability:
|
|
14
|
+
* 1. Add an entry to the relevant mode array in CAPABILITY_REGISTRY below.
|
|
15
|
+
* 2. Set status to 'not-implemented' or 'stub' until the implementation lands.
|
|
16
|
+
* 3. Flip to 'implemented' in the same PR that wires the real code.
|
|
17
|
+
* 4. Add a CHANGELOG entry referencing this file and the wiring commit.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
export const CAPABILITY_REGISTRY = {
|
|
21
|
+
solo: [
|
|
22
|
+
{ id: 'filesystem-queue', label: 'Filesystem task queue', status: 'implemented' },
|
|
23
|
+
{ id: 'local-memory', label: 'Local memory', status: 'implemented' },
|
|
24
|
+
{ id: 'embedded-lancedb', label: 'Embedded LanceDB vector store', status: 'implemented' },
|
|
25
|
+
{ id: 'direct-mcp', label: 'Direct MCP dispatch', status: 'implemented' },
|
|
26
|
+
],
|
|
27
|
+
team: [
|
|
28
|
+
{ id: 'postgres-queue', label: 'Postgres task queue', status: 'implemented' },
|
|
29
|
+
{ id: 'shared-memory', label: 'Shared memory store', status: 'stub' },
|
|
30
|
+
{ id: 'worker-heartbeat', label: 'Worker identity and heartbeat', status: 'implemented' },
|
|
31
|
+
{ id: 'docker-workers', label: 'Docker worker pool', status: 'not-implemented' },
|
|
32
|
+
{ id: 'central-telemetry', label: 'Central telemetry', status: 'stub' },
|
|
33
|
+
{ id: 'brokered-mcp', label: 'Brokered MCP dispatch', status: 'stub' },
|
|
34
|
+
],
|
|
35
|
+
enterprise: [
|
|
36
|
+
{ id: 'tenant-isolation', label: 'Tenant isolation', status: 'not-implemented' },
|
|
37
|
+
{ id: 'rbac', label: 'RBAC/ABAC', status: 'not-implemented' },
|
|
38
|
+
{ id: 'worker-heartbeat', label: 'Worker identity and heartbeat', status: 'implemented' },
|
|
39
|
+
{ id: 'isolated-workers', label: 'Isolated worker containers', status: 'not-implemented' },
|
|
40
|
+
{ id: 'signed-mcp-allowlists', label: 'Signed MCP allowlists', status: 'not-implemented' },
|
|
41
|
+
{ id: 'mandatory-audit', label: 'Mandatory audit log', status: 'implemented' },
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// Returns the capability list for the given mode, or an empty array for unknown modes.
|
|
46
|
+
|
|
47
|
+
export function getCapabilities(mode) {
|
|
48
|
+
return CAPABILITY_REGISTRY[mode] ?? [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Aggregates the capability list into a single readiness label.
|
|
52
|
+
// 'fully-implemented' — every capability is 'implemented'.
|
|
53
|
+
// 'degraded' — at least one is 'stub' (and none are 'not-implemented').
|
|
54
|
+
// 'unsupported' — at least one is 'not-implemented'.
|
|
55
|
+
|
|
56
|
+
export function getModeCapabilityStatus(mode) {
|
|
57
|
+
const caps = getCapabilities(mode);
|
|
58
|
+
if (caps.length === 0) return 'unsupported';
|
|
59
|
+
if (caps.some(c => c.status === 'not-implemented')) return 'unsupported';
|
|
60
|
+
if (caps.some(c => c.status === 'stub')) return 'degraded';
|
|
61
|
+
return 'fully-implemented';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Returns capabilities whose status is not 'implemented'.
|
|
65
|
+
|
|
66
|
+
export function getUnsupportedCapabilities(mode) {
|
|
67
|
+
return getCapabilities(mode).filter(c => c.status !== 'implemented');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// The three valid status values, as a constant so callers don't string-compare.
|
|
71
|
+
|
|
72
|
+
export const CAPABILITY_STATUSES = ['implemented', 'stub', 'not-implemented'];
|
|
73
|
+
|
|
74
|
+
// ADR-0057 (LMCP-A7) partitions enterprise capabilities beyond the three raw
|
|
75
|
+
// statuses above into four status-output categories: 'active' (implemented),
|
|
76
|
+
// 'fail-closed' (architecturally non-trivial — must hard-error rather than
|
|
77
|
+
// degrade silently), 'later' (no near-term implementation path, no runtime
|
|
78
|
+
// effect), and 'absent' (not advertised at all, the default for any id in
|
|
79
|
+
// neither set). construct status and construct doctor both key off this so a
|
|
80
|
+
// fail-closed gap is never visually or behaviorally confused with a later one.
|
|
81
|
+
|
|
82
|
+
export const ENTERPRISE_FAIL_CLOSED_IDS = new Set(['tenant-isolation', 'isolated-workers']);
|
|
83
|
+
export const ENTERPRISE_LATER_IDS = new Set(['rbac', 'signed-mcp-allowlists']);
|
|
84
|
+
|
|
85
|
+
export function categorizeEnterpriseCapability(cap) {
|
|
86
|
+
if (cap.status === 'implemented') return 'active';
|
|
87
|
+
if (ENTERPRISE_FAIL_CLOSED_IDS.has(cap.id)) return 'fail-closed';
|
|
88
|
+
if (ENTERPRISE_LATER_IDS.has(cap.id)) return 'later';
|
|
89
|
+
return 'absent';
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Test-only injection: temporarily append a capability to the given mode(s) and return a cleanup fn.
|
|
93
|
+
|
|
94
|
+
export function _injectCapabilityForTesting({ id, label = id, modes = {} }) {
|
|
95
|
+
const injected = [];
|
|
96
|
+
for (const [mode, status] of Object.entries(modes)) {
|
|
97
|
+
if (!CAPABILITY_REGISTRY[mode]) CAPABILITY_REGISTRY[mode] = [];
|
|
98
|
+
const entry = { id, label, status };
|
|
99
|
+
CAPABILITY_REGISTRY[mode].push(entry);
|
|
100
|
+
injected.push({ mode, entry });
|
|
101
|
+
}
|
|
102
|
+
return function cleanup() {
|
|
103
|
+
for (const { mode, entry } of injected) {
|
|
104
|
+
const arr = CAPABILITY_REGISTRY[mode];
|
|
105
|
+
const idx = arr.indexOf(entry);
|
|
106
|
+
if (idx !== -1) arr.splice(idx, 1);
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
}
|
package/lib/model-router.mjs
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import fs from "node:fs";
|
|
25
25
|
import path from "node:path";
|
|
26
|
+
import { fileURLToPath } from "node:url";
|
|
26
27
|
import { spawnSync } from "child_process";
|
|
27
28
|
import { hasAnySecret } from "./providers/secret-resolver.mjs";
|
|
28
29
|
import { isOllamaModelInstalled, toOllamaNativeModelId } from "./ollama/installed-models.mjs";
|
|
@@ -177,6 +178,21 @@ export function selectLocalEditorModel(candidates = []) {
|
|
|
177
178
|
* - `resolve`: Function that maps selected tier values to concrete model IDs.
|
|
178
179
|
*
|
|
179
180
|
* Families are consulted in order; the first match wins.
|
|
181
|
+
*
|
|
182
|
+
* FALLBACK CATALOG ONLY (construct-at27): these dated ids are the last-resort
|
|
183
|
+
* defaults when a tier is unconfigured — runtime call sites must prefer the
|
|
184
|
+
* user's configured/selected models and degrade gracefully when a catalog id
|
|
185
|
+
* has been retired by the provider; never treat an entry here as guaranteed
|
|
186
|
+
* to exist.
|
|
187
|
+
*
|
|
188
|
+
* OpenRouter's free-tier rate limit is enforced per account across every
|
|
189
|
+
* `:free`-suffixed model, not per model id — pinning a different `:free`
|
|
190
|
+
* model does not avoid a 429 by itself. The `:free` ids below are
|
|
191
|
+
* cross-checked against a live `GET https://openrouter.ai/api/v1/models`
|
|
192
|
+
* response (construct-r7bp) so at least one alternate candidate remains
|
|
193
|
+
* live if a specific model is retired or under its own provider-side quota;
|
|
194
|
+
* durable retry/fallback handling of the shared account limit is
|
|
195
|
+
* construct-5wkl's scope, not this catalog.
|
|
180
196
|
*/
|
|
181
197
|
const PROVIDER_FAMILY_TIERS = [
|
|
182
198
|
{
|
|
@@ -236,7 +252,7 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
236
252
|
options: {
|
|
237
253
|
reasoning: ["openrouter/deepseek/deepseek-r1", "openrouter/deepseek/deepseek-v3"],
|
|
238
254
|
standard: ["openrouter/deepseek/deepseek-v3", "openrouter/deepseek/deepseek-r1"],
|
|
239
|
-
fast: ["openrouter/qwen/qwen3-coder:free", "openrouter/deepseek/deepseek-v3"],
|
|
255
|
+
fast: ["openrouter/qwen/qwen3-coder:free", "openrouter/meta-llama/llama-3.3-70b-instruct:free", "openrouter/deepseek/deepseek-v3"],
|
|
240
256
|
},
|
|
241
257
|
},
|
|
242
258
|
{
|
|
@@ -246,12 +262,12 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
246
262
|
resolve: ({ reasoning, standard, fast }) => ({
|
|
247
263
|
reasoning: reasoning ?? "openrouter/qwen/qwen3-coder",
|
|
248
264
|
standard: standard ?? "openrouter/qwen/qwen3-coder:free",
|
|
249
|
-
fast: fast ?? "openrouter/qwen/
|
|
265
|
+
fast: fast ?? "openrouter/qwen/qwen-2.5-coder-32b-instruct",
|
|
250
266
|
}),
|
|
251
267
|
options: {
|
|
252
268
|
reasoning: ["openrouter/qwen/qwen3-coder", "openrouter/qwen/qwen3-coder:free"],
|
|
253
|
-
standard: ["openrouter/qwen/qwen3-coder:free", "openrouter/qwen/qwen3-coder"],
|
|
254
|
-
fast: ["openrouter/qwen/
|
|
269
|
+
standard: ["openrouter/qwen/qwen3-coder:free", "openrouter/qwen/qwen3-next-80b-a3b-instruct:free", "openrouter/qwen/qwen3-coder"],
|
|
270
|
+
fast: ["openrouter/qwen/qwen-2.5-coder-32b-instruct", "openrouter/qwen/qwen3-coder:free"],
|
|
255
271
|
},
|
|
256
272
|
},
|
|
257
273
|
{
|
|
@@ -313,7 +329,7 @@ const PROVIDER_FAMILY_TIERS = [
|
|
|
313
329
|
}),
|
|
314
330
|
options: {
|
|
315
331
|
reasoning: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder", "openrouter/deepseek/deepseek-r1"],
|
|
316
|
-
standard: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/google/gemini-2.0-flash-001"],
|
|
332
|
+
standard: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/openai/gpt-oss-120b:free", "openrouter/google/gemini-2.0-flash-001"],
|
|
317
333
|
fast: ["openrouter/openrouter/free", "openrouter/qwen/qwen3-coder:free", "openrouter/meta-llama/llama-3.3-70b-instruct:free"],
|
|
318
334
|
},
|
|
319
335
|
},
|
|
@@ -413,8 +429,10 @@ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
|
|
|
413
429
|
const varNames = PROVIDER_ENV_MAP[familyId];
|
|
414
430
|
if (!varNames?.length) return false;
|
|
415
431
|
|
|
416
|
-
// Special case: Ollama on default port (no OLLAMA_BASE_URL needed)
|
|
417
|
-
|
|
432
|
+
// Special case: Ollama on default port (no OLLAMA_BASE_URL needed). Ambient-gated:
|
|
433
|
+
// a hermetic caller (allowAmbient: false — functional tests, credential-fallback
|
|
434
|
+
// resolution against a synthetic env) must not probe the machine's local daemon.
|
|
435
|
+
if (familyId === 'ollama' && allowAmbient) {
|
|
418
436
|
try {
|
|
419
437
|
const r = spawnSync('curl', ['-s', '--connect-timeout', '1', '-o', '/dev/null', '-w', '%{http_code}', 'http://localhost:11434/api/tags'], { encoding: 'utf8', timeout: 3000 });
|
|
420
438
|
if (r.status === 0 && r.stdout?.trim() === '200') return true;
|
|
@@ -431,8 +449,9 @@ function isProviderConfigured(familyId, env, { allowAmbient = true } = {}) {
|
|
|
431
449
|
if (hasAnySecret(varNames, { env, allowAmbient })) return true;
|
|
432
450
|
|
|
433
451
|
// GitHub Copilot authenticates via OAuth rather than an API key: a stored
|
|
434
|
-
// device-flow credential, or a gh CLI session as a fallback, both count.
|
|
435
|
-
|
|
452
|
+
// device-flow credential, or a gh CLI session as a fallback, both count. Both
|
|
453
|
+
// checks read machine-local state outside `env`, so they are ambient-gated too.
|
|
454
|
+
if (familyId === 'github-copilot' && allowAmbient) {
|
|
436
455
|
if (hasCopilotCredential()) return true;
|
|
437
456
|
try {
|
|
438
457
|
const r = spawnSync('gh', ['auth', 'status'], { encoding: 'utf8', timeout: 3000 });
|
|
@@ -1282,6 +1301,20 @@ export function setModelWithTierInference(envPath, tier, modelId, registryModels
|
|
|
1282
1301
|
return resolved;
|
|
1283
1302
|
}
|
|
1284
1303
|
|
|
1304
|
+
// Optional operator-supplied tier defaults, resolved under the toolkit dir
|
|
1305
|
+
// (CX_TOOLKIT_DIR wins, else the installed package root). No active registry
|
|
1306
|
+
// ships — a starter template lives at specialists/org/models.json.example — so
|
|
1307
|
+
// an unconfigured machine degrades honestly (no implicit model defaults) rather
|
|
1308
|
+
// than resolving a model whose credentials may be absent. A dropped-in
|
|
1309
|
+
// models.json supplies registry-sourced tier defaults that an env pin overrides.
|
|
1310
|
+
|
|
1311
|
+
const MODEL_REGISTRY_MODULE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
1312
|
+
|
|
1313
|
+
export function defaultModelRegistryPath(env = process.env) {
|
|
1314
|
+
const root = env.CX_TOOLKIT_DIR || MODEL_REGISTRY_MODULE_ROOT;
|
|
1315
|
+
return path.join(root, "specialists", "org", "models.json");
|
|
1316
|
+
}
|
|
1317
|
+
|
|
1285
1318
|
export function resolveModelTiers(options = {}) {
|
|
1286
1319
|
const {
|
|
1287
1320
|
env = process.env,
|
package/lib/models/catalog.mjs
CHANGED
|
@@ -11,9 +11,10 @@ import { loadRegistry } from '../registry/loader.mjs';
|
|
|
11
11
|
import path from 'node:path';
|
|
12
12
|
import os from 'node:os';
|
|
13
13
|
import { pollFreeModels } from '../model-free-selector.mjs';
|
|
14
|
-
import { resolveFirstSecret } from '../providers/secret-resolver.mjs';
|
|
14
|
+
import { resolveFirstSecret, hasAnySecret } from '../providers/secret-resolver.mjs';
|
|
15
15
|
import { loadProjectConfig } from '../config/project-config.mjs';
|
|
16
16
|
import { doctorRoot } from '../config/xdg.mjs';
|
|
17
|
+
import { loadManifestsFromDir, mergeManifests, resolveManifestDirs } from '../extensions/loader.mjs';
|
|
17
18
|
|
|
18
19
|
export const MODEL_VISIBILITY_MODES = ['all_configured', 'tier_defaults', 'explicit'];
|
|
19
20
|
|
|
@@ -210,6 +211,44 @@ export function loadModelsCatalogContext({ cwd = process.cwd(), env = process.en
|
|
|
210
211
|
return { modelsConfig, registryModels, liveModels };
|
|
211
212
|
}
|
|
212
213
|
|
|
214
|
+
// Bridge onto the unified extension registry (LMCP-B1/B4): model provider
|
|
215
|
+
// families register `kind: 'model'` manifests under lib/extensions/manifests/
|
|
216
|
+
// alongside data-source/queue/storage manifests, so `construct provider list`
|
|
217
|
+
// can enumerate them through the same substrate. This is additive — it does
|
|
218
|
+
// not replace PROVIDER_FAMILY_TIERS (lib/model-router.mjs), which still owns
|
|
219
|
+
// tier resolution, live polling, and visibility filtering above.
|
|
220
|
+
|
|
221
|
+
export const MODEL_MANIFEST_KIND = 'model';
|
|
222
|
+
|
|
223
|
+
export function listModelProviderManifests({ rootDir = process.cwd(), homeDir } = {}) {
|
|
224
|
+
const dirs = resolveManifestDirs({ rootDir, homeDir });
|
|
225
|
+
const builtin = loadManifestsFromDir(dirs.builtin).manifests;
|
|
226
|
+
const user = loadManifestsFromDir(dirs.user).manifests;
|
|
227
|
+
const project = loadManifestsFromDir(dirs.project).manifests;
|
|
228
|
+
return mergeManifests(builtin, user, project).filter((m) => m.kind === MODEL_MANIFEST_KIND);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function describeModelProviders({ rootDir = process.cwd(), homeDir, env = process.env, allowAmbient = true } = {}) {
|
|
232
|
+
return listModelProviderManifests({ rootDir, homeDir }).map((manifest) => {
|
|
233
|
+
const secretEnvKeys = Array.isArray(manifest.secretEnvKeys) ? manifest.secretEnvKeys : [];
|
|
234
|
+
const configured = secretEnvKeys.length === 0 || hasAnySecret(secretEnvKeys, { env, allowAmbient });
|
|
235
|
+
return {
|
|
236
|
+
id: manifest.id,
|
|
237
|
+
displayName: manifest.docs || manifest.id,
|
|
238
|
+
description: manifest.docs || null,
|
|
239
|
+
capabilities: Array.isArray(manifest.capabilities) ? manifest.capabilities : [],
|
|
240
|
+
kind: manifest.kind,
|
|
241
|
+
source: 'model-catalog',
|
|
242
|
+
health: {
|
|
243
|
+
ok: configured,
|
|
244
|
+
detail: configured
|
|
245
|
+
? 'credentials configured'
|
|
246
|
+
: `missing env: ${secretEnvKeys.join(', ') || 'unknown'}`,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
|
|
213
252
|
export function isModelVisible(modelId, {
|
|
214
253
|
visibility = DEFAULT_MODELS_CONFIG.visibility,
|
|
215
254
|
registryModels = {},
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lib/net-guard.mjs — shared SSRF / DNS-rebinding egress guard (LMCP-N7).
|
|
3
|
+
*
|
|
4
|
+
* Every outbound HTTP call Construct makes on behalf of a manifest, provider,
|
|
5
|
+
* or url-type MCP entry is attacker-influenced in its destination: a malicious
|
|
6
|
+
* manifest URL or a rebinding DNS record can point an otherwise-trusted call at
|
|
7
|
+
* a loopback or private-range service and pivot into the host. This guard is
|
|
8
|
+
* the single chokepoint that prevents that.
|
|
9
|
+
*
|
|
10
|
+
* Two properties, enforced together:
|
|
11
|
+
* 1. Address policy — the destination hostname is resolved and EVERY returned
|
|
12
|
+
* address is classified; loopback, private, link-local (incl. cloud
|
|
13
|
+
* metadata 169.254.169.254), CGNAT, and unspecified ranges are denied by
|
|
14
|
+
* default. `allowPrivate: true` is the explicit, audited opt-out for
|
|
15
|
+
* on-host/dev use.
|
|
16
|
+
* 2. Rebinding pinning — the address validated at check time is the exact
|
|
17
|
+
* address the socket connects to. `resolveGuardedTarget` resolves ONCE and
|
|
18
|
+
* returns a pinned IP; `guardedFetch` connects to that pinned IP with the
|
|
19
|
+
* original hostname preserved as TLS servername + Host header, so a DNS
|
|
20
|
+
* record that flips to a private address between check and connect can
|
|
21
|
+
* never move the connection — the socket never re-resolves.
|
|
22
|
+
*
|
|
23
|
+
* The security decision lives entirely in the pure, network-free
|
|
24
|
+
* `classifyAddress` / `resolveGuardedTarget` (DNS is injectable), so the policy
|
|
25
|
+
* is unit-testable without a socket; `guardedFetch` is the thin I/O shell.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { request as httpsRequest } from 'node:https';
|
|
29
|
+
import { request as httpRequest } from 'node:http';
|
|
30
|
+
import { lookup as dnsLookup } from 'node:dns';
|
|
31
|
+
import { isIP } from 'node:net';
|
|
32
|
+
import { promisify } from 'node:util';
|
|
33
|
+
import { gunzipSync, inflateSync, brotliDecompressSync } from 'node:zlib';
|
|
34
|
+
|
|
35
|
+
const dnsLookupAll = promisify(dnsLookup);
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_ALLOWED_SCHEMES = Object.freeze(['https:', 'http:']);
|
|
38
|
+
|
|
39
|
+
/** Named egress-policy rejection; `.reason` is a stable machine tag. */
|
|
40
|
+
export class SsrfBlockedError extends Error {
|
|
41
|
+
constructor(message, reason) {
|
|
42
|
+
super(message);
|
|
43
|
+
this.name = 'SsrfBlockedError';
|
|
44
|
+
this.reason = reason;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function ipv4ToInt(ip) {
|
|
49
|
+
const parts = ip.split('.').map((n) => Number(n));
|
|
50
|
+
if (parts.length !== 4 || parts.some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return null;
|
|
51
|
+
return ((parts[0] << 24) >>> 0) + (parts[1] << 16) + (parts[2] << 8) + parts[3];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function inCidr4(ipInt, baseIp, prefix) {
|
|
55
|
+
const base = ipv4ToInt(baseIp);
|
|
56
|
+
const mask = prefix === 0 ? 0 : (0xffffffff << (32 - prefix)) >>> 0;
|
|
57
|
+
return (ipInt & mask) === (base & mask);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const BLOCKED_V4 = [
|
|
61
|
+
['0.0.0.0', 8, 'unspecified'],
|
|
62
|
+
['10.0.0.0', 8, 'private-address'],
|
|
63
|
+
['100.64.0.0', 10, 'cgnat'],
|
|
64
|
+
['127.0.0.0', 8, 'loopback'],
|
|
65
|
+
['169.254.0.0', 16, 'link-local'],
|
|
66
|
+
['172.16.0.0', 12, 'private-address'],
|
|
67
|
+
['192.0.0.0', 24, 'ietf-protocol'],
|
|
68
|
+
['192.168.0.0', 16, 'private-address'],
|
|
69
|
+
['198.18.0.0', 15, 'benchmark'],
|
|
70
|
+
['224.0.0.0', 4, 'multicast'],
|
|
71
|
+
['240.0.0.0', 4, 'reserved'],
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Classify a raw IP literal. Returns a stable reason tag when the address is in
|
|
76
|
+
* a range that must not be reached from an attacker-influenced URL, or null
|
|
77
|
+
* when the address is a routable public address.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} ip
|
|
80
|
+
* @returns {string|null}
|
|
81
|
+
*/
|
|
82
|
+
export function classifyAddress(ip) {
|
|
83
|
+
const family = isIP(ip);
|
|
84
|
+
if (family === 4) {
|
|
85
|
+
const asInt = ipv4ToInt(ip);
|
|
86
|
+
if (asInt === null) return 'unparseable';
|
|
87
|
+
for (const [base, prefix, reason] of BLOCKED_V4) {
|
|
88
|
+
if (inCidr4(asInt, base, prefix)) return reason;
|
|
89
|
+
}
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
if (family === 6) {
|
|
93
|
+
const norm = ip.toLowerCase().replace(/^\[|\]$/g, '');
|
|
94
|
+
if (norm === '::1') return 'loopback';
|
|
95
|
+
if (norm === '::' ) return 'unspecified';
|
|
96
|
+
if (norm.startsWith('fe80')) return 'link-local';
|
|
97
|
+
if (norm.startsWith('fc') || norm.startsWith('fd')) return 'unique-local';
|
|
98
|
+
// IPv4-mapped (::ffff:a.b.c.d) is classified on its embedded v4 address.
|
|
99
|
+
const mapped = norm.match(/::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
100
|
+
if (mapped) return classifyAddress(mapped[1]);
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
return 'not-an-ip';
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Validate a URL's scheme + host and resolve its hostname to a single pinned,
|
|
108
|
+
* policy-checked address. Pure with respect to the network: DNS is injected via
|
|
109
|
+
* `lookup`. Throws SsrfBlockedError on any violation.
|
|
110
|
+
*
|
|
111
|
+
* @param {string} rawUrl
|
|
112
|
+
* @param {object} [opts]
|
|
113
|
+
* @param {(hostname: string) => Promise<Array<{address: string, family: number}>>} [opts.lookup]
|
|
114
|
+
* @param {boolean} [opts.allowPrivate] - permit private/loopback ranges (audited opt-out)
|
|
115
|
+
* @param {string[]} [opts.allowedSchemes]
|
|
116
|
+
* @returns {Promise<{ url: URL, pinnedAddress: string, family: number, port: number, servername: string }>}
|
|
117
|
+
*/
|
|
118
|
+
export async function resolveGuardedTarget(rawUrl, opts = {}) {
|
|
119
|
+
const { allowPrivate = false, allowedSchemes = DEFAULT_ALLOWED_SCHEMES } = opts;
|
|
120
|
+
const lookup = opts.lookup ?? ((hostname) => dnsLookupAll(hostname, { all: true }));
|
|
121
|
+
|
|
122
|
+
let url;
|
|
123
|
+
try {
|
|
124
|
+
url = new URL(rawUrl);
|
|
125
|
+
} catch {
|
|
126
|
+
throw new SsrfBlockedError(`net-guard: not a valid URL: ${String(rawUrl).slice(0, 120)}`, 'invalid-url');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (!allowedSchemes.includes(url.protocol)) {
|
|
130
|
+
throw new SsrfBlockedError(`net-guard: scheme '${url.protocol}' is not allowed`, 'blocked-scheme');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const hostname = url.hostname.replace(/^\[|\]$/g, '');
|
|
134
|
+
|
|
135
|
+
// A URL that already carries an IP literal skips DNS but is still classified —
|
|
136
|
+
// http://127.0.0.1 must be blocked exactly like a hostname resolving to it.
|
|
137
|
+
const literalFamily = isIP(hostname);
|
|
138
|
+
let addresses;
|
|
139
|
+
if (literalFamily) {
|
|
140
|
+
addresses = [{ address: hostname, family: literalFamily }];
|
|
141
|
+
} else {
|
|
142
|
+
addresses = await lookup(hostname);
|
|
143
|
+
if (!Array.isArray(addresses) || addresses.length === 0) {
|
|
144
|
+
throw new SsrfBlockedError(`net-guard: '${hostname}' did not resolve to any address`, 'no-address');
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ALL resolved addresses must pass — a hostname that returns one public and
|
|
149
|
+
// one private address is rejected, never partially trusted.
|
|
150
|
+
for (const { address } of addresses) {
|
|
151
|
+
if (allowPrivate) continue;
|
|
152
|
+
const reason = classifyAddress(address);
|
|
153
|
+
if (reason) {
|
|
154
|
+
throw new SsrfBlockedError(`net-guard: '${hostname}' resolves to a blocked address (${address}: ${reason})`, reason);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const pinned = addresses[0];
|
|
159
|
+
const port = url.port ? Number(url.port) : (url.protocol === 'https:' ? 443 : 80);
|
|
160
|
+
return { url, pinnedAddress: pinned.address, family: pinned.family, port, servername: hostname };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function decodeBody(buffer, encoding) {
|
|
164
|
+
if (!buffer.length) return buffer;
|
|
165
|
+
try {
|
|
166
|
+
if (encoding === 'gzip') return gunzipSync(buffer);
|
|
167
|
+
if (encoding === 'deflate') return inflateSync(buffer);
|
|
168
|
+
if (encoding === 'br') return brotliDecompressSync(buffer);
|
|
169
|
+
} catch {
|
|
170
|
+
return buffer;
|
|
171
|
+
}
|
|
172
|
+
return buffer;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function makeResponse({ statusCode, headers, bodyBuffer, url }) {
|
|
176
|
+
const decoded = decodeBody(bodyBuffer, String(headers['content-encoding'] ?? '').toLowerCase());
|
|
177
|
+
const headerMap = new Map(Object.entries(headers).map(([k, v]) => [k.toLowerCase(), Array.isArray(v) ? v.join(', ') : v]));
|
|
178
|
+
return {
|
|
179
|
+
status: statusCode,
|
|
180
|
+
ok: statusCode >= 200 && statusCode < 300,
|
|
181
|
+
url,
|
|
182
|
+
headers: { get: (name) => headerMap.get(String(name).toLowerCase()) ?? null },
|
|
183
|
+
async text() { return decoded.toString('utf8'); },
|
|
184
|
+
async json() { return JSON.parse(decoded.toString('utf8') || 'null'); },
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* fetch-compatible egress call that enforces the guard and connects to the
|
|
190
|
+
* pinned, validated address (rebinding-proof). Follows redirects manually,
|
|
191
|
+
* re-validating every hop through the same guard. Returns a minimal Response
|
|
192
|
+
* ({ status, ok, headers.get, text, json }) covering what the provider
|
|
193
|
+
* transports use.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} rawUrl
|
|
196
|
+
* @param {{ method?: string, headers?: Record<string,string>, body?: string }} [init]
|
|
197
|
+
* @param {object} [opts] - forwarded to resolveGuardedTarget, plus maxRedirects
|
|
198
|
+
* @returns {Promise<object>}
|
|
199
|
+
*/
|
|
200
|
+
export async function guardedFetch(rawUrl, init = {}, opts = {}) {
|
|
201
|
+
const maxRedirects = opts.maxRedirects ?? 5;
|
|
202
|
+
let currentUrl = rawUrl;
|
|
203
|
+
|
|
204
|
+
for (let hop = 0; hop <= maxRedirects; hop++) {
|
|
205
|
+
const target = await resolveGuardedTarget(currentUrl, opts);
|
|
206
|
+
const res = await requestPinned(target, init);
|
|
207
|
+
|
|
208
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
209
|
+
currentUrl = new URL(res.headers.location, target.url).href;
|
|
210
|
+
if (res.statusCode === 303) { init = { ...init, method: 'GET', body: undefined }; }
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
return makeResponse({ statusCode: res.statusCode, headers: res.headers, bodyBuffer: res.bodyBuffer, url: target.url.href });
|
|
214
|
+
}
|
|
215
|
+
throw new SsrfBlockedError(`net-guard: exceeded ${maxRedirects} redirects for ${rawUrl}`, 'too-many-redirects');
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function requestPinned(target, init) {
|
|
219
|
+
const { url, pinnedAddress, family, port, servername } = target;
|
|
220
|
+
const doRequest = url.protocol === 'https:' ? httpsRequest : httpRequest;
|
|
221
|
+
|
|
222
|
+
// The socket connects to the pinned IP; the hostname survives only as TLS
|
|
223
|
+
// servername + Host header. The custom lookup guarantees the pinned address
|
|
224
|
+
// is the only one the agent can dial — DNS is never consulted again here.
|
|
225
|
+
const pinnedLookup = (_hostname, _options, cb) => cb(null, pinnedAddress, family);
|
|
226
|
+
|
|
227
|
+
const options = {
|
|
228
|
+
method: init.method ?? 'GET',
|
|
229
|
+
host: servername,
|
|
230
|
+
port,
|
|
231
|
+
path: `${url.pathname}${url.search}`,
|
|
232
|
+
headers: { Host: url.host, ...(init.headers ?? {}) },
|
|
233
|
+
servername,
|
|
234
|
+
lookup: pinnedLookup,
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
return new Promise((resolve, reject) => {
|
|
238
|
+
const req = doRequest(options, (res) => {
|
|
239
|
+
const chunks = [];
|
|
240
|
+
res.on('data', (c) => chunks.push(c));
|
|
241
|
+
res.on('end', () => resolve({ statusCode: res.statusCode, headers: res.headers, bodyBuffer: Buffer.concat(chunks) }));
|
|
242
|
+
});
|
|
243
|
+
req.on('error', reject);
|
|
244
|
+
if (init.body) req.write(init.body);
|
|
245
|
+
req.end();
|
|
246
|
+
});
|
|
247
|
+
}
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
* Storage layout:
|
|
10
10
|
* .cx/observations/index.json — lightweight listing for fast filtering
|
|
11
11
|
* .cx/observations/<id>.json — full observation record
|
|
12
|
+
* <stateRoot>/lancedb — vector index (ADR-0066: machine-scoped,
|
|
13
|
+
* not project-local); the JSON records
|
|
14
|
+
* above remain project-local for now
|
|
12
15
|
*/
|
|
13
16
|
import crypto from 'node:crypto';
|
|
14
17
|
import fs from 'node:fs';
|
|
@@ -16,6 +19,7 @@ import path from 'node:path';
|
|
|
16
19
|
import { embedText as embedTextEngine } from './storage/embeddings-engine.mjs';
|
|
17
20
|
import { VectorClient } from './storage/vector-client.mjs';
|
|
18
21
|
import { ensureCxDir } from './project-init-shared.mjs';
|
|
22
|
+
import { resolveStateDir } from './state-root.mjs';
|
|
19
23
|
|
|
20
24
|
const OBS_DIR = '.cx/observations';
|
|
21
25
|
const INDEX_FILE = 'index.json';
|
|
@@ -31,7 +35,7 @@ const MAX_INDEX = 1000;
|
|
|
31
35
|
function vectorClientFor(rootDir) {
|
|
32
36
|
const env = process.env.CONSTRUCT_LANCEDB_PATH
|
|
33
37
|
? process.env
|
|
34
|
-
: { ...process.env, CONSTRUCT_LANCEDB_PATH:
|
|
38
|
+
: { ...process.env, CONSTRUCT_LANCEDB_PATH: resolveStateDir(rootDir, 'lancedb', { ensureDir: false }) };
|
|
35
39
|
return new VectorClient({ env });
|
|
36
40
|
}
|
|
37
41
|
|
|
@@ -111,7 +115,7 @@ function logCapDrop(rootDir, kind, dropped, total) {
|
|
|
111
115
|
dropped,
|
|
112
116
|
total,
|
|
113
117
|
cap: MAX_INDEX,
|
|
114
|
-
remedy: '
|
|
118
|
+
remedy: "Run 'construct memory consolidate' to compact stored observations.",
|
|
115
119
|
});
|
|
116
120
|
fs.appendFileSync(logPath, entry + '\n');
|
|
117
121
|
} catch { /* cap warnings are best-effort */ }
|
|
@@ -18,6 +18,7 @@ import { spawnSync } from "node:child_process";
|
|
|
18
18
|
import { mkdtempSync, writeFileSync, rmSync } from "node:fs";
|
|
19
19
|
import { tmpdir } from "node:os";
|
|
20
20
|
import { join } from "node:path";
|
|
21
|
+
import { isMainModule } from '../roots.mjs';
|
|
21
22
|
|
|
22
23
|
const DEFAULT_NUM_CTX = 32768;
|
|
23
24
|
const CHATML_STOPS = ["<|im_start|>", "<|im_end|>", "<|endoftext|>"];
|
|
@@ -325,7 +326,7 @@ export async function probeStreamingToolCallLeak(model, { baseURL = "http://127.
|
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
|
328
|
-
if (import.meta.url
|
|
329
|
+
if (isMainModule(import.meta.url)) {
|
|
329
330
|
if (process.argv.includes("--probe")) {
|
|
330
331
|
const model = process.argv.find((a) => a.startsWith("--model="))?.split("=")[1];
|
|
331
332
|
const targets = model ? [model] : listModels();
|
package/lib/opencode-config.mjs
CHANGED
|
@@ -175,10 +175,20 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
|
|
|
175
175
|
if (sessions.includes('github-copilot')) {
|
|
176
176
|
const envValues = parseEnvFile(getUserEnvPath());
|
|
177
177
|
const port = envValues.COPILOT_BRIDGE_PORT || '5174';
|
|
178
|
-
|
|
178
|
+
const provider = config.provider['github-copilot'] || {
|
|
179
179
|
baseUrl: `http://localhost:${port}/v1`,
|
|
180
|
-
apiKey: "noop" //
|
|
180
|
+
apiKey: "noop" // Copilot itself is authed via gh CLI inside the bridge; this field is unused
|
|
181
181
|
};
|
|
182
|
+
|
|
183
|
+
// The bridge now requires a per-launch bearer token. Wire it as an {env:} reference
|
|
184
|
+
// so OpenCode resolves it from its own env at request time and the token is never
|
|
185
|
+
// written into opencode.json in the clear.
|
|
186
|
+
if (envValues.CONSTRUCT_COPILOT_BRIDGE_TOKEN) {
|
|
187
|
+
provider.options = provider.options || {};
|
|
188
|
+
provider.options.headers = provider.options.headers || {};
|
|
189
|
+
provider.options.headers.Authorization = '{env:CONSTRUCT_COPILOT_BRIDGE_TOKEN}';
|
|
190
|
+
}
|
|
191
|
+
config.provider['github-copilot'] = provider;
|
|
182
192
|
}
|
|
183
193
|
}
|
|
184
194
|
|
|
@@ -259,6 +269,15 @@ export function writeOpenCodeConfig(config, file = findOpenCodeConfigPath()) {
|
|
|
259
269
|
});
|
|
260
270
|
}
|
|
261
271
|
|
|
262
|
-
fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, "utf8");
|
|
272
|
+
fs.writeFileSync(target, `${JSON.stringify(sanitizeOpenCodeConfig(config), null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
273
|
+
|
|
274
|
+
// opencode.json can carry a resolved provider apiKey or Authorization header.
|
|
275
|
+
// writeFileSync keeps an existing inode's mode, so the mode option above does not
|
|
276
|
+
// tighten a file an earlier write left 0644; re-apply 0600 on every write, matching
|
|
277
|
+
// lib/env-config.mjs's writeEnvValues contract for config.env.
|
|
278
|
+
|
|
279
|
+
if (process.platform !== "win32") {
|
|
280
|
+
try { fs.chmodSync(target, 0o600); } catch { /* exotic filesystem */ }
|
|
281
|
+
}
|
|
263
282
|
return target;
|
|
264
283
|
}
|