@su-record/vibe 4.2.34 → 4.2.36
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/dist/cli/common.js +4 -1
- package/dist/cli/common.js.map +1 -1
- package/dist/cli/heldout.js +10 -4
- package/dist/cli/heldout.js.map +1 -1
- package/dist/cli/human.js +8 -4
- package/dist/cli/human.js.map +1 -1
- package/dist/cli/internal.js +37 -24
- package/dist/cli/internal.js.map +1 -1
- package/dist/cli/ledger.js +67 -0
- package/dist/cli/ledger.js.map +1 -0
- package/dist/cli/map.js +5 -3
- package/dist/cli/map.js.map +1 -1
- package/dist/cli/memory.js +104 -74
- package/dist/cli/memory.js.map +1 -1
- package/dist/cli/risks.js +20 -3
- package/dist/cli/risks.js.map +1 -1
- package/dist/cli/session.js +4 -2
- package/dist/cli/session.js.map +1 -1
- package/dist/cli/setup.js +44 -13
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/verification.js +1 -1
- package/dist/cli/verification.js.map +1 -1
- package/dist/cli/work.js +75 -22
- package/dist/cli/work.js.map +1 -1
- package/dist/cli.js +18 -6
- package/dist/cli.js.map +1 -1
- package/dist/core/analyze.js +21 -7
- package/dist/core/analyze.js.map +1 -1
- package/dist/core/artifact-paths.js +14 -3
- package/dist/core/artifact-paths.js.map +1 -1
- package/dist/core/capabilities.js +69 -15
- package/dist/core/capabilities.js.map +1 -1
- package/dist/core/check-authorization.js +37 -0
- package/dist/core/check-authorization.js.map +1 -0
- package/dist/core/check-process.js +12 -4
- package/dist/core/check-process.js.map +1 -1
- package/dist/core/check-report.js +66 -0
- package/dist/core/check-report.js.map +1 -0
- package/dist/core/check-selection.js +4 -2
- package/dist/core/check-selection.js.map +1 -1
- package/dist/core/check.js +66 -130
- package/dist/core/check.js.map +1 -1
- package/dist/core/checks/changed.js +20 -6
- package/dist/core/checks/changed.js.map +1 -1
- package/dist/core/checks/eval.js +35 -10
- package/dist/core/checks/eval.js.map +1 -1
- package/dist/core/checks/file.js +26 -23
- package/dist/core/checks/file.js.map +1 -1
- package/dist/core/checks/fileRules.js +42 -16
- package/dist/core/checks/fileRules.js.map +1 -1
- package/dist/core/checks/http.js +35 -34
- package/dist/core/checks/http.js.map +1 -1
- package/dist/core/checks/packs.js +1 -1
- package/dist/core/checks/packs.js.map +1 -1
- package/dist/core/checks/review.js +75 -40
- package/dist/core/checks/review.js.map +1 -1
- package/dist/core/checks/run.js +5 -2
- package/dist/core/checks/run.js.map +1 -1
- package/dist/core/checks/schema.js +25 -0
- package/dist/core/checks/schema.js.map +1 -0
- package/dist/core/checks/source.js +11 -6
- package/dist/core/checks/source.js.map +1 -1
- package/dist/core/config.js +9 -2
- package/dist/core/config.js.map +1 -1
- package/dist/core/consent.js +24 -16
- package/dist/core/consent.js.map +1 -1
- package/dist/core/context-notes.js +15 -7
- package/dist/core/context-notes.js.map +1 -1
- package/dist/core/context.js +24 -12
- package/dist/core/context.js.map +1 -1
- package/dist/core/conventions.js +22 -9
- package/dist/core/conventions.js.map +1 -1
- package/dist/core/docs/cfb.js +81 -45
- package/dist/core/docs/cfb.js.map +1 -1
- package/dist/core/docs/html.js +14 -3
- package/dist/core/docs/html.js.map +1 -1
- package/dist/core/docs/hwp.js +44 -22
- package/dist/core/docs/hwp.js.map +1 -1
- package/dist/core/docs/office.js +20 -10
- package/dist/core/docs/office.js.map +1 -1
- package/dist/core/docs/pdf.js +10 -4
- package/dist/core/docs/pdf.js.map +1 -1
- package/dist/core/docs/read.js +32 -24
- package/dist/core/docs/read.js.map +1 -1
- package/dist/core/docs/zip.js +32 -15
- package/dist/core/docs/zip.js.map +1 -1
- package/dist/core/evidence.js +17 -7
- package/dist/core/evidence.js.map +1 -1
- package/dist/core/failure-message.js +35 -13
- package/dist/core/failure-message.js.map +1 -1
- package/dist/core/failure.js +49 -14
- package/dist/core/failure.js.map +1 -1
- package/dist/core/github.js +3 -1
- package/dist/core/github.js.map +1 -1
- package/dist/core/handoff.js +20 -7
- package/dist/core/handoff.js.map +1 -1
- package/dist/core/heldout-run.js +6 -3
- package/dist/core/heldout-run.js.map +1 -1
- package/dist/core/heldout.js +24 -13
- package/dist/core/heldout.js.map +1 -1
- package/dist/core/inbox.js +3 -1
- package/dist/core/inbox.js.map +1 -1
- package/dist/core/inputs.js +76 -0
- package/dist/core/inputs.js.map +1 -0
- package/dist/core/inspect.js +61 -20
- package/dist/core/inspect.js.map +1 -1
- package/dist/core/intent.js +50 -12
- package/dist/core/intent.js.map +1 -1
- package/dist/core/knowledge-find.js +31 -12
- package/dist/core/knowledge-find.js.map +1 -1
- package/dist/core/knowledge.js +22 -12
- package/dist/core/knowledge.js.map +1 -1
- package/dist/core/lang.js +5 -2
- package/dist/core/lang.js.map +1 -1
- package/dist/core/ledger.js +44 -20
- package/dist/core/ledger.js.map +1 -1
- package/dist/core/locations.js +12 -0
- package/dist/core/locations.js.map +1 -0
- package/dist/core/map/imports.js +20 -9
- package/dist/core/map/imports.js.map +1 -1
- package/dist/core/map/index.js +28 -13
- package/dist/core/map/index.js.map +1 -1
- package/dist/core/map/symbols.js +27 -12
- package/dist/core/map/symbols.js.map +1 -1
- package/dist/core/output-capture.js +7 -4
- package/dist/core/output-capture.js.map +1 -1
- package/dist/core/paths.js +2 -1
- package/dist/core/paths.js.map +1 -1
- package/dist/core/performance.js +43 -17
- package/dist/core/performance.js.map +1 -1
- package/dist/core/profile.js +6 -3
- package/dist/core/profile.js.map +1 -1
- package/dist/core/reader.js +32 -16
- package/dist/core/reader.js.map +1 -1
- package/dist/core/readerSession.js +34 -14
- package/dist/core/readerSession.js.map +1 -1
- package/dist/core/recent.js +20 -6
- package/dist/core/recent.js.map +1 -1
- package/dist/core/regress.js +21 -12
- package/dist/core/regress.js.map +1 -1
- package/dist/core/repair.js +22 -4
- package/dist/core/repair.js.map +1 -1
- package/dist/core/research-queries.js +85 -0
- package/dist/core/research-queries.js.map +1 -0
- package/dist/core/research.js +89 -88
- package/dist/core/research.js.map +1 -1
- package/dist/core/results.js +89 -0
- package/dist/core/results.js.map +1 -0
- package/dist/core/risk-signals.js +55 -16
- package/dist/core/risk-signals.js.map +1 -1
- package/dist/core/run-id.js +10 -6
- package/dist/core/run-id.js.map +1 -1
- package/dist/core/scenarios.js +97 -57
- package/dist/core/scenarios.js.map +1 -1
- package/dist/core/session.js +3 -2
- package/dist/core/session.js.map +1 -1
- package/dist/core/size-gate.js +57 -0
- package/dist/core/size-gate.js.map +1 -0
- package/dist/core/size.js +32 -10
- package/dist/core/size.js.map +1 -1
- package/dist/core/skills.js +66 -28
- package/dist/core/skills.js.map +1 -1
- package/dist/core/skillsmp.js +26 -12
- package/dist/core/skillsmp.js.map +1 -1
- package/dist/core/source-basis.js +21 -9
- package/dist/core/source-basis.js.map +1 -1
- package/dist/core/status.js +26 -13
- package/dist/core/status.js.map +1 -1
- package/dist/core/table.js +8 -1
- package/dist/core/table.js.map +1 -1
- package/dist/core/tokens.js +20 -10
- package/dist/core/tokens.js.map +1 -1
- package/dist/core/tree.js +26 -16
- package/dist/core/tree.js.map +1 -1
- package/dist/core/verification-plan.js +26 -10
- package/dist/core/verification-plan.js.map +1 -1
- package/dist/core/verification.js +14 -7
- package/dist/core/verification.js.map +1 -1
- package/dist/core/view.js +93 -44
- package/dist/core/view.js.map +1 -1
- package/dist/install/global.js +37 -18
- package/dist/install/global.js.map +1 -1
- package/dist/install/hooks.js +17 -8
- package/dist/install/hooks.js.map +1 -1
- package/dist/install/manifest.js +11 -0
- package/dist/install/manifest.js.map +1 -0
- package/dist/install/mcpb.js +52 -23
- package/dist/install/mcpb.js.map +1 -1
- package/dist/install/plugin.js +73 -33
- package/dist/install/plugin.js.map +1 -1
- package/dist/install/project.js +8 -7
- package/dist/install/project.js.map +1 -1
- package/dist/install/register.js +83 -48
- package/dist/install/register.js.map +1 -1
- package/dist/install/tree.js +27 -11
- package/dist/install/tree.js.map +1 -1
- package/dist/install/update.js +46 -20
- package/dist/install/update.js.map +1 -1
- package/hooks/code-map.cjs +22 -9
- package/hooks/file-size.cjs +83 -0
- package/hooks/journal.cjs +28 -18
- package/hooks/knowledge-auto.cjs +55 -21
- package/hooks/knowledge-journal.cjs +47 -20
- package/hooks/knowledge-projects.cjs +50 -21
- package/hooks/knowledge-recall.cjs +16 -6
- package/hooks/knowledge-upkeep.cjs +22 -5
- package/hooks/locations.cjs +35 -0
- package/hooks/notify.js +52 -24
- package/hooks/obsidian.cjs +17 -7
- package/hooks/private-store.cjs +25 -10
- package/hooks/retest.cjs +39 -16
- package/hooks/session-context.cjs +30 -13
- package/hooks/session-files.cjs +44 -21
- package/hooks/session-repair.cjs +26 -11
- package/hooks/session-state.cjs +78 -38
- package/hooks/session.js +19 -8
- package/hooks/slice-command.cjs +38 -17
- package/hooks/slice-guard.cjs +20 -6
- package/hooks/slice-store.cjs +25 -12
- package/internal/guides/code.md +17 -0
- package/mcpb/server/index.js +40 -12
- package/package.json +1 -1
package/hooks/notify.js
CHANGED
|
@@ -19,12 +19,28 @@ import path from 'node:path';
|
|
|
19
19
|
import { fileURLToPath } from 'node:url';
|
|
20
20
|
import codeMap from './code-map.cjs';
|
|
21
21
|
import knowledgeAuto from './knowledge-auto.cjs';
|
|
22
|
+
import locations from './locations.cjs';
|
|
22
23
|
import knowledgeJournal from './knowledge-journal.cjs';
|
|
23
24
|
import retest from './retest.cjs';
|
|
24
25
|
import sessionRuntime from './session-state.cjs';
|
|
25
26
|
import sessionFiles from './session-files.cjs';
|
|
26
27
|
import sliceRuntime from './slice-guard.cjs';
|
|
27
28
|
|
|
29
|
+
const { claudeSettings } = locations;
|
|
30
|
+
|
|
31
|
+
const NOTIFY_HOOK = 'hooks/notify.js';
|
|
32
|
+
const SETTINGS_MAX_BYTES = 64 * 1024;
|
|
33
|
+
const PROJECT_DIR = '.vibe';
|
|
34
|
+
const STATE_TIMEOUT_MS = 15000;
|
|
35
|
+
// An authorize record counts for this long.
|
|
36
|
+
const MS_PER_MINUTE = 60 * 1000;
|
|
37
|
+
const AUTHORIZE_WINDOW_MS = 10 * MS_PER_MINUTE;
|
|
38
|
+
const DEFAULT_READ_ADVISE_LINES = sliceRuntime.READ_ADVISE_LINES;
|
|
39
|
+
// Exit code that stops a tool call in Claude Code.
|
|
40
|
+
const BLOCK_EXIT = 2;
|
|
41
|
+
// Hook modes that run in a project with or without a .vibe folder.
|
|
42
|
+
const ANYWHERE_MODES = ['stop', 'session', 'pre'];
|
|
43
|
+
|
|
28
44
|
const mode = process.argv[2] || 'post';
|
|
29
45
|
const asPlugin = process.argv.includes('--plugin');
|
|
30
46
|
const personal = process.argv.includes('--personal');
|
|
@@ -38,11 +54,11 @@ if (asPlugin) {
|
|
|
38
54
|
// The reader refuses a link anywhere in a path; a home under one (macOS /var → /private/var) is the person's own.
|
|
39
55
|
let home = process.env.VIBE_HOME_DIR || os.homedir();
|
|
40
56
|
try { home = fs.realpathSync(home); } catch { /* a missing home has no settings to read */ }
|
|
41
|
-
for (const file of [
|
|
57
|
+
for (const file of [claudeSettings(home)]) {
|
|
42
58
|
try {
|
|
43
|
-
const settings = JSON.parse(sessionFiles.optional(file,
|
|
59
|
+
const settings = JSON.parse(sessionFiles.optional(file, SETTINGS_MAX_BYTES) ?? '{}');
|
|
44
60
|
const owned = Object.values(settings.hooks ?? {}).some(entries => entries.some(entry =>
|
|
45
|
-
entry.hooks?.some(hook => hook.command?.replaceAll('\\', '/').includes(
|
|
61
|
+
entry.hooks?.some(hook => hook.command?.replaceAll('\\', '/').includes(NOTIFY_HOOK))));
|
|
46
62
|
if (owned) process.exit(0);
|
|
47
63
|
} catch {
|
|
48
64
|
/* no such file — keep going */
|
|
@@ -58,19 +74,17 @@ function readPayload() {
|
|
|
58
74
|
catch { return { session_id: 'invalid payload' }; }
|
|
59
75
|
}
|
|
60
76
|
|
|
61
|
-
|
|
62
|
-
const view = sessionRuntime.sessionStatus(payload);
|
|
63
|
-
return sessionRuntime.message(view, view.status === 'bound' ? 'session status only; run vibe state for the procedure' : `${view.status}; use explicit vibe session bind in the intended worktree`);
|
|
64
|
-
}
|
|
77
|
+
const structuralSession = payload => sessionRuntime.structuralNote(sessionRuntime.sessionStatus(payload));
|
|
65
78
|
function sliceClient() {
|
|
66
79
|
const declared = process.env.VIBE_CLIENT;
|
|
67
80
|
if (declared) return ['codex', 'chatgpt'].includes(declared) ? 'codex' : 'claude';
|
|
68
81
|
return process.env.CODEX_THREAD_ID && !process.env.CLAUDE_SESSION_ID ? 'codex' : 'claude';
|
|
69
82
|
}
|
|
70
83
|
|
|
71
|
-
function
|
|
72
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName
|
|
84
|
+
function emit(hookEventName, additionalContext) {
|
|
85
|
+
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName, additionalContext } })}\n`);
|
|
73
86
|
}
|
|
87
|
+
const emitContext = text => emit(mode === 'post' ? 'PostToolUse' : 'PreToolUse', text);
|
|
74
88
|
|
|
75
89
|
// The same actions the check gate names (src/core/checks/mutation.ts), so a blocked tool call and a blocked
|
|
76
90
|
// check ask for the same `vibe authorize --action`; `send` is the hook's own. A command that only reads —
|
|
@@ -91,10 +105,12 @@ const IRREVERSIBLE = [
|
|
|
91
105
|
];
|
|
92
106
|
const READS_ONLY = /^\s*(?:grep|rg|cat|head|tail|less|ls|find|wc|echo|printf|sed\s+-n|git\s+(?:log|diff|show|status|grep|blame|branch|ls-files))\b/;
|
|
93
107
|
|
|
108
|
+
const hasProjectDir = () => fs.existsSync(path.join(root, PROJECT_DIR));
|
|
109
|
+
|
|
94
110
|
function recentAuthorize(action) {
|
|
95
111
|
try {
|
|
96
|
-
const lines = fs.readFileSync(path.join(root,
|
|
97
|
-
const cutoff = Date.now() -
|
|
112
|
+
const lines = fs.readFileSync(path.join(root, PROJECT_DIR, 'ledger.jsonl'), 'utf-8').trim().split('\n');
|
|
113
|
+
const cutoff = Date.now() - AUTHORIZE_WINDOW_MS;
|
|
98
114
|
return lines.some((line) => {
|
|
99
115
|
try {
|
|
100
116
|
const e = JSON.parse(line);
|
|
@@ -108,16 +124,25 @@ function recentAuthorize(action) {
|
|
|
108
124
|
}
|
|
109
125
|
}
|
|
110
126
|
|
|
111
|
-
if (!
|
|
127
|
+
if (!ANYWHERE_MODES.includes(mode) && !hasProjectDir()) process.exit(0);
|
|
128
|
+
|
|
129
|
+
function irreversibleNotice(action, blocking) {
|
|
130
|
+
return [
|
|
131
|
+
`[vibe] "${action}" is irreversible and no authorize record exists in the last ${AUTHORIZE_WINDOW_MS / MS_PER_MINUTE} minutes —`,
|
|
132
|
+
`${blocking ? 'blocked: ' : ''}get a human token with \`vibe ask --needs authorize:${action}\``,
|
|
133
|
+
'and run `vibe authorize` first, as its own command: the gate reads the ledger before this command runs,',
|
|
134
|
+
'so an authorize chained in front of the action is not seen',
|
|
135
|
+
].join(' ');
|
|
136
|
+
}
|
|
112
137
|
|
|
113
138
|
function tokenPolicy() {
|
|
114
139
|
try {
|
|
115
|
-
return JSON.parse(fs.readFileSync(path.join(root,
|
|
140
|
+
return JSON.parse(fs.readFileSync(path.join(root, PROJECT_DIR, 'config.json'), 'utf-8')).tokens || 'off';
|
|
116
141
|
} catch {
|
|
117
142
|
return 'off';
|
|
118
143
|
}
|
|
119
144
|
}
|
|
120
|
-
const READ_ADVISE_LINES = Number(process.env.VIBE_READ_ADVISE_LINES ||
|
|
145
|
+
const READ_ADVISE_LINES = Number(process.env.VIBE_READ_ADVISE_LINES || DEFAULT_READ_ADVISE_LINES);
|
|
121
146
|
|
|
122
147
|
function countLines(file) {
|
|
123
148
|
try {
|
|
@@ -134,7 +159,10 @@ function adviseRead(payload) {
|
|
|
134
159
|
const lines = countLines(path.resolve(root, file));
|
|
135
160
|
if (lines <= READ_ADVISE_LINES) return;
|
|
136
161
|
const shown = (path.isAbsolute(file) ? path.relative(root, file) || file : file).split(path.sep).join('/');
|
|
137
|
-
emitContext(
|
|
162
|
+
emitContext([
|
|
163
|
+
`[vibe] ${shown} is ${lines} lines — when it only has to be understood, not edited or debugged,`,
|
|
164
|
+
`\`vibe read ${shown} --ask "<question>"\` lets a low-reasoning model read it and returns the answer with line numbers`,
|
|
165
|
+
].join(' '));
|
|
138
166
|
}
|
|
139
167
|
|
|
140
168
|
const cli = path.join(here, '..', 'dist', 'cli.js');
|
|
@@ -158,14 +186,14 @@ if (mode === 'stop') {
|
|
|
158
186
|
if (mode === 'session') {
|
|
159
187
|
const payload = readPayload();
|
|
160
188
|
const recall = knowledgeAuto.start(payload);
|
|
161
|
-
|
|
189
|
+
emit('SessionStart', [structuralSession(payload), recall, codeMap.codeMap(root)].filter(Boolean).join('\n'));
|
|
162
190
|
process.exit(0);
|
|
163
191
|
}
|
|
164
192
|
|
|
165
193
|
if (mode === 'pre') {
|
|
166
194
|
const payload = readPayload();
|
|
167
195
|
if (payload.tool_name === 'Read') {
|
|
168
|
-
if (
|
|
196
|
+
if (hasProjectDir()) adviseRead(payload);
|
|
169
197
|
process.exit(0);
|
|
170
198
|
}
|
|
171
199
|
const raw = payload.tool_input && payload.tool_input.command;
|
|
@@ -176,13 +204,13 @@ if (mode === 'pre') {
|
|
|
176
204
|
const leave = (code) => { if (notes.length) emitContext(notes.join('\n')); process.exit(code); };
|
|
177
205
|
let gate = null;
|
|
178
206
|
try { gate = retest.before(payload, process.env.VIBE_HOME_DIR || os.homedir()); } catch { /* the command runs */ }
|
|
179
|
-
if (gate?.decision === 'block') { process.stderr.write(`${gate.message}\n`); process.exit(
|
|
207
|
+
if (gate?.decision === 'block') { process.stderr.write(`${gate.message}\n`); process.exit(BLOCK_EXIT); }
|
|
180
208
|
if (gate?.decision === 'warn') notes.push(gate.message);
|
|
181
209
|
if (personal && tokenPolicy() === 'off') leave(0);
|
|
182
210
|
const slice = personal ? { decision: 'allow' } : sliceRuntime.sliceGuard(root, command, sliceClient(), cwd);
|
|
183
|
-
if (slice.decision === 'block') { process.stderr.write(`${slice.message}\n`); process.exit(
|
|
211
|
+
if (slice.decision === 'block') { process.stderr.write(`${slice.message}\n`); process.exit(BLOCK_EXIT); }
|
|
184
212
|
if (slice.decision === 'warn') notes.push(slice.message);
|
|
185
|
-
if (!
|
|
213
|
+
if (!hasProjectDir()) leave(0);
|
|
186
214
|
// Every segment is judged — `echo x && git push`, `ls; rm -rf build`, `cat x | git push` carry the action in a later one
|
|
187
215
|
// Vibe gates its own execution; its approval and inbox commands must remain reachable.
|
|
188
216
|
const segments = command.split(/&&|\|\||;|\||\n/).map((s) => s.trim()).filter((s) => s && !READS_ONLY.test(s) && !/^vibe(?:\s|$)/.test(s));
|
|
@@ -190,8 +218,8 @@ if (mode === 'pre') {
|
|
|
190
218
|
for (const [action, re] of IRREVERSIBLE) {
|
|
191
219
|
if (segments.some((s) => re.test(s)) && !recentAuthorize(action)) {
|
|
192
220
|
const blocking = tokenPolicy() !== 'off';
|
|
193
|
-
process.stderr.write(
|
|
194
|
-
if (blocking) process.exit(
|
|
221
|
+
process.stderr.write(`${irreversibleNotice(action, blocking)}\n`);
|
|
222
|
+
if (blocking) process.exit(BLOCK_EXIT);
|
|
195
223
|
leave(0);
|
|
196
224
|
}
|
|
197
225
|
}
|
|
@@ -200,8 +228,8 @@ if (mode === 'pre') {
|
|
|
200
228
|
|
|
201
229
|
// Inside the npm package dist/cli.js sits next to us; inside a plugin tree it does not, so fall back to `vibe` on PATH.
|
|
202
230
|
const result = fs.existsSync(cli)
|
|
203
|
-
? spawnSync(process.execPath, [cli, 'state', '--json'], { cwd: root, encoding: 'utf-8', timeout:
|
|
204
|
-
: spawnSync('vibe', ['state', '--json'], { cwd: root, encoding: 'utf-8', timeout:
|
|
231
|
+
? spawnSync(process.execPath, [cli, 'state', '--json'], { cwd: root, encoding: 'utf-8', timeout: STATE_TIMEOUT_MS })
|
|
232
|
+
: spawnSync('vibe', ['state', '--json'], { cwd: root, encoding: 'utf-8', timeout: STATE_TIMEOUT_MS, shell: process.platform === 'win32' });
|
|
205
233
|
if (result.status !== 0 || !result.stdout) process.exit(0);
|
|
206
234
|
try {
|
|
207
235
|
const view = JSON.parse(result.stdout);
|
package/hooks/obsidian.cjs
CHANGED
|
@@ -4,16 +4,22 @@ const fs = require('node:fs');
|
|
|
4
4
|
const path = require('node:path');
|
|
5
5
|
const { createHash, randomBytes } = require('node:crypto');
|
|
6
6
|
|
|
7
|
+
const REGISTRY_FILE = 'obsidian.json';
|
|
8
|
+
const VAULT_ID_CHARS = 16;
|
|
9
|
+
const TEMP_SUFFIX_BYTES = 6;
|
|
10
|
+
|
|
7
11
|
function readJson(file) {
|
|
8
12
|
try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; }
|
|
9
13
|
}
|
|
10
14
|
|
|
11
15
|
/** Where Obsidian keeps its vault list on each platform, including the Flatpak build on Linux. */
|
|
12
16
|
function obsidianRegistries(home, platform = process.platform, env = process.env) {
|
|
13
|
-
if (platform === 'darwin') return [path.join(home, 'Library', 'Application Support', 'obsidian',
|
|
14
|
-
if (platform === 'win32') return [path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'obsidian',
|
|
15
|
-
return [
|
|
16
|
-
path.join(
|
|
17
|
+
if (platform === 'darwin') return [path.join(home, 'Library', 'Application Support', 'obsidian', REGISTRY_FILE)];
|
|
18
|
+
if (platform === 'win32') return [path.join(env.APPDATA || path.join(home, 'AppData', 'Roaming'), 'obsidian', REGISTRY_FILE)];
|
|
19
|
+
return [
|
|
20
|
+
path.join(env.XDG_CONFIG_HOME || path.join(home, '.config'), 'obsidian', REGISTRY_FILE),
|
|
21
|
+
path.join(home, '.var', 'app', 'md.obsidian.Obsidian', 'config', 'obsidian', REGISTRY_FILE),
|
|
22
|
+
];
|
|
17
23
|
}
|
|
18
24
|
|
|
19
25
|
/** Registered vaults that still exist, by real path: the open one first, then the most recently used. */
|
|
@@ -29,7 +35,11 @@ function obsidianVaults(home, platform, env) {
|
|
|
29
35
|
return [...found.values()].sort((a, b) => Number(b.open) - Number(a.open) || b.ts - a.ts);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
|
|
38
|
+
// A folder name in a path, and the service whose sync folder carries it.
|
|
39
|
+
const CLOUD = [
|
|
40
|
+
['Mobile Documents', 'iCloud'], ['iCloud', 'iCloud'], ['Dropbox', 'Dropbox'], ['OneDrive', 'OneDrive'],
|
|
41
|
+
['Google Drive', 'Google Drive'], ['CloudStorage', 'a cloud drive'],
|
|
42
|
+
];
|
|
33
43
|
|
|
34
44
|
/** The service that copies this folder off the computer, when its path or Obsidian's own Sync plugin shows one. */
|
|
35
45
|
function syncedBy(folder) {
|
|
@@ -76,9 +86,9 @@ function connectVault(folder, home, platform, env) {
|
|
|
76
86
|
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) return 'waiting';
|
|
77
87
|
const real = fs.realpathSync(folder);
|
|
78
88
|
if (obsidianVaults(home, platform, env).some(v => real === v.path || real.startsWith(`${v.path}${path.sep}`))) return 'in-vault';
|
|
79
|
-
const id = createHash('sha256').update(real).digest('hex').slice(0,
|
|
89
|
+
const id = createHash('sha256').update(real).digest('hex').slice(0, VAULT_ID_CHARS);
|
|
80
90
|
const next = { ...doc, vaults: { ...(doc.vaults ?? {}), [id]: { path: real, ts: Date.now() } } };
|
|
81
|
-
const temporary = `${registry}.${randomBytes(
|
|
91
|
+
const temporary = `${registry}.${randomBytes(TEMP_SUFFIX_BYTES).toString('hex')}.tmp`;
|
|
82
92
|
fs.writeFileSync(temporary, JSON.stringify(next));
|
|
83
93
|
fs.renameSync(temporary, registry);
|
|
84
94
|
return 'connected';
|
package/hooks/private-store.cjs
CHANGED
|
@@ -3,31 +3,44 @@ const os = require('node:os');
|
|
|
3
3
|
const path = require('node:path');
|
|
4
4
|
const { randomBytes } = require('node:crypto');
|
|
5
5
|
|
|
6
|
+
const STORE_DIR = '.vibe-runtime';
|
|
7
|
+
const DIRECTORY_MODE = 0o700;
|
|
8
|
+
const FILE_MODE = 0o600;
|
|
9
|
+
// Any group or other permission bit.
|
|
10
|
+
const OTHERS_MASK = 0o077;
|
|
11
|
+
const DEFAULT_MAX_BYTES = 2 * 1024 * 1024;
|
|
12
|
+
const TEMP_SUFFIX_BYTES = 12;
|
|
13
|
+
const ENTRY_NAME = /^[a-z0-9.-]+$/i;
|
|
14
|
+
const LINKED_ENTRY = 'local store entry must be a real private directory or regular file, never linked';
|
|
15
|
+
const PERMISSIONS = 'local store permissions must be owner-private (0700 directory, 0600 file)';
|
|
16
|
+
const CHANGED_ENTRY = 'local store entry changed or exceeds its read limit';
|
|
17
|
+
|
|
6
18
|
function inside(parent, file) {
|
|
7
19
|
const relative = path.relative(parent, file);
|
|
8
20
|
return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative));
|
|
9
21
|
}
|
|
10
22
|
function privateEntry(stat, directory) {
|
|
11
|
-
|
|
12
|
-
if (
|
|
23
|
+
const wrongKind = directory ? !stat.isDirectory() : !stat.isFile();
|
|
24
|
+
if (stat.isSymbolicLink() || wrongKind || (!directory && stat.nlink !== 1)) throw new Error(LINKED_ENTRY);
|
|
25
|
+
if (process.platform !== 'win32' && (stat.uid !== process.getuid?.() || (stat.mode & OTHERS_MASK) !== 0)) throw new Error(PERMISSIONS);
|
|
13
26
|
return stat;
|
|
14
27
|
}
|
|
15
28
|
function privateStat(file, directory) { return privateEntry(fs.lstatSync(file), directory); }
|
|
16
29
|
function privateDirectory(root, create, homeDirectory = os.homedir()) {
|
|
17
30
|
const home = fs.realpathSync(homeDirectory);
|
|
18
|
-
const dir = path.join(home,
|
|
31
|
+
const dir = path.join(home, STORE_DIR);
|
|
19
32
|
if (inside(fs.realpathSync(root), dir)) throw new Error('local execution consent storage must be outside the project');
|
|
20
33
|
if (!fs.lstatSync(dir, { throwIfNoEntry: false })) {
|
|
21
34
|
if (!create) return dir;
|
|
22
|
-
fs.mkdirSync(dir, { mode:
|
|
35
|
+
fs.mkdirSync(dir, { mode: DIRECTORY_MODE });
|
|
23
36
|
}
|
|
24
37
|
privateStat(dir, true);
|
|
25
38
|
return dir;
|
|
26
39
|
}
|
|
27
40
|
function entryName(name) {
|
|
28
|
-
if (
|
|
41
|
+
if (!ENTRY_NAME.test(name)) throw new Error('invalid local store entry name');
|
|
29
42
|
}
|
|
30
|
-
function readPrivate(root, name, maxBytes =
|
|
43
|
+
function readPrivate(root, name, maxBytes = DEFAULT_MAX_BYTES, homeDirectory) {
|
|
31
44
|
entryName(name);
|
|
32
45
|
const dir = privateDirectory(root, false, homeDirectory);
|
|
33
46
|
if (!fs.existsSync(dir)) return null;
|
|
@@ -37,11 +50,13 @@ function readPrivate(root, name, maxBytes = 2097152, homeDirectory) {
|
|
|
37
50
|
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0));
|
|
38
51
|
try {
|
|
39
52
|
const actual = privateEntry(fs.fstatSync(fd), false);
|
|
40
|
-
if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size > maxBytes) throw new Error(
|
|
53
|
+
if (actual.dev !== before.dev || actual.ino !== before.ino || actual.size > maxBytes) throw new Error(CHANGED_ENTRY);
|
|
41
54
|
const buffer = Buffer.alloc(maxBytes + 1);
|
|
42
55
|
const length = fs.readSync(fd, buffer, 0, buffer.length, 0);
|
|
43
56
|
const after = privateStat(file, false);
|
|
44
|
-
|
|
57
|
+
const same = after.ino === actual.ino && after.dev === actual.dev && after.size === actual.size
|
|
58
|
+
&& after.mtimeMs === actual.mtimeMs && after.ctimeMs === actual.ctimeMs;
|
|
59
|
+
if (length > maxBytes || length !== actual.size || !same) throw new Error(CHANGED_ENTRY);
|
|
45
60
|
return buffer.subarray(0, length).toString('utf8');
|
|
46
61
|
} finally { fs.closeSync(fd); }
|
|
47
62
|
}
|
|
@@ -50,8 +65,8 @@ function writePrivate(root, name, text, homeDirectory) {
|
|
|
50
65
|
const dir = privateDirectory(root, true, homeDirectory);
|
|
51
66
|
const file = path.join(dir, name);
|
|
52
67
|
if (fs.lstatSync(file, { throwIfNoEntry: false })) privateStat(file, false);
|
|
53
|
-
const temporary = path.join(dir, `${name}.${randomBytes(
|
|
54
|
-
fs.writeFileSync(temporary, text, { flag: 'wx', mode:
|
|
68
|
+
const temporary = path.join(dir, `${name}.${randomBytes(TEMP_SUFFIX_BYTES).toString('hex')}.tmp`);
|
|
69
|
+
fs.writeFileSync(temporary, text, { flag: 'wx', mode: FILE_MODE });
|
|
55
70
|
try { privateStat(dir, true); fs.renameSync(temporary, file); }
|
|
56
71
|
finally { if (fs.existsSync(temporary)) fs.unlinkSync(temporary); }
|
|
57
72
|
return file;
|
package/hooks/retest.cjs
CHANGED
|
@@ -5,19 +5,32 @@
|
|
|
5
5
|
// in front of a command lets one run through when the user asked for it or the test depends on more than files.
|
|
6
6
|
const fs = require('node:fs');
|
|
7
7
|
const path = require('node:path');
|
|
8
|
+
const { vibeConfig, SETTINGS } = require('./locations.cjs');
|
|
8
9
|
const { spawnSync } = require('node:child_process');
|
|
9
10
|
const { createHash } = require('node:crypto');
|
|
10
|
-
const { segments, runOutcome } = require('./knowledge-journal.cjs');
|
|
11
|
+
const { DAY, segments, runOutcome } = require('./knowledge-journal.cjs');
|
|
11
12
|
const { readPrivate, writePrivate } = require('./private-store.cjs');
|
|
12
13
|
|
|
13
|
-
const DAY = 86400000;
|
|
14
14
|
const MODES = ['block', 'warn', 'off'];
|
|
15
15
|
const KEEP_PASSED = 100;
|
|
16
16
|
const KEEP_EVENTS = 500;
|
|
17
17
|
const MAX_FILES = 2000;
|
|
18
|
-
|
|
18
|
+
// A file larger than this is fingerprinted by size and modification time instead of its content.
|
|
19
|
+
const HASH_LIMIT = 16 * 1024 * 1024;
|
|
20
|
+
const STORE_MAX_BYTES = 1024 * 1024;
|
|
21
|
+
const GIT_TIMEOUT_MS = 3000;
|
|
22
|
+
// How long the gate's own stops and warnings are kept and reported.
|
|
23
|
+
const EVENT_DAYS = 30;
|
|
24
|
+
const STORE_HASH_CHARS = 32;
|
|
25
|
+
const STATE_HASH_CHARS = 16;
|
|
26
|
+
const SUMMARY_COMMANDS = 10;
|
|
27
|
+
// The `HH:MM` part of an ISO time.
|
|
28
|
+
const CLOCK_START = 11;
|
|
29
|
+
const CLOCK_END = 16;
|
|
30
|
+
// `git status --porcelain=v1` puts two status letters and a space before the path.
|
|
31
|
+
const STATUS_PREFIX = 3;
|
|
19
32
|
|
|
20
|
-
const settingsFile = home =>
|
|
33
|
+
const settingsFile = home => vibeConfig(home, SETTINGS.retest);
|
|
21
34
|
function mode(home) {
|
|
22
35
|
try { const raw = JSON.parse(fs.readFileSync(settingsFile(home), 'utf8')); return MODES.includes(raw.mode) ? raw.mode : 'block'; } catch { return 'block'; }
|
|
23
36
|
}
|
|
@@ -51,7 +64,7 @@ function testCommand(command) {
|
|
|
51
64
|
const bypassed = command => segments(unwrapped(command)).some(part => /^(?:[A-Za-z_][A-Za-z0-9_]*=\S*\s+)*VIBE_RETEST=1(?:\s|$)/.test(part));
|
|
52
65
|
|
|
53
66
|
function git(cwd, args) {
|
|
54
|
-
const r = spawnSync('git', args, { cwd, encoding: 'buffer', timeout:
|
|
67
|
+
const r = spawnSync('git', args, { cwd, encoding: 'buffer', timeout: GIT_TIMEOUT_MS, windowsHide: true });
|
|
55
68
|
return r.status === 0 ? r.stdout : null;
|
|
56
69
|
}
|
|
57
70
|
function fileId(file) {
|
|
@@ -70,17 +83,17 @@ function workState(cwd) {
|
|
|
70
83
|
const [root, head] = top;
|
|
71
84
|
const status = git(root, ['status', '--porcelain=v1', '-z', '--untracked-files=all', '--no-renames']);
|
|
72
85
|
if (status === null) return null;
|
|
73
|
-
const files = status.toString('utf8').split('\0').filter(Boolean).map(entry => entry.slice(
|
|
86
|
+
const files = status.toString('utf8').split('\0').filter(Boolean).map(entry => entry.slice(STATUS_PREFIX)).filter(file => !file.startsWith('.vibe/')).sort();
|
|
74
87
|
if (files.length > MAX_FILES) return null;
|
|
75
88
|
const hash = createHash('sha256').update(head);
|
|
76
89
|
for (const file of files) hash.update(`\n${file}:${fileId(path.join(root, file))}`);
|
|
77
|
-
return { root: fs.realpathSync(root), state: hash.digest('hex').slice(0,
|
|
90
|
+
return { root: fs.realpathSync(root), state: hash.digest('hex').slice(0, STATE_HASH_CHARS) };
|
|
78
91
|
}
|
|
79
92
|
|
|
80
|
-
const storeName = root => `retest-${createHash('sha256').update(root).digest('hex').slice(0,
|
|
93
|
+
const storeName = root => `retest-${createHash('sha256').update(root).digest('hex').slice(0, STORE_HASH_CHARS)}.json`;
|
|
81
94
|
function readStore(root, home) {
|
|
82
95
|
try {
|
|
83
|
-
const raw = JSON.parse(readPrivate(root, storeName(root),
|
|
96
|
+
const raw = JSON.parse(readPrivate(root, storeName(root), STORE_MAX_BYTES, home) ?? '{}');
|
|
84
97
|
return { passed: Array.isArray(raw.passed) ? raw.passed : [], events: Array.isArray(raw.events) ? raw.events : [] };
|
|
85
98
|
} catch { return { passed: [], events: [] }; }
|
|
86
99
|
}
|
|
@@ -91,8 +104,18 @@ function where(payload) {
|
|
|
91
104
|
const cwd = input.cwd ?? input.workdir ?? payload.cwd ?? process.cwd();
|
|
92
105
|
return path.resolve(typeof cwd === 'string' ? cwd : process.cwd());
|
|
93
106
|
}
|
|
94
|
-
|
|
95
|
-
|
|
107
|
+
function folderOf(root, cwd) {
|
|
108
|
+
let real = cwd;
|
|
109
|
+
try { real = fs.realpathSync(cwd); } catch { /* a folder that is gone is compared as given */ }
|
|
110
|
+
return path.relative(root, real).split(path.sep).join('/');
|
|
111
|
+
}
|
|
112
|
+
const clock = at => new Date(at).toISOString().slice(CLOCK_START, CLOCK_END);
|
|
113
|
+
|
|
114
|
+
const BLOCKED = [
|
|
115
|
+
'not run again. Use that result.',
|
|
116
|
+
'Run it again only when the user asks or the test depends on something outside the files (a service, data, the clock):',
|
|
117
|
+
'put VIBE_RETEST=1 in front of the command.',
|
|
118
|
+
].join(' ');
|
|
96
119
|
|
|
97
120
|
/** PreToolUse(Bash): null to let the command run, or the decision and the message for the model. */
|
|
98
121
|
function before(payload, home, now = Date.now()) {
|
|
@@ -107,11 +130,11 @@ function before(payload, home, now = Date.now()) {
|
|
|
107
130
|
const folder = folderOf(work.root, cwd);
|
|
108
131
|
const hit = store.passed.find(p => p.cmd === key && p.cwd === folder && p.state === work.state);
|
|
109
132
|
if (!hit) return null;
|
|
110
|
-
store.events = [...store.events.filter(e => e.at >= now -
|
|
133
|
+
store.events = [...store.events.filter(e => e.at >= now - EVENT_DAYS * DAY), { at: now, cmd: key, action: setting }].slice(-KEEP_EVENTS);
|
|
111
134
|
writeStore(work.root, store, home);
|
|
112
135
|
const said = `[vibe] \`${key}\` already passed on exactly this code at ${clock(hit.at)} UTC and no file has changed since`;
|
|
113
136
|
return setting === 'block'
|
|
114
|
-
? { decision: 'block', message: `${said}:
|
|
137
|
+
? { decision: 'block', message: `${said}: ${BLOCKED}` }
|
|
115
138
|
: { decision: 'warn', message: `${said}; running it again repeats a known result unless the test depends on something outside the files.` };
|
|
116
139
|
}
|
|
117
140
|
|
|
@@ -132,13 +155,13 @@ function after(payload, home, now = Date.now()) {
|
|
|
132
155
|
function summary(cwd, home, now = Date.now()) {
|
|
133
156
|
let root;
|
|
134
157
|
try { root = fs.realpathSync(git(cwd, ['rev-parse', '--show-toplevel'])?.toString('utf8').trim() || cwd); } catch { root = path.resolve(cwd); }
|
|
135
|
-
const events = readStore(root, home).events.filter(e => e.at >= now -
|
|
158
|
+
const events = readStore(root, home).events.filter(e => e.at >= now - EVENT_DAYS * DAY);
|
|
136
159
|
const counts = new Map();
|
|
137
160
|
for (const e of events) counts.set(e.cmd, (counts.get(e.cmd) ?? 0) + 1);
|
|
138
161
|
return {
|
|
139
|
-
mode: mode(home), days:
|
|
162
|
+
mode: mode(home), days: EVENT_DAYS,
|
|
140
163
|
stopped: events.filter(e => e.action === 'block').length, warned: events.filter(e => e.action === 'warn').length,
|
|
141
|
-
commands: [...counts].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0,
|
|
164
|
+
commands: [...counts].map(([command, count]) => ({ command, count })).sort((a, b) => b.count - a.count).slice(0, SUMMARY_COMMANDS),
|
|
142
165
|
};
|
|
143
166
|
}
|
|
144
167
|
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
const fs = require('node:fs');
|
|
2
2
|
const os = require('node:os');
|
|
3
3
|
const path = require('node:path');
|
|
4
|
+
const { vibeConfig, FOLDERS } = require('./locations.cjs');
|
|
4
5
|
const { readPrivate } = require('./private-store.cjs');
|
|
5
6
|
const { bounded, digest } = require('./session-files.cjs');
|
|
6
7
|
|
|
7
|
-
const
|
|
8
|
+
const WINDOWS_SHELL = 'C:\\Windows\\System32\\cmd.exe';
|
|
9
|
+
const POSIX_SHELL = '/bin/sh';
|
|
10
|
+
const MAX_CHECKS = 1000;
|
|
11
|
+
const MAX_VERIFIERS = 512;
|
|
12
|
+
const VERIFIER_FILE_MAX_BYTES = 1024 * 1024;
|
|
13
|
+
const VERIFIER_TOTAL_MAX_BYTES = 32 * 1024 * 1024;
|
|
14
|
+
const VERIFIER_DEADLINE_MS = 1000;
|
|
15
|
+
const VERIFIER_LIMIT = 'verifier limit';
|
|
16
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
17
|
+
// The environment a recorded plan must still see unchanged.
|
|
18
|
+
const ENVIRONMENT = [
|
|
19
|
+
'HOME', 'USERPROFILE', 'VIBE_HOME_DIR', 'VIBE_REVIEW_CMD', 'VIBE_REVIEW_CLIENT', 'VIBE_REVIEWER_MODEL', 'VIBE_REVIEWER_EFFORT',
|
|
20
|
+
];
|
|
8
21
|
const fingerprint = value => digest(JSON.stringify(value));
|
|
9
22
|
function resolvedDirectory(directory) {
|
|
10
23
|
let existing = path.resolve(directory);
|
|
@@ -13,31 +26,35 @@ function resolvedDirectory(directory) {
|
|
|
13
26
|
return path.join(fs.realpathSync(existing), ...suffix);
|
|
14
27
|
}
|
|
15
28
|
function runtimeMatches(root, plan) {
|
|
16
|
-
const shell = process.platform === 'win32' ? process.env.ComSpec ??
|
|
17
|
-
|
|
29
|
+
const shell = process.platform === 'win32' ? process.env.ComSpec ?? WINDOWS_SHELL : POSIX_SHELL;
|
|
30
|
+
const sameRuntime = plan.project === fs.realpathSync(root) && plan.platform === process.platform
|
|
31
|
+
&& plan.shell === fs.realpathSync(shell) && plan.node === fs.realpathSync(process.execPath);
|
|
32
|
+
if (!sameRuntime) return false;
|
|
18
33
|
if (plan.path !== (process.env.PATH ?? '') || plan.pathExt !== (process.env.PATHEXT ?? '')) return false;
|
|
19
34
|
if (!plan.environment || ENVIRONMENT.some(key => plan.environment[key] !== (process.env[key] ?? null))) return false;
|
|
20
|
-
const
|
|
35
|
+
const readerFolder = vibeConfig(process.env.VIBE_HOME_DIR ?? os.homedir(), FOLDERS.reader);
|
|
36
|
+
const reviewerCwd = process.env.VIBE_REVIEW_CMD ? root : resolvedDirectory(readerFolder);
|
|
21
37
|
if (!plan.reviewer || plan.reviewer.cwd !== reviewerCwd) return false;
|
|
22
38
|
return plan.checks.every(item => {
|
|
23
|
-
const
|
|
39
|
+
const { type, cwd } = item.check;
|
|
40
|
+
const expected = type === 'review' ? reviewerCwd : fs.realpathSync(type === 'run' && cwd ? path.resolve(root, cwd) : root);
|
|
24
41
|
return item.cwd === expected;
|
|
25
42
|
});
|
|
26
43
|
}
|
|
27
44
|
function verifiersMatch(plan) {
|
|
28
45
|
const seen = new Map();
|
|
29
|
-
const deadline = Date.now() +
|
|
46
|
+
const deadline = Date.now() + VERIFIER_DEADLINE_MS;
|
|
30
47
|
let bytes = 0;
|
|
31
48
|
for (const item of plan.checks) {
|
|
32
|
-
if (!Array.isArray(item.verifiers) || item.verifiers.length >
|
|
49
|
+
if (!Array.isArray(item.verifiers) || item.verifiers.length > MAX_VERIFIERS) throw new Error(VERIFIER_LIMIT);
|
|
33
50
|
for (const verifier of item.verifiers) {
|
|
34
|
-
if (Date.now() > deadline) throw new Error(
|
|
35
|
-
if (typeof verifier.file !== 'string' || !path.isAbsolute(verifier.file) ||
|
|
51
|
+
if (Date.now() > deadline) throw new Error(VERIFIER_LIMIT);
|
|
52
|
+
if (typeof verifier.file !== 'string' || !path.isAbsolute(verifier.file) || !SHA256.test(verifier.sha256)) return false;
|
|
36
53
|
if (!seen.has(verifier.file)) {
|
|
37
|
-
if (seen.size >=
|
|
38
|
-
const content = bounded(verifier.file,
|
|
54
|
+
if (seen.size >= MAX_VERIFIERS) throw new Error(VERIFIER_LIMIT);
|
|
55
|
+
const content = bounded(verifier.file, VERIFIER_FILE_MAX_BYTES);
|
|
39
56
|
bytes += content.length;
|
|
40
|
-
if (bytes >
|
|
57
|
+
if (bytes > VERIFIER_TOTAL_MAX_BYTES) throw new Error(VERIFIER_LIMIT);
|
|
41
58
|
seen.set(verifier.file, digest(content));
|
|
42
59
|
}
|
|
43
60
|
if (seen.get(verifier.file) !== verifier.sha256) return false;
|
|
@@ -47,7 +64,7 @@ function verifiersMatch(plan) {
|
|
|
47
64
|
}
|
|
48
65
|
function contextStatus(root, plan) {
|
|
49
66
|
try {
|
|
50
|
-
if (!plan || plan.schemaVersion !== 1 || !Array.isArray(plan.checks) || plan.checks.length >
|
|
67
|
+
if (!plan || plan.schemaVersion !== 1 || !Array.isArray(plan.checks) || plan.checks.length > MAX_CHECKS) return 'unavailable';
|
|
51
68
|
const receipt = readPrivate(root, `consent-${fingerprint(root)}.json`);
|
|
52
69
|
if (receipt === null) return 'stale';
|
|
53
70
|
const saved = JSON.parse(receipt);
|