@nexrall/code-core 1.4.24 → 1.4.25
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/dist/agent/agentTypes.d.ts +31 -0
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +182 -33
- package/dist/agent/loop.d.ts +14 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +64 -10
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/plugins/index.d.ts +20 -0
- package/dist/plugins/index.d.ts.map +1 -1
- package/dist/plugins/index.js +44 -3
- package/dist/plugins/installer.d.ts +40 -2
- package/dist/plugins/installer.d.ts.map +1 -1
- package/dist/plugins/installer.js +85 -14
- package/dist/plugins/sources.d.ts +49 -0
- package/dist/plugins/sources.d.ts.map +1 -0
- package/dist/plugins/sources.js +197 -0
- package/package.json +1 -1
|
@@ -20,6 +20,20 @@ export interface AgentType {
|
|
|
20
20
|
*/
|
|
21
21
|
testFilesOnly?: boolean;
|
|
22
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* A problem found while loading an agent definition.
|
|
25
|
+
*
|
|
26
|
+
* These used to be silently swallowed. Every one of them changed what an agent
|
|
27
|
+
* could DO — a mistyped tool name removed a capability, a missing frontmatter
|
|
28
|
+
* block removed the allowlist entirely — and the user was told nothing.
|
|
29
|
+
*/
|
|
30
|
+
export interface AgentWarning {
|
|
31
|
+
/** Absolute path of the definition file the problem was found in. */
|
|
32
|
+
file: string;
|
|
33
|
+
/** Agent name, when one could be determined. */
|
|
34
|
+
agent: string;
|
|
35
|
+
message: string;
|
|
36
|
+
}
|
|
23
37
|
/**
|
|
24
38
|
* Discover all agent types. Precedence: project > global > plugin > builtin.
|
|
25
39
|
*
|
|
@@ -30,6 +44,23 @@ export interface AgentType {
|
|
|
30
44
|
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
31
45
|
*/
|
|
32
46
|
export declare function loadAgentTypes(workDir: string): AgentType[];
|
|
47
|
+
/**
|
|
48
|
+
* As `loadAgentTypes`, but also reports what was wrong with the definitions.
|
|
49
|
+
*
|
|
50
|
+
* Split in two so the common caller stays a one-liner while the CLI's `/agents`
|
|
51
|
+
* and the agent loop can surface problems. Warnings never hide an agent: a
|
|
52
|
+
* flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
|
|
53
|
+
* because making a user's agent vanish over a typo is its own kind of silent
|
|
54
|
+
* failure.
|
|
55
|
+
*/
|
|
56
|
+
export declare function loadAgentTypesWithWarnings(workDir: string): {
|
|
57
|
+
types: AgentType[];
|
|
58
|
+
warnings: AgentWarning[];
|
|
59
|
+
};
|
|
60
|
+
/** The tool names a hand-written allowlist may use (exported for validation + tests). */
|
|
61
|
+
export declare function knownToolNames(): string[];
|
|
62
|
+
/** The built-in agents, for clients that want to show them alongside user-defined ones. */
|
|
63
|
+
export declare function builtinAgents(): AgentType[];
|
|
33
64
|
/**
|
|
34
65
|
* One line per agent for the system prompt's <available_subagents> block.
|
|
35
66
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"agentTypes.d.ts","sourceRoot":"","sources":["../../src/agent/agentTypes.ts"],"names":[],"mappings":"AAiCA,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,6EAA6E;IAC7E,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;IACjB,iDAAiD;IACjD,KAAK,CAAC,EAAE,OAAO,GAAG,KAAK,GAAG,OAAO,CAAC;IAClC,qEAAqE;IACrE,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,SAAS,GAAG,QAAQ,GAAG,SAAS,GAAG,QAAQ,CAAC;IACpD;;;;;;;;;OASG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AA6QD;;;;;;GAMG;AACH,MAAM,WAAW,YAAY;IAC3B,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,gDAAgD;IAChD,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;CACjB;AA0JD;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAE3D;AAED;;;;;;;;GAQG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAC;IAAC,QAAQ,EAAE,YAAY,EAAE,CAAA;CAAE,CAqB5G;AAED,yFAAyF;AACzF,wBAAgB,cAAc,IAAI,MAAM,EAAE,CAEzC;AAED,2FAA2F;AAC3F,wBAAgB,aAAa,IAAI,SAAS,EAAE,CAE3C;AACD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,SAAS,EAAE,GAAG,MAAM,CAY1D;AAQD,wBAAgB,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,GAAG,SAAS,CAIjG"}
|
package/dist/agent/agentTypes.js
CHANGED
|
@@ -34,6 +34,9 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.loadAgentTypes = loadAgentTypes;
|
|
37
|
+
exports.loadAgentTypesWithWarnings = loadAgentTypesWithWarnings;
|
|
38
|
+
exports.knownToolNames = knownToolNames;
|
|
39
|
+
exports.builtinAgents = builtinAgents;
|
|
37
40
|
exports.summariseAgents = summariseAgents;
|
|
38
41
|
exports.findAgentType = findAgentType;
|
|
39
42
|
const fs = __importStar(require("fs"));
|
|
@@ -57,17 +60,38 @@ const index_1 = require("../plugins/index");
|
|
|
57
60
|
const READ_ONLY_TOOLS = [
|
|
58
61
|
// Universal
|
|
59
62
|
'read_file', 'search_files', 'glob', 'list_directory', 'bash', 'bash_output',
|
|
63
|
+
// kill_shell belongs next to bash_output: an agent that can start a background
|
|
64
|
+
// process and poll it but never stop it leaks that process past its own
|
|
65
|
+
// lifetime. Stopping a shell you started is not a write to the repo.
|
|
66
|
+
'kill_shell',
|
|
60
67
|
'notebook_read', 'todo_write', 'todo_read',
|
|
68
|
+
// Skills are reusable prompt playbooks, and loop.ts advertises the skills
|
|
69
|
+
// catalogue to sub-agents at EVERY depth — so withholding the tool that loads
|
|
70
|
+
// one meant showing every sub-agent a menu it could not order from.
|
|
71
|
+
'use_skill',
|
|
61
72
|
// VS Code language server (ignored on the CLI)
|
|
62
73
|
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
63
74
|
'get_hover', 'get_diagnostics',
|
|
64
75
|
];
|
|
65
|
-
/**
|
|
66
|
-
|
|
76
|
+
/**
|
|
77
|
+
* Read-only + the network, for agents that must consult external sources.
|
|
78
|
+
*
|
|
79
|
+
* `web_search` is deliberately ABSENT despite the name of this list. It is a
|
|
80
|
+
* SERVER-SIDE tool: Anthropic executes it and returns the result inside the same
|
|
81
|
+
* assistant message, so loop.ts filters those blocks out before the permission
|
|
82
|
+
* gate ever runs. Listing it here would be theatre — it grants nothing (agents
|
|
83
|
+
* without it can still search) and denies nothing. Naming the absence is the
|
|
84
|
+
* only way to stop it being "helpfully" re-added.
|
|
85
|
+
*/
|
|
86
|
+
const RESEARCH_TOOLS = [...READ_ONLY_TOOLS, 'fetch_url'];
|
|
67
87
|
/** Read-only + the write tools, for agents that produce code. */
|
|
68
88
|
const WRITE_TOOLS = [
|
|
69
89
|
...READ_ONLY_TOOLS,
|
|
70
90
|
'write_file', 'edit_file', 'multi_edit', 'create_directory', 'move_file', 'copy_file',
|
|
91
|
+
// notebook_edit is a write tool like any other, and allowsTestOnlyWrite already
|
|
92
|
+
// knows its shape (`source` is cell CONTENT, not a path). Omitting it just meant
|
|
93
|
+
// test-writer silently could not touch notebooks.
|
|
94
|
+
'notebook_edit',
|
|
71
95
|
];
|
|
72
96
|
const BUILTIN_AGENTS = [
|
|
73
97
|
{
|
|
@@ -247,17 +271,43 @@ const BUILTIN_AGENTS = [
|
|
|
247
271
|
].join('\n'),
|
|
248
272
|
},
|
|
249
273
|
];
|
|
274
|
+
/**
|
|
275
|
+
* Every tool name a client may offer, for validating a hand-written allowlist.
|
|
276
|
+
*
|
|
277
|
+
* Deliberately a SEPARATE list rather than an import from tools/executor.ts:
|
|
278
|
+
* that module's TOOL_MAP is the CLI's local dispatch table and legitimately
|
|
279
|
+
* lacks `get_diagnostics` (intercepted by VS Code before the executor) and
|
|
280
|
+
* `web_search` (run server-side by Anthropic). Validating against it would
|
|
281
|
+
* reject two perfectly valid names. A canary test asserts every name used by the
|
|
282
|
+
* builtin agents appears here, so the two cannot drift apart unnoticed.
|
|
283
|
+
*/
|
|
284
|
+
const KNOWN_TOOL_NAMES = new Set([
|
|
285
|
+
'read_file', 'write_file', 'edit_file', 'multi_edit', 'list_directory', 'create_directory',
|
|
286
|
+
'move_file', 'copy_file', 'delete_file', 'search_files', 'glob',
|
|
287
|
+
'bash', 'bash_output', 'kill_shell',
|
|
288
|
+
'notebook_read', 'notebook_edit',
|
|
289
|
+
'todo_write', 'todo_read', 'memory_write', 'memory_read', 'use_skill',
|
|
290
|
+
'fetch_url', 'web_search', 'generate_image', 'stock_photo', 'open_in_browser',
|
|
291
|
+
'task',
|
|
292
|
+
'get_symbols', 'get_workspace_symbols', 'find_references', 'go_to_definition',
|
|
293
|
+
'get_hover', 'get_diagnostics',
|
|
294
|
+
]);
|
|
295
|
+
/** Frontmatter keys this parser understands, for typo detection. */
|
|
296
|
+
const KNOWN_META_KEYS = new Set([
|
|
297
|
+
'name', 'description', 'tools', 'model', 'test_files_only', 'testfilesonly',
|
|
298
|
+
]);
|
|
299
|
+
const VALID_MODELS = ['turbo', 'pro', 'ultra'];
|
|
250
300
|
function parseFrontmatter(raw) {
|
|
251
301
|
const m = /^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(raw);
|
|
252
302
|
if (!m)
|
|
253
|
-
return { meta: {}, body: raw.trim() };
|
|
303
|
+
return { meta: {}, body: raw.trim(), ok: false };
|
|
254
304
|
const meta = {};
|
|
255
305
|
for (const line of m[1].split(/\r?\n/)) {
|
|
256
306
|
const kv = /^([A-Za-z0-9_-]+)\s*:\s*(.*)$/.exec(line.trim());
|
|
257
307
|
if (kv)
|
|
258
308
|
meta[kv[1].toLowerCase()] = kv[2].trim().replace(/^["']|["']$/g, '');
|
|
259
309
|
}
|
|
260
|
-
return { meta, body: (m[2] ?? '').trim() };
|
|
310
|
+
return { meta, body: (m[2] ?? '').trim(), ok: true };
|
|
261
311
|
}
|
|
262
312
|
function parseModel(v) {
|
|
263
313
|
const s = (v ?? '').toLowerCase();
|
|
@@ -277,41 +327,118 @@ function parseToolList(v) {
|
|
|
277
327
|
.filter(Boolean);
|
|
278
328
|
return tools.length ? tools : undefined;
|
|
279
329
|
}
|
|
280
|
-
function loadDir(dir, source, into) {
|
|
330
|
+
function loadDir(dir, source, into, warnings) {
|
|
281
331
|
let entries;
|
|
282
332
|
try {
|
|
283
|
-
entries = fs.readdirSync(dir
|
|
333
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
284
334
|
}
|
|
285
335
|
catch {
|
|
336
|
+
// Overwhelmingly "no .nexrall/agents here", which is the normal case and not
|
|
337
|
+
// worth a word. A genuine permission error is rare enough that the cost of
|
|
338
|
+
// staying quiet is lower than warning on every repo that has no agents.
|
|
286
339
|
return;
|
|
287
340
|
}
|
|
288
|
-
for (const
|
|
341
|
+
for (const entry of entries) {
|
|
342
|
+
const full = path.join(dir, entry.name);
|
|
343
|
+
// Recurse into subfolders so definitions can be organised (agents/review/…),
|
|
344
|
+
// matching Claude Code. Identity still comes only from the name field / filename.
|
|
345
|
+
if (entry.isDirectory()) {
|
|
346
|
+
loadDir(full, source, into, warnings);
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
if (!entry.name.endsWith('.md'))
|
|
350
|
+
continue;
|
|
351
|
+
let raw;
|
|
289
352
|
try {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
353
|
+
raw = fs.readFileSync(full, 'utf-8');
|
|
354
|
+
}
|
|
355
|
+
catch (err) {
|
|
356
|
+
warnings.push({ file: full, agent: path.basename(entry.name, '.md'), message: `could not be read (${err.message}) — this agent was skipped` });
|
|
357
|
+
continue;
|
|
358
|
+
}
|
|
359
|
+
const { meta, body, ok } = parseFrontmatter(raw);
|
|
360
|
+
const name = (meta.name || path.basename(entry.name, '.md')).trim();
|
|
361
|
+
if (!name)
|
|
362
|
+
continue;
|
|
363
|
+
// Earlier tiers win (project > global > plugin).
|
|
364
|
+
if (source !== 'project' && into.has(name))
|
|
365
|
+
continue;
|
|
366
|
+
const tools = parseToolList(meta.tools);
|
|
367
|
+
let effectiveTools = tools;
|
|
368
|
+
// ── FAIL CLOSED on a definition we could not parse ────────────────────────
|
|
369
|
+
//
|
|
370
|
+
// A file with no `---` frontmatter block used to yield tools === undefined,
|
|
371
|
+
// which means "no allowlist" — i.e. FULL access, including write_file, bash
|
|
372
|
+
// and delete_file. So the worse the file, the more power it got: the exact
|
|
373
|
+
// inversion you do not want in a permission system. A malformed definition
|
|
374
|
+
// is now read-only, which is both the safe reading and, for anyone hand-
|
|
375
|
+
// writing an agent, almost always the intended one.
|
|
376
|
+
if (!ok) {
|
|
377
|
+
effectiveTools = READ_ONLY_TOOLS;
|
|
378
|
+
warnings.push({
|
|
379
|
+
file: full,
|
|
380
|
+
agent: name,
|
|
381
|
+
message: 'has no valid YAML frontmatter (a `---` block must be the first thing in the file), so no ' +
|
|
382
|
+
'tool allowlist could be read. Treating it as READ-ONLY. Add frontmatter with a `tools:` line ' +
|
|
383
|
+
'to grant more.',
|
|
310
384
|
});
|
|
311
385
|
}
|
|
312
|
-
|
|
313
|
-
|
|
386
|
+
else {
|
|
387
|
+
if (!meta.description) {
|
|
388
|
+
warnings.push({
|
|
389
|
+
file: full,
|
|
390
|
+
agent: name,
|
|
391
|
+
message: 'has no `description:` — that text is the ONLY thing the model uses to decide when to delegate to this agent, so it will rarely be picked.',
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
if (meta.model !== undefined && parseModel(meta.model) === undefined) {
|
|
395
|
+
warnings.push({
|
|
396
|
+
file: full,
|
|
397
|
+
agent: name,
|
|
398
|
+
message: `has model: "${meta.model}", which is not valid — use one of ${VALID_MODELS.join(', ')}, or omit the line to inherit the current session's model.`,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
// The highest-value warning of the lot. An allowlist only ever GRANTS, so a
|
|
402
|
+
// misspelled name is not an error anywhere — the tool is simply never
|
|
403
|
+
// permitted, and the model is told "Permission denied", which points it at
|
|
404
|
+
// the user rather than at the typo.
|
|
405
|
+
const unknown = (tools ?? []).filter((t) => !KNOWN_TOOL_NAMES.has(t) && !t.includes('__'));
|
|
406
|
+
if (unknown.length) {
|
|
407
|
+
warnings.push({
|
|
408
|
+
file: full,
|
|
409
|
+
agent: name,
|
|
410
|
+
message: `lists unknown tool name(s): ${unknown.join(', ')}. An allowlist only grants, so these silently do nothing and the agent cannot use them. Tool names are lower_snake_case (read_file, search_files, glob, bash).`,
|
|
411
|
+
});
|
|
412
|
+
}
|
|
413
|
+
const strayKeys = Object.keys(meta).filter((k) => !KNOWN_META_KEYS.has(k));
|
|
414
|
+
if (strayKeys.length) {
|
|
415
|
+
warnings.push({
|
|
416
|
+
file: full,
|
|
417
|
+
agent: name,
|
|
418
|
+
message: `has unrecognised frontmatter key(s): ${strayKeys.join(', ')} — these are ignored.`,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
if (!body) {
|
|
422
|
+
warnings.push({
|
|
423
|
+
file: full,
|
|
424
|
+
agent: name,
|
|
425
|
+
message: 'has an empty body — the text below the frontmatter IS the agent\'s system prompt, so it currently has no instructions.',
|
|
426
|
+
});
|
|
427
|
+
}
|
|
314
428
|
}
|
|
429
|
+
into.set(name, {
|
|
430
|
+
name,
|
|
431
|
+
description: meta.description || `Custom ${name} agent`,
|
|
432
|
+
tools: effectiveTools,
|
|
433
|
+
model: parseModel(meta.model),
|
|
434
|
+
prompt: body,
|
|
435
|
+
source,
|
|
436
|
+
// Exposed to user/plugin definitions too — `test_files_only: true` (or
|
|
437
|
+
// `testFilesOnly`) lets anyone build a test-writing agent that genuinely
|
|
438
|
+
// cannot touch production source, rather than only the builtin getting
|
|
439
|
+
// that guarantee.
|
|
440
|
+
...(parseBool(meta.test_files_only ?? meta.testfilesonly) ? { testFilesOnly: true } : {}),
|
|
441
|
+
});
|
|
315
442
|
}
|
|
316
443
|
}
|
|
317
444
|
/**
|
|
@@ -324,11 +451,24 @@ function loadDir(dir, source, into) {
|
|
|
324
451
|
* readdir order is not guaranteed to be stable across machines or platforms.
|
|
325
452
|
*/
|
|
326
453
|
function loadAgentTypes(workDir) {
|
|
454
|
+
return loadAgentTypesWithWarnings(workDir).types;
|
|
455
|
+
}
|
|
456
|
+
/**
|
|
457
|
+
* As `loadAgentTypes`, but also reports what was wrong with the definitions.
|
|
458
|
+
*
|
|
459
|
+
* Split in two so the common caller stays a one-liner while the CLI's `/agents`
|
|
460
|
+
* and the agent loop can surface problems. Warnings never hide an agent: a
|
|
461
|
+
* flawed definition still loads (fail-closed on PERMISSIONS, not on existence),
|
|
462
|
+
* because making a user's agent vanish over a typo is its own kind of silent
|
|
463
|
+
* failure.
|
|
464
|
+
*/
|
|
465
|
+
function loadAgentTypesWithWarnings(workDir) {
|
|
327
466
|
const out = new Map();
|
|
328
|
-
|
|
329
|
-
loadDir(path.join(
|
|
467
|
+
const warnings = [];
|
|
468
|
+
loadDir(path.join(workDir, '.nexrall', 'agents'), 'project', out, warnings);
|
|
469
|
+
loadDir(path.join(os.homedir(), '.nexrall', 'agents'), 'global', out, warnings);
|
|
330
470
|
for (const dir of (0, index_1.pluginAssetDirs)(workDir, 'agents'))
|
|
331
|
-
loadDir(dir, 'plugin', out);
|
|
471
|
+
loadDir(dir, 'plugin', out, warnings);
|
|
332
472
|
for (const agent of BUILTIN_AGENTS) {
|
|
333
473
|
if (!out.has(agent.name))
|
|
334
474
|
out.set(agent.name, agent);
|
|
@@ -336,7 +476,7 @@ function loadAgentTypes(workDir) {
|
|
|
336
476
|
// Builtins first in their declared order (the common, cache-friendly case), then
|
|
337
477
|
// everything user-supplied alphabetically.
|
|
338
478
|
const builtinOrder = new Map(BUILTIN_AGENTS.map((a, i) => [a.name, i]));
|
|
339
|
-
|
|
479
|
+
const types = [...out.values()].sort((a, b) => {
|
|
340
480
|
const ai = builtinOrder.get(a.name);
|
|
341
481
|
const bi = builtinOrder.get(b.name);
|
|
342
482
|
if (ai !== undefined && bi !== undefined)
|
|
@@ -347,6 +487,15 @@ function loadAgentTypes(workDir) {
|
|
|
347
487
|
return 1;
|
|
348
488
|
return a.name.localeCompare(b.name);
|
|
349
489
|
});
|
|
490
|
+
return { types, warnings };
|
|
491
|
+
}
|
|
492
|
+
/** The tool names a hand-written allowlist may use (exported for validation + tests). */
|
|
493
|
+
function knownToolNames() {
|
|
494
|
+
return [...KNOWN_TOOL_NAMES].sort();
|
|
495
|
+
}
|
|
496
|
+
/** The built-in agents, for clients that want to show them alongside user-defined ones. */
|
|
497
|
+
function builtinAgents() {
|
|
498
|
+
return BUILTIN_AGENTS;
|
|
350
499
|
}
|
|
351
500
|
/**
|
|
352
501
|
* One line per agent for the system prompt's <available_subagents> block.
|
package/dist/agent/loop.d.ts
CHANGED
|
@@ -6,6 +6,20 @@ export declare function resolveMaxIterations(optionValue: number | undefined, se
|
|
|
6
6
|
* dozen lines.
|
|
7
7
|
*/
|
|
8
8
|
export declare function createLimiter(max: number): <T>(fn: () => Promise<T>) => Promise<T>;
|
|
9
|
+
/**
|
|
10
|
+
* Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
|
|
11
|
+
* tool — as opposed to the user declining it.
|
|
12
|
+
*
|
|
13
|
+
* The distinction matters to the model, which is why this is an exception rather
|
|
14
|
+
* than a `false`: both used to collapse into "Permission denied by user", so an
|
|
15
|
+
* agent blocked by its own allowlist (very often a mistyped tool name) was told
|
|
16
|
+
* the human had refused. The rational response to that is to ask again, which
|
|
17
|
+
* can never succeed. Carrying a reason lets the tool_result say what is actually
|
|
18
|
+
* true and what to do instead.
|
|
19
|
+
*/
|
|
20
|
+
export declare class ToolNotAllowedError extends Error {
|
|
21
|
+
constructor(message: string);
|
|
22
|
+
}
|
|
9
23
|
/**
|
|
10
24
|
* Reduce a sub-agent's message history to the text its parent should receive.
|
|
11
25
|
*
|
package/dist/agent/loop.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,OAAO,EAMP,gBAAgB,EAChB,UAAU,EACX,MAAM,UAAU,CAAC;AAyKlB,wBAAgB,oBAAoB,CAClC,WAAW,EAAE,MAAM,GAAG,SAAS,EAC/B,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GACnC,MAAM,CAWR;AAkED;;;;GAIG;AACH,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,OAAO,CAAC,CAAC,CAAC,KAAK,OAAO,CAAC,CAAC,CAAC,CAgBlF;
|
|
1
|
+
{"version":3,"file":"loop.d.ts","sourceRoot":"","sources":["../../src/agent/loop.ts"],"names":[],"mappings":"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"}
|
package/dist/agent/loop.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = void 0;
|
|
36
|
+
exports.VERIFY_CMD_RE = exports.WRITE_TOOL_NAMES = exports.ToolNotAllowedError = void 0;
|
|
37
37
|
exports.resolveMaxIterations = resolveMaxIterations;
|
|
38
38
|
exports.createLimiter = createLimiter;
|
|
39
39
|
exports.extractSubTaskText = extractSubTaskText;
|
|
@@ -406,6 +406,24 @@ const SUBTASK_TIMEOUT_MS = Number(process.env.NEXRALL_SUBTASK_TIMEOUT_MS) > 0
|
|
|
406
406
|
/** Cap on the text a sub-task hands back, so one verbose sub-agent can't blow up
|
|
407
407
|
* the PARENT's context in a single tool_result. */
|
|
408
408
|
const SUBTASK_MAX = 48000; // chars (~12k tokens)
|
|
409
|
+
/**
|
|
410
|
+
* Thrown by a sub-agent's permission gate when the AGENT DEFINITION forbids a
|
|
411
|
+
* tool — as opposed to the user declining it.
|
|
412
|
+
*
|
|
413
|
+
* The distinction matters to the model, which is why this is an exception rather
|
|
414
|
+
* than a `false`: both used to collapse into "Permission denied by user", so an
|
|
415
|
+
* agent blocked by its own allowlist (very often a mistyped tool name) was told
|
|
416
|
+
* the human had refused. The rational response to that is to ask again, which
|
|
417
|
+
* can never succeed. Carrying a reason lets the tool_result say what is actually
|
|
418
|
+
* true and what to do instead.
|
|
419
|
+
*/
|
|
420
|
+
class ToolNotAllowedError extends Error {
|
|
421
|
+
constructor(message) {
|
|
422
|
+
super(message);
|
|
423
|
+
this.name = 'ToolNotAllowedError';
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
exports.ToolNotAllowedError = ToolNotAllowedError;
|
|
409
427
|
/**
|
|
410
428
|
* Slice `s` to at most `max` UTF-16 units without splitting a surrogate pair.
|
|
411
429
|
*
|
|
@@ -511,11 +529,32 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
511
529
|
return { error: 'Sub-agents cannot spawn further sub-agents. Do this work directly, or report back so the main agent can delegate it.' };
|
|
512
530
|
}
|
|
513
531
|
// Resolve an optional custom agent type (subagent_type).
|
|
532
|
+
//
|
|
533
|
+
// `agentTypes` is a snapshot taken once at the top of runAgentLoop, before the
|
|
534
|
+
// model said anything. That made "write .nexrall/agents/x.md, then use it"
|
|
535
|
+
// impossible within a single turn: the file existed on disk, but this lookup
|
|
536
|
+
// consulted a list captured before it was written, and the model was told the
|
|
537
|
+
// agent did not exist — which reads as "creating it failed".
|
|
538
|
+
//
|
|
539
|
+
// So on a MISS ONLY, re-read from disk before giving up. The hit path (every
|
|
540
|
+
// normal call) still costs zero syscalls, and the miss path costs ~4 mostly-
|
|
541
|
+
// ENOENT stats — against a sub-agent that is about to run for seconds to
|
|
542
|
+
// minutes. Note runAgentLoop re-reads for the sub-agent anyway, so the old
|
|
543
|
+
// behaviour was already inconsistent: fresh for the child, stale for the lookup.
|
|
514
544
|
const requestedType = typeof input.subagent_type === 'string' ? input.subagent_type : '';
|
|
515
|
-
|
|
545
|
+
let agent = (0, agentTypes_1.findAgentType)(agentTypes, requestedType);
|
|
546
|
+
let knownTypes = agentTypes;
|
|
516
547
|
if (requestedType && !agent) {
|
|
517
|
-
|
|
518
|
-
|
|
548
|
+
knownTypes = (0, agentTypes_1.loadAgentTypes)(options.workDir);
|
|
549
|
+
agent = (0, agentTypes_1.findAgentType)(knownTypes, requestedType);
|
|
550
|
+
}
|
|
551
|
+
if (requestedType && !agent) {
|
|
552
|
+
const known = knownTypes.map((a) => a.name).join(', ') || '(none defined)';
|
|
553
|
+
return {
|
|
554
|
+
error: `Unknown subagent_type "${requestedType}". Available types: ${known}.\n` +
|
|
555
|
+
'If you just created .nexrall/agents/' + requestedType + '.md, make sure the write finished in an ' +
|
|
556
|
+
'EARLIER tool call than this one — a file written in the same batch may not be on disk yet.',
|
|
557
|
+
};
|
|
519
558
|
}
|
|
520
559
|
// A custom agent's persona is delivered through the project-instructions
|
|
521
560
|
// channel (authoritative in the system prompt), layered above the project's
|
|
@@ -525,14 +564,24 @@ async function runSubTask(input, options, agentTypes) {
|
|
|
525
564
|
(options.nexrallMd ? `\n\n---\n\n${options.nexrallMd}` : '')
|
|
526
565
|
: options.nexrallMd;
|
|
527
566
|
// Optional tool allowlist — deny anything outside it for this sub-agent.
|
|
567
|
+
//
|
|
568
|
+
// A refusal here is reported through `deniedReason` rather than the generic
|
|
569
|
+
// "Permission denied by user", which was actively misleading: the user denied
|
|
570
|
+
// nothing, and a model told that will re-ask for approval instead of noticing
|
|
571
|
+
// that the agent's own allowlist (often a typo'd tool name) is what stopped it.
|
|
528
572
|
const allowed = agent?.tools ? new Set(agent.tools) : null;
|
|
529
573
|
const gatedPermission = async (req) => {
|
|
530
|
-
if (allowed && !allowed.has(req.tool))
|
|
531
|
-
|
|
574
|
+
if (allowed && !allowed.has(req.tool)) {
|
|
575
|
+
throw new ToolNotAllowedError(`The "${agent.name}" sub-agent is not allowed to use \`${req.tool}\` — it is not in that agent's ` +
|
|
576
|
+
'tool allowlist. This is a restriction of the agent definition, NOT a user decision: do not ask ' +
|
|
577
|
+
'for approval, use one of the tools you do have, or report back that the task needs a different agent.');
|
|
578
|
+
}
|
|
532
579
|
// Path-scoped write restriction (agent.testFilesOnly) — see
|
|
533
580
|
// allowsTestOnlyWrite for the reasoning and its known limit.
|
|
534
|
-
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input))
|
|
535
|
-
|
|
581
|
+
if (agent?.testFilesOnly && !allowsTestOnlyWrite(req.tool, req.input)) {
|
|
582
|
+
throw new ToolNotAllowedError(`The "${agent.name}" sub-agent may only write to TEST files, so \`${req.tool}\` was refused for this ` +
|
|
583
|
+
'path. Do not try to work around it: if production code must change, say so in your report instead.');
|
|
584
|
+
}
|
|
536
585
|
return options.requestPermission(req);
|
|
537
586
|
};
|
|
538
587
|
const subMessages = [
|
|
@@ -1748,14 +1797,19 @@ async function runAgentLoop(initialMessages, options) {
|
|
|
1748
1797
|
// Request permission
|
|
1749
1798
|
const description = humanDescription(name, input);
|
|
1750
1799
|
let permitted;
|
|
1800
|
+
// A definition-level refusal carries its own explanation and must not be
|
|
1801
|
+
// flattened into the generic user-denial message below.
|
|
1802
|
+
let deniedReason = null;
|
|
1751
1803
|
try {
|
|
1752
1804
|
permitted = await options.requestPermission({ tool: name, input, description });
|
|
1753
1805
|
}
|
|
1754
|
-
catch {
|
|
1806
|
+
catch (err) {
|
|
1755
1807
|
permitted = false;
|
|
1808
|
+
if (err instanceof ToolNotAllowedError)
|
|
1809
|
+
deniedReason = err.message;
|
|
1756
1810
|
}
|
|
1757
1811
|
if (!permitted) {
|
|
1758
|
-
result = { error: 'Permission denied by user' };
|
|
1812
|
+
result = { error: deniedReason ?? 'Permission denied by user' };
|
|
1759
1813
|
}
|
|
1760
1814
|
else if (name === 'task') {
|
|
1761
1815
|
// Gated so a burst of `task` blocks in one message becomes a QUEUE
|
package/dist/index.d.ts
CHANGED
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,uBAAuB,CAAC;AACtC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,gBAAgB,CAAC;AAC/B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -37,4 +37,5 @@ __exportStar(require("./permissions/rules"), exports);
|
|
|
37
37
|
__exportStar(require("./permissions/destructive"), exports);
|
|
38
38
|
__exportStar(require("./plugins/index"), exports);
|
|
39
39
|
__exportStar(require("./plugins/installer"), exports);
|
|
40
|
+
__exportStar(require("./plugins/sources"), exports);
|
|
40
41
|
//# sourceMappingURL=index.js.map
|
package/dist/plugins/index.d.ts
CHANGED
|
@@ -7,6 +7,26 @@ export interface PluginInfo {
|
|
|
7
7
|
dir: string;
|
|
8
8
|
scope: 'project' | 'global';
|
|
9
9
|
}
|
|
10
|
+
/**
|
|
11
|
+
* Where each single-file component may live, in priority order.
|
|
12
|
+
*
|
|
13
|
+
* Our own layout is checked first so a plugin shipping both is unambiguous, and
|
|
14
|
+
* so this stays additive: nothing that worked before changes behaviour.
|
|
15
|
+
*
|
|
16
|
+
* SECURITY: `hooks` and `mcp` here are code-execution surfaces. Every reader
|
|
17
|
+
* below and `inspectPluginDir` in installer.ts MUST resolve through this same
|
|
18
|
+
* table — if the loader reads a path the inspector does not check, a plugin can
|
|
19
|
+
* run commands that the install-time warning never mentioned. Adding a candidate
|
|
20
|
+
* without updating the inspector is exactly that bug, so the two are pinned
|
|
21
|
+
* together by a test.
|
|
22
|
+
*/
|
|
23
|
+
export declare const FILE_CANDIDATES: {
|
|
24
|
+
readonly manifest: readonly ["plugin.json", string];
|
|
25
|
+
readonly hooks: readonly ["hooks.json", string];
|
|
26
|
+
readonly mcp: readonly ["mcp.json", ".mcp.json"];
|
|
27
|
+
};
|
|
28
|
+
/** First existing candidate path for a component, or null. */
|
|
29
|
+
export declare function resolvePluginFile(dir: string, kind: keyof typeof FILE_CANDIDATES): string | null;
|
|
10
30
|
/** Discover installed plugins (project scope shadows global on name clash). */
|
|
11
31
|
export declare function loadPlugins(workDir: string): PluginInfo[];
|
|
12
32
|
/** Subdirectories of every installed plugin that hold `kind` assets (existing only). */
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/plugins/index.ts"],"names":[],"mappings":"AAsCA,MAAM,WAAW,UAAU;IACzB,2DAA2D;IAC3D,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6CAA6C;IAC7C,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,SAAS,GAAG,QAAQ,CAAC;CAC7B;AAED;;;;;;;;;;;;GAYG;AACH,eAAO,MAAM,eAAe;;;;CAIlB,CAAC;AAEX,8DAA8D;AAC9D,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,OAAO,eAAe,GAAG,MAAM,GAAG,IAAI,CAQhG;AAwCD,+EAA+E;AAC/E,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,UAAU,EAAE,CAKzD;AAED,wFAAwF;AACxF,wBAAgB,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,UAAU,GAAG,QAAQ,GAAG,QAAQ,GAAG,MAAM,EAAE,CAMjG;AAED,2FAA2F;AAC3F,wBAAgB,WAAW,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC,CAiBtE;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAezE"}
|
package/dist/plugins/index.js
CHANGED
|
@@ -33,6 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.FILE_CANDIDATES = void 0;
|
|
37
|
+
exports.resolvePluginFile = resolvePluginFile;
|
|
36
38
|
exports.loadPlugins = loadPlugins;
|
|
37
39
|
exports.pluginAssetDirs = pluginAssetDirs;
|
|
38
40
|
exports.pluginHooks = pluginHooks;
|
|
@@ -40,9 +42,42 @@ exports.pluginMcpServers = pluginMcpServers;
|
|
|
40
42
|
const fs = __importStar(require("fs"));
|
|
41
43
|
const path = __importStar(require("path"));
|
|
42
44
|
const os = __importStar(require("os"));
|
|
45
|
+
/**
|
|
46
|
+
* Where each single-file component may live, in priority order.
|
|
47
|
+
*
|
|
48
|
+
* Our own layout is checked first so a plugin shipping both is unambiguous, and
|
|
49
|
+
* so this stays additive: nothing that worked before changes behaviour.
|
|
50
|
+
*
|
|
51
|
+
* SECURITY: `hooks` and `mcp` here are code-execution surfaces. Every reader
|
|
52
|
+
* below and `inspectPluginDir` in installer.ts MUST resolve through this same
|
|
53
|
+
* table — if the loader reads a path the inspector does not check, a plugin can
|
|
54
|
+
* run commands that the install-time warning never mentioned. Adding a candidate
|
|
55
|
+
* without updating the inspector is exactly that bug, so the two are pinned
|
|
56
|
+
* together by a test.
|
|
57
|
+
*/
|
|
58
|
+
exports.FILE_CANDIDATES = {
|
|
59
|
+
manifest: ['plugin.json', path.join('.claude-plugin', 'plugin.json')],
|
|
60
|
+
hooks: ['hooks.json', path.join('hooks', 'hooks.json')],
|
|
61
|
+
mcp: ['mcp.json', '.mcp.json'],
|
|
62
|
+
};
|
|
63
|
+
/** First existing candidate path for a component, or null. */
|
|
64
|
+
function resolvePluginFile(dir, kind) {
|
|
65
|
+
for (const rel of exports.FILE_CANDIDATES[kind]) {
|
|
66
|
+
const full = path.join(dir, rel);
|
|
67
|
+
try {
|
|
68
|
+
if (fs.statSync(full).isFile())
|
|
69
|
+
return full;
|
|
70
|
+
}
|
|
71
|
+
catch { /* try the next candidate */ }
|
|
72
|
+
}
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
43
75
|
function readMeta(dir) {
|
|
76
|
+
const file = resolvePluginFile(dir, 'manifest');
|
|
77
|
+
if (!file)
|
|
78
|
+
return {};
|
|
44
79
|
try {
|
|
45
|
-
const raw = fs.readFileSync(
|
|
80
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
46
81
|
const j = JSON.parse(raw);
|
|
47
82
|
return {
|
|
48
83
|
name: typeof j.name === 'string' ? j.name : undefined,
|
|
@@ -104,8 +139,11 @@ function pluginAssetDirs(workDir, kind) {
|
|
|
104
139
|
function pluginHooks(workDir) {
|
|
105
140
|
const merged = {};
|
|
106
141
|
for (const p of loadPlugins(workDir)) {
|
|
142
|
+
const file = resolvePluginFile(p.dir, 'hooks');
|
|
143
|
+
if (!file)
|
|
144
|
+
continue;
|
|
107
145
|
try {
|
|
108
|
-
const raw = fs.readFileSync(
|
|
146
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
109
147
|
const j = JSON.parse(raw);
|
|
110
148
|
// Accept either { hooks: {...} } or the bare hooks object.
|
|
111
149
|
const hooks = (j.hooks ?? j);
|
|
@@ -123,8 +161,11 @@ function pluginHooks(workDir) {
|
|
|
123
161
|
function pluginMcpServers(workDir) {
|
|
124
162
|
const merged = {};
|
|
125
163
|
for (const p of loadPlugins(workDir)) {
|
|
164
|
+
const file = resolvePluginFile(p.dir, 'mcp');
|
|
165
|
+
if (!file)
|
|
166
|
+
continue;
|
|
126
167
|
try {
|
|
127
|
-
const raw = fs.readFileSync(
|
|
168
|
+
const raw = fs.readFileSync(file, 'utf-8');
|
|
128
169
|
const j = JSON.parse(raw);
|
|
129
170
|
const servers = (j.mcpServers ?? j);
|
|
130
171
|
for (const [name, cfg] of Object.entries(servers)) {
|
|
@@ -26,12 +26,29 @@ export interface InstallReceipt {
|
|
|
26
26
|
ref?: string;
|
|
27
27
|
subdir?: string;
|
|
28
28
|
installedAt: string;
|
|
29
|
+
/**
|
|
30
|
+
* The exact commit these files came from.
|
|
31
|
+
*
|
|
32
|
+
* Without it an install was not reproducible and, worse, not auditable: the
|
|
33
|
+
* receipt recorded `"owner/repo"`, so nobody — including `nex plugin update` —
|
|
34
|
+
* could say WHICH code had been reviewed and approved. Recording the resolved
|
|
35
|
+
* SHA lets an update report `abc1234 → def5678` instead of silently swapping
|
|
36
|
+
* the contents of a plugin the user already trusted.
|
|
37
|
+
*
|
|
38
|
+
* Optional because resolution needs a network call that must never be the
|
|
39
|
+
* reason an install fails (see resolveCommitSha).
|
|
40
|
+
*/
|
|
41
|
+
sha?: string;
|
|
29
42
|
}
|
|
30
43
|
export interface InstallResult {
|
|
31
44
|
name: string;
|
|
32
45
|
dir: string;
|
|
33
46
|
scope: 'project' | 'global';
|
|
34
47
|
inspection: PluginInspection;
|
|
48
|
+
/** Commit installed, when known. */
|
|
49
|
+
sha?: string;
|
|
50
|
+
/** For an update: the commit that was previously installed, when known. */
|
|
51
|
+
previousSha?: string;
|
|
35
52
|
}
|
|
36
53
|
export interface RegistryPlugin {
|
|
37
54
|
name: string;
|
|
@@ -57,7 +74,17 @@ export declare function getRegistryPlugin(name: string): Promise<RegistryPlugin
|
|
|
57
74
|
export declare function reportInstall(name: string): void;
|
|
58
75
|
/** Parse a user-supplied plugin spec into a structured source. Throws on junk. */
|
|
59
76
|
export declare function parsePluginSource(spec: string): PluginSource;
|
|
60
|
-
/**
|
|
77
|
+
/**
|
|
78
|
+
* What does this plugin contain? Callers must warn on hasHooks/hasMcp.
|
|
79
|
+
*
|
|
80
|
+
* Resolves every single-file component through the SAME candidate table the
|
|
81
|
+
* loader uses (plugins/index.ts FILE_CANDIDATES). That shared lookup is a
|
|
82
|
+
* security requirement, not tidiness: this function produces the warning shown
|
|
83
|
+
* before install, so any path the loader would execute but the inspector does
|
|
84
|
+
* not check is a plugin that runs code the user was never told about. A Claude
|
|
85
|
+
* Code plugin shipping `.mcp.json` used to be exactly that — reported as
|
|
86
|
+
* containing no MCP servers.
|
|
87
|
+
*/
|
|
61
88
|
export declare function inspectPluginDir(dir: string): PluginInspection;
|
|
62
89
|
export interface InstallOptions {
|
|
63
90
|
/** 'global' (default) → ~/.nexrall/plugins; 'project' → <workDir>/.nexrall/plugins */
|
|
@@ -71,7 +98,18 @@ export interface InstallOptions {
|
|
|
71
98
|
* Called after inspection, before finalising. Return false to abort.
|
|
72
99
|
* Callers should surface hooks/MCP warnings here.
|
|
73
100
|
*/
|
|
74
|
-
confirm?: (inspection: PluginInspection, name: string) => Promise<boolean> | boolean;
|
|
101
|
+
confirm?: (inspection: PluginInspection, name: string, ctx?: InstallContext) => Promise<boolean> | boolean;
|
|
102
|
+
/** Internal: SHA previously installed, so an update can be described as a change. */
|
|
103
|
+
_previousSha?: string;
|
|
104
|
+
}
|
|
105
|
+
/** Extra facts about what is being installed, for the confirmation prompt. */
|
|
106
|
+
export interface InstallContext {
|
|
107
|
+
/** Commit about to be installed, when it could be resolved. */
|
|
108
|
+
sha?: string;
|
|
109
|
+
/** Commit currently installed (updates only). */
|
|
110
|
+
previousSha?: string;
|
|
111
|
+
/** True when this replaces an existing install of the same name. */
|
|
112
|
+
isUpdate: boolean;
|
|
75
113
|
}
|
|
76
114
|
export declare function installPlugin(spec: string, opts?: InstallOptions): Promise<InstallResult>;
|
|
77
115
|
export declare function readReceipt(pluginDir: string): InstallReceipt | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"installer.d.ts","sourceRoot":"","sources":["../../src/plugins/installer.ts"],"names":[],"mappings":"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"}
|
|
@@ -47,6 +47,8 @@ const path = __importStar(require("path"));
|
|
|
47
47
|
const os = __importStar(require("os"));
|
|
48
48
|
const child_process_1 = require("child_process");
|
|
49
49
|
const client_1 = require("../api/client");
|
|
50
|
+
const index_1 = require("./index");
|
|
51
|
+
const sources_1 = require("./sources");
|
|
50
52
|
const RECEIPT_FILE = '_install.json';
|
|
51
53
|
/** Search the registry (empty query = list all, official first). */
|
|
52
54
|
async function searchRegistry(query, limit = 50) {
|
|
@@ -134,23 +136,36 @@ function listMd(dir) {
|
|
|
134
136
|
return [];
|
|
135
137
|
}
|
|
136
138
|
}
|
|
137
|
-
/**
|
|
139
|
+
/**
|
|
140
|
+
* What does this plugin contain? Callers must warn on hasHooks/hasMcp.
|
|
141
|
+
*
|
|
142
|
+
* Resolves every single-file component through the SAME candidate table the
|
|
143
|
+
* loader uses (plugins/index.ts FILE_CANDIDATES). That shared lookup is a
|
|
144
|
+
* security requirement, not tidiness: this function produces the warning shown
|
|
145
|
+
* before install, so any path the loader would execute but the inspector does
|
|
146
|
+
* not check is a plugin that runs code the user was never told about. A Claude
|
|
147
|
+
* Code plugin shipping `.mcp.json` used to be exactly that — reported as
|
|
148
|
+
* containing no MCP servers.
|
|
149
|
+
*/
|
|
138
150
|
function inspectPluginDir(dir) {
|
|
139
151
|
let meta = {};
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
152
|
+
const manifest = (0, index_1.resolvePluginFile)(dir, 'manifest');
|
|
153
|
+
if (manifest) {
|
|
154
|
+
try {
|
|
155
|
+
const j = JSON.parse(fs.readFileSync(manifest, 'utf-8'));
|
|
156
|
+
meta = {
|
|
157
|
+
name: typeof j.name === 'string' ? j.name : undefined,
|
|
158
|
+
version: typeof j.version === 'string' ? j.version : undefined,
|
|
159
|
+
description: typeof j.description === 'string' ? j.description : undefined,
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
catch { /* optional */ }
|
|
147
163
|
}
|
|
148
|
-
catch { /* optional */ }
|
|
149
164
|
return {
|
|
150
165
|
commands: listMd(path.join(dir, 'commands')),
|
|
151
166
|
agents: listMd(path.join(dir, 'agents')),
|
|
152
|
-
hasHooks:
|
|
153
|
-
hasMcp:
|
|
167
|
+
hasHooks: (0, index_1.resolvePluginFile)(dir, 'hooks') !== null,
|
|
168
|
+
hasMcp: (0, index_1.resolvePluginFile)(dir, 'mcp') !== null,
|
|
154
169
|
meta,
|
|
155
170
|
};
|
|
156
171
|
}
|
|
@@ -160,6 +175,30 @@ function looksLikePlugin(dir) {
|
|
|
160
175
|
return i.commands.length > 0 || i.agents.length > 0 || i.hasHooks || i.hasMcp || i.meta.name !== undefined;
|
|
161
176
|
}
|
|
162
177
|
// ─── Download & extract ───────────────────────────────────────────────────────
|
|
178
|
+
/**
|
|
179
|
+
* Resolve a ref (branch/tag/HEAD/sha) to the commit SHA it points at right now.
|
|
180
|
+
*
|
|
181
|
+
* Deliberately best-effort: returns null on any failure instead of throwing.
|
|
182
|
+
* Pinning is an audit and reproducibility feature, and making it mandatory would
|
|
183
|
+
* mean a GitHub API hiccup or an exhausted unauthenticated rate limit could block
|
|
184
|
+
* an install that would otherwise succeed — trading a real capability for a
|
|
185
|
+
* bookkeeping nicety. An unknown SHA is recorded as absent and said out loud,
|
|
186
|
+
* rather than guessed.
|
|
187
|
+
*/
|
|
188
|
+
async function resolveCommitSha(repo, ref) {
|
|
189
|
+
try {
|
|
190
|
+
const res = await fetch(`https://api.github.com/repos/${repo}/commits/${encodeURIComponent(ref || 'HEAD')}`, {
|
|
191
|
+
headers: { Accept: 'application/vnd.github.sha', 'User-Agent': 'nexrall-code' },
|
|
192
|
+
});
|
|
193
|
+
if (!res.ok)
|
|
194
|
+
return null;
|
|
195
|
+
const sha = (await res.text()).trim();
|
|
196
|
+
return /^[0-9a-f]{40}$/i.test(sha) ? sha : null;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return null;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
163
202
|
async function downloadTarball(repo, ref, destFile) {
|
|
164
203
|
const r = ref || 'HEAD';
|
|
165
204
|
const url = `https://codeload.github.com/${repo}/tar.gz/${encodeURIComponent(r)}`;
|
|
@@ -219,6 +258,16 @@ function pluginsRoot(scope, workDir) {
|
|
|
219
258
|
async function installPlugin(spec, opts = {}) {
|
|
220
259
|
const source = parsePluginSource(spec);
|
|
221
260
|
const scope = opts.scope ?? 'global';
|
|
261
|
+
// Enterprise policy, enforced HERE rather than in each command.
|
|
262
|
+
//
|
|
263
|
+
// Every install path in every client funnels through this function, so this is
|
|
264
|
+
// the one place a restriction cannot be routed around by a caller that forgot
|
|
265
|
+
// to check — the CLI, the VS Code UI and `update` all inherit it for free. A
|
|
266
|
+
// check in the CLI command alone would have been decorative.
|
|
267
|
+
const patterns = (0, sources_1.allowedSourcePatterns)();
|
|
268
|
+
if (patterns && source.kind === 'github' && !(0, sources_1.isSourceAllowed)(source.raw, patterns)) {
|
|
269
|
+
throw new Error(`Blocked by your organisation's policy: plugins may only be installed from ${patterns.join(', ')}.`);
|
|
270
|
+
}
|
|
222
271
|
const tmpBase = fs.mkdtempSync(path.join(os.tmpdir(), 'nexrall-plugin-'));
|
|
223
272
|
try {
|
|
224
273
|
// 1. Materialise the source into tmp
|
|
@@ -234,6 +283,12 @@ async function installPlugin(spec, opts = {}) {
|
|
|
234
283
|
await downloadTarball(source.repo, source.ref, tarFile);
|
|
235
284
|
repoRoot = extractTarball(tarFile, path.join(tmpBase, 'x'));
|
|
236
285
|
}
|
|
286
|
+
// Resolve the commit alongside the download. Note this is a SEPARATE request
|
|
287
|
+
// from the tarball, so in principle the branch could move between the two;
|
|
288
|
+
// the SHA is therefore an audit record of what we asked for, not a checksum
|
|
289
|
+
// of the bytes on disk. Saying that plainly is better than implying a
|
|
290
|
+
// guarantee we do not verify.
|
|
291
|
+
const sha = source.kind === 'github' ? await resolveCommitSha(source.repo, source.ref) : null;
|
|
237
292
|
// 2. Locate the plugin root
|
|
238
293
|
let pluginRoot = source.subdir ? path.join(repoRoot, source.subdir) : repoRoot;
|
|
239
294
|
pluginRoot = path.resolve(pluginRoot);
|
|
@@ -264,7 +319,11 @@ async function installPlugin(spec, opts = {}) {
|
|
|
264
319
|
throw new Error(`Invalid plugin name "${name}".`);
|
|
265
320
|
// 4. Caller confirmation (safety gate — hooks/MCP warnings live here)
|
|
266
321
|
if (opts.confirm) {
|
|
267
|
-
const ok = await opts.confirm(inspection, name
|
|
322
|
+
const ok = await opts.confirm(inspection, name, {
|
|
323
|
+
sha: sha ?? undefined,
|
|
324
|
+
previousSha: opts._previousSha,
|
|
325
|
+
isUpdate: Boolean(opts._previousSha) || Boolean(opts.force),
|
|
326
|
+
});
|
|
268
327
|
if (!ok)
|
|
269
328
|
throw new Error('Installation cancelled.');
|
|
270
329
|
}
|
|
@@ -284,9 +343,10 @@ async function installPlugin(spec, opts = {}) {
|
|
|
284
343
|
ref: source.ref,
|
|
285
344
|
subdir: source.subdir,
|
|
286
345
|
installedAt: new Date().toISOString(),
|
|
346
|
+
...(sha ? { sha } : {}),
|
|
287
347
|
};
|
|
288
348
|
fs.writeFileSync(path.join(dest, RECEIPT_FILE), JSON.stringify(receipt, null, 2));
|
|
289
|
-
return { name, dir: dest, scope, inspection };
|
|
349
|
+
return { name, dir: dest, scope, inspection, sha: sha ?? undefined, previousSha: opts._previousSha };
|
|
290
350
|
}
|
|
291
351
|
finally {
|
|
292
352
|
fs.rmSync(tmpBase, { recursive: true, force: true });
|
|
@@ -328,7 +388,18 @@ async function updatePlugin(name, opts = {}) {
|
|
|
328
388
|
if (receipt.kind === 'local') {
|
|
329
389
|
throw new Error(`Plugin "${name}" was installed from a local path — re-run 'nex plugin install ${receipt.source} --force'.`);
|
|
330
390
|
}
|
|
331
|
-
|
|
391
|
+
// Re-installs from the ORIGINAL spec, so an unpinned source still tracks its
|
|
392
|
+
// branch — that is what "update" means. The recorded SHA is carried through so
|
|
393
|
+
// the confirmation can show what is actually changing rather than presenting a
|
|
394
|
+
// possibly wholesale content swap as a routine refresh.
|
|
395
|
+
return installPlugin(receipt.source, {
|
|
396
|
+
scope: s,
|
|
397
|
+
workDir: opts.workDir,
|
|
398
|
+
name,
|
|
399
|
+
force: true,
|
|
400
|
+
confirm: opts.confirm,
|
|
401
|
+
_previousSha: receipt.sha,
|
|
402
|
+
});
|
|
332
403
|
}
|
|
333
404
|
throw new Error(`Plugin "${name}" is not installed.`);
|
|
334
405
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/** Where a declared source came from, which decides how much it is trusted. */
|
|
2
|
+
export type SourceTier = 'managed' | 'user' | 'project';
|
|
3
|
+
export interface DeclaredSource {
|
|
4
|
+
/** Short alias used as `nex plugin install <alias>`. */
|
|
5
|
+
name: string;
|
|
6
|
+
/** A spec parsePluginSource understands: owner/repo, owner/repo#ref, owner/repo/subdir. */
|
|
7
|
+
spec: string;
|
|
8
|
+
tier: SourceTier;
|
|
9
|
+
/** Absolute path of the settings file that declared it (for display/debugging). */
|
|
10
|
+
file: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* All declared sources, most-trusted tier first.
|
|
14
|
+
*
|
|
15
|
+
* `includeProject` is false by default. Project-tier entries come from the
|
|
16
|
+
* repository, so a caller must consciously opt in AFTER workspace trust has been
|
|
17
|
+
* established — making the safe path the default one and the risky path an
|
|
18
|
+
* explicit argument.
|
|
19
|
+
*/
|
|
20
|
+
export declare function loadDeclaredSources(workDir: string, opts?: {
|
|
21
|
+
includeProject?: boolean;
|
|
22
|
+
}): DeclaredSource[];
|
|
23
|
+
/**
|
|
24
|
+
* Enterprise restriction: which sources may be installed from at all.
|
|
25
|
+
*
|
|
26
|
+
* Read ONLY from the managed policy file — a repository or a user cannot widen
|
|
27
|
+
* their own allowlist, which is the entire point of a managed setting. Absent
|
|
28
|
+
* (the normal case) means unrestricted.
|
|
29
|
+
*/
|
|
30
|
+
export declare function allowedSourcePatterns(): string[] | null;
|
|
31
|
+
/**
|
|
32
|
+
* Is this spec permitted by enterprise policy?
|
|
33
|
+
*
|
|
34
|
+
* Patterns match `owner/repo` with a trailing `*` wildcard, e.g. `acme/*`. The
|
|
35
|
+
* comparison strips any `#ref` and subdirectory so a policy cannot be defeated
|
|
36
|
+
* by appending to the spec.
|
|
37
|
+
*/
|
|
38
|
+
export declare function isSourceAllowed(spec: string, patterns?: string[] | null): boolean;
|
|
39
|
+
/** Resolve an alias to its declared spec, or null when it is not a known alias. */
|
|
40
|
+
export declare function resolveSourceAlias(alias: string, workDir: string, opts?: {
|
|
41
|
+
includeProject?: boolean;
|
|
42
|
+
}): DeclaredSource | null;
|
|
43
|
+
/** Add or update a source in a settings file, creating it when absent. */
|
|
44
|
+
export declare function writeDeclaredSource(file: string, name: string, spec: string): void;
|
|
45
|
+
/** Remove a source from a settings file. Returns false when it was not there. */
|
|
46
|
+
export declare function removeDeclaredSource(file: string, name: string): boolean;
|
|
47
|
+
/** The settings file a given scope writes to. */
|
|
48
|
+
export declare function sourcesFileFor(scope: 'user' | 'project', workDir: string): string;
|
|
49
|
+
//# sourceMappingURL=sources.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sources.d.ts","sourceRoot":"","sources":["../../src/plugins/sources.ts"],"names":[],"mappings":"AA0CA,+EAA+E;AAC/E,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,MAAM,GAAG,SAAS,CAAC;AAExD,MAAM,WAAW,cAAc;IAC7B,wDAAwD;IACxD,IAAI,EAAE,MAAM,CAAC;IACb,2FAA2F;IAC3F,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,UAAU,CAAC;IACjB,mFAAmF;IACnF,IAAI,EAAE,MAAM,CAAC;CACd;AA0DD;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,cAAc,EAAE,CASlB;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,IAAI,MAAM,EAAE,GAAG,IAAI,CAMvD;AAED;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,GAAE,MAAM,EAAE,GAAG,IAA8B,GAAG,OAAO,CAU1G;AAED,mFAAmF;AACnF,wBAAgB,kBAAkB,CAChC,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,GAAE;IAAE,cAAc,CAAC,EAAE,OAAO,CAAA;CAAO,GACtC,cAAc,GAAG,IAAI,CAGvB;AAED,0EAA0E;AAC1E,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX,IAAI,CAWN;AAED,iFAAiF;AACjF,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAQxE;AAED,iDAAiD;AACjD,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAIjF"}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.loadDeclaredSources = loadDeclaredSources;
|
|
37
|
+
exports.allowedSourcePatterns = allowedSourcePatterns;
|
|
38
|
+
exports.isSourceAllowed = isSourceAllowed;
|
|
39
|
+
exports.resolveSourceAlias = resolveSourceAlias;
|
|
40
|
+
exports.writeDeclaredSource = writeDeclaredSource;
|
|
41
|
+
exports.removeDeclaredSource = removeDeclaredSource;
|
|
42
|
+
exports.sourcesFileFor = sourcesFileFor;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const os = __importStar(require("os"));
|
|
46
|
+
function managedSettingsPath() {
|
|
47
|
+
if (process.platform === 'darwin')
|
|
48
|
+
return '/Library/Application Support/Nexrall/managed-settings.json';
|
|
49
|
+
if (process.platform === 'win32')
|
|
50
|
+
return path.join(process.env.PROGRAMDATA ?? 'C:\\ProgramData', 'Nexrall', 'managed-settings.json');
|
|
51
|
+
return '/etc/nexrall/managed-settings.json';
|
|
52
|
+
}
|
|
53
|
+
function readJson(file) {
|
|
54
|
+
try {
|
|
55
|
+
return JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Alias syntax. Kept strict so an alias can never be confused with a path or a ref. */
|
|
62
|
+
const ALIAS_RE = /^[a-z0-9][\w.-]{0,63}$/i;
|
|
63
|
+
/**
|
|
64
|
+
* A source entry may be a bare string or `{ source: "..." }`.
|
|
65
|
+
*
|
|
66
|
+
* The object form exists only to match what people already write in Claude Code
|
|
67
|
+
* settings, so a team migrating does not have to rewrite their config to find
|
|
68
|
+
* out whether we accept it.
|
|
69
|
+
*/
|
|
70
|
+
function readSpec(value) {
|
|
71
|
+
if (typeof value === 'string')
|
|
72
|
+
return value.trim() || null;
|
|
73
|
+
if (value && typeof value === 'object') {
|
|
74
|
+
const v = value;
|
|
75
|
+
const inner = v.source ?? v.repo ?? v.url;
|
|
76
|
+
if (typeof inner === 'string')
|
|
77
|
+
return inner.trim() || null;
|
|
78
|
+
// Claude Code nests one deeper: { source: { source: "github", repo: "o/r" } }
|
|
79
|
+
if (inner && typeof inner === 'object') {
|
|
80
|
+
const deep = inner.repo ?? inner.url;
|
|
81
|
+
if (typeof deep === 'string')
|
|
82
|
+
return deep.trim() || null;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
87
|
+
function collect(file, tier, into) {
|
|
88
|
+
const obj = readJson(file);
|
|
89
|
+
const raw = obj?.pluginSources;
|
|
90
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
91
|
+
return;
|
|
92
|
+
for (const [name, value] of Object.entries(raw)) {
|
|
93
|
+
if (!ALIAS_RE.test(name))
|
|
94
|
+
continue;
|
|
95
|
+
const spec = readSpec(value);
|
|
96
|
+
if (!spec)
|
|
97
|
+
continue;
|
|
98
|
+
// First tier to declare a name wins, and tiers are visited most-trusted
|
|
99
|
+
// first. So a repository cannot redefine an alias the user or an
|
|
100
|
+
// administrator already bound — otherwise a project file could point a
|
|
101
|
+
// familiar, trusted-looking name at somewhere else entirely.
|
|
102
|
+
if (into.has(name))
|
|
103
|
+
continue;
|
|
104
|
+
into.set(name, { name, spec, tier, file });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* All declared sources, most-trusted tier first.
|
|
109
|
+
*
|
|
110
|
+
* `includeProject` is false by default. Project-tier entries come from the
|
|
111
|
+
* repository, so a caller must consciously opt in AFTER workspace trust has been
|
|
112
|
+
* established — making the safe path the default one and the risky path an
|
|
113
|
+
* explicit argument.
|
|
114
|
+
*/
|
|
115
|
+
function loadDeclaredSources(workDir, opts = {}) {
|
|
116
|
+
const out = new Map();
|
|
117
|
+
collect(managedSettingsPath(), 'managed', out);
|
|
118
|
+
collect(path.join(os.homedir(), '.nexrall', 'settings.json'), 'user', out);
|
|
119
|
+
if (opts.includeProject) {
|
|
120
|
+
collect(path.join(workDir, '.nexrall', 'settings.json'), 'project', out);
|
|
121
|
+
collect(path.join(workDir, '.nexrall', 'settings.local.json'), 'project', out);
|
|
122
|
+
}
|
|
123
|
+
return [...out.values()];
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Enterprise restriction: which sources may be installed from at all.
|
|
127
|
+
*
|
|
128
|
+
* Read ONLY from the managed policy file — a repository or a user cannot widen
|
|
129
|
+
* their own allowlist, which is the entire point of a managed setting. Absent
|
|
130
|
+
* (the normal case) means unrestricted.
|
|
131
|
+
*/
|
|
132
|
+
function allowedSourcePatterns() {
|
|
133
|
+
const obj = readJson(managedSettingsPath());
|
|
134
|
+
const v = obj?.allowedPluginSources;
|
|
135
|
+
if (!Array.isArray(v))
|
|
136
|
+
return null;
|
|
137
|
+
const pats = v.filter((x) => typeof x === 'string' && x.trim().length > 0);
|
|
138
|
+
return pats.length ? pats : null;
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Is this spec permitted by enterprise policy?
|
|
142
|
+
*
|
|
143
|
+
* Patterns match `owner/repo` with a trailing `*` wildcard, e.g. `acme/*`. The
|
|
144
|
+
* comparison strips any `#ref` and subdirectory so a policy cannot be defeated
|
|
145
|
+
* by appending to the spec.
|
|
146
|
+
*/
|
|
147
|
+
function isSourceAllowed(spec, patterns = allowedSourcePatterns()) {
|
|
148
|
+
if (!patterns)
|
|
149
|
+
return true;
|
|
150
|
+
const cleaned = spec.trim().replace(/^https?:\/\/(?:www\.)?github\.com\//i, '').replace(/\.git$/, '');
|
|
151
|
+
const base = cleaned.split('#')[0].split('/').slice(0, 2).join('/').toLowerCase();
|
|
152
|
+
return patterns.some((p) => {
|
|
153
|
+
const pat = p.trim().toLowerCase().replace(/^https?:\/\/(?:www\.)?github\.com\//i, '');
|
|
154
|
+
if (pat === '*')
|
|
155
|
+
return true;
|
|
156
|
+
if (pat.endsWith('/*'))
|
|
157
|
+
return base.startsWith(pat.slice(0, -1));
|
|
158
|
+
return base === pat.split('#')[0].split('/').slice(0, 2).join('/');
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/** Resolve an alias to its declared spec, or null when it is not a known alias. */
|
|
162
|
+
function resolveSourceAlias(alias, workDir, opts = {}) {
|
|
163
|
+
const want = alias.trim().toLowerCase();
|
|
164
|
+
return loadDeclaredSources(workDir, opts).find((s) => s.name.toLowerCase() === want) ?? null;
|
|
165
|
+
}
|
|
166
|
+
/** Add or update a source in a settings file, creating it when absent. */
|
|
167
|
+
function writeDeclaredSource(file, name, spec) {
|
|
168
|
+
if (!ALIAS_RE.test(name)) {
|
|
169
|
+
throw new Error(`Invalid source name "${name}". Use letters, numbers, dot, dash or underscore.`);
|
|
170
|
+
}
|
|
171
|
+
const obj = readJson(file) ?? {};
|
|
172
|
+
const existing = (obj.pluginSources && typeof obj.pluginSources === 'object' && !Array.isArray(obj.pluginSources))
|
|
173
|
+
? obj.pluginSources
|
|
174
|
+
: {};
|
|
175
|
+
obj.pluginSources = { ...existing, [name]: spec };
|
|
176
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
177
|
+
fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
178
|
+
}
|
|
179
|
+
/** Remove a source from a settings file. Returns false when it was not there. */
|
|
180
|
+
function removeDeclaredSource(file, name) {
|
|
181
|
+
const obj = readJson(file);
|
|
182
|
+
if (!obj?.pluginSources || typeof obj.pluginSources !== 'object')
|
|
183
|
+
return false;
|
|
184
|
+
const sources = obj.pluginSources;
|
|
185
|
+
if (!(name in sources))
|
|
186
|
+
return false;
|
|
187
|
+
delete sources[name];
|
|
188
|
+
fs.writeFileSync(file, JSON.stringify(obj, null, 2) + '\n', 'utf-8');
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
/** The settings file a given scope writes to. */
|
|
192
|
+
function sourcesFileFor(scope, workDir) {
|
|
193
|
+
return scope === 'user'
|
|
194
|
+
? path.join(os.homedir(), '.nexrall', 'settings.json')
|
|
195
|
+
: path.join(workDir, '.nexrall', 'settings.json');
|
|
196
|
+
}
|
|
197
|
+
//# sourceMappingURL=sources.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexrall/code-core",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.25",
|
|
4
4
|
"description": "Core agent loop, tools, and extension primitives for Nexrall Code — embed an AI coding agent in any Node.js application.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Nexrall <support@nexrall.com> (https://nexrall.com)",
|