@ganglion/xacpx 0.24.4-beta.0 → 0.24.5

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.
@@ -0,0 +1,35 @@
1
+ export type BotRuntimeScope = "bot-direct" | "group-member" | "group-controller";
2
+ export interface BotProfile {
3
+ id: string;
4
+ name: string;
5
+ avatar?: string;
6
+ role?: string;
7
+ instructions?: string;
8
+ agent: string;
9
+ workspace: string;
10
+ cwd?: string;
11
+ model?: string;
12
+ effort?: string;
13
+ enabled: boolean;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
17
+ interface BotRuntimeBindingBase {
18
+ id: string;
19
+ conversationId: string;
20
+ topicId: string;
21
+ logicalSessionId: string;
22
+ sessionAlias: string;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ }
26
+ export type BotRuntimeBinding = (BotRuntimeBindingBase & {
27
+ scope: "bot-direct";
28
+ botId: string;
29
+ }) | (BotRuntimeBindingBase & {
30
+ scope: "group-member";
31
+ botId: string;
32
+ }) | (BotRuntimeBindingBase & {
33
+ scope: "group-controller";
34
+ });
35
+ export {};
@@ -9911,6 +9911,45 @@ var init_session_effort = __esm(() => {
9911
9911
  ]);
9912
9912
  });
9913
9913
 
9914
+ // src/util/async.ts
9915
+ function settleWithinTimeout(work, timeoutMs) {
9916
+ return new Promise((resolve3) => {
9917
+ let settled = false;
9918
+ const finish = () => {
9919
+ if (!settled) {
9920
+ settled = true;
9921
+ resolve3();
9922
+ }
9923
+ };
9924
+ const timer = setTimeout(finish, timeoutMs);
9925
+ if (typeof timer.unref === "function") {
9926
+ timer.unref();
9927
+ }
9928
+ work.then(() => {
9929
+ clearTimeout(timer);
9930
+ finish();
9931
+ }, () => {
9932
+ clearTimeout(timer);
9933
+ finish();
9934
+ });
9935
+ });
9936
+ }
9937
+ function raceWithTimeout(work, timeoutMs, onTimeout) {
9938
+ return new Promise((resolve3, reject) => {
9939
+ const timer = setTimeout(() => reject(onTimeout()), timeoutMs);
9940
+ if (typeof timer.unref === "function") {
9941
+ timer.unref();
9942
+ }
9943
+ work.then((value) => {
9944
+ clearTimeout(timer);
9945
+ resolve3(value);
9946
+ }, (error) => {
9947
+ clearTimeout(timer);
9948
+ reject(error);
9949
+ });
9950
+ });
9951
+ }
9952
+
9914
9953
  // src/bridge/engine/runtime/physical-session-identity.ts
9915
9954
  import { createHash as createHash6 } from "node:crypto";
9916
9955
  import { resolve as resolvePath } from "node:path";
@@ -10266,7 +10305,7 @@ function isEligibleForRuntime(policy, nonInteractivePermissions, interactiveAvai
10266
10305
  }
10267
10306
  return true;
10268
10307
  }
10269
- function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings, transport) {
10308
+ function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings, transport, options) {
10270
10309
  if (!hasPersistedRuntimeBindings)
10271
10310
  return;
10272
10311
  let parsedPolicy;
@@ -10274,7 +10313,7 @@ function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings, t
10274
10313
  parsedPolicy = typeof transport.permissionPolicy === "object" && transport.permissionPolicy !== null ? transport.permissionPolicy : parseXacpxPermissionPolicy(transport.permissionPolicy);
10275
10314
  }
10276
10315
  const nonInteractive = typeof transport.nonInteractivePermissions === "string" ? transport.nonInteractivePermissions : undefined;
10277
- if (!isEligibleForRuntime(parsedPolicy, nonInteractive, false)) {
10316
+ if (!isEligibleForRuntime(parsedPolicy, nonInteractive, options?.interactionAvailable ?? false)) {
10278
10317
  throw new Error('cannot apply permission policy: runtime-ineligible policy (nonInteractive="fail" or escalate without interactive) with persisted runtime bindings');
10279
10318
  }
10280
10319
  }
@@ -10286,6 +10325,7 @@ var init_runtime_permission_policy = __esm(() => {
10286
10325
  // src/bridge/engine/runtime/runtime-permission-resolver.ts
10287
10326
  var exports_runtime_permission_resolver = {};
10288
10327
  __export(exports_runtime_permission_resolver, {
10328
+ readToolInputFromReq: () => readToolInputFromReq,
10289
10329
  configFromRaw: () => configFromRaw,
10290
10330
  RuntimePermissionResolver: () => RuntimePermissionResolver
10291
10331
  });
@@ -10344,6 +10384,49 @@ function readRawKindFromReq(req) {
10344
10384
  }
10345
10385
  return;
10346
10386
  }
10387
+ function readToolInputFromReq(req) {
10388
+ const raw = getRawObject(req);
10389
+ if (!raw)
10390
+ return;
10391
+ const toolCall = raw.toolCall ?? raw.tool;
10392
+ if (toolCall && typeof toolCall === "object" && !Array.isArray(toolCall)) {
10393
+ const rec = toolCall;
10394
+ if (rec.rawInput !== undefined)
10395
+ return rec.rawInput;
10396
+ if (rec.input !== undefined)
10397
+ return rec.input;
10398
+ const contentText = readTextFromToolContent(rec.content);
10399
+ if (contentText !== undefined)
10400
+ return contentText;
10401
+ }
10402
+ if (raw.rawInput !== undefined)
10403
+ return raw.rawInput;
10404
+ if (raw.input !== undefined)
10405
+ return raw.input;
10406
+ if (raw.subject !== undefined && raw.subject !== null)
10407
+ return raw.subject;
10408
+ if (typeof raw.description === "string" && raw.description.trim().length > 0) {
10409
+ return raw.description;
10410
+ }
10411
+ return;
10412
+ }
10413
+ function readTextFromToolContent(content) {
10414
+ if (!Array.isArray(content))
10415
+ return;
10416
+ const parts = [];
10417
+ for (const block of content.slice(0, 8)) {
10418
+ if (block && typeof block === "object" && !Array.isArray(block)) {
10419
+ const rec = block;
10420
+ if (rec.type === "text" && typeof rec.text === "string" && rec.text.trim().length > 0) {
10421
+ parts.push(rec.text);
10422
+ }
10423
+ }
10424
+ }
10425
+ if (parts.length === 0)
10426
+ return;
10427
+ return parts.join(`
10428
+ `);
10429
+ }
10347
10430
  function inferToolKindForReq(req) {
10348
10431
  if (req.inferredKind && typeof req.inferredKind === "string" && req.inferredKind.trim().length > 0) {
10349
10432
  return normalizeMatcher(req.inferredKind);
@@ -12284,10 +12367,8 @@ class RuntimeWorkerClient {
12284
12367
  if (!handler) {
12285
12368
  decision = { outcome: "reject_once" };
12286
12369
  } else {
12287
- const withTimeout = await Promise.race([
12288
- handler(payload),
12289
- new Promise((_, reject) => setTimeout(() => reject(new Error("permission UI timeout")), 8000).unref?.())
12290
- ]);
12370
+ const timeoutMs = this.deps?.permissionTimeoutMs ?? 125000;
12371
+ const withTimeout = await raceWithTimeout(handler(payload), timeoutMs, () => new Error("permission UI timeout"));
12291
12372
  const outcome = withTimeout?.outcome;
12292
12373
  if (outcome !== "allow_once" && outcome !== "allow_always" && outcome !== "reject_once" && outcome !== "reject_always" && outcome !== "cancel") {
12293
12374
  decision = { outcome: "reject_once" };
@@ -14343,6 +14424,7 @@ class RuntimeEngine {
14343
14424
  const fenceDir = this.options.fenceDir ?? (() => join22(this.durableRoot(), "worker-fences"));
14344
14425
  const permissionDeps = {
14345
14426
  ...options.workerClientDeps ?? {},
14427
+ ...options.permissionRequestTimeoutMs !== undefined ? options.workerClientDeps?.permissionTimeoutMs === undefined ? { permissionTimeoutMs: options.permissionRequestTimeoutMs } : {} : {},
14346
14428
  spawnEnv: {
14347
14429
  ...resolveAcpxHostPolicyEnv({
14348
14430
  acpxMaxIncomingMessageBytes: options.acpxMaxIncomingMessageBytes,
@@ -14437,7 +14519,7 @@ class RuntimeEngine {
14437
14519
  await this.ensureSessionHandle(input, client, agentProcessEnv);
14438
14520
  try {
14439
14521
  const attachments = await buildRuntimeAttachments(options.media);
14440
- const outcome = await client.request("prompt", { text, ...attachments.length > 0 ? { attachments } : {} }, {
14522
+ const outcome = await client.request("prompt", { text, ...attachments.length > 0 ? { attachments } : {}, ...options.interactionId ? { interactionId: options.interactionId } : {} }, {
14441
14523
  onEvent: (payload) => {
14442
14524
  const event = payload;
14443
14525
  const sink = options.onEvent;
@@ -14684,7 +14766,7 @@ class RuntimeEngine {
14684
14766
  isRuntimeEligible() {
14685
14767
  try {
14686
14768
  const policy = this.options.permissionPolicy !== undefined ? parseXacpxPermissionPolicy(this.options.permissionPolicy) : undefined;
14687
- const interactiveAvailable = this.options.permissionInteractionAvailable === true;
14769
+ const interactiveAvailable = this.options.permissionInteractionCapable === true;
14688
14770
  return isEligibleForRuntime(policy, this.options.nonInteractivePermissions, interactiveAvailable);
14689
14771
  } catch {
14690
14772
  return false;
@@ -14703,10 +14785,8 @@ class RuntimeEngine {
14703
14785
  return { outcome: "reject_once" };
14704
14786
  if (this.options.onPermissionRequest) {
14705
14787
  try {
14706
- const res = await Promise.race([
14707
- this.options.onPermissionRequest(payload),
14708
- new Promise((_, reject) => setTimeout(() => reject(new Error("permission UI timeout")), 8000).unref?.())
14709
- ]);
14788
+ const timeoutMs = this.options.permissionRequestTimeoutMs ?? 125000;
14789
+ const res = await raceWithTimeout(this.options.onPermissionRequest(payload), timeoutMs, () => new Error("permission UI timeout"));
14710
14790
  if (this.deleting.has(key) || this.shuttingDown)
14711
14791
  return { outcome: "reject_once" };
14712
14792
  if (payload.policyGeneration !== this.permissionGeneration)
@@ -15287,7 +15367,7 @@ class RuntimeEngine {
15287
15367
  throw new RuntimeError("RUNTIME_INIT_FAILED", `session "${key}" is being deleted`);
15288
15368
  }
15289
15369
  try {
15290
- const result = await this.executeRuntimeTurn(input, input.text, { onEvent, media: input.media, toolEventMode: input.toolEventMode, toolEvents: input.toolEvents });
15370
+ const result = await this.executeRuntimeTurn(input, input.text, { onEvent, media: input.media, toolEventMode: input.toolEventMode, toolEvents: input.toolEvents, ...input.interactionId ? { interactionId: input.interactionId } : {} });
15291
15371
  return result;
15292
15372
  } finally {
15293
15373
  try {
@@ -16726,6 +16806,7 @@ class BridgeServer {
16726
16806
  case "prompt":
16727
16807
  const media = asOptionalPromptMediaInput(params.media);
16728
16808
  const resolvedToolEventMode = asOptionalToolEventMode(params.toolEventMode);
16809
+ const promptInteractionId = asOptionalString(params.interactionId);
16729
16810
  return await this.engines.prompt({
16730
16811
  agent: requireString(params, "agent"),
16731
16812
  ...agentExecutionSettings(params),
@@ -16742,7 +16823,8 @@ class BridgeServer {
16742
16823
  replyMode: asOptionalReplyMode(params.replyMode),
16743
16824
  toolEvents: params.toolEvents === true,
16744
16825
  ...resolvedToolEventMode ? { toolEventMode: resolvedToolEventMode } : {},
16745
- media
16826
+ media,
16827
+ ...promptInteractionId ? { interactionId: promptInteractionId } : {}
16746
16828
  }, (event) => {
16747
16829
  if (event.type === "prompt.segment") {
16748
16830
  writeLine?.(encodeBridgePromptSegmentEvent({
@@ -17249,9 +17331,10 @@ async function runBridgeMain() {
17249
17331
  durableRootDir: durableRoot,
17250
17332
  queueDir,
17251
17333
  fenceDir,
17334
+ permissionInteractionCapable: coreEnv("BRIDGE_PERMISSION_INTERACTION_CAPABLE") === "1",
17252
17335
  onPermissionRequest: async (payload) => {
17253
17336
  try {
17254
- const result = await server.requestDaemon("resolvePermissionRequest", payload, { timeoutMs: 8000 });
17337
+ const result = await server.requestDaemon("resolvePermissionRequest", payload, { timeoutMs: 125000 });
17255
17338
  const outcome = result?.outcome;
17256
17339
  if (outcome === "allow_once" || outcome === "allow_always" || outcome === "reject_once" || outcome === "reject_always" || outcome === "cancel") {
17257
17340
  return { outcome };
@@ -129,6 +129,7 @@ export interface EnginePromptInput extends EngineSessionInput {
129
129
  toolEvents?: boolean;
130
130
  toolEventMode?: ToolEventMode;
131
131
  media?: PromptMediaInput;
132
+ interactionId?: string;
132
133
  }
133
134
  export interface EngineInjectInput extends EngineSessionInput {
134
135
  text: string;
@@ -10,7 +10,7 @@ export declare function isEligibleForRuntime(policy: XacpxPermissionPolicy | und
10
10
  * Shared fail-closed gate for permission changes while persisted Runtime
11
11
  * bindings exist (watcher reload in main.ts, `/config set` + `/pm` handlers).
12
12
  * Pure: takes the already-loaded transport snapshot and the bindings flag, so
13
- * the two call sites cannot drift on `permissionInteractionAvailable` or the
13
+ * the two call sites cannot drift on `permissionInteractionCapable` or the
14
14
  * error contract. Throws with the canonical message when the change must not
15
15
  * apply; returns without effect otherwise (including when no Runtime bindings
16
16
  * exist — any permission tuple is then appliable).
@@ -18,4 +18,6 @@ export declare function isEligibleForRuntime(policy: XacpxPermissionPolicy | und
18
18
  export declare function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings: boolean, transport: {
19
19
  permissionPolicy?: unknown;
20
20
  nonInteractivePermissions?: unknown;
21
+ }, options?: {
22
+ interactionAvailable?: boolean;
21
23
  }): void;
@@ -46,6 +46,45 @@ var __export = (target, all) => {
46
46
  var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
47
47
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
48
48
 
49
+ // src/util/async.ts
50
+ function settleWithinTimeout(work, timeoutMs) {
51
+ return new Promise((resolve) => {
52
+ let settled = false;
53
+ const finish = () => {
54
+ if (!settled) {
55
+ settled = true;
56
+ resolve();
57
+ }
58
+ };
59
+ const timer = setTimeout(finish, timeoutMs);
60
+ if (typeof timer.unref === "function") {
61
+ timer.unref();
62
+ }
63
+ work.then(() => {
64
+ clearTimeout(timer);
65
+ finish();
66
+ }, () => {
67
+ clearTimeout(timer);
68
+ finish();
69
+ });
70
+ });
71
+ }
72
+ function raceWithTimeout(work, timeoutMs, onTimeout) {
73
+ return new Promise((resolve, reject) => {
74
+ const timer = setTimeout(() => reject(onTimeout()), timeoutMs);
75
+ if (typeof timer.unref === "function") {
76
+ timer.unref();
77
+ }
78
+ work.then((value) => {
79
+ clearTimeout(timer);
80
+ resolve(value);
81
+ }, (error) => {
82
+ clearTimeout(timer);
83
+ reject(error);
84
+ });
85
+ });
86
+ }
87
+
49
88
  // src/runtime/core-env.ts
50
89
  function coreEnv(suffix, env = process.env) {
51
90
  return env[`${PRIMARY_PREFIX}${suffix}`] ?? env[`${LEGACY_PREFIX}${suffix}`];
@@ -2799,7 +2838,7 @@ function isEligibleForRuntime(policy, nonInteractivePermissions, interactiveAvai
2799
2838
  }
2800
2839
  return true;
2801
2840
  }
2802
- function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings, transport) {
2841
+ function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings, transport, options) {
2803
2842
  if (!hasPersistedRuntimeBindings)
2804
2843
  return;
2805
2844
  let parsedPolicy;
@@ -2807,7 +2846,7 @@ function assertEligibleForRuntimePermissionChange(hasPersistedRuntimeBindings, t
2807
2846
  parsedPolicy = typeof transport.permissionPolicy === "object" && transport.permissionPolicy !== null ? transport.permissionPolicy : parseXacpxPermissionPolicy(transport.permissionPolicy);
2808
2847
  }
2809
2848
  const nonInteractive = typeof transport.nonInteractivePermissions === "string" ? transport.nonInteractivePermissions : undefined;
2810
- if (!isEligibleForRuntime(parsedPolicy, nonInteractive, false)) {
2849
+ if (!isEligibleForRuntime(parsedPolicy, nonInteractive, options?.interactionAvailable ?? false)) {
2811
2850
  throw new Error('cannot apply permission policy: runtime-ineligible policy (nonInteractive="fail" or escalate without interactive) with persisted runtime bindings');
2812
2851
  }
2813
2852
  }
@@ -2819,6 +2858,7 @@ var init_runtime_permission_policy = __esm(() => {
2819
2858
  // src/bridge/engine/runtime/runtime-permission-resolver.ts
2820
2859
  var exports_runtime_permission_resolver = {};
2821
2860
  __export(exports_runtime_permission_resolver, {
2861
+ readToolInputFromReq: () => readToolInputFromReq,
2822
2862
  configFromRaw: () => configFromRaw,
2823
2863
  RuntimePermissionResolver: () => RuntimePermissionResolver
2824
2864
  });
@@ -2877,6 +2917,49 @@ function readRawKindFromReq(req) {
2877
2917
  }
2878
2918
  return;
2879
2919
  }
2920
+ function readToolInputFromReq(req) {
2921
+ const raw = getRawObject(req);
2922
+ if (!raw)
2923
+ return;
2924
+ const toolCall = raw.toolCall ?? raw.tool;
2925
+ if (toolCall && typeof toolCall === "object" && !Array.isArray(toolCall)) {
2926
+ const rec = toolCall;
2927
+ if (rec.rawInput !== undefined)
2928
+ return rec.rawInput;
2929
+ if (rec.input !== undefined)
2930
+ return rec.input;
2931
+ const contentText = readTextFromToolContent(rec.content);
2932
+ if (contentText !== undefined)
2933
+ return contentText;
2934
+ }
2935
+ if (raw.rawInput !== undefined)
2936
+ return raw.rawInput;
2937
+ if (raw.input !== undefined)
2938
+ return raw.input;
2939
+ if (raw.subject !== undefined && raw.subject !== null)
2940
+ return raw.subject;
2941
+ if (typeof raw.description === "string" && raw.description.trim().length > 0) {
2942
+ return raw.description;
2943
+ }
2944
+ return;
2945
+ }
2946
+ function readTextFromToolContent(content) {
2947
+ if (!Array.isArray(content))
2948
+ return;
2949
+ const parts = [];
2950
+ for (const block of content.slice(0, 8)) {
2951
+ if (block && typeof block === "object" && !Array.isArray(block)) {
2952
+ const rec = block;
2953
+ if (rec.type === "text" && typeof rec.text === "string" && rec.text.trim().length > 0) {
2954
+ parts.push(rec.text);
2955
+ }
2956
+ }
2957
+ }
2958
+ if (parts.length === 0)
2959
+ return;
2960
+ return parts.join(`
2961
+ `);
2962
+ }
2880
2963
  function inferToolKindForReq(req) {
2881
2964
  if (req.inferredKind && typeof req.inferredKind === "string" && req.inferredKind.trim().length > 0) {
2882
2965
  return normalizeMatcher(req.inferredKind);
@@ -6251,6 +6334,7 @@ var init_runtime_mcp = __esm(() => {
6251
6334
  });
6252
6335
 
6253
6336
  // src/bridge/engine/runtime/runtime-worker-main.ts
6337
+ import { randomUUID as randomUUID5 } from "node:crypto";
6254
6338
  import { createInterface } from "node:readline";
6255
6339
 
6256
6340
  // src/bridge/engine/runtime/worker-eof.ts
@@ -7467,23 +7551,42 @@ async function initializeRuntime(params) {
7467
7551
  return { outcome: "reject_once" };
7468
7552
  if (ctx.signal.aborted)
7469
7553
  return { outcome: "reject_once" };
7470
- const requestId = `perm-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`;
7471
- const toolCallId = (() => {
7472
- const raw = req.raw;
7473
- const id = typeof raw?.toolCall?.toolCallId === "string" && raw.toolCall.toolCallId.length > 0 ? raw.toolCall.toolCallId : typeof raw?.toolCall?.id === "string" && raw.toolCall.id.length > 0 ? raw.toolCall.id : requestId;
7474
- return id;
7475
- })();
7476
- const title = (() => {
7477
- const raw = req.raw;
7478
- const t2 = raw?.toolCall?.title;
7479
- return typeof t2 === "string" ? t2 : undefined;
7480
- })();
7481
- const kind = (() => {
7482
- const raw = req.raw;
7483
- const k = raw?.toolCall?.kind;
7484
- return typeof k === "string" ? k : undefined;
7554
+ const activeInteractionId = state.activeInteractionId;
7555
+ if (!activeInteractionId)
7556
+ return { outcome: "reject_once" };
7557
+ const requestId = randomUUID5();
7558
+ const asRecord = (value) => {
7559
+ if (!value || typeof value !== "object" || Array.isArray(value))
7560
+ return;
7561
+ return value;
7562
+ };
7563
+ const rawValue = "raw" in req ? asRecord(req.raw) : undefined;
7564
+ const toolValue = rawValue?.toolCall ?? rawValue?.tool;
7565
+ const toolRecord = asRecord(toolValue);
7566
+ const readField = (holder, key) => {
7567
+ if (!holder)
7568
+ return;
7569
+ const value = holder[key];
7570
+ return typeof value === "string" && value.length > 0 ? value : undefined;
7571
+ };
7572
+ const toolCallId = readField(toolRecord, "toolCallId") ?? readField(toolRecord, "id") ?? requestId;
7573
+ const title = readField(rawValue, "title") ?? readField(toolRecord, "title");
7574
+ const inferredKind = "inferredKind" in req && typeof req.inferredKind === "string" && req.inferredKind ? req.inferredKind : undefined;
7575
+ const kind = inferredKind ?? readField(rawValue, "kind") ?? readField(toolRecord, "kind");
7576
+ const rawInput = readToolInputFromReq(req);
7577
+ const availableOutcomes = (() => {
7578
+ const options = rawValue?.options;
7579
+ if (!Array.isArray(options))
7580
+ return;
7581
+ const kinds = new Set;
7582
+ for (const option of options) {
7583
+ const record = asRecord(option);
7584
+ if (record && typeof record.kind === "string")
7585
+ kinds.add(record.kind);
7586
+ }
7587
+ const valid = ["allow_once", "allow_always", "reject_once", "reject_always", "cancel"].filter((k) => kinds.has(k));
7588
+ return valid.length > 0 ? valid : undefined;
7485
7589
  })();
7486
- const rawInput = req.raw;
7487
7590
  const payload = {
7488
7591
  logicalSessionId: state.ensureParams?.logicalSessionId ?? params.logicalSessionId ?? state.ensureParams?.sessionKey ?? params.sessionKey,
7489
7592
  sessionKey: state.ensureParams?.sessionKey ?? params.sessionKey,
@@ -7493,7 +7596,9 @@ async function initializeRuntime(params) {
7493
7596
  ...kind ? { kind } : {},
7494
7597
  ...rawInput !== undefined ? { rawInput } : {},
7495
7598
  policyGeneration: state.permissionGeneration,
7496
- workerGeneration: state.workerGeneration
7599
+ workerGeneration: state.workerGeneration,
7600
+ interactionId: activeInteractionId,
7601
+ ...availableOutcomes ? { availableOutcomes } : {}
7497
7602
  };
7498
7603
  const pending = new Promise((resolve, reject) => {
7499
7604
  state.pendingPermissions.set(requestId, { resolve, reject, generation: state.permissionGeneration, workerGeneration: state.workerGeneration });
@@ -7510,10 +7615,7 @@ async function initializeRuntime(params) {
7510
7615
  });
7511
7616
  process.stdout.write(encodeWorkerMessage({ id: requestId, event: "permission.request", payload }));
7512
7617
  try {
7513
- const decision = await Promise.race([
7514
- pending,
7515
- new Promise((_, reject) => setTimeout(() => reject(new Error("host permission timeout")), 9000).unref?.())
7516
- ]);
7618
+ const decision = await raceWithTimeout(pending, 125000, () => new Error("host permission timeout"));
7517
7619
  const outcome = decision.outcome;
7518
7620
  if (outcome !== "allow_once" && outcome !== "allow_always" && outcome !== "reject_once" && outcome !== "reject_always" && outcome !== "cancel") {
7519
7621
  return { outcome: "reject_once" };
@@ -7601,6 +7703,9 @@ async function runPrompt(requestId, params) {
7601
7703
  onElicitation
7602
7704
  });
7603
7705
  state.activeTurn = turn;
7706
+ const promptInteractionId = params.interactionId;
7707
+ if (promptInteractionId)
7708
+ state.activeInteractionId = promptInteractionId;
7604
7709
  try {
7605
7710
  await turn.promptStarted;
7606
7711
  let finalText = "";
@@ -7615,6 +7720,9 @@ async function runPrompt(requestId, params) {
7615
7720
  } finally {
7616
7721
  if (state.activeTurn === turn)
7617
7722
  state.activeTurn = undefined;
7723
+ if (promptInteractionId && state.activeInteractionId === promptInteractionId) {
7724
+ state.activeInteractionId = undefined;
7725
+ }
7618
7726
  }
7619
7727
  }
7620
7728
  async function dispatch(request) {
@@ -10,6 +10,36 @@ import type { ActiveTurnRegistry } from "../sessions/active-turn-registry.js";
10
10
  import type { Locale } from "../i18n/index.js";
11
11
  import type { ControlService } from "../control/control-service.js";
12
12
  export type { ChatAgent };
13
+ export type PermissionOutcome = "allow_once" | "allow_always" | "reject_once" | "reject_always" | "cancel";
14
+ export interface ChannelPermissionRequest {
15
+ requestId: string;
16
+ chatKey: string;
17
+ accountId?: string;
18
+ replyContextToken?: string;
19
+ requester: {
20
+ senderId: string;
21
+ senderName?: string;
22
+ isOwner?: boolean;
23
+ };
24
+ toolCallId: string;
25
+ title?: string;
26
+ kind?: string;
27
+ summary?: string;
28
+ availableOutcomes: PermissionOutcome[];
29
+ expiresAt: number;
30
+ signal: AbortSignal;
31
+ }
32
+ export interface ChannelPermissionDecision {
33
+ outcome: PermissionOutcome;
34
+ /**
35
+ * Platform user id that activated the approval control. REQUIRED: every
36
+ * `requestPermission()` implementation must return an authenticated
37
+ * responder; the broker re-verifies it against the bound initiator (I3).
38
+ * A platform that cannot prove responder identity must not implement
39
+ * `requestPermission()` at all.
40
+ */
41
+ responderId: string;
42
+ }
13
43
  export interface OutboundQuota {
14
44
  onInbound(chatKey: string): void;
15
45
  reserveMidSegment(chatKey: string): boolean;
@@ -151,6 +181,14 @@ export interface MessageChannelRuntime {
151
181
  notifyTaskProgress(task: OrchestrationTaskRecord, text: string): Promise<void>;
152
182
  sendCoordinatorMessage(input: CoordinatorMessageInput): Promise<void>;
153
183
  sendScheduledMessage?(input: ScheduledChannelMessageInput): Promise<void>;
184
+ /**
185
+ * Interactive permission UI (plan channel-permission-interaction).
186
+ * Optional so already-published plugins stay compatible; absent means
187
+ * interactive permission is unavailable and the broker fails closed.
188
+ * Implementations MUST verify the responder equals request.requester.senderId
189
+ * and MUST settle exactly once (first terminal decision wins).
190
+ */
191
+ requestPermission?(request: ChannelPermissionRequest): Promise<ChannelPermissionDecision>;
154
192
  /**
155
193
  * Preferred render format for `/ssn` native session lists. weixin renders
156
194
  * markdown tables poorly and declares "cards"; channels that omit this are