@phnx-labs/agents-cli 1.22.85 → 1.22.86
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 +39 -0
- package/dist/commands/alias.js +7 -3
- package/dist/lib/agent-spec/agents.js +6 -0
- package/dist/lib/daemon/auth-sync-service.js +21 -17
- package/dist/lib/installations/shims.d.ts +6 -4
- package/dist/lib/installations/shims.js +17 -5
- package/dist/lib/secrets-client.d.ts +10 -2
- package/dist/lib/secrets-client.js +12 -4
- package/dist/lib/secrets-policy.d.ts +13 -0
- package/dist/lib/secrets-policy.js +32 -10
- package/dist/lib/self-heal/checks/shims.js +5 -3
- package/package.json +1 -1
- package/scripts/postinstall.js +31 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.86
|
|
4
|
+
|
|
5
|
+
- **Workers now materialize a slot for every registered Claude account, not only
|
|
6
|
+
the ones added through the v2 `accounts add` flow (PHNX-3940).** The daemon's
|
|
7
|
+
slot reconciliation skipped any account row without a `workerCredential` field —
|
|
8
|
+
every account registered before the v2 model — on the assumption that a legacy
|
|
9
|
+
row "resolves its token at spawn". That holds for `agents run claude#<name>` but
|
|
10
|
+
not for the interactive picker, so on a worker whose `auth` bundle already held
|
|
11
|
+
all eight setup-tokens `agents run claude --interactive --device auto` listed the
|
|
12
|
+
installed version labels, showed most of them `logged out`, and launching one put
|
|
13
|
+
a Claude login screen on a headless box. `reconcileLocalWorkerSlots` now resolves
|
|
14
|
+
each row through the same `reservedSyncTargets` the push plan uses (a v2 row's
|
|
15
|
+
reserved `__claude__` key, a legacy row's email-keyed `auth` token), so every
|
|
16
|
+
account whose token is on the box gets a `durable` slot with a 0600
|
|
17
|
+
`.oauth_token` and the seeded identity the adapter injects from. Two related
|
|
18
|
+
fixes ride along: the credential publisher is now a ready **headed** device
|
|
19
|
+
(`electPublisher`, headed-first then by name) instead of the alphabetically first
|
|
20
|
+
ready box, which had made a worker the fleet's source of truth; and slot
|
|
21
|
+
reconciliation runs first in the `auth-sync` tick, so a failed shared-state git
|
|
22
|
+
exchange no longer postpones local provisioning. Source:
|
|
23
|
+
`cli/src/lib/secrets/reserved-sync.ts`, `cli/src/lib/daemon/auth-sync-service.ts`.
|
|
24
|
+
|
|
25
|
+
- **`agents secrets` no longer recurses into the legacy `secrets` shim after an
|
|
26
|
+
in-place upgrade (PHNX-3989).** Boxes that ran agents-cli before the standalone
|
|
27
|
+
`secrets` engine still carry `~/.agents/.cache/shims/secrets`, a command shim that
|
|
28
|
+
`exec`s `agents secrets`. It sits first on PATH and survives an upgrade because its
|
|
29
|
+
baked entrypoint still exists, and 1.22.85 resolved the standalone with a plain
|
|
30
|
+
`which secrets` — so on every such box `agents secrets …` (and the daemon's
|
|
31
|
+
reserved-store sync, which uses the same resolver) re-entered itself through the
|
|
32
|
+
shim without bound. The resolver now skips agents-cli's own shims dir when looking
|
|
33
|
+
for the standalone (`SECRETS_BIN` still wins; a miss still fails loud with the
|
|
34
|
+
install command), and the self-heal shim pass removes that legacy `secrets` shim
|
|
35
|
+
even though its target install is alive, since it can only recurse. The package's
|
|
36
|
+
postinstall — which used to write that very shim on every install, so an upgrade
|
|
37
|
+
kept bringing it back — no longer writes a `secrets` alias at all and removes one
|
|
38
|
+
a previous install left behind; the bare-command aliases are now `sessions`,
|
|
39
|
+
`browser`, `pty`, and `teams`. Install the standalone with
|
|
40
|
+
`npm i -g @phnx-labs/secrets-cli` if `agents secrets` reports it missing.
|
|
41
|
+
|
|
3
42
|
## 1.22.85
|
|
4
43
|
|
|
5
44
|
- **`agents run codex` marks an exhausted account so rotation stops re-picking it
|
package/dist/commands/alias.js
CHANGED
|
@@ -8,9 +8,13 @@ import { setHelpSections } from '../lib/help.js';
|
|
|
8
8
|
function aliasesFile() {
|
|
9
9
|
return path.join(getUserAgentsDir(), 'aliases.json');
|
|
10
10
|
}
|
|
11
|
-
/**
|
|
11
|
+
/**
|
|
12
|
+
* Names that would clobber an agent CLI shim, the `agents` binary itself, or the
|
|
13
|
+
* standalone `secrets` CLI (PHNX-3989): the shims dir sits first on PATH, so an
|
|
14
|
+
* alias named `secrets` would shadow `@phnx-labs/secrets-cli` for every caller.
|
|
15
|
+
*/
|
|
12
16
|
function reservedNames() {
|
|
13
|
-
const reserved = new Set(['agents']);
|
|
17
|
+
const reserved = new Set(['agents', 'secrets']);
|
|
14
18
|
for (const id of ALL_AGENT_IDS)
|
|
15
19
|
reserved.add(AGENTS[id].cliCommand);
|
|
16
20
|
return reserved;
|
|
@@ -85,7 +89,7 @@ export function registerAliasCommand(setupCmd) {
|
|
|
85
89
|
process.exit(1);
|
|
86
90
|
}
|
|
87
91
|
if (reservedNames().has(name)) {
|
|
88
|
-
console.error(chalk.red(`"${name}" is reserved (collides with an agent CLI or the
|
|
92
|
+
console.error(chalk.red(`"${name}" is reserved (collides with an agent CLI, the agents binary, or the standalone secrets CLI).`));
|
|
89
93
|
process.exit(1);
|
|
90
94
|
}
|
|
91
95
|
const target = shimPath(name);
|
|
@@ -127,6 +127,12 @@ export function findInPath(command, options = {}) {
|
|
|
127
127
|
const stat = fs.statSync(full);
|
|
128
128
|
if (!stat.isFile())
|
|
129
129
|
continue;
|
|
130
|
+
// A shell only runs an executable file; `which` skips a mode-644 namesake and
|
|
131
|
+
// so must this, or a stray non-executable `secrets`/`claude` file earlier on
|
|
132
|
+
// PATH would shadow the real binary further down. On win32 the mode bit is
|
|
133
|
+
// meaningless (PATHEXT decides), so the check is POSIX-only.
|
|
134
|
+
if (process.platform !== 'win32')
|
|
135
|
+
fs.accessSync(full, fs.constants.X_OK);
|
|
130
136
|
const native = resolveNativeBinaryPath(command, full, options);
|
|
131
137
|
if (native)
|
|
132
138
|
return native;
|
|
@@ -22,7 +22,24 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
22
22
|
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
23
23
|
}
|
|
24
24
|
async onTick(ctx) {
|
|
25
|
-
const { publishReservedAuthVerdict, syncReservedAuthBundle } = await import('../secrets-policy.js');
|
|
25
|
+
const { publishReservedAuthVerdict, reconcileLocalWorkerSlots, syncReservedAuthBundle, syncReservedStores, } = await import('../secrets-policy.js');
|
|
26
|
+
// Worker-side slot materialization FIRST (PHNX-3940 T6): for each registered
|
|
27
|
+
// account whose durable key is already on this box, create the HOME-shaped
|
|
28
|
+
// slot the picker and spawn read. It touches only local state — the registry
|
|
29
|
+
// copy and the file-backed store — so it never waits on the git exchange
|
|
30
|
+
// below. It used to sit after `if (!transport.success) return`, so a single
|
|
31
|
+
// `git rebase timed out` postponed every slot on the box by another tick.
|
|
32
|
+
// Self-gated on device role: a headed box returns immediately.
|
|
33
|
+
try {
|
|
34
|
+
const slots = reconcileLocalWorkerSlots();
|
|
35
|
+
if (slots.provisioned.length > 0)
|
|
36
|
+
ctx.log('INFO', `auth-sync: provisioned worker slot(s) for ${slots.provisioned.join(', ')}`);
|
|
37
|
+
for (const err of slots.errors)
|
|
38
|
+
ctx.log('WARN', `auth-sync: worker slot ${err.accountId}: ${err.message}`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
|
|
42
|
+
}
|
|
26
43
|
const published = await publishReservedAuthVerdict();
|
|
27
44
|
if (published.error)
|
|
28
45
|
ctx.log('WARN', `auth-sync: verdict: ${published.error}`);
|
|
@@ -44,13 +61,10 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
44
61
|
for (const err of result.errors) {
|
|
45
62
|
ctx.log('WARN', `auth-sync: ${err.device}: ${err.message}`);
|
|
46
63
|
}
|
|
47
|
-
// Generalized per-account, per-key, per-role reserved-store
|
|
64
|
+
// Generalized per-account, per-key, per-role reserved-store push (PHNX-3940
|
|
48
65
|
// T6). On the elected headed publisher this pushes every portable account's
|
|
49
|
-
// reserved `__<harness>__` store to the worker peers missing it
|
|
50
|
-
//
|
|
51
|
-
// self-gates on device role, so exactly one arm acts per box. The push is the
|
|
52
|
-
// only transport — provisioning writes only locally (invariant 1).
|
|
53
|
-
const { syncReservedStores, reconcileLocalWorkerSlots } = await import('../secrets-policy.js');
|
|
66
|
+
// reserved `__<harness>__` store to the worker peers missing it. The push is
|
|
67
|
+
// the only transport — provisioning (above) writes only locally (invariant 1).
|
|
54
68
|
try {
|
|
55
69
|
const stores = await syncReservedStores();
|
|
56
70
|
for (const p of stores.pushed)
|
|
@@ -61,15 +75,5 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
61
75
|
catch (err) {
|
|
62
76
|
ctx.log('WARN', `auth-sync: reserved-store sync: ${err.message}`);
|
|
63
77
|
}
|
|
64
|
-
try {
|
|
65
|
-
const slots = reconcileLocalWorkerSlots();
|
|
66
|
-
if (slots.provisioned.length > 0)
|
|
67
|
-
ctx.log('INFO', `auth-sync: provisioned worker slot(s) for ${slots.provisioned.join(', ')}`);
|
|
68
|
-
for (const err of slots.errors)
|
|
69
|
-
ctx.log('WARN', `auth-sync: worker slot ${err.accountId}: ${err.message}`);
|
|
70
|
-
}
|
|
71
|
-
catch (err) {
|
|
72
|
-
ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
|
|
73
|
-
}
|
|
74
78
|
}
|
|
75
79
|
}
|
|
@@ -358,10 +358,12 @@ export declare function listShimFileNames(): string[];
|
|
|
358
358
|
/**
|
|
359
359
|
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
360
360
|
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|
|
361
|
-
* are
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
* removed when the baked target is gone, so a working shim is never touched
|
|
361
|
+
* are `exec "$AGENTS_BIN" <cmd>` command shims (browser/sessions/pty/teams) that
|
|
362
|
+
* `scripts/postinstall.js` writes for the LIVE install; one baked at a removed
|
|
363
|
+
* install either dies with `exit 127` or shadows the real package bin on PATH.
|
|
364
|
+
* Only removed when the baked target is gone, so a working shim is never touched —
|
|
365
|
+
* except the `LEGACY_SHIMS_ALWAYS_PRUNED` set, which cannot work at all (and which
|
|
366
|
+
* postinstall no longer writes).
|
|
365
367
|
* Returns true if removed.
|
|
366
368
|
*/
|
|
367
369
|
export declare function pruneOrphanedCommandShim(fileName: string): boolean;
|
|
@@ -2165,13 +2165,25 @@ export function listShimFileNames() {
|
|
|
2165
2165
|
return [];
|
|
2166
2166
|
}
|
|
2167
2167
|
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Legacy command shims that are wrong even when their baked install is alive.
|
|
2170
|
+
* `secrets`: the shim `exec`s `agents secrets`, which since PHNX-3989 is a
|
|
2171
|
+
* passthrough to the STANDALONE `secrets` binary — so a `secrets` shim that
|
|
2172
|
+
* re-enters `agents secrets` is a recursion, never a working command. It sits
|
|
2173
|
+
* first on PATH and shadows the real `@phnx-labs/secrets-cli` bin, so it is
|
|
2174
|
+
* pruned unconditionally (the liveness check below is what kept it alive across
|
|
2175
|
+
* every in-place upgrade from 1.22.84).
|
|
2176
|
+
*/
|
|
2177
|
+
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets']);
|
|
2168
2178
|
/**
|
|
2169
2179
|
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
2170
2180
|
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|
|
2171
|
-
* are
|
|
2172
|
-
*
|
|
2173
|
-
*
|
|
2174
|
-
* removed when the baked target is gone, so a working shim is never touched
|
|
2181
|
+
* are `exec "$AGENTS_BIN" <cmd>` command shims (browser/sessions/pty/teams) that
|
|
2182
|
+
* `scripts/postinstall.js` writes for the LIVE install; one baked at a removed
|
|
2183
|
+
* install either dies with `exit 127` or shadows the real package bin on PATH.
|
|
2184
|
+
* Only removed when the baked target is gone, so a working shim is never touched —
|
|
2185
|
+
* except the `LEGACY_SHIMS_ALWAYS_PRUNED` set, which cannot work at all (and which
|
|
2186
|
+
* postinstall no longer writes).
|
|
2175
2187
|
* Returns true if removed.
|
|
2176
2188
|
*/
|
|
2177
2189
|
export function pruneOrphanedCommandShim(fileName) {
|
|
@@ -2192,7 +2204,7 @@ export function pruneOrphanedCommandShim(fileName) {
|
|
|
2192
2204
|
const bin = readAgentsBinFromShim(shimPath);
|
|
2193
2205
|
if (!bin)
|
|
2194
2206
|
return false; // not an AGENTS_BIN-baked shim
|
|
2195
|
-
if (fs.existsSync(bin))
|
|
2207
|
+
if (fs.existsSync(bin) && !LEGACY_SHIMS_ALWAYS_PRUNED.has(fileName))
|
|
2196
2208
|
return false; // its install is still alive — leave it
|
|
2197
2209
|
try {
|
|
2198
2210
|
fs.rmSync(shimPath);
|
|
@@ -56,8 +56,16 @@ export declare function parseBundleValue(raw: BundleValue): {
|
|
|
56
56
|
};
|
|
57
57
|
/**
|
|
58
58
|
* Resolve the standalone `secrets` executable: `$SECRETS_BIN` if set, else the
|
|
59
|
-
* `secrets` command on PATH
|
|
60
|
-
*
|
|
59
|
+
* `secrets` command on PATH — resolved with `findInPath`, which skips agents-cli's
|
|
60
|
+
* own shims dir. That skip is load-bearing, not tidiness: before PHNX-3989 the
|
|
61
|
+
* shims dir carried a `secrets` command shim that `exec`s `agents secrets`, it
|
|
62
|
+
* sits FIRST on PATH, and it survives an in-place upgrade (its baked entrypoint
|
|
63
|
+
* still exists). A plain `which secrets` returned that shim, so `agents secrets`
|
|
64
|
+
* → shim → `agents secrets` → … spawned an unbounded process chain on every box
|
|
65
|
+
* upgraded from 1.22.84 (the whole fleet, via R5 auto-update). A `secrets` inside
|
|
66
|
+
* our shims dir can never be the standalone, so it is not a candidate at all.
|
|
67
|
+
* Cached for the process. A miss throws with install guidance — there is NO
|
|
68
|
+
* fallback to the embedded engine (DIST-1).
|
|
61
69
|
*/
|
|
62
70
|
export declare function resolveSecretsBin(): string;
|
|
63
71
|
/**
|
|
@@ -45,7 +45,7 @@ import { spawn, spawnSync } from 'node:child_process';
|
|
|
45
45
|
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
46
46
|
import { tmpdir } from 'node:os';
|
|
47
47
|
import { join } from 'node:path';
|
|
48
|
-
import {
|
|
48
|
+
import { findInPath } from './agent-spec/agents.js';
|
|
49
49
|
import { getUserAgentsDir } from './state.js';
|
|
50
50
|
/**
|
|
51
51
|
* Wire contract, mirrored from `secrets-cli/src/protocol.ts`. Both sides MUST
|
|
@@ -190,14 +190,22 @@ export function parseBundleValue(raw) {
|
|
|
190
190
|
let cachedBin;
|
|
191
191
|
/**
|
|
192
192
|
* Resolve the standalone `secrets` executable: `$SECRETS_BIN` if set, else the
|
|
193
|
-
* `secrets` command on PATH
|
|
194
|
-
*
|
|
193
|
+
* `secrets` command on PATH — resolved with `findInPath`, which skips agents-cli's
|
|
194
|
+
* own shims dir. That skip is load-bearing, not tidiness: before PHNX-3989 the
|
|
195
|
+
* shims dir carried a `secrets` command shim that `exec`s `agents secrets`, it
|
|
196
|
+
* sits FIRST on PATH, and it survives an in-place upgrade (its baked entrypoint
|
|
197
|
+
* still exists). A plain `which secrets` returned that shim, so `agents secrets`
|
|
198
|
+
* → shim → `agents secrets` → … spawned an unbounded process chain on every box
|
|
199
|
+
* upgraded from 1.22.84 (the whole fleet, via R5 auto-update). A `secrets` inside
|
|
200
|
+
* our shims dir can never be the standalone, so it is not a candidate at all.
|
|
201
|
+
* Cached for the process. A miss throws with install guidance — there is NO
|
|
202
|
+
* fallback to the embedded engine (DIST-1).
|
|
195
203
|
*/
|
|
196
204
|
export function resolveSecretsBin() {
|
|
197
205
|
if (cachedBin)
|
|
198
206
|
return cachedBin;
|
|
199
207
|
const explicit = process.env.SECRETS_BIN?.trim();
|
|
200
|
-
const resolved = explicit && explicit.length > 0 ? explicit :
|
|
208
|
+
const resolved = explicit && explicit.length > 0 ? explicit : findInPath('secrets');
|
|
201
209
|
if (!resolved) {
|
|
202
210
|
throw new SecretsClientError('SECRETS_BIN_MISSING', 'The standalone `secrets` CLI was not found. Install it with:\n' +
|
|
203
211
|
' npm i -g @phnx-labs/secrets-cli\n' +
|
|
@@ -75,9 +75,21 @@ export interface AuthSyncDeps {
|
|
|
75
75
|
localName?: string;
|
|
76
76
|
userAgentsDir?: string;
|
|
77
77
|
isPinned?: (name: string) => boolean;
|
|
78
|
+
peerRole?: (name: string) => ReturnType<typeof selfConfiguredDeviceRole>;
|
|
79
|
+
selfRole?: () => ReturnType<typeof selfConfiguredDeviceRole>;
|
|
78
80
|
push?: (bundle: string, host: string) => Promise<PushBundleResult>;
|
|
79
81
|
sshTarget?: (device: DeviceProfile) => string;
|
|
80
82
|
}
|
|
83
|
+
/**
|
|
84
|
+
* The one device that pushes credentials this tick. A ready HEADED device
|
|
85
|
+
* (`personal`/`desktop`) wins over any ready worker: the headed box is where
|
|
86
|
+
* tokens are minted (invariant 7), so it is the copy of record; a worker holds
|
|
87
|
+
* only what was once pushed to it. Name order breaks ties so every box elects
|
|
88
|
+
* the same publisher from the same shared verdicts. Before this rule the sort
|
|
89
|
+
* was by name alone, which elected `mac-mini` (a worker with a 6-day-old copy)
|
|
90
|
+
* over `zion`, and zion then skipped every peer as a non-publisher.
|
|
91
|
+
*/
|
|
92
|
+
export declare function electPublisher(ready: readonly string[], roleOf: (name: string) => ReturnType<typeof selfConfiguredDeviceRole>): string | null;
|
|
81
93
|
export interface PublishAuthVerdictOptions {
|
|
82
94
|
inspectLocal?: () => {
|
|
83
95
|
exists: boolean;
|
|
@@ -181,6 +193,7 @@ export interface ReservedStoreSyncDeps {
|
|
|
181
193
|
readMetaFn?: () => Pick<Meta, 'accounts' | 'deviceAccounts'>;
|
|
182
194
|
isPinned?: (name: string) => boolean;
|
|
183
195
|
peerRole?: (name: string) => ReturnType<typeof selfConfiguredDeviceRole>;
|
|
196
|
+
selfRole?: () => ReturnType<typeof selfConfiguredDeviceRole>;
|
|
184
197
|
localReady?: boolean;
|
|
185
198
|
/** Does THIS publisher hold (bundle, key) to push? Defaults to a local bundle read. */
|
|
186
199
|
hasLocalKey?: (bundle: string, key: string) => boolean;
|
|
@@ -122,6 +122,19 @@ export function planAuthBundlePush(localAuthOk, localIsPublisher, devices) {
|
|
|
122
122
|
return { action: 'push', device: device.name };
|
|
123
123
|
});
|
|
124
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The one device that pushes credentials this tick. A ready HEADED device
|
|
127
|
+
* (`personal`/`desktop`) wins over any ready worker: the headed box is where
|
|
128
|
+
* tokens are minted (invariant 7), so it is the copy of record; a worker holds
|
|
129
|
+
* only what was once pushed to it. Name order breaks ties so every box elects
|
|
130
|
+
* the same publisher from the same shared verdicts. Before this rule the sort
|
|
131
|
+
* was by name alone, which elected `mac-mini` (a worker with a 6-day-old copy)
|
|
132
|
+
* over `zion`, and zion then skipped every peer as a non-publisher.
|
|
133
|
+
*/
|
|
134
|
+
export function electPublisher(ready, roleOf) {
|
|
135
|
+
const rank = (name) => (isHeadedDeviceRole(roleOf(name)) ? 0 : 1);
|
|
136
|
+
return [...ready].sort((a, b) => rank(a) - rank(b) || normalizeHost(a).localeCompare(normalizeHost(b)))[0] ?? null;
|
|
137
|
+
}
|
|
125
138
|
function defaultDevices() {
|
|
126
139
|
return Object.values(loadDevicesSync());
|
|
127
140
|
}
|
|
@@ -175,8 +188,9 @@ export async function syncReservedAuthBundle(deps = {}) {
|
|
|
175
188
|
if (localStatus === 'ready' && !readyPublishers.some((name) => normalizeHost(name) === localNorm)) {
|
|
176
189
|
readyPublishers.push(localName);
|
|
177
190
|
}
|
|
178
|
-
|
|
179
|
-
|
|
191
|
+
const peerRole = deps.peerRole ?? configuredDeviceRole;
|
|
192
|
+
const selfRole = deps.selfRole ?? selfConfiguredDeviceRole;
|
|
193
|
+
result.publisher = electPublisher(readyPublishers, (name) => (normalizeHost(name) === localNorm ? selfRole() : peerRole(name)));
|
|
180
194
|
const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
|
|
181
195
|
const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
|
|
182
196
|
const plan = planAuthBundlePush(localStatus === 'ready', localIsPublisher, devices.map((device) => ({
|
|
@@ -386,8 +400,9 @@ export async function syncReservedStores(deps = {}) {
|
|
|
386
400
|
.map((s) => s.device);
|
|
387
401
|
if (localReady && !readyPublishers.some((n) => normalizeHost(n) === localNorm))
|
|
388
402
|
readyPublishers.push(localName);
|
|
389
|
-
|
|
390
|
-
|
|
403
|
+
const peerRole = deps.peerRole ?? configuredDeviceRole;
|
|
404
|
+
const selfRole = deps.selfRole ?? selfConfiguredDeviceRole;
|
|
405
|
+
result.publisher = electPublisher(readyPublishers, (name) => (normalizeHost(name) === localNorm ? selfRole() : peerRole(name)));
|
|
391
406
|
const localIsPublisher = result.publisher !== null && normalizeHost(result.publisher) === localNorm;
|
|
392
407
|
if (targets.length === 0 || !localIsPublisher) {
|
|
393
408
|
for (const d of devices) {
|
|
@@ -397,7 +412,6 @@ export async function syncReservedStores(deps = {}) {
|
|
|
397
412
|
}
|
|
398
413
|
const memo = readDeliveryMemo(deps.cacheDir);
|
|
399
414
|
const pinned = deps.isPinned ?? ((name) => isHostPinned(name, managedKnownHostsPath()));
|
|
400
|
-
const peerRole = deps.peerRole ?? configuredDeviceRole;
|
|
401
415
|
const peers = devices.map((d) => ({
|
|
402
416
|
name: d.name,
|
|
403
417
|
headed: isHeadedDeviceRole(peerRole(d.name)),
|
|
@@ -459,11 +473,19 @@ export function reconcileLocalWorkerSlots(deps = {}) {
|
|
|
459
473
|
const slots = readSlots(meta);
|
|
460
474
|
const hasLocalKey = deps.hasLocalKey ?? defaultHasLocalKey;
|
|
461
475
|
const provision = deps.provision ?? provisionWorkerSlot;
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
476
|
+
const byId = new Map(listNativeAccounts(meta).map((account) => [account.id, account]));
|
|
477
|
+
// Resolve each account to the one (bundle, key) the push plan uses: a T1 row's
|
|
478
|
+
// reserved `__<harness>__` key, or the legacy `auth` key by email for a claude
|
|
479
|
+
// row predating T1. Both are worker credentials this box may already hold, so
|
|
480
|
+
// both get a slot. Skipping the legacy rows here is what left every registered
|
|
481
|
+
// pre-T1 account without a slot on every worker while its token sat in `auth`:
|
|
482
|
+
// the picker then fell back to identity-less version homes and Claude showed a
|
|
483
|
+
// login screen on a headless box (the mac-mini 2026-09-06 incident).
|
|
484
|
+
for (const target of reservedSyncTargets(meta)) {
|
|
485
|
+
const account = byId.get(target.accountId);
|
|
486
|
+
if (!account)
|
|
487
|
+
continue;
|
|
488
|
+
if (!hasLocalKey(target.bundle, target.key)) {
|
|
467
489
|
result.skipped.push({ accountId: account.id, reason: 'durable key not synced yet' });
|
|
468
490
|
continue;
|
|
469
491
|
}
|
|
@@ -46,10 +46,12 @@ export const shimsCheck = {
|
|
|
46
46
|
if (!ctx.dryRun && removeLegacyUserShim(agent))
|
|
47
47
|
fixed.push(`removed legacy ${cmd} shim`);
|
|
48
48
|
}
|
|
49
|
-
// Prune orphaned legacy command shims (browser/
|
|
50
|
-
//
|
|
49
|
+
// Prune orphaned legacy command shims (browser/sessions/… left by a removed
|
|
50
|
+
// install) whose baked AGENTS_BIN is dead — the current source never
|
|
51
51
|
// regenerates them, and they either die with exit 127 or shadow the real
|
|
52
|
-
// package bin on PATH. Only removes shims whose target install is gone
|
|
52
|
+
// package bin on PATH. Only removes shims whose target install is gone — plus
|
|
53
|
+
// the legacy `secrets` shim regardless: it re-enters `agents secrets`, which is
|
|
54
|
+
// a passthrough to the standalone binary since PHNX-3989, so it can only recurse.
|
|
53
55
|
if (!ctx.dryRun) {
|
|
54
56
|
for (const name of listShimFileNames()) {
|
|
55
57
|
if (pruneOrphanedCommandShim(name))
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.86",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -50,9 +50,39 @@ function shellQuote(value) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
// Shorthands that delegate to the installed agents-cli entrypoint.
|
|
53
|
-
const ALIASES = ['sessions', '
|
|
53
|
+
const ALIASES = ['sessions', 'browser', 'pty', 'teams'];
|
|
54
|
+
|
|
55
|
+
// Aliases this script USED to write and must now remove on every install.
|
|
56
|
+
// `secrets`: the name belongs to the standalone `@phnx-labs/secrets-cli` binary
|
|
57
|
+
// (PHNX-3989); `agents secrets` is a passthrough to it, so an alias shim that
|
|
58
|
+
// `exec`s `agents secrets` can only re-enter the passthrough, and it shadows the
|
|
59
|
+
// real `secrets` bin because the shims dir sits first on PATH. The self-heal
|
|
60
|
+
// shim pass prunes it too, but that only runs from a daemon or a TTY session —
|
|
61
|
+
// postinstall is what every upgrade path actually executes, so it cleans up here.
|
|
62
|
+
const RETIRED_ALIASES = ['secrets'];
|
|
63
|
+
|
|
64
|
+
function removeRetiredAliasShims() {
|
|
65
|
+
for (const name of RETIRED_ALIASES) {
|
|
66
|
+
for (const file of [path.join(SHIMS_DIR, name), path.join(SHIMS_DIR, name + '.cmd')]) {
|
|
67
|
+
let content;
|
|
68
|
+
try {
|
|
69
|
+
content = fs.readFileSync(file, 'utf8');
|
|
70
|
+
} catch {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// Only ever remove OUR alias — the POSIX shim ends in `<name> "$@"`, the
|
|
74
|
+
// Windows companion in `<name> %*` — never an unrelated file someone placed
|
|
75
|
+
// under that name, and never a user's own `agents setup alias` shim, which
|
|
76
|
+
// is marked `# Alias shim:` (the same guard pruneOrphanedCommandShim keeps).
|
|
77
|
+
if (content.includes('# Alias shim:')) continue;
|
|
78
|
+
if (!content.includes(`${name} "$@"`) && !content.includes(`${name} %*`)) continue;
|
|
79
|
+
fs.rmSync(file, { force: true });
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
54
83
|
|
|
55
84
|
function writeAliasShims() {
|
|
85
|
+
removeRetiredAliasShims();
|
|
56
86
|
const written = [];
|
|
57
87
|
for (const name of ALIASES) {
|
|
58
88
|
const target = path.join(SHIMS_DIR, name);
|