@phnx-labs/agents-cli 1.18.6 → 1.19.0
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 +13 -2
- package/README.md +22 -20
- package/dist/commands/browser.js +25 -2
- package/dist/commands/cloud.js +3 -3
- package/dist/commands/computer.d.ts +6 -0
- package/dist/commands/computer.js +477 -0
- package/dist/commands/doctor.js +19 -17
- package/dist/commands/exec.js +37 -59
- package/dist/commands/factory.js +12 -5
- package/dist/commands/import.js +6 -1
- package/dist/commands/mcp.js +9 -4
- package/dist/commands/packages.d.ts +3 -0
- package/dist/commands/packages.js +20 -12
- package/dist/commands/permissions.d.ts +2 -0
- package/dist/commands/permissions.js +20 -1
- package/dist/commands/plugins.d.ts +2 -0
- package/dist/commands/plugins.js +23 -4
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/pty.js +126 -112
- package/dist/commands/pull.js +29 -25
- package/dist/commands/repo.js +24 -26
- package/dist/commands/routines.js +29 -26
- package/dist/commands/secrets.js +66 -73
- package/dist/commands/sessions-tail.js +21 -22
- package/dist/commands/sessions.js +36 -68
- package/dist/commands/setup.js +20 -24
- package/dist/commands/teams.js +30 -39
- package/dist/commands/versions.js +60 -68
- package/dist/commands/worktree.d.ts +20 -0
- package/dist/commands/worktree.js +242 -0
- package/dist/computer.d.ts +2 -0
- package/dist/computer.js +7 -0
- package/dist/index.js +70 -26
- package/dist/lib/agents.d.ts +4 -1
- package/dist/lib/agents.js +23 -5
- package/dist/lib/browser/cdp.d.ts +15 -1
- package/dist/lib/browser/cdp.js +77 -8
- package/dist/lib/browser/chrome.js +17 -24
- package/dist/lib/browser/drivers/ssh.d.ts +1 -0
- package/dist/lib/browser/drivers/ssh.js +20 -8
- package/dist/lib/browser/ipc.js +38 -5
- package/dist/lib/browser/profiles.js +34 -2
- package/dist/lib/browser/runtime-state.d.ts +1 -2
- package/dist/lib/browser/runtime-state.js +11 -3
- package/dist/lib/browser/service.d.ts +5 -0
- package/dist/lib/browser/service.js +32 -4
- package/dist/lib/browser/types.d.ts +1 -1
- package/dist/lib/browser/upload.d.ts +2 -0
- package/dist/lib/browser/upload.js +34 -0
- package/dist/lib/cloud/rush.d.ts +2 -1
- package/dist/lib/cloud/rush.js +28 -9
- package/dist/lib/computer-rpc.d.ts +24 -0
- package/dist/lib/computer-rpc.js +263 -0
- package/dist/lib/daemon.js +7 -7
- package/dist/lib/exec.d.ts +2 -1
- package/dist/lib/exec.js +3 -2
- package/dist/lib/fs-atomic.d.ts +18 -0
- package/dist/lib/fs-atomic.js +76 -0
- package/dist/lib/git.js +2 -4
- package/dist/lib/help.d.ts +15 -0
- package/dist/lib/help.js +41 -0
- package/dist/lib/hooks/match.d.ts +1 -0
- package/dist/lib/hooks/match.js +57 -12
- package/dist/lib/hooks.d.ts +1 -0
- package/dist/lib/hooks.js +27 -10
- package/dist/lib/import.d.ts +1 -0
- package/dist/lib/import.js +7 -0
- package/dist/lib/manifest.js +27 -1
- package/dist/lib/mcp.d.ts +14 -0
- package/dist/lib/mcp.js +79 -14
- package/dist/lib/migrate.js +3 -3
- package/dist/lib/models.js +3 -1
- package/dist/lib/permissions.d.ts +5 -0
- package/dist/lib/permissions.js +35 -0
- package/dist/lib/plugin-marketplace.d.ts +3 -1
- package/dist/lib/plugin-marketplace.js +36 -1
- package/dist/lib/plugins.d.ts +19 -1
- package/dist/lib/plugins.js +99 -8
- package/dist/lib/redact.d.ts +4 -0
- package/dist/lib/redact.js +18 -0
- package/dist/lib/registry.d.ts +2 -0
- package/dist/lib/registry.js +15 -0
- package/dist/lib/sandbox.js +15 -5
- package/dist/lib/secrets/bundles.d.ts +7 -12
- package/dist/lib/secrets/bundles.js +45 -29
- package/dist/lib/secrets/index.js +4 -4
- package/dist/lib/session/cloud.d.ts +2 -0
- package/dist/lib/session/cloud.js +34 -6
- package/dist/lib/session/parse.js +7 -2
- package/dist/lib/session/render.d.ts +4 -1
- package/dist/lib/session/render.js +81 -35
- package/dist/lib/shims.d.ts +5 -2
- package/dist/lib/shims.js +29 -7
- package/dist/lib/state.d.ts +5 -5
- package/dist/lib/state.js +43 -13
- package/dist/lib/teams/agents.js +1 -1
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/types.js +0 -2
- package/dist/lib/versions.js +65 -40
- package/dist/lib/workflows.d.ts +7 -0
- package/dist/lib/workflows.js +42 -1
- package/npm-shrinkwrap.json +3256 -0
- package/package.json +32 -26
- package/scripts/postinstall.js +8 -2
package/dist/lib/permissions.js
CHANGED
|
@@ -19,6 +19,41 @@ const HOME = os.homedir();
|
|
|
19
19
|
export const PERMISSIONS_CAPABLE_AGENTS = ['claude', 'codex', 'opencode'];
|
|
20
20
|
/** Filename used for Codex Starlark deny-rules generated from permission groups. */
|
|
21
21
|
export const CODEX_RULES_FILENAME = 'agents-deny.rules';
|
|
22
|
+
export function containsBroadGrants(rules) {
|
|
23
|
+
const broad = [];
|
|
24
|
+
const reasons = new Set();
|
|
25
|
+
for (const perm of rules.allow) {
|
|
26
|
+
if (BLANKET_BASH_FORMS.has(perm)) {
|
|
27
|
+
broad.push(perm);
|
|
28
|
+
reasons.add('allows any bash command and maps Codex to approval_policy=never');
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
const parsed = parseCanonicalPattern(perm);
|
|
32
|
+
if (!parsed)
|
|
33
|
+
continue;
|
|
34
|
+
if (parsed.tool === 'bash' && (parsed.pattern === '*' || parsed.pattern === '**')) {
|
|
35
|
+
broad.push(perm);
|
|
36
|
+
reasons.add('allows any bash command and maps Codex to approval_policy=never');
|
|
37
|
+
}
|
|
38
|
+
else if (parsed.tool === 'bash' && parsed.pattern.startsWith('/') && parsed.pattern.includes('*')) {
|
|
39
|
+
broad.push(perm);
|
|
40
|
+
reasons.add('allows wildcard absolute bash paths');
|
|
41
|
+
}
|
|
42
|
+
else if ((parsed.tool === 'write' || parsed.tool === 'read') && (parsed.pattern === '*' || parsed.pattern === '**')) {
|
|
43
|
+
broad.push(perm);
|
|
44
|
+
reasons.add(`allows broad ${parsed.tool} filesystem access`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
for (const dir of rules.additionalDirectories || []) {
|
|
48
|
+
if (dir.startsWith('/') || dir.startsWith('~/') || dir === '~' || dir.split(/[\\/]/).includes('..')) {
|
|
49
|
+
broad.push(`additionalDirectories:${dir}`);
|
|
50
|
+
reasons.add('adds a broad or parent-traversing sandbox directory');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (broad.length === 0)
|
|
54
|
+
return null;
|
|
55
|
+
return { broad, reason: Array.from(reasons).join('; ') };
|
|
56
|
+
}
|
|
22
57
|
/**
|
|
23
58
|
* Convert canonical deny rules to Codex Starlark .rules format.
|
|
24
59
|
* E.g. "Bash(git reset:*)" -> prefix_rule(pattern=["git", "reset"], decision="forbidden")
|
|
@@ -68,7 +68,9 @@ export declare function unregisterMarketplace(agent: AgentId, versionHome: strin
|
|
|
68
68
|
* enabledPlugins["<plugin>@agents-cli"]: true. Reads, mutates, writes —
|
|
69
69
|
* preserving every other key.
|
|
70
70
|
*/
|
|
71
|
-
export declare function enablePluginInSettings(pluginName: string, agent: AgentId, versionHome: string
|
|
71
|
+
export declare function enablePluginInSettings(pluginName: string, agent: AgentId, versionHome: string, options?: {
|
|
72
|
+
allowExecSurfaces?: boolean;
|
|
73
|
+
}): void;
|
|
72
74
|
/**
|
|
73
75
|
* Remove the enabledPlugins key for this plugin. Inverse of enablePluginInSettings.
|
|
74
76
|
*/
|
|
@@ -151,7 +151,10 @@ export function unregisterMarketplace(agent, versionHome) {
|
|
|
151
151
|
* enabledPlugins["<plugin>@agents-cli"]: true. Reads, mutates, writes —
|
|
152
152
|
* preserving every other key.
|
|
153
153
|
*/
|
|
154
|
-
export function enablePluginInSettings(pluginName, agent, versionHome) {
|
|
154
|
+
export function enablePluginInSettings(pluginName, agent, versionHome, options = {}) {
|
|
155
|
+
if (!options.allowExecSurfaces && marketplacePluginHasExecSurfaces(pluginName, agent, versionHome)) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
155
158
|
const sPath = settingsPath(agent, versionHome);
|
|
156
159
|
let settings = {};
|
|
157
160
|
if (fs.existsSync(sPath)) {
|
|
@@ -173,6 +176,38 @@ export function enablePluginInSettings(pluginName, agent, versionHome) {
|
|
|
173
176
|
fs.mkdirSync(path.dirname(sPath), { recursive: true });
|
|
174
177
|
fs.writeFileSync(sPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
|
|
175
178
|
}
|
|
179
|
+
function marketplacePluginHasExecSurfaces(pluginName, agent, versionHome) {
|
|
180
|
+
const root = path.join(marketplaceRoot(agent, versionHome), 'plugins', pluginName);
|
|
181
|
+
if (fs.existsSync(path.join(root, '.mcp.json')))
|
|
182
|
+
return true;
|
|
183
|
+
for (const dir of ['bin', 'scripts', 'permissions']) {
|
|
184
|
+
if (fs.existsSync(path.join(root, dir)))
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
const hooksFile = path.join(root, 'hooks', 'hooks.json');
|
|
188
|
+
if (fs.existsSync(hooksFile))
|
|
189
|
+
return true;
|
|
190
|
+
const hooksDir = path.join(root, 'hooks');
|
|
191
|
+
if (fs.existsSync(hooksDir)) {
|
|
192
|
+
try {
|
|
193
|
+
if (fs.readdirSync(hooksDir).some((entry) => !entry.startsWith('.')))
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
catch {
|
|
197
|
+
return true;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const settingsFile = path.join(root, 'settings.json');
|
|
201
|
+
if (!fs.existsSync(settingsFile))
|
|
202
|
+
return false;
|
|
203
|
+
try {
|
|
204
|
+
const settings = JSON.parse(fs.readFileSync(settingsFile, 'utf-8'));
|
|
205
|
+
return Object.keys(settings).some((key) => key !== 'permissions') || 'permissions' in settings;
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return true;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
176
211
|
/**
|
|
177
212
|
* Remove the enabledPlugins key for this plugin. Inverse of enablePluginInSettings.
|
|
178
213
|
*/
|
package/dist/lib/plugins.d.ts
CHANGED
|
@@ -9,16 +9,30 @@
|
|
|
9
9
|
* contents into agent version homes.
|
|
10
10
|
*/
|
|
11
11
|
import type { AgentId, DiscoveredPlugin, PluginManifest } from './types.js';
|
|
12
|
+
export interface PluginCapabilities {
|
|
13
|
+
hasHooks: boolean;
|
|
14
|
+
hasMcp: boolean;
|
|
15
|
+
hasBin: boolean;
|
|
16
|
+
hasScripts: boolean;
|
|
17
|
+
hasSettings: boolean;
|
|
18
|
+
hasPermissions: boolean;
|
|
19
|
+
}
|
|
20
|
+
export declare const PLUGIN_EXEC_SURFACE_LABELS: Record<keyof PluginCapabilities, string>;
|
|
12
21
|
/**
|
|
13
22
|
* Discover all plugins in ~/.agents/plugins/.
|
|
14
23
|
* A valid plugin has a .claude-plugin/plugin.json manifest.
|
|
15
24
|
*/
|
|
16
25
|
export declare function discoverPlugins(): DiscoveredPlugin[];
|
|
17
26
|
export declare function buildDiscoveredPlugin(pluginRoot: string, manifest: PluginManifest): DiscoveredPlugin;
|
|
27
|
+
export declare function inspectPluginCapabilities(pluginRoot: string): PluginCapabilities;
|
|
28
|
+
export declare function hasPluginExecSurfaces(capabilities: PluginCapabilities): boolean;
|
|
29
|
+
export declare function pluginCapabilityLabels(capabilities: PluginCapabilities): string[];
|
|
18
30
|
/**
|
|
19
31
|
* Load a plugin manifest from a plugin directory.
|
|
20
32
|
*/
|
|
21
33
|
export declare function loadPluginManifest(pluginRoot: string): PluginManifest | null;
|
|
34
|
+
export declare function validatePluginName(name: string): boolean;
|
|
35
|
+
export declare function assertPluginTargetContained(targetRoot: string, pluginsDir: string): void;
|
|
22
36
|
/**
|
|
23
37
|
* Get a specific plugin by name.
|
|
24
38
|
*/
|
|
@@ -79,7 +93,10 @@ export declare function checkPluginDependencies(manifest: PluginManifest): strin
|
|
|
79
93
|
* native install path and is marked enabled — see
|
|
80
94
|
* https://code.claude.com/docs/en/plugins.
|
|
81
95
|
*/
|
|
82
|
-
export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string
|
|
96
|
+
export declare function syncPluginToVersion(plugin: DiscoveredPlugin, agent: AgentId, versionHome: string, options?: {
|
|
97
|
+
allowExecSurfaces?: boolean;
|
|
98
|
+
version?: string;
|
|
99
|
+
}): {
|
|
83
100
|
success: boolean;
|
|
84
101
|
skills: string[];
|
|
85
102
|
commands: string[];
|
|
@@ -152,6 +169,7 @@ export declare function installPlugin(spec: string): Promise<{
|
|
|
152
169
|
name: string;
|
|
153
170
|
root: string;
|
|
154
171
|
isNew: boolean;
|
|
172
|
+
capabilities: PluginCapabilities;
|
|
155
173
|
}>;
|
|
156
174
|
/**
|
|
157
175
|
* Update an installed plugin by re-pulling from its original source.
|
package/dist/lib/plugins.js
CHANGED
|
@@ -10,15 +10,24 @@
|
|
|
10
10
|
*/
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
|
-
import {
|
|
13
|
+
import { execFileSync } from 'child_process';
|
|
14
14
|
import { getPluginsDir, getTrashPluginsDir } from './state.js';
|
|
15
15
|
import { listInstalledVersions, getVersionHomePath } from './versions.js';
|
|
16
16
|
import { AGENTS, PLUGINS_CAPABLE_AGENTS } from './agents.js';
|
|
17
|
+
import { shouldInstallCommandAsSkill, installCommandSkillToVersion } from './command-skills.js';
|
|
17
18
|
import { copyPluginToMarketplace, syncMarketplaceManifest, registerMarketplace, unregisterMarketplace, enablePluginInSettings, disablePluginInSettings, removePluginFromMarketplace, marketplaceIsEmpty, removeEmptyMarketplaceDir, isInstalledInMarketplace, marketplaceRoot, } from './plugin-marketplace.js';
|
|
18
19
|
const PLUGIN_MANIFEST_DIR = '.claude-plugin';
|
|
19
20
|
const PLUGIN_MANIFEST_FILE = 'plugin.json';
|
|
20
21
|
const USER_CONFIG_FILE = '.user-config.json';
|
|
21
22
|
const SOURCE_FILE = '.source';
|
|
23
|
+
export const PLUGIN_EXEC_SURFACE_LABELS = {
|
|
24
|
+
hasHooks: 'hooks/',
|
|
25
|
+
hasMcp: '.mcp.json',
|
|
26
|
+
hasBin: 'bin/',
|
|
27
|
+
hasScripts: 'scripts/',
|
|
28
|
+
hasSettings: 'settings.json',
|
|
29
|
+
hasPermissions: 'permissions/',
|
|
30
|
+
};
|
|
22
31
|
/**
|
|
23
32
|
* Discover all plugins in ~/.agents/plugins/.
|
|
24
33
|
* A valid plugin has a .claude-plugin/plugin.json manifest.
|
|
@@ -59,6 +68,26 @@ export function buildDiscoveredPlugin(pluginRoot, manifest) {
|
|
|
59
68
|
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
60
69
|
};
|
|
61
70
|
}
|
|
71
|
+
export function inspectPluginCapabilities(pluginRoot) {
|
|
72
|
+
const manifest = loadPluginManifest(pluginRoot);
|
|
73
|
+
const plugin = manifest ? buildDiscoveredPlugin(pluginRoot, manifest) : null;
|
|
74
|
+
return {
|
|
75
|
+
hasHooks: (plugin?.hooks.length || 0) > 0 || pluginHasDirectoryEntries(pluginRoot, 'hooks'),
|
|
76
|
+
hasMcp: fs.existsSync(path.join(pluginRoot, '.mcp.json')),
|
|
77
|
+
hasBin: (plugin?.bin.length || 0) > 0,
|
|
78
|
+
hasScripts: (plugin?.scripts.length || 0) > 0,
|
|
79
|
+
hasSettings: pluginHasNonPermissionSettings(pluginRoot),
|
|
80
|
+
hasPermissions: pluginHasPermissionsPath(pluginRoot),
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
export function hasPluginExecSurfaces(capabilities) {
|
|
84
|
+
return Object.values(capabilities).some(Boolean);
|
|
85
|
+
}
|
|
86
|
+
export function pluginCapabilityLabels(capabilities) {
|
|
87
|
+
return Object.keys(PLUGIN_EXEC_SURFACE_LABELS)
|
|
88
|
+
.filter((key) => capabilities[key])
|
|
89
|
+
.map((key) => PLUGIN_EXEC_SURFACE_LABELS[key]);
|
|
90
|
+
}
|
|
62
91
|
/**
|
|
63
92
|
* Load a plugin manifest from a plugin directory.
|
|
64
93
|
*/
|
|
@@ -72,7 +101,7 @@ export function loadPluginManifest(pluginRoot) {
|
|
|
72
101
|
const parsed = JSON.parse(content);
|
|
73
102
|
if (!parsed.name || !parsed.version)
|
|
74
103
|
return null;
|
|
75
|
-
if (
|
|
104
|
+
if (!validatePluginName(parsed.name)) {
|
|
76
105
|
return null;
|
|
77
106
|
}
|
|
78
107
|
return parsed;
|
|
@@ -81,6 +110,20 @@ export function loadPluginManifest(pluginRoot) {
|
|
|
81
110
|
return null;
|
|
82
111
|
}
|
|
83
112
|
}
|
|
113
|
+
export function validatePluginName(name) {
|
|
114
|
+
return name.length > 0
|
|
115
|
+
&& !/[/\\]/.test(name)
|
|
116
|
+
&& !name.includes('..')
|
|
117
|
+
&& !name.includes('\0');
|
|
118
|
+
}
|
|
119
|
+
export function assertPluginTargetContained(targetRoot, pluginsDir) {
|
|
120
|
+
const resolvedPluginsDir = path.resolve(pluginsDir);
|
|
121
|
+
const resolvedTargetRoot = path.resolve(targetRoot);
|
|
122
|
+
if (resolvedTargetRoot !== resolvedPluginsDir
|
|
123
|
+
&& !resolvedTargetRoot.startsWith(`${resolvedPluginsDir}${path.sep}`)) {
|
|
124
|
+
throw new Error(`Plugin install target escapes plugins directory: ${targetRoot}`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
84
127
|
/**
|
|
85
128
|
* Get a specific plugin by name.
|
|
86
129
|
*/
|
|
@@ -279,7 +322,7 @@ export function checkPluginDependencies(manifest) {
|
|
|
279
322
|
* native install path and is marked enabled — see
|
|
280
323
|
* https://code.claude.com/docs/en/plugins.
|
|
281
324
|
*/
|
|
282
|
-
export function syncPluginToVersion(plugin, agent, versionHome) {
|
|
325
|
+
export function syncPluginToVersion(plugin, agent, versionHome, options = {}) {
|
|
283
326
|
const result = {
|
|
284
327
|
success: false,
|
|
285
328
|
skills: [],
|
|
@@ -302,10 +345,36 @@ export function syncPluginToVersion(plugin, agent, versionHome) {
|
|
|
302
345
|
if (Object.keys(userConfig).length > 0) {
|
|
303
346
|
expandUserConfigInDir(installDir, userConfig);
|
|
304
347
|
}
|
|
348
|
+
// 2b. Write agent-native manifest dir alongside .claude-plugin/ when the agent
|
|
349
|
+
// expects a different directory name (e.g. Codex uses .codex-plugin/).
|
|
350
|
+
const agentManifestDir = AGENTS[agent].pluginManifestDir;
|
|
351
|
+
if (agentManifestDir && agentManifestDir !== PLUGIN_MANIFEST_DIR) {
|
|
352
|
+
const srcManifest = path.join(installDir, PLUGIN_MANIFEST_DIR, PLUGIN_MANIFEST_FILE);
|
|
353
|
+
if (fs.existsSync(srcManifest)) {
|
|
354
|
+
const destManifestDir = path.join(installDir, agentManifestDir);
|
|
355
|
+
fs.mkdirSync(destManifestDir, { recursive: true });
|
|
356
|
+
fs.copyFileSync(srcManifest, path.join(destManifestDir, PLUGIN_MANIFEST_FILE));
|
|
357
|
+
}
|
|
358
|
+
}
|
|
305
359
|
// 3-5. Synthesize manifest, register marketplace, enable plugin.
|
|
306
360
|
syncMarketplaceManifest(agent, versionHome);
|
|
307
361
|
registerMarketplace(agent, versionHome);
|
|
308
|
-
enablePluginInSettings(plugin.name, agent, versionHome
|
|
362
|
+
enablePluginInSettings(plugin.name, agent, versionHome, {
|
|
363
|
+
allowExecSurfaces: options.allowExecSurfaces === true,
|
|
364
|
+
});
|
|
365
|
+
// 5b. Convert plugin commands/ to skills for agents that dropped command support
|
|
366
|
+
// (Codex >= 0.117.0). Skill name is prefixed with plugin name to avoid
|
|
367
|
+
// collision with standalone command skills.
|
|
368
|
+
if (options.version && shouldInstallCommandAsSkill(agent, options.version) && plugin.commands.length > 0) {
|
|
369
|
+
const agentDir = path.join(versionHome, `.${AGENTS[agent].id}`);
|
|
370
|
+
const skillSourceDirs = [path.join(agentDir, 'skills')];
|
|
371
|
+
for (const cmd of plugin.commands) {
|
|
372
|
+
const srcPath = path.join(plugin.root, 'commands', `${cmd}.md`);
|
|
373
|
+
if (fs.existsSync(srcPath)) {
|
|
374
|
+
installCommandSkillToVersion(agentDir, `${plugin.name}-${cmd}`, srcPath, skillSourceDirs);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
309
378
|
// 6. Migrate legacy dual-dash flat layout from previous versions of agents-cli.
|
|
310
379
|
migrateLegacyFlatLayout(plugin, agent, versionHome);
|
|
311
380
|
// Populate the result shape for backward-compatible callers/reporting.
|
|
@@ -321,7 +390,13 @@ export function syncPluginToVersion(plugin, agent, versionHome) {
|
|
|
321
390
|
return result;
|
|
322
391
|
}
|
|
323
392
|
function pluginHasPermissions(plugin) {
|
|
324
|
-
|
|
393
|
+
return pluginHasPermissionsPath(plugin.root);
|
|
394
|
+
}
|
|
395
|
+
function pluginHasPermissionsPath(pluginRoot) {
|
|
396
|
+
const permissionsDir = path.join(pluginRoot, 'permissions');
|
|
397
|
+
if (fs.existsSync(permissionsDir))
|
|
398
|
+
return true;
|
|
399
|
+
const settingsPath = path.join(pluginRoot, 'settings.json');
|
|
325
400
|
if (!fs.existsSync(settingsPath))
|
|
326
401
|
return false;
|
|
327
402
|
try {
|
|
@@ -332,6 +407,17 @@ function pluginHasPermissions(plugin) {
|
|
|
332
407
|
return false;
|
|
333
408
|
}
|
|
334
409
|
}
|
|
410
|
+
function pluginHasDirectoryEntries(pluginRoot, dirName) {
|
|
411
|
+
const dir = path.join(pluginRoot, dirName);
|
|
412
|
+
if (!fs.existsSync(dir))
|
|
413
|
+
return false;
|
|
414
|
+
try {
|
|
415
|
+
return fs.readdirSync(dir).some((entry) => !entry.startsWith('.'));
|
|
416
|
+
}
|
|
417
|
+
catch {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
335
421
|
/**
|
|
336
422
|
* Walk a directory and replace ${user_config.*} placeholders in text files.
|
|
337
423
|
* Leaves all other variables (${CLAUDE_PLUGIN_ROOT}, ${CLAUDE_PLUGIN_DATA}) alone.
|
|
@@ -846,7 +932,11 @@ export async function installPlugin(spec) {
|
|
|
846
932
|
targetName = path.basename(resolvedSource).replace(/\.git$/, '');
|
|
847
933
|
}
|
|
848
934
|
}
|
|
935
|
+
if (!validatePluginName(targetName)) {
|
|
936
|
+
throw new Error(`Invalid plugin name: ${targetName}`);
|
|
937
|
+
}
|
|
849
938
|
const targetRoot = path.join(pluginsDir, targetName);
|
|
939
|
+
assertPluginTargetContained(targetRoot, pluginsDir);
|
|
850
940
|
const isNew = !fs.existsSync(targetRoot);
|
|
851
941
|
if (isLocalPath) {
|
|
852
942
|
// Copy local directory
|
|
@@ -860,7 +950,7 @@ export async function installPlugin(spec) {
|
|
|
860
950
|
if (fs.existsSync(targetRoot)) {
|
|
861
951
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
862
952
|
}
|
|
863
|
-
|
|
953
|
+
execFileSync('git', ['clone', '--depth', '1', resolvedSource, targetRoot], {
|
|
864
954
|
stdio: 'pipe',
|
|
865
955
|
});
|
|
866
956
|
}
|
|
@@ -870,9 +960,10 @@ export async function installPlugin(spec) {
|
|
|
870
960
|
fs.rmSync(targetRoot, { recursive: true, force: true });
|
|
871
961
|
throw new Error(`Installed source has no valid .claude-plugin/plugin.json`);
|
|
872
962
|
}
|
|
963
|
+
const capabilities = inspectPluginCapabilities(targetRoot);
|
|
873
964
|
// Persist source for future updates
|
|
874
965
|
fs.writeFileSync(path.join(targetRoot, SOURCE_FILE), JSON.stringify({ source, isGit: !isLocalPath }), 'utf-8');
|
|
875
|
-
return { name: manifest.name, root: targetRoot, isNew };
|
|
966
|
+
return { name: manifest.name, root: targetRoot, isNew, capabilities };
|
|
876
967
|
}
|
|
877
968
|
/**
|
|
878
969
|
* Update an installed plugin by re-pulling from its original source.
|
|
@@ -896,7 +987,7 @@ export async function updatePlugin(name) {
|
|
|
896
987
|
}
|
|
897
988
|
try {
|
|
898
989
|
if (sourceInfo.isGit) {
|
|
899
|
-
|
|
990
|
+
execFileSync('git', ['-C', plugin.root, 'pull', '--ff-only'], { stdio: 'pipe' });
|
|
900
991
|
}
|
|
901
992
|
else {
|
|
902
993
|
const resolvedSource = sourceInfo.source.replace(/^~/, process.env.HOME || '~');
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared redaction helpers for text that may be exported or logged.
|
|
3
|
+
*/
|
|
4
|
+
const SECRET_PATTERNS = [
|
|
5
|
+
[/\bAKIA[0-9A-Z]{16}\b/g, '[REDACTED_AWS_KEY]'],
|
|
6
|
+
[/\bghp_[A-Za-z0-9]{36}\b/g, '[REDACTED_GITHUB_TOKEN]'],
|
|
7
|
+
[/\bsk-[A-Za-z0-9]{20,}\b/g, '[REDACTED_API_KEY]'],
|
|
8
|
+
[/\bnpm_[A-Za-z0-9]{36}\b/g, '[REDACTED_NPM_TOKEN]'],
|
|
9
|
+
[/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g, '[REDACTED_JWT]'],
|
|
10
|
+
[/\b([A-Z0-9_]*(?:TOKEN|KEY|SECRET|PASSWORD)[A-Z0-9_]*)=("[^"]*"|'[^']*'|\S+)/gi, '$1=[REDACTED]'],
|
|
11
|
+
];
|
|
12
|
+
export function redactSecrets(text) {
|
|
13
|
+
let safe = text;
|
|
14
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) {
|
|
15
|
+
safe = safe.replace(pattern, replacement);
|
|
16
|
+
}
|
|
17
|
+
return safe;
|
|
18
|
+
}
|
package/dist/lib/registry.d.ts
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* with transport, runtime, and argument metadata.
|
|
7
7
|
*/
|
|
8
8
|
import type { RegistryType, RegistryConfig, McpServerEntry, SkillEntry, RegistrySearchResult, ResolvedPackage } from './types.js';
|
|
9
|
+
export declare function validatedNpmSpec(spec: string): string;
|
|
10
|
+
export declare function validatedPyPISpec(spec: string): string;
|
|
9
11
|
/** Get all registries of a given type, merging defaults with user overrides. */
|
|
10
12
|
export declare function getRegistries(type: RegistryType): Record<string, RegistryConfig>;
|
|
11
13
|
/** Get only the enabled registries of a given type. */
|
package/dist/lib/registry.js
CHANGED
|
@@ -8,6 +8,21 @@
|
|
|
8
8
|
import * as fs from 'fs';
|
|
9
9
|
import { DEFAULT_REGISTRIES } from './types.js';
|
|
10
10
|
import { readMeta, writeMeta } from './state.js';
|
|
11
|
+
const UNSAFE_PACKAGE_SPEC_CHARS = /[;&|`$\s\x00-\x1f\x7f]/;
|
|
12
|
+
const NPM_SPEC_PATTERN = /^(@[a-z0-9][a-z0-9-_.]*\/)?[a-z0-9][a-z0-9-_.]*(@[A-Za-z0-9._+-]+)?$/;
|
|
13
|
+
const PYPI_SPEC_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*(\[[A-Za-z0-9,_-]+\])?(==[A-Za-z0-9._-]+)?$/;
|
|
14
|
+
export function validatedNpmSpec(spec) {
|
|
15
|
+
if (spec.length > 214 || UNSAFE_PACKAGE_SPEC_CHARS.test(spec) || !NPM_SPEC_PATTERN.test(spec)) {
|
|
16
|
+
throw new Error(`Invalid npm package spec: ${spec}`);
|
|
17
|
+
}
|
|
18
|
+
return spec;
|
|
19
|
+
}
|
|
20
|
+
export function validatedPyPISpec(spec) {
|
|
21
|
+
if (UNSAFE_PACKAGE_SPEC_CHARS.test(spec) || !PYPI_SPEC_PATTERN.test(spec)) {
|
|
22
|
+
throw new Error(`Invalid PyPI package spec: ${spec}`);
|
|
23
|
+
}
|
|
24
|
+
return spec;
|
|
25
|
+
}
|
|
11
26
|
/** Get all registries of a given type, merging defaults with user overrides. */
|
|
12
27
|
export function getRegistries(type) {
|
|
13
28
|
const meta = readMeta();
|
package/dist/lib/sandbox.js
CHANGED
|
@@ -11,7 +11,15 @@ import * as path from 'path';
|
|
|
11
11
|
import * as os from 'os';
|
|
12
12
|
import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from './gemini-settings.js';
|
|
13
13
|
import { getRoutinesDir } from './state.js';
|
|
14
|
-
|
|
14
|
+
function resolveRealHome() {
|
|
15
|
+
const home = os.homedir();
|
|
16
|
+
try {
|
|
17
|
+
return fs.realpathSync(home);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return home;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
15
23
|
/** Environment variables forwarded from the parent process into the sandbox. */
|
|
16
24
|
const ENV_ALLOWLIST = [
|
|
17
25
|
'PATH',
|
|
@@ -93,8 +101,9 @@ export function cleanJobHome(name) {
|
|
|
93
101
|
}
|
|
94
102
|
/** Symlink allowed directories into the overlay HOME, skipping paths outside the real HOME. */
|
|
95
103
|
export function symlinkAllowedDirs(overlayHome, dirs) {
|
|
104
|
+
const realHome = resolveRealHome();
|
|
96
105
|
for (const dir of dirs) {
|
|
97
|
-
const expanded = dir.replace(/^~/,
|
|
106
|
+
const expanded = dir.replace(/^~/, realHome);
|
|
98
107
|
// Resolve .. and symlinks to prevent path traversal outside HOME
|
|
99
108
|
let realPath;
|
|
100
109
|
try {
|
|
@@ -105,10 +114,10 @@ export function symlinkAllowedDirs(overlayHome, dirs) {
|
|
|
105
114
|
// Directory doesn't exist yet — resolve .. components without following symlinks
|
|
106
115
|
realPath = path.resolve(expanded);
|
|
107
116
|
}
|
|
108
|
-
if (!realPath.startsWith(
|
|
117
|
+
if (!realPath.startsWith(realHome + path.sep) && realPath !== realHome) {
|
|
109
118
|
continue;
|
|
110
119
|
}
|
|
111
|
-
const relativePath = path.relative(
|
|
120
|
+
const relativePath = path.relative(realHome, realPath);
|
|
112
121
|
const symlinkTarget = path.join(overlayHome, relativePath);
|
|
113
122
|
const parentDir = path.dirname(symlinkTarget);
|
|
114
123
|
fs.mkdirSync(parentDir, { recursive: true });
|
|
@@ -122,6 +131,7 @@ export function symlinkAllowedDirs(overlayHome, dirs) {
|
|
|
122
131
|
}
|
|
123
132
|
/** Generate a Claude settings.json in the overlay with scoped permissions from the job config. */
|
|
124
133
|
export function generateClaudeConfig(overlayHome, config) {
|
|
134
|
+
const realHome = resolveRealHome();
|
|
125
135
|
const claudeDir = path.join(overlayHome, '.claude');
|
|
126
136
|
fs.mkdirSync(claudeDir, { recursive: true });
|
|
127
137
|
const allowPermissions = [];
|
|
@@ -152,7 +162,7 @@ export function generateClaudeConfig(overlayHome, config) {
|
|
|
152
162
|
// Scope filesystem tools to allowed dirs
|
|
153
163
|
if (config.allow?.dirs) {
|
|
154
164
|
for (const dir of config.allow.dirs) {
|
|
155
|
-
const resolved = dir.replace(/^~/,
|
|
165
|
+
const resolved = dir.replace(/^~/, realHome);
|
|
156
166
|
// Read always granted for allowed dirs
|
|
157
167
|
allowPermissions.push(`Read(${resolved}/**)`);
|
|
158
168
|
if (config.mode === 'edit') {
|
|
@@ -40,9 +40,15 @@ export interface SecretsBundle {
|
|
|
40
40
|
/** Optional per-var metadata, keyed by var name (parallel to `vars`). */
|
|
41
41
|
meta?: Record<string, VarMeta>;
|
|
42
42
|
}
|
|
43
|
+
export interface LegacyBundleCandidate {
|
|
44
|
+
name: string;
|
|
45
|
+
file: string;
|
|
46
|
+
keys: string[];
|
|
47
|
+
}
|
|
43
48
|
export declare const RESERVED_ENV_NAMES: Set<string>;
|
|
44
49
|
export declare function bundleToEnvPrefix(name: string): string;
|
|
45
50
|
export declare function isReservedEnvName(key: string): boolean;
|
|
51
|
+
export declare function isLoaderOrInterpreterEnv(name: string): boolean;
|
|
46
52
|
/** Validate a bundle name against the allowed pattern. Throws on invalid input. */
|
|
47
53
|
export declare function validateBundleName(name: string): void;
|
|
48
54
|
export declare function validateEnvKey(key: string): void;
|
|
@@ -107,16 +113,5 @@ export declare function keychainItemsForBundle(bundle: SecretsBundle): Array<{
|
|
|
107
113
|
item: string;
|
|
108
114
|
}>;
|
|
109
115
|
export declare function parseDotenv(content: string): Record<string, string>;
|
|
110
|
-
|
|
111
|
-
* One-shot migration: move legacy YAML bundles into the keychain. Scans both
|
|
112
|
-
* `~/.agents/secrets/` and `~/.agents-system/secrets/` — past versions of the
|
|
113
|
-
* CLI sometimes wrote bundles into the system repo even though that's never
|
|
114
|
-
* been a legitimate location. After migration the directories are removed so
|
|
115
|
-
* the system repo never carries a `secrets/` subdir again.
|
|
116
|
-
*
|
|
117
|
-
* Idempotent: re-runs after the dirs are gone are no-ops. Called eagerly at
|
|
118
|
-
* the top of every `agents secrets` subcommand. Skipped on the latency-
|
|
119
|
-
* sensitive `agents run` path.
|
|
120
|
-
*/
|
|
121
|
-
export declare function migrateLegacyBundles(): void;
|
|
116
|
+
export declare function migrateLegacyBundles(confirmBundle: (candidate: LegacyBundleCandidate) => boolean | Promise<boolean>): Promise<number>;
|
|
122
117
|
export type { SecretRef };
|
|
@@ -43,7 +43,24 @@ export function bundleToEnvPrefix(name) {
|
|
|
43
43
|
return name.replace(/[-\.]/g, '_').toUpperCase();
|
|
44
44
|
}
|
|
45
45
|
export function isReservedEnvName(key) {
|
|
46
|
-
return RESERVED_ENV_NAMES.has(key);
|
|
46
|
+
return RESERVED_ENV_NAMES.has(key.toUpperCase());
|
|
47
|
+
}
|
|
48
|
+
export function isLoaderOrInterpreterEnv(name) {
|
|
49
|
+
const upper = name.toUpperCase();
|
|
50
|
+
return upper.startsWith('LD_') ||
|
|
51
|
+
upper.startsWith('DYLD_') ||
|
|
52
|
+
[
|
|
53
|
+
'NODE_OPTIONS',
|
|
54
|
+
'PYTHONPATH',
|
|
55
|
+
'PYTHONSTARTUP',
|
|
56
|
+
'BASH_ENV',
|
|
57
|
+
'ENV',
|
|
58
|
+
'PERL5OPT',
|
|
59
|
+
'RUBYOPT',
|
|
60
|
+
'PROMPT_COMMAND',
|
|
61
|
+
'IFS',
|
|
62
|
+
'CDPATH',
|
|
63
|
+
].includes(upper);
|
|
47
64
|
}
|
|
48
65
|
/** Validate a bundle name against the allowed pattern. Throws on invalid input. */
|
|
49
66
|
export function validateBundleName(name) {
|
|
@@ -55,6 +72,9 @@ export function validateEnvKey(key) {
|
|
|
55
72
|
if (!ENV_KEY_PATTERN.test(key)) {
|
|
56
73
|
throw new Error(`Invalid environment variable name '${key}'. Must match [A-Za-z_][A-Za-z0-9_]*.`);
|
|
57
74
|
}
|
|
75
|
+
if (isLoaderOrInterpreterEnv(key) || isReservedEnvName(key)) {
|
|
76
|
+
throw new Error(`Env key "${key}" is reserved — cannot be used in a secrets bundle. Reserved keys include PATH, HOME, USER, and dynamic-loader/interpreter vars (LD_*, DYLD_*, NODE_OPTIONS, etc.).`);
|
|
77
|
+
}
|
|
58
78
|
}
|
|
59
79
|
/** Assert that `t` is one of the known SECRET_TYPES. Throws with the allowed list otherwise. */
|
|
60
80
|
export function validateSecretType(t) {
|
|
@@ -396,18 +416,7 @@ export function parseDotenv(content) {
|
|
|
396
416
|
}
|
|
397
417
|
return out;
|
|
398
418
|
}
|
|
399
|
-
|
|
400
|
-
* One-shot migration: move legacy YAML bundles into the keychain. Scans both
|
|
401
|
-
* `~/.agents/secrets/` and `~/.agents-system/secrets/` — past versions of the
|
|
402
|
-
* CLI sometimes wrote bundles into the system repo even though that's never
|
|
403
|
-
* been a legitimate location. After migration the directories are removed so
|
|
404
|
-
* the system repo never carries a `secrets/` subdir again.
|
|
405
|
-
*
|
|
406
|
-
* Idempotent: re-runs after the dirs are gone are no-ops. Called eagerly at
|
|
407
|
-
* the top of every `agents secrets` subcommand. Skipped on the latency-
|
|
408
|
-
* sensitive `agents run` path.
|
|
409
|
-
*/
|
|
410
|
-
export function migrateLegacyBundles() {
|
|
419
|
+
export async function migrateLegacyBundles(confirmBundle) {
|
|
411
420
|
const home = os.homedir();
|
|
412
421
|
const dirs = [
|
|
413
422
|
path.join(home, '.agents', 'secrets'),
|
|
@@ -426,26 +435,35 @@ export function migrateLegacyBundles() {
|
|
|
426
435
|
for (const entry of ymls) {
|
|
427
436
|
const file = path.join(dir, entry);
|
|
428
437
|
const name = entry.replace(/\.(yml|yaml)$/, '');
|
|
438
|
+
let parsed;
|
|
429
439
|
try {
|
|
430
440
|
validateBundleName(name);
|
|
431
441
|
const raw = fs.readFileSync(file, 'utf-8');
|
|
432
|
-
|
|
433
|
-
if (!parsed || typeof parsed !== 'object')
|
|
434
|
-
continue;
|
|
435
|
-
const bundle = {
|
|
436
|
-
name,
|
|
437
|
-
description: parsed.description,
|
|
438
|
-
allow_exec: Boolean(parsed.allow_exec),
|
|
439
|
-
icloud_sync: Boolean(parsed.icloud_sync),
|
|
440
|
-
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
441
|
-
};
|
|
442
|
-
writeBundle(bundle);
|
|
443
|
-
fs.unlinkSync(file);
|
|
444
|
-
migrated++;
|
|
442
|
+
parsed = yaml.parse(raw);
|
|
445
443
|
}
|
|
446
444
|
catch {
|
|
447
445
|
// Leave malformed YAMLs in place so the user can inspect them.
|
|
446
|
+
continue;
|
|
447
|
+
}
|
|
448
|
+
if (!parsed || typeof parsed !== 'object')
|
|
449
|
+
continue;
|
|
450
|
+
const bundle = {
|
|
451
|
+
name,
|
|
452
|
+
description: parsed.description,
|
|
453
|
+
allow_exec: Boolean(parsed.allow_exec),
|
|
454
|
+
icloud_sync: Boolean(parsed.icloud_sync),
|
|
455
|
+
vars: parsed.vars && typeof parsed.vars === 'object' ? parsed.vars : {},
|
|
456
|
+
};
|
|
457
|
+
const keys = Object.keys(bundle.vars);
|
|
458
|
+
for (const key of keys) {
|
|
459
|
+
validateEnvKey(key);
|
|
448
460
|
}
|
|
461
|
+
const proceed = await confirmBundle({ name, file, keys });
|
|
462
|
+
if (!proceed)
|
|
463
|
+
continue;
|
|
464
|
+
writeBundle(bundle);
|
|
465
|
+
fs.unlinkSync(file);
|
|
466
|
+
migrated++;
|
|
449
467
|
}
|
|
450
468
|
try {
|
|
451
469
|
if (fs.readdirSync(dir).length === 0)
|
|
@@ -453,7 +471,5 @@ export function migrateLegacyBundles() {
|
|
|
453
471
|
}
|
|
454
472
|
catch { /* not empty or already gone */ }
|
|
455
473
|
}
|
|
456
|
-
|
|
457
|
-
console.log(`Migrated ${migrated} legacy bundle${migrated === 1 ? '' : 's'} into keychain.`);
|
|
458
|
-
}
|
|
474
|
+
return migrated;
|
|
459
475
|
}
|
|
@@ -78,9 +78,9 @@ export function setKeychainBackendForTest(b) {
|
|
|
78
78
|
backend = b;
|
|
79
79
|
return prev;
|
|
80
80
|
}
|
|
81
|
-
// Backend routing: non-sync items go through /usr/bin/security
|
|
82
|
-
//
|
|
83
|
-
//
|
|
81
|
+
// Backend routing: non-sync items go through /usr/bin/security with an empty
|
|
82
|
+
// trusted-app ACL; existing items written by older versions retain their ACL.
|
|
83
|
+
// Sync items must go through the signed .app — only the .app
|
|
84
84
|
// holds the keychain-access-groups entitlement macOS requires for
|
|
85
85
|
// kSecAttrSynchronizable. Enumeration also goes through the .app because the
|
|
86
86
|
// security CLI doesn't expose listing by service prefix.
|
|
@@ -166,7 +166,7 @@ export function setKeychainToken(item, value, sync = false) {
|
|
|
166
166
|
}
|
|
167
167
|
// `security -i` keeps the value out of argv (and `ps`).
|
|
168
168
|
const user = os.userInfo().username;
|
|
169
|
-
const cmd = `add-generic-password -a ${quoteForSecurityCli(user)} -s ${quoteForSecurityCli(item)} -w ${quoteForSecurityCli(value)} -U\n`;
|
|
169
|
+
const cmd = `add-generic-password -a ${quoteForSecurityCli(user)} -s ${quoteForSecurityCli(item)} -w ${quoteForSecurityCli(value)} -T ${quoteForSecurityCli('')} -U\n`;
|
|
170
170
|
const result = spawnSync('security', ['-i'], {
|
|
171
171
|
input: cmd,
|
|
172
172
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -10,6 +10,8 @@
|
|
|
10
10
|
* GET /api/v1/cloud-runs/:id/session.jsonl → raw captured jsonl
|
|
11
11
|
*/
|
|
12
12
|
import type { SessionMeta } from './types.js';
|
|
13
|
+
export declare function assertContained(candidate: string, rootDir: string): string;
|
|
14
|
+
export declare function validateCloudExecutionId(executionId: string): string;
|
|
13
15
|
/**
|
|
14
16
|
* List cloud executions the user has captured sessions for. Includes
|
|
15
17
|
* completed + needs_review + failed; an empty session_path means capture
|