@effect-agent/testing 0.1.0-beta.9 → 0.1.0-beta.90
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/dist/Certification.d.mts +106 -0
- package/dist/Certification.mjs +633 -0
- package/dist/Certification.mjs.map +1 -0
- package/dist/Chaos.d.mts +126 -0
- package/dist/Chaos.mjs +755 -0
- package/dist/Chaos.mjs.map +1 -0
- package/dist/CodeExecutorConformance.d.mts +33 -0
- package/dist/CodeExecutorConformance.mjs +231 -0
- package/dist/CodeExecutorConformance.mjs.map +1 -0
- package/dist/CodeExecutorSubstitute.d.mts +21 -0
- package/dist/CodeExecutorSubstitute.mjs +368 -0
- package/dist/CodeExecutorSubstitute.mjs.map +1 -0
- package/dist/DocsResearcher.d.mts +276 -0
- package/dist/DocsResearcher.mjs +490 -0
- package/dist/DocsResearcher.mjs.map +1 -0
- package/dist/ScriptedModel-DAvxIiud.d.mts +220 -0
- package/dist/ScriptedModel.d.mts +2 -0
- package/dist/ScriptedModel.mjs +155 -0
- package/dist/ScriptedModel.mjs.map +1 -0
- package/dist/TravelPlanner.d.mts +1680 -0
- package/dist/TravelPlanner.mjs +1963 -0
- package/dist/TravelPlanner.mjs.map +1 -0
- package/dist/deterministic-layers-D5owIoke.mjs +358 -0
- package/dist/deterministic-layers-D5owIoke.mjs.map +1 -0
- package/dist/index.d.mts +2 -3408
- package/dist/index.mjs +2 -4771
- package/dist/rolldown-runtime-D7D4PA-g.mjs +13 -0
- package/package.json +1 -48
- package/src/{certification.ts → Certification.ts} +366 -171
- package/src/{chaos.ts → Chaos.ts} +241 -127
- package/src/{code-executor-conformance.ts → CodeExecutorConformance.ts} +30 -7
- package/src/{code-executor-substitute.ts → CodeExecutorSubstitute.ts} +113 -46
- package/src/{fixtures/docs-researcher/index.ts → DocsResearcher.ts} +68 -5
- package/src/{scripted-model.ts → ScriptedModel.ts} +25 -29
- package/src/TravelPlanner.ts +232 -0
- package/src/fixtures/docs-researcher/definition.ts +20 -11
- package/src/fixtures/docs-researcher/harness.ts +41 -34
- package/src/fixtures/docs-researcher/mcp.ts +49 -3
- package/src/fixtures/travel-planner/definition.ts +15 -2
- package/src/fixtures/travel-planner/deterministic-layers.ts +47 -4
- package/src/fixtures/travel-planner/phase2.ts +4 -3
- package/src/fixtures/travel-planner/phase3.ts +19 -42
- package/src/fixtures/travel-planner/phase4.ts +24 -37
- package/src/fixtures/travel-planner/phase5.ts +35 -42
- package/src/fixtures/travel-planner/phase6.ts +191 -88
- package/src/fixtures/travel-planner/phase7.ts +4 -102
- package/src/fixtures/travel-planner/scenarios.ts +3 -4
- package/src/fixtures/travel-planner/subagents-durable.ts +35 -61
- package/src/fixtures/travel-planner/subagents.ts +36 -14
- package/src/index.ts +1 -11
- package/src/internal/certification-report.ts +25 -0
- package/dist/index.mjs.map +0 -1
- package/src/code-executor-conformance.d.ts +0 -30
- package/src/fixtures/travel-planner/index.ts +0 -11
- package/src/fixtures/warehouse/index.ts +0 -412
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { CodeExecutor, SandboxImplementation } from "@effect-agent/sandbox";
|
|
2
|
-
import { Effect, Schema } from "effect";
|
|
3
|
-
declare const CodeExecutorConformanceViolation_base: Schema.Class<CodeExecutorConformanceViolation, Schema.TaggedStruct<"CodeExecutorConformanceViolation", {
|
|
4
|
-
readonly caseName: Schema.String;
|
|
5
|
-
readonly message: Schema.String;
|
|
6
|
-
}>, import("effect/Cause").YieldableError>;
|
|
7
|
-
/**
|
|
8
|
-
* Shared `CodeExecutor` conformance (TEST-015). Every adapter — the
|
|
9
|
-
* deterministic `unisolated` substitute and each isolated adapter — runs
|
|
10
|
-
* `codeExecutorConformanceCases` verbatim. Enforcement cases that only genuine
|
|
11
|
-
* isolation can prove (ambient network denial, synchronous CPU runaway
|
|
12
|
-
* termination) are NOT here; they belong to isolated adapters only
|
|
13
|
-
* (testing spec §8.1).
|
|
14
|
-
*
|
|
15
|
-
* Cases assume the live `Clock` (the wall-clock case uses a short real
|
|
16
|
-
* deadline) and take one fresh executor pass per case, so a suite may share
|
|
17
|
-
* one executor Layer across cases.
|
|
18
|
-
*/
|
|
19
|
-
export declare class CodeExecutorConformanceViolation extends CodeExecutorConformanceViolation_base {
|
|
20
|
-
}
|
|
21
|
-
export interface CodeExecutorConformanceCase {
|
|
22
|
-
readonly name: string;
|
|
23
|
-
readonly run: Effect.Effect<void, CodeExecutorConformanceViolation, CodeExecutor>;
|
|
24
|
-
}
|
|
25
|
-
export interface CodeExecutorConformanceOptions {
|
|
26
|
-
/** The posture the adapter under test must stamp on results and errors. */
|
|
27
|
-
readonly implementation: SandboxImplementation;
|
|
28
|
-
}
|
|
29
|
-
export declare const codeExecutorConformanceCases: (options: CodeExecutorConformanceOptions) => ReadonlyArray<CodeExecutorConformanceCase>;
|
|
30
|
-
export {};
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
export * from "./definition.ts";
|
|
2
|
-
export * from "./deterministic-layers.ts";
|
|
3
|
-
export * from "./phase2.ts";
|
|
4
|
-
export * from "./phase3.ts";
|
|
5
|
-
export * from "./phase4.ts";
|
|
6
|
-
export * from "./phase5.ts";
|
|
7
|
-
export * from "./phase6.ts";
|
|
8
|
-
export * from "./phase7.ts";
|
|
9
|
-
export * from "./scenarios.ts";
|
|
10
|
-
export * from "./subagents.ts";
|
|
11
|
-
export * from "./subagents-durable.ts";
|
|
@@ -1,412 +0,0 @@
|
|
|
1
|
-
import { ToolExecutionClass } from "@effect-agent/engine";
|
|
2
|
-
import { SqliteClient } from "@effect/sql-sqlite-node";
|
|
3
|
-
import { Context, Duration, Effect, Layer, Option, Predicate, Schema } from "effect";
|
|
4
|
-
import { Tool, Toolkit } from "effect/unstable/ai";
|
|
5
|
-
import * as SqlClient from "effect/unstable/sql/SqlClient";
|
|
6
|
-
import { SqlError } from "effect/unstable/sql/SqlError";
|
|
7
|
-
|
|
8
|
-
/**
|
|
9
|
-
* The C3 read-only SQL reference integration (plan §8, SEC-015): application
|
|
10
|
-
* code built from native Effect AI primitives proving that generated Code
|
|
11
|
-
* Mode programs can query an internal service without the executor ever
|
|
12
|
-
* seeing a connection, credential, or network authority.
|
|
13
|
-
*
|
|
14
|
-
* The guarantee is database authority and data topology, not SQL text
|
|
15
|
-
* inspection: the fixture materializes a curated, tenant-scoped copy of the
|
|
16
|
-
* warehouse into a private in-memory SQLite database (cross-tenant rows are
|
|
17
|
-
* physically absent), then locks the connection with `PRAGMA query_only = ON`
|
|
18
|
-
* so the engine itself denies every write. The keyword scanner underneath is
|
|
19
|
-
* defense in depth for the escape hatches `query_only` cannot close (PRAGMA
|
|
20
|
-
* could re-enable writes; ATTACH reaches the filesystem) — it is never the
|
|
21
|
-
* primary boundary.
|
|
22
|
-
*
|
|
23
|
-
* Statement timeout: the synchronous SQLite driver cannot cancel a running
|
|
24
|
-
* statement, so a `statementTimeout` request is rejected typed at Layer
|
|
25
|
-
* construction rather than silently unenforced (CAP-015 posture). Wall-clock
|
|
26
|
-
* bounding of a whole pass belongs to the Run duration budget and, in Code
|
|
27
|
-
* Mode, the executor's own deadline.
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
const BoundedSqlText = Schema.NonEmptyString.check(Schema.isMaxLength(16 * 1024));
|
|
31
|
-
const SqlParameter = Schema.Union([Schema.String, Schema.Number, Schema.Boolean, Schema.Null]);
|
|
32
|
-
|
|
33
|
-
/** One curated multi-tenant seed row; only the configured tenant's rows are materialized. */
|
|
34
|
-
export class WarehouseSeedRow extends Schema.Class<WarehouseSeedRow>(
|
|
35
|
-
"@effect-agent/testing/WarehouseSeedRow",
|
|
36
|
-
)({
|
|
37
|
-
tenant: Schema.NonEmptyString,
|
|
38
|
-
customer: Schema.NonEmptyString,
|
|
39
|
-
region: Schema.NonEmptyString,
|
|
40
|
-
revenue: Schema.Int,
|
|
41
|
-
createdAt: Schema.NonEmptyString,
|
|
42
|
-
}) {}
|
|
43
|
-
|
|
44
|
-
export class WarehouseLimits extends Schema.Class<WarehouseLimits>(
|
|
45
|
-
"@effect-agent/testing/WarehouseLimits",
|
|
46
|
-
)({
|
|
47
|
-
maxRows: Schema.Int.check(Schema.isGreaterThan(0), Schema.isLessThanOrEqualTo(10_000)),
|
|
48
|
-
maxResultBytes: Schema.Int.check(
|
|
49
|
-
Schema.isGreaterThan(0),
|
|
50
|
-
Schema.isLessThanOrEqualTo(4 * 1024 * 1024),
|
|
51
|
-
),
|
|
52
|
-
}) {}
|
|
53
|
-
|
|
54
|
-
const defaultLimits = WarehouseLimits.make({ maxRows: 256, maxResultBytes: 256 * 1024 });
|
|
55
|
-
|
|
56
|
-
/** The query was denied before or by the read-only database authority. */
|
|
57
|
-
export class WarehouseQueryDenied extends Schema.TaggedError<WarehouseQueryDenied>()(
|
|
58
|
-
"WarehouseQueryDenied",
|
|
59
|
-
{
|
|
60
|
-
reason: Schema.Literals(["write-denied", "multi-statement", "denied-keyword"]),
|
|
61
|
-
message: Schema.String.check(Schema.isMaxLength(2 * 1024)),
|
|
62
|
-
},
|
|
63
|
-
) {}
|
|
64
|
-
|
|
65
|
-
/** The database rejected the query for an ordinary reason (syntax, missing view). */
|
|
66
|
-
export class WarehouseQueryFailed extends Schema.TaggedError<WarehouseQueryFailed>()(
|
|
67
|
-
"WarehouseQueryFailed",
|
|
68
|
-
{
|
|
69
|
-
message: Schema.String.check(Schema.isMaxLength(2 * 1024)),
|
|
70
|
-
},
|
|
71
|
-
) {}
|
|
72
|
-
|
|
73
|
-
/** The fixture cannot honestly enforce a requested policy (CAP-015). */
|
|
74
|
-
export class WarehouseConfigurationError extends Schema.TaggedError<WarehouseConfigurationError>()(
|
|
75
|
-
"WarehouseConfigurationError",
|
|
76
|
-
{
|
|
77
|
-
message: Schema.String,
|
|
78
|
-
},
|
|
79
|
-
) {}
|
|
80
|
-
|
|
81
|
-
export class WarehouseQuerySuccess extends Schema.Class<WarehouseQuerySuccess>(
|
|
82
|
-
"@effect-agent/testing/WarehouseQuerySuccess",
|
|
83
|
-
)({
|
|
84
|
-
columns: Schema.Array(Schema.String).check(Schema.isMaxLength(64)),
|
|
85
|
-
rows: Schema.Array(Schema.Record(Schema.String, Schema.Json)).check(Schema.isMaxLength(10_000)),
|
|
86
|
-
rowCount: Schema.Natural,
|
|
87
|
-
truncated: Schema.Boolean,
|
|
88
|
-
}) {}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* The read-only warehouse query service. Tenant identity is fixed at Layer
|
|
92
|
-
* construction by the host — never by model-controlled arguments (SEC-015).
|
|
93
|
-
*/
|
|
94
|
-
export class WarehouseDb extends Context.Service<
|
|
95
|
-
WarehouseDb,
|
|
96
|
-
{
|
|
97
|
-
readonly query: (
|
|
98
|
-
sql: string,
|
|
99
|
-
parameters: ReadonlyArray<string | number | boolean | null>,
|
|
100
|
-
) => Effect.Effect<WarehouseQuerySuccess, WarehouseQueryDenied | WarehouseQueryFailed>;
|
|
101
|
-
}
|
|
102
|
-
>()("@effect-agent/testing/WarehouseDb") {}
|
|
103
|
-
|
|
104
|
-
// ---------------------------------------------------------------------------
|
|
105
|
-
// Defense-in-depth SQL scanning: strips string literals and comments, then
|
|
106
|
-
// rejects multi-statements and the escape-hatch keywords the read-only
|
|
107
|
-
// database authority cannot police itself.
|
|
108
|
-
// ---------------------------------------------------------------------------
|
|
109
|
-
|
|
110
|
-
const stripLiteralsAndComments = (sql: string): string => {
|
|
111
|
-
let output = "";
|
|
112
|
-
let index = 0;
|
|
113
|
-
while (index < sql.length) {
|
|
114
|
-
const char = sql[index];
|
|
115
|
-
if (char === "'") {
|
|
116
|
-
index += 1;
|
|
117
|
-
while (index < sql.length) {
|
|
118
|
-
if (sql[index] === "'" && sql[index + 1] === "'") {
|
|
119
|
-
index += 2;
|
|
120
|
-
continue;
|
|
121
|
-
}
|
|
122
|
-
if (sql[index] === "'") {
|
|
123
|
-
index += 1;
|
|
124
|
-
break;
|
|
125
|
-
}
|
|
126
|
-
index += 1;
|
|
127
|
-
}
|
|
128
|
-
output += " ";
|
|
129
|
-
continue;
|
|
130
|
-
}
|
|
131
|
-
if (char === "-" && sql[index + 1] === "-") {
|
|
132
|
-
while (index < sql.length && sql[index] !== "\n") {
|
|
133
|
-
index += 1;
|
|
134
|
-
}
|
|
135
|
-
continue;
|
|
136
|
-
}
|
|
137
|
-
if (char === "/" && sql[index + 1] === "*") {
|
|
138
|
-
index += 2;
|
|
139
|
-
while (index < sql.length && !(sql[index] === "*" && sql[index + 1] === "/")) {
|
|
140
|
-
index += 1;
|
|
141
|
-
}
|
|
142
|
-
index += 2;
|
|
143
|
-
continue;
|
|
144
|
-
}
|
|
145
|
-
output += char;
|
|
146
|
-
index += 1;
|
|
147
|
-
}
|
|
148
|
-
return output;
|
|
149
|
-
};
|
|
150
|
-
|
|
151
|
-
const deniedKeywords = /\b(pragma|attach|detach|vacuum|load_extension)\b/i;
|
|
152
|
-
|
|
153
|
-
const scanSql = (sql: string): WarehouseQueryDenied | undefined => {
|
|
154
|
-
const stripped = stripLiteralsAndComments(sql);
|
|
155
|
-
const semicolon = stripped.indexOf(";");
|
|
156
|
-
if (semicolon !== -1 && stripped.slice(semicolon + 1).trim().length > 0) {
|
|
157
|
-
return WarehouseQueryDenied.make({
|
|
158
|
-
reason: "multi-statement",
|
|
159
|
-
message: "Exactly one SQL statement is allowed per call",
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
const denied = deniedKeywords.exec(stripped);
|
|
163
|
-
if (denied !== null) {
|
|
164
|
-
return WarehouseQueryDenied.make({
|
|
165
|
-
reason: "denied-keyword",
|
|
166
|
-
message: `${denied[1].toUpperCase()} is not available through the read-only warehouse`,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
return undefined;
|
|
170
|
-
};
|
|
171
|
-
|
|
172
|
-
/** Non-cryptographic FNV-1a 64-bit digest for audit correlation of SQL text. */
|
|
173
|
-
const queryDigest = (sql: string): string => {
|
|
174
|
-
let hash = 0xcbf29ce484222325n;
|
|
175
|
-
for (let index = 0; index < sql.length; index += 1) {
|
|
176
|
-
hash ^= BigInt(sql.charCodeAt(index));
|
|
177
|
-
hash = (hash * 0x100000001b3n) & 0xffffffffffffffffn;
|
|
178
|
-
}
|
|
179
|
-
return hash.toString(16).padStart(16, "0");
|
|
180
|
-
};
|
|
181
|
-
|
|
182
|
-
const utf8ByteLength = (value: string): number => {
|
|
183
|
-
let total = 0;
|
|
184
|
-
for (const character of value) {
|
|
185
|
-
const codePoint = character.codePointAt(0) ?? 0;
|
|
186
|
-
total += codePoint <= 0x7f ? 1 : codePoint <= 0x7ff ? 2 : codePoint <= 0xffff ? 3 : 4;
|
|
187
|
-
}
|
|
188
|
-
return total;
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
const decodeRow = Schema.decodeUnknownOption(Schema.Record(Schema.String, Schema.Json));
|
|
192
|
-
|
|
193
|
-
/**
|
|
194
|
-
* The read-only authority denial is structural: node-sqlite reports SQLite
|
|
195
|
-
* result code 8 (`SQLITE_READONLY`; extended codes keep 8 in the low byte)
|
|
196
|
-
* on the reason's cause. No error-text matching.
|
|
197
|
-
*/
|
|
198
|
-
const isWriteDenial = (error: SqlError): boolean => {
|
|
199
|
-
const cause = (error.reason as { readonly cause?: unknown }).cause;
|
|
200
|
-
return (
|
|
201
|
-
Predicate.isObject(cause) &&
|
|
202
|
-
"errcode" in cause &&
|
|
203
|
-
typeof cause.errcode === "number" &&
|
|
204
|
-
(cause.errcode & 0xff) === 8
|
|
205
|
-
);
|
|
206
|
-
};
|
|
207
|
-
|
|
208
|
-
const sqlFailureMessage = (error: SqlError): string => {
|
|
209
|
-
const cause = (error.reason as { readonly cause?: unknown }).cause;
|
|
210
|
-
const detail =
|
|
211
|
-
Predicate.isObject(cause) && "errstr" in cause && typeof cause.errstr === "string"
|
|
212
|
-
? `: ${cause.errstr}`
|
|
213
|
-
: "";
|
|
214
|
-
return `${error.reason.message ?? "SQL execution failed"}${detail}`.slice(0, 2_000);
|
|
215
|
-
};
|
|
216
|
-
|
|
217
|
-
export interface WarehouseOptions {
|
|
218
|
-
/** Host-owned tenant identity; only this tenant's rows are materialized. */
|
|
219
|
-
readonly tenant: string;
|
|
220
|
-
readonly seed: ReadonlyArray<WarehouseSeedRow>;
|
|
221
|
-
readonly limits?: WarehouseLimits | undefined;
|
|
222
|
-
/**
|
|
223
|
-
* Not supported: the synchronous driver cannot cancel a statement, so any
|
|
224
|
-
* requested timeout fails Layer construction typed instead of being
|
|
225
|
-
* silently unenforced.
|
|
226
|
-
*/
|
|
227
|
-
readonly statementTimeout?: Duration.Duration | undefined;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Build the tenant-scoped read-only warehouse. The returned Layer owns the
|
|
232
|
-
* private in-memory database; nothing else ever sees the connection.
|
|
233
|
-
*/
|
|
234
|
-
export const warehouseDbLayer = (
|
|
235
|
-
options: WarehouseOptions,
|
|
236
|
-
): Layer.Layer<WarehouseDb, WarehouseConfigurationError | SqlError> =>
|
|
237
|
-
Layer.effect(WarehouseDb)(
|
|
238
|
-
Effect.gen(function* () {
|
|
239
|
-
if (options.statementTimeout !== undefined) {
|
|
240
|
-
return yield* WarehouseConfigurationError.make({
|
|
241
|
-
message:
|
|
242
|
-
"The reference warehouse cannot enforce a statement timeout on the synchronous SQLite driver and refuses to pretend otherwise; bound the enclosing pass instead",
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
const limits = options.limits ?? defaultLimits;
|
|
246
|
-
const sql = yield* SqlClient.SqlClient;
|
|
247
|
-
|
|
248
|
-
// Curated, tenant-scoped topology: only this tenant's rows exist in the
|
|
249
|
-
// database the tool can reach, so cross-tenant reads are physically
|
|
250
|
-
// impossible rather than merely filtered.
|
|
251
|
-
yield* sql`CREATE TABLE invoice_summary (
|
|
252
|
-
customer TEXT NOT NULL,
|
|
253
|
-
region TEXT NOT NULL,
|
|
254
|
-
revenue INTEGER NOT NULL,
|
|
255
|
-
created_at TEXT NOT NULL
|
|
256
|
-
)`;
|
|
257
|
-
const tenantRows = options.seed.filter((row) => row.tenant === options.tenant);
|
|
258
|
-
for (const row of tenantRows) {
|
|
259
|
-
yield* sql`INSERT INTO invoice_summary ${sql.insert({
|
|
260
|
-
customer: row.customer,
|
|
261
|
-
region: row.region,
|
|
262
|
-
revenue: row.revenue,
|
|
263
|
-
created_at: row.createdAt,
|
|
264
|
-
})}`;
|
|
265
|
-
}
|
|
266
|
-
// Database authority: the same connection now refuses every write.
|
|
267
|
-
yield* sql`PRAGMA query_only = ON`.withoutTransform;
|
|
268
|
-
|
|
269
|
-
const query = Effect.fn("WarehouseDb.query")(function* (
|
|
270
|
-
text: string,
|
|
271
|
-
parameters: ReadonlyArray<string | number | boolean | null>,
|
|
272
|
-
) {
|
|
273
|
-
const denied = scanSql(text);
|
|
274
|
-
if (denied !== undefined) {
|
|
275
|
-
return yield* denied;
|
|
276
|
-
}
|
|
277
|
-
const rawRows = yield* sql
|
|
278
|
-
.unsafe<Record<string, unknown>>(text, parameters as Array<unknown>)
|
|
279
|
-
.withoutTransform.pipe(
|
|
280
|
-
Effect.catchTag("SqlError", (error) =>
|
|
281
|
-
Effect.fail<WarehouseQueryDenied | WarehouseQueryFailed>(
|
|
282
|
-
isWriteDenial(error)
|
|
283
|
-
? WarehouseQueryDenied.make({
|
|
284
|
-
reason: "write-denied",
|
|
285
|
-
message: "The warehouse database authority is read-only",
|
|
286
|
-
})
|
|
287
|
-
: WarehouseQueryFailed.make({
|
|
288
|
-
message: sqlFailureMessage(error),
|
|
289
|
-
}),
|
|
290
|
-
),
|
|
291
|
-
),
|
|
292
|
-
);
|
|
293
|
-
const rows: Array<Record<string, Schema.Json>> = [];
|
|
294
|
-
let truncated = false;
|
|
295
|
-
let usedBytes = 0;
|
|
296
|
-
for (const raw of rawRows) {
|
|
297
|
-
if (rows.length >= limits.maxRows) {
|
|
298
|
-
truncated = true;
|
|
299
|
-
break;
|
|
300
|
-
}
|
|
301
|
-
const decoded = decodeRow(raw);
|
|
302
|
-
if (Option.isNone(decoded)) {
|
|
303
|
-
return yield* WarehouseQueryFailed.make({
|
|
304
|
-
message: "A result row contained a value outside the JSON surface",
|
|
305
|
-
});
|
|
306
|
-
}
|
|
307
|
-
const bytes = utf8ByteLength(JSON.stringify(decoded.value));
|
|
308
|
-
if (usedBytes + bytes > limits.maxResultBytes) {
|
|
309
|
-
truncated = true;
|
|
310
|
-
break;
|
|
311
|
-
}
|
|
312
|
-
usedBytes += bytes;
|
|
313
|
-
rows.push(decoded.value);
|
|
314
|
-
}
|
|
315
|
-
const columns = rows.length === 0 ? [] : Object.keys(rows[0]);
|
|
316
|
-
const success = WarehouseQuerySuccess.make({
|
|
317
|
-
columns,
|
|
318
|
-
rows,
|
|
319
|
-
rowCount: rows.length,
|
|
320
|
-
truncated,
|
|
321
|
-
});
|
|
322
|
-
// Structural audit metadata: digest and shape only — never SQL text,
|
|
323
|
-
// parameter values, or row contents (SEC-015, spec §11).
|
|
324
|
-
yield* Effect.logDebug("warehouse query settled").pipe(
|
|
325
|
-
Effect.annotateLogs({
|
|
326
|
-
queryDigest: queryDigest(text),
|
|
327
|
-
parameterCount: parameters.length,
|
|
328
|
-
rowCount: success.rowCount,
|
|
329
|
-
truncated: success.truncated,
|
|
330
|
-
}),
|
|
331
|
-
);
|
|
332
|
-
return success;
|
|
333
|
-
});
|
|
334
|
-
|
|
335
|
-
return WarehouseDb.of({ query });
|
|
336
|
-
}),
|
|
337
|
-
).pipe(Layer.provide(SqliteClient.layer({ filename: ":memory:" })));
|
|
338
|
-
|
|
339
|
-
/**
|
|
340
|
-
* The native read-only SQL Tool over the curated warehouse (plan §8.3). The
|
|
341
|
-
* curated schema is part of the model-facing description; results report
|
|
342
|
-
* truncation honestly (`truncated: true` success, D-035 default).
|
|
343
|
-
*/
|
|
344
|
-
export const warehouseQueryTool = Tool.make("query_warehouse", {
|
|
345
|
-
description:
|
|
346
|
-
"Run one read-only SQL statement against the curated warehouse view `invoice_summary` (columns: customer TEXT, region TEXT, revenue INTEGER, created_at TEXT). Use ? placeholders with the parameters array; exactly one statement per call. Oversized results return truncated: true.",
|
|
347
|
-
parameters: Schema.Struct({
|
|
348
|
-
sql: BoundedSqlText,
|
|
349
|
-
parameters: Schema.optionalKey(Schema.Array(SqlParameter).check(Schema.isMaxLength(32))),
|
|
350
|
-
}),
|
|
351
|
-
success: WarehouseQuerySuccess,
|
|
352
|
-
failure: Schema.Union([WarehouseQueryDenied, WarehouseQueryFailed]),
|
|
353
|
-
})
|
|
354
|
-
.annotate(Tool.Readonly, true)
|
|
355
|
-
.annotate(ToolExecutionClass, "readonly");
|
|
356
|
-
|
|
357
|
-
export const warehouseToolkit = Toolkit.make(warehouseQueryTool);
|
|
358
|
-
|
|
359
|
-
/** Handler Layer binding the Tool to the tenant-scoped read-only service. */
|
|
360
|
-
export const warehouseHandlersLayer = warehouseToolkit.toLayer(
|
|
361
|
-
Effect.gen(function* () {
|
|
362
|
-
const db = yield* WarehouseDb;
|
|
363
|
-
return {
|
|
364
|
-
query_warehouse: ({
|
|
365
|
-
sql,
|
|
366
|
-
parameters,
|
|
367
|
-
}: {
|
|
368
|
-
readonly sql: string;
|
|
369
|
-
readonly parameters?: ReadonlyArray<string | number | boolean | null> | undefined;
|
|
370
|
-
}) => db.query(sql, parameters ?? []),
|
|
371
|
-
};
|
|
372
|
-
}),
|
|
373
|
-
);
|
|
374
|
-
|
|
375
|
-
/** A small deterministic multi-tenant seed shared by tests and the demo. */
|
|
376
|
-
export const warehouseDemoSeed: ReadonlyArray<WarehouseSeedRow> = [
|
|
377
|
-
WarehouseSeedRow.make({
|
|
378
|
-
tenant: "acme",
|
|
379
|
-
customer: "Stellar Freight",
|
|
380
|
-
region: "emea",
|
|
381
|
-
revenue: 48_200,
|
|
382
|
-
createdAt: "2026-07-03",
|
|
383
|
-
}),
|
|
384
|
-
WarehouseSeedRow.make({
|
|
385
|
-
tenant: "acme",
|
|
386
|
-
customer: "Nimbus Analytics",
|
|
387
|
-
region: "amer",
|
|
388
|
-
revenue: 12_800,
|
|
389
|
-
createdAt: "2026-07-11",
|
|
390
|
-
}),
|
|
391
|
-
WarehouseSeedRow.make({
|
|
392
|
-
tenant: "acme",
|
|
393
|
-
customer: "Copper Kettle Co",
|
|
394
|
-
region: "amer",
|
|
395
|
-
revenue: 730,
|
|
396
|
-
createdAt: "2026-07-15",
|
|
397
|
-
}),
|
|
398
|
-
WarehouseSeedRow.make({
|
|
399
|
-
tenant: "acme",
|
|
400
|
-
customer: "Harbor Lights Ltd",
|
|
401
|
-
region: "apac",
|
|
402
|
-
revenue: 9_400,
|
|
403
|
-
createdAt: "2026-07-21",
|
|
404
|
-
}),
|
|
405
|
-
WarehouseSeedRow.make({
|
|
406
|
-
tenant: "umbra",
|
|
407
|
-
customer: "Shadow Cartel",
|
|
408
|
-
region: "emea",
|
|
409
|
-
revenue: 999_999,
|
|
410
|
-
createdAt: "2026-07-01",
|
|
411
|
-
}),
|
|
412
|
-
];
|