@corenel/sidecar 0.9.0 → 0.10.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 +313 -40
  2. package/package.json +5 -5
package/dist/cli.js CHANGED
@@ -25468,6 +25468,11 @@ var SidecarServer = class {
25468
25468
  /** The friendly label from the same `hello`, kept beside `clientId` so the
25469
25469
  * dirName resolver is handed the identity the manifest keys on. */
25470
25470
  clientName;
25471
+ /** This connection's attended-client object, retained so an answer it sends
25472
+ * can be attributed to it. Typed `unknown` because its only consumer is the
25473
+ * registry's own identity lookup, which takes `unknown` for the
25474
+ * dependency-direction reason on `SidecarAttendanceRegistry.answer`. */
25475
+ attended;
25471
25476
  /** Detach function for this connection's attendance attachment, set on a
25472
25477
  * successful `hello` when `opts.attendance` is wired. Called once, on channel
25473
25478
  * close, so a dead connection stops receiving/counting toward asks. */
@@ -25579,7 +25584,7 @@ var SidecarServer = class {
25579
25584
  return;
25580
25585
  case "permission-answer":
25581
25586
  if (!this.authed) return;
25582
- this.opts.attendance?.answer(m2.id, m2.decision);
25587
+ this.opts.attendance?.answer(m2.id, m2.decision, this.attended);
25583
25588
  return;
25584
25589
  case "roots-browse":
25585
25590
  if (!this.authed) {
@@ -25829,10 +25834,11 @@ var SidecarServer = class {
25829
25834
  }
25830
25835
  this.authed = true;
25831
25836
  this.hostStateDir = m2.stateDir ?? ".corenel";
25837
+ let accounts = [];
25832
25838
  if (typeof m2.clientId === "string" && m2.clientId) {
25833
25839
  const wireId = m2.clientId.slice(0, MAX_IDENTITY_FIELD_LEN);
25834
25840
  const claimed = Array.isArray(m2.accounts) ? m2.accounts : [];
25835
- const accounts = claimed.length > MAX_CLAIMED_ACCOUNTS ? [] : claimed.filter((a2) => typeof a2 === "string" && a2.length <= MAX_IDENTITY_FIELD_LEN);
25841
+ accounts = claimed.length > MAX_CLAIMED_ACCOUNTS ? [] : claimed.filter((a2) => typeof a2 === "string" && a2.length <= MAX_IDENTITY_FIELD_LEN);
25836
25842
  let clientId = wireId;
25837
25843
  if (this.opts.clientKey) {
25838
25844
  try {
@@ -25861,11 +25867,16 @@ var SidecarServer = class {
25861
25867
  }
25862
25868
  }
25863
25869
  if (this.opts.attendance) {
25864
- this.attendedDetach = this.opts.attendance.attach({
25870
+ const attended = {
25865
25871
  send: (ask) => this.ch.send({ t: "permission-ask", ...ask }),
25866
- resolved: (id, decision, by) => this.ch.send({ t: "permission-resolved", id, decision, by }),
25872
+ resolved: (id, decision, by, byClientId) => this.ch.send({ t: "permission-resolved", id, decision, by, ...byClientId ? { byClientId } : {} }),
25867
25873
  runActivity: (activity) => this.ch.send({ t: "run-activity", activity }),
25868
25874
  registrations: (registrations) => this.ch.send({ t: "registrations", registrations })
25875
+ };
25876
+ this.attended = attended;
25877
+ this.attendedDetach = this.opts.attendance.attach(attended, {
25878
+ clientId: this.clientId ?? "",
25879
+ accounts
25869
25880
  });
25870
25881
  }
25871
25882
  this.opts.log?.(`paired \u2014 ${this.tools.size} ${this.tools.size === 1 ? "tool" : "tools"} advertised (host dir: ${this.hostStateDir})`);
@@ -26543,8 +26554,20 @@ var SidecarServer = class {
26543
26554
  this.ch.send({ t: "room-open-result", id: m2.id, ok: false, problems: ["this sidecar does not host rooms"] });
26544
26555
  return;
26545
26556
  }
26557
+ if (this.opts.teamMode && this.clientId === void 0) {
26558
+ this.ch.send({
26559
+ t: "room-open-result",
26560
+ id: m2.id,
26561
+ ok: false,
26562
+ problems: ["This connection has no verified identity (no clientId from hello), so it cannot open a room."]
26563
+ });
26564
+ return;
26565
+ }
26566
+ const clientId = this.clientId;
26567
+ const owner = clientId === void 0 ? void 0 : m2.req.participants.find((p2) => p2.kind === "human" && p2.role === "owner");
26568
+ const req = owner && clientId !== void 0 ? { ...m2.req, participants: m2.req.participants.map((p2) => p2 === owner ? { ...p2, id: clientId } : p2) } : m2.req;
26546
26569
  try {
26547
- const r2 = await this.opts.rooms.open(m2.req);
26570
+ const r2 = await this.opts.rooms.open(req);
26548
26571
  this.ch.send(r2.ok ? { t: "room-open-result", id: m2.id, ok: true, room: r2.room } : { t: "room-open-result", id: m2.id, ok: false, problems: r2.problems });
26549
26572
  } catch (err) {
26550
26573
  this.ch.send({ t: "room-open-result", id: m2.id, ok: false, problems: [errMessage(err)] });
@@ -26556,7 +26579,9 @@ var SidecarServer = class {
26556
26579
  return;
26557
26580
  }
26558
26581
  try {
26559
- this.ch.send({ t: "room-list-result", id: m2.id, ok: true, rooms: await this.opts.rooms.list() });
26582
+ const rooms = await this.opts.rooms.list();
26583
+ const mine = !this.opts.teamMode ? rooms : this.clientId === void 0 ? [] : rooms.filter((r2) => r2.participants.some((p2) => p2.id === this.clientId));
26584
+ this.ch.send({ t: "room-list-result", id: m2.id, ok: true, rooms: mine });
26560
26585
  } catch (err) {
26561
26586
  this.ch.send({ t: "room-list-result", id: m2.id, ok: false, error: errMessage(err) });
26562
26587
  }
@@ -26568,6 +26593,26 @@ var SidecarServer = class {
26568
26593
  }
26569
26594
  try {
26570
26595
  const found = await this.opts.rooms.read(m2.roomId);
26596
+ if (found && this.opts.teamMode) {
26597
+ if (this.clientId === void 0) {
26598
+ this.ch.send({
26599
+ t: "room-read-result",
26600
+ id: m2.id,
26601
+ ok: false,
26602
+ error: "This connection has no verified identity, so membership in this room cannot be proven."
26603
+ });
26604
+ return;
26605
+ }
26606
+ if (!found.room.participants.some((p2) => p2.id === this.clientId)) {
26607
+ this.ch.send({
26608
+ t: "room-read-result",
26609
+ id: m2.id,
26610
+ ok: false,
26611
+ error: `"${this.clientId}" is not a member of room "${m2.roomId}".`
26612
+ });
26613
+ return;
26614
+ }
26615
+ }
26571
26616
  this.ch.send(found ? { t: "room-read-result", id: m2.id, ok: true, room: found.room, messages: found.messages, turnCosts: found.turnCosts } : { t: "room-read-result", id: m2.id, ok: true, room: null, messages: [], turnCosts: [] });
26572
26617
  } catch (err) {
26573
26618
  this.ch.send({ t: "room-read-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26585,13 +26630,45 @@ var SidecarServer = class {
26585
26630
  this.ch.send({ t: "room-resume-result", id: m2.id, ok: false, error: errMessage(err) });
26586
26631
  }
26587
26632
  }
26633
+ /* ONE identity rule for every room verb.
26634
+ *
26635
+ * `room-read` judges membership on `this.clientId` -- the identity this
26636
+ * connection PROVED at `hello`. The mutating verbs used to hand
26637
+ * `m.participantId` straight off the wire to `requireRole`/`mayAct`, so one
26638
+ * branch enforced while another trusted the caller: a viewer could send the
26639
+ * owner's participantId and close the room. Under team mode every verb now
26640
+ * judges the caller the same way `room-read` does, and a connection with no
26641
+ * proven identity is refused rather than attributed to whatever it claimed.
26642
+ *
26643
+ * KNOWN, DELIBERATE CONSEQUENCE (R9): the observed `clientId` and the
26644
+ * participant ids a room is opened with are currently DIFFERENT NAMESPACES
26645
+ * for everyone but the owner (whose id `onRoomOpen` overwrites with the
26646
+ * observed one). Until those namespaces are reconciled, these verbs will
26647
+ * REFUSE non-owner participants under `--mode team`. That refusal is the
26648
+ * intended outcome -- fail closed, consistently -- and must NOT be "fixed"
26649
+ * by falling back to the wire id, which is the hole this closes.
26650
+ *
26651
+ * With `--mode team` OFF this is the pre-existing behaviour, untouched: the
26652
+ * wire id is attribution only, exactly as the spec knowingly accepted. */
26653
+ actingId(claimed) {
26654
+ if (!this.opts.teamMode) return { ok: true, id: claimed };
26655
+ if (this.clientId === void 0) {
26656
+ return { ok: false, error: "This connection has no verified identity (no clientId from hello), so it cannot act on a room." };
26657
+ }
26658
+ return { ok: true, id: this.clientId };
26659
+ }
26588
26660
  async onRoomAnswer(m2) {
26589
26661
  if (!this.opts.rooms) {
26590
26662
  this.ch.send({ t: "room-answer-result", id: m2.id, ok: false, error: "this sidecar does not host rooms" });
26591
26663
  return;
26592
26664
  }
26593
26665
  try {
26594
- const room = await this.opts.rooms.answer(m2.roomId, m2.participantId, m2.text, m2.addressed);
26666
+ const actor = this.actingId(m2.participantId);
26667
+ if (!actor.ok) {
26668
+ this.ch.send({ t: "room-answer-result", id: m2.id, ok: false, error: actor.error });
26669
+ return;
26670
+ }
26671
+ const room = await this.opts.rooms.answer(m2.roomId, actor.id, m2.text, m2.addressed);
26595
26672
  this.ch.send({ t: "room-answer-result", id: m2.id, ok: true, room });
26596
26673
  } catch (err) {
26597
26674
  this.ch.send({ t: "room-answer-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26603,7 +26680,12 @@ var SidecarServer = class {
26603
26680
  return;
26604
26681
  }
26605
26682
  try {
26606
- const room = await this.opts.rooms.interject(m2.roomId, m2.participantId, m2.text);
26683
+ const actor = this.actingId(m2.participantId);
26684
+ if (!actor.ok) {
26685
+ this.ch.send({ t: "room-interject-result", id: m2.id, ok: false, error: actor.error });
26686
+ return;
26687
+ }
26688
+ const room = await this.opts.rooms.interject(m2.roomId, actor.id, m2.text);
26607
26689
  this.ch.send({ t: "room-interject-result", id: m2.id, ok: true, room });
26608
26690
  } catch (err) {
26609
26691
  this.ch.send({ t: "room-interject-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26615,7 +26697,12 @@ var SidecarServer = class {
26615
26697
  return;
26616
26698
  }
26617
26699
  try {
26618
- const room = await this.opts.rooms.close(m2.roomId, m2.participantId, m2.reason);
26700
+ const actor = this.actingId(m2.participantId);
26701
+ if (!actor.ok) {
26702
+ this.ch.send({ t: "room-close-result", id: m2.id, ok: false, error: actor.error });
26703
+ return;
26704
+ }
26705
+ const room = await this.opts.rooms.close(m2.roomId, actor.id, m2.reason);
26619
26706
  this.ch.send({ t: "room-close-result", id: m2.id, ok: true, room });
26620
26707
  } catch (err) {
26621
26708
  this.ch.send({ t: "room-close-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -26714,7 +26801,12 @@ var SidecarServer = class {
26714
26801
  return;
26715
26802
  }
26716
26803
  try {
26717
- const findings = await this.opts.rooms.editFinding(m2.roomId, m2.participantId, m2.edit);
26804
+ const actor = this.actingId(m2.participantId);
26805
+ if (!actor.ok) {
26806
+ this.ch.send({ t: "room-finding-edit-result", id: m2.id, ok: false, error: actor.error });
26807
+ return;
26808
+ }
26809
+ const findings = await this.opts.rooms.editFinding(m2.roomId, actor.id, m2.edit);
26718
26810
  this.ch.send({ t: "room-finding-edit-result", id: m2.id, ok: true, findings });
26719
26811
  } catch (err) {
26720
26812
  this.ch.send({ t: "room-finding-edit-result", id: m2.id, ok: false, error: errMessage(err) });
@@ -27017,6 +27109,7 @@ function startSidecar(opts) {
27017
27109
  procs: opts.procs,
27018
27110
  sessions: opts.sessions,
27019
27111
  rooms: opts.rooms,
27112
+ teamMode: opts.teamMode,
27020
27113
  pauses: opts.pauses,
27021
27114
  clientDirName: opts.clientDirName,
27022
27115
  hosts: opts.hosts,
@@ -27201,6 +27294,7 @@ var FLAGS = [
27201
27294
  { flag: "--allow-origin", arg: "<url>", help: "Allow a browser origin to connect. Repeatable." },
27202
27295
  { flag: "--allow-shell", help: "Enable the shell tool. Off by default." },
27203
27296
  { flag: "--allow-lan", help: "Serve the local network, not just this machine: binds all interfaces and serves wss with a self-signed cert. --host / --tls-cert override each half." },
27297
+ { flag: "--mode", arg: "team", help: "Route a permission ask to the connection whose run raised it, plus the other devices that owner is signed in on, rather than to everyone attached. The identity a client sends is CLAIMED and this daemon cannot verify it: attribution, not authorization. An ask is never diverted away from its owner, so a false claim can join a prompt but never steal one." },
27204
27298
  { flag: "--tls-cert", arg: "<file>", help: "TLS certificate for wss://." },
27205
27299
  { flag: "--tls-key", arg: "<file>", help: "TLS private key for wss://." },
27206
27300
  { flag: "--self-signed", help: "Generate a self-signed certificate instead of supplying one." },
@@ -28164,6 +28258,7 @@ var KNOWN_KEYS = [
28164
28258
  "maxChainDepth",
28165
28259
  "name",
28166
28260
  "appUrl",
28261
+ "mode",
28167
28262
  "defaultModel",
28168
28263
  "mcpServers",
28169
28264
  "mcpAllow"
@@ -31996,26 +32091,39 @@ function makeAgentMemoryProvider(getFiles, memoryRoot) {
31996
32091
  };
31997
32092
  }
31998
32093
 
32094
+ // ../crew/room/types.ts
32095
+ function roleOf(p2) {
32096
+ return p2.kind === "agent" ? null : p2.role ?? "contributor";
32097
+ }
32098
+
31999
32099
  // ../crew/room/order.ts
32000
32100
  var ID_RE = /^[A-Za-z0-9_-]+$/;
32001
32101
  function isValidParticipantId(id) {
32002
32102
  return ID_RE.test(id);
32003
32103
  }
32104
+ function rotating(state) {
32105
+ return state.mode === "conversation" ? state.participants.filter((p2) => p2.kind === "agent") : state.participants;
32106
+ }
32004
32107
  function currentSpeaker(state) {
32005
32108
  if (state.closed) return null;
32006
- if (state.jump) return state.participants.find((p2) => p2.id === state.jump) ?? null;
32007
- return state.participants[state.floor] ?? null;
32109
+ const roster = rotating(state);
32110
+ if (state.jump) {
32111
+ const jumped = roster.find((p2) => p2.id === state.jump);
32112
+ if (jumped) return jumped;
32113
+ }
32114
+ return roster[state.floor] ?? null;
32008
32115
  }
32009
32116
  function advance(state, addressed) {
32010
32117
  if (state.closed) return state;
32118
+ const roster = rotating(state);
32011
32119
  const speaker = currentSpeaker(state);
32012
32120
  const spokeViaJump = state.jump != null;
32013
32121
  const turns = state.turns + 1;
32014
32122
  const mayAddress = !spokeViaJump;
32015
- const target = mayAddress && addressed && addressed !== speaker?.id && state.participants.some((p2) => p2.id === addressed) ? addressed : null;
32016
- let floor = spokeViaJump ? state.floor : (state.floor + 1) % state.participants.length;
32017
- if (target != null && state.participants[floor]?.id === target) {
32018
- floor = (floor + 1) % state.participants.length;
32123
+ const target = mayAddress && addressed && addressed !== speaker?.id && roster.some((p2) => p2.id === addressed) ? addressed : null;
32124
+ let floor = spokeViaJump ? state.floor : (state.floor + 1) % roster.length;
32125
+ if (target != null && roster[floor]?.id === target) {
32126
+ floor = (floor + 1) % roster.length;
32019
32127
  }
32020
32128
  const closed = turns >= state.maxTurns ? { by: "room", reason: `Turn cap reached (${state.maxTurns}).`, at: (/* @__PURE__ */ new Date()).toISOString() } : null;
32021
32129
  return { ...state, floor, jump: target, jumpBy: target ? speaker?.id ?? null : null, turns, closed };
@@ -32063,6 +32171,9 @@ function foldRoom(text) {
32063
32171
  maxUsd: typeof line.maxUsd === "number" ? line.maxUsd : null,
32064
32172
  spentUsd: 0,
32065
32173
  closed: null,
32174
+ // Absent means a room written before conversation mode existed, and
32175
+ // every one of those is a rotation room.
32176
+ mode: line.mode ?? "rotation",
32066
32177
  // Absent, rather than 0, on a line whose `at` is missing or not a
32067
32178
  // number: "the log does not say" is not a date at the epoch.
32068
32179
  ...typeof line.at === "number" ? { openedAt: line.at } : {}
@@ -32142,6 +32253,7 @@ function foldSession(text) {
32142
32253
  const turns = [];
32143
32254
  let title = "";
32144
32255
  let titleLocked;
32256
+ let movedTo;
32145
32257
  let updatedAt = 0;
32146
32258
  for (const raw of text.split("\n")) {
32147
32259
  const s2 = raw.trim();
@@ -32163,6 +32275,9 @@ function foldSession(text) {
32163
32275
  } else if (line.k === "turn" && Number.isInteger(line.i) && line.i >= 0 && typeof line.at === "number") {
32164
32276
  turns[line.i] = line.m;
32165
32277
  updatedAt = Math.max(updatedAt, line.at);
32278
+ } else if (line.k === "moved" && typeof line.roomId === "string" && typeof line.at === "number") {
32279
+ movedTo = { movedToRoom: line.roomId, at: line.at };
32280
+ updatedAt = Math.max(updatedAt, line.at);
32166
32281
  }
32167
32282
  }
32168
32283
  if (!meta) return null;
@@ -32173,7 +32288,12 @@ function foldSession(text) {
32173
32288
  updatedAt: updatedAt || meta.createdAt,
32174
32289
  messages: turns.filter((t2) => t2 !== void 0),
32175
32290
  ...meta.surface ? { surface: meta.surface } : {},
32176
- ...titleLocked !== void 0 ? { titleLocked } : {}
32291
+ // Only a STRING owner survives the fold: the line came from disk (or a
32292
+ // sidecar), so its declared type guarantees nothing, and a non-string here
32293
+ // would reach a comparison that decides how a row is drawn.
32294
+ ...typeof meta.owner === "string" && meta.owner ? { owner: meta.owner } : {},
32295
+ ...titleLocked !== void 0 ? { titleLocked } : {},
32296
+ ...movedTo !== void 0 ? { movedTo } : {}
32177
32297
  };
32178
32298
  }
32179
32299
 
@@ -32583,9 +32703,37 @@ function validateRoomRequest(req) {
32583
32703
  "A browser cannot host a room: the messages and outcome a turn produces never leave the browser tab's worker, so the room would rotate blindly with every hand-over and close silently lost. Choose the daemon runtime instead."
32584
32704
  );
32585
32705
  }
32706
+ if (req.mode === "conversation") {
32707
+ if (!req.participants.some((p2) => p2.kind === "agent")) {
32708
+ problems.push(
32709
+ "A conversation room needs at least one agent: humans never hold the floor in this mode, so a room without one has nobody who can ever take a turn."
32710
+ );
32711
+ }
32712
+ if (req.presence === "wait") {
32713
+ problems.push(
32714
+ 'presence "wait" means waiting for a person to take their turn, and in a conversation room nobody does. Use "carry-on".'
32715
+ );
32716
+ }
32717
+ }
32586
32718
  if (!Number.isInteger(req.maxTurns) || req.maxTurns < 1) {
32587
32719
  problems.push("The turn cap must be a positive whole number.");
32588
32720
  }
32721
+ const ROLES = ["owner", "contributor", "viewer"];
32722
+ const owners = req.participants.filter((p2) => p2.kind === "human" && p2.role === "owner");
32723
+ if (owners.length > 1) {
32724
+ problems.push(
32725
+ `A room has one owner; this request names ${owners.length}: ${owners.map((p2) => p2.name).join(", ")}.`
32726
+ );
32727
+ }
32728
+ for (const p2 of req.participants) {
32729
+ const role = p2.role;
32730
+ if (role === void 0) continue;
32731
+ if (p2.kind === "agent") {
32732
+ problems.push(`"${p2.name}" is an agent and cannot hold a role: agents are the work, not participants who are granted access.`);
32733
+ } else if (!ROLES.includes(role)) {
32734
+ problems.push(`"${role}" is not a role. Known roles: ${ROLES.join(", ")}.`);
32735
+ }
32736
+ }
32589
32737
  return problems;
32590
32738
  }
32591
32739
  async function planOpen(req, reach, at2) {
@@ -32617,7 +32765,8 @@ async function planOpen(req, reach, at2) {
32617
32765
  brief: (req.brief ?? "").trim(),
32618
32766
  ...req.maxUsd !== void 0 ? { maxUsd: req.maxUsd } : {},
32619
32767
  runtime: req.runtime,
32620
- presence: req.presence
32768
+ presence: req.presence,
32769
+ ...req.mode !== void 0 ? { mode: req.mode } : {}
32621
32770
  }
32622
32771
  };
32623
32772
  }
@@ -43136,9 +43285,10 @@ function now() {
43136
43285
  return lastTs;
43137
43286
  }
43138
43287
  var JsonlSessionStore = class _JsonlSessionStore {
43139
- constructor(getFiles, root = sessionsRoot) {
43288
+ constructor(getFiles, root = sessionsRoot, owner = () => void 0) {
43140
43289
  this.getFiles = getFiles;
43141
43290
  this.root = root;
43291
+ this.owner = owner;
43142
43292
  }
43143
43293
  cursors = /* @__PURE__ */ new Map();
43144
43294
  async files() {
@@ -43173,11 +43323,13 @@ var JsonlSessionStore = class _JsonlSessionStore {
43173
43323
  async create(title, surface) {
43174
43324
  const createdAt = now();
43175
43325
  const id = uid("ses");
43176
- const meta = { k: "meta", id, title, createdAt, ...surface ? { surface } : {} };
43326
+ const owner = this.owner();
43327
+ const own = owner ? { owner } : {};
43328
+ const meta = { k: "meta", id, title, createdAt, ...surface ? { surface } : {}, ...own };
43177
43329
  const text = encodeLine(meta);
43178
43330
  await (await this.files()).write(this.path(id), text);
43179
43331
  this.cursors.set(id, { text, turnJson: [], title });
43180
- return { id, title, createdAt, updatedAt: createdAt, messages: [], ...surface ? { surface } : {} };
43332
+ return { id, title, createdAt, updatedAt: createdAt, messages: [], ...surface ? { surface } : {}, ...own };
43181
43333
  }
43182
43334
  async get(id) {
43183
43335
  const text = await (await this.files()).read(this.path(id)).catch(() => null);
@@ -43320,7 +43472,8 @@ var JsonlSessionStore = class _JsonlSessionStore {
43320
43472
  id: session.id,
43321
43473
  title: session.title,
43322
43474
  createdAt: session.createdAt,
43323
- ...session.surface ? { surface: session.surface } : {}
43475
+ ...session.surface ? { surface: session.surface } : {},
43476
+ ...session.owner ? { owner: session.owner } : {}
43324
43477
  });
43325
43478
  return { text: meta, turnJson: [], title: session.title, titleLocked: void 0 };
43326
43479
  }
@@ -43838,7 +43991,7 @@ function makeUnattendedPrompt(deps) {
43838
43991
  for (; ; ) {
43839
43992
  if (remaining <= 0) return { by: "timeout" };
43840
43993
  const tickMs = Math.min(remaining, PARK_POLL_MS);
43841
- if (deps.registry.isAttended()) {
43994
+ if (deps.registry.isAttended(deps.owner)) {
43842
43995
  const ctrl2 = new AbortController();
43843
43996
  const answered = deps.registry.broadcastAsk(ask, ctrl2.signal);
43844
43997
  const ticked = sleep2(tickMs, ctrl2.signal).then(() => "tick");
@@ -43876,7 +44029,11 @@ function makeUnattendedPrompt(deps) {
43876
44029
  agent: deps.agent,
43877
44030
  tool: req.tool,
43878
44031
  input: req.input,
43879
- deadline: now2() + bound
44032
+ deadline: now2() + bound,
44033
+ // Spread-when-present rather than `owner: deps.owner`, so an ownerless
44034
+ // ask has no `owner` KEY at all -- the registry's `!ask.owner` test
44035
+ // stays unambiguous and a serialised ask does not carry a null.
44036
+ ...deps.owner ? { owner: deps.owner } : {}
43880
44037
  };
43881
44038
  if (deps.unattended === "park") {
43882
44039
  deps.onParkChange?.(true);
@@ -43896,7 +44053,7 @@ function makeUnattendedPrompt(deps) {
43896
44053
  }
43897
44054
  }
43898
44055
  const fallthrough = deps.unattended === "allow" ? "allow" : "deny";
43899
- if (deps.registry.isAttended()) {
44056
+ if (deps.registry.isAttended(deps.owner)) {
43900
44057
  deps.onParkChange?.(true);
43901
44058
  deps.onAskWait?.(ask);
43902
44059
  try {
@@ -44298,6 +44455,21 @@ var NodeRunnerService = class {
44298
44455
  registry: registry2,
44299
44456
  runId: sessionId,
44300
44457
  agent,
44458
+ /* WHOSE RUN THIS IS, for `--mode team`'s ask routing.
44459
+ *
44460
+ * Only the `{ clientId }` variant names a connection. `{ owner }` is
44461
+ * a dirName the daemon's own trigger dispatcher resolved from
44462
+ * filesystem enumeration -- there is no socket behind it, so an ask
44463
+ * it raises has nobody to route to and correctly falls back to
44464
+ * fan-out. Same for a `{ clientId }` of `undefined` (an older peer).
44465
+ *
44466
+ * Deliberately the RAW clientId rather than `owner` (the resolved
44467
+ * dirName computed above): this value is only ever compared against
44468
+ * the identity a connection ATTACHED with, which is the same
44469
+ * authenticated clientId the harness filed it under. It is never
44470
+ * used as a path, so the manifest resolution that `owner` needs
44471
+ * would not make it safer -- it would just make it fail to match. */
44472
+ ..."owner" in identity || identity.clientId === void 0 ? {} : { owner: identity.clientId },
44301
44473
  unattended: def.policy.unattended ?? "deny",
44302
44474
  ...def.policy.parkTimeoutMs != null ? { parkTimeoutMs: def.policy.parkTimeoutMs } : {},
44303
44475
  remainingMs,
@@ -45079,6 +45251,20 @@ function evaluateRunComplete(completion, agents, maxChainDepth, log) {
45079
45251
  }
45080
45252
 
45081
45253
  // src/runner/roomService.ts
45254
+ function hasRecordedOwner(state) {
45255
+ return state.participants.some((p2) => p2.kind === "human" && p2.role === "owner");
45256
+ }
45257
+ function mayAct(state, participantId, verb) {
45258
+ const member = state.participants.find((p2) => p2.id === participantId);
45259
+ if (!member) return `"${participantId}" is not a member of this room.`;
45260
+ const role = roleOf(member);
45261
+ if (role === null) return `"${participantId}" is an agent, not a participant who acts on the room.`;
45262
+ if (verb === "close") {
45263
+ if (!hasRecordedOwner(state)) return null;
45264
+ return role === "owner" ? null : `Only the owner can ${verb} this room; you are a ${role}.`;
45265
+ }
45266
+ return role === "viewer" ? `You are a viewer in this room and cannot ${verb}.` : null;
45267
+ }
45082
45268
  function isDepsFactory(d2) {
45083
45269
  return typeof d2 === "function";
45084
45270
  }
@@ -45200,6 +45386,11 @@ function makeRoomService(opts) {
45200
45386
  }
45201
45387
  async function open(req) {
45202
45388
  return enqueue(req.roomId, async () => {
45389
+ if (req.fromSession && req.fromSession.messages.length !== req.fromSession.turns) {
45390
+ const message = `Room "${req.roomId}" was not opened: the promoted thread arrived with ${req.fromSession.messages.length} message(s) but ${req.fromSession.turns} were expected. Refusing to open a room missing part of the conversation.`;
45391
+ log(`room "${req.roomId}" could not be opened: ${message}`);
45392
+ return { ok: false, problems: [message] };
45393
+ }
45203
45394
  let deps;
45204
45395
  try {
45205
45396
  deps = await resolveDeps(req.participants, req.maxUsd ?? null);
@@ -45210,6 +45401,15 @@ function makeRoomService(opts) {
45210
45401
  }
45211
45402
  const opened = await openRoom(req, opts.reachability, deps);
45212
45403
  if (!opened.ok) return opened;
45404
+ if (req.fromSession) {
45405
+ try {
45406
+ await deps.store.appendTurns(req.roomId, 0, req.fromSession.messages, deps.now());
45407
+ } catch (err) {
45408
+ const message = err instanceof Error ? err.message : String(err);
45409
+ log(`room "${req.roomId}" opened but its promoted transcript failed to append: ${message}`);
45410
+ return { ok: false, problems: [`Room "${req.roomId}" opened, but the promoted conversation could not be saved into it: ${message}`] };
45411
+ }
45412
+ }
45213
45413
  depsByRoom.set(req.roomId, deps);
45214
45414
  const room = summaryOf(opened.state, null, null, 0);
45215
45415
  record(room);
@@ -45265,10 +45465,22 @@ function makeRoomService(opts) {
45265
45465
  }
45266
45466
  return fresh;
45267
45467
  }
45468
+ async function requireRole(deps, roomId, participantId, verb) {
45469
+ const state = await deps.store.read(roomId);
45470
+ if (!state || state.closed) return;
45471
+ const refusal = mayAct(state, participantId, verb);
45472
+ if (refusal) throw new Error(refusal);
45473
+ }
45268
45474
  async function interject(roomId, participantId, text) {
45269
45475
  return enqueue(roomId, async () => {
45270
45476
  const deps = await baseDeps();
45477
+ await requireRole(deps, roomId, participantId, "interject");
45271
45478
  await interjectHuman(roomId, participantId, text, deps);
45479
+ const state = await deps.store.read(roomId);
45480
+ if (state && !state.closed && state.mode === "conversation") {
45481
+ const driveDeps = await depsFor(roomId);
45482
+ if (driveDeps) return driveRoom(roomId, driveDeps);
45483
+ }
45272
45484
  const summary = await summaryNow(roomId);
45273
45485
  record(summary);
45274
45486
  return summary;
@@ -45277,6 +45489,7 @@ function makeRoomService(opts) {
45277
45489
  async function closeRoom(roomId, participantId, reason) {
45278
45490
  return enqueue(roomId, async () => {
45279
45491
  const deps = await baseDeps();
45492
+ await requireRole(deps, roomId, participantId, "close");
45280
45493
  await closeRoomByHuman(roomId, participantId, reason, deps);
45281
45494
  const summary = await summaryNow(roomId);
45282
45495
  record(summary);
@@ -45290,6 +45503,7 @@ function makeRoomService(opts) {
45290
45503
  async function editFinding(roomId, participantId, edit) {
45291
45504
  return enqueue(roomId, async () => {
45292
45505
  const deps = await baseDeps();
45506
+ await requireRole(deps, roomId, participantId, "finding-edit");
45293
45507
  const doc = await editFindingByHuman(roomId, participantId, edit, deps);
45294
45508
  record(await summaryNow(roomId));
45295
45509
  return doc;
@@ -45299,6 +45513,7 @@ function makeRoomService(opts) {
45299
45513
  return enqueue(roomId, async () => {
45300
45514
  const deps = await depsFor(roomId);
45301
45515
  if (!deps) throw new Error(`No room "${roomId}".`);
45516
+ await requireRole(deps, roomId, participantId, "answer");
45302
45517
  await answerHumanTurn(roomId, participantId, text, deps, addressed);
45303
45518
  return driveRoom(roomId, deps);
45304
45519
  });
@@ -45865,27 +46080,49 @@ function startDaemonCore(opts) {
45865
46080
  function makeAttendanceRegistry(opts) {
45866
46081
  const log = opts?.log ?? (() => {
45867
46082
  });
46083
+ const teamMode = opts?.teamMode === true;
46084
+ const EMPTY = /* @__PURE__ */ new Set();
45868
46085
  const clients = /* @__PURE__ */ new Set();
46086
+ const identities = /* @__PURE__ */ new Map();
45869
46087
  let lastRegistrations = null;
45870
46088
  const outstanding = /* @__PURE__ */ new Map();
45871
46089
  let idCounter = 0;
45872
- const notifyResolved = (id, decision, by) => {
46090
+ const recipients = (ask) => {
46091
+ if (!teamMode || !ask.owner) return clients;
46092
+ const owners = [];
46093
+ const ownerAccounts = /* @__PURE__ */ new Set();
46094
+ for (const [client, id] of identities) {
46095
+ if (id.clientId !== ask.owner) continue;
46096
+ owners.push(client);
46097
+ for (const a2 of id.accounts) ownerAccounts.add(a2);
46098
+ }
46099
+ if (owners.length === 0) return EMPTY;
46100
+ const out = new Set(owners);
46101
+ if (ownerAccounts.size > 0) {
46102
+ for (const [client, id] of identities) {
46103
+ if (id.accounts.some((a2) => ownerAccounts.has(a2))) out.add(client);
46104
+ }
46105
+ }
46106
+ return out;
46107
+ };
46108
+ const notifyResolved = (id, decision, by, byClientId) => {
45873
46109
  for (const c2 of clients) {
45874
- c2.resolved(id, decision, by);
46110
+ c2.resolved(id, decision, by, byClientId);
45875
46111
  }
45876
46112
  };
45877
- const resolveEntry = (id, decision, by) => {
46113
+ const resolveEntry = (id, decision, by, byClientId) => {
45878
46114
  const entry = outstanding.get(id);
45879
46115
  if (!entry) return;
45880
46116
  outstanding.delete(id);
45881
46117
  entry.resolve(decision);
45882
- notifyResolved(id, decision, by);
46118
+ notifyResolved(id, decision, by, byClientId);
45883
46119
  };
45884
46120
  return {
45885
- attach(client) {
46121
+ attach(client, identity) {
45886
46122
  clients.add(client);
46123
+ if (identity) identities.set(client, identity);
45887
46124
  for (const { ask } of outstanding.values()) {
45888
- client.send(ask);
46125
+ if (recipients(ask).has(client)) client.send(ask);
45889
46126
  }
45890
46127
  if (lastRegistrations) {
45891
46128
  try {
@@ -45898,6 +46135,7 @@ function makeAttendanceRegistry(opts) {
45898
46135
  if (detached) return;
45899
46136
  detached = true;
45900
46137
  clients.delete(client);
46138
+ identities.delete(client);
45901
46139
  if (clients.size === 0 && outstanding.size > 0) {
45902
46140
  log(`attendance: last client detached with ${outstanding.size} ask(s) outstanding; resolving null`);
45903
46141
  for (const [id, entry] of Array.from(outstanding.entries())) {
@@ -45907,14 +46145,22 @@ function makeAttendanceRegistry(opts) {
45907
46145
  }
45908
46146
  };
45909
46147
  },
45910
- isAttended() {
45911
- return clients.size > 0;
46148
+ attachedIdentities() {
46149
+ return Array.from(identities.values());
46150
+ },
46151
+ isAttended(owner) {
46152
+ if (!teamMode || !owner) return clients.size > 0;
46153
+ for (const id of identities.values()) {
46154
+ if (id.clientId === owner) return true;
46155
+ }
46156
+ return false;
45912
46157
  },
45913
46158
  outstanding() {
45914
46159
  return Array.from(outstanding.values()).map((e2) => e2.ask);
45915
46160
  },
45916
46161
  broadcastAsk(ask, signal) {
45917
- if (clients.size === 0) {
46162
+ const to = recipients(ask);
46163
+ if (to.size === 0) {
45918
46164
  return Promise.resolve(null);
45919
46165
  }
45920
46166
  return new Promise((resolve14) => {
@@ -45932,7 +46178,7 @@ function makeAttendanceRegistry(opts) {
45932
46178
  resolve14(decision);
45933
46179
  }
45934
46180
  });
45935
- for (const c2 of clients) {
46181
+ for (const c2 of to) {
45936
46182
  c2.send(ask);
45937
46183
  }
45938
46184
  if (signal) {
@@ -45962,8 +46208,9 @@ function makeAttendanceRegistry(opts) {
45962
46208
  }
45963
46209
  }
45964
46210
  },
45965
- answer(id, decision) {
45966
- resolveEntry(id, decision, "client");
46211
+ answer(id, decision, by) {
46212
+ const named = by ? identities.get(by)?.clientId : void 0;
46213
+ resolveEntry(id, decision, "client", named);
45967
46214
  },
45968
46215
  settle(id, decision, by) {
45969
46216
  resolveEntry(id, decision, by);
@@ -47361,6 +47608,7 @@ var FLAG_TO_KEY = {
47361
47608
  "--clone-under": "cloneUnder",
47362
47609
  "--allow-origin": "allowOrigins",
47363
47610
  "--allow-lan": "allowLan",
47611
+ "--mode": "teamMode",
47364
47612
  "--allow-shell": "allowShell",
47365
47613
  "--keep-transcripts": "keepTranscripts",
47366
47614
  "--isolation": "isolation",
@@ -47379,7 +47627,7 @@ var FLAG_TO_KEY = {
47379
47627
  "--default-model": "defaultModel",
47380
47628
  "--name": "name"
47381
47629
  };
47382
- var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.9.0";
47630
+ var DEFAULT_GUEST_IMAGE2 = "prompd/corenel-sandbox:0.10.0";
47383
47631
  function parseArgsWithProvenance(argv) {
47384
47632
  const explicit = /* @__PURE__ */ new Set();
47385
47633
  for (const a2 of argv) {
@@ -47396,6 +47644,7 @@ function parseArgs(argv) {
47396
47644
  allowOrigins: [],
47397
47645
  allowShell: false,
47398
47646
  allowLan: false,
47647
+ teamMode: false,
47399
47648
  keepTranscripts: false,
47400
47649
  isolation: "none",
47401
47650
  msbBin: process.env.MSB_BIN ?? "msb",
@@ -47450,7 +47699,11 @@ function parseArgs(argv) {
47450
47699
  throw new Error(`--snapshot-budget: ${e2 instanceof Error ? e2.message : String(e2)}`);
47451
47700
  }
47452
47701
  } else if (a2 === "--allow-lan") out.allowLan = true;
47453
- else if (a2 === "--tls-cert") out.tlsCert = resolve10(argv[++i2]);
47702
+ else if (a2 === "--mode") {
47703
+ const mode = argv[++i2];
47704
+ if (mode !== "team") throw new Error(`unknown --mode "${mode}". Known modes: team`);
47705
+ out.teamMode = true;
47706
+ } else if (a2 === "--tls-cert") out.tlsCert = resolve10(argv[++i2]);
47454
47707
  else if (a2 === "--tls-key") out.tlsKey = resolve10(argv[++i2]);
47455
47708
  else if (a2 === "--self-signed") out.selfSigned = true;
47456
47709
  else if (a2 === "--tunnel") out.tunnel = argv[++i2];
@@ -47572,6 +47825,17 @@ function layerConfig(args, explicit, file) {
47572
47825
  from[key] = "file";
47573
47826
  if (key === "endpoint") out.endpointExplicit = true;
47574
47827
  }
47828
+ if (explicit.has("teamMode")) {
47829
+ from.teamMode = "flag";
47830
+ } else if (file.mode === void 0) {
47831
+ from.teamMode = "default";
47832
+ } else if (file.mode === "team") {
47833
+ out.teamMode = true;
47834
+ from.teamMode = "file";
47835
+ } else {
47836
+ warnings.push(`daemon.json: "mode" must be "team", got ${JSON.stringify(file.mode)} -- ignoring it, using the default`);
47837
+ from.teamMode = "default";
47838
+ }
47575
47839
  if (out.allowLan) {
47576
47840
  if (from.host === "default") out.host = "0.0.0.0";
47577
47841
  if (from.selfSigned === "default" && !out.tlsCert && !out.tlsKey) out.selfSigned = true;
@@ -47753,6 +48017,11 @@ var KEY_TYPES = {
47753
48017
  allowShell: "boolean",
47754
48018
  keepTranscripts: "boolean",
47755
48019
  isolation: "string",
48020
+ // A string, not a boolean, so the file spells it the way the flag does
48021
+ // (`--mode team`) and a second mode needs no second key. layer.ts refuses
48022
+ // any value but 'team', with a warning -- typed loosely here, validated
48023
+ // strictly there, the same split `isolation` has.
48024
+ mode: "string",
47756
48025
  msbBin: "string",
47757
48026
  guestImage: "string",
47758
48027
  maxChainDepth: "number",
@@ -51244,7 +51513,7 @@ async function main() {
51244
51513
  // schedule looks broken for a quarter of an hour.
51245
51514
  onWrite: () => core?.wake()
51246
51515
  });
51247
- const attendance = makeAttendanceRegistry({ log: slog });
51516
+ const attendance = makeAttendanceRegistry({ log: slog, teamMode: args.teamMode });
51248
51517
  const runs = makeRunHub({ log: slog });
51249
51518
  let core;
51250
51519
  let crewSessions;
@@ -51560,6 +51829,10 @@ async function main() {
51560
51829
  // (roomCtx above) -- absent otherwise, so `welcome.features` omits
51561
51830
  // 'rooms' exactly like every other optional daemon capability here.
51562
51831
  rooms: core?.rooms,
51832
+ // Same operator flag `attendance.ts` already reads for ask routing --
51833
+ // `room-list` applies the identical private-by-default narrowing to
51834
+ // rooms once this is on (see `onRoomList`'s R7 comment).
51835
+ teamMode: args.teamMode,
51563
51836
  // The pause control plane. Absent without --daemon, so 'agent-pause' is not
51564
51837
  // advertised and a client correctly reports that this daemon holds none.
51565
51838
  ...pauses ? {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corenel/sidecar",
3
- "version": "0.9.0",
3
+ "version": "0.10.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,11 +36,11 @@
36
36
  "esbuild": "^0.21.5",
37
37
  "tsx": "^4.19.2",
38
38
  "typescript": "^5.9.3",
39
- "@corenel/config": "0.2.0",
40
- "@corenel/protocol": "0.8.0",
41
- "@corenel/tools-node": "0.5.0",
39
+ "@corenel/config": "0.3.0",
42
40
  "@corenel/mcp": "0.2.0",
43
- "@corenel/harness": "0.9.0",
41
+ "@corenel/protocol": "0.9.0",
42
+ "@corenel/harness": "0.10.0",
43
+ "@corenel/tools-node": "0.5.0",
44
44
  "@corenel/crew": "0.0.0"
45
45
  },
46
46
  "repository": {