@phnx-labs/agents-cli 1.22.65 → 1.22.66
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
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.66
|
|
4
|
+
|
|
5
|
+
- **Fleet usage/auth sync now reaches FQDN-only-pinned workers (PHNX-3505).** The
|
|
6
|
+
usage-sync and reserved-auth-sync push/pull planners judged a peer "pinned" by its
|
|
7
|
+
bare device name, but a tailnet-discovered worker's host key is pinned under its
|
|
8
|
+
FQDN (`yosemite-m6.tail….ts.net`). So every worker not also pinned under its short
|
|
9
|
+
name was silently skipped — its `claude-usage.json` went days stale and `balanced`
|
|
10
|
+
routed into weekly-maxed accounts, and its auth token never propagated. The pin
|
|
11
|
+
check now resolves the same host string the ssh connection dials
|
|
12
|
+
(`isDevicePinned`), matching either form. Source:
|
|
13
|
+
`cli/src/lib/devices/known-hosts.ts`, `cli/src/lib/accounting/usage-sync.ts`,
|
|
14
|
+
`cli/src/lib/secrets/reserved-sync.ts`.
|
|
15
|
+
|
|
3
16
|
## 1.22.65
|
|
4
17
|
|
|
5
18
|
- **`agents view` shows the last-known usage number with its age instead of hiding it (PHNX-3453).** When the usage endpoint 429-backs-off an account so a window is not refreshed within its own lifetime (Claude's 5h session window) or a billing period rolls over (Grok's weekly credit window), the view now renders the last reading with a staleness suffix — `S: █▌░░░ 30% · 6h old`, `W: ██▏░░ 42% · stale (period ended 1h ago)` — instead of `S: ┄┄┄┄┄ unavailable` or a numberless `run grok once to refresh usage`. The dropped windows ride a new VIEW-ONLY `UsageSnapshot.staleWindows` field; routing still reads only `windows`, so `isUsageVerified`/`hasStaleUsage`/`deriveUsageStatusFromSnapshot` continue to treat a stale/expired number as unverified and never route on it. Source: `cli/src/lib/accounting/usage.ts`.
|
package/dist/commands/apply.js
CHANGED
|
@@ -14,7 +14,7 @@ import chalk from 'chalk';
|
|
|
14
14
|
import { setHelpSections } from '../lib/help.js';
|
|
15
15
|
import { machineId } from '../lib/session/sync/config.js';
|
|
16
16
|
import { loadDevices } from '../lib/devices/registry.js';
|
|
17
|
-
import { isHostPinned, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
|
|
17
|
+
import { isHostPinned, isDevicePinned, managedKnownHostsPath } from '../lib/devices/known-hosts.js';
|
|
18
18
|
import { ensureDevicesRegistered } from '../lib/devices/sync.js';
|
|
19
19
|
import { readFleetFile, resolveDesired, emptyTargetsMessage } from '../lib/fleet/manifest.js';
|
|
20
20
|
import { snapshotAuth } from '../lib/fleet/auth-sync.js';
|
|
@@ -233,7 +233,14 @@ async function runApply(opts) {
|
|
|
233
233
|
provisionSecrets: opts.provisionSecrets === true,
|
|
234
234
|
forceSecrets: opts.force === true,
|
|
235
235
|
// Portable secret values only ever go to a host whose key is already pinned.
|
|
236
|
-
|
|
236
|
+
// Resolve the device to its profile so the pin is checked against the FQDN the
|
|
237
|
+
// ssh connection dials (isDevicePinned), not the bare name — a tailnet worker
|
|
238
|
+
// pinned only under its FQDN would otherwise be refused as "not pinned"
|
|
239
|
+
// (PHNX-3505). A name with no profile falls back to the raw host-string check.
|
|
240
|
+
isHostPinned: (device) => {
|
|
241
|
+
const p = nameToProfile.get(device);
|
|
242
|
+
return p ? isDevicePinned(p) : isHostPinned(device, managedKnownHostsPath());
|
|
243
|
+
},
|
|
237
244
|
});
|
|
238
245
|
// --only filter.
|
|
239
246
|
if (opts.only) {
|
|
@@ -29,7 +29,7 @@ import { buildRemoteAgentsInvocation, buildWindowsStdinAgentsCommand, remoteShel
|
|
|
29
29
|
import { resolveRemoteOsSync } from '../hosts/remote-os.js';
|
|
30
30
|
import { loadDevicesSync } from '../devices/registry.js';
|
|
31
31
|
import { sshTargetFor } from '../devices/connect.js';
|
|
32
|
-
import { isHostPinned } from '../devices/known-hosts.js';
|
|
32
|
+
import { isHostPinned, isDevicePinned } from '../devices/known-hosts.js';
|
|
33
33
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
34
34
|
import { isHeadedDeviceRole, listConfiguredDeviceRoles, selfConfiguredDeviceRole, } from '../device-config.js';
|
|
35
35
|
import { exportClaudeUsageCacheRows, ingestPeerClaudeUsageRows, } from './usage.js';
|
|
@@ -154,7 +154,7 @@ export function pullUsageFromPrimary(deps = {}) {
|
|
|
154
154
|
const primary = devices
|
|
155
155
|
.filter((device) => roles[device.name] === 'personal')
|
|
156
156
|
.concat(devices.filter((device) => roles[device.name] === 'desktop'))
|
|
157
|
-
.find((device) => device.tailscale?.online !== false &&
|
|
157
|
+
.find((device) => device.tailscale?.online !== false && isDevicePinned(device, isPinned));
|
|
158
158
|
if (!primary) {
|
|
159
159
|
result.error = 'no reachable, pinned personal or desktop device is configured as the usage primary';
|
|
160
160
|
return result;
|
|
@@ -205,7 +205,7 @@ export function syncFleetUsageSnapshots(deps = {}) {
|
|
|
205
205
|
name: d.name,
|
|
206
206
|
role: roles[d.name],
|
|
207
207
|
online: d.tailscale?.online !== false,
|
|
208
|
-
pinned:
|
|
208
|
+
pinned: isDevicePinned(d, isPinned),
|
|
209
209
|
}));
|
|
210
210
|
const rows = (deps.exportRows ?? exportClaudeUsageCacheRows)();
|
|
211
211
|
const plan = planUsagePush(isHeadedDeviceRole(selfRole), Object.keys(rows).length > 0, targets);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { DeviceProfile } from './registry.js';
|
|
1
2
|
/** Path to the CLI-managed known_hosts store (created lazily, mode 0600). */
|
|
2
3
|
export declare function managedKnownHostsPath(): string;
|
|
3
4
|
/** Ensure the parent directory of the managed store exists (mode 0700). */
|
|
@@ -12,6 +13,20 @@ export declare function readManagedKnownHosts(file?: string): string;
|
|
|
12
13
|
export declare function isHostPinnedIn(content: string, host: string): boolean;
|
|
13
14
|
/** True if `host` is pinned in the managed store on disk. */
|
|
14
15
|
export declare function isHostPinned(host: string, file?: string): boolean;
|
|
16
|
+
/**
|
|
17
|
+
* True if a DEVICE's host key is pinned — checked against the SAME host string
|
|
18
|
+
* the ssh connection dials (`hostNameFor(device)`: the Tailscale dnsName/IP that
|
|
19
|
+
* `sshTargetFor` resolves), falling back to the bare device name.
|
|
20
|
+
*
|
|
21
|
+
* A device discovered over the tailnet is pinned under its FQDN
|
|
22
|
+
* (`yosemite-m6.tail….ts.net`), so a caller that checks the bare `device.name`
|
|
23
|
+
* misses the pin and wrongly treats a reachable, pinned peer as unpinned —
|
|
24
|
+
* silently excluding it from usage/auth fan-out while ssh to it would have
|
|
25
|
+
* succeeded (PHNX-3505). Accepting the bare name too keeps a device pinned only
|
|
26
|
+
* under its short name working. `isPinned` is injectable for tests; it defaults
|
|
27
|
+
* to the managed on-disk store.
|
|
28
|
+
*/
|
|
29
|
+
export declare function isDevicePinned(device: DeviceProfile, isPinned?: (host: string) => boolean): boolean;
|
|
15
30
|
/**
|
|
16
31
|
* The host-key-checking ssh options for a connection.
|
|
17
32
|
*
|
|
@@ -22,6 +22,7 @@ import { spawnSync } from 'child_process';
|
|
|
22
22
|
import { getCacheDir } from '../state.js';
|
|
23
23
|
import { assertValidSshTarget } from '../ssh-exec.js';
|
|
24
24
|
import { parseKnownHosts } from '../hosts/ssh-config.js';
|
|
25
|
+
import { hostNameFor } from './ssh-config.js';
|
|
25
26
|
/** Path to the CLI-managed known_hosts store (created lazily, mode 0600). */
|
|
26
27
|
export function managedKnownHostsPath() {
|
|
27
28
|
return path.join(getCacheDir(), 'devices', 'known_hosts');
|
|
@@ -54,6 +55,23 @@ export function isHostPinnedIn(content, host) {
|
|
|
54
55
|
export function isHostPinned(host, file = managedKnownHostsPath()) {
|
|
55
56
|
return isHostPinnedIn(readManagedKnownHosts(file), host);
|
|
56
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* True if a DEVICE's host key is pinned — checked against the SAME host string
|
|
60
|
+
* the ssh connection dials (`hostNameFor(device)`: the Tailscale dnsName/IP that
|
|
61
|
+
* `sshTargetFor` resolves), falling back to the bare device name.
|
|
62
|
+
*
|
|
63
|
+
* A device discovered over the tailnet is pinned under its FQDN
|
|
64
|
+
* (`yosemite-m6.tail….ts.net`), so a caller that checks the bare `device.name`
|
|
65
|
+
* misses the pin and wrongly treats a reachable, pinned peer as unpinned —
|
|
66
|
+
* silently excluding it from usage/auth fan-out while ssh to it would have
|
|
67
|
+
* succeeded (PHNX-3505). Accepting the bare name too keeps a device pinned only
|
|
68
|
+
* under its short name working. `isPinned` is injectable for tests; it defaults
|
|
69
|
+
* to the managed on-disk store.
|
|
70
|
+
*/
|
|
71
|
+
export function isDevicePinned(device, isPinned = (host) => isHostPinned(host)) {
|
|
72
|
+
const host = device.address ? hostNameFor(device) : undefined;
|
|
73
|
+
return (host != null && isPinned(host)) || isPinned(device.name);
|
|
74
|
+
}
|
|
57
75
|
/**
|
|
58
76
|
* The host-key-checking ssh options for a connection.
|
|
59
77
|
*
|
|
@@ -17,7 +17,7 @@ import { AUTH_BUNDLE_NAME, inspectReservedAuthBundle } from './bundles.js';
|
|
|
17
17
|
import { pushBundleToHost } from './push.js';
|
|
18
18
|
import { loadDevicesSync } from '../devices/registry.js';
|
|
19
19
|
import { sshTargetFor } from '../devices/connect.js';
|
|
20
|
-
import { isHostPinned, managedKnownHostsPath } from '../devices/known-hosts.js';
|
|
20
|
+
import { isHostPinned, isDevicePinned, managedKnownHostsPath } from '../devices/known-hosts.js';
|
|
21
21
|
import { machineId, normalizeHost } from '../session/sync/config.js';
|
|
22
22
|
import { probeDevice } from '../fleet/apply.js';
|
|
23
23
|
export function planAuthBundlePush(localAuthOk, devices) {
|
|
@@ -81,9 +81,9 @@ export function syncReservedAuthBundle(deps = {}) {
|
|
|
81
81
|
// No live probe until we know a push is even possible: missing local auth
|
|
82
82
|
// skips every device, and an unpinned host is refused before we SSH.
|
|
83
83
|
if (!localOk) {
|
|
84
|
-
return { name: d.name, reachable: true, pinned:
|
|
84
|
+
return { name: d.name, reachable: true, pinned: isDevicePinned(d, pinned), remoteHasAuth: false };
|
|
85
85
|
}
|
|
86
|
-
const isPinnedDev =
|
|
86
|
+
const isPinnedDev = isDevicePinned(d, pinned);
|
|
87
87
|
if (!isPinnedDev) {
|
|
88
88
|
return { name: d.name, reachable: true, pinned: false, remoteHasAuth: false };
|
|
89
89
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.66",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|