@mnstry/atelier 0.2.0-alpha.5 → 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 +191 -0
- package/README.md +86 -25
- 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-repository-observation.v1.schema.json +163 -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 +12 -2
- package/docs/atelier-runtime.md +13 -2
- package/docs/atelier-sync.md +172 -0
- package/docs/blocks/claims.md +28 -18
- package/docs/blocks/will-not-do.md +12 -3
- 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 +99 -2
- package/docs/review-portability.md +55 -0
- package/docs/skill-steward.md +156 -0
- package/docs/upgrade.md +70 -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-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
- package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -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 +61 -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/content-rules.mjs +9 -4
- package/src/boundary/policy.mjs +74 -42
- package/src/cli/project-options.mjs +58 -0
- package/src/cli/run.mjs +61 -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 +107 -0
- package/src/composition/registry.mjs +27 -0
- package/src/composition/wire.mjs +122 -0
- package/src/contracts/corpus.mjs +35 -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/index.mjs +29 -0
- package/src/intake/store.mjs +119 -0
- package/src/preview/channel.mjs +89 -0
- package/src/project/config.mjs +50 -49
- 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/git-adapter.mjs +189 -0
- package/src/runtime/local-state.mjs +439 -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/repository-observation.mjs +491 -0
- package/src/runtime/supervisor.mjs +803 -0
- 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,266 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { EXCHANGE_CONSTANTS } from './exchange.mjs'
|
|
3
|
+
|
|
4
|
+
// The fixed script that performs one conditional publication. It runs in two
|
|
5
|
+
// places with the same body: inside the Obsidian renderer, sent through the
|
|
6
|
+
// official CLI's `eval`, and in this process when no Obsidian is running.
|
|
7
|
+
//
|
|
8
|
+
// The body is a constant. The only variable input is a closed, validated JSON
|
|
9
|
+
// payload that travels base64-encoded, so note text and paths never become
|
|
10
|
+
// code. Candidate and recovery bytes travel as files bound by SHA-256.
|
|
11
|
+
|
|
12
|
+
export const PROTOCOL_ID = 'obsidian-cli-critical-section/v1'
|
|
13
|
+
export const POLICY_SETTINGS_PATH = '.obsidian/core-plugins.json'
|
|
14
|
+
|
|
15
|
+
const OPS = new Set(['inspect', 'publish', 'collect'])
|
|
16
|
+
const MODES = new Set(['replace', 'remove'])
|
|
17
|
+
const SHA = /^[0-9a-f]{64}$/
|
|
18
|
+
const OPERATION_ID = /^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/
|
|
19
|
+
const KEYS = {
|
|
20
|
+
inspect: ['op', 'vaultRoot', 'path'],
|
|
21
|
+
collect: ['op', 'vaultRoot', 'path'],
|
|
22
|
+
replace: ['op', 'mode', 'vaultRoot', 'path', 'operationId', 'baseSha256', 'candidateSha256', 'stagedPath', 'recoveryPath'],
|
|
23
|
+
remove: ['op', 'mode', 'vaultRoot', 'path', 'operationId', 'baseSha256', 'recoveryPath'],
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const isAbsolute = (value) => typeof value === 'string' && value.startsWith('/') && !value.includes('\u0000')
|
|
27
|
+
|
|
28
|
+
// A vault-relative path the publisher may address: visible files only, plus
|
|
29
|
+
// the one policy-owned settings file. Every other settings path is the
|
|
30
|
+
// person's and cannot be named in a payload at all.
|
|
31
|
+
export function isAddressableVaultPath(value) {
|
|
32
|
+
if (typeof value !== 'string' || value.length === 0 || value.length > 1024 || value.startsWith('/') || value.includes('\\') || value.includes('\u0000')) return false
|
|
33
|
+
if (value === POLICY_SETTINGS_PATH) return true
|
|
34
|
+
return value.split('/').every((part) => part !== '' && part !== '.' && part !== '..' && !part.startsWith('.'))
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function validatePayload(payload) {
|
|
38
|
+
const fail = (message) => { throw new TypeError(`Invalid bridge payload: ${message}`) }
|
|
39
|
+
if (!payload || typeof payload !== 'object' || Array.isArray(payload)) fail('object required')
|
|
40
|
+
if (!OPS.has(payload.op)) fail('unknown op')
|
|
41
|
+
if (payload.op === 'publish' && !MODES.has(payload.mode)) fail('unknown mode')
|
|
42
|
+
const allowed = KEYS[payload.op === 'publish' ? payload.mode : payload.op]
|
|
43
|
+
for (const key of Object.keys(payload)) if (!allowed.includes(key)) fail(`unknown key ${key}`)
|
|
44
|
+
for (const key of allowed) if (payload[key] === undefined) fail(`missing key ${key}`)
|
|
45
|
+
if (!isAbsolute(payload.vaultRoot)) fail('vaultRoot must be absolute')
|
|
46
|
+
if (!isAddressableVaultPath(payload.path)) fail('path must be a relative visible path or the policy settings file')
|
|
47
|
+
if (payload.op === 'publish') {
|
|
48
|
+
if (!OPERATION_ID.test(payload.operationId)) fail('operationId required')
|
|
49
|
+
if (!SHA.test(payload.baseSha256)) fail('baseSha256 required')
|
|
50
|
+
if (!isAbsolute(payload.recoveryPath)) fail('recoveryPath must be absolute')
|
|
51
|
+
if (payload.mode === 'replace') {
|
|
52
|
+
if (!SHA.test(payload.candidateSha256)) fail('candidateSha256 required')
|
|
53
|
+
if (!isAbsolute(payload.stagedPath)) fail('stagedPath must be absolute')
|
|
54
|
+
if (payload.baseSha256 === payload.candidateSha256) fail('candidate equals base; nothing to publish')
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return payload
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Everything between "critical section begins" and "critical section ends" is
|
|
61
|
+
// synchronous. Inside the app that means no editor input event, in any window
|
|
62
|
+
// of the vault, can interleave with it. The function is serialized with
|
|
63
|
+
// toString(), so it may not close over anything in this module: all it uses
|
|
64
|
+
// arrives through `host`.
|
|
65
|
+
//
|
|
66
|
+
// host: { app, window, document, require, process, performance, exchange }
|
|
67
|
+
// `host.crashSeam` is a test seam for crash injection. It is a function-valued
|
|
68
|
+
// host property: no payload can carry it and buildEvalCode never sets it.
|
|
69
|
+
export function criticalSection(P, host) {
|
|
70
|
+
const fs = host.require('fs')
|
|
71
|
+
const nodePath = host.require('path')
|
|
72
|
+
const crypto = host.require('crypto')
|
|
73
|
+
const app = host.app || null
|
|
74
|
+
const sha = (value) => crypto.createHash('sha256').update(value).digest('hex')
|
|
75
|
+
const t0 = host.performance.now()
|
|
76
|
+
const trace = []
|
|
77
|
+
const step = (event, detail) => trace.push({ ms: Number((host.performance.now() - t0).toFixed(3)), event, ...(detail || {}) })
|
|
78
|
+
const crash = (point) => { if (host.crashSeam && host.crashSeam.at === point) host.crashSeam.halt(point) }
|
|
79
|
+
const real = (target) => { try { return fs.realpathSync(target) } catch (error) { return null } }
|
|
80
|
+
const vaultBasePath = app ? real(app.vault.adapter.getBasePath()) : real(P.vaultRoot)
|
|
81
|
+
const full = nodePath.join(P.vaultRoot, P.path)
|
|
82
|
+
const views = () => {
|
|
83
|
+
const found = []
|
|
84
|
+
if (!app) return found
|
|
85
|
+
app.workspace.iterateAllLeaves((leaf) => {
|
|
86
|
+
const view = leaf.view
|
|
87
|
+
if (view && view.file && view.file.path === P.path && view.editor && typeof view.getViewData === 'function') found.push(view)
|
|
88
|
+
})
|
|
89
|
+
return found
|
|
90
|
+
}
|
|
91
|
+
const describe = (view) => ({ dirty: Boolean(view.dirty), bufferSha256: sha(view.getViewData()), popout: view.containerEl.ownerDocument !== host.document,
|
|
92
|
+
savedMarker: typeof view.lastSavedData === 'string' })
|
|
93
|
+
const lstat = (target) => { try { return fs.lstatSync(target) } catch (error) { if (error.code === 'ENOENT') return null; throw error } }
|
|
94
|
+
// No write-through links: every directory on the way is a real directory and the leaf, when present, is a regular file.
|
|
95
|
+
const unsafe = () => {
|
|
96
|
+
let current = P.vaultRoot
|
|
97
|
+
const parts = P.path.split('/')
|
|
98
|
+
for (let index = 0; index < parts.length; index += 1) {
|
|
99
|
+
current = nodePath.join(current, parts[index])
|
|
100
|
+
const stat = lstat(current)
|
|
101
|
+
if (!stat) return false
|
|
102
|
+
if (stat.isSymbolicLink() || (index < parts.length - 1 ? !stat.isDirectory() : !stat.isFile())) return true
|
|
103
|
+
}
|
|
104
|
+
return false
|
|
105
|
+
}
|
|
106
|
+
const disk = () => (lstat(full) ? sha(fs.readFileSync(full)) : null)
|
|
107
|
+
const snapshot = () => ({ vaultBasePath, diskSha256: disk(), views: views().map(describe) })
|
|
108
|
+
const store = (host.window.__atelierObsidianPublication = host.window.__atelierObsidianPublication || {})
|
|
109
|
+
// Every outcome is kept here so a caller whose reply was lost re-reads it and never resends.
|
|
110
|
+
const done = (status, extra) => {
|
|
111
|
+
if (P.op === 'publish') store[P.path] = { ...(extra || {}), operationId: P.operationId, outcome: status }
|
|
112
|
+
return JSON.stringify({ status, operationId: P.operationId, ...(extra || {}), trace })
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (vaultBasePath === null || vaultBasePath !== P.vaultRoot) return done('vault-mismatch', { vaultBasePath, wrote: false })
|
|
116
|
+
// The app answered for this vault; the path itself is the problem.
|
|
117
|
+
if (unsafe()) return done('path-unsafe', { vaultBasePath, wrote: false })
|
|
118
|
+
if (P.op === 'inspect') return done('inspected', snapshot())
|
|
119
|
+
if (P.op === 'collect') return JSON.stringify({ status: 'collected', ...(store[P.path] || { missing: true }), ...snapshot() })
|
|
120
|
+
|
|
121
|
+
// ---- critical section begins (synchronous) ----
|
|
122
|
+
step('enter')
|
|
123
|
+
if (!lstat(full)) return done('note-missing', { ...snapshot(), wrote: false })
|
|
124
|
+
const open = views()
|
|
125
|
+
// Capability floor: the no-write editor update relies on the view's saved-content field. Refuse open notes on an app build that lacks it.
|
|
126
|
+
if (P.mode === 'replace' && open.some((view) => typeof view.lastSavedData !== 'string')) return done('unsupported-app', { wrote: false })
|
|
127
|
+
if (open.some((view) => view.dirty || sha(view.getViewData()) !== P.baseSha256)) return done('editor-edit', { ...snapshot(), wrote: false })
|
|
128
|
+
if (disk() !== P.baseSha256) return done('disk-changed', { ...snapshot(), wrote: false })
|
|
129
|
+
if (lstat(P.recoveryPath)) return done('recovery-path-occupied', { wrote: false })
|
|
130
|
+
|
|
131
|
+
if (P.mode === 'remove') {
|
|
132
|
+
// A move, never a delete: whatever occupies the path at this instant is kept at the recovery path.
|
|
133
|
+
try { fs.renameSync(full, P.recoveryPath) } catch (error) { return done('move-failed', { ...snapshot(), wrote: false, errorCode: error.code || null }) }
|
|
134
|
+
step('moved')
|
|
135
|
+
crash('after-removal-move')
|
|
136
|
+
const movedSha256 = sha(fs.readFileSync(P.recoveryPath))
|
|
137
|
+
if (movedSha256 !== P.baseSha256) {
|
|
138
|
+
// Something replaced the note between the check and the move. Put it back without overwriting anything.
|
|
139
|
+
try { fs.linkSync(P.recoveryPath, full) } catch (error) {
|
|
140
|
+
// The path is taken again, or cannot be linked: the moved bytes stay in recovery.
|
|
141
|
+
return done('removed-external-captured', { ...snapshot(), wrote: true, externalCaptured: true, recoveredSha256: movedSha256 })
|
|
142
|
+
}
|
|
143
|
+
// The note is live again from here on, whatever happens to the second name.
|
|
144
|
+
let recoveryNameLeft = false
|
|
145
|
+
try { fs.unlinkSync(P.recoveryPath) } catch (error) { recoveryNameLeft = true }
|
|
146
|
+
return done('remove-reverted', { ...snapshot(), wrote: false, observedSha256: movedSha256, recoveryNameLeft })
|
|
147
|
+
}
|
|
148
|
+
return done('removed', { ...snapshot(), wrote: true, externalCaptured: false, recoveredSha256: movedSha256 })
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let candidate
|
|
152
|
+
try { candidate = fs.readFileSync(P.stagedPath) } catch (error) { return done('staged-missing', { wrote: false, errorCode: error.code || null }) }
|
|
153
|
+
if (sha(candidate) !== P.candidateSha256) return done('staged-mismatch', { wrote: false })
|
|
154
|
+
const call = host.exchange.calls[`${host.process.platform}/${host.process.arch}`]
|
|
155
|
+
const perl = host.exchange.perlCandidates.find((candidatePath) => fs.existsSync(candidatePath))
|
|
156
|
+
if (!call || !perl) return done('exchange-unavailable', { wrote: false })
|
|
157
|
+
// The interpreter runs inside the app: only a root-owned binary that neither group nor others can write.
|
|
158
|
+
let perlStat = null
|
|
159
|
+
try { perlStat = fs.statSync(perl) } catch (error) { perlStat = null }
|
|
160
|
+
if (!perlStat || perlStat.uid !== 0 || (perlStat.mode & 0o022) !== 0) return done('exchange-interpreter-untrusted', { wrote: false })
|
|
161
|
+
// Atomic exchange: whatever occupied the note path at this instant lands at
|
|
162
|
+
// the staged path, so no concurrent replacement of the note can be lost.
|
|
163
|
+
let exitStatus = 0
|
|
164
|
+
try {
|
|
165
|
+
host.require('child_process').execFileSync(perl, ['-e', host.exchange.script, '--', String(call.number), String(call.cwd), P.stagedPath, full], { stdio: 'ignore' })
|
|
166
|
+
} catch (error) {
|
|
167
|
+
exitStatus = error.status == null ? null : error.status
|
|
168
|
+
}
|
|
169
|
+
// What happened is read from the files, not from the exit status: a helper
|
|
170
|
+
// killed after the call returned reports failure although the exchange took
|
|
171
|
+
// place. The staged path is private, so bytes there that are not the
|
|
172
|
+
// candidate can only be what the exchange displaced from the note path.
|
|
173
|
+
let stagedNow = null
|
|
174
|
+
try { stagedNow = sha(fs.readFileSync(P.stagedPath)) } catch (error) { stagedNow = null }
|
|
175
|
+
if (stagedNow === null || stagedNow === P.candidateSha256) {
|
|
176
|
+
// All-or-nothing: a failure (for example a full disk) leaves the note as it was.
|
|
177
|
+
const seen = snapshot()
|
|
178
|
+
return done('exchange-failed', { ...seen, wrote: stagedNow === null && seen.diskSha256 === P.candidateSha256, exitStatus })
|
|
179
|
+
}
|
|
180
|
+
step('exchanged', exitStatus === 0 ? undefined : { exitStatus })
|
|
181
|
+
crash('after-exchange')
|
|
182
|
+
fs.renameSync(P.stagedPath, P.recoveryPath)
|
|
183
|
+
const recoveredSha256 = sha(fs.readFileSync(P.recoveryPath))
|
|
184
|
+
const externalCaptured = recoveredSha256 !== P.baseSha256
|
|
185
|
+
step('recovered', { externalCaptured })
|
|
186
|
+
crash('after-recovery-move')
|
|
187
|
+
|
|
188
|
+
const candidateText = candidate.toString('utf8')
|
|
189
|
+
let editorUpdateFailed = false
|
|
190
|
+
open.forEach((view) => {
|
|
191
|
+
// Route the change through the editor so the app never takes its lossy
|
|
192
|
+
// external-modification merge path for this view, and never writes the
|
|
193
|
+
// file itself afterwards: an in-place save here would overwrite an outside
|
|
194
|
+
// writer that replaced the note after the exchange. Minimal line hunks in
|
|
195
|
+
// one transaction keep carets and selections in untouched text.
|
|
196
|
+
try {
|
|
197
|
+
const a = view.getViewData().split(/(?<=\n)/)
|
|
198
|
+
const b = candidateText.split(/(?<=\n)/)
|
|
199
|
+
let head = 0
|
|
200
|
+
while (head < a.length && head < b.length && a[head] === b[head]) head += 1
|
|
201
|
+
let tail = 0
|
|
202
|
+
while (tail < a.length - head && tail < b.length - head && a[a.length - 1 - tail] === b[b.length - 1 - tail]) tail += 1
|
|
203
|
+
const starts = [0]
|
|
204
|
+
for (const line of a) starts.push(starts[starts.length - 1] + line.length)
|
|
205
|
+
const am = a.slice(head, a.length - tail)
|
|
206
|
+
const bm = b.slice(head, b.length - tail)
|
|
207
|
+
const changes = []
|
|
208
|
+
if (am.length * bm.length > 4000000) {
|
|
209
|
+
// Too large for a line table inside a synchronous section: one hunk over the differing middle.
|
|
210
|
+
changes.push({ from: starts[head], to: starts[a.length - tail], old: am.join(''), text: bm.join('') })
|
|
211
|
+
} else {
|
|
212
|
+
const lcs = Array.from({ length: am.length + 1 }, () => new Uint32Array(bm.length + 1))
|
|
213
|
+
for (let i = am.length - 1; i >= 0; i -= 1) {
|
|
214
|
+
for (let j = bm.length - 1; j >= 0; j -= 1) lcs[i][j] = am[i] === bm[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1])
|
|
215
|
+
}
|
|
216
|
+
let current = null
|
|
217
|
+
for (let i = 0, j = 0; i < am.length || j < bm.length;) {
|
|
218
|
+
if (i < am.length && j < bm.length && am[i] === bm[j]) { current = null; i += 1; j += 1; continue }
|
|
219
|
+
if (!current) { current = { from: starts[head + i], to: starts[head + i], old: '', text: '' }; changes.push(current) }
|
|
220
|
+
if (j < bm.length && (i >= am.length || lcs[i][j + 1] >= lcs[i + 1][j])) { current.text += bm[j]; j += 1 } else { current.old += am[i]; i += 1; current.to = starts[head + i] }
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
for (const change of changes) {
|
|
224
|
+
let lead = 0
|
|
225
|
+
while (lead < change.old.length && lead < change.text.length && change.old[lead] === change.text[lead]) lead += 1
|
|
226
|
+
let trail = 0
|
|
227
|
+
while (trail < change.old.length - lead && trail < change.text.length - lead
|
|
228
|
+
&& change.old[change.old.length - 1 - trail] === change.text[change.text.length - 1 - trail]) trail += 1
|
|
229
|
+
change.from += lead; change.to -= trail; change.text = change.text.slice(lead, change.text.length - trail)
|
|
230
|
+
}
|
|
231
|
+
view.editor.transaction({ changes: changes.map((change) => ({ from: view.editor.offsetToPos(change.from), to: view.editor.offsetToPos(change.to), text: change.text })) })
|
|
232
|
+
if (view.getViewData() !== candidateText) { step('hunk-fallback'); view.editor.setValue(candidateText) }
|
|
233
|
+
view.lastSavedData = candidateText // the app's delayed save now sees nothing to write
|
|
234
|
+
} catch (error) {
|
|
235
|
+
// The note is already published and the buffer was clean: the app reloads a clean buffer from disk without loss.
|
|
236
|
+
editorUpdateFailed = true
|
|
237
|
+
step('editor-update-failed')
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
step('editor-routed', { openViews: open.length })
|
|
241
|
+
crash('after-editor-update')
|
|
242
|
+
// ---- critical section ends ----
|
|
243
|
+
return done(externalCaptured ? 'published-external-captured' : 'published', { ...snapshot(), wrote: true, openViews: open.length, externalCaptured, recoveredSha256, editorUpdateFailed })
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const encodePayload = (payload) => Buffer.from(JSON.stringify(validatePayload(payload)), 'utf8').toString('base64')
|
|
247
|
+
|
|
248
|
+
// The code sent to the app. Only the base64 literal varies between calls.
|
|
249
|
+
export function buildEvalCode(payload) {
|
|
250
|
+
return `(${criticalSection.toString()})(JSON.parse(new TextDecoder().decode(Uint8Array.from(atob('${encodePayload(payload)}'),c=>c.charCodeAt(0)))),`
|
|
251
|
+
+ `{app,window,document,require,process,performance,exchange:${JSON.stringify(EXCHANGE_CONSTANTS)}})`
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const nodeRequire = createRequire(import.meta.url)
|
|
255
|
+
|
|
256
|
+
// A host for running the same script in this process. With no `app` the
|
|
257
|
+
// script sees no editors; that is only correct when no Obsidian has the vault
|
|
258
|
+
// open, which the publisher establishes before choosing this path.
|
|
259
|
+
export function createInProcessHost({ app = null, window = {}, document = {}, crashSeam } = {}) {
|
|
260
|
+
return { app, window, document, require: nodeRequire, process, performance, exchange: EXCHANGE_CONSTANTS, ...(crashSeam ? { crashSeam } : {}) }
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
export function runInProcess(payload, host) {
|
|
264
|
+
// The payload takes the same JSON round trip it takes on its way into the app.
|
|
265
|
+
return JSON.parse(criticalSection(JSON.parse(JSON.stringify(validatePayload(payload))), host))
|
|
266
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import fs from 'node:fs'
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
|
|
6
|
+
// Atomic exchange of two paths on one volume. After a successful call each
|
|
7
|
+
// path names the file the other path named before it; a failed call changes
|
|
8
|
+
// nothing. This is the only primitive the publisher uses to put generated
|
|
9
|
+
// bytes at an editable path, because whatever occupied that path at the
|
|
10
|
+
// instant of the call is kept, under the other name, instead of destroyed.
|
|
11
|
+
//
|
|
12
|
+
// The call is reached through the system perl as a raw syscall: no perl
|
|
13
|
+
// module, no compiler and no package dependency.
|
|
14
|
+
// macOS: renameatx_np(AT_FDCWD, from, AT_FDCWD, to, RENAME_SWAP)
|
|
15
|
+
// Linux: renameat2(AT_FDCWD, from, AT_FDCWD, to, RENAME_EXCHANGE)
|
|
16
|
+
// The perl binary must be owned by uid 0 and writable by neither group nor
|
|
17
|
+
// others, here and inside the app; otherwise the caller refuses.
|
|
18
|
+
// Link-then-rename is not a substitute and is not offered. Where the exchange
|
|
19
|
+
// is unavailable the caller refuses and publishes nothing.
|
|
20
|
+
|
|
21
|
+
export const EXCHANGE_CONSTANTS = Object.freeze({
|
|
22
|
+
perlCandidates: Object.freeze(['/usr/bin/perl', '/bin/perl', '/usr/local/bin/perl']),
|
|
23
|
+
// argv: syscall number, AT_FDCWD, from, to. The exit status is errno.
|
|
24
|
+
script: 'my ($n,$d,$from,$to)=@ARGV; my $r = syscall($n+0, $d+0, $from, $d+0, $to, 2); exit($r == 0 ? 0 : ($!+0 || 1));',
|
|
25
|
+
calls: Object.freeze({
|
|
26
|
+
'darwin/arm64': Object.freeze({ number: 488, cwd: -2 }),
|
|
27
|
+
'darwin/x64': Object.freeze({ number: 488, cwd: -2 }),
|
|
28
|
+
'linux/x64': Object.freeze({ number: 316, cwd: -100 }),
|
|
29
|
+
'linux/arm64': Object.freeze({ number: 276, cwd: -100 }),
|
|
30
|
+
}),
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
export class ExchangeRefusal extends Error {
|
|
34
|
+
constructor(code, message, detail = {}) {
|
|
35
|
+
super(`${code}: ${message}`)
|
|
36
|
+
this.name = 'ExchangeRefusal'
|
|
37
|
+
this.code = code
|
|
38
|
+
this.detail = detail
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// The interpreter runs inside the publisher and, through the fixed script,
|
|
43
|
+
// inside the app. It is used only when it belongs to the system: owned by
|
|
44
|
+
// uid 0 and writable by neither group nor others.
|
|
45
|
+
export const isTrustedInterpreter = (stat) => Boolean(stat) && stat.uid === 0 && (stat.mode & 0o022) === 0
|
|
46
|
+
|
|
47
|
+
export function resolveExchange({ platform = process.platform, arch = process.arch, perlPath, existsSync = fs.existsSync, statSync = fs.statSync } = {}) {
|
|
48
|
+
if (platform !== 'darwin' && platform !== 'linux') {
|
|
49
|
+
throw new ExchangeRefusal('exchange-unsupported-platform', 'no atomic exchange is known for this platform; nothing is published', { platform })
|
|
50
|
+
}
|
|
51
|
+
const call = EXCHANGE_CONSTANTS.calls[`${platform}/${arch}`]
|
|
52
|
+
if (!call) throw new ExchangeRefusal('exchange-unsupported-architecture', 'the exchange syscall number is not known for this architecture', { platform, arch })
|
|
53
|
+
const perl = perlPath ?? EXCHANGE_CONSTANTS.perlCandidates.find((candidate) => existsSync(candidate))
|
|
54
|
+
if (!perl || !existsSync(perl)) throw new ExchangeRefusal('exchange-interpreter-missing', 'the system perl used to reach the exchange syscall is absent', { platform })
|
|
55
|
+
let stat = null
|
|
56
|
+
try { stat = statSync(perl) } catch { /* unreadable is untrusted */ }
|
|
57
|
+
if (!isTrustedInterpreter(stat)) {
|
|
58
|
+
throw new ExchangeRefusal('exchange-interpreter-untrusted', 'the perl used to reach the exchange syscall is not owned by root, or is writable by group or others; nothing is published', { perl })
|
|
59
|
+
}
|
|
60
|
+
return { perl, number: call.number, cwd: call.cwd }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function exchangeFiles(from, to, options = {}) {
|
|
64
|
+
if (!path.isAbsolute(from) || !path.isAbsolute(to)) throw new TypeError('exchangeFiles needs absolute paths')
|
|
65
|
+
const { perl, number, cwd } = resolveExchange(options)
|
|
66
|
+
try {
|
|
67
|
+
execFileSync(perl, ['-e', EXCHANGE_CONSTANTS.script, '--', String(number), String(cwd), from, to], { stdio: 'ignore' })
|
|
68
|
+
} catch (error) {
|
|
69
|
+
throw new ExchangeRefusal('exchange-failed', 'the atomic exchange did not happen; nothing changed', { exitStatus: error.status ?? null })
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const probed = new Map()
|
|
74
|
+
|
|
75
|
+
export function resetExchangeProbeCache() {
|
|
76
|
+
probed.clear()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Self-test on two scratch files inside `directory`, which must be on the
|
|
80
|
+
// volume that holds the vault. The publisher passes a directory inside the
|
|
81
|
+
// recovery area, where exchange candidates live. The verdict is cached per volume. A refusal is
|
|
82
|
+
// returned, not thrown: { supported: false, code, message }.
|
|
83
|
+
export function probeExchange({ directory, ...options } = {}) {
|
|
84
|
+
if (typeof directory !== 'string' || !path.isAbsolute(directory)) throw new TypeError('probeExchange needs an absolute directory')
|
|
85
|
+
let key
|
|
86
|
+
try {
|
|
87
|
+
key = `${options.platform ?? process.platform}/${options.arch ?? process.arch}/${options.perlPath ?? ''}/${fs.statSync(directory).dev}`
|
|
88
|
+
} catch (error) {
|
|
89
|
+
return { supported: false, code: 'exchange-probe-failed', message: `the exchange directory cannot be read: ${error.code ?? error.message}` }
|
|
90
|
+
}
|
|
91
|
+
if (probed.has(key)) return probed.get(key)
|
|
92
|
+
const id = randomUUID()
|
|
93
|
+
const first = path.join(directory, `.atelier-exchange-probe-${id}.a`)
|
|
94
|
+
const second = path.join(directory, `.atelier-exchange-probe-${id}.b`)
|
|
95
|
+
let verdict
|
|
96
|
+
try {
|
|
97
|
+
const resolved = resolveExchange(options)
|
|
98
|
+
fs.writeFileSync(first, 'a', { flag: 'wx', mode: 0o600 })
|
|
99
|
+
fs.writeFileSync(second, 'b', { flag: 'wx', mode: 0o600 })
|
|
100
|
+
exchangeFiles(first, second, options)
|
|
101
|
+
if (fs.readFileSync(first, 'utf8') !== 'b' || fs.readFileSync(second, 'utf8') !== 'a') {
|
|
102
|
+
throw new ExchangeRefusal('exchange-unsupported-filesystem', 'the exchange call returned success without exchanging the files')
|
|
103
|
+
}
|
|
104
|
+
verdict = { supported: true, perl: resolved.perl }
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (!(error instanceof ExchangeRefusal)) {
|
|
107
|
+
// A scratch file could not even be written (for example a full disk). That says nothing durable about the volume.
|
|
108
|
+
return { supported: false, code: 'exchange-probe-failed', message: `the exchange self-test could not run: ${error.code ?? error.message}` }
|
|
109
|
+
}
|
|
110
|
+
const code = error.code === 'exchange-failed' ? 'exchange-unsupported-filesystem' : error.code
|
|
111
|
+
verdict = { supported: false, code, message: error.message, detail: error.detail }
|
|
112
|
+
} finally {
|
|
113
|
+
for (const file of [first, second]) fs.rmSync(file, { force: true })
|
|
114
|
+
}
|
|
115
|
+
probed.set(key, verdict)
|
|
116
|
+
return verdict
|
|
117
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// Conditional publication of a prepared view into an editable Obsidian vault.
|
|
2
|
+
export { PROTOCOL_ID, buildEvalCode, createInProcessHost, criticalSection, isAddressableVaultPath, runInProcess, validatePayload } from './bridge-script.mjs'
|
|
3
|
+
export { EXCHANGE_CONSTANTS, ExchangeRefusal, exchangeFiles, probeExchange, resetExchangeProbeCache, resolveExchange } from './exchange.mjs'
|
|
4
|
+
export { TransportTimeout, createDirectAdapter, createEditorAdapter, createInProcessCall, createObsidianCliAdapter, createObsidianCliCall, defaultCliPath, defaultObsidianProcessProbe } from './transport.mjs'
|
|
5
|
+
export { publishView } from './publisher.mjs'
|