@phnx-labs/agents-cli 1.22.25 → 1.22.27
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 +427 -0
- package/README.md +35 -3
- package/dist/bin/agents +0 -0
- package/dist/browser.js +14 -4
- package/dist/commands/apply.js +52 -8
- package/dist/commands/bench.d.ts +2 -0
- package/dist/commands/bench.js +101 -0
- package/dist/commands/browser.js +35 -0
- package/dist/commands/doctor.js +32 -10
- package/dist/commands/exec.js +17 -16
- package/dist/commands/insights.d.ts +25 -19
- package/dist/commands/insights.js +146 -38
- package/dist/commands/output.js +100 -25
- package/dist/commands/reconnect.d.ts +46 -0
- package/dist/commands/reconnect.js +109 -0
- package/dist/commands/resume.d.ts +11 -0
- package/dist/commands/resume.js +51 -0
- package/dist/commands/routines.js +2 -2
- package/dist/commands/secrets.d.ts +2 -8
- package/dist/commands/secrets.js +29 -105
- package/dist/commands/sessions-picker.js +12 -7
- package/dist/commands/sessions.d.ts +47 -1
- package/dist/commands/sessions.js +349 -37
- package/dist/commands/setup-secrets.d.ts +1 -0
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/setup.d.ts +26 -3
- package/dist/commands/setup.js +105 -46
- package/dist/commands/ssh.js +184 -11
- package/dist/commands/teams.d.ts +6 -0
- package/dist/commands/teams.js +46 -3
- package/dist/commands/trends.d.ts +8 -0
- package/dist/commands/trends.js +10 -156
- package/dist/index.js +17 -21
- package/dist/lib/agents.d.ts +11 -0
- package/dist/lib/agents.js +29 -2
- package/dist/lib/analytics/dashboard.d.ts +10 -6
- package/dist/lib/analytics/dashboard.js +6 -4
- package/dist/lib/analytics/mix-commands.d.ts +53 -0
- package/dist/lib/analytics/mix-commands.js +229 -0
- package/dist/lib/analytics/recipes.d.ts +19 -14
- package/dist/lib/analytics/recipes.js +4 -2
- package/dist/lib/auth-health.d.ts +47 -3
- package/dist/lib/auth-health.js +74 -15
- package/dist/lib/bench/index.d.ts +4 -0
- package/dist/lib/bench/index.js +4 -0
- package/dist/lib/bench/runner.d.ts +16 -0
- package/dist/lib/bench/runner.js +111 -0
- package/dist/lib/bench/schema.d.ts +5 -0
- package/dist/lib/bench/schema.js +91 -0
- package/dist/lib/bench/storage.d.ts +5 -0
- package/dist/lib/bench/storage.js +32 -0
- package/dist/lib/bench/types.d.ts +40 -0
- package/dist/lib/bench/types.js +1 -0
- package/dist/lib/browser/ipc.d.ts +26 -0
- package/dist/lib/browser/ipc.js +139 -24
- package/dist/lib/browser/profiles.d.ts +11 -0
- package/dist/lib/browser/profiles.js +1 -1
- package/dist/lib/browser/stream.d.ts +14 -0
- package/dist/lib/browser/stream.js +71 -0
- package/dist/lib/channels/owner-sink.d.ts +27 -0
- package/dist/lib/channels/owner-sink.js +93 -0
- package/dist/lib/claude-account-token.d.ts +2 -0
- package/dist/lib/claude-account-token.js +27 -1
- package/dist/lib/crabbox/cli.d.ts +2 -0
- package/dist/lib/crabbox/cli.js +2 -0
- package/dist/lib/crabbox/lease.js +7 -1
- package/dist/lib/daemon.js +20 -0
- package/dist/lib/devices/connect.d.ts +2 -0
- package/dist/lib/devices/connect.js +7 -0
- package/dist/lib/devices/doctor-findings.d.ts +10 -1
- package/dist/lib/devices/doctor-findings.js +47 -1
- package/dist/lib/devices/harness-inventory.d.ts +97 -0
- package/dist/lib/devices/harness-inventory.js +0 -0
- package/dist/lib/devices/registry.d.ts +2 -0
- package/dist/lib/devices/ssh-config.js +3 -0
- package/dist/lib/devices/windows-ssh-enrollment.d.ts +20 -0
- package/dist/lib/devices/windows-ssh-enrollment.js +98 -0
- package/dist/lib/exec.bench.d.ts +1 -0
- package/dist/lib/exec.bench.js +186 -0
- package/dist/lib/exec.js +18 -8
- package/dist/lib/fleet/apply.d.ts +59 -3
- package/dist/lib/fleet/apply.js +191 -12
- package/dist/lib/fleet/remote-login.d.ts +4 -3
- package/dist/lib/fleet/remote-login.js +11 -9
- package/dist/lib/fleet/types.d.ts +21 -2
- package/dist/lib/gemini-settings.d.ts +0 -1
- package/dist/lib/gemini-settings.js +12 -7
- package/dist/lib/hooks/cache.js +15 -0
- package/dist/lib/hooks.d.ts +8 -5
- package/dist/lib/hooks.js +10 -6
- package/dist/lib/hosts/dispatch.js +7 -3
- package/dist/lib/hosts/passthrough.d.ts +45 -0
- package/dist/lib/hosts/passthrough.js +59 -8
- package/dist/lib/hosts/progress.d.ts +2 -1
- package/dist/lib/hosts/progress.js +4 -3
- package/dist/lib/hosts/providers/devices.js +1 -0
- package/dist/lib/hosts/ready.d.ts +6 -2
- package/dist/lib/hosts/ready.js +33 -8
- package/dist/lib/hosts/reconcile.d.ts +1 -1
- package/dist/lib/hosts/reconcile.js +16 -7
- package/dist/lib/hosts/reconnect.d.ts +56 -21
- package/dist/lib/hosts/reconnect.js +128 -71
- package/dist/lib/hosts/registry.d.ts +2 -2
- package/dist/lib/hosts/registry.js +3 -5
- package/dist/lib/hosts/remote-cmd.d.ts +17 -0
- package/dist/lib/hosts/remote-cmd.js +29 -0
- package/dist/lib/hosts/tasks.d.ts +2 -0
- package/dist/lib/hosts/types.d.ts +1 -0
- package/dist/lib/hosts/types.js +3 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +9 -6
- package/dist/lib/menubar/install-menubar.js +20 -9
- package/dist/lib/models.d.ts +30 -1
- package/dist/lib/models.js +42 -144
- package/dist/lib/pricing/cost.d.ts +9 -0
- package/dist/lib/pricing/cost.js +24 -0
- package/dist/lib/pricing/index.d.ts +1 -1
- package/dist/lib/pricing/index.js +1 -1
- package/dist/lib/redact.js +8 -3
- package/dist/lib/remote-agents-json.d.ts +32 -0
- package/dist/lib/remote-agents-json.js +47 -16
- package/dist/lib/resource-profiles.js +1 -2
- package/dist/lib/routine-notify-owner.d.ts +102 -0
- package/dist/lib/routine-notify-owner.js +232 -0
- package/dist/lib/routines.d.ts +10 -0
- package/dist/lib/routines.js +14 -2
- package/dist/lib/runner.d.ts +9 -4
- package/dist/lib/runner.js +182 -30
- package/dist/lib/sandbox.d.ts +0 -2
- package/dist/lib/sandbox.js +2 -19
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +3 -2
- package/dist/lib/secrets/lease.d.ts +25 -0
- package/dist/lib/secrets/lease.js +44 -0
- package/dist/lib/secrets/push.d.ts +94 -0
- package/dist/lib/secrets/push.js +145 -0
- package/dist/lib/secrets/reaper.d.ts +15 -1
- package/dist/lib/secrets/reaper.js +30 -3
- package/dist/lib/self-update.d.ts +20 -5
- package/dist/lib/self-update.js +93 -16
- package/dist/lib/session/db.d.ts +38 -3
- package/dist/lib/session/db.js +273 -16
- package/dist/lib/session/discover.d.ts +22 -1
- package/dist/lib/session/discover.js +239 -51
- package/dist/lib/session/insights.d.ts +37 -0
- package/dist/lib/session/insights.js +219 -9
- package/dist/lib/session/recovery.d.ts +17 -1
- package/dist/lib/session/recovery.js +111 -4
- package/dist/lib/session/remote-list.d.ts +10 -9
- package/dist/lib/session/remote-list.js +25 -23
- package/dist/lib/session/resume-owner.d.ts +55 -0
- package/dist/lib/session/resume-owner.js +69 -0
- package/dist/lib/session/team-filter.d.ts +65 -0
- package/dist/lib/session/team-filter.js +98 -3
- package/dist/lib/session/tool-index.js +133 -22
- package/dist/lib/session/tool-store.d.ts +26 -2
- package/dist/lib/session/tool-store.js +36 -17
- package/dist/lib/session/types.d.ts +33 -0
- package/dist/lib/smart-launch.d.ts +31 -5
- package/dist/lib/smart-launch.js +43 -7
- package/dist/lib/ssh-exec.d.ts +27 -0
- package/dist/lib/ssh-exec.js +42 -3
- package/dist/lib/ssh-tunnel.d.ts +3 -2
- package/dist/lib/ssh-tunnel.js +25 -16
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +6 -0
- package/dist/lib/teams/agents.d.ts +13 -0
- package/dist/lib/teams/agents.js +75 -7
- package/dist/lib/teams/placement-probe.d.ts +21 -0
- package/dist/lib/teams/placement-probe.js +135 -0
- package/dist/lib/teams/scheduler.d.ts +74 -1
- package/dist/lib/teams/scheduler.js +187 -10
- package/dist/lib/tmux/session.d.ts +8 -0
- package/dist/lib/tmux/session.js +22 -0
- package/dist/lib/types.d.ts +2 -0
- package/package.json +1 -1
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
|
|
9
9
|
*/
|
|
10
10
|
import type { DeviceProfile } from '../devices/registry.js';
|
|
11
|
+
import { type RemoteBackend } from '../secrets/push.js';
|
|
11
12
|
import type { DeviceDesired, DeviceProbe, DeviceDiff, FleetAction, FleetPlan, AuthFilePayload } from './types.js';
|
|
12
13
|
/** Strip a version suffix from an agent spec: `claude@latest` -> `claude`. */
|
|
13
14
|
export declare function agentIdOf(spec: string): string;
|
|
@@ -54,21 +55,76 @@ export interface DiffContext {
|
|
|
54
55
|
/** agents-cli version the source is on — the fleet target version. */
|
|
55
56
|
targetCliVersion: string;
|
|
56
57
|
sourceAuth: SourceAuth;
|
|
57
|
-
/** Secrets-bundle names the profile declares.
|
|
58
|
-
* can't be pushed, so each reachable device surfaces them as a manual recreate
|
|
59
|
-
* (`needs-secret`) — informational, never an executed mutation. */
|
|
58
|
+
/** Secrets-bundle names the profile declares. */
|
|
60
59
|
secretsBundles?: string[];
|
|
60
|
+
/** `--provision-secrets`. OFF by default: pushing a bundle moves credential
|
|
61
|
+
* VALUES to another machine, so it is opted into per invocation and never
|
|
62
|
+
* defaulted from the shared `agents.yaml` (RUSH-1968). */
|
|
63
|
+
provisionSecrets?: boolean;
|
|
64
|
+
/** Is this device's host key pinned? Injected so `decideSecretPush` stays pure
|
|
65
|
+
* and its refusals are testable against real known_hosts fixtures with no
|
|
66
|
+
* network. Absent = treated as unpinned, i.e. refuse. */
|
|
67
|
+
isHostPinned?: (device: string) => boolean;
|
|
68
|
+
/** `--force`: push a declared bundle even when the device already has it. */
|
|
69
|
+
forceSecrets?: boolean;
|
|
61
70
|
}
|
|
62
71
|
/** Pure: desired vs probed -> per-device diff + flat action list. */
|
|
63
72
|
export declare function diffFleet(desired: DeviceDesired[], probes: Map<string, DeviceProbe>, ctx: DiffContext): FleetPlan;
|
|
73
|
+
/** Why a declared bundle is or is not pushed to one device. */
|
|
74
|
+
export interface SecretPushDecision {
|
|
75
|
+
push: boolean;
|
|
76
|
+
/** Where it would land on the remote. Only meaningful when `push`. */
|
|
77
|
+
backend: RemoteBackend;
|
|
78
|
+
/** Set when `push` is false — rendered as the `needs-secret` reminder. */
|
|
79
|
+
reason: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Decide whether `fleet apply` may push one declared bundle to one device.
|
|
83
|
+
*
|
|
84
|
+
* PURE — no ssh, no keychain, no filesystem beyond the injectable pin check — so
|
|
85
|
+
* every branch is unit-testable with no live fleet. Three gates, in order, and
|
|
86
|
+
* each REFUSAL still yields a `needs-secret` reminder rather than silence:
|
|
87
|
+
*
|
|
88
|
+
* 1. `--provision-secrets` must be set. Off by default, and deliberately a
|
|
89
|
+
* flag rather than an `agents.yaml` field: the manifest is shared, so a
|
|
90
|
+
* file-level default means someone else's `apply -y` ships credential
|
|
91
|
+
* values without deciding to (RUSH-1968's shape of accident).
|
|
92
|
+
* 2. The device must be reachable — nothing to push to otherwise.
|
|
93
|
+
* 3. The host key must be PINNED. This moves credential values to another
|
|
94
|
+
* machine, so it reuses the same bar `agents exec --copy-creds` already
|
|
95
|
+
* sets (EXEC-34): an unpinned device earns its pin through a normal
|
|
96
|
+
* `agents ssh <device>` first.
|
|
97
|
+
*
|
|
98
|
+
* Backend follows the platform, and this is the load-bearing default of the
|
|
99
|
+
* whole feature: **file on Linux, keychain on macOS/Windows**. A headless Linux
|
|
100
|
+
* box has no keychain (`lib/secrets/linux.ts`), and the file store there
|
|
101
|
+
* auto-provisions its OWN machine-local key — so each box ends up with an
|
|
102
|
+
* unshared at-rest key and NO passphrase is forwarded. That is the direct
|
|
103
|
+
* alternative to the fleet-wide shared secret this ticket exists to remove.
|
|
104
|
+
*/
|
|
105
|
+
export declare function decideSecretPush(bundle: string, desired: DeviceDesired, probe: DeviceProbe, ctx: DiffContext): SecretPushDecision;
|
|
64
106
|
export interface ProbeOptions {
|
|
65
107
|
/** Also fetch per-agent installed versions (one extra `agents view --json`
|
|
66
108
|
* round-trip). Enable only when the plan has a version-pinned spec. */
|
|
67
109
|
withVersions?: boolean;
|
|
110
|
+
/** Also fetch which secrets bundles the device already has (one extra
|
|
111
|
+
* `agents secrets list --json`). Enable only when the manifest declares
|
|
112
|
+
* bundles and provisioning is on — same cost discipline as `withVersions`. */
|
|
113
|
+
withSecrets?: boolean;
|
|
68
114
|
}
|
|
69
115
|
/** Probe one device: reachability + agents-cli version + installed agent ids
|
|
70
116
|
* (and, when `withVersions`, the installed version strings per agent). */
|
|
71
117
|
export declare function probeDevice(device: DeviceProfile, opts?: ProbeOptions): DeviceProbe;
|
|
118
|
+
/**
|
|
119
|
+
* Narrow a remote `secrets list --json` payload to `name -> updated_at`.
|
|
120
|
+
*
|
|
121
|
+
* Exported and pure so the parse is unit-tested against real payload shapes with
|
|
122
|
+
* no live fleet. Returns `{}` rather than throwing on anything unexpected: the
|
|
123
|
+
* remote runs its own agents-cli version, and a parse failure must degrade to
|
|
124
|
+
* "unknown, so push" — never to "present, so skip", which would silently leave a
|
|
125
|
+
* device unprovisioned.
|
|
126
|
+
*/
|
|
127
|
+
export declare function parseRemoteBundles(stdout: string): Record<string, string>;
|
|
72
128
|
export interface ApplyStep {
|
|
73
129
|
kind: FleetAction['kind'];
|
|
74
130
|
ok: boolean;
|
package/dist/lib/fleet/apply.js
CHANGED
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* (`readyProbe`, `bootstrapAgentsCli`, `buildRemoteAgentsInvocation`, `sshExec`).
|
|
9
9
|
*/
|
|
10
10
|
import * as os from 'os';
|
|
11
|
-
import {
|
|
11
|
+
import { pushBundleToHost } from '../secrets/push.js';
|
|
12
|
+
import { deviceIdentityArgs, sshTargetFor } from '../devices/connect.js';
|
|
12
13
|
import { readyProbe, bootstrapAgentsCli } from '../hosts/ready.js';
|
|
13
14
|
import { buildRemoteAgentsInvocation } from '../hosts/remote-cmd.js';
|
|
14
15
|
import { sshExec } from '../ssh-exec.js';
|
|
@@ -167,14 +168,33 @@ export function diffFleet(desired, probes, ctx) {
|
|
|
167
168
|
}
|
|
168
169
|
}
|
|
169
170
|
}
|
|
170
|
-
// secrets
|
|
171
|
-
//
|
|
172
|
-
//
|
|
173
|
-
//
|
|
171
|
+
// secrets. Declared once at the manifest level, so every reachable device
|
|
172
|
+
// is considered. Historically this was ALWAYS a manual reminder — "surfaced,
|
|
173
|
+
// never pushed" — and that gap is a direct cause of RUSH-1968: an operator
|
|
174
|
+
// who needed secrets on a worker box had no supported path, so they
|
|
175
|
+
// hand-exported the file store's master key across the fleet instead.
|
|
176
|
+
//
|
|
177
|
+
// It is now pushable, but only deliberately. `--provision-secrets` is off by
|
|
178
|
+
// default and is a FLAG, not a manifest field: `agents.yaml` is shared, and
|
|
179
|
+
// a file-level default would mean someone else's `apply -y` silently ships
|
|
180
|
+
// credential values — the same shape of accident this ticket is about.
|
|
181
|
+
// Everything the gate refuses stays a `needs-secret` reminder, so nothing
|
|
182
|
+
// is ever silently skipped.
|
|
174
183
|
if (ctx.secretsBundles && ctx.secretsBundles.length > 0) {
|
|
175
184
|
for (const bundle of ctx.secretsBundles) {
|
|
176
|
-
|
|
177
|
-
|
|
185
|
+
const decision = decideSecretPush(bundle, d, probe, ctx);
|
|
186
|
+
if (decision.push) {
|
|
187
|
+
rowActions.push({
|
|
188
|
+
device: d.device,
|
|
189
|
+
kind: 'push-secret',
|
|
190
|
+
bundle,
|
|
191
|
+
detail: `push secrets bundle '${bundle}' (${decision.backend} backend)`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
else {
|
|
195
|
+
secretsNeeded.push(bundle);
|
|
196
|
+
rowActions.push({ device: d.device, kind: 'needs-secret', bundle, detail: decision.reason });
|
|
197
|
+
}
|
|
178
198
|
}
|
|
179
199
|
}
|
|
180
200
|
}
|
|
@@ -183,6 +203,66 @@ export function diffFleet(desired, probes, ctx) {
|
|
|
183
203
|
}
|
|
184
204
|
return { devices, actions };
|
|
185
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* Decide whether `fleet apply` may push one declared bundle to one device.
|
|
208
|
+
*
|
|
209
|
+
* PURE — no ssh, no keychain, no filesystem beyond the injectable pin check — so
|
|
210
|
+
* every branch is unit-testable with no live fleet. Three gates, in order, and
|
|
211
|
+
* each REFUSAL still yields a `needs-secret` reminder rather than silence:
|
|
212
|
+
*
|
|
213
|
+
* 1. `--provision-secrets` must be set. Off by default, and deliberately a
|
|
214
|
+
* flag rather than an `agents.yaml` field: the manifest is shared, so a
|
|
215
|
+
* file-level default means someone else's `apply -y` ships credential
|
|
216
|
+
* values without deciding to (RUSH-1968's shape of accident).
|
|
217
|
+
* 2. The device must be reachable — nothing to push to otherwise.
|
|
218
|
+
* 3. The host key must be PINNED. This moves credential values to another
|
|
219
|
+
* machine, so it reuses the same bar `agents exec --copy-creds` already
|
|
220
|
+
* sets (EXEC-34): an unpinned device earns its pin through a normal
|
|
221
|
+
* `agents ssh <device>` first.
|
|
222
|
+
*
|
|
223
|
+
* Backend follows the platform, and this is the load-bearing default of the
|
|
224
|
+
* whole feature: **file on Linux, keychain on macOS/Windows**. A headless Linux
|
|
225
|
+
* box has no keychain (`lib/secrets/linux.ts`), and the file store there
|
|
226
|
+
* auto-provisions its OWN machine-local key — so each box ends up with an
|
|
227
|
+
* unshared at-rest key and NO passphrase is forwarded. That is the direct
|
|
228
|
+
* alternative to the fleet-wide shared secret this ticket exists to remove.
|
|
229
|
+
*/
|
|
230
|
+
export function decideSecretPush(bundle, desired, probe, ctx) {
|
|
231
|
+
const device = desired.device;
|
|
232
|
+
const backend = probe.platform === 'linux' ? 'file' : 'keychain';
|
|
233
|
+
const manual = `recreate secrets bundle '${bundle}' (\`agents ssh ${device} -- secrets create ${bundle}\`)`;
|
|
234
|
+
if (!ctx.provisionSecrets) {
|
|
235
|
+
return { push: false, backend, reason: `${manual} — or re-run with --provision-secrets to push it` };
|
|
236
|
+
}
|
|
237
|
+
if (!probe.reachable) {
|
|
238
|
+
return { push: false, backend, reason: manual };
|
|
239
|
+
}
|
|
240
|
+
// Already there? Skip — otherwise every `apply` re-resolves the bundle, and a
|
|
241
|
+
// resolve can prompt for Touch ID, so a converged fleet would nag on every run.
|
|
242
|
+
//
|
|
243
|
+
// Known limitation, stated rather than hidden: this compares PRESENCE (and
|
|
244
|
+
// carries `updated_at` for a future content check). It is a timestamp
|
|
245
|
+
// heuristic, not a content hash — a bundle whose VALUES changed locally still
|
|
246
|
+
// reads as present. `--force` is the way to overwrite regardless.
|
|
247
|
+
// hasOwnProperty, NOT `in`: `in` walks the prototype chain, so a bundle named
|
|
248
|
+
// `toString` / `constructor` / `valueOf` would read as present on an EMPTY map
|
|
249
|
+
// and be silently skipped — leaving that device unprovisioned, the worse of the
|
|
250
|
+
// two errors this gate can make.
|
|
251
|
+
if (!ctx.forceSecrets && probe.remoteBundles
|
|
252
|
+
&& Object.prototype.hasOwnProperty.call(probe.remoteBundles, bundle)) {
|
|
253
|
+
return { push: false, backend, reason: `secrets bundle '${bundle}' already present on ${device} — pass --force to overwrite` };
|
|
254
|
+
}
|
|
255
|
+
if (!ctx.isHostPinned?.(device)) {
|
|
256
|
+
// Same bar as `exec --copy-creds`: never ship credential values to a host
|
|
257
|
+
// whose key we have not pinned.
|
|
258
|
+
return {
|
|
259
|
+
push: false,
|
|
260
|
+
backend,
|
|
261
|
+
reason: `${manual} — host key not pinned; run \`agents ssh ${device}\` once to pin it, then re-apply`,
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { push: true, backend, reason: '' };
|
|
265
|
+
}
|
|
186
266
|
// ---- execution (real SSH; verified end-to-end, not unit-mocked) ----
|
|
187
267
|
function osHint(platform) {
|
|
188
268
|
return platform === 'windows' ? 'windows' : undefined;
|
|
@@ -202,13 +282,14 @@ export function probeDevice(device, opts) {
|
|
|
202
282
|
return { device: device.name, reachable: false, platform: device.platform, installedAgents: [], note: e.message };
|
|
203
283
|
}
|
|
204
284
|
const hint = osHint(device.platform);
|
|
205
|
-
const
|
|
285
|
+
const extraSshArgs = deviceIdentityArgs(device);
|
|
286
|
+
const ready = readyProbe(target, hint, extraSshArgs);
|
|
206
287
|
if (!ready.reachable) {
|
|
207
288
|
return { device: device.name, reachable: false, platform: device.platform, installedAgents: [], note: 'unreachable' };
|
|
208
289
|
}
|
|
209
290
|
let installed = [];
|
|
210
291
|
const remoteCmd = buildRemoteAgentsInvocation(['teams', 'doctor', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
211
|
-
const res = sshExec(target, remoteCmd, { timeoutMs: 30000, multiplex: true });
|
|
292
|
+
const res = sshExec(target, remoteCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs });
|
|
212
293
|
if (res.code === 0) {
|
|
213
294
|
try {
|
|
214
295
|
const map = JSON.parse(res.stdout);
|
|
@@ -221,10 +302,19 @@ export function probeDevice(device, opts) {
|
|
|
221
302
|
let installedVersions;
|
|
222
303
|
if (opts?.withVersions) {
|
|
223
304
|
const viewCmd = buildRemoteAgentsInvocation(['view', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
224
|
-
const vres = sshExec(target, viewCmd, { timeoutMs: 30000, multiplex: true });
|
|
305
|
+
const vres = sshExec(target, viewCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs });
|
|
225
306
|
if (vres.code === 0)
|
|
226
307
|
installedVersions = parseInstalledVersions(vres.stdout);
|
|
227
308
|
}
|
|
309
|
+
let remoteBundles;
|
|
310
|
+
if (opts?.withSecrets) {
|
|
311
|
+
// Metadata only — `secrets list --json` returns names + timestamps and never
|
|
312
|
+
// values, which is why this is safe to run across the fleet.
|
|
313
|
+
const listCmd = buildRemoteAgentsInvocation(['secrets', 'list', '--json'], undefined, hint, remoteEnv(device.platform));
|
|
314
|
+
const lres = sshExec(target, listCmd, { timeoutMs: 30000, multiplex: true, extraSshArgs });
|
|
315
|
+
if (lres.code === 0)
|
|
316
|
+
remoteBundles = parseRemoteBundles(lres.stdout);
|
|
317
|
+
}
|
|
228
318
|
return {
|
|
229
319
|
device: device.name,
|
|
230
320
|
reachable: true,
|
|
@@ -232,8 +322,52 @@ export function probeDevice(device, opts) {
|
|
|
232
322
|
cliVersion: ready.version ?? undefined,
|
|
233
323
|
installedAgents: installed,
|
|
234
324
|
installedVersions,
|
|
325
|
+
remoteBundles,
|
|
235
326
|
};
|
|
236
327
|
}
|
|
328
|
+
/**
|
|
329
|
+
* Narrow a remote `secrets list --json` payload to `name -> updated_at`.
|
|
330
|
+
*
|
|
331
|
+
* Exported and pure so the parse is unit-tested against real payload shapes with
|
|
332
|
+
* no live fleet. Returns `{}` rather than throwing on anything unexpected: the
|
|
333
|
+
* remote runs its own agents-cli version, and a parse failure must degrade to
|
|
334
|
+
* "unknown, so push" — never to "present, so skip", which would silently leave a
|
|
335
|
+
* device unprovisioned.
|
|
336
|
+
*/
|
|
337
|
+
export function parseRemoteBundles(stdout) {
|
|
338
|
+
try {
|
|
339
|
+
const parsed = JSON.parse(stdout);
|
|
340
|
+
const rows = Array.isArray(parsed)
|
|
341
|
+
? parsed
|
|
342
|
+
: Array.isArray(parsed?.bundles)
|
|
343
|
+
? parsed.bundles
|
|
344
|
+
: [];
|
|
345
|
+
// Null-prototype: a remote-supplied name is used as a KEY here, so `{}` would
|
|
346
|
+
// let `__proto__` hit the prototype setter instead of becoming an own
|
|
347
|
+
// property (and then read back as absent). It also means the presence check
|
|
348
|
+
// cannot see inherited names.
|
|
349
|
+
const out = Object.create(null);
|
|
350
|
+
for (const row of rows) {
|
|
351
|
+
if (!row || typeof row !== 'object')
|
|
352
|
+
continue;
|
|
353
|
+
const r = row;
|
|
354
|
+
const name = typeof r.name === 'string' ? r.name : undefined;
|
|
355
|
+
if (!name)
|
|
356
|
+
continue;
|
|
357
|
+
// `updatedAt` is the real field name in `secrets list --json` — verified
|
|
358
|
+
// against a live payload, not assumed. `updated_at` is accepted too so an
|
|
359
|
+
// older remote is not silently recorded with an empty timestamp.
|
|
360
|
+
const ts = typeof r.updatedAt === 'string' ? r.updatedAt
|
|
361
|
+
: typeof r.updated_at === 'string' ? r.updated_at
|
|
362
|
+
: '';
|
|
363
|
+
out[name] = ts;
|
|
364
|
+
}
|
|
365
|
+
return out;
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
return {};
|
|
369
|
+
}
|
|
370
|
+
}
|
|
237
371
|
/** Execute one device's planned actions in order. Real SSH — no mocks. */
|
|
238
372
|
export function reconcileDevice(row, device, ctx) {
|
|
239
373
|
if (!row.probe.reachable) {
|
|
@@ -249,12 +383,13 @@ export function reconcileDevice(row, device, ctx) {
|
|
|
249
383
|
}
|
|
250
384
|
const hint = osHint(device.platform);
|
|
251
385
|
const env = remoteEnv(device.platform);
|
|
386
|
+
const extraSshArgs = deviceIdentityArgs(device);
|
|
252
387
|
let ok = true;
|
|
253
|
-
const sshAgents = (args, input) => sshExec(target, buildRemoteAgentsInvocation(args, undefined, hint, env), { timeoutMs: 300000, multiplex: true, input });
|
|
388
|
+
const sshAgents = (args, input) => sshExec(target, buildRemoteAgentsInvocation(args, undefined, hint, env), { timeoutMs: 300000, multiplex: true, input, extraSshArgs });
|
|
254
389
|
// 1. agents-cli install/upgrade.
|
|
255
390
|
const cliAction = row.actions.find((a) => a.kind === 'install-cli' || a.kind === 'upgrade-cli');
|
|
256
391
|
if (cliAction) {
|
|
257
|
-
const r = bootstrapAgentsCli(target, ctx.targetCliVersion, hint);
|
|
392
|
+
const r = bootstrapAgentsCli(target, ctx.targetCliVersion, hint, extraSshArgs);
|
|
258
393
|
steps.push({ kind: cliAction.kind, ok: r.ok, detail: cliAction.detail });
|
|
259
394
|
ok = ok && r.ok;
|
|
260
395
|
}
|
|
@@ -290,6 +425,50 @@ export function reconcileDevice(row, device, ctx) {
|
|
|
290
425
|
steps.push({ kind: 'push-login', ok: r.code === 0, detail: `propagate login: ${pushAgents.join(', ')}` });
|
|
291
426
|
ok = ok && r.code === 0;
|
|
292
427
|
}
|
|
428
|
+
// 5. secrets provisioning — LAST, and deliberately so. It is the most
|
|
429
|
+
// sensitive mutation apply performs (credential VALUES crossing to another
|
|
430
|
+
// machine), so every lower-risk step above is already recorded before we
|
|
431
|
+
// touch it: a failure here never obscures what did land.
|
|
432
|
+
//
|
|
433
|
+
// Resolve ONCE per device even for several bundles is not possible (a resolve
|
|
434
|
+
// is per bundle), but each bundle resolves once and pushes once — the read can
|
|
435
|
+
// prompt, so it must not repeat.
|
|
436
|
+
const pushSecrets = row.actions.filter((a) => a.kind === 'push-secret');
|
|
437
|
+
for (const action of pushSecrets) {
|
|
438
|
+
const bundle = action.bundle;
|
|
439
|
+
if (!bundle) {
|
|
440
|
+
// A push-secret action without a bundle name is a planner bug, not a
|
|
441
|
+
// recoverable state — fail loud rather than push nothing and report ok.
|
|
442
|
+
steps.push({ kind: 'push-secret', ok: false, detail: 'push-secret action carried no bundle name' });
|
|
443
|
+
ok = false;
|
|
444
|
+
continue;
|
|
445
|
+
}
|
|
446
|
+
const backend = device.platform === 'linux' ? 'file' : 'keychain';
|
|
447
|
+
try {
|
|
448
|
+
const out = pushBundleToHost(bundle, target, {
|
|
449
|
+
remoteBackend: backend,
|
|
450
|
+
operation: `fleet apply ${row.device}`,
|
|
451
|
+
// No passphrase, ever, from this path. On the file backend the remote
|
|
452
|
+
// auto-provisions its OWN machine-local key, which is the entire point:
|
|
453
|
+
// each box gets an unshared at-rest key instead of the fleet-wide shared
|
|
454
|
+
// secret RUSH-1968 is about.
|
|
455
|
+
});
|
|
456
|
+
steps.push({
|
|
457
|
+
kind: 'push-secret',
|
|
458
|
+
ok: out.ok,
|
|
459
|
+
detail: out.ok
|
|
460
|
+
? `secrets '${bundle}' -> ${row.device} (${backend}): ${out.message}`
|
|
461
|
+
: `secrets '${bundle}' -> ${row.device}: ${out.message}`,
|
|
462
|
+
});
|
|
463
|
+
ok = ok && out.ok;
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
// A local resolve failure (locked store, missing bundle, multi-line value)
|
|
467
|
+
// is reported per bundle rather than aborting the whole device.
|
|
468
|
+
steps.push({ kind: 'push-secret', ok: false, detail: `secrets '${bundle}': ${e.message}` });
|
|
469
|
+
ok = false;
|
|
470
|
+
}
|
|
471
|
+
}
|
|
293
472
|
// Surface blocked logins as (non-fatal) informational steps.
|
|
294
473
|
for (const blocked of row.loginBlocked) {
|
|
295
474
|
steps.push({ kind: 'needs-login', ok: false, detail: `${blocked} needs a manual login (\`agents ssh ${row.device} -- ${blocked}\`)` });
|
|
@@ -44,6 +44,7 @@ export interface PendingLogin {
|
|
|
44
44
|
platform?: string;
|
|
45
45
|
/** ssh dial target for the device. */
|
|
46
46
|
target: string;
|
|
47
|
+
extraSshArgs?: string[];
|
|
47
48
|
flow: LoginFlow;
|
|
48
49
|
/** True when a device-code flow this command can actually drive. */
|
|
49
50
|
remotable: boolean;
|
|
@@ -75,7 +76,7 @@ export declare function selectLoginTargets(devices: DeviceProfile[], agents: (Ag
|
|
|
75
76
|
* devices need the askpass shim env, which the PTY env allowlist strips — those
|
|
76
77
|
* are a follow-up (TODO), surfaced as an error rather than silently mis-driven.
|
|
77
78
|
*/
|
|
78
|
-
export declare function buildRemoteLoginSshCommand(target: string, flow: LoginFlow): string;
|
|
79
|
+
export declare function buildRemoteLoginSshCommand(target: string, flow: LoginFlow, extraSshArgs?: string[]): string;
|
|
79
80
|
export type LoginMode = 'bulk' | 'interactive';
|
|
80
81
|
/** Per-pair status shape shared between the driver, the status endpoint, and the page. */
|
|
81
82
|
export type LoginStatusState = 'pending' | 'driving' | 'ready' | 'authorized' | 'error' | 'skipped';
|
|
@@ -138,7 +139,7 @@ export interface DriveResult extends ScrapedLogin {
|
|
|
138
139
|
* in the browser; the caller polls the credential file for completion. Testable
|
|
139
140
|
* against a fake {@link PtyDriver}.
|
|
140
141
|
*/
|
|
141
|
-
export declare function driveRemoteLogin(target: string, flow: LoginFlow, driver: PtyDriver, opts?: DriveOptions): Promise<DriveResult>;
|
|
142
|
+
export declare function driveRemoteLogin(target: string, flow: LoginFlow, driver: PtyDriver, opts?: DriveOptions, extraSshArgs?: string[]): Promise<DriveResult>;
|
|
142
143
|
export interface DetectOptions {
|
|
143
144
|
agents?: (AgentId | string)[];
|
|
144
145
|
devices?: string[];
|
|
@@ -157,7 +158,7 @@ export declare function detectPending(opts?: DetectOptions): Promise<PendingLogi
|
|
|
157
158
|
* bumping after the human completes the browser step. POSIX `stat` only (the
|
|
158
159
|
* device-code agents are all POSIX-fleet).
|
|
159
160
|
*/
|
|
160
|
-
export declare function remoteFileMtime(target: string, homeRel: string): Promise<number>;
|
|
161
|
+
export declare function remoteFileMtime(target: string, homeRel: string, extraSshArgs?: string[]): Promise<number>;
|
|
161
162
|
export interface RunFleetLoginOptions {
|
|
162
163
|
agents?: (AgentId | string)[];
|
|
163
164
|
devices?: string[];
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
import * as http from 'http';
|
|
30
30
|
import { isControlDevice } from '../devices/registry.js';
|
|
31
31
|
import { loadDevices } from '../devices/registry.js';
|
|
32
|
-
import { fleetDialTarget } from '../devices/connect.js';
|
|
32
|
+
import { deviceIdentityArgs, fleetDialTarget } from '../devices/connect.js';
|
|
33
33
|
import { planFleetTargets } from '../devices/fleet.js';
|
|
34
34
|
import { assertValidSshTarget, shellQuote, sshExecAsync } from '../ssh-exec.js';
|
|
35
35
|
import { machineId } from '../session/sync/config.js';
|
|
@@ -134,6 +134,7 @@ export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
|
134
134
|
agent,
|
|
135
135
|
platform: device.platform,
|
|
136
136
|
target: fleetDialTarget(device),
|
|
137
|
+
extraSshArgs: deviceIdentityArgs(device),
|
|
137
138
|
flow,
|
|
138
139
|
remotable,
|
|
139
140
|
reason,
|
|
@@ -152,7 +153,7 @@ export function selectLoginTargets(devices, agents, cache, opts = {}) {
|
|
|
152
153
|
* devices need the askpass shim env, which the PTY env allowlist strips — those
|
|
153
154
|
* are a follow-up (TODO), surfaced as an error rather than silently mis-driven.
|
|
154
155
|
*/
|
|
155
|
-
export function buildRemoteLoginSshCommand(target, flow) {
|
|
156
|
+
export function buildRemoteLoginSshCommand(target, flow, extraSshArgs = []) {
|
|
156
157
|
assertValidSshTarget(target);
|
|
157
158
|
// The agent CLIs (kimi/droid/codex/…) are agents-cli shims that live in
|
|
158
159
|
// ~/.agents/.cache/shims and are only on PATH in an interactive/login shell.
|
|
@@ -165,6 +166,7 @@ export function buildRemoteLoginSshCommand(target, flow) {
|
|
|
165
166
|
'ssh', '-tt',
|
|
166
167
|
'-o', 'StrictHostKeyChecking=accept-new',
|
|
167
168
|
'-o', 'ConnectTimeout=10',
|
|
169
|
+
...extraSshArgs.map(shellQuote),
|
|
168
170
|
shellQuote(target),
|
|
169
171
|
shellQuote(remoteCmd),
|
|
170
172
|
].join(' ');
|
|
@@ -367,7 +369,7 @@ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
|
367
369
|
* in the browser; the caller polls the credential file for completion. Testable
|
|
368
370
|
* against a fake {@link PtyDriver}.
|
|
369
371
|
*/
|
|
370
|
-
export async function driveRemoteLogin(target, flow, driver, opts = {}) {
|
|
372
|
+
export async function driveRemoteLogin(target, flow, driver, opts = {}, extraSshArgs = []) {
|
|
371
373
|
const initialDelayMs = opts.initialDelayMs ?? 4000;
|
|
372
374
|
const pollMs = opts.pollMs ?? 1000;
|
|
373
375
|
const timeoutMs = opts.timeoutMs ?? 90000;
|
|
@@ -377,7 +379,7 @@ export async function driveRemoteLogin(target, flow, driver, opts = {}) {
|
|
|
377
379
|
// `ssh -tt` process it drives) leaks until the sidecar's idle reaper. On the
|
|
378
380
|
// normal return paths the caller owns stop() via the returned sessionId.
|
|
379
381
|
try {
|
|
380
|
-
await driver.exec(id, buildRemoteLoginSshCommand(target, flow));
|
|
382
|
+
await driver.exec(id, buildRemoteLoginSshCommand(target, flow, extraSshArgs));
|
|
381
383
|
await sleep(initialDelayMs);
|
|
382
384
|
if (flow.deviceCodeSelect) {
|
|
383
385
|
await driver.write(id, flow.deviceCodeSelect);
|
|
@@ -431,9 +433,9 @@ export async function detectPending(opts = {}) {
|
|
|
431
433
|
* bumping after the human completes the browser step. POSIX `stat` only (the
|
|
432
434
|
* device-code agents are all POSIX-fleet).
|
|
433
435
|
*/
|
|
434
|
-
export async function remoteFileMtime(target, homeRel) {
|
|
436
|
+
export async function remoteFileMtime(target, homeRel, extraSshArgs = []) {
|
|
435
437
|
const cmd = `stat -c %Y "$HOME/${homeRel}" 2>/dev/null || stat -f %m "$HOME/${homeRel}" 2>/dev/null || echo 0`;
|
|
436
|
-
const res = await sshExecAsync(target, cmd, { timeoutMs: 15000, multiplex: true }).catch(() => null);
|
|
438
|
+
const res = await sshExecAsync(target, cmd, { timeoutMs: 15000, multiplex: true, extraSshArgs }).catch(() => null);
|
|
437
439
|
if (!res || res.code !== 0)
|
|
438
440
|
return 0;
|
|
439
441
|
const n = parseInt(res.stdout.trim(), 10);
|
|
@@ -507,10 +509,10 @@ export async function runFleetLogin(opts = {}) {
|
|
|
507
509
|
const k = key(p.device, p.agent);
|
|
508
510
|
const st = statuses.get(k);
|
|
509
511
|
st.state = 'driving';
|
|
510
|
-
const baseMtime = await remoteFileMtime(p.target, p.flow.successFile).catch(() => 0);
|
|
512
|
+
const baseMtime = await remoteFileMtime(p.target, p.flow.successFile, p.extraSshArgs).catch(() => 0);
|
|
511
513
|
let sessionId;
|
|
512
514
|
try {
|
|
513
|
-
const r = await driveRemoteLogin(p.target, p.flow, driver, opts.drive);
|
|
515
|
+
const r = await driveRemoteLogin(p.target, p.flow, driver, opts.drive, p.extraSshArgs);
|
|
514
516
|
sessionId = r.sessionId;
|
|
515
517
|
if (r.url && r.code) {
|
|
516
518
|
st.url = r.url;
|
|
@@ -539,7 +541,7 @@ export async function runFleetLogin(opts = {}) {
|
|
|
539
541
|
st.detail = 'device code expired before authorization';
|
|
540
542
|
break;
|
|
541
543
|
}
|
|
542
|
-
const mtime = await remoteFileMtime(p.target, p.flow.successFile).catch(() => 0);
|
|
544
|
+
const mtime = await remoteFileMtime(p.target, p.flow.successFile, p.extraSshArgs).catch(() => 0);
|
|
543
545
|
if (mtime > baseMtime) {
|
|
544
546
|
st.state = 'authorized';
|
|
545
547
|
break;
|
|
@@ -95,13 +95,28 @@ export interface DeviceProbe {
|
|
|
95
95
|
* undefined, version-pinned specs fall back to id-level presence.
|
|
96
96
|
*/
|
|
97
97
|
installedVersions?: Record<string, string[]>;
|
|
98
|
+
/**
|
|
99
|
+
* Secrets bundles already present on the device: name -> `updated_at` (or ''
|
|
100
|
+
* when the remote reports none). Only populated when the manifest declares
|
|
101
|
+
* bundles AND `--provision-secrets` is set — a fleet that uses no bundles
|
|
102
|
+
* never pays for the extra round trip.
|
|
103
|
+
*
|
|
104
|
+
* METADATA ONLY. `agents secrets list --json` returns names and timestamps and
|
|
105
|
+
* explicitly never values, which is what makes this probe safe to run.
|
|
106
|
+
*/
|
|
107
|
+
remoteBundles?: Record<string, string>;
|
|
98
108
|
/** Reason string when `reachable` is false or the probe partially failed. */
|
|
99
109
|
note?: string;
|
|
100
110
|
}
|
|
101
111
|
/** One planned action against a device, in a single reconcile dimension. */
|
|
102
112
|
export type FleetActionKind = 'install-cli' | 'upgrade-cli' | 'add-agent' | 'sync-config' | 'push-login' | 'needs-login'
|
|
103
|
-
/**
|
|
104
|
-
*
|
|
113
|
+
/** Push a declared secrets bundle to the device over SSH. Opt-in only
|
|
114
|
+
* (`--provision-secrets`) and gated on a pinned host key, because this moves
|
|
115
|
+
* credential VALUES to another machine (RUSH-1968). */
|
|
116
|
+
| 'push-secret'
|
|
117
|
+
/** A declared secrets bundle that could NOT be pushed — the flag is off, the
|
|
118
|
+
* host key isn't pinned, or the bundle is already current. Surfaced as a manual
|
|
119
|
+
* recreate, like `needs-login`. */
|
|
105
120
|
| 'needs-secret';
|
|
106
121
|
export interface FleetAction {
|
|
107
122
|
device: string;
|
|
@@ -111,6 +126,10 @@ export interface FleetAction {
|
|
|
111
126
|
/** Full agent spec for `add-agent` (e.g. `claude@2.1.170`) so the plan can show
|
|
112
127
|
* the exact version being installed; equals the id for a bare/latest spec. */
|
|
113
128
|
spec?: string;
|
|
129
|
+
/** Bundle name for `push-secret` / `needs-secret`, so the executor pushes the
|
|
130
|
+
* bundle the planner decided on rather than re-deriving it from the detail
|
|
131
|
+
* string. */
|
|
132
|
+
bundle?: string;
|
|
114
133
|
/** Human, one-line description of the action. */
|
|
115
134
|
detail: string;
|
|
116
135
|
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
1
|
export declare function readGeminiSettings(settingsPath: string): Record<string, unknown>;
|
|
2
2
|
export declare function writeGeminiSettings(settingsPath: string, settings: Record<string, unknown>): void;
|
|
3
3
|
export declare function updateGeminiSettings(settingsPath: string, mutate: (settings: Record<string, unknown>) => void): Record<string, unknown>;
|
|
4
|
-
export declare function setGeminiAutoUpdateDisabled(settings: Record<string, unknown>): void;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic reader/writer for a `.gemini/…/settings.json`-shaped JSON config.
|
|
3
|
+
*
|
|
4
|
+
* Despite the name, this module is no longer gemini-specific: gemini itself
|
|
5
|
+
* is hard-deprecated and has no live caller left (its own settings writer,
|
|
6
|
+
* `generateGeminiConfig`, was removed with the RUSH-2202 runner.ts fix — a
|
|
7
|
+
* gemini routine is rejected before it ever reaches sandbox prep). Antigravity
|
|
8
|
+
* nests its own `settings.json` under the same `.gemini/antigravity-cli/` tree
|
|
9
|
+
* and shares this exact shape, so `permissions.ts` reuses `updateGeminiSettings`
|
|
10
|
+
* for antigravity's live permission writes. Keep the generic helpers; do not
|
|
11
|
+
* reintroduce gemini-only logic here.
|
|
12
|
+
*/
|
|
1
13
|
import * as fs from 'fs';
|
|
2
14
|
import * as path from 'path';
|
|
3
15
|
function isRecord(value) {
|
|
@@ -24,10 +36,3 @@ export function updateGeminiSettings(settingsPath, mutate) {
|
|
|
24
36
|
writeGeminiSettings(settingsPath, settings);
|
|
25
37
|
return settings;
|
|
26
38
|
}
|
|
27
|
-
export function setGeminiAutoUpdateDisabled(settings) {
|
|
28
|
-
const general = isRecord(settings.general) ? settings.general : {};
|
|
29
|
-
settings.general = {
|
|
30
|
-
...general,
|
|
31
|
-
enableAutoUpdate: false,
|
|
32
|
-
};
|
|
33
|
-
}
|
package/dist/lib/hooks/cache.js
CHANGED
|
@@ -505,6 +505,21 @@ if [ "$CACHE_STATUS" = miss ]; then
|
|
|
505
505
|
if [ "$_in_backoff" -eq 0 ]; then
|
|
506
506
|
# Fix 1: lockdir — only one background refresh runs at a time.
|
|
507
507
|
LOCK_DIR="$CACHE_FILE.bg.lck"
|
|
508
|
+
# Fix 4 (RUSH-2259): reclaim a stale lock. The lock is released by the bg
|
|
509
|
+
# subshell's EXIT trap, but a hard kill (SIGKILL, OOM, reboot) skips the
|
|
510
|
+
# trap and orphans the dir — after which every future mkdir fails and bg
|
|
511
|
+
# refresh stops FOREVER while stale cache is served indefinitely. The dir
|
|
512
|
+
# mtime is fixed at mkdir time (the refresh writes elsewhere), so its age
|
|
513
|
+
# is the time the lock has been held; treat a lock older than LOCK_TTL_SEC
|
|
514
|
+
# as abandoned and remove it before locking. LOCK_TTL_SEC is set well above
|
|
515
|
+
# any real hook runtime so a live refresh is never reclaimed out from under.
|
|
516
|
+
LOCK_TTL_SEC=300
|
|
517
|
+
if [ -d "$LOCK_DIR" ]; then
|
|
518
|
+
_lock_mtime=$("$PY" -c 'import os,sys; print(int(os.path.getmtime(sys.argv[1])))' "$LOCK_DIR" 2>/dev/null || echo 0)
|
|
519
|
+
_lock_now_s=$(date +%s)
|
|
520
|
+
_lock_age=$((_lock_now_s - \${_lock_mtime:-0}))
|
|
521
|
+
[ "$_lock_age" -ge "$LOCK_TTL_SEC" ] && rm -rf "$LOCK_DIR" 2>/dev/null
|
|
522
|
+
fi
|
|
508
523
|
if mkdir "$LOCK_DIR" 2>/dev/null; then
|
|
509
524
|
tmp="$CACHE_FILE.new.$$"
|
|
510
525
|
# Fix 3: background subshell captures and logs its own real exit code.
|
package/dist/lib/hooks.d.ts
CHANGED
|
@@ -217,12 +217,15 @@ export declare function listCentralHooks(): HookEntry[];
|
|
|
217
217
|
*
|
|
218
218
|
* A bare number stays seconds (`timeout: 30` → 30) for backward compatibility.
|
|
219
219
|
* A Go-style duration string is parsed into seconds: `5s`, `2m`, `1h30m`,
|
|
220
|
-
* `90s`, `1h`.
|
|
221
|
-
*
|
|
222
|
-
*
|
|
220
|
+
* `90s`, `1h`. Suffixed durations longer than 24 hours are rejected as likely
|
|
221
|
+
* typos, while bare seconds stay uncapped for backward compatibility. This
|
|
222
|
+
* intentionally does NOT reuse {@link parseTimeout} from routines.ts — that one
|
|
223
|
+
* returns milliseconds, has no seconds (`s`) unit, and floors at one minute,
|
|
224
|
+
* none of which fit hook timeouts (typically 5–600s).
|
|
223
225
|
*
|
|
224
|
-
* Returns the seconds value, or `null` when the input is not a positive number
|
|
225
|
-
* or a
|
|
226
|
+
* Returns the seconds value, or `null` when the input is not a positive number,
|
|
227
|
+
* is not parseable, or is a suffixed duration longer than 24 hours — the caller
|
|
228
|
+
* decides how to surface that.
|
|
226
229
|
*/
|
|
227
230
|
export declare function normalizeHookTimeoutSeconds(value: unknown): number | null;
|
|
228
231
|
/**
|
package/dist/lib/hooks.js
CHANGED
|
@@ -1160,17 +1160,21 @@ export function listCentralHooks() {
|
|
|
1160
1160
|
}
|
|
1161
1161
|
return results;
|
|
1162
1162
|
}
|
|
1163
|
+
const MAX_HOOK_DURATION_SECONDS = 24 * 60 * 60;
|
|
1163
1164
|
/**
|
|
1164
1165
|
* Normalize a hook `timeout` from agents.yaml into a whole number of seconds.
|
|
1165
1166
|
*
|
|
1166
1167
|
* A bare number stays seconds (`timeout: 30` → 30) for backward compatibility.
|
|
1167
1168
|
* A Go-style duration string is parsed into seconds: `5s`, `2m`, `1h30m`,
|
|
1168
|
-
* `90s`, `1h`.
|
|
1169
|
-
*
|
|
1170
|
-
*
|
|
1169
|
+
* `90s`, `1h`. Suffixed durations longer than 24 hours are rejected as likely
|
|
1170
|
+
* typos, while bare seconds stay uncapped for backward compatibility. This
|
|
1171
|
+
* intentionally does NOT reuse {@link parseTimeout} from routines.ts — that one
|
|
1172
|
+
* returns milliseconds, has no seconds (`s`) unit, and floors at one minute,
|
|
1173
|
+
* none of which fit hook timeouts (typically 5–600s).
|
|
1171
1174
|
*
|
|
1172
|
-
* Returns the seconds value, or `null` when the input is not a positive number
|
|
1173
|
-
* or a
|
|
1175
|
+
* Returns the seconds value, or `null` when the input is not a positive number,
|
|
1176
|
+
* is not parseable, or is a suffixed duration longer than 24 hours — the caller
|
|
1177
|
+
* decides how to surface that.
|
|
1174
1178
|
*/
|
|
1175
1179
|
export function normalizeHookTimeoutSeconds(value) {
|
|
1176
1180
|
if (typeof value === 'number') {
|
|
@@ -1194,7 +1198,7 @@ export function normalizeHookTimeoutSeconds(value) {
|
|
|
1194
1198
|
const minutes = Number(m[4] || 0);
|
|
1195
1199
|
const seconds = Number(m[5] || 0);
|
|
1196
1200
|
const total = ((weeks * 7 + days) * 24 + hours) * 3600 + minutes * 60 + seconds;
|
|
1197
|
-
return total > 0 ? total : null;
|
|
1201
|
+
return total > 0 && total <= MAX_HOOK_DURATION_SECONDS ? total : null;
|
|
1198
1202
|
}
|
|
1199
1203
|
return null;
|
|
1200
1204
|
}
|