History

Address review: classifier coverage, write bookkeeping, equality bound

Rhys Sullivan committed 2 months ago

Commit a30e4331 parent 27bd4196 files · +98 −10Browse files

Unified · Split

6 files
  1. packages/core/sdk/src/shape-inference.test.ts +10 −0
  2. packages/core/sdk/src/shape-inference.ts +17 −1
  3. packages/core/sdk/src/shape-memory.test.ts +33 −3
  4. packages/core/sdk/src/shape-memory.ts +9 −2
  5. packages/core/sdk/src/tool-result-normalization.test.ts +16 −0
  6. packages/core/sdk/src/tool-result-normalization.ts +13 −4
packages/core/sdk/src/shape-inference.test.ts+10−0
@@ -57,6 +57,10 @@ describe("inferShape", () => { · 56 unmodified lines
57 { "12345": { qty: 2 }, "67890": { qty: 1 } },57 { "12345": { qty: 2 }, "67890": { qty: 1 } },
58 { "https://example.com/page": 3 },58 { "https://example.com/page": 3 },
59 { deadbeefdeadbeef00: true },59 { deadbeefdeadbeef00: true },
60 { U012ABCDEF: { presence: "active" } },
61 { cus_9s6XKzkNRiz8i3: { plan: "pro" } },
62 { "10.0.0.7": "reachable" },
63 { sk4bcD3fGh1jKlMnOpQr: true },
60 ];64 ];
61 for (const value of cases) {65 for (const value of cases) {
62 const shape = inferShape(value);66 const shape = inferShape(value);
@@ -71,13 +75,19 @@ describe("inferShape", () => { · 8 unmodified lines
71 created_at: "2026-01-01",75 created_at: "2026-01-01",
72 pageUrl: "https://x",76 pageUrl: "https://x",
73 email2fa: true,77 email2fa: true,
78 "@odata.context": "ctx",
79 organizationMembershipSettings: {},
80 sha256Fingerprint: "…",
74 });81 });
75 expect(shape.properties).toBeDefined();82 expect(shape.properties).toBeDefined();
76 expect(Object.keys(shape.properties ?? {}).sort()).toEqual([83 expect(Object.keys(shape.properties ?? {}).sort()).toEqual([
84 "@odata.context",
77 "created_at",85 "created_at",
78 "email2fa",86 "email2fa",
79 "id",87 "id",
88 "organizationMembershipSettings",
80 "pageUrl",89 "pageUrl",
90 "sha256Fingerprint",
81 ]);91 ]);
82 });92 });
83 93
packages/core/sdk/src/shape-inference.ts+17−1
@@ -55,12 +55,28 @@ const isUnknown = (shape: InferredShape): boolean => · 54 unmodified lines
55 * collapsing.55 * collapsing.
56 */56 */
57const DATA_KEY_PATTERNS: readonly RegExp[] = [57const DATA_KEY_PATTERNS: readonly RegExp[] = [
58 /@/,58 // Email addresses (full-string — `@odata.context`-style annotation keys are
59 // legitimate API surface and must NOT collapse).
60 /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
61 // UUIDs.
59 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,62 /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
63 // Timestamps / dates.
60 /^\d{4}-\d{2}-\d{2}/,64 /^\d{4}-\d{2}-\d{2}/,
65 // Bare numbers and IPv4 addresses.
61 /^\d+$/,66 /^\d+$/,
67 /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/,
68 // URLs.
62 /^https?:\/\//,69 /^https?:\/\//,
70 // Long hex tokens (hashes, ids).
63 /^[0-9a-f]{16,}$/i,71 /^[0-9a-f]{16,}$/i,
72 // Platform opaque ids: Slack-style ALL-CAPS ids (U012ABCDEF), and
73 // prefix_body ids (cus_..., price_..., asst_...). Real field names in
74 // snake_case are lowercase words, not lowercase prefix + mixed-case body.
75 /^[A-Z][A-Z0-9]{8,}$/,
76 /^[a-z]{1,6}_(?=.*[A-Z0-9])[A-Za-z0-9]{10,}$/,
77 // Generic digit-bearing opaque tokens (API keys, base62/base64 ids). Long
78 // camelCase field names rarely contain digits at this length.
79 /^(?=.*\d)[A-Za-z0-9+/=_-]{20,}$/,
64];80];
65 81
66const looksLikeDataKey = (key: string): boolean =>82const looksLikeDataKey = (key: string): boolean =>
packages/core/sdk/src/shape-memory.test.ts+33−3
@@ -27,6 +27,7 @@ const makeStubStorage = () => { · 26 unmodified lines
27 updatedAt: new Date(0),27 updatedAt: new Date(0),
28 };28 };
29 };29 };
30 let failNextWrites = 0;
30 const unsupported = (member: string) => () =>31 const unsupported = (member: string) => () =>
31 Effect.die(`stub storage does not implement ${member}`);32 Effect.die(`stub storage does not implement ${member}`);
32 const storage: PluginStorageFacade = {33 const storage: PluginStorageFacade = {
@@ -43,16 +44,27 @@ const makeStubStorage = () => { · 10 unmodified lines
43 getForOwner: (input) => Effect.sync(() => entryFor(input.key)),44 getForOwner: (input) => Effect.sync(() => entryFor(input.key)),
44 list: unsupported("list"),45 list: unsupported("list"),
45 put: (input) =>46 put: (input) =>
46 Effect.sync(() => {47 Effect.suspend(() => {
48 if (failNextWrites > 0) {
49 failNextWrites -= 1;
50 return Effect.fail({ _tag: "StorageError" as const }) as never;
51 }
47 writes += 1;52 writes += 1;
48 rows.set(input.key, input.data);53 rows.set(input.key, input.data);
49 return entryFor(input.key) as never;54 return Effect.sync(() => entryFor(input.key) as never);
50 }),55 }),
51 putMany: unsupported("putMany"),56 putMany: unsupported("putMany"),
52 remove: unsupported("remove"),57 remove: unsupported("remove"),
53 removeMany: unsupported("removeMany"),58 removeMany: unsupported("removeMany"),
54 };59 };
55 return { storage, rows, writeCount: () => writes };60 return {
61 storage,
62 rows,
63 writeCount: () => writes,
64 failWrites: (count: number) => {
65 failNextWrites = count;
66 },
67 };
56};68};
57 69
58const HOUR = 60 * 60 * 1000;70const HOUR = 60 * 60 * 1000;
@@ -120,6 +132,24 @@ describe("makeShapeMemory", () => { · 61 unmodified lines
120 }),132 }),
121 );133 );
122 134
135 it.effect("retries after a failed write instead of pretending it persisted", () =>
136 Effect.gen(function* () {
137 const stub = makeStubStorage();
138 const memory = makeShapeMemory(stub.storage);
139
140 stub.failWrites(1);
141 yield* memory.observe(ADDRESS, OWNER, "direct", { id: 1 });
142 expect(stub.rows.has(ADDRESS), "the failed write stored nothing").toBe(false);
143
144 // The very next observation retries — no waiting out the freshness
145 // interval on bookkeeping that lied about persisting.
146 yield* memory.observe(ADDRESS, OWNER, "direct", { id: 2 });
147 expect(stub.rows.has(ADDRESS), "the retry persisted").toBe(true);
148 const stored = stub.rows.get(ADDRESS) as { observations: number };
149 expect(stored.observations).toBe(2);
150 }),
151 );
152
123 it.effect("treats legacy records without a contract field as direct", () =>153 it.effect("treats legacy records without a contract field as direct", () =>
124 Effect.gen(function* () {154 Effect.gen(function* () {
125 const stub = makeStubStorage();155 const stub = makeStubStorage();
packages/core/sdk/src/shape-memory.ts+9−2
@@ -125,9 +125,16 @@ export const makeShapeMemory = (storage: PluginStorageFacade): ShapeMemory => { · 124 unmodified lines
125 next.observations % OBSERVATION_WRITE_MILESTONE === 0 ||125 next.observations % OBSERVATION_WRITE_MILESTONE === 0 ||
126 now - lastWrite >= FRESHNESS_WRITE_INTERVAL_MS;126 now - lastWrite >= FRESHNESS_WRITE_INTERVAL_MS;
127 if (!shouldWrite) return;127 if (!shouldWrite) return;
128 yield* storage128 // Advance the persisted-state bookkeeping only on a successful write:
129 // otherwise a transient storage failure would silence retries for the
130 // whole freshness interval while nothing is actually stored.
131 const wrote = yield* storage
129 .put({ owner, collection: COLLECTION, key: address, data: next })132 .put({ owner, collection: COLLECTION, key: address, data: next })
130 .pipe(Effect.catch(() => Effect.succeed(null)));133 .pipe(
134 Effect.map(() => true),
135 Effect.catch(() => Effect.succeed(false)),
136 );
137 if (!wrote) return;
131 persistedSchema.set(key, schemaJson);138 persistedSchema.set(key, schemaJson);
132 persistedAt.set(key, now);139 persistedAt.set(key, now);
133 }).pipe(Effect.catchCause(() => Effect.void));140 }).pipe(Effect.catchCause(() => Effect.void));
packages/core/sdk/src/tool-result-normalization.test.ts+16−0
@@ -86,6 +86,22 @@ describe("normalizeMcpCallToolResult", () => { · 85 unmodified lines
86 expect(normalized.meta).toEqual({ "io.modelcontextprotocol/serverInfo": { name: "s" } });86 expect(normalized.meta).toEqual({ "io.modelcontextprotocol/serverInfo": { name: "s" } });
87 });87 });
88 88
89 it("survives adversarially deep valid JSON without blowing the stack", () => {
90 const deep = "[".repeat(50_000) + "]".repeat(50_000);
91 // Deep duplicate-detection degrades to "not a duplicate" (block kept);
92 // it must never throw from inside the invocation path.
93 const normalized = normalizeMcpCallToolResult({
94 content: [text(deep)],
95 structuredContent: { safe: true },
96 });
97 expect(normalized.data).toEqual({ safe: true });
98 expect(normalized.content).toEqual([text(deep)]);
99 // The lone-text parse path is equally safe: a stack-exceeding parse is
100 // simply "not JSON" and the text stays a string.
101 const lone = normalizeMcpCallToolResult({ content: [text(deep)] });
102 expect(typeof lone.data === "string" || Array.isArray(lone.data)).toBe(true);
103 });
104
89 it("passes non-envelope values through untouched", () => {105 it("passes non-envelope values through untouched", () => {
90 expect(normalizeMcpCallToolResult({ rows: [1] }).data).toEqual({ rows: [1] });106 expect(normalizeMcpCallToolResult({ rows: [1] }).data).toEqual({ rows: [1] });
91 expect(normalizeMcpCallToolResult("plain").data).toBe("plain");107 expect(normalizeMcpCallToolResult("plain").data).toBe("plain");
packages/core/sdk/src/tool-result-normalization.ts+13−4
@@ -77,21 +77,30 @@ const parseJsonPayload = (text: string): unknown | undefined => { · 76 unmodified lines
77 }77 }
78};78};
79 79
80/** Depth bound for duplicate detection. Beyond it two values are treated as
81 * DIFFERENT — the safe direction: the block is kept rather than dropped —
82 * and, critically, an adversarially deep (but valid) upstream JSON cannot
83 * blow the stack inside the invocation path. */
84const MAX_EQUALITY_DEPTH = 64;
85
80/** Structural equality against the parsed form of a text block, used to86/** Structural equality against the parsed form of a text block, used to
81 * suppress the spec-mandated serialized duplicate of `structuredContent`.87 * suppress the spec-mandated serialized duplicate of `structuredContent`.
82 * Key-order insensitive; bounded by the same size guard as payload parsing. */88 * Key-order insensitive; depth-bounded. */
83const structurallyEquals = (left: unknown, right: unknown): boolean => {89const structurallyEquals = (left: unknown, right: unknown, depth = 0): boolean => {
84 if (Object.is(left, right)) return true;90 if (Object.is(left, right)) return true;
91 if (depth >= MAX_EQUALITY_DEPTH) return false;
85 if (Array.isArray(left) && Array.isArray(right)) {92 if (Array.isArray(left) && Array.isArray(right)) {
86 return (93 return (
87 left.length === right.length &&94 left.length === right.length &&
88 left.every((item, index) => structurallyEquals(item, right[index]))95 left.every((item, index) => structurallyEquals(item, right[index], depth + 1))
89 );96 );
90 }97 }
91 if (isRecord(left) && isRecord(right)) {98 if (isRecord(left) && isRecord(right)) {
92 const leftKeys = Object.keys(left);99 const leftKeys = Object.keys(left);
93 if (leftKeys.length !== Object.keys(right).length) return false;100 if (leftKeys.length !== Object.keys(right).length) return false;
94 return leftKeys.every((key) => key in right && structurallyEquals(left[key], right[key]));101 return leftKeys.every(
102 (key) => key in right && structurallyEquals(left[key], right[key], depth + 1),
103 );
95 }104 }
96 return false;105 return false;
97};106};