@phnx-labs/agents-cli 1.22.78 → 1.22.80
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 +15 -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 +69 -3
- package/dist/commands/view.js +235 -75
- 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
|
@@ -17,19 +17,10 @@ function runId() {
|
|
|
17
17
|
}
|
|
18
18
|
function moveDir(from, to) {
|
|
19
19
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
// Windows refuses a rename while any file in the tree is open, and a
|
|
25
|
-
// cross-device staging dir cannot be renamed at all. Copy+remove is the same
|
|
26
|
-
// observable move; it is slower, so it is the fallback, not the default.
|
|
27
|
-
const code = err.code;
|
|
28
|
-
if (code !== 'EPERM' && code !== 'EACCES' && code !== 'EXDEV')
|
|
29
|
-
throw err;
|
|
30
|
-
fs.cpSync(from, to, { recursive: true });
|
|
31
|
-
fs.rmSync(from, { recursive: true, force: true });
|
|
32
|
-
}
|
|
20
|
+
// Staging and rollback are siblings on the installation filesystem. Never
|
|
21
|
+
// turn an atomic rename failure into a partially successful copy-and-delete:
|
|
22
|
+
// an open Windows file or unexpected mount must leave the source intact.
|
|
23
|
+
fs.renameSync(from, to);
|
|
33
24
|
}
|
|
34
25
|
/**
|
|
35
26
|
* Entries a swap replaces: everything npm owns inside a version dir. The lockfile
|
|
@@ -101,32 +92,61 @@ const npmPackageStrategy = {
|
|
|
101
92
|
const dir = installationDir(ctx.agent, ctx.installation.label);
|
|
102
93
|
const rollbackDir = path.join(dir, `.rollback-${runId()}`);
|
|
103
94
|
const displaced = [];
|
|
104
|
-
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
continue;
|
|
112
|
-
moveDir(live, path.join(rollbackDir, entry));
|
|
113
|
-
displaced.push(entry);
|
|
114
|
-
}
|
|
115
|
-
for (const entry of NPM_LIVE_ENTRIES) {
|
|
116
|
-
const from = path.join(staged.stagingDir, entry);
|
|
117
|
-
if (fs.existsSync(from))
|
|
118
|
-
moveDir(from, path.join(dir, entry));
|
|
119
|
-
}
|
|
120
|
-
return {
|
|
121
|
-
undo: () => {
|
|
122
|
-
for (const entry of NPM_LIVE_ENTRIES) {
|
|
95
|
+
const stagedIn = [];
|
|
96
|
+
// Restore each displaced entry. If restoration itself fails, retain every
|
|
97
|
+
// remaining backup and surface its path; never erase the only good copy.
|
|
98
|
+
const restorePreCommitState = () => {
|
|
99
|
+
const errors = [];
|
|
100
|
+
for (const entry of [...stagedIn].reverse()) {
|
|
101
|
+
try {
|
|
123
102
|
fs.rmSync(path.join(dir, entry), { recursive: true, force: true });
|
|
124
103
|
}
|
|
125
|
-
|
|
104
|
+
catch (err) {
|
|
105
|
+
errors.push(`${entry}: ${err.message}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const entry of [...displaced].reverse()) {
|
|
109
|
+
try {
|
|
126
110
|
moveDir(path.join(rollbackDir, entry), path.join(dir, entry));
|
|
127
111
|
}
|
|
128
|
-
|
|
129
|
-
|
|
112
|
+
catch (err) {
|
|
113
|
+
errors.push(`${entry}: ${err.message}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (errors.length)
|
|
117
|
+
throw new Error(`Rollback incomplete; recovery files retained at ${rollbackDir}: ${errors.join('; ')}`);
|
|
118
|
+
fs.rmSync(rollbackDir, { recursive: true, force: true });
|
|
119
|
+
};
|
|
120
|
+
try {
|
|
121
|
+
// Move the live tree aside first, then move the staged tree in. Doing it
|
|
122
|
+
// in this order means the failure window contains no half-merged tree on
|
|
123
|
+
// its own: either the old entries are all aside (restorable) or the new
|
|
124
|
+
// ones are all in place — the try/catch below is what makes a throw
|
|
125
|
+
// BETWEEN those two loops (or partway through the second one) recover
|
|
126
|
+
// to the pre-commit state instead of leaving whichever entries already
|
|
127
|
+
// moved exactly where the exception left them.
|
|
128
|
+
for (const entry of NPM_LIVE_ENTRIES) {
|
|
129
|
+
const live = path.join(dir, entry);
|
|
130
|
+
if (!fs.existsSync(live))
|
|
131
|
+
continue;
|
|
132
|
+
moveDir(live, path.join(rollbackDir, entry));
|
|
133
|
+
displaced.push(entry);
|
|
134
|
+
}
|
|
135
|
+
for (const entry of NPM_LIVE_ENTRIES) {
|
|
136
|
+
const from = path.join(staged.stagingDir, entry);
|
|
137
|
+
if (fs.existsSync(from)) {
|
|
138
|
+
moveDir(from, path.join(dir, entry));
|
|
139
|
+
stagedIn.push(entry);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
restorePreCommitState();
|
|
145
|
+
throw new Error(`${err.message} The swap was interrupted partway through and has been rolled back to `
|
|
146
|
+
+ `${ctx.installation.releaseVersion}.`);
|
|
147
|
+
}
|
|
148
|
+
return {
|
|
149
|
+
undo: restorePreCommitState,
|
|
130
150
|
finalize: () => fs.rmSync(rollbackDir, { recursive: true, force: true }),
|
|
131
151
|
};
|
|
132
152
|
},
|
|
@@ -50,7 +50,22 @@ export interface Installation {
|
|
|
50
50
|
updatedAt: string;
|
|
51
51
|
/** Newest last. Always non-empty: creation seeds it with the first release. */
|
|
52
52
|
history: InstallationRelease[];
|
|
53
|
+
/**
|
|
54
|
+
* How the automatic-update pass (`installations/update-runtime.ts`) treats
|
|
55
|
+
* this installation. `'latest'` (the default) lets it ride the automatic
|
|
56
|
+
* pass; `'pinned'` excludes it — set implicitly by `agents update
|
|
57
|
+
* <agent>@<label> --to <concrete-release>` and cleared by `--to latest`.
|
|
58
|
+
*
|
|
59
|
+
* Absent means `'latest'`: every installation created before this field
|
|
60
|
+
* existed is legacy data, not an opt-out, so a missing key must resolve the
|
|
61
|
+
* same as an explicit `'latest'` rather than being treated as unset/invalid.
|
|
62
|
+
* Read through {@link effectiveUpdatePolicy} in `update-policy.ts` — never
|
|
63
|
+
* compare this field directly, so that default stays in one place.
|
|
64
|
+
*/
|
|
65
|
+
updatePolicy?: UpdatePolicy;
|
|
53
66
|
}
|
|
67
|
+
/** See {@link Installation.updatePolicy}. */
|
|
68
|
+
export type UpdatePolicy = 'latest' | 'pinned';
|
|
54
69
|
/**
|
|
55
70
|
* How an installation's release is replaced. Selected from the agent registry's
|
|
56
71
|
* capabilities, never from an agent id — see `selectUpdateStrategy`.
|
|
@@ -70,6 +85,8 @@ export interface UpdateOutcome {
|
|
|
70
85
|
toRelease: string;
|
|
71
86
|
/** True when the resolved target already matched the installed release. */
|
|
72
87
|
unchanged: boolean;
|
|
88
|
+
/** No swap occurred because activity, cancellation, or policy prevented it. */
|
|
89
|
+
deferred?: string;
|
|
73
90
|
/**
|
|
74
91
|
* Installations other than the target whose recorded release also moved,
|
|
75
92
|
* because the strategy replaced a binary they share (global-binary only).
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform cooperative cancellation for the harness auto-update pass
|
|
3
|
+
* (PHNX-3940).
|
|
4
|
+
*
|
|
5
|
+
* The pass mutates the filesystem transactionally — stage an npm install into a
|
|
6
|
+
* sibling dir, launch-probe it, swap it in, record the new release, drop the
|
|
7
|
+
* rollback material (see `update.ts`). Interrupting it mid-swap can leave an
|
|
8
|
+
* installation whose directory and its own metadata disagree, with no way back.
|
|
9
|
+
* So it must be cancellable from OUTSIDE the process — on the daemon's per-tick
|
|
10
|
+
* deadline, or on daemon shutdown — WITHOUT terminating a process that is mid-swap.
|
|
11
|
+
*
|
|
12
|
+
* `execFile`'s `timeout`/`AbortSignal` do exactly the wrong thing here: they
|
|
13
|
+
* FORCE-terminate the child. On Windows that is unconditional (there are no POSIX
|
|
14
|
+
* signals to cooperate with — the SIGTERM listener the pass installs never runs),
|
|
15
|
+
* so a Windows daemon would kill its own update worker mid-rename. Even on POSIX
|
|
16
|
+
* the forced kill races the swap.
|
|
17
|
+
*
|
|
18
|
+
* The fix is a request, not a kill: the daemon spawns the pass as a child over a
|
|
19
|
+
* Node IPC channel and asks it to stop by SENDING A MESSAGE. The child flips a
|
|
20
|
+
* flag that `update.ts`'s existing `shouldCancel` reads at each safe boundary
|
|
21
|
+
* (before the next installation, before staging, and immediately before commit)
|
|
22
|
+
* and finishes what it is doing cleanly. The channel closing (parent gone) and a
|
|
23
|
+
* received SIGTERM/SIGINT are treated as the same request, so a force-killed
|
|
24
|
+
* daemon can never strand a child mutating forever, and a manual `agents update
|
|
25
|
+
* --auto` interrupted with Ctrl+C also stops at a boundary rather than mid-swap.
|
|
26
|
+
*
|
|
27
|
+
* This module is a dependency-free LEAF on purpose: `index.ts` reads the guard
|
|
28
|
+
* depth below through the shared {@link GUARDED_AUTO_UPDATE_SYMBOL} registry key
|
|
29
|
+
* (never importing this module, so the slim CLI shell stays slim), and the daemon
|
|
30
|
+
* child fixture in tests imports {@link withGuardedUpdateCancellation} without
|
|
31
|
+
* pulling the installations graph.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The hidden verb the daemon spawns to run one auto-update pass with IPC-driven
|
|
35
|
+
* cancellation (dispatched in `index.ts`, handled by
|
|
36
|
+
* `update-runtime.ts`'s `runHarnessUpdateChild`). Internal protocol — not a
|
|
37
|
+
* public command.
|
|
38
|
+
*/
|
|
39
|
+
export declare const HARNESS_UPDATE_CHILD_CMD = "__harness-update-run";
|
|
40
|
+
/** IPC message `type` the daemon sends to request a cooperative stop. */
|
|
41
|
+
export declare const HARNESS_UPDATE_CANCEL_MSG = "harness-update:cancel";
|
|
42
|
+
/**
|
|
43
|
+
* Well-known cross-realm key naming the count of guarded auto-update passes
|
|
44
|
+
* running IN THIS PROCESS right now. `index.ts`'s top-level SIGINT handler reads
|
|
45
|
+
* it via `Symbol.for(...)` WITHOUT importing this module (keeping the slim entry
|
|
46
|
+
* shell free of a heavy static import — see `agent.test.ts`'s import guard), so
|
|
47
|
+
* the guard state has to live somewhere both can reach: the global symbol
|
|
48
|
+
* registry.
|
|
49
|
+
*/
|
|
50
|
+
export declare const GUARDED_AUTO_UPDATE_SYMBOL: unique symbol;
|
|
51
|
+
/** The IPC payload the daemon sends; `child.send(cancelMessage())`. */
|
|
52
|
+
export declare function cancelMessage(): {
|
|
53
|
+
type: typeof HARNESS_UPDATE_CANCEL_MSG;
|
|
54
|
+
};
|
|
55
|
+
/**
|
|
56
|
+
* True while a guarded auto-update pass is mutating this process. Read by
|
|
57
|
+
* `index.ts`'s SIGINT handler (which reads the same registry symbol directly) to
|
|
58
|
+
* DEFER its default `process.exit(130)` so a Ctrl+C can't tear a swap apart; the
|
|
59
|
+
* pass still observes the SIGINT cooperatively and stops at its next boundary.
|
|
60
|
+
* Ref-counted (a depth, not a boolean) so it is correct even if a pass ever nests.
|
|
61
|
+
*/
|
|
62
|
+
export declare function isGuardedAutoUpdateActive(): boolean;
|
|
63
|
+
/**
|
|
64
|
+
* Run `run(cancelled)` with cooperative cancellation wired from every source
|
|
65
|
+
* that can reach this process, and the SIGINT guard held for its duration:
|
|
66
|
+
*
|
|
67
|
+
* - **IPC message** `{ type: HARNESS_UPDATE_CANCEL_MSG }` — the primary,
|
|
68
|
+
* cross-platform request the daemon sends. Works on Windows, where a signal
|
|
69
|
+
* would force-kill.
|
|
70
|
+
* - **`disconnect`** — the IPC channel closed (the daemon went away). Stop
|
|
71
|
+
* rather than continue mutating orphaned.
|
|
72
|
+
* - **SIGTERM / SIGINT** — a direct `kill`/Ctrl+C on this process. Cooperative,
|
|
73
|
+
* not fatal; paired with `index.ts`'s guard, which defers its hard exit while
|
|
74
|
+
* {@link isGuardedAutoUpdateActive} holds.
|
|
75
|
+
*
|
|
76
|
+
* `cancelled()` never un-sets once set. Listeners are installed for the call's
|
|
77
|
+
* lifetime only and removed in `finally`, and the process-wide `message`/
|
|
78
|
+
* `disconnect` listeners are harmless when there is no IPC channel (they simply
|
|
79
|
+
* never fire), so this is safe on the in-process manual `agents update --auto`
|
|
80
|
+
* path too.
|
|
81
|
+
*/
|
|
82
|
+
export declare function withGuardedUpdateCancellation<T>(run: (cancelled: () => boolean) => Promise<T>): Promise<T>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cross-platform cooperative cancellation for the harness auto-update pass
|
|
3
|
+
* (PHNX-3940).
|
|
4
|
+
*
|
|
5
|
+
* The pass mutates the filesystem transactionally — stage an npm install into a
|
|
6
|
+
* sibling dir, launch-probe it, swap it in, record the new release, drop the
|
|
7
|
+
* rollback material (see `update.ts`). Interrupting it mid-swap can leave an
|
|
8
|
+
* installation whose directory and its own metadata disagree, with no way back.
|
|
9
|
+
* So it must be cancellable from OUTSIDE the process — on the daemon's per-tick
|
|
10
|
+
* deadline, or on daemon shutdown — WITHOUT terminating a process that is mid-swap.
|
|
11
|
+
*
|
|
12
|
+
* `execFile`'s `timeout`/`AbortSignal` do exactly the wrong thing here: they
|
|
13
|
+
* FORCE-terminate the child. On Windows that is unconditional (there are no POSIX
|
|
14
|
+
* signals to cooperate with — the SIGTERM listener the pass installs never runs),
|
|
15
|
+
* so a Windows daemon would kill its own update worker mid-rename. Even on POSIX
|
|
16
|
+
* the forced kill races the swap.
|
|
17
|
+
*
|
|
18
|
+
* The fix is a request, not a kill: the daemon spawns the pass as a child over a
|
|
19
|
+
* Node IPC channel and asks it to stop by SENDING A MESSAGE. The child flips a
|
|
20
|
+
* flag that `update.ts`'s existing `shouldCancel` reads at each safe boundary
|
|
21
|
+
* (before the next installation, before staging, and immediately before commit)
|
|
22
|
+
* and finishes what it is doing cleanly. The channel closing (parent gone) and a
|
|
23
|
+
* received SIGTERM/SIGINT are treated as the same request, so a force-killed
|
|
24
|
+
* daemon can never strand a child mutating forever, and a manual `agents update
|
|
25
|
+
* --auto` interrupted with Ctrl+C also stops at a boundary rather than mid-swap.
|
|
26
|
+
*
|
|
27
|
+
* This module is a dependency-free LEAF on purpose: `index.ts` reads the guard
|
|
28
|
+
* depth below through the shared {@link GUARDED_AUTO_UPDATE_SYMBOL} registry key
|
|
29
|
+
* (never importing this module, so the slim CLI shell stays slim), and the daemon
|
|
30
|
+
* child fixture in tests imports {@link withGuardedUpdateCancellation} without
|
|
31
|
+
* pulling the installations graph.
|
|
32
|
+
*/
|
|
33
|
+
/**
|
|
34
|
+
* The hidden verb the daemon spawns to run one auto-update pass with IPC-driven
|
|
35
|
+
* cancellation (dispatched in `index.ts`, handled by
|
|
36
|
+
* `update-runtime.ts`'s `runHarnessUpdateChild`). Internal protocol — not a
|
|
37
|
+
* public command.
|
|
38
|
+
*/
|
|
39
|
+
export const HARNESS_UPDATE_CHILD_CMD = '__harness-update-run';
|
|
40
|
+
/** IPC message `type` the daemon sends to request a cooperative stop. */
|
|
41
|
+
export const HARNESS_UPDATE_CANCEL_MSG = 'harness-update:cancel';
|
|
42
|
+
/**
|
|
43
|
+
* Well-known cross-realm key naming the count of guarded auto-update passes
|
|
44
|
+
* running IN THIS PROCESS right now. `index.ts`'s top-level SIGINT handler reads
|
|
45
|
+
* it via `Symbol.for(...)` WITHOUT importing this module (keeping the slim entry
|
|
46
|
+
* shell free of a heavy static import — see `agent.test.ts`'s import guard), so
|
|
47
|
+
* the guard state has to live somewhere both can reach: the global symbol
|
|
48
|
+
* registry.
|
|
49
|
+
*/
|
|
50
|
+
export const GUARDED_AUTO_UPDATE_SYMBOL = Symbol.for('agents.guardedAutoUpdateDepth');
|
|
51
|
+
/** The IPC payload the daemon sends; `child.send(cancelMessage())`. */
|
|
52
|
+
export function cancelMessage() {
|
|
53
|
+
return { type: HARNESS_UPDATE_CANCEL_MSG };
|
|
54
|
+
}
|
|
55
|
+
function isCancelMessage(msg) {
|
|
56
|
+
return (typeof msg === 'object' &&
|
|
57
|
+
msg !== null &&
|
|
58
|
+
msg.type === HARNESS_UPDATE_CANCEL_MSG);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* True while a guarded auto-update pass is mutating this process. Read by
|
|
62
|
+
* `index.ts`'s SIGINT handler (which reads the same registry symbol directly) to
|
|
63
|
+
* DEFER its default `process.exit(130)` so a Ctrl+C can't tear a swap apart; the
|
|
64
|
+
* pass still observes the SIGINT cooperatively and stops at its next boundary.
|
|
65
|
+
* Ref-counted (a depth, not a boolean) so it is correct even if a pass ever nests.
|
|
66
|
+
*/
|
|
67
|
+
export function isGuardedAutoUpdateActive() {
|
|
68
|
+
return (globalThis[GUARDED_AUTO_UPDATE_SYMBOL] ?? 0) > 0;
|
|
69
|
+
}
|
|
70
|
+
function beginGuardedAutoUpdate() {
|
|
71
|
+
const holder = globalThis;
|
|
72
|
+
holder[GUARDED_AUTO_UPDATE_SYMBOL] = (holder[GUARDED_AUTO_UPDATE_SYMBOL] ?? 0) + 1;
|
|
73
|
+
}
|
|
74
|
+
function endGuardedAutoUpdate() {
|
|
75
|
+
const holder = globalThis;
|
|
76
|
+
const depth = holder[GUARDED_AUTO_UPDATE_SYMBOL] ?? 0;
|
|
77
|
+
holder[GUARDED_AUTO_UPDATE_SYMBOL] = depth > 0 ? depth - 1 : 0;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Run `run(cancelled)` with cooperative cancellation wired from every source
|
|
81
|
+
* that can reach this process, and the SIGINT guard held for its duration:
|
|
82
|
+
*
|
|
83
|
+
* - **IPC message** `{ type: HARNESS_UPDATE_CANCEL_MSG }` — the primary,
|
|
84
|
+
* cross-platform request the daemon sends. Works on Windows, where a signal
|
|
85
|
+
* would force-kill.
|
|
86
|
+
* - **`disconnect`** — the IPC channel closed (the daemon went away). Stop
|
|
87
|
+
* rather than continue mutating orphaned.
|
|
88
|
+
* - **SIGTERM / SIGINT** — a direct `kill`/Ctrl+C on this process. Cooperative,
|
|
89
|
+
* not fatal; paired with `index.ts`'s guard, which defers its hard exit while
|
|
90
|
+
* {@link isGuardedAutoUpdateActive} holds.
|
|
91
|
+
*
|
|
92
|
+
* `cancelled()` never un-sets once set. Listeners are installed for the call's
|
|
93
|
+
* lifetime only and removed in `finally`, and the process-wide `message`/
|
|
94
|
+
* `disconnect` listeners are harmless when there is no IPC channel (they simply
|
|
95
|
+
* never fire), so this is safe on the in-process manual `agents update --auto`
|
|
96
|
+
* path too.
|
|
97
|
+
*/
|
|
98
|
+
export async function withGuardedUpdateCancellation(run) {
|
|
99
|
+
let cancelled = typeof process.send === 'function' && process.connected === false;
|
|
100
|
+
const requestStop = () => {
|
|
101
|
+
cancelled = true;
|
|
102
|
+
};
|
|
103
|
+
const onMessage = (msg) => {
|
|
104
|
+
if (isCancelMessage(msg))
|
|
105
|
+
requestStop();
|
|
106
|
+
};
|
|
107
|
+
process.on('SIGTERM', requestStop);
|
|
108
|
+
process.on('SIGINT', requestStop);
|
|
109
|
+
process.on('message', onMessage);
|
|
110
|
+
process.on('disconnect', requestStop);
|
|
111
|
+
beginGuardedAutoUpdate();
|
|
112
|
+
try {
|
|
113
|
+
return await run(() => cancelled);
|
|
114
|
+
}
|
|
115
|
+
finally {
|
|
116
|
+
endGuardedAutoUpdate();
|
|
117
|
+
process.removeListener('SIGTERM', requestStop);
|
|
118
|
+
process.removeListener('SIGINT', requestStop);
|
|
119
|
+
process.removeListener('message', onMessage);
|
|
120
|
+
process.removeListener('disconnect', requestStop);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings that gate the AUTOMATIC update pass (PHNX-3940): whether it runs at
|
|
3
|
+
* all (global + per-harness), and whether one installation participates.
|
|
4
|
+
*
|
|
5
|
+
* Two independent axes, deliberately kept apart:
|
|
6
|
+
* - `updates.auto` / `updates.<agent>.auto` — an OPERATOR switch, global and
|
|
7
|
+
* per-harness, stored centrally (`~/.agents/agents.yaml` `config:`) so it
|
|
8
|
+
* syncs fleet-wide like `summarizer.*`. Both are registered as ordinary
|
|
9
|
+
* TYPED entries in `device-config.ts`'s `CONFIG_KEYS` — the one canonical
|
|
10
|
+
* store for user-scope config — rather than a second untyped read/write
|
|
11
|
+
* path against `Meta.config` directly, so a config rewrite that only knows
|
|
12
|
+
* about the registry (validation, `agents config list`, the fleet sync
|
|
13
|
+
* that round-trips `config:`) can't silently drop these keys.
|
|
14
|
+
* - `Installation.updatePolicy` — an INSTALLATION property. Manual
|
|
15
|
+
* `agents update <agent>@<label> --to <concrete>` pins it; `--to latest`
|
|
16
|
+
* (or a fresh install) leaves/returns it to `'latest'`.
|
|
17
|
+
*
|
|
18
|
+
* The global switch is a HARD KILL SWITCH, not a default an explicit
|
|
19
|
+
* per-harness `true` can override: `updates.auto=false` must stop every
|
|
20
|
+
* harness's automatic pass even when an operator separately turned one
|
|
21
|
+
* harness's switch on, because the global switch is the "something is
|
|
22
|
+
* wrong, stop touching installations fleet-wide" lever and a forgotten
|
|
23
|
+
* per-harness override must never defeat it. See
|
|
24
|
+
* {@link isAutoUpdateEnabledForAgent}.
|
|
25
|
+
*/
|
|
26
|
+
import type { AgentId } from '../types.js';
|
|
27
|
+
import type { Installation, UpdatePolicy } from './types.js';
|
|
28
|
+
/** The raw, explicitly-set global switch, or `undefined` when never set (default: on). */
|
|
29
|
+
export declare function rawGlobalAutoUpdateSetting(): boolean | undefined;
|
|
30
|
+
export declare function setGlobalAutoUpdateEnabled(enabled: boolean): void;
|
|
31
|
+
export declare function unsetGlobalAutoUpdateEnabled(): void;
|
|
32
|
+
/** True unless the operator explicitly turned automatic updates off globally. */
|
|
33
|
+
export declare function isGlobalAutoUpdateEnabled(): boolean;
|
|
34
|
+
/** The raw, explicitly-set per-harness switch, or `undefined` when it defers to the global one. */
|
|
35
|
+
export declare function rawAgentAutoUpdateSetting(agent: AgentId): boolean | undefined;
|
|
36
|
+
export declare function setAgentAutoUpdateEnabled(agent: AgentId, enabled: boolean): void;
|
|
37
|
+
export declare function unsetAgentAutoUpdateEnabled(agent: AgentId): void;
|
|
38
|
+
/**
|
|
39
|
+
* Whether the automatic-update pass may consider this agent at all.
|
|
40
|
+
*
|
|
41
|
+
* `updates.auto=false` is a hard kill switch: it wins even over an explicit
|
|
42
|
+
* `updates.<agent>.auto=true` (see the module docblock). With the global
|
|
43
|
+
* switch on (the default), the per-harness switch refines it when explicitly
|
|
44
|
+
* set, else the harness is enabled too.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isAutoUpdateEnabledForAgent(agent: AgentId): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The effective policy for one installation. Absent (legacy data, or a fresh
|
|
49
|
+
* install that never set it) means `'latest'` — see {@link Installation.updatePolicy}.
|
|
50
|
+
* Read through this everywhere rather than comparing the field directly.
|
|
51
|
+
*/
|
|
52
|
+
export declare function effectiveUpdatePolicy(installation: Pick<Installation, 'updatePolicy'>): UpdatePolicy;
|
|
53
|
+
/**
|
|
54
|
+
* Persist an installation's update policy. Takes the SAME per-installation
|
|
55
|
+
* lock `updateInstallation` (`update.ts`) holds for its whole transaction —
|
|
56
|
+
* without it, a manual `--to <release>`/`--to latest` pin racing an automatic
|
|
57
|
+
* pass's own `recordRelease` write (both a read-modify-write of the same
|
|
58
|
+
* `installation.json`) could lose whichever wrote second, silently reverting
|
|
59
|
+
* a just-applied pin or an update's own release bump. Reloads the record
|
|
60
|
+
* fresh from disk under the lock (never trusts a possibly-stale in-memory
|
|
61
|
+
* copy) and writes back only the policy field — never touches
|
|
62
|
+
* `history`/`releaseVersion`, since pinning or unpinning is not a release
|
|
63
|
+
* change. Locks with the SAME `staleMs`/`acquireTimeoutMs`
|
|
64
|
+
* ({@link INSTALLATION_LOCK_OPTIONS}) as that transaction — the fs-atomic
|
|
65
|
+
* default (5s stale) is far shorter than an update can legitimately run, so
|
|
66
|
+
* using it here would let this write break the transaction's
|
|
67
|
+
* still-legitimately-held lock out from under it.
|
|
68
|
+
*/
|
|
69
|
+
export declare function setInstallationUpdatePolicy(agent: AgentId, label: string, policy: UpdatePolicy): Promise<Installation>;
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings that gate the AUTOMATIC update pass (PHNX-3940): whether it runs at
|
|
3
|
+
* all (global + per-harness), and whether one installation participates.
|
|
4
|
+
*
|
|
5
|
+
* Two independent axes, deliberately kept apart:
|
|
6
|
+
* - `updates.auto` / `updates.<agent>.auto` — an OPERATOR switch, global and
|
|
7
|
+
* per-harness, stored centrally (`~/.agents/agents.yaml` `config:`) so it
|
|
8
|
+
* syncs fleet-wide like `summarizer.*`. Both are registered as ordinary
|
|
9
|
+
* TYPED entries in `device-config.ts`'s `CONFIG_KEYS` — the one canonical
|
|
10
|
+
* store for user-scope config — rather than a second untyped read/write
|
|
11
|
+
* path against `Meta.config` directly, so a config rewrite that only knows
|
|
12
|
+
* about the registry (validation, `agents config list`, the fleet sync
|
|
13
|
+
* that round-trips `config:`) can't silently drop these keys.
|
|
14
|
+
* - `Installation.updatePolicy` — an INSTALLATION property. Manual
|
|
15
|
+
* `agents update <agent>@<label> --to <concrete>` pins it; `--to latest`
|
|
16
|
+
* (or a fresh install) leaves/returns it to `'latest'`.
|
|
17
|
+
*
|
|
18
|
+
* The global switch is a HARD KILL SWITCH, not a default an explicit
|
|
19
|
+
* per-harness `true` can override: `updates.auto=false` must stop every
|
|
20
|
+
* harness's automatic pass even when an operator separately turned one
|
|
21
|
+
* harness's switch on, because the global switch is the "something is
|
|
22
|
+
* wrong, stop touching installations fleet-wide" lever and a forgotten
|
|
23
|
+
* per-harness override must never defeat it. See
|
|
24
|
+
* {@link isAutoUpdateEnabledForAgent}.
|
|
25
|
+
*/
|
|
26
|
+
import { getConfigValue, setConfigValue, unsetConfigValue } from '../device-config.js';
|
|
27
|
+
import { withFileLockAsync } from '../fs-atomic.js';
|
|
28
|
+
import * as fs from 'node:fs';
|
|
29
|
+
import { ensureInstallationLocked, installationDir, writeInstallation } from './store.js';
|
|
30
|
+
import { installationLockTarget, INSTALLATION_LOCK_OPTIONS } from './installation-lock.js';
|
|
31
|
+
function agentAutoKey(agent) {
|
|
32
|
+
return `updates.${agent}.auto`;
|
|
33
|
+
}
|
|
34
|
+
/** The raw, explicitly-set global switch, or `undefined` when never set (default: on). */
|
|
35
|
+
export function rawGlobalAutoUpdateSetting() {
|
|
36
|
+
return getConfigValue('updates.auto').value;
|
|
37
|
+
}
|
|
38
|
+
export function setGlobalAutoUpdateEnabled(enabled) {
|
|
39
|
+
setConfigValue('updates.auto', enabled);
|
|
40
|
+
}
|
|
41
|
+
export function unsetGlobalAutoUpdateEnabled() {
|
|
42
|
+
unsetConfigValue('updates.auto');
|
|
43
|
+
}
|
|
44
|
+
/** True unless the operator explicitly turned automatic updates off globally. */
|
|
45
|
+
export function isGlobalAutoUpdateEnabled() {
|
|
46
|
+
return rawGlobalAutoUpdateSetting() !== false;
|
|
47
|
+
}
|
|
48
|
+
/** The raw, explicitly-set per-harness switch, or `undefined` when it defers to the global one. */
|
|
49
|
+
export function rawAgentAutoUpdateSetting(agent) {
|
|
50
|
+
return getConfigValue(agentAutoKey(agent)).value;
|
|
51
|
+
}
|
|
52
|
+
export function setAgentAutoUpdateEnabled(agent, enabled) {
|
|
53
|
+
setConfigValue(agentAutoKey(agent), enabled);
|
|
54
|
+
}
|
|
55
|
+
export function unsetAgentAutoUpdateEnabled(agent) {
|
|
56
|
+
unsetConfigValue(agentAutoKey(agent));
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Whether the automatic-update pass may consider this agent at all.
|
|
60
|
+
*
|
|
61
|
+
* `updates.auto=false` is a hard kill switch: it wins even over an explicit
|
|
62
|
+
* `updates.<agent>.auto=true` (see the module docblock). With the global
|
|
63
|
+
* switch on (the default), the per-harness switch refines it when explicitly
|
|
64
|
+
* set, else the harness is enabled too.
|
|
65
|
+
*/
|
|
66
|
+
export function isAutoUpdateEnabledForAgent(agent) {
|
|
67
|
+
if (!isGlobalAutoUpdateEnabled())
|
|
68
|
+
return false;
|
|
69
|
+
const perAgent = rawAgentAutoUpdateSetting(agent);
|
|
70
|
+
return perAgent !== false;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The effective policy for one installation. Absent (legacy data, or a fresh
|
|
74
|
+
* install that never set it) means `'latest'` — see {@link Installation.updatePolicy}.
|
|
75
|
+
* Read through this everywhere rather than comparing the field directly.
|
|
76
|
+
*/
|
|
77
|
+
export function effectiveUpdatePolicy(installation) {
|
|
78
|
+
return installation.updatePolicy ?? 'latest';
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Persist an installation's update policy. Takes the SAME per-installation
|
|
82
|
+
* lock `updateInstallation` (`update.ts`) holds for its whole transaction —
|
|
83
|
+
* without it, a manual `--to <release>`/`--to latest` pin racing an automatic
|
|
84
|
+
* pass's own `recordRelease` write (both a read-modify-write of the same
|
|
85
|
+
* `installation.json`) could lose whichever wrote second, silently reverting
|
|
86
|
+
* a just-applied pin or an update's own release bump. Reloads the record
|
|
87
|
+
* fresh from disk under the lock (never trusts a possibly-stale in-memory
|
|
88
|
+
* copy) and writes back only the policy field — never touches
|
|
89
|
+
* `history`/`releaseVersion`, since pinning or unpinning is not a release
|
|
90
|
+
* change. Locks with the SAME `staleMs`/`acquireTimeoutMs`
|
|
91
|
+
* ({@link INSTALLATION_LOCK_OPTIONS}) as that transaction — the fs-atomic
|
|
92
|
+
* default (5s stale) is far shorter than an update can legitimately run, so
|
|
93
|
+
* using it here would let this write break the transaction's
|
|
94
|
+
* still-legitimately-held lock out from under it.
|
|
95
|
+
*/
|
|
96
|
+
export async function setInstallationUpdatePolicy(agent, label, policy) {
|
|
97
|
+
if (!fs.existsSync(installationDir(agent, label)))
|
|
98
|
+
throw new Error(`No installation directory for ${agent}@${label}.`);
|
|
99
|
+
// Guarantees `installation.json` exists and is VALID before locking on it,
|
|
100
|
+
// migrating a legacy pre-frozen version dir when needed — same reasoning as
|
|
101
|
+
// `launch-gate.ts`'s identical call. Throws its own clear
|
|
102
|
+
// "no installation directory" error when `label` was never installed at
|
|
103
|
+
// all, which is a real caller bug, not a race to reconcile under the lock.
|
|
104
|
+
const recordPath = installationLockTarget(agent, label);
|
|
105
|
+
return withFileLockAsync(recordPath, () => {
|
|
106
|
+
const current = ensureInstallationLocked(agent, label);
|
|
107
|
+
if (!current) {
|
|
108
|
+
throw new Error(`No installation record for ${agent}@${label} — cannot set its update policy.`);
|
|
109
|
+
}
|
|
110
|
+
const next = { ...current, updatePolicy: policy, updatedAt: new Date().toISOString() };
|
|
111
|
+
writeInstallation(next);
|
|
112
|
+
return next;
|
|
113
|
+
}, INSTALLATION_LOCK_OPTIONS);
|
|
114
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The automatic-update pass (PHNX-3940): decide which installations may be
|
|
3
|
+
* moved to their harness's latest release with no operator in the loop, then
|
|
4
|
+
* (optionally) move them.
|
|
5
|
+
*
|
|
6
|
+
* Split into a PLAN (`planAutoUpdates` — the `--check` dry-run and the
|
|
7
|
+
* daemon's own decision step both read it; it reads through
|
|
8
|
+
* {@link listInstallationSnapshots}, which NEVER mutates disk, so a preview is
|
|
9
|
+
* genuinely a preview) and a RUN (`runAutoUpdatePass`, which migrates a
|
|
10
|
+
* legacy installation for real, under its own lock, and regenerates its
|
|
11
|
+
* already-owned shim/versioned-alias, before driving eligible plan entries
|
|
12
|
+
* through the existing `updateInstallation` transaction). Both share one
|
|
13
|
+
* eligibility computation so a dry-run can never report "would update" for
|
|
14
|
+
* something the real run would skip, or vice versa.
|
|
15
|
+
*
|
|
16
|
+
* Eligibility is deliberately narrow:
|
|
17
|
+
* - Only the `npm-package` strategy is transactional AND isolated per
|
|
18
|
+
* installation (stage into a sibling dir, launch-probe it, swap, keep the
|
|
19
|
+
* displaced tree until the swap is proven) — the shape every existing
|
|
20
|
+
* safety property in `update.ts` depends on. A global-binary or
|
|
21
|
+
* install-script harness has no reversible per-installation swap to offer,
|
|
22
|
+
* so it is reported honestly as manual/vendor-managed rather than silently
|
|
23
|
+
* skipped or, worse, "updated" via an irreversible reinstall with no
|
|
24
|
+
* operator watching.
|
|
25
|
+
* - The operator switch (`updates.auto` / `updates.<agent>.auto`,
|
|
26
|
+
* `update-policy.ts`) and the per-installation policy
|
|
27
|
+
* (`Installation.updatePolicy`) must both allow it.
|
|
28
|
+
* - The installation must not look ACTIVE right now (see
|
|
29
|
+
* {@link isInstallationLikelyActive}) — a real OS process-table scan, not
|
|
30
|
+
* just this box's session registry, so a harness launched by a bare
|
|
31
|
+
* generated shim with no session bookkeeping still defers correctly.
|
|
32
|
+
*
|
|
33
|
+
* The latest release for a harness is resolved ONCE per pass (not once per
|
|
34
|
+
* installation) — `resolveSharedTargets` below — so a fleet with a dozen
|
|
35
|
+
* pinned-to-latest Claude installations issues one npm registry read, not a
|
|
36
|
+
* dozen.
|
|
37
|
+
*/
|
|
38
|
+
import type { AgentId } from '../types.js';
|
|
39
|
+
import { type Installation, type UpdateOutcome, type UpdatePolicy } from './types.js';
|
|
40
|
+
export interface AutoUpdatePlanEntry {
|
|
41
|
+
agent: AgentId;
|
|
42
|
+
installation: Installation;
|
|
43
|
+
currentRelease: string;
|
|
44
|
+
/** The resolved latest release, or `null` when it could not be resolved (network error, unsupported strategy). */
|
|
45
|
+
targetRelease: string | null;
|
|
46
|
+
policy: UpdatePolicy;
|
|
47
|
+
/** Whether the automatic pass would act on this installation at all (ignoring whether it is currently deferred). */
|
|
48
|
+
eligible: boolean;
|
|
49
|
+
/** Whether a live process for this installation held it back THIS pass. Independent of `eligible`. */
|
|
50
|
+
deferred: boolean;
|
|
51
|
+
/** Human-readable reason `eligible` is false, or `deferred` is true, or `targetRelease` is null. Unset when there is nothing to explain (already current, or a real update would run). */
|
|
52
|
+
reason?: string;
|
|
53
|
+
}
|
|
54
|
+
export interface AutoUpdatePassOutcome {
|
|
55
|
+
entry: AutoUpdatePlanEntry;
|
|
56
|
+
outcome?: UpdateOutcome;
|
|
57
|
+
error?: string;
|
|
58
|
+
}
|
|
59
|
+
export interface AutoUpdatePassResult {
|
|
60
|
+
plan: AutoUpdatePlanEntry[];
|
|
61
|
+
outcomes: AutoUpdatePassOutcome[];
|
|
62
|
+
/** True when the pass stopped early on a cancellation request (deadline, daemon shutdown, IPC, or SIGINT/SIGTERM) rather than exhausting the plan. */
|
|
63
|
+
cancelled?: boolean;
|
|
64
|
+
}
|
|
65
|
+
export interface AutoUpdatePassOptions {
|
|
66
|
+
/** Scope to these agents only. Default: every non-hard-deprecated managed agent. */
|
|
67
|
+
agents?: AgentId[];
|
|
68
|
+
onProgress?: (message: string) => void;
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Read-only enumeration of installations — the ONLY listing {@link planAutoUpdates}
|
|
72
|
+
* may use. `listInstallations` (`store.ts`) migrates a legacy version dir's
|
|
73
|
+
* `installation.json` into existence via `ensureInstallation` as a side
|
|
74
|
+
* effect of merely being READ, which made `agents update --check` (a
|
|
75
|
+
* "preview") write to disk. A legacy dir with no persisted record yet gets an
|
|
76
|
+
* {@link ephemeralInstallationSnapshot} instead — real fields, but never
|
|
77
|
+
* written and never a real id. The real run migrates it for real, under this
|
|
78
|
+
* installation's own lock, immediately before acting on it — see
|
|
79
|
+
* {@link runAutoUpdatePass}.
|
|
80
|
+
*/
|
|
81
|
+
export declare function listInstallationSnapshots(agent: AgentId): Installation[];
|
|
82
|
+
/**
|
|
83
|
+
* Build the automatic-update plan: one entry per installation of every scoped
|
|
84
|
+
* agent, with eligibility, deferral, and the resolved target release already
|
|
85
|
+
* computed. Never mutates anything — reads only through
|
|
86
|
+
* {@link listInstallationSnapshots} — so it is genuinely safe to call from
|
|
87
|
+
* `--check` or before every real pass.
|
|
88
|
+
*/
|
|
89
|
+
export declare function planAutoUpdates(opts?: AutoUpdatePassOptions): Promise<AutoUpdatePlanEntry[]>;
|
|
90
|
+
/**
|
|
91
|
+
* Run the automatic-update pass: plan, then drive every eligible,
|
|
92
|
+
* non-deferred, actually-behind entry through {@link updateInstallation}.
|
|
93
|
+
* Installations update sequentially — this runs from a bounded daemon-spawned
|
|
94
|
+
* child process (`harness-update-service.ts`) on its own schedule, not a
|
|
95
|
+
* user-facing wait, so there is no reason to parallelize and every reason not
|
|
96
|
+
* to (concurrent npm installs sharing this box's npm cache have a history of
|
|
97
|
+
* corrupting each other).
|
|
98
|
+
*
|
|
99
|
+
* `abortIfPinnedBeforeCommit` / `abortIfAutoDisabledBeforeCommit: true` on
|
|
100
|
+
* every call: this is the ONE caller for whom a policy or switch change
|
|
101
|
+
* mid-staging must cancel the commit (see `update.ts`'s docblock on those
|
|
102
|
+
* options) — a manual `agents update` never routes through here.
|
|
103
|
+
*/
|
|
104
|
+
export declare function runAutoUpdatePass(opts?: AutoUpdatePassOptions): Promise<AutoUpdatePassResult>;
|
|
105
|
+
/**
|
|
106
|
+
* The hidden `__harness-update-run` verb the daemon spawns with an IPC channel
|
|
107
|
+
* (dispatched in `index.ts`). Runs ONE auto-update pass with the cooperative,
|
|
108
|
+
* cross-platform cancellation above, writes a compact JSON summary to stdout for
|
|
109
|
+
* the daemon's log, and returns the process exit code.
|
|
110
|
+
*
|
|
111
|
+
* Exit code mirrors the manual `agents update --auto` path
|
|
112
|
+
* (`commands/update.ts`): a per-installation error is a NORMAL tick outcome (a
|
|
113
|
+
* bad vendor release) surfaced as a non-zero exit the daemon logs as a warning,
|
|
114
|
+
* not a service failure. A cooperative cancel is not itself an error, so it does
|
|
115
|
+
* not force a non-zero exit — the child exits on its own and the daemon observes
|
|
116
|
+
* that true completion rather than reading a killed process.
|
|
117
|
+
*/
|
|
118
|
+
export declare function runHarnessUpdateChild(): Promise<number>;
|