@phnx-labs/agents-cli 1.22.84 → 1.22.86
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -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/alias.js +7 -3
- 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 +38 -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 +32 -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 +21 -4
- package/dist/lib/installations/shims.js +86 -5
- 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 +199 -0
- package/dist/lib/secrets-client.js +718 -0
- package/dist/lib/secrets-policy.d.ts +235 -0
- package/dist/lib/secrets-policy.js +505 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/self-heal/checks/shims.js +5 -3
- 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 +35 -30
- 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
|
@@ -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
|
|
@@ -2096,13 +2165,25 @@ export function listShimFileNames() {
|
|
|
2096
2165
|
return [];
|
|
2097
2166
|
}
|
|
2098
2167
|
}
|
|
2168
|
+
/**
|
|
2169
|
+
* Legacy command shims that are wrong even when their baked install is alive.
|
|
2170
|
+
* `secrets`: the shim `exec`s `agents secrets`, which since PHNX-3989 is a
|
|
2171
|
+
* passthrough to the STANDALONE `secrets` binary — so a `secrets` shim that
|
|
2172
|
+
* re-enters `agents secrets` is a recursion, never a working command. It sits
|
|
2173
|
+
* first on PATH and shadows the real `@phnx-labs/secrets-cli` bin, so it is
|
|
2174
|
+
* pruned unconditionally (the liveness check below is what kept it alive across
|
|
2175
|
+
* every in-place upgrade from 1.22.84).
|
|
2176
|
+
*/
|
|
2177
|
+
const LEGACY_SHIMS_ALWAYS_PRUNED = new Set(['secrets']);
|
|
2099
2178
|
/**
|
|
2100
2179
|
* Prune a stale, orphaned shim: one that is NOT a managed agent shim and NOT a user
|
|
2101
2180
|
* alias, whose baked `AGENTS_BIN` points at an install that no longer exists. These
|
|
2102
|
-
* are
|
|
2103
|
-
*
|
|
2104
|
-
*
|
|
2105
|
-
* removed when the baked target is gone, so a working shim is never touched
|
|
2181
|
+
* are `exec "$AGENTS_BIN" <cmd>` command shims (browser/sessions/pty/teams) that
|
|
2182
|
+
* `scripts/postinstall.js` writes for the LIVE install; one baked at a removed
|
|
2183
|
+
* install either dies with `exit 127` or shadows the real package bin on PATH.
|
|
2184
|
+
* Only removed when the baked target is gone, so a working shim is never touched —
|
|
2185
|
+
* except the `LEGACY_SHIMS_ALWAYS_PRUNED` set, which cannot work at all (and which
|
|
2186
|
+
* postinstall no longer writes).
|
|
2106
2187
|
* Returns true if removed.
|
|
2107
2188
|
*/
|
|
2108
2189
|
export function pruneOrphanedCommandShim(fileName) {
|
|
@@ -2123,7 +2204,7 @@ export function pruneOrphanedCommandShim(fileName) {
|
|
|
2123
2204
|
const bin = readAgentsBinFromShim(shimPath);
|
|
2124
2205
|
if (!bin)
|
|
2125
2206
|
return false; // not an AGENTS_BIN-baked shim
|
|
2126
|
-
if (fs.existsSync(bin))
|
|
2207
|
+
if (fs.existsSync(bin) && !LEGACY_SHIMS_ALWAYS_PRUNED.has(fileName))
|
|
2127
2208
|
return false; // its install is still alive — leave it
|
|
2128
2209
|
try {
|
|
2129
2210
|
fs.rmSync(shimPath);
|
|
@@ -60,6 +60,42 @@ export declare function listInstallationLabels(agent: AgentId): string[];
|
|
|
60
60
|
* that disappears mid-scan is skipped rather than failing the whole listing.
|
|
61
61
|
*/
|
|
62
62
|
export declare function listInstallations(agent: AgentId): Installation[];
|
|
63
|
+
/**
|
|
64
|
+
* The label of the single managed installation every harness gets under the
|
|
65
|
+
* account model v2 (PHNX-3940). One managed install per harness per device;
|
|
66
|
+
* accounts live in credential slots beside it, and a second home is an expert
|
|
67
|
+
* `--isolated` copy, never another managed install.
|
|
68
|
+
*/
|
|
69
|
+
export declare const MANAGED_INSTALLATION_LABEL = "main";
|
|
70
|
+
/**
|
|
71
|
+
* Resolve the ONE managed installation of a harness, or null when the harness
|
|
72
|
+
* has no managed install yet. The winner is deterministic: the `main` label
|
|
73
|
+
* first, then the recorded global default (the install the user actually
|
|
74
|
+
* launches), then the first remaining non-isolated installation by label.
|
|
75
|
+
* Isolated copies (`agents add --isolated`) never count — they are expert
|
|
76
|
+
* second homes, not the managed install.
|
|
77
|
+
*/
|
|
78
|
+
export declare function resolveManagedInstallation(agent: AgentId): Installation | null;
|
|
79
|
+
export interface EnsureHarnessInstallationResult {
|
|
80
|
+
installation: Installation;
|
|
81
|
+
/** True when this call installed the harness just now; false when the managed installation already existed and was reused unchanged. */
|
|
82
|
+
installed: boolean;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* The taught entry point for "make this harness runnable" (PHNX-3940): return
|
|
86
|
+
* the harness's single managed installation, installing the current release
|
|
87
|
+
* into the `main` label when the harness is not installed yet. `release` pins
|
|
88
|
+
* the fresh install to a concrete release (recorded as a pinned update policy
|
|
89
|
+
* by `installVersion`); it has no effect when an installation already exists —
|
|
90
|
+
* moving an existing install is `agents update <agent> --to <release>`'s job.
|
|
91
|
+
*
|
|
92
|
+
* Throws (fail loud) when the install fails; never returns a half-installed
|
|
93
|
+
* record.
|
|
94
|
+
*/
|
|
95
|
+
export declare function ensureHarnessInstallation(agent: AgentId, opts?: {
|
|
96
|
+
release?: string;
|
|
97
|
+
onProgress?: (message: string) => void;
|
|
98
|
+
}): Promise<EnsureHarnessInstallationResult>;
|
|
63
99
|
/**
|
|
64
100
|
* Get the directory where a specific version is installed.
|
|
65
101
|
*/
|
|
@@ -154,6 +190,13 @@ export declare function pickCanonicalGlobalBinaryVersion(agent: AgentId, version
|
|
|
154
190
|
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
155
191
|
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
156
192
|
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
193
|
+
*
|
|
194
|
+
* A directory is an installation only when it has a working launch binary.
|
|
195
|
+
* Account credential slots (PHNX-3940, `~/.agents/.history/accounts/**`) are
|
|
196
|
+
* HOME-shaped dirs with no binary and no `installation.json`, so they can
|
|
197
|
+
* never appear here. Record-less pre-frozen installs are reported as-is; their
|
|
198
|
+
* migration belongs to the record-based paths (`listInstallations`), never to
|
|
199
|
+
* this read surface.
|
|
157
200
|
*/
|
|
158
201
|
export declare function listInstalledVersions(agent: AgentId): string[];
|
|
159
202
|
/**
|
|
@@ -224,6 +224,62 @@ export function listInstallations(agent) {
|
|
|
224
224
|
}
|
|
225
225
|
return out;
|
|
226
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* The label of the single managed installation every harness gets under the
|
|
229
|
+
* account model v2 (PHNX-3940). One managed install per harness per device;
|
|
230
|
+
* accounts live in credential slots beside it, and a second home is an expert
|
|
231
|
+
* `--isolated` copy, never another managed install.
|
|
232
|
+
*/
|
|
233
|
+
export const MANAGED_INSTALLATION_LABEL = 'main';
|
|
234
|
+
/**
|
|
235
|
+
* Resolve the ONE managed installation of a harness, or null when the harness
|
|
236
|
+
* has no managed install yet. The winner is deterministic: the `main` label
|
|
237
|
+
* first, then the recorded global default (the install the user actually
|
|
238
|
+
* launches), then the first remaining non-isolated installation by label.
|
|
239
|
+
* Isolated copies (`agents add --isolated`) never count — they are expert
|
|
240
|
+
* second homes, not the managed install.
|
|
241
|
+
*/
|
|
242
|
+
export function resolveManagedInstallation(agent) {
|
|
243
|
+
const managed = listInstallations(agent).filter((i) => !isVersionIsolated(agent, i.label));
|
|
244
|
+
if (managed.length === 0)
|
|
245
|
+
return null;
|
|
246
|
+
const main = managed.find((i) => i.label === MANAGED_INSTALLATION_LABEL);
|
|
247
|
+
if (main)
|
|
248
|
+
return main;
|
|
249
|
+
const globalDefault = getGlobalDefault(agent);
|
|
250
|
+
const def = globalDefault ? managed.find((i) => i.label === globalDefault) : undefined;
|
|
251
|
+
return def ?? managed[0];
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* The taught entry point for "make this harness runnable" (PHNX-3940): return
|
|
255
|
+
* the harness's single managed installation, installing the current release
|
|
256
|
+
* into the `main` label when the harness is not installed yet. `release` pins
|
|
257
|
+
* the fresh install to a concrete release (recorded as a pinned update policy
|
|
258
|
+
* by `installVersion`); it has no effect when an installation already exists —
|
|
259
|
+
* moving an existing install is `agents update <agent> --to <release>`'s job.
|
|
260
|
+
*
|
|
261
|
+
* Throws (fail loud) when the install fails; never returns a half-installed
|
|
262
|
+
* record.
|
|
263
|
+
*/
|
|
264
|
+
export async function ensureHarnessInstallation(agent, opts = {}) {
|
|
265
|
+
const existing = resolveManagedInstallation(agent);
|
|
266
|
+
if (existing)
|
|
267
|
+
return { installation: existing, installed: false };
|
|
268
|
+
const release = opts.release ?? 'latest';
|
|
269
|
+
// installVersion lives in ./versions.js, which imports this file. A dynamic
|
|
270
|
+
// import keeps the static module graph acyclic (see the docblock at the top
|
|
271
|
+
// of this file) while the call itself runs after both modules are loaded.
|
|
272
|
+
const { installVersion } = await import('./versions.js');
|
|
273
|
+
const result = await installVersion(agent, release, opts.onProgress, { installationLabel: MANAGED_INSTALLATION_LABEL });
|
|
274
|
+
if (!result.success) {
|
|
275
|
+
throw new Error(result.error || `Failed to install ${agent}@${release}.`);
|
|
276
|
+
}
|
|
277
|
+
const record = readInstallation(agent, MANAGED_INSTALLATION_LABEL);
|
|
278
|
+
if (!record) {
|
|
279
|
+
throw new Error(`Install of ${agent}@${release} reported success but left no installation record at ${MANAGED_INSTALLATION_LABEL}.`);
|
|
280
|
+
}
|
|
281
|
+
return { installation: record, installed: true };
|
|
282
|
+
}
|
|
227
283
|
/**
|
|
228
284
|
* Get the directory where a specific version is installed.
|
|
229
285
|
*/
|
|
@@ -534,6 +590,13 @@ function collapseGlobalBinaryVersions(agent, versions) {
|
|
|
534
590
|
* For a self-updating global-binary agent (droid) every version dir resolves to
|
|
535
591
|
* the SAME binary, so this collapses them to a single canonical entry — one
|
|
536
592
|
* install, one row in `agents view`, never the phantom set of semver dir names.
|
|
593
|
+
*
|
|
594
|
+
* A directory is an installation only when it has a working launch binary.
|
|
595
|
+
* Account credential slots (PHNX-3940, `~/.agents/.history/accounts/**`) are
|
|
596
|
+
* HOME-shaped dirs with no binary and no `installation.json`, so they can
|
|
597
|
+
* never appear here. Record-less pre-frozen installs are reported as-is; their
|
|
598
|
+
* migration belongs to the record-based paths (`listInstallations`), never to
|
|
599
|
+
* this read surface.
|
|
537
600
|
*/
|
|
538
601
|
export function listInstalledVersions(agent) {
|
|
539
602
|
const agentVersionsDir = path.join(getVersionsDir(), agent);
|
|
@@ -554,13 +617,18 @@ export function listInstalledVersions(agent) {
|
|
|
554
617
|
const entries = fs.readdirSync(agentVersionsDir, { withFileTypes: true });
|
|
555
618
|
const versions = [];
|
|
556
619
|
for (const entry of entries) {
|
|
557
|
-
if (entry.isDirectory())
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
620
|
+
if (!entry.isDirectory())
|
|
621
|
+
continue;
|
|
622
|
+
// Probe the real launch binary (isVersionInstalled), not just the
|
|
623
|
+
// node_modules/.bin wrapper — a gutted install must not count as healthy
|
|
624
|
+
// in the balanced account/version picker. A dir with no working binary
|
|
625
|
+
// (a slot, a partial download) is not an installation at all. Read-only:
|
|
626
|
+
// `agents view` enumerates through here and must leave every version home
|
|
627
|
+
// byte-identical (issue #2058), so a record-less pre-frozen install is
|
|
628
|
+
// reported as-is — its migration belongs to the record-based paths
|
|
629
|
+
// (listInstallations), never to a read surface.
|
|
630
|
+
if (isVersionInstalled(agent, entry.name)) {
|
|
631
|
+
versions.push(entry.name);
|
|
564
632
|
}
|
|
565
633
|
}
|
|
566
634
|
versions.sort(compareVersions);
|
|
@@ -1044,7 +1044,7 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1044
1044
|
throw new Error(`Invalid version: ${JSON.stringify(version)}`);
|
|
1045
1045
|
}
|
|
1046
1046
|
// A caller-supplied installation label decouples the addressable version-dir
|
|
1047
|
-
// name from the vendor release (PHNX-3940): `agents accounts
|
|
1047
|
+
// name from the vendor release (PHNX-3940): `agents accounts add` mints an
|
|
1048
1048
|
// opaque stable label so ten accounts can share the same upstream `latest`
|
|
1049
1049
|
// under distinct isolated homes, each with its own native login. It follows
|
|
1050
1050
|
// the same identity/release split the installScript branch already uses via
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
|
|
3
3
|
*
|
|
4
|
-
* Mirrors
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Mirrors the stable-Application-Support-path-survives-npm-re-sign pattern and
|
|
5
|
+
* a RunAtLoad + KeepAlive user service, the same shape the deleted embedded
|
|
6
|
+
* secrets-agent used before it moved to the standalone secrets-cli (PHNX-3989).
|
|
7
7
|
*
|
|
8
8
|
* The helper is a no-Dock `.accessory` status-bar app. It reads live agent
|
|
9
9
|
* state directly from disk and shells `agents` only for actions, so the plist
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Install + lifecycle for the macOS menu-bar helper (`MenubarHelper.app`).
|
|
3
3
|
*
|
|
4
|
-
* Mirrors
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* Mirrors the stable-Application-Support-path-survives-npm-re-sign pattern and
|
|
5
|
+
* a RunAtLoad + KeepAlive user service, the same shape the deleted embedded
|
|
6
|
+
* secrets-agent used before it moved to the standalone secrets-cli (PHNX-3989).
|
|
7
7
|
*
|
|
8
8
|
* The helper is a no-Dock `.accessory` status-bar app. It reads live agent
|
|
9
9
|
* state directly from disk and shells `agents` only for actions, so the plist
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native-account lookup leaf (PHNX-3940).
|
|
3
|
+
*
|
|
4
|
+
* A PURE, types-only module: it answers "which registered account is this
|
|
5
|
+
* email / what does its identity key decode to" from a `Meta` snapshot passed
|
|
6
|
+
* in by the caller — it never reads state, the keychain, or the network, and it
|
|
7
|
+
* imports nothing from `account-registry` (which imports the agent-spec engine).
|
|
8
|
+
* That import-graph constraint is load-bearing: `agent-spec/agents.ts`
|
|
9
|
+
* completes an email-only worker home from the registry row here, and importing
|
|
10
|
+
* `account-registry` there would be a cycle. `account-registry` re-exports both
|
|
11
|
+
* functions so existing callers keep one import site.
|
|
12
|
+
*
|
|
13
|
+
* The merge of central (`accounts.native`, `scope:'version'`) and device-scoped
|
|
14
|
+
* (`deviceAccounts.native`) rows mirrors `listNativeAccounts`, inlined so this
|
|
15
|
+
* module stays a leaf.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentId, Meta, NativeAccountRecord } from './types.js';
|
|
18
|
+
/**
|
|
19
|
+
* The one native row for `agent` whose `identityLabel` (email) matches, or null.
|
|
20
|
+
*
|
|
21
|
+
* Returns null when zero OR several rows match — never guesses. Several rows can
|
|
22
|
+
* share one email (a Team seat AND a personal Max under the same address are two
|
|
23
|
+
* accounts, two orgs), and picking one would attribute a worker home to the
|
|
24
|
+
* wrong org. The read-side caller falls closed to email-only identity in that
|
|
25
|
+
* case, which is correct: an ambiguous identity is better left unresolved than
|
|
26
|
+
* resolved wrong.
|
|
27
|
+
*/
|
|
28
|
+
export declare function registeredNativeAccountForEmail(meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, agent: AgentId, email: string): NativeAccountRecord | null;
|
|
29
|
+
/**
|
|
30
|
+
* Decode an `<agent>:<label>=<value>[:<label>=<value>…]` identity key into its
|
|
31
|
+
* parts, or null when it does not belong to `agent` or is malformed. The key
|
|
32
|
+
* shape is what `buildIdentityKey` in `agent-spec/agents.ts` writes — e.g.
|
|
33
|
+
* `claude:account=<uuid>:org=<uuid>` → `{ account, org }`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseNativeIdentityKey(agent: AgentId, identityKey: string): Record<string, string> | null;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/** Central + device-scoped native rows, device winning on a shared id. */
|
|
2
|
+
function mergedNativeAccounts(meta) {
|
|
3
|
+
const merged = { ...meta.accounts?.native, ...meta.deviceAccounts?.native };
|
|
4
|
+
return Object.values(merged);
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* The one native row for `agent` whose `identityLabel` (email) matches, or null.
|
|
8
|
+
*
|
|
9
|
+
* Returns null when zero OR several rows match — never guesses. Several rows can
|
|
10
|
+
* share one email (a Team seat AND a personal Max under the same address are two
|
|
11
|
+
* accounts, two orgs), and picking one would attribute a worker home to the
|
|
12
|
+
* wrong org. The read-side caller falls closed to email-only identity in that
|
|
13
|
+
* case, which is correct: an ambiguous identity is better left unresolved than
|
|
14
|
+
* resolved wrong.
|
|
15
|
+
*/
|
|
16
|
+
export function registeredNativeAccountForEmail(meta, agent, email) {
|
|
17
|
+
const needle = email.trim().toLowerCase();
|
|
18
|
+
if (!needle)
|
|
19
|
+
return null;
|
|
20
|
+
const matches = mergedNativeAccounts(meta).filter((account) => account.agent === agent && account.identityLabel?.trim().toLowerCase() === needle);
|
|
21
|
+
return matches.length === 1 ? matches[0] : null;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Decode an `<agent>:<label>=<value>[:<label>=<value>…]` identity key into its
|
|
25
|
+
* parts, or null when it does not belong to `agent` or is malformed. The key
|
|
26
|
+
* shape is what `buildIdentityKey` in `agent-spec/agents.ts` writes — e.g.
|
|
27
|
+
* `claude:account=<uuid>:org=<uuid>` → `{ account, org }`.
|
|
28
|
+
*/
|
|
29
|
+
export function parseNativeIdentityKey(agent, identityKey) {
|
|
30
|
+
const prefix = `${agent}:`;
|
|
31
|
+
if (!identityKey.startsWith(prefix))
|
|
32
|
+
return null;
|
|
33
|
+
const parts = {};
|
|
34
|
+
for (const segment of identityKey.slice(prefix.length).split(':')) {
|
|
35
|
+
const eq = segment.indexOf('=');
|
|
36
|
+
if (eq < 1 || eq === segment.length - 1)
|
|
37
|
+
return null;
|
|
38
|
+
parts[segment.slice(0, eq)] = segment.slice(eq + 1);
|
|
39
|
+
}
|
|
40
|
+
return Object.keys(parts).length > 0 ? parts : null;
|
|
41
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bounded-close helper for a local `net.Server`. Generic — no secrets/engine
|
|
3
|
+
* dependency — so consumers that only needed it for that reason (the webhook
|
|
4
|
+
* receiver, the daemon's webhook service) no longer import `lib/secrets/*`.
|
|
5
|
+
*/
|
|
6
|
+
import type * as net from 'node:net';
|
|
7
|
+
/**
|
|
8
|
+
* Close `server`, resolving once closed or after `timeoutMs`, whichever comes
|
|
9
|
+
* first — a shutdown must never hang on a client that never disconnects.
|
|
10
|
+
*/
|
|
11
|
+
export declare function closeServerBounded(server: net.Server, timeoutMs?: number): Promise<void>;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
const SERVER_CLOSE_TIMEOUT_MS = 5000;
|
|
2
|
+
/**
|
|
3
|
+
* Close `server`, resolving once closed or after `timeoutMs`, whichever comes
|
|
4
|
+
* first — a shutdown must never hang on a client that never disconnects.
|
|
5
|
+
*/
|
|
6
|
+
export function closeServerBounded(server, timeoutMs = SERVER_CLOSE_TIMEOUT_MS) {
|
|
7
|
+
return new Promise((resolve) => {
|
|
8
|
+
let settled = false;
|
|
9
|
+
const finish = () => {
|
|
10
|
+
if (settled)
|
|
11
|
+
return;
|
|
12
|
+
settled = true;
|
|
13
|
+
clearTimeout(timer);
|
|
14
|
+
resolve();
|
|
15
|
+
};
|
|
16
|
+
const timer = setTimeout(finish, timeoutMs);
|
|
17
|
+
try {
|
|
18
|
+
server.close(() => finish());
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Already closed / not listening — treat as released.
|
|
22
|
+
finish();
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
}
|
|
@@ -1,6 +1,32 @@
|
|
|
1
1
|
import { execFileSync, spawnSync } from 'child_process';
|
|
2
2
|
import { getCliLaunch } from './cli-entry.js';
|
|
3
|
-
|
|
3
|
+
/**
|
|
4
|
+
* argv for writing a bare (non-`agents-cli.`) keychain item via
|
|
5
|
+
* `/usr/bin/security add-generic-password`, deliberately WITHOUT the value: the
|
|
6
|
+
* secret travels over stdin (see below) so it never lands in argv or a `ps`
|
|
7
|
+
* snapshot. Pure — no agents-cli secrets engine involved (this migration writes
|
|
8
|
+
* directly to the OpenClaw account's OWN macOS keychain entries, bypassing the
|
|
9
|
+
* `agents secrets` bundle store entirely), so it's a local helper rather than a
|
|
10
|
+
* process-client call.
|
|
11
|
+
*/
|
|
12
|
+
function buildAddGenericPasswordArgs(account, item) {
|
|
13
|
+
return ['add-generic-password', '-U', '-a', account, '-s', item, '-w'];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* spawnSync options for the bare `-w` keychain write. `input` pipes the value
|
|
17
|
+
* TWICE (bare `-w` prompts enter+confirm; one line fails the confirm and stores
|
|
18
|
+
* an empty secret). `detached: true` runs `security` in a new session with no
|
|
19
|
+
* controlling terminal, so readpassphrase(3) falls back to our piped stdin
|
|
20
|
+
* instead of prompting the user's `/dev/tty` in an interactive shell.
|
|
21
|
+
*/
|
|
22
|
+
function buildAddGenericPasswordSpawnOptions(value) {
|
|
23
|
+
return {
|
|
24
|
+
input: `${value}\n${value}\n`,
|
|
25
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
26
|
+
timeout: 10_000,
|
|
27
|
+
detached: true,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
4
30
|
export const OPENCLAW_KEYCHAIN_ACCOUNT = 'openclaw';
|
|
5
31
|
export const OPENCLAW_KEYCHAIN_PROVIDER = 'agents_keychain';
|
|
6
32
|
export const OPENCLAW_KEYCHAIN_ENV_SERVICES = {
|
package/dist/lib/profiles.js
CHANGED
|
@@ -11,7 +11,7 @@ import * as crypto from 'node:crypto';
|
|
|
11
11
|
import * as yaml from 'yaml';
|
|
12
12
|
import { ALL_AGENT_IDS } from './agents.js';
|
|
13
13
|
import { getUserAgentsDir } from './state.js';
|
|
14
|
-
import {
|
|
14
|
+
import { deleteKeychainTokenSync, getKeychainTokenSync, hasKeychainTokenSync, isSecretsClientError, profileKeychainItem } from './secrets-client.js';
|
|
15
15
|
import { getPreset } from './profiles-presets.js';
|
|
16
16
|
import { MODEL_TIERS, isTierToken } from './model-tiers.js';
|
|
17
17
|
import { addAccount, findAccount, resolveCredentialAccount } from './account-registry.js';
|
|
@@ -85,7 +85,7 @@ function migrateLegacyProfileAuth(profile, file) {
|
|
|
85
85
|
const accountName = `legacy-${profile.provider}-${suffix}`;
|
|
86
86
|
if (!findAccount(accountName)) {
|
|
87
87
|
const kind = profile.auth.envVar.includes('BEARER_TOKEN') ? 'bearer-token' : 'api-key';
|
|
88
|
-
addAccount(accountName, provider, kind,
|
|
88
|
+
addAccount(accountName, provider, kind, getKeychainTokenSync(profile.auth.keychainItem));
|
|
89
89
|
}
|
|
90
90
|
const migratedAccount = findAccount(accountName);
|
|
91
91
|
const oldItem = profile.auth.keychainItem;
|
|
@@ -110,7 +110,7 @@ function migrateLegacyProfileAuth(profile, file) {
|
|
|
110
110
|
}
|
|
111
111
|
});
|
|
112
112
|
if (!stillReferenced)
|
|
113
|
-
|
|
113
|
+
deleteKeychainTokenSync(oldItem);
|
|
114
114
|
}
|
|
115
115
|
/** Write a profile to disk atomically (write-to-tmp then rename). */
|
|
116
116
|
export function writeProfile(profile) {
|
|
@@ -250,7 +250,7 @@ export function profileAuthLabel(profile) {
|
|
|
250
250
|
return `${provider} ${maskToken(token)}`;
|
|
251
251
|
}
|
|
252
252
|
if (profile.auth) {
|
|
253
|
-
return `${provider} ${
|
|
253
|
+
return `${provider} ${hasKeychainTokenSync(profile.auth.keychainItem) ? 'stored' : 'missing'}`;
|
|
254
254
|
}
|
|
255
255
|
return provider;
|
|
256
256
|
}
|
|
@@ -324,7 +324,7 @@ export function profileFromPreset(profileName, preset, version) {
|
|
|
324
324
|
env: { ...preset.env },
|
|
325
325
|
auth: {
|
|
326
326
|
envVar: preset.authEnvVar,
|
|
327
|
-
keychainItem:
|
|
327
|
+
keychainItem: profileKeychainItem(preset.provider),
|
|
328
328
|
},
|
|
329
329
|
authOptional: preset.authOptional,
|
|
330
330
|
description: preset.description,
|
|
@@ -392,7 +392,7 @@ export function profileFromHostModel(name, host, model, opts = {}) {
|
|
|
392
392
|
forkedFrom: host,
|
|
393
393
|
};
|
|
394
394
|
if (opts.provider && opts.authEnvVar) {
|
|
395
|
-
profile.auth = { envVar: opts.authEnvVar, keychainItem:
|
|
395
|
+
profile.auth = { envVar: opts.authEnvVar, keychainItem: profileKeychainItem(opts.provider) };
|
|
396
396
|
profile.authOptional = false;
|
|
397
397
|
}
|
|
398
398
|
return profile;
|
|
@@ -455,7 +455,7 @@ export function forkProfile(source, name, opts = {}) {
|
|
|
455
455
|
throw new Error(`Host '${host}' has no known auth env var; --provider cannot be attached to this fork.`);
|
|
456
456
|
}
|
|
457
457
|
forked.provider = opts.provider;
|
|
458
|
-
forked.auth = { envVar, keychainItem:
|
|
458
|
+
forked.auth = { envVar, keychainItem: profileKeychainItem(opts.provider) };
|
|
459
459
|
forked.authOptional = source.authOptional ?? false;
|
|
460
460
|
}
|
|
461
461
|
return forked;
|
|
@@ -529,10 +529,23 @@ export function resolveProfileEnv(profile) {
|
|
|
529
529
|
// Optional auth (host manages its own login) with no stored token: inject
|
|
530
530
|
// nothing and let the host use its own credentials. Only required auth
|
|
531
531
|
// hard-fails on a missing keychain item.
|
|
532
|
-
if (profile.authOptional && !
|
|
532
|
+
if (profile.authOptional && !hasKeychainTokenSync(profile.auth.keychainItem)) {
|
|
533
533
|
return env;
|
|
534
534
|
}
|
|
535
|
-
|
|
535
|
+
let token;
|
|
536
|
+
try {
|
|
537
|
+
token = getKeychainTokenSync(profile.auth.keychainItem);
|
|
538
|
+
}
|
|
539
|
+
catch (err) {
|
|
540
|
+
// The standalone deliberately reports only a code, so name the item and
|
|
541
|
+
// the repair here — a bare `NOT_FOUND` gives the user nothing to act on.
|
|
542
|
+
if (!isSecretsClientError(err, 'NOT_FOUND'))
|
|
543
|
+
throw err;
|
|
544
|
+
throw new Error(`Harness '${profile.name}' needs a ${profile.provider ?? 'provider'} key, but its keychain item ` +
|
|
545
|
+
`'${profile.auth.keychainItem}' is missing on this device. Store one with ` +
|
|
546
|
+
`'agents harness edit ${profile.name} --auth-provider ${profile.provider ?? '<provider>'}' ` +
|
|
547
|
+
`(or --from-secrets <bundle>:<key>), or bind an account with 'agents harness edit ${profile.name} --account <name>'.`);
|
|
548
|
+
}
|
|
536
549
|
env[profile.auth.envVar] = token;
|
|
537
550
|
}
|
|
538
551
|
return env;
|
|
@@ -9,7 +9,7 @@ import { markdownToToml } from './convert.js';
|
|
|
9
9
|
import { safeJoin } from './paths.js';
|
|
10
10
|
import { subagentTarget } from './subagents-registry.js';
|
|
11
11
|
import { parseSubagentFrontmatter } from './subagents.js';
|
|
12
|
-
import { syncWorkflowToVersion } from './workflows.js';
|
|
12
|
+
import { syncWorkflowToVersion } from './workflows-registry.js';
|
|
13
13
|
const MANIFEST_FILE = '.agents-managed.json';
|
|
14
14
|
const MANIFEST_VERSION = 1;
|
|
15
15
|
const COPY_IGNORE = new Set(['.DS_Store', '.git', '.gitignore', '.venv', '__pycache__', 'node_modules']);
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { SecretsBackend } from './secrets-types.js';
|
|
2
|
+
import { type AgentId } from './types.js';
|
|
3
|
+
/** Legacy readable alias for `__claude__`. Not migrated in this track. */
|
|
4
|
+
export declare const AUTH_STORE_ALIAS = "auth";
|
|
5
|
+
/** The one reserved bundle name most callers actually need. */
|
|
6
|
+
export declare const AUTH_BUNDLE_NAME = "auth";
|
|
7
|
+
/**
|
|
8
|
+
* The reserved `auth` bundle holds per-account Claude setup-tokens for
|
|
9
|
+
* unattended usage/probe and MUST be file-backed (headless, fleet-shareable —
|
|
10
|
+
* credential-management.md invariant 7). The standalone enforces the same
|
|
11
|
+
* rule on its write path (`WRONG_BACKEND`); agents-cli asserts it on every
|
|
12
|
+
* read so a keychain/vault-backed `auth` left over from an older layout fails
|
|
13
|
+
* loud instead of silently being ignored by usage/probe (SEC-GAP-3).
|
|
14
|
+
*/
|
|
15
|
+
export declare const AUTH_BUNDLE_BACKEND: SecretsBackend;
|
|
16
|
+
export declare const RESERVED_BUNDLE_NAMES: Set<string>;
|
|
17
|
+
export declare function isReservedBundleName(name: string): boolean;
|
|
18
|
+
/** Thrown when the reserved `auth` bundle is found on the wrong backend. */
|
|
19
|
+
export declare class ReservedBundleWrongBackendError extends Error {
|
|
20
|
+
readonly bundle: string;
|
|
21
|
+
readonly backend: SecretsBackend;
|
|
22
|
+
constructor(bundle: string, backend: SecretsBackend);
|
|
23
|
+
}
|
|
24
|
+
/** Fail loud when the reserved `auth` bundle is on any backend but `file`. */
|
|
25
|
+
export declare function assertReservedAuthBackend(backend: SecretsBackend): void;
|
|
26
|
+
/**
|
|
27
|
+
* True for the wrong-backend refusal in either shape: raised here from a
|
|
28
|
+
* read-side check, or returned by the standalone (`WRONG_BACKEND`) when its
|
|
29
|
+
* own write/resolve guard fired.
|
|
30
|
+
*/
|
|
31
|
+
export declare function isReservedBundleBackendError(error: unknown): boolean;
|
|
32
|
+
export declare const RESERVED_STORES: Record<AgentId, string>;
|
|
33
|
+
export declare function reservedStoreName(harness: AgentId): string;
|
|
34
|
+
export declare function isReservedStoreName(name: string): boolean;
|
|
35
|
+
export type StorableCredentialKind = 'setup-token' | 'api-key';
|
|
36
|
+
/**
|
|
37
|
+
* Fail loud at the write boundary: only a setup-token or an API key may enter
|
|
38
|
+
* a reserved store. Rotating OAuth/session credentials stay in their slot on
|
|
39
|
+
* the device that minted them.
|
|
40
|
+
*/
|
|
41
|
+
export declare function assertStorableCredentialKind(kind: string, harness?: AgentId): asserts kind is StorableCredentialKind;
|
|
42
|
+
/**
|
|
43
|
+
* Whether the local reserved `auth` bundle exists and is on the expected
|
|
44
|
+
* (file) backend. `ok: true` on a missing bundle — nothing to be wrong about.
|
|
45
|
+
* Read-only status probe (`agents doctor`, `agents fleet apply`); an
|
|
46
|
+
* unreachable standalone is treated the same as "bundle absent" so a
|
|
47
|
+
* diagnostic never crashes over one optional finding.
|
|
48
|
+
*/
|
|
49
|
+
export declare function inspectReservedAuthBundle(): {
|
|
50
|
+
exists: boolean;
|
|
51
|
+
backend: SecretsBackend | null;
|
|
52
|
+
ok: boolean;
|
|
53
|
+
};
|