@phnx-labs/agents-cli 1.22.116 → 1.22.118
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 +247 -0
- package/dist/bootstrap.js +4 -11
- package/dist/commands/browser.d.ts +52 -70
- package/dist/commands/browser.js +192 -3562
- package/dist/commands/daemon.d.ts +39 -2
- package/dist/commands/daemon.js +72 -73
- package/dist/commands/events.js +1 -1
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +104 -12
- package/dist/commands/repo.js +13 -49
- package/dist/commands/route.js +5 -5
- package/dist/commands/routines.js +39 -12
- package/dist/commands/sessions-render.d.ts +1 -1
- package/dist/commands/sessions-render.js +0 -1
- package/dist/commands/sessions.test-fixture.d.ts +0 -1
- package/dist/commands/sessions.test-fixture.js +0 -37
- package/dist/commands/setup-browser.d.ts +17 -12
- package/dist/commands/setup-browser.js +52 -87
- package/dist/commands/setup-preferences.d.ts +0 -17
- package/dist/commands/setup-preferences.js +18 -49
- package/dist/commands/setup-term.d.ts +4 -3
- package/dist/commands/setup-term.js +5 -4
- package/dist/commands/setup.js +11 -11
- package/dist/commands/ssh.js +10 -1
- package/dist/commands/sync.js +6 -44
- package/dist/commands/teams.js +0 -1
- package/dist/commands/update.d.ts +19 -0
- package/dist/commands/update.js +34 -6
- package/dist/commands/view.d.ts +12 -1
- package/dist/commands/view.js +47 -2
- package/dist/index.js +107 -26
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-catalog.d.ts +38 -0
- package/dist/lib/account-catalog.js +110 -26
- package/dist/lib/account-provider-registry.js +1 -1
- package/dist/lib/account-registry.d.ts +1 -1
- package/dist/lib/account-registry.js +36 -2
- package/dist/lib/accounting/rotate.js +22 -8
- package/dist/lib/accounting/usage-ingest.d.ts +6 -1
- package/dist/lib/accounting/usage-ingest.js +115 -45
- package/dist/lib/accounting/usage-sync.d.ts +134 -31
- package/dist/lib/accounting/usage-sync.js +227 -66
- package/dist/lib/accounting/usage.d.ts +16 -3
- package/dist/lib/accounting/usage.js +26 -8
- package/dist/lib/accounts/slots.d.ts +8 -0
- package/dist/lib/accounts/slots.js +17 -0
- package/dist/lib/add-dir.js +0 -1
- package/dist/lib/agent-cli-commands.js +0 -1
- package/dist/lib/agent-spec/agents.d.ts +6 -7
- package/dist/lib/agent-spec/agents.js +6 -60
- package/dist/lib/auth-health.d.ts +69 -1
- package/dist/lib/auth-health.js +133 -4
- package/dist/lib/browser/context.d.ts +82 -0
- package/dist/lib/browser/context.js +88 -0
- package/dist/lib/browser/paths.d.ts +25 -0
- package/dist/lib/browser/paths.js +51 -0
- package/dist/lib/browser/record.d.ts +38 -0
- package/dist/lib/browser/record.js +57 -0
- package/dist/lib/browser/sessions-list.js +1 -2
- package/dist/lib/browser-client.d.ts +158 -0
- package/dist/lib/browser-client.js +234 -0
- package/dist/lib/channels/owner-sink.d.ts +1 -18
- package/dist/lib/channels/owner-sink.js +29 -82
- package/dist/lib/channels/providers/rush.d.ts +7 -7
- package/dist/lib/channels/providers/rush.js +127 -57
- package/dist/lib/claude-statusline.d.ts +1 -1
- package/dist/lib/claude-statusline.js +15 -3
- package/dist/lib/commands.js +1 -1
- package/dist/lib/crabbox/lease.js +1 -1
- package/dist/lib/crabbox/runtimes.js +1 -3
- package/dist/lib/daemon/account-state-daemon-service.js +1 -0
- package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
- package/dist/lib/daemon/auth-sync-service.js +37 -43
- package/dist/lib/daemon/daemon.d.ts +25 -14
- package/dist/lib/daemon/daemon.js +92 -154
- package/dist/lib/daemon/runner.d.ts +2 -2
- package/dist/lib/daemon/runner.js +32 -9
- package/dist/lib/daemon/self-update-service.d.ts +35 -34
- package/dist/lib/daemon/self-update-service.js +40 -36
- package/dist/lib/daemon/service.d.ts +12 -8
- package/dist/lib/daemon/supervisor.d.ts +65 -54
- package/dist/lib/daemon/supervisor.js +136 -171
- package/dist/lib/daemon/testdata/process-view-start.js +5 -2
- package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
- package/dist/lib/daemon/usage-sync-service.js +46 -78
- package/dist/lib/daemon-health.d.ts +22 -5
- package/dist/lib/daemon-health.js +47 -3
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +0 -10
- package/dist/lib/daemon-ticks.js +4 -2
- package/dist/lib/devices/doctor-findings.js +8 -11
- package/dist/lib/devices/harness-inventory.js +14 -5
- package/dist/lib/exec-account-home.js +5 -0
- package/dist/lib/exec.js +0 -11
- package/dist/lib/feed/events.js +0 -1
- package/dist/lib/feed/tool-activity.js +5 -2
- package/dist/lib/fleet-shared-state.d.ts +45 -0
- package/dist/lib/fleet-shared-state.js +78 -9
- package/dist/lib/fs-atomic.js +25 -0
- package/dist/lib/harness-auth-capabilities.js +0 -1
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hooks/cache.d.ts +7 -0
- package/dist/lib/hooks/cache.js +27 -2
- package/dist/lib/hooks/install.js +7 -1
- package/dist/lib/hosts/ready.d.ts +38 -8
- package/dist/lib/hosts/ready.js +62 -14
- package/dist/lib/hosts/remote-cmd.js +1 -0
- package/dist/lib/installations/active-check.d.ts +27 -0
- package/dist/lib/installations/active-check.js +61 -0
- package/dist/lib/installations/migrate.js +6 -3
- package/dist/lib/installations/shims.js +9 -7
- package/dist/lib/installations/update.js +21 -15
- package/dist/lib/installations/versions.js +12 -1
- package/dist/lib/manifest.js +1 -1
- package/dist/lib/models.js +0 -2
- package/dist/lib/open-url.js +28 -54
- package/dist/lib/placement.d.ts +2 -0
- package/dist/lib/placement.js +5 -0
- package/dist/lib/profiles.js +0 -2
- package/dist/lib/refresh.js +1 -1
- package/dist/lib/routine-readiness.d.ts +35 -0
- package/dist/lib/routine-readiness.js +74 -16
- package/dist/lib/rules/compile.js +6 -0
- package/dist/lib/sandbox.js +0 -2
- package/dist/lib/secrets-policy.d.ts +89 -11
- package/dist/lib/secrets-policy.js +175 -28
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +65 -18
- package/dist/lib/session/discover.js +1 -283
- package/dist/lib/session/mirror.d.ts +4 -3
- package/dist/lib/session/mirror.js +4 -3
- package/dist/lib/session/parse.d.ts +0 -2
- package/dist/lib/session/parse.js +1 -138
- package/dist/lib/session/throughput.d.ts +1 -3
- package/dist/lib/session/throughput.js +0 -20
- package/dist/lib/session/types.d.ts +2 -2
- package/dist/lib/session/types.js +2 -2
- package/dist/lib/sessions-client.d.ts +60 -1
- package/dist/lib/sessions-client.js +227 -4
- package/dist/lib/setup-tool-status.js +8 -12
- package/dist/lib/signin-badge.d.ts +3 -2
- package/dist/lib/signin-badge.js +6 -3
- package/dist/lib/smart-launch.js +4 -1
- package/dist/lib/staleness/detectors/commands.d.ts +1 -1
- package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
- package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
- package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
- package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
- package/dist/lib/staleness/detectors/rules.d.ts +1 -1
- package/dist/lib/staleness/detectors/skills.d.ts +1 -1
- package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
- package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
- package/dist/lib/staleness/writers/commands.d.ts +1 -1
- package/dist/lib/staleness/writers/hooks.d.ts +1 -1
- package/dist/lib/staleness/writers/mcp.d.ts +1 -1
- package/dist/lib/staleness/writers/permissions.d.ts +1 -1
- package/dist/lib/staleness/writers/plugins.d.ts +1 -1
- package/dist/lib/staleness/writers/rules.d.ts +1 -1
- package/dist/lib/staleness/writers/skills.d.ts +1 -1
- package/dist/lib/staleness/writers/sources.js +5 -0
- package/dist/lib/staleness/writers/subagents.d.ts +1 -1
- package/dist/lib/staleness/writers/workflows.d.ts +1 -1
- package/dist/lib/state.d.ts +6 -4
- package/dist/lib/state.js +7 -17
- package/dist/lib/teams/parsers.d.ts +1 -1
- package/dist/lib/teams/parsers.js +1 -136
- package/dist/lib/teams/placement-probe.js +50 -15
- package/dist/lib/teams/scheduler.d.ts +7 -0
- package/dist/lib/types.d.ts +10 -1
- package/dist/lib/types.js +1 -1
- package/dist/lib/view-types.d.ts +32 -0
- package/dist/lib/watchdog/read.js +0 -1
- package/dist/lib/watchdog/rotate.js +1 -2
- package/dist/lib/watchdog/runner.js +2 -2
- package/dist/lib/watchdog/watchdog.d.ts +1 -1
- package/dist/lib/watchdog/watchdogTail.js +0 -31
- package/package.json +1 -1
- package/scripts/postinstall.js +8 -8
- package/dist/browser.d.ts +0 -2
- package/dist/browser.js +0 -17
- package/dist/commands/browser-picker.d.ts +0 -18
- package/dist/commands/browser-picker.js +0 -97
- package/dist/lib/browser/arc-discovery.d.ts +0 -52
- package/dist/lib/browser/arc-discovery.js +0 -197
- package/dist/lib/browser/arc-dom.d.ts +0 -14
- package/dist/lib/browser/arc-dom.js +0 -121
- package/dist/lib/browser/caller-identity.d.ts +0 -40
- package/dist/lib/browser/caller-identity.js +0 -175
- package/dist/lib/browser/cdp.d.ts +0 -51
- package/dist/lib/browser/cdp.js +0 -254
- package/dist/lib/browser/chrome.d.ts +0 -141
- package/dist/lib/browser/chrome.js +0 -704
- package/dist/lib/browser/chromium-discovery.d.ts +0 -28
- package/dist/lib/browser/chromium-discovery.js +0 -96
- package/dist/lib/browser/devices.d.ts +0 -34
- package/dist/lib/browser/devices.js +0 -61
- package/dist/lib/browser/domain-skills.d.ts +0 -71
- package/dist/lib/browser/domain-skills.js +0 -195
- package/dist/lib/browser/drivers/arc.d.ts +0 -40
- package/dist/lib/browser/drivers/arc.js +0 -276
- package/dist/lib/browser/drivers/firefox.d.ts +0 -99
- package/dist/lib/browser/drivers/firefox.js +0 -377
- package/dist/lib/browser/drivers/local.d.ts +0 -55
- package/dist/lib/browser/drivers/local.js +0 -266
- package/dist/lib/browser/drivers/ssh.d.ts +0 -120
- package/dist/lib/browser/drivers/ssh.js +0 -467
- package/dist/lib/browser/editor.d.ts +0 -3
- package/dist/lib/browser/editor.js +0 -50
- package/dist/lib/browser/ffmpeg.d.ts +0 -12
- package/dist/lib/browser/ffmpeg.js +0 -184
- package/dist/lib/browser/firefox-discovery.d.ts +0 -69
- package/dist/lib/browser/firefox-discovery.js +0 -162
- package/dist/lib/browser/har.d.ts +0 -85
- package/dist/lib/browser/har.js +0 -77
- package/dist/lib/browser/hygiene.d.ts +0 -97
- package/dist/lib/browser/hygiene.js +0 -153
- package/dist/lib/browser/index.d.ts +0 -5
- package/dist/lib/browser/index.js +0 -5
- package/dist/lib/browser/input.d.ts +0 -7
- package/dist/lib/browser/input.js +0 -92
- package/dist/lib/browser/ipc.d.ts +0 -204
- package/dist/lib/browser/ipc.js +0 -1453
- package/dist/lib/browser/login-detection.d.ts +0 -87
- package/dist/lib/browser/login-detection.js +0 -274
- package/dist/lib/browser/profiles.d.ts +0 -422
- package/dist/lib/browser/profiles.js +0 -1157
- package/dist/lib/browser/refs.d.ts +0 -89
- package/dist/lib/browser/refs.js +0 -191
- package/dist/lib/browser/registry.d.ts +0 -70
- package/dist/lib/browser/registry.js +0 -252
- package/dist/lib/browser/remote-control.d.ts +0 -59
- package/dist/lib/browser/remote-control.js +0 -85
- package/dist/lib/browser/resolve-target.d.ts +0 -66
- package/dist/lib/browser/resolve-target.js +0 -264
- package/dist/lib/browser/runtime-state.d.ts +0 -291
- package/dist/lib/browser/runtime-state.js +0 -584
- package/dist/lib/browser/secret-ref.d.ts +0 -10
- package/dist/lib/browser/secret-ref.js +0 -14
- package/dist/lib/browser/service.d.ts +0 -812
- package/dist/lib/browser/service.js +0 -4693
- package/dist/lib/browser/stream.d.ts +0 -17
- package/dist/lib/browser/stream.js +0 -72
- package/dist/lib/browser/task-index.d.ts +0 -78
- package/dist/lib/browser/task-index.js +0 -199
- package/dist/lib/browser/types.d.ts +0 -647
- package/dist/lib/browser/types.js +0 -61
- package/dist/lib/browser/upload.d.ts +0 -29
- package/dist/lib/browser/upload.js +0 -298
- package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
- package/dist/lib/daemon/browser-ipc-service.js +0 -46
- package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
- package/dist/lib/daemon/browser-task-reap-service.js +0 -26
- package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
- package/dist/lib/fleet-shared-repo-sync.js +0 -490
|
@@ -2,99 +2,46 @@
|
|
|
2
2
|
* Owner-delivery-sink reachability probe (RUSH-2262).
|
|
3
3
|
*
|
|
4
4
|
* The feed/notify owner-delivery lane (`agents send --to owner`, `agents feed post
|
|
5
|
-
* --level important` / `--blocked`) reaches the owner
|
|
6
|
-
*
|
|
7
|
-
* from a context that BOTH finds the `rush` CLI on PATH and can read its
|
|
8
|
-
* keychain-bound session. So a headless Linux fleet box (no rush) or a non-GUI
|
|
9
|
-
* SSH session on a mac (login keychain locked) structurally cannot escalate —
|
|
10
|
-
* and the failure is silent until a block is filed, surfacing only as the
|
|
11
|
-
* after-the-fact `owner failed: …` line. `agents doctor` had no signal for it,
|
|
12
|
-
* which is exactly the gap RUSH-2258 / RUSH-2262 flagged.
|
|
5
|
+
* --level important` / `--blocked`) reaches the owner through the channel
|
|
6
|
+
* providers registered for each configured channel in humans.yaml.
|
|
13
7
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
8
|
+
* Transport capabilities differ by channel and platform:
|
|
9
|
+
* - **imessage:** macOS only (osascript → Messages.app). Linux boxes rely on
|
|
10
|
+
* the peer-forward in owner-forward.ts to reach a macOS peer.
|
|
11
|
+
* - **slack:** requires a `SLACK_BOT_TOKEN` in env or the `webhooks` secrets
|
|
12
|
+
* bundle. Platform-independent.
|
|
13
|
+
* - **telegram / discord:** requires the Rush daemon (removed); only
|
|
14
|
+
* openclaw-telegram is available as a Telegram alternative.
|
|
20
15
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* token is a keychain item, NOT `~/.rush/user.yaml` (RUSH-2262), so this never
|
|
26
|
-
* reads that file — checking it is the mistake that made a signed-in box look
|
|
27
|
-
* signed out.
|
|
28
|
-
*
|
|
29
|
-
* `agents send --to owner --dry-run` is NOT this probe: it short-circuits before the
|
|
30
|
-
* `which rush` preflight (`providers/rush.ts`), so it reports `ok:true` on a box
|
|
31
|
-
* with no rush at all. Resolvability (does the envelope build?) and reachability
|
|
32
|
-
* (can this box actually deliver?) are different questions; this answers the
|
|
33
|
-
* second.
|
|
16
|
+
* `agents send --to owner --dry-run` is NOT this probe: dry-run short-circuits
|
|
17
|
+
* before the capability check. Resolvability (does the envelope build?) and
|
|
18
|
+
* reachability (can this box actually deliver?) are different questions; this
|
|
19
|
+
* answers the second.
|
|
34
20
|
*/
|
|
35
|
-
import {
|
|
36
|
-
import { promisify } from 'util';
|
|
21
|
+
import { platform } from 'os';
|
|
37
22
|
import { readOwnerDest } from './send.js';
|
|
38
|
-
import { RUSH_CHANNELS } from './providers/rush.js';
|
|
39
|
-
const execFileAsync = promisify(execFile);
|
|
40
|
-
async function rushOnPath() {
|
|
41
|
-
try {
|
|
42
|
-
await execFileAsync('which', ['rush'], { timeout: 4000 });
|
|
43
|
-
return true;
|
|
44
|
-
}
|
|
45
|
-
catch {
|
|
46
|
-
return false;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/** Signed-in state from `rush whoami`, read with THIS context's keychain access.
|
|
50
|
-
* 'unknown' (a timeout or output we can't classify) is treated as reachable by
|
|
51
|
-
* the caller — a slow or unexpected probe must not cry wolf and paint a false
|
|
52
|
-
* critical. Only a definitive "not logged in" is reported as signed out. */
|
|
53
|
-
async function rushSignedIn() {
|
|
54
|
-
const classify = (s) => {
|
|
55
|
-
const out = s.toLowerCase();
|
|
56
|
-
if (/not (logged in|signed in)/.test(out))
|
|
57
|
-
return 'no';
|
|
58
|
-
if (/logged in as|session:\s*valid/.test(out))
|
|
59
|
-
return 'yes';
|
|
60
|
-
return 'unknown';
|
|
61
|
-
};
|
|
62
|
-
try {
|
|
63
|
-
const { stdout, stderr } = await execFileAsync('rush', ['whoami'], { timeout: 5000 });
|
|
64
|
-
return classify(`${stdout}\n${stderr}`);
|
|
65
|
-
}
|
|
66
|
-
catch (err) {
|
|
67
|
-
const e = err;
|
|
68
|
-
if (e.killed)
|
|
69
|
-
return 'unknown'; // timed out — do not conclude signed-out
|
|
70
|
-
// A signed-out rush commonly exits non-zero; trust an explicit message only.
|
|
71
|
-
return classify(`${e.stdout ?? ''}\n${e.stderr ?? ''}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Probe whether THIS box can deliver an owner notification right now. Returns
|
|
76
|
-
* `configured:false` (and the caller emits no finding) when the fleet has no
|
|
77
|
-
* owner channel configured — an un-opted-in box is not broken. When configured,
|
|
78
|
-
* reports whether the resolved transport can actually deliver from here.
|
|
79
|
-
*
|
|
80
|
-
* Only the concrete rush-backed failure the lane hits is reported as unreachable;
|
|
81
|
-
* non-rush transports (desktop / mailbox / …) deliver locally and are treated as
|
|
82
|
-
* reachable rather than probed, so this never invents a critical it cannot back.
|
|
83
|
-
*/
|
|
23
|
+
import { RUSH_CHANNELS, resolveSlackToken } from './providers/rush.js';
|
|
84
24
|
export async function probeOwnerSink(meta) {
|
|
85
25
|
const dest = readOwnerDest(meta);
|
|
86
26
|
if (!dest)
|
|
87
27
|
return { configured: false, reachable: false };
|
|
88
28
|
const channel = dest.channel;
|
|
89
29
|
const transport = meta.notify?.transports?.[channel] ?? channel;
|
|
90
|
-
if (RUSH_CHANNELS.includes(transport)) {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
if ((
|
|
95
|
-
return { configured: true, reachable: false, channel, transport, reason: '
|
|
30
|
+
if (!RUSH_CHANNELS.includes(transport)) {
|
|
31
|
+
return { configured: true, reachable: true, channel, transport };
|
|
32
|
+
}
|
|
33
|
+
if (transport === 'imessage') {
|
|
34
|
+
if (platform() !== 'darwin') {
|
|
35
|
+
return { configured: true, reachable: false, channel, transport, reason: 'imessage-not-macos' };
|
|
96
36
|
}
|
|
97
37
|
return { configured: true, reachable: true, channel, transport };
|
|
98
38
|
}
|
|
99
|
-
|
|
39
|
+
if (transport === 'slack') {
|
|
40
|
+
if (resolveSlackToken()) {
|
|
41
|
+
return { configured: true, reachable: true, channel, transport };
|
|
42
|
+
}
|
|
43
|
+
return { configured: true, reachable: false, channel, transport, reason: 'slack-no-token' };
|
|
44
|
+
}
|
|
45
|
+
// telegram / discord — daemon removed, no direct transport.
|
|
46
|
+
return { configured: true, reachable: false, channel, transport, reason: 'channel-unsupported' };
|
|
100
47
|
}
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ChannelProvider
|
|
2
|
-
type RushChannel = 'telegram' | 'imessage' | 'slack' | 'discord';
|
|
1
|
+
import type { ChannelProvider } from '../registry.js';
|
|
2
|
+
export type RushChannel = 'telegram' | 'imessage' | 'slack' | 'discord';
|
|
3
3
|
export declare const RUSH_CHANNELS: RushChannel[];
|
|
4
|
-
/** Build the
|
|
5
|
-
export declare function
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/** Build the osascript argv for sending an iMessage. */
|
|
5
|
+
export declare function buildImessageOsascriptArgs(text: string, phone: string): string[];
|
|
6
|
+
export declare function resolveSlackToken(): string | undefined;
|
|
7
|
+
/** Build the Slack chat.postMessage payload (exported for tests). */
|
|
8
|
+
export declare function buildSlackPayload(channel: string, text: string, thread?: string): Record<string, string>;
|
|
8
9
|
export declare const rushProviders: ChannelProvider[];
|
|
9
|
-
export {};
|
|
@@ -1,70 +1,140 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Channel providers formerly backed by the Rush daemon's live gateways.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The desktop-era `rush message send` (iMessage) and `rush send --channel`
|
|
5
|
+
* (Slack/Telegram/Discord) commands were removed with the daemon in the
|
|
6
|
+
* thin-client trim (PHNX-3839/3933). This module now delivers directly:
|
|
7
|
+
*
|
|
8
|
+
* - **imessage:** `osascript` → Messages.app (macOS only; Linux boxes
|
|
9
|
+
* rely on the peer-forward in owner-forward.ts to reach a macOS peer).
|
|
10
|
+
* - **slack:** Slack Web API `chat.postMessage` via `fetch()`, reading
|
|
11
|
+
* `SLACK_BOT_TOKEN` from env or the `webhooks` secrets bundle.
|
|
12
|
+
* - **telegram / discord:** error — no direct transport; openclaw-telegram
|
|
13
|
+
* is available as a separate provider for Telegram.
|
|
7
14
|
*/
|
|
8
15
|
import { execFile } from 'child_process';
|
|
16
|
+
import { platform } from 'os';
|
|
9
17
|
import { promisify } from 'util';
|
|
10
18
|
const execFileAsync = promisify(execFile);
|
|
11
19
|
export const RUSH_CHANNELS = ['telegram', 'imessage', 'slack', 'discord'];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
+
// ── iMessage via osascript ──────────────────────────────────────────────
|
|
21
|
+
function escapeAppleScript(s) {
|
|
22
|
+
return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
23
|
+
}
|
|
24
|
+
/** Build the osascript argv for sending an iMessage. */
|
|
25
|
+
export function buildImessageOsascriptArgs(text, phone) {
|
|
26
|
+
const escapedText = escapeAppleScript(text);
|
|
27
|
+
const escapedPhone = escapeAppleScript(phone);
|
|
28
|
+
const script = [
|
|
29
|
+
'tell application "Messages"',
|
|
30
|
+
` set targetBuddy to buddy "${escapedPhone}" of (first account whose service type is iMessage)`,
|
|
31
|
+
` send "${escapedText}" to targetBuddy`,
|
|
32
|
+
'end tell',
|
|
33
|
+
].join('\n');
|
|
34
|
+
return ['-e', script];
|
|
35
|
+
}
|
|
36
|
+
async function sendImessage(text, opts) {
|
|
37
|
+
const channel = 'imessage';
|
|
38
|
+
if (opts.dryRun)
|
|
39
|
+
return { ok: true, channel, id: opts.target };
|
|
40
|
+
if (platform() !== 'darwin') {
|
|
41
|
+
return { ok: false, channel, id: opts.target, error: 'iMessage requires macOS (peer-forward handles Linux delivery)' };
|
|
42
|
+
}
|
|
43
|
+
if ((opts.attachments?.length ?? 0) > 0) {
|
|
44
|
+
return { ok: false, channel, id: opts.target, error: 'iMessage attachments not supported via osascript' };
|
|
45
|
+
}
|
|
46
|
+
try {
|
|
47
|
+
await execFileAsync('osascript', buildImessageOsascriptArgs(text, opts.target), { timeout: 10_000 });
|
|
48
|
+
return { ok: true, channel, id: opts.target };
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
return { ok: false, channel, id: opts.target, error: err.message };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ── Slack via Web API ───────────────────────────────────────────────────
|
|
55
|
+
export function resolveSlackToken() {
|
|
56
|
+
// 1. Environment variable (set by `secrets exec` or the daemon).
|
|
57
|
+
if (process.env.SLACK_BOT_TOKEN) {
|
|
58
|
+
return process.env.SLACK_BOT_TOKEN;
|
|
59
|
+
}
|
|
60
|
+
// 2. Secrets bundle — the webhook receiver bundle carries SLACK_BOT_TOKEN
|
|
61
|
+
// when a Slack app is configured for this fleet.
|
|
62
|
+
try {
|
|
63
|
+
// Dynamic import to avoid a hard dependency on the secrets subsystem in
|
|
64
|
+
// contexts where it is unavailable (CI, containers without a secrets store).
|
|
65
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
66
|
+
const { readAndResolveBundleEnvSync } = require('../../secrets-client.js');
|
|
67
|
+
const { env } = readAndResolveBundleEnvSync('webhooks', { caller: 'slack-provider', agentOnly: true });
|
|
68
|
+
if (env.SLACK_BOT_TOKEN) {
|
|
69
|
+
return env.SLACK_BOT_TOKEN;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
// Bundle missing, store locked, or secrets CLI not available — not fatal.
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
/** Build the Slack chat.postMessage payload (exported for tests). */
|
|
78
|
+
export function buildSlackPayload(channel, text, thread) {
|
|
79
|
+
const payload = { channel, text };
|
|
80
|
+
if (thread)
|
|
81
|
+
payload.thread_ts = thread;
|
|
82
|
+
return payload;
|
|
20
83
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
84
|
+
async function sendSlack(text, opts) {
|
|
85
|
+
const channel = 'slack';
|
|
86
|
+
if (opts.dryRun)
|
|
87
|
+
return { ok: true, channel, id: opts.target };
|
|
88
|
+
const token = resolveSlackToken();
|
|
89
|
+
if (!token) {
|
|
90
|
+
return {
|
|
91
|
+
ok: false,
|
|
92
|
+
channel,
|
|
93
|
+
id: opts.target,
|
|
94
|
+
error: 'No SLACK_BOT_TOKEN — set it in env or the webhooks secrets bundle',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
try {
|
|
98
|
+
const payload = buildSlackPayload(opts.target, text, opts.thread);
|
|
99
|
+
const res = await fetch('https://slack.com/api/chat.postMessage', {
|
|
100
|
+
method: 'POST',
|
|
101
|
+
headers: {
|
|
102
|
+
'Authorization': `Bearer ${token}`,
|
|
103
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
104
|
+
},
|
|
105
|
+
body: JSON.stringify(payload),
|
|
106
|
+
});
|
|
107
|
+
const body = await res.json();
|
|
108
|
+
if (!body.ok) {
|
|
109
|
+
return { ok: false, channel, id: opts.target, error: `Slack API: ${body.error ?? 'unknown error'}` };
|
|
110
|
+
}
|
|
111
|
+
return { ok: true, channel, id: opts.target };
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
return { ok: false, channel, id: opts.target, error: err.message };
|
|
115
|
+
}
|
|
24
116
|
}
|
|
25
|
-
|
|
117
|
+
// ── Unsupported (daemon-era only) ───────────────────────────────────────
|
|
118
|
+
function unsupportedProvider(name) {
|
|
26
119
|
return {
|
|
27
|
-
name
|
|
28
|
-
async send(
|
|
29
|
-
if (opts.dryRun)
|
|
30
|
-
return { ok: true, channel, id: opts.target
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
return { ok: false, channel, id: opts.target, error: 'rush CLI not found on PATH' };
|
|
38
|
-
}
|
|
39
|
-
try {
|
|
40
|
-
if (channel === 'imessage' && opts.ownerScoped) {
|
|
41
|
-
if ((opts.attachments?.length ?? 0) > 0) {
|
|
42
|
-
return {
|
|
43
|
-
ok: false,
|
|
44
|
-
channel,
|
|
45
|
-
id: opts.target,
|
|
46
|
-
error: 'owner-scoped iMessage does not support attachments',
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
await execFileAsync('rush', buildRushOwnerMessageArgs(text));
|
|
50
|
-
return { ok: true, channel, id: opts.target };
|
|
51
|
-
}
|
|
52
|
-
const { stdout } = await execFileAsync('rush', buildRushSendArgs(channel, text, opts));
|
|
53
|
-
const parsed = JSON.parse(stdout);
|
|
54
|
-
return {
|
|
55
|
-
ok: parsed.ok === true,
|
|
56
|
-
channel,
|
|
57
|
-
id: opts.target,
|
|
58
|
-
attachments: parsed.attachments,
|
|
59
|
-
error: parsed.ok === true ? undefined : 'rush send returned ok=false',
|
|
60
|
-
};
|
|
61
|
-
}
|
|
62
|
-
catch (err) {
|
|
63
|
-
// Daemon down surfaces here as the exec error (ErrDaemonDown), as does a
|
|
64
|
-
// non-JSON stdout (parse throw). Fail loud — never silently reroute.
|
|
65
|
-
return { ok: false, channel, id: opts.target, error: err.message };
|
|
66
|
-
}
|
|
120
|
+
name,
|
|
121
|
+
async send(_text, opts) {
|
|
122
|
+
if (opts.dryRun)
|
|
123
|
+
return { ok: true, channel: name, id: opts.target };
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
channel: name,
|
|
127
|
+
id: opts.target,
|
|
128
|
+
error: `${name} delivery requires the Rush daemon (removed); use openclaw-telegram for Telegram`,
|
|
129
|
+
};
|
|
67
130
|
},
|
|
68
131
|
};
|
|
69
132
|
}
|
|
70
|
-
|
|
133
|
+
// ── Provider registry ───────────────────────────────────────────────────
|
|
134
|
+
const PROVIDERS = {
|
|
135
|
+
imessage: { name: 'imessage', send: sendImessage },
|
|
136
|
+
slack: { name: 'slack', send: sendSlack },
|
|
137
|
+
telegram: unsupportedProvider('telegram'),
|
|
138
|
+
discord: unsupportedProvider('discord'),
|
|
139
|
+
};
|
|
140
|
+
export const rushProviders = RUSH_CHANNELS.map((ch) => PROVIDERS[ch]);
|
|
@@ -30,7 +30,7 @@ interface ClaudeStatusLinePayload {
|
|
|
30
30
|
};
|
|
31
31
|
};
|
|
32
32
|
}
|
|
33
|
-
export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, identity: ClaudeHomeIdentity | null): boolean;
|
|
33
|
+
export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, identity: ClaudeHomeIdentity | null, versionHome?: string | null): boolean;
|
|
34
34
|
/**
|
|
35
35
|
* Where the running Claude keeps the config this render attributes to: the version
|
|
36
36
|
* home when the launch shim set CLAUDE_CONFIG_DIR, otherwise the real HOME — the
|
|
@@ -9,6 +9,7 @@ import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
|
|
|
9
9
|
import { loadReminders, pickReminderForSession } from './reminders.js';
|
|
10
10
|
import { readMeta } from './state.js';
|
|
11
11
|
import { machineId } from './machine-id.js';
|
|
12
|
+
import { recordRunAuthOutcome } from './auth-health.js';
|
|
12
13
|
export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
|
|
13
14
|
const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
|
|
14
15
|
// The private subcommand this feature runs. It is only ever invoked internally,
|
|
@@ -64,7 +65,7 @@ function windowFromNative(key, value) {
|
|
|
64
65
|
windowMinutes: key === 'session' ? 300 : 10_080,
|
|
65
66
|
};
|
|
66
67
|
}
|
|
67
|
-
export function ingestClaudeStatusLineUsage(payload, identity) {
|
|
68
|
+
export function ingestClaudeStatusLineUsage(payload, identity, versionHome) {
|
|
68
69
|
if (!identity?.usageKey || !payload.rate_limits)
|
|
69
70
|
return false;
|
|
70
71
|
const windows = [
|
|
@@ -80,8 +81,19 @@ export function ingestClaudeStatusLineUsage(payload, identity) {
|
|
|
80
81
|
windows,
|
|
81
82
|
freshness: { source: 'statusline', poller: machineId() },
|
|
82
83
|
});
|
|
84
|
+
// A status-line payload with rate_limits IS a real inference response — the
|
|
85
|
+
// token authenticated moments ago. Record that as the per-account auth FACT
|
|
86
|
+
// (`last used ok`), keyed to the account this box actually ran (PHNX-4116).
|
|
87
|
+
recordRunAuthOutcome({
|
|
88
|
+
agent: 'claude',
|
|
89
|
+
accountId: resolveNativeAccount(identity)?.id ?? null,
|
|
90
|
+
home: versionHome ?? null,
|
|
91
|
+
outcome: { ok: true },
|
|
92
|
+
});
|
|
93
|
+
// Keep this box's own state file current so the next placement probe or
|
|
94
|
+
// usage-sync fan-out carries this reading; the file is local, no transport.
|
|
83
95
|
void import('./accounting/usage-sync.js')
|
|
84
|
-
.then((mod) => mod.
|
|
96
|
+
.then((mod) => mod.publishUsageSnapshotToSharedStore())
|
|
85
97
|
.catch(() => { });
|
|
86
98
|
return true;
|
|
87
99
|
}
|
|
@@ -211,7 +223,7 @@ export async function runClaudeStatusLine() {
|
|
|
211
223
|
const versionHome = versionHomeFromEnv(process.env);
|
|
212
224
|
const identity = readClaudeIdentity(claudeHomeFromEnv(process.env));
|
|
213
225
|
if (versionHome)
|
|
214
|
-
ingestClaudeStatusLineUsage(payload, identity);
|
|
226
|
+
ingestClaudeStatusLineUsage(payload, identity, versionHome);
|
|
215
227
|
process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id), formatAccountPart(identity, resolveNativeAccount(identity))));
|
|
216
228
|
return 0;
|
|
217
229
|
}
|
package/dist/lib/commands.js
CHANGED
|
@@ -571,7 +571,7 @@ function commandContentMatches(agentId, commandName, sourcePath) {
|
|
|
571
571
|
* Get the project-scoped commands directory for an agent.
|
|
572
572
|
* Claude: .claude/commands/
|
|
573
573
|
* Codex: .codex/prompts/
|
|
574
|
-
*
|
|
574
|
+
* Cursor: .cursor/commands/
|
|
575
575
|
*/
|
|
576
576
|
function getProjectCommandsDirs(agentId, cwd = process.cwd()) {
|
|
577
577
|
const agent = AGENTS[agentId];
|
|
@@ -145,7 +145,7 @@ export function buildBootstrapScript(opts) {
|
|
|
145
145
|
// Runs regardless of --keep-box (it's in the box body, not teardown), so a kept
|
|
146
146
|
// box still loses the token after the run — minimizing the credential window.
|
|
147
147
|
const shredPaths = credentialRuntimes.flatMap((id) => {
|
|
148
|
-
const paths = { claude: ['.claude.json', CLAUDE_TOKEN_REMOTE], codex: ['.codex/auth.json'],
|
|
148
|
+
const paths = { claude: ['.claude.json', CLAUDE_TOKEN_REMOTE], codex: ['.codex/auth.json'], grok: ['.grok/auth.json'] }[id];
|
|
149
149
|
return paths ?? [];
|
|
150
150
|
});
|
|
151
151
|
if (opts.dispatchProfile)
|
|
@@ -22,13 +22,12 @@ import { listInstalledVersions, getVersionHomePath } from '../installations/vers
|
|
|
22
22
|
export const LEASE_RUNTIMES = [
|
|
23
23
|
{ id: 'claude', label: 'Claude Code', localCandidates: ['.claude/.claude.json', '.claude.json'], remote: '.claude.json' },
|
|
24
24
|
{ id: 'codex', label: 'Codex CLI', localCandidates: ['.codex/auth.json'], remote: '.codex/auth.json' },
|
|
25
|
-
{ id: 'gemini', label: 'Gemini CLI', localCandidates: ['.gemini/google_accounts.json'], remote: '.gemini/google_accounts.json' },
|
|
26
25
|
{ id: 'grok', label: 'Grok CLI', localCandidates: ['.grok/auth.json'], remote: '.grok/auth.json' },
|
|
27
26
|
];
|
|
28
27
|
/**
|
|
29
28
|
* Every runtime whose login this module can serialize (`LEASE_RUNTIMES`) is a
|
|
30
29
|
* native, rotating OAuth / session credential (Claude OAuth token, codex/grok
|
|
31
|
-
* `auth.json
|
|
30
|
+
* `auth.json`). The fleet-auth contract forbids
|
|
32
31
|
* copying any of them between devices — including to an ephemeral leased box —
|
|
33
32
|
* because a shared refresh token rotates server-side on the next refresh and
|
|
34
33
|
* invalidates every other copy (`docs/specifications.md` SING-1b,
|
|
@@ -142,7 +141,6 @@ const PROFILE_AUTH_ENV_KEYS_BY_RUNTIME = {
|
|
|
142
141
|
'ANTHROPIC_FOUNDRY_API_KEY',
|
|
143
142
|
],
|
|
144
143
|
codex: ['OPENAI_API_KEY'],
|
|
145
|
-
gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
|
|
146
144
|
grok: ['XAI_API_KEY', 'GROK_API_KEY'],
|
|
147
145
|
};
|
|
148
146
|
/** True when a profile already carries auth for its host runtime via env. */
|
|
@@ -3,25 +3,28 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This tick owns only the NON-git duties of auth sync: it materializes worker
|
|
5
5
|
* slots from durable keys already on the box, then (on the elected headed
|
|
6
|
-
* publisher) provisions peers
|
|
7
|
-
*
|
|
6
|
+
* publisher) provisions peers missing a reserved key by pushing the credential
|
|
7
|
+
* over SSH. The secret never enters Git.
|
|
8
8
|
*
|
|
9
|
-
* It no
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* (`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
9
|
+
* It runs no transport of its own (PHNX-4051): the per-account readiness
|
|
10
|
+
* verdicts it reads are published and delivered by the usage-sync tick's SSH
|
|
11
|
+
* exchange, which stores each peer's reply at `devices/<peer>/daemon-state.json`
|
|
12
|
+
* stamped `receivedAt`. This tick keeps its own deadline and circuit breaker, so
|
|
13
|
+
* a hung peer SSH push parks only auth-sync and never the usage delivery.
|
|
14
|
+
*
|
|
15
|
+
* The pushes read each peer's OWN reply file (PHNX-4116 PR 5): a peer reporting
|
|
16
|
+
* a `missing` verdict for an account is pushed that account's key; a peer that
|
|
17
|
+
* has never sent a reply (no file), or whose reply carries no `accounts.rows`
|
|
18
|
+
* field (an older CLI — fail closed), is skipped this tick and logged at INFO.
|
|
19
|
+
* Presence is also gated on a delivered-fingerprint match, so a re-mint
|
|
20
|
+
* (`accounts login` bumps `workerCredential.mintedAt` while the old token still
|
|
21
|
+
* authenticates) re-pushes the rotated key within a tick instead of reading
|
|
22
|
+
* `present` forever. The push is idempotent, so a stale reply is harmless — a
|
|
23
|
+
* stale "has key" is fine, a stale "missing key" costs one redundant push —
|
|
24
|
+
* which is why the per-peer `receivedAt` replaced the old global freshness gate
|
|
25
|
+
* (`readLastSuccessfulExchangeMs`) that skipped EVERY push when the newest
|
|
26
|
+
* exchange across the fleet went stale. The reply file is first-hand and
|
|
27
|
+
* timestamped, so acting on it needs no such fleet-wide gate.
|
|
25
28
|
*/
|
|
26
29
|
import { BasePeriodicService, type DaemonContext } from './service.js';
|
|
27
30
|
import type { DaemonServiceId } from '../daemon-services.js';
|
|
@@ -3,28 +3,30 @@
|
|
|
3
3
|
*
|
|
4
4
|
* This tick owns only the NON-git duties of auth sync: it materializes worker
|
|
5
5
|
* slots from durable keys already on the box, then (on the elected headed
|
|
6
|
-
* publisher) provisions peers
|
|
7
|
-
*
|
|
6
|
+
* publisher) provisions peers missing a reserved key by pushing the credential
|
|
7
|
+
* over SSH. The secret never enters Git.
|
|
8
8
|
*
|
|
9
|
-
* It no
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* (`
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
9
|
+
* It runs no transport of its own (PHNX-4051): the per-account readiness
|
|
10
|
+
* verdicts it reads are published and delivered by the usage-sync tick's SSH
|
|
11
|
+
* exchange, which stores each peer's reply at `devices/<peer>/daemon-state.json`
|
|
12
|
+
* stamped `receivedAt`. This tick keeps its own deadline and circuit breaker, so
|
|
13
|
+
* a hung peer SSH push parks only auth-sync and never the usage delivery.
|
|
14
|
+
*
|
|
15
|
+
* The pushes read each peer's OWN reply file (PHNX-4116 PR 5): a peer reporting
|
|
16
|
+
* a `missing` verdict for an account is pushed that account's key; a peer that
|
|
17
|
+
* has never sent a reply (no file), or whose reply carries no `accounts.rows`
|
|
18
|
+
* field (an older CLI — fail closed), is skipped this tick and logged at INFO.
|
|
19
|
+
* Presence is also gated on a delivered-fingerprint match, so a re-mint
|
|
20
|
+
* (`accounts login` bumps `workerCredential.mintedAt` while the old token still
|
|
21
|
+
* authenticates) re-pushes the rotated key within a tick instead of reading
|
|
22
|
+
* `present` forever. The push is idempotent, so a stale reply is harmless — a
|
|
23
|
+
* stale "has key" is fine, a stale "missing key" costs one redundant push —
|
|
24
|
+
* which is why the per-peer `receivedAt` replaced the old global freshness gate
|
|
25
|
+
* (`readLastSuccessfulExchangeMs`) that skipped EVERY push when the newest
|
|
26
|
+
* exchange across the fleet went stale. The reply file is first-hand and
|
|
27
|
+
* timestamped, so acting on it needs no such fleet-wide gate.
|
|
25
28
|
*/
|
|
26
29
|
import { BasePeriodicService } from './service.js';
|
|
27
|
-
import { USAGE_SYNC_TICK_MS } from './usage-sync-service.js';
|
|
28
30
|
export const AUTH_SYNC_TICK_MS = 15 * 60_000;
|
|
29
31
|
const AUTH_SYNC_DEADLINE_MS = 2 * 60_000;
|
|
30
32
|
export const AUTH_SYNC_KICKOFF_MS = 60_000;
|
|
@@ -40,7 +42,7 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
40
42
|
// Nothing to release — the supervisor's timer teardown is the only cleanup.
|
|
41
43
|
}
|
|
42
44
|
async onTick(ctx) {
|
|
43
|
-
const { reconcileLocalWorkerSlots, syncReservedAuthBundle, syncReservedStores, } = await import('../secrets-policy.js');
|
|
45
|
+
const { reconcileLocalWorkerSlots, syncReservedAuthBundle, syncReservedStores, SKIP_REASON_NO_PEER_REPLY, SKIP_REASON_NO_ACCOUNT_ROWS, } = await import('../secrets-policy.js');
|
|
44
46
|
// Worker-side slot materialization FIRST (PHNX-3940 T6): for each registered
|
|
45
47
|
// account whose durable key is already on this box, create the HOME-shaped
|
|
46
48
|
// slot the picker and spawn read. It touches only local state — the registry
|
|
@@ -67,29 +69,12 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
67
69
|
catch (err) {
|
|
68
70
|
ctx.log('WARN', `auth-sync: worker slot reconcile: ${err.message}`);
|
|
69
71
|
}
|
|
70
|
-
// The
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
//
|
|
74
|
-
//
|
|
75
|
-
//
|
|
76
|
-
// that already received the key still reads `missing`, or a cleared `missing`
|
|
77
|
-
// still reads stale — so skip the pushes and WARN rather than pushing off it.
|
|
78
|
-
// The worker-slot reconcile above is NOT gated: it reads only local durable
|
|
79
|
-
// keys, never delivered peer verdicts.
|
|
80
|
-
const { readLastSuccessfulExchangeMs } = await import('../fleet-shared-repo-sync.js');
|
|
81
|
-
const lastExchangeMs = readLastSuccessfulExchangeMs();
|
|
82
|
-
const ageMs = lastExchangeMs === null ? null : Date.now() - lastExchangeMs;
|
|
83
|
-
// The threshold is the PRODUCER's cadence (USAGE_SYNC_TICK_MS), not this
|
|
84
|
-
// service's own AUTH_SYNC_TICK_MS: the delivered peer verdicts are refreshed
|
|
85
|
-
// once per usage-sync exchange, so "stale" is measured against that tick, not
|
|
86
|
-
// this one. The two constants are equal today, but sourcing it here keeps the
|
|
87
|
-
// gate tracking the usage-sync cadence if either is ever retuned alone.
|
|
88
|
-
if (ageMs === null || ageMs > USAGE_SYNC_TICK_MS) {
|
|
89
|
-
const age = ageMs === null ? 'never completed' : `last completed ${Math.round(ageMs / 1000)}s ago`;
|
|
90
|
-
ctx.log('WARN', `auth-sync: skipping credential push — usage-sync exchange ${age} (need one within ${Math.round(USAGE_SYNC_TICK_MS / 1000)}s); peer verdicts may be stale`);
|
|
91
|
-
return;
|
|
92
|
-
}
|
|
72
|
+
// The pushes read each peer's OWN daemon-state reply, first-hand and
|
|
73
|
+
// timestamped, so they run every tick with no fleet-wide freshness gate: a
|
|
74
|
+
// peer that has replied is planned off its verdict (any age — the push is
|
|
75
|
+
// idempotent), and a peer that has never replied is skipped and logged at
|
|
76
|
+
// INFO below. The worker-slot reconcile above reads only local durable keys,
|
|
77
|
+
// never peer verdicts, so it was never gated either.
|
|
93
78
|
const result = await syncReservedAuthBundle();
|
|
94
79
|
if (result.pushed.length > 0) {
|
|
95
80
|
ctx.log('INFO', `auth-sync: pushed auth to ${result.pushed.join(', ')}`);
|
|
@@ -107,6 +92,15 @@ export class AuthSyncService extends BasePeriodicService {
|
|
|
107
92
|
ctx.log('INFO', `auth-sync: adopted ${stores.adopted.length} legacy reserved item(s) into their bundle: ${stores.adopted.map((a) => `${a.bundle} ${a.key}`).join(', ')}`);
|
|
108
93
|
for (const p of stores.pushed)
|
|
109
94
|
ctx.log('INFO', `auth-sync: pushed ${p.bundle} (${p.keys.length} key(s)) to ${p.device}`);
|
|
95
|
+
// A peer with no reply yet — or a reply with no account rows (older CLI,
|
|
96
|
+
// fail-closed) — is informational, not a warning: a brand-new or never-
|
|
97
|
+
// dialed worker legitimately has none on an early tick, and the no-rows
|
|
98
|
+
// skip is transient during a rolling upgrade. One line per such peer per tick.
|
|
99
|
+
for (const s of stores.skipped) {
|
|
100
|
+
if (s.reason === SKIP_REASON_NO_PEER_REPLY || s.reason === SKIP_REASON_NO_ACCOUNT_ROWS) {
|
|
101
|
+
ctx.log('INFO', `auth-sync: ${s.device}: ${s.reason}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
110
104
|
for (const err of stores.errors)
|
|
111
105
|
ctx.log('WARN', `auth-sync: reserved-store ${err.device}: ${err.message}`);
|
|
112
106
|
}
|