@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
|
@@ -4,7 +4,7 @@ import * as path from 'path';
|
|
|
4
4
|
import * as os from 'os';
|
|
5
5
|
import { getProfileRuntimeDir } from './profiles.js';
|
|
6
6
|
import { discoverBrowserWsUrl, registerPipeTransport } from './cdp.js';
|
|
7
|
-
import { readAndResolveBundleEnv, bundleExists } from '../secrets/bundles.js';
|
|
7
|
+
import { readAndResolveBundleEnv, bundleExists, isHeadlessSecretsContext } from '../secrets/bundles.js';
|
|
8
8
|
import { writeProfileRuntime, readProfileRuntime } from './runtime-state.js';
|
|
9
9
|
// Windows install roots. Resolve from the environment (fall back to the usual
|
|
10
10
|
// defaults) so per-user installs under %LOCALAPPDATA% and 64-bit Program Files
|
|
@@ -260,7 +260,7 @@ isElectron = false) {
|
|
|
260
260
|
let env = { ...process.env };
|
|
261
261
|
if (secrets && bundleExists(secrets)) {
|
|
262
262
|
try {
|
|
263
|
-
const { env: bundleEnv } = readAndResolveBundleEnv(secrets, { caller: 'browser profile' });
|
|
263
|
+
const { env: bundleEnv } = readAndResolveBundleEnv(secrets, { caller: 'browser profile', agentOnly: isHeadlessSecretsContext() });
|
|
264
264
|
env = { ...env, ...bundleEnv };
|
|
265
265
|
}
|
|
266
266
|
catch {
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* back to GEMINI_API_KEY / GOOGLE_API_KEY in the environment.
|
|
21
21
|
*/
|
|
22
22
|
import { resolveDispatchRepos, normalizeProviderStatus } from './types.js';
|
|
23
|
-
import { readAndResolveBundleEnv } from '../secrets/bundles.js';
|
|
23
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../secrets/bundles.js';
|
|
24
24
|
const INTERACTIONS_URL = 'https://generativelanguage.googleapis.com/v1beta/interactions';
|
|
25
25
|
const DEFAULT_MODEL = 'antigravity-preview-05-2026';
|
|
26
26
|
const KEY_NAMES = ['GEMINI_API_KEY', 'GOOGLE_API_KEY'];
|
|
@@ -65,7 +65,7 @@ export class AntigravityCloudProvider {
|
|
|
65
65
|
resolveApiKey() {
|
|
66
66
|
if (this.secretsBundle) {
|
|
67
67
|
try {
|
|
68
|
-
const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:antigravity' });
|
|
68
|
+
const { env } = readAndResolveBundleEnv(this.secretsBundle, { caller: 'cloud:antigravity', agentOnly: isHeadlessSecretsContext() });
|
|
69
69
|
for (const k of KEY_NAMES) {
|
|
70
70
|
if (env[k])
|
|
71
71
|
return env[k];
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host cloud provider — your own machines as a task-execution backend.
|
|
3
|
+
*
|
|
4
|
+
* A thin adapter over the hosts subsystem (lib/hosts/*): `agents cloud run
|
|
5
|
+
* --provider host --host <name>` dispatches through the SAME detached-SSH
|
|
6
|
+
* launch as `agents run --host`, and the resulting task shows up in BOTH
|
|
7
|
+
* `agents cloud ps` and `agents hosts ps` — one store (the host-task sidecars
|
|
8
|
+
* under ~/.agents/.cache/hosts/), two views. No new transport, no relay: SSH
|
|
9
|
+
* is the only wire, exactly like the rest of the hosts design (docs/hosts.md).
|
|
10
|
+
*
|
|
11
|
+
* Status semantics follow reconcile.ts's prime rule: completion is only ever
|
|
12
|
+
* CONFIRMED from the remote `.exit` file — an unreachable host leaves a task
|
|
13
|
+
* `running`, never guessed as failed. The cloud SQLite store row is a cached
|
|
14
|
+
* index (the `cloud list` refresh loop upserts what `status()` returns); the
|
|
15
|
+
* sidecar stays the source of truth. Reachability is memoized per target for
|
|
16
|
+
* the life of the process so a down host costs ONE short timeout, not one per
|
|
17
|
+
* task ("care around the cloud status-refresh path", lib/hosts/tasks.ts).
|
|
18
|
+
*/
|
|
19
|
+
import type { CloudEvent, CloudProvider, CloudTarget, CloudTask, CloudTaskStatus, DispatchOptions, ProviderCapabilities } from './types.js';
|
|
20
|
+
import type { HostTask } from '../hosts/tasks.js';
|
|
21
|
+
/** Project a host-task sidecar into the cloud task shape. */
|
|
22
|
+
export declare function hostTaskToCloudTask(task: HostTask): CloudTask;
|
|
23
|
+
export declare class HostCloudProvider implements CloudProvider {
|
|
24
|
+
readonly id: "host";
|
|
25
|
+
readonly name = "Host (your machines)";
|
|
26
|
+
readonly targetKind: "host";
|
|
27
|
+
/**
|
|
28
|
+
* Reachability memo, per target, for the life of this process. The cloud
|
|
29
|
+
* refresh loop calls `status()` once per active task; without the memo a
|
|
30
|
+
* down host would cost one ~6s ssh timeout PER task instead of one total.
|
|
31
|
+
* CLI processes are short-lived, so staleness is bounded by the invocation.
|
|
32
|
+
*/
|
|
33
|
+
private reachable;
|
|
34
|
+
capabilities(): ProviderCapabilities;
|
|
35
|
+
dispatch(options: DispatchOptions): Promise<CloudTask>;
|
|
36
|
+
status(taskId: string): Promise<CloudTask>;
|
|
37
|
+
list(filter?: {
|
|
38
|
+
status?: CloudTaskStatus;
|
|
39
|
+
}): Promise<CloudTask[]>;
|
|
40
|
+
/**
|
|
41
|
+
* `reconcileTask` with the per-process reachability memo folded in: probe a
|
|
42
|
+
* target at most once per process; a down host leaves its tasks `running`.
|
|
43
|
+
*/
|
|
44
|
+
private reconcileMemoized;
|
|
45
|
+
/**
|
|
46
|
+
* Offset-tail the remote log (the same one-round-trip fetch the `run --host`
|
|
47
|
+
* follow uses) and yield it as `text` events until the `.exit` file lands.
|
|
48
|
+
*/
|
|
49
|
+
stream(taskId: string): AsyncIterable<CloudEvent>;
|
|
50
|
+
cancel(taskId: string): Promise<void>;
|
|
51
|
+
/**
|
|
52
|
+
* Follow-up message = resume the run's session on the same host. Only runs
|
|
53
|
+
* that captured a session id (Claude — the one agent that takes
|
|
54
|
+
* `--session-id`) can be resumed; others get an actionable refusal.
|
|
55
|
+
*/
|
|
56
|
+
message(taskId: string, content: string): Promise<void>;
|
|
57
|
+
/** The unified host pool (enrolled hosts ∪ devices), dispatchable ones only. */
|
|
58
|
+
listTargets(): Promise<CloudTarget[]>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Host cloud provider — your own machines as a task-execution backend.
|
|
3
|
+
*
|
|
4
|
+
* A thin adapter over the hosts subsystem (lib/hosts/*): `agents cloud run
|
|
5
|
+
* --provider host --host <name>` dispatches through the SAME detached-SSH
|
|
6
|
+
* launch as `agents run --host`, and the resulting task shows up in BOTH
|
|
7
|
+
* `agents cloud ps` and `agents hosts ps` — one store (the host-task sidecars
|
|
8
|
+
* under ~/.agents/.cache/hosts/), two views. No new transport, no relay: SSH
|
|
9
|
+
* is the only wire, exactly like the rest of the hosts design (docs/hosts.md).
|
|
10
|
+
*
|
|
11
|
+
* Status semantics follow reconcile.ts's prime rule: completion is only ever
|
|
12
|
+
* CONFIRMED from the remote `.exit` file — an unreachable host leaves a task
|
|
13
|
+
* `running`, never guessed as failed. The cloud SQLite store row is a cached
|
|
14
|
+
* index (the `cloud list` refresh loop upserts what `status()` returns); the
|
|
15
|
+
* sidecar stays the source of truth. Reachability is memoized per target for
|
|
16
|
+
* the life of the process so a down host costs ONE short timeout, not one per
|
|
17
|
+
* task ("care around the cloud status-refresh path", lib/hosts/tasks.ts).
|
|
18
|
+
*/
|
|
19
|
+
import { MissingTargetError, resolveDispatchRepos } from './types.js';
|
|
20
|
+
import { listTasks, loadTask, terminalPatch, updateTask } from '../hosts/tasks.js';
|
|
21
|
+
import { readRemoteExit } from '../hosts/reconcile.js';
|
|
22
|
+
import { sshReachable } from '../ssh-exec.js';
|
|
23
|
+
import { fetchProgress } from '../hosts/progress.js';
|
|
24
|
+
import { dispatchPromptToHost, resolveHostRunTarget } from '../hosts/run-target.js';
|
|
25
|
+
import { listAllHosts } from '../hosts/registry.js';
|
|
26
|
+
import { terminateDispatchedTask } from '../hosts/dispatch.js';
|
|
27
|
+
/** Host-task lifecycle → canonical cloud enum. `unknown` stays `running`
|
|
28
|
+
* (completion is confirmed, never guessed — reconcile.ts's rule). */
|
|
29
|
+
function toCloudStatus(status) {
|
|
30
|
+
switch (status) {
|
|
31
|
+
case 'completed': return 'completed';
|
|
32
|
+
case 'failed': return 'failed';
|
|
33
|
+
case 'running':
|
|
34
|
+
case 'unknown':
|
|
35
|
+
default:
|
|
36
|
+
return 'running';
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
/** Project a host-task sidecar into the cloud task shape. */
|
|
40
|
+
export function hostTaskToCloudTask(task) {
|
|
41
|
+
return {
|
|
42
|
+
id: task.id,
|
|
43
|
+
provider: 'host',
|
|
44
|
+
status: toCloudStatus(task.status),
|
|
45
|
+
agent: task.agent,
|
|
46
|
+
prompt: task.prompt,
|
|
47
|
+
createdAt: task.createdAt,
|
|
48
|
+
updatedAt: task.finishedAt ?? task.createdAt,
|
|
49
|
+
summary: `on ${task.host}${task.name ? ` as "${task.name}"` : ''}`,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export class HostCloudProvider {
|
|
53
|
+
id = 'host';
|
|
54
|
+
name = 'Host (your machines)';
|
|
55
|
+
targetKind = 'host';
|
|
56
|
+
/**
|
|
57
|
+
* Reachability memo, per target, for the life of this process. The cloud
|
|
58
|
+
* refresh loop calls `status()` once per active task; without the memo a
|
|
59
|
+
* down host would cost one ~6s ssh timeout PER task instead of one total.
|
|
60
|
+
* CLI processes are short-lived, so staleness is bounded by the invocation.
|
|
61
|
+
*/
|
|
62
|
+
reachable = new Map();
|
|
63
|
+
capabilities() {
|
|
64
|
+
return {
|
|
65
|
+
available: true, // ssh is the only dependency; per-host reachability is probed at dispatch
|
|
66
|
+
dispatch: true,
|
|
67
|
+
status: true,
|
|
68
|
+
list: true,
|
|
69
|
+
stream: true,
|
|
70
|
+
cancel: true,
|
|
71
|
+
message: true, // gated per task: needs the sessionId only Claude runs carry
|
|
72
|
+
multiRepo: false,
|
|
73
|
+
skills: false,
|
|
74
|
+
images: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
async dispatch(options) {
|
|
78
|
+
const hostName = options.providerOptions?.host;
|
|
79
|
+
if (!hostName) {
|
|
80
|
+
throw new MissingTargetError('host', 'No host given. Pass --host <name> (a registered host, a device, a capability tag, or user@host).', 'List your machines: agents hosts list · register more: agents devices sync / agents hosts add');
|
|
81
|
+
}
|
|
82
|
+
if (resolveDispatchRepos(options).length > 0) {
|
|
83
|
+
throw new Error('--repo has no meaning for --provider host — the run executes in a directory on the machine, not a cloned repo. ' +
|
|
84
|
+
'Pass the working directory via providerOptions.remoteCwd (CLI: --remote-cwd) instead.');
|
|
85
|
+
}
|
|
86
|
+
if (options.branch) {
|
|
87
|
+
throw new Error('--branch has no meaning for --provider host (no clone step). Check out the branch on the host, or use --remote-cwd.');
|
|
88
|
+
}
|
|
89
|
+
// DeviceOffloadUnsupportedError / HostResolutionError propagate — both carry
|
|
90
|
+
// actionable messages the CLI prints verbatim.
|
|
91
|
+
const host = await resolveHostRunTarget(hostName, {
|
|
92
|
+
any: options.providerOptions?.any === true,
|
|
93
|
+
});
|
|
94
|
+
const { task } = await dispatchPromptToHost(host, {
|
|
95
|
+
agent: options.agent ?? 'claude',
|
|
96
|
+
prompt: options.prompt,
|
|
97
|
+
mode: options.providerOptions?.mode,
|
|
98
|
+
model: options.model,
|
|
99
|
+
timeout: options.timeout,
|
|
100
|
+
remoteCwd: options.providerOptions?.remoteCwd,
|
|
101
|
+
name: options.providerOptions?.name,
|
|
102
|
+
follow: false, // the cloud pipeline streams via stream(); never block dispatch
|
|
103
|
+
});
|
|
104
|
+
return hostTaskToCloudTask(task);
|
|
105
|
+
}
|
|
106
|
+
async status(taskId) {
|
|
107
|
+
const task = loadTask(taskId);
|
|
108
|
+
if (!task)
|
|
109
|
+
throw new Error(`Unknown host task: ${taskId}`);
|
|
110
|
+
return hostTaskToCloudTask(this.reconcileMemoized(task));
|
|
111
|
+
}
|
|
112
|
+
async list(filter) {
|
|
113
|
+
const tasks = listTasks().map((t) => hostTaskToCloudTask(this.reconcileMemoized(t)));
|
|
114
|
+
return filter?.status ? tasks.filter((t) => t.status === filter.status) : tasks;
|
|
115
|
+
}
|
|
116
|
+
/**
|
|
117
|
+
* `reconcileTask` with the per-process reachability memo folded in: probe a
|
|
118
|
+
* target at most once per process; a down host leaves its tasks `running`.
|
|
119
|
+
*/
|
|
120
|
+
reconcileMemoized(task) {
|
|
121
|
+
if (task.status !== 'running')
|
|
122
|
+
return task;
|
|
123
|
+
if (!this.reachable.has(task.target)) {
|
|
124
|
+
this.reachable.set(task.target, sshReachable(task.target, 6000));
|
|
125
|
+
}
|
|
126
|
+
if (!this.reachable.get(task.target))
|
|
127
|
+
return task;
|
|
128
|
+
const st = readRemoteExit(task.target, task.remoteExit);
|
|
129
|
+
if (st.state !== 'done')
|
|
130
|
+
return task;
|
|
131
|
+
return updateTask(task.id, terminalPatch(st.code)) ?? task;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Offset-tail the remote log (the same one-round-trip fetch the `run --host`
|
|
135
|
+
* follow uses) and yield it as `text` events until the `.exit` file lands.
|
|
136
|
+
*/
|
|
137
|
+
async *stream(taskId) {
|
|
138
|
+
const task = loadTask(taskId);
|
|
139
|
+
if (!task)
|
|
140
|
+
throw new Error(`Unknown host task: ${taskId}`);
|
|
141
|
+
if (task.status !== 'running') {
|
|
142
|
+
yield { type: 'status', status: toCloudStatus(task.status) };
|
|
143
|
+
yield { type: 'done', status: toCloudStatus(task.status) };
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
let offset = 0;
|
|
147
|
+
const fastPollMs = 1500;
|
|
148
|
+
const maxPollMs = 6000;
|
|
149
|
+
let pollMs = fastPollMs;
|
|
150
|
+
for (;;) {
|
|
151
|
+
const fetched = fetchProgress(task.target, {
|
|
152
|
+
remoteLog: task.remoteLog,
|
|
153
|
+
remoteExit: task.remoteExit,
|
|
154
|
+
taskId: task.id,
|
|
155
|
+
offset,
|
|
156
|
+
});
|
|
157
|
+
if (fetched) {
|
|
158
|
+
if (fetched.logChunk.length > 0) {
|
|
159
|
+
offset += fetched.logChunk.length;
|
|
160
|
+
pollMs = fastPollMs; // output is flowing — snap back to the fast poll
|
|
161
|
+
yield { type: 'text', content: fetched.logChunk.toString('utf8') };
|
|
162
|
+
}
|
|
163
|
+
else {
|
|
164
|
+
pollMs = Math.min(Math.round(pollMs * 1.5), maxPollMs); // idle backoff
|
|
165
|
+
}
|
|
166
|
+
const exit = fetched.exit.trim();
|
|
167
|
+
if (exit !== '') {
|
|
168
|
+
const code = Number.parseInt(exit, 10);
|
|
169
|
+
const finished = updateTask(task.id, terminalPatch(Number.isFinite(code) ? code : 0));
|
|
170
|
+
const status = toCloudStatus(finished?.status ?? (code === 0 ? 'completed' : 'failed'));
|
|
171
|
+
yield { type: 'done', status };
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async cancel(taskId) {
|
|
179
|
+
const task = loadTask(taskId);
|
|
180
|
+
if (!task)
|
|
181
|
+
throw new Error(`Unknown host task: ${taskId}`);
|
|
182
|
+
terminateDispatchedTask(task);
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Follow-up message = resume the run's session on the same host. Only runs
|
|
186
|
+
* that captured a session id (Claude — the one agent that takes
|
|
187
|
+
* `--session-id`) can be resumed; others get an actionable refusal.
|
|
188
|
+
*/
|
|
189
|
+
async message(taskId, content) {
|
|
190
|
+
const task = loadTask(taskId);
|
|
191
|
+
if (!task)
|
|
192
|
+
throw new Error(`Unknown host task: ${taskId}`);
|
|
193
|
+
if (!task.sessionId) {
|
|
194
|
+
throw new Error(`Host task ${taskId} has no session id to resume (only Claude runs capture one). ` +
|
|
195
|
+
`Start a follow-up run instead: agents run ${task.agent} "<prompt>" --host ${task.host}`);
|
|
196
|
+
}
|
|
197
|
+
const host = await resolveHostRunTarget(task.host);
|
|
198
|
+
await dispatchPromptToHost(host, {
|
|
199
|
+
agent: task.agent,
|
|
200
|
+
prompt: content,
|
|
201
|
+
resume: task.sessionId,
|
|
202
|
+
name: task.name,
|
|
203
|
+
follow: false,
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** The unified host pool (enrolled hosts ∪ devices), dispatchable ones only. */
|
|
207
|
+
async listTargets() {
|
|
208
|
+
const hosts = await listAllHosts();
|
|
209
|
+
return hosts
|
|
210
|
+
.filter((h) => h.dispatchable !== false)
|
|
211
|
+
.map((h) => ({
|
|
212
|
+
id: h.name,
|
|
213
|
+
label: [
|
|
214
|
+
h.provider === 'devices' ? 'device' : h.source,
|
|
215
|
+
h.os,
|
|
216
|
+
h.status && h.status !== 'unknown' ? h.status : undefined,
|
|
217
|
+
h.caps?.length ? `caps: ${h.caps.join(',')}` : undefined,
|
|
218
|
+
]
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
.join(' · '),
|
|
221
|
+
kind: 'host',
|
|
222
|
+
}));
|
|
223
|
+
}
|
|
224
|
+
}
|
|
@@ -11,6 +11,7 @@ import { RushCloudProvider } from './rush.js';
|
|
|
11
11
|
import { CodexCloudProvider } from './codex.js';
|
|
12
12
|
import { FactoryCloudProvider } from './factory.js';
|
|
13
13
|
import { AntigravityCloudProvider } from './antigravity.js';
|
|
14
|
+
import { HostCloudProvider } from './host.js';
|
|
14
15
|
import { getUserAgentsDir } from '../state.js';
|
|
15
16
|
import { AGENTS } from '../agents.js';
|
|
16
17
|
const META_FILE = path.join(getUserAgentsDir(), 'agents.yaml');
|
|
@@ -43,6 +44,9 @@ function initProviders() {
|
|
|
43
44
|
providers.set('codex', new CodexCloudProvider(config.providers?.codex));
|
|
44
45
|
providers.set('factory', new FactoryCloudProvider(config.providers?.factory));
|
|
45
46
|
providers.set('antigravity', new AntigravityCloudProvider(config.providers?.antigravity));
|
|
47
|
+
// Your own machines (agents hosts / agents devices) over SSH. No agent
|
|
48
|
+
// auto-routes here — it's always an explicit --provider host / --host choice.
|
|
49
|
+
providers.set('host', new HostCloudProvider());
|
|
46
50
|
}
|
|
47
51
|
/**
|
|
48
52
|
* The cloud provider an agent dispatches to by default. Reads the canonical
|
|
@@ -9,16 +9,18 @@ import type { JobTrigger } from '../routines.js';
|
|
|
9
9
|
/**
|
|
10
10
|
* Identifier for a supported cloud dispatch backend.
|
|
11
11
|
*
|
|
12
|
-
* Each id is one agent's *own* cloud:
|
|
12
|
+
* Each id is one agent's *own* cloud — plus your own machines:
|
|
13
13
|
* - `rush` — Rush Cloud (runs Claude against a GitHub repo → PR)
|
|
14
14
|
* - `codex` — OpenAI Codex Cloud (`codex cloud exec`)
|
|
15
15
|
* - `factory` — Factory Droid Computers (`droid computer ssh` + remote `droid exec`)
|
|
16
16
|
* - `antigravity` — Google Gemini Managed Agents (Interactions API)
|
|
17
|
+
* - `host` — a machine you own (`agents hosts` / `agents devices`), over SSH
|
|
17
18
|
*
|
|
18
19
|
* Agents route to their native cloud automatically (see `cloudProvider` in the
|
|
19
|
-
* agent registry); `--provider` overrides.
|
|
20
|
+
* agent registry); `--provider` overrides. Nothing auto-routes to `host` — it
|
|
21
|
+
* is always an explicit `--provider host` (or `--host <name>`) choice.
|
|
20
22
|
*/
|
|
21
|
-
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity';
|
|
23
|
+
export type CloudProviderId = 'rush' | 'codex' | 'factory' | 'antigravity' | 'host';
|
|
22
24
|
/**
|
|
23
25
|
* Lifecycle state of a cloud-dispatched task.
|
|
24
26
|
*
|
|
@@ -219,7 +221,7 @@ export interface CloudTarget {
|
|
|
219
221
|
kind: TargetKind;
|
|
220
222
|
}
|
|
221
223
|
/** Which dispatch option a provider's pre-provisioned target maps to. */
|
|
222
|
-
export type TargetKind = 'env' | 'computer';
|
|
224
|
+
export type TargetKind = 'env' | 'computer' | 'host';
|
|
223
225
|
/**
|
|
224
226
|
* Thrown by a provider's `dispatch()` when it needs a pre-provisioned target
|
|
225
227
|
* (Codex env / Factory computer) and none was supplied. The CLI catches this
|
package/dist/lib/computer-rpc.js
CHANGED
|
@@ -341,7 +341,9 @@ class TcpClient extends BaseClient {
|
|
|
341
341
|
this.failPending('helper_exited', 'tcp connection closed before reply');
|
|
342
342
|
});
|
|
343
343
|
// Kick off the auth handshake synchronously so its frame (id 1) is the
|
|
344
|
-
// first thing written.
|
|
344
|
+
// first thing written. The Windows daemon now REQUIRES a token (it refuses to
|
|
345
|
+
// start without one); a null token here means no auth frame is sent, the
|
|
346
|
+
// daemon drops the connection, and the caller is told to re-run `setup`.
|
|
345
347
|
this.authReady = token ? this.authenticate(token) : Promise.resolve();
|
|
346
348
|
}
|
|
347
349
|
async authenticate(token) {
|
package/dist/lib/crabbox/cli.js
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* from a secrets bundle when one is configured (see `crabboxEnv`).
|
|
13
13
|
*/
|
|
14
14
|
import { spawn, spawnSync } from 'child_process';
|
|
15
|
-
import { readAndResolveBundleEnv, listBundles, bundleExists } from '../secrets/bundles.js';
|
|
15
|
+
import { readAndResolveBundleEnv, isHeadlessSecretsContext, listBundles, bundleExists } from '../secrets/bundles.js';
|
|
16
16
|
import { readMeta, writeMeta } from '../state.js';
|
|
17
17
|
/** Locate the crabbox binary, or throw an actionable error. */
|
|
18
18
|
export function findCrabbox() {
|
|
@@ -105,6 +105,10 @@ export function crabboxEnv(opts) {
|
|
|
105
105
|
const { env } = readAndResolveBundleEnv(resolved.name, {
|
|
106
106
|
caller: 'agents run --lease (crabbox)',
|
|
107
107
|
keys: resolved.keys,
|
|
108
|
+
// --lease is headless by contract and crabboxEnv is called several times
|
|
109
|
+
// per run (list/wait/spawn/stop) — resolve broker-only so a keychain bundle
|
|
110
|
+
// can't pop repeated unwatched Touch ID sheets mid-lease.
|
|
111
|
+
agentOnly: isHeadlessSecretsContext(),
|
|
108
112
|
});
|
|
109
113
|
return { ...process.env, ...env };
|
|
110
114
|
}
|
|
@@ -158,8 +158,17 @@ export async function resolveClaudeCredentialsBlob(opts) {
|
|
|
158
158
|
if (process.platform === 'darwin') {
|
|
159
159
|
// 1) Bare service — the default native (non-managed) install.
|
|
160
160
|
const bare = tryRead(service(undefined));
|
|
161
|
-
if (bare)
|
|
162
|
-
|
|
161
|
+
if (bare) {
|
|
162
|
+
if (!opts?.preferEmail)
|
|
163
|
+
return bare;
|
|
164
|
+
// preferEmail is set — verify the bare service belongs to the right account
|
|
165
|
+
// before handing it back; on mismatch fall through to managed installs.
|
|
166
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
167
|
+
const bareEmail = await accountEmail(realHome).catch(() => null);
|
|
168
|
+
if (bareEmail === opts.preferEmail)
|
|
169
|
+
return bare;
|
|
170
|
+
// email mismatch — fall through
|
|
171
|
+
}
|
|
163
172
|
// 2) Managed installs — hash-suffixed service keyed to each version home.
|
|
164
173
|
// Prefer the version whose account email matches the copied config.
|
|
165
174
|
let homes;
|
package/dist/lib/daemon.d.ts
CHANGED
|
@@ -81,10 +81,26 @@ export declare function readDaemonClaudeOAuthToken(opts?: {
|
|
|
81
81
|
* embed long-lived credentials.
|
|
82
82
|
*/
|
|
83
83
|
export declare function writeOwnerOnlyServiceManifest(filePath: string, content: string): void;
|
|
84
|
-
/**
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
84
|
+
/**
|
|
85
|
+
* Generate a macOS launchd plist for auto-starting the daemon.
|
|
86
|
+
*
|
|
87
|
+
* The plist never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
88
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
89
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
90
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the
|
|
91
|
+
* Keychain-backed secure store and never touches the unit file.
|
|
92
|
+
*/
|
|
93
|
+
export declare function generateLaunchdPlist(agentsBin?: string): string;
|
|
94
|
+
/**
|
|
95
|
+
* Generate a Linux systemd user unit for auto-starting the daemon.
|
|
96
|
+
*
|
|
97
|
+
* The unit never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
98
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
99
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
100
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the secure
|
|
101
|
+
* store and never touches the unit file.
|
|
102
|
+
*/
|
|
103
|
+
export declare function generateSystemdUnit(agentsBin?: string): string;
|
|
88
104
|
export declare function getAgentsBinPath(argv1?: string | undefined, execPath?: string): string;
|
|
89
105
|
/** Start the daemon via launchd, systemd, or as a detached process. */
|
|
90
106
|
export declare function startDaemon(agentsBin?: string): {
|
package/dist/lib/daemon.js
CHANGED
|
@@ -14,6 +14,7 @@ import { getDaemonDir as getDaemonDirRoot } from './state.js';
|
|
|
14
14
|
import { isAlive, killTree, backgroundSpawnOptions } from './platform/index.js';
|
|
15
15
|
import { listJobs as listAllJobs } from './routines.js';
|
|
16
16
|
import { JobScheduler } from './scheduler.js';
|
|
17
|
+
import { MonitorEngine } from './monitors/engine.js';
|
|
17
18
|
import { executeJobDetached, monitorRunningJobs } from './runner.js';
|
|
18
19
|
import { detectOverdueJobs, notifyOverdue } from './overdue.js';
|
|
19
20
|
import { BrowserService } from './browser/service.js';
|
|
@@ -293,6 +294,21 @@ export async function runDaemon() {
|
|
|
293
294
|
process.exit(0);
|
|
294
295
|
}
|
|
295
296
|
log('INFO', `Daemon started (PID: ${process.pid})`);
|
|
297
|
+
// RUSH-1759: the launchd plist / systemd unit no longer bake the Claude OAuth
|
|
298
|
+
// token onto disk. Obtain it here from the secure `claude` secrets bundle and
|
|
299
|
+
// inject into this process's env so every routine run this daemon spawns still
|
|
300
|
+
// receives it (via the sandbox allowlist), without the token ever being
|
|
301
|
+
// persisted in the service manifest. A read from a file-backed store (Linux)
|
|
302
|
+
// needs no prompt; on macOS it resolves broker-only from an unlocked
|
|
303
|
+
// secrets-agent and is otherwise absent (leaving the daemon on its existing
|
|
304
|
+
// interactive OAuth session), matching the detached-start path. Never blocks.
|
|
305
|
+
if (!process.env.CLAUDE_CODE_OAUTH_TOKEN) {
|
|
306
|
+
const oauthToken = readDaemonClaudeOAuthToken();
|
|
307
|
+
if (oauthToken) {
|
|
308
|
+
process.env.CLAUDE_CODE_OAUTH_TOKEN = oauthToken;
|
|
309
|
+
log('INFO', 'Loaded Claude OAuth token from secrets bundle for routine runs');
|
|
310
|
+
}
|
|
311
|
+
}
|
|
296
312
|
// Reap any stray duplicate daemon of this install that slipped past the start
|
|
297
313
|
// lock or was orphaned by a hard-crash — before it can double-fire jobs.
|
|
298
314
|
try {
|
|
@@ -349,6 +365,16 @@ export async function runDaemon() {
|
|
|
349
365
|
for (const job of scheduled) {
|
|
350
366
|
log('INFO', ` ${job.name} -> next: ${job.nextRun?.toISOString() || 'unknown'}`);
|
|
351
367
|
}
|
|
368
|
+
// Monitor engine: event-triggered watchers, beside the cron scheduler. Same
|
|
369
|
+
// daemon, same dispatch seam — a monitor is a routine whose trigger is a
|
|
370
|
+
// watched source instead of a clock. Reloads on SIGHUP alongside the scheduler.
|
|
371
|
+
const monitorEngine = new MonitorEngine((level, message) => log(level, message));
|
|
372
|
+
try {
|
|
373
|
+
monitorEngine.start();
|
|
374
|
+
}
|
|
375
|
+
catch (err) {
|
|
376
|
+
log('ERROR', `Monitor engine failed to start: ${err.message}`);
|
|
377
|
+
}
|
|
352
378
|
// Backlog detection: any enabled recurring job whose most-recent expected
|
|
353
379
|
// fire is older than its most-recent recorded run is overdue. Happens when
|
|
354
380
|
// the laptop was off or the daemon crashed through a scheduled fire.
|
|
@@ -603,6 +629,12 @@ export async function runDaemon() {
|
|
|
603
629
|
scheduler.reloadAll();
|
|
604
630
|
const reloaded = scheduler.listScheduled();
|
|
605
631
|
log('INFO', `Reloaded ${reloaded.length} jobs`);
|
|
632
|
+
try {
|
|
633
|
+
monitorEngine.reload();
|
|
634
|
+
}
|
|
635
|
+
catch (err) {
|
|
636
|
+
log('ERROR', `Monitor engine reload failed: ${err.message}`);
|
|
637
|
+
}
|
|
606
638
|
// Drop the memoized R2 config so rotated/added sync credentials are re-read
|
|
607
639
|
// on the next cycle instead of waiting for a restart.
|
|
608
640
|
void import('./session/sync/config.js').then(m => m.clearR2ConfigCache());
|
|
@@ -610,6 +642,7 @@ export async function runDaemon() {
|
|
|
610
642
|
const handleShutdown = async () => {
|
|
611
643
|
log('INFO', 'Daemon shutting down');
|
|
612
644
|
scheduler.stopAll();
|
|
645
|
+
monitorEngine.stop();
|
|
613
646
|
await browserIPC.stop();
|
|
614
647
|
clearInterval(monitorInterval);
|
|
615
648
|
clearInterval(syncInterval);
|
|
@@ -680,15 +713,18 @@ export function writeOwnerOnlyServiceManifest(filePath, content) {
|
|
|
680
713
|
fs.rmSync(filePath, { force: true });
|
|
681
714
|
fs.writeFileSync(filePath, content, { encoding: 'utf-8', mode: 0o600 });
|
|
682
715
|
}
|
|
683
|
-
/**
|
|
684
|
-
|
|
716
|
+
/**
|
|
717
|
+
* Generate a macOS launchd plist for auto-starting the daemon.
|
|
718
|
+
*
|
|
719
|
+
* The plist never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
720
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
721
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
722
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the
|
|
723
|
+
* Keychain-backed secure store and never touches the unit file.
|
|
724
|
+
*/
|
|
725
|
+
export function generateLaunchdPlist(agentsBin = getAgentsBinPath()) {
|
|
685
726
|
const launch = getDaemonLaunch(agentsBin);
|
|
686
727
|
const logPath = getLogPath();
|
|
687
|
-
const oauthEntry = oauthToken
|
|
688
|
-
? `
|
|
689
|
-
<key>${DAEMON_OAUTH_KEY}</key>
|
|
690
|
-
<string>${xmlEscape(oauthToken)}</string>`
|
|
691
|
-
: '';
|
|
692
728
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
693
729
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
694
730
|
<plist version="1.0">
|
|
@@ -710,7 +746,7 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
710
746
|
<key>EnvironmentVariables</key>
|
|
711
747
|
<dict>
|
|
712
748
|
<key>PATH</key>
|
|
713
|
-
<string>${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.bun/bin</string
|
|
749
|
+
<string>${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.bun/bin</string>
|
|
714
750
|
</dict>
|
|
715
751
|
</dict>
|
|
716
752
|
</plist>`;
|
|
@@ -719,13 +755,18 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
719
755
|
function systemdExecArg(value) {
|
|
720
756
|
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
|
721
757
|
}
|
|
722
|
-
/**
|
|
723
|
-
|
|
758
|
+
/**
|
|
759
|
+
* Generate a Linux systemd user unit for auto-starting the daemon.
|
|
760
|
+
*
|
|
761
|
+
* The unit never embeds the Claude OAuth token (RUSH-1759): a persisted service
|
|
762
|
+
* manifest is a plaintext credential on disk even at 0600. The daemon instead
|
|
763
|
+
* obtains the token at startup from the `claude` secrets bundle
|
|
764
|
+
* (readDaemonClaudeOAuthToken, injected in runDaemon), so it stays in the secure
|
|
765
|
+
* store and never touches the unit file.
|
|
766
|
+
*/
|
|
767
|
+
export function generateSystemdUnit(agentsBin = getAgentsBinPath()) {
|
|
724
768
|
const launch = getDaemonLaunch(agentsBin);
|
|
725
769
|
const execStart = [launch.command, ...launch.args].map(systemdExecArg).join(' ');
|
|
726
|
-
const oauthLine = oauthToken
|
|
727
|
-
? `\nEnvironment=${DAEMON_OAUTH_KEY}=${oauthToken}`
|
|
728
|
-
: '';
|
|
729
770
|
return `[Unit]
|
|
730
771
|
Description=Agents Daemon - Scheduled Job Runner
|
|
731
772
|
After=network.target
|
|
@@ -735,7 +776,7 @@ Type=simple
|
|
|
735
776
|
ExecStart=${execStart}
|
|
736
777
|
Restart=always
|
|
737
778
|
RestartSec=10
|
|
738
|
-
Environment=PATH=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin
|
|
779
|
+
Environment=PATH=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin
|
|
739
780
|
|
|
740
781
|
[Install]
|
|
741
782
|
WantedBy=default.target`;
|
|
@@ -852,9 +893,10 @@ function startDaemonLocked(agentsBin) {
|
|
|
852
893
|
if (!fs.existsSync(plistDir)) {
|
|
853
894
|
fs.mkdirSync(plistDir, { recursive: true });
|
|
854
895
|
}
|
|
855
|
-
// The plist
|
|
856
|
-
// create owner-only atomically
|
|
857
|
-
|
|
896
|
+
// The plist carries no credential (RUSH-1759 — the daemon reads the OAuth
|
|
897
|
+
// token itself at startup); still create owner-only atomically to match the
|
|
898
|
+
// detached path and keep the log/PATH surface owner-private.
|
|
899
|
+
writeOwnerOnlyServiceManifest(plistPath, generateLaunchdPlist(agentsBin));
|
|
858
900
|
try {
|
|
859
901
|
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] });
|
|
860
902
|
}
|
|
@@ -881,8 +923,9 @@ function startDaemonLocked(agentsBin) {
|
|
|
881
923
|
if (!fs.existsSync(unitDir)) {
|
|
882
924
|
fs.mkdirSync(unitDir, { recursive: true });
|
|
883
925
|
}
|
|
884
|
-
//
|
|
885
|
-
|
|
926
|
+
// Carries no credential (RUSH-1759 — the daemon reads the OAuth token
|
|
927
|
+
// itself at startup); owner-only to keep the PATH/log surface private.
|
|
928
|
+
writeOwnerOnlyServiceManifest(unitPath, generateSystemdUnit(agentsBin));
|
|
886
929
|
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8' });
|
|
887
930
|
execFileSync('systemctl', ['--user', 'enable', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
888
931
|
execFileSync('systemctl', ['--user', 'start', SYSTEMD_UNIT], { encoding: 'utf-8' });
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
* throws (misconfigured auth, etc.) become `failed` rows — they never abort
|
|
8
8
|
* the remaining devices.
|
|
9
9
|
*/
|
|
10
|
-
import type
|
|
11
|
-
export type FleetSkipReason = 'offline' | 'no-address';
|
|
10
|
+
import { type DeviceProfile, type DeviceRegistry } from './registry.js';
|
|
11
|
+
export type FleetSkipReason = 'offline' | 'no-address' | 'control';
|
|
12
12
|
/** npm dist-tags / semver pins only — rejects shell metacharacters. */
|
|
13
13
|
export declare const FLEET_VERSION_RE: RegExp;
|
|
14
14
|
export interface FleetTarget {
|
|
@@ -27,6 +27,7 @@ export interface FleetRunResult {
|
|
|
27
27
|
/**
|
|
28
28
|
* Classify each registered device for a fleet operation.
|
|
29
29
|
*
|
|
30
|
+
* - Control-only device (a cockpit, e.g. a paired iPhone) → skip `control`
|
|
30
31
|
* - Tailscale-offline → skip `offline`
|
|
31
32
|
* - No address → skip `no-address`
|
|
32
33
|
* - Everything else is a target (including this machine, reached over ssh when
|