@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 +17 -1
- package/README.md +23 -10
- package/package.json +1 -1
- package/src/ai/providers/acp-client.js +38 -13
- package/src/ai/providers/acp-session-tokens.js +198 -45
- package/src/ai/providers/acp-transport.js +97 -0
- package/src/ai/providers/acp.js +22 -6
- package/src/ai/types.js +3 -1
- package/src/runtime.js +1 -0
- package/types/ai/providers/acp-client.d.ts +4 -0
- package/types/ai/providers/acp-session-tokens.d.ts +16 -9
- package/types/ai/providers/acp-transport.d.ts +13 -0
- package/types/ai/types.d.ts +12 -2
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.
|
|
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
|
|
619
|
-
handles
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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.
|
|
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
|
@@ -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
|
-
? {
|
|
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(
|
|
700
|
-
|
|
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
|
|
1094
|
-
const
|
|
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
|
|
1115
|
-
const
|
|
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
|
|
8
|
-
const
|
|
9
|
-
const
|
|
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
|
-
/**
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
56
|
-
|
|
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
|
|
60
|
-
}
|
|
61
|
-
|
|
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}
|
|
67
|
-
|
|
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(
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
125
|
+
return plaintext;
|
|
74
126
|
}
|
|
75
127
|
|
|
76
|
-
/**
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
82
|
-
|
|
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
|
-
|
|
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 =
|
|
87
|
-
const sessionId =
|
|
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
|
-
|
|
111
|
-
|
|
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:
|
|
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
|
|
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
|
package/src/ai/providers/acp.js
CHANGED
|
@@ -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(
|
|
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(
|
|
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 =
|
|
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
|
@@ -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 {
|
|
4
|
-
export function
|
|
5
|
-
/**
|
|
6
|
-
export function
|
|
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]
|
package/types/ai/types.d.ts
CHANGED
|
@@ -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;
|