@commonlyai/cli 0.1.166 → 0.1.167

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@commonlyai/cli",
3
- "version": "0.1.166",
3
+ "version": "0.1.167",
4
4
  "license": "Apache-2.0",
5
5
  "description": "The Commonly CLI — connect agents, manage pods, iterate fast",
6
6
  "type": "module",
@@ -1,3 +1,6 @@
1
+ import { createOwnKeyResolver, createMachineReportClient } from '../lib/own-key-launch.js';
2
+ import { WALKED_HARNESSES } from '../lib/runtime-inventory.js';
3
+ import { createRuntimeReporter } from '../lib/runtime-observation.js';
1
4
  /**
2
5
  * commonly agent <subcommand>
3
6
  *
@@ -1262,8 +1265,12 @@ export const performRun = ({
1262
1265
  sleepImpl = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
1263
1266
  // The agent to @mention when the model provider refuses a request's content
1264
1267
  // (lib/content-refusal.js). Null: the refusal notice names no one.
1268
+ reportRuntime = false,
1269
+ resolveOwnProviderKey = null,
1270
+ runtimeReportClient = null,
1265
1271
  refusalHandoff = null,
1266
1272
  }) => {
1273
+ reportRuntime = reportRuntime && WALKED_HARNESSES.includes(adapter?.name);
1267
1274
  if (environment?.provider !== undefined && !adapter?.supportsProvider) {
1268
1275
  throw new Error('environment.provider requires a provider-capable adapter');
1269
1276
  }
@@ -1271,6 +1278,11 @@ export const performRun = ({
1271
1278
  const effectiveTurnTimeoutMs = Number.isFinite(requestedTurnTimeout) && requestedTurnTimeout > 0
1272
1279
  ? Math.min(requestedTurnTimeout, 2_147_483_647) : 15 * 60 * 1000;
1273
1280
  const client = createClient({ instance: instanceUrl, token });
1281
+ const runtimeReporter = createRuntimeReporter({
1282
+ client: runtimeReportClient || { post: async () => {} },
1283
+ harness: adapter.name, detect: () => adapter.detect(),
1284
+ });
1285
+ const runtimeConnected = reportRuntime ? runtimeReporter.connect() : Promise.resolve();
1274
1286
  const missingModelNotices = new Set();
1275
1287
  const handoffAgent = sanitizeHandoffAgent(refusalHandoff);
1276
1288
  if (refusalHandoff && !handoffAgent) {
@@ -1321,12 +1333,18 @@ export const performRun = ({
1321
1333
  // the adapter spawns with, and the stored record keeps its broker, so the
1322
1334
  // daemon stays the layer that judges a host it knows. Applied once for the
1323
1335
  // run rather than per turn: `agent run` reads its record once at boot.
1324
- const seatEnvironment = withholdGrantBroker(environment, adapter?.name, {
1336
+ const requestedSeatEnvironment = withholdGrantBroker(environment, adapter?.name, {
1325
1337
  instanceUrl,
1326
1338
  onRefuse: (refusal, names) => log(
1327
1339
  `${refusal.code} (${refusal.reason}) — withholding ${names.join(', ')}: ${refusal.detail}`,
1328
1340
  ),
1329
1341
  });
1342
+ // Without native effort inventory a machine runner cannot promise that a
1343
+ // stored effort is supported. Preserve the record and use harness default.
1344
+ // Standalone CLI behavior remains under the operator's control.
1345
+ const runtimeInventoryPromise = reportRuntime ? Promise.resolve(adapter.inventory?.()).then((value) => value || { models: null, efforts: [] })
1346
+ .catch(() => ({ models: null, efforts: [] })) : Promise.resolve(null);
1347
+ let seatEnvironment = requestedSeatEnvironment;
1330
1348
  // OpenCode retains tool-call IDs in its resumed history. A Gemini proxy
1331
1349
  // marks those IDs with signature references, so changing providers must
1332
1350
  // start a fresh session before another upstream can receive them.
@@ -1384,6 +1402,10 @@ export const performRun = ({
1384
1402
  const batchLimit = Math.min(50, Math.max(1, Number(inboxBatchLimit) || 10));
1385
1403
 
1386
1404
  const processEvent = async (event) => {
1405
+ const inventory = await runtimeInventoryPromise;
1406
+ if (reportRuntime && requestedSeatEnvironment && !inventory?.efforts.includes(requestedSeatEnvironment.effort)) {
1407
+ seatEnvironment = { ...requestedSeatEnvironment, effort: undefined };
1408
+ }
1387
1409
  const eventPodId = event.podId || podId;
1388
1410
  const prompt = extractPrompt(event);
1389
1411
  if (!prompt || !eventPodId) {
@@ -1785,6 +1807,7 @@ export const performRun = ({
1785
1807
  ? { mounted: serverSkills.mounted, conflicted: serverSkills.conflicted, ...(skillReadUnavailable ? { status: 'unavailable' } : {}) }
1786
1808
  : null;
1787
1809
  let spawnCompleted = false;
1810
+ let observedSpawn = false;
1788
1811
  let commitAttribution = null;
1789
1812
  try {
1790
1813
  // The local CLI is a wrapper for a Commonly seat, so its Git commits
@@ -1802,7 +1825,24 @@ export const performRun = ({
1802
1825
  effort: seatEnvironment?.effort,
1803
1826
  });
1804
1827
  }
1828
+ await runtimeConnected;
1829
+ let providerKey;
1830
+ if (reportRuntime && resolveOwnProviderKey) {
1831
+ try {
1832
+ providerKey = await resolveOwnProviderKey({ opened, harness: adapter.name,
1833
+ provider: seatEnvironment?.provider });
1834
+ } catch (error) {
1835
+ if (!error.ownKeySpawnRefused) throw error;
1836
+ return { outcome: 'no_action', refused: { reason: 'delivery-refused' },
1837
+ reason: 'own-key-requires-scoped-spawn' };
1838
+ }
1839
+ }
1805
1840
  result = await adapter.spawn(frameDecisionForkRule(promptForSpawn), {
1841
+ onSpawn: reportRuntime ? (controls) => {
1842
+ observedSpawn = true;
1843
+ return runtimeReporter.spawn(controls);
1844
+ } : undefined,
1845
+ providerKey,
1806
1846
  timeoutMs: effectiveTurnTimeoutMs,
1807
1847
  sessionId,
1808
1848
  cwd: agentCwd,
@@ -1831,6 +1871,7 @@ export const performRun = ({
1831
1871
  spawnCompleted = true;
1832
1872
  } finally {
1833
1873
  try {
1874
+ if (reportRuntime) await runtimeReporter.settled(observedSpawn ? spawnCompleted ? 'completed' : 'failed' : null);
1834
1875
  commitAttribution?.cleanup();
1835
1876
  } finally {
1836
1877
  if (spawnCompleted && skillMount) {
@@ -3537,6 +3578,11 @@ Docs:
3537
3578
  // TASK-102 part B: the operator's TTL ask, if any. Unset means the
3538
3579
  // server's published default, which is the only lifetime we can justify
3539
3580
  // without an operator saying otherwise.
3581
+ runtimeReportClient: createMachineReportClient({ instanceUrl: record.instanceUrl,
3582
+ machineId: record.machineId, agentName: record.agentName, instanceId: record.instanceId }),
3583
+ resolveOwnProviderKey: createOwnKeyResolver({ instanceUrl: record.instanceUrl,
3584
+ machineId: record.machineId, agentName: record.agentName, instanceId: record.instanceId }),
3585
+ reportRuntime: Boolean(record.machineId || process.env.COMMONLY_MANAGED_RUNNER === '1'),
3540
3586
  spawnCredentialTtlSeconds: resolveSpawnTtlSeconds(process.env),
3541
3587
  refusalHandoff: opts.refusalHandoff || null,
3542
3588
  log: (line) => console.log(`${stamp()} [${name}] ${line}`),
@@ -1,3 +1,4 @@
1
+ import { collectRuntimeInventory } from '../lib/runtime-inventory.js';
1
2
  /**
2
3
  * commonly daemon <subcommand>
3
4
  *
@@ -474,6 +475,8 @@ Examples:
474
475
  // exit parks it on macOS; systemd honors 77 directly.
475
476
  process.exitCode = saved || process.platform !== 'darwin' ? 77 : 0;
476
477
  };
478
+ let cachedInventory;
479
+ let inventoryCheckedAt = 0;
477
480
  supervisor = createDaemonSupervisor({
478
481
  record,
479
482
  client,
@@ -488,10 +491,18 @@ Examples:
488
491
  chmodSync(seatLog, 0o600);
489
492
  return spawn(process.execPath, [process.argv[1], 'agent', 'run', agentName], {
490
493
  stdio: ['ignore', out, out],
494
+ env: { ...process.env, COMMONLY_MANAGED_RUNNER: '1' },
491
495
  });
492
496
  },
493
497
  loadToken: loadAgentToken,
494
498
  saveToken: saveAgentToken,
499
+ runtimeInventory: async () => {
500
+ if (!cachedInventory || Date.now() - inventoryCheckedAt > 300_000) {
501
+ cachedInventory = await collectRuntimeInventory(getAdapter);
502
+ inventoryCheckedAt = Date.now();
503
+ }
504
+ return cachedInventory;
505
+ },
495
506
  resolveAdapter: (runtime) => resolveAdapterForRuntime(runtime),
496
507
  log: stampLog,
497
508
  persistState: (seats) => saveDaemonState({
@@ -1,3 +1,5 @@
1
+ import { claudeRuntimeInventory } from '../native-runtime-inventory.js';
2
+ import { observeSpawn, controlsFromArguments } from '../runtime-observation.js';
1
3
  /**
2
4
  * claude adapter — wraps the local `claude` CLI as a Commonly agent.
3
5
  *
@@ -289,12 +291,13 @@ const publicSkillPromptArgs = async (skillSources) => {
289
291
  return { args: text ? ['--append-system-prompt', text] : [], included };
290
292
  };
291
293
 
292
- const runClaude = ({ cmd, args, cwd, env, timeoutMs, credentials = [], spawnImpl = childSpawn }) => new Promise((resolve, reject) => {
294
+ const runClaude = ({ cmd, args, cwd, env, timeoutMs, credentials = [], spawnImpl = childSpawn, onSpawn, controls = {} }) => new Promise((resolve, reject) => {
293
295
  const proc = spawnImpl(cmd, args, {
294
296
  cwd,
295
297
  env,
296
298
  stdio: ['ignore', 'pipe', 'pipe'],
297
299
  });
300
+ observeSpawn(proc, onSpawn, controls);
298
301
  let stdout = '';
299
302
  let stderr = '';
300
303
  let timedOut = false;
@@ -736,11 +739,13 @@ export default {
736
739
  // hosted variant.
737
740
  runtimeType: 'claude-code',
738
741
 
742
+ inventory: () => claudeRuntimeInventory({ env: withClaudePath(process.env) }),
743
+
739
744
  async detect() {
740
745
  try {
741
746
  const claudeEnv = withClaudePath(process.env);
742
747
  const res = spawnSync('claude', ['--version'], {
743
- encoding: 'utf8',
748
+ encoding: 'utf8', timeout: 5000,
744
749
  env: claudeEnv,
745
750
  });
746
751
  if (res.error || res.status !== 0) return null;
@@ -900,7 +905,7 @@ export default {
900
905
  // and its provider key is not an env var here, so the seat credential is
901
906
  // the value a tail can echo back.
902
907
  credentials: spawnCredentials(spawnCtx),
903
- spawnImpl: ctx._spawnImpl, // test seam only — do not use in production
908
+ spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(modelArgs, { modelFlag: '--model', effortFlag: '--effort' }), // test seam only — do not use in production
904
909
  });
905
910
  // TASK-440: the context this turn left, read from the transcript the
906
911
  // child wrote under its own env (an isolated HOME for public seats).
@@ -951,7 +956,7 @@ export default {
951
956
  env: retry.env,
952
957
  timeoutMs: ctx.timeoutMs || DEFAULT_TIMEOUT_MS,
953
958
  credentials: spawnCredentials(retryCtx),
954
- spawnImpl: ctx._spawnImpl,
959
+ spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(modelArgs, { modelFlag: '--model', effortFlag: '--effort' }),
955
960
  });
956
961
  const contextTokens = claudeContextTokens({
957
962
  cwd: ctx.cwd, sessionId: freshId,
@@ -1,3 +1,5 @@
1
+ import { codexRuntimeInventory } from '../native-runtime-inventory.js';
2
+ import { observeSpawn, controlsFromArguments } from '../runtime-observation.js';
1
3
  /**
2
4
  * codex adapter — wraps the local `codex` CLI as a Commonly agent.
3
5
  *
@@ -570,7 +572,7 @@ const CODEX_PROVIDER_KEY_ENVS = [
570
572
  'CODEX_API_KEY',
571
573
  ];
572
574
 
573
- const runCodex = ({ args, cwd, env, timeoutMs, credentials = [], spawnImpl = childSpawn }) => new Promise((resolve, reject) => { // stdio: ['ignore', 'pipe', 'pipe'] — without this, child_process.spawn
575
+ const runCodex = ({ args, cwd, env, timeoutMs, credentials = [], spawnImpl = childSpawn, onSpawn, controls = {} }) => new Promise((resolve, reject) => { // stdio: ['ignore', 'pipe', 'pipe'] — without this, child_process.spawn
574
576
  // defaults stdin to a fresh pipe. Codex 0.125.0's `exec` then blocks on
575
577
  // `Reading additional input from stdin...` because it sees an open pipe
576
578
  // and waits for input that never arrives. Interactive runs are fine because
@@ -578,6 +580,7 @@ const runCodex = ({ args, cwd, env, timeoutMs, credentials = [], spawnImpl = chi
578
580
  // `'ignore'` gives codex /dev/null → immediate EOF → it falls back to the
579
581
  // argv prompt as intended. Surfaced live during ADR-005 Phase 2 smoke.
580
582
  const proc = spawnImpl('codex', args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
583
+ observeSpawn(proc, onSpawn, controls);
581
584
  let stderr = '';
582
585
  let timedOut = false;
583
586
  const events = makeEventParser();
@@ -625,9 +628,11 @@ export default {
625
628
  // `runtimeType` and differ only on `host`.
626
629
  runtimeType: 'codex',
627
630
 
631
+ inventory: codexRuntimeInventory,
632
+
628
633
  async detect() {
629
634
  try {
630
- const res = spawnSync('codex', ['--version'], { encoding: 'utf8' });
635
+ const res = spawnSync('codex', ['--version'], { encoding: 'utf8', timeout: 5000 });
631
636
  if (res.error || res.status !== 0) return null;
632
637
  // `codex --version` prints e.g. "codex-cli 0.125.0" — last token is
633
638
  // the version. Defensive against future format tweaks: if no
@@ -734,7 +739,7 @@ export default {
734
739
  // already removed the token from the child copy, and after that there is
735
740
  // nothing left to compare a tail against.
736
741
  credentials: spawnCredentials(ctx, CODEX_PROVIDER_KEY_ENVS),
737
- spawnImpl: ctx._spawnImpl, // test seam only — do not use in production
742
+ spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(args.slice(args.indexOf('exec') + (args[args.indexOf('exec') + 1] === 'resume' ? 3 : 1), -1), { modelFlag: '--model', valueFlags: ['-c', '-o', '--permission-profile'], codex: true }), // test seam only — do not use in production
738
743
  });
739
744
 
740
745
  let text = '';
@@ -1,3 +1,4 @@
1
+ import { observeSpawn } from '../runtime-observation.js';
1
2
  /** DeepSeek Harness 0.2.1-rc.2, private seats only. Configuration is per spawn. */
2
3
  import { requireSeatModel } from './seat-model.js';
3
4
  import { spawn as childSpawn, spawnSync } from 'child_process';
@@ -167,8 +168,9 @@ const parseEvent = (line, state) => {
167
168
  }
168
169
  };
169
170
 
170
- export const runDsh = ({ binary, args, cwd, env, timeoutMs, credentials, spawnImpl = childSpawn }) => new Promise((resolve, reject) => {
171
+ export const runDsh = ({ binary, args, cwd, env, timeoutMs, credentials, spawnImpl = childSpawn, onSpawn, controls = {} }) => new Promise((resolve, reject) => {
171
172
  const proc = spawnImpl(binary, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
173
+ observeSpawn(proc, onSpawn, controls);
172
174
  const state = { sessionId: null, final: null, error: null, buffer: '' };
173
175
  let stderr = '';
174
176
  let timedOut = false;
@@ -277,11 +279,14 @@ export default {
277
279
  const mcp = resolveCommonlyMcp(ctx.environment?.mcp, {
278
280
  credentialFile: credential.path, instanceUrl: ctx.instanceUrl,
279
281
  });
280
- const patch = JSON.stringify(buildPatch({
282
+ const patchData = buildPatch({
281
283
  proxyBaseURL: proxy.baseURL, model: seatModel,
282
284
  cwd: ctx.cwd, mcp, effort: ctx.environment.effort,
283
285
  deepseekOfficial: new URL(ctx.environment.provider.baseURL).hostname === 'api.deepseek.com',
284
- }), null, 2);
286
+ });
287
+ const patch = JSON.stringify(patchData, null, 2);
288
+ const passedProvider = patchData.find((entry) => entry.id === 'llm-pi-ai').config.providers.commonly;
289
+ const passedModel = patchData.find((entry) => entry.id === 'agent-default-model').config.model;
285
290
  assertPatchHasNoCredentials(patch, [ctx.runtimeToken, bearer]);
286
291
  const patchPath = join(tempDir, 'seat.patch.yml');
287
292
  await writeFile(patchPath, `${patch}\n`, { mode: 0o600 });
@@ -296,7 +301,7 @@ export default {
296
301
  ? Math.min(requested, MAX_TIMEOUT_MS) : MAX_TIMEOUT_MS;
297
302
  return await runDsh({
298
303
  binary, args, cwd: ctx.cwd, env, timeoutMs,
299
- credentials: [...spawnCredentials(ctx, []), bearer], spawnImpl: ctx._spawnImpl,
304
+ credentials: [...spawnCredentials(ctx, []), bearer], spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: { model: passedModel || null, effort: passedProvider.reasoning || null },
300
305
  });
301
306
  } finally {
302
307
  try { await proxy?.close(); } catch { /* best effort */ }
@@ -1,3 +1,4 @@
1
+ import { observeSpawn, controlsFromArguments } from '../runtime-observation.js';
1
2
  /** Hermes 0.22.2 local CLI wrapper. Hermes owns provider I/O; this file only spawns it. */
2
3
  import { requireSeatModel, seatModelRequired } from './seat-model.js';
3
4
  import { spawn as childSpawn, spawnSync } from 'child_process';
@@ -49,10 +50,11 @@ export const readHermesJsonl = (line, state) => {
49
50
  }
50
51
  };
51
52
 
52
- const runHermes = ({ args, prompt, cwd, env, timeoutMs, spawnImpl, credentials }) => new Promise((resolve, reject) => {
53
+ const runHermes = ({ args, prompt, cwd, env, timeoutMs, spawnImpl, credentials, onSpawn, controls = {} }) => new Promise((resolve, reject) => {
53
54
  const child = (spawnImpl || childSpawn)('hermes', args, {
54
55
  cwd, env, stdio: ['pipe', 'pipe', 'pipe'],
55
56
  });
57
+ observeSpawn(child, onSpawn, controls);
56
58
  const state = { started: false, result: null };
57
59
  const decoder = new StringDecoder('utf8');
58
60
  let buffer = '';
@@ -190,7 +192,7 @@ export default {
190
192
  ? Math.min(requestedTimeout, DEFAULT_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS;
191
193
  return runHermes({
192
194
  args, prompt: fullPrompt, cwd: ctx.cwd, env, timeoutMs,
193
- spawnImpl: ctx._spawnImpl,
195
+ spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(args, { modelFlag: '--model', valueFlags: ['--query-file', '--format', '--provider', '--source', '--resume'] }),
194
196
  credentials: spawnCredentials(ctx, ['OPENROUTER_API_KEY']),
195
197
  });
196
198
  },
@@ -1,3 +1,4 @@
1
+ import { observeSpawn, controlsFromArguments } from '../runtime-observation.js';
1
2
  /**
2
3
  * OpenCode adapter — wraps `opencode run --format json` as a Commonly seat.
3
4
  *
@@ -393,7 +394,7 @@ const publicToolFailedError = ({ tool, error }) => {
393
394
  return failure;
394
395
  };
395
396
 
396
- const runOpenCode = ({ cmd, args, cwd, env, timeoutMs, credentials, spawnImpl = childSpawn }) => new Promise((resolve, reject) => {
397
+ const runOpenCode = ({ cmd, args, cwd, env, timeoutMs, credentials, spawnImpl = childSpawn, onSpawn, controls = {} }) => new Promise((resolve, reject) => {
397
398
  let proc;
398
399
  try {
399
400
  proc = spawnImpl(cmd, args, { cwd, env, stdio: ['ignore', 'pipe', 'pipe'] });
@@ -401,6 +402,7 @@ const runOpenCode = ({ cmd, args, cwd, env, timeoutMs, credentials, spawnImpl =
401
402
  reject(err);
402
403
  return;
403
404
  }
405
+ observeSpawn(proc, onSpawn, controls);
404
406
  let stderr = '';
405
407
  let timedOut = false;
406
408
  let killTimer = null;
@@ -634,7 +636,7 @@ export default {
634
636
 
635
637
  async detect() {
636
638
  try {
637
- const res = spawnSync('opencode', ['--version'], { encoding: 'utf8' });
639
+ const res = spawnSync('opencode', ['--version'], { encoding: 'utf8', timeout: VERSION_CHECK_TIMEOUT_MS });
638
640
  if (res.error || res.status !== 0) return null;
639
641
  const stdout = (res.stdout || '').trim();
640
642
  const version = parseOpenCodeVersion(stdout) || stdout || 'unknown';
@@ -666,7 +668,10 @@ export default {
666
668
  if (isPublic && !provider) {
667
669
  throw new Error('public OpenCode seats require environment.provider; the implicit public tier and operator auth.json are unsupported for public trust');
668
670
  }
669
- const providerKeyFile = await validateProviderEnvironment(ctx.environment, ctx.cwd);
671
+ if (ctx.providerKey && (!isPublic || !['workspace', 'read-only', 'bwrap'].includes(sandboxMode))) {
672
+ throw new Error('Own provider keys require a confined OpenCode runtime');
673
+ }
674
+ const providerKeyFile = ctx.providerKey ? null : await validateProviderEnvironment(ctx.environment, ctx.cwd);
670
675
 
671
676
  const sourceEnv = ctx.env || process.env;
672
677
  const childSourceEnv = Object.fromEntries(Object.entries(sourceEnv)
@@ -739,7 +744,7 @@ export default {
739
744
  // the bearer only prevents unrelated local processes from using it.
740
745
  providerProxy = await startProviderProxy({
741
746
  provider,
742
- keyFile: providerKeyFile,
747
+ keyFile: providerKeyFile, keyMaterial: ctx.providerKey,
743
748
  tokenFile: join(tempDir, 'provider-proxy-token'),
744
749
  ...(sandboxMode === 'bwrap' ? {
745
750
  socketPath: join(tempDir, 'provider-proxy.sock'),
@@ -868,7 +873,7 @@ export default {
868
873
  });
869
874
  [cmd, ...spawnArgs] = wrapped;
870
875
  }
871
- const credentials = spawnCredentials(ctx, sourceAuthEnvNames(sourceEnv));
876
+ const credentials = [...spawnCredentials(ctx, sourceAuthEnvNames(sourceEnv)), ctx.providerKey].filter(Boolean);
872
877
  const result = await runOpenCode({
873
878
  cmd,
874
879
  args: spawnArgs,
@@ -876,7 +881,7 @@ export default {
876
881
  env: childEnv,
877
882
  timeoutMs: ctx.timeoutMs || DEFAULT_TIMEOUT_MS,
878
883
  credentials,
879
- spawnImpl: ctx._spawnImpl,
884
+ spawnImpl: ctx._spawnImpl, onSpawn: ctx.onSpawn, controls: controlsFromArguments(args.slice(0, -1), { modelFlag: '-m', effortFlag: '--variant', valueFlags: ['-s', '--dir', '--title', '--format'] }),
880
885
  });
881
886
  if (isPublic) {
882
887
  const failure = publicToolFailure(result.toolErrors, ctx.environment?.mcp);
@@ -64,6 +64,7 @@ export const createDaemonSupervisor = ({
64
64
  spawnChild, // (agentName) => child emitting 'exit'; must expose .kill()
65
65
  loadToken, // (agentName) => token record | null
66
66
  saveToken, // (agentName, record) => void
67
+ runtimeInventory = async () => undefined,
67
68
  resolveAdapter, // async (runtime) => adapter name for THIS machine
68
69
  persistState = () => {}, // (agentStates) => void; must not persist secrets
69
70
  log = () => {},
@@ -343,6 +344,7 @@ export const createDaemonSupervisor = ({
343
344
  const workspacePath = workspacePathFor(nextEnvironment);
344
345
  const nextRecord = {
345
346
  ...existing,
347
+ machineId: record.machineId,
346
348
  ...(adapterChanged ? { adapter: nextAdapter } : {}),
347
349
  environment: nextEnvironment,
348
350
  ...(workspacePath ? { workspacePath } : {}),
@@ -445,6 +447,7 @@ export const createDaemonSupervisor = ({
445
447
  instanceId: row.instanceId,
446
448
  runtimeToken: minted.token,
447
449
  instanceUrl: record.instanceUrl,
450
+ machineId: record.machineId,
448
451
  podId: row.podIds?.[0] || null,
449
452
  adapter,
450
453
  ...(recordEnvironment ? { environment: recordEnvironment } : {}),
@@ -552,7 +555,7 @@ export const createDaemonSupervisor = ({
552
555
  const heartbeat = async () => {
553
556
  if (stopped || now() < reconnectAfter) return;
554
557
  try {
555
- await client.post(`/api/machines/${record.machineDbId}/heartbeat`, { agents: agentStates() });
558
+ await client.post(`/api/machines/${record.machineDbId}/heartbeat`, { agents: agentStates(), runtimeInventory: await runtimeInventory() });
556
559
  connectionRestored();
557
560
  } catch (error) {
558
561
  connectionFailure(error, 'heartbeat failed');
@@ -0,0 +1,33 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import { readFile, stat } from 'node:fs/promises';
3
+ import { homedir, tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+
6
+ export const claudeEffortsFromHelp = (help) => {
7
+ const section = String(help).match(/--effort\s+<[^>]+>([\s\S]*?)(?=\n\s+--|$)/)?.[1];
8
+ const declared = section?.match(/\((low(?:\s*,\s*(?:medium|high|xhigh|max))*)\)/)?.[1];
9
+ return declared ? declared.split(',').map((level) => level.trim()) : [];
10
+ };
11
+ export const codexModelsFromCache = (cache) => {
12
+ const models = Array.isArray(cache?.models) ? cache.models.filter((row) => typeof row.slug === 'string'
13
+ && row.slug.length > 0 && row.slug.length <= 256 && row.visibility !== 'hidden') : [];
14
+ const levels = models.map((row) => (Array.isArray(row.supported_reasoning_levels)
15
+ ? row.supported_reasoning_levels.flatMap((level) => typeof level.effort === 'string' ? [level.effort] : []) : []));
16
+ return { models: models.length ? models.map((row) => row.slug) : null,
17
+ // Only levels supported by every advertised model are offered globally.
18
+ efforts: levels.length ? levels[0].filter((level) => levels.every((list) => list.includes(level))) : [] };
19
+ };
20
+ export const claudeRuntimeInventory = async ({ spawnSyncImpl = spawnSync, env = process.env } = {}) => {
21
+ const result = spawnSyncImpl('claude', ['--help'], {
22
+ encoding: 'utf8', timeout: 5000, maxBuffer: 128 * 1024, cwd: tmpdir(), env,
23
+ });
24
+ return { models: null, efforts: result.status === 0 ? claudeEffortsFromHelp(result.stdout) : [] };
25
+ };
26
+ export const codexRuntimeInventory = async ({ readFileImpl = readFile, statImpl = stat, env = process.env } = {}) => {
27
+ // Native cache only: no command that could execute workspace extensions.
28
+ const path = join(env.CODEX_HOME || join(homedir(), '.codex'), 'models_cache.json');
29
+ try {
30
+ if ((await statImpl(path)).size > 2 * 1024 * 1024) return { models: null, efforts: [] };
31
+ return codexModelsFromCache(JSON.parse(await readFileImpl(path, 'utf8')));
32
+ } catch { return { models: null, efforts: [] }; }
33
+ };
@@ -0,0 +1,42 @@
1
+ import { loadDaemonRecord } from './daemon-store.js';
2
+ import { createClient } from './api.js';
3
+
4
+ // The daemon bearer stays in this parent closure. Seat tokens can be inherited
5
+ // by model children during mint fallback, so they never authorize key reads.
6
+ export const createOwnKeyResolver = ({ instanceUrl, machineId, agentName, instanceId = 'default',
7
+ loadRecord = loadDaemonRecord, clientFactory = createClient }) => async ({ opened, harness, provider }) => {
8
+ if (harness !== 'opencode' || !provider) return undefined;
9
+ const record = loadRecord();
10
+ if (!record || (machineId && record.machineId !== machineId)
11
+ || new URL(record.instanceUrl).href !== new URL(instanceUrl).href) {
12
+ throw new Error('Own provider key requires this machine’s registered daemon');
13
+ }
14
+ let launch;
15
+ try {
16
+ launch = await clientFactory({ instance: instanceUrl, token: record.daemonToken })
17
+ .post('/api/agents/runtime/provider-key/launch', { agentName, instanceId, harness,
18
+ providerId: provider.id, baseURL: provider.baseURL, metadataOnly: opened?.source !== 'spawn' });
19
+ } catch (error) {
20
+ if ([404, 405].includes(error.status)) return undefined;
21
+ throw new Error('Own provider key could not be resolved; retry after checking Settings');
22
+ }
23
+ if (!launch?.set) return undefined;
24
+ if (opened?.source !== 'spawn') {
25
+ const error = new Error('Own provider key requires a scoped spawn credential; turn was not started');
26
+ error.ownKeySpawnRefused = true;
27
+ throw error;
28
+ }
29
+ return launch.key;
30
+ };
31
+
32
+ // Parent-only receipt client. Failure to load machine authority disables
33
+ // diagnostics; it must never fall back to a seat bearer.
34
+ export const createMachineReportClient = ({ instanceUrl, machineId, agentName, instanceId = 'default' }) => {
35
+ try {
36
+ const record = loadDaemonRecord();
37
+ if (!record || (machineId && machineId !== record.machineId)
38
+ || new URL(record.instanceUrl).href !== new URL(instanceUrl).href) return null;
39
+ const client = createClient({ instance: instanceUrl, token: record.daemonToken });
40
+ return { post: (path, body) => client.post(path, { ...body, agentName, instanceId }) };
41
+ } catch { return null; }
42
+ };
@@ -1,3 +1,4 @@
1
+ import { Transform } from 'node:stream';
1
2
  /** The per-spawn loopback proxy shared by OpenAI-compatible CLI adapters. */
2
3
  import { randomBytes, timingSafeEqual } from 'crypto';
3
4
  import { constants as fsConstants, realpathSync } from 'fs';
@@ -6,6 +7,30 @@ import { request as httpsRequest } from 'https';
6
7
  import { chmod, lstat, open, unlink, writeFile } from 'fs/promises';
7
8
  import { isAbsolute, relative, sep } from 'path';
8
9
 
10
+ // Providers sometimes echo request headers in an error response. Redact the
11
+ // material across arbitrary stream chunk boundaries before any model sees it.
12
+ export const redactProviderMaterial = (material) => {
13
+ const secret = Buffer.from(material);
14
+ let pending = Buffer.alloc(0);
15
+ return new Transform({
16
+ transform(chunk, _encoding, done) {
17
+ pending = Buffer.concat([pending, Buffer.from(chunk)]);
18
+ while (pending.length >= secret.length) {
19
+ const index = pending.indexOf(secret);
20
+ if (index >= 0) {
21
+ this.push(pending.subarray(0, index)); this.push('[redacted]');
22
+ pending = pending.subarray(index + secret.length);
23
+ } else {
24
+ const count = pending.length - secret.length + 1;
25
+ this.push(pending.subarray(0, count)); pending = pending.subarray(count);
26
+ }
27
+ }
28
+ done();
29
+ },
30
+ flush(done) { this.push(pending); pending = Buffer.alloc(0); done(); },
31
+ });
32
+ };
33
+
9
34
  const isPathWithin = (parent, candidate) => {
10
35
  const fromParent = relative(parent, candidate);
11
36
  return fromParent === '' || (fromParent !== '..'
@@ -68,7 +93,7 @@ const readProviderKey = async (keyFile, adapterLabel) => {
68
93
  };
69
94
 
70
95
  export const startProviderProxy = async ({
71
- provider, keyFile, tokenFile, socketPath, isolatedPort, adapterLabel = 'OpenCode',
96
+ provider, keyFile, keyMaterial, tokenFile, socketPath, isolatedPort, adapterLabel = 'OpenCode',
72
97
  }) => {
73
98
  const upstream = new URL(provider.baseURL);
74
99
  if (socketPath && (!isAbsolute(socketPath)
@@ -80,7 +105,8 @@ export const startProviderProxy = async ({
80
105
  throw new Error(`${adapterLabel} provider.baseURL must be an http(s) URL without embedded credentials or query data`);
81
106
  }
82
107
  const basePath = upstream.pathname.replace(/\/+$/, '') || '/';
83
- const upstreamKey = await readProviderKey(keyFile, adapterLabel);
108
+ const upstreamKey = keyMaterial === undefined ? await readProviderKey(keyFile, adapterLabel) : keyMaterial;
109
+ if (typeof upstreamKey !== 'string' || !upstreamKey || /[\r\n]/.test(upstreamKey)) throw new Error('Invalid provider key');
84
110
  const proxyToken = randomBytes(32).toString('base64url');
85
111
  await writeFile(tokenFile, proxyToken, { encoding: 'utf8', mode: 0o600 });
86
112
  await chmod(tokenFile, 0o600);
@@ -176,6 +202,7 @@ export const startProviderProxy = async ({
176
202
  ].includes(name.toLowerCase())));
177
203
  headers.authorization = `Bearer ${upstreamKey}`;
178
204
  headers.host = target.host;
205
+ headers['accept-encoding'] = 'identity';
179
206
  const request = (target.protocol === 'https:' ? httpsRequest : httpRequest)({
180
207
  protocol: target.protocol,
181
208
  hostname: target.hostname,
@@ -189,13 +216,21 @@ export const startProviderProxy = async ({
189
216
  outgoing.writeHead(502).end('Provider redirects are not supported');
190
217
  return;
191
218
  }
219
+ if (response.headers['content-encoding'] && response.headers['content-encoding'] !== 'identity') {
220
+ response.resume(); outgoing.writeHead(502).end('Compressed provider responses are not supported'); return;
221
+ }
192
222
  const responseHeaders = Object.fromEntries(Object.entries(response.headers)
193
223
  .filter(([name]) => ![
194
224
  'connection', 'keep-alive', 'proxy-authenticate', 'proxy-authorization',
195
225
  'te', 'trailer', 'transfer-encoding', 'upgrade',
196
226
  ].includes(name.toLowerCase())));
227
+ delete responseHeaders['content-length'];
228
+ for (const [name, value] of Object.entries(responseHeaders)) {
229
+ if (typeof value === 'string') responseHeaders[name] = value.split(upstreamKey).join('[redacted]');
230
+ else if (Array.isArray(value)) responseHeaders[name] = value.map((entry) => entry.split(upstreamKey).join('[redacted]'));
231
+ }
197
232
  outgoing.writeHead(response.statusCode || 502, responseHeaders);
198
- response.pipe(outgoing);
233
+ response.pipe(redactProviderMaterial(upstreamKey)).pipe(outgoing);
199
234
  });
200
235
  request.on('error', () => {
201
236
  if (!outgoing.headersSent) outgoing.writeHead(502);
@@ -0,0 +1,12 @@
1
+ // Probe only the walked adapter's version command. Never enumerate workspace
2
+ // extensions to discover models: a model picker must not execute project code.
3
+ export const WALKED_HARNESSES = ['claude', 'codex', 'opencode', 'hermes', 'dsh'];
4
+ export const collectRuntimeInventory = async (getAdapter, capabilities) => Promise.all(WALKED_HARNESSES.map(async (harness) => {
5
+ const adapter = getAdapter(harness);
6
+ let detected = null;
7
+ try { detected = await adapter?.detect(); } catch { /* unavailable */ }
8
+ let choices = { models: null, efforts: [] };
9
+ if (detected) { try { choices = (await (capabilities ? capabilities(harness) : adapter?.inventory?.())) || choices; } catch { /* unavailable */ } }
10
+ return { harness, installed: Boolean(detected), version: detected?.version || null,
11
+ models: choices.models, efforts: choices.efforts };
12
+ }));
@@ -0,0 +1,51 @@
1
+ // Read only adapter-owned control arguments. Callers exclude prompt/stdin and
2
+ // provide every value-taking option, so a flag-shaped model/session value is
3
+ // never reinterpreted as a second option. Raw arguments never leave the runner.
4
+ export const controlsFromArguments = (args, { modelFlag, effortFlag, valueFlags = [], codex = false }) => {
5
+ const controls = { model: null, effort: null };
6
+ const flags = new Set([modelFlag, effortFlag, ...valueFlags].filter(Boolean));
7
+ for (let index = 0; index < args.length; index += 1) {
8
+ const flag = args[index];
9
+ if (!flags.has(flag)) continue;
10
+ const value = args[++index];
11
+ if (flag === modelFlag) controls.model = value || null;
12
+ if (flag === effortFlag) controls.effort = value || null;
13
+ if (codex && flag === '-c' && value?.startsWith('model_reasoning_effort=')) {
14
+ try { controls.effort = JSON.parse(value.slice('model_reasoning_effort='.length)); } catch { /* unknown encoding */ }
15
+ }
16
+ }
17
+ return controls;
18
+ };
19
+
20
+ // Called at the child's successful OS spawn event, not when a requested
21
+ // configuration is saved or when a turn is merely queued. Never send argv.
22
+ export const observeSpawn = (child, onSpawn, controls) => {
23
+ if (typeof onSpawn !== 'function') return;
24
+ child.once('spawn', () => {
25
+ try { onSpawn({ model: controls.model || null, effort: controls.effort || null, at: new Date().toISOString() }); } catch { /* diagnostic only */ }
26
+ });
27
+ };
28
+
29
+ export const createRuntimeReporter = ({ client, harness, detect }) => {
30
+ let queue = Promise.resolve();
31
+ const send = (body) => {
32
+ // Serialize so a fast process cannot have its settled report overtake its
33
+ // spawn report. Old backends and diagnostic outages don't stop a turn.
34
+ queue = queue.then(() => client.post('/api/agents/runtime/observation', body)).catch(() => {});
35
+ return queue;
36
+ };
37
+ return {
38
+ async connect() {
39
+ return send({ event: 'connected' });
40
+ },
41
+ spawn: ({ model, effort, at }) => {
42
+ const receipt = { event: 'spawn', harness, version: null, model, effort, ...(at ? { at } : {}) };
43
+ queue = queue.then(async () => {
44
+ try { receipt.version = (await detect())?.version || null; } catch { /* unavailable */ }
45
+ await client.post('/api/agents/runtime/observation', receipt);
46
+ }).catch(() => {});
47
+ return queue;
48
+ },
49
+ settled: (outcome = null) => send({ event: 'settled', ...(outcome ? { outcome } : {}) }),
50
+ };
51
+ };