@phnx-labs/agents-cli 1.22.98 → 1.22.100
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 +14 -0
- package/README.md +16 -7
- package/dist/commands/setup-secrets.d.ts +18 -11
- package/dist/commands/setup-secrets.js +50 -18
- package/dist/lib/account-catalog.js +20 -4
- package/dist/lib/accounts/slots.d.ts +19 -0
- package/dist/lib/accounts/slots.js +61 -1
- package/dist/lib/auth-health.d.ts +79 -5
- package/dist/lib/auth-health.js +133 -33
- package/dist/lib/browser/arc-discovery.d.ts +1 -1
- package/dist/lib/browser/arc-discovery.js +14 -4
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -4
- package/dist/lib/daemon/auth-sync-service.js +49 -21
- package/dist/lib/daemon/usage-sync-service.d.ts +26 -7
- package/dist/lib/daemon/usage-sync-service.js +35 -8
- package/dist/lib/daemon-ticks.d.ts +22 -5
- package/dist/lib/daemon-ticks.js +38 -13
- package/dist/lib/fleet-shared-repo-sync.d.ts +7 -0
- package/dist/lib/fleet-shared-repo-sync.js +66 -3
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/reconcile-and-repair.d.ts +8 -0
- package/dist/lib/reconcile-and-repair.js +32 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.100
|
|
4
|
+
|
|
5
|
+
- **The daemon's 20-minute auth-probe reuse works again on a box that has uninstalled a version (PHNX-4051).** `isCachedFleetAuthProbeFresh` required EVERY cached auth row to be under 20 minutes old, including rows left behind for versions that are no longer installed. Nothing re-probes those — the probe only walks homes that exist — so their timestamp froze and the check was permanently false: the 3-minute tick live-probed the rate-limited `/api/oauth/usage` for every account on every tick, re-arming the per-account 429 backoff and keeping the usage refresher parked (the RUSH-2998 failure the reuse window exists to prevent; observed on a worker holding rows dated Sep 2 / Sep 6 for uninstalled Claude versions). Freshness now considers only rows for currently installed `(agent, version)` homes and account slots, and the tick prunes the orphan rows when it writes the cache, so `agents view` and fleet status stop reporting a verdict for a version that is gone. `agents devices ping [--strict]` still forces a genuinely live probe. Source: `cli/src/lib/daemon-ticks.ts` (`installedAuthRows`, `isCachedFleetAuthProbeFresh`, `refreshLocalFleetAuthState`), `cli/src/lib/auth-health.ts` (`authTargetKey`, `enumerateLocalAuthInstalls`, `localAuthTargetKeys`, `writeFleetAuthRows`).
|
|
6
|
+
|
|
7
|
+
- **Fleet usage-sync transport no longer starves `--device auto` (PHNX-4051).** The daemon's shared-state git exchange failed on every tick two ways, so workers never received a fresh Claude usage snapshot and the 40-min placement gate turned every worker into "no ready device". Fixed both at the source: (1) the usage-sync tick is now the single committer — it also publishes the reserved-auth verdict, and auth-sync no longer runs its own `syncFleetSharedStateRepo`, so the two ticks can't contend for the one shared-repo lock ("Lock file is already being held"); (2) the untracked-collision scan is scoped to the tracked roots origin carries (`git ls-tree origin/<branch>`) — every path origin tracks stays in scope, so no path the rebase can overwrite is missed, while an untracked-only tree origin never tracks (like artifacts-cli's `~/.agents/artifact-history/`) can never push `git ls-files --others` past the 1 MiB output cap. auth-sync's credential pushes are also gated on the exchange's freshness marker, so they never push off peer state older than one tick interval. Source: `cli/src/lib/fleet-shared-repo-sync.ts`, `cli/src/lib/daemon/usage-sync-service.ts`, `cli/src/lib/daemon/auth-sync-service.ts`.
|
|
8
|
+
|
|
9
|
+
- **An untitled Arc Space no longer hides every Arc Space from `agents browser`.** `parseSidebarSpaces` treated the display-only Space title as part of a Space's identity and rejected the whole sidebar when one Space had none, so `agents browser start` warned that a configured `arc-gmail` "can't launch on this machine" while `agents browser profiles doctor arc-gmail` saw Arc and the Space. Identity is the stable Space id and its profile binding; a Space Arc stored without a title (key absent or `null`) is now listed as `arc-space` (two or more get their Space-id prefix appended, as same-titled Spaces already do). A mismatched id, a duplicate id, an unknown or malformed profile mapping, and a title that is present but not text still fail discovery closed. Source: `cli/src/lib/browser/arc-discovery.ts`, `cli/src/lib/browser/arc-discovery.test.ts`, `cli/src/lib/browser/testdata/arc/`.
|
|
10
|
+
|
|
11
|
+
## 1.22.99
|
|
12
|
+
|
|
13
|
+
- **`agents setup secrets` installs the published secrets CLI instead of only printing a hint (PHNX-3989).** A missing `secrets` binary tries `agents clis install secrets` then a pinned `npm i -g @phnx-labs/secrets-cli@0.1.2`, then hands off to `secrets migrate`. Docs point at the standalone engine; the broker is `secrets _agent-run` on macOS, not the agents daemon. Source: `cli/src/commands/setup-secrets.ts`, `cli/docs/secrets.md`.
|
|
14
|
+
|
|
15
|
+
- `agents accounts list` / `agents view` no longer report a signed-in account slot as `MISSING`. A slot's STATE now comes from a live read of the slot's own credential and from the daemon's auth probe, which now covers account slots (`slot:<accountId>` rows) as well as version homes. Before, a slot's verdict was written only by `accounts add/login`, the daemon never re-derived it, and `signedIn` was read from an unrelated version home — so a slot re-materialized as `unconfigured` while the device doc was unreadable (a duplicate YAML key from a shared-repo autostash merge) stayed `MISSING` forever even though `agents run` used its login fine.
|
|
16
|
+
|
|
3
17
|
## 1.22.98
|
|
4
18
|
|
|
5
19
|
- `agents run codex#<account>` on macOS now runs as that account. The SUN_LEN short-home relocation keyed `CODEX_HOME` by version alone, so an account slot (long enough to overflow too) was silently redirected to the default version's `~/.agents/.codex-homes/<version>/.codex` and its login — `agents run codex#getrush` opened the gmail session while the banner still said `getrush`. The short home is now keyed by the origin (`a-<account id>` for a slot). A version/account reinstall that recreates a fresh `.codex` adopts its existing short home (which holds the login) instead of failing, and a slot mis-linked onto a foreign home by the old layout is repointed to its own home rather than run as the wrong account.
|
package/README.md
CHANGED
|
@@ -1228,19 +1228,28 @@ replacement (`add` / `login` / `default` / `run <harness>#<name>`).
|
|
|
1228
1228
|
|
|
1229
1229
|
## Secrets
|
|
1230
1230
|
|
|
1231
|
-
|
|
1232
|
-
> On Windows (non-WSL), use environment variables or a `.env` file instead.
|
|
1231
|
+
Secrets are the standalone **`secrets` CLI** (`@phnx-labs/secrets-cli`). agi-cli does not ship the engine. `agents secrets` is a passthrough to the same binary.
|
|
1233
1232
|
|
|
1234
1233
|
```bash
|
|
1235
|
-
#
|
|
1236
|
-
agents
|
|
1237
|
-
|
|
1238
|
-
|
|
1234
|
+
# Install (pick one — no extra env vars)
|
|
1235
|
+
agents clis install secrets
|
|
1236
|
+
# or
|
|
1237
|
+
npm i -g @phnx-labs/secrets-cli@0.1.2
|
|
1238
|
+
# or, after agents is installed:
|
|
1239
|
+
agents setup secrets
|
|
1240
|
+
```
|
|
1239
1241
|
|
|
1240
|
-
|
|
1242
|
+
```bash
|
|
1243
|
+
# API keys in Keychain, not in .env files.
|
|
1244
|
+
secrets create prod-stripe
|
|
1245
|
+
secrets add prod-stripe STRIPE_SECRET_KEY # Prompts, stores in Keychain
|
|
1241
1246
|
agents run claude "charge a test card" --secrets prod-stripe
|
|
1242
1247
|
```
|
|
1243
1248
|
|
|
1249
|
+
> **Platform:** macOS Keychain, Linux libsecret / file store. On Windows (non-WSL), use environment variables or a `.env` file instead.
|
|
1250
|
+
|
|
1251
|
+
The macOS broker is **not** the agents daemon. It is `secrets _agent-run` (started by `secrets start` or on first unlock), socket under `$SECRETS_HOME/.cache/helpers/secrets-agent/` (`~/.agents` when you go through `agents secrets`). Linux has no broker. `agents daemon` never hosts it.
|
|
1252
|
+
|
|
1244
1253
|
<p align="center">
|
|
1245
1254
|
<img src="assets/secrets.svg" alt="How agi-cli secrets work: bundle definitions live in the macOS Keychain alongside their values, agi-cli resolves at runtime and injects the env into the child process" width="100%" />
|
|
1246
1255
|
</p>
|
|
@@ -1,22 +1,29 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents setup secrets` — install
|
|
3
|
-
* then
|
|
2
|
+
* `agents setup secrets` — install the standalone `secrets` CLI if missing,
|
|
3
|
+
* then hand off to its own `secrets migrate` onboarding (PHNX-3989).
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* in-process fallback when it isn't installed: this command names the install
|
|
10
|
-
* command and stops.
|
|
5
|
+
* The engine lives in `@phnx-labs/secrets-cli`. agents-cli never rebundles it
|
|
6
|
+
* (DIST-1). A missing binary is a routine install, not a fatal gap: try
|
|
7
|
+
* `agents clis install secrets` (system `clis/secrets.yaml`) then a pinned
|
|
8
|
+
* `npm i -g`. Users do not set extra env vars.
|
|
11
9
|
*/
|
|
12
10
|
import type { Command } from 'commander';
|
|
11
|
+
export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.2";
|
|
12
|
+
export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.2";
|
|
13
13
|
export declare function setupSecretsPrefsPath(): string;
|
|
14
14
|
/** True when the standalone `secrets` executable resolves ($SECRETS_BIN or PATH). */
|
|
15
15
|
export declare function isSecretsCliInstalled(): boolean;
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* Install the published secrets CLI. Prefers the host-CLI manifest
|
|
18
|
+
* (`clis/secrets.yaml` in the system repo) so doctor/clis stay one path.
|
|
19
|
+
* Falls back to a pinned npm global install. Returns whether `secrets` is
|
|
20
|
+
* on PATH afterwards. Never writes an `agents secrets` shim.
|
|
21
|
+
*/
|
|
22
|
+
export declare function installSecretsCli(): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Install the standalone if missing, then hand off to `secrets migrate`.
|
|
25
|
+
* Returns whether setup is now complete (installed, and migrate exited 0
|
|
26
|
+
* when it ran).
|
|
20
27
|
*/
|
|
21
28
|
export declare function runSecretsSetupWizard(): Promise<boolean>;
|
|
22
29
|
/** Register `agents setup secrets` under the parent `setup` command. */
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agents setup secrets` — install
|
|
3
|
-
* then
|
|
2
|
+
* `agents setup secrets` — install the standalone `secrets` CLI if missing,
|
|
3
|
+
* then hand off to its own `secrets migrate` onboarding (PHNX-3989).
|
|
4
4
|
*
|
|
5
|
-
* The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* in-process fallback when it isn't installed: this command names the install
|
|
10
|
-
* command and stops.
|
|
5
|
+
* The engine lives in `@phnx-labs/secrets-cli`. agents-cli never rebundles it
|
|
6
|
+
* (DIST-1). A missing binary is a routine install, not a fatal gap: try
|
|
7
|
+
* `agents clis install secrets` (system `clis/secrets.yaml`) then a pinned
|
|
8
|
+
* `npm i -g`. Users do not set extra env vars.
|
|
11
9
|
*/
|
|
12
10
|
import chalk from 'chalk';
|
|
13
11
|
import * as fs from 'fs';
|
|
14
12
|
import * as path from 'path';
|
|
15
13
|
import { spawnSync } from 'node:child_process';
|
|
16
14
|
import { getHistoryDir } from '../lib/state.js';
|
|
17
|
-
import { resolveSecretsBin, invocation, SecretsClientError } from '../lib/secrets-client.js';
|
|
18
|
-
|
|
15
|
+
import { resolveSecretsBin, invocation, SecretsClientError, _resetSecretsClientForTest } from '../lib/secrets-client.js';
|
|
16
|
+
import { installCli, resolveCliManifest } from '../lib/cli-resources.js';
|
|
17
|
+
export const SECRETS_CLI_PACKAGE = '@phnx-labs/secrets-cli@0.1.2';
|
|
18
|
+
export const INSTALL_HINT = `agents clis install secrets # or: npm i -g ${SECRETS_CLI_PACKAGE}`;
|
|
19
19
|
export function setupSecretsPrefsPath() {
|
|
20
20
|
return path.join(getHistoryDir(), 'setup', 'secrets.json');
|
|
21
21
|
}
|
|
@@ -37,16 +37,48 @@ function recordSetupComplete() {
|
|
|
37
37
|
fs.writeFileSync(file, JSON.stringify({ updatedAt: new Date().toISOString() }, null, 2) + '\n', { mode: 0o600 });
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
40
|
+
* Install the published secrets CLI. Prefers the host-CLI manifest
|
|
41
|
+
* (`clis/secrets.yaml` in the system repo) so doctor/clis stay one path.
|
|
42
|
+
* Falls back to a pinned npm global install. Returns whether `secrets` is
|
|
43
|
+
* on PATH afterwards. Never writes an `agents secrets` shim.
|
|
44
|
+
*/
|
|
45
|
+
export function installSecretsCli() {
|
|
46
|
+
if (isSecretsCliInstalled())
|
|
47
|
+
return true;
|
|
48
|
+
const manifest = resolveCliManifest('secrets');
|
|
49
|
+
if (manifest) {
|
|
50
|
+
console.log(chalk.gray(`Installing ${manifest.name} via agents clis…`));
|
|
51
|
+
const result = installCli(manifest);
|
|
52
|
+
_resetSecretsClientForTest();
|
|
53
|
+
if (result.installed)
|
|
54
|
+
return true;
|
|
55
|
+
if (result.error)
|
|
56
|
+
console.error(chalk.gray(result.error));
|
|
57
|
+
}
|
|
58
|
+
console.log(chalk.gray(`Installing ${SECRETS_CLI_PACKAGE}…`));
|
|
59
|
+
const r = spawnSync('npm', ['install', '-g', SECRETS_CLI_PACKAGE], { stdio: 'inherit' });
|
|
60
|
+
_resetSecretsClientForTest();
|
|
61
|
+
if (r.error) {
|
|
62
|
+
console.error(chalk.red(`npm install failed: ${r.error.message}`));
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
if ((r.status ?? 1) !== 0)
|
|
66
|
+
return false;
|
|
67
|
+
return isSecretsCliInstalled();
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Install the standalone if missing, then hand off to `secrets migrate`.
|
|
71
|
+
* Returns whether setup is now complete (installed, and migrate exited 0
|
|
72
|
+
* when it ran).
|
|
43
73
|
*/
|
|
44
74
|
export async function runSecretsSetupWizard() {
|
|
45
75
|
if (!isSecretsCliInstalled()) {
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
76
|
+
if (!installSecretsCli()) {
|
|
77
|
+
console.log(chalk.yellow('The standalone `secrets` CLI is not installed.'));
|
|
78
|
+
console.log(chalk.gray('Install it, then re-run `agents setup secrets`:'));
|
|
79
|
+
console.log(chalk.cyan(` ${INSTALL_HINT}`));
|
|
80
|
+
return false;
|
|
81
|
+
}
|
|
50
82
|
}
|
|
51
83
|
const bin = resolveSecretsBin();
|
|
52
84
|
const { command, prefix } = invocation(bin);
|
|
@@ -60,7 +92,7 @@ export async function runSecretsSetupWizard() {
|
|
|
60
92
|
export function registerSetupSecretsCommand(setupCmd) {
|
|
61
93
|
setupCmd
|
|
62
94
|
.command('secrets')
|
|
63
|
-
.description('Install
|
|
95
|
+
.description('Install the standalone `secrets` CLI if missing, then run its `secrets migrate` onboarding.')
|
|
64
96
|
.action(async () => {
|
|
65
97
|
if (!(await runSecretsSetupWizard()))
|
|
66
98
|
process.exitCode = 1;
|
|
@@ -8,7 +8,7 @@ import { readSlots } from './accounts/slots.js';
|
|
|
8
8
|
import { harnessWorkerIsPerDevice } from './harness-auth-capabilities.js';
|
|
9
9
|
import { hasKeychainTokenSync, isSecretsTransportError } from './secrets-client.js';
|
|
10
10
|
import { isLaunchableSignedIn as isCredentialLaunchable } from './accounting/rotate.js';
|
|
11
|
-
import { authCacheKey, readAuthHealthCache } from './auth-health.js';
|
|
11
|
+
import { authCacheKey, readAuthHealthCache, slotAuthVersionKey } from './auth-health.js';
|
|
12
12
|
import { readFleetSharedDeviceStates } from './fleet-shared-state.js';
|
|
13
13
|
import { machineId } from './machine-id.js';
|
|
14
14
|
import { isHeadedDeviceRole, selfConfiguredDeviceRole } from './device-config.js';
|
|
@@ -245,9 +245,21 @@ export async function loadAccountCatalog() {
|
|
|
245
245
|
const localHome = row.installations.find((home) => home.label === row.home)
|
|
246
246
|
?? row.installations.find((home) => home.signedIn)
|
|
247
247
|
?? row.installations[0];
|
|
248
|
-
const cached = localHome ? auth[authCacheKey(host, row.agent, localHome.label)] : undefined;
|
|
249
248
|
const slot = row.id ? localSlots[row.id] : undefined;
|
|
250
|
-
|
|
249
|
+
// A slot account's local truth is the SLOT — its own credential file and its
|
|
250
|
+
// own daemon probe row (keyed `slot:<id>`) — never the version home its label
|
|
251
|
+
// happens to match. Reading the version home here is what rendered a
|
|
252
|
+
// signed-in slot MISSING: the slot record held ensureSlot's `unconfigured`
|
|
253
|
+
// default, the daemon never probed the slot, and `signedIn` came from an
|
|
254
|
+
// unrelated home.
|
|
255
|
+
const slotSignedIn = slot
|
|
256
|
+
? await getAccountInfo(row.agent, slot.slotDir)
|
|
257
|
+
.then((info) => isLaunchableSignedIn(row.agent, slot.slotDir, info))
|
|
258
|
+
.catch(() => false)
|
|
259
|
+
: false;
|
|
260
|
+
const cached = (slot && row.id ? auth[authCacheKey(host, row.agent, slotAuthVersionKey(row.id))] : undefined)
|
|
261
|
+
?? (localHome ? auth[authCacheKey(host, row.agent, localHome.label)] : undefined);
|
|
262
|
+
const observation = resolveLocalAccountObservation(slot, cached, slot ? slotSignedIn : localHome?.signedIn === true);
|
|
251
263
|
const local = {
|
|
252
264
|
device: host,
|
|
253
265
|
authMode: observation.authMode
|
|
@@ -529,8 +541,12 @@ export function listDevicesWithoutAccountVerdicts(userAgentsDir) {
|
|
|
529
541
|
return out.sort((a, b) => a.localeCompare(b));
|
|
530
542
|
}
|
|
531
543
|
function normalizeAuthVerdict(verdict, signedIn) {
|
|
544
|
+
// `unconfigured` is the slot record's DEFAULT (ensureSlot), not a probe
|
|
545
|
+
// result — the daemon never publishes it (probeLocalFleetAuth drops those
|
|
546
|
+
// rows). So it says nothing about the credential on disk; the live signedIn
|
|
547
|
+
// read of the slot decides, exactly as it does when no verdict exists at all.
|
|
532
548
|
if (verdict === 'unconfigured')
|
|
533
|
-
return 'missing';
|
|
549
|
+
return signedIn ? 'unverified' : 'missing';
|
|
534
550
|
if (verdict === 'error')
|
|
535
551
|
return signedIn ? 'unverified' : 'missing';
|
|
536
552
|
return verdict ?? (signedIn ? 'unverified' : 'missing');
|
|
@@ -3,6 +3,25 @@ export type { DeviceAccountSlot };
|
|
|
3
3
|
export declare function slotDir(harness: AgentId, accountId: string): string;
|
|
4
4
|
export declare function readSlots(meta: Pick<Meta, 'deviceAccounts'>): Record<string, DeviceAccountSlot>;
|
|
5
5
|
export declare function recordSlot(accountId: string, slot: DeviceAccountSlot): void;
|
|
6
|
+
export interface SlotProjection {
|
|
7
|
+
accountId: string;
|
|
8
|
+
/** Account name as registered (`agents accounts list`). */
|
|
9
|
+
name: string;
|
|
10
|
+
slotDir: string;
|
|
11
|
+
/** The version home the slot was projected from. */
|
|
12
|
+
from: string;
|
|
13
|
+
/** `<kind>/<name>` artifacts removed because the source no longer has them. */
|
|
14
|
+
pruned: string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Re-project settings and resources into every account slot this device holds
|
|
18
|
+
* for `harness`, from the harness's default version home. `ensureSlot` does
|
|
19
|
+
* this once at `agents accounts add`; every reconcile calls this so a slot
|
|
20
|
+
* never lags the version home it was cloned from (PHNX-3940). Credentials are
|
|
21
|
+
* never touched: `carryForwardSettings` and the resource writers exclude them.
|
|
22
|
+
* A slot whose directory is gone is skipped, not recreated.
|
|
23
|
+
*/
|
|
24
|
+
export declare function projectAccountSlots(harness: AgentId): SlotProjection[];
|
|
6
25
|
/**
|
|
7
26
|
* Create the HOME-shaped slot dir and project settings/resources from the
|
|
8
27
|
* managed install. Does not copy credentials (carryForwardSettings excludes
|
|
@@ -17,7 +17,7 @@ import { agentConfigDirName } from '../agents.js';
|
|
|
17
17
|
import { harnessAuth, harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
|
|
18
18
|
import { getGlobalDefault, getVersionHomePath, listInstalledVersions } from '../installations/store.js';
|
|
19
19
|
import { carryForwardSettings } from '../settings-manifest.js';
|
|
20
|
-
import { getHistoryDir, updateMeta } from '../state.js';
|
|
20
|
+
import { getHistoryDir, readMeta, updateMeta } from '../state.js';
|
|
21
21
|
import { ALL_RESOURCE_KINDS, getDetector, getWriter, kindToCapability } from '../staleness/registry.js';
|
|
22
22
|
import { supports } from '../capabilities.js';
|
|
23
23
|
import { isAgentId } from '../types.js';
|
|
@@ -72,6 +72,66 @@ function projectResources(harness, version, destHome, fromHome) {
|
|
|
72
72
|
writer.write({ version, versionHome: destHome, selection: names, cwd });
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* Remove slot artifacts whose source name is gone. Only the name-keyed kinds
|
|
77
|
+
* that implement `remove` (commands, skills, hooks) are pruned; wholesale kinds
|
|
78
|
+
* (rules, permissions) are rewritten by the projection itself.
|
|
79
|
+
*/
|
|
80
|
+
function pruneSlotResources(harness, version, slotHome, fromHome) {
|
|
81
|
+
const cwd = process.cwd();
|
|
82
|
+
const pruned = [];
|
|
83
|
+
for (const kind of ALL_RESOURCE_KINDS) {
|
|
84
|
+
if (!supports(harness, kindToCapability(kind), version).ok)
|
|
85
|
+
continue;
|
|
86
|
+
const writer = getWriter(kind, harness);
|
|
87
|
+
const detector = getDetector(kind, harness);
|
|
88
|
+
if (!writer?.remove || !detector)
|
|
89
|
+
continue;
|
|
90
|
+
const source = new Set(detector.list({ version, versionHome: fromHome, cwd }));
|
|
91
|
+
for (const name of detector.list({ version, versionHome: slotHome, cwd })) {
|
|
92
|
+
if (source.has(name))
|
|
93
|
+
continue;
|
|
94
|
+
if (writer.remove({ version, versionHome: slotHome, name, cwd }).removed)
|
|
95
|
+
pruned.push(`${kind}/${name}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return pruned;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Re-project settings and resources into every account slot this device holds
|
|
102
|
+
* for `harness`, from the harness's default version home. `ensureSlot` does
|
|
103
|
+
* this once at `agents accounts add`; every reconcile calls this so a slot
|
|
104
|
+
* never lags the version home it was cloned from (PHNX-3940). Credentials are
|
|
105
|
+
* never touched: `carryForwardSettings` and the resource writers exclude them.
|
|
106
|
+
* A slot whose directory is gone is skipped, not recreated.
|
|
107
|
+
*/
|
|
108
|
+
export function projectAccountSlots(harness) {
|
|
109
|
+
const version = sourceVersion(harness);
|
|
110
|
+
if (!version)
|
|
111
|
+
return [];
|
|
112
|
+
const fromHome = getVersionHomePath(harness, version);
|
|
113
|
+
if (!fs.existsSync(fromHome))
|
|
114
|
+
return [];
|
|
115
|
+
const meta = readMeta();
|
|
116
|
+
const slots = readSlots(meta);
|
|
117
|
+
const native = { ...meta.accounts?.native, ...meta.deviceAccounts?.native };
|
|
118
|
+
const projected = [];
|
|
119
|
+
for (const account of Object.values(native)) {
|
|
120
|
+
if (account.agent !== harness)
|
|
121
|
+
continue;
|
|
122
|
+
// The slot record is device-local and written after identity capture; a
|
|
123
|
+
// slot dir minted by an earlier build, or an add that stopped short of
|
|
124
|
+
// recording, is still the HOME an account run uses — project it too.
|
|
125
|
+
const dir = slots[account.id]?.slotDir ?? slotDir(harness, account.id);
|
|
126
|
+
if (!fs.existsSync(path.join(dir, agentConfigDirName(harness))))
|
|
127
|
+
continue;
|
|
128
|
+
carryForwardSettings(harness, fromHome, dir);
|
|
129
|
+
const pruned = pruneSlotResources(harness, version, dir, fromHome);
|
|
130
|
+
projectResources(harness, version, dir, fromHome);
|
|
131
|
+
projected.push({ accountId: account.id, name: account.name, slotDir: dir, from: version, pruned });
|
|
132
|
+
}
|
|
133
|
+
return projected;
|
|
134
|
+
}
|
|
75
135
|
/**
|
|
76
136
|
* Create the HOME-shaped slot dir and project settings/resources from the
|
|
77
137
|
* managed install. Does not copy credentials (carryForwardSettings excludes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type AccountInfo } from './agents.js';
|
|
2
|
-
import type { AgentId } from './types.js';
|
|
2
|
+
import type { AgentId, Meta } from './types.js';
|
|
3
3
|
import { type ProviderProbe } from './accounting/usage.js';
|
|
4
4
|
/**
|
|
5
5
|
* - `live` — completed an authenticated request (200).
|
|
@@ -134,10 +134,66 @@ export declare function formatCheckedAge(checkedAt: number, now?: number): strin
|
|
|
134
134
|
export declare function authAccountLabel(info: Pick<AccountInfo, 'email' | 'accountId' | 'userId'> | null | undefined): string | undefined;
|
|
135
135
|
/** Cache key: one entry per install — (host, agent, version). Unique per token. */
|
|
136
136
|
export declare function authCacheKey(host: string, agent: AgentId | string, version: string): string;
|
|
137
|
+
/**
|
|
138
|
+
* Host-independent identity of one probe target — the (agent, version) pair
|
|
139
|
+
* {@link authCacheKey} is keyed by. The separator cannot appear in either half,
|
|
140
|
+
* so an agent id can never run into a version the way a `:` join allows.
|
|
141
|
+
*/
|
|
142
|
+
export declare function authTargetKey(agent: AgentId | string, version: string): string;
|
|
143
|
+
/**
|
|
144
|
+
* The `version` slot an account SLOT occupies in the auth cache and the probe
|
|
145
|
+
* rows: `slot:<accountId>`. A slot is a HOME-shaped dir, not an installed
|
|
146
|
+
* version, so it needs its own key or its verdict would collide with (or be
|
|
147
|
+
* silently dropped in favor of) whichever version home the account's label
|
|
148
|
+
* happened to match.
|
|
149
|
+
*/
|
|
150
|
+
export declare function slotAuthVersionKey(accountId: string): string;
|
|
151
|
+
/** One account slot on this device that the auth probe must cover. */
|
|
152
|
+
export interface SlotAuthInstall extends FleetAuthInstall {
|
|
153
|
+
home: string;
|
|
154
|
+
accountId: string;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Every registered account's slot on this device (PHNX-3940 T1), as probe
|
|
158
|
+
* targets. Before this the probe walked `listInstalledVersions` only, so a
|
|
159
|
+
* slot's verdict was never re-derived after `accounts add/login` wrote it —
|
|
160
|
+
* a slot re-materialized as `unconfigured` while the device doc was unreadable
|
|
161
|
+
* stayed MISSING forever even though its login was live. A slot whose dir is
|
|
162
|
+
* gone is skipped: there is nothing to probe and the row would only say so.
|
|
163
|
+
*/
|
|
164
|
+
export declare function enumerateSlotInstalls(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agentIds: readonly AgentId[]): SlotAuthInstall[];
|
|
165
|
+
/** One probe target on this device: an installed version home, or an account slot. */
|
|
166
|
+
export interface LocalAuthInstall extends FleetAuthInstall {
|
|
167
|
+
home: string;
|
|
168
|
+
accountId?: string;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Every (agent, version) home the local auth probe covers on this device —
|
|
172
|
+
* installed version homes plus account slots. {@link probeLocalFleetAuth} probes
|
|
173
|
+
* exactly this set, so it is also the answer to "which cached rows are still
|
|
174
|
+
* backed by something on disk" (PHNX-4051).
|
|
175
|
+
*/
|
|
176
|
+
export declare function enumerateLocalAuthInstalls(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agentIds: readonly AgentId[]): LocalAuthInstall[];
|
|
177
|
+
/**
|
|
178
|
+
* {@link authTargetKey} for every local probe target — what a cached row must
|
|
179
|
+
* match to still be about this device. A row outside it is an ORPHAN: its home
|
|
180
|
+
* was uninstalled, so nothing will ever re-probe it and its `checkedAt` is
|
|
181
|
+
* frozen at whatever the last probe left (PHNX-4051).
|
|
182
|
+
*/
|
|
183
|
+
export declare function localAuthTargetKeys(agentIds?: readonly AgentId[]): Set<string>;
|
|
137
184
|
/** Read the whole cache (best-effort; a corrupt/missing file yields an empty map). */
|
|
138
185
|
export declare function readAuthHealthCache(): Record<string, AuthHealth>;
|
|
139
186
|
/** Read one entry, or null. */
|
|
140
187
|
export declare function readAuthHealth(host: string, agent: AgentId | string, version: string): AuthHealth | null;
|
|
188
|
+
/**
|
|
189
|
+
* Split a cache key back into the install it names, for one host. Returns null
|
|
190
|
+
* for a key that belongs to another host or does not name a known agent — the
|
|
191
|
+
* one place the `host:agent:version` join is undone.
|
|
192
|
+
*/
|
|
193
|
+
export declare function parseAuthCacheKey(key: string, host: string): {
|
|
194
|
+
agent: AgentId;
|
|
195
|
+
version: string;
|
|
196
|
+
} | null;
|
|
141
197
|
/** Reconstruct one host's published probe rows for a lease waiter/CLI reader. */
|
|
142
198
|
export declare function readFleetAuthRows(host: string): AuthProbeRow[];
|
|
143
199
|
/**
|
|
@@ -148,8 +204,15 @@ export declare function readFleetAuthRows(host: string): AuthProbeRow[];
|
|
|
148
204
|
* behaviour promised by the `error` doc on AuthVerdict ("keep the last known
|
|
149
205
|
* one"). Pure, so it's unit-tested directly. */
|
|
150
206
|
export declare function mergeAuthHealthEntries(current: Record<string, AuthHealth>, incoming: Record<string, AuthHealth>): Record<string, AuthHealth>;
|
|
151
|
-
/**
|
|
152
|
-
|
|
207
|
+
/**
|
|
208
|
+
* Merge one or more entries into the cache (best-effort write).
|
|
209
|
+
*
|
|
210
|
+
* `drop` removes entries the writer knows are gone — it runs on the PRE-merge
|
|
211
|
+
* cache, so an incoming row always wins over a drop of the same key and the two
|
|
212
|
+
* can never fight. Only a writer that knows the full truth for the keys it drops
|
|
213
|
+
* may pass one (see {@link writeFleetAuthRows}).
|
|
214
|
+
*/
|
|
215
|
+
export declare function writeAuthHealthEntries(entries: Record<string, AuthHealth>, drop?: (key: string) => boolean): void;
|
|
153
216
|
/**
|
|
154
217
|
* Complete a live auth probe for one (agent, home). For claude/kimi/droid this
|
|
155
218
|
* hits the provider — unless the account's usage cache already holds a fresh
|
|
@@ -239,5 +302,16 @@ export declare function probeLocalFleetAuth(opts?: {
|
|
|
239
302
|
*/
|
|
240
303
|
signal?: AbortSignal;
|
|
241
304
|
}): Promise<AuthProbeRow[]>;
|
|
242
|
-
/**
|
|
243
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Persist a host's probed rows into the cache (keyed by host+agent+version).
|
|
307
|
+
*
|
|
308
|
+
* `installed` — the {@link authTargetKey} set of homes that still exist on
|
|
309
|
+
* `host` ({@link localAuthTargetKeys}) — prunes this host's ORPHAN entries: rows
|
|
310
|
+
* for a version that has since been uninstalled. Nothing re-probes those, so
|
|
311
|
+
* they never age out on their own; they froze the reuse window permanently false
|
|
312
|
+
* and kept surfacing in `agents view` / fleet status (PHNX-4051). Only a caller
|
|
313
|
+
* that enumerated `host`'s real installs may pass it — the `fleet ping` fan-out
|
|
314
|
+
* writes a PEER's rows and cannot enumerate that box's homes, so it omits it and
|
|
315
|
+
* merges as before.
|
|
316
|
+
*/
|
|
317
|
+
export declare function writeFleetAuthRows(host: string, rows: AuthProbeRow[], installed?: ReadonlySet<string>): void;
|