@phnx-labs/agents-cli 1.22.83 → 1.22.85
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +96 -0
- package/README.md +80 -53
- package/dist/bootstrap.d.ts +7 -6
- package/dist/bootstrap.js +20 -27
- package/dist/cli/command-registry.js +5 -1
- package/dist/commands/accounts.d.ts +6 -13
- package/dist/commands/accounts.js +330 -143
- package/dist/commands/apply.js +1 -1
- package/dist/commands/auth-mint.d.ts +7 -3
- package/dist/commands/auth-mint.js +21 -9
- package/dist/commands/auth.js +2 -3
- package/dist/commands/browser.js +10 -10
- package/dist/commands/daemon.d.ts +7 -3
- package/dist/commands/daemon.js +40 -25
- package/dist/commands/doctor.js +28 -2
- package/dist/commands/exec.js +87 -39
- package/dist/commands/fleet-capture.js +21 -2
- package/dist/commands/harness-wizard.js +2 -2
- package/dist/commands/lease.js +7 -12
- package/dist/commands/profiles.d.ts +2 -2
- package/dist/commands/profiles.js +20 -13
- package/dist/commands/repo.js +3 -3
- package/dist/commands/run-account-picker.d.ts +3 -3
- package/dist/commands/run-account-picker.js +29 -30
- package/dist/commands/secrets-passthrough.d.ts +20 -0
- package/dist/commands/secrets-passthrough.js +43 -0
- package/dist/commands/sessions-trace.d.ts +11 -0
- package/dist/commands/sessions-trace.js +71 -0
- package/dist/commands/setup-accounts.d.ts +3 -2
- package/dist/commands/setup-accounts.js +4 -4
- package/dist/commands/setup-secrets.d.ts +16 -21
- package/dist/commands/setup-secrets.js +43 -224
- package/dist/commands/ssh.js +2 -2
- package/dist/commands/sync.js +1 -1
- package/dist/commands/update.d.ts +10 -0
- package/dist/commands/update.js +43 -40
- package/dist/commands/versions.d.ts +11 -11
- package/dist/commands/versions.js +131 -83
- package/dist/commands/view.d.ts +1 -9
- package/dist/commands/view.js +41 -55
- package/dist/commands/webhook.js +5 -5
- package/dist/commands/workflows.js +2 -1
- package/dist/index.d.ts +12 -12
- package/dist/index.js +13 -42
- package/dist/lib/account-capabilities.d.ts +1 -1
- package/dist/lib/account-capabilities.js +1 -1
- package/dist/lib/account-catalog.d.ts +121 -6
- package/dist/lib/account-catalog.js +421 -7
- package/dist/lib/account-registry.d.ts +8 -30
- package/dist/lib/account-registry.js +36 -79
- package/dist/lib/account-schema.d.ts +1 -1
- package/dist/lib/account-schema.js +1 -1
- package/dist/lib/accounting/account-pool-collect.js +16 -4
- package/dist/lib/accounting/rotate.d.ts +20 -7
- package/dist/lib/accounting/rotate.js +89 -13
- package/dist/lib/accounting/usage.js +11 -16
- package/dist/lib/accounts/add.d.ts +138 -0
- package/dist/lib/accounts/add.js +651 -0
- package/dist/lib/accounts/migrate.d.ts +117 -0
- package/dist/lib/accounts/migrate.js +536 -0
- package/dist/lib/accounts/slots.d.ts +13 -0
- package/dist/lib/accounts/slots.js +100 -0
- package/dist/lib/agent-spec/agents.d.ts +16 -0
- package/dist/lib/agent-spec/agents.js +32 -4
- package/dist/lib/app-bundle-install.js +5 -4
- package/dist/lib/auth-health.d.ts +3 -0
- package/dist/lib/auth-health.js +11 -0
- package/dist/lib/auth-mint.d.ts +52 -16
- package/dist/lib/auth-mint.js +117 -39
- package/dist/lib/browser/chrome.d.ts +1 -1
- package/dist/lib/browser/chrome.js +5 -5
- package/dist/lib/byok-usage.js +3 -3
- package/dist/lib/claude-account-token.d.ts +47 -3
- package/dist/lib/claude-account-token.js +172 -50
- package/dist/lib/cloud/antigravity.js +4 -4
- package/dist/lib/cloud/cursor.js +4 -4
- package/dist/lib/crabbox/cli.d.ts +1 -1
- package/dist/lib/crabbox/cli.js +6 -6
- package/dist/lib/crabbox/runtimes.d.ts +3 -3
- package/dist/lib/crabbox/runtimes.js +5 -5
- package/dist/lib/daemon/account-state-daemon-service.d.ts +37 -1
- package/dist/lib/daemon/account-state-daemon-service.js +199 -3
- package/dist/lib/daemon/auth-sync-service.js +28 -1
- package/dist/lib/daemon/daemon.d.ts +6 -16
- package/dist/lib/daemon/daemon.js +52 -97
- package/dist/lib/daemon/daemon.test-fixture.d.ts +7 -9
- package/dist/lib/daemon/daemon.test-fixture.js +22 -40
- package/dist/lib/daemon/harness-update-service.d.ts +1 -1
- package/dist/lib/daemon/harness-update-service.js +1 -1
- package/dist/lib/daemon/runner.d.ts +15 -3
- package/dist/lib/daemon/runner.js +49 -24
- package/dist/lib/daemon-health.d.ts +1 -2
- package/dist/lib/daemon-health.js +7 -6
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +1 -11
- package/dist/lib/daemon-ticks.js +23 -2
- package/dist/lib/daemon-webhooks.d.ts +8 -7
- package/dist/lib/daemon-webhooks.js +10 -9
- package/dist/lib/device-config.js +1 -1
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/harness-inventory.d.ts +39 -0
- package/dist/lib/devices/harness-inventory.js +126 -4
- package/dist/lib/doctor-diff.js +2 -1
- package/dist/lib/exec-account-home.d.ts +38 -0
- package/dist/lib/exec-account-home.js +164 -0
- package/dist/lib/exec.d.ts +30 -0
- package/dist/lib/exec.js +101 -31
- package/dist/lib/feed/activity-stream.d.ts +60 -0
- package/dist/lib/feed/activity-stream.js +271 -0
- package/dist/lib/feed/activity.d.ts +7 -0
- package/dist/lib/feed/activity.js +10 -4
- package/dist/lib/feed/watch.d.ts +9 -3
- package/dist/lib/feed/watch.js +72 -36
- package/dist/lib/fleet/apply.d.ts +2 -2
- package/dist/lib/fleet/apply.js +4 -4
- package/dist/lib/fleet/auth-sync.js +4 -3
- package/dist/lib/fleet-shared-repo-sync.js +6 -9
- package/dist/lib/fleet-shared-state.d.ts +6 -0
- package/dist/lib/harness/adapter.d.ts +7 -7
- package/dist/lib/harness/adapter.js +10 -6
- package/dist/lib/harness/adapters/grok.js +8 -3
- package/dist/lib/harness/adapters/muse.js +1 -1
- package/dist/lib/harness/adapters/opencode.js +12 -3
- package/dist/lib/harness-auth-capabilities.d.ts +61 -0
- package/dist/lib/harness-auth-capabilities.js +52 -0
- package/dist/lib/helper-versions.d.ts +6 -4
- package/dist/lib/helper-versions.js +5 -4
- package/dist/lib/hosts/credential-transport.d.ts +10 -0
- package/dist/lib/hosts/credential-transport.js +37 -0
- package/dist/lib/hosts/dispatch.d.ts +7 -0
- package/dist/lib/hosts/dispatch.js +13 -6
- package/dist/lib/identity/client.d.ts +3 -3
- package/dist/lib/identity/client.js +3 -3
- package/dist/lib/installations/index.d.ts +1 -1
- package/dist/lib/installations/index.js +1 -1
- package/dist/lib/installations/migrate.js +9 -0
- package/dist/lib/installations/resolve.js +1 -1
- package/dist/lib/installations/shims.d.ts +15 -0
- package/dist/lib/installations/shims.js +69 -0
- package/dist/lib/installations/store.d.ts +43 -0
- package/dist/lib/installations/store.js +75 -7
- package/dist/lib/installations/versions.js +1 -1
- package/dist/lib/menubar/install-menubar.d.ts +3 -3
- package/dist/lib/menubar/install-menubar.js +3 -3
- package/dist/lib/native-accounts.d.ts +35 -0
- package/dist/lib/native-accounts.js +41 -0
- package/dist/lib/net-close.d.ts +11 -0
- package/dist/lib/net-close.js +25 -0
- package/dist/lib/openclaw-keychain.js +27 -1
- package/dist/lib/profiles.js +22 -9
- package/dist/lib/project-resources.js +1 -1
- package/dist/lib/reserved-stores.d.ts +53 -0
- package/dist/lib/reserved-stores.js +120 -0
- package/dist/lib/secrets-client.d.ts +191 -0
- package/dist/lib/secrets-client.js +710 -0
- package/dist/lib/secrets-policy.d.ts +222 -0
- package/dist/lib/secrets-policy.js +483 -0
- package/dist/lib/secrets-types.d.ts +188 -0
- package/dist/lib/secrets-types.js +24 -0
- package/dist/lib/service-manifest.js +3 -4
- package/dist/lib/session/active.d.ts +38 -4
- package/dist/lib/session/active.js +36 -4
- package/dist/lib/session/bash-command.js +10 -0
- package/dist/lib/session/db.d.ts +65 -2
- package/dist/lib/session/db.js +178 -18
- package/dist/lib/session/mirror.d.ts +5 -0
- package/dist/lib/session/mirror.js +166 -0
- package/dist/lib/session/parse.d.ts +49 -0
- package/dist/lib/session/parse.js +324 -30
- package/dist/lib/session/prompt.d.ts +93 -2
- package/dist/lib/session/prompt.js +266 -15
- package/dist/lib/session/remote/peer-stream.d.ts +47 -0
- package/dist/lib/session/remote/peer-stream.js +142 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +41 -42
- package/dist/lib/session/session-cache.d.ts +9 -0
- package/dist/lib/session/session-cache.js +40 -2
- package/dist/lib/session/sync/config.js +2 -2
- package/dist/lib/session/timeline-pass.d.ts +129 -0
- package/dist/lib/session/timeline-pass.js +323 -0
- package/dist/lib/session/timeline.d.ts +182 -0
- package/dist/lib/session/timeline.js +636 -0
- package/dist/lib/session/types.d.ts +155 -1
- package/dist/lib/sha256-asset.d.ts +11 -16
- package/dist/lib/sha256-asset.js +11 -16
- package/dist/lib/share/config.js +14 -5
- package/dist/lib/signin-badge.d.ts +26 -0
- package/dist/lib/signin-badge.js +42 -0
- package/dist/lib/staleness/detectors/workflows.js +3 -105
- package/dist/lib/staleness/writers/workflows.js +2 -1
- package/dist/lib/state.d.ts +7 -4
- package/dist/lib/state.js +18 -7
- package/dist/lib/summarizer/pass.d.ts +2 -0
- package/dist/lib/summarizer/pass.js +14 -1
- package/dist/lib/summarizer/summarize.d.ts +7 -0
- package/dist/lib/summarizer/summarize.js +3 -0
- package/dist/lib/sync-umbrella.js +2 -2
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +74 -33
- package/dist/lib/view-types.d.ts +6 -3
- package/dist/lib/workflows-registry.d.ts +71 -0
- package/dist/lib/workflows-registry.js +280 -0
- package/dist/lib/workflows.d.ts +21 -21
- package/dist/lib/workflows.js +12 -327
- package/package.json +2 -3
- package/scripts/postinstall.js +4 -29
- package/dist/commands/secrets-import.d.ts +0 -18
- package/dist/commands/secrets-import.js +0 -74
- package/dist/commands/secrets-migrate.d.ts +0 -25
- package/dist/commands/secrets-migrate.js +0 -334
- package/dist/commands/secrets-rotate-passphrase.d.ts +0 -17
- package/dist/commands/secrets-rotate-passphrase.js +0 -96
- package/dist/commands/secrets-sync.d.ts +0 -11
- package/dist/commands/secrets-sync.js +0 -153
- package/dist/commands/secrets-vault.d.ts +0 -10
- package/dist/commands/secrets-vault.js +0 -130
- package/dist/commands/secrets.d.ts +0 -212
- package/dist/commands/secrets.js +0 -3030
- package/dist/lib/accounts/connect.d.ts +0 -170
- package/dist/lib/accounts/connect.js +0 -383
- package/dist/lib/daemon/keychain-reap-service.d.ts +0 -17
- package/dist/lib/daemon/keychain-reap-service.js +0 -32
- package/dist/lib/daemon/secrets-broker-service.d.ts +0 -21
- package/dist/lib/daemon/secrets-broker-service.js +0 -51
- package/dist/lib/secrets/agent.d.ts +0 -358
- package/dist/lib/secrets/agent.js +0 -1291
- package/dist/lib/secrets/audit.d.ts +0 -46
- package/dist/lib/secrets/audit.js +0 -101
- package/dist/lib/secrets/bundles.d.ts +0 -290
- package/dist/lib/secrets/bundles.js +0 -1547
- package/dist/lib/secrets/download-keychain.d.ts +0 -47
- package/dist/lib/secrets/download-keychain.js +0 -70
- package/dist/lib/secrets/drivers/rush.d.ts +0 -14
- package/dist/lib/secrets/drivers/rush.js +0 -90
- package/dist/lib/secrets/fallback.d.ts +0 -48
- package/dist/lib/secrets/fallback.js +0 -48
- package/dist/lib/secrets/filestore.d.ts +0 -222
- package/dist/lib/secrets/filestore.js +0 -1099
- package/dist/lib/secrets/headless.d.ts +0 -43
- package/dist/lib/secrets/headless.js +0 -56
- package/dist/lib/secrets/icloud-import.d.ts +0 -79
- package/dist/lib/secrets/icloud-import.js +0 -206
- package/dist/lib/secrets/index.d.ts +0 -451
- package/dist/lib/secrets/index.js +0 -1568
- package/dist/lib/secrets/install-helper.d.ts +0 -72
- package/dist/lib/secrets/install-helper.js +0 -245
- package/dist/lib/secrets/lease.d.ts +0 -25
- package/dist/lib/secrets/lease.js +0 -44
- package/dist/lib/secrets/linux.d.ts +0 -77
- package/dist/lib/secrets/linux.js +0 -393
- package/dist/lib/secrets/list-filter.d.ts +0 -109
- package/dist/lib/secrets/list-filter.js +0 -261
- package/dist/lib/secrets/mcp.d.ts +0 -93
- package/dist/lib/secrets/mcp.js +0 -211
- package/dist/lib/secrets/profiles.d.ts +0 -10
- package/dist/lib/secrets/profiles.js +0 -13
- package/dist/lib/secrets/push.d.ts +0 -133
- package/dist/lib/secrets/push.js +0 -273
- package/dist/lib/secrets/rc-hygiene.d.ts +0 -63
- package/dist/lib/secrets/rc-hygiene.js +0 -143
- package/dist/lib/secrets/read-backoff.d.ts +0 -27
- package/dist/lib/secrets/read-backoff.js +0 -64
- package/dist/lib/secrets/reaper.d.ts +0 -90
- package/dist/lib/secrets/reaper.js +0 -243
- package/dist/lib/secrets/remote.d.ts +0 -152
- package/dist/lib/secrets/remote.js +0 -344
- package/dist/lib/secrets/reserved-sync.d.ts +0 -66
- package/dist/lib/secrets/reserved-sync.js +0 -147
- package/dist/lib/secrets/scope.d.ts +0 -26
- package/dist/lib/secrets/scope.js +0 -29
- package/dist/lib/secrets/session-store.d.ts +0 -107
- package/dist/lib/secrets/session-store.js +0 -342
- package/dist/lib/secrets/sync-backend.d.ts +0 -48
- package/dist/lib/secrets/sync-backend.js +0 -13
- package/dist/lib/secrets/sync-commands.d.ts +0 -21
- package/dist/lib/secrets/sync-commands.js +0 -21
- package/dist/lib/secrets/sync.d.ts +0 -48
- package/dist/lib/secrets/sync.js +0 -237
- package/dist/lib/secrets/unlock-hints.d.ts +0 -27
- package/dist/lib/secrets/unlock-hints.js +0 -36
- package/dist/lib/secrets/usage-db.d.ts +0 -46
- package/dist/lib/secrets/usage-db.js +0 -96
- package/dist/lib/secrets/vault-age-helper.d.ts +0 -1
- package/dist/lib/secrets/vault-age-helper.js +0 -34
- package/dist/lib/secrets/vault.d.ts +0 -49
- package/dist/lib/secrets/vault.js +0 -399
- package/dist/lib/secrets/windows.d.ts +0 -81
- package/dist/lib/secrets/windows.js +0 -556
- package/scripts/install-helper.js +0 -97
- /package/dist/lib/{secrets/sync-passphrase.d.ts → sync-passphrase.d.ts} +0 -0
- /package/dist/lib/{secrets/sync-passphrase.js → sync-passphrase.js} +0 -0
|
@@ -21,10 +21,23 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
23
23
|
import type { DaemonServiceId } from '../daemon-services.js';
|
|
24
|
+
import type { AuthProbeRow } from '../auth-health.js';
|
|
24
25
|
/** Usage cache refresh cadence. */
|
|
25
26
|
export declare const USAGE_STATE_TICK_MS = 60000;
|
|
26
27
|
/** Fleet-auth refresh cadence — deliberately slower (rate-limited endpoint). */
|
|
27
28
|
export declare const AUTH_STATE_TICK_MS: number;
|
|
29
|
+
type AccountAuthRefresh = (signal: AbortSignal) => Promise<AuthProbeRow[] | void>;
|
|
30
|
+
type AccountTransitionNotifier = (transition: DeadAccountTransition) => Promise<void>;
|
|
31
|
+
export interface DeadAccountTransition {
|
|
32
|
+
agent: AuthProbeRow['agent'];
|
|
33
|
+
/** Display label (email / account name) — presentation only, never a key. */
|
|
34
|
+
account: string;
|
|
35
|
+
verdict: 'expired' | 'revoked';
|
|
36
|
+
}
|
|
37
|
+
interface AccountAuthServiceOptions {
|
|
38
|
+
stateFile?: string;
|
|
39
|
+
notify?: AccountTransitionNotifier;
|
|
40
|
+
}
|
|
28
41
|
/**
|
|
29
42
|
* Refresh the usage cache the `agents run` router reads. Independent circuit
|
|
30
43
|
* breaker from auth — a persistently-failing usage endpoint parks only this.
|
|
@@ -49,8 +62,31 @@ export declare class AccountAuthService extends BasePeriodicService {
|
|
|
49
62
|
readonly intervalMs: number;
|
|
50
63
|
readonly deadlineMs: number;
|
|
51
64
|
private readonly refresh;
|
|
52
|
-
|
|
65
|
+
private readonly stateFile;
|
|
66
|
+
private readonly notify;
|
|
67
|
+
constructor(refresh?: AccountAuthRefresh, options?: AccountAuthServiceOptions);
|
|
53
68
|
protected onStart(): Promise<void>;
|
|
54
69
|
protected onStop(): Promise<void>;
|
|
55
70
|
protected onTick(_ctx: DaemonContext, signal: AbortSignal): Promise<void>;
|
|
56
71
|
}
|
|
72
|
+
export declare function publishAccountDaemonStateRows(rows: readonly AuthProbeRow[], userAgentsDir?: string): Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Detect live → expired/revoked transitions and deliver one owner-important
|
|
75
|
+
* feed post each, through a persisted outbox:
|
|
76
|
+
*
|
|
77
|
+
* 1. Newly-dead accounts are ENQUEUED into `pending` and the state is written
|
|
78
|
+
* BEFORE any delivery is attempted — a crash or a failed sink can never
|
|
79
|
+
* strand a transition in the already-recorded `entries` with no retry.
|
|
80
|
+
* 2. Every pending item (fresh and retried) is delivered; an item leaves
|
|
81
|
+
* `pending` only after its notifier resolves, and the state is written
|
|
82
|
+
* again. A crash in the narrow window between a successful post and that
|
|
83
|
+
* second write re-sends once — at-least-once, never zero.
|
|
84
|
+
*
|
|
85
|
+
* Delivery failures keep their outbox entries AND throw after the state is
|
|
86
|
+
* durable, so the supervisor's circuit breaker still sees the unhealthy tick.
|
|
87
|
+
*/
|
|
88
|
+
export declare function processAccountAuthTransitions(rows: readonly AuthProbeRow[], options?: {
|
|
89
|
+
stateFile?: string;
|
|
90
|
+
notify?: AccountTransitionNotifier;
|
|
91
|
+
}): Promise<DeadAccountTransition[]>;
|
|
92
|
+
export {};
|
|
@@ -19,14 +19,29 @@
|
|
|
19
19
|
* (`AbortSignal.timeout` at the leaf), and the supervisor abandons + restarts a
|
|
20
20
|
* hung tick regardless.
|
|
21
21
|
*/
|
|
22
|
+
import * as fsp from 'node:fs/promises';
|
|
23
|
+
import * as path from 'node:path';
|
|
22
24
|
import { BasePeriodicService } from './service.js';
|
|
23
|
-
import { runUsageRefreshTick,
|
|
25
|
+
import { runUsageRefreshTick, refreshLocalFleetAuthState } from '../daemon-ticks.js';
|
|
26
|
+
import { getDaemonDir } from '../state.js';
|
|
27
|
+
import { getCliLaunch } from '../cli-entry.js';
|
|
28
|
+
import { execFileBounded } from '../exec-bounded.js';
|
|
29
|
+
import { fleetSharedStatePath, FLEET_SHARED_STATE_VERSION } from '../fleet-shared-state.js';
|
|
30
|
+
import { withFileLockAsync } from '../fs-atomic.js';
|
|
31
|
+
import { machineId } from '../machine-id.js';
|
|
32
|
+
import { getConfigValueAsync, isHeadedDeviceRole } from '../device-config.js';
|
|
33
|
+
import { harnessWorkerIsPerDevice } from '../harness-auth-capabilities.js';
|
|
24
34
|
/** Usage cache refresh cadence. */
|
|
25
35
|
export const USAGE_STATE_TICK_MS = 60_000;
|
|
26
36
|
/** Fleet-auth refresh cadence — deliberately slower (rate-limited endpoint). */
|
|
27
37
|
export const AUTH_STATE_TICK_MS = 3 * 60_000;
|
|
28
38
|
/** A hung refresh is abandoned at this deadline; a real sweep is far under it. */
|
|
29
39
|
const REFRESH_DEADLINE_MS = 2 * 60_000;
|
|
40
|
+
const NOTIFY_DEADLINE_MS = 30_000;
|
|
41
|
+
const ACCOUNT_TRANSITIONS_FILE = 'account-auth-transitions.json';
|
|
42
|
+
// Resolve the current CLI once at daemon startup. getCliLaunch performs sync
|
|
43
|
+
// filesystem probes, so it must never run inside a service tick.
|
|
44
|
+
const FEED_POST_LAUNCH = getCliLaunch([]);
|
|
30
45
|
/**
|
|
31
46
|
* A promise that rejects when `signal` aborts (immediately if already aborted),
|
|
32
47
|
* so a tick can `Promise.race` its work against the supervisor's deadline and
|
|
@@ -74,16 +89,197 @@ export class AccountAuthService extends BasePeriodicService {
|
|
|
74
89
|
intervalMs = AUTH_STATE_TICK_MS;
|
|
75
90
|
deadlineMs = REFRESH_DEADLINE_MS;
|
|
76
91
|
refresh;
|
|
77
|
-
|
|
92
|
+
stateFile;
|
|
93
|
+
notify;
|
|
94
|
+
constructor(refresh = async (signal) => (await refreshLocalFleetAuthState({ signal })).authRows, options = {}) {
|
|
78
95
|
super();
|
|
79
96
|
this.refresh = refresh;
|
|
97
|
+
this.stateFile = options.stateFile ?? path.join(getDaemonDir(), ACCOUNT_TRANSITIONS_FILE);
|
|
98
|
+
this.notify = options.notify ?? postImportantAccountTransition;
|
|
80
99
|
}
|
|
81
100
|
async onStart() { }
|
|
82
101
|
async onStop() { }
|
|
83
102
|
async onTick(_ctx, signal) {
|
|
84
|
-
await Promise.race([
|
|
103
|
+
const rows = await Promise.race([
|
|
85
104
|
this.refresh(signal),
|
|
86
105
|
abortRejection(signal, 'auth refresh aborted at deadline'),
|
|
87
106
|
]);
|
|
107
|
+
if (rows) {
|
|
108
|
+
await publishAccountDaemonStateRows(rows);
|
|
109
|
+
await processAccountAuthTransitions(rows, {
|
|
110
|
+
stateFile: this.stateFile,
|
|
111
|
+
notify: this.notify,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
export async function publishAccountDaemonStateRows(rows, userAgentsDir) {
|
|
117
|
+
const device = machineId();
|
|
118
|
+
const file = fleetSharedStatePath(device, userAgentsDir);
|
|
119
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
120
|
+
const handle = await fsp.open(file, 'a', 0o600);
|
|
121
|
+
await handle.close();
|
|
122
|
+
const role = (await getConfigValueAsync('role', { device })).value;
|
|
123
|
+
const headed = isHeadedDeviceRole(role);
|
|
124
|
+
const accountRows = [...collapseAccountRows(rows).values()].map((row) => ({
|
|
125
|
+
// The registered account id is the only stable identity: two accounts may
|
|
126
|
+
// share one email (different orgs), so the display label is never a key.
|
|
127
|
+
// Unregistered legacy homes have no registry id and fall back to the label.
|
|
128
|
+
accountId: row.accountId ?? row.account ?? row.version,
|
|
129
|
+
identityLabel: row.account,
|
|
130
|
+
harness: row.agent,
|
|
131
|
+
authMode: harnessWorkerIsPerDevice(row.agent)
|
|
132
|
+
? 'per-device'
|
|
133
|
+
: headed ? 'native' : 'durable',
|
|
134
|
+
verdict: row.health.verdict === 'unconfigured'
|
|
135
|
+
? 'missing'
|
|
136
|
+
: row.health.verdict === 'error' ? 'unverified' : row.health.verdict,
|
|
137
|
+
checkedAt: new Date(row.health.checkedAt).toISOString(),
|
|
138
|
+
}));
|
|
139
|
+
await withFileLockAsync(file, async () => {
|
|
140
|
+
const raw = await fsp.readFile(file, 'utf-8');
|
|
141
|
+
let current = {};
|
|
142
|
+
if (raw.trim()) {
|
|
143
|
+
const parsed = JSON.parse(raw);
|
|
144
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
145
|
+
throw new Error(`Cannot publish account state: ${file} is not an object.`);
|
|
146
|
+
}
|
|
147
|
+
current = parsed;
|
|
148
|
+
if (current.device !== device || current.version !== FLEET_SHARED_STATE_VERSION) {
|
|
149
|
+
throw new Error(`Cannot publish account state: ${file} has an unrecognized owner or version.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const next = {
|
|
153
|
+
...current,
|
|
154
|
+
version: FLEET_SHARED_STATE_VERSION,
|
|
155
|
+
device,
|
|
156
|
+
accounts: { rows: accountRows },
|
|
157
|
+
};
|
|
158
|
+
const serialized = `${JSON.stringify(next, null, 2)}\n`;
|
|
159
|
+
if (serialized === raw)
|
|
160
|
+
return;
|
|
161
|
+
const temp = `${file}.${process.pid}.accounts.tmp`;
|
|
162
|
+
await fsp.writeFile(temp, serialized, { mode: 0o600 });
|
|
163
|
+
await fsp.rename(temp, file);
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function transitionKey(row) {
|
|
167
|
+
// Stable identity first (registered account id); the display label is the
|
|
168
|
+
// fallback for unregistered legacy homes, never the key for a named account.
|
|
169
|
+
return `${row.agent}:${row.accountId ?? row.account ?? `version:${row.version}`}`;
|
|
170
|
+
}
|
|
171
|
+
function verdictRank(verdict) {
|
|
172
|
+
switch (verdict) {
|
|
173
|
+
case 'revoked': return 7;
|
|
174
|
+
case 'expired': return 6;
|
|
175
|
+
case 'rate_limited': return 5;
|
|
176
|
+
case 'live': return 4;
|
|
177
|
+
case 'unverified': return 3;
|
|
178
|
+
case 'error': return 2;
|
|
179
|
+
case 'unconfigured': return 1;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function collapseAccountRows(rows) {
|
|
183
|
+
const accounts = new Map();
|
|
184
|
+
for (const row of rows) {
|
|
185
|
+
const key = transitionKey(row);
|
|
186
|
+
const current = accounts.get(key);
|
|
187
|
+
if (!current || verdictRank(row.health.verdict) > verdictRank(current.health.verdict)) {
|
|
188
|
+
accounts.set(key, row);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return accounts;
|
|
192
|
+
}
|
|
193
|
+
async function readTransitionState(file) {
|
|
194
|
+
try {
|
|
195
|
+
const parsed = JSON.parse(await fsp.readFile(file, 'utf-8'));
|
|
196
|
+
if ((parsed.version === 1 || parsed.version === 2) && parsed.entries && typeof parsed.entries === 'object') {
|
|
197
|
+
// Version 1 files predate the outbox; they upgrade with an empty one.
|
|
198
|
+
return { version: 2, entries: parsed.entries, pending: parsed.pending ?? {} };
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
catch (error) {
|
|
202
|
+
if (error.code !== 'ENOENT') {
|
|
203
|
+
throw new Error(`Cannot read account transition state at ${file}: ${error.message}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return { version: 2, entries: {}, pending: {} };
|
|
207
|
+
}
|
|
208
|
+
async function writeTransitionState(file, state) {
|
|
209
|
+
await fsp.mkdir(path.dirname(file), { recursive: true });
|
|
210
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
211
|
+
await fsp.writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
|
|
212
|
+
await fsp.rename(temp, file);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Detect live → expired/revoked transitions and deliver one owner-important
|
|
216
|
+
* feed post each, through a persisted outbox:
|
|
217
|
+
*
|
|
218
|
+
* 1. Newly-dead accounts are ENQUEUED into `pending` and the state is written
|
|
219
|
+
* BEFORE any delivery is attempted — a crash or a failed sink can never
|
|
220
|
+
* strand a transition in the already-recorded `entries` with no retry.
|
|
221
|
+
* 2. Every pending item (fresh and retried) is delivered; an item leaves
|
|
222
|
+
* `pending` only after its notifier resolves, and the state is written
|
|
223
|
+
* again. A crash in the narrow window between a successful post and that
|
|
224
|
+
* second write re-sends once — at-least-once, never zero.
|
|
225
|
+
*
|
|
226
|
+
* Delivery failures keep their outbox entries AND throw after the state is
|
|
227
|
+
* durable, so the supervisor's circuit breaker still sees the unhealthy tick.
|
|
228
|
+
*/
|
|
229
|
+
export async function processAccountAuthTransitions(rows, options = {}) {
|
|
230
|
+
const file = options.stateFile ?? path.join(getDaemonDir(), ACCOUNT_TRANSITIONS_FILE);
|
|
231
|
+
const notify = options.notify ?? postImportantAccountTransition;
|
|
232
|
+
const previous = await readTransitionState(file);
|
|
233
|
+
const next = {
|
|
234
|
+
version: 2,
|
|
235
|
+
entries: { ...previous.entries },
|
|
236
|
+
pending: { ...previous.pending },
|
|
237
|
+
};
|
|
238
|
+
for (const [key, row] of collapseAccountRows(rows)) {
|
|
239
|
+
const before = previous.entries[key]?.verdict;
|
|
240
|
+
const after = row.health.verdict;
|
|
241
|
+
next.entries[key] = { verdict: after, checkedAt: row.health.checkedAt };
|
|
242
|
+
if (before === 'live' && (after === 'expired' || after === 'revoked') && !next.pending[key]) {
|
|
243
|
+
next.pending[key] = {
|
|
244
|
+
agent: row.agent,
|
|
245
|
+
account: row.account ?? row.version,
|
|
246
|
+
verdict: after,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Durable BEFORE delivery: a pending entry survives a crash mid-notify.
|
|
251
|
+
await writeTransitionState(file, next);
|
|
252
|
+
const delivered = [];
|
|
253
|
+
const failures = [];
|
|
254
|
+
for (const [key, transition] of Object.entries(next.pending)) {
|
|
255
|
+
try {
|
|
256
|
+
await notify(transition);
|
|
257
|
+
delete next.pending[key];
|
|
258
|
+
delivered.push(transition);
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
failures.push(`${transition.agent} account ${transition.account}: ${error.message}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
await writeTransitionState(file, next);
|
|
265
|
+
if (failures.length > 0) {
|
|
266
|
+
throw new Error(`Account expiry notification failed (kept in the outbox for the next tick): ${failures.join('; ')}`);
|
|
267
|
+
}
|
|
268
|
+
return delivered;
|
|
269
|
+
}
|
|
270
|
+
async function postImportantAccountTransition(transition) {
|
|
271
|
+
const title = `${transition.agent} account ${transition.verdict}`;
|
|
272
|
+
const text = `${transition.agent} account ${transition.account} changed from live to ${transition.verdict}. Run agents accounts list ${transition.agent} for the exact fix.`;
|
|
273
|
+
const args = [...FEED_POST_LAUNCH.args,
|
|
274
|
+
'feed', 'post',
|
|
275
|
+
'--session', 'account-state-daemon',
|
|
276
|
+
'--title', title,
|
|
277
|
+
'--level', 'important',
|
|
278
|
+
text,
|
|
279
|
+
];
|
|
280
|
+
const result = await execFileBounded(FEED_POST_LAUNCH.command, args, { timeoutMs: NOTIFY_DEADLINE_MS });
|
|
281
|
+
if (result.code !== 0) {
|
|
282
|
+
const detail = result.timedOut ? 'timed out' : (result.stderr.trim() || `exit ${result.code}`);
|
|
283
|
+
throw new Error(`Account expiry feed notification failed: ${detail}`);
|
|
88
284
|
}
|
|
89
285
|
}
|
|
@@ -22,7 +22,7 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
22
22
|
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
23
23
|
}
|
|
24
24
|
async onTick(ctx) {
|
|
25
|
-
const { publishReservedAuthVerdict, syncReservedAuthBundle } = await import('../secrets
|
|
25
|
+
const { publishReservedAuthVerdict, syncReservedAuthBundle } = await import('../secrets-policy.js');
|
|
26
26
|
const published = await publishReservedAuthVerdict();
|
|
27
27
|
if (published.error)
|
|
28
28
|
ctx.log('WARN', `auth-sync: verdict: ${published.error}`);
|
|
@@ -44,5 +44,32 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
44
44
|
for (const err of result.errors) {
|
|
45
45
|
ctx.log('WARN', `auth-sync: ${err.device}: ${err.message}`);
|
|
46
46
|
}
|
|
47
|
+
// Generalized per-account, per-key, per-role reserved-store sync (PHNX-3940
|
|
48
|
+
// T6). On the elected headed publisher this pushes every portable account's
|
|
49
|
+
// reserved `__<harness>__` store to the worker peers missing it; on a worker
|
|
50
|
+
// it materializes a slot for each account whose durable key has landed. Each
|
|
51
|
+
// self-gates on device role, so exactly one arm acts per box. The push is the
|
|
52
|
+
// only transport — provisioning writes only locally (invariant 1).
|
|
53
|
+
const { syncReservedStores, reconcileLocalWorkerSlots } = await import('../secrets-policy.js');
|
|
54
|
+
try {
|
|
55
|
+
const stores = await syncReservedStores();
|
|
56
|
+
for (const p of stores.pushed)
|
|
57
|
+
ctx.log('INFO', `auth-sync: pushed ${p.bundle} (${p.keys.length} key(s)) to ${p.device}`);
|
|
58
|
+
for (const err of stores.errors)
|
|
59
|
+
ctx.log('WARN', `auth-sync: reserved-store ${err.device}: ${err.message}`);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
ctx.log('WARN', `auth-sync: reserved-store sync: ${err.message}`);
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const slots = reconcileLocalWorkerSlots();
|
|
66
|
+
if (slots.provisioned.length > 0)
|
|
67
|
+
ctx.log('INFO', `auth-sync: provisioned worker slot(s) for ${slots.provisioned.join(', ')}`);
|
|
68
|
+
for (const err of slots.errors)
|
|
69
|
+
ctx.log('WARN', `auth-sync: worker slot ${err.accountId}: ${err.message}`);
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
|
|
73
|
+
}
|
|
47
74
|
}
|
|
48
75
|
}
|
|
@@ -53,15 +53,6 @@ export declare function daemonSystemdUnitName(): string;
|
|
|
53
53
|
* start` is the deliberate override and is never gated by this.
|
|
54
54
|
*/
|
|
55
55
|
export declare const DAEMON_AUTOSTART_FAILURE_LIMIT = 5;
|
|
56
|
-
/**
|
|
57
|
-
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
58
|
-
* broker. The startup host decision is one-shot; this drives the periodic
|
|
59
|
-
* self-heal re-check. Take over ONLY when the daemon is not already hosting AND
|
|
60
|
-
* no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
|
|
61
|
-
* start has since died or crash-looped. Never take over while our in-process
|
|
62
|
-
* broker is hosting, and never clobber a reachable (healthy) broker.
|
|
63
|
-
*/
|
|
64
|
-
export declare function shouldTakeOverBroker(isHosting: boolean, brokerReachable: boolean): boolean;
|
|
65
56
|
/**
|
|
66
57
|
* What a gate re-evaluation must do with the routines scheduler. The daemon
|
|
67
58
|
* re-evaluates `scheduler.enabled` on every SIGHUP reload so flipping the key
|
|
@@ -436,13 +427,12 @@ export declare function findSurvivingStateDirDaemons(exclude: Set<number>): numb
|
|
|
436
427
|
* from launchd/systemd if applicable.
|
|
437
428
|
*
|
|
438
429
|
* The SIGTERM → grace → killTree sequence is unchanged; what it adds is
|
|
439
|
-
* verification. After the daemon is gone it checks that the
|
|
440
|
-
*
|
|
441
|
-
*
|
|
442
|
-
*
|
|
443
|
-
*
|
|
444
|
-
*
|
|
445
|
-
* left stale; this reclaims each (the owner is provably dead) and reports it. It
|
|
430
|
+
* verification. After the daemon is gone it checks that the browser IPC
|
|
431
|
+
* binding actually released — a stale socket present on disk but with no live
|
|
432
|
+
* owner is the orphan that keeps clients hanging on it — and that no
|
|
433
|
+
* `__daemon-run` for THIS state dir survives. A killTree escalation exits
|
|
434
|
+
* without running the daemon's graceful handleShutdown, so that socket can be
|
|
435
|
+
* left stale; this reclaims it (the owner is provably dead) and reports it. It
|
|
446
436
|
* never reports success on an unverified stop.
|
|
447
437
|
*/
|
|
448
438
|
export declare function stopDaemon(): DaemonStopResult;
|
|
@@ -24,7 +24,6 @@ import { notifyRoutineStart, notifyRoutineFinish, notifyRoutineStartFailed } fro
|
|
|
24
24
|
import { notifyOwnerRoutineFinish, notifyOwnerRoutineStartFailed } from '../routine-notify-owner.js';
|
|
25
25
|
import { BrowserService } from '../browser/service.js';
|
|
26
26
|
import { getSocketPath as getBrowserIpcSocketPath } from '../browser/ipc.js';
|
|
27
|
-
import { secretsBrokerSocketPath, brokerPidAlive } from '../secrets/agent.js';
|
|
28
27
|
import { redactSecrets } from '../redact.js';
|
|
29
28
|
import { getAgentsBinPath, getCliLaunch, BUN_VIRTUAL_ROOT } from '../cli-entry.js';
|
|
30
29
|
import { localBinDir } from '../platform/posixpath.js';
|
|
@@ -33,7 +32,6 @@ import { recordSubsystemOk, recordSubsystemError, recordSubsystemErrorReason, re
|
|
|
33
32
|
import { ServiceSupervisor } from './supervisor.js';
|
|
34
33
|
import { SessionIndexService } from './session-index-service.js';
|
|
35
34
|
import { SessionSummarizerService } from './session-summarizer-service.js';
|
|
36
|
-
import { SecretsBrokerService } from './secrets-broker-service.js';
|
|
37
35
|
import { MonitorEngineService } from './monitor-engine-service.js';
|
|
38
36
|
import { AccountUsageService, AccountAuthService } from './account-state-daemon-service.js';
|
|
39
37
|
import { CatchupService } from './catchup-service.js';
|
|
@@ -43,7 +41,6 @@ import { DeviceProbeService } from './device-probe-service.js';
|
|
|
43
41
|
import { SelfHealService } from './self-heal-service.js';
|
|
44
42
|
import { SelfUpdateService } from './self-update-service.js';
|
|
45
43
|
import { HarnessUpdateService } from './harness-update-service.js';
|
|
46
|
-
import { KeychainReapService } from './keychain-reap-service.js';
|
|
47
44
|
import { AuthSyncService } from './auth-sync-service.js';
|
|
48
45
|
import { UsageSyncService } from './usage-sync-service.js';
|
|
49
46
|
import { StateDirCheckService } from './state-dir-check-service.js';
|
|
@@ -125,18 +122,15 @@ export function daemonSystemdUnitName() {
|
|
|
125
122
|
/**
|
|
126
123
|
* Cadences for the in-process background ticks, named here beside the other
|
|
127
124
|
* tick constants rather than left as inline literals at their `setInterval`
|
|
128
|
-
* (RUSH-2423). Self-heal,
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
136
|
-
* - **Broker self-heal** is a bare `agentPing`, and the failure it recovers
|
|
137
|
-
* from wedges every keychain-backed secret on the box, so it runs minutely.
|
|
125
|
+
* (RUSH-2423). Self-heal, state-dir-check, watchdog, and device-probe
|
|
126
|
+
* cadences moved to their own `*-service.ts` files (RUSH-3193 P3, alongside
|
|
127
|
+
* session-index/account-state/etc.) — see those files for the per-service
|
|
128
|
+
* trade-offs (self-heal's 6h/cheap-to-be-late repair cadence, state-dir-check's
|
|
129
|
+
* env override for tests, watchdog/device-probe's shared 3min in-process
|
|
130
|
+
* housekeeping cadence, NOT a routine — RUSH-2495). The secrets broker and its
|
|
131
|
+
* self-heal/reap ticks moved out of this daemon entirely with the standalone
|
|
132
|
+
* `secrets` engine (PHNX-3989 OWN-1) — this daemon no longer hosts that broker.
|
|
138
133
|
*/
|
|
139
|
-
// BROKER_SELF_HEAL_TICK_MS moved to secrets-broker-service.ts (RUSH-3193 P2).
|
|
140
134
|
// Session-index warm interval/deadline live in session-index-service.ts now
|
|
141
135
|
// (RUSH-3193 — migrated onto ServiceSupervisor).
|
|
142
136
|
const WEDGE_THRESHOLD_TICKS = 3;
|
|
@@ -169,17 +163,6 @@ const DAEMON_START_LIMIT_BURST = 5;
|
|
|
169
163
|
* start` is the deliberate override and is never gated by this.
|
|
170
164
|
*/
|
|
171
165
|
export const DAEMON_AUTOSTART_FAILURE_LIMIT = 5;
|
|
172
|
-
/**
|
|
173
|
-
* RUSH-1817: decide whether the daemon should (re)take over hosting the secrets
|
|
174
|
-
* broker. The startup host decision is one-shot; this drives the periodic
|
|
175
|
-
* self-heal re-check. Take over ONLY when the daemon is not already hosting AND
|
|
176
|
-
* no healthy broker answers a ping — i.e. a standalone the daemon deferred to at
|
|
177
|
-
* start has since died or crash-looped. Never take over while our in-process
|
|
178
|
-
* broker is hosting, and never clobber a reachable (healthy) broker.
|
|
179
|
-
*/
|
|
180
|
-
export function shouldTakeOverBroker(isHosting, brokerReachable) {
|
|
181
|
-
return !isHosting && !brokerReachable;
|
|
182
|
-
}
|
|
183
166
|
export function schedulerGateTransition(running, enabled) {
|
|
184
167
|
if (running)
|
|
185
168
|
return enabled ? 'reload' : 'stop';
|
|
@@ -504,11 +487,10 @@ export function claimDaemonInstance() {
|
|
|
504
487
|
const existing = resolveLiveDaemonPid(true);
|
|
505
488
|
if (existing !== null && existing !== process.pid) {
|
|
506
489
|
// Evict, and WAIT for the incumbent to be provably dead — its graceful
|
|
507
|
-
// handleShutdown releasing the browser IPC binding
|
|
508
|
-
//
|
|
509
|
-
//
|
|
510
|
-
//
|
|
511
|
-
// prior owner is gone.
|
|
490
|
+
// handleShutdown releasing the browser IPC binding — before we write our
|
|
491
|
+
// pid and (later, in runDaemon) bind our own. Binding before the release
|
|
492
|
+
// recreates the two-owners-on-one-socket orphan documented at stopDaemon
|
|
493
|
+
// below, so the pid file is not written until the prior owner is gone.
|
|
512
494
|
if (!evictIncumbentDaemon(existing))
|
|
513
495
|
return false;
|
|
514
496
|
}
|
|
@@ -521,8 +503,10 @@ export function claimDaemonInstance() {
|
|
|
521
503
|
}
|
|
522
504
|
/**
|
|
523
505
|
* SIGTERM a live incumbent daemon and block until it is provably dead, so its
|
|
524
|
-
* graceful handleShutdown has released the browser IPC binding
|
|
525
|
-
*
|
|
506
|
+
* graceful handleShutdown has released the browser IPC binding BEFORE the
|
|
507
|
+
* newcomer binds anything of its own (SING-11) — the daemon no longer hosts a
|
|
508
|
+
* secrets broker socket to release (the standalone `secrets` CLI owns it now,
|
|
509
|
+
* PHNX-3989 OWN-1). Escalates
|
|
526
510
|
* to killTree after the grace window. Passes the POSITIVE pid so the kill reaches
|
|
527
511
|
* only the incumbent daemon — never its detached routine children, which run in
|
|
528
512
|
* their own process groups and must survive takeover (SING-11a); the new daemon
|
|
@@ -878,12 +862,12 @@ export function assertTestDaemonHome(daemonDir = getDaemonDir(), testHome = proc
|
|
|
878
862
|
// named in stack traces, readable without scrolling through runDaemon's
|
|
879
863
|
// 500-line body, and not recreated on every function invocation.
|
|
880
864
|
//
|
|
881
|
-
// self-heal
|
|
882
|
-
//
|
|
883
|
-
//
|
|
884
|
-
//
|
|
885
|
-
//
|
|
886
|
-
//
|
|
865
|
+
// self-heal moved to SelfHealService on the ServiceSupervisor (RUSH-3193 P3)
|
|
866
|
+
// — the supervisor's own per-tick deadline + inFlight guard replaces its local
|
|
867
|
+
// `healing` flag. state-dir-check moved to StateDirCheckService (RUSH-3193
|
|
868
|
+
// P3), registered after `handleShutdown` is declared — see its registration
|
|
869
|
+
// site below. The secrets broker (self-heal, reap, and hosting) moved out of
|
|
870
|
+
// this daemon entirely with the standalone `secrets` engine (PHNX-3989 OWN-1).
|
|
887
871
|
// ---------------------------------------------------------------------------
|
|
888
872
|
export async function runDaemon() {
|
|
889
873
|
// PHNX-2545 test-home tripwire — FIRST, before this daemon claims an instance,
|
|
@@ -999,21 +983,17 @@ export async function runDaemon() {
|
|
|
999
983
|
catch (err) {
|
|
1000
984
|
log('ERROR', `Stray daemon reaper failed: ${err.message}`);
|
|
1001
985
|
}
|
|
1002
|
-
// Socket services:
|
|
1003
|
-
//
|
|
1004
|
-
//
|
|
1005
|
-
//
|
|
1006
|
-
//
|
|
986
|
+
// Socket services: monitor engine, account-state, and browser IPC are all
|
|
987
|
+
// managed by the ServiceSupervisor (RUSH-3193 P2). The secrets broker moved
|
|
988
|
+
// with the standalone `secrets` engine (PHNX-3989 OWN-1) — this daemon no
|
|
989
|
+
// longer hosts or takes over that broker; the standalone owns its own
|
|
990
|
+
// lifecycle exclusively.
|
|
1007
991
|
const supervisor = new ServiceSupervisor();
|
|
1008
992
|
if (isEnabled('session-state')) {
|
|
1009
993
|
supervisor.register(new SessionStateService(() => supervisor.runNow('session-state')));
|
|
1010
994
|
}
|
|
1011
995
|
else
|
|
1012
996
|
log('INFO', 'Live session-state service disabled');
|
|
1013
|
-
if (isEnabled('secrets-broker'))
|
|
1014
|
-
supervisor.register(new SecretsBrokerService());
|
|
1015
|
-
else
|
|
1016
|
-
log('INFO', 'Secrets broker service disabled; daemon not hosting it');
|
|
1017
997
|
const monitorEngineSvc = new MonitorEngineService();
|
|
1018
998
|
if (isEnabled('monitors'))
|
|
1019
999
|
supervisor.register(monitorEngineSvc);
|
|
@@ -1071,10 +1051,10 @@ export async function runDaemon() {
|
|
|
1071
1051
|
supervisor.register(new SessionSummarizerService());
|
|
1072
1052
|
else
|
|
1073
1053
|
log('INFO', 'Session summarizer service disabled');
|
|
1074
|
-
// Watchdog, device-probe, self-heal
|
|
1075
|
-
//
|
|
1076
|
-
// the
|
|
1077
|
-
//
|
|
1054
|
+
// Watchdog, device-probe, and self-heal are all periodic services managed
|
|
1055
|
+
// by the ServiceSupervisor (RUSH-3193 P3). Each is gated the same way as
|
|
1056
|
+
// the socket services above; state-dir-check is registered separately,
|
|
1057
|
+
// later, after `handleShutdown` exists (see below).
|
|
1078
1058
|
if (isEnabled('watchdog'))
|
|
1079
1059
|
supervisor.register(new WatchdogService());
|
|
1080
1060
|
else
|
|
@@ -1095,10 +1075,6 @@ export async function runDaemon() {
|
|
|
1095
1075
|
supervisor.register(new HarnessUpdateService());
|
|
1096
1076
|
else
|
|
1097
1077
|
log('INFO', 'Harness-update service disabled');
|
|
1098
|
-
if (isEnabled('keychain-reap'))
|
|
1099
|
-
supervisor.register(new KeychainReapService());
|
|
1100
|
-
else
|
|
1101
|
-
log('INFO', 'Keychain-reap service disabled');
|
|
1102
1078
|
if (isEnabled('auth-sync'))
|
|
1103
1079
|
supervisor.register(new AuthSyncService());
|
|
1104
1080
|
else
|
|
@@ -1129,7 +1105,7 @@ export async function runDaemon() {
|
|
|
1129
1105
|
activeServiceSupervisor = supervisor;
|
|
1130
1106
|
// scheduler.enabled=false in this machine's device doc means NO routines fire
|
|
1131
1107
|
// here — the scheduler and its catchup recovery simply never start, while the
|
|
1132
|
-
// daemon keeps its other duties (
|
|
1108
|
+
// daemon keeps its other duties (browser IPC, session sync).
|
|
1133
1109
|
// The refusal message is the same one the start surfaces
|
|
1134
1110
|
// (`routines add` auto-start, manual `routines start`) raise. The gate is
|
|
1135
1111
|
// re-evaluated on every SIGHUP reload (handleReload below) via
|
|
@@ -1338,22 +1314,23 @@ export async function runDaemon() {
|
|
|
1338
1314
|
// Browser orphan reap and IPC server start are now managed by BrowserIPCService
|
|
1339
1315
|
// on the supervisor (RUSH-3193 P2). The orphan reap runs inside onStart().
|
|
1340
1316
|
// Webhook receivers: signed webhook receiver(s) + their funnel (RUSH-2548).
|
|
1341
|
-
//
|
|
1342
|
-
//
|
|
1343
|
-
// unless daemon/webhooks.yaml declares a receiver, so an
|
|
1317
|
+
// Resolves each receiver's signing secret headlessly through the standalone
|
|
1318
|
+
// `secrets` CLI (an agentOnly secrets-client read) — no AGENTS_SECRETS_PASSPHRASE,
|
|
1319
|
+
// no nohup. Binds nothing unless daemon/webhooks.yaml declares a receiver, so an
|
|
1320
|
+
// unconfigured box no-ops.
|
|
1344
1321
|
// Signed webhook ingress is owned by WebhookReceiverService, including
|
|
1345
1322
|
// per-service failure isolation, measured health, and shutdown cleanup.
|
|
1346
1323
|
// Resource self-heal is now managed by SelfHealService on the supervisor
|
|
1347
1324
|
// (RUSH-3193 P3), registered above alongside the socket services. The
|
|
1348
1325
|
// supervisor's immediate first tick on start replaces the old
|
|
1349
1326
|
// SELF_HEAL_KICKOFF_MS (30s) delayed kickoff timer — see self-heal-service.ts.
|
|
1350
|
-
//
|
|
1351
|
-
//
|
|
1352
|
-
//
|
|
1353
|
-
//
|
|
1354
|
-
// RUSH-2501: reap tmux sessions whose panes are all dead.
|
|
1355
|
-
//
|
|
1356
|
-
//
|
|
1327
|
+
// The secrets broker's self-heal and keychain-reap ticks (formerly
|
|
1328
|
+
// SecretsBrokerService / KeychainReapService, RUSH-1817 / RUSH-2232) moved
|
|
1329
|
+
// out of this daemon entirely with the standalone `secrets` engine
|
|
1330
|
+
// (PHNX-3989 OWN-1) — the standalone owns its own broker lifecycle.
|
|
1331
|
+
// RUSH-2501: reap tmux sessions whose panes are all dead. Daemon-only
|
|
1332
|
+
// (single executor). Dead managed panes and their orphan helpers are
|
|
1333
|
+
// reaped by TmuxReapService.
|
|
1357
1334
|
// RUSH-2622: close abandoned browser-task tabs on the same 5-min cadence,
|
|
1358
1335
|
// reusing the daemon's long-lived BrowserService when browser IPC is enabled.
|
|
1359
1336
|
// Abandoned browser tasks are reaped by BrowserTaskReapService, sharing the
|
|
@@ -1501,7 +1478,7 @@ export async function runDaemon() {
|
|
|
1501
1478
|
});
|
|
1502
1479
|
// State-dir self-check (RUSH-2367 self-terminate guard) is registered on
|
|
1503
1480
|
// the supervisor here — AFTER `handleShutdown` above — rather than
|
|
1504
|
-
// alongside watchdog/device-probe/self-heal
|
|
1481
|
+
// alongside watchdog/device-probe/self-heal earlier. The
|
|
1505
1482
|
// supervisor fires an immediate first tick on `register()`+`start()`; doing
|
|
1506
1483
|
// that before `handleShutdown` exists would reference the const in its
|
|
1507
1484
|
// temporal dead zone the moment a mismatch is ever detected. Registering it
|
|
@@ -1661,9 +1638,8 @@ Environment=AGENTS_REAL_HOME=${realHome}
|
|
|
1661
1638
|
WantedBy=default.target`;
|
|
1662
1639
|
}
|
|
1663
1640
|
// Binary-resolution helpers (getAgentsBinPath / isNodeScriptEntry / getCliLaunch)
|
|
1664
|
-
// live in ./cli-entry.js — a leaf module
|
|
1665
|
-
//
|
|
1666
|
-
// getAgentsBinPath keep resolving.
|
|
1641
|
+
// live in ./cli-entry.js — a leaf module. Re-exported so existing
|
|
1642
|
+
// `from './daemon.js'` importers of getAgentsBinPath keep resolving.
|
|
1667
1643
|
export { getAgentsBinPath };
|
|
1668
1644
|
/**
|
|
1669
1645
|
* Ask the service manager for the daemon's live PID. Used as a fallback when
|
|
@@ -2207,13 +2183,12 @@ export function findSurvivingStateDirDaemons(exclude) {
|
|
|
2207
2183
|
* from launchd/systemd if applicable.
|
|
2208
2184
|
*
|
|
2209
2185
|
* The SIGTERM → grace → killTree sequence is unchanged; what it adds is
|
|
2210
|
-
* verification. After the daemon is gone it checks that the
|
|
2211
|
-
*
|
|
2212
|
-
*
|
|
2213
|
-
*
|
|
2214
|
-
*
|
|
2215
|
-
*
|
|
2216
|
-
* left stale; this reclaims each (the owner is provably dead) and reports it. It
|
|
2186
|
+
* verification. After the daemon is gone it checks that the browser IPC
|
|
2187
|
+
* binding actually released — a stale socket present on disk but with no live
|
|
2188
|
+
* owner is the orphan that keeps clients hanging on it — and that no
|
|
2189
|
+
* `__daemon-run` for THIS state dir survives. A killTree escalation exits
|
|
2190
|
+
* without running the daemon's graceful handleShutdown, so that socket can be
|
|
2191
|
+
* left stale; this reclaims it (the owner is provably dead) and reports it. It
|
|
2217
2192
|
* never reports success on an unverified stop.
|
|
2218
2193
|
*/
|
|
2219
2194
|
export function stopDaemon() {
|
|
@@ -2447,26 +2422,6 @@ function stopDaemonLocked() {
|
|
|
2447
2422
|
else {
|
|
2448
2423
|
released.push('browser IPC socket');
|
|
2449
2424
|
}
|
|
2450
|
-
// Secrets broker socket: released when it is gone, or still owned by a
|
|
2451
|
-
// DIFFERENT live broker (a standalone service the daemon never hosted). A
|
|
2452
|
-
// socket present with NO live owner is the orphan — reclaim it.
|
|
2453
|
-
const brokerSock = secretsBrokerSocketPath();
|
|
2454
|
-
if (!fs.existsSync(brokerSock)) {
|
|
2455
|
-
released.push('secrets broker socket');
|
|
2456
|
-
}
|
|
2457
|
-
else if (brokerPidAlive()) {
|
|
2458
|
-
released.push('secrets broker socket (standalone owner)');
|
|
2459
|
-
}
|
|
2460
|
-
else {
|
|
2461
|
-
try {
|
|
2462
|
-
fs.unlinkSync(brokerSock);
|
|
2463
|
-
}
|
|
2464
|
-
catch { /* raced with a fresh bind */ }
|
|
2465
|
-
if (fs.existsSync(brokerSock))
|
|
2466
|
-
surviving.push('secrets broker socket not released');
|
|
2467
|
-
else
|
|
2468
|
-
released.push('secrets broker socket (reclaimed)');
|
|
2469
|
-
}
|
|
2470
2425
|
}
|
|
2471
2426
|
// ── The state files a killed daemon cannot clean up itself (RUSH-2421) ─────
|
|
2472
2427
|
// handleShutdown removes the lifetime marker, the heartbeat and this pid's
|