@volter/supercode-teams 0.2.103 → 0.2.105

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/machine.mjs +38 -5
  2. package/package.json +1 -1
package/machine.mjs CHANGED
@@ -582,6 +582,39 @@ export class TeamsMachine extends MachineCore {
582
582
  * address) on a pane another session, a board or a person started is refused, naming who started it. A person (no
583
583
  * caller) and a board (its cards' own door) act on any pane.
584
584
  */
585
+ /**
586
+ * Who is calling, for the pane doors. On the local socket the daemon decides: it hands the connection to its own
587
+ * supercode (`teams caller --peer-fd 3`), which reads the peer's pid from the kernel and resolves that process's
588
+ * session from its ancestry and its own environment, so nothing the client sends about itself is believed. Asked
589
+ * once per connection. A request the Teams connector forwards from another machine keeps the caller it carries,
590
+ * as does a platform with no peer credentials (Windows named pipes). A local peer nothing can resolve is refused.
591
+ */
592
+ #callerOf(entry, params) {
593
+ const named = typeof params.caller === 'string' && params.caller ? params.caller : null;
594
+ if (entry.principal !== 'local' || !entry.socket) return Promise.resolve(named);
595
+ entry.peerCaller ??= new Promise((resolve) => {
596
+ let out = '';
597
+ let child;
598
+ try {
599
+ // The descriptor's number, not the socket: a stream handed to spawn stops being read here, and the
600
+ // connection's next request would never be answered. The child gets its own duplicate of the descriptor.
601
+ const fd = entry.socket._handle?.fd;
602
+ if (!Number.isInteger(fd) || fd < 0) { resolve({ unsupported: true }); return; }
603
+ child = spawn(this.supercodeBin, ['teams', 'caller', '--peer-fd', '3'], { stdio: ['ignore', 'pipe', 'ignore', fd], env: this.env, windowsHide: true });
604
+ } catch { resolve({ unknown: true }); return; }
605
+ child.stdout.on('data', (chunk) => { out += chunk; });
606
+ child.on('error', () => resolve({ unknown: true }));
607
+ child.on('close', () => { try { resolve(JSON.parse(out.trim().split('\n').pop())); } catch { resolve({ unknown: true }); } });
608
+ });
609
+ return entry.peerCaller.then((answer) => {
610
+ // No peer credentials here, or a supercode older than the question (it answers for itself, with no pid): the
611
+ // caller the client names, as before.
612
+ if (answer.unsupported || (answer.caller !== undefined && !('pid' in answer))) return named;
613
+ if (answer.unknown || !('caller' in answer)) throw Object.assign(new Error('cannot tell which process is calling, so the pane doors cannot hold it to the panes it started; nothing was done'), { code: 403 });
614
+ return answer.caller ?? null;
615
+ });
616
+ }
617
+
585
618
  #refuseForeign(pane, caller) {
586
619
  if (typeof caller !== 'string' || !caller || /^sc:[^:]+:board:/.test(caller)) return;
587
620
  const by = this.launches.all().find((entry) => entry.pane === pane)?.by ?? null;
@@ -1155,7 +1188,7 @@ export class TeamsMachine extends MachineCore {
1155
1188
  const contextKey = params.pane ?? `p_${randomBytes(9).toString('base64url')}`;
1156
1189
  // Who started the pane: the launching session's (or board's) address, null for a person. A session's later
1157
1190
  // input and close on the pane are refused unless it is this one (#refuseForeign).
1158
- const by = typeof params.caller === 'string' && params.caller ? params.caller : null;
1191
+ const by = await this.#callerOf(entry, params).catch(() => null);
1159
1192
  // Who to tell when the pane ends (`open --new … --notify-when-ended`): a session's mail address, and this
1160
1193
  // machine's name as the address a notice from its pane comes from.
1161
1194
  if (params.notify !== undefined && !(params.notify && typeof params.notify.to === 'string' && /^sc:[^:\s]+:[^:\s]+:\S+$/.test(params.notify.to) && typeof params.notify.machine === 'string' && /^[a-z0-9][a-z0-9-]*$/.test(params.notify.machine))) throw Object.assign(new Error('launch notify is { to: <sc: address>, machine: <this machine\'s mail name> }'), { code: 400 });
@@ -1233,7 +1266,7 @@ export class TeamsMachine extends MachineCore {
1233
1266
  }
1234
1267
  case 'panes.close': {
1235
1268
  need(operator ? 'control' : 'control', `pane:${params.pane}`);
1236
- this.#refuseForeign(params.pane, params.caller);
1269
+ this.#refuseForeign(params.pane, await this.#callerOf(entry, params));
1237
1270
  const session = await this.#paneByContext(params.pane);
1238
1271
  if (!session) throw new Error(`no pane with context ${params.pane}`);
1239
1272
  await this.host.closeSession(session.id);
@@ -1246,7 +1279,7 @@ export class TeamsMachine extends MachineCore {
1246
1279
  // working, only `{input}` is refused — until `resume`, which may carry
1247
1280
  // the input that hands the pane back. Local tmux input is never touched.
1248
1281
  need('control', `pane:${params.pane}`);
1249
- this.#refuseForeign(params.pane, params.caller);
1282
+ this.#refuseForeign(params.pane, await this.#callerOf(entry, params));
1250
1283
  const session = await this.#paneByContext(params.pane);
1251
1284
  if (!session) throw new Error(`no pane with context ${params.pane}`);
1252
1285
  const doc = readJson(this.p.suspended, { v: 1, panes: {} });
@@ -1268,7 +1301,7 @@ export class TeamsMachine extends MachineCore {
1268
1301
  // mechanics) for callers that are services rather than terminals. Same gate as an
1269
1302
  // attachment in control mode, same suspend rule as remote input everywhere.
1270
1303
  need('control', `pane:${params.pane}`);
1271
- this.#refuseForeign(params.pane, params.caller);
1304
+ this.#refuseForeign(params.pane, await this.#callerOf(entry, params));
1272
1305
  const session = await this.#paneByContext(params.pane);
1273
1306
  if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
1274
1307
  if (this.suspended.has(params.pane)) throw Object.assign(new Error(`pane ${params.pane} is suspended: remote input is refused until resume`), { code: 423 });
@@ -1287,7 +1320,7 @@ export class TeamsMachine extends MachineCore {
1287
1320
  // answer, not an error: the caller keeps the turn and asks again. Its own
1288
1321
  // method, so a daemon without it refuses instead of typing into a draft.
1289
1322
  need('control', `pane:${params.pane}`);
1290
- this.#refuseForeign(params.pane, params.caller);
1323
+ this.#refuseForeign(params.pane, await this.#callerOf(entry, params));
1291
1324
  const session = await this.#paneByContext(params.pane);
1292
1325
  if (!session) throw Object.assign(new Error(`no pane with context ${params.pane}`), { code: 404 });
1293
1326
  if (this.suspended.has(params.pane)) throw Object.assign(new Error(`pane ${params.pane} is suspended: remote input is refused until resume`), { code: 423 });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volter/supercode-teams",
3
- "version": "0.2.103",
3
+ "version": "0.2.105",
4
4
  "type": "module",
5
5
  "description": "A machine daemon serving this machine's doors, and a local or remote Teams server for shared session discovery, OpenTelemetry ingestion and access to enrolled machines.",
6
6
  "license": "MIT",