@intentic/sandbox-contract 1.245.0 → 1.247.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +17 -1
- package/dist/batch-runs.d.ts +2 -0
- package/dist/batch-runs.d.ts.map +1 -1
- package/dist/batch-runs.js +1 -0
- package/dist/batch-runs.js.map +1 -1
- 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/agent.contract.d.ts +19 -0
- package/dist/contracts/agent.contract.d.ts.map +1 -1
- 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 +58 -20
- package/dist/contracts/settings.contract.d.ts.map +1 -1
- package/dist/contracts/system.contract.d.ts +80 -30
- 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/contracts/usage.contract.d.ts +22 -0
- package/dist/contracts/usage.contract.d.ts.map +1 -1
- package/dist/contracts/usage.contract.js +19 -0
- package/dist/contracts/usage.contract.js.map +1 -1
- package/dist/definition.d.ts +20 -28
- 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/fast-tier.js +1 -1
- package/dist/fast-tier.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 +453 -306
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -15
- package/dist/index.js.map +1 -1
- package/dist/model-pins.d.ts +17 -0
- package/dist/model-pins.d.ts.map +1 -0
- package/dist/{quick-model.js → model-pins.js} +18 -11
- package/dist/model-pins.js.map +1 -0
- package/dist/model-roles.d.ts +144 -0
- package/dist/model-roles.d.ts.map +1 -0
- package/dist/model-roles.js +129 -0
- package/dist/model-roles.js.map +1 -0
- 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/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 +27 -8
- package/dist/schemas/agent.d.ts.map +1 -1
- package/dist/schemas/agent.js +10 -4
- 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} +154 -60
- 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/plan-limits.d.ts +20 -0
- package/dist/schemas/plan-limits.d.ts.map +1 -1
- package/dist/schemas/plan-limits.js +21 -0
- package/dist/schemas/plan-limits.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 +42 -16
- package/dist/schemas/settings.d.ts.map +1 -1
- package/dist/schemas/settings.js +22 -28
- 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 +5 -2
- package/dist/schemas/usage.d.ts.map +1 -1
- package/dist/schemas/usage.js +5 -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 +8 -0
- package/dist/workspace-state.d.ts.map +1 -1
- package/dist/workspace-state.js +13 -5
- package/dist/workspace-state.js.map +1 -1
- package/package.json +37 -4
- package/src/agent-catalog.ts +2 -2
- package/src/arrival.ts +3 -3
- package/src/batch-runs.test.ts +10 -5
- package/src/batch-runs.ts +10 -3
- 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/usage.contract.ts +31 -0
- 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 +31 -4
- package/src/fast-tier.test.ts +1 -1
- package/src/fast-tier.ts +5 -5
- package/src/history-state.ts +12 -3
- package/src/host-protocol.ts +2 -2
- package/src/index.ts +8 -16
- package/src/model-order.ts +1 -1
- package/src/{quick-model.test.ts → model-pins.test.ts} +73 -29
- package/src/model-pins.ts +183 -0
- package/src/model-roles.ts +224 -0
- 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/plan-pools.ts +1 -1
- package/src/prompt-complexity.test.ts +1 -1
- package/src/prompt-complexity.ts +2 -2
- package/src/provider-specs.test.ts +45 -18
- package/src/provider-specs.ts +147 -67
- 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 +83 -30
- package/src/schemas/agents.ts +67 -6
- 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} +190 -107
- package/src/schemas/hosts.ts +5 -1
- package/src/schemas/issues.ts +0 -4
- package/src/schemas/personas.ts +8 -3
- package/src/schemas/plan-limits.ts +50 -0
- package/src/schemas/provider-oauth.ts +49 -52
- package/src/schemas/settings.ts +105 -140
- package/src/schemas/terminal.ts +12 -12
- package/src/schemas/usage.ts +62 -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 +48 -1
- package/src/workspace-state.ts +48 -11
- package/dist/agent-run-model.d.ts +0 -4
- package/dist/agent-run-model.d.ts.map +0 -1
- package/dist/agent-run-model.js +0 -13
- package/dist/agent-run-model.js.map +0 -1
- 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/quick-model.d.ts +0 -15
- package/dist/quick-model.d.ts.map +0 -1
- package/dist/quick-model.js.map +0 -1
- package/dist/schemas/computers.d.ts.map +0 -1
- package/dist/schemas/computers.js +0 -135
- package/dist/schemas/computers.js.map +0 -1
- package/src/agent-run-model.test.ts +0 -76
- package/src/agent-run-model.ts +0 -65
- 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/quick-model.ts +0 -155
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MCP_PROTOCOL_VERSION } from "./host-protocol.js";
|
|
3
|
+
|
|
4
|
+
/* THE MCP SERVER A PEER RUNS, on the peer, not in the sandbox.
|
|
5
|
+
*
|
|
6
|
+
* The sandbox's daemon forwards JSON-RPC verbatim and interprets none of it, so the tool table handed to this is
|
|
7
|
+
* the entire tool surface: what a device or a browser can do is decided by the build installed on it, and a
|
|
8
|
+
* peer that upgrades learns new tools without anything changing in the sandbox. That is the reason for the
|
|
9
|
+
* split; the alternative (schemas in the daemon, execution on the peer) makes every new tool a coordinated
|
|
10
|
+
* release of two products.
|
|
11
|
+
*
|
|
12
|
+
* The protocol implemented is the subset a Streamable HTTP client actually uses against a stateless server:
|
|
13
|
+
* initialize, tools/list, tools/call, ping, and notifications (which get no reply). Anything else answers
|
|
14
|
+
* "method not found", which is the correct JSON-RPC response and not an error worth logging.
|
|
15
|
+
*
|
|
16
|
+
* A FAILED TOOL IS NOT A FAILED CALL. Every error, a refused scope, a missing file, a command that exited 1, an
|
|
17
|
+
* argument that does not typecheck, comes back as a normal result with isError, because that is what a model can
|
|
18
|
+
* read and act on; a JSON-RPC error surfaces as a transport fault and invites a retry loop against a peer that
|
|
19
|
+
* will refuse it exactly the same way the second time.
|
|
20
|
+
*
|
|
21
|
+
* EACH TOOL'S ARGUMENTS ARE DESCRIBED ONCE. The zod schema a tool is built with is what the model is shown
|
|
22
|
+
* (`tools/list` publishes it as JSON Schema) AND what an arriving call is checked against, so the advertised
|
|
23
|
+
* shape and the accepted one cannot drift, the failure mode of writing both by hand, where a renamed field
|
|
24
|
+
* keeps validating and the model keeps being told about the old name. A handler receives its arguments typed. */
|
|
25
|
+
|
|
26
|
+
export interface McpTool<Ctx> {
|
|
27
|
+
readonly name: string;
|
|
28
|
+
readonly description: string;
|
|
29
|
+
// JSON Schema for `tools/list`, derived from the zod schema once at module load rather than per request.
|
|
30
|
+
readonly inputSchema: Record<string, unknown>;
|
|
31
|
+
readonly call: (args: unknown, ctx: Ctx) => Promise<Record<string, unknown>>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const textResult = (text: string, isError = false): Record<string, unknown> => ({ content: [{ type: "text", text }], isError });
|
|
35
|
+
|
|
36
|
+
/* One tool, from the only description of its arguments there is. The generic is what carries the schema's type
|
|
37
|
+
* through to the handler's parameter; `McpTool` erases it again, because the dispatch table holds them all and
|
|
38
|
+
* the parse is what re-establishes the type at the boundary. `Ctx` is what the peer hands every call beside
|
|
39
|
+
* its arguments: a device's live grant, nothing for a browser.
|
|
40
|
+
*
|
|
41
|
+
* `$schema` is dropped: the enclosing tool entry already says what this document is, and MCP clients read the
|
|
42
|
+
* keywords rather than the dialect declaration. */
|
|
43
|
+
export const tool = <Schema extends z.ZodType, Ctx>(spec: {
|
|
44
|
+
readonly name: string;
|
|
45
|
+
readonly description: string;
|
|
46
|
+
readonly input: Schema;
|
|
47
|
+
readonly run: (args: z.output<Schema>, ctx: Ctx) => Promise<Record<string, unknown>>;
|
|
48
|
+
}): McpTool<Ctx> => {
|
|
49
|
+
const { $schema: _dialect, ...inputSchema } = z.toJSONSchema(spec.input, { io: "input" });
|
|
50
|
+
return {
|
|
51
|
+
name: spec.name,
|
|
52
|
+
description: spec.description,
|
|
53
|
+
inputSchema,
|
|
54
|
+
call: async (args, ctx) => {
|
|
55
|
+
const parsed = spec.input.safeParse(args);
|
|
56
|
+
// Readable enough for a model to fix its own call: which field, and what was expected there.
|
|
57
|
+
return parsed.success ? await spec.run(parsed.data, ctx) : textResult(z.prettifyError(parsed.error), true);
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// What the peer's audit log is told about one call: the arguments verbatim (redaction is the peer's, it knows
|
|
63
|
+
// which of its tools carry typed secrets), and how it ended. A tool that answered with isError is `ok: false`
|
|
64
|
+
// with no message; one that threw carries what it said, and whether it was the peer's own refusal.
|
|
65
|
+
export interface McpAuditEntry {
|
|
66
|
+
readonly tool: string;
|
|
67
|
+
readonly args: Record<string, unknown>;
|
|
68
|
+
readonly ok: boolean;
|
|
69
|
+
readonly failure?: { readonly refused: boolean; readonly message: string };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export interface McpServerSpec<Ctx> {
|
|
73
|
+
readonly serverInfo: () => { readonly name: string; readonly version: string };
|
|
74
|
+
readonly tools: readonly McpTool<Ctx>[];
|
|
75
|
+
// The sentence for a tool this peer does not have, in the peer's own noun.
|
|
76
|
+
readonly noSuchTool: (name: string) => string;
|
|
77
|
+
// Whether a thrown error is this peer's own refusal (a switch that is off, a site that is not granted) as
|
|
78
|
+
// opposed to a tool that failed: the audit line says which.
|
|
79
|
+
readonly refused: (error: unknown) => boolean;
|
|
80
|
+
readonly errorMessage: (error: unknown) => string;
|
|
81
|
+
// Every call, accepted or refused, once it has an outcome. Best-effort: a log that cannot be written must
|
|
82
|
+
// never fail the answer, so a rejection here is swallowed.
|
|
83
|
+
readonly audit: (entry: McpAuditEntry) => Promise<void> | void;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const isRecord = (value: unknown): value is Record<string, unknown> => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
87
|
+
|
|
88
|
+
// Handle one JSON-RPC message. Returns the response, or undefined for a notification (nothing to answer). `ctx`
|
|
89
|
+
// is read per call by the tool that runs, so a grant pushed mid-session takes effect on the very next call.
|
|
90
|
+
export const createMcpServer = <Ctx>(spec: McpServerSpec<Ctx>): ((message: unknown, ctx: Ctx) => Promise<Record<string, unknown> | undefined>) => {
|
|
91
|
+
const byName = new Map(spec.tools.map((entry) => [entry.name, entry]));
|
|
92
|
+
const listing = spec.tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema }));
|
|
93
|
+
const audited = async (entry: McpAuditEntry): Promise<void> => {
|
|
94
|
+
try {
|
|
95
|
+
await spec.audit(entry);
|
|
96
|
+
} catch {
|
|
97
|
+
// Deliberately silent: a record for a human, never a control.
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const callTool = async (name: string, args: Record<string, unknown>, ctx: Ctx): Promise<Record<string, unknown>> => {
|
|
102
|
+
const found = byName.get(name);
|
|
103
|
+
if (found === undefined) {
|
|
104
|
+
return textResult(spec.noSuchTool(name), true);
|
|
105
|
+
}
|
|
106
|
+
try {
|
|
107
|
+
const result = await found.call(args, ctx);
|
|
108
|
+
await audited({ tool: name, args, ok: result["isError"] !== true });
|
|
109
|
+
return result;
|
|
110
|
+
} catch (error) {
|
|
111
|
+
const message = spec.errorMessage(error);
|
|
112
|
+
await audited({ tool: name, args, ok: false, failure: { refused: spec.refused(error), message } });
|
|
113
|
+
return textResult(message, true);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
return async (message, ctx) => {
|
|
118
|
+
if (!isRecord(message)) {
|
|
119
|
+
return { jsonrpc: "2.0", id: null, error: { code: -32600, message: "invalid request" } };
|
|
120
|
+
}
|
|
121
|
+
const id = message["id"];
|
|
122
|
+
if (id === undefined) {
|
|
123
|
+
return undefined;
|
|
124
|
+
}
|
|
125
|
+
const method = message["method"];
|
|
126
|
+
const reply = (result: Record<string, unknown>): Record<string, unknown> => ({ jsonrpc: "2.0", id, result });
|
|
127
|
+
if (method === "initialize") {
|
|
128
|
+
return reply({ protocolVersion: MCP_PROTOCOL_VERSION, capabilities: { tools: {} }, serverInfo: spec.serverInfo() });
|
|
129
|
+
}
|
|
130
|
+
if (method === "ping") {
|
|
131
|
+
return reply({});
|
|
132
|
+
}
|
|
133
|
+
if (method === "tools/list") {
|
|
134
|
+
return reply({ tools: listing });
|
|
135
|
+
}
|
|
136
|
+
if (method === "tools/call") {
|
|
137
|
+
const params = isRecord(message["params"]) ? message["params"] : {};
|
|
138
|
+
const name = typeof params["name"] === "string" ? params["name"] : "";
|
|
139
|
+
const args = isRecord(params["arguments"]) ? params["arguments"] : {};
|
|
140
|
+
return reply(await callTool(name, args, ctx));
|
|
141
|
+
}
|
|
142
|
+
return { jsonrpc: "2.0", id, error: { code: -32601, message: `method "${String(method)}" is not supported` } };
|
|
143
|
+
};
|
|
144
|
+
};
|
package/src/plan-pools.ts
CHANGED
|
@@ -6,7 +6,7 @@ import type { AccountUsage, UsageWindow, WindowGates } from "./schemas/plan-limi
|
|
|
6
6
|
* call, is this Google fleet spent for Claude Opus, when does the pool that refused this turn reopen, what
|
|
7
7
|
* does the ring beside the composer measure. The pools a reading carries answer that only through their
|
|
8
8
|
* `gates` (UsageWindowSchema says why the reader decides them), and this file is the one place the gate is
|
|
9
|
-
* read, so the daemon's account picker, its
|
|
9
|
+
* read, so the daemon's account picker, its one-shot helper walk, its refusal dressing and the browser's rings,
|
|
10
10
|
* rail and picker rows all agree about which pool is binding for a given model.
|
|
11
11
|
*
|
|
12
12
|
* WITHOUT A MODEL the answer is the account's own tightest pool, which is what a roster or a rail that has not
|
|
@@ -88,7 +88,7 @@ test("plan mode is a request to think, so it is never answered by the model that
|
|
|
88
88
|
});
|
|
89
89
|
|
|
90
90
|
test("a surface-started run is never downgraded, because nobody is watching it fail", () => {
|
|
91
|
-
// Same call
|
|
91
|
+
// Same call a `run` role already makes in the other direction: a run billed whole, with a worktree in
|
|
92
92
|
// it, is not the place to spend a guess.
|
|
93
93
|
expect(tierOf(`what is a closure?`, { unattended: true })).toBe(`standard`);
|
|
94
94
|
});
|
package/src/prompt-complexity.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* The job is narrow on purpose: decide whether this turn could have run on the cheap rung of the provider the
|
|
4
4
|
* user is already on. Nothing here picks a model, nothing here reads a catalog, and nothing here calls
|
|
5
5
|
* anything. It is a pure function over the turn's own words and shape, so the daemon and the composer can both
|
|
6
|
-
* ask it and get the same answer, which is the same reason
|
|
6
|
+
* ask it and get the same answer, which is the same reason model-pins.ts lives in the contract rather than in
|
|
7
7
|
* either of them.
|
|
8
8
|
*
|
|
9
9
|
* IT CAN ONLY EVER ROUTE DOWN. The standard tier is not a setting: it is whatever the user already picked. So
|
|
@@ -213,7 +213,7 @@ const forcing = (input: ComplexityInput, text: string): ComplexityRule[] => {
|
|
|
213
213
|
rules.push("plan-mode");
|
|
214
214
|
}
|
|
215
215
|
/* An unattended run is billed whole and nobody is watching it fail. The settings this repo already ships
|
|
216
|
-
* make the same call in the other direction:
|
|
216
|
+
* make the same call in the other direction: a `run` role resolves to NOTHING when empty precisely
|
|
217
217
|
* because "nothing here can judge whether a job is worth the frontier tier". This file does judge, but not
|
|
218
218
|
* for the runs where a wrong guess costs a whole session with a worktree in it. */
|
|
219
219
|
if (input.unattended) {
|
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import { PROVIDER_BRAND_PATHS } from "@intentic/constants";
|
|
2
2
|
import { describe, expect, test } from "vitest";
|
|
3
3
|
import { accessFor, capabilitiesOf, harnessChoosable, PROVIDER_ACCESS, PROVIDER_VENDOR, PROVIDERS, providerLabel } from "./agent-catalog.js";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
ACCESS_COST,
|
|
6
|
+
cliProxyIdOf,
|
|
7
|
+
MINTED_PROVIDERS,
|
|
8
|
+
mintedVariant,
|
|
9
|
+
mintedVariants,
|
|
10
|
+
NATIVE_PROVIDERS,
|
|
11
|
+
PROVIDER_SPECS,
|
|
12
|
+
providerSpec,
|
|
13
|
+
} from "./provider-specs.js";
|
|
5
14
|
import { NativeProviderParamSchema } from "./schemas/agent.js";
|
|
6
15
|
import { TranslatorAccountsSchema } from "./schemas/plan-limits.js";
|
|
7
16
|
import { KeyedProviderSchema } from "./schemas/provider-subscriptions.js";
|
|
@@ -44,22 +53,39 @@ describe("every provider in the table", () => {
|
|
|
44
53
|
expect(Object.keys(TranslatorAccountsSchema.shape), `${id} has no slot in TranslatorAccounts`).toContain(id);
|
|
45
54
|
return;
|
|
46
55
|
}
|
|
47
|
-
if (spec.auth.kind === "
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
if (spec.auth.kind === "minted") {
|
|
57
|
+
expect(MINTED_PROVIDERS, `${id} is minted but missing from MINTED_PROVIDERS`).toContain(id);
|
|
58
|
+
// At least one estate to sign in to, because the head of this list is what a `login/start` that names
|
|
59
|
+
// no variant gets: an empty one is a provider whose connect row has no button that can do anything.
|
|
60
|
+
expect(spec.auth.variants.length, `${id} is minted but offers no estate to sign in to`).toBeGreaterThan(0);
|
|
61
|
+
for (const variant of spec.auth.variants) {
|
|
62
|
+
/* THE TURN URL AND THE CATALOG URL ARE DIFFERENT SURFACES OF ONE ESTATE, and both halves of that
|
|
63
|
+
* sentence are asserted: same host (a pair that drifted apart would send the catalog to one
|
|
64
|
+
* vendor and the turn to another), and the turn's base carries NO version segment, because the
|
|
65
|
+
* harness appends `/v1/messages` itself and a doubled one is a 404 mid-conversation. */
|
|
66
|
+
expect(new URL(variant.anthropicBase).host, `${id}/${variant.id}'s catalog and turn hosts differ`).toBe(
|
|
67
|
+
new URL(variant.catalogBase).host,
|
|
68
|
+
);
|
|
69
|
+
expect(variant.anthropicBase, `${id}/${variant.id}'s turn base carries a version segment the harness would double`).not.toMatch(
|
|
70
|
+
/\/v\d+$/,
|
|
71
|
+
);
|
|
72
|
+
expect(variant.label.trim(), `${id}/${variant.id} has no estate label`).not.toBe("");
|
|
73
|
+
// The lookup every surface goes through, rather than the row behind it: an id that names a
|
|
74
|
+
// variant must resolve to that variant and nothing else.
|
|
75
|
+
expect(mintedVariant(id, variant.id), `${id}/${variant.id} does not resolve`).toEqual(variant);
|
|
76
|
+
}
|
|
77
|
+
// An absent variant takes the head of the list, which is what a single-estate provider's connect row
|
|
78
|
+
// always sends and what a `login/start` with no choice on it means.
|
|
79
|
+
expect(mintedVariant(id), `${id}'s default estate is not the head of its list`).toEqual(spec.auth.variants[0]);
|
|
80
|
+
// An estate id nobody declared is NOT the default: falling back would mint a key on one estate and
|
|
81
|
+
// dial it at another, and report the refusal as an authentication problem.
|
|
82
|
+
expect(mintedVariant(id, "no-such-estate"), `${id} defaults an unknown estate instead of refusing`).toBeUndefined();
|
|
58
83
|
return;
|
|
59
84
|
}
|
|
60
85
|
// An oauth provider is served by a handshake this daemon runs; it is in neither routed list.
|
|
61
86
|
expect(KeyedProviderSchema.options, `${id} is not routed but appears in KeyedProvider`).not.toContain(id);
|
|
62
|
-
expect(
|
|
87
|
+
expect(MINTED_PROVIDERS, `${id} is not minted but appears in MINTED_PROVIDERS`).not.toContain(id);
|
|
88
|
+
expect(mintedVariants(id), `${id} is not minted but names estates`).toBeUndefined();
|
|
63
89
|
});
|
|
64
90
|
|
|
65
91
|
test.each(PROVIDER_SPECS.map((spec) => ({ id: spec.id })))("$id is in every derived list", ({ id }) => {
|
|
@@ -89,7 +115,7 @@ test("provider ids are unique", () => {
|
|
|
89
115
|
|
|
90
116
|
/* An id may contain neither a slash nor a colon, and both exclusions are load-bearing rather than tidy.
|
|
91
117
|
* `endpoint/<id>` uses the slash to namespace a capability-minted provider, and the picker's pinned selections
|
|
92
|
-
* are `${provider}:${model}` split on the FIRST colon (
|
|
118
|
+
* are `${provider}:${model}` split on the FIRST colon (model-pins.ts), so an id carrying either would parse as
|
|
93
119
|
* something else entirely, silently. */
|
|
94
120
|
test("no provider id can be mistaken for an endpoint or a pinned selection", () => {
|
|
95
121
|
for (const spec of PROVIDER_SPECS) {
|
|
@@ -109,11 +135,11 @@ test("the harness chip is offered exactly where the two harnesses run different
|
|
|
109
135
|
}
|
|
110
136
|
});
|
|
111
137
|
|
|
112
|
-
/* A
|
|
138
|
+
/* A MINTED PROVIDER HAS NO NATIVE RUNTIME, by construction rather than by coincidence: it is reached by pointing
|
|
113
139
|
* the Claude Code loop at the vendor's own Anthropic endpoint, and there is no second loop to point anywhere
|
|
114
140
|
* else. A spec row that claimed one would produce a picker chip offering a runtime nothing serves. */
|
|
115
|
-
test("a
|
|
116
|
-
for (const provider of
|
|
141
|
+
test("a minted provider runs the Claude Code loop on both harnesses", () => {
|
|
142
|
+
for (const provider of MINTED_PROVIDERS) {
|
|
117
143
|
expect(capabilitiesOf(provider, "native").runtime, provider).toBe("claude-code");
|
|
118
144
|
expect(capabilitiesOf(provider, "claude-code").runtime, provider).toBe("claude-code");
|
|
119
145
|
}
|
|
@@ -125,6 +151,7 @@ test("an id that is not a provider resolves to nothing", () => {
|
|
|
125
151
|
for (const id of ["", "some-installed-agent", "endpoint/ollama", "META", "z.ai"]) {
|
|
126
152
|
expect(providerSpec(id), id).toBeUndefined();
|
|
127
153
|
expect(cliProxyIdOf(id), id).toBeUndefined();
|
|
128
|
-
expect(
|
|
154
|
+
expect(mintedVariants(id), id).toBeUndefined();
|
|
155
|
+
expect(mintedVariant(id), id).toBeUndefined();
|
|
129
156
|
}
|
|
130
157
|
});
|