@ordewell/core 0.4.6 → 0.4.8

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/index.mjs CHANGED
@@ -108,7 +108,6 @@ var AUTO_COMMANDS = [
108
108
  "pwd",
109
109
  "which",
110
110
  "type",
111
- "env",
112
111
  "uname",
113
112
  "whoami",
114
113
  "cut",
@@ -309,6 +308,52 @@ var MULTIPLEXERS = [
309
308
  "flutter",
310
309
  "dart"
311
310
  ];
311
+ var HELP_FLAGS = ["--help", "--version"];
312
+ var WRAPPER_FAMILY = {
313
+ env: {
314
+ valueFlags: ["-u", "--unset", "-C", "--chdir"],
315
+ booleanFlags: [
316
+ "-i",
317
+ "--ignore-environment",
318
+ "-0",
319
+ "--null",
320
+ "-v",
321
+ "--debug",
322
+ // Optional-value flags: see WrapperSpec.booleanFlags.
323
+ "--block-signal",
324
+ "--default-signal",
325
+ "--ignore-signal",
326
+ "--list-signal-handling",
327
+ ...HELP_FLAGS
328
+ ],
329
+ stringFlags: ["-S", "--split-string"],
330
+ acceptsAssignments: true
331
+ },
332
+ nice: {
333
+ valueFlags: ["-n", "--adjustment"],
334
+ booleanFlags: HELP_FLAGS,
335
+ booleanPattern: /^-\d+$/
336
+ },
337
+ timeout: {
338
+ valueFlags: ["-s", "--signal", "-k", "--kill-after"],
339
+ booleanFlags: ["--preserve-status", "--foreground", "-v", "--verbose", ...HELP_FLAGS],
340
+ positionals: 1
341
+ },
342
+ nohup: { booleanFlags: HELP_FLAGS },
343
+ setsid: { booleanFlags: ["-c", "--ctty", "-f", "--fork", "-w", "--wait", "-h", "-V", ...HELP_FLAGS] },
344
+ stdbuf: {
345
+ valueFlags: ["-i", "--input", "-o", "--output", "-e", "--error"],
346
+ booleanFlags: HELP_FLAGS
347
+ },
348
+ ionice: {
349
+ valueFlags: ["-c", "--class", "-n", "--classdata", "-p", "--pid", "-P", "--pgid", "-u", "--uid"],
350
+ booleanFlags: ["-t", "--ignore", "-h", "-V", ...HELP_FLAGS]
351
+ },
352
+ // The multi-call binary ships its own `rm`, `mv` and `sh`, so the refused
353
+ // name arrives as its first argument.
354
+ busybox: { booleanFlags: ["--list", "--install", ...HELP_FLAGS] },
355
+ command: { booleanFlags: ["-p"], noExecFlags: ["-v", "-V"] }
356
+ };
312
357
  var POSIX_DIALECT = {
313
358
  escape: "\\",
314
359
  escapeInQuotes: true,
@@ -523,10 +568,92 @@ function binaryName(token, dialect) {
523
568
  if (dot <= 0) return base;
524
569
  return dialect.strippedExtensions.includes(base.slice(dot).toLowerCase()) ? base.slice(0, dot) : base;
525
570
  }
571
+ var ASSIGNMENT = /^[A-Za-z_][A-Za-z0-9_]*=/;
526
572
  function toSegment(tokens, piped, dialect) {
527
573
  const rest = [...tokens];
528
- while (rest.length > 0 && /^[A-Za-z_][A-Za-z0-9_]*=/.test(rest[0])) rest.shift();
529
- return { binary: rest.length > 0 ? binaryName(rest[0], dialect) : "", args: rest.slice(1), piped, expandable: false };
574
+ const assignments = [];
575
+ while (rest.length > 0 && ASSIGNMENT.test(rest[0])) assignments.push(rest.shift());
576
+ return {
577
+ binary: rest.length > 0 ? binaryName(rest[0], dialect) : "",
578
+ args: rest.slice(1),
579
+ assignments,
580
+ piped,
581
+ expandable: false
582
+ };
583
+ }
584
+ function joinedShortFlag(list, token) {
585
+ if (token.length <= 2) return void 0;
586
+ return list.find((f) => f.length === 2 && token.startsWith(f));
587
+ }
588
+ function scanWrapperArgs(binary, spec, args) {
589
+ const valueFlags = spec.valueFlags ?? [];
590
+ const stringFlags = spec.stringFlags ?? [];
591
+ const assignments = [];
592
+ let positionals = spec.positionals ?? 0;
593
+ let i = 0;
594
+ while (i < args.length) {
595
+ const token = args[i];
596
+ if (token === "--") {
597
+ i++;
598
+ break;
599
+ }
600
+ if (/^-./.test(token)) {
601
+ const eq = token.startsWith("--") ? token.indexOf("=") : -1;
602
+ const name = eq > 0 ? token.slice(0, eq) : token;
603
+ const selfContained = eq > 0;
604
+ if ((spec.noExecFlags ?? []).includes(name)) return { kind: "no-exec" };
605
+ if (stringFlags.includes(name) || joinedShortFlag(stringFlags, token)) {
606
+ return {
607
+ kind: "refuse",
608
+ reason: `"${binary} ${name}" splits a string into a command, which this classifier cannot inspect. Use the read-only research tools, or describe it as a task.`
609
+ };
610
+ }
611
+ if ((spec.booleanFlags ?? []).includes(name) || spec.booleanPattern?.test(token)) {
612
+ i++;
613
+ continue;
614
+ }
615
+ if (valueFlags.includes(name)) {
616
+ i += selfContained ? 1 : 2;
617
+ continue;
618
+ }
619
+ if (joinedShortFlag(valueFlags, token)) {
620
+ i++;
621
+ continue;
622
+ }
623
+ return {
624
+ kind: "refuse",
625
+ reason: `"${token}" is not a flag this classifier knows on the wrapper "${binary}", so it cannot tell which command "${binary}" would actually run. Re-run without it.`
626
+ };
627
+ }
628
+ if (spec.acceptsAssignments && ASSIGNMENT.test(token)) {
629
+ assignments.push(token);
630
+ i++;
631
+ continue;
632
+ }
633
+ if (positionals > 0) {
634
+ positionals--;
635
+ i++;
636
+ continue;
637
+ }
638
+ break;
639
+ }
640
+ return { kind: "command", tokens: args.slice(i), assignments };
641
+ }
642
+ function unwrap(seg, dialect) {
643
+ const wrappers = [];
644
+ const assignments = [...seg.assignments];
645
+ let current = seg;
646
+ while (WRAPPER_FAMILY[current.binary]) {
647
+ const scan = scanWrapperArgs(current.binary, WRAPPER_FAMILY[current.binary], current.args);
648
+ if (scan.kind === "refuse") return { seg: { ...current, assignments }, wrappers, reason: scan.reason };
649
+ if (scan.kind === "no-exec" || scan.tokens.length === 0) break;
650
+ assignments.push(...scan.assignments);
651
+ wrappers.push(current.binary);
652
+ const inner = toSegment(scan.tokens, current.piped, dialect);
653
+ assignments.push(...inner.assignments);
654
+ current = { ...inner, assignments: [], expandable: current.expandable };
655
+ }
656
+ return { seg: { ...current, assignments }, wrappers };
530
657
  }
531
658
  function lexAll(command, dialect) {
532
659
  const queue = [];
@@ -589,6 +716,16 @@ function refusalFor(seg) {
589
716
  return `"git ${sub} ${flag}" mutates refs. You are a read-only planner \u2014 describe the change as a task instead.`;
590
717
  }
591
718
  }
719
+ const execPathFlag = seg.args.find((a) => a === "--exec-path" || a.startsWith("--exec-path="));
720
+ if (execPathFlag) {
721
+ return `"git ${execPathFlag}" redirects where git looks for its helper programs, and can make it run an arbitrary one. Use the read-only research tools, or describe the change as a task.`;
722
+ }
723
+ if (sub === "grep") {
724
+ const pagerFlag = seg.args.find((a) => a === "-O" || a === "--open-files-in-pager" || a.startsWith("--open-files-in-pager="));
725
+ if (pagerFlag) {
726
+ return `"git grep ${pagerFlag}" opens the results in an arbitrary named program. Use the read-only research tools, or describe the change as a task.`;
727
+ }
728
+ }
592
729
  }
593
730
  if (seg.binary === "find") {
594
731
  const flag = seg.args.find((a) => ["-exec", "-execdir", "-ok", "-okdir", "-delete"].includes(a));
@@ -611,8 +748,17 @@ function refusalFor(seg) {
611
748
  return `Inline code via "${seg.binary} ${seg.args.find(isInlineCodeFlag)}" is not available to the planner. Use the read-only research tools, or describe it as a task.`;
612
749
  }
613
750
  }
751
+ if (seg.assignments.length > 0) {
752
+ return `The environment assignment "${seg.assignments[0]}" decides what "${seg.binary}" actually does, and this classifier cannot judge the value. Re-run the command without the assignment.`;
753
+ }
614
754
  return void 0;
615
755
  }
756
+ function withWrapperNote(reason, wrappers) {
757
+ if (wrappers.length === 0) return reason;
758
+ const quoted = wrappers.map((w) => `"${w}"`);
759
+ const names = quoted.length > 1 ? `${quoted.slice(0, -1).join(", ")} and ${quoted[quoted.length - 1]}` : quoted[0];
760
+ return `${reason} Wrapping it in ${names} does not change what runs.`;
761
+ }
616
762
  function classifyCommand(command, opts = {}) {
617
763
  const trimmed = command.trim();
618
764
  if (!trimmed) return { tier: "refuse", scope: "", reason: "Empty command." };
@@ -641,11 +787,13 @@ function classifyCommand(command, opts = {}) {
641
787
  };
642
788
  }
643
789
  if (segments.length === 0) return { tier: "refuse", scope: "", reason: "No command found." };
644
- for (const seg of segments) {
790
+ const unwrapped = segments.map((seg) => unwrap(seg, dialect));
791
+ for (const { seg, wrappers, reason: wrapperReason } of unwrapped) {
792
+ if (wrapperReason) return { tier: "refuse", scope: "", reason: wrapperReason };
645
793
  const reason = refusalFor(seg);
646
- if (reason) return { tier: "refuse", scope: "", reason };
794
+ if (reason) return { tier: "refuse", scope: "", reason: withWrapperNote(reason, wrappers) };
647
795
  }
648
- const nonAuto = segments.filter((s) => !isAuto(s));
796
+ const nonAuto = unwrapped.map((u) => u.seg).filter((s) => !isAuto(s));
649
797
  if (nonAuto.length === 0) return { tier: "auto", scope: "" };
650
798
  const scope = [...new Set(nonAuto.map(scopeFor))].sort().join(" + ");
651
799
  return { tier: "ask", scope };
@@ -1144,14 +1292,14 @@ var PendingApprovals = class {
1144
1292
  ask(request) {
1145
1293
  const id = `ap-${Date.now()}-${counter++}`;
1146
1294
  const pending = { id, request, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
1147
- return new Promise((resolve3) => {
1295
+ return new Promise((resolve4) => {
1148
1296
  const settle = (granted) => {
1149
1297
  const entry = this.entries.get(id);
1150
1298
  if (!entry) return;
1151
1299
  clearTimeout(entry.timer);
1152
1300
  this.entries.delete(id);
1153
1301
  this.opts.onSettled?.(id, granted);
1154
- resolve3(granted);
1302
+ resolve4(granted);
1155
1303
  };
1156
1304
  const timer = setTimeout(() => settle(false), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS);
1157
1305
  timer.unref?.();
@@ -5791,28 +5939,28 @@ function posixBinDirs() {
5791
5939
  ];
5792
5940
  }
5793
5941
  function windowsBinDirs(env, home) {
5794
- const join14 = (...parts) => path5.win32.join(...parts);
5795
- const under = (base, ...parts) => base ? [join14(base, ...parts)] : [];
5942
+ const join16 = (...parts) => path5.win32.join(...parts);
5943
+ const under = (base, ...parts) => base ? [join16(base, ...parts)] : [];
5796
5944
  const appData = env.APPDATA;
5797
5945
  const localAppData = env.LOCALAPPDATA;
5798
5946
  const programData = env.ProgramData;
5799
5947
  return [
5800
5948
  // Native per-user installers, PowerShell-driven and otherwise.
5801
- join14(home, ".local", "bin"),
5802
- join14(home, ".opencode", "bin"),
5803
- join14(home, ".claude", "bin"),
5804
- join14(home, ".codex", "bin"),
5949
+ join16(home, ".local", "bin"),
5950
+ join16(home, ".opencode", "bin"),
5951
+ join16(home, ".claude", "bin"),
5952
+ join16(home, ".codex", "bin"),
5805
5953
  ...under(localAppData, "Programs", "claude"),
5806
5954
  // Node package managers. npm's global prefix is `%APPDATA%\npm`; the
5807
5955
  // literal spelling is kept as a fallback for a host that does not export
5808
5956
  // APPDATA (a service account, a stripped CI container).
5809
5957
  ...under(appData, "npm"),
5810
- join14(home, "AppData", "Roaming", "npm"),
5958
+ join16(home, "AppData", "Roaming", "npm"),
5811
5959
  ...under(localAppData, "pnpm"),
5812
5960
  ...under(localAppData, "Yarn", "bin"),
5813
- join14(home, ".bun", "bin"),
5961
+ join16(home, ".bun", "bin"),
5814
5962
  // Windows package managers.
5815
- join14(home, "scoop", "shims"),
5963
+ join16(home, "scoop", "shims"),
5816
5964
  ...under(env.SCOOP, "shims"),
5817
5965
  ...under(env.SCOOP_GLOBAL ?? programData, "scoop", "shims"),
5818
5966
  ...under(programData, "chocolatey", "bin"),
@@ -5822,7 +5970,7 @@ function windowsBinDirs(env, home) {
5822
5970
  ...under(localAppData, "Microsoft", "WindowsApps"),
5823
5971
  // Version managers. Volta's Windows home is not the POSIX `~/.volta`.
5824
5972
  ...under(localAppData, "Volta", "bin"),
5825
- join14(home, ".volta", "bin")
5973
+ join16(home, ".volta", "bin")
5826
5974
  ];
5827
5975
  }
5828
5976
  function wellKnownBinDirs(deps = {}) {
@@ -6123,7 +6271,7 @@ var defaultAppServerClient = async (command, args, options) => {
6123
6271
  const PATH = await augmentedPath();
6124
6272
  const launch = await planDirectLaunch(command, args).catch(() => null);
6125
6273
  if (!launch) return null;
6126
- return new Promise((resolve3) => {
6274
+ return new Promise((resolve4) => {
6127
6275
  let child;
6128
6276
  try {
6129
6277
  child = spawn(launch.file, launch.args, {
@@ -6132,7 +6280,7 @@ var defaultAppServerClient = async (command, args, options) => {
6132
6280
  windowsVerbatimArguments: launch.verbatim
6133
6281
  });
6134
6282
  } catch {
6135
- resolve3(null);
6283
+ resolve4(null);
6136
6284
  return;
6137
6285
  }
6138
6286
  let settled = false;
@@ -6141,7 +6289,7 @@ var defaultAppServerClient = async (command, args, options) => {
6141
6289
  settled = true;
6142
6290
  clearTimeout(timer);
6143
6291
  killTree(child);
6144
- resolve3(value);
6292
+ resolve4(value);
6145
6293
  };
6146
6294
  const timer = setTimeout(() => finish(null), options?.timeout ?? 2e4);
6147
6295
  child.on("error", () => finish(null));
@@ -7181,11 +7329,71 @@ var RunnerInstallation = class {
7181
7329
  }
7182
7330
  };
7183
7331
 
7332
+ // src/services/settingsEnvAllowlist.ts
7333
+ var ORDEWELL_SETTABLE_ENV = [
7334
+ "AI_PROVIDER",
7335
+ "ORCHESTRATOR_MODEL",
7336
+ "ORDEWELL_SUBAGENT_MODEL",
7337
+ "ORDEWELL_PLANNER_EFFORT",
7338
+ "ORDEWELL_RESEARCH_ENABLED",
7339
+ "ORDEWELL_MAX_PARALLEL"
7340
+ ];
7341
+ var SETTINGS_ENV_REFUSED = [
7342
+ // Approval posture and pre-approved scope.
7343
+ "ORDEWELL_AUTONOMOUS_MODE",
7344
+ "ORDEWELL_APPROVAL_MODE",
7345
+ "ORDEWELL_APPROVAL_ALLOW",
7346
+ // Where settings are persisted.
7347
+ "ORDEWELL_SETTINGS_PATH",
7348
+ // Runtime options and dynamic loaders.
7349
+ "NODE_OPTIONS",
7350
+ "NODE_REPL_EXTERNAL_MODULE",
7351
+ "NODE_PATH",
7352
+ "ELECTRON_RUN_AS_NODE",
7353
+ "LD_PRELOAD",
7354
+ "LD_LIBRARY_PATH",
7355
+ "LD_AUDIT",
7356
+ "DYLD_INSERT_LIBRARIES",
7357
+ "DYLD_LIBRARY_PATH",
7358
+ "DYLD_FRAMEWORK_PATH",
7359
+ // Executable resolution.
7360
+ "PATH",
7361
+ "PATHEXT",
7362
+ "NODE"
7363
+ ];
7364
+ var REFUSED = new Set(SETTINGS_ENV_REFUSED);
7365
+ function providerEnvKeys() {
7366
+ const keys = [];
7367
+ for (const meta of Object.values(ALL_PROVIDERS)) {
7368
+ if (meta.apiKeyEnvVar) keys.push(meta.apiKeyEnvVar);
7369
+ if (meta.baseUrlEnvVar) keys.push(meta.baseUrlEnvVar);
7370
+ for (const detect of meta.detectEnvVars) keys.push(detect);
7371
+ }
7372
+ return keys;
7373
+ }
7374
+ var PROVIDER_CREDENTIAL_ENV = new Set(providerEnvKeys());
7375
+ var SETTINGS_ENV_ALLOWLIST = new Set(
7376
+ [...providerEnvKeys(), ...ORDEWELL_SETTABLE_ENV].filter((key) => !REFUSED.has(key))
7377
+ );
7378
+ function admitSettingsEnv(env) {
7379
+ const accepted = {};
7380
+ const rejected = [];
7381
+ for (const [key, value] of Object.entries(env)) {
7382
+ if (REFUSED.has(key) || !SETTINGS_ENV_ALLOWLIST.has(key) || typeof value !== "string") {
7383
+ rejected.push(key);
7384
+ continue;
7385
+ }
7386
+ accepted[key] = value;
7387
+ }
7388
+ return { accepted, rejected };
7389
+ }
7390
+
7184
7391
  // src/services/harness/CliAgentAiService.ts
7185
7392
  import { spawn as nodeSpawn } from "child_process";
7186
7393
 
7187
7394
  // src/utils/workspace.ts
7188
7395
  import * as fs3 from "fs";
7396
+ import * as path9 from "path";
7189
7397
  var WorkspaceNotFoundError = class extends Error {
7190
7398
  constructor(workspace) {
7191
7399
  super(`Workspace "${workspace}" does not exist.`);
@@ -7205,6 +7413,46 @@ function assertWorkspaceExists(workspace, deps = {}) {
7205
7413
  const isDirectory = deps.isDirectory ?? defaultIsDirectory;
7206
7414
  if (!isDirectory(workspace)) throw new WorkspaceNotFoundError(workspace);
7207
7415
  }
7416
+ var WorkspaceNotAProjectError = class extends Error {
7417
+ constructor(workspace) {
7418
+ super(`Workspace "${workspace}" is not a project directory (no .ordewell state, version control, or recognized manifest found).`);
7419
+ this.workspace = workspace;
7420
+ this.name = "WorkspaceNotAProjectError";
7421
+ }
7422
+ workspace;
7423
+ };
7424
+ var PROJECT_MARKER_DIRS = [".ordewell", ".git", ".hg", ".svn", ".bzr"];
7425
+ var PROJECT_MARKER_FILES = [
7426
+ "package.json",
7427
+ "Cargo.toml",
7428
+ "go.mod",
7429
+ "pyproject.toml",
7430
+ "setup.py",
7431
+ "requirements.txt",
7432
+ "Pipfile",
7433
+ "pom.xml",
7434
+ "build.gradle",
7435
+ "build.gradle.kts",
7436
+ "Gemfile",
7437
+ "composer.json",
7438
+ "pubspec.yaml",
7439
+ "mix.exs",
7440
+ "CMakeLists.txt"
7441
+ ];
7442
+ function assertWorkspaceIsProject(workspace, deps = {}) {
7443
+ const realpath = deps.realpath ?? ((candidate) => fs3.realpathSync(candidate));
7444
+ const exists = deps.exists ?? ((candidate) => fs3.existsSync(candidate));
7445
+ let resolved;
7446
+ try {
7447
+ resolved = realpath(workspace);
7448
+ } catch {
7449
+ throw new WorkspaceNotAProjectError(workspace);
7450
+ }
7451
+ const hasMarker = [...PROJECT_MARKER_DIRS, ...PROJECT_MARKER_FILES].some(
7452
+ (marker) => exists(path9.join(resolved, marker))
7453
+ );
7454
+ if (!hasMarker) throw new WorkspaceNotAProjectError(workspace);
7455
+ }
7208
7456
 
7209
7457
  // src/services/harness/AgentAdapter.ts
7210
7458
  var LineBuffer = class {
@@ -7260,8 +7508,8 @@ var StdioAgentAdapter = class {
7260
7508
  }
7261
7509
  async start(opts) {
7262
7510
  assertWorkspaceExists(opts.cwd, { isDirectory: this.deps.isDirectory });
7263
- this.processEnded = new Promise((resolve3) => {
7264
- this.markEnded = resolve3;
7511
+ this.processEnded = new Promise((resolve4) => {
7512
+ this.markEnded = resolve4;
7265
7513
  });
7266
7514
  const spec = this.spawnSpec(opts);
7267
7515
  const resolvePath = this.deps.resolvePath ?? augmentedPath;
@@ -7309,7 +7557,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
7309
7557
  onEvent({ type: "error", message: this.exitMessage() });
7310
7558
  return;
7311
7559
  }
7312
- await new Promise((resolve3) => {
7560
+ await new Promise((resolve4) => {
7313
7561
  let settled = false;
7314
7562
  const finish = () => {
7315
7563
  if (settled) return;
@@ -7318,7 +7566,7 @@ ${err.message}`).slice(-STDERR_TAIL_CHARS);
7318
7566
  signal?.removeEventListener("abort", onAbort);
7319
7567
  this.process?.removeListener("exit", onExit);
7320
7568
  this.process?.removeListener("error", onExit);
7321
- resolve3();
7569
+ resolve4();
7322
7570
  };
7323
7571
  const emit = (event) => {
7324
7572
  onEvent(event);
@@ -7519,14 +7767,14 @@ function codexSandboxUnavailableMessage() {
7519
7767
  ].join("\n");
7520
7768
  }
7521
7769
  function runProbe(deps, cwd, env, flags) {
7522
- return new Promise((resolve3) => {
7770
+ return new Promise((resolve4) => {
7523
7771
  let output = "";
7524
7772
  let settled = false;
7525
7773
  const finish = (code) => {
7526
7774
  if (settled) return;
7527
7775
  settled = true;
7528
7776
  clearTimeout(timer);
7529
- resolve3({ code, output });
7777
+ resolve4({ code, output });
7530
7778
  };
7531
7779
  const timer = setTimeout(() => finish(null), PROBE_TIMEOUT_MS);
7532
7780
  timer.unref?.();
@@ -7609,8 +7857,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
7609
7857
  async handshake(opts) {
7610
7858
  this.sandbox = await probeCodexSandbox(this.deps, opts.cwd, this.spawnEnv);
7611
7859
  if (this.sandbox === "unavailable") throw new Error(codexSandboxUnavailableMessage());
7612
- const ready = new Promise((resolve3) => {
7613
- this.settleHandshake = resolve3;
7860
+ const ready = new Promise((resolve4) => {
7861
+ this.settleHandshake = resolve4;
7614
7862
  });
7615
7863
  this.writeLine({
7616
7864
  jsonrpc: "2.0",
@@ -7623,8 +7871,8 @@ var CodexAdapter = class extends StdioAgentAdapter {
7623
7871
  // A binary that rejects `app-server` outright dies immediately; waiting
7624
7872
  // out the timeout would turn a one-line diagnostic into a 30s stall.
7625
7873
  this.processEnded.then(() => false),
7626
- new Promise((resolve3) => {
7627
- const t = setTimeout(() => resolve3(false), HANDSHAKE_TIMEOUT_MS);
7874
+ new Promise((resolve4) => {
7875
+ const t = setTimeout(() => resolve4(false), HANDSHAKE_TIMEOUT_MS);
7628
7876
  t.unref?.();
7629
7877
  })
7630
7878
  ]);
@@ -7912,12 +8160,12 @@ var OpenCodeAdapter = class {
7912
8160
  cwd: opts.cwd,
7913
8161
  windowsVerbatimArguments: launch.verbatim
7914
8162
  });
7915
- const banner = new Promise((resolve3) => {
8163
+ const banner = new Promise((resolve4) => {
7916
8164
  let seen = "";
7917
8165
  const scan = (chunk) => {
7918
8166
  seen += chunk.toString();
7919
8167
  const match = seen.match(/https?:\/\/[^\s]+/);
7920
- if (match) resolve3(match[0].replace(/[.,)]$/, ""));
8168
+ if (match) resolve4(match[0].replace(/[.,)]$/, ""));
7921
8169
  };
7922
8170
  this.process.stdout?.on("data", scan);
7923
8171
  this.process.stderr?.on("data", (chunk) => {
@@ -7926,15 +8174,15 @@ var OpenCodeAdapter = class {
7926
8174
  });
7927
8175
  this.process.on("exit", () => {
7928
8176
  this.exited = true;
7929
- resolve3(null);
8177
+ resolve4(null);
7930
8178
  });
7931
8179
  this.process.on("error", (err) => {
7932
8180
  this.exited = true;
7933
8181
  this.stderrTail = (this.stderrTail + `
7934
8182
  ${err.message}`).slice(-STDERR_TAIL_CHARS2);
7935
- resolve3(null);
8183
+ resolve4(null);
7936
8184
  });
7937
- const timer = setTimeout(() => resolve3(null), SERVER_READY_TIMEOUT_MS);
8185
+ const timer = setTimeout(() => resolve4(null), SERVER_READY_TIMEOUT_MS);
7938
8186
  timer.unref?.();
7939
8187
  });
7940
8188
  this.baseUrl = await banner;
@@ -7964,8 +8212,8 @@ ${this.stderrTail.trim()}` : ""}`);
7964
8212
  const streamAbort = new AbortController();
7965
8213
  let connected = () => {
7966
8214
  };
7967
- const streamReady = new Promise((resolve3) => {
7968
- connected = resolve3;
8215
+ const streamReady = new Promise((resolve4) => {
8216
+ connected = resolve4;
7969
8217
  });
7970
8218
  const live = this.streamEvents(
7971
8219
  streamAbort.signal,
@@ -8117,14 +8365,14 @@ ${this.stderrTail.trim()}` : ""}`);
8117
8365
  }
8118
8366
  }
8119
8367
  }
8120
- async json(method, path15, body, signal) {
8121
- const response = await this.deps.fetch(`${this.baseUrl}${path15}`, {
8368
+ async json(method, path17, body, signal) {
8369
+ const response = await this.deps.fetch(`${this.baseUrl}${path17}`, {
8122
8370
  method,
8123
8371
  headers: { "content-type": "application/json" },
8124
8372
  ...body === void 0 ? {} : { body: JSON.stringify(body) },
8125
8373
  ...signal ? { signal } : {}
8126
8374
  });
8127
- if (!response.ok) throw new Error(`${method} ${path15} failed: ${response.status} ${response.statusText}`);
8375
+ if (!response.ok) throw new Error(`${method} ${path17} failed: ${response.status} ${response.statusText}`);
8128
8376
  return await response.json().catch(() => null);
8129
8377
  }
8130
8378
  nativeSessionId() {
@@ -8938,11 +9186,11 @@ function executionSummary(tasks) {
8938
9186
 
8939
9187
  // src/utils/sessionStore.ts
8940
9188
  import * as fs5 from "fs";
8941
- import * as path10 from "path";
9189
+ import * as path11 from "path";
8942
9190
 
8943
9191
  // src/utils/fsHelpers.ts
8944
9192
  import * as fs4 from "fs";
8945
- import * as path9 from "path";
9193
+ import * as path10 from "path";
8946
9194
  var STATE_DIR = ".ordewell";
8947
9195
  function ensureDir(dir) {
8948
9196
  if (!fs4.existsSync(dir)) {
@@ -8950,13 +9198,19 @@ function ensureDir(dir) {
8950
9198
  }
8951
9199
  }
8952
9200
  function getStateDir(baseDir) {
8953
- return path9.join(baseDir ?? process.cwd(), STATE_DIR);
9201
+ return path10.join(baseDir ?? process.cwd(), STATE_DIR);
9202
+ }
9203
+
9204
+ // src/utils/sessionId.ts
9205
+ import { randomBytes } from "crypto";
9206
+ function mintSessionId() {
9207
+ return `session-${randomBytes(16).toString("hex")}`;
8954
9208
  }
8955
9209
 
8956
9210
  // src/utils/sessionStore.ts
8957
9211
  var SESSIONS_SUBDIR = "sessions";
8958
9212
  function getSessionsDir(baseDir) {
8959
- return path10.join(getStateDir(baseDir), SESSIONS_SUBDIR);
9213
+ return path11.join(getStateDir(baseDir), SESSIONS_SUBDIR);
8960
9214
  }
8961
9215
  var REJECTION_MESSAGE = "This session was created with an older version of Ordewell. Please re-plan.";
8962
9216
  function hasScalarRunner(parsed) {
@@ -8977,7 +9231,7 @@ function saveSession(plan, goal, baseDir, id) {
8977
9231
  ensureDir(sessionsDir);
8978
9232
  const now = (/* @__PURE__ */ new Date()).toISOString();
8979
9233
  const meta = {
8980
- id: id || `session-${Date.now()}`,
9234
+ id: id || mintSessionId(),
8981
9235
  goal: goal || "(untitled)",
8982
9236
  runners: plan.runners,
8983
9237
  taskCount: plan.tasks.flatMap((t) => [t, ...t.subtasks || []]).length,
@@ -8990,7 +9244,7 @@ function saveSession(plan, goal, baseDir, id) {
8990
9244
  plan: JSON.parse(JSON.stringify(plan))
8991
9245
  };
8992
9246
  const filename = sessionFilename(meta);
8993
- const filePath = path10.join(sessionsDir, filename);
9247
+ const filePath = path11.join(sessionsDir, filename);
8994
9248
  fs5.writeFileSync(filePath, JSON.stringify(session, null, 2));
8995
9249
  return meta;
8996
9250
  }
@@ -9000,7 +9254,7 @@ function listSessions(baseDir, logger = defaultLogger) {
9000
9254
  const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
9001
9255
  return files.map((f) => {
9002
9256
  try {
9003
- const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
9257
+ const raw = fs5.readFileSync(path11.join(sessionsDir, f), "utf-8");
9004
9258
  const parsed = JSON.parse(raw);
9005
9259
  if (hasScalarRunner(parsed)) {
9006
9260
  logger.warn("sessionStore", `session file ${f}: ${REJECTION_MESSAGE}`);
@@ -9018,7 +9272,7 @@ function findSessionFile(sessionsDir, sessionId, logger) {
9018
9272
  const files = fs5.readdirSync(sessionsDir).filter((f) => f.endsWith(".json"));
9019
9273
  for (const f of files) {
9020
9274
  try {
9021
- const raw = fs5.readFileSync(path10.join(sessionsDir, f), "utf-8");
9275
+ const raw = fs5.readFileSync(path11.join(sessionsDir, f), "utf-8");
9022
9276
  const session = JSON.parse(raw);
9023
9277
  if (session.meta.id === sessionId) return f;
9024
9278
  } catch (err) {
@@ -9033,7 +9287,7 @@ function loadSession(sessionId, baseDir, logger = defaultLogger) {
9033
9287
  try {
9034
9288
  const file = findSessionFile(sessionsDir, sessionId, logger);
9035
9289
  if (file) {
9036
- const raw = fs5.readFileSync(path10.join(sessionsDir, file), "utf-8");
9290
+ const raw = fs5.readFileSync(path11.join(sessionsDir, file), "utf-8");
9037
9291
  const parsed = JSON.parse(raw);
9038
9292
  if (hasScalarRunner(parsed)) {
9039
9293
  logger.warn("sessionStore", `session file ${file}: ${REJECTION_MESSAGE}`);
@@ -9067,7 +9321,7 @@ function deleteSession(sessionId, baseDir, logger = defaultLogger) {
9067
9321
  try {
9068
9322
  const file = findSessionFile(sessionsDir, sessionId, logger);
9069
9323
  if (file) {
9070
- fs5.unlinkSync(path10.join(sessionsDir, file));
9324
+ fs5.unlinkSync(path11.join(sessionsDir, file));
9071
9325
  return true;
9072
9326
  }
9073
9327
  } catch (err) {
@@ -9086,11 +9340,6 @@ var PlanEditError = class extends Error {
9086
9340
  function sessionRuntimeSettings(settings) {
9087
9341
  return { ...plannerRuntimeToggles(settings), modelAllowlist: settings.modelAllowlist };
9088
9342
  }
9089
- var lastMintedAt = 0;
9090
- function mintSessionId() {
9091
- lastMintedAt = Math.max(Date.now(), lastMintedAt + 1);
9092
- return `session-${lastMintedAt}`;
9093
- }
9094
9343
  var Session = class {
9095
9344
  /** Injected by a test; when present it is the service, forever. */
9096
9345
  pinnedAiService;
@@ -10531,8 +10780,8 @@ var HeadlessRunner = class extends AbstractRunner {
10531
10780
  import { execFile as execFile2 } from "child_process";
10532
10781
  import { promisify as promisify4 } from "util";
10533
10782
  import { tmpdir } from "os";
10534
- import { join as join9 } from "path";
10535
- import { existsSync as existsSync5, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
10783
+ import { join as join10 } from "path";
10784
+ import { existsSync as existsSync6, readFileSync as readFileSync4, unlinkSync as unlinkSync2, writeFileSync as writeFileSync5 } from "fs";
10536
10785
 
10537
10786
  // src/utils/tmux.ts
10538
10787
  import { execSync as execSync2 } from "child_process";
@@ -10637,7 +10886,7 @@ var TmuxSession = class extends AbstractTerminalSession {
10637
10886
  if (this.exited) return;
10638
10887
  let content;
10639
10888
  try {
10640
- content = existsSync5(this.logPath) ? readFileSync4(this.logPath, "utf8") : "";
10889
+ content = existsSync6(this.logPath) ? readFileSync4(this.logPath, "utf8") : "";
10641
10890
  } catch {
10642
10891
  return;
10643
10892
  }
@@ -10816,7 +11065,7 @@ var TmuxRunner = class extends AbstractRunner {
10816
11065
  windowName,
10817
11066
  this.execFileImpl,
10818
11067
  this.pollIntervalMs,
10819
- join9(this.logDir, `${id}.log`),
11068
+ join10(this.logDir, `${id}.log`),
10820
11069
  this.socket
10821
11070
  );
10822
11071
  invocation.env = { ...invocation.env, PATH: await this.resolvePath() };
@@ -10826,26 +11075,128 @@ var TmuxRunner = class extends AbstractRunner {
10826
11075
  }
10827
11076
  };
10828
11077
 
11078
+ // src/utils/daemonToken.ts
11079
+ import { randomBytes as randomBytes2, timingSafeEqual } from "crypto";
11080
+ import { closeSync, mkdirSync as mkdirSync5, openSync, readFileSync as readFileSync5, unlinkSync as unlinkSync3, writeSync } from "fs";
11081
+ import { homedir as homedir5 } from "os";
11082
+ import { join as join11 } from "path";
11083
+ var BEARER_PREFIX = "bearer ";
11084
+ var DAEMON_TOKEN_SUBPROTOCOL_PREFIX = "ordewell.token.";
11085
+ var DAEMON_SUBPROTOCOL = "ordewell.v1";
11086
+ function configDir() {
11087
+ return join11(homedir5(), ".config", "ordewell");
11088
+ }
11089
+ function daemonTokenPath(port) {
11090
+ return join11(configDir(), `server-${port}.token`);
11091
+ }
11092
+ function mintDaemonToken(port) {
11093
+ const token = randomBytes2(32).toString("base64url");
11094
+ const file = daemonTokenPath(port);
11095
+ mkdirSync5(configDir(), { recursive: true });
11096
+ try {
11097
+ unlinkSync3(file);
11098
+ } catch {
11099
+ }
11100
+ const fd = openSync(file, "wx", 384);
11101
+ try {
11102
+ writeSync(fd, token);
11103
+ } finally {
11104
+ closeSync(fd);
11105
+ }
11106
+ return { token, file };
11107
+ }
11108
+ function readDaemonToken(port) {
11109
+ try {
11110
+ const token = readFileSync5(daemonTokenPath(port), "utf8").trim();
11111
+ return token === "" ? void 0 : token;
11112
+ } catch {
11113
+ return void 0;
11114
+ }
11115
+ }
11116
+ function clearDaemonToken(port) {
11117
+ try {
11118
+ unlinkSync3(daemonTokenPath(port));
11119
+ } catch {
11120
+ }
11121
+ }
11122
+ function bearerHeaderValue(token) {
11123
+ return `Bearer ${token}`;
11124
+ }
11125
+ function tokenSubprotocols(token) {
11126
+ return [DAEMON_SUBPROTOCOL, `${DAEMON_TOKEN_SUBPROTOCOL_PREFIX}${token}`];
11127
+ }
11128
+ function extractPresentedToken(carriers) {
11129
+ const authorization = carriers.authorization;
11130
+ if (authorization && authorization.toLowerCase().startsWith(BEARER_PREFIX)) {
11131
+ const value = authorization.slice(BEARER_PREFIX.length).trim();
11132
+ if (value !== "") return value;
11133
+ }
11134
+ for (const offered of (carriers.secWebSocketProtocol ?? "").split(",")) {
11135
+ const value = offered.trim();
11136
+ if (!value.startsWith(DAEMON_TOKEN_SUBPROTOCOL_PREFIX)) continue;
11137
+ const token = value.slice(DAEMON_TOKEN_SUBPROTOCOL_PREFIX.length);
11138
+ if (token !== "") return token;
11139
+ }
11140
+ return void 0;
11141
+ }
11142
+ function tokensMatch(presented, expected) {
11143
+ if (presented === void 0) return false;
11144
+ const a = Buffer.from(presented, "utf8");
11145
+ const b = Buffer.from(expected, "utf8");
11146
+ if (a.length !== b.length) return false;
11147
+ return timingSafeEqual(a, b);
11148
+ }
11149
+
10829
11150
  // src/plugins/RunnerRegistry.ts
10830
11151
  import * as fs7 from "fs";
10831
- import * as path12 from "path";
11152
+ import * as path14 from "path";
10832
11153
  import * as os5 from "os";
10833
- import { execSync as execSync3 } from "child_process";
11154
+ import { execFileSync } from "child_process";
11155
+ import { randomBytes as randomBytes3 } from "crypto";
10834
11156
 
10835
11157
  // src/plugins/FsPluginStore.ts
10836
11158
  import * as fs6 from "fs";
10837
- import * as path11 from "path";
11159
+ import * as path13 from "path";
10838
11160
  import * as os4 from "os";
10839
- function userPluginsDir() {
10840
- return path11.join(os4.homedir(), ".config", "ordewell", "plugins");
11161
+
11162
+ // src/plugins/pluginNames.ts
11163
+ import * as path12 from "path";
11164
+ var PLUGIN_NAME_PATTERN = /^[A-Za-z0-9](?:[A-Za-z0-9._-]*[A-Za-z0-9])?$/;
11165
+ var MAX_PLUGIN_NAME_LENGTH = 64;
11166
+ function isPlainPluginName(name) {
11167
+ return typeof name === "string" && name.length <= MAX_PLUGIN_NAME_LENGTH && PLUGIN_NAME_PATTERN.test(name);
11168
+ }
11169
+ function assertPlainPluginName(name) {
11170
+ if (!isPlainPluginName(name)) {
11171
+ throw new Error(
11172
+ `Invalid plugin name: ${JSON.stringify(name)}. Names must be a single plain segment of letters, digits, '.', '-' or '_' (max ${MAX_PLUGIN_NAME_LENGTH} characters).`
11173
+ );
11174
+ }
10841
11175
  }
11176
+ function resolvePluginInstallDir(pluginsDir, name) {
11177
+ assertPlainPluginName(name);
11178
+ const root = path12.resolve(pluginsDir);
11179
+ const dest = path12.resolve(root, name);
11180
+ const relative3 = path12.relative(root, dest);
11181
+ if (relative3 !== name || relative3 === "" || path12.isAbsolute(relative3)) {
11182
+ throw new Error(`Plugin install path escapes the plugins directory: ${JSON.stringify(name)}`);
11183
+ }
11184
+ return dest;
11185
+ }
11186
+
11187
+ // src/plugins/manifestValidation.ts
10842
11188
  function isValidManifest(obj) {
10843
11189
  if (!obj || typeof obj !== "object") return false;
10844
11190
  const m = obj;
10845
- return typeof m.name === "string" && typeof m.displayName === "string" && typeof m.description === "string" && typeof m.version === "string" && typeof m.runner === "object" && m.runner !== null && typeof m.runner.command === "string" && Array.isArray(m.runner.argsTemplate) && typeof m.features === "object" && m.features !== null && typeof m.modelDiscovery === "object" && m.modelDiscovery !== null;
11191
+ return isPlainPluginName(m.name) && typeof m.displayName === "string" && typeof m.description === "string" && typeof m.version === "string" && typeof m.runner === "object" && m.runner !== null && typeof m.runner.command === "string" && Array.isArray(m.runner.argsTemplate) && typeof m.features === "object" && m.features !== null && typeof m.modelDiscovery === "object" && m.modelDiscovery !== null;
11192
+ }
11193
+
11194
+ // src/plugins/FsPluginStore.ts
11195
+ function userPluginsDir() {
11196
+ return path13.join(os4.homedir(), ".config", "ordewell", "plugins");
10846
11197
  }
10847
11198
  function loadManifestFromDir(dir) {
10848
- const manifestPath = path11.join(dir, "manifest.json");
11199
+ const manifestPath = path13.join(dir, "manifest.json");
10849
11200
  if (!fs6.existsSync(manifestPath)) return null;
10850
11201
  try {
10851
11202
  const raw = fs6.readFileSync(manifestPath, "utf8");
@@ -10860,8 +11211,8 @@ function copyDirSync(src, dest) {
10860
11211
  ensureDir(dest);
10861
11212
  const entries = fs6.readdirSync(src, { withFileTypes: true });
10862
11213
  for (const entry of entries) {
10863
- const srcPath = path11.join(src, entry.name);
10864
- const destPath = path11.join(dest, entry.name);
11214
+ const srcPath = path13.join(src, entry.name);
11215
+ const destPath = path13.join(dest, entry.name);
10865
11216
  if (entry.isDirectory()) {
10866
11217
  copyDirSync(srcPath, destPath);
10867
11218
  } else {
@@ -10873,7 +11224,7 @@ function rmDirSync(dir) {
10873
11224
  if (!fs6.existsSync(dir)) return;
10874
11225
  const entries = fs6.readdirSync(dir, { withFileTypes: true });
10875
11226
  for (const entry of entries) {
10876
- const p = path11.join(dir, entry.name);
11227
+ const p = path13.join(dir, entry.name);
10877
11228
  if (entry.isDirectory()) {
10878
11229
  rmDirSync(p);
10879
11230
  } else {
@@ -10897,6 +11248,13 @@ var FsPluginStore = class {
10897
11248
  }
10898
11249
  return entries.filter((e) => e.isDirectory()).map((e) => e.name);
10899
11250
  }
11251
+ listDir(dir) {
11252
+ try {
11253
+ return fs6.readdirSync(dir);
11254
+ } catch {
11255
+ return [];
11256
+ }
11257
+ }
10900
11258
  loadManifest(pluginDir) {
10901
11259
  return loadManifestFromDir(pluginDir);
10902
11260
  }
@@ -10919,11 +11277,11 @@ var FsPluginStore = class {
10919
11277
  return null;
10920
11278
  }
10921
11279
  }
10922
- dirExists(path15) {
10923
- return fs6.existsSync(path15) && fs6.statSync(path15).isDirectory();
11280
+ dirExists(path17) {
11281
+ return fs6.existsSync(path17) && fs6.statSync(path17).isDirectory();
10924
11282
  }
10925
- exists(path15) {
10926
- return fs6.existsSync(path15);
11283
+ exists(path17) {
11284
+ return fs6.existsSync(path17);
10927
11285
  }
10928
11286
  };
10929
11287
 
@@ -11211,17 +11569,98 @@ var OPENCODE_MANIFEST = {
11211
11569
  ]
11212
11570
  };
11213
11571
 
11572
+ // src/plugins/pluginSource.ts
11573
+ var ALLOWED_PLUGIN_HOSTS = [
11574
+ "github.com",
11575
+ "gitlab.com",
11576
+ "bitbucket.org",
11577
+ "codeberg.org"
11578
+ ];
11579
+ var SAFE_REPO_PATH = /^\/[A-Za-z0-9._~-]+(?:\/[A-Za-z0-9._~-]+)*$/;
11580
+ var HAS_SCHEME = /^[A-Za-z][A-Za-z0-9+.-]+:/;
11581
+ var SCP_LIKE_REMOTE = /^[^/\\]+@[^/\\]+:/;
11582
+ function hasControlCharacter(value) {
11583
+ for (let i = 0; i < value.length; i++) {
11584
+ const code = value.charCodeAt(i);
11585
+ if (code < 32 || code === 127) return true;
11586
+ }
11587
+ return false;
11588
+ }
11589
+ function assertInstallablePluginUrl(url) {
11590
+ if (typeof url !== "string" || url.trim() === "") {
11591
+ throw new Error("A plugin repository URL is required.");
11592
+ }
11593
+ if (hasControlCharacter(url)) {
11594
+ throw new Error("Plugin URL contains control characters.");
11595
+ }
11596
+ let parsed;
11597
+ try {
11598
+ parsed = new URL(url.trim());
11599
+ } catch {
11600
+ throw new Error(`Not a valid plugin repository URL: ${url}`);
11601
+ }
11602
+ if (parsed.protocol !== "https:") {
11603
+ throw new Error(
11604
+ `Plugin repositories must be cloned over https (got "${parsed.protocol}//"). git transport helpers and non-https schemes are not accepted.`
11605
+ );
11606
+ }
11607
+ if (parsed.username || parsed.password) {
11608
+ throw new Error("Plugin URL must not embed credentials.");
11609
+ }
11610
+ if (parsed.port) {
11611
+ throw new Error("Plugin URL must not specify a port.");
11612
+ }
11613
+ if (parsed.search || parsed.hash) {
11614
+ throw new Error("Plugin URL must not carry a query string or fragment.");
11615
+ }
11616
+ const host = parsed.hostname.toLowerCase();
11617
+ if (!ALLOWED_PLUGIN_HOSTS.includes(host)) {
11618
+ throw new Error(
11619
+ `Plugin host not allowed: ${host}. Allowed hosts: ${ALLOWED_PLUGIN_HOSTS.join(", ")}.`
11620
+ );
11621
+ }
11622
+ if (!SAFE_REPO_PATH.test(parsed.pathname)) {
11623
+ throw new Error(`Not a plain repository path: ${parsed.pathname}`);
11624
+ }
11625
+ return parsed;
11626
+ }
11627
+ function classifyPluginSource(source) {
11628
+ const trimmed = source.trim();
11629
+ if (trimmed.startsWith("github:")) {
11630
+ const repo = trimmed.slice("github:".length).replace(/\.git$/, "");
11631
+ return { kind: "git", url: `https://github.com/${repo}.git` };
11632
+ }
11633
+ if (HAS_SCHEME.test(trimmed) || SCP_LIKE_REMOTE.test(trimmed)) {
11634
+ return { kind: "git", url: trimmed };
11635
+ }
11636
+ return { kind: "path", path: source };
11637
+ }
11638
+
11214
11639
  // src/plugins/RunnerRegistry.ts
11215
11640
  var BUILTIN_MANIFESTS = [
11216
11641
  CLAUDE_CODE_MANIFEST,
11217
11642
  CODEX_MANIFEST,
11218
11643
  OPENCODE_MANIFEST
11219
11644
  ];
11645
+ var RESERVED_RUNNER_NAMES = new Set(BUILTIN_MANIFESTS.map((m) => m.name.toLowerCase()));
11646
+ function isReservedRunnerName(name) {
11647
+ return RESERVED_RUNNER_NAMES.has(name.toLowerCase());
11648
+ }
11649
+ function gitClone(url, destDir) {
11650
+ execFileSync("git", ["clone", "--depth", "1", "--", url, destDir], {
11651
+ stdio: "pipe",
11652
+ timeout: 3e4,
11653
+ // A repository that demands credentials should fail, not hang on a prompt.
11654
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
11655
+ });
11656
+ }
11220
11657
  var RunnerRegistry = class {
11221
11658
  plugins = /* @__PURE__ */ new Map();
11222
11659
  store;
11223
- constructor(store) {
11660
+ clone;
11661
+ constructor(store, clone) {
11224
11662
  this.store = store ?? new FsPluginStore();
11663
+ this.clone = clone ?? gitClone;
11225
11664
  this.loadBuiltins();
11226
11665
  }
11227
11666
  loadBuiltins() {
@@ -11232,9 +11671,15 @@ var RunnerRegistry = class {
11232
11671
  loadUserPlugins() {
11233
11672
  const names = this.store.listUserPluginDirs();
11234
11673
  for (const name of names) {
11235
- const pluginDir = path12.join(this.store.getUserPluginsDir(), name);
11674
+ let pluginDir;
11675
+ try {
11676
+ pluginDir = resolvePluginInstallDir(this.store.getUserPluginsDir(), name);
11677
+ } catch {
11678
+ continue;
11679
+ }
11236
11680
  const manifest = this.store.loadManifest(pluginDir);
11237
- if (!manifest) continue;
11681
+ if (!manifest || !isPlainPluginName(manifest.name)) continue;
11682
+ if (isReservedRunnerName(manifest.name)) continue;
11238
11683
  this.plugins.set(manifest.name, {
11239
11684
  manifest,
11240
11685
  source: "user",
@@ -11263,7 +11708,7 @@ var RunnerRegistry = class {
11263
11708
  return entry?.source === "builtin";
11264
11709
  }
11265
11710
  installFromPath(sourcePath) {
11266
- const absPath = path12.resolve(sourcePath);
11711
+ const absPath = path14.resolve(sourcePath);
11267
11712
  if (!this.store.dirExists(absPath)) {
11268
11713
  throw new Error(`Plugin source must be a directory: ${absPath}`);
11269
11714
  }
@@ -11271,48 +11716,50 @@ var RunnerRegistry = class {
11271
11716
  if (!manifest) {
11272
11717
  throw new Error(`No valid manifest.json found in: ${absPath}`);
11273
11718
  }
11274
- const destDir = path12.join(this.store.getUserPluginsDir(), manifest.name);
11275
- this.store.ensureDir(destDir);
11276
- this.store.copyDir(absPath, destDir);
11277
- this.plugins.set(manifest.name, {
11278
- manifest,
11279
- source: "user",
11280
- installPath: destDir
11281
- });
11282
- return manifest;
11719
+ return this.registerInstalled(manifest, absPath);
11283
11720
  }
11284
11721
  installFromGit(url) {
11285
- const repoName = url.split("/").pop()?.replace(/\.git$/, "") || "plugin";
11286
- const tmpDir = path12.join(os5.tmpdir(), `ordewell-plugin-${Date.now()}`);
11722
+ const validated = assertInstallablePluginUrl(url);
11723
+ const tmpDir = path14.join(os5.tmpdir(), `ordewell-plugin-${randomBytes3(12).toString("hex")}`);
11287
11724
  this.store.ensureDir(tmpDir);
11288
11725
  try {
11289
- execSync3(`git clone --depth 1 "${url}" "${tmpDir}"`, { stdio: "pipe", timeout: 3e4 });
11290
- } catch (err) {
11291
- const message = err instanceof Error ? err.message : String(err);
11292
- throw new Error(`Failed to clone repository: ${message}`);
11293
- }
11294
- const contents = fsReaddirSync(tmpDir);
11295
- const hasManifest = contents.some((f) => f === "manifest.json");
11296
- if (!hasManifest && contents.length === 1 && this.store.dirExists(path12.join(tmpDir, contents[0]))) {
11297
- const subDir = path12.join(tmpDir, contents[0]);
11298
- return this.finishInstall(subDir, repoName, tmpDir);
11299
- }
11300
- if (!hasManifest) {
11726
+ try {
11727
+ this.clone(validated.toString(), tmpDir);
11728
+ } catch (err) {
11729
+ const message = err instanceof Error ? err.message : String(err);
11730
+ throw new Error(`Failed to clone repository: ${message}`);
11731
+ }
11732
+ const contents = this.store.listDir(tmpDir);
11733
+ const hasManifest = contents.some((f) => f === "manifest.json");
11734
+ let sourceDir = tmpDir;
11735
+ if (!hasManifest) {
11736
+ const onlyEntry = contents.length === 1 ? path14.join(tmpDir, contents[0]) : void 0;
11737
+ if (!onlyEntry || !this.store.dirExists(onlyEntry)) {
11738
+ throw new Error(`No manifest.json found in repository: ${validated.toString()}`);
11739
+ }
11740
+ sourceDir = onlyEntry;
11741
+ }
11742
+ const manifest = this.store.loadManifest(sourceDir);
11743
+ if (!manifest) {
11744
+ throw new Error("No valid manifest.json found in cloned repository");
11745
+ }
11746
+ return this.registerInstalled(manifest, sourceDir);
11747
+ } finally {
11301
11748
  this.store.removeDir(tmpDir);
11302
- throw new Error(`No manifest.json found in repository: ${url}`);
11303
11749
  }
11304
- return this.finishInstall(tmpDir, repoName, tmpDir);
11305
11750
  }
11306
- finishInstall(sourceDir, repoName, tmpDir) {
11307
- const manifest = this.store.loadManifest(sourceDir);
11308
- if (!manifest) {
11309
- this.store.removeDir(tmpDir);
11310
- throw new Error(`No valid manifest.json found in cloned repository`);
11751
+ /**
11752
+ * The single destination-building step both install routes share: the name is
11753
+ * constrained to a plain segment and the resolved destination is asserted to
11754
+ * be inside the plugins directory before anything is copied.
11755
+ */
11756
+ registerInstalled(manifest, sourceDir) {
11757
+ if (isReservedRunnerName(manifest.name)) {
11758
+ throw new Error(`Cannot install a plugin named after a built-in runner: ${manifest.name}`);
11311
11759
  }
11312
- const destDir = path12.join(this.store.getUserPluginsDir(), manifest.name);
11760
+ const destDir = resolvePluginInstallDir(this.store.getUserPluginsDir(), manifest.name);
11313
11761
  this.store.ensureDir(destDir);
11314
11762
  this.store.copyDir(sourceDir, destDir);
11315
- this.store.removeDir(tmpDir);
11316
11763
  this.plugins.set(manifest.name, {
11317
11764
  manifest,
11318
11765
  source: "user",
@@ -11333,7 +11780,8 @@ var RunnerRegistry = class {
11333
11780
  this.plugins.delete(name);
11334
11781
  }
11335
11782
  createSkeleton(name, outputDir) {
11336
- const dir = path12.resolve(outputDir, name);
11783
+ assertPlainPluginName(name);
11784
+ const dir = path14.resolve(outputDir, name);
11337
11785
  this.store.ensureDir(dir);
11338
11786
  const manifest = {
11339
11787
  name,
@@ -11365,8 +11813,8 @@ var RunnerRegistry = class {
11365
11813
  ]
11366
11814
  }
11367
11815
  };
11368
- this.store.writeFile(path12.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
11369
- const runSh = path12.join(dir, "run.sh");
11816
+ this.store.writeFile(path14.join(dir, "manifest.json"), JSON.stringify(manifest, null, 2) + "\n");
11817
+ const runSh = path14.join(dir, "run.sh");
11370
11818
  const scriptContent = `#!/usr/bin/env bash
11371
11819
  # Custom runner script for ${name}
11372
11820
  # Receives:
@@ -11390,13 +11838,6 @@ echo "Running ${name} with prompt: $prompt"
11390
11838
  return dir;
11391
11839
  }
11392
11840
  };
11393
- function fsReaddirSync(dir) {
11394
- try {
11395
- return fs7.readdirSync(dir);
11396
- } catch {
11397
- return [];
11398
- }
11399
- }
11400
11841
  function fsChmodSync(fpath, mode) {
11401
11842
  try {
11402
11843
  fs7.chmodSync(fpath, mode);
@@ -11406,14 +11847,14 @@ function fsChmodSync(fpath, mode) {
11406
11847
 
11407
11848
  // src/utils/stateStore.ts
11408
11849
  import * as fs8 from "fs";
11409
- import * as path13 from "path";
11850
+ import * as path15 from "path";
11410
11851
  var STATE_FILE = "state.json";
11411
11852
  function getStatePath(baseDir) {
11412
- return path13.join(getStateDir(baseDir), STATE_FILE);
11853
+ return path15.join(getStateDir(baseDir), STATE_FILE);
11413
11854
  }
11414
11855
  function saveState(plan, baseDir) {
11415
11856
  const statePath = getStatePath(baseDir ?? process.cwd());
11416
- ensureDir(path13.dirname(statePath));
11857
+ ensureDir(path15.dirname(statePath));
11417
11858
  plan.lastUpdated = (/* @__PURE__ */ new Date()).toISOString();
11418
11859
  fs8.writeFileSync(statePath, JSON.stringify(plan, null, 2));
11419
11860
  }
@@ -11446,7 +11887,7 @@ function stateExists(baseDir) {
11446
11887
 
11447
11888
  // src/services/SettingsService.ts
11448
11889
  import * as fs9 from "fs";
11449
- import * as path14 from "path";
11890
+ import * as path16 from "path";
11450
11891
  import * as os6 from "os";
11451
11892
  var DEFAULTS = {
11452
11893
  grillMe: { enabled: false },
@@ -11459,7 +11900,7 @@ var DEFAULTS = {
11459
11900
  function getSettingsPath() {
11460
11901
  const override = process.env.ORDEWELL_SETTINGS_PATH;
11461
11902
  if (override && override.trim()) return override.trim();
11462
- return path14.join(os6.homedir(), ".config", "ordewell", "settings.json");
11903
+ return path16.join(os6.homedir(), ".config", "ordewell", "settings.json");
11463
11904
  }
11464
11905
  function readPlannerModels(raw) {
11465
11906
  if (!raw || typeof raw !== "object" || Array.isArray(raw)) return void 0;
@@ -11624,7 +12065,7 @@ var SettingsService = class {
11624
12065
  return { ...DEFAULTS, grillMe: { ...DEFAULTS.grillMe }, tdd: { ...DEFAULTS.tdd }, prd: { ...DEFAULTS.prd }, review: { ...DEFAULTS.review }, verification: { ...DEFAULTS.verification }, researchSubagents: { ...DEFAULTS.researchSubagents } };
11625
12066
  }
11626
12067
  persist() {
11627
- const dir = path14.dirname(this.filePath);
12068
+ const dir = path16.dirname(this.filePath);
11628
12069
  if (!fs9.existsSync(dir)) {
11629
12070
  fs9.mkdirSync(dir, { recursive: true });
11630
12071
  }
@@ -11668,6 +12109,7 @@ var PlannerModelMemory = class {
11668
12109
  }
11669
12110
  };
11670
12111
  export {
12112
+ ALLOWED_PLUGIN_HOSTS,
11671
12113
  ALL_PROVIDERS,
11672
12114
  AUTO_COMMANDS,
11673
12115
  AbstractRunner,
@@ -11686,6 +12128,8 @@ export {
11686
12128
  CommandLineTooLongError,
11687
12129
  ConsoleLogger,
11688
12130
  ContextCollector,
12131
+ DAEMON_SUBPROTOCOL,
12132
+ DAEMON_TOKEN_SUBPROTOCOL_PREFIX,
11689
12133
  DENY_ALL,
11690
12134
  EmbeddedNewlineError,
11691
12135
  EnvConfig,
@@ -11702,9 +12146,12 @@ export {
11702
12146
  ModelResolver,
11703
12147
  OPENCODE_MANIFEST,
11704
12148
  ORCHESTRATOR_SHORTCUTS,
12149
+ ORDEWELL_SETTABLE_ENV,
11705
12150
  OpenAiService,
11706
12151
  OpenCodeAdapter,
11707
12152
  PLAN_ENVELOPE_KEY,
12153
+ PLUGIN_NAME_PATTERN,
12154
+ PROVIDER_CREDENTIAL_ENV,
11708
12155
  PROVIDER_DETECT_PRIORITY,
11709
12156
  PROVIDER_LABEL,
11710
12157
  PROVIDER_PRIORITY,
@@ -11719,6 +12166,8 @@ export {
11719
12166
  RunnerInstallation,
11720
12167
  RunnerRegistry,
11721
12168
  SEARCH_EXCLUSIONS,
12169
+ SETTINGS_ENV_ALLOWLIST,
12170
+ SETTINGS_ENV_REFUSED,
11722
12171
  STATE_DIR,
11723
12172
  STOPPED_TOOL_RESULT,
11724
12173
  SYMBOL_LANGUAGES,
@@ -11731,13 +12180,19 @@ export {
11731
12180
  TmuxRunner,
11732
12181
  VerdictEngine,
11733
12182
  WINDOWS_MAX_COMMAND_LINE,
12183
+ WorkspaceNotAProjectError,
11734
12184
  WorkspaceNotFoundError,
11735
12185
  addTaskToPlan,
12186
+ admitSettingsEnv,
11736
12187
  applyHeadLimit,
11737
12188
  applyTaskOps,
12189
+ assertInstallablePluginUrl,
12190
+ assertPlainPluginName,
11738
12191
  assertWorkspaceExists,
12192
+ assertWorkspaceIsProject,
11739
12193
  augmentPromptWithPriorOutputs,
11740
12194
  augmentedPath,
12195
+ bearerHeaderValue,
11741
12196
  buildConversationSystemPrompt,
11742
12197
  buildFallbackGrepArgs,
11743
12198
  buildGlobArgs,
@@ -11765,7 +12220,9 @@ export {
11765
12220
  classifyCommand,
11766
12221
  classifyOutcome,
11767
12222
  classifyPlannerReply,
12223
+ classifyPluginSource,
11768
12224
  clearAugmentedPathCache,
12225
+ clearDaemonToken,
11769
12226
  clearResearchShellCache,
11770
12227
  clearState,
11771
12228
  clipboardCopyCommand,
@@ -11777,6 +12234,7 @@ export {
11777
12234
  createAiService,
11778
12235
  createEmptyPlan,
11779
12236
  createTask,
12237
+ daemonTokenPath,
11780
12238
  defaultLogger,
11781
12239
  definitionPattern,
11782
12240
  deleteSession,
@@ -11794,6 +12252,7 @@ export {
11794
12252
  extractObjectWithBalance,
11795
12253
  extractObjectsWithKey,
11796
12254
  extractPrdBlock,
12255
+ extractPresentedToken,
11797
12256
  fetchAllProviderModels,
11798
12257
  filterFallbackByAnchoredInclude,
11799
12258
  filterModelsForPrompt,
@@ -11811,6 +12270,9 @@ export {
11811
12270
  isCliProvider,
11812
12271
  isExecutableResolved,
11813
12272
  isOpenAiProvider,
12273
+ isPlainPluginName,
12274
+ isReservedRunnerName,
12275
+ isValidManifest,
11814
12276
  killTree,
11815
12277
  knownModelId,
11816
12278
  languageForId,
@@ -11823,6 +12285,8 @@ export {
11823
12285
  migrateLegacyPlan,
11824
12286
  migratePlanState,
11825
12287
  migrateTask,
12288
+ mintDaemonToken,
12289
+ mintSessionId,
11826
12290
  modelContextBlock,
11827
12291
  modifyValidationFeedback,
11828
12292
  normalizeAgentArgs,
@@ -11838,6 +12302,7 @@ export {
11838
12302
  providerForRunner,
11839
12303
  reEmitPlanPrompt,
11840
12304
  reEmitTaskOpsPrompt,
12305
+ readDaemonToken,
11841
12306
  referencePattern,
11842
12307
  removeTaskFromPlan,
11843
12308
  renderPlanMap,
@@ -11849,6 +12314,7 @@ export {
11849
12314
  resolveArgs,
11850
12315
  resolveDefaultMode,
11851
12316
  resolveModelShortcut,
12317
+ resolvePluginInstallDir,
11852
12318
  resolveProvider,
11853
12319
  resolveProviderFromPrefix,
11854
12320
  resolveResearchShell,
@@ -11879,6 +12345,8 @@ export {
11879
12345
  tmuxSocketName,
11880
12346
  tmuxWindowName,
11881
12347
  toOrchestratorOptions,
12348
+ tokenSubprotocols,
12349
+ tokensMatch,
11882
12350
  truncatedPlanReEmitPrompt,
11883
12351
  updateTaskInPlan,
11884
12352
  validateModifiedPlan,