@phnx-labs/agents-cli 1.22.68 → 1.22.70
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 +46 -0
- package/README.md +17 -14
- package/dist/bootstrap.js +3 -0
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +33 -23
- package/dist/commands/feed.js +20 -7
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +3 -0
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +35 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/share.js +43 -14
- package/dist/commands/ssh.js +205 -2
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage.d.ts +7 -2
- package/dist/lib/accounting/usage.js +142 -10
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/ipc.js +9 -1
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/daemon.js +28 -8
- package/dist/lib/daemon/runner.js +6 -2
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +7 -34
- package/dist/lib/feed-broadcast.d.ts +47 -5
- package/dist/lib/feed-broadcast.js +127 -24
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/monitors/engine.js +18 -0
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +55 -0
- package/dist/lib/owner-message.js +64 -0
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/run-trace-sync.d.ts +15 -0
- package/dist/lib/run-trace-sync.js +43 -21
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +12 -1
- package/dist/lib/session/db.js +20 -1
- package/dist/lib/session/discover.js +81 -1
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +7 -1
- package/dist/lib/session/prompt.js +12 -2
- package/dist/lib/session/recovery.d.ts +21 -12
- package/dist/lib/session/recovery.js +29 -11
- package/dist/lib/session/remote/watch.js +5 -2
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +13 -3
- package/dist/lib/share/publish.js +19 -15
- package/dist/lib/share/worker-template.js +5 -1
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
|
@@ -11,6 +11,15 @@ import type { MonitorSource } from '../config.js';
|
|
|
11
11
|
export interface Observation {
|
|
12
12
|
raw: string;
|
|
13
13
|
meta?: Record<string, unknown>;
|
|
14
|
+
/**
|
|
15
|
+
* The source flagged this snapshot as an OBSERVATION FAILURE (a poll that
|
|
16
|
+
* exited non-zero or emitted a transport/auth/rate-limit error), not a value.
|
|
17
|
+
* The engine skips it: no fire, watched-state untouched, counted as a failed
|
|
18
|
+
* check for drought health (PHNX-3510).
|
|
19
|
+
*/
|
|
20
|
+
failed?: boolean;
|
|
21
|
+
/** Short human reason for `failed`, surfaced in drought health and `test`. */
|
|
22
|
+
failureReason?: string;
|
|
14
23
|
}
|
|
15
24
|
/** Poll-model evaluator: return one observation, or null when none is available. */
|
|
16
25
|
export type SourceEvaluator = (source: MonitorSource) => Promise<Observation | null>;
|
package/dist/lib/notify.d.ts
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
import type { OpenBlock } from './feed/feed.js';
|
|
17
17
|
import type { Meta } from './types.js';
|
|
18
18
|
import type { SendResult } from './channels/registry.js';
|
|
19
|
+
import { type SinkMessageFormat } from './sink-format.js';
|
|
19
20
|
export interface OwnerNotifyOptions {
|
|
20
21
|
/** Config source (defaults to `readMeta()`); lets callers/tests inject it. */
|
|
21
22
|
meta?: Meta;
|
|
@@ -28,6 +29,16 @@ export interface OwnerNotifyOptions {
|
|
|
28
29
|
thread?: string;
|
|
29
30
|
attachments?: string[];
|
|
30
31
|
from?: string;
|
|
32
|
+
/**
|
|
33
|
+
* Per-destination body composer (PHNX-3698). The owner policy fans one alert
|
|
34
|
+
* out to several channels (imessage + slack, …), and only Slack can render a
|
|
35
|
+
* labeled link — so when set, each destination gets the body shaped for its
|
|
36
|
+
* OWN resolved provider (Slack → `mrkdwn` `<url|label>` links, everything else
|
|
37
|
+
* → `plain` the human sentence, no URLs) instead of the single `text` going to
|
|
38
|
+
* every channel. Absent for callers whose body is already final (urgent
|
|
39
|
+
* blocks, monitor summaries): `text` is delivered verbatim to every channel.
|
|
40
|
+
*/
|
|
41
|
+
composeForFormat?: (format: SinkMessageFormat) => string;
|
|
31
42
|
}
|
|
32
43
|
export interface NotifyResult {
|
|
33
44
|
ok: boolean;
|
package/dist/lib/notify.js
CHANGED
|
@@ -3,6 +3,7 @@ import { getOwnerNotifyDestinationsFromHumans } from './humans.js';
|
|
|
3
3
|
import { registerBuiltinProviders } from './channels/providers/index.js';
|
|
4
4
|
import { lookupTransport } from './channels/resolve.js';
|
|
5
5
|
import { forwardOwnerNotifyToPeer } from './channels/owner-forward.js';
|
|
6
|
+
import { sinkMessageFormat } from './sink-format.js';
|
|
6
7
|
export function formatUrgentBlockMessage(block) {
|
|
7
8
|
const q = block.questions[0];
|
|
8
9
|
const header = q?.header ? `[${q.header}] ` : '';
|
|
@@ -72,11 +73,20 @@ export async function sendToOwner(text, options = {}) {
|
|
|
72
73
|
registerBuiltinProviders();
|
|
73
74
|
const deliveries = [];
|
|
74
75
|
for (const { channel, to: target } of addressable) {
|
|
75
|
-
const { provider, error } = lookupTransport(channel, meta);
|
|
76
|
+
const { provider, providerName, error } = lookupTransport(channel, meta);
|
|
77
|
+
// Each destination gets the body shaped for the provider it ACTUALLY
|
|
78
|
+
// delivers through (the same `notify.transports` remap `lookupTransport`
|
|
79
|
+
// applied), so a Slack destination turns its ticket keys + session crumb
|
|
80
|
+
// into `<url|label>` links while a sibling iMessage copy stays plain
|
|
81
|
+
// (PHNX-3698). Without a composer the caller's already-final `text` is used
|
|
82
|
+
// for every channel.
|
|
83
|
+
const body = options.composeForFormat
|
|
84
|
+
? options.composeForFormat(sinkMessageFormat(providerName))
|
|
85
|
+
: text;
|
|
76
86
|
let result;
|
|
77
87
|
try {
|
|
78
88
|
result = provider
|
|
79
|
-
? await provider.send(
|
|
89
|
+
? await provider.send(body, {
|
|
80
90
|
target,
|
|
81
91
|
ownerScoped: options.target === undefined,
|
|
82
92
|
dryRun: options.dryRun,
|
|
@@ -99,12 +109,15 @@ export async function sendToOwner(text, options = {}) {
|
|
|
99
109
|
};
|
|
100
110
|
}
|
|
101
111
|
else {
|
|
102
|
-
result = await forwardOwnerNotifyToPeer(
|
|
112
|
+
result = await forwardOwnerNotifyToPeer(body, channel, target, meta, {
|
|
103
113
|
envelope: { thread: options.thread, from: options.from },
|
|
104
114
|
}) ?? result;
|
|
105
115
|
}
|
|
106
116
|
}
|
|
107
|
-
|
|
117
|
+
// Echo the exact per-destination body delivered, so a caller (and
|
|
118
|
+
// `agents notify --dry-run --json`) can see Slack got the labeled-link
|
|
119
|
+
// variant and iMessage the plain one — the observable proof of PHNX-3698.
|
|
120
|
+
deliveries.push({ ...result, body });
|
|
108
121
|
}
|
|
109
122
|
if (deliveries.length === 1)
|
|
110
123
|
return deliveries[0];
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose an owner-bound phone ping through the SAME shaper `agents feed post`
|
|
3
|
+
* uses, so `agents notify` / `agents send --to owner` stop shipping a raw body
|
|
4
|
+
* dump (PHNX-3698).
|
|
5
|
+
*
|
|
6
|
+
* Before this, an owner send delivered the body verbatim: a long wall of prose,
|
|
7
|
+
* a `TEAM-N` key iMessage renders as dead text, and no way back to the session.
|
|
8
|
+
* Routing the body through {@link composeBroadcastMessage} makes an owner ping
|
|
9
|
+
* identical to an important feed post of the same event — short-shaped body,
|
|
10
|
+
* every ticket key linkified to its Linear URL, and the current session's
|
|
11
|
+
* `…/console/sessions/<id>` page as a tappable crumb.
|
|
12
|
+
*
|
|
13
|
+
* The session/agent/host are resolved the same way a feed post resolves them
|
|
14
|
+
* ({@link resolvePostIdentity} — the pid-registry / env walk), so a `notify`
|
|
15
|
+
* run inside an agent session inherits that session with no flag. Outside a
|
|
16
|
+
* session (a human at a shell) identity is undefined: the ping still gets
|
|
17
|
+
* short-shaping and ticket linkification, just no session crumb.
|
|
18
|
+
*/
|
|
19
|
+
import { type FeedBroadcastContext } from './feed-broadcast.js';
|
|
20
|
+
import type { SinkMessageFormat } from './sink-format.js';
|
|
21
|
+
export interface OwnerMessageOptions {
|
|
22
|
+
/** Scannable subject line, when the caller has one (feed post does; notify does not). */
|
|
23
|
+
title?: string;
|
|
24
|
+
/** Explicit session id (`--session`); otherwise resolved from the run environment. */
|
|
25
|
+
sessionId?: string;
|
|
26
|
+
/**
|
|
27
|
+
* Rendering vocabulary for this body (PHNX-3698). Slack `mrkdwn` linkifies the
|
|
28
|
+
* ticket keys and the session crumb as `<url|label>`; `plain` (the default)
|
|
29
|
+
* keeps the human sentence with no URLs, for iMessage / owner-scoped rush.
|
|
30
|
+
* The owner fan-out passes one format per destination.
|
|
31
|
+
*/
|
|
32
|
+
format?: SinkMessageFormat;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Resolve the run identity into the broadcast context an owner ping is built
|
|
36
|
+
* from — the shared shape a `feed post` uses. Pure except for the identity/index
|
|
37
|
+
* reads that {@link resolvePostIdentity} / {@link getSessionById} already perform
|
|
38
|
+
* for feed posts. Built ONCE so the owner fan-out can render it in more than one
|
|
39
|
+
* format (Slack vs iMessage) without re-walking the pid registry per destination.
|
|
40
|
+
*/
|
|
41
|
+
export declare function ownerMessageContext(rawText: string, opts?: OwnerMessageOptions): FeedBroadcastContext;
|
|
42
|
+
/**
|
|
43
|
+
* Shape a raw owner-send body into the composed broadcast message. `format`
|
|
44
|
+
* decides how links surface — `plain` (default) for iMessage / owner-scoped rush,
|
|
45
|
+
* `mrkdwn` for a Slack owner destination (PHNX-3698).
|
|
46
|
+
*/
|
|
47
|
+
export declare function composeOwnerMessage(rawText: string, opts?: OwnerMessageOptions): string;
|
|
48
|
+
/**
|
|
49
|
+
* A per-format composer bound to ONE resolved context — the shape the owner
|
|
50
|
+
* fan-out wants (PHNX-3698). `agents notify` / `agents send --to owner` build
|
|
51
|
+
* this once, use `compose('plain')` for the envelope's display body, and hand
|
|
52
|
+
* `compose` to `sendToOwner` so each policy destination re-renders in its own
|
|
53
|
+
* format without re-walking the pid registry.
|
|
54
|
+
*/
|
|
55
|
+
export declare function ownerMessageComposer(rawText: string, opts?: OwnerMessageOptions): (format: SinkMessageFormat) => string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compose an owner-bound phone ping through the SAME shaper `agents feed post`
|
|
3
|
+
* uses, so `agents notify` / `agents send --to owner` stop shipping a raw body
|
|
4
|
+
* dump (PHNX-3698).
|
|
5
|
+
*
|
|
6
|
+
* Before this, an owner send delivered the body verbatim: a long wall of prose,
|
|
7
|
+
* a `TEAM-N` key iMessage renders as dead text, and no way back to the session.
|
|
8
|
+
* Routing the body through {@link composeBroadcastMessage} makes an owner ping
|
|
9
|
+
* identical to an important feed post of the same event — short-shaped body,
|
|
10
|
+
* every ticket key linkified to its Linear URL, and the current session's
|
|
11
|
+
* `…/console/sessions/<id>` page as a tappable crumb.
|
|
12
|
+
*
|
|
13
|
+
* The session/agent/host are resolved the same way a feed post resolves them
|
|
14
|
+
* ({@link resolvePostIdentity} — the pid-registry / env walk), so a `notify`
|
|
15
|
+
* run inside an agent session inherits that session with no flag. Outside a
|
|
16
|
+
* session (a human at a shell) identity is undefined: the ping still gets
|
|
17
|
+
* short-shaping and ticket linkification, just no session crumb.
|
|
18
|
+
*/
|
|
19
|
+
import { composeBroadcastMessage } from './feed-broadcast.js';
|
|
20
|
+
import { resolvePostIdentity } from './feed-post.js';
|
|
21
|
+
import { getSessionById, resolveFullSessionId } from './session/db.js';
|
|
22
|
+
import { linearIssueUrl } from './session/linear.js';
|
|
23
|
+
/**
|
|
24
|
+
* Resolve the run identity into the broadcast context an owner ping is built
|
|
25
|
+
* from — the shared shape a `feed post` uses. Pure except for the identity/index
|
|
26
|
+
* reads that {@link resolvePostIdentity} / {@link getSessionById} already perform
|
|
27
|
+
* for feed posts. Built ONCE so the owner fan-out can render it in more than one
|
|
28
|
+
* format (Slack vs iMessage) without re-walking the pid registry per destination.
|
|
29
|
+
*/
|
|
30
|
+
export function ownerMessageContext(rawText, opts = {}) {
|
|
31
|
+
const identity = resolvePostIdentity({ sessionId: opts.sessionId });
|
|
32
|
+
// A footer crumb that would 404 (an 8-char short id) is upgraded to the full
|
|
33
|
+
// indexed id so the console URL resolves; a full/native id passes through.
|
|
34
|
+
const session = resolveFullSessionId(identity?.sessionId);
|
|
35
|
+
const ticket = session ? getSessionById(session)?.ticketId : undefined;
|
|
36
|
+
return {
|
|
37
|
+
...(opts.title?.trim() ? { title: opts.title.trim() } : {}),
|
|
38
|
+
text: rawText,
|
|
39
|
+
level: 'important',
|
|
40
|
+
...(ticket ? { ticket, ticketUrl: linearIssueUrl(ticket) } : {}),
|
|
41
|
+
...(identity?.agent ? { agent: identity.agent } : {}),
|
|
42
|
+
...(identity?.host ? { host: identity.host } : {}),
|
|
43
|
+
...(session ? { session } : {}),
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Shape a raw owner-send body into the composed broadcast message. `format`
|
|
48
|
+
* decides how links surface — `plain` (default) for iMessage / owner-scoped rush,
|
|
49
|
+
* `mrkdwn` for a Slack owner destination (PHNX-3698).
|
|
50
|
+
*/
|
|
51
|
+
export function composeOwnerMessage(rawText, opts = {}) {
|
|
52
|
+
return composeBroadcastMessage(ownerMessageContext(rawText, opts), opts.format ?? 'plain');
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* A per-format composer bound to ONE resolved context — the shape the owner
|
|
56
|
+
* fan-out wants (PHNX-3698). `agents notify` / `agents send --to owner` build
|
|
57
|
+
* this once, use `compose('plain')` for the envelope's display body, and hand
|
|
58
|
+
* `compose` to `sendToOwner` so each policy destination re-renders in its own
|
|
59
|
+
* format without re-walking the pid registry.
|
|
60
|
+
*/
|
|
61
|
+
export function ownerMessageComposer(rawText, opts = {}) {
|
|
62
|
+
const ctx = ownerMessageContext(rawText, opts);
|
|
63
|
+
return (format) => composeBroadcastMessage(ctx, format);
|
|
64
|
+
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import type { AgentId, PermissionSet } from './types.js';
|
|
2
2
|
/** Canonical tool (lowercased) -> Grok's lowercase tool vocabulary. */
|
|
3
3
|
export declare const GROK_TOOL_BY_CANONICAL: Record<string, string | undefined>;
|
|
4
|
-
/** Canonical tool -> Kiro CLI v3 capability id. */
|
|
5
|
-
export declare const KIRO_CAPABILITY_BY_TOOL: Record<string, string | undefined>;
|
|
6
4
|
/** Canonical tool -> OpenClaw tool id (tool-level granularity only). */
|
|
7
5
|
export declare const CANONICAL_TO_OPENCLAW_TOOL: Record<string, string>;
|
|
8
6
|
/** Canonical tool -> Antigravity's action namespace. */
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* behind `agents permissions add <path>` used to dispatch through hand-written
|
|
8
8
|
* 3-arm switches while
|
|
9
9
|
* `applyPermissionsToVersion` wrote 13 harnesses, so permissions were written
|
|
10
|
-
* for cursor, antigravity, grok, kimi, droid, copilot,
|
|
10
|
+
* for cursor, antigravity, grok, kimi, droid, copilot, openclaw and
|
|
11
11
|
* hermes and then reported as absent for all ten (RUSH-2676).
|
|
12
12
|
*
|
|
13
13
|
* The key set is pinned to `capableAgents('allowlist')` by
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* ## Every reverse projection is lossy, and says how
|
|
25
25
|
*
|
|
26
26
|
* The forward direction discards information — several canonical tools collapse
|
|
27
|
-
* onto one native id (`Read`/`Grep`/`Glob` all become
|
|
27
|
+
* onto one native id (`Read`/`Grep`/`Glob` all become a coarse capability), and
|
|
28
28
|
* pattern grammars differ (`git:*` becomes `git *`). Reading back therefore
|
|
29
29
|
* recovers a permission set that GRANTS THE SAME ACCESS, not the byte-identical
|
|
30
30
|
* strings that were written. Each target documents its own collapse. Callers that
|
|
@@ -45,21 +45,6 @@ export const GROK_TOOL_BY_CANONICAL = {
|
|
|
45
45
|
grep: 'grep',
|
|
46
46
|
webfetch: 'webfetch',
|
|
47
47
|
};
|
|
48
|
-
/** Canonical tool -> Kiro CLI v3 capability id. */
|
|
49
|
-
export const KIRO_CAPABILITY_BY_TOOL = {
|
|
50
|
-
bash: 'shell',
|
|
51
|
-
read: 'fs_read',
|
|
52
|
-
grep: 'fs_read',
|
|
53
|
-
glob: 'fs_read',
|
|
54
|
-
write: 'fs_write',
|
|
55
|
-
edit: 'fs_write',
|
|
56
|
-
notebookedit: 'fs_write',
|
|
57
|
-
webfetch: 'web_fetch',
|
|
58
|
-
websearch: 'web_search',
|
|
59
|
-
mcp: 'mcp',
|
|
60
|
-
subagent: 'subagent',
|
|
61
|
-
skill: 'skill',
|
|
62
|
-
};
|
|
63
48
|
/** Canonical tool -> OpenClaw tool id (tool-level granularity only). */
|
|
64
49
|
export const CANONICAL_TO_OPENCLAW_TOOL = {
|
|
65
50
|
bash: 'exec',
|
|
@@ -81,7 +66,7 @@ export const ANTIGRAVITY_ACTION_BY_TOOL = {
|
|
|
81
66
|
* native id. Several canonical tools collapse onto one native id, so the
|
|
82
67
|
* inverse must pick a representative; declaration order in the forward table is
|
|
83
68
|
* that choice, which is why `read` (declared before `grep`/`glob`) represents
|
|
84
|
-
*
|
|
69
|
+
* a coarser native capability.
|
|
85
70
|
*/
|
|
86
71
|
function invertFirstWins(forward) {
|
|
87
72
|
const out = {};
|
|
@@ -94,7 +79,6 @@ function invertFirstWins(forward) {
|
|
|
94
79
|
return out;
|
|
95
80
|
}
|
|
96
81
|
const CANONICAL_BY_GROK_TOOL = invertFirstWins(GROK_TOOL_BY_CANONICAL);
|
|
97
|
-
const CANONICAL_BY_KIRO_CAPABILITY = invertFirstWins(KIRO_CAPABILITY_BY_TOOL);
|
|
98
82
|
const CANONICAL_BY_OPENCLAW_TOOL = invertFirstWins(CANONICAL_TO_OPENCLAW_TOOL);
|
|
99
83
|
const CANONICAL_BY_ANTIGRAVITY_ACTION = invertFirstWins(ANTIGRAVITY_ACTION_BY_TOOL);
|
|
100
84
|
/** Filename Codex stores generated deny rules under `.codex/rules/`. */
|
|
@@ -585,37 +569,6 @@ export const PERMISSION_TARGETS = {
|
|
|
585
569
|
return permissionSet(allow, []);
|
|
586
570
|
},
|
|
587
571
|
},
|
|
588
|
-
kiro: {
|
|
589
|
-
home: (h) => path.join(h, '.kiro', 'settings', 'permissions.yaml'),
|
|
590
|
-
lossyBecause: 'Read/Grep/Glob share fs_read and Write/Edit/NotebookEdit share fs_write',
|
|
591
|
-
toCanonical(configPath) {
|
|
592
|
-
const config = readYaml(configPath);
|
|
593
|
-
const rules = config?.rules;
|
|
594
|
-
if (!Array.isArray(rules))
|
|
595
|
-
return null;
|
|
596
|
-
const allow = [];
|
|
597
|
-
const deny = [];
|
|
598
|
-
for (const rule of rules) {
|
|
599
|
-
if (!rule || typeof rule !== 'object' || Array.isArray(rule))
|
|
600
|
-
continue;
|
|
601
|
-
const r = rule;
|
|
602
|
-
if (typeof r.capability !== 'string')
|
|
603
|
-
continue;
|
|
604
|
-
const lowerTool = CANONICAL_BY_KIRO_CAPABILITY[r.capability];
|
|
605
|
-
if (!lowerTool)
|
|
606
|
-
continue;
|
|
607
|
-
const matches = stringList(r.match);
|
|
608
|
-
const canonicals = matches.length
|
|
609
|
-
? matches.map((m) => canonicalRule(lowerTool, lowerTool === 'bash' ? denormalizeBashPattern(m) : m))
|
|
610
|
-
: [canonicalRule(lowerTool, null)];
|
|
611
|
-
if (r.effect === 'deny')
|
|
612
|
-
deny.push(...canonicals);
|
|
613
|
-
else if (r.effect === 'allow')
|
|
614
|
-
allow.push(...canonicals);
|
|
615
|
-
}
|
|
616
|
-
return permissionSet(allow, deny);
|
|
617
|
-
},
|
|
618
|
-
},
|
|
619
572
|
openclaw: {
|
|
620
573
|
home: (h) => path.join(h, '.openclaw', 'openclaw.json'),
|
|
621
574
|
lossyBecause: 'OpenClaw gates whole tools, so only blanket rules were written and all read back as blanket',
|
|
@@ -190,19 +190,6 @@ export type KimiRule = {
|
|
|
190
190
|
decision: 'allow' | 'deny';
|
|
191
191
|
pattern: string;
|
|
192
192
|
};
|
|
193
|
-
export type KiroRule = {
|
|
194
|
-
capability: string;
|
|
195
|
-
effect: 'allow' | 'deny';
|
|
196
|
-
match?: string[];
|
|
197
|
-
exclude?: string[];
|
|
198
|
-
};
|
|
199
|
-
/**
|
|
200
|
-
* Convert canonical permissions to Kiro CLI v3 capability rules.
|
|
201
|
-
* Kiro stores these rules in ~/.kiro/settings/permissions.yaml.
|
|
202
|
-
*/
|
|
203
|
-
export declare function convertToKiroFormat(set: PermissionSet): {
|
|
204
|
-
rules: KiroRule[];
|
|
205
|
-
};
|
|
206
193
|
/**
|
|
207
194
|
* Convert a canonical permission set to Kimi Code's `[permission].rules` format.
|
|
208
195
|
* Kimi (`~/.kimi-code/config.toml`) reads rules of the form
|
|
@@ -264,7 +251,7 @@ export declare function convertToCodexFormat(set: PermissionSet, cwd?: string):
|
|
|
264
251
|
* Every other allowlist-capable harness returns the canonical `PermissionSet`
|
|
265
252
|
* that `PERMISSION_TARGETS` reads back. Before RUSH-2676 they returned `null`,
|
|
266
253
|
* so permissions written for cursor, antigravity, grok, kimi, droid,
|
|
267
|
-
* copilot,
|
|
254
|
+
* copilot, openclaw and hermes were reported as absent.
|
|
268
255
|
*/
|
|
269
256
|
export declare function readAgentPermissions(agentId: AgentId, scope?: 'user' | 'project', cwd?: string, options?: {
|
|
270
257
|
home?: string;
|
|
@@ -315,8 +302,7 @@ export declare function exportPermissionsFromPath(filePath: string): PermissionS
|
|
|
315
302
|
/**
|
|
316
303
|
* Which harness owns `filePath`, by comparing against the trailing path segments
|
|
317
304
|
* each registry target declares. Longer (more specific) suffixes win, so
|
|
318
|
-
|
|
319
|
-
*/
|
|
305
|
+
*/
|
|
320
306
|
export declare function detectPermissionAgentFromPath(filePath: string): AgentId | null;
|
|
321
307
|
/**
|
|
322
308
|
* Get the default permission set from central storage.
|
|
@@ -356,7 +342,7 @@ export declare function saveDefaultPermissionSet(set: PermissionSet): {
|
|
|
356
342
|
* (`lossyBecause` in `permissions-registry.ts`), so a canonical subset check
|
|
357
343
|
* would false-diff a correctly-synced home.
|
|
358
344
|
*
|
|
359
|
-
* Every other allowlist harness (codex/grok/kimi/
|
|
345
|
+
* Every other allowlist harness (codex/grok/kimi/antigravity/hermes/copilot)
|
|
360
346
|
* stores a lossy projection — a sandbox flag, whole-tool gate, per-directory
|
|
361
347
|
* approval, or a split/merged pattern — with no faithful per-group provenance, so
|
|
362
348
|
* doctor stays presence-only there and says so (`detail: 'format cannot verify
|
package/dist/lib/permissions.js
CHANGED
|
@@ -20,7 +20,7 @@ import { updateGeminiSettings } from './gemini-settings.js';
|
|
|
20
20
|
// The canonical<->native tool vocabularies live in the registry, which also owns
|
|
21
21
|
// the reverse projections — so the serializers below and the readers there can
|
|
22
22
|
// never disagree about what `fs_read` or `developer__shell` means.
|
|
23
|
-
import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, CODEX_RULES_FILENAME, stripJsonComments, GROK_TOOL_BY_CANONICAL,
|
|
23
|
+
import { ANTIGRAVITY_ACTION_BY_TOOL, CANONICAL_TO_OPENCLAW_TOOL, CODEX_RULES_FILENAME, stripJsonComments, GROK_TOOL_BY_CANONICAL, PERMISSION_TARGETS, readCanonicalPermissions, } from './permissions-registry.js';
|
|
24
24
|
export { CODEX_RULES_FILENAME };
|
|
25
25
|
const HOME = os.homedir();
|
|
26
26
|
export const COMPUTER_PERMISSION_RULE_PREFIX = 'Computer';
|
|
@@ -799,43 +799,6 @@ function canonicalToGrokRule(perm, action) {
|
|
|
799
799
|
}
|
|
800
800
|
return { action, tool, pattern };
|
|
801
801
|
}
|
|
802
|
-
/**
|
|
803
|
-
* Convert canonical permissions to Kiro CLI v3 capability rules.
|
|
804
|
-
* Kiro stores these rules in ~/.kiro/settings/permissions.yaml.
|
|
805
|
-
*/
|
|
806
|
-
export function convertToKiroFormat(set) {
|
|
807
|
-
const rules = [];
|
|
808
|
-
for (const perm of set.allow) {
|
|
809
|
-
const rule = canonicalToKiroRule(perm, 'allow');
|
|
810
|
-
if (rule)
|
|
811
|
-
rules.push(rule);
|
|
812
|
-
}
|
|
813
|
-
for (const perm of set.deny ?? []) {
|
|
814
|
-
const rule = canonicalToKiroRule(perm, 'deny');
|
|
815
|
-
if (rule)
|
|
816
|
-
rules.push(rule);
|
|
817
|
-
}
|
|
818
|
-
return { rules };
|
|
819
|
-
}
|
|
820
|
-
function canonicalToKiroRule(perm, effect) {
|
|
821
|
-
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
822
|
-
return { capability: 'shell', effect };
|
|
823
|
-
}
|
|
824
|
-
const parsed = parseCanonicalPreserveCase(perm);
|
|
825
|
-
const capability = KIRO_CAPABILITY_BY_TOOL[parsed.tool.toLowerCase()];
|
|
826
|
-
if (!capability)
|
|
827
|
-
return null;
|
|
828
|
-
if (parsed.pattern === null || parsed.pattern === '*' || parsed.pattern === '**') {
|
|
829
|
-
return { capability, effect };
|
|
830
|
-
}
|
|
831
|
-
const lowerTool = parsed.tool.toLowerCase();
|
|
832
|
-
const pattern = lowerTool === 'bash'
|
|
833
|
-
? normalizeBashPattern(parsed.pattern)
|
|
834
|
-
: (lowerTool === 'webfetch' || lowerTool === 'websearch') && parsed.pattern.startsWith('domain:')
|
|
835
|
-
? parsed.pattern.slice('domain:'.length)
|
|
836
|
-
: parsed.pattern;
|
|
837
|
-
return { capability, effect, match: [pattern] };
|
|
838
|
-
}
|
|
839
802
|
/**
|
|
840
803
|
* Parse a canonical permission string preserving the tool's original casing.
|
|
841
804
|
* `parseCanonicalPattern` lowercases the tool name, which is fine for Grok
|
|
@@ -1147,7 +1110,7 @@ function readCodexPermissions(scope = 'user', cwd, options) {
|
|
|
1147
1110
|
* Every other allowlist-capable harness returns the canonical `PermissionSet`
|
|
1148
1111
|
* that `PERMISSION_TARGETS` reads back. Before RUSH-2676 they returned `null`,
|
|
1149
1112
|
* so permissions written for cursor, antigravity, grok, kimi, droid,
|
|
1150
|
-
* copilot,
|
|
1113
|
+
* copilot, openclaw and hermes were reported as absent.
|
|
1151
1114
|
*/
|
|
1152
1115
|
export function readAgentPermissions(agentId, scope = 'user', cwd, options) {
|
|
1153
1116
|
switch (agentId) {
|
|
@@ -1598,37 +1561,6 @@ export function applyPermissionsToVersion(agentId, set, versionHome, merge = tru
|
|
|
1598
1561
|
fs.writeFileSync(configPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
1599
1562
|
return { success: true };
|
|
1600
1563
|
}
|
|
1601
|
-
if (agentId === 'kiro') {
|
|
1602
|
-
const permissionsPath = path.join(versionHome, '.kiro', 'settings', 'permissions.yaml');
|
|
1603
|
-
let config = {};
|
|
1604
|
-
if (fs.existsSync(permissionsPath)) {
|
|
1605
|
-
const parsed = yaml.parse(fs.readFileSync(permissionsPath, 'utf-8'));
|
|
1606
|
-
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
1607
|
-
config = parsed;
|
|
1608
|
-
}
|
|
1609
|
-
}
|
|
1610
|
-
const newRules = convertToKiroFormat(set).rules;
|
|
1611
|
-
if (merge) {
|
|
1612
|
-
const existingRules = Array.isArray(config.rules) ? config.rules : [];
|
|
1613
|
-
const seen = new Set();
|
|
1614
|
-
config.rules = [...existingRules, ...newRules].filter((rule) => {
|
|
1615
|
-
if (!rule || typeof rule !== 'object' || Array.isArray(rule))
|
|
1616
|
-
return false;
|
|
1617
|
-
const record = rule;
|
|
1618
|
-
const key = `${String(record.effect)}|${String(record.capability)}|${JSON.stringify(record.match ?? [])}|${JSON.stringify(record.exclude ?? [])}`;
|
|
1619
|
-
if (seen.has(key))
|
|
1620
|
-
return false;
|
|
1621
|
-
seen.add(key);
|
|
1622
|
-
return true;
|
|
1623
|
-
});
|
|
1624
|
-
}
|
|
1625
|
-
else {
|
|
1626
|
-
config.rules = newRules;
|
|
1627
|
-
}
|
|
1628
|
-
fs.mkdirSync(path.dirname(permissionsPath), { recursive: true });
|
|
1629
|
-
fs.writeFileSync(permissionsPath, yaml.stringify(config), 'utf-8');
|
|
1630
|
-
return { success: true };
|
|
1631
|
-
}
|
|
1632
1564
|
if (agentId === 'openclaw') {
|
|
1633
1565
|
// OpenClaw's allowlist lives in ~/.openclaw/openclaw.json under `tools`.
|
|
1634
1566
|
// Only blanket tool-level rules map (see convertToOpenClawFormat). We
|
|
@@ -1788,8 +1720,7 @@ export function exportPermissionsFromPath(filePath) {
|
|
|
1788
1720
|
/**
|
|
1789
1721
|
* Which harness owns `filePath`, by comparing against the trailing path segments
|
|
1790
1722
|
* each registry target declares. Longer (more specific) suffixes win, so
|
|
1791
|
-
|
|
1792
|
-
*/
|
|
1723
|
+
*/
|
|
1793
1724
|
export function detectPermissionAgentFromPath(filePath) {
|
|
1794
1725
|
const normalized = path.resolve(filePath).split(path.sep).join('/');
|
|
1795
1726
|
let best = null;
|
|
@@ -1906,7 +1837,7 @@ export function saveDefaultPermissionSet(set) {
|
|
|
1906
1837
|
* (`lossyBecause` in `permissions-registry.ts`), so a canonical subset check
|
|
1907
1838
|
* would false-diff a correctly-synced home.
|
|
1908
1839
|
*
|
|
1909
|
-
* Every other allowlist harness (codex/grok/kimi/
|
|
1840
|
+
* Every other allowlist harness (codex/grok/kimi/antigravity/hermes/copilot)
|
|
1910
1841
|
* stores a lossy projection — a sandbox flag, whole-tool gate, per-directory
|
|
1911
1842
|
* approval, or a split/merged pattern — with no faithful per-group provenance, so
|
|
1912
1843
|
* doctor stays presence-only there and says so (`detail: 'format cannot verify
|
|
@@ -7,7 +7,22 @@ export declare function shouldAutoSyncTraces(disabled: boolean): boolean;
|
|
|
7
7
|
* Arm a fire-and-forget `agents traces sync` for when this process exits.
|
|
8
8
|
* No-op unless {@link shouldAutoSyncTraces} passes. Best-effort by
|
|
9
9
|
* construction: a missing binary or a stalled child never affects the run.
|
|
10
|
+
* The spawn happens in the exit handler because the upload is async work Node
|
|
11
|
+
* cannot run after `exit` — a watchdog here could never fire.
|
|
10
12
|
*/
|
|
11
13
|
export declare function armRunFinishTraceSync(opts?: {
|
|
12
14
|
disabled?: boolean;
|
|
13
15
|
}): void;
|
|
16
|
+
/**
|
|
17
|
+
* Fire a fire-and-forget `agents traces sync` NOW (not on exit). An important
|
|
18
|
+
* owner-bound ping (`feed post --level important`, `agents notify`,
|
|
19
|
+
* `send --to owner`) links the caller's `…/console/sessions/<id>` page, and that
|
|
20
|
+
* page only exists once the session's shard has been uploaded — trace sync fires
|
|
21
|
+
* on run exit (PHNX-3628), not when a mid-run ping is posted. This closes that
|
|
22
|
+
* gap so the tapped link resolves instead of 404ing. No-op unless
|
|
23
|
+
* {@link shouldAutoSyncTraces} passes (signed in + already opted into the store);
|
|
24
|
+
* the incremental watermark keeps the push to essentially just this session.
|
|
25
|
+
*/
|
|
26
|
+
export declare function fireTraceSyncInBackground(opts?: {
|
|
27
|
+
disabled?: boolean;
|
|
28
|
+
}): void;
|
|
@@ -45,33 +45,55 @@ export function shouldAutoSyncTraces(disabled) {
|
|
|
45
45
|
return false; // never opted into the traces store
|
|
46
46
|
return true;
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Spawn a detached, unref'd `agents traces sync` and return immediately. The one
|
|
50
|
+
* place the fire-and-forget child is built, shared by the run-exit arm and the
|
|
51
|
+
* feed/notify important-post trigger.
|
|
52
|
+
*
|
|
53
|
+
* Re-invoke this CLI through getCliLaunch, NOT a hand-rolled
|
|
54
|
+
* [process.execPath, process.argv[1], …] — under the compiled standalone binary
|
|
55
|
+
* argv[1] is the bun virtual entry (/$bunfs/root/agents), which would become a
|
|
56
|
+
* bogus subcommand and silently no-op the whole feature (cli-entry.ts). The
|
|
57
|
+
* spawned `traces sync` is a fresh process with its own event loop, so its own
|
|
58
|
+
* per-request network timeouts (syncTraces) bound it.
|
|
59
|
+
*/
|
|
60
|
+
function spawnDetachedTraceSync() {
|
|
61
|
+
try {
|
|
62
|
+
const { command, args } = getCliLaunch(['traces', 'sync']);
|
|
63
|
+
const child = spawn(command, args, { detached: true, stdio: 'ignore' });
|
|
64
|
+
// A child that never starts (ENOENT) emits an async 'error'; without a
|
|
65
|
+
// listener Node re-throws it as uncaught. Swallow — this is best-effort.
|
|
66
|
+
child.on('error', () => { });
|
|
67
|
+
child.unref();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// A synchronous spawn failure must never change the caller's outcome.
|
|
71
|
+
}
|
|
72
|
+
}
|
|
48
73
|
/**
|
|
49
74
|
* Arm a fire-and-forget `agents traces sync` for when this process exits.
|
|
50
75
|
* No-op unless {@link shouldAutoSyncTraces} passes. Best-effort by
|
|
51
76
|
* construction: a missing binary or a stalled child never affects the run.
|
|
77
|
+
* The spawn happens in the exit handler because the upload is async work Node
|
|
78
|
+
* cannot run after `exit` — a watchdog here could never fire.
|
|
52
79
|
*/
|
|
53
80
|
export function armRunFinishTraceSync(opts = {}) {
|
|
54
81
|
if (!shouldAutoSyncTraces(!!opts.disabled))
|
|
55
82
|
return;
|
|
56
|
-
process.on('exit',
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
}
|
|
73
|
-
catch {
|
|
74
|
-
// A synchronous spawn failure must never change the run's exit outcome.
|
|
75
|
-
}
|
|
76
|
-
});
|
|
83
|
+
process.on('exit', spawnDetachedTraceSync);
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Fire a fire-and-forget `agents traces sync` NOW (not on exit). An important
|
|
87
|
+
* owner-bound ping (`feed post --level important`, `agents notify`,
|
|
88
|
+
* `send --to owner`) links the caller's `…/console/sessions/<id>` page, and that
|
|
89
|
+
* page only exists once the session's shard has been uploaded — trace sync fires
|
|
90
|
+
* on run exit (PHNX-3628), not when a mid-run ping is posted. This closes that
|
|
91
|
+
* gap so the tapped link resolves instead of 404ing. No-op unless
|
|
92
|
+
* {@link shouldAutoSyncTraces} passes (signed in + already opted into the store);
|
|
93
|
+
* the incremental watermark keeps the push to essentially just this session.
|
|
94
|
+
*/
|
|
95
|
+
export function fireTraceSyncInBackground(opts = {}) {
|
|
96
|
+
if (!shouldAutoSyncTraces(!!opts.disabled))
|
|
97
|
+
return;
|
|
98
|
+
spawnDetachedTraceSync();
|
|
77
99
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rush session freshness — the ONE judgment shared by every consumer of
|
|
3
|
+
* ~/.rush/user.yaml (cloud dispatch, cloud session source, secrets sync driver).
|
|
4
|
+
*
|
|
5
|
+
* A Rush session stores `expires_at` as Unix seconds. Two values mean "never
|
|
6
|
+
* expires" and MUST NOT be read as an absolute timestamp:
|
|
7
|
+
* - `0` — an opaque Phoenix `pid_` bearer written by `rush login`;
|
|
8
|
+
* `0` = non-expiring by contract (mirrors rush/cli's isFresh,
|
|
9
|
+
* the same fix RUSH-1310 landed for the daemon-less freshness
|
|
10
|
+
* check and PHNX-3637 landed for the release endpoints).
|
|
11
|
+
* - `undefined` — a token written without an expiry.
|
|
12
|
+
*
|
|
13
|
+
* Reading `expires_at: 0` as epoch-1970 was PHNX-3645: every Phoenix-authed box
|
|
14
|
+
* had its valid, non-expiring session rejected as "Rush session expired at
|
|
15
|
+
* 1970-01-01", so `agents run --cloud`, `agents secrets push/pull` over Rush,
|
|
16
|
+
* and cloud session discovery all refused to run. Every consumer that parses
|
|
17
|
+
* freshness routes through this predicate so the special-case lives in one place.
|
|
18
|
+
*/
|
|
19
|
+
export declare function isRushSessionExpired(expiresAt: number | undefined): boolean;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rush session freshness — the ONE judgment shared by every consumer of
|
|
3
|
+
* ~/.rush/user.yaml (cloud dispatch, cloud session source, secrets sync driver).
|
|
4
|
+
*
|
|
5
|
+
* A Rush session stores `expires_at` as Unix seconds. Two values mean "never
|
|
6
|
+
* expires" and MUST NOT be read as an absolute timestamp:
|
|
7
|
+
* - `0` — an opaque Phoenix `pid_` bearer written by `rush login`;
|
|
8
|
+
* `0` = non-expiring by contract (mirrors rush/cli's isFresh,
|
|
9
|
+
* the same fix RUSH-1310 landed for the daemon-less freshness
|
|
10
|
+
* check and PHNX-3637 landed for the release endpoints).
|
|
11
|
+
* - `undefined` — a token written without an expiry.
|
|
12
|
+
*
|
|
13
|
+
* Reading `expires_at: 0` as epoch-1970 was PHNX-3645: every Phoenix-authed box
|
|
14
|
+
* had its valid, non-expiring session rejected as "Rush session expired at
|
|
15
|
+
* 1970-01-01", so `agents run --cloud`, `agents secrets push/pull` over Rush,
|
|
16
|
+
* and cloud session discovery all refused to run. Every consumer that parses
|
|
17
|
+
* freshness routes through this predicate so the special-case lives in one place.
|
|
18
|
+
*/
|
|
19
|
+
export function isRushSessionExpired(expiresAt) {
|
|
20
|
+
// `0` (non-expiring pid_ bearer) and a missing value are never expired.
|
|
21
|
+
if (typeof expiresAt !== 'number' || expiresAt === 0)
|
|
22
|
+
return false;
|
|
23
|
+
return expiresAt <= Date.now() / 1000;
|
|
24
|
+
}
|
|
@@ -13,6 +13,7 @@ import * as fs from 'fs';
|
|
|
13
13
|
import * as os from 'os';
|
|
14
14
|
import * as path from 'path';
|
|
15
15
|
import * as yaml from 'yaml';
|
|
16
|
+
import { isRushSessionExpired } from '../../rush-session.js';
|
|
16
17
|
const PROXY_BASE = 'https://api.prix.dev';
|
|
17
18
|
const USER_YAML = path.join(os.homedir(), '.rush', 'user.yaml');
|
|
18
19
|
const BUNDLE_ENDPOINT = '/api/v1/secrets/bundles';
|
|
@@ -27,7 +28,7 @@ function readRushToken() {
|
|
|
27
28
|
throw new Error('No session token in ~/.rush/user.yaml. Run `rush login` first.');
|
|
28
29
|
}
|
|
29
30
|
const expiresAt = data.session?.expires_at;
|
|
30
|
-
if (
|
|
31
|
+
if (isRushSessionExpired(expiresAt)) {
|
|
31
32
|
const expiredAt = new Date(expiresAt * 1000).toISOString();
|
|
32
33
|
throw new Error(`Rush session expired at ${expiredAt}. Run \`rush login\` to refresh.`);
|
|
33
34
|
}
|