@prohost/cli 0.16.0 → 0.16.2
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 +36 -0
- package/README.md +13 -6
- package/dist/agent/api.d.ts +6 -0
- package/dist/agent/prompt.js +1 -1
- package/dist/agent/run.d.ts +9 -0
- package/dist/agent/run.js +46 -14
- package/dist/codehost/checkouts.js +1 -1
- package/dist/codehost/settings.d.ts +7 -0
- package/dist/codehost/settings.js +12 -0
- package/dist/codehost/terminals.js +10 -0
- package/dist/host/command.d.ts +8 -0
- package/dist/host/command.js +70 -27
- package/dist/host/install.d.ts +8 -0
- package/dist/host/install.js +46 -13
- package/dist/host/service.js +4 -0
- package/dist/host/status.d.ts +2 -0
- package/dist/index.js +3 -1
- 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,42 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.2
|
|
10
|
+
|
|
11
|
+
More `prohost host` follow-ups (they missed #22513):
|
|
12
|
+
|
|
13
|
+
- **Unlabelled `host pause` / `resume` / `status` act on this home only.** A dedicated service on
|
|
14
|
+
the same computer is addressed only by its `--label`: a personal sign-out (the desktop runs
|
|
15
|
+
`host pause`) never pauses it. A labelled `pause` / `resume` / `status` is about that service
|
|
16
|
+
alone: this home's pause, pairing and service never mix into it.
|
|
17
|
+
- **`host install --dedicated --remote-terminal | --no-remote-terminal`:** terminal sessions on a
|
|
18
|
+
dedicated service, off unless asked for; set in the service's definition, shown as
|
|
19
|
+
`remote_terminal` in `host status --json` and declared as the `remote_terminal` capability.
|
|
20
|
+
Every reader of the switches — the daemon, the relay and the permission hook in each Claude
|
|
21
|
+
window — sees the same setting, so remote approvals wait for an answer from ProhostAI.
|
|
22
|
+
- **`host logins add` on Windows** starts npm's `.cmd` shims through `cmd.exe /d /s /c`.
|
|
23
|
+
- **Reply classes:** only the server's (`general`, `status_update`, `pricing_action`,
|
|
24
|
+
`billing_action`, `legal_contractual`, `bug_in_our_code`); any other is sent as none, so the
|
|
25
|
+
server holds the reply.
|
|
26
|
+
|
|
27
|
+
## 0.16.1
|
|
28
|
+
|
|
29
|
+
Review follow-ups to `prohost host` that landed on its branch after #22455 merged:
|
|
30
|
+
|
|
31
|
+
- **Windows dedicated install:** the service home is `C:\ProgramData\Prohost\homes\<account>`, made in
|
|
32
|
+
one step with administrators-only access, given the computer credential (`wx`), and only then
|
|
33
|
+
opened to the account — the account can never swap a path in it while an administrator writes
|
|
34
|
+
there. An existing home is never written into. `--service-home` is refused on Windows (only that folder can
|
|
35
|
+
be prepared safely), and an install over a 0.16.0 Windows dedicated service (its home was in the
|
|
36
|
+
account's profile, under another task name) stops and says how to remove it and carry its
|
|
37
|
+
computer over (`--from-home`), rather than leaving two services on one key.
|
|
38
|
+
- **Claude token scoping:** an adopted Claude harness's sign-in checks add the dedicated install's
|
|
39
|
+
Claude token only when they run its own Claude binary; the account snapshot's Codex checks never
|
|
40
|
+
see it.
|
|
41
|
+
- **Muted threads:** an answer dropped because the thread is muted (`409 ai_muted`) — the final one
|
|
42
|
+
or an early one, Claude or Codex — makes the run forget the conversation's session, so after
|
|
43
|
+
unmuting nothing resumes from a turn nobody saw.
|
|
44
|
+
|
|
9
45
|
## 0.16.0
|
|
10
46
|
|
|
11
47
|
- **`prohost host` — one service per computer, many employees.** A computer is added once
|
package/README.md
CHANGED
|
@@ -202,9 +202,14 @@ from an administrator prompt. It:
|
|
|
202
202
|
afterwards (owner, no junction, no other principal), and the token is always a
|
|
203
203
|
new file (`wx`) in
|
|
204
204
|
`C:\ProgramData\Prohost\secrets\<label>` (the account may only read it), and
|
|
205
|
-
the account's home is
|
|
206
|
-
|
|
205
|
+
the account's service home is `C:\ProgramData\Prohost\homes\<account>`:
|
|
206
|
+
made in one step with administrators-only access, given the credential, and
|
|
207
|
+
only then opened to the account (an existing one is never written into). If your own (personal) service runs on the same computer
|
|
207
208
|
key, install says so and how to remove it;
|
|
209
|
+
- hosts terminal sessions only with `--remote-terminal` (`--no-remote-terminal`
|
|
210
|
+
or nothing: none) — decided in the service's definition, shown as
|
|
211
|
+
`remote_terminal` in `host status --json` and declared as the
|
|
212
|
+
`remote_terminal` capability only when on;
|
|
208
213
|
- logs where the service account can't redirect it: the journal on Linux,
|
|
209
214
|
`/Library/Logs/Prohost/<label>.log` (in a root-owned folder) on macOS,
|
|
210
215
|
`C:\ProgramData\Prohost\Logs` (ACL: SYSTEM, administrators, the service
|
|
@@ -213,8 +218,9 @@ from an administrator prompt. It:
|
|
|
213
218
|
emails, in a root-owned `/Library/Application Support/Prohost/hosts/<label>`,
|
|
214
219
|
`/var/lib/prohost/hosts/<label>` or `C:\ProgramData\Prohost\hosts\<label>`: the service owns only
|
|
215
220
|
`state.json`, `activity.json` and `events.json` there, and `control.json` is
|
|
216
|
-
the pause switch anyone on the computer may flip. `host status
|
|
217
|
-
`host pause
|
|
221
|
+
the pause switch anyone on the computer may flip. `host status --label <id>`
|
|
222
|
+
and `host pause|resume --label <id>` run as the logged-in user address it
|
|
223
|
+
there (unlabelled, they act on their own home only), and
|
|
218
224
|
`sudo prohost host uninstall` finds the service by it (install prints the exact
|
|
219
225
|
command with its `--label`).
|
|
220
226
|
|
|
@@ -286,8 +292,9 @@ start it again. `--dry-run` shows what it would do; `--json` prints the report.
|
|
|
286
292
|
`login_moved`, `hold_started`, `hold_ended`, with the `/events` fields plus
|
|
287
293
|
`computer_id` / `computer_name`), newest last, each with a stable `id`.
|
|
288
294
|
`service` says whether a service runs for this home (`host/service.lock`), how
|
|
289
|
-
many homes it adopted, and —
|
|
290
|
-
|
|
295
|
+
many homes it adopted, and — with `--label <id>` of a dedicated service — that
|
|
296
|
+
it is the dedicated service (`activity_file` is then its shared copy).
|
|
297
|
+
`remote_terminal` says whether remote terminal sessions are on.
|
|
291
298
|
- `host logins --json` → `{logins: [...same shape as status...]}`.
|
|
292
299
|
- `$PROHOST_HOME/host/activity.json` → `{v: 1, busy, running: [...as in status...], updatedAt}`.
|
|
293
300
|
- `$PROHOST_HOME/host/state.json` is the service's own state (0600); read
|
package/dist/agent/api.d.ts
CHANGED
|
@@ -29,6 +29,12 @@ export interface ApiOptions {
|
|
|
29
29
|
}
|
|
30
30
|
export interface ApiResult {
|
|
31
31
|
ok: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* A send the server answered without sending (still `ok`: the run is done):
|
|
34
|
+
* saved as a draft for the team (`suggestion`), or dropped because the thread
|
|
35
|
+
* is muted for AI (`muted`).
|
|
36
|
+
*/
|
|
37
|
+
withheld?: 'suggestion' | 'muted';
|
|
32
38
|
/** HTTP status, or ``0`` when no response arrived at all — see ``failure``. */
|
|
33
39
|
status: number;
|
|
34
40
|
error?: string;
|
package/dist/agent/prompt.js
CHANGED
|
@@ -178,7 +178,7 @@ export function buildPrompt(run, context) {
|
|
|
178
178
|
if (context.resumeNote)
|
|
179
179
|
lines.push(context.resumeNote, '');
|
|
180
180
|
if (context.askConfidence) {
|
|
181
|
-
lines.push('End your reply with one last line, exactly: [[confidence=<0 to 1> reply_class=<class>]] — how sure you are', 'the reply is right and safe to send as is, and its class: general,
|
|
181
|
+
lines.push('End your reply with one last line, exactly: [[confidence=<0 to 1> reply_class=<class>]] — how sure you are', 'the reply is right and safe to send as is, and its class: general, status_update, pricing_action,', 'billing_action, legal_contractual or bug_in_our_code. That line is removed before sending; below the bar, or for a class', 'that needs approval, your reply is saved as a draft for the team instead of sent.', '');
|
|
182
182
|
}
|
|
183
183
|
// The operator's own configuration comes first, above everything the server
|
|
184
184
|
// generated. It is who this agent *is* — persona, house rules, escalation
|
package/dist/agent/run.d.ts
CHANGED
|
@@ -276,6 +276,15 @@ type RunOutcome = 'succeeded' | 'failed' | 'cancelled' | 'resolved' | 'unreporte
|
|
|
276
276
|
* muted; nothing saved, nothing to retry). Either way the run is done.
|
|
277
277
|
*/
|
|
278
278
|
export declare function isHeldAsSuggestion(result: ApiResult): boolean;
|
|
279
|
+
/**
|
|
280
|
+
* A sign-in checker that adds `extra` only when it runs `binary` (this
|
|
281
|
+
* harness's Claude): the account snapshot checks other runtimes' logins too,
|
|
282
|
+
* and a Codex check must never see the Claude token.
|
|
283
|
+
*/
|
|
284
|
+
export declare function scopedCommandRunner(binary: string, base: CommandRunner | undefined, extra: () => NodeJS.ProcessEnv): CommandRunner;
|
|
285
|
+
/** A line that is the confidence trailer, however the model dressed it (backticks, bold, commas, quotes). */
|
|
286
|
+
/** The reply classes the server's send rule knows (PR B); any other is sent as none. */
|
|
287
|
+
export declare const REPLY_CLASSES: ReadonlySet<string>;
|
|
279
288
|
/**
|
|
280
289
|
* A reply without its confidence trailer (`[[confidence=0.9 reply_class=general]]`,
|
|
281
290
|
* see `replyConfidence`), and what the last one said. Every trailer is removed,
|
package/dist/agent/run.js
CHANGED
|
@@ -212,10 +212,11 @@ async function postReplyWithReplay(api, send, label, log, sleep) {
|
|
|
212
212
|
const result = await postReplyOnce(api, send, label, log, sleep);
|
|
213
213
|
// The send rule kept it as a suggestion for the team: handled, not failed.
|
|
214
214
|
if (isHeldAsSuggestion(result)) {
|
|
215
|
-
|
|
215
|
+
const muted = /ai_muted/.test(result.error ?? '');
|
|
216
|
+
log(muted
|
|
216
217
|
? `${timestamp()} ◦ ${label} reply was not sent: the thread is muted for AI`
|
|
217
218
|
: `${timestamp()} ◦ ${label} reply was held as a suggestion for the team (the employee's send rule)`);
|
|
218
|
-
return { ...result, ok: true };
|
|
219
|
+
return { ...result, ok: true, withheld: muted ? 'muted' : 'suggestion' };
|
|
219
220
|
}
|
|
220
221
|
return result;
|
|
221
222
|
}
|
|
@@ -278,11 +279,28 @@ function resolveWorkdir(options, log) {
|
|
|
278
279
|
export function isHeldAsSuggestion(result) {
|
|
279
280
|
return result.status === 409 && /held_as_suggestion|ai_muted/.test(result.error ?? '');
|
|
280
281
|
}
|
|
282
|
+
/**
|
|
283
|
+
* A sign-in checker that adds `extra` only when it runs `binary` (this
|
|
284
|
+
* harness's Claude): the account snapshot checks other runtimes' logins too,
|
|
285
|
+
* and a Codex check must never see the Claude token.
|
|
286
|
+
*/
|
|
287
|
+
export function scopedCommandRunner(binary, base, extra) {
|
|
288
|
+
return (command, args, env) => (base ?? defaultCommandRunner)(command, args, command === binary ? { ...env, ...extra() } : env);
|
|
289
|
+
}
|
|
281
290
|
/** The run's extra variables, resolved now. */
|
|
282
291
|
function extraEnvOf(options) {
|
|
283
292
|
return typeof options.extraEnv === 'function' ? options.extraEnv() : (options.extraEnv ?? {});
|
|
284
293
|
}
|
|
285
294
|
/** A line that is the confidence trailer, however the model dressed it (backticks, bold, commas, quotes). */
|
|
295
|
+
/** The reply classes the server's send rule knows (PR B); any other is sent as none. */
|
|
296
|
+
export const REPLY_CLASSES = new Set([
|
|
297
|
+
'general',
|
|
298
|
+
'status_update',
|
|
299
|
+
'pricing_action',
|
|
300
|
+
'billing_action',
|
|
301
|
+
'legal_contractual',
|
|
302
|
+
'bug_in_our_code',
|
|
303
|
+
]);
|
|
286
304
|
/** A trailer token anywhere, with any decoration hugging it. */
|
|
287
305
|
const TRAILER_TOKEN = /[`*_]*\[\[\s*confidence\b[^\]]*\]\][`*_]*\.?/gi;
|
|
288
306
|
/** The confidence a trailer states: a plain decimal from 0 to 1, read whole — anything else is no confidence. */
|
|
@@ -319,7 +337,9 @@ export function splitConfidence(reply) {
|
|
|
319
337
|
.join('\n')
|
|
320
338
|
.replace(/\s+$/, '');
|
|
321
339
|
const confidence = trailerConfidence(last);
|
|
322
|
-
const
|
|
340
|
+
const named = /reply_class\s*[=:]\s*["']?([A-Za-z_]{1,40})/i.exec(last)?.[1]?.toLowerCase();
|
|
341
|
+
// Only the server's own classes; anything else is no class, so the server holds the reply.
|
|
342
|
+
const replyClass = named && REPLY_CLASSES.has(named) ? named : undefined;
|
|
323
343
|
if (confidence === undefined)
|
|
324
344
|
return { text };
|
|
325
345
|
return { text, confidence, ...(replyClass ? { replyClass } : {}) };
|
|
@@ -392,7 +412,7 @@ function buildRuntime(options, log) {
|
|
|
392
412
|
binary: brain === 'claude' ? (options.exec.trim().split(/\s+/)[0] ?? 'claude') : codexBinary(options.exec),
|
|
393
413
|
// A Claude harness's sign-in check sees the run's extra variables too (the Claude token).
|
|
394
414
|
run: brain === 'claude' && options.extraEnv
|
|
395
|
-
? (
|
|
415
|
+
? scopedCommandRunner(options.exec.trim().split(/\s+/)[0] ?? 'claude', options.accountCommandRunner, () => extraEnvOf(options))
|
|
396
416
|
: options.accountCommandRunner,
|
|
397
417
|
warn: (message) => log(`${timestamp()} ! ${message}`),
|
|
398
418
|
});
|
|
@@ -1173,6 +1193,8 @@ limited) {
|
|
|
1173
1193
|
let earlyCount = 0;
|
|
1174
1194
|
let earlyPosted = 0;
|
|
1175
1195
|
let earlyPosts = Promise.resolve();
|
|
1196
|
+
/** An answer of this run was dropped because the thread is muted: its session must not continue. */
|
|
1197
|
+
let muted = false;
|
|
1176
1198
|
const earlyPath = run.reply_path;
|
|
1177
1199
|
const earlyBodyKey = run.reply_body_key;
|
|
1178
1200
|
const stoppedNow = () => control.signal.aborted || control.disposition() !== undefined;
|
|
@@ -1200,6 +1222,8 @@ limited) {
|
|
|
1200
1222
|
}
|
|
1201
1223
|
early.shift();
|
|
1202
1224
|
earlyPosted += 1;
|
|
1225
|
+
if (posted.withheld === 'muted')
|
|
1226
|
+
muted = true;
|
|
1203
1227
|
log(`${timestamp()} ✓ ${label} posted an answer; a background task is still running`);
|
|
1204
1228
|
}
|
|
1205
1229
|
};
|
|
@@ -1224,6 +1248,17 @@ limited) {
|
|
|
1224
1248
|
// A venue session is never continued: the next turn there starts fresh from
|
|
1225
1249
|
// the thread the server sends, so nothing of this one is remembered.
|
|
1226
1250
|
const sessionId = venue ? undefined : result.ok ? result.sessionId : undefined;
|
|
1251
|
+
/**
|
|
1252
|
+
* Continue the conversation's session from this turn — unless an answer of
|
|
1253
|
+
* it was dropped because the thread is muted (any runtime): then the session
|
|
1254
|
+
* holds a turn nobody saw, so it is forgotten, eager record or resumed one alike.
|
|
1255
|
+
*/
|
|
1256
|
+
const settleSession = (id) => {
|
|
1257
|
+
if (muted)
|
|
1258
|
+
runtime.sessions.forget(sessionKeyFor(run));
|
|
1259
|
+
else if (id)
|
|
1260
|
+
runtime.sessions.remember(sessionKeyFor(run), id);
|
|
1261
|
+
};
|
|
1227
1262
|
// Whatever the turn did, the timeline is whole BEFORE the reply is posted or
|
|
1228
1263
|
// the run is closed — a completion that beats its own last tool row would
|
|
1229
1264
|
// leave a `calling` row on a finished run. Never rejects, and after `stop`
|
|
@@ -1345,9 +1380,7 @@ limited) {
|
|
|
1345
1380
|
log(` ! could not mark the run complete (${describeFailure(completed)})`);
|
|
1346
1381
|
return 'unreported';
|
|
1347
1382
|
}
|
|
1348
|
-
|
|
1349
|
-
runtime.sessions.remember(sessionKeyFor(run), sessionId);
|
|
1350
|
-
}
|
|
1383
|
+
settleSession(sessionId);
|
|
1351
1384
|
return 'succeeded';
|
|
1352
1385
|
}
|
|
1353
1386
|
// A run with no reply surface is a legitimate outcome, not an error: it was
|
|
@@ -1367,9 +1400,7 @@ limited) {
|
|
|
1367
1400
|
log(` ! could not mark the run complete (${describeFailure(completed)})`);
|
|
1368
1401
|
return 'unreported';
|
|
1369
1402
|
}
|
|
1370
|
-
|
|
1371
|
-
runtime.sessions.remember(sessionKeyFor(run), sessionId);
|
|
1372
|
-
}
|
|
1403
|
+
settleSession(sessionId);
|
|
1373
1404
|
return 'succeeded';
|
|
1374
1405
|
}
|
|
1375
1406
|
const replyPath = run.reply_path;
|
|
@@ -1392,10 +1423,11 @@ limited) {
|
|
|
1392
1423
|
}
|
|
1393
1424
|
// Advance continuity only after the answer is durable on its reply surface.
|
|
1394
1425
|
// Remembering at process exit would strand the conversation behind an answer
|
|
1395
|
-
// it never received whenever this POST is rejected.
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1426
|
+
// it never received whenever this POST is rejected. A muted thread kept
|
|
1427
|
+
// nothing of it, so its session is not advanced either (and an eager record undone).
|
|
1428
|
+
if (posted.withheld === 'muted')
|
|
1429
|
+
muted = true;
|
|
1430
|
+
settleSession(sessionId);
|
|
1399
1431
|
const completed = await completeWithRetry(api, run.completion_path, { ok: true }, log, sleep, runtime.model());
|
|
1400
1432
|
if (!completed.ok) {
|
|
1401
1433
|
// The reply is already in the conversation, so this is a bookkeeping
|
|
@@ -72,7 +72,7 @@ const OFFERED_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]*(\/[A-Za-z0-9][A-Za-z0-9._-]*)?
|
|
|
72
72
|
/** What a person reads when a thread names a repository this Mac has no checkout of. */
|
|
73
73
|
export function notOfferedReason(repo, offered) {
|
|
74
74
|
if (offered.length === 0) {
|
|
75
|
-
return `This Mac isn't set up with any repositories for
|
|
75
|
+
return `This Mac isn't set up with any repositories for terminals yet, so it can't work on ${repo}.`;
|
|
76
76
|
}
|
|
77
77
|
return `This Mac isn't set up for ${repo}. Pick one of its repositories: ${offered.join(', ')}.`;
|
|
78
78
|
}
|
|
@@ -24,6 +24,13 @@ export interface CodeHostSettings {
|
|
|
24
24
|
allowRemoteTerminal: boolean;
|
|
25
25
|
}
|
|
26
26
|
export declare const DEFAULT_SETTINGS: CodeHostSettings;
|
|
27
|
+
/**
|
|
28
|
+
* A dedicated `prohost host` service decides terminal sessions in its own definition
|
|
29
|
+
* (`host install --dedicated --remote-terminal | --no-remote-terminal`), not in the home's
|
|
30
|
+
* `settings.json`: `1` hosts with remote control on, `0` hosts nothing. Every reader — the daemon,
|
|
31
|
+
* the relay and the permission hook in each Claude window (which inherits it) — sees the same.
|
|
32
|
+
*/
|
|
33
|
+
export declare const TERMINAL_SESSIONS_ENV = "PROHOST_HOST_TERMINAL_SESSIONS";
|
|
27
34
|
/** `$PROHOST_HOME/code-host`. */
|
|
28
35
|
export declare function codeHostDir(env?: NodeJS.ProcessEnv): string;
|
|
29
36
|
export declare function settingsPath(env?: NodeJS.ProcessEnv): string;
|
|
@@ -27,6 +27,13 @@ export const DEFAULT_SETTINGS = Object.freeze({
|
|
|
27
27
|
enabled: false,
|
|
28
28
|
allowRemoteTerminal: false,
|
|
29
29
|
});
|
|
30
|
+
/**
|
|
31
|
+
* A dedicated `prohost host` service decides terminal sessions in its own definition
|
|
32
|
+
* (`host install --dedicated --remote-terminal | --no-remote-terminal`), not in the home's
|
|
33
|
+
* `settings.json`: `1` hosts with remote control on, `0` hosts nothing. Every reader — the daemon,
|
|
34
|
+
* the relay and the permission hook in each Claude window (which inherits it) — sees the same.
|
|
35
|
+
*/
|
|
36
|
+
export const TERMINAL_SESSIONS_ENV = 'PROHOST_HOST_TERMINAL_SESSIONS';
|
|
30
37
|
/** `$PROHOST_HOME/code-host`. */
|
|
31
38
|
export function codeHostDir(env = process.env) {
|
|
32
39
|
return path.join(prohostHome(env), 'code-host');
|
|
@@ -94,6 +101,11 @@ export function readJsonObject(file) {
|
|
|
94
101
|
* is the default — both off — and only a literal `true` turns a switch on.
|
|
95
102
|
*/
|
|
96
103
|
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 };
|
|
97
109
|
const raw = readJsonObject(settingsPath(env));
|
|
98
110
|
if (!raw || raw.version !== 1)
|
|
99
111
|
return { ...DEFAULT_SETTINGS };
|
|
@@ -33,6 +33,7 @@ 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';
|
|
36
37
|
import { defaultSizeArgs, HOLD_OPTION, holdChannel, holdCommand, resizeWindowArgs, TerminalSizing } from './sizing.js';
|
|
37
38
|
import { noteInputTakeover } from './sizing.js';
|
|
38
39
|
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';
|
|
@@ -169,6 +170,14 @@ const CLAUDE_THREAD_ENV = { CLAUDE_CODE_ENABLE_CFC: '0' };
|
|
|
169
170
|
* and the like), then the thread's overrides, which a login's environment never undoes. The one
|
|
170
171
|
* builder for it: `startOnce` launches with it, and `adoptReadiness` reads the config dir from it.
|
|
171
172
|
*/
|
|
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
|
+
}
|
|
172
181
|
function agentWindowEnv(agent, accountEnv) {
|
|
173
182
|
return { ...stringEnv(accountEnv), ...(agent === 'claude_code' ? CLAUDE_THREAD_ENV : {}) };
|
|
174
183
|
}
|
|
@@ -480,6 +489,7 @@ export class CodeTerminals {
|
|
|
480
489
|
cwd: checkout.path,
|
|
481
490
|
env: {
|
|
482
491
|
...agentWindowEnv(request.agent, account.env),
|
|
492
|
+
...terminalSessionsEnv(this.options.processEnv ?? process.env),
|
|
483
493
|
PROHOST_HOME: this.options.home,
|
|
484
494
|
[TERMINAL_ID_ENV]: request.terminalId,
|
|
485
495
|
[CODE_THREAD_ID_ENV]: request.codeThreadId,
|
package/dist/host/command.d.ts
CHANGED
|
@@ -55,5 +55,13 @@ export declare function statusJson(env: NodeJS.ProcessEnv, now?: number, options
|
|
|
55
55
|
label?: string;
|
|
56
56
|
platform?: NodeJS.Platform;
|
|
57
57
|
}): Record<string, unknown>;
|
|
58
|
+
/**
|
|
59
|
+
* How to start a sign-in. On Windows npm installs `claude` / `codex` as `.cmd`
|
|
60
|
+
* shims, which only `cmd.exe` can run: the whole line goes through
|
|
61
|
+
* `cmd.exe /d /s /c "…"`, each part quoted (they are fixed words here).
|
|
62
|
+
*/
|
|
63
|
+
export declare function signInSpawn(command: string, args: string[], platform?: NodeJS.Platform): [string, string[], {
|
|
64
|
+
windowsVerbatimArguments?: boolean;
|
|
65
|
+
}];
|
|
58
66
|
export declare function runHost(subcommand: string | undefined, flags: Flags, printHelp: () => void, io?: HostIo): Promise<number>;
|
|
59
67
|
export {};
|
package/dist/host/command.js
CHANGED
|
@@ -29,7 +29,7 @@ import { addAccount, isValidLabel, loadAccounts, parseAccountRuntime, accountEnv
|
|
|
29
29
|
import { revokeComputer } from './api.js';
|
|
30
30
|
import { readSharedObject, acquireHostLock, clearComputer, computerOs, controlPath, hostLockPath, readObject, statePath, tryLoadComputer, writePrivateFile, } from './computer.js';
|
|
31
31
|
import { journalPath, readEvents } from './events.js';
|
|
32
|
-
import { LAUNCHD_HOST_LABEL, defaultLabel, installService, serviceFilePath, serviceUserHome, sharedBase, sharedDirFor, uninstallService, } from './install.js';
|
|
32
|
+
import { LAUNCHD_HOST_LABEL, defaultLabel, defaultServiceRunner, installService, windowsTaskOf, serviceFilePath, serviceUserHome, sharedBase, sharedDirFor, uninstallService, } from './install.js';
|
|
33
33
|
import { LegacyHarnesses } from './legacy.js';
|
|
34
34
|
import { detectLogins } from './logins.js';
|
|
35
35
|
import { adoptComputer, loadAdopted, runMigrate } from './migrate.js';
|
|
@@ -97,26 +97,29 @@ export function hardwareFacts() {
|
|
|
97
97
|
*/
|
|
98
98
|
export function statusJson(env, now = Date.now(), options = {}) {
|
|
99
99
|
const computer = tryLoadComputer(env);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
?
|
|
105
|
-
:
|
|
106
|
-
const
|
|
107
|
-
|
|
100
|
+
// A dedicated service is reported only when asked for by its `--label`, and then only from what
|
|
101
|
+
// it publishes: unlabelled, status is about this home's own service, whatever else runs on the
|
|
102
|
+
// computer; labelled, nothing of this home's (its pause, its pairing, its lock) is mixed in.
|
|
103
|
+
const labelDir = options.label
|
|
104
|
+
? path.join(options.sharedBase ?? sharedBase(options.platform ?? process.platform), options.label)
|
|
105
|
+
: undefined;
|
|
106
|
+
const shared = options.label
|
|
107
|
+
? dedicatedState(options.sharedBase ?? sharedBase(options.platform ?? process.platform), now, options.label)
|
|
108
|
+
: undefined;
|
|
109
|
+
const own = labelDir ? undefined : readObject(statePath(env));
|
|
110
|
+
const state = labelDir ? shared?.state : own;
|
|
111
|
+
const fresh = labelDir ? shared !== undefined : own !== undefined && now - Date.parse(own.updatedAt) < STATE_STALE_MS;
|
|
108
112
|
const sharedComputer = shared?.state.computer;
|
|
109
|
-
const paused = readObject(controlPath(env))?.paused === true ||
|
|
110
|
-
(shared !== undefined && readObject(path.join(shared.dir, 'control.json'))?.paused === true) ||
|
|
113
|
+
const paused = readObject(labelDir ? path.join(labelDir, 'control.json') : controlPath(env))?.paused === true ||
|
|
111
114
|
(fresh && state?.paused === true);
|
|
112
115
|
const recommended = state?.recommended_capacity ?? recommendCapacity({ memoryGb: os.totalmem() / 1024 ** 3, cores: os.cpus().length });
|
|
113
116
|
const capacity = state?.capacity ?? recommended;
|
|
114
|
-
const lock = readObject(hostLockPath(env));
|
|
115
|
-
const events =
|
|
117
|
+
const lock = labelDir ? undefined : readObject(hostLockPath(env));
|
|
118
|
+
const events = readEvents(labelDir ? path.join(labelDir, 'events.json') : journalPath(env), now);
|
|
116
119
|
return {
|
|
117
120
|
// Reporting the dedicated service: its own computer, never this home's (they can differ).
|
|
118
|
-
paired:
|
|
119
|
-
computer:
|
|
121
|
+
paired: labelDir ? sharedComputer !== undefined : computer !== null,
|
|
122
|
+
computer: labelDir
|
|
120
123
|
? (sharedComputer ?? null)
|
|
121
124
|
: computer
|
|
122
125
|
? { id: computer.computer_id, name: (fresh && state?.name) || computer.name, account: computer.account }
|
|
@@ -131,12 +134,13 @@ export function statusJson(env, now = Date.now(), options = {}) {
|
|
|
131
134
|
hardware: hardwareFacts(),
|
|
132
135
|
events,
|
|
133
136
|
service: {
|
|
134
|
-
running: shared !== undefined
|
|
135
|
-
pid:
|
|
136
|
-
adopted: loadAdopted(env)?.homes.length ?? 0,
|
|
137
|
-
dedicated:
|
|
138
|
-
...(
|
|
137
|
+
running: labelDir ? shared !== undefined : alive(lock?.pid),
|
|
138
|
+
pid: labelDir ? (shared?.state.pid ?? null) : alive(lock?.pid) ? lock?.pid : null,
|
|
139
|
+
adopted: labelDir ? 0 : (loadAdopted(env)?.homes.length ?? 0),
|
|
140
|
+
dedicated: labelDir !== undefined,
|
|
141
|
+
...(labelDir ? { activity_file: path.join(labelDir, 'activity.json') } : {}),
|
|
139
142
|
},
|
|
143
|
+
remote_terminal: Boolean(fresh && state?.remote_terminal),
|
|
140
144
|
version: CLI_VERSION,
|
|
141
145
|
};
|
|
142
146
|
}
|
|
@@ -222,9 +226,21 @@ function homeOfUser(platform, user) {
|
|
|
222
226
|
}
|
|
223
227
|
return path.join('/home', user, '.prohost');
|
|
224
228
|
}
|
|
229
|
+
/**
|
|
230
|
+
* How to start a sign-in. On Windows npm installs `claude` / `codex` as `.cmd`
|
|
231
|
+
* shims, which only `cmd.exe` can run: the whole line goes through
|
|
232
|
+
* `cmd.exe /d /s /c "…"`, each part quoted (they are fixed words here).
|
|
233
|
+
*/
|
|
234
|
+
export function signInSpawn(command, args, platform = process.platform) {
|
|
235
|
+
if (platform !== 'win32')
|
|
236
|
+
return [command, args, {}];
|
|
237
|
+
const quote = (part) => (/^[A-Za-z0-9._:\\/-]+$/.test(part) ? part : `"${part.replace(/"/g, '""')}"`);
|
|
238
|
+
return ['cmd.exe', ['/d', '/s', '/c', `"${[command, ...args].map(quote).join(' ')}"`], { windowsVerbatimArguments: true }];
|
|
239
|
+
}
|
|
225
240
|
/** `claude auth login` / `codex login`, in this terminal, as the new login. */
|
|
226
241
|
const defaultSignIn = (command, args, env) => new Promise((resolve) => {
|
|
227
|
-
const
|
|
242
|
+
const [file, argv, extra] = signInSpawn(command, args);
|
|
243
|
+
const child = spawn(file, argv, { stdio: 'inherit', env: { ...process.env, ...env }, ...extra });
|
|
228
244
|
child.on('error', () => resolve(null));
|
|
229
245
|
child.on('exit', (code) => resolve(code));
|
|
230
246
|
});
|
|
@@ -411,8 +427,14 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
411
427
|
pathEnv: env.PATH ?? '',
|
|
412
428
|
userHome,
|
|
413
429
|
label: str(flags, 'label') ?? defaultLabel(home, path.join(userHome, '.prohost')),
|
|
430
|
+
// Terminal sessions on a dedicated service: off unless --remote-terminal.
|
|
431
|
+
...(dedicated ? { terminalSessions: flags['remote-terminal'] === true && flags['no-remote-terminal'] !== true } : {}),
|
|
414
432
|
electron: io.electron ?? (Boolean(process.versions.electron) || env.ELECTRON_RUN_AS_NODE === '1'),
|
|
415
433
|
};
|
|
434
|
+
if (flags['remote-terminal'] === true && flags['no-remote-terminal'] === true) {
|
|
435
|
+
err('Pass --remote-terminal or --no-remote-terminal, not both.');
|
|
436
|
+
return 1;
|
|
437
|
+
}
|
|
416
438
|
const tokenFile = str(flags, 'claude-token-file');
|
|
417
439
|
if (flags['claude-token-file'] === true) {
|
|
418
440
|
err('--claude-token-file needs a path.');
|
|
@@ -434,6 +456,19 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
434
456
|
(o.ok ? out : err)(o.message);
|
|
435
457
|
return shown.every((o) => o.ok) ? 0 : 1;
|
|
436
458
|
}
|
|
459
|
+
// 0.16.0 put a Windows dedicated service's home in the account's profile, under another
|
|
460
|
+
// label: replacing it here would leave two services on one key. Its owner removes it first.
|
|
461
|
+
if (platform === 'win32' && dedicated && serviceUser && !str(flags, 'service-home')) {
|
|
462
|
+
const oldHome = path.win32.join('C:\\Users', serviceUser, '.prohost');
|
|
463
|
+
const oldLabel = defaultLabel(oldHome, path.join(userHome, '.prohost'));
|
|
464
|
+
const oldTask = windowsTaskOf({ label: oldLabel });
|
|
465
|
+
if ((io.serviceIo?.run ?? defaultServiceRunner)('schtasks', ['/Query', '/TN', oldTask]).status === 0) {
|
|
466
|
+
err(`✗ A host service from CLI 0.16.0 is installed for ${serviceUser} (task ${oldTask}, home ${oldHome}). ` +
|
|
467
|
+
`Remove it first: prohost host uninstall --label ${oldLabel} — then run this again with ` +
|
|
468
|
+
`--from-home ${oldHome} to carry its computer over.`);
|
|
469
|
+
return 1;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
437
472
|
// The invoking user's home: under sudo, $PROHOST_HOME and $HOME are root's.
|
|
438
473
|
const adoptFrom = str(flags, 'from-home') ??
|
|
439
474
|
(env.SUDO_USER && !env.PROHOST_HOME ? homeOfUser(platform, env.SUDO_USER) : ownHome);
|
|
@@ -503,20 +538,28 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
503
538
|
case 'resume': {
|
|
504
539
|
const paused = subcommand === 'pause';
|
|
505
540
|
const control = { v: 1, paused, updated_at: new Date().toISOString() };
|
|
506
|
-
|
|
507
|
-
//
|
|
541
|
+
// A dedicated service reads its own shared switch (writable by everyone on the computer):
|
|
542
|
+
// flipped only when it is named (`--label`), and then it alone. Unlabelled, this home alone
|
|
543
|
+
// is paused — a personal sign-out never pauses a dedicated service on the same computer.
|
|
508
544
|
const label = str(flags, 'label');
|
|
509
|
-
|
|
545
|
+
if (label) {
|
|
546
|
+
const dir = path.join(io.sharedBase ?? sharedBase(platform), label);
|
|
510
547
|
try {
|
|
511
548
|
writeFileSync(path.join(dir, 'control.json'), `${JSON.stringify(control)}\n`);
|
|
512
549
|
}
|
|
513
550
|
catch (e) {
|
|
514
|
-
err(`
|
|
551
|
+
err(`Could not ${subcommand} the service in ${dir}: ${e instanceof Error ? e.message : String(e)}`);
|
|
552
|
+
return 1;
|
|
515
553
|
}
|
|
516
554
|
}
|
|
555
|
+
else {
|
|
556
|
+
writePrivateFile(controlPath(env), control);
|
|
557
|
+
}
|
|
558
|
+
const which = label ? `the service ${label}` : 'this computer';
|
|
559
|
+
const again = label ? `prohost host resume --label ${label}` : 'prohost host resume';
|
|
517
560
|
out(paused
|
|
518
|
-
?
|
|
519
|
-
:
|
|
561
|
+
? `⏸ Paused: ${which} takes no new work. Work already going finishes. Resume with: ${again}`
|
|
562
|
+
: `▶ Resumed: ${which} takes new work again.`);
|
|
520
563
|
return 0;
|
|
521
564
|
}
|
|
522
565
|
case 'migrate': {
|
package/dist/host/install.d.ts
CHANGED
|
@@ -31,6 +31,7 @@ export declare const STOP_TIMEOUT_SECONDS = 600;
|
|
|
31
31
|
export declare const RESTART_WAIT_MS: number;
|
|
32
32
|
/** Set in a dedicated service's environment: where it publishes its state for the logged-in user. */
|
|
33
33
|
export declare const SHARED_DIR_ENV = "PROHOST_HOST_SHARED_DIR";
|
|
34
|
+
/** Set in a dedicated service's environment: terminal sessions on (`1`) or off (`0`), from install. */
|
|
34
35
|
/** The `claude setup-token` token a dedicated install hands the service user (read by `host run`). */
|
|
35
36
|
export declare const CLAUDE_TOKEN_FILE = "claude-oauth-token";
|
|
36
37
|
export type ServiceRunner = (command: string, args: string[], input?: string) => {
|
|
@@ -65,6 +66,11 @@ export interface ServiceSpec {
|
|
|
65
66
|
* service keeps `ELECTRON_RUN_AS_NODE=1`, or launching it would open the app.
|
|
66
67
|
*/
|
|
67
68
|
electron?: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* Dedicated only: terminal sessions (remote terminals) on this service —
|
|
71
|
+
* `--remote-terminal`. Off unless asked for.
|
|
72
|
+
*/
|
|
73
|
+
terminalSessions?: boolean;
|
|
68
74
|
}
|
|
69
75
|
/** The label for a home: the plain one for the default home, else one with a short digest of the home. */
|
|
70
76
|
export declare function defaultLabel(home: string, defaultHome: string): string;
|
|
@@ -135,6 +141,8 @@ export interface DedicatedSetup {
|
|
|
135
141
|
}
|
|
136
142
|
/** Validate a spec before anything is written. */
|
|
137
143
|
export declare function specProblem(spec: ServiceSpec, io?: InstallIo, setup?: DedicatedSetup): string | undefined;
|
|
144
|
+
/** Where Windows dedicated services' homes live: an administrators' folder. */
|
|
145
|
+
export declare function windowsHomesDir(): string;
|
|
138
146
|
/** Where a Windows dedicated install keeps the Claude token: an administrators' folder the account may only read. */
|
|
139
147
|
export declare function windowsTokenPath(spec: Pick<ServiceSpec, 'label'>): string;
|
|
140
148
|
/**
|
package/dist/host/install.js
CHANGED
|
@@ -24,6 +24,7 @@ import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, rmSync, writ
|
|
|
24
24
|
import os from 'node:os';
|
|
25
25
|
import path from 'node:path';
|
|
26
26
|
import { buildLaunchdPlist, waitUntilUnloaded } from '../agent/daemon.js';
|
|
27
|
+
import { TERMINAL_SESSIONS_ENV } from '../codehost/settings.js';
|
|
27
28
|
export const LAUNCHD_HOST_LABEL = 'ai.prohost.host';
|
|
28
29
|
export const SYSTEMD_HOST_UNIT = 'prohost-host.service';
|
|
29
30
|
export const WINDOWS_TASK_NAME = '\\ProhostAI\\Host';
|
|
@@ -37,6 +38,7 @@ export const STOP_TIMEOUT_SECONDS = 600;
|
|
|
37
38
|
export const RESTART_WAIT_MS = (STOP_TIMEOUT_SECONDS + 30) * 1000;
|
|
38
39
|
/** Set in a dedicated service's environment: where it publishes its state for the logged-in user. */
|
|
39
40
|
export const SHARED_DIR_ENV = 'PROHOST_HOST_SHARED_DIR';
|
|
41
|
+
/** Set in a dedicated service's environment: terminal sessions on (`1`) or off (`0`), from install. */
|
|
40
42
|
/** The `claude setup-token` token a dedicated install hands the service user (read by `host run`). */
|
|
41
43
|
export const CLAUDE_TOKEN_FILE = 'claude-oauth-token';
|
|
42
44
|
export const defaultServiceRunner = (command, args, input) => {
|
|
@@ -101,7 +103,9 @@ export function serviceEnv(spec) {
|
|
|
101
103
|
return {
|
|
102
104
|
PATH: spec.pathEnv,
|
|
103
105
|
PROHOST_HOME: spec.home,
|
|
104
|
-
...(spec.dedicated
|
|
106
|
+
...(spec.dedicated
|
|
107
|
+
? { PROHOST_HOST_DEDICATED: '1', [SHARED_DIR_ENV]: sharedDirFor(spec), [TERMINAL_SESSIONS_ENV]: spec.terminalSessions ? '1' : '0' }
|
|
108
|
+
: {}),
|
|
105
109
|
...(spec.dedicated && spec.platform === 'darwin' && spec.serviceUser
|
|
106
110
|
? { HOME: path.posix.join('/Users', spec.serviceUser) }
|
|
107
111
|
: {}),
|
|
@@ -272,6 +276,10 @@ export function specProblem(spec, io = {}, setup = {}) {
|
|
|
272
276
|
return '--dedicated needs --service-user <name>: the account the service runs as.';
|
|
273
277
|
if (!SERVICE_USER.test(spec.serviceUser))
|
|
274
278
|
return `"${spec.serviceUser}" is not a usable account name.`;
|
|
279
|
+
if (spec.platform === 'win32' && path.win32.resolve(path.win32.dirname(spec.home)) !== path.win32.resolve(windowsHomesDir())) {
|
|
280
|
+
// Only a home under the administrators' folder can be prepared safely (see `prepareDedicated`).
|
|
281
|
+
return `On Windows a dedicated service's home is ${path.win32.join(windowsHomesDir(), spec.serviceUser)}; --service-home is not supported there.`;
|
|
282
|
+
}
|
|
275
283
|
const admin = io.isAdmin ??
|
|
276
284
|
(spec.platform === 'win32'
|
|
277
285
|
? // `net session` succeeds only from an elevated prompt.
|
|
@@ -332,6 +340,10 @@ function ensureServiceUser(spec, run) {
|
|
|
332
340
|
}
|
|
333
341
|
return undefined;
|
|
334
342
|
}
|
|
343
|
+
/** Where Windows dedicated services' homes live: an administrators' folder. */
|
|
344
|
+
export function windowsHomesDir() {
|
|
345
|
+
return 'C:\\ProgramData\\Prohost\\homes';
|
|
346
|
+
}
|
|
335
347
|
/** Where a Windows dedicated install keeps the Claude token: an administrators' folder the account may only read. */
|
|
336
348
|
export function windowsTokenPath(spec) {
|
|
337
349
|
return path.win32.join('C:\\ProgramData\\Prohost\\secrets', labelOf(spec), CLAUDE_TOKEN_FILE);
|
|
@@ -385,6 +397,7 @@ function claimWindowsDirs(spec, run, mkdirOnce, isSymlink) {
|
|
|
385
397
|
[path.win32.join(base, 'secrets'), admin],
|
|
386
398
|
[path.win32.dirname(windowsTokenPath(spec)), [...admin, `${user}:(OI)(CI)R`]],
|
|
387
399
|
[path.win32.join(base, 'services'), admin],
|
|
400
|
+
[windowsHomesDir(), admin],
|
|
388
401
|
[windowsServiceDir(spec), [...admin, `${user}:(OI)(CI)RX`]],
|
|
389
402
|
];
|
|
390
403
|
const trusted = (owner) => /\\(Administrators|SYSTEM)$/i.test(owner);
|
|
@@ -500,17 +513,36 @@ function prepareDedicated(spec, io, setup) {
|
|
|
500
513
|
// The account's home is ACL'd and filled only when this install creates
|
|
501
514
|
// it: an existing one is the account's, and an administrator writing into
|
|
502
515
|
// it could be redirected.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
516
|
+
// The service home is an administrators' folder (ProgramData\Prohost\homes\<label>): made
|
|
517
|
+
// in one step with no access for the account, filled, and only then opened to it — so it can
|
|
518
|
+
// never swap a path in it while an administrator writes there. An existing one is never
|
|
519
|
+
// written into; a --service-home elsewhere is the account's own business.
|
|
520
|
+
if (path.win32.resolve(path.win32.dirname(spec.home)) === path.win32.resolve(windowsHomesDir())) {
|
|
521
|
+
const made = (io.mkdirOnce ?? mkdirOnce)(spec.home);
|
|
522
|
+
if (made === 'exists') {
|
|
523
|
+
if (isSymlink(spec.home))
|
|
524
|
+
return `${spec.home} is a link or junction: remove it, then install again.`;
|
|
525
|
+
const owner = windowsOwner(run, spec.home);
|
|
526
|
+
if (!/\\(Administrators|SYSTEM)$/i.test(owner))
|
|
527
|
+
return `${spec.home} is owned by ${owner || 'someone else'}: remove it, then install again.`;
|
|
528
|
+
}
|
|
529
|
+
else {
|
|
530
|
+
for (const args of [
|
|
531
|
+
[spec.home, '/setowner', 'Administrators'],
|
|
532
|
+
[spec.home, '/inheritance:r', '/grant:r', 'SYSTEM:(OI)(CI)F', 'Administrators:(OI)(CI)F'],
|
|
533
|
+
]) {
|
|
534
|
+
const out = run('icacls', args);
|
|
535
|
+
if (out.status !== 0)
|
|
536
|
+
return failed(`icacls ${args.slice(0, 2).join(' ')}`, out);
|
|
537
|
+
}
|
|
538
|
+
mkdir(hostDir, 0o700);
|
|
539
|
+
if (credential)
|
|
540
|
+
write(join(hostDir, 'computer.json'), credential, 0o600, 'wx');
|
|
541
|
+
}
|
|
542
|
+
// The account may now use its home.
|
|
543
|
+
const open = run('icacls', [spec.home, '/grant', `${user}:(OI)(CI)M`]);
|
|
544
|
+
if (open.status !== 0)
|
|
545
|
+
return failed(`icacls ${spec.home}`, open);
|
|
514
546
|
}
|
|
515
547
|
}
|
|
516
548
|
else {
|
|
@@ -749,7 +781,8 @@ export function uninstallService(spec, io = {}) {
|
|
|
749
781
|
export function serviceUserHome(platform, user) {
|
|
750
782
|
if (platform === 'darwin')
|
|
751
783
|
return path.join('/Users', user, '.prohost');
|
|
784
|
+
// On Windows: an administrators' folder per account (see `prepareDedicated`), not the account's profile.
|
|
752
785
|
if (platform === 'win32')
|
|
753
|
-
return path.win32.join(
|
|
786
|
+
return path.win32.join(windowsHomesDir(), user);
|
|
754
787
|
return path.join('/home', user, '.prohost');
|
|
755
788
|
}
|
package/dist/host/service.js
CHANGED
|
@@ -185,6 +185,9 @@ async function serveHost(options) {
|
|
|
185
185
|
const codeHostEnv = computer.code_host_home
|
|
186
186
|
? { ...process.env, ...env, PROHOST_HOME: computer.code_host_home }
|
|
187
187
|
: { ...process.env, ...env };
|
|
188
|
+
// A dedicated install decides terminal sessions in the service's own definition
|
|
189
|
+
// (`--remote-terminal` / `--no-remote-terminal`, `TERMINAL_SESSIONS_ENV`), which `loadSettings`
|
|
190
|
+
// reads over the home's switches; a personal service follows the home's switches.
|
|
188
191
|
const codeHost = options.codeHost !== undefined
|
|
189
192
|
? (options.codeHost ?? undefined)
|
|
190
193
|
: loadCodeHostSettings(codeHostEnv).enabled
|
|
@@ -367,6 +370,7 @@ async function serveHost(options) {
|
|
|
367
370
|
online,
|
|
368
371
|
paused: localPaused || config.paused,
|
|
369
372
|
draining,
|
|
373
|
+
remote_terminal: capabilities().includes(REMOTE_TERMINAL_CAPABILITY),
|
|
370
374
|
slots: slotsFor(capacity(), active.size + terminals(), held),
|
|
371
375
|
capacity: capacity(),
|
|
372
376
|
recommended_capacity: config.recommended_capacity ?? recommended(),
|
package/dist/host/status.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export interface HostState {
|
|
|
80
80
|
online: boolean;
|
|
81
81
|
paused: boolean;
|
|
82
82
|
draining: boolean;
|
|
83
|
+
/** Remote terminal sessions are on (the `remote_terminal` capability). */
|
|
84
|
+
remote_terminal?: boolean;
|
|
83
85
|
slots: Slots;
|
|
84
86
|
capacity: number;
|
|
85
87
|
recommended_capacity: number;
|
package/dist/index.js
CHANGED
|
@@ -35,7 +35,7 @@ function printHelp() {
|
|
|
35
35
|
' prohost host unpair Remove this computer from its workspace (revokes its key)',
|
|
36
36
|
' prohost host run One service for every AI employee placed on this computer',
|
|
37
37
|
' prohost host install [--label <id>] [--dedicated --service-user <name> [--service-home <dir>]',
|
|
38
|
-
' [--claude-token-file <path>]] | uninstall [--label <id>]',
|
|
38
|
+
' [--claude-token-file <path>] [--remote-terminal|--no-remote-terminal]] | uninstall [--label <id>]',
|
|
39
39
|
' prohost host status [--json] [--label <id>] | pause | resume',
|
|
40
40
|
' prohost host logins [--json] | logins add --runtime claude_code|codex [--label <l>]',
|
|
41
41
|
' prohost host migrate [--from <home>]… [--home <dir,…>] [--keep-services] [--dry-run] [--json]',
|
|
@@ -72,6 +72,8 @@ function printHelp() {
|
|
|
72
72
|
' publishes its status for `host status` run by anyone on the computer.',
|
|
73
73
|
' --claude-token-file `host install --dedicated`: a file holding a `claude setup-token` token, installed',
|
|
74
74
|
' for the service user as CLAUDE_CODE_OAUTH_TOKEN. Read once, never printed.',
|
|
75
|
+
' --remote-terminal / --no-remote-terminal `host install --dedicated`: terminal sessions on this',
|
|
76
|
+
' service (off unless --remote-terminal); `host status --json` shows remote_terminal.',
|
|
75
77
|
' --from-home `host install --dedicated`: the home whose computer.json the service takes over',
|
|
76
78
|
" (default: $PROHOST_HOME, or $SUDO_USER's ~/.prohost under sudo).",
|
|
77
79
|
' --label `host install` / `uninstall`: the service\'s label (launchd label, systemd unit,',
|
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.2";
|
|
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.2";
|
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.2';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|