@prohost/cli 0.13.3 → 0.15.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.
- package/CHANGELOG.md +90 -0
- package/README.md +129 -0
- package/dist/agent/command.d.ts +8 -0
- package/dist/agent/command.js +40 -7
- package/dist/agent/daemon.d.ts +3 -1
- package/dist/agent/daemon.js +1 -2
- package/dist/agent/run.d.ts +30 -1
- package/dist/agent/run.js +230 -9
- package/dist/agent/runtime_status.d.ts +99 -3
- package/dist/agent/runtime_status.js +173 -5
- package/dist/agent/worktrees.d.ts +2 -0
- package/dist/agent/worktrees.js +3 -2
- package/dist/codehost/activity.d.ts +64 -0
- package/dist/codehost/activity.js +135 -0
- package/dist/codehost/agents.d.ts +31 -0
- package/dist/codehost/agents.js +57 -0
- package/dist/codehost/checkouts.d.ts +130 -0
- package/dist/codehost/checkouts.js +270 -0
- package/dist/codehost/claude_trust.d.ts +40 -0
- package/dist/codehost/claude_trust.js +129 -0
- package/dist/codehost/command.d.ts +25 -0
- package/dist/codehost/command.js +136 -0
- package/dist/codehost/decision.d.ts +156 -0
- package/dist/codehost/decision.js +553 -0
- package/dist/codehost/events.d.ts +49 -0
- package/dist/codehost/events.js +87 -0
- package/dist/codehost/folders.d.ts +122 -0
- package/dist/codehost/folders.js +317 -0
- package/dist/codehost/history.d.ts +80 -0
- package/dist/codehost/history.js +98 -0
- package/dist/codehost/hooks.d.ts +186 -0
- package/dist/codehost/hooks.js +737 -0
- package/dist/codehost/host.d.ts +297 -0
- package/dist/codehost/host.js +635 -0
- package/dist/codehost/host_stats.d.ts +71 -0
- package/dist/codehost/host_stats.js +166 -0
- package/dist/codehost/invocations.d.ts +12 -0
- package/dist/codehost/invocations.js +82 -0
- package/dist/codehost/ledger.d.ts +35 -0
- package/dist/codehost/ledger.js +91 -0
- package/dist/codehost/local_server.d.ts +106 -0
- package/dist/codehost/local_server.js +339 -0
- package/dist/codehost/pty.d.ts +38 -0
- package/dist/codehost/pty.js +47 -0
- package/dist/codehost/ready.d.ts +31 -0
- package/dist/codehost/ready.js +109 -0
- package/dist/codehost/relay.d.ts +175 -0
- package/dist/codehost/relay.js +598 -0
- package/dist/codehost/settings.d.ts +52 -0
- package/dist/codehost/settings.js +114 -0
- package/dist/codehost/sizing.d.ts +181 -0
- package/dist/codehost/sizing.js +434 -0
- package/dist/codehost/terminals.d.ts +421 -0
- package/dist/codehost/terminals.js +1027 -0
- package/dist/codehost/tmux.d.ts +201 -0
- package/dist/codehost/tmux.js +394 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +21 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +4 -1
package/dist/agent/run.js
CHANGED
|
@@ -27,10 +27,12 @@
|
|
|
27
27
|
* way a stop pressed in the UI reaches a machine that accepts no connections.
|
|
28
28
|
*/
|
|
29
29
|
import os from 'node:os';
|
|
30
|
+
import path from 'node:path';
|
|
31
|
+
import { fileURLToPath } from 'node:url';
|
|
30
32
|
import { spawnSync } from 'node:child_process';
|
|
31
33
|
import WebSocket from 'ws';
|
|
32
34
|
import { HarnessAccount } from './account_runtime.js';
|
|
33
|
-
import { EVENT_AGENT_CONFIG_UPDATED, machineId } from './accounts.js';
|
|
35
|
+
import { EVENT_AGENT_CONFIG_UPDATED, accountEnv, machineId } from './accounts.js';
|
|
34
36
|
import { completeRun, describeFailure, isIdempotencyInFlight, postReply } from './api.js';
|
|
35
37
|
import { MAX_SALVAGE_CHARS, claudeCommand, isClaudeExec, looksLikeResumeFailure, readClaudeStream, writeMcpConfig, } from './claude.js';
|
|
36
38
|
import { APPS_FEATURE, MCP_API_KEY_ENV_VAR, PLUGINS_DISABLED_OVERRIDE, codexBinary, codexCommand, codexMcpInventoryFlag, codexProfileName, codexShellSyntax, extraMcpEntries, isCodexExec, isolateConfiguredServers, listMcpServers, mcpServersOverride, prohostMcpEntry, codexFailureMessage, looksLikeCodexResumeFailure, parseCodexOutput, } from './codex.js';
|
|
@@ -45,11 +47,13 @@ import { buildPrompt, replyFromStdout } from './prompt.js';
|
|
|
45
47
|
import { NO_CAPABILITIES, capabilityProbe } from './capabilities.js';
|
|
46
48
|
import { RunStore } from './runstore.js';
|
|
47
49
|
import { DEFAULT_CONCURRENCY, MAX_CONCURRENCY, RunScheduler } from './scheduler.js';
|
|
48
|
-
import { RUNTIME_STATUS_FEATURE, RuntimeStatusReporter, claudeQuotaProvider, codexConfiguredModel, codexQuotaProvider, modelFromExec, parseVersion, readCodexObservation, } from './runtime_status.js';
|
|
50
|
+
import { CODE_HOST_FOLDERS_FEATURE, RUNTIME_STATUS_FEATURE, RuntimeStatusReporter, claudeQuotaProvider, codexConfiguredModel, codexQuotaProvider, modelFromExec, parseVersion, readCodexObservation, shortHostname, } from './runtime_status.js';
|
|
49
51
|
import { SessionStore, sessionKeyFor } from './sessionstore.js';
|
|
50
52
|
import { verifySignature } from './signature.js';
|
|
51
53
|
import { VENUE_RUN_TOKEN_CAPABILITY, apiForRun, claudeSupportsVenueSessions, claudeVenueCommand, VENUE_PROGRESS, VENUE_SESSION_FEATURE, VENUE_SESSION_FRAME, VENUE_SESSION_OPEN_FRAME, VENUE_GRANT_CHECK_MS, VENUE_EXPIRY_MARGIN_MS, VENUE_GRANT_ENDED_ERROR, VENUE_GRANT_EXPIRED_ERROR, isVenueRun, venueEnv, venueGrantLive, venueGrantRemainingMs, venueRunFromSession, venueWorkdir, } from './venue.js';
|
|
52
54
|
import { RUN_REPOS_ENV_VAR, WorktreeManager } from './worktrees.js';
|
|
55
|
+
import { CODE_THREAD_FOLDERS_CAPABILITY, CODE_THREAD_SKIP_PERMISSIONS_CAPABILITY, CODE_THREADS_CAPABILITY, CodeHost, RefusedDeliveryError, REMOTE_TERMINAL_CAPABILITY, EVENT_CODE_THREAD_APPROVAL_DECIDED, EVENT_CODE_THREAD_INPUT_REQUESTED, EVENT_CODE_THREAD_START_REQUESTED, } from '../codehost/host.js';
|
|
56
|
+
import { loadSettings as loadCodeHostSettings } from '../codehost/settings.js';
|
|
53
57
|
import { isWebhookEvent } from '../forward.js';
|
|
54
58
|
import { DEFAULT_WS_URL } from '../listen.js';
|
|
55
59
|
import { ServerLiveness, enableTcpKeepAlive, seconds, terminateSocket } from '../liveness.js';
|
|
@@ -105,9 +109,15 @@ const HANDSHAKE_TIMEOUT_MS = 30_000;
|
|
|
105
109
|
* uses it to stop a run from sitting on CI after the PR is open.
|
|
106
110
|
*/
|
|
107
111
|
export const PAIRED_RUN_ENV_VAR = 'PROHOST_PAIRED_RUN';
|
|
112
|
+
/** What a run sent to a `--host-only` harness is answered with: it never executes. */
|
|
113
|
+
export const HOST_ONLY_REFUSAL = "This computer only hosts Code threads; it doesn't answer messages.";
|
|
114
|
+
/** Declared by a `--host-only` harness: it executes no runs (the server may say so, or stop sending them). */
|
|
115
|
+
export const HOST_ONLY_CAPABILITY = 'host_only';
|
|
108
116
|
/** Ack status codes reported back over the socket for each frame. */
|
|
109
117
|
const ACK_ACCEPTED = 200;
|
|
110
118
|
const ACK_IGNORED = 204;
|
|
119
|
+
/** A Code-thread reply or decision refused before anything was typed; the ack names why. */
|
|
120
|
+
const ACK_REFUSED = 409;
|
|
111
121
|
const ACK_REJECTED = 401;
|
|
112
122
|
/** Received, but the run could not be taken on yet: the server keeps retrying the delivery. */
|
|
113
123
|
const ACK_RETRY = 503;
|
|
@@ -1280,6 +1290,63 @@ mcp) {
|
|
|
1280
1290
|
* Reconnects on any drop with jittered exponential backoff; an ``auth_error``
|
|
1281
1291
|
* is fatal, because retrying a rejected credential just hammers the server.
|
|
1282
1292
|
*/
|
|
1293
|
+
/**
|
|
1294
|
+
* The login a Code thread's agent runs on: the account picked for this
|
|
1295
|
+
* agent, as its ordinary runs get it. A thread for the other CLI, or an
|
|
1296
|
+
* account that is no longer on this machine, is refused rather than run on
|
|
1297
|
+
* some other login — it would spend a subscription the agent doesn't report.
|
|
1298
|
+
* A harness without accounts (a plain `--exec`) and shell threads use the
|
|
1299
|
+
* operator's default login, exactly as everything else they run does.
|
|
1300
|
+
*/
|
|
1301
|
+
export function codeThreadAccountEnv(account, agent) {
|
|
1302
|
+
if (!account || agent === 'shell')
|
|
1303
|
+
return { ok: true, env: {} };
|
|
1304
|
+
if (account.runtime !== agent) {
|
|
1305
|
+
const name = (runtime) => (runtime === 'codex' ? 'Codex' : 'Claude Code');
|
|
1306
|
+
return {
|
|
1307
|
+
ok: false,
|
|
1308
|
+
reason: `this Mac's agent runs ${name(account.runtime)}, so it can't host a ${name(agent)} thread`,
|
|
1309
|
+
};
|
|
1310
|
+
}
|
|
1311
|
+
const resolved = account.resolve();
|
|
1312
|
+
return resolved.ok ? { ok: true, env: accountEnv(resolved.record) } : { ok: false, reason: resolved.error };
|
|
1313
|
+
}
|
|
1314
|
+
/**
|
|
1315
|
+
* The Code-thread host, when this machine's owner turned hosting on — else
|
|
1316
|
+
* `undefined`, and nothing about the harness changes.
|
|
1317
|
+
*/
|
|
1318
|
+
function buildCodeHost(options, machine, id, log, slots, accountEnv, onRemoteChanged, onCodeHostChanged) {
|
|
1319
|
+
const env = { ...process.env, ...options.env };
|
|
1320
|
+
if (!loadCodeHostSettings(env).enabled)
|
|
1321
|
+
return undefined;
|
|
1322
|
+
// The same id the event socket authenticates with, so both name one machine.
|
|
1323
|
+
if (!id) {
|
|
1324
|
+
log(`${timestamp()} ! Code-thread hosting is on, but this machine has no id yet — not hosting`);
|
|
1325
|
+
return undefined;
|
|
1326
|
+
}
|
|
1327
|
+
try {
|
|
1328
|
+
return new CodeHost({
|
|
1329
|
+
env,
|
|
1330
|
+
baseUrl: options.credentials.base_url,
|
|
1331
|
+
apiKey: options.credentials.api_key,
|
|
1332
|
+
machineId: id,
|
|
1333
|
+
machine,
|
|
1334
|
+
clientVersion: CLI_VERSION,
|
|
1335
|
+
accountId: options.credentials.prohost_account?.id,
|
|
1336
|
+
repos: options.repos ?? [],
|
|
1337
|
+
cliEntrypoint: path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', 'index.js'),
|
|
1338
|
+
log,
|
|
1339
|
+
slots,
|
|
1340
|
+
accountEnv,
|
|
1341
|
+
onRemoteChanged,
|
|
1342
|
+
onCodeHostChanged,
|
|
1343
|
+
});
|
|
1344
|
+
}
|
|
1345
|
+
catch (err) {
|
|
1346
|
+
log(`${timestamp()} ! Code-thread hosting unavailable: ${err instanceof Error ? err.message : String(err)}`);
|
|
1347
|
+
return undefined;
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1283
1350
|
export async function runAgentHarness(options) {
|
|
1284
1351
|
const log = options.onLine ?? ((line) => process.stdout.write(`${line}\n`));
|
|
1285
1352
|
const Ctor = options.webSocketCtor ?? WebSocket;
|
|
@@ -1288,9 +1355,19 @@ export async function runAgentHarness(options) {
|
|
|
1288
1355
|
const url = options.url ?? streamUrlFor(options.credentials, DEFAULT_WS_URL);
|
|
1289
1356
|
const metrics = newMetrics();
|
|
1290
1357
|
const runtime = buildRuntime(options, log);
|
|
1358
|
+
if (options.hostOnly && options.credentials.account && options.credentials.account !== 'default') {
|
|
1359
|
+
// Host-only runs no agent of its own, so it has no account to pick: never a silent switch.
|
|
1360
|
+
log(`${timestamp()} ! --host-only: this pairing's account "${options.credentials.account}" is not used — ` +
|
|
1361
|
+
"Code threads run on this Mac's default Claude Code / Codex login");
|
|
1362
|
+
}
|
|
1291
1363
|
// Keys this computer's accounts server-side; the label above is display
|
|
1292
1364
|
// only. Only a runtime that reports accounts needs one.
|
|
1293
|
-
|
|
1365
|
+
// Hosting Code threads needs it too: the server finds a Mac by this id.
|
|
1366
|
+
const hostingCodeThreads = !options.dryRun &&
|
|
1367
|
+
(options.codeHost !== undefined
|
|
1368
|
+
? options.codeHost !== null
|
|
1369
|
+
: loadCodeHostSettings({ ...process.env, ...options.env }).enabled);
|
|
1370
|
+
const machineIdValue = runtime.account || hostingCodeThreads ? machineId({ ...process.env, ...options.env }) : undefined;
|
|
1294
1371
|
// A credential file written by 0.1.0 stores a socket URL on a host that only
|
|
1295
1372
|
// ever answers 403. Upgrading the CLI would not have fixed those installs on
|
|
1296
1373
|
// its own — the stored value wins over the constant — so say what happened
|
|
@@ -1331,7 +1408,39 @@ export async function runAgentHarness(options) {
|
|
|
1331
1408
|
accountKeys: runtime.account ? () => runtime.account.keys() : undefined,
|
|
1332
1409
|
// A run in flight reports fresh numbers when it ends; don't probe beside it.
|
|
1333
1410
|
isBusy: () => [...accepted.values()].some((entry) => entry.executing),
|
|
1334
|
-
capabilities: () =>
|
|
1411
|
+
capabilities: () => [
|
|
1412
|
+
...(runtime.venueSessions ? [VENUE_RUN_TOKEN_CAPABILITY] : []),
|
|
1413
|
+
...(options.hostOnly ? [HOST_ONLY_CAPABILITY] : []),
|
|
1414
|
+
// Only a harness that hosts threads and handles `code_thread.*` says so:
|
|
1415
|
+
// the server sends a Mac start only to a socket that declared it.
|
|
1416
|
+
// Only while the relay hasn't refused the pairing: the server lists and admits on it.
|
|
1417
|
+
// With which instance this home is: the server binds each thread to the one that started it.
|
|
1418
|
+
// With folder starts (`folder` / `separateCopy`): the server sends one only to a socket saying so.
|
|
1419
|
+
// With the agents whose binaries resolve here, re-checked per frame: the server offers only those.
|
|
1420
|
+
...(codeHost?.authorized()
|
|
1421
|
+
? [
|
|
1422
|
+
CODE_THREADS_CAPABILITY,
|
|
1423
|
+
CODE_THREAD_FOLDERS_CAPABILITY,
|
|
1424
|
+
codeHost.instanceCapability(),
|
|
1425
|
+
...(codeHost.agentCapabilities?.() ?? []),
|
|
1426
|
+
]
|
|
1427
|
+
: []),
|
|
1428
|
+
// Read on every frame, so `remote off` stops being declared at the next one.
|
|
1429
|
+
// With it, skipping permission prompts (`skipPermissions` on a start): only while remote control
|
|
1430
|
+
// is on, as the host applies it only then; the server records a thread as skipping only so.
|
|
1431
|
+
...(codeHost?.authorized() && codeHost.remoteTerminalAllowed()
|
|
1432
|
+
? [REMOTE_TERMINAL_CAPABILITY, CODE_THREAD_SKIP_PERMISSIONS_CAPABILITY]
|
|
1433
|
+
: []),
|
|
1434
|
+
],
|
|
1435
|
+
// `--machine` names this machine everywhere, exactly as given: whoever set it never sends the
|
|
1436
|
+
// real hostname. Only the real hostname is cut to its short form.
|
|
1437
|
+
hostname: () => (options.machine == null ? shortHostname() : options.machine),
|
|
1438
|
+
// Only while hosting Code threads: the owner's opt-in covers sending this machine's load.
|
|
1439
|
+
host: () => codeHost?.latestStats(),
|
|
1440
|
+
// What a person may pick for a thread on this Mac; the server lists these and refuses others.
|
|
1441
|
+
codeHostRepos: () => (codeHost?.authorized() ? codeHost.offeredRepos?.() : undefined),
|
|
1442
|
+
// The folders this Mac has opened, and its home (for `~/…`): what a phone may start a thread in.
|
|
1443
|
+
codeHostFolders: () => (codeHost?.authorized() ? codeHost.openedFolders?.() : undefined),
|
|
1335
1444
|
log,
|
|
1336
1445
|
});
|
|
1337
1446
|
runtime.observe = (observation) => status.observe(observation);
|
|
@@ -1394,6 +1503,17 @@ export async function runAgentHarness(options) {
|
|
|
1394
1503
|
const concurrency = Math.min(MAX_CONCURRENCY, Math.max(1, Math.floor(options.concurrency ?? DEFAULT_CONCURRENCY)));
|
|
1395
1504
|
const scheduler = new RunScheduler(concurrency);
|
|
1396
1505
|
const worktrees = new WorktreeManager({ repos: options.repos ?? [], env: options.env, log });
|
|
1506
|
+
const codeHost = !hostingCodeThreads
|
|
1507
|
+
? undefined
|
|
1508
|
+
: options.codeHost === undefined
|
|
1509
|
+
? buildCodeHost(options, machine, machineIdValue, log, () => ({ busy: scheduler.active, total: concurrency }), (agent) => codeThreadAccountEnv(runtime.account, agent), () => status.capabilitiesChanged(), () => status.codeHostChanged())
|
|
1510
|
+
: (options.codeHost ?? undefined);
|
|
1511
|
+
if (codeHost) {
|
|
1512
|
+
await codeHost.start().catch((err) => {
|
|
1513
|
+
log(`${timestamp()} ! Code-thread hosting did not start: ${err instanceof Error ? err.message : String(err)}`);
|
|
1514
|
+
});
|
|
1515
|
+
}
|
|
1516
|
+
let codeHostIgnoredLogged = false;
|
|
1397
1517
|
if (concurrency > 1) {
|
|
1398
1518
|
log(`${timestamp()} ⚙ up to ${concurrency} runs at once${options.concurrency === undefined ? ' (the default; --concurrency 1 for one at a time)' : ''} — runs in the same conversation still take turns` +
|
|
1399
1519
|
(worktrees.enabled
|
|
@@ -1510,6 +1630,13 @@ export async function runAgentHarness(options) {
|
|
|
1510
1630
|
log(`${timestamp()} … disconnected; reconnecting in ${(delay / 1000).toFixed(1)}s`);
|
|
1511
1631
|
await sleep(delay);
|
|
1512
1632
|
}
|
|
1633
|
+
// Remote terminals end the moment the harness does — before draining runs,
|
|
1634
|
+
// which can take hours: nothing may attach or type after a stop. The
|
|
1635
|
+
// terminals themselves stay (tmux owns them; the next start re-adopts them).
|
|
1636
|
+
// Fail-soft: a cleanup error must never keep an in-flight run from reporting.
|
|
1637
|
+
await codeHost?.stop().catch((err) => {
|
|
1638
|
+
log(`${timestamp()} ! stopping Code-thread hosting: ${err instanceof Error ? err.message : String(err)}`);
|
|
1639
|
+
});
|
|
1513
1640
|
// Let an in-flight run finish reporting before we hand back metrics — and
|
|
1514
1641
|
// any cancellation the heartbeat loop started and deliberately did not wait
|
|
1515
1642
|
// for, so the harness never exits with a run half-closed.
|
|
@@ -1645,8 +1772,11 @@ export async function runAgentHarness(options) {
|
|
|
1645
1772
|
// mentions" describes a narrower trigger than the one this harness
|
|
1646
1773
|
// actually waits on, and sends anyone debugging a silent agent to
|
|
1647
1774
|
// read mention code instead of the run dispatcher.
|
|
1648
|
-
log(
|
|
1649
|
-
`${
|
|
1775
|
+
log(options.hostOnly
|
|
1776
|
+
? `${timestamp()} ● connected as "${name}" — hosting Code threads only; ` +
|
|
1777
|
+
`every ${EVENT_AGENT_RUN_REQUESTED} is refused, nothing is executed`
|
|
1778
|
+
: `${timestamp()} ● connected as "${name}" — waiting for ` +
|
|
1779
|
+
`${EVENT_AGENT_RUN_REQUESTED} (exec: ${options.exec})`);
|
|
1650
1780
|
// Only a server that says it understands the frame gets one; an
|
|
1651
1781
|
// older server may treat an unknown frame type as a protocol error.
|
|
1652
1782
|
// A pick made while this agent was offline: the server repeats it on
|
|
@@ -1663,7 +1793,10 @@ export async function runAgentHarness(options) {
|
|
|
1663
1793
|
venueSessionsOffered = Array.isArray(f.features) && f.features.includes(VENUE_SESSION_FEATURE);
|
|
1664
1794
|
if (Array.isArray(f.features) && f.features.includes(RUNTIME_STATUS_FEATURE)) {
|
|
1665
1795
|
primeStatus();
|
|
1666
|
-
|
|
1796
|
+
// A server that takes the opened folders whole (64 KiB frames) and early when they change.
|
|
1797
|
+
status.attach((frame) => ws.send(JSON.stringify(frame)), {
|
|
1798
|
+
codeHostFolders: f.features.includes(CODE_HOST_FOLDERS_FEATURE),
|
|
1799
|
+
});
|
|
1667
1800
|
}
|
|
1668
1801
|
return;
|
|
1669
1802
|
}
|
|
@@ -1694,9 +1827,9 @@ export async function runAgentHarness(options) {
|
|
|
1694
1827
|
if (!isWebhookEvent(frame))
|
|
1695
1828
|
return;
|
|
1696
1829
|
metrics.eventsReceived += 1;
|
|
1697
|
-
const ack = (status) => {
|
|
1830
|
+
const ack = (status, reason) => {
|
|
1698
1831
|
try {
|
|
1699
|
-
ws.send(JSON.stringify({ type: 'ack', delivery_id: frame.delivery_id, status }));
|
|
1832
|
+
ws.send(JSON.stringify({ type: 'ack', delivery_id: frame.delivery_id, status, ...(reason ? { reason } : {}) }));
|
|
1700
1833
|
}
|
|
1701
1834
|
catch {
|
|
1702
1835
|
/* best-effort */
|
|
@@ -1717,6 +1850,72 @@ export async function runAgentHarness(options) {
|
|
|
1717
1850
|
ack(applied ? ACK_ACCEPTED : ACK_IGNORED);
|
|
1718
1851
|
return;
|
|
1719
1852
|
}
|
|
1853
|
+
if (frame.event === EVENT_CODE_THREAD_START_REQUESTED ||
|
|
1854
|
+
frame.event === EVENT_CODE_THREAD_INPUT_REQUESTED ||
|
|
1855
|
+
frame.event === EVENT_CODE_THREAD_APPROVAL_DECIDED) {
|
|
1856
|
+
if (!codeHost) {
|
|
1857
|
+
if (!codeHostIgnoredLogged) {
|
|
1858
|
+
codeHostIgnoredLogged = true;
|
|
1859
|
+
log(options.dryRun
|
|
1860
|
+
? `${timestamp()} ◦ ${frame.event} ignored — [dry-run] Code-thread actions are disabled`
|
|
1861
|
+
: `${timestamp()} ◦ ${frame.event} ignored — this machine does not host Code threads ` +
|
|
1862
|
+
'(prohost agent code-host enable)');
|
|
1863
|
+
}
|
|
1864
|
+
metrics.ignored += 1;
|
|
1865
|
+
ack(ACK_IGNORED);
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
// It starts processes on this machine, so it is held to the run's signature rule.
|
|
1869
|
+
if (!verifySignature(options.credentials.webhook_secret, frame.payload, frame.signature) &&
|
|
1870
|
+
!options.allowUnverified) {
|
|
1871
|
+
metrics.signatureRejections += 1;
|
|
1872
|
+
log(`${timestamp()} ✗ rejected ${frame.event} — signature did not verify`);
|
|
1873
|
+
ack(ACK_REJECTED);
|
|
1874
|
+
return;
|
|
1875
|
+
}
|
|
1876
|
+
const data = eventData(frame.payload);
|
|
1877
|
+
if (!codeHost.admits(frame.event, data)) {
|
|
1878
|
+
log(`${timestamp()} ◦ ${frame.event} refused — it needs the remote terminal on ` +
|
|
1879
|
+
'(prohost agent code-host remote on)');
|
|
1880
|
+
metrics.ignored += 1;
|
|
1881
|
+
// A reply or decision refused only for the switch: 409 + why, so the server hands it
|
|
1882
|
+
// back on the thread rather than count it delivered.
|
|
1883
|
+
const refusal = codeHost.refusal(frame.event);
|
|
1884
|
+
if (refusal)
|
|
1885
|
+
ack(ACK_REFUSED, refusal);
|
|
1886
|
+
else
|
|
1887
|
+
ack(ACK_IGNORED);
|
|
1888
|
+
return;
|
|
1889
|
+
}
|
|
1890
|
+
const starting = frame.event === EVENT_CODE_THREAD_START_REQUESTED;
|
|
1891
|
+
const handled = starting
|
|
1892
|
+
? codeHost.handleStart(data)
|
|
1893
|
+
: frame.event === EVENT_CODE_THREAD_INPUT_REQUESTED
|
|
1894
|
+
? codeHost.handleInput(data, frame.delivery_id)
|
|
1895
|
+
: codeHost.handleApproval(data, frame.delivery_id);
|
|
1896
|
+
// Acknowledged once handled, never before: a start acked and then
|
|
1897
|
+
// lost to a crash or a shutdown would leave its thread starting
|
|
1898
|
+
// forever. A start that ran — or failed, which it reports — is 200;
|
|
1899
|
+
// one that threw is retried, and the redelivery finds any window it
|
|
1900
|
+
// made (or joins the start still in flight). Input and approvals are
|
|
1901
|
+
// 200 when they reached a pane here, 409 + reason when refused before
|
|
1902
|
+
// typing anything (the server hands them back), else 204.
|
|
1903
|
+
background.push(handled
|
|
1904
|
+
.then((ok) => {
|
|
1905
|
+
ack(starting || ok ? ACK_ACCEPTED : ACK_IGNORED);
|
|
1906
|
+
})
|
|
1907
|
+
.catch((err) => {
|
|
1908
|
+
if (err instanceof RefusedDeliveryError) {
|
|
1909
|
+
log(`${timestamp()} ◦ ${frame.event} refused before typing anything: ${err.reason}`);
|
|
1910
|
+
metrics.ignored += 1;
|
|
1911
|
+
ack(ACK_REFUSED, err.reason);
|
|
1912
|
+
return;
|
|
1913
|
+
}
|
|
1914
|
+
log(`${timestamp()} ✗ ${frame.event} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
1915
|
+
ack(ACK_RETRY);
|
|
1916
|
+
}));
|
|
1917
|
+
return;
|
|
1918
|
+
}
|
|
1720
1919
|
if (frame.event !== EVENT_AGENT_RUN_REQUESTED) {
|
|
1721
1920
|
if (frame.event === EVENT_MENTION_CREATED || frame.event === EVENT_MESSAGE_TEAM_CHAT) {
|
|
1722
1921
|
// "runs are triggered by mentions" was wrong twice over: the gate
|
|
@@ -1761,6 +1960,28 @@ export async function runAgentHarness(options) {
|
|
|
1761
1960
|
handledThisSession.add(run.run_id);
|
|
1762
1961
|
/** Take the run on: track it, heartbeat it, queue it. */
|
|
1763
1962
|
const accept = (run) => {
|
|
1963
|
+
if (options.hostOnly && !previous) {
|
|
1964
|
+
// Never executed: closed out as failed at once, through the run's own completion
|
|
1965
|
+
// path, so the person who mentioned this machine hears why instead of waiting.
|
|
1966
|
+
metrics.runsFailed += 1;
|
|
1967
|
+
if (options.dryRun) {
|
|
1968
|
+
log(`${timestamp()} [dry-run] would refuse run ${run.run_id.slice(0, 12)} — this harness only hosts Code threads`);
|
|
1969
|
+
return;
|
|
1970
|
+
}
|
|
1971
|
+
log(`${timestamp()} ⊘ run ${run.run_id.slice(0, 12)} refused — this harness only hosts Code threads`);
|
|
1972
|
+
background.push(completeWithRetry(apiForRun(api, run), run.completion_path, { ok: false, error: HOST_ONLY_REFUSAL }, log, sleep)
|
|
1973
|
+
.then((closed) => {
|
|
1974
|
+
if (closed.ok)
|
|
1975
|
+
runs.markCompleted(run.run_id);
|
|
1976
|
+
else
|
|
1977
|
+
handledThisSession.delete(run.run_id);
|
|
1978
|
+
})
|
|
1979
|
+
.catch((err) => {
|
|
1980
|
+
handledThisSession.delete(run.run_id);
|
|
1981
|
+
log(`${timestamp()} ✗ refusing run ${run.run_id.slice(0, 12)} failed: ${String(err)}`);
|
|
1982
|
+
}));
|
|
1983
|
+
return;
|
|
1984
|
+
}
|
|
1764
1985
|
if (previous) {
|
|
1765
1986
|
// We ran this one before but never got a terminal state accepted —
|
|
1766
1987
|
// most likely the process died between the command and the
|
|
@@ -36,6 +36,34 @@ export declare const STATUS_INTERVAL_MS: number;
|
|
|
36
36
|
* the frame that carried a change.
|
|
37
37
|
*/
|
|
38
38
|
export declare const MIN_FRAME_SPACING_MS = 31000;
|
|
39
|
+
/**
|
|
40
|
+
* Inside that window the server still accepts a frame whose capabilities
|
|
41
|
+
* changed, one per 5 s (`MIN_CAPABILITY_CHANGE_INTERVAL_SECONDS`), and a strict
|
|
42
|
+
* narrowing (a capability dropped, nothing added) one per second
|
|
43
|
+
* (`MIN_CAPABILITY_NARROWING_INTERVAL_SECONDS`). A capability can be a switch
|
|
44
|
+
* the server enforces (`remote_terminal`). Margin on both.
|
|
45
|
+
*/
|
|
46
|
+
export declare const MIN_CAPABILITY_CHANGE_SPACING_MS = 6000;
|
|
47
|
+
export declare const MIN_CAPABILITY_NARROWING_SPACING_MS = 1500;
|
|
48
|
+
/**
|
|
49
|
+
* The server drops a status frame over 8 KB (`MAX_FRAME_BYTES`). The opened
|
|
50
|
+
* folders are the one part of it that can grow that far, so they are cut,
|
|
51
|
+
* oldest first, until the frame fits under this, with margin.
|
|
52
|
+
*/
|
|
53
|
+
export declare const MAX_STATUS_FRAME_BYTES = 8000;
|
|
54
|
+
/**
|
|
55
|
+
* The budget once the server lists {@link CODE_HOST_FOLDERS_FEATURE} in `auth_ok.features`: it
|
|
56
|
+
* takes frames up to 64 KiB, so all 50 folders fit. Margin under the cap.
|
|
57
|
+
*/
|
|
58
|
+
export declare const MAX_LARGE_STATUS_FRAME_BYTES: number;
|
|
59
|
+
/**
|
|
60
|
+
* A server listing this feature takes a status frame up to 64 KiB, and one whose
|
|
61
|
+
* `code_host_repos` / `code_host_folders` / `code_host_home` changed inside its spacing window
|
|
62
|
+
* (a 5 s floor) — the opened folders reach phones within seconds of `folders add`.
|
|
63
|
+
*/
|
|
64
|
+
export declare const CODE_HOST_FOLDERS_FEATURE = "code_host_folders";
|
|
65
|
+
/** The early code-host frame's spacing: the server's 5 s floor, with margin. */
|
|
66
|
+
export declare const MIN_CODE_HOST_CHANGE_SPACING_MS = 6000;
|
|
39
67
|
/** Ask the agent CLI for fresh numbers when the last ones are this old. */
|
|
40
68
|
export declare const QUOTA_REFRESH_AFTER_MS: number;
|
|
41
69
|
/** Ceiling on one idle quota probe. */
|
|
@@ -80,8 +108,10 @@ export declare function fromClaudeRateLimitInfo(info: unknown): QuotaWindow[];
|
|
|
80
108
|
* Windows from a `get_usage` control response (0–100, ISO strings).
|
|
81
109
|
*
|
|
82
110
|
* A window with a usage but a `null` reset is kept, with `resets_at: null` —
|
|
83
|
-
* that is how an idle account's 5-hour window reads
|
|
84
|
-
*
|
|
111
|
+
* that is how an idle account's 5-hour window reads. Reporting stays complete:
|
|
112
|
+
* dropping it here, with the list replaced whole by each source, erased a known
|
|
113
|
+
* 5-hour window (B3). Whether to show an idle one is the server's call (it
|
|
114
|
+
* doesn't: `shared.ai_usage.windows.visible_windows`).
|
|
85
115
|
*
|
|
86
116
|
* Per-model weekly windows come from `rate_limits.limits[]` rows of kind
|
|
87
117
|
* `weekly_scoped` (classified on `kind`, as the schema asks, never on a
|
|
@@ -181,9 +211,16 @@ export interface AccountsSnapshot {
|
|
|
181
211
|
*/
|
|
182
212
|
all?: AccountBlock[];
|
|
183
213
|
}
|
|
214
|
+
/** This machine's hostname up to its first dot (`studio-001.local` is `studio-001`), or `undefined`. */
|
|
215
|
+
export declare function shortHostname(raw?: string): string | undefined;
|
|
184
216
|
export interface RuntimeStatusReporterOptions {
|
|
185
217
|
/** `claude_code` | `codex` | `goose` | `custom`. */
|
|
186
218
|
runtime: string;
|
|
219
|
+
/**
|
|
220
|
+
* The machine's short hostname, for ProhostAI staff reading a run's details. Defaults to
|
|
221
|
+
* {@link shortHostname}; `agent run` passes its `--machine` name when one was given.
|
|
222
|
+
*/
|
|
223
|
+
hostname?: () => string | undefined;
|
|
187
224
|
/** Idle quota source; omit for a runtime that has none. */
|
|
188
225
|
provider?: QuotaProvider;
|
|
189
226
|
/**
|
|
@@ -212,6 +249,29 @@ export interface RuntimeStatusReporterOptions {
|
|
|
212
249
|
* frame, so a downgraded harness stops declaring what it no longer does.
|
|
213
250
|
*/
|
|
214
251
|
capabilities?: () => string[];
|
|
252
|
+
/**
|
|
253
|
+
* This machine's load (`codehost/host_stats.ts`), read for every frame. Only
|
|
254
|
+
* set while Code-thread hosting is on — the owner's opt-in — so a harness
|
|
255
|
+
* that doesn't host threads sends exactly the frame it always did.
|
|
256
|
+
*/
|
|
257
|
+
host?: () => Record<string, unknown> | undefined;
|
|
258
|
+
/**
|
|
259
|
+
* The repositories this harness offers Code threads (`codehost/checkouts.ts`),
|
|
260
|
+
* read for every frame. Only set while it hosts them; `undefined` sends none.
|
|
261
|
+
*/
|
|
262
|
+
codeHostRepos?: () => string[] | undefined;
|
|
263
|
+
/**
|
|
264
|
+
* The folders this harness has opened for Code threads and its home directory
|
|
265
|
+
* (`codehost/folders.ts`), read for every frame as `code_host_folders` /
|
|
266
|
+
* `code_host_home`. Only set while it hosts them; `undefined` sends neither.
|
|
267
|
+
*/
|
|
268
|
+
codeHostFolders?: () => {
|
|
269
|
+
folders: {
|
|
270
|
+
path: string;
|
|
271
|
+
git: boolean;
|
|
272
|
+
}[];
|
|
273
|
+
home: string;
|
|
274
|
+
} | undefined;
|
|
215
275
|
now?: () => number;
|
|
216
276
|
/** Timer seam for tests. Returns a cancel function. */
|
|
217
277
|
schedule?: (fn: () => void, ms: number) => () => void;
|
|
@@ -235,6 +295,16 @@ export declare class RuntimeStatusReporter {
|
|
|
235
295
|
private quotaObservedAt;
|
|
236
296
|
private send;
|
|
237
297
|
private lastSentAt;
|
|
298
|
+
/** The capabilities the last frame carried, and when a changed set last went out at once. */
|
|
299
|
+
private lastSentCapabilities;
|
|
300
|
+
private lastCapabilityChangeAt;
|
|
301
|
+
private lastCapabilityNarrowingAt;
|
|
302
|
+
/** The code-host fields the last frame carried, and when a changed set last went out early. */
|
|
303
|
+
private lastSentCodeHost;
|
|
304
|
+
private lastCodeHostChangeAt;
|
|
305
|
+
private cancelCodeHostPending;
|
|
306
|
+
/** The server lists {@link CODE_HOST_FOLDERS_FEATURE}: 64 KiB frames, early code-host frames. */
|
|
307
|
+
private codeHostServer;
|
|
238
308
|
private cancelPending;
|
|
239
309
|
private cancelTick;
|
|
240
310
|
private refreshing;
|
|
@@ -253,6 +323,13 @@ export declare class RuntimeStatusReporter {
|
|
|
253
323
|
setAccounts(snapshot: AccountsSnapshot | undefined, options?: {
|
|
254
324
|
quiet?: boolean;
|
|
255
325
|
}): void;
|
|
326
|
+
/**
|
|
327
|
+
* What the harness declares changed (the remote-terminal switch): sent at
|
|
328
|
+
* once, since the server lets a changed capability set through its spacing
|
|
329
|
+
* window and stops sending what this machine now refuses; and again in the
|
|
330
|
+
* next spaced slot, for a server that predates that.
|
|
331
|
+
*/
|
|
332
|
+
capabilitiesChanged(): void;
|
|
256
333
|
/**
|
|
257
334
|
* The account choice changed (an operator picked one in ProhostAI): re-read
|
|
258
335
|
* accounts and capacity on the next tick rather than in fifteen minutes.
|
|
@@ -261,11 +338,28 @@ export declare class RuntimeStatusReporter {
|
|
|
261
338
|
/** The model to report on a run's completion, when nothing more specific is known. */
|
|
262
339
|
model(): string | undefined;
|
|
263
340
|
/** Start reporting on a socket that advertised the feature. */
|
|
264
|
-
attach(send: (frame: Record<string, unknown>) => void
|
|
341
|
+
attach(send: (frame: Record<string, unknown>) => void, server?: {
|
|
342
|
+
codeHostFolders?: boolean;
|
|
343
|
+
}): void;
|
|
344
|
+
/**
|
|
345
|
+
* What this Mac offers Code threads changed (its opened folders, say, after `folders add`). A
|
|
346
|
+
* server listing {@link CODE_HOST_FOLDERS_FEATURE} takes such a frame early, at most every
|
|
347
|
+
* {@link MIN_CODE_HOST_CHANGE_SPACING_MS}; an older one gets it in the next spaced slot.
|
|
348
|
+
*/
|
|
349
|
+
codeHostChanged(): void;
|
|
350
|
+
private codeHostKey;
|
|
265
351
|
/** Stop reporting — the socket is gone. */
|
|
266
352
|
detach(): void;
|
|
267
353
|
/** The frame as it would be sent now. */
|
|
268
354
|
frame(): Record<string, unknown>;
|
|
355
|
+
/**
|
|
356
|
+
* `frame` with the opened folders, as many as fit (newest kept): under {@link MAX_STATUS_FRAME_BYTES},
|
|
357
|
+
* or {@link MAX_LARGE_STATUS_FRAME_BYTES} for a server that takes large frames.
|
|
358
|
+
*/
|
|
359
|
+
private withCodeHostFolders;
|
|
360
|
+
private codeHostReposField;
|
|
361
|
+
private hostnameField;
|
|
362
|
+
private hostField;
|
|
269
363
|
/**
|
|
270
364
|
* Whether the machine's registry no longer holds exactly the accounts the
|
|
271
365
|
* snapshot lists: another agent home started on a login of its own, or an
|
|
@@ -278,6 +372,8 @@ export declare class RuntimeStatusReporter {
|
|
|
278
372
|
private refresh;
|
|
279
373
|
/** Send now if spacing allows, else once it does. Coalesces. */
|
|
280
374
|
private sendSoon;
|
|
375
|
+
private capabilitiesList;
|
|
376
|
+
private capabilitiesKey;
|
|
281
377
|
private flush;
|
|
282
378
|
}
|
|
283
379
|
export {};
|