@massu/core 1.10.8 → 1.12.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 (178) hide show
  1. package/CHANGELOG.md +1334 -0
  2. package/dist/adapter.js +6 -4
  3. package/dist/cli.js +581 -448
  4. package/dist/detect/adapters/.bundle-shasums.json +4 -7
  5. package/dist/detect/adapters/rails.js +6 -4
  6. package/dist/detect/adapters/spring.js +6 -4
  7. package/dist/detect/adapters/tree-sitter-loader.js +6 -4
  8. package/dist/hooks/auto-learning-pipeline.js +24 -1
  9. package/dist/hooks/classify-failure.js +32 -3
  10. package/dist/hooks/cost-tracker.js +24 -0
  11. package/dist/hooks/fix-detector.js +97 -1
  12. package/dist/hooks/incident-pipeline.js +30 -1
  13. package/dist/hooks/intent-suggester.js +6 -3
  14. package/dist/hooks/post-edit-context.js +24 -1
  15. package/dist/hooks/post-tool-use.js +67 -19
  16. package/dist/hooks/pre-compact.js +28 -3
  17. package/dist/hooks/pre-delete-check.js +53 -27
  18. package/dist/hooks/pre-tool-use-gate.js +962 -0
  19. package/dist/hooks/quality-event.js +24 -0
  20. package/dist/hooks/rule-enforcement-pipeline.js +24 -1
  21. package/dist/hooks/security-gate.js +29 -16
  22. package/dist/hooks/session-end.js +31 -6
  23. package/dist/hooks/session-start.js +8709 -8459
  24. package/dist/hooks/user-prompt.js +25 -1
  25. package/package.json +15 -18
  26. package/commands/README.md +0 -134
  27. package/src/adapter.ts +0 -121
  28. package/src/adr-generator.ts +0 -292
  29. package/src/analytics.ts +0 -373
  30. package/src/audit-trail.ts +0 -450
  31. package/src/backfill-sessions.ts +0 -182
  32. package/src/changelog-generator.ts +0 -178
  33. package/src/cli.ts +0 -246
  34. package/src/cloud-sync.ts +0 -234
  35. package/src/commands/adapters.ts +0 -824
  36. package/src/commands/changelog.ts +0 -165
  37. package/src/commands/config-check-drift.ts +0 -133
  38. package/src/commands/config-refresh.ts +0 -396
  39. package/src/commands/config-upgrade.ts +0 -127
  40. package/src/commands/doctor.ts +0 -485
  41. package/src/commands/hook-runner.ts +0 -145
  42. package/src/commands/init.ts +0 -1823
  43. package/src/commands/install-commands.ts +0 -692
  44. package/src/commands/install-hooks.ts +0 -27
  45. package/src/commands/permissions.ts +0 -150
  46. package/src/commands/refresh-log.ts +0 -37
  47. package/src/commands/show-template.ts +0 -65
  48. package/src/commands/template-engine.ts +0 -301
  49. package/src/commands/watch.ts +0 -430
  50. package/src/config.ts +0 -765
  51. package/src/cost-tracker.ts +0 -355
  52. package/src/db.ts +0 -370
  53. package/src/dependency-scorer.ts +0 -337
  54. package/src/detect/adapters/aspnet.ts +0 -4
  55. package/src/detect/adapters/discover.ts +0 -469
  56. package/src/detect/adapters/file-sampler.ts +0 -225
  57. package/src/detect/adapters/go-chi.ts +0 -4
  58. package/src/detect/adapters/index.ts +0 -51
  59. package/src/detect/adapters/nextjs-trpc.ts +0 -166
  60. package/src/detect/adapters/parse-guard.ts +0 -133
  61. package/src/detect/adapters/phoenix.ts +0 -4
  62. package/src/detect/adapters/python-django.ts +0 -208
  63. package/src/detect/adapters/python-fastapi.ts +0 -223
  64. package/src/detect/adapters/python-flask.ts +0 -235
  65. package/src/detect/adapters/query-helpers.ts +0 -170
  66. package/src/detect/adapters/rails.ts +0 -4
  67. package/src/detect/adapters/runner.ts +0 -284
  68. package/src/detect/adapters/spring.ts +0 -4
  69. package/src/detect/adapters/swift-swiftui.ts +0 -171
  70. package/src/detect/adapters/tree-sitter-loader.ts +0 -517
  71. package/src/detect/adapters/types.ts +0 -191
  72. package/src/detect/codebase-introspector.ts +0 -210
  73. package/src/detect/domain-inferrer.ts +0 -197
  74. package/src/detect/drift.ts +0 -199
  75. package/src/detect/framework-detector.ts +0 -307
  76. package/src/detect/index.ts +0 -209
  77. package/src/detect/manifest-registry.ts +0 -261
  78. package/src/detect/migrate.ts +0 -362
  79. package/src/detect/monorepo-detector.ts +0 -348
  80. package/src/detect/package-detector.ts +0 -828
  81. package/src/detect/passthrough.ts +0 -108
  82. package/src/detect/regex-fallback.ts +0 -449
  83. package/src/detect/source-dir-detector.ts +0 -297
  84. package/src/detect/vr-command-map.ts +0 -167
  85. package/src/docs-map.json +0 -100
  86. package/src/docs-tools.ts +0 -519
  87. package/src/domains.ts +0 -182
  88. package/src/hooks/auto-learning-pipeline.ts +0 -228
  89. package/src/hooks/classify-failure.ts +0 -259
  90. package/src/hooks/cost-tracker.ts +0 -66
  91. package/src/hooks/fix-detector.ts +0 -186
  92. package/src/hooks/incident-pipeline.ts +0 -190
  93. package/src/hooks/intent-suggester.ts +0 -131
  94. package/src/hooks/post-edit-context.ts +0 -91
  95. package/src/hooks/post-tool-use.ts +0 -413
  96. package/src/hooks/pre-compact.ts +0 -168
  97. package/src/hooks/pre-delete-check.ts +0 -243
  98. package/src/hooks/quality-event.ts +0 -127
  99. package/src/hooks/rule-enforcement-pipeline.ts +0 -159
  100. package/src/hooks/security-gate.ts +0 -153
  101. package/src/hooks/session-end.ts +0 -467
  102. package/src/hooks/session-start.ts +0 -436
  103. package/src/hooks/user-prompt.ts +0 -157
  104. package/src/import-resolver.ts +0 -226
  105. package/src/knowledge-db.ts +0 -176
  106. package/src/knowledge-indexer.ts +0 -708
  107. package/src/knowledge-tools.ts +0 -1414
  108. package/src/lib/fileLock.ts +0 -203
  109. package/src/lib/framework-supports.ts +0 -85
  110. package/src/lib/gitToplevel.ts +0 -22
  111. package/src/lib/hook-registry.ts +0 -43
  112. package/src/lib/installLock.ts +0 -66
  113. package/src/lib/memory-path.ts +0 -49
  114. package/src/lib/pidLiveness.ts +0 -67
  115. package/src/lib/settings-local.ts +0 -110
  116. package/src/lib/sql-table-names.ts +0 -71
  117. package/src/license.ts +0 -547
  118. package/src/lsp/auto-detect.ts +0 -98
  119. package/src/lsp/client.ts +0 -776
  120. package/src/lsp/enrich.ts +0 -127
  121. package/src/lsp/types.ts +0 -221
  122. package/src/memory-db.ts +0 -1614
  123. package/src/memory-file-ingest.ts +0 -128
  124. package/src/memory-tools.ts +0 -424
  125. package/src/middleware-tree.ts +0 -71
  126. package/src/observability-tools.ts +0 -343
  127. package/src/observation-extractor.ts +0 -418
  128. package/src/page-deps.ts +0 -285
  129. package/src/permissions.ts +0 -422
  130. package/src/prompt-analyzer.ts +0 -332
  131. package/src/python/coupling-detector.ts +0 -125
  132. package/src/python/domain-enforcer.ts +0 -84
  133. package/src/python/impact-analyzer.ts +0 -96
  134. package/src/python/import-parser.ts +0 -244
  135. package/src/python/import-resolver.ts +0 -136
  136. package/src/python/migration-indexer.ts +0 -116
  137. package/src/python/migration-parser.ts +0 -332
  138. package/src/python/model-indexer.ts +0 -71
  139. package/src/python/model-parser.ts +0 -279
  140. package/src/python/route-indexer.ts +0 -59
  141. package/src/python/route-parser.ts +0 -317
  142. package/src/python-tools.ts +0 -630
  143. package/src/regression-detector.ts +0 -319
  144. package/src/rules.ts +0 -57
  145. package/src/schema-mapper.ts +0 -232
  146. package/src/security/adapter-origin.ts +0 -130
  147. package/src/security/adapter-verifier.ts +0 -319
  148. package/src/security/atomic-write.ts +0 -164
  149. package/src/security/fetcher.ts +0 -200
  150. package/src/security/install-tracking.ts +0 -319
  151. package/src/security/license-pubkey.generated.ts +0 -30
  152. package/src/security/license-response-verifier.ts +0 -149
  153. package/src/security/local-fingerprint.ts +0 -225
  154. package/src/security/manifest-cache.ts +0 -333
  155. package/src/security/manifest-schema.ts +0 -129
  156. package/src/security/registry-pubkey.generated.ts +0 -35
  157. package/src/security/telemetry.ts +0 -320
  158. package/src/security-scorer.ts +0 -405
  159. package/src/security-utils.ts +0 -133
  160. package/src/sentinel-db.ts +0 -580
  161. package/src/sentinel-scanner.ts +0 -406
  162. package/src/sentinel-tools.ts +0 -513
  163. package/src/sentinel-types.ts +0 -140
  164. package/src/server-dispatch.ts +0 -225
  165. package/src/server.ts +0 -107
  166. package/src/session-archiver.ts +0 -111
  167. package/src/session-state-generator.ts +0 -174
  168. package/src/team-knowledge.ts +0 -407
  169. package/src/tool-db-needs.ts +0 -232
  170. package/src/tools.ts +0 -1206
  171. package/src/transcript-parser.ts +0 -458
  172. package/src/trpc-index.ts +0 -215
  173. package/src/validate-features-runner.ts +0 -107
  174. package/src/validation-engine.ts +0 -358
  175. package/src/watch/daemon.ts +0 -385
  176. package/src/watch/lockfile-detector.ts +0 -65
  177. package/src/watch/paths.ts +0 -279
  178. package/src/watch/state.ts +0 -178
@@ -1,333 +0,0 @@
1
- /**
2
- * Adapter manifest cache (Plan 3c Phase 5 5F.3 — gap-37 + gap-54 + gap-59).
3
- *
4
- * Persists the verified registry manifest at `~/.massu/adapter-manifest.json`
5
- * so subsequent `npx massu adapters *` invocations do NOT re-fetch (and the
6
- * watcher daemon's auto-refresh-on-quiescence path is fast). The cache is
7
- * self-validating: every read goes back through verifyManifest against the
8
- * bundled Ed25519 pubkey, so a tampered cache file refuses to load.
9
- *
10
- * Concurrency model (gap-59):
11
- * - Reader path: NO lock acquired. POSIX renameSync inside atomicWrite
12
- * guarantees readers see EITHER old OR new content, never torn. Multiple
13
- * parallel readers (CLI + watcher + coverage) read concurrently with no
14
- * serialization cost.
15
- * - Writer path: acquires `~/.massu/.adapter-manifest.lock` via the shared
16
- * `withFileLockSync` primitive. Two concurrent `refreshManifest` invocations
17
- * both perform the async fetch in parallel (independent network operations),
18
- * then serialize on the lock for the brief atomicWrite call. The later
19
- * write wins; both contents are equally valid (manifest is signed; the
20
- * later signed-at timestamp is fresher).
21
- * - Lock is held ONLY for the sync atomicWrite — never around the async
22
- * fetch — so contention bounded to ~1ms even under heavy load.
23
- *
24
- * Rotation drift detection (gap-54):
25
- * - Cache wrapper records `bundled_pubkey_fingerprint` at write time
26
- * (sha256 hex of REGISTRY_PUBKEY_ED25519 the writer was bundling).
27
- * - On read, if the cache's recorded fingerprint != currently-running
28
- * @massu/core's bundled pubkey fingerprint, mark cache STALE-DUE-TO-
29
- * ROTATION (separate from staleness-by-age). Caller must force refresh
30
- * to pick up the new pubkey's signed manifest.
31
- *
32
- * Staleness model:
33
- * - `fetched_at` records the cache write time (NOT the manifest's
34
- * `signed_at`, which the publisher controls).
35
- * - `MAX_FRESH_MS` (24h) — cache is considered fresh; reads short-circuit.
36
- * - `MAX_STALE_MS` (7 days) — cache may be used offline. Caller surfaces
37
- * the staleness in UX.
38
- * - Beyond 7 days — refuse to use any new adapter from this cache; require
39
- * manual `npx massu adapters refresh`.
40
- *
41
- * File mode (gap-37): cache file is mode 0o600; ~/.massu/ parent dir is 0o700.
42
- */
43
- import { existsSync, readFileSync, statSync } from 'node:fs';
44
- import { homedir } from 'node:os';
45
- import { resolve } from 'node:path';
46
- import { z } from 'zod';
47
- import { atomicWrite } from './atomic-write.js';
48
- import { withFileLockSync } from '../lib/fileLock.js';
49
- import { verifyManifest, type VerifyManifestResult } from './adapter-verifier.js';
50
- import { EnvelopeSchema, PrintableAsciiStringSchema, type Envelope } from './manifest-schema.js';
51
- import {
52
- REGISTRY_PUBKEY_ED25519,
53
- REGISTRY_PUBKEY_FINGERPRINT_HEX,
54
- } from './registry-pubkey.generated.js';
55
- import { fetchUrl } from './fetcher.js';
56
-
57
- export const MAX_FRESH_MS = 24 * 60 * 60 * 1000; // 24h
58
- export const MAX_STALE_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
59
- export const REGISTRY_MANIFEST_URL = 'https://registry.massu.ai/adapters/manifest.json';
60
-
61
- /**
62
- * Wrapper format persisted to ~/.massu/adapter-manifest.json. The envelope
63
- * is the verified registry envelope (post-verifyManifest); fetched_at is the
64
- * client-side timestamp; bundled_pubkey_fingerprint records which @massu/core
65
- * pubkey signed this cache entry (rotation detection per gap-54).
66
- */
67
- const CacheWrapperSchema = z.object({
68
- envelope: EnvelopeSchema,
69
- // CR-9 iter-6 audit LOW-NEW6-1 fix: fetched_at flows into the
70
- // 'fetched_at not parseable as Date' reason render at line 163 → wrapped
71
- // by getManifest into 'cache invalid: ...' → reaches stderr in
72
- // commands/adapters.ts. Same control-char log-injection vector iter-3
73
- // closed for InstallEntrySchema.ts AND iter-5 closed for
74
- // FingerprintSentinelSchema.ts. Third sibling closure; the new AST
75
- // drift-guard test (test_security_schemas_printable_ascii_drift.test.ts)
76
- // makes this class of bug structurally impossible to recur.
77
- fetched_at: PrintableAsciiStringSchema,
78
- bundled_pubkey_fingerprint: z.string().regex(/^[0-9a-f]{64}$/),
79
- }).strict();
80
- export type CacheWrapper = z.infer<typeof CacheWrapperSchema>;
81
-
82
- export type CacheReadResult =
83
- | { kind: 'fresh'; envelope: Envelope; ageMs: number; warnings: string[] }
84
- | { kind: 'stale'; envelope: Envelope; ageMs: number; warnings: string[]; reason: string }
85
- | { kind: 'expired'; ageMs: number; reason: string }
86
- | { kind: 'rotation-detected'; reason: string }
87
- | { kind: 'absent' }
88
- | { kind: 'invalid'; reason: string };
89
-
90
- export interface CachePaths {
91
- cachePath: string;
92
- lockPath: string;
93
- }
94
-
95
- /**
96
- * Compute the canonical cache + lock paths under the user's home directory.
97
- * Exported so tests can reuse the same logic with a sandboxed home.
98
- */
99
- export function defaultCachePaths(): CachePaths {
100
- const dir = resolve(homedir(), '.massu');
101
- return {
102
- cachePath: resolve(dir, 'adapter-manifest.json'),
103
- lockPath: resolve(dir, '.adapter-manifest.lock'),
104
- };
105
- }
106
-
107
- /**
108
- * Read + validate the cached manifest. Returns a tagged result the caller
109
- * dispatches on:
110
- * - 'fresh' — cache is valid + signed + age < MAX_FRESH_MS
111
- * - 'stale' — cache is valid + signed + age < MAX_STALE_MS
112
- * (caller may use; should surface staleness in UX)
113
- * - 'expired' — age > MAX_STALE_MS; caller MUST refresh
114
- * - 'rotation-detected' — bundled_pubkey_fingerprint mismatch; caller MUST
115
- * refresh under the new bundled pubkey
116
- * - 'absent' — no cache file
117
- * - 'invalid' — file exists but failed parse / signature verify;
118
- * treat as 'absent' but surface reason for logs
119
- */
120
- export function loadCachedManifest(paths: CachePaths = defaultCachePaths()): CacheReadResult {
121
- if (!existsSync(paths.cachePath)) {
122
- return { kind: 'absent' };
123
- }
124
-
125
- let raw: unknown;
126
- try {
127
- const content = readFileSync(paths.cachePath, 'utf-8');
128
- raw = JSON.parse(content);
129
- } catch (err) {
130
- return {
131
- kind: 'invalid',
132
- reason: `cache JSON parse failed: ${err instanceof Error ? err.message : String(err)}`,
133
- };
134
- }
135
-
136
- const wrapperParsed = CacheWrapperSchema.safeParse(raw);
137
- if (!wrapperParsed.success) {
138
- const issues = wrapperParsed.error.issues
139
- .map((i) => `${i.path.join('.') || '(root)'}: ${i.message}`)
140
- .join('; ');
141
- return { kind: 'invalid', reason: `cache wrapper shape invalid: ${issues}` };
142
- }
143
- const wrapper = wrapperParsed.data;
144
-
145
- // Rotation detection (gap-54): if the cache's recorded bundled-pubkey
146
- // fingerprint does NOT match the currently-running @massu/core's pubkey,
147
- // the cache was signed under a different key and we can't verify it
148
- // with our current bundle. Caller MUST refresh.
149
- if (wrapper.bundled_pubkey_fingerprint !== REGISTRY_PUBKEY_FINGERPRINT_HEX) {
150
- return {
151
- kind: 'rotation-detected',
152
- reason:
153
- `cache was written under @massu/core bundled pubkey ` +
154
- `${wrapper.bundled_pubkey_fingerprint.slice(0, 16)}... but this @massu/core ` +
155
- `bundles ${REGISTRY_PUBKEY_FINGERPRINT_HEX.slice(0, 16)}.... Cache must be ` +
156
- `refreshed under the current bundled pubkey.`,
157
- };
158
- }
159
-
160
- // Verify the envelope via the canonical 9-step verifier. Pure, no I/O.
161
- const verifyResult: VerifyManifestResult = verifyManifest({
162
- envelope: wrapper.envelope,
163
- publicKey: REGISTRY_PUBKEY_ED25519,
164
- });
165
- if (!verifyResult.ok) {
166
- return { kind: 'invalid', reason: `cached envelope failed verify: ${verifyResult.reason}` };
167
- }
168
-
169
- const fetchedAtMs = Date.parse(wrapper.fetched_at);
170
- if (!Number.isFinite(fetchedAtMs)) {
171
- return { kind: 'invalid', reason: `fetched_at not parseable as Date: ${wrapper.fetched_at}` };
172
- }
173
- const ageMs = Date.now() - fetchedAtMs;
174
- if (ageMs < MAX_FRESH_MS) {
175
- return { kind: 'fresh', envelope: verifyResult.envelope, ageMs, warnings: verifyResult.warnings };
176
- }
177
- if (ageMs < MAX_STALE_MS) {
178
- return {
179
- kind: 'stale',
180
- envelope: verifyResult.envelope,
181
- ageMs,
182
- warnings: verifyResult.warnings,
183
- reason: `cache is ${Math.floor(ageMs / 3600_000)}h old; consider refreshing`,
184
- };
185
- }
186
- return {
187
- kind: 'expired',
188
- ageMs,
189
- reason: `cache is ${Math.floor(ageMs / 86400_000)}d old (> 7d); refusing to use`,
190
- };
191
- }
192
-
193
- /**
194
- * Atomic write of a verified envelope to the cache file under the writer
195
- * lock. Returns void on success; throws if (a) the lock cannot be acquired
196
- * within the configured block, or (b) the atomicWrite fails.
197
- *
198
- * Caller is responsible for ensuring the envelope is verified BEFORE
199
- * calling this function — cacheManifest does NOT re-verify (the verify
200
- * already happens inside refreshManifest before writing).
201
- */
202
- export function cacheManifest(envelope: Envelope, paths: CachePaths = defaultCachePaths()): void {
203
- const wrapper: CacheWrapper = {
204
- envelope,
205
- fetched_at: new Date().toISOString(),
206
- bundled_pubkey_fingerprint: REGISTRY_PUBKEY_FINGERPRINT_HEX,
207
- };
208
- withFileLockSync(paths.lockPath, () => {
209
- const result = atomicWrite(paths.cachePath, JSON.stringify(wrapper, null, 2), {
210
- mode: 0o600,
211
- ensureParentDirMode: 0o700,
212
- });
213
- if (!result.written) {
214
- throw new Error(`cacheManifest: atomicWrite failed: ${result.error}`);
215
- }
216
- });
217
- }
218
-
219
- export type RefreshResult =
220
- | { kind: 'refreshed'; envelope: Envelope; warnings: string[] }
221
- | { kind: 'fetch-failed'; reason: string }
222
- | { kind: 'verify-failed'; reason: string };
223
-
224
- /**
225
- * Fetch + verify the live registry manifest, write to cache. Returns a
226
- * tagged result. The caller decides whether to surface refresh failures
227
- * as fatal (stale-cache > 7d) or non-fatal (cache fresh but stale-by-age).
228
- */
229
- export async function refreshManifest(
230
- paths: CachePaths = defaultCachePaths(),
231
- fetchFn: typeof fetchUrl = fetchUrl,
232
- ): Promise<RefreshResult> {
233
- let body: string;
234
- try {
235
- const response = await fetchFn(REGISTRY_MANIFEST_URL);
236
- if (response.status !== 200) {
237
- return { kind: 'fetch-failed', reason: `registry returned HTTP ${response.status}` };
238
- }
239
- body = response.body;
240
- } catch (err) {
241
- return {
242
- kind: 'fetch-failed',
243
- reason: `fetch ${REGISTRY_MANIFEST_URL} failed: ${err instanceof Error ? err.message : String(err)}`,
244
- };
245
- }
246
-
247
- let parsed: unknown;
248
- try {
249
- parsed = JSON.parse(body);
250
- } catch (err) {
251
- return {
252
- kind: 'fetch-failed',
253
- reason: `registry response not JSON: ${err instanceof Error ? err.message : String(err)}`,
254
- };
255
- }
256
-
257
- const verified = verifyManifest({ envelope: parsed, publicKey: REGISTRY_PUBKEY_ED25519 });
258
- if (!verified.ok) {
259
- return { kind: 'verify-failed', reason: verified.reason };
260
- }
261
-
262
- cacheManifest(verified.envelope, paths);
263
- return { kind: 'refreshed', envelope: verified.envelope, warnings: verified.warnings };
264
- }
265
-
266
- /**
267
- * High-level getter: try cache → if fresh, return. Else refresh + return new
268
- * envelope. If `force=true`, skip cache entirely. If refresh fails AND a
269
- * stale cache is available (< 7d), return it with a `staleAcceptedReason`.
270
- * If everything fails, returns null + error reasons in the result object.
271
- */
272
- export interface GetManifestOpts {
273
- paths?: CachePaths;
274
- force?: boolean;
275
- fetchFn?: typeof fetchUrl;
276
- }
277
-
278
- export type GetManifestResult =
279
- | { kind: 'ok'; envelope: Envelope; source: 'cache-fresh' | 'cache-stale' | 'refreshed'; warnings: string[]; staleReason?: string }
280
- | { kind: 'fail'; reasons: string[] };
281
-
282
- export async function getManifest(opts: GetManifestOpts = {}): Promise<GetManifestResult> {
283
- const paths = opts.paths ?? defaultCachePaths();
284
- const fetchFn = opts.fetchFn ?? fetchUrl;
285
- const reasons: string[] = [];
286
-
287
- if (!opts.force) {
288
- const cacheRead = loadCachedManifest(paths);
289
- if (cacheRead.kind === 'fresh') {
290
- return { kind: 'ok', envelope: cacheRead.envelope, source: 'cache-fresh', warnings: cacheRead.warnings };
291
- }
292
- if (cacheRead.kind === 'stale') {
293
- // Try refresh first; fall back to stale cache if refresh fails.
294
- const refreshed = await refreshManifest(paths, fetchFn);
295
- if (refreshed.kind === 'refreshed') {
296
- return { kind: 'ok', envelope: refreshed.envelope, source: 'refreshed', warnings: refreshed.warnings };
297
- }
298
- reasons.push(`refresh failed: ${refreshed.kind === 'fetch-failed' ? refreshed.reason : refreshed.reason}`);
299
- return {
300
- kind: 'ok',
301
- envelope: cacheRead.envelope,
302
- source: 'cache-stale',
303
- warnings: cacheRead.warnings,
304
- staleReason: cacheRead.reason,
305
- };
306
- }
307
- if (cacheRead.kind === 'invalid') {
308
- reasons.push(`cache invalid: ${cacheRead.reason}`);
309
- } else if (cacheRead.kind === 'rotation-detected') {
310
- reasons.push(`rotation: ${cacheRead.reason}`);
311
- } else if (cacheRead.kind === 'expired') {
312
- reasons.push(`expired: ${cacheRead.reason}`);
313
- }
314
- }
315
-
316
- // Fall through to refresh (force=true OR cache absent/invalid/rotation/expired).
317
- const refreshed = await refreshManifest(paths, fetchFn);
318
- if (refreshed.kind === 'refreshed') {
319
- return { kind: 'ok', envelope: refreshed.envelope, source: 'refreshed', warnings: refreshed.warnings };
320
- }
321
- reasons.push(`refresh failed: ${refreshed.kind === 'fetch-failed' ? refreshed.reason : refreshed.reason}`);
322
- return { kind: 'fail', reasons };
323
- }
324
-
325
- /**
326
- * Helper for tests + telemetry observability: returns the on-disk file
327
- * mode of the cache file (or null if absent). Used to assert gap-37
328
- * file-mode discipline post-write.
329
- */
330
- export function getCacheFileMode(paths: CachePaths = defaultCachePaths()): number | null {
331
- if (!existsSync(paths.cachePath)) return null;
332
- return statSync(paths.cachePath).mode & 0o777;
333
- }
@@ -1,129 +0,0 @@
1
- /**
2
- * Zod schemas for the registry adapter manifest envelope and body.
3
- *
4
- * Plan 3c Phase 5 deliverable. The publisher (registry-publish.sh, version
5
- * with manifest_b64 — see project_plan_3c_phase5_canonicalization_gap.md
6
- * memory + commit 1b724d3) emits envelopes matching EnvelopeSchema; the
7
- * verifier (adapter-verifier.ts) consumes ParsedEnvelope. The body inside
8
- * manifest_b64 (base64-decoded) parses against ManifestBodySchema.
9
- *
10
- * Forward-compatibility model (Plan 3c gap-56):
11
- * - Manifest body has `manifest_schema_version: 1` (integer, default 1 if
12
- * absent for legacy pre-3c manifests).
13
- * - Schemas use `.passthrough()` so unknown additive keys (gap-57
14
- * `deprecated`, `unpublished`, future `revoked_at` etc.) are preserved
15
- * on parse — verifier emits a one-time stderr warning when
16
- * manifest_schema_version > KNOWN_MAX (NOT a refusal — additive
17
- * forward-compat).
18
- * - manifest_schema_version < MIN_KNOWN_VERSION → verifier REFUSES (a
19
- * future v2 may drop fields v1 consumers expect).
20
- *
21
- * Single source of truth: this is the ONLY definition of the envelope and
22
- * manifest shapes in @massu/core. Other modules (verifier, cache, CLI) MUST
23
- * import these schemas + their inferred types instead of re-declaring.
24
- */
25
- import { z } from 'zod';
26
-
27
- /** Minimum schema version this @massu/core build accepts. Below this → refuse. */
28
- export const MIN_KNOWN_SCHEMA_VERSION = 1 as const;
29
- /** Maximum schema version this @massu/core build understands. Above this → warn + continue. */
30
- export const KNOWN_MAX_SCHEMA_VERSION = 1 as const;
31
-
32
- /** sha256 hex string. 64 lowercase hex chars. */
33
- const Sha256HexSchema = z.string().regex(/^[0-9a-f]{64}$/, 'sha256 hex must be 64 lowercase hex chars');
34
-
35
- /**
36
- * Printable ASCII string — 0x20 to 0x7e, length 1+. Rejects control
37
- * characters (including ESC \x1b for ANSI escapes), tabs, newlines, and
38
- * any non-ASCII Unicode. Used for fields that get rendered to stderr or
39
- * stdout in the CLI; without this constraint, an attacker could embed
40
- * ANSI escape sequences in (manifest entry version, package.json name,
41
- * sidecar version, etc.) to log-inject CI/operator-terminal output.
42
- *
43
- * CR-9 iter-4 audit single-source-of-truth (LOW-NEW4-1/2/3 fix): every
44
- * schema field that's downstream of a stderr/stdout emit MUST use this
45
- * type instead of bare z.string(). Adding a new string field that's
46
- * later rendered in CLI output without reaching for this type is a
47
- * regression worth catching at code review.
48
- */
49
- export const PrintableAsciiStringSchema = z.string().min(1).regex(
50
- /^[\x20-\x7e]+$/,
51
- 'must be printable ASCII (0x20-0x7e); control characters like ESC, tab, newline, and non-ASCII are rejected to prevent log injection',
52
- );
53
-
54
- /**
55
- * Standard base64 (RFC 4648) — alphabet [A-Za-z0-9+/], length must be a
56
- * multiple of 4 (with up to two `=` padding chars), no newlines.
57
- * CR-9 audit L1 fix: the prior `^[A-Za-z0-9+/]*={0,2}$` regex permitted a
58
- * lone `=` (decodes to zero bytes) which downstream code would parse as
59
- * a valid empty input. The strict RFC 4648 form below rejects malformed
60
- * base64 at the schema layer instead of relying on a downstream length
61
- * check.
62
- */
63
- const Base64Schema = z.string()
64
- .regex(
65
- /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{4})$/,
66
- 'must be standard base64 (RFC 4648; length divisible by 4 with optional == or = padding)',
67
- )
68
- .min(1);
69
-
70
- /**
71
- * Single adapter entry in the manifest. Plan 3c gap-31 + gap-57:
72
- * - `package` + `version` + `sha256` are the install-time + load-time
73
- * verification primitives.
74
- * - `signing_key_id` is the sha256 fingerprint of the pubkey that signed
75
- * THIS entry (per gap-61, v1 always uses the single registry key).
76
- * - `deprecated` (gap-57) — additive optional. Loader warns + still loads.
77
- * - `unpublished` (gap-57) — additive optional. Loader REFUSES to load.
78
- * - `.passthrough()` preserves unknown additive fields (gap-56 forward-compat).
79
- */
80
- export const AdapterEntrySchema = z.object({
81
- // CR-9 iter-4 audit LOW-NEW4-3 fix: every string field rendered in CLI
82
- // output (discover.ts deprecation warning + adapters.ts search status)
83
- // uses PrintableAsciiStringSchema to prevent log-injection from a
84
- // compromised registry. The trust model assumes the registry can be
85
- // adversarial pre-signing; the verifier's signature check + this
86
- // schema together neutralize that vector.
87
- package: PrintableAsciiStringSchema,
88
- version: PrintableAsciiStringSchema,
89
- sha256: Sha256HexSchema,
90
- signing_key_id: Sha256HexSchema,
91
- deprecated: z.object({
92
- since: PrintableAsciiStringSchema,
93
- replacement: PrintableAsciiStringSchema.nullable().optional(),
94
- reason: PrintableAsciiStringSchema,
95
- }).optional(),
96
- unpublished: z.boolean().optional(),
97
- }).passthrough();
98
- export type AdapterEntry = z.infer<typeof AdapterEntrySchema>;
99
-
100
- /**
101
- * Manifest body — the JSON object inside base64-decoded `manifest_b64`.
102
- * Plan 3c gap-56: schema_version is a numeric integer. Defaults to 1 if
103
- * absent (legacy pre-3c manifests) so the verifier can still read them
104
- * during the rollout transition.
105
- */
106
- export const ManifestBodySchema = z.object({
107
- manifest_schema_version: z.number().int().positive().default(1),
108
- issued_at: z.string().min(1),
109
- adapters: z.array(AdapterEntrySchema),
110
- }).passthrough();
111
- export type ManifestBody = z.infer<typeof ManifestBodySchema>;
112
-
113
- /**
114
- * Full envelope as served by registry.massu.ai/adapters/manifest.json AND
115
- * cached at ~/.massu/adapter-manifest.json. Plan 3c canonicalization-gap
116
- * fix (commit 1b724d3): manifest_b64 is the byte-equal-to-signed-input
117
- * field; verifier MUST consume manifest_b64 (NOT the parsed `manifest`
118
- * field, which is a human-readable re-serialization that does NOT round-
119
- * trip to the signed bytes).
120
- */
121
- export const EnvelopeSchema = z.object({
122
- manifest: ManifestBodySchema,
123
- manifest_b64: Base64Schema,
124
- signature: Base64Schema,
125
- manifest_sha256: Sha256HexSchema,
126
- signed_at: z.string().min(1),
127
- signing_key_id: Sha256HexSchema,
128
- }).passthrough();
129
- export type Envelope = z.infer<typeof EnvelopeSchema>;
@@ -1,35 +0,0 @@
1
- // AUTO-GENERATED by scripts/bundle-pubkey.mjs at 2026-05-17T16:00:08.460Z.
2
- // Source pem: packages/core/security/registry-pubkey.pem
3
- // RAW-bytes sha256: 3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c
4
- // DO NOT EDIT — regenerate via `node scripts/bundle-pubkey.mjs` or
5
- // the prepublishOnly hook (which runs on every npm publish).
6
-
7
- /**
8
- * Ed25519 public key for the Massu adapter registry (registry.massu.ai).
9
- * Used by packages/core/src/security/adapter-verifier.ts to verify signed
10
- * manifests. Plan 3c gap-29 (Phase D P4-003) generated and vendored.
11
- *
12
- * To rotate: regenerate the keypair (operator-only, store private in
13
- * macOS Keychain at `massu/registry/signing/private`), update
14
- * packages/core/security/registry-pubkey.pem with the new public key,
15
- * append the new RAW-bytes sha256 to KNOWN_PUBKEY_FINGERPRINTS in
16
- * scripts/bundle-pubkey.mjs (DO NOT delete the old entry — rotation grace
17
- * window per gap-54), then run `node scripts/bundle-pubkey.mjs`.
18
- */
19
- export const REGISTRY_PUBKEY_ED25519: Uint8Array = new Uint8Array([12, 36, 232, 78, 159, 80, 198, 224, 21, 238, 144, 34, 84, 236, 219, 135, 224, 122, 164, 204, 0, 204, 155, 131, 30, 57, 213, 182, 61, 243, 141, 7]);
20
-
21
- /** Hex sha256 of REGISTRY_PUBKEY_ED25519 (raw 32 bytes). */
22
- export const REGISTRY_PUBKEY_FINGERPRINT_HEX = '3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c';
23
-
24
- /**
25
- * Allowlist of historically-trusted pubkey fingerprints. CR-9 audit L2
26
- * fix: the runtime verifier asserts that REGISTRY_PUBKEY_FINGERPRINT_HEX
27
- * is a member of this set; a bundled key that does not appear in the
28
- * allowlist refuses to load even if compiled in. Mirror of
29
- * KNOWN_PUBKEY_FINGERPRINTS in scripts/bundle-pubkey.mjs (kept in sync
30
- * by the bundle script's emit step). Append on rotation; never remove
31
- * during the grace window.
32
- */
33
- export const KNOWN_PUBKEY_FINGERPRINTS: ReadonlySet<string> = new Set([
34
- "3b6226d036c472e533110d11a7d0cd2773ce1d7d4f1003517d5bd69c5418ed4c"
35
- ]);