@phnx-labs/agents-cli 1.22.9 → 1.22.11
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 +37 -4
- package/dist/bin/agents +0 -0
- package/dist/commands/beta.js +21 -9
- package/dist/commands/browser.js +2 -2
- package/dist/commands/events.d.ts +1 -1
- package/dist/commands/events.js +1 -1
- package/dist/commands/projects.d.ts +1 -2
- package/dist/commands/projects.js +39 -47
- package/dist/commands/secrets.js +38 -39
- package/dist/commands/ssh.js +4 -5
- package/dist/commands/workflows.js +1 -1
- package/dist/index.js +1 -1
- package/dist/lib/beta.d.ts +1 -1
- package/dist/lib/beta.js +1 -1
- package/dist/lib/event-stream.d.ts +1 -1
- package/dist/lib/event-stream.js +1 -1
- package/dist/lib/events.d.ts +1 -1
- package/dist/lib/events.js +3 -3
- package/dist/lib/feed-broadcast.d.ts +19 -5
- package/dist/lib/feed-broadcast.js +41 -8
- 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 +61 -0
- package/dist/lib/plugins.d.ts +14 -5
- package/dist/lib/plugins.js +29 -5
- package/dist/lib/project-probe.d.ts +1 -1
- package/dist/lib/project-probe.js +1 -1
- package/dist/lib/projects.d.ts +1 -1
- package/dist/lib/resources/types.d.ts +2 -1
- package/dist/lib/resources/workflows.d.ts +1 -1
- package/dist/lib/resources/workflows.js +22 -20
- 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/audit.js +1 -1
- package/dist/lib/secrets/bundles.d.ts +9 -0
- package/dist/lib/secrets/bundles.js +9 -23
- package/dist/lib/secrets/headless.d.ts +3 -15
- package/dist/lib/secrets/headless.js +5 -33
- package/dist/lib/secrets/index.d.ts +8 -0
- package/dist/lib/secrets/index.js +16 -36
- package/dist/lib/share/config.js +5 -7
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +1 -1
- package/dist/lib/types.d.ts +7 -1
- package/dist/lib/workflows.d.ts +19 -4
- package/dist/lib/workflows.js +81 -7
- package/package.json +1 -1
package/dist/lib/events.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Centralized event logging for agents-cli.
|
|
3
3
|
*
|
|
4
|
-
* Structured JSONL audit log at ~/.agents/events.jsonl with lossless numbered
|
|
4
|
+
* Structured JSONL audit log at ~/.agents/.history/events/events.jsonl with lossless numbered
|
|
5
5
|
* gzip rotation at 10 MB and rich metadata for debugging/auditing.
|
|
6
6
|
*
|
|
7
7
|
* Features:
|
|
@@ -17,7 +17,7 @@ import * as os from 'os';
|
|
|
17
17
|
import { createHash } from 'node:crypto';
|
|
18
18
|
import { gzipSync, gunzipSync } from 'node:zlib';
|
|
19
19
|
import { ensureLockTarget, withFileLock } from './fs-atomic.js';
|
|
20
|
-
import {
|
|
20
|
+
import { getHistoryDir } from './state.js';
|
|
21
21
|
import { stampProvenance, resetEventProvenanceForTest } from './event-provenance.js';
|
|
22
22
|
/** Lazy perf warehouse write — avoids a hard cycle at module load. */
|
|
23
23
|
function recordPerfTiming(payload) {
|
|
@@ -48,7 +48,7 @@ function recordPerfTiming(payload) {
|
|
|
48
48
|
// a test spawns, so fixture events can never land in the user's real log (#910).
|
|
49
49
|
let _eventsPath;
|
|
50
50
|
function eventsPath() {
|
|
51
|
-
return (_eventsPath ??= process.env.AGENTS_EVENTS_PATH || path.join(
|
|
51
|
+
return (_eventsPath ??= process.env.AGENTS_EVENTS_PATH || path.join(getHistoryDir(), 'events', 'events.jsonl'));
|
|
52
52
|
}
|
|
53
53
|
function eventsDir() {
|
|
54
54
|
return path.dirname(eventsPath());
|
|
@@ -48,8 +48,14 @@ export interface FeedBroadcastContext {
|
|
|
48
48
|
class?: string;
|
|
49
49
|
/** Block-only: cost-of-delay tag used by the urgency filter. */
|
|
50
50
|
cost?: string;
|
|
51
|
-
/** Block-only: the literal `agents focus <id>` command
|
|
51
|
+
/** Block-only: the literal `agents focus <id>` command (for a `{focus}` sink). */
|
|
52
52
|
focus?: string;
|
|
53
|
+
/** Block-only: the answer choices the operator can pick, in order. */
|
|
54
|
+
options?: string[];
|
|
55
|
+
/** Block-only: the fallback applied if nobody answers in time. */
|
|
56
|
+
safeDefault?: string;
|
|
57
|
+
/** Block-only: minutes before `safeDefault` applies. */
|
|
58
|
+
timeoutMinutes?: number;
|
|
53
59
|
}
|
|
54
60
|
/**
|
|
55
61
|
* Map an open block onto the broadcast context, so a block reaches the same sinks
|
|
@@ -69,9 +75,14 @@ export declare function blockBroadcastContext(block: {
|
|
|
69
75
|
host?: string;
|
|
70
76
|
questions?: Array<{
|
|
71
77
|
text?: string;
|
|
78
|
+
options?: Array<{
|
|
79
|
+
label?: string;
|
|
80
|
+
}>;
|
|
72
81
|
}>;
|
|
73
82
|
blockClass?: string;
|
|
74
83
|
costOfDelay?: string;
|
|
84
|
+
safeDefault?: string;
|
|
85
|
+
timeoutMinutes?: number;
|
|
75
86
|
ticket?: string;
|
|
76
87
|
pr?: string;
|
|
77
88
|
}, extras?: {
|
|
@@ -137,14 +148,17 @@ export declare function composeBroadcastFooter(ctx: FeedBroadcastContext): strin
|
|
|
137
148
|
* Title in a few words
|
|
138
149
|
*
|
|
139
150
|
* Body of what happened or the ask.
|
|
151
|
+
* Options: publish / wait (blocks with choices)
|
|
152
|
+
* Default in 15 min: wait (blocks with a safe default)
|
|
140
153
|
*
|
|
141
154
|
* Sent from grok/a02da0e2 on mac-mini
|
|
142
|
-
*
|
|
143
|
-
* https://… (optional attach URL)
|
|
155
|
+
* https://… (optional attach URL)
|
|
144
156
|
* ```
|
|
145
157
|
*
|
|
146
|
-
* Title first (scannable subject). Blank line. Body.
|
|
147
|
-
*
|
|
158
|
+
* Title first (scannable subject). Blank line. Body. Then the phone-actionable
|
|
159
|
+
* choices + default (a block that has stopped for the human), then footer
|
|
160
|
+
* provenance. No `agents focus <id>` line: a CLI command is unusable from a phone,
|
|
161
|
+
* so the safe default is the fallback and the message carries it. Prefer `{message}`
|
|
148
162
|
* over bare `{text}` in messaging sinks.
|
|
149
163
|
*/
|
|
150
164
|
export declare function composeBroadcastMessage(ctx: FeedBroadcastContext): string;
|
|
@@ -62,6 +62,12 @@ export function parseFeedPostLevel(raw) {
|
|
|
62
62
|
export function blockBroadcastContext(block, extras = {}) {
|
|
63
63
|
const ask = block.questions?.[0]?.text?.trim() || 'agent is blocked';
|
|
64
64
|
const links = [block.pr].filter((l) => !!l && /^https?:\/\//i.test(l));
|
|
65
|
+
// The answer choices + the safe default are what make the phone message
|
|
66
|
+
// actionable: the operator sees the options and what happens if they do not
|
|
67
|
+
// reply, instead of a `agents focus <id>` CLI command they cannot run from a phone.
|
|
68
|
+
const options = (block.questions?.[0]?.options ?? [])
|
|
69
|
+
.map((o) => o?.label?.trim())
|
|
70
|
+
.filter((l) => !!l);
|
|
65
71
|
// Prefer explicit title/body from the feed post; fall back to the ask as body.
|
|
66
72
|
const title = extras.title?.trim() || undefined;
|
|
67
73
|
const text = extras.body?.trim() || ask;
|
|
@@ -78,8 +84,12 @@ export function blockBroadcastContext(block, extras = {}) {
|
|
|
78
84
|
class: block.blockClass,
|
|
79
85
|
cost: block.costOfDelay,
|
|
80
86
|
// Short id: `agents focus` matches on a prefix, and a full uuid in a phone
|
|
81
|
-
// message is noise
|
|
87
|
+
// message is noise. Kept for a `{focus}` sink; the human message no longer
|
|
88
|
+
// shows it (a CLI command is unusable from a phone).
|
|
82
89
|
focus: `agents focus ${block.sessionId.slice(0, 8)}`,
|
|
90
|
+
...(options.length ? { options } : {}),
|
|
91
|
+
...(block.safeDefault ? { safeDefault: block.safeDefault } : {}),
|
|
92
|
+
...(block.timeoutMinutes ? { timeoutMinutes: block.timeoutMinutes } : {}),
|
|
83
93
|
...(links.length ? { links } : {}),
|
|
84
94
|
};
|
|
85
95
|
}
|
|
@@ -179,14 +189,17 @@ export function composeBroadcastFooter(ctx) {
|
|
|
179
189
|
* Title in a few words
|
|
180
190
|
*
|
|
181
191
|
* Body of what happened or the ask.
|
|
192
|
+
* Options: publish / wait (blocks with choices)
|
|
193
|
+
* Default in 15 min: wait (blocks with a safe default)
|
|
182
194
|
*
|
|
183
195
|
* Sent from grok/a02da0e2 on mac-mini
|
|
184
|
-
*
|
|
185
|
-
* https://… (optional attach URL)
|
|
196
|
+
* https://… (optional attach URL)
|
|
186
197
|
* ```
|
|
187
198
|
*
|
|
188
|
-
* Title first (scannable subject). Blank line. Body.
|
|
189
|
-
*
|
|
199
|
+
* Title first (scannable subject). Blank line. Body. Then the phone-actionable
|
|
200
|
+
* choices + default (a block that has stopped for the human), then footer
|
|
201
|
+
* provenance. No `agents focus <id>` line: a CLI command is unusable from a phone,
|
|
202
|
+
* so the safe default is the fallback and the message carries it. Prefer `{message}`
|
|
190
203
|
* over bare `{text}` in messaging sinks.
|
|
191
204
|
*/
|
|
192
205
|
export function composeBroadcastMessage(ctx) {
|
|
@@ -197,9 +210,21 @@ export function composeBroadcastMessage(ctx) {
|
|
|
197
210
|
const mid = title && body && title !== body ? body : undefined;
|
|
198
211
|
const footer = composeBroadcastFooter(ctx);
|
|
199
212
|
const link = ctx.links?.find((l) => /^https?:\/\//i.test(l));
|
|
200
|
-
//
|
|
201
|
-
//
|
|
202
|
-
|
|
213
|
+
// The action block: the one thing the operator can act on from a phone. Show the
|
|
214
|
+
// choices, then what happens if they do not answer. Deliberately NOT a CLI command
|
|
215
|
+
// (`agents focus <id>` is unusable from a phone) -- the safe default is the real
|
|
216
|
+
// fallback, so a block meant for a phone should always carry one.
|
|
217
|
+
const choices = ctx.options?.length
|
|
218
|
+
? `Options: ${ctx.options.map((o) => scrubOutboundDashes(o)).join(' / ')}`
|
|
219
|
+
: undefined;
|
|
220
|
+
const fallback = ctx.safeDefault
|
|
221
|
+
? (ctx.timeoutMinutes && ctx.timeoutMinutes > 0
|
|
222
|
+
? `Default in ${ctx.timeoutMinutes} min: ${scrubOutboundDashes(ctx.safeDefault)}`
|
|
223
|
+
: `Default: ${scrubOutboundDashes(ctx.safeDefault)}`)
|
|
224
|
+
: undefined;
|
|
225
|
+
const action = [choices, fallback].filter(Boolean).join('\n') || undefined;
|
|
226
|
+
// Link trail after the "Sent from" footer so the human sentence stays at the top.
|
|
227
|
+
const trail = [footer, link].filter(Boolean);
|
|
203
228
|
const parts = [];
|
|
204
229
|
if (head)
|
|
205
230
|
parts.push(head);
|
|
@@ -208,6 +233,12 @@ export function composeBroadcastMessage(ctx) {
|
|
|
208
233
|
parts.push('');
|
|
209
234
|
parts.push(mid);
|
|
210
235
|
}
|
|
236
|
+
if (action) {
|
|
237
|
+
// The choices/default hug the ask under a blank line so they read as the reply.
|
|
238
|
+
if (parts.length)
|
|
239
|
+
parts.push('');
|
|
240
|
+
parts.push(action);
|
|
241
|
+
}
|
|
211
242
|
if (trail.length) {
|
|
212
243
|
// Blank line before the footer block (iPhone "Sent from my iPhone" spacing).
|
|
213
244
|
if (parts.length)
|
|
@@ -233,6 +264,8 @@ function templateVars(ctx) {
|
|
|
233
264
|
class: ctx.class,
|
|
234
265
|
cost: ctx.cost,
|
|
235
266
|
focus: ctx.focus,
|
|
267
|
+
options: ctx.options?.length ? ctx.options.join(' / ') : undefined,
|
|
268
|
+
default: ctx.safeDefault,
|
|
236
269
|
};
|
|
237
270
|
}
|
|
238
271
|
/**
|
|
Binary file
|
|
Binary file
|
package/dist/lib/migrate.js
CHANGED
|
@@ -998,6 +998,7 @@ function migrateRuntimeToHistory() {
|
|
|
998
998
|
moveDirOnce(path.join(USER_DIR, '.backups'), path.join(HISTORY_DIR, 'backups'));
|
|
999
999
|
moveDirOnce(path.join(USER_DIR, 'routines', 'runs'), path.join(HISTORY_DIR, 'runs'));
|
|
1000
1000
|
moveDirOnce(path.join(USER_DIR, 'teams', 'agents'), path.join(HISTORY_DIR, 'teams', 'agents'));
|
|
1001
|
+
migrateEventLogsToHistory();
|
|
1001
1002
|
// Drop any empty leftover skeletons created mid-rename (e.g. `versions/<agent>/<v>/home/`
|
|
1002
1003
|
// recreated by a concurrent process). The real data is already under .history/.
|
|
1003
1004
|
rmEmptyDirTree(path.join(USER_DIR, 'versions'));
|
|
@@ -1013,6 +1014,66 @@ function migrateRuntimeToHistory() {
|
|
|
1013
1014
|
catch { /* best-effort */ }
|
|
1014
1015
|
}
|
|
1015
1016
|
}
|
|
1017
|
+
/** Move the operational event stream out of the git-backed user-repo root. */
|
|
1018
|
+
function migrateEventLogsToHistory() {
|
|
1019
|
+
const destination = path.join(HISTORY_DIR, 'events');
|
|
1020
|
+
let files = [];
|
|
1021
|
+
try {
|
|
1022
|
+
files = fs.readdirSync(USER_DIR).filter((file) => file === 'events.jsonl' || /^events\.\d+\.jsonl\.gz$/.test(file));
|
|
1023
|
+
}
|
|
1024
|
+
catch {
|
|
1025
|
+
return;
|
|
1026
|
+
}
|
|
1027
|
+
try {
|
|
1028
|
+
fs.mkdirSync(destination, { recursive: true, mode: 0o700 });
|
|
1029
|
+
}
|
|
1030
|
+
catch {
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const active = files.find((file) => file === 'events.jsonl');
|
|
1034
|
+
if (active) {
|
|
1035
|
+
const src = path.join(USER_DIR, active);
|
|
1036
|
+
const dest = path.join(destination, active);
|
|
1037
|
+
if (!fs.existsSync(dest)) {
|
|
1038
|
+
moveFileOnce(src, dest);
|
|
1039
|
+
}
|
|
1040
|
+
else {
|
|
1041
|
+
try {
|
|
1042
|
+
const lines = [fs.readFileSync(src, 'utf-8'), fs.readFileSync(dest, 'utf-8')]
|
|
1043
|
+
.flatMap((content) => content.split('\n').filter(Boolean))
|
|
1044
|
+
.map((line, index) => {
|
|
1045
|
+
try {
|
|
1046
|
+
const timestamp = Date.parse(JSON.parse(line).ts ?? '');
|
|
1047
|
+
return { line, index, timestamp: Number.isNaN(timestamp) ? Number.MAX_SAFE_INTEGER : timestamp };
|
|
1048
|
+
}
|
|
1049
|
+
catch {
|
|
1050
|
+
return { line, index, timestamp: Number.MAX_SAFE_INTEGER };
|
|
1051
|
+
}
|
|
1052
|
+
})
|
|
1053
|
+
.sort((a, b) => a.timestamp - b.timestamp || a.index - b.index)
|
|
1054
|
+
.map(({ line }) => line);
|
|
1055
|
+
atomicWriteFileSync(dest, `${lines.join('\n')}\n`, { mode: 0o600 });
|
|
1056
|
+
fs.unlinkSync(src);
|
|
1057
|
+
}
|
|
1058
|
+
catch { /* preserve the source for a later retry */ }
|
|
1059
|
+
}
|
|
1060
|
+
}
|
|
1061
|
+
const archives = files
|
|
1062
|
+
.map((file) => ({ file, match: file.match(/^events\.(\d+)\.jsonl\.gz$/) }))
|
|
1063
|
+
.filter((entry) => entry.match !== null)
|
|
1064
|
+
.sort((a, b) => Number(a.match[1]) - Number(b.match[1]));
|
|
1065
|
+
let nextArchive = fs.readdirSync(destination).reduce((max, file) => {
|
|
1066
|
+
const match = file.match(/^events\.(\d+)\.jsonl\.gz$/);
|
|
1067
|
+
return match ? Math.max(max, Number(match[1])) : max;
|
|
1068
|
+
}, 0) + 1;
|
|
1069
|
+
for (const archive of archives) {
|
|
1070
|
+
const preferred = path.join(destination, archive.file);
|
|
1071
|
+
const dest = fs.existsSync(preferred)
|
|
1072
|
+
? path.join(destination, `events.${nextArchive++}.jsonl.gz`)
|
|
1073
|
+
: preferred;
|
|
1074
|
+
moveFileOnce(path.join(USER_DIR, archive.file), dest);
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1016
1077
|
/**
|
|
1017
1078
|
* Restore plugins from the cache bucket back to the user-root.
|
|
1018
1079
|
*
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
4
|
* Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
|
|
5
|
-
* commands, agents, bin scripts, MCP servers, and settings under a
|
|
6
|
-
* manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
-
* skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
-
* module discovers plugins, validates their manifests, and syncs their
|
|
9
|
-
* contents into agent version homes.
|
|
5
|
+
* commands, agents, workflows, bin scripts, MCP servers, and settings under a
|
|
6
|
+
* single manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
+
* alongside skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
+
* This module discovers plugins, validates their manifests, and syncs their
|
|
9
|
+
* contents into agent version homes. Workflows under a plugin’s workflows/
|
|
10
|
+
* are resolved at run time by resolveWorkflowRef (Phase 5 packaging).
|
|
10
11
|
*/
|
|
11
12
|
import type { AgentId, DiscoveredPlugin, PluginManifest, MarketplaceSpec } from './types.js';
|
|
12
13
|
export interface PluginCapabilities {
|
|
@@ -95,6 +96,14 @@ export declare function discoverPluginHooks(pluginRoot: string): string[];
|
|
|
95
96
|
export declare function discoverPluginCommands(pluginRoot: string): string[];
|
|
96
97
|
/** Discover agent definition .md files inside a plugin's agents/ directory. */
|
|
97
98
|
export declare function discoverPluginAgentDefs(pluginRoot: string): string[];
|
|
99
|
+
/**
|
|
100
|
+
* Discover workflow directories inside a plugin's `workflows/` folder.
|
|
101
|
+
* A valid workflow is a directory containing WORKFLOW.md (same contract as
|
|
102
|
+
* project/user/system workflows). Phase 5: plugins package workflows as
|
|
103
|
+
* entrypoints so `agents run <name>` can resolve them without a separate
|
|
104
|
+
* install into ~/.agents/workflows/.
|
|
105
|
+
*/
|
|
106
|
+
export declare function discoverPluginWorkflows(pluginRoot: string): string[];
|
|
98
107
|
/** Discover executable files in a plugin's bin/ directory. */
|
|
99
108
|
export declare function discoverPluginBin(pluginRoot: string): string[];
|
|
100
109
|
/** Discover MCP server names from .mcp.json at the plugin root. */
|
package/dist/lib/plugins.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* Plugin discovery, validation, and syncing.
|
|
3
3
|
*
|
|
4
4
|
* Plugins are bundles in ~/.agents/plugins/ that package skills, hooks,
|
|
5
|
-
* commands, agents, bin scripts, MCP servers, and settings under a
|
|
6
|
-
* manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
-
* skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
-
* module discovers plugins, validates their manifests, and syncs their
|
|
9
|
-
* contents into agent version homes.
|
|
5
|
+
* commands, agents, workflows, bin scripts, MCP servers, and settings under a
|
|
6
|
+
* single manifest (plugin.json). They are user-authored resources, sitting
|
|
7
|
+
* alongside skills/, commands/, hooks/, etc. — git-tracked as source of truth.
|
|
8
|
+
* This module discovers plugins, validates their manifests, and syncs their
|
|
9
|
+
* contents into agent version homes. Workflows under a plugin’s workflows/
|
|
10
|
+
* are resolved at run time by resolveWorkflowRef (Phase 5 packaging).
|
|
10
11
|
*/
|
|
11
12
|
import * as fs from 'fs';
|
|
12
13
|
import * as path from 'path';
|
|
@@ -107,6 +108,7 @@ export function buildDiscoveredPlugin(pluginRoot, manifest, spec = { kind: 'user
|
|
|
107
108
|
scripts: discoverPluginScripts(pluginRoot),
|
|
108
109
|
commands: discoverPluginCommands(pluginRoot),
|
|
109
110
|
agentDefs: discoverPluginAgentDefs(pluginRoot),
|
|
111
|
+
workflows: discoverPluginWorkflows(pluginRoot),
|
|
110
112
|
memory: discoverPluginMemory(pluginRoot),
|
|
111
113
|
bin: discoverPluginBin(pluginRoot),
|
|
112
114
|
mcpServers: discoverPluginMcpServers(pluginRoot),
|
|
@@ -131,6 +133,7 @@ export function pluginResourceGroups(plugin) {
|
|
|
131
133
|
{ label: 'skills', items: plugin.skills.map((s) => `/${plugin.name}:${s}`) },
|
|
132
134
|
{ label: 'commands', items: plugin.commands.map((c) => `/${plugin.name}:${c}`) },
|
|
133
135
|
{ label: 'subagents', items: plugin.agentDefs },
|
|
136
|
+
{ label: 'workflows', items: plugin.workflows },
|
|
134
137
|
{ label: 'hooks', items: plugin.hooks },
|
|
135
138
|
{ label: 'memory', items: plugin.memory },
|
|
136
139
|
{ label: 'mcp', items: plugin.mcpServers },
|
|
@@ -334,6 +337,27 @@ export function discoverPluginAgentDefs(pluginRoot) {
|
|
|
334
337
|
.filter(f => f.endsWith('.md') && !f.startsWith('.'))
|
|
335
338
|
.map(f => f.slice(0, -3));
|
|
336
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Discover workflow directories inside a plugin's `workflows/` folder.
|
|
342
|
+
* A valid workflow is a directory containing WORKFLOW.md (same contract as
|
|
343
|
+
* project/user/system workflows). Phase 5: plugins package workflows as
|
|
344
|
+
* entrypoints so `agents run <name>` can resolve them without a separate
|
|
345
|
+
* install into ~/.agents/workflows/.
|
|
346
|
+
*/
|
|
347
|
+
export function discoverPluginWorkflows(pluginRoot) {
|
|
348
|
+
const workflowsDir = path.join(pluginRoot, 'workflows');
|
|
349
|
+
if (!fs.existsSync(workflowsDir))
|
|
350
|
+
return [];
|
|
351
|
+
try {
|
|
352
|
+
return fs.readdirSync(workflowsDir, { withFileTypes: true })
|
|
353
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith('.') &&
|
|
354
|
+
fs.existsSync(path.join(workflowsDir, e.name, 'WORKFLOW.md')))
|
|
355
|
+
.map((e) => e.name);
|
|
356
|
+
}
|
|
357
|
+
catch {
|
|
358
|
+
return [];
|
|
359
|
+
}
|
|
360
|
+
}
|
|
337
361
|
/** Discover executable files in a plugin's bin/ directory. */
|
|
338
362
|
export function discoverPluginBin(pluginRoot) {
|
|
339
363
|
const binDir = path.join(pluginRoot, 'bin');
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project workspace probing — the drift signal behind `projects status
|
|
2
|
+
* Project workspace probing — the drift signal behind `projects status`.
|
|
3
3
|
*
|
|
4
4
|
* Projects are natively multi-device: the same definition (home-relative paths)
|
|
5
5
|
* re-roots on every fleet machine, and the question is whether the project's
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Project workspace probing — the drift signal behind `projects status
|
|
2
|
+
* Project workspace probing — the drift signal behind `projects status`.
|
|
3
3
|
*
|
|
4
4
|
* Projects are natively multi-device: the same definition (home-relative paths)
|
|
5
5
|
* re-roots on every fleet machine, and the question is whether the project's
|
package/dist/lib/projects.d.ts
CHANGED
|
@@ -36,7 +36,7 @@ export interface ProjectRepo {
|
|
|
36
36
|
/**
|
|
37
37
|
* Optional home-relative local checkout of this repo. The def's `root` only
|
|
38
38
|
* knows the primary repo on disk; `path` opts an additional repo into
|
|
39
|
-
* workspace probing (`projects status
|
|
39
|
+
* workspace probing (`projects status`).
|
|
40
40
|
*/
|
|
41
41
|
path?: string;
|
|
42
42
|
}
|
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
* - Override on name conflict: Higher layer wins (project > user > system)
|
|
7
7
|
*/
|
|
8
8
|
export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid' | 'hermes' | 'pi';
|
|
9
|
-
|
|
9
|
+
/** Resource origin. Precedence (highest first): project > user > plugin > system. */
|
|
10
|
+
export type Layer = 'system' | 'user' | 'project' | 'plugin';
|
|
10
11
|
export type ResourceKind = 'command' | 'hook' | 'skill' | 'rule' | 'mcp' | 'permission' | 'subagent' | 'workflow' | 'memory';
|
|
11
12
|
/** A resolved resource with its origin layer. */
|
|
12
13
|
export interface ResolvedItem<T> {
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
5
|
* They optionally contain subagents/, skills/, and plugins/ subdirectories.
|
|
6
|
-
* Resolution order: project > user > system.
|
|
6
|
+
* Resolution order (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
7
7
|
*/
|
|
8
8
|
import type { AgentId, ResolvedItem, ResourceHandler } from './types.js';
|
|
9
9
|
export interface WorkflowItem {
|
|
@@ -3,12 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Workflows are directory bundles with a WORKFLOW.md containing YAML frontmatter.
|
|
5
5
|
* They optionally contain subagents/, skills/, and plugins/ subdirectories.
|
|
6
|
-
* Resolution order: project > user > system.
|
|
6
|
+
* Resolution order (docs/07-entrypoints): project > user > plugin > extra > system.
|
|
7
7
|
*/
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { getProjectAgentsDir, getUserWorkflowsDir, getSystemWorkflowsDir, getEnabledExtraRepos, } from '../state.js';
|
|
11
|
-
import { parseWorkflowFrontmatter, countWorkflowSubagents } from '../workflows.js';
|
|
11
|
+
import { parseWorkflowFrontmatter, countWorkflowSubagents, listPluginWorkflowDirs, isBareWorkflowName, } from '../workflows.js';
|
|
12
12
|
function getLayerDirs(cwd) {
|
|
13
13
|
const projectDir = getProjectAgentsDir(cwd);
|
|
14
14
|
const extraRepos = getEnabledExtraRepos();
|
|
@@ -32,20 +32,27 @@ function listWorkflowsInDir(dir) {
|
|
|
32
32
|
return [];
|
|
33
33
|
}
|
|
34
34
|
}
|
|
35
|
+
/** Precedence-ordered (dir, layer) pairs for name lookup / listing. */
|
|
36
|
+
function orderedWorkflowSearchDirs(cwd) {
|
|
37
|
+
const dirs = getLayerDirs(cwd);
|
|
38
|
+
const out = [];
|
|
39
|
+
if (dirs.project)
|
|
40
|
+
out.push({ dir: dirs.project, layer: 'project' });
|
|
41
|
+
out.push({ dir: dirs.user, layer: 'user' });
|
|
42
|
+
for (const pluginDir of listPluginWorkflowDirs(cwd ?? process.cwd())) {
|
|
43
|
+
out.push({ dir: pluginDir, layer: 'plugin' });
|
|
44
|
+
}
|
|
45
|
+
for (const extraDir of dirs.extra)
|
|
46
|
+
out.push({ dir: extraDir, layer: 'system' });
|
|
47
|
+
out.push({ dir: dirs.system, layer: 'system' });
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
35
50
|
class WorkflowsHandlerImpl {
|
|
36
51
|
kind = 'workflow';
|
|
37
52
|
listAll(_agent, cwd) {
|
|
38
|
-
const dirs = getLayerDirs(cwd);
|
|
39
53
|
const seen = new Set();
|
|
40
54
|
const results = [];
|
|
41
|
-
const
|
|
42
|
-
if (dirs.project)
|
|
43
|
-
layerDirs.push({ dir: dirs.project, layer: 'project' });
|
|
44
|
-
layerDirs.push({ dir: dirs.user, layer: 'user' });
|
|
45
|
-
layerDirs.push({ dir: dirs.system, layer: 'system' });
|
|
46
|
-
for (const extraDir of dirs.extra)
|
|
47
|
-
layerDirs.push({ dir: extraDir, layer: 'system' });
|
|
48
|
-
for (const { dir, layer } of layerDirs) {
|
|
55
|
+
for (const { dir, layer } of orderedWorkflowSearchDirs(cwd)) {
|
|
49
56
|
for (const { name, path: workflowPath } of listWorkflowsInDir(dir)) {
|
|
50
57
|
if (seen.has(name))
|
|
51
58
|
continue;
|
|
@@ -69,15 +76,10 @@ class WorkflowsHandlerImpl {
|
|
|
69
76
|
return results.sort((a, b) => a.name.localeCompare(b.name));
|
|
70
77
|
}
|
|
71
78
|
resolve(_agent, name, cwd) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
searchDirs.push({ dir: dirs.user, layer: 'user' });
|
|
77
|
-
searchDirs.push({ dir: dirs.system, layer: 'system' });
|
|
78
|
-
for (const extraDir of dirs.extra)
|
|
79
|
-
searchDirs.push({ dir: extraDir, layer: 'system' });
|
|
80
|
-
for (const { dir, layer } of searchDirs) {
|
|
79
|
+
// Same bare-name gate as resolveWorkflowRef — never path-join traversal refs.
|
|
80
|
+
if (!isBareWorkflowName(name))
|
|
81
|
+
return null;
|
|
82
|
+
for (const { dir, layer } of orderedWorkflowSearchDirs(cwd)) {
|
|
81
83
|
const workflowPath = path.join(dir, name);
|
|
82
84
|
const fm = parseWorkflowFrontmatter(workflowPath);
|
|
83
85
|
if (fm) {
|
|
Binary file
|
|
Binary file
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* This is the ONE write path for secret events. Every path that creates,
|
|
5
5
|
* imports, exports, views, reads a VALUE from, or unlocks a bundle funnels its
|
|
6
6
|
* audit through here, so the operational event stream — `agents events`, backed
|
|
7
|
-
* by the append-only `~/.agents/events.jsonl` audit log — carries a uniform,
|
|
7
|
+
* by the append-only `~/.agents/.history/events/events.jsonl` audit log — carries a uniform,
|
|
8
8
|
* value-free provenance record: bundle, key NAMES, the resolving agent/harness
|
|
9
9
|
* identity, operation, source, status. The ts / host / session / caller fields
|
|
10
10
|
* are filled in by `emit()` itself. The secret VALUE is never part of the
|
|
@@ -23,6 +23,8 @@
|
|
|
23
23
|
import { type BundleValue, type SecretRef } from './index.js';
|
|
24
24
|
/** Which store carries a bundle's items. */
|
|
25
25
|
export type SecretsBackend = 'keychain' | 'file' | 'vault';
|
|
26
|
+
/** Disable the broker-only guard for in-memory keychain tests. */
|
|
27
|
+
export declare function setKeychainAgentOnlyBypassForTest(bypass: boolean): void;
|
|
26
28
|
/**
|
|
27
29
|
* Discover a bundle's backend by location: a file-backed bundle's metadata
|
|
28
30
|
* item exists in the encrypted-file store. This is a plain file-existence
|
|
@@ -184,6 +186,13 @@ export declare function deleteBundle(name: string): boolean;
|
|
|
184
186
|
* extra keychain read is issued. Exported for tests. Returns the count healed.
|
|
185
187
|
*/
|
|
186
188
|
export declare function healKeychainBundleMetadata(metaJsonByName: Map<string, string>): number;
|
|
189
|
+
/**
|
|
190
|
+
* One-time driver around healKeychainBundleMetadata (RUSH-1759). macOS + real
|
|
191
|
+
* keychain only — libsecret/CredMan have no biometry ACL to shed, and a test
|
|
192
|
+
* backend has no real keychain — and gated by a sentinel so it runs at most
|
|
193
|
+
* once. Best-effort: a heal failure never breaks bundle listing.
|
|
194
|
+
*/
|
|
195
|
+
export declare function healKeychainBundleMetadataAclOnce(metaJsonByName: Map<string, string>): void;
|
|
187
196
|
export declare function listBundles(): SecretsBundle[];
|
|
188
197
|
export interface BundleEntryInfo {
|
|
189
198
|
key: string;
|
|
@@ -86,6 +86,11 @@ const vaultStore = {
|
|
|
86
86
|
delete: vaultDeleteItem,
|
|
87
87
|
list: vaultListItems,
|
|
88
88
|
};
|
|
89
|
+
let keychainAgentOnlyBypassForTest = false;
|
|
90
|
+
/** Disable the broker-only guard for in-memory keychain tests. */
|
|
91
|
+
export function setKeychainAgentOnlyBypassForTest(bypass) {
|
|
92
|
+
keychainAgentOnlyBypassForTest = bypass;
|
|
93
|
+
}
|
|
89
94
|
function itemStore(backend) {
|
|
90
95
|
if (backend === 'file')
|
|
91
96
|
return fileItemStore;
|
|
@@ -606,7 +611,7 @@ export function healKeychainBundleMetadata(metaJsonByName) {
|
|
|
606
611
|
* backend has no real keychain — and gated by a sentinel so it runs at most
|
|
607
612
|
* once. Best-effort: a heal failure never breaks bundle listing.
|
|
608
613
|
*/
|
|
609
|
-
function healKeychainBundleMetadataAclOnce(metaJsonByName) {
|
|
614
|
+
export function healKeychainBundleMetadataAclOnce(metaJsonByName) {
|
|
610
615
|
if (metaJsonByName.size === 0)
|
|
611
616
|
return;
|
|
612
617
|
if (process.platform !== 'darwin')
|
|
@@ -684,7 +689,6 @@ export function listBundles() {
|
|
|
684
689
|
// still prompt once; it heals on the next interactive scan.)
|
|
685
690
|
const fetched = getKeychainTokens(keychainServices, { silentNoAcl: true });
|
|
686
691
|
const keychainBundles = [];
|
|
687
|
-
const metaJsonByName = new Map();
|
|
688
692
|
for (const service of keychainServices) {
|
|
689
693
|
const json = fetched.get(service);
|
|
690
694
|
if (json === undefined)
|
|
@@ -695,7 +699,6 @@ export function listBundles() {
|
|
|
695
699
|
const bundle = parseBundleMeta(nameHint, json, 'keychain');
|
|
696
700
|
if (bundle) {
|
|
697
701
|
keychainBundles.push(bundle);
|
|
698
|
-
metaJsonByName.set(bundle.name, json);
|
|
699
702
|
}
|
|
700
703
|
}
|
|
701
704
|
for (const bundle of keychainBundles)
|
|
@@ -707,13 +710,6 @@ export function listBundles() {
|
|
|
707
710
|
if (useAgent && keychainBundles.length > 0) {
|
|
708
711
|
agentAutoLoadMetaSync(nameSetHash, keychainBundles, secretsHoldMs());
|
|
709
712
|
}
|
|
710
|
-
// One-time RUSH-1759 heal: bundles written before the metadata-no-ACL
|
|
711
|
-
// change carry an ACL'd metadata item, so this fresh read (a broker miss)
|
|
712
|
-
// popped Touch ID just to enumerate them. Re-home each metadata item
|
|
713
|
-
// no-ACL now — reusing the JSON we just read, so the heal adds no extra
|
|
714
|
-
// prompt — and every later enumeration is silent. Runs at most once (a
|
|
715
|
-
// sentinel under the regenerable helpers dir).
|
|
716
|
-
healKeychainBundleMetadataAclOnce(metaJsonByName);
|
|
717
713
|
}
|
|
718
714
|
}
|
|
719
715
|
}
|
|
@@ -1099,24 +1095,14 @@ export function readAndResolveBundleEnv(name, opts = {}) {
|
|
|
1099
1095
|
return filtered;
|
|
1100
1096
|
}
|
|
1101
1097
|
}
|
|
1102
|
-
// Never/no-ACL bundles remain prompt-free regardless.
|
|
1103
|
-
//
|
|
1104
|
-
// Explicit opt-in ONLY — a deliberate NARROWING of the agent-triggered approval
|
|
1105
|
-
// added in RUSH-2032 (b99796f8 removed this throw so an agent could raise the
|
|
1106
|
-
// sheet itself; 4eeada68 generalized the daemon rule into `!interactiveUnlock`).
|
|
1107
|
-
// That default — true whenever an agent name was present — was the spec, not a
|
|
1108
|
-
// bug. It is unwanted: each keychain read runs in its own helper process, so the
|
|
1109
|
-
// biometric assertion never reuses and one agent launch meant one sheet per
|
|
1110
|
-
// bundle. `agentOnly` decides alone now; a human in a plain shell carries no
|
|
1111
|
-
// AGENTS_RUNTIME, so isHeadlessSecretsContext() is false, agentOnly is false, the
|
|
1112
|
-
// guard never fires, and they still get their prompt. No caller passes this flag;
|
|
1113
|
-
// it remains the seam for a future unlock path that wants the sheet on purpose.
|
|
1098
|
+
// Never/no-ACL bundles remain prompt-free regardless. Every ordinary caller
|
|
1099
|
+
// sets agentOnly; only the unlock handler opts into interactive authentication.
|
|
1114
1100
|
const interactiveUnlock = opts.interactiveUnlock ?? false;
|
|
1115
1101
|
// A `never`-policy bundle's items carry no biometry ACL, so once the policy
|
|
1116
1102
|
// check below proves that, the batch read is silent even in a headless
|
|
1117
1103
|
// context — attest it to the raw-read storm guard via `silentNoAcl`.
|
|
1118
1104
|
let verifiedNoAclBundle = false;
|
|
1119
|
-
if (opts.agentOnly && backend === 'keychain' && !interactiveUnlock) {
|
|
1105
|
+
if (opts.agentOnly && backend === 'keychain' && !interactiveUnlock && !keychainAgentOnlyBypassForTest) {
|
|
1120
1106
|
try {
|
|
1121
1107
|
verifiedNoAclBundle = bundlePolicy(readBundle(name)) === 'never';
|
|
1122
1108
|
}
|
|
@@ -5,22 +5,13 @@
|
|
|
5
5
|
* importing bundles.ts (which already imports index.ts).
|
|
6
6
|
*/
|
|
7
7
|
/**
|
|
8
|
-
* True when the current process
|
|
9
|
-
* must NEVER raise a Keychain biometry prompt on the
|
|
10
|
-
* a prompt nobody is watching. Two signals, either sufficient:
|
|
8
|
+
* True when the current process was structurally launched by an agent runtime
|
|
9
|
+
* that must NEVER raise a Keychain biometry prompt on the user's screen:
|
|
11
10
|
* - `AGENTS_RUNTIME` is `headless`, `teams`, or `terminal` — i.e. ANY agent
|
|
12
11
|
* launch, interactive included, and inherited by everything spawned beneath
|
|
13
12
|
* one (set on the child env by `agents run --headless`, scheduled routines,
|
|
14
13
|
* teammates, and interactive runs — see exec.ts:430, runner.ts,
|
|
15
14
|
* teams/agents.ts).
|
|
16
|
-
* - neither stdin nor stdout is a TTY (a detached/backgrounded task whose
|
|
17
|
-
* stdio is redirected to a log — e.g. a release script run in the
|
|
18
|
-
* background as `( ... ) >log 2>&1 </dev/null`).
|
|
19
|
-
* `AGENTS_SECRETS_NO_PROMPT=1` forces headless-safe; `=0` force-allows a prompt
|
|
20
|
-
* even in a non-TTY context. An `eval "$(agents secrets export X)"` typed in a
|
|
21
|
-
* PLAIN shell has no AGENTS_RUNTIME, so it is not classified headless and still
|
|
22
|
-
* prompts. Run beneath an agent it inherits AGENTS_RUNTIME and resolves
|
|
23
|
-
* broker-only — the agent, not the human, is the caller there.
|
|
24
15
|
*
|
|
25
16
|
* Only **macOS keychain** reads pop an interactive Touch ID sheet — the secrets
|
|
26
17
|
* broker itself is a no-op off darwin (see agent.ts), and libsecret (Linux) /
|
|
@@ -33,7 +24,4 @@
|
|
|
33
24
|
* fast with an actionable error instead of hijacking Touch ID. This generalizes
|
|
34
25
|
* the per-caller broker-only pattern used across the headless secrets readers.
|
|
35
26
|
*/
|
|
36
|
-
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform
|
|
37
|
-
stdin?: boolean;
|
|
38
|
-
stdout?: boolean;
|
|
39
|
-
}): boolean;
|
|
27
|
+
export declare function isHeadlessSecretsContext(env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform): boolean;
|