@phnx-labs/agents-cli 1.20.35 → 1.20.36
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 +91 -0
- package/LICENSE +185 -21
- package/README.md +8 -4
- package/dist/commands/audit.d.ts +14 -0
- package/dist/commands/audit.js +68 -0
- package/dist/commands/browser.js +82 -8
- package/dist/commands/check.d.ts +15 -0
- package/dist/commands/check.js +84 -0
- package/dist/commands/cloud.js +143 -3
- package/dist/commands/computer.js +81 -0
- package/dist/commands/daemon.js +4 -1
- package/dist/commands/doctor.js +1 -89
- package/dist/commands/events.js +3 -3
- package/dist/commands/exec.d.ts +19 -0
- package/dist/commands/exec.js +277 -11
- package/dist/commands/hosts.js +10 -6
- package/dist/commands/inspect.js +8 -9
- package/dist/commands/lock.d.ts +12 -0
- package/dist/commands/lock.js +70 -0
- package/dist/commands/message.d.ts +15 -0
- package/dist/commands/message.js +56 -0
- package/dist/commands/routines.js +101 -5
- package/dist/commands/secrets-migrate.js +106 -57
- package/dist/commands/secrets.d.ts +31 -18
- package/dist/commands/secrets.js +156 -75
- package/dist/commands/serve.d.ts +10 -0
- package/dist/commands/serve.js +37 -0
- package/dist/commands/sessions-inject.d.ts +14 -0
- package/dist/commands/sessions-inject.js +111 -0
- package/dist/commands/sessions-picker.d.ts +2 -0
- package/dist/commands/sessions-picker.js +24 -3
- package/dist/commands/sessions-resume.js +20 -8
- package/dist/commands/sessions.d.ts +71 -1
- package/dist/commands/sessions.js +365 -37
- package/dist/commands/setup.js +4 -2
- package/dist/commands/sync.d.ts +3 -1
- package/dist/commands/sync.js +156 -4
- package/dist/commands/teams.js +217 -0
- package/dist/commands/versions.js +2 -4
- package/dist/commands/watchdog.d.ts +18 -0
- package/dist/commands/watchdog.js +238 -0
- package/dist/index.js +25 -2
- package/dist/lib/audit/log.d.ts +92 -0
- package/dist/lib/audit/log.js +177 -0
- package/dist/lib/auto-pull.js +2 -1
- package/dist/lib/browser/chrome.d.ts +10 -0
- package/dist/lib/browser/chrome.js +18 -7
- package/dist/lib/browser/drivers/ssh.js +2 -1
- package/dist/lib/browser/har.d.ts +84 -0
- package/dist/lib/browser/har.js +77 -0
- package/dist/lib/browser/ipc.js +24 -3
- package/dist/lib/browser/profiles.d.ts +1 -1
- package/dist/lib/browser/profiles.js +8 -10
- package/dist/lib/browser/refs.d.ts +65 -0
- package/dist/lib/browser/refs.js +73 -1
- package/dist/lib/browser/runtime-state.js +1 -0
- package/dist/lib/browser/service.d.ts +38 -2
- package/dist/lib/browser/service.js +112 -8
- package/dist/lib/browser/types.d.ts +14 -1
- package/dist/lib/budget/live-cloud.d.ts +42 -0
- package/dist/lib/budget/live-cloud.js +79 -0
- package/dist/lib/budget/live-team.d.ts +31 -0
- package/dist/lib/budget/live-team.js +115 -0
- package/dist/lib/cloud/codex.js +4 -0
- package/dist/lib/cloud/rush.d.ts +12 -1
- package/dist/lib/cloud/rush.js +13 -3
- package/dist/lib/cloud/types.d.ts +9 -0
- package/dist/lib/computer/dispatch.d.ts +8 -0
- package/dist/lib/computer/dispatch.js +125 -0
- package/dist/lib/computer/loop.d.ts +62 -0
- package/dist/lib/computer/loop.js +98 -0
- package/dist/lib/computer/model.d.ts +44 -0
- package/dist/lib/computer/model.js +157 -0
- package/dist/lib/concurrency.d.ts +19 -0
- package/dist/lib/concurrency.js +33 -0
- package/dist/lib/daemon.d.ts +57 -0
- package/dist/lib/daemon.js +192 -16
- package/dist/lib/devices/registry.d.ts +7 -0
- package/dist/lib/devices/registry.js +24 -0
- package/dist/lib/devices/tailscale.js +1 -1
- package/dist/lib/drift.d.ts +52 -0
- package/dist/lib/drift.js +112 -0
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +31 -13
- package/dist/lib/exec.d.ts +17 -0
- package/dist/lib/exec.js +79 -13
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +56 -1
- package/dist/lib/hooks/cache.d.ts +6 -0
- package/dist/lib/hooks/cache.js +54 -12
- package/dist/lib/hooks.d.ts +27 -0
- package/dist/lib/hooks.js +127 -8
- package/dist/lib/hosts/dispatch.d.ts +15 -0
- package/dist/lib/hosts/dispatch.js +39 -6
- package/dist/lib/hosts/logs.js +30 -1
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/passthrough.js +3 -1
- package/dist/lib/hosts/ready.d.ts +29 -6
- package/dist/lib/hosts/ready.js +66 -15
- package/dist/lib/hosts/registry.d.ts +19 -2
- package/dist/lib/hosts/registry.js +58 -2
- package/dist/lib/hosts/remote-cmd.d.ts +62 -1
- package/dist/lib/hosts/remote-cmd.js +70 -1
- package/dist/lib/hosts/remote-os.d.ts +17 -0
- package/dist/lib/hosts/remote-os.js +30 -0
- package/dist/lib/hosts/session-index.d.ts +34 -0
- package/dist/lib/hosts/session-index.js +56 -0
- package/dist/lib/hosts/tasks.d.ts +14 -0
- package/dist/lib/hosts/tasks.js +15 -0
- package/dist/lib/lock.d.ts +93 -0
- package/dist/lib/lock.js +207 -0
- package/dist/lib/loop.js +16 -1
- package/dist/lib/machine-id.d.ts +21 -0
- package/dist/lib/machine-id.js +26 -0
- package/dist/lib/mailbox-target.d.ts +36 -0
- package/dist/lib/mailbox-target.js +45 -0
- package/dist/lib/mailbox.d.ts +47 -0
- package/dist/lib/mailbox.js +194 -0
- package/dist/lib/mcp.d.ts +5 -0
- package/dist/lib/mcp.js +24 -8
- package/dist/lib/migrate.d.ts +19 -0
- package/dist/lib/migrate.js +134 -26
- package/dist/lib/overdue.js +3 -0
- package/dist/lib/picker.d.ts +2 -0
- package/dist/lib/picker.js +4 -1
- package/dist/lib/platform/exec.d.ts +46 -0
- package/dist/lib/platform/exec.js +74 -0
- package/dist/lib/platform/process.d.ts +31 -0
- package/dist/lib/platform/process.js +34 -1
- package/dist/lib/platform/winpath.js +2 -0
- package/dist/lib/plugins.js +16 -6
- package/dist/lib/profiles.d.ts +25 -0
- package/dist/lib/profiles.js +22 -6
- package/dist/lib/pty-client.js +2 -1
- package/dist/lib/rotate.d.ts +61 -0
- package/dist/lib/rotate.js +52 -0
- package/dist/lib/routines.d.ts +40 -2
- package/dist/lib/routines.js +66 -8
- package/dist/lib/runner.d.ts +11 -2
- package/dist/lib/runner.js +49 -7
- package/dist/lib/scheduler.js +6 -1
- package/dist/lib/secrets/bundles.d.ts +60 -4
- package/dist/lib/secrets/bundles.js +131 -12
- package/dist/lib/secrets/filestore.d.ts +3 -0
- package/dist/lib/secrets/filestore.js +42 -16
- package/dist/lib/secrets/index.d.ts +43 -2
- package/dist/lib/secrets/index.js +102 -3
- package/dist/lib/secrets/mcp.d.ts +93 -0
- package/dist/lib/secrets/mcp.js +205 -0
- package/dist/lib/secrets/remote.js +12 -5
- package/dist/lib/secrets/sync.js +83 -4
- package/dist/lib/secrets/windows.js +14 -3
- package/dist/lib/serve/data.d.ts +81 -0
- package/dist/lib/serve/data.js +91 -0
- package/dist/lib/serve/page.d.ts +7 -0
- package/dist/lib/serve/page.js +140 -0
- package/dist/lib/serve/server.d.ts +46 -0
- package/dist/lib/serve/server.js +115 -0
- package/dist/lib/session/active.d.ts +54 -0
- package/dist/lib/session/active.js +190 -19
- package/dist/lib/session/discover.d.ts +37 -0
- package/dist/lib/session/discover.js +111 -28
- package/dist/lib/session/inject.d.ts +18 -0
- package/dist/lib/session/inject.js +21 -0
- package/dist/lib/session/parse.js +23 -20
- package/dist/lib/session/pid-registry.d.ts +1 -0
- package/dist/lib/session/pid-registry.js +24 -0
- package/dist/lib/session/provenance.d.ts +14 -2
- package/dist/lib/session/provenance.js +39 -8
- package/dist/lib/session/remote-active.js +19 -7
- package/dist/lib/session/remote-list.d.ts +51 -0
- package/dist/lib/session/remote-list.js +213 -0
- package/dist/lib/session/remote.d.ts +7 -1
- package/dist/lib/session/remote.js +16 -2
- package/dist/lib/session/sync/config.d.ts +1 -15
- package/dist/lib/session/sync/config.js +4 -20
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/shims.d.ts +36 -6
- package/dist/lib/shims.js +91 -29
- package/dist/lib/ssh-exec.js +2 -0
- package/dist/lib/ssh-tunnel.js +2 -1
- package/dist/lib/startup/command-registry.d.ts +6 -0
- package/dist/lib/startup/command-registry.js +13 -1
- package/dist/lib/state.d.ts +13 -0
- package/dist/lib/state.js +103 -9
- package/dist/lib/sync-umbrella.d.ts +14 -7
- package/dist/lib/sync-umbrella.js +17 -9
- package/dist/lib/teams/forEach.d.ts +110 -0
- package/dist/lib/teams/forEach.js +186 -0
- package/dist/lib/teams/index.d.ts +1 -0
- package/dist/lib/teams/index.js +1 -0
- package/dist/lib/teams/pr-watch.d.ts +226 -0
- package/dist/lib/teams/pr-watch.js +371 -0
- package/dist/lib/teams/supervisor.d.ts +14 -1
- package/dist/lib/teams/supervisor.js +19 -0
- package/dist/lib/teams/worktree.d.ts +9 -0
- package/dist/lib/teams/worktree.js +32 -0
- package/dist/lib/terminal/backends/index.d.ts +2 -1
- package/dist/lib/terminal/backends/index.js +3 -1
- package/dist/lib/terminal/backends/vscodium-agent.d.ts +31 -0
- package/dist/lib/terminal/backends/vscodium-agent.js +72 -0
- package/dist/lib/terminal/index.d.ts +4 -1
- package/dist/lib/terminal/index.js +4 -1
- package/dist/lib/terminal/inject.d.ts +204 -0
- package/dist/lib/terminal/inject.js +247 -0
- package/dist/lib/terminal/resolve.d.ts +64 -0
- package/dist/lib/terminal/resolve.js +90 -0
- package/dist/lib/terminal/types.d.ts +1 -1
- package/dist/lib/triggers/webhook.d.ts +85 -0
- package/dist/lib/triggers/webhook.js +141 -0
- package/dist/lib/versions.d.ts +23 -0
- package/dist/lib/versions.js +119 -13
- package/dist/lib/watchdog/index.d.ts +3 -0
- package/dist/lib/watchdog/index.js +5 -0
- package/dist/lib/watchdog/read.d.ts +35 -0
- package/dist/lib/watchdog/read.js +149 -0
- package/dist/lib/watchdog/runner.d.ts +127 -0
- package/dist/lib/watchdog/runner.js +322 -0
- package/dist/lib/watchdog/watchdog.d.ts +40 -0
- package/dist/lib/watchdog/watchdog.js +166 -0
- package/dist/lib/watchdog/watchdogTail.d.ts +5 -0
- package/dist/lib/watchdog/watchdogTail.js +154 -0
- package/dist/lib/workflows.d.ts +166 -0
- package/dist/lib/workflows.js +193 -0
- package/package.json +5 -4
package/dist/lib/profiles.d.ts
CHANGED
|
@@ -22,6 +22,14 @@ export interface Profile {
|
|
|
22
22
|
description?: string;
|
|
23
23
|
preset?: string;
|
|
24
24
|
provider?: string;
|
|
25
|
+
/**
|
|
26
|
+
* Optional secondary model retried on the same host when the primary model
|
|
27
|
+
* env value hits a rate limit. Reuses the `--fallback` cascade in
|
|
28
|
+
* `runWithFallback` (src/lib/exec.ts) — the swap is expressed as an
|
|
29
|
+
* envOverride on a same-agent FallbackEntry, so only the model env var
|
|
30
|
+
* changes; auth, base URL, and every other profile env value are preserved.
|
|
31
|
+
*/
|
|
32
|
+
fallback_model?: string;
|
|
25
33
|
}
|
|
26
34
|
/**
|
|
27
35
|
* Stable, machine-readable summary used by `agents view` and `--json`.
|
|
@@ -59,6 +67,13 @@ export declare function profileHostLabel(profile: Profile): string;
|
|
|
59
67
|
export declare function profileProviderLabel(profile: Profile): string;
|
|
60
68
|
/** Return the configured model env value for display. */
|
|
61
69
|
export declare function profileModelLabel(profile: Profile): string;
|
|
70
|
+
/**
|
|
71
|
+
* Return the env var key that carries this profile's model (e.g.
|
|
72
|
+
* `ANTHROPIC_MODEL`), or null when the profile has no recognizable model env.
|
|
73
|
+
* `fallback_model` swaps THIS key so provider selection, auth, and base URL
|
|
74
|
+
* are all preserved on retry.
|
|
75
|
+
*/
|
|
76
|
+
export declare function profileModelEnvKey(profile: Profile): string | null;
|
|
62
77
|
/**
|
|
63
78
|
* Build a non-secret auth identity/status label for list surfaces.
|
|
64
79
|
*
|
|
@@ -89,6 +104,16 @@ export interface ResolvedProfileRun {
|
|
|
89
104
|
version?: string;
|
|
90
105
|
env: Record<string, string>;
|
|
91
106
|
profileName: string;
|
|
107
|
+
/**
|
|
108
|
+
* Same-host model swap for the `--fallback` cascade. Present only when the
|
|
109
|
+
* profile declares `fallback_model` AND the profile has an identifiable
|
|
110
|
+
* model env key to swap. `envKey` names the var (e.g. `ANTHROPIC_MODEL`),
|
|
111
|
+
* `model` is the value to write on the retry attempt.
|
|
112
|
+
*/
|
|
113
|
+
fallbackModel?: {
|
|
114
|
+
envKey: string;
|
|
115
|
+
model: string;
|
|
116
|
+
};
|
|
92
117
|
}
|
|
93
118
|
/**
|
|
94
119
|
* Resolve a name into (agent, version, env). Throws if the name is not a
|
package/dist/lib/profiles.js
CHANGED
|
@@ -111,16 +111,25 @@ const MODEL_ENV_KEYS = [
|
|
|
111
111
|
];
|
|
112
112
|
/** Return the configured model env value for display. */
|
|
113
113
|
export function profileModelLabel(profile) {
|
|
114
|
+
const key = profileModelEnvKey(profile);
|
|
115
|
+
return key ? profile.env[key] : '-';
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Return the env var key that carries this profile's model (e.g.
|
|
119
|
+
* `ANTHROPIC_MODEL`), or null when the profile has no recognizable model env.
|
|
120
|
+
* `fallback_model` swaps THIS key so provider selection, auth, and base URL
|
|
121
|
+
* are all preserved on retry.
|
|
122
|
+
*/
|
|
123
|
+
export function profileModelEnvKey(profile) {
|
|
114
124
|
for (const key of MODEL_ENV_KEYS) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
return value;
|
|
125
|
+
if (profile.env[key])
|
|
126
|
+
return key;
|
|
118
127
|
}
|
|
119
128
|
for (const [key, value] of Object.entries(profile.env)) {
|
|
120
129
|
if ((key === 'MODEL' || key.endsWith('_MODEL')) && value)
|
|
121
|
-
return
|
|
130
|
+
return key;
|
|
122
131
|
}
|
|
123
|
-
return
|
|
132
|
+
return null;
|
|
124
133
|
}
|
|
125
134
|
function decodeJwtPayload(token) {
|
|
126
135
|
const parts = token.split('.');
|
|
@@ -239,12 +248,19 @@ export function resolveProfileEnv(profile) {
|
|
|
239
248
|
*/
|
|
240
249
|
export function resolveProfileForRun(name) {
|
|
241
250
|
const profile = readProfile(name);
|
|
242
|
-
|
|
251
|
+
const resolved = {
|
|
243
252
|
agent: profile.host.agent,
|
|
244
253
|
version: profile.host.version,
|
|
245
254
|
env: resolveProfileEnv(profile),
|
|
246
255
|
profileName: profile.name,
|
|
247
256
|
};
|
|
257
|
+
if (profile.fallback_model) {
|
|
258
|
+
const envKey = profileModelEnvKey(profile);
|
|
259
|
+
if (envKey) {
|
|
260
|
+
resolved.fallbackModel = { envKey, model: profile.fallback_model };
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return resolved;
|
|
248
264
|
}
|
|
249
265
|
/**
|
|
250
266
|
* Look up the preset a profile was created from, if any. Used by
|
package/dist/lib/pty-client.js
CHANGED
|
@@ -10,6 +10,7 @@ import { spawn, execSync } from 'child_process';
|
|
|
10
10
|
import { fileURLToPath } from 'url';
|
|
11
11
|
import * as path from 'path';
|
|
12
12
|
import { getSocketPath, getPtyLogPath, isPtyServerRunning } from './pty-server.js';
|
|
13
|
+
import { backgroundSpawnOptions } from './platform/process.js';
|
|
13
14
|
const CONNECT_TIMEOUT_MS = 5000;
|
|
14
15
|
const RESPONSE_TIMEOUT_MS = 30000;
|
|
15
16
|
const IS_WINDOWS = process.platform === 'win32';
|
|
@@ -38,7 +39,7 @@ async function ensureServer() {
|
|
|
38
39
|
const logFd = fs.openSync(logPath, 'a');
|
|
39
40
|
const child = spawn(bin, args, {
|
|
40
41
|
stdio: ['ignore', logFd, logFd],
|
|
41
|
-
|
|
42
|
+
...backgroundSpawnOptions({ fdStdio: true }),
|
|
42
43
|
});
|
|
43
44
|
child.unref();
|
|
44
45
|
fs.closeSync(logFd);
|
package/dist/lib/rotate.d.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* authentication tokens so users maintain active sessions across version switches.
|
|
6
6
|
*/
|
|
7
7
|
import type { AgentId, RunStrategy } from './types.js';
|
|
8
|
+
import type { FallbackEntry } from './exec.js';
|
|
8
9
|
import { type AccountInfo } from './agents.js';
|
|
9
10
|
import { type UsageSnapshot } from './usage.js';
|
|
10
11
|
export interface RotateCandidate {
|
|
@@ -101,3 +102,63 @@ export declare function resolveRunVersion(agent: AgentId, strategy: RunStrategy,
|
|
|
101
102
|
version: string | null;
|
|
102
103
|
rotation: RotateResult | null;
|
|
103
104
|
}>;
|
|
105
|
+
/**
|
|
106
|
+
* Cap on the number of healthy accounts a single run will re-dispatch through
|
|
107
|
+
* after a mid-run rate limit. Bounds the synthesized chain so a machine signed
|
|
108
|
+
* into many accounts can't turn one 429 into an unbounded cascade of retries.
|
|
109
|
+
*/
|
|
110
|
+
export declare const DEFAULT_ROTATION_FAILOVER_LIMIT = 3;
|
|
111
|
+
/**
|
|
112
|
+
* Synthesize a same-agent, cross-account fallback chain from a pre-flight
|
|
113
|
+
* rotation result (issue #348: mid-run rate-limit failover).
|
|
114
|
+
*
|
|
115
|
+
* The account rotation picks ONE version pre-spawn; today a 429 mid-run kills
|
|
116
|
+
* the run with no recovery. `runWithFallback` + `detectRateLimit` already
|
|
117
|
+
* re-dispatch to the NEXT chain entry on a rate limit and hand off the session
|
|
118
|
+
* via `/continue <id>` — but only for explicit `--fallback` chains. This turns
|
|
119
|
+
* the OTHER healthy rotation candidates (every account except the one already
|
|
120
|
+
* picked as the primary) into `FallbackEntry`s so that SAME machinery re-runs
|
|
121
|
+
* the task on the next healthy account of the same agent when the primary 429s.
|
|
122
|
+
*
|
|
123
|
+
* Each account is a distinct installed version (its own home/auth), so the
|
|
124
|
+
* entries are same-agent, different-version — exactly what runWithFallback
|
|
125
|
+
* spawns and what buildFallbackPrompt continues (claude→claude via `/continue`).
|
|
126
|
+
* Candidates are consumed in `rotation.healthy` order, which is sorted by
|
|
127
|
+
* remaining capacity (most headroom first, see compareCandidates), so failover
|
|
128
|
+
* prefers the freshest account.
|
|
129
|
+
*
|
|
130
|
+
* Returns `[]` when there is no rotation (pinned strategy) or the picked account
|
|
131
|
+
* is the only healthy one — so single-account users and non-rotation runs are
|
|
132
|
+
* completely unchanged.
|
|
133
|
+
*/
|
|
134
|
+
export declare function rotationFailoverChain(rotation: RotateResult | null, pickedVersion: string, limit?: number): FallbackEntry[];
|
|
135
|
+
/**
|
|
136
|
+
* Whether a run is eligible to have a mid-run rate-limit failover chain armed
|
|
137
|
+
* (issue #348). Failover injects synthesized `FallbackEntry`s into the same
|
|
138
|
+
* `fallback` array that `--fallback` uses — so it must NOT arm for run shapes
|
|
139
|
+
* that reject a non-empty fallback chain, or the run hard-exits on a flag the
|
|
140
|
+
* user never passed. Specifically:
|
|
141
|
+
*
|
|
142
|
+
* - `acp` and `loop` runs bail with "not compatible with --fallback yet" the
|
|
143
|
+
* moment `fallback.length > 0` (src/commands/exec.ts), so arming failover
|
|
144
|
+
* would break a previously-working `agents run … --loop` / `--acp`.
|
|
145
|
+
* - `resumeCheckpoint` runs take the loop path (same guard).
|
|
146
|
+
* - `interactive` / no-prompt runs can't be re-dispatched headlessly.
|
|
147
|
+
* - `explicitFallback` (a user `--fallback` chain OR a profile fallback already
|
|
148
|
+
* unshifted) defines its own recovery; don't layer rotation failover on top.
|
|
149
|
+
* - `hasRotation`/`hasVersion` gate on an actual pre-flight rotation having
|
|
150
|
+
* picked an account, so pinned and non-rotation runs are untouched.
|
|
151
|
+
*
|
|
152
|
+
* Pure so the arming matrix is unit-testable without invoking the run command.
|
|
153
|
+
*/
|
|
154
|
+
export interface FailoverArmingContext {
|
|
155
|
+
hasRotation: boolean;
|
|
156
|
+
hasVersion: boolean;
|
|
157
|
+
hasPrompt: boolean;
|
|
158
|
+
explicitFallback: boolean;
|
|
159
|
+
interactive: boolean;
|
|
160
|
+
acp: boolean;
|
|
161
|
+
loop: boolean;
|
|
162
|
+
resumeCheckpoint: boolean;
|
|
163
|
+
}
|
|
164
|
+
export declare function shouldArmRotationFailover(ctx: FailoverArmingContext): boolean;
|
package/dist/lib/rotate.js
CHANGED
|
@@ -341,3 +341,55 @@ export async function resolveRunVersion(agent, strategy, cwd = process.cwd()) {
|
|
|
341
341
|
}
|
|
342
342
|
return { version: fallback, rotation: null };
|
|
343
343
|
}
|
|
344
|
+
/**
|
|
345
|
+
* Cap on the number of healthy accounts a single run will re-dispatch through
|
|
346
|
+
* after a mid-run rate limit. Bounds the synthesized chain so a machine signed
|
|
347
|
+
* into many accounts can't turn one 429 into an unbounded cascade of retries.
|
|
348
|
+
*/
|
|
349
|
+
export const DEFAULT_ROTATION_FAILOVER_LIMIT = 3;
|
|
350
|
+
/**
|
|
351
|
+
* Synthesize a same-agent, cross-account fallback chain from a pre-flight
|
|
352
|
+
* rotation result (issue #348: mid-run rate-limit failover).
|
|
353
|
+
*
|
|
354
|
+
* The account rotation picks ONE version pre-spawn; today a 429 mid-run kills
|
|
355
|
+
* the run with no recovery. `runWithFallback` + `detectRateLimit` already
|
|
356
|
+
* re-dispatch to the NEXT chain entry on a rate limit and hand off the session
|
|
357
|
+
* via `/continue <id>` — but only for explicit `--fallback` chains. This turns
|
|
358
|
+
* the OTHER healthy rotation candidates (every account except the one already
|
|
359
|
+
* picked as the primary) into `FallbackEntry`s so that SAME machinery re-runs
|
|
360
|
+
* the task on the next healthy account of the same agent when the primary 429s.
|
|
361
|
+
*
|
|
362
|
+
* Each account is a distinct installed version (its own home/auth), so the
|
|
363
|
+
* entries are same-agent, different-version — exactly what runWithFallback
|
|
364
|
+
* spawns and what buildFallbackPrompt continues (claude→claude via `/continue`).
|
|
365
|
+
* Candidates are consumed in `rotation.healthy` order, which is sorted by
|
|
366
|
+
* remaining capacity (most headroom first, see compareCandidates), so failover
|
|
367
|
+
* prefers the freshest account.
|
|
368
|
+
*
|
|
369
|
+
* Returns `[]` when there is no rotation (pinned strategy) or the picked account
|
|
370
|
+
* is the only healthy one — so single-account users and non-rotation runs are
|
|
371
|
+
* completely unchanged.
|
|
372
|
+
*/
|
|
373
|
+
export function rotationFailoverChain(rotation, pickedVersion, limit = DEFAULT_ROTATION_FAILOVER_LIMIT) {
|
|
374
|
+
if (!rotation || limit <= 0)
|
|
375
|
+
return [];
|
|
376
|
+
const chain = [];
|
|
377
|
+
for (const candidate of rotation.healthy) {
|
|
378
|
+
if (candidate.version === pickedVersion)
|
|
379
|
+
continue; // the primary account
|
|
380
|
+
chain.push({ agent: candidate.agent, version: candidate.version });
|
|
381
|
+
if (chain.length >= limit)
|
|
382
|
+
break;
|
|
383
|
+
}
|
|
384
|
+
return chain;
|
|
385
|
+
}
|
|
386
|
+
export function shouldArmRotationFailover(ctx) {
|
|
387
|
+
return (ctx.hasRotation &&
|
|
388
|
+
ctx.hasVersion &&
|
|
389
|
+
ctx.hasPrompt &&
|
|
390
|
+
!ctx.explicitFallback &&
|
|
391
|
+
!ctx.interactive &&
|
|
392
|
+
!ctx.acp &&
|
|
393
|
+
!ctx.loop &&
|
|
394
|
+
!ctx.resumeCheckpoint);
|
|
395
|
+
}
|
package/dist/lib/routines.d.ts
CHANGED
|
@@ -7,16 +7,50 @@
|
|
|
7
7
|
* scheduling.
|
|
8
8
|
*/
|
|
9
9
|
import type { AgentId } from './types.js';
|
|
10
|
+
import type { LoopConfig } from './loop.js';
|
|
10
11
|
/** Tool/site/directory allow-list for sandboxed job execution. */
|
|
11
12
|
export interface JobAllowConfig {
|
|
12
13
|
tools?: string[];
|
|
13
14
|
sites?: string[];
|
|
14
15
|
dirs?: string[];
|
|
15
16
|
}
|
|
16
|
-
/**
|
|
17
|
+
/** GitHub webhook events a routine can be triggered by. */
|
|
18
|
+
export type GithubTriggerEvent = 'pull_request' | 'push' | 'issue_comment' | 'workflow_run';
|
|
19
|
+
/** Canonical set of accepted GitHub trigger events — single source for validation. */
|
|
20
|
+
export declare const GITHUB_TRIGGER_EVENTS: readonly GithubTriggerEvent[];
|
|
21
|
+
/**
|
|
22
|
+
* Map a user-facing `--on` alias to a canonical GitHub trigger event.
|
|
23
|
+
* Accepts the canonical names plus friendly shortcuts (e.g. `pr`, `pr_opened`
|
|
24
|
+
* → `pull_request`, `comment` → `issue_comment`). Returns null when unknown.
|
|
25
|
+
*/
|
|
26
|
+
export declare function normalizeTriggerEvent(input: string): GithubTriggerEvent | null;
|
|
27
|
+
/**
|
|
28
|
+
* Event-based fire condition for a routine — an alternative (or complement) to
|
|
29
|
+
* `schedule`. Currently only `github_event`: an incoming GitHub webhook whose
|
|
30
|
+
* event (and optional repo/branch) match fires the job through the same
|
|
31
|
+
* dispatch path a cron fire uses. See `src/lib/triggers/webhook.ts`.
|
|
32
|
+
*/
|
|
33
|
+
export interface JobTrigger {
|
|
34
|
+
type: 'github_event';
|
|
35
|
+
event: GithubTriggerEvent;
|
|
36
|
+
/** `owner/name` — when set, only payloads for this repo match. */
|
|
37
|
+
repo?: string;
|
|
38
|
+
/** git branch (ref short name) — when set, only payloads for this branch match. */
|
|
39
|
+
branch?: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Full configuration for a routine (persisted as YAML).
|
|
43
|
+
*
|
|
44
|
+
* A job fires on a `schedule` (cron), on a `trigger` (event/webhook), or both.
|
|
45
|
+
* `schedule` remains a first-class field; trigger-only jobs omit it and are
|
|
46
|
+
* skipped by the cron scheduler (they fire only via the webhook receiver).
|
|
47
|
+
*/
|
|
17
48
|
export interface JobConfig {
|
|
18
49
|
name: string;
|
|
19
|
-
|
|
50
|
+
/** Cron expression. Optional when `trigger` is set (event-only routine). */
|
|
51
|
+
schedule?: string;
|
|
52
|
+
/** Event/webhook fire condition. Optional when `schedule` is set. */
|
|
53
|
+
trigger?: JobTrigger;
|
|
20
54
|
agent: AgentId;
|
|
21
55
|
workflow?: string;
|
|
22
56
|
mode: 'plan' | 'edit' | 'auto' | 'skip' | 'full';
|
|
@@ -33,6 +67,8 @@ export interface JobConfig {
|
|
|
33
67
|
version?: string;
|
|
34
68
|
runOnce?: boolean;
|
|
35
69
|
endAt?: string;
|
|
70
|
+
/** When set, executeJob runs this job through the loop driver instead of once. */
|
|
71
|
+
loop?: LoopConfig;
|
|
36
72
|
}
|
|
37
73
|
/** Metadata for a single job execution, persisted as JSON in the run directory. */
|
|
38
74
|
export interface RunMeta {
|
|
@@ -67,6 +103,8 @@ export declare function deleteJob(name: string): boolean;
|
|
|
67
103
|
export declare function setJobEnabled(name: string, enabled: boolean): void;
|
|
68
104
|
/** Validate a partial job config, returning a list of human-readable errors. */
|
|
69
105
|
export declare function validateJob(config: Partial<JobConfig>): string[];
|
|
106
|
+
/** Validate a job trigger block, returning a list of human-readable errors. */
|
|
107
|
+
export declare function validateTrigger(trigger: unknown): string[];
|
|
70
108
|
/** True when a job's endAt has already elapsed. False when endAt is unset or in the future. */
|
|
71
109
|
export declare function isPastEndAt(config: Pick<JobConfig, 'endAt'>, now?: Date): boolean;
|
|
72
110
|
/** Expand built-in and user-defined template variables in a job's prompt string. */
|
package/dist/lib/routines.js
CHANGED
|
@@ -13,6 +13,33 @@ import { Cron } from 'croner';
|
|
|
13
13
|
import { getRoutinesDir, getRunsDir, ensureAgentsDir, getProjectRoutinesDir } from './state.js';
|
|
14
14
|
import { safeJoin } from './paths.js';
|
|
15
15
|
import { ALL_AGENT_IDS } from './agents.js';
|
|
16
|
+
/** Canonical set of accepted GitHub trigger events — single source for validation. */
|
|
17
|
+
export const GITHUB_TRIGGER_EVENTS = [
|
|
18
|
+
'pull_request',
|
|
19
|
+
'push',
|
|
20
|
+
'issue_comment',
|
|
21
|
+
'workflow_run',
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Map a user-facing `--on` alias to a canonical GitHub trigger event.
|
|
25
|
+
* Accepts the canonical names plus friendly shortcuts (e.g. `pr`, `pr_opened`
|
|
26
|
+
* → `pull_request`, `comment` → `issue_comment`). Returns null when unknown.
|
|
27
|
+
*/
|
|
28
|
+
export function normalizeTriggerEvent(input) {
|
|
29
|
+
const key = input.trim().toLowerCase();
|
|
30
|
+
const aliases = {
|
|
31
|
+
pull_request: 'pull_request',
|
|
32
|
+
pr: 'pull_request',
|
|
33
|
+
pr_opened: 'pull_request',
|
|
34
|
+
pull: 'pull_request',
|
|
35
|
+
push: 'push',
|
|
36
|
+
issue_comment: 'issue_comment',
|
|
37
|
+
comment: 'issue_comment',
|
|
38
|
+
workflow_run: 'workflow_run',
|
|
39
|
+
workflow: 'workflow_run',
|
|
40
|
+
};
|
|
41
|
+
return aliases[key] ?? null;
|
|
42
|
+
}
|
|
16
43
|
/** Default values applied to every job config when fields are omitted. */
|
|
17
44
|
const JOB_DEFAULTS = {
|
|
18
45
|
mode: 'plan',
|
|
@@ -137,18 +164,28 @@ export function validateJob(config) {
|
|
|
137
164
|
if (!config.name || typeof config.name !== 'string') {
|
|
138
165
|
errors.push('name is required');
|
|
139
166
|
}
|
|
140
|
-
|
|
141
|
-
|
|
167
|
+
const hasSchedule = Boolean(config.schedule && typeof config.schedule === 'string');
|
|
168
|
+
const hasTrigger = config.trigger !== undefined;
|
|
169
|
+
if (!hasSchedule && !hasTrigger) {
|
|
170
|
+
errors.push('schedule (cron expression) or trigger is required');
|
|
142
171
|
}
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
new Cron(config.schedule);
|
|
172
|
+
if (config.schedule !== undefined) {
|
|
173
|
+
if (typeof config.schedule !== 'string') {
|
|
174
|
+
errors.push('schedule must be a cron expression string');
|
|
147
175
|
}
|
|
148
|
-
|
|
149
|
-
|
|
176
|
+
else {
|
|
177
|
+
// Validate cron expression is parseable
|
|
178
|
+
try {
|
|
179
|
+
new Cron(config.schedule);
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
errors.push(`invalid cron expression: "${config.schedule}"`);
|
|
183
|
+
}
|
|
150
184
|
}
|
|
151
185
|
}
|
|
186
|
+
if (config.trigger !== undefined) {
|
|
187
|
+
errors.push(...validateTrigger(config.trigger));
|
|
188
|
+
}
|
|
152
189
|
const hasAgent = Boolean(config.agent && typeof config.agent === 'string');
|
|
153
190
|
const hasWorkflow = Boolean(config.workflow && typeof config.workflow === 'string');
|
|
154
191
|
if (!hasAgent && !hasWorkflow) {
|
|
@@ -184,6 +221,27 @@ export function validateJob(config) {
|
|
|
184
221
|
}
|
|
185
222
|
return errors;
|
|
186
223
|
}
|
|
224
|
+
/** Validate a job trigger block, returning a list of human-readable errors. */
|
|
225
|
+
export function validateTrigger(trigger) {
|
|
226
|
+
const errors = [];
|
|
227
|
+
if (!trigger || typeof trigger !== 'object') {
|
|
228
|
+
return ['trigger must be an object'];
|
|
229
|
+
}
|
|
230
|
+
const t = trigger;
|
|
231
|
+
if (t.type !== 'github_event') {
|
|
232
|
+
errors.push("trigger.type must be 'github_event'");
|
|
233
|
+
}
|
|
234
|
+
if (!t.event || !GITHUB_TRIGGER_EVENTS.includes(t.event)) {
|
|
235
|
+
errors.push(`trigger.event must be one of: ${GITHUB_TRIGGER_EVENTS.join(', ')}`);
|
|
236
|
+
}
|
|
237
|
+
if (t.repo !== undefined && (typeof t.repo !== 'string' || !/^[^/\s]+\/[^/\s]+$/.test(t.repo))) {
|
|
238
|
+
errors.push('trigger.repo must be in owner/name form');
|
|
239
|
+
}
|
|
240
|
+
if (t.branch !== undefined && typeof t.branch !== 'string') {
|
|
241
|
+
errors.push('trigger.branch must be a string');
|
|
242
|
+
}
|
|
243
|
+
return errors;
|
|
244
|
+
}
|
|
187
245
|
function isParseableDate(value) {
|
|
188
246
|
if (!value.trim())
|
|
189
247
|
return false;
|
package/dist/lib/runner.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { JobConfig, RunMeta } from './routines.js';
|
|
10
10
|
import type { AgentId } from './types.js';
|
|
11
|
+
import type { LoopDeps } from './loop.js';
|
|
11
12
|
/** Result of a completed job execution, including metadata and optional report. */
|
|
12
13
|
export interface RunResult {
|
|
13
14
|
meta: RunMeta;
|
|
@@ -15,8 +16,16 @@ export interface RunResult {
|
|
|
15
16
|
}
|
|
16
17
|
/** Build the full CLI argv for executing a job, applying mode, model, and permission flags. */
|
|
17
18
|
export declare function buildJobCommand(config: JobConfig, resolvedPrompt: string): string[];
|
|
18
|
-
/**
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Execute a job synchronously (waits for completion or timeout before resolving).
|
|
21
|
+
*
|
|
22
|
+
* When `config.loop` is set the job is routed through the loop driver (`runLoop`
|
|
23
|
+
* from loop.ts) instead of a single spawn — same driver as `agents run --loop` and
|
|
24
|
+
* workflow `loop:` blocks (issue #400). The optional `deps` parameter provides
|
|
25
|
+
* injectable seams (runIteration, sleep, writeCheckpoint) used by tests; production
|
|
26
|
+
* callers omit it and get the defaults.
|
|
27
|
+
*/
|
|
28
|
+
export declare function executeJob(config: JobConfig, deps?: LoopDeps): Promise<RunResult>;
|
|
20
29
|
/** Spawn a job as a detached process and return immediately with run metadata. */
|
|
21
30
|
export declare function executeJobDetached(config: JobConfig): Promise<RunMeta>;
|
|
22
31
|
/** Extract the final assistant message from a stream-JSON log file as a markdown report. */
|
package/dist/lib/runner.js
CHANGED
|
@@ -16,6 +16,7 @@ import { prepareJobHome, buildSpawnEnv } from './sandbox.js';
|
|
|
16
16
|
import { resolveModel, buildReasoningFlags } from './models.js';
|
|
17
17
|
import { createTimer, maybeRotate, redactPrompt } from './events.js';
|
|
18
18
|
import { normalizeMode } from './exec.js';
|
|
19
|
+
import { backgroundSpawnOptions } from './platform/process.js';
|
|
19
20
|
/** CLI command templates per agent, with {prompt} as a placeholder. */
|
|
20
21
|
const AGENT_COMMANDS = {
|
|
21
22
|
claude: ['claude', '-p', '--verbose', '{prompt}', '--output-format', 'stream-json', '--permission-mode', 'plan'],
|
|
@@ -152,8 +153,16 @@ function appendModelAndReasoning(cmd, config) {
|
|
|
152
153
|
function generateRunId() {
|
|
153
154
|
return new Date().toISOString().replace(/[:.]/g, '-');
|
|
154
155
|
}
|
|
155
|
-
/**
|
|
156
|
-
|
|
156
|
+
/**
|
|
157
|
+
* Execute a job synchronously (waits for completion or timeout before resolving).
|
|
158
|
+
*
|
|
159
|
+
* When `config.loop` is set the job is routed through the loop driver (`runLoop`
|
|
160
|
+
* from loop.ts) instead of a single spawn — same driver as `agents run --loop` and
|
|
161
|
+
* workflow `loop:` blocks (issue #400). The optional `deps` parameter provides
|
|
162
|
+
* injectable seams (runIteration, sleep, writeCheckpoint) used by tests; production
|
|
163
|
+
* callers omit it and get the defaults.
|
|
164
|
+
*/
|
|
165
|
+
export async function executeJob(config, deps) {
|
|
157
166
|
maybeRotate();
|
|
158
167
|
const timer = createTimer('agent.run', {
|
|
159
168
|
agent: config.agent,
|
|
@@ -164,14 +173,11 @@ export async function executeJob(config) {
|
|
|
164
173
|
schedule: config.schedule,
|
|
165
174
|
});
|
|
166
175
|
const resolvedPrompt = resolveJobPrompt(config);
|
|
167
|
-
const cmd = buildJobCommand(config, resolvedPrompt);
|
|
168
176
|
const useSandbox = config.sandbox !== false;
|
|
169
177
|
const overlayHome = useSandbox ? prepareJobHome(config) : undefined;
|
|
170
178
|
const runId = generateRunId();
|
|
171
179
|
const runDir = getRunDir(config.name, runId);
|
|
172
180
|
fs.mkdirSync(runDir, { recursive: true });
|
|
173
|
-
const stdoutPath = path.join(runDir, 'stdout.log');
|
|
174
|
-
const stdoutFd = fs.openSync(stdoutPath, 'w', 0o600);
|
|
175
181
|
let spawnEnv = useSandbox ? buildSpawnEnv(overlayHome) : { ...process.env };
|
|
176
182
|
if (config.timezone) {
|
|
177
183
|
spawnEnv.TZ = config.timezone;
|
|
@@ -192,10 +198,46 @@ export async function executeJob(config) {
|
|
|
192
198
|
};
|
|
193
199
|
writeRunMeta(meta);
|
|
194
200
|
const timeoutMs = parseTimeout(config.timeout) || 10 * 60 * 1000;
|
|
201
|
+
// Loop path: delegate to runLoop (same driver as `agents run --loop` / workflow loop:).
|
|
202
|
+
if (config.loop) {
|
|
203
|
+
const execOptions = {
|
|
204
|
+
agent: effectiveAgent,
|
|
205
|
+
version: config.version,
|
|
206
|
+
prompt: resolvedPrompt,
|
|
207
|
+
mode: normalizeMode(config.mode),
|
|
208
|
+
effort: config.effort,
|
|
209
|
+
env: spawnEnv,
|
|
210
|
+
json: true,
|
|
211
|
+
headless: true,
|
|
212
|
+
...(config.config?.model ? { model: config.config.model } : {}),
|
|
213
|
+
...(config.allow?.dirs ? {
|
|
214
|
+
addDirs: config.allow.dirs
|
|
215
|
+
.filter((d) => !d.startsWith('-'))
|
|
216
|
+
.map((d) => d.replace(/^~/, os.homedir())),
|
|
217
|
+
} : {}),
|
|
218
|
+
};
|
|
219
|
+
const { runLoop } = await import('./loop.js');
|
|
220
|
+
const loopResult = await runLoop(execOptions, config.loop, {
|
|
221
|
+
runId,
|
|
222
|
+
runDir,
|
|
223
|
+
agent: effectiveAgent,
|
|
224
|
+
version: config.version,
|
|
225
|
+
}, deps);
|
|
226
|
+
meta.status = loopResult.stoppedBy === 'error' ? 'failed' : 'completed';
|
|
227
|
+
meta.completedAt = new Date().toISOString();
|
|
228
|
+
meta.exitCode = loopResult.stoppedBy === 'error' ? 1 : 0;
|
|
229
|
+
writeRunMeta(meta);
|
|
230
|
+
timer.end({ status: meta.status, exitCode: meta.exitCode ?? undefined, runId });
|
|
231
|
+
return { meta, reportPath: null };
|
|
232
|
+
}
|
|
233
|
+
// Single-shot path (no loop): build the command, open a log file, and spawn once.
|
|
234
|
+
const cmd = buildJobCommand(config, resolvedPrompt);
|
|
235
|
+
const stdoutPath = path.join(runDir, 'stdout.log');
|
|
236
|
+
const stdoutFd = fs.openSync(stdoutPath, 'w', 0o600);
|
|
195
237
|
return new Promise((resolve) => {
|
|
196
238
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
197
239
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
198
|
-
|
|
240
|
+
...backgroundSpawnOptions({ fdStdio: true }),
|
|
199
241
|
env: spawnEnv,
|
|
200
242
|
});
|
|
201
243
|
// Mark startup time (time from function call to process spawn)
|
|
@@ -290,7 +332,7 @@ export async function executeJobDetached(config) {
|
|
|
290
332
|
};
|
|
291
333
|
const child = spawn(cmd[0], cmd.slice(1), {
|
|
292
334
|
stdio: ['ignore', stdoutFd, stdoutFd],
|
|
293
|
-
|
|
335
|
+
...backgroundSpawnOptions({ fdStdio: true }),
|
|
294
336
|
env: spawnEnv,
|
|
295
337
|
});
|
|
296
338
|
child.unref();
|
package/dist/lib/scheduler.js
CHANGED
|
@@ -17,12 +17,17 @@ export class JobScheduler {
|
|
|
17
17
|
loadAll() {
|
|
18
18
|
const configs = listJobs();
|
|
19
19
|
for (const config of configs) {
|
|
20
|
-
|
|
20
|
+
// Trigger-only jobs (no cron schedule) fire via the webhook receiver,
|
|
21
|
+
// not the cron loop — skip them here.
|
|
22
|
+
if (config.enabled && config.schedule) {
|
|
21
23
|
this.schedule(config);
|
|
22
24
|
}
|
|
23
25
|
}
|
|
24
26
|
}
|
|
25
27
|
schedule(config) {
|
|
28
|
+
// A schedule-less (trigger-only) job has nothing to hand to croner.
|
|
29
|
+
if (!config.schedule)
|
|
30
|
+
return;
|
|
26
31
|
this.unschedule(config.name);
|
|
27
32
|
// catch: true — a throw from one job's callback should not kill the
|
|
28
33
|
// whole cron loop. Each invocation of onTrigger is already wrapped in
|
|
@@ -48,13 +48,20 @@ export interface VarMeta {
|
|
|
48
48
|
* - `always`: asks every time. Never auto-held — only an explicit `agents
|
|
49
49
|
* secrets unlock` ever holds it; every other read pops Touch ID. Opt a
|
|
50
50
|
* high-value bundle into this when you want to confirm every single read.
|
|
51
|
+
* - `never`: stored WITHOUT the biometry access control — reads are fully
|
|
52
|
+
* silent (no Touch ID, no broker). The least-safe tier: any code running as
|
|
53
|
+
* the user reads it with no user-presence check. Reserved for low-sensitivity,
|
|
54
|
+
* automation-only credentials. Writing a `never` item needs the signed helper's
|
|
55
|
+
* `set-no-acl` path (see keychain-helper.swift); an older pinned helper rejects
|
|
56
|
+
* it loudly rather than silently downgrading to `always`.
|
|
51
57
|
*
|
|
52
58
|
* The default is configurable via `secrets.policy` in agents.yaml. Stored on disk
|
|
53
59
|
* under the legacy `tier` key (`session` == `daily`, `biometry` == explicit
|
|
54
|
-
* `always`, absent == inherit the default) so bundles stay
|
|
55
|
-
* CLI versions on synced machines. The user-facing
|
|
60
|
+
* `always`, `none` == `never`, absent == inherit the default) so bundles stay
|
|
61
|
+
* readable across mixed CLI versions on synced machines. The user-facing
|
|
62
|
+
* vocabulary is `policy`/`always`/`daily`/`never`.
|
|
56
63
|
*/
|
|
57
|
-
export type SecretsPolicy = 'always' | 'daily';
|
|
64
|
+
export type SecretsPolicy = 'always' | 'daily' | 'never';
|
|
58
65
|
/** A named set of environment variable definitions backed by various secret providers. */
|
|
59
66
|
export interface SecretsBundle {
|
|
60
67
|
name: string;
|
|
@@ -131,7 +138,54 @@ export interface ResolveBundleOptions {
|
|
|
131
138
|
* needs live values. Also honored via AGENTS_SECRETS_NO_AGENT=1.
|
|
132
139
|
*/
|
|
133
140
|
noAgent?: boolean;
|
|
141
|
+
/**
|
|
142
|
+
* Inject only this subset of keys from the bundle. Keys not in this list are
|
|
143
|
+
* silently excluded from the returned env map. An error is thrown if any
|
|
144
|
+
* requested key is absent from the bundle (fail-loud, never silent skip).
|
|
145
|
+
* When absent or empty, all keys are injected (original behaviour).
|
|
146
|
+
*/
|
|
147
|
+
keys?: string[];
|
|
148
|
+
/**
|
|
149
|
+
* When true, skip the pre-run expiry check and inject keys even if their
|
|
150
|
+
* `expires` date is in the past. By default any expired key (or a key whose
|
|
151
|
+
* bundle-level expiry has passed) aborts the run before Touch ID is popped.
|
|
152
|
+
*/
|
|
153
|
+
allowExpired?: boolean;
|
|
134
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Apply the --keys subset + expiry gate to an already-resolved snapshot from
|
|
157
|
+
* the secrets-agent fast-path. The agent stores the FULL bundle env, so a
|
|
158
|
+
* naive fast-path return would silently defeat --keys and inject expired
|
|
159
|
+
* values. Mirrors the slow-path pre-checks in `resolveBundleEnv` /
|
|
160
|
+
* `readAndResolveBundleEnv` and returns a new env whose keys match the subset.
|
|
161
|
+
*
|
|
162
|
+
* Exported for tests; production callers reach it via the fast-path branch in
|
|
163
|
+
* `readAndResolveBundleEnv`.
|
|
164
|
+
*/
|
|
165
|
+
export declare function filterAgentHitBySubsetAndExpiry(hit: {
|
|
166
|
+
bundle: SecretsBundle;
|
|
167
|
+
env: Record<string, string>;
|
|
168
|
+
}, opts: ResolveBundleOptions): {
|
|
169
|
+
bundle: SecretsBundle;
|
|
170
|
+
env: Record<string, string>;
|
|
171
|
+
};
|
|
172
|
+
/**
|
|
173
|
+
* Guard for remote-bundle callers (`bundle@host` / `--host`) — the SSH
|
|
174
|
+
* resolver in `remoteResolveEnv` does not thread --keys or --allow-expired
|
|
175
|
+
* yet. Silently applying them would inject the full remote env or an expired
|
|
176
|
+
* value, defeating the least-privilege intent, so we fail loud.
|
|
177
|
+
*
|
|
178
|
+
* Exported so `agents run --secrets bundle@host` and `agents secrets exec
|
|
179
|
+
* --host` share the exact same error text; the tests exercise this helper
|
|
180
|
+
* directly instead of driving the whole CLI.
|
|
181
|
+
*/
|
|
182
|
+
export declare function assertRemoteBundleFlagsUnsupported(bundleName: string, host: string, opts: {
|
|
183
|
+
keys?: string[];
|
|
184
|
+
allowExpired?: boolean;
|
|
185
|
+
}, flagLabels: {
|
|
186
|
+
keysFlag: string;
|
|
187
|
+
allowExpiredFlag: string;
|
|
188
|
+
}): void;
|
|
135
189
|
export declare function resolveBundleEnv(bundle: SecretsBundle, _opts?: ResolveBundleOptions): Record<string, string>;
|
|
136
190
|
/**
|
|
137
191
|
* Read a bundle's metadata AND resolve its env in a single Touch ID prompt.
|
|
@@ -193,7 +247,9 @@ export declare function renameBundle(oldName: string, newName: string, opts?: Re
|
|
|
193
247
|
* `import` / `remove` / `delete`. Pass the bundle's resolved backend
|
|
194
248
|
* (`bundle.backend ?? 'keychain'`).
|
|
195
249
|
*/
|
|
196
|
-
export declare function bundleItemStore(backend: SecretsBackend | undefined
|
|
250
|
+
export declare function bundleItemStore(backend: SecretsBackend | undefined, opts?: {
|
|
251
|
+
noAcl?: boolean;
|
|
252
|
+
}): {
|
|
197
253
|
set(item: string, value: string): void;
|
|
198
254
|
delete(item: string): boolean;
|
|
199
255
|
get(item: string): string;
|