@mandujs/core 0.53.2 → 0.54.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.
- package/README.md +654 -654
- package/package.json +2 -2
- package/src/a11y/__tests__/run-audit.test.ts +333 -333
- package/src/a11y/fix-hints.ts +76 -76
- package/src/a11y/index.ts +18 -18
- package/src/a11y/run-audit.ts +394 -394
- package/src/a11y/types.ts +125 -125
- package/src/auth/__tests__/login.test.ts +1 -1
- package/src/auth/__tests__/password.test.ts +122 -122
- package/src/auth/__tests__/tokens.test.ts +274 -274
- package/src/auth/__tests__/verification.test.ts +274 -274
- package/src/auth/index.ts +76 -76
- package/src/auth/login.ts +225 -225
- package/src/auth/password.ts +120 -120
- package/src/auth/reset.ts +243 -243
- package/src/auth/tokens.ts +612 -612
- package/src/auth/verification.ts +253 -253
- package/src/brain/__tests__/redactor.test.ts +94 -94
- package/src/brain/adapters/__tests__/_helpers.ts +83 -83
- package/src/brain/adapters/__tests__/anthropic-oauth.test.ts +196 -196
- package/src/brain/adapters/__tests__/chatgpt-auth.test.ts +193 -193
- package/src/brain/adapters/__tests__/openai-oauth.test.ts +209 -209
- package/src/brain/adapters/__tests__/resolver.test.ts +143 -143
- package/src/brain/adapters/anthropic-oauth.ts +1 -1
- package/src/brain/adapters/chatgpt-auth.ts +300 -300
- package/src/brain/adapters/index.ts +319 -319
- package/src/brain/adapters/oauth-flow.ts +439 -439
- package/src/brain/consent.ts +240 -240
- package/src/brain/credentials.ts +396 -396
- package/src/bundler/__tests__/build-runner.ts +113 -113
- package/src/bundler/__tests__/dev-reliability.test.ts +619 -619
- package/src/bundler/__tests__/extended-watch.test.ts +711 -711
- package/src/bundler/__tests__/fast-refresh.test.ts +10 -10
- package/src/bundler/__tests__/hdr.test.ts +353 -353
- package/src/bundler/__tests__/hmr-client.test.ts +532 -532
- package/src/bundler/__tests__/manifest-schema.test.ts +266 -266
- package/src/bundler/__tests__/prod-smoke.test.ts +138 -138
- package/src/bundler/__tests__/reverse-import-graph.test.ts +519 -519
- package/src/bundler/__tests__/slot-dispatch.test.ts +573 -573
- package/src/bundler/__tests__/url-cap-and-slot-regex.test.ts +286 -286
- package/src/bundler/__tests__/vendor-cache.test.ts +455 -455
- package/src/bundler/budget.ts +404 -404
- package/src/bundler/build.test.ts +179 -179
- package/src/bundler/build.ts +55 -55
- package/src/bundler/dev.ts +42 -42
- package/src/bundler/manifest-schema.ts +301 -301
- package/src/bundler/plugins/__tests__/block-generated-imports.test.ts +263 -263
- package/src/bundler/plugins/__tests__/react-compiler-config.test.ts +83 -83
- package/src/bundler/plugins/__tests__/react-compiler-lint.test.ts +110 -110
- package/src/bundler/plugins/block-generated-imports.ts +155 -155
- package/src/bundler/plugins/index.ts +83 -83
- package/src/bundler/plugins/react-compiler-config.ts +108 -108
- package/src/bundler/plugins/react-compiler-lint.ts +253 -253
- package/src/bundler/plugins/react-compiler.ts +162 -162
- package/src/bundler/reverse-import-graph.ts +339 -339
- package/src/bundler/safe-build.test.ts +201 -201
- package/src/bundler/safe-build.ts +103 -103
- package/src/bundler/scenario-matrix.ts +229 -229
- package/src/bundler/types.ts +10 -10
- package/src/bundler/vendor-cache-types.ts +130 -130
- package/src/bundler/vendor-cache.ts +526 -526
- package/src/change/snapshot.ts +18 -18
- package/src/client/Form.tsx +105 -105
- package/src/client/Link.tsx +9 -9
- package/src/client/__tests__/use-sse.test.ts +153 -153
- package/src/client/globals.ts +1 -1
- package/src/client/hooks.ts +362 -362
- package/src/client/hydrate.ts +340 -340
- package/src/client/prefetch-helper.ts +55 -55
- package/src/client/router.ts +11 -11
- package/src/client/runtime.ts +47 -47
- package/src/client/serialize.ts +404 -404
- package/src/client/use-fetch.ts +6 -6
- package/src/client/use-head.ts +197 -197
- package/src/client/use-sse.ts +378 -378
- package/src/client/window-state.ts +101 -101
- package/src/components/Image.tsx +162 -162
- package/src/config/validate.ts +3 -3
- package/src/config/watcher.ts +311 -311
- package/src/constants.ts +40 -40
- package/src/content/collection.ts +8 -8
- package/src/content/content-layer.ts +7 -7
- package/src/content/data-store.ts +245 -245
- package/src/content/frontmatter.ts +189 -189
- package/src/content/loader-context.ts +171 -171
- package/src/content/loaders/api.ts +216 -216
- package/src/content/loaders/file.ts +172 -172
- package/src/content/loaders/glob.ts +253 -253
- package/src/content/loaders/index.ts +34 -34
- package/src/content/loaders/types.ts +137 -137
- package/src/content/meta-store.ts +209 -209
- package/src/content/prebuild.test.ts +571 -571
- package/src/content/prebuild.ts +636 -636
- package/src/content/schema.ts +20 -20
- package/src/content/sidebar.ts +630 -630
- package/src/content/slug.ts +110 -110
- package/src/content/types.ts +282 -282
- package/src/content/watcher.ts +135 -135
- package/src/contract/client-safe.test.ts +42 -42
- package/src/contract/client-safe.ts +114 -114
- package/src/contract/define.ts +11 -11
- package/src/contract/index.ts +1 -1
- package/src/contract/normalize.test.ts +276 -276
- package/src/contract/normalize.ts +410 -410
- package/src/contract/registry.test.ts +206 -206
- package/src/contract/route-helpers.ts +1 -1
- package/src/contract/rpc.ts +443 -443
- package/src/contract/schema.ts +48 -48
- package/src/contract/types.ts +58 -58
- package/src/db/__tests__/db.test.ts +482 -482
- package/src/db/index.ts +138 -138
- package/src/db/migrations/history-table.ts +345 -345
- package/src/db/migrations/index.ts +3 -3
- package/src/db/migrations/lock.ts +324 -324
- package/src/db/migrations/runner.ts +650 -650
- package/src/deploy/cache.ts +140 -140
- package/src/deploy/compile/vercel.ts +344 -344
- package/src/deploy/index.ts +87 -87
- package/src/deploy/inference/brain.ts +268 -268
- package/src/deploy/inference/context.ts +182 -182
- package/src/deploy/inference/filling-extract.ts +245 -245
- package/src/deploy/inference/heuristic.ts +182 -182
- package/src/deploy/intent.ts +173 -173
- package/src/deploy/plan.ts +178 -178
- package/src/design/__tests__/agents-link.test.ts +109 -109
- package/src/design/__tests__/extract-patch-diff.test.ts +265 -265
- package/src/design/__tests__/lint.test.ts +110 -110
- package/src/design/__tests__/parser.test.ts +195 -195
- package/src/design/__tests__/tailwind-theme.test.ts +229 -229
- package/src/design/agents-link.ts +165 -165
- package/src/design/diff.ts +138 -138
- package/src/design/extract.ts +285 -285
- package/src/design/index.ts +102 -102
- package/src/design/lint.ts +209 -209
- package/src/design/parser.ts +555 -555
- package/src/design/patch.ts +241 -241
- package/src/design/scaffold.ts +147 -147
- package/src/design/tailwind-theme.ts +441 -441
- package/src/design/types.ts +210 -210
- package/src/desktop/__tests__/webview-fallback.test.ts +254 -254
- package/src/desktop/__tests__/window.test.ts +248 -248
- package/src/desktop/__tests__/worker.test.ts +266 -266
- package/src/desktop/index.ts +43 -43
- package/src/desktop/types.ts +158 -158
- package/src/desktop/worker.ts +180 -180
- package/src/dev-error-overlay/__tests__/overlay-injector.test.ts +241 -241
- package/src/dev-error-overlay/index.ts +30 -30
- package/src/dev-error-overlay/overlay-injector.ts +243 -243
- package/src/dev-error-overlay/overlay-styles.ts +52 -52
- package/src/dev-error-overlay/types.ts +66 -66
- package/src/devtools/ai/context-builder.ts +375 -375
- package/src/devtools/ai/index.ts +25 -25
- package/src/devtools/ai/mcp-connector.ts +25 -25
- package/src/devtools/client/catchers/error-catcher.ts +344 -344
- package/src/devtools/client/catchers/index.ts +18 -18
- package/src/devtools/client/components/index.ts +39 -39
- package/src/devtools/client/components/mandu-character.tsx +331 -331
- package/src/devtools/client/components/overlay.tsx +368 -368
- package/src/devtools/client/components/panel/errors-panel.tsx +259 -259
- package/src/devtools/client/components/panel/guard-panel.tsx +30 -30
- package/src/devtools/client/components/panel/islands-panel.tsx +320 -320
- package/src/devtools/client/components/panel/network-panel.tsx +291 -291
- package/src/devtools/client/components/panel/panel-container.tsx +500 -500
- package/src/devtools/client/components/panel/preview-panel.tsx +46 -46
- package/src/devtools/client/filters/context-filters.ts +282 -282
- package/src/devtools/client/filters/index.ts +16 -16
- package/src/devtools/client/index.ts +63 -63
- package/src/devtools/client/persistence.ts +335 -335
- package/src/devtools/hook/create-hook.ts +207 -207
- package/src/devtools/hook/index.ts +13 -13
- package/src/devtools/index.ts +439 -439
- package/src/devtools/init.ts +265 -265
- package/src/devtools/protocol.ts +237 -237
- package/src/devtools/server/index.ts +17 -17
- package/src/devtools/server/source-context.ts +450 -450
- package/src/devtools/types.ts +35 -35
- package/src/devtools/worker/index.ts +25 -25
- package/src/devtools/worker/redaction-worker.ts +233 -233
- package/src/devtools/worker/worker-manager.ts +410 -410
- package/src/diagnose/__tests__/checks.test.ts +451 -380
- package/src/diagnose/checks.ts +832 -720
- package/src/diagnose/index.ts +17 -16
- package/src/diagnose/run.ts +93 -91
- package/src/diagnose/types.ts +53 -53
- package/src/email/__tests__/email.test.ts +355 -355
- package/src/email/index.ts +282 -282
- package/src/email/smtp.ts +64 -64
- package/src/error/domains.ts +265 -265
- package/src/error/result.ts +60 -60
- package/src/error/types.ts +6 -6
- package/src/errors/extractor.ts +409 -409
- package/src/errors/index.ts +19 -19
- package/src/filling/__tests__/session-sqlite.test.ts +454 -454
- package/src/filling/auth.ts +308 -308
- package/src/filling/cookie-codec.ts +299 -299
- package/src/filling/deps.ts +265 -265
- package/src/filling/session-sqlite.ts +617 -617
- package/src/filling/sse.ts +5 -5
- package/src/filling/ws.ts +78 -78
- package/src/generator/index.ts +3 -3
- package/src/guard/__tests__/design-inline-class.test.ts +219 -219
- package/src/guard/__tests__/tsgolint-bridge.test.ts +347 -347
- package/src/guard/analyzer.ts +360 -360
- package/src/guard/auto-correct.ts +1 -1
- package/src/guard/contract-guard.ts +9 -9
- package/src/guard/define-rule.ts +243 -243
- package/src/guard/design-inline-class.ts +393 -393
- package/src/guard/file-type.test.ts +24 -24
- package/src/guard/graph.ts +1 -1
- package/src/guard/healing.ts +36 -36
- package/src/guard/presets/atomic.ts +70 -70
- package/src/guard/presets/clean.ts +77 -77
- package/src/guard/presets/fsd.ts +79 -79
- package/src/guard/presets/hexagonal.ts +68 -68
- package/src/guard/reporter.ts +442 -442
- package/src/guard/rule-presets.ts +379 -379
- package/src/guard/semantic-slots.ts +1 -1
- package/src/guard/suggestions.ts +358 -358
- package/src/guard/tsgolint-bridge.ts +512 -512
- package/src/guard/types.ts +348 -348
- package/src/guard/watcher.ts +405 -405
- package/src/i18n/define.ts +126 -126
- package/src/i18n/index.ts +52 -52
- package/src/i18n/message-registry.ts +173 -173
- package/src/i18n/types.ts +112 -112
- package/src/id/index.ts +105 -105
- package/src/index.ts +2 -2
- package/src/kitchen/api/agent-devtools-api.ts +779 -544
- package/src/kitchen/api/errors-grouping.ts +126 -0
- package/src/kitchen/kitchen-handler.ts +192 -63
- package/src/kitchen/kitchen-ui.ts +842 -464
- package/src/logging/index.ts +22 -22
- package/src/logging/transports.ts +365 -365
- package/src/middleware/bridge.ts +147 -147
- package/src/middleware/compose.ts +134 -134
- package/src/middleware/compress.ts +62 -62
- package/src/middleware/cors.ts +47 -47
- package/src/middleware/csrf.ts +328 -328
- package/src/middleware/define.ts +132 -132
- package/src/middleware/jwt.ts +134 -134
- package/src/middleware/logger.ts +58 -58
- package/src/middleware/oauth/__tests__/oauth.test.ts +1 -1
- package/src/middleware/oauth/index.ts +505 -505
- package/src/middleware/oauth/providers.ts +115 -115
- package/src/middleware/rate-limit/index.ts +522 -522
- package/src/middleware/rate-limit/sqlite-store.ts +382 -382
- package/src/middleware/scheduler-cron.ts +96 -96
- package/src/middleware/secure/__tests__/secure.test.ts +360 -360
- package/src/middleware/secure/csp.ts +193 -193
- package/src/middleware/session.ts +174 -174
- package/src/middleware/timeout.ts +55 -55
- package/src/observability/logger-adapter.ts +36 -36
- package/src/observability/sqlite-store.ts +254 -254
- package/src/openapi/generator.ts +1 -1
- package/src/openapi/openapi.test.ts +43 -43
- package/src/perf/__tests__/user-marks.test.ts +354 -354
- package/src/perf/index.ts +133 -133
- package/src/plugins/__tests__/lifecycle-integration.test.ts +272 -272
- package/src/plugins/__tests__/runner.test.ts +409 -409
- package/src/plugins/define.ts +124 -124
- package/src/plugins/examples/dep-check-plugin.ts +80 -80
- package/src/plugins/examples/prerender-cache-plugin.ts +111 -111
- package/src/plugins/examples/sitemap-plugin.ts +65 -65
- package/src/plugins/runner.ts +361 -361
- package/src/plugins/types.ts +368 -368
- package/src/report/index.ts +1 -1
- package/src/resource/__tests__/generator.test.ts +32 -32
- package/src/resource/ddl/__tests__/diff.test.ts +639 -639
- package/src/resource/ddl/__tests__/emit.test.ts +823 -823
- package/src/resource/ddl/__tests__/snapshot.test.ts +499 -499
- package/src/resource/ddl/emit.ts +559 -559
- package/src/resource/ddl/persistence-types.ts +218 -218
- package/src/resource/ddl/type-map.ts +223 -223
- package/src/resource/ddl/types.ts +232 -232
- package/src/resource/generator-repo.ts +630 -630
- package/src/resource/schema.ts +1 -1
- package/src/router/fs-patterns.test.ts +96 -96
- package/src/router/fs-routes.ts +6 -6
- package/src/routes/index.ts +74 -74
- package/src/routes/metadata-routes.ts +427 -427
- package/src/routes/types.ts +341 -341
- package/src/runtime/__tests__/error-boundary-redaction.test.ts +141 -141
- package/src/runtime/__tests__/hdr-client.test.ts +223 -223
- package/src/runtime/__tests__/http-errors.test.ts +117 -117
- package/src/runtime/__tests__/not-found.test.ts +152 -152
- package/src/runtime/adapter.ts +47 -47
- package/src/runtime/boundary.tsx +252 -252
- package/src/runtime/cache.ts +494 -494
- package/src/runtime/compose.ts +222 -222
- package/src/runtime/fast-refresh-runtime.ts +322 -322
- package/src/runtime/handler.ts +65 -65
- package/src/runtime/handlers.ts +300 -300
- package/src/runtime/http-errors.ts +113 -113
- package/src/runtime/image-handler.ts +1 -1
- package/src/runtime/lifecycle.ts +381 -381
- package/src/runtime/logger.test.ts +345 -345
- package/src/runtime/middleware.ts +264 -264
- package/src/runtime/not-found.ts +93 -93
- package/src/runtime/openapi-endpoint.ts +236 -236
- package/src/runtime/ppr.ts +74 -74
- package/src/runtime/registry.ts +171 -171
- package/src/runtime/router.ts +105 -105
- package/src/runtime/server.ts +67 -67
- package/src/runtime/shims.ts +48 -48
- package/src/runtime/ssr.ts +24 -31
- package/src/runtime/streaming-ssr.ts +32 -37
- package/src/runtime/trace.ts +144 -144
- package/src/scheduler/validate.ts +169 -169
- package/src/seo/index.ts +219 -219
- package/src/seo/integration/ssr.ts +306 -306
- package/src/seo/render/basic.ts +435 -435
- package/src/seo/render/index.ts +143 -143
- package/src/seo/render/jsonld.ts +539 -539
- package/src/seo/render/opengraph.ts +197 -197
- package/src/seo/render/robots.ts +116 -116
- package/src/seo/render/sitemap.ts +137 -137
- package/src/seo/render/twitter.ts +127 -127
- package/src/seo/resolve/opengraph.ts +143 -143
- package/src/seo/resolve/robots.ts +73 -73
- package/src/seo/resolve/title.ts +94 -94
- package/src/seo/resolve/twitter.ts +73 -73
- package/src/seo/resolve/url.ts +104 -104
- package/src/seo/routes/index.ts +290 -290
- package/src/seo/types.ts +588 -588
- package/src/slot/validator.ts +39 -39
- package/src/storage/s3/__tests__/s3.test.ts +479 -479
- package/src/storage/s3/index.ts +412 -412
- package/src/testing/__tests__/assertions.test.ts +632 -632
- package/src/testing/__tests__/reporter.test.ts +454 -454
- package/src/testing/assertions.ts +986 -986
- package/src/testing/db.ts +157 -157
- package/src/testing/mocks.ts +203 -203
- package/src/testing/session.ts +190 -190
- package/src/types/branded.ts +56 -56
- package/src/types/index.ts +1 -1
- package/src/utils/safe-io.ts +188 -188
- package/src/utils/string-safe.ts +298 -298
- package/src/watcher/watcher.ts +18 -18
package/src/brain/consent.ts
CHANGED
|
@@ -1,240 +1,240 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Brain — first-use consent prompt + cache (Issue #235).
|
|
3
|
-
*
|
|
4
|
-
* On the first `brain_doctor --useLLM` call that would dispatch to a
|
|
5
|
-
* cloud adapter, Mandu prints a one-line summary of what will be
|
|
6
|
-
* transmitted (diff excerpt, violation report shape, target model) and
|
|
7
|
-
* prompts `y/N`. Consent is cached per-provider, per-project at
|
|
8
|
-
* `~/.mandu/brain-consent.json` so the prompt only appears once.
|
|
9
|
-
*
|
|
10
|
-
* The CI escape hatch is `MANDU_BRAIN_AUTO_CONSENT=1` — used by
|
|
11
|
-
* non-interactive pipelines that have already reviewed the data policy
|
|
12
|
-
* out of band.
|
|
13
|
-
*
|
|
14
|
-
* Privacy invariants:
|
|
15
|
-
* - If consent is not granted, the adapter MUST fall through to the
|
|
16
|
-
* template tier in the resolver (NoopAdapter).
|
|
17
|
-
* - `telemetryOptOut: true` in config bypasses this module entirely —
|
|
18
|
-
* cloud adapters are never constructed in that case.
|
|
19
|
-
* - The consent cache only stores `{ providerId, projectFingerprint,
|
|
20
|
-
* grantedAt }`; no prompt content ever reaches this file.
|
|
21
|
-
*/
|
|
22
|
-
|
|
23
|
-
import { promises as fs } from "node:fs";
|
|
24
|
-
import os from "node:os";
|
|
25
|
-
import path from "node:path";
|
|
26
|
-
import { createHash } from "node:crypto";
|
|
27
|
-
|
|
28
|
-
const CONSENT_DIR = path.join(os.homedir(), ".mandu");
|
|
29
|
-
const CONSENT_FILE = path.join(CONSENT_DIR, "brain-consent.json");
|
|
30
|
-
|
|
31
|
-
export type ConsentProvider = "openai" | "anthropic";
|
|
32
|
-
|
|
33
|
-
export interface ConsentEntry {
|
|
34
|
-
provider: ConsentProvider;
|
|
35
|
-
/** SHA-256 of the absolute project root. Lets us re-prompt per-project. */
|
|
36
|
-
project: string;
|
|
37
|
-
/** ISO timestamp of the grant. */
|
|
38
|
-
grantedAt: string;
|
|
39
|
-
/** Model the user was informed about at grant time. */
|
|
40
|
-
modelAtGrant: string;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface ConsentContext {
|
|
44
|
-
/** Absolute path of the project root — identifies the consent scope. */
|
|
45
|
-
projectRoot: string;
|
|
46
|
-
provider: ConsentProvider;
|
|
47
|
-
/** Model the adapter will transmit to — printed in the prompt. */
|
|
48
|
-
model: string;
|
|
49
|
-
/**
|
|
50
|
-
* One-line human summary of the payload shape that will be sent.
|
|
51
|
-
* Example: "Guard violation report (3 entries) + 120-line diff excerpt".
|
|
52
|
-
* Adapters compose this from their prompt shape.
|
|
53
|
-
*/
|
|
54
|
-
payloadDescription: string;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export interface ConsentPromptDeps {
|
|
58
|
-
/** stdout writer — default process.stdout.write. Tests inject a stub. */
|
|
59
|
-
write?: (msg: string) => void;
|
|
60
|
-
/** Read one line of input. Tests inject a stub; production wraps readline. */
|
|
61
|
-
ask?: (prompt: string) => Promise<string>;
|
|
62
|
-
/** Environment — tests can force `MANDU_BRAIN_AUTO_CONSENT`. */
|
|
63
|
-
env?: NodeJS.ProcessEnv;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Fingerprint a project root so the cache key is collision-resistant
|
|
68
|
-
* AND does not leak the absolute path to the on-disk file. We normalize
|
|
69
|
-
* the path (lowercase on win32) before hashing so a case-variant path
|
|
70
|
-
* resolves to the same entry.
|
|
71
|
-
*/
|
|
72
|
-
export function fingerprintProject(projectRoot: string): string {
|
|
73
|
-
const normalized =
|
|
74
|
-
process.platform === "win32" ? projectRoot.toLowerCase() : projectRoot;
|
|
75
|
-
return createHash("sha256")
|
|
76
|
-
.update(normalized)
|
|
77
|
-
.digest("hex")
|
|
78
|
-
.slice(0, 32);
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
async function readAllConsent(): Promise<ConsentEntry[]> {
|
|
82
|
-
try {
|
|
83
|
-
const raw = await fs.readFile(CONSENT_FILE, "utf8");
|
|
84
|
-
const parsed = JSON.parse(raw);
|
|
85
|
-
if (Array.isArray(parsed)) return parsed as ConsentEntry[];
|
|
86
|
-
return [];
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (
|
|
89
|
-
err instanceof Error &&
|
|
90
|
-
(err as NodeJS.ErrnoException).code === "ENOENT"
|
|
91
|
-
) {
|
|
92
|
-
return [];
|
|
93
|
-
}
|
|
94
|
-
return [];
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
async function writeAllConsent(all: ConsentEntry[]): Promise<void> {
|
|
99
|
-
await fs.mkdir(CONSENT_DIR, { recursive: true, mode: 0o700 });
|
|
100
|
-
const tmp = `${CONSENT_FILE}.${process.pid}.tmp`;
|
|
101
|
-
await fs.writeFile(tmp, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
102
|
-
await fs.rename(tmp, CONSENT_FILE);
|
|
103
|
-
try {
|
|
104
|
-
await fs.chmod(CONSENT_FILE, 0o600);
|
|
105
|
-
} catch {
|
|
106
|
-
/* best-effort on Windows */
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
/**
|
|
111
|
-
* Check whether a (provider, project) pair already has stored consent.
|
|
112
|
-
*/
|
|
113
|
-
export async function hasConsent(
|
|
114
|
-
provider: ConsentProvider,
|
|
115
|
-
projectRoot: string,
|
|
116
|
-
): Promise<boolean> {
|
|
117
|
-
const fp = fingerprintProject(projectRoot);
|
|
118
|
-
const all = await readAllConsent();
|
|
119
|
-
return all.some((e) => e.provider === provider && e.project === fp);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Record consent for a (provider, project) pair. Idempotent.
|
|
124
|
-
*/
|
|
125
|
-
export async function grantConsent(
|
|
126
|
-
ctx: ConsentContext,
|
|
127
|
-
): Promise<void> {
|
|
128
|
-
const fp = fingerprintProject(ctx.projectRoot);
|
|
129
|
-
const all = await readAllConsent();
|
|
130
|
-
const existing = all.find(
|
|
131
|
-
(e) => e.provider === ctx.provider && e.project === fp,
|
|
132
|
-
);
|
|
133
|
-
if (existing) return;
|
|
134
|
-
all.push({
|
|
135
|
-
provider: ctx.provider,
|
|
136
|
-
project: fp,
|
|
137
|
-
grantedAt: new Date().toISOString(),
|
|
138
|
-
modelAtGrant: ctx.model,
|
|
139
|
-
});
|
|
140
|
-
await writeAllConsent(all);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* Revoke consent — used by `mandu brain logout --provider=...`.
|
|
145
|
-
*/
|
|
146
|
-
export async function revokeConsent(
|
|
147
|
-
provider: ConsentProvider,
|
|
148
|
-
projectRoot?: string,
|
|
149
|
-
): Promise<void> {
|
|
150
|
-
const all = await readAllConsent();
|
|
151
|
-
const fp = projectRoot ? fingerprintProject(projectRoot) : null;
|
|
152
|
-
const next = all.filter((e) => {
|
|
153
|
-
if (e.provider !== provider) return true;
|
|
154
|
-
if (fp === null) return false; // revoke all projects for this provider
|
|
155
|
-
return e.project !== fp;
|
|
156
|
-
});
|
|
157
|
-
await writeAllConsent(next);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
/**
|
|
161
|
-
* Ensure consent exists for the given context. Prompts the user if
|
|
162
|
-
* necessary. Returns `true` when consent is granted (fresh or cached),
|
|
163
|
-
* `false` when the user declined or non-interactive stdin is closed.
|
|
164
|
-
*
|
|
165
|
-
* Never throws — Brain is isolated from the Core execution path.
|
|
166
|
-
*/
|
|
167
|
-
export async function ensureConsent(
|
|
168
|
-
ctx: ConsentContext,
|
|
169
|
-
deps: ConsentPromptDeps = {},
|
|
170
|
-
): Promise<boolean> {
|
|
171
|
-
const env = deps.env ?? process.env;
|
|
172
|
-
|
|
173
|
-
// Already consented — fast path.
|
|
174
|
-
if (await hasConsent(ctx.provider, ctx.projectRoot)) {
|
|
175
|
-
return true;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// CI / pipeline opt-in.
|
|
179
|
-
if (env.MANDU_BRAIN_AUTO_CONSENT === "1") {
|
|
180
|
-
await grantConsent(ctx);
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
const write = deps.write ?? ((m: string) => process.stdout.write(m));
|
|
185
|
-
const ask = deps.ask ?? defaultReadLine;
|
|
186
|
-
|
|
187
|
-
write(
|
|
188
|
-
[
|
|
189
|
-
"",
|
|
190
|
-
"Mandu Brain — cloud adapter consent",
|
|
191
|
-
"-----------------------------------",
|
|
192
|
-
` Provider : ${ctx.provider}`,
|
|
193
|
-
` Model : ${ctx.model}`,
|
|
194
|
-
` Payload : ${ctx.payloadDescription}`,
|
|
195
|
-
"",
|
|
196
|
-
"Secrets detected in source code are scrubbed before transmission",
|
|
197
|
-
"(audit log: .mandu/brain-redactions.jsonl).",
|
|
198
|
-
"",
|
|
199
|
-
"Consent is cached per-project at ~/.mandu/brain-consent.json.",
|
|
200
|
-
"Set MANDU_BRAIN_AUTO_CONSENT=1 in CI to skip this prompt.",
|
|
201
|
-
"",
|
|
202
|
-
].join("\n"),
|
|
203
|
-
);
|
|
204
|
-
|
|
205
|
-
const answer = (await ask("Proceed? [y/N]: ")).trim().toLowerCase();
|
|
206
|
-
if (answer === "y" || answer === "yes") {
|
|
207
|
-
await grantConsent(ctx);
|
|
208
|
-
return true;
|
|
209
|
-
}
|
|
210
|
-
return false;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Default interactive reader — wraps node:readline. Returns an empty
|
|
215
|
-
* string when stdin is non-TTY so non-interactive shells treat the
|
|
216
|
-
* prompt as declined (safer default than auto-accepting).
|
|
217
|
-
*/
|
|
218
|
-
async function defaultReadLine(prompt: string): Promise<string> {
|
|
219
|
-
if (!process.stdin.isTTY) return "";
|
|
220
|
-
process.stdout.write(prompt);
|
|
221
|
-
// Inline readline to avoid a top-level import — keeps the Brain
|
|
222
|
-
// module graph small and avoids pulling readline into the SSR bundle.
|
|
223
|
-
const readline = await import("node:readline");
|
|
224
|
-
const rl = readline.createInterface({
|
|
225
|
-
input: process.stdin,
|
|
226
|
-
output: process.stdout,
|
|
227
|
-
terminal: true,
|
|
228
|
-
});
|
|
229
|
-
return new Promise<string>((resolve) => {
|
|
230
|
-
rl.question("", (answer) => {
|
|
231
|
-
rl.close();
|
|
232
|
-
resolve(answer);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
/** Consent file path — surfaced to the user in `mandu brain status`. */
|
|
238
|
-
export function consentFilePath(): string {
|
|
239
|
-
return CONSENT_FILE;
|
|
240
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Brain — first-use consent prompt + cache (Issue #235).
|
|
3
|
+
*
|
|
4
|
+
* On the first `brain_doctor --useLLM` call that would dispatch to a
|
|
5
|
+
* cloud adapter, Mandu prints a one-line summary of what will be
|
|
6
|
+
* transmitted (diff excerpt, violation report shape, target model) and
|
|
7
|
+
* prompts `y/N`. Consent is cached per-provider, per-project at
|
|
8
|
+
* `~/.mandu/brain-consent.json` so the prompt only appears once.
|
|
9
|
+
*
|
|
10
|
+
* The CI escape hatch is `MANDU_BRAIN_AUTO_CONSENT=1` — used by
|
|
11
|
+
* non-interactive pipelines that have already reviewed the data policy
|
|
12
|
+
* out of band.
|
|
13
|
+
*
|
|
14
|
+
* Privacy invariants:
|
|
15
|
+
* - If consent is not granted, the adapter MUST fall through to the
|
|
16
|
+
* template tier in the resolver (NoopAdapter).
|
|
17
|
+
* - `telemetryOptOut: true` in config bypasses this module entirely —
|
|
18
|
+
* cloud adapters are never constructed in that case.
|
|
19
|
+
* - The consent cache only stores `{ providerId, projectFingerprint,
|
|
20
|
+
* grantedAt }`; no prompt content ever reaches this file.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { promises as fs } from "node:fs";
|
|
24
|
+
import os from "node:os";
|
|
25
|
+
import path from "node:path";
|
|
26
|
+
import { createHash } from "node:crypto";
|
|
27
|
+
|
|
28
|
+
const CONSENT_DIR = path.join(os.homedir(), ".mandu");
|
|
29
|
+
const CONSENT_FILE = path.join(CONSENT_DIR, "brain-consent.json");
|
|
30
|
+
|
|
31
|
+
export type ConsentProvider = "openai" | "anthropic";
|
|
32
|
+
|
|
33
|
+
export interface ConsentEntry {
|
|
34
|
+
provider: ConsentProvider;
|
|
35
|
+
/** SHA-256 of the absolute project root. Lets us re-prompt per-project. */
|
|
36
|
+
project: string;
|
|
37
|
+
/** ISO timestamp of the grant. */
|
|
38
|
+
grantedAt: string;
|
|
39
|
+
/** Model the user was informed about at grant time. */
|
|
40
|
+
modelAtGrant: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ConsentContext {
|
|
44
|
+
/** Absolute path of the project root — identifies the consent scope. */
|
|
45
|
+
projectRoot: string;
|
|
46
|
+
provider: ConsentProvider;
|
|
47
|
+
/** Model the adapter will transmit to — printed in the prompt. */
|
|
48
|
+
model: string;
|
|
49
|
+
/**
|
|
50
|
+
* One-line human summary of the payload shape that will be sent.
|
|
51
|
+
* Example: "Guard violation report (3 entries) + 120-line diff excerpt".
|
|
52
|
+
* Adapters compose this from their prompt shape.
|
|
53
|
+
*/
|
|
54
|
+
payloadDescription: string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface ConsentPromptDeps {
|
|
58
|
+
/** stdout writer — default process.stdout.write. Tests inject a stub. */
|
|
59
|
+
write?: (msg: string) => void;
|
|
60
|
+
/** Read one line of input. Tests inject a stub; production wraps readline. */
|
|
61
|
+
ask?: (prompt: string) => Promise<string>;
|
|
62
|
+
/** Environment — tests can force `MANDU_BRAIN_AUTO_CONSENT`. */
|
|
63
|
+
env?: NodeJS.ProcessEnv;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Fingerprint a project root so the cache key is collision-resistant
|
|
68
|
+
* AND does not leak the absolute path to the on-disk file. We normalize
|
|
69
|
+
* the path (lowercase on win32) before hashing so a case-variant path
|
|
70
|
+
* resolves to the same entry.
|
|
71
|
+
*/
|
|
72
|
+
export function fingerprintProject(projectRoot: string): string {
|
|
73
|
+
const normalized =
|
|
74
|
+
process.platform === "win32" ? projectRoot.toLowerCase() : projectRoot;
|
|
75
|
+
return createHash("sha256")
|
|
76
|
+
.update(normalized)
|
|
77
|
+
.digest("hex")
|
|
78
|
+
.slice(0, 32);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function readAllConsent(): Promise<ConsentEntry[]> {
|
|
82
|
+
try {
|
|
83
|
+
const raw = await fs.readFile(CONSENT_FILE, "utf8");
|
|
84
|
+
const parsed = JSON.parse(raw);
|
|
85
|
+
if (Array.isArray(parsed)) return parsed as ConsentEntry[];
|
|
86
|
+
return [];
|
|
87
|
+
} catch (err) {
|
|
88
|
+
if (
|
|
89
|
+
err instanceof Error &&
|
|
90
|
+
(err as NodeJS.ErrnoException).code === "ENOENT"
|
|
91
|
+
) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function writeAllConsent(all: ConsentEntry[]): Promise<void> {
|
|
99
|
+
await fs.mkdir(CONSENT_DIR, { recursive: true, mode: 0o700 });
|
|
100
|
+
const tmp = `${CONSENT_FILE}.${process.pid}.tmp`;
|
|
101
|
+
await fs.writeFile(tmp, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
102
|
+
await fs.rename(tmp, CONSENT_FILE);
|
|
103
|
+
try {
|
|
104
|
+
await fs.chmod(CONSENT_FILE, 0o600);
|
|
105
|
+
} catch {
|
|
106
|
+
/* best-effort on Windows */
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Check whether a (provider, project) pair already has stored consent.
|
|
112
|
+
*/
|
|
113
|
+
export async function hasConsent(
|
|
114
|
+
provider: ConsentProvider,
|
|
115
|
+
projectRoot: string,
|
|
116
|
+
): Promise<boolean> {
|
|
117
|
+
const fp = fingerprintProject(projectRoot);
|
|
118
|
+
const all = await readAllConsent();
|
|
119
|
+
return all.some((e) => e.provider === provider && e.project === fp);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Record consent for a (provider, project) pair. Idempotent.
|
|
124
|
+
*/
|
|
125
|
+
export async function grantConsent(
|
|
126
|
+
ctx: ConsentContext,
|
|
127
|
+
): Promise<void> {
|
|
128
|
+
const fp = fingerprintProject(ctx.projectRoot);
|
|
129
|
+
const all = await readAllConsent();
|
|
130
|
+
const existing = all.find(
|
|
131
|
+
(e) => e.provider === ctx.provider && e.project === fp,
|
|
132
|
+
);
|
|
133
|
+
if (existing) return;
|
|
134
|
+
all.push({
|
|
135
|
+
provider: ctx.provider,
|
|
136
|
+
project: fp,
|
|
137
|
+
grantedAt: new Date().toISOString(),
|
|
138
|
+
modelAtGrant: ctx.model,
|
|
139
|
+
});
|
|
140
|
+
await writeAllConsent(all);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Revoke consent — used by `mandu brain logout --provider=...`.
|
|
145
|
+
*/
|
|
146
|
+
export async function revokeConsent(
|
|
147
|
+
provider: ConsentProvider,
|
|
148
|
+
projectRoot?: string,
|
|
149
|
+
): Promise<void> {
|
|
150
|
+
const all = await readAllConsent();
|
|
151
|
+
const fp = projectRoot ? fingerprintProject(projectRoot) : null;
|
|
152
|
+
const next = all.filter((e) => {
|
|
153
|
+
if (e.provider !== provider) return true;
|
|
154
|
+
if (fp === null) return false; // revoke all projects for this provider
|
|
155
|
+
return e.project !== fp;
|
|
156
|
+
});
|
|
157
|
+
await writeAllConsent(next);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Ensure consent exists for the given context. Prompts the user if
|
|
162
|
+
* necessary. Returns `true` when consent is granted (fresh or cached),
|
|
163
|
+
* `false` when the user declined or non-interactive stdin is closed.
|
|
164
|
+
*
|
|
165
|
+
* Never throws — Brain is isolated from the Core execution path.
|
|
166
|
+
*/
|
|
167
|
+
export async function ensureConsent(
|
|
168
|
+
ctx: ConsentContext,
|
|
169
|
+
deps: ConsentPromptDeps = {},
|
|
170
|
+
): Promise<boolean> {
|
|
171
|
+
const env = deps.env ?? process.env;
|
|
172
|
+
|
|
173
|
+
// Already consented — fast path.
|
|
174
|
+
if (await hasConsent(ctx.provider, ctx.projectRoot)) {
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// CI / pipeline opt-in.
|
|
179
|
+
if (env.MANDU_BRAIN_AUTO_CONSENT === "1") {
|
|
180
|
+
await grantConsent(ctx);
|
|
181
|
+
return true;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const write = deps.write ?? ((m: string) => process.stdout.write(m));
|
|
185
|
+
const ask = deps.ask ?? defaultReadLine;
|
|
186
|
+
|
|
187
|
+
write(
|
|
188
|
+
[
|
|
189
|
+
"",
|
|
190
|
+
"Mandu Brain — cloud adapter consent",
|
|
191
|
+
"-----------------------------------",
|
|
192
|
+
` Provider : ${ctx.provider}`,
|
|
193
|
+
` Model : ${ctx.model}`,
|
|
194
|
+
` Payload : ${ctx.payloadDescription}`,
|
|
195
|
+
"",
|
|
196
|
+
"Secrets detected in source code are scrubbed before transmission",
|
|
197
|
+
"(audit log: .mandu/brain-redactions.jsonl).",
|
|
198
|
+
"",
|
|
199
|
+
"Consent is cached per-project at ~/.mandu/brain-consent.json.",
|
|
200
|
+
"Set MANDU_BRAIN_AUTO_CONSENT=1 in CI to skip this prompt.",
|
|
201
|
+
"",
|
|
202
|
+
].join("\n"),
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
const answer = (await ask("Proceed? [y/N]: ")).trim().toLowerCase();
|
|
206
|
+
if (answer === "y" || answer === "yes") {
|
|
207
|
+
await grantConsent(ctx);
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Default interactive reader — wraps node:readline. Returns an empty
|
|
215
|
+
* string when stdin is non-TTY so non-interactive shells treat the
|
|
216
|
+
* prompt as declined (safer default than auto-accepting).
|
|
217
|
+
*/
|
|
218
|
+
async function defaultReadLine(prompt: string): Promise<string> {
|
|
219
|
+
if (!process.stdin.isTTY) return "";
|
|
220
|
+
process.stdout.write(prompt);
|
|
221
|
+
// Inline readline to avoid a top-level import — keeps the Brain
|
|
222
|
+
// module graph small and avoids pulling readline into the SSR bundle.
|
|
223
|
+
const readline = await import("node:readline");
|
|
224
|
+
const rl = readline.createInterface({
|
|
225
|
+
input: process.stdin,
|
|
226
|
+
output: process.stdout,
|
|
227
|
+
terminal: true,
|
|
228
|
+
});
|
|
229
|
+
return new Promise<string>((resolve) => {
|
|
230
|
+
rl.question("", (answer) => {
|
|
231
|
+
rl.close();
|
|
232
|
+
resolve(answer);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** Consent file path — surfaced to the user in `mandu brain status`. */
|
|
238
|
+
export function consentFilePath(): string {
|
|
239
|
+
return CONSENT_FILE;
|
|
240
|
+
}
|