Skip to content
Closed
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
148 changes: 148 additions & 0 deletions apps/web/src/lib/agent-harness/operation-contract.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,148 @@
import { expect, it, jest } from '@jest/globals';
import { TRPCError } from '@trpc/server';
import { ErrorSchema } from '@kilocode/agent-harness/contracts';
import * as fixture from './operation-test-fixture';
import type * as Contract from './operation-contract';

const {
HarnessOperationSchema,
harnessOperationScope,
harnessOperationFailure,
safeError,
bounded,
} = jest.requireActual<typeof Contract>('./operation-contract');
const { call, capability, authorizedInput, conversationId, operationId, runId } = fixture;
const identity = { conversationId, operationId };
const projection = {
id: runId,
key: 'projection',
role: 'assistant',
content: '',
createdAt: '2026-08-30T00:00:00.000Z',
};
it.each([
call(),
{ ...call(), type: 'reconcile' },
{ ...identity, type: 'read', purpose: 'read' },
{ ...identity, type: 'history' },
{ ...identity, type: 'projection', projection },
{ ...identity, type: 'retirement', generation: 0 },
])('authorizes strict $type wire input', async input => {
await expect(authorizedInput(input)).resolves.toMatchObject({ authority: fixture.authority });
expect(HarnessOperationSchema.safeParse({ ...input, userId: 'forged' }).success).toBe(false);
});
it.each([
[{ ...identity, type: 'history', limit: undefined }, { limit: 50 }],
[
call('web.search', { query: 'kilo', limit: undefined }),
{ request: { arguments: { limit: 5 } } },
],
[{ ...identity, type: 'projection', projection }, { projection: { clientId: null } }],
])('preserves schema defaults through signed JSON %#', async (input, expected) => {
await expect(authorizedInput(input)).resolves.toMatchObject({ input: expected });
});
it('authorizes absent optional fields without inventing a dispatch timestamp', async () => {
const input = {
...call('kilo.sessions.start', { prompt: 'Build', modelId: 'model', repository: undefined }),
dispatchStartedAt: undefined,
reservation: undefined,
};
const result = await authorizedInput(input);
expect(harnessOperationScope(input)).toEqual(result.scope);
expect(result.input).not.toHaveProperty('dispatchStartedAt');
expect(result.input).not.toHaveProperty('reservation');
expect(result.input).toMatchObject({
request: { arguments: { prompt: 'Build', modelId: 'model' } },
});
});
it('authorizes reordered arguments with the original capability', async () => {
const token = capability(
call('kilo.invite', { recipient: 'member@example.com', role: 'member' })
);
const input = call('kilo.invite', { role: 'member', recipient: 'member@example.com' });
await expect(authorizedInput(input, token)).resolves.toMatchObject({
authority: fixture.authority,
});
});
it.each([
{ ...call(), type: 'arbitrary.trpc' },
{ ...call(), userId: 'forged' },
{ ...call(), userId: undefined },
call('arbitrary.trpc'),
call('kilo.organizations', { userId: 'forged' }),
{ ...call(), dispatchStartedAt: NaN },
{ ...call(), reservation: {} },
{ ...identity, type: 'history', limit: 51 },
{ ...identity, type: 'read', purpose: 'execute' },
{ ...identity, type: 'retirement', generation: -1 },
{ ...identity, type: 'projection', projection: { ...projection, token: 'secret' } },
])('rejects malformed input before wire normalization %#', input => {
expect(HarnessOperationSchema.safeParse(input).success).toBe(false);
expect(() => harnessOperationScope(input)).toThrow();
});
it.each([
{ type: 'reconcile' },
{ conversationId: runId },
{ operationId: runId },
{ runId: operationId },
{ toolCallId: operationId },
{ dispatchStartedAt: fixture.originalTime + 1 },
{ request: { name: 'kilo.members', arguments: {} } },
{ request: { name: 'web.search', arguments: { query: 'changed' } } },
])('rejects changed signed operation input %#', async patch => {
const input = call('web.search', { query: 'kilo' });
await expect(authorizedInput({ ...input, ...patch }, capability(input))).rejects.toMatchObject({
code: 'FORBIDDEN',
});
});
it.each([
['SERVICE_UNAVAILABLE', 'unavailable_tool', true, false],
['PRECONDITION_FAILED', 'reauthorization_required', false, false],
['BAD_REQUEST', 'invalid_input', false, false],
['FORBIDDEN', 'access_revoked', false, false],
['TIMEOUT', 'storage_unavailable', true, false],
['TIMEOUT', 'outcome_unknown', false, true],
['PAYLOAD_TOO_LARGE', 'limit_exceeded', false, false],
['PAYLOAD_TOO_LARGE', 'outcome_unknown', false, true],
['BAD_REQUEST', 'invalid_input', false, true],
] as const)(
'sanitizes %s without merging recovery states %#',
(code, expected, retryable, uncertain) => {
const failure = new TRPCError({
code,
message: code === 'PRECONDITION_FAILED' ? 'reauthorization_required' : 'provider-secret',
cause: new Error('secret'),
});
const result = harnessOperationFailure(failure, uncertain);
expect(result).toMatchObject({ error: { code: expected, retryable } });
expect(JSON.stringify(result)).not.toContain('secret');
}
);
it.each(ErrorSchema.shape.code.options)('redacts extra fields from %s errors', code => {
const error = { code, message: 'secret', retryable: false, credentials: 'secret' };
const result = safeError(error);
expect(result).toEqual({ code, message: expect.any(String), retryable: false });
expect(JSON.stringify(result)).not.toContain('secret');
});
it('bounds UTF-8 output including JSON overhead', () => {
expect(bounded('é', 4)).toBe('é');
expect(() => bounded('é', 3)).toThrow(expect.objectContaining({ code: 'PAYLOAD_TOO_LARGE' }));
expect(bounded('a'.repeat(65534))).toHaveLength(65534);
expect(() => bounded('a'.repeat(65535))).toThrow(
expect.objectContaining({ code: 'PAYLOAD_TOO_LARGE' })
);
});
it.each([[], {}, null, ''])('preserves empty output %j', value => {
expect(bounded(value)).toEqual(value);
});
it.each([undefined, 1n])('classifies non-JSON output %# as invalid, not retryable', value => {
let failure: unknown;
try {
bounded(value);
} catch (error) {
failure = error;
}
expect(harnessOperationFailure(failure)).toMatchObject({
error: { code: 'invalid_output', retryable: false },
});
});
149 changes: 149 additions & 0 deletions apps/web/src/lib/agent-harness/operation-contract.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import 'server-only';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import type { ErrorSchema } from '@kilocode/agent-harness/contracts';
import { ToolRequestSchema } from '@kilocode/agent-harness/tools';
import { harnessInputDigest } from './authorization';

export const Id = z.uuid().transform(value => value.toLowerCase());
export const Time = z.int().nonnegative();
const identity = { conversationId: Id, operationId: Id };
// The Worker supplies these fields from the committed reservation and its owning run checkpoint.
const WebReservation = z.strictObject({
id: Id,
runId: Id,
toolCallId: Id,
startedAt: Time,
deadline: Time,
kind: z.literal('tool'),
status: z.literal('reserved'),
webRequest: z.literal(true),
});
export const HarnessOperationSchema = z.discriminatedUnion('type', [
z.strictObject({
type: z.literal(['execute', 'reconcile']),
...identity,
runId: Id,
toolCallId: Id,
request: ToolRequestSchema,
// Legacy reconciliation omits this time; retain absence until those stored attempts are gone.
dispatchStartedAt: Time.optional(),
reservation: WebReservation.optional(),
}),
z.strictObject({
type: z.literal('read'),
...identity,
purpose: z.enum(['read', 'import', 'project', 'drain']),
}),
z.strictObject({
type: z.literal('history'),
...identity,
limit: z.int().min(1).max(50).default(50),
}),
z.strictObject({
type: z.literal('projection'),
...identity,
projection: z.strictObject({
id: Id,
key: z.string().min(1),
role: z.enum(['user', 'assistant']),
content: z.string(),
clientId: z.string().nullable().default(null),
createdAt: z.iso.datetime(),
}),
}),
z.strictObject({ type: z.literal('retirement'), ...identity, generation: Time }),
]);
export type HarnessOperation = z.infer<typeof HarnessOperationSchema>;
export function harnessOperationScope(raw: unknown) {
const input = HarnessOperationSchema.parse(raw);
return {
audience: 'agent-harness:operations',
conversationId: input.conversationId,
operation: input.type,
definitionVersion: '1',
// Validate before serialization so malformed fields cannot disappear from the signed input.
inputDigest: harnessInputDigest(JSON.parse(JSON.stringify(input))),
dispatchId: input.operationId,
target: { kind: 'backend' as const },
};
}
export const messages = {
stale_revision: 'Refresh the conversation before continuing.',
command_conflict: 'This operation has different recorded input.',
access_revoked: 'Access to this conversation is unavailable.',
retired: 'This conversation is retired.',
storage_unavailable: 'The operation service is unavailable. Retry synchronization.',
unsupported_protocol: 'Update the client before continuing.',
unavailable_tool: 'This tool is unavailable in the current context.',
reauthorization_required: 'Reconnect the integration in this context.',
invalid_input: 'The operation input is invalid.',
invalid_output: 'The operation returned invalid output.',
limit_exceeded: 'The operation exceeds its limit.',
cancelled: 'The operation was cancelled.',
outcome_unknown: 'Check the recorded outcome; do not repeat this mutation.',
} satisfies Record<z.infer<typeof ErrorSchema>['code'], string>;
export const safeError = (error: z.infer<typeof ErrorSchema>) => ({
code: error.code,
message: messages[error.code],
retryable: error.retryable,
});
export function harnessOperationFailure(error: unknown, uncertain = false) {
const codes: Partial<Record<TRPCError['code'], z.infer<typeof ErrorSchema>['code']>> = {
FORBIDDEN: 'access_revoked',
UNAUTHORIZED: 'access_revoked',
BAD_REQUEST: 'invalid_input',
PRECONDITION_FAILED: 'unavailable_tool',
NOT_FOUND: 'unavailable_tool',
CONFLICT: 'command_conflict',
PAYMENT_REQUIRED: 'limit_exceeded',
PAYLOAD_TOO_LARGE: 'limit_exceeded',
UNPROCESSABLE_CONTENT: 'invalid_output',
SERVICE_UNAVAILABLE: 'unavailable_tool',
};
const rejection =
error instanceof TRPCError &&
[
'FORBIDDEN',
'UNAUTHORIZED',
'BAD_REQUEST',
'PRECONDITION_FAILED',
'PAYMENT_REQUIRED',
'CONFLICT',
].includes(error.code);
const code =
uncertain && !rejection
? 'outcome_unknown'
: error instanceof TRPCError
? error.code === 'PRECONDITION_FAILED' && error.message === 'reauthorization_required'
? 'reauthorization_required'
: (codes[error.code] ?? 'storage_unavailable')
: error instanceof z.ZodError
? 'invalid_output'
: 'storage_unavailable';
return {
error: safeError({
code,
message: '',
retryable:
code === 'storage_unavailable' ||
(code === 'unavailable_tool' &&
error instanceof TRPCError &&
error.code === 'SERVICE_UNAVAILABLE'),
}),
};
}
export function bounded<T>(value: T, limit = 64 * 1024): T {
let json: string | undefined;
try {
json = JSON.stringify(value);
} catch {
throw new TRPCError({ code: 'UNPROCESSABLE_CONTENT' });
}
if (json === undefined) throw new TRPCError({ code: 'UNPROCESSABLE_CONTENT' });
if (Buffer.byteLength(json, 'utf8') > limit) throw new TRPCError({ code: 'PAYLOAD_TOO_LARGE' });
return value;
}
export const invalid = () => {
throw new TRPCError({ code: 'BAD_REQUEST' });
};
Loading
Loading