@agentproto/runtime 0.5.0 → 0.6.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.
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
- import { randomBytes, randomUUID, createHash } from 'crypto';
1
+ import { randomBytes, randomUUID, createHash, createHmac, timingSafeEqual } from 'crypto';
2
2
  import { existsSync, mkdirSync, writeFileSync, renameSync, readFileSync, promises, createReadStream, createWriteStream, openSync, closeSync } from 'fs';
3
- import { mkdir, writeFile, unlink, readdir, readFile, stat, appendFile, chmod, rm, mkdtemp, access } from 'fs/promises';
3
+ import { mkdir, writeFile, unlink, readdir, readFile, stat, chmod, rename, appendFile, rm, mkdtemp, access } from 'fs/promises';
4
4
  import { resolve, join, dirname, isAbsolute, normalize, relative, basename } from 'path';
5
5
  import { createMcpServer } from '@agentproto/mcp-server';
6
6
  import { homedir, tmpdir } from 'os';
@@ -8,9 +8,10 @@ import { execFile, spawn } from 'child_process';
8
8
  import { z } from 'zod';
9
9
  import { EventEmitter } from 'events';
10
10
  import { createInterface } from 'readline';
11
+ import { getModelProvider, resolvePricing } from '@agentproto/model-catalog/llm';
12
+ import * as providers_store_star from '@agentproto/providers-store';
11
13
  import { makeAdapterLister, makeAdapterResolver, makeCredsStore, discoverAdapterPackages, makeSetupLedger, makeListTool, makeSetupTool } from '@agentproto/provider-kit';
12
14
  import { SandboxSpecSchema, resolveLifecyclePolicy, createSandboxAgentSessionHost } from '@agentproto/sandbox';
13
- import { resolvePricing } from '@agentproto/model-catalog/llm';
14
15
  import matter from 'gray-matter';
15
16
  import { createServer } from 'http';
16
17
  import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
@@ -29,12 +30,27 @@ import { ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
29
30
  import { Cron } from 'croner';
30
31
  import { promisify } from 'util';
31
32
  import { createServer as createServer$1 } from 'net';
33
+ import { daemonHandshakeOverSink } from '@agentproto/acp/tunnel';
34
+ import { PairingError, encodeOfferUrl, HOSTED_RENDEZVOUS_URL, decodePairingHello, respondToHandshake, encodePairingMessage, derivePairRoot, deriveEpochRoutingToken, currentEpoch } from '@agentproto/secrets/pairing';
35
+ import { identityFingerprint } from '@agentproto/secrets/identity';
32
36
 
33
37
  /**
34
38
  * @agentproto/runtime v0.1.0-alpha
35
39
  * Long-running gateway: MCP server + HTTP transport + HEARTBEAT autonomy + conversation persistence over a workspace dir.
36
40
  */
37
-
41
+ var __defProp = Object.defineProperty;
42
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
43
+ var __getOwnPropNames = Object.getOwnPropertyNames;
44
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
45
+ var __copyProps = (to, from, except, desc) => {
46
+ if (from && typeof from === "object" || typeof from === "function") {
47
+ for (let key of __getOwnPropNames(from))
48
+ if (!__hasOwnProp.call(to, key) && key !== except)
49
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
50
+ }
51
+ return to;
52
+ };
53
+ var __reExport = (target, mod, secondTarget) => (__copyProps(target, mod, "default"), secondTarget);
38
54
  async function writeRuntimeMeta(workspace, meta) {
39
55
  const dir = join(workspace, ".agentproto");
40
56
  try {
@@ -317,12 +333,12 @@ async function runCommand(input) {
317
333
  return buf;
318
334
  }
319
335
  const room = STREAM_BUFFER_CAP - buf.length;
320
- const text5 = chunk.toString("utf8");
321
- if (text5.length > room) {
336
+ const text6 = chunk.toString("utf8");
337
+ if (text6.length > room) {
322
338
  truncated = true;
323
- return buf + text5.slice(0, room);
339
+ return buf + text6.slice(0, room);
324
340
  }
325
- return buf + text5;
341
+ return buf + text6;
326
342
  }
327
343
  child.stdout?.on("data", (d) => {
328
344
  stdout = appendCapped(stdout, d);
@@ -727,14 +743,14 @@ function extractText(value) {
727
743
  return null;
728
744
  }
729
745
  function formatToolResult(toolName, result, isError) {
730
- const text5 = extractText(result);
746
+ const text6 = extractText(result);
731
747
  if (isError) {
732
- const message = text5 ?? (result != null ? JSON.stringify(result) : "failed");
748
+ const message = text6 ?? (result != null ? JSON.stringify(result) : "failed");
733
749
  const firstLine = message.split(/\r?\n/)[0] ?? message;
734
750
  return truncate(firstLine, MAX_RESULT_LENGTH);
735
751
  }
736
- if (text5 == null) return null;
737
- const trimmed = text5.trim();
752
+ if (text6 == null) return null;
753
+ const trimmed = text6.trim();
738
754
  if (!trimmed) return null;
739
755
  const lines = trimmed.split(/\r?\n/);
740
756
  if (lines.length > 1) {
@@ -750,20 +766,42 @@ function sessionTranscriptDir(sessionId, baseDir) {
750
766
  function sessionEventsPath(sessionId, baseDir) {
751
767
  return join(sessionTranscriptDir(sessionId, baseDir), "events.jsonl");
752
768
  }
769
+ function highestSeqOnDisk(path) {
770
+ let raw;
771
+ try {
772
+ raw = readFileSync(path, "utf8");
773
+ } catch {
774
+ return 0;
775
+ }
776
+ let max = 0;
777
+ for (const line of raw.split("\n")) {
778
+ const trimmed = line.trim();
779
+ if (!trimmed) continue;
780
+ try {
781
+ const rec = JSON.parse(trimmed);
782
+ if (typeof rec.seq === "number" && rec.seq > max) max = rec.seq;
783
+ } catch {
784
+ }
785
+ }
786
+ return max;
787
+ }
753
788
  function createTranscriptWriter(opts) {
754
789
  const baseDir = opts?.baseDir;
755
790
  const states = /* @__PURE__ */ new Map();
791
+ const listeners = /* @__PURE__ */ new Map();
756
792
  const getState = (sessionId) => {
757
793
  const existing = states.get(sessionId);
758
794
  if (existing) return existing;
759
795
  mkdirSync(sessionTranscriptDir(sessionId, baseDir), { recursive: true });
760
- const stream = createWriteStream(sessionEventsPath(sessionId, baseDir), { flags: "a" });
796
+ const eventsPath = sessionEventsPath(sessionId, baseDir);
797
+ const resumeSeq = highestSeqOnDisk(eventsPath);
798
+ const stream = createWriteStream(eventsPath, { flags: "a" });
761
799
  stream.on("error", (err) => {
762
800
  console.warn(`[transcript-writer] session ${sessionId}: ${err.message}`);
763
801
  });
764
802
  const state = {
765
803
  stream,
766
- seq: 0,
804
+ seq: resumeSeq,
767
805
  textBuf: "",
768
806
  thoughtBuf: "",
769
807
  textDebounce: null,
@@ -774,9 +812,12 @@ function createTranscriptWriter(opts) {
774
812
  };
775
813
  const writeRecord = (sessionId, state, record) => {
776
814
  state.seq += 1;
777
- state.stream.write(
778
- JSON.stringify({ seq: state.seq, ts: (/* @__PURE__ */ new Date()).toISOString(), ...record }) + "\n"
779
- );
815
+ const full = { seq: state.seq, ts: (/* @__PURE__ */ new Date()).toISOString(), ...record };
816
+ state.stream.write(JSON.stringify(full) + "\n");
817
+ const subs = listeners.get(sessionId);
818
+ if (subs) {
819
+ for (const onRecord of subs) onRecord(full);
820
+ }
780
821
  };
781
822
  const flushTextBuf = (sessionId, state) => {
782
823
  if (state.textDebounce) {
@@ -784,9 +825,9 @@ function createTranscriptWriter(opts) {
784
825
  state.textDebounce = null;
785
826
  }
786
827
  if (!state.textBuf) return;
787
- const text5 = state.textBuf;
828
+ const text6 = state.textBuf;
788
829
  state.textBuf = "";
789
- writeRecord(sessionId, state, { kind: "text-delta", sessionId, text: text5 });
830
+ writeRecord(sessionId, state, { kind: "text-delta", sessionId, text: text6 });
790
831
  };
791
832
  const flushThoughtBuf = (sessionId, state) => {
792
833
  if (state.thoughtDebounce) {
@@ -794,9 +835,9 @@ function createTranscriptWriter(opts) {
794
835
  state.thoughtDebounce = null;
795
836
  }
796
837
  if (!state.thoughtBuf) return;
797
- const text5 = state.thoughtBuf;
838
+ const text6 = state.thoughtBuf;
798
839
  state.thoughtBuf = "";
799
- writeRecord(sessionId, state, { kind: "thought", sessionId, text: text5 });
840
+ writeRecord(sessionId, state, { kind: "thought", sessionId, text: text6 });
800
841
  };
801
842
  const flushBuffers = (sessionId, state) => {
802
843
  flushThoughtBuf(sessionId, state);
@@ -807,9 +848,9 @@ function createTranscriptWriter(opts) {
807
848
  state.textDebounce = setTimeout(() => {
808
849
  state.textDebounce = null;
809
850
  if (!state.textBuf) return;
810
- const text5 = state.textBuf;
851
+ const text6 = state.textBuf;
811
852
  state.textBuf = "";
812
- writeRecord(sessionId, state, { kind: "text-delta", sessionId, text: text5, partial: true });
853
+ writeRecord(sessionId, state, { kind: "text-delta", sessionId, text: text6, partial: true });
813
854
  }, DEBOUNCE_MS);
814
855
  };
815
856
  const scheduleThoughtDebounce = (sessionId, state) => {
@@ -817,17 +858,17 @@ function createTranscriptWriter(opts) {
817
858
  state.thoughtDebounce = setTimeout(() => {
818
859
  state.thoughtDebounce = null;
819
860
  if (!state.thoughtBuf) return;
820
- const text5 = state.thoughtBuf;
861
+ const text6 = state.thoughtBuf;
821
862
  state.thoughtBuf = "";
822
- writeRecord(sessionId, state, { kind: "thought", sessionId, text: text5, partial: true });
863
+ writeRecord(sessionId, state, { kind: "thought", sessionId, text: text6, partial: true });
823
864
  }, DEBOUNCE_MS);
824
865
  };
825
866
  return {
826
867
  recordPrompt(sessionId, message) {
827
868
  const state = getState(sessionId);
828
869
  flushBuffers(sessionId, state);
829
- const text5 = typeof message === "string" ? message : JSON.stringify(message);
830
- writeRecord(sessionId, state, { kind: "user-prompt", sessionId, text: text5 });
870
+ const text6 = typeof message === "string" ? message : JSON.stringify(message);
871
+ writeRecord(sessionId, state, { kind: "user-prompt", sessionId, text: text6 });
831
872
  },
832
873
  recordEvent(sessionId, evt) {
833
874
  const state = getState(sessionId);
@@ -863,7 +904,12 @@ function createTranscriptWriter(opts) {
863
904
  sessionId,
864
905
  toolCallId: evt.toolCallId,
865
906
  toolName: evt.toolName,
866
- arguments: evt.arguments
907
+ arguments: evt.arguments,
908
+ // Carried through so a replay can tell an ENRICHMENT of an
909
+ // announced call from a genuine second call. Readers merge by
910
+ // toolCallId either way, but a record that silently drops the
911
+ // distinction can't be reasoned about after the fact.
912
+ ...evt.isUpdate ? { isUpdate: true } : {}
867
913
  });
868
914
  break;
869
915
  case "tool-result":
@@ -936,8 +982,8 @@ function createTranscriptWriter(opts) {
936
982
  if (!state) return Promise.resolve();
937
983
  flushBuffers(sessionId, state);
938
984
  states.delete(sessionId);
939
- return new Promise((resolve13) => {
940
- state.stream.end(() => resolve13());
985
+ return new Promise((resolve12) => {
986
+ state.stream.end(() => resolve12());
941
987
  });
942
988
  },
943
989
  closeAll() {
@@ -946,10 +992,24 @@ function createTranscriptWriter(opts) {
946
992
  const state = states.get(sessionId);
947
993
  if (!state) continue;
948
994
  flushBuffers(sessionId, state);
949
- closings.push(new Promise((resolve13) => state.stream.end(() => resolve13())));
995
+ closings.push(new Promise((resolve12) => state.stream.end(() => resolve12())));
950
996
  }
951
997
  states.clear();
952
998
  return Promise.all(closings).then(() => void 0);
999
+ },
1000
+ subscribe(sessionId, onRecord) {
1001
+ let subs = listeners.get(sessionId);
1002
+ if (!subs) {
1003
+ subs = /* @__PURE__ */ new Set();
1004
+ listeners.set(sessionId, subs);
1005
+ }
1006
+ subs.add(onRecord);
1007
+ return () => {
1008
+ const current = listeners.get(sessionId);
1009
+ if (!current) return;
1010
+ current.delete(onRecord);
1011
+ if (current.size === 0) listeners.delete(sessionId);
1012
+ };
953
1013
  }
954
1014
  };
955
1015
  }
@@ -961,10 +1021,10 @@ var ROLE_ICON = {
961
1021
  tool: "\u{1F527} Tool",
962
1022
  system: "\u2699\uFE0F System"
963
1023
  };
964
- function trunc(text5, n) {
965
- if (text5.length <= n) return text5;
966
- return text5.slice(0, n) + `
967
- \u2026 [${text5.length - n} chars truncated]`;
1024
+ function trunc(text6, n) {
1025
+ if (text6.length <= n) return text6;
1026
+ return text6.slice(0, n) + `
1027
+ \u2026 [${text6.length - n} chars truncated]`;
968
1028
  }
969
1029
  function renderMarkdown(session, opts = {}) {
970
1030
  const { meta, messages } = session;
@@ -1065,9 +1125,9 @@ async function exportClaudeCodeSession(adapterSessionId, cwd) {
1065
1125
  let stream;
1066
1126
  try {
1067
1127
  stream = createReadStream(filePath, { encoding: "utf8" });
1068
- await new Promise((resolve13, reject) => {
1128
+ await new Promise((resolve12, reject) => {
1069
1129
  stream.once("error", reject);
1070
- stream.once("open", resolve13);
1130
+ stream.once("open", resolve12);
1071
1131
  });
1072
1132
  } catch (err) {
1073
1133
  const code = err.code;
@@ -1270,7 +1330,7 @@ async function exportHermesSession(adapterSessionId) {
1270
1330
  }
1271
1331
  async function defaultHermesRunner(adapterSessionId) {
1272
1332
  const { spawn: spawn6 } = await import('child_process');
1273
- return new Promise((resolve13, reject) => {
1333
+ return new Promise((resolve12, reject) => {
1274
1334
  const chunks = [];
1275
1335
  const errChunks = [];
1276
1336
  const proc = spawn6(
@@ -1289,7 +1349,7 @@ async function defaultHermesRunner(adapterSessionId) {
1289
1349
  )
1290
1350
  );
1291
1351
  } else {
1292
- resolve13(Buffer.concat(chunks).toString("utf8"));
1352
+ resolve12(Buffer.concat(chunks).toString("utf8"));
1293
1353
  }
1294
1354
  });
1295
1355
  });
@@ -1326,9 +1386,9 @@ async function exportDaemonEventsSession(sessionId, desc) {
1326
1386
  let stream;
1327
1387
  try {
1328
1388
  stream = createReadStream(filePath, { encoding: "utf8" });
1329
- await new Promise((resolve13, reject) => {
1389
+ await new Promise((resolve12, reject) => {
1330
1390
  stream.once("error", reject);
1331
- stream.once("open", resolve13);
1391
+ stream.once("open", resolve12);
1332
1392
  });
1333
1393
  } catch (err) {
1334
1394
  const code = err.code;
@@ -1398,10 +1458,10 @@ Either the session never drove an agent-cli turn, or it predates this feature.`
1398
1458
  case "tool-result": {
1399
1459
  flushAssistant();
1400
1460
  const name = rec.toolCallId ? toolNameById.get(rec.toolCallId) : void 0;
1401
- const text5 = typeof rec.result === "string" ? rec.result : JSON.stringify(rec.result ?? "");
1461
+ const text6 = typeof rec.result === "string" ? rec.result : JSON.stringify(rec.result ?? "");
1402
1462
  messages.push({
1403
1463
  role: "tool",
1404
- text: rec.isError ? `[error] ${text5}` : text5,
1464
+ text: rec.isError ? `[error] ${text6}` : text6,
1405
1465
  ...name ? { toolName: name } : {},
1406
1466
  ...tsOrUndefined !== void 0 ? { ts: tsOrUndefined } : {}
1407
1467
  });
@@ -1571,6 +1631,11 @@ async function loadWorkspacesConfig(path = DEFAULT_CONFIG_PATH()) {
1571
1631
  function findWorkspace(config, slug) {
1572
1632
  return config.workspaces.find((w) => w.slug === sanitizeSlug(slug));
1573
1633
  }
1634
+ function findWorkspaceByPath(config, dir) {
1635
+ const resolved = resolve(dir);
1636
+ const candidates = config.workspaces.filter((w) => resolved.startsWith(resolve(w.path) + "/") || resolved === resolve(w.path)).sort((a, b) => b.path.length - a.path.length);
1637
+ return candidates[0];
1638
+ }
1574
1639
  function getActiveWorkspace(config) {
1575
1640
  if (!config.active) return config.workspaces[0];
1576
1641
  return findWorkspace(config, config.active) ?? config.workspaces[0];
@@ -1609,13 +1674,36 @@ function normalizeConfig(parsed) {
1609
1674
  return out;
1610
1675
  }
1611
1676
  var CONFIG_FILE_PATH = () => join(homedir(), ".agentproto", "config.json");
1677
+ function sanitizeAcpAgents(raw, target) {
1678
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
1679
+ console.warn(
1680
+ `[runtime/config] ${target}: 'acpAgents' is not an object \u2014 ignoring`
1681
+ );
1682
+ return void 0;
1683
+ }
1684
+ const out = {};
1685
+ for (const [slug, value] of Object.entries(raw)) {
1686
+ if (value && typeof value === "object" && !Array.isArray(value) && typeof value.bin === "string" && value.bin.length > 0) {
1687
+ out[slug] = value;
1688
+ } else {
1689
+ console.warn(
1690
+ `[runtime/config] ${target}: acpAgents.${slug} is missing a string 'bin' \u2014 ignoring`
1691
+ );
1692
+ }
1693
+ }
1694
+ return Object.keys(out).length > 0 ? out : void 0;
1695
+ }
1612
1696
  async function loadConfig(path) {
1613
1697
  const target = CONFIG_FILE_PATH();
1614
1698
  try {
1615
1699
  const raw = await promises.readFile(target, "utf8");
1616
1700
  const parsed = JSON.parse(raw);
1617
1701
  if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1618
- return parsed;
1702
+ const cfg = parsed;
1703
+ if (cfg.acpAgents !== void 0) {
1704
+ cfg.acpAgents = sanitizeAcpAgents(cfg.acpAgents, target);
1705
+ }
1706
+ return cfg;
1619
1707
  }
1620
1708
  console.warn(
1621
1709
  `[runtime/config] ${target}: top-level value is not an object \u2014 ignoring`
@@ -1632,6 +1720,10 @@ async function loadConfig(path) {
1632
1720
  }
1633
1721
  }
1634
1722
 
1723
+ // src/providers-store.ts
1724
+ var providers_store_exports = {};
1725
+ __reExport(providers_store_exports, providers_store_star);
1726
+
1635
1727
  // src/spawn-defaults.ts
1636
1728
  function resolveSpawnDefaults(defaults, adapterSlug, input) {
1637
1729
  const adapterDefaults = defaults?.adapters?.[adapterSlug];
@@ -1643,7 +1735,111 @@ function resolveSpawnDefaults(defaults, adapterSlug, input) {
1643
1735
  const skills = input.skills !== void 0 ? input.skills : Array.from(
1644
1736
  /* @__PURE__ */ new Set([...defaults?.skills ?? [], ...adapterDefaults?.skills ?? []])
1645
1737
  );
1646
- return { skills, options };
1738
+ const requestedMode = input.auth?.mode ?? adapterDefaults?.auth?.mode;
1739
+ const explicit = input.auth !== void 0 || adapterDefaults?.auth !== void 0;
1740
+ const subscriptionCredential = input.auth?.token ?? adapterDefaults?.auth?.token;
1741
+ const apiKeyCredential = input.auth?.apiKey ?? adapterDefaults?.auth?.apiKey;
1742
+ const authProvider = input.auth?.provider ?? adapterDefaults?.auth?.provider;
1743
+ return {
1744
+ skills,
1745
+ options,
1746
+ auth: {
1747
+ explicit,
1748
+ ...requestedMode ? { requestedMode } : {},
1749
+ ...subscriptionCredential !== void 0 ? { subscriptionCredential } : {},
1750
+ ...apiKeyCredential !== void 0 ? { apiKeyCredential } : {},
1751
+ ...authProvider ? { provider: authProvider } : {}
1752
+ }
1753
+ };
1754
+ }
1755
+ var CREDENTIAL_FINGERPRINT_PREFIXES = [
1756
+ "sk-ant-oat",
1757
+ "sk-ant-api",
1758
+ "sk-ant-",
1759
+ "sk-or-v1-",
1760
+ "sk-or-",
1761
+ "sk-proj-",
1762
+ "sk-",
1763
+ "gsk_",
1764
+ "AIza"
1765
+ ];
1766
+ function credentialFingerprint(mode, credential) {
1767
+ const prefix = CREDENTIAL_FINGERPRINT_PREFIXES.find((p) => credential.startsWith(p));
1768
+ const last4 = credential.slice(-4);
1769
+ return prefix ? `${mode} \xB7 ${prefix}\u2026${last4}` : `${mode} \xB7 \u2026${last4}`;
1770
+ }
1771
+ var AuthResolutionError = class extends Error {
1772
+ code = "unsupported_auth_mode";
1773
+ constructor(message) {
1774
+ super(message);
1775
+ this.name = "AuthResolutionError";
1776
+ }
1777
+ };
1778
+ function resolveAuthSpec(input) {
1779
+ const provider = input.requestedProvider ?? input.descriptor.provider ?? (input.model ? getModelProvider(input.model) : void 0);
1780
+ if (!provider) return void 0;
1781
+ const sub = input.descriptor.authSubscription;
1782
+ const supportsSub = sub !== void 0;
1783
+ const enforce = input.descriptor.authEnforce ?? "when-configured";
1784
+ const apiKeyEnv = (0, providers_store_exports.providerEnvVar)(provider);
1785
+ const subCredAvailable = input.subscriptionCredential !== void 0;
1786
+ const apiCredAvailable = input.apiKeyConfigCredential !== void 0 || input.apiKeyStoreCredential !== void 0;
1787
+ let mode;
1788
+ if (input.requestedMode) {
1789
+ if (input.requestedMode === "subscription" && !supportsSub) {
1790
+ throw new AuthResolutionError(
1791
+ `auth mode "subscription" is not supported for provider "${provider}" (this adapter declares no authSubscription) \u2014 use api-key instead.`
1792
+ );
1793
+ }
1794
+ mode = input.requestedMode;
1795
+ } else {
1796
+ const preference = supportsSub ? ["subscription", "api-key"] : ["api-key"];
1797
+ mode = preference.find((m) => m === "subscription" ? subCredAvailable : apiCredAvailable) ?? preference[0];
1798
+ }
1799
+ let setEnv;
1800
+ let credential;
1801
+ let credentialSource;
1802
+ if (mode === "subscription") {
1803
+ setEnv = sub.setEnv;
1804
+ credential = input.subscriptionCredential;
1805
+ credentialSource = credential !== void 0 ? "explicit-config" : "none";
1806
+ } else {
1807
+ setEnv = apiKeyEnv;
1808
+ if (input.apiKeyConfigCredential !== void 0) {
1809
+ credential = input.apiKeyConfigCredential;
1810
+ credentialSource = "explicit-config";
1811
+ } else if (input.apiKeyStoreCredential !== void 0) {
1812
+ credential = input.apiKeyStoreCredential;
1813
+ credentialSource = "providers-store";
1814
+ } else {
1815
+ credential = void 0;
1816
+ credentialSource = "none";
1817
+ }
1818
+ }
1819
+ const credVars = /* @__PURE__ */ new Set([apiKeyEnv]);
1820
+ if (sub) {
1821
+ credVars.add(sub.setEnv);
1822
+ for (const c of sub.conflictEnv ?? []) credVars.add(c);
1823
+ }
1824
+ credVars.delete(setEnv);
1825
+ const unsetEnv = [...credVars];
1826
+ if (mode === "subscription" && sub?.unsetEnvAdd) unsetEnv.push(...sub.unsetEnvAdd);
1827
+ const spec = {
1828
+ mode,
1829
+ ...credential !== void 0 ? { credential } : {},
1830
+ setEnv,
1831
+ unsetEnv,
1832
+ explicit: input.explicit,
1833
+ enforce
1834
+ };
1835
+ const echo = {
1836
+ provider,
1837
+ authMode: mode,
1838
+ credentialSource,
1839
+ setEnv,
1840
+ ...credential !== void 0 ? { fingerprint: credentialFingerprint(mode, credential) } : {}
1841
+ };
1842
+ return { spec, echo };
1647
1843
  }
1648
1844
  function normalizeSkillsOption(skills, options, declaredOptions) {
1649
1845
  if (skills.length === 0 || "skills" in options) return options;
@@ -1868,8 +2064,8 @@ var MAX_OUTPUT_LINES = 500;
1868
2064
  function extractPromptText(message) {
1869
2065
  if (typeof message === "string") return message;
1870
2066
  if (message && typeof message === "object" && "text" in message) {
1871
- const text5 = message.text;
1872
- if (typeof text5 === "string") return text5;
2067
+ const text6 = message.text;
2068
+ if (typeof text6 === "string") return text6;
1873
2069
  }
1874
2070
  return JSON.stringify(message);
1875
2071
  }
@@ -1928,6 +2124,16 @@ function createSandboxAgentSessionProxy(opts) {
1928
2124
  }
1929
2125
 
1930
2126
  // src/session-spawn.ts
2127
+ var SPAWN_CLAIM_WINDOW_MS = 3e4;
2128
+ var spawnClaimsByRegistry = /* @__PURE__ */ new WeakMap();
2129
+ function claimsFor(registry) {
2130
+ let claims = spawnClaimsByRegistry.get(registry);
2131
+ if (!claims) {
2132
+ claims = /* @__PURE__ */ new Map();
2133
+ spawnClaimsByRegistry.set(registry, claims);
2134
+ }
2135
+ return claims;
2136
+ }
1931
2137
  function cleanAgentLines(lines) {
1932
2138
  return lines.map((l) => l.replace(/\x1b\[[0-9;]*m/g, "")).filter((l) => {
1933
2139
  const t = l.trim();
@@ -1947,18 +2153,26 @@ async function spawnAgentSession(deps2, input) {
1947
2153
  resolveSandboxProvider: resolveSandboxProvider2
1948
2154
  } = deps2;
1949
2155
  let cwd = input.cwd;
1950
- let resolvedSlug = input.workspaceSlug ?? "default";
1951
- if (!cwd) {
2156
+ let resolvedSlug = input.workspaceSlug;
2157
+ if (!cwd || !resolvedSlug) {
1952
2158
  try {
1953
2159
  const config = await loadWorkspacesConfig();
1954
- const ws = input.workspaceSlug ? findWorkspace(config, input.workspaceSlug) : getActiveWorkspace(config);
1955
- if (ws) {
1956
- cwd = ws.path;
1957
- resolvedSlug = ws.slug;
2160
+ if (!cwd) {
2161
+ const ws = input.workspaceSlug ? findWorkspace(config, input.workspaceSlug) : getActiveWorkspace(config);
2162
+ if (ws) {
2163
+ cwd = ws.path;
2164
+ resolvedSlug = ws.slug;
2165
+ }
2166
+ } else if (!resolvedSlug) {
2167
+ const ws = findWorkspaceByPath(config, cwd);
2168
+ if (ws) {
2169
+ resolvedSlug = ws.slug;
2170
+ }
1958
2171
  }
1959
2172
  } catch {
1960
2173
  }
1961
2174
  }
2175
+ resolvedSlug = resolvedSlug ?? "default";
1962
2176
  if (!cwd) {
1963
2177
  return {
1964
2178
  ok: false,
@@ -2078,8 +2292,43 @@ async function spawnAgentSession(deps2, input) {
2078
2292
  }
2079
2293
  const spawnDefaults = resolveSpawnDefaults(configDefaults, input.adapter, {
2080
2294
  skills: input.skills,
2081
- options: input.options
2295
+ options: input.options,
2296
+ auth: input.auth
2082
2297
  });
2298
+ let authSpec;
2299
+ let authEcho;
2300
+ if (resolved && input.sandbox === void 0 && resolved.authDescriptor) {
2301
+ const authModel = input.model ?? resolved.defaultModel;
2302
+ const pinnedProvider = spawnDefaults.auth.provider;
2303
+ const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
2304
+ const apiKeyStoreCredential = resolvedProvider && spawnDefaults.auth.explicit && spawnDefaults.auth.apiKeyCredential === void 0 ? await (0, providers_store_exports.getProviderKey)(resolvedProvider) : void 0;
2305
+ try {
2306
+ const result = resolveAuthSpec({
2307
+ descriptor: resolved.authDescriptor,
2308
+ ...authModel ? { model: authModel } : {},
2309
+ ...pinnedProvider ? { requestedProvider: pinnedProvider } : {},
2310
+ ...spawnDefaults.auth.requestedMode ? { requestedMode: spawnDefaults.auth.requestedMode } : {},
2311
+ explicit: spawnDefaults.auth.explicit,
2312
+ ...spawnDefaults.auth.subscriptionCredential !== void 0 ? { subscriptionCredential: spawnDefaults.auth.subscriptionCredential } : {},
2313
+ ...spawnDefaults.auth.apiKeyCredential !== void 0 ? { apiKeyConfigCredential: spawnDefaults.auth.apiKeyCredential } : {},
2314
+ ...apiKeyStoreCredential !== void 0 ? { apiKeyStoreCredential } : {}
2315
+ });
2316
+ if (result) {
2317
+ authSpec = result.spec;
2318
+ authEcho = result.echo;
2319
+ }
2320
+ } catch (err) {
2321
+ if (err instanceof AuthResolutionError) {
2322
+ return {
2323
+ ok: false,
2324
+ code: "unsupported_auth_mode",
2325
+ message: `agent_start: ${err.message}`,
2326
+ details: { adapter: input.adapter, provider: resolvedProvider }
2327
+ };
2328
+ }
2329
+ throw err;
2330
+ }
2331
+ }
2083
2332
  const effectiveOptions = normalizeSkillsOption(
2084
2333
  spawnDefaults.skills,
2085
2334
  spawnDefaults.options,
@@ -2088,6 +2337,39 @@ async function spawnAgentSession(deps2, input) {
2088
2337
  const effectivePrompt = input.prompt ? `${composeRoleContext(role, input.promptAppend, roleRegistry)}
2089
2338
 
2090
2339
  ${input.prompt}` : input.prompt;
2340
+ let settleClaim;
2341
+ if (input.idempotencyKey) {
2342
+ const claims = claimsFor(registry);
2343
+ const key = `${input.adapter}${cwd}${input.idempotencyKey}`;
2344
+ const now = Date.now();
2345
+ for (const [k, claim] of claims) {
2346
+ if (claim.resolvedAt !== void 0 && now - claim.resolvedAt > SPAWN_CLAIM_WINDOW_MS) {
2347
+ claims.delete(k);
2348
+ }
2349
+ }
2350
+ const existing = claims.get(key);
2351
+ if (existing) {
2352
+ const result = await existing.result;
2353
+ return result.ok ? { ...result, deduped: true } : result;
2354
+ }
2355
+ let resolveClaim;
2356
+ claims.set(key, { result: new Promise((resolve12) => {
2357
+ resolveClaim = resolve12;
2358
+ }) });
2359
+ settleClaim = (result) => {
2360
+ if (result.ok) {
2361
+ const claim = claims.get(key);
2362
+ if (claim) claim.resolvedAt = Date.now();
2363
+ } else {
2364
+ claims.delete(key);
2365
+ }
2366
+ resolveClaim(result);
2367
+ };
2368
+ }
2369
+ const finish = (result) => {
2370
+ settleClaim?.(result);
2371
+ return result;
2372
+ };
2091
2373
  try {
2092
2374
  const resolvedMcpServers = await resolveMcpCredentialHeaders(mcpServers);
2093
2375
  let liveSessionId;
@@ -2128,7 +2410,9 @@ ${input.prompt}` : input.prompt;
2128
2410
  ...Object.keys(effectiveOptions).length > 0 ? { options: effectiveOptions } : {},
2129
2411
  ...input.model ? { model: input.model } : {},
2130
2412
  ...input.effort ? { effort: input.effort } : {},
2413
+ ...authSpec ? { auth: authSpec } : {},
2131
2414
  ...resolvedMcpServers ? { mcpServers: resolvedMcpServers } : {},
2415
+ ...input.permissionHold ? { permissionHold: true } : {},
2132
2416
  onActivity: () => {
2133
2417
  if (liveSessionId) registry.pulseActivity(liveSessionId);
2134
2418
  }
@@ -2157,8 +2441,28 @@ ${input.prompt}` : input.prompt;
2157
2441
  ...input.maxCostUsd !== void 0 ? { maxCostUsd: input.maxCostUsd } : {},
2158
2442
  ...readUsage ? { readUsage } : {},
2159
2443
  ...input.trace !== void 0 ? { trace: input.trace } : {},
2444
+ // Verifiability: record the OBSERVABLE echo — resolved provider, mode,
2445
+ // credential source, the env var actually set, and a non-secret
2446
+ // fingerprint (never the credential). DECISION 9③/10②. Recorded only
2447
+ // when a credential actually resolved (fingerprint present); a
2448
+ // configured-but-missing-credential spawn fails fast in the driver
2449
+ // before a descriptor exists. Absent for sandbox spawns (the box
2450
+ // resolves its own credential, so a host-side echo would misrepresent
2451
+ // it — `authEcho` is already left undefined for those above).
2452
+ ...authEcho?.fingerprint ? {
2453
+ auth: {
2454
+ mode: authEcho.authMode,
2455
+ fingerprint: authEcho.fingerprint,
2456
+ provider: authEcho.provider,
2457
+ credentialSource: authEcho.credentialSource,
2458
+ setEnv: authEcho.setEnv
2459
+ }
2460
+ } : {},
2160
2461
  ...sandboxId ? { remote: true, sandboxId } : {},
2161
- ...sandboxTeardown ? { sandboxTeardown } : {}
2462
+ ...sandboxTeardown ? { sandboxTeardown } : {},
2463
+ // Hold mode is a local-driver capability; a sandbox spawn proxies to the
2464
+ // box's own daemon, which handles permissions there.
2465
+ ...input.permissionHold && input.sandbox === void 0 ? { permissionHold: true } : {}
2162
2466
  });
2163
2467
  liveSessionId = desc.id;
2164
2468
  bindOrchestratorLifecycle?.(desc.id);
@@ -2174,28 +2478,28 @@ ${input.prompt}` : input.prompt;
2174
2478
  if (waitUnsub) waitUnsub();
2175
2479
  const waitTail = waitLines.slice(-80);
2176
2480
  const output = cleanAgentLines(waitTail);
2177
- return { ok: true, descriptor: desc, output };
2481
+ return finish({ ok: true, descriptor: desc, output });
2178
2482
  }
2179
- return { ok: true, descriptor: desc };
2483
+ return finish({ ok: true, descriptor: desc });
2180
2484
  } catch (err) {
2181
- return {
2485
+ return finish({
2182
2486
  ok: false,
2183
2487
  code: "agent_spawn_failed",
2184
2488
  message: `agent_start: spawn failed \u2014 ${err instanceof Error ? err.message : String(err)}`
2185
- };
2489
+ });
2186
2490
  }
2187
2491
  }
2188
2492
  async function resolveMcpCredentialHeaders(mcpServers) {
2189
2493
  if (!mcpServers || mcpServers.length === 0) return mcpServers;
2190
- const { resolveMcpCredentialHeaders: resolve13 } = getMcpCredentialDeps();
2191
- if (!resolve13) return mcpServers;
2494
+ const { resolveMcpCredentialHeaders: resolve12 } = getMcpCredentialDeps();
2495
+ if (!resolve12) return mcpServers;
2192
2496
  return Promise.all(
2193
2497
  mcpServers.map(async (entry) => {
2194
2498
  const ref = entry.credentialRef;
2195
2499
  if (!ref) return entry;
2196
2500
  let brokered;
2197
2501
  try {
2198
- brokered = await resolve13({ credentialRef: ref });
2502
+ brokered = await resolve12({ credentialRef: ref });
2199
2503
  } catch (err) {
2200
2504
  console.warn(
2201
2505
  `[agent_start] credentialRef resolution failed for "${entry.name}" (${ref}): ${err instanceof Error ? err.message : String(err)}`
@@ -2294,10 +2598,10 @@ async function bootSandboxAgentSession(opts) {
2294
2598
  };
2295
2599
  }
2296
2600
  async function resolveSandboxSecret(slug) {
2297
- const { resolveSandboxSecret: resolve13 } = getMcpCredentialDeps();
2298
- if (!resolve13) return null;
2601
+ const { resolveSandboxSecret: resolve12 } = getMcpCredentialDeps();
2602
+ if (!resolve12) return null;
2299
2603
  try {
2300
- return await resolve13(slug);
2604
+ return await resolve12(slug);
2301
2605
  } catch (err) {
2302
2606
  console.warn(
2303
2607
  `[agent_start] sandbox secret resolution failed for "${slug}": ${err instanceof Error ? err.message : String(err)}`
@@ -2322,6 +2626,24 @@ var mcpPositiveNumber = z.preprocess(
2322
2626
  (v) => typeof v === "string" && v.trim() !== "" ? Number(v) : v,
2323
2627
  z.number().positive()
2324
2628
  );
2629
+ var sessionIdField = z.string().optional().describe(
2630
+ "Session id (as returned by agent_start). Accepts either `sessionId` or its alias `id` \u2014 pass whichever you have."
2631
+ );
2632
+ var sessionIdAliasField = z.string().optional().describe("Alias for `sessionId` \u2014 the `id` field returned by agent_start.");
2633
+ function resolveSessionIdArg(input) {
2634
+ return input.sessionId ?? input.id;
2635
+ }
2636
+ function missingSessionIdError(tool) {
2637
+ return {
2638
+ content: [
2639
+ {
2640
+ type: "text",
2641
+ text: `${tool}: missing session id \u2014 pass \`sessionId\` (or its alias \`id\`).`
2642
+ }
2643
+ ],
2644
+ isError: true
2645
+ };
2646
+ }
2325
2647
  function registerAgentTools(server, opts) {
2326
2648
  const {
2327
2649
  registry,
@@ -2356,6 +2678,12 @@ function registerAgentTools(server, opts) {
2356
2678
  mode: z.string().optional().describe(
2357
2679
  "Manifest-declared mode id (AIP-45 `modes`) applied at spawn time, BEFORE the child process starts \u2014 e.g. claude-code's 'plan' (read-only: reasons and proposes but does not edit or run commands), 'accept-edits', 'bypass-permissions'; codex's 'read-only' / 'full-access'; mastracode/opencode's 'plan' / 'build'. Adapters that don't declare `modes` (e.g. hermes) reject ANY value here \u2014 only pass this for adapters known to support it. Omit for the adapter's normal interactive mode."
2358
2680
  ),
2681
+ idempotencyKey: z.string().min(1).optional().describe(
2682
+ "Caller-declared 'this is the same logical spawn' token. A retried agent_start call (e.g. after a slow/lost response) that repeats the same `idempotencyKey` for the same `adapter`+`cwd` within ~30s of a successful spawn gets that SAME session's descriptor back instead of forking a second process \u2014 set `deduped: true` on the response so you can tell. Omit to spawn unconditionally (today's behaviour, and still required for deliberate concurrent spawns into the same cwd \u2014 this field can't distinguish a retry from an intentional duplicate spawn, only your own declared key can). Recommended for any caller that might retry a spawn it can't otherwise confirm succeeded."
2683
+ ),
2684
+ permissionHold: mcpBool.optional().describe(
2685
+ "Start the session in permission-hold mode: every ACP permission request the agent raises (Write, Bash, \u2026) is SURFACED and HELD in the cross-session inbox (`permissions_list` / `permissions_respond`) instead of auto-answered, and the agent blocks until a human/orchestrator approves or denies it. Default false = today's auto-answer behaviour. ACP adapters only; others ignore it."
2686
+ ),
2359
2687
  options: jsonTolerant(
2360
2688
  z.record(z.string(), z.union([z.boolean(), z.number(), z.string()]))
2361
2689
  ).optional().describe(
@@ -2370,6 +2698,15 @@ function registerAgentTools(server, opts) {
2370
2698
  effort: z.string().optional().describe(
2371
2699
  "Reasoning effort level (e.g. 'low', 'medium', 'high', 'xhigh', 'max', 'ultracode'). IMPORTANT: effort is calibrated per model \u2014 the same label maps to different compute budgets across models, and defaults differ by model (Sonnet 4.6 / Opus 4.8 default 'high'; Opus 4.7 default 'xhigh'). 'max' and 'ultracode' are session-only. Omit to keep the model's own default."
2372
2700
  ),
2701
+ auth: jsonTolerant(
2702
+ z.object({
2703
+ mode: z.enum(["subscription", "api-key"]).optional(),
2704
+ token: z.string().optional(),
2705
+ apiKey: z.string().optional()
2706
+ })
2707
+ ).optional().describe(
2708
+ "Deterministic billing-auth mode + EXPLICIT credential for adapters that declare it (today: claude-code). EXPLICIT credential selection, not scrub-by-absence: `mode` picks 'subscription' (default) or 'api-key'; `token`/`apiKey` (matching the resolved mode) is the secret VALUE, merged against `~/.agentproto/config.json`'s `defaults.adapters.claude-code.auth` (this field's `mode` wins; the credential for the resolved mode wins over the matching config field). For claude-code, 'subscription' SETS CLAUDE_CODE_OAUTH_TOKEN to `token` (a bearer token minted via `claude setup-token` \u2014 bills the Max/Pro subscription, not API credits) and DELETES ANTHROPIC_API_KEY + the cloud-provider redirect toggles + ANTHROPIC_BASE_URL. 'api-key' SETS ANTHROPIC_API_KEY to `apiKey` and DELETES ANTHROPIC_AUTH_TOKEN \u2014 the deliberate 'bill the API' choice. FAILS FAST (refuses the spawn, no fallback) when the resolved mode has no credential configured anywhere. The secret is never logged or echoed back \u2014 only a fingerprint appears on the session descriptor / `agent_sessions_list`. Adapters that don't declare this vocabulary ignore this field entirely."
2709
+ ),
2373
2710
  mcpServers: jsonTolerant(
2374
2711
  z.array(
2375
2712
  z.object({
@@ -2460,18 +2797,22 @@ function registerAgentTools(server, opts) {
2460
2797
  input
2461
2798
  );
2462
2799
  if (result.ok) {
2463
- const body = result.output ? { ...result.descriptor, output: result.output } : result.descriptor;
2800
+ const body = {
2801
+ ...result.descriptor,
2802
+ ...result.output ? { output: result.output } : {},
2803
+ ...result.deduped ? { deduped: true } : {}
2804
+ };
2464
2805
  return {
2465
2806
  content: [{ type: "text", text: JSON.stringify(body, null, 2) }]
2466
2807
  };
2467
2808
  }
2468
- const text5 = result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? JSON.stringify(
2809
+ const text6 = result.code === "orchestrator_max_depth_exceeded" || result.code === "orchestrator_child_quota_exceeded" || result.code === "role_spawn_denied" ? JSON.stringify(
2469
2810
  { error: result.code, message: result.message, ...result.details },
2470
2811
  null,
2471
2812
  2
2472
2813
  ) : result.message;
2473
2814
  return {
2474
- content: [{ type: "text", text: text5 }],
2815
+ content: [{ type: "text", text: text6 }],
2475
2816
  isError: true
2476
2817
  };
2477
2818
  }
@@ -2480,15 +2821,18 @@ function registerAgentTools(server, opts) {
2480
2821
  "agent_prompt",
2481
2822
  "Send a follow-up prompt to a live agent session \u2014 multi-turn continuity without re-spawning. The session id comes from `agent_start` (or `agent_sessions_list`). Returns immediately; tail output via `agent_output` or the SSE /sessions/:id/stream endpoint. By default, a session mid-turn rejects the new prompt \u2014 pass `interrupt: true` to cancel the in-flight turn and redirect the SAME session onto this prompt instead, without losing its context (unlike `agent_kill`, which ends the session entirely). `interrupt` is a no-op on an already-idle session.",
2482
2823
  {
2483
- sessionId: z.string().describe("Session id returned by agent_start."),
2824
+ sessionId: sessionIdField,
2825
+ id: sessionIdAliasField,
2484
2826
  prompt: z.string().min(1).describe("The next user turn (plain text)."),
2485
2827
  interrupt: z.boolean().optional().describe(
2486
2828
  "When true and the session is mid-turn, cancel the in-flight turn and deliver this prompt on the same session instead of rejecting. No-op when the session is already idle. Default false (mid-turn rejects, as today)."
2487
2829
  )
2488
2830
  },
2489
2831
  async (input) => {
2832
+ const sessionId = resolveSessionIdArg(input);
2833
+ if (!sessionId) return missingSessionIdError("agent_prompt");
2490
2834
  try {
2491
- await registry.enqueuePrompt(input.sessionId, input.prompt, {
2835
+ await registry.enqueuePrompt(sessionId, input.prompt, {
2492
2836
  interrupt: input.interrupt
2493
2837
  });
2494
2838
  return {
@@ -2496,7 +2840,7 @@ function registerAgentTools(server, opts) {
2496
2840
  {
2497
2841
  type: "text",
2498
2842
  text: JSON.stringify(
2499
- { ok: true, sessionId: input.sessionId, queued: true },
2843
+ { ok: true, sessionId, queued: true },
2500
2844
  null,
2501
2845
  2
2502
2846
  )
@@ -2520,25 +2864,28 @@ function registerAgentTools(server, opts) {
2520
2864
  "agent_output",
2521
2865
  "Tail the recent output of a session. Returns the last N lines of the ring buffer (stdout + stderr inter-leaved, newest last). Use this to read an agent's reply after `agent_prompt`.",
2522
2866
  {
2523
- sessionId: z.string().describe("Session id."),
2867
+ sessionId: sessionIdField,
2868
+ id: sessionIdAliasField,
2524
2869
  lastN: z.number().int().min(1).max(500).optional().describe("Max lines to return. Default 80, max 500."),
2525
2870
  clean: mcpBool.optional().describe(
2526
2871
  "Strip ANSI codes and drop framing/decoration lines, returning human-readable text."
2527
2872
  )
2528
2873
  },
2529
2874
  async (input) => {
2530
- const desc = registry.get(input.sessionId);
2875
+ const sessionId = resolveSessionIdArg(input);
2876
+ if (!sessionId) return missingSessionIdError("agent_output");
2877
+ const desc = registry.get(sessionId);
2531
2878
  if (!desc) {
2532
2879
  return {
2533
2880
  content: [
2534
- { type: "text", text: `agent_output: no session "${input.sessionId}"` }
2881
+ { type: "text", text: `agent_output: no session "${sessionId}"` }
2535
2882
  ],
2536
2883
  isError: true
2537
2884
  };
2538
2885
  }
2539
2886
  const limit = input.lastN ?? 80;
2540
2887
  const lines = [];
2541
- const unsub = registry.attach(input.sessionId, (line, _stream) => {
2888
+ const unsub = registry.attach(sessionId, (line, _stream) => {
2542
2889
  lines.push(line);
2543
2890
  });
2544
2891
  if (unsub) unsub();
@@ -2555,7 +2902,7 @@ function registerAgentTools(server, opts) {
2555
2902
  type: "text",
2556
2903
  text: JSON.stringify(
2557
2904
  {
2558
- sessionId: input.sessionId,
2905
+ sessionId,
2559
2906
  status: desc.status,
2560
2907
  lastOutputAt: desc.lastOutputAt,
2561
2908
  // Distinct liveness heartbeat: advances on ANY adapter-process
@@ -2585,15 +2932,18 @@ function registerAgentTools(server, opts) {
2585
2932
  "agent_kill",
2586
2933
  "Stop a session \u2014 SIGTERM the underlying child + close the agent protocol session. Use to free resources after the operator is done, or when a session is wedged.",
2587
2934
  {
2588
- sessionId: z.string().describe("Session id.")
2935
+ sessionId: sessionIdField,
2936
+ id: sessionIdAliasField
2589
2937
  },
2590
2938
  async (input) => {
2939
+ const sessionId = resolveSessionIdArg(input);
2940
+ if (!sessionId) return missingSessionIdError("agent_kill");
2591
2941
  if (callerScope) {
2592
2942
  const subtree = collectSubtree(
2593
2943
  callerScope.ownerSessionId,
2594
2944
  registry.list()
2595
2945
  );
2596
- if (!subtree.has(input.sessionId)) {
2946
+ if (!subtree.has(sessionId)) {
2597
2947
  return {
2598
2948
  content: [
2599
2949
  {
@@ -2601,9 +2951,9 @@ function registerAgentTools(server, opts) {
2601
2951
  text: JSON.stringify(
2602
2952
  {
2603
2953
  error: "orchestrator_session_out_of_scope",
2604
- message: `agent_kill: session "${input.sessionId}" is not in your subtree \u2014 a scoped orchestrator can only kill sessions it (transitively) spawned. No action taken.`,
2954
+ message: `agent_kill: session "${sessionId}" is not in your subtree \u2014 a scoped orchestrator can only kill sessions it (transitively) spawned. No action taken.`,
2605
2955
  ok: false,
2606
- sessionId: input.sessionId
2956
+ sessionId
2607
2957
  },
2608
2958
  null,
2609
2959
  2
@@ -2614,12 +2964,12 @@ function registerAgentTools(server, opts) {
2614
2964
  };
2615
2965
  }
2616
2966
  }
2617
- const ok = registry.kill(input.sessionId);
2967
+ const ok = registry.kill(sessionId);
2618
2968
  return {
2619
2969
  content: [
2620
2970
  {
2621
2971
  type: "text",
2622
- text: JSON.stringify({ ok, sessionId: input.sessionId }, null, 2)
2972
+ text: JSON.stringify({ ok, sessionId }, null, 2)
2623
2973
  }
2624
2974
  ]
2625
2975
  };
@@ -2729,9 +3079,10 @@ function registerExportSessionTool(server, ops) {
2729
3079
  "agent_export",
2730
3080
  "Export a clean, human-readable transcript of an agent session. Prefers the source the adapter already persists (claude-code: JSONL in ~/.claude/projects/; hermes: state.db in ~/.hermes/), falling back to agentproto's own daemon-captured events.jsonl for every other adapter (or once the native store is unreadable). Returns markdown (default) or JSON. Works on stopped and running sessions alike. Use after a long agent run to review the full conversation without the ANSI noise of the ring buffer.",
2731
3081
  {
2732
- sessionId: z.string().describe(
2733
- "agentproto session id (sess_xxx), adapter-native id, or session name."
3082
+ sessionId: z.string().optional().describe(
3083
+ "agentproto session id (sess_xxx), adapter-native id, or session name. Accepts either `sessionId` or its alias `id`."
2734
3084
  ),
3085
+ id: sessionIdAliasField,
2735
3086
  adapter: z.string().optional().describe(
2736
3087
  "Override adapter slug (e.g. 'claude-code', 'hermes') when the session is not in the registry. Required when passing a raw adapter-native id."
2737
3088
  ),
@@ -2746,8 +3097,10 @@ function registerExportSessionTool(server, ops) {
2746
3097
  )
2747
3098
  },
2748
3099
  async (input) => {
3100
+ const sessionId = resolveSessionIdArg(input);
3101
+ if (!sessionId) return missingSessionIdError("agent_export");
2749
3102
  const result = await doExport({
2750
- sessionId: input.sessionId,
3103
+ sessionId,
2751
3104
  registry: ops.registry,
2752
3105
  ...input.adapter ? { adapter: input.adapter } : {},
2753
3106
  ...input.cwd ? { cwd: input.cwd } : {},
@@ -3138,8 +3491,6 @@ function tokenizeCommand(s) {
3138
3491
  if (buf) out.push(buf);
3139
3492
  return out;
3140
3493
  }
3141
-
3142
- // src/session-restart-core.ts
3143
3494
  async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {}) {
3144
3495
  const augmented = opts.forceAgentResume ? prev : await augmentWithFsResume(prev);
3145
3496
  const strategy = opts.forceAgentResume ? { kind: "agent", resumeSessionId: prev.adapterSessionId } : decideRestartStrategy(augmented);
@@ -3161,6 +3512,33 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3161
3512
  let cwd = prev.cwd;
3162
3513
  if (!cwd) console.warn(`[restartAgentSession] no cwd on prior descriptor ${prev.id} \u2014 falling back to daemon's cwd ${process.cwd()}`);
3163
3514
  cwd ??= process.cwd();
3515
+ let authSpec;
3516
+ let authEcho;
3517
+ if (resolved.authDescriptor) {
3518
+ const configDefaults = opts.loadDefaultsConfig ? await opts.loadDefaultsConfig() : (await loadConfig()).defaults;
3519
+ const explicitAuthInput = prev.auth ? { mode: prev.auth.mode } : void 0;
3520
+ const spawnDefaults = resolveSpawnDefaults(configDefaults, adapterSlug, {
3521
+ auth: explicitAuthInput
3522
+ });
3523
+ const authModel = prev.model ?? resolved.defaultModel;
3524
+ const pinnedProvider = spawnDefaults.auth.provider;
3525
+ const resolvedProvider = pinnedProvider ?? resolved.authDescriptor.provider ?? (authModel ? getModelProvider(authModel) : void 0);
3526
+ const apiKeyStoreCredential = resolvedProvider && spawnDefaults.auth.explicit && spawnDefaults.auth.apiKeyCredential === void 0 ? await (0, providers_store_exports.getProviderKey)(resolvedProvider) : void 0;
3527
+ const result = resolveAuthSpec({
3528
+ descriptor: resolved.authDescriptor,
3529
+ ...authModel ? { model: authModel } : {},
3530
+ ...pinnedProvider ? { requestedProvider: pinnedProvider } : {},
3531
+ ...spawnDefaults.auth.requestedMode ? { requestedMode: spawnDefaults.auth.requestedMode } : {},
3532
+ explicit: spawnDefaults.auth.explicit,
3533
+ ...spawnDefaults.auth.subscriptionCredential !== void 0 ? { subscriptionCredential: spawnDefaults.auth.subscriptionCredential } : {},
3534
+ ...spawnDefaults.auth.apiKeyCredential !== void 0 ? { apiKeyConfigCredential: spawnDefaults.auth.apiKeyCredential } : {},
3535
+ ...apiKeyStoreCredential !== void 0 ? { apiKeyStoreCredential } : {}
3536
+ });
3537
+ if (result) {
3538
+ authSpec = result.spec;
3539
+ authEcho = result.echo;
3540
+ }
3541
+ }
3164
3542
  const spawnWithResume = async (resumeSessionId) => {
3165
3543
  let liveSessionId;
3166
3544
  const agentSession = await resolved.startSession({
@@ -3168,6 +3546,7 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3168
3546
  ...resumeSessionId ? { resumeSessionId } : {},
3169
3547
  ...prev.model ? { model: prev.model } : {},
3170
3548
  ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
3549
+ ...authSpec ? { auth: authSpec } : {},
3171
3550
  onActivity: () => {
3172
3551
  if (liveSessionId) registry.pulseActivity(liveSessionId);
3173
3552
  }
@@ -3180,7 +3559,19 @@ async function restartAgentSession(registry, resolveAgentAdapter, prev, opts = {
3180
3559
  ...prev.label ? { label: prev.label } : {},
3181
3560
  ...prev.mcpServers ? { mcpServers: prev.mcpServers } : {},
3182
3561
  ...prev.model ? { model: prev.model } : {},
3183
- ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {}
3562
+ ...resolved.commandPreview ? { commandPreview: resolved.commandPreview } : {},
3563
+ // Verifiability echo (never the credential) — see the auth
3564
+ // resolution block above. Absent when no credential resolved,
3565
+ // same as session-spawn.ts.
3566
+ ...authEcho?.fingerprint ? {
3567
+ auth: {
3568
+ mode: authEcho.authMode,
3569
+ fingerprint: authEcho.fingerprint,
3570
+ provider: authEcho.provider,
3571
+ credentialSource: authEcho.credentialSource,
3572
+ setEnv: authEcho.setEnv
3573
+ }
3574
+ } : {}
3184
3575
  });
3185
3576
  liveSessionId = desc2.id;
3186
3577
  return desc2;
@@ -3274,24 +3665,30 @@ function normalize3(parsed) {
3274
3665
  }
3275
3666
  return { version: IMPORTED_MCPS_VERSION, imports };
3276
3667
  }
3668
+ function plausibleContextUsed(contextSize, contextUsed) {
3669
+ if (contextUsed === void 0) return void 0;
3670
+ if (contextSize !== void 0 && contextUsed > contextSize) return void 0;
3671
+ return contextUsed;
3672
+ }
3277
3673
  var defaultResolver = (model) => resolvePricing(model);
3278
3674
  function tokenCost(tokens, pricePer1M) {
3279
3675
  return tokens === void 0 ? 0 : tokens * pricePer1M / 1e6;
3280
3676
  }
3281
- function deriveSessionUsage(input, resolve13 = defaultResolver) {
3677
+ function deriveSessionUsage(input, resolve12 = defaultResolver) {
3678
+ const contextUsed = plausibleContextUsed(input.contextSize, input.contextUsed);
3282
3679
  const base = {
3283
3680
  ...input.model !== void 0 ? { model: input.model } : {},
3284
3681
  ...input.tokensIn !== void 0 ? { tokensIn: input.tokensIn } : {},
3285
3682
  ...input.tokensOut !== void 0 ? { tokensOut: input.tokensOut } : {},
3286
3683
  ...input.contextSize !== void 0 ? { contextSize: input.contextSize } : {},
3287
- ...input.contextUsed !== void 0 ? { contextUsed: input.contextUsed } : {}
3684
+ ...contextUsed !== void 0 ? { contextUsed } : {}
3288
3685
  };
3289
3686
  if (input.adapterCostUsd !== void 0) {
3290
3687
  return { ...base, costUsd: input.adapterCostUsd, source: "adapter" };
3291
3688
  }
3292
3689
  const hasTokens = input.tokensIn !== void 0 || input.tokensOut !== void 0;
3293
3690
  if (hasTokens) {
3294
- const pricing = input.model !== void 0 ? resolve13(input.model) : void 0;
3691
+ const pricing = input.model !== void 0 ? resolve12(input.model) : void 0;
3295
3692
  if (!pricing) {
3296
3693
  return { ...base, source: "no-pricing" };
3297
3694
  }
@@ -3301,13 +3698,14 @@ function deriveSessionUsage(input, resolve13 = defaultResolver) {
3301
3698
  return { ...base, source: "none" };
3302
3699
  }
3303
3700
  function projectSessionUsage(desc) {
3701
+ const contextUsed = plausibleContextUsed(desc.contextSize, desc.contextUsed);
3304
3702
  return {
3305
3703
  ...desc.model !== void 0 ? { model: desc.model } : {},
3306
3704
  ...desc.costUsd !== void 0 ? { costUsd: desc.costUsd } : {},
3307
3705
  ...desc.tokensIn !== void 0 ? { tokensIn: desc.tokensIn } : {},
3308
3706
  ...desc.tokensOut !== void 0 ? { tokensOut: desc.tokensOut } : {},
3309
3707
  ...desc.contextSize !== void 0 ? { contextSize: desc.contextSize } : {},
3310
- ...desc.contextUsed !== void 0 ? { contextUsed: desc.contextUsed } : {},
3708
+ ...contextUsed !== void 0 ? { contextUsed } : {},
3311
3709
  source: desc.usageSource ?? "none"
3312
3710
  };
3313
3711
  }
@@ -3995,6 +4393,13 @@ function registerSessionTools(rawServer, opts) {
3995
4393
  }
3996
4394
  } catch {
3997
4395
  }
4396
+ } else if (!input.workspaceSlug) {
4397
+ try {
4398
+ const config = await loadWorkspacesConfig();
4399
+ const ws = findWorkspaceByPath(config, cwd);
4400
+ if (ws) resolvedSlug = ws.slug;
4401
+ } catch {
4402
+ }
3998
4403
  }
3999
4404
  if (!cwd) {
4000
4405
  return {
@@ -4184,8 +4589,8 @@ var okResult = (r) => ({
4184
4589
  content: [{ type: "text", text: JSON.stringify(r, null, 2) }],
4185
4590
  structuredContent: r
4186
4591
  });
4187
- var errResult = (text5) => ({
4188
- content: [{ type: "text", text: text5 }],
4592
+ var errResult = (text6) => ({
4593
+ content: [{ type: "text", text: text6 }],
4189
4594
  isError: true
4190
4595
  });
4191
4596
  function registerBrowserTools(server, opts) {
@@ -6850,8 +7255,8 @@ function expandTemplate(template, when) {
6850
7255
  const date = when.toISOString().slice(0, 10);
6851
7256
  return template.replace(/\{date\}/g, date);
6852
7257
  }
6853
- function preview(text5, max = 120) {
6854
- const flat = text5.replace(/\s+/g, " ").trim();
7258
+ function preview(text6, max = 120) {
7259
+ const flat = text6.replace(/\s+/g, " ").trim();
6855
7260
  return flat.length > max ? `${flat.slice(0, max - 1)}\u2026` : flat;
6856
7261
  }
6857
7262
  function writeCommandLogEntry(sessionId, entry, baseDir) {
@@ -6974,8 +7379,8 @@ function createTerminalTranscriptWriter(opts) {
6974
7379
  const stream = streams.get(sessionId);
6975
7380
  if (!stream) return Promise.resolve();
6976
7381
  streams.delete(sessionId);
6977
- return new Promise((resolve13) => {
6978
- stream.end(() => resolve13());
7382
+ return new Promise((resolve12) => {
7383
+ stream.end(() => resolve12());
6979
7384
  });
6980
7385
  },
6981
7386
  closeAll() {
@@ -6984,8 +7389,8 @@ function createTerminalTranscriptWriter(opts) {
6984
7389
  const stream = streams.get(sessionId);
6985
7390
  if (!stream) continue;
6986
7391
  closings.push(
6987
- new Promise((resolve13) => {
6988
- stream.end(() => resolve13());
7392
+ new Promise((resolve12) => {
7393
+ stream.end(() => resolve12());
6989
7394
  })
6990
7395
  );
6991
7396
  }
@@ -7007,6 +7412,35 @@ function normalizeAgentPromptOptions(raw) {
7007
7412
  }).filter((s) => typeof s === "string");
7008
7413
  return labels.length > 0 ? labels : void 0;
7009
7414
  }
7415
+ function normalizePermissionOptions(raw) {
7416
+ if (!Array.isArray(raw)) return [];
7417
+ const out = [];
7418
+ for (const o of raw) {
7419
+ if (!o || typeof o !== "object") continue;
7420
+ const r = o;
7421
+ if (typeof r.optionId !== "string") continue;
7422
+ out.push({
7423
+ optionId: r.optionId,
7424
+ ...typeof r.name === "string" ? { name: r.name } : {},
7425
+ ...typeof r.kind === "string" ? { kind: r.kind } : {}
7426
+ });
7427
+ }
7428
+ return out;
7429
+ }
7430
+ function selectPermissionOptionId(options, input) {
7431
+ if (input.optionId) {
7432
+ return options.some((o) => o.optionId === input.optionId) ? { optionId: input.optionId } : null;
7433
+ }
7434
+ const kindStarts = (prefix) => options.find((o) => typeof o.kind === "string" && o.kind.startsWith(prefix));
7435
+ if (input.decision === "approve") {
7436
+ const chosen = (input.scope === "always" ? options.find((o) => o.kind === "allow_always") : options.find((o) => o.kind === "allow_once")) ?? kindStarts("allow") ?? // Some agents label the grant "proceed"/"yes" without an `allow_` kind —
7437
+ // fall back to the first non-reject option before giving up.
7438
+ options.find((o) => !(typeof o.kind === "string" && o.kind.startsWith("reject")));
7439
+ return chosen ? { optionId: chosen.optionId } : null;
7440
+ }
7441
+ const reject = options.find((o) => o.kind === "reject_once") ?? kindStarts("reject") ?? kindStarts("deny");
7442
+ return reject ? { optionId: reject.optionId } : { cancelled: true };
7443
+ }
7010
7444
  function classifyBlockedOn(toolName) {
7011
7445
  if (!toolName) return void 0;
7012
7446
  const n = toolName.toLowerCase();
@@ -7014,6 +7448,10 @@ function classifyBlockedOn(toolName) {
7014
7448
  if (/^(command_execute|terminal_start)$|bash|terminal|command/.test(n)) return "command";
7015
7449
  return void 0;
7016
7450
  }
7451
+ function releaseBlockedOn(desc) {
7452
+ desc.blockedOn = void 0;
7453
+ desc.pendingToolCallId = void 0;
7454
+ }
7017
7455
  function isAbortError(err) {
7018
7456
  if (typeof err !== "object" || err === null) return false;
7019
7457
  const name = "name" in err ? err.name : void 0;
@@ -7095,11 +7533,12 @@ function createSessionsRegistry(opts) {
7095
7533
  const resumeAgent = opts?.resumeAgent;
7096
7534
  const sessionEvents = opts?.sessionEvents;
7097
7535
  const sessions = /* @__PURE__ */ new Map();
7536
+ const pendingPermissions = /* @__PURE__ */ new Map();
7098
7537
  let persistTimer = null;
7099
7538
  let nextSubId = 1;
7100
7539
  let shutdownDone = false;
7101
7540
  if (persist) {
7102
- loadHistorySnapshot(persistPath, sessions);
7541
+ loadHistorySnapshot(persistPath, sessions, sessionEvents);
7103
7542
  }
7104
7543
  const onProcessExit = () => {
7105
7544
  shutdownImpl();
@@ -7108,7 +7547,9 @@ function createSessionsRegistry(opts) {
7108
7547
  process.on("exit", onProcessExit);
7109
7548
  }
7110
7549
  const emitExited = (rt) => {
7111
- if (!sessionEvents || rt.exitedEmitted) return;
7550
+ if (rt.exitedEmitted) return;
7551
+ cancelPendingPermissionsForSession(rt);
7552
+ if (!sessionEvents) return;
7112
7553
  rt.exitedEmitted = true;
7113
7554
  sessionEvents.emit({
7114
7555
  type: "session:exited",
@@ -7116,9 +7557,72 @@ function createSessionsRegistry(opts) {
7116
7557
  exitCode: rt.desc.exitCode,
7117
7558
  status: rt.desc.status,
7118
7559
  label: rt.desc.label,
7119
- ts: (/* @__PURE__ */ new Date()).toISOString()
7560
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
7561
+ ...rt.desc.endedReason ? { reason: rt.desc.endedReason } : {}
7120
7562
  });
7121
7563
  };
7564
+ const refreshAwaitingPermission = (rt) => {
7565
+ let has = false;
7566
+ for (const p of pendingPermissions.values()) {
7567
+ if (p.sessionId === rt.desc.id) {
7568
+ has = true;
7569
+ break;
7570
+ }
7571
+ }
7572
+ if (has) rt.desc.awaitingPermission = true;
7573
+ else delete rt.desc.awaitingPermission;
7574
+ };
7575
+ const registerPendingPermission = (rt, evt) => {
7576
+ const id = evt.toolCallId;
7577
+ if (!id) return;
7578
+ const options = normalizePermissionOptions(evt.options);
7579
+ const toolName = evt.toolName;
7580
+ const text6 = evt.text ?? (toolName ? `Allow "${toolName}"?` : "The agent is requesting permission.");
7581
+ const pending = {
7582
+ id,
7583
+ sessionId: rt.desc.id,
7584
+ toolCallId: id,
7585
+ ...toolName ? { toolName } : {},
7586
+ text: text6,
7587
+ options,
7588
+ requestedAt: (/* @__PURE__ */ new Date()).toISOString()
7589
+ };
7590
+ pendingPermissions.set(id, pending);
7591
+ rt.desc.awaitingPermission = true;
7592
+ schedulePersist();
7593
+ if (sessionEvents) {
7594
+ sessionEvents.emit({
7595
+ type: "session:permission-request",
7596
+ sessionId: rt.desc.id,
7597
+ permissionId: id,
7598
+ ...toolName ? { toolName } : {},
7599
+ text: text6,
7600
+ ...rt.desc.label ? { label: rt.desc.label } : {},
7601
+ ts: pending.requestedAt
7602
+ });
7603
+ }
7604
+ };
7605
+ const cancelPendingPermissionsForSession = (rt) => {
7606
+ for (const [id, pending] of pendingPermissions) {
7607
+ if (pending.sessionId !== rt.desc.id) continue;
7608
+ pendingPermissions.delete(id);
7609
+ try {
7610
+ void rt.agentSession?.respondPermission?.(id, { cancelled: true });
7611
+ } catch {
7612
+ }
7613
+ if (sessionEvents) {
7614
+ sessionEvents.emit({
7615
+ type: "session:permission-resolved",
7616
+ sessionId: rt.desc.id,
7617
+ permissionId: id,
7618
+ decision: "cancelled",
7619
+ ...rt.desc.label ? { label: rt.desc.label } : {},
7620
+ ts: (/* @__PURE__ */ new Date()).toISOString()
7621
+ });
7622
+ }
7623
+ }
7624
+ delete rt.desc.awaitingPermission;
7625
+ };
7122
7626
  const schedulePersist = () => {
7123
7627
  if (!persist) return;
7124
7628
  if (persistTimer) clearTimeout(persistTimer);
@@ -7198,6 +7702,7 @@ function createSessionsRegistry(opts) {
7198
7702
  const projectEvent = (rt, evt) => {
7199
7703
  switch (evt.kind) {
7200
7704
  case "text-delta":
7705
+ releaseBlockedOn(rt.desc);
7201
7706
  if (evt.text) {
7202
7707
  const combined = rt.textBuf + evt.text;
7203
7708
  const lines = combined.split(/\r?\n/);
@@ -7223,17 +7728,18 @@ function createSessionsRegistry(opts) {
7223
7728
  rt.desc.blockedOn = blocked;
7224
7729
  rt.desc.pendingToolCallId = evt.toolCallId;
7225
7730
  }
7226
- appendLine(
7227
- rt,
7228
- `\x1B[36m[tool] ${formatToolCall(evt.toolName ?? "?", evt.arguments)}\x1B[0m`,
7229
- "stdout"
7230
- );
7731
+ if (!evt.isUpdate) {
7732
+ appendLine(
7733
+ rt,
7734
+ `\x1B[36m[tool] ${formatToolCall(evt.toolName ?? "?", evt.arguments)}\x1B[0m`,
7735
+ "stdout"
7736
+ );
7737
+ }
7231
7738
  break;
7232
7739
  }
7233
7740
  case "tool-result": {
7234
7741
  if (rt.desc.blockedOn && evt.toolCallId === rt.desc.pendingToolCallId) {
7235
- rt.desc.blockedOn = void 0;
7236
- rt.desc.pendingToolCallId = void 0;
7742
+ releaseBlockedOn(rt.desc);
7237
7743
  }
7238
7744
  const summary = formatToolResult(evt.toolName, evt.result, evt.isError ?? false);
7239
7745
  if (summary) {
@@ -7257,7 +7763,12 @@ function createSessionsRegistry(opts) {
7257
7763
  source: "structured"
7258
7764
  };
7259
7765
  }
7260
- appendLine(rt, `\x1B[33m[awaiting input]\x1B[0m`, "stdout");
7766
+ if (rt.permissionHold) {
7767
+ registerPendingPermission(rt, evt);
7768
+ appendLine(rt, `\x1B[33m[permission] ${evt.text ?? evt.toolName ?? "requesting permission"}\x1B[0m`, "stdout");
7769
+ } else {
7770
+ appendLine(rt, `\x1B[33m[awaiting input]\x1B[0m`, "stdout");
7771
+ }
7261
7772
  break;
7262
7773
  }
7263
7774
  case "turn-end": {
@@ -7282,6 +7793,7 @@ function createSessionsRegistry(opts) {
7282
7793
  break;
7283
7794
  }
7284
7795
  case "error": {
7796
+ releaseBlockedOn(rt.desc);
7285
7797
  const code = typeof evt.error?.code === "number" ? ` (code ${evt.error.code})` : "";
7286
7798
  appendLine(
7287
7799
  rt,
@@ -7314,7 +7826,10 @@ function createSessionsRegistry(opts) {
7314
7826
  // reported cost/tokens are available live to session_list / session_usage.
7315
7827
  case "usage_update": {
7316
7828
  if (typeof evt.size === "number" && evt.size > 0) rt.desc.contextSize = evt.size;
7317
- if (typeof evt.used === "number" && evt.used > 0) rt.desc.contextUsed = evt.used;
7829
+ if (typeof evt.used === "number" && evt.used > 0) {
7830
+ const used = plausibleContextUsed(rt.desc.contextSize, evt.used);
7831
+ if (used !== void 0) rt.desc.contextUsed = used;
7832
+ }
7318
7833
  if (evt.cost) {
7319
7834
  rt.desc.costUsd = evt.cost.amount;
7320
7835
  rt.adapterReportedCost = true;
@@ -7344,13 +7859,13 @@ function createSessionsRegistry(opts) {
7344
7859
  }
7345
7860
  return rt;
7346
7861
  };
7347
- const waitForTurnSettled = (rt, id) => {
7862
+ const waitForTurnSettled = (rt, id, caller) => {
7348
7863
  if (!rt.busy) return Promise.resolve();
7349
- return new Promise((resolve13, reject) => {
7864
+ return new Promise((resolve12, reject) => {
7350
7865
  const onBusy = (busy) => {
7351
7866
  if (busy) return;
7352
7867
  cleanup();
7353
- resolve13();
7868
+ resolve12();
7354
7869
  };
7355
7870
  const cleanup = () => {
7356
7871
  clearTimeout(timer);
@@ -7360,28 +7875,28 @@ function createSessionsRegistry(opts) {
7360
7875
  cleanup();
7361
7876
  reject(
7362
7877
  new Error(
7363
- `enqueuePrompt: session "${id}" did not settle after interrupt within ${INTERRUPT_SETTLE_TIMEOUT_MS}ms`
7878
+ `${caller}: session "${id}" did not settle after interrupt within ${INTERRUPT_SETTLE_TIMEOUT_MS}ms`
7364
7879
  )
7365
7880
  );
7366
7881
  }, INTERRUPT_SETTLE_TIMEOUT_MS);
7367
7882
  rt.emitter.on("busy", onBusy);
7368
7883
  });
7369
7884
  };
7370
- const interruptInFlightTurn = async (rt, id) => {
7885
+ const interruptInFlightTurn = async (rt, id, caller) => {
7371
7886
  const session = rt.agentSession;
7372
7887
  if (!session) {
7373
7888
  throw new Error(
7374
- `enqueuePrompt: session "${id}" is mid-turn but has no live agent session to cancel`
7889
+ `${caller}: session "${id}" is mid-turn but has no live agent session to cancel`
7375
7890
  );
7376
7891
  }
7377
7892
  try {
7378
7893
  await session.cancel();
7379
7894
  } catch (err) {
7380
7895
  throw new Error(
7381
- `enqueuePrompt: session "${id}" does not support interrupt \u2014 cancelling the in-flight turn failed: ${err instanceof Error ? err.message : String(err)}`
7896
+ `${caller}: session "${id}" does not support interrupt \u2014 cancelling the in-flight turn failed: ${err instanceof Error ? err.message : String(err)}`
7382
7897
  );
7383
7898
  }
7384
- await waitForTurnSettled(rt, id);
7899
+ await waitForTurnSettled(rt, id, caller);
7385
7900
  };
7386
7901
  const maybeResumeAgent = async (rt) => {
7387
7902
  if (rt.agentSession) return;
@@ -7469,12 +7984,13 @@ function createSessionsRegistry(opts) {
7469
7984
  rt.emitter.emit("busy", true);
7470
7985
  rt.desc.awaitingInput = false;
7471
7986
  rt.desc.awaitingQuestion = void 0;
7472
- rt.desc.blockedOn = void 0;
7473
- rt.desc.pendingToolCallId = void 0;
7987
+ releaseBlockedOn(rt.desc);
7474
7988
  let turnCompleted = false;
7475
7989
  let sawTurnEnd = false;
7476
7990
  let abnormalReason;
7477
7991
  let turnEndReason;
7992
+ let sawAssistantText = false;
7993
+ let sawToolCall = false;
7478
7994
  try {
7479
7995
  appendLine(
7480
7996
  rt,
@@ -7486,6 +8002,8 @@ function createSessionsRegistry(opts) {
7486
8002
  for await (const evt of rt.agentSession.send(wrapped)) {
7487
8003
  transcriptWriter.recordEvent(rt.desc.id, evt);
7488
8004
  projectEvent(rt, evt);
8005
+ if (evt.kind === "text-delta" && evt.text?.trim()) sawAssistantText = true;
8006
+ else if (evt.kind === "tool-call") sawToolCall = true;
7489
8007
  if (evt.kind === "turn-end") {
7490
8008
  sawTurnEnd = true;
7491
8009
  turnEndReason = evt.reason;
@@ -7512,8 +8030,7 @@ function createSessionsRegistry(opts) {
7512
8030
  rt.busy = false;
7513
8031
  rt.desc.busy = false;
7514
8032
  rt.emitter.emit("busy", false);
7515
- rt.desc.blockedOn = void 0;
7516
- rt.desc.pendingToolCallId = void 0;
8033
+ releaseBlockedOn(rt.desc);
7517
8034
  if (!sawTurnEnd) {
7518
8035
  const reason = abnormalReason ?? (rt.desc.status === "killed" ? "aborted" : "exited");
7519
8036
  const synthetic = { kind: "turn-end", reason };
@@ -7566,6 +8083,14 @@ function createSessionsRegistry(opts) {
7566
8083
  void transcriptWriter.close(rt.desc.id);
7567
8084
  tracedSessions.delete(rt.desc.id);
7568
8085
  }
8086
+ const emptyTurn = !sawAssistantText && !sawToolCall && !(rt.desc.awaitingInput ?? false);
8087
+ if (emptyTurn) {
8088
+ appendLine(
8089
+ rt,
8090
+ `\x1B[33m[warning] empty turn \u2014 no assistant output, no tool call, cost ${rt.desc.costUsd !== void 0 ? `$${rt.desc.costUsd}` : "unknown"}. Likely an invalid model id or a provider that returned nothing; verify the model slug (agentproto models <adapter>).\x1B[0m`,
8091
+ "stderr"
8092
+ );
8093
+ }
7569
8094
  if (sessionEvents) {
7570
8095
  if (rt.desc.awaitingInput && !rt.desc.awaitingQuestion) {
7571
8096
  rt.desc.awaitingQuestion = deriveHeuristicQuestion(rt.recentLines);
@@ -7578,7 +8103,8 @@ function createSessionsRegistry(opts) {
7578
8103
  label: rt.desc.label,
7579
8104
  ts,
7580
8105
  ...rt.desc.awaitingQuestion ? { question: rt.desc.awaitingQuestion } : {},
7581
- ...turnEndReason ? { reason: turnEndReason } : {}
8106
+ ...turnEndReason ? { reason: turnEndReason } : {},
8107
+ ...emptyTurn ? { empty: true } : {}
7582
8108
  });
7583
8109
  if (rt.desc.awaitingInput) {
7584
8110
  sessionEvents.emit({
@@ -7608,8 +8134,8 @@ function createSessionsRegistry(opts) {
7608
8134
  };
7609
8135
  const wireOutputStreams = (rt) => {
7610
8136
  const onChunk = (stream) => (chunk) => {
7611
- const text5 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
7612
- for (const line of text5.split(/\r?\n/)) {
8137
+ const text6 = typeof chunk === "string" ? chunk : chunk.toString("utf8");
8138
+ for (const line of text6.split(/\r?\n/)) {
7613
8139
  if (line.length > 0) appendLine(rt, line, stream);
7614
8140
  }
7615
8141
  };
@@ -7764,6 +8290,7 @@ function createSessionsRegistry(opts) {
7764
8290
  ...input.parentSessionId ? { parentSessionId: input.parentSessionId } : {},
7765
8291
  depth: input.depth ?? 0,
7766
8292
  ...input.model ? { model: input.model } : {},
8293
+ ...input.auth ? { auth: input.auth } : {},
7767
8294
  ...priorCommandSessionId ? { priorCommandSessionId } : {},
7768
8295
  ...input.remote ? { remote: true } : {},
7769
8296
  ...input.sandboxId ? { sandboxId: input.sandboxId } : {},
@@ -7784,7 +8311,8 @@ function createSessionsRegistry(opts) {
7784
8311
  textBuf: "",
7785
8312
  thoughtBuf: "",
7786
8313
  maxCostUsd: input.maxCostUsd,
7787
- readUsage: input.readUsage
8314
+ readUsage: input.readUsage,
8315
+ ...input.permissionHold ? { permissionHold: true } : {}
7788
8316
  };
7789
8317
  rt.emitter.setMaxListeners(50);
7790
8318
  sessions.set(id, rt);
@@ -7983,8 +8511,11 @@ function createSessionsRegistry(opts) {
7983
8511
  schedulePersist();
7984
8512
  return desc;
7985
8513
  },
7986
- async sendPrompt(id, message) {
8514
+ async sendPrompt(id, message, opts2) {
7987
8515
  const rtPre = sessions.get(id);
8516
+ if (opts2?.interrupt && rtPre?.busy) {
8517
+ await interruptInFlightTurn(rtPre, id, "sendPrompt");
8518
+ }
7988
8519
  if (rtPre) await maybeResumeAgent(rtPre);
7989
8520
  const rt = validateAgentTurn(id, "sendPrompt");
7990
8521
  await runAgentTurn(rt, message);
@@ -7995,7 +8526,7 @@ function createSessionsRegistry(opts) {
7995
8526
  throw new Error(`enqueuePrompt: no session "${id}"`);
7996
8527
  }
7997
8528
  if (opts2?.interrupt && rtPre.busy) {
7998
- await interruptInFlightTurn(rtPre, id);
8529
+ await interruptInFlightTurn(rtPre, id, "enqueuePrompt");
7999
8530
  }
8000
8531
  await maybeResumeAgent(rtPre);
8001
8532
  const rt = validateAgentTurn(id, "enqueuePrompt");
@@ -8032,6 +8563,9 @@ function createSessionsRegistry(opts) {
8032
8563
  rt.emitter.on("line", handler);
8033
8564
  return () => rt.emitter.off("line", handler);
8034
8565
  },
8566
+ subscribeToRecords(id, onRecord) {
8567
+ return baseTranscriptWriter.subscribe(id, onRecord);
8568
+ },
8035
8569
  attachPty(id, initial, onData, onExit) {
8036
8570
  const rt = sessions.get(id);
8037
8571
  if (!rt || !rt.pty || !rt.ptySubscribers) return null;
@@ -8126,6 +8660,74 @@ function createSessionsRegistry(opts) {
8126
8660
  emitExited(rt);
8127
8661
  return true;
8128
8662
  },
8663
+ listPendingPermissions(filter) {
8664
+ const all = Array.from(pendingPermissions.values());
8665
+ const scoped = filter?.sessionId ? all.filter((p) => p.sessionId === filter.sessionId) : all;
8666
+ return scoped.map((p) => ({ ...p, options: p.options.map((o) => ({ ...o })) }));
8667
+ },
8668
+ async respondPermission(id, input) {
8669
+ const pending = pendingPermissions.get(id);
8670
+ if (!pending) {
8671
+ return {
8672
+ ok: false,
8673
+ error: "not_found",
8674
+ message: `no pending permission "${id}" (unknown or already resolved)`
8675
+ };
8676
+ }
8677
+ const rt = sessions.get(pending.sessionId);
8678
+ if (!rt || !rt.agentSession) {
8679
+ pendingPermissions.delete(id);
8680
+ if (rt) refreshAwaitingPermission(rt);
8681
+ return {
8682
+ ok: false,
8683
+ error: "session_gone",
8684
+ message: `session "${pending.sessionId}" for permission "${id}" is no longer alive`
8685
+ };
8686
+ }
8687
+ const respond = rt.agentSession.respondPermission;
8688
+ if (!respond) {
8689
+ return {
8690
+ ok: false,
8691
+ error: "unsupported",
8692
+ message: `session "${pending.sessionId}" driver does not support held permissions`
8693
+ };
8694
+ }
8695
+ const selected = selectPermissionOptionId(pending.options, input);
8696
+ if (selected === null) {
8697
+ return {
8698
+ ok: false,
8699
+ error: "no_matching_option",
8700
+ message: `permission "${id}" offers no ${input.decision === "approve" ? "allow" : "reject"}-flavored option; pass an explicit optionId (one of: ${pending.options.map((o) => o.optionId).join(", ")})`
8701
+ };
8702
+ }
8703
+ pendingPermissions.delete(id);
8704
+ const chosenOptionId = "optionId" in selected ? selected.optionId : void 0;
8705
+ delete rt.desc.awaitingInput;
8706
+ rt.desc.awaitingQuestion = void 0;
8707
+ refreshAwaitingPermission(rt);
8708
+ const okResolved = await respond(id, selected);
8709
+ if (sessionEvents) {
8710
+ sessionEvents.emit({
8711
+ type: "session:permission-resolved",
8712
+ sessionId: pending.sessionId,
8713
+ permissionId: id,
8714
+ decision: input.decision,
8715
+ ...chosenOptionId ? { optionId: chosenOptionId } : {},
8716
+ ...rt.desc.label ? { label: rt.desc.label } : {},
8717
+ ts: (/* @__PURE__ */ new Date()).toISOString()
8718
+ });
8719
+ }
8720
+ schedulePersist();
8721
+ if (!okResolved) {
8722
+ return { ok: true, permission: pending, decision: input.decision, ...chosenOptionId ? { optionId: chosenOptionId } : {} };
8723
+ }
8724
+ return {
8725
+ ok: true,
8726
+ permission: pending,
8727
+ decision: input.decision,
8728
+ ...chosenOptionId ? { optionId: chosenOptionId } : {}
8729
+ };
8730
+ },
8129
8731
  forget(id) {
8130
8732
  const rt = sessions.get(id);
8131
8733
  if (!rt) return false;
@@ -8157,6 +8759,8 @@ function createSessionsRegistry(opts) {
8157
8759
  if (rt.desc.status === "running" || rt.desc.status === "starting") {
8158
8760
  rt.desc.status = "killed";
8159
8761
  rt.desc.endedAt = nowIso;
8762
+ rt.desc.endedReason = "daemon-restart";
8763
+ clearInFlightFlags(rt.desc);
8160
8764
  if (rt.agentSession) {
8161
8765
  recordExitUsageSnapshot(rt);
8162
8766
  void rt.agentSession.close().catch(() => void 0);
@@ -8168,6 +8772,7 @@ function createSessionsRegistry(opts) {
8168
8772
  }
8169
8773
  }
8170
8774
  rt.child?.kill("SIGTERM");
8775
+ emitExited(rt);
8171
8776
  }
8172
8777
  }
8173
8778
  void transcriptWriter.closeAll();
@@ -8191,7 +8796,15 @@ function createSessionsRegistry(opts) {
8191
8796
  sessions.clear();
8192
8797
  }
8193
8798
  }
8194
- function loadHistorySnapshot(persistPath, sessions) {
8799
+ function clearInFlightFlags(desc) {
8800
+ desc.busy = false;
8801
+ desc.awaitingInput = false;
8802
+ desc.awaitingQuestion = void 0;
8803
+ delete desc.awaitingPermission;
8804
+ desc.blockedOn = void 0;
8805
+ desc.pendingToolCallId = void 0;
8806
+ }
8807
+ function loadHistorySnapshot(persistPath, sessions, sessionEvents) {
8195
8808
  let raw;
8196
8809
  try {
8197
8810
  raw = readFileSync(persistPath, "utf8");
@@ -8216,8 +8829,14 @@ function loadHistorySnapshot(persistPath, sessions) {
8216
8829
  const reclassified = wasAlive ? {
8217
8830
  ...desc,
8218
8831
  status: "killed",
8219
- endedAt: desc.endedAt ?? now
8832
+ endedAt: desc.endedAt ?? now,
8833
+ endedReason: "daemon-restart"
8220
8834
  } : desc;
8835
+ clearInFlightFlags(reclassified);
8836
+ reclassified.contextUsed = plausibleContextUsed(
8837
+ reclassified.contextSize,
8838
+ reclassified.contextUsed
8839
+ );
8221
8840
  const rt = {
8222
8841
  desc: reclassified,
8223
8842
  recentLines: [],
@@ -8226,10 +8845,22 @@ function loadHistorySnapshot(persistPath, sessions) {
8226
8845
  emitter: new EventEmitter(),
8227
8846
  busy: false,
8228
8847
  textBuf: "",
8229
- thoughtBuf: ""
8848
+ thoughtBuf: "",
8849
+ exitedEmitted: wasAlive
8230
8850
  };
8231
8851
  rt.emitter.setMaxListeners(50);
8232
8852
  sessions.set(desc.id, rt);
8853
+ if (wasAlive) {
8854
+ sessionEvents?.emit({
8855
+ type: "session:exited",
8856
+ sessionId: reclassified.id,
8857
+ exitCode: reclassified.exitCode,
8858
+ status: "killed",
8859
+ ...reclassified.label ? { label: reclassified.label } : {},
8860
+ reason: "daemon-restart",
8861
+ ts: now
8862
+ });
8863
+ }
8233
8864
  }
8234
8865
  }
8235
8866
  function quoteArg(arg) {
@@ -8318,7 +8949,7 @@ async function monitorSessionWait(opts) {
8318
8949
  };
8319
8950
  }
8320
8951
  }
8321
- return new Promise((resolve13) => {
8952
+ return new Promise((resolve12) => {
8322
8953
  const unsubs = [];
8323
8954
  let settled = false;
8324
8955
  const finish = (result) => {
@@ -8326,7 +8957,7 @@ async function monitorSessionWait(opts) {
8326
8957
  settled = true;
8327
8958
  clearTimeout(timer);
8328
8959
  for (const u of unsubs) u();
8329
- resolve13(result);
8960
+ resolve12(result);
8330
8961
  };
8331
8962
  const relevantTypes = targetEvent === "any" ? ["session:turn-end", "session:awaiting-input", "session:exited"] : targetEvent === "turn-end" ? ["session:turn-end", "session:awaiting-input"] : targetEvent === "awaiting-input" ? ["session:awaiting-input"] : ["session:exited"];
8332
8963
  const idSet = new Set(resolvedIds);
@@ -8362,13 +8993,13 @@ async function monitorPolicyWait(opts) {
8362
8993
  if (isSettledStatus(initial.status)) {
8363
8994
  return { timedOut: false, state: initial };
8364
8995
  }
8365
- return new Promise((resolve13) => {
8996
+ return new Promise((resolve12) => {
8366
8997
  let settled = false;
8367
8998
  const timer = setTimeout(() => {
8368
8999
  if (settled) return;
8369
9000
  settled = true;
8370
9001
  unsub();
8371
- resolve13({ timedOut: true });
9002
+ resolve12({ timedOut: true });
8372
9003
  }, timeoutMs);
8373
9004
  const unsub = supervisor.onSettle((id) => {
8374
9005
  if (id !== policyId) return;
@@ -8378,7 +9009,7 @@ async function monitorPolicyWait(opts) {
8378
9009
  settled = true;
8379
9010
  clearTimeout(timer);
8380
9011
  unsub();
8381
- resolve13({ timedOut: false, state });
9012
+ resolve12({ timedOut: false, state });
8382
9013
  });
8383
9014
  });
8384
9015
  }
@@ -8398,7 +9029,7 @@ function registerOrchestrationTools(rawServer, opts) {
8398
9029
  "Opaque cursor from a prior call's `nextCursor`. Omit (or pass 0) to start from the current tail \u2014 returns events emitted after this call."
8399
9030
  ),
8400
9031
  sessionIds: z.array(z.string()).optional().describe("Filter to these session ids. Omit \u2192 all sessions."),
8401
- types: z.array(z.enum(["turn-end", "awaiting-input", "exited", "command-done", "policy:passed", "policy:failed", "policy:commit-ready", "policy:committed", "cron:fired", "cron:succeeded", "cron:failed"])).optional().describe("Filter to these event types. Omit \u2192 all types."),
9032
+ types: z.array(z.enum(["turn-end", "awaiting-input", "permission-request", "permission-resolved", "exited", "command-done", "policy:passed", "policy:failed", "policy:commit-ready", "policy:committed", "cron:fired", "cron:succeeded", "cron:failed"])).optional().describe("Filter to these event types. Omit \u2192 all types."),
8402
9033
  limit: z.number().int().min(1).max(200).optional().describe("Max events to return. Default 50.")
8403
9034
  },
8404
9035
  async (input) => {
@@ -8415,6 +9046,88 @@ function registerOrchestrationTools(rawServer, opts) {
8415
9046
  };
8416
9047
  }
8417
9048
  );
9049
+ const isSessionInScope = (sessionId) => {
9050
+ if (!callerScope) return true;
9051
+ if (!callerScope.ownerSessionId) return false;
9052
+ return collectSubtree(callerScope.ownerSessionId, registry.list()).has(sessionId);
9053
+ };
9054
+ const enrichPermission2 = (p) => {
9055
+ const desc = registry.get(p.sessionId);
9056
+ const ageMs = Date.now() - new Date(p.requestedAt).getTime();
9057
+ return {
9058
+ ...p,
9059
+ ...desc?.adapterSlug ? { adapter: desc.adapterSlug } : {},
9060
+ ...desc?.label ? { sessionLabel: desc.label } : {},
9061
+ ...desc?.command ? { sessionTitle: desc.command } : {},
9062
+ ageMs: ageMs >= 0 ? ageMs : 0
9063
+ };
9064
+ };
9065
+ server.tool(
9066
+ "permissions_list",
9067
+ "List permission requests currently HELD across all permission-hold sessions (spawned with `permissionHold`). Each entry: id, sessionId, session adapter/title, the tool being requested, the offered options, and age. Resolve one with `permissions_respond`. Optionally filter by `sessionId`. Empty list = nothing is waiting on a decision.",
9068
+ {
9069
+ sessionId: z.string().optional().describe("Filter to one session's pending permissions. Omit \u2192 all sessions.")
9070
+ },
9071
+ async (input) => {
9072
+ const pending = registry.listPendingPermissions(input.sessionId ? { sessionId: input.sessionId } : void 0).filter((p) => isSessionInScope(p.sessionId)).map(enrichPermission2);
9073
+ return {
9074
+ content: [{ type: "text", text: JSON.stringify({ permissions: pending }, null, 2) }]
9075
+ };
9076
+ }
9077
+ );
9078
+ server.tool(
9079
+ "permissions_respond",
9080
+ 'Resolve a permission request parked by a permission-hold session (see `permissions_list`). `decision:"approve"` selects an allow option (allow-always when `scope:"always"` is offered, else allow-once); `decision:"deny"` selects a reject option (or cancels the request when none is offered). An explicit `optionId` overrides the decision\u2192option mapping. Errors clearly on an unknown/already-resolved id. The agent\'s turn unblocks with the chosen outcome.',
9081
+ {
9082
+ id: z.string().describe("Pending permission id from `permissions_list`."),
9083
+ decision: z.enum(["approve", "deny"]).describe("approve \u2192 an allow option; deny \u2192 a reject option / cancel."),
9084
+ optionId: z.string().optional().describe("Explicit offered optionId \u2014 wins over `decision` mapping."),
9085
+ scope: z.enum(["once", "always"]).optional().describe("For approve: prefer allow-always when the request offers it.")
9086
+ },
9087
+ async (input) => {
9088
+ const pending = registry.listPendingPermissions().find((p) => p.id === input.id);
9089
+ if (!pending || !isSessionInScope(pending.sessionId)) {
9090
+ return {
9091
+ content: [
9092
+ {
9093
+ type: "text",
9094
+ text: JSON.stringify({ error: "not_found", message: `no pending permission "${input.id}"` })
9095
+ }
9096
+ ],
9097
+ isError: true
9098
+ };
9099
+ }
9100
+ const result = await registry.respondPermission(input.id, {
9101
+ decision: input.decision,
9102
+ ...input.optionId ? { optionId: input.optionId } : {},
9103
+ ...input.scope ? { scope: input.scope } : {}
9104
+ });
9105
+ if (!result.ok) {
9106
+ return {
9107
+ content: [{ type: "text", text: JSON.stringify({ error: result.error, message: result.message }) }],
9108
+ isError: true
9109
+ };
9110
+ }
9111
+ return {
9112
+ content: [
9113
+ {
9114
+ type: "text",
9115
+ text: JSON.stringify(
9116
+ {
9117
+ ok: true,
9118
+ id: input.id,
9119
+ sessionId: result.permission.sessionId,
9120
+ decision: result.decision,
9121
+ ...result.optionId ? { optionId: result.optionId } : {}
9122
+ },
9123
+ null,
9124
+ 2
9125
+ )
9126
+ }
9127
+ ]
9128
+ };
9129
+ }
9130
+ );
8418
9131
  const { routineRunner } = opts;
8419
9132
  if (routineRunner) {
8420
9133
  server.tool(
@@ -8899,7 +9612,16 @@ function registerOrchestrationTools(rawServer, opts) {
8899
9612
  "session_monitor",
8900
9613
  "Multiplexed long-poll: block until ANY of the listed sessions fires a lifecycle event. Returns immediately when a session is already in the target state. Eliminates polling in multi-session fan-in orchestration \u2014 one call replaces N concurrent waitForTurnEnd calls.",
8901
9614
  {
8902
- sessionIds: z.array(z.string()).min(1).max(20).describe("Session ids (or names) to watch. Returns on the first hit."),
9615
+ // Accept the natural shapes an agent already has on hand: a single id
9616
+ // (the `id` agent_start returns, or `sessionId` from the drive tools),
9617
+ // or the fan-in array. `sessionIds` additionally tolerates a bare
9618
+ // string (not just an array) so a one-session monitor doesn't force
9619
+ // array-wrapping. All three coalesce into the watched-id list below.
9620
+ sessionIds: jsonTolerant(z.union([z.string(), z.array(z.string())])).optional().describe(
9621
+ "Session ids (or names) to watch \u2014 an array for fan-in, or a single id. Returns on the first hit. Provide this OR `sessionId`/`id`."
9622
+ ),
9623
+ sessionId: z.string().optional().describe("Single session id/name \u2014 singular alias for `sessionIds`."),
9624
+ id: z.string().optional().describe("Alias for `sessionId` \u2014 the `id` returned by agent_start."),
8903
9625
  timeoutMs: z.number().int().min(1e3).max(49e3).optional().describe("Max wait in ms. Default 25 000. Stays under MCP request timeout."),
8904
9626
  event: z.enum(["turn-end", "awaiting-input", "exited", "any"]).optional().describe(
8905
9627
  "Event type to wait for. Default 'any'. 'turn-end' also matches 'awaiting-input' (both signal end-of-turn)."
@@ -8911,14 +9633,43 @@ function registerOrchestrationTools(rawServer, opts) {
8911
9633
  async (input) => {
8912
9634
  const timeout = input.timeoutMs ?? 25e3;
8913
9635
  const targetEvent = input.event ?? "any";
8914
- const result = await monitorSessionWait({
8915
- registry,
8916
- sessionEvents,
8917
- eventRing,
8918
- sessionIds: input.sessionIds,
8919
- event: targetEvent,
8920
- timeoutMs: timeout,
8921
- ...input.since !== void 0 ? { since: input.since } : {}
9636
+ const fromSessionIds = input.sessionIds === void 0 ? [] : Array.isArray(input.sessionIds) ? input.sessionIds : [input.sessionIds];
9637
+ const singular = input.sessionId ?? input.id;
9638
+ const sessionIds = [...fromSessionIds, ...singular ? [singular] : []];
9639
+ if (sessionIds.length === 0) {
9640
+ return {
9641
+ content: [
9642
+ {
9643
+ type: "text",
9644
+ text: JSON.stringify({
9645
+ error: "session_monitor: no sessions to watch \u2014 pass `sessionIds` (array or single id) or `sessionId`/`id`."
9646
+ })
9647
+ }
9648
+ ],
9649
+ isError: true
9650
+ };
9651
+ }
9652
+ if (sessionIds.length > 20) {
9653
+ return {
9654
+ content: [
9655
+ {
9656
+ type: "text",
9657
+ text: JSON.stringify({
9658
+ error: `session_monitor: too many sessions (${sessionIds.length}); max 20 per call.`
9659
+ })
9660
+ }
9661
+ ],
9662
+ isError: true
9663
+ };
9664
+ }
9665
+ const result = await monitorSessionWait({
9666
+ registry,
9667
+ sessionEvents,
9668
+ eventRing,
9669
+ sessionIds,
9670
+ event: targetEvent,
9671
+ timeoutMs: timeout,
9672
+ ...input.since !== void 0 ? { since: input.since } : {}
8922
9673
  });
8923
9674
  const payload = { ...result };
8924
9675
  if (result.source === "ring" && input.since !== void 0) {
@@ -9777,6 +10528,28 @@ async function startHttpServer(opts) {
9777
10528
  );
9778
10529
  if (handled) return;
9779
10530
  }
10531
+ if (opts.sessions && path.startsWith("/permissions")) {
10532
+ if ((req.method ?? "GET") !== "GET") {
10533
+ const gate = checkSessionsToken(req);
10534
+ if (gate !== "ok") {
10535
+ rejectUnauthorizedSession(req, res, gate);
10536
+ return;
10537
+ }
10538
+ }
10539
+ const handled = await handlePermissions(req, res, path, opts.sessions);
10540
+ if (handled) return;
10541
+ }
10542
+ if (opts.pairings && path.startsWith("/pairings")) {
10543
+ if ((req.method ?? "GET") !== "GET") {
10544
+ const gate = checkSessionsToken(req);
10545
+ if (gate !== "ok") {
10546
+ rejectUnauthorizedSession(req, res, gate);
10547
+ return;
10548
+ }
10549
+ }
10550
+ const handled = await handlePairings(req, res, path, opts.pairings);
10551
+ if (handled) return;
10552
+ }
9780
10553
  if (opts.cronScheduler && path.startsWith("/cron")) {
9781
10554
  const handled = await handleCron(req, res, path, opts.cronScheduler);
9782
10555
  if (handled) return;
@@ -9842,16 +10615,16 @@ async function startHttpServer(opts) {
9842
10615
  });
9843
10616
  });
9844
10617
  const bind = opts.bind ?? "127.0.0.1";
9845
- await new Promise((resolve13, reject) => {
10618
+ await new Promise((resolve12, reject) => {
9846
10619
  server.once("error", reject);
9847
- server.listen(opts.port, bind, () => resolve13());
10620
+ server.listen(opts.port, bind, () => resolve12());
9848
10621
  });
9849
10622
  return {
9850
10623
  url: `http://${bind}:${opts.port}`,
9851
10624
  async stop() {
9852
10625
  wss.close();
9853
10626
  server.closeAllConnections();
9854
- await new Promise((resolve13) => server.close(() => resolve13()));
10627
+ await new Promise((resolve12) => server.close(() => resolve12()));
9855
10628
  }
9856
10629
  };
9857
10630
  }
@@ -9969,6 +10742,33 @@ function clampInt(raw, fallback, min, max) {
9969
10742
  if (n > max) return max;
9970
10743
  return n;
9971
10744
  }
10745
+ function deliverRecordsExactlyOnce(opts) {
10746
+ let lastSeqSent = opts.since;
10747
+ let replaying = true;
10748
+ const buffered = [];
10749
+ const gate = (record) => {
10750
+ const seq = record.seq;
10751
+ if (typeof seq !== "number" || seq <= lastSeqSent) return;
10752
+ lastSeqSent = seq;
10753
+ opts.send(record);
10754
+ };
10755
+ const unsubscribe = opts.subscribe((record) => {
10756
+ if (replaying) {
10757
+ buffered.push(record);
10758
+ } else {
10759
+ gate(record);
10760
+ }
10761
+ });
10762
+ const done = (async () => {
10763
+ for await (const rec of opts.diskRecords) {
10764
+ gate(rec);
10765
+ }
10766
+ replaying = false;
10767
+ for (const rec of buffered) gate(rec);
10768
+ buffered.length = 0;
10769
+ })();
10770
+ return { unsubscribe, done };
10771
+ }
9972
10772
  function parseOrchestratorField(raw) {
9973
10773
  const value = typeof raw === "string" ? tryParseJson(raw) : raw;
9974
10774
  if (typeof value === "boolean") return value;
@@ -10002,6 +10802,39 @@ function parseMcpServersField(raw) {
10002
10802
  }
10003
10803
  return servers;
10004
10804
  }
10805
+ function parseOptionsField(raw) {
10806
+ const value = typeof raw === "string" ? tryParseJson(raw) : raw;
10807
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
10808
+ const out = {};
10809
+ for (const [k, v] of Object.entries(value)) {
10810
+ if (typeof v === "boolean" || typeof v === "number" || typeof v === "string") {
10811
+ out[k] = v;
10812
+ }
10813
+ }
10814
+ return out;
10815
+ }
10816
+ function parseAuthField(raw) {
10817
+ const value = typeof raw === "string" ? tryParseJson(raw) : raw;
10818
+ if (!value || typeof value !== "object" || Array.isArray(value)) return void 0;
10819
+ const obj = value;
10820
+ const mode = obj.mode === "subscription" || obj.mode === "api-key" ? obj.mode : void 0;
10821
+ const token = typeof obj.token === "string" ? obj.token : void 0;
10822
+ const apiKey = typeof obj.apiKey === "string" ? obj.apiKey : void 0;
10823
+ return {
10824
+ ...mode ? { mode } : {},
10825
+ ...token !== void 0 ? { token } : {},
10826
+ ...apiKey !== void 0 ? { apiKey } : {}
10827
+ };
10828
+ }
10829
+ async function resolveSlugFromCwd(cwd) {
10830
+ if (!cwd) return void 0;
10831
+ try {
10832
+ const config = await loadWorkspacesConfig();
10833
+ return findWorkspaceByPath(config, cwd)?.slug;
10834
+ } catch {
10835
+ return void 0;
10836
+ }
10837
+ }
10005
10838
  async function handleSessions(req, res, path, registry, resolveAgentAdapter, ptyEnabled = false, resolveBrowserAdapter, listBrowserAdapters, sessionEvents, eventRing, buildOrchestratorMcp, daemonMcpUrl) {
10006
10839
  const json = (status, body) => {
10007
10840
  res.writeHead(status, { "content-type": "application/json" });
@@ -10040,8 +10873,17 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10040
10873
  ...typeof b.mode === "string" && b.mode.length > 0 ? { mode: b.mode } : {},
10041
10874
  ...typeof b.model === "string" && b.model.length > 0 ? { model: b.model } : {},
10042
10875
  ...typeof b.effort === "string" && b.effort.length > 0 ? { effort: b.effort } : {},
10876
+ ...b.options !== void 0 ? (() => {
10877
+ const parsed = parseOptionsField(b.options);
10878
+ return parsed !== void 0 ? { options: parsed } : {};
10879
+ })() : {},
10880
+ ...b.auth !== void 0 ? (() => {
10881
+ const parsed = parseAuthField(b.auth);
10882
+ return parsed !== void 0 ? { auth: parsed } : {};
10883
+ })() : {},
10043
10884
  ...typeof b.prompt === "string" ? { prompt: b.prompt } : {},
10044
10885
  ...typeof b.label === "string" ? { label: b.label } : {},
10886
+ ...typeof b.idempotencyKey === "string" && b.idempotencyKey.length > 0 ? { idempotencyKey: b.idempotencyKey } : {},
10045
10887
  ...typeof b.role === "string" && b.role.length > 0 ? { role: b.role } : {},
10046
10888
  ...typeof b.promptAppend === "string" ? { promptAppend: b.promptAppend } : {},
10047
10889
  ...b.orchestrator !== void 0 ? (() => {
@@ -10059,6 +10901,13 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10059
10901
  ...b.trace !== void 0 ? (() => {
10060
10902
  const t = typeof b.trace === "boolean" ? b.trace : b.trace === "true" ? true : b.trace === "false" ? false : void 0;
10061
10903
  return t !== void 0 ? { trace: t } : {};
10904
+ })() : {},
10905
+ // Permission-hold mode — the HTTP twin of the MCP `agent_start` tool's
10906
+ // `permissionHold` field (and `agentproto sessions start
10907
+ // --hold-permissions`). Tolerate a stringified boolean like `trace`.
10908
+ ...b.permissionHold !== void 0 ? (() => {
10909
+ const h = typeof b.permissionHold === "boolean" ? b.permissionHold : b.permissionHold === "true" ? true : b.permissionHold === "false" ? false : void 0;
10910
+ return h ? { permissionHold: true } : {};
10062
10911
  })() : {}
10063
10912
  }
10064
10913
  );
@@ -10071,7 +10920,10 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10071
10920
  });
10072
10921
  return true;
10073
10922
  }
10074
- json(201, result.descriptor);
10923
+ json(201, {
10924
+ ...result.descriptor,
10925
+ ...result.deduped ? { deduped: true } : {}
10926
+ });
10075
10927
  return true;
10076
10928
  }
10077
10929
  if (path === "/sessions/browser" && req.method === "POST") {
@@ -10205,7 +11057,9 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10205
11057
  `[/sessions/terminal] no cwd resolvable for argv=${argv.join(" ")} \u2014 falling back to daemon's cwd ${cwd}`
10206
11058
  );
10207
11059
  }
10208
- if (!workspaceSlug) workspaceSlug = "default";
11060
+ if (!workspaceSlug) {
11061
+ workspaceSlug = await resolveSlugFromCwd(cwd) ?? "default";
11062
+ }
10209
11063
  try {
10210
11064
  const desc = registry.spawnPty({
10211
11065
  argv,
@@ -10249,7 +11103,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10249
11103
  await registry.enqueuePrompt(id2, prompt, { interrupt });
10250
11104
  json(202, { ok: true, id: id2, queued: true });
10251
11105
  } else {
10252
- await registry.sendPrompt(id2, prompt);
11106
+ await registry.sendPrompt(id2, prompt, { interrupt });
10253
11107
  json(200, { ok: true, id: id2 });
10254
11108
  }
10255
11109
  } catch (err) {
@@ -10276,10 +11130,14 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10276
11130
  return true;
10277
11131
  }
10278
11132
  try {
11133
+ const rawCwd = typeof b.cwd === "string" ? b.cwd : process.cwd();
10279
11134
  const desc = registry.spawn({
10280
11135
  kind: b.kind === "terminal" || b.kind === "agent-cli" || b.kind === "command" ? b.kind : "command",
10281
- workspaceSlug: typeof b.workspaceSlug === "string" ? b.workspaceSlug : "default",
10282
- cwd: typeof b.cwd === "string" ? b.cwd : process.cwd(),
11136
+ // Same reverse-map as /sessions/terminal and spawnAgentSession: an
11137
+ // explicit slug wins, otherwise derive it from cwd rather than
11138
+ // dumping the session into "default".
11139
+ workspaceSlug: typeof b.workspaceSlug === "string" ? b.workspaceSlug : await resolveSlugFromCwd(rawCwd) ?? "default",
11140
+ cwd: rawCwd,
10283
11141
  argv,
10284
11142
  env: b.env && typeof b.env === "object" ? b.env : void 0,
10285
11143
  label: typeof b.label === "string" ? b.label : void 0
@@ -10294,7 +11152,7 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10294
11152
  return true;
10295
11153
  }
10296
11154
  const idMatch = path.match(
10297
- /^\/sessions\/([^/]+)(\/stream|\/kill|\/preview|\/export|\/events|\/wait)?$/
11155
+ /^\/sessions\/([^/]+)(\/events\/stream|\/stream|\/kill|\/preview|\/export|\/events|\/wait)?$/
10298
11156
  );
10299
11157
  if (!idMatch) return false;
10300
11158
  const [, rawIdOrName, suffix] = idMatch;
@@ -10351,9 +11209,9 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10351
11209
  let fileStream;
10352
11210
  try {
10353
11211
  fileStream = createReadStream(filePath, { encoding: "utf8" });
10354
- await new Promise((resolve13, reject) => {
11212
+ await new Promise((resolve12, reject) => {
10355
11213
  fileStream.once("error", reject);
10356
- fileStream.once("open", resolve13);
11214
+ fileStream.once("open", resolve12);
10357
11215
  });
10358
11216
  } catch (err) {
10359
11217
  const code = err.code;
@@ -10391,6 +11249,85 @@ async function handleSessions(req, res, path, registry, resolveAgentAdapter, pty
10391
11249
  });
10392
11250
  return true;
10393
11251
  }
11252
+ if (suffix === "/events/stream" && req.method === "GET") {
11253
+ const reqUrl = req.url ?? "";
11254
+ const qs = new URLSearchParams(
11255
+ reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : ""
11256
+ );
11257
+ const sinceRaw = qs.get("since");
11258
+ if (sinceRaw !== null && !/^\d+$/.test(sinceRaw)) {
11259
+ json(400, {
11260
+ error: "invalid_since",
11261
+ message: "since must be a non-negative integer"
11262
+ });
11263
+ return true;
11264
+ }
11265
+ const since = sinceRaw !== null ? Number.parseInt(sinceRaw, 10) : 0;
11266
+ const filePath = sessionEventsPath(id);
11267
+ let fileStream;
11268
+ try {
11269
+ fileStream = createReadStream(filePath, { encoding: "utf8" });
11270
+ await new Promise((resolve12, reject) => {
11271
+ fileStream.once("error", reject);
11272
+ fileStream.once("open", resolve12);
11273
+ });
11274
+ } catch (err) {
11275
+ const code = err.code;
11276
+ if (code === "ENOENT") {
11277
+ json(404, { error: "no_transcript" });
11278
+ return true;
11279
+ }
11280
+ throw err;
11281
+ }
11282
+ res.writeHead(200, {
11283
+ "content-type": "text/event-stream",
11284
+ "cache-control": "no-cache",
11285
+ connection: "keep-alive"
11286
+ });
11287
+ res.write(`: connected
11288
+
11289
+ `);
11290
+ const ping = setInterval(() => {
11291
+ try {
11292
+ res.write(`: keep-alive
11293
+
11294
+ `);
11295
+ } catch {
11296
+ clearInterval(ping);
11297
+ }
11298
+ }, 25e3);
11299
+ async function* diskRecords() {
11300
+ const rl = createInterface({ input: fileStream, crlfDelay: Infinity });
11301
+ for await (const line of rl) {
11302
+ const trimmed = line.trim();
11303
+ if (!trimmed) continue;
11304
+ try {
11305
+ yield JSON.parse(trimmed);
11306
+ } catch {
11307
+ continue;
11308
+ }
11309
+ }
11310
+ }
11311
+ const { unsubscribe, done } = deliverRecordsExactlyOnce({
11312
+ since,
11313
+ diskRecords: diskRecords(),
11314
+ subscribe: (onRecord) => registry.subscribeToRecords(id, onRecord),
11315
+ send: (record) => {
11316
+ try {
11317
+ res.write(`data: ${JSON.stringify(record)}
11318
+
11319
+ `);
11320
+ } catch {
11321
+ }
11322
+ }
11323
+ });
11324
+ req.once("close", () => {
11325
+ unsubscribe();
11326
+ clearInterval(ping);
11327
+ });
11328
+ await done;
11329
+ return true;
11330
+ }
10394
11331
  if (suffix === "/preview" && req.method === "GET") {
10395
11332
  if (!resolvedDesc) {
10396
11333
  json(404, { error: "session_not_found", id: rawIdOrName });
@@ -10915,6 +11852,133 @@ async function handlePolicies(req, res, path, supervisor, registry) {
10915
11852
  json(200, state);
10916
11853
  return true;
10917
11854
  }
11855
+ async function handlePermissions(req, res, path, registry) {
11856
+ const json = (status, body) => {
11857
+ res.writeHead(status, { "content-type": "application/json" });
11858
+ res.end(JSON.stringify(body));
11859
+ };
11860
+ if (path === "/permissions" && req.method === "GET") {
11861
+ const reqUrl = req.url ?? "";
11862
+ const qs = new URLSearchParams(
11863
+ reqUrl.includes("?") ? reqUrl.slice(reqUrl.indexOf("?") + 1) : ""
11864
+ );
11865
+ const sessionId = qs.get("sessionId") ?? void 0;
11866
+ const permissions = registry.listPendingPermissions(sessionId ? { sessionId } : void 0).map((p) => enrichPermission(p, registry));
11867
+ json(200, { permissions });
11868
+ return true;
11869
+ }
11870
+ const idMatch = path.match(/^\/permissions\/([^/]+)$/);
11871
+ if (idMatch && req.method === "POST") {
11872
+ const id = decodeURIComponent(idMatch[1] ?? "");
11873
+ const body = await readJsonBody(req);
11874
+ const b = body && typeof body === "object" ? body : {};
11875
+ const decision = b.decision;
11876
+ if (decision !== "approve" && decision !== "deny") {
11877
+ json(400, {
11878
+ error: "invalid_decision",
11879
+ message: 'body.decision must be "approve" or "deny"'
11880
+ });
11881
+ return true;
11882
+ }
11883
+ const optionId = typeof b.optionId === "string" ? b.optionId : void 0;
11884
+ const scope = b.scope === "always" || b.scope === "once" ? b.scope : void 0;
11885
+ const result = await registry.respondPermission(id, {
11886
+ decision,
11887
+ ...optionId ? { optionId } : {},
11888
+ ...scope ? { scope } : {}
11889
+ });
11890
+ if (!result.ok) {
11891
+ const status = result.error === "not_found" || result.error === "session_gone" ? 404 : 409;
11892
+ json(status, { error: result.error, message: result.message });
11893
+ return true;
11894
+ }
11895
+ json(200, {
11896
+ ok: true,
11897
+ id,
11898
+ sessionId: result.permission.sessionId,
11899
+ decision: result.decision,
11900
+ ...result.optionId ? { optionId: result.optionId } : {}
11901
+ });
11902
+ return true;
11903
+ }
11904
+ if (path === "/permissions" || idMatch) {
11905
+ json(405, { error: "method_not_allowed", message: "GET /permissions or POST /permissions/:id" });
11906
+ return true;
11907
+ }
11908
+ return false;
11909
+ }
11910
+ function enrichPermission(p, registry) {
11911
+ const desc = registry.get(p.sessionId);
11912
+ const ageMs = Date.now() - new Date(p.requestedAt).getTime();
11913
+ return {
11914
+ ...p,
11915
+ ...desc?.adapterSlug ? { adapter: desc.adapterSlug } : {},
11916
+ ...desc?.label ? { sessionLabel: desc.label } : {},
11917
+ ...desc?.command ? { sessionTitle: desc.command } : {},
11918
+ ageMs: ageMs >= 0 ? ageMs : 0
11919
+ };
11920
+ }
11921
+ async function handlePairings(req, res, path, registry) {
11922
+ const json = (status, body) => {
11923
+ res.writeHead(status, { "content-type": "application/json" });
11924
+ res.end(JSON.stringify(body));
11925
+ };
11926
+ if (path === "/pairings/offer" && req.method === "POST") {
11927
+ const body = await readJsonBody(req);
11928
+ const b = body && typeof body === "object" ? body : {};
11929
+ const ttlMinutes = typeof b.ttlMinutes === "number" ? b.ttlMinutes : void 0;
11930
+ const rendezvous = typeof b.rendezvous === "string" ? b.rendezvous : void 0;
11931
+ try {
11932
+ const offer = await registry.createOffer({
11933
+ ...ttlMinutes ? { ttlMs: ttlMinutes * 6e4 } : {},
11934
+ ...rendezvous ? { rendezvousUrl: rendezvous } : {}
11935
+ });
11936
+ json(200, {
11937
+ url: offer.url,
11938
+ fingerprint: offer.fingerprint,
11939
+ rendezvous: offer.rendezvousUrl,
11940
+ rendezvousIsHostedDefault: offer.rendezvousIsHostedDefault,
11941
+ expiresAt: new Date(offer.exp * 1e3).toISOString()
11942
+ });
11943
+ } catch (err) {
11944
+ json(400, {
11945
+ error: "offer_failed",
11946
+ message: err instanceof Error ? err.message : String(err)
11947
+ });
11948
+ }
11949
+ return true;
11950
+ }
11951
+ if (path === "/pairings" && req.method === "GET") {
11952
+ const pairings = (await registry.list()).map((p) => ({
11953
+ name: p.name,
11954
+ fingerprint: p.fingerprint,
11955
+ createdAt: p.createdAt,
11956
+ lastSeen: p.lastSeen,
11957
+ rendezvous: p.rendezvousUrl
11958
+ }));
11959
+ json(200, { pairings });
11960
+ return true;
11961
+ }
11962
+ const fpMatch = path.match(/^\/pairings\/([^/]+)$/);
11963
+ if (fpMatch && req.method === "DELETE") {
11964
+ const target = decodeURIComponent(fpMatch[1] ?? "");
11965
+ const revoked = await registry.revoke(target);
11966
+ if (!revoked) {
11967
+ json(404, { error: "not_found", message: `no pairing matched "${target}"` });
11968
+ return true;
11969
+ }
11970
+ json(200, { ok: true, revoked: target });
11971
+ return true;
11972
+ }
11973
+ if (path === "/pairings" || path === "/pairings/offer" || fpMatch) {
11974
+ json(405, {
11975
+ error: "method_not_allowed",
11976
+ message: "POST /pairings/offer \xB7 GET /pairings \xB7 DELETE /pairings/:fingerprint"
11977
+ });
11978
+ return true;
11979
+ }
11980
+ return false;
11981
+ }
10918
11982
  async function handleCron(req, res, path, scheduler) {
10919
11983
  const json = (status, body) => {
10920
11984
  res.writeHead(status, { "content-type": "application/json" });
@@ -11634,11 +12698,11 @@ function createRoutineRunner(opts) {
11634
12698
  const persist = () => {
11635
12699
  saveRuns(runs, persistPath);
11636
12700
  };
11637
- const waitTurnEnd = (sessionId) => new Promise((resolve13) => {
12701
+ const waitTurnEnd = (sessionId) => new Promise((resolve12) => {
11638
12702
  const unsubs = [];
11639
12703
  const done = () => {
11640
12704
  for (const u of unsubs) u();
11641
- resolve13();
12705
+ resolve12();
11642
12706
  };
11643
12707
  unsubs.push(
11644
12708
  sessionEvents.on("session:turn-end", (ev) => {
@@ -11954,9 +13018,9 @@ var SessionsRegistryAgentHost = class {
11954
13018
  if (!("role" in m)) continue;
11955
13019
  if (m.role !== "assistant") continue;
11956
13020
  if (!("text" in m)) continue;
11957
- const text5 = m.text;
11958
- if (typeof text5 === "string" && text5.trim()) {
11959
- return text5;
13021
+ const text6 = m.text;
13022
+ if (typeof text6 === "string" && text6.trim()) {
13023
+ return text6;
11960
13024
  }
11961
13025
  }
11962
13026
  return "";
@@ -11967,11 +13031,11 @@ var SessionsRegistryAgentHost = class {
11967
13031
  }
11968
13032
  // ── Internal helpers ──────────────────────────────────────────────────
11969
13033
  waitTurnEnd(sessionId) {
11970
- return new Promise((resolve13, reject) => {
13034
+ return new Promise((resolve12, reject) => {
11971
13035
  const unsubs = [];
11972
13036
  const done = () => {
11973
13037
  for (const u of unsubs) u();
11974
- resolve13();
13038
+ resolve12();
11975
13039
  };
11976
13040
  const fail = (reason) => {
11977
13041
  for (const u of unsubs) u();
@@ -12185,9 +13249,9 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
12185
13249
  state.run.status = "cancelled";
12186
13250
  state.run.endedAt = (/* @__PURE__ */ new Date()).toISOString();
12187
13251
  } else {
12188
- const errMsg = err instanceof Error ? err.message : String(err);
13252
+ const errMsg2 = err instanceof Error ? err.message : String(err);
12189
13253
  state.run.status = "failed";
12190
- state.run.error = errMsg;
13254
+ state.run.error = errMsg2;
12191
13255
  state.run.endedAt = (/* @__PURE__ */ new Date()).toISOString();
12192
13256
  for (let i = 0; i < state.run.stages.length; i++) {
12193
13257
  const stage = state.run.stages[i];
@@ -12196,7 +13260,7 @@ async function executeRunWorkflow(state, runtimeWf, agents, signal, cache, cache
12196
13260
  for (const step of stage.steps) {
12197
13261
  step.status = "failed";
12198
13262
  step.endedAt = (/* @__PURE__ */ new Date()).toISOString();
12199
- step.error = errMsg;
13263
+ step.error = errMsg2;
12200
13264
  }
12201
13265
  }
12202
13266
  }
@@ -12445,11 +13509,11 @@ var ANSI_RE2 = /\x1b\[[0-9;?]*[A-Za-z]/g;
12445
13509
  function stripAnsi2(s) {
12446
13510
  return s.replace(ANSI_RE2, "");
12447
13511
  }
12448
- function parseVerdict(text5) {
13512
+ function parseVerdict(text6) {
12449
13513
  const re = /VERDICT:\s*(PASS|FAIL)/gi;
12450
13514
  let last = null;
12451
13515
  let m;
12452
- while ((m = re.exec(text5)) !== null) {
13516
+ while ((m = re.exec(text6)) !== null) {
12453
13517
  last = m[1].toUpperCase();
12454
13518
  }
12455
13519
  return last;
@@ -13226,11 +14290,11 @@ function createInboundWatcher(opts) {
13226
14290
  return;
13227
14291
  }
13228
14292
  const raw = out.result;
13229
- const text5 = raw?.content?.find((c) => c.type === "text")?.text;
13230
- if (!text5) return;
14293
+ const text6 = raw?.content?.find((c) => c.type === "text")?.text;
14294
+ if (!text6) return;
13231
14295
  let data;
13232
14296
  try {
13233
- data = JSON.parse(text5);
14297
+ data = JSON.parse(text6);
13234
14298
  } catch {
13235
14299
  return;
13236
14300
  }
@@ -13646,7 +14710,9 @@ var DEFAULT_ORCHESTRATOR_TOOLS = [
13646
14710
  "policy_attach",
13647
14711
  "policy_status",
13648
14712
  "policy_list",
13649
- "policy_cancel"
14713
+ "policy_cancel",
14714
+ "permissions_list",
14715
+ "permissions_respond"
13650
14716
  ];
13651
14717
  var DEFAULT_MAX_DEPTH = 3;
13652
14718
  var HARD_MAX_DEPTH = 8;
@@ -13756,7 +14822,7 @@ function createOrchestratorInjector(deps2) {
13756
14822
  return { entry, scope, bindLifecycle };
13757
14823
  };
13758
14824
  }
13759
- function makeBrowserHandle(entry, resolve13) {
14825
+ function makeBrowserHandle(entry, resolve12) {
13760
14826
  return {
13761
14827
  slug: entry.id,
13762
14828
  name: entry.name,
@@ -13767,7 +14833,7 @@ function makeBrowserHandle(entry, resolve13) {
13767
14833
  // check() is available for on-demand health probes but is never called
13768
14834
  // by the lister (kit invariant OQ-5). Returns true when the adapter
13769
14835
  // is present in the injected resolver map, false otherwise.
13770
- check: async () => resolve13 ? resolve13(entry.id) != null : true
14836
+ check: async () => resolve12 ? resolve12(entry.id) != null : true
13771
14837
  };
13772
14838
  }
13773
14839
  var noopLedger = {
@@ -13825,13 +14891,13 @@ async function spawnCloudflaredUntil(argv, opts) {
13825
14891
  };
13826
14892
  let settled = false;
13827
14893
  let forwardedLen = 0;
13828
- return await new Promise((resolve13, reject) => {
14894
+ return await new Promise((resolve12, reject) => {
13829
14895
  const poll = setInterval(() => {
13830
- const text5 = readAll();
14896
+ const text6 = readAll();
13831
14897
  if (opts.onLog) {
13832
- const lastNl = text5.lastIndexOf("\n");
14898
+ const lastNl = text6.lastIndexOf("\n");
13833
14899
  if (lastNl + 1 > forwardedLen) {
13834
- const chunk = text5.slice(forwardedLen, lastNl + 1);
14900
+ const chunk = text6.slice(forwardedLen, lastNl + 1);
13835
14901
  for (const line of chunk.split(/\r?\n/)) {
13836
14902
  if (line.length > 0) opts.onLog(`[cloudflared] ${line}`);
13837
14903
  }
@@ -13839,11 +14905,11 @@ async function spawnCloudflaredUntil(argv, opts) {
13839
14905
  }
13840
14906
  }
13841
14907
  if (settled) return;
13842
- const m = text5.match(opts.readyRegex);
14908
+ const m = text6.match(opts.readyRegex);
13843
14909
  if (m) {
13844
14910
  settled = true;
13845
14911
  clearTimeout(timer);
13846
- resolve13({ proc, match: m[0], stopTail: () => clearInterval(poll) });
14912
+ resolve12({ proc, match: m[0], stopTail: () => clearInterval(poll) });
13847
14913
  }
13848
14914
  }, POLL_INTERVAL_MS);
13849
14915
  if (poll.unref) poll.unref();
@@ -13977,15 +15043,15 @@ function quickTunnelProvider() {
13977
15043
  }
13978
15044
  }, 3e3);
13979
15045
  timer.unref();
13980
- await new Promise((resolve13) => {
15046
+ await new Promise((resolve12) => {
13981
15047
  if (proc.exitCode !== null) {
13982
15048
  clearTimeout(timer);
13983
- resolve13();
15049
+ resolve12();
13984
15050
  return;
13985
15051
  }
13986
15052
  proc.once("exit", () => {
13987
15053
  clearTimeout(timer);
13988
- resolve13();
15054
+ resolve12();
13989
15055
  });
13990
15056
  });
13991
15057
  }
@@ -14224,9 +15290,75 @@ function registerRemoteTools(server, opts) {
14224
15290
  }
14225
15291
  );
14226
15292
  }
15293
+ function text3(value) {
15294
+ return {
15295
+ content: [
15296
+ {
15297
+ type: "text",
15298
+ text: typeof value === "string" ? value : JSON.stringify(value, null, 2)
15299
+ }
15300
+ ]
15301
+ };
15302
+ }
15303
+ function registerPairingTools(server, opts) {
15304
+ const { registry } = opts;
15305
+ server.tool(
15306
+ "pair_offer",
15307
+ "Mint a one-time pairing offer and start listening for a client on the rendezvous broker. Returns an `agentproto://pair?\u2026` offer URL (share it / render it as a QR), the daemon's identity fingerprint, and the offer expiry. The URL is the bootstrap secret \u2014 it carries the daemon's public keys (MITM-proof against the broker) and a short-lived single-use token. The client runs `agentproto pair accept \"<url>\"`.",
15308
+ {
15309
+ ttlMinutes: z.number().int().min(1).max(1440).optional().describe("Offer time-to-live in minutes (default 10)."),
15310
+ rendezvous: z.string().optional().describe(
15311
+ "Rendezvous WS URL to route through (ws:// or wss://). Defaults to `pairing.rendezvous` from config.json, or the hosted broker (wss://rdv.agentproto.sh/v1) when neither is set. The response's `rendezvousIsHostedDefault` flags when that fallback applied."
15312
+ )
15313
+ },
15314
+ async ({ ttlMinutes, rendezvous }) => {
15315
+ const offer = await registry.createOffer({
15316
+ ...ttlMinutes ? { ttlMs: ttlMinutes * 6e4 } : {},
15317
+ ...rendezvous ? { rendezvousUrl: rendezvous } : {}
15318
+ });
15319
+ return text3({
15320
+ url: offer.url,
15321
+ fingerprint: offer.fingerprint,
15322
+ rendezvous: offer.rendezvousUrl,
15323
+ rendezvousIsHostedDefault: offer.rendezvousIsHostedDefault,
15324
+ expiresAt: new Date(offer.exp * 1e3).toISOString()
15325
+ });
15326
+ }
15327
+ );
15328
+ server.tool(
15329
+ "pair_list",
15330
+ "List clients paired with this daemon: name, fingerprint, when first paired, and last seen. Read-only.",
15331
+ {},
15332
+ async () => {
15333
+ const pairings = await registry.list();
15334
+ return text3({
15335
+ pairings: pairings.map((p) => ({
15336
+ name: p.name,
15337
+ fingerprint: p.fingerprint,
15338
+ createdAt: p.createdAt,
15339
+ lastSeen: p.lastSeen,
15340
+ rendezvous: p.rendezvousUrl
15341
+ }))
15342
+ });
15343
+ }
15344
+ );
15345
+ server.tool(
15346
+ "pair_revoke",
15347
+ "Revoke a pairing by fingerprint or name. Drops its rendezvous connections so the client can no longer reconnect, and removes it from pairings.json.",
15348
+ {
15349
+ target: z.string().describe("The pairing's fingerprint or name (see pair_list).")
15350
+ },
15351
+ async ({ target }) => {
15352
+ const revoked = await registry.revoke(target);
15353
+ return text3(
15354
+ revoked ? { ok: true, revoked: target } : { ok: false, message: `no pairing matched "${target}"` }
15355
+ );
15356
+ }
15357
+ );
15358
+ }
14227
15359
 
14228
15360
  // src/daemon-health-tools.ts
14229
- function text3(value) {
15361
+ function text4(value) {
14230
15362
  return {
14231
15363
  content: [
14232
15364
  {
@@ -14242,7 +15374,7 @@ function registerDaemonHealthTools(server, opts) {
14242
15374
  "Cheap, side-effect-free liveness probe for this daemon. Returns the same fields as GET /health but in-process. `alive` is always true when the tool returns successfully; if the daemon is unreachable the failure is observed as a thrown/failed tool call at the transport level, not as a false field.",
14243
15375
  {},
14244
15376
  async () => {
14245
- return text3({
15377
+ return text4({
14246
15378
  alive: true,
14247
15379
  status: "ok",
14248
15380
  workspace: opts.workspace,
@@ -14382,15 +15514,15 @@ function namedTunnelProvider(cfg) {
14382
15514
  }
14383
15515
  }, 3e3);
14384
15516
  timer.unref();
14385
- await new Promise((resolve13) => {
15517
+ await new Promise((resolve12) => {
14386
15518
  if (proc.exitCode !== null) {
14387
15519
  clearTimeout(timer);
14388
- resolve13();
15520
+ resolve12();
14389
15521
  return;
14390
15522
  }
14391
15523
  proc.once("exit", () => {
14392
15524
  clearTimeout(timer);
14393
- resolve13();
15525
+ resolve12();
14394
15526
  });
14395
15527
  });
14396
15528
  }
@@ -14507,14 +15639,14 @@ function ngrokTunnelProvider(opts) {
14507
15639
  });
14508
15640
  child = proc;
14509
15641
  const forwardLogs = (chunk) => {
14510
- const text5 = chunk.toString("utf8");
14511
- for (const line of text5.split(/\r?\n/)) {
15642
+ const text6 = chunk.toString("utf8");
15643
+ for (const line of text6.split(/\r?\n/)) {
14512
15644
  if (line.length > 0) opts2.onLog?.(`[ngrok] ${line}`);
14513
15645
  }
14514
15646
  };
14515
15647
  proc.stderr?.on("data", forwardLogs);
14516
15648
  proc.stdout?.on("data", forwardLogs);
14517
- const url = await new Promise((resolve13, reject) => {
15649
+ const url = await new Promise((resolve12, reject) => {
14518
15650
  let settled = false;
14519
15651
  const timer = setTimeout(() => {
14520
15652
  if (settled) return;
@@ -14542,7 +15674,7 @@ function ngrokTunnelProvider(opts) {
14542
15674
  settled = true;
14543
15675
  clearTimeout(timer);
14544
15676
  if (apiPollHandle) clearInterval(apiPollHandle);
14545
- resolve13(pubUrl);
15677
+ resolve12(pubUrl);
14546
15678
  }
14547
15679
  } catch {
14548
15680
  }
@@ -14551,16 +15683,16 @@ function ngrokTunnelProvider(opts) {
14551
15683
  };
14552
15684
  const onStdout = (chunk) => {
14553
15685
  if (settled) return;
14554
- const text5 = chunk.toString("utf8");
14555
- const reMatch = text5.match(URL_JSON_REGEX);
15686
+ const text6 = chunk.toString("utf8");
15687
+ const reMatch = text6.match(URL_JSON_REGEX);
14556
15688
  if (reMatch) {
14557
15689
  settled = true;
14558
15690
  clearTimeout(timer);
14559
15691
  if (apiPollHandle) clearInterval(apiPollHandle);
14560
- resolve13(reMatch[1]);
15692
+ resolve12(reMatch[1]);
14561
15693
  return;
14562
15694
  }
14563
- for (const line of text5.split(/\r?\n/)) {
15695
+ for (const line of text6.split(/\r?\n/)) {
14564
15696
  if (line.length === 0) continue;
14565
15697
  try {
14566
15698
  const parsed = JSON.parse(line);
@@ -14568,7 +15700,7 @@ function ngrokTunnelProvider(opts) {
14568
15700
  settled = true;
14569
15701
  clearTimeout(timer);
14570
15702
  if (apiPollHandle) clearInterval(apiPollHandle);
14571
- resolve13(parsed.url);
15703
+ resolve12(parsed.url);
14572
15704
  return;
14573
15705
  }
14574
15706
  } catch {
@@ -14615,15 +15747,15 @@ function ngrokTunnelProvider(opts) {
14615
15747
  }
14616
15748
  }, 3e3);
14617
15749
  timer.unref();
14618
- await new Promise((resolve13) => {
15750
+ await new Promise((resolve12) => {
14619
15751
  if (proc.exitCode !== null) {
14620
15752
  clearTimeout(timer);
14621
- resolve13();
15753
+ resolve12();
14622
15754
  return;
14623
15755
  }
14624
15756
  proc.once("exit", () => {
14625
15757
  clearTimeout(timer);
14626
- resolve13();
15758
+ resolve12();
14627
15759
  });
14628
15760
  });
14629
15761
  }
@@ -14993,7 +16125,7 @@ function makeStubProvider() {
14993
16125
  }
14994
16126
  };
14995
16127
  }
14996
- function text4(value) {
16128
+ function text5(value) {
14997
16129
  return {
14998
16130
  content: [
14999
16131
  {
@@ -15057,7 +16189,7 @@ function registerTunnelTools(server, opts) {
15057
16189
  ...input.tunnelId ? { tunnelId: input.tunnelId } : {},
15058
16190
  ...input.credentialsFile ? { credentialsFile: input.credentialsFile } : {}
15059
16191
  });
15060
- return text4(desc);
16192
+ return text5(desc);
15061
16193
  } catch (err) {
15062
16194
  return errText("tunnel_create", err);
15063
16195
  }
@@ -15078,7 +16210,7 @@ function registerTunnelTools(server, opts) {
15078
16210
  (t) => t.status === "starting" || t.status === "active"
15079
16211
  );
15080
16212
  }
15081
- return text4({ tunnels });
16213
+ return text5({ tunnels });
15082
16214
  }
15083
16215
  );
15084
16216
  server.tool(
@@ -15103,7 +16235,7 @@ function registerTunnelTools(server, opts) {
15103
16235
  isError: true
15104
16236
  };
15105
16237
  }
15106
- return text4({ ok, tunnelId: input.tunnelId });
16238
+ return text5({ ok, tunnelId: input.tunnelId });
15107
16239
  } catch (err) {
15108
16240
  return errText("tunnel_stop", err);
15109
16241
  }
@@ -15130,7 +16262,7 @@ function registerTunnelTools(server, opts) {
15130
16262
  isError: true
15131
16263
  };
15132
16264
  }
15133
- return text4(desc);
16265
+ return text5(desc);
15134
16266
  }
15135
16267
  );
15136
16268
  }
@@ -15294,7 +16426,7 @@ var localSandboxProvider = {
15294
16426
  }
15295
16427
  };
15296
16428
  async function getFreePort() {
15297
- return new Promise((resolve13, reject) => {
16429
+ return new Promise((resolve12, reject) => {
15298
16430
  const srv = createServer$1();
15299
16431
  srv.once("error", reject);
15300
16432
  srv.listen(0, "127.0.0.1", () => {
@@ -15304,7 +16436,7 @@ async function getFreePort() {
15304
16436
  return;
15305
16437
  }
15306
16438
  const { port } = address;
15307
- srv.close(() => resolve13(port));
16439
+ srv.close(() => resolve12(port));
15308
16440
  });
15309
16441
  });
15310
16442
  }
@@ -15317,7 +16449,7 @@ async function probeHealth(url, timeoutMs) {
15317
16449
  } catch {
15318
16450
  }
15319
16451
  if (Date.now() >= deadline) return false;
15320
- await new Promise((resolve13) => setTimeout(resolve13, POLL_INTERVAL_MS2));
16452
+ await new Promise((resolve12) => setTimeout(resolve12, POLL_INTERVAL_MS2));
15321
16453
  }
15322
16454
  }
15323
16455
 
@@ -15625,84 +16757,355 @@ function createWorkspaceFs(opts) {
15625
16757
  }
15626
16758
  };
15627
16759
  }
15628
- var PROVIDER_ENV_VARS = {
15629
- anthropic: "ANTHROPIC_API_KEY",
15630
- openrouter: "OPENROUTER_API_KEY",
15631
- openai: "OPENAI_API_KEY",
15632
- google: "GOOGLE_GENERATIVE_AI_API_KEY",
15633
- groq: "GROQ_API_KEY",
15634
- mistral: "MISTRAL_API_KEY",
15635
- // Moonshot (Kimi) — an Anthropic-compatible gateway; its key is sent as a
15636
- // Bearer token (ANTHROPIC_AUTH_TOKEN) with a custom base_url by the claude-sdk
15637
- // gateway resolver. Native routers (Mastra/hermes) read this env name directly.
15638
- moonshot: "MOONSHOT_API_KEY",
15639
- // MiniMax — direct LLM provider (also serves voice/video under the same key).
15640
- minimax: "MINIMAX_API_KEY",
15641
- // Vercel AI Gateway a gateway provider, like openrouter, that fronts many
15642
- // upstream model families behind one key.
15643
- "vercel-ai-gateway": "AI_GATEWAY_API_KEY"
15644
- };
15645
- function emptyFile() {
15646
- return { version: 1, providers: {} };
15647
- }
15648
- function providersPath() {
15649
- return resolve(homedir(), ".agentproto", "providers.json");
16760
+ var PAIRINGS_VERSION = 1;
16761
+ var DEFAULT_TTL_MS = 10 * 6e4;
16762
+ var DEFAULT_RECONNECT_MIN_MS = 1e3;
16763
+ var DEFAULT_RECONNECT_MAX_MS = 3e4;
16764
+ var DEFAULT_HANDSHAKE_TIMEOUT_MS = 11e4;
16765
+ function defaultPairingsPath() {
16766
+ return join(homedir(), ".agentproto", "pairings.json");
16767
+ }
16768
+ function b64url(bytes) {
16769
+ return bytes.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
16770
+ }
16771
+ function constantTimeEqual(a, b) {
16772
+ const key = randomBytes(32);
16773
+ const da = createHmac("sha256", key).update(a, "utf8").digest();
16774
+ const db = createHmac("sha256", key).update(b, "utf8").digest();
16775
+ return timingSafeEqual(da, db);
16776
+ }
16777
+ function createPairingRegistry(deps2) {
16778
+ const pairingsPath = deps2.pairingsPath ?? defaultPairingsPath();
16779
+ const now = deps2.now ?? Date.now;
16780
+ const log = deps2.log ?? (() => {
16781
+ });
16782
+ const reconnectMinMs = deps2.reconnectMinMs ?? DEFAULT_RECONNECT_MIN_MS;
16783
+ const reconnectMaxMs = deps2.reconnectMaxMs ?? DEFAULT_RECONNECT_MAX_MS;
16784
+ const handshakeTimeoutMs = deps2.handshakeTimeoutMs ?? DEFAULT_HANDSHAKE_TIMEOUT_MS;
16785
+ const pairings = /* @__PURE__ */ new Map();
16786
+ const offers = /* @__PURE__ */ new Map();
16787
+ const loops = /* @__PURE__ */ new Map();
16788
+ const channels = /* @__PURE__ */ new Set();
16789
+ let shuttingDown = false;
16790
+ let loaded = false;
16791
+ async function ensureLoaded() {
16792
+ if (loaded) return;
16793
+ loaded = true;
16794
+ try {
16795
+ const raw = await readFile(pairingsPath, "utf8");
16796
+ const parsed = JSON.parse(raw);
16797
+ if (isPairingsFile(parsed)) {
16798
+ for (const rec of parsed.pairings) pairings.set(rec.fingerprint, rec);
16799
+ }
16800
+ } catch (err) {
16801
+ if (!isEnoent(err)) {
16802
+ log(`[pairing] could not read ${pairingsPath}: ${errMsg(err)}`);
16803
+ }
16804
+ }
16805
+ }
16806
+ async function persist() {
16807
+ const file = {
16808
+ v: PAIRINGS_VERSION,
16809
+ pairings: Array.from(pairings.values())
16810
+ };
16811
+ const dir = dirname(pairingsPath);
16812
+ await mkdir(dir, { recursive: true });
16813
+ const tmp = join(dir, `.${basename(pairingsPath)}.tmp-${process.pid}`);
16814
+ await writeFile(tmp, JSON.stringify(file, null, 2) + "\n", { encoding: "utf8", mode: 384 });
16815
+ await chmod(tmp, 384).catch(() => {
16816
+ });
16817
+ await rename(tmp, pairingsPath);
16818
+ }
16819
+ function startLoop(spec) {
16820
+ if (loops.has(spec.key)) return;
16821
+ const abort = new AbortController();
16822
+ const done = runLoop(spec, abort.signal).catch((err) => {
16823
+ log(`[pairing] loop ${spec.key} ended: ${errMsg(err)}`);
16824
+ });
16825
+ loops.set(spec.key, { abort, done });
16826
+ }
16827
+ async function stopLoop(key) {
16828
+ const loop = loops.get(key);
16829
+ if (!loop) return;
16830
+ loops.delete(key);
16831
+ loop.abort.abort();
16832
+ await loop.done.catch(() => {
16833
+ });
16834
+ }
16835
+ async function runLoop(spec, signal) {
16836
+ let backoff = reconnectMinMs;
16837
+ while (!signal.aborted && spec.shouldContinue()) {
16838
+ const expected = spec.token();
16839
+ let sink;
16840
+ try {
16841
+ sink = await deps2.dial(dialUrl(spec.rendezvousUrl, expected), signal);
16842
+ } catch (err) {
16843
+ if (signal.aborted) break;
16844
+ log(`[pairing] dial ${spec.key} failed: ${errMsg(err)}`);
16845
+ await sleep(backoff, signal);
16846
+ backoff = Math.min(backoff * 2, reconnectMaxMs);
16847
+ continue;
16848
+ }
16849
+ const onAbort = () => {
16850
+ try {
16851
+ sink.close("registry shutdown");
16852
+ } catch {
16853
+ }
16854
+ };
16855
+ signal.addEventListener("abort", onAbort, { once: true });
16856
+ try {
16857
+ let capturedSession = null;
16858
+ let capturedHello = null;
16859
+ let wrapped;
16860
+ try {
16861
+ const identity = await deps2.loadIdentity();
16862
+ wrapped = await daemonHandshakeOverSink(
16863
+ sink,
16864
+ (helloBytes) => {
16865
+ const hello = decodePairingHello(helloBytes);
16866
+ const result = respondToHandshake(hello, {
16867
+ identity,
16868
+ verifyOfferToken: (presented) => spec.verify(presented, expected)
16869
+ });
16870
+ capturedSession = result.session;
16871
+ capturedHello = hello;
16872
+ return { reply: encodePairingMessage(result.reply), keys: result.session };
16873
+ },
16874
+ { timeoutMs: handshakeTimeoutMs }
16875
+ );
16876
+ } catch (err) {
16877
+ if (signal.aborted) break;
16878
+ void err;
16879
+ await sleep(backoff, signal);
16880
+ backoff = Math.min(backoff * 2, reconnectMaxMs);
16881
+ continue;
16882
+ }
16883
+ backoff = reconnectMinMs;
16884
+ if (!capturedSession || !capturedHello) {
16885
+ wrapped.close("internal: missing session");
16886
+ continue;
16887
+ }
16888
+ let ctx;
16889
+ try {
16890
+ ctx = await spec.onPaired(capturedSession, capturedHello);
16891
+ } catch (err) {
16892
+ log(`[pairing] persist for ${spec.key} failed: ${errMsg(err)}`);
16893
+ wrapped.close("persist failed");
16894
+ continue;
16895
+ }
16896
+ const handle = deps2.serve(wrapped, ctx);
16897
+ channels.add(handle);
16898
+ log(`[pairing] channel up (${ctx.mode}) for ${ctx.fingerprint} via ${spec.key}`);
16899
+ await waitClosed(wrapped, signal);
16900
+ channels.delete(handle);
16901
+ await handle.close().catch(() => {
16902
+ });
16903
+ log(`[pairing] channel closed (${ctx.mode}) for ${ctx.fingerprint}`);
16904
+ if (spec.singleUse) break;
16905
+ } finally {
16906
+ signal.removeEventListener("abort", onAbort);
16907
+ }
16908
+ }
16909
+ }
16910
+ async function createOffer(input = {}) {
16911
+ await ensureLoaded();
16912
+ let rendezvousUrl;
16913
+ let rendezvousIsHostedDefault = false;
16914
+ if (input.rendezvousUrl) {
16915
+ rendezvousUrl = input.rendezvousUrl;
16916
+ } else if (deps2.defaultRendezvousUrl !== void 0) {
16917
+ if (deps2.defaultRendezvousUrl === "") {
16918
+ throw new PairingError(
16919
+ "malformed_offer",
16920
+ 'rendezvous disabled \u2014 pairing.rendezvous is set to "" (explicit opt-out); pass --rendezvous to route this offer through a specific broker'
16921
+ );
16922
+ }
16923
+ rendezvousUrl = deps2.defaultRendezvousUrl;
16924
+ } else {
16925
+ rendezvousUrl = HOSTED_RENDEZVOUS_URL;
16926
+ rendezvousIsHostedDefault = true;
16927
+ }
16928
+ const identity = await deps2.loadIdentity();
16929
+ const token = b64url(randomBytes(16));
16930
+ const ttlMs = input.ttlMs ?? DEFAULT_TTL_MS;
16931
+ const exp = Math.floor((now() + ttlMs) / 1e3);
16932
+ offers.set(token, { token, exp, spent: false, rendezvousUrl });
16933
+ const fingerprint = identityFingerprint(identity.x25519.pub);
16934
+ const url = encodeOfferUrl({
16935
+ v: 1,
16936
+ rendezvousUrl,
16937
+ fingerprint,
16938
+ daemonX25519Pub: identity.x25519.pub,
16939
+ daemonEd25519Pub: identity.ed25519.pub,
16940
+ token,
16941
+ exp
16942
+ });
16943
+ startOfferLoop(token, rendezvousUrl);
16944
+ log(
16945
+ `[pairing] offer minted (exp ${new Date(exp * 1e3).toISOString()}) via ${rendezvousUrl}` + (rendezvousIsHostedDefault ? " (hosted default)" : "")
16946
+ );
16947
+ return { token, exp, url, fingerprint, rendezvousUrl, rendezvousIsHostedDefault };
16948
+ }
16949
+ function offerValid(token) {
16950
+ const entry = offers.get(token);
16951
+ return !!entry && !entry.spent && entry.exp * 1e3 > now();
16952
+ }
16953
+ function startOfferLoop(token, rendezvousUrl) {
16954
+ startLoop({
16955
+ key: `offer:${token}`,
16956
+ rendezvousUrl,
16957
+ token: () => token,
16958
+ shouldContinue: () => offerValid(token),
16959
+ singleUse: true,
16960
+ verify: (presented, expected) => {
16961
+ const entry = offers.get(expected);
16962
+ if (!entry || entry.spent || entry.exp * 1e3 <= now()) return false;
16963
+ if (!constantTimeEqual(presented, expected)) return false;
16964
+ entry.spent = true;
16965
+ return true;
16966
+ },
16967
+ onPaired: async (session, hello) => {
16968
+ const pairRoot = derivePairRoot(session);
16969
+ const fingerprint = session.peerFingerprint;
16970
+ const nowIso = new Date(now()).toISOString();
16971
+ const record = {
16972
+ clientPub: hello.ePub,
16973
+ name: session.clientName ?? fingerprint,
16974
+ fingerprint,
16975
+ createdAt: pairings.get(fingerprint)?.createdAt ?? nowIso,
16976
+ lastSeen: nowIso,
16977
+ pairRoot,
16978
+ rendezvousUrl
16979
+ };
16980
+ pairings.set(fingerprint, record);
16981
+ await persist();
16982
+ startReconnectLoops(record);
16983
+ return { mode: "offer", fingerprint, name: record.name };
16984
+ }
16985
+ });
16986
+ }
16987
+ function startReconnectLoops(record) {
16988
+ for (const slot of ["cur", "prev"]) {
16989
+ const epochOf = () => slot === "cur" ? currentEpoch(now()) : currentEpoch(now()) - 1;
16990
+ startLoop({
16991
+ key: `pair:${record.fingerprint}:${slot}`,
16992
+ rendezvousUrl: record.rendezvousUrl,
16993
+ token: () => deriveEpochRoutingToken(record.pairRoot, epochOf()),
16994
+ shouldContinue: () => pairings.has(record.fingerprint),
16995
+ singleUse: false,
16996
+ verify: (presented, expected) => constantTimeEqual(presented, expected),
16997
+ onPaired: async (session, _hello) => {
16998
+ const existing = pairings.get(record.fingerprint);
16999
+ if (existing) {
17000
+ existing.lastSeen = new Date(now()).toISOString();
17001
+ await persist().catch((err) => log(`[pairing] lastSeen persist failed: ${errMsg(err)}`));
17002
+ }
17003
+ return {
17004
+ mode: "reconnect",
17005
+ fingerprint: record.fingerprint,
17006
+ name: existing?.name ?? record.name
17007
+ };
17008
+ }
17009
+ });
17010
+ }
17011
+ }
17012
+ async function startAutoconnect() {
17013
+ await ensureLoaded();
17014
+ for (const record of pairings.values()) {
17015
+ startReconnectLoops(record);
17016
+ }
17017
+ if (pairings.size > 0) {
17018
+ log(`[pairing] autoconnect: standing connections for ${pairings.size} pairing(s)`);
17019
+ }
17020
+ }
17021
+ async function revoke(idOrName) {
17022
+ await ensureLoaded();
17023
+ let target = pairings.get(idOrName);
17024
+ if (!target) {
17025
+ for (const rec of pairings.values()) {
17026
+ if (rec.name === idOrName) {
17027
+ target = rec;
17028
+ break;
17029
+ }
17030
+ }
17031
+ }
17032
+ if (!target) return false;
17033
+ pairings.delete(target.fingerprint);
17034
+ await persist();
17035
+ await stopLoop(`pair:${target.fingerprint}:cur`);
17036
+ await stopLoop(`pair:${target.fingerprint}:prev`);
17037
+ log(`[pairing] revoked ${target.fingerprint} (${target.name})`);
17038
+ return true;
17039
+ }
17040
+ async function list() {
17041
+ await ensureLoaded();
17042
+ return Array.from(pairings.values()).map((r) => ({ ...r }));
17043
+ }
17044
+ async function shutdown() {
17045
+ if (shuttingDown) return;
17046
+ shuttingDown = true;
17047
+ for (const key of Array.from(loops.keys())) {
17048
+ await stopLoop(key);
17049
+ }
17050
+ for (const handle of Array.from(channels)) {
17051
+ await handle.close().catch(() => {
17052
+ });
17053
+ }
17054
+ channels.clear();
17055
+ }
17056
+ return { createOffer, list, revoke, startAutoconnect, shutdown };
15650
17057
  }
15651
- function providerEnvVar(provider) {
15652
- return PROVIDER_ENV_VARS[provider] ?? `${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_API_KEY`;
17058
+ function dialUrl(rendezvousUrl, token) {
17059
+ const sep = rendezvousUrl.includes("?") ? "&" : "?";
17060
+ return `${rendezvousUrl}${sep}side=daemon&t=${encodeURIComponent(token)}`;
15653
17061
  }
15654
- async function loadProviders() {
15655
- try {
15656
- const raw = await readFile(providersPath(), "utf8");
15657
- const parsed = JSON.parse(raw);
15658
- if (!parsed || typeof parsed !== "object" || !("providers" in parsed) || typeof parsed.providers !== "object" || parsed.providers === null) {
15659
- return emptyFile();
17062
+ function waitClosed(sink, signal) {
17063
+ return new Promise((resolve12) => {
17064
+ if (!sink.isOpen) {
17065
+ resolve12();
17066
+ return;
15660
17067
  }
15661
- return {
15662
- version: 1,
15663
- providers: parsed.providers
17068
+ let settled = false;
17069
+ const finish = () => {
17070
+ if (settled) return;
17071
+ settled = true;
17072
+ resolve12();
15664
17073
  };
15665
- } catch {
15666
- return emptyFile();
15667
- }
17074
+ sink.onClose(() => finish());
17075
+ signal.addEventListener("abort", () => {
17076
+ sink.close("registry shutdown");
17077
+ finish();
17078
+ });
17079
+ });
15668
17080
  }
15669
- async function writeProviders(file) {
15670
- const dir = join(homedir(), ".agentproto");
15671
- await mkdir(dir, { recursive: true });
15672
- await writeFile(providersPath(), JSON.stringify(file, null, 2) + "\n", {
15673
- encoding: "utf8",
15674
- mode: 384
17081
+ function sleep(ms, signal) {
17082
+ return new Promise((resolve12) => {
17083
+ if (signal.aborted) return resolve12();
17084
+ const timer = setTimeout(resolve12, ms);
17085
+ if (typeof timer.unref === "function") timer.unref();
17086
+ signal.addEventListener("abort", () => {
17087
+ clearTimeout(timer);
17088
+ resolve12();
17089
+ });
15675
17090
  });
15676
17091
  }
15677
- async function setProviderKey(provider, apiKey, baseUrl) {
15678
- const file = await loadProviders();
15679
- file.providers[provider] = {
15680
- apiKey,
15681
- ...baseUrl ? { baseUrl } : {},
15682
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
15683
- };
15684
- await writeProviders(file);
15685
- return providerEnvVar(provider);
15686
- }
15687
- async function removeProviderKey(provider) {
15688
- const file = await loadProviders();
15689
- if (!(provider in file.providers)) return false;
15690
- delete file.providers[provider];
15691
- await writeProviders(file);
15692
- return true;
17092
+ function isPairingsFile(v) {
17093
+ if (typeof v !== "object" || v === null) return false;
17094
+ const rec = v;
17095
+ if (rec["v"] !== PAIRINGS_VERSION) return false;
17096
+ if (!Array.isArray(rec["pairings"])) return false;
17097
+ return rec["pairings"].every(isPairingRecord);
15693
17098
  }
15694
- async function injectProviderKeysIntoEnv(env = process.env) {
15695
- const file = await loadProviders();
15696
- const injected = [];
15697
- for (const [provider, entry] of Object.entries(file.providers)) {
15698
- if (!entry?.apiKey) continue;
15699
- const name = providerEnvVar(provider);
15700
- if (env[name]) continue;
15701
- env[name] = entry.apiKey;
15702
- if (entry.baseUrl) env[`${provider.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}_BASE_URL`] = entry.baseUrl;
15703
- injected.push(provider);
15704
- }
15705
- return injected;
17099
+ function isPairingRecord(v) {
17100
+ if (typeof v !== "object" || v === null) return false;
17101
+ const r = v;
17102
+ return typeof r["clientPub"] === "string" && typeof r["name"] === "string" && typeof r["fingerprint"] === "string" && typeof r["createdAt"] === "string" && typeof r["lastSeen"] === "string" && typeof r["pairRoot"] === "string" && typeof r["rendezvousUrl"] === "string";
17103
+ }
17104
+ function isEnoent(err) {
17105
+ return typeof err === "object" && err !== null && "code" in err && err.code === "ENOENT";
17106
+ }
17107
+ function errMsg(err) {
17108
+ return err instanceof Error ? err.message : String(err);
15706
17109
  }
15707
17110
 
15708
17111
  // src/index.ts
@@ -15714,7 +17117,9 @@ var DEFAULT_ALWAYS_ON_TOOLS = [
15714
17117
  "agent_kill",
15715
17118
  "session_list",
15716
17119
  "session_monitor",
15717
- "session_events_poll"
17120
+ "session_events_poll",
17121
+ "permissions_list",
17122
+ "permissions_respond"
15718
17123
  ];
15719
17124
  async function createGateway(opts) {
15720
17125
  const startedAt = Date.now();
@@ -15891,6 +17296,9 @@ async function createGateway(opts) {
15891
17296
  registerDaemonHealthTools(server, { workspace, registered, startedAt });
15892
17297
  registerCommandTools(server, { workspace, registry: sessions });
15893
17298
  registerRemoteTools(server, { controller: remote });
17299
+ if (opts.pairingRegistry) {
17300
+ registerPairingTools(server, { registry: opts.pairingRegistry });
17301
+ }
15894
17302
  registerSessionTools(server, {
15895
17303
  registry: sessions,
15896
17304
  mcpProxy,
@@ -16038,6 +17446,7 @@ async function createGateway(opts) {
16038
17446
  token,
16039
17447
  ptyEnabled: opts.spawnPty != null,
16040
17448
  tunnels,
17449
+ ...opts.pairingRegistry ? { pairings: opts.pairingRegistry } : {},
16041
17450
  sessionEvents,
16042
17451
  eventRing,
16043
17452
  supervisor,
@@ -16082,6 +17491,7 @@ async function createGateway(opts) {
16082
17491
  registered,
16083
17492
  sessions,
16084
17493
  tunnels,
17494
+ ...opts.pairingRegistry ? { pairing: opts.pairingRegistry } : {},
16085
17495
  token,
16086
17496
  mintOrchestratorScope: scopeTokens.mint,
16087
17497
  async stop() {
@@ -16090,6 +17500,10 @@ async function createGateway(opts) {
16090
17500
  cronScheduler.shutdown();
16091
17501
  supervisor.shutdown();
16092
17502
  sessions.shutdown();
17503
+ if (opts.pairingRegistry) {
17504
+ await opts.pairingRegistry.shutdown().catch(() => {
17505
+ });
17506
+ }
16093
17507
  await mcpProxy.closeAll();
16094
17508
  await tunnels.shutdown();
16095
17509
  await remote.shutdown();
@@ -16125,7 +17539,15 @@ async function runBoot(workspace, opts, conversations, events) {
16125
17539
  durationMs: 0
16126
17540
  });
16127
17541
  }
17542
+ var export_PROVIDER_ENV_VARS = providers_store_exports.PROVIDER_ENV_VARS;
17543
+ var export_getProviderKey = providers_store_exports.getProviderKey;
17544
+ var export_injectProviderKeysIntoEnv = providers_store_exports.injectProviderKeysIntoEnv;
17545
+ var export_loadProviders = providers_store_exports.loadProviders;
17546
+ var export_providerEnvVar = providers_store_exports.providerEnvVar;
17547
+ var export_providersPath = providers_store_exports.providersPath;
17548
+ var export_removeProviderKey = providers_store_exports.removeProviderKey;
17549
+ var export_setProviderKey = providers_store_exports.setProviderKey;
16128
17550
 
16129
- export { DEFAULT_ORCHESTRATOR_TOOLS, PROVIDER_ENV_VARS, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createScopeTokenRegistry, createWorkspaceFs, daemonRegistryDir, declaredPresetToProviderPreset, deriveSessionUsage, fileConversationStore, formatToolCall, formatToolResult, getMcpCredentialDeps, injectProviderKeysIntoEnv, listPresets, loadProviders, makeBrowserAdapterLister, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, parseDuration, projectSessionUsage, providerEnvVar, providersPath, readDaemonRegistry, readRuntimeMeta, removeProviderKey, setMcpCredentialDeps, setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
17551
+ export { AuthResolutionError, DEFAULT_ORCHESTRATOR_TOOLS, PAIRINGS_VERSION, export_PROVIDER_ENV_VARS as PROVIDER_ENV_VARS, composeSessionObservers, createFileStepCache, createGateway, createOrchestratorInjector, createOrchestratorMcpServerFactory, createPairingRegistry, createScopeTokenRegistry, createWorkspaceFs, credentialFingerprint, daemonRegistryDir, declaredPresetToProviderPreset, deriveSessionUsage, fileConversationStore, formatToolCall, formatToolResult, getMcpCredentialDeps, export_getProviderKey as getProviderKey, export_injectProviderKeysIntoEnv as injectProviderKeysIntoEnv, listPresets, export_loadProviders as loadProviders, makeBrowserAdapterLister, monitorPolicyWait, monitorSessionWait, narrowOrchestratorTools, normalizeSkillsOption, parseDuration, projectSessionUsage, export_providerEnvVar as providerEnvVar, export_providersPath as providersPath, readDaemonRegistry, readRuntimeMeta, registerPairingTools, export_removeProviderKey as removeProviderKey, resolveAuthSpec, resolveSpawnDefaults, setMcpCredentialDeps, export_setProviderKey as setProviderKey, sweepStaleDaemonRegistry, sweepStaleRuntimeMetas, unlinkDaemonRegistryEntry, unlinkRuntimeMeta, writeDaemonRegistryEntry };
16130
17552
  //# sourceMappingURL=index.mjs.map
16131
17553
  //# sourceMappingURL=index.mjs.map