@prohost/cli 0.16.0 → 0.16.1
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 +18 -0
- package/README.md +3 -2
- package/dist/agent/api.d.ts +6 -0
- package/dist/agent/run.d.ts +6 -0
- package/dist/agent/run.js +34 -13
- package/dist/host/command.js +14 -1
- package/dist/host/install.d.ts +2 -0
- package/dist/host/install.js +41 -12
- 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,24 @@ builds via `prepack`, runs the suite, publishes, and tags `cli-v<version>`.
|
|
|
6
6
|
|
|
7
7
|
## Unreleased
|
|
8
8
|
|
|
9
|
+
## 0.16.1
|
|
10
|
+
|
|
11
|
+
Review follow-ups to `prohost host` that landed on its branch after #22455 merged:
|
|
12
|
+
|
|
13
|
+
- **Windows dedicated install:** the service home is `C:\ProgramData\Prohost\homes\<account>`, made in
|
|
14
|
+
one step with administrators-only access, given the computer credential (`wx`), and only then
|
|
15
|
+
opened to the account — the account can never swap a path in it while an administrator writes
|
|
16
|
+
there. An existing home is never written into. `--service-home` is refused on Windows (only that folder can
|
|
17
|
+
be prepared safely), and an install over a 0.16.0 Windows dedicated service (its home was in the
|
|
18
|
+
account's profile, under another task name) stops and says how to remove it and carry its
|
|
19
|
+
computer over (`--from-home`), rather than leaving two services on one key.
|
|
20
|
+
- **Claude token scoping:** an adopted Claude harness's sign-in checks add the dedicated install's
|
|
21
|
+
Claude token only when they run its own Claude binary; the account snapshot's Codex checks never
|
|
22
|
+
see it.
|
|
23
|
+
- **Muted threads:** an answer dropped because the thread is muted (`409 ai_muted`) — the final one
|
|
24
|
+
or an early one, Claude or Codex — makes the run forget the conversation's session, so after
|
|
25
|
+
unmuting nothing resumes from a turn nobody saw.
|
|
26
|
+
|
|
9
27
|
## 0.16.0
|
|
10
28
|
|
|
11
29
|
- **`prohost host` — one service per computer, many employees.** A computer is added once
|
package/README.md
CHANGED
|
@@ -202,8 +202,9 @@ 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;
|
|
208
209
|
- logs where the service account can't redirect it: the journal on Linux,
|
|
209
210
|
`/Library/Logs/Prohost/<label>.log` (in a root-owned folder) on macOS,
|
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/run.d.ts
CHANGED
|
@@ -276,6 +276,12 @@ 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;
|
|
279
285
|
/**
|
|
280
286
|
* A reply without its confidence trailer (`[[confidence=0.9 reply_class=general]]`,
|
|
281
287
|
* 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,6 +279,14 @@ 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 ?? {});
|
|
@@ -392,7 +401,7 @@ function buildRuntime(options, log) {
|
|
|
392
401
|
binary: brain === 'claude' ? (options.exec.trim().split(/\s+/)[0] ?? 'claude') : codexBinary(options.exec),
|
|
393
402
|
// A Claude harness's sign-in check sees the run's extra variables too (the Claude token).
|
|
394
403
|
run: brain === 'claude' && options.extraEnv
|
|
395
|
-
? (
|
|
404
|
+
? scopedCommandRunner(options.exec.trim().split(/\s+/)[0] ?? 'claude', options.accountCommandRunner, () => extraEnvOf(options))
|
|
396
405
|
: options.accountCommandRunner,
|
|
397
406
|
warn: (message) => log(`${timestamp()} ! ${message}`),
|
|
398
407
|
});
|
|
@@ -1173,6 +1182,8 @@ limited) {
|
|
|
1173
1182
|
let earlyCount = 0;
|
|
1174
1183
|
let earlyPosted = 0;
|
|
1175
1184
|
let earlyPosts = Promise.resolve();
|
|
1185
|
+
/** An answer of this run was dropped because the thread is muted: its session must not continue. */
|
|
1186
|
+
let muted = false;
|
|
1176
1187
|
const earlyPath = run.reply_path;
|
|
1177
1188
|
const earlyBodyKey = run.reply_body_key;
|
|
1178
1189
|
const stoppedNow = () => control.signal.aborted || control.disposition() !== undefined;
|
|
@@ -1200,6 +1211,8 @@ limited) {
|
|
|
1200
1211
|
}
|
|
1201
1212
|
early.shift();
|
|
1202
1213
|
earlyPosted += 1;
|
|
1214
|
+
if (posted.withheld === 'muted')
|
|
1215
|
+
muted = true;
|
|
1203
1216
|
log(`${timestamp()} ✓ ${label} posted an answer; a background task is still running`);
|
|
1204
1217
|
}
|
|
1205
1218
|
};
|
|
@@ -1224,6 +1237,17 @@ limited) {
|
|
|
1224
1237
|
// A venue session is never continued: the next turn there starts fresh from
|
|
1225
1238
|
// the thread the server sends, so nothing of this one is remembered.
|
|
1226
1239
|
const sessionId = venue ? undefined : result.ok ? result.sessionId : undefined;
|
|
1240
|
+
/**
|
|
1241
|
+
* Continue the conversation's session from this turn — unless an answer of
|
|
1242
|
+
* it was dropped because the thread is muted (any runtime): then the session
|
|
1243
|
+
* holds a turn nobody saw, so it is forgotten, eager record or resumed one alike.
|
|
1244
|
+
*/
|
|
1245
|
+
const settleSession = (id) => {
|
|
1246
|
+
if (muted)
|
|
1247
|
+
runtime.sessions.forget(sessionKeyFor(run));
|
|
1248
|
+
else if (id)
|
|
1249
|
+
runtime.sessions.remember(sessionKeyFor(run), id);
|
|
1250
|
+
};
|
|
1227
1251
|
// Whatever the turn did, the timeline is whole BEFORE the reply is posted or
|
|
1228
1252
|
// the run is closed — a completion that beats its own last tool row would
|
|
1229
1253
|
// leave a `calling` row on a finished run. Never rejects, and after `stop`
|
|
@@ -1345,9 +1369,7 @@ limited) {
|
|
|
1345
1369
|
log(` ! could not mark the run complete (${describeFailure(completed)})`);
|
|
1346
1370
|
return 'unreported';
|
|
1347
1371
|
}
|
|
1348
|
-
|
|
1349
|
-
runtime.sessions.remember(sessionKeyFor(run), sessionId);
|
|
1350
|
-
}
|
|
1372
|
+
settleSession(sessionId);
|
|
1351
1373
|
return 'succeeded';
|
|
1352
1374
|
}
|
|
1353
1375
|
// A run with no reply surface is a legitimate outcome, not an error: it was
|
|
@@ -1367,9 +1389,7 @@ limited) {
|
|
|
1367
1389
|
log(` ! could not mark the run complete (${describeFailure(completed)})`);
|
|
1368
1390
|
return 'unreported';
|
|
1369
1391
|
}
|
|
1370
|
-
|
|
1371
|
-
runtime.sessions.remember(sessionKeyFor(run), sessionId);
|
|
1372
|
-
}
|
|
1392
|
+
settleSession(sessionId);
|
|
1373
1393
|
return 'succeeded';
|
|
1374
1394
|
}
|
|
1375
1395
|
const replyPath = run.reply_path;
|
|
@@ -1392,10 +1412,11 @@ limited) {
|
|
|
1392
1412
|
}
|
|
1393
1413
|
// Advance continuity only after the answer is durable on its reply surface.
|
|
1394
1414
|
// Remembering at process exit would strand the conversation behind an answer
|
|
1395
|
-
// it never received whenever this POST is rejected.
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1415
|
+
// it never received whenever this POST is rejected. A muted thread kept
|
|
1416
|
+
// nothing of it, so its session is not advanced either (and an eager record undone).
|
|
1417
|
+
if (posted.withheld === 'muted')
|
|
1418
|
+
muted = true;
|
|
1419
|
+
settleSession(sessionId);
|
|
1399
1420
|
const completed = await completeWithRetry(api, run.completion_path, { ok: true }, log, sleep, runtime.model());
|
|
1400
1421
|
if (!completed.ok) {
|
|
1401
1422
|
// The reply is already in the conversation, so this is a bookkeeping
|
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';
|
|
@@ -434,6 +434,19 @@ export async function runHost(subcommand, flags, printHelp, io = {}) {
|
|
|
434
434
|
(o.ok ? out : err)(o.message);
|
|
435
435
|
return shown.every((o) => o.ok) ? 0 : 1;
|
|
436
436
|
}
|
|
437
|
+
// 0.16.0 put a Windows dedicated service's home in the account's profile, under another
|
|
438
|
+
// label: replacing it here would leave two services on one key. Its owner removes it first.
|
|
439
|
+
if (platform === 'win32' && dedicated && serviceUser && !str(flags, 'service-home')) {
|
|
440
|
+
const oldHome = path.win32.join('C:\\Users', serviceUser, '.prohost');
|
|
441
|
+
const oldLabel = defaultLabel(oldHome, path.join(userHome, '.prohost'));
|
|
442
|
+
const oldTask = windowsTaskOf({ label: oldLabel });
|
|
443
|
+
if ((io.serviceIo?.run ?? defaultServiceRunner)('schtasks', ['/Query', '/TN', oldTask]).status === 0) {
|
|
444
|
+
err(`✗ A host service from CLI 0.16.0 is installed for ${serviceUser} (task ${oldTask}, home ${oldHome}). ` +
|
|
445
|
+
`Remove it first: prohost host uninstall --label ${oldLabel} — then run this again with ` +
|
|
446
|
+
`--from-home ${oldHome} to carry its computer over.`);
|
|
447
|
+
return 1;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
437
450
|
// The invoking user's home: under sudo, $PROHOST_HOME and $HOME are root's.
|
|
438
451
|
const adoptFrom = str(flags, 'from-home') ??
|
|
439
452
|
(env.SUDO_USER && !env.PROHOST_HOME ? homeOfUser(platform, env.SUDO_USER) : ownHome);
|
package/dist/host/install.d.ts
CHANGED
|
@@ -135,6 +135,8 @@ export interface DedicatedSetup {
|
|
|
135
135
|
}
|
|
136
136
|
/** Validate a spec before anything is written. */
|
|
137
137
|
export declare function specProblem(spec: ServiceSpec, io?: InstallIo, setup?: DedicatedSetup): string | undefined;
|
|
138
|
+
/** Where Windows dedicated services' homes live: an administrators' folder. */
|
|
139
|
+
export declare function windowsHomesDir(): string;
|
|
138
140
|
/** Where a Windows dedicated install keeps the Claude token: an administrators' folder the account may only read. */
|
|
139
141
|
export declare function windowsTokenPath(spec: Pick<ServiceSpec, 'label'>): string;
|
|
140
142
|
/**
|
package/dist/host/install.js
CHANGED
|
@@ -272,6 +272,10 @@ export function specProblem(spec, io = {}, setup = {}) {
|
|
|
272
272
|
return '--dedicated needs --service-user <name>: the account the service runs as.';
|
|
273
273
|
if (!SERVICE_USER.test(spec.serviceUser))
|
|
274
274
|
return `"${spec.serviceUser}" is not a usable account name.`;
|
|
275
|
+
if (spec.platform === 'win32' && path.win32.resolve(path.win32.dirname(spec.home)) !== path.win32.resolve(windowsHomesDir())) {
|
|
276
|
+
// Only a home under the administrators' folder can be prepared safely (see `prepareDedicated`).
|
|
277
|
+
return `On Windows a dedicated service's home is ${path.win32.join(windowsHomesDir(), spec.serviceUser)}; --service-home is not supported there.`;
|
|
278
|
+
}
|
|
275
279
|
const admin = io.isAdmin ??
|
|
276
280
|
(spec.platform === 'win32'
|
|
277
281
|
? // `net session` succeeds only from an elevated prompt.
|
|
@@ -332,6 +336,10 @@ function ensureServiceUser(spec, run) {
|
|
|
332
336
|
}
|
|
333
337
|
return undefined;
|
|
334
338
|
}
|
|
339
|
+
/** Where Windows dedicated services' homes live: an administrators' folder. */
|
|
340
|
+
export function windowsHomesDir() {
|
|
341
|
+
return 'C:\\ProgramData\\Prohost\\homes';
|
|
342
|
+
}
|
|
335
343
|
/** Where a Windows dedicated install keeps the Claude token: an administrators' folder the account may only read. */
|
|
336
344
|
export function windowsTokenPath(spec) {
|
|
337
345
|
return path.win32.join('C:\\ProgramData\\Prohost\\secrets', labelOf(spec), CLAUDE_TOKEN_FILE);
|
|
@@ -385,6 +393,7 @@ function claimWindowsDirs(spec, run, mkdirOnce, isSymlink) {
|
|
|
385
393
|
[path.win32.join(base, 'secrets'), admin],
|
|
386
394
|
[path.win32.dirname(windowsTokenPath(spec)), [...admin, `${user}:(OI)(CI)R`]],
|
|
387
395
|
[path.win32.join(base, 'services'), admin],
|
|
396
|
+
[windowsHomesDir(), admin],
|
|
388
397
|
[windowsServiceDir(spec), [...admin, `${user}:(OI)(CI)RX`]],
|
|
389
398
|
];
|
|
390
399
|
const trusted = (owner) => /\\(Administrators|SYSTEM)$/i.test(owner);
|
|
@@ -500,17 +509,36 @@ function prepareDedicated(spec, io, setup) {
|
|
|
500
509
|
// The account's home is ACL'd and filled only when this install creates
|
|
501
510
|
// it: an existing one is the account's, and an administrator writing into
|
|
502
511
|
// it could be redirected.
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
if (
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
512
|
+
// The service home is an administrators' folder (ProgramData\Prohost\homes\<label>): made
|
|
513
|
+
// in one step with no access for the account, filled, and only then opened to it — so it can
|
|
514
|
+
// never swap a path in it while an administrator writes there. An existing one is never
|
|
515
|
+
// written into; a --service-home elsewhere is the account's own business.
|
|
516
|
+
if (path.win32.resolve(path.win32.dirname(spec.home)) === path.win32.resolve(windowsHomesDir())) {
|
|
517
|
+
const made = (io.mkdirOnce ?? mkdirOnce)(spec.home);
|
|
518
|
+
if (made === 'exists') {
|
|
519
|
+
if (isSymlink(spec.home))
|
|
520
|
+
return `${spec.home} is a link or junction: remove it, then install again.`;
|
|
521
|
+
const owner = windowsOwner(run, spec.home);
|
|
522
|
+
if (!/\\(Administrators|SYSTEM)$/i.test(owner))
|
|
523
|
+
return `${spec.home} is owned by ${owner || 'someone else'}: remove it, then install again.`;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
for (const args of [
|
|
527
|
+
[spec.home, '/setowner', 'Administrators'],
|
|
528
|
+
[spec.home, '/inheritance:r', '/grant:r', 'SYSTEM:(OI)(CI)F', 'Administrators:(OI)(CI)F'],
|
|
529
|
+
]) {
|
|
530
|
+
const out = run('icacls', args);
|
|
531
|
+
if (out.status !== 0)
|
|
532
|
+
return failed(`icacls ${args.slice(0, 2).join(' ')}`, out);
|
|
533
|
+
}
|
|
534
|
+
mkdir(hostDir, 0o700);
|
|
535
|
+
if (credential)
|
|
536
|
+
write(join(hostDir, 'computer.json'), credential, 0o600, 'wx');
|
|
537
|
+
}
|
|
538
|
+
// The account may now use its home.
|
|
539
|
+
const open = run('icacls', [spec.home, '/grant', `${user}:(OI)(CI)M`]);
|
|
540
|
+
if (open.status !== 0)
|
|
541
|
+
return failed(`icacls ${spec.home}`, open);
|
|
514
542
|
}
|
|
515
543
|
}
|
|
516
544
|
else {
|
|
@@ -749,7 +777,8 @@ export function uninstallService(spec, io = {}) {
|
|
|
749
777
|
export function serviceUserHome(platform, user) {
|
|
750
778
|
if (platform === 'darwin')
|
|
751
779
|
return path.join('/Users', user, '.prohost');
|
|
780
|
+
// On Windows: an administrators' folder per account (see `prepareDedicated`), not the account's profile.
|
|
752
781
|
if (platform === 'win32')
|
|
753
|
-
return path.win32.join(
|
|
782
|
+
return path.win32.join(windowsHomesDir(), user);
|
|
754
783
|
return path.join('/home', user, '.prohost');
|
|
755
784
|
}
|
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.1";
|
|
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.1";
|
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.1';
|
|
11
11
|
/** Sent on every HTTP request the CLI makes back into ProhostAI. */
|
|
12
12
|
export const USER_AGENT = `prohost-cli/${CLI_VERSION}`;
|