@phnx-labs/agents-cli 1.22.15 → 1.22.17
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 +52 -0
- package/README.md +13 -2
- package/dist/bin/agents +0 -0
- package/dist/commands/cloud.d.ts +0 -1
- package/dist/commands/cloud.js +19 -185
- package/dist/commands/doctor.js +16 -14
- package/dist/commands/exec.js +153 -16
- package/dist/commands/feed.d.ts +1 -0
- package/dist/commands/feed.js +36 -32
- package/dist/commands/humans.d.ts +10 -0
- package/dist/commands/humans.js +91 -0
- package/dist/commands/resume.d.ts +17 -0
- package/dist/commands/resume.js +80 -0
- package/dist/commands/run-cloud.d.ts +26 -0
- package/dist/commands/run-cloud.js +162 -0
- package/dist/commands/sessions.d.ts +13 -4
- package/dist/commands/sessions.js +37 -19
- package/dist/commands/versions.js +4 -2
- package/dist/commands/view.js +1 -54
- package/dist/index.js +3 -2
- package/dist/lib/agents.js +4 -2
- package/dist/lib/channels/send.d.ts +5 -2
- package/dist/lib/channels/send.js +16 -13
- package/dist/lib/cloud/dispatch.d.ts +27 -0
- package/dist/lib/cloud/dispatch.js +214 -0
- package/dist/lib/devices/doctor-findings.d.ts +14 -0
- package/dist/lib/devices/doctor-findings.js +92 -51
- package/dist/lib/exec.d.ts +6 -2
- package/dist/lib/exec.js +22 -4
- package/dist/lib/feed-post.d.ts +2 -2
- package/dist/lib/feed-post.js +15 -10
- package/dist/lib/hooks.d.ts +4 -2
- package/dist/lib/hooks.js +173 -23
- package/dist/lib/hosts/remote-cmd.js +8 -0
- package/dist/lib/humans.d.ts +26 -0
- package/dist/lib/humans.js +73 -0
- package/dist/lib/memory.js +2 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/migrate.js +112 -4
- package/dist/lib/notify.d.ts +4 -4
- package/dist/lib/notify.js +4 -3
- package/dist/lib/permissions.js +12 -10
- package/dist/lib/placement.d.ts +8 -4
- package/dist/lib/placement.js +14 -8
- package/dist/lib/projects.d.ts +3 -3
- package/dist/lib/projects.js +19 -17
- 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/bundles.d.ts +11 -0
- package/dist/lib/secrets/bundles.js +122 -5
- package/dist/lib/session/actor-sidecar.d.ts +5 -2
- package/dist/lib/session/actor-sidecar.js +4 -2
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +19 -3
- package/dist/lib/session/types.d.ts +4 -0
- package/dist/lib/settings-manifest.js +7 -1
- package/dist/lib/staleness/writers/sources.d.ts +6 -1
- package/dist/lib/staleness/writers/sources.js +93 -7
- package/dist/lib/startup/command-registry.d.ts +2 -0
- package/dist/lib/startup/command-registry.js +5 -0
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +6 -6
- package/dist/lib/types.d.ts +52 -0
- package/dist/lib/versions.js +48 -14
- package/package.json +1 -1
package/dist/lib/permissions.js
CHANGED
|
@@ -122,9 +122,9 @@ export function convertDenyToCodexRules(deny) {
|
|
|
122
122
|
* Ensure central permissions directory exists.
|
|
123
123
|
*/
|
|
124
124
|
function ensurePermissionsDir() {
|
|
125
|
-
const
|
|
126
|
-
if (!fs.existsSync(
|
|
127
|
-
fs.mkdirSync(
|
|
125
|
+
const groupsDir = path.join(getUserPermissionsDir(), 'groups');
|
|
126
|
+
if (!fs.existsSync(groupsDir)) {
|
|
127
|
+
fs.mkdirSync(groupsDir, { recursive: true });
|
|
128
128
|
}
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
@@ -350,7 +350,8 @@ export function listInstalledPermissions() {
|
|
|
350
350
|
ensureAgentsDir();
|
|
351
351
|
const seen = new Set();
|
|
352
352
|
const results = [];
|
|
353
|
-
for (const
|
|
353
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
354
|
+
const dir = path.join(baseDir, 'groups');
|
|
354
355
|
if (!fs.existsSync(dir))
|
|
355
356
|
continue;
|
|
356
357
|
try {
|
|
@@ -377,10 +378,11 @@ export function listInstalledPermissions() {
|
|
|
377
378
|
return results;
|
|
378
379
|
}
|
|
379
380
|
/**
|
|
380
|
-
* Get a specific permission set by name. Searches user dir first, then system
|
|
381
|
+
* Get a specific permission set by name. Searches user groups/ dir first, then system groups/.
|
|
381
382
|
*/
|
|
382
383
|
function getPermissionSet(name) {
|
|
383
|
-
for (const
|
|
384
|
+
for (const baseDir of [getUserPermissionsDir(), getPermissionsDir()]) {
|
|
385
|
+
const dir = path.join(baseDir, 'groups');
|
|
384
386
|
for (const ext of ['.yml', '.yaml']) {
|
|
385
387
|
const filePath = safeJoin(dir, name + ext);
|
|
386
388
|
if (fs.existsSync(filePath)) {
|
|
@@ -402,7 +404,7 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
402
404
|
if (!set) {
|
|
403
405
|
return { success: false, error: 'Invalid permission file' };
|
|
404
406
|
}
|
|
405
|
-
const targetPath = safeJoin(getUserPermissionsDir(), name + '.yml');
|
|
407
|
+
const targetPath = safeJoin(path.join(getUserPermissionsDir(), 'groups'), name + '.yml');
|
|
406
408
|
try {
|
|
407
409
|
fs.copyFileSync(sourcePath, targetPath);
|
|
408
410
|
return { success: true };
|
|
@@ -416,9 +418,9 @@ export function installPermissionSet(sourcePath, name) {
|
|
|
416
418
|
* sets are intentionally not deletable from user commands.
|
|
417
419
|
*/
|
|
418
420
|
export function removePermissionSet(name) {
|
|
419
|
-
const
|
|
421
|
+
const groupsDir = path.join(getUserPermissionsDir(), 'groups');
|
|
420
422
|
for (const ext of ['.yml', '.yaml']) {
|
|
421
|
-
const filePath = safeJoin(
|
|
423
|
+
const filePath = safeJoin(groupsDir, name + ext);
|
|
422
424
|
if (fs.existsSync(filePath)) {
|
|
423
425
|
try {
|
|
424
426
|
fs.unlinkSync(filePath);
|
|
@@ -2025,7 +2027,7 @@ export function exportPermissionsFromPath(filePath) {
|
|
|
2025
2027
|
*/
|
|
2026
2028
|
function savePermissionSet(set) {
|
|
2027
2029
|
ensurePermissionsDir();
|
|
2028
|
-
const filePath = safeJoin(getUserPermissionsDir(), set.name + '.yml');
|
|
2030
|
+
const filePath = safeJoin(path.join(getUserPermissionsDir(), 'groups'), set.name + '.yml');
|
|
2029
2031
|
try {
|
|
2030
2032
|
const content = yaml.stringify({
|
|
2031
2033
|
name: set.name,
|
package/dist/lib/placement.d.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Placement — one model for "where does the body run?"
|
|
3
3
|
*
|
|
4
4
|
* The CLI grew several doors that all mean execution target:
|
|
5
|
-
* run --host / --device / --lease / --box
|
|
5
|
+
* run --host / --device / --lease / --box / --cloud
|
|
6
6
|
* routines --placement / --run-on / hostStrategy
|
|
7
7
|
* monitors --run-on (body) vs --device (owner — NOT placement)
|
|
8
8
|
* teams --device (teammate pin)
|
|
@@ -41,6 +41,10 @@ export interface RunPlacementFlags {
|
|
|
41
41
|
computer?: string;
|
|
42
42
|
lease?: string | boolean;
|
|
43
43
|
box?: string;
|
|
44
|
+
/** --cloud: vendor cloud placement (the agent's native cloud provider). */
|
|
45
|
+
cloud?: boolean;
|
|
46
|
+
/** --provider: refines the cloud placement; not a placement on its own. */
|
|
47
|
+
provider?: string;
|
|
44
48
|
}
|
|
45
49
|
export declare class PlacementError extends Error {
|
|
46
50
|
constructor(message: string);
|
|
@@ -68,10 +72,10 @@ export declare function placementFromRunFlags(flags: RunPlacementFlags): Placeme
|
|
|
68
72
|
* Expand a resolved placement into the concrete run option fields the
|
|
69
73
|
* existing dispatch paths already understand. Pure — does not mutate input.
|
|
70
74
|
*
|
|
71
|
-
* `
|
|
72
|
-
*
|
|
75
|
+
* `fleet` is not valid for a bare `agents run` (it is a routines placement);
|
|
76
|
+
* it throws so callers fail loud.
|
|
73
77
|
*/
|
|
74
|
-
export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box'>;
|
|
78
|
+
export declare function expandPlacementToRunFlags(placement: Placement): Pick<RunPlacementFlags, 'host' | 'device' | 'lease' | 'box' | 'cloud' | 'provider'>;
|
|
75
79
|
/** Map routines hostStrategy (+ optional host) onto the shared Placement. */
|
|
76
80
|
export declare function placementFromHostStrategy(strategy: 'local' | 'host' | 'fleet' | 'cloud', host?: string): Placement;
|
|
77
81
|
/** One-line human form for logs / help. */
|
package/dist/lib/placement.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Placement — one model for "where does the body run?"
|
|
3
3
|
*
|
|
4
4
|
* The CLI grew several doors that all mean execution target:
|
|
5
|
-
* run --host / --device / --lease / --box
|
|
5
|
+
* run --host / --device / --lease / --box / --cloud
|
|
6
6
|
* routines --placement / --run-on / hostStrategy
|
|
7
7
|
* monitors --run-on (body) vs --device (owner — NOT placement)
|
|
8
8
|
* teams --device (teammate pin)
|
|
@@ -93,6 +93,7 @@ export function placementFromRunFlags(flags) {
|
|
|
93
93
|
const hostT = hostFamilyTarget(flags);
|
|
94
94
|
const hasLease = flags.lease !== undefined && flags.lease !== false;
|
|
95
95
|
const hasBox = !!flags.box;
|
|
96
|
+
const hasCloud = flags.cloud === true;
|
|
96
97
|
const placementFlags = [];
|
|
97
98
|
if (where)
|
|
98
99
|
placementFlags.push('--where');
|
|
@@ -102,12 +103,16 @@ export function placementFromRunFlags(flags) {
|
|
|
102
103
|
placementFlags.push('--lease');
|
|
103
104
|
if (hasBox)
|
|
104
105
|
placementFlags.push('--box');
|
|
106
|
+
if (hasCloud)
|
|
107
|
+
placementFlags.push('--cloud');
|
|
105
108
|
if (placementFlags.length > 1) {
|
|
106
109
|
throw new PlacementError(`Conflicting placement flags: ${placementFlags.join(' + ')}. ` +
|
|
107
|
-
`Use one door — prefer --where (device:<name> | auto | lease | local).`);
|
|
110
|
+
`Use one door — prefer --where (device:<name> | auto | lease | cloud | local).`);
|
|
108
111
|
}
|
|
109
112
|
if (where)
|
|
110
113
|
return parseWhereSpec(where, '--where');
|
|
114
|
+
if (hasCloud)
|
|
115
|
+
return { kind: 'cloud', target: flags.provider, source: '--cloud' };
|
|
111
116
|
if (hasBox)
|
|
112
117
|
return { kind: 'lease', target: flags.box, source: '--box' };
|
|
113
118
|
if (hasLease) {
|
|
@@ -122,8 +127,8 @@ export function placementFromRunFlags(flags) {
|
|
|
122
127
|
* Expand a resolved placement into the concrete run option fields the
|
|
123
128
|
* existing dispatch paths already understand. Pure — does not mutate input.
|
|
124
129
|
*
|
|
125
|
-
* `
|
|
126
|
-
*
|
|
130
|
+
* `fleet` is not valid for a bare `agents run` (it is a routines placement);
|
|
131
|
+
* it throws so callers fail loud.
|
|
127
132
|
*/
|
|
128
133
|
export function expandPlacementToRunFlags(placement) {
|
|
129
134
|
switch (placement.kind) {
|
|
@@ -140,12 +145,13 @@ export function expandPlacementToRunFlags(placement) {
|
|
|
140
145
|
if (placement.source === '--box')
|
|
141
146
|
return { box: placement.target };
|
|
142
147
|
return placement.target ? { lease: placement.target } : { lease: true };
|
|
148
|
+
case 'cloud':
|
|
149
|
+
// Vendor cloud placement — `--where cloud[:provider]` expands to the
|
|
150
|
+
// --cloud flag (+ --provider refinement) the run action dispatches on.
|
|
151
|
+
return placement.target ? { cloud: true, provider: placement.target } : { cloud: true };
|
|
143
152
|
case 'fleet':
|
|
144
153
|
throw new PlacementError(`fleet placement is for routines (agents routines add … --placement fleet), not agents run. ` +
|
|
145
154
|
`Use --where device:auto for an affinity pick, or --where device:<name>.`);
|
|
146
|
-
case 'cloud':
|
|
147
|
-
throw new PlacementError(`cloud placement is agents cloud run (vendor cloud), not agents run. ` +
|
|
148
|
-
`For a disposable box use --where lease; for your fleet use --where device:<name>.`);
|
|
149
155
|
}
|
|
150
156
|
}
|
|
151
157
|
/** Map routines hostStrategy (+ optional host) onto the shared Placement. */
|
|
@@ -180,9 +186,9 @@ export const PLACEMENT_MATRIX = `
|
|
|
180
186
|
Affinity pick (14d usage) --where auto (= --device auto)
|
|
181
187
|
Disposable cloud box --where lease (= --lease)
|
|
182
188
|
Reuse warm crabbox --box <slug>
|
|
189
|
+
Vendor cloud task --cloud (= --where cloud[:provider])
|
|
183
190
|
Routines: body on one box --run-on <name> / --placement host
|
|
184
191
|
Routines: pick any online --placement fleet
|
|
185
|
-
Vendor cloud task agents cloud run …
|
|
186
192
|
Monitors: who evaluates --device <owner> (NOT body placement)
|
|
187
193
|
Monitors: where action runs --run-on <host>
|
|
188
194
|
`.trim();
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -132,9 +132,9 @@ export declare function validateProjectDef(raw: unknown, sourceName?: string): P
|
|
|
132
132
|
*/
|
|
133
133
|
export declare function loadProjectDef(name: string): ProjectDef | undefined;
|
|
134
134
|
/**
|
|
135
|
-
* List every defined project, sorted by name.
|
|
136
|
-
*
|
|
137
|
-
*
|
|
135
|
+
* List every defined project, sorted by name. A missing projects directory is
|
|
136
|
+
* the empty state; malformed definitions and filesystem failures stay loud so
|
|
137
|
+
* CLI callers (including Factory) can show the actual error.
|
|
138
138
|
*/
|
|
139
139
|
export declare function listProjectDefs(): ProjectDef[];
|
|
140
140
|
/**
|
package/dist/lib/projects.js
CHANGED
|
@@ -187,15 +187,18 @@ export function loadProjectDef(name) {
|
|
|
187
187
|
try {
|
|
188
188
|
raw = fs.readFileSync(projectDefPath(name), 'utf8');
|
|
189
189
|
}
|
|
190
|
-
catch {
|
|
191
|
-
|
|
190
|
+
catch (error) {
|
|
191
|
+
if (error.code === 'ENOENT') {
|
|
192
|
+
return undefined; // absent — not a defined project
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
192
195
|
}
|
|
193
196
|
return validateProjectDef(yaml.parse(raw), name);
|
|
194
197
|
}
|
|
195
198
|
/**
|
|
196
|
-
* List every defined project, sorted by name.
|
|
197
|
-
*
|
|
198
|
-
*
|
|
199
|
+
* List every defined project, sorted by name. A missing projects directory is
|
|
200
|
+
* the empty state; malformed definitions and filesystem failures stay loud so
|
|
201
|
+
* CLI callers (including Factory) can show the actual error.
|
|
199
202
|
*/
|
|
200
203
|
export function listProjectDefs() {
|
|
201
204
|
let files;
|
|
@@ -205,20 +208,17 @@ export function listProjectDefs() {
|
|
|
205
208
|
// loader and silently drop the project. One extension, one code path.
|
|
206
209
|
files = fs.readdirSync(getProjectsDir()).filter((f) => f.endsWith('.yaml'));
|
|
207
210
|
}
|
|
208
|
-
catch {
|
|
209
|
-
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error.code === 'ENOENT')
|
|
213
|
+
return [];
|
|
214
|
+
throw error;
|
|
210
215
|
}
|
|
211
216
|
const out = [];
|
|
212
217
|
for (const f of files) {
|
|
213
218
|
const name = f.replace(/\.yaml$/, '');
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
out.push(def);
|
|
218
|
-
}
|
|
219
|
-
catch {
|
|
220
|
-
/* malformed — skip in the listing */
|
|
221
|
-
}
|
|
219
|
+
const def = loadProjectDef(name);
|
|
220
|
+
if (def)
|
|
221
|
+
out.push(def);
|
|
222
222
|
}
|
|
223
223
|
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
224
224
|
}
|
|
@@ -255,8 +255,10 @@ export function removeProjectDef(name) {
|
|
|
255
255
|
fs.unlinkSync(projectDefPath(name));
|
|
256
256
|
return true;
|
|
257
257
|
}
|
|
258
|
-
catch {
|
|
259
|
-
|
|
258
|
+
catch (error) {
|
|
259
|
+
if (error.code === 'ENOENT')
|
|
260
|
+
return false;
|
|
261
|
+
throw error;
|
|
260
262
|
}
|
|
261
263
|
}
|
|
262
264
|
/**
|
|
Binary file
|
|
Binary file
|
|
@@ -178,6 +178,17 @@ export declare function shouldEvictAfterBundleWrite(skipRequested: boolean, noAg
|
|
|
178
178
|
export declare function writeBundle(bundle: SecretsBundle, opts?: WriteBundleOptions): void;
|
|
179
179
|
export declare function writeBundleWithItems(bundle: SecretsBundle, items: Map<string, string>, opts?: WriteBundleOptions): void;
|
|
180
180
|
export declare function deleteBundle(name: string): boolean;
|
|
181
|
+
declare function readBundleMetaIndex(): string[] | null;
|
|
182
|
+
declare function writeBundleMetaIndex(services: string[]): void;
|
|
183
|
+
declare function addBundleToMetaIndex(name: string): void;
|
|
184
|
+
declare function removeBundleFromMetaIndex(name: string): void;
|
|
185
|
+
/** Test-only accessors for the metadata-name index. Never called in production. */
|
|
186
|
+
export declare const __metaIndexForTest: {
|
|
187
|
+
read: typeof readBundleMetaIndex;
|
|
188
|
+
write: typeof writeBundleMetaIndex;
|
|
189
|
+
add: typeof addBundleToMetaIndex;
|
|
190
|
+
remove: typeof removeBundleFromMetaIndex;
|
|
191
|
+
};
|
|
181
192
|
/**
|
|
182
193
|
* Re-write already-read keychain bundle metadata items WITHOUT the biometry ACL.
|
|
183
194
|
* `metaJsonByName` maps bundle name → the exact metadata JSON listBundles just
|
|
@@ -468,6 +468,8 @@ export function writeBundle(bundle, opts = {}) {
|
|
|
468
468
|
// un-updated pinned helper this write fails loudly (the no-ACL command is
|
|
469
469
|
// missing) rather than silently landing an ACL'd item.
|
|
470
470
|
itemStore(prepared.backend).set(prepared.metadataItem, prepared.metadataJson, { noAcl: true });
|
|
471
|
+
if (prepared.backend === 'keychain')
|
|
472
|
+
addBundleToMetaIndex(bundle.name);
|
|
471
473
|
finishBundleWrite(bundle, opts);
|
|
472
474
|
}
|
|
473
475
|
export function writeBundleWithItems(bundle, items, opts = {}) {
|
|
@@ -485,6 +487,7 @@ export function writeBundleWithItems(bundle, items, opts = {}) {
|
|
|
485
487
|
store.setBatch(new Map(items), { noAcl: bundle.policy === 'never' });
|
|
486
488
|
}
|
|
487
489
|
store.set(prepared.metadataItem, prepared.metadataJson, { noAcl: true });
|
|
490
|
+
addBundleToMetaIndex(bundle.name);
|
|
488
491
|
}
|
|
489
492
|
else {
|
|
490
493
|
// file / vault: no ACL concept (noAcl is ignored), so one batched write is
|
|
@@ -497,8 +500,11 @@ export function writeBundleWithItems(bundle, items, opts = {}) {
|
|
|
497
500
|
}
|
|
498
501
|
export function deleteBundle(name) {
|
|
499
502
|
validateBundleName(name);
|
|
500
|
-
const
|
|
503
|
+
const backend = bundleBackend(name);
|
|
504
|
+
const deleted = itemStore(backend).delete(bundleMetaItem(name));
|
|
501
505
|
if (deleted) {
|
|
506
|
+
if (backend === 'keychain')
|
|
507
|
+
removeBundleFromMetaIndex(name);
|
|
502
508
|
emit('secrets.delete', { module: 'secrets', bundle: name });
|
|
503
509
|
if (shouldEvictAfterBundleWrite(false, process.env.AGENTS_SECRETS_NO_AGENT, isKeychainBackendOverridden())) {
|
|
504
510
|
agentEvictSync(name);
|
|
@@ -581,6 +587,104 @@ function markBundleMetadataAclHealed() {
|
|
|
581
587
|
// on the next broker-miss listing.
|
|
582
588
|
}
|
|
583
589
|
}
|
|
590
|
+
// ── No-ACL bundle-metadata name index (kills the enumeration Touch ID storm) ──
|
|
591
|
+
// listBundles cannot ask the keychain for "just the metadata items": with hashed
|
|
592
|
+
// service names (#316) the metadata names are opaque (`agents-cli.h.<ns>.m`), so
|
|
593
|
+
// listKeychainItems(BUNDLE_META_PREFIX) falls back to a BROAD `agents-cli.` scan
|
|
594
|
+
// that also MATCHES the ACL'd secret VALUE items. On some machines macOS
|
|
595
|
+
// evaluates those value ACLs during that attributes-only scan and pops a generic
|
|
596
|
+
// "Agents CLI needs to authenticate" sheet — on EVERY launch (session-title
|
|
597
|
+
// generation, `agents devices list`, every agent run), because listBundles runs
|
|
598
|
+
// on essentially every secrets touch. Neither UIFail nor LAContext can list the
|
|
599
|
+
// no-ACL items while skipping the ACL'd ones (both return nothing), so the fix is
|
|
600
|
+
// to NOT do the broad scan: keep a per-machine index of the metadata items'
|
|
601
|
+
// STORAGE names in the regenerable helpers dir and read THAT (a silent file read)
|
|
602
|
+
// instead. The index holds opaque hashes only — no cleartext bundle names, so it
|
|
603
|
+
// leaks nothing #316 didn't already. It self-heals: absent/unbuilt → listBundles
|
|
604
|
+
// rebuilds it from the one-time broad scan; a stale entry only makes `secrets
|
|
605
|
+
// list` cosmetically incomplete and never affects a resolve-by-name (which
|
|
606
|
+
// computes the hashed name directly, never through this index).
|
|
607
|
+
function bundleMetaIndexPath() {
|
|
608
|
+
// Test-only override (mirrors AGENTS_DAEMON_DIR): redirect to a fork-private
|
|
609
|
+
// temp so unit tests never touch the real helpers dir. Never set in prod.
|
|
610
|
+
return (process.env.AGENTS_SECRETS_META_INDEX_FILE ||
|
|
611
|
+
path.join(getHelpersDir(), 'secrets-agent', 'bundle-meta-index.json'));
|
|
612
|
+
}
|
|
613
|
+
// Changes iff the service-name hashing key changes (the #316 re-key, or a
|
|
614
|
+
// cleartext<->hashed transition). Stamped into the index so an index built under
|
|
615
|
+
// an OLD key reads as absent and is rebuilt — otherwise a re-key would leave
|
|
616
|
+
// stale hashed names that resolve to nothing and make every bundle "vanish".
|
|
617
|
+
function metaIndexFingerprint() {
|
|
618
|
+
return keychainServiceAlias(`${BUNDLE_META_PREFIX}meta-index-fingerprint`);
|
|
619
|
+
}
|
|
620
|
+
function readBundleMetaIndex() {
|
|
621
|
+
// A test-installed in-memory keychain is NOT the real store this index mirrors;
|
|
622
|
+
// reading (and later writing) the real ~/.agents index from a mock-backend test
|
|
623
|
+
// would leak fixture bundle names into a developer's live cache. Same guard the
|
|
624
|
+
// sibling healKeychainBundleMetadataAclOnce uses — treat the index as absent so
|
|
625
|
+
// listBundles falls back to the (mock) scan.
|
|
626
|
+
if (isKeychainBackendOverridden())
|
|
627
|
+
return null;
|
|
628
|
+
try {
|
|
629
|
+
const parsed = JSON.parse(fs.readFileSync(bundleMetaIndexPath(), 'utf-8'));
|
|
630
|
+
if (!parsed || typeof parsed !== 'object' || !Array.isArray(parsed.services))
|
|
631
|
+
return null;
|
|
632
|
+
if (parsed.fp !== metaIndexFingerprint())
|
|
633
|
+
return null; // built under a different hashing key
|
|
634
|
+
return parsed.services.every((s) => typeof s === 'string') ? parsed.services : null;
|
|
635
|
+
}
|
|
636
|
+
catch {
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
function writeBundleMetaIndex(services) {
|
|
641
|
+
if (isKeychainBackendOverridden())
|
|
642
|
+
return; // never write the real index from a mock-backend test
|
|
643
|
+
try {
|
|
644
|
+
const file = bundleMetaIndexPath();
|
|
645
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
646
|
+
const payload = { fp: metaIndexFingerprint(), services: [...new Set(services)].sort() };
|
|
647
|
+
// Atomic write (unique temp + rename) so a concurrent reader/writer never
|
|
648
|
+
// sees a half-written file. The read-modify-write in add/remove can still
|
|
649
|
+
// race two concurrent BUNDLE mutations and drop an entry, but that only makes
|
|
650
|
+
// a `secrets list` cosmetically incomplete (never a resolve-by-name) and
|
|
651
|
+
// self-heals on the next rebuild — an acceptable trade for rare bundle edits.
|
|
652
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
653
|
+
fs.writeFileSync(tmp, JSON.stringify(payload), 'utf8');
|
|
654
|
+
fs.renameSync(tmp, file);
|
|
655
|
+
}
|
|
656
|
+
catch {
|
|
657
|
+
// Best effort — a missing index just means listBundles rebuilds it from the
|
|
658
|
+
// one-time broad scan on the next enumeration.
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
// Append a metadata item's STORAGE name to an ALREADY-BUILT index. No-op when the
|
|
662
|
+
// index has not been built yet (null): never create a one-entry index that would
|
|
663
|
+
// hide every OTHER bundle — listBundles builds the complete index on its first
|
|
664
|
+
// scan, and this newly-written bundle is included in that scan.
|
|
665
|
+
function addBundleToMetaIndex(name) {
|
|
666
|
+
const cur = readBundleMetaIndex();
|
|
667
|
+
if (cur === null)
|
|
668
|
+
return;
|
|
669
|
+
const svc = keychainServiceAlias(bundleMetaItem(name));
|
|
670
|
+
if (!cur.includes(svc))
|
|
671
|
+
writeBundleMetaIndex([...cur, svc]);
|
|
672
|
+
}
|
|
673
|
+
function removeBundleFromMetaIndex(name) {
|
|
674
|
+
const cur = readBundleMetaIndex();
|
|
675
|
+
if (cur === null)
|
|
676
|
+
return;
|
|
677
|
+
const svc = keychainServiceAlias(bundleMetaItem(name));
|
|
678
|
+
if (cur.includes(svc))
|
|
679
|
+
writeBundleMetaIndex(cur.filter((s) => s !== svc));
|
|
680
|
+
}
|
|
681
|
+
/** Test-only accessors for the metadata-name index. Never called in production. */
|
|
682
|
+
export const __metaIndexForTest = {
|
|
683
|
+
read: readBundleMetaIndex,
|
|
684
|
+
write: writeBundleMetaIndex,
|
|
685
|
+
add: addBundleToMetaIndex,
|
|
686
|
+
remove: removeBundleFromMetaIndex,
|
|
687
|
+
};
|
|
584
688
|
/**
|
|
585
689
|
* Re-write already-read keychain bundle metadata items WITHOUT the biometry ACL.
|
|
586
690
|
* `metaJsonByName` maps bundle name → the exact metadata JSON listBundles just
|
|
@@ -643,11 +747,24 @@ export function listBundles() {
|
|
|
643
747
|
// file store is the single source of truth, so the block below covers all.
|
|
644
748
|
if (!keychainUsesFileFallback()) {
|
|
645
749
|
let keychainServices = [];
|
|
646
|
-
|
|
647
|
-
|
|
750
|
+
// Prefer the no-ACL metadata-name index (a silent file read) over the broad
|
|
751
|
+
// `agents-cli.` keychain scan — that scan also matches ACL'd secret VALUE
|
|
752
|
+
// items and pops Touch ID on every launch (see the index helpers above).
|
|
753
|
+
// Absent index (first list after upgrade / a cache wipe) → do the one broad
|
|
754
|
+
// scan and build the index from it, so that scan is the LAST one this machine
|
|
755
|
+
// performs.
|
|
756
|
+
const indexedServices = readBundleMetaIndex();
|
|
757
|
+
if (indexedServices !== null) {
|
|
758
|
+
keychainServices = indexedServices;
|
|
648
759
|
}
|
|
649
|
-
|
|
650
|
-
|
|
760
|
+
else {
|
|
761
|
+
try {
|
|
762
|
+
keychainServices = listKeychainItems(BUNDLE_META_PREFIX);
|
|
763
|
+
writeBundleMetaIndex(keychainServices);
|
|
764
|
+
}
|
|
765
|
+
catch {
|
|
766
|
+
keychainServices = [];
|
|
767
|
+
}
|
|
651
768
|
}
|
|
652
769
|
// With hashed service names (macOS, #316) the enumerated services are
|
|
653
770
|
// opaque (`agents-cli.h.<ns>.m`) — the display name is recovered from the
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import type { SessionRunMode } from './types.js';
|
|
1
2
|
export interface SessionActorRecord {
|
|
2
3
|
sessionId: string;
|
|
3
4
|
/** Resolved actor id (`resolveActor().id`) — the responsible human/agent. */
|
|
4
|
-
actor
|
|
5
|
+
actor?: string;
|
|
5
6
|
/** Actor kind (`resolveActor().kind`). */
|
|
6
|
-
initiatedBy
|
|
7
|
+
initiatedBy?: 'human' | 'agent';
|
|
8
|
+
/** Effective permissions mode used by the launcher. */
|
|
9
|
+
mode?: SessionRunMode;
|
|
7
10
|
startedAtMs: number;
|
|
8
11
|
}
|
|
9
12
|
/**
|
|
@@ -62,7 +62,8 @@ export function readSessionActorRecord(sessionId) {
|
|
|
62
62
|
}
|
|
63
63
|
try {
|
|
64
64
|
const parsed = JSON.parse(raw);
|
|
65
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.sessionId === 'string' &&
|
|
65
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.sessionId === 'string' &&
|
|
66
|
+
(typeof parsed.actor === 'string' || typeof parsed.mode === 'string')) {
|
|
66
67
|
return parsed;
|
|
67
68
|
}
|
|
68
69
|
}
|
|
@@ -89,7 +90,8 @@ export function loadSessionActorIndex() {
|
|
|
89
90
|
for (const f of files) {
|
|
90
91
|
try {
|
|
91
92
|
const parsed = JSON.parse(fs.readFileSync(path.join(sidecarDir(), f), 'utf8'));
|
|
92
|
-
if (parsed && typeof parsed === 'object' && typeof parsed.sessionId === 'string' &&
|
|
93
|
+
if (parsed && typeof parsed === 'object' && typeof parsed.sessionId === 'string' &&
|
|
94
|
+
(typeof parsed.actor === 'string' || typeof parsed.mode === 'string')) {
|
|
93
95
|
out.set(parsed.sessionId, parsed);
|
|
94
96
|
}
|
|
95
97
|
}
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { type IndexedToolCall } from './tool-calls.js';
|
|
|
12
12
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
13
13
|
* assert against the constant instead of hardcoding a number that every bump
|
|
14
14
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
15
|
-
export declare const SCHEMA_VERSION =
|
|
15
|
+
export declare const SCHEMA_VERSION = 32;
|
|
16
16
|
/**
|
|
17
17
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
18
18
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -30,6 +30,7 @@ export interface SessionRow {
|
|
|
30
30
|
routine_run_id: string | null;
|
|
31
31
|
version: string | null;
|
|
32
32
|
account: string | null;
|
|
33
|
+
mode: string | null;
|
|
33
34
|
timestamp: string;
|
|
34
35
|
last_activity: string | null;
|
|
35
36
|
project: string | null;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -25,7 +25,7 @@ const DB_PATH = getSessionsDbPath();
|
|
|
25
25
|
/** Current schema version; bumped when migrations are added. Exported so tests
|
|
26
26
|
* assert against the constant instead of hardcoding a number that every bump
|
|
27
27
|
* then has to chase (docs/05-sessions.md calls the constant the source of truth). */
|
|
28
|
-
export const SCHEMA_VERSION =
|
|
28
|
+
export const SCHEMA_VERSION = 32;
|
|
29
29
|
/**
|
|
30
30
|
* Bump to force `agents sessions backfill resources` to re-derive every
|
|
31
31
|
* session's skill/slash-command tallies on its next run (resource_scan_ledger
|
|
@@ -67,6 +67,7 @@ CREATE TABLE IF NOT EXISTS sessions (
|
|
|
67
67
|
routine_run_id TEXT,
|
|
68
68
|
version TEXT,
|
|
69
69
|
account TEXT,
|
|
70
|
+
mode TEXT,
|
|
70
71
|
timestamp TEXT NOT NULL,
|
|
71
72
|
last_activity TEXT,
|
|
72
73
|
project TEXT,
|
|
@@ -721,6 +722,14 @@ function migrateSchema(db, fromVersion) {
|
|
|
721
722
|
);
|
|
722
723
|
`);
|
|
723
724
|
}
|
|
725
|
+
if (fromVersion < 32) {
|
|
726
|
+
// v31 → v32: persist the effective managed launch mode so resume can
|
|
727
|
+
// restore the same permission boundary instead of falling back to a CLI
|
|
728
|
+
// default that may be more or less permissive.
|
|
729
|
+
const cols = new Set(db.prepare(`PRAGMA table_info(sessions)`).all().map((column) => column.name));
|
|
730
|
+
if (!cols.has('mode'))
|
|
731
|
+
db.exec(`ALTER TABLE sessions ADD COLUMN mode TEXT`);
|
|
732
|
+
}
|
|
724
733
|
}
|
|
725
734
|
/** Open (or return the cached) sessions database, applying migrations as needed. */
|
|
726
735
|
export function getDB() {
|
|
@@ -1115,7 +1124,7 @@ export function recordDirScans(entries) {
|
|
|
1115
1124
|
const upsertSessionStmt = (db) => db.prepare(`
|
|
1116
1125
|
INSERT INTO sessions (
|
|
1117
1126
|
id, short_id, agent, origin, routine_name, routine_run_id,
|
|
1118
|
-
version, account, timestamp, last_activity,
|
|
1127
|
+
version, account, mode, timestamp, last_activity,
|
|
1119
1128
|
project, cwd, git_branch, topic, label, message_count, token_count,
|
|
1120
1129
|
output_tokens, cost_usd, duration_ms, model, tool_call_count,
|
|
1121
1130
|
file_path, file_mtime_ms, file_size, scanned_at, is_team_origin,
|
|
@@ -1124,7 +1133,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1124
1133
|
actor, initiated_by, used_browser, used_computer
|
|
1125
1134
|
) VALUES (
|
|
1126
1135
|
@id, @short_id, @agent, @origin, @routine_name, @routine_run_id,
|
|
1127
|
-
@version, @account, @timestamp, @last_activity,
|
|
1136
|
+
@version, @account, @mode, @timestamp, @last_activity,
|
|
1128
1137
|
@project, @cwd, @git_branch, @topic, @label, @message_count, @token_count,
|
|
1129
1138
|
@output_tokens, @cost_usd, @duration_ms, @model, @tool_call_count,
|
|
1130
1139
|
@file_path, @file_mtime_ms, @file_size, @scanned_at, @is_team_origin,
|
|
@@ -1140,6 +1149,7 @@ const upsertSessionStmt = (db) => db.prepare(`
|
|
|
1140
1149
|
routine_run_id = excluded.routine_run_id,
|
|
1141
1150
|
version = excluded.version,
|
|
1142
1151
|
account = excluded.account,
|
|
1152
|
+
mode = COALESCE(excluded.mode, sessions.mode),
|
|
1143
1153
|
timestamp = excluded.timestamp,
|
|
1144
1154
|
last_activity = excluded.last_activity,
|
|
1145
1155
|
project = excluded.project,
|
|
@@ -1382,6 +1392,7 @@ export function upsertSession(meta, content, scan) {
|
|
|
1382
1392
|
routine_run_id: meta.routineRunId ?? null,
|
|
1383
1393
|
version: meta.version ?? null,
|
|
1384
1394
|
account: meta.account ?? null,
|
|
1395
|
+
mode: meta.mode ?? actorRec?.mode ?? null,
|
|
1385
1396
|
timestamp: meta.timestamp,
|
|
1386
1397
|
last_activity: resolveLastActivity(meta, scan),
|
|
1387
1398
|
project: meta.project ?? null,
|
|
@@ -1545,6 +1556,7 @@ export function upsertSessionsBatch(entries) {
|
|
|
1545
1556
|
routine_run_id: meta.routineRunId ?? null,
|
|
1546
1557
|
version: meta.version ?? null,
|
|
1547
1558
|
account: meta.account ?? null,
|
|
1559
|
+
mode: meta.mode ?? actorIndex.get(meta.id)?.mode ?? null,
|
|
1548
1560
|
timestamp: meta.timestamp,
|
|
1549
1561
|
last_activity: resolveLastActivity(meta, scan),
|
|
1550
1562
|
project: meta.project ?? null,
|
|
@@ -1768,6 +1780,7 @@ function rowToMeta(row) {
|
|
|
1768
1780
|
toolCallCount: row.tool_call_count ?? undefined,
|
|
1769
1781
|
version: row.version ?? undefined,
|
|
1770
1782
|
account: row.account ?? undefined,
|
|
1783
|
+
mode: isSessionRunMode(row.mode) ? row.mode : undefined,
|
|
1771
1784
|
topic: row.topic ?? undefined,
|
|
1772
1785
|
label: row.label ?? undefined,
|
|
1773
1786
|
isTeamOrigin: row.is_team_origin === 1,
|
|
@@ -1793,6 +1806,9 @@ function rowToMeta(row) {
|
|
|
1793
1806
|
usedComputer: row.used_computer === null ? undefined : row.used_computer === 1,
|
|
1794
1807
|
};
|
|
1795
1808
|
}
|
|
1809
|
+
function isSessionRunMode(value) {
|
|
1810
|
+
return value === 'plan' || value === 'edit' || value === 'auto' || value === 'skip';
|
|
1811
|
+
}
|
|
1796
1812
|
function parseJsonColumn(value) {
|
|
1797
1813
|
if (!value)
|
|
1798
1814
|
return undefined;
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
10
|
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid' | 'cursor';
|
|
11
|
+
/** Effective permissions mode used to launch a managed agent session. */
|
|
12
|
+
export type SessionRunMode = 'plan' | 'edit' | 'auto' | 'skip';
|
|
11
13
|
/** All agents with session discovery support, in display order. */
|
|
12
14
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
15
|
/**
|
|
@@ -151,6 +153,8 @@ export interface SessionMeta {
|
|
|
151
153
|
toolCallCount?: number;
|
|
152
154
|
version?: string;
|
|
153
155
|
account?: string;
|
|
156
|
+
/** Effective normalized launch mode captured by the SessionStart hook. */
|
|
157
|
+
mode?: SessionRunMode;
|
|
154
158
|
topic?: string;
|
|
155
159
|
/**
|
|
156
160
|
* The session's human-readable name — one field, several sources with a plain
|
|
@@ -30,7 +30,13 @@ const SETTINGS_MANIFEST = {
|
|
|
30
30
|
strategy: 'toml-merge',
|
|
31
31
|
stateKeys: ['notice', 'windows_wsl_setup_acknowledged'],
|
|
32
32
|
},
|
|
33
|
-
|
|
33
|
+
// `.codex/auth.json` is deliberately NOT carried forward. Copying it seeded
|
|
34
|
+
// every new Codex version with the current default's ChatGPT token, so two
|
|
35
|
+
// installed versions always reported the same account and could never sign
|
|
36
|
+
// into separate accounts. Claude omits its credential (`.claude.json`) for
|
|
37
|
+
// the same reason — a version home holds its own login, keeping accounts
|
|
38
|
+
// per-version. A fresh Codex version installs signed-out; run `codex login`
|
|
39
|
+
// (or `agents run codex --version <v>`) inside it to authenticate.
|
|
34
40
|
{ rel: '.codex/instructions.md', strategy: 'copy-if-absent' },
|
|
35
41
|
{ rel: '.codex/hooks.json', strategy: 'copy-if-absent' },
|
|
36
42
|
{ rel: '.codex/prompts', strategy: 'dir-entries' },
|
|
@@ -19,7 +19,12 @@ export declare function listPluginSkillNames(options?: {
|
|
|
19
19
|
agent?: AgentId;
|
|
20
20
|
plugins?: Set<string>;
|
|
21
21
|
}): string[];
|
|
22
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Find the trusted source for a hook by name.
|
|
24
|
+
* - File basename (`04-session-identity.sh`) or relative path
|
|
25
|
+
* (`session-starts/04-session-identity.sh`) → script file
|
|
26
|
+
* - Directory basename (`tests`) → directory bundle (fixtures-only etc.)
|
|
27
|
+
*/
|
|
23
28
|
export declare function resolveHookSource(name: string): string | null;
|
|
24
29
|
/** All trusted command-skill source roots, used to dedup name collisions for commands-as-skills writes. */
|
|
25
30
|
export declare function trustedSkillRoots(): string[];
|