@commonlyai/cli 0.1.165 → 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 +1 -1
- package/src/commands/agent.js +74 -3
- package/src/commands/daemon.js +11 -0
- package/src/lib/adapters/claude.js +9 -4
- package/src/lib/adapters/codex.js +8 -3
- package/src/lib/adapters/dsh.js +9 -4
- package/src/lib/adapters/hermes.js +4 -2
- package/src/lib/adapters/opencode.js +11 -6
- package/src/lib/daemon-supervisor.js +4 -1
- package/src/lib/native-runtime-inventory.js +33 -0
- package/src/lib/own-key-launch.js +42 -0
- package/src/lib/provider-proxy.js +38 -3
- package/src/lib/runtime-inventory.js +12 -0
- package/src/lib/runtime-observation.js +51 -0
package/package.json
CHANGED
package/src/commands/agent.js
CHANGED
|
@@ -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
|
*
|
|
@@ -667,12 +670,29 @@ export const updateAgentConfiguration = async ({
|
|
|
667
670
|
model = null,
|
|
668
671
|
effort = null,
|
|
669
672
|
envPath = null,
|
|
673
|
+
runtimeKey = false,
|
|
670
674
|
parseEnv = parseEnvironmentFile,
|
|
671
675
|
adapterRegistry = { getAdapter, listAdapterNames },
|
|
672
676
|
}) => {
|
|
673
677
|
if (!record?.agentName) {
|
|
674
678
|
throw new Error('token record is missing agentName — re-attach the agent');
|
|
675
679
|
}
|
|
680
|
+
if (runtimeKey) {
|
|
681
|
+
if (adapter || envPath) throw new Error('--adapter and --env require an owner login; run commonly login');
|
|
682
|
+
const controls = Object.fromEntries(Object.entries({ model, effort })
|
|
683
|
+
.filter(([, value]) => value !== null && value !== undefined));
|
|
684
|
+
if (!Object.keys(controls).length) throw new Error('provide --model or --effort');
|
|
685
|
+
const environment = { ...record.environment, ...controls };
|
|
686
|
+
const validation = validateEnvironmentSpec(environment);
|
|
687
|
+
if (!validation.ok) throw new Error(validation.errors.join('; '));
|
|
688
|
+
const result = await client.patch('/api/agents/runtime/configuration', {
|
|
689
|
+
agentName: record.agentName, instanceId: record.instanceId || 'default', ...controls,
|
|
690
|
+
});
|
|
691
|
+
return {
|
|
692
|
+
agentName: record.agentName, instanceId: record.instanceId || 'default', podId: result.podId,
|
|
693
|
+
changed: Object.keys(controls), environment,
|
|
694
|
+
};
|
|
695
|
+
}
|
|
676
696
|
const instanceId = record.instanceId || 'default';
|
|
677
697
|
const runtime = {};
|
|
678
698
|
const environmentRuntime = {};
|
|
@@ -1245,8 +1265,12 @@ export const performRun = ({
|
|
|
1245
1265
|
sleepImpl = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }),
|
|
1246
1266
|
// The agent to @mention when the model provider refuses a request's content
|
|
1247
1267
|
// (lib/content-refusal.js). Null: the refusal notice names no one.
|
|
1268
|
+
reportRuntime = false,
|
|
1269
|
+
resolveOwnProviderKey = null,
|
|
1270
|
+
runtimeReportClient = null,
|
|
1248
1271
|
refusalHandoff = null,
|
|
1249
1272
|
}) => {
|
|
1273
|
+
reportRuntime = reportRuntime && WALKED_HARNESSES.includes(adapter?.name);
|
|
1250
1274
|
if (environment?.provider !== undefined && !adapter?.supportsProvider) {
|
|
1251
1275
|
throw new Error('environment.provider requires a provider-capable adapter');
|
|
1252
1276
|
}
|
|
@@ -1254,6 +1278,11 @@ export const performRun = ({
|
|
|
1254
1278
|
const effectiveTurnTimeoutMs = Number.isFinite(requestedTurnTimeout) && requestedTurnTimeout > 0
|
|
1255
1279
|
? Math.min(requestedTurnTimeout, 2_147_483_647) : 15 * 60 * 1000;
|
|
1256
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();
|
|
1257
1286
|
const missingModelNotices = new Set();
|
|
1258
1287
|
const handoffAgent = sanitizeHandoffAgent(refusalHandoff);
|
|
1259
1288
|
if (refusalHandoff && !handoffAgent) {
|
|
@@ -1304,12 +1333,18 @@ export const performRun = ({
|
|
|
1304
1333
|
// the adapter spawns with, and the stored record keeps its broker, so the
|
|
1305
1334
|
// daemon stays the layer that judges a host it knows. Applied once for the
|
|
1306
1335
|
// run rather than per turn: `agent run` reads its record once at boot.
|
|
1307
|
-
const
|
|
1336
|
+
const requestedSeatEnvironment = withholdGrantBroker(environment, adapter?.name, {
|
|
1308
1337
|
instanceUrl,
|
|
1309
1338
|
onRefuse: (refusal, names) => log(
|
|
1310
1339
|
`${refusal.code} (${refusal.reason}) — withholding ${names.join(', ')}: ${refusal.detail}`,
|
|
1311
1340
|
),
|
|
1312
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;
|
|
1313
1348
|
// OpenCode retains tool-call IDs in its resumed history. A Gemini proxy
|
|
1314
1349
|
// marks those IDs with signature references, so changing providers must
|
|
1315
1350
|
// start a fresh session before another upstream can receive them.
|
|
@@ -1367,6 +1402,10 @@ export const performRun = ({
|
|
|
1367
1402
|
const batchLimit = Math.min(50, Math.max(1, Number(inboxBatchLimit) || 10));
|
|
1368
1403
|
|
|
1369
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
|
+
}
|
|
1370
1409
|
const eventPodId = event.podId || podId;
|
|
1371
1410
|
const prompt = extractPrompt(event);
|
|
1372
1411
|
if (!prompt || !eventPodId) {
|
|
@@ -1768,6 +1807,7 @@ export const performRun = ({
|
|
|
1768
1807
|
? { mounted: serverSkills.mounted, conflicted: serverSkills.conflicted, ...(skillReadUnavailable ? { status: 'unavailable' } : {}) }
|
|
1769
1808
|
: null;
|
|
1770
1809
|
let spawnCompleted = false;
|
|
1810
|
+
let observedSpawn = false;
|
|
1771
1811
|
let commitAttribution = null;
|
|
1772
1812
|
try {
|
|
1773
1813
|
// The local CLI is a wrapper for a Commonly seat, so its Git commits
|
|
@@ -1785,7 +1825,24 @@ export const performRun = ({
|
|
|
1785
1825
|
effort: seatEnvironment?.effort,
|
|
1786
1826
|
});
|
|
1787
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
|
+
}
|
|
1788
1840
|
result = await adapter.spawn(frameDecisionForkRule(promptForSpawn), {
|
|
1841
|
+
onSpawn: reportRuntime ? (controls) => {
|
|
1842
|
+
observedSpawn = true;
|
|
1843
|
+
return runtimeReporter.spawn(controls);
|
|
1844
|
+
} : undefined,
|
|
1845
|
+
providerKey,
|
|
1789
1846
|
timeoutMs: effectiveTurnTimeoutMs,
|
|
1790
1847
|
sessionId,
|
|
1791
1848
|
cwd: agentCwd,
|
|
@@ -1814,6 +1871,7 @@ export const performRun = ({
|
|
|
1814
1871
|
spawnCompleted = true;
|
|
1815
1872
|
} finally {
|
|
1816
1873
|
try {
|
|
1874
|
+
if (reportRuntime) await runtimeReporter.settled(observedSpawn ? spawnCompleted ? 'completed' : 'failed' : null);
|
|
1817
1875
|
commitAttribution?.cleanup();
|
|
1818
1876
|
} finally {
|
|
1819
1877
|
if (spawnCompleted && skillMount) {
|
|
@@ -3520,6 +3578,11 @@ Docs:
|
|
|
3520
3578
|
// TASK-102 part B: the operator's TTL ask, if any. Unset means the
|
|
3521
3579
|
// server's published default, which is the only lifetime we can justify
|
|
3522
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'),
|
|
3523
3586
|
spawnCredentialTtlSeconds: resolveSpawnTtlSeconds(process.env),
|
|
3524
3587
|
refusalHandoff: opts.refusalHandoff || null,
|
|
3525
3588
|
log: (line) => console.log(`${stamp()} [${name}] ${line}`),
|
|
@@ -3774,13 +3837,21 @@ Use --local to find the name you'd pass to 'agent run' or 'agent detach'.
|
|
|
3774
3837
|
process.exit(1);
|
|
3775
3838
|
}
|
|
3776
3839
|
const instance = opts.instance || record.instanceUrl;
|
|
3777
|
-
const
|
|
3840
|
+
const ownerToken = getToken(instance);
|
|
3841
|
+
const runtimeKey = !opts.adapter && !opts.env && !ownerToken && Boolean(record.runtimeToken);
|
|
3842
|
+
const token = runtimeKey ? record.runtimeToken : ownerToken;
|
|
3778
3843
|
if (!token) { console.error('Not logged in. Run: commonly login'); process.exit(1); }
|
|
3779
|
-
const client = createClient({ instance: resolveInstanceUrl(instance), token });
|
|
3780
3844
|
try {
|
|
3845
|
+
const instanceUrl = resolveInstanceUrl(instance);
|
|
3846
|
+
if (runtimeKey && (!record.instanceUrl
|
|
3847
|
+
|| new URL(instanceUrl).href.replace(/\/$/, '') !== new URL(record.instanceUrl).href.replace(/\/$/, ''))) {
|
|
3848
|
+
throw new Error('Changing --instance requires an owner login for that instance; run commonly login');
|
|
3849
|
+
}
|
|
3850
|
+
const client = createClient({ instance: instanceUrl, token });
|
|
3781
3851
|
const result = await updateAgentConfiguration({
|
|
3782
3852
|
client,
|
|
3783
3853
|
record,
|
|
3854
|
+
runtimeKey,
|
|
3784
3855
|
adapter: opts.adapter,
|
|
3785
3856
|
model: opts.model,
|
|
3786
3857
|
effort: opts.effort,
|
package/src/commands/daemon.js
CHANGED
|
@@ -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 = '';
|
package/src/lib/adapters/dsh.js
CHANGED
|
@@ -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
|
|
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
|
-
})
|
|
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
|
-
|
|
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
|
+
};
|