@mono-agent/agent-runtime 0.18.0 → 0.18.1

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/MIGRATION.md CHANGED
@@ -63,7 +63,23 @@ the configuration schema.
63
63
  authoritative and the runtime emits a bounded
64
64
  `live_input_callback_failed` warning.
65
65
 
66
- ## 0.18.x
66
+ ## 0.18.1
67
+
68
+ - ACP provider-session ids and session-list cursors are now confidential,
69
+ authenticated v2 handles. Hosts must persist one exact 32-byte binary
70
+ `acpSessionTokenKey` and pass it to ACP task runs, list/delete helpers, and
71
+ `validateAcpProviderSessionId(value, expectedProfileId, key)`. A changed or
72
+ missing key fails before profile resolution or process spawn. Existing v1
73
+ handles are rejected; discard them and obtain fresh v2 handles. Preserve the
74
+ complete returned value for resume, pagination, validation, and delete, but
75
+ do not compare ciphertexts for equality or parse/substitute the remote
76
+ agent's raw session id or cursor.
77
+ - Payload-bearing diagnostics from the pinned ACP SDK are scoped to the owned
78
+ ACP receive loop and reduced to content-free labels. Malformed or hostile
79
+ agent notifications cannot copy elicitation values or URL secrets into
80
+ process-wide console diagnostics.
81
+
82
+ ## 0.18.0
67
83
 
68
84
  - `acp:<profile-id>` is now a canonical runtime model reference when paired
69
85
  with `executionMode: "acp"`. Hosts must provide `resolveAcpProfile`; profiles
package/README.md CHANGED
@@ -615,19 +615,30 @@ transport frame is too structurally complex for the bounded host sanitizer,
615
615
  the turn fails explicitly as `provider_protocol` instead of emitting a partial
616
616
  tool, plan, or message event.
617
617
 
618
- ACP provider-session ids and list cursors are opaque, profile-bound runtime
619
- handles. Preserve them byte-for-byte and pass them back only to the matching
620
- high-level resume, list, validation, or delete operation; raw protocol session
621
- ids, cursors, and transport connections are private runtime state. Under the
622
- default `auto` recovery policy, the client prefers `session/resume`, then
623
- `session/load`, and finally a fresh session when neither capability is
624
- advertised. Explicit `resume` or `load` policies fail closed if missing. Stable
625
- usage comes from the latest typed `usage_update` notification; unstable
626
- `PromptResponse.usage` is ignored.
618
+ ACP provider-session ids and list cursors are confidential, authenticated v2
619
+ handles bound to their token kind and profile. The host must supply an exact
620
+ 32-byte binary `acpSessionTokenKey` for every task run, list, validation, and
621
+ delete operation. Call
622
+ `validateAcpProviderSessionId(handle, expectedProfileId, key)` at untrusted
623
+ ingress. Keep the key stable and secret across host restarts; changing it
624
+ invalidates every outstanding handle. Legacy `acp:v1:` and `acp-cursor:v1:`
625
+ values are rejected.
626
+
627
+ Preserve each returned handle byte-for-byte and pass it back only to the
628
+ matching high-level resume, list, validation, or delete operation. Encryption
629
+ uses a fresh nonce, so two handles for the same remote id are not equality
630
+ keys. Raw protocol session ids, cursors, token keys, and transport connections
631
+ remain private runtime state and are omitted from profile resolver context,
632
+ callbacks, and diagnostics. Under the default `auto` recovery policy, the
633
+ client prefers `session/resume`, then `session/load`, and finally a fresh
634
+ session when neither capability is advertised. Explicit `resume` or `load`
635
+ policies fail closed if missing. Stable usage comes from the latest typed
636
+ `usage_update` notification; unstable `PromptResponse.usage` is ignored.
627
637
 
628
638
  ### `createRuntime(host)`
629
639
 
630
- Pass host-level integration once at boot. All keys are optional.
640
+ Pass host-level integration once at boot. Keys are optional unless the selected
641
+ backend contract requires them.
631
642
 
632
643
  ```js
633
644
  createRuntime({
@@ -636,6 +647,7 @@ createRuntime({
636
647
  resolvePiApiKey, // async (provider) => string | undefined
637
648
  resolveAcpProfile, // async (profileId, context) => AcpProfileDescriptor
638
649
  onAcpInteractionRequest, // async permission/elicitation fallback callback
650
+ acpSessionTokenKey, // Uint8Array(32), required for ACP task/session-handle operations
639
651
  persistArtifact, // ({ filename, buffer, toolName, toolUseId }) => path | null
640
652
  onCompactionRecorded, // (compactionRow) => void — fired when the pi bridge
641
653
  // runs an automatic compaction (proactive or reactive
@@ -751,6 +763,7 @@ Per-call options (a non-exhaustive selection):
751
763
  | `onEvent` | `(event) => void` | Fired for every runtime event (assistant text, tool calls/results, applied live input, runtime warnings, structured output). |
752
764
  | `runId` | `string` | Tag this run for downstream callbacks (e.g. `onCompactionRecorded`). |
753
765
  | `providerSessionId` | `string` | Resume a prior provider session. |
766
+ | `acpSessionTokenKey` | `Uint8Array(32)` | Required for ACP task runs when not bound at `createRuntime()`; keep it secret and stable across restarts. |
754
767
  | `runArtifactDir` | `string` | Used by some providers as the Playwright MCP filename target. |
755
768
  | `codexAppServerCommand` | `string` | Override the Codex CLI binary. |
756
769
  | `codexAppServerArgs` | `string[]` | Override the Codex CLI arguments. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mono-agent/agent-runtime",
3
- "version": "0.18.0",
3
+ "version": "0.18.1",
4
4
  "description": "Agent runtime supporting Claude SDK/CLI, Codex, OpenCode, Pi SDK, and ACP v1 bridges out of the box",
5
5
  "type": "module",
6
6
  "license": "GPL-3.0-only",
@@ -8,6 +8,7 @@ import { passthroughSandbox } from "../../agent/sandbox-seam.js";
8
8
  import {
9
9
  ACP_DEFAULT_MAX_LINE_BYTES,
10
10
  AcpTransportError,
11
+ connectWithSafeAcpSdkDiagnostics,
11
12
  createBoundedAcpStdioStream,
12
13
  normalizeAcpMaxLineBytes,
13
14
  } from "./acp-transport.js";
@@ -20,10 +21,12 @@ import {
20
21
  encodeAcpSessionCursor,
21
22
  validateAcpProfileId,
22
23
  validateAcpProviderSessionId,
24
+ validateAcpSessionTokenKey,
23
25
  } from "./acp-session-tokens.js";
24
26
 
25
27
  const OWNERS = new Set(["client", "agent"]);
26
28
  const RESUME_STRATEGIES = new Set(["auto", "load", "resume"]);
29
+ const TOKEN_FREE_OPERATIONS = new Set(["probe", "authenticate", "logout"]);
27
30
  const DEFAULT_PROCESS_POLICY = Object.freeze({
28
31
  startupTimeoutMs: 10_000,
29
32
  requestTimeoutMs: 60_000,
@@ -135,6 +138,7 @@ const DEFAULT_PROCESS_POLICY = Object.freeze({
135
138
  * @property {string} [cwd]
136
139
  * @property {AbortSignal} [signal]
137
140
  * @property {Record<string, unknown>} [context]
141
+ * @property {Uint8Array} [acpSessionTokenKey] Host-owned 32-byte key required by operations that emit or consume opaque session handles.
138
142
  */
139
143
 
140
144
  export {
@@ -527,6 +531,9 @@ export async function connectAcpProfile(profileId, options) {
527
531
  validateAcpProfileId(profileId);
528
532
  throwIfAborted(options?.signal);
529
533
  const operation = options?.operation || "connect";
534
+ const sessionTokenKey = TOKEN_FREE_OPERATIONS.has(operation)
535
+ ? undefined
536
+ : validateAcpSessionTokenKey(options?.acpSessionTokenKey);
530
537
  const descriptor = await resolveProfile(profileId, { ...options, operation });
531
538
  throwIfAborted(options?.signal);
532
539
  const capabilities = clientCapabilities(descriptor);
@@ -575,7 +582,13 @@ export async function connectAcpProfile(profileId, options) {
575
582
  const safeCallbackContext = (context, rawSessionId) => ({
576
583
  ...context,
577
584
  ...(typeof rawSessionId === "string"
578
- ? { providerSessionId: encodeAcpProviderSessionId(profileId, rawSessionId) }
585
+ ? {
586
+ providerSessionId: encodeAcpProviderSessionId(
587
+ profileId,
588
+ rawSessionId,
589
+ /** @type {Uint8Array} */ (sessionTokenKey),
590
+ ),
591
+ }
579
592
  : {}),
580
593
  ...(context.requestId === undefined
581
594
  ? {}
@@ -696,9 +709,11 @@ export async function connectAcpProfile(profileId, options) {
696
709
 
697
710
  let connection;
698
711
  try {
699
- connection = app.connect(createBoundedAcpStdioStream(child, {
700
- maxLineBytes: descriptor.process.maxLineBytes,
701
- }));
712
+ connection = connectWithSafeAcpSdkDiagnostics(() => app.connect(
713
+ createBoundedAcpStdioStream(child, {
714
+ maxLineBytes: descriptor.process.maxLineBytes,
715
+ }),
716
+ ));
702
717
  } catch (error) {
703
718
  child.kill("SIGTERM");
704
719
  const exited = await waitForExit(exitPromise, descriptor.process.killGraceMs);
@@ -1010,15 +1025,15 @@ function validateMcpServers(servers, descriptor, initializeResult) {
1010
1025
  });
1011
1026
  }
1012
1027
 
1013
- /** @param {string} profileId @param {any} request */
1014
- function protocolSessionListRequest(profileId, request) {
1028
+ /** @param {string} profileId @param {any} request @param {Uint8Array} key */
1029
+ function protocolSessionListRequest(profileId, request, key) {
1015
1030
  if (!request || typeof request !== "object" || Array.isArray(request)) {
1016
1031
  throw new AcpClientError("invalid_request", "ACP session/list request must be an object.");
1017
1032
  }
1018
1033
  const { cursor, _meta: _meta, ...rest } = request;
1019
1034
  return {
1020
1035
  ...rest,
1021
- ...(cursor == null ? {} : { cursor: decodeAcpSessionCursor(profileId, cursor) }),
1036
+ ...(cursor == null ? {} : { cursor: decodeAcpSessionCursor(profileId, cursor, key) }),
1022
1037
  };
1023
1038
  }
1024
1039
 
@@ -1090,18 +1105,23 @@ export async function logoutAcpProfile(profileId, options) {
1090
1105
  * @returns {Promise<AcpSessionListResult>}
1091
1106
  */
1092
1107
  export async function listAcpSessions(profileId, request = {}, options = /** @type {any} */ ({})) {
1093
- const protocolRequest = protocolSessionListRequest(profileId, request);
1094
- const connection = await connectAcpProfile(profileId, { ...options, operation: "list_sessions" });
1108
+ const key = validateAcpSessionTokenKey(options?.acpSessionTokenKey);
1109
+ const protocolRequest = protocolSessionListRequest(profileId, request, key);
1110
+ const connection = await connectAcpProfile(profileId, {
1111
+ ...options,
1112
+ operation: "list_sessions",
1113
+ acpSessionTokenKey: key,
1114
+ });
1095
1115
  try {
1096
1116
  const result = await connection.listSessions(protocolRequest);
1097
1117
  return {
1098
1118
  profileId,
1099
1119
  sessions: (result.sessions || []).map((session) => ({
1100
1120
  ...sanitizeAcpHostValue(session, [session.sessionId, result.nextCursor]),
1101
- providerSessionId: encodeAcpProviderSessionId(profileId, session.sessionId),
1121
+ providerSessionId: encodeAcpProviderSessionId(profileId, session.sessionId, key),
1102
1122
  })),
1103
1123
  nextCursor: typeof result.nextCursor === "string"
1104
- ? encodeAcpSessionCursor(profileId, result.nextCursor)
1124
+ ? encodeAcpSessionCursor(profileId, result.nextCursor, key)
1105
1125
  : null,
1106
1126
  };
1107
1127
  } finally {
@@ -1111,8 +1131,13 @@ export async function listAcpSessions(profileId, request = {}, options = /** @ty
1111
1131
 
1112
1132
  /** @param {string} providerSessionId @param {AcpClientHostOptions} options */
1113
1133
  export async function deleteAcpSession(providerSessionId, options) {
1114
- const { profileId, sessionId } = decodeAcpProviderSessionId(providerSessionId);
1115
- const connection = await connectAcpProfile(profileId, { ...options, operation: "delete_session" });
1134
+ const key = validateAcpSessionTokenKey(options?.acpSessionTokenKey);
1135
+ const { profileId, sessionId } = decodeAcpProviderSessionId(providerSessionId, key);
1136
+ const connection = await connectAcpProfile(profileId, {
1137
+ ...options,
1138
+ operation: "delete_session",
1139
+ acpSessionTokenKey: key,
1140
+ });
1116
1141
  try {
1117
1142
  await connection.deleteSession(sessionId);
1118
1143
  return { profileId, providerSessionId, deleted: true };
@@ -1,16 +1,31 @@
1
1
  // @ts-check
2
2
 
3
+ import {
4
+ createCipheriv,
5
+ createDecipheriv,
6
+ hkdfSync,
7
+ randomBytes,
8
+ } from "node:crypto";
9
+
3
10
  const PROFILE_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
4
11
  const BASE64URL_RE = /^[A-Za-z0-9_-]+$/;
5
12
  const MAX_PROFILE_ID_LENGTH = 128;
6
13
  const MAX_RAW_TOKEN_BYTES = 4_096;
7
- const MAX_ENCODED_TOKEN_LENGTH = Math.ceil(MAX_RAW_TOKEN_BYTES * 4 / 3);
8
- const PROVIDER_SESSION_PREFIX = "acp:v1:";
9
- const SESSION_CURSOR_PREFIX = "acp-cursor:v1:";
14
+ const TOKEN_KEY_BYTES = 32;
15
+ const NONCE_BYTES = 12;
16
+ const AUTH_TAG_BYTES = 16;
17
+ const MAX_SEALED_TOKEN_BYTES = NONCE_BYTES + MAX_RAW_TOKEN_BYTES + AUTH_TAG_BYTES;
18
+ const MAX_ENCODED_TOKEN_LENGTH = Math.ceil(MAX_SEALED_TOKEN_BYTES * 4 / 3);
19
+ const PROVIDER_SESSION_PREFIX = "acp:v2:";
20
+ const SESSION_CURSOR_PREFIX = "acp-cursor:v2:";
10
21
  const MAX_PROVIDER_SESSION_ID_LENGTH = PROVIDER_SESSION_PREFIX.length
11
22
  + MAX_PROFILE_ID_LENGTH + 1 + MAX_ENCODED_TOKEN_LENGTH;
12
23
  const MAX_SESSION_CURSOR_LENGTH = SESSION_CURSOR_PREFIX.length
13
24
  + MAX_PROFILE_ID_LENGTH + 1 + MAX_ENCODED_TOKEN_LENGTH;
25
+ const TOKEN_DOMAIN = Buffer.from("mono-agent/acp-session-token", "utf8");
26
+ const TOKEN_VERSION = Buffer.from("v2", "utf8");
27
+ const HKDF_SALT = Buffer.from("mono-agent/acp-session-token/v2/hkdf", "utf8");
28
+ const ENCRYPTION_INFO = Buffer.from("mono-agent/acp-session-token/v2/encryption", "utf8");
14
29
 
15
30
  export class AcpClientError extends Error {
16
31
  /**
@@ -45,46 +60,187 @@ function requiredTokenString(value, code, label) {
45
60
  return value;
46
61
  }
47
62
 
48
- /** @param {string} encoded @param {string} code @param {string} label */
49
- function decodeToken(encoded, code, label) {
50
- if (!BASE64URL_RE.test(encoded)) throw new AcpClientError(code, `Invalid ${label} encoding.`);
51
- const bytes = Buffer.from(encoded, "base64url");
52
- if (bytes.length === 0 || bytes.toString("base64url") !== encoded) {
53
- throw new AcpClientError(code, `Non-canonical ${label} encoding.`);
63
+ /**
64
+ * The token key is binary on purpose: accepting textual secrets here would
65
+ * make encoding, truncation, and cross-host persistence ambiguous.
66
+ * @param {unknown} value
67
+ */
68
+ function sessionTokenKey(value) {
69
+ if (!(value instanceof Uint8Array) || value.byteLength !== TOKEN_KEY_BYTES) {
70
+ throw new AcpClientError(
71
+ "invalid_token_key",
72
+ `ACP session token key must be exactly ${TOKEN_KEY_BYTES} bytes.`,
73
+ );
54
74
  }
55
- if (bytes.length > MAX_RAW_TOKEN_BYTES) {
56
- throw new AcpClientError(code, `${label} exceeds ${MAX_RAW_TOKEN_BYTES} bytes.`);
75
+ return Buffer.from(value.buffer, value.byteOffset, value.byteLength);
76
+ }
77
+
78
+ /** @param {unknown} key @returns {Buffer} */
79
+ export function validateAcpSessionTokenKey(key) {
80
+ return Buffer.from(sessionTokenKey(key));
81
+ }
82
+
83
+ /** @param {ReadonlyArray<Uint8Array>} parts */
84
+ function frame(parts) {
85
+ const size = parts.reduce((total, part) => total + 4 + part.byteLength, 0);
86
+ const result = Buffer.allocUnsafe(size);
87
+ let offset = 0;
88
+ for (const part of parts) {
89
+ result.writeUInt32BE(part.byteLength, offset);
90
+ offset += 4;
91
+ Buffer.from(part.buffer, part.byteOffset, part.byteLength).copy(result, offset);
92
+ offset += part.byteLength;
57
93
  }
94
+ return result;
95
+ }
96
+
97
+ /** @param {"session"|"cursor"} kind @param {string} profileId */
98
+ function tokenAad(kind, profileId) {
99
+ return frame([
100
+ TOKEN_DOMAIN,
101
+ TOKEN_VERSION,
102
+ Buffer.from(kind, "utf8"),
103
+ Buffer.from(profileId, "utf8"),
104
+ ]);
105
+ }
106
+
107
+ /** @param {Uint8Array} key */
108
+ function encryptionKey(key) {
109
+ const rawKey = Buffer.from(sessionTokenKey(key));
58
110
  try {
59
- return requiredTokenString(new TextDecoder("utf-8", { fatal: true }).decode(bytes), code, label);
60
- } catch (error) {
61
- if (error instanceof AcpClientError) throw error;
62
- throw new AcpClientError(code, `${label} is not valid UTF-8.`);
111
+ return Buffer.from(hkdfSync("sha256", rawKey, HKDF_SALT, ENCRYPTION_INFO, TOKEN_KEY_BYTES));
112
+ } finally {
113
+ rawKey.fill(0);
63
114
  }
64
115
  }
65
116
 
66
- /** @param {string} profileId @param {string} sessionId */
67
- export function encodeAcpProviderSessionId(profileId, sessionId) {
117
+ /** @param {string} profileId @param {string} raw @param {string} code @param {string} label */
118
+ function rawTokenBytes(profileId, raw, code, label) {
68
119
  validateAcpProfileId(profileId);
69
- requiredTokenString(sessionId, "invalid_session_id", "ACP session id");
70
- if (Buffer.byteLength(sessionId, "utf8") > MAX_RAW_TOKEN_BYTES) {
71
- throw new AcpClientError("invalid_session_id", `ACP session id exceeds ${MAX_RAW_TOKEN_BYTES} bytes.`);
120
+ requiredTokenString(raw, code, label);
121
+ const plaintext = Buffer.from(raw, "utf8");
122
+ if (plaintext.byteLength > MAX_RAW_TOKEN_BYTES) {
123
+ throw new AcpClientError(code, `${label} exceeds ${MAX_RAW_TOKEN_BYTES} bytes.`);
72
124
  }
73
- return `${PROVIDER_SESSION_PREFIX}${profileId}:${Buffer.from(sessionId, "utf8").toString("base64url")}`;
125
+ return plaintext;
74
126
  }
75
127
 
76
- /** Internal protocol-state decoder. This module is not a package export. @param {string} providerSessionId */
77
- export function decodeAcpProviderSessionId(providerSessionId) {
78
- if (typeof providerSessionId !== "string") {
79
- throw new AcpClientError("invalid_session_id", "ACP provider session id must be a string.");
128
+ /**
129
+ * @param {"session"|"cursor"} kind
130
+ * @param {string} profileId
131
+ * @param {string} raw
132
+ * @param {Uint8Array} key
133
+ * @param {string} code
134
+ * @param {string} label
135
+ */
136
+ function sealToken(kind, profileId, raw, key, code, label) {
137
+ const plaintext = rawTokenBytes(profileId, raw, code, label);
138
+ const aad = tokenAad(kind, profileId);
139
+ const nonce = randomBytes(NONCE_BYTES);
140
+ const derivedKey = encryptionKey(key);
141
+ let cipher;
142
+ try {
143
+ cipher = createCipheriv("aes-256-gcm", derivedKey, nonce);
144
+ } finally {
145
+ derivedKey.fill(0);
80
146
  }
81
- if (providerSessionId.length > MAX_PROVIDER_SESSION_ID_LENGTH) {
82
- throw new AcpClientError("invalid_session_id", "ACP provider session id exceeds the supported length.");
147
+ cipher.setAAD(aad);
148
+ const ciphertext = Buffer.concat([cipher.update(plaintext), cipher.final()]);
149
+ return Buffer.concat([nonce, ciphertext, cipher.getAuthTag()]).toString("base64url");
150
+ }
151
+
152
+ /** @param {string} profileId @param {string} code @param {string} label */
153
+ function tokenProfileId(profileId, code, label) {
154
+ try {
155
+ return validateAcpProfileId(profileId);
156
+ } catch {
157
+ throw new AcpClientError(code, `Invalid ${label}.`);
83
158
  }
84
- const match = /^acp:v1:([^:]+):([^:]+)$/.exec(providerSessionId);
159
+ }
160
+
161
+ /**
162
+ * @param {string} encoded
163
+ * @param {string} code
164
+ * @param {string} label
165
+ */
166
+ function sealedTokenBytes(encoded, code, label) {
167
+ if (!BASE64URL_RE.test(encoded)) throw new AcpClientError(code, `Invalid ${label}.`);
168
+ const sealed = Buffer.from(encoded, "base64url");
169
+ if (sealed.toString("base64url") !== encoded
170
+ || sealed.byteLength <= NONCE_BYTES + AUTH_TAG_BYTES
171
+ || sealed.byteLength > MAX_SEALED_TOKEN_BYTES) {
172
+ throw new AcpClientError(code, `Invalid ${label}.`);
173
+ }
174
+ return sealed;
175
+ }
176
+
177
+ /**
178
+ * @param {"session"|"cursor"} kind
179
+ * @param {string} profileId
180
+ * @param {string} encoded
181
+ * @param {Uint8Array} key
182
+ * @param {string} code
183
+ * @param {string} label
184
+ */
185
+ function openToken(kind, profileId, encoded, key, code, label) {
186
+ const sealed = sealedTokenBytes(encoded, code, label);
187
+ const aad = tokenAad(kind, profileId);
188
+ const nonce = sealed.subarray(0, NONCE_BYTES);
189
+ const ciphertext = sealed.subarray(NONCE_BYTES, -AUTH_TAG_BYTES);
190
+ const authTag = sealed.subarray(-AUTH_TAG_BYTES);
191
+ const derivedKey = encryptionKey(key);
192
+ let decipher;
193
+ try {
194
+ decipher = createDecipheriv("aes-256-gcm", derivedKey, nonce);
195
+ } finally {
196
+ derivedKey.fill(0);
197
+ }
198
+ let plaintext;
199
+ try {
200
+ decipher.setAAD(aad);
201
+ decipher.setAuthTag(authTag);
202
+ plaintext = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
203
+ } catch {
204
+ throw new AcpClientError(code, `Invalid ${label}.`);
205
+ }
206
+ try {
207
+ return requiredTokenString(
208
+ new TextDecoder("utf-8", { fatal: true }).decode(plaintext),
209
+ code,
210
+ label,
211
+ );
212
+ } catch (error) {
213
+ if (error instanceof AcpClientError) throw error;
214
+ throw new AcpClientError(code, `Invalid ${label}.`);
215
+ }
216
+ }
217
+
218
+ /** @param {string} profileId @param {string} sessionId @param {Uint8Array} key */
219
+ export function encodeAcpProviderSessionId(profileId, sessionId, key) {
220
+ const encoded = sealToken("session", profileId, sessionId, key, "invalid_session_id", "ACP session id");
221
+ return `${PROVIDER_SESSION_PREFIX}${profileId}:${encoded}`;
222
+ }
223
+
224
+ /**
225
+ * Internal protocol-state decoder. This module is not a package export.
226
+ * @param {string} providerSessionId
227
+ * @param {Uint8Array} key
228
+ */
229
+ export function decodeAcpProviderSessionId(providerSessionId, key) {
230
+ if (typeof providerSessionId !== "string" || providerSessionId.length > MAX_PROVIDER_SESSION_ID_LENGTH) {
231
+ throw new AcpClientError("invalid_session_id", "Invalid ACP provider session id.");
232
+ }
233
+ const match = /^acp:v2:([^:]+):([^:]+)$/.exec(providerSessionId);
85
234
  if (!match) throw new AcpClientError("invalid_session_id", "Invalid ACP provider session id.");
86
- const profileId = validateAcpProfileId(match[1]);
87
- const sessionId = decodeToken(match[2], "invalid_session_id", "ACP session id");
235
+ const profileId = tokenProfileId(match[1], "invalid_session_id", "ACP provider session id");
236
+ const sessionId = openToken(
237
+ "session",
238
+ profileId,
239
+ match[2],
240
+ key,
241
+ "invalid_session_id",
242
+ "ACP provider session id",
243
+ );
88
244
  return { profileId, sessionId };
89
245
  }
90
246
 
@@ -94,36 +250,33 @@ export function decodeAcpProviderSessionId(providerSessionId) {
94
250
  *
95
251
  * @param {string} providerSessionId
96
252
  * @param {string} expectedProfileId
253
+ * @param {Uint8Array} key Host-owned 32-byte ACP session-token key.
97
254
  * @returns {string}
98
255
  */
99
- export function validateAcpProviderSessionId(providerSessionId, expectedProfileId) {
256
+ export function validateAcpProviderSessionId(providerSessionId, expectedProfileId, key) {
100
257
  const profileId = validateAcpProfileId(expectedProfileId);
101
- const decoded = decodeAcpProviderSessionId(providerSessionId);
258
+ const decoded = decodeAcpProviderSessionId(providerSessionId, key);
102
259
  if (decoded.profileId !== profileId) {
103
260
  throw new AcpClientError("invalid_session_id", "ACP provider session belongs to a different profile.");
104
261
  }
105
262
  return providerSessionId;
106
263
  }
107
264
 
108
- /** @param {string} profileId @param {string} cursor */
109
- export function encodeAcpSessionCursor(profileId, cursor) {
110
- validateAcpProfileId(profileId);
111
- requiredTokenString(cursor, "invalid_cursor", "ACP session cursor");
112
- if (Buffer.byteLength(cursor, "utf8") > MAX_RAW_TOKEN_BYTES) {
113
- throw new AcpClientError("invalid_cursor", `ACP session cursor exceeds ${MAX_RAW_TOKEN_BYTES} bytes.`);
114
- }
115
- return `${SESSION_CURSOR_PREFIX}${profileId}:${Buffer.from(cursor, "utf8").toString("base64url")}`;
265
+ /** @param {string} profileId @param {string} cursor @param {Uint8Array} key */
266
+ export function encodeAcpSessionCursor(profileId, cursor, key) {
267
+ const encoded = sealToken("cursor", profileId, cursor, key, "invalid_cursor", "ACP session cursor");
268
+ return `${SESSION_CURSOR_PREFIX}${profileId}:${encoded}`;
116
269
  }
117
270
 
118
- /** @param {string} profileId @param {unknown} cursor */
119
- export function decodeAcpSessionCursor(profileId, cursor) {
271
+ /** @param {string} profileId @param {unknown} cursor @param {Uint8Array} key */
272
+ export function decodeAcpSessionCursor(profileId, cursor, key) {
120
273
  validateAcpProfileId(profileId);
121
274
  if (typeof cursor !== "string" || cursor.length > MAX_SESSION_CURSOR_LENGTH) {
122
275
  throw new AcpClientError("invalid_cursor", "ACP session cursor must be an opaque cursor returned by listAcpSessions.");
123
276
  }
124
- const match = /^acp-cursor:v1:([^:]+):([^:]+)$/.exec(cursor);
125
- if (!match || match[1] !== profileId) {
277
+ const match = /^acp-cursor:v2:([^:]+):([^:]+)$/.exec(cursor);
278
+ if (!match || tokenProfileId(match[1], "invalid_cursor", "ACP session cursor") !== profileId) {
126
279
  throw new AcpClientError("invalid_cursor", "ACP session cursor is invalid for this profile.");
127
280
  }
128
- return decodeToken(match[2], "invalid_cursor", "ACP session cursor");
281
+ return openToken("cursor", profileId, match[2], key, "invalid_cursor", "ACP session cursor");
129
282
  }
@@ -1,5 +1,7 @@
1
1
  // @ts-check
2
2
 
3
+ import { AsyncLocalStorage } from "node:async_hooks";
4
+
3
5
  // Strict, bounded ACP v1 newline transport for an owned stdio child process.
4
6
  // The SDK's stock ndJsonStream intentionally tolerates malformed input and its
5
7
  // line buffer is unbounded, which is a poor fit for a long-lived host boundary.
@@ -7,6 +9,101 @@
7
9
  const DEFAULT_MAX_LINE_BYTES = 1024 * 1024;
8
10
  const MAX_MAX_LINE_BYTES = 16 * 1024 * 1024;
9
11
 
12
+ // @agentclientprotocol/sdk 1.3.0 sends the trailing arguments of these
13
+ // diagnostics directly to console. Those values include raw JSON-RPC payloads
14
+ // (and, for malformed notifications, Zod error details derived from them).
15
+ // Preserve the useful classification while dropping payload-bearing details.
16
+ const ACP_SDK_PAYLOAD_DIAGNOSTICS = Object.freeze({
17
+ error: new Set([
18
+ "Invalid message",
19
+ "Error handling notification",
20
+ "Got response to unknown request",
21
+ "Failed to parse JSON message:",
22
+ "ACP connection router stopped unexpectedly:",
23
+ ]),
24
+ warn: new Set([
25
+ "Skipping JSON line that is not an object:",
26
+ ]),
27
+ });
28
+ const acpSdkDiagnosticScope = new AsyncLocalStorage();
29
+ let activeAcpSdkDiagnosticGuards = 0;
30
+ /** @type {null|{
31
+ * originalError: typeof console.error,
32
+ * originalWarn: typeof console.warn,
33
+ * guardedError: typeof console.error,
34
+ * guardedWarn: typeof console.warn,
35
+ * }} */
36
+ let acpSdkConsoleGuard = null;
37
+
38
+ function installAcpSdkConsoleGuard() {
39
+ activeAcpSdkDiagnosticGuards += 1;
40
+ if (acpSdkConsoleGuard) return;
41
+
42
+ const originalError = console.error;
43
+ const originalWarn = console.warn;
44
+ /** @type {typeof console.error} */
45
+ const guardedError = (...args) => {
46
+ const contentFree = args[0];
47
+ if (acpSdkDiagnosticScope.getStore() === true
48
+ && typeof contentFree === "string"
49
+ && ACP_SDK_PAYLOAD_DIAGNOSTICS.error.has(contentFree)) {
50
+ Reflect.apply(originalError, console, [contentFree]);
51
+ return;
52
+ }
53
+ Reflect.apply(originalError, console, args);
54
+ };
55
+ /** @type {typeof console.warn} */
56
+ const guardedWarn = (...args) => {
57
+ const contentFree = args[0];
58
+ if (acpSdkDiagnosticScope.getStore() === true
59
+ && typeof contentFree === "string"
60
+ && ACP_SDK_PAYLOAD_DIAGNOSTICS.warn.has(contentFree)) {
61
+ Reflect.apply(originalWarn, console, [contentFree]);
62
+ return;
63
+ }
64
+ Reflect.apply(originalWarn, console, args);
65
+ };
66
+
67
+ acpSdkConsoleGuard = { originalError, originalWarn, guardedError, guardedWarn };
68
+ console.error = guardedError;
69
+ console.warn = guardedWarn;
70
+ }
71
+
72
+ function releaseAcpSdkConsoleGuard() {
73
+ activeAcpSdkDiagnosticGuards = Math.max(0, activeAcpSdkDiagnosticGuards - 1);
74
+ if (activeAcpSdkDiagnosticGuards !== 0 || !acpSdkConsoleGuard) return;
75
+ const guard = acpSdkConsoleGuard;
76
+ acpSdkConsoleGuard = null;
77
+ if (console.error === guard.guardedError) console.error = guard.originalError;
78
+ if (console.warn === guard.guardedWarn) console.warn = guard.originalWarn;
79
+ }
80
+
81
+ /**
82
+ * Open one SDK connection inside an async context that strips payload-bearing
83
+ * SDK console arguments. The SDK starts its detached receive loop during
84
+ * `connect`, so descendants retain this scope without muting concurrent host
85
+ * work or other ACP connections.
86
+ *
87
+ * @template {{closed: Promise<unknown>}} T
88
+ * @param {() => T} connect
89
+ * @returns {T}
90
+ */
91
+ export function connectWithSafeAcpSdkDiagnostics(connect) {
92
+ installAcpSdkConsoleGuard();
93
+ let connection;
94
+ try {
95
+ connection = acpSdkDiagnosticScope.run(true, connect);
96
+ } catch (error) {
97
+ releaseAcpSdkConsoleGuard();
98
+ throw error;
99
+ }
100
+ void Promise.resolve(connection.closed).then(
101
+ releaseAcpSdkConsoleGuard,
102
+ releaseAcpSdkConsoleGuard,
103
+ );
104
+ return connection;
105
+ }
106
+
10
107
  export class AcpTransportError extends Error {
11
108
  /**
12
109
  * @param {string} code
@@ -11,6 +11,7 @@ import {
11
11
  decodeAcpProviderSessionId,
12
12
  encodeAcpProviderSessionId,
13
13
  validateAcpProviderSessionId,
14
+ validateAcpSessionTokenKey,
14
15
  } from "./acp-session-tokens.js";
15
16
  import {
16
17
  ownAcpSessionUpdateKind,
@@ -288,8 +289,8 @@ function selectValues(options) {
288
289
  return values;
289
290
  }
290
291
 
291
- /** @param {any} connection @param {any} descriptor @param {any} req @param {string} profileId @param {(notification:any)=>void} onUpdate */
292
- async function openSession(connection, descriptor, req, profileId, onUpdate) {
292
+ /** @param {any} connection @param {any} descriptor @param {any} req @param {string} profileId @param {Uint8Array} key @param {(notification:any)=>void} onUpdate */
293
+ async function openSession(connection, descriptor, req, profileId, key, onUpdate) {
293
294
  const cwd = workspaceFor(descriptor, req);
294
295
  const config = sessionConfig(descriptor);
295
296
  const baseRequest = { cwd, ...config };
@@ -297,7 +298,7 @@ async function openSession(connection, descriptor, req, profileId, onUpdate) {
297
298
  const response = await connection.newSession(baseRequest);
298
299
  return { sessionId: response.sessionId, response, resumed: false, resumeMethod: null };
299
300
  }
300
- const decoded = decodeAcpProviderSessionId(req.providerSessionId);
301
+ const decoded = decodeAcpProviderSessionId(req.providerSessionId, key);
301
302
  if (decoded.profileId !== profileId) {
302
303
  throw new AcpClientError("invalid_session_id", "ACP provider session belongs to a different profile.");
303
304
  }
@@ -369,8 +370,13 @@ export async function generateAcpResponse(systemPrompt, req) {
369
370
  let setup = null;
370
371
  let configuration = { modeApplied: false, configOptionsApplied: [] };
371
372
  try {
373
+ const sessionTokenKey = validateAcpSessionTokenKey(req?.acpSessionTokenKey);
372
374
  if (req?.providerSessionId != null) {
373
- providerSessionId = validateAcpProviderSessionId(req.providerSessionId, profileId);
375
+ providerSessionId = validateAcpProviderSessionId(
376
+ req.providerSessionId,
377
+ profileId,
378
+ sessionTokenKey,
379
+ );
374
380
  }
375
381
  connection = await connectAcpProfile(profileId, {
376
382
  resolveAcpProfile: req.resolveAcpProfile,
@@ -382,6 +388,7 @@ export async function generateAcpResponse(systemPrompt, req) {
382
388
  signal: req.abortSignal,
383
389
  context: { operation: "run", model: reference },
384
390
  operation: "run",
391
+ acpSessionTokenKey: sessionTokenKey,
385
392
  });
386
393
  capture({
387
394
  type: "capabilities_resolved",
@@ -395,9 +402,18 @@ export async function generateAcpResponse(systemPrompt, req) {
395
402
  const onUpdate = (notification) => {
396
403
  for (const event of normalizeUpdate(notification, state)) capture(event);
397
404
  };
398
- setup = await openSession(connection, connection.descriptor, req, profileId, onUpdate);
405
+ setup = await openSession(
406
+ connection,
407
+ connection.descriptor,
408
+ req,
409
+ profileId,
410
+ sessionTokenKey,
411
+ onUpdate,
412
+ );
399
413
  sessionId = setup.sessionId;
400
- providerSessionId = encodeAcpProviderSessionId(profileId, sessionId);
414
+ providerSessionId = setup.resumed && providerSessionId
415
+ ? providerSessionId
416
+ : encodeAcpProviderSessionId(profileId, sessionId, sessionTokenKey);
401
417
  configuration = await applyClientConfiguration(
402
418
  connection,
403
419
  sessionId,
package/src/ai/types.js CHANGED
@@ -165,6 +165,7 @@
165
165
  * @property {RuntimePromptOverrides} [prompts] Per-run prompt-fragment overrides (run wins over the host default).
166
166
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["resolveAcpProfile"]} [resolveAcpProfile] Per-run ACP profile resolver; wins over the host default.
167
167
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["onAcpInteractionRequest"]} [onAcpInteractionRequest] Per-run ACP permission/elicitation callback; wins over the host default.
168
+ * @property {Uint8Array} [acpSessionTokenKey] Host-owned 32-byte key for confidential authenticated ACP session handles. Required for every ACP task run.
168
169
  * @property {{backend?: "auto"|"searxng"|"keyless", endpoint?: string}} [webSearchConfig] Run-scoped WebSearch backend configuration.
169
170
  * @property {{render?: "never"|"auto", browserCommand?: string}} [webFetchConfig] Run-scoped WebFetch extraction/render configuration.
170
171
  * @property {"sequential"|"safe-parallel"} [piToolExecutionMode] Pi built-in tool scheduling mode. Safe parallelism is the default.
@@ -182,7 +183,7 @@
182
183
 
183
184
  /**
184
185
  * @typedef {RuntimeRunOptions
185
- * & Pick<AgentRuntimeHostOptions, "resolveCustomPricing" | "resolvePiApiKey" | "resolveAcpProfile" | "onAcpInteractionRequest" | "persistArtifact" | "onCompactionRecorded" | "onToolApprovalRequest" | "toolRiskTiers" | "approvalDefaultRiskTier" | "approvalTimeoutMs" | "approvalAlwaysAllowTools">
186
+ * & Pick<AgentRuntimeHostOptions, "resolveCustomPricing" | "resolvePiApiKey" | "resolveAcpProfile" | "onAcpInteractionRequest" | "acpSessionTokenKey" | "persistArtifact" | "onCompactionRecorded" | "onToolApprovalRequest" | "toolRiskTiers" | "approvalDefaultRiskTier" | "approvalTimeoutMs" | "approvalAlwaysAllowTools">
186
187
  * & {runtimeBrand: import('../runtime-brand.js').RuntimeBrand, toolContext?: import('../agent/tools/shared/tool-context.js').ToolContext, observerHub: {emit: (event: RuntimeEvent) => void, flush: () => Promise<void>}}
187
188
  * } RuntimeRequest
188
189
  * The request shape a bridge's `execute(systemPrompt, req)` receives as its
@@ -364,6 +365,7 @@
364
365
  * @property {import('../pi-auth.js').PiApiKeyResolver} [resolvePiApiKey] See createPiOAuthApiKeyResolver (pi-auth.js) for a ready-made implementation.
365
366
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["resolveAcpProfile"]} [resolveAcpProfile] Default ACP profile resolver; a per-run callback wins.
366
367
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["onAcpInteractionRequest"]} [onAcpInteractionRequest] Default ACP interaction callback; a per-run callback wins.
368
+ * @property {Uint8Array} [acpSessionTokenKey] Default host-owned 32-byte key for confidential authenticated ACP session handles.
367
369
  * @property {(artifact: {filename: string, buffer: Buffer, toolName: string, toolUseId: (string|null)}) => (string|null)} [persistArtifact]
368
370
  * @property {(record: CompactionRecordedPayload) => void} [onCompactionRecorded]
369
371
  * @property {(payload: ApprovalRequestPayload) => Promise<ApprovalDecision>} [onToolApprovalRequest]
package/src/runtime.js CHANGED
@@ -55,6 +55,7 @@ const HOST_KEYS = [
55
55
  "resolvePiApiKey",
56
56
  "resolveAcpProfile",
57
57
  "onAcpInteractionRequest",
58
+ "acpSessionTokenKey",
58
59
  "persistArtifact",
59
60
  "onCompactionRecorded",
60
61
  "onToolApprovalRequest",
@@ -214,6 +214,10 @@ export type AcpClientHostOptions = {
214
214
  cwd?: string;
215
215
  signal?: AbortSignal;
216
216
  context?: Record<string, unknown>;
217
+ /**
218
+ * Host-owned 32-byte key required by operations that emit or consume opaque session handles.
219
+ */
220
+ acpSessionTokenKey?: Uint8Array;
217
221
  };
218
222
  import { AcpClientError } from "./acp-session-tokens.js";
219
223
  import { encodeAcpProviderSessionId } from "./acp-session-tokens.js";
@@ -1,9 +1,15 @@
1
1
  /** @param {string} profileId @returns {string} */
2
2
  export function validateAcpProfileId(profileId: string): string;
3
- /** @param {string} profileId @param {string} sessionId */
4
- export function encodeAcpProviderSessionId(profileId: string, sessionId: string): string;
5
- /** Internal protocol-state decoder. This module is not a package export. @param {string} providerSessionId */
6
- export function decodeAcpProviderSessionId(providerSessionId: string): {
3
+ /** @param {unknown} key @returns {Buffer} */
4
+ export function validateAcpSessionTokenKey(key: unknown): Buffer;
5
+ /** @param {string} profileId @param {string} sessionId @param {Uint8Array} key */
6
+ export function encodeAcpProviderSessionId(profileId: string, sessionId: string, key: Uint8Array): string;
7
+ /**
8
+ * Internal protocol-state decoder. This module is not a package export.
9
+ * @param {string} providerSessionId
10
+ * @param {Uint8Array} key
11
+ */
12
+ export function decodeAcpProviderSessionId(providerSessionId: string, key: Uint8Array): {
7
13
  profileId: string;
8
14
  sessionId: string;
9
15
  };
@@ -13,13 +19,14 @@ export function decodeAcpProviderSessionId(providerSessionId: string): {
13
19
  *
14
20
  * @param {string} providerSessionId
15
21
  * @param {string} expectedProfileId
22
+ * @param {Uint8Array} key Host-owned 32-byte ACP session-token key.
16
23
  * @returns {string}
17
24
  */
18
- export function validateAcpProviderSessionId(providerSessionId: string, expectedProfileId: string): string;
19
- /** @param {string} profileId @param {string} cursor */
20
- export function encodeAcpSessionCursor(profileId: string, cursor: string): string;
21
- /** @param {string} profileId @param {unknown} cursor */
22
- export function decodeAcpSessionCursor(profileId: string, cursor: unknown): string;
25
+ export function validateAcpProviderSessionId(providerSessionId: string, expectedProfileId: string, key: Uint8Array): string;
26
+ /** @param {string} profileId @param {string} cursor @param {Uint8Array} key */
27
+ export function encodeAcpSessionCursor(profileId: string, cursor: string, key: Uint8Array): string;
28
+ /** @param {string} profileId @param {unknown} cursor @param {Uint8Array} key */
29
+ export function decodeAcpSessionCursor(profileId: string, cursor: unknown, key: Uint8Array): string;
23
30
  export class AcpClientError extends Error {
24
31
  /**
25
32
  * @param {string} code
@@ -1,3 +1,16 @@
1
+ /**
2
+ * Open one SDK connection inside an async context that strips payload-bearing
3
+ * SDK console arguments. The SDK starts its detached receive loop during
4
+ * `connect`, so descendants retain this scope without muting concurrent host
5
+ * work or other ACP connections.
6
+ *
7
+ * @template {{closed: Promise<unknown>}} T
8
+ * @param {() => T} connect
9
+ * @returns {T}
10
+ */
11
+ export function connectWithSafeAcpSdkDiagnostics<T extends {
12
+ closed: Promise<unknown>;
13
+ }>(connect: () => T): T;
1
14
  /**
2
15
  * @param {unknown} value
3
16
  * @param {number} [fallback]
@@ -136,6 +136,7 @@
136
136
  * @property {RuntimePromptOverrides} [prompts] Per-run prompt-fragment overrides (run wins over the host default).
137
137
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["resolveAcpProfile"]} [resolveAcpProfile] Per-run ACP profile resolver; wins over the host default.
138
138
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["onAcpInteractionRequest"]} [onAcpInteractionRequest] Per-run ACP permission/elicitation callback; wins over the host default.
139
+ * @property {Uint8Array} [acpSessionTokenKey] Host-owned 32-byte key for confidential authenticated ACP session handles. Required for every ACP task run.
139
140
  * @property {{backend?: "auto"|"searxng"|"keyless", endpoint?: string}} [webSearchConfig] Run-scoped WebSearch backend configuration.
140
141
  * @property {{render?: "never"|"auto", browserCommand?: string}} [webFetchConfig] Run-scoped WebFetch extraction/render configuration.
141
142
  * @property {"sequential"|"safe-parallel"} [piToolExecutionMode] Pi built-in tool scheduling mode. Safe parallelism is the default.
@@ -152,7 +153,7 @@
152
153
  */
153
154
  /**
154
155
  * @typedef {RuntimeRunOptions
155
- * & Pick<AgentRuntimeHostOptions, "resolveCustomPricing" | "resolvePiApiKey" | "resolveAcpProfile" | "onAcpInteractionRequest" | "persistArtifact" | "onCompactionRecorded" | "onToolApprovalRequest" | "toolRiskTiers" | "approvalDefaultRiskTier" | "approvalTimeoutMs" | "approvalAlwaysAllowTools">
156
+ * & Pick<AgentRuntimeHostOptions, "resolveCustomPricing" | "resolvePiApiKey" | "resolveAcpProfile" | "onAcpInteractionRequest" | "acpSessionTokenKey" | "persistArtifact" | "onCompactionRecorded" | "onToolApprovalRequest" | "toolRiskTiers" | "approvalDefaultRiskTier" | "approvalTimeoutMs" | "approvalAlwaysAllowTools">
156
157
  * & {runtimeBrand: import('../runtime-brand.js').RuntimeBrand, toolContext?: import('../agent/tools/shared/tool-context.js').ToolContext, observerHub: {emit: (event: RuntimeEvent) => void, flush: () => Promise<void>}}
157
158
  * } RuntimeRequest
158
159
  * The request shape a bridge's `execute(systemPrompt, req)` receives as its
@@ -322,6 +323,7 @@
322
323
  * @property {import('../pi-auth.js').PiApiKeyResolver} [resolvePiApiKey] See createPiOAuthApiKeyResolver (pi-auth.js) for a ready-made implementation.
323
324
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["resolveAcpProfile"]} [resolveAcpProfile] Default ACP profile resolver; a per-run callback wins.
324
325
  * @property {import('./providers/acp-client.js').AcpClientHostOptions["onAcpInteractionRequest"]} [onAcpInteractionRequest] Default ACP interaction callback; a per-run callback wins.
326
+ * @property {Uint8Array} [acpSessionTokenKey] Default host-owned 32-byte key for confidential authenticated ACP session handles.
325
327
  * @property {(artifact: {filename: string, buffer: Buffer, toolName: string, toolUseId: (string|null)}) => (string|null)} [persistArtifact]
326
328
  * @property {(record: CompactionRecordedPayload) => void} [onCompactionRecorded]
327
329
  * @property {(payload: ApprovalRequestPayload) => Promise<ApprovalDecision>} [onToolApprovalRequest]
@@ -642,6 +644,10 @@ export type RuntimeRunOptions = {
642
644
  * Per-run ACP permission/elicitation callback; wins over the host default.
643
645
  */
644
646
  onAcpInteractionRequest?: import("./providers/acp-client.js").AcpClientHostOptions["onAcpInteractionRequest"];
647
+ /**
648
+ * Host-owned 32-byte key for confidential authenticated ACP session handles. Required for every ACP task run.
649
+ */
650
+ acpSessionTokenKey?: Uint8Array;
645
651
  /**
646
652
  * Run-scoped WebSearch backend configuration.
647
653
  */
@@ -698,7 +704,7 @@ export type RuntimeRunOptions = {
698
704
  * createRuntime), and the per-run observerHub (onEvent is overridden to the
699
705
  * hub's emit). `systemPrompt` is passed positionally, not folded into this object.
700
706
  */
701
- export type RuntimeRequest = RuntimeRunOptions & Pick<AgentRuntimeHostOptions, "resolveCustomPricing" | "resolvePiApiKey" | "resolveAcpProfile" | "onAcpInteractionRequest" | "persistArtifact" | "onCompactionRecorded" | "onToolApprovalRequest" | "toolRiskTiers" | "approvalDefaultRiskTier" | "approvalTimeoutMs" | "approvalAlwaysAllowTools"> & {
707
+ export type RuntimeRequest = RuntimeRunOptions & Pick<AgentRuntimeHostOptions, "resolveCustomPricing" | "resolvePiApiKey" | "resolveAcpProfile" | "onAcpInteractionRequest" | "acpSessionTokenKey" | "persistArtifact" | "onCompactionRecorded" | "onToolApprovalRequest" | "toolRiskTiers" | "approvalDefaultRiskTier" | "approvalTimeoutMs" | "approvalAlwaysAllowTools"> & {
702
708
  runtimeBrand: import("../runtime-brand.js").RuntimeBrand;
703
709
  toolContext?: import("../agent/tools/shared/tool-context.js").ToolContext;
704
710
  observerHub: {
@@ -975,6 +981,10 @@ export type AgentRuntimeHostOptions = {
975
981
  * Default ACP interaction callback; a per-run callback wins.
976
982
  */
977
983
  onAcpInteractionRequest?: import("./providers/acp-client.js").AcpClientHostOptions["onAcpInteractionRequest"];
984
+ /**
985
+ * Default host-owned 32-byte key for confidential authenticated ACP session handles.
986
+ */
987
+ acpSessionTokenKey?: Uint8Array;
978
988
  persistArtifact?: (artifact: {
979
989
  filename: string;
980
990
  buffer: Buffer;