@phnx-labs/agents-cli 1.20.63 → 1.20.65
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +56 -3
- package/README.md +46 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/apply.d.ts +12 -0
- package/dist/commands/apply.js +274 -0
- package/dist/commands/browser.js +2 -2
- package/dist/commands/cloud.js +32 -2
- package/dist/commands/doctor.js +4 -1
- package/dist/commands/exec.js +149 -77
- package/dist/commands/feed.d.ts +4 -0
- package/dist/commands/feed.js +52 -19
- package/dist/commands/hosts.js +44 -6
- package/dist/commands/lease.d.ts +23 -0
- package/dist/commands/lease.js +201 -0
- package/dist/commands/mailboxes.d.ts +20 -0
- package/dist/commands/mailboxes.js +390 -0
- package/dist/commands/mcp.js +55 -5
- package/dist/commands/monitors.d.ts +12 -0
- package/dist/commands/monitors.js +740 -0
- package/dist/commands/output.js +2 -2
- package/dist/commands/routines.js +43 -16
- package/dist/commands/secrets.d.ts +16 -0
- package/dist/commands/secrets.js +215 -64
- package/dist/commands/serve.js +31 -0
- package/dist/commands/sessions-export.d.ts +2 -0
- package/dist/commands/sessions-export.js +284 -0
- package/dist/commands/sessions-import.d.ts +2 -0
- package/dist/commands/sessions-import.js +230 -0
- package/dist/commands/sessions.d.ts +16 -0
- package/dist/commands/sessions.js +40 -6
- package/dist/commands/ssh.js +143 -5
- package/dist/commands/usage.d.ts +2 -0
- package/dist/commands/usage.js +7 -2
- package/dist/commands/versions.js +7 -3
- package/dist/commands/view.d.ts +27 -1
- package/dist/commands/view.js +32 -9
- package/dist/commands/webhook.js +10 -2
- package/dist/index.js +35 -14
- package/dist/lib/agents.d.ts +36 -0
- package/dist/lib/agents.js +80 -20
- package/dist/lib/auto-dispatch-provider.js +7 -2
- package/dist/lib/auto-dispatch.d.ts +3 -0
- package/dist/lib/auto-dispatch.js +3 -0
- package/dist/lib/browser/chrome.js +2 -2
- package/dist/lib/browser/drivers/ssh.js +19 -2
- package/dist/lib/cloud/antigravity.js +2 -2
- package/dist/lib/cloud/host.d.ts +59 -0
- package/dist/lib/cloud/host.js +224 -0
- package/dist/lib/cloud/registry.js +4 -0
- package/dist/lib/cloud/types.d.ts +6 -4
- package/dist/lib/comms-render.d.ts +37 -0
- package/dist/lib/comms-render.js +89 -0
- package/dist/lib/computer-rpc.js +3 -1
- package/dist/lib/crabbox/cli.d.ts +72 -0
- package/dist/lib/crabbox/cli.js +162 -9
- package/dist/lib/crabbox/runtimes.d.ts +13 -0
- package/dist/lib/crabbox/runtimes.js +35 -2
- package/dist/lib/daemon.d.ts +20 -4
- package/dist/lib/daemon.js +68 -20
- package/dist/lib/devices/fleet.d.ts +3 -2
- package/dist/lib/devices/fleet.js +9 -0
- package/dist/lib/devices/health.d.ts +77 -0
- package/dist/lib/devices/health.js +186 -0
- package/dist/lib/devices/registry.d.ts +15 -0
- package/dist/lib/devices/registry.js +9 -0
- package/dist/lib/exec.d.ts +19 -2
- package/dist/lib/exec.js +41 -13
- package/dist/lib/fleet/apply.d.ts +63 -0
- package/dist/lib/fleet/apply.js +214 -0
- package/dist/lib/fleet/auth-sync.d.ts +67 -0
- package/dist/lib/fleet/auth-sync.js +142 -0
- package/dist/lib/fleet/manifest.d.ts +29 -0
- package/dist/lib/fleet/manifest.js +127 -0
- package/dist/lib/fleet/types.d.ts +129 -0
- package/dist/lib/fleet/types.js +13 -0
- package/dist/lib/git.d.ts +27 -0
- package/dist/lib/git.js +34 -2
- package/dist/lib/hosts/dispatch.d.ts +29 -8
- package/dist/lib/hosts/dispatch.js +46 -18
- package/dist/lib/hosts/passthrough.js +2 -0
- package/dist/lib/hosts/providers/devices.d.ts +27 -0
- package/dist/lib/hosts/providers/devices.js +98 -0
- package/dist/lib/hosts/registry.d.ts +10 -16
- package/dist/lib/hosts/registry.js +17 -50
- package/dist/lib/hosts/remote-cmd.d.ts +23 -0
- package/dist/lib/hosts/remote-cmd.js +71 -0
- package/dist/lib/hosts/run-target.d.ts +84 -0
- package/dist/lib/hosts/run-target.js +99 -0
- package/dist/lib/hosts/types.d.ts +23 -5
- package/dist/lib/hosts/types.js +22 -4
- package/dist/lib/linear-autoclose.d.ts +30 -0
- package/dist/lib/linear-autoclose.js +22 -0
- package/dist/lib/mailbox.d.ts +39 -0
- package/dist/lib/mailbox.js +112 -0
- package/dist/lib/mcp.d.ts +27 -1
- package/dist/lib/mcp.js +126 -12
- package/dist/lib/monitors/config.d.ts +161 -0
- package/dist/lib/monitors/config.js +372 -0
- package/dist/lib/monitors/dispatch.d.ts +28 -0
- package/dist/lib/monitors/dispatch.js +91 -0
- package/dist/lib/monitors/engine.d.ts +61 -0
- package/dist/lib/monitors/engine.js +201 -0
- package/dist/lib/monitors/sources/command.d.ts +11 -0
- package/dist/lib/monitors/sources/command.js +31 -0
- package/dist/lib/monitors/sources/device.d.ts +13 -0
- package/dist/lib/monitors/sources/device.js +35 -0
- package/dist/lib/monitors/sources/file.d.ts +14 -0
- package/dist/lib/monitors/sources/file.js +57 -0
- package/dist/lib/monitors/sources/http.d.ts +10 -0
- package/dist/lib/monitors/sources/http.js +34 -0
- package/dist/lib/monitors/sources/index.d.ts +14 -0
- package/dist/lib/monitors/sources/index.js +31 -0
- package/dist/lib/monitors/sources/poll.d.ts +9 -0
- package/dist/lib/monitors/sources/poll.js +9 -0
- package/dist/lib/monitors/sources/types.d.ts +18 -0
- package/dist/lib/monitors/sources/types.js +9 -0
- package/dist/lib/monitors/sources/webhook.d.ts +23 -0
- package/dist/lib/monitors/sources/webhook.js +47 -0
- package/dist/lib/monitors/sources/ws.d.ts +14 -0
- package/dist/lib/monitors/sources/ws.js +45 -0
- package/dist/lib/monitors/state.d.ts +69 -0
- package/dist/lib/monitors/state.js +144 -0
- package/dist/lib/paths.d.ts +13 -0
- package/dist/lib/paths.js +26 -4
- package/dist/lib/platform/exec.d.ts +16 -0
- package/dist/lib/platform/exec.js +17 -0
- package/dist/lib/plugins.js +101 -2
- package/dist/lib/redact.d.ts +14 -1
- package/dist/lib/redact.js +47 -1
- package/dist/lib/remote-agents-json.js +7 -1
- package/dist/lib/rotate.d.ts +6 -3
- package/dist/lib/rotate.js +0 -1
- package/dist/lib/routines.d.ts +37 -2
- package/dist/lib/routines.js +54 -12
- package/dist/lib/runner.d.ts +1 -0
- package/dist/lib/runner.js +348 -13
- package/dist/lib/sandbox.d.ts +9 -1
- package/dist/lib/sandbox.js +11 -2
- package/dist/lib/secrets/agent.d.ts +48 -10
- package/dist/lib/secrets/agent.js +123 -15
- package/dist/lib/secrets/bundles.d.ts +26 -0
- package/dist/lib/secrets/bundles.js +59 -8
- package/dist/lib/secrets/mcp.js +4 -2
- package/dist/lib/secrets/remote.d.ts +17 -0
- package/dist/lib/secrets/remote.js +40 -0
- package/dist/lib/self-update.d.ts +20 -0
- package/dist/lib/self-update.js +54 -1
- package/dist/lib/serve/control.d.ts +95 -0
- package/dist/lib/serve/control.js +260 -0
- package/dist/lib/serve/server.d.ts +35 -1
- package/dist/lib/serve/server.js +106 -76
- package/dist/lib/serve/stream.d.ts +43 -0
- package/dist/lib/serve/stream.js +116 -0
- package/dist/lib/serve/token.d.ts +35 -0
- package/dist/lib/serve/token.js +85 -0
- package/dist/lib/session/bundle.d.ts +164 -0
- package/dist/lib/session/bundle.js +200 -0
- package/dist/lib/session/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote-bundle.js +61 -0
- package/dist/lib/session/remote-list.js +5 -1
- package/dist/lib/session/state.d.ts +7 -25
- package/dist/lib/session/state.js +16 -6
- package/dist/lib/session/sync/agents.d.ts +54 -6
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/sync/config.js +8 -2
- package/dist/lib/session/sync/manifest.d.ts +14 -3
- package/dist/lib/session/sync/manifest.js +4 -0
- package/dist/lib/session/sync/sync.d.ts +23 -2
- package/dist/lib/session/sync/sync.js +177 -74
- package/dist/lib/session/types.d.ts +30 -0
- package/dist/lib/ssh-tunnel.d.ts +19 -1
- package/dist/lib/ssh-tunnel.js +99 -8
- package/dist/lib/staleness/detectors/subagents.d.ts +5 -0
- package/dist/lib/staleness/detectors/subagents.js +5 -192
- package/dist/lib/staleness/writers/subagents.d.ts +10 -0
- package/dist/lib/staleness/writers/subagents.js +11 -102
- package/dist/lib/startup/command-registry.d.ts +4 -0
- package/dist/lib/startup/command-registry.js +9 -0
- package/dist/lib/state.d.ts +5 -0
- package/dist/lib/state.js +12 -0
- package/dist/lib/subagents-registry.d.ts +85 -0
- package/dist/lib/subagents-registry.js +393 -0
- package/dist/lib/subagents.d.ts +8 -8
- package/dist/lib/subagents.js +32 -663
- package/dist/lib/sync-umbrella.d.ts +1 -0
- package/dist/lib/sync-umbrella.js +14 -3
- package/dist/lib/tmux/session.d.ts +7 -0
- package/dist/lib/tmux/session.js +3 -1
- package/dist/lib/triggers/webhook.d.ts +18 -0
- package/dist/lib/triggers/webhook.js +105 -0
- package/dist/lib/types.d.ts +35 -1
- package/dist/lib/usage.d.ts +42 -3
- package/dist/lib/usage.js +163 -21
- package/dist/lib/versions.js +14 -11
- package/dist/lib/workflows.d.ts +20 -0
- package/dist/lib/workflows.js +24 -0
- package/package.json +2 -1
|
@@ -118,9 +118,20 @@ export async function runUmbrellaSync(args) {
|
|
|
118
118
|
const { isSyncConfigured } = await import('./session/sync/config.js');
|
|
119
119
|
if (isBetaEnabled('session-sync') && isSyncConfigured()) {
|
|
120
120
|
const { syncSessions } = await import('./session/sync/sync.js');
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
121
|
+
// Record a failure here instead of letting it throw: like the fetchSecrets
|
|
122
|
+
// block above, a failure in one fetch stage must not abort the others or
|
|
123
|
+
// the reconcile below (see this function's doc comment). Without this,
|
|
124
|
+
// syncSessions() throwing — e.g. via the C1 containment reject on a
|
|
125
|
+
// malicious peer manifest — would skip `plan.reconcile` entirely.
|
|
126
|
+
try {
|
|
127
|
+
const r = await syncSessions();
|
|
128
|
+
result.sessions = { ran: true, pushed: r.pushed, pulled: r.pulled, merged: r.merged };
|
|
129
|
+
log(`sessions: pushed ${r.pushed}, pulled ${r.pulled}, merged ${r.merged}`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
result.sessions = { ran: false, pushed: 0, pulled: 0, merged: 0, error: err.message };
|
|
133
|
+
log(`sessions: failed — ${err.message}`);
|
|
134
|
+
}
|
|
124
135
|
}
|
|
125
136
|
else {
|
|
126
137
|
result.sessions = { ran: false, pushed: 0, pulled: 0, merged: 0 };
|
|
@@ -33,6 +33,13 @@ export interface SessionMeta {
|
|
|
33
33
|
export interface CreateSessionOptions {
|
|
34
34
|
name: string;
|
|
35
35
|
cmd?: string;
|
|
36
|
+
/**
|
|
37
|
+
* Redacted copy of `cmd` to persist in SessionMeta.cmd instead of the real
|
|
38
|
+
* command. When set, `cmd` is still executed but only `metaCmd` is written to
|
|
39
|
+
* disk — used to keep resolved secret values out of the informational cmd
|
|
40
|
+
* field (see exec.ts buildTmuxAgentCommand, RUSH-1758). Defaults to `cmd`.
|
|
41
|
+
*/
|
|
42
|
+
metaCmd?: string;
|
|
36
43
|
cwd?: string;
|
|
37
44
|
env?: NodeJS.ProcessEnv;
|
|
38
45
|
socket?: string;
|
package/dist/lib/tmux/session.js
CHANGED
|
@@ -106,7 +106,9 @@ export async function createSession(opts) {
|
|
|
106
106
|
name: opts.name,
|
|
107
107
|
socket,
|
|
108
108
|
createdAt: Date.now(),
|
|
109
|
-
|
|
109
|
+
// Persist the redacted copy when provided so resolved secret values in the
|
|
110
|
+
// launch command never land on disk (RUSH-1758).
|
|
111
|
+
cmd: opts.metaCmd ?? opts.cmd,
|
|
110
112
|
cwd: opts.cwd,
|
|
111
113
|
source: opts.source ?? 'cli',
|
|
112
114
|
labels: opts.labels,
|
|
@@ -99,6 +99,18 @@ export interface DeliveryStore {
|
|
|
99
99
|
markJob(id: string, jobName: string): void;
|
|
100
100
|
}
|
|
101
101
|
export declare function createMemoryDeliveryStore(maxEntries?: number): DeliveryStore;
|
|
102
|
+
/**
|
|
103
|
+
* A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
|
|
104
|
+
* seen delivery ids survive a process restart and are bounded by AGE, not by a
|
|
105
|
+
* fixed entry count — so a captured valid delivery cannot re-fire after a
|
|
106
|
+
* restart or after count-based LRU eviction would have dropped it.
|
|
107
|
+
*
|
|
108
|
+
* `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
|
|
109
|
+
* still on record (younger than the window) is rejected as a duplicate; entries
|
|
110
|
+
* older than the window are pruned (keeping the file bounded) since a webhook
|
|
111
|
+
* source will not legitimately retry a delivery that old.
|
|
112
|
+
*/
|
|
113
|
+
export declare function createFileDeliveryStore(filePath: string, retentionMs?: number): DeliveryStore;
|
|
102
114
|
export interface RateLimiter {
|
|
103
115
|
take(key: string): boolean;
|
|
104
116
|
}
|
|
@@ -117,6 +129,12 @@ export interface WebhookServerOptions {
|
|
|
117
129
|
rateLimiter?: RateLimiter;
|
|
118
130
|
rateLimitPerMinute?: number;
|
|
119
131
|
maxBodyBytes?: number;
|
|
132
|
+
/** Per-IP ingress throttle applied BEFORE the body read (bad-sig flood guard). */
|
|
133
|
+
ipRateLimiter?: RateLimiter;
|
|
134
|
+
/** Per-source-IP requests/minute allowed through to the body read. Default 120. */
|
|
135
|
+
ipRateLimitPerMinute?: number;
|
|
136
|
+
/** Max concurrent TCP connections the receiver accepts. Default 256. */
|
|
137
|
+
maxConnections?: number;
|
|
120
138
|
}
|
|
121
139
|
/**
|
|
122
140
|
* Start a localhost-bound receiver. It accepts only:
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
* verification, idempotency, source allow-listing, and rate limiting.
|
|
13
13
|
*/
|
|
14
14
|
import * as crypto from 'crypto';
|
|
15
|
+
import * as fs from 'fs';
|
|
15
16
|
import * as http from 'http';
|
|
17
|
+
import * as path from 'path';
|
|
16
18
|
import { jobRunsOnThisDevice, listJobs } from '../routines.js';
|
|
17
19
|
import { executeJobDetached } from '../runner.js';
|
|
18
20
|
/** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
|
|
@@ -236,6 +238,88 @@ export function createMemoryDeliveryStore(maxEntries = 1000) {
|
|
|
236
238
|
},
|
|
237
239
|
};
|
|
238
240
|
}
|
|
241
|
+
/**
|
|
242
|
+
* A durable, disk-backed delivery store. Unlike `createMemoryDeliveryStore`,
|
|
243
|
+
* seen delivery ids survive a process restart and are bounded by AGE, not by a
|
|
244
|
+
* fixed entry count — so a captured valid delivery cannot re-fire after a
|
|
245
|
+
* restart or after count-based LRU eviction would have dropped it.
|
|
246
|
+
*
|
|
247
|
+
* `retentionMs` doubles as the replay-acceptance window: a delivery whose id is
|
|
248
|
+
* still on record (younger than the window) is rejected as a duplicate; entries
|
|
249
|
+
* older than the window are pruned (keeping the file bounded) since a webhook
|
|
250
|
+
* source will not legitimately retry a delivery that old.
|
|
251
|
+
*/
|
|
252
|
+
export function createFileDeliveryStore(filePath, retentionMs = 14 * 24 * 60 * 60 * 1000) {
|
|
253
|
+
const seen = new Map();
|
|
254
|
+
// Load persisted state (best-effort: a corrupt/missing file starts empty).
|
|
255
|
+
try {
|
|
256
|
+
const raw = JSON.parse(fs.readFileSync(filePath, 'utf-8'));
|
|
257
|
+
const loadedAt = Date.now();
|
|
258
|
+
for (const [id, entry] of Object.entries(raw)) {
|
|
259
|
+
if (typeof entry?.updatedAt !== 'number' || loadedAt - entry.updatedAt > retentionMs)
|
|
260
|
+
continue;
|
|
261
|
+
seen.set(id, {
|
|
262
|
+
complete: entry.complete === true,
|
|
263
|
+
jobs: new Set(Array.isArray(entry.jobs) ? entry.jobs : []),
|
|
264
|
+
updatedAt: entry.updatedAt,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
catch {
|
|
269
|
+
// no prior file / unreadable — start empty
|
|
270
|
+
}
|
|
271
|
+
const persist = () => {
|
|
272
|
+
const snapshot = {};
|
|
273
|
+
for (const [id, entry] of seen) {
|
|
274
|
+
snapshot[id] = { complete: entry.complete, jobs: [...entry.jobs], updatedAt: entry.updatedAt };
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
278
|
+
const tmp = `${filePath}.${process.pid}.tmp`;
|
|
279
|
+
fs.writeFileSync(tmp, JSON.stringify(snapshot), 'utf-8');
|
|
280
|
+
fs.renameSync(tmp, filePath);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
// best-effort durability; an unwritable dir must not crash ingress
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
const prune = (now) => {
|
|
287
|
+
for (const [id, entry] of seen) {
|
|
288
|
+
if (now - entry.updatedAt > retentionMs)
|
|
289
|
+
seen.delete(id);
|
|
290
|
+
}
|
|
291
|
+
};
|
|
292
|
+
const touch = (id) => {
|
|
293
|
+
const now = Date.now();
|
|
294
|
+
prune(now);
|
|
295
|
+
let current = seen.get(id);
|
|
296
|
+
if (!current) {
|
|
297
|
+
current = { complete: false, jobs: new Set(), updatedAt: now };
|
|
298
|
+
seen.set(id, current);
|
|
299
|
+
}
|
|
300
|
+
current.updatedAt = now;
|
|
301
|
+
return current;
|
|
302
|
+
};
|
|
303
|
+
return {
|
|
304
|
+
seen: (id) => {
|
|
305
|
+
const entry = seen.get(id);
|
|
306
|
+
if (!entry)
|
|
307
|
+
return false;
|
|
308
|
+
if (Date.now() - entry.updatedAt > retentionMs)
|
|
309
|
+
return false;
|
|
310
|
+
return entry.complete === true;
|
|
311
|
+
},
|
|
312
|
+
mark: (id) => {
|
|
313
|
+
touch(id).complete = true;
|
|
314
|
+
persist();
|
|
315
|
+
},
|
|
316
|
+
completedJobs: (id) => new Set(seen.get(id)?.jobs ?? []),
|
|
317
|
+
markJob: (id, jobName) => {
|
|
318
|
+
touch(id).jobs.add(jobName);
|
|
319
|
+
persist();
|
|
320
|
+
},
|
|
321
|
+
};
|
|
322
|
+
}
|
|
239
323
|
export function createMemoryRateLimiter(limit, windowMs) {
|
|
240
324
|
const buckets = new Map();
|
|
241
325
|
return {
|
|
@@ -285,6 +369,7 @@ async function readRawBody(req, maxBytes) {
|
|
|
285
369
|
export function startWebhookServer(options) {
|
|
286
370
|
const deliveryStore = options.deliveryStore ?? createMemoryDeliveryStore();
|
|
287
371
|
const rateLimiter = options.rateLimiter ?? createMemoryRateLimiter(options.rateLimitPerMinute ?? 60, 60_000);
|
|
372
|
+
const ipRateLimiter = options.ipRateLimiter ?? createMemoryRateLimiter(options.ipRateLimitPerMinute ?? 120, 60_000);
|
|
288
373
|
const maxBodyBytes = options.maxBodyBytes ?? 1024 * 1024;
|
|
289
374
|
const server = http.createServer((req, res) => {
|
|
290
375
|
void (async () => {
|
|
@@ -305,6 +390,23 @@ export function startWebhookServer(options) {
|
|
|
305
390
|
res.end(JSON.stringify({ ok: false, error: `missing ${source} webhook secret` }));
|
|
306
391
|
return;
|
|
307
392
|
}
|
|
393
|
+
// Per-IP throttle + declared-size cap BEFORE the (expensive) body read +
|
|
394
|
+
// HMAC. A bad-signature flood of 1 MiB POSTs must be rejected without
|
|
395
|
+
// forcing a full body read and an HMAC per request — the signed-delivery
|
|
396
|
+
// rate limit further down runs only after a signature passes, so it can't
|
|
397
|
+
// shed this load on its own.
|
|
398
|
+
const ip = req.socket.remoteAddress ?? 'unknown';
|
|
399
|
+
if (!ipRateLimiter.take(ip)) {
|
|
400
|
+
res.writeHead(429, { 'content-type': 'application/json' });
|
|
401
|
+
res.end(JSON.stringify({ ok: false, error: 'rate limit exceeded' }));
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
const declaredLength = Number.parseInt(header(req.headers, 'content-length') ?? '', 10);
|
|
405
|
+
if (Number.isFinite(declaredLength) && declaredLength > maxBodyBytes) {
|
|
406
|
+
res.writeHead(413, { 'content-type': 'application/json' });
|
|
407
|
+
res.end(JSON.stringify({ ok: false, error: `payload exceeds ${maxBodyBytes} bytes` }));
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
308
410
|
try {
|
|
309
411
|
const rawBody = await readRawBody(req, maxBodyBytes);
|
|
310
412
|
const valid = source === 'github'
|
|
@@ -357,6 +459,9 @@ export function startWebhookServer(options) {
|
|
|
357
459
|
}
|
|
358
460
|
})();
|
|
359
461
|
});
|
|
462
|
+
// Connection cap: bound how many concurrent TCP connections the receiver
|
|
463
|
+
// will hold open, so a flood cannot exhaust file descriptors / memory.
|
|
464
|
+
server.maxConnections = options.maxConnections ?? 256;
|
|
360
465
|
server.listen(options.port ?? 0, options.host ?? '127.0.0.1');
|
|
361
466
|
return server;
|
|
362
467
|
}
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -515,6 +515,20 @@ export interface PluginManifest {
|
|
|
515
515
|
userConfig?: PluginUserConfigField[];
|
|
516
516
|
/** Other plugin names this plugin depends on. Missing deps produce a warning. */
|
|
517
517
|
dependencies?: string[];
|
|
518
|
+
/**
|
|
519
|
+
* Inline hook configuration (or a path to a hooks JSON file) declared directly
|
|
520
|
+
* in the manifest, per the official plugin format — an execution surface even
|
|
521
|
+
* when the plugin ships no `hooks/` directory. Untyped because the shape is a
|
|
522
|
+
* path string or an inline event map; capability detection only needs to know
|
|
523
|
+
* whether it is present and non-empty.
|
|
524
|
+
*/
|
|
525
|
+
hooks?: unknown;
|
|
526
|
+
/**
|
|
527
|
+
* Inline MCP-server configuration (or a path to an MCP JSON file) declared
|
|
528
|
+
* directly in the manifest — an execution surface even when the plugin ships
|
|
529
|
+
* no `.mcp.json`. Untyped for the same reason as `hooks`.
|
|
530
|
+
*/
|
|
531
|
+
mcpServers?: unknown;
|
|
518
532
|
}
|
|
519
533
|
/** A plugin found on disk with its parsed manifest and resource inventory. */
|
|
520
534
|
export interface DiscoveredPlugin {
|
|
@@ -624,15 +638,28 @@ export interface ExtraRepoConfig {
|
|
|
624
638
|
export interface Meta {
|
|
625
639
|
agents?: Partial<Record<AgentId, string>>;
|
|
626
640
|
run?: RunConfig;
|
|
641
|
+
/**
|
|
642
|
+
* `agents run --lease` config. `secretsBundle` names the keychain secrets bundle
|
|
643
|
+
* whose provider token (e.g. `HCLOUD_TOKEN`) crabbox uses to reach the cloud API.
|
|
644
|
+
* When unset, the bundle is resolved by env (`AGENTS_LEASE_SECRETS_BUNDLE`) then
|
|
645
|
+
* auto-detected (the first bundle that declares a provider token key).
|
|
646
|
+
*/
|
|
647
|
+
lease?: {
|
|
648
|
+
secretsBundle?: string;
|
|
649
|
+
};
|
|
627
650
|
/** macOS secrets-agent config. `policy` is the default prompt policy for
|
|
628
651
|
* bundles without an explicit per-bundle policy: `daily` (the default) asks
|
|
629
652
|
* once per ~7 days, `always` asks every time. `auto` (default on) lets the
|
|
630
653
|
* first real keychain read of a `daily` bundle populate the broker so
|
|
631
|
-
* concurrent runs read silently — set it `false` to force a prompt on every read.
|
|
654
|
+
* concurrent runs read silently — set it `false` to force a prompt on every read.
|
|
655
|
+
* `holdMs` caps how long an unlocked/auto-cached bundle is held before the next
|
|
656
|
+
* read re-prompts (default 7 days; e.g. 86400000 for a 24h cap). Clamped to
|
|
657
|
+
* [1 minute, 30 days]. */
|
|
632
658
|
secrets?: {
|
|
633
659
|
policy?: 'always' | 'daily';
|
|
634
660
|
agent?: {
|
|
635
661
|
auto?: boolean;
|
|
662
|
+
holdMs?: number;
|
|
636
663
|
};
|
|
637
664
|
};
|
|
638
665
|
/** Spend guardrails (issue #346). User-global caps; project agents.yaml overrides. */
|
|
@@ -689,6 +716,13 @@ export interface Meta {
|
|
|
689
716
|
* are never copied. `inline` hosts carry their own address/user.
|
|
690
717
|
*/
|
|
691
718
|
hosts?: Record<string, HostEntry>;
|
|
719
|
+
/**
|
|
720
|
+
* Declarative fleet profile (`agents apply` / `ag apply`). Additive to the
|
|
721
|
+
* schema — project `agents:` version-pins are untouched. Declares which agents
|
|
722
|
+
* every device should have, which config to sync, and how login propagates.
|
|
723
|
+
* Full shape in `lib/fleet/types.ts` (FleetManifest).
|
|
724
|
+
*/
|
|
725
|
+
fleet?: import('./fleet/types.js').FleetManifest;
|
|
692
726
|
}
|
|
693
727
|
/** Persisted agent-host entry in agents.yaml (overlay or inline). */
|
|
694
728
|
export interface HostEntry {
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type AccountInfo } from './agents.js';
|
|
2
2
|
import type { AgentId } from './types.js';
|
|
3
3
|
/** Discriminator for usage window types. */
|
|
4
|
-
export type UsageWindowKey = 'session' | 'week' | 'sonnet_week';
|
|
4
|
+
export type UsageWindowKey = 'session' | 'week' | 'sonnet_week' | 'month';
|
|
5
5
|
/** A single rate-limit window with utilization percentage and reset time. */
|
|
6
6
|
export interface UsageWindow {
|
|
7
7
|
key: UsageWindowKey;
|
|
@@ -154,7 +154,46 @@ export interface KimiUsagesResponse {
|
|
|
154
154
|
export declare function normalizeKimiWindows(data: KimiUsagesResponse): UsageWindow[];
|
|
155
155
|
/** Derive a display plan label from Kimi's membership tier or subscription type. */
|
|
156
156
|
export declare function formatKimiPlan(data: KimiUsagesResponse): string | null;
|
|
157
|
-
/**
|
|
157
|
+
/** A single Droid token-rate-limit window from /api/billing/limits. */
|
|
158
|
+
interface DroidLimitWindow {
|
|
159
|
+
usedPercent?: number | null;
|
|
160
|
+
windowEnd?: string | null;
|
|
161
|
+
}
|
|
162
|
+
/** Response shape from Factory's billing limits endpoint (subset we render). */
|
|
163
|
+
export interface DroidBillingLimitsResponse {
|
|
164
|
+
usesTokenRateLimitsBilling?: boolean | null;
|
|
165
|
+
limits?: {
|
|
166
|
+
standard?: {
|
|
167
|
+
fiveHour?: DroidLimitWindow | null;
|
|
168
|
+
weekly?: DroidLimitWindow | null;
|
|
169
|
+
monthly?: DroidLimitWindow | null;
|
|
170
|
+
} | null;
|
|
171
|
+
} | null;
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
175
|
+
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
|
176
|
+
* meaningful windows, so they render nothing — mirrors droid's own gate on
|
|
177
|
+
* `usesTokenRateLimitsBilling` before it reads `limits.standard`.
|
|
178
|
+
*/
|
|
179
|
+
export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse): UsageWindow[];
|
|
180
|
+
/**
|
|
181
|
+
* Load a version home's Claude OAuth credential from the two stores Claude Code
|
|
182
|
+
* uses, tried in order:
|
|
183
|
+
*
|
|
184
|
+
* 1. The OS keychain (`getKeychainToken`). Canonical on macOS — Claude Code
|
|
185
|
+
* writes the token to the login keychain and we read it via `/usr/bin/security`.
|
|
186
|
+
* 2. `<home>/.claude/.credentials.json`. On a headless Linux box (the
|
|
187
|
+
* `agents view --host <linux>` case) there is no reachable Secret Service, so
|
|
188
|
+
* the Claude CLI stores its OAuth token in this plaintext file instead. The
|
|
189
|
+
* keychain read above finds nothing on that platform, so we fall back to the
|
|
190
|
+
* file. Same wrapped `{ claudeAiOauth }` shape, so one parser handles both.
|
|
191
|
+
* Mirrors `readClaudeCredentialsBlob` (cloud/rush.ts), the proven pattern.
|
|
192
|
+
*
|
|
193
|
+
* Without step 2 the live usage fetch got no token on Linux, so `agents view`
|
|
194
|
+
* (run remotely over SSH by `--host`) rendered no usage bars even though the
|
|
195
|
+
* account + plan — read from the plaintext `.claude.json` — showed fine.
|
|
196
|
+
*/
|
|
158
197
|
export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
|
|
159
198
|
/**
|
|
160
199
|
* Derive the Keychain service name for a Claude home directory.
|
package/dist/lib/usage.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Usage and rate-limit tracking for Claude and
|
|
2
|
+
* Usage and rate-limit tracking for Claude, Codex, Kimi, and Droid agents.
|
|
3
3
|
*
|
|
4
|
-
* Fetches live usage data from
|
|
4
|
+
* Fetches live usage data from each agent's usage API (Anthropic OAuth for
|
|
5
|
+
* Claude, Kimi Code /usages, Factory billing limits for Droid) or parses
|
|
5
6
|
* rate-limit events from Codex session logs. Results are normalized into a
|
|
6
7
|
* common UsageSnapshot shape, cached to disk, and rendered as terminal
|
|
7
8
|
* progress bars for the `agents view` command.
|
|
@@ -14,6 +15,7 @@ import * as path from 'path';
|
|
|
14
15
|
import * as readline from 'readline';
|
|
15
16
|
import { promisify } from 'util';
|
|
16
17
|
import chalk from 'chalk';
|
|
18
|
+
import { decodeJwtPayload, decryptDroidAuthPayload } from './agents.js';
|
|
17
19
|
import { walkForFiles } from './fs-walk.js';
|
|
18
20
|
import { getKeychainToken, setKeychainToken, deleteKeychainToken, } from './secrets/index.js';
|
|
19
21
|
import { getCacheDir } from './state.js';
|
|
@@ -34,6 +36,7 @@ const CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
|
|
|
34
36
|
const getClaudeUsageCachePath = () => path.join(getCacheDir(), 'claude-usage.json');
|
|
35
37
|
const CACHED_CLAUDE_USAGE_SOURCE_LABEL = 'last seen live account data';
|
|
36
38
|
const KIMI_USAGES_URL = 'https://api.kimi.com/coding/v1/usages';
|
|
39
|
+
const DROID_USAGE_URL = 'https://api.factory.ai/api/billing/limits';
|
|
37
40
|
const COMPACT_BAR_LEN = 5;
|
|
38
41
|
const USAGE_BAR_LEN = 10;
|
|
39
42
|
const FULL = '\u2588';
|
|
@@ -47,6 +50,8 @@ export async function getUsageInfo(agentId, options) {
|
|
|
47
50
|
return getCodexUsageInfo(options);
|
|
48
51
|
case 'kimi':
|
|
49
52
|
return getKimiUsageInfo(options);
|
|
53
|
+
case 'droid':
|
|
54
|
+
return getDroidUsageInfo(options);
|
|
50
55
|
default:
|
|
51
56
|
return { snapshot: null, error: null };
|
|
52
57
|
}
|
|
@@ -117,13 +122,14 @@ const inFlightRefreshes = new Map();
|
|
|
117
122
|
*/
|
|
118
123
|
export async function getUsageInfoForIdentity(input) {
|
|
119
124
|
const usageKey = getUsageLookupKey(input.info);
|
|
120
|
-
// Agents whose usage comes from a live network call (Claude, Kimi) go
|
|
121
|
-
// the stale-while-revalidate cache below so `agents run`/`agents view`
|
|
122
|
-
// the network on the hot path. Everything else (Codex reads local
|
|
123
|
-
// logs) takes the legacy blocking path. The on-disk cache is shared and
|
|
124
|
-
// by usageKey, which is namespaced per agent (`claude:org=…`,
|
|
125
|
-
// so one cache file holds every account without
|
|
126
|
-
|
|
125
|
+
// Agents whose usage comes from a live network call (Claude, Kimi, Droid) go
|
|
126
|
+
// through the stale-while-revalidate cache below so `agents run`/`agents view`
|
|
127
|
+
// stay off the network on the hot path. Everything else (Codex reads local
|
|
128
|
+
// session logs) takes the legacy blocking path. The on-disk cache is shared and
|
|
129
|
+
// keyed by usageKey, which is namespaced per agent (`claude:org=…`,
|
|
130
|
+
// `kimi:user=…`, `droid:org=…`), so one cache file holds every account without
|
|
131
|
+
// collision.
|
|
132
|
+
const usesNetworkUsage = input.agentId === 'claude' || input.agentId === 'kimi' || input.agentId === 'droid';
|
|
127
133
|
if (!usesNetworkUsage || !usageKey) {
|
|
128
134
|
return getUsageInfo(input.agentId, {
|
|
129
135
|
home: input.home,
|
|
@@ -201,6 +207,12 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
|
201
207
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
202
208
|
}
|
|
203
209
|
if (snapshot) {
|
|
210
|
+
// Compact rows show every BLOCKING window — the same set
|
|
211
|
+
// deriveUsageStatusFromSnapshot uses for the rate-limited badge — so an
|
|
212
|
+
// account throttled by its month window (Droid meters on 5h/week/month)
|
|
213
|
+
// shows the bar that explains why. Claude's Sonnet week is a per-model
|
|
214
|
+
// sub-limit, not a blocking window; it renders only in the full
|
|
215
|
+
// per-version usage section.
|
|
204
216
|
const windows = snapshot.windows
|
|
205
217
|
.filter((window) => window.key !== 'sonnet_week')
|
|
206
218
|
.map((window) => `${chalk.gray(`${window.shortLabel}:`)} ${renderCompactUsageBar(window.usedPercent)}`);
|
|
@@ -513,6 +525,95 @@ export function formatKimiPlan(data) {
|
|
|
513
525
|
return null;
|
|
514
526
|
return tail.charAt(0).toUpperCase() + tail.slice(1).toLowerCase();
|
|
515
527
|
}
|
|
528
|
+
/**
|
|
529
|
+
* Fetch Droid usage via Factory's billing limits API — the same endpoint the
|
|
530
|
+
* droid CLI polls for its token-limit banner. The WorkOS access token comes
|
|
531
|
+
* from the locally decrypted ~/.factory/auth.v2.file (the same credential
|
|
532
|
+
* account identity in agents.ts reads).
|
|
533
|
+
*
|
|
534
|
+
* Deliberately NO token refresh, for a sharper reason than Kimi's: WorkOS
|
|
535
|
+
* refresh tokens are single-use and rotate on every exchange, so refreshing
|
|
536
|
+
* here would race a concurrently running droid session and can permanently
|
|
537
|
+
* invalidate the user's login chain. Droid refreshes its own credential when
|
|
538
|
+
* it runs; if the stored token is expired we skip the live fetch and let the
|
|
539
|
+
* SWR cache serve the last-seen snapshot.
|
|
540
|
+
*/
|
|
541
|
+
async function getDroidUsageInfo(options) {
|
|
542
|
+
try {
|
|
543
|
+
const cred = decryptDroidAuthPayload(options?.home || os.homedir());
|
|
544
|
+
const accessToken = cred?.access_token;
|
|
545
|
+
if (typeof accessToken !== 'string' || !accessToken) {
|
|
546
|
+
return { snapshot: null, error: null };
|
|
547
|
+
}
|
|
548
|
+
const exp = decodeJwtPayload(accessToken)?.exp;
|
|
549
|
+
if (typeof exp === 'number' && Date.now() / 1000 >= exp) {
|
|
550
|
+
return { snapshot: null, error: null };
|
|
551
|
+
}
|
|
552
|
+
const response = await fetch(DROID_USAGE_URL, {
|
|
553
|
+
method: 'GET',
|
|
554
|
+
headers: {
|
|
555
|
+
Authorization: `Bearer ${accessToken}`,
|
|
556
|
+
Accept: 'application/json',
|
|
557
|
+
},
|
|
558
|
+
signal: AbortSignal.timeout(5000),
|
|
559
|
+
});
|
|
560
|
+
// 401 => revoked/expired token; render nothing rather than a misleading
|
|
561
|
+
// empty bar.
|
|
562
|
+
if (!response.ok) {
|
|
563
|
+
return { snapshot: null, error: null };
|
|
564
|
+
}
|
|
565
|
+
const data = await response.json();
|
|
566
|
+
const windows = normalizeDroidWindows(data);
|
|
567
|
+
if (windows.length === 0) {
|
|
568
|
+
return { snapshot: null, error: null };
|
|
569
|
+
}
|
|
570
|
+
return {
|
|
571
|
+
snapshot: {
|
|
572
|
+
source: 'live',
|
|
573
|
+
sourceLabel: 'live account data',
|
|
574
|
+
capturedAt: new Date(),
|
|
575
|
+
windows,
|
|
576
|
+
},
|
|
577
|
+
error: null,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
catch {
|
|
581
|
+
return { snapshot: null, error: null };
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* Normalize the Factory billing-limits payload into the common UsageWindow
|
|
586
|
+
* shape. Orgs on the legacy (non token-rate-limit) billing model have no
|
|
587
|
+
* meaningful windows, so they render nothing — mirrors droid's own gate on
|
|
588
|
+
* `usesTokenRateLimitsBilling` before it reads `limits.standard`.
|
|
589
|
+
*/
|
|
590
|
+
export function normalizeDroidWindows(data) {
|
|
591
|
+
if (data.usesTokenRateLimitsBilling !== true)
|
|
592
|
+
return [];
|
|
593
|
+
const standard = data.limits?.standard;
|
|
594
|
+
if (!standard)
|
|
595
|
+
return [];
|
|
596
|
+
const windows = [
|
|
597
|
+
normalizeDroidWindow(standard.fiveHour, 'session', 'Current session', 'S'),
|
|
598
|
+
normalizeDroidWindow(standard.weekly, 'week', 'Current week', 'W'),
|
|
599
|
+
normalizeDroidWindow(standard.monthly, 'month', 'Current month', 'M'),
|
|
600
|
+
];
|
|
601
|
+
return windows.filter((window) => window !== null);
|
|
602
|
+
}
|
|
603
|
+
/** Normalize a single Droid billing-limits window. */
|
|
604
|
+
function normalizeDroidWindow(window, key, label, shortLabel) {
|
|
605
|
+
const usedPercent = normalizePercent(window?.usedPercent);
|
|
606
|
+
if (usedPercent === null)
|
|
607
|
+
return null;
|
|
608
|
+
return {
|
|
609
|
+
key,
|
|
610
|
+
label,
|
|
611
|
+
shortLabel,
|
|
612
|
+
usedPercent,
|
|
613
|
+
resetsAt: parseDateValue(window?.windowEnd),
|
|
614
|
+
windowMinutes: inferWindowMinutes(key),
|
|
615
|
+
};
|
|
616
|
+
}
|
|
516
617
|
/** Collect Codex JSONL session files sorted newest-first. */
|
|
517
618
|
function collectCodexSessionFiles(home) {
|
|
518
619
|
const base = home || os.homedir();
|
|
@@ -609,19 +710,16 @@ function normalizeClaudeWindow(window, key, label, shortLabel) {
|
|
|
609
710
|
windowMinutes: inferWindowMinutes(key),
|
|
610
711
|
};
|
|
611
712
|
}
|
|
612
|
-
/**
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
713
|
+
/**
|
|
714
|
+
* Parse a wrapped Claude OAuth payload — the `{ claudeAiOauth, organizationUuid }`
|
|
715
|
+
* shape written by BOTH the macOS Keychain item and the Linux `.credentials.json`
|
|
716
|
+
* file — into our credential struct. Returns null when there is no usable access
|
|
717
|
+
* token. Never throws (malformed JSON => null).
|
|
718
|
+
*/
|
|
719
|
+
function parseClaudeOauthPayload(raw) {
|
|
618
720
|
try {
|
|
619
|
-
const
|
|
620
|
-
|
|
621
|
-
const payload = JSON.parse(stdout.trim());
|
|
622
|
-
if (typeof payload?.claudeAiOauth?.accessToken !== 'string')
|
|
623
|
-
return null;
|
|
624
|
-
if (!payload.claudeAiOauth) {
|
|
721
|
+
const payload = JSON.parse(raw.trim());
|
|
722
|
+
if (!payload?.claudeAiOauth || typeof payload.claudeAiOauth.accessToken !== 'string') {
|
|
625
723
|
return null;
|
|
626
724
|
}
|
|
627
725
|
return {
|
|
@@ -633,6 +731,48 @@ export async function loadClaudeOauth(home) {
|
|
|
633
731
|
return null;
|
|
634
732
|
}
|
|
635
733
|
}
|
|
734
|
+
/**
|
|
735
|
+
* Load a version home's Claude OAuth credential from the two stores Claude Code
|
|
736
|
+
* uses, tried in order:
|
|
737
|
+
*
|
|
738
|
+
* 1. The OS keychain (`getKeychainToken`). Canonical on macOS — Claude Code
|
|
739
|
+
* writes the token to the login keychain and we read it via `/usr/bin/security`.
|
|
740
|
+
* 2. `<home>/.claude/.credentials.json`. On a headless Linux box (the
|
|
741
|
+
* `agents view --host <linux>` case) there is no reachable Secret Service, so
|
|
742
|
+
* the Claude CLI stores its OAuth token in this plaintext file instead. The
|
|
743
|
+
* keychain read above finds nothing on that platform, so we fall back to the
|
|
744
|
+
* file. Same wrapped `{ claudeAiOauth }` shape, so one parser handles both.
|
|
745
|
+
* Mirrors `readClaudeCredentialsBlob` (cloud/rush.ts), the proven pattern.
|
|
746
|
+
*
|
|
747
|
+
* Without step 2 the live usage fetch got no token on Linux, so `agents view`
|
|
748
|
+
* (run remotely over SSH by `--host`) rendered no usage bars even though the
|
|
749
|
+
* account + plan — read from the plaintext `.claude.json` — showed fine.
|
|
750
|
+
*/
|
|
751
|
+
export async function loadClaudeOauth(home) {
|
|
752
|
+
// The OS keychain/keyring step is macOS/Linux-only. Windows skips straight
|
|
753
|
+
// to the .credentials.json fallback — the Claude CLI has no keychain
|
|
754
|
+
// integration there and stores its OAuth token in that file too.
|
|
755
|
+
if (process.platform === 'darwin' || process.platform === 'linux') {
|
|
756
|
+
try {
|
|
757
|
+
const fromKeychain = parseClaudeOauthPayload(getKeychainToken(getClaudeKeychainService(home)));
|
|
758
|
+
if (fromKeychain)
|
|
759
|
+
return fromKeychain;
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// No keychain item, or no reachable keyring (headless Linux) — fall through.
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
const credsPath = path.join(home ?? os.homedir(), '.claude', '.credentials.json');
|
|
766
|
+
try {
|
|
767
|
+
if (fs.existsSync(credsPath)) {
|
|
768
|
+
return parseClaudeOauthPayload(fs.readFileSync(credsPath, 'utf-8'));
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
catch {
|
|
772
|
+
// Unreadable file — treat as not signed in.
|
|
773
|
+
}
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
636
776
|
/**
|
|
637
777
|
* Save Claude OAuth credentials to the system keychain/keyring.
|
|
638
778
|
* Reads the existing payload, merges the new OAuth fields, and writes back.
|
|
@@ -895,6 +1035,8 @@ function inferWindowMinutes(key) {
|
|
|
895
1035
|
case 'week':
|
|
896
1036
|
case 'sonnet_week':
|
|
897
1037
|
return 10080;
|
|
1038
|
+
case 'month':
|
|
1039
|
+
return 43200;
|
|
898
1040
|
}
|
|
899
1041
|
}
|
|
900
1042
|
/** Parse a date value from a number (epoch seconds or ms) or ISO string. */
|
package/dist/lib/versions.js
CHANGED
|
@@ -30,7 +30,7 @@ import { listResources } from './resources.js';
|
|
|
30
30
|
import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
|
|
31
31
|
import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath, isSelfUpdatingAgent } from './agents.js';
|
|
32
32
|
import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
|
|
33
|
-
import { parseMcpServerConfig } from './mcp.js';
|
|
33
|
+
import { parseMcpServerConfig, isProjectMcpTrusted } from './mcp.js';
|
|
34
34
|
import { createVersionedAlias, removeVersionedAlias, getConfigSymlinkVersion, ensureClaudeInsideSymlink } from './shims.js';
|
|
35
35
|
import { importInstallScriptBinary } from './import.js';
|
|
36
36
|
import { IS_WINDOWS, composeWin32CommandLine } from './platform/index.js';
|
|
@@ -2511,20 +2511,23 @@ export function syncResourcesToVersion(agent, version, selection, options = {})
|
|
|
2511
2511
|
// For Claude/Codex: uses CLI commands (claude mcp add, codex mcp add)
|
|
2512
2512
|
// For others: edits config files directly
|
|
2513
2513
|
//
|
|
2514
|
-
// Mirror the hooks defense:
|
|
2515
|
-
//
|
|
2516
|
-
//
|
|
2517
|
-
//
|
|
2518
|
-
//
|
|
2519
|
-
//
|
|
2520
|
-
//
|
|
2521
|
-
//
|
|
2522
|
-
const
|
|
2514
|
+
// Mirror the hooks defense: an MCP server is an executable invoked under the
|
|
2515
|
+
// agent's authority, so a cloned public repo's .agents/mcp/foo.yaml could
|
|
2516
|
+
// install an arbitrary command (RUSH-1776). Project-scoped MCPs are UNTRUSTED
|
|
2517
|
+
// by default — drop them before handing the list to installMcpServers unless
|
|
2518
|
+
// the user has explicitly trusted this project (`agents mcp trust`). The
|
|
2519
|
+
// register/spawn choke in lib/mcp.ts (getMcpServersByName) enforces the same
|
|
2520
|
+
// trust gate and drops untrusted project entries before dedup, so a project
|
|
2521
|
+
// entry can no longer shadow a same-named user entry either.
|
|
2522
|
+
const projectMcpTrusted = projectAgentsDir ? isProjectMcpTrusted(projectAgentsDir) : false;
|
|
2523
|
+
const untrustedProjectMcpNames = new Set(projectMcpTrusted
|
|
2524
|
+
? []
|
|
2525
|
+
: getScopedMcpResources(cwd).filter(r => r.scope === 'project').map(r => r.name));
|
|
2523
2526
|
const mcpWriter = getWriter('mcp', agent);
|
|
2524
2527
|
const mcpToSyncAll = selection
|
|
2525
2528
|
? resolveSelection(selection.mcp, available.mcp)
|
|
2526
2529
|
: (mcpWriter ? available.mcp : []);
|
|
2527
|
-
const mcpToSync = mcpToSyncAll.filter(n => !
|
|
2530
|
+
const mcpToSync = mcpToSyncAll.filter(n => !untrustedProjectMcpNames.has(n));
|
|
2528
2531
|
if (mcpToSync.length > 0 && mcpWriter) {
|
|
2529
2532
|
const r = mcpWriter.write({ version, versionHome, selection: mcpToSync, cwd });
|
|
2530
2533
|
result.mcp = r.synced;
|