@xingular/harness 0.1.0 → 0.1.2

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/cli.js CHANGED
@@ -24,7 +24,7 @@ function log(...parts) {
24
24
 
25
25
  // src/harness.ts
26
26
  import { mkdirSync as mkdirSync14 } from "node:fs";
27
- import { join as join38 } from "node:path";
27
+ import { join as join39 } from "node:path";
28
28
 
29
29
  // src/accounts/accounts.ts
30
30
  import { chmodSync as chmodSync3, rmSync as rmSync2 } from "node:fs";
@@ -133,9 +133,9 @@ function newId() {
133
133
  }
134
134
 
135
135
  // src/accounts/env.ts
136
- import { chmodSync, lstatSync, mkdirSync as mkdirSync2, readlinkSync, renameSync as renameSync2, symlinkSync, unlinkSync } from "node:fs";
136
+ import { chmodSync, copyFileSync, linkSync, lstatSync, mkdirSync as mkdirSync2, readlinkSync, renameSync as renameSync2, rmdirSync, statSync, symlinkSync, unlinkSync, utimesSync } from "node:fs";
137
137
  import { homedir } from "node:os";
138
- import { basename as basename2, dirname as dirname2, join as join2, sep as sep2 } from "node:path";
138
+ import { basename as basename2, dirname as dirname2, join as join2, resolve, sep as sep2 } from "node:path";
139
139
  var DIR_VARS = ["CODEX_HOME", "CLAUDE_CONFIG_DIR", "PI_CODING_AGENT_DIR"];
140
140
  function isCredentialVar(name) {
141
141
  const upper = name.toUpperCase();
@@ -197,43 +197,124 @@ var modified = (path3) => {
197
197
  }
198
198
  };
199
199
  function backUp(path3) {
200
- renameSync2(path3, join2(dirname2(path3), `${basename2(path3)}.agentx-backup-${Math.floor(Date.now() / 1e3)}`));
200
+ const backup2 = join2(dirname2(path3), `${basename2(path3)}.agentx-backup-${Math.floor(Date.now() / 1e3)}`);
201
+ renameSync2(path3, backup2);
202
+ return backup2;
201
203
  }
202
- function linkShared(accountsDir, dir) {
203
- const shared = ensureSharedDir(accountsDir);
204
- for (const [name, isDir2] of SHARED_ENTRIES) {
205
- const local = join2(dir, name);
206
- const target = join2(shared, name);
207
- let meta = null;
208
- try {
209
- meta = lstatSync(local);
210
- } catch {
211
- meta = null;
212
- }
213
- if (meta?.isSymbolicLink()) {
214
- if (readlinkSync(local) === target) {
215
- if (isDir2) mkdirSync2(target, { recursive: true });
216
- continue;
204
+ var nodeLinker = {
205
+ symlink: (target, path3, isDir2) => symlinkSync(resolve(target), path3, isDir2 ? "junction" : "file"),
206
+ hardLink: (target, path3) => linkSync(target, path3)
207
+ };
208
+ var codeOf = (error) => error.code ?? String(error);
209
+ var PRIVILEGE_CODES = ["EPERM", "EACCES"];
210
+ function createLink(target, path3, isDir2, linker) {
211
+ try {
212
+ linker.symlink(target, path3, isDir2);
213
+ } catch (error) {
214
+ if (isDir2 || !PRIVILEGE_CODES.includes(codeOf(error))) throw error;
215
+ linker.hardLink(target, path3);
216
+ }
217
+ }
218
+ var isLink = (path3) => {
219
+ try {
220
+ return lstatSync(path3).isSymbolicLink();
221
+ } catch {
222
+ return false;
223
+ }
224
+ };
225
+ var LONG_PREFIX = /^\\\\\?\\/;
226
+ function samePath(a, b) {
227
+ const normal = (path3) => {
228
+ const full = resolve(path3.replace(LONG_PREFIX, ""));
229
+ return process.platform === "win32" ? full.toLowerCase() : full;
230
+ };
231
+ return normal(a) === normal(b);
232
+ }
233
+ function sameFile(a, b) {
234
+ try {
235
+ const left = lstatSync(a);
236
+ const right = lstatSync(b);
237
+ return left.ino !== 0 && left.dev === right.dev && left.ino === right.ino;
238
+ } catch {
239
+ return false;
240
+ }
241
+ }
242
+ function isLinkTo(path3, target, isDir2) {
243
+ try {
244
+ const meta = lstatSync(path3);
245
+ if (meta.isSymbolicLink()) return samePath(readlinkSync(path3), target);
246
+ return !isDir2 && meta.isFile() && sameFile(path3, target);
247
+ } catch {
248
+ return false;
249
+ }
250
+ }
251
+ function removeLink(path3, isDir2) {
252
+ try {
253
+ unlinkSync(path3);
254
+ } catch (error) {
255
+ if (!isDir2) throw error;
256
+ rmdirSync(path3);
257
+ }
258
+ }
259
+ function keepCopy(target, path3, isDir2) {
260
+ if (isDir2 || present(path3) || !present(target)) return;
261
+ try {
262
+ copyFileSync(target, path3);
263
+ const { atime, mtime } = statSync(target);
264
+ utimesSync(path3, atime, mtime);
265
+ } catch {
266
+ }
267
+ }
268
+ function linkOne(name, target, path3, isDir2, linker) {
269
+ if (isLinkTo(path3, target, isDir2)) {
270
+ if (isDir2) mkdirSync2(target, { recursive: true });
271
+ return { name, state: "linked" };
272
+ }
273
+ const undo2 = [];
274
+ try {
275
+ if (isLink(path3)) {
276
+ const previous = readlinkSync(path3);
277
+ removeLink(path3, isDir2);
278
+ undo2.push(() => nodeLinker.symlink(previous, path3, isDir2));
279
+ } else if (present(path3)) {
280
+ if (!present(target)) {
281
+ renameSync2(path3, target);
282
+ undo2.push(() => renameSync2(target, path3));
283
+ } else if (!isDir2 && modified(path3) > modified(target)) {
284
+ const backup2 = backUp(target);
285
+ undo2.push(() => renameSync2(backup2, target));
286
+ renameSync2(path3, target);
287
+ undo2.push(() => renameSync2(target, path3));
288
+ } else {
289
+ const backup2 = backUp(path3);
290
+ undo2.push(() => renameSync2(backup2, path3));
217
291
  }
218
- unlinkSync(local);
219
- } else if (meta) {
220
- if (!present(target)) renameSync2(local, target);
221
- else if (!isDir2 && modified(local) > modified(target)) {
222
- backUp(target);
223
- renameSync2(local, target);
224
- } else backUp(local);
225
292
  }
226
293
  if (isDir2) mkdirSync2(target, { recursive: true });
227
- symlinkSync(target, local);
294
+ createLink(target, path3, isDir2, linker);
295
+ return { name, state: "linked" };
296
+ } catch (error) {
297
+ for (const step of undo2.reverse()) {
298
+ try {
299
+ step();
300
+ } catch {
301
+ }
302
+ }
303
+ keepCopy(target, path3, isDir2);
304
+ return { name, state: "unshared", reason: codeOf(error) };
228
305
  }
229
306
  }
230
- function ensureHome(accountsDir, account) {
307
+ function linkSharedEntries(accountsDir, account, linker = nodeLinker) {
231
308
  makePrivate(join2(accountsDir, account.id));
232
309
  makePrivate(homeDir(accountsDir, account.id));
233
310
  const dir = configDir(accountsDir, account);
234
311
  makePrivate(dir);
235
- linkShared(accountsDir, dir);
236
- return dir;
312
+ const shared = ensureSharedDir(accountsDir);
313
+ const entries2 = SHARED_ENTRIES.map(([name, isDir2]) => linkOne(name, join2(shared, name), join2(dir, name), isDir2, linker));
314
+ return { dir, entries: entries2 };
315
+ }
316
+ function ensureHome(accountsDir, account) {
317
+ return linkSharedEntries(accountsDir, account).dir;
237
318
  }
238
319
  var inside = (path3, parent) => path3 === parent || path3.startsWith(parent.endsWith(sep2) ? parent : `${parent}${sep2}`);
239
320
  function assertIsolated(accountsDir, dir) {
@@ -242,16 +323,21 @@ function assertIsolated(accountsDir, dir) {
242
323
  throw new HarnessError("account_required", "A session cannot start outside its isolated account folder.");
243
324
  }
244
325
  }
245
- function launchEnv(accountsDir, account, inherited = process.env) {
246
- const dir = ensureHome(accountsDir, account);
247
- assertIsolated(accountsDir, dir);
326
+ function sessionEnv(dir, inherited) {
248
327
  const env = {};
249
328
  for (const name of inheritedCredentialVars(inherited)) env[name] = null;
250
329
  env.PI_CODING_AGENT_DIR = dir;
251
330
  return env;
252
331
  }
332
+ function launchEnv(accountsDir, account, inherited = process.env) {
333
+ const dir = ensureHome(accountsDir, account);
334
+ assertIsolated(accountsDir, dir);
335
+ return sessionEnv(dir, inherited);
336
+ }
253
337
  function accountContext(accountsDir, account) {
254
- return { id: account.id, provider: "pi", label: account.label, env: launchEnv(accountsDir, account), configDir: configDir(accountsDir, account) };
338
+ const { dir, entries: entries2 } = linkSharedEntries(accountsDir, account);
339
+ assertIsolated(accountsDir, dir);
340
+ return { id: account.id, provider: "pi", label: account.label, env: sessionEnv(dir, process.env), configDir: dir, sharing: entries2 };
255
341
  }
256
342
 
257
343
  // src/accounts/secrets.ts
@@ -406,7 +492,7 @@ function find(dir, accountId) {
406
492
  return account;
407
493
  }
408
494
  var usable = (state2) => state2.health === "ok" || state2.health === "unknown";
409
- function resolve(dir, accountId, poolId) {
495
+ function resolve2(dir, accountId, poolId) {
410
496
  const registry = loadRegistry(dir);
411
497
  if (accountId) {
412
498
  const account2 = find(dir, accountId);
@@ -432,7 +518,7 @@ function resolve(dir, accountId, poolId) {
432
518
  }
433
519
  function defaultAccount(dir) {
434
520
  try {
435
- return resolve(dir);
521
+ return resolve2(dir);
436
522
  } catch {
437
523
  return null;
438
524
  }
@@ -545,7 +631,7 @@ var Accounts = class {
545
631
  }
546
632
  /** The account a new session uses; a session that already has one passes it and keeps it. */
547
633
  resolve(accountId, poolId) {
548
- return resolve(this.dir, accountId, poolId);
634
+ return resolve2(this.dir, accountId, poolId);
549
635
  }
550
636
  context(account) {
551
637
  return accountContext(this.dir, account);
@@ -801,9 +887,9 @@ var Accounts = class {
801
887
  const interaction = {
802
888
  signal: controller.signal,
803
889
  notify: (notification) => event(notification),
804
- prompt: (prompt2) => new Promise((resolve8, reject) => {
890
+ prompt: (prompt2) => new Promise((resolve9, reject) => {
805
891
  const promptId = randomUUID2();
806
- login.prompts.set(promptId, { resolve: resolve8, reject });
892
+ login.prompts.set(promptId, { resolve: resolve9, reject });
807
893
  prompt2.signal?.addEventListener("abort", () => {
808
894
  login.prompts.delete(promptId);
809
895
  event({ type: "promptClosed", promptId });
@@ -922,7 +1008,7 @@ var Accounts = class {
922
1008
  };
923
1009
 
924
1010
  // src/plugins/api.ts
925
- import { join as join27 } from "node:path";
1011
+ import { join as join28 } from "node:path";
926
1012
 
927
1013
  // src/core/projects.ts
928
1014
  import { existsSync } from "node:fs";
@@ -1048,13 +1134,24 @@ function forget(attachments, scope) {
1048
1134
  if (scope.agentId) delete attachments.agents[scope.agentId];
1049
1135
  if (scope.projectId) delete attachments.projects[scope.projectId];
1050
1136
  }
1137
+ function forgetId(attachments, id) {
1138
+ const forgetFrom = (item) => {
1139
+ item.on = item.on.filter((entry) => entry !== id);
1140
+ item.off = item.off.filter((entry) => entry !== id);
1141
+ };
1142
+ forgetFrom(attachments.global);
1143
+ for (const item of Object.values(attachments.agents)) forgetFrom(item);
1144
+ for (const item of Object.values(attachments.projects)) forgetFrom(item);
1145
+ attachments.agents = Object.fromEntries(Object.entries(attachments.agents).filter(([, item]) => item.on.length || item.off.length));
1146
+ attachments.projects = Object.fromEntries(Object.entries(attachments.projects).filter(([, item]) => item.on.length || item.off.length));
1147
+ }
1051
1148
 
1052
1149
  // src/prompt/compile.ts
1053
- import { readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync2 } from "node:fs";
1150
+ import { readdirSync as readdirSync6, readFileSync as readFileSync11, statSync as statSync3 } from "node:fs";
1054
1151
  import { join as join18 } from "node:path";
1055
1152
 
1056
1153
  // src/core/index.ts
1057
- import { copyFileSync, existsSync as existsSync2, readdirSync, rmSync as rmSync3 } from "node:fs";
1154
+ import { copyFileSync as copyFileSync2, existsSync as existsSync2, readdirSync, rmSync as rmSync3 } from "node:fs";
1058
1155
  import { join as join7 } from "node:path";
1059
1156
  import { DatabaseSync } from "node:sqlite";
1060
1157
  var RECALL_SCHEMA = `CREATE TABLE recall_runs (run_id TEXT PRIMARY KEY, passages INTEGER NOT NULL, status TEXT NOT NULL);
@@ -1141,14 +1238,14 @@ function migrate(path3, migrations) {
1141
1238
  throw sqlError(error);
1142
1239
  }
1143
1240
  if (current >= migrations.length) return database;
1144
- const backup = `${path3.replace(/\.sqlite$/, "")}.sqlite.bak-v${current}`;
1241
+ const backup2 = `${path3.replace(/\.sqlite$/, "")}.sqlite.bak-v${current}`;
1145
1242
  const hadFile = existsSync2(path3) && current > 0;
1146
1243
  if (hadFile) {
1147
1244
  try {
1148
1245
  database.exec("PRAGMA wal_checkpoint(TRUNCATE);");
1149
1246
  } catch {
1150
1247
  }
1151
- copyFileSync(path3, backup);
1248
+ copyFileSync2(path3, backup2);
1152
1249
  }
1153
1250
  for (let index = current; index < migrations.length; index += 1) {
1154
1251
  try {
@@ -1158,7 +1255,7 @@ function migrate(path3, migrations) {
1158
1255
  });
1159
1256
  } catch (error) {
1160
1257
  database.close();
1161
- if (hadFile) copyFileSync(backup, path3);
1258
+ if (hadFile) copyFileSync2(backup2, path3);
1162
1259
  else rmSync3(path3, { force: true });
1163
1260
  throw new HarnessError("migration_failed", `Index migration ${index + 1} failed and the previous index was restored: ${error instanceof Error ? error.message : String(error)}`);
1164
1261
  }
@@ -1280,7 +1377,7 @@ import { join as join9 } from "node:path";
1280
1377
 
1281
1378
  // src/memory/store.ts
1282
1379
  import { createHash } from "node:crypto";
1283
- import { existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync } from "node:fs";
1380
+ import { existsSync as existsSync3, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync3, statSync as statSync2 } from "node:fs";
1284
1381
  import { join as join8, relative } from "node:path";
1285
1382
  import { stringify as stringify2 } from "yaml";
1286
1383
 
@@ -1453,7 +1550,7 @@ function markdownFiles(directory, into) {
1453
1550
  const path3 = join8(directory, name);
1454
1551
  let directoryEntry = false;
1455
1552
  try {
1456
- directoryEntry = statSync(path3).isDirectory();
1553
+ directoryEntry = statSync2(path3).isDirectory();
1457
1554
  } catch {
1458
1555
  continue;
1459
1556
  }
@@ -1463,14 +1560,14 @@ function markdownFiles(directory, into) {
1463
1560
  }
1464
1561
  function isFile(path3) {
1465
1562
  try {
1466
- return statSync(path3).isFile();
1563
+ return statSync2(path3).isFile();
1467
1564
  } catch {
1468
1565
  return false;
1469
1566
  }
1470
1567
  }
1471
1568
  function isDir(path3) {
1472
1569
  try {
1473
- return statSync(path3).isDirectory();
1570
+ return statSync2(path3).isDirectory();
1474
1571
  } catch {
1475
1572
  return false;
1476
1573
  }
@@ -1802,7 +1899,7 @@ function editRefusal(caller, block2) {
1802
1899
  if (block2.scope === "project") return block2.owner === caller.projectSlug ? null : "A session may edit only its own project's brief.";
1803
1900
  return `A Harness session can read the ${label} block but cannot edit it. Report what you learned in your result instead.`;
1804
1901
  }
1805
- function resolve2(root, caller, rawLabel) {
1902
+ function resolve3(root, caller, rawLabel) {
1806
1903
  const wiki = wikiDir(root);
1807
1904
  const label = rawLabel.trim().replace(/^\/+/, "");
1808
1905
  const unknown = () => invalid(`There is no memory block called ${label}. Use one of the labels shown in your memory blocks.`);
@@ -1887,7 +1984,7 @@ function write(wiki, block2, front, body2, actor, reason) {
1887
1984
  }
1888
1985
  function edit(root, caller, label, change) {
1889
1986
  const wiki = wikiDir(root);
1890
- const block2 = resolve2(root, caller, label);
1987
+ const block2 = resolve3(root, caller, label);
1891
1988
  const refusal = editRefusal(caller, block2);
1892
1989
  if (refusal) throw invalid(refusal);
1893
1990
  const body2 = applyEdit(block2.body, change);
@@ -2181,6 +2278,14 @@ import { join as join14 } from "node:path";
2181
2278
  import { lstatSync as lstatSync2, mkdirSync as mkdirSync7, mkdtempSync, readdirSync as readdirSync4, readFileSync as readFileSync7, renameSync as renameSync4, rmSync as rmSync4, unlinkSync as unlinkSync2, writeFileSync } from "node:fs";
2182
2279
  import { basename as basename3, dirname as dirname4, join as join12 } from "node:path";
2183
2280
  var KINDS = ["skills", "prompts", "rules"];
2281
+ function contributionId(item) {
2282
+ return `${item.builtin ? "included" : "yours"}/${item.id}`;
2283
+ }
2284
+ function limitToContributions(items, contributions2) {
2285
+ if (contributions2 === void 0) return [...items];
2286
+ const allowed2 = new Set(contributions2);
2287
+ return items.filter((item) => allowed2.has(contributionId(item)));
2288
+ }
2184
2289
  var MAX_SKILL_BUNDLES = 20;
2185
2290
  var MAX_SKILL_FILES = 100;
2186
2291
  var MAX_SKILL_IMPORT_BYTES = 10 * 1024 * 1024;
@@ -2254,7 +2359,8 @@ function enablementIds(map, key) {
2254
2359
  }
2255
2360
  var BUILTIN_OFF_KEY = "builtinOff";
2256
2361
  var isItemId = (id) => KINDS.some((kind) => id.startsWith(`${kind}/`));
2257
- function enabledFor(root, projectId, agentId = null) {
2362
+ function enabledFor(root, projectId, agentId = null, contributions2) {
2363
+ if (contributions2 !== void 0) return limitToContributions(list(root), contributions2);
2258
2364
  const choices = load2(root);
2259
2365
  return list(root).filter((item) => attached(choices, [item.id], { agentId, projectId }, !!item.builtin));
2260
2366
  }
@@ -2266,8 +2372,8 @@ function itemBody(item) {
2266
2372
  return "";
2267
2373
  }
2268
2374
  }
2269
- function resources(root, projectId, agentId = null) {
2270
- const items = enabledFor(root, projectId, agentId);
2375
+ function resources(root, projectId, agentId = null, contributions2) {
2376
+ const items = enabledFor(root, projectId, agentId, contributions2);
2271
2377
  return {
2272
2378
  ids: items.map((item) => item.id),
2273
2379
  skillPaths: items.filter((item) => item.kind === "skills").map((item) => item.path),
@@ -3238,7 +3344,7 @@ function cite(wiki, id) {
3238
3344
  }
3239
3345
 
3240
3346
  // src/sessions/runStore.ts
3241
- import { appendFileSync, copyFileSync as copyFileSync2, existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync10 } from "node:fs";
3347
+ import { appendFileSync, copyFileSync as copyFileSync3, existsSync as existsSync7, mkdirSync as mkdirSync8, readFileSync as readFileSync10 } from "node:fs";
3242
3348
  import { join as join17 } from "node:path";
3243
3349
  function runDir(root, runId) {
3244
3350
  return join17(root, "runs", safeId(runId));
@@ -3384,8 +3490,8 @@ function markInterruptedRuns(root, reason = STOPPED) {
3384
3490
  const status = terminalStatus(settled2);
3385
3491
  const settledReason = reasonOf(settled2);
3386
3492
  if (record3.status !== status || record3.endedAt !== settled2.timestamp || record3.terminalReason !== settledReason) {
3387
- const backup = `${path3}.before-outcome-repair`;
3388
- if (!existsSync7(backup)) copyFileSync2(path3, backup);
3493
+ const backup2 = `${path3}.before-outcome-repair`;
3494
+ if (!existsSync7(backup2)) copyFileSync3(path3, backup2);
3389
3495
  Object.assign(record3, { status, endedAt: settled2.timestamp, terminalReason: settledReason });
3390
3496
  write2(root, record3);
3391
3497
  }
@@ -3716,10 +3822,10 @@ function hostSection(host, kept) {
3716
3822
  return section(host.id, host.title, own ? `${host.pinned.text}
3717
3823
  ${own.body}` : host.pinned.text, host.budget, host.pinned.pointer ?? host.pointer ?? "", []);
3718
3824
  }
3719
- function compile(env, context, launch = {}) {
3825
+ function compile(env, context, launch = {}, contributions2) {
3720
3826
  const { root } = env;
3721
3827
  const wiki = wikiDir(root);
3722
- const active = enabledFor(root, context.projectId ?? null, context.agentId ?? null);
3828
+ const active = enabledFor(root, context.projectId ?? null, context.agentId ?? null, contributions2);
3723
3829
  const groups = toolGroups(context, launch);
3724
3830
  const contextProfile = profileFor(root, context);
3725
3831
  const keep = (id) => id === "references" || keeps(contextProfile, id);
@@ -3832,7 +3938,7 @@ function sessionFile(sessionDir, resume) {
3832
3938
  const files = names.filter((name) => name.endsWith(".jsonl") && name.length > ".jsonl".length).flatMap((name) => {
3833
3939
  const path3 = join18(sessionDir, name);
3834
3940
  try {
3835
- return [{ path: path3, modified: statSync2(path3).mtimeMs }];
3941
+ return [{ path: path3, modified: statSync3(path3).mtimeMs }];
3836
3942
  } catch {
3837
3943
  return [];
3838
3944
  }
@@ -3844,8 +3950,8 @@ function sameSection(left, right) {
3844
3950
  if (!left || !right) return !left && !right;
3845
3951
  return left.id === right.id && left.title === right.title && left.body === right.body && left.budget === right.budget && left.size === right.size && left.cut === right.cut && left.sources.length === right.sources.length && left.sources.every((source2, index) => source2.id === right.sources[index].id && source2.kind === right.sources[index].kind && (source2.revision ?? null) === (right.sources[index].revision ?? null));
3846
3952
  }
3847
- function sessionLaunch(env, context, launch, historyFile) {
3848
- const pack = compile(env, context, launch);
3953
+ function sessionLaunch(env, context, launch, historyFile, contributions2) {
3954
+ const pack = compile(env, context, launch, contributions2);
3849
3955
  const dynamic = [...BUILT_IN_DYNAMIC, ...(launch.sections ?? []).filter((host) => host.dynamic).map((host) => ({ id: host.id, order: host.order }))].map((slot, index) => ({ ...slot, index })).sort((left, right) => left.order - right.order || left.index - right.index).map((slot) => slot.id);
3850
3956
  const previous = historyFile ? deliveredSnapshot(historyFile) : null;
3851
3957
  const snapshot3 = pack.sections.filter((entry) => dynamic.includes(entry.id));
@@ -4443,6 +4549,30 @@ import { join as join21, sep as sep3 } from "node:path";
4443
4549
 
4444
4550
  // src/plugins/manifest.ts
4445
4551
  import { basename as basename4, join as join20 } from "node:path";
4552
+ import { z as z3 } from "zod";
4553
+ var connectorTemplateSchema = z3.object({
4554
+ id: z3.string().regex(/^[a-z0-9-]+$/),
4555
+ name: z3.string().min(1).max(120),
4556
+ description: z3.string().max(500),
4557
+ url: z3.string().url().startsWith("https://"),
4558
+ auth: z3.enum(["none", "headers", "oauth"]),
4559
+ oauthClient: z3.enum(["dynamic", "user"]).nullable(),
4560
+ oauthScopes: z3.array(z3.string().max(500)).max(30),
4561
+ docsUrl: z3.string().url().startsWith("https://").nullable(),
4562
+ fields: z3.array(z3.object({
4563
+ header: z3.string().regex(/^[A-Za-z0-9_-]+$/),
4564
+ label: z3.string().min(1).max(120),
4565
+ prefix: z3.string().max(100),
4566
+ help: z3.string().max(500)
4567
+ }).strict()).max(12),
4568
+ setup: z3.array(z3.string().max(1e3)).max(20),
4569
+ notice: z3.string().max(1e3).nullable()
4570
+ }).strict().superRefine((template, context) => {
4571
+ const oauth = template.auth === "oauth";
4572
+ const headers = template.auth === "headers";
4573
+ if (oauth !== (template.oauthClient !== null)) context.addIssue({ code: "custom", message: "OAuth templates must state a client type and other templates must not.", path: ["oauthClient"] });
4574
+ if (headers !== template.fields.length > 0) context.addIssue({ code: "custom", message: "Header templates must declare input fields and other templates must not.", path: ["fields"] });
4575
+ });
4446
4576
  function inside2(path3) {
4447
4577
  if (typeof path3 !== "string" || !path3.trim()) return null;
4448
4578
  try {
@@ -4459,7 +4589,11 @@ function packageManifest(packageRoot) {
4459
4589
  description: typeof manifest.description === "string" ? manifest.description : "",
4460
4590
  version: typeof manifest.version === "string" ? manifest.version : null,
4461
4591
  extends: (Array.isArray(own.extends) ? own.extends : typeof own.extends === "string" ? [own.extends] : []).filter((id) => typeof id === "string" && !!id),
4462
- hooks: inside2(own.hooks)
4592
+ hooks: inside2(own.hooks),
4593
+ connectorTemplates: (Array.isArray(own.connectors) ? own.connectors : []).flatMap((value) => {
4594
+ const parsed2 = connectorTemplateSchema.safeParse(value);
4595
+ return parsed2.success ? [parsed2.data] : [];
4596
+ })
4463
4597
  };
4464
4598
  }
4465
4599
 
@@ -4488,14 +4622,14 @@ var piResolver = async ({ cwd, agentDir, install: install3, packages }) => {
4488
4622
  const settings = pi.SettingsManager.create(cwd, agentDir);
4489
4623
  const shared = new Set((settings.getGlobalSettings().packages ?? []).map((entry) => typeof entry === "string" ? entry : entry.source));
4490
4624
  const manager2 = () => new pi.DefaultPackageManager({ cwd, agentDir, settingsManager: settings, builtinExtensions: [...PI_BUILTINS] });
4491
- const resolve8 = (installing) => manager2().resolve(async (source2) => installing && shared.has(source2) ? "install" : "skip");
4625
+ const resolve9 = (installing) => manager2().resolve(async (source2) => installing && shared.has(source2) ? "install" : "skip");
4492
4626
  let resolved2;
4493
4627
  try {
4494
- resolved2 = install3 ? await withoutInstallScripts(() => resolve8(true)) : await resolve8(false);
4628
+ resolved2 = install3 ? await withoutInstallScripts(() => resolve9(true)) : await resolve9(false);
4495
4629
  } catch (error) {
4496
4630
  if (!install3) throw error;
4497
4631
  log("a plugin package could not be installed", error instanceof Error ? error.message : String(error));
4498
- resolved2 = await resolve8(false);
4632
+ resolved2 = await resolve9(false);
4499
4633
  }
4500
4634
  const found = [];
4501
4635
  const add = (paths) => {
@@ -4547,8 +4681,8 @@ var piResolver = async ({ cwd, agentDir, install: install3, packages }) => {
4547
4681
 
4548
4682
  // src/plugins/runtime.ts
4549
4683
  import { createHash as createHash2 } from "node:crypto";
4550
- import { readFileSync as readFileSync12, readdirSync as readdirSync7, statSync as statSync3 } from "node:fs";
4551
- import { dirname as dirname7, resolve as resolve3 } from "node:path";
4684
+ import { readFileSync as readFileSync12, readdirSync as readdirSync7, statSync as statSync4 } from "node:fs";
4685
+ import { dirname as dirname7, resolve as resolve4 } from "node:path";
4552
4686
 
4553
4687
  // src/plugins/identity.ts
4554
4688
  import { basename as basename5, dirname as dirname6 } from "node:path";
@@ -4566,10 +4700,10 @@ function describeFound(item) {
4566
4700
  const described = describeFile(item.path);
4567
4701
  return { name: described?.name ?? file, description: described?.description ?? "" };
4568
4702
  }
4569
- var contributionId = (pluginId, kind, name) => `${pluginId}/${kind}/${name}`;
4703
+ var contributionId2 = (pluginId, kind, name) => `${pluginId}/${kind}/${name}`;
4570
4704
  function foundIds(item) {
4571
4705
  const pluginId = pluginIdOf(item);
4572
- return [contributionId(pluginId, FOUND_KINDS[item.kind], describeFound(item).name), pluginId];
4706
+ return [contributionId2(pluginId, FOUND_KINDS[item.kind], describeFound(item).name), pluginId];
4573
4707
  }
4574
4708
 
4575
4709
  // src/plugins/runtime.ts
@@ -4620,21 +4754,21 @@ function observeResources(pi, loader, loading, records, publish) {
4620
4754
  collected.push(resource);
4621
4755
  return loading.resourceAllowed?.({ ...owner, kind, path: resourcePath }) !== false;
4622
4756
  };
4623
- const absolute = (entry) => resolve3(dirname7(path3), entry);
4757
+ const absolute = (entry) => resolve4(dirname7(path3), entry);
4624
4758
  const skills2 = pi.loadSkills({ cwd: dirname7(path3), agentDir: dirname7(path3), includeDefaults: false, skillPaths: (result.skillPaths ?? []).map(absolute) }).skills;
4625
4759
  const scanner = new pi.DefaultResourceLoader({ cwd: dirname7(path3), agentDir: dirname7(path3), settingsManager: pi.SettingsManager.inMemory({}), noExtensions: true, noSkills: true, noThemes: true, noContextFiles: true, noPromptTemplates: true, additionalPromptTemplatePaths: (result.promptPaths ?? []).map(absolute) });
4626
4760
  await scanner.reload();
4627
4761
  const prompts = scanner.getPrompts().prompts;
4628
4762
  const themes = (result.themePaths ?? []).map(absolute).flatMap((theme) => {
4629
4763
  try {
4630
- if (statSync3(theme).isDirectory()) return readdirSync7(theme).filter((name) => {
4764
+ if (statSync4(theme).isDirectory()) return readdirSync7(theme).filter((name) => {
4631
4765
  if (!name.endsWith(".json")) return false;
4632
4766
  try {
4633
- return statSync3(resolve3(theme, name)).isFile();
4767
+ return statSync4(resolve4(theme, name)).isFile();
4634
4768
  } catch {
4635
4769
  return false;
4636
4770
  }
4637
- }).map((name) => resolve3(theme, name));
4771
+ }).map((name) => resolve4(theme, name));
4638
4772
  } catch {
4639
4773
  }
4640
4774
  return [theme];
@@ -4800,8 +4934,9 @@ var Plugins = class {
4800
4934
  if (item.kind === "hook") return !attached(choices, [pluginIdOf(item)], {}, true);
4801
4935
  return !attached(choices, foundIds(item), input.caller ?? {}, true);
4802
4936
  };
4803
- const detached = found.filter(off).map((item) => item.path);
4804
- const chosen = found.filter((item) => loads(item, trusted) && !detached.includes(item.path));
4937
+ const allowed2 = (item) => input.contributions === void 0 ? !off(item) : input.contributions.includes(foundIds(item)[0]);
4938
+ const detached = found.filter((item) => !allowed2(item)).map((item) => item.path);
4939
+ const chosen = found.filter((item) => loads(item, trusted) && allowed2(item));
4805
4940
  const paths = (kind, builtin) => chosen.filter((item) => item.kind === kind && item.origin === "builtin" === builtin).map((item) => item.path);
4806
4941
  return {
4807
4942
  extensionPaths: paths("extension", false),
@@ -4812,7 +4947,7 @@ var Plugins = class {
4812
4947
  hooks: chosen.filter((item) => item.kind === "hook" && item.packageRoot).map((item) => ({ path: item.path, packageRoot: item.packageRoot, hash: packageCodeState[item.packageRoot]?.hash ?? "" })),
4813
4948
  found,
4814
4949
  detached,
4815
- resourceAllowed: (item) => !off(item),
4950
+ resourceAllowed: allowed2,
4816
4951
  projectCode: projectCodeState,
4817
4952
  packageCode: packageCodeState
4818
4953
  };
@@ -4842,7 +4977,7 @@ var Plugins = class {
4842
4977
  // src/plugins/catalogue.ts
4843
4978
  var REGISTRY_KINDS = { rules: "instruction", skills: "skill", prompts: "command" };
4844
4979
  var contribution = (plugin, kind, name, rest = {}) => ({
4845
- id: contributionId(plugin, kind, name),
4980
+ id: contributionId2(plugin, kind, name),
4846
4981
  kind,
4847
4982
  name,
4848
4983
  description: "",
@@ -4873,6 +5008,7 @@ function fromDescriptor(descriptor, source2, attachments = null, caller = {}) {
4873
5008
  required: descriptor.required ?? null,
4874
5009
  manage: descriptor.manage ?? (descriptor.tools?.length ? "tools" : null),
4875
5010
  settings: (descriptor.settings ?? []).map((setting) => ({ ...setting, value: settingValue(setting, attachments?.settings[descriptor.id]) })),
5011
+ connectorTemplates: descriptor.connectorTemplates ?? [],
4876
5012
  attachment: switchable2 ? describe(attachments, descriptor.id, switchable2.on) : null,
4877
5013
  contributions: [
4878
5014
  ...(descriptor.code ?? []).map((code) => contribution(descriptor.id, "code", code.name, { description: code.description, state: on ? "on" : "off" })),
@@ -4934,7 +5070,7 @@ function registeredLine(entry) {
4934
5070
  function foundContribution(plugin, item, trusted, input, projects) {
4935
5071
  const kind = FOUND_KINDS[item.kind];
4936
5072
  const { name, description } = describeFound(item);
4937
- const id = contributionId(plugin, kind, name);
5073
+ const id = contributionId2(plugin, kind, name);
4938
5074
  const attachment = switchable(item) && item.kind !== "hook" ? describe(input.attachments, id, attached(input.attachments, [plugin], {}, true)) : null;
4939
5075
  const on = item.kind === "hook" ? attached(input.attachments, [plugin], {}, true) : !attachment || attached(input.attachments, [id, plugin], input.caller, true);
4940
5076
  const state2 = !item.enabled || !on ? "off" : loads(item, trusted) ? "on" : "untrusted";
@@ -4955,7 +5091,7 @@ function foundContribution(plugin, item, trusted, input, projects) {
4955
5091
  if (item.kind === "context") return contribution(plugin, kind, name, { ...rest, description: CONTEXT_NOTES[basename7(item.path)] ?? "Read by Pi at the start of every session in this folder.", path: item.path });
4956
5092
  return contribution(plugin, kind, name, { ...rest, description: description ?? "", path: item.path });
4957
5093
  }
4958
- var empty = (id, title, description, source2, path3) => ({ id, title, description, version: null, source: source2, path: path3, runsCode: false, trust: "none", code: null, install: null, extends: [], required: null, manage: "files", settings: [], attachment: null, contributions: [] });
5094
+ var empty = (id, title, description, source2, path3) => ({ id, title, description, version: null, source: source2, path: path3, runsCode: false, trust: "none", code: null, install: null, extends: [], required: null, manage: "files", settings: [], connectorTemplates: [], attachment: null, contributions: [] });
4959
5095
  function catalogue(input) {
4960
5096
  const plugins = /* @__PURE__ */ new Map();
4961
5097
  for (const descriptor of input.descriptors) plugins.set(descriptor.id, fromDescriptor(descriptor, descriptor.id === "pi" ? "pi" : descriptor.id === YOURS_PLUGIN.id ? "yours" : input.application.has(descriptor.id) ? "application" : "builtin", input.attachments, input.caller));
@@ -4986,7 +5122,7 @@ function catalogue(input) {
4986
5122
  const manifest = packageManifest(item.packageRoot);
4987
5123
  const install3 = item.project ? null : item.packageRoot.startsWith(`${input.packagesDir}/`) ? { kind: "folder", source: item.packageRoot } : item.source ? { kind: "settings", source: item.source } : null;
4988
5124
  const extended = manifest.extends.filter((part) => input.descriptors.some((descriptor) => descriptor.id === part));
4989
- plugin = { ...empty(id, manifest.name, manifest.description, item.project ? "project" : "package", item.packageRoot), version: manifest.version, install: install3, extends: extended };
5125
+ plugin = { ...empty(id, manifest.name, manifest.description, item.project ? "project" : "package", item.packageRoot), version: manifest.version, install: install3, extends: extended, connectorTemplates: manifest.connectorTemplates };
4990
5126
  } else if (item.origin === "home") plugin = empty(id, "Home folder skills", "Skills in the .agents folder of your home folder. Pi reads them for every account.", "home", dirname9(dirname9(item.path)));
4991
5127
  else if (item.origin === "project") plugin = empty(id, "Project folder", "Found in the working folder: its .pi and .agents folders, and the instruction files Pi reads on the way up from it.", "project", input.cwd);
4992
5128
  else plugin = empty(id, "Shared Pi folder", "Extensions, skills, prompts and instruction files in the folder every account shares.", "shared", input.sharedDir);
@@ -4999,13 +5135,13 @@ function catalogue(input) {
4999
5135
  if (item.kind === "extension" && added.registered) {
5000
5136
  const registered = added.registered;
5001
5137
  const append = (kind, name, rest = {}) => {
5002
- const id2 = contributionId(plugin.id, kind, name);
5138
+ const id2 = contributionId2(plugin.id, kind, name);
5003
5139
  if (!plugin.contributions.some((existing) => existing.id === id2)) plugin.contributions.push(contribution(plugin.id, kind, name, { state: added.state, description: `Registered by ${added.name}. Observed ${registered.seenAt ?? "in a session"}.`, ...rest }));
5004
5140
  };
5005
5141
  for (const name of registered.tools) append("tool", name);
5006
5142
  for (const name of registered.commands) append("command", name);
5007
5143
  for (const resource of registered.resources ?? []) {
5008
- const id2 = contributionId(plugin.id, resource.kind, resource.name);
5144
+ const id2 = contributionId2(plugin.id, resource.kind, resource.name);
5009
5145
  const on = attached(input.attachments, [id2, plugin.id], input.caller, true);
5010
5146
  append(resource.kind, resource.name, { path: resource.path, state: on ? added.state : "off", attachment: describe(input.attachments, id2, attached(input.attachments, [plugin.id], {}, true)) });
5011
5147
  }
@@ -5024,15 +5160,15 @@ function catalogue(input) {
5024
5160
  }
5025
5161
 
5026
5162
  // src/plugins/inventory.ts
5027
- import { readFileSync as readFileSync14 } from "node:fs";
5163
+ import { readFileSync as readFileSync15 } from "node:fs";
5028
5164
 
5029
5165
  // src/mcp/connections.ts
5030
- import { chmodSync as chmodSync6 } from "node:fs";
5031
- import { isAbsolute as isAbsolute3, join as join26 } from "node:path";
5166
+ import { chmodSync as chmodSync7 } from "node:fs";
5167
+ import { isAbsolute as isAbsolute3, join as join27 } from "node:path";
5032
5168
 
5033
5169
  // src/core/path.ts
5034
5170
  import { execFileSync } from "node:child_process";
5035
- import { accessSync, constants, statSync as statSync4 } from "node:fs";
5171
+ import { accessSync, constants, statSync as statSync5 } from "node:fs";
5036
5172
  import { homedir as homedir3 } from "node:os";
5037
5173
  import { delimiter, isAbsolute as isAbsolute2, join as join25 } from "node:path";
5038
5174
  var resolved = null;
@@ -5054,7 +5190,7 @@ function searchPath() {
5054
5190
  function isExecutable(path3) {
5055
5191
  try {
5056
5192
  accessSync(path3, constants.X_OK);
5057
- return statSync4(path3).isFile();
5193
+ return statSync5(path3).isFile();
5058
5194
  } catch {
5059
5195
  return false;
5060
5196
  }
@@ -5068,51 +5204,212 @@ function findBinary(name) {
5068
5204
  return null;
5069
5205
  }
5070
5206
 
5207
+ // src/mcp/scopeMigration.ts
5208
+ import { chmodSync as chmodSync6, existsSync as existsSync10, readFileSync as readFileSync14, unlinkSync as unlinkSync3 } from "node:fs";
5209
+ import { join as join26 } from "node:path";
5210
+
5071
5211
  // src/mcp/types.ts
5072
- import { z as z3 } from "zod";
5212
+ import { z as z4 } from "zod";
5073
5213
  var MCP_OAUTH_PORT = 47823;
5074
5214
  var MCP_OAUTH_REDIRECT_URL = `http://127.0.0.1:${MCP_OAUTH_PORT}/oauth/callback`;
5075
- var connectionSchema = z3.object({
5076
- id: z3.string().default(""),
5077
- name: z3.string().default(""),
5215
+ var connectionCapabilityId = (id) => `connected-apps/connection/${id}`;
5216
+ var connectionSchema = z4.object({
5217
+ id: z4.string().default(""),
5218
+ name: z4.string().default(""),
5078
5219
  /** `workspace` (the first-party Xingular Workspace server), `local` (a command), or `remote` (Streamable HTTP). */
5079
- kind: z3.string().default(""),
5080
- command: z3.string().default(""),
5081
- args: z3.array(z3.string()).default([]),
5082
- cwd: z3.string().nullable().default(null),
5083
- envKeys: z3.array(z3.string()).default([]),
5084
- enabled: z3.boolean().default(false),
5220
+ kind: z4.string().default(""),
5221
+ command: z4.string().default(""),
5222
+ args: z4.array(z4.string()).default([]),
5223
+ cwd: z4.string().nullable().default(null),
5224
+ envKeys: z4.array(z4.string()).default([]),
5225
+ enabled: z4.boolean().default(false),
5085
5226
  /** Available to every agent and project, except those excluded. */
5086
- global: z3.boolean().default(false),
5087
- agentIds: z3.array(z3.string()).default([]),
5088
- projectIds: z3.array(z3.string()).default([]),
5089
- excludedAgentIds: z3.array(z3.string()).default([]),
5090
- excludedProjectIds: z3.array(z3.string()).default([]),
5227
+ global: z4.boolean().default(false),
5228
+ agentIds: z4.array(z4.string()).default([]),
5229
+ projectIds: z4.array(z4.string()).default([]),
5230
+ excludedAgentIds: z4.array(z4.string()).default([]),
5231
+ excludedProjectIds: z4.array(z4.string()).default([]),
5091
5232
  /** Tools that run without asking. */
5092
- readTools: z3.array(z3.string()).default([]),
5093
- expectedVaultId: z3.string().nullable().default(null),
5094
- checkedAt: z3.string().nullable().default(null),
5095
- serverName: z3.string().nullable().default(null),
5096
- tools: z3.array(z3.unknown()).default([]),
5097
- error: z3.string().nullable().default(null),
5098
- url: z3.string().nullable().default(null),
5233
+ readTools: z4.array(z4.string()).default([]),
5234
+ expectedVaultId: z4.string().nullable().default(null),
5235
+ checkedAt: z4.string().nullable().default(null),
5236
+ serverName: z4.string().nullable().default(null),
5237
+ tools: z4.array(z4.unknown()).default([]),
5238
+ error: z4.string().nullable().default(null),
5239
+ url: z4.string().nullable().default(null),
5099
5240
  /** `none`, `headers`, or `oauth`. */
5100
- auth: z3.string().default(""),
5101
- headerKeys: z3.array(z3.string()).default([]),
5102
- oauthClient: z3.string().nullable().default(null),
5103
- oauthScopes: z3.array(z3.string()).default([]),
5104
- authState: z3.string().nullable().default(null),
5105
- catalogId: z3.string().nullable().default(null)
5241
+ auth: z4.string().default(""),
5242
+ headerKeys: z4.array(z4.string()).default([]),
5243
+ oauthClient: z4.string().nullable().default(null),
5244
+ oauthScopes: z4.array(z4.string()).default([]),
5245
+ authState: z4.string().nullable().default(null),
5246
+ catalogId: z4.string().nullable().default(null)
5106
5247
  });
5107
5248
  var connectionInputSchema = connectionSchema.extend({
5108
- env: z3.record(z3.string(), z3.string()).nullish(),
5109
- removeEnv: z3.array(z3.string()).nullish(),
5110
- headers: z3.record(z3.string(), z3.string()).nullish(),
5111
- removeHeaders: z3.array(z3.string()).nullish(),
5249
+ env: z4.record(z4.string(), z4.string()).nullish(),
5250
+ removeEnv: z4.array(z4.string()).nullish(),
5251
+ headers: z4.record(z4.string(), z4.string()).nullish(),
5252
+ removeHeaders: z4.array(z4.string()).nullish(),
5112
5253
  /** A client registered by the user with the provider; replaces any saved sign-in. */
5113
- oauthCredentials: z3.object({ clientId: z3.string(), clientSecret: z3.string() }).nullish()
5254
+ oauthCredentials: z4.object({ clientId: z4.string(), clientSecret: z4.string() }).nullish()
5114
5255
  });
5115
5256
 
5257
+ // src/mcp/scopeMigration.ts
5258
+ var recordPath2 = (root) => join26(root, "config/capability-scope-migration.json");
5259
+ var attachmentsPath = (root) => join26(root, "config/plugin-attachments.json");
5260
+ var connectionPath = (stateDir) => join26(stateDir, "connections.json");
5261
+ var connectionsBackupPath = (stateDir) => join26(stateDir, "connections.pre-capability-scope.json");
5262
+ var attachmentsBackupPath = (root) => join26(root, "config/plugin-attachments.pre-capability-scope.json");
5263
+ function connectionScopeMigrationComplete(root) {
5264
+ try {
5265
+ const marker = readJson(recordPath2(root));
5266
+ return marker?.version === 1 && marker.status === "complete";
5267
+ } catch {
5268
+ return false;
5269
+ }
5270
+ }
5271
+ function unique(values2) {
5272
+ return [...new Set(values2)];
5273
+ }
5274
+ function scopeRows(connection) {
5275
+ const agents = /* @__PURE__ */ new Map();
5276
+ const projects = /* @__PURE__ */ new Map();
5277
+ for (const id of unique([...connection.agentIds, ...connection.excludedAgentIds])) {
5278
+ agents.set(id, !connection.excludedAgentIds.includes(id));
5279
+ }
5280
+ for (const id of unique([...connection.projectIds, ...connection.excludedProjectIds])) {
5281
+ projects.set(id, !connection.excludedProjectIds.includes(id));
5282
+ }
5283
+ return { global: connection.global, agents, projects };
5284
+ }
5285
+ function assignmentCount(rows) {
5286
+ return 1 + rows.agents.size + rows.projects.size;
5287
+ }
5288
+ function removeId(choices, id) {
5289
+ forgetId(choices, id);
5290
+ }
5291
+ function put(map, id, value) {
5292
+ map.on = map.on.filter((item) => item !== id);
5293
+ map.off = map.off.filter((item) => item !== id);
5294
+ map[value ? "on" : "off"].push(id);
5295
+ }
5296
+ function writeRows(choices, id, rows) {
5297
+ removeId(choices, id);
5298
+ put(choices.global, id, rows.global);
5299
+ for (const [scopeId, on] of rows.agents) {
5300
+ const target = choices.agents[scopeId] ??= { on: [], off: [] };
5301
+ put(target, id, on);
5302
+ }
5303
+ for (const [scopeId, on] of rows.projects) {
5304
+ const target = choices.projects[scopeId] ??= { on: [], off: [] };
5305
+ put(target, id, on);
5306
+ }
5307
+ }
5308
+ function readValue(item, id) {
5309
+ if (item?.off.includes(id)) return false;
5310
+ if (item?.on.includes(id)) return true;
5311
+ return null;
5312
+ }
5313
+ function verifyRows(choices, id, expected) {
5314
+ if (readValue(choices.global, id) !== expected.global) return false;
5315
+ for (const [scopeId, on] of expected.agents) {
5316
+ if (readValue(choices.agents[scopeId], id) !== on) return false;
5317
+ }
5318
+ for (const [scopeId, on] of expected.projects) {
5319
+ if (readValue(choices.projects[scopeId], id) !== on) return false;
5320
+ }
5321
+ return true;
5322
+ }
5323
+ function backup(source2, target) {
5324
+ if (!existsSync10(source2)) return false;
5325
+ if (!existsSync10(target)) atomicWrite(target, readFileSync14(source2));
5326
+ chmodSync6(target, 384);
5327
+ return true;
5328
+ }
5329
+ function setPrivate(path3) {
5330
+ try {
5331
+ chmodSync6(path3, 384);
5332
+ } catch {
5333
+ }
5334
+ }
5335
+ function stripScopes(stateDir) {
5336
+ const path3 = connectionPath(stateDir);
5337
+ if (!existsSync10(path3)) return;
5338
+ const stored = readJson(path3);
5339
+ if (!Array.isArray(stored)) throw new Error("Stored connector data must be a list before its scopes can be migrated.");
5340
+ const records = stored.map((item) => {
5341
+ if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error("A stored connector record is invalid; no scope data was removed.");
5342
+ const { global: _global, agentIds: _agentIds, projectIds: _projectIds, excludedAgentIds: _excludedAgentIds, excludedProjectIds: _excludedProjectIds, ...connection } = item;
5343
+ return connection;
5344
+ });
5345
+ writeJson(path3, records);
5346
+ setPrivate(path3);
5347
+ }
5348
+ function migrateConnectionScopes(root, stateDir) {
5349
+ const marker = readJson(recordPath2(root));
5350
+ if (marker?.version === 1 && marker.status === "complete") return marker;
5351
+ if (marker?.version === 1 && marker.status === "attachments-written") {
5352
+ stripScopes(stateDir);
5353
+ const complete = { ...marker, status: "complete" };
5354
+ writeJson(recordPath2(root), complete);
5355
+ return complete;
5356
+ }
5357
+ const connectionsFile = connectionPath(stateDir);
5358
+ const hadConnections = existsSync10(connectionsFile);
5359
+ const raw = hadConnections ? readJson(connectionsFile) : [];
5360
+ if (!Array.isArray(raw)) throw new Error("Stored connector data must be a list before its scopes can be migrated.");
5361
+ const parsed2 = raw.map((item) => {
5362
+ const result = connectionSchema.safeParse(item);
5363
+ if (!result.success) throw new Error("A stored connector record is invalid; no scope data was removed.");
5364
+ return result.data;
5365
+ });
5366
+ const connectionBackup = hadConnections ? connectionsBackupPath(stateDir) : null;
5367
+ const hadAttachments = existsSync10(attachmentsPath(root));
5368
+ const attachmentBackup = hadAttachments ? attachmentsBackupPath(root) : null;
5369
+ const migration = {
5370
+ version: 1,
5371
+ status: "attachments-written",
5372
+ connectionCount: parsed2.length,
5373
+ scopeAssignmentCount: parsed2.reduce((count, connection) => count + assignmentCount(scopeRows(connection)), 0),
5374
+ connectionsBackup: connectionBackup,
5375
+ attachmentsBackup: attachmentBackup,
5376
+ hadAttachments
5377
+ };
5378
+ backup(connectionsFile, connectionBackup ?? "");
5379
+ backup(attachmentsPath(root), attachmentBackup ?? "");
5380
+ try {
5381
+ const choices = load2(root);
5382
+ const expected = /* @__PURE__ */ new Map();
5383
+ for (const connection of parsed2) {
5384
+ const id = connectionCapabilityId(connection.id);
5385
+ const rows = scopeRows(connection);
5386
+ expected.set(id, rows);
5387
+ writeRows(choices, id, rows);
5388
+ }
5389
+ const migratedAssignments = [...expected.values()].reduce((count, rows) => count + assignmentCount(rows), 0);
5390
+ if (migratedAssignments !== migration.scopeAssignmentCount || [...expected].some(([id, rows]) => !verifyRows(choices, id, rows))) {
5391
+ throw new Error("Connector scope count check failed; the original files were restored.");
5392
+ }
5393
+ save(root, choices);
5394
+ const saved = load2(root);
5395
+ if ([...expected].some(([id, rows]) => !verifyRows(saved, id, rows))) throw new Error("Connector scope count check failed after saving; the original files were restored.");
5396
+ writeJson(recordPath2(root), migration);
5397
+ stripScopes(stateDir);
5398
+ const complete = { ...migration, status: "complete" };
5399
+ writeJson(recordPath2(root), complete);
5400
+ return complete;
5401
+ } catch (error) {
5402
+ if (attachmentBackup && existsSync10(attachmentBackup)) atomicWrite(attachmentsPath(root), readFileSync14(attachmentBackup));
5403
+ else if (!hadAttachments && existsSync10(attachmentsPath(root))) unlinkSync3(attachmentsPath(root));
5404
+ if (connectionBackup && existsSync10(connectionBackup)) atomicWrite(connectionsFile, readFileSync14(connectionBackup));
5405
+ try {
5406
+ unlinkSync3(recordPath2(root));
5407
+ } catch {
5408
+ }
5409
+ throw error;
5410
+ }
5411
+ }
5412
+
5116
5413
  // src/mcp/connections.ts
5117
5414
  var OAUTH_STATE = "oauth";
5118
5415
  var OAUTH_CLIENT_ID = "oauth_client_id";
@@ -5133,6 +5430,17 @@ function allowed(connection, scope) {
5133
5430
  if (scope.projectId && connection.excludedProjectIds.includes(scope.projectId)) return false;
5134
5431
  return connection.global || Boolean(scope.agentId && connection.agentIds.includes(scope.agentId)) || Boolean(scope.projectId && connection.projectIds.includes(scope.projectId));
5135
5432
  }
5433
+ function setAgentScope(all2, agentId, ids) {
5434
+ for (const connection of all2) {
5435
+ connection.agentIds = connection.agentIds.filter((id) => id !== agentId);
5436
+ connection.excludedAgentIds = connection.excludedAgentIds.filter((id) => id !== agentId);
5437
+ if (ids.includes(connection.id)) {
5438
+ if (!connection.global) connection.agentIds.push(agentId);
5439
+ } else if (connection.global) {
5440
+ connection.excludedAgentIds.push(agentId);
5441
+ }
5442
+ }
5443
+ }
5136
5444
  function setScope(connection, scope, on) {
5137
5445
  const without = (ids, id) => ids.filter((item) => item !== id);
5138
5446
  if (scope.agentId) {
@@ -5203,23 +5511,48 @@ function scrubWith(value, secrets) {
5203
5511
  function revisionOf(all2) {
5204
5512
  return all2.map((item) => ({ id: item.id, enabled: item.enabled, global: item.global, agents: item.agentIds, projects: item.projectIds, excludedAgents: item.excludedAgentIds, excludedProjects: item.excludedProjectIds, checkedAt: item.checkedAt, error: item.error }));
5205
5513
  }
5514
+ function sameScope(left, right) {
5515
+ const ids = (values2) => [...values2].sort();
5516
+ return left.global === right.global && JSON.stringify(ids(left.agentIds)) === JSON.stringify(ids(right.agentIds)) && JSON.stringify(ids(left.projectIds)) === JSON.stringify(ids(right.projectIds)) && JSON.stringify(ids(left.excludedAgentIds)) === JSON.stringify(ids(right.excludedAgentIds)) && JSON.stringify(ids(left.excludedProjectIds)) === JSON.stringify(ids(right.excludedProjectIds));
5517
+ }
5518
+ function hasScope(connection) {
5519
+ return connection.global || connection.agentIds.length > 0 || connection.projectIds.length > 0 || connection.excludedAgentIds.length > 0 || connection.excludedProjectIds.length > 0;
5520
+ }
5521
+ function scopeMigrationError() {
5522
+ return new HarnessError("connection_scope_migration_pending", "Connector access settings cannot be changed until their scope migration succeeds. Reopen the data root to retry it.");
5523
+ }
5206
5524
  var Connections = class {
5207
- constructor(stateDir, secrets, bus) {
5525
+ constructor(stateDir, secrets, bus, attachmentRoot = () => null) {
5208
5526
  this.secrets = secrets;
5209
5527
  this.bus = bus;
5210
- this.path = join26(stateDir, "connections.json");
5528
+ this.attachmentRoot = attachmentRoot;
5529
+ this.path = join27(stateDir, "connections.json");
5211
5530
  }
5212
5531
  path;
5213
5532
  /** Every secret value a connection holds, so results can be redacted without waiting on the store. */
5214
5533
  known = /* @__PURE__ */ new Map();
5215
5534
  list() {
5216
5535
  const stored = readJson(this.path) ?? [];
5536
+ const root = this.attachmentRoot();
5537
+ const choices = root ? load2(root) : null;
5217
5538
  return stored.flatMap((item) => {
5218
5539
  const parsed2 = connectionSchema.safeParse(item);
5219
5540
  if (!parsed2.success) return [];
5220
5541
  const connection = parsed2.data;
5221
5542
  if (!connection.auth) connection.auth = "none";
5222
5543
  if (connection.kind === "workspace" && connection.name === "X Workspace") connection.name = "Xingular Workspace";
5544
+ if (choices) {
5545
+ const id = connectionCapabilityId(connection.id);
5546
+ const hasChoice = choices.global.on.includes(id) || choices.global.off.includes(id) || Object.values(choices.agents).some((entry) => entry.on.includes(id) || entry.off.includes(id)) || Object.values(choices.projects).some((entry) => entry.on.includes(id) || entry.off.includes(id));
5547
+ if (hasChoice || !(connection.global || connection.agentIds.length || connection.projectIds.length || connection.excludedAgentIds.length || connection.excludedProjectIds.length)) {
5548
+ const attachment = describe(choices, id, connection.global);
5549
+ connection.global = attachment.global;
5550
+ connection.agentIds = attachment.on.agentIds;
5551
+ connection.projectIds = attachment.on.projectIds;
5552
+ connection.excludedAgentIds = attachment.off.agentIds;
5553
+ connection.excludedProjectIds = attachment.off.projectIds;
5554
+ }
5555
+ }
5223
5556
  return [connection];
5224
5557
  });
5225
5558
  }
@@ -5235,12 +5568,49 @@ var Connections = class {
5235
5568
  return revisionOf(this.list());
5236
5569
  }
5237
5570
  write(all2) {
5238
- writeJson(this.path, all2);
5571
+ const root = this.attachmentRoot();
5572
+ let stored = all2;
5573
+ if (root && connectionScopeMigrationComplete(root)) {
5574
+ stored = all2.map(({ global: _global, agentIds: _agentIds, projectIds: _projectIds, excludedAgentIds: _excludedAgentIds, excludedProjectIds: _excludedProjectIds, ...connection }) => connection);
5575
+ } else if (root) {
5576
+ const previous = /* @__PURE__ */ new Map();
5577
+ for (const item of readJson(this.path) ?? []) {
5578
+ const parsed2 = connectionSchema.safeParse(item);
5579
+ if (parsed2.success) previous.set(parsed2.data.id, parsed2.data);
5580
+ }
5581
+ stored = all2.map((connection) => {
5582
+ const legacy = previous.get(connection.id);
5583
+ return legacy ? {
5584
+ ...connection,
5585
+ global: legacy.global,
5586
+ agentIds: legacy.agentIds,
5587
+ projectIds: legacy.projectIds,
5588
+ excludedAgentIds: legacy.excludedAgentIds,
5589
+ excludedProjectIds: legacy.excludedProjectIds
5590
+ } : connection;
5591
+ });
5592
+ }
5593
+ writeJson(this.path, stored);
5239
5594
  try {
5240
- chmodSync6(this.path, 384);
5595
+ chmodSync7(this.path, 384);
5241
5596
  } catch {
5242
5597
  }
5243
5598
  }
5599
+ writeScope(root, connection) {
5600
+ const choices = load2(root);
5601
+ const id = connectionCapabilityId(connection.id);
5602
+ forgetId(choices, id);
5603
+ set(choices, [id], {}, connection.global, false);
5604
+ for (const agentId of /* @__PURE__ */ new Set([...connection.agentIds, ...connection.excludedAgentIds])) {
5605
+ const on = connection.agentIds.includes(agentId) && !connection.excludedAgentIds.includes(agentId);
5606
+ set(choices, [id], { agentId }, on, connection.global);
5607
+ }
5608
+ for (const projectId of /* @__PURE__ */ new Set([...connection.projectIds, ...connection.excludedProjectIds])) {
5609
+ const on = connection.projectIds.includes(projectId) && !connection.excludedProjectIds.includes(projectId);
5610
+ set(choices, [id], { projectId }, on, connection.global);
5611
+ }
5612
+ save(root, choices);
5613
+ }
5244
5614
  changed() {
5245
5615
  this.bus.emit({ type: "connections.changed" });
5246
5616
  }
@@ -5249,17 +5619,95 @@ var Connections = class {
5249
5619
  const all2 = this.list();
5250
5620
  const connection = all2.find((item) => item.id === id);
5251
5621
  if (!connection) throw notFound("Connection");
5622
+ const before = { ...connection, agentIds: [...connection.agentIds], projectIds: [...connection.projectIds], excludedAgentIds: [...connection.excludedAgentIds], excludedProjectIds: [...connection.excludedProjectIds] };
5252
5623
  change(connection);
5624
+ const root = this.attachmentRoot();
5625
+ if (root && !connectionScopeMigrationComplete(root) && !sameScope(before, connection)) throw scopeMigrationError();
5253
5626
  this.write(all2);
5254
5627
  return connection;
5255
5628
  }
5256
5629
  /** Read-modify-write of every connection. */
5257
5630
  updateAll(change) {
5258
5631
  const all2 = this.list();
5632
+ const before = new Map(all2.map((connection) => [connection.id, {
5633
+ ...connection,
5634
+ agentIds: [...connection.agentIds],
5635
+ projectIds: [...connection.projectIds],
5636
+ excludedAgentIds: [...connection.excludedAgentIds],
5637
+ excludedProjectIds: [...connection.excludedProjectIds]
5638
+ }]));
5259
5639
  change(all2);
5640
+ const root = this.attachmentRoot();
5641
+ if (root && !connectionScopeMigrationComplete(root)) {
5642
+ const remainingIds = new Set(all2.map((connection) => connection.id));
5643
+ const scopeChanged = all2.some((connection) => {
5644
+ const previous = before.get(connection.id);
5645
+ return previous ? !sameScope(previous, connection) : hasScope(connection);
5646
+ }) || [...before].some(([id, connection]) => !remainingIds.has(id) && hasScope(connection));
5647
+ if (scopeChanged) throw scopeMigrationError();
5648
+ }
5260
5649
  this.write(all2);
5261
5650
  this.changed();
5262
5651
  }
5652
+ /** Switches one connector in the shared attachment store, preserving all other scopes. */
5653
+ setScope(connectionId, scope, on) {
5654
+ const connection = this.find(connectionId);
5655
+ const root = this.attachmentRoot();
5656
+ if (root && !connectionScopeMigrationComplete(root)) throw scopeMigrationError();
5657
+ if (!root) {
5658
+ this.update(connectionId, (item) => setScope(item, scope, on));
5659
+ this.changed();
5660
+ return this.forScope(scope);
5661
+ }
5662
+ const choices = load2(root);
5663
+ const fallback = connection.global;
5664
+ set(choices, [connectionCapabilityId(connectionId)], scope, on, fallback);
5665
+ save(root, choices);
5666
+ this.changed();
5667
+ return this.forScope(scope);
5668
+ }
5669
+ /** Gives one agent an exact connector set without changing project or global choices. */
5670
+ setAgentScope(agentId, ids) {
5671
+ const all2 = this.list();
5672
+ const root = this.attachmentRoot();
5673
+ if (root && !connectionScopeMigrationComplete(root)) throw scopeMigrationError();
5674
+ if (!root) {
5675
+ setAgentScope(all2, agentId, ids);
5676
+ this.write(all2);
5677
+ this.changed();
5678
+ return this.forScope({ agentId });
5679
+ }
5680
+ const choices = load2(root);
5681
+ for (const connection of all2) {
5682
+ set(choices, [connectionCapabilityId(connection.id)], { agentId }, ids.includes(connection.id), connection.global);
5683
+ }
5684
+ save(root, choices);
5685
+ this.changed();
5686
+ return this.forScope({ agentId });
5687
+ }
5688
+ /** Removes only connector choices when an agent is deleted; other capability choices are separate. */
5689
+ removeAgentScope(agentId) {
5690
+ const root = this.attachmentRoot();
5691
+ if (root && !connectionScopeMigrationComplete(root)) return;
5692
+ if (!root) {
5693
+ this.updateAll((all2) => {
5694
+ for (const connection of all2) {
5695
+ connection.agentIds = connection.agentIds.filter((id) => id !== agentId);
5696
+ connection.excludedAgentIds = connection.excludedAgentIds.filter((id) => id !== agentId);
5697
+ }
5698
+ });
5699
+ return;
5700
+ }
5701
+ const choices = load2(root);
5702
+ const entry = choices.agents[agentId];
5703
+ if (!entry) return;
5704
+ const isConnector = (id) => id.startsWith("connected-apps/connection/");
5705
+ entry.on = entry.on.filter((id) => !isConnector(id));
5706
+ entry.off = entry.off.filter((id) => !isConnector(id));
5707
+ if (!entry.on.length && !entry.off.length) delete choices.agents[agentId];
5708
+ save(root, choices);
5709
+ this.changed();
5710
+ }
5263
5711
  secret(connectionId, name) {
5264
5712
  return this.secrets.get(secretKey(`mcp--${connectionId}`, name));
5265
5713
  }
@@ -5387,6 +5835,10 @@ var Connections = class {
5387
5835
  if (connection.cwd && (!isAbsolute3(connection.cwd) || !isDir(connection.cwd))) throw invalid("Working directory must be an existing absolute directory.");
5388
5836
  const all2 = this.list();
5389
5837
  const existing = all2.find((item) => item.id === connection.id);
5838
+ const root = this.attachmentRoot();
5839
+ if (root && !connectionScopeMigrationComplete(root) && (existing ? !sameScope(existing, connection) : hasScope(connection))) {
5840
+ throw scopeMigrationError();
5841
+ }
5390
5842
  connection.envKeys = [...existing?.envKeys ?? []];
5391
5843
  connection.headerKeys = [...existing?.headerKeys ?? []];
5392
5844
  const sameServer = (existing?.url ?? null) === connection.url && (existing?.auth ?? "") === connection.auth && !oauthCredentials;
@@ -5417,6 +5869,7 @@ var Connections = class {
5417
5869
  connection.envKeys = connection.envKeys.filter((value) => value !== key);
5418
5870
  }
5419
5871
  Object.assign(connection, { checkedAt: null, tools: [], serverName: null, error: null });
5872
+ if (root && connectionScopeMigrationComplete(root)) this.writeScope(root, connection);
5420
5873
  this.write([...all2.filter((item) => item.id !== connection.id), connection]);
5421
5874
  await this.refreshSecrets();
5422
5875
  this.changed();
@@ -5444,6 +5897,12 @@ var Connections = class {
5444
5897
  for (const name of [OAUTH_STATE, OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET]) await this.deleteSecret(connection.id, name);
5445
5898
  }
5446
5899
  this.write(all2.filter((item) => item.id !== connectionId));
5900
+ const root = this.attachmentRoot();
5901
+ if (root) {
5902
+ const choices = load2(root);
5903
+ forgetId(choices, connectionCapabilityId(connectionId));
5904
+ save(root, choices);
5905
+ }
5447
5906
  this.known.delete(connectionId);
5448
5907
  this.changed();
5449
5908
  }
@@ -5463,7 +5922,7 @@ var Connections = class {
5463
5922
  var size = (path3) => {
5464
5923
  if (!path3) return null;
5465
5924
  try {
5466
- return charCount(readFileSync14(path3, "utf8"));
5925
+ return charCount(readFileSync15(path3, "utf8"));
5467
5926
  } catch {
5468
5927
  return null;
5469
5928
  }
@@ -5563,7 +6022,7 @@ async function list2(harness, scope = {}) {
5563
6022
  loading: await loadingFor(harness, scope),
5564
6023
  cwd: scope.cwd || null,
5565
6024
  sharedDir: sharedDir(harness.accounts.dir),
5566
- packagesDir: join27(harness.env.root, "plugins"),
6025
+ packagesDir: join28(harness.env.root, "plugins"),
5567
6026
  registered: harness.plugins.registered(),
5568
6027
  hooks: harness.hooks.status(),
5569
6028
  attachments: load2(harness.env.root),
@@ -5650,7 +6109,7 @@ function forgetAgent(harness, agentId) {
5650
6109
  }
5651
6110
 
5652
6111
  // src/capabilities/api.ts
5653
- var unique = (values2) => [...new Set(values2.filter((value) => value.length > 0))];
6112
+ var unique2 = (values2) => [...new Set(values2.filter((value) => value.length > 0))];
5654
6113
  function contributions(plugins) {
5655
6114
  return plugins.flatMap((plugin) => plugin.contributions.map((item) => ({
5656
6115
  id: item.id,
@@ -5667,17 +6126,26 @@ async function list3(harness, params = {}) {
5667
6126
  const plugins = await list2(harness, params);
5668
6127
  return { plugins, capabilities: contributions(plugins) };
5669
6128
  }
5670
- async function resolve4(harness, params) {
6129
+ async function resolve5(harness, params) {
5671
6130
  const caller = { agentId: params.agentId ?? null, projectId: params.projectId ?? null };
5672
6131
  const catalogue2 = await list3(harness, params);
5673
6132
  const connectors = harness.apps.connections.forScope(caller).map((connection) => connection.id);
5674
6133
  return {
5675
6134
  version: 1,
5676
- tools: unique(params.tools ?? []),
5677
- connectors: unique(connectors),
6135
+ tools: unique2(params.tools ?? []),
6136
+ connectors: unique2(connectors),
5678
6137
  contributions: catalogue2.capabilities.filter((item) => item.state === "on").map((item) => item.id)
5679
6138
  };
5680
6139
  }
6140
+ function toolIntersection(parent, assignee, taskTools) {
6141
+ const permitted = taskTools === void 0 ? null : new Set(taskTools);
6142
+ return {
6143
+ version: 1,
6144
+ tools: parent.tools.filter((tool) => assignee.tools.includes(tool) && (!permitted || permitted.has(tool))),
6145
+ connectors: parent.connectors.filter((id) => assignee.connectors.includes(id)),
6146
+ contributions: parent.contributions.filter((id) => assignee.contributions.includes(id))
6147
+ };
6148
+ }
5681
6149
  function inventory2(harness, params = {}) {
5682
6150
  return inventoryFor(harness, params);
5683
6151
  }
@@ -5692,7 +6160,7 @@ async function switchCapability(harness, params, on) {
5692
6160
 
5693
6161
  // src/mcp/service.ts
5694
6162
  import { createHash as createHash4, randomUUID as randomUUID5 } from "node:crypto";
5695
- import { join as join28 } from "node:path";
6163
+ import { join as join29 } from "node:path";
5696
6164
 
5697
6165
  // src/mcp/manager.ts
5698
6166
  import { Client, SSEClientTransport, StreamableHTTPClientTransport, UnauthorizedError as UnauthorizedError2 } from "@modelcontextprotocol/client";
@@ -5781,8 +6249,8 @@ var cancelActive = null;
5781
6249
  function listenForCallback(expectedState, clientName) {
5782
6250
  return new Promise((ready, failed) => {
5783
6251
  let settle;
5784
- const result = new Promise((resolve8, reject) => {
5785
- settle = { resolve: resolve8, reject };
6252
+ const result = new Promise((resolve9, reject) => {
6253
+ settle = { resolve: resolve9, reject };
5786
6254
  });
5787
6255
  result.catch(() => void 0);
5788
6256
  const server = createServer((request2, response3) => {
@@ -5806,7 +6274,7 @@ function listenForCallback(expectedState, clientName) {
5806
6274
  clearTimeout(timer);
5807
6275
  failed(new HarnessError("invalid_request", error.code === "EADDRINUSE" ? `Port ${MCP_OAUTH_PORT} is in use. Finish or cancel the other sign-in, or quit the program using that port, then try again.` : "The sign-in listener could not start."));
5808
6276
  });
5809
- const closed = new Promise((resolve8) => server.once("close", resolve8));
6277
+ const closed = new Promise((resolve9) => server.once("close", resolve9));
5810
6278
  const close = () => {
5811
6279
  clearTimeout(timer);
5812
6280
  if (cancelActive === cancel) cancelActive = null;
@@ -5991,7 +6459,7 @@ var UNATTENDED_SCOPE = "plugin-hooks";
5991
6459
  var ConnectedApps = class {
5992
6460
  constructor(options2) {
5993
6461
  this.options = options2;
5994
- this.connections = new Connections(options2.stateDir, options2.secrets, options2.bus);
6462
+ this.connections = new Connections(options2.stateDir, options2.secrets, options2.bus, () => options2.env.dataRoot);
5995
6463
  this.oauth = {
5996
6464
  clientName: options2.env.productName,
5997
6465
  load: (id) => this.connections.oauthLoad(id),
@@ -6086,7 +6554,7 @@ var ConnectedApps = class {
6086
6554
  return { tools: tools3, errors: [] };
6087
6555
  } catch (error) {
6088
6556
  const unauthorized = isSignInRequired(error);
6089
- const message = unauthorized ? signInRequired(connection).message : `${connection.name} could not load its tools. Check the connection in Settings \u2192 Connections.`;
6557
+ const message = unauthorized ? signInRequired(connection).message : `${connection.name} could not load its tools. Check its scope in Capability \u2192 Connectors.`;
6090
6558
  if (unauthorized) await this.manager.close(connection.id, session.sessionId);
6091
6559
  this.connectionFailed(connection.id, unauthorized);
6092
6560
  return { tools: [], errors: [{ connection: connection.name, message }] };
@@ -6206,7 +6674,7 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6206
6674
  }).slice(0, 100);
6207
6675
  const trace = { id: toolCallId, connectionId: connection.id, connectionName: connection.name, tool, at: now(), ok, summary: tool, result: takeChars(raw, 24e3), sources, truncated: charCount(raw) > 24e3 || structured?.truncated === true };
6208
6676
  try {
6209
- const file = join28(runDir(this.options.env.root, runId), "mcp-context.json");
6677
+ const file = join29(runDir(this.options.env.root, runId), "mcp-context.json");
6210
6678
  writeJson(file, [...readJson(file) ?? [], trace]);
6211
6679
  } catch {
6212
6680
  }
@@ -6214,7 +6682,7 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6214
6682
  }
6215
6683
  /** What a run retrieved from connected apps, in order. */
6216
6684
  runContext(runId) {
6217
- return readJson(join28(runDir(this.options.env.root, runId), "mcp-context.json")) ?? [];
6685
+ return readJson(join29(runDir(this.options.env.root, runId), "mcp-context.json")) ?? [];
6218
6686
  }
6219
6687
  /** Changes whenever what a session can discover may have changed. */
6220
6688
  async revision(session) {
@@ -6230,8 +6698,8 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6230
6698
  return [
6231
6699
  {
6232
6700
  name: "mcp_list_tools",
6233
- label: "App and connected tools",
6234
- description: `Discover enabled ${this.options.env.productName} app operations and connected-app tools by name. Pass tool to get one tool with its input schema.${this.brokered?.discoveryNote ? ` ${this.brokered.discoveryNote}` : ""}`,
6701
+ label: "App and connector tools",
6702
+ description: `Discover enabled ${this.options.env.productName} app operations and connector tools by name. Pass tool to get one tool with its input schema.${this.brokered?.discoveryNote ? ` ${this.brokered.discoveryNote}` : ""}`,
6235
6703
  parameters: (Type) => Type.Object({
6236
6704
  tool: Type.Optional(Type.String({ description: "One tool name, to get its input schema." })),
6237
6705
  query: Type.Optional(Type.String({ description: "Only tools whose name or description contains this text." })),
@@ -6247,8 +6715,8 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6247
6715
  },
6248
6716
  {
6249
6717
  name: "mcp_call_tool",
6250
- label: "Use connected app",
6251
- description: "Invoke an enabled app or connected-app tool returned by mcp_list_tools. Current settings are checked on every call. Changes require approval.",
6718
+ label: "Use connector",
6719
+ description: "Invoke an enabled app or connector tool returned by mcp_list_tools. Current settings are checked on every call. Changes require approval.",
6252
6720
  parameters: (Type) => Type.Object({ name: Type.String(), arguments: Type.Record(Type.String(), Type.Unknown()) }),
6253
6721
  execute: async (input) => JSON.stringify(await this.invoke(session, String(input.name), input.arguments ?? {}))
6254
6722
  }
@@ -6325,23 +6793,13 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6325
6793
  }
6326
6794
  /** Gives one agent exactly these connections, if its creator may reach each of them. */
6327
6795
  setAgentScope(creator, agentId, ids) {
6328
- this.connections.updateAll((all2) => {
6329
- for (const id of ids) {
6330
- const connection = all2.find((item) => item.id === id);
6331
- if (!connection) throw notFound("Connection");
6332
- if (!allowed(connection, creator)) throw new HarnessError("required_connections_unavailable", `${connection.name} is disabled or outside your connection scope.`);
6333
- }
6334
- for (const connection of all2) {
6335
- connection.agentIds = connection.agentIds.filter((id) => id !== agentId);
6336
- connection.excludedAgentIds = connection.excludedAgentIds.filter((id) => id !== agentId);
6337
- if (ids.includes(connection.id)) {
6338
- if (!connection.global) connection.agentIds.push(agentId);
6339
- } else if (connection.global) {
6340
- connection.excludedAgentIds.push(agentId);
6341
- }
6342
- }
6343
- });
6344
- return this.connections.forScope({ agentId });
6796
+ const all2 = this.connections.list();
6797
+ for (const id of ids) {
6798
+ const connection = all2.find((item) => item.id === id);
6799
+ if (!connection) throw notFound("Connection");
6800
+ if (!allowed(connection, creator)) throw new HarnessError("required_connections_unavailable", `${connection.name} is disabled or outside your connection scope.`);
6801
+ }
6802
+ return this.connections.setAgentScope(agentId, ids);
6345
6803
  }
6346
6804
  /**
6347
6805
  * A call with no session and nobody to ask, made by a plugin's hook. It reaches a connection
@@ -6358,21 +6816,13 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6358
6816
  }
6359
6817
  /** Switches one saved connection on or off for everyone, one agent, or one project. */
6360
6818
  attach(connectionId, scope, on) {
6361
- this.connections.updateAll((all2) => {
6362
- const connection = all2.find((item) => item.id === connectionId);
6363
- if (!connection) throw notFound("Connection");
6364
- if (!connection.enabled) throw invalid(`${connection.name} is switched off. Switch it on in its own settings first.`);
6365
- setScope(connection, scope, on);
6366
- });
6819
+ const connection = this.connections.find(connectionId);
6820
+ if (!connection.enabled) throw invalid(`${connection.name} is switched off. Switch it on in its own settings first.`);
6821
+ this.connections.setScope(connectionId, scope, on);
6367
6822
  }
6368
6823
  /** Forgets an agent that no longer exists. */
6369
6824
  removeAgentScope(agentId) {
6370
- this.connections.updateAll((all2) => {
6371
- for (const connection of all2) {
6372
- connection.agentIds = connection.agentIds.filter((id) => id !== agentId);
6373
- connection.excludedAgentIds = connection.excludedAgentIds.filter((id) => id !== agentId);
6374
- }
6375
- });
6825
+ this.connections.removeAgentScope(agentId);
6376
6826
  }
6377
6827
  async close() {
6378
6828
  await this.manager.close(void 0, UNATTENDED_SCOPE).catch(() => void 0);
@@ -6381,56 +6831,56 @@ ${action}`, standing: "switch", timeoutMs: 3e5 });
6381
6831
  };
6382
6832
 
6383
6833
  // src/memory/maintenance.ts
6384
- import { readFileSync as readFileSync16 } from "node:fs";
6385
- import { join as join30 } from "node:path";
6834
+ import { readFileSync as readFileSync17 } from "node:fs";
6835
+ import { join as join31 } from "node:path";
6386
6836
 
6387
6837
  // src/memory/api.ts
6388
6838
  import { dirname as dirname11 } from "node:path";
6389
6839
 
6390
6840
  // src/memory/proposals.ts
6391
6841
  import { createHash as createHash5 } from "node:crypto";
6392
- import { mkdirSync as mkdirSync10, readFileSync as readFileSync15, renameSync as renameSync6, rmSync as rmSync6 } from "node:fs";
6393
- import { dirname as dirname10, join as join29, relative as relative4 } from "node:path";
6394
- import { z as z4 } from "zod";
6395
- var optionalText2 = z4.string().nullish().transform((value) => value ?? null);
6396
- var sourceRefSchema = z4.object({
6397
- kind: z4.string(),
6398
- ref: z4.string(),
6842
+ import { mkdirSync as mkdirSync10, readFileSync as readFileSync16, renameSync as renameSync6, rmSync as rmSync6 } from "node:fs";
6843
+ import { dirname as dirname10, join as join30, relative as relative4 } from "node:path";
6844
+ import { z as z5 } from "zod";
6845
+ var optionalText2 = z5.string().nullish().transform((value) => value ?? null);
6846
+ var sourceRefSchema = z5.object({
6847
+ kind: z5.string(),
6848
+ ref: z5.string(),
6399
6849
  /** A passage quoted from the evidence. A run or source reference counts only when this passage is found in it. */
6400
- excerpt: z4.string().nullish().transform((value) => value ?? void 0)
6850
+ excerpt: z5.string().nullish().transform((value) => value ?? void 0)
6401
6851
  });
6402
- var linkProposalSchema = z4.object({ toId: z4.string(), type: z4.string() });
6403
- var pageProposalSchema = z4.object({
6852
+ var linkProposalSchema = z5.object({ toId: z5.string(), type: z5.string() });
6853
+ var pageProposalSchema = z5.object({
6404
6854
  id: optionalText2,
6405
- title: z4.string(),
6406
- type: z4.string(),
6407
- body: z4.string(),
6855
+ title: z5.string(),
6856
+ type: z5.string(),
6857
+ body: z5.string(),
6408
6858
  /** Where a new page is filed (`/library`, `/projects/<slug>`); the job's namespace by default. */
6409
6859
  namespace: optionalText2,
6410
6860
  confidence: optionalText2,
6411
- sourceRefs: z4.array(sourceRefSchema).default([]),
6412
- links: z4.array(linkProposalSchema).default([])
6861
+ sourceRefs: z5.array(sourceRefSchema).default([]),
6862
+ links: z5.array(linkProposalSchema).default([])
6413
6863
  });
6414
- var memoryProposalSchema = z4.object({
6864
+ var memoryProposalSchema = z5.object({
6415
6865
  id: optionalText2,
6416
- title: z4.string(),
6417
- kind: z4.string(),
6418
- body: z4.string(),
6866
+ title: z5.string(),
6867
+ kind: z5.string(),
6868
+ body: z5.string(),
6419
6869
  /** `/` for everything, or a project namespace. The job's project namespace by default. */
6420
6870
  namespace: optionalText2,
6421
6871
  confidence: optionalText2,
6422
6872
  supersedes: optionalText2,
6423
- volatile: z4.boolean().nullish().transform((value) => value ?? null),
6424
- sourceRefs: z4.array(sourceRefSchema).default([]),
6425
- links: z4.array(linkProposalSchema).default([])
6873
+ volatile: z5.boolean().nullish().transform((value) => value ?? null),
6874
+ sourceRefs: z5.array(sourceRefSchema).default([]),
6875
+ links: z5.array(linkProposalSchema).default([])
6426
6876
  });
6427
- var blockProposalSchema = z4.object({ block: z4.string(), body: z4.string(), sourceRefs: z4.array(sourceRefSchema).default([]) });
6428
- var claimFlagSchema = z4.object({ claim: z4.string(), kind: z4.string(), reason: z4.string() });
6429
- var proposalSetSchema = z4.object({
6430
- pages: z4.array(pageProposalSchema).default([]),
6431
- memories: z4.array(memoryProposalSchema).default([]),
6432
- blocks: z4.array(blockProposalSchema).default([]),
6433
- flags: z4.array(claimFlagSchema).default([])
6877
+ var blockProposalSchema = z5.object({ block: z5.string(), body: z5.string(), sourceRefs: z5.array(sourceRefSchema).default([]) });
6878
+ var claimFlagSchema = z5.object({ claim: z5.string(), kind: z5.string(), reason: z5.string() });
6879
+ var proposalSetSchema = z5.object({
6880
+ pages: z5.array(pageProposalSchema).default([]),
6881
+ memories: z5.array(memoryProposalSchema).default([]),
6882
+ blocks: z5.array(blockProposalSchema).default([]),
6883
+ flags: z5.array(claimFlagSchema).default([])
6434
6884
  });
6435
6885
  function parseProposalSet(value) {
6436
6886
  const parsed2 = proposalSetSchema.safeParse(value ?? {});
@@ -6468,14 +6918,14 @@ function triggerRunId(trigger) {
6468
6918
  }
6469
6919
  function fileHash(path3) {
6470
6920
  try {
6471
- return createHash5("sha256").update(readFileSync15(path3)).digest("hex");
6921
+ return createHash5("sha256").update(readFileSync16(path3)).digest("hex");
6472
6922
  } catch {
6473
6923
  return null;
6474
6924
  }
6475
6925
  }
6476
6926
  function readOrNull(path3) {
6477
6927
  try {
6478
- return readFileSync15(path3, "utf8");
6928
+ return readFileSync16(path3, "utf8");
6479
6929
  } catch {
6480
6930
  return null;
6481
6931
  }
@@ -6506,11 +6956,11 @@ function writeIndex(wiki) {
6506
6956
 
6507
6957
  `;
6508
6958
  for (const page2 of own) {
6509
- const inside4 = relative4(join29(wiki, folder), page2.path);
6959
+ const inside4 = relative4(join30(wiki, folder), page2.path);
6510
6960
  text6 += `- [${page2.frontmatter.title}](${inside4.startsWith("..") ? page2.path : inside4}) (${page2.frontmatter.type}, \`${page2.frontmatter.id}\`) \u2014 ${firstParagraph(page2.body)}
6511
6961
  `;
6512
6962
  }
6513
- atomicWrite(join29(wiki, folder, "index.md"), text6);
6963
+ atomicWrite(join30(wiki, folder, "index.md"), text6);
6514
6964
  }
6515
6965
  const active = memories2.filter((memory) => memory.front.status === "active");
6516
6966
  if (active.length) {
@@ -6518,7 +6968,7 @@ function writeIndex(wiki) {
6518
6968
  for (const memory of active) root += `- [${memory.front.title}](facts/${memory.front.id}.md) (${memory.front.kind}, ${memory.front.namespace}${memory.front.volatile ? ", volatile" : ""})
6519
6969
  `;
6520
6970
  }
6521
- atomicWrite(join29(wiki, "index.md"), root);
6971
+ atomicWrite(join30(wiki, "index.md"), root);
6522
6972
  }
6523
6973
  function apply(wiki, defaultNamespace, trigger, proposals, captured, evidence) {
6524
6974
  const existing = loadPages(wiki);
@@ -6618,7 +7068,7 @@ ${takeChars(body2, 600)}`, [entryId2], null, payload);
6618
7068
  const sourceList = current ? [...current.frontmatter.sourceIds] : [];
6619
7069
  for (const id of found) if (!sourceList.includes(id)) sourceList.push(id);
6620
7070
  const front = { id: pageId, title: proposal.title.trim(), type: proposal.type, updatedAt: now(), sourceIds: sourceList, links: links2 };
6621
- const path3 = current?.path ?? join29(targetDir, `${pageId}.md`);
7071
+ const path3 = current?.path ?? join30(targetDir, `${pageId}.md`);
6622
7072
  outcome.notes.push(`Page "${proposal.title}" (${pageId}) ${current ? "updated" : "created"} in ${namespace}.`);
6623
7073
  writes.push({ entryId: pageId, oldText: current ? readOrNull(current.path) : null, newText: serializePage(front, proposal.body), path: path3 });
6624
7074
  }
@@ -6747,7 +7197,7 @@ ${takeChars(proposal.body, 600)}`, [proposal.block]);
6747
7197
  afterRevisionId: snapshot(wiki, write3.entryId, write3.newText, actor, `Update ${short} (${described})`).id
6748
7198
  }));
6749
7199
  const operation = { id: operationId, createdAt: now(), trigger: described, pages, undone: false };
6750
- writeJson(join29(operationsDir(wiki), `${operationId}.json`), operation);
7200
+ writeJson(join30(operationsDir(wiki), `${operationId}.json`), operation);
6751
7201
  for (const write3 of writes) noteChange({ wiki, id: write3.entryId, path: write3.path, text: write3.newText, actor, reason: described });
6752
7202
  writeIndex(wiki);
6753
7203
  log2(
@@ -6765,13 +7215,13 @@ ${takeChars(proposal.body, 600)}`, [proposal.block]);
6765
7215
  outcome.operation = operation;
6766
7216
  return outcome;
6767
7217
  }
6768
- var registryProposalSchema = z4.object({
7218
+ var registryProposalSchema = z5.object({
6769
7219
  /** `skill`, `prompt`, or `rule`. */
6770
- kind: z4.string(),
6771
- name: z4.string(),
6772
- description: z4.string().default(""),
6773
- body: z4.string(),
6774
- reason: z4.string().default("")
7220
+ kind: z5.string(),
7221
+ name: z5.string(),
7222
+ description: z5.string().default(""),
7223
+ body: z5.string(),
7224
+ reason: z5.string().default("")
6775
7225
  });
6776
7226
  function parkRegistryProposal(wiki, trigger, proposal) {
6777
7227
  if (!["skill", "prompt", "rule"].includes(proposal.kind) || !proposal.name.trim() || !proposal.body.trim()) {
@@ -6800,7 +7250,7 @@ function parsed(schema, value) {
6800
7250
  }
6801
7251
  function accept(root, itemId) {
6802
7252
  const wiki = wikiDir(root);
6803
- const path3 = join29(wiki, "inbox", `${safeId(itemId)}.json`);
7253
+ const path3 = join30(wiki, "inbox", `${safeId(itemId)}.json`);
6804
7254
  const item = readJson(path3);
6805
7255
  if (!item) throw notFound("Inbox item");
6806
7256
  const payload = item.payload;
@@ -6830,9 +7280,9 @@ function accept(root, itemId) {
6830
7280
  } else {
6831
7281
  throw invalid("Unknown proposal kind.");
6832
7282
  }
6833
- const target = join29(wiki, "inbox", "dismissed");
7283
+ const target = join30(wiki, "inbox", "dismissed");
6834
7284
  mkdirSync10(target, { recursive: true });
6835
- renameSync6(path3, join29(target, `${item.id}.json`));
7285
+ renameSync6(path3, join30(target, `${item.id}.json`));
6836
7286
  return message;
6837
7287
  }
6838
7288
 
@@ -6991,13 +7441,13 @@ function list4(env, params) {
6991
7441
  const pages = loadPages(wiki).filter((page2) => !namespace || page2.location.namespace === namespace || page2.location.path.startsWith(prefix)).slice(0, 200).map((page2) => `- ${page2.location.path} \u2014 ${page2.frontmatter.title} (${page2.frontmatter.type}) [${page2.frontmatter.id}]`);
6992
7442
  return text6 + (pages.length ? pages.join("\n") : "No pages here.");
6993
7443
  }
6994
- function skill(env, context, params) {
7444
+ function skill(env, context, params, contributions2) {
6995
7445
  const { root } = env;
6996
7446
  const name = textParam(params, "name").toLowerCase();
6997
- const items = list(root).filter((item) => item.kind === "skills");
7447
+ const items = limitToContributions(list(root).filter((item) => item.kind === "skills"), contributions2);
6998
7448
  if (!name) {
6999
7449
  if (!items.length) return "No skills yet.";
7000
- const active = enabledFor(root, context.projectId ?? null, context.agentId ?? null).map((item) => item.id);
7450
+ const active = (contributions2 === void 0 ? enabledFor(root, context.projectId ?? null, context.agentId ?? null) : items).map((item) => item.id);
7001
7451
  return items.map((item) => `- ${item.name} (${item.id})${active.includes(item.id) ? " \xB7 active" : ""} \u2014 ${item.description}`).join("\n");
7002
7452
  }
7003
7453
  const found = items.find((item) => item.id.toLowerCase() === name || item.id.replace(/^skills\//, "").toLowerCase() === name || item.name.toLowerCase() === name);
@@ -7299,7 +7749,7 @@ function chatUpdatesPaused(settings) {
7299
7749
  function runEvidence(root, runId) {
7300
7750
  const run3 = findRun(root, runId);
7301
7751
  if (!run3) throw new Error("Run was not found.");
7302
- const settings = readJsonOr(join30(root, "config/settings.json"), null);
7752
+ const settings = readJsonOr(join31(root, "config/settings.json"), null);
7303
7753
  if (Array.isArray(settings?.excludedSourceIds) && settings.excludedSourceIds.includes(runId)) throw new Error("run excluded in settings");
7304
7754
  const projectId = run3.mode === "chat" || run3.mode === "code" ? run3.projectId : null;
7305
7755
  let namespace;
@@ -7322,7 +7772,7 @@ function runBlocks(env, runId) {
7322
7772
  const run3 = findRun(env.root, runId);
7323
7773
  if (!run3) return [];
7324
7774
  const chat = run3.mode === "chat";
7325
- const recorded = readJsonOr(join30(env.root, "runs", run3.id, "caller.json"), null);
7775
+ const recorded = readJsonOr(join31(env.root, "runs", run3.id, "caller.json"), null);
7326
7776
  const context = recorded ?? {
7327
7777
  runId: run3.id,
7328
7778
  sessionId: run3.sessionId,
@@ -7421,12 +7871,12 @@ var Maintenance = class {
7421
7871
  let request2;
7422
7872
  let shown2 = "";
7423
7873
  if (job.kind === "ingest") {
7424
- const directory = join30(wiki, "raw", job.sourceId);
7425
- const name = readJsonOr(join30(directory, "meta.json"), null)?.name;
7874
+ const directory = join31(wiki, "raw", job.sourceId);
7875
+ const name = readJsonOr(join31(directory, "meta.json"), null)?.name;
7426
7876
  const sourceName = typeof name === "string" ? name : "source";
7427
7877
  let text6 = null;
7428
7878
  try {
7429
- text6 = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync16(join30(directory, sourceName)));
7879
+ text6 = new TextDecoder("utf-8", { fatal: true }).decode(readFileSync17(join31(directory, sourceName)));
7430
7880
  } catch {
7431
7881
  text6 = null;
7432
7882
  }
@@ -7473,7 +7923,7 @@ var Maintenance = class {
7473
7923
  const context = trigger.kind === "consolidate" ? { pages: [], memories: [] } : maintainerContext(root, namespace, shown2);
7474
7924
  let index = "";
7475
7925
  try {
7476
- index = takeChars(readFileSync16(join30(wiki, "index.md"), "utf8"), 8e3);
7926
+ index = takeChars(readFileSync17(join31(wiki, "index.md"), "utf8"), 8e3);
7477
7927
  } catch {
7478
7928
  index = "";
7479
7929
  }
@@ -7534,28 +7984,28 @@ var Maintenance = class {
7534
7984
  };
7535
7985
 
7536
7986
  // src/memory/migrate.ts
7537
- import { existsSync as existsSync10, renameSync as renameSync7, rmSync as rmSync7 } from "node:fs";
7538
- import { join as join31 } from "node:path";
7987
+ import { existsSync as existsSync11, renameSync as renameSync7, rmSync as rmSync7 } from "node:fs";
7988
+ import { join as join32 } from "node:path";
7539
7989
  var WIKI_VERSION = 3;
7540
- var storagePath = (root) => join31(root, "config/storage.json");
7990
+ var storagePath = (root) => join32(root, "config/storage.json");
7541
7991
  function version(root) {
7542
7992
  const stored = readJsonOr(storagePath(root), null)?.wikiVersion;
7543
7993
  if (typeof stored === "number" && Number.isInteger(stored) && stored >= 0) return stored;
7544
7994
  const wiki = wikiDir(root);
7545
- if (isDir(join31(root, "knowledge"))) return 1;
7546
- if (!existsSync10(join31(wiki, "blocks")) && (isDir(join31(wiki, "docs")) || isDir(join31(wiki, "memory")))) return 2;
7995
+ if (isDir(join32(root, "knowledge"))) return 1;
7996
+ if (!existsSync11(join32(wiki, "blocks")) && (isDir(join32(wiki, "docs")) || isDir(join32(wiki, "memory")))) return 2;
7547
7997
  return WIKI_VERSION;
7548
7998
  }
7549
7999
  var stamp = () => now().replace(/[:.]/g, "-");
7550
8000
  function backUp2(root, from, name) {
7551
- if (!existsSync10(from)) return;
7552
- renameSync7(from, existsSync10(join31(root, name)) ? join31(root, `${name}-${stamp()}`) : join31(root, name));
8001
+ if (!existsSync11(from)) return;
8002
+ renameSync7(from, existsSync11(join32(root, name)) ? join32(root, `${name}-${stamp()}`) : join32(root, name));
7553
8003
  }
7554
8004
  function run2(env, agentIds = []) {
7555
8005
  const { root } = env;
7556
8006
  if (version(root) >= WIKI_VERSION) return false;
7557
8007
  const failed = (detail) => new HarnessError("migration_failed", `The Wiki was not migrated to version 3; your Wiki is unchanged. ${detail}`);
7558
- const staging = join31(root, "wiki.migrating");
8008
+ const staging = join32(root, "wiki.migrating");
7559
8009
  rmSync7(staging, { recursive: true, force: true });
7560
8010
  try {
7561
8011
  ensureLayoutAt(staging, env.productName);
@@ -7569,7 +8019,7 @@ function run2(env, agentIds = []) {
7569
8019
  throw failed(error instanceof Error ? error.message : String(error));
7570
8020
  }
7571
8021
  renameSync7(staging, wikiDir(root));
7572
- backUp2(root, join31(root, "knowledge"), "knowledge.pre-v2");
8022
+ backUp2(root, join32(root, "knowledge"), "knowledge.pre-v2");
7573
8023
  ensureLayout(root, env.productName);
7574
8024
  for (const project of listProjects(root)) ensureProjectNamespace(root, project.slug, project.name);
7575
8025
  const enablement2 = enablement(root);
@@ -7578,7 +8028,7 @@ function run2(env, agentIds = []) {
7578
8028
  for (const id of enablementIds(enablement2, "personal")) if (!ids.includes(id)) ids.push(id);
7579
8029
  delete enablement2.personal;
7580
8030
  enablement2[GLOBAL_KEY] = ids;
7581
- writeJson(join31(root, "config/registry-enabled.json"), enablement2);
8031
+ writeJson(join32(root, "config/registry-enabled.json"), enablement2);
7582
8032
  }
7583
8033
  writeIndex(wikiDir(root));
7584
8034
  writeJson(storagePath(root), { wikiVersion: WIKI_VERSION, migratedAt: now() });
@@ -7754,8 +8204,8 @@ function searchAll(env, query, filter = {}) {
7754
8204
  }
7755
8205
 
7756
8206
  // src/plugins/hooks.ts
7757
- import { chmodSync as chmodSync7 } from "node:fs";
7758
- import { join as join32, sep as sep5 } from "node:path";
8207
+ import { chmodSync as chmodSync8 } from "node:fs";
8208
+ import { join as join33, sep as sep5 } from "node:path";
7759
8209
  import { pathToFileURL } from "node:url";
7760
8210
  var HOOK_EVENTS = ["memory.saved", "memory.block.changed", "wiki.page.saved", "history.run.ended"];
7761
8211
  var importFile = new Function("address", "return import(address)");
@@ -7770,7 +8220,7 @@ function entryEvent(change) {
7770
8220
  var Hooks = class {
7771
8221
  constructor(options2) {
7772
8222
  this.options = options2;
7773
- this.statusPath = join32(options2.stateDir, "plugin-hooks.json");
8223
+ this.statusPath = join33(options2.stateDir, "plugin-hooks.json");
7774
8224
  this.stop = onChange((change) => {
7775
8225
  if (change.wiki === this.wiki) this.emit(entryEvent(change));
7776
8226
  });
@@ -7797,7 +8247,7 @@ var Hooks = class {
7797
8247
  if (JSON.stringify(before) === JSON.stringify(next) && all2[path3]) return;
7798
8248
  all2[path3] = next;
7799
8249
  writeJson(this.statusPath, all2);
7800
- chmodSync7(this.statusPath, 384);
8250
+ chmodSync8(this.statusPath, 384);
7801
8251
  if (before.lastError !== next.lastError || before.events.join() !== next.events.join()) this.options.bus.emit({ type: "plugins.changed" });
7802
8252
  }
7803
8253
  /** Loads the hooks files that should run and drops the ones that should not. Safe to call often. */
@@ -7882,8 +8332,8 @@ var Hooks = class {
7882
8332
  };
7883
8333
 
7884
8334
  // src/sessions/approvals.ts
7885
- import { chmodSync as chmodSync8 } from "node:fs";
7886
- import { join as join33 } from "node:path";
8335
+ import { chmodSync as chmodSync9 } from "node:fs";
8336
+ import { join as join34 } from "node:path";
7887
8337
  var APPROVAL_KEYS = ["chat", "pi"];
7888
8338
  function decide(policy, key, sessionId, target) {
7889
8339
  if (policy.auto[key]) return "always";
@@ -7897,7 +8347,7 @@ var approvalKey = (run3) => run3.mode === "chat" ? "chat" : run3.harness;
7897
8347
  var Approvals = class {
7898
8348
  constructor(stateDir, bus) {
7899
8349
  this.bus = bus;
7900
- this.path = join33(stateDir, "approvals.json");
8350
+ this.path = join34(stateDir, "approvals.json");
7901
8351
  }
7902
8352
  path;
7903
8353
  read() {
@@ -7909,7 +8359,7 @@ var Approvals = class {
7909
8359
  update2(policy);
7910
8360
  writeJson(this.path, policy);
7911
8361
  try {
7912
- chmodSync8(this.path, 384);
8362
+ chmodSync9(this.path, 384);
7913
8363
  } catch {
7914
8364
  }
7915
8365
  this.bus.emit({ type: "approvals.changed" });
@@ -8029,7 +8479,7 @@ var RunRegistry = class {
8029
8479
  }
8030
8480
  emit("approval.requested", { approvalId, ...payload });
8031
8481
  emit("status.changed", { status: "waiting_for_approval" });
8032
- return new Promise((resolve8) => {
8482
+ return new Promise((resolve9) => {
8033
8483
  const timer = timeoutMs ? setTimeout(() => this.approvals.get(approvalId)?.resolve("denied"), timeoutMs) : null;
8034
8484
  this.approvals.set(approvalId, {
8035
8485
  runId,
@@ -8038,7 +8488,7 @@ var RunRegistry = class {
8038
8488
  this.approvals.delete(approvalId);
8039
8489
  emit("approval.resolved", { approvalId, decision });
8040
8490
  emit("status.changed", { status: waitingStatus() });
8041
- resolve8(decision);
8491
+ resolve9(decision);
8042
8492
  }
8043
8493
  });
8044
8494
  });
@@ -8049,7 +8499,7 @@ var RunRegistry = class {
8049
8499
  if (finished || this.nativeCleared.delete(questionId)) return Promise.resolve({ selected: [] });
8050
8500
  emit("question.requested", { questionId, ...payload });
8051
8501
  emit("status.changed", { status: "waiting_for_input" });
8052
- return new Promise((resolve8) => {
8502
+ return new Promise((resolve9) => {
8053
8503
  this.questions.set(questionId, {
8054
8504
  runId,
8055
8505
  input,
@@ -8057,7 +8507,7 @@ var RunRegistry = class {
8057
8507
  this.questions.delete(questionId);
8058
8508
  emit("question.answered", { questionId, ...answer });
8059
8509
  emit("status.changed", { status: waitingStatus() });
8060
- resolve8(answer);
8510
+ resolve9(answer);
8061
8511
  }
8062
8512
  });
8063
8513
  });
@@ -8136,7 +8586,7 @@ var RunRegistry = class {
8136
8586
  }
8137
8587
  /** Interrupts every live run, giving each a moment to stop cleanly. */
8138
8588
  async interruptAll(timeoutMs = 3e3) {
8139
- await Promise.race([Promise.allSettled(this.activeRunIds().map((id) => this.interrupt(id))), new Promise((resolve8) => setTimeout(resolve8, timeoutMs))]);
8589
+ await Promise.race([Promise.allSettled(this.activeRunIds().map((id) => this.interrupt(id))), new Promise((resolve9) => setTimeout(resolve9, timeoutMs))]);
8140
8590
  }
8141
8591
  resolveApproval(runId, approvalId, decision) {
8142
8592
  if (decision !== "approved" && decision !== "denied" && decision !== "native") throw new HarnessError("invalid_request", "Unknown decision.");
@@ -8167,13 +8617,13 @@ var RunRegistry = class {
8167
8617
  };
8168
8618
 
8169
8619
  // src/sessions/sessions.ts
8170
- import { existsSync as existsSync12, mkdirSync as mkdirSync11 } from "node:fs";
8171
- import { join as join35 } from "node:path";
8620
+ import { existsSync as existsSync13, mkdirSync as mkdirSync11 } from "node:fs";
8621
+ import { join as join36 } from "node:path";
8172
8622
 
8173
8623
  // src/capabilities/runSnapshot.ts
8174
- import { existsSync as existsSync11 } from "node:fs";
8175
- import { join as join34 } from "node:path";
8176
- var path2 = (root, runId) => join34(root, "runs", safeId(runId), "capabilities.json");
8624
+ import { existsSync as existsSync12 } from "node:fs";
8625
+ import { join as join35 } from "node:path";
8626
+ var path2 = (root, runId) => join35(root, "runs", safeId(runId), "capabilities.json");
8177
8627
  function strings2(value) {
8178
8628
  return Array.isArray(value) ? [...new Set(value.filter((item) => typeof item === "string" && item.length > 0))] : [];
8179
8629
  }
@@ -8191,7 +8641,7 @@ function freezeRun(root, runId, caller, set2) {
8191
8641
  connectors: strings2(set2.connectors),
8192
8642
  contributions: strings2(set2.contributions)
8193
8643
  };
8194
- const existing = existsSync11(target) ? readJson(target) : null;
8644
+ const existing = existsSync12(target) ? readJson(target) : null;
8195
8645
  if (existing) {
8196
8646
  if (JSON.stringify(existing) !== JSON.stringify(snapshot3)) throw new HarnessError("capabilities_frozen", "This run already has a different frozen capability set.");
8197
8647
  return existing;
@@ -8214,7 +8664,7 @@ function loadRun2(root, runId) {
8214
8664
  }
8215
8665
 
8216
8666
  // src/memory/dispatch.ts
8217
- function dispatch(env, context, call, params) {
8667
+ function dispatch(env, context, call, params, contributions2) {
8218
8668
  switch (call) {
8219
8669
  case "wiki.entrypoint": {
8220
8670
  const recorded = context.runId ? safely(() => load3(env.root, context.runId)) : null;
@@ -8238,7 +8688,7 @@ function dispatch(env, context, call, params) {
8238
8688
  case "wiki.list":
8239
8689
  return list4(env, params);
8240
8690
  case "wiki.skill":
8241
- return skill(env, context, params);
8691
+ return skill(env, context, params, contributions2);
8242
8692
  case "wiki.memory":
8243
8693
  return memories(env, context, params);
8244
8694
  case "wiki.propose": {
@@ -8260,7 +8710,7 @@ function safely(read3) {
8260
8710
  }
8261
8711
 
8262
8712
  // src/tools/memoryTools.ts
8263
- function memoryTools(env, context) {
8713
+ function memoryTools(env, context, contributions2) {
8264
8714
  return WIKI_AGENT_TOOLS.map((tool) => ({
8265
8715
  name: tool.name,
8266
8716
  label: tool.label,
@@ -8268,7 +8718,7 @@ function memoryTools(env, context) {
8268
8718
  parameters: (Type) => Type.Unsafe(tool.parameters),
8269
8719
  execute: async (input) => {
8270
8720
  try {
8271
- return dispatch(env, context, tool.call, input) || "Nothing found.";
8721
+ return dispatch(env, context, tool.call, input, contributions2) || "Nothing found.";
8272
8722
  } catch (error) {
8273
8723
  return `The Wiki could not do that: ${error instanceof Error ? error.message : String(error)}`;
8274
8724
  }
@@ -8277,14 +8727,14 @@ function memoryTools(env, context) {
8277
8727
  }
8278
8728
 
8279
8729
  // src/sessions/history.ts
8280
- import { readFileSync as readFileSync17, realpathSync } from "node:fs";
8730
+ import { readFileSync as readFileSync18, realpathSync } from "node:fs";
8281
8731
  import { isAbsolute as isAbsolute4, relative as relative5 } from "node:path";
8282
8732
  var unavailable = () => new HarnessError("native_resume_unavailable", "The saved conversation is unavailable; this reply cannot be continued.");
8283
8733
  function nativeId(sessionDir, path3) {
8284
8734
  const file = realpathSync(path3);
8285
8735
  const within = relative5(realpathSync(sessionDir), file);
8286
8736
  if (within.startsWith("..") || isAbsolute4(within) || !file.endsWith(".jsonl")) throw new Error("Outside history");
8287
- const header = JSON.parse(readFileSync17(file, "utf8").split("\n", 1)[0]);
8737
+ const header = JSON.parse(readFileSync18(file, "utf8").split("\n", 1)[0]);
8288
8738
  if (header.type !== "session" || !header.id) throw new Error("Missing session identity");
8289
8739
  return header.id;
8290
8740
  }
@@ -8336,7 +8786,7 @@ ${text6}`);
8336
8786
  async function waitForChildren(root, runId, signal) {
8337
8787
  while (!signal?.aborted) {
8338
8788
  if (settled(root, runId)) return true;
8339
- await new Promise((resolve8) => setTimeout(resolve8, 250));
8789
+ await new Promise((resolve9) => setTimeout(resolve9, 250));
8340
8790
  }
8341
8791
  return false;
8342
8792
  }
@@ -8417,11 +8867,11 @@ var MAX_TEXT = 4e3;
8417
8867
  var clip2 = (text6) => text6.length > MAX_TEXT ? `${text6.slice(0, MAX_TEXT)}\u2026` : text6;
8418
8868
  function dismissable(answer, fallback, options2) {
8419
8869
  if (!options2?.signal && !options2?.timeout) return answer;
8420
- return new Promise((resolve8) => {
8870
+ return new Promise((resolve9) => {
8421
8871
  let timer;
8422
8872
  const done = (value) => {
8423
8873
  if (timer) clearTimeout(timer);
8424
- resolve8(value);
8874
+ resolve9(value);
8425
8875
  };
8426
8876
  if (options2.signal?.aborted) return done(fallback);
8427
8877
  options2.signal?.addEventListener("abort", () => done(fallback), { once: true });
@@ -8903,10 +9353,10 @@ var piSessionFactory = async ({ params, allow, gate, tools: tools3, runtime: loa
8903
9353
  };
8904
9354
 
8905
9355
  // src/sessions/sessions.ts
8906
- function firstPartyFor(root, caller) {
9356
+ function firstPartyFor(root, contributions2) {
8907
9357
  const choices = load2(root);
8908
- const scope = { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null };
8909
- return firstPartyExtensions((id) => attached(choices, [id], scope, FIRST_PARTY_PLUGINS.find((plugin) => plugin.id === id)?.switchable?.on ?? false), choices.settings);
9358
+ const allowed2 = new Set(contributions2);
9359
+ return firstPartyExtensions((id) => FIRST_PARTY_PLUGINS.find((plugin) => plugin.id === id)?.code?.some((code) => allowed2.has(`${id}/code/${code.name}`)) ?? false, choices.settings);
8910
9360
  }
8911
9361
  var Sessions = class {
8912
9362
  constructor(options2) {
@@ -8969,7 +9419,7 @@ var Sessions = class {
8969
9419
  ...WIKI_TOOL_NAMES,
8970
9420
  ...broker?.toolNames ?? []
8971
9421
  ])];
8972
- const available = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, requestedTools) : { version: 1, tools: requestedTools, connectors: [], contributions: [] };
9422
+ const available = this.options.resolveCapabilities ? await this.options.resolveCapabilities(caller, requestedTools, input.cwd) : { version: 1, tools: requestedTools, connectors: [], contributions: [] };
8973
9423
  const requested = input.capabilities ? capabilitySet(input.capabilities) : available;
8974
9424
  const capabilities = {
8975
9425
  version: 1,
@@ -9004,7 +9454,7 @@ var Sessions = class {
9004
9454
  trigger: input.run.trigger ?? null
9005
9455
  });
9006
9456
  try {
9007
- writeJson(join35(runDir(root, runId), "caller.json"), caller);
9457
+ writeJson(join36(runDir(root, runId), "caller.json"), caller);
9008
9458
  } catch {
9009
9459
  }
9010
9460
  const fail = (error, code = "provider_unavailable") => {
@@ -9015,7 +9465,7 @@ var Sessions = class {
9015
9465
  let system;
9016
9466
  let update2;
9017
9467
  try {
9018
- if (!existsSync12(input.sessionDir)) mkdirSync11(input.sessionDir, { recursive: true });
9468
+ if (!existsSync13(input.sessionDir)) mkdirSync11(input.sessionDir, { recursive: true });
9019
9469
  const launch = input.launch ?? {};
9020
9470
  const effectiveLaunch = {
9021
9471
  ...launch,
@@ -9023,7 +9473,7 @@ var Sessions = class {
9023
9473
  connectionTools: (launch.connectionTools ?? []).filter((name) => capabilities.tools.includes(name)),
9024
9474
  toolGroups: launch.toolGroups?.map((group) => ({ ...group, tools: group.tools.filter((name) => capabilities.tools.includes(name)) })).filter((group) => group.tools.length > 0)
9025
9475
  };
9026
- ({ system, update: update2 } = sessionLaunch(this.env, caller, effectiveLaunch, historyFile));
9476
+ ({ system, update: update2 } = sessionLaunch(this.env, caller, effectiveLaunch, historyFile, capabilities.contributions));
9027
9477
  } catch (error) {
9028
9478
  fail(error);
9029
9479
  throw error;
@@ -9037,7 +9487,7 @@ var Sessions = class {
9037
9487
  try {
9038
9488
  const tools3 = [
9039
9489
  ...broker ? broker.tools(session, { readOnly: typeof input.connections === "object" && input.connections.readOnly === true }) : [],
9040
- ...memoryTools(this.options.env, caller),
9490
+ ...memoryTools(this.options.env, caller, capabilities.contributions),
9041
9491
  askUserTool(run3),
9042
9492
  pluginProposeTool(this.options.env, caller)
9043
9493
  ];
@@ -9055,9 +9505,9 @@ var Sessions = class {
9055
9505
  preamble: input.preamble ?? null,
9056
9506
  automatic: input.automatic,
9057
9507
  account: context,
9058
- resources: resources(root, caller.projectId ?? null, caller.agentId ?? null),
9059
- inline: [...firstPartyFor(root, caller), runLineageJoin(root, runId)],
9060
- ...plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null } }), loaded: (extensions) => plugins.loaded(extensions) } } : {},
9508
+ resources: resources(root, caller.projectId ?? null, caller.agentId ?? null, capabilities.contributions),
9509
+ inline: [...firstPartyFor(root, capabilities.contributions), runLineageJoin(root, runId)],
9510
+ ...plugins ? { plugins: { loading: () => plugins.loading({ cwd: input.cwd, agentDir: context.configDir, install: true, caller: { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null }, contributions: capabilities.contributions }), loaded: (extensions) => plugins.loaded(extensions) } } : {},
9061
9511
  system,
9062
9512
  update: update2
9063
9513
  };
@@ -9093,8 +9543,8 @@ var Sessions = class {
9093
9543
  };
9094
9544
 
9095
9545
  // src/terminal/terminals.ts
9096
- import { chmodSync as chmodSync10, existsSync as existsSync13, mkdirSync as mkdirSync13, readFileSync as readFileSync18, writeFileSync as writeFileSync3 } from "node:fs";
9097
- import { dirname as dirname13, join as join37 } from "node:path";
9546
+ import { chmodSync as chmodSync11, existsSync as existsSync14, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync3 } from "node:fs";
9547
+ import { dirname as dirname13, join as join38 } from "node:path";
9098
9548
  import { fileURLToPath } from "node:url";
9099
9549
 
9100
9550
  // src/terminal/client.ts
@@ -9114,7 +9564,7 @@ function connectionFromEnv(env = process.env) {
9114
9564
  return { socket, token };
9115
9565
  }
9116
9566
  function request(connection, body2, what, timeoutMs) {
9117
- return new Promise((resolve8, reject) => {
9567
+ return new Promise((resolve9, reject) => {
9118
9568
  const stream = createConnection(connection.socket);
9119
9569
  let buffer = "";
9120
9570
  let settled2 = false;
@@ -9140,7 +9590,7 @@ function request(connection, body2, what, timeoutMs) {
9140
9590
  if (reply.error) {
9141
9591
  const error = typeof reply.error === "string" ? { message: reply.error } : reply.error;
9142
9592
  finish(() => reject(new SocketError(error.code ?? "error", error.message ?? `${what} failed.`)));
9143
- } else finish(() => resolve8(reply.result));
9593
+ } else finish(() => resolve9(reply.result));
9144
9594
  } catch {
9145
9595
  finish(() => reject(new SocketError("invalid_reply", "The harness sent an unreadable reply.")));
9146
9596
  }
@@ -9344,15 +9794,15 @@ var TerminalController = class {
9344
9794
  if (this.disposed) throw new HarnessError("terminal_not_ready", "Reopen the terminal before sending instructions.");
9345
9795
  if (this.activeRun || this.delivery) throw new HarnessError("session_busy", "This terminal has an active task or delivery.");
9346
9796
  const deadline = Date.now() + READY_TIMEOUT_MS;
9347
- while (!this.ready && !this.startupError && !this.disposed && Date.now() < deadline) await new Promise((resolve8) => setTimeout(resolve8, 50));
9797
+ while (!this.ready && !this.startupError && !this.disposed && Date.now() < deadline) await new Promise((resolve9) => setTimeout(resolve9, 50));
9348
9798
  if (this.disposed) throw new HarnessError("terminal_not_ready", "The terminal stopped during initialization.");
9349
9799
  if (this.startupError) throw new HarnessError("terminal_not_ready", this.startupError);
9350
9800
  if (!this.ready) throw new HarnessError("terminal_not_ready", `The native telemetry handshake is missing (${this.startupPhase}). Inspect the terminal sign-in or trust screen.`);
9351
9801
  if (this.dirty) throw new HarnessError("terminal_has_draft", "The terminal contains user input. Submit or clear it before handing back control.");
9352
9802
  let timer;
9353
- const ack = new Promise((resolve8, reject) => {
9803
+ const ack = new Promise((resolve9, reject) => {
9354
9804
  timer = setTimeout(() => reject(new HarnessError("delivery_uncertain", "The native prompt acknowledgement was not received. Inspect the terminal; this instruction will not be resent.")), ACK_TIMEOUT_MS);
9355
- this.delivery = { runId, text: text6, resolve: resolve8, reject };
9805
+ this.delivery = { runId, text: text6, resolve: resolve9, reject };
9356
9806
  });
9357
9807
  void ack.catch(() => {
9358
9808
  });
@@ -9469,16 +9919,16 @@ var TerminalController = class {
9469
9919
 
9470
9920
  // src/terminal/socket.ts
9471
9921
  import { createHash as createHash7 } from "node:crypto";
9472
- import { chmodSync as chmodSync9, mkdirSync as mkdirSync12, rmSync as rmSync8 } from "node:fs";
9922
+ import { chmodSync as chmodSync10, mkdirSync as mkdirSync12, rmSync as rmSync8 } from "node:fs";
9473
9923
  import { createServer as createServer2 } from "node:net";
9474
9924
  import { tmpdir } from "node:os";
9475
- import { dirname as dirname12, join as join36 } from "node:path";
9925
+ import { dirname as dirname12, join as join37 } from "node:path";
9476
9926
  var MAX_REQUEST_BYTES = 1024 * 1024;
9477
9927
  var MAX_SOCKET_PATH = 100;
9478
9928
  function socketPath(stateDir) {
9479
- const path3 = join36(stateDir, "run", "harness.sock");
9929
+ const path3 = join37(stateDir, "run", "harness.sock");
9480
9930
  if (Buffer.byteLength(path3) <= MAX_SOCKET_PATH) return path3;
9481
- return join36(tmpdir(), `xh-${createHash7("sha256").update(path3).digest("hex").slice(0, 16)}.sock`);
9931
+ return join37(tmpdir(), `xh-${createHash7("sha256").update(path3).digest("hex").slice(0, 16)}.sock`);
9482
9932
  }
9483
9933
  var SessionSocket = class {
9484
9934
  constructor(path3, handlers) {
@@ -9575,7 +10025,7 @@ var SessionSocket = class {
9575
10025
  }
9576
10026
  /** Binds the socket once; later calls wait for the same bind. */
9577
10027
  listen() {
9578
- this.listening ??= new Promise((resolve8, reject) => {
10028
+ this.listening ??= new Promise((resolve9, reject) => {
9579
10029
  mkdirSync12(dirname12(this.path), { recursive: true, mode: 448 });
9580
10030
  rmSync8(this.path, { force: true });
9581
10031
  const server = createServer2((client) => this.serve(client));
@@ -9585,12 +10035,12 @@ var SessionSocket = class {
9585
10035
  });
9586
10036
  server.listen(this.path, () => {
9587
10037
  try {
9588
- chmodSync9(this.path, 384);
10038
+ chmodSync10(this.path, 384);
9589
10039
  } catch (error) {
9590
10040
  log("session socket permissions", error instanceof Error ? error.message : String(error));
9591
10041
  }
9592
10042
  this.server = server;
9593
- resolve8();
10043
+ resolve9();
9594
10044
  });
9595
10045
  });
9596
10046
  return this.listening;
@@ -9601,7 +10051,7 @@ var SessionSocket = class {
9601
10051
  this.server = null;
9602
10052
  this.listening = null;
9603
10053
  for (const client of this.clients) client.destroy();
9604
- if (server) await new Promise((resolve8) => server.close(() => resolve8()));
10054
+ if (server) await new Promise((resolve9) => server.close(() => resolve9()));
9605
10055
  rmSync8(this.path, { force: true });
9606
10056
  }
9607
10057
  };
@@ -9609,8 +10059,8 @@ var SessionSocket = class {
9609
10059
  // src/terminal/terminals.ts
9610
10060
  function defaultAssets() {
9611
10061
  const here = dirname13(fileURLToPath(import.meta.url));
9612
- const dist = [here, join37(here, "..", "..", "dist"), join37(here, "..", "dist")].find((folder) => existsSync13(join37(folder, "terminal-pi.js"))) ?? here;
9613
- return { node: process.execPath, cli: join37(dist, "cli.js"), piExtension: join37(dist, "terminal-pi.js") };
10062
+ const dist = [here, join38(here, "..", "..", "dist"), join38(here, "..", "dist")].find((folder) => existsSync14(join38(folder, "terminal-pi.js"))) ?? here;
10063
+ return { node: process.execPath, cli: join38(dist, "cli.js"), piExtension: join38(dist, "terminal-pi.js") };
9614
10064
  }
9615
10065
  var READ_ONLY_TOOLS = /* @__PURE__ */ new Set(["read", "grep", "find", "ls"]);
9616
10066
  var Terminals = class {
@@ -9628,23 +10078,23 @@ var Terminals = class {
9628
10078
  assets;
9629
10079
  /** The commands a terminal session finds on PATH. Rewritten only when they change. */
9630
10080
  ensureCommands() {
9631
- const bin = join37(this.options.stateDir, "run", "bin");
10081
+ const bin = join38(this.options.stateDir, "run", "bin");
9632
10082
  mkdirSync13(bin, { recursive: true });
9633
10083
  const quote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
9634
10084
  for (const [name, mode] of [[WIKI_COMMAND, "wiki"], [APPS_COMMAND, "mcp"]]) {
9635
- const wrapper = join37(bin, name);
10085
+ const wrapper = join38(bin, name);
9636
10086
  const text6 = `#!/bin/sh
9637
10087
  exec ${quote(this.assets.node)} ${quote(this.assets.cli)} ${mode} "$@"
9638
10088
  `;
9639
10089
  let current = null;
9640
10090
  try {
9641
- current = readFileSync18(wrapper, "utf8");
10091
+ current = readFileSync19(wrapper, "utf8");
9642
10092
  } catch {
9643
10093
  current = null;
9644
10094
  }
9645
10095
  if (current !== text6) {
9646
10096
  writeFileSync3(wrapper, text6);
9647
- chmodSync10(wrapper, 493);
10097
+ chmodSync11(wrapper, 493);
9648
10098
  }
9649
10099
  }
9650
10100
  return bin;
@@ -9711,7 +10161,7 @@ exec ${quote(this.assets.node)} ${quote(this.assets.cli)} ${mode} "$@"
9711
10161
  return live;
9712
10162
  }
9713
10163
  historyDir(input) {
9714
- return join37(input.sessionFolder, "pi");
10164
+ return join38(input.sessionFolder, "pi");
9715
10165
  }
9716
10166
  runRecord(live, runId, text6) {
9717
10167
  const { input } = live;
@@ -9943,12 +10393,12 @@ var Harness = class {
9943
10393
  constructor(options2) {
9944
10394
  this.stateDir = options2.stateDir;
9945
10395
  mkdirSync14(this.stateDir, { recursive: true });
9946
- isolateProcess(join38(this.stateDir, "accounts"));
10396
+ isolateProcess(join39(this.stateDir, "accounts"));
9947
10397
  this.env = new Env(options2.dataRoot || null, options2.directory ?? EMPTY_DIRECTORY, options2.productName ?? DEFAULT_PRODUCT_NAME, this.bus);
9948
10398
  this.policy = options2.policy ?? {};
9949
10399
  configureBuiltins(options2.builtins ?? []);
9950
- const secrets = options2.secrets ?? fileSecretStore(join38(this.stateDir, "secrets.json"));
9951
- this.accounts = new Accounts({ dir: join38(this.stateDir, "accounts"), secrets, bus: this.bus });
10400
+ const secrets = options2.secrets ?? fileSecretStore(join39(this.stateDir, "secrets.json"));
10401
+ this.accounts = new Accounts({ dir: join39(this.stateDir, "accounts"), secrets, bus: this.bus });
9952
10402
  this.approvals = new Approvals(this.stateDir, this.bus);
9953
10403
  this.runs = new RunRegistry({
9954
10404
  root: () => this.env.root,
@@ -9960,7 +10410,7 @@ var Harness = class {
9960
10410
  ended: (runId, sessionId) => this.apps.revokeRun(runId, sessionId)
9961
10411
  });
9962
10412
  this.apps = new ConnectedApps({ env: this.env, stateDir: this.stateDir, secrets, bus: this.bus, runs: this.runs, policy: this.policy });
9963
- this.plugins = new Plugins({ stateDir: this.stateDir, bus: this.bus, resolver: options2.resolver, attachments: () => this.env.dataRoot ? load2(this.env.dataRoot) : null, packagesDir: () => this.env.dataRoot ? join38(this.env.dataRoot, "plugins") : null });
10413
+ this.plugins = new Plugins({ stateDir: this.stateDir, bus: this.bus, resolver: options2.resolver, attachments: () => this.env.dataRoot ? load2(this.env.dataRoot) : null, packagesDir: () => this.env.dataRoot ? join39(this.env.dataRoot, "plugins") : null });
9964
10414
  this.hooks = new Hooks({
9965
10415
  stateDir: this.stateDir,
9966
10416
  bus: this.bus,
@@ -9968,9 +10418,9 @@ var Harness = class {
9968
10418
  // The shared folder stands in for an account: packages are the same for all of them.
9969
10419
  wanted: async () => {
9970
10420
  if (!this.env.dataRoot) return [];
9971
- const shared = ensureSharedDir(join38(this.stateDir, "accounts"));
9972
- const named2 = readJsonOr(join38(shared, "settings.json"), {}).packages;
9973
- if (!packageFolders(join38(this.env.dataRoot, "plugins")).length && !(Array.isArray(named2) && named2.length)) return [];
10421
+ const shared = ensureSharedDir(join39(this.stateDir, "accounts"));
10422
+ const named2 = readJsonOr(join39(shared, "settings.json"), {}).packages;
10423
+ if (!packageFolders(join39(this.env.dataRoot, "plugins")).length && !(Array.isArray(named2) && named2.length)) return [];
9974
10424
  return (await this.plugins.loading({ cwd: this.stateDir, agentDir: shared, caller: {} })).hooks;
9975
10425
  },
9976
10426
  call: (connection, tool, args) => this.apps.callUnattended(connection, tool, args ?? {}),
@@ -9993,7 +10443,7 @@ var Harness = class {
9993
10443
  policy: this.policy,
9994
10444
  broker: this.apps,
9995
10445
  plugins: this.plugins,
9996
- resolveCapabilities: (caller, tools3) => resolve4(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, tools: tools3 }),
10446
+ resolveCapabilities: (caller, tools3, cwd) => resolve5(this, { agentId: caller.agentId ?? null, projectId: caller.projectId ?? null, cwd, tools: tools3 }),
9997
10447
  factory: options2.sessionFactory
9998
10448
  });
9999
10449
  this.maintenance = new Maintenance({ env: this.env, bus: this.bus, accounts: this.accounts, factory: options2.maintainerFactory, idleMs: options2.idleMs });
@@ -10029,15 +10479,24 @@ var Harness = class {
10029
10479
  const root = dataRoot;
10030
10480
  this.hooks.watch(wikiDir(root));
10031
10481
  void this.hooks.reload();
10032
- for (const folder of ["registry/skills", "registry/prompts", "registry/rules", "config", "runs"]) mkdirSync14(join38(root, folder), { recursive: true });
10482
+ for (const folder of ["registry/skills", "registry/prompts", "registry/rules", "config", "runs"]) mkdirSync14(join39(root, folder), { recursive: true });
10033
10483
  let migrationError = null;
10484
+ let wikiMigrationError = null;
10034
10485
  try {
10035
10486
  if (run2(this.env, agentIds)) log(`started Wiki storage version ${WIKI_VERSION}; the earlier Wiki is kept as a backup in the data root`);
10036
10487
  } catch (error) {
10037
10488
  migrationError = error instanceof Error ? error.message : String(error);
10489
+ wikiMigrationError = migrationError;
10038
10490
  log("Wiki migration failed", migrationError);
10039
10491
  }
10040
- if (!migrationError) {
10492
+ try {
10493
+ migrateConnectionScopes(root, this.stateDir);
10494
+ } catch (error) {
10495
+ const detail = error instanceof Error ? error.message : String(error);
10496
+ migrationError = migrationError ? `${migrationError}; connector scope migration: ${detail}` : detail;
10497
+ log("connector scope migration failed", detail);
10498
+ }
10499
+ if (!wikiMigrationError) {
10041
10500
  try {
10042
10501
  ensureLayout(root, this.env.productName);
10043
10502
  for (const project of projectsOrNone(root)) ensureProjectNamespace(root, project.slug, project.name);
@@ -10095,18 +10554,18 @@ function createHarness(options2) {
10095
10554
 
10096
10555
  // src/plugins/cli.ts
10097
10556
  import { homedir as homedir5 } from "node:os";
10098
- import { join as join40, resolve as resolve6 } from "node:path";
10557
+ import { join as join41, resolve as resolve7 } from "node:path";
10099
10558
  import { createInterface } from "node:readline/promises";
10100
10559
 
10101
10560
  // src/plugins/install.ts
10102
- import { existsSync as existsSync14, mkdirSync as mkdirSync15, renameSync as renameSync8, statSync as statSync5 } from "node:fs";
10561
+ import { existsSync as existsSync15, mkdirSync as mkdirSync15, renameSync as renameSync8, statSync as statSync6 } from "node:fs";
10103
10562
  import { homedir as homedir4 } from "node:os";
10104
- import { basename as basename8, isAbsolute as isAbsolute5, join as join39, resolve as resolve5 } from "node:path";
10563
+ import { basename as basename8, isAbsolute as isAbsolute5, join as join40, resolve as resolve6 } from "node:path";
10105
10564
  function normalizeSource(input) {
10106
10565
  const source2 = input.trim();
10107
10566
  if (!source2) throw invalid("Name a package: a folder, an npm name, or a git address.");
10108
- if (source2.startsWith("~/")) return join39(process.env.HOME || homedir4(), source2.slice(2));
10109
- if (isAbsolute5(source2)) return resolve5(source2);
10567
+ if (source2.startsWith("~/")) return join40(process.env.HOME || homedir4(), source2.slice(2));
10568
+ if (isAbsolute5(source2)) return resolve6(source2);
10110
10569
  if (source2.startsWith("./") || source2.startsWith("../")) throw invalid("Give the whole path of the folder.");
10111
10570
  return /^(npm:|git:|https?:\/\/|ssh:\/\/|git@)/.test(source2) ? source2 : `npm:${source2}`;
10112
10571
  }
@@ -10119,7 +10578,7 @@ async function manager(harness) {
10119
10578
  var changed = (harness) => harness.bus.emit({ type: "plugins.changed" });
10120
10579
  async function install2(harness, params) {
10121
10580
  const source2 = normalizeSource(params.source);
10122
- if (isAbsolute5(source2) && !(existsSync14(source2) && statSync5(source2).isDirectory())) throw invalid("That folder does not exist.");
10581
+ if (isAbsolute5(source2) && !(existsSync15(source2) && statSync6(source2).isDirectory())) throw invalid("That folder does not exist.");
10123
10582
  await ensureAdopted(harness);
10124
10583
  const before = new Set((await list2(harness, params)).map((plugin) => plugin.id));
10125
10584
  const { settings, packages } = await manager(harness);
@@ -10149,9 +10608,9 @@ function installed(plugins, pluginId) {
10149
10608
  async function remove(harness, params) {
10150
10609
  const plugin = installed(await list2(harness, params), params.pluginId);
10151
10610
  if (plugin.install.kind === "folder") {
10152
- const trash = join39(harness.env.root, "trash", "plugins");
10611
+ const trash = join40(harness.env.root, "trash", "plugins");
10153
10612
  mkdirSync15(trash, { recursive: true });
10154
- renameSync8(plugin.install.source, join39(trash, `${basename8(plugin.install.source)}-${now().replace(/[:.]/g, "-")}`));
10613
+ renameSync8(plugin.install.source, join40(trash, `${basename8(plugin.install.source)}-${now().replace(/[:.]/g, "-")}`));
10155
10614
  } else {
10156
10615
  await (await manager(harness)).packages.removeAndPersist(plugin.install.source);
10157
10616
  }
@@ -10240,10 +10699,10 @@ async function runPluginsCli(argv, dependencies = {}) {
10240
10699
  return command ? 0 : 2;
10241
10700
  }
10242
10701
  const base = dependencies.cwd ?? process.cwd();
10243
- const root = join40(dependencies.home ?? homedir5(), ".xingular-harness");
10244
- const options2 = { dataRoot: parsed2.values.data ? resolve6(base, parsed2.values.data) : join40(root, "data"), stateDir: parsed2.values.state ? resolve6(base, parsed2.values.state) : join40(root, "state"), productName: "Xingular Harness" };
10702
+ const root = join41(dependencies.home ?? homedir5(), ".xingular-harness");
10703
+ const options2 = { dataRoot: parsed2.values.data ? resolve7(base, parsed2.values.data) : join41(root, "data"), stateDir: parsed2.values.state ? resolve7(base, parsed2.values.state) : join41(root, "state"), productName: "Xingular Harness" };
10245
10704
  const harness = dependencies.createHarness ? dependencies.createHarness(options2) : new Harness(options2);
10246
- const scope = { cwd: parsed2.values.cwd ? resolve6(base, parsed2.values.cwd) : null };
10705
+ const scope = { cwd: parsed2.values.cwd ? resolve7(base, parsed2.values.cwd) : null };
10247
10706
  const need = (what) => {
10248
10707
  if (!target) throw new Error(`${command} needs ${what}.`);
10249
10708
  return target;
@@ -10429,7 +10888,7 @@ var RpcPeer = class {
10429
10888
  /** Asks the host to perform a named operation. */
10430
10889
  callHost(call, params, timeoutMs = 3e4) {
10431
10890
  const id = `s${++this.nextCallId}`;
10432
- return new Promise((resolve8, reject) => {
10891
+ return new Promise((resolve9, reject) => {
10433
10892
  const timer = setTimeout(() => {
10434
10893
  this.pendingCalls.delete(id);
10435
10894
  reject(new HarnessError("host_timeout", `The host did not answer ${call}.`));
@@ -10437,7 +10896,7 @@ var RpcPeer = class {
10437
10896
  this.pendingCalls.set(id, {
10438
10897
  resolve: (value) => {
10439
10898
  clearTimeout(timer);
10440
- resolve8(value);
10899
+ resolve9(value);
10441
10900
  },
10442
10901
  reject: (error) => {
10443
10902
  clearTimeout(timer);
@@ -10488,7 +10947,7 @@ var RpcPeer = class {
10488
10947
  };
10489
10948
 
10490
10949
  // src/server/methods.ts
10491
- import { readFileSync as readFileSync19, statSync as statSync6 } from "node:fs";
10950
+ import { readFileSync as readFileSync20, statSync as statSync7 } from "node:fs";
10492
10951
  import { basename as basename9 } from "node:path";
10493
10952
 
10494
10953
  // src/memory/graph.ts
@@ -10702,8 +11161,8 @@ function methods(harness) {
10702
11161
  "wiki.sources": () => listSources(wikiDir2()),
10703
11162
  "wiki.ingest": (params) => {
10704
11163
  const path3 = text5(params, "path");
10705
- if (statSync6(path3).size > MAX_SOURCE_BYTES) throw invalid("Sources larger than 50 MB are not imported.");
10706
- const source2 = maintenance.ingest(optional2(params, "name") ?? basename9(path3), readFileSync19(path3), optional2(params, "namespace") ?? LIBRARY_NAMESPACE);
11164
+ if (statSync7(path3).size > MAX_SOURCE_BYTES) throw invalid("Sources larger than 50 MB are not imported.");
11165
+ const source2 = maintenance.ingest(optional2(params, "name") ?? basename9(path3), readFileSync20(path3), optional2(params, "namespace") ?? LIBRARY_NAMESPACE);
10707
11166
  harness.bus.emit({ type: "wiki.changed" });
10708
11167
  return source2;
10709
11168
  },
@@ -10800,8 +11259,13 @@ function methods(harness) {
10800
11259
  "capabilities.inventory": (params) => inventory2(harness, { ...pluginScope(params), context: params.context ?? {}, launch: params.launch ?? {}, tools: Array.isArray(params.tools) ? params.tools : [], connections: params.connections }),
10801
11260
  "capabilities.attach": (params) => switchCapability(harness, { ...pluginScope(params), id: text5(params, "id"), scope: params.scope ?? null }, true),
10802
11261
  "capabilities.detach": (params) => switchCapability(harness, { ...pluginScope(params), id: text5(params, "id"), scope: params.scope ?? null }, false),
10803
- "capabilities.resolve": (params) => resolve4(harness, { ...pluginScope(params), tools: Array.isArray(params.tools) ? params.tools.filter((item) => typeof item === "string") : [] }),
11262
+ "capabilities.resolve": (params) => resolve5(harness, { ...pluginScope(params), tools: Array.isArray(params.tools) ? params.tools.filter((item) => typeof item === "string") : [] }),
10804
11263
  "capabilities.run": (params) => loadRun2(root(), text5(params, "runId")),
11264
+ "capabilities.intersect": (params) => toolIntersection(
11265
+ capabilitySet(params.parent),
11266
+ capabilitySet(params.assignee),
11267
+ params.tools === void 0 ? void 0 : Array.isArray(params.tools) ? params.tools.filter((item) => typeof item === "string") : []
11268
+ ),
10805
11269
  // ---- Runs --------------------------------------------------------------------------------------
10806
11270
  "runs.load": (params) => snapshot2(root(), text5(params, "runId")),
10807
11271
  "runs.family": (params) => runFamily(root(), text5(params, "runId")),
@@ -10863,24 +11327,24 @@ function attach3(harness, peer) {
10863
11327
  import { spawn as nodeSpawn } from "node:child_process";
10864
11328
  import { createHash as createHash8, randomUUID as randomUUID8 } from "node:crypto";
10865
11329
  import { homedir as homedir6 } from "node:os";
10866
- import { join as join41, resolve as resolve7 } from "node:path";
11330
+ import { join as join42, resolve as resolve8 } from "node:path";
10867
11331
  function standaloneArguments(argv) {
10868
11332
  const command = argv[0];
10869
11333
  if (!command || command.startsWith("--") || command === "-h") return argv;
10870
11334
  return command === "terminal" ? argv.slice(1) : null;
10871
11335
  }
10872
11336
  function parseStandaloneOptions(argv, home = homedir6(), cwd = process.cwd()) {
10873
- const root = join41(home, ".xingular-harness");
11337
+ const root = join42(home, ".xingular-harness");
10874
11338
  const paths = {
10875
- dataRoot: join41(root, "data"),
10876
- stateDir: join41(root, "state")
11339
+ dataRoot: join42(root, "data"),
11340
+ stateDir: join42(root, "state")
10877
11341
  };
10878
11342
  for (let index = 0; index < argv.length; index += 1) {
10879
11343
  const argument = argv[index];
10880
11344
  if (argument !== "--data" && argument !== "--state") throw new Error(`Unknown standalone option: ${argument}`);
10881
11345
  const value = argv[index + 1];
10882
11346
  if (!value || value.startsWith("--")) throw new Error(`${argument} needs a directory path.`);
10883
- paths[argument === "--data" ? "dataRoot" : "stateDir"] = resolve7(cwd, value);
11347
+ paths[argument === "--data" ? "dataRoot" : "stateDir"] = resolve8(cwd, value);
10884
11348
  index += 1;
10885
11349
  }
10886
11350
  return paths;
@@ -10935,7 +11399,7 @@ async function runStandaloneTerminal(argv, dependencies = {}) {
10935
11399
  if (!(dependencies.stdinIsTTY ?? process.stdin.isTTY) || !(dependencies.stdoutIsTTY ?? process.stdout.isTTY)) {
10936
11400
  throw new Error("The interactive Harness terminal needs a TTY. Run it from a terminal, or use `xingular-harness serve` for a host process.");
10937
11401
  }
10938
- const cwd = resolve7(dependencies.cwd ?? process.cwd());
11402
+ const cwd = resolve8(dependencies.cwd ?? process.cwd());
10939
11403
  const paths = parseStandaloneOptions(argv, dependencies.home, cwd);
10940
11404
  const makeHarness = dependencies.createHarness ?? createHarness;
10941
11405
  const harness = makeHarness({ dataRoot: paths.dataRoot, stateDir: paths.stateDir, productName: "Xingular Harness" });
@@ -10944,7 +11408,7 @@ async function runStandaloneTerminal(argv, dependencies = {}) {
10944
11408
  await harness.ready;
10945
11409
  if (harness.accounts.overview().accounts.length === 0) harness.accounts.create("Default", "subscription");
10946
11410
  const cwdHash = createHash8("sha256").update(cwd).digest("hex");
10947
- const sessionFolder = join41(paths.dataRoot, "sessions", "terminal", cwdHash);
11411
+ const sessionFolder = join42(paths.dataRoot, "sessions", "terminal", cwdHash);
10948
11412
  const launch = await harness.terminals.prepare({ sessionId, cwd, sessionFolder });
10949
11413
  return await spawnPi(launch, cwd, dependencies.spawn);
10950
11414
  } finally {
@@ -11005,12 +11469,12 @@ function serveCommand(argv) {
11005
11469
  const harness = createHarness(options2);
11006
11470
  if (switches.has("host-tools")) harness.sessions.provide(remoteTools(peer));
11007
11471
  attach3(harness, peer);
11008
- return new Promise((resolve8) => {
11472
+ return new Promise((resolve9) => {
11009
11473
  let closing = false;
11010
11474
  const shutdown = () => {
11011
11475
  if (closing) return;
11012
11476
  closing = true;
11013
- void Promise.race([harness.close(), new Promise((done) => setTimeout(done, 5e3))]).then(() => resolve8(0));
11477
+ void Promise.race([harness.close(), new Promise((done) => setTimeout(done, 5e3))]).then(() => resolve9(0));
11014
11478
  };
11015
11479
  peer.handle("harness.shutdown", () => void setImmediate(shutdown));
11016
11480
  process.stdin.on("end", shutdown);