@aikdna/kdna-studio-cli 0.10.1 → 0.11.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.
@@ -0,0 +1,561 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('node:crypto');
4
+ const fs = require('node:fs');
5
+ const os = require('node:os');
6
+ const path = require('node:path');
7
+ const { spawnSync } = require('node:child_process');
8
+
9
+ const MAX_AGENT_INPUT_BYTES = 2 * 1024 * 1024;
10
+ const MAX_AGENT_SCHEMA_BYTES = 512 * 1024;
11
+ const MAX_AGENT_OUTPUT_BYTES = 8 * 1024 * 1024;
12
+ const MAX_AUTH_BYTES = 2 * 1024 * 1024;
13
+ const DISABLED_CODEX_FEATURES = Object.freeze([
14
+ 'apps',
15
+ 'browser_use',
16
+ 'browser_use_external',
17
+ 'browser_use_full_cdp_access',
18
+ 'code_mode_host',
19
+ 'computer_use',
20
+ 'enable_fanout',
21
+ 'enable_mcp_apps',
22
+ 'hooks',
23
+ 'image_generation',
24
+ 'in_app_browser',
25
+ 'multi_agent',
26
+ 'multi_agent_v2',
27
+ 'plugins',
28
+ 'remote_plugin',
29
+ 'shell_snapshot',
30
+ 'shell_tool',
31
+ 'shell_zsh_fork',
32
+ 'standalone_web_search',
33
+ 'tool_suggest',
34
+ 'unified_exec',
35
+ 'unified_exec_zsh_fork',
36
+ 'workspace_dependencies',
37
+ ]);
38
+
39
+ class ApplicationHostError extends Error {
40
+ constructor(code, message) {
41
+ super(message);
42
+ this.code = code;
43
+ }
44
+ }
45
+
46
+ function sha256(value) {
47
+ return `sha256:${crypto
48
+ .createHash('sha256')
49
+ .update(value)
50
+ .digest('hex')}`;
51
+ }
52
+
53
+ function sandboxLiteral(value) {
54
+ return JSON.stringify(path.resolve(value));
55
+ }
56
+
57
+ function sandboxProfile({ nativeCommand, roleRoot, deniedPaths }) {
58
+ const temporaryRoot = fs.realpathSync(os.tmpdir());
59
+ const explicitDenies = [...new Set(
60
+ deniedPaths
61
+ .filter(Boolean)
62
+ .map((entry) => path.resolve(entry))
63
+ .filter((entry) => !entry.startsWith(`${roleRoot}${path.sep}`)),
64
+ )].map((entry) => (
65
+ `(deny file-read* (subpath ${sandboxLiteral(entry)}))`
66
+ ));
67
+ return [
68
+ '(version 1)',
69
+ '(deny default)',
70
+ // A model-generated subprocess cannot start. Only the already selected
71
+ // native Codex host executable may execute.
72
+ `(allow process-exec (literal ${sandboxLiteral(nativeCommand)}))`,
73
+ `(allow file-read* (literal ${sandboxLiteral(nativeCommand)}))`,
74
+ // The dynamic linker and system frameworks required to load the selected
75
+ // native executable.
76
+ '(allow file-read* (subpath "/usr/lib"))',
77
+ '(allow file-read* (subpath "/System/Library"))',
78
+ '(allow file-read* (subpath "/Library/Apple/System/Library"))',
79
+ '(allow file-read* (subpath "/opt/homebrew"))',
80
+ '(allow file-read-metadata)',
81
+ // The host resolves the filesystem root; only that root node is readable.
82
+ '(allow file-read-data (literal "/"))',
83
+ // Standard device and system configuration nodes the host may touch.
84
+ '(allow file-read* (subpath "/dev"))',
85
+ '(allow file-read* (subpath "/private/dev"))',
86
+ '(allow file-read* (subpath "/private/etc"))',
87
+ '(allow file-read* (subpath "/etc"))',
88
+ // The adapter may read and write only inside this role's disposable 0700
89
+ // root.
90
+ `(allow file-read* (subpath ${sandboxLiteral(roleRoot)}))`,
91
+ `(allow file-write* (subpath ${sandboxLiteral(roleRoot)}))`,
92
+ // The selected Host runtime writes transient session state under the
93
+ // system temporary directories; user data, project files, and credentials
94
+ // remain outside the write namespace.
95
+ '(allow file-write* (subpath "/private/var/folders"))',
96
+ '(allow file-write* (subpath "/private/tmp"))',
97
+ '(allow file-write* (subpath "/var"))',
98
+ // Process, system, network, IPC and logging primitives the isolated host
99
+ // needs to reach its declared service.
100
+ '(allow process-fork)',
101
+ '(allow sysctl-read)',
102
+ '(allow network-outbound)',
103
+ '(allow mach-lookup)',
104
+ '(allow ipc-posix-shm)',
105
+ '(allow ipc-posix-sem)',
106
+ '(allow signal)',
107
+ // User data, credentials, project files, and unrelated temporary runs are
108
+ // outside the role's readable namespace.
109
+ '(deny file-read* (subpath "/Users"))',
110
+ '(deny file-read* (subpath "/private/tmp"))',
111
+ `(deny file-read-data (require-all (subpath ${sandboxLiteral(temporaryRoot)}) (require-not (subpath ${sandboxLiteral(roleRoot)}))))`,
112
+ ...explicitDenies,
113
+ ].join('\n');
114
+ }
115
+
116
+ function makePrivateRoot(prefix) {
117
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
118
+ fs.chmodSync(root, 0o700);
119
+ return fs.realpathSync(root);
120
+ }
121
+
122
+ function writePrivateBytes(target, bytes) {
123
+ const descriptor = fs.openSync(
124
+ target,
125
+ fs.constants.O_CREAT |
126
+ fs.constants.O_EXCL |
127
+ fs.constants.O_WRONLY,
128
+ 0o600,
129
+ );
130
+ try {
131
+ fs.writeFileSync(descriptor, bytes);
132
+ } finally {
133
+ fs.closeSync(descriptor);
134
+ }
135
+ }
136
+
137
+ function writePrivateJson(target, value) {
138
+ const bytes = Buffer.from(JSON.stringify(value));
139
+ try {
140
+ writePrivateBytes(target, bytes);
141
+ } finally {
142
+ bytes.fill(0);
143
+ }
144
+ }
145
+
146
+ function createPrivateOutput(target) {
147
+ const descriptor = fs.openSync(
148
+ target,
149
+ fs.constants.O_CREAT |
150
+ fs.constants.O_EXCL |
151
+ fs.constants.O_WRONLY,
152
+ 0o600,
153
+ );
154
+ fs.closeSync(descriptor);
155
+ }
156
+
157
+ function readPrivateJson(target) {
158
+ const stat = fs.lstatSync(target);
159
+ if (!stat.isFile() || stat.isSymbolicLink()) {
160
+ throw new ApplicationHostError(
161
+ 'application_host_output_invalid',
162
+ 'The isolated Host did not produce a regular structured output file.',
163
+ );
164
+ }
165
+ if ((stat.mode & 0o077) !== 0 || stat.size > MAX_AGENT_OUTPUT_BYTES) {
166
+ throw new ApplicationHostError(
167
+ 'application_host_output_invalid',
168
+ 'The isolated Host output permissions or size are invalid.',
169
+ );
170
+ }
171
+ const bytes = fs.readFileSync(target);
172
+ try {
173
+ return JSON.parse(bytes.toString('utf8'));
174
+ } catch {
175
+ throw new ApplicationHostError(
176
+ 'application_host_output_invalid',
177
+ 'The isolated Host did not return the required structured JSON.',
178
+ );
179
+ } finally {
180
+ bytes.fill(0);
181
+ }
182
+ }
183
+
184
+ function executableFromPath(command, environmentPath = process.env.PATH) {
185
+ if (path.isAbsolute(command)) return path.resolve(command);
186
+ for (const directory of String(environmentPath || '').split(path.delimiter)) {
187
+ if (!directory) continue;
188
+ const candidate = path.join(directory, command);
189
+ try {
190
+ fs.accessSync(candidate, fs.constants.X_OK);
191
+ return fs.realpathSync(candidate);
192
+ } catch {
193
+ // Continue through PATH without invoking a shell.
194
+ }
195
+ }
196
+ throw new ApplicationHostError(
197
+ 'application_host_unavailable',
198
+ 'The Codex Host adapter is not installed.',
199
+ );
200
+ }
201
+
202
+ function nativeCodexExecutable(command) {
203
+ const executable = executableFromPath(command);
204
+ const stat = fs.lstatSync(executable);
205
+ if (!stat.isFile() || stat.isSymbolicLink()) {
206
+ throw new ApplicationHostError(
207
+ 'application_host_unavailable',
208
+ 'The Codex Host command is not a regular executable.',
209
+ );
210
+ }
211
+ const prefix = Buffer.alloc(4);
212
+ let descriptor;
213
+ try {
214
+ descriptor = fs.openSync(executable, 'r');
215
+ fs.readSync(descriptor, prefix, 0, prefix.length, 0);
216
+ } finally {
217
+ if (descriptor !== undefined) fs.closeSync(descriptor);
218
+ }
219
+ const isMachO = [
220
+ 'feedface',
221
+ 'cefaedfe',
222
+ 'feedfacf',
223
+ 'cffaedfe',
224
+ 'cafebabe',
225
+ 'bebafeca',
226
+ ].includes(prefix.toString('hex'));
227
+ prefix.fill(0);
228
+ if (isMachO) return executable;
229
+
230
+ // The official npm command is a small JS launcher. Resolve its packaged
231
+ // native binary once, then execute only that binary inside the sandbox.
232
+ const packageRoot = path.resolve(path.dirname(executable), '..');
233
+ const modulesRoot = path.join(packageRoot, 'node_modules', '@openai');
234
+ let packages;
235
+ try {
236
+ packages = fs.readdirSync(modulesRoot);
237
+ } catch {
238
+ throw new ApplicationHostError(
239
+ 'application_host_unavailable',
240
+ 'The installed Codex command does not expose a packaged native executable.',
241
+ );
242
+ }
243
+ const candidates = [];
244
+ for (const packageName of packages) {
245
+ if (!packageName.startsWith('codex-')) continue;
246
+ const vendor = path.join(modulesRoot, packageName, 'vendor');
247
+ let triples = [];
248
+ try {
249
+ triples = fs.readdirSync(vendor);
250
+ } catch {
251
+ continue;
252
+ }
253
+ for (const triple of triples) {
254
+ const candidate = path.join(vendor, triple, 'bin', 'codex');
255
+ try {
256
+ const candidateStat = fs.lstatSync(candidate);
257
+ fs.accessSync(candidate, fs.constants.X_OK);
258
+ if (candidateStat.isFile() && !candidateStat.isSymbolicLink()) {
259
+ candidates.push(fs.realpathSync(candidate));
260
+ }
261
+ } catch {
262
+ // Ignore packages for another platform.
263
+ }
264
+ }
265
+ }
266
+ if (candidates.length !== 1) {
267
+ throw new ApplicationHostError(
268
+ 'application_host_unavailable',
269
+ 'The installed Codex command does not resolve to one native executable for this Host.',
270
+ );
271
+ }
272
+ return candidates[0];
273
+ }
274
+
275
+ function authSourcePath(options) {
276
+ if (options.authPath) return path.resolve(options.authPath);
277
+ const codexHome = process.env.CODEX_HOME
278
+ ? path.resolve(process.env.CODEX_HOME)
279
+ : path.join(os.homedir(), '.codex');
280
+ return path.join(codexHome, 'auth.json');
281
+ }
282
+
283
+ function installEphemeralAuth(roleRoot, options) {
284
+ const source = authSourcePath(options);
285
+ let stat;
286
+ try {
287
+ stat = fs.lstatSync(source);
288
+ } catch {
289
+ throw new ApplicationHostError(
290
+ 'application_host_auth_unavailable',
291
+ 'The exact Codex Host has no installed authentication substrate.',
292
+ );
293
+ }
294
+ if (
295
+ !stat.isFile() ||
296
+ stat.isSymbolicLink() ||
297
+ stat.size <= 0 ||
298
+ stat.size > MAX_AUTH_BYTES
299
+ ) {
300
+ throw new ApplicationHostError(
301
+ 'application_host_auth_unavailable',
302
+ 'The installed Codex authentication substrate is not a bounded regular file.',
303
+ );
304
+ }
305
+ const codexHome = path.join(roleRoot, 'codex-home');
306
+ fs.mkdirSync(codexHome, { mode: 0o700 });
307
+ const bytes = fs.readFileSync(source);
308
+ try {
309
+ writePrivateBytes(path.join(codexHome, 'auth.json'), bytes);
310
+ } finally {
311
+ bytes.fill(0);
312
+ }
313
+ return codexHome;
314
+ }
315
+
316
+ function codexCoordinate(nativeCommand, run = spawnSync) {
317
+ const result = run(nativeCommand, ['--version'], {
318
+ encoding: null,
319
+ timeout: 10_000,
320
+ maxBuffer: 64 * 1024,
321
+ env: {
322
+ HOME: os.tmpdir(),
323
+ PATH: '/usr/bin:/bin',
324
+ NO_COLOR: '1',
325
+ },
326
+ });
327
+ const stdout = Buffer.isBuffer(result.stdout)
328
+ ? result.stdout
329
+ : Buffer.from(result.stdout || '');
330
+ const stderr = Buffer.isBuffer(result.stderr)
331
+ ? result.stderr
332
+ : Buffer.from(result.stderr || '');
333
+ try {
334
+ if (result.status !== 0) {
335
+ throw new ApplicationHostError(
336
+ 'application_host_unavailable',
337
+ 'The Codex Host adapter could not report its version.',
338
+ );
339
+ }
340
+ return {
341
+ adapter: 'kdna.studio.application-host/codex-exec',
342
+ host: stdout.toString('utf8').trim(),
343
+ isolation: 'macos-sandbox-tool-less-role-root',
344
+ };
345
+ } finally {
346
+ stdout.fill(0);
347
+ stderr.fill(0);
348
+ }
349
+ }
350
+
351
+ function createCodexApplicationHost(options = {}) {
352
+ if (process.platform !== 'darwin') {
353
+ throw new ApplicationHostError(
354
+ 'application_host_capability_isolation_unavailable',
355
+ 'This Codex application adapter currently requires the macOS capability sandbox.',
356
+ );
357
+ }
358
+ const command = options.command || 'codex';
359
+ const run = options.spawnSync || spawnSync;
360
+ const nativeCommand =
361
+ options.nativeCommand || nativeCodexExecutable(command);
362
+ const deniedPaths = Array.isArray(options.deniedPaths)
363
+ ? options.deniedPaths
364
+ : [];
365
+ const coordinate = codexCoordinate(nativeCommand, run);
366
+ const isolationPolicy = {
367
+ schema: 'kdna.studio.application-host-isolation/0.1.0',
368
+ tool_features_disabled: DISABLED_CODEX_FEATURES,
369
+ child_processes: 'deny',
370
+ role_filesystem: 'disposable-role-root-only',
371
+ inherited_environment: 'deny',
372
+ };
373
+ const runnerDigest = sha256(JSON.stringify({
374
+ coordinate,
375
+ isolation_policy_digest: sha256(JSON.stringify(isolationPolicy)),
376
+ }));
377
+
378
+ function runStructured({ role, prompt, schema }) {
379
+ const promptBytes = Buffer.from(prompt, 'utf8');
380
+ const schemaBytes = Buffer.from(JSON.stringify(schema), 'utf8');
381
+ if (
382
+ promptBytes.length > MAX_AGENT_INPUT_BYTES ||
383
+ schemaBytes.length > MAX_AGENT_SCHEMA_BYTES
384
+ ) {
385
+ promptBytes.fill(0);
386
+ schemaBytes.fill(0);
387
+ throw new ApplicationHostError(
388
+ 'application_host_input_too_large',
389
+ 'The exact Runtime Capsule and task envelope exceed this Host adapter input budget; no input was truncated.',
390
+ );
391
+ }
392
+ const root = makePrivateRoot(`kdna-${role}-`);
393
+ const schemaPath = path.join(root, 'output.schema.json');
394
+ const outputPath = path.join(root, 'output.json');
395
+ let result = null;
396
+ try {
397
+ const roleHome = path.join(root, 'home');
398
+ const roleTmp = path.join(root, 'tmp');
399
+ fs.mkdirSync(roleHome, { mode: 0o700 });
400
+ fs.mkdirSync(roleTmp, { mode: 0o700 });
401
+ const codexHome = installEphemeralAuth(root, options);
402
+ writePrivateBytes(schemaPath, schemaBytes);
403
+ createPrivateOutput(outputPath);
404
+ const profile = sandboxProfile({
405
+ nativeCommand,
406
+ roleRoot: root,
407
+ deniedPaths: [
408
+ ...deniedPaths,
409
+ options.workspacePath,
410
+ options.projectPath,
411
+ ],
412
+ });
413
+ const args = [
414
+ 'exec',
415
+ '--ephemeral',
416
+ '--ignore-user-config',
417
+ '--ignore-rules',
418
+ '--skip-git-repo-check',
419
+ '--sandbox',
420
+ 'read-only',
421
+ '--cd',
422
+ root,
423
+ '--output-schema',
424
+ schemaPath,
425
+ '--output-last-message',
426
+ outputPath,
427
+ '--color',
428
+ 'never',
429
+ ...DISABLED_CODEX_FEATURES.flatMap(
430
+ (feature) => ['--disable', feature],
431
+ ),
432
+ '-',
433
+ ];
434
+ result = run(
435
+ '/usr/bin/sandbox-exec',
436
+ ['-p', profile, nativeCommand, ...args],
437
+ {
438
+ cwd: root,
439
+ input: promptBytes,
440
+ encoding: null,
441
+ timeout: options.timeoutMs || 300_000,
442
+ maxBuffer: MAX_AGENT_OUTPUT_BYTES,
443
+ env: {
444
+ HOME: roleHome,
445
+ CODEX_HOME: codexHome,
446
+ TMPDIR: roleTmp,
447
+ PATH: '/usr/bin:/bin',
448
+ LANG: 'C.UTF-8',
449
+ LC_ALL: 'C.UTF-8',
450
+ NO_COLOR: '1',
451
+ },
452
+ },
453
+ );
454
+ if (
455
+ result.status !== 0 ||
456
+ result.signal ||
457
+ !fs.existsSync(outputPath)
458
+ ) {
459
+ if (typeof options.onDiagnostic === 'function') {
460
+ const stderrText = Buffer.isBuffer(result.stderr)
461
+ ? result.stderr.toString('utf8')
462
+ : '';
463
+ options.onDiagnostic({
464
+ phase: role,
465
+ status: result.status,
466
+ signal: result.signal || null,
467
+ failure_class: /operation not permitted|permission denied|deny\(/i.test(
468
+ stderrText,
469
+ )
470
+ ? 'capability-sandbox'
471
+ : (
472
+ /output schema|schema/i.test(stderrText)
473
+ ? 'structured-output'
474
+ : (
475
+ /auth|login|credential/i.test(stderrText)
476
+ ? 'host-authentication'
477
+ : (
478
+ /config|feature|unknown field/i.test(stderrText)
479
+ ? 'host-configuration'
480
+ : (
481
+ /network|connect|tls|http/i.test(stderrText)
482
+ ? 'host-network'
483
+ : 'host-execution'
484
+ )
485
+ )
486
+ )
487
+ ),
488
+ stdout_digest: Buffer.isBuffer(result.stdout)
489
+ ? sha256(result.stdout)
490
+ : null,
491
+ stderr_digest: Buffer.isBuffer(result.stderr)
492
+ ? sha256(result.stderr)
493
+ : null,
494
+ });
495
+ }
496
+ throw new ApplicationHostError(
497
+ 'application_host_execution_failed',
498
+ `The isolated ${role} Host execution did not complete.`,
499
+ );
500
+ }
501
+ const output = readPrivateJson(outputPath);
502
+ const outputBytes = Buffer.from(JSON.stringify(output));
503
+ try {
504
+ return {
505
+ output,
506
+ output_digest: sha256(outputBytes),
507
+ run_digest: sha256(Buffer.concat([
508
+ Buffer.from(`${role}\0`),
509
+ promptBytes,
510
+ Buffer.from('\0'),
511
+ outputBytes,
512
+ ])),
513
+ runner_digest: runnerDigest,
514
+ coordinate,
515
+ isolation_policy_digest:
516
+ sha256(JSON.stringify(isolationPolicy)),
517
+ };
518
+ } finally {
519
+ outputBytes.fill(0);
520
+ }
521
+ } finally {
522
+ promptBytes.fill(0);
523
+ schemaBytes.fill(0);
524
+ if (Buffer.isBuffer(result?.stdout)) result.stdout.fill(0);
525
+ if (Buffer.isBuffer(result?.stderr)) result.stderr.fill(0);
526
+ fs.rmSync(root, { recursive: true, force: true });
527
+ }
528
+ }
529
+
530
+ return Object.freeze({
531
+ coordinate,
532
+ runner_digest: runnerDigest,
533
+ isolation_policy_digest: sha256(JSON.stringify(isolationPolicy)),
534
+ generateOracle(input) {
535
+ return runStructured({
536
+ role: 'application-oracle',
537
+ prompt: input.prompt,
538
+ schema: input.schema,
539
+ });
540
+ },
541
+ runConsumer(input) {
542
+ return runStructured({
543
+ role: 'application-consumer',
544
+ prompt: input.prompt,
545
+ schema: input.schema,
546
+ });
547
+ },
548
+ runEvaluator(input) {
549
+ return runStructured({
550
+ role: 'application-evaluator',
551
+ prompt: input.prompt,
552
+ schema: input.schema,
553
+ });
554
+ },
555
+ });
556
+ }
557
+
558
+ module.exports = {
559
+ ApplicationHostError,
560
+ createCodexApplicationHost,
561
+ };