@nexrall/code-core 1.4.23 → 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 +66 -2
- package/dist/agent/agentTypes.d.ts.map +1 -1
- package/dist/agent/agentTypes.js +415 -31
- package/dist/agent/loop.d.ts +34 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +117 -8
- package/dist/agent/securityLint.d.ts +27 -0
- package/dist/agent/securityLint.d.ts.map +1 -0
- package/dist/agent/securityLint.js +195 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -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/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +24 -4
- package/package.json +1 -1
|
@@ -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
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAiuEtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,GACjC,OAAO,CAAC,UAAU,CAAC,CAoBrB"}
|
package/dist/tools/executor.js
CHANGED
|
@@ -44,6 +44,7 @@ const child_process_1 = require("child_process");
|
|
|
44
44
|
const sandbox_1 = require("./sandbox");
|
|
45
45
|
const auth_1 = require("../auth");
|
|
46
46
|
const editCompleteness_1 = require("../agent/editCompleteness");
|
|
47
|
+
const securityLint_1 = require("../agent/securityLint");
|
|
47
48
|
const memory_1 = require("../agent/memory");
|
|
48
49
|
const skills_1 = require("../agent/skills");
|
|
49
50
|
const testIntegrity_1 = require("../agent/testIntegrity");
|
|
@@ -413,8 +414,16 @@ async function writeFile(input, workDir) {
|
|
|
413
414
|
atomicWrite(resolved, content, existingMode);
|
|
414
415
|
const bytes = Buffer.byteLength(content, 'utf-8');
|
|
415
416
|
const lines = content.split('\n').length;
|
|
417
|
+
// Inline security review of what was just written.
|
|
418
|
+
//
|
|
419
|
+
// Six gates already run automatically on every write (elision, cross-file
|
|
420
|
+
// breakage, test integrity, claim evidence, flaky tests, destructive commands)
|
|
421
|
+
// and none of them was about security — so an agent that writes production code
|
|
422
|
+
// could commit a hardcoded credential or an injection sink with nothing to say
|
|
423
|
+
// otherwise. This warns; it never blocks (see securityLint.ts for why).
|
|
424
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(content));
|
|
416
425
|
if (isNew) {
|
|
417
|
-
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)` };
|
|
426
|
+
return { output: `Created ${resolved} (${lines} lines, ${bytes} bytes)${sec}` };
|
|
418
427
|
}
|
|
419
428
|
const xfile = crossFileBreakageWarning(resolved, normalizeLF(priorContent), normalizeLF(content), workDir ?? process.cwd());
|
|
420
429
|
// Reward-hacking guard: a write_file that OVERWRITES an existing test file can
|
|
@@ -426,7 +435,7 @@ async function writeFile(input, workDir) {
|
|
|
426
435
|
const ti = (0, testIntegrity_1.analyzeTestEdit)(resolved, normalizeLF(priorContent), normalizeLF(content));
|
|
427
436
|
if (ti.suspicious)
|
|
428
437
|
tiMarker = (0, testIntegrity_1.encodeTestIntegrityMarker)(ti.findings);
|
|
429
|
-
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${tiMarker}` };
|
|
438
|
+
return { output: `Overwrote ${resolved} (${lines} lines, ${bytes} bytes)${xfile}${sec}${tiMarker}` };
|
|
430
439
|
}
|
|
431
440
|
catch (err) {
|
|
432
441
|
return { error: err.message };
|
|
@@ -1408,7 +1417,12 @@ async function editFile(input, workDir) {
|
|
|
1408
1417
|
const linesBefore = origNorm.split('\n').length;
|
|
1409
1418
|
const linesAfter = updated.split('\n').length;
|
|
1410
1419
|
const xfile = crossFileBreakageWarning(resolved, origNorm, normalizeLF(updated), workDir ?? process.cwd());
|
|
1411
|
-
|
|
1420
|
+
// Security-lint only the NEWLY INSERTED text, not the whole file. Scanning the
|
|
1421
|
+
// full file would re-report pre-existing findings on every unrelated edit —
|
|
1422
|
+
// noise that has nothing to do with the change being made, and the fastest way
|
|
1423
|
+
// to teach everyone to ignore the warning.
|
|
1424
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(newNorm));
|
|
1425
|
+
return { output: `Edited ${resolved} (${linesBefore} → ${linesAfter} lines)\n\n${diff}${xfile}${sec}` };
|
|
1412
1426
|
}
|
|
1413
1427
|
catch (err) {
|
|
1414
1428
|
return { error: err.message };
|
|
@@ -1747,6 +1761,9 @@ async function multiEdit(input, workDir) {
|
|
|
1747
1761
|
let content = normalizeLF(rawFile);
|
|
1748
1762
|
const originalNorm = content; // captured before edits mutate `content`
|
|
1749
1763
|
const diffs = [];
|
|
1764
|
+
// Inserted text from every edit, collected for the security lint below so it
|
|
1765
|
+
// scans only what this call ADDED rather than the whole file.
|
|
1766
|
+
const insertedText = [];
|
|
1750
1767
|
for (let i = 0; i < edits.length; i++) {
|
|
1751
1768
|
const edit = edits[i];
|
|
1752
1769
|
// Normalize incoming old/new strings too
|
|
@@ -1766,6 +1783,7 @@ async function multiEdit(input, workDir) {
|
|
|
1766
1783
|
// Pass the pre-replace content so line numbers reflect the file state at
|
|
1767
1784
|
// the point this edit was applied (not the final state after all edits).
|
|
1768
1785
|
diffs.push(buildDiff(filePath, oldStr, newStr, content));
|
|
1786
|
+
insertedText.push(newStr);
|
|
1769
1787
|
content = literalReplace(content, oldStr, newStr);
|
|
1770
1788
|
}
|
|
1771
1789
|
// Restore original line endings before writing (atomic temp+rename),
|
|
@@ -1773,9 +1791,11 @@ async function multiEdit(input, workDir) {
|
|
|
1773
1791
|
const finalContent = wasCRLF ? content.replace(/\n/g, '\r\n') : content;
|
|
1774
1792
|
atomicWritePreservingMode(resolved, finalContent, pre.mode);
|
|
1775
1793
|
const xfile = crossFileBreakageWarning(resolved, originalNorm, content, workDir ?? process.cwd());
|
|
1794
|
+
// Lint only the newly inserted text (see the same reasoning in editFile).
|
|
1795
|
+
const sec = (0, securityLint_1.securityNoteText)((0, securityLint_1.checkSecurity)(insertedText.join('\n')));
|
|
1776
1796
|
return {
|
|
1777
1797
|
output: `Applied ${edits.length} edit(s) to ${resolved}:\n` +
|
|
1778
|
-
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile,
|
|
1798
|
+
diffs.map((d, i) => `\n--- edit #${i + 1} ---\n${d}`).join('\n') + xfile + sec,
|
|
1779
1799
|
};
|
|
1780
1800
|
}
|
|
1781
1801
|
catch (err) {
|
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)",
|