@phnx-labs/agents-cli 1.20.64 → 1.20.65
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 +39 -3
- package/README.md +37 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +94 -49
- package/dist/commands/feed.js +25 -11
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +23 -2
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.js +8 -3
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +36 -6
- package/dist/commands/ssh.js +45 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +26 -0
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +34 -14
- package/dist/lib/agents.d.ts +18 -0
- package/dist/lib/agents.js +53 -3
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.js +5 -1
- package/dist/lib/crabbox/runtimes.js +11 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +62 -19
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +16 -0
- package/dist/lib/routines.js +19 -0
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +102 -9
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +14 -0
- package/dist/lib/session/bundle.js +12 -1
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +86 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +4 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +26 -1
- package/dist/lib/usage.js +7 -5
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Login/token propagation — the "one login instead of ~48 OAuth flows" core of
|
|
3
|
+
* `agents apply`.
|
|
4
|
+
*
|
|
5
|
+
* A source machine that is already signed in to a set of harnesses has portable
|
|
6
|
+
* credential files on disk (verified per-agent locations below). `snapshotAuth`
|
|
7
|
+
* captures those; `materializeAuth` writes them into the corresponding paths on
|
|
8
|
+
* a target device. Transport is the caller's job — `apply` streams the bundle
|
|
9
|
+
* over the existing (encrypted, authenticated) SSH channel via `sshExec`'s
|
|
10
|
+
* stdin `input`, so no app-layer crypto is layered on top of SSH here.
|
|
11
|
+
*
|
|
12
|
+
* Honest boundary: on macOS, claude and antigravity keep their tokens in the
|
|
13
|
+
* login keychain, ACL-bound to the harness process — unreadable by us. Those are
|
|
14
|
+
* classified `bound` and surfaced for a one-time manual login, never faked.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
/**
|
|
19
|
+
* Verified portable auth-file locations per agent (home-relative). Sourced from
|
|
20
|
+
* live inspection of a Linux fleet box + the agent registry. Agents absent here
|
|
21
|
+
* have no portable credential file we can propagate.
|
|
22
|
+
*/
|
|
23
|
+
export const FLEET_AUTH_FILES = {
|
|
24
|
+
claude: [{ rel: '.claude/.credentials.json', mode: 0o600 }],
|
|
25
|
+
codex: [{ rel: '.codex/auth.json', mode: 0o600 }],
|
|
26
|
+
gemini: [{ rel: '.gemini/oauth_creds.json', mode: 0o600 }],
|
|
27
|
+
grok: [{ rel: '.grok/auth.json', mode: 0o600 }],
|
|
28
|
+
kimi: [{ rel: '.kimi-code/credentials/kimi-code.json', mode: 0o600 }],
|
|
29
|
+
opencode: [{ rel: '.local/share/opencode/auth.json', mode: 0o600 }],
|
|
30
|
+
droid: [
|
|
31
|
+
{ rel: '.factory/auth.v2.file', mode: 0o600 },
|
|
32
|
+
{ rel: '.factory/auth.v2.key', mode: 0o600 },
|
|
33
|
+
],
|
|
34
|
+
antigravity: [{ rel: '.gemini/antigravity-cli/antigravity-oauth-token', mode: 0o600 }],
|
|
35
|
+
};
|
|
36
|
+
/** Agents whose macOS credentials live in the ACL-bound login keychain. */
|
|
37
|
+
export const KEYCHAIN_BOUND_ON_MAC = new Set(['claude', 'antigravity']);
|
|
38
|
+
/** Which agents `apply` can propagate auth for at all. */
|
|
39
|
+
export function isPropagatableAgent(agent) {
|
|
40
|
+
return agent in FLEET_AUTH_FILES;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Capture portable credential files for the given agents from a source home.
|
|
44
|
+
* Returns the readable file payloads plus the list of agents whose auth is
|
|
45
|
+
* device-bound (macOS keychain) and therefore cannot be captured. Agents that
|
|
46
|
+
* are simply not signed in (no file on disk) are silently omitted — nothing to
|
|
47
|
+
* propagate, not an error.
|
|
48
|
+
*/
|
|
49
|
+
export function snapshotAuth(agents, opts) {
|
|
50
|
+
const files = [];
|
|
51
|
+
const bound = [];
|
|
52
|
+
for (const agent of agents) {
|
|
53
|
+
const specs = FLEET_AUTH_FILES[agent];
|
|
54
|
+
if (!specs)
|
|
55
|
+
continue; // not propagatable — caller surfaces separately if desired
|
|
56
|
+
if (opts.platform === 'darwin' && KEYCHAIN_BOUND_ON_MAC.has(agent)) {
|
|
57
|
+
bound.push(agent);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
for (const spec of specs) {
|
|
61
|
+
const abs = path.join(opts.home, spec.rel);
|
|
62
|
+
let stat;
|
|
63
|
+
try {
|
|
64
|
+
stat = fs.statSync(abs);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
continue; // not signed in for this agent — nothing to carry
|
|
68
|
+
}
|
|
69
|
+
if (!stat.isFile())
|
|
70
|
+
continue;
|
|
71
|
+
const content = fs.readFileSync(abs); // follows symlinks into version homes
|
|
72
|
+
files.push({
|
|
73
|
+
agent,
|
|
74
|
+
rel: spec.rel,
|
|
75
|
+
contentB64: content.toString('base64'),
|
|
76
|
+
mode: (stat.mode & 0o777) || spec.mode,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { files, bound };
|
|
81
|
+
}
|
|
82
|
+
/** Assemble the plaintext bundle shipped to a target. */
|
|
83
|
+
export function buildAuthBundle(source, files) {
|
|
84
|
+
return { v: 1, source, files };
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Write a captured auth bundle into a target home. Parent dirs are created; the
|
|
88
|
+
* captured POSIX mode is restored (0600 for credentials). Writing through an
|
|
89
|
+
* existing symlink lands in the agent's active version home, which is exactly
|
|
90
|
+
* what per-version credential carry-forward expects.
|
|
91
|
+
*/
|
|
92
|
+
export function materializeAuth(bundle, opts) {
|
|
93
|
+
const written = new Set();
|
|
94
|
+
const errors = [];
|
|
95
|
+
for (const f of bundle.files) {
|
|
96
|
+
const abs = path.join(opts.home, f.rel);
|
|
97
|
+
try {
|
|
98
|
+
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
99
|
+
fs.writeFileSync(abs, Buffer.from(f.contentB64, 'base64'));
|
|
100
|
+
fs.chmodSync(abs, f.mode || 0o600);
|
|
101
|
+
written.add(f.agent);
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
errors.push(`${f.rel}: ${e.message}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return { written: [...written], errors };
|
|
108
|
+
}
|
|
109
|
+
/** Parse + validate a bundle received on stdin (the remote `--_recv-auth` path). */
|
|
110
|
+
export function parseAuthBundle(raw) {
|
|
111
|
+
let doc;
|
|
112
|
+
try {
|
|
113
|
+
doc = JSON.parse(raw);
|
|
114
|
+
}
|
|
115
|
+
catch (e) {
|
|
116
|
+
throw new Error(`auth bundle is not valid JSON: ${e.message}`);
|
|
117
|
+
}
|
|
118
|
+
if (typeof doc !== 'object' || doc === null)
|
|
119
|
+
throw new Error('auth bundle must be an object.');
|
|
120
|
+
const o = doc;
|
|
121
|
+
if (o.v !== 1)
|
|
122
|
+
throw new Error(`unsupported auth bundle version ${JSON.stringify(o.v)}.`);
|
|
123
|
+
if (typeof o.source !== 'string')
|
|
124
|
+
throw new Error('auth bundle missing source.');
|
|
125
|
+
if (!Array.isArray(o.files))
|
|
126
|
+
throw new Error('auth bundle missing files[].');
|
|
127
|
+
for (const f of o.files) {
|
|
128
|
+
if (typeof f !== 'object' || f === null ||
|
|
129
|
+
typeof f.agent !== 'string' ||
|
|
130
|
+
typeof f.rel !== 'string' ||
|
|
131
|
+
typeof f.contentB64 !== 'string' ||
|
|
132
|
+
typeof f.mode !== 'number') {
|
|
133
|
+
throw new Error('auth bundle has a malformed file entry.');
|
|
134
|
+
}
|
|
135
|
+
// Reject path traversal — rel must stay under $HOME.
|
|
136
|
+
const rel = f.rel;
|
|
137
|
+
if (rel.startsWith('/') || rel.split('/').includes('..')) {
|
|
138
|
+
throw new Error(`auth bundle rejected unsafe path: ${rel}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return doc;
|
|
142
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + validate the `fleet:` block of a profile manifest and resolve it into
|
|
3
|
+
* per-device desired state.
|
|
4
|
+
*
|
|
5
|
+
* `fleet:` is additive to the `Meta` schema (`types.ts`). Any `-f <file>` that
|
|
6
|
+
* carries a `fleet:` block is a valid manifest, so `ag apply -f agents.yaml`
|
|
7
|
+
* works against the project file. The functions here are pure (no SSH, no
|
|
8
|
+
* registry I/O) so they're fully unit-testable; device enumeration is injected.
|
|
9
|
+
*/
|
|
10
|
+
import type { FleetManifest, DeviceDesired } from './types.js';
|
|
11
|
+
/** Validate a raw `fleet:` object (already YAML-parsed) into a typed manifest. */
|
|
12
|
+
export declare function parseFleetManifest(raw: unknown): FleetManifest;
|
|
13
|
+
/** Read a YAML file and extract + validate its `fleet:` block. */
|
|
14
|
+
export declare function readFleetFile(filePath: string): FleetManifest;
|
|
15
|
+
export interface ResolveContext {
|
|
16
|
+
/** Device names currently online (used to expand `devices: all`). */
|
|
17
|
+
onlineDevices: string[];
|
|
18
|
+
/** All registered device names (used to validate explicit entries). */
|
|
19
|
+
registeredDevices: string[];
|
|
20
|
+
/** The source machine, always excluded from the target set. */
|
|
21
|
+
source: string;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Expand a manifest into concrete per-device desired states. `devices: all`
|
|
25
|
+
* expands to every online registered device minus the source; an explicit map
|
|
26
|
+
* validates each name against the registry. Devices with no desired agents,
|
|
27
|
+
* sync scopes, and `login: skip` are still returned (probe/report only).
|
|
28
|
+
*/
|
|
29
|
+
export declare function resolveDesired(manifest: FleetManifest, ctx: ResolveContext): DeviceDesired[];
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse + validate the `fleet:` block of a profile manifest and resolve it into
|
|
3
|
+
* per-device desired state.
|
|
4
|
+
*
|
|
5
|
+
* `fleet:` is additive to the `Meta` schema (`types.ts`). Any `-f <file>` that
|
|
6
|
+
* carries a `fleet:` block is a valid manifest, so `ag apply -f agents.yaml`
|
|
7
|
+
* works against the project file. The functions here are pure (no SSH, no
|
|
8
|
+
* registry I/O) so they're fully unit-testable; device enumeration is injected.
|
|
9
|
+
*/
|
|
10
|
+
import * as fs from 'fs';
|
|
11
|
+
import * as yaml from 'yaml';
|
|
12
|
+
const LOGIN_MODES = ['sync', 'skip'];
|
|
13
|
+
function isStringArray(v) {
|
|
14
|
+
return Array.isArray(v) && v.every((x) => typeof x === 'string');
|
|
15
|
+
}
|
|
16
|
+
function validateLogin(v, where) {
|
|
17
|
+
if (v === undefined)
|
|
18
|
+
return undefined;
|
|
19
|
+
if (typeof v !== 'string' || !LOGIN_MODES.includes(v)) {
|
|
20
|
+
throw new Error(`fleet: ${where}.login must be one of ${LOGIN_MODES.join(' | ')} (got ${JSON.stringify(v)}).`);
|
|
21
|
+
}
|
|
22
|
+
return v;
|
|
23
|
+
}
|
|
24
|
+
function validateDefaults(v, where) {
|
|
25
|
+
if (v === undefined)
|
|
26
|
+
return {};
|
|
27
|
+
if (typeof v !== 'object' || v === null || Array.isArray(v)) {
|
|
28
|
+
throw new Error(`fleet: ${where} must be a mapping.`);
|
|
29
|
+
}
|
|
30
|
+
const o = v;
|
|
31
|
+
if (o.agents !== undefined && !isStringArray(o.agents)) {
|
|
32
|
+
throw new Error(`fleet: ${where}.agents must be a list of agent specs (e.g. [claude@latest]).`);
|
|
33
|
+
}
|
|
34
|
+
if (o.sync !== undefined && !isStringArray(o.sync)) {
|
|
35
|
+
throw new Error(`fleet: ${where}.sync must be a list of scope names (e.g. [user]).`);
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
agents: o.agents,
|
|
39
|
+
sync: o.sync,
|
|
40
|
+
login: validateLogin(o.login, where),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/** Validate a raw `fleet:` object (already YAML-parsed) into a typed manifest. */
|
|
44
|
+
export function parseFleetManifest(raw) {
|
|
45
|
+
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
46
|
+
throw new Error('fleet: block must be a mapping with a `devices:` key.');
|
|
47
|
+
}
|
|
48
|
+
const o = raw;
|
|
49
|
+
const defaults = validateDefaults(o.defaults, 'defaults');
|
|
50
|
+
if (o.devices === undefined) {
|
|
51
|
+
throw new Error('fleet: a `devices:` key is required (use `devices: all` or an explicit map).');
|
|
52
|
+
}
|
|
53
|
+
let devices;
|
|
54
|
+
if (o.devices === 'all') {
|
|
55
|
+
devices = 'all';
|
|
56
|
+
}
|
|
57
|
+
else if (typeof o.devices === 'object' && o.devices !== null && !Array.isArray(o.devices)) {
|
|
58
|
+
const map = {};
|
|
59
|
+
for (const [name, ov] of Object.entries(o.devices)) {
|
|
60
|
+
// An empty entry (`device: {}`) inherits defaults — represent as {}.
|
|
61
|
+
const merged = ov == null ? {} : validateDefaults(ov, `devices.${name}`);
|
|
62
|
+
map[name] = merged;
|
|
63
|
+
}
|
|
64
|
+
devices = map;
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
throw new Error(`fleet: devices must be the string 'all' or a mapping of device -> overrides (got ${JSON.stringify(o.devices)}).`);
|
|
68
|
+
}
|
|
69
|
+
return { defaults, devices };
|
|
70
|
+
}
|
|
71
|
+
/** Read a YAML file and extract + validate its `fleet:` block. */
|
|
72
|
+
export function readFleetFile(filePath) {
|
|
73
|
+
if (!fs.existsSync(filePath)) {
|
|
74
|
+
throw new Error(`Manifest not found: ${filePath}`);
|
|
75
|
+
}
|
|
76
|
+
let doc;
|
|
77
|
+
try {
|
|
78
|
+
doc = yaml.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
79
|
+
}
|
|
80
|
+
catch (e) {
|
|
81
|
+
throw new Error(`Failed to parse YAML in ${filePath}: ${e.message}`);
|
|
82
|
+
}
|
|
83
|
+
if (typeof doc !== 'object' || doc === null || Array.isArray(doc)) {
|
|
84
|
+
throw new Error(`${filePath} is not a mapping — no fleet: block to apply.`);
|
|
85
|
+
}
|
|
86
|
+
const fleet = doc.fleet;
|
|
87
|
+
if (fleet === undefined) {
|
|
88
|
+
throw new Error(`${filePath} has no fleet: block. Add one to declare the profile (see \`agents apply --help\`).`);
|
|
89
|
+
}
|
|
90
|
+
return parseFleetManifest(fleet);
|
|
91
|
+
}
|
|
92
|
+
/** Merge a per-device override over defaults into a concrete desired state. */
|
|
93
|
+
function mergeDesired(device, defaults, override) {
|
|
94
|
+
return {
|
|
95
|
+
device,
|
|
96
|
+
agents: override.agents ?? defaults.agents ?? [],
|
|
97
|
+
sync: override.sync ?? defaults.sync ?? [],
|
|
98
|
+
login: override.login ?? defaults.login ?? 'sync',
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Expand a manifest into concrete per-device desired states. `devices: all`
|
|
103
|
+
* expands to every online registered device minus the source; an explicit map
|
|
104
|
+
* validates each name against the registry. Devices with no desired agents,
|
|
105
|
+
* sync scopes, and `login: skip` are still returned (probe/report only).
|
|
106
|
+
*/
|
|
107
|
+
export function resolveDesired(manifest, ctx) {
|
|
108
|
+
const defaults = manifest.defaults ?? {};
|
|
109
|
+
const out = [];
|
|
110
|
+
if (manifest.devices === 'all') {
|
|
111
|
+
for (const name of ctx.onlineDevices) {
|
|
112
|
+
if (name === ctx.source)
|
|
113
|
+
continue;
|
|
114
|
+
out.push(mergeDesired(name, defaults, {}));
|
|
115
|
+
}
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
for (const [name, override] of Object.entries(manifest.devices)) {
|
|
119
|
+
if (name === ctx.source)
|
|
120
|
+
continue;
|
|
121
|
+
if (!ctx.registeredDevices.includes(name)) {
|
|
122
|
+
throw new Error(`fleet: device '${name}' is not a registered device. Run \`agents devices add ${name}\` or fix the manifest.`);
|
|
123
|
+
}
|
|
124
|
+
out.push(mergeDesired(name, defaults, override));
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the fleet profile-sync feature (`agents apply` / `ag apply`).
|
|
3
|
+
*
|
|
4
|
+
* `agents.yaml` gains an additive `fleet:` block that declares a *profile*: which
|
|
5
|
+
* agents every device should have installed, which config scopes to reconcile,
|
|
6
|
+
* and whether logins/tokens propagate. `apply` reconciles the live fleet to it.
|
|
7
|
+
*
|
|
8
|
+
* These types are the contract shared by the manifest parser (`manifest.ts`),
|
|
9
|
+
* the reconcile engine (`apply.ts`), the auth propagation (`auth-sync.ts`), and
|
|
10
|
+
* the command (`commands/apply.ts`). Runtime probe/diff shapes live here too so
|
|
11
|
+
* the pure diff can be unit-tested without SSH.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* How login/token state propagates to a device.
|
|
15
|
+
* - `sync` (default): push portable credentials where possible, surface the rest
|
|
16
|
+
* as a manual login.
|
|
17
|
+
* - `skip`: probe/report only; take no login action.
|
|
18
|
+
* (A per-agent interactive `prompt` mode is intentionally not offered yet — it
|
|
19
|
+
* was removed rather than accepted as a silent no-op that behaves like `skip`.)
|
|
20
|
+
*/
|
|
21
|
+
export type FleetLoginMode = 'sync' | 'skip';
|
|
22
|
+
/** Defaults applied to every targeted device unless a per-device entry overrides. */
|
|
23
|
+
export interface FleetDefaults {
|
|
24
|
+
/** Agent specs to ensure installed, e.g. `['claude@latest', 'codex@latest']`. */
|
|
25
|
+
agents?: string[];
|
|
26
|
+
/** Config sync scopes to reconcile on each device, e.g. `['user']`. */
|
|
27
|
+
sync?: string[];
|
|
28
|
+
/** Login propagation strategy. Default `'sync'`. */
|
|
29
|
+
login?: FleetLoginMode;
|
|
30
|
+
}
|
|
31
|
+
/** Per-device override; any omitted field inherits from `defaults`. */
|
|
32
|
+
export interface FleetDeviceOverride {
|
|
33
|
+
agents?: string[];
|
|
34
|
+
sync?: string[];
|
|
35
|
+
login?: FleetLoginMode;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* The `fleet:` block as it appears in `agents.yaml` (or any `-f` file). `devices`
|
|
39
|
+
* is either the literal string `'all'` (every online registered device minus the
|
|
40
|
+
* source machine) or an explicit map of device-name -> override.
|
|
41
|
+
*/
|
|
42
|
+
export interface FleetManifest {
|
|
43
|
+
defaults?: FleetDefaults;
|
|
44
|
+
devices: 'all' | Record<string, FleetDeviceOverride>;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* A device's desired state after merging defaults with its override and
|
|
48
|
+
* expanding `devices: all`. This is what the reconcile engine drives toward.
|
|
49
|
+
*/
|
|
50
|
+
export interface DeviceDesired {
|
|
51
|
+
/** Registered device name (from `agents devices`). */
|
|
52
|
+
device: string;
|
|
53
|
+
/** Resolved agent specs to ensure installed. */
|
|
54
|
+
agents: string[];
|
|
55
|
+
/** Config sync scopes. */
|
|
56
|
+
sync: string[];
|
|
57
|
+
/** Login propagation strategy for this device. */
|
|
58
|
+
login: FleetLoginMode;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What a probe found on one device. Populated from `readyProbe` plus an
|
|
62
|
+
* installed-agents listing; `reachable: false` short-circuits everything else.
|
|
63
|
+
*/
|
|
64
|
+
export interface DeviceProbe {
|
|
65
|
+
device: string;
|
|
66
|
+
reachable: boolean;
|
|
67
|
+
/** Platform of the device (`linux` | `macos` | `windows`), for login classification. */
|
|
68
|
+
platform?: string;
|
|
69
|
+
/** agents-cli version present on the device (undefined if not installed). */
|
|
70
|
+
cliVersion?: string;
|
|
71
|
+
/** Agent ids currently installed on the device. */
|
|
72
|
+
installedAgents: string[];
|
|
73
|
+
/** Reason string when `reachable` is false or the probe partially failed. */
|
|
74
|
+
note?: string;
|
|
75
|
+
}
|
|
76
|
+
/** One planned action against a device, in a single reconcile dimension. */
|
|
77
|
+
export type FleetActionKind = 'install-cli' | 'upgrade-cli' | 'add-agent' | 'sync-config' | 'push-login' | 'needs-login';
|
|
78
|
+
export interface FleetAction {
|
|
79
|
+
device: string;
|
|
80
|
+
kind: FleetActionKind;
|
|
81
|
+
/** Agent id for agent/login actions; undefined for cli/config actions. */
|
|
82
|
+
agent?: string;
|
|
83
|
+
/** Human, one-line description of the action. */
|
|
84
|
+
detail: string;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* The full reconcile plan: per-device desired vs probed, plus the flat list of
|
|
88
|
+
* actions. Pure output of `diffFleet(desired, probes)` — drives both `--plan`
|
|
89
|
+
* rendering and the confirm prompt.
|
|
90
|
+
*/
|
|
91
|
+
export interface FleetPlan {
|
|
92
|
+
devices: DeviceDiff[];
|
|
93
|
+
actions: FleetAction[];
|
|
94
|
+
}
|
|
95
|
+
/** Per-device diff row rendered in the plan matrix. */
|
|
96
|
+
export interface DeviceDiff {
|
|
97
|
+
device: string;
|
|
98
|
+
desired: DeviceDesired;
|
|
99
|
+
probe: DeviceProbe;
|
|
100
|
+
actions: FleetAction[];
|
|
101
|
+
/** Agents that must be logged in on the device but can't be propagated
|
|
102
|
+
* (source token is device-bound, e.g. macOS keychain). Surfaced, not faked. */
|
|
103
|
+
loginBlocked: string[];
|
|
104
|
+
}
|
|
105
|
+
/** A portable auth file captured from a source agent home, ready to propagate. */
|
|
106
|
+
export interface AuthFilePayload {
|
|
107
|
+
/** Agent id this file belongs to. */
|
|
108
|
+
agent: string;
|
|
109
|
+
/** Path relative to the agent's config dir (or $HOME), reconstructed on target. */
|
|
110
|
+
rel: string;
|
|
111
|
+
/** File contents, base64. */
|
|
112
|
+
contentB64: string;
|
|
113
|
+
/** POSIX mode to restore (e.g. 0o600 for credentials). */
|
|
114
|
+
mode: number;
|
|
115
|
+
}
|
|
116
|
+
/** The plaintext we encrypt before shipping auth over the wire. */
|
|
117
|
+
export interface AuthBundle {
|
|
118
|
+
/** Schema version for forward-compat. */
|
|
119
|
+
v: 1;
|
|
120
|
+
/** Source machine name the snapshot was taken on. */
|
|
121
|
+
source: string;
|
|
122
|
+
files: AuthFilePayload[];
|
|
123
|
+
}
|
|
124
|
+
/** Result of classifying one source agent's auth for propagation. */
|
|
125
|
+
export interface AuthSnapshotResult {
|
|
126
|
+
files: AuthFilePayload[];
|
|
127
|
+
/** Agent ids whose auth is device-bound (keychain) and cannot be captured. */
|
|
128
|
+
bound: string[];
|
|
129
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared types for the fleet profile-sync feature (`agents apply` / `ag apply`).
|
|
3
|
+
*
|
|
4
|
+
* `agents.yaml` gains an additive `fleet:` block that declares a *profile*: which
|
|
5
|
+
* agents every device should have installed, which config scopes to reconcile,
|
|
6
|
+
* and whether logins/tokens propagate. `apply` reconciles the live fleet to it.
|
|
7
|
+
*
|
|
8
|
+
* These types are the contract shared by the manifest parser (`manifest.ts`),
|
|
9
|
+
* the reconcile engine (`apply.ts`), the auth propagation (`auth-sync.ts`), and
|
|
10
|
+
* the command (`commands/apply.ts`). Runtime probe/diff shapes live here too so
|
|
11
|
+
* the pure diff can be unit-tested without SSH.
|
|
12
|
+
*/
|
|
13
|
+
export {};
|
package/dist/lib/git.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git operations for the agents-cli system repo and package repositories.
|
|
3
|
+
*
|
|
4
|
+
* Handles cloning, pulling, syncing, and inspecting git repos used by
|
|
5
|
+
* the agents version management and plugin/package system. Includes
|
|
6
|
+
* source parsing for GitHub shorthand, SSH, HTTPS, and local paths.
|
|
7
|
+
*/
|
|
8
|
+
import { SimpleGit } from 'simple-git';
|
|
1
9
|
/**
|
|
2
10
|
* Validate that a clone/pull source uses a safe git transport before it is
|
|
3
11
|
* handed to `git`.
|
|
@@ -16,6 +24,25 @@
|
|
|
16
24
|
* @throws Error if the source uses a disallowed transport.
|
|
17
25
|
*/
|
|
18
26
|
export declare function assertSafeGitTransport(source: string): void;
|
|
27
|
+
/**
|
|
28
|
+
* Validate a branch name before it is passed to `git push` / `git pull`.
|
|
29
|
+
*
|
|
30
|
+
* A name beginning with `-` is parsed by git as a command-line option
|
|
31
|
+
* (e.g. `--mirror`, `--receive-pack=…`), not a ref. Pure string check —
|
|
32
|
+
* identical on every OS, no spawn.
|
|
33
|
+
*
|
|
34
|
+
* @throws Error if the name is empty or would be interpreted as a git option.
|
|
35
|
+
*/
|
|
36
|
+
export declare function assertValidBranchName(branch: string): void;
|
|
37
|
+
/**
|
|
38
|
+
* `git push origin <branch>` with option-injection hardening:
|
|
39
|
+
* 1. {@link assertValidBranchName} rejects leading `-`
|
|
40
|
+
* 2. `--` ends option parsing so a hostile ref cannot be read as a flag
|
|
41
|
+
*
|
|
42
|
+
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
43
|
+
* repo state rather than a hard-coded literal.
|
|
44
|
+
*/
|
|
45
|
+
export declare function pushOrigin(git: SimpleGit, branch: string): Promise<void>;
|
|
19
46
|
/** Parsed representation of a git source string (GitHub, generic URL, or local path). */
|
|
20
47
|
export interface GitSource {
|
|
21
48
|
type: 'github' | 'url' | 'local';
|
package/dist/lib/git.js
CHANGED
|
@@ -51,6 +51,36 @@ export function assertSafeGitTransport(source) {
|
|
|
51
51
|
}
|
|
52
52
|
// No scheme -> SCP-style SSH ("git@host:path") or a local path; both safe.
|
|
53
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Validate a branch name before it is passed to `git push` / `git pull`.
|
|
56
|
+
*
|
|
57
|
+
* A name beginning with `-` is parsed by git as a command-line option
|
|
58
|
+
* (e.g. `--mirror`, `--receive-pack=…`), not a ref. Pure string check —
|
|
59
|
+
* identical on every OS, no spawn.
|
|
60
|
+
*
|
|
61
|
+
* @throws Error if the name is empty or would be interpreted as a git option.
|
|
62
|
+
*/
|
|
63
|
+
export function assertValidBranchName(branch) {
|
|
64
|
+
const b = branch.trim();
|
|
65
|
+
if (!b) {
|
|
66
|
+
throw new Error(`Invalid branch name ${JSON.stringify(branch)}: branch name is empty.`);
|
|
67
|
+
}
|
|
68
|
+
if (b.startsWith('-')) {
|
|
69
|
+
throw new Error(`Invalid branch name ${JSON.stringify(branch)}: a name starting with "-" is interpreted as a git option.`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* `git push origin <branch>` with option-injection hardening:
|
|
74
|
+
* 1. {@link assertValidBranchName} rejects leading `-`
|
|
75
|
+
* 2. `--` ends option parsing so a hostile ref cannot be read as a flag
|
|
76
|
+
*
|
|
77
|
+
* Prefer this over `git.push(remote, branch)` whenever the branch comes from
|
|
78
|
+
* repo state rather than a hard-coded literal.
|
|
79
|
+
*/
|
|
80
|
+
export async function pushOrigin(git, branch) {
|
|
81
|
+
assertValidBranchName(branch);
|
|
82
|
+
await git.raw(['push', '--', 'origin', branch]);
|
|
83
|
+
}
|
|
54
84
|
/**
|
|
55
85
|
* Whether installing a cloned/pulled repo's `.githooks/` is enabled.
|
|
56
86
|
*
|
|
@@ -377,6 +407,7 @@ export async function commitAndPush(repoPath, message) {
|
|
|
377
407
|
const git = simpleGit(repoPath);
|
|
378
408
|
let status = await git.status();
|
|
379
409
|
const branch = status.current || 'main';
|
|
410
|
+
assertValidBranchName(branch);
|
|
380
411
|
let committed = false;
|
|
381
412
|
if (status.files.length > 0) {
|
|
382
413
|
await git.add('-A');
|
|
@@ -402,7 +433,7 @@ export async function commitAndPush(repoPath, message) {
|
|
|
402
433
|
catch {
|
|
403
434
|
/* origin/<branch> may not exist yet (first push) */
|
|
404
435
|
}
|
|
405
|
-
await git
|
|
436
|
+
await pushOrigin(git, branch);
|
|
406
437
|
let after = '';
|
|
407
438
|
try {
|
|
408
439
|
after = (await git.raw(['rev-parse', '--short=8', 'HEAD'])).trim();
|
|
@@ -735,12 +766,13 @@ export async function syncRepoGit(dir, opts) {
|
|
|
735
766
|
};
|
|
736
767
|
}
|
|
737
768
|
const branch = status.current || 'main';
|
|
769
|
+
assertValidBranchName(branch);
|
|
738
770
|
await git.fetch('origin');
|
|
739
771
|
await git.pull('origin', branch, { '--rebase': 'true' });
|
|
740
772
|
installGithooksSymlinks(dir);
|
|
741
773
|
let pushed = false;
|
|
742
774
|
if (opts.push) {
|
|
743
|
-
await git
|
|
775
|
+
await pushOrigin(git, branch);
|
|
744
776
|
pushed = true;
|
|
745
777
|
}
|
|
746
778
|
const log = await git.log({ maxCount: 1 });
|
|
@@ -62,24 +62,37 @@ export interface DispatchOptions {
|
|
|
62
62
|
prompt: string;
|
|
63
63
|
/** Explicit agent version pin (e.g. "2.1.207") to forward as `agent@version`. */
|
|
64
64
|
version?: string;
|
|
65
|
-
/**
|
|
65
|
+
/** Run strategy (e.g. "balanced") — the remote picks among ITS signed-in accounts. */
|
|
66
66
|
strategy?: string;
|
|
67
|
+
balanced?: boolean;
|
|
68
|
+
fallback?: string;
|
|
67
69
|
mode?: string;
|
|
68
70
|
model?: string;
|
|
69
|
-
/** Reasoning effort
|
|
71
|
+
/** Reasoning effort — forwarded unless 'auto' (the remote default). */
|
|
70
72
|
effort?: string;
|
|
71
|
-
/**
|
|
73
|
+
/** `--env k=v` pairs, forwarded verbatim (the remote CLI parses them). */
|
|
74
|
+
env?: string[];
|
|
75
|
+
/** `--add-dir` grants, already made remote-portable. */
|
|
72
76
|
addDir?: string[];
|
|
73
|
-
/**
|
|
77
|
+
/** Agent wall-clock cap, forwarded as `--timeout <duration>` for the REMOTE to enforce. */
|
|
78
|
+
timeout?: string;
|
|
79
|
+
/** Loop family — the loop driver runs on the host. */
|
|
80
|
+
loop?: boolean;
|
|
81
|
+
maxIterations?: string;
|
|
82
|
+
budget?: string;
|
|
83
|
+
until?: string;
|
|
84
|
+
interval?: string;
|
|
85
|
+
/** Remote emits ndjson into its log; the local follow streams it verbatim. */
|
|
74
86
|
json?: boolean;
|
|
75
|
-
/** Show detailed execution logs. */
|
|
76
87
|
verbose?: boolean;
|
|
77
|
-
/**
|
|
78
|
-
timeout?: string;
|
|
79
|
-
/** Skip the interactive budget-confirm prompt. */
|
|
88
|
+
/** Skip the budget-confirm prompt — a detached remote run can't answer one. */
|
|
80
89
|
yes?: boolean;
|
|
81
90
|
/** Route through the Agent Client Protocol. */
|
|
82
91
|
acp?: boolean;
|
|
92
|
+
/** False forwards --no-auto-secrets (workflow secrets resolve on the REMOTE keychain). */
|
|
93
|
+
autoSecrets?: boolean;
|
|
94
|
+
/** Native-CLI passthrough (everything after `--`), appended last. */
|
|
95
|
+
passthroughArgs?: string[];
|
|
83
96
|
remoteCwd?: string;
|
|
84
97
|
/**
|
|
85
98
|
* Force the remote run's NEW session to use this exact id (Claude only, via
|
|
@@ -105,6 +118,10 @@ export interface DispatchOptions {
|
|
|
105
118
|
* session-id / resume flag wiring is unit-testable without an SSH round-trip.
|
|
106
119
|
* `--session-id` and `--resume` are mutually exclusive (the CLI rejects both);
|
|
107
120
|
* resume wins when — defensively — both are set.
|
|
121
|
+
*
|
|
122
|
+
* Every field here is classified 'forward' in RUN_OPTION_FORWARDING
|
|
123
|
+
* (remote-cmd.ts) — keep the two in lockstep; run-forwarding.test.ts asserts
|
|
124
|
+
* the table side.
|
|
108
125
|
*/
|
|
109
126
|
export declare function buildRunForwardedArgs(opts: DispatchOptions): string[];
|
|
110
127
|
export interface InteractiveDispatchOptions {
|
|
@@ -139,6 +156,10 @@ export interface InteractiveDispatchOptions {
|
|
|
139
156
|
raw?: boolean;
|
|
140
157
|
/** Forward `--interactive` to the remote so a prompt-bearing run still starts the TUI. */
|
|
141
158
|
forceInteractive?: boolean;
|
|
159
|
+
/** `--env k=v` pairs, forwarded verbatim (the remote CLI parses them). */
|
|
160
|
+
env?: string[];
|
|
161
|
+
balanced?: boolean;
|
|
162
|
+
fallback?: string;
|
|
142
163
|
/** Copy runtime credentials to the host before the run and shred them after. */
|
|
143
164
|
copyCreds?: HostCredentials;
|
|
144
165
|
}
|