@phnx-labs/agents-cli 1.22.4 → 1.22.6
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 +32 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/events.js +8 -0
- package/dist/commands/exec.js +18 -6
- package/dist/commands/inspect.js +18 -5
- package/dist/commands/models.d.ts +1 -1
- package/dist/commands/models.js +68 -9
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/view.d.ts +12 -0
- package/dist/commands/view.js +2 -0
- package/dist/commands/webhook.js +8 -4
- package/dist/index.js +2 -1
- package/dist/lib/browser/chrome.d.ts +12 -0
- package/dist/lib/browser/chrome.js +27 -11
- package/dist/lib/cloud/antigravity.js +8 -2
- package/dist/lib/crabbox/cli.js +72 -30
- package/dist/lib/event-stream.d.ts +3 -0
- package/dist/lib/event-stream.js +5 -0
- package/dist/lib/events.d.ts +2 -0
- package/dist/lib/events.js +6 -1
- package/dist/lib/feed.d.ts +1 -1
- package/dist/lib/feed.js +19 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/model-tier-overrides.d.ts +43 -0
- package/dist/lib/model-tier-overrides.js +97 -0
- package/dist/lib/model-tiers.d.ts +13 -7
- package/dist/lib/model-tiers.js +104 -35
- package/dist/lib/models.d.ts +16 -0
- package/dist/lib/models.js +17 -3
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/index.d.ts +29 -0
- package/dist/lib/secrets/index.js +32 -0
- package/dist/lib/secrets/mcp.js +8 -4
- package/dist/lib/session/sync/config.d.ts +12 -11
- package/dist/lib/session/sync/config.js +40 -38
- package/dist/lib/share/config.js +4 -0
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/versions.js +35 -0
- package/package.json +1 -1
package/dist/lib/model-tiers.js
CHANGED
|
@@ -1,11 +1,27 @@
|
|
|
1
1
|
import { getModelCatalog } from './models.js';
|
|
2
2
|
import { getModelPricing } from './pricing/index.js';
|
|
3
|
+
import { resolveTierOverride } from './model-tier-overrides.js';
|
|
3
4
|
/** The four cross-harness cost tiers, cheapest -> most capable. */
|
|
4
5
|
export const MODEL_TIERS = ['cheap', 'default', 'best', 'ultra'];
|
|
5
6
|
/** True if `s` is one of the four tier tokens (not a concrete model id). */
|
|
6
7
|
export function isTierToken(s) {
|
|
7
8
|
return !!s && MODEL_TIERS.includes(s);
|
|
8
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Curated tier ladders for harnesses the auto-ranker can't order from names/price
|
|
12
|
+
* (subscription harnesses with no price signal). Each rung is `[tier, matcher]` in
|
|
13
|
+
* cheap -> best order; the newest catalog id matching each rung fills that tier,
|
|
14
|
+
* missing tiers clamp. Extend this table rather than adding per-harness branches.
|
|
15
|
+
*/
|
|
16
|
+
const CURATED_LADDERS = {
|
|
17
|
+
// Kimi: K2.7 Highspeed < K2.7 Coding < K3 (the 1M-context default; k3-256k folds
|
|
18
|
+
// into K3). No ultra. The name heuristic can't tell K3 > K2.7, so curate it.
|
|
19
|
+
kimi: [
|
|
20
|
+
{ tier: 'cheap', match: /highspeed/i },
|
|
21
|
+
{ tier: 'default', match: /for-coding(?!.*highspeed)/i },
|
|
22
|
+
{ tier: 'best', match: /(^|[-/])k3\b/i }, // K3 family incl. k3-256k; the plain id represents it
|
|
23
|
+
],
|
|
24
|
+
};
|
|
9
25
|
// --- single-model harnesses: the tier is reasoning effort, not a model ---------
|
|
10
26
|
const TIER_EFFORT = {
|
|
11
27
|
cheap: 'low',
|
|
@@ -169,29 +185,100 @@ function rankCatalog(agent, models) {
|
|
|
169
185
|
function rungIndexFor(tierIndex, n) {
|
|
170
186
|
return n >= 4 ? Math.round((tierIndex / 3) * (n - 1)) : Math.min(tierIndex, n - 1);
|
|
171
187
|
}
|
|
188
|
+
/** Bucket an ordered (cheap -> dear) rung list onto the four tiers, clamping when < 4. */
|
|
189
|
+
function bucketRungs(rungs) {
|
|
190
|
+
const n = rungs.length;
|
|
191
|
+
const map = {};
|
|
192
|
+
if (n === 0) {
|
|
193
|
+
// Fail-safe: no catalog -> every tier null, caller drops the --model flag.
|
|
194
|
+
for (const t of MODEL_TIERS)
|
|
195
|
+
map[t] = { tier: t, model: null };
|
|
196
|
+
return map;
|
|
197
|
+
}
|
|
198
|
+
// A tier that shares the rung of the tier below has no distinct rung -> mark clamped.
|
|
199
|
+
for (let i = 0; i < MODEL_TIERS.length; i++) {
|
|
200
|
+
const t = MODEL_TIERS[i];
|
|
201
|
+
const idx = rungIndexFor(i, n);
|
|
202
|
+
const shared = i > 0 && rungIndexFor(i - 1, n) === idx;
|
|
203
|
+
map[t] = shared
|
|
204
|
+
? { tier: t, model: rungs[idx].id, clampedFrom: MODEL_TIERS[i - 1], note: `no distinct ${t} rung; using ${MODEL_TIERS[i - 1]}`, source: 'auto' }
|
|
205
|
+
: { tier: t, model: rungs[idx].id, source: 'auto' };
|
|
206
|
+
}
|
|
207
|
+
return map;
|
|
208
|
+
}
|
|
209
|
+
/** Build a tier map from a curated ladder against a catalog (newest match per rung). */
|
|
210
|
+
function tierizeFromLadder(ladder, models) {
|
|
211
|
+
const usable = models.filter((m) => !PSEUDO.test(m.id));
|
|
212
|
+
const rungs = [];
|
|
213
|
+
for (const rung of ladder) {
|
|
214
|
+
const matches = usable.filter((m) => rung.match.test(m.id));
|
|
215
|
+
if (matches.length === 0)
|
|
216
|
+
continue;
|
|
217
|
+
// Prefer a plain id over a context-size variant (k3 over k3-256k) -- the
|
|
218
|
+
// variant folds into the rung but the plain model represents it -- then newest.
|
|
219
|
+
const plain = matches.filter((m) => !/-\d+[km]\b/i.test(m.id));
|
|
220
|
+
const pool = plain.length ? plain : matches;
|
|
221
|
+
rungs.push({ id: pool.reduce((a, b) => (newer(b.id, a.id) > 0 ? b : a)).id });
|
|
222
|
+
}
|
|
223
|
+
const map = bucketRungs(rungs);
|
|
224
|
+
for (const t of MODEL_TIERS)
|
|
225
|
+
if (map[t].model)
|
|
226
|
+
map[t].source = 'curated';
|
|
227
|
+
return map;
|
|
228
|
+
}
|
|
172
229
|
/**
|
|
173
|
-
* Resolve all four tiers for an (agent, version)
|
|
174
|
-
*
|
|
230
|
+
* Resolve all four tiers for an (agent, version) -- what `agents models` prints and
|
|
231
|
+
* `resolveTier` indexes. Precedence: user override -> curated ladder / auto-ranking.
|
|
175
232
|
*/
|
|
176
233
|
export function resolveTierMap(agent, version) {
|
|
177
|
-
|
|
234
|
+
let base;
|
|
235
|
+
let catalogIds;
|
|
178
236
|
if (agent === 'droid') {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
237
|
+
// Droid: curated credit-multiplier map (no live catalog to validate against).
|
|
238
|
+
base = {
|
|
239
|
+
cheap: { tier: 'cheap', model: DROID_TIERS.cheap, note: 'Droid Core 0.55x', source: 'curated' },
|
|
240
|
+
default: { tier: 'default', model: DROID_TIERS.default, note: 'Droid Core 0.6x', source: 'curated' },
|
|
241
|
+
best: { tier: 'best', model: DROID_TIERS.best, note: '2x', source: 'curated' },
|
|
242
|
+
ultra: { tier: 'ultra', model: DROID_TIERS.ultra, clampedFrom: 'best', note: 'capped at 2x (4x excluded)', source: 'curated' },
|
|
184
243
|
};
|
|
244
|
+
catalogIds = null;
|
|
185
245
|
}
|
|
186
|
-
|
|
187
|
-
|
|
246
|
+
else {
|
|
247
|
+
const catalog = getModelCatalog(agent, version);
|
|
248
|
+
const models = catalog?.models ?? [];
|
|
249
|
+
const ladder = CURATED_LADDERS[agent];
|
|
250
|
+
base = ladder ? tierizeFromLadder(ladder, models) : tierizeModels(agent, models);
|
|
251
|
+
catalogIds = catalog ? new Set(models.map((m) => m.id)) : null;
|
|
252
|
+
}
|
|
253
|
+
const overrides = resolveTierOverride(agent, version);
|
|
254
|
+
return applyTierOverrides(overrides, `${agent}@${version}`, catalogIds, base);
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Apply user overrides on top of the auto/curated map. Pure (takes the resolved
|
|
258
|
+
* override map, no config lookup) so it is directly testable. An overridden id is
|
|
259
|
+
* used only when the version actually ships it (or when there is no catalog to
|
|
260
|
+
* check, e.g. Droid); otherwise the tier keeps its base value with a note.
|
|
261
|
+
*/
|
|
262
|
+
export function applyTierOverrides(overrides, label, catalogIds, base) {
|
|
263
|
+
if (Object.keys(overrides).length === 0)
|
|
264
|
+
return base;
|
|
265
|
+
const out = { ...base };
|
|
266
|
+
for (const t of MODEL_TIERS) {
|
|
267
|
+
const id = overrides[t];
|
|
268
|
+
if (!id)
|
|
269
|
+
continue;
|
|
270
|
+
if (!catalogIds || catalogIds.has(id)) {
|
|
271
|
+
out[t] = { tier: t, model: id, source: 'override' };
|
|
272
|
+
}
|
|
273
|
+
else {
|
|
274
|
+
out[t] = { ...base[t], note: `override "${id}" not shipped by ${label}; kept the ${base[t].source ?? 'auto'} pick`, source: base[t].source ?? 'auto' };
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
188
278
|
}
|
|
189
279
|
/**
|
|
190
|
-
* Map a harness's catalog models onto the four tiers. Pure (no catalog lookup)
|
|
191
|
-
*
|
|
192
|
-
* variants, buckets onto cheap/default/best/ultra, and clamps absent tiers down
|
|
193
|
-
* to the nearest lower one. A single-model harness maps the tiers to reasoning
|
|
194
|
-
* effort instead of models.
|
|
280
|
+
* Map a harness's catalog models onto the four tiers. Pure (no catalog lookup) so
|
|
281
|
+
* it is directly testable. A single-model harness maps the tiers to reasoning effort.
|
|
195
282
|
*/
|
|
196
283
|
export function tierizeModels(agent, models) {
|
|
197
284
|
const rungs = rankCatalog(agent, models);
|
|
@@ -200,28 +287,10 @@ export function tierizeModels(agent, models) {
|
|
|
200
287
|
const only = rungs[0].id;
|
|
201
288
|
const map = {};
|
|
202
289
|
for (const t of MODEL_TIERS)
|
|
203
|
-
map[t] = { tier: t, model: only, effort: TIER_EFFORT[t], note: 'single model — tier maps to reasoning effort' };
|
|
204
|
-
return map;
|
|
205
|
-
}
|
|
206
|
-
const n = rungs.length;
|
|
207
|
-
const map = {};
|
|
208
|
-
if (n === 0) {
|
|
209
|
-
// Fail-safe: no catalog -> every tier null, caller drops the --model flag.
|
|
210
|
-
for (const t of MODEL_TIERS)
|
|
211
|
-
map[t] = { tier: t, model: null };
|
|
290
|
+
map[t] = { tier: t, model: only, effort: TIER_EFFORT[t], note: 'single model — tier maps to reasoning effort', source: 'auto' };
|
|
212
291
|
return map;
|
|
213
292
|
}
|
|
214
|
-
|
|
215
|
-
// has no distinct rung of its own, so mark it clamped for an honest display.
|
|
216
|
-
for (let i = 0; i < MODEL_TIERS.length; i++) {
|
|
217
|
-
const t = MODEL_TIERS[i];
|
|
218
|
-
const idx = rungIndexFor(i, n);
|
|
219
|
-
const shared = i > 0 && rungIndexFor(i - 1, n) === idx;
|
|
220
|
-
map[t] = shared
|
|
221
|
-
? { tier: t, model: rungs[idx].id, clampedFrom: MODEL_TIERS[i - 1], note: `no distinct ${t} rung; using ${MODEL_TIERS[i - 1]}` }
|
|
222
|
-
: { tier: t, model: rungs[idx].id };
|
|
223
|
-
}
|
|
224
|
-
return map;
|
|
293
|
+
return bucketRungs(rungs);
|
|
225
294
|
}
|
|
226
295
|
/** Resolve one tier for an (agent, version). Null model => caller drops the flag. */
|
|
227
296
|
export function resolveTier(agent, version, tier) {
|
package/dist/lib/models.d.ts
CHANGED
|
@@ -60,6 +60,22 @@ export interface ModelSource {
|
|
|
60
60
|
* Returns null if nothing usable is found.
|
|
61
61
|
*/
|
|
62
62
|
export declare function locateModelSource(agent: AgentId, version: string): ModelSource | null;
|
|
63
|
+
/**
|
|
64
|
+
* Extract Claude's model catalog from its bundle/binary.
|
|
65
|
+
*
|
|
66
|
+
* Bundle/binary contains:
|
|
67
|
+
* - alias map: {opus:"claude-opus-4-7",sonnet:"claude-sonnet-4-6",haiku:"..."}
|
|
68
|
+
* - per-cloud maps: {firstParty:"claude-opus-4-5-...",bedrock:"...",vertex:"...",...}
|
|
69
|
+
* - constants: {OPUS_ID:"...",OPUS_NAME:"...",SONNET_ID:"...",...}
|
|
70
|
+
*/
|
|
71
|
+
/**
|
|
72
|
+
* Drop a bare `claude-<family>-<major>` (e.g. `claude-opus-4`) when a more specific
|
|
73
|
+
* sibling (`claude-opus-4-8`) is present. The bare form is only ever an internal
|
|
74
|
+
* `.includes("claude-opus-4")` prefix-check string in the binary, not a submittable
|
|
75
|
+
* id (issue #1892); a bare id with no sibling (e.g. `claude-sonnet-5`) is a real
|
|
76
|
+
* current model and is kept.
|
|
77
|
+
*/
|
|
78
|
+
export declare function dropBareLegacyIds(ids: string[]): string[];
|
|
63
79
|
/**
|
|
64
80
|
* Parse `grok models` stdout into a catalog. Exported for unit tests.
|
|
65
81
|
*
|
package/dist/lib/models.js
CHANGED
|
@@ -21,7 +21,7 @@ const CACHE_PATH = getModelsCachePath();
|
|
|
21
21
|
* Bump when the extractor logic changes shape in an incompatible way so cached
|
|
22
22
|
* catalogs from older agents-cli builds are re-extracted.
|
|
23
23
|
*/
|
|
24
|
-
const CACHE_SCHEMA_VERSION =
|
|
24
|
+
const CACHE_SCHEMA_VERSION = 4;
|
|
25
25
|
/**
|
|
26
26
|
* How long a cached 0-model extraction is trusted before we retry it. Bounds
|
|
27
27
|
* the self-healing window for a transient failure (mid-install, a broken
|
|
@@ -310,6 +310,19 @@ function extractStrings(filePath, minLen = 6) {
|
|
|
310
310
|
* - per-cloud maps: {firstParty:"claude-opus-4-5-...",bedrock:"...",vertex:"...",...}
|
|
311
311
|
* - constants: {OPUS_ID:"...",OPUS_NAME:"...",SONNET_ID:"...",...}
|
|
312
312
|
*/
|
|
313
|
+
/**
|
|
314
|
+
* Drop a bare `claude-<family>-<major>` (e.g. `claude-opus-4`) when a more specific
|
|
315
|
+
* sibling (`claude-opus-4-8`) is present. The bare form is only ever an internal
|
|
316
|
+
* `.includes("claude-opus-4")` prefix-check string in the binary, not a submittable
|
|
317
|
+
* id (issue #1892); a bare id with no sibling (e.g. `claude-sonnet-5`) is a real
|
|
318
|
+
* current model and is kept.
|
|
319
|
+
*/
|
|
320
|
+
export function dropBareLegacyIds(ids) {
|
|
321
|
+
return ids.filter((id) => {
|
|
322
|
+
const bareMajor = /^claude-[a-z]+-\d+$/.test(id);
|
|
323
|
+
return !(bareMajor && ids.some((o) => o !== id && o.startsWith(`${id}-`)));
|
|
324
|
+
});
|
|
325
|
+
}
|
|
313
326
|
function extractClaudeCatalog(text) {
|
|
314
327
|
const aliases = {};
|
|
315
328
|
const aliasMapMatch = text.match(/\{opus:"(claude-[^"]+)",sonnet:"(claude-[^"]+)",haiku:"(claude-[^"]+)"\}/);
|
|
@@ -380,8 +393,9 @@ function extractClaudeCatalog(text) {
|
|
|
380
393
|
let sm;
|
|
381
394
|
while ((sm = idRe.exec(text)) !== null)
|
|
382
395
|
scanned.add(sm[0]);
|
|
383
|
-
|
|
384
|
-
|
|
396
|
+
const filtered = dropBareLegacyIds([...scanned]);
|
|
397
|
+
if (filtered.length >= 2)
|
|
398
|
+
models = build(filtered);
|
|
385
399
|
}
|
|
386
400
|
return { models, aliases };
|
|
387
401
|
}
|
|
Binary file
|
|
Binary file
|
|
@@ -91,6 +91,21 @@ export declare function setKeychainBackendForTest(b: KeychainBackend | null): Ke
|
|
|
91
91
|
* fast-path must not engage. Always false in production (`backend` is null). */
|
|
92
92
|
export declare function isKeychainBackendOverridden(): boolean;
|
|
93
93
|
export declare const HMAC_KEY_ITEM = "agents-cli.hmackey";
|
|
94
|
+
interface HmacKeyRecord {
|
|
95
|
+
v: number;
|
|
96
|
+
/** 64 hex chars — the raw HMAC-SHA256 key. */
|
|
97
|
+
k: string;
|
|
98
|
+
/** True once the one-time re-key has moved every cleartext-named item. */
|
|
99
|
+
migrated: boolean;
|
|
100
|
+
/** Old cleartext services whose hashed copies are verified but whose
|
|
101
|
+
* originals are not yet deleted (crash-resume list; deletes are silent). */
|
|
102
|
+
pendingDeletes?: string[];
|
|
103
|
+
/** True once this record has been re-stored no-ACL to heal a hmackey item that
|
|
104
|
+
* an OLD helper (pre the metadata/hmackey no-ACL migration fix) re-stamped with
|
|
105
|
+
* a biometry ACL. Set on the first read that heals it, so the heal runs exactly
|
|
106
|
+
* once per machine and never churns the keychain afterward. */
|
|
107
|
+
healedNoAcl?: boolean;
|
|
108
|
+
}
|
|
94
109
|
/** Force hashed service names on with a fixed key (test only). Pass null to
|
|
95
110
|
* restore lazy production resolution. Composes with setKeychainBackendForTest
|
|
96
111
|
* so unit tests exercise the exact transform production uses. */
|
|
@@ -106,6 +121,20 @@ export declare function withRawKeychainServiceNames<T>(fn: () => T): T;
|
|
|
106
121
|
* the re-key migration and tests; runtime callers go through the primitives,
|
|
107
122
|
* which apply this transparently. */
|
|
108
123
|
export declare function hashedServiceName(item: string, key: Buffer): string;
|
|
124
|
+
/**
|
|
125
|
+
* Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
|
|
126
|
+
* migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
|
|
127
|
+
* keychain lookup pop the generic "Agents CLI needs to authenticate" sheet,
|
|
128
|
+
* because the HMAC key is read before every hashed name resolves. The migration
|
|
129
|
+
* fix stopped the re-stamping but never un-stamped an already-damaged item, and
|
|
130
|
+
* nothing else re-stores it once hashing is already active — so it prompts forever.
|
|
131
|
+
*
|
|
132
|
+
* This re-stores the record no-ACL exactly once per machine (guarded by
|
|
133
|
+
* `healedNoAcl`), turning every future read silent. The read that produced `rec`
|
|
134
|
+
* has already happened (and already prompted if it was ACL'd); this only writes.
|
|
135
|
+
* Returns true if it healed. Exported for tests. No-op when already healed.
|
|
136
|
+
*/
|
|
137
|
+
export declare function healHmacKeyNoAclOnce(rec: HmacKeyRecord): boolean;
|
|
109
138
|
/**
|
|
110
139
|
* The storage-layer service name for `item`: hashed when hashing is active,
|
|
111
140
|
* the item itself otherwise. For callers that mix helper-enumerated
|
|
@@ -257,6 +257,25 @@ function writeHmacKeyRecord(rec) {
|
|
|
257
257
|
setKeychainToken(HMAC_KEY_ITEM, JSON.stringify(rec), { noAcl: true });
|
|
258
258
|
hashStateCache = null;
|
|
259
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Heal a `hmackey` item that an OLD helper (pre the metadata/hmackey no-ACL
|
|
262
|
+
* migration fix) re-stamped with a biometry ACL. Such an item makes EVERY hashed
|
|
263
|
+
* keychain lookup pop the generic "Agents CLI needs to authenticate" sheet,
|
|
264
|
+
* because the HMAC key is read before every hashed name resolves. The migration
|
|
265
|
+
* fix stopped the re-stamping but never un-stamped an already-damaged item, and
|
|
266
|
+
* nothing else re-stores it once hashing is already active — so it prompts forever.
|
|
267
|
+
*
|
|
268
|
+
* This re-stores the record no-ACL exactly once per machine (guarded by
|
|
269
|
+
* `healedNoAcl`), turning every future read silent. The read that produced `rec`
|
|
270
|
+
* has already happened (and already prompted if it was ACL'd); this only writes.
|
|
271
|
+
* Returns true if it healed. Exported for tests. No-op when already healed.
|
|
272
|
+
*/
|
|
273
|
+
export function healHmacKeyNoAclOnce(rec) {
|
|
274
|
+
if (rec.healedNoAcl)
|
|
275
|
+
return false;
|
|
276
|
+
writeHmacKeyRecord({ ...rec, healedNoAcl: true });
|
|
277
|
+
return true;
|
|
278
|
+
}
|
|
260
279
|
function resolveHashState() {
|
|
261
280
|
if (forcedTestKey)
|
|
262
281
|
return { active: true, key: forcedTestKey, record: null };
|
|
@@ -404,6 +423,19 @@ function maybeAutoRekey() {
|
|
|
404
423
|
return;
|
|
405
424
|
const st = resolveHashState();
|
|
406
425
|
if (st.active) {
|
|
426
|
+
// Heal an already-active machine whose hmackey was re-stamped ACL'd by an old
|
|
427
|
+
// helper (its read popped the generic Touch ID sheet on every hashed lookup).
|
|
428
|
+
// Runs once per machine; mutate the local so a later finishPendingDeletes write
|
|
429
|
+
// preserves the healed flag.
|
|
430
|
+
if (st.record && !st.record.healedNoAcl) {
|
|
431
|
+
try {
|
|
432
|
+
healHmacKeyNoAclOnce(st.record);
|
|
433
|
+
st.record.healedNoAcl = true;
|
|
434
|
+
}
|
|
435
|
+
catch {
|
|
436
|
+
/* next process retries */
|
|
437
|
+
}
|
|
438
|
+
}
|
|
407
439
|
if (st.record?.pendingDeletes?.length) {
|
|
408
440
|
try {
|
|
409
441
|
finishPendingDeletes(st.record);
|
package/dist/lib/secrets/mcp.js
CHANGED
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
* unit-testable in-process.
|
|
25
25
|
*/
|
|
26
26
|
import * as readline from 'readline';
|
|
27
|
-
import { listBundles, readAndResolveBundleEnv,
|
|
27
|
+
import { listBundles, readAndResolveBundleEnv, readBundle, validateBundleName, } from './bundles.js';
|
|
28
28
|
/** MCP protocol revision this server negotiates. */
|
|
29
29
|
export const MCP_PROTOCOL_VERSION = '2024-11-05';
|
|
30
30
|
/** The single tool this server exposes. */
|
|
@@ -68,9 +68,13 @@ export function resolveSecret(bundle, key) {
|
|
|
68
68
|
throw new Error(`Key '${key}' not found in bundle '${bundle}'.` +
|
|
69
69
|
(available.length ? ` Available keys: ${available.join(', ')}.` : ' Bundle has no keys.'));
|
|
70
70
|
}
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
71
|
+
// An MCP `get_secret` tool call is a program asking for a value, never a human
|
|
72
|
+
// at a Touch ID sheet — so the read is always `agentOnly` (SEC-13: never pop
|
|
73
|
+
// biometry on its own). A `never`/no-ACL or broker-held bundle resolves
|
|
74
|
+
// silently; a locked bundle THROWS the actionable "unlock <name>" message,
|
|
75
|
+
// which propagates as the MCP tool error (the caller surfaces it) rather than
|
|
76
|
+
// popping an unanswerable prompt.
|
|
77
|
+
const { env } = readAndResolveBundleEnv(bundle, { caller: 'secrets-mcp', keys: [key], keyMode: 'storage', agentOnly: true });
|
|
74
78
|
const value = env[key];
|
|
75
79
|
if (value === undefined) {
|
|
76
80
|
throw new Error(`Key '${key}' in bundle '${bundle}' could not be resolved.`);
|
|
@@ -29,24 +29,25 @@ export interface R2Config {
|
|
|
29
29
|
*/
|
|
30
30
|
syncEncKey?: string;
|
|
31
31
|
}
|
|
32
|
-
/** Window after a prompt-bearing resolution failure during which we skip
|
|
33
|
-
* re-attempting (and thus re-prompting). SIGHUP / restart bypasses it. */
|
|
34
|
-
export declare const RESOLVE_RETRY_COOLDOWN_MS: number;
|
|
35
32
|
/** Drop the cached resolution so the next call reads the bundle fresh. Called on
|
|
36
33
|
* daemon SIGHUP (to pick up rotated credentials) and between tests. */
|
|
37
34
|
export declare function clearR2ConfigCache(): void;
|
|
38
35
|
/**
|
|
39
36
|
* Resolve R2 credentials, reading the keychain at most once per process. The
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* read is `agentOnly` (resolveR2Config), so it never prompts: a `never`/no-ACL or
|
|
38
|
+
* broker-held bundle resolves silently and is memoized; a locked `hold`/`always`
|
|
39
|
+
* bundle throws the actionable "unlock r2.backups" error. Throws (not memoized)
|
|
40
|
+
* when the bundle/keys are missing or locked — isSyncConfigured catches the throw
|
|
41
|
+
* and degrades to no-transport.
|
|
43
42
|
*/
|
|
44
43
|
export declare function loadR2Config(): R2Config;
|
|
45
44
|
/**
|
|
46
|
-
* True when the sync bundle exists and resolves, without throwing.
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
45
|
+
* True when the sync bundle exists and resolves, without throwing. A missing OR
|
|
46
|
+
* locked bundle resolves to false (session-sync degrades to no-transport) and,
|
|
47
|
+
* because the `agentOnly` read never prompts, it is re-checked each cycle — so a
|
|
48
|
+
* later `agents secrets add` / `agents secrets unlock r2.backups` is picked up
|
|
49
|
+
* promptly with no daemon restart. `now` is accepted for a stable test signature
|
|
50
|
+
* but no longer gates a cooldown (there is no prompt-bearing failure to back off).
|
|
50
51
|
*/
|
|
51
|
-
export declare function isSyncConfigured(
|
|
52
|
+
export declare function isSyncConfigured(_now?: number): boolean;
|
|
52
53
|
export { machineId, normalizeHost } from '../../machine-id.js';
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* actually wired. Credentials come from the `r2.backups` secrets bundle (OS
|
|
12
12
|
* keychain on macOS, libsecret on Linux) — never from env or disk.
|
|
13
13
|
*/
|
|
14
|
-
import { readAndResolveBundleEnv
|
|
14
|
+
import { readAndResolveBundleEnv } from '../../secrets/bundles.js';
|
|
15
15
|
/** Secrets bundle holding the R2 credentials. */
|
|
16
16
|
export const SYNC_BUNDLE = 'r2.backups';
|
|
17
17
|
/**
|
|
@@ -20,13 +20,17 @@ export const SYNC_BUNDLE = 'r2.backups';
|
|
|
20
20
|
* without real credentials (no silent fallback).
|
|
21
21
|
*/
|
|
22
22
|
function resolveR2Config() {
|
|
23
|
-
//
|
|
24
|
-
// resolve
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
23
|
+
// Session-sync is a BACKGROUND read: the daemon's ~90s cycle (and the ~2-min
|
|
24
|
+
// watchdog) resolve this on their own, never at a human's request — so it must
|
|
25
|
+
// NEVER pop a Touch ID sheet, on the interactive launcher included (SEC-13: an
|
|
26
|
+
// agent launch never raises biometry on its own). The read is always
|
|
27
|
+
// `agentOnly`: a `never`/no-ACL or broker-held `r2.backups` bundle resolves
|
|
28
|
+
// silently; a locked `hold`/`always` bundle THROWS the actionable "unlock
|
|
29
|
+
// r2.backups" message instead of prompting. isSyncConfigured catches that throw
|
|
30
|
+
// and degrades to no-transport (sync disabled) with no prompt and no crash —
|
|
31
|
+
// unlock once (`agents secrets unlock r2.backups`) or set it no-ACL
|
|
32
|
+
// (`agents secrets policy r2.backups never`) for silent zero-friction sync.
|
|
33
|
+
const { env } = readAndResolveBundleEnv(SYNC_BUNDLE, { caller: 'session-transport', agentOnly: true });
|
|
30
34
|
const accountId = env.R2_ACCOUNT_ID?.trim();
|
|
31
35
|
const bucket = env.R2_BUCKET_NAME?.trim();
|
|
32
36
|
const accessKeyId = env.R2_ACCESS_KEY_ID?.trim();
|
|
@@ -55,31 +59,32 @@ function resolveR2Config() {
|
|
|
55
59
|
};
|
|
56
60
|
}
|
|
57
61
|
// ── Resolution cache ────────────────────────────────────────────────────────
|
|
58
|
-
// The daemon calls isSyncConfigured() + syncSessions() every ~90s
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
// at most once per process: a success is memoized for the process lifetime
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
62
|
+
// The daemon calls isSyncConfigured() + syncSessions() every ~90s. The read is
|
|
63
|
+
// now `agentOnly` (resolveR2Config) so it can NEVER pop Touch ID — a locked bundle
|
|
64
|
+
// throws a cheap, deterministic "unlock r2.backups" error instead. We still resolve
|
|
65
|
+
// at most once per process: a success is memoized for the process lifetime (cleared
|
|
66
|
+
// on daemon SIGHUP via clearR2ConfigCache), so subsequent cycles never touch the
|
|
67
|
+
// keychain again. A failure (absent bundle, or a LOCKED `hold`/`always` bundle) is
|
|
68
|
+
// NOT memoized and never prompts, so it is re-checked each cycle — session-sync
|
|
69
|
+
// degrades to no-transport until the bundle is added / unlocked, then picks it up
|
|
70
|
+
// promptly with no restart.
|
|
71
|
+
//
|
|
72
|
+
// The historical prompt-backoff cooldown (a cancelled Touch ID sheet) is gone: with
|
|
73
|
+
// agentOnly there is no sheet to cancel, so no failure is prompt-bearing and none
|
|
74
|
+
// needs a backoff.
|
|
67
75
|
let cachedConfig = null;
|
|
68
|
-
let lastPromptFailureAt = 0;
|
|
69
|
-
/** Window after a prompt-bearing resolution failure during which we skip
|
|
70
|
-
* re-attempting (and thus re-prompting). SIGHUP / restart bypasses it. */
|
|
71
|
-
export const RESOLVE_RETRY_COOLDOWN_MS = 30 * 60 * 1000; // 30 minutes
|
|
72
76
|
/** Drop the cached resolution so the next call reads the bundle fresh. Called on
|
|
73
77
|
* daemon SIGHUP (to pick up rotated credentials) and between tests. */
|
|
74
78
|
export function clearR2ConfigCache() {
|
|
75
79
|
cachedConfig = null;
|
|
76
|
-
lastPromptFailureAt = 0;
|
|
77
80
|
}
|
|
78
81
|
/**
|
|
79
82
|
* Resolve R2 credentials, reading the keychain at most once per process. The
|
|
80
|
-
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
+
* read is `agentOnly` (resolveR2Config), so it never prompts: a `never`/no-ACL or
|
|
84
|
+
* broker-held bundle resolves silently and is memoized; a locked `hold`/`always`
|
|
85
|
+
* bundle throws the actionable "unlock r2.backups" error. Throws (not memoized)
|
|
86
|
+
* when the bundle/keys are missing or locked — isSyncConfigured catches the throw
|
|
87
|
+
* and degrades to no-transport.
|
|
83
88
|
*/
|
|
84
89
|
export function loadR2Config() {
|
|
85
90
|
if (cachedConfig)
|
|
@@ -88,26 +93,23 @@ export function loadR2Config() {
|
|
|
88
93
|
return cachedConfig;
|
|
89
94
|
}
|
|
90
95
|
/**
|
|
91
|
-
* True when the sync bundle exists and resolves, without throwing.
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
96
|
+
* True when the sync bundle exists and resolves, without throwing. A missing OR
|
|
97
|
+
* locked bundle resolves to false (session-sync degrades to no-transport) and,
|
|
98
|
+
* because the `agentOnly` read never prompts, it is re-checked each cycle — so a
|
|
99
|
+
* later `agents secrets add` / `agents secrets unlock r2.backups` is picked up
|
|
100
|
+
* promptly with no daemon restart. `now` is accepted for a stable test signature
|
|
101
|
+
* but no longer gates a cooldown (there is no prompt-bearing failure to back off).
|
|
95
102
|
*/
|
|
96
|
-
export function isSyncConfigured(
|
|
103
|
+
export function isSyncConfigured(_now = Date.now()) {
|
|
97
104
|
if (cachedConfig)
|
|
98
105
|
return true;
|
|
99
|
-
if (lastPromptFailureAt && now - lastPromptFailureAt < RESOLVE_RETRY_COOLDOWN_MS)
|
|
100
|
-
return false;
|
|
101
106
|
try {
|
|
102
107
|
loadR2Config();
|
|
103
108
|
return true;
|
|
104
109
|
}
|
|
105
|
-
catch
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
// have cost a prompt (cancelled Touch ID, keychain error) — back off.
|
|
109
|
-
if (!/not found/i.test(err.message))
|
|
110
|
-
lastPromptFailureAt = now;
|
|
110
|
+
catch {
|
|
111
|
+
// Absent or locked bundle — never prompted (agentOnly), so no backoff: keep
|
|
112
|
+
// re-checking each cycle for fast pickup once the bundle is added / unlocked.
|
|
111
113
|
return false;
|
|
112
114
|
}
|
|
113
115
|
}
|
package/dist/lib/share/config.js
CHANGED
|
@@ -76,6 +76,9 @@ export function storeWriteToken(token) {
|
|
|
76
76
|
export function readWriteTokenFromBundle() {
|
|
77
77
|
const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
|
|
78
78
|
caller: 'share',
|
|
79
|
+
// Explicit `agents share` command (a human published a file): a headless agent
|
|
80
|
+
// subprocess resolves broker-only, an interactive human may unlock. This is NOT
|
|
81
|
+
// an agent LAUNCH read (that is exec.ts's --secrets injection, always agentOnly).
|
|
79
82
|
agentOnly: isHeadlessSecretsContext(),
|
|
80
83
|
});
|
|
81
84
|
const token = env[SHARE_TOKEN_KEY];
|
|
@@ -133,6 +136,7 @@ export function readCloudflareCreds(bundle = DEFAULT_CF_BUNDLE, override) {
|
|
|
133
136
|
}
|
|
134
137
|
const { env } = readAndResolveBundleEnv(bundle, {
|
|
135
138
|
caller: 'share',
|
|
139
|
+
// Explicit `agents share setup` provisioning read — not an agent launch.
|
|
136
140
|
agentOnly: isHeadlessSecretsContext(),
|
|
137
141
|
});
|
|
138
142
|
const find = (re) => {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -764,6 +764,16 @@ export interface Meta {
|
|
|
764
764
|
*/
|
|
765
765
|
isolatedAgents?: Partial<Record<AgentId, string>>;
|
|
766
766
|
run?: RunConfig;
|
|
767
|
+
/**
|
|
768
|
+
* Cost-tier overrides for `--model cheap|default|best|ultra`. Keyed by the same
|
|
769
|
+
* `<agent>:<version>` selector run.defaults uses (`kimi:*`, `kimi:0.19.2`); each
|
|
770
|
+
* value maps a tier to a concrete model id. Written by `agents models tier set`,
|
|
771
|
+
* never hand-edited. Resolution: exact version selector wins over `<agent>:*`,
|
|
772
|
+
* which wins over the auto-ranking. See lib/model-tier-overrides.ts.
|
|
773
|
+
*/
|
|
774
|
+
model?: {
|
|
775
|
+
tiers?: Record<string, Partial<Record<'cheap' | 'default' | 'best' | 'ultra', string>>>;
|
|
776
|
+
};
|
|
767
777
|
/**
|
|
768
778
|
* Daemon watchdog config. `rotate` (default `on`) lets the watchdog rotate a
|
|
769
779
|
* rate-limited session IN PLACE onto a healthy account/harness via
|
package/dist/lib/versions.js
CHANGED
|
@@ -1358,6 +1358,38 @@ function relocateGrokBinaryToVersionHome(installedVersion) {
|
|
|
1358
1358
|
return;
|
|
1359
1359
|
}
|
|
1360
1360
|
}
|
|
1361
|
+
/**
|
|
1362
|
+
* Grok's version directory is keyed by release number alone, not by account —
|
|
1363
|
+
* so two DIFFERENT accounts that both self-update to the identical upstream
|
|
1364
|
+
* release ("latest") target the SAME on-disk `versions/grok/<version>/` home.
|
|
1365
|
+
* When that happens, the second account's `agents add grok@latest` writes
|
|
1366
|
+
* into an already-signed-in directory: grok's own auth flow treats
|
|
1367
|
+
* `~/.grok/auth.json` as authoritative for whichever process invoked it, so
|
|
1368
|
+
* the second account's credential record lands in a file that still names it
|
|
1369
|
+
* as the first account's install everywhere else in agents-cli's bookkeeping.
|
|
1370
|
+
*
|
|
1371
|
+
* Refuse before that happens: if the target version's home already has a
|
|
1372
|
+
* signed-in account whose identity differs from the account currently
|
|
1373
|
+
* driving this update (the previous global default), fail loud instead of
|
|
1374
|
+
* letting the second account silently displace the first's install.
|
|
1375
|
+
*/
|
|
1376
|
+
async function checkGrokAccountCollision(installedVersion) {
|
|
1377
|
+
const targetHome = getVersionHomePath('grok', installedVersion);
|
|
1378
|
+
if (!fs.existsSync(targetHome))
|
|
1379
|
+
return; // fresh directory, nothing to collide with
|
|
1380
|
+
const sourceVersion = getGlobalDefault('grok');
|
|
1381
|
+
if (!sourceVersion || sourceVersion === installedVersion)
|
|
1382
|
+
return; // same install, not a collision
|
|
1383
|
+
const [targetEmail, sourceEmail] = await Promise.all([
|
|
1384
|
+
getAccountEmail('grok', targetHome),
|
|
1385
|
+
getAccountEmail('grok', getVersionHomePath('grok', sourceVersion)),
|
|
1386
|
+
]);
|
|
1387
|
+
if (!targetEmail || !sourceEmail || targetEmail === sourceEmail)
|
|
1388
|
+
return;
|
|
1389
|
+
throw new Error(`grok@${installedVersion} is already installed for ${targetEmail}, but this update is running as ${sourceEmail}. ` +
|
|
1390
|
+
`Grok's self-updater can't distinguish two accounts that land on the same release — sign in to ${targetEmail}'s ` +
|
|
1391
|
+
`install (agents use grok@${installedVersion}) before updating it, or wait until the releases diverge.`);
|
|
1392
|
+
}
|
|
1361
1393
|
/**
|
|
1362
1394
|
* Install a specific version of an agent.
|
|
1363
1395
|
*/
|
|
@@ -1428,6 +1460,9 @@ export async function installVersion(agent, version, onProgress, opts) {
|
|
|
1428
1460
|
// install into the real version so it stops shadowing `agents view`.
|
|
1429
1461
|
await reconcileStaleLatestDir(agent, installedVersion);
|
|
1430
1462
|
}
|
|
1463
|
+
if (agent === 'grok') {
|
|
1464
|
+
await checkGrokAccountCollision(installedVersion);
|
|
1465
|
+
}
|
|
1431
1466
|
onProgress?.(`${agentConfig.name} installed. Setting up agents-cli version home for isolation...`);
|
|
1432
1467
|
}
|
|
1433
1468
|
catch (err) {
|
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.6",
|
|
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",
|