@nexrall/code-core 1.4.24 → 1.4.26

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.
@@ -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":"AAuBA,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;AAsCD,+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,CAetE;AAED,iGAAiG;AACjG,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAazE"}
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"}
@@ -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(path.join(dir, 'plugin.json'), 'utf-8');
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(path.join(p.dir, 'hooks.json'), 'utf-8');
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(path.join(p.dir, 'mcp.json'), 'utf-8');
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
- /** What does this plugin contain? Callers must warn on hasHooks/hasMcp. */
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":"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"}
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
- /** What does this plugin contain? Callers must warn on hasHooks/hasMcp. */
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
- try {
141
- const j = JSON.parse(fs.readFileSync(path.join(dir, 'plugin.json'), 'utf-8'));
142
- meta = {
143
- name: typeof j.name === 'string' ? j.name : undefined,
144
- version: typeof j.version === 'string' ? j.version : undefined,
145
- description: typeof j.description === 'string' ? j.description : undefined,
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: fs.existsSync(path.join(dir, 'hooks.json')),
153
- hasMcp: fs.existsSync(path.join(dir, 'mcp.json')),
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
- return installPlugin(receipt.source, { scope: s, workDir: opts.workDir, name, force: true, confirm: opts.confirm });
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/dist/types.d.ts CHANGED
@@ -321,6 +321,15 @@ export interface AgentLoopOptions {
321
321
  env?: EnvContext;
322
322
  editorContext?: EditorContext | null;
323
323
  nexrallMd?: string;
324
+ /**
325
+ * Session-wide read-only lock ("plan mode"). When true the loop refuses every
326
+ * mutating tool BEFORE requestPermission is consulted, so no user answer at a
327
+ * permission prompt can unlock it — only leaving plan mode can.
328
+ *
329
+ * Inherited by every sub-agent: a lock the main agent can delegate its way
330
+ * out of is not a lock.
331
+ */
332
+ planMode?: boolean;
324
333
  /** Client type — controls which tools Claude is told about. */
325
334
  clientType?: string;
326
335
  /**