@mnstry/atelier 0.2.0-alpha.1

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 (260) hide show
  1. package/CHANGELOG.md +383 -0
  2. package/LICENSE +202 -0
  3. package/NOTICE +5 -0
  4. package/README.md +262 -0
  5. package/SECURITY.md +93 -0
  6. package/TRADEMARKS.md +88 -0
  7. package/announcements/2026-08-06-announcements-channel.v1.json +12 -0
  8. package/announcements/keys/mnstry-announcements.public.v1.json +9 -0
  9. package/bin/atelier.mjs +2 -0
  10. package/bin/mnstry-atelier.mjs +3 -0
  11. package/contracts/analysis-adapter.v1.schema.json +248 -0
  12. package/contracts/atelier-action-intent.v1.schema.json +143 -0
  13. package/contracts/atelier-analysis-adapter.v1.schema.json +360 -0
  14. package/contracts/atelier-attestation.v1.schema.json +239 -0
  15. package/contracts/atelier-boundary-policy.v1.schema.json +339 -0
  16. package/contracts/atelier-claim.v1.schema.json +97 -0
  17. package/contracts/atelier-export.v1.schema.json +1015 -0
  18. package/contracts/atelier-extension-pack.v1.schema.json +290 -0
  19. package/contracts/atelier-kit-manifest.v1.schema.json +273 -0
  20. package/contracts/atelier-lock.v1.schema.json +364 -0
  21. package/contracts/atelier-migration.v1.schema.json +162 -0
  22. package/contracts/atelier-project-config.v1.schema.json +352 -0
  23. package/contracts/atelier-readiness-protocol.v1.schema.json +517 -0
  24. package/contracts/atelier-readiness-run.v1.schema.json +229 -0
  25. package/contracts/atelier-readiness.v1.schema.json +457 -0
  26. package/contracts/compat-baseline.json +10 -0
  27. package/contracts/git-promote-event.v1.schema.json +145 -0
  28. package/contracts/knowledge-source-sidecar.v1.schema.json +157 -0
  29. package/contracts/mnstry-atelier-semantic-profile.v1.json +254 -0
  30. package/docs/announcements.md +101 -0
  31. package/docs/atelier-runtime.md +34 -0
  32. package/docs/attestation.md +201 -0
  33. package/docs/continuity.md +55 -0
  34. package/docs/contract-stability.md +111 -0
  35. package/docs/distributions.md +220 -0
  36. package/docs/install.md +140 -0
  37. package/docs/knowledge-graph.md +140 -0
  38. package/docs/ontology.md +206 -0
  39. package/docs/release-engineering.md +151 -0
  40. package/docs/repo-boundary-guard.md +199 -0
  41. package/docs/tenant-readiness.md +64 -0
  42. package/docs/upgrade.md +124 -0
  43. package/fixtures/analysis/invalid/native-graph.v1.json +4 -0
  44. package/fixtures/analysis/invalid/promoted-claim.v1.json +17 -0
  45. package/fixtures/analysis/invalid/runtime-import.v1.json +20 -0
  46. package/fixtures/analysis/valid/output.claims.v1.json +17 -0
  47. package/fixtures/analysis-adapter/invalid/manifest-hidden-provider.v1.json +24 -0
  48. package/fixtures/analysis-adapter/invalid/output-frontmatter-mutation.v1.json +21 -0
  49. package/fixtures/analysis-adapter/invalid/output-native-graph.v1.json +14 -0
  50. package/fixtures/analysis-adapter/manifest.disabled.v1.json +22 -0
  51. package/fixtures/analysis-adapter/manifest.enabled-local.v1.json +23 -0
  52. package/fixtures/analysis-adapter/output.claims.v1.json +21 -0
  53. package/fixtures/atelier-action-intent/invalid/runtime-mutation-authority.v1.json +16 -0
  54. package/fixtures/atelier-action-intent/invalid/unknown-top-level-field.v1.json +17 -0
  55. package/fixtures/atelier-action-intent/invalid/writes-beyond-clipboard.v1.json +16 -0
  56. package/fixtures/atelier-action-intent/valid/copy-agent-prompt.v1.json +16 -0
  57. package/fixtures/atelier-action-intent/valid/copy-repo-path.v1.json +22 -0
  58. package/fixtures/atelier-analysis-adapter/invalid/hidden-model-provider.v1.json +24 -0
  59. package/fixtures/atelier-analysis-adapter/invalid/inert-with-execute-command.v1.json +24 -0
  60. package/fixtures/atelier-analysis-adapter/invalid/proposals-with-runtime-import.v1.json +30 -0
  61. package/fixtures/atelier-analysis-adapter/valid/manifest-inert.v1.json +24 -0
  62. package/fixtures/atelier-analysis-adapter/valid/proposals-claim-only.v1.json +27 -0
  63. package/fixtures/atelier-attestation/invalid/bad-digest.v1.json +23 -0
  64. package/fixtures/atelier-attestation/invalid/conformance-scope.v1.json +23 -0
  65. package/fixtures/atelier-attestation/invalid/missing-signature.v1.json +22 -0
  66. package/fixtures/atelier-attestation/invalid/unknown-root-field.v1.json +24 -0
  67. package/fixtures/atelier-attestation/keys/roundtrip-issuer.public.v1.json +9 -0
  68. package/fixtures/atelier-attestation/valid/rejected-with-reasons.v1.json +49 -0
  69. package/fixtures/atelier-attestation/valid/signed-admitted.v1.json +35 -0
  70. package/fixtures/atelier-attestation/valid/signed-roundtrip.v1.json +29 -0
  71. package/fixtures/atelier-attestation/valid/unsigned-advisory.v1.json +23 -0
  72. package/fixtures/atelier-claim/invalid/unknown-provider.v1.json +15 -0
  73. package/fixtures/atelier-claim/invalid/unknown-top-level-field.v1.json +16 -0
  74. package/fixtures/atelier-claim/invalid/wrong-schema-const.v1.json +15 -0
  75. package/fixtures/atelier-claim/valid/adapter-style-proposed-relation.v1.json +19 -0
  76. package/fixtures/atelier-claim/valid/codex-proposed-relation.v1.json +16 -0
  77. package/fixtures/atelier-claim/valid/manual-proposed-relation.v1.json +16 -0
  78. package/fixtures/atelier-claim/valid/readiness-proposed-relation.v1.json +18 -0
  79. package/fixtures/atelier-export/invalid/dirty-unforced-empty-taint.v1.json +19 -0
  80. package/fixtures/atelier-export/invalid/object-local-audience-visibility.v1.json +11 -0
  81. package/fixtures/atelier-export/invalid/open-object-class-owner.v1.json +23 -0
  82. package/fixtures/atelier-export/invalid/pricing-legacy-amount.v1.json +23 -0
  83. package/fixtures/atelier-export/invalid/public-disguised-internal-source-ref.v1.json +11 -0
  84. package/fixtures/atelier-export/invalid/public-nested-private-source-ref.v1.json +17 -0
  85. package/fixtures/atelier-export/invalid/public-resolved-private-source-ref.v1.json +13 -0
  86. package/fixtures/atelier-export/invalid/public-resolved-sensitive-source-ref.v1.json +13 -0
  87. package/fixtures/atelier-export/invalid/public-source-ref-missing-provenance.v1.json +20 -0
  88. package/fixtures/atelier-export/invalid/public-source-team-kg.json +12 -0
  89. package/fixtures/atelier-export/invalid/source-node-legacy-visibility.v1.json +21 -0
  90. package/fixtures/atelier-export/invalid/source-node-unknown-field.v1.json +18 -0
  91. package/fixtures/atelier-export/invalid/unresolved-public-source-ref.v1.json +11 -0
  92. package/fixtures/atelier-export/sample-studio-offer.v1.json +397 -0
  93. package/fixtures/atelier-extension-pack/invalid/bundled-slug-collision.v1.json +22 -0
  94. package/fixtures/atelier-extension-pack/invalid/escapes-root.v1.json +22 -0
  95. package/fixtures/atelier-extension-pack/invalid/posture-tampered.v1.json +22 -0
  96. package/fixtures/atelier-extension-pack/invalid/protocols/autonomous-authority.v1.json +118 -0
  97. package/fixtures/atelier-extension-pack/invalid/protocols/offer-map.v1.json +109 -0
  98. package/fixtures/atelier-extension-pack/invalid/redefines-runtime-term.v1.json +19 -0
  99. package/fixtures/atelier-extension-pack/invalid/wrong-namespace.v1.json +19 -0
  100. package/fixtures/atelier-extension-pack/valid/protocols/contract-gate.v1.json +109 -0
  101. package/fixtures/atelier-extension-pack/valid/sample-pack.v1.json +48 -0
  102. package/fixtures/atelier-kit-manifest/invalid/runtime-copy-without-canonical-role.v1.json +53 -0
  103. package/fixtures/atelier-kit-manifest/invalid/telemetry-send-path.v1.json +21 -0
  104. package/fixtures/atelier-kit-manifest/valid/mnstry-atelier.valid.v1.json +76 -0
  105. package/fixtures/atelier-lock/invalid/absolute-contract-path.v1.json +40 -0
  106. package/fixtures/atelier-lock/invalid/runtime-mutation-enabled.v1.json +41 -0
  107. package/fixtures/atelier-lock/invalid/unknown-top-level-field.v1.json +42 -0
  108. package/fixtures/atelier-lock/valid/extension-pack.v1.json +55 -0
  109. package/fixtures/atelier-lock/valid/minimal.v1.json +44 -0
  110. package/fixtures/atelier-migration/invalid/live-apply-mode.v1.json +21 -0
  111. package/fixtures/atelier-migration/invalid/missing-review-evidence.v1.json +20 -0
  112. package/fixtures/atelier-migration/invalid/runtime-mutation-enabled.v1.json +21 -0
  113. package/fixtures/atelier-migration/valid/minimal.v1.json +32 -0
  114. package/fixtures/atelier-project-config/invalid/absolute-repo-path.v1.json +11 -0
  115. package/fixtures/atelier-project-config/invalid/ext-not-object.v1.json +12 -0
  116. package/fixtures/atelier-project-config/invalid/external-kind-read-boundary.v1.json +12 -0
  117. package/fixtures/atelier-project-config/invalid/missing-repos-and-alignment.v1.json +7 -0
  118. package/fixtures/atelier-project-config/invalid/unknown-top-level-field.v1.json +14 -0
  119. package/fixtures/atelier-project-config/valid/alignment-scaffold.v1.json +15 -0
  120. package/fixtures/atelier-project-config/valid/ext-extension-packs.v1.json +36 -0
  121. package/fixtures/atelier-project-config/valid/repo-workspace.v1.json +23 -0
  122. package/fixtures/atelier-project-config/validator-only/ext-member-not-object.v1.json +14 -0
  123. package/fixtures/atelier-readiness/invalid/dry-run-only-false.v1.json +87 -0
  124. package/fixtures/atelier-readiness/invalid/support-bundle-send-path.v1.json +87 -0
  125. package/fixtures/atelier-readiness/invalid/unknown-top-level-field.v1.json +88 -0
  126. package/fixtures/atelier-readiness/valid/ready-workspace.v1.json +84 -0
  127. package/fixtures/boundary-policy/invalid/content-rule-exception-blanket-path.v1.json +67 -0
  128. package/fixtures/boundary-policy/invalid/invalid-actor-private-repo-mapping.v1.json +20 -0
  129. package/fixtures/boundary-policy/invalid/legacy-warning-missing-warning.v1.json +20 -0
  130. package/fixtures/boundary-policy/invalid/missing-repo-coverage.v1.json +19 -0
  131. package/fixtures/boundary-policy/invalid/unknown-key.v1.json +12 -0
  132. package/fixtures/boundary-policy/valid/content-rule-exception.v1.json +67 -0
  133. package/fixtures/boundary-policy/valid/legacy-warning.v1.json +33 -0
  134. package/fixtures/boundary-policy/valid/strict-private-shared.v1.json +40 -0
  135. package/fixtures/git-promote/invalid/provision-named-revocable.v1.json +29 -0
  136. package/fixtures/git-promote/private-to-team.v1.json +29 -0
  137. package/fixtures/knowledge-source-sidecar/invalid/legacy-visibility-field.v1.json +17 -0
  138. package/fixtures/knowledge-source-sidecar/invalid/missing-audience.v1.json +15 -0
  139. package/fixtures/knowledge-source-sidecar/invalid/unknown-relation-kind.v1.json +18 -0
  140. package/fixtures/knowledge-source-sidecar/valid/html-source.v1.json +18 -0
  141. package/fixtures/knowledge-source-sidecar/valid/pdf-evidence.v1.json +18 -0
  142. package/fixtures/projects/neutral-project/atelier.project.json +15 -0
  143. package/fixtures/projects/neutral-project/ops/config/repo-access.v1.json +12 -0
  144. package/fixtures/projects/neutral-project/project-knowledge.graph.json +69 -0
  145. package/fixtures/projects/sample-workspace/atelier.project.json +23 -0
  146. package/fixtures/projects/sample-workspace/boundary-policy.v1.json +59 -0
  147. package/fixtures/projects/sample-workspace/content/README.md +16 -0
  148. package/fixtures/projects/sample-workspace/content/source.html +5 -0
  149. package/fixtures/projects/sample-workspace/content/source.html.kg.json +15 -0
  150. package/fixtures/projects/sample-workspace/repo-access.v1.json +9 -0
  151. package/fixtures/projects/source-formats-workspace/atelier.project.json +23 -0
  152. package/fixtures/projects/source-formats-workspace/content/README.md +17 -0
  153. package/fixtures/projects/source-formats-workspace/content/data.json +7 -0
  154. package/fixtures/projects/source-formats-workspace/content/data.json.kg.json +15 -0
  155. package/fixtures/projects/source-formats-workspace/content/logo.png.kg.json +15 -0
  156. package/fixtures/projects/source-formats-workspace/content/metrics.csv +3 -0
  157. package/fixtures/projects/source-formats-workspace/content/metrics.csv.kg.json +15 -0
  158. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml +4 -0
  159. package/fixtures/projects/source-formats-workspace/content/pipeline.yaml.kg.json +15 -0
  160. package/fixtures/projects/source-formats-workspace/repo-access.v1.json +9 -0
  161. package/fixtures/readiness-protocols/extension-pack/invalid/protocol-missing-path.v1.json +20 -0
  162. package/fixtures/readiness-protocols/extension-pack/valid/with-protocol.v1.json +24 -0
  163. package/fixtures/readiness-protocols/protocol/invalid/missing-check-target.v1.json +103 -0
  164. package/fixtures/readiness-protocols/protocol/invalid/runtime-mutation-enabled.v1.json +107 -0
  165. package/fixtures/readiness-protocols/protocol/valid/minimal.v1.json +100 -0
  166. package/fixtures/readiness-protocols/run/invalid/missing-evidence.v1.json +37 -0
  167. package/fixtures/readiness-protocols/run/invalid/pass-with-blocker.v1.json +22 -0
  168. package/fixtures/readiness-protocols/run/invalid/runtime-mutation-enabled.v1.json +22 -0
  169. package/fixtures/readiness-protocols/run/valid/pass.v1.json +44 -0
  170. package/fixtures/support/invalid/leaky-support.v1.json +13 -0
  171. package/fixtures/support/valid/minimal-safe.v1.json +20 -0
  172. package/package.json +98 -0
  173. package/skills/claude/atelier-open/SKILL.md +68 -0
  174. package/skills/claude/mnstry-readiness/SKILL.md +41 -0
  175. package/skills/codex/atelier-open/SKILL.md +68 -0
  176. package/skills/codex/mnstry-readiness/SKILL.md +41 -0
  177. package/src/analysis/adapter.mjs +20 -0
  178. package/src/analysis/analysis-claim-contract.mjs +335 -0
  179. package/src/attestation/jcs.mjs +64 -0
  180. package/src/attestation/sign.mjs +305 -0
  181. package/src/boundary/content-rules.mjs +262 -0
  182. package/src/boundary/policy.mjs +806 -0
  183. package/src/check-atelier-export-contract.mjs +116 -0
  184. package/src/cli/run.mjs +283 -0
  185. package/src/collaboration/proposals.mjs +262 -0
  186. package/src/commands/analysis.mjs +8 -0
  187. package/src/commands/announcements.mjs +308 -0
  188. package/src/commands/attestation.mjs +198 -0
  189. package/src/commands/boundary.mjs +25 -0
  190. package/src/commands/config.mjs +4 -0
  191. package/src/commands/context.mjs +3 -0
  192. package/src/commands/distribution.mjs +224 -0
  193. package/src/commands/egress.mjs +11 -0
  194. package/src/commands/extension-pack.mjs +209 -0
  195. package/src/commands/feedback.mjs +3 -0
  196. package/src/commands/graph.mjs +3 -0
  197. package/src/commands/init.mjs +130 -0
  198. package/src/commands/lock.mjs +3 -0
  199. package/src/commands/project.mjs +3 -0
  200. package/src/commands/promote.mjs +4 -0
  201. package/src/commands/readiness.mjs +3 -0
  202. package/src/commands/server.mjs +6 -0
  203. package/src/commands/setup.mjs +250 -0
  204. package/src/commands/support.mjs +3 -0
  205. package/src/commands/upgrade.mjs +3 -0
  206. package/src/contracts/corpus.mjs +171 -0
  207. package/src/egress/check.mjs +45 -0
  208. package/src/egress/forbidden-egress.mjs +323 -0
  209. package/src/export/atelier-export-contract.mjs +722 -0
  210. package/src/extension-packs/loader.mjs +491 -0
  211. package/src/graph/graph.mjs +370 -0
  212. package/src/graph/knowledge-graph.mjs +887 -0
  213. package/src/harness/context-client.mjs +144 -0
  214. package/src/harness/context.mjs +27 -0
  215. package/src/index.mjs +88 -0
  216. package/src/project/config.mjs +557 -0
  217. package/src/project/file-class.mjs +113 -0
  218. package/src/project/git-ignore.mjs +31 -0
  219. package/src/project/path-match.mjs +30 -0
  220. package/src/project/repo-identity.mjs +176 -0
  221. package/src/projection/alignment-projection.mjs +169 -0
  222. package/src/projection/policy.mjs +90 -0
  223. package/src/projection/project.mjs +152 -0
  224. package/src/readiness/readiness.mjs +312 -0
  225. package/src/readiness-protocols/bundled-pack.mjs +766 -0
  226. package/src/readiness-protocols/runtime.mjs +419 -0
  227. package/src/server/local-sidecar.mjs +640 -0
  228. package/src/server/security.mjs +229 -0
  229. package/src/server/server.mjs +42 -0
  230. package/src/support/feedback-report.mjs +414 -0
  231. package/src/support/support-bundle.mjs +226 -0
  232. package/src/ui/html-primitives.mjs +104 -0
  233. package/src/upgrade/upgrade.mjs +701 -0
  234. package/src/validate-atelier-export-dry-run.mjs +75 -0
  235. package/templates/atelier.project.example.json +23 -0
  236. package/templates/distribution-workspace/.claude/launch.json +12 -0
  237. package/templates/distribution-workspace/README.md +86 -0
  238. package/templates/distribution-workspace/atelier.project.json +36 -0
  239. package/templates/distribution-workspace/boundary-policy.v1.json +61 -0
  240. package/templates/distribution-workspace/gitignore +29 -0
  241. package/templates/distribution-workspace/governance/repo-boundary-ledger.md +5 -0
  242. package/templates/distribution-workspace/project/README.md +19 -0
  243. package/templates/distribution-workspace/repo-access.v1.json +9 -0
  244. package/templates/launch.json +12 -0
  245. package/templates/private-domain-workspace/.claude/launch.json +12 -0
  246. package/templates/private-domain-workspace/README.md +50 -0
  247. package/templates/private-domain-workspace/atelier.project.json +28 -0
  248. package/templates/private-domain-workspace/boundary-policy.v1.json +54 -0
  249. package/templates/private-domain-workspace/domain/README.md +19 -0
  250. package/templates/private-domain-workspace/gitignore +29 -0
  251. package/templates/private-domain-workspace/governance/repo-boundary-ledger.md +4 -0
  252. package/templates/private-domain-workspace/repo-access.v1.json +9 -0
  253. package/templates/shared-project-workspace/.claude/launch.json +12 -0
  254. package/templates/shared-project-workspace/README.md +51 -0
  255. package/templates/shared-project-workspace/atelier.project.json +28 -0
  256. package/templates/shared-project-workspace/boundary-policy.v1.json +61 -0
  257. package/templates/shared-project-workspace/gitignore +29 -0
  258. package/templates/shared-project-workspace/governance/repo-boundary-ledger.md +5 -0
  259. package/templates/shared-project-workspace/project/README.md +19 -0
  260. package/templates/shared-project-workspace/repo-access.v1.json +9 -0
@@ -0,0 +1,308 @@
1
+ #!/usr/bin/env node
2
+ // atelier announcements list|verify|show
3
+ //
4
+ // Pull-only inbound channel: MNSTRY announcements are signed JSON documents
5
+ // committed under announcements/ in this repository. Users receive them by
6
+ // running `git pull` — pulling is the consent act — and verify them against
7
+ // the committed MNSTRY announcements public key. This command never fetches
8
+ // anything: it only reads local files. No network access anywhere.
9
+ //
10
+ // The announcement shape (mnstry.announcement@v1) is runtime-defined, not a
11
+ // contracts/ schema: it is validated structurally here in code so the
12
+ // governed contract corpus stays stable.
13
+ //
14
+ // Self-contained subcommand parser (no shared CLI plumbing) so the module can
15
+ // be invoked directly: node src/commands/announcements.mjs <subcommand> ...
16
+ //
17
+ // Trust anchor rule: the public key is ALWAYS the package-committed key
18
+ // (DEFAULT_PUBLIC_KEY) unless the operator explicitly passes --public-key.
19
+ // --dir relocates only where announcement documents are read from; it never
20
+ // relocates the key. A key loaded from inside the tree being verified is not
21
+ // a trust anchor at all — anyone who can write that directory could mint an
22
+ // attacker keypair under the genuine keyId and have forgeries list as
23
+ // verified. Every list run prints the key it verified against for exactly
24
+ // this reason.
25
+ //
26
+ // Exit codes: 0 success (all documents verified), 1 one or more documents
27
+ // failed verification, 2 usage or input error. Log-safety contract: output
28
+ // may name keyId, algorithm, package-relative file paths, and — only after a
29
+ // signature verifies — announcement content. An unverified document's body is
30
+ // never printed, and a key path outside the package is named by basename only
31
+ // so machine layout never reaches a log.
32
+
33
+ import fs from 'node:fs'
34
+ import path from 'node:path'
35
+ import { fileURLToPath } from 'node:url'
36
+ import { safeLabel, verifyDocument } from '../attestation/sign.mjs'
37
+
38
+ const PACKAGE_ROOT = process.env.MNSTRY_ATELIER_PACKAGE_ROOT
39
+ ?? fileURLToPath(new URL('../..', import.meta.url))
40
+ const DEFAULT_DIR = path.join(PACKAGE_ROOT, 'announcements')
41
+ const DEFAULT_PUBLIC_KEY = path.join(DEFAULT_DIR, 'keys', 'mnstry-announcements.public.v1.json')
42
+ const ANNOUNCEMENT_SCHEMA = 'mnstry.announcement@v1'
43
+
44
+ const USAGE = `Usage: atelier announcements <subcommand>
45
+
46
+ Subcommands:
47
+ list [--dir DIR] [--public-key FILE]
48
+ List every announcement in the announcements directory (default: the
49
+ package's announcements/), verifying each against the committed MNSTRY
50
+ announcements key. --dir changes only which documents are read; the
51
+ trust anchor stays the committed key unless you pass --public-key.
52
+ Every run prints the key path and keyId it verified against.
53
+ Documents that do not verify are flagged loudly and fail the exit code.
54
+
55
+ verify <file> [--public-key FILE] [--json]
56
+ Verify one announcement document. --public-key defaults to the
57
+ committed MNSTRY announcements key. --json prints { valid, reasons }.
58
+
59
+ show <file> [--public-key FILE]
60
+ Print an announcement's title and body — only if its signature
61
+ verifies. Refuses (exit 1) otherwise; an unverified body is never shown.
62
+
63
+ This channel is pull-only: announcements arrive via git pull and nothing in
64
+ this kit ever contacts a network to look for them.
65
+
66
+ Exit codes: 0 all documents verified, 1 verification failed, 2 usage or
67
+ input error.`
68
+
69
+ function fail(message) {
70
+ console.error(message)
71
+ process.exit(2)
72
+ }
73
+
74
+ function parseOptions(argv, spec) {
75
+ const options = { _: [] }
76
+ for (let i = 0; i < argv.length; i += 1) {
77
+ const arg = argv[i]
78
+ if (arg === '--help' || arg === '-h') {
79
+ console.log(USAGE)
80
+ process.exit(0)
81
+ }
82
+ if (arg.startsWith('--')) {
83
+ const name = arg.slice(2)
84
+ if (!(name in spec)) fail(`unknown option --${name}\n\n${USAGE}`)
85
+ if (spec[name] === 'flag') {
86
+ options[name] = true
87
+ } else {
88
+ i += 1
89
+ if (argv[i] === undefined) fail(`--${name} requires a value`)
90
+ options[name] = argv[i]
91
+ }
92
+ } else {
93
+ options._.push(arg)
94
+ }
95
+ }
96
+ return options
97
+ }
98
+
99
+ function readJsonFile(file, label) {
100
+ let raw
101
+ try {
102
+ raw = fs.readFileSync(file, 'utf8')
103
+ } catch {
104
+ fail(`cannot read ${label} file: ${safeLabel(file, 120)}`)
105
+ }
106
+ try {
107
+ return JSON.parse(raw)
108
+ } catch {
109
+ fail(`${label} file is not valid JSON: ${file}`)
110
+ }
111
+ return null
112
+ }
113
+
114
+ function loadPublicKey(keyPath) {
115
+ const doc = readJsonFile(keyPath, 'public key')
116
+ if (!doc || typeof doc !== 'object' || !doc.publicKeyJwk) {
117
+ fail(`public key file has no publicKeyJwk member: ${keyPath}`)
118
+ }
119
+ return doc
120
+ }
121
+
122
+ // Name a key file for output. Inside the package it is named by its
123
+ // package-relative path (stable across machines and checkouts); anywhere else
124
+ // only the basename is printed, because an absolute path leaks the operator's
125
+ // machine layout into logs and transcripts.
126
+ function describeKeyPath(keyPath) {
127
+ const resolved = path.resolve(keyPath)
128
+ const relative = path.relative(PACKAGE_ROOT, resolved)
129
+ if (relative && !relative.startsWith('..') && !path.isAbsolute(relative)) {
130
+ return relative.split(path.sep).join('/')
131
+ }
132
+ return `<outside package>/${path.basename(resolved)}`
133
+ }
134
+
135
+ // The key-identity header. Printing which key produced a verdict is what
136
+ // makes a swapped trust anchor visible rather than silent, so it prints on
137
+ // every run — and says so out loud when the key is not the committed default.
138
+ function keyIdentityHeader(keyPath, keyDoc, { explicit }) {
139
+ const keyId = safeLabel(keyDoc.keyId)
140
+ const provenance = explicit
141
+ ? 'explicit --public-key, not the committed default key'
142
+ : 'committed default key'
143
+ // The path comes from operator argv, but a filename is an injection sink
144
+ // like any other: escapes can erase this very line, and an over-long
145
+ // basename wraps the header so the true provenance lands off-screen.
146
+ return `key: ${safeLabel(describeKeyPath(keyPath), 120)} (keyId ${keyId}, ${provenance})`
147
+ }
148
+
149
+ // Structural validation of the runtime-defined announcement shape. Returns
150
+ // reason objects in the same { code, message } vocabulary the signature
151
+ // verifier uses, so callers can report one merged list.
152
+ function announcementShapeReasons(doc) {
153
+ if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
154
+ return [{ code: 'announcement.shape', message: 'announcement is not a JSON object' }]
155
+ }
156
+ const reasons = []
157
+ const push = (message) => reasons.push({ code: 'announcement.shape', message })
158
+ if (doc.schema !== ANNOUNCEMENT_SCHEMA) {
159
+ push(`schema must be the constant ${ANNOUNCEMENT_SCHEMA}`)
160
+ }
161
+ if (typeof doc.announcementId !== 'string' || !doc.announcementId.startsWith('announcement:')) {
162
+ push("announcementId must be a string with the 'announcement:' prefix")
163
+ }
164
+ if (typeof doc.publishedAt !== 'string' || !/^\d{4}-\d{2}-\d{2}([T ]|$)/.test(doc.publishedAt)
165
+ || Number.isNaN(Date.parse(doc.publishedAt))) {
166
+ push('publishedAt must be an ISO date string (YYYY-MM-DD, optionally with a time part)')
167
+ }
168
+ if (typeof doc.title !== 'string' || doc.title.length === 0) {
169
+ push('title must be a non-empty string')
170
+ }
171
+ if (typeof doc.body !== 'string') {
172
+ push('body must be a markdown string')
173
+ }
174
+ if (!('signature' in doc)) {
175
+ push('signature member is required (null only while unsigned; an unsigned announcement never verifies)')
176
+ }
177
+ return reasons
178
+ }
179
+
180
+ // Verify one parsed announcement document: structural shape first, then the
181
+ // detached signature over the JCS bytes with signature set to null (the same
182
+ // normative rule the attestation flow uses). Never throws on bad documents.
183
+ function verifyAnnouncement(doc, publicKey) {
184
+ const reasons = [...announcementShapeReasons(doc)]
185
+ if (doc && typeof doc === 'object' && !Array.isArray(doc)) {
186
+ reasons.push(...verifyDocument(doc, { publicKey }).reasons)
187
+ }
188
+ return { valid: reasons.length === 0, reasons }
189
+ }
190
+
191
+ function runVerify(argv) {
192
+ const options = parseOptions(argv, { 'public-key': 'value', json: 'flag' })
193
+ const [file] = options._
194
+ if (!file || options._.length !== 1) fail(`announcements verify requires exactly one announcement file\n\n${USAGE}`)
195
+ const explicitKey = options['public-key'] !== undefined
196
+ const keyPath = explicitKey ? options['public-key'] : DEFAULT_PUBLIC_KEY
197
+ const publicKey = loadPublicKey(keyPath)
198
+ const doc = readJsonFile(file, 'announcement')
199
+ const { valid, reasons } = verifyAnnouncement(doc, publicKey)
200
+ if (options.json) {
201
+ console.log(JSON.stringify({
202
+ valid,
203
+ reasons,
204
+ key: safeLabel(describeKeyPath(keyPath), 120),
205
+ keyId: safeLabel(publicKey.keyId),
206
+ explicitKey,
207
+ }, null, 2))
208
+ } else if (valid) {
209
+ console.log(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
210
+ console.log(`announcement signature is valid (keyId ${safeLabel(doc.signature.keyId)})`)
211
+ } else {
212
+ console.error(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
213
+ for (const reason of reasons) console.error(`[${reason.code}] ${reason.message}`)
214
+ }
215
+ process.exit(valid ? 0 : 1)
216
+ }
217
+
218
+ function runShow(argv) {
219
+ const options = parseOptions(argv, { 'public-key': 'value' })
220
+ const [file] = options._
221
+ if (!file || options._.length !== 1) fail(`announcements show requires exactly one announcement file\n\n${USAGE}`)
222
+ const explicitKey = options['public-key'] !== undefined
223
+ const keyPath = explicitKey ? options['public-key'] : DEFAULT_PUBLIC_KEY
224
+ const publicKey = loadPublicKey(keyPath)
225
+ const doc = readJsonFile(file, 'announcement')
226
+ const { valid, reasons } = verifyAnnouncement(doc, publicKey)
227
+ if (!valid) {
228
+ console.error(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
229
+ console.error(`refusing to show ${safeLabel(file, 120)}: the announcement does not verify, so its content is untrusted`)
230
+ for (const reason of reasons) console.error(`[${reason.code}] ${reason.message}`)
231
+ process.exit(1)
232
+ }
233
+ // The anchor prints before any attacker-authored content, so a reader sees
234
+ // which key vouched for what they are about to read.
235
+ console.log(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
236
+ console.log(`${doc.title}`)
237
+ console.log(`${doc.announcementId} — ${doc.publishedAt} (signature valid, keyId ${safeLabel(doc.signature.keyId)})`)
238
+ console.log('')
239
+ console.log(doc.body)
240
+ process.exit(0)
241
+ }
242
+
243
+ function runList(argv) {
244
+ const options = parseOptions(argv, { dir: 'value', 'public-key': 'value' })
245
+ if (options._.length) fail(`announcements list takes options only\n\n${USAGE}`)
246
+ const dir = options.dir ?? DEFAULT_DIR
247
+ // The trust anchor is the committed key — the same one verify and show
248
+ // default to — never a key found under --dir. Only --public-key moves it,
249
+ // and the header below says so when it does.
250
+ const explicitKey = options['public-key'] !== undefined
251
+ const keyPath = explicitKey ? options['public-key'] : DEFAULT_PUBLIC_KEY
252
+ const publicKey = loadPublicKey(keyPath)
253
+ console.log(keyIdentityHeader(keyPath, publicKey, { explicit: explicitKey }))
254
+ if (options.dir !== undefined) {
255
+ console.log('--dir changes only which documents are read; the trust anchor above is unchanged.')
256
+ }
257
+ let entries
258
+ try {
259
+ entries = fs.readdirSync(dir, { withFileTypes: true })
260
+ } catch {
261
+ fail(`cannot read announcements directory: ${safeLabel(dir, 120)}`)
262
+ }
263
+ const files = entries
264
+ .filter((entry) => entry.isFile() && entry.name.endsWith('.json'))
265
+ .map((entry) => entry.name)
266
+ .sort()
267
+ if (files.length === 0) {
268
+ console.log(`no announcements found in ${dir}`)
269
+ process.exit(0)
270
+ }
271
+ let failures = 0
272
+ for (const name of files) {
273
+ const file = path.join(dir, name)
274
+ let doc = null
275
+ let reasons = null
276
+ try {
277
+ doc = JSON.parse(fs.readFileSync(file, 'utf8'))
278
+ } catch {
279
+ reasons = [{ code: 'announcement.unreadable', message: 'file is missing or not valid JSON' }]
280
+ }
281
+ if (!reasons) {
282
+ const result = verifyAnnouncement(doc, publicKey)
283
+ if (!result.valid) reasons = result.reasons
284
+ }
285
+ if (reasons) {
286
+ failures += 1
287
+ console.error(`!! UNVERIFIED ${safeLabel(name, 96)} — do not trust this file's content`)
288
+ for (const reason of reasons) console.error(` [${reason.code}] ${reason.message}`)
289
+ } else {
290
+ console.log(`${doc.announcementId} ${doc.publishedAt} ${doc.title}`)
291
+ }
292
+ }
293
+ if (failures > 0) {
294
+ console.error(`${failures} of ${files.length} announcement file(s) failed verification`)
295
+ }
296
+ process.exit(failures > 0 ? 1 : 0)
297
+ }
298
+
299
+ const argv = process.argv.slice(2)
300
+ const subcommand = argv[0]
301
+ if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
302
+ console.log(USAGE)
303
+ process.exit(0)
304
+ }
305
+ if (subcommand === 'list') runList(argv.slice(1))
306
+ else if (subcommand === 'verify') runVerify(argv.slice(1))
307
+ else if (subcommand === 'show') runShow(argv.slice(1))
308
+ else fail(`unknown announcements subcommand: ${subcommand}\n\n${USAGE}`)
@@ -0,0 +1,198 @@
1
+ #!/usr/bin/env node
2
+ // atelier attestation hash|sign|verify|keygen
3
+ //
4
+ // Self-contained subcommand parser (no shared CLI plumbing) so the module can
5
+ // be invoked directly: node src/commands/attestation.mjs <subcommand> ...
6
+ //
7
+ // Exit codes: 0 success (verify: valid), 1 verify found the attestation
8
+ // invalid, 2 usage or input error. No network access anywhere in this
9
+ // command. Log-safety contract: output may name keyId and algorithm, never
10
+ // any private JWK member and never file contents.
11
+
12
+ import fs from 'node:fs'
13
+ import path from 'node:path'
14
+ import {
15
+ generateKeyPair,
16
+ loadSigningKey,
17
+ payloadHashOf,
18
+ signAttestation,
19
+ verifyAttestation,
20
+ verifyPayloadBinding,
21
+ LOCAL_KEY_FILE,
22
+ } from '../attestation/sign.mjs'
23
+
24
+ const USAGE = `Usage: atelier attestation <subcommand>
25
+
26
+ Subcommands:
27
+ hash <payload.json>
28
+ Print the canonical payload hash object (sha-256 over RFC 8785 JCS).
29
+
30
+ sign <attestation.json> [--key FILE] [--out FILE]
31
+ Sign an unsigned attestation (signature must be null) with the local
32
+ signing key file. Writes the signed document to stdout, or to --out.
33
+
34
+ verify <attestation.json> --public-key FILE [--payload FILE] [--json]
35
+ Verify the signature against a public key file, and, when --payload is
36
+ given, the payload binding. --json prints { valid, reasons }.
37
+
38
+ keygen --key-id ID [--algorithm ed25519|es256] [--out FILE]
39
+ Generate a signing key pair. Writes the private key file (default
40
+ ${LOCAL_KEY_FILE}, mode 0600, refuses to overwrite)
41
+ and prints only the public key document.
42
+
43
+ Exit codes: 0 success or valid, 1 verify judged the attestation invalid,
44
+ 2 usage or input error.`
45
+
46
+ function fail(message) {
47
+ console.error(message)
48
+ process.exit(2)
49
+ }
50
+
51
+ function parseOptions(argv, spec) {
52
+ const options = { _: [] }
53
+ for (let i = 0; i < argv.length; i += 1) {
54
+ const arg = argv[i]
55
+ if (arg === '--help' || arg === '-h') {
56
+ console.log(USAGE)
57
+ process.exit(0)
58
+ }
59
+ if (arg.startsWith('--')) {
60
+ const name = arg.slice(2)
61
+ if (!(name in spec)) fail(`unknown option --${name}\n\n${USAGE}`)
62
+ if (spec[name] === 'flag') {
63
+ options[name] = true
64
+ } else {
65
+ i += 1
66
+ if (argv[i] === undefined) fail(`--${name} requires a value`)
67
+ options[name] = argv[i]
68
+ }
69
+ } else {
70
+ options._.push(arg)
71
+ }
72
+ }
73
+ return options
74
+ }
75
+
76
+ function readJsonFile(file, label) {
77
+ let raw
78
+ try {
79
+ raw = fs.readFileSync(file, 'utf8')
80
+ } catch {
81
+ fail(`cannot read ${label} file: ${file}`)
82
+ }
83
+ try {
84
+ return JSON.parse(raw)
85
+ } catch {
86
+ fail(`${label} file is not valid JSON: ${file}`)
87
+ }
88
+ return null
89
+ }
90
+
91
+ function runHash(argv) {
92
+ const options = parseOptions(argv, {})
93
+ const [file] = options._
94
+ if (!file || options._.length !== 1) fail(`attestation hash requires exactly one payload file\n\n${USAGE}`)
95
+ const payload = readJsonFile(file, 'payload')
96
+ try {
97
+ console.log(JSON.stringify(payloadHashOf(payload), null, 2))
98
+ } catch (error) {
99
+ fail(`payload cannot be canonicalized: ${error.message}`)
100
+ }
101
+ }
102
+
103
+ function runSign(argv) {
104
+ const options = parseOptions(argv, { key: 'value', out: 'value' })
105
+ const [file] = options._
106
+ if (!file || options._.length !== 1) fail(`attestation sign requires exactly one attestation file\n\n${USAGE}`)
107
+ const attestation = readJsonFile(file, 'attestation')
108
+ let signed
109
+ try {
110
+ const key = loadSigningKey({ keyPath: options.key ?? null })
111
+ signed = signAttestation({ attestation, key })
112
+ } catch (error) {
113
+ fail(error.message)
114
+ }
115
+ const output = `${JSON.stringify(signed, null, 2)}\n`
116
+ if (options.out) {
117
+ fs.writeFileSync(options.out, output)
118
+ console.log(`signed attestation written to ${options.out}`)
119
+ } else {
120
+ process.stdout.write(output)
121
+ }
122
+ }
123
+
124
+ function runVerify(argv) {
125
+ const options = parseOptions(argv, { 'public-key': 'value', payload: 'value', json: 'flag' })
126
+ const [file] = options._
127
+ if (!file || options._.length !== 1) fail(`attestation verify requires exactly one attestation file\n\n${USAGE}`)
128
+ if (!options['public-key']) fail(`attestation verify requires --public-key FILE\n\n${USAGE}`)
129
+ const attestation = readJsonFile(file, 'attestation')
130
+ const publicKey = readJsonFile(options['public-key'], 'public key')
131
+ if (!publicKey || typeof publicKey !== 'object' || !publicKey.publicKeyJwk) {
132
+ fail(`public key file has no publicKeyJwk member: ${options['public-key']}`)
133
+ }
134
+ const signature = verifyAttestation({ attestation, publicKey })
135
+ const reasons = [...signature.reasons]
136
+ let payloadBinding = null
137
+ if (options.payload) {
138
+ const payload = readJsonFile(options.payload, 'payload')
139
+ payloadBinding = verifyPayloadBinding({ attestation, payload })
140
+ reasons.push(...payloadBinding.reasons)
141
+ }
142
+ const valid = signature.valid && (payloadBinding === null || payloadBinding.valid)
143
+ if (options.json) {
144
+ console.log(JSON.stringify({ valid, payloadBindingChecked: payloadBinding !== null, reasons }, null, 2))
145
+ } else if (valid) {
146
+ console.log(`attestation signature is valid (keyId ${attestation?.signature?.keyId ?? 'unknown'})`)
147
+ if (payloadBinding === null) console.log('payload binding not checked (pass --payload FILE to check it)')
148
+ else console.log('payload binding is valid')
149
+ } else {
150
+ for (const reason of reasons) console.error(`[${reason.code}] ${reason.message}`)
151
+ }
152
+ process.exit(valid ? 0 : 1)
153
+ }
154
+
155
+ function runKeygen(argv) {
156
+ const options = parseOptions(argv, { 'key-id': 'value', algorithm: 'value', out: 'value' })
157
+ if (options._.length) fail(`attestation keygen takes options only\n\n${USAGE}`)
158
+ const keyId = options['key-id']
159
+ if (!keyId) fail(`attestation keygen requires --key-id ID\n\n${USAGE}`)
160
+ const algorithm = options.algorithm ?? 'ed25519'
161
+ if (algorithm !== 'ed25519' && algorithm !== 'es256') {
162
+ fail(`--algorithm must be ed25519 or es256, got ${algorithm}`)
163
+ }
164
+ const outPath = path.resolve(options.out ?? LOCAL_KEY_FILE)
165
+ const { privateKeyDoc, publicKeyDoc } = generateKeyPair({ algorithm, keyId })
166
+ // Atomic create-exclusive ('wx' = O_CREAT|O_EXCL, mode 0600): fails on any
167
+ // existing path AND on symlinks — including dangling ones — so a planted
168
+ // link can never redirect the private key to an attacker-chosen path, and
169
+ // there is no check-then-write race window. Error output stays path-only.
170
+ let fd = null
171
+ try {
172
+ fd = fs.openSync(outPath, 'wx', 0o600)
173
+ } catch (error) {
174
+ if (error?.code === 'EEXIST') {
175
+ fail(`refusing to write signing key file: path already exists or is a symlink: ${outPath}`)
176
+ }
177
+ fail(`cannot create signing key file: ${outPath}`)
178
+ }
179
+ try {
180
+ fs.writeSync(fd, `${JSON.stringify(privateKeyDoc, null, 2)}\n`)
181
+ } finally {
182
+ fs.closeSync(fd)
183
+ }
184
+ console.error(`signing key file written to ${outPath} (mode 0600). Keep it out of version control; ${LOCAL_KEY_FILE} is gitignored by default.`)
185
+ console.log(JSON.stringify(publicKeyDoc, null, 2))
186
+ }
187
+
188
+ const argv = process.argv.slice(2)
189
+ const subcommand = argv[0]
190
+ if (!subcommand || subcommand === '--help' || subcommand === '-h' || subcommand === 'help') {
191
+ console.log(USAGE)
192
+ process.exit(0)
193
+ }
194
+ if (subcommand === 'hash') runHash(argv.slice(1))
195
+ else if (subcommand === 'sign') runSign(argv.slice(1))
196
+ else if (subcommand === 'verify') runVerify(argv.slice(1))
197
+ else if (subcommand === 'keygen') runKeygen(argv.slice(1))
198
+ else fail(`unknown attestation subcommand: ${subcommand}\n\n${USAGE}`)
@@ -0,0 +1,25 @@
1
+ #!/usr/bin/env node
2
+ import { parseArgs } from '../project/config.mjs'
3
+ import {
4
+ runBoundaryAuditCommand,
5
+ runBoundaryCheckCommand,
6
+ runBoundaryInstallHooksCommand,
7
+ runBoundaryPushCheckCommand,
8
+ } from '../boundary/policy.mjs'
9
+
10
+ const args = parseArgs(process.argv.slice(2))
11
+ const subcommand = args._[0] || 'check'
12
+ const rest = (...names) => process.argv.slice(2).filter((arg) => !names.includes(arg))
13
+
14
+ if (subcommand === 'check' || subcommand === 'doctor') {
15
+ runBoundaryCheckCommand(rest('check', 'doctor'))
16
+ } else if (subcommand === 'push-check') {
17
+ runBoundaryPushCheckCommand(rest('push-check'))
18
+ } else if (subcommand === 'audit') {
19
+ runBoundaryAuditCommand(rest('audit'))
20
+ } else if (subcommand === 'install-hooks') {
21
+ runBoundaryInstallHooksCommand(rest('install-hooks'))
22
+ } else {
23
+ console.error(`Unknown boundary command: ${subcommand}`)
24
+ process.exit(1)
25
+ }
@@ -0,0 +1,4 @@
1
+ #!/usr/bin/env node
2
+ import { commandProject } from '../project/config.mjs'
3
+ const project = commandProject()
4
+ console.log(JSON.stringify({ ok: true, schema: project.schema, configPath: project.configPath, repos: project.repos.map((repo) => repo.name) }, null, 2))
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { runContextCommand } from '../harness/context.mjs'
3
+ runContextCommand()