@phnx-labs/agents-cli 1.22.84 → 1.22.85
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 +57 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/db.d.ts +18 -0
- package/dist/lib/session/db.js +47 -14
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -176
- package/dist/lib/accounts/connect.js +0 -453
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -138,9 +138,10 @@ export const FLEET_LOGIN_FLOWS = {
|
|
|
138
138
|
// claude: interactive `/login` inside the TUI is a loopback browser flow, and
|
|
139
139
|
// the token is keychain-bound on macOS. That native login stays per-box and
|
|
140
140
|
// is never copied. The shareable `claude setup-token` mint is a different
|
|
141
|
-
// credential class —
|
|
142
|
-
// (`lib/auth-mint.ts`), not as a
|
|
143
|
-
// is account-scoped, not
|
|
141
|
+
// credential class — minted by `agents accounts add claude [name]` /
|
|
142
|
+
// `agents accounts login claude#<name>` (`lib/auth-mint.ts`), not as a
|
|
143
|
+
// fleet-login device-code flow (a setup-token is account-scoped, not
|
|
144
|
+
// per-machine).
|
|
144
145
|
claude: {
|
|
145
146
|
loginCommand: 'claude',
|
|
146
147
|
flowType: 'loopback',
|
|
@@ -267,15 +267,12 @@ async function performFleetSharedRepoSync(root, device, timeoutMs) {
|
|
|
267
267
|
}
|
|
268
268
|
const ownedFile = fleetSharedStatePath(device, root);
|
|
269
269
|
const relativeOwnedFile = path.relative(root, ownedFile).split(path.sep).join('/');
|
|
270
|
-
// The central `agents.yaml` is fleet-shared state too —
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
//
|
|
276
|
-
// rows instead of stashing them, which is also what `accounts label --help`
|
|
277
|
-
// already promises ("labels live on the central account rows in agents.yaml,
|
|
278
|
-
// which repo push/pull already syncs fleet-wide").
|
|
270
|
+
// The central `agents.yaml` is fleet-shared state too — native account rows
|
|
271
|
+
// land there (lib/state.ts writeMetaUnlocked) as a plain file write with no
|
|
272
|
+
// commit. Publishing only the per-device file and then `rebase --autostash`-ing
|
|
273
|
+
// over a dirty central file silently destroyed those names: every box lost its
|
|
274
|
+
// account names on its next daemon publish (PHNX-3887). Commit it alongside the
|
|
275
|
+
// device doc so the rebase carries the rows instead of stashing them.
|
|
279
276
|
const centralFile = path.join(root, 'agents.yaml');
|
|
280
277
|
const publishPaths = [relativeOwnedFile];
|
|
281
278
|
if (fs.existsSync(centralFile))
|
|
@@ -150,16 +150,16 @@ export interface HarnessAdapter {
|
|
|
150
150
|
routineModeArgs?(cmd: string[], ctx: RoutineLaunchCtx): void;
|
|
151
151
|
}
|
|
152
152
|
/**
|
|
153
|
-
*
|
|
154
|
-
* per-harness branch in
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
*
|
|
153
|
+
* Config-dir env keys a harness pins to its slot / version home. Every
|
|
154
|
+
* per-harness branch in buildExecEnv deletes the ones it does NOT set, so a
|
|
155
|
+
* slot never inherits another account's dir (PHNX-3940 T5). GROK_HOME,
|
|
156
|
+
* OPENCODE_CONFIG_DIR, and the XDG pair were previously omitted, which let a
|
|
157
|
+
* parent agent's pin leak into a sibling slot.
|
|
158
158
|
*/
|
|
159
|
-
export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME"];
|
|
159
|
+
export declare const CONFIG_DIR_ENV_KEYS: readonly ["CLAUDE_CONFIG_DIR", "CODEX_HOME", "COPILOT_HOME", "KIMI_CODE_HOME", "GROK_HOME", "OPENCODE_CONFIG_DIR", "XDG_CONFIG_HOME", "XDG_DATA_HOME"];
|
|
160
160
|
/**
|
|
161
161
|
* Strip every config-dir env key except the one(s) this harness sets. `keep=[]`
|
|
162
|
-
* (the default / no-config-dir harness) deletes all
|
|
162
|
+
* (the default / no-config-dir harness) deletes all of them — the old `else` arm.
|
|
163
163
|
*/
|
|
164
164
|
export declare function stripForeignConfigDir(result: NodeJS.ProcessEnv, keep?: readonly string[]): void;
|
|
165
165
|
export declare function registerHarnessAdapter(adapter: HarnessAdapter): void;
|
|
@@ -1,19 +1,23 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* per-harness branch in
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
2
|
+
* Config-dir env keys a harness pins to its slot / version home. Every
|
|
3
|
+
* per-harness branch in buildExecEnv deletes the ones it does NOT set, so a
|
|
4
|
+
* slot never inherits another account's dir (PHNX-3940 T5). GROK_HOME,
|
|
5
|
+
* OPENCODE_CONFIG_DIR, and the XDG pair were previously omitted, which let a
|
|
6
|
+
* parent agent's pin leak into a sibling slot.
|
|
7
7
|
*/
|
|
8
8
|
export const CONFIG_DIR_ENV_KEYS = [
|
|
9
9
|
'CLAUDE_CONFIG_DIR',
|
|
10
10
|
'CODEX_HOME',
|
|
11
11
|
'COPILOT_HOME',
|
|
12
12
|
'KIMI_CODE_HOME',
|
|
13
|
+
'GROK_HOME',
|
|
14
|
+
'OPENCODE_CONFIG_DIR',
|
|
15
|
+
'XDG_CONFIG_HOME',
|
|
16
|
+
'XDG_DATA_HOME',
|
|
13
17
|
];
|
|
14
18
|
/**
|
|
15
19
|
* Strip every config-dir env key except the one(s) this harness sets. `keep=[]`
|
|
16
|
-
* (the default / no-config-dir harness) deletes all
|
|
20
|
+
* (the default / no-config-dir harness) deletes all of them — the old `else` arm.
|
|
17
21
|
*/
|
|
18
22
|
export function stripForeignConfigDir(result, keep = []) {
|
|
19
23
|
for (const key of CONFIG_DIR_ENV_KEYS) {
|
|
@@ -1,8 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
// whole config tree via GROK_HOME in the generated shim.
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { stripForeignConfigDir } from '../adapter.js';
|
|
4
3
|
export const grokAdapter = {
|
|
5
4
|
id: 'grok',
|
|
5
|
+
applyExecConfigEnv(result, ctx) {
|
|
6
|
+
if (ctx.versionHome) {
|
|
7
|
+
result.GROK_HOME = path.join(ctx.versionHome, '.grok');
|
|
8
|
+
}
|
|
9
|
+
stripForeignConfigDir(result, ['GROK_HOME']);
|
|
10
|
+
},
|
|
6
11
|
shimConfigEnvBash() {
|
|
7
12
|
return `
|
|
8
13
|
# Grok Build uses GROK_HOME to isolate its entire configuration tree
|
|
@@ -13,7 +13,7 @@ export const museAdapter = {
|
|
|
13
13
|
result.XDG_CONFIG_HOME = path.join(ctx.versionHome, '.config');
|
|
14
14
|
result.XDG_DATA_HOME = path.join(ctx.versionHome, '.local', 'share');
|
|
15
15
|
}
|
|
16
|
-
stripForeignConfigDir(result);
|
|
16
|
+
stripForeignConfigDir(result, ['XDG_CONFIG_HOME', 'XDG_DATA_HOME']);
|
|
17
17
|
},
|
|
18
18
|
shimConfigEnvBash() {
|
|
19
19
|
return `
|
|
@@ -1,8 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
//
|
|
1
|
+
import * as path from 'path';
|
|
2
|
+
import { stripForeignConfigDir } from '../adapter.js';
|
|
3
|
+
// OpenCode reads plugins/agents/commands from OPENCODE_CONFIG_DIR and auth from
|
|
4
|
+
// $XDG_DATA_HOME/opencode (HARNESS_AUTH.slotEnv). Pin both at the slot / version
|
|
5
|
+
// home so two accounts in one install never share a credential (PHNX-3940 T5).
|
|
4
6
|
export const opencodeAdapter = {
|
|
5
7
|
id: 'opencode',
|
|
8
|
+
applyExecConfigEnv(result, ctx) {
|
|
9
|
+
if (ctx.versionHome) {
|
|
10
|
+
result.OPENCODE_CONFIG_DIR = path.join(ctx.versionHome, '.config', 'opencode');
|
|
11
|
+
result.XDG_DATA_HOME = path.join(ctx.versionHome, '.local', 'share');
|
|
12
|
+
}
|
|
13
|
+
stripForeignConfigDir(result, ['OPENCODE_CONFIG_DIR', 'XDG_DATA_HOME']);
|
|
14
|
+
},
|
|
6
15
|
shimConfigEnvBash() {
|
|
7
16
|
return `
|
|
8
17
|
# OpenCode reads plugins, agents, commands, and other config-directory
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-harness auth capability table (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* One row per `ALL_AGENT_IDS` entry. Governs `accounts add` / connect login
|
|
5
|
+
* argv, identity strength, the durable worker credential (if any), and the
|
|
6
|
+
* config-dir env that pins a slot. Completeness is pinned by the test against
|
|
7
|
+
* `ALL_AGENT_IDS` — a new harness without a row is a type error and a failing
|
|
8
|
+
* test, never a silent skip.
|
|
9
|
+
*
|
|
10
|
+
* Values are taken from evidence-harness-auth.md (2026-09-06) and the adapters
|
|
11
|
+
* under `lib/harness/adapters/`. `LOGIN_INVOCATIONS` is the connect-era subset
|
|
12
|
+
* (claude, codex) derived from this table so existing connect callers keep
|
|
13
|
+
* working until T4 folds connect into `accounts add`.
|
|
14
|
+
*/
|
|
15
|
+
import type { AgentId } from './types.js';
|
|
16
|
+
export type HarnessIdentityKind = 'strong' | 'email' | 'opaque';
|
|
17
|
+
/**
|
|
18
|
+
* Durable worker credential, or `none` when the harness must log in per box.
|
|
19
|
+
* Codex is both: an API key (portable, bills the API) OR a per-device
|
|
20
|
+
* ChatGPT-plan device-auth login (the plan seat; never stored in the reserved
|
|
21
|
+
* store because it is a rotating session).
|
|
22
|
+
*/
|
|
23
|
+
export type HarnessWorkerKind = 'setup-token' | 'none' | `api-key:${string}` | `per-device${'' | `:${string}`}`;
|
|
24
|
+
export type HarnessWorker = HarnessWorkerKind | HarnessWorkerKind[];
|
|
25
|
+
export interface HarnessAuthCapability {
|
|
26
|
+
/** argv after the harness binary to start native login, or null when there is no finite login command. */
|
|
27
|
+
login: string[] | null;
|
|
28
|
+
/** argv to probe login status, or null when the CLI has no status command. */
|
|
29
|
+
status: string[] | null;
|
|
30
|
+
identity: HarnessIdentityKind;
|
|
31
|
+
worker: HarnessWorker;
|
|
32
|
+
/** Config-dir env that pins a slot, or null when isolation is HOME-swap only. */
|
|
33
|
+
slotEnv: string | null;
|
|
34
|
+
}
|
|
35
|
+
export declare const HARNESS_AUTH: Record<AgentId, HarnessAuthCapability>;
|
|
36
|
+
export declare function harnessAuth(agent: AgentId): HarnessAuthCapability;
|
|
37
|
+
/** Worker kinds as a flat list (codex's dual path is two entries). */
|
|
38
|
+
export declare function harnessWorkerKinds(agent: AgentId): HarnessWorkerKind[];
|
|
39
|
+
/**
|
|
40
|
+
* True when this harness has no portable worker credential and must log in
|
|
41
|
+
* per box. `worker: 'none'` (kimi, antigravity, …) and a sole `per-device…`
|
|
42
|
+
* kind both count; a dual path like Codex (API key OR device-auth) does not.
|
|
43
|
+
*/
|
|
44
|
+
export declare function harnessWorkerIsPerDevice(agent: AgentId): boolean;
|
|
45
|
+
/**
|
|
46
|
+
* Native-login invocation per harness. Only harnesses with a REAL, finite login
|
|
47
|
+
* COMMAND that connect currently drives — connect fails clearly for anything
|
|
48
|
+
* else rather than faking a flow that never signs the user in. Verified against
|
|
49
|
+
* the installed CLIs (PHNX-3940): `claude auth login --help` → "Sign in to your
|
|
50
|
+
* Anthropic account" with `--email`; `codex login` drives the OAuth flow.
|
|
51
|
+
* Args are the `HARNESS_AUTH.login` values for the same ids.
|
|
52
|
+
*/
|
|
53
|
+
export interface LoginInvocation {
|
|
54
|
+
/** argv passed to the installed binary to start the native login. */
|
|
55
|
+
args: string[];
|
|
56
|
+
/** Flag that pre-fills the login email (appended as `[emailFlag, email]`), when supported. */
|
|
57
|
+
emailFlag?: string;
|
|
58
|
+
/** One-line hint shown before the login flow takes over. */
|
|
59
|
+
hint?: string;
|
|
60
|
+
}
|
|
61
|
+
export declare const LOGIN_INVOCATIONS: Partial<Record<AgentId, LoginInvocation>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const HARNESS_AUTH = {
|
|
2
|
+
claude: { login: ['auth', 'login'], status: ['auth', 'status'], identity: 'strong', worker: 'setup-token', slotEnv: 'CLAUDE_CONFIG_DIR' },
|
|
3
|
+
codex: { login: ['login'], status: ['login', 'status'], identity: 'strong', worker: ['api-key:OPENAI_API_KEY', 'per-device:device-auth'], slotEnv: 'CODEX_HOME' },
|
|
4
|
+
grok: { login: ['login'], status: null, identity: 'strong', worker: 'api-key:XAI_API_KEY', slotEnv: 'GROK_HOME' },
|
|
5
|
+
// auth.json has no email claim; identity is the sorted provider-id join
|
|
6
|
+
// (`resolveOpenCodeAccountId`). NATIVE_ACCOUNT_CAPABILITIES.opencode.inspection
|
|
7
|
+
// is already 'opaque'.
|
|
8
|
+
opencode: { login: ['auth', 'login'], status: ['auth', 'list'], identity: 'opaque', worker: 'api-key:provider', slotEnv: 'XDG_DATA_HOME' },
|
|
9
|
+
cursor: { login: ['login'], status: ['status'], identity: 'strong', worker: 'api-key:CURSOR_API_KEY', slotEnv: null },
|
|
10
|
+
// No finite login argv — launch bare `kimi`, then `/login` in the TUI
|
|
11
|
+
// (`loginHint('kimi') === 'kimi'`, fleet/auth-sync `loginCommand: 'kimi'`).
|
|
12
|
+
kimi: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'KIMI_CODE_HOME' },
|
|
13
|
+
antigravity: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
14
|
+
droid: { login: null, status: null, identity: 'opaque', worker: 'api-key:FACTORY_API_KEY', slotEnv: null },
|
|
15
|
+
gemini: { login: null, status: null, identity: 'email', worker: 'none', slotEnv: null },
|
|
16
|
+
copilot: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: 'COPILOT_HOME' },
|
|
17
|
+
openclaw: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
18
|
+
amp: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
19
|
+
goose: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
20
|
+
hermes: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
21
|
+
// Auth lives under $XDG_CONFIG_HOME/muse (adapter pins XDG_CONFIG_HOME +
|
|
22
|
+
// XDG_DATA_HOME). slotEnv names the config pin; T5 must not HOME-swap.
|
|
23
|
+
muse: { login: null, status: null, identity: 'email', worker: 'none', slotEnv: 'XDG_CONFIG_HOME' },
|
|
24
|
+
warp: { login: null, status: null, identity: 'opaque', worker: 'none', slotEnv: null },
|
|
25
|
+
};
|
|
26
|
+
export function harnessAuth(agent) {
|
|
27
|
+
const cap = HARNESS_AUTH[agent];
|
|
28
|
+
if (!cap)
|
|
29
|
+
throw new Error(`No harness-auth capability for '${agent}'.`);
|
|
30
|
+
return cap;
|
|
31
|
+
}
|
|
32
|
+
/** Worker kinds as a flat list (codex's dual path is two entries). */
|
|
33
|
+
export function harnessWorkerKinds(agent) {
|
|
34
|
+
const worker = harnessAuth(agent).worker;
|
|
35
|
+
return Array.isArray(worker) ? worker : [worker];
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* True when this harness has no portable worker credential and must log in
|
|
39
|
+
* per box. `worker: 'none'` (kimi, antigravity, …) and a sole `per-device…`
|
|
40
|
+
* kind both count; a dual path like Codex (API key OR device-auth) does not.
|
|
41
|
+
*/
|
|
42
|
+
export function harnessWorkerIsPerDevice(agent) {
|
|
43
|
+
return harnessWorkerKinds(agent).every((kind) => kind === 'none' || kind.startsWith('per-device'));
|
|
44
|
+
}
|
|
45
|
+
export const LOGIN_INVOCATIONS = {
|
|
46
|
+
claude: {
|
|
47
|
+
args: HARNESS_AUTH.claude.login,
|
|
48
|
+
emailFlag: '--email',
|
|
49
|
+
hint: 'Complete the Claude sign-in in your browser.',
|
|
50
|
+
},
|
|
51
|
+
codex: { args: HARNESS_AUTH.codex.login },
|
|
52
|
+
};
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* never be derived from `getCliVersion()`.
|
|
28
28
|
*/
|
|
29
29
|
/** The helpers that have their own release train. */
|
|
30
|
-
export type HelperName = 'menubar' | '
|
|
30
|
+
export type HelperName = 'menubar' | 'computer-mac' | 'computer-win';
|
|
31
31
|
/** One helper's release identity. */
|
|
32
32
|
export interface HelperRelease {
|
|
33
33
|
/** Tag prefix — the release is `<tagPrefix>/v<version>`. */
|
|
@@ -41,9 +41,11 @@ export interface HelperRelease {
|
|
|
41
41
|
/**
|
|
42
42
|
* Floors, by helper.
|
|
43
43
|
*
|
|
44
|
-
* `menubar`
|
|
45
|
-
*
|
|
46
|
-
*
|
|
44
|
+
* `menubar` starts at 1.0.0 — the first build published under its own tag
|
|
45
|
+
* rather than the CLI's. It is not "version 1 of the helper"; it is version 1
|
|
46
|
+
* of its independent release train. The keychain helper moved with the
|
|
47
|
+
* standalone `secrets` engine (PHNX-3989) — it downloads and verifies its own
|
|
48
|
+
* helper release now, off this table entirely.
|
|
47
49
|
*/
|
|
48
50
|
export declare const HELPER_RELEASES: Readonly<Record<HelperName, HelperRelease>>;
|
|
49
51
|
/** The release tag for one helper at one version, e.g. `menubar/v1.0.0`. */
|
|
@@ -29,13 +29,14 @@
|
|
|
29
29
|
/**
|
|
30
30
|
* Floors, by helper.
|
|
31
31
|
*
|
|
32
|
-
* `menubar`
|
|
33
|
-
*
|
|
34
|
-
*
|
|
32
|
+
* `menubar` starts at 1.0.0 — the first build published under its own tag
|
|
33
|
+
* rather than the CLI's. It is not "version 1 of the helper"; it is version 1
|
|
34
|
+
* of its independent release train. The keychain helper moved with the
|
|
35
|
+
* standalone `secrets` engine (PHNX-3989) — it downloads and verifies its own
|
|
36
|
+
* helper release now, off this table entirely.
|
|
35
37
|
*/
|
|
36
38
|
export const HELPER_RELEASES = {
|
|
37
39
|
menubar: { tagPrefix: 'menubar', floor: '1.0.0' },
|
|
38
|
-
keychain: { tagPrefix: 'keychain', floor: '1.0.0' },
|
|
39
40
|
'computer-mac': { tagPrefix: 'computer-mac', floor: '1.0.0' },
|
|
40
41
|
// The Windows helper is a bare .exe, not an .app bundle, so it does not share
|
|
41
42
|
// helper-download.ts's zip/codesign/notarize machinery -- but it has the same
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** The host part of an ssh target (`user@host` -> `host`) for known_hosts matching. */
|
|
2
|
+
export declare function hostKeyLookupName(target: string): string;
|
|
3
|
+
/** Refuse durable credential transfer until the destination SSH key is pinned. */
|
|
4
|
+
export declare function assertCredentialTransportHostPinned(target: string, pinned?: boolean): void;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a `--device` value to an ssh target string for a credential-carrying
|
|
7
|
+
* remote operation. Delegates to the same resolver `run --device` uses; on a
|
|
8
|
+
* miss, treats the value as a raw ssh target and validates it.
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveHostSshTarget(nameOrAlias: string): Promise<string>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential transport policy for a fleet host (agents-owned, PHNX-3989).
|
|
3
|
+
*
|
|
4
|
+
* Whether a durable credential may leave this device for a given host is
|
|
5
|
+
* agents fleet policy, not a secrets-engine concern: it hangs off the managed
|
|
6
|
+
* `known_hosts` pin and the hosts/devices registry. The standalone `secrets`
|
|
7
|
+
* CLI only carries the bundle once agents-cli has decided the destination is
|
|
8
|
+
* trustworthy, so these guards live here and are passed INTO the transport,
|
|
9
|
+
* never re-implemented on the engine side.
|
|
10
|
+
*/
|
|
11
|
+
import { assertValidSshTarget } from '../ssh-exec.js';
|
|
12
|
+
import { resolveHost } from './registry.js';
|
|
13
|
+
import { sshTargetFor } from './types.js';
|
|
14
|
+
import { isHostPinned } from '../devices/known-hosts.js';
|
|
15
|
+
/** The host part of an ssh target (`user@host` -> `host`) for known_hosts matching. */
|
|
16
|
+
export function hostKeyLookupName(target) {
|
|
17
|
+
return target.split('@').pop() ?? target;
|
|
18
|
+
}
|
|
19
|
+
/** Refuse durable credential transfer until the destination SSH key is pinned. */
|
|
20
|
+
export function assertCredentialTransportHostPinned(target, pinned = isHostPinned(hostKeyLookupName(target))) {
|
|
21
|
+
if (pinned)
|
|
22
|
+
return;
|
|
23
|
+
throw new Error(`Refusing to transfer provider credentials to '${target}' before its SSH host key is pinned. ` +
|
|
24
|
+
`Connect once with 'agents ssh ${target}' and verify the host, then retry.`);
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Resolve a `--device` value to an ssh target string for a credential-carrying
|
|
28
|
+
* remote operation. Delegates to the same resolver `run --device` uses; on a
|
|
29
|
+
* miss, treats the value as a raw ssh target and validates it.
|
|
30
|
+
*/
|
|
31
|
+
export async function resolveHostSshTarget(nameOrAlias) {
|
|
32
|
+
const host = await resolveHost(nameOrAlias);
|
|
33
|
+
if (host)
|
|
34
|
+
return sshTargetFor(host);
|
|
35
|
+
assertValidSshTarget(nameOrAlias);
|
|
36
|
+
return nameOrAlias;
|
|
37
|
+
}
|
|
@@ -170,6 +170,13 @@ export interface DispatchOptions {
|
|
|
170
170
|
* (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
|
|
171
171
|
* the table side.
|
|
172
172
|
*/
|
|
173
|
+
/** Compose `agent[@version][#account]` so the peer resolves ITS slot (PHNX-3940 T5). */
|
|
174
|
+
export declare function runAgentSpecArg(opts: {
|
|
175
|
+
agent: string;
|
|
176
|
+
version?: string;
|
|
177
|
+
account?: string;
|
|
178
|
+
accountPicker?: boolean;
|
|
179
|
+
}): string;
|
|
173
180
|
export declare function buildRunForwardedArgs(opts: DispatchOptions): string[];
|
|
174
181
|
export interface InteractiveDispatchOptions {
|
|
175
182
|
agent: string;
|
|
@@ -359,8 +359,19 @@ async function launchDetached(host, target, opts) {
|
|
|
359
359
|
* (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
|
|
360
360
|
* the table side.
|
|
361
361
|
*/
|
|
362
|
+
/** Compose `agent[@version][#account]` so the peer resolves ITS slot (PHNX-3940 T5). */
|
|
363
|
+
export function runAgentSpecArg(opts) {
|
|
364
|
+
if (opts.accountPicker)
|
|
365
|
+
return `${opts.agent}@`;
|
|
366
|
+
let spec = opts.agent;
|
|
367
|
+
if (opts.version)
|
|
368
|
+
spec += `@${opts.version}`;
|
|
369
|
+
if (opts.account)
|
|
370
|
+
spec += `#${opts.account}`;
|
|
371
|
+
return spec;
|
|
372
|
+
}
|
|
362
373
|
export function buildRunForwardedArgs(opts) {
|
|
363
|
-
const agentArg = opts
|
|
374
|
+
const agentArg = runAgentSpecArg(opts);
|
|
364
375
|
const args = ['run', agentArg, opts.prompt, '--quiet'];
|
|
365
376
|
if (opts.mode)
|
|
366
377
|
args.push('--mode', opts.mode);
|
|
@@ -427,11 +438,7 @@ export function buildInteractiveRunForwardedArgs(opts) {
|
|
|
427
438
|
if (opts.version && opts.accountPicker) {
|
|
428
439
|
throw new Error('Interactive host dispatch cannot combine an account picker with a version pin');
|
|
429
440
|
}
|
|
430
|
-
const agentArg = opts
|
|
431
|
-
? `${opts.agent}@`
|
|
432
|
-
: opts.version
|
|
433
|
-
? `${opts.agent}@${opts.version}`
|
|
434
|
-
: opts.agent;
|
|
441
|
+
const agentArg = runAgentSpecArg(opts);
|
|
435
442
|
const args = ['run', agentArg];
|
|
436
443
|
if (opts.prompt && opts.forceInteractive)
|
|
437
444
|
args.push(opts.prompt);
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* here and nothing else.
|
|
11
11
|
*
|
|
12
12
|
* The shape mirrors the seams this repo already proved elsewhere —
|
|
13
|
-
*
|
|
14
|
-
* (`lib/cloud/types.ts`) — so a second identity backend, if
|
|
15
|
-
* needed, is a swap here rather than a sweep across commands.
|
|
13
|
+
* the bounded secrets process client (`lib/secrets-client.ts`) and
|
|
14
|
+
* `CloudProvider` (`lib/cloud/types.ts`) — so a second identity backend, if
|
|
15
|
+
* one is ever needed, is a swap here rather than a sweep across commands.
|
|
16
16
|
*/
|
|
17
17
|
/**
|
|
18
18
|
* Where the account backend lives. Config, never a literal at a call site.
|
|
@@ -10,9 +10,9 @@
|
|
|
10
10
|
* here and nothing else.
|
|
11
11
|
*
|
|
12
12
|
* The shape mirrors the seams this repo already proved elsewhere —
|
|
13
|
-
*
|
|
14
|
-
* (`lib/cloud/types.ts`) — so a second identity backend, if
|
|
15
|
-
* needed, is a swap here rather than a sweep across commands.
|
|
13
|
+
* the bounded secrets process client (`lib/secrets-client.ts`) and
|
|
14
|
+
* `CloudProvider` (`lib/cloud/types.ts`) — so a second identity backend, if
|
|
15
|
+
* one is ever needed, is a swap here rather than a sweep across commands.
|
|
16
16
|
*/
|
|
17
17
|
import * as fs from 'fs';
|
|
18
18
|
import * as path from 'path';
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* per-installation isolation track consumes exactly this).
|
|
9
9
|
*/
|
|
10
10
|
export { type Installation, type InstallationRelease, type UpdateOutcome, type UpdateStrategyId, type UpdatePolicy, } from './types.js';
|
|
11
|
-
export { createInstallation, listInstallations, recordRelease, } from './store.js';
|
|
11
|
+
export { createInstallation, listInstallations, recordRelease, MANAGED_INSTALLATION_LABEL, resolveManagedInstallation, ensureHarnessInstallation, type EnsureHarnessInstallationResult, } from './store.js';
|
|
12
12
|
export { describeInstallation, resolveInstallation, type ResolveInstallationOptions, } from './resolve.js';
|
|
13
13
|
export { selectUpdateStrategy, supportsPinnedUpdate, type UpdateStrategy, } from './strategies.js';
|
|
14
14
|
export { updateInstallation, type UpdateInstallationOptions } from './update.js';
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* it, so the internal split can change without breaking callers (the Cursor
|
|
8
8
|
* per-installation isolation track consumes exactly this).
|
|
9
9
|
*/
|
|
10
|
-
export { createInstallation, listInstallations, recordRelease, } from './store.js';
|
|
10
|
+
export { createInstallation, listInstallations, recordRelease, MANAGED_INSTALLATION_LABEL, resolveManagedInstallation, ensureHarnessInstallation, } from './store.js';
|
|
11
11
|
export { describeInstallation, resolveInstallation, } from './resolve.js';
|
|
12
12
|
export { selectUpdateStrategy, supportsPinnedUpdate, } from './strategies.js';
|
|
13
13
|
export { updateInstallation } from './update.js';
|
|
@@ -2366,4 +2366,13 @@ export async function runMigration() {
|
|
|
2366
2366
|
catch {
|
|
2367
2367
|
// best-effort — a migration must never fail because tmux wasn't reachable
|
|
2368
2368
|
}
|
|
2369
|
+
// PHNX-3940 T7: report leftover per-account installations. Dry-run only —
|
|
2370
|
+
// moving homes requires an explicit `agents accounts migrate --apply`.
|
|
2371
|
+
try {
|
|
2372
|
+
const { reportAccountSlotMigrationOnUpgrade } = await import('../accounts/migrate.js');
|
|
2373
|
+
await reportAccountSlotMigrationOnUpgrade();
|
|
2374
|
+
}
|
|
2375
|
+
catch {
|
|
2376
|
+
// best-effort — a corrupt install must not block the rest of upgrade
|
|
2377
|
+
}
|
|
2369
2378
|
}
|
|
@@ -21,7 +21,7 @@ export class InstallationNotFoundError extends Error {
|
|
|
21
21
|
// Nothing installed is a different problem from "your selector missed", and
|
|
22
22
|
// the remedy differs — say which one it is rather than printing an empty list.
|
|
23
23
|
super(available.length === 0
|
|
24
|
-
? `No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}
|
|
24
|
+
? `No ${AGENTS[agent].name} installations are managed by agents-cli. Install one with: agents add ${agent}`
|
|
25
25
|
: `No ${AGENTS[agent].name} installation matches '${selector}'. Installed: ${available.map((i) => describeInstallation(i)).join(', ')}`);
|
|
26
26
|
this.agent = agent;
|
|
27
27
|
this.selector = selector;
|
|
@@ -201,6 +201,21 @@ export declare const CONFIG_ENV_ISOLATED_AGENTS: readonly AgentId[];
|
|
|
201
201
|
* separate from the user's real `~/.<agent>`. See {@link CONFIG_ENV_ISOLATED_AGENTS}.
|
|
202
202
|
*/
|
|
203
203
|
export declare function supportsIsolatedInstall(agent: AgentId): boolean;
|
|
204
|
+
/**
|
|
205
|
+
* Harnesses that isolate by symlink-adopting `~/.<config>` rather than a
|
|
206
|
+
* config-dir env (PHNX-3940 T5). One active slot per device; `accounts default`
|
|
207
|
+
* repoints the adopted symlink under the auth-op lock.
|
|
208
|
+
*/
|
|
209
|
+
export declare function isSymlinkAdoptedHarness(agent: AgentId): boolean;
|
|
210
|
+
/**
|
|
211
|
+
* Repoint this harness's adopted `~/.<config>` symlink at `home`'s config dir.
|
|
212
|
+
* No-op for env-isolated harnesses. Fails loud when the adopted path is a real
|
|
213
|
+
* directory rather than a symlink — adopting that is `agents use`, not default.
|
|
214
|
+
*/
|
|
215
|
+
export declare function repointAdoptedConfigToHome(agent: AgentId, home: string): {
|
|
216
|
+
success: boolean;
|
|
217
|
+
error?: string;
|
|
218
|
+
};
|
|
204
219
|
/**
|
|
205
220
|
* Generate a versioned alias script that directly execs a specific version.
|
|
206
221
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|
|
@@ -10,6 +10,7 @@ import { IS_WINDOWS, prependToWindowsUserPath } from '../platform/index.js';
|
|
|
10
10
|
import { getShimsDir, getVersionsDir, getBackupsDir, getHistoryDir, ensureAgentsDir } from '../state.js';
|
|
11
11
|
export { getShimsDir };
|
|
12
12
|
import { AGENTS, agentConfigDirName, readAuthAccountIdentity } from '../agents.js';
|
|
13
|
+
import { acquireAuthOperationLock } from '../accounts/auth-operation-lock.js';
|
|
13
14
|
import { codexHomeShimBash } from '../codex-home.js';
|
|
14
15
|
import { resolveHarnessAdapter } from '../harness/index.js';
|
|
15
16
|
import { randomUUID } from 'node:crypto';
|
|
@@ -1130,6 +1131,74 @@ export const CONFIG_ENV_ISOLATED_AGENTS = ['claude', 'codex', 'copilot', 'cursor
|
|
|
1130
1131
|
export function supportsIsolatedInstall(agent) {
|
|
1131
1132
|
return CONFIG_ENV_ISOLATED_AGENTS.includes(agent);
|
|
1132
1133
|
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Harnesses that isolate by symlink-adopting `~/.<config>` rather than a
|
|
1136
|
+
* config-dir env (PHNX-3940 T5). One active slot per device; `accounts default`
|
|
1137
|
+
* repoints the adopted symlink under the auth-op lock.
|
|
1138
|
+
*/
|
|
1139
|
+
export function isSymlinkAdoptedHarness(agent) {
|
|
1140
|
+
return !CONFIG_ENV_ISOLATED_AGENTS.includes(agent);
|
|
1141
|
+
}
|
|
1142
|
+
/**
|
|
1143
|
+
* Repoint this harness's adopted `~/.<config>` symlink at `home`'s config dir.
|
|
1144
|
+
* No-op for env-isolated harnesses. Fails loud when the adopted path is a real
|
|
1145
|
+
* directory rather than a symlink — adopting that is `agents use`, not default.
|
|
1146
|
+
*/
|
|
1147
|
+
export function repointAdoptedConfigToHome(agent, home) {
|
|
1148
|
+
if (!isSymlinkAdoptedHarness(agent))
|
|
1149
|
+
return { success: true };
|
|
1150
|
+
const lock = acquireAuthOperationLock(agent);
|
|
1151
|
+
try {
|
|
1152
|
+
lock.assertHeld();
|
|
1153
|
+
const configPath = getAgentConfigPath(agent);
|
|
1154
|
+
const configDirName = path.relative(os.homedir(), AGENTS[agent].configDir);
|
|
1155
|
+
const target = path.join(home, configDirName);
|
|
1156
|
+
fs.mkdirSync(target, { recursive: true });
|
|
1157
|
+
try {
|
|
1158
|
+
const stat = fs.lstatSync(configPath);
|
|
1159
|
+
if (stat.isSymbolicLink()) {
|
|
1160
|
+
const current = path.resolve(path.dirname(configPath), fs.readlinkSync(configPath));
|
|
1161
|
+
if (current === path.resolve(target))
|
|
1162
|
+
return { success: true };
|
|
1163
|
+
}
|
|
1164
|
+
else {
|
|
1165
|
+
return {
|
|
1166
|
+
success: false,
|
|
1167
|
+
error: `${configPath} is not a symlink; refusing to replace a real config directory. `
|
|
1168
|
+
+ `Run \`agents use ${agent}\` once to adopt it, then \`agents accounts default ${agent}\`.`,
|
|
1169
|
+
};
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
catch (err) {
|
|
1173
|
+
if (err.code !== 'ENOENT')
|
|
1174
|
+
throw err;
|
|
1175
|
+
}
|
|
1176
|
+
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
1177
|
+
// Atomic retarget: create temp symlink, rename over existing (same pattern
|
|
1178
|
+
// as switchHomeFileSymlinks). A window of "path missing" would let a
|
|
1179
|
+
// harness process observe neither the old nor the new target.
|
|
1180
|
+
const tmpPath = `${configPath}.agents-tmp-${process.pid}`;
|
|
1181
|
+
try {
|
|
1182
|
+
fs.unlinkSync(tmpPath);
|
|
1183
|
+
}
|
|
1184
|
+
catch { /* leftover from a killed prior swap */ }
|
|
1185
|
+
try {
|
|
1186
|
+
fs.symlinkSync(target, tmpPath, process.platform === 'win32' ? 'junction' : undefined);
|
|
1187
|
+
fs.renameSync(tmpPath, configPath);
|
|
1188
|
+
}
|
|
1189
|
+
catch (err) {
|
|
1190
|
+
try {
|
|
1191
|
+
fs.unlinkSync(tmpPath);
|
|
1192
|
+
}
|
|
1193
|
+
catch { /* rename may have already consumed it */ }
|
|
1194
|
+
throw err;
|
|
1195
|
+
}
|
|
1196
|
+
return { success: true };
|
|
1197
|
+
}
|
|
1198
|
+
finally {
|
|
1199
|
+
lock.release();
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1133
1202
|
/**
|
|
1134
1203
|
* Generate a versioned alias script that directly execs a specific version.
|
|
1135
1204
|
* e.g., claude@2.0.65 -> directly runs that version's binary
|