@mnstry/atelier 0.2.0-alpha.6 → 0.2.0-alpha.8
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 +176 -0
- package/README.md +59 -15
- package/SECURITY.md +6 -1
- package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
- package/contracts/atelier-coauthor.v1.schema.json +163 -0
- package/contracts/atelier-decision-request.v1.schema.json +476 -0
- package/contracts/atelier-decision-result.v1.schema.json +500 -0
- package/contracts/atelier-guide.v1.schema.json +227 -0
- package/contracts/atelier-intake.v1.schema.json +151 -0
- package/contracts/atelier-migration.v2.schema.json +61 -0
- package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
- package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
- package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
- package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
- package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
- package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
- package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
- package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
- package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
- package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
- package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
- package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
- package/contracts/atelier-presentation.v1.schema.json +1293 -0
- package/contracts/atelier-review.v1.schema.json +1601 -0
- package/contracts/atelier-skill-steward.v1.schema.json +692 -0
- package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
- package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
- package/contracts/atelier-vault-publication.v1.schema.json +58 -0
- package/docs/assurance-controls.md +9 -1
- package/docs/atelier-sync.md +5 -4
- package/docs/blocks/claims.md +15 -10
- package/docs/blocks/will-not-do.md +4 -1
- package/docs/coauthor-session.md +57 -0
- package/docs/connected-composition.md +75 -0
- package/docs/decisions.md +167 -0
- package/docs/exact-upgrades.md +159 -0
- package/docs/extension-pack-lifecycle.md +33 -0
- package/docs/guided-upgrades.md +99 -0
- package/docs/install.md +65 -9
- package/docs/intake-and-guides.md +65 -0
- package/docs/integration-and-review-plan.md +409 -0
- package/docs/integration-contract-decisions.md +22 -0
- package/docs/local-review.md +84 -0
- package/docs/local-services.md +227 -0
- package/docs/local-state.md +107 -0
- package/docs/obsidian-contract.md +687 -0
- package/docs/obsidian.md +387 -0
- package/docs/portable-presentation.md +328 -0
- package/docs/presentation-consumer-boundaries.md +75 -0
- package/docs/presentation-review-disposition.md +47 -0
- package/docs/project-command-inventory.md +38 -0
- package/docs/project-options.md +53 -0
- package/docs/release-engineering.md +88 -0
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +55 -2
- package/docs/vault-service.md +292 -0
- package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
- package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
- package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
- package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
- package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
- package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
- package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
- package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
- package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
- package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
- package/fixtures/atelier-intake/valid/source.v1.json +7 -0
- package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
- package/fixtures/atelier-review/valid/bound.v1.json +229 -0
- package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
- package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
- package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
- package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
- package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
- package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
- package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
- package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
- package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
- package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
- package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
- package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
- package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
- package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
- package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
- package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
- package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
- package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
- package/fixtures/decisions/request/valid/triage.v1.json +60 -0
- package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
- package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
- package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
- package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
- package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
- package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
- package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
- package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
- package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
- package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
- package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
- package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
- package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
- package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
- package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
- package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
- package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
- package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
- package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
- package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
- package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
- package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
- package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
- package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
- package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
- package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
- package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
- package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
- package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
- package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
- package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
- package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
- package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
- package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
- package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
- package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
- package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
- package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
- package/fixtures/obsidian/edits/workspace.json +65 -0
- package/fixtures/obsidian/materialization/expected-full.json +99 -0
- package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
- package/fixtures/obsidian/materialization/workspace.json +54 -0
- package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
- package/fixtures/ui/presentation/pane.v1.json +7 -0
- package/fixtures/ui/presentation/reference.v1.json +293 -0
- package/package.json +57 -6
- package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/claude/atelier-open/SKILL.md +5 -0
- package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
- package/skills/claude/mnstry-readiness/SKILL.md +5 -0
- package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
- package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
- package/skills/codex/atelier-open/SKILL.md +5 -0
- package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
- package/skills/codex/mnstry-readiness/SKILL.md +5 -0
- package/src/access/preview-enforcer.mjs +84 -0
- package/src/boundary/policy.mjs +57 -25
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +43 -3
- package/src/coauthor/session.mjs +161 -0
- package/src/coauthor/store.mjs +173 -0
- package/src/collaboration/event-ledger.mjs +16 -33
- package/src/collaboration/inspection-bundle.mjs +223 -0
- package/src/collaboration/review-contracts.mjs +25 -0
- package/src/collaboration/review-store.mjs +305 -0
- package/src/commands/coauthor.mjs +26 -0
- package/src/commands/config.mjs +6 -1
- package/src/commands/extension-pack.mjs +15 -8
- package/src/commands/init.mjs +44 -5
- package/src/commands/obsidian.mjs +368 -0
- package/src/commands/review.mjs +147 -0
- package/src/commands/setup.mjs +51 -14
- package/src/commands/skills.mjs +199 -0
- package/src/commands/sync.mjs +8 -1
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +29 -0
- package/src/decisions/contracts.d.mts +96 -0
- package/src/decisions/contracts.mjs +321 -0
- package/src/disclosure/content-scan.mjs +13 -0
- package/src/export/atelier-export-contract.mjs +16 -1
- package/src/extension-packs/lifecycle.mjs +154 -0
- package/src/graph/graph.mjs +64 -2
- package/src/graph/knowledge-graph.mjs +548 -24
- package/src/guides/contracts.mjs +54 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +26 -27
- package/src/project/durable-state.mjs +161 -0
- package/src/project/file-class.mjs +166 -0
- package/src/project/private-state.mjs +11 -1
- package/src/projection/obsidian/contracts.mjs +571 -0
- package/src/projection/obsidian/edits/align.mjs +169 -0
- package/src/projection/obsidian/edits/apply.mjs +889 -0
- package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
- package/src/projection/obsidian/edits/contribution.mjs +108 -0
- package/src/projection/obsidian/edits/index.mjs +19 -0
- package/src/projection/obsidian/edits/object-identity.mjs +61 -0
- package/src/projection/obsidian/edits/object-store.mjs +517 -0
- package/src/projection/obsidian/edits/observe.mjs +123 -0
- package/src/projection/obsidian/edits/policy.mjs +123 -0
- package/src/projection/obsidian/edits/regions.mjs +648 -0
- package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
- package/src/projection/obsidian/materialize/index.mjs +7 -0
- package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
- package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
- package/src/projection/obsidian/materialize/settings.mjs +57 -0
- package/src/projection/obsidian/materialize/stage.mjs +27 -0
- package/src/projection/obsidian/proposals/adapter.mjs +572 -0
- package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
- package/src/projection/obsidian/proposals/contribution.mjs +55 -0
- package/src/projection/obsidian/proposals/index.mjs +23 -0
- package/src/projection/obsidian/proposals/observation.mjs +216 -0
- package/src/projection/obsidian/proposals/queue.mjs +277 -0
- package/src/projection/obsidian/proposals/router.mjs +168 -0
- package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
- package/src/projection/obsidian/publication/exchange.mjs +117 -0
- package/src/projection/obsidian/publication/index.mjs +5 -0
- package/src/projection/obsidian/publication/publisher.mjs +588 -0
- package/src/projection/obsidian/publication/test-seam.mjs +16 -0
- package/src/projection/obsidian/publication/transport.mjs +139 -0
- package/src/projection/obsidian/recovery/index.mjs +6 -0
- package/src/projection/obsidian/recovery/journal.mjs +134 -0
- package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
- package/src/projection/obsidian/recovery/restart.mjs +316 -0
- package/src/projection/obsidian/recovery/store.mjs +234 -0
- package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
- package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
- package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
- package/src/projection/obsidian/selection-ui/index.mjs +12 -0
- package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
- package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
- package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
- package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
- package/src/projection/project.mjs +28 -19
- package/src/readiness-protocols/evidence.mjs +282 -0
- package/src/readiness-protocols/runtime.mjs +4 -3
- package/src/readiness-protocols/source-read.mjs +39 -0
- package/src/runtime/obsidian/app-capability.mjs +110 -0
- package/src/runtime/obsidian/app-production-seams.mjs +96 -0
- package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
- package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
- package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
- package/src/runtime/obsidian/contributions.mjs +27 -0
- package/src/runtime/obsidian/documents.mjs +37 -0
- package/src/runtime/obsidian/enablement.mjs +53 -0
- package/src/runtime/obsidian/engine.mjs +532 -0
- package/src/runtime/obsidian/errors.mjs +14 -0
- package/src/runtime/obsidian/extension-points.mjs +103 -0
- package/src/runtime/obsidian/index.mjs +34 -0
- package/src/runtime/obsidian/lifecycle.mjs +258 -0
- package/src/runtime/obsidian/machine-settings.mjs +260 -0
- package/src/runtime/obsidian/observation.mjs +158 -0
- package/src/runtime/obsidian/opening.mjs +243 -0
- package/src/runtime/obsidian/pending-edits.mjs +141 -0
- package/src/runtime/obsidian/pipeline.mjs +122 -0
- package/src/runtime/obsidian/private-lock.mjs +156 -0
- package/src/runtime/obsidian/process-identity.mjs +58 -0
- package/src/runtime/obsidian/service-client.mjs +66 -0
- package/src/runtime/obsidian/service-main.mjs +89 -0
- package/src/runtime/obsidian/service-record.mjs +169 -0
- package/src/runtime/obsidian/service-server.mjs +121 -0
- package/src/runtime/obsidian/service.mjs +216 -0
- package/src/runtime/obsidian/startup-adapters.mjs +87 -0
- package/src/runtime/obsidian/state-store.mjs +180 -0
- package/src/runtime/obsidian/sync-notice.mjs +33 -0
- package/src/runtime/obsidian/tick-loop.mjs +84 -0
- package/src/runtime/obsidian/watchers.mjs +36 -0
- package/src/runtime/supervisor.mjs +16 -1
- package/src/server/local-sidecar.mjs +44 -0
- package/src/server/server.mjs +1 -0
- package/src/skills/steward.mjs +780 -0
- package/src/ui/presentation/browser.mjs +278 -0
- package/src/ui/presentation/contract.mjs +116 -0
- package/src/ui/presentation/index.mjs +7 -0
- package/src/ui/presentation/native.mjs +153 -0
- package/src/ui/presentation/proof.mjs +57 -0
- package/src/ui/presentation/schema-check.mjs +30 -0
- package/src/ui/presentation/schema.generated.mjs +1371 -0
- package/src/ui/presentation/state.mjs +43 -0
- package/src/ui/presentation/styles.mjs +67 -0
- package/src/ui/presentation/tokens.mjs +74 -0
- package/src/ui/presentation/web.mjs +85 -0
- package/src/ui/review-page.mjs +32 -0
- package/src/upgrade/explanation.mjs +33 -0
- package/src/upgrade/provenance.mjs +338 -0
- package/src/upgrade/transaction-files.mjs +103 -0
- package/src/upgrade/transaction.mjs +475 -0
- package/src/upgrade/upgrade.mjs +63 -28
- package/src/vault/hosts.mjs +21 -0
- package/src/vault/identity.mjs +19 -0
- package/src/vault/index.mjs +7 -0
- package/src/vault/interface.mjs +11 -0
- package/src/vault/metadata.mjs +31 -0
- package/src/vault/privacy.mjs +148 -0
- package/src/vault/probe.mjs +89 -0
- package/src/vault/service.mjs +131 -0
- package/src/vault/source.mjs +45 -0
- package/src/vault/storage.mjs +23 -0
- package/templates/distribution-workspace/.claude/launch.json +6 -2
- package/templates/distribution-workspace/gitignore +1 -0
- package/templates/distribution-workspace/project/README.md +2 -2
- package/templates/external-project-workspace/README.md +45 -0
- package/templates/external-project-workspace/answers.example.json +4 -0
- package/templates/external-project-workspace/atelier.project.json +40 -0
- package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
- package/templates/external-project-workspace/gitignore +8 -0
- package/templates/external-project-workspace/pack-lifecycle.json +14 -0
- package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
- package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
- package/templates/external-project-workspace/repo-access.v1.json +9 -0
- package/templates/external-project-workspace/source/README.md +11 -0
- package/templates/launch.json +6 -2
- package/templates/private-domain-workspace/.claude/launch.json +6 -2
- package/templates/private-domain-workspace/domain/README.md +2 -2
- package/templates/private-domain-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/.claude/launch.json +6 -2
- package/templates/shared-project-workspace/gitignore +1 -0
- package/templates/shared-project-workspace/project/README.md +2 -2
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { canonicalize } from '../attestation/jcs.mjs';
|
|
4
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs';
|
|
5
|
+
|
|
6
|
+
const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-guide.v1.schema.json', import.meta.url), 'utf8'));
|
|
7
|
+
export function guideDocumentDigest(value) { return createHash('sha256').update(canonicalize(value)).digest('hex'); }
|
|
8
|
+
export function validateGuideDocument(value, kind) {
|
|
9
|
+
if (!['offer', 'capability', 'consent', 'engagement'].includes(kind)) return ['unknown guide document kind'];
|
|
10
|
+
const errors = validateJsonSchema({ $schema: schema.$schema, $defs: schema.$defs, $ref: `#/$defs/${kind}` }, value);
|
|
11
|
+
if (kind === 'capability' && !errors.length) {
|
|
12
|
+
const url = new URL(value.serviceRef);
|
|
13
|
+
if (url.username || url.password || url.search || url.hash) errors.push('service reference must not contain credentials, query or fragment');
|
|
14
|
+
}
|
|
15
|
+
return errors;
|
|
16
|
+
}
|
|
17
|
+
function requireValid(value, kind) {
|
|
18
|
+
if (validateGuideDocument(value, kind).length) throw new Error(`invalid guide ${kind}`);
|
|
19
|
+
}
|
|
20
|
+
export function createGuideEngagement(offer) {
|
|
21
|
+
requireValid(offer, 'offer');
|
|
22
|
+
return { schema: 'mnstry.atelier-guide-engagement@v1', offerDigest: guideDocumentDigest(offer),
|
|
23
|
+
revision: 0, status: 'proposed', authority: 'local-assertion-only' };
|
|
24
|
+
}
|
|
25
|
+
export function transitionGuideEngagement(state, { expectedRevision, action, offerDigest }) {
|
|
26
|
+
requireValid(state, 'engagement');
|
|
27
|
+
if (expectedRevision !== state.revision || offerDigest !== state.offerDigest) throw new Error('stale or mismatched guide engagement');
|
|
28
|
+
const routes = { proposed: { accept: 'accepted', revoke: 'revoked' }, accepted: { pause: 'paused', revoke: 'revoked' }, paused: { resume: 'accepted', revoke: 'revoked' }, revoked: {} };
|
|
29
|
+
const status = Object.hasOwn(routes[state.status], action) ? routes[state.status][action] : null;
|
|
30
|
+
if (!status) throw new Error('guide transition refused');
|
|
31
|
+
return { ...state, revision: state.revision + 1, status };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// No connection, credentials, invocation, payment, or permission grant occurs.
|
|
35
|
+
// A host must independently authenticate participants and revalidate revocation,
|
|
36
|
+
// consent, policy, schema and exact bytes immediately before any disclosure.
|
|
37
|
+
export function assessGuideInvocation({ offer, capability, engagement, consent, payload, now }) {
|
|
38
|
+
const blockers = [];
|
|
39
|
+
for (const [kind, value] of Object.entries({ offer, capability, engagement, consent })) {
|
|
40
|
+
if (validateGuideDocument(value, kind).length) blockers.push(`invalid ${kind}`);
|
|
41
|
+
}
|
|
42
|
+
if (blockers.length) return { status: 'blocked', blockers, executionAuthority: false };
|
|
43
|
+
let bytes;
|
|
44
|
+
try { bytes = canonicalize(payload); } catch { blockers.push('payload is not canonical JSON'); }
|
|
45
|
+
if (!Number.isFinite(now)) blockers.push('explicit current time required');
|
|
46
|
+
if (engagement.status !== 'accepted') blockers.push('engagement is not accepted');
|
|
47
|
+
if (engagement.offerDigest !== guideDocumentDigest(offer) || consent.offerDigest !== engagement.offerDigest) blockers.push('offer binding mismatch');
|
|
48
|
+
if (consent.engagementRevision !== engagement.revision) blockers.push('consent revision is stale');
|
|
49
|
+
if (!offer.capabilityIds.includes(capability.id) || consent.capabilityDigest !== guideDocumentDigest(capability)) blockers.push('capability binding mismatch');
|
|
50
|
+
if (consent.status !== 'approved') blockers.push('disclosure consent is not approved');
|
|
51
|
+
if (now < Date.parse(consent.validFrom) || now >= Date.parse(consent.expiresAt) || Date.parse(consent.validFrom) >= Date.parse(consent.expiresAt)) blockers.push('consent is not currently valid');
|
|
52
|
+
if (bytes !== undefined && (Buffer.byteLength(bytes) > capability.maxInputBytes || guideDocumentDigest(payload) !== consent.payloadDigest)) blockers.push('payload binding or byte limit mismatch');
|
|
53
|
+
return { status: blockers.length ? 'blocked' : 'eligible-for-host-validation', blockers, executionAuthority: false };
|
|
54
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { withPrivateLock, publishPrivateFile } from '../project/durable-state.mjs';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import { execFileSync } from 'node:child_process';
|
|
6
|
+
import { canonicalize } from '../attestation/jcs.mjs';
|
|
7
|
+
import { validateJsonSchema } from '../export/atelier-export-contract.mjs';
|
|
8
|
+
import { ensureContainedPrivateDirectory, openRegularFileNoFollow } from '../project/private-state.mjs';
|
|
9
|
+
|
|
10
|
+
export const INTAKE_MAX_BYTES = 16 * 1024 * 1024;
|
|
11
|
+
const schema = JSON.parse(fs.readFileSync(new URL('../../contracts/atelier-intake.v1.schema.json', import.meta.url), 'utf8'));
|
|
12
|
+
export function validateIntakeDocument(value) { return validateJsonSchema(schema, value); }
|
|
13
|
+
function valid(value) { if (validateIntakeDocument(value).length) throw new Error('invalid intake document'); return value; }
|
|
14
|
+
export function intakeDigest(bytes) { return createHash('sha256').update(bytes).digest('hex'); }
|
|
15
|
+
const idPattern = /^[a-z][a-z0-9-]{0,63}$/;
|
|
16
|
+
const digestPattern = /^[0-9a-f]{64}$/;
|
|
17
|
+
function digest(value) { if (!digestPattern.test(value ?? '')) throw new Error('invalid content digest'); return value; }
|
|
18
|
+
function id(value) { if (!idPattern.test(value ?? '')) throw new Error('invalid intake identifier'); return value; }
|
|
19
|
+
function read(file) {
|
|
20
|
+
const fd = openRegularFileNoFollow(file);
|
|
21
|
+
try {
|
|
22
|
+
if (fs.fstatSync(fd).size > INTAKE_MAX_BYTES) throw new Error('intake byte ceiling exceeded');
|
|
23
|
+
const bytes = fs.readFileSync(fd);
|
|
24
|
+
if (bytes.length > INTAKE_MAX_BYTES) throw new Error('intake byte ceiling exceeded');
|
|
25
|
+
return bytes;
|
|
26
|
+
} finally { fs.closeSync(fd); }
|
|
27
|
+
}
|
|
28
|
+
function immutable(file, bytes) {
|
|
29
|
+
publishPrivateFile(file, bytes);
|
|
30
|
+
if (!read(file).equals(Buffer.from(bytes))) throw new Error('immutable intake readback mismatch');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Local integrity storage, not a worker, semantic acceptance service, or import
|
|
34
|
+
// authority. Callers retain their source trees and choose their existing extractor.
|
|
35
|
+
export function createIntakeStore({ workspaceRoot = process.cwd() } = {}) {
|
|
36
|
+
const root = fs.realpathSync(workspaceRoot);
|
|
37
|
+
function placement() {
|
|
38
|
+
const env = Object.fromEntries(Object.entries(process.env).filter(([key]) => !key.toUpperCase().startsWith('GIT_')));
|
|
39
|
+
try {
|
|
40
|
+
if (execFileSync('git', ['-C', root, 'ls-files', '-z', '--', '.atelier-local'], { env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] })) throw new Error('tracked state');
|
|
41
|
+
execFileSync('git', ['-C', root, 'check-ignore', '--quiet', '.atelier-local/'], { env, stdio: 'ignore' });
|
|
42
|
+
} catch { throw new Error('intake requires ignored, untracked .atelier-local/ in a Git workspace'); }
|
|
43
|
+
}
|
|
44
|
+
placement();
|
|
45
|
+
function directory(...parts) {
|
|
46
|
+
return ensureContainedPrivateDirectory({ workspaceRoot: root, directory: path.join(root, '.atelier-local', 'intake', ...parts), label: 'intake state' });
|
|
47
|
+
}
|
|
48
|
+
function locked(fn) {
|
|
49
|
+
placement();
|
|
50
|
+
const file = path.join(directory(), 'operation.lock');
|
|
51
|
+
return withPrivateLock(file, fn);
|
|
52
|
+
}
|
|
53
|
+
function sourceFile(ref) {
|
|
54
|
+
if (typeof ref !== 'string' || !ref || path.isAbsolute(ref) || ref.includes('\\') ||
|
|
55
|
+
ref.split('/').some(p => !p || p === '.' || p === '..' || p.startsWith('.'))) throw new Error('source must be a visible workspace-relative file');
|
|
56
|
+
let file = root;
|
|
57
|
+
for (const part of ref.split('/')) {
|
|
58
|
+
file = path.join(file, part);
|
|
59
|
+
if (fs.lstatSync(file).isSymbolicLink()) throw new Error('redirected intake source refused');
|
|
60
|
+
}
|
|
61
|
+
return file;
|
|
62
|
+
}
|
|
63
|
+
function blob(blobId) {
|
|
64
|
+
const bytes = read(path.join(directory('blobs'), digest(blobId)));
|
|
65
|
+
if (intakeDigest(bytes) !== blobId) throw new Error('stored blob digest mismatch');
|
|
66
|
+
return bytes;
|
|
67
|
+
}
|
|
68
|
+
function record(dir, name, value) { valid(value); immutable(path.join(dir, name), canonicalize(value) + '\n'); return value; }
|
|
69
|
+
return Object.freeze({
|
|
70
|
+
ingest({ ref, expectedDigest }) {
|
|
71
|
+
return locked(() => {
|
|
72
|
+
digest(expectedDigest);
|
|
73
|
+
const file = sourceFile(ref), bytes = read(file);
|
|
74
|
+
if (intakeDigest(bytes) !== expectedDigest) throw new Error('source digest mismatch');
|
|
75
|
+
immutable(path.join(directory('blobs'), expectedDigest), bytes);
|
|
76
|
+
if (intakeDigest(read(sourceFile(ref))) !== expectedDigest) throw new Error('source changed during intake');
|
|
77
|
+
const manifest = { schema: 'mnstry.atelier-intake-source@v1', sourceRef: ref, blobId: expectedDigest, bytes: bytes.length, originalMutation: false };
|
|
78
|
+
return record(directory('sources'), intakeDigest(canonicalize(manifest)) + '.json', manifest);
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
beginAttempt({ attemptId, blobId, extractorId, extractorVersion, configurationDigest }) {
|
|
82
|
+
return locked(() => {
|
|
83
|
+
id(attemptId); id(extractorId); digest(configurationDigest); blob(blobId);
|
|
84
|
+
if (typeof extractorVersion !== 'string' || !/^[a-zA-Z0-9._-]{1,64}$/.test(extractorVersion)) throw new Error('invalid extractor version');
|
|
85
|
+
const manifest = { schema: 'mnstry.atelier-intake-attempt@v1', attemptId, blobId, extractorId, extractorVersion, configurationDigest };
|
|
86
|
+
return record(directory('attempts', attemptId), 'attempt.json', manifest);
|
|
87
|
+
});
|
|
88
|
+
},
|
|
89
|
+
completeAttempt({ attemptId, output, expectedOutputDigest }) {
|
|
90
|
+
return locked(() => {
|
|
91
|
+
id(attemptId); digest(expectedOutputDigest);
|
|
92
|
+
if (typeof output !== 'string' || Buffer.byteLength(output) > INTAKE_MAX_BYTES) throw new Error('output must be bounded UTF-8 text');
|
|
93
|
+
if (intakeDigest(output) !== expectedOutputDigest) throw new Error('output digest mismatch');
|
|
94
|
+
const dir = directory('attempts', attemptId);
|
|
95
|
+
const attemptBytes = read(path.join(dir, 'attempt.json'));
|
|
96
|
+
const attempt = valid(JSON.parse(attemptBytes));
|
|
97
|
+
if (attempt.attemptId !== attemptId || attempt.schema !== 'mnstry.atelier-intake-attempt@v1') throw new Error('attempt identity mismatch');
|
|
98
|
+
blob(attempt.blobId);
|
|
99
|
+
immutable(path.join(dir, 'output.txt'), output);
|
|
100
|
+
// Completion is written last. A crash before it is incomplete, not success.
|
|
101
|
+
return record(dir, 'completion.json', { schema: 'mnstry.atelier-intake-completion@v1', attemptId,
|
|
102
|
+
attemptDigest: intakeDigest(attemptBytes), outputDigest: expectedOutputDigest, bytes: Buffer.byteLength(output),
|
|
103
|
+
integrity: 'verified', semanticAcceptance: 'pending' });
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
readCompletion(attemptId) {
|
|
107
|
+
placement(); id(attemptId);
|
|
108
|
+
const dir = directory('attempts', attemptId), receipt = valid(JSON.parse(read(path.join(dir, 'completion.json'))));
|
|
109
|
+
if (receipt.schema !== 'mnstry.atelier-intake-completion@v1' || receipt.attemptId !== attemptId ||
|
|
110
|
+
receipt.integrity !== 'verified' || receipt.semanticAcceptance !== 'pending') throw new Error('invalid completion receipt');
|
|
111
|
+
const attemptBytes = read(path.join(dir, 'attempt.json')), output = read(path.join(dir, 'output.txt'));
|
|
112
|
+
if (intakeDigest(attemptBytes) !== receipt.attemptDigest || intakeDigest(output) !== receipt.outputDigest || output.length !== receipt.bytes) throw new Error('completion integrity mismatch');
|
|
113
|
+
const attempt = valid(JSON.parse(attemptBytes));
|
|
114
|
+
if (attempt.attemptId !== attemptId || attempt.schema !== 'mnstry.atelier-intake-attempt@v1') throw new Error('attempt identity mismatch');
|
|
115
|
+
blob(attempt.blobId);
|
|
116
|
+
return receipt;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
// Browser-only channel. Host/session/service authority never enters this module.
|
|
2
|
+
import { parseRequest, parseBoundedJSON, closed, need, isRef, isDigest, MODES } from '../composition/wire.mjs';
|
|
3
|
+
const HELLO = 'atelier.preview.mount/experimental-v1';
|
|
4
|
+
function origin(value) { const u = new URL(value); need(u.origin !== 'null' && u.protocol === 'https:' && u.origin === value); return value; }
|
|
5
|
+
function resultCheck(bytes, request, generation) {
|
|
6
|
+
const result = parseBoundedJSON(bytes);
|
|
7
|
+
closed(result, ['state'], ['projectedData']);
|
|
8
|
+
const s = result.state;
|
|
9
|
+
closed(s, ['schema', 'requestId', 'placementId', 'sourceDigest', 'mode', 'state', 'sessionGeneration', 'publicMessage']);
|
|
10
|
+
need(s.schema === 'atelier-preview-state/proposal-v1' && s.requestId === request.requestId && s.placementId === request.placementId && s.sourceDigest === request.sourceDigest && s.mode === request.mode);
|
|
11
|
+
need(s.sessionGeneration === generation && ['loading', 'ready', 'unauthenticated', 'forbidden', 'expired', 'unavailable', 'stale'].includes(s.state));
|
|
12
|
+
need(typeof s.publicMessage === 'string' && s.publicMessage.length <= 160);
|
|
13
|
+
need(s.state === 'ready' ? Object.hasOwn(result, 'projectedData') : !Object.hasOwn(result, 'projectedData'));
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
function fail(request, generation) { return { state: { schema: 'atelier-preview-state/proposal-v1', requestId: request.requestId, placementId: request.placementId,
|
|
17
|
+
sourceDigest: request.sourceDigest, mode: request.mode, state: 'unavailable', sessionGeneration: generation, publicMessage: 'Preview unavailable.' } }; }
|
|
18
|
+
|
|
19
|
+
export function mountPreview({ frame, previewOrigin, requestBytes, generation, handle, onState = () => {} }) {
|
|
20
|
+
origin(previewOrigin); need(new URL(frame.src).origin === previewOrigin && previewOrigin !== window.location.origin);
|
|
21
|
+
const template = parseRequest(requestBytes); need(!template.intentRef && Number.isSafeInteger(generation) && generation >= 0 && typeof handle === 'function');
|
|
22
|
+
const nonce = crypto.randomUUID(), channel = new MessageChannel(), controller = new AbortController();
|
|
23
|
+
let active = true, ready = false, sequence = 0, latest = 0;
|
|
24
|
+
const invalidate = () => {
|
|
25
|
+
if (!active) return; active = false; controller.abort();
|
|
26
|
+
channel.port1.postMessage(JSON.stringify({type:'invalidate',nonce})); channel.port1.close(); frame.removeEventListener('load', invalidate);
|
|
27
|
+
onState('stale');
|
|
28
|
+
};
|
|
29
|
+
frame.addEventListener('load', invalidate); // Future navigation invalidates this mount.
|
|
30
|
+
channel.port1.onmessage = async event => {
|
|
31
|
+
if (!active) return;
|
|
32
|
+
try {
|
|
33
|
+
const v = parseBoundedJSON(event.data);
|
|
34
|
+
if (!ready) { closed(v, ['type', 'nonce']); need(v.type === 'ready' && v.nonce === nonce); ready = true; onState('connected'); return; }
|
|
35
|
+
closed(v, ['type', 'nonce', 'sequence', 'request']);
|
|
36
|
+
need(v.type === 'request' && v.nonce === nonce && Number.isSafeInteger(v.sequence) && v.sequence > sequence);
|
|
37
|
+
const request = parseRequest(v.request);
|
|
38
|
+
need(request.placementId === template.placementId && request.sourceDigest === template.sourceDigest && request.mode === template.mode);
|
|
39
|
+
sequence = v.sequence; latest = sequence; const thisSequence = sequence;
|
|
40
|
+
let result;
|
|
41
|
+
try { result = resultCheck(JSON.stringify(await handle(v.request, controller.signal)), request, generation); }
|
|
42
|
+
catch { result = fail(request, generation); }
|
|
43
|
+
if (!active || controller.signal.aborted || thisSequence !== latest) return;
|
|
44
|
+
channel.port1.postMessage(JSON.stringify({ type: 'result', nonce, sequence: thisSequence, result }));
|
|
45
|
+
} catch { invalidate(); }
|
|
46
|
+
};
|
|
47
|
+
channel.port1.start();
|
|
48
|
+
frame.contentWindow.postMessage(JSON.stringify({ type: HELLO, nonce, generation, request: requestBytes }), previewOrigin, [channel.port2]);
|
|
49
|
+
return Object.freeze({ dispose: invalidate });
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function receivePreview({ shellOrigin, onMount, onResult, onInvalidate = () => {} }) {
|
|
53
|
+
origin(shellOrigin); need(shellOrigin !== window.location.origin);
|
|
54
|
+
let active = null;
|
|
55
|
+
function clear() { active?.port.close(); active = null; onInvalidate(); }
|
|
56
|
+
function receive(event) {
|
|
57
|
+
if (event.origin !== shellOrigin || event.source !== window.parent) return;
|
|
58
|
+
try {
|
|
59
|
+
const v = parseBoundedJSON(event.data);
|
|
60
|
+
closed(v, ['type', 'nonce', 'generation', 'request']);
|
|
61
|
+
need(v.type === HELLO && isRef(v.nonce) && Number.isSafeInteger(v.generation) && v.generation >= 0 && event.ports.length === 1);
|
|
62
|
+
const request = parseRequest(v.request); need(!request.intentRef && isDigest(request.sourceDigest) && MODES.includes(request.mode));
|
|
63
|
+
clear();
|
|
64
|
+
const mount = { port: event.ports[0], nonce: v.nonce, generation: v.generation, request, sequence: 0, settled: 0 };
|
|
65
|
+
active = mount;
|
|
66
|
+
mount.port.onmessage = e => {
|
|
67
|
+
if (active !== mount) return;
|
|
68
|
+
try {
|
|
69
|
+
const packet = parseBoundedJSON(e.data);
|
|
70
|
+
if (packet?.type === 'invalidate') { closed(packet, ['type','nonce']); need(packet.nonce === mount.nonce); clear(); return; }
|
|
71
|
+
closed(packet, ['type', 'nonce', 'sequence', 'result']);
|
|
72
|
+
need(packet.type === 'result' && packet.nonce === mount.nonce && packet.sequence === mount.sequence && packet.sequence > mount.settled);
|
|
73
|
+
const result = resultCheck(JSON.stringify(packet.result), mount.pending, mount.generation);
|
|
74
|
+
mount.settled = packet.sequence; onResult(result);
|
|
75
|
+
} catch { clear(); }
|
|
76
|
+
};
|
|
77
|
+
mount.port.start(); mount.port.postMessage(JSON.stringify({ type: 'ready', nonce: mount.nonce }));
|
|
78
|
+
onMount(request);
|
|
79
|
+
} catch { clear(); }
|
|
80
|
+
}
|
|
81
|
+
window.addEventListener('message', receive);
|
|
82
|
+
return Object.freeze({ request(intent) {
|
|
83
|
+
if (!active) return false;
|
|
84
|
+
const sequence = ++active.sequence;
|
|
85
|
+
const request = { ...active.request, requestId: crypto.randomUUID(), ...(intent ? { schema: 'atelier-component-intent/proposal-v1', intentRef: intent.ref, payload: intent.payload } : {}) };
|
|
86
|
+
parseRequest(JSON.stringify(request)); active.pending = request;
|
|
87
|
+
active.port.postMessage(JSON.stringify({ type: 'request', nonce: active.nonce, sequence, request: JSON.stringify(request) })); return true;
|
|
88
|
+
}, dispose() { clear(); window.removeEventListener('message', receive); } });
|
|
89
|
+
}
|
package/src/project/config.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import fs from 'node:fs'
|
|
|
2
2
|
import path from 'node:path'
|
|
3
3
|
import { spawnSync } from 'node:child_process'
|
|
4
4
|
import { sanitizedGitEnvironment } from '../runtime/git-adapter.mjs'
|
|
5
|
+
import { parseProjectOptions } from '../cli/project-options.mjs'
|
|
5
6
|
|
|
6
7
|
export const PROJECT_CONFIG_ARG_PREFIX = '--project-config='
|
|
7
8
|
export const PROJECT_CONFIG_ENV = 'MNSTRY_ATELIER_PROJECT_CONFIG'
|
|
@@ -95,14 +96,7 @@ export function writeJson(file, value) {
|
|
|
95
96
|
}
|
|
96
97
|
|
|
97
98
|
export function projectConfigArg(argv = process.argv.slice(2), prefix = PROJECT_CONFIG_ARG_PREFIX) {
|
|
98
|
-
|
|
99
|
-
const arg = argv[index]
|
|
100
|
-
if (typeof arg !== 'string') continue
|
|
101
|
-
if (arg.startsWith(prefix)) return arg.slice(prefix.length).trim() || null
|
|
102
|
-
if ((arg === '--project-config' || arg === '--project') && argv[index + 1]) return String(argv[index + 1]).trim() || null
|
|
103
|
-
if (arg.startsWith('--project=')) return arg.slice('--project='.length).trim() || null
|
|
104
|
-
}
|
|
105
|
-
return null
|
|
99
|
+
return parseProjectOptions(argv, prefix).project
|
|
106
100
|
}
|
|
107
101
|
|
|
108
102
|
export function stripProjectConfigArgs(argv = [], prefix = PROJECT_CONFIG_ARG_PREFIX) {
|
|
@@ -171,22 +165,7 @@ export function readLocalOverlay({ configDir, env = process.env, cwd = process.c
|
|
|
171
165
|
}
|
|
172
166
|
|
|
173
167
|
export function parseRepoPathOverrides(argv = process.argv.slice(2), cwd = process.cwd()) {
|
|
174
|
-
|
|
175
|
-
for (let index = 0; index < argv.length; index += 1) {
|
|
176
|
-
const arg = argv[index]
|
|
177
|
-
let value = null
|
|
178
|
-
if (arg === '--repo-path' && argv[index + 1]) {
|
|
179
|
-
value = argv[index + 1]
|
|
180
|
-
index += 1
|
|
181
|
-
} else if (typeof arg === 'string' && arg.startsWith('--repo-path=')) {
|
|
182
|
-
value = arg.slice('--repo-path='.length)
|
|
183
|
-
}
|
|
184
|
-
if (!value || !value.includes('=')) continue
|
|
185
|
-
const [name, ...rest] = value.split('=')
|
|
186
|
-
const repoPath = rest.join('=')
|
|
187
|
-
if (firstString(name) && firstString(repoPath)) out.set(name.trim(), resolvePathValue(repoPath, cwd))
|
|
188
|
-
}
|
|
189
|
-
return out
|
|
168
|
+
return new Map([...parseProjectOptions(argv).repoPaths].map(([name, value]) => [name, resolvePathValue(value, cwd)]))
|
|
190
169
|
}
|
|
191
170
|
|
|
192
171
|
function overlayRepoPath(overlay, repoName) {
|
|
@@ -271,6 +250,7 @@ export function resolveProjectConfig({
|
|
|
271
250
|
configArgPrefix = PROJECT_CONFIG_ARG_PREFIX,
|
|
272
251
|
gitExecutable = 'git',
|
|
273
252
|
configEnv = PROJECT_CONFIG_ENV,
|
|
253
|
+
writeLocalState = true,
|
|
274
254
|
defaults = {},
|
|
275
255
|
} = {}) {
|
|
276
256
|
const argPath = projectConfigArg(argv, configArgPrefix)
|
|
@@ -387,12 +367,12 @@ export function resolveProjectConfig({
|
|
|
387
367
|
}
|
|
388
368
|
}),
|
|
389
369
|
}
|
|
390
|
-
resolved.localState = ensureLocalState(resolved, { write:
|
|
370
|
+
resolved.localState = ensureLocalState(resolved, { write: writeLocalState, gitExecutable, env })
|
|
391
371
|
return resolved
|
|
392
372
|
}
|
|
393
373
|
|
|
394
|
-
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd(), gitExecutable = 'git' } = {}) {
|
|
395
|
-
const project = resolveProjectConfig({ argv, env, cwd, gitExecutable })
|
|
374
|
+
export function commandProject({ argv = process.argv.slice(2), env = process.env, cwd = process.cwd(), gitExecutable = 'git', writeLocalState = true } = {}) {
|
|
375
|
+
const project = resolveProjectConfig({ argv, env, cwd, gitExecutable, writeLocalState })
|
|
396
376
|
// Fail closed at CLI entry, but only when a real config file was loaded; the
|
|
397
377
|
// defaults/no-file path resolves with an empty config that would spuriously
|
|
398
378
|
// fail document validation.
|
|
@@ -504,6 +484,19 @@ function extErrors(value, label) {
|
|
|
504
484
|
.map(([key]) => `${label}.${key} must be an object; ext members are namespaced objects`)
|
|
505
485
|
}
|
|
506
486
|
|
|
487
|
+
// Hands one namespaced ext member to its structured consumer, uninterpreted.
|
|
488
|
+
// The project validator above deliberately does not look inside a member, so
|
|
489
|
+
// the consumer validates it against its own closed contract and fails closed.
|
|
490
|
+
// `present` separates "no such member" (the consumer's feature is simply not
|
|
491
|
+
// configured) from a member that exists with any value at all, including a
|
|
492
|
+
// malformed one, which the consumer must refuse rather than ignore.
|
|
493
|
+
export function projectExtMember(project, key) {
|
|
494
|
+
const ext = project?.config?.ext
|
|
495
|
+
if (ext == null) return { present: false, value: undefined }
|
|
496
|
+
if (typeof ext !== 'object' || Array.isArray(ext)) return { present: true, value: undefined }
|
|
497
|
+
return Object.hasOwn(ext, key) ? { present: true, value: ext[key] } : { present: false, value: undefined }
|
|
498
|
+
}
|
|
499
|
+
|
|
507
500
|
export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {}) {
|
|
508
501
|
const errors = []
|
|
509
502
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return ['project config must be a JSON object']
|
|
@@ -565,6 +558,7 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
|
|
|
565
558
|
}
|
|
566
559
|
if (alignment.appRepo != null && !firstString(alignment.appRepo)) errors.push('alignment.appRepo must be a non-empty string')
|
|
567
560
|
|
|
561
|
+
const repoNames = new Set()
|
|
568
562
|
for (const [index, repo] of (Array.isArray(doc.repos) ? doc.repos : []).entries()) {
|
|
569
563
|
if (!repo || typeof repo !== 'object' || Array.isArray(repo)) {
|
|
570
564
|
errors.push(`repos[${index}] must be an object`)
|
|
@@ -573,6 +567,11 @@ export function validateProjectConfigDoc(doc, { neutralTemplate = false } = {})
|
|
|
573
567
|
errors.push(...unknownKeyErrors(repo, `repos[${index}]`, new Set(['name', 'path', 'readBoundary', 'role', 'kind', 'remote', 'identity', 'aliases', 'required', 'ext'])))
|
|
574
568
|
errors.push(...extErrors(repo.ext, `repos[${index}].ext`))
|
|
575
569
|
if (!firstString(repo.name)) errors.push(`repos[${index}].name is required`)
|
|
570
|
+
else {
|
|
571
|
+
const key = repo.name.toLowerCase()
|
|
572
|
+
if (repoNames.has(key)) errors.push(`repos[${index}].name duplicates another repository name`)
|
|
573
|
+
repoNames.add(key)
|
|
574
|
+
}
|
|
576
575
|
if (repo.identity != null) {
|
|
577
576
|
const identity = asObject(repo.identity)
|
|
578
577
|
errors.push(...unknownKeyErrors(identity, `repos[${index}].identity`, new Set(['provider', 'id', 'ext'])))
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
4
|
+
import { hostname } from 'node:os';
|
|
5
|
+
import { ensureContainedPrivateDirectory, openRegularFileNoFollow, readRegularTextNoFollow, syncPrivateDirectory } from './private-state.mjs';
|
|
6
|
+
|
|
7
|
+
const host = createHash('sha256').update(hostname()).digest('hex');
|
|
8
|
+
const pendingPattern = /^\.atelier-write-[a-f0-9-]{36}\.tmp$/;
|
|
9
|
+
export const isPendingPrivateWrite = name => pendingPattern.test(name);
|
|
10
|
+
|
|
11
|
+
export { syncPrivateDirectory };
|
|
12
|
+
|
|
13
|
+
// Publish a complete immutable file with an atomic, non-overwriting hard link.
|
|
14
|
+
// A process interruption can leave a private staging file, never a partial final.
|
|
15
|
+
export function publishPrivateFile(file, bytes) {
|
|
16
|
+
const directory = path.dirname(file);
|
|
17
|
+
const pending = path.join(directory, `.atelier-write-${randomUUID()}.tmp`);
|
|
18
|
+
let fd;
|
|
19
|
+
try {
|
|
20
|
+
fd = openRegularFileNoFollow(pending, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600);
|
|
21
|
+
fs.writeFileSync(fd, bytes); fs.fsyncSync(fd); fs.closeSync(fd); fd = undefined;
|
|
22
|
+
try { fs.linkSync(pending, file); }
|
|
23
|
+
catch (error) {
|
|
24
|
+
if (error.code !== 'EEXIST') throw error;
|
|
25
|
+
const existing = openRegularFileNoFollow(file);
|
|
26
|
+
try { if (!fs.readFileSync(existing).equals(Buffer.from(bytes))) throw error; }
|
|
27
|
+
finally { fs.closeSync(existing); }
|
|
28
|
+
}
|
|
29
|
+
syncPrivateDirectory(directory);
|
|
30
|
+
const actual = openRegularFileNoFollow(file);
|
|
31
|
+
try { if (!fs.readFileSync(actual).equals(Buffer.from(bytes))) throw new Error('published file readback mismatch'); }
|
|
32
|
+
finally { fs.closeSync(actual); }
|
|
33
|
+
} finally {
|
|
34
|
+
if (fd !== undefined) fs.closeSync(fd);
|
|
35
|
+
try { fs.unlinkSync(pending); } catch (error) { if (error.code !== 'ENOENT') throw error; }
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function unavailable(diagnostic = { reason: 'ownership changed' }) {
|
|
40
|
+
return Object.assign(new Error(`EEXIST: private state is locked (${diagnostic.reason}${diagnostic.file ? `: ${diagnostic.file}` : ''}); inspectPrivateLock and preserve ownership records before offline recovery`), { code: 'EEXIST', diagnostic });
|
|
41
|
+
}
|
|
42
|
+
function ownerState(record, legacy = false) {
|
|
43
|
+
const pid = typeof record === 'number' ? record : record?.pid;
|
|
44
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return 'invalid owner';
|
|
45
|
+
if (!legacy && record?.host !== host) return 'host identity differs';
|
|
46
|
+
try { process.kill(pid, 0); return 'process exists'; }
|
|
47
|
+
catch (error) { return error.code === 'ESRCH' ? 'absent' : 'process identity unavailable'; }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Read-only diagnosis. Older tickets were superseded by a successful successor;
|
|
51
|
+
// only the newest ticket owns this lock. A release marker prevents a completed
|
|
52
|
+
// successor from disappearing and resurrecting an obsolete owner on PID reuse.
|
|
53
|
+
export function inspectPrivateLock(lockPath) {
|
|
54
|
+
const directory = `${lockPath}.owners`;
|
|
55
|
+
let names;
|
|
56
|
+
try {
|
|
57
|
+
const stat = fs.lstatSync(directory);
|
|
58
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return { available: false, reason: 'unsafe ownership directory' };
|
|
59
|
+
names = fs.readdirSync(directory).filter(name => !isPendingPrivateWrite(name)).sort();
|
|
60
|
+
} catch (error) { if (error.code !== 'ENOENT') throw error; names = []; }
|
|
61
|
+
const unexpected = names.find(name => !/^\d{12}\.(json|released)$/.test(name));
|
|
62
|
+
if (unexpected) return { available: false, reason: 'unknown ownership file', file: unexpected };
|
|
63
|
+
const nameSet = new Set(names);
|
|
64
|
+
const tickets = names.filter(name => name.endsWith('.json'));
|
|
65
|
+
const orphan = names.find(name => name.endsWith('.released') && !nameSet.has(name.replace(/\.released$/, '.json')));
|
|
66
|
+
if (orphan) return { available: false, reason: 'release without owner', file: orphan };
|
|
67
|
+
let legacy, legacyDigest = null;
|
|
68
|
+
try {
|
|
69
|
+
const bytes = readRegularTextNoFollow(lockPath);
|
|
70
|
+
legacy = JSON.parse(bytes); legacyDigest = createHash('sha256').update(bytes).digest('hex');
|
|
71
|
+
} catch (error) {
|
|
72
|
+
if (error.code !== 'ENOENT') return { available: false, reason: 'unreadable legacy owner', file: path.basename(lockPath) };
|
|
73
|
+
}
|
|
74
|
+
if (tickets.length) {
|
|
75
|
+
const file = tickets.at(-1), generation = Number(file.slice(0, 12));
|
|
76
|
+
let body, record;
|
|
77
|
+
try { body = readRegularTextNoFollow(path.join(directory, file)); record = JSON.parse(body); }
|
|
78
|
+
catch { return { available: false, reason: 'unreadable owner', file }; }
|
|
79
|
+
if (!Number.isSafeInteger(record?.pid) || record.pid <= 0 || !/^[a-f0-9]{64}$/.test(record?.host) || !/^[a-f0-9-]{36}$/.test(record?.nonce)) return { available: false, reason: 'invalid owner', file };
|
|
80
|
+
if (legacyDigest !== null && record.legacyDigest !== legacyDigest) {
|
|
81
|
+
const reason = ownerState(legacy, true);
|
|
82
|
+
if (reason !== 'absent') return { available: false, reason: `legacy ${reason}`, file: path.basename(lockPath) };
|
|
83
|
+
}
|
|
84
|
+
const marker = file.replace(/\.json$/, '.released');
|
|
85
|
+
if (names.includes(marker)) {
|
|
86
|
+
try {
|
|
87
|
+
if (readRegularTextNoFollow(path.join(directory, marker)) !== body) return { available: false, reason: 'release identity differs', file: marker };
|
|
88
|
+
} catch { return { available: false, reason: 'unreadable release', file: marker }; }
|
|
89
|
+
return { available: true, reason: 'released', generation, file, legacyDigest };
|
|
90
|
+
}
|
|
91
|
+
const reason = ownerState(record);
|
|
92
|
+
return { available: reason === 'absent', reason, generation, file, legacyDigest };
|
|
93
|
+
}
|
|
94
|
+
if (legacyDigest === null) return { available: true, reason: 'unused', generation: 0, legacyDigest };
|
|
95
|
+
const reason = ownerState(legacy, true);
|
|
96
|
+
return { available: reason === 'absent', reason, generation: 0, file: path.basename(lockPath), legacyDigest };
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function acquirePrivateLock(lockPath) {
|
|
100
|
+
const parent = path.dirname(lockPath);
|
|
101
|
+
const directory = ensureContainedPrivateDirectory({ workspaceRoot: parent, directory: `${lockPath}.owners`, label: 'private lock' });
|
|
102
|
+
let ticket, body;
|
|
103
|
+
try {
|
|
104
|
+
const status = inspectPrivateLock(lockPath);
|
|
105
|
+
if (!status.available) throw unavailable(status);
|
|
106
|
+
const generation = status.generation;
|
|
107
|
+
if (generation >= 999999999999) throw unavailable({ reason: 'ownership generation exhausted' });
|
|
108
|
+
ticket = path.join(directory, `${String(generation + 1).padStart(12, '0')}.json`);
|
|
109
|
+
body = JSON.stringify({ pid: process.pid, host, nonce: randomUUID(), legacyDigest: status.legacyDigest });
|
|
110
|
+
publishPrivateFile(ticket, body);
|
|
111
|
+
let released = false;
|
|
112
|
+
return () => {
|
|
113
|
+
if (released) return;
|
|
114
|
+
if (readRegularTextNoFollow(ticket) !== body) throw unavailable();
|
|
115
|
+
publishPrivateFile(ticket.replace(/\.json$/, '.released'), body);
|
|
116
|
+
released = true;
|
|
117
|
+
};
|
|
118
|
+
} catch (error) {
|
|
119
|
+
// No operation has started: clean up only this exact attempted owner.
|
|
120
|
+
if (ticket && body) {
|
|
121
|
+
try { if (readRegularTextNoFollow(ticket) === body) fs.unlinkSync(ticket); }
|
|
122
|
+
catch { /* Preserve uncertain ownership for offline diagnosis. */ }
|
|
123
|
+
}
|
|
124
|
+
if (error.code === 'EEXIST' && !error.diagnostic) throw unavailable(); throw error;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function withPrivateLock(lockPath, operation) {
|
|
129
|
+
const release = acquirePrivateLock(lockPath);
|
|
130
|
+
try { return operation(); } finally { release(); }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Revalidate file identities on every read; unchanged prefixes reuse their
|
|
134
|
+
// verified result. A changed existing file forces replay, a missing tail is
|
|
135
|
+
// refused for the lifetime of this reader. Startup always verifies all history.
|
|
136
|
+
export function createVerifiedFileSequence({ directory, initial, apply, ignoreFiles = [] }) {
|
|
137
|
+
let names = [], identities = [], value = initial();
|
|
138
|
+
const identity = file => {
|
|
139
|
+
const s = fs.lstatSync(file, { bigint: true });
|
|
140
|
+
if (!s.isFile() || s.isSymbolicLink()) throw new Error('non-regular history refused');
|
|
141
|
+
return [s.dev, s.ino, s.size, s.mtimeNs, s.ctimeNs].join(':');
|
|
142
|
+
};
|
|
143
|
+
return () => {
|
|
144
|
+
const listed = fs.readdirSync(directory).filter(n => !isPendingPrivateWrite(n)).sort();
|
|
145
|
+
// Explicit adapter metadata exceptions must still be regular files. Never
|
|
146
|
+
// hide unknown filenames or malformed committed records behind a glob.
|
|
147
|
+
for (const name of listed.filter(n => ignoreFiles.includes(n))) identity(path.join(directory, name));
|
|
148
|
+
const next = listed.filter(n => !ignoreFiles.includes(n));
|
|
149
|
+
if (next.length < names.length) throw new Error('history shortened; preserve and inspect');
|
|
150
|
+
const ids = next.map(n => identity(path.join(directory, n)));
|
|
151
|
+
const reuse = names.every((n, i) => n === next[i] && identities[i] === ids[i]);
|
|
152
|
+
let result = reuse ? value : initial();
|
|
153
|
+
for (let i = reuse ? names.length : 0; i < next.length; i++) {
|
|
154
|
+
const file = path.join(directory, next[i]);
|
|
155
|
+
result = apply(readRegularTextNoFollow(file), result, i + 1, next[i]);
|
|
156
|
+
if (identity(file) !== ids[i]) throw new Error('history changed during verification');
|
|
157
|
+
}
|
|
158
|
+
names = next; identities = ids; value = result;
|
|
159
|
+
return structuredClone(result);
|
|
160
|
+
};
|
|
161
|
+
}
|