@actuarial-ts/agents 0.2.0 → 0.3.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 +4 -2
- package/dist/advisor.d.ts +2 -1
- package/dist/advisor.d.ts.map +1 -1
- package/dist/advisor.js +3 -1
- package/dist/advisor.js.map +1 -1
- package/dist/divergence.d.ts.map +1 -1
- package/dist/divergence.js +6 -1
- package/dist/divergence.js.map +1 -1
- package/dist/judgment.d.ts +26 -0
- package/dist/judgment.d.ts.map +1 -1
- package/dist/judgment.js +21 -0
- package/dist/judgment.js.map +1 -1
- package/dist/mcp.d.ts +44 -9
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +57 -40
- package/dist/mcp.js.map +1 -1
- package/dist/promotion.d.ts +26 -0
- package/dist/promotion.d.ts.map +1 -1
- package/dist/promotion.js +49 -1
- package/dist/promotion.js.map +1 -1
- package/dist/remote.d.ts.map +1 -1
- package/dist/remote.js +11 -1
- package/dist/remote.js.map +1 -1
- package/dist/tools.d.ts +87 -6
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +175 -15
- package/dist/tools.js.map +1 -1
- package/package.json +7 -5
- package/src/advisor.ts +141 -0
- package/src/divergence.ts +642 -0
- package/src/errors.ts +62 -0
- package/src/evals.ts +162 -0
- package/src/index.ts +9 -0
- package/src/judgment.ts +458 -0
- package/src/mcp.ts +274 -0
- package/src/promotion.ts +1240 -0
- package/src/remote.ts +371 -0
- package/src/tools.ts +561 -0
package/src/mcp.ts
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP tenant seam + boot self-test for the governed workspace.
|
|
3
|
+
*
|
|
4
|
+
* When the workspace is exposed over the Model Context Protocol, external AI
|
|
5
|
+
* clients read everything and mutate nothing directly. The one hard guarantee
|
|
6
|
+
* is that EVERY exposed tool resolves its tenant (project id) from the MCP
|
|
7
|
+
* request's auth info, never from the model — the same secureToolWrapper rule
|
|
8
|
+
* the ActNG server proved in production, restated for the MCP transport.
|
|
9
|
+
*
|
|
10
|
+
* @mastra/mcp gives no built-in per-tool authorization outside its EE FGA path,
|
|
11
|
+
* so a missed wire-up FAILS OPEN: a tool that forgets to read authInfo would
|
|
12
|
+
* happily serve an unauthenticated caller. These two helpers close that caveat:
|
|
13
|
+
*
|
|
14
|
+
* 1. requireMcpTenant — the read side. A tool calls it instead of trusting the
|
|
15
|
+
* model; absent auth info it THROWS AgentsError("NO_TENANT_CONTEXT"), which
|
|
16
|
+
* the defineActuarialTool wrapper turns into a { success:false } envelope.
|
|
17
|
+
* A tool built on it fails CLOSED.
|
|
18
|
+
*
|
|
19
|
+
* 2. assertFailClosed — the proof. At server startup, drive a probe read tool
|
|
20
|
+
* through the server WITHOUT auth and assert it fails closed. If the probe
|
|
21
|
+
* SUCCEEDS, the seam is not wired up: this throws MCP_SELF_TEST_FAILED and
|
|
22
|
+
* startup MUST abort.
|
|
23
|
+
*
|
|
24
|
+
* ---------------------------------------------------------------------------
|
|
25
|
+
* VERIFIED against the installed @mastra/mcp 1.14.0 (house rule — types and
|
|
26
|
+
* compiled source, not memory):
|
|
27
|
+
*
|
|
28
|
+
* - MCPServer.executeTool(toolId, args, executionContext?: { messages?,
|
|
29
|
+
* toolCallId?, requestContext? }): Promise<any> returns the tool's execute
|
|
30
|
+
* result VERBATIM (dist/index.js: `const result = await tool.execute(...);
|
|
31
|
+
* return result;`). For a defineActuarialTool with no outputSchema the core
|
|
32
|
+
* tool builder passes the value through untouched, so an executeTool caller
|
|
33
|
+
* sees exactly the { success:false, error:{ code } } envelope the wrapper
|
|
34
|
+
* produced. Argument validation runs FIRST and, on failure, short-circuits
|
|
35
|
+
* to a { error:true, message } object before the tool ever executes — hence
|
|
36
|
+
* assertFailClosed passes minimal VALID probe args.
|
|
37
|
+
*
|
|
38
|
+
* - Auth-context access path. On the real streamable-HTTP call path the SDK
|
|
39
|
+
* copies the transport `extra` (with `authInfo` from `req.auth`) into the
|
|
40
|
+
* tool context TWO ways: directly at `context.mcp.extra`, and — via
|
|
41
|
+
* createProxiedRequestContext — as INDIVIDUAL keys set on a fresh
|
|
42
|
+
* RequestContext (`context.requestContext.get("authInfo")`). NOTE the
|
|
43
|
+
* surprise: the installed 1.14.0 sets each extra key on the RequestContext
|
|
44
|
+
* verbatim, so the tenant lives at `requestContext.get("authInfo")`, NOT
|
|
45
|
+
* under a single `"mcp.extra"` key as the research draft documented. This
|
|
46
|
+
* helper tries all three shapes so it is correct against both the installed
|
|
47
|
+
* package and the documented pattern.
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
import type { ActuarialToolContext, ToolEnvelopeFailure } from "./tools.js";
|
|
51
|
+
import { resolveTenant } from "./tools.js";
|
|
52
|
+
import { AgentsError, type AgentsErrorCode } from "./errors.js";
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Tenant seam (read side)
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The MCP auth-info bag: whatever the host's bearer-token middleware set on
|
|
59
|
+
* `req.auth` before delegating to startHTTP (e.g. `{ projectId }`). Values are
|
|
60
|
+
* unknown — requireMcpTenant proves the tenant is a non-empty string.
|
|
61
|
+
*/
|
|
62
|
+
export interface McpAuthInfo {
|
|
63
|
+
[key: string]: unknown;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The transport `extra` (MCP SDK RequestHandlerExtra) surfaced to a tool at
|
|
68
|
+
* `context.mcp.extra`. Only `authInfo` is load-bearing here; the rest
|
|
69
|
+
* (sessionId, requestInfo, signal, ...) is carried opaquely.
|
|
70
|
+
*/
|
|
71
|
+
export interface McpRequestExtra {
|
|
72
|
+
authInfo?: McpAuthInfo;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The structural slice of a Mastra tool-execution context this seam needs on
|
|
78
|
+
* the MCP path: the tenant-seam `requestContext` (inherited from
|
|
79
|
+
* ActuarialToolContext) plus the MCP-specific `mcp.extra`. Typed structurally
|
|
80
|
+
* so tests exercise the helper with a plain object and no live MCP server.
|
|
81
|
+
*/
|
|
82
|
+
export interface McpToolContext extends ActuarialToolContext {
|
|
83
|
+
mcp?: { extra?: McpRequestExtra };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Reads the tenant id (default key "projectId") from the MCP execution
|
|
90
|
+
* context's auth info — the server-set identity, never the model. THROWS a
|
|
91
|
+
* typed AgentsError("NO_TENANT_CONTEXT") when the auth info or the key is
|
|
92
|
+
* absent, non-string, or empty. Inside a defineActuarialTool execute the
|
|
93
|
+
* wrapper converts that throw into a { success:false } envelope, so a tool
|
|
94
|
+
* built on requireMcpTenant fails CLOSED for any unauthenticated MCP caller.
|
|
95
|
+
*/
|
|
96
|
+
export function requireMcpTenant(context: McpToolContext | undefined, key = "projectId"): string {
|
|
97
|
+
// One seam, one reader: this is resolveTenant with the MCP source pinned.
|
|
98
|
+
// Prefer declaring `tenant: "required", tenantSource: "mcp-auth"` on the
|
|
99
|
+
// tool itself, which makes the wrapper do this before the body runs.
|
|
100
|
+
return resolveTenant(context, { source: "mcp-auth", key });
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ---------------------------------------------------------------------------
|
|
104
|
+
// Boot self-test (proof side)
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* The structural slice of @mastra/mcp's MCPServer that assertFailClosed drives.
|
|
108
|
+
* A real MCPServer satisfies it; so does a bare `{ executeTool }` stub, so the
|
|
109
|
+
* self-test is unit-testable without booting a transport. Verified against the
|
|
110
|
+
* installed executeTool signature (see the file header).
|
|
111
|
+
*/
|
|
112
|
+
export interface McpToolServer {
|
|
113
|
+
executeTool(
|
|
114
|
+
toolId: string,
|
|
115
|
+
args: Record<string, unknown>,
|
|
116
|
+
executionContext?: { requestContext?: unknown; messages?: unknown[]; toolCallId?: string },
|
|
117
|
+
): Promise<unknown>;
|
|
118
|
+
/**
|
|
119
|
+
* Tool enumeration, present on the installed @mastra/mcp MCPServer. The
|
|
120
|
+
* DECLARATION (server.d.ts) types entries by `name` and allows a Promise
|
|
121
|
+
* return; the runtime also carries `id`. Typed from the declaration — a
|
|
122
|
+
* runtime probe is how the first draft of this signature broke assignability.
|
|
123
|
+
* Optional so a bare `{ executeTool }` stub still satisfies the type; the
|
|
124
|
+
* self-test's probe-everything form requires it and says so when absent.
|
|
125
|
+
*/
|
|
126
|
+
getToolListInfo?():
|
|
127
|
+
| { tools: Array<{ name: string; id?: string }> }
|
|
128
|
+
| Promise<{ tools: Array<{ name: string; id?: string }> }>;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export interface AssertFailClosedOptions {
|
|
132
|
+
/** The MCP server whose exposed tools are being wired up (typically the workspace MCPServer). */
|
|
133
|
+
server: McpToolServer;
|
|
134
|
+
/**
|
|
135
|
+
* Probe ONE tool instead of every tool. Prefer omitting this: the
|
|
136
|
+
* single-tool form proved exactly one wire-up, and a sibling tool that
|
|
137
|
+
* skipped the tenant seam sailed through boot while serving every tenant's
|
|
138
|
+
* data to unauthenticated callers.
|
|
139
|
+
*/
|
|
140
|
+
probeToolId?: string;
|
|
141
|
+
/**
|
|
142
|
+
* Minimal VALID args for the single-tool form. Defaults to `{}`
|
|
143
|
+
* (correct for read tools whose schema is `z.object({})` or all-optional).
|
|
144
|
+
* Args that fail schema validation would short-circuit before the tenant
|
|
145
|
+
* check runs, so pass real minimal args for probes that require input.
|
|
146
|
+
*/
|
|
147
|
+
probeArgs?: Record<string, unknown>;
|
|
148
|
+
/**
|
|
149
|
+
* Per-tool minimal valid args for the probe-everything form, keyed by tool
|
|
150
|
+
* id. Any tool not listed is driven with `{}`.
|
|
151
|
+
*/
|
|
152
|
+
argsByTool?: Record<string, Record<string, unknown>>;
|
|
153
|
+
/**
|
|
154
|
+
* Tools that are tenant-free BY DESIGN and therefore expected to succeed
|
|
155
|
+
* without auth. The same contract as `tenant: "none"` on the definition:
|
|
156
|
+
* greppable, deliberate, reviewable. A name listed here that is not on the
|
|
157
|
+
* server fails the self-test — a stale exemption is how the next leak hides.
|
|
158
|
+
*/
|
|
159
|
+
exempt?: string[];
|
|
160
|
+
/** The failure code every probed tool must fail closed with. Defaults to NO_TENANT_CONTEXT. */
|
|
161
|
+
expectedErrorCode?: AgentsErrorCode;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Structural check for the { success:false, error:{ code } } tool-failure envelope. */
|
|
165
|
+
function isFailureEnvelope(result: unknown): result is ToolEnvelopeFailure {
|
|
166
|
+
if (typeof result !== "object" || result === null) return false;
|
|
167
|
+
if ((result as { success?: unknown }).success !== false) return false;
|
|
168
|
+
const error = (result as { error?: { code?: unknown } }).error;
|
|
169
|
+
return typeof error?.code === "string";
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** A short, log-safe description of an unexpected probe result for the abort message. */
|
|
173
|
+
function describeResult(result: unknown): string {
|
|
174
|
+
if (isFailureEnvelope(result)) {
|
|
175
|
+
return `a { success: false } envelope with code "${result.error.code}"`;
|
|
176
|
+
}
|
|
177
|
+
if (typeof result === "object" && result !== null) {
|
|
178
|
+
if ((result as { error?: unknown }).error === true) {
|
|
179
|
+
return "an argument-validation error (the probe never reached the tenant check — pass valid probeArgs)";
|
|
180
|
+
}
|
|
181
|
+
if ((result as { success?: unknown }).success === true) {
|
|
182
|
+
return "a { success: true } result (the probe SUCCEEDED without tenant context — the seam is not wired up)";
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return "a non-envelope result (the probe did not fail closed on tenant context)";
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The boot self-test. Drives EVERY tool on the server WITHOUT any auth info
|
|
190
|
+
* and asserts each fails closed with the tenant error code, reporting every
|
|
191
|
+
* hole at once. THROWS AgentsError("MCP_SELF_TEST_FAILED") loudly for any
|
|
192
|
+
* other outcome — most importantly if a probe SUCCEEDED, which means that tool
|
|
193
|
+
* did not require tenant context and the MCP tenant seam is a fail-open hole.
|
|
194
|
+
* Tenant-free-by-design tools are excused only via the greppable `exempt`
|
|
195
|
+
* list; a single-tool form remains for targeted re-checks.
|
|
196
|
+
*
|
|
197
|
+
* Call this at server startup whenever MCP is enabled, and ABORT startup if it
|
|
198
|
+
* throws: a governed workspace must never accept unauthenticated MCP clients.
|
|
199
|
+
*/
|
|
200
|
+
export async function assertFailClosed(options: AssertFailClosedOptions): Promise<void> {
|
|
201
|
+
const { server, probeToolId, probeArgs = {}, expectedErrorCode = "NO_TENANT_CONTEXT" } = options;
|
|
202
|
+
|
|
203
|
+
// Single-tool form: kept for hosts that need a targeted re-check, but the
|
|
204
|
+
// probe-everything form below is the one to call at boot.
|
|
205
|
+
if (probeToolId !== undefined) {
|
|
206
|
+
await probeOne(server, probeToolId, probeArgs, expectedErrorCode);
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
if (typeof server.getToolListInfo !== "function") {
|
|
211
|
+
throw new AgentsError(
|
|
212
|
+
"MCP_SELF_TEST_FAILED",
|
|
213
|
+
"MCP boot self-test cannot enumerate this server's tools (no getToolListInfo); " +
|
|
214
|
+
"pass probeToolId per tool, or upgrade @mastra/mcp. Refusing to certify a server " +
|
|
215
|
+
"whose tool list cannot be inspected.",
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const listing = await server.getToolListInfo();
|
|
220
|
+
const toolIds = listing.tools.map((tool) => tool.id ?? tool.name);
|
|
221
|
+
const exempt = new Set(options.exempt ?? []);
|
|
222
|
+
const failures: string[] = [];
|
|
223
|
+
|
|
224
|
+
// A stale exemption is a future hole: the tool it excused is gone, and the
|
|
225
|
+
// next tool to take that name inherits a free pass nobody remembers granting.
|
|
226
|
+
for (const name of exempt) {
|
|
227
|
+
if (!toolIds.includes(name)) {
|
|
228
|
+
failures.push(`"${name}" is exempted but not on the server; remove the stale exemption`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const toolId of toolIds) {
|
|
233
|
+
if (exempt.has(toolId)) continue;
|
|
234
|
+
try {
|
|
235
|
+
await probeOne(server, toolId, options.argsByTool?.[toolId] ?? {}, expectedErrorCode);
|
|
236
|
+
} catch (err) {
|
|
237
|
+
// Collect EVERY hole before reporting: a boot test that stops at the
|
|
238
|
+
// first one hides the second.
|
|
239
|
+
failures.push(err instanceof AgentsError ? err.message : String(err));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (failures.length > 0) {
|
|
244
|
+
throw new AgentsError(
|
|
245
|
+
"MCP_SELF_TEST_FAILED",
|
|
246
|
+
`MCP boot self-test failed on ${failures.length} of ${toolIds.length} tool(s):\n` +
|
|
247
|
+
failures.map((message) => ` - ${message}`).join("\n") +
|
|
248
|
+
"\nAbort startup: a governed workspace must never accept unauthenticated MCP clients.",
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Drives one tool with no auth info; it must fail closed with the expected code. */
|
|
254
|
+
async function probeOne(
|
|
255
|
+
server: McpToolServer,
|
|
256
|
+
toolId: string,
|
|
257
|
+
args: Record<string, unknown>,
|
|
258
|
+
expectedErrorCode: AgentsErrorCode,
|
|
259
|
+
): Promise<void> {
|
|
260
|
+
// No requestContext, no mcp.extra -> the probe sees no auth info at all.
|
|
261
|
+
const result = await server.executeTool(toolId, args, {});
|
|
262
|
+
|
|
263
|
+
if (isFailureEnvelope(result) && result.error.code === expectedErrorCode) {
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
throw new AgentsError(
|
|
268
|
+
"MCP_SELF_TEST_FAILED",
|
|
269
|
+
`probe tool "${toolId}" did not fail closed without auth. ` +
|
|
270
|
+
`Expected a { success: false, error.code: "${expectedErrorCode}" } envelope but got ${describeResult(result)}. ` +
|
|
271
|
+
`An MCP-exposed tool must either enforce the tenant seam (tenant: "required", tenantSource: "mcp-auth") ` +
|
|
272
|
+
"or be a declared exemption; this outcome means it is neither.",
|
|
273
|
+
);
|
|
274
|
+
}
|