@prohost/cli 0.16.2 → 0.16.3
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 +19 -0
- package/dist/agent/api.d.ts +4 -3
- package/dist/agent/run.d.ts +5 -6
- package/dist/agent/run.js +84 -21
- package/dist/codehost/settings.d.ts +16 -3
- package/dist/codehost/settings.js +31 -8
- package/dist/codehost/terminals.js +0 -10
- package/dist/host/service.d.ts +3 -0
- package/dist/host/service.js +15 -8
- package/dist/version.d.ts +2 -2
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,25 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.3
|
|
10
|
+
|
|
11
|
+
- **A dedicated service records its terminal switch in its home's code-host settings** as it
|
|
12
|
+
starts (`--remote-terminal` / `--no-remote-terminal`), instead of an environment variable a
|
|
13
|
+
surviving tmux window would keep: after a reinstall with `--no-remote-terminal`, a Claude window
|
|
14
|
+
still open from before stops advertising remote approvals nobody can give, and answers its
|
|
15
|
+
permission prompts in the terminal. It fails closed: `--no-remote-terminal` never hosts (the
|
|
16
|
+
definition decides in the service, whatever the file says), and a switch it can't record hosts
|
|
17
|
+
nothing and removes the old file when it can, so hooks read remote control as off (the log says
|
|
18
|
+
when even that fails).
|
|
19
|
+
- **A muted thread's turn is never resumed.** When an early answer was dropped because the thread
|
|
20
|
+
is muted (`409 ai_muted`), the conversation's session is forgotten on every exit — cancelled,
|
|
21
|
+
usage-limited, closed server-side, expired or failed too — not only when the run finishes.
|
|
22
|
+
- **Replies the server holds back are handled, not failed or retried** (dispatch #22485):
|
|
23
|
+
`409 approval_required` ("waiting for approval": sent when someone approves the card — the
|
|
24
|
+
session continues), `409 send_blocked` ("blocked: <reason>", e.g. a duplicate — never delivered,
|
|
25
|
+
so the session is forgotten), and `held_as_suggestion` with any reason, `pinned_rule` included
|
|
26
|
+
(the session continues, as before). Any other 409 is still a failure. Works with today's server.
|
|
27
|
+
|
|
9
28
|
## 0.16.2
|
|
10
29
|
|
|
11
30
|
More `prohost host` follow-ups (they missed #22513):
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -31,10 +31,11 @@ export interface ApiResult {
|
|
|
31
31
|
ok: boolean;
|
|
32
32
|
/**
|
|
33
33
|
* A send the server answered without sending (still `ok`: the run is done):
|
|
34
|
-
* saved as a draft for the team (`suggestion`),
|
|
35
|
-
* is muted for AI (`muted`)
|
|
34
|
+
* saved as a draft for the team (`suggestion`), waiting on an approval card
|
|
35
|
+
* (`approval`), dropped because the thread is muted for AI (`muted`), or
|
|
36
|
+
* stopped by the outbound guard (`blocked`). See `withheldBy` in run.ts.
|
|
36
37
|
*/
|
|
37
|
-
withheld?: 'suggestion' | 'muted';
|
|
38
|
+
withheld?: 'suggestion' | 'approval' | 'muted' | 'blocked';
|
|
38
39
|
/** HTTP status, or ``0`` when no response arrived at all — see ``failure``. */
|
|
39
40
|
status: number;
|
|
40
41
|
error?: string;
|
package/dist/agent/run.d.ts
CHANGED
|
@@ -270,12 +270,11 @@ export interface AgentRunMetrics {
|
|
|
270
270
|
* nor a failure and must not be counted as either.
|
|
271
271
|
*/
|
|
272
272
|
type RunOutcome = 'succeeded' | 'failed' | 'cancelled' | 'resolved' | 'unreported' | 'skipped';
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
export declare function isHeldAsSuggestion(result: ApiResult): boolean;
|
|
273
|
+
export declare function withheldBy(result: ApiResult): {
|
|
274
|
+
kind: NonNullable<ApiResult['withheld']>;
|
|
275
|
+
reason?: string;
|
|
276
|
+
approvalRequestId?: string;
|
|
277
|
+
} | undefined;
|
|
279
278
|
/**
|
|
280
279
|
* A sign-in checker that adds `extra` only when it runs `binary` (this
|
|
281
280
|
* harness's Claude): the account snapshot checks other runtimes' logins too,
|
package/dist/agent/run.js
CHANGED
|
@@ -210,13 +210,18 @@ const REPLAY_IN_FLIGHT_DELAY_MS = 5_000;
|
|
|
210
210
|
*/
|
|
211
211
|
async function postReplyWithReplay(api, send, label, log, sleep) {
|
|
212
212
|
const result = await postReplyOnce(api, send, label, log, sleep);
|
|
213
|
-
// The
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
213
|
+
// The server decided not to send it (yet): handled, not failed, and never retried.
|
|
214
|
+
const held = withheldBy(result);
|
|
215
|
+
if (held) {
|
|
216
|
+
const why = held.reason ? `: ${held.reason}` : '';
|
|
217
|
+
log(held.kind === 'muted'
|
|
217
218
|
? `${timestamp()} ◦ ${label} reply was not sent: the thread is muted for AI`
|
|
218
|
-
:
|
|
219
|
-
|
|
219
|
+
: held.kind === 'approval'
|
|
220
|
+
? `${timestamp()} ◦ ${label} reply is waiting for approval${held.approvalRequestId ? ` (approval ${held.approvalRequestId})` : ''}: it is sent when someone approves it`
|
|
221
|
+
: held.kind === 'blocked'
|
|
222
|
+
? `${timestamp()} ◦ ${label} reply was not sent: blocked: ${held.reason ?? 'the outbound guard'}`
|
|
223
|
+
: `${timestamp()} ◦ ${label} reply was held as a suggestion for the team (the employee's send rule${why})`);
|
|
224
|
+
return { ...result, ok: true, withheld: held.kind };
|
|
220
225
|
}
|
|
221
226
|
return result;
|
|
222
227
|
}
|
|
@@ -272,12 +277,62 @@ function resolveWorkdir(options, log) {
|
|
|
272
277
|
}
|
|
273
278
|
}
|
|
274
279
|
/**
|
|
275
|
-
* A send the server answered without sending, deliberately
|
|
276
|
-
*
|
|
277
|
-
*
|
|
280
|
+
* A send the server answered without sending, deliberately (a 409). Either way the run is done,
|
|
281
|
+
* nothing is retried, and whether the conversation's session continues follows from it:
|
|
282
|
+
*
|
|
283
|
+
* - `held_as_suggestion` → `suggestion`: saved as a draft for the team (the send rule, or a
|
|
284
|
+
* `pinned_rule`). The team may send it as is: the session continues.
|
|
285
|
+
* - `approval_required` → `approval`: below the employee's threshold, it waits on an approval card
|
|
286
|
+
* and the server sends it when someone approves: the session continues.
|
|
287
|
+
* - `ai_muted` → `muted`: the thread is muted; nothing saved. The guest never sees it: the session
|
|
288
|
+
* is forgotten.
|
|
289
|
+
* - `send_blocked` → `blocked`: the shared outbound guard stopped it (e.g. a duplicate). Not
|
|
290
|
+
* delivered by this run: the session is forgotten.
|
|
291
|
+
*
|
|
292
|
+
* Any other 409 is not a decision and stays a failure.
|
|
278
293
|
*/
|
|
279
|
-
|
|
280
|
-
|
|
294
|
+
const WITHHELD_CODES = new Map([
|
|
295
|
+
['ai_muted', 'muted'],
|
|
296
|
+
['approval_required', 'approval'],
|
|
297
|
+
['send_blocked', 'blocked'],
|
|
298
|
+
['held_as_suggestion', 'suggestion'],
|
|
299
|
+
]);
|
|
300
|
+
export function withheldBy(result) {
|
|
301
|
+
if (result.status !== 409)
|
|
302
|
+
return undefined;
|
|
303
|
+
const text = result.error ?? '';
|
|
304
|
+
// The server's own code, matched exactly — never a word found elsewhere in the body (a 409
|
|
305
|
+
// like `reply_race_held` quotes another reply's text, which may name any of these):
|
|
306
|
+
// `{detail: {error, reason, approval_request_id, …}}`, the same fields at the top, or
|
|
307
|
+
// `{detail: "<code>"}`. A body cut short (`api.ts` keeps 500 chars) is read for its first
|
|
308
|
+
// `"error": "…"` field only.
|
|
309
|
+
let fields = {};
|
|
310
|
+
let code;
|
|
311
|
+
try {
|
|
312
|
+
const parsed = JSON.parse(text);
|
|
313
|
+
const top = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
314
|
+
const detail = top.detail;
|
|
315
|
+
if (typeof detail === 'string')
|
|
316
|
+
code = detail;
|
|
317
|
+
else {
|
|
318
|
+
fields = detail && typeof detail === 'object' && !Array.isArray(detail) ? detail : top;
|
|
319
|
+
code = typeof fields.error === 'string' ? fields.error : undefined;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
code = /"error"\s*:\s*"([a-z_]+)"/.exec(text)?.[1];
|
|
324
|
+
}
|
|
325
|
+
const kind = code === undefined ? undefined : WITHHELD_CODES.get(code);
|
|
326
|
+
if (!kind)
|
|
327
|
+
return undefined;
|
|
328
|
+
const str = (value) => (typeof value === 'string' && value ? value.slice(0, 120) : undefined);
|
|
329
|
+
const reason = str(fields.reason);
|
|
330
|
+
const approvalRequestId = str(fields.approval_request_id);
|
|
331
|
+
return { kind, ...(reason ? { reason } : {}), ...(approvalRequestId ? { approvalRequestId } : {}) };
|
|
332
|
+
}
|
|
333
|
+
/** A withheld reply the guest will never get from this run: its session must not continue. */
|
|
334
|
+
function undeliveredFor(withheld) {
|
|
335
|
+
return withheld === 'muted' || withheld === 'blocked';
|
|
281
336
|
}
|
|
282
337
|
/**
|
|
283
338
|
* A sign-in checker that adds `extra` only when it runs `binary` (this
|
|
@@ -1193,8 +1248,11 @@ limited) {
|
|
|
1193
1248
|
let earlyCount = 0;
|
|
1194
1249
|
let earlyPosted = 0;
|
|
1195
1250
|
let earlyPosts = Promise.resolve();
|
|
1196
|
-
/**
|
|
1197
|
-
|
|
1251
|
+
/**
|
|
1252
|
+
* An answer of this run will never reach the guest (the thread is muted, or the outbound guard
|
|
1253
|
+
* blocked it — `undeliveredFor`): its session must not continue.
|
|
1254
|
+
*/
|
|
1255
|
+
let undelivered = false;
|
|
1198
1256
|
const earlyPath = run.reply_path;
|
|
1199
1257
|
const earlyBodyKey = run.reply_body_key;
|
|
1200
1258
|
const stoppedNow = () => control.signal.aborted || control.disposition() !== undefined;
|
|
@@ -1222,8 +1280,8 @@ limited) {
|
|
|
1222
1280
|
}
|
|
1223
1281
|
early.shift();
|
|
1224
1282
|
earlyPosted += 1;
|
|
1225
|
-
if (posted.withheld
|
|
1226
|
-
|
|
1283
|
+
if (undeliveredFor(posted.withheld))
|
|
1284
|
+
undelivered = true;
|
|
1227
1285
|
log(`${timestamp()} ✓ ${label} posted an answer; a background task is still running`);
|
|
1228
1286
|
}
|
|
1229
1287
|
};
|
|
@@ -1250,15 +1308,19 @@ limited) {
|
|
|
1250
1308
|
const sessionId = venue ? undefined : result.ok ? result.sessionId : undefined;
|
|
1251
1309
|
/**
|
|
1252
1310
|
* Continue the conversation's session from this turn — unless an answer of
|
|
1253
|
-
* it
|
|
1311
|
+
* it will never reach the guest (muted thread, blocked send; any runtime): then the session
|
|
1254
1312
|
* holds a turn nobody saw, so it is forgotten, eager record or resumed one alike.
|
|
1255
1313
|
*/
|
|
1256
1314
|
const settleSession = (id) => {
|
|
1257
|
-
if (
|
|
1315
|
+
if (undelivered)
|
|
1258
1316
|
runtime.sessions.forget(sessionKeyFor(run));
|
|
1259
1317
|
else if (id)
|
|
1260
1318
|
runtime.sessions.remember(sessionKeyFor(run), id);
|
|
1261
1319
|
};
|
|
1320
|
+
// An early answer the guest will never get (muted, blocked): forgotten on every exit, the
|
|
1321
|
+
// stopped, failed and usage-limited ones below included — none may continue a turn nobody saw.
|
|
1322
|
+
if (undelivered)
|
|
1323
|
+
runtime.sessions.forget(sessionKeyFor(run));
|
|
1262
1324
|
// Whatever the turn did, the timeline is whole BEFORE the reply is posted or
|
|
1263
1325
|
// the run is closed — a completion that beats its own last tool row would
|
|
1264
1326
|
// leave a `calling` row on a finished run. Never rejects, and after `stop`
|
|
@@ -1423,10 +1485,11 @@ limited) {
|
|
|
1423
1485
|
}
|
|
1424
1486
|
// Advance continuity only after the answer is durable on its reply surface.
|
|
1425
1487
|
// Remembering at process exit would strand the conversation behind an answer
|
|
1426
|
-
// it never received whenever this POST is rejected. A muted thread
|
|
1427
|
-
// nothing of it, so its session is not advanced either (and an eager record undone)
|
|
1428
|
-
|
|
1429
|
-
|
|
1488
|
+
// it never received whenever this POST is rejected. A muted thread or a blocked send delivers
|
|
1489
|
+
// nothing of it, so its session is not advanced either (and an eager record undone); a
|
|
1490
|
+
// suggestion or a reply waiting for approval may still be sent as written, so it continues.
|
|
1491
|
+
if (undeliveredFor(posted.withheld))
|
|
1492
|
+
undelivered = true;
|
|
1430
1493
|
settleSession(sessionId);
|
|
1431
1494
|
const completed = await completeWithRetry(api, run.completion_path, { ok: true }, log, sleep, runtime.model());
|
|
1432
1495
|
if (!completed.ok) {
|
|
@@ -26,9 +26,10 @@ export interface CodeHostSettings {
|
|
|
26
26
|
export declare const DEFAULT_SETTINGS: CodeHostSettings;
|
|
27
27
|
/**
|
|
28
28
|
* A dedicated `prohost host` service decides terminal sessions in its own definition
|
|
29
|
-
* (`host install --dedicated --remote-terminal | --no-remote-terminal`)
|
|
30
|
-
*
|
|
31
|
-
* the relay and the permission hook
|
|
29
|
+
* (`host install --dedicated --remote-terminal | --no-remote-terminal`): `1` hosts with remote
|
|
30
|
+
* control on, `0` hosts nothing. The service writes it into its home's `settings.json` as it
|
|
31
|
+
* starts, so every reader — the daemon, the relay and the permission hook of a Claude window
|
|
32
|
+
* that outlived a reinstall — reads the one file.
|
|
32
33
|
*/
|
|
33
34
|
export declare const TERMINAL_SESSIONS_ENV = "PROHOST_HOST_TERMINAL_SESSIONS";
|
|
34
35
|
/** `$PROHOST_HOME/code-host`. */
|
|
@@ -57,3 +58,15 @@ export declare function readJsonObject(file: string): Record<string, unknown> |
|
|
|
57
58
|
*/
|
|
58
59
|
export declare function loadSettings(env?: NodeJS.ProcessEnv): CodeHostSettings;
|
|
59
60
|
export declare function saveSettings(settings: CodeHostSettings, env?: NodeJS.ProcessEnv): void;
|
|
61
|
+
/**
|
|
62
|
+
* A dedicated service's terminal switch (`TERMINAL_SESSIONS_ENV`), recorded in the home's
|
|
63
|
+
* `settings.json` as it starts — for the readers outside the service's process: the permission
|
|
64
|
+
* hook of each window, which may have outlived a reinstall. True only when it is on AND recorded.
|
|
65
|
+
* Fails closed: a switch it can't record hosts nothing, and the old file is removed when it can be
|
|
66
|
+
* (a missing or unreadable file is off to every reader). When even that fails — `code-host/`
|
|
67
|
+
* itself unwritable — the error says so: windows still open keep reading the old switches.
|
|
68
|
+
*/
|
|
69
|
+
export declare function recordServiceSwitch(on: boolean, env: NodeJS.ProcessEnv, save?: (settings: CodeHostSettings, env: NodeJS.ProcessEnv) => void): {
|
|
70
|
+
hosting: boolean;
|
|
71
|
+
error?: string;
|
|
72
|
+
};
|
|
@@ -29,9 +29,10 @@ export const DEFAULT_SETTINGS = Object.freeze({
|
|
|
29
29
|
});
|
|
30
30
|
/**
|
|
31
31
|
* A dedicated `prohost host` service decides terminal sessions in its own definition
|
|
32
|
-
* (`host install --dedicated --remote-terminal | --no-remote-terminal`)
|
|
33
|
-
*
|
|
34
|
-
* the relay and the permission hook
|
|
32
|
+
* (`host install --dedicated --remote-terminal | --no-remote-terminal`): `1` hosts with remote
|
|
33
|
+
* control on, `0` hosts nothing. The service writes it into its home's `settings.json` as it
|
|
34
|
+
* starts, so every reader — the daemon, the relay and the permission hook of a Claude window
|
|
35
|
+
* that outlived a reinstall — reads the one file.
|
|
35
36
|
*/
|
|
36
37
|
export const TERMINAL_SESSIONS_ENV = 'PROHOST_HOST_TERMINAL_SESSIONS';
|
|
37
38
|
/** `$PROHOST_HOME/code-host`. */
|
|
@@ -101,11 +102,6 @@ export function readJsonObject(file) {
|
|
|
101
102
|
* is the default — both off — and only a literal `true` turns a switch on.
|
|
102
103
|
*/
|
|
103
104
|
export function loadSettings(env = process.env) {
|
|
104
|
-
const decided = env[TERMINAL_SESSIONS_ENV];
|
|
105
|
-
if (decided === '1')
|
|
106
|
-
return { version: 1, enabled: true, allowRemoteTerminal: true };
|
|
107
|
-
if (decided === '0')
|
|
108
|
-
return { ...DEFAULT_SETTINGS };
|
|
109
105
|
const raw = readJsonObject(settingsPath(env));
|
|
110
106
|
if (!raw || raw.version !== 1)
|
|
111
107
|
return { ...DEFAULT_SETTINGS };
|
|
@@ -124,3 +120,30 @@ export function saveSettings(settings, env = process.env) {
|
|
|
124
120
|
allowRemoteTerminal: settings.enabled && settings.allowRemoteTerminal,
|
|
125
121
|
});
|
|
126
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* A dedicated service's terminal switch (`TERMINAL_SESSIONS_ENV`), recorded in the home's
|
|
125
|
+
* `settings.json` as it starts — for the readers outside the service's process: the permission
|
|
126
|
+
* hook of each window, which may have outlived a reinstall. True only when it is on AND recorded.
|
|
127
|
+
* Fails closed: a switch it can't record hosts nothing, and the old file is removed when it can be
|
|
128
|
+
* (a missing or unreadable file is off to every reader). When even that fails — `code-host/`
|
|
129
|
+
* itself unwritable — the error says so: windows still open keep reading the old switches.
|
|
130
|
+
*/
|
|
131
|
+
export function recordServiceSwitch(on, env, save = saveSettings) {
|
|
132
|
+
try {
|
|
133
|
+
save({ version: 1, enabled: on, allowRemoteTerminal: on }, env);
|
|
134
|
+
return { hosting: on && loadSettings(env).allowRemoteTerminal };
|
|
135
|
+
}
|
|
136
|
+
catch (e) {
|
|
137
|
+
const error = e instanceof Error ? e.message : String(e);
|
|
138
|
+
try {
|
|
139
|
+
rmSync(settingsPath(env), { force: true });
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return {
|
|
143
|
+
hosting: false,
|
|
144
|
+
error: `${error}; the old switches could not be removed either, so windows still open may wait for remote approvals until ${settingsPath(env)} is writable`,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
return { hosting: false, error };
|
|
148
|
+
}
|
|
149
|
+
}
|
|
@@ -33,7 +33,6 @@ import { readHistory } from './history.js';
|
|
|
33
33
|
import { CODE_THREAD_ID_ENV, TERMINAL_ID_ENV } from './hooks.js';
|
|
34
34
|
import { MAX_OPEN_PTYS } from './pty.js';
|
|
35
35
|
import { claudeSessionSince, isReady, markReady } from './ready.js';
|
|
36
|
-
import { TERMINAL_SESSIONS_ENV } from './settings.js';
|
|
37
36
|
import { defaultSizeArgs, HOLD_OPTION, holdChannel, holdCommand, resizeWindowArgs, TerminalSizing } from './sizing.js';
|
|
38
37
|
import { noteInputTakeover } from './sizing.js';
|
|
39
38
|
import { attachArgs, baseArgs, clampDim, DEFAULT_COLS, DEFAULT_ROWS, hasSessionArgs, isCopyModeExitKey, isHumanInput, isTypedText, isInterruptInput, isScrollGesture, leaveCopyModeArgs, mayEnterCopyMode, paneInModeArgs, isValidId, killSessionArgs, listWindowsArgs, newSessionArgs, newWindowArgs, parseListWindows, sendTextCommands, setWindowMetaArgs, TERMINAL_WINDOWS, DAEMON_ONLY_ENV, paneEnv, viewSessionName, windowName, windowTarget, } from './tmux.js';
|
|
@@ -170,14 +169,6 @@ const CLAUDE_THREAD_ENV = { CLAUDE_CODE_ENABLE_CFC: '0' };
|
|
|
170
169
|
* and the like), then the thread's overrides, which a login's environment never undoes. The one
|
|
171
170
|
* builder for it: `startOnce` launches with it, and `adoptReadiness` reads the config dir from it.
|
|
172
171
|
*/
|
|
173
|
-
/**
|
|
174
|
-
* A dedicated service's terminal switch (`TERMINAL_SESSIONS_ENV`), handed to each window so its
|
|
175
|
-
* permission hook reads the same switches as the daemon whatever the tmux server inherited.
|
|
176
|
-
*/
|
|
177
|
-
function terminalSessionsEnv(env) {
|
|
178
|
-
const decided = env[TERMINAL_SESSIONS_ENV];
|
|
179
|
-
return decided === undefined ? {} : { [TERMINAL_SESSIONS_ENV]: decided };
|
|
180
|
-
}
|
|
181
172
|
function agentWindowEnv(agent, accountEnv) {
|
|
182
173
|
return { ...stringEnv(accountEnv), ...(agent === 'claude_code' ? CLAUDE_THREAD_ENV : {}) };
|
|
183
174
|
}
|
|
@@ -489,7 +480,6 @@ export class CodeTerminals {
|
|
|
489
480
|
cwd: checkout.path,
|
|
490
481
|
env: {
|
|
491
482
|
...agentWindowEnv(request.agent, account.env),
|
|
492
|
-
...terminalSessionsEnv(this.options.processEnv ?? process.env),
|
|
493
483
|
PROHOST_HOME: this.options.home,
|
|
494
484
|
[TERMINAL_ID_ENV]: request.terminalId,
|
|
495
485
|
[CODE_THREAD_ID_ENV]: request.codeThreadId,
|
package/dist/host/service.d.ts
CHANGED
|
@@ -30,6 +30,7 @@ import { executeHostedRun } from '../agent/run.js';
|
|
|
30
30
|
import type { AgentRunOptions } from '../agent/run.js';
|
|
31
31
|
import type { CodeHostLike } from '../codehost/host.js';
|
|
32
32
|
import type { HostStats } from '../codehost/host_stats.js';
|
|
33
|
+
import { recordServiceSwitch } from '../codehost/settings.js';
|
|
33
34
|
import type { Workload } from './capacity.js';
|
|
34
35
|
import type { ComputerCredentials } from './computer.js';
|
|
35
36
|
import type { LoginInfo } from './logins.js';
|
|
@@ -92,6 +93,8 @@ export interface HostServiceOptions {
|
|
|
92
93
|
peak?: PeakMemory;
|
|
93
94
|
detectLogins?: () => Promise<LoginInfo[]>;
|
|
94
95
|
codeHost?: CodeHostLike | null;
|
|
96
|
+
/** Records a dedicated service's terminal switch in the home (tests: one that fails). */
|
|
97
|
+
recordTerminalSwitch?: typeof recordServiceSwitch;
|
|
95
98
|
executeRun?: typeof executeHostedRun;
|
|
96
99
|
updateAvailable?: () => boolean;
|
|
97
100
|
statusIntervalMs?: number;
|
package/dist/host/service.js
CHANGED
|
@@ -40,7 +40,7 @@ import { SessionStore, sessionKeyFor } from '../agent/sessionstore.js';
|
|
|
40
40
|
import { verifySignature } from '../agent/signature.js';
|
|
41
41
|
import { CODE_THREAD_FOLDERS_CAPABILITY, CODE_THREAD_SKIP_PERMISSIONS_CAPABILITY, CODE_THREADS_CAPABILITY, CodeHost, EVENT_CODE_THREAD_APPROVAL_DECIDED, EVENT_CODE_THREAD_INPUT_REQUESTED, EVENT_CODE_THREAD_START_REQUESTED, REMOTE_TERMINAL_CAPABILITY, RefusedDeliveryError, } from '../codehost/host.js';
|
|
42
42
|
import { HostStatsSampler } from '../codehost/host_stats.js';
|
|
43
|
-
import { loadSettings as loadCodeHostSettings } from '../codehost/settings.js';
|
|
43
|
+
import { TERMINAL_SESSIONS_ENV, loadSettings as loadCodeHostSettings, recordServiceSwitch } from '../codehost/settings.js';
|
|
44
44
|
import { isWebhookEvent } from '../forward.js';
|
|
45
45
|
import { ServerLiveness, enableTcpKeepAlive, terminateSocket } from '../liveness.js';
|
|
46
46
|
import { CLI_VERSION, USER_AGENT } from '../version.js';
|
|
@@ -186,13 +186,20 @@ async function serveHost(options) {
|
|
|
186
186
|
? { ...process.env, ...env, PROHOST_HOME: computer.code_host_home }
|
|
187
187
|
: { ...process.env, ...env };
|
|
188
188
|
// A dedicated install decides terminal sessions in the service's own definition
|
|
189
|
-
// (`--remote-terminal` / `--no-remote-terminal
|
|
190
|
-
//
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
189
|
+
// (`--remote-terminal` / `--no-remote-terminal`): written into the home's switches as it starts,
|
|
190
|
+
// the one file the daemon, the relay and every window's permission hook read (a window that
|
|
191
|
+
// outlived a reinstall too). A personal service follows the home's switches as the owner set them.
|
|
192
|
+
// The definition is authoritative in this process: off never hosts, whatever the file says or
|
|
193
|
+
// whether it could be written; on hosts only once recorded (`recordServiceSwitch` fails closed).
|
|
194
|
+
const decided = env[TERMINAL_SESSIONS_ENV] === '1' ? true : env[TERMINAL_SESSIONS_ENV] === '0' ? false : undefined;
|
|
195
|
+
let hosting = decided ?? loadCodeHostSettings(codeHostEnv).enabled;
|
|
196
|
+
if (decided !== undefined) {
|
|
197
|
+
const recorded = (options.recordTerminalSwitch ?? recordServiceSwitch)(decided, codeHostEnv);
|
|
198
|
+
hosting = decided && recorded.hosting;
|
|
199
|
+
if (recorded.error)
|
|
200
|
+
log(`terminal sessions: off — could not record the service's switch (${recorded.error})`);
|
|
201
|
+
}
|
|
202
|
+
const codeHost = options.codeHost !== undefined ? (options.codeHost ?? undefined) : hosting ? buildCodeHost() : undefined;
|
|
196
203
|
function buildCodeHost() {
|
|
197
204
|
try {
|
|
198
205
|
return new CodeHost({
|
package/dist/version.d.ts
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export declare const CLI_VERSION = "0.16.
|
|
10
|
+
export declare const CLI_VERSION = "0.16.3";
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
|
-
export declare const USER_AGENT = "prohost-cli/0.16.
|
|
12
|
+
export declare const USER_AGENT = "prohost-cli/0.16.3";
|
package/dist/version.js
CHANGED
|
@@ -7,6 +7,6 @@
|
|
|
7
7
|
* `package.json`, so a release bump that forgets this file fails the suite
|
|
8
8
|
* instead of shipping a `User-Agent` that lies about which build is calling.
|
|
9
9
|
*/
|
|
10
|
-
export const CLI_VERSION = '0.16.
|
|
10
|
+
export const CLI_VERSION = '0.16.3';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|