@phnx-labs/agents-cli 1.22.97 → 1.22.99
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 +23 -1
- package/dist/lib/auth-health.js +47 -9
- package/dist/lib/browser/service.js +7 -4
- package/dist/lib/codex-home.d.ts +44 -12
- package/dist/lib/codex-home.js +123 -37
- package/dist/lib/harness/adapters/codex.js +7 -4
- 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,9 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.99
|
|
4
|
+
|
|
5
|
+
- **`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`.
|
|
6
|
+
|
|
7
|
+
- `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.
|
|
8
|
+
|
|
9
|
+
## 1.22.98
|
|
10
|
+
|
|
11
|
+
- `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.
|
|
12
|
+
|
|
13
|
+
- **A Firefox task no longer parks the browser-task reaper, and `record` on Firefox says "recording" (PHNX-4043).** The daemon's idle reaper asks every live task for its recording status before closing it; on a Firefox (or native Arc) task that query threw a capability error, so one Firefox task failed every reap tick and the `browser-task-reap` service parked after three failures, leaving abandoned tabs open for every profile on that box. Recording status on a backend that cannot record is now a truthful "not recording". `agents browser record start`/`stop` and `start --record` on a Firefox profile still fail loud, now naming `recording` instead of `screenshot` (screenshots do work on Firefox). Source: `cli/src/lib/browser/service.ts` `recordStart`/`recordStop`/`recordStatus`.
|
|
14
|
+
|
|
3
15
|
## 1.22.97
|
|
4
16
|
|
|
5
17
|
- `agents browser --help` and the `agents setup browser` install hint no longer claim Firefox is unsupported: they now state Chromium-family over CDP, Arc natively, and Firefox over WebDriver BiDi (Safari stays unsupported). `tabs --all` gains a help example and rows in the Arc and Firefox capability tables.
|
|
6
18
|
|
|
19
|
+
## 1.22.96
|
|
20
|
+
|
|
7
21
|
- **`agents browser tabs --all` shows every tab open in the profile browser, yours included (PHNX-4043).** Until now `tabs` listed only the task's own tabs, so an agent sharing an Arc Space, a Comet store, or a Firefox profile with you could not see what else was open. `--all` lists every top-level tab: OWNER names the agents-cli task that owns it, or `you` for a tab no task owns; `*` still marks the task's current tab. Read-only on every backend (Chromium `Target.getTargets`, the Arc Space's tabs, Firefox `browsingContext.getTree`). Source: `cli/src/lib/browser/service.ts` `profileTabs`, `cli/src/commands/browser.ts`.
|
|
8
22
|
|
|
9
23
|
## 1.22.95
|
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,6 +134,28 @@ 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
|
+
* The `version` slot an account SLOT occupies in the auth cache and the probe
|
|
139
|
+
* rows: `slot:<accountId>`. A slot is a HOME-shaped dir, not an installed
|
|
140
|
+
* version, so it needs its own key or its verdict would collide with (or be
|
|
141
|
+
* silently dropped in favor of) whichever version home the account's label
|
|
142
|
+
* happened to match.
|
|
143
|
+
*/
|
|
144
|
+
export declare function slotAuthVersionKey(accountId: string): string;
|
|
145
|
+
/** One account slot on this device that the auth probe must cover. */
|
|
146
|
+
export interface SlotAuthInstall extends FleetAuthInstall {
|
|
147
|
+
home: string;
|
|
148
|
+
accountId: string;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Every registered account's slot on this device (PHNX-3940 T1), as probe
|
|
152
|
+
* targets. Before this the probe walked `listInstalledVersions` only, so a
|
|
153
|
+
* slot's verdict was never re-derived after `accounts add/login` wrote it —
|
|
154
|
+
* a slot re-materialized as `unconfigured` while the device doc was unreadable
|
|
155
|
+
* stayed MISSING forever even though its login was live. A slot whose dir is
|
|
156
|
+
* gone is skipped: there is nothing to probe and the row would only say so.
|
|
157
|
+
*/
|
|
158
|
+
export declare function enumerateSlotInstalls(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agentIds: readonly AgentId[]): SlotAuthInstall[];
|
|
137
159
|
/** Read the whole cache (best-effort; a corrupt/missing file yields an empty map). */
|
|
138
160
|
export declare function readAuthHealthCache(): Record<string, AuthHealth>;
|
|
139
161
|
/** Read one entry, or null. */
|
package/dist/lib/auth-health.js
CHANGED
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
import * as fs from 'fs';
|
|
20
20
|
import * as path from 'path';
|
|
21
21
|
import { ALL_AGENT_IDS, getAccountInfo } from './agents.js';
|
|
22
|
+
import { listNativeAccounts } from './account-registry.js';
|
|
23
|
+
import { readSlots } from './accounts/slots.js';
|
|
22
24
|
import { getCacheDir } from './state.js';
|
|
23
25
|
import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, readClaudeUsageCache, } from './accounting/usage.js';
|
|
24
26
|
import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
|
|
@@ -218,6 +220,37 @@ export function authAccountLabel(info) {
|
|
|
218
220
|
export function authCacheKey(host, agent, version) {
|
|
219
221
|
return `${host}:${agent}:${version}`;
|
|
220
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* The `version` slot an account SLOT occupies in the auth cache and the probe
|
|
225
|
+
* rows: `slot:<accountId>`. A slot is a HOME-shaped dir, not an installed
|
|
226
|
+
* version, so it needs its own key or its verdict would collide with (or be
|
|
227
|
+
* silently dropped in favor of) whichever version home the account's label
|
|
228
|
+
* happened to match.
|
|
229
|
+
*/
|
|
230
|
+
export function slotAuthVersionKey(accountId) {
|
|
231
|
+
return `slot:${accountId}`;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* Every registered account's slot on this device (PHNX-3940 T1), as probe
|
|
235
|
+
* targets. Before this the probe walked `listInstalledVersions` only, so a
|
|
236
|
+
* slot's verdict was never re-derived after `accounts add/login` wrote it —
|
|
237
|
+
* a slot re-materialized as `unconfigured` while the device doc was unreadable
|
|
238
|
+
* stayed MISSING forever even though its login was live. A slot whose dir is
|
|
239
|
+
* gone is skipped: there is nothing to probe and the row would only say so.
|
|
240
|
+
*/
|
|
241
|
+
export function enumerateSlotInstalls(meta, agentIds) {
|
|
242
|
+
const byId = new Map(listNativeAccounts(meta).map((account) => [account.id, account]));
|
|
243
|
+
const out = [];
|
|
244
|
+
for (const [accountId, slot] of Object.entries(readSlots(meta))) {
|
|
245
|
+
const account = byId.get(accountId);
|
|
246
|
+
if (!account || !agentIds.includes(account.agent))
|
|
247
|
+
continue;
|
|
248
|
+
if (!fs.existsSync(slot.slotDir))
|
|
249
|
+
continue;
|
|
250
|
+
out.push({ agent: account.agent, version: slotAuthVersionKey(accountId), home: slot.slotDir, account: undefined, accountId });
|
|
251
|
+
}
|
|
252
|
+
return out;
|
|
253
|
+
}
|
|
221
254
|
function cacheFilePath() {
|
|
222
255
|
return path.join(getCacheDir(), '.auth-health.json');
|
|
223
256
|
}
|
|
@@ -410,26 +443,31 @@ export function groupFleetAuthInstalls(installs, isMergeable = () => true) {
|
|
|
410
443
|
*/
|
|
411
444
|
export async function probeLocalFleetAuth(opts) {
|
|
412
445
|
const agentIds = opts?.agents ?? ALL_AGENT_IDS;
|
|
413
|
-
// Enumerate every install
|
|
414
|
-
//
|
|
415
|
-
//
|
|
446
|
+
// Enumerate every install AND every account slot on this device, then resolve
|
|
447
|
+
// each one's account label. getAccountInfo is a local credential-file read
|
|
448
|
+
// (no network), so this fan-out is cheap and cannot contribute to the rate
|
|
449
|
+
// limit the probe grouping below exists to avoid.
|
|
450
|
+
const [{ readMeta }, { findNativeAccountByIdentity }] = await Promise.all([
|
|
451
|
+
import('./state.js'),
|
|
452
|
+
import('./account-registry.js'),
|
|
453
|
+
]);
|
|
454
|
+
const meta = readMeta();
|
|
416
455
|
const installs = [];
|
|
417
456
|
for (const agent of agentIds) {
|
|
418
457
|
for (const version of listInstalledVersions(agent)) {
|
|
419
458
|
installs.push({ agent, version, home: getVersionHomePath(agent, version), info: null, account: undefined });
|
|
420
459
|
}
|
|
421
460
|
}
|
|
461
|
+
for (const slot of enumerateSlotInstalls(meta, agentIds)) {
|
|
462
|
+
installs.push({ ...slot, info: null });
|
|
463
|
+
}
|
|
422
464
|
await Promise.all(installs.map(async (inst) => {
|
|
423
465
|
inst.info = await getAccountInfo(inst.agent, inst.home).catch(() => null);
|
|
424
466
|
inst.account = authAccountLabel(inst.info);
|
|
425
467
|
}));
|
|
426
|
-
const [{ readMeta }, { findNativeAccountByIdentity }] = await Promise.all([
|
|
427
|
-
import('./state.js'),
|
|
428
|
-
import('./account-registry.js'),
|
|
429
|
-
]);
|
|
430
|
-
const meta = readMeta();
|
|
431
468
|
for (const inst of installs) {
|
|
432
|
-
|
|
469
|
+
// A slot already knows its account; a version home is joined by identity.
|
|
470
|
+
inst.accountId ??= findNativeAccountByIdentity(meta, inst.agent, inst.info)?.id;
|
|
433
471
|
}
|
|
434
472
|
// Probe once per (agent, account) — but only for the network-probing agents
|
|
435
473
|
// that can actually 429; best-effort agents stay per-install (see
|
|
@@ -1592,7 +1592,7 @@ export class BrowserService {
|
|
|
1592
1592
|
throw new Error(`Task "${taskId}" is already recording. Call record stop first.`);
|
|
1593
1593
|
}
|
|
1594
1594
|
const { conn, task, key: runtimeKey } = await this.findTask(taskId);
|
|
1595
|
-
requireCdp(conn, '
|
|
1595
|
+
requireCdp(conn, 'recording');
|
|
1596
1596
|
const shortId = tabHint ? await this.resolveTabHint(conn, task, tabHint) : this.resolveCurrentTab(task);
|
|
1597
1597
|
const cdpTargetId = this.getCdpTargetId(task, shortId);
|
|
1598
1598
|
const target = await this.getTarget(conn, cdpTargetId);
|
|
@@ -1796,7 +1796,7 @@ export class BrowserService {
|
|
|
1796
1796
|
async recordStop(taskId, reason = 'manual') {
|
|
1797
1797
|
const activeTask = await this.findTask(taskId).catch(() => undefined);
|
|
1798
1798
|
if (activeTask)
|
|
1799
|
-
requireCdp(activeTask.conn, '
|
|
1799
|
+
requireCdp(activeTask.conn, 'recording');
|
|
1800
1800
|
const rec = this.recordings.get(taskId);
|
|
1801
1801
|
if (!rec) {
|
|
1802
1802
|
throw new Error(`Task "${taskId}" is not currently recording`);
|
|
@@ -1883,9 +1883,12 @@ export class BrowserService {
|
|
|
1883
1883
|
// not route an already-live task through findTask(), which stamps it as
|
|
1884
1884
|
// active and defeats idle detection. A cold daemon may still rehydrate for
|
|
1885
1885
|
// an explicit user query.
|
|
1886
|
+
// A status query, not an action: a backend that cannot record (Arc native,
|
|
1887
|
+
// Firefox BiDi) is truthfully "not recording". Throwing here took the
|
|
1888
|
+
// idle reaper down with it — one Firefox task parked reaping for every task.
|
|
1886
1889
|
const live = this.findTaskByHandle(taskId) ?? await this.findTask(taskId).catch(() => undefined);
|
|
1887
|
-
if (live)
|
|
1888
|
-
|
|
1890
|
+
if (live && (live.conn.backend === 'arc-native' || live.conn.backend === 'bidi'))
|
|
1891
|
+
return { recording: false };
|
|
1889
1892
|
const rec = this.recordings.get(taskId);
|
|
1890
1893
|
if (!rec)
|
|
1891
1894
|
return { recording: false };
|
package/dist/lib/codex-home.d.ts
CHANGED
|
@@ -8,24 +8,56 @@ export declare const CODEX_CONTROL_SOCKET_SUFFIX = "/app-server-control/app-serv
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function codexHomeOverflowsSunLen(home: string): boolean;
|
|
10
10
|
/**
|
|
11
|
-
* The short
|
|
12
|
-
* `~/.agents/.codex-homes/<
|
|
13
|
-
* SUN_LEN while staying stable across reboots (unlike $TMPDIR) and
|
|
14
|
-
*
|
|
11
|
+
* The short codex home used when an origin home overflows.
|
|
12
|
+
* `~/.agents/.codex-homes/<key>/.codex` keeps the socket path well under
|
|
13
|
+
* SUN_LEN while staying stable across reboots (unlike $TMPDIR) and isolated
|
|
14
|
+
* per origin.
|
|
15
15
|
*/
|
|
16
|
-
export declare function shortCodexHome(agentsUserDir: string,
|
|
16
|
+
export declare function shortCodexHome(agentsUserDir: string, key: string): string;
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
18
|
+
* The short-home key for a codex config home: the version for a version home,
|
|
19
|
+
* `a-<accountId prefix>` for an account slot under `<historyDir>/accounts/codex/`.
|
|
20
|
+
* Twelve hex characters keep the socket path under SUN_LEN for any home dir
|
|
21
|
+
* (`~/.agents/.codex-homes/a-XXXXXXXXXXXX/.codex` + suffix = 99 bytes on a
|
|
22
|
+
* `/Users/<user>` prefix); the origin-link check in {@link resolveCodexHome}
|
|
23
|
+
* is what rules out two accounts sharing a prefix.
|
|
24
|
+
*/
|
|
25
|
+
export declare function codexShortKey(home: string, version: string, historyDir: string): string;
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a macOS SUN_LEN-safe CODEX_HOME for the given origin home, migrating
|
|
28
|
+
* the home to a short real path (once, idempotently) when needed. `key` is the
|
|
29
|
+
* short-home key from {@link codexShortKey} — it uniquely identifies the origin
|
|
30
|
+
* (the version for a version home, `a-<accountId>` for a slot), so
|
|
31
|
+
* `.codex-homes/<key>` can only ever be THIS origin's own short home. Two
|
|
32
|
+
* different origins never map to one key, which is what makes every branch
|
|
33
|
+
* below safe without guessing whose login a short home holds.
|
|
20
34
|
*
|
|
21
|
-
* On non-darwin platforms, or when the
|
|
22
|
-
*
|
|
23
|
-
*
|
|
35
|
+
* On non-darwin platforms, or when the origin already fits, the origin is
|
|
36
|
+
* returned unchanged. Otherwise the short home is authoritative for `key`, and
|
|
37
|
+
* the origin is made a symlink onto it:
|
|
38
|
+
* - already linked onto it → return it (healthy, the common path).
|
|
39
|
+
* - linked onto a DIFFERENT target (a pre-fix layout where a slot's `.codex`
|
|
40
|
+
* was captured into a foreign version short home) → REPOINT to this key's own
|
|
41
|
+
* short home and warn; the foreign login is never run. The account may need a
|
|
42
|
+
* re-login, which is correct — its login was never isolated.
|
|
43
|
+
* - a real directory with the short home ALREADY present → a version/account
|
|
44
|
+
* reinstall recreated a fresh `.codex`; adopt the short home (which holds the
|
|
45
|
+
* real login), backing the fresh dir aside so freshly-synced resources are
|
|
46
|
+
* not lost. This is the case the earlier refuse-and-throw broke
|
|
47
|
+
* (`agents remove codex@x && agents add codex@x` then `agents run`).
|
|
48
|
+
* - a real directory with no short home yet → first migration: rename it into
|
|
49
|
+
* the short home and leave a symlink.
|
|
50
|
+
* - absent → create/adopt the short home and link the origin to it.
|
|
51
|
+
* If any filesystem step fails the origin is returned (no worse than the pre-fix
|
|
52
|
+
* behavior).
|
|
24
53
|
*/
|
|
25
|
-
export declare function resolveCodexHome(
|
|
54
|
+
export declare function resolveCodexHome(originHome: string, agentsUserDir: string, key: string, platform?: NodeJS.Platform): string;
|
|
26
55
|
/**
|
|
27
56
|
* Emit the bash block that a generated codex shim uses to export a
|
|
28
|
-
* SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome}
|
|
57
|
+
* SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome} for the one origin
|
|
58
|
+
* a shim ever sees, the version home: an account-slot launch reaches the shim
|
|
59
|
+
* with CODEX_HOME already pinned by `buildExecEnv`, and the block below keeps a
|
|
60
|
+
* caller-provided CODEX_HOME, so the slot key never has to be derived in bash.
|
|
29
61
|
*
|
|
30
62
|
* @param homeExpr shell expression for the versioned codex home
|
|
31
63
|
* (e.g. `$VERSION_DIR/home/.codex`)
|
package/dist/lib/codex-home.js
CHANGED
|
@@ -25,6 +25,14 @@
|
|
|
25
25
|
* the TS resolver for `agents run`/`agents exec`; `shims.ts` emits the bash
|
|
26
26
|
* equivalent (`codexHomeShimBash`) into the generated codex shims. Keep the two
|
|
27
27
|
* in lockstep.
|
|
28
|
+
*
|
|
29
|
+
* The short home is keyed by the ORIGIN it relocates, never by the version
|
|
30
|
+
* alone. An account slot (`~/.agents/.history/accounts/codex/<id>/.codex`) is
|
|
31
|
+
* long enough to overflow too, and keying it by version handed every
|
|
32
|
+
* `agents run codex#<account>` on macOS the default version's short home —
|
|
33
|
+
* whichever login that happened to hold. `codexShortKey` derives the key; a
|
|
34
|
+
* short home that is not the resolved link target of its origin is refused
|
|
35
|
+
* rather than reused.
|
|
28
36
|
*/
|
|
29
37
|
import * as fs from 'fs';
|
|
30
38
|
import * as path from 'path';
|
|
@@ -40,59 +48,137 @@ export function codexHomeOverflowsSunLen(home) {
|
|
|
40
48
|
return home.length + CODEX_CONTROL_SOCKET_SUFFIX.length > SUN_LEN;
|
|
41
49
|
}
|
|
42
50
|
/**
|
|
43
|
-
* The short
|
|
44
|
-
* `~/.agents/.codex-homes/<
|
|
45
|
-
* SUN_LEN while staying stable across reboots (unlike $TMPDIR) and
|
|
46
|
-
*
|
|
51
|
+
* The short codex home used when an origin home overflows.
|
|
52
|
+
* `~/.agents/.codex-homes/<key>/.codex` keeps the socket path well under
|
|
53
|
+
* SUN_LEN while staying stable across reboots (unlike $TMPDIR) and isolated
|
|
54
|
+
* per origin.
|
|
47
55
|
*/
|
|
48
|
-
export function shortCodexHome(agentsUserDir,
|
|
49
|
-
return path.join(agentsUserDir, '.codex-homes',
|
|
56
|
+
export function shortCodexHome(agentsUserDir, key) {
|
|
57
|
+
return path.join(agentsUserDir, '.codex-homes', key, '.codex');
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* The short-home key for a codex config home: the version for a version home,
|
|
61
|
+
* `a-<accountId prefix>` for an account slot under `<historyDir>/accounts/codex/`.
|
|
62
|
+
* Twelve hex characters keep the socket path under SUN_LEN for any home dir
|
|
63
|
+
* (`~/.agents/.codex-homes/a-XXXXXXXXXXXX/.codex` + suffix = 99 bytes on a
|
|
64
|
+
* `/Users/<user>` prefix); the origin-link check in {@link resolveCodexHome}
|
|
65
|
+
* is what rules out two accounts sharing a prefix.
|
|
66
|
+
*/
|
|
67
|
+
export function codexShortKey(home, version, historyDir) {
|
|
68
|
+
const slotsRoot = path.resolve(historyDir, 'accounts', 'codex');
|
|
69
|
+
const resolved = path.resolve(home);
|
|
70
|
+
if (!resolved.startsWith(slotsRoot + path.sep))
|
|
71
|
+
return version;
|
|
72
|
+
const accountId = resolved.slice(slotsRoot.length + 1).split(path.sep)[0] ?? '';
|
|
73
|
+
if (!accountId)
|
|
74
|
+
return version;
|
|
75
|
+
return `a-${accountId.slice(0, 12)}`;
|
|
76
|
+
}
|
|
77
|
+
function realpathOrNull(p) {
|
|
78
|
+
try {
|
|
79
|
+
return fs.realpathSync(p);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function isSymlinkOnto(link, target) {
|
|
86
|
+
try {
|
|
87
|
+
if (!fs.lstatSync(link).isSymbolicLink())
|
|
88
|
+
return false;
|
|
89
|
+
const t = realpathOrNull(target);
|
|
90
|
+
return t !== null && realpathOrNull(link) === t;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
50
95
|
}
|
|
51
96
|
/**
|
|
52
|
-
* Resolve a macOS SUN_LEN-safe CODEX_HOME for the given
|
|
53
|
-
*
|
|
97
|
+
* Resolve a macOS SUN_LEN-safe CODEX_HOME for the given origin home, migrating
|
|
98
|
+
* the home to a short real path (once, idempotently) when needed. `key` is the
|
|
99
|
+
* short-home key from {@link codexShortKey} — it uniquely identifies the origin
|
|
100
|
+
* (the version for a version home, `a-<accountId>` for a slot), so
|
|
101
|
+
* `.codex-homes/<key>` can only ever be THIS origin's own short home. Two
|
|
102
|
+
* different origins never map to one key, which is what makes every branch
|
|
103
|
+
* below safe without guessing whose login a short home holds.
|
|
54
104
|
*
|
|
55
|
-
* On non-darwin platforms, or when the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
105
|
+
* On non-darwin platforms, or when the origin already fits, the origin is
|
|
106
|
+
* returned unchanged. Otherwise the short home is authoritative for `key`, and
|
|
107
|
+
* the origin is made a symlink onto it:
|
|
108
|
+
* - already linked onto it → return it (healthy, the common path).
|
|
109
|
+
* - linked onto a DIFFERENT target (a pre-fix layout where a slot's `.codex`
|
|
110
|
+
* was captured into a foreign version short home) → REPOINT to this key's own
|
|
111
|
+
* short home and warn; the foreign login is never run. The account may need a
|
|
112
|
+
* re-login, which is correct — its login was never isolated.
|
|
113
|
+
* - a real directory with the short home ALREADY present → a version/account
|
|
114
|
+
* reinstall recreated a fresh `.codex`; adopt the short home (which holds the
|
|
115
|
+
* real login), backing the fresh dir aside so freshly-synced resources are
|
|
116
|
+
* not lost. This is the case the earlier refuse-and-throw broke
|
|
117
|
+
* (`agents remove codex@x && agents add codex@x` then `agents run`).
|
|
118
|
+
* - a real directory with no short home yet → first migration: rename it into
|
|
119
|
+
* the short home and leave a symlink.
|
|
120
|
+
* - absent → create/adopt the short home and link the origin to it.
|
|
121
|
+
* If any filesystem step fails the origin is returned (no worse than the pre-fix
|
|
122
|
+
* behavior).
|
|
58
123
|
*/
|
|
59
|
-
export function resolveCodexHome(
|
|
124
|
+
export function resolveCodexHome(originHome, agentsUserDir, key, platform = process.platform) {
|
|
60
125
|
if (platform !== 'darwin')
|
|
61
|
-
return
|
|
62
|
-
if (!codexHomeOverflowsSunLen(
|
|
63
|
-
return
|
|
64
|
-
const short = shortCodexHome(agentsUserDir,
|
|
126
|
+
return originHome;
|
|
127
|
+
if (!codexHomeOverflowsSunLen(originHome))
|
|
128
|
+
return originHome;
|
|
129
|
+
const short = shortCodexHome(agentsUserDir, key);
|
|
130
|
+
if (isSymlinkOnto(originHome, short))
|
|
131
|
+
return short;
|
|
132
|
+
const origin = fs.lstatSync(originHome, { throwIfNoEntry: false });
|
|
65
133
|
try {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
fs.renameSync(versionedHome, short);
|
|
74
|
-
fs.symlinkSync(short, versionedHome);
|
|
75
|
-
}
|
|
76
|
-
else if (!st) {
|
|
77
|
-
// Fresh install: create the short home and link the versioned path to it.
|
|
134
|
+
fs.mkdirSync(path.dirname(short), { recursive: true });
|
|
135
|
+
if (origin?.isSymbolicLink()) {
|
|
136
|
+
// Mis-linked onto a foreign target (pre-fix bug): never run that identity.
|
|
137
|
+
// Repoint onto this key's own short home, creating it if absent.
|
|
138
|
+
process.stderr.write(`[codex] ${originHome} was linked to a foreign home; repointing to ${short} `
|
|
139
|
+
+ `(this account may need to log in again).\n`);
|
|
140
|
+
if (!fs.existsSync(short))
|
|
78
141
|
fs.mkdirSync(short, { recursive: true });
|
|
79
|
-
|
|
142
|
+
fs.rmSync(originHome); // removes the symlink only, never its target
|
|
143
|
+
fs.symlinkSync(short, originHome);
|
|
144
|
+
}
|
|
145
|
+
else if (origin?.isDirectory()) {
|
|
146
|
+
if (fs.existsSync(short)) {
|
|
147
|
+
// Reinstall: the fresh real `.codex` has re-derivable resources but no
|
|
148
|
+
// login; the short home holds the real login. Adopt the short home and
|
|
149
|
+
// set the fresh dir aside (non-destructive) rather than lose either.
|
|
150
|
+
const superseded = `${originHome}.superseded-${Date.now()}`;
|
|
151
|
+
fs.renameSync(originHome, superseded);
|
|
152
|
+
fs.symlinkSync(short, originHome);
|
|
80
153
|
}
|
|
81
|
-
|
|
82
|
-
|
|
154
|
+
else {
|
|
155
|
+
// First migration: relocate the deep home so config/auth/state stay
|
|
156
|
+
// intact, then leave a symlink behind so anything referencing the origin
|
|
157
|
+
// path still resolves (session discovery, the slot record, the shim).
|
|
158
|
+
fs.renameSync(originHome, short);
|
|
159
|
+
fs.symlinkSync(short, originHome);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
else {
|
|
163
|
+
// Fresh origin: create (or adopt) the short home and link the origin to it.
|
|
164
|
+
fs.mkdirSync(short, { recursive: true });
|
|
165
|
+
fs.mkdirSync(path.dirname(originHome), { recursive: true });
|
|
166
|
+
fs.symlinkSync(short, originHome);
|
|
83
167
|
}
|
|
84
168
|
}
|
|
85
169
|
catch {
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
|
|
89
|
-
return versionedHome;
|
|
170
|
+
// A filesystem step lost a race or hit a permission error. Fall back to the
|
|
171
|
+
// origin rather than crash the invocation.
|
|
172
|
+
return originHome;
|
|
90
173
|
}
|
|
91
|
-
return
|
|
174
|
+
return isSymlinkOnto(originHome, short) ? short : originHome;
|
|
92
175
|
}
|
|
93
176
|
/**
|
|
94
177
|
* Emit the bash block that a generated codex shim uses to export a
|
|
95
|
-
* SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome}
|
|
178
|
+
* SUN_LEN-safe CODEX_HOME. Mirrors {@link resolveCodexHome} for the one origin
|
|
179
|
+
* a shim ever sees, the version home: an account-slot launch reaches the shim
|
|
180
|
+
* with CODEX_HOME already pinned by `buildExecEnv`, and the block below keeps a
|
|
181
|
+
* caller-provided CODEX_HOME, so the slot key never has to be derived in bash.
|
|
96
182
|
*
|
|
97
183
|
* @param homeExpr shell expression for the versioned codex home
|
|
98
184
|
* (e.g. `$VERSION_DIR/home/.codex`)
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as os from 'os';
|
|
2
2
|
import * as path from 'path';
|
|
3
3
|
import { getHistoryDir } from '../../state.js';
|
|
4
|
-
import {
|
|
4
|
+
import { codexHomeShimBash, codexShortKey, resolveCodexHome } from '../../codex-home.js';
|
|
5
5
|
import { codexEditWritableRoots, codexPolicyArgs } from '../../codex-policy.js';
|
|
6
6
|
import { stripForeignConfigDir } from '../adapter.js';
|
|
7
7
|
/**
|
|
@@ -20,9 +20,12 @@ export const codexAdapter = {
|
|
|
20
20
|
// On macOS the deep versioned home overflows the Unix-socket SUN_LEN
|
|
21
21
|
// limit for codex's app-server control socket; resolve to a short,
|
|
22
22
|
// SUN_LEN-safe home (migrating once if needed). See codex-home.ts.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
// ctx.versionHome is the account slot on a `codex#<account>` launch, so
|
|
24
|
+
// the short home is keyed by that origin, never by the version alone.
|
|
25
|
+
const originHome = path.join(ctx.versionHome, '.codex');
|
|
26
|
+
const historyDir = getHistoryDir();
|
|
27
|
+
const agentsUserDir = path.dirname(historyDir);
|
|
28
|
+
result.CODEX_HOME = resolveCodexHome(originHome, agentsUserDir, codexShortKey(originHome, ctx.version, historyDir));
|
|
26
29
|
}
|
|
27
30
|
stripForeignConfigDir(result, ['CODEX_HOME']);
|
|
28
31
|
},
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
* helper release now, off this table entirely.
|
|
37
37
|
*/
|
|
38
38
|
export const HELPER_RELEASES = {
|
|
39
|
-
menubar: { tagPrefix: 'menubar', floor: '1.1.
|
|
39
|
+
menubar: { tagPrefix: 'menubar', floor: '1.1.2' },
|
|
40
40
|
'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
|
|
41
41
|
// The Windows helper is a bare .exe, not an .app bundle, so it does not share
|
|
42
42
|
// helper-download.ts's zip/codesign/notarize machinery -- but it has the same
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import type { AgentId } from './types.js';
|
|
20
20
|
import { type HealResult } from './heal.js';
|
|
21
|
+
import { type SlotProjection } from './accounts/slots.js';
|
|
21
22
|
import { type HookRuntimeRepairReport } from './hooks/install.js';
|
|
22
23
|
import { type RemediateStaleInstallsResult, type FindAgentsCliInstallsOptions } from './self-update.js';
|
|
23
24
|
export interface HookRewireResult {
|
|
@@ -36,6 +37,13 @@ export interface RepairAfterSyncReport {
|
|
|
36
37
|
hookRuntimeRepair: HookRuntimeRepairReport;
|
|
37
38
|
/** Only populated on the umbrella (no-agent) path; null otherwise. */
|
|
38
39
|
staleInstallPurge: RemediateStaleInstallsResult | null;
|
|
40
|
+
/**
|
|
41
|
+
* Account slots re-projected from their harness's default version home
|
|
42
|
+
* (PHNX-3940): one entry per slot, with the artifacts pruned because the
|
|
43
|
+
* source no longer carries them. Errors are per-harness sentences.
|
|
44
|
+
*/
|
|
45
|
+
slotProjection: SlotProjection[];
|
|
46
|
+
slotProjectionErrors: string[];
|
|
39
47
|
}
|
|
40
48
|
/**
|
|
41
49
|
* Test/override hook that scopes the destructive stale-CLI purge to injected
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { AGENTS, ALL_AGENT_IDS } from './agents.js';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import { heal, healChangedAnything } from './heal.js';
|
|
4
|
+
import { projectAccountSlots } from './accounts/slots.js';
|
|
4
5
|
import { checkVersionHookWiring, registerHooksToSettings, repairManagedHookRuntimeArtifacts, } from './hooks/install.js';
|
|
5
6
|
import { getVersionHomePath, listInstalledVersions, isVersionIsolated, } from './installations/versions.js';
|
|
6
7
|
import { invalidateDoctorOverviewCache } from './devices/doctor-overview-cache.js';
|
|
@@ -126,11 +127,27 @@ export async function repairAfterSync(opts) {
|
|
|
126
127
|
const staleInstallPurge = (!opts.agent && opts.pruneClis === true)
|
|
127
128
|
? purgeStaleAgentsCliCopies(opts.purgeInjection)
|
|
128
129
|
: null;
|
|
130
|
+
// Account slots follow the version home (PHNX-3940). `agents accounts add`
|
|
131
|
+
// projects a slot once; without this every later sync would update the
|
|
132
|
+
// version home and leave each slot on the resources it was cloned with.
|
|
133
|
+
const slotProjection = [];
|
|
134
|
+
const slotProjectionErrors = [];
|
|
135
|
+
const slotAgents = opts.agent ? [opts.agent] : ALL_AGENT_IDS.filter((a) => listInstalledVersions(a).length > 0);
|
|
136
|
+
for (const agent of slotAgents) {
|
|
137
|
+
try {
|
|
138
|
+
slotProjection.push(...projectAccountSlots(agent));
|
|
139
|
+
}
|
|
140
|
+
catch (err) {
|
|
141
|
+
slotProjectionErrors.push(`${agent}: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
129
144
|
const report = {
|
|
130
145
|
heal: healResult,
|
|
131
146
|
hookRewire,
|
|
132
147
|
hookRuntimeRepair,
|
|
133
148
|
staleInstallPurge,
|
|
149
|
+
slotProjection,
|
|
150
|
+
slotProjectionErrors,
|
|
134
151
|
};
|
|
135
152
|
if (repairChangedAnything(report))
|
|
136
153
|
invalidateDoctorOverviewCache();
|
|
@@ -142,7 +159,9 @@ export function repairChangedAnything(report) {
|
|
|
142
159
|
report.hookRewire.some((r) => r.rewired > 0 || r.remaining > 0 || r.failure !== undefined) ||
|
|
143
160
|
report.hookRuntimeRepair.attempts.length > 0 ||
|
|
144
161
|
(report.staleInstallPurge !== null &&
|
|
145
|
-
(report.staleInstallPurge.removed.length > 0 || report.staleInstallPurge.failed.length > 0))
|
|
162
|
+
(report.staleInstallPurge.removed.length > 0 || report.staleInstallPurge.failed.length > 0)) ||
|
|
163
|
+
report.slotProjection.length > 0 ||
|
|
164
|
+
report.slotProjectionErrors.length > 0);
|
|
146
165
|
}
|
|
147
166
|
/**
|
|
148
167
|
* True when the RECONCILE repair left a per-version problem a human must fix — an
|
|
@@ -174,6 +193,8 @@ export function repairAfterSyncJson(report) {
|
|
|
174
193
|
hookRewire: report.hookRewire,
|
|
175
194
|
hookRuntimeRepair: report.hookRuntimeRepair,
|
|
176
195
|
staleInstallPurge: report.staleInstallPurge,
|
|
196
|
+
slotProjection: report.slotProjection,
|
|
197
|
+
slotProjectionErrors: report.slotProjectionErrors,
|
|
177
198
|
hadFailures: repairHadFailures(report),
|
|
178
199
|
};
|
|
179
200
|
}
|
|
@@ -264,4 +285,14 @@ export function renderRepairAfterSync(report, log = (s) => console.log(s)) {
|
|
|
264
285
|
renderHookRuntimeRepairText(report.hookRuntimeRepair, log);
|
|
265
286
|
if (report.staleInstallPurge)
|
|
266
287
|
renderStaleInstallPurgeText(report.staleInstallPurge, log);
|
|
288
|
+
renderSlotProjectionText(report, log);
|
|
289
|
+
}
|
|
290
|
+
function renderSlotProjectionText(report, log) {
|
|
291
|
+
for (const slot of report.slotProjection) {
|
|
292
|
+
const pruned = slot.pruned.length > 0 ? chalk.gray(` — removed ${slot.pruned.join(', ')}`) : '';
|
|
293
|
+
log(` ${chalk.green('slot')} account ${chalk.bold(slot.name)} ${chalk.gray(`← ${slot.from}`)}${pruned}`);
|
|
294
|
+
}
|
|
295
|
+
for (const err of report.slotProjectionErrors) {
|
|
296
|
+
log(` ${chalk.yellow('slot')} ${err}`);
|
|
297
|
+
}
|
|
267
298
|
}
|
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.99",
|
|
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",
|