zelari-code 2.15.0 → 2.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/dist/cli/companion/cors.js +52 -0
  2. package/dist/cli/companion/cors.js.map +1 -0
  3. package/dist/cli/companion/runManager.js +233 -1
  4. package/dist/cli/companion/runManager.js.map +1 -1
  5. package/dist/cli/companion/serve.js +21 -4
  6. package/dist/cli/companion/serve.js.map +1 -1
  7. package/dist/cli/extensions/extensionToolWiring.js +57 -0
  8. package/dist/cli/extensions/extensionToolWiring.js.map +1 -0
  9. package/dist/cli/extensions/loader.js +213 -0
  10. package/dist/cli/extensions/loader.js.map +1 -0
  11. package/dist/cli/extensions/sandboxedFs.js +69 -0
  12. package/dist/cli/extensions/sandboxedFs.js.map +1 -0
  13. package/dist/cli/headless/runOneTurn.js +783 -0
  14. package/dist/cli/headless/runOneTurn.js.map +1 -0
  15. package/dist/cli/kraken/completionGate.js +22 -5
  16. package/dist/cli/kraken/completionGate.js.map +1 -1
  17. package/dist/cli/lsp/manager.js +16 -0
  18. package/dist/cli/lsp/manager.js.map +1 -1
  19. package/dist/cli/main.bundled.js +17714 -15752
  20. package/dist/cli/main.bundled.js.map +4 -4
  21. package/dist/cli/main.js +20 -0
  22. package/dist/cli/main.js.map +1 -1
  23. package/dist/cli/runHeadless.js +5 -695
  24. package/dist/cli/runHeadless.js.map +1 -1
  25. package/dist/cli/safety/jails/darwin.js +87 -0
  26. package/dist/cli/safety/jails/darwin.js.map +1 -0
  27. package/dist/cli/safety/jails/linux.js +80 -0
  28. package/dist/cli/safety/jails/linux.js.map +1 -0
  29. package/dist/cli/safety/jails/win32.js +19 -0
  30. package/dist/cli/safety/jails/win32.js.map +1 -0
  31. package/dist/cli/safety/lifecycleHooks.js +33 -3
  32. package/dist/cli/safety/lifecycleHooks.js.map +1 -1
  33. package/dist/cli/safety/osJail.js +289 -0
  34. package/dist/cli/safety/osJail.js.map +1 -0
  35. package/dist/cli/safety/policyLoadMode.js.map +1 -1
  36. package/dist/cli/serve/harnessClient.js +150 -0
  37. package/dist/cli/serve/harnessClient.js.map +1 -0
  38. package/dist/cli/serve/harnessServer.js +333 -0
  39. package/dist/cli/serve/harnessServer.js.map +1 -0
  40. package/dist/cli/serve/sessionControl.js +67 -0
  41. package/dist/cli/serve/sessionControl.js.map +1 -0
  42. package/dist/cli/toolRegistry.js +338 -33
  43. package/dist/cli/toolRegistry.js.map +1 -1
  44. package/dist/cli/tools/execProcess.js +36 -15
  45. package/dist/cli/tools/execProcess.js.map +1 -1
  46. package/package.json +2 -2
@@ -0,0 +1,213 @@
1
+ /**
2
+ * extensions/loader.ts — disk loading for the ExtensionAPI seam (t30).
3
+ *
4
+ * Trust model (mirrors safety/lifecycleHooks.ts):
5
+ * 1. `~/.zelari-code/extensions/*` — user-global, ALWAYS active
6
+ * 2. `<project>/.zelari/extensions/*` — active ONLY when the folder is
7
+ * trusted (folderTrust.ts). Like project MCP + project hooks, trust
8
+ * gates project self-execution; it NEVER touches policy.json.
9
+ *
10
+ * Code arrives from DISK ONLY — `import()` of real files, never strings
11
+ * from the model/prompt (ADR-0022: seam, not a plugin framework).
12
+ *
13
+ * Optional `extensions.lock` (same dir, JSON): maps FILE name → sha256 of
14
+ * the file bytes. The hash check runs BEFORE the dynamic import, so an
15
+ * unverified module is never evaluated. If a lock exists and any candidate
16
+ * file is missing from it or hashes differently:
17
+ * - strict surface (activePolicyLoadMode strict — headless/mission/CI):
18
+ * the WHOLE load FAILS with a typed ExtensionLockError (no silent skip,
19
+ * no partial load); the host decides how loudly to die.
20
+ * - permissive (TUI): warn + skip the offending file, load the rest.
21
+ *
22
+ * @since 2.22.0 (t30)
23
+ */
24
+ import { homedir } from 'node:os';
25
+ import { join } from 'node:path';
26
+ import { readdirSync, readFileSync } from 'node:fs';
27
+ import { createHash } from 'node:crypto';
28
+ import { pathToFileURL } from 'node:url';
29
+ import { ExtensionRegistry, } from '@zelari/core/harness';
30
+ import { activePolicyLoadMode } from '../safety/policyLoadMode.js';
31
+ import { isFolderTrusted } from '../safety/folderTrust.js';
32
+ import { bindSandboxedFs } from './sandboxedFs.js';
33
+ /** `~/.zelari-code/extensions` — user-global extensions (always active). */
34
+ export function globalExtensionsDir() {
35
+ return join(homedir(), '.zelari-code', 'extensions');
36
+ }
37
+ /** `<project>/.zelari/extensions` — project extensions (trusted only). */
38
+ export function projectExtensionsDir(projectRoot) {
39
+ return join(projectRoot, '.zelari', 'extensions');
40
+ }
41
+ /** Lockfile name, co-located with the extension files. */
42
+ export const EXTENSIONS_LOCK_FILE = 'extensions.lock';
43
+ const EXTENSION_FILE_EXTS = new Set(['.js', '.mjs', '.cjs']);
44
+ /**
45
+ * Typed strict-mode failure (same spirit as PolicyLoadError): the caller
46
+ * gets a machine-readable reason list, one entry per offending file.
47
+ */
48
+ export class ExtensionLockError extends Error {
49
+ mismatches;
50
+ constructor(message, mismatches) {
51
+ super(message);
52
+ this.mismatches = mismatches;
53
+ this.name = 'ExtensionLockError';
54
+ }
55
+ }
56
+ function sha256File(file) {
57
+ return createHash('sha256').update(readFileSync(file)).digest('hex');
58
+ }
59
+ function candidateFiles(dir) {
60
+ let names;
61
+ try {
62
+ names = readdirSync(dir);
63
+ }
64
+ catch {
65
+ return []; // dir missing → no extensions (same as hooks/MCP dirs)
66
+ }
67
+ return names
68
+ .filter((n) => EXTENSION_FILE_EXTS.has(n.slice(n.lastIndexOf('.')).toLowerCase()))
69
+ .sort()
70
+ .map((n) => join(dir, n));
71
+ }
72
+ /** Extract + validate the ZelariExtension export from a module namespace. */
73
+ function extractExtension(mod) {
74
+ const m = mod;
75
+ const candidate = (m && typeof m === 'object' && m !== null ? m['default'] : undefined) ??
76
+ (m && typeof m === 'object' ? m['extension'] : undefined) ??
77
+ (m && typeof m === 'object' ? m['zelariExtension'] : undefined);
78
+ if (!candidate || typeof candidate !== 'object')
79
+ return null;
80
+ const ext = candidate;
81
+ if (typeof ext['id'] !== 'string' || ext['id'].trim() === '')
82
+ return null;
83
+ if (typeof ext['register'] !== 'function')
84
+ return null;
85
+ return candidate;
86
+ }
87
+ /**
88
+ * Load extensions from explicit dirs (global first, project second).
89
+ * Never throws: strict lock violations come back as `{ ok: false, error }`,
90
+ * everything else is a loud skip recorded in `skipped`.
91
+ */
92
+ export async function loadExtensionsFromDirs(dirs, options = {}) {
93
+ const logger = options.logger ?? ((msg) => console.error(`[extensions] ${msg}`));
94
+ const mode = options.mode ?? activePolicyLoadMode();
95
+ const importModule = options.importModule ?? ((file) => import(pathToFileURL(file).href));
96
+ const skipped = [];
97
+ const runtime = {
98
+ registry: new ExtensionRegistry(),
99
+ loaded: [],
100
+ skipped,
101
+ };
102
+ const fsRoot = options.fsRoot ?? process.cwd();
103
+ let fs = null;
104
+ // Phase 1 — INTEGRITY: verify every candidate against its dir lock (if
105
+ // any) BEFORE importing anything. A strict mismatch aborts the whole load.
106
+ const pending = [];
107
+ for (const dir of dirs) {
108
+ const files = candidateFiles(dir.path);
109
+ if (files.length === 0)
110
+ continue;
111
+ let lock = null;
112
+ try {
113
+ lock = JSON.parse(readFileSync(join(dir.path, EXTENSIONS_LOCK_FILE), 'utf8'));
114
+ }
115
+ catch {
116
+ lock = null; // no/invalid lock file → no hash gate (documented optional)
117
+ if (dirHasLockFile(dir.path)) {
118
+ skipped.push(`${EXTENSIONS_LOCK_FILE} in ${dir.path}: unreadable — lock ignored`);
119
+ }
120
+ }
121
+ for (const file of files) {
122
+ if (!lock) {
123
+ pending.push({ file, scope: dir.scope });
124
+ continue;
125
+ }
126
+ const expected = lock[basenameOf(file)];
127
+ const actual = sha256File(file);
128
+ if (typeof expected !== 'string' || expected.toLowerCase() !== actual.toLowerCase()) {
129
+ const why = typeof expected !== 'string'
130
+ ? 'not listed in extensions.lock'
131
+ : 'sha256 mismatch vs extensions.lock';
132
+ if (mode === 'strict') {
133
+ // Collected across ALL dirs first? No — fail fast keeps the typed
134
+ // error focused; remaining files were never imported anyway.
135
+ return {
136
+ ok: false,
137
+ error: new ExtensionLockError(`extension "${basenameOf(file)}" in ${dir.path}: ${why}`, [`${file}: ${why}`]),
138
+ skipped,
139
+ };
140
+ }
141
+ logger(`[extensions] skipping ${file}: ${why} (permissive mode)`);
142
+ skipped.push(`${file}: ${why}`);
143
+ continue;
144
+ }
145
+ pending.push({ file, scope: dir.scope });
146
+ }
147
+ }
148
+ // Phase 2 — IMPORT + REGISTER. One bad module never takes down the batch.
149
+ for (const { file, scope } of pending) {
150
+ let ext = null;
151
+ try {
152
+ ext = extractExtension(await importModule(file));
153
+ }
154
+ catch (err) {
155
+ const msg = err instanceof Error ? err.message : String(err);
156
+ logger(`failed to import extension ${file}: ${msg}`);
157
+ skipped.push(`${file}: import failed (${msg})`);
158
+ continue;
159
+ }
160
+ if (!ext) {
161
+ logger(`skipping ${file}: no default/extension export shaped like ZelariExtension`);
162
+ skipped.push(`${file}: missing ZelariExtension export`);
163
+ continue;
164
+ }
165
+ if (!fs)
166
+ fs = bindSandboxedFs(fsRoot);
167
+ try {
168
+ await runtime.registry.registerExtension(ext, { fs });
169
+ runtime.loaded.push({ id: ext.id, file, scope });
170
+ }
171
+ catch (err) {
172
+ const msg = err instanceof Error ? err.message : String(err);
173
+ logger(`extension "${ext.id}" (${file}) failed to register: ${msg}`);
174
+ skipped.push(`${file}: registration failed (${msg})`);
175
+ runtime.registry.removeExtension(ext.id);
176
+ }
177
+ }
178
+ return { ok: true, runtime };
179
+ }
180
+ function basenameOf(file) {
181
+ const i = Math.max(file.lastIndexOf('/'), file.lastIndexOf('\\'));
182
+ return i === -1 ? file : file.slice(i + 1);
183
+ }
184
+ function dirHasLockFile(dir) {
185
+ try {
186
+ readFileSync(join(dir, EXTENSIONS_LOCK_FILE));
187
+ return true;
188
+ }
189
+ catch {
190
+ return false;
191
+ }
192
+ }
193
+ /**
194
+ * Standard dir layout for a workspace: global extensions ALWAYS, project
195
+ * extensions only when `projectRoot` is trusted. This is the ONLY place
196
+ * the trust decision is made for extensions — and it never affects
197
+ * policy.json.
198
+ */
199
+ export function extensionSourceDirs(projectRoot) {
200
+ const dirs = [{ path: globalExtensionsDir(), scope: 'global' }];
201
+ if (isFolderTrusted(projectRoot)) {
202
+ dirs.push({ path: projectExtensionsDir(projectRoot), scope: 'project' });
203
+ }
204
+ return dirs;
205
+ }
206
+ /** Convenience: load with the standard dir layout for a workspace. */
207
+ export function loadDefaultExtensionRuntime(projectRoot, options = {}) {
208
+ return loadExtensionsFromDirs(extensionSourceDirs(projectRoot), {
209
+ fsRoot: projectRoot,
210
+ ...options,
211
+ });
212
+ }
213
+ //# sourceMappingURL=loader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"loader.js","sourceRoot":"","sources":["../../../src/cli/extensions/loader.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AACjC,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACpD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AACzC,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EACL,iBAAiB,GAGlB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,oBAAoB,EAAE,MAAM,6BAA6B,CAAC;AACnE,OAAO,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAC3D,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAEnD,4EAA4E;AAC5E,MAAM,UAAU,mBAAmB;IACjC,OAAO,IAAI,CAAC,OAAO,EAAE,EAAE,cAAc,EAAE,YAAY,CAAC,CAAC;AACvD,CAAC;AAED,0EAA0E;AAC1E,MAAM,UAAU,oBAAoB,CAAC,WAAmB;IACtD,OAAO,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,YAAY,CAAC,CAAC;AACpD,CAAC;AAED,0DAA0D;AAC1D,MAAM,CAAC,MAAM,oBAAoB,GAAG,iBAAiB,CAAC;AAEtD,MAAM,mBAAmB,GAAG,IAAI,GAAG,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAE7D;;;GAGG;AACH,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAGhC;IAFX,YACE,OAAe,EACN,UAA6B;QAEtC,KAAK,CAAC,OAAO,CAAC,CAAC;QAFN,eAAU,GAAV,UAAU,CAAmB;QAGtC,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;IACnC,CAAC;CACF;AAmCD,SAAS,UAAU,CAAC,IAAY;IAC9B,OAAO,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;AACvE,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,KAAe,CAAC;IACpB,IAAI,CAAC;QACH,KAAK,GAAG,WAAW,CAAC,GAAG,CAAC,CAAC;IAC3B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,uDAAuD;IACpE,CAAC;IACD,OAAO,KAAK;SACT,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,mBAAmB,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC;SACjF,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC,CAAC;AAC9B,CAAC;AAED,6EAA6E;AAC7E,SAAS,gBAAgB,CAAC,GAAY;IACpC,MAAM,CAAC,GAAG,GAAiD,CAAC;IAC5D,MAAM,SAAS,GAAG,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACrF,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzD,CAAC,CAAC,IAAI,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,iBAAiB,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAClE,IAAI,CAAC,SAAS,IAAI,OAAO,SAAS,KAAK,QAAQ;QAAE,OAAO,IAAI,CAAC;IAC7D,MAAM,GAAG,GAAG,SAAoC,CAAC;IACjD,IAAI,OAAO,GAAG,CAAC,IAAI,CAAC,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,IAAI,CAAC;IAC1E,IAAI,OAAO,GAAG,CAAC,UAAU,CAAC,KAAK,UAAU;QAAE,OAAO,IAAI,CAAC;IACvD,OAAO,SAAuC,CAAC;AACjD,CAAC;AAED;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,sBAAsB,CAC1C,IAAmC,EACnC,UAAiC,EAAE;IAEnC,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,CAAC,CAAC,GAAW,EAAE,EAAE,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,GAAG,EAAE,CAAC,CAAC,CAAC;IACzF,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,IAAI,oBAAoB,EAAE,CAAC;IACpD,MAAM,YAAY,GAAG,OAAO,CAAC,YAAY,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,aAAa,CAAC,IAAI,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;IAC1F,MAAM,OAAO,GAAa,EAAE,CAAC;IAC7B,MAAM,OAAO,GAA2B;QACtC,QAAQ,EAAE,IAAI,iBAAiB,EAAE;QACjC,MAAM,EAAE,EAAE;QACV,OAAO;KACR,CAAC;IACF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAC/C,IAAI,EAAE,GAAuB,IAAI,CAAC;IAElC,uEAAuE;IACvE,2EAA2E;IAC3E,MAAM,OAAO,GAAyD,EAAE,CAAC;IACzE,KAAK,MAAM,GAAG,IAAI,IAAI,EAAE,CAAC;QACvB,MAAM,KAAK,GAAG,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACvC,IAAI,KAAK,CAAC,MAAM,KAAK,CAAC;YAAE,SAAS;QACjC,IAAI,IAAI,GAAmC,IAAI,CAAC;QAChD,IAAI,CAAC;YACH,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,YAAY,CAAC,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,oBAAoB,CAAC,EAAE,MAAM,CAAC,CAA4B,CAAC;QAC3G,CAAC;QAAC,MAAM,CAAC;YACP,IAAI,GAAG,IAAI,CAAC,CAAC,4DAA4D;YACzE,IAAI,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,EAAE,CAAC;gBAC7B,OAAO,CAAC,IAAI,CAAC,GAAG,oBAAoB,OAAO,GAAG,CAAC,IAAI,6BAA6B,CAAC,CAAC;YACpF,CAAC;QACH,CAAC;QACD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,IAAI,CAAC,IAAI,EAAE,CAAC;gBACV,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;gBACzC,SAAS;YACX,CAAC;YACD,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC;YACxC,MAAM,MAAM,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;YAChC,IAAI,OAAO,QAAQ,KAAK,QAAQ,IAAI,QAAQ,CAAC,WAAW,EAAE,KAAK,MAAM,CAAC,WAAW,EAAE,EAAE,CAAC;gBACpF,MAAM,GAAG,GACP,OAAO,QAAQ,KAAK,QAAQ;oBAC1B,CAAC,CAAC,+BAA+B;oBACjC,CAAC,CAAC,oCAAoC,CAAC;gBAC3C,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;oBACtB,kEAAkE;oBAClE,6DAA6D;oBAC7D,OAAO;wBACL,EAAE,EAAE,KAAK;wBACT,KAAK,EAAE,IAAI,kBAAkB,CAC3B,cAAc,UAAU,CAAC,IAAI,CAAC,QAAQ,GAAG,CAAC,IAAI,KAAK,GAAG,EAAE,EACxD,CAAC,GAAG,IAAI,KAAK,GAAG,EAAE,CAAC,CACpB;wBACD,OAAO;qBACR,CAAC;gBACJ,CAAC;gBACD,MAAM,CAAC,yBAAyB,IAAI,KAAK,GAAG,oBAAoB,CAAC,CAAC;gBAClE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,KAAK,GAAG,EAAE,CAAC,CAAC;gBAChC,SAAS;YACX,CAAC;YACD,OAAO,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IAED,0EAA0E;IAC1E,KAAK,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,OAAO,EAAE,CAAC;QACtC,IAAI,GAAG,GAA2B,IAAI,CAAC;QACvC,IAAI,CAAC;YACH,GAAG,GAAG,gBAAgB,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,MAAM,CAAC,8BAA8B,IAAI,KAAK,GAAG,EAAE,CAAC,CAAC;YACrD,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,oBAAoB,GAAG,GAAG,CAAC,CAAC;YAChD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,MAAM,CAAC,YAAY,IAAI,2DAA2D,CAAC,CAAC;YACpF,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,kCAAkC,CAAC,CAAC;YACxD,SAAS;QACX,CAAC;QACD,IAAI,CAAC,EAAE;YAAE,EAAE,GAAG,eAAe,CAAC,MAAM,CAAC,CAAC;QACtC,IAAI,CAAC;YACH,MAAM,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAC,GAAG,EAAE,EAAE,EAAE,EAAE,CAAC,CAAC;YACtD,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAC7D,MAAM,CAAC,cAAc,GAAG,CAAC,EAAE,MAAM,IAAI,yBAAyB,GAAG,EAAE,CAAC,CAAC;YACrE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,0BAA0B,GAAG,GAAG,CAAC,CAAC;YACtD,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC3C,CAAC;IACH,CAAC;IACD,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC;AAC/B,CAAC;AAED,SAAS,UAAU,CAAC,IAAY;IAC9B,MAAM,CAAC,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC,CAAC;IAClE,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC;AAC7C,CAAC;AAED,SAAS,cAAc,CAAC,GAAW;IACjC,IAAI,CAAC;QACH,YAAY,CAAC,IAAI,CAAC,GAAG,EAAE,oBAAoB,CAAC,CAAC,CAAC;QAC9C,OAAO,IAAI,CAAC;IACd,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,KAAK,CAAC;IACf,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,WAAmB;IACrD,MAAM,IAAI,GAAyB,CAAC,EAAE,IAAI,EAAE,mBAAmB,EAAE,EAAE,KAAK,EAAE,QAAQ,EAAE,CAAC,CAAC;IACtF,IAAI,eAAe,CAAC,WAAW,CAAC,EAAE,CAAC;QACjC,IAAI,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,oBAAoB,CAAC,WAAW,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE,CAAC,CAAC;IAC3E,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED,sEAAsE;AACtE,MAAM,UAAU,2BAA2B,CACzC,WAAmB,EACnB,UAAiC,EAAE;IAEnC,OAAO,sBAAsB,CAAC,mBAAmB,CAAC,WAAW,CAAC,EAAE;QAC9D,MAAM,EAAE,WAAW;QACnB,GAAG,OAAO;KACX,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * sandboxedFs.ts — CLI binding for the core `SandboxedFs` seam (t30).
3
+ *
4
+ * Core defines the extension-facing INTERFACE; this module binds it to the
5
+ * real, symlink-safe sandbox resolver (safety/sandboxPath.ts): every path
6
+ * an extension passes is resolved against the workspace root (lexical +
7
+ * realpath layers) and writes get a `verifyContainment` re-check as the
8
+ * last step before touching disk. Escapes surface as typed
9
+ * `{ ok: false, error: '[extension-fs] …' }` results — never a throw into
10
+ * extension code, and never a silent fallback.
11
+ *
12
+ * @since 2.22.0 (t30)
13
+ */
14
+ import { promises as fsp } from 'node:fs';
15
+ import path from 'node:path';
16
+ import { typedErr, typedOk } from '@zelari/core/harness/tools/toolTypes';
17
+ import { resolveSandboxedPath, verifyContainment } from '../safety/sandboxPath.js';
18
+ function errText(prefix, p, err) {
19
+ const msg = err instanceof Error ? err.message : String(err);
20
+ return `[extension-fs] ${prefix} "${p}": ${msg}`;
21
+ }
22
+ /**
23
+ * Bind the extension FS seam to `root` (normally the workspace root).
24
+ * Relative paths are joined to the root; absolute paths must already sit
25
+ * inside it — anything else is rejected by the shared sandbox resolver,
26
+ * so an extension has exactly the same containment guarantees as the
27
+ * built-in filesystem tools.
28
+ */
29
+ export function bindSandboxedFs(root) {
30
+ const resolvedRoot = path.resolve(root);
31
+ return {
32
+ root: resolvedRoot,
33
+ async readFile(relativePath) {
34
+ try {
35
+ const target = resolveSandboxedPath(relativePath, { root: resolvedRoot });
36
+ const data = await fsp.readFile(target, 'utf8');
37
+ return typedOk(data);
38
+ }
39
+ catch (err) {
40
+ return typedErr(errText('readFile denied', relativePath, err));
41
+ }
42
+ },
43
+ async writeFile(relativePath, data) {
44
+ try {
45
+ const target = resolveSandboxedPath(relativePath, { root: resolvedRoot });
46
+ // TOCTOU re-check 〔PW §5〕: BOTH layers on fresh syscalls as the LAST
47
+ // step before the mutation (same discipline as wrapWithSandbox t26).
48
+ verifyContainment(target, { root: resolvedRoot });
49
+ await fsp.mkdir(path.dirname(target), { recursive: true });
50
+ await fsp.writeFile(target, data, 'utf8');
51
+ return typedOk({ path: target });
52
+ }
53
+ catch (err) {
54
+ return typedErr(errText('writeFile denied', relativePath, err));
55
+ }
56
+ },
57
+ async listFiles(relativePath = '.') {
58
+ try {
59
+ const target = resolveSandboxedPath(relativePath, { root: resolvedRoot });
60
+ const entries = await fsp.readdir(target, { withFileTypes: true });
61
+ return typedOk(entries.map((e) => (e.isDirectory() ? `${e.name}/` : e.name)).sort());
62
+ }
63
+ catch (err) {
64
+ return typedErr(errText('listFiles denied', relativePath, err));
65
+ }
66
+ },
67
+ };
68
+ }
69
+ //# sourceMappingURL=sandboxedFs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sandboxedFs.js","sourceRoot":"","sources":["../../../src/cli/extensions/sandboxedFs.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,QAAQ,IAAI,GAAG,EAAE,MAAM,SAAS,CAAC;AAC1C,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,sCAAsC,CAAC;AACzE,OAAO,EAAE,oBAAoB,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAEnF,SAAS,OAAO,CAAC,MAAc,EAAE,CAAS,EAAE,GAAY;IACtD,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;IAC7D,OAAO,kBAAkB,MAAM,KAAK,CAAC,MAAM,GAAG,EAAE,CAAC;AACnD,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,IAAY;IAC1C,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;IACxC,OAAO;QACL,IAAI,EAAE,YAAY;QAElB,KAAK,CAAC,QAAQ,CAAC,YAAoB;YACjC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,oBAAoB,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC1E,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;gBAChD,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;YACvB,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,QAAQ,CAAC,OAAO,CAAC,iBAAiB,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC;YACjE,CAAC;QACH,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,YAAoB,EAAE,IAAY;YAChD,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,oBAAoB,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC1E,qEAAqE;gBACrE,qEAAqE;gBACrE,iBAAiB,CAAC,MAAM,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAClD,MAAM,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;gBAC3D,MAAM,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;gBAC1C,OAAO,OAAO,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACnC,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,QAAQ,CAAC,OAAO,CAAC,kBAAkB,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;QAED,KAAK,CAAC,SAAS,CAAC,YAAY,GAAG,GAAG;YAChC,IAAI,CAAC;gBACH,MAAM,MAAM,GAAG,oBAAoB,CAAC,YAAY,EAAE,EAAE,IAAI,EAAE,YAAY,EAAE,CAAC,CAAC;gBAC1E,MAAM,OAAO,GAAG,MAAM,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,aAAa,EAAE,IAAI,EAAE,CAAC,CAAC;gBACnE,OAAO,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;YACvF,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,OAAO,QAAQ,CAAC,OAAO,CAAC,kBAAkB,EAAE,YAAY,EAAE,GAAG,CAAC,CAAC,CAAC;YAClE,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}