Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/agent-interface/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# @tangle-network/agent-interface

## 2.6.1

### Patch Changes

- Allow streamed event content and terminal response text up to the existing 1 MiB serialized UTF-8 contract limit.
Keep metadata, identifiers, and structural bounds unchanged, and export the shared content validator for providers.

## 2.6.0

### Minor Changes
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-interface/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@tangle-network/agent-interface",
"version": "2.6.0",
"version": "2.6.1",
"type": "module",
"sideEffects": false,
"license": "MIT",
Expand Down
156 changes: 156 additions & 0 deletions packages/agent-interface/src/contract-limits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,142 @@ export function isBoundedJsonValue(value: unknown): boolean {
return true;
}

/** Exact serialized JSON UTF-8 byte count for one string scalar. */
function serializedJsonStringBytes(value: string): number {
let bytes = 2; // Opening and closing quotes.
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code === 0x22 || code === 0x5c || code === 0x08 || code === 0x0c || code === 0x0a || code === 0x0d || code === 0x09) {
bytes += 2;
} else if (code < 0x20) {
bytes += 6;
} else if (code >= 0xd800 && code <= 0xdbff && index + 1 < value.length) {
const next = value.charCodeAt(index + 1);
if (next >= 0xdc00 && next <= 0xdfff) {
bytes += 4;
index += 1;
} else {
bytes += 6;
}
} else if (code >= 0xd800 && code <= 0xdfff) {
bytes += 6;
} else if (code < 0x80) {
bytes += 1;
} else if (code < 0x800) {
bytes += 2;
} else {
bytes += 3;
}
if (bytes > CONTRACT_MAX_JSON_BYTES) return bytes;
}
return bytes;
}

/**
* Validate provider event and terminal content. Unlike ordinary metadata,
* content may contain a single large transcript or tool result. It remains
* finite, plain JSON with the normal structural limits, and its complete JSON
* representation is capped exactly by UTF-8 bytes.
*/
export function isBoundedEventContentJson(
value: unknown,
{ omitUndefinedObjectFields = false }: { omitUndefinedObjectFields?: boolean } = {},
): boolean {
const pending: Array<{
value: unknown;
depth: number;
leave?: boolean;
omitUndefined?: boolean;
}> = [
{ value, depth: 0 },
];
const ancestors = new Set<object>();
let nodes = 0;
let bytes = 0;
const addBytes = (additional: number) => {
bytes += additional;
return bytes <= CONTRACT_MAX_JSON_BYTES;
};
while (pending.length > 0) {
const item = pending.pop();
if (!item) continue;
const current = item.value;
if (current === undefined && item.omitUndefined === true) continue;
nodes += 1;
if (nodes > CONTRACT_MAX_JSON_NODES) return false;
if (item.leave) {
ancestors.delete(current as object);
continue;
}
if (current === null) {
if (!addBytes(4)) return false;
continue;
}
if (typeof current === "boolean") {
if (!addBytes(current ? 4 : 5)) return false;
continue;
}
if (typeof current === "string") {
if (!addBytes(serializedJsonStringBytes(current))) return false;
continue;
}
if (typeof current === "number") {
if (!Number.isFinite(current)) return false;
if (!addBytes(JSON.stringify(current).length)) return false;
continue;
}
if (typeof current !== "object" || item.depth >= CONTRACT_MAX_JSON_DEPTH) {
return false;
}
if (ancestors.has(current)) return false;
ancestors.add(current);
pending.push({ value: current, depth: item.depth, leave: true });
if (Array.isArray(current)) {
if (current.length > CONTRACT_MAX_ARRAY_LENGTH) return false;
if (!addBytes(2 + Math.max(current.length - 1, 0))) return false;
const prototype = Object.getPrototypeOf(current);
if (prototype !== Array.prototype && prototype !== null) return false;
const keys = Reflect.ownKeys(current);
const entryKeys = keys.filter((key) => key !== "length");
if (entryKeys.length !== current.length) return false;
if (entryKeys.some((key) => {
if (typeof key !== "string" || !/^(?:0|[1-9][0-9]*)$/u.test(key)) return true;
const index = Number(key);
return !Number.isSafeInteger(index) || index < 0 || index >= current.length || index >= 4_294_967_295;
})) {
return false;
}
for (const key of entryKeys) {
const descriptor = Object.getOwnPropertyDescriptor(current, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) return false;
pending.push({ value: descriptor.value, depth: item.depth + 1 });
}
continue;
}
const prototype = Object.getPrototypeOf(current);
if (prototype !== Object.prototype && prototype !== null) return false;
const entries: Array<[string, PropertyDescriptor]> = [];
for (const key of Reflect.ownKeys(current)) {
if (typeof key !== "string" || key.length > CONTRACT_MAX_IDENTIFIER_LENGTH) return false;
const descriptor = Object.getOwnPropertyDescriptor(current, key);
if (descriptor === undefined || !descriptor.enumerable || !("value" in descriptor)) return false;
if (descriptor.value === undefined && omitUndefinedObjectFields) continue;
entries.push([key, descriptor]);
if (entries.length > CONTRACT_MAX_MAP_ENTRIES) return false;
}
if (!addBytes(2 + Math.max(entries.length - 1, 0))) return false;
for (const [key, descriptor] of entries) {
if (!addBytes(serializedJsonStringBytes(key) + 1)) return false;
pending.push({
value: descriptor.value,
depth: item.depth + 1,
omitUndefined: omitUndefinedObjectFields,
});
}
}
return true;
}

/** Validate digest input while matching JSON's omission of undefined object fields. */
export function isBoundedJsonMaterial(value: unknown): boolean {
const pending: Array<{
Expand Down Expand Up @@ -162,6 +298,26 @@ export const boundedJsonRecordSchema = z.custom<Record<string, unknown>>(
{ message: "metadata exceeds the contract bounds or is not a JSON object" },
);

/** One provider event payload or terminal response, bounded as serialized UTF-8 JSON. */
export const boundedEventContentJsonSchema = z.custom<unknown>(
isBoundedEventContentJson,
{ message: "event content exceeds its serialized byte bound or is not finite JSON" },
);

export const boundedEventContentRecordSchema = z.custom<Record<string, unknown>>(
(value) =>
typeof value === "object" &&
value !== null &&
!Array.isArray(value) &&
isBoundedEventContentJson(value),
{ message: "event content exceeds its serialized byte bound or is not a JSON object" },
);

export const boundedEventContentStringSchema = z.custom<string>(
(value) => typeof value === "string" && isBoundedEventContentJson(value),
{ message: "event content exceeds its serialized byte bound or is not a string" },
);

export function assertBoundedJson(value: unknown): void {
if (!isBoundedJsonValue(value)) {
throw new Error("value exceeds the contract bounds or is not finite JSON");
Expand Down
8 changes: 4 additions & 4 deletions packages/agent-interface/src/environment-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type {
AgentWorkspaceBranchingProvider,
} from "./workspace-branching.js";
import { AgentProfileCapabilitiesSchema } from "./environment-profile-capabilities.js";
import { boundedIdentifierSchema, boundedJsonRecordSchema, boundedJsonSchema, boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js";
import { boundedEventContentJsonSchema, boundedEventContentRecordSchema, boundedEventContentStringSchema, boundedIdentifierSchema, boundedJsonRecordSchema, boundedStringSchema, CONTRACT_MAX_ARRAY_LENGTH } from "./contract-limits.js";
import { InputPartSchema } from "./portable-context-shared.js";
import { deepFreeze } from "./deep-freeze.js";
import type { AgentEnvironmentEgressMode, AgentEnvironmentEgressPolicy, AgentEnvironmentQuery, AgentEnvironmentStatus, AgentEnvironmentSummary, AgentProfileRef, AgentSessionStatus, CheckpointRef, CheckpointRequest, ExecRequest, ExecResult, ForkRequest, PlacementInfo, ResourceRequest, WorkspaceRequest } from "./environment-requests.js";
Expand Down Expand Up @@ -96,17 +96,17 @@ const TokenUsageSchema = z.strictObject({

const AgentEnvironmentEventSchema = z.strictObject({
type: boundedIdentifierSchema,
data: boundedJsonRecordSchema,
data: boundedEventContentRecordSchema,
id: boundedIdentifierSchema.optional(),
normalized: CanonicalStreamEventSchema.optional(),
usage: TokenUsageSchema.optional(),
usageMode: z.enum(["delta", "cumulative"]).optional(),
providerEvent: boundedJsonSchema.optional(),
providerEvent: boundedEventContentJsonSchema.optional(),
}) satisfies z.ZodType<AgentEnvironmentEvent>;

/** Runtime validator for a provider turn returned from durable continuation. */
export const AgentTurnResultSchema = z.strictObject({
text: boundedStringSchema,
text: boundedEventContentStringSchema,
success: z.boolean(),
error: boundedStringSchema.optional(),
sessionId: boundedIdentifierSchema.optional(),
Expand Down
5 changes: 5 additions & 0 deletions packages/agent-interface/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,11 @@ export * from "./profile-security.js";
export * from "./sandbox-size.js";
export {
CONTRACT_MAX_CONFIDENTIAL_ATTESTATION_QUOTE_LENGTH,
CONTRACT_MAX_JSON_BYTES,
boundedEventContentJsonSchema,
boundedEventContentRecordSchema,
boundedEventContentStringSchema,
isBoundedEventContentJson,
} from "./contract-limits.js";

export { AgentEnvironmentEgressPolicySchema } from "./environment-requests.js";
71 changes: 70 additions & 1 deletion packages/agent-interface/src/leaf-modules.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ import {
InputPartSchema,
wireDigest as portableWireDigest,
} from "./portable-context-shared.js";
import { isBoundedJsonValue } from "./contract-limits.js";
import { isBoundedEventContentJson, isBoundedJsonValue } from "./contract-limits.js";
import {
ContextTransferReceiptSchema,
ContextTransferRequestSchema,
Expand All @@ -73,10 +73,12 @@ import {
CONTRACT_MAX_CONFIDENTIAL_ATTESTATION_QUOTE_LENGTH,
CONTRACT_MAX_JSON_BYTES,
CONTRACT_MAX_STRING_LENGTH,
boundedEventContentStringSchema,
boundedJsonSchema,
boundedStringSchema,
nullPrototypeRecord,
} from "./contract-limits.js";
import { CanonicalStreamEventSchema } from "./runtime-control.js";
import {
WorkspaceCheckpointRefSchema,
WorkspaceCheckpointRequestSchema,
Expand Down Expand Up @@ -228,6 +230,73 @@ describe("interface split leaf modules", () => {
expect(() => portableWireDigest(unicodeMaterial)).toThrow(/byte bound/);
});

it("accepts large event content up to its exact serialized UTF-8 bound", () => {
const maximum = "x".repeat(CONTRACT_MAX_JSON_BYTES - 2);
expect(isBoundedEventContentJson(maximum)).toBe(true);
expect(boundedEventContentStringSchema.parse(maximum)).toBe(maximum);
expect(isBoundedEventContentJson(`${maximum}x`)).toBe(false);
expect(boundedEventContentStringSchema.safeParse(`${maximum}x`).success).toBe(false);
expect(isBoundedEventContentJson({ unicode: "é".repeat(600_000) })).toBe(false);
expect(isBoundedEventContentJson({ ordinary: "x".repeat(CONTRACT_MAX_STRING_LENGTH + 1) })).toBe(true);
const altered = ["small"] as unknown as { toJSON: () => string };
altered.toJSON = () => "x".repeat(CONTRACT_MAX_JSON_BYTES + 1);
expect(isBoundedEventContentJson(altered)).toBe(false);
const inheritedToJson = ["small"];
Object.setPrototypeOf(inheritedToJson, { toJSON: () => "x".repeat(CONTRACT_MAX_JSON_BYTES + 1) });
expect(isBoundedEventContentJson(inheritedToJson)).toBe(false);
const sparseWithNonIndex = new Array(1) as unknown as Record<string, unknown>;
sparseWithNonIndex["4294967295"] = "ignored by JSON.stringify";
expect(isBoundedEventContentJson(sparseWithNonIndex)).toBe(false);
for (const pattern of ["\ud800", "\"", "\\", "\u0000", "\b", "\n"]) {
const scalarBytes = Buffer.byteLength(JSON.stringify(pattern), "utf8") - 2;
const atLimit = pattern.repeat(Math.floor((CONTRACT_MAX_JSON_BYTES - 2) / scalarBytes));
for (const value of [atLimit, `${atLimit}${pattern}`]) {
expect(isBoundedEventContentJson(value)).toBe(
Buffer.byteLength(JSON.stringify(value), "utf8") <= CONTRACT_MAX_JSON_BYTES,
);
}
}
});

it("keeps long canonical stream and terminal response content without widening metadata", () => {
const content = "x".repeat(CONTRACT_MAX_STRING_LENGTH + 1);
const normalized = {
type: "message.part.updated" as const,
part: {
id: "part-1",
sessionID: "session-1",
messageID: "message-1",
type: "tool" as const,
tool: "shell",
state: { status: "completed" as const, input: {}, output: { content } },
},
delta: content,
};
expect(CanonicalStreamEventSchema.parse(normalized)).toEqual(normalized);
expect(AgentTurnResultSchema.parse({
text: content,
success: true,
events: [{ type: "provider-event", data: { content }, normalized, providerEvent: { content } }],
}).text).toBe(content);
expect(AgentTurnResultSchema.safeParse({
text: "done",
success: true,
metadata: { content },
}).success).toBe(false);
const withExplicitUndefined = {
type: "message.part.updated" as const,
part: {
id: "part-2",
sessionID: "session-1",
messageID: "message-1",
type: "text" as const,
text: "present",
},
delta: undefined,
};
expect(CanonicalStreamEventSchema.parse(withExplicitUndefined)).toEqual(withExplicitUndefined);
});

it("exports the portable workspace cwd leaf contract", () => {
expect(canonicalWorkspaceCwd({ base: "repository", path: "./packages//braid/." })).toEqual({
base: "repository",
Expand Down
Loading