@starci/hfs 4.7.31 → 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 +12 -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,343 @@
|
|
|
1
|
+
// install.mjs — puts the shared-checkout guard around an op or [Worker] agent.
|
|
2
|
+
//
|
|
3
|
+
// starci kernel dispatch (opGuardLaunch), the [Worker] launch (workers.mjs workerGuard) and the Kernel launch
|
|
4
|
+
// (scripts/kernel/start-workflow.mjs, role 'kernel') call guardLaunch() for every agent they start
|
|
5
|
+
// (modules/kernel/api.yaml conventions.sharedCheckout). Each layer is idempotent and best effort - a
|
|
6
|
+
// guard that cannot be installed is reported on the dispatch receipt, never a reason to refuse the launch:
|
|
7
|
+
// 1. <guards root>/jobs/<job>.json — the job's identity and owned paths as absolute paths. worker-start owns the
|
|
8
|
+
// agent's environment, so the launch binds it to the agent's Orca terminal (bindGuardTerminal ->
|
|
9
|
+
// <guards root>/terminals/<handle>.json, unbound when that terminal closes). The agent's host runs
|
|
10
|
+
// scripts/guards/command-guard.mjs as a PreToolUse hook (registered by launch trust, scripts/agent/trust.mjs):
|
|
11
|
+
// it finds the guard by ORCA_TERMINAL_HANDLE and refuses a shell command the policy forbids before it runs.
|
|
12
|
+
// 2. the target repository's reference-transaction hook (git runs it for every ref update, whoever the caller is):
|
|
13
|
+
// a protected branch only moves forward and is never deleted; an op (its Orca terminal bound to a guard) creates
|
|
14
|
+
// no worktree and lands only its owned paths. (refs/stash stays writable: lint-staged's pre-commit backup stores
|
|
15
|
+
// one; the command guard refuses a sweeping stash.)
|
|
16
|
+
// 3. the target repository's pre-commit hook (ensureWorkHook): a commit that stages files under .starciwork/ or
|
|
17
|
+
// .starcistacks/ is refused when a YAML does not parse, a record the commit touches fails its scoped strict
|
|
18
|
+
// validation, or a non-.enc file carries a secret (scripts/work/validate/work-hygiene.mjs; e2e never runs there).
|
|
19
|
+
// config.yaml `guards: {historyHook: false}` / `{workHook: false}` switches a hook layer off.
|
|
20
|
+
import fs from 'node:fs';
|
|
21
|
+
import path from 'node:path';
|
|
22
|
+
import { guardsRoot } from './guards-root.mjs';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
import { revParseQuery } from '../api/git/rev-parse-query.mjs';
|
|
25
|
+
import { checkIgnore } from '../api/git/check-ignore.mjs';
|
|
26
|
+
import { lsFiles } from '../api/git/ls-files.mjs';
|
|
27
|
+
import { worktreeListQuery } from '../api/git/worktree-list-query.mjs';
|
|
28
|
+
import { safeRemove } from '../api/fs/safe-remove.mjs';
|
|
29
|
+
import { artifactHoldReason } from '../machine/artifact-hold.mjs';
|
|
30
|
+
import { allocationMs } from '../../engine/config.mjs';
|
|
31
|
+
|
|
32
|
+
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
33
|
+
export { guardsRoot };
|
|
34
|
+
const HOOK_MARKER = 'starci-history-guard';
|
|
35
|
+
export const HOOK_VERSION = 9;
|
|
36
|
+
export const WORK_HOOK_MARKER = 'starci-work-guard';
|
|
37
|
+
export const RUNTIME_GIT_HOOKS_MARKER = 'starci-git-hooks';
|
|
38
|
+
export const WORK_HOOK_VERSION = 1;
|
|
39
|
+
|
|
40
|
+
const normOwned = (p) => path.resolve(p).replaceAll('\\', '/');
|
|
41
|
+
const safeName = (s) => String(s).replace(/[^A-Za-z0-9._-]/g, '_');
|
|
42
|
+
// A single-quoted shell word: ' wrapped as '\'' (close, escaped quote, reopen).
|
|
43
|
+
const SQUOTE = String.raw`'\''`;
|
|
44
|
+
export const shellQuote = (s) => `'${String(s).replaceAll('\\', '/').replaceAll("'", SQUOTE)}'`;
|
|
45
|
+
const JOB_GUARD_TTL_MS = allocationMs('jobGuard.ttlMs');
|
|
46
|
+
const terminalsDir = (skillRoot = path.resolve(here, '..', '..')) => path.join(guardsRoot(skillRoot), 'terminals');
|
|
47
|
+
|
|
48
|
+
// tmp + rename: a reader never sees a torn guard file. Files of the directory older than JOB_GUARD_TTL_MS are pruned: a
|
|
49
|
+
// guard outlives its worker only as history.
|
|
50
|
+
function writeGuardFile(dir, name, body) {
|
|
51
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
52
|
+
const file = path.join(dir, `${safeName(name)}.json`);
|
|
53
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
54
|
+
fs.writeFileSync(tmp, `${JSON.stringify(body, null, 2)}\n`);
|
|
55
|
+
fs.renameSync(tmp, file);
|
|
56
|
+
try {
|
|
57
|
+
const cutoff = Date.now() - JOB_GUARD_TTL_MS;
|
|
58
|
+
for (const other of fs.readdirSync(dir)) {
|
|
59
|
+
const full = path.join(dir, other);
|
|
60
|
+
if (full !== file && fs.statSync(full).mtimeMs < cutoff) fs.rmSync(full, { force: true });
|
|
61
|
+
}
|
|
62
|
+
} catch { /* pruning is housekeeping */ }
|
|
63
|
+
return file;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The roles a job guard is written for. 'op' is an op or [Worker] agent. 'kernel' is the Kernel: its shell commands meet
|
|
68
|
+
* the same command guard (PreToolUse) as an op's, but the history hook does not apply its op rules to a kernel guard,
|
|
69
|
+
* because the runtime's own git (checkpoints, land scratch trees) runs as children of the Kernel's api calls and
|
|
70
|
+
* inherits its ORCA_TERMINAL_HANDLE (contract change kernel-guard-file). 'critic' is the Critic of a product: its `reach` names the
|
|
71
|
+
* one directory it may read and the one file it may write (scripts/guards/critic-reach.mjs).
|
|
72
|
+
*/
|
|
73
|
+
const GUARD_ROLES = Object.freeze(['op', 'kernel', 'critic']);
|
|
74
|
+
|
|
75
|
+
/** <guards root>/jobs/<job>.json — who the worker is, its role and which absolute paths it owns. */
|
|
76
|
+
export function writeJobGuard({ skillRoot = path.resolve(here, '..', '..'), jobId, workflowId, ledgerRepo, owned, workflowWorktree = null, role = 'op', op = null, reach = null }) {
|
|
77
|
+
if (!GUARD_ROLES.includes(role)) throw new Error(`unknown guard role ${role}`);
|
|
78
|
+
// op: the job's op id, for the rules one op family owes (a uat op never starts a test world, launch-verdict.mjs).
|
|
79
|
+
return writeGuardFile(path.join(guardsRoot(skillRoot), 'jobs'), jobId, { schema: 'starci/op-guard@1', role, jobId, workflowId, op: op ?? null,
|
|
80
|
+
ledgerRepo: ledgerRepo ? path.resolve(ledgerRepo) : null, owned: [...new Set((owned ?? []).filter(Boolean).map(normOwned))],
|
|
81
|
+
// The workflow worktree the op works in (scripts/kernel/workflow-worktree.mjs), or null: the guard refuses git history
|
|
82
|
+
// rewrites and ref changes inside it; scoped worker commits are adopted by the runtime at settle.
|
|
83
|
+
workflowWorktree: workflowWorktree ? path.resolve(workflowWorktree) : null, ...(reach ? { reach: { dir: path.resolve(reach.dir), verdictFile: reach.verdictFile } } : {}), writtenAt: new Date().toISOString() });
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* <guards root>/terminals/<handle>.json — the job guard of a managed op, keyed by the Orca terminal its agent runs
|
|
88
|
+
* in. worker-start owns a managed agent's environment; Orca exports ORCA_TERMINAL_HANDLE into that terminal, and the
|
|
89
|
+
* command guard (PreToolUse hook) and the history hook find the op's guard by it.
|
|
90
|
+
*/
|
|
91
|
+
export function bindGuardTerminal({ skillRoot = path.resolve(here, '..', '..'), handle, jobFile }) {
|
|
92
|
+
const guard = JSON.parse(fs.readFileSync(jobFile, 'utf8'));
|
|
93
|
+
return writeGuardFile(terminalsDir(skillRoot), handle, { ...guard, terminal: handle, boundAt: new Date().toISOString() });
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* <guards root>/seats/<handle>.json — the tools a seat's agent may not use, keyed by the Orca terminal it runs in.
|
|
98
|
+
* worker-start takes no provider argv, so a seat's tool denial (the [Supervisor]'s Agent/Task, start-supervisor.mjs
|
|
99
|
+
* SEAT_DENIED_TOOLS) is enforced by the project PreToolUse hook (.claude/settings.json -> scripts/guards/seat-tools.mjs),
|
|
100
|
+
* which denies a tool only for the terminal bound here.
|
|
101
|
+
*/
|
|
102
|
+
const seatsDir = (skillRoot = path.resolve(here, '..', '..')) => path.join(guardsRoot(skillRoot), 'seats');
|
|
103
|
+
export function bindSeatGuard({ skillRoot = path.resolve(here, '..', '..'), handle, role, deniedTools }) {
|
|
104
|
+
return writeGuardFile(seatsDir(skillRoot), handle, { schema: 'starci/seat-guard@1', role, terminal: handle,
|
|
105
|
+
deniedTools: [...new Set((deniedTools ?? []).map(String))], boundAt: new Date().toISOString() });
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Remove the guard bound to terminal `handle` once that terminal is closed; true when a file was removed. */
|
|
109
|
+
export function unbindGuardTerminal({ skillRoot = path.resolve(here, '..', '..'), handle }) {
|
|
110
|
+
if (!handle) return false;
|
|
111
|
+
const file = path.join(terminalsDir(skillRoot), `${safeName(handle)}.json`);
|
|
112
|
+
if (!fs.existsSync(file)) return false;
|
|
113
|
+
fs.rmSync(file, { force: true });
|
|
114
|
+
return true;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** One git call (a scripts/api/git call file) in the repository at `cwd`. */
|
|
118
|
+
const git = (call, cwd, args) => call(args, { dir: cwd, timeout: 20_000 });
|
|
119
|
+
|
|
120
|
+
export function historyHookBody({ branches = [], root, nodePath = process.execPath, terminals = terminalsDir() }) {
|
|
121
|
+
const protectedList = [...new Set(['main', 'master', ...branches.filter((b) => /^[A-Za-z0-9._/-]+$/.test(b))])].join(' ');
|
|
122
|
+
const q = shellQuote;
|
|
123
|
+
return `#!/bin/sh
|
|
124
|
+
# ${HOOK_MARKER} v${HOOK_VERSION} — installed by the StarCi runtime (scripts/guards/hook-install.mjs); rewritten on every op dispatch.
|
|
125
|
+
# The shared branch is append-only (modules/ops/_common.yaml "Evidence, completion and commits"):
|
|
126
|
+
# a protected branch only moves forward and is never deleted. An op - named by the Orca terminal it runs in
|
|
127
|
+
# (<guards root>/terminals/<handle>.json) - never creates a worktree, and the commits it lands carry only its
|
|
128
|
+
# owned paths (starci guard verify-commit).
|
|
129
|
+
if [ "$1" != "prepared" ]; then exit 0; fi
|
|
130
|
+
PROTECTED=" ${protectedList} "
|
|
131
|
+
guard=""
|
|
132
|
+
if [ -n "\${ORCA_TERMINAL_HANDLE:-}" ]; then
|
|
133
|
+
guard=${q(terminals)}/"$(printf '%s' "$ORCA_TERMINAL_HANDLE" | tr -c 'A-Za-z0-9._-' '_')".json
|
|
134
|
+
fi
|
|
135
|
+
[ -n "$guard" ] && [ -f "$guard" ] || guard=""
|
|
136
|
+
# A kernel guard (role "kernel", writeJobGuard) is the command guard's alone: the runtime's own git under the Kernel's
|
|
137
|
+
# api calls inherits its terminal, so the op rules below never read it.
|
|
138
|
+
if [ -n "$guard" ] && grep -q '"role": "kernel"' "$guard"; then guard=""; fi
|
|
139
|
+
status=0
|
|
140
|
+
op_worktree_refused=0
|
|
141
|
+
while read -r old new ref; do
|
|
142
|
+
case "$ref" in
|
|
143
|
+
HEAD)
|
|
144
|
+
# An op worker never creates a git worktree. \`git worktree add\` writes the new
|
|
145
|
+
# worktree's HEAD from the checkout it runs in, whose own HEAD is not locked: however the worker ran git, the
|
|
146
|
+
# new worktree's first ref update is refused here.
|
|
147
|
+
if [ -n "$guard" ] && [ "$op_worktree_refused" = 0 ]; then
|
|
148
|
+
gd=$(git rev-parse --git-dir 2>/dev/null)
|
|
149
|
+
fmt=$(git rev-parse --show-ref-format 2>/dev/null)
|
|
150
|
+
# A git that predates --show-ref-format (before 2.46) echoes the flag back and has only the files backend.
|
|
151
|
+
case "$fmt" in files|--show-ref-format) fmt=files ;; esac
|
|
152
|
+
if [ -n "$gd" ] && [ "$fmt" = "files" ] && [ ! -e "$gd/HEAD.lock" ]; then
|
|
153
|
+
echo "starci history guard: refused - an op worker never creates a git worktree; work in the checkout you were dispatched to (a private worktree with links into the live repository deleted live files)" >&2
|
|
154
|
+
op_worktree_refused=1; status=1
|
|
155
|
+
fi
|
|
156
|
+
fi ;;
|
|
157
|
+
refs/heads/*)
|
|
158
|
+
b="\${ref#refs/heads/}"
|
|
159
|
+
case "$PROTECTED" in *" $b "*) ;; *) continue ;; esac
|
|
160
|
+
case "$new" in *[!0]*) ;; *)
|
|
161
|
+
echo "starci history guard: refused deleting protected branch $b" >&2; status=1; continue ;; esac
|
|
162
|
+
case "$old" in *[!0]*) ;; *) old=$(git rev-parse -q --verify "$ref^{commit}" 2>/dev/null) ;; esac
|
|
163
|
+
[ -z "$old" ] && continue
|
|
164
|
+
if ! git merge-base --is-ancestor "$old" "$new" 2>/dev/null; then
|
|
165
|
+
# The runtime rewinds a workflow branch to its last checkpoint (preserveAndReset) naming the target in STARCI_BRANCH_REWIND; that is
|
|
166
|
+
# allowed only while the commit it leaves is kept under refs/heads/preserved, so nothing is lost.
|
|
167
|
+
if [ "\${STARCI_BRANCH_REWIND:-}" = "$new" ] && [ -n "$(git for-each-ref --contains "$old" --format=x refs/heads/preserved 2>/dev/null)" ]; then continue; fi
|
|
168
|
+
echo "starci history guard: refused moving protected branch $b from $old to $new - not a fast-forward (reset/amend/rebase rewrite a branch other workflows commit on; undo a commit with git revert)" >&2
|
|
169
|
+
status=1; continue
|
|
170
|
+
fi
|
|
171
|
+
if [ -n "$guard" ]; then
|
|
172
|
+
STARCI_GUARD_FILE="$guard" STARCI_RUNTIME=${q(root)} ${q(nodePath)} ${q(path.join(root, 'packages', 'cli', 'bin', 'starci.mjs'))} guard verify-commit "$old" "$new" || status=1
|
|
173
|
+
fi ;;
|
|
174
|
+
esac
|
|
175
|
+
done
|
|
176
|
+
exit $status
|
|
177
|
+
`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// A relative core.hooksPath (husky's .husky/_) resolves per checkout: husky generates that directory, with
|
|
181
|
+
// its own `.gitignore` of `*`, only where `npm install` ran, so a linked worktree has none. A hooks directory
|
|
182
|
+
// that does not exist yet, holds no file and has nothing tracked is given husky's own self-ignoring layout;
|
|
183
|
+
// anything else stays refused. True when the hook file may be written.
|
|
184
|
+
const admitFreshHooksDir = (root, hooksDir, isIgnored) => {
|
|
185
|
+
const relDir = path.relative(root, hooksDir).replaceAll('\\', '/');
|
|
186
|
+
const absent = !fs.existsSync(hooksDir);
|
|
187
|
+
const empty = absent || (fs.statSync(hooksDir).isDirectory() && fs.readdirSync(hooksDir).length === 0);
|
|
188
|
+
const tracked = git(lsFiles, root, ['--', relDir]);
|
|
189
|
+
if (!empty || tracked.status !== 0 || tracked.stdout.trim()) return false;
|
|
190
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
191
|
+
fs.writeFileSync(path.join(hooksDir, '.gitignore'), '*\n');
|
|
192
|
+
if (isIgnored()) return true;
|
|
193
|
+
if (absent) safeRemove(hooksDir, { hold: artifactHoldReason });
|
|
194
|
+
else fs.rmSync(path.join(hooksDir, '.gitignore'), { force: true });
|
|
195
|
+
return false;
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* hookTarget(repoRoot, name) -> {root, hooksDir, file} | {installed: false, reason, path?}
|
|
200
|
+
* Resolves where hook `name` goes: into the repository's effective hooks
|
|
201
|
+
* directory (core.hooksPath, e.g. husky's .husky/_, else .git/hooks). A foreign
|
|
202
|
+
* hook of that name is never overwritten; a hooks directory whose new file git
|
|
203
|
+
* would offer for tracking is left alone (no foreign file in a product repo),
|
|
204
|
+
* except an absent, untracked one (husky's .husky/_ in a linked worktree), which
|
|
205
|
+
* gets husky's own self-ignoring `.gitignore` of `*`.
|
|
206
|
+
*/
|
|
207
|
+
function hookTarget(repoRoot, name) {
|
|
208
|
+
const top = git(revParseQuery, repoRoot, ['--show-toplevel']);
|
|
209
|
+
if (top.status !== 0) return { installed: false, reason: 'not-a-git-checkout' };
|
|
210
|
+
const root = path.resolve(top.stdout.trim());
|
|
211
|
+
const hooks = git(revParseQuery, root, ['--git-path', 'hooks']);
|
|
212
|
+
if (hooks.status !== 0) return { installed: false, reason: 'no-hooks-path' };
|
|
213
|
+
const hooksDir = path.resolve(root, hooks.stdout.trim());
|
|
214
|
+
const file = path.join(hooksDir, name);
|
|
215
|
+
const inside = (parent, child) => { const rel = path.relative(parent, child); return !rel.startsWith('..') && !path.isAbsolute(rel); };
|
|
216
|
+
const inWorktree = inside(root, hooksDir) && !inside(path.join(root, '.git'), hooksDir);
|
|
217
|
+
if (inWorktree && !fs.existsSync(file)) {
|
|
218
|
+
const rel = path.relative(root, file).replaceAll('\\', '/');
|
|
219
|
+
const isIgnored = () => git(checkIgnore, root, ['-q', '--no-index', '--', rel]).status === 0;
|
|
220
|
+
if (!isIgnored() && !admitFreshHooksDir(root, hooksDir, isIgnored)) return { installed: false, reason: 'hooks-dir-tracked', path: file };
|
|
221
|
+
}
|
|
222
|
+
return { root, hooksDir, file };
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* ensureHistoryHook(repoRoot) -> {installed, path?, reason?}
|
|
227
|
+
* Writes the reference-transaction hook into the repository's effective hooks directory (hookTarget).
|
|
228
|
+
* A foreign hook of that name is never overwritten.
|
|
229
|
+
*/
|
|
230
|
+
export function ensureHistoryHook(repoRoot, { skillRoot = path.resolve(here, '..', '..'), nodePath = process.execPath } = {}) {
|
|
231
|
+
const target = hookTarget(repoRoot, 'reference-transaction');
|
|
232
|
+
if (target.installed === false) return target;
|
|
233
|
+
const { root, hooksDir, file } = target;
|
|
234
|
+
const branches = [];
|
|
235
|
+
const list = git(worktreeListQuery, root, ['--porcelain']);
|
|
236
|
+
if (list.status === 0) for (const line of list.stdout.split(/\r?\n/)) if (line.startsWith('branch refs/heads/')) branches.push(line.slice('branch refs/heads/'.length));
|
|
237
|
+
const body = historyHookBody({ branches, root: skillRoot, nodePath, terminals: terminalsDir(skillRoot) });
|
|
238
|
+
if (fs.existsSync(file)) {
|
|
239
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
240
|
+
if (!current.includes(HOOK_MARKER)) return { installed: false, reason: 'foreign-hook', path: file };
|
|
241
|
+
if (current === body) return { installed: true, path: file, changed: false };
|
|
242
|
+
}
|
|
243
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
244
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
245
|
+
fs.writeFileSync(tmp, body, { mode: 0o700 });
|
|
246
|
+
fs.renameSync(tmp, file);
|
|
247
|
+
try { fs.chmodSync(file, 0o700); } catch { /* windows */ }
|
|
248
|
+
return { installed: true, path: file, changed: true };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/** The work-record check shared by the dispatch hook and the runtime's generated pre-commit hook. */
|
|
252
|
+
export function workHookCheck({ root, nodePath = process.execPath }) {
|
|
253
|
+
const q = shellQuote;
|
|
254
|
+
return String.raw`# ${WORK_HOOK_MARKER} v${WORK_HOOK_VERSION} - installed by the StarCi runtime (scripts/guards/hook-install.mjs); rewritten on every op dispatch.
|
|
255
|
+
# Staged Work and stack files are checked before the commit exists: YAML that parses, records that pass their scoped
|
|
256
|
+
# strict validation, and no secret outside an .enc file (starci work hygiene). e2e never runs here.
|
|
257
|
+
if git diff --cached --name-only --diff-filter=ACMR | grep -Eq '(^|/)\.(starciwork|starcistacks)/'; then
|
|
258
|
+
STARCI_RUNTIME=${q(root)} ${q(nodePath)} ${q(path.join(root, 'packages', 'cli', 'bin', 'starci.mjs'))} work hygiene staged --repo "$(git rev-parse --show-toplevel)" || exit 1
|
|
259
|
+
fi
|
|
260
|
+
`;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* The pre-commit hook body. The check runs only when the index holds a file under .starciwork/ or .starcistacks/ (a
|
|
265
|
+
* commit of anything else costs one git call). A husky dispatcher that shared the hook's place is chained after it.
|
|
266
|
+
*/
|
|
267
|
+
function workHookBody({ root, nodePath = process.execPath }) {
|
|
268
|
+
return `#!/bin/sh
|
|
269
|
+
${workHookCheck({ root, nodePath })}
|
|
270
|
+
# husky's generated dispatcher (.husky/_/h) keeps running the repository's own pre-commit
|
|
271
|
+
if [ -f "$(dirname "$0")/h" ]; then . "$(dirname "$0")/h"; fi
|
|
272
|
+
`;
|
|
273
|
+
}
|
|
274
|
+
// husky 9 writes this two-line dispatcher into .husky/_/<hook>; it is generated output, so it may be wrapped.
|
|
275
|
+
const HUSKY_DISPATCHER = /^#!\/usr\/bin\/env sh\s+\.\s+"\$\(dirname "\$0"\)\/h"\s*$/;
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* ensureWorkHook(repoRoot) -> {installed, path?, reason?, changed?}
|
|
279
|
+
* Writes the pre-commit hook next to ensureHistoryHook's, by the same hookTarget rules. A foreign hook is never
|
|
280
|
+
* overwritten, except husky's generated dispatcher, which the new hook wraps.
|
|
281
|
+
*/
|
|
282
|
+
export function ensureWorkHook(repoRoot, { skillRoot = path.resolve(here, '..', '..'), nodePath = process.execPath } = {}) {
|
|
283
|
+
const target = hookTarget(repoRoot, 'pre-commit');
|
|
284
|
+
if (target.installed === false) return target;
|
|
285
|
+
const { hooksDir, file } = target;
|
|
286
|
+
const body = workHookBody({ root: skillRoot, nodePath });
|
|
287
|
+
if (fs.existsSync(file)) {
|
|
288
|
+
const current = fs.readFileSync(file, 'utf8');
|
|
289
|
+
if (current.includes(RUNTIME_GIT_HOOKS_MARKER)) return { installed: true, path: file, changed: false };
|
|
290
|
+
if (current.includes(WORK_HOOK_MARKER)) { if (current === body) return { installed: true, path: file, changed: false }; }
|
|
291
|
+
else if (!HUSKY_DISPATCHER.test(current)) return { installed: false, reason: 'foreign-hook', path: file };
|
|
292
|
+
}
|
|
293
|
+
fs.mkdirSync(hooksDir, { recursive: true });
|
|
294
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
295
|
+
fs.writeFileSync(tmp, body, { mode: 0o700 });
|
|
296
|
+
fs.renameSync(tmp, file);
|
|
297
|
+
try { fs.chmodSync(file, 0o700); } catch { /* windows */ }
|
|
298
|
+
return { installed: true, path: file, changed: true };
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const guardSettings = (config) => ({
|
|
302
|
+
historyHook: config?.guards?.historyHook !== false,
|
|
303
|
+
workHook: config?.guards?.workHook !== false,
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* guardLaunch({jobId, workflowId, ledgerRepo, owned, repos, config, workflowWorktree, role, op}) -> {receipt}
|
|
308
|
+
* receipt rides on the dispatch record. The caller binds receipt.jobFile to the agent's Orca terminal once
|
|
309
|
+
* worker-start returns it (bindGuardTerminal), which is what the command guard and the history hook read.
|
|
310
|
+
*/
|
|
311
|
+
export function guardLaunch({ skillRoot = path.resolve(here, '..', '..'), jobId, workflowId, ledgerRepo, owned = [], repos = [], config = null, workflowWorktree = null, role = 'op', op = null }) {
|
|
312
|
+
const settings = guardSettings(config);
|
|
313
|
+
const receipt = { jobFile: null, hooks: [] };
|
|
314
|
+
try { receipt.jobFile = writeJobGuard({ skillRoot, jobId, workflowId, ledgerRepo, owned, workflowWorktree, role, op }); }
|
|
315
|
+
catch (e) { receipt.jobFile = { error: String(e?.message ?? e) }; }
|
|
316
|
+
const installPerRepo = (repos, install, sink) => {
|
|
317
|
+
for (const repo of new Set(repos.filter(Boolean).map((r) => path.resolve(r)))) {
|
|
318
|
+
try { sink.push({ repo, ...install(repo) }); }
|
|
319
|
+
catch (e) { sink.push({ repo, installed: false, reason: String(e?.message ?? e) }); }
|
|
320
|
+
}
|
|
321
|
+
};
|
|
322
|
+
if (settings.historyHook) installPerRepo(repos, (repo) => ensureHistoryHook(repo, { skillRoot }), receipt.hooks);
|
|
323
|
+
else receipt.hooks = [{ disabled: true }];
|
|
324
|
+
if (settings.workHook) { receipt.workHooks = []; installPerRepo(repos, (repo) => ensureWorkHook(repo, { skillRoot }), receipt.workHooks); }
|
|
325
|
+
else receipt.workHooks = [{ disabled: true }];
|
|
326
|
+
return { receipt };
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* The layers a dispatch receipt (guardLaunch's, as starci kernel dispatch records it on op-dispatched `guard`) says did not
|
|
331
|
+
* install: [] for a whole guard. A switched-off layer is not a failure.
|
|
332
|
+
*/
|
|
333
|
+
export function guardReceiptErrors(receipt) {
|
|
334
|
+
if (!receipt || typeof receipt !== 'object') return [];
|
|
335
|
+
const out = [];
|
|
336
|
+
const err = (layer, value) => { if (value) out.push(`${layer}: ${String(value).slice(0, 160)}`); };
|
|
337
|
+
err('guard', receipt.error);
|
|
338
|
+
err('jobFile', receipt.jobFile?.error);
|
|
339
|
+
err('terminal', receipt.terminal?.error);
|
|
340
|
+
for (const hook of Array.isArray(receipt.hooks) ? receipt.hooks : []) if (hook?.installed === false) err(`history hook ${hook.repo ?? ''}`.trim(), hook.reason ?? 'not installed');
|
|
341
|
+
for (const hook of Array.isArray(receipt.workHooks) ? receipt.workHooks : []) if (hook?.installed === false) err(`work hook ${hook.repo ?? ''}`.trim(), hook.reason ?? 'not installed');
|
|
342
|
+
return out;
|
|
343
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { HOOK_INPUT_LIMITS } from '../lib/hook-limits.mjs';
|
|
2
|
+
export { HOOK_INPUT_LIMITS } from '../lib/hook-limits.mjs';
|
|
3
|
+
|
|
4
|
+
/** Keep fail-open and fail-closed input diagnostics on one stderr line, including parser errors containing input. */
|
|
5
|
+
export const hookErrorMessage = (error) => String(error?.message ?? error).replace(/[\r\n\u2028\u2029]+/g, ' ');
|
|
6
|
+
|
|
7
|
+
/** Flush hook output before explicit exit; false means the output deadline elapsed before the write completed. */
|
|
8
|
+
export const writeHookOutput = (stream, text) => new Promise((resolve, reject) => {
|
|
9
|
+
let settled = false;
|
|
10
|
+
const onError = (error) => done(error);
|
|
11
|
+
const done = (error) => {
|
|
12
|
+
if (settled) return;
|
|
13
|
+
settled = true;
|
|
14
|
+
clearTimeout(timer);
|
|
15
|
+
// A Writable reports callback failure before emitting its error event. Keep the listener through that turn.
|
|
16
|
+
if (error) setImmediate(() => stream.removeListener?.('error', onError));
|
|
17
|
+
else stream.removeListener?.('error', onError);
|
|
18
|
+
if (error) reject(error);
|
|
19
|
+
else resolve(true);
|
|
20
|
+
};
|
|
21
|
+
const timer = setTimeout(() => {
|
|
22
|
+
settled = true;
|
|
23
|
+
stream.removeListener?.('error', onError);
|
|
24
|
+
resolve(false);
|
|
25
|
+
}, HOOK_INPUT_LIMITS.outputTimeoutMs);
|
|
26
|
+
stream.once?.('error', onError);
|
|
27
|
+
try { stream.write(text, done); }
|
|
28
|
+
catch (error) { done(error); }
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Consume one EOF-delimited hook payload within a wall-clock deadline and byte budget. The stream belongs to this
|
|
33
|
+
* one-shot hook: every outcome releases its input handle and listeners. Callers choose their existing failure policy.
|
|
34
|
+
*/
|
|
35
|
+
export const readInput = (stream, { timeoutMs = HOOK_INPUT_LIMITS.minInputWindowMs, maxBytes = HOOK_INPUT_LIMITS.maxBytes } = {}) => new Promise((resolve, reject) => {
|
|
36
|
+
const chunks = [];
|
|
37
|
+
const deadline = performance.now() + timeoutMs;
|
|
38
|
+
const deadlineError = () => Object.assign(new Error(`hook stdin deadline exceeded (${timeoutMs} ms)`), { kind: 'deadline' });
|
|
39
|
+
let bytes = 0;
|
|
40
|
+
let settled = false;
|
|
41
|
+
let closeCheck;
|
|
42
|
+
const finish = (error) => {
|
|
43
|
+
if (settled) return;
|
|
44
|
+
if (!error && performance.now() >= deadline) error = deadlineError();
|
|
45
|
+
settled = true;
|
|
46
|
+
clearTimeout(timer);
|
|
47
|
+
clearImmediate(closeCheck);
|
|
48
|
+
stream.removeListener('data', onData);
|
|
49
|
+
stream.removeListener('end', onEnd);
|
|
50
|
+
stream.removeListener('error', onError);
|
|
51
|
+
stream.removeListener('close', onClose);
|
|
52
|
+
stream.pause();
|
|
53
|
+
stream.unref?.();
|
|
54
|
+
stream.destroy();
|
|
55
|
+
if (error) reject(error);
|
|
56
|
+
else resolve(Buffer.concat(chunks, bytes).toString('utf8'));
|
|
57
|
+
};
|
|
58
|
+
const onData = (chunk) => {
|
|
59
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
60
|
+
bytes += buffer.length;
|
|
61
|
+
if (bytes > maxBytes) finish(Object.assign(new Error(`hook stdin exceeds ${maxBytes} bytes; split the tool input`), { kind: 'too-large' }));
|
|
62
|
+
else chunks.push(buffer);
|
|
63
|
+
};
|
|
64
|
+
const onEnd = () => finish();
|
|
65
|
+
const onError = (error) => finish(error);
|
|
66
|
+
const onClose = () => finish(stream.errored ?? new Error('hook stdin closed before EOF'));
|
|
67
|
+
// Keep the timer referenced so an idle pipe still reaches the caller's bound-session refusal.
|
|
68
|
+
const timer = setTimeout(() => finish(deadlineError()), timeoutMs);
|
|
69
|
+
stream.on('data', onData);
|
|
70
|
+
stream.once('end', onEnd);
|
|
71
|
+
stream.once('error', onError);
|
|
72
|
+
stream.once('close', onClose);
|
|
73
|
+
// destroy(error) marks a stream closed before its queued error event; let that event reach our listener first.
|
|
74
|
+
if (stream.destroyed) closeCheck = setImmediate(onClose);
|
|
75
|
+
else if (stream.readableEnded) onEnd();
|
|
76
|
+
});
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { hookErrorMessage } from './hook-io.mjs';
|
|
2
|
+
|
|
3
|
+
/** One IPC request from the owning hook; the process ends after its result reaches that parent. */
|
|
4
|
+
export function receiveVerdict(decide) {
|
|
5
|
+
process.once('disconnect', () => process.exit(2));
|
|
6
|
+
process.once('message', async ({ input, root, bindings }) => {
|
|
7
|
+
let result;
|
|
8
|
+
try { result = { ok: true, decision: await decide(input, { root, bindings }) }; }
|
|
9
|
+
catch (error) { result = { ok: false, error: hookErrorMessage(error) }; }
|
|
10
|
+
process.send(result, (error) => process.exit(error ? 2 : 0));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
// The refusals of install-family and Kernel-mailbox commands, split out of command-guard.mjs (which calls them).
|
|
2
|
+
/** The install-through-link refusal of one npm/pnpm/yarn command (no guard needed), or null. `npm`: deps-guard.mjs. */
|
|
3
|
+
export function installLinkVerdict({ program, args, cwd, npm }) {
|
|
4
|
+
if (npm.classifyInstall(program, args).kind === 'pass') return null;
|
|
5
|
+
const linked = npm.linkedNodeModulesOf(program, args, cwd);
|
|
6
|
+
if (!linked) return null;
|
|
7
|
+
return { tool: program, code: 'DEPS_THROUGH_LINK', command: `${program} ${args.join(' ')}`.slice(0, 200),
|
|
8
|
+
reason: `${linked.nodeModules} is a link to ${linked.target ?? 'another tree'}: ${program} would empty that live node_modules`,
|
|
9
|
+
remedy: 'node_modules here is a link to another checkout: unlink it first (cmd /c rmdir) and install for real' };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function installVerdict({ program, args, cwd, guard, deps }) {
|
|
13
|
+
const { classifyInstall, peerLeasedJobs } = deps.npm;
|
|
14
|
+
const kind = classifyInstall(program, args).kind;
|
|
15
|
+
if (kind === 'pass') return null;
|
|
16
|
+
const command = `${program} ${args.join(' ')}`.slice(0, 200);
|
|
17
|
+
const linked = installLinkVerdict({ program, args, cwd, npm: deps.npm });
|
|
18
|
+
if (linked) return linked;
|
|
19
|
+
if (kind !== 'clean-install' || !guard?.ledgerRepo) return null;
|
|
20
|
+
let peers;
|
|
21
|
+
try { peers = await peerLeasedJobs({ ledgerRepo: guard.ledgerRepo, workflowId: guard.workflowId }); }
|
|
22
|
+
catch (e) { deps.say(`starci guard: could not read peer leases (${e?.message ?? e})`); return null; }
|
|
23
|
+
if (!peers.jobs.length) return null;
|
|
24
|
+
const names = peers.jobs.slice(0, 5).map((j) => `${j.jobId} (${j.workflowId})`).join(', ');
|
|
25
|
+
return { tool: 'npm', code: 'DEPS_DELETE_WHILE_PEER_LEASED', command,
|
|
26
|
+
reason: `${command} deletes node_modules while other workflows' jobs run checks from it: ${names}`,
|
|
27
|
+
remedy: 'use `npm install`, or report blocked environment naming the missing dependency' };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The Kernel never reads Orca's mailbox itself: `orchestration check --ack` consumes deliveries before the ledger records
|
|
31
|
+
// them, so it reads through `starci kernel messages` / `starci kernel questions` and the runtime's api drains (lane MAILC). Ops and
|
|
32
|
+
// [Worker]s keep `check`: Orca's worker protocol has them read their own Run's deliveries (modules/host/orca/api.yaml
|
|
33
|
+
// operationAgent), and no ledger record depends on those.
|
|
34
|
+
export function kernelMailboxVerdict(program, args, guard) {
|
|
35
|
+
if (program !== 'orca' || guard?.role !== 'kernel') return null;
|
|
36
|
+
const words = args.filter((a) => !a.startsWith('-'));
|
|
37
|
+
if (words[0] !== 'orchestration' || words[1] !== 'check') return null;
|
|
38
|
+
return { code: 'KERNEL_ORCA_CHECK', command: ['orca', ...args].join(' ').slice(0, 200),
|
|
39
|
+
reason: 'the Kernel never runs orca orchestration check: its --ack consumes deliveries before the ledger records them',
|
|
40
|
+
remedy: 'read messages with `starci kernel messages --repo <repo> --workflow <id>` and questions with `starci kernel questions`; the runtime drains the mailbox' };
|
|
41
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// kernel-seat.mjs — what a Kernel seat (a bound guard of role kernel) may run, from the `kernel` table of
|
|
2
|
+
// modules/kernel/command-policy.yaml. The PreToolUse guard and the PATH shims evaluate it (command-policy.mjs policyVerdict): a
|
|
3
|
+
// mutating verb is the runtime's, never the seat's. Refusals name the status and decision verbs without running them.
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { plainWords } from '../lib/menu-parts.mjs';
|
|
6
|
+
import { redirectTargetsOf, refusal } from './rights.mjs';
|
|
7
|
+
import { kernelMailboxVerdict } from './install-verdict.mjs';
|
|
8
|
+
import { orcaSelfLifecycleAllowed } from './orca-self-lifecycle.mjs';
|
|
9
|
+
|
|
10
|
+
const OPTION = /^--?([A-Za-z][\w-]*)(?:=.*)?$/;
|
|
11
|
+
const flagsOf = (args) => new Set(args.map((value) => OPTION.exec(String(value))?.[1]).filter(Boolean));
|
|
12
|
+
|
|
13
|
+
/** The first two non-option words of a starci call: its group and verb. */
|
|
14
|
+
/** Whether the Kernel seat may run `starci <group> <verb>` with these option names: {allowed, reason?}. */
|
|
15
|
+
function kernelSeatAllowsVerb(table, group, verb, flags = new Set()) {
|
|
16
|
+
if (!(table?.verbs?.[group] ?? []).includes(verb)) return { allowed: false, reason: `starci ${group} ${verb} is not a verb of the Kernel seat` };
|
|
17
|
+
const blocked = (table['read-flags']?.[verb] ?? []).find((flag) => flags.has(flag));
|
|
18
|
+
return blocked ? { allowed: false, reason: `starci ${group} ${verb} --${blocked} mutates the workflow` } : { allowed: true };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The refusal of a call that redirects output into a file, or null. The Kernel writes no file: a redirection (> file, 2>file, >> file) leaves a stray in the workflow
|
|
23
|
+
* tree it works in, which a later fail decision of that tree then has to recover from (live: a file named 0, left by a redirect, made every settle-fail of a job unrecoverable).
|
|
24
|
+
* The null devices are not files; the status text is read with --field, never redirected.
|
|
25
|
+
*/
|
|
26
|
+
function kernelRedirectVerdict({ command, cwd }) {
|
|
27
|
+
const targets = redirectTargetsOf(command, cwd);
|
|
28
|
+
if (!targets.length) return null;
|
|
29
|
+
const use = 'read with starci kernel status --field <name> or starci kernel logs; the call needs no file';
|
|
30
|
+
return refusal('KERNEL_NO_FILE_WRITE', command, `the Kernel seat writes no file: the redirection into ${path.basename(targets[0])} would leave a stray file in its workflow tree`, use, use);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** The Kernel-seat verdicts of one shell call as a tool verdict, or null for any other role: today the no-file-write rule. */
|
|
34
|
+
export const kernelSeatCallVerdict = ({ role, command, cwd }) => {
|
|
35
|
+
const v = role === 'lead' ? kernelRedirectVerdict({ command, cwd }) : null;
|
|
36
|
+
return v ? { tool: 'shell', ...v } : null;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const useOf = (table) => table.use ?? 'starci kernel decide';
|
|
40
|
+
|
|
41
|
+
/** The refusal of a call a Kernel seat may not make, or null when the seat may run it. Programs other than starci are refused unless pure reads. */
|
|
42
|
+
export function kernelSeatVerdict({ policy, program, args, text, guard = null, handle = null }) {
|
|
43
|
+
const table = policy?.kernel;
|
|
44
|
+
if (!table) return null;
|
|
45
|
+
// The seat is an Orca worker: its own heartbeat, ask and worker_done stay, and its mailbox stays the runtime's.
|
|
46
|
+
if (program === 'orca') {
|
|
47
|
+
const mailbox = kernelMailboxVerdict('orca', args, guard);
|
|
48
|
+
if (mailbox) return mailbox;
|
|
49
|
+
if (orcaSelfLifecycleAllowed({ role: 'lead', args, handle, policy })) return null;
|
|
50
|
+
}
|
|
51
|
+
if (program === 'starci') {
|
|
52
|
+
const [group, verb] = plainWords(args, 2);
|
|
53
|
+
const verdict = kernelSeatAllowsVerb(table, group, verb, flagsOf(args));
|
|
54
|
+
if (verdict.allowed) return null;
|
|
55
|
+
const use = useOf(table);
|
|
56
|
+
return refusal('KERNEL_USE_DECIDE', text, `${verdict.reason}: the runtime runs it, and the Kernel answers the items of its menu`, use, use);
|
|
57
|
+
}
|
|
58
|
+
const reads = new Set((policy.read ?? []).map(String));
|
|
59
|
+
const denied = new Set((table['deny-programs'] ?? []).map(String));
|
|
60
|
+
if (reads.has(program) && !denied.has(program)) return null;
|
|
61
|
+
const use = useOf(table);
|
|
62
|
+
return refusal('KERNEL_STARCI_ONLY', text, `the Kernel seat runs starci verbs and pure reads: ${program} is neither`, use, use);
|
|
63
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// The refusals of launches outside the runtime (raw terminal create, headless agent CLIs, an agent's own Orca worktree, a test
|
|
2
|
+
// world in a uat op), split out of command-guard.mjs.
|
|
3
|
+
// An agent launch outside Orca is invisible to it: no liveness, no stop, no release, and a dead worker sits unnoticed
|
|
4
|
+
// (owner rule 2026-10-01). Every agent and every worker terminal starts through `orca orchestration worker-start`.
|
|
5
|
+
const WORKER_START = '`orca orchestration worker-start --agent <provider> [--model <id>] --worktree <selector> --spec "<task>" --task-title "<title>"`, supervised with worker-show / worker-read / worker-stop / worker-release';
|
|
6
|
+
// The first non-flag word of `args`, skipping the values of `valued` flags.
|
|
7
|
+
const subcommandOf = (args, valued) => {
|
|
8
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
9
|
+
const a = args[i];
|
|
10
|
+
if (a === '--') return args[i + 1] ?? null;
|
|
11
|
+
if (a.startsWith('-')) {
|
|
12
|
+
if (valued.test(a)) { i += 1; }
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
return a;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
};
|
|
19
|
+
const CODEX_VALUED = /^(?:-m|--model|-c|--config|-C|--cd|-s|--sandbox|-a|--ask-for-approval|-p|--profile|-i|--image|--enable|--disable|--add-dir|--local-provider)$/;
|
|
20
|
+
const printFlag = (args, flags) => {
|
|
21
|
+
const options = args.includes('--') ? args.slice(0, args.indexOf('--')) : args;
|
|
22
|
+
return options.find((a) => flags.includes(a)) ?? null;
|
|
23
|
+
};
|
|
24
|
+
// The one headless call the runtime makes for an op (tiers.yaml calls.imagegen; owner decision 2026-10-07).
|
|
25
|
+
const IMAGE_CALL = ' An image is generated with `starci work imagegen` (interface.draw, interface.asset and brand.decide), never with a hand-run codex exec.';
|
|
26
|
+
const launchRefusal = (program, args, how) => ({ command: [program, ...args].join(' ').slice(0, 200),
|
|
27
|
+
reason: `${how} outside Orca's supervision: no liveness, no stop or release, and a dead worker goes unnoticed (owner rule 2026-10-01)`,
|
|
28
|
+
remedy: `launch every agent and worker through ${WORKER_START}${program === 'codex' ? IMAGE_CALL : ''}` });
|
|
29
|
+
// The workspace and runtime rules (workspace.manage, runtime.operate; OPS2 3.1): an agent never creates or removes an Orca
|
|
30
|
+
// worktree itself - the runtime's worktree API (scripts/machine/worktree-orca.mjs createOrcaWorktree / removeOrcaWorktree,
|
|
31
|
+
// releaseWorkflowWorktree) runs the link check first and keeps the registry and its cap (git worktree writes are
|
|
32
|
+
// git-policy.mjs WORKTREE_NOT_OPS).
|
|
33
|
+
const ORCA_WORKTREE_WRITES = new Set(['create', 'add', 'rm', 'remove', 'delete', 'prune', 'move']);
|
|
34
|
+
// The uat rule (uat.verify, uat.assisted.*; OPS2 3.1): a walk runs on the app's real dev stack, never a test world.
|
|
35
|
+
const TEST_WORLD_RUNNER = /(?:^|[\\/])test-world-run\.mjs$/;
|
|
36
|
+
const testWorldOf = (program, args) => {
|
|
37
|
+
if (program === 'node' && args.some((a) => TEST_WORLD_RUNNER.test(a))) return 'test-world-run.mjs';
|
|
38
|
+
if (program !== 'starci') return null;
|
|
39
|
+
const words = args.filter((a) => !a.startsWith('-'));
|
|
40
|
+
return words[0] === 'gate' && words[1] === 'test-world' ? 'starci gate test-world' : null;
|
|
41
|
+
};
|
|
42
|
+
const headlessHow = (program, args) => {
|
|
43
|
+
if (program === 'codex') {
|
|
44
|
+
const sub = subcommandOf(args, CODEX_VALUED);
|
|
45
|
+
return sub === 'exec' || sub === 'e' ? `codex ${sub} runs a headless Codex agent` : null;
|
|
46
|
+
}
|
|
47
|
+
if (program === 'claude' || program === 'cursor-agent' || program === 'devin') {
|
|
48
|
+
const flag = printFlag(args, ['-p', '--print']);
|
|
49
|
+
return flag ? `${program} ${flag} runs a headless agent` : null;
|
|
50
|
+
}
|
|
51
|
+
if (program === 'gemini') {
|
|
52
|
+
const flag = printFlag(args, ['-p', '--prompt']);
|
|
53
|
+
return flag ? `gemini ${flag} runs a headless agent` : null;
|
|
54
|
+
}
|
|
55
|
+
if (program === 'opencode' && subcommandOf(args, /^(?:-m|--model|--agent|--log-level)$/) === 'run') return 'opencode run runs a headless agent';
|
|
56
|
+
return null;
|
|
57
|
+
};
|
|
58
|
+
export const launchVerdict = (program, args, guard = null) => {
|
|
59
|
+
if (program === 'orca') {
|
|
60
|
+
const words = args.filter((a) => !a.startsWith('-'));
|
|
61
|
+
if (words[0] === 'terminal' && words[1] === 'create') return { code: 'RAW_TERMINAL_CREATE', ...launchRefusal(program, args, 'a raw terminal create starts a terminal outside worker-start') };
|
|
62
|
+
if (words[0] === 'worktree' && ORCA_WORKTREE_WRITES.has(words[1])) return { code: 'AGENT_ORCA_WORKTREE', command: [program, ...args].join(' ').slice(0, 200),
|
|
63
|
+
reason: `orca worktree ${words[1]}: an agent never creates or removes a worktree - a removal without the runtime's link check follows node_modules junctions into the live tree (inc-c8fbf76aa499; Orca itself walks junctions), and a tree outside the registry escapes its cap and GC`,
|
|
64
|
+
remedy: 'inspect with `starci machine worktrees counts`; remove clean merged lane worktrees with `starci machine worktrees-clean`; worktrees are created and removed only by the runtime worktree API (scripts/machine/worktree-orca.mjs createOrcaWorktree / removeOrcaWorktree, scripts/kernel/workflow-worktree.mjs releaseWorkflowWorktree); report a need you cannot meet as blocked environment' };
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const op = String(guard?.op ?? '');
|
|
68
|
+
const testWorld = op.startsWith('uat.') ? testWorldOf(program, args) : null;
|
|
69
|
+
if (testWorld) return { code: 'UAT_TEST_WORLD', command: [program, ...args].join(' ').slice(0, 200),
|
|
70
|
+
reason: `${op} walks the app's real dev stack: a test world (${testWorld}) fakes what the walk must prove`,
|
|
71
|
+
remedy: 'bring up the dev stack the app-root .starcistacks/<env> declares with its own start commands, check it with env-health, and walk it; a stack that will not come up is reported blocked environment' };
|
|
72
|
+
const how = headlessHow(program, args);
|
|
73
|
+
return how ? { code: 'AGENT_HEADLESS_LAUNCH', ...launchRefusal(program, args, how) } : null;
|
|
74
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// op-starci.mjs — the starci groups an Op never runs, from the `op` table of modules/kernel/command-policy.yaml. An Op's starci calls are
|
|
2
|
+
// its work: the capability verbs for servers, renders, image generation and tests, the checks, and its report to its Kernel. The control
|
|
3
|
+
// plane (the Supervisor, the reconciler, workflow start and routing, Debug) belongs to the controllers, the Kernel and the Supervisor.
|
|
4
|
+
import { refusal } from './rights.mjs';
|
|
5
|
+
|
|
6
|
+
/** The first non-option word of a starci call: its group. */
|
|
7
|
+
const groupOf = (args) => String(args.find((word) => !String(word).startsWith('-')) ?? '');
|
|
8
|
+
|
|
9
|
+
/** The refusal of a starci call of a group an Op never runs, or null. */
|
|
10
|
+
export function opStarciVerdict({ policy, program, args, text }) {
|
|
11
|
+
const table = policy?.op;
|
|
12
|
+
if (!table || program !== 'starci') return null;
|
|
13
|
+
const group = groupOf(args);
|
|
14
|
+
if (!(table['deny-groups'] ?? []).includes(group)) return null;
|
|
15
|
+
return refusal('RIGHTS_OP_CONTROL_PLANE', text, `starci ${group} is the control plane: an Op runs the capability verbs of its work and reports to its Kernel`, table.use, table.use);
|
|
16
|
+
}
|