@massu/core 1.4.0 → 1.5.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.
- package/dist/cli.js +9431 -5167
- package/dist/hooks/auto-learning-pipeline.js +18 -0
- package/dist/hooks/classify-failure.js +18 -0
- package/dist/hooks/cost-tracker.js +18 -0
- package/dist/hooks/fix-detector.js +18 -0
- package/dist/hooks/incident-pipeline.js +18 -0
- package/dist/hooks/post-edit-context.js +18 -0
- package/dist/hooks/post-tool-use.js +18 -0
- package/dist/hooks/pre-compact.js +18 -0
- package/dist/hooks/pre-delete-check.js +18 -0
- package/dist/hooks/quality-event.js +18 -0
- package/dist/hooks/rule-enforcement-pipeline.js +18 -0
- package/dist/hooks/session-end.js +18 -0
- package/dist/hooks/session-start.js +2952 -2740
- package/dist/hooks/user-prompt.js +18 -0
- package/docs/AUTHORING-ADAPTERS.md +207 -0
- package/docs/SECURITY.md +250 -0
- package/package.json +7 -3
- package/src/adapter.ts +90 -0
- package/src/cli.ts +7 -0
- package/src/commands/adapters.ts +824 -0
- package/src/commands/config-check-drift.ts +1 -0
- package/src/commands/config-refresh.ts +1 -0
- package/src/commands/config-upgrade.ts +1 -0
- package/src/commands/doctor.ts +2 -0
- package/src/commands/init.ts +151 -2
- package/src/config.ts +63 -0
- package/src/detect/adapters/aspnet.ts +293 -0
- package/src/detect/adapters/discover.ts +469 -0
- package/src/detect/adapters/go-chi.ts +261 -0
- package/src/detect/adapters/index.ts +49 -0
- package/src/detect/adapters/phoenix.ts +277 -0
- package/src/detect/adapters/python-flask.ts +235 -0
- package/src/detect/adapters/rails.ts +279 -0
- package/src/detect/adapters/runner.ts +32 -0
- package/src/detect/adapters/spring.ts +284 -0
- package/src/detect/adapters/tree-sitter-loader.ts +50 -0
- package/src/detect/adapters/types.ts +18 -0
- package/src/detect/framework-detector.ts +26 -0
- package/src/detect/manifest-registry.ts +261 -0
- package/src/detect/monorepo-detector.ts +1 -0
- package/src/detect/package-detector.ts +162 -62
- package/src/detect/source-dir-detector.ts +7 -0
- package/src/hooks/post-tool-use.ts +1 -0
- package/src/hooks/session-start.ts +1 -0
- package/src/lib/fileLock.ts +203 -0
- package/src/lib/installLock.ts +31 -144
- package/src/memory-file-ingest.ts +1 -0
- package/src/security/adapter-origin.ts +130 -0
- package/src/security/adapter-verifier.ts +319 -0
- package/src/security/atomic-write.ts +164 -0
- package/src/security/fetcher.ts +200 -0
- package/src/security/install-tracking.ts +319 -0
- package/src/security/local-fingerprint.ts +225 -0
- package/src/security/manifest-cache.ts +333 -0
- package/src/security/manifest-schema.ts +129 -0
- package/src/security/registry-pubkey.generated.ts +35 -0
- package/src/security/telemetry.ts +320 -0
- package/templates/aspnet/massu.config.yaml +61 -0
- package/templates/go-chi/massu.config.yaml +52 -0
- package/templates/phoenix/massu.config.yaml +54 -0
- package/templates/python-flask/massu.config.yaml +51 -0
- package/templates/rails/massu.config.yaml +56 -0
- package/templates/spring/massu.config.yaml +56 -0
|
@@ -292,6 +292,18 @@ 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({
|
|
@@ -347,6 +359,10 @@ var RawConfigSchema = z.object({
|
|
|
347
359
|
detected: DetectedConfigSchema,
|
|
348
360
|
// Plan 3a: file-watcher daemon tunables
|
|
349
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,
|
|
350
366
|
// Plan 3b Phase 4: optional LSP enrichment of AST adapter results.
|
|
351
367
|
lsp: LSPConfigSchema.optional()
|
|
352
368
|
}).passthrough();
|
|
@@ -458,6 +474,8 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
458
474
|
detection: parsed.detection,
|
|
459
475
|
detected: parsed.detected,
|
|
460
476
|
watch: parsed.watch,
|
|
477
|
+
adapters: parsed.adapters,
|
|
478
|
+
telemetry: parsed.telemetry,
|
|
461
479
|
lsp: parsed.lsp
|
|
462
480
|
};
|
|
463
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
|
package/docs/SECURITY.md
ADDED
|
@@ -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.
|
|
3
|
+
"version": "1.5.1",
|
|
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": {
|
|
@@ -26,7 +30,7 @@
|
|
|
26
30
|
"tar": "^7.4.3",
|
|
27
31
|
"tweetnacl": "^1.0.3",
|
|
28
32
|
"vscode-languageserver-protocol": "^3.17.5",
|
|
29
|
-
"web-tree-sitter": "
|
|
33
|
+
"web-tree-sitter": "~0.25.10",
|
|
30
34
|
"yaml": "^2.4.0",
|
|
31
35
|
"zod": "^3.23.0"
|
|
32
36
|
},
|
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
|