@starci/hfs 4.7.32 → 4.7.33
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/CHANGELOG.md +9 -0
- package/README.md +13 -6
- package/emit/contracts.mjs +4 -3
- package/emit/form-bounds-reader.ts.partial +58 -0
- package/emit/form-bounds.mjs +155 -0
- package/emit/graphql-input-bounds.mjs +97 -0
- package/emit/input-bounds.mjs +44 -0
- package/emit/operation-schemas.mjs +137 -0
- package/emit/operations-worker.mjs +2 -2
- package/emit/operations.mjs +52 -16
- package/emit/schema-worker.mjs +17 -4
- package/emit/type-schema.mjs +17 -5
- package/package.json +9 -2
- package/release/gate.mjs +18 -23
- package/release/index.mjs +1 -1
- package/release/push.mjs +4 -5
- package/runtime/engine/admission.mjs +331 -0
- package/runtime/engine/canonical-json.mjs +14 -0
- package/runtime/engine/db/blob.mjs +380 -0
- package/runtime/engine/db/event-compact.mjs +73 -0
- package/runtime/engine/db/ledger-open.mjs +38 -0
- package/runtime/engine/db/ledger-paths.mjs +87 -0
- package/runtime/engine/db/ledger.mjs +1068 -0
- package/runtime/engine/db/machine-connection.mjs +150 -0
- package/runtime/engine/db/machine-open.mjs +60 -0
- package/runtime/engine/db/machine-schema.mjs +105 -0
- package/runtime/engine/db/machine.mjs +1317 -0
- package/runtime/engine/db/provider-reservations.mjs +127 -0
- package/runtime/engine/db/provision-authority.mjs +76 -0
- package/runtime/engine/db/provision-lock.mjs +29 -0
- package/runtime/engine/db/provision-operations.mjs +146 -0
- package/runtime/engine/db/ref-value.mjs +67 -0
- package/runtime/engine/db/schema/machine.sql +920 -0
- package/runtime/engine/db/schema/runtime.sql +1113 -0
- package/runtime/engine/refuse.mjs +11 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +65 -9
- package/runtime/knowledge/hfs/design-truths.yaml +149 -0
- package/runtime/knowledge/hfs/fe-stack.yaml +23 -5
- package/runtime/knowledge/hfs/rules.yaml +269 -48
- package/runtime/knowledge/hfs/slots.yaml +113 -53
- package/runtime/knowledge/patterns/be/api.yaml +46 -20
- package/runtime/knowledge/patterns/be/event-bus.yaml +19 -12
- package/runtime/knowledge/patterns/be/idempotency.yaml +265 -0
- package/runtime/knowledge/patterns/be/jobs.yaml +17 -12
- package/runtime/knowledge/patterns/be/projections.yaml +2 -2
- package/runtime/knowledge/patterns/be/queues.yaml +57 -22
- package/runtime/knowledge/patterns/be/reactors.yaml +1 -1
- package/runtime/knowledge/patterns/be/realtime.yaml +326 -27
- package/runtime/knowledge/patterns/be/saga.yaml +2 -2
- package/runtime/knowledge/sonar-gate.yaml +1 -1
- package/runtime/modules/kernel/command-policy.yaml +249 -0
- package/runtime/modules/kernel/failure-codes.yaml +211 -52
- package/runtime/modules/kernel/hook-input.yaml +6 -0
- package/runtime/modules/kernel/protected-zone.yaml +81 -0
- package/runtime/modules/kernel/removed-vocabulary.yaml +62 -0
- package/runtime/modules/kernel/roles.yaml +405 -0
- package/runtime/modules/models/registry.yaml +3 -1
- package/runtime/modules/models/runtimes.yaml +2 -0
- package/runtime/modules/schemas/application-stacks.schema.yaml +10 -2
- package/runtime/packages/cli/src/catalog.generated.mjs +46 -59
- package/runtime/packages/grammar/scripts/build-stamp.mjs +98 -0
- package/runtime/scripts/agent/codex-app-server.mjs +79 -0
- package/runtime/scripts/agent/codex-mirror-source.mjs +34 -0
- package/runtime/scripts/agent/host-agents.mjs +80 -0
- package/runtime/scripts/agent/launch-trust-policy.mjs +53 -0
- package/runtime/scripts/agent/trust-launch.mjs +191 -0
- package/runtime/scripts/agent/trust.mjs +523 -0
- package/runtime/scripts/api/fs/rename-over.mjs +21 -0
- package/runtime/scripts/api/fs/temp-path.mjs +8 -0
- package/runtime/scripts/api/fs/write-json-file.mjs +10 -0
- package/runtime/scripts/api/git/diff-names.mjs +8 -0
- package/runtime/scripts/api/git/head-time.mjs +10 -0
- package/runtime/scripts/api/git/index-lock.mjs +78 -0
- package/runtime/scripts/api/git/is-ancestor.mjs +5 -0
- package/runtime/scripts/api/git/log.mjs +5 -0
- package/runtime/scripts/api/git/merge-base-query.mjs +5 -0
- package/runtime/scripts/api/git/merge-tree.mjs +5 -0
- package/runtime/scripts/api/git/rev-list.mjs +5 -0
- package/runtime/scripts/api/git/rev-parse.mjs +13 -0
- package/runtime/scripts/api/git/status-query.mjs +5 -0
- package/runtime/scripts/api/git/worktree-list-query.mjs +5 -0
- package/runtime/scripts/api/node/spawn-node.mjs +6 -0
- package/runtime/scripts/api/npm/dist-tag.mjs +8 -0
- package/runtime/scripts/api/npm/lib.mjs +32 -0
- package/runtime/scripts/api/npm/pack-dry-run.mjs +9 -0
- package/runtime/scripts/api/npm/run-npm.mjs +6 -0
- package/runtime/scripts/api/npm/view.mjs +30 -0
- package/runtime/scripts/api/npm/whoami.mjs +8 -0
- package/runtime/scripts/api/process/hide-child-windows.mjs +31 -0
- package/runtime/scripts/api/process/kill-tree.mjs +22 -0
- package/runtime/scripts/api/process/lib.mjs +125 -0
- package/runtime/scripts/api/process/probe-guard-command.mjs +74 -0
- package/runtime/scripts/api/process/process-list.mjs +28 -0
- package/runtime/scripts/api/process/system-tool.mjs +37 -0
- package/runtime/scripts/gates/canon-digest.mjs +81 -0
- package/runtime/scripts/gates/canon-pins.mjs +16 -0
- package/runtime/scripts/gates/commit-proof.mjs +53 -0
- package/runtime/scripts/gates/gate-input.mjs +98 -0
- package/runtime/scripts/gates/gate.mjs +669 -0
- package/runtime/scripts/gates/grammar-dist.mjs +206 -0
- package/runtime/scripts/gates/runtime-package-selection.mjs +27 -0
- package/runtime/scripts/gates/test-world-run.mjs +254 -0
- package/runtime/scripts/gates/type-impact.mjs +277 -0
- package/runtime/scripts/gates/type-program.mjs +113 -0
- package/runtime/scripts/guards/assigned-command.mjs +12 -0
- package/runtime/scripts/guards/call-rights.mjs +47 -0
- package/runtime/scripts/guards/command-guard.mjs +468 -0
- package/runtime/scripts/guards/command-policy.mjs +342 -0
- package/runtime/scripts/guards/critic-reach.mjs +82 -0
- package/runtime/scripts/guards/deps-guard.mjs +150 -0
- package/runtime/scripts/guards/env-dump-verdict.mjs +89 -0
- package/runtime/scripts/guards/git-pathspec.mjs +225 -0
- package/runtime/scripts/guards/git-policy.mjs +355 -0
- package/runtime/scripts/guards/guards-root.mjs +27 -0
- package/runtime/scripts/guards/hook-install.mjs +343 -0
- package/runtime/scripts/guards/hook-io.mjs +76 -0
- package/runtime/scripts/guards/hook-verdict.mjs +12 -0
- package/runtime/scripts/guards/install-verdict.mjs +41 -0
- package/runtime/scripts/guards/kernel-seat.mjs +63 -0
- package/runtime/scripts/guards/launch-verdict.mjs +74 -0
- package/runtime/scripts/guards/op-starci.mjs +16 -0
- package/runtime/scripts/guards/op-write-scope.mjs +27 -0
- package/runtime/scripts/guards/orca-self-lifecycle.mjs +39 -0
- package/runtime/scripts/guards/process-kill-verdict.mjs +106 -0
- package/runtime/scripts/guards/protected-zone.mjs +56 -0
- package/runtime/scripts/guards/refusals.mjs +27 -0
- package/runtime/scripts/guards/release-definition.mjs +185 -0
- package/runtime/scripts/guards/release-push-gate.mjs +72 -0
- package/runtime/scripts/guards/release-record.mjs +58 -0
- package/runtime/scripts/guards/release-suite-mode.mjs +15 -0
- package/runtime/scripts/guards/rights.mjs +268 -0
- package/runtime/scripts/guards/seat-tools.mjs +47 -0
- package/runtime/scripts/guards/shell-commands.mjs +328 -0
- package/runtime/scripts/guards/supervisor-seat.mjs +80 -0
- package/runtime/scripts/guards/verdict-process.mjs +81 -0
- package/runtime/scripts/hfs/architecture/dead-exports.mjs +10 -10
- package/runtime/scripts/hfs/architecture/function-spec-providers.mjs +1 -1
- package/runtime/scripts/hfs/architecture/operation-contract-roots.mjs +53 -0
- package/runtime/scripts/hfs/architecture/reachability.mjs +1 -1
- package/runtime/scripts/hfs/architecture/sql-owner.mjs +17 -6
- package/runtime/scripts/hfs/architecture/sql-token-forms.mjs +105 -0
- package/runtime/scripts/hfs/architecture/sql-tokens.mjs +38 -52
- package/runtime/scripts/hfs/architecture/supabase-keys.mjs +261 -0
- package/runtime/scripts/hfs/architecture/transport-owner.mjs +49 -3
- package/runtime/scripts/hfs/architecture/typescript.mjs +1 -1
- package/runtime/scripts/hfs/check.mjs +6 -4
- package/runtime/scripts/hfs/coverage-scope.mjs +28 -1
- package/runtime/scripts/hfs/db-restamp-verb.mjs +54 -14
- package/runtime/scripts/hfs/declaration-shape.mjs +4 -4
- package/runtime/scripts/hfs/declaration-slots.mjs +14 -3
- package/runtime/scripts/hfs/manifest-shape.mjs +10 -6
- package/runtime/scripts/hfs/rule-params-shape.mjs +18 -6
- package/runtime/scripts/hfs/rules/a15-findings.mjs +4 -1
- package/runtime/scripts/hfs/rules/bypass-markers.mjs +22 -7
- package/runtime/scripts/hfs/rules/database-migrations.mjs +69 -14
- package/runtime/scripts/hfs/rules/database-queue-sources.mjs +9 -7
- package/runtime/scripts/hfs/rules/database-queues.mjs +68 -18
- package/runtime/scripts/hfs/rules/edition.mjs +56 -12
- package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +1 -10
- package/runtime/scripts/hfs/rules/fe-stack.mjs +12 -7
- package/runtime/scripts/hfs/rules/lite-proof-lane.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-lite-backing.mjs +75 -9
- package/runtime/scripts/hfs/rules/stacks-runtime.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +15 -4
- package/runtime/scripts/hfs/rules/text-integrity.mjs +73 -0
- package/runtime/scripts/hfs/runtime-rules/hook-shape.mjs +74 -0
- package/runtime/scripts/hfs/runtime-rules/release-notes.mjs +44 -0
- package/runtime/scripts/hfs/sealed-members.mjs +3 -3
- package/runtime/scripts/hfs/slot-side-problems.mjs +3 -3
- package/runtime/scripts/hfs/slots.mjs +2 -1
- package/runtime/scripts/hfs/stack-deploy-config.mjs +22 -5
- package/runtime/scripts/hfs/stacks-gate.mjs +28 -7
- package/runtime/scripts/hfs/view.mjs +2 -1
- package/runtime/scripts/lib/app-data.mjs +11 -0
- package/runtime/scripts/lib/bypass-markers.mjs +272 -75
- package/runtime/scripts/lib/check-format.mjs +7 -0
- package/runtime/scripts/lib/cli-arg.mjs +120 -0
- package/runtime/scripts/lib/css-rules.mjs +220 -0
- package/runtime/scripts/lib/dom-tree.mjs +4 -0
- package/runtime/scripts/lib/escape.mjs +16 -0
- package/runtime/scripts/lib/event-items.mjs +22 -0
- package/runtime/scripts/lib/failure-code-template-errors.mjs +161 -0
- package/runtime/scripts/lib/git-dir.mjs +120 -0
- package/runtime/scripts/lib/grammar-vendor-parity.mjs +377 -0
- package/runtime/scripts/lib/guard-command.mjs +24 -0
- package/runtime/scripts/lib/hook-limits.mjs +5 -0
- package/runtime/scripts/lib/interrupt-cleanup.mjs +38 -0
- package/runtime/scripts/lib/js-lex.mjs +200 -0
- package/runtime/scripts/lib/json-formats.mjs +0 -1
- package/runtime/scripts/lib/language.mjs +94 -1
- package/runtime/scripts/lib/loosening-budgets.mjs +44 -0
- package/runtime/scripts/lib/loosening-config.mjs +361 -0
- package/runtime/scripts/lib/loosening-state.mjs +223 -0
- package/runtime/scripts/lib/menu-parts.mjs +11 -0
- package/runtime/scripts/lib/normalize.mjs +25 -0
- package/runtime/scripts/lib/op-gate.mjs +15 -0
- package/runtime/scripts/lib/redact.mjs +208 -0
- package/runtime/scripts/lib/runtime-location.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-inventory.mjs +21 -0
- package/runtime/scripts/lib/runtime-package-manifests.mjs +84 -0
- package/runtime/scripts/lib/runtime-publication-tags.mjs +99 -0
- package/runtime/scripts/lib/seat-env.mjs +43 -0
- package/runtime/scripts/lib/secret-patterns.mjs +58 -9
- package/runtime/scripts/lib/shell-source.mjs +4 -0
- package/runtime/scripts/lib/source-phrases.mjs +34 -0
- package/runtime/scripts/lib/sqlite.mjs +21 -0
- package/runtime/scripts/lib/swarm-stack.mjs +2 -1
- package/runtime/scripts/lib/tar-files.mjs +39 -0
- package/runtime/scripts/lib/ts-analysis.mjs +10 -0
- package/runtime/scripts/lib/worktree-exclude.mjs +35 -0
- package/runtime/scripts/lib/worktree-record.mjs +14 -0
- package/runtime/scripts/machine/artifact-hold.mjs +52 -0
- package/runtime/scripts/machine/ask-receipts.mjs +113 -0
- package/runtime/scripts/machine/evidence-store.mjs +223 -0
- package/runtime/scripts/machine/home.mjs +216 -0
- package/runtime/scripts/machine/host-lock.mjs +184 -0
- package/runtime/scripts/machine/lock-recovery.mjs +55 -0
- package/runtime/scripts/machine/roles-contract.mjs +90 -0
- package/runtime/scripts/machine/runtime-change.mjs +20 -0
- package/runtime/scripts/machine/workflow-tree.mjs +122 -0
- package/runtime/scripts/reconciler/checklist-items.mjs +7 -0
- package/runtime/scripts/reconciler/guard-row.mjs +25 -0
- package/runtime/scripts/work/asset-slot.mjs +284 -0
- package/runtime/scripts/work/brand/brand-colour.mjs +125 -0
- package/runtime/scripts/work/brand/brand-tokens.mjs +167 -0
- package/runtime/scripts/work/brand/brand-value-source.mjs +106 -0
- package/runtime/scripts/work/brand/brand.mjs +892 -0
- package/runtime/scripts/work/draw/draw-dna-anatomy.mjs +60 -0
- package/runtime/scripts/work/draw/draw-dna-findings.mjs +256 -0
- package/runtime/scripts/work/draw/draw-dna-selector.mjs +38 -0
- package/runtime/scripts/work/draw/draw-dna.mjs +483 -0
- package/runtime/scripts/work/draw/draw-layer.mjs +305 -0
- package/runtime/scripts/work/draw/draw-rationale-page.mjs +237 -0
- package/runtime/scripts/work/draw/draw-rationale-ref.mjs +14 -0
- package/runtime/scripts/work/draw/draw-rationale.mjs +353 -0
- package/runtime/scripts/work/draw/draw-source.mjs +412 -0
- package/runtime/scripts/work/draw/draw-taste.mjs +278 -0
- package/runtime/scripts/work/draw/draw-typecheck.mjs +72 -0
- package/runtime/scripts/work/draw/rationale-findings.mjs +205 -0
- package/runtime/scripts/work/draw-grammar.mjs +175 -0
- package/runtime/scripts/work/draw-render-page.mjs +188 -0
- package/runtime/scripts/work/draw-render.mjs +626 -0
- package/runtime/scripts/work/grammar-proposal.mjs +279 -0
- package/runtime/scripts/work/png.mjs +237 -0
- package/runtime/scripts/work/render-tools.mjs +64 -0
- package/runtime/scripts/work/work-io.mjs +119 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-plan.mjs +40 -13
- package/scaffold/add-queue-lite.mjs +372 -0
- package/scaffold/add.mjs +5 -2
- package/scaffold/app.mjs +1 -2
- package/scaffold/edition-gate.mjs +11 -7
- package/scaffold/lite-exports.mjs +10 -0
- package/scaffold/pattern-file-slots.mjs +33 -0
- package/scaffold/test-world.mjs +1 -1
- package/scaffold/yaml-block-edit.mjs +117 -0
- package/src/main.mjs +5 -4
- package/stack/deploy.mjs +7 -3
- package/stack/post-deploy.mjs +48 -0
- package/sync/index.mjs +10 -0
- package/templates/app/ci-workflows/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/stack-deploy.yml +1 -1
- package/templates/app/codegen-graphql.mjs.partial +59 -12
- package/templates/app/hooks/husky/pre-commit +2 -2
- package/templates/app/hooks/husky/pre-push +10 -3
- package/templates/app/package-scripts/redis-dev.json +3 -0
- package/templates/app/package-scripts-lite/package.json +1 -0
- package/templates/app/skeleton/.starcistacks/application-stacks.yaml +36 -8
- package/templates/app/skeleton/.starcistacks/dev/README.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/README.md +10 -9
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/postgres-init.sh +21 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/recovery/restore-proof.md +1 -1
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/releases/images.lock.yaml +1 -0
- package/templates/app/skeleton/.starcistacks/production/infra/swarm/stack.yaml +47 -5
- package/templates/app/skeleton/.starcistacks/production/runtime/env/KEYS.md +5 -3
- package/templates/app/skeleton/README.md +2 -0
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +2 -2
- package/templates/app/skeleton-lite/.starcistacks/production/README.md +1 -1
- package/templates/app/skeleton-lite/README.md +1 -1
- package/templates/be/inbox/platform/client-operation-outcomes.sql.tpl +18 -0
- package/templates/be/inbox/platform/event-bus/errors/event-bus.error.ts.tpl +16 -0
- package/templates/be/inbox/platform/event-bus/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/inbox/platform/event-bus/event-bus.port.ts.tpl +1 -1
- package/templates/be/inbox/platform/event-bus/event-bus.service.spec.ts.tpl +15 -3
- package/templates/be/inbox/platform/event-bus/event-bus.service.ts.tpl +11 -3
- package/templates/be/inbox/platform/event-bus/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/event-bus/messages/event-bus.messages.ts.tpl +7 -0
- package/templates/be/inbox/platform/event-bus/persistence/event-bus.sql.ts.tpl +2 -2
- package/templates/be/inbox/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/inbox/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/inbox/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/inbox/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/inbox/platform/inbox/inbox.port.ts.tpl +21 -0
- package/templates/be/inbox/platform/inbox/inbox.service.spec.ts.tpl +334 -11
- package/templates/be/inbox/platform/inbox/inbox.service.ts.tpl +55 -4
- package/templates/be/inbox/platform/inbox/index.ts.tpl +2 -0
- package/templates/be/inbox/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/inbox/platform/inbox/persistence/inbox.sql.ts.tpl +26 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/inbox/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/inbox/platform/migration.sql.tpl +1 -0
- package/templates/be/inbox/service.spec.ts.tpl +4 -4
- package/templates/be/inbox/service.ts.tpl +3 -1
- package/templates/be/inbox/webhook-door.spec.ts.tpl +3 -2
- package/templates/be/inbox/webhook-door.ts.tpl +3 -2
- package/templates/be/patterns/event-bus/platform/errors/event-bus.error.ts.tpl +3 -0
- package/templates/be/patterns/event-bus/platform/event-bus.contracts.ts.tpl +10 -0
- package/templates/be/patterns/event-bus/platform/event-bus.port.ts.tpl +2 -2
- package/templates/be/patterns/event-bus/platform/event-bus.service.spec.ts.tpl +16 -3
- package/templates/be/patterns/event-bus/platform/event-bus.service.ts.tpl +11 -3
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.spec.ts.tpl +139 -50
- package/templates/be/patterns/event-bus/platform/event-envelope.policy.ts.tpl +25 -2
- package/templates/be/patterns/event-bus/platform/event-relay.service.spec.ts.tpl +139 -78
- package/templates/be/patterns/event-bus/platform/event-relay.service.ts.tpl +10 -6
- package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +262 -159
- package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +32 -5
- package/templates/be/patterns/event-bus/platform/messages/event-bus.messages.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/entities/event-outbox.entity.ts.tpl +5 -1
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.rows.ts.tpl +2 -0
- package/templates/be/patterns/event-bus/platform/persistence/event-bus.sql.ts.tpl +4 -4
- package/templates/be/patterns/event-bus/platform/persistence/migrations/1789800009000-create-event-outbox.ts.tpl +1 -0
- package/templates/be/patterns/idempotency/platform/idempotency/errors/idempotency.error.ts.tpl +19 -0
- package/templates/be/patterns/idempotency/platform/idempotency/index.ts.tpl +2 -0
- package/templates/be/patterns/idempotency/platform/idempotency/messages/idempotency.messages.ts.tpl +13 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.spec.ts.tpl +32 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-operation.policy.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.spec.ts.tpl +66 -0
- package/templates/be/patterns/idempotency/platform/inbox/client-outcome-backfill.client.ts.tpl +24 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.spec.ts.tpl +60 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-backfill.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.spec.ts.tpl +49 -0
- package/templates/be/patterns/idempotency/platform/inbox/delivery-claim-release.client.ts.tpl +12 -0
- package/templates/be/patterns/idempotency/platform/inbox/effectful.decorators.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/errors/inbox.error.ts.tpl +22 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.decorators.ts.tpl +17 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module.ts.tpl +28 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.options.ts.tpl +7 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.port.ts.tpl +44 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.spec.ts.tpl +388 -0
- package/templates/be/patterns/idempotency/platform/inbox/inbox.service.ts.tpl +77 -0
- package/templates/be/patterns/idempotency/platform/inbox/index.ts.tpl +10 -0
- package/templates/be/patterns/idempotency/platform/inbox/messages/inbox.messages.ts.tpl +15 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/connection.ts.tpl +9 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/entities/inbox-claim.entity.ts.tpl +25 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.rows.ts.tpl +33 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/inbox.sql.ts.tpl +39 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1789800005000-create-inbox-claims.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/persistence/migrations/1792800000000-client-operation-outcomes.ts.tpl +21 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.spec.ts.tpl +76 -0
- package/templates/be/patterns/idempotency/platform/inbox/request-fingerprint.policy.ts.tpl +36 -0
- package/templates/be/patterns/jobs/platform/fenced.processor.spec.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/job-claim.service.spec.ts.tpl +4 -3
- package/templates/be/patterns/jobs/platform/job-claim.service.ts.tpl +2 -0
- package/templates/be/patterns/jobs/platform/job-runner.service.spec.ts.tpl +20 -2
- package/templates/be/patterns/jobs/platform/job-runner.service.ts.tpl +1 -0
- package/templates/be/patterns/jobs/platform/jobs.contracts.ts.tpl +4 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.index.ts.tpl +10 -0
- package/templates/be/patterns/jobs/platform-lite/jobs.sql.tpl +44 -0
- package/templates/be/patterns/jobs/platform-lite/persistence.jobs.sql.ts.tpl +33 -0
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.spec.ts.tpl +203 -18
- package/templates/be/patterns/queues/platform/bullmq-queue-transport.client.ts.tpl +78 -22
- package/templates/be/patterns/queues/platform/errors/queue.error.ts.tpl +19 -0
- package/templates/be/patterns/queues/platform/messages/queue.messages.ts.tpl +13 -0
- package/templates/be/patterns/queues/platform/persistence.migration.ts.tpl +1 -0
- package/templates/be/patterns/queues/platform/persistence.queue-outbox.entity.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/persistence.queue.rows.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/persistence.queue.sql.ts.tpl +4 -4
- package/templates/be/patterns/queues/platform/queue-outbox.service.spec.ts.tpl +14 -2
- package/templates/be/patterns/queues/platform/queue-outbox.service.ts.tpl +5 -2
- package/templates/be/patterns/queues/platform/queue-relay.service.spec.ts.tpl +72 -74
- package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform/queue-transport.port.ts.tpl +23 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.spec.ts.tpl +29 -8
- package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +7 -3
- package/templates/be/patterns/queues/platform/queue.contracts.ts.tpl +5 -0
- package/templates/be/patterns/queues/platform/queue.index.ts.tpl +2 -0
- package/templates/be/patterns/queues/platform/queue.log-events.ts.tpl +4 -0
- package/templates/be/patterns/queues/platform/queue.module.ts.tpl +2 -1
- package/templates/be/patterns/queues/platform/queue.port.ts.tpl +1 -1
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.spec.ts.tpl +417 -0
- package/templates/be/patterns/queues/platform-lite/bullmq-queue-transport.client.ts.tpl +172 -0
- package/templates/be/patterns/queues/platform-lite/persistence.queue.sql.ts.tpl +12 -0
- package/templates/be/patterns/queues/platform-lite/queue-outbox.sql.tpl +32 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.spec.ts.tpl +148 -0
- package/templates/be/patterns/queues/platform-lite/queue-relay.service.ts.tpl +51 -0
- package/templates/be/patterns/queues/platform-lite/queue-transport.port.ts.tpl +163 -0
- package/templates/be/patterns/queues/platform-lite/queue.config.ts.tpl +17 -0
- package/templates/be/patterns/queues/platform-lite/queue.error.ts.tpl +25 -0
- package/templates/be/patterns/queues/platform-lite/queue.index.ts.tpl +8 -0
- package/templates/be/patterns/queues/platform-lite/queue.messages.ts.tpl +21 -0
- package/templates/be/patterns/queues/platform-lite/queue.module.ts.tpl +74 -0
- package/templates/be/patterns/queues/platform-lite/queue.options.ts.tpl +38 -0
- package/templates/be/patterns/queues/queue.queue.ts.tpl +2 -2
- package/templates/be/patterns/reactors/application.contracts.ts.tpl +2 -0
- package/templates/be/patterns/reactors/consumer.ts.tpl +5 -1
- package/templates/be/platform/graphql/graphql.contracts.ts +2 -1
- package/templates/be/platform/graphql/localize-errors.mapper.spec.ts +1 -1
- package/templates/be/skeleton/apps/__app__/src/compose/platform.ts +6 -1
- package/templates/be/skeleton/apps/__app__/src/main.ts +7 -4
- package/templates/be/skeleton/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton/apps/cli/src/main.ts +7 -4
- package/templates/be/skeleton/src/modules/platform/database/index.ts +1 -1
- package/templates/be/skeleton/src/modules/platform/http/errors/http.error.ts +22 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.spec.ts +235 -0
- package/templates/be/skeleton/src/modules/platform/http/fetch-http-client.client.ts +85 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.spec.ts +92 -0
- package/templates/be/skeleton/src/modules/platform/http/graphql-call.mapper.ts +52 -0
- package/templates/be/skeleton/src/modules/platform/http/http.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/http.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/http/http.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/http/http.port.ts +40 -0
- package/templates/be/skeleton/src/modules/platform/http/index.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/http/messages/http.messages.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.decorators.ts +9 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +7 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.module.ts +19 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.options.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/ids/ids.port.ts +5 -0
- package/templates/be/skeleton/src/modules/platform/ids/index.ts +4 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.spec.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/ids/system-ids.service.ts +10 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.spec.ts +59 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation-request.policy.ts +21 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.contracts.ts +16 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.spec.ts +150 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.policy.ts +42 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.port.ts +15 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.spec.ts +23 -0
- package/templates/be/skeleton/src/modules/platform/logging/correlation.service.ts +28 -0
- package/templates/be/skeleton/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.spec.ts +39 -6
- package/templates/be/skeleton/src/modules/platform/logging/json-logger.service.ts +15 -3
- package/templates/be/skeleton/src/modules/platform/logging/logging.decorators.ts +17 -2
- package/templates/be/skeleton/src/modules/platform/logging/logging.log-events.ts +13 -1
- package/templates/be/skeleton/src/modules/platform/logging/logging.module.ts +13 -3
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.spec.ts +69 -0
- package/templates/be/skeleton/src/modules/platform/logging/nest-logger.service.ts +48 -0
- package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.spec.ts +12 -0
- package/templates/be/skeleton/src/modules/platform/primitives/record.policy.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/platform.ts +6 -1
- package/templates/be/skeleton-lite/apps/cli/src/compose/platform.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/logging/index.ts +14 -1
- package/templates/be/skeleton-lite/src/modules/platform/logging/logging.log-events.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
- package/templates/be/test-world/world/test-apps.options.ts.tpl +2 -0
- package/templates/be/test-world/world/test-world.config.ts.tpl +1 -0
- package/upgrade/check.mjs +12 -135
- package/upgrade/file-state.mjs +1 -16
- package/upgrade/index.mjs +34 -361
- package/runtime/scripts/lib/check-code-loosening.mjs +0 -128
- package/runtime/scripts/lib/gate-loosening.mjs +0 -292
- package/templates/app/upgrade-full/.starcistacks/application-stacks.yaml +0 -89
- package/templates/app/upgrade-full/.starcistacks/dev/README.md +0 -16
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/api.yaml +0 -13
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/compose.yaml +0 -3
- package/templates/app/upgrade-full/.starcistacks/dev/infra/compose/postgres.yaml +0 -12
- package/templates/app/upgrade-full/.starcistacks/dev/infra/metadata.json +0 -7
- package/templates/be/skeleton-lite/apps/api/src/main.ts +0 -26
- package/templates/be/upgrade-full/apps/cli/src/app.module.ts +0 -12
- package/templates/be/upgrade-full/apps/cli/src/cli.env.ts +0 -7
- package/templates/be/upgrade-full/apps/cli/src/compose/cli.ts +0 -5
- package/templates/be/upgrade-full/apps/cli/src/compose/platform.ts +0 -9
- package/templates/be/upgrade-full/apps/cli/src/main.ts +0 -29
- package/templates/be/upgrade-full/src/features/cli/cli.module.ts +0 -7
- package/templates/be/upgrade-full/src/features/cli/migrate/subs/run.cli.ts +0 -23
|
@@ -0,0 +1,523 @@
|
|
|
1
|
+
// trust.mjs — prepare provider trust and guard settings for the launch directory.
|
|
2
|
+
//
|
|
3
|
+
// Automatic launch trust requires this installation's explicit launchTrust owner
|
|
4
|
+
// profile and an exact approved repository root. Missing or declined adoption
|
|
5
|
+
// refuses the launch. Existing provider declines are preserved.
|
|
6
|
+
//
|
|
7
|
+
// ensureLaunchTrust({ agent, cwd })
|
|
8
|
+
// SCOPE (lead ruling 2026-10-01): the command guard hook and every launch setting of Claude and Devin are written
|
|
9
|
+
// to the launch worktree's PROJECT files (projectTargets), never to a user-global settings file, so they reach only
|
|
10
|
+
// the agents the runtime starts in that worktree; each such file is kept out of git status through the repository's
|
|
11
|
+
// own info/exclude (excludeFromGit). The per-user records are the ones a host keeps nowhere else: its trust of the
|
|
12
|
+
// directory, and Codex's guard hook with its trusted hash (the guard acts only for a terminal bound to a guard).
|
|
13
|
+
// claude → ~/.claude.json projects[<cwd>].hasTrustDialogAccepted = true, in
|
|
14
|
+
// every key form Claude writes (win32: `<drive>:/…` and `<drive>:\…`); in
|
|
15
|
+
// <cwd>/.claude/settings.local.json: skipDangerousModePermissionPrompt
|
|
16
|
+
// (set only when missing), each agents/claude.yaml launchEnv key under
|
|
17
|
+
// env (DISABLE_AUTOUPDATER: Orca composes a worker's command, so its
|
|
18
|
+
// launch env cannot carry it) and the guard hook.
|
|
19
|
+
// codex → [projects."<path>"] trust_level = "trusted" in each Codex home the worker can start in
|
|
20
|
+
// (CODEX_HOME, else Orca's codex-runtime-home and the system ~/.codex) for the launch
|
|
21
|
+
// cwd and the git root Codex keys trust by, in the key forms Codex
|
|
22
|
+
// writes (win32: a '<drive>:\lower\case' literal and a "<DRIVE>:\\exact" basic), plus the
|
|
23
|
+
// update-check and model-nudge notices; the guard hook as a block in
|
|
24
|
+
// each home's own config.toml (Codex 0.160.0 lists a project
|
|
25
|
+
// layer's hooks only for a repository's main checkout, never for a linked
|
|
26
|
+
// worktree) and the hash Codex trusts it by (codex app-server hooks/list,
|
|
27
|
+
// then config/batchWrite hooks.state - the way Orca trusts its own hooks).
|
|
28
|
+
// devin → <cwd>/.devin/config.local.json (Devin's local project config): the
|
|
29
|
+
// guard hook only: Devin has no per-worker model pin through Orca.
|
|
30
|
+
// Returns the receipt the launch event records:
|
|
31
|
+
// {agent, paths, status: written|already|skipped|failed, written[], already[], …}
|
|
32
|
+
//
|
|
33
|
+
// Both files are rewritten by running agent sessions, so every write is a
|
|
34
|
+
// read-modify-write with an atomic rename, re-read and verified; a lost update
|
|
35
|
+
// is retried a bounded number of times. Only the named project keys change —
|
|
36
|
+
// every other field/table (and TOML comments) is kept. A test process
|
|
37
|
+
// (NODE_TEST_CONTEXT) never touches the real files: it writes only under
|
|
38
|
+
// STARCI_AGENT_TRUST_HOME, or skips.
|
|
39
|
+
import fs from 'node:fs';
|
|
40
|
+
import os from 'node:os';
|
|
41
|
+
import path from 'node:path';
|
|
42
|
+
import { fileURLToPath } from 'node:url';
|
|
43
|
+
import { parseYaml } from '../../engine/yaml.mjs';
|
|
44
|
+
import { lsFiles } from '../api/git/ls-files.mjs'; import { revParseQuery } from '../api/git/rev-parse-query.mjs';
|
|
45
|
+
import { renameOver } from '../api/fs/rename-over.mjs';
|
|
46
|
+
import { sleepSync } from '../lib/sleep-sync.mjs';
|
|
47
|
+
import { parseJson } from '../lib/json.mjs';
|
|
48
|
+
import { isSpecRun } from '../lib/env.mjs';
|
|
49
|
+
import { isGuardCommand } from '../lib/guard-command.mjs';
|
|
50
|
+
import { HOOK_INPUT_LIMITS } from '../lib/hook-limits.mjs';
|
|
51
|
+
import { orcaUserData } from './host-agents.mjs';
|
|
52
|
+
export { codexTrustPaths, launchTrustVerdict } from './launch-trust-policy.mjs';
|
|
53
|
+
import { codexAppServer } from './codex-app-server.mjs';
|
|
54
|
+
export { codexAppServer };
|
|
55
|
+
|
|
56
|
+
const CLAUDE_CARD = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', 'modules', 'models', 'agents', 'claude.yaml');
|
|
57
|
+
|
|
58
|
+
const ATTEMPTS = 5;
|
|
59
|
+
|
|
60
|
+
/* ---------------------------------------------------------------- targets */
|
|
61
|
+
|
|
62
|
+
/** Orca's CODEX_HOME: <Electron userData>/codex-runtime-home/home, resolved the way Orca does. */
|
|
63
|
+
export function orcaCodexHome({ env = process.env, platform = process.platform, home = os.homedir() } = {}) {
|
|
64
|
+
if (env.STARCI_ORCA_CODEX_HOME) return env.STARCI_ORCA_CODEX_HOME;
|
|
65
|
+
return path.join(orcaUserData({ env, platform, home }), 'codex-runtime-home', 'home');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* The files trust is written to. STARCI_AGENT_TRUST_HOME re-roots every one of
|
|
70
|
+
* them (specs); otherwise Claude's own CLAUDE_CONFIG_DIR and Codex's CODEX_HOME
|
|
71
|
+
* are honoured. Returns {claudeJson, codexHomes[], home, skipped?}.
|
|
72
|
+
*/
|
|
73
|
+
export function trustTargets({ env = process.env, platform = process.platform } = {}) {
|
|
74
|
+
const root = env.STARCI_AGENT_TRUST_HOME || null;
|
|
75
|
+
if (!root && isSpecRun(env))
|
|
76
|
+
return { skipped: 'a test process writes agent trust only under STARCI_AGENT_TRUST_HOME' };
|
|
77
|
+
const home = root || os.homedir();
|
|
78
|
+
const claudeDir = !root && env.CLAUDE_CONFIG_DIR ? env.CLAUDE_CONFIG_DIR : null;
|
|
79
|
+
const codexHomes = [];
|
|
80
|
+
const addHome = (kind, dir) => {
|
|
81
|
+
if (!dir) return;
|
|
82
|
+
const key = path.resolve(dir).toLowerCase();
|
|
83
|
+
if (!codexHomes.some((h) => path.resolve(h.dir).toLowerCase() === key)) codexHomes.push({ kind, dir: path.resolve(dir) });
|
|
84
|
+
};
|
|
85
|
+
// The active worker home; the system home joins it in codexLaunchHomes (codex-mirror-source.mjs).
|
|
86
|
+
addHome('active-codex-home', root ? path.join(root,'.codex') : (env.CODEX_HOME || orcaCodexHome({ env, platform, home })));
|
|
87
|
+
return {
|
|
88
|
+
claudeJson: claudeDir ? path.join(claudeDir, '.claude.json') : path.join(home, '.claude.json'),
|
|
89
|
+
codexHomes, home,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* The PROJECT-scoped files of the launch directory `dir` (never a user-global file): Claude's local project settings
|
|
95
|
+
* and Devin's local project config. They carry the command guard hook and each host's launch settings, so they
|
|
96
|
+
* apply only to agents started in that worktree.
|
|
97
|
+
*/
|
|
98
|
+
export const projectTargets = (dir) => ({
|
|
99
|
+
claudeSettings: path.join(dir, '.claude', 'settings.local.json'),
|
|
100
|
+
devinConfig: path.join(dir, '.devin', 'config.local.json'),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Keep a project file the runtime wrote out of `git status`: its path (relative to the checkout's top level) goes in
|
|
105
|
+
* the repository's own info/exclude (`git rev-parse --git-path info/exclude`), never a user-global ignore. A directory
|
|
106
|
+
* that is no git checkout, or a file git already tracks, is left alone. Returns {file, state} or null.
|
|
107
|
+
*/
|
|
108
|
+
export function excludeFromGit(dir, file) {
|
|
109
|
+
const topOut = revParseQuery(['--show-toplevel'], { cwd: dir });
|
|
110
|
+
const excludeOut = revParseQuery(['--path-format=absolute', '--git-path', 'info/exclude'], { cwd: dir });
|
|
111
|
+
if (topOut?.status !== 0 || excludeOut?.status !== 0) return null;
|
|
112
|
+
const top = String(topOut.stdout).trim();
|
|
113
|
+
const rel = path.relative(top, file).split(path.sep).join('/');
|
|
114
|
+
if (!rel || rel.startsWith('..')) return null;
|
|
115
|
+
if (lsFiles(['--error-unmatch', '--', rel], { cwd: top })?.status === 0) return { file: rel, state: 'tracked' };
|
|
116
|
+
const target = String(excludeOut.stdout).trim();
|
|
117
|
+
const line = '/' + rel;
|
|
118
|
+
let text = '';
|
|
119
|
+
try { text = fs.readFileSync(target, 'utf8'); } catch { text = ''; }
|
|
120
|
+
if (text.split(/\r?\n/).includes(line)) return { file: rel, state: 'already' };
|
|
121
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
122
|
+
fs.appendFileSync(target, (text && !text.endsWith('\n') ? '\n' : '') + line + '\n');
|
|
123
|
+
return { file: rel, state: 'written' };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/* -------------------------------------------------------------- key forms */
|
|
127
|
+
|
|
128
|
+
const upperDrive = (p) => p.replace(/^([a-z]):/, (m, d) => `${d.toUpperCase()}:`);
|
|
129
|
+
|
|
130
|
+
/** Claude's project keys for `dir`: win32 writes both `<drive>:/…` and `<drive>:\…`. */
|
|
131
|
+
export function claudeKeyForms(dir, platform = process.platform) {
|
|
132
|
+
if (platform !== 'win32') return [path.posix.resolve(String(dir))];
|
|
133
|
+
const abs = upperDrive(path.win32.resolve(String(dir)));
|
|
134
|
+
return [...new Set([abs.replaceAll('\\', '/'), abs.replaceAll('/', '\\')])];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Codex's project keys for `dir`: win32 writes '<drive>:\lower' (its own form) and "<DRIVE>:\\exact". */
|
|
138
|
+
export function codexKeyForms(dir, platform = process.platform) {
|
|
139
|
+
if (platform !== 'win32') return [path.posix.resolve(String(dir))];
|
|
140
|
+
const abs = upperDrive(path.win32.resolve(String(dir))).replaceAll('/', '\\');
|
|
141
|
+
return [...new Set([abs.toLowerCase(), abs])];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/* ------------------------------------------------------ atomic rewriting */
|
|
145
|
+
|
|
146
|
+
// A transform that wants its input as text: an absent file reads as the empty string.
|
|
147
|
+
const asText = (transform) => (text) => transform(text ?? '');
|
|
148
|
+
export const readText = (file) => { try { return fs.readFileSync(file, 'utf8'); } catch (e) { if (e.code === 'ENOENT') { return null; } throw e; } };
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Read-modify-write `file` with an atomic rename, then re-read and verify.
|
|
152
|
+
* transform(text|null) → {text: next|null (no change needed), result}
|
|
153
|
+
* verify(text) → true when the change is on disk
|
|
154
|
+
* A concurrent rewrite between read and rename, or one that lands after the
|
|
155
|
+
* rename and drops the change, is a lost update: retried up to `attempts`.
|
|
156
|
+
* hooks.beforeRename / hooks.afterRename let specs simulate that writer.
|
|
157
|
+
*/
|
|
158
|
+
function atomicUpdate(file, transform, verify, { attempts = ATTEMPTS, hooks = {} } = {}) {
|
|
159
|
+
const trail = [];
|
|
160
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
161
|
+
const before = readText(file);
|
|
162
|
+
const { text: next, result } = transform(before);
|
|
163
|
+
if (next == null) {
|
|
164
|
+
if (verify(before)) return { ok: true, changed: false, attempts: attempt, trail, result };
|
|
165
|
+
return { ok: false, error: 'transform produced no change but verification failed', attempts: attempt, trail };
|
|
166
|
+
}
|
|
167
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
168
|
+
const tmp = `${file}.starci-trust-${process.pid}-${Date.now()}-${attempt}.tmp`;
|
|
169
|
+
fs.writeFileSync(tmp, next);
|
|
170
|
+
try {
|
|
171
|
+
hooks.beforeRename?.({ attempt, file });
|
|
172
|
+
if (readText(file) !== before) { trail.push({ attempt, lost: 'rewritten-before-rename' }); continue; }
|
|
173
|
+
renameOver(tmp, file, { delayMs: (i) => 25 * (i + 1) });
|
|
174
|
+
} finally { try { fs.rmSync(tmp, { force: true }); } catch { /* best-effort */ } }
|
|
175
|
+
hooks.afterRename?.({ attempt, file });
|
|
176
|
+
const onDisk = readText(file);
|
|
177
|
+
if (onDisk != null && verify(onDisk)) return { ok: true, changed: true, attempts: attempt, trail, result };
|
|
178
|
+
trail.push({ attempt, lost: 'overwritten-after-rename' });
|
|
179
|
+
sleepSync(20 * attempt);
|
|
180
|
+
}
|
|
181
|
+
return { ok: false, error: `lost update: ${file} was rewritten on each of ${attempts} attempts`, attempts, trail };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/* ------------------------------------------------------------------ claude */
|
|
185
|
+
|
|
186
|
+
// The project shape Claude itself writes for a new directory.
|
|
187
|
+
const CLAUDE_PROJECT_DEFAULT = () => ({
|
|
188
|
+
allowedTools: [], mcpContextUris: [], mcpServers: {}, enabledMcpjsonServers: [], disabledMcpjsonServers: [],
|
|
189
|
+
hasTrustDialogAccepted: true, hasClaudeMdExternalIncludesApproved: false, hasClaudeMdExternalIncludesWarningShown: false,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
// JSON.parse → stringify is exact for every value except integers past 2^53.
|
|
193
|
+
// Refuse to rewrite a file whose long number literals would not survive.
|
|
194
|
+
const longNumbers = (text) => (text.match(/(?<![\w."])-?\d{16,}(?:\.\d+)?(?:[eE][+-]?\d+)?(?![\w"])/g) ?? []);
|
|
195
|
+
const numbersSurvive = (before, after) => longNumbers(before).every((n) => after.includes(n));
|
|
196
|
+
|
|
197
|
+
export const jsonOf = (text) => parseJson(text, undefined);
|
|
198
|
+
|
|
199
|
+
/** Set projects[key].hasTrustDialogAccepted = true for every key in `file` (~/.claude.json). */
|
|
200
|
+
export function writeClaudeTrust({ file, keys, hooks }) {
|
|
201
|
+
const verify = (text) => {
|
|
202
|
+
const doc = text == null ? null : jsonOf(text);
|
|
203
|
+
return !!doc && keys.every((k) => doc.projects?.[k]?.hasTrustDialogAccepted === true);
|
|
204
|
+
};
|
|
205
|
+
const out = { file, written: [], already: [] };
|
|
206
|
+
const updated = atomicUpdate(file, (text) => {
|
|
207
|
+
const doc = text == null ? {} : jsonOf(text);
|
|
208
|
+
if (doc === undefined || !doc || typeof doc !== 'object' || Array.isArray(doc))
|
|
209
|
+
throw new Error(`${file} is not a JSON object; refusing to rewrite it`);
|
|
210
|
+
const written = [], already = [];
|
|
211
|
+
if (!doc.projects || typeof doc.projects !== 'object') doc.projects = {};
|
|
212
|
+
for (const key of keys) {
|
|
213
|
+
const existing = doc.projects[key];
|
|
214
|
+
if (existing?.hasTrustDialogAccepted === true) { already.push(key); continue; }
|
|
215
|
+
if (existing?.hasTrustDialogAccepted === false) throw new Error(`${file}: owner declined trust for ${key}`);
|
|
216
|
+
doc.projects[key] = existing && typeof existing === 'object' ? { ...existing, hasTrustDialogAccepted: true } : CLAUDE_PROJECT_DEFAULT();
|
|
217
|
+
written.push(key);
|
|
218
|
+
}
|
|
219
|
+
if (!written.length) return { text: null, result: { written, already } };
|
|
220
|
+
const next = JSON.stringify(doc, null, 2) + (text?.endsWith('\n') ? '\n' : '');
|
|
221
|
+
if (text && !numbersSurvive(text, next)) throw new Error(`${file} holds a number JSON cannot round-trip; refusing to rewrite it`);
|
|
222
|
+
return { text: next, result: { written, already } };
|
|
223
|
+
}, verify, { hooks });
|
|
224
|
+
if (!updated.ok) return { ...out, ok: false, error: updated.error, trail: updated.trail };
|
|
225
|
+
out.written = updated.result.written;
|
|
226
|
+
out.already = updated.result.already;
|
|
227
|
+
return { ...out, ok: true, attempts: updated.attempts, ...(updated.trail.length ? { trail: updated.trail } : {}) };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Assert skipDangerousModePermissionPrompt in Claude's settings.json; set it only when missing. */
|
|
231
|
+
export function assertClaudeBypassConsent({ file, hooks }) {
|
|
232
|
+
const current = readText(file);
|
|
233
|
+
const doc = current == null ? {} : jsonOf(current);
|
|
234
|
+
if (doc === undefined || !doc || typeof doc !== 'object') return { file, ok: false, state: 'unreadable' };
|
|
235
|
+
if (doc.skipDangerousModePermissionPrompt === true) return { file, ok: true, state: 'already' };
|
|
236
|
+
if ('skipDangerousModePermissionPrompt' in doc) return { file, ok: false, state: 'owner-set-false' };
|
|
237
|
+
const updated = atomicUpdate(file, (text) => {
|
|
238
|
+
const d = text == null ? {} : jsonOf(text);
|
|
239
|
+
if (d === undefined || !d || typeof d !== 'object') throw new Error(`${file} is not a JSON object`);
|
|
240
|
+
if ('skipDangerousModePermissionPrompt' in d) return { text: null, result: null };
|
|
241
|
+
d.skipDangerousModePermissionPrompt = true;
|
|
242
|
+
return { text: JSON.stringify(d, null, 2) + (text?.endsWith('\n') ? '\n' : ''), result: null };
|
|
243
|
+
}, (text) => jsonOf(text ?? '')?.skipDangerousModePermissionPrompt === true, { hooks });
|
|
244
|
+
return updated.ok ? { file, ok: true, state: updated.changed ? 'written' : 'already' } : { file, ok: false, state: 'failed', error: updated.error };
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** agents/claude.yaml launchEnv: {KEY: 'value'} (string values only), or {} when the card has none. */
|
|
248
|
+
export function claudeLaunchEnv({ file = CLAUDE_CARD } = {}) {
|
|
249
|
+
try {
|
|
250
|
+
const vars = parseYaml(fs.readFileSync(file, 'utf8'))?.launchEnv;
|
|
251
|
+
if (!vars || typeof vars !== 'object' || Array.isArray(vars)) return {};
|
|
252
|
+
return Object.fromEntries(Object.entries(vars).filter(([k, v]) => /^[A-Z_][A-Z0-9_]*$/.test(k) && v != null).map(([k, v]) => [k, String(v)]));
|
|
253
|
+
} catch { return {}; }
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Assert each `vars` key under settings.json env; set only the keys that are missing (an owner-set value, even a
|
|
258
|
+
* different one, is kept). Returns {file, ok, state: written|already|owner-set|unreadable|failed, owner?: [keys]}.
|
|
259
|
+
*/
|
|
260
|
+
export function assertClaudeSettingsEnv({ file, vars, hooks }) {
|
|
261
|
+
const keys = Object.keys(vars ?? {});
|
|
262
|
+
if (!keys.length) return { file, ok: true, state: 'already' };
|
|
263
|
+
const current = readText(file);
|
|
264
|
+
const doc = current == null ? {} : jsonOf(current);
|
|
265
|
+
if (doc === undefined || !doc || typeof doc !== 'object' || Array.isArray(doc)) return { file, ok: false, state: 'unreadable' };
|
|
266
|
+
if (doc.env != null && (typeof doc.env !== 'object' || Array.isArray(doc.env))) return { file, ok: false, state: 'unreadable' };
|
|
267
|
+
const has = (d, k) => d.env && typeof d.env === 'object' && Object.hasOwn(d.env, k);
|
|
268
|
+
const owner = keys.filter((k) => has(doc, k) && String(doc.env[k]) !== vars[k]);
|
|
269
|
+
const missing = keys.filter((k) => !has(doc, k));
|
|
270
|
+
if (!missing.length) return { file, ok: true, state: owner.length ? 'owner-set' : 'already', ...(owner.length ? { owner } : {}) };
|
|
271
|
+
const updated = atomicUpdate(file, (text) => {
|
|
272
|
+
const d = text == null ? {} : jsonOf(text);
|
|
273
|
+
if (d === undefined || !d || typeof d !== 'object' || Array.isArray(d)) throw new Error(`${file} is not a JSON object`);
|
|
274
|
+
const add = keys.filter((k) => !has(d, k));
|
|
275
|
+
if (!add.length) return { text: null, result: null };
|
|
276
|
+
d.env = { ...(d.env && typeof d.env === 'object' ? d.env : {}), ...Object.fromEntries(add.map((k) => [k, vars[k]])) };
|
|
277
|
+
return { text: JSON.stringify(d, null, 2) + (text?.endsWith('\n') ? '\n' : ''), result: null };
|
|
278
|
+
}, (text) => { const d = jsonOf(text ?? ''); return !!d && keys.every((k) => has(d, k)); }, { hooks });
|
|
279
|
+
return updated.ok ? { file, ok: true, state: updated.changed ? 'written' : 'already', ...(owner.length ? { owner } : {}) }
|
|
280
|
+
: { file, ok: false, state: 'failed', error: updated.error };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* ------------------------------------------------------------------- codex */
|
|
284
|
+
|
|
285
|
+
// A TOML key segment: bare, 'literal' or "basic" (with escapes).
|
|
286
|
+
const KEY_SEG = String.raw`(?:[A-Za-z0-9_-]+|'[^'\r\n]*'|"(?:[^"\\\r\n]|\\.)*")`;
|
|
287
|
+
const PROJECT_HEADER = new RegExp(String.raw`^[ \t]*\[[ \t]*projects[ \t]*\.[ \t]*(${KEY_SEG})[ \t]*\][ \t]*(?:#.*)?$`);
|
|
288
|
+
const ANY_HEADER = /^[ \t]*\[/;
|
|
289
|
+
const TRUST_LINE = /^([ \t]*trust_level[ \t]*=[ \t]*)("(?:[^"\\]|\\.)*"|'[^']*')(.*)$/;
|
|
290
|
+
|
|
291
|
+
const decodeKey = (seg) => {
|
|
292
|
+
if (seg.startsWith("'")) return seg.slice(1, -1);
|
|
293
|
+
if (!seg.startsWith('"')) return seg;
|
|
294
|
+
return seg.slice(1, -1).replace(/\\(u[0-9a-fA-F]{4}|U[0-9a-fA-F]{8}|.)/g, (m, e) => {
|
|
295
|
+
if (e[0] === 'u' || e[0] === 'U') return String.fromCodePoint(Number.parseInt(e.slice(1), 16));
|
|
296
|
+
return { b: '\b', t: '\t', n: '\n', f: '\f', r: '\r', '"': '"', '\\': '\\' }[e] ?? e;
|
|
297
|
+
});
|
|
298
|
+
};
|
|
299
|
+
const decodeValue = (v) => (v.startsWith("'") ? v.slice(1, -1) : decodeKey(v));
|
|
300
|
+
|
|
301
|
+
const ESCAPED_QUOTE = String.raw`\"`;
|
|
302
|
+
/** A TOML basic-string body: backslash and double quote escaped. */
|
|
303
|
+
const tomlEscaped = (v) => String(v).replaceAll('\\', '\\\\').replaceAll('"', ESCAPED_QUOTE);
|
|
304
|
+
|
|
305
|
+
/** The header Codex would write for `key`: a literal for its lowercase form, else a basic string. */
|
|
306
|
+
function codexHeader(key) {
|
|
307
|
+
const literal = !/['\r\n]/.test(key) && key === key.toLowerCase() && key.includes('\\');
|
|
308
|
+
return literal ? `[projects.'${key}']` : `[projects."${tomlEscaped(key)}"]`;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/** {key → {header line index, trust line index|null, trust value|null, end}} for every [projects.<key>] table. */
|
|
312
|
+
export function codexProjectTables(text) {
|
|
313
|
+
const lines = String(text ?? '').split('\n');
|
|
314
|
+
const tables = new Map();
|
|
315
|
+
let current = null;
|
|
316
|
+
lines.forEach((line, i) => {
|
|
317
|
+
const bare = line.replace(/\r$/, '');
|
|
318
|
+
const m = PROJECT_HEADER.exec(bare);
|
|
319
|
+
if (m) { current = { key: decodeKey(m[1]), header: i, trustLine: null, trust: null, end: i }; if (!tables.has(current.key)) { tables.set(current.key, current); } return; }
|
|
320
|
+
if (ANY_HEADER.test(bare)) { current = null; return; }
|
|
321
|
+
if (!current) return;
|
|
322
|
+
if (bare.trim()) current.end = i;
|
|
323
|
+
const t = TRUST_LINE.exec(bare);
|
|
324
|
+
if (t && current.trustLine === null) { current.trustLine = i; current.trust = decodeValue(t[2]); }
|
|
325
|
+
});
|
|
326
|
+
return tables;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const ownerTrustSet = (table) => table?.trust !== null && table?.trust !== undefined;
|
|
330
|
+
|
|
331
|
+
function trustExistingLine(lines, index) {
|
|
332
|
+
const cr = lines[index].endsWith('\r') ? '\r' : '';
|
|
333
|
+
const m = TRUST_LINE.exec(lines[index].replace(/\r$/, ''));
|
|
334
|
+
lines[index] = `${m[1]}"trusted"${m[3]}${cr}`;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function appendTrustTables(text, keys, eol) {
|
|
338
|
+
let next = text;
|
|
339
|
+
if (keys.length && next && !next.endsWith('\n')) next += eol;
|
|
340
|
+
for (const key of keys) next += `${next ? eol : ''}${codexHeader(key)}${eol}trust_level = "trusted"${eol}`;
|
|
341
|
+
return next;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** The edit that trusts every key of a Codex config.toml: `{ text: next|null, result: { written, already } }`. */
|
|
345
|
+
function codexTrustEdit(source, keys, file) {
|
|
346
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
347
|
+
const tables = codexProjectTables(source);
|
|
348
|
+
const lines = source.split('\n');
|
|
349
|
+
const written = [], already = [], append = [];
|
|
350
|
+
const inserts = [];
|
|
351
|
+
for (const key of keys) {
|
|
352
|
+
const table = tables.get(key);
|
|
353
|
+
if (table?.trust === 'trusted') { already.push(key); continue; }
|
|
354
|
+
if (ownerTrustSet(table)) throw new Error(`${file}: owner set ${key} trust_level=${table.trust}; refusing to override`);
|
|
355
|
+
written.push(key);
|
|
356
|
+
if (!table) append.push(key);
|
|
357
|
+
else if (table.trustLine === null) inserts.push(table.header);
|
|
358
|
+
else trustExistingLine(lines, table.trustLine);
|
|
359
|
+
}
|
|
360
|
+
if (!written.length) return { text: null, result: { written, already } };
|
|
361
|
+
for (const at of inserts.toSorted((a, b) => b - a)) lines.splice(at + 1, 0, `trust_level = "trusted"${eol === '\r\n' ? '\r' : ''}`);
|
|
362
|
+
return { text: appendTrustTables(lines.join('\n'), append, eol), result: { written, already } };
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
/** Ensure trust_level = "trusted" for every key in a Codex config.toml. Appends; edits only an untrusted table's own line. */
|
|
366
|
+
export function writeCodexTrust({ file, keys, hooks }) {
|
|
367
|
+
const verify = (text) => {
|
|
368
|
+
const tables = codexProjectTables(text ?? '');
|
|
369
|
+
return keys.every((k) => tables.get(k)?.trust === 'trusted');
|
|
370
|
+
};
|
|
371
|
+
const out = { file, written: [], already: [] };
|
|
372
|
+
const updated = atomicUpdate(file, asText((source) => codexTrustEdit(source, keys, file)), verify, { hooks });
|
|
373
|
+
if (!updated.ok) return { ...out, ok: false, error: updated.error, trail: updated.trail };
|
|
374
|
+
out.written = updated.result.written;
|
|
375
|
+
out.already = updated.result.already;
|
|
376
|
+
return { ...out, ok: true, attempts: updated.attempts, ...(updated.trail.length ? { trail: updated.trail } : {}) };
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// A new Codex release turned every fresh Codex launch into an interactive
|
|
380
|
+
// "Update available! … Press enter to continue" menu: eighteen op launches in
|
|
381
|
+
// three workflows failed readiness or model attestation in three hours while
|
|
382
|
+
// the kernels, started before the release, kept working. The launch must
|
|
383
|
+
// never wait on it, so the top-level check_for_update_on_startup is pinned
|
|
384
|
+
// false in every Codex home, like trust.
|
|
385
|
+
const UPDATE_CHECK_LINE = /^[ \t]*check_for_update_on_startup[ \t]*=.*$/m;
|
|
386
|
+
const topLevelOf = (text) => { const at = String(text ?? '').search(/^[ \t]*\[/m); return at < 0 ? String(text ?? '') : String(text ?? '').slice(0, at); };
|
|
387
|
+
export function writeCodexNoUpdateCheck({ file, hooks }) {
|
|
388
|
+
const verify = (text) => /^[ \t]*check_for_update_on_startup[ \t]*=[ \t]*false[ \t]*(?:#.*)?\r?$/m.test(topLevelOf(text));
|
|
389
|
+
const updated = atomicUpdate(file, asText((source) => {
|
|
390
|
+
if (verify(source)) return { text: null, result: { written: false } };
|
|
391
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
392
|
+
const top = topLevelOf(source);
|
|
393
|
+
// An existing key is flipped in place; a new one closes the top-level block,
|
|
394
|
+
// so the owner's own leading keys stay where they were.
|
|
395
|
+
const head = top && !top.endsWith('\n') ? `${top}${eol}` : top;
|
|
396
|
+
const next = UPDATE_CHECK_LINE.test(top)
|
|
397
|
+
? top.replace(UPDATE_CHECK_LINE, 'check_for_update_on_startup = false') + source.slice(top.length)
|
|
398
|
+
: `${head}check_for_update_on_startup = false${eol}${source.slice(top.length)}`;
|
|
399
|
+
return { text: next, result: { written: true } };
|
|
400
|
+
}), verify, { hooks });
|
|
401
|
+
if (!updated.ok) return { file, ok: false, error: updated.error };
|
|
402
|
+
return { file, ok: true, written: updated.result.written };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
// Codex's quota nudge ("Approaching rate limits - Switch to <cheaper model>?") stopped a Collab interface.draw op
|
|
406
|
+
// at its menu (2026-09-24). Owner: always keep the current model and never show it again - the answer Codex
|
|
407
|
+
// itself records as [notice] hide_rate_limit_model_nudge = true, pinned in every Codex home.
|
|
408
|
+
const NUDGE_LINE = /^[ \t]*hide_rate_limit_model_nudge[ \t]*=.*$/m;
|
|
409
|
+
const noticeTable = (text) => {
|
|
410
|
+
const at = text.search(/^[ \t]*\[notice\][ \t]*(?:#.*)?\r?$/m);
|
|
411
|
+
if (at < 0) return null;
|
|
412
|
+
const bodyAt = text.indexOf('\n', at) + 1 || text.length;
|
|
413
|
+
const next = text.slice(bodyAt).search(/^[ \t]*\[/m);
|
|
414
|
+
return { bodyAt, end: next < 0 ? text.length : bodyAt + next };
|
|
415
|
+
};
|
|
416
|
+
export function writeCodexNoModelNudge({ file, hooks }) {
|
|
417
|
+
const verify = (text) => {
|
|
418
|
+
const table = noticeTable(String(text ?? ''));
|
|
419
|
+
return Boolean(table) && /^[ \t]*hide_rate_limit_model_nudge[ \t]*=[ \t]*true[ \t]*(?:#.*)?\r?$/m.test(String(text).slice(table.bodyAt, table.end));
|
|
420
|
+
};
|
|
421
|
+
const updated = atomicUpdate(file, asText((source) => {
|
|
422
|
+
if (verify(source)) return { text: null, result: { written: false } };
|
|
423
|
+
const eol = source.includes('\r\n') ? '\r\n' : '\n';
|
|
424
|
+
const table = noticeTable(source);
|
|
425
|
+
let next;
|
|
426
|
+
if (table) {
|
|
427
|
+
const body = source.slice(table.bodyAt, table.end);
|
|
428
|
+
const fixed = NUDGE_LINE.test(body) ? body.replace(NUDGE_LINE, 'hide_rate_limit_model_nudge = true') : `hide_rate_limit_model_nudge = true${eol}${body}`;
|
|
429
|
+
next = source.slice(0, table.bodyAt) + fixed + source.slice(table.end);
|
|
430
|
+
} else {
|
|
431
|
+
const base = source && !source.endsWith('\n') ? `${source}${eol}` : source;
|
|
432
|
+
next = `${base}${base ? eol : ''}[notice]${eol}hide_rate_limit_model_nudge = true${eol}`;
|
|
433
|
+
}
|
|
434
|
+
return { text: next, result: { written: true } };
|
|
435
|
+
}), verify, { hooks });
|
|
436
|
+
if (!updated.ok) return { file, ok: false, error: updated.error };
|
|
437
|
+
return { file, ok: true, written: updated.result.written };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* ------------------------------------------------------ the command guard */
|
|
441
|
+
|
|
442
|
+
export const TOOL_GUARD_MATCHER = 'Bash|PowerShell|Edit|Write|MultiEdit|NotebookEdit';
|
|
443
|
+
const isGuardHandler = (h) => typeof h?.command === 'string' && (isGuardCommand(h.command) || h.command.includes('command-guard.mjs'));
|
|
444
|
+
const isGuardGroup = (g) => Array.isArray(g?.hooks) && g.hooks.some(isGuardHandler);
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Ensure hooks.PreToolUse of a Claude-format JSON config (Claude's settings.json, Devin's config.json) holds exactly
|
|
448
|
+
* one command-guard group: {matcher?, hooks: [{type: command, command, timeout}]}. An older guard group (another
|
|
449
|
+
* runtime path) is replaced; every other hook is kept.
|
|
450
|
+
*/
|
|
451
|
+
export function assertJsonToolGuard({ file, command, matcher = null, hooks }) {
|
|
452
|
+
const group = { ...(matcher ? { matcher } : {}), hooks: [{ type: 'command', command, timeout: HOOK_INPUT_LIMITS.hookTimeoutSeconds }] };
|
|
453
|
+
const holds = (d) => {
|
|
454
|
+
const list = Array.isArray(d?.hooks?.PreToolUse) ? d.hooks.PreToolUse.filter(isGuardGroup) : [];
|
|
455
|
+
return list.length === 1 && JSON.stringify(list[0]) === JSON.stringify(group);
|
|
456
|
+
};
|
|
457
|
+
let updated;
|
|
458
|
+
try { updated = atomicUpdate(file, (text) => {
|
|
459
|
+
const d = text == null ? {} : jsonOf(text);
|
|
460
|
+
if (d === undefined || !d || typeof d !== 'object' || Array.isArray(d)) throw new Error(`${file} is not a JSON object`);
|
|
461
|
+
if (d.hooks != null && (typeof d.hooks !== 'object' || Array.isArray(d.hooks))) throw new Error(`${file} hooks is not an object`);
|
|
462
|
+
if (holds(d)) return { text: null, result: null };
|
|
463
|
+
d.hooks = { ...d.hooks };
|
|
464
|
+
const others = Array.isArray(d.hooks.PreToolUse) ? d.hooks.PreToolUse.filter((g) => !isGuardGroup(g)) : [];
|
|
465
|
+
d.hooks.PreToolUse = [...others, group];
|
|
466
|
+
const next = JSON.stringify(d, null, 2) + (text?.endsWith('\n') ? '\n' : '');
|
|
467
|
+
if (text && !numbersSurvive(text, next)) throw new Error(`${file} holds a number JSON cannot round-trip; refusing to rewrite it`);
|
|
468
|
+
return { text: next, result: null };
|
|
469
|
+
}, (text) => { const d = jsonOf(text ?? ''); return !!d && holds(d); }, { hooks }); }
|
|
470
|
+
catch (e) { return { file, ok: false, state: 'failed', error: String(e?.message ?? e) }; }
|
|
471
|
+
return updated.ok ? { file, ok: true, state: updated.changed ? 'written' : 'already' } : { file, ok: false, state: 'failed', error: updated.error };
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/** Devin's project config: the command guard only. LIVE E7 (2026-10-02): Devin honours agent.model only in the user config (--config) or DEVIN_MODEL, never in <project>/.devin/config*.json, and Orca refuses a launch-time model for Devin, so no model is written here (an external limitation, see the CHANGELOG). */
|
|
475
|
+
export function writeDevinProfile({ file, command, hooks }) {
|
|
476
|
+
return assertJsonToolGuard({ file, command, hooks });
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// A Codex hook lives in the home's config.toml as an array-of-tables block the runtime owns, marked on its first line.
|
|
480
|
+
const CODEX_GUARD_BEGIN = '# starci-command-guard (scripts/agent/trust.mjs): the op command guard, a PreToolUse hook';
|
|
481
|
+
export function codexGuardBlock(command, eol = '\n') {
|
|
482
|
+
const q = (v) => `"${tomlEscaped(v)}"`;
|
|
483
|
+
return [CODEX_GUARD_BEGIN, '[[hooks.PreToolUse]]', 'matcher = "^Bash$"', '', '[[hooks.PreToolUse.hooks]]', 'type = "command"', `command = ${q(command)}`, `timeout = ${HOOK_INPUT_LIMITS.hookTimeoutSeconds}`, ''].join(eol);
|
|
484
|
+
}
|
|
485
|
+
/** Put the guard block in a Codex config.toml: appended, or an older block (another runtime path) replaced in place. */
|
|
486
|
+
export function writeCodexToolGuard({ file, command, hooks }) {
|
|
487
|
+
const eolOf = (text) => (String(text ?? '').includes('\r\n') ? '\r\n' : '\n');
|
|
488
|
+
const verify = (text) => String(text ?? '').includes(codexGuardBlock(command, eolOf(text)));
|
|
489
|
+
const updated = atomicUpdate(file, asText((source) => {
|
|
490
|
+
if (verify(source)) return { text: null, result: { written: false } };
|
|
491
|
+
const eol = eolOf(source);
|
|
492
|
+
const at = source.indexOf(CODEX_GUARD_BEGIN);
|
|
493
|
+
if (at >= 0) {
|
|
494
|
+
// The block ends at the first table header that is not its own.
|
|
495
|
+
const rest = source.slice(at);
|
|
496
|
+
const end = /^[ \t]*\[(?!\[hooks\.PreToolUse(?:\.hooks)?\]\])/m.exec(rest);
|
|
497
|
+
return { text: source.slice(0, at) + codexGuardBlock(command, eol) + (end ? eol + rest.slice(end.index) : ''), result: { written: true } };
|
|
498
|
+
}
|
|
499
|
+
const base = source && !source.endsWith('\n') ? `${source}${eol}` : source;
|
|
500
|
+
return { text: `${base}${base ? eol : ''}${codexGuardBlock(command, eol)}`, result: { written: true } };
|
|
501
|
+
}), verify, { hooks });
|
|
502
|
+
return updated.ok ? { file, ok: true, written: updated.result.written } : { file, ok: false, error: updated.error };
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// The `codex app-server` client lives in codex-app-server.mjs (this file is over its size budget); it is re-exported
|
|
506
|
+
// above so callers of trust.mjs keep their import. A launch that gets no answer throws APP_SERVER_FAILURE with
|
|
507
|
+
// codex's own exit, stderr tail and version - a worker-start strike of the codex circuit (provider-outage.mjs).
|
|
508
|
+
|
|
509
|
+
/** Trust the guard hook of one Codex home: hooks/list, then hooks.state.<key>.trusted_hash for each untrusted one. */
|
|
510
|
+
export function trustCodexToolGuard({ home, cwd, command, appServer = codexAppServer }) {
|
|
511
|
+
const listed = (result) => (result?.data ?? []).flatMap((d) => d?.hooks ?? []).filter((h) => h?.command === command && h?.eventName === 'preToolUse');
|
|
512
|
+
const [before] = appServer({ home, requests: [{ method: 'hooks/list', params: { cwds: [cwd] } }] });
|
|
513
|
+
const entries = listed(before);
|
|
514
|
+
if (!entries.length) return { home, ok: false, error: 'codex hooks/list does not show the command guard' };
|
|
515
|
+
const untrusted = entries.filter((h) => h.trustStatus !== 'trusted');
|
|
516
|
+
if (!untrusted.length) return { home, ok: true, trusted: 'already' };
|
|
517
|
+
const value = Object.fromEntries(untrusted.map((h) => [h.key, { trusted_hash: h.currentHash }]));
|
|
518
|
+
const [, after] = appServer({ home, requests: [
|
|
519
|
+
{ method: 'config/batchWrite', params: { edits: [{ keyPath: 'hooks.state', value, mergeStrategy: 'upsert' }], reloadUserConfig: true } },
|
|
520
|
+
{ method: 'hooks/list', params: { cwds: [cwd] } }] });
|
|
521
|
+
const still = listed(after).filter((h) => h.trustStatus !== 'trusted');
|
|
522
|
+
return still.length ? { home, ok: false, error: `codex still reports the guard hook ${still[0].trustStatus ?? 'untrusted'}` } : { home, ok: true, trusted: 'written' };
|
|
523
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// rename-over.mjs — replacing a file whole through a temp file beside it.
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { sleepSync } from '../../lib/sleep-sync.mjs';
|
|
4
|
+
import { FS_BUSY } from './lib.mjs';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rename `tmp` over `file`. A busy refusal (FS_BUSY of lib.mjs) is retried up to `retries` times, `delayMs(attempt)`
|
|
8
|
+
* apart. When the rename fails for good, `tmp` is removed and the last error is thrown. `rename` is the seam
|
|
9
|
+
* a spec injects (default fs.renameSync).
|
|
10
|
+
*/
|
|
11
|
+
export function renameOver(tmp, file, { retries = 20, delayMs = () => 25, sleep = sleepSync, rename = fs.renameSync } = {}) {
|
|
12
|
+
for (let attempt = 0; ; attempt += 1) {
|
|
13
|
+
try { rename(tmp, file); return; } catch (error) {
|
|
14
|
+
if (attempt >= retries || !FS_BUSY.includes(error?.code)) {
|
|
15
|
+
try { fs.rmSync(tmp, { force: true }); } catch { /* best effort */ }
|
|
16
|
+
throw error;
|
|
17
|
+
}
|
|
18
|
+
sleep(delayMs(attempt));
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// temp-path.mjs — a path under the temp root (engine/temp-root.mjs tempRoot, ensured by ensure-temp-root.mjs) whose directory exists: a scratch file or a named subdirectory the caller makes.
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { ensureTempRoot } from './ensure-temp-root.mjs';
|
|
4
|
+
|
|
5
|
+
/** `parts` joined under the temp root, which is created when it is missing (TEMP_ROOT_UNUSABLE when it cannot be made). */
|
|
6
|
+
export function tempPath(...parts) {
|
|
7
|
+
return path.join(ensureTempRoot(), ...parts);
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// write-json-file.mjs - one JSON document written to a file (the runtime's pretty-printed state files).
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
|
|
5
|
+
/** `value` written to `file` as two-space JSON plus a trailing newline (parent directories created). */
|
|
6
|
+
export function writeJsonFile(file, value) {
|
|
7
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
8
|
+
fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}
|
|
9
|
+
`);
|
|
10
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// diff-names.mjs — `git diff --name-only <from>..<to> -- <paths>`: the files a commit range changed under `paths`.
|
|
2
|
+
import { gitRunner } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/** The changed paths ([] when none), or null when git does not answer. git: the caller's runner or null. */
|
|
5
|
+
export function diffNames(cwd, from, to, { paths = [], git = null } = {}) {
|
|
6
|
+
const r = gitRunner(git)(['diff', '--name-only', `${from}..${to}`, '--', ...paths], { cwd, timeout: 20_000 });
|
|
7
|
+
return r.ok ? r.stdout.split(/\r?\n/).map((l) => l.trim()).filter(Boolean) : null;
|
|
8
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// head-time.mjs — `git log -1 --format=%ct %h`: the committer time (seconds) and short sha of HEAD, or null.
|
|
2
|
+
import { runGit } from './lib.mjs';
|
|
3
|
+
|
|
4
|
+
/** {time, sha} of HEAD in the repository at `cwd` (time in seconds); null when `cwd` is not a checkout. */
|
|
5
|
+
export const headTime = (cwd) => {
|
|
6
|
+
const r = runGit(['log', '-1', '--format=%ct %h'], { cwd, timeout: 5000 });
|
|
7
|
+
if (r.error || r.status !== 0) return null;
|
|
8
|
+
const [ct, sha] = String(r.stdout ?? '').trim().split(' ');
|
|
9
|
+
return /^\d+$/.test(ct) && sha ? { time: Number(ct), sha } : null;
|
|
10
|
+
};
|