context-mode 1.0.126 → 1.0.128
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/.openclaw-plugin/openclaw.plugin.json +1 -1
- package/.openclaw-plugin/package.json +1 -1
- package/build/cli.js +31 -0
- package/build/db-base.js +53 -4
- package/build/server.js +7 -0
- package/build/util/db-lock.d.ts +65 -0
- package/build/util/db-lock.js +166 -0
- package/build/util/project-dir.d.ts +13 -0
- package/build/util/project-dir.js +11 -2
- package/build/util/sibling-mcp.d.ts +79 -0
- package/build/util/sibling-mcp.js +181 -0
- package/cli.bundle.mjs +131 -131
- package/hooks/core/routing.mjs +114 -22
- package/hooks/gemini-cli/sessionstart.mjs +8 -6
- package/hooks/security.bundle.mjs +1 -0
- package/hooks/session-db.bundle.mjs +2 -2
- package/hooks/sessionstart.mjs +18 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +3 -3
- package/scripts/plugin-cache-integrity.mjs +101 -21
- package/server.bundle.mjs +92 -92
package/hooks/core/routing.mjs
CHANGED
|
@@ -249,39 +249,88 @@ let securityInitFailed = false;
|
|
|
249
249
|
/**
|
|
250
250
|
* @returns {boolean} true if security module loaded successfully.
|
|
251
251
|
*
|
|
252
|
-
* Loud fail: if `build/security.js` is
|
|
253
|
-
* clear stderr warning instead of swallowing the error
|
|
254
|
-
* this, user-configured `permissions.deny` patterns
|
|
255
|
-
* with no indication that policy enforcement is
|
|
256
|
-
* security regression.
|
|
252
|
+
* Loud fail: if neither the esbuild bundle nor `build/security.js` is
|
|
253
|
+
* importable, log a clear stderr warning instead of swallowing the error
|
|
254
|
+
* silently. Without this, user-configured `permissions.deny` patterns
|
|
255
|
+
* (#466) become no-ops with no indication that policy enforcement is
|
|
256
|
+
* disabled — a fail-open security regression.
|
|
257
|
+
*
|
|
258
|
+
* ─── Resolution order (#558) ───────────────────────────────────────────
|
|
259
|
+
*
|
|
260
|
+
* 1. `hooks/security.bundle.mjs` — esbuild output, sibling of routing.mjs's
|
|
261
|
+
* parent. Marketplace installs (`git clone` install path) ship this
|
|
262
|
+
* bundle via CI's `git add -f`, so it's the only artifact reliably
|
|
263
|
+
* present across BOTH `npm install` (build/ generated by tsc) AND
|
|
264
|
+
* marketplace install (build/ excluded by .gitignore, never built).
|
|
265
|
+
*
|
|
266
|
+
* 2. `<buildDir>/security.js` — tsc output. Present after `npm run build`.
|
|
267
|
+
* Kept as a fallback so source checkouts that bypass `npm run bundle`
|
|
268
|
+
* still degrade gracefully to the tsc-emitted module.
|
|
269
|
+
*
|
|
270
|
+
* Bundle path is computed from `import.meta.url` (sibling layout:
|
|
271
|
+
* `hooks/core/routing.mjs` → `hooks/security.bundle.mjs`).
|
|
272
|
+
* `CONTEXT_MODE_SECURITY_BUNDLE_PATH` is a test seam — it lets
|
|
273
|
+
* subprocess-based tests stage a bundle in tmpdir without polluting the
|
|
274
|
+
* repo's hooks/ directory.
|
|
257
275
|
*/
|
|
258
276
|
export async function initSecurity(buildDir) {
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
277
|
+
const { existsSync } = await import("node:fs");
|
|
278
|
+
const { resolve, dirname } = await import("node:path");
|
|
279
|
+
const { fileURLToPath, pathToFileURL } = await import("node:url");
|
|
280
|
+
|
|
281
|
+
// Default: <hooks/core/ dir>/../security.bundle.mjs → hooks/security.bundle.mjs.
|
|
282
|
+
const defaultBundlePath = resolve(
|
|
283
|
+
dirname(fileURLToPath(import.meta.url)),
|
|
284
|
+
"..",
|
|
285
|
+
"security.bundle.mjs",
|
|
286
|
+
);
|
|
287
|
+
const bundlePath = process.env.CONTEXT_MODE_SECURITY_BUNDLE_PATH || defaultBundlePath;
|
|
288
|
+
const secPath = resolve(buildDir, "security.js");
|
|
289
|
+
|
|
290
|
+
// Bundle-first: marketplace installs ship the bundle, never the build/ dir.
|
|
291
|
+
if (existsSync(bundlePath)) {
|
|
292
|
+
try {
|
|
293
|
+
security = await import(pathToFileURL(bundlePath).href);
|
|
294
|
+
return true;
|
|
295
|
+
} catch (err) {
|
|
265
296
|
if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
|
|
266
297
|
process.stderr.write(
|
|
267
|
-
`[context-mode] WARNING: ${
|
|
268
|
-
`Run \`npm run build\` to generate it. Set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1 to silence.\n`,
|
|
298
|
+
`[context-mode] WARNING: failed to load security bundle (${bundlePath}) — deny patterns NOT enforced: ${err?.message ?? err}\n`,
|
|
269
299
|
);
|
|
270
300
|
}
|
|
271
301
|
securityInitFailed = true;
|
|
272
302
|
return false;
|
|
273
303
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Fallback: tsc-emitted build/security.js (source checkout + `npm run build`).
|
|
307
|
+
if (existsSync(secPath)) {
|
|
308
|
+
try {
|
|
309
|
+
security = await import(pathToFileURL(secPath).href);
|
|
310
|
+
return true;
|
|
311
|
+
} catch (err) {
|
|
312
|
+
if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
|
|
313
|
+
process.stderr.write(
|
|
314
|
+
`[context-mode] WARNING: failed to load security module — deny patterns NOT enforced: ${err?.message ?? err}\n`,
|
|
315
|
+
);
|
|
316
|
+
}
|
|
317
|
+
securityInitFailed = true;
|
|
318
|
+
return false;
|
|
281
319
|
}
|
|
282
|
-
securityInitFailed = true;
|
|
283
|
-
return false;
|
|
284
320
|
}
|
|
321
|
+
|
|
322
|
+
// Neither artifact present — preserve fail-open with an actionable warning
|
|
323
|
+
// that mentions BOTH paths so users on either install model can self-diagnose.
|
|
324
|
+
if (!securityInitFailed && !process.env.CONTEXT_MODE_SUPPRESS_SECURITY_WARNING) {
|
|
325
|
+
process.stderr.write(
|
|
326
|
+
`[context-mode] WARNING: security module not found — security deny patterns will NOT be enforced.\n` +
|
|
327
|
+
` Searched: ${bundlePath} (bundle) and ${secPath} (build).\n` +
|
|
328
|
+
` Marketplace installs ship hooks/security.bundle.mjs via CI; for source checkouts run \`npm run bundle\` (or \`npm run build\`).\n` +
|
|
329
|
+
` Set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1 to silence.\n`,
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
securityInitFailed = true;
|
|
333
|
+
return false;
|
|
285
334
|
}
|
|
286
335
|
|
|
287
336
|
/** @returns {boolean} true if a previous initSecurity() call failed to load the module. */
|
|
@@ -289,6 +338,49 @@ export function isSecurityInitFailed() {
|
|
|
289
338
|
return securityInitFailed;
|
|
290
339
|
}
|
|
291
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Build the agent-facing additionalContext block surfacing the security
|
|
343
|
+
* init failure (#558).
|
|
344
|
+
*
|
|
345
|
+
* Pre-558 the only signal of a fail-open security regression was a
|
|
346
|
+
* stderr WARNING line that adapters typically suppress / discard. The
|
|
347
|
+
* user had no in-band signal that `permissions.deny` was no-op'd.
|
|
348
|
+
*
|
|
349
|
+
* Returns a structured XML-ish block when initSecurity() has failed,
|
|
350
|
+
* `null` otherwise. SessionStart hooks append the block to their
|
|
351
|
+
* additionalContext so the agent (and through the agent, the user)
|
|
352
|
+
* sees the warning the next time they view the session — not just in
|
|
353
|
+
* suppressed stderr.
|
|
354
|
+
*
|
|
355
|
+
* The block format intentionally mirrors the `<context_guidance>`
|
|
356
|
+
* shape used elsewhere in routing so existing prompt-template
|
|
357
|
+
* scaffolding picks it up without special-casing.
|
|
358
|
+
*/
|
|
359
|
+
export function buildSecurityWarningContext() {
|
|
360
|
+
if (!securityInitFailed) return null;
|
|
361
|
+
return [
|
|
362
|
+
"<context_mode_security_warning>",
|
|
363
|
+
" <severity>HIGH</severity>",
|
|
364
|
+
" <issue>",
|
|
365
|
+
" The context-mode security module failed to load.",
|
|
366
|
+
" User-configured `permissions.deny` patterns are NOT being enforced.",
|
|
367
|
+
" Bash commands and file operations bypass the deny gate (fail-open).",
|
|
368
|
+
" </issue>",
|
|
369
|
+
" <root_cause>",
|
|
370
|
+
" `hooks/security.bundle.mjs` (and `build/security.js`) are absent or unloadable.",
|
|
371
|
+
" Common on marketplace installs where `build/` is gitignored and the",
|
|
372
|
+
" bundle was missing prior to v1.0.127.",
|
|
373
|
+
" </root_cause>",
|
|
374
|
+
" <fix>",
|
|
375
|
+
" Run `npm run bundle` from the context-mode source checkout, OR",
|
|
376
|
+
" upgrade context-mode to v1.0.127+ (which ships hooks/security.bundle.mjs",
|
|
377
|
+
" via CI). To opt in to fail-CLOSED instead, set CONTEXT_MODE_REQUIRE_SECURITY=1.",
|
|
378
|
+
" To silence this warning while you investigate, set CONTEXT_MODE_SUPPRESS_SECURITY_WARNING=1.",
|
|
379
|
+
" </fix>",
|
|
380
|
+
"</context_mode_security_warning>",
|
|
381
|
+
].join("\n");
|
|
382
|
+
}
|
|
383
|
+
|
|
292
384
|
/**
|
|
293
385
|
* Normalize platform-specific tool names to canonical (Claude Code) names.
|
|
294
386
|
*
|
|
@@ -25,7 +25,7 @@ import { createSessionLoaders } from "../session-loaders.mjs";
|
|
|
25
25
|
import { join, dirname } from "node:path";
|
|
26
26
|
import { readFileSync, unlinkSync } from "node:fs";
|
|
27
27
|
import { homedir } from "node:os";
|
|
28
|
-
import { fileURLToPath
|
|
28
|
+
import { fileURLToPath } from "node:url";
|
|
29
29
|
|
|
30
30
|
const HOOK_DIR = dirname(fileURLToPath(import.meta.url));
|
|
31
31
|
const { loadSessionDB } = createSessionLoaders(HOOK_DIR);
|
|
@@ -88,11 +88,13 @@ try {
|
|
|
88
88
|
const sessionId = getSessionId(input, OPTS);
|
|
89
89
|
db.ensureSession(sessionId, projectDir);
|
|
90
90
|
|
|
91
|
-
//
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
91
|
+
// NOTE (#558): excised the old GEMINI.md auto-write block. It loaded an
|
|
92
|
+
// adapter from build/ (gitignored, missing on marketplace installs) and
|
|
93
|
+
// called a method that was deleted from every adapter in commit 6dae20c.
|
|
94
|
+
// Both layers were silently no-op'd by the surrounding try/catch on every
|
|
95
|
+
// install path for many releases. If routing-instruction auto-write is
|
|
96
|
+
// reintroduced it must come with its own PRD, method spec, and format
|
|
97
|
+
// tests — out of scope for the security regression fix.
|
|
96
98
|
|
|
97
99
|
const ruleFilePaths = [
|
|
98
100
|
join(homedir(), ".gemini", "GEMINI.md"),
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{readFileSync as S,realpathSync as R}from"node:fs";import{resolve as u}from"node:path";import{resolve as a}from"node:path";import{homedir as d}from"node:os";import{createRequire as C}from"node:module";function v(e=process.env){let s=e.CLAUDE_CONFIG_DIR;return s&&s.trim()!==""?s.startsWith("~")?a(d(),s.replace(/^~[/\\]?/,"")):a(s):a(d(),".claude")}function E(e=process.env){return a(v(e),"settings.json")}function h(e=process.env){let s=[],t=null,n=null;try{let r=C(import.meta.url)("../adapters/detect.js");t=r.detectPlatform(),n=r.getSessionDirSegments}catch{}if(t&&n&&t.platform!=="claude-code"){let o=n(t.platform);o&&o.length>0&&s.push(a(d(),...o,"settings.json"))}let i=E(e);return s.includes(i)||s.push(i),s}function w(e){let s=e.match(/^Bash\((.+)\)$/);return s?s[1]:null}function k(e){let s=e.match(/^(\w+)\((.+)\)$/);return s?{tool:s[1],glob:s[2]}:null}function $(e){return e.replace(/[.*+?^${}()|[\]\\\/\-]/g,"\\$&")}function P(e){return e.replace(/[.+?^${}()|[\]\\\/\-]/g,"\\$&").replace(/\*/g,".*")}function A(e,s=!1){let t,n=e.indexOf(":");if(n!==-1){let i=e.slice(0,n),o=e.slice(n+1),r=$(i),l=P(o);t=`^${r}(\\s${l})?$`}else t=`^${P(e)}$`;return new RegExp(t,s?"i":"")}function G(e,s=!1){let t="",n=0;for(;n<e.length;)e[n]==="*"&&e[n+1]==="*"?n+2<e.length&&e[n+2]==="/"?(t+="(.*/)?",n+=3):(t+=".*",n+=2):e[n]==="*"?(t+="[^/]*",n++):e[n]==="?"?(t+="[^/]",n++):(t+=e[n].replace(/[.+^${}()|[\]\\\/\-]/g,"\\$&"),n++);return new RegExp(`^${t}$`,s?"i":"")}function f(e,s,t=!1){for(let n of s){let i=w(n);if(i&&A(i,t).test(e))return n}return null}function b(e){let s=[],t="",n=!1,i=!1,o=!1;for(let r=0;r<e.length;r++){let l=e[r],c=r>0?e[r-1]:"";l==="'"&&!i&&!o&&c!=="\\"?(n=!n,t+=l):l==='"'&&!n&&!o&&c!=="\\"?(i=!i,t+=l):l==="`"&&!n&&!i&&c!=="\\"?(o=!o,t+=l):!n&&!i&&!o?l===";"?(s.push(t.trim()),t=""):l==="|"&&e[r+1]==="|"||l==="&"&&e[r+1]==="&"?(s.push(t.trim()),t="",r++):l==="|"?(s.push(t.trim()),t=""):t+=l:t+=l}return t.trim()&&s.push(t.trim()),s.filter(r=>r.length>0)}function m(e){let s;try{s=S(e,"utf-8")}catch{return null}let t;try{t=JSON.parse(s)}catch{return null}let n=t?.permissions;if(!n||typeof n!="object")return null;let i=o=>Array.isArray(o)?o.filter(r=>typeof r=="string"&&w(r)!==null):[];return{allow:i(n.allow),deny:i(n.deny),ask:i(n.ask)}}function L(e,s){let t=[];if(e){let i=u(e,".claude","settings.local.json"),o=m(i);o&&t.push(o);let r=u(e,".claude","settings.json"),l=m(r);l&&t.push(l)}let n=s!==void 0?[s]:h();for(let i of n){let o=m(i);o&&t.push(o)}return t}function M(e,s,t){let n=[],i=r=>{let l;try{l=S(r,"utf-8")}catch{return null}let c;try{c=JSON.parse(l)}catch{return null}let g=c?.permissions?.deny;if(!Array.isArray(g))return[];let y=[];for(let x of g){if(typeof x!="string")continue;let p=k(x);p&&p.tool===e&&y.push(p.glob)}return y};if(s){let r=i(u(s,".claude","settings.local.json"));r!==null&&n.push(r);let l=i(u(s,".claude","settings.json"));l!==null&&n.push(l)}let o=t!==void 0?[t]:h();for(let r of o){let l=i(r);l!==null&&n.push(l)}return n}function q(e,s,t=process.platform==="win32"){let n=b(e);for(let i of n)for(let o of s){let r=f(i,o.deny,t);if(r)return{decision:"deny",matchedPattern:r}}for(let i of s){let o=f(e,i.ask,t);if(o)return{decision:"ask",matchedPattern:o};let r=f(e,i.allow,t);if(r)return{decision:"allow",matchedPattern:r}}return{decision:"ask"}}function z(e,s,t=process.platform==="win32"){let n=b(e);for(let i of n)for(let o of s){let r=f(i,o.deny,t);if(r)return{decision:"deny",matchedPattern:r}}return{decision:"allow"}}function I(e,s,t=process.platform==="win32",n){let i=r=>r.replace(/\\/g,"/"),o=new Set;if(o.add(i(e)),n){let r=u(n,e);o.add(i(r));try{o.add(i(R(r)))}catch{}}for(let r of s)for(let l of r){let c=G(i(l),t);for(let g of o)if(c.test(g))return{denied:!0,matchedPattern:l}}return{denied:!1}}var _={python:[/os\.system\(\s*(['"])(.*?)\1\s*\)/g,/subprocess\.(?:run|call|Popen|check_output|check_call)\(\s*(['"])(.*?)\1/g],javascript:[/exec(?:Sync|File|FileSync)?\(\s*(['"`])(.*?)\1/g,/spawn(?:Sync)?\(\s*(['"`])(.*?)\1/g],typescript:[/exec(?:Sync|File|FileSync)?\(\s*(['"`])(.*?)\1/g,/spawn(?:Sync)?\(\s*(['"`])(.*?)\1/g],ruby:[/system\(\s*(['"])(.*?)\1/g,/`(.*?)`/g],go:[/exec\.Command\(\s*(['"`])(.*?)\1/g],php:[/shell_exec\(\s*(['"`])(.*?)\1/g,/(?:^|[^.])exec\(\s*(['"`])(.*?)\1/g,/(?:^|[^.])system\(\s*(['"`])(.*?)\1/g,/passthru\(\s*(['"`])(.*?)\1/g,/proc_open\(\s*(['"`])(.*?)\1/g],rust:[/Command::new\(\s*(['"`])(.*?)\1/g]};function F(e){let s=[],t=/subprocess\.(?:run|call|Popen|check_output|check_call)\(\s*\[([^\]]+)\]/g,n;for(;(n=t.exec(e))!==null;){let o=[...n[1].matchAll(/(['"])(.*?)\1/g)].map(r=>r[2]);o.length>0&&s.push(o.join(" "))}return s}function H(e,s){let t=_[s];if(!t&&s!=="python")return[];let n=[];if(t)for(let i of t){i.lastIndex=0;let o;for(;(o=i.exec(e))!==null;){let r=o[o.length-1];r&&n.push(r)}}return s==="python"&&n.push(...F(e)),n}export{q as evaluateCommand,z as evaluateCommandDenyOnly,I as evaluateFilePath,H as extractShellCommands,G as fileGlobToRegex,A as globToRegex,f as matchesAnyPattern,w as parseBashPattern,k as parseToolPattern,L as readBashPolicies,M as readToolDenyPatterns,b as splitChainedCommands};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import{createRequire as
|
|
1
|
+
import{createRequire as et}from"node:module";import{existsSync as st,unlinkSync as X,renameSync as nt}from"node:fs";import{tmpdir as rt}from"node:os";import{join as it}from"node:path";import{writeFileSync as A,readFileSync as M,unlinkSync as J}from"node:fs";import{tmpdir as Z}from"node:os";var l=class extends Error{pid;dbPath;constructor(t,e){super(`Another context-mode server is already running (PID: ${t}). Stop it before starting a new instance.`),this.name="DatabaseLockedError",this.pid=t,this.dbPath=e}};function tt(n){try{return process.kill(n,0),!0}catch{return!1}}function F(n){return`${n}.lock`}function P(n){let t=Z();return n===t||n.startsWith(t+"/")||n.startsWith(t+"\\")}function k(n){let{dbPath:t}=n;if(P(t))return{skipped:!0};let e=F(t),s=String(process.pid);try{return A(e,s,{flag:"wx"}),{skipped:!1}}catch(c){if(c?.code!=="EEXIST")throw c}let r;try{r=M(e,"utf-8").trim()}catch{try{return A(e,s,{flag:"wx"}),{skipped:!1}}catch{throw new l(0,t)}}let o=Number.parseInt(r,10);if(Number.isFinite(o)&&o>0&&tt(o))throw new l(o,t);A(e,s,{flag:"w"});let a;try{a=Number.parseInt(M(e,"utf-8").trim(),10)}catch{throw new l(0,t)}if(a!==process.pid)throw new l(a,t);return{skipped:!1}}function E(n){let{dbPath:t}=n;if(!P(t))try{J(F(t))}catch{}}var C=class{#t;constructor(t){this.#t=t}pragma(t){let s=this.#t.prepare(`PRAGMA ${t}`).all();if(!s||s.length===0)return;if(s.length>1)return s;let r=Object.values(s[0]);return r.length===1?r[0]:s[0]}exec(t){let e="",s=null;for(let o=0;o<t.length;o++){let a=t[o];if(s)e+=a,a===s&&(s=null);else if(a==="'"||a==='"')e+=a,s=a;else if(a===";"){let c=e.trim();c&&this.#t.prepare(c).run(),e=""}else e+=a}let r=e.trim();return r&&this.#t.prepare(r).run(),this}prepare(t){let e=this.#t.prepare(t);return{run:(...s)=>e.run(...s),get:(...s)=>{let r=e.get(...s);return r===null?void 0:r},all:(...s)=>e.all(...s),iterate:(...s)=>e.iterate(...s)}}transaction(t){return this.#t.transaction(t)}close(){this.#t.close()}},O=class{#t;constructor(t){this.#t=t}pragma(t){let s=this.#t.prepare(`PRAGMA ${t}`).all();if(!s||s.length===0)return;if(s.length>1)return s;let r=Object.values(s[0]);return r.length===1?r[0]:s[0]}exec(t){return this.#t.exec(t),this}prepare(t){let e=this.#t.prepare(t);return{run:(...s)=>e.run(...s),get:(...s)=>e.get(...s),all:(...s)=>e.all(...s),iterate:(...s)=>typeof e.iterate=="function"?e.iterate(...s):e.all(...s)[Symbol.iterator]()}}transaction(t){return(...e)=>{this.#t.exec("BEGIN");try{let s=t(...e);return this.#t.exec("COMMIT"),s}catch(s){throw this.#t.exec("ROLLBACK"),s}}}close(){this.#t.close()}},m=null;function ot(n){let t=null;try{return t=new n(":memory:"),t.exec("CREATE VIRTUAL TABLE __fts5_probe USING fts5(x)"),!0}catch{return!1}finally{try{t?.close()}catch{}}}function at(n,t){let e=t!==void 0?t:globalThis.Bun;if(typeof e<"u"&&e!==null)return!0;let s=n??process.versions,[r,o]=(s.node??"0.0.0").split("."),a=Number(r),c=Number(o);return!Number.isFinite(a)||!Number.isFinite(c)?!1:a>22||a===22&&c>=5}function ct(){if(!m){let n=et(import.meta.url);if(globalThis.Bun){let t=n(["bun","sqlite"].join(":")).Database;m=function(s,r){let o=new t(s,{readonly:r?.readonly,create:!0}),a=new C(o);return r?.timeout&&a.pragma(`busy_timeout = ${r.timeout}`),a}}else if(at()){let t=null;try{({DatabaseSync:t}=n(["node","sqlite"].join(":")))}catch{t=null}t&&ot(t)?m=function(s,r){let o=new t(s,{readOnly:r?.readonly??!1});return new O(o)}:m=n("better-sqlite3")}else m=n("better-sqlite3")}return m}function B(n){n.pragma("journal_mode = WAL"),n.pragma("synchronous = NORMAL");try{n.pragma("mmap_size = 268435456")}catch{}try{n.pragma("locking_mode = EXCLUSIVE")}catch{}}function j(n){if(!st(n))for(let t of["-wal","-shm"])try{X(n+t)}catch{}}function ut(n){for(let t of["","-wal","-shm"])try{X(n+t)}catch{}}function w(n){try{n.pragma("wal_checkpoint(TRUNCATE)")}catch{}try{n.close()}catch{}}function W(n="context-mode"){return it(rt(),`${n}-${process.pid}.db`)}function dt(n,t=[100,500,2e3]){let e;for(let s=0;s<=t.length;s++)try{return n()}catch(r){let o=r instanceof Error?r.message:String(r);if(!o.includes("SQLITE_BUSY")&&!o.includes("database is locked"))throw r;if(e=r instanceof Error?r:new Error(o),s<t.length){let a=t[s],c=Date.now();for(;Date.now()-c<a;);}}throw new Error(`SQLITE_BUSY: database is locked after ${t.length} retries. Original error: ${e?.message}`)}function lt(n){return n.includes("SQLITE_CORRUPT")||n.includes("SQLITE_NOTADB")||n.includes("database disk image is malformed")||n.includes("file is not a database")}function Et(n){let t=Date.now();for(let e of["","-wal","-shm"])try{nt(n+e,`${n}${e}.corrupt-${t}`)}catch{}}var _=Symbol.for("__context_mode_live_dbs_v2__"),D=(()=>{let n=globalThis;return n[_]||(n[_]=new Map,process.on("exit",()=>{for(let[t,e]of n[_])w(t),E({dbPath:e});n[_].clear()})),n[_]})(),f=class{#t;#e;constructor(t){let e=ct();this.#t=t,k({dbPath:t}),j(t);let s;try{s=new e(t,{timeout:3e4}),B(s)}catch(r){let o=r instanceof Error?r.message:String(r);if(lt(o)){Et(t),j(t);try{s=new e(t,{timeout:3e4}),B(s)}catch(a){throw E({dbPath:t}),new Error(`Failed to create fresh DB after renaming corrupt file: ${a instanceof Error?a.message:String(a)}`)}}else throw E({dbPath:t}),r}this.#e=s,D.set(this.#e,t),this.initSchema(),this.prepareStatements()}get db(){return this.#e}get dbPath(){return this.#t}close(){D.delete(this.#e),w(this.#e),E({dbPath:this.#t})}withRetry(t){return dt(t)}cleanup(){D.delete(this.#e),w(this.#e),ut(this.#t),E({dbPath:this.#t})}};import{createHash as y}from"node:crypto";import{execFileSync as mt}from"node:child_process";import{existsSync as S,realpathSync as pt,renameSync as I}from"node:fs";import{join as L}from"node:path";var p;function T(n){let t=n.replace(/\\/g,"/");return/^\/+$/.test(t)?"/":/^[A-Za-z]:\/+$/.test(t)?`${t.slice(0,2)}/`:t.replace(/\/+$/,"")}function H(n){let t=n;try{t=pt.native(n)}catch{}let e=T(t);return process.platform==="win32"||process.platform==="darwin"?e.toLowerCase():e}function q(n,t){return mt("git",["-C",n,...t],{encoding:"utf-8",timeout:2e3,stdio:["ignore","pipe","ignore"]}).trim()}function gt(n){let t=q(n,["rev-parse","--show-toplevel"]);return t.length>0?T(t):null}function _t(n){let t=q(n,["worktree","list","--porcelain"]).split(/\r?\n/).find(e=>e.startsWith("worktree "))?.replace("worktree ","")?.trim();return t?T(t):null}function yt(n=process.cwd()){let t=process.env.CONTEXT_MODE_SESSION_SUFFIX;if(p&&p.projectDir===n&&p.envSuffix===t)return p.suffix;let e="";if(t!==void 0)e=t?`__${t}`:"";else try{let s=gt(n),r=_t(n);if(s&&r){let o=H(s),a=H(r);o!==a&&(e=`__${y("sha256").update(o).digest("hex").slice(0,8)}`)}}catch{}return p={projectDir:n,envSuffix:t,suffix:e},e}function xt(){p=void 0}function V(n){return y("sha256").update(T(n)).digest("hex").slice(0,16)}function z(n){let t=T(n),e=process.platform==="darwin"||process.platform==="win32"?t.toLowerCase():t;return y("sha256").update(e).digest("hex").slice(0,16)}function Ut(n){let{projectDir:t,contentDir:e}=n,s=z(t),r=L(e,`${s}.db`);if(S(r))return r;let o=V(t);if(o===s)return r;let a=L(e,`${o}.db`);if(S(a))try{I(a,r);for(let c of["-wal","-shm"])try{I(a+c,r+c)}catch{}}catch{}return r}function Mt(n){return Tt({...n,ext:".db"})}function Tt(n){let{projectDir:t,sessionsDir:e,ext:s}=n,r=n.suffix??yt(t),o=z(t),a=L(e,`${o}${r}${s}`);if(S(a))return a;let c=V(t);if(c===o)return a;let d=L(e,`${c}${r}${s}`);if(S(d))try{I(d,a)}catch{}return a}var $=1e3,Y=5;function b(n){let t=Number(n);return!Number.isFinite(t)||t<=0?0:Math.floor(t)}var i={insertEvent:"insertEvent",getEvents:"getEvents",getEventsByType:"getEventsByType",getEventsByPriority:"getEventsByPriority",getEventsByTypeAndPriority:"getEventsByTypeAndPriority",getEventCount:"getEventCount",getLatestAttributedProject:"getLatestAttributedProject",checkDuplicate:"checkDuplicate",evictLowestPriority:"evictLowestPriority",updateMetaLastEvent:"updateMetaLastEvent",ensureSession:"ensureSession",getSessionStats:"getSessionStats",incrementCompactCount:"incrementCompactCount",upsertResume:"upsertResume",getResume:"getResume",markResumeConsumed:"markResumeConsumed",claimLatestUnconsumedResume:"claimLatestUnconsumedResume",deleteEvents:"deleteEvents",deleteMeta:"deleteMeta",deleteResume:"deleteResume",getOldSessions:"getOldSessions",searchEvents:"searchEvents",incrementToolCall:"incrementToolCall",getToolCallTotals:"getToolCallTotals",getToolCallByTool:"getToolCallByTool",getEventBytesSummary:"getEventBytesSummary"},G=class extends f{constructor(t){super(t?.dbPath??W("session"))}stmt(t){return this.stmts.get(t)}initSchema(){try{let e=this.db.pragma("table_xinfo(session_events)").find(s=>s.name==="data_hash");e&&e.hidden!==0&&this.db.exec("DROP TABLE session_events")}catch{}this.db.exec(`
|
|
2
2
|
CREATE TABLE IF NOT EXISTS session_events (
|
|
3
3
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
4
4
|
session_id TEXT NOT NULL,
|
|
@@ -116,4 +116,4 @@ import{createRequire as Y}from"node:module";import{existsSync as G,unlinkSync as
|
|
|
116
116
|
FROM tool_calls WHERE session_id = ?`),t(i.getToolCallByTool,`SELECT tool, calls, bytes_returned
|
|
117
117
|
FROM tool_calls WHERE session_id = ? ORDER BY calls DESC`),t(i.getEventBytesSummary,`SELECT COALESCE(SUM(bytes_avoided), 0) AS bytes_avoided,
|
|
118
118
|
COALESCE(SUM(bytes_returned), 0) AS bytes_returned
|
|
119
|
-
FROM session_events WHERE session_id = ?`)}insertEvent(t,e,s="PostToolUse",r,o){let a=
|
|
119
|
+
FROM session_events WHERE session_id = ?`)}insertEvent(t,e,s="PostToolUse",r,o){let a=y("sha256").update(e.data).digest("hex").slice(0,16).toUpperCase(),c=String(r?.projectDir??e.project_dir??"").trim(),d=String(r?.source??e.attribution_source??"unknown"),u=Number(r?.confidence??e.attribution_confidence??0),h=Number.isFinite(u)?Math.max(0,Math.min(1,u)):0,g=b(o?.bytesAvoided),R=b(o?.bytesReturned),v=this.db.transaction(()=>{if(this.stmt(i.checkDuplicate).get(t,Y,e.type,a))return;this.stmt(i.getEventCount).get(t).cnt>=$&&this.stmt(i.evictLowestPriority).run(t),this.stmt(i.insertEvent).run(t,e.type,e.category,e.priority,e.data,c,d,h,g,R,s,a),this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>v())}bulkInsertEvents(t,e,s="PostToolUse",r,o){if(!e||e.length===0)return;if(e.length===1){this.insertEvent(t,e[0],s,r?.[0],o?.[0]);return}let a=e.map((d,u)=>{let h=y("sha256").update(d.data).digest("hex").slice(0,16).toUpperCase(),g=r?.[u],R=String(g?.projectDir??d.project_dir??"").trim(),v=String(g?.source??d.attribution_source??"unknown"),N=Number(g?.confidence??d.attribution_confidence??0),x=Number.isFinite(N)?Math.max(0,Math.min(1,N)):0,U=o?.[u],K=b(U?.bytesAvoided),Q=b(U?.bytesReturned);return{event:d,dataHash:h,projectDir:R,attributionSource:v,attributionConfidence:x,bytesAvoided:K,bytesReturned:Q}}),c=this.db.transaction(()=>{let d=this.stmt(i.getEventCount).get(t).cnt;for(let u of a)this.stmt(i.checkDuplicate).get(t,Y,u.event.type,u.dataHash)||(d>=$?this.stmt(i.evictLowestPriority).run(t):d++,this.stmt(i.insertEvent).run(t,u.event.type,u.event.category,u.event.priority,u.event.data,u.projectDir,u.attributionSource,u.attributionConfidence,u.bytesAvoided,u.bytesReturned,s,u.dataHash));this.stmt(i.updateMetaLastEvent).run(t)});this.withRetry(()=>c())}getEvents(t,e){let s=e?.limit??1e3,r=e?.type,o=e?.minPriority;return r&&o!==void 0?this.stmt(i.getEventsByTypeAndPriority).all(t,r,o,s):r?this.stmt(i.getEventsByType).all(t,r,s):o!==void 0?this.stmt(i.getEventsByPriority).all(t,o,s):this.stmt(i.getEvents).all(t,s)}getEventCount(t){return this.stmt(i.getEventCount).get(t).cnt}getEventBytesSummary(t){let e=this.stmt(i.getEventBytesSummary).get(t);return{bytesAvoided:Number(e?.bytes_avoided??0),bytesReturned:Number(e?.bytes_returned??0)}}getLatestAttributedProjectDir(t){return this.stmt(i.getLatestAttributedProject).get(t)?.project_dir||null}searchEvents(t,e,s,r){try{let o=t.replace(/[%_]/g,c=>"\\"+c),a=r??null;return this.stmt(i.searchEvents).all(s,o,o,a,a,e)}catch{return[]}}ensureSession(t,e){this.stmt(i.ensureSession).run(t,e)}getSessionStats(t){return this.stmt(i.getSessionStats).get(t)??null}incrementCompactCount(t){this.stmt(i.incrementCompactCount).run(t)}upsertResume(t,e,s){this.stmt(i.upsertResume).run(t,e,s??0)}getResume(t){return this.stmt(i.getResume).get(t)??null}markResumeConsumed(t){this.stmt(i.markResumeConsumed).run(t)}claimLatestUnconsumedResume(t){let e=this.stmt(i.claimLatestUnconsumedResume).get(t);return e?{sessionId:e.session_id,snapshot:e.snapshot}:null}getLatestSessionId(){try{return this.db.prepare("SELECT session_id FROM session_meta ORDER BY started_at DESC LIMIT 1").get()?.session_id??null}catch{return null}}incrementToolCall(t,e,s=0){let r=Number.isFinite(s)&&s>0?Math.round(s):0;try{this.stmt(i.incrementToolCall).run(t,e,r)}catch{}}getToolCallStats(t){try{let e=this.stmt(i.getToolCallTotals).get(t),s=this.stmt(i.getToolCallByTool).all(t),r={};for(let o of s)r[o.tool]={calls:o.calls,bytesReturned:o.bytes_returned};return{totalCalls:e?.calls??0,totalBytesReturned:e?.bytes_returned??0,byTool:r}}catch{return{totalCalls:0,totalBytesReturned:0,byTool:{}}}}deleteSession(t){this.db.transaction(()=>{this.stmt(i.deleteEvents).run(t),this.stmt(i.deleteResume).run(t),this.stmt(i.deleteMeta).run(t)})()}cleanupOldSessions(t=7){let e=`-${t}`,s=this.stmt(i.getOldSessions).all(e);for(let{session_id:r}of s)this.deleteSession(r);return s.length}};export{G as SessionDB,xt as _resetWorktreeSuffixCacheForTests,yt as getWorktreeSuffix,z as hashProjectDirCanonical,V as hashProjectDirLegacy,T as normalizeWorktreePath,Ut as resolveContentStorePath,Mt as resolveSessionDbPath,Tt as resolveSessionPath};
|
package/hooks/sessionstart.mjs
CHANGED
|
@@ -53,6 +53,24 @@ await runHook(async () => {
|
|
|
53
53
|
|
|
54
54
|
let additionalContext = ROUTING_BLOCK;
|
|
55
55
|
|
|
56
|
+
// ─── #558: surface security init failure as agent-facing context ───
|
|
57
|
+
//
|
|
58
|
+
// Pre-558 the only signal of a fail-open security regression was a
|
|
59
|
+
// stderr WARNING line (suppressed/discarded by most adapters). The
|
|
60
|
+
// SessionStart additionalContext block is the in-band channel — the
|
|
61
|
+
// agent reads it, the user sees it. Idempotent by virtue of
|
|
62
|
+
// SessionStart's once-per-session lifecycle.
|
|
63
|
+
try {
|
|
64
|
+
const { initSecurity, isSecurityInitFailed, buildSecurityWarningContext } =
|
|
65
|
+
await import("./core/routing.mjs");
|
|
66
|
+
const { resolve: _resolve } = await import("node:path");
|
|
67
|
+
await initSecurity(_resolve(HOOK_DIR, "..", "build"));
|
|
68
|
+
if (isSecurityInitFailed()) {
|
|
69
|
+
const warning = buildSecurityWarningContext();
|
|
70
|
+
if (warning) additionalContext = warning + "\n\n" + additionalContext;
|
|
71
|
+
}
|
|
72
|
+
} catch { /* security probe is best-effort — never block session start */ }
|
|
73
|
+
|
|
56
74
|
try {
|
|
57
75
|
const raw = await readStdin();
|
|
58
76
|
const input = parseStdin(raw);
|
package/openclaw.plugin.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"name": "Context Mode",
|
|
4
4
|
"kind": "tool",
|
|
5
5
|
"description": "OpenClaw plugin that saves 98% of your context window. Sandboxed code execution in 11 languages, FTS5 knowledge base with BM25 ranking, and intent-driven search.",
|
|
6
|
-
"version": "1.0.
|
|
6
|
+
"version": "1.0.128",
|
|
7
7
|
"sandbox": {
|
|
8
8
|
"mode": "permissive",
|
|
9
9
|
"filesystem_access": "full",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-mode",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.128",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "MCP plugin that saves 98% of your context window. Works with Claude Code, Gemini CLI, VS Code Copilot, OpenCode, and Codex CLI. Sandboxed code execution, FTS5 knowledge base, and intent-driven search.",
|
|
6
6
|
"author": "Mert Koseoğlu",
|
|
@@ -85,9 +85,9 @@
|
|
|
85
85
|
],
|
|
86
86
|
"scripts": {
|
|
87
87
|
"build": "tsc && node -e \"if(process.platform!=='win32'){require('fs').chmodSync('build/cli.js',0o755)}\" && npm run bundle && npm run assert-bundle && npm run assert-asymmetric-drift",
|
|
88
|
-
"assert-bundle": "node scripts/assert-bundle.mjs server.bundle.mjs cli.bundle.mjs hooks/session-extract.bundle.mjs hooks/session-snapshot.bundle.mjs hooks/session-db.bundle.mjs",
|
|
88
|
+
"assert-bundle": "node scripts/assert-bundle.mjs server.bundle.mjs cli.bundle.mjs hooks/session-extract.bundle.mjs hooks/session-snapshot.bundle.mjs hooks/session-db.bundle.mjs hooks/security.bundle.mjs",
|
|
89
89
|
"assert-asymmetric-drift": "node scripts/assert-asymmetric-drift.mjs",
|
|
90
|
-
"bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify && esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=cli.bundle.mjs --external:better-sqlite3 --minify && esbuild src/session/extract.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-extract.bundle.mjs --minify && esbuild src/session/snapshot.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-snapshot.bundle.mjs --minify && esbuild src/session/db.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-db.bundle.mjs --external:better-sqlite3 --minify",
|
|
90
|
+
"bundle": "esbuild src/server.ts --bundle --platform=node --target=node18 --format=esm --outfile=server.bundle.mjs --external:better-sqlite3 --external:turndown --external:turndown-plugin-gfm --external:@mixmark-io/domino --minify && esbuild src/cli.ts --bundle --platform=node --target=node18 --format=esm --outfile=cli.bundle.mjs --external:better-sqlite3 --minify && esbuild src/session/extract.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-extract.bundle.mjs --minify && esbuild src/session/snapshot.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-snapshot.bundle.mjs --minify && esbuild src/session/db.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/session-db.bundle.mjs --external:better-sqlite3 --minify && esbuild src/security.ts --bundle --platform=node --target=node18 --format=esm --outfile=hooks/security.bundle.mjs --minify",
|
|
91
91
|
"version-sync": "node scripts/version-sync.mjs",
|
|
92
92
|
"version": "node scripts/version-sync.mjs && git add package.json .claude-plugin/plugin.json .claude-plugin/marketplace.json .cursor-plugin/plugin.json .codex-plugin/plugin.json .openclaw-plugin/openclaw.plugin.json .openclaw-plugin/package.json openclaw.plugin.json .pi/extensions/context-mode/package.json",
|
|
93
93
|
"prepublishOnly": "npm run build",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* developer ran during `pretest`.
|
|
30
30
|
*/
|
|
31
31
|
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
32
|
-
import { join, relative } from "node:path";
|
|
32
|
+
import { join, relative, sep } from "node:path";
|
|
33
33
|
|
|
34
34
|
/**
|
|
35
35
|
* Walk a directory recursively, returning a flat list of relative file
|
|
@@ -102,22 +102,15 @@ export function derivePluginManifest({ pkg, pluginRoot }) {
|
|
|
102
102
|
}
|
|
103
103
|
|
|
104
104
|
/**
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
105
|
+
* LEGACY_FALLBACK — the v1.0.126 hardcoded REQUIRED_RUNTIME_SIBLINGS,
|
|
106
|
+
* preserved verbatim. Forms the union seed for the algorithmic set so
|
|
107
|
+
* the post-558 contract is strictly additive over the pre-558 contract
|
|
108
|
+
* (no required sibling ever silently disappears).
|
|
109
109
|
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
* without cli.bundle.mjs `context-mode doctor` can't run.
|
|
113
|
-
* - hooks/{5 hook scripts}.mjs are spawned per Claude Code event.
|
|
114
|
-
* Missing any one produces a silent hook failure.
|
|
115
|
-
*
|
|
116
|
-
* Other files in package.json files[] (insight/, configs/, README, …)
|
|
117
|
-
* are not boot-critical, so missing them is a "warn"-class issue
|
|
118
|
-
* surfaced only via the doctor — never enough to fail-fast at boot.
|
|
110
|
+
* Also acts as a safety net when `package.json` is unreadable — the
|
|
111
|
+
* boot gate stays loud even if the publish manifest is corrupted.
|
|
119
112
|
*/
|
|
120
|
-
const
|
|
113
|
+
const LEGACY_FALLBACK = Object.freeze([
|
|
121
114
|
"server.bundle.mjs",
|
|
122
115
|
"cli.bundle.mjs",
|
|
123
116
|
join("hooks", "pretooluse.mjs"),
|
|
@@ -127,6 +120,94 @@ const REQUIRED_RUNTIME_SIBLINGS = Object.freeze([
|
|
|
127
120
|
join("hooks", "userpromptsubmit.mjs"),
|
|
128
121
|
]);
|
|
129
122
|
|
|
123
|
+
/**
|
|
124
|
+
* SOFT_FALLBACK_BUNDLES — bundles that already implement
|
|
125
|
+
* bundle-first / build-fallback resolution (via session-loaders.mjs or
|
|
126
|
+
* session-helpers.mjs). Their absence on a published install is
|
|
127
|
+
* gracefully recoverable, so they MUST NOT join the fail-fast boot
|
|
128
|
+
* gate — the gate would refuse to start a working install.
|
|
129
|
+
*
|
|
130
|
+
* The security bundle is intentionally NOT here: its absence creates a
|
|
131
|
+
* silent fail-OPEN regression (#558), so it IS boot-critical.
|
|
132
|
+
*/
|
|
133
|
+
const SOFT_FALLBACK_BUNDLES = new Set([
|
|
134
|
+
"hooks/session-extract.bundle.mjs",
|
|
135
|
+
"hooks/session-snapshot.bundle.mjs",
|
|
136
|
+
"hooks/session-db.bundle.mjs",
|
|
137
|
+
"hooks/session-attribution.bundle.mjs",
|
|
138
|
+
]);
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Algorithmically extract every esbuild output path from
|
|
142
|
+
* `package.json scripts.bundle`. The bundle script is the SINGLE
|
|
143
|
+
* SOURCE OF TRUTH for "what bundles this build produces" — parsing
|
|
144
|
+
* its `--outfile=…` arguments avoids the parallel-list trap that
|
|
145
|
+
* bit Algo-D4 v1.0.126 (the hardcoded REQUIRED list lagged the
|
|
146
|
+
* actual bundle output).
|
|
147
|
+
*
|
|
148
|
+
* Returns POSIX-style relative paths (forward slashes) for stable
|
|
149
|
+
* comparison with SOFT_FALLBACK_BUNDLES. Caller normalizes to
|
|
150
|
+
* `path.join` shape before pluginRoot-relative resolution.
|
|
151
|
+
*/
|
|
152
|
+
function extractBundleOutfiles(pkg) {
|
|
153
|
+
const script = pkg?.scripts?.bundle;
|
|
154
|
+
if (typeof script !== "string") return [];
|
|
155
|
+
const out = new Set();
|
|
156
|
+
// Match every `--outfile=<path>` token (path is whitespace-delimited
|
|
157
|
+
// because the script chains commands with `&&`).
|
|
158
|
+
const re = /--outfile=(\S+)/g;
|
|
159
|
+
let m;
|
|
160
|
+
while ((m = re.exec(script)) !== null) {
|
|
161
|
+
out.add(m[1]);
|
|
162
|
+
}
|
|
163
|
+
return [...out];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Algorithmic — derive the boot-critical sibling set as the union of:
|
|
168
|
+
* 1. LEGACY_FALLBACK (the v1.0.126 contract, preserved verbatim).
|
|
169
|
+
* 2. Every esbuild output path from `package.json scripts.bundle`
|
|
170
|
+
* that is NOT in SOFT_FALLBACK_BUNDLES.
|
|
171
|
+
*
|
|
172
|
+
* Why algorithmic instead of hardcoded:
|
|
173
|
+
*
|
|
174
|
+
* v1.0.126 shipped Algo-D4 with a hardcoded REQUIRED_RUNTIME_SIBLINGS
|
|
175
|
+
* array that omitted `hooks/security.bundle.mjs` (the bundle didn't
|
|
176
|
+
* ship until v1.0.127). The hardcoded list would need manual
|
|
177
|
+
* extension every time a runtime bundle is added — the same trap
|
|
178
|
+
* would re-bite the next bundle. Deriving from `scripts.bundle`
|
|
179
|
+
* closes the trap: any new bundle output is auto-gated unless it
|
|
180
|
+
* joins the soft-fallback whitelist (which is itself an explicit
|
|
181
|
+
* architectural decision, not a maintenance burden). (#558)
|
|
182
|
+
*
|
|
183
|
+
* Returns OS-native-separator relative paths (suitable for
|
|
184
|
+
* `path.join(pluginRoot, …)`).
|
|
185
|
+
*
|
|
186
|
+
* If `package.json` is unreadable, returns LEGACY_FALLBACK as a
|
|
187
|
+
* safety net so the boot gate never goes silent due to a parse
|
|
188
|
+
* error in the publish manifest.
|
|
189
|
+
*/
|
|
190
|
+
export function getRequiredRuntimeSiblings(pluginRoot) {
|
|
191
|
+
let pkg;
|
|
192
|
+
try {
|
|
193
|
+
pkg = JSON.parse(readFileSync(join(pluginRoot, "package.json"), "utf-8"));
|
|
194
|
+
} catch {
|
|
195
|
+
return [...LEGACY_FALLBACK];
|
|
196
|
+
}
|
|
197
|
+
const required = new Set(LEGACY_FALLBACK);
|
|
198
|
+
for (const outfile of extractBundleOutfiles(pkg)) {
|
|
199
|
+
// Normalize to POSIX for soft-fallback membership check —
|
|
200
|
+
// scripts.bundle is hand-authored with forward slashes already,
|
|
201
|
+
// but be defensive in case a Windows-authored package.json ever
|
|
202
|
+
// reaches us.
|
|
203
|
+
const posix = outfile.split(sep).join("/");
|
|
204
|
+
if (SOFT_FALLBACK_BUNDLES.has(posix)) continue;
|
|
205
|
+
// Convert back to OS-native sep for downstream filesystem ops.
|
|
206
|
+
required.add(posix.split("/").join(sep));
|
|
207
|
+
}
|
|
208
|
+
return [...required];
|
|
209
|
+
}
|
|
210
|
+
|
|
130
211
|
/**
|
|
131
212
|
* Verify boot-critical siblings exist at pluginRoot.
|
|
132
213
|
*
|
|
@@ -134,15 +215,14 @@ const REQUIRED_RUNTIME_SIBLINGS = Object.freeze([
|
|
|
134
215
|
* stderr. The caller (start.mjs at boot, src/cli.ts at doctor) decides
|
|
135
216
|
* the failure surface (fail-fast exit 2 vs. doctor diagnostic).
|
|
136
217
|
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* check fails — that's the "drift between contract and tarball" trap.
|
|
218
|
+
* Required-set is computed by `getRequiredRuntimeSiblings()` —
|
|
219
|
+
* algorithmically derived from `package.json files[]` filtered to the
|
|
220
|
+
* RUNTIME_CRITICAL_PATTERN. Drift between publish manifest and runtime
|
|
221
|
+
* contract becomes architecturally impossible (#558).
|
|
142
222
|
*/
|
|
143
223
|
export function assertPluginCacheIntegrity({ pluginRoot }) {
|
|
144
224
|
const missing = [];
|
|
145
|
-
for (const rel of
|
|
225
|
+
for (const rel of getRequiredRuntimeSiblings(pluginRoot)) {
|
|
146
226
|
const abs = join(pluginRoot, rel);
|
|
147
227
|
if (!existsSync(abs)) missing.push(abs);
|
|
148
228
|
}
|