@corenel/sidecar 0.6.1 → 0.7.0

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.
Files changed (2) hide show
  1. package/dist/cli.js +446 -48
  2. package/package.json +4 -4
package/dist/cli.js CHANGED
@@ -14040,7 +14040,7 @@ var require_express_app = __commonJS({
14040
14040
  this.name += this.ext = (this.defaultEngine[0] !== "." ? "." : "") + this.defaultEngine;
14041
14041
  }
14042
14042
  }
14043
- NunjucksView.prototype.render = function render(opts, cb) {
14043
+ NunjucksView.prototype.render = function render2(opts, cb) {
14044
14044
  env.render(this.name, opts, cb);
14045
14045
  };
14046
14046
  app.set("view", NunjucksView);
@@ -14313,7 +14313,7 @@ var require_environment = __commonJS({
14313
14313
  _proto.express = function express(app) {
14314
14314
  return expressApp(this, app);
14315
14315
  };
14316
- _proto.render = function render(name, ctx, cb) {
14316
+ _proto.render = function render2(name, ctx, cb) {
14317
14317
  if (lib.isFunction(ctx)) {
14318
14318
  cb = ctx;
14319
14319
  ctx = null;
@@ -14441,7 +14441,7 @@ var require_environment = __commonJS({
14441
14441
  this.compiled = false;
14442
14442
  }
14443
14443
  };
14444
- _proto3.render = function render(ctx, parentFrame, cb) {
14444
+ _proto3.render = function render2(ctx, parentFrame, cb) {
14445
14445
  var _this6 = this;
14446
14446
  if (typeof ctx === "function") {
14447
14447
  cb = ctx;
@@ -15021,7 +15021,7 @@ var require_nunjucks = __commonJS({
15021
15021
  }
15022
15022
  return new Template(src2, env, path, eagerCompile);
15023
15023
  },
15024
- render: function render(name, ctx, cb) {
15024
+ render: function render2(name, ctx, cb) {
15025
15025
  if (!e2) {
15026
15026
  configure();
15027
15027
  }
@@ -22488,6 +22488,10 @@ var SidecarServer = class {
22488
22488
  * is the session layer's call, because the operator's permission lives
22489
22489
  * there; this class would have to duplicate that rule to decide, and a
22490
22490
  * duplicated permission rule is one that eventually disagrees. */
22491
+ // NOT from the wire: `allowMutations` is the daemon's own decision,
22492
+ // derived from an agent's `session:` block. A client that could set it
22493
+ // would grant itself mutating tools inside a sandbox. Deliberately
22494
+ // absent -- see session/guestRunner.ts.
22491
22495
  this.opts.sessions.run(m2.sessionId, agent, input, onEvent2, identity, { keepTranscript: m2.keepTranscript === true })
22492
22496
  ) : (agent, input, onEvent2, identity) => this.opts.runner.run(agent, input, onEvent2, identity);
22493
22497
  let runId;
@@ -23279,6 +23283,11 @@ function helpText() {
23279
23283
  " roots list|add|remove [path]",
23280
23284
  " edit the working directories in daemon.json (add stores the",
23281
23285
  " resolved, absolute path)",
23286
+ " add-under list|add|remove [path]",
23287
+ " edit the areas a client may add a root under, same way",
23288
+ " config list|get|set|unset|add|remove [key] [value]",
23289
+ " read or change any daemon.json setting, including turning",
23290
+ " a capability back off (config set allow-shell false)",
23282
23291
  " doctor report what this machine would serve, and what is wrong",
23283
23292
  " (exits non-zero on a definite fault -- safe to script)",
23284
23293
  " login|logout|auth manage the signed-in account",
@@ -28247,6 +28256,9 @@ async function readTranscript(files, crewRoot2, agent, runId) {
28247
28256
  }
28248
28257
 
28249
28258
  // src/session/manager.ts
28259
+ function isErrorStopReason(stopReason) {
28260
+ return stopReason === "error" || (stopReason ?? "").startsWith("error:");
28261
+ }
28250
28262
  var SessionManager = class {
28251
28263
  constructor(deps) {
28252
28264
  this.deps = deps;
@@ -28332,11 +28344,11 @@ var SessionManager = class {
28332
28344
  }
28333
28345
  this.deps.log?.(`session ${sessionId}: running agent "${agent}" inside the session`);
28334
28346
  const kept2 = this.transcriptOf(onEvent, opts?.keepTranscript === true);
28335
- return this.recorded(session, agent, identity, () => this.deps.guestRunnerFor(sessionId).run(agent, input, kept2.tee, identity), kept2.buffer);
28347
+ return this.recorded(session, agent, identity, () => this.deps.guestRunnerFor(sessionId, { allowMutations: opts?.allowMutations === true }).run(agent, input, kept2.tee, identity), kept2.buffer, opts?.source);
28336
28348
  }
28337
28349
  this.deps.log?.(`session ${sessionId}: running agent "${agent}" on the host (isolation: none)`);
28338
28350
  const kept = this.transcriptOf(onEvent, opts?.keepTranscript === true);
28339
- return this.recorded(session, agent, identity, () => this.deps.runner.runFrom ? this.deps.runner.runFrom(session.workdir, agent, input, kept.tee, identity, session.stateDir) : this.deps.runner.run(agent, input, kept.tee, identity), kept.buffer);
28351
+ return this.recorded(session, agent, identity, () => this.deps.runner.runFrom ? this.deps.runner.runFrom(session.workdir, agent, input, kept.tee, identity, session.stateDir) : this.deps.runner.run(agent, input, kept.tee, identity, void 0, opts?.source), kept.buffer, opts?.source);
28340
28352
  }
28341
28353
  /**
28342
28354
  * Run, then write one line about it — whatever happened.
@@ -28374,7 +28386,7 @@ var SessionManager = class {
28374
28386
  }
28375
28387
  };
28376
28388
  }
28377
- async recorded(session, agent, identity, go, transcript) {
28389
+ async recorded(session, agent, identity, go, transcript, source = "chat") {
28378
28390
  const startedAt = Date.now();
28379
28391
  const keep = (runId) => {
28380
28392
  if (!this.deps.recordTranscript || !transcript || transcript.length === 0) return;
@@ -28392,7 +28404,7 @@ var SessionManager = class {
28392
28404
  sessionId: session.id,
28393
28405
  isolated: this.isolated,
28394
28406
  kind: this.deps.backend.kind,
28395
- source: "chat",
28407
+ source,
28396
28408
  startedAt,
28397
28409
  endedAt: Date.now(),
28398
28410
  ok,
@@ -28404,7 +28416,7 @@ var SessionManager = class {
28404
28416
  void r2.done.then(
28405
28417
  (d2) => {
28406
28418
  keep(r2.runId);
28407
- write3(r2.runId, d2.stopReason !== "error", d2.stopReason);
28419
+ write3(r2.runId, !isErrorStopReason(d2.stopReason), d2.stopReason);
28408
28420
  },
28409
28421
  (e2) => {
28410
28422
  keep(r2.runId);
@@ -28726,6 +28738,7 @@ var LABEL_CREATED = "corenel.created";
28726
28738
  var LABEL_REPO_URL = "corenel.repo.url";
28727
28739
  var LABEL_REPO_REF = "corenel.repo.ref";
28728
28740
  var LABEL_REPO_COMMIT = "corenel.repo.commit";
28741
+ var LABEL_CREW_SESSION = "corenel.crew.session";
28729
28742
  function idFromSandboxName(name) {
28730
28743
  const m2 = /^corenel-(sess_\d+)$/.exec(name);
28731
28744
  return m2 ? m2[1] : null;
@@ -28845,7 +28858,7 @@ function makeMicrovmBackend(deps) {
28845
28858
  if (!live.length) return;
28846
28859
  const r2 = await deps.run(listArgs());
28847
28860
  if (r2.code !== 0) return;
28848
- const present2 = new Set(parseSandboxList(r2.stdout).map((row) => row.name));
28861
+ const present2 = new Set(parseSandboxList(r2.stdout).map((row2) => row2.name));
28849
28862
  for (const s2 of live) {
28850
28863
  if (present2.has(sandboxName(s2.id))) continue;
28851
28864
  if (at2 - s2.createdAt < RECONCILE_MS) continue;
@@ -28891,7 +28904,8 @@ function makeMicrovmBackend(deps) {
28891
28904
  [LABEL_REPO_URL]: spec.source.url,
28892
28905
  [LABEL_REPO_COMMIT]: spec.source.commit,
28893
28906
  ...spec.source.ref ? { [LABEL_REPO_REF]: spec.source.ref } : {}
28894
- } : {}
28907
+ } : {},
28908
+ ...spec.sessionName ? { [LABEL_CREW_SESSION]: spec.sessionName } : {}
28895
28909
  }
28896
28910
  }), `create session ${id}`);
28897
28911
  const handle = {
@@ -28903,7 +28917,8 @@ function makeMicrovmBackend(deps) {
28903
28917
  // Kept so a run in this session looks for the agent where the creator's
28904
28918
  // workspace actually keeps it.
28905
28919
  ...spec.stateDir ? { stateDir: spec.stateDir } : {},
28906
- ...spec.source ? { source: spec.source } : {}
28920
+ ...spec.source ? { source: spec.source } : {},
28921
+ ...spec.sessionName ? { sessionName: spec.sessionName } : {}
28907
28922
  };
28908
28923
  sessions.set(id, handle);
28909
28924
  deps.log?.(
@@ -29006,9 +29021,28 @@ function makeMicrovmBackend(deps) {
29006
29021
  *
29007
29022
  * CAVEAT, deliberate: the log is the SANDBOX's, not this exec's. Entries
29008
29023
  * carry an `id` per invocation, but it is not known before the exec starts,
29009
- * so concurrent execs in one sandbox would interleave. That is acceptable
29010
- * because SessionManager admits one run per session at a time — and it is
29011
- * written down because it stops being acceptable the moment that changes.
29024
+ * so concurrent execs in one sandbox interleave.
29025
+ *
29026
+ * NOTHING PREVENTS THAT, and no earlier claim here should be believed.
29027
+ * This comment once said SessionManager admits one run per session at a
29028
+ * time; a correction then said a scheduled agent's runs "already chain on
29029
+ * that queue". BOTH ARE FALSE. `manager.ts` checks existence and status
29030
+ * only; `SessionStatus` is 'created' | 'paused' | 'destroyed' (types.ts)
29031
+ * with no 'running'; and `RunQueue` (runnerService.ts) wraps the HOST
29032
+ * executor, which the bound path never reaches -- `dispatcher.startInSession`
29033
+ * goes straight to `sessions.run` -> `crewSessions.ts` -> `manager.run`.
29034
+ * Nothing anywhere serializes two runs into one session.
29035
+ *
29036
+ * What NARROWS the window is weaker than a lock and worth stating exactly:
29037
+ * a bound agent gets its own box (spec D3), and the dispatcher dedupes on
29038
+ * an in-flight key of `owner:agent:trigger`.
29039
+ *
29040
+ * What is STILL OPEN, inside the scheduled path: an agent carrying both a
29041
+ * `schedule` and a `continuous` trigger holds two DISTINCT in-flight keys,
29042
+ * so both fire concurrently by design (dispatcher.ts says so in as many
29043
+ * words), and `resolveCrewSession` keys reuse on (name, workdir) only --
29044
+ * so both land in the same sandbox and interleave in one log. The browser
29045
+ * can likewise send two runs into one session.
29012
29046
  */
29013
29047
  ...deps.stream ? {
29014
29048
  execStream(id, command, onLine, opts) {
@@ -29130,21 +29164,21 @@ function makeMicrovmBackend(deps) {
29130
29164
  deps.log?.(`sessions: could not list existing sandboxes (msb exit ${r2.code}) \u2014 nothing adopted`);
29131
29165
  return { adopted, unmanaged };
29132
29166
  }
29133
- for (const row of parseSandboxList(r2.stdout)) {
29134
- const id = idFromSandboxName(row.name);
29167
+ for (const row2 of parseSandboxList(r2.stdout)) {
29168
+ const id = idFromSandboxName(row2.name);
29135
29169
  if (!id) continue;
29136
29170
  seq2 = Math.max(seq2, seqOf(id));
29137
29171
  if (sessions.has(id)) continue;
29138
- const inspected = await deps.run(inspectArgs(row.name)).catch(() => null);
29172
+ const inspected = await deps.run(inspectArgs(row2.name)).catch(() => null);
29139
29173
  const labels = inspected && inspected.code === 0 ? parseSandboxLabels(inspected.stdout) : {};
29140
29174
  const workdir = labels[LABEL_WORKDIR];
29141
29175
  if (!workdir) {
29142
- unmanaged.push(row.name);
29176
+ unmanaged.push(row2.name);
29143
29177
  continue;
29144
29178
  }
29145
29179
  if (!within(workdir)) {
29146
- deps.log?.(`sessions: sandbox "${row.name}" holds ${workdir}, outside this daemon's roots \u2014 not adopted`);
29147
- unmanaged.push(row.name);
29180
+ deps.log?.(`sessions: sandbox "${row2.name}" holds ${workdir}, outside this daemon's roots \u2014 not adopted`);
29181
+ unmanaged.push(row2.name);
29148
29182
  continue;
29149
29183
  }
29150
29184
  const stamped = Number(labels[LABEL_CREATED]);
@@ -29152,13 +29186,15 @@ function makeMicrovmBackend(deps) {
29152
29186
  const repoUrl = labels[LABEL_REPO_URL];
29153
29187
  const repoCommit = labels[LABEL_REPO_COMMIT];
29154
29188
  const repoRef = labels[LABEL_REPO_REF];
29189
+ const sessionName = labels[LABEL_CREW_SESSION];
29155
29190
  sessions.set(id, {
29156
29191
  id,
29157
- status: statusFromSandbox(row.status),
29192
+ status: statusFromSandbox(row2.status),
29158
29193
  workdir,
29159
- createdAt: Number.isFinite(stamped) && stamped > 0 ? stamped : row.createdAt ?? now2(),
29194
+ createdAt: Number.isFinite(stamped) && stamped > 0 ? stamped : row2.createdAt ?? now2(),
29160
29195
  ...owner ? { owner } : {},
29161
29196
  ...repoUrl && repoCommit ? { source: { url: repoUrl, commit: repoCommit, ...repoRef ? { ref: repoRef } : {} } } : {},
29197
+ ...sessionName ? { sessionName } : {},
29162
29198
  /* THE ONLY PLACE THIS IS EVER SET TRUE. `create()` never touches it,
29163
29199
  * so a card can tell "this daemon booted it" from "this daemon
29164
29200
  * found it already running" — procs.ts's own words: a running
@@ -29258,10 +29294,11 @@ function makeMicrovmBackend(deps) {
29258
29294
  }
29259
29295
 
29260
29296
  // src/session/guestRunner.ts
29261
- var DEFAULT_GUEST_COMMAND = (agent, input) => [
29297
+ var DEFAULT_GUEST_COMMAND = (agent, input, opts) => [
29262
29298
  "corenel",
29263
29299
  "run-agent",
29264
29300
  agent,
29301
+ ...opts.allowMutations ? ["--allow-mutations"] : [],
29265
29302
  "--input",
29266
29303
  input
29267
29304
  ];
@@ -29306,7 +29343,7 @@ function makeGuestRunner(deps) {
29306
29343
  const exec = deps.backend.exec.bind(deps.backend);
29307
29344
  const runId = `grun_${++seq3}`;
29308
29345
  deps.log?.(`session ${sessionId}: running "${agent}" inside the guest (${runId})`);
29309
- const command = build(agent, input);
29346
+ const command = build(agent, input, { allowMutations: deps.allowMutations === true });
29310
29347
  const timeout = deps.timeoutMs === void 0 ? void 0 : { timeoutMs: deps.timeoutMs };
29311
29348
  const seen = [];
29312
29349
  let skipped = 0;
@@ -29514,9 +29551,10 @@ async function wireSessions(opts) {
29514
29551
  ...opts.recordTranscript ? { recordTranscript: opts.recordTranscript } : {},
29515
29552
  // One runner per session, built on demand: the manager knows which session a
29516
29553
  // run belongs to, so nothing has to infer it.
29517
- guestRunnerFor: (sessionId) => makeGuestRunner({
29554
+ guestRunnerFor: (sessionId, guestOpts) => makeGuestRunner({
29518
29555
  backend,
29519
29556
  sessionId: () => sessionId,
29557
+ allowMutations: guestOpts.allowMutations,
29520
29558
  ...opts.log ? { log: opts.log } : {}
29521
29559
  }),
29522
29560
  ...opts.log ? { log: opts.log } : {}
@@ -29528,6 +29566,26 @@ async function wireSessions(opts) {
29528
29566
  return { manager, backend, port: makeSessionsPort({ manager, backend, guestEnv, ...portExtras(opts) }) };
29529
29567
  }
29530
29568
 
29569
+ // src/runner/crewSessions.ts
29570
+ function makeCrewSessions(src2, opts) {
29571
+ if (!src2?.manager.isolated) return void 0;
29572
+ const { manager, backend } = src2;
29573
+ return {
29574
+ list: () => backend.list(),
29575
+ adopt: () => backend.adopt?.() ?? Promise.resolve(void 0),
29576
+ create: async (spec) => {
29577
+ const env = await opts.guestEnv().catch(() => ({}));
29578
+ return manager.create({
29579
+ workdir: spec.workdir,
29580
+ sessionName: spec.sessionName,
29581
+ stateDir: opts.stateDir,
29582
+ env: { ...env, CORENEL_STATE_DIR: opts.stateDir }
29583
+ });
29584
+ },
29585
+ run: (sessionId, agent, input, onEvent, identity, runOpts) => manager.run(sessionId, agent, input, onEvent, identity, runOpts)
29586
+ };
29587
+ }
29588
+
29531
29589
  // src/runner/owners.ts
29532
29590
  import { readdir } from "node:fs/promises";
29533
29591
  import { join as join8 } from "node:path";
@@ -30215,6 +30273,41 @@ var CREW_EXCLUDED_TOOLS = /* @__PURE__ */ new Set(["ask_user", "spawn_agent", "s
30215
30273
 
30216
30274
  // ../crew/store.ts
30217
30275
  var import_yaml3 = __toESM(require_dist(), 1);
30276
+
30277
+ // ../crew/sessionBinding.ts
30278
+ var SESSION_NAME_RE = /^[a-z0-9_-]{1,32}$/;
30279
+ function isValidSessionName(name) {
30280
+ return SESSION_NAME_RE.test(name);
30281
+ }
30282
+ function parseSessionBinding(meta) {
30283
+ const raw = meta.session;
30284
+ if (raw === void 0) return { kind: "absent" };
30285
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
30286
+ return { kind: "malformed", reason: "session: must be a block with `name:` and `workdir:`" };
30287
+ }
30288
+ const o2 = raw;
30289
+ const name = typeof o2.name === "string" ? o2.name : "";
30290
+ const workdir = typeof o2.workdir === "string" ? o2.workdir.trim() : "";
30291
+ if (!isValidSessionName(name)) {
30292
+ return {
30293
+ kind: "malformed",
30294
+ reason: `session.name ${JSON.stringify(name)} must be 1-32 characters of a-z, 0-9, "-" or "_"`
30295
+ };
30296
+ }
30297
+ if (!workdir) return { kind: "malformed", reason: "session.workdir is required" };
30298
+ return {
30299
+ kind: "binding",
30300
+ binding: {
30301
+ name,
30302
+ workdir,
30303
+ // Anything but an explicit `false` is true, matching how `enabled` is read
30304
+ // in store.ts — absent means the default, and the default is on.
30305
+ createIfMissing: o2.createIfMissing !== false
30306
+ }
30307
+ };
30308
+ }
30309
+
30310
+ // ../crew/store.ts
30218
30311
  var TRIGGER_TYPES = [
30219
30312
  "manual",
30220
30313
  "file-change",
@@ -30300,6 +30393,7 @@ async function loadCrewAgentAt(files, root, name, scope = "workspace") {
30300
30393
  if (!isValidAgentName(name)) throw new Error(`invalid crew agent name: ${name}`);
30301
30394
  const dir = `${root}/${name}`;
30302
30395
  const { meta, body: body2 } = parseFrontmatter(await readOr(files, `${dir}/AGENT.md`));
30396
+ const session = parseSessionBinding(meta);
30303
30397
  return {
30304
30398
  name,
30305
30399
  jobTitle: typeof meta.jobTitle === "string" ? meta.jobTitle : "",
@@ -30314,7 +30408,9 @@ async function loadCrewAgentAt(files, root, name, scope = "workspace") {
30314
30408
  policy: parsePolicyYaml(await readOr(files, `${dir}/POLICY.yaml`), name),
30315
30409
  settings: parseSettings(await readOr(files, `${dir}/settings.json`, "{}")),
30316
30410
  budget: parseBudget(meta),
30317
- canCall: parseCanCall(meta)
30411
+ canCall: parseCanCall(meta),
30412
+ ...session.kind === "binding" ? { session: session.binding } : {},
30413
+ ...session.kind === "malformed" ? { sessionError: session.reason } : {}
30318
30414
  };
30319
30415
  }
30320
30416
  async function loadCrewAgentAtIfExists(files, root, name, scope = "workspace") {
@@ -42613,6 +42709,26 @@ function makeTriggerSchedule(opts) {
42613
42709
  return { due, nextWakeAfter, registrations };
42614
42710
  }
42615
42711
 
42712
+ // src/session/crewSessionResolve.ts
42713
+ function sameDir(a2, b2) {
42714
+ const norm2 = (p2) => p2.split("\\").join("/").replace(/[/]+$/, "").toLowerCase();
42715
+ return norm2(a2) === norm2(b2);
42716
+ }
42717
+ function resolveCrewSession(binding, handles) {
42718
+ const matches = handles.filter((h2) => h2.status === "created" && h2.sessionName === binding.name && sameDir(h2.workdir, binding.workdir));
42719
+ if (matches.length) {
42720
+ const newest = matches.reduce((a2, b2) => b2.createdAt > a2.createdAt ? b2 : a2);
42721
+ return { kind: "reuse", sessionId: newest.id };
42722
+ }
42723
+ if (!binding.createIfMissing) {
42724
+ return {
42725
+ kind: "refuse",
42726
+ reason: `no live session "${binding.name}" on ${binding.workdir}, and createIfMissing is off`
42727
+ };
42728
+ }
42729
+ return { kind: "create", spec: { workdir: binding.workdir, sessionName: binding.name } };
42730
+ }
42731
+
42616
42732
  // src/runner/dispatcher.ts
42617
42733
  function hasBudget2(def) {
42618
42734
  return def.budget?.maxUsd != null || def.budget?.maxTokens != null;
@@ -42636,11 +42752,17 @@ function makeDispatcher(opts) {
42636
42752
  });
42637
42753
  return next;
42638
42754
  }
42639
- async function recordFailedDispatch(d2, root, startedAt, runId, message) {
42755
+ async function recordFailedDispatch(d2, root, startedAt, runId, message, where) {
42640
42756
  await appendRun(root.files, root.root, {
42641
42757
  runId,
42642
42758
  agent: d2.agent,
42643
- where: "host",
42759
+ /* PASSED, NOT ASSUMED. This said `where: 'host'` unconditionally, so
42760
+ * every D5 refusal -- no session backend, `createIfMissing: false`, a
42761
+ * `create` that threw, a malformed `session:` block -- told the operator
42762
+ * the run had happened on the host, which is the one thing this feature
42763
+ * guarantees did NOT happen. A bound agent that refuses files a
42764
+ * `where: 'session'` row: nothing ran, and nothing ran THERE. */
42765
+ where,
42644
42766
  source: d2.trigger,
42645
42767
  startedAt,
42646
42768
  endedAt: now2(),
@@ -42648,6 +42770,35 @@ function makeDispatcher(opts) {
42648
42770
  stopReason: message
42649
42771
  }, log);
42650
42772
  }
42773
+ async function startInSession(binding, d2) {
42774
+ const sessions = opts.sessions?.();
42775
+ if (!sessions) {
42776
+ throw new Error(
42777
+ `crew agent "${d2.agent}" is bound to session "${binding.name}", but this daemon has no session backend`
42778
+ );
42779
+ }
42780
+ try {
42781
+ await sessions.adopt?.();
42782
+ } catch {
42783
+ }
42784
+ const decision = resolveCrewSession(binding, sessions.list());
42785
+ if (decision.kind === "refuse") {
42786
+ throw new Error(`crew agent "${d2.agent}": ${decision.reason}`);
42787
+ }
42788
+ const sessionId = decision.kind === "reuse" ? decision.sessionId : (await sessions.create(decision.spec)).id;
42789
+ return sessions.run(
42790
+ sessionId,
42791
+ d2.agent,
42792
+ d2.input,
42793
+ () => {
42794
+ },
42795
+ { owner: d2.owner },
42796
+ // `source` is the trigger, not 'chat': the session's own run record is
42797
+ // the only durable answer to "why did this happen", and it is written by
42798
+ // the session layer, not by this file's failure path.
42799
+ { allowMutations: true, source: d2.trigger }
42800
+ );
42801
+ }
42651
42802
  async function fire(d2) {
42652
42803
  const dispatchedAt = now2();
42653
42804
  const key = `${d2.owner}:${d2.agent}:${d2.trigger}`;
@@ -42656,6 +42807,7 @@ function makeDispatcher(opts) {
42656
42807
  return;
42657
42808
  }
42658
42809
  inFlight.add(key);
42810
+ let where = "host";
42659
42811
  try {
42660
42812
  const roots = await opts.ownerRoots();
42661
42813
  const match = roots.find((r2) => r2.owner === d2.owner);
@@ -42668,12 +42820,17 @@ function makeDispatcher(opts) {
42668
42820
  log(`dispatcher: agent "${d2.agent}" not found (owner "${d2.owner}") -- skipped`);
42669
42821
  return;
42670
42822
  }
42823
+ where = def.session || def.sessionError ? "session" : "host";
42671
42824
  if (d2.trigger === "continuous" && !hasBudget2(def)) {
42672
42825
  log(`refusing continuous run for "${d2.agent}" (owner "${d2.owner}"): a budget (maxUsd or maxTokens) is required`);
42673
42826
  return;
42674
42827
  }
42675
42828
  await withAgentStateLock(`${d2.owner}:${d2.agent}`, () => persistDispatch(d2, match));
42676
- const started = await runner.run(d2.agent, d2.input, () => {
42829
+ if (def.sessionError) {
42830
+ throw new Error(`crew agent "${d2.agent}" has an unusable session: block -- ${def.sessionError}`);
42831
+ }
42832
+ const binding = def.session;
42833
+ const started = binding ? await startInSession(binding, d2) : await runner.run(d2.agent, d2.input, () => {
42677
42834
  }, { owner: d2.owner }, d2.generation ?? 0, d2.trigger);
42678
42835
  const startedAt = now2();
42679
42836
  const announce = (extra) => {
@@ -42705,7 +42862,7 @@ function makeDispatcher(opts) {
42705
42862
  announce({ endedAt: now2(), ok: false, stopReason: msg });
42706
42863
  log(`dispatcher: run of "${d2.agent}" (${d2.trigger}) failed -- ${msg}`);
42707
42864
  void settleSelfDirected(d2, match);
42708
- void recordFailedDispatch(d2, match, startedAt, started.runId, msg);
42865
+ void recordFailedDispatch(d2, match, startedAt, started.runId, msg, where);
42709
42866
  }
42710
42867
  );
42711
42868
  } catch (err) {
@@ -42714,7 +42871,7 @@ function makeDispatcher(opts) {
42714
42871
  try {
42715
42872
  const roots = await opts.ownerRoots();
42716
42873
  const root = roots.find((r2) => r2.owner === d2.owner);
42717
- if (root) await recordFailedDispatch(d2, root, dispatchedAt, `dispatch-${dispatchedAt}`, msg);
42874
+ if (root) await recordFailedDispatch(d2, root, dispatchedAt, `dispatch-${dispatchedAt}`, msg, where);
42718
42875
  } catch {
42719
42876
  }
42720
42877
  } finally {
@@ -43404,6 +43561,9 @@ function startDaemonCore(opts) {
43404
43561
  // it -- the same contract `schedule_self` has. `clock` is declared below
43405
43562
  // and only read when this fires, long after it is initialized.
43406
43563
  onScheduled: () => clock.wake(),
43564
+ // Read per fire(), so a daemon whose session layer comes up after this
43565
+ // core still routes bound agents into it.
43566
+ ...opts.sessions ? { sessions: opts.sessions } : {},
43407
43567
  log
43408
43568
  });
43409
43569
  const roomProgressSubs = /* @__PURE__ */ new Set();
@@ -45398,6 +45558,7 @@ async function resolveEndpoint(args, env = process.env) {
45398
45558
  import { readFile as readFile2, writeFile, mkdir as mkdir3 } from "node:fs/promises";
45399
45559
  import { dirname as dirname7, join as join18 } from "node:path";
45400
45560
  var DAEMON_CONFIG_VERSION = 1;
45561
+ var SECRET_KEYS = ["acpAuth", "tunnelToken", "token"];
45401
45562
  var KNOWN_KEYS = [
45402
45563
  "version",
45403
45564
  "port",
@@ -45514,6 +45675,10 @@ function layerConfig(args, explicit, file) {
45514
45675
  from[key] = "file";
45515
45676
  if (key === "endpoint") out.endpointExplicit = true;
45516
45677
  }
45678
+ if (out.allowLan) {
45679
+ if (from.host === "default") out.host = "0.0.0.0";
45680
+ if (from.selfSigned === "default" && !out.tlsCert && !out.tlsKey) out.selfSigned = true;
45681
+ }
45517
45682
  return { args: out, from, warnings };
45518
45683
  }
45519
45684
 
@@ -45576,59 +45741,285 @@ async function runSetup(argv, io) {
45576
45741
  return 0;
45577
45742
  }
45578
45743
 
45579
- // src/config/roots.ts
45744
+ // src/config/pathList.ts
45580
45745
  import { resolve as resolve10 } from "node:path";
45581
- async function runRoots(argv, io) {
45746
+ function stripStateDir(argv) {
45747
+ const at2 = argv.indexOf("--state-dir");
45748
+ if (at2 < 0) return argv;
45749
+ return [...argv.slice(0, at2), ...argv.slice(at2 + (argv[at2 + 1] === void 0 ? 1 : 2))];
45750
+ }
45751
+ async function runPathList(raw, io, spec) {
45752
+ const argv = stripStateDir(raw);
45582
45753
  const sub = argv[0];
45583
45754
  const { config, unknown, corrupt } = await readDaemonConfig(io.stateDir);
45584
- const roots = Array.isArray(config.roots) ? config.roots : [];
45755
+ const stored = config[spec.key];
45756
+ const list = Array.isArray(stored) ? stored : [];
45585
45757
  if (corrupt && (sub === "add" || sub === "remove")) {
45586
45758
  io.err(`daemon.json in ${io.stateDir} is not valid JSON; fix or delete it before writing.`);
45587
45759
  return 2;
45588
45760
  }
45589
45761
  if (sub === "list") {
45590
- if (roots.length === 0) {
45591
- io.out("No working directories are configured. Add one with: corenel roots add <path>");
45762
+ if (list.length === 0) {
45763
+ io.out(spec.empty);
45592
45764
  return 0;
45593
45765
  }
45594
- for (const r2 of roots) io.out(r2);
45766
+ for (const p2 of list) io.out(p2);
45595
45767
  return 0;
45596
45768
  }
45597
45769
  if (sub === "add") {
45598
45770
  const path = argv[1];
45599
45771
  if (!path) {
45600
- io.err("corenel roots add needs a path.");
45772
+ io.err(`${spec.command} add needs a path.`);
45601
45773
  return 2;
45602
45774
  }
45603
45775
  const full = resolve10(path);
45604
- if (roots.some((r2) => resolve10(r2) === full)) {
45605
- io.out(`${full} is already a configured working directory.`);
45776
+ if (list.some((p2) => resolve10(p2) === full)) {
45777
+ io.out(`${full} is already a configured ${spec.noun}.`);
45606
45778
  return 0;
45607
45779
  }
45608
- await writeDaemonConfig(io.stateDir, { ...config, roots: [...roots, full] }, unknown);
45780
+ await writeDaemonConfig(io.stateDir, { ...config, [spec.key]: [...list, full] }, unknown);
45609
45781
  io.out(`Added ${full} to daemon.json. A running daemon picks this up on restart.`);
45610
45782
  return 0;
45611
45783
  }
45612
45784
  if (sub === "remove") {
45613
45785
  const path = argv[1];
45614
45786
  if (!path) {
45615
- io.err("corenel roots remove needs a path.");
45787
+ io.err(`${spec.command} remove needs a path.`);
45616
45788
  return 2;
45617
45789
  }
45618
45790
  const full = resolve10(path);
45619
- if (!roots.some((r2) => resolve10(r2) === full)) {
45620
- io.err(`${full} is not a configured working directory.`);
45791
+ if (!list.some((p2) => resolve10(p2) === full)) {
45792
+ io.err(`${full} is not a configured ${spec.noun}.`);
45621
45793
  return 2;
45622
45794
  }
45623
45795
  await writeDaemonConfig(
45624
45796
  io.stateDir,
45625
- { ...config, roots: roots.filter((r2) => resolve10(r2) !== full) },
45797
+ { ...config, [spec.key]: list.filter((p2) => resolve10(p2) !== full) },
45626
45798
  unknown
45627
45799
  );
45628
45800
  io.out(`Removed ${full} from daemon.json. A running daemon keeps it until restart.`);
45629
45801
  return 0;
45630
45802
  }
45631
- io.err(`Unknown subcommand "${sub ?? ""}". Use: corenel roots list | add <path> | remove <path>`);
45803
+ io.err(`Unknown subcommand "${sub ?? ""}". Use: ${spec.command} list | add <path> | remove <path>`);
45804
+ return 2;
45805
+ }
45806
+
45807
+ // src/config/roots.ts
45808
+ function runRoots(argv, io) {
45809
+ return runPathList(argv, io, {
45810
+ key: "roots",
45811
+ command: "corenel roots",
45812
+ noun: "working directory",
45813
+ empty: "No working directories are configured. Add one with: corenel roots add <path>"
45814
+ });
45815
+ }
45816
+
45817
+ // src/config/addUnder.ts
45818
+ function runAddUnder(argv, io) {
45819
+ return runPathList(argv, io, {
45820
+ key: "addUnder",
45821
+ command: "corenel add-under",
45822
+ noun: "area a client may add a root under",
45823
+ empty: "No areas are configured. Clients cannot add roots. Add one with: corenel add-under add <path>"
45824
+ });
45825
+ }
45826
+
45827
+ // src/config/keyTypes.ts
45828
+ var KEY_TYPES = {
45829
+ port: "number",
45830
+ host: "string",
45831
+ roots: "string[]",
45832
+ addUnder: "string[]",
45833
+ cloneUnder: "string[]",
45834
+ allowLan: "boolean",
45835
+ allowOrigins: "string[]",
45836
+ tlsCert: "string",
45837
+ tlsKey: "string",
45838
+ selfSigned: "boolean",
45839
+ tunnel: "string",
45840
+ tunnelName: "string",
45841
+ tunnelHostname: "string",
45842
+ endpoint: "string",
45843
+ allowShell: "boolean",
45844
+ keepTranscripts: "boolean",
45845
+ isolation: "string",
45846
+ msbBin: "string",
45847
+ guestImage: "string",
45848
+ maxChainDepth: "number",
45849
+ name: "string",
45850
+ appUrl: "string",
45851
+ defaultModel: "string"
45852
+ };
45853
+ var kebab = (k2) => k2.replace(/[A-Z]/g, (c2) => `-${c2.toLowerCase()}`);
45854
+ function resolveKeyName(input) {
45855
+ if (Object.prototype.hasOwnProperty.call(KEY_TYPES, input)) return input;
45856
+ for (const k2 of Object.keys(KEY_TYPES)) if (kebab(k2) === input) return k2;
45857
+ return null;
45858
+ }
45859
+ function isListKey(key) {
45860
+ return KEY_TYPES[key] === "string[]";
45861
+ }
45862
+ function coerceValue(key, raw) {
45863
+ const type = KEY_TYPES[key];
45864
+ if (!type) return { ok: false, reason: `"${key}" is not a daemon.json setting.` };
45865
+ if (type === "boolean") {
45866
+ if (raw === "true") return { ok: true, value: true };
45867
+ if (raw === "false") return { ok: true, value: false };
45868
+ return { ok: false, reason: `${key} must be true or false, not ${JSON.stringify(raw)}.` };
45869
+ }
45870
+ if (type === "number") {
45871
+ if (!/^-?\d+$/.test(raw)) return { ok: false, reason: `${key} must be a whole number, not ${JSON.stringify(raw)}.` };
45872
+ return { ok: true, value: Number(raw) };
45873
+ }
45874
+ return { ok: true, value: raw };
45875
+ }
45876
+
45877
+ // src/config/configCmd.ts
45878
+ var USAGE = "Use: corenel config list | get <key> | set <key> <value> | unset <key> | add <key> <value> | remove <key> <value>";
45879
+ function render(value) {
45880
+ if (Array.isArray(value)) return value.length ? value.join(", ") : "(none)";
45881
+ if (value === void 0) return "(unset)";
45882
+ return String(value);
45883
+ }
45884
+ function row(key, value, origin) {
45885
+ return ` ${key.padEnd(18)} ${render(value).padEnd(24)} (${origin})`;
45886
+ }
45887
+ function keyOrReason(name) {
45888
+ if (SECRET_KEYS.includes(name)) {
45889
+ return { reason: `${name} is a credential and does not belong in daemon.json. It goes in auth.json.` };
45890
+ }
45891
+ const key = resolveKeyName(name);
45892
+ if (!key) return { reason: `"${name}" is not a daemon.json setting. ${USAGE}` };
45893
+ return { key };
45894
+ }
45895
+ async function runConfig(raw, io) {
45896
+ const argv = stripStateDir(raw);
45897
+ const sub = argv[0];
45898
+ const { config, unknown, corrupt } = await readDaemonConfig(io.stateDir);
45899
+ const isWrite = sub === "set" || sub === "unset" || sub === "add" || sub === "remove";
45900
+ if (corrupt && isWrite) {
45901
+ io.err(`daemon.json in ${io.stateDir} is not valid JSON; fix or delete it before writing.`);
45902
+ return 2;
45903
+ }
45904
+ const read3 = (key) => {
45905
+ const value = config[key];
45906
+ return { value, origin: value === void 0 ? "default" : "file" };
45907
+ };
45908
+ const commit = async (next, said) => {
45909
+ await writeDaemonConfig(io.stateDir, next, unknown);
45910
+ io.out(`${said} A running daemon picks this up on restart.`);
45911
+ return 0;
45912
+ };
45913
+ if (sub === "list") {
45914
+ for (const key of Object.keys(KEY_TYPES).sort()) {
45915
+ const { value, origin } = read3(key);
45916
+ io.out(row(key, value, origin));
45917
+ }
45918
+ const extras = Object.keys(unknown);
45919
+ if (extras.length) {
45920
+ io.out("");
45921
+ io.out(" Keys this version does not manage (kept as they are):");
45922
+ for (const k2 of extras.sort()) io.out(` ${k2.padEnd(18)} ${render(unknown[k2])}`);
45923
+ }
45924
+ if (corrupt) io.err(`daemon.json in ${io.stateDir} is not valid JSON; these are the defaults, not what the file says.`);
45925
+ return 0;
45926
+ }
45927
+ if (sub === "get") {
45928
+ const name = argv[1];
45929
+ if (!name) {
45930
+ io.err(`corenel config get needs a key. ${USAGE}`);
45931
+ return 2;
45932
+ }
45933
+ const found = keyOrReason(name);
45934
+ if ("reason" in found) {
45935
+ io.err(found.reason);
45936
+ return 2;
45937
+ }
45938
+ const { value, origin } = read3(found.key);
45939
+ io.out(` ${render(value).padEnd(24)} (${origin})`);
45940
+ return 0;
45941
+ }
45942
+ if (sub === "set") {
45943
+ const name = argv[1];
45944
+ const values = argv.slice(2);
45945
+ if (!name) {
45946
+ io.err(`corenel config set needs a key. ${USAGE}`);
45947
+ return 2;
45948
+ }
45949
+ const found = keyOrReason(name);
45950
+ if ("reason" in found) {
45951
+ io.err(found.reason);
45952
+ return 2;
45953
+ }
45954
+ if (values.length === 0) {
45955
+ io.err(`corenel config set ${name} needs a value.`);
45956
+ return 2;
45957
+ }
45958
+ if (isListKey(found.key)) {
45959
+ await commit({ ...config, [found.key]: values }, `Set ${found.key} to ${values.join(", ")}.`);
45960
+ return 0;
45961
+ }
45962
+ if (values.length > 1) {
45963
+ io.err(`${found.key} holds one value; give just one.`);
45964
+ return 2;
45965
+ }
45966
+ const coerced = coerceValue(found.key, values[0]);
45967
+ if (!coerced.ok) {
45968
+ io.err(coerced.reason);
45969
+ return 2;
45970
+ }
45971
+ return commit({ ...config, [found.key]: coerced.value }, `Set ${found.key} to ${String(coerced.value)}.`);
45972
+ }
45973
+ if (sub === "unset") {
45974
+ const name = argv[1];
45975
+ if (!name) {
45976
+ io.err(`corenel config unset needs a key. ${USAGE}`);
45977
+ return 2;
45978
+ }
45979
+ const found = keyOrReason(name);
45980
+ if ("reason" in found) {
45981
+ io.err(found.reason);
45982
+ return 2;
45983
+ }
45984
+ const next = { ...config };
45985
+ delete next[found.key];
45986
+ return commit(next, `Unset ${found.key}; the default applies again.`);
45987
+ }
45988
+ if (sub === "add" || sub === "remove") {
45989
+ const name = argv[1];
45990
+ const value = argv[2];
45991
+ if (!name || !value) {
45992
+ io.err(`corenel config ${sub} needs a key and a value.`);
45993
+ return 2;
45994
+ }
45995
+ const found = keyOrReason(name);
45996
+ if ("reason" in found) {
45997
+ io.err(found.reason);
45998
+ return 2;
45999
+ }
46000
+ if (!isListKey(found.key)) {
46001
+ io.err(`${found.key} holds one value; change it with: corenel config set ${name} <value>`);
46002
+ return 2;
46003
+ }
46004
+ const raw2 = config[found.key];
46005
+ const list = Array.isArray(raw2) ? raw2 : [];
46006
+ if (sub === "add") {
46007
+ if (list.includes(value)) {
46008
+ io.out(`${value} is already in ${found.key}.`);
46009
+ return 0;
46010
+ }
46011
+ return commit({ ...config, [found.key]: [...list, value] }, `Added ${value} to ${found.key}.`);
46012
+ }
46013
+ if (!list.includes(value)) {
46014
+ io.err(`${value} is not in ${found.key}.`);
46015
+ return 2;
46016
+ }
46017
+ return commit(
46018
+ { ...config, [found.key]: list.filter((v2) => v2 !== value) },
46019
+ `Removed ${value} from ${found.key}.`
46020
+ );
46021
+ }
46022
+ io.err(`Unknown subcommand "${sub ?? ""}". ${USAGE}`);
45632
46023
  return 2;
45633
46024
  }
45634
46025
 
@@ -47253,7 +47644,7 @@ async function main() {
47253
47644
  await runAcpAgent(rawArgv.slice(1));
47254
47645
  return;
47255
47646
  }
47256
- if (rawArgv[0] === "setup" || rawArgv[0] === "roots" || rawArgv[0] === "doctor") {
47647
+ if (rawArgv[0] === "setup" || rawArgv[0] === "roots" || rawArgv[0] === "add-under" || rawArgv[0] === "config" || rawArgv[0] === "doctor") {
47257
47648
  const sub = rawArgv[0];
47258
47649
  const rest = rawArgv.slice(1);
47259
47650
  try {
@@ -47266,6 +47657,8 @@ async function main() {
47266
47657
  };
47267
47658
  if (sub === "setup") process.exit(await runSetup(rest, io));
47268
47659
  if (sub === "roots") process.exit(await runRoots(rest, io));
47660
+ if (sub === "add-under") process.exit(await runAddUnder(rest, io));
47661
+ if (sub === "config") process.exit(await runConfig(rest, io));
47269
47662
  const { args: flagArgs2, explicit: explicit2 } = parseArgsWithProvenance(rest);
47270
47663
  const { config, corrupt } = await readDaemonConfig(io.stateDir);
47271
47664
  const { args: args2, from, warnings } = layerConfig(flagArgs2, explicit2, config);
@@ -47419,6 +47812,7 @@ async function main() {
47419
47812
  const attendance = makeAttendanceRegistry({ log: slog });
47420
47813
  const runs = makeRunHub({ log: slog });
47421
47814
  let core;
47815
+ let crewSessions;
47422
47816
  const pauses = args.daemon ? await loadPauseStore(args.stateDir) : void 0;
47423
47817
  let controlPlane = null;
47424
47818
  const runner = args.daemon ? new NodeRunnerService({
@@ -47533,6 +47927,9 @@ async function main() {
47533
47927
  // through the attendance registry -- there is no room-progress member on
47534
47928
  // AttendanceRegistry (unlike runActivity/registrations), and adding one
47535
47929
  // is out of this task's scope.
47930
+ // Bound crew agents run in the session layer built below -- read through
47931
+ // the holder, never captured by value, because it does not exist yet.
47932
+ sessions: () => crewSessions,
47536
47933
  onRoomProgress: (room) => slog(`room ${room.roomId}: ${room.turns} turn(s), ${room.participants.length} participant(s)${room.closed ? ` -- closed (${room.closed.reason})` : ""}`)
47537
47934
  });
47538
47935
  }
@@ -47658,6 +48055,7 @@ async function main() {
47658
48055
  git: createGitSource({ areas: args.cloneUnder, run: makeGitRunner(), log: slog }),
47659
48056
  approveCheckout: (dir) => rootsController.addFromClone(dir)
47660
48057
  }) : void 0;
48058
+ crewSessions = makeCrewSessions(sessions, { stateDir: stateDirName(), guestEnv });
47661
48059
  const handle = await startSidecar({
47662
48060
  port: args.port,
47663
48061
  host: args.host,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/sidecar",
3
- "version": "0.6.1",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "Elastic-2.0",
6
6
  "description": "Prompd sidecar — a local capability server. Run it next to Prompd (prompd.app) so the browser app can reach your machine: local LLMs (Ollama/LM Studio/vLLM) without CORS, your filesystem, and (opt-in) shell.",
@@ -36,10 +36,10 @@
36
36
  "esbuild": "^0.21.5",
37
37
  "tsx": "^4.19.2",
38
38
  "typescript": "^5.9.3",
39
- "@corenel/protocol": "0.6.1",
40
- "@corenel/harness": "0.6.1",
41
39
  "@corenel/tools-node": "0.3.1",
42
- "@corenel/crew": "0.0.0"
40
+ "@corenel/protocol": "0.6.1",
41
+ "@corenel/crew": "0.0.0",
42
+ "@corenel/harness": "0.6.1"
43
43
  },
44
44
  "repository": {
45
45
  "type": "git",