negotium 0.1.13 → 0.1.14

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/main.js CHANGED
@@ -3324,7 +3324,7 @@ var init_claude_provider = __esm(async () => {
3324
3324
  });
3325
3325
 
3326
3326
  // ../../packages/core/src/version.ts
3327
- var NEGOTIUM_VERSION = "0.1.13";
3327
+ var NEGOTIUM_VERSION = "0.1.14";
3328
3328
 
3329
3329
  // ../../packages/core/src/agents/codex-native-multi-agent.ts
3330
3330
  import { spawn as spawn2 } from "child_process";
@@ -14799,6 +14799,10 @@ function getRuntimeProcessLease(role, now = Date.now(), staleMs = PROCESS_LEASE_
14799
14799
  const lease = rowToLease2(row);
14800
14800
  return now - lease.heartbeatAt <= staleMs ? lease : null;
14801
14801
  }
14802
+ function listRuntimeProcessLeases(rolePrefix = "", now = Date.now(), staleMs = PROCESS_LEASE_STALE_MS) {
14803
+ const rows = rolePrefix ? db.query("SELECT * FROM runtime_process_leases WHERE role LIKE ? ORDER BY role").all(`${rolePrefix}%`) : db.query("SELECT * FROM runtime_process_leases ORDER BY role").all();
14804
+ return rows.map(rowToLease2).filter((lease) => now - lease.heartbeatAt <= staleMs);
14805
+ }
14802
14806
  function heartbeatRuntimeProcessLease(role, ownerId, now = Date.now()) {
14803
14807
  const result = db.query(`UPDATE runtime_process_leases
14804
14808
  SET heartbeat_at = ?
@@ -16016,6 +16020,7 @@ __export(exports_src, {
16016
16020
  listVaultEntries: () => listVaultEntries,
16017
16021
  listTopics: () => listTopics,
16018
16022
  listRuntimeTurnLeases: () => listRuntimeTurnLeases,
16023
+ listRuntimeProcessLeases: () => listRuntimeProcessLeases,
16019
16024
  listRuntimeEventsAfter: () => listRuntimeEventsAfter,
16020
16025
  listRunningTopicQueries: () => listRunningTopicQueries,
16021
16026
  listRunningTopicIds: () => listRunningTopicIds,
@@ -17852,14 +17857,17 @@ function createNodeControlHandler(options) {
17852
17857
  const topic = topicForUser(topicId, userId);
17853
17858
  if (!topic)
17854
17859
  return jsonError(404, "Topic not found");
17855
- const { message } = submitUserMessage({
17860
+ const sourceAdapter = body.sourceAdapter === "telegram" || body.sourceAdapter === "otium" ? body.sourceAdapter : "terminal";
17861
+ const { message, queryId } = submitUserMessage({
17856
17862
  topic,
17857
17863
  userId,
17858
17864
  text: text2,
17859
- sourceAdapter: "terminal",
17865
+ sourceAdapter,
17866
+ visualTools: body.visualTools === true,
17867
+ fileDeliveryTools: body.fileDeliveryTools === true,
17860
17868
  startTurn: options.startTurn
17861
17869
  });
17862
- return Response.json({ ok: true, message }, { status: 201 });
17870
+ return Response.json({ ok: true, message, queryId }, { status: 201 });
17863
17871
  }
17864
17872
  const recentMatch = path.match(/^\/topics\/([^/]+)\/recent-events$/);
17865
17873
  if (recentMatch && req.method === "GET") {
@@ -20013,6 +20021,7 @@ async function startDefaultNode(opts = {}) {
20013
20021
  async function runNodeDaemon(opts = {}) {
20014
20022
  const node = await startDefaultNode({
20015
20023
  port: opts.port ?? 0,
20024
+ ...opts.maxRequestBodySize ? { maxRequestBodySize: opts.maxRequestBodySize } : {},
20016
20025
  advertise: true,
20017
20026
  singleton: true
20018
20027
  });
@@ -20629,7 +20638,7 @@ var MAX_CLIPBOARD_BYTES = 1e5;
20629
20638
 
20630
20639
  // ../../adapters/terminal/src/clock.ts
20631
20640
  function terminalNowMs() {
20632
- return performance.timeOrigin + performance.now();
20641
+ return Date.now();
20633
20642
  }
20634
20643
 
20635
20644
  // ../../adapters/terminal/src/commands.ts
@@ -25688,13 +25697,25 @@ Warning: enable the bot administrator permission "Manage Topics" before creating
25688
25697
  const rememberTarget = (queryId2) => {
25689
25698
  targetByQueryId.set(queryId2, target);
25690
25699
  };
25691
- const { queryId } = submitUserMessage({
25700
+ const input = {
25692
25701
  topic,
25693
25702
  userId,
25694
25703
  text: prompt,
25695
25704
  sourceAdapter: "telegram",
25696
25705
  visualTools: false,
25697
- fileDeliveryTools: true,
25706
+ fileDeliveryTools: true
25707
+ };
25708
+ if (opts.submitTurn) {
25709
+ opts.submitTurn(input).then(({ queryId: queryId2 }) => {
25710
+ if (queryId2)
25711
+ rememberTarget(queryId2);
25712
+ }).catch((error) => {
25713
+ logger.error({ err: error, topicId: topic.id }, "telegram adapter: remote turn failed");
25714
+ });
25715
+ return;
25716
+ }
25717
+ const { queryId } = submitUserMessage({
25718
+ ...input,
25698
25719
  onDispatched: rememberTarget,
25699
25720
  startTurn: dispatchTurn
25700
25721
  });
@@ -25883,7 +25904,8 @@ ${caption}` : voiceText : caption;
25883
25904
  }
25884
25905
  case "/abort": {
25885
25906
  const mapping = byKey.get(mappingKey(chatId, threadId));
25886
- reply(chatId, threadId, mapping && topicService.abortTurn(mapping.topicId, userId) ? "aborted" : "nothing running");
25907
+ const aborted = mapping ? await (opts.abortTurn?.(mapping.topicId, userId) ?? topicService.abortTurn(mapping.topicId, userId)) : false;
25908
+ reply(chatId, threadId, aborted ? "aborted" : "nothing running");
25887
25909
  return;
25888
25910
  }
25889
25911
  case "/agent": {
@@ -26260,7 +26282,7 @@ __export(exports_cli2, {
26260
26282
  runTelegramCli: () => runTelegramCli
26261
26283
  });
26262
26284
  import TelegramBot from "node-telegram-bot-api";
26263
- function startTelegramFromEnv() {
26285
+ function startTelegramFromEnv(options = {}) {
26264
26286
  const token = process.env.TELEGRAM_BOT_TOKEN?.trim();
26265
26287
  if (!token)
26266
26288
  throw new Error("TELEGRAM_BOT_TOKEN is required");
@@ -26268,6 +26290,7 @@ function startTelegramFromEnv() {
26268
26290
  const requestedAgent = process.env.TELEGRAM_DEFAULT_AGENT?.trim();
26269
26291
  const forumChatId = Number.parseInt(process.env.TELEGRAM_FORUM_CHAT_ID ?? "", 10);
26270
26292
  const adapter = startTelegramAdapter({
26293
+ ...options,
26271
26294
  client: bot,
26272
26295
  allowedUsers: (process.env.TELEGRAM_ALLOWED_USERS ?? "").split(",").map((value) => value.trim()).filter(Boolean),
26273
26296
  ...process.env.TELEGRAM_VAULT_OWNER_USER_ID?.trim() ? { vaultOwnerTelegramUserId: process.env.TELEGRAM_VAULT_OWNER_USER_ID.trim() } : {},
@@ -26288,38 +26311,101 @@ function startTelegramFromEnv() {
26288
26311
  }
26289
26312
  };
26290
26313
  }
26291
- async function runTelegramCli() {
26314
+ async function spawnCanonicalNode() {
26315
+ const entry = process.argv[1];
26316
+ if (!entry)
26317
+ throw new Error("cannot locate the Negotium CLI entrypoint");
26318
+ const child = Bun.spawn({
26319
+ cmd: [process.execPath, entry, "__node-daemon", "--port=0"],
26320
+ detached: true,
26321
+ env: { ...process.env, LOG_LEVEL: process.env.NEGOTIUM_NODE_LOG_LEVEL?.trim() || "info" },
26322
+ stdin: "ignore",
26323
+ stdout: "ignore",
26324
+ stderr: Bun.file(`${LOG_DIR}/node-daemon.log`)
26325
+ });
26326
+ child.unref();
26327
+ }
26328
+ async function ensureCanonicalNode() {
26329
+ const status = await inspectNodeDaemon();
26330
+ if (!status.running)
26331
+ await spawnCanonicalNode();
26332
+ return waitForNodeDaemon(15000);
26333
+ }
26334
+ async function runCanonicalNodeChild() {
26335
+ const { runNodeDaemon: runNodeDaemon2 } = await init_src5().then(() => exports_src3);
26336
+ await runNodeDaemon2({ port: 0 });
26337
+ }
26338
+ async function runTelegramCli(args = process.argv.slice(2)) {
26339
+ if (args[0] === "__node-daemon") {
26340
+ await runCanonicalNodeChild();
26341
+ return;
26342
+ }
26292
26343
  if (!process.env.TELEGRAM_BOT_TOKEN?.trim()) {
26293
26344
  throw new Error("TELEGRAM_BOT_TOKEN is required");
26294
26345
  }
26295
- let node;
26346
+ const initialNode = await ensureCanonicalNode();
26296
26347
  const singleton = await waitForRequiredRuntimeProcessLease("adapter:telegram", {
26297
26348
  workloadName: "Telegram adapter",
26298
26349
  onLost: () => {
26299
26350
  process.stderr.write(`negotium-telegram: singleton lease lost; shutting down
26300
26351
  `);
26301
- node?.stop();
26352
+ runShutdown("test");
26302
26353
  }
26303
26354
  });
26304
- try {
26305
- node = await startDefaultNode({ port: 0 });
26306
- } catch (error) {
26307
- singleton.stop();
26308
- throw error;
26309
- }
26310
26355
  let channel;
26311
26356
  try {
26312
- channel = startTelegramFromEnv();
26357
+ channel = startTelegramFromEnv({
26358
+ abortTurn: async (topicId, userId) => {
26359
+ const node = await waitForNodeDaemon(1500);
26360
+ const response = await fetch(`${node.baseUrl}${NODE_CONTROL_BASE_PATH}/topics/${encodeURIComponent(topicId)}/abort`, {
26361
+ method: "POST",
26362
+ headers: {
26363
+ authorization: `Bearer ${NODE_CONTROL_TOKEN}`,
26364
+ "content-type": "application/json"
26365
+ },
26366
+ body: JSON.stringify({ userId })
26367
+ });
26368
+ const body = await response.json();
26369
+ if (!response.ok)
26370
+ throw new Error(body.error ?? `node returned HTTP ${response.status}`);
26371
+ return body.aborted === true;
26372
+ },
26373
+ submitTurn: async (input) => {
26374
+ const node = await waitForNodeDaemon(1500);
26375
+ const response = await fetch(`${node.baseUrl}${NODE_CONTROL_BASE_PATH}/topics/${encodeURIComponent(input.topic.id)}/messages`, {
26376
+ method: "POST",
26377
+ headers: {
26378
+ authorization: `Bearer ${NODE_CONTROL_TOKEN}`,
26379
+ "content-type": "application/json"
26380
+ },
26381
+ body: JSON.stringify({
26382
+ userId: input.userId,
26383
+ text: input.text,
26384
+ sourceAdapter: input.sourceAdapter,
26385
+ visualTools: input.visualTools,
26386
+ fileDeliveryTools: input.fileDeliveryTools
26387
+ })
26388
+ });
26389
+ const body = await response.json();
26390
+ if (!response.ok)
26391
+ throw new Error(body.error ?? `node returned HTTP ${response.status}`);
26392
+ return { queryId: body.queryId };
26393
+ }
26394
+ });
26313
26395
  } catch (error) {
26314
- await node.stop();
26315
26396
  singleton.stop();
26316
26397
  throw error;
26317
26398
  }
26318
26399
  onShutdown("telegram-channel", 100, () => channel.stop());
26319
26400
  onShutdown("telegram-singleton", 90, () => singleton.stop());
26320
- process.stdout.write(`negotium Telegram adapter listening through node :${node.port}
26401
+ let resolveCompleted;
26402
+ const completed = new Promise((resolve13) => {
26403
+ resolveCompleted = resolve13;
26404
+ });
26405
+ onShutdown("telegram-completed", -100, resolveCompleted);
26406
+ process.stdout.write(`negotium Telegram adapter connected to canonical node pid ${initialNode.info?.pid ?? "unknown"}
26321
26407
  `);
26322
- await node.completed;
26408
+ await completed;
26323
26409
  }
26324
26410
  var init_cli2 = __esm(async () => {
26325
26411
  await init_src();
@@ -26328,37 +26414,6 @@ var init_cli2 = __esm(async () => {
26328
26414
  if (false) {}
26329
26415
  });
26330
26416
 
26331
- // ../../adapters/otium/src/protocol.ts
26332
- function str(body, field) {
26333
- const value = body[field];
26334
- return typeof value === "string" && value.trim() ? value : null;
26335
- }
26336
- function parseExecutionSpec(value) {
26337
- if (!value || typeof value !== "object" || Array.isArray(value))
26338
- return null;
26339
- const raw = value;
26340
- const agent = str(raw, "agent");
26341
- const model = str(raw, "model");
26342
- const effort = str(raw, "effort");
26343
- const rawMcp = raw.mcp;
26344
- if (!agent || !model || !effort || !Array.isArray(rawMcp) || !rawMcp.every((entry) => typeof entry === "string" && entry.trim().length > 0) || typeof raw.canSpawnSubagents !== "boolean") {
26345
- return null;
26346
- }
26347
- return {
26348
- agent,
26349
- model,
26350
- effort,
26351
- ...str(raw, "description") ? { description: str(raw, "description") } : {},
26352
- mcp: [...new Set(rawMcp.map((entry) => entry.trim()))],
26353
- canSpawnSubagents: raw.canSpawnSubagents
26354
- };
26355
- }
26356
- var PEER_PROTOCOL_VERSION = 1, MAX_PEER_MESSAGE_LENGTH = 1e4, MAX_PEER_INPUT_FILE_BYTES, MAX_PEER_INPUT_REQUEST_BYTES;
26357
- var init_protocol = __esm(() => {
26358
- MAX_PEER_INPUT_FILE_BYTES = 2 * 1024 * 1024 * 1024;
26359
- MAX_PEER_INPUT_REQUEST_BYTES = MAX_PEER_INPUT_FILE_BYTES + 8 * 1024 * 1024;
26360
- });
26361
-
26362
26417
  // ../../adapters/otium/src/central.ts
26363
26418
  var exports_central = {};
26364
26419
  __export(exports_central, {
@@ -26812,403 +26867,96 @@ var init_join = __esm(async () => {
26812
26867
  await init_src();
26813
26868
  });
26814
26869
 
26815
- // ../../adapters/otium/src/enrollment.ts
26816
- import {
26817
- createDecipheriv as createDecipheriv2,
26818
- createPrivateKey,
26819
- createPublicKey,
26820
- diffieHellman,
26821
- generateKeyPairSync,
26822
- hkdfSync,
26823
- randomUUID as randomUUID24
26824
- } from "crypto";
26825
- import {
26826
- chmodSync as chmodSync6,
26827
- closeSync as closeSync4,
26828
- existsSync as existsSync26,
26829
- fsyncSync as fsyncSync3,
26830
- linkSync as linkSync2,
26831
- mkdirSync as mkdirSync29,
26832
- openSync as openSync4,
26833
- readFileSync as readFileSync21,
26834
- renameSync as renameSync14,
26835
- unlinkSync as unlinkSync19,
26836
- writeFileSync as writeFileSync19
26837
- } from "fs";
26838
- import { dirname as dirname18, resolve as resolve14 } from "path";
26839
- function parseEnrollmentInvite(code) {
26840
- let parsed;
26841
- try {
26842
- parsed = JSON.parse(Buffer.from(code.trim(), "base64url").toString("utf8"));
26843
- } catch {
26844
- throw new Error("production invite must be base64url JSON");
26845
- }
26846
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
26847
- throw new Error("production invite must decode to an object");
26848
- }
26849
- const raw = parsed;
26850
- const central = typeof raw.central === "string" ? raw.central.trim().replace(/\/+$/, "") : "";
26851
- const token = typeof raw.token === "string" ? raw.token.trim() : "";
26852
- if (raw.v !== 2 || !central || !token.startsWith("nei_")) {
26853
- throw new Error("production invite requires {v:2, central:http(s), token:nei_\u2026}");
26870
+ // ../../adapters/otium/src/control-protocol.ts
26871
+ var OTIUM_ADAPTER_CONTROL_PREFIX = "/api/v1/adapter/otium", OTIUM_ADAPTER_CONTROL_HEADER = "x-negotium-adapter-token";
26872
+
26873
+ // ../../adapters/otium/src/protocol.ts
26874
+ function str(body, field) {
26875
+ const value = body[field];
26876
+ return typeof value === "string" && value.trim() ? value : null;
26877
+ }
26878
+ function parseExecutionSpec(value) {
26879
+ if (!value || typeof value !== "object" || Array.isArray(value))
26880
+ return null;
26881
+ const raw = value;
26882
+ const agent = str(raw, "agent");
26883
+ const model = str(raw, "model");
26884
+ const effort = str(raw, "effort");
26885
+ const rawMcp = raw.mcp;
26886
+ if (!agent || !model || !effort || !Array.isArray(rawMcp) || !rawMcp.every((entry) => typeof entry === "string" && entry.trim().length > 0) || typeof raw.canSpawnSubagents !== "boolean") {
26887
+ return null;
26854
26888
  }
26855
- assertSecureCentralUrl(central);
26856
- return { v: 2, central, token };
26889
+ return {
26890
+ agent,
26891
+ model,
26892
+ effort,
26893
+ ...str(raw, "description") ? { description: str(raw, "description") } : {},
26894
+ mcp: [...new Set(rawMcp.map((entry) => entry.trim()))],
26895
+ canSpawnSubagents: raw.canSpawnSubagents
26896
+ };
26857
26897
  }
26858
- function pendingEnrollmentPath() {
26859
- return resolve14(DATA_DIR, "otium-enrollment-pending.json");
26898
+ var PEER_PROTOCOL_VERSION = 1, MAX_PEER_MESSAGE_LENGTH = 1e4, MAX_PEER_INPUT_FILE_BYTES, MAX_PEER_INPUT_REQUEST_BYTES;
26899
+ var init_protocol = __esm(() => {
26900
+ MAX_PEER_INPUT_FILE_BYTES = 2 * 1024 * 1024 * 1024;
26901
+ MAX_PEER_INPUT_REQUEST_BYTES = MAX_PEER_INPUT_FILE_BYTES + 8 * 1024 * 1024;
26902
+ });
26903
+
26904
+ // ../../adapters/otium/src/canonical-mcp-bridge.ts
26905
+ import { randomUUID as randomUUID24 } from "crypto";
26906
+ function readAuthorization(request) {
26907
+ const value = request.headers.get("authorization");
26908
+ return value?.startsWith("Bearer ") ? value.slice(7) : null;
26860
26909
  }
26861
- function isEnrollmentPending(invite) {
26862
- const path = pendingEnrollmentPath();
26863
- if (!existsSync26(path))
26864
- return false;
26910
+ async function readLimitedJson(request) {
26911
+ const declared = Number(request.headers.get("content-length") ?? "0");
26912
+ if (Number.isFinite(declared) && declared > MAX_BODY_BYTES)
26913
+ return null;
26914
+ const reader = request.body?.getReader();
26915
+ if (!reader)
26916
+ return null;
26917
+ const chunks = [];
26918
+ let size = 0;
26919
+ const deadline = Date.now() + BODY_READ_TIMEOUT_MS;
26865
26920
  try {
26866
- const saved = JSON.parse(readFileSync21(path, "utf8"));
26867
- return saved.central === invite.central && saved.token === invite.token;
26921
+ while (true) {
26922
+ const remaining = deadline - Date.now();
26923
+ if (remaining <= 0)
26924
+ throw new Error("body read timeout");
26925
+ let timer;
26926
+ const { done, value } = await Promise.race([
26927
+ reader.read(),
26928
+ new Promise((_, reject) => {
26929
+ timer = setTimeout(() => reject(new Error("body read timeout")), remaining);
26930
+ })
26931
+ ]).finally(() => {
26932
+ if (timer)
26933
+ clearTimeout(timer);
26934
+ });
26935
+ if (done)
26936
+ break;
26937
+ size += value.byteLength;
26938
+ if (size > MAX_BODY_BYTES)
26939
+ throw new Error("body too large");
26940
+ chunks.push(value);
26941
+ }
26868
26942
  } catch {
26869
- return false;
26870
- }
26871
- }
26872
- function loadOrCreatePending(invite, nodeName) {
26873
- const path = pendingEnrollmentPath();
26874
- if (existsSync26(path)) {
26875
- const saved = JSON.parse(readFileSync21(path, "utf8"));
26876
- if (saved.central === invite.central && saved.token === invite.token)
26877
- return saved;
26878
- throw new Error(`another Otium enrollment is pending at ${path}`);
26943
+ await reader.cancel().catch(() => {
26944
+ return;
26945
+ });
26946
+ return null;
26879
26947
  }
26880
- const pair = generateKeyPairSync("x25519");
26881
- const pending2 = {
26882
- ...invite,
26883
- idempotencyKey: randomUUID24(),
26884
- privateKey: pair.privateKey.export({ format: "der", type: "pkcs8" }).toString("base64url"),
26885
- publicKey: pair.publicKey.export({ format: "der", type: "spki" }).toString("base64url"),
26886
- ...nodeName ? { nodeName } : {}
26887
- };
26888
- mkdirSync29(dirname18(path), { recursive: true });
26889
- const temporaryPath = resolve14(dirname18(path), `.otium-enrollment-pending.json.${process.pid}.${randomUUID24()}.tmp`);
26890
- let fd;
26891
26948
  try {
26892
- fd = openSync4(temporaryPath, "wx", 384);
26893
- writeFileSync19(fd, `${JSON.stringify(pending2, null, 2)}
26894
- `, "utf8");
26895
- fsyncSync3(fd);
26896
- closeSync4(fd);
26897
- fd = undefined;
26898
- linkSync2(temporaryPath, path);
26899
- unlinkSync19(temporaryPath);
26900
- chmodSync6(path, 384);
26901
- const directoryFd = openSync4(dirname18(path), "r");
26902
- try {
26903
- fsyncSync3(directoryFd);
26904
- } finally {
26905
- closeSync4(directoryFd);
26906
- }
26907
- } catch (error) {
26908
- if (fd !== undefined)
26909
- closeSync4(fd);
26910
- if (existsSync26(temporaryPath))
26911
- unlinkSync19(temporaryPath);
26912
- if (error.code === "EEXIST" && existsSync26(path)) {
26913
- const saved = JSON.parse(readFileSync21(path, "utf8"));
26914
- if (saved.central === invite.central && saved.token === invite.token)
26915
- return saved;
26916
- }
26917
- throw error;
26918
- }
26919
- return pending2;
26920
- }
26921
- function replacePendingEnrollment(pending2) {
26922
- const path = pendingEnrollmentPath();
26923
- const directory = dirname18(path);
26924
- const temporaryPath = resolve14(directory, `.otium-enrollment-pending.json.${process.pid}.${randomUUID24()}.tmp`);
26925
- let fd;
26926
- try {
26927
- fd = openSync4(temporaryPath, "wx", 384);
26928
- writeFileSync19(fd, `${JSON.stringify(pending2, null, 2)}
26929
- `, "utf8");
26930
- fsyncSync3(fd);
26931
- closeSync4(fd);
26932
- fd = undefined;
26933
- renameSync14(temporaryPath, path);
26934
- const directoryFd = openSync4(directory, "r");
26935
- try {
26936
- fsyncSync3(directoryFd);
26937
- } finally {
26938
- closeSync4(directoryFd);
26939
- }
26940
- } catch (error) {
26941
- if (fd !== undefined)
26942
- closeSync4(fd);
26943
- if (existsSync26(temporaryPath))
26944
- unlinkSync19(temporaryPath);
26945
- throw error;
26946
- }
26947
- }
26948
- function recordClaimedCredential(pending2, join31) {
26949
- withJoinCredentialLock(() => {
26950
- const current3 = JSON.parse(readFileSync21(pendingEnrollmentPath(), "utf8"));
26951
- if (current3.central !== pending2.central || current3.token !== pending2.token || current3.idempotencyKey !== pending2.idempotencyKey || current3.publicKey !== pending2.publicKey) {
26952
- throw new Error("pending Otium enrollment changed while its claim was in flight");
26953
- }
26954
- const claimed = { digest: joinCredentialDigest(join31), cellId: join31.cellId };
26955
- if (current3.claimed && (current3.claimed.digest !== claimed.digest || current3.claimed.cellId !== claimed.cellId)) {
26956
- throw new Error("central returned different credentials for an idempotent enrollment claim");
26957
- }
26958
- replacePendingEnrollment({ ...current3, claimed });
26959
- });
26960
- }
26961
- function openCredential(envelope, privateKeyDer) {
26962
- if (envelope.v !== 1 || envelope.algorithm !== "X25519-HKDF-SHA256-AES-256-GCM") {
26963
- throw new Error("unsupported enrollment credential envelope");
26964
- }
26965
- const privateKey = createPrivateKey({
26966
- key: Buffer.from(privateKeyDer, "base64url"),
26967
- format: "der",
26968
- type: "pkcs8"
26969
- });
26970
- const ephemeral = createPublicKey({
26971
- key: Buffer.from(envelope.ephemeralPublicKey, "base64url"),
26972
- format: "der",
26973
- type: "spki"
26974
- });
26975
- const shared = diffieHellman({ privateKey, publicKey: ephemeral });
26976
- const key = Buffer.from(hkdfSync("sha256", shared, Buffer.from(envelope.salt, "base64url"), INFO, 32));
26977
- const decipher = createDecipheriv2("aes-256-gcm", key, Buffer.from(envelope.nonce, "base64url"));
26978
- decipher.setAAD(INFO);
26979
- decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
26980
- return Buffer.concat([
26981
- decipher.update(Buffer.from(envelope.ciphertext, "base64url")),
26982
- decipher.final()
26983
- ]).toString("utf8");
26984
- }
26985
- async function postJson(url, body, headers) {
26986
- const response = await fetch(url, {
26987
- method: "POST",
26988
- headers: { "content-type": "application/json", ...headers },
26989
- body: JSON.stringify(body)
26990
- });
26991
- const result = await response.json();
26992
- if (!response.ok)
26993
- throw new Error(String(result.error || `request failed (${response.status})`));
26994
- return result;
26995
- }
26996
- async function previewEnrollment(invite) {
26997
- return postJson(`${invite.central}/api/v1/node-enrollments/preview`, {
26998
- token: invite.token
26999
- });
27000
- }
27001
- async function claimEnrollment(invite, nodeName) {
27002
- const pending2 = loadOrCreatePending(invite, nodeName);
27003
- const response = await postJson(`${invite.central}/api/v1/node-enrollments/claim`, {
27004
- token: invite.token,
27005
- credentialPublicKey: pending2.publicKey,
27006
- ...pending2.nodeName ? { nodeName: pending2.nodeName } : {}
27007
- }, { "idempotency-key": pending2.idempotencyKey });
27008
- const credential = response.credential;
27009
- const secret = openCredential(credential, pending2.privateKey);
27010
- if (!secret.startsWith("rcs_"))
27011
- throw new Error("central returned an invalid runtime credential");
27012
- const join31 = {
27013
- v: 2,
27014
- central: invite.central,
27015
- relay: String(response.relayUrl),
27016
- cellId: String(response.cell?.id),
27017
- secret
27018
- };
27019
- if (!join31.relay || !join31.cellId || join31.cellId === "undefined") {
27020
- throw new Error("central returned an incomplete enrollment response");
27021
- }
27022
- assertSecureRelayUrl(join31.relay);
27023
- recordClaimedCredential(pending2, join31);
27024
- return join31;
27025
- }
27026
- function commitEnrollment(join31, options = {}) {
27027
- return withJoinCredentialLock(() => {
27028
- const pendingPath = pendingEnrollmentPath();
27029
- const pending2 = existsSync26(pendingPath) ? JSON.parse(readFileSync21(pendingPath, "utf8")) : null;
27030
- const digest = joinCredentialDigest(join31);
27031
- if (pending2 && (!pending2.claimed || pending2.claimed.digest !== digest || pending2.claimed.cellId !== join31.cellId)) {
27032
- throw new Error(`pending Otium enrollment at ${pendingPath} does not match these credentials`);
27033
- }
27034
- const path = saveJoinWhileLocked(join31, options);
27035
- if (!isJoinPersisted(join31)) {
27036
- throw new Error("Otium join credentials were not durably persisted");
27037
- }
27038
- if (!pending2)
27039
- return path;
27040
- unlinkSync19(pendingPath);
27041
- const directoryFd = openSync4(dirname18(pendingPath), "r");
27042
- try {
27043
- fsyncSync3(directoryFd);
27044
- } finally {
27045
- closeSync4(directoryFd);
27046
- }
27047
- return path;
27048
- });
27049
- }
27050
- var INFO;
27051
- var init_enrollment = __esm(async () => {
27052
- await init_src();
27053
- await init_join();
27054
- INFO = Buffer.from("otium-node-enrollment-v1", "utf8");
27055
- });
27056
-
27057
- // ../../adapters/otium/src/join-cli.ts
27058
- var exports_join_cli = {};
27059
- __export(exports_join_cli, {
27060
- joinCommand: () => joinCommand
27061
- });
27062
- function option2(args, name) {
27063
- const index = args.indexOf(`--${name}`);
27064
- return index >= 0 ? args[index + 1]?.trim() : undefined;
27065
- }
27066
- async function confirmEnrollment(message) {
27067
- if (!process.stdin.isTTY)
27068
- return false;
27069
- const { createInterface } = await import("readline/promises");
27070
- const prompt = createInterface({ input: process.stdin, output: process.stdout });
27071
- try {
27072
- return /^(?:y|yes)$/i.test((await prompt.question(`${message} [y/N] `)).trim());
27073
- } finally {
27074
- prompt.close();
27075
- }
27076
- }
27077
- async function joinCommand(args) {
27078
- const code = args[0]?.trim();
27079
- if (!code) {
27080
- console.error("usage: negotium-otium join <invite-code> [--yes] [--name <node-name>] [--legacy] [--replace]");
27081
- process.exitCode = 1;
27082
- return;
27083
- }
27084
- let join31;
27085
- let productionEnrollment = false;
27086
- if (args.includes("--legacy")) {
27087
- try {
27088
- join31 = parseInviteCode(code);
27089
- } catch (err2) {
27090
- console.error(`invalid legacy invite code: ${err2 instanceof Error ? err2.message : err2}`);
27091
- process.exitCode = 1;
27092
- return;
27093
- }
27094
- } else {
27095
- try {
27096
- const invite = parseEnrollmentInvite(code);
27097
- const resuming = isEnrollmentPending(invite);
27098
- let nodeName = option2(args, "name");
27099
- if (resuming) {
27100
- console.log(`Resuming interrupted Otium enrollment with ${invite.central}`);
27101
- } else {
27102
- const preview = await previewEnrollment(invite);
27103
- const workspace = preview.preview?.workspace;
27104
- nodeName ||= preview.preview?.suggestedNodeName || undefined;
27105
- console.log(`Otium workspace: ${workspace?.name ?? workspace?.slug ?? workspace?.id}`);
27106
- console.log(` central: ${invite.central}`);
27107
- console.log(` transport: ${preview.preview?.transport ?? "relay"}`);
27108
- console.log(` access: ${preview.preview?.topics ?? "explicitly shared topics only"}`);
27109
- const accepted = args.includes("--yes") || await confirmEnrollment("Join this workspace?");
27110
- if (!accepted) {
27111
- console.log("enrollment cancelled");
27112
- return;
27113
- }
27114
- }
27115
- join31 = await claimEnrollment(invite, nodeName);
27116
- productionEnrollment = true;
27117
- } catch (err2) {
27118
- console.error(`enrollment failed: ${err2 instanceof Error ? err2.message : err2}`);
27119
- process.exitCode = 1;
27120
- return;
27121
- }
27122
- }
27123
- let path;
27124
- try {
27125
- const saveOptions = { replaceExisting: args.includes("--replace") };
27126
- path = productionEnrollment ? commitEnrollment(join31, saveOptions) : saveJoin(join31, saveOptions);
27127
- } catch (err2) {
27128
- console.error(`could not save join credentials: ${err2 instanceof Error ? err2.message : err2}`);
27129
- process.exitCode = 1;
27130
- return;
27131
- }
27132
- console.log(`otium join credentials saved to ${path}`);
27133
- console.log(` central: ${join31.central}`);
27134
- console.log(` cellId: ${join31.cellId}`);
27135
- configureOtiumCentral(join31);
27136
- try {
27137
- const self = await selfPeerNode();
27138
- if (self) {
27139
- console.log(`attached to workspace as "${self.nodeName ?? self.cellId}" (baseUrl ${self.baseUrl})`);
27140
- } else {
27141
- console.warn("warning: central answered but this cell has no visible assignment yet \u2014 check the workspace assignment");
27142
- }
27143
- } catch (err2) {
27144
- console.warn(`warning: could not verify against central (${err2 instanceof Error ? err2.message : err2}) \u2014 credentials saved anyway`);
27145
- } finally {
27146
- configureOtiumCentral(null);
27147
- }
27148
- console.log("\nnext: `negotium-otium serve` (mounts the otium peer routes automatically)");
27149
- }
27150
- var init_join_cli = __esm(async () => {
27151
- await init_central();
27152
- await init_enrollment();
27153
- await init_join();
27154
- });
27155
-
27156
- // ../../adapters/otium/src/canonical-mcp-bridge.ts
27157
- import { randomUUID as randomUUID25 } from "crypto";
27158
- function readAuthorization(request) {
27159
- const value = request.headers.get("authorization");
27160
- return value?.startsWith("Bearer ") ? value.slice(7) : null;
27161
- }
27162
- async function readLimitedJson(request) {
27163
- const declared = Number(request.headers.get("content-length") ?? "0");
27164
- if (Number.isFinite(declared) && declared > MAX_BODY_BYTES)
27165
- return null;
27166
- const reader = request.body?.getReader();
27167
- if (!reader)
27168
- return null;
27169
- const chunks = [];
27170
- let size = 0;
27171
- const deadline = Date.now() + BODY_READ_TIMEOUT_MS;
27172
- try {
27173
- while (true) {
27174
- const remaining = deadline - Date.now();
27175
- if (remaining <= 0)
27176
- throw new Error("body read timeout");
27177
- let timer;
27178
- const { done, value } = await Promise.race([
27179
- reader.read(),
27180
- new Promise((_, reject) => {
27181
- timer = setTimeout(() => reject(new Error("body read timeout")), remaining);
27182
- })
27183
- ]).finally(() => {
27184
- if (timer)
27185
- clearTimeout(timer);
27186
- });
27187
- if (done)
27188
- break;
27189
- size += value.byteLength;
27190
- if (size > MAX_BODY_BYTES)
27191
- throw new Error("body too large");
27192
- chunks.push(value);
27193
- }
27194
- } catch {
27195
- await reader.cancel().catch(() => {
27196
- return;
27197
- });
27198
- return null;
27199
- }
27200
- try {
27201
- const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
27202
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
27203
- return null;
27204
- const body = parsed;
27205
- if (typeof body.tool !== "string" || body.tool.length > 128)
27206
- return null;
27207
- if (!body.input || typeof body.input !== "object" || Array.isArray(body.input))
27208
- return null;
27209
- return { tool: body.tool, input: body.input };
27210
- } catch {
27211
- return null;
26949
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
26950
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed))
26951
+ return null;
26952
+ const body = parsed;
26953
+ if (typeof body.tool !== "string" || body.tool.length > 128)
26954
+ return null;
26955
+ if (!body.input || typeof body.input !== "object" || Array.isArray(body.input))
26956
+ return null;
26957
+ return { tool: body.tool, input: body.input };
26958
+ } catch {
26959
+ return null;
27212
26960
  }
27213
26961
  }
27214
26962
  async function forwardCanonicalTool(capability, request) {
@@ -27295,7 +27043,7 @@ function startCanonicalMcpBridge(options = {}) {
27295
27043
  const url = `http://127.0.0.1:${server.port}/`;
27296
27044
  const unregister = registerCanonicalMcpBridgeEnvProvider((scope) => {
27297
27045
  sweep();
27298
- const token = `${randomUUID25()}${randomUUID25()}`;
27046
+ const token = `${randomUUID24()}${randomUUID24()}`;
27299
27047
  capabilities.set(token, {
27300
27048
  surface: scope.surface,
27301
27049
  userId: scope.userId,
@@ -27844,7 +27592,7 @@ function createTurnForwarder(opts) {
27844
27592
  logger.warn({ requestId, seq, type: event.type, attempt, error: result.error }, "otium: peer event delivery to hub failed");
27845
27593
  if (attempt < PEER_EVENT_MAX_ATTEMPTS) {
27846
27594
  const delayMs = retryBaseMs * 2 ** (attempt - 1);
27847
- await new Promise((resolve15) => setTimeout(resolve15, delayMs));
27595
+ await new Promise((resolve14) => setTimeout(resolve14, delayMs));
27848
27596
  }
27849
27597
  }
27850
27598
  forwarder.deliveryBlocked = true;
@@ -27983,8 +27731,8 @@ var init_event_backflow = __esm(async () => {
27983
27731
  });
27984
27732
 
27985
27733
  // ../../adapters/otium/src/peer-files.ts
27986
- import { randomUUID as randomUUID26 } from "crypto";
27987
- import { copyFileSync as copyFileSync3, mkdirSync as mkdirSync30, rmSync as rmSync6, statSync as statSync12 } from "fs";
27734
+ import { randomUUID as randomUUID25 } from "crypto";
27735
+ import { copyFileSync as copyFileSync3, mkdirSync as mkdirSync29, rmSync as rmSync6, statSync as statSync12 } from "fs";
27988
27736
  import { basename as basename8, extname as extname5, join as join31 } from "path";
27989
27737
  function fileType(mimeType) {
27990
27738
  if (mimeType.startsWith("image/"))
@@ -28053,8 +27801,8 @@ function recordFile(args) {
28053
27801
  });
28054
27802
  }
28055
27803
  function insertLocalFile(args) {
28056
- const id = randomUUID26();
28057
- mkdirSync30(PEER_FILES_DIR, { recursive: true });
27804
+ const id = randomUUID25();
27805
+ mkdirSync29(PEER_FILES_DIR, { recursive: true });
28058
27806
  const path = join31(PEER_FILES_DIR, `${id}-${safeFilename(args.filename)}`);
28059
27807
  copyFileSync3(args.sourcePath, path);
28060
27808
  return recordFile({ id, path, sizeBytes: statSync12(path).size, ...args });
@@ -28066,8 +27814,8 @@ function deletePeerFilesForTopic(topicId) {
28066
27814
  rmSync6(row.path, { force: true });
28067
27815
  }
28068
27816
  async function storePeerInputFile(file, access) {
28069
- const id = randomUUID26();
28070
- mkdirSync30(PEER_FILES_DIR, { recursive: true });
27817
+ const id = randomUUID25();
27818
+ mkdirSync29(PEER_FILES_DIR, { recursive: true });
28071
27819
  const filename = file.name || "upload";
28072
27820
  const path = join31(PEER_FILES_DIR, `${id}-${safeFilename(filename)}`);
28073
27821
  const sizeBytes = await Bun.write(path, file);
@@ -28132,7 +27880,7 @@ var init_peer_files = __esm(async () => {
28132
27880
  });
28133
27881
 
28134
27882
  // ../../adapters/otium/src/runtime-bridge.ts
28135
- import { randomUUID as randomUUID27 } from "crypto";
27883
+ import { randomUUID as randomUUID26 } from "crypto";
28136
27884
  function isMcpToolResult(value) {
28137
27885
  if (!value || typeof value !== "object" || Array.isArray(value))
28138
27886
  return false;
@@ -28202,7 +27950,7 @@ var init_runtime_bridge = __esm(async () => {
28202
27950
  } catch (error) {
28203
27951
  return errorResult(`Error: Failed to open hub question: ${error.message}`);
28204
27952
  }
28205
- const bridgeRequestId = `bridge-ask-${randomUUID27()}`;
27953
+ const bridgeRequestId = `bridge-ask-${randomUUID26()}`;
28206
27954
  const baseUrl = hubNode.baseUrl.replace(/\/+$/, "");
28207
27955
  const post = async (path, body) => {
28208
27956
  const response = await fetch(`${baseUrl}${path}`, {
@@ -29418,6 +29166,248 @@ var init_bindings = __esm(async () => {
29418
29166
  await init_store2();
29419
29167
  });
29420
29168
 
29169
+ // ../../adapters/otium/src/enrollment.ts
29170
+ import {
29171
+ createDecipheriv as createDecipheriv2,
29172
+ createPrivateKey,
29173
+ createPublicKey,
29174
+ diffieHellman,
29175
+ generateKeyPairSync,
29176
+ hkdfSync,
29177
+ randomUUID as randomUUID27
29178
+ } from "crypto";
29179
+ import {
29180
+ chmodSync as chmodSync6,
29181
+ closeSync as closeSync4,
29182
+ existsSync as existsSync26,
29183
+ fsyncSync as fsyncSync3,
29184
+ linkSync as linkSync2,
29185
+ mkdirSync as mkdirSync30,
29186
+ openSync as openSync4,
29187
+ readFileSync as readFileSync21,
29188
+ renameSync as renameSync14,
29189
+ unlinkSync as unlinkSync19,
29190
+ writeFileSync as writeFileSync19
29191
+ } from "fs";
29192
+ import { dirname as dirname18, resolve as resolve14 } from "path";
29193
+ function parseEnrollmentInvite(code) {
29194
+ let parsed;
29195
+ try {
29196
+ parsed = JSON.parse(Buffer.from(code.trim(), "base64url").toString("utf8"));
29197
+ } catch {
29198
+ throw new Error("production invite must be base64url JSON");
29199
+ }
29200
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
29201
+ throw new Error("production invite must decode to an object");
29202
+ }
29203
+ const raw = parsed;
29204
+ const central = typeof raw.central === "string" ? raw.central.trim().replace(/\/+$/, "") : "";
29205
+ const token = typeof raw.token === "string" ? raw.token.trim() : "";
29206
+ if (raw.v !== 2 || !central || !token.startsWith("nei_")) {
29207
+ throw new Error("production invite requires {v:2, central:http(s), token:nei_\u2026}");
29208
+ }
29209
+ assertSecureCentralUrl(central);
29210
+ return { v: 2, central, token };
29211
+ }
29212
+ function pendingEnrollmentPath() {
29213
+ return resolve14(DATA_DIR, "otium-enrollment-pending.json");
29214
+ }
29215
+ function isEnrollmentPending(invite) {
29216
+ const path = pendingEnrollmentPath();
29217
+ if (!existsSync26(path))
29218
+ return false;
29219
+ try {
29220
+ const saved = JSON.parse(readFileSync21(path, "utf8"));
29221
+ return saved.central === invite.central && saved.token === invite.token;
29222
+ } catch {
29223
+ return false;
29224
+ }
29225
+ }
29226
+ function loadOrCreatePending(invite, nodeName) {
29227
+ const path = pendingEnrollmentPath();
29228
+ if (existsSync26(path)) {
29229
+ const saved = JSON.parse(readFileSync21(path, "utf8"));
29230
+ if (saved.central === invite.central && saved.token === invite.token)
29231
+ return saved;
29232
+ throw new Error(`another Otium enrollment is pending at ${path}`);
29233
+ }
29234
+ const pair = generateKeyPairSync("x25519");
29235
+ const pending2 = {
29236
+ ...invite,
29237
+ idempotencyKey: randomUUID27(),
29238
+ privateKey: pair.privateKey.export({ format: "der", type: "pkcs8" }).toString("base64url"),
29239
+ publicKey: pair.publicKey.export({ format: "der", type: "spki" }).toString("base64url"),
29240
+ ...nodeName ? { nodeName } : {}
29241
+ };
29242
+ mkdirSync30(dirname18(path), { recursive: true });
29243
+ const temporaryPath = resolve14(dirname18(path), `.otium-enrollment-pending.json.${process.pid}.${randomUUID27()}.tmp`);
29244
+ let fd;
29245
+ try {
29246
+ fd = openSync4(temporaryPath, "wx", 384);
29247
+ writeFileSync19(fd, `${JSON.stringify(pending2, null, 2)}
29248
+ `, "utf8");
29249
+ fsyncSync3(fd);
29250
+ closeSync4(fd);
29251
+ fd = undefined;
29252
+ linkSync2(temporaryPath, path);
29253
+ unlinkSync19(temporaryPath);
29254
+ chmodSync6(path, 384);
29255
+ const directoryFd = openSync4(dirname18(path), "r");
29256
+ try {
29257
+ fsyncSync3(directoryFd);
29258
+ } finally {
29259
+ closeSync4(directoryFd);
29260
+ }
29261
+ } catch (error) {
29262
+ if (fd !== undefined)
29263
+ closeSync4(fd);
29264
+ if (existsSync26(temporaryPath))
29265
+ unlinkSync19(temporaryPath);
29266
+ if (error.code === "EEXIST" && existsSync26(path)) {
29267
+ const saved = JSON.parse(readFileSync21(path, "utf8"));
29268
+ if (saved.central === invite.central && saved.token === invite.token)
29269
+ return saved;
29270
+ }
29271
+ throw error;
29272
+ }
29273
+ return pending2;
29274
+ }
29275
+ function replacePendingEnrollment(pending2) {
29276
+ const path = pendingEnrollmentPath();
29277
+ const directory = dirname18(path);
29278
+ const temporaryPath = resolve14(directory, `.otium-enrollment-pending.json.${process.pid}.${randomUUID27()}.tmp`);
29279
+ let fd;
29280
+ try {
29281
+ fd = openSync4(temporaryPath, "wx", 384);
29282
+ writeFileSync19(fd, `${JSON.stringify(pending2, null, 2)}
29283
+ `, "utf8");
29284
+ fsyncSync3(fd);
29285
+ closeSync4(fd);
29286
+ fd = undefined;
29287
+ renameSync14(temporaryPath, path);
29288
+ const directoryFd = openSync4(directory, "r");
29289
+ try {
29290
+ fsyncSync3(directoryFd);
29291
+ } finally {
29292
+ closeSync4(directoryFd);
29293
+ }
29294
+ } catch (error) {
29295
+ if (fd !== undefined)
29296
+ closeSync4(fd);
29297
+ if (existsSync26(temporaryPath))
29298
+ unlinkSync19(temporaryPath);
29299
+ throw error;
29300
+ }
29301
+ }
29302
+ function recordClaimedCredential(pending2, join32) {
29303
+ withJoinCredentialLock(() => {
29304
+ const current3 = JSON.parse(readFileSync21(pendingEnrollmentPath(), "utf8"));
29305
+ if (current3.central !== pending2.central || current3.token !== pending2.token || current3.idempotencyKey !== pending2.idempotencyKey || current3.publicKey !== pending2.publicKey) {
29306
+ throw new Error("pending Otium enrollment changed while its claim was in flight");
29307
+ }
29308
+ const claimed = { digest: joinCredentialDigest(join32), cellId: join32.cellId };
29309
+ if (current3.claimed && (current3.claimed.digest !== claimed.digest || current3.claimed.cellId !== claimed.cellId)) {
29310
+ throw new Error("central returned different credentials for an idempotent enrollment claim");
29311
+ }
29312
+ replacePendingEnrollment({ ...current3, claimed });
29313
+ });
29314
+ }
29315
+ function openCredential(envelope, privateKeyDer) {
29316
+ if (envelope.v !== 1 || envelope.algorithm !== "X25519-HKDF-SHA256-AES-256-GCM") {
29317
+ throw new Error("unsupported enrollment credential envelope");
29318
+ }
29319
+ const privateKey = createPrivateKey({
29320
+ key: Buffer.from(privateKeyDer, "base64url"),
29321
+ format: "der",
29322
+ type: "pkcs8"
29323
+ });
29324
+ const ephemeral = createPublicKey({
29325
+ key: Buffer.from(envelope.ephemeralPublicKey, "base64url"),
29326
+ format: "der",
29327
+ type: "spki"
29328
+ });
29329
+ const shared = diffieHellman({ privateKey, publicKey: ephemeral });
29330
+ const key = Buffer.from(hkdfSync("sha256", shared, Buffer.from(envelope.salt, "base64url"), INFO, 32));
29331
+ const decipher = createDecipheriv2("aes-256-gcm", key, Buffer.from(envelope.nonce, "base64url"));
29332
+ decipher.setAAD(INFO);
29333
+ decipher.setAuthTag(Buffer.from(envelope.tag, "base64url"));
29334
+ return Buffer.concat([
29335
+ decipher.update(Buffer.from(envelope.ciphertext, "base64url")),
29336
+ decipher.final()
29337
+ ]).toString("utf8");
29338
+ }
29339
+ async function postJson(url, body, headers) {
29340
+ const response = await fetch(url, {
29341
+ method: "POST",
29342
+ headers: { "content-type": "application/json", ...headers },
29343
+ body: JSON.stringify(body)
29344
+ });
29345
+ const result = await response.json();
29346
+ if (!response.ok)
29347
+ throw new Error(String(result.error || `request failed (${response.status})`));
29348
+ return result;
29349
+ }
29350
+ async function previewEnrollment(invite) {
29351
+ return postJson(`${invite.central}/api/v1/node-enrollments/preview`, {
29352
+ token: invite.token
29353
+ });
29354
+ }
29355
+ async function claimEnrollment(invite, nodeName) {
29356
+ const pending2 = loadOrCreatePending(invite, nodeName);
29357
+ const response = await postJson(`${invite.central}/api/v1/node-enrollments/claim`, {
29358
+ token: invite.token,
29359
+ credentialPublicKey: pending2.publicKey,
29360
+ ...pending2.nodeName ? { nodeName: pending2.nodeName } : {}
29361
+ }, { "idempotency-key": pending2.idempotencyKey });
29362
+ const credential = response.credential;
29363
+ const secret = openCredential(credential, pending2.privateKey);
29364
+ if (!secret.startsWith("rcs_"))
29365
+ throw new Error("central returned an invalid runtime credential");
29366
+ const join32 = {
29367
+ v: 2,
29368
+ central: invite.central,
29369
+ relay: String(response.relayUrl),
29370
+ cellId: String(response.cell?.id),
29371
+ secret
29372
+ };
29373
+ if (!join32.relay || !join32.cellId || join32.cellId === "undefined") {
29374
+ throw new Error("central returned an incomplete enrollment response");
29375
+ }
29376
+ assertSecureRelayUrl(join32.relay);
29377
+ recordClaimedCredential(pending2, join32);
29378
+ return join32;
29379
+ }
29380
+ function commitEnrollment(join32, options = {}) {
29381
+ return withJoinCredentialLock(() => {
29382
+ const pendingPath = pendingEnrollmentPath();
29383
+ const pending2 = existsSync26(pendingPath) ? JSON.parse(readFileSync21(pendingPath, "utf8")) : null;
29384
+ const digest = joinCredentialDigest(join32);
29385
+ if (pending2 && (!pending2.claimed || pending2.claimed.digest !== digest || pending2.claimed.cellId !== join32.cellId)) {
29386
+ throw new Error(`pending Otium enrollment at ${pendingPath} does not match these credentials`);
29387
+ }
29388
+ const path = saveJoinWhileLocked(join32, options);
29389
+ if (!isJoinPersisted(join32)) {
29390
+ throw new Error("Otium join credentials were not durably persisted");
29391
+ }
29392
+ if (!pending2)
29393
+ return path;
29394
+ unlinkSync19(pendingPath);
29395
+ const directoryFd = openSync4(dirname18(pendingPath), "r");
29396
+ try {
29397
+ fsyncSync3(directoryFd);
29398
+ } finally {
29399
+ closeSync4(directoryFd);
29400
+ }
29401
+ return path;
29402
+ });
29403
+ }
29404
+ var INFO;
29405
+ var init_enrollment = __esm(async () => {
29406
+ await init_src();
29407
+ await init_join();
29408
+ INFO = Buffer.from("otium-node-enrollment-v1", "utf8");
29409
+ });
29410
+
29421
29411
  // ../../adapters/otium/src/turn-bridge.ts
29422
29412
  import { randomUUID as randomUUID28 } from "crypto";
29423
29413
  function executionFor(payload) {
@@ -30205,67 +30195,20 @@ async function handleOtiumPeerRequest(req) {
30205
30195
  }
30206
30196
  }
30207
30197
  var RUNTIME_VERSION;
30208
- var init_peer_server = __esm(async () => {
30209
- await init_src();
30210
- await init_bindings();
30211
- await init_central();
30212
- await init_peer_files();
30213
- init_protocol();
30214
- await init_session_bridge();
30215
- await init_store2();
30216
- await init_turn_bridge();
30217
- RUNTIME_VERSION = NEGOTIUM_VERSION;
30218
- });
30219
-
30220
- // ../../adapters/otium/src/index.ts
30221
- var exports_src5 = {};
30222
- __export(exports_src5, {
30223
- verifyPeerToken: () => verifyPeerToken,
30224
- unbindOtiumTopic: () => unbindOtiumTopic,
30225
- translateBusEvent: () => translateBusEvent,
30226
- stopEventBackflow: () => stopEventBackflow,
30227
- startOtiumWorker: () => startOtiumWorker,
30228
- startOtiumAdapter: () => startOtiumAdapter,
30229
- startEventBackflow: () => startEventBackflow,
30230
- shareOtiumTopic: () => shareOtiumTopic,
30231
- setOtiumTopicPrivate: () => setOtiumTopicPrivate,
30232
- selfPeerNode: () => selfPeerNode,
30233
- saveJoin: () => saveJoin,
30234
- runPeerTurn: () => runPeerTurn,
30235
- resolvePeerNodeByCellId: () => resolvePeerNodeByCellId,
30236
- resetPeerCentralCaches: () => resetPeerCentralCaches,
30237
- registerTurnForwarder: () => registerTurnForwarder,
30238
- provisionMirrorTopic: () => provisionMirrorTopic,
30239
- previewEnrollment: () => previewEnrollment,
30240
- pendingEnrollmentPath: () => pendingEnrollmentPath,
30241
- parseInviteCode: () => parseInviteCode,
30242
- parseEnrollmentInvite: () => parseEnrollmentInvite,
30243
- otiumPeerRuntimeBridge: () => otiumPeerRuntimeBridge,
30244
- otiumCentralConfig: () => otiumCentralConfig,
30245
- otiumAdapter: () => otiumAdapter,
30246
- mintPeerToken: () => mintPeerToken,
30247
- loadJoin: () => loadJoin,
30248
- listPeerNodes: () => listPeerNodes,
30249
- listOtiumTopicBindings: () => listOtiumTopicBindings,
30250
- joinFilePath: () => joinFilePath,
30251
- isJoinPersisted: () => isJoinPersisted,
30252
- isEnrollmentPending: () => isEnrollmentPending,
30253
- hubEventSender: () => hubEventSender,
30254
- handleOtiumPeerRequest: () => handleOtiumPeerRequest,
30255
- getActiveForwarder: () => getActiveForwarder,
30256
- failInterruptedPeerTurnRequestsOnStartup: () => failInterruptedPeerTurnRequestsOnStartup,
30257
- createTurnForwarder: () => createTurnForwarder,
30258
- configureOtiumCentral: () => configureOtiumCentral,
30259
- commitEnrollment: () => commitEnrollment,
30260
- cleanupPeerStateForLocalTopic: () => cleanupPeerStateForLocalTopic,
30261
- claimEnrollment: () => claimEnrollment,
30262
- bindOtiumTopic: () => bindOtiumTopic,
30263
- abortHostedPeerTurn: () => abortHostedPeerTurn,
30264
- TunnelClient: () => TunnelClient,
30265
- RELAY_PROTOCOL_VERSION: () => PROTOCOL_VERSION,
30266
- PEER_PROTOCOL_VERSION: () => PEER_PROTOCOL_VERSION
30198
+ var init_peer_server = __esm(async () => {
30199
+ await init_src();
30200
+ await init_bindings();
30201
+ await init_central();
30202
+ await init_peer_files();
30203
+ init_protocol();
30204
+ await init_session_bridge();
30205
+ await init_store2();
30206
+ await init_turn_bridge();
30207
+ RUNTIME_VERSION = NEGOTIUM_VERSION;
30267
30208
  });
30268
- function startOtiumAdapter(options) {
30209
+
30210
+ // ../../adapters/otium/src/index.ts
30211
+ function startOtiumNodeRuntime(options) {
30269
30212
  const { join: join32 } = options;
30270
30213
  configureOtiumCentral(join32);
30271
30214
  const failed = failInterruptedPeerTurnRequestsOnStartup();
@@ -30293,7 +30236,6 @@ function startOtiumAdapter(options) {
30293
30236
  }
30294
30237
  });
30295
30238
  let stopped = false;
30296
- let tunnel = null;
30297
30239
  logger.info({ central: join32.central, cellId: join32.cellId }, "otium: worker mode enabled");
30298
30240
  selfPeerNode().then((self) => {
30299
30241
  if (self) {
@@ -30305,17 +30247,40 @@ function startOtiumAdapter(options) {
30305
30247
  return {
30306
30248
  name: "otium",
30307
30249
  join: join32,
30250
+ stop: () => {
30251
+ if (stopped)
30252
+ return;
30253
+ stopped = true;
30254
+ unsubscribeTopicCleanup();
30255
+ unregisterRuntimeBridge();
30256
+ unregisterSessionBridge();
30257
+ sessionBridgeIpc.stop();
30258
+ canonicalMcpBridge.stop();
30259
+ stopPeerReplyOutbox();
30260
+ uninstallFileHooks();
30261
+ stopBackflow();
30262
+ configureOtiumCentral(null);
30263
+ }
30264
+ };
30265
+ }
30266
+ function startOtiumAdapter(options) {
30267
+ const runtime = startOtiumNodeRuntime(options);
30268
+ let tunnel = null;
30269
+ let stopped = false;
30270
+ return {
30271
+ name: "otium",
30272
+ join: runtime.join,
30308
30273
  startTunnel: ({ targetOrigin, relayUrl }) => {
30309
30274
  if (stopped || tunnel)
30310
30275
  return;
30311
- const selectedRelay = relayUrl?.trim() || join32.relay || process.env.OTIUM_RELAY_URL?.trim();
30276
+ const selectedRelay = relayUrl?.trim() || runtime.join.relay || process.env.OTIUM_RELAY_URL?.trim();
30312
30277
  if (!selectedRelay) {
30313
30278
  logger.info({}, "otium: relay tunnel disabled (no relay URL configured)");
30314
30279
  return;
30315
30280
  }
30316
30281
  tunnel = new TunnelClient({
30317
30282
  relayUrl: selectedRelay,
30318
- token: join32.secret,
30283
+ token: runtime.join.secret,
30319
30284
  targetOrigin,
30320
30285
  nodeVersion: `negotium@${NEGOTIUM_VERSION}`,
30321
30286
  logger
@@ -30328,24 +30293,10 @@ function startOtiumAdapter(options) {
30328
30293
  stopped = true;
30329
30294
  tunnel?.stop();
30330
30295
  tunnel = null;
30331
- unsubscribeTopicCleanup();
30332
- unregisterRuntimeBridge();
30333
- unregisterSessionBridge();
30334
- sessionBridgeIpc.stop();
30335
- canonicalMcpBridge.stop();
30336
- stopPeerReplyOutbox();
30337
- uninstallFileHooks();
30338
- stopBackflow();
30339
- configureOtiumCentral(null);
30296
+ runtime.stop();
30340
30297
  }
30341
30298
  };
30342
30299
  }
30343
- function startOtiumWorker() {
30344
- const join32 = loadJoin();
30345
- if (!join32)
30346
- return null;
30347
- return startOtiumAdapter({ join: join32 });
30348
- }
30349
30300
  var otiumAdapter;
30350
30301
  var init_src8 = __esm(async () => {
30351
30302
  await init_src();
@@ -30387,6 +30338,245 @@ var init_src8 = __esm(async () => {
30387
30338
  });
30388
30339
  });
30389
30340
 
30341
+ // ../../adapters/otium/src/node-runtime.ts
30342
+ var exports_node_runtime = {};
30343
+ __export(exports_node_runtime, {
30344
+ mountConfiguredOtiumNodeRuntime: () => mountConfiguredOtiumNodeRuntime,
30345
+ handleOtiumAdapterControlRequest: () => handleOtiumAdapterControlRequest,
30346
+ OTIUM_ADAPTER_CONTROL_PREFIX: () => OTIUM_ADAPTER_CONTROL_PREFIX,
30347
+ OTIUM_ADAPTER_CONTROL_HEADER: () => OTIUM_ADAPTER_CONTROL_HEADER,
30348
+ MAX_PEER_INPUT_REQUEST_BYTES: () => MAX_PEER_INPUT_REQUEST_BYTES
30349
+ });
30350
+ async function handleOtiumAdapterControlRequest(req) {
30351
+ const url = new URL(req.url);
30352
+ if (!url.pathname.startsWith(`${OTIUM_ADAPTER_CONTROL_PREFIX}/`))
30353
+ return null;
30354
+ if (req.headers.get(OTIUM_ADAPTER_CONTROL_HEADER) !== NODE_CONTROL_TOKEN) {
30355
+ return Response.json({ ok: false, error: "Unauthorized" }, { status: 401 });
30356
+ }
30357
+ const peerPath = url.pathname.slice(OTIUM_ADAPTER_CONTROL_PREFIX.length) || "/";
30358
+ const peerUrl = new URL(req.url);
30359
+ peerUrl.pathname = peerPath;
30360
+ const headers = new Headers(req.headers);
30361
+ headers.delete(OTIUM_ADAPTER_CONTROL_HEADER);
30362
+ const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
30363
+ return await handleOtiumPeerRequest(new Request(peerUrl.toString(), { method: req.method, headers, body, signal: req.signal })) ?? Response.json({ ok: false, error: "Otium route not found" }, { status: 404 });
30364
+ }
30365
+ function mountConfiguredOtiumNodeRuntime() {
30366
+ const join32 = loadJoin();
30367
+ if (!join32)
30368
+ return null;
30369
+ const runtime = startOtiumNodeRuntime({ join: join32 });
30370
+ registerNodeRequestHandler("otium-adapter-control", handleOtiumAdapterControlRequest);
30371
+ let stopped = false;
30372
+ return {
30373
+ ...runtime,
30374
+ stop() {
30375
+ if (stopped)
30376
+ return;
30377
+ stopped = true;
30378
+ unregisterNodeRequestHandler("otium-adapter-control");
30379
+ runtime.stop();
30380
+ }
30381
+ };
30382
+ }
30383
+ var init_node_runtime = __esm(async () => {
30384
+ await init_src();
30385
+ await init_src8();
30386
+ await init_join();
30387
+ await init_peer_server();
30388
+ init_protocol();
30389
+ });
30390
+
30391
+ // ../../adapters/otium/src/join-cli.ts
30392
+ var exports_join_cli = {};
30393
+ __export(exports_join_cli, {
30394
+ joinCommand: () => joinCommand
30395
+ });
30396
+ function option2(args, name) {
30397
+ const index = args.indexOf(`--${name}`);
30398
+ return index >= 0 ? args[index + 1]?.trim() : undefined;
30399
+ }
30400
+ async function confirmEnrollment(message) {
30401
+ if (!process.stdin.isTTY)
30402
+ return false;
30403
+ const { createInterface } = await import("readline/promises");
30404
+ const prompt = createInterface({ input: process.stdin, output: process.stdout });
30405
+ try {
30406
+ return /^(?:y|yes)$/i.test((await prompt.question(`${message} [y/N] `)).trim());
30407
+ } finally {
30408
+ prompt.close();
30409
+ }
30410
+ }
30411
+ async function joinCommand(args) {
30412
+ const code = args[0]?.trim();
30413
+ if (!code) {
30414
+ console.error("usage: negotium-otium join <invite-code> [--yes] [--name <node-name>] [--legacy] [--replace]");
30415
+ process.exitCode = 1;
30416
+ return;
30417
+ }
30418
+ let join32;
30419
+ let productionEnrollment = false;
30420
+ if (args.includes("--legacy")) {
30421
+ try {
30422
+ join32 = parseInviteCode(code);
30423
+ } catch (err2) {
30424
+ console.error(`invalid legacy invite code: ${err2 instanceof Error ? err2.message : err2}`);
30425
+ process.exitCode = 1;
30426
+ return;
30427
+ }
30428
+ } else {
30429
+ try {
30430
+ const invite = parseEnrollmentInvite(code);
30431
+ const resuming = isEnrollmentPending(invite);
30432
+ let nodeName = option2(args, "name");
30433
+ if (resuming) {
30434
+ console.log(`Resuming interrupted Otium enrollment with ${invite.central}`);
30435
+ } else {
30436
+ const preview = await previewEnrollment(invite);
30437
+ const workspace = preview.preview?.workspace;
30438
+ nodeName ||= preview.preview?.suggestedNodeName || undefined;
30439
+ console.log(`Otium workspace: ${workspace?.name ?? workspace?.slug ?? workspace?.id}`);
30440
+ console.log(` central: ${invite.central}`);
30441
+ console.log(` transport: ${preview.preview?.transport ?? "relay"}`);
30442
+ console.log(` access: ${preview.preview?.topics ?? "explicitly shared topics only"}`);
30443
+ const accepted = args.includes("--yes") || await confirmEnrollment("Join this workspace?");
30444
+ if (!accepted) {
30445
+ console.log("enrollment cancelled");
30446
+ return;
30447
+ }
30448
+ }
30449
+ join32 = await claimEnrollment(invite, nodeName);
30450
+ productionEnrollment = true;
30451
+ } catch (err2) {
30452
+ console.error(`enrollment failed: ${err2 instanceof Error ? err2.message : err2}`);
30453
+ process.exitCode = 1;
30454
+ return;
30455
+ }
30456
+ }
30457
+ let path;
30458
+ try {
30459
+ const saveOptions = { replaceExisting: args.includes("--replace") };
30460
+ path = productionEnrollment ? commitEnrollment(join32, saveOptions) : saveJoin(join32, saveOptions);
30461
+ } catch (err2) {
30462
+ console.error(`could not save join credentials: ${err2 instanceof Error ? err2.message : err2}`);
30463
+ process.exitCode = 1;
30464
+ return;
30465
+ }
30466
+ console.log(`otium join credentials saved to ${path}`);
30467
+ console.log(` central: ${join32.central}`);
30468
+ console.log(` cellId: ${join32.cellId}`);
30469
+ configureOtiumCentral(join32);
30470
+ try {
30471
+ const self = await selfPeerNode();
30472
+ if (self) {
30473
+ console.log(`attached to workspace as "${self.nodeName ?? self.cellId}" (baseUrl ${self.baseUrl})`);
30474
+ } else {
30475
+ console.warn("warning: central answered but this cell has no visible assignment yet \u2014 check the workspace assignment");
30476
+ }
30477
+ } catch (err2) {
30478
+ console.warn(`warning: could not verify against central (${err2 instanceof Error ? err2.message : err2}) \u2014 credentials saved anyway`);
30479
+ } finally {
30480
+ configureOtiumCentral(null);
30481
+ }
30482
+ console.log("\nnext: `negotium-otium serve` (mounts the otium peer routes automatically)");
30483
+ }
30484
+ var init_join_cli = __esm(async () => {
30485
+ await init_central();
30486
+ await init_enrollment();
30487
+ await init_join();
30488
+ });
30489
+
30490
+ // ../../adapters/otium/src/sidecar.ts
30491
+ var exports_sidecar = {};
30492
+ __export(exports_sidecar, {
30493
+ runOtiumSidecar: () => runOtiumSidecar,
30494
+ proxyOtiumPeerRequest: () => proxyOtiumPeerRequest
30495
+ });
30496
+ async function proxyOtiumPeerRequest(req, dependencies = {}) {
30497
+ const inspectNode = dependencies.inspectNode ?? inspectNodeDaemon;
30498
+ const fetchRequest = dependencies.fetch ?? fetch;
30499
+ const status = await inspectNode();
30500
+ if (!status.running || !status.info) {
30501
+ return Response.json({ ok: false, error: "canonical Negotium node is unavailable" }, { status: 503 });
30502
+ }
30503
+ const source = new URL(req.url);
30504
+ const target = new URL(`http://127.0.0.1:${status.info.port}`);
30505
+ target.pathname = `${OTIUM_ADAPTER_CONTROL_PREFIX}${source.pathname}`;
30506
+ target.search = source.search;
30507
+ const headers = new Headers(req.headers);
30508
+ headers.set(OTIUM_ADAPTER_CONTROL_HEADER, NODE_CONTROL_TOKEN);
30509
+ try {
30510
+ const body = req.method === "GET" || req.method === "HEAD" ? undefined : await req.arrayBuffer();
30511
+ return await fetchRequest(new Request(target.toString(), { method: req.method, headers, body, signal: req.signal }));
30512
+ } catch (error) {
30513
+ logger.warn({ err: error }, "otium sidecar: canonical node request failed");
30514
+ return Response.json({ ok: false, error: "canonical Negotium node connection failed" }, { status: 503 });
30515
+ }
30516
+ }
30517
+ async function runOtiumSidecar(options) {
30518
+ const join32 = loadJoin();
30519
+ if (!join32) {
30520
+ throw new Error("not joined to an Otium workspace \u2014 run `negotium otium join <code>` first");
30521
+ }
30522
+ const initialNode = await inspectNodeDaemon();
30523
+ if (!initialNode.running) {
30524
+ throw new Error("canonical Negotium node is not running");
30525
+ }
30526
+ const ready = await proxyOtiumPeerRequest(new Request("http://127.0.0.1/ready"));
30527
+ if (!ready.ok) {
30528
+ throw new Error("canonical Negotium node has no Otium runtime; restart it after joining the workspace");
30529
+ }
30530
+ let server;
30531
+ const lease = await waitForRequiredRuntimeProcessLease("adapter:otium", {
30532
+ workloadName: "Otium adapter",
30533
+ onLost: () => {
30534
+ process.stderr.write(`negotium otium: singleton lease lost; shutting down
30535
+ `);
30536
+ runShutdown("test");
30537
+ }
30538
+ });
30539
+ try {
30540
+ server = Bun.serve({
30541
+ port: options.port,
30542
+ hostname: "127.0.0.1",
30543
+ idleTimeout: 240,
30544
+ maxRequestBodySize: MAX_PEER_INPUT_REQUEST_BYTES,
30545
+ fetch: (req) => proxyOtiumPeerRequest(req)
30546
+ });
30547
+ } catch (error) {
30548
+ lease.stop();
30549
+ throw error;
30550
+ }
30551
+ const selectedRelay = options.relayUrl?.trim() || join32.relay || process.env.OTIUM_RELAY_URL?.trim();
30552
+ const tunnel = selectedRelay ? new TunnelClient({
30553
+ relayUrl: selectedRelay,
30554
+ token: join32.secret,
30555
+ targetOrigin: `http://127.0.0.1:${server.port}`,
30556
+ nodeVersion: `negotium@${NEGOTIUM_VERSION}`,
30557
+ logger
30558
+ }) : null;
30559
+ tunnel?.start();
30560
+ let resolveCompleted;
30561
+ const completed = new Promise((resolve15) => {
30562
+ resolveCompleted = resolve15;
30563
+ });
30564
+ onShutdown("otium-sidecar-server", 130, () => server?.stop(true));
30565
+ onShutdown("otium-sidecar-tunnel", 120, () => tunnel?.stop());
30566
+ onShutdown("otium-sidecar-lease", 110, () => lease.stop());
30567
+ onShutdown("otium-sidecar-completed", -100, resolveCompleted);
30568
+ process.stdout.write(`negotium Otium adapter listening on 127.0.0.1:${server.port} (canonical node pid ${initialNode.info?.pid})
30569
+ `);
30570
+ await completed;
30571
+ }
30572
+ var init_sidecar = __esm(async () => {
30573
+ await init_src();
30574
+ await init_src5();
30575
+ await init_join();
30576
+ init_protocol();
30577
+ init_tunnel_client();
30578
+ });
30579
+
30390
30580
  // ../../adapters/otium/src/cli.ts
30391
30581
  var exports_cli3 = {};
30392
30582
  __export(exports_cli3, {
@@ -30464,66 +30654,57 @@ async function resolveHostNodeId(explicit) {
30464
30654
  configureOtiumCentral2(null);
30465
30655
  }
30466
30656
  }
30657
+ async function spawnCanonicalNode2() {
30658
+ const entry = process.argv[1];
30659
+ if (!entry)
30660
+ throw new Error("cannot locate the Negotium CLI entrypoint");
30661
+ const { LOG_DIR: LOG_DIR2 } = await init_src().then(() => exports_src);
30662
+ const child = Bun.spawn({
30663
+ cmd: [process.execPath, entry, "__node-daemon", "--port=0"],
30664
+ detached: true,
30665
+ env: { ...process.env, LOG_LEVEL: process.env.NEGOTIUM_NODE_LOG_LEVEL?.trim() || "info" },
30666
+ stdin: "ignore",
30667
+ stdout: "ignore",
30668
+ stderr: Bun.file(`${LOG_DIR2}/node-daemon.log`)
30669
+ });
30670
+ child.unref();
30671
+ }
30672
+ async function ensureCanonicalNode2() {
30673
+ const { inspectNodeDaemon: inspectNodeDaemon2, waitForNodeDaemon: waitForNodeDaemon2 } = await init_src5().then(() => exports_src3);
30674
+ const status = await inspectNodeDaemon2();
30675
+ if (status.running)
30676
+ return;
30677
+ await spawnCanonicalNode2();
30678
+ await waitForNodeDaemon2(15000);
30679
+ }
30680
+ async function runCanonicalNodeChild2() {
30681
+ const { onShutdown: onShutdown2 } = await init_src().then(() => exports_src);
30682
+ const { MAX_PEER_INPUT_REQUEST_BYTES: MAX_PEER_INPUT_REQUEST_BYTES2, mountConfiguredOtiumNodeRuntime: mountConfiguredOtiumNodeRuntime2 } = await init_node_runtime().then(() => exports_node_runtime);
30683
+ const { runNodeDaemon: runNodeDaemon2 } = await init_src5().then(() => exports_src3);
30684
+ const runtime = mountConfiguredOtiumNodeRuntime2();
30685
+ if (runtime)
30686
+ onShutdown2("otium-node-runtime", 125, () => runtime.stop());
30687
+ await runNodeDaemon2({ port: 0, maxRequestBodySize: MAX_PEER_INPUT_REQUEST_BYTES2 });
30688
+ }
30467
30689
  async function runOtiumCli(args = process.argv.slice(2)) {
30468
30690
  const [command, ...commandArgs] = args;
30469
30691
  switch (command) {
30692
+ case "__node-daemon": {
30693
+ await runCanonicalNodeChild2();
30694
+ break;
30695
+ }
30470
30696
  case "join": {
30471
30697
  const { joinCommand: joinCommand2 } = await init_join_cli().then(() => exports_join_cli);
30472
30698
  await joinCommand2(commandArgs);
30473
30699
  break;
30474
30700
  }
30475
30701
  case "serve": {
30476
- const {
30477
- NEGOTIUM_PORT: NEGOTIUM_PORT2,
30478
- onShutdown: onShutdown2,
30479
- registerNodeRequestHandler: registerNodeRequestHandler2,
30480
- unregisterNodeRequestHandler: unregisterNodeRequestHandler2,
30481
- waitForRequiredRuntimeProcessLease: waitForRequiredRuntimeProcessLease2
30482
- } = await init_src().then(() => exports_src);
30483
- const [{ startDefaultNode: startDefaultNode2 }, otium] = await Promise.all([
30484
- init_src5().then(() => exports_src3),
30485
- init_src8().then(() => exports_src5)
30486
- ]);
30702
+ const { NEGOTIUM_PORT: NEGOTIUM_PORT2 } = await init_src().then(() => exports_src);
30703
+ const { runOtiumSidecar: runOtiumSidecar2 } = await init_sidecar().then(() => exports_sidecar);
30487
30704
  const port = parseOtiumServePort(commandArgs, NEGOTIUM_PORT2);
30488
30705
  const relayUrl = parseOtiumServeRelayUrl(commandArgs);
30489
- let node;
30490
- const singleton = await waitForRequiredRuntimeProcessLease2("adapter:otium", {
30491
- workloadName: "Otium adapter",
30492
- onLost: () => {
30493
- console.error("negotium otium: singleton lease lost; shutting down");
30494
- node?.stop();
30495
- }
30496
- });
30497
- const { handleOtiumPeerRequest: handleOtiumPeerRequest2, startOtiumWorker: startOtiumWorker2 } = otium;
30498
- const worker = startOtiumWorker2();
30499
- if (!worker) {
30500
- singleton.stop();
30501
- throw new Error("not joined to an otium workspace \u2014 run `negotium otium join <code>` first");
30502
- }
30503
- registerNodeRequestHandler2("otium-peer", handleOtiumPeerRequest2);
30504
- let workerStopped = false;
30505
- const stopWorker = () => {
30506
- if (workerStopped)
30507
- return;
30508
- workerStopped = true;
30509
- unregisterNodeRequestHandler2("otium-peer");
30510
- worker.stop();
30511
- };
30512
- onShutdown2("otium-worker", 100, stopWorker);
30513
- onShutdown2("otium-singleton", 90, () => singleton.stop());
30514
- try {
30515
- node = await startDefaultNode2({
30516
- port,
30517
- maxRequestBodySize: MAX_PEER_INPUT_REQUEST_BYTES
30518
- });
30519
- } catch (error) {
30520
- stopWorker();
30521
- singleton.stop();
30522
- throw error;
30523
- }
30524
- console.log(`negotium node (otium worker) listening on 127.0.0.1:${node.port} (ctrl-c to stop)`);
30525
- worker.startTunnel({ targetOrigin: `http://127.0.0.1:${node.port}`, relayUrl });
30526
- await node.completed;
30706
+ await ensureCanonicalNode2();
30707
+ await runOtiumSidecar2({ port, relayUrl });
30527
30708
  break;
30528
30709
  }
30529
30710
  case "bindings": {
@@ -30590,7 +30771,6 @@ async function runOtiumCli(args = process.argv.slice(2)) {
30590
30771
  }
30591
30772
  }
30592
30773
  var init_cli3 = __esm(() => {
30593
- init_protocol();
30594
30774
  if (false) {}
30595
30775
  });
30596
30776
 
@@ -31304,6 +31484,36 @@ function numericOption(values, name, fallback) {
31304
31484
  const parsed = Number.parseInt(values.find((value) => value.startsWith(prefix))?.slice(prefix.length) ?? "", 10);
31305
31485
  return Number.isInteger(parsed) && parsed >= 0 ? parsed : fallback;
31306
31486
  }
31487
+ async function runCanonicalNode(port) {
31488
+ const { onShutdown: onShutdown2 } = await init_src().then(() => exports_src);
31489
+ const { MAX_PEER_INPUT_REQUEST_BYTES: MAX_PEER_INPUT_REQUEST_BYTES2, mountConfiguredOtiumNodeRuntime: mountConfiguredOtiumNodeRuntime2 } = await init_node_runtime().then(() => exports_node_runtime);
31490
+ const { startDefaultNode: startDefaultNode2 } = await init_src5().then(() => exports_src3);
31491
+ const otiumRuntime = mountConfiguredOtiumNodeRuntime2();
31492
+ if (otiumRuntime)
31493
+ onShutdown2("otium-node-runtime", 125, () => otiumRuntime.stop());
31494
+ const node = await startDefaultNode2({
31495
+ port,
31496
+ advertise: true,
31497
+ singleton: true,
31498
+ maxRequestBodySize: MAX_PEER_INPUT_REQUEST_BYTES2
31499
+ });
31500
+ console.log(`negotium node listening on 127.0.0.1:${node.port} (ctrl-c to stop)`);
31501
+ await node.completed;
31502
+ }
31503
+ async function stopAdapter(name) {
31504
+ const { getRuntimeProcessLease: getRuntimeProcessLease2 } = await init_src().then(() => exports_src);
31505
+ const lease = getRuntimeProcessLease2(`adapter:${name}`);
31506
+ if (!lease)
31507
+ return false;
31508
+ try {
31509
+ process.kill(lease.pid, "SIGTERM");
31510
+ return true;
31511
+ } catch (error) {
31512
+ if (error.code === "ESRCH")
31513
+ return false;
31514
+ throw error;
31515
+ }
31516
+ }
31307
31517
  switch (command) {
31308
31518
  case "init": {
31309
31519
  const { initCommand: initCommand2 } = await init_init().then(() => exports_init);
@@ -31316,22 +31526,20 @@ switch (command) {
31316
31526
  break;
31317
31527
  }
31318
31528
  case "serve": {
31319
- const { startDefaultNode: startDefaultNode2 } = await init_src5().then(() => exports_src3);
31320
- const node = await startDefaultNode2({
31321
- port: numericOption(args, "port", 7777),
31322
- advertise: true,
31323
- singleton: true
31324
- });
31325
- console.log(`negotium node listening on 127.0.0.1:${node.port} (ctrl-c to stop)`);
31326
- await node.completed;
31529
+ if (args[0] === "otium") {
31530
+ const { runOtiumCli: runOtiumCli2 } = await loadOtiumCli();
31531
+ await runOtiumCli2(["serve", ...args.slice(1)]);
31532
+ break;
31533
+ }
31534
+ await runCanonicalNode(numericOption(args, "port", 7777));
31327
31535
  break;
31328
31536
  }
31329
31537
  case "__node-daemon": {
31330
- const { runNodeDaemon: runNodeDaemon2 } = await init_src5().then(() => exports_src3);
31331
- await runNodeDaemon2({ port: numericOption(args, "port", 0) });
31538
+ await runCanonicalNode(numericOption(args, "port", 0));
31332
31539
  break;
31333
31540
  }
31334
31541
  case "status": {
31542
+ const { listRuntimeProcessLeases: listRuntimeProcessLeases2 } = await init_src().then(() => exports_src);
31335
31543
  const { inspectNodeDaemon: inspectNodeDaemon2 } = await init_src5().then(() => exports_src3);
31336
31544
  const status = await inspectNodeDaemon2();
31337
31545
  if (status.running && status.info) {
@@ -31342,10 +31550,33 @@ switch (command) {
31342
31550
  } else {
31343
31551
  console.log("negotium node is stopped");
31344
31552
  }
31553
+ const adapters = listRuntimeProcessLeases2("adapter:");
31554
+ if (adapters.length === 0)
31555
+ console.log("adapters: none");
31556
+ else {
31557
+ for (const adapter of adapters) {
31558
+ console.log(`adapter ${adapter.role.slice("adapter:".length)} running (pid ${adapter.pid}, since ${new Date(adapter.startedAt).toISOString()})`);
31559
+ }
31560
+ }
31345
31561
  break;
31346
31562
  }
31347
31563
  case "stop": {
31348
31564
  const { stopNodeDaemon: stopNodeDaemon2 } = await init_src5().then(() => exports_src3);
31565
+ const target = args[0];
31566
+ if (target === "otium" || target === "telegram") {
31567
+ const stopped2 = await stopAdapter(target);
31568
+ console.log(stopped2 ? `${target} adapter shutdown requested` : `${target} adapter is not running`);
31569
+ break;
31570
+ }
31571
+ if (target && target !== "--all") {
31572
+ throw new Error("usage: negotium stop [otium|telegram|--all]");
31573
+ }
31574
+ if (target === "--all") {
31575
+ for (const name of ["otium", "telegram"]) {
31576
+ if (await stopAdapter(name))
31577
+ console.log(`${name} adapter shutdown requested`);
31578
+ }
31579
+ }
31349
31580
  const stopped = await stopNodeDaemon2();
31350
31581
  console.log(stopped ? "negotium node shutdown requested" : "negotium node is not running");
31351
31582
  break;
@@ -31377,11 +31608,14 @@ switch (command) {
31377
31608
  }
31378
31609
  case "telegram": {
31379
31610
  const { runTelegramCli: runTelegramCli2 } = await loadTelegramCli();
31380
- await runTelegramCli2();
31611
+ await runTelegramCli2(args);
31381
31612
  break;
31382
31613
  }
31383
31614
  case "otium": {
31384
31615
  const { runOtiumCli: runOtiumCli2 } = await loadOtiumCli();
31616
+ if (args[0] === "serve") {
31617
+ process.stderr.write("warning: `negotium otium serve` is deprecated; use `negotium serve otium`\n");
31618
+ }
31385
31619
  await runOtiumCli2(args);
31386
31620
  break;
31387
31621
  }
@@ -31400,9 +31634,10 @@ switch (command) {
31400
31634
  " init bootstrap ~/.negotium and check agent auth",
31401
31635
  " chat [topic] interactive chat (creates the topic if missing)",
31402
31636
  " options: --agent=claude|codex|maestro",
31403
- " serve foreground long-lived node (default port 7777)",
31404
- " status show the local long-lived node status",
31405
- " stop stop the local long-lived node",
31637
+ " serve foreground canonical node (default port 7777)",
31638
+ " serve otium ensure the canonical node and run the Otium sidecar",
31639
+ " status show the canonical node and adapter processes",
31640
+ " stop [otium|telegram|--all] stop the node, one adapter, or everything",
31406
31641
  " topics list topics on this node",
31407
31642
  " mcp list|add|remove|enable|disable manage node MCP manifest",
31408
31643
  " vault list|set|get|del node secret store (encrypted at rest)",
@@ -31420,4 +31655,4 @@ switch (command) {
31420
31655
  }
31421
31656
  }
31422
31657
 
31423
- //# debugId=4DB206AEE32F96CD64756E2164756E21
31658
+ //# debugId=F87B7DCB5931FEC964756E2164756E21