@massu/core 1.4.0-soak.0 → 1.5.0

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 (68) hide show
  1. package/commands/README.md +0 -3
  2. package/dist/cli.js +9423 -5453
  3. package/dist/hooks/auto-learning-pipeline.js +27 -1
  4. package/dist/hooks/classify-failure.js +27 -1
  5. package/dist/hooks/cost-tracker.js +27 -1
  6. package/dist/hooks/fix-detector.js +27 -1
  7. package/dist/hooks/incident-pipeline.js +27 -1
  8. package/dist/hooks/post-edit-context.js +27 -1
  9. package/dist/hooks/post-tool-use.js +27 -1
  10. package/dist/hooks/pre-compact.js +27 -1
  11. package/dist/hooks/pre-delete-check.js +27 -1
  12. package/dist/hooks/quality-event.js +27 -1
  13. package/dist/hooks/rule-enforcement-pipeline.js +27 -1
  14. package/dist/hooks/session-end.js +27 -1
  15. package/dist/hooks/session-start.js +2677 -2675
  16. package/dist/hooks/user-prompt.js +27 -1
  17. package/docs/AUTHORING-ADAPTERS.md +207 -0
  18. package/docs/SECURITY.md +250 -0
  19. package/package.json +10 -3
  20. package/src/adapter.ts +90 -0
  21. package/src/cli.ts +7 -0
  22. package/src/commands/adapters.ts +824 -0
  23. package/src/commands/config-check-drift.ts +1 -0
  24. package/src/commands/config-refresh.ts +4 -3
  25. package/src/commands/config-upgrade.ts +1 -0
  26. package/src/commands/doctor.ts +2 -0
  27. package/src/commands/init.ts +3 -1
  28. package/src/commands/template-engine.ts +0 -2
  29. package/src/commands/watch.ts +1 -1
  30. package/src/config.ts +71 -0
  31. package/src/detect/adapters/aspnet.ts +293 -0
  32. package/src/detect/adapters/discover.ts +469 -0
  33. package/src/detect/adapters/go-chi.ts +261 -0
  34. package/src/detect/adapters/index.ts +49 -0
  35. package/src/detect/adapters/phoenix.ts +277 -0
  36. package/src/detect/adapters/python-flask.ts +235 -0
  37. package/src/detect/adapters/rails.ts +279 -0
  38. package/src/detect/adapters/runner.ts +32 -0
  39. package/src/detect/adapters/spring.ts +284 -0
  40. package/src/detect/adapters/tree-sitter-loader.ts +171 -2
  41. package/src/detect/adapters/types.ts +19 -2
  42. package/src/detect/migrate.ts +4 -4
  43. package/src/detect/monorepo-detector.ts +1 -0
  44. package/src/hooks/post-tool-use.ts +1 -0
  45. package/src/hooks/session-start.ts +1 -0
  46. package/src/lib/fileLock.ts +203 -0
  47. package/src/lib/installLock.ts +31 -144
  48. package/src/lsp/auto-detect.ts +10 -1
  49. package/src/lsp/client.ts +188 -2
  50. package/src/memory-file-ingest.ts +1 -0
  51. package/src/security/adapter-origin.ts +130 -0
  52. package/src/security/adapter-verifier.ts +319 -0
  53. package/src/security/atomic-write.ts +164 -0
  54. package/src/security/fetcher.ts +200 -0
  55. package/src/security/install-tracking.ts +319 -0
  56. package/src/security/local-fingerprint.ts +225 -0
  57. package/src/security/manifest-cache.ts +333 -0
  58. package/src/security/manifest-schema.ts +129 -0
  59. package/src/security/registry-pubkey.generated.ts +35 -0
  60. package/src/security/telemetry.ts +320 -0
  61. package/src/watch/daemon.ts +1 -1
  62. package/src/watch/paths.ts +2 -2
  63. package/templates/aspnet/massu.config.yaml +57 -0
  64. package/templates/go-chi/massu.config.yaml +52 -0
  65. package/templates/phoenix/massu.config.yaml +54 -0
  66. package/templates/python-flask/massu.config.yaml +51 -0
  67. package/templates/rails/massu.config.yaml +56 -0
  68. package/templates/spring/massu.config.yaml +56 -0
@@ -292,11 +292,31 @@ var WatchConfigSchema = z.object({
292
292
  max_watched_files: z.number().int().positive().default(1e4),
293
293
  paths_full_root_opt_in: z.boolean().default(false)
294
294
  }).passthrough().optional();
295
+ var AdapterLocalPathSchema = z.string().refine((s) => !/^([A-Za-z]:[\\/]|[\\/])/.test(s), {
296
+ message: "absolute paths are rejected; adapters.local entries must be relative to the massu.config.yaml directory"
297
+ }).refine((s) => !s.split(/[\\/]/).includes(".."), {
298
+ message: "parent-directory traversal (`..`) is rejected; adapters.local entries must stay inside the project tree"
299
+ }).transform((s) => s.split(/[\\/]/).filter((part) => part !== "" && part !== ".").join("/"));
300
+ var AdaptersConfigSchema = z.object({
301
+ enabled: z.boolean().default(false),
302
+ local: z.array(AdapterLocalPathSchema).default([])
303
+ }).passthrough().optional();
304
+ var TelemetryConfigSchema = z.object({
305
+ adapters: z.boolean().default(false)
306
+ }).passthrough().optional();
295
307
  var LSPConfigSchema = z.object({
296
308
  enabled: z.boolean().default(false),
297
309
  servers: z.array(z.object({
298
310
  language: z.string(),
299
- command: z.string()
311
+ command: z.string(),
312
+ // F-014 (closed 2026-05-06): explicit opt-in to spawn SUID/SGID
313
+ // binaries. Default false — argv[0] with the SUID bit is rejected
314
+ // unless this is true. Decision is auditable in the YAML.
315
+ allow_setuid: z.boolean().default(false),
316
+ // F-015 (closed 2026-05-06): per-server RSS budget (MB). Watchdog
317
+ // SIGKILLs the server after sustained breach. Default 1024 MB.
318
+ // Set to 0 to disable the watchdog for this server.
319
+ max_rss_mb: z.number().int().nonnegative().default(1024)
300
320
  })).default([]),
301
321
  autoDetect: z.object({
302
322
  viaPortScan: z.boolean().default(false)
@@ -339,6 +359,10 @@ var RawConfigSchema = z.object({
339
359
  detected: DetectedConfigSchema,
340
360
  // Plan 3a: file-watcher daemon tunables
341
361
  watch: WatchConfigSchema,
362
+ // Plan 3c: third-party adapter registry kill-switch + signing override + local-path opt-in.
363
+ adapters: AdaptersConfigSchema,
364
+ // Plan 3c: anonymous adapter-discovery telemetry opt-in (default off).
365
+ telemetry: TelemetryConfigSchema,
342
366
  // Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
343
367
  lsp: LSPConfigSchema.optional()
344
368
  }).passthrough();
@@ -450,6 +474,8 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
450
474
  detection: parsed.detection,
451
475
  detected: parsed.detected,
452
476
  watch: parsed.watch,
477
+ adapters: parsed.adapters,
478
+ telemetry: parsed.telemetry,
453
479
  lsp: parsed.lsp
454
480
  };
455
481
  if (!_config.cloud?.apiKey && process.env.MASSU_API_KEY) {
@@ -0,0 +1,207 @@
1
+ # Authoring a Massu Adapter
2
+
3
+ > Plan 3c gap-31 deliverable. Documents the adapter authoring workflow for
4
+ > third-party `@massu/adapter-*` packages and project-local TypeScript
5
+ > adapters listed in `massu.config.yaml > adapters.local`.
6
+
7
+ ## Quickstart
8
+
9
+ ```bash
10
+ mkdir my-adapter && cd my-adapter
11
+ npm init -y
12
+ npm install --save-peer @massu/core
13
+ ```
14
+
15
+ Edit `package.json`:
16
+
17
+ ```json
18
+ {
19
+ "name": "@your-org/adapter-yourframework",
20
+ "version": "0.1.0",
21
+ "type": "module",
22
+ "main": "dist/index.js",
23
+ "massu-adapter": true,
24
+ "massu-adapter-api-version": "1",
25
+ "peerDependencies": { "@massu/core": ">=1.5.0 <2.0.0" }
26
+ }
27
+ ```
28
+
29
+ Create `src/index.ts`:
30
+
31
+ ```typescript
32
+ import { defineAdapter, type CodebaseAdapter } from '@massu/core/adapter';
33
+
34
+ export default defineAdapter({
35
+ id: 'your-framework',
36
+ languages: ['typescript'],
37
+
38
+ matches(signals) {
39
+ return Boolean(
40
+ signals.packageJson?.dependencies?.['your-framework'] ??
41
+ signals.packageJson?.devDependencies?.['your-framework'],
42
+ );
43
+ },
44
+
45
+ async introspect(files, rootDir) {
46
+ // Run Tree-sitter queries, walk ASTs, sample files…
47
+ // The runner has already pre-read `files`; do NOT re-read from disk inside introspect.
48
+ return {
49
+ conventions: {
50
+ router: 'your-framework',
51
+ // …whatever fields your framework's variant template consumes
52
+ },
53
+ provenance: [
54
+ // Each field's provenance: where in the codebase + which query found it.
55
+ { field: 'router', value: 'src/main.ts:12', query: 'package.json dependency' },
56
+ ],
57
+ confidence: 'high', // 'high' | 'medium' | 'low' | 'none'
58
+ };
59
+ },
60
+ });
61
+ ```
62
+
63
+ Build + publish:
64
+
65
+ ```bash
66
+ npm run build
67
+ npm pack # smoke-test the tarball
68
+ npm publish --access public
69
+ ```
70
+
71
+ ## The contract
72
+
73
+ Every adapter package's default export MUST conform to `CodebaseAdapter`:
74
+
75
+ ```typescript
76
+ interface CodebaseAdapter {
77
+ id: string; // stable kebab-case id
78
+ languages: TreeSitterLanguage[]; // languages this adapter consumes
79
+ matches(signals: DetectionSignals): boolean; // cheap signal check, NO file IO
80
+ introspect(files: SourceFile[], rootDir: string): Promise<AdapterResult>;
81
+ }
82
+ ```
83
+
84
+ `defineAdapter` is a no-op identity function at runtime; it exists so adapter
85
+ authors get IDE autocomplete + compile-time type errors for missing or
86
+ mistyped fields. Use it instead of `const adapter: CodebaseAdapter = { ... }`
87
+ for cleaner author ergonomics.
88
+
89
+ ### `matches(signals)` rules
90
+
91
+ - Cheap: NO file IO, NO async work, NO network. Returns `boolean` synchronously.
92
+ - Idempotent: same `signals` input → same return.
93
+ - The runner builds `signals` once per project scan and passes the same
94
+ reference to every adapter; do NOT mutate.
95
+
96
+ ### `introspect(files, rootDir)` rules
97
+
98
+ - May be slow + async. The runner isolates failures (a thrown error in
99
+ one adapter does NOT abort the whole scan; the runner records it under
100
+ `MergedAdapterOutput.errored`).
101
+ - `files` is pre-read by the runner. Re-reading from disk inside
102
+ `introspect` defeats the runner's sampling discipline + breaks tests.
103
+ - Return per-field `provenance` so consumers can trace where each
104
+ convention came from. `confidence` is per-adapter for v1; field-level
105
+ confidence is reserved for a future major version.
106
+
107
+ ## Three trust classes
108
+
109
+ The Massu adapter loader classifies every adapter into exactly one of three
110
+ trust classes. Pick the right one for your adapter:
111
+
112
+ | Class | When to use | Verification |
113
+ |---|---|---|
114
+ | **CORE-BUNDLED** | You contributed your adapter to `@massu/core` itself. | Inherits trust from `@massu/core`'s npm provenance. No per-load signature check. |
115
+ | **REGISTRY-VERIFIED** | You publish `@your-org/adapter-yourframework` separately on npm. | Manifest at `https://registry.massu.ai/adapters/manifest.json` lists the package + per-version sha256. Loader verifies the Ed25519 manifest signature + per-package sha256 before loading. |
116
+ | **LOCAL-EXPLICIT** | You have a project-internal TypeScript adapter that should NOT be published. | Listed in `massu.config.yaml > adapters.local` (POSIX-relative path). Loader checks the `~/.massu/adapters-local-fingerprint.json` sentinel — operator must run `massu adapters add-local <path>` to acknowledge each entry. |
117
+
118
+ Choose REGISTRY-VERIFIED for any community-contributed adapter. The
119
+ `@your-org/` prefix is up to you; the loader accepts any package whose
120
+ `package.json` declares `"massu-adapter": true` and whose name matches a
121
+ manifest entry.
122
+
123
+ ## Submitting a REGISTRY-VERIFIED adapter
124
+
125
+ Adapter packages are added to the signed registry manifest by the Massu
126
+ maintainer (`ethankowen-73`) via PR review. The flow:
127
+
128
+ 1. Author your package per the Quickstart above.
129
+ 2. Publish to npm: `npm publish --access public`.
130
+ 3. Open a PR against `https://github.com/massu-ai/massu` proposing the
131
+ manifest entry. The PR body should include:
132
+ - Package name + version
133
+ - sha256 of the published tarball (from `npm view <pkg>@<version> dist.shasum`)
134
+ - Brief description of what the adapter detects
135
+ 4. The maintainer reviews the package source, audits the introspect
136
+ logic for resource use + secrets handling, verifies the sha256 matches
137
+ the npm tarball, signs the updated manifest with the registry
138
+ Ed25519 key, and deploys to `https://registry.massu.ai`.
139
+
140
+ Until the manifest is updated + redeployed, the loader will refuse your
141
+ adapter with a "not in the signed registry manifest" error. There is no
142
+ unsigned-loading bypass — the prior `allow_unsigned` config flag was
143
+ removed in CR-9 audit C2 because it was a tripwire: parsed but never
144
+ consulted by any callsite, creating a footgun for future contributors
145
+ who might wire it incorrectly. Authors who want to test their adapter
146
+ locally before the manifest is updated should use the LOCAL-EXPLICIT
147
+ class instead (path entry in `adapters.local`), which is the supported
148
+ operator-acknowledged loading path for in-development adapters.
149
+
150
+ ## LOCAL-EXPLICIT adapter authoring
151
+
152
+ For project-internal adapters that you do not want to publish, list the
153
+ TS file path under `adapters.local`:
154
+
155
+ ```yaml
156
+ adapters:
157
+ local:
158
+ - adapters/internal-thing.ts
159
+ ```
160
+
161
+ Then run:
162
+
163
+ ```bash
164
+ npx massu adapters add-local adapters/internal-thing.ts
165
+ ```
166
+
167
+ The CLI:
168
+ 1. Validates the path (rejects absolute paths + parent traversal; normalizes to POSIX).
169
+ 2. Appends to `massu.config.yaml > adapters.local` preserving comments.
170
+ 3. Updates `~/.massu/adapters-local-fingerprint.json` so the loader
171
+ recognizes the new entry as operator-acknowledged.
172
+
173
+ To remove:
174
+
175
+ ```bash
176
+ npx massu adapters remove-local adapters/internal-thing.ts
177
+ ```
178
+
179
+ If you edit `massu.config.yaml > adapters.local` directly (instead of
180
+ via the CLI), the loader will refuse to load any local adapter on the
181
+ next run — it cannot tell whether the change was operator-intentional
182
+ or a malicious postinstall script. To re-acknowledge the current state:
183
+
184
+ ```bash
185
+ npx massu adapters resync-local-fingerprint
186
+ ```
187
+
188
+ This is the only-supported escape hatch for "I edited the yaml directly
189
+ and I trust the result."
190
+
191
+ ## Stability commitment
192
+
193
+ Every export in `@massu/core/adapter` is part of the SemVer-stable surface.
194
+ Breaking changes to `CodebaseAdapter` (renamed fields, removed methods)
195
+ require a major version bump of `@massu/core` AND adapter packages
196
+ declaring `"massu-adapter-api-version": "1"` will be refused at startup
197
+ under the new major. This is intentional — the contract change requires
198
+ adapter authors to opt-in to the new shape.
199
+
200
+ Additive changes (new optional fields on result types, new
201
+ TreeSitterLanguage enum entries) are minor-version compatible.
202
+
203
+ ## See also
204
+
205
+ - [`SECURITY.md`](./SECURITY.md) — signing model, key rotation, supply-chain risks
206
+ - [`@massu/core/adapter`](../src/adapter.ts) — the SDK source
207
+ - Plan 3c (internal): adapter registry + supply-chain security architecture
@@ -0,0 +1,250 @@
1
+ # Security Model
2
+
3
+ > Plan 3c gap-29, gap-47, gap-54, gap-61 deliverable. Documents the
4
+ > Massu adapter trust model: signing, verification, key rotation, three
5
+ > trust classes, and the supply-chain risks the design mitigates.
6
+
7
+ ## Threat model
8
+
9
+ Massu adapters run **unsandboxed** inside the host Node process. An
10
+ adapter that's compromised at install or load time can do anything the
11
+ host process can do — read environment variables, write files, make
12
+ outbound HTTP requests, etc. Sandboxing (Node `vm.Context` isolation
13
+ or worker-thread separation with a defined adapter API surface) is a
14
+ deferred 40h+ scope; for the foreseeable future, mitigations are
15
+ **signature/sha-pinning + revocation**, not isolation.
16
+
17
+ The supply-chain attack surface is the largest in the entire Plan 3
18
+ series. The defenses are layered:
19
+
20
+ 1. **Three-class trust model** — every adapter classifies into exactly
21
+ one origin (CORE-BUNDLED / REGISTRY-VERIFIED / LOCAL-EXPLICIT).
22
+ Anything that doesn't classify refuses to load.
23
+ 2. **Signed manifest** — REGISTRY-VERIFIED adapters appear in a
24
+ per-release Ed25519-signed manifest at
25
+ `https://registry.massu.ai/adapters/manifest.json`. The signature
26
+ is verified against a public key bundled inside `@massu/core`.
27
+ 3. **Per-package sha256** — each manifest entry pins a sha256 of the
28
+ adapter's published tarball. Tampering with the unpacked package
29
+ in `node_modules` is detected at load time (gap-37 follow-up).
30
+ 4. **Postinstall-poisoning fingerprint** — `adapters.local` mutations
31
+ require an operator-CLI sentinel file. A malicious postinstall script
32
+ that mutates `massu.config.yaml > adapters.local` is detected at
33
+ startup; the loader refuses all local adapters until the operator
34
+ re-acknowledges via `massu adapters resync-local-fingerprint`.
35
+ 5. **Strict cache schema** — cached manifest at
36
+ `~/.massu/adapter-manifest.json` parses through Zod; corrupted or
37
+ unknown-shape entries are dropped without disclosing why.
38
+ 6. **Telemetry strictness** — the optional adapter-discovery telemetry
39
+ (off by default) emits ONLY four allowlisted fields, enforced at
40
+ write AND replay time by a `.strict()` Zod schema. PII keys are
41
+ rejected at write time, never persisted.
42
+
43
+ ## Three trust classes
44
+
45
+ | Class | Trust derives from | Verification path |
46
+ |---|---|---|
47
+ | **CORE-BUNDLED** | `@massu/core`'s own npm publish + `prepublish-check.sh` audit + your `npm install @massu/core` choice | Skips signature verification (this IS the trusted baseline) |
48
+ | **REGISTRY-VERIFIED** | The signed `manifest.json` at registry.massu.ai | Manifest sig + per-package sha256 + `signing_key_id` rotation drift detection |
49
+ | **LOCAL-EXPLICIT** | The operator's per-path opt-in via `adapters.local` | `~/.massu/adapters-local-fingerprint.json` sentinel must match current config + be sourced from `cli` or `cli-resync` |
50
+
51
+ The loader REFUSES to load any adapter that doesn't classify into exactly
52
+ one class. Multi-class collisions (an id matching CORE-BUNDLED and
53
+ LOCAL-EXPLICIT simultaneously) also refuse with a clear stderr warning
54
+ naming all the matching classes.
55
+
56
+ ## Manifest signing
57
+
58
+ The registry manifest envelope at
59
+ `https://registry.massu.ai/adapters/manifest.json` has the following
60
+ structure:
61
+
62
+ ```json
63
+ {
64
+ "manifest": { "manifest_schema_version": 1, "issued_at": "...", "adapters": [...] },
65
+ "manifest_b64": "<base64 of the EXACT bytes that were signed>",
66
+ "signature": "<base64 Ed25519 signature>",
67
+ "manifest_sha256": "<sha256 hex of manifest_b64-decoded bytes>",
68
+ "signed_at": "<ISO8601>",
69
+ "signing_key_id": "<sha256 hex of the public key>"
70
+ }
71
+ ```
72
+
73
+ The `manifest_b64` field is required: it carries the byte-for-byte
74
+ input that `nacl.sign.detached(msg, priv)` was invoked over. The
75
+ verifier base64-decodes it, computes sha256, compares to
76
+ `manifest_sha256`, JSON-parses, deep-equals against `manifest`,
77
+ runs `nacl.sign.detached.verify` against the bundled public key, and
78
+ verifies that `signing_key_id == sha256(bundled-pubkey)`. Any step
79
+ failing → refuse the manifest. (The cache + the live registry both
80
+ ship the same envelope shape.)
81
+
82
+ ## Adapter signing model
83
+
84
+ For v1, **a single registry signing key signs the entire manifest**,
85
+ including entries for `@massu/`-org-published adapter packages
86
+ (rails, phoenix, aspnet, spring, go-chi) AND community contributions.
87
+ The `signing_key_id` field is in the per-entry shape but always equals
88
+ the same single registry key for v1; the field is reserved for a future
89
+ federated model where individual adapter publishers countersign their
90
+ own entries (deferred to a future major version).
91
+
92
+ Implication for community contributors: a third-party developer
93
+ publishing `@your-org/adapter-foo` on npm CANNOT have their package
94
+ added to the signed manifest without the Massu maintainer's review +
95
+ signature. The PR-to-manifest flow is documented in
96
+ [`AUTHORING-ADAPTERS.md`](./AUTHORING-ADAPTERS.md) under "Submitting a
97
+ REGISTRY-VERIFIED adapter."
98
+
99
+ The maintainer (`ethankowen-73`) holds the signing key in macOS
100
+ Keychain at `massu/registry/signing/private`. A backup maintainer
101
+ documented in this file's "Succession" section below is the single-
102
+ point-of-failure mitigation.
103
+
104
+ ## Key rotation
105
+
106
+ The Ed25519 keypair is rotated annually OR on suspected compromise.
107
+ Rotation procedure:
108
+
109
+ 1. Generate a new keypair via `tweetnacl` (operator-only, store private
110
+ in macOS Keychain replacing the prior entry).
111
+ 2. Update `packages/core/security/registry-pubkey.{pem,b64,env}` with
112
+ the new public key bytes in all 3 formats (PEM-wrapped, raw base64,
113
+ env-var format).
114
+ 3. Append the new RAW-bytes sha256 to `KNOWN_PUBKEY_FINGERPRINTS` in
115
+ `scripts/bundle-pubkey.mjs` (DO NOT remove the old entry — rotation
116
+ grace window).
117
+ 4. Run `bash scripts/bundle-pubkey.mjs` to regenerate
118
+ `packages/core/src/security/registry-pubkey.generated.ts`.
119
+ 5. Sign a transition manifest **countersigned by both old AND new keys**
120
+ so consumers running pre-rotation `@massu/core` can still verify
121
+ under the old key during the grace window. (The Phase 5 verifier
122
+ accepts manifests countersigned by ANY entry in the `KNOWN_PUBKEY_FINGERPRINTS`
123
+ allowlist during the rotation grace window.)
124
+ 6. Ship a new `@massu/core` minor release bundling the new pubkey;
125
+ document the rotation in the release CHANGELOG.
126
+ 7. Old-key-only verification remains accepted until the NEXT minor
127
+ release after rotation, at which point old-key entries are removed
128
+ from `KNOWN_PUBKEY_FINGERPRINTS`.
129
+
130
+ The cached manifest at `~/.massu/adapter-manifest.json` records the
131
+ `bundled_pubkey_fingerprint` of the @massu/core that wrote the cache.
132
+ On read, if the cache's fingerprint != currently-running @massu/core's
133
+ bundled pubkey fingerprint, the cache is treated as STALE-DUE-TO-ROTATION
134
+ and the loader forces a fresh fetch from the registry. This catches the
135
+ upgrade case where an operator runs `npm install -g @massu/core@latest`
136
+ mid-rotation and would otherwise hold a manifest signed under the old
137
+ key.
138
+
139
+ ## Postinstall-poisoning defense
140
+
141
+ `adapters.local` listings bypass the registry-signed allowlist (operators
142
+ opt-in per-path). To prevent malicious npm postinstall scripts from
143
+ mutating `adapters.local` to inject attacker-controlled paths, the
144
+ loader checks a sentinel file at `~/.massu/adapters-local-fingerprint.json`:
145
+
146
+ ```json
147
+ {
148
+ "fingerprint": "<sha256 hex of canonical-stringified sorted adapters.local array>",
149
+ "source": "cli" | "cli-resync",
150
+ "ts": "<ISO8601>"
151
+ }
152
+ ```
153
+
154
+ Any time the operator runs `massu adapters add-local <path>`, `massu adapters
155
+ remove-local <path>`, or `massu adapters resync-local-fingerprint`, the
156
+ sentinel is updated with `source: "cli"` (or `cli-resync`). At loader
157
+ startup, the current `massu.config.yaml > adapters.local` content's
158
+ fingerprint is compared to the sentinel:
159
+
160
+ - **Match** → proceed to load LOCAL-EXPLICIT adapters.
161
+ - **Drift OR sentinel absent** → REFUSE all LOCAL-EXPLICIT adapters
162
+ with a stderr warning naming the divergence + pointing the operator
163
+ at `massu adapters resync-local-fingerprint`.
164
+
165
+ A malicious postinstall script could, in principle, also mutate the
166
+ sentinel. Mitigations:
167
+
168
+ - The sentinel is mode `0o600` (owner-only). A postinstall script running
169
+ as the same user CAN write to it, but doing so requires advance
170
+ knowledge of the file format AND the canonical-fingerprint scheme. The
171
+ schema is `.strict()`, so any unknown-key write is rejected at
172
+ read time (treated as "no sentinel").
173
+ - A future hardening (gap-32 follow-up) could require a HMAC over the
174
+ sentinel using a key only the CLI knows, but the CLI is itself
175
+ operator-installed, so HMAC keys would need to derive from operator
176
+ state outside the npm tree.
177
+
178
+ ## Telemetry posture
179
+
180
+ Telemetry is **off by default**. Enable via:
181
+
182
+ ```yaml
183
+ telemetry:
184
+ adapters: true
185
+ ```
186
+
187
+ When enabled, the adapter-discovery writer emits ONE JSONL line per
188
+ discovery event matching this schema (`.strict()` so unknown keys are
189
+ rejected):
190
+
191
+ ```typescript
192
+ {
193
+ adapter_id: string; // canonical id (e.g. "@massu/adapter-rails")
194
+ count: number; // discovery events observed in this batch
195
+ version: string; // adapter version when known
196
+ ts: string; // ISO8601
197
+ }
198
+ ```
199
+
200
+ Specifically:
201
+
202
+ - File paths are NEVER sent.
203
+ - Symbol names are NEVER sent.
204
+ - Source code content is NEVER sent.
205
+ - Project names are NEVER sent.
206
+ - Operator identity is NEVER sent.
207
+
208
+ The transport is HTTPS POST to `https://telemetry.massu.ai/adapter-discovery`
209
+ through the same allowlisted fetcher (`packages/core/src/security/fetcher.ts`)
210
+ that the registry uses. Pending events buffer at
211
+ `~/.massu/telemetry-pending.jsonl` (mode `0o600`) when the endpoint is
212
+ unreachable, with a 1MB / 1000-entry hard cap. Replay re-validates every
213
+ entry against the same `.strict()` schema before sending; entries that
214
+ fail re-validation are dropped without sending. Disabling telemetry
215
+ mid-flight stops both new records AND any pending replay.
216
+
217
+ ## Succession
218
+
219
+ The npm `@massu` org is currently owned by `ethankowen-73`. Backup
220
+ maintainer assignment (open action — required before Phase 9
221
+ publish per the canonical plan): a second account must hold
222
+ `Maintainer` role on the `@massu` org so deprecate / unpublish capability
223
+ is not single-point-of-failure on holiday or illness. Verify via
224
+ `npm org ls @massu` showing ≥2 maintainers before the next minor
225
+ release ships.
226
+
227
+ ## Reporting a vulnerability
228
+
229
+ If you discover a supply-chain or signing-flow vulnerability in
230
+ `@massu/core` or any registry-listed adapter, do NOT open a public
231
+ GitHub issue. Email `security@massu.ai` with the details (mailbox
232
+ provisioning is an open action — required before Phase 9 publish
233
+ per the canonical plan). The maintainer will:
234
+
235
+ 1. Acknowledge within 48h.
236
+ 2. Investigate + reproduce.
237
+ 3. Issue a CVE if confirmed.
238
+ 4. Publish a patched `@massu/core` release with `npm deprecate` on the
239
+ affected versions.
240
+ 5. Add the affected adapter to the manifest's `unpublished: true` list
241
+ if applicable, so all consumers refuse to load on next refresh.
242
+
243
+ ## See also
244
+
245
+ - [`AUTHORING-ADAPTERS.md`](./AUTHORING-ADAPTERS.md) — how to write a
246
+ conformant adapter
247
+ - `packages/core/src/security/` — the verifier, fetcher, atomic-write,
248
+ fingerprint, and cache modules implementing this model
249
+ - Plan 3c (internal): full architectural background + threat-model
250
+ decisions
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.4.0-soak.0",
3
+ "version": "1.5.0",
4
4
  "type": "module",
5
5
  "description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 72 total), 55+ workflow commands, 11 agents, 20+ patterns",
6
6
  "main": "src/server.ts",
7
+ "exports": {
8
+ ".": "./src/server.ts",
9
+ "./adapter": "./src/adapter.ts"
10
+ },
7
11
  "bin": {
8
12
  "massu": "./dist/cli.js"
9
13
  },
@@ -13,7 +17,7 @@
13
17
  "build": "tsc --noEmit && npm run build:cli && npm run build:hooks",
14
18
  "build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
15
19
  "build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
16
- "prepublishOnly": "bash ../../scripts/prepublish-check.sh && npm run build",
20
+ "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && npm run build",
17
21
  "bench:watch": "tsx test/perf/watch-benchmark.ts"
18
22
  },
19
23
  "dependencies": {
@@ -23,8 +27,10 @@
23
27
  "fast-glob": "^3.3.0",
24
28
  "proper-lockfile": "^4.1.2",
25
29
  "smol-toml": "^1.3.0",
30
+ "tar": "^7.4.3",
31
+ "tweetnacl": "^1.0.3",
26
32
  "vscode-languageserver-protocol": "^3.17.5",
27
- "web-tree-sitter": "^0.26.8",
33
+ "web-tree-sitter": "~0.25.10",
28
34
  "yaml": "^2.4.0",
29
35
  "zod": "^3.23.0"
30
36
  },
@@ -48,6 +54,7 @@
48
54
  "dist/**/*",
49
55
  "commands/**/*",
50
56
  "agents/**/*",
57
+ "docs/**/*",
51
58
  "patterns/**/*",
52
59
  "protocols/**/*",
53
60
  "reference/**/*",
package/src/adapter.ts ADDED
@@ -0,0 +1,90 @@
1
+ /**
2
+ * `@massu/core/adapter` — public adapter authoring SDK (Plan 3c gap-31, gap-35).
3
+ *
4
+ * Adapter authors import from this subpath ONLY; everything inside @massu/core
5
+ * outside this entry point is implementation detail subject to change without
6
+ * a major version bump. The contract surface here is part of the SemVer-
7
+ * stable API.
8
+ *
9
+ * Usage (adapter package author):
10
+ *
11
+ * import { defineAdapter, type CodebaseAdapter } from '@massu/core/adapter';
12
+ *
13
+ * export default defineAdapter({
14
+ * id: 'rails-active-record',
15
+ * languages: ['ruby'],
16
+ * matches(signals) {
17
+ * return Boolean(signals.gemfile?.includes('rails'));
18
+ * },
19
+ * async introspect(files, rootDir) {
20
+ * // Tree-sitter queries, AST walks, file sampling…
21
+ * return {
22
+ * conventions: { router: 'rails' },
23
+ * provenance: [{ field: 'router', value: 'config/routes.rb:1', query: 'gemfile' }],
24
+ * confidence: 'high',
25
+ * };
26
+ * },
27
+ * });
28
+ *
29
+ * Then in the adapter package's package.json:
30
+ * {
31
+ * "name": "@massu/adapter-rails",
32
+ * "version": "0.1.0",
33
+ * "main": "dist/index.js",
34
+ * "type": "module",
35
+ * "massu-adapter": true,
36
+ * "massu-adapter-api-version": "1",
37
+ * "peerDependencies": { "@massu/core": ">=1.5.0 <2.0.0" }
38
+ * }
39
+ *
40
+ * The adapter loader (Plan 3b runner.ts) does
41
+ * const mod = await import(`<package-dir>/${main}`);
42
+ * const adapter = (mod.default ?? mod) as CodebaseAdapter;
43
+ * and dispatches matches() + introspect() accordingly.
44
+ *
45
+ * defineAdapter is a NO-OP at runtime — it's an identity function that
46
+ * exists for compile-time type narrowing (so adapter authors get IDE
47
+ * autocomplete + type errors for missing fields). The factory's only job
48
+ * is to anchor the type contract; it does NOT validate at runtime, register
49
+ * anywhere, or mutate state. Runtime validation happens at the loader
50
+ * (Plan 3b runner.ts) which checks the dispatched object shape before
51
+ * invoking matches/introspect.
52
+ *
53
+ * Stability: every export here is part of @massu/core's public SemVer
54
+ * surface. Breaking changes to the CodebaseAdapter shape (renamed fields,
55
+ * removed methods) require a major version bump per the
56
+ * massu-adapter-api-version contract. Adapter packages declare
57
+ * `"massu-adapter-api-version": "1"` so the loader refuses incompatible
58
+ * majors at startup.
59
+ */
60
+
61
+ export {
62
+ // The contract every adapter package must implement.
63
+ type CodebaseAdapter,
64
+ // Inputs the runner provides to matches() / introspect().
65
+ type DetectionSignals,
66
+ type SourceFile,
67
+ type TreeSitterLanguage,
68
+ // Output shapes from introspect() + the runner's merge step.
69
+ type AdapterResult,
70
+ type Provenance,
71
+ type AdapterResolved,
72
+ type MergedAdapterOutput,
73
+ } from './detect/adapters/types.js';
74
+
75
+ import type { CodebaseAdapter } from './detect/adapters/types.js';
76
+
77
+ /**
78
+ * Identity factory — narrows the input's type to `CodebaseAdapter` so
79
+ * authors get compile-time checking + IDE autocomplete for missing /
80
+ * mistyped fields. Runtime: returns the input unchanged. Use this in
81
+ * place of an inline `const adapter: CodebaseAdapter = { ... }`
82
+ * annotation.
83
+ *
84
+ * Returning the input (instead of `void`) means adapter packages can do
85
+ * `export default defineAdapter({ ... })` and the loader's
86
+ * `mod.default` destructuring just works.
87
+ */
88
+ export function defineAdapter(spec: CodebaseAdapter): CodebaseAdapter {
89
+ return spec;
90
+ }
package/src/cli.ts CHANGED
@@ -73,6 +73,12 @@ async function main(): Promise<void> {
73
73
  await handleConfigSubcommand(args.slice(1));
74
74
  break;
75
75
  }
76
+ case 'adapters': {
77
+ const { handleAdaptersSubcommand } = await import('./commands/adapters.ts');
78
+ const result = await handleAdaptersSubcommand(args.slice(1));
79
+ process.exit(result.exitCode);
80
+ return;
81
+ }
76
82
  case '--help':
77
83
  case '-h': {
78
84
  printHelp();
@@ -162,6 +168,7 @@ Commands:
162
168
  refresh-log [N] Show the last N watcher auto-refresh events
163
169
  validate-config Validate massu.config.yaml (alias: config validate)
164
170
  config <sub> Config lifecycle: refresh | validate | upgrade | doctor | check-drift
171
+ adapters <sub> Third-party adapter registry: list | refresh | search | add-local | remove-local | install | resign
165
172
 
166
173
  Options:
167
174
  --help, -h Show this help message