@intentic/sandbox-contract 1.244.0 → 1.246.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/README.md +17 -1
- package/dist/batch-runs.d.ts +32 -0
- package/dist/batch-runs.d.ts.map +1 -0
- package/dist/batch-runs.js +43 -0
- package/dist/batch-runs.js.map +1 -0
- package/dist/command-classes.d.ts +6 -3
- package/dist/command-classes.d.ts.map +1 -1
- package/dist/command-classes.js +43 -18
- package/dist/command-classes.js.map +1 -1
- package/dist/contracts/{cursor.contract.d.ts → accounts.contract.d.ts} +102 -3
- package/dist/contracts/accounts.contract.d.ts.map +1 -0
- package/dist/contracts/accounts.contract.js +61 -0
- package/dist/contracts/accounts.contract.js.map +1 -0
- package/dist/contracts/agents.contract.d.ts +121 -0
- package/dist/contracts/agents.contract.d.ts.map +1 -1
- package/dist/contracts/agents.contract.js +4 -4
- package/dist/contracts/agents.contract.js.map +1 -1
- package/dist/contracts/ci.contract.d.ts +2 -0
- package/dist/contracts/ci.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.d.ts +35 -0
- package/dist/contracts/host.contract.d.ts.map +1 -1
- package/dist/contracts/host.contract.js +3 -2
- package/dist/contracts/host.contract.js.map +1 -1
- package/dist/contracts/personas.contract.d.ts +4 -2
- package/dist/contracts/personas.contract.d.ts.map +1 -1
- package/dist/contracts/runner.contract.d.ts +2 -2
- package/dist/contracts/settings.contract.d.ts +2 -51
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +52 -2
- package/dist/contracts/system.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.js +26 -17
- package/dist/contracts/system.contract.js.map +1 -1
- package/dist/definition.d.ts +4 -8
- package/dist/definition.d.ts.map +1 -1
- package/dist/documents.d.ts +0 -1
- package/dist/documents.d.ts.map +1 -1
- package/dist/documents.js +1 -2
- package/dist/documents.js.map +1 -1
- package/dist/embed.d.ts +23 -0
- package/dist/embed.d.ts.map +1 -0
- package/dist/embed.js +84 -0
- package/dist/embed.js.map +1 -0
- package/dist/events.d.ts +21 -0
- package/dist/events.d.ts.map +1 -1
- package/dist/events.js +5 -2
- package/dist/events.js.map +1 -1
- package/dist/history-state.d.ts.map +1 -1
- package/dist/history-state.js +2 -0
- package/dist/history-state.js.map +1 -1
- package/dist/index.d.ts +354 -335
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -13
- package/dist/index.js.map +1 -1
- package/dist/peer-dial.d.ts +33 -0
- package/dist/peer-dial.d.ts.map +1 -0
- package/dist/peer-dial.js +79 -0
- package/dist/peer-dial.js.map +1 -0
- package/dist/peer-mcp-server.d.ts +36 -0
- package/dist/peer-mcp-server.d.ts.map +1 -0
- package/dist/peer-mcp-server.js +71 -0
- package/dist/peer-mcp-server.js.map +1 -0
- package/dist/provider-specs.d.ts +38 -20
- package/dist/provider-specs.d.ts.map +1 -1
- package/dist/provider-specs.js +39 -13
- package/dist/provider-specs.js.map +1 -1
- package/dist/quick-model.d.ts.map +1 -1
- package/dist/quick-model.js +2 -1
- package/dist/quick-model.js.map +1 -1
- package/dist/runtime-state.d.ts +1 -1
- package/dist/runtime-state.js +1 -1
- package/dist/runtime-state.js.map +1 -1
- package/dist/safety-policy.d.ts +12 -3
- package/dist/safety-policy.d.ts.map +1 -1
- package/dist/safety-policy.js +30 -5
- package/dist/safety-policy.js.map +1 -1
- package/dist/schemas/agent.d.ts +6 -6
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +6 -2
- package/dist/schemas/agent.js.map +1 -1
- package/dist/schemas/agents.d.ts +42 -0
- package/dist/schemas/agents.d.ts.map +1 -1
- package/dist/schemas/agents.js +25 -4
- package/dist/schemas/agents.js.map +1 -1
- package/dist/schemas/automations.d.ts +11 -2
- package/dist/schemas/automations.d.ts.map +1 -1
- package/dist/schemas/automations.js +1 -1
- package/dist/schemas/automations.js.map +1 -1
- package/dist/schemas/ci.d.ts +6 -0
- package/dist/schemas/ci.d.ts.map +1 -1
- package/dist/schemas/ci.js +3 -2
- package/dist/schemas/ci.js.map +1 -1
- package/dist/schemas/context.d.ts +30 -0
- package/dist/schemas/context.d.ts.map +1 -0
- package/dist/schemas/context.js +34 -0
- package/dist/schemas/context.js.map +1 -0
- package/dist/schemas/{computers.d.ts → devices.d.ts} +155 -61
- package/dist/schemas/devices.d.ts.map +1 -0
- package/dist/schemas/devices.js +157 -0
- package/dist/schemas/devices.js.map +1 -0
- package/dist/schemas/hosts.d.ts +12 -0
- package/dist/schemas/hosts.d.ts.map +1 -1
- package/dist/schemas/hosts.js +1 -0
- package/dist/schemas/hosts.js.map +1 -1
- package/dist/schemas/issues.d.ts +0 -5
- package/dist/schemas/issues.d.ts.map +1 -1
- package/dist/schemas/issues.js +0 -1
- package/dist/schemas/issues.js.map +1 -1
- package/dist/schemas/personas.d.ts +5 -3
- package/dist/schemas/personas.d.ts.map +1 -1
- package/dist/schemas/personas.js +3 -2
- package/dist/schemas/personas.js.map +1 -1
- package/dist/schemas/provider-oauth.d.ts +48 -16
- package/dist/schemas/provider-oauth.d.ts.map +1 -1
- package/dist/schemas/provider-oauth.js +22 -20
- package/dist/schemas/provider-oauth.js.map +1 -1
- package/dist/schemas/settings.d.ts +1 -52
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +8 -11
- package/dist/schemas/settings.js.map +1 -1
- package/dist/schemas/terminal.js +9 -9
- package/dist/schemas/terminal.js.map +1 -1
- package/dist/schemas/usage.d.ts +0 -2
- package/dist/schemas/usage.d.ts.map +1 -1
- package/dist/schemas/usage.js +0 -2
- package/dist/schemas/usage.js.map +1 -1
- package/dist/shell-regions.d.ts +4 -0
- package/dist/shell-regions.d.ts.map +1 -0
- package/dist/shell-regions.js +156 -0
- package/dist/shell-regions.js.map +1 -0
- package/dist/workspace-state.d.ts +15 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +21 -5
- package/dist/workspace-state.js.map +1 -1
- package/package.json +48 -4
- package/src/agent-catalog.ts +1 -1
- package/src/arrival.ts +3 -3
- package/src/batch-runs.test.ts +167 -0
- package/src/batch-runs.ts +181 -0
- package/src/command-classes.test.ts +195 -71
- package/src/command-classes.ts +148 -46
- package/src/contracts/accounts.contract.ts +94 -0
- package/src/contracts/agents.contract.ts +4 -3
- package/src/contracts/exit.contract.ts +2 -2
- package/src/contracts/host.contract.ts +17 -5
- package/src/contracts/settings.contract.ts +1 -1
- package/src/contracts/system.contract.ts +43 -24
- package/src/contracts/vpn.contract.ts +2 -2
- package/src/documents.test.ts +2 -1
- package/src/documents.ts +7 -11
- package/src/embed.test.ts +68 -0
- package/src/embed.ts +164 -0
- package/src/events.ts +30 -3
- package/src/history-state.ts +12 -3
- package/src/host-protocol.ts +2 -2
- package/src/index.ts +6 -14
- package/src/model-order.ts +1 -1
- package/src/peer-dial.test.ts +203 -0
- package/src/peer-dial.ts +163 -0
- package/src/peer-mcp-server.test.ts +104 -0
- package/src/peer-mcp-server.ts +144 -0
- package/src/provider-specs.test.ts +44 -17
- package/src/provider-specs.ts +147 -67
- package/src/quick-model.ts +10 -3
- package/src/routes.test.ts +6 -3
- package/src/runner-protocol.ts +1 -1
- package/src/runtime-state.ts +2 -2
- package/src/safety-policy.test.ts +88 -0
- package/src/safety-policy.ts +84 -14
- package/src/schemas/agent.ts +41 -13
- package/src/schemas/agents.ts +65 -4
- package/src/schemas/automations.ts +6 -4
- package/src/schemas/capabilities.ts +4 -4
- package/src/schemas/ci.ts +23 -6
- package/src/schemas/context.ts +87 -0
- package/src/schemas/{computers.ts → devices.ts} +201 -109
- package/src/schemas/hosts.ts +5 -1
- package/src/schemas/issues.ts +0 -4
- package/src/schemas/personas.ts +8 -3
- package/src/schemas/provider-oauth.ts +49 -52
- package/src/schemas/settings.ts +30 -54
- package/src/schemas/terminal.ts +12 -12
- package/src/schemas/usage.ts +3 -27
- package/src/schemas/version-seam.test.ts +0 -1
- package/src/shell-regions.ts +289 -0
- package/src/versions.ts +2 -2
- package/src/webext-links.ts +2 -2
- package/src/webext-protocol.ts +2 -2
- package/src/workspace-state.test.ts +55 -1
- package/src/workspace-state.ts +76 -11
- package/dist/contracts/claude.contract.d.ts +0 -91
- package/dist/contracts/claude.contract.d.ts.map +0 -1
- package/dist/contracts/claude.contract.js +0 -50
- package/dist/contracts/claude.contract.js.map +0 -1
- package/dist/contracts/cursor.contract.d.ts.map +0 -1
- package/dist/contracts/cursor.contract.js +0 -50
- package/dist/contracts/cursor.contract.js.map +0 -1
- package/dist/contracts/grok.contract.d.ts +0 -36
- package/dist/contracts/grok.contract.d.ts.map +0 -1
- package/dist/contracts/grok.contract.js +0 -31
- package/dist/contracts/grok.contract.js.map +0 -1
- package/dist/contracts/keys.contract.d.ts +0 -81
- package/dist/contracts/keys.contract.d.ts.map +0 -1
- package/dist/contracts/keys.contract.js +0 -51
- package/dist/contracts/keys.contract.js.map +0 -1
- package/dist/schemas/computers.d.ts.map +0 -1
- package/dist/schemas/computers.js +0 -134
- package/dist/schemas/computers.js.map +0 -1
- package/src/contracts/claude.contract.ts +0 -71
- package/src/contracts/cursor.contract.ts +0 -74
- package/src/contracts/grok.contract.ts +0 -41
- package/src/contracts/keys.contract.ts +0 -79
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ContractRoute } from "./routes.js";
|
|
2
2
|
import { contractRoutes, requestPathFor, routeForProcedure, routeNameForRequest, routeShapes } from "./routes.js";
|
|
3
|
+
import { accountsContract } from "./contracts/accounts.contract.js";
|
|
3
4
|
import { activityContract } from "./contracts/activity.contract.js";
|
|
4
5
|
import { agentContract } from "./contracts/agent.contract.js";
|
|
5
6
|
import { agentsContract } from "./contracts/agents.contract.js";
|
|
@@ -8,14 +9,11 @@ import { automationsContract } from "./contracts/automations.contract.js";
|
|
|
8
9
|
import { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
9
10
|
import { choresContract } from "./contracts/chores.contract.js";
|
|
10
11
|
import { ciContract } from "./contracts/ci.contract.js";
|
|
11
|
-
import { claudeContract } from "./contracts/claude.contract.js";
|
|
12
|
-
import { cursorContract } from "./contracts/cursor.contract.js";
|
|
13
12
|
import { endpointsContract } from "./contracts/endpoints.contract.js";
|
|
14
13
|
import { exitContract } from "./contracts/exit.contract.js";
|
|
15
14
|
import { extensionsContract } from "./contracts/extensions.contract.js";
|
|
16
15
|
import { personasContract } from "./contracts/personas.contract.js";
|
|
17
16
|
import { gitContract } from "./contracts/git.contract.js";
|
|
18
|
-
import { grokContract } from "./contracts/grok.contract.js";
|
|
19
17
|
import { historyContract } from "./contracts/history.contract.js";
|
|
20
18
|
import { intenticContract } from "./contracts/intentic.contract.js";
|
|
21
19
|
import { inventoryContract } from "./contracts/inventory.contract.js";
|
|
@@ -26,7 +24,6 @@ import { panelsContract } from "./contracts/panels.contract.js";
|
|
|
26
24
|
import { portsContract } from "./contracts/ports.contract.js";
|
|
27
25
|
import { publicContract } from "./contracts/public.contract.js";
|
|
28
26
|
import { prepushContract } from "./contracts/prepush.contract.js";
|
|
29
|
-
import { keysContract } from "./contracts/keys.contract.js";
|
|
30
27
|
import { providersContract } from "./contracts/providers.contract.js";
|
|
31
28
|
import { pushContract } from "./contracts/push.contract.js";
|
|
32
29
|
import { safetyContract } from "./contracts/safety.contract.js";
|
|
@@ -42,6 +39,7 @@ import { vpnContract } from "./contracts/vpn.contract.js";
|
|
|
42
39
|
import { workflowsContract } from "./contracts/workflows.contract.js";
|
|
43
40
|
import { workspaceContract } from "./contracts/workspace.contract.js";
|
|
44
41
|
|
|
42
|
+
export { accountsContract } from "./contracts/accounts.contract.js";
|
|
45
43
|
export { activityContract } from "./contracts/activity.contract.js";
|
|
46
44
|
export { agentContract } from "./contracts/agent.contract.js";
|
|
47
45
|
export { agentsContract } from "./contracts/agents.contract.js";
|
|
@@ -50,17 +48,14 @@ export { automationsContract } from "./contracts/automations.contract.js";
|
|
|
50
48
|
export { capabilitiesContract } from "./contracts/capabilities.contract.js";
|
|
51
49
|
export { choresContract } from "./contracts/chores.contract.js";
|
|
52
50
|
export { ciContract } from "./contracts/ci.contract.js";
|
|
53
|
-
export { claudeContract } from "./contracts/claude.contract.js";
|
|
54
|
-
export { cursorContract } from "./contracts/cursor.contract.js";
|
|
55
51
|
export { endpointsContract, type TrialHealth, TrialStatusSchema, type TrialStatusResponse } from "./contracts/endpoints.contract.js";
|
|
56
52
|
export { exitContract } from "./contracts/exit.contract.js";
|
|
57
53
|
export { extensionsContract } from "./contracts/extensions.contract.js";
|
|
58
54
|
export { personasContract } from "./contracts/personas.contract.js";
|
|
59
55
|
export { gitContract } from "./contracts/git.contract.js";
|
|
60
|
-
export { grokContract } from "./contracts/grok.contract.js";
|
|
61
56
|
export { historyContract } from "./contracts/history.contract.js";
|
|
62
57
|
/* Deliberately NOT part of `sandboxContract` below: that map is the daemon's own HTTP surface, and this one is
|
|
63
|
-
* spoken the other way round, over a connected
|
|
58
|
+
* spoken the other way round, over a connected device's WebSocket, with the MACHINE implementing it. */
|
|
64
59
|
export { hostContract } from "./contracts/host.contract.js";
|
|
65
60
|
// The same inversion again, one layer in: spoken over the socket a BROWSER EXTENSION opened, with the
|
|
66
61
|
// extension implementing it (contracts/webext.contract.ts).
|
|
@@ -77,7 +72,6 @@ export { panelsContract } from "./contracts/panels.contract.js";
|
|
|
77
72
|
export { portsContract } from "./contracts/ports.contract.js";
|
|
78
73
|
export { publicContract } from "./contracts/public.contract.js";
|
|
79
74
|
export { prepushContract } from "./contracts/prepush.contract.js";
|
|
80
|
-
export { keysContract } from "./contracts/keys.contract.js";
|
|
81
75
|
export { providersContract } from "./contracts/providers.contract.js";
|
|
82
76
|
export { pushContract } from "./contracts/push.contract.js";
|
|
83
77
|
export { safetyContract } from "./contracts/safety.contract.js";
|
|
@@ -159,7 +153,8 @@ export * from "./schemas/capabilities.js";
|
|
|
159
153
|
export * from "./schemas/ci.js";
|
|
160
154
|
export * from "./schemas/claude-gate.js";
|
|
161
155
|
export * from "./schemas/codebase-health.js";
|
|
162
|
-
export * from "./schemas/
|
|
156
|
+
export * from "./schemas/context.js";
|
|
157
|
+
export * from "./schemas/devices.js";
|
|
163
158
|
export * from "./schemas/engines.js";
|
|
164
159
|
export * from "./schemas/environment.js";
|
|
165
160
|
export * from "./schemas/exit.js";
|
|
@@ -212,6 +207,7 @@ export * from "./workflow-faults.js";
|
|
|
212
207
|
// browser's typed oRPC client (ContractRouterClient<typeof sandboxContract>). The wire paths it declares are
|
|
213
208
|
// mounted at the sandbox root, so /health and /workspace/raw (plain Hono routes) sit alongside it.
|
|
214
209
|
export const sandboxContract = {
|
|
210
|
+
accounts: accountsContract,
|
|
215
211
|
activity: activityContract,
|
|
216
212
|
agent: agentContract,
|
|
217
213
|
agents: agentsContract,
|
|
@@ -220,8 +216,6 @@ export const sandboxContract = {
|
|
|
220
216
|
capabilities: capabilitiesContract,
|
|
221
217
|
chores: choresContract,
|
|
222
218
|
ci: ciContract,
|
|
223
|
-
claude: claudeContract,
|
|
224
|
-
cursor: cursorContract,
|
|
225
219
|
endpoints: endpointsContract,
|
|
226
220
|
extensions: extensionsContract,
|
|
227
221
|
personas: personasContract,
|
|
@@ -232,7 +226,6 @@ export const sandboxContract = {
|
|
|
232
226
|
skills: skillsContract,
|
|
233
227
|
intentic: intenticContract,
|
|
234
228
|
git: gitContract,
|
|
235
|
-
grok: grokContract,
|
|
236
229
|
history: historyContract,
|
|
237
230
|
workspace: workspaceContract,
|
|
238
231
|
inventory: inventoryContract,
|
|
@@ -243,7 +236,6 @@ export const sandboxContract = {
|
|
|
243
236
|
ports: portsContract,
|
|
244
237
|
public: publicContract,
|
|
245
238
|
prepush: prepushContract,
|
|
246
|
-
keys: keysContract,
|
|
247
239
|
providers: providersContract,
|
|
248
240
|
push: pushContract,
|
|
249
241
|
secrets: secretsContract,
|
package/src/model-order.ts
CHANGED
|
@@ -146,7 +146,7 @@ const releaseTierRankOf = (family: string): number => lastRankOf(family, RELEASE
|
|
|
146
146
|
*
|
|
147
147
|
* That is not a small mis-sort. Thinking is the difference between a commit message that is in the box before
|
|
148
148
|
* the user has finished reading the file list and one that arrives half a minute later, measured at ~2s versus
|
|
149
|
-
* ~27s on the same model and the same diff (
|
|
149
|
+
* ~27s on the same model and the same diff (claude/claude-one-shot.ts, which disables thinking for the rungs where a
|
|
150
150
|
* request parameter can). For a routed rung there is no such parameter: the id IS the setting, so this ranking
|
|
151
151
|
* is the only place the choice can be made.
|
|
152
152
|
*
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
import { expect, test, vi } from "vitest";
|
|
2
|
+
import { dialPeer, type SocketLike } from "./peer-dial.js";
|
|
3
|
+
|
|
4
|
+
/* The loop every peer runs, over a socket the test plays. What is at stake is the order of the two phases
|
|
5
|
+
* (handler attached BEFORE the hello goes out), the retry after a drop, the one close that is never retried,
|
|
6
|
+
* and that a stop reaches a dial still deciding where to go. */
|
|
7
|
+
|
|
8
|
+
class FakeSocket implements SocketLike {
|
|
9
|
+
readyState = 0;
|
|
10
|
+
readonly sent: string[] = [];
|
|
11
|
+
closed: { readonly code: number | undefined; readonly reason: string | undefined } | undefined;
|
|
12
|
+
private readonly listeners = new Map<string, Set<(event: { readonly code?: number }) => void>>();
|
|
13
|
+
|
|
14
|
+
addEventListener(type: "open" | "close" | "error", listener: (event: { readonly code?: number }) => void): void {
|
|
15
|
+
const set = this.listeners.get(type) ?? new Set();
|
|
16
|
+
set.add(listener);
|
|
17
|
+
this.listeners.set(type, set);
|
|
18
|
+
}
|
|
19
|
+
send(data: string): void {
|
|
20
|
+
this.sent.push(data);
|
|
21
|
+
}
|
|
22
|
+
close(code?: number, reason?: string): void {
|
|
23
|
+
this.closed = { code, reason };
|
|
24
|
+
this.readyState = 3;
|
|
25
|
+
}
|
|
26
|
+
opens(): void {
|
|
27
|
+
this.readyState = 1;
|
|
28
|
+
this.emit("open");
|
|
29
|
+
}
|
|
30
|
+
drops(code: number): void {
|
|
31
|
+
this.readyState = 3;
|
|
32
|
+
this.emit("close", code);
|
|
33
|
+
}
|
|
34
|
+
private emit(type: string, code?: number): void {
|
|
35
|
+
for (const listener of this.listeners.get(type) ?? []) {
|
|
36
|
+
listener(code === undefined ? {} : { code });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// A ladder that answers with a fixed delay and records what it was told, so the test can read the held time.
|
|
42
|
+
const ladder = (delay = 1_000) => {
|
|
43
|
+
const held: number[] = [];
|
|
44
|
+
return {
|
|
45
|
+
held,
|
|
46
|
+
next: (heldMs: number) => {
|
|
47
|
+
held.push(heldMs);
|
|
48
|
+
return delay;
|
|
49
|
+
},
|
|
50
|
+
};
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const dialling = () => {
|
|
54
|
+
const sockets: FakeSocket[] = [];
|
|
55
|
+
const attached: FakeSocket[] = [];
|
|
56
|
+
const said: string[] = [];
|
|
57
|
+
const revoked = vi.fn();
|
|
58
|
+
const link = dialPeer<FakeSocket>({
|
|
59
|
+
open: async () => {
|
|
60
|
+
const socket = new FakeSocket();
|
|
61
|
+
sockets.push(socket);
|
|
62
|
+
return { socket, said: `connected #${sockets.length}` };
|
|
63
|
+
},
|
|
64
|
+
hello: () => ({ type: "hello", token: "iht_test", version: "1.0.0" }),
|
|
65
|
+
attach: (socket) => void attached.push(socket),
|
|
66
|
+
backoff: ladder(),
|
|
67
|
+
log: (message) => void said.push(message),
|
|
68
|
+
revoked,
|
|
69
|
+
});
|
|
70
|
+
return { link, sockets, attached, said, revoked };
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
test("the handler is attached before the hello goes out, and the hello is the first frame", async () => {
|
|
74
|
+
const { link, sockets, attached, said } = dialling();
|
|
75
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
76
|
+
expect(link.state()).toBe("connecting");
|
|
77
|
+
sockets[0]?.opens();
|
|
78
|
+
await vi.waitFor(() => expect(sockets[0]?.sent).toHaveLength(1));
|
|
79
|
+
expect(attached).toEqual([sockets[0]]);
|
|
80
|
+
expect(JSON.parse(sockets[0]?.sent[0] ?? "{}")).toEqual({ type: "hello", token: "iht_test", version: "1.0.0" });
|
|
81
|
+
expect(said).toEqual(["connected #1"]);
|
|
82
|
+
expect(link.state()).toBe("open");
|
|
83
|
+
link.stop();
|
|
84
|
+
await link.done;
|
|
85
|
+
expect(sockets[0]?.closed).toEqual({ code: 1000, reason: "stopping" });
|
|
86
|
+
expect(link.state()).toBe("closed");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test("a drop redials on the ladder, telling it how long the socket held", async () => {
|
|
90
|
+
vi.useFakeTimers();
|
|
91
|
+
try {
|
|
92
|
+
const { link, sockets, said } = dialling();
|
|
93
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
94
|
+
sockets[0]?.opens();
|
|
95
|
+
await vi.advanceTimersByTimeAsync(5_000);
|
|
96
|
+
sockets[0]?.drops(1006);
|
|
97
|
+
expect(link.state()).toBe("connecting");
|
|
98
|
+
expect(said.at(-1)).toBe("disconnected (1006); reconnecting in 1s");
|
|
99
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
100
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(2));
|
|
101
|
+
link.stop();
|
|
102
|
+
await link.done;
|
|
103
|
+
} finally {
|
|
104
|
+
vi.useRealTimers();
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("a refused enrollment (1008) is never retried and is reported once", async () => {
|
|
109
|
+
vi.useFakeTimers();
|
|
110
|
+
try {
|
|
111
|
+
const { link, sockets, revoked } = dialling();
|
|
112
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
113
|
+
sockets[0]?.opens();
|
|
114
|
+
sockets[0]?.drops(1008);
|
|
115
|
+
await link.done;
|
|
116
|
+
expect(revoked).toHaveBeenCalledTimes(1);
|
|
117
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
118
|
+
expect(sockets).toHaveLength(1);
|
|
119
|
+
expect(link.state()).toBe("closed");
|
|
120
|
+
} finally {
|
|
121
|
+
vi.useRealTimers();
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// A stop that lands while the address is still being decided has nothing to close and must open nothing after
|
|
126
|
+
// the fact: a socket dialled by a loop that has already reported itself done would be a connection nobody stops.
|
|
127
|
+
test("a stop during resolution opens nothing, and a socket a slow resolver still hands over is closed", async () => {
|
|
128
|
+
let answer: (() => void) | undefined;
|
|
129
|
+
const sockets: FakeSocket[] = [];
|
|
130
|
+
const link = dialPeer<FakeSocket>({
|
|
131
|
+
open: async (signal) => {
|
|
132
|
+
await new Promise<void>((resolve) => {
|
|
133
|
+
answer = resolve;
|
|
134
|
+
});
|
|
135
|
+
if (signal.aborted) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
const socket = new FakeSocket();
|
|
139
|
+
sockets.push(socket);
|
|
140
|
+
return { socket };
|
|
141
|
+
},
|
|
142
|
+
hello: () => ({}),
|
|
143
|
+
attach: () => undefined,
|
|
144
|
+
backoff: ladder(),
|
|
145
|
+
log: () => undefined,
|
|
146
|
+
revoked: () => undefined,
|
|
147
|
+
});
|
|
148
|
+
await vi.waitFor(() => expect(answer).toEqual(expect.any(Function)));
|
|
149
|
+
link.stop();
|
|
150
|
+
await link.done;
|
|
151
|
+
answer?.();
|
|
152
|
+
await new Promise<void>((resolve) => setTimeout(resolve, 0));
|
|
153
|
+
expect(sockets).toHaveLength(0);
|
|
154
|
+
|
|
155
|
+
// The other order: a resolver that ignores the signal and hands a socket over anyway.
|
|
156
|
+
const late: FakeSocket[] = [];
|
|
157
|
+
const careless = dialPeer<FakeSocket>({
|
|
158
|
+
open: async () => {
|
|
159
|
+
await new Promise<void>((resolve) => {
|
|
160
|
+
answer = resolve;
|
|
161
|
+
});
|
|
162
|
+
const socket = new FakeSocket();
|
|
163
|
+
late.push(socket);
|
|
164
|
+
return { socket };
|
|
165
|
+
},
|
|
166
|
+
hello: () => ({}),
|
|
167
|
+
attach: () => undefined,
|
|
168
|
+
backoff: ladder(),
|
|
169
|
+
log: () => undefined,
|
|
170
|
+
revoked: () => undefined,
|
|
171
|
+
});
|
|
172
|
+
await vi.waitFor(() => expect(answer).toEqual(expect.any(Function)));
|
|
173
|
+
careless.stop();
|
|
174
|
+
answer?.();
|
|
175
|
+
await vi.waitFor(() => expect(late[0]?.closed).toEqual({ code: 1000, reason: "stopping" }));
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
test("a pairing that is gone by the next attempt ends the loop rather than dialling nowhere", async () => {
|
|
179
|
+
let pairings = 1;
|
|
180
|
+
const sockets: FakeSocket[] = [];
|
|
181
|
+
const link = dialPeer<FakeSocket>({
|
|
182
|
+
open: async () => {
|
|
183
|
+
if (pairings === 0) {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
pairings -= 1;
|
|
187
|
+
const socket = new FakeSocket();
|
|
188
|
+
sockets.push(socket);
|
|
189
|
+
return { socket };
|
|
190
|
+
},
|
|
191
|
+
hello: () => ({}),
|
|
192
|
+
attach: () => undefined,
|
|
193
|
+
backoff: ladder(0),
|
|
194
|
+
log: () => undefined,
|
|
195
|
+
revoked: () => undefined,
|
|
196
|
+
});
|
|
197
|
+
await vi.waitFor(() => expect(sockets).toHaveLength(1));
|
|
198
|
+
sockets[0]?.opens();
|
|
199
|
+
sockets[0]?.drops(1006);
|
|
200
|
+
await link.done;
|
|
201
|
+
expect(sockets).toHaveLength(1);
|
|
202
|
+
expect(link.state()).toBe("closed");
|
|
203
|
+
});
|
package/src/peer-dial.ts
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/* THE ONE SOCKET a peer holds to its sandbox, written once for the three things that hold one: the machine
|
|
2
|
+
* agent (@intentic/machine), the browser extension (@intentic/webext) and a runner (the daemon itself, in
|
|
3
|
+
* runner mode). Each dials OUT, sends a plain-JSON hello carrying its enrollment token (host-protocol.ts,
|
|
4
|
+
* webext-protocol.ts, runner-protocol.ts), and from then on everything the sandbox asks arrives on the socket
|
|
5
|
+
* as oRPC against the door's contract, with the PEER serving.
|
|
6
|
+
*
|
|
7
|
+
* OUTBOUND ONLY, and that is the entire networking story: no port is opened, no router configured, no VPN
|
|
8
|
+
* joined. A laptop on hotel wifi behind a corporate proxy holds this connection because it is an ordinary
|
|
9
|
+
* outbound wss://, the same thing every chat app on the machine is already doing.
|
|
10
|
+
*
|
|
11
|
+
* The socket has exactly two phases. First the hello, plain JSON, because the sandbox has nothing to call until
|
|
12
|
+
* it knows whose socket this is (the token rides the FRAME, never the URL, which would write a durable key into
|
|
13
|
+
* every proxy log between the peer and the sandbox). Then the sandbox attaches its client and the socket is pure
|
|
14
|
+
* oRPC: request/response correlation, argument validation and error shape all belong to the link from that
|
|
15
|
+
* point on, which is why there is no message plumbing here.
|
|
16
|
+
*
|
|
17
|
+
* THE HANDLER IS ATTACHED BEFORE THE HELLO GOES OUT: the sandbox may call the moment it has verified the token,
|
|
18
|
+
* and a race there would drop the first `setScopes`, the one call whose loss would leave a device enforcing a
|
|
19
|
+
* stale grant.
|
|
20
|
+
*
|
|
21
|
+
* RECONNECTION IS THE NORMAL CASE, not the failure case. Lids close, wifi changes, tunnels idle out, sandboxes
|
|
22
|
+
* restart on every rebuild. So a dropped socket is routine and comes back on an exponential ladder the caller
|
|
23
|
+
* supplies (@intentic/base's createBackoff, over PEER_LINK_BACKOFF): a link that held for a minute was working
|
|
24
|
+
* and its drop redials at the floor; one that opened and died at once keeps climbing. The one thing that is NOT
|
|
25
|
+
* retried is a refused enrollment (close code 1008): the owner revoked this peer, that never heals on its own,
|
|
26
|
+
* and hammering a door that has been locked is how an agent turns a revocation into a support ticket.
|
|
27
|
+
*
|
|
28
|
+
* Here, in the contract, beside the hello schemas it sends and the URL builders it dials, rather than in a
|
|
29
|
+
* package of its own: every peer already depends on this one, and the loop is the far end of the same wire. */
|
|
30
|
+
|
|
31
|
+
// Backoff bounds. The first retry is fast because the overwhelmingly common cause is a sandbox restart, which
|
|
32
|
+
// takes seconds; the ceiling is low enough that a laptop opened after a night asleep is back within a minute.
|
|
33
|
+
export const PEER_LINK_BACKOFF = { floorMs: 1_000, capMs: 30_000, stableMs: 60_000 } as const;
|
|
34
|
+
|
|
35
|
+
// The sandbox closes with this when the token is not enrolled: a decision, not a fault, and one that never heals.
|
|
36
|
+
const UNAUTHORIZED = 1008;
|
|
37
|
+
|
|
38
|
+
// A socket's readyState value for "open", per the WebSocket spec, named so a caller's fake need not import a class.
|
|
39
|
+
const OPEN = 1;
|
|
40
|
+
|
|
41
|
+
// What the two runtimes' sockets have in common, taken structurally: a browser's WebSocket, node's global one,
|
|
42
|
+
// and a test's fake all satisfy it without a cast.
|
|
43
|
+
export interface SocketLike {
|
|
44
|
+
readonly readyState: number;
|
|
45
|
+
addEventListener(type: "open" | "close" | "error", listener: (event: { readonly code?: number }) => void): void;
|
|
46
|
+
send(data: string): void;
|
|
47
|
+
close(code?: number, reason?: string): void;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface PeerDialSpec<S extends SocketLike> {
|
|
51
|
+
/* ONE ATTEMPT'S SOCKET, dialled at whatever address the caller resolves for THIS attempt: a sandbox on the
|
|
52
|
+
* same machine is a loopback hop away on one attempt and behind its tunnel on the next, and asking on every
|
|
53
|
+
* attempt rather than once at startup is exactly what a reconnect is about. `said` is the line the log gets
|
|
54
|
+
* once the socket is open. Undefined ⇒ there is nothing to dial any more (the pairing was forgotten), and
|
|
55
|
+
* the loop ends. `signal` is aborted by `stop`, so a resolution that outlives the stop opens nothing. */
|
|
56
|
+
readonly open: (signal: AbortSignal) => Promise<{ readonly socket: S; readonly said?: string } | undefined>;
|
|
57
|
+
// The first frame, carrying the enrollment token and whatever the door's hello schema asks for.
|
|
58
|
+
readonly hello: () => Promise<Record<string, unknown>> | Record<string, unknown>;
|
|
59
|
+
// Hand the open socket to the oRPC handler serving the door's contract.
|
|
60
|
+
readonly attach: (socket: S) => void;
|
|
61
|
+
// The retry ladder: how long to wait after a drop, given how long the socket had held.
|
|
62
|
+
readonly backoff: { readonly next: (heldMs: number) => number };
|
|
63
|
+
readonly log: (message: string) => void;
|
|
64
|
+
// The sandbox refused the enrollment (1008). The loop has already stopped; this is where the peer says so.
|
|
65
|
+
readonly revoked: () => void;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface PeerLink {
|
|
69
|
+
// Resolves when the loop is asked to stop or refused for good; never rejects, a connection error is a retry.
|
|
70
|
+
readonly done: Promise<void>;
|
|
71
|
+
readonly stop: (reason?: string) => void;
|
|
72
|
+
// "connecting" covers a dial in flight AND a retry waiting on the ladder: both mean nobody should start another.
|
|
73
|
+
readonly state: () => "open" | "connecting" | "closed";
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export const dialPeer = <S extends SocketLike>(spec: PeerDialSpec<S>): PeerLink => {
|
|
77
|
+
const stopping = new AbortController();
|
|
78
|
+
let socket: S | undefined;
|
|
79
|
+
let waiting = false;
|
|
80
|
+
let openedAt: number | undefined;
|
|
81
|
+
let resolveDone: () => void = () => undefined;
|
|
82
|
+
const done = new Promise<void>((resolve) => {
|
|
83
|
+
resolveDone = resolve;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const open = async (): Promise<void> => {
|
|
87
|
+
waiting = true;
|
|
88
|
+
const attempt = await spec.open(stopping.signal);
|
|
89
|
+
waiting = false;
|
|
90
|
+
// Stopped while the address was still being decided: there is nothing to open, and `stop` has already
|
|
91
|
+
// settled `done`. A socket a slow resolver still handed over is closed rather than left dialling.
|
|
92
|
+
if (stopping.signal.aborted) {
|
|
93
|
+
attempt?.socket.close(1000, "stopping");
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
if (attempt === undefined) {
|
|
97
|
+
stopping.abort();
|
|
98
|
+
resolveDone();
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const ws = attempt.socket;
|
|
102
|
+
socket = ws;
|
|
103
|
+
|
|
104
|
+
ws.addEventListener("open", () => {
|
|
105
|
+
openedAt = Date.now();
|
|
106
|
+
spec.attach(ws);
|
|
107
|
+
const send = (hello: Record<string, unknown>): void => {
|
|
108
|
+
ws.send(JSON.stringify(hello));
|
|
109
|
+
if (attempt.said !== undefined) {
|
|
110
|
+
spec.log(attempt.said);
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
// Sent in the open handler's own tick where the hello is already in hand: the sandbox's auth
|
|
114
|
+
// deadline is generous, but a frame that could go now and waits a tick is a frame a test cannot
|
|
115
|
+
// read, and a hello that needs a read first (a runner's settings claim) simply follows it.
|
|
116
|
+
const hello = spec.hello();
|
|
117
|
+
if (hello instanceof Promise) {
|
|
118
|
+
void hello.then(send);
|
|
119
|
+
} else {
|
|
120
|
+
send(hello);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
ws.addEventListener("close", (event) => {
|
|
125
|
+
socket = undefined;
|
|
126
|
+
if (stopping.signal.aborted) {
|
|
127
|
+
resolveDone();
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (event.code === UNAUTHORIZED) {
|
|
131
|
+
stopping.abort();
|
|
132
|
+
resolveDone();
|
|
133
|
+
spec.revoked();
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const delay = spec.backoff.next(openedAt === undefined ? 0 : Date.now() - openedAt);
|
|
137
|
+
openedAt = undefined;
|
|
138
|
+
spec.log(`disconnected (${event.code ?? "no code"}); reconnecting in ${Math.round(delay / 1000)}s`);
|
|
139
|
+
waiting = true;
|
|
140
|
+
setTimeout(() => void open(), delay);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// A socket error is always followed by a close event, which owns the retry; this only records the cause,
|
|
144
|
+
// which the close code alone never carries.
|
|
145
|
+
ws.addEventListener("error", () => spec.log("connection error"));
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
void open();
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
done,
|
|
152
|
+
stop: (reason = "stopping") => {
|
|
153
|
+
stopping.abort();
|
|
154
|
+
waiting = false;
|
|
155
|
+
// Forgotten before the close frame arrives, so `state` reads closed the moment the peer asked.
|
|
156
|
+
const held = socket;
|
|
157
|
+
socket = undefined;
|
|
158
|
+
held?.close(1000, reason);
|
|
159
|
+
resolveDone();
|
|
160
|
+
},
|
|
161
|
+
state: () => (socket?.readyState === OPEN ? "open" : socket !== undefined || waiting ? "connecting" : "closed"),
|
|
162
|
+
};
|
|
163
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { expect, test } from "vitest";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { createMcpServer, type McpAuditEntry, textResult, tool } from "./peer-mcp-server.js";
|
|
4
|
+
|
|
5
|
+
/* The dispatch every peer serves its tools through, against two hand-written tools: what the model is shown,
|
|
6
|
+
* what an arriving call is checked against, and the rule that a failed tool is a result and not a fault. */
|
|
7
|
+
|
|
8
|
+
class Refused extends Error {}
|
|
9
|
+
|
|
10
|
+
const build = () => {
|
|
11
|
+
const audits: McpAuditEntry[] = [];
|
|
12
|
+
const handle = createMcpServer<{ readonly allowed: boolean }>({
|
|
13
|
+
serverInfo: () => ({ name: "intentic-test", version: "9.9.9" }),
|
|
14
|
+
tools: [
|
|
15
|
+
tool({
|
|
16
|
+
name: "echo",
|
|
17
|
+
description: "Say it back.",
|
|
18
|
+
input: z.object({ text: z.string().min(1) }),
|
|
19
|
+
run: async ({ text }, ctx) => {
|
|
20
|
+
if (!ctx.allowed) {
|
|
21
|
+
throw new Refused("the switch is off");
|
|
22
|
+
}
|
|
23
|
+
return textResult(text);
|
|
24
|
+
},
|
|
25
|
+
}),
|
|
26
|
+
tool({
|
|
27
|
+
name: "fail",
|
|
28
|
+
description: "Always errs as a result.",
|
|
29
|
+
input: z.object({}),
|
|
30
|
+
run: async () => textResult("nope", true),
|
|
31
|
+
}),
|
|
32
|
+
],
|
|
33
|
+
noSuchTool: (name) => `This peer has no tool called "${name}".`,
|
|
34
|
+
refused: (error) => error instanceof Refused,
|
|
35
|
+
errorMessage: (error) => (error instanceof Error ? error.message : String(error)),
|
|
36
|
+
audit: (entry) => void audits.push(entry),
|
|
37
|
+
});
|
|
38
|
+
return { handle, audits };
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const call = (name: string, args: Record<string, unknown>, allowed = true) => build().handle({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name, arguments: args } }, { allowed });
|
|
42
|
+
|
|
43
|
+
test("initialize names the peer and its build; ping answers empty; an unknown method is a JSON-RPC error", async () => {
|
|
44
|
+
const { handle } = build();
|
|
45
|
+
expect(await handle({ jsonrpc: "2.0", id: 1, method: "initialize" }, { allowed: true })).toMatchObject({
|
|
46
|
+
result: { capabilities: { tools: {} }, serverInfo: { name: "intentic-test", version: "9.9.9" } },
|
|
47
|
+
});
|
|
48
|
+
expect(await handle({ jsonrpc: "2.0", id: 2, method: "ping" }, { allowed: true })).toEqual({ jsonrpc: "2.0", id: 2, result: {} });
|
|
49
|
+
expect(await handle({ jsonrpc: "2.0", id: 3, method: "resources/list" }, { allowed: true })).toMatchObject({ error: { code: -32601 } });
|
|
50
|
+
expect(await handle({ jsonrpc: "2.0", method: "notifications/initialized" }, { allowed: true })).toBeUndefined();
|
|
51
|
+
expect(await handle("not an object", { allowed: true })).toMatchObject({ error: { code: -32600 } });
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test("tools/list publishes each tool's schema as JSON Schema, without the dialect line", async () => {
|
|
55
|
+
const { handle } = build();
|
|
56
|
+
const listed = (await handle({ jsonrpc: "2.0", id: 1, method: "tools/list" }, { allowed: true })) as { result: { tools: { name: string; inputSchema: Record<string, unknown> }[] } };
|
|
57
|
+
expect(listed.result.tools.map((entry) => entry.name)).toEqual(["echo", "fail"]);
|
|
58
|
+
expect(listed.result.tools[0]?.inputSchema).toMatchObject({ type: "object", properties: { text: { type: "string" } } });
|
|
59
|
+
expect(listed.result.tools[0]?.inputSchema).not.toHaveProperty("$schema");
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("a call is checked against the same schema, and a bad argument is a readable result, not a fault", async () => {
|
|
63
|
+
expect(await call("echo", { text: "hi" })).toEqual({ jsonrpc: "2.0", id: 1, result: { content: [{ type: "text", text: "hi" }], isError: false } });
|
|
64
|
+
const bad = (await call("echo", { text: "" })) as { result: { content: { text: string }[]; isError: boolean } };
|
|
65
|
+
expect(bad.result.isError).toBe(true);
|
|
66
|
+
expect(bad.result.content[0]?.text).toContain("text");
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("a tool this peer does not have answers in the peer's own words", async () => {
|
|
70
|
+
expect(await call("delete_everything", {})).toEqual({
|
|
71
|
+
jsonrpc: "2.0",
|
|
72
|
+
id: 1,
|
|
73
|
+
result: { content: [{ type: "text", text: `This peer has no tool called "delete_everything".` }], isError: true },
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
test("a thrown refusal and a thrown failure are both results, and the audit line says which", async () => {
|
|
78
|
+
const { handle, audits } = build();
|
|
79
|
+
const refused = (await handle({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "echo", arguments: { text: "hi" } } }, { allowed: false })) as {
|
|
80
|
+
result: { content: { text: string }[]; isError: boolean };
|
|
81
|
+
};
|
|
82
|
+
expect(refused.result).toEqual({ content: [{ type: "text", text: "the switch is off" }], isError: true });
|
|
83
|
+
await handle({ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "fail", arguments: {} } }, { allowed: true });
|
|
84
|
+
await handle({ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "echo", arguments: { text: "ok" } } }, { allowed: true });
|
|
85
|
+
expect(audits).toEqual([
|
|
86
|
+
{ tool: "echo", args: { text: "hi" }, ok: false, failure: { refused: true, message: "the switch is off" } },
|
|
87
|
+
{ tool: "fail", args: {}, ok: false },
|
|
88
|
+
{ tool: "echo", args: { text: "ok" }, ok: true },
|
|
89
|
+
]);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("an audit log that cannot be written never fails the answer", async () => {
|
|
93
|
+
const handle = createMcpServer<undefined>({
|
|
94
|
+
serverInfo: () => ({ name: "x", version: "1" }),
|
|
95
|
+
tools: [tool({ name: "hi", description: "Hi.", input: z.object({}), run: async () => textResult("hi") })],
|
|
96
|
+
noSuchTool: () => "no",
|
|
97
|
+
refused: () => false,
|
|
98
|
+
errorMessage: String,
|
|
99
|
+
audit: () => {
|
|
100
|
+
throw new Error("disk full");
|
|
101
|
+
},
|
|
102
|
+
});
|
|
103
|
+
expect(await handle({ jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "hi", arguments: {} } }, undefined)).toMatchObject({ result: { isError: false } });
|
|
104
|
+
});
|