@mnstry/atelier 0.2.0-alpha.1 → 0.2.0-alpha.11

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.
Files changed (414) hide show
  1. package/CHANGELOG.md +413 -0
  2. package/README.md +402 -205
  3. package/SECURITY.md +6 -1
  4. package/contracts/atelier-adoption-policy.v1.schema.json +49 -0
  5. package/contracts/atelier-coauthor.v1.schema.json +163 -0
  6. package/contracts/atelier-decision-request.v1.schema.json +476 -0
  7. package/contracts/atelier-decision-result.v1.schema.json +500 -0
  8. package/contracts/atelier-guide.v1.schema.json +227 -0
  9. package/contracts/atelier-intake.v1.schema.json +151 -0
  10. package/contracts/atelier-migration.v2.schema.json +61 -0
  11. package/contracts/atelier-obsidian-acceptance-receipt.v1.schema.json +211 -0
  12. package/contracts/atelier-obsidian-apply-policy.v1.schema.json +298 -0
  13. package/contracts/atelier-obsidian-corpus-profile.v1.schema.json +98 -0
  14. package/contracts/atelier-obsidian-edit-operation.v1.schema.json +138 -0
  15. package/contracts/atelier-obsidian-ext-settings.v1.schema.json +287 -0
  16. package/contracts/atelier-obsidian-generation-manifest.v1.schema.json +353 -0
  17. package/contracts/atelier-obsidian-proposal-receipt.v1.schema.json +110 -0
  18. package/contracts/atelier-obsidian-publication-journal.v1.schema.json +167 -0
  19. package/contracts/atelier-obsidian-scope.v1.schema.json +264 -0
  20. package/contracts/atelier-obsidian-service-state.v1.schema.json +149 -0
  21. package/contracts/atelier-obsidian-source-snapshot.v1.schema.json +179 -0
  22. package/contracts/atelier-pane-presentation.v1.schema.json +76 -0
  23. package/contracts/atelier-presentation.v1.schema.json +1293 -0
  24. package/contracts/atelier-repository-observation.v1.schema.json +163 -0
  25. package/contracts/atelier-review.v1.schema.json +1601 -0
  26. package/contracts/atelier-skill-steward.v1.schema.json +692 -0
  27. package/contracts/atelier-upgrade-plan.v2.schema.json +398 -0
  28. package/contracts/atelier-upgrade-receipt.v1.schema.json +99 -0
  29. package/contracts/atelier-vault-publication.v1.schema.json +58 -0
  30. package/contracts/public-api-baseline.json +57 -0
  31. package/docs/assurance-controls.md +49 -0
  32. package/docs/atelier-runtime.md +28 -2
  33. package/docs/atelier-sync.md +172 -0
  34. package/docs/blocks/audience-visibility.md +9 -0
  35. package/docs/blocks/claims.md +61 -0
  36. package/docs/blocks/conformance-admission.md +15 -0
  37. package/docs/blocks/will-not-do.md +18 -0
  38. package/docs/coauthor-session.md +57 -0
  39. package/docs/connected-composition.md +75 -0
  40. package/docs/continuity.md +3 -4
  41. package/docs/decisions.md +167 -0
  42. package/docs/design.md +167 -0
  43. package/docs/exact-upgrades.md +159 -0
  44. package/docs/extension-pack-lifecycle.md +33 -0
  45. package/docs/guided-upgrades.md +99 -0
  46. package/docs/install.md +99 -16
  47. package/docs/intake-and-guides.md +65 -0
  48. package/docs/integration-and-review-plan.md +409 -0
  49. package/docs/integration-contract-decisions.md +22 -0
  50. package/docs/knowledge-graph.md +8 -4
  51. package/docs/local-review.md +84 -0
  52. package/docs/local-services.md +328 -0
  53. package/docs/local-state.md +107 -0
  54. package/docs/obsidian-contract.md +698 -0
  55. package/docs/obsidian.md +484 -0
  56. package/docs/portable-presentation.md +328 -0
  57. package/docs/presentation-consumer-boundaries.md +75 -0
  58. package/docs/presentation-review-disposition.md +47 -0
  59. package/docs/project-command-inventory.md +38 -0
  60. package/docs/project-options.md +53 -0
  61. package/docs/release-engineering.md +174 -12
  62. package/docs/repo-boundary-guard.md +12 -2
  63. package/docs/review-portability.md +55 -0
  64. package/docs/skill-steward.md +156 -0
  65. package/docs/upgrade.md +155 -5
  66. package/docs/vault-service.md +292 -0
  67. package/fixtures/atelier-coauthor/invalid/authority.v1.json +5 -0
  68. package/fixtures/atelier-coauthor/valid/config.v1.json +12 -0
  69. package/fixtures/atelier-guide/invalid/authority.v1.json +11 -0
  70. package/fixtures/atelier-guide/valid/capability.v1.json +10 -0
  71. package/fixtures/atelier-guide/valid/consent.v1.json +11 -0
  72. package/fixtures/atelier-guide/valid/engagement.v1.json +7 -0
  73. package/fixtures/atelier-guide/valid/offer.v1.json +10 -0
  74. package/fixtures/atelier-intake/invalid/authority.v1.json +9 -0
  75. package/fixtures/atelier-intake/valid/attempt.v1.json +8 -0
  76. package/fixtures/atelier-intake/valid/completion.v1.json +9 -0
  77. package/fixtures/atelier-intake/valid/source.v1.json +7 -0
  78. package/fixtures/atelier-repository-observation/invalid/complete-with-blocker.v1.json +18 -0
  79. package/fixtures/atelier-repository-observation/valid/complete-local.v1.json +48 -0
  80. package/fixtures/atelier-review/invalid/authenticated-identity.v1.json +22 -0
  81. package/fixtures/atelier-review/valid/bound.v1.json +229 -0
  82. package/fixtures/atelier-review/valid/bundle.v1.json +267 -0
  83. package/fixtures/atelier-review/valid/contribution.v1.json +22 -0
  84. package/fixtures/atelier-review/valid/lifecycle.v1.json +14 -0
  85. package/fixtures/atelier-skill-steward/audit/invalid/source-mutation.v1.json +26 -0
  86. package/fixtures/atelier-skill-steward/audit/valid/clean.v1.json +26 -0
  87. package/fixtures/atelier-skill-steward/candidates/invalid/workflow-key.v1.json +37 -0
  88. package/fixtures/atelier-skill-steward/candidates/valid/create.v1.json +37 -0
  89. package/fixtures/atelier-skill-steward/lock/invalid/bad-digest.v1.json +18 -0
  90. package/fixtures/atelier-skill-steward/lock/valid/installed.v1.json +23 -0
  91. package/fixtures/atelier-skill-steward/plan/invalid/delete-action.v1.json +33 -0
  92. package/fixtures/atelier-skill-steward/plan/valid/add.v1.json +33 -0
  93. package/fixtures/atelier-upgrade-transaction/adoption-policy/invalid/example.json +12 -0
  94. package/fixtures/atelier-upgrade-transaction/adoption-policy/valid/example.json +11 -0
  95. package/fixtures/atelier-upgrade-transaction/migration/invalid/example.json +20 -0
  96. package/fixtures/atelier-upgrade-transaction/migration/valid/example.json +19 -0
  97. package/fixtures/atelier-upgrade-transaction/upgrade-plan/invalid/example.json +58 -0
  98. package/fixtures/atelier-upgrade-transaction/upgrade-plan/valid/example.json +57 -0
  99. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/invalid/example.json +15 -0
  100. package/fixtures/atelier-upgrade-transaction/upgrade-receipt/valid/example.json +14 -0
  101. package/fixtures/atelier-vault-publication/invalid/publication.json +5 -0
  102. package/fixtures/atelier-vault-publication/valid/publication.json +11 -0
  103. package/fixtures/decisions/request/invalid/unknown-authority.v1.json +61 -0
  104. package/fixtures/decisions/request/valid/triage.v1.json +60 -0
  105. package/fixtures/decisions/result/invalid/execution-authority.v1.json +50 -0
  106. package/fixtures/decisions/result/valid/abstained.v1.json +23 -0
  107. package/fixtures/decisions/result/valid/assessed.v1.json +50 -0
  108. package/fixtures/obsidian/acceptance/ap05-extra-notes.json +14 -0
  109. package/fixtures/obsidian/acceptance/receipts/G07.valid.v1.json +63 -0
  110. package/fixtures/obsidian/acceptance/receipts/G13.valid.v1.json +69 -0
  111. package/fixtures/obsidian/acceptance/receipts/G14.valid.v1.json +62 -0
  112. package/fixtures/obsidian/acceptance/receipts/G15.valid.v1.json +56 -0
  113. package/fixtures/obsidian/acceptance/receipts/G16.valid.v1.json +73 -0
  114. package/fixtures/obsidian/acceptance/receipts/G17.valid.v1.json +75 -0
  115. package/fixtures/obsidian/acceptance/receipts/G18.valid.v1.json +64 -0
  116. package/fixtures/obsidian/acceptance/service-sentinels.json +10 -0
  117. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/duplicate-evidence-name.v1.json +39 -0
  118. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/evidence-by-absolute-path.v1.json +34 -0
  119. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/missing-candidate-tree.v1.json +33 -0
  120. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/no-evidence.v1.json +28 -0
  121. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-outcome.v1.json +34 -0
  122. package/fixtures/obsidian/contracts/acceptance-receipt/invalid/unknown-top-level-field.v1.json +35 -0
  123. package/fixtures/obsidian/contracts/acceptance-receipt/valid/node-gate.v1.json +34 -0
  124. package/fixtures/obsidian/contracts/apply-policy/invalid/automatic-without-edit-class.v1.json +27 -0
  125. package/fixtures/obsidian/contracts/apply-policy/invalid/conflict-overwrite.v1.json +29 -0
  126. package/fixtures/obsidian/contracts/apply-policy/invalid/missing-policy-digest.v1.json +28 -0
  127. package/fixtures/obsidian/contracts/apply-policy/invalid/unbounded-batch.v1.json +29 -0
  128. package/fixtures/obsidian/contracts/apply-policy/invalid/unimplemented-edit-class.v1.json +30 -0
  129. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-mode.v1.json +29 -0
  130. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-selector-operator.v1.json +22 -0
  131. package/fixtures/obsidian/contracts/apply-policy/invalid/unknown-top-level-field.v1.json +30 -0
  132. package/fixtures/obsidian/contracts/apply-policy/valid/automatic-scoped.v1.json +29 -0
  133. package/fixtures/obsidian/contracts/apply-policy/valid/manual-default.v1.json +20 -0
  134. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-path-in-ext.v1.json +30 -0
  135. package/fixtures/obsidian/contracts/corpus-profile/invalid/absolute-repository-root.v1.json +27 -0
  136. package/fixtures/obsidian/contracts/corpus-profile/invalid/duplicate-repository-id.v1.json +27 -0
  137. package/fixtures/obsidian/contracts/corpus-profile/invalid/overlapping-managed-roots.v1.json +27 -0
  138. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-enrollment.v1.json +27 -0
  139. package/fixtures/obsidian/contracts/corpus-profile/invalid/unknown-top-level-field.v1.json +28 -0
  140. package/fixtures/obsidian/contracts/corpus-profile/valid/three-repositories.v1.json +27 -0
  141. package/fixtures/obsidian/contracts/edit-operation/invalid/absolute-recovery-ref.v1.json +21 -0
  142. package/fixtures/obsidian/contracts/edit-operation/invalid/inline-observed-bytes.v1.json +22 -0
  143. package/fixtures/obsidian/contracts/edit-operation/invalid/missing-base-digest.v1.json +20 -0
  144. package/fixtures/obsidian/contracts/edit-operation/invalid/short-idempotency-key.v1.json +21 -0
  145. package/fixtures/obsidian/contracts/edit-operation/invalid/unknown-kind.v1.json +21 -0
  146. package/fixtures/obsidian/contracts/edit-operation/valid/pending-body-replacement.v1.json +21 -0
  147. package/fixtures/obsidian/contracts/ext-settings/invalid/absolute-path-prefix.v1.json +28 -0
  148. package/fixtures/obsidian/contracts/ext-settings/invalid/apply-policy-in-portable-settings.v1.json +30 -0
  149. package/fixtures/obsidian/contracts/ext-settings/invalid/duplicate-scope-id.v1.json +27 -0
  150. package/fixtures/obsidian/contracts/ext-settings/invalid/machine-local-vault-path.v1.json +28 -0
  151. package/fixtures/obsidian/contracts/ext-settings/invalid/service-port-in-portable-settings.v1.json +28 -0
  152. package/fixtures/obsidian/contracts/ext-settings/invalid/unknown-default-scope.v1.json +27 -0
  153. package/fixtures/obsidian/contracts/ext-settings/valid/disabled.v1.json +5 -0
  154. package/fixtures/obsidian/contracts/ext-settings/valid/enabled-with-scopes.v1.json +27 -0
  155. package/fixtures/obsidian/contracts/generation-manifest/invalid/absolute-note-path.v1.json +114 -0
  156. package/fixtures/obsidian/contracts/generation-manifest/invalid/complete-with-unwritten-notes.v1.json +114 -0
  157. package/fixtures/obsidian/contracts/generation-manifest/invalid/derived-link-claimed-declared.v1.json +114 -0
  158. package/fixtures/obsidian/contracts/generation-manifest/invalid/duplicate-note-identity.v1.json +114 -0
  159. package/fixtures/obsidian/contracts/generation-manifest/invalid/in-scope-endpoint-missing.v1.json +114 -0
  160. package/fixtures/obsidian/contracts/generation-manifest/invalid/title-only-note-path.v1.json +114 -0
  161. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-note-field.v1.json +115 -0
  162. package/fixtures/obsidian/contracts/generation-manifest/invalid/unknown-relation-type.v1.json +114 -0
  163. package/fixtures/obsidian/contracts/generation-manifest/invalid/withheld-endpoint.v1.json +114 -0
  164. package/fixtures/obsidian/contracts/generation-manifest/valid/two-notes.v1.json +114 -0
  165. package/fixtures/obsidian/contracts/oracles/scope-cases.json +412 -0
  166. package/fixtures/obsidian/contracts/proposal-receipt/invalid/accepted-without-proposal-id.v1.json +12 -0
  167. package/fixtures/obsidian/contracts/proposal-receipt/invalid/missing-adapter-operation-id.v1.json +11 -0
  168. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-backpressure-outcome.v1.json +12 -0
  169. package/fixtures/obsidian/contracts/proposal-receipt/invalid/unknown-top-level-field.v1.json +13 -0
  170. package/fixtures/obsidian/contracts/proposal-receipt/valid/accepted-new.v1.json +12 -0
  171. package/fixtures/obsidian/contracts/proposal-receipt/valid/deferred.v1.json +12 -0
  172. package/fixtures/obsidian/contracts/publication-journal/invalid/absolute-recovery-ref.v1.json +47 -0
  173. package/fixtures/obsidian/contracts/publication-journal/invalid/duplicate-sequence.v1.json +47 -0
  174. package/fixtures/obsidian/contracts/publication-journal/invalid/empty-protocol-id.v1.json +47 -0
  175. package/fixtures/obsidian/contracts/publication-journal/invalid/missing-protocol-id.v1.json +46 -0
  176. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-entry-field.v1.json +48 -0
  177. package/fixtures/obsidian/contracts/publication-journal/invalid/unknown-state.v1.json +47 -0
  178. package/fixtures/obsidian/contracts/publication-journal/valid/committed.v1.json +47 -0
  179. package/fixtures/obsidian/contracts/publication-journal/valid/updating-after-restart.v1.json +22 -0
  180. package/fixtures/obsidian/contracts/scope/invalid/absolute-path-prefix.v1.json +15 -0
  181. package/fixtures/obsidian/contracts/scope/invalid/duplicate-ids.v1.json +18 -0
  182. package/fixtures/obsidian/contracts/scope/invalid/expansion-without-node-budget.v1.json +40 -0
  183. package/fixtures/obsidian/contracts/scope/invalid/full-mode-with-subset.v1.json +8 -0
  184. package/fixtures/obsidian/contracts/scope/invalid/unknown-mode.v1.json +41 -0
  185. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-key.v1.json +15 -0
  186. package/fixtures/obsidian/contracts/scope/invalid/unknown-selector-operator.v1.json +14 -0
  187. package/fixtures/obsidian/contracts/scope/invalid/unknown-top-level-field.v1.json +42 -0
  188. package/fixtures/obsidian/contracts/scope/valid/explicit-empty.v1.json +8 -0
  189. package/fixtures/obsidian/contracts/scope/valid/focus.v1.json +11 -0
  190. package/fixtures/obsidian/contracts/scope/valid/full.v1.json +8 -0
  191. package/fixtures/obsidian/contracts/scope/valid/scoped-set-expression.v1.json +41 -0
  192. package/fixtures/obsidian/contracts/service-state/invalid/hostname-instead-of-literal-loopback.v1.json +23 -0
  193. package/fixtures/obsidian/contracts/service-state/invalid/missing-consent.v1.json +18 -0
  194. package/fixtures/obsidian/contracts/service-state/invalid/privileged-port.v1.json +23 -0
  195. package/fixtures/obsidian/contracts/service-state/invalid/unknown-top-level-field.v1.json +24 -0
  196. package/fixtures/obsidian/contracts/service-state/invalid/wildcard-bind.v1.json +23 -0
  197. package/fixtures/obsidian/contracts/service-state/valid/healthy.v1.json +23 -0
  198. package/fixtures/obsidian/contracts/source-snapshot/invalid/absolute-file-path.v1.json +48 -0
  199. package/fixtures/obsidian/contracts/source-snapshot/invalid/duplicate-repository-id.v1.json +48 -0
  200. package/fixtures/obsidian/contracts/source-snapshot/invalid/mixed-read.v1.json +48 -0
  201. package/fixtures/obsidian/contracts/source-snapshot/invalid/parent-traversal-path.v1.json +48 -0
  202. package/fixtures/obsidian/contracts/source-snapshot/invalid/unknown-file-field.v1.json +49 -0
  203. package/fixtures/obsidian/contracts/source-snapshot/valid/two-repositories.v1.json +48 -0
  204. package/fixtures/obsidian/edits/workspace.json +65 -0
  205. package/fixtures/obsidian/materialization/expected-full.json +99 -0
  206. package/fixtures/obsidian/materialization/expected-scoped.json +39 -0
  207. package/fixtures/obsidian/materialization/workspace.json +54 -0
  208. package/fixtures/obsidian/proposals/expected-shapes.json +11 -0
  209. package/fixtures/projects/sample-workspace/content/source.html.kg.json +4 -1
  210. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +4 -1
  211. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +4 -1
  212. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +4 -1
  213. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +4 -1
  214. package/fixtures/ui/presentation/pane.v1.json +7 -0
  215. package/fixtures/ui/presentation/reference.v1.json +293 -0
  216. package/package.json +86 -10
  217. package/skills/claude/atelier-guided-coauthor/SKILL.md +49 -0
  218. package/skills/claude/atelier-guided-upgrade/SKILL.md +97 -0
  219. package/skills/claude/atelier-local-service/SKILL.md +47 -0
  220. package/skills/claude/atelier-open/SKILL.md +11 -5
  221. package/skills/claude/atelier-public-boundary/SKILL.md +31 -0
  222. package/skills/claude/atelier-skill-steward/SKILL.md +62 -0
  223. package/skills/claude/mnstry-readiness/SKILL.md +5 -0
  224. package/skills/codex/atelier-guided-coauthor/SKILL.md +49 -0
  225. package/skills/codex/atelier-guided-upgrade/SKILL.md +97 -0
  226. package/skills/codex/atelier-local-service/SKILL.md +47 -0
  227. package/skills/codex/atelier-open/SKILL.md +11 -5
  228. package/skills/codex/atelier-public-boundary/SKILL.md +31 -0
  229. package/skills/codex/atelier-skill-steward/SKILL.md +62 -0
  230. package/skills/codex/mnstry-readiness/SKILL.md +5 -0
  231. package/src/access/preview-enforcer.mjs +84 -0
  232. package/src/boundary/content-rules.mjs +283 -20
  233. package/src/boundary/policy.mjs +213 -91
  234. package/src/cli/execute-command.mjs +36 -0
  235. package/src/cli/project-options.mjs +58 -0
  236. package/src/cli/run.mjs +78 -10
  237. package/src/coauthor/session.mjs +161 -0
  238. package/src/coauthor/store.mjs +173 -0
  239. package/src/collaboration/event-ledger.mjs +348 -0
  240. package/src/collaboration/index.mjs +17 -0
  241. package/src/collaboration/inspection-bundle.mjs +223 -0
  242. package/src/collaboration/proposals.mjs +265 -65
  243. package/src/collaboration/review-contracts.mjs +25 -0
  244. package/src/collaboration/review-store.mjs +305 -0
  245. package/src/commands/attestation.mjs +20 -6
  246. package/src/commands/coauthor.mjs +26 -0
  247. package/src/commands/config.mjs +6 -1
  248. package/src/commands/disclosure.mjs +133 -0
  249. package/src/commands/distribution.mjs +2 -1
  250. package/src/commands/extension-pack.mjs +17 -9
  251. package/src/commands/init.mjs +46 -6
  252. package/src/commands/obsidian.mjs +371 -0
  253. package/src/commands/review.mjs +147 -0
  254. package/src/commands/server.mjs +1 -4
  255. package/src/commands/setup.mjs +51 -14
  256. package/src/commands/skills.mjs +199 -0
  257. package/src/commands/sync.mjs +107 -0
  258. package/src/composition/registry.mjs +27 -0
  259. package/src/composition/wire.mjs +122 -0
  260. package/src/contracts/corpus.mjs +35 -0
  261. package/src/decisions/contracts.d.mts +96 -0
  262. package/src/decisions/contracts.mjs +321 -0
  263. package/src/disclosure/content-scan.mjs +206 -0
  264. package/src/egress/check.mjs +7 -38
  265. package/src/egress/forbidden-egress.mjs +32 -18
  266. package/src/export/atelier-export-contract.mjs +16 -1
  267. package/src/extension-packs/lifecycle.mjs +154 -0
  268. package/src/graph/graph.mjs +176 -316
  269. package/src/graph/knowledge-graph.mjs +643 -43
  270. package/src/guides/contracts.mjs +54 -0
  271. package/src/harness/context-client.mjs +9 -1
  272. package/src/index.mjs +41 -0
  273. package/src/intake/store.mjs +119 -0
  274. package/src/preview/channel.mjs +89 -0
  275. package/src/project/config.mjs +112 -52
  276. package/src/project/durable-state.mjs +161 -0
  277. package/src/project/file-class.mjs +180 -0
  278. package/src/project/package-root.mjs +10 -0
  279. package/src/project/path-match.mjs +38 -15
  280. package/src/project/private-state.mjs +120 -0
  281. package/src/projection/obsidian/contracts.mjs +571 -0
  282. package/src/projection/obsidian/edits/align.mjs +169 -0
  283. package/src/projection/obsidian/edits/apply.mjs +889 -0
  284. package/src/projection/obsidian/edits/arbitrate.mjs +414 -0
  285. package/src/projection/obsidian/edits/contribution.mjs +108 -0
  286. package/src/projection/obsidian/edits/index.mjs +19 -0
  287. package/src/projection/obsidian/edits/object-identity.mjs +61 -0
  288. package/src/projection/obsidian/edits/object-store.mjs +517 -0
  289. package/src/projection/obsidian/edits/observe.mjs +123 -0
  290. package/src/projection/obsidian/edits/policy.mjs +123 -0
  291. package/src/projection/obsidian/edits/regions.mjs +648 -0
  292. package/src/projection/obsidian/materialize/byte-lens.mjs +98 -0
  293. package/src/projection/obsidian/materialize/index.mjs +7 -0
  294. package/src/projection/obsidian/materialize/path-registry.mjs +105 -0
  295. package/src/projection/obsidian/materialize/prepare-view.mjs +746 -0
  296. package/src/projection/obsidian/materialize/settings.mjs +57 -0
  297. package/src/projection/obsidian/materialize/stage.mjs +27 -0
  298. package/src/projection/obsidian/proposals/adapter.mjs +572 -0
  299. package/src/projection/obsidian/proposals/backpressure.mjs +113 -0
  300. package/src/projection/obsidian/proposals/contribution.mjs +55 -0
  301. package/src/projection/obsidian/proposals/index.mjs +23 -0
  302. package/src/projection/obsidian/proposals/observation.mjs +216 -0
  303. package/src/projection/obsidian/proposals/queue.mjs +277 -0
  304. package/src/projection/obsidian/proposals/router.mjs +168 -0
  305. package/src/projection/obsidian/publication/bridge-script.mjs +266 -0
  306. package/src/projection/obsidian/publication/exchange.mjs +117 -0
  307. package/src/projection/obsidian/publication/index.mjs +5 -0
  308. package/src/projection/obsidian/publication/publisher.mjs +588 -0
  309. package/src/projection/obsidian/publication/test-seam.mjs +16 -0
  310. package/src/projection/obsidian/publication/transport.mjs +212 -0
  311. package/src/projection/obsidian/recovery/index.mjs +6 -0
  312. package/src/projection/obsidian/recovery/journal.mjs +134 -0
  313. package/src/projection/obsidian/recovery/late-writer.mjs +46 -0
  314. package/src/projection/obsidian/recovery/restart.mjs +316 -0
  315. package/src/projection/obsidian/recovery/store.mjs +234 -0
  316. package/src/projection/obsidian/selection-ui/conflict-view.mjs +91 -0
  317. package/src/projection/obsidian/selection-ui/contribution.mjs +141 -0
  318. package/src/projection/obsidian/selection-ui/focus.mjs +82 -0
  319. package/src/projection/obsidian/selection-ui/index.mjs +12 -0
  320. package/src/projection/obsidian/selection-ui/policy-setup.mjs +107 -0
  321. package/src/projection/obsidian/selection-ui/receipt.mjs +173 -0
  322. package/src/projection/obsidian/selection-ui/selection-state.mjs +122 -0
  323. package/src/projection/obsidian/selection-ui/selection.mjs +109 -0
  324. package/src/projection/project.mjs +28 -19
  325. package/src/readiness-protocols/evidence.mjs +282 -0
  326. package/src/readiness-protocols/runtime.mjs +4 -3
  327. package/src/readiness-protocols/source-read.mjs +39 -0
  328. package/src/runtime/git-adapter.mjs +189 -0
  329. package/src/runtime/local-state.mjs +439 -0
  330. package/src/runtime/obsidian/app-capability.mjs +138 -0
  331. package/src/runtime/obsidian/app-production-seams.mjs +107 -0
  332. package/src/runtime/obsidian/contributions/proposal-adapter.mjs +4 -0
  333. package/src/runtime/obsidian/contributions/selection-ui.mjs +4 -0
  334. package/src/runtime/obsidian/contributions/source-apply.mjs +4 -0
  335. package/src/runtime/obsidian/contributions.mjs +27 -0
  336. package/src/runtime/obsidian/documents.mjs +37 -0
  337. package/src/runtime/obsidian/enablement.mjs +53 -0
  338. package/src/runtime/obsidian/engine.mjs +532 -0
  339. package/src/runtime/obsidian/errors.mjs +14 -0
  340. package/src/runtime/obsidian/extension-points.mjs +103 -0
  341. package/src/runtime/obsidian/index.mjs +34 -0
  342. package/src/runtime/obsidian/lifecycle.mjs +258 -0
  343. package/src/runtime/obsidian/machine-settings.mjs +260 -0
  344. package/src/runtime/obsidian/observation.mjs +158 -0
  345. package/src/runtime/obsidian/opening.mjs +280 -0
  346. package/src/runtime/obsidian/pending-edits.mjs +141 -0
  347. package/src/runtime/obsidian/pipeline.mjs +122 -0
  348. package/src/runtime/obsidian/private-lock.mjs +156 -0
  349. package/src/runtime/obsidian/process-identity.mjs +58 -0
  350. package/src/runtime/obsidian/service-client.mjs +66 -0
  351. package/src/runtime/obsidian/service-entry-path.mjs +9 -0
  352. package/src/runtime/obsidian/service-main.mjs +93 -0
  353. package/src/runtime/obsidian/service-record.mjs +169 -0
  354. package/src/runtime/obsidian/service-server.mjs +121 -0
  355. package/src/runtime/obsidian/service.mjs +216 -0
  356. package/src/runtime/obsidian/startup-adapters.mjs +87 -0
  357. package/src/runtime/obsidian/state-store.mjs +180 -0
  358. package/src/runtime/obsidian/sync-notice.mjs +33 -0
  359. package/src/runtime/obsidian/tick-loop.mjs +84 -0
  360. package/src/runtime/obsidian/watchers.mjs +36 -0
  361. package/src/runtime/repository-observation.mjs +491 -0
  362. package/src/runtime/supervisor.mjs +803 -0
  363. package/src/server/local-sidecar.mjs +125 -59
  364. package/src/server/security.mjs +89 -4
  365. package/src/server/server.mjs +4 -2
  366. package/src/skills/steward.mjs +780 -0
  367. package/src/support/feedback-report.mjs +4 -3
  368. package/src/ui/presentation/browser.mjs +278 -0
  369. package/src/ui/presentation/contract.mjs +116 -0
  370. package/src/ui/presentation/index.mjs +7 -0
  371. package/src/ui/presentation/native.mjs +153 -0
  372. package/src/ui/presentation/proof.mjs +57 -0
  373. package/src/ui/presentation/schema-check.mjs +30 -0
  374. package/src/ui/presentation/schema.generated.mjs +1371 -0
  375. package/src/ui/presentation/state.mjs +43 -0
  376. package/src/ui/presentation/styles.mjs +67 -0
  377. package/src/ui/presentation/tokens.mjs +74 -0
  378. package/src/ui/presentation/web.mjs +85 -0
  379. package/src/ui/review-page.mjs +32 -0
  380. package/src/upgrade/explanation.mjs +33 -0
  381. package/src/upgrade/provenance.mjs +338 -0
  382. package/src/upgrade/transaction-files.mjs +103 -0
  383. package/src/upgrade/transaction.mjs +475 -0
  384. package/src/upgrade/upgrade.mjs +65 -29
  385. package/src/vault/hosts.mjs +21 -0
  386. package/src/vault/identity.mjs +19 -0
  387. package/src/vault/index.mjs +7 -0
  388. package/src/vault/interface.mjs +11 -0
  389. package/src/vault/metadata.mjs +31 -0
  390. package/src/vault/privacy.mjs +148 -0
  391. package/src/vault/probe.mjs +89 -0
  392. package/src/vault/service.mjs +131 -0
  393. package/src/vault/source.mjs +45 -0
  394. package/src/vault/storage.mjs +23 -0
  395. package/templates/distribution-workspace/.claude/launch.json +6 -2
  396. package/templates/distribution-workspace/gitignore +1 -0
  397. package/templates/distribution-workspace/project/README.md +2 -2
  398. package/templates/external-project-workspace/README.md +45 -0
  399. package/templates/external-project-workspace/answers.example.json +4 -0
  400. package/templates/external-project-workspace/atelier.project.json +40 -0
  401. package/templates/external-project-workspace/boundary-policy.v1.json +60 -0
  402. package/templates/external-project-workspace/gitignore +8 -0
  403. package/templates/external-project-workspace/pack-lifecycle.json +14 -0
  404. package/templates/external-project-workspace/packs/protocols/contract-gate.v1.json +109 -0
  405. package/templates/external-project-workspace/packs/sample-pack.v1.json +48 -0
  406. package/templates/external-project-workspace/repo-access.v1.json +9 -0
  407. package/templates/external-project-workspace/source/README.md +11 -0
  408. package/templates/launch.json +6 -2
  409. package/templates/private-domain-workspace/.claude/launch.json +6 -2
  410. package/templates/private-domain-workspace/domain/README.md +2 -2
  411. package/templates/private-domain-workspace/gitignore +1 -0
  412. package/templates/shared-project-workspace/.claude/launch.json +6 -2
  413. package/templates/shared-project-workspace/gitignore +1 -0
  414. package/templates/shared-project-workspace/project/README.md +2 -2
@@ -3,7 +3,7 @@ import crypto from 'node:crypto'
3
3
  import fs from 'node:fs'
4
4
  import path from 'node:path'
5
5
  import { buildGraph } from '../graph/graph.mjs'
6
- import { buildProjectProjection } from '../projection/project.mjs'
6
+ import { buildProjectProjection, buildProjectManifest } from '../projection/project.mjs'
7
7
  import { buildReadiness } from '../readiness/readiness.mjs'
8
8
  import { validateJsonSchema } from '../export/atelier-export-contract.mjs'
9
9
  import {
@@ -14,6 +14,7 @@ import {
14
14
  resolvePathValue,
15
15
  writeJson,
16
16
  } from '../project/config.mjs'
17
+ import { packageRootFrom } from '../project/package-root.mjs'
17
18
  import {
18
19
  BOUNDARY_POLICY_SCHEMA,
19
20
  checkBoundaryPolicy,
@@ -22,6 +23,10 @@ import {
22
23
  } from '../boundary/policy.mjs'
23
24
  import { bundledMnstryReadinessPackV1 } from '../readiness-protocols/bundled-pack.mjs'
24
25
  import { loadExtensionPacks } from '../extension-packs/loader.mjs'
26
+ import { inspectPackageProvenance, legacyPackageSource } from './provenance.mjs'
27
+ import { prepareUpgrade, applySavedUpgrade, upgradeOperationStatus, recoverUpgradeDryRun, explainSavedUpgrade } from './transaction.mjs'
28
+ import { renderUpgradeExplanation } from './explanation.mjs'
29
+ export { prepareUpgrade, applySavedUpgrade, upgradeOperationStatus, recoverUpgradeDryRun, explainSavedUpgrade }
25
30
 
26
31
  export const ATELIER_LOCK_SCHEMA = 'mnstry.atelier-lock@v1'
27
32
  export const ATELIER_MIGRATION_SCHEMA = 'mnstry.atelier-migration@v1'
@@ -36,7 +41,7 @@ export const MIGRATION_CLASSES = new Set([
36
41
  'breaking',
37
42
  ])
38
43
 
39
- const packageRoot = path.resolve(new URL('../..', import.meta.url).pathname)
44
+ const packageRoot = packageRootFrom(import.meta.url)
40
45
  const packageJson = readJson(path.join(packageRoot, 'package.json'))
41
46
  const lockSchema = readJson(path.join(packageRoot, 'contracts', 'atelier-lock.v1.schema.json'))
42
47
  const REVIEW_MARKER_RE = /(Atelier-Boundary-Review|boundary-review)\s*:\s*(approved|reviewed)/i
@@ -76,18 +81,7 @@ function packageGitSha() {
76
81
  }
77
82
 
78
83
  function packageSource() {
79
- const gitSha = packageGitSha()
80
- const repository = packageGitRoot()
81
- ? git(packageRoot, ['config', '--get', 'remote.origin.url']) || packageJson.repository?.url || null
82
- : packageJson.repository?.url || null
83
- if (repository && gitSha) return { type: 'git', repository, gitSha }
84
- if (repository) return { type: 'private_github', repository, gitSha: null }
85
- return {
86
- type: 'local_path',
87
- path: '.',
88
- gitSha,
89
- repository,
90
- }
84
+ return legacyPackageSource(inspectPackageProvenance(packageRoot, { includeInventory: false }))
91
85
  }
92
86
 
93
87
  export function lockPathForProject(project) {
@@ -171,7 +165,7 @@ export function buildAtelierLock({ project, templateId = 'existing-workspace', a
171
165
  }
172
166
  }
173
167
 
174
- export function writeAtelierLock({ project, templateId = 'existing-workspace' } = {}) {
168
+ export function writeAtelierLock({ project, templateId } = {}) {
175
169
  const lockPath = lockPathForProject(project)
176
170
  // Packs load in throwing mode on purpose: a broken extension pack cannot be
177
171
  // locked. Lock verification is disabled for this one load so re-pinning is
@@ -179,8 +173,18 @@ export function writeAtelierLock({ project, templateId = 'existing-workspace' }
179
173
  // moved or set aside, so it exists at every observable point, and writeJson
180
174
  // replaces it atomically (temp file + renameSync). A load failure throws
181
175
  // before any write and leaves the previous lock untouched.
176
+ const previous = readOptionalJson(lockPath)
177
+ if (previous) {
178
+ const errors = validateJsonSchema(lockSchema, previous)
179
+ if (errors.length) throw new Error(`cannot preserve invalid lock history: ${errors.join('; ')}`)
180
+ }
182
181
  const { packs } = loadExtensionPacks(project, { verifyLock: false })
183
- const lock = buildAtelierLock({ project, templateId, packs })
182
+ const lock = buildAtelierLock({ project, templateId: templateId || previous?.template?.id || 'existing-workspace', packs })
183
+ if (previous) {
184
+ lock.appliedMigrations = previous.appliedMigrations
185
+ lock.lastSuccessfulUpgrade = previous.lastSuccessfulUpgrade
186
+ lock.template = { ...previous.template, ...(templateId ? { id: templateId } : {}) }
187
+ }
184
188
  writeJson(lockPath, lock)
185
189
  return lock
186
190
  }
@@ -518,13 +522,7 @@ function runGeneratedRefresh(project) {
518
522
  const projection = buildProjectProjection(project)
519
523
  fs.mkdirSync(path.dirname(projection.output), { recursive: true })
520
524
  fs.writeFileSync(projection.output, projection.html)
521
- writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), {
522
- schema: 'mnstry.atelier-manifest@v1',
523
- generatedAt: 'deterministic',
524
- graphPath: project.graphPath,
525
- entry: 'index.html',
526
- nodes: projection.graph.nodes.map((node) => ({ id: node.id, title: node.title, audience: node.audience, path: node.path })),
527
- })
525
+ writeJson(path.join(project.outputRoot, 'atelier.manifest.json'), buildProjectManifest(project, projection))
528
526
  writeJson(project.readinessPath, buildReadiness({ project, graph }))
529
527
  }
530
528
 
@@ -616,7 +614,7 @@ export function checkAtelierLock(project) {
616
614
  if (lock && lock.package?.version !== packageJson.version) errors.push(`lock package version ${lock.package?.version} does not match ${packageJson.version}`)
617
615
  if (lock && lock.contracts?.boundaryPolicy !== BOUNDARY_POLICY_SCHEMA) errors.push(`lock boundary policy contract must be ${BOUNDARY_POLICY_SCHEMA}`)
618
616
  const loadedPolicy = loadPolicyForLock(project)
619
- if (lock?.boundaryPolicy?.digest && loadedPolicy.digest && lock.boundaryPolicy.digest !== loadedPolicy.digest) {
617
+ if (lock && (lock.boundaryPolicy?.digest ?? null) !== loadedPolicy.digest) {
620
618
  errors.push('boundary policy digest has changed; run upgrade --dry-run before applying')
621
619
  }
622
620
  if (lock) {
@@ -655,24 +653,62 @@ export function checkAtelierLock(project) {
655
653
  export function runLockCommand(argv = process.argv.slice(2)) {
656
654
  const args = parseArgs(argv)
657
655
  const subcommand = args._[0] || 'check'
656
+ if (subcommand === 'provenance') {
657
+ const report = inspectPackageProvenance(packageRoot)
658
+ console.log(JSON.stringify(report, null, 2))
659
+ process.exitCode = args['exact-source-required'] && !report.sourceVerified ? 1 : 0
660
+ return
661
+ }
658
662
  const project = commandProject({ argv })
659
663
  if (subcommand === 'write') {
660
- const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) || 'existing-workspace' })
664
+ const lock = writeAtelierLock({ project, templateId: firstString(args.template, args['template-id'], project.config?.template?.id) || undefined })
661
665
  console.log(JSON.stringify({ ok: true, path: lockPathForProject(project), lock }, null, 2))
662
- process.exit(0)
666
+ process.exitCode = 0
667
+ return
663
668
  }
664
669
  if (subcommand === 'check') {
665
670
  const report = checkAtelierLock(project)
671
+ if (args['exact-source-required']) {
672
+ report.provenance = inspectPackageProvenance(packageRoot)
673
+ if (!report.provenance.sourceVerified) { report.ok = false; report.errors.push('installed package source is unverified') }
674
+ }
666
675
  console.log(JSON.stringify(report, null, 2))
667
- process.exit(report.ok ? 0 : 1)
676
+ process.exitCode = report.ok ? 0 : 1
677
+ return
668
678
  }
669
679
  console.error(`Unknown lock command: ${subcommand}`)
670
- process.exit(1)
680
+ process.exitCode = 1
671
681
  }
672
682
 
673
683
  export function runUpgradeCommand(argv = process.argv.slice(2)) {
674
684
  const args = parseArgs(argv)
675
- const project = commandProject({ argv })
685
+ const project = commandProject({ argv, writeLocalState: args._[0] !== 'explain' })
686
+ if (['plan', 'apply', 'status', 'recover', 'explain'].includes(args._[0])) {
687
+ try {
688
+ let result
689
+ if (args._[0] === 'explain') {
690
+ if (!firstString(args.plan)) throw new Error('--plan is required')
691
+ if (args.format != null && !['json', 'markdown'].includes(args.format)) throw new Error('--format must be json or markdown')
692
+ result = explainSavedUpgrade({ project, planFile: args.plan })
693
+ console.log(args.format === 'markdown' ? renderUpgradeExplanation(result) : JSON.stringify(result, null, 2))
694
+ return
695
+ } else if (args._[0] === 'plan') {
696
+ if (args.save !== true) throw new Error('exact planning requires --save')
697
+ result = prepareUpgrade({ project })
698
+ } else if (args._[0] === 'apply') {
699
+ if (!firstString(args.plan)) throw new Error('--plan is required')
700
+ result = applySavedUpgrade({ project, planFile: args.plan, confirm: args.confirm })
701
+ } else if (args._[0] === 'status') {
702
+ result = upgradeOperationStatus({ project, operationId: args.operation })
703
+ } else {
704
+ if (args['dry-run'] !== true) throw new Error('recovery supports --dry-run only; mutation requires a separately reviewed recovery plan')
705
+ result = recoverUpgradeDryRun({ project, operationId: args.operation })
706
+ }
707
+ console.log(JSON.stringify(result, null, 2))
708
+ process.exitCode = result.ok === false ? 1 : 0
709
+ } catch (error) { console.error(error.message); process.exitCode = 1 }
710
+ return
711
+ }
676
712
  const confirmBreaking = asArray(args['confirm-breaking']).concat(firstString(args['confirm-breaking']) ? [String(args['confirm-breaking'])] : [])
677
713
  const allowDirtyGenerated = Boolean(args['allow-dirty-generated'])
678
714
  if (args.check) {
@@ -0,0 +1,21 @@
1
+ import { createVaultService } from './service.mjs'
2
+ import { vaultIdentity } from './identity.mjs'
3
+ import { r2PrivateStorage, vercelPrivateStorage } from './storage.mjs'
4
+ import { d1VaultMetadata, postgresVaultMetadata } from './metadata.mjs'
5
+
6
+ /** Vercel host passes its established session verifier, credential store and SDKs. */
7
+ export function createVercelVault({ blob, postgres, verifySession, lookupCredential, privacy, deployment }) {
8
+ return createVaultService({ privacy, deployment, storage: vercelPrivateStorage(blob), metadata: postgresVaultMetadata(postgres), identity: vaultIdentity({ verifySession, lookupCredential, privacy, deployment }) })
9
+ }
10
+ /** Create a handler per request so D1 sessions cannot carry stale bookmarks. */
11
+ export function createCloudflareVault({ verifySession, lookupCredential, privacy, deployment }) {
12
+ vaultIdentity({ verifySession, lookupCredential })
13
+ async function handle(request, env) {
14
+ try {
15
+ const db = env.VAULT_DB.withSession('first-primary')
16
+ const identity = vaultIdentity({ verifySession: req => verifySession(req, env), lookupCredential: hash => lookupCredential(hash, env, db) })
17
+ return await createVaultService({ privacy, deployment, storage: r2PrivateStorage(env.VAULT_OBJECTS), metadata: d1VaultMetadata(db), identity })(request)
18
+ } catch { return new Response(null, { status: 503, headers: { "Cache-Control": "private, no-store", "X-Content-Type-Options": "nosniff" } }) }
19
+ }
20
+ return { fetch: handle }
21
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * The host verifies browser sessions using its established identity provider.
3
+ * Machine credentials are high-entropy secrets whose SHA-256 hashes are stored
4
+ * by the host. Lookup must read authoritative, current expiry/revocation state.
5
+ */
6
+ import { digest } from './service.mjs'
7
+ export function vaultIdentity({ verifySession, lookupCredential, now = () => Date.now() }) {
8
+ if (typeof verifySession !== 'function' || typeof lookupCredential !== 'function') throw new TypeError('Identity bindings required')
9
+ return {
10
+ read: request => verifySession(request),
11
+ async publish(request, vault) {
12
+ const value = request.headers.get('authorization')
13
+ if (!value || !/^Bearer [A-Za-z0-9_-]{43,128}$/.test(value)) return null
14
+ const record = await lookupCredential(await digest(new TextEncoder().encode(value.slice(7))))
15
+ if (!record || record.vault !== vault || record.revoked !== false || !Number.isFinite(record.expiresAt) || record.expiresAt <= now()) return null
16
+ return record.owner
17
+ },
18
+ }
19
+ }
@@ -0,0 +1,7 @@
1
+ export { createVaultService, preparePublication } from './service.mjs'
2
+ export { vaultIdentity } from './identity.mjs'
3
+ export { r2PrivateStorage, vercelPrivateStorage } from './storage.mjs'
4
+ export { VAULT_TABLE_SQL, d1VaultMetadata, postgresVaultMetadata } from './metadata.mjs'
5
+ export { createVercelVault, createCloudflareVault } from './hosts.mjs'
6
+ export { assessVaultPrivacy, createVaultPrivacyState, createVaultContext, VaultAlarmPersistenceError } from './privacy.mjs'
7
+ export { createVaultPrivacyProbe } from './probe.mjs'
@@ -0,0 +1,11 @@
1
+ const escape = value => String(value).replace(/[&<>"']/g, c => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]))
2
+ export function renderVaultHome({ vault, revision, manifest, privacy, query = '' }) {
3
+ const files = manifest.filter(file => /\.(html|pdf|txt)$/.test(file.path))
4
+ const matches = files.filter(file => file.path.toLowerCase().includes(query.toLowerCase()))
5
+ const verified = privacy.status === 'verified'
6
+ const label = verified ? 'Unauthorized-access checks passed' : privacy.status === 'exposed' ? 'Privacy failure — external routes may remain exposed' : 'Privacy not verified — access blocked'
7
+ const list = verified ? matches.map(file => `<li><a href="/${escape(vault)}/${file.path.split('/').map(encodeURIComponent).join('/')}">${escape(file.path)}</a><span>${escape(file.type.split(';')[0])}</span></li>`).join('') : ''
8
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escape(vault)} · Atelier</title><style>
9
+ *{box-sizing:border-box}body{margin:0;background:#f6f5ef;color:#20382d;font:17px/1.6 system-ui,sans-serif}main{max-width:960px;margin:auto;padding:clamp(20px,5vw,64px)}header{border-bottom:1px solid #cad3c7;padding-bottom:24px}h1{font-size:clamp(30px,6vw,52px);line-height:1.15;overflow-wrap:anywhere}a{color:inherit;text-underline-offset:4px}a:focus-visible,input:focus-visible,button:focus-visible{outline:3px solid #28734e;outline-offset:4px}.status{padding:16px;border:1px solid #738975;border-radius:8px;margin:24px 0}.blocked{background:#fff0dc;border-color:#936125}label{display:block;font-weight:600}form{display:flex;gap:12px;flex-wrap:wrap}input{flex:1;min-width:0;font:inherit;padding:12px;border:1px solid #738975;border-radius:6px}button{font:inherit;padding:12px 20px;background:#20382d;color:white;border:0;border-radius:6px}ul{list-style:none;padding:0}li{padding:20px 0;border-bottom:1px solid #cad3c7}li a{display:block;overflow-wrap:anywhere}li span,footer{font-size:14px;color:#506354}footer{margin-top:40px}.skip{position:absolute;left:-10000px}.skip:focus{left:16px;top:8px}</style></head><body><a class="skip" href="#artifacts">Skip to artifacts</a><main><header><p>ATELIER · PRIVATE VAULT</p><h1>${escape(vault)}</h1><p>Published revision ${escape(revision)} · ${files.length} artifact${files.length === 1 ? '' : 's'}</p></header><section class="status ${verified ? '' : 'blocked'}" aria-label="Privacy status"><strong>${label}</strong><p>${escape(privacy.reason)}</p>${verified ? `<small>Checked ${escape(new Date(privacy.checkedAt).toISOString())}. Authorization and evidence freshness are checked when you open an artifact.</small>` : '<p>Ask the workspace operator to verify destination protection, then reload. Artifacts are unavailable until the checks pass.</p>'}</section><label for="search">Find an artifact</label><form role="search" method="get" action="/${escape(vault)}/"><input id="search" name="q" type="search" value="${escape(query)}" maxlength="200"><button type="submit">Search</button></form>${verified ? `<ul id="artifacts" tabindex="-1" aria-label="Artifacts">${list}</ul>${matches.length ? '' : `<p>${files.length ? 'No artifacts match your search.' : 'No artifacts have been published yet.'}</p>`}` : ''}<footer>This website displays published artifacts. Saving or reviewing source material does not publish it. Generated artifacts run without script privileges.</footer></main></body></html>`
10
+ }
11
+ export const vaultHomePolicy = "default-src 'none'; style-src 'unsafe-inline'; form-action 'self'; base-uri 'none'; frame-ancestors 'none'"
@@ -0,0 +1,31 @@
1
+ /** SQL adapters require pre-provisioned vaults; neither endpoint creates owners. */
2
+ export const VAULT_TABLE_SQL = `CREATE TABLE atelier_vaults (
3
+ id TEXT PRIMARY KEY,
4
+ owner_issuer TEXT NOT NULL,
5
+ owner_subject TEXT NOT NULL,
6
+ revision INTEGER NOT NULL DEFAULT 0,
7
+ manifest TEXT NOT NULL DEFAULT '[]',
8
+ publication TEXT
9
+ )`
10
+ function record(row) {
11
+ return row ? { owner: { issuer: row.owner_issuer, subject: row.owner_subject }, revision: Number(row.revision), manifest: JSON.parse(row.manifest), publication: row.publication } : null
12
+ }
13
+ export function d1VaultMetadata(db) {
14
+ return {
15
+ async get(vault) { return record(await db.prepare('SELECT * FROM atelier_vaults WHERE id = ?').bind(vault).first()) },
16
+ async commit(vault, update) {
17
+ const result = await db.prepare('UPDATE atelier_vaults SET revision = revision + 1, manifest = ?, publication = ? WHERE id = ? AND revision = ? AND owner_issuer = ? AND owner_subject = ?')
18
+ .bind(JSON.stringify(update.manifest), update.publication, vault, update.expectedRevision, update.owner.issuer, update.owner.subject).run()
19
+ return result.success === true && result.meta.changes === 1
20
+ },
21
+ }
22
+ }
23
+ export function postgresVaultMetadata(db) {
24
+ return {
25
+ async get(vault) { return record((await db.query('SELECT * FROM atelier_vaults WHERE id = $1', [vault])).rows[0]) },
26
+ async commit(vault, update) {
27
+ const result = await db.query('UPDATE atelier_vaults SET revision = revision + 1, manifest = $1, publication = $2 WHERE id = $3 AND revision = $4 AND owner_issuer = $5 AND owner_subject = $6', [JSON.stringify(update.manifest), update.publication, vault, update.expectedRevision, update.owner.issuer, update.owner.subject])
28
+ return result.rowCount === 1
29
+ },
30
+ }
31
+ }
@@ -0,0 +1,148 @@
1
+ /** Trusted host evidence only: never accept evidence from a publisher. */
2
+ const unknown = reason => ({ status: 'unknown', reason })
3
+ export function vaultObjects(vault, manifest) {
4
+ return [...new Map(manifest.map(file => [`${vault}/${file.sha256}`, { key: `${vault}/${file.sha256}`, sha256: file.sha256, size: file.size }])).values()]
5
+ }
6
+ export function validDeployment(deployment) {
7
+ if (!deployment || typeof deployment.id !== 'string' || !deployment.id || !Array.isArray(deployment.origins) || !deployment.origins.length || deployment.origins.length > 16) return false
8
+ return new Set(deployment.origins).size === deployment.origins.length && deployment.origins.every(origin => {
9
+ try { const u = new URL(origin); return u.protocol === 'https:' && u.origin === origin && !u.username && !u.password } catch { return false }
10
+ })
11
+ }
12
+ export function assessVaultPrivacy(evidence, context) {
13
+ const { vault, publication, revision, phase, owner, deployment, manifest = [], objects = [], request, now = Date.now() } = context
14
+ // Exposure is a sticky alarm, not authorization. Even stale/mismatched trusted
15
+ // evidence must not turn a known exposure into an ordinary availability error.
16
+ if (evidence?.targets?.some?.(t => t?.anonymous === 'content' || t?.otherUser === 'content')) return { status: 'exposed', reason: 'Unauthorized content observed. This handler refuses delivery; external routes may remain exposed.' }
17
+ if (!evidence || evidence.vault !== vault || evidence.publication !== publication) return unknown('Protection evidence does not match this publication.')
18
+ if (!Number.isSafeInteger(revision) || revision < 0 || evidence.revision !== revision) return unknown('Protection evidence does not match the metadata revision.')
19
+ if (evidence.phase !== phase || !['before-upload', 'before-activation', 'read'].includes(phase) || !owner?.issuer || !owner?.subject || evidence.owner?.issuer !== owner.issuer || evidence.owner?.subject !== owner.subject) return unknown('Protection evidence does not match the owner and operation.')
20
+ if (!validDeployment(deployment) || evidence.deployment?.id !== deployment.id || JSON.stringify(evidence.deployment?.origins) !== JSON.stringify(deployment.origins)) return unknown('Protection evidence does not match this deployment.')
21
+ if (request && (!deployment.origins.includes(request.origin) || (phase === 'read' && request.path !== `/${vault}` && request.path !== `/${vault}/` && !manifest.some(f => request.path === `/${vault}/${f.path}`)))) return unknown('Request is outside the verified inventory.')
22
+ if (!Number.isFinite(evidence.checkedAt) || !Number.isFinite(evidence.validUntil) || evidence.checkedAt > now || evidence.validUntil <= now || evidence.validUntil <= evidence.checkedAt || evidence.validUntil - evidence.checkedAt > 300000) return unknown('Protection evidence is missing or expired.')
23
+ if (typeof evidence.policyRevision !== 'string' || !evidence.policyRevision) return unknown('Provider policy identity is missing.')
24
+ const config = evidence.configuration
25
+ if (config?.ownerOnly !== true || config?.privateStorage !== true || config?.completeInventory !== true) return unknown('Provider protection or route inventory is unverified.')
26
+ if (!Array.isArray(evidence.targets) || !evidence.targets.length || evidence.targets.length > 2000) return unknown('Access checks are missing or oversized.')
27
+ const urls = new Set(), routes = new Set(), keys = new Set()
28
+ for (const target of evidence.targets) {
29
+ let url
30
+ try { url = new URL(target.url) } catch { return unknown('Access check URL is invalid.') }
31
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash || urls.has(url.href)) return unknown('Access check inventory is invalid.')
32
+ urls.add(url.href)
33
+ if (target.anonymous !== 'denied' || target.otherUser !== 'denied') return unknown('Required access checks did not complete successfully.')
34
+ if (target.kind === 'storage') {
35
+ if (!objects.some(o => o.key === target.key && o.sha256 === target.sha256 && o.size === target.size)) return unknown('Storage check does not match a staged object.')
36
+ keys.add(target.key)
37
+ } else if (target.kind === 'route') {
38
+ if (!deployment.origins.includes(url.origin)) return unknown('Access check uses an unconfigured origin.')
39
+ const file = manifest.find(f => url.pathname === `/${vault}/${f.path}`)
40
+ const home = url.pathname === `/${vault}` || url.pathname === `/${vault}/`
41
+ if (!home && (!file || file.sha256 !== target.sha256)) return unknown('Access check does not match a manifest path and digest.')
42
+ routes.add(url.href)
43
+ } else return unknown('Access check target is invalid.')
44
+ }
45
+ for (const origin of deployment.origins) {
46
+ for (const path of [`/${vault}`, `/${vault}/`, ...manifest.map(f => `/${vault}/${f.path}`)]) if (!routes.has(origin + path)) return unknown('A configured route is missing from access checks.')
47
+ }
48
+ for (const object of objects) if (!keys.has(object.key)) return unknown('A staged storage object is missing from access checks.')
49
+ return { status: 'verified', reason: 'Configured routes refused unauthorized access at the recorded check time. Owner rendering is qualified separately.', checkedAt: evidence.checkedAt, validUntil: evidence.validUntil, policyRevision: evidence.policyRevision }
50
+ }
51
+ export async function verifyVaultPrivacy(privacy, context) {
52
+ const method = context.phase === 'read' ? 'current' : 'verify'
53
+ if (typeof privacy?.[method] !== 'function') return unknown('No provider privacy evidence is configured.')
54
+ try { return assessVaultPrivacy(await privacy[method](context), context) }
55
+ catch (error) {
56
+ if (error instanceof VaultAlarmPersistenceError) return { ...assessVaultPrivacy(error.evidence, context), persistence: 'unconfirmed' }
57
+ return unknown('Provider privacy verification is unavailable.')
58
+ }
59
+ }
60
+
61
+ /** The host must alert/retry this failure; the local read latch remains set. */
62
+ export class VaultAlarmPersistenceError extends Error {
63
+ constructor(evidence, cause) {
64
+ super('Vault exposure is latched locally but durable persistence is unconfirmed.', { cause })
65
+ this.name = 'VaultAlarmPersistenceError'
66
+ this.code = 'VAULT_ALARM_NOT_PERSISTED'
67
+ this.evidence = evidence
68
+ }
69
+ }
70
+
71
+ /** Public builder keeps refresh contexts aligned with service storage keys. */
72
+ export function createVaultContext({ vault, record, deployment, request, phase = 'read' }) {
73
+ if (!record || !Number.isSafeInteger(record.revision) || record.revision < 0 || !Array.isArray(record.manifest)) throw new TypeError('Authoritative revision and manifest required')
74
+ return { vault, owner: record.owner, publication: record.publication ?? null, revision: record.revision, manifest: record.manifest, objects: vaultObjects(vault, record.manifest), deployment, ...(request ? { request } : {}), phase }
75
+ }
76
+
77
+ /** Host-owned versioned evidence store. Refresh is explicit; reads never probe. */
78
+ export function createVaultPrivacyState({ probe, load, compareAndSet }) {
79
+ if (typeof probe?.verify !== 'function' || typeof load !== 'function' || typeof compareAndSet !== 'function') throw new TypeError('Probe and versioned evidence store required')
80
+ const alarms = new Map()
81
+ const key = context => {
82
+ if (!Number.isSafeInteger(context?.revision) || context.revision < 0) throw new TypeError('Authoritative nonnegative metadata revision required')
83
+ return JSON.stringify([context.deployment?.id, context.vault])
84
+ }
85
+ const exposed = (evidence, context) => assessVaultPrivacy(evidence, context).status === 'exposed'
86
+ async function snapshot(id, context) {
87
+ const record = await load(id)
88
+ if (!record || !Number.isSafeInteger(record.version) || record.version < 0 || !Object.hasOwn(record, 'evidence')) throw new TypeError('Invalid evidence store snapshot')
89
+ if (exposed(record.evidence, context)) alarms.set(id, { evidence: record.evidence, persisted: true })
90
+ return record
91
+ }
92
+ async function persistAlarm(id, context) {
93
+ // Alarm writes may replace newer green evidence regardless of revision.
94
+ // Reload on conflict, never reuse a stale version or retry green evidence.
95
+ for (let attempt = 0; attempt < 3; attempt++) {
96
+ if (alarms.get(id).persisted) return alarms.get(id).evidence
97
+ try {
98
+ const record = await snapshot(id, context)
99
+ if (alarms.get(id).persisted) return alarms.get(id).evidence
100
+ const evidence = alarms.get(id).evidence
101
+ if (await compareAndSet(id, evidence, { expectedVersion: record.version }) === true) {
102
+ alarms.set(id, { evidence, persisted: true })
103
+ return evidence
104
+ }
105
+ } catch (cause) { throw new VaultAlarmPersistenceError(alarms.get(id).evidence, cause) }
106
+ }
107
+ throw new VaultAlarmPersistenceError(alarms.get(id).evidence, new Error('Alarm write conflict limit reached'))
108
+ }
109
+ async function collect(context) {
110
+ const id = key(context)
111
+ // Pending latches retry persistence without re-probing a possibly vanished
112
+ // or staged-only exposure. Confirmed durable alarms need no rewrite.
113
+ if (alarms.has(id)) return persistAlarm(id, context)
114
+ const previous = await snapshot(id, context)
115
+ if (alarms.has(id)) return persistAlarm(id, context)
116
+ if (previous.evidence?.revision > context.revision) return null
117
+ const evidence = await probe.verify(context)
118
+ if (exposed(evidence, context)) {
119
+ if (!alarms.has(id)) alarms.set(id, { evidence, persisted: false })
120
+ return persistAlarm(id, context)
121
+ }
122
+ if (alarms.has(id)) return persistAlarm(id, context)
123
+ if (context.phase !== 'read') {
124
+ // Another instance may have latched an incident during the slow probe.
125
+ // This is still a check, not a transaction with metadata activation.
126
+ const latest = await snapshot(id, context)
127
+ if (alarms.has(id)) return persistAlarm(id, context)
128
+ return latest.evidence?.revision > context.revision ? null : evidence
129
+ }
130
+ // Green writes retain their original version. Conflicts and store failures
131
+ // must never be interpreted as a successful refresh.
132
+ if (await compareAndSet(id, evidence, { expectedVersion: previous.version }) !== true) {
133
+ await snapshot(id, context)
134
+ return alarms.has(id) ? persistAlarm(id, context) : null
135
+ }
136
+ return alarms.has(id) ? persistAlarm(id, context) : evidence
137
+ }
138
+ return {
139
+ verify: collect,
140
+ async current(context) {
141
+ const id = key(context)
142
+ if (alarms.has(id)) return alarms.get(id).evidence
143
+ const record = await snapshot(id, context)
144
+ return alarms.get(id)?.evidence ?? record.evidence
145
+ },
146
+ async refresh(context) { return collect({ ...context, phase: 'read' }) },
147
+ }
148
+ }
@@ -0,0 +1,89 @@
1
+ import { digest } from './service.mjs'
2
+ const canonical = value => JSON.stringify(value, (_, item) => item && typeof item === 'object' && !Array.isArray(item) ? Object.fromEntries(Object.keys(item).sort().map(k => [k, item[k]])) : item)
3
+ const MAX_BYTES = 4 * 1024 * 1024
4
+ const cleanUrl = value => {
5
+ const url = new URL(value)
6
+ if (url.protocol !== 'https:' || url.username || url.password || url.search || url.hash) throw new Error('Unsafe probe URL')
7
+ return url.href
8
+ }
9
+ async function classify(response, target, loginUrls) {
10
+ if (!(response instanceof Response)) return 'unknown'
11
+ const reader = response.body?.getReader()
12
+ const chunks = []; let length = 0
13
+ try {
14
+ for (; reader;) {
15
+ const { value, done } = await reader.read()
16
+ if (done) break
17
+ length += value.byteLength
18
+ if (length > MAX_BYTES) { await reader.cancel(); return 'unknown' }
19
+ chunks.push(value)
20
+ }
21
+ } finally { reader?.releaseLock() }
22
+ const bytes = new Uint8Array(length); let offset = 0
23
+ for (const value of chunks) { bytes.set(value, offset); offset += value.length }
24
+ // Content exposure wins over misleading error or redirect status codes.
25
+ if (length > 0 && (await digest(bytes) === target.sha256 || (typeof target.canary === 'string' && target.canary.length >= 32 && new TextDecoder().decode(bytes).includes(target.canary)))) return 'content'
26
+ if (response.redirected) return 'unknown'
27
+ if ([401, 403, 404].includes(response.status)) return length === 0 ? 'denied' : 'unknown'
28
+ if ([302, 303, 307, 308].includes(response.status)) {
29
+ try {
30
+ const location = new URL(response.headers.get('location'), target.url)
31
+ if (location.protocol !== 'https:' || location.username || location.password || location.hash) return 'unknown'
32
+ const endpoint = loginUrls.find(item => item.url === location.origin + location.pathname)
33
+ return endpoint && [...location.searchParams.keys()].every(key => endpoint.queryKeys.includes(key)) ? 'denied' : 'unknown'
34
+ } catch { return 'unknown' }
35
+ }
36
+ return 'unknown'
37
+ }
38
+ /**
39
+ * inspect(context) is a trusted provider configuration/inventory collector.
40
+ * request({url,identity,redirect,signal}) is host transport; credentials never
41
+ * cross identities or redirects. No global fetch or ambient credentials here.
42
+ * Each collection re-inspects policy after probing to refuse changed settings.
43
+ */
44
+ export function createVaultPrivacyProbe({ inspect, request, now = () => Date.now(), timeoutMs = 60000, onExposure }) {
45
+ if (typeof inspect !== 'function' || typeof request !== 'function' || !Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 60000) throw new TypeError('Explicit probe bindings required')
46
+ return {
47
+ async verify(context) {
48
+ const controller = new AbortController()
49
+ let timer
50
+ const checkedAt = now()
51
+ const deadline = new Promise((_, reject) => { timer = setTimeout(() => { controller.abort(); reject(new Error('Probe deadline exceeded')) }, timeoutMs) })
52
+ const collect = async () => {
53
+ const inventory = structuredClone(await inspect(context, { signal: controller.signal }))
54
+ if (!inventory || !Array.isArray(inventory.targets) || !inventory.targets.length || inventory.targets.length > 2000) throw new Error('Invalid probe inventory')
55
+ const targets = inventory.targets.map(target => {
56
+ if (!target || !/^[a-f0-9]{64}$/.test(target.sha256)) throw new Error('Expected content digest required')
57
+ return { ...target, url: cleanUrl(target.url) }
58
+ })
59
+ const loginUrls = (inventory.loginEndpoints ?? []).map(item => {
60
+ if (!Array.isArray(item.queryKeys) || item.queryKeys.some(key => !['state', 'redirect_uri', 'redirect_url', 'returnTo', 'next', 'client_id', 'response_type', 'scope', 'nonce', 'code_challenge', 'code_challenge_method'].includes(key))) throw new Error('Unsafe login parameters')
61
+ return { url: cleanUrl(item.url), queryKeys: item.queryKeys }
62
+ })
63
+ const evidence = results => ({ vault: context.vault, publication: context.publication, revision: context.revision, phase: context.phase, owner: context.owner, deployment: context.deployment, checkedAt, validUntil: checkedAt + 60000, policyRevision: inventory.policyRevision, configuration: inventory.configuration, targets: results })
64
+ const results = []
65
+ for (const target of targets) {
66
+ const result = { url: target.url, kind: target.kind, sha256: target.sha256, key: target.key, size: target.size }
67
+ for (const identity of ['anonymous', 'otherUser']) {
68
+ if (controller.signal.aborted) throw new Error('Probe aborted')
69
+ try { result[identity] = await classify(await request({ url: target.url, identity, redirect: 'manual', signal: controller.signal }), target, loginUrls) }
70
+ catch { result[identity] = 'unknown' }
71
+ if (result[identity] === 'content') {
72
+ const alarm = evidence([...results, result])
73
+ // Notification cannot delay or downgrade the exposure verdict. The
74
+ // host owns durable incident persistence and provider containment.
75
+ if (typeof onExposure === 'function') Promise.resolve().then(() => onExposure(alarm)).catch(() => {})
76
+ return alarm
77
+ }
78
+ }
79
+ results.push(result)
80
+ }
81
+ const after = await inspect(context, { signal: controller.signal })
82
+ if (canonical(after) !== canonical(inventory)) throw new Error('Provider protection changed during verification')
83
+ return evidence(results)
84
+ }
85
+ try { return await Promise.race([collect(), deadline]) }
86
+ finally { clearTimeout(timer); controller.abort() }
87
+ },
88
+ }
89
+ }