@phnx-labs/agents-cli 1.22.78 → 1.22.79
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 +11 -0
- package/README.md +28 -1
- package/dist/bootstrap.js +40 -12
- package/dist/commands/accounts.d.ts +22 -0
- package/dist/commands/accounts.js +158 -35
- package/dist/commands/config.js +37 -0
- package/dist/commands/exec.js +21 -10
- package/dist/commands/update.js +169 -18
- package/dist/commands/versions.d.ts +11 -0
- package/dist/commands/versions.js +30 -4
- package/dist/commands/view.d.ts +4 -0
- package/dist/commands/view.js +114 -19
- package/dist/index.js +35 -2
- package/dist/lib/account-catalog.d.ts +97 -1
- package/dist/lib/account-catalog.js +134 -6
- package/dist/lib/account-registry.d.ts +43 -0
- package/dist/lib/account-registry.js +97 -2
- package/dist/lib/accounting/rotate.d.ts +2 -2
- package/dist/lib/accounting/rotate.js +5 -5
- package/dist/lib/accounts/auth-operation-lock.d.ts +9 -0
- package/dist/lib/accounts/auth-operation-lock.js +55 -0
- package/dist/lib/accounts/connect.d.ts +170 -0
- package/dist/lib/accounts/connect.js +383 -0
- package/dist/lib/capabilities.js +2 -0
- package/dist/lib/commands.js +2 -0
- package/dist/lib/config-keys.d.ts +11 -2
- package/dist/lib/config-keys.js +21 -1
- package/dist/lib/daemon/daemon.js +5 -0
- package/dist/lib/daemon/harness-update-service.d.ts +110 -0
- package/dist/lib/daemon/harness-update-service.js +216 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/device-config.d.ts +0 -1
- package/dist/lib/device-config.js +50 -5
- package/dist/lib/exec.js +26 -2
- package/dist/lib/fs-atomic.d.ts +2 -0
- package/dist/lib/fs-atomic.js +2 -0
- package/dist/lib/hooks/install.js +7 -2
- package/dist/lib/installations/active-check.d.ts +48 -0
- package/dist/lib/installations/active-check.js +84 -0
- package/dist/lib/installations/index.d.ts +5 -1
- package/dist/lib/installations/index.js +4 -0
- package/dist/lib/installations/installation-lock.d.ts +6 -0
- package/dist/lib/installations/installation-lock.js +29 -0
- package/dist/lib/installations/launch-gate.d.ts +69 -0
- package/dist/lib/installations/launch-gate.js +133 -0
- package/dist/lib/installations/native-command.d.ts +5 -0
- package/dist/lib/installations/native-command.js +52 -0
- package/dist/lib/installations/shims.d.ts +8 -2
- package/dist/lib/installations/shims.js +105 -2
- package/dist/lib/installations/store.d.ts +5 -1
- package/dist/lib/installations/store.js +24 -3
- package/dist/lib/installations/strategies.js +55 -35
- package/dist/lib/installations/types.d.ts +17 -0
- package/dist/lib/installations/update-cancellation.d.ts +82 -0
- package/dist/lib/installations/update-cancellation.js +122 -0
- package/dist/lib/installations/update-policy.d.ts +69 -0
- package/dist/lib/installations/update-policy.js +114 -0
- package/dist/lib/installations/update-runtime.d.ts +118 -0
- package/dist/lib/installations/update-runtime.js +321 -0
- package/dist/lib/installations/update.d.ts +25 -0
- package/dist/lib/installations/update.js +141 -2
- package/dist/lib/installations/versions.d.ts +1 -0
- package/dist/lib/installations/versions.js +166 -131
- package/dist/lib/platform/process.d.ts +3 -1
- package/dist/lib/platform/process.js +2 -2
- package/dist/lib/staleness/detectors/commands.d.ts +1 -2
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -2
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -2
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -2
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -7
- package/dist/lib/staleness/detectors/rules.d.ts +1 -2
- package/dist/lib/staleness/detectors/skills.d.ts +1 -2
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -7
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -2
- package/dist/lib/staleness/writers/commands.d.ts +1 -2
- package/dist/lib/staleness/writers/hooks.d.ts +1 -2
- package/dist/lib/staleness/writers/mcp.d.ts +1 -2
- package/dist/lib/staleness/writers/permissions.d.ts +1 -12
- package/dist/lib/staleness/writers/plugins.d.ts +1 -6
- package/dist/lib/staleness/writers/rules.d.ts +1 -2
- package/dist/lib/staleness/writers/skills.d.ts +1 -2
- package/dist/lib/staleness/writers/subagents.d.ts +1 -2
- package/dist/lib/staleness/writers/workflows.d.ts +1 -8
- package/dist/lib/state.d.ts +3 -1
- package/dist/lib/state.js +38 -13
- package/dist/lib/types.d.ts +26 -1
- package/dist/lib/types.js +5 -0
- package/dist/lib/view-types.d.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Daemon harness-update service (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* Runs the automatic-update pass (`installations/update-runtime.ts`) on a
|
|
5
|
+
* schedule so a managed, transactional npm harness (Claude, Codex, …) stays
|
|
6
|
+
* current with no operator action, subject to the `updates.auto` /
|
|
7
|
+
* `updates.<agent>.auto` switches and each installation's own update policy.
|
|
8
|
+
*
|
|
9
|
+
* The pass itself does real, synchronous filesystem work per installation —
|
|
10
|
+
* staging an npm install into a sibling directory, `fs.renameSync`/`fs.cpSync`
|
|
11
|
+
* swaps, `fs.rmSync` cleanup — potentially across several installations in one
|
|
12
|
+
* tick. Running that inline on the daemon's own event loop would stall every
|
|
13
|
+
* other service (secrets broker, browser IPC, the scheduler) for the
|
|
14
|
+
* duration, the same class of problem `self-update-service.ts` solves for the
|
|
15
|
+
* CLI's OWN upgrade by installing into a fresh process it then exits into.
|
|
16
|
+
* Here there is no "exit and let the supervisor restart" option (the daemon
|
|
17
|
+
* itself isn't what's being updated), so instead this tick SPAWNS a bounded
|
|
18
|
+
* child (`agents __harness-update-run`) over a Node IPC channel and only waits
|
|
19
|
+
* on that child — every sync fs call happens in the child's own event loop /
|
|
20
|
+
* thread pool, never this one.
|
|
21
|
+
*
|
|
22
|
+
* Cancellation is COOPERATIVE and cross-platform (PHNX-3940). On the tick's
|
|
23
|
+
* deadline or daemon shutdown — both delivered as the supervisor aborting the
|
|
24
|
+
* tick's `AbortSignal` — the daemon does NOT force-kill the child (execFile's
|
|
25
|
+
* `timeout`/`signal` would, unconditionally on Windows, mid-swap). It SENDS the
|
|
26
|
+
* child an IPC cancel message; the child stops at its next safe boundary (see
|
|
27
|
+
* `installations/update-cancellation.ts` + `update.ts`'s `shouldCancel`) and
|
|
28
|
+
* exits on its own, and the daemon waits for that TRUE exit. A wedged child that
|
|
29
|
+
* ignores the request past a generous grace — never the normal path, since the
|
|
30
|
+
* child's own npm/probe work is bounded — is force-reaped only as an
|
|
31
|
+
* orphan-prevention backstop, and that abnormal case is reported as a failure,
|
|
32
|
+
* never as a clean pass.
|
|
33
|
+
*/
|
|
34
|
+
import { spawn } from 'child_process';
|
|
35
|
+
import { BasePeriodicService } from './service.js';
|
|
36
|
+
import { getCliLaunch, getAgentsBinPath } from '../cli-entry.js';
|
|
37
|
+
import { HARNESS_UPDATE_CHILD_CMD, cancelMessage } from '../installations/update-cancellation.js';
|
|
38
|
+
/** Runs every 15 minutes — a design decision, not an externally-fixed cadence (PHNX-3940). */
|
|
39
|
+
const HARNESS_UPDATE_TICK_MS = 15 * 60_000;
|
|
40
|
+
/**
|
|
41
|
+
* Cancellation deadline per tick. A real pass can touch several installations, each an npm
|
|
42
|
+
* install (`INSTALL_TIMEOUT_MS` = 120s in strategies.ts) plus two launch
|
|
43
|
+
* probes; 10 minutes leaves headroom for a handful of harnesses in one pass
|
|
44
|
+
* while staying comfortably under the 15-minute cadence above.
|
|
45
|
+
*/
|
|
46
|
+
export const HARNESS_UPDATE_DEADLINE_MS = 10 * 60_000;
|
|
47
|
+
/** First tick fires 60 seconds after daemon boot — long enough for shims/PATH to settle, short enough that a fresh box doesn't wait a full interval for its first check. */
|
|
48
|
+
const HARNESS_UPDATE_STARTUP_DELAY_MS = 60_000;
|
|
49
|
+
/**
|
|
50
|
+
* How long to wait for the child to exit AFTER a cooperative cancel before
|
|
51
|
+
* force-reaping it as an orphan backstop. The child's own work is bounded — one
|
|
52
|
+
* npm install (`INSTALL_TIMEOUT_MS` = 120s in strategies.ts) plus two launch
|
|
53
|
+
* probes, then a fast synchronous swap — so a healthy child stops well within
|
|
54
|
+
* this window. A slow staging/postinstall can also exhaust it, but a delivered
|
|
55
|
+
* cancel prevents that stage from committing. The backstop kills the process
|
|
56
|
+
* group on POSIX and the worker on Windows; the tick reports a failure.
|
|
57
|
+
*/
|
|
58
|
+
export const HARNESS_UPDATE_CANCEL_GRACE_MS = 3 * 60_000;
|
|
59
|
+
export function defaultHarnessUpdateDeps() {
|
|
60
|
+
return {
|
|
61
|
+
runAutoUpdatePass(signal) {
|
|
62
|
+
const { command, args } = getCliLaunch([HARNESS_UPDATE_CHILD_CMD], getAgentsBinPath());
|
|
63
|
+
return driveCooperativeChild(command, args, signal, HARNESS_UPDATE_CANCEL_GRACE_MS);
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Spawn `command args` as a child over a Node IPC channel and drive it to a TRUE
|
|
69
|
+
* completion, requesting a cooperative stop (never a kill) when `signal` aborts —
|
|
70
|
+
* the supervisor aborts it on the tick's deadline OR on daemon shutdown.
|
|
71
|
+
*
|
|
72
|
+
* Resolves with the child's real exit code and captured output. Rejects only on:
|
|
73
|
+
* - a spawn failure (missing binary) — a genuine service failure; and
|
|
74
|
+
* - the child having to be force-reaped past the grace window, or dying to an
|
|
75
|
+
* external signal — never a clean pass, so the tick logs it as ERROR instead
|
|
76
|
+
* of reading a killed process as a completed update.
|
|
77
|
+
*
|
|
78
|
+
* A non-zero exit with no kill is resolved (not rejected): the pass exits
|
|
79
|
+
* non-zero for a per-installation vendor error, which is a normal tick outcome.
|
|
80
|
+
*
|
|
81
|
+
* Exported for the real-subprocess cancellation tests.
|
|
82
|
+
*/
|
|
83
|
+
export function driveCooperativeChild(command, args, signal, graceMs, spawnOpts = {}) {
|
|
84
|
+
return new Promise((resolve, reject) => {
|
|
85
|
+
let child;
|
|
86
|
+
try {
|
|
87
|
+
child = spawn(command, args, {
|
|
88
|
+
// stdio[3] = 'ipc' is the cancel channel. Detached on POSIX so the child
|
|
89
|
+
// leads its own process group and the backstop below can reap its whole
|
|
90
|
+
// npm subtree; the parent still waits on it (never unref'd).
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe', 'ipc'],
|
|
92
|
+
detached: process.platform !== 'win32',
|
|
93
|
+
windowsHide: true,
|
|
94
|
+
env: spawnOpts.env,
|
|
95
|
+
cwd: spawnOpts.cwd,
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const MAX = 16 * 1024 * 1024;
|
|
103
|
+
let stdout = '';
|
|
104
|
+
let stderr = '';
|
|
105
|
+
let settled = false;
|
|
106
|
+
let cancelRequested = false;
|
|
107
|
+
let forceReaped = false;
|
|
108
|
+
let graceTimer;
|
|
109
|
+
const cleanup = () => {
|
|
110
|
+
if (graceTimer)
|
|
111
|
+
clearTimeout(graceTimer);
|
|
112
|
+
signal.removeEventListener('abort', requestCancel);
|
|
113
|
+
};
|
|
114
|
+
function requestCancel() {
|
|
115
|
+
if (cancelRequested || settled)
|
|
116
|
+
return;
|
|
117
|
+
cancelRequested = true;
|
|
118
|
+
// Cooperative: a message the child reads at its next safe boundary. NEVER a
|
|
119
|
+
// signal — that would interrupt a swap (and is fatal on Windows, which has
|
|
120
|
+
// no cooperative SIGTERM). If the channel is already gone the child's own
|
|
121
|
+
// `disconnect` handler cancels it, so a failed send is not an error.
|
|
122
|
+
try {
|
|
123
|
+
child.send(cancelMessage(), () => { });
|
|
124
|
+
}
|
|
125
|
+
catch { /* channel closed; disconnect handles it */ }
|
|
126
|
+
graceTimer = setTimeout(() => {
|
|
127
|
+
if (settled)
|
|
128
|
+
return;
|
|
129
|
+
forceReaped = true;
|
|
130
|
+
try {
|
|
131
|
+
if (process.platform !== 'win32' && typeof child.pid === 'number')
|
|
132
|
+
process.kill(-child.pid, 'SIGKILL');
|
|
133
|
+
else
|
|
134
|
+
child.kill('SIGKILL');
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
try {
|
|
138
|
+
child.kill('SIGKILL');
|
|
139
|
+
}
|
|
140
|
+
catch { /* already gone */ }
|
|
141
|
+
}
|
|
142
|
+
}, graceMs);
|
|
143
|
+
graceTimer.unref?.();
|
|
144
|
+
}
|
|
145
|
+
if (signal.aborted)
|
|
146
|
+
requestCancel();
|
|
147
|
+
else
|
|
148
|
+
signal.addEventListener('abort', requestCancel, { once: true });
|
|
149
|
+
child.stdout?.on('data', (d) => { if (stdout.length < MAX)
|
|
150
|
+
stdout += d.toString(); });
|
|
151
|
+
child.stderr?.on('data', (d) => { if (stderr.length < MAX)
|
|
152
|
+
stderr += d.toString(); });
|
|
153
|
+
child.on('error', (err) => {
|
|
154
|
+
if (settled)
|
|
155
|
+
return;
|
|
156
|
+
settled = true;
|
|
157
|
+
cleanup();
|
|
158
|
+
reject(err);
|
|
159
|
+
});
|
|
160
|
+
child.on('close', (code, sigName) => {
|
|
161
|
+
if (settled)
|
|
162
|
+
return;
|
|
163
|
+
settled = true;
|
|
164
|
+
cleanup();
|
|
165
|
+
if (forceReaped || sigName) {
|
|
166
|
+
reject(new Error(`harness-update child did not exit cooperatively after cancel `
|
|
167
|
+
+ `(${forceReaped ? `force-reaped after ${graceMs}ms grace` : `died to ${sigName}`}). `
|
|
168
|
+
+ (stderr.slice(0, 500) || stdout.slice(0, 500) || 'no output')));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
resolve({ exitCode: code, stdout: stdout || stderr, cancelled: cancelRequested });
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Core decision + spawn, shared so a future on-demand trigger (mirroring
|
|
177
|
+
* `triggerSelfUpdateInBackground`) can reuse it. Returns rather than throws —
|
|
178
|
+
* the periodic tick logs the outcome and moves on either way.
|
|
179
|
+
*/
|
|
180
|
+
export async function runHarnessUpdateTick(ctx, signal, deps = defaultHarnessUpdateDeps()) {
|
|
181
|
+
try {
|
|
182
|
+
const { exitCode, stdout, cancelled } = await deps.runAutoUpdatePass(signal);
|
|
183
|
+
if (exitCode !== 0) {
|
|
184
|
+
ctx.log('WARN', `harness-update: pass exited ${exitCode} — see stdout for per-installation errors: ${stdout.slice(0, 2000)}`);
|
|
185
|
+
}
|
|
186
|
+
else if (cancelled) {
|
|
187
|
+
ctx.log('INFO', 'harness-update: pass cancelled at a safe boundary (deadline or daemon shutdown); no work left half-done');
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
ctx.log('INFO', 'harness-update: pass completed');
|
|
191
|
+
}
|
|
192
|
+
return { ran: true, exitCode, stdout, cancelled };
|
|
193
|
+
}
|
|
194
|
+
catch (err) {
|
|
195
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
196
|
+
ctx.log('ERROR', `harness-update: pass failed to run: ${message}`);
|
|
197
|
+
return { ran: false, reason: message };
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
export class HarnessUpdateService extends BasePeriodicService {
|
|
201
|
+
id = 'harness-update';
|
|
202
|
+
intervalMs = HARNESS_UPDATE_TICK_MS;
|
|
203
|
+
deadlineMs = HARNESS_UPDATE_DEADLINE_MS;
|
|
204
|
+
startupDelayMs = HARNESS_UPDATE_STARTUP_DELAY_MS;
|
|
205
|
+
async onStart(_ctx) {
|
|
206
|
+
// No connections/handles to open — each tick spawns its own bounded child.
|
|
207
|
+
}
|
|
208
|
+
async onStop() {
|
|
209
|
+
// The in-flight child (if any) is asked to stop COOPERATIVELY: the supervisor
|
|
210
|
+
// aborts `signal` on the current tick, which `driveCooperativeChild` turns
|
|
211
|
+
// into an IPC cancel message, never a kill. Nothing else to release here.
|
|
212
|
+
}
|
|
213
|
+
async onTick(ctx, signal) {
|
|
214
|
+
await runHarnessUpdateTick(ctx, signal);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* enabled without pulling in the whole daemon lifecycle.
|
|
8
8
|
*/
|
|
9
9
|
/** Every service the daemon can host. IDs are kebab-case and stable. */
|
|
10
|
-
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'keychain-reap' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'session-summarizer';
|
|
10
|
+
export type DaemonServiceId = 'secrets-broker' | 'scheduler' | 'catchup' | 'monitors' | 'browser-ipc' | 'webhook-receiver' | 'self-heal' | 'self-update' | 'keychain-reap' | 'account-state' | 'account-auth' | 'watchdog' | 'device-probe' | 'state-dir-check' | 'session-index' | 'auth-sync' | 'usage-sync' | 'daemon-heartbeat' | 'tmux-reap' | 'browser-task-reap' | 'session-state' | 'session-summarizer' | 'harness-update';
|
|
11
11
|
/** Human-readable metadata for each service. */
|
|
12
12
|
export interface DaemonServiceDef {
|
|
13
13
|
id: DaemonServiceId;
|
|
@@ -122,6 +122,11 @@ export const DAEMON_SERVICES = [
|
|
|
122
122
|
title: 'Usage snapshot sync',
|
|
123
123
|
description: 'Bidirectional per-account usage sync: a headed personal/desktop box pushes its snapshot to worker peers, and a worker with a stale cache pulls from the primary — so workers that cannot read the endpoint themselves still route by real capacity.',
|
|
124
124
|
},
|
|
125
|
+
{
|
|
126
|
+
id: 'harness-update',
|
|
127
|
+
title: 'Harness auto-update',
|
|
128
|
+
description: 'Moves eligible, non-pinned npm-package harness installations (Claude, Codex, …) to their latest release via a bounded child process, subject to updates.auto / updates.<agent>.auto and each installation\'s own update policy (PHNX-3940).',
|
|
129
|
+
},
|
|
125
130
|
];
|
|
126
131
|
/** Stable order of service IDs. */
|
|
127
132
|
export const DAEMON_SERVICE_IDS = DAEMON_SERVICES.map((s) => s.id);
|
|
@@ -91,7 +91,6 @@ declare const DEVICE_ROLES: readonly ["worker", "personal", "desktop"];
|
|
|
91
91
|
/** Which devices automatic placement may pick — see the `auto.pool` key below. */
|
|
92
92
|
declare const AUTO_POOL_MODES: readonly ["workers", "all"];
|
|
93
93
|
export declare const CONFIG_KEYS: readonly ConfigKeySpec[];
|
|
94
|
-
/** Look up a key spec by CLI dotted name, or throw listing the known keys. */
|
|
95
94
|
export declare function configKeySpec(name: string): ConfigKeySpec;
|
|
96
95
|
/**
|
|
97
96
|
* Fold the legacy config/pins stores into the current layout, once per
|
|
@@ -39,6 +39,7 @@ import { atomicWriteFileSync } from './fs-atomic.js';
|
|
|
39
39
|
import { machineId } from './machine-id.js';
|
|
40
40
|
import { assertValidDeviceName, assertRegistrableDeviceName } from './devices/registry.js';
|
|
41
41
|
import { migrateDeviceConfigStores } from './devices/config-migration.js';
|
|
42
|
+
import { isAgentId } from './types.js';
|
|
42
43
|
const DEVICE_PLATFORMS = ['windows', 'linux', 'macos', 'unknown'];
|
|
43
44
|
const SSH_AUTH_METHODS = ['key', 'password'];
|
|
44
45
|
/** Roles a device can be marked with — see the `role` key below. */
|
|
@@ -100,6 +101,18 @@ export const CONFIG_KEYS = [
|
|
|
100
101
|
type: 'string',
|
|
101
102
|
description: 'Model the summarizer requests from summarizer.baseUrl, e.g. qwen2.5:3b. Overridden per-process by AGENTS_SUMMARIZER_MODEL.',
|
|
102
103
|
},
|
|
104
|
+
{
|
|
105
|
+
name: 'updates.auto',
|
|
106
|
+
yamlKey: 'updatesAuto',
|
|
107
|
+
scope: 'user',
|
|
108
|
+
type: 'bool',
|
|
109
|
+
defaultValue: true,
|
|
110
|
+
description: 'Whether the daemon\'s automatic-update pass (PHNX-3940) may move any safely-transactional npm harness ' +
|
|
111
|
+
'(Claude, Codex, …) to its latest release with no operator action. This is the fleet-wide KILL SWITCH: ' +
|
|
112
|
+
'off, no harness auto-updates regardless of any updates.<agent>.auto override. On (the default), each ' +
|
|
113
|
+
"harness's own updates.<agent>.auto refines it. Per-installation pins (agents update … --to <release>) " +
|
|
114
|
+
'apply on top of this either way.',
|
|
115
|
+
},
|
|
103
116
|
{
|
|
104
117
|
name: 'browser.viewer',
|
|
105
118
|
yamlKey: 'browserViewer',
|
|
@@ -333,8 +346,31 @@ export const CONFIG_KEYS = [
|
|
|
333
346
|
},
|
|
334
347
|
];
|
|
335
348
|
/** Look up a key spec by CLI dotted name, or throw listing the known keys. */
|
|
349
|
+
/**
|
|
350
|
+
* Per-harness override of `updates.auto` (PHNX-3940) — `updates.<agent>.auto`,
|
|
351
|
+
* one boolean key per registered agent id. Use the lightweight canonical ID
|
|
352
|
+
* catalog: importing the installation/runtime registry here creates a cycle
|
|
353
|
+
* through the harness adapters and makes ordinary config readers load it all.
|
|
354
|
+
*/
|
|
355
|
+
function dynamicAgentAutoUpdateSpec(name) {
|
|
356
|
+
const match = name.match(/^updates\.(.+)\.auto$/);
|
|
357
|
+
if (!match)
|
|
358
|
+
return null;
|
|
359
|
+
const agent = match[1];
|
|
360
|
+
if (!isAgentId(agent))
|
|
361
|
+
return null;
|
|
362
|
+
return {
|
|
363
|
+
name,
|
|
364
|
+
yamlKey: `updatesAgentAuto.${agent}`,
|
|
365
|
+
scope: 'user',
|
|
366
|
+
type: 'bool',
|
|
367
|
+
description: `Per-harness override of updates.auto for ${agent}. Only takes effect while ` +
|
|
368
|
+
'updates.auto is on (the global switch is a hard kill switch, not a default this can override). Unset ' +
|
|
369
|
+
'defers to updates.auto.',
|
|
370
|
+
};
|
|
371
|
+
}
|
|
336
372
|
export function configKeySpec(name) {
|
|
337
|
-
const spec = CONFIG_KEYS.find((k) => k.name === name);
|
|
373
|
+
const spec = CONFIG_KEYS.find((k) => k.name === name) ?? dynamicAgentAutoUpdateSpec(name);
|
|
338
374
|
if (!spec) {
|
|
339
375
|
throw new Error(`Unknown config key '${name}'. Known keys: ${CONFIG_KEYS.map((k) => k.name).join(', ')}.`);
|
|
340
376
|
}
|
|
@@ -500,12 +536,19 @@ function assertLocalTarget(spec, device) {
|
|
|
500
536
|
}
|
|
501
537
|
/** Get one config key's effective value and the layer that set it. */
|
|
502
538
|
export function getConfigValue(name, opts) {
|
|
503
|
-
ensureDeviceConfigMigrated();
|
|
504
539
|
const spec = configKeySpec(name);
|
|
505
540
|
if (spec.scope === 'user') {
|
|
506
|
-
|
|
541
|
+
// A user-scope key reads purely from central `agents.yaml` (readMeta) and
|
|
542
|
+
// is untouched by the device-config fold, which only relocates DEVICE-scope
|
|
543
|
+
// legacy stores (config-migration.ts) — none of them a user key. So a
|
|
544
|
+
// user-scope read stays a PURE read and MUST NOT trigger the migration
|
|
545
|
+
// write: a read-only `agents update --check` reads the `updates.auto` /
|
|
546
|
+
// `updates.<agent>.auto` policy through here, and migrating disk on that
|
|
547
|
+
// read is exactly the unsolicited startup write --check must not do.
|
|
548
|
+
const value = readMeta({ migrate: false }).config?.[spec.yamlKey];
|
|
507
549
|
return { spec, value, source: value !== undefined ? 'user' : 'default' };
|
|
508
550
|
}
|
|
551
|
+
ensureDeviceConfigMigrated();
|
|
509
552
|
if (opts?.fleet) {
|
|
510
553
|
const value = readFleetConfigDefaults()[spec.yamlKey];
|
|
511
554
|
return { spec, value, source: value !== undefined ? 'fleet' : 'default' };
|
|
@@ -530,12 +573,14 @@ export function getConfigValue(name, opts) {
|
|
|
530
573
|
* after the first process-wide fold — neither blocks the loop meaningfully.
|
|
531
574
|
*/
|
|
532
575
|
export async function getConfigValueAsync(name, opts) {
|
|
533
|
-
ensureDeviceConfigMigrated();
|
|
534
576
|
const spec = configKeySpec(name);
|
|
535
577
|
if (spec.scope === 'user') {
|
|
536
|
-
|
|
578
|
+
// Pure user-scope read — same reasoning as the sync twin: no device-config
|
|
579
|
+
// fold, so a user read never migrates disk.
|
|
580
|
+
const value = readMeta({ migrate: false }).config?.[spec.yamlKey];
|
|
537
581
|
return { spec, value, source: value !== undefined ? 'user' : 'default' };
|
|
538
582
|
}
|
|
583
|
+
ensureDeviceConfigMigrated();
|
|
539
584
|
if (opts?.fleet) {
|
|
540
585
|
const value = readFleetConfigDefaults()[spec.yamlKey];
|
|
541
586
|
return { spec, value, source: value !== undefined ? 'fleet' : 'default' };
|
package/dist/lib/exec.js
CHANGED
|
@@ -20,6 +20,9 @@ import { resolveActor, actorEnv } from './actor.js';
|
|
|
20
20
|
import { expandLocalHome } from './project-root.js';
|
|
21
21
|
import { getShimsDir, getHistoryDir, getRuntimeStateDir } from './state.js';
|
|
22
22
|
import { readCodexConfiguredModel } from './installations/shims.js';
|
|
23
|
+
import { withInstallationLease } from './installations/launch-gate.js';
|
|
24
|
+
import { getCliLaunch, getAgentsBinPath } from './cli-entry.js';
|
|
25
|
+
import { installedReleaseFor } from './installations/store.js';
|
|
23
26
|
import { writePidSessionEntry, extractSessionIdArg } from './session/pid-registry.js';
|
|
24
27
|
import { writeSessionActorRecord, writeSessionAliasRecord } from './session/actor-sidecar.js';
|
|
25
28
|
import { loadHookSessionIndex, resolveHookSessionId } from './session/hook-sessions.js';
|
|
@@ -706,7 +709,7 @@ export function nativeResume(agent, version) {
|
|
|
706
709
|
return false;
|
|
707
710
|
if (!resume.since)
|
|
708
711
|
return true;
|
|
709
|
-
return !!version && compareVersions(version, resume.since) >= 0;
|
|
712
|
+
return !!version && compareVersions(installedReleaseFor(agent, version), resume.since) >= 0;
|
|
710
713
|
}
|
|
711
714
|
/**
|
|
712
715
|
* Build the `-c` value that adds `dir` to codex's workspace-write writable
|
|
@@ -1111,6 +1114,12 @@ export function resolveShimSpawn(platform, binary, extraArgs) {
|
|
|
1111
1114
|
* keeping version resolution in one place instead of reimplementing it in batch.
|
|
1112
1115
|
*/
|
|
1113
1116
|
export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
1117
|
+
const version = pinnedVersion ?? resolveVersion(agent, cwd) ?? undefined;
|
|
1118
|
+
if (!version || !isVersionInstalled(agent, version))
|
|
1119
|
+
return execShimPassthroughLeased(agent, rawArgs, cwd, version);
|
|
1120
|
+
return withInstallationLease(agent, version, () => execShimPassthroughLeased(agent, rawArgs, cwd, version));
|
|
1121
|
+
}
|
|
1122
|
+
async function execShimPassthroughLeased(agent, rawArgs, cwd, pinnedVersion) {
|
|
1114
1123
|
const version = pinnedVersion ?? resolveVersion(agent, cwd) ?? undefined;
|
|
1115
1124
|
if (!version || !isVersionInstalled(agent, version)) {
|
|
1116
1125
|
process.stderr.write(`agents: no installed default for ${agent}. Set one with: agents use ${agent} <version>\n`);
|
|
@@ -1153,6 +1162,8 @@ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
|
|
|
1153
1162
|
// signed-in verdict from the pinned version home (no rotated pick here).
|
|
1154
1163
|
await emitRunLaunch({ agent, version, resolvedVia: 'shim' });
|
|
1155
1164
|
return new Promise((resolve) => {
|
|
1165
|
+
// Register listeners in the same synchronous turn as spawn: awaiting a lock
|
|
1166
|
+
// release after spawn can miss a fast child's exit/error event.
|
|
1156
1167
|
const child = spawn(command, args, { cwd, stdio: 'inherit', env, shell });
|
|
1157
1168
|
// Record the launch so `ag sessions --active` can attribute the agent
|
|
1158
1169
|
// process to its cwd (and exact session when the caller passed
|
|
@@ -1507,7 +1518,14 @@ async function runInTmux(options, executable, args) {
|
|
|
1507
1518
|
// which never carried real values anyway (RUSH-1758).
|
|
1508
1519
|
const envFile = path.join(getRuntimeStateDir(), 'tmux-env', `${name}-${randomUUID().slice(0, 8)}.env`);
|
|
1509
1520
|
writeTmuxEnvFile(execEnv, envFile);
|
|
1510
|
-
|
|
1521
|
+
let cmd = buildTmuxAgentCommand(executable, args, execEnv, { envFile });
|
|
1522
|
+
const leaseVersion = options.version ?? resolveVersion(options.agent, cwd);
|
|
1523
|
+
if (leaseVersion && isVersionInstalled(options.agent, leaseVersion)) {
|
|
1524
|
+
const leaseCli = getCliLaunch(['__launch-lease', options.agent, leaseVersion], getAgentsBinPath());
|
|
1525
|
+
// The pane outlives a detached launcher. Lease its own exec-replaced shell
|
|
1526
|
+
// before launching, so detaching cannot open an unprotected launch gap.
|
|
1527
|
+
cmd = `${[leaseCli.command, ...leaseCli.args].map(shellQuote).join(' ')} "$$" || exit 1; ${cmd}`;
|
|
1528
|
+
}
|
|
1511
1529
|
const metaCmd = buildTmuxAgentCommand(executable, args, execEnv, { redactEnvValues: true });
|
|
1512
1530
|
const labels = { agent: options.agent };
|
|
1513
1531
|
// Only publish an id the harness actually received. createSession turns this
|
|
@@ -1719,6 +1737,12 @@ async function emitRunLaunch(ctx) {
|
|
|
1719
1737
|
}
|
|
1720
1738
|
}
|
|
1721
1739
|
async function spawnAgent(options) {
|
|
1740
|
+
const version = options.version ?? resolveVersion(options.agent, options.cwd || process.cwd());
|
|
1741
|
+
if (!version || !isVersionInstalled(options.agent, version))
|
|
1742
|
+
return spawnAgentLeased(options);
|
|
1743
|
+
return withInstallationLease(options.agent, version, () => spawnAgentLeased(options));
|
|
1744
|
+
}
|
|
1745
|
+
async function spawnAgentLeased(options) {
|
|
1722
1746
|
bootMark('spawn-agent:enter');
|
|
1723
1747
|
// Assign a known session id up front for agents that accept one, so the
|
|
1724
1748
|
// launcher can record an EXACT pid -> session mapping (see pid-registry) —
|
package/dist/lib/fs-atomic.d.ts
CHANGED
|
@@ -40,6 +40,8 @@ export declare function atomicWriteJsonSync(filePath: string, data: unknown): vo
|
|
|
40
40
|
export interface FileLockOptions {
|
|
41
41
|
staleMs?: number;
|
|
42
42
|
acquireTimeoutMs?: number;
|
|
43
|
+
/** Lock a canonical absolute path before its file exists (e.g. a new installation record). */
|
|
44
|
+
realpath?: boolean;
|
|
43
45
|
}
|
|
44
46
|
export declare function withFileLock<T>(filePath: string, fn: (heartbeat: () => void) => T, opts?: FileLockOptions): T;
|
|
45
47
|
/**
|
package/dist/lib/fs-atomic.js
CHANGED
|
@@ -79,6 +79,7 @@ export function withFileLock(filePath, fn, opts = {}) {
|
|
|
79
79
|
try {
|
|
80
80
|
release = lockfile.lockSync(filePath, {
|
|
81
81
|
stale: staleMs,
|
|
82
|
+
realpath: opts.realpath ?? true,
|
|
82
83
|
onCompromised: (err) => { compromised = err; },
|
|
83
84
|
});
|
|
84
85
|
break;
|
|
@@ -147,6 +148,7 @@ export async function withFileLockAsync(filePath, fn, opts = {}) {
|
|
|
147
148
|
try {
|
|
148
149
|
release = await lockfile.lock(filePath, {
|
|
149
150
|
stale: staleMs,
|
|
151
|
+
realpath: opts.realpath ?? true,
|
|
150
152
|
onCompromised: (err) => { compromised = err; },
|
|
151
153
|
});
|
|
152
154
|
break;
|
|
@@ -3284,7 +3284,7 @@ export async function installSessionTrackerHook(agent, version, home) {
|
|
|
3284
3284
|
}
|
|
3285
3285
|
try {
|
|
3286
3286
|
await execFileAsync(invocation.command, invocation.args, {
|
|
3287
|
-
env:
|
|
3287
|
+
env: sessionTrackerInstallEnv(agent, version, home),
|
|
3288
3288
|
encoding: 'utf8',
|
|
3289
3289
|
});
|
|
3290
3290
|
return { installed: true };
|
|
@@ -3326,7 +3326,7 @@ export function installSessionTrackerHookSync(agent, version, home) {
|
|
|
3326
3326
|
}
|
|
3327
3327
|
try {
|
|
3328
3328
|
execFileSync(invocation.command, invocation.args, {
|
|
3329
|
-
env:
|
|
3329
|
+
env: sessionTrackerInstallEnv(agent, version, home),
|
|
3330
3330
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
3331
3331
|
encoding: 'utf8',
|
|
3332
3332
|
});
|
|
@@ -3336,3 +3336,8 @@ export function installSessionTrackerHookSync(agent, version, home) {
|
|
|
3336
3336
|
return { installed: false, error: installFailureMessage(err) };
|
|
3337
3337
|
}
|
|
3338
3338
|
}
|
|
3339
|
+
/** The hook installer must target this account, not the current global home. */
|
|
3340
|
+
function sessionTrackerInstallEnv(agent, version, home) {
|
|
3341
|
+
const target = home ?? (version ? getVersionHomePath(agent, version) : undefined);
|
|
3342
|
+
return target ? { ...process.env, HOME: target, USERPROFILE: target } : { ...process.env };
|
|
3343
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-process-state "is this installation busy right now?" check (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* A leaf module deliberately kept free of `update.js`/`update-runtime.js`
|
|
5
|
+
* imports so both can depend on it without an import cycle: `update.ts` needs
|
|
6
|
+
* it for the pre-commit re-check (narrowing the launch/update race), and
|
|
7
|
+
* `update-runtime.ts` (which itself calls into `update.ts`) needs it for the
|
|
8
|
+
* plan-time deferral decision.
|
|
9
|
+
*
|
|
10
|
+
* Two independent signals, either one is enough to defer:
|
|
11
|
+
* - A live OS process whose command line names this installation's own
|
|
12
|
+
* version-dir path — a real process-table scan, not this box's session
|
|
13
|
+
* registry, so a harness launched by a bare generated shim with no
|
|
14
|
+
* session bookkeeping still defers correctly.
|
|
15
|
+
* - A live launch lease (`shims.ts`) — catches a launch that started after
|
|
16
|
+
* the process-table scan ran but hasn't hit the process table yet, e.g.
|
|
17
|
+
* mid-staging of a long npm install. See `shims.ts`'s docblock for what
|
|
18
|
+
* this does and does not close (it narrows the race, not eliminates it).
|
|
19
|
+
*/
|
|
20
|
+
import type { Installation } from './types.js';
|
|
21
|
+
/**
|
|
22
|
+
* Raw process-table snapshot, one command line per entry. Injectable so tests
|
|
23
|
+
* can drive real string matching without shelling out or requiring a live
|
|
24
|
+
* agent process on the test box.
|
|
25
|
+
*/
|
|
26
|
+
export interface ProcessSnapshot {
|
|
27
|
+
listCommandLines(): Promise<string[]>;
|
|
28
|
+
}
|
|
29
|
+
export declare const realProcessSnapshot: ProcessSnapshot;
|
|
30
|
+
/**
|
|
31
|
+
* Does any live process reference this installation's own directory? Matches
|
|
32
|
+
* on the absolute version-dir path rather than the bare CLI command name, so
|
|
33
|
+
* it distinguishes between installations of the same agent (two Claude
|
|
34
|
+
* installs are two different directories) and catches every way the binary
|
|
35
|
+
* ends up running under that path: an `agents run` session, a bare generated
|
|
36
|
+
* PATH/`.cmd` shim exec'd directly with no session bookkeeping at all, or a
|
|
37
|
+
* routine/teammate process — all of them carry the resolved absolute binary
|
|
38
|
+
* path in their command line, since none of the launch surfaces exec a bare
|
|
39
|
+
* relative name.
|
|
40
|
+
*/
|
|
41
|
+
export declare function installationLooksActive(installation: Pick<Installation, 'agent' | 'label'>, commandLines: string[]): boolean;
|
|
42
|
+
/**
|
|
43
|
+
* Whether this installation appears to be busy right now: a live launch
|
|
44
|
+
* lease, or a live process naming its directory per a fresh OS process-table
|
|
45
|
+
* scan. On a scan failure, defers (returns true) rather than risking an
|
|
46
|
+
* update to a harness this check simply failed to observe.
|
|
47
|
+
*/
|
|
48
|
+
export declare function isInstallationLikelyActive(installation: Pick<Installation, 'agent' | 'label'>, snapshot?: ProcessSnapshot): Promise<boolean>;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Real-process-state "is this installation busy right now?" check (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* A leaf module deliberately kept free of `update.js`/`update-runtime.js`
|
|
5
|
+
* imports so both can depend on it without an import cycle: `update.ts` needs
|
|
6
|
+
* it for the pre-commit re-check (narrowing the launch/update race), and
|
|
7
|
+
* `update-runtime.ts` (which itself calls into `update.ts`) needs it for the
|
|
8
|
+
* plan-time deferral decision.
|
|
9
|
+
*
|
|
10
|
+
* Two independent signals, either one is enough to defer:
|
|
11
|
+
* - A live OS process whose command line names this installation's own
|
|
12
|
+
* version-dir path — a real process-table scan, not this box's session
|
|
13
|
+
* registry, so a harness launched by a bare generated shim with no
|
|
14
|
+
* session bookkeeping still defers correctly.
|
|
15
|
+
* - A live launch lease (`shims.ts`) — catches a launch that started after
|
|
16
|
+
* the process-table scan ran but hasn't hit the process table yet, e.g.
|
|
17
|
+
* mid-staging of a long npm install. See `shims.ts`'s docblock for what
|
|
18
|
+
* this does and does not close (it narrows the race, not eliminates it).
|
|
19
|
+
*/
|
|
20
|
+
import { execFile } from 'child_process';
|
|
21
|
+
import { promisify } from 'util';
|
|
22
|
+
import { getVersionDir } from './store.js';
|
|
23
|
+
import { hasLiveLaunchLease } from './shims.js';
|
|
24
|
+
const execFileAsync = promisify(execFile);
|
|
25
|
+
async function listCommandLinesPosix() {
|
|
26
|
+
try {
|
|
27
|
+
const { stdout } = await execFileAsync('ps', ['-Ao', 'args'], {
|
|
28
|
+
timeout: 5_000,
|
|
29
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
30
|
+
});
|
|
31
|
+
return stdout.split('\n');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// A ps failure must not silently mean "nothing is running" — that would let
|
|
35
|
+
// an update proceed against a harness this pass simply failed to observe.
|
|
36
|
+
// Callers treat a thrown scan as "assume active", the safe default.
|
|
37
|
+
throw new Error('could not read the process table (ps failed)');
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
async function listCommandLinesWindows() {
|
|
41
|
+
try {
|
|
42
|
+
const { stdout } = await execFileAsync('powershell.exe', ['-NoLogo', '-NoProfile', '-NonInteractive', '-Command',
|
|
43
|
+
"$ErrorActionPreference = 'Stop'; Get-CimInstance Win32_Process | ForEach-Object { $_.CommandLine }"], { timeout: 5_000, maxBuffer: 16 * 1024 * 1024, windowsHide: true });
|
|
44
|
+
return stdout.split(/\r?\n/);
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
throw new Error('could not read the process table (PowerShell CIM query failed)');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
export const realProcessSnapshot = {
|
|
51
|
+
listCommandLines: () => (process.platform === 'win32' ? listCommandLinesWindows() : listCommandLinesPosix()),
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Does any live process reference this installation's own directory? Matches
|
|
55
|
+
* on the absolute version-dir path rather than the bare CLI command name, so
|
|
56
|
+
* it distinguishes between installations of the same agent (two Claude
|
|
57
|
+
* installs are two different directories) and catches every way the binary
|
|
58
|
+
* ends up running under that path: an `agents run` session, a bare generated
|
|
59
|
+
* PATH/`.cmd` shim exec'd directly with no session bookkeeping at all, or a
|
|
60
|
+
* routine/teammate process — all of them carry the resolved absolute binary
|
|
61
|
+
* path in their command line, since none of the launch surfaces exec a bare
|
|
62
|
+
* relative name.
|
|
63
|
+
*/
|
|
64
|
+
export function installationLooksActive(installation, commandLines) {
|
|
65
|
+
const versionDir = getVersionDir(installation.agent, installation.label);
|
|
66
|
+
return commandLines.some((line) => line.includes(versionDir));
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* Whether this installation appears to be busy right now: a live launch
|
|
70
|
+
* lease, or a live process naming its directory per a fresh OS process-table
|
|
71
|
+
* scan. On a scan failure, defers (returns true) rather than risking an
|
|
72
|
+
* update to a harness this check simply failed to observe.
|
|
73
|
+
*/
|
|
74
|
+
export async function isInstallationLikelyActive(installation, snapshot = realProcessSnapshot) {
|
|
75
|
+
if (hasLiveLaunchLease(installation.agent, installation.label))
|
|
76
|
+
return true;
|
|
77
|
+
try {
|
|
78
|
+
const lines = await snapshot.listCommandLines();
|
|
79
|
+
return installationLooksActive(installation, lines);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
@@ -7,8 +7,12 @@
|
|
|
7
7
|
* it, so the internal split can change without breaking callers (the Cursor
|
|
8
8
|
* per-installation isolation track consumes exactly this).
|
|
9
9
|
*/
|
|
10
|
-
export { type Installation, type InstallationRelease, type UpdateOutcome, type UpdateStrategyId, } from './types.js';
|
|
10
|
+
export { type Installation, type InstallationRelease, type UpdateOutcome, type UpdateStrategyId, type UpdatePolicy, } from './types.js';
|
|
11
11
|
export { createInstallation, listInstallations, recordRelease, } from './store.js';
|
|
12
12
|
export { describeInstallation, resolveInstallation, type ResolveInstallationOptions, } from './resolve.js';
|
|
13
13
|
export { selectUpdateStrategy, supportsPinnedUpdate, type UpdateStrategy, } from './strategies.js';
|
|
14
14
|
export { updateInstallation, type UpdateInstallationOptions } from './update.js';
|
|
15
|
+
export { effectiveUpdatePolicy, isAutoUpdateEnabledForAgent, isGlobalAutoUpdateEnabled, rawAgentAutoUpdateSetting, rawGlobalAutoUpdateSetting, setAgentAutoUpdateEnabled, setGlobalAutoUpdateEnabled, setInstallationUpdatePolicy, unsetAgentAutoUpdateEnabled, unsetGlobalAutoUpdateEnabled, } from './update-policy.js';
|
|
16
|
+
export { planAutoUpdates, runAutoUpdatePass, listInstallationSnapshots, type AutoUpdatePlanEntry, type AutoUpdatePassOutcome, type AutoUpdatePassOptions, type AutoUpdatePassResult, } from './update-runtime.js';
|
|
17
|
+
export { installationLooksActive, isInstallationLikelyActive, realProcessSnapshot, type ProcessSnapshot, } from './active-check.js';
|
|
18
|
+
export { recordLaunchLease, hasLiveLaunchLease } from './shims.js';
|
|
@@ -11,3 +11,7 @@ export { createInstallation, listInstallations, recordRelease, } from './store.j
|
|
|
11
11
|
export { describeInstallation, resolveInstallation, } from './resolve.js';
|
|
12
12
|
export { selectUpdateStrategy, supportsPinnedUpdate, } from './strategies.js';
|
|
13
13
|
export { updateInstallation } from './update.js';
|
|
14
|
+
export { effectiveUpdatePolicy, isAutoUpdateEnabledForAgent, isGlobalAutoUpdateEnabled, rawAgentAutoUpdateSetting, rawGlobalAutoUpdateSetting, setAgentAutoUpdateEnabled, setGlobalAutoUpdateEnabled, setInstallationUpdatePolicy, unsetAgentAutoUpdateEnabled, unsetGlobalAutoUpdateEnabled, } from './update-policy.js';
|
|
15
|
+
export { planAutoUpdates, runAutoUpdatePass, listInstallationSnapshots, } from './update-runtime.js';
|
|
16
|
+
export { installationLooksActive, isInstallationLikelyActive, realProcessSnapshot, } from './active-check.js';
|
|
17
|
+
export { recordLaunchLease, hasLiveLaunchLease } from './shims.js';
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type FileLockOptions } from '../fs-atomic.js';
|
|
2
|
+
import { type AgentId } from '../types.js';
|
|
3
|
+
export declare function installationLockTarget(agent: AgentId, label: string): string;
|
|
4
|
+
export declare const INSTALLATION_LOCK_STALE_MS: number;
|
|
5
|
+
export declare const INSTALLATION_LOCK_ACQUIRE_TIMEOUT_MS: number;
|
|
6
|
+
export declare const INSTALLATION_LOCK_OPTIONS: Required<FileLockOptions>;
|