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
147 changes: 147 additions & 0 deletions gate-engine/critique/provider-adapters/claude-subagent-stop.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { types as utilTypes } from 'node:util';
import {
PLAN_CRITIQUE_CALLBACK_IDENTITY_MAX_BYTES,
type PlanCritiqueCompletedCallbackV1,
} from '../capture-normalizer.mts';
import { PLAN_CRITIQUE_EXACT_RESPONSE_MAX_BYTES, sha256Bytes } from '../evidence-record.mts';

export const CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES = 1024;

export const CLAUDE_PLAN_CRITIQUE_SKIP_REASONS = [
'invalid_payload',
'unsupported_event',
'unsupported_agent_type',
'continuation_state_unavailable',
'work_identity_unavailable',
'callback_identity_unavailable',
'final_message_unavailable',
'final_message_too_large',
] as const;

export type ClaudePlanCritiqueSkipReason = (typeof CLAUDE_PLAN_CRITIQUE_SKIP_REASONS)[number];

/** `work_unbindable` must be durably quarantined before any runtime hook activation. */
export type ClaudePlanCritiqueSubagentStopResultV1 =
| { kind: 'ready'; callback: PlanCritiqueCompletedCallbackV1 }
| { kind: 'work_unbindable'; reason: 'hook_continuation'; workId: string }
| { kind: 'skipped'; reason: ClaudePlanCritiqueSkipReason };

export interface ClaudePlanCritiqueAdapterContextV1 {
repository: PlanCritiqueCompletedCallbackV1['repository'];
}

function plainRecord(value: unknown): Record<string, unknown> | null {
if (value === null || typeof value !== 'object') return null;
try {
if (utilTypes.isProxy(value) || Array.isArray(value)) return null;
const prototype = Object.getPrototypeOf(value);
if (prototype !== Object.prototype && prototype !== null) return null;
return value as Record<string, unknown>;
} catch {
return null;
}
}

function ownDataValue(record: Record<string, unknown>, key: string): unknown {
try {
const descriptor = Object.getOwnPropertyDescriptor(record, key);
return descriptor?.enumerable && Object.hasOwn(descriptor, 'value')
? descriptor.value
: undefined;
} catch {
return undefined;
}
}

function opaqueIdentifier(value: unknown): value is string {
if (
typeof value !== 'string' ||
value.length === 0 ||
value.length > CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES ||
Buffer.byteLength(value, 'utf8') > CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES ||
value.trim().length === 0
)
return false;
for (let index = 0; index < value.length; index += 1) {
const code = value.charCodeAt(index);
if (code <= 0x1f || (code >= 0x7f && code <= 0x9f)) return false;
}
return true;
}

function tupleHash(domain: string, values: readonly string[]): string {
return sha256Bytes(Buffer.from(JSON.stringify([domain, 1, ...values]), 'utf8'));
}

/** Stable turn identity shared by Claude SubagentStop capture and future Stop observation. */
export function deriveClaudePlanCritiqueWorkId(sessionId: string, promptId: string): string {
if (!opaqueIdentifier(sessionId) || !opaqueIdentifier(promptId))
throw new Error('invalid Claude plan critique work identity');
return `pcw1_${tupleHash('claude_plan_critique_work', [sessionId, promptId])}`;
}

function callbackIdentity(sessionId: string, promptId: string, agentId: string): string {
return JSON.stringify(['claude_plan_critique_subagent_stop', 1, sessionId, promptId, agentId]);
}

const skipped = (reason: ClaudePlanCritiqueSkipReason): ClaudePlanCritiqueSubagentStopResultV1 => ({
kind: 'skipped',
reason,
});

/**
* Adapt Claude's documented SubagentStop payload without reading transcripts or provider paths.
* The final message is the exact hook field text encoded as UTF-8, not original wire bytes.
*/
export function adaptClaudeFeatureCritiqueSubagentStop(
payload: unknown,
context: ClaudePlanCritiqueAdapterContextV1,
): ClaudePlanCritiqueSubagentStopResultV1 {
const record = plainRecord(payload);
if (!record) return skipped('invalid_payload');
if (ownDataValue(record, 'hook_event_name') !== 'SubagentStop')
return skipped('unsupported_event');
if (ownDataValue(record, 'agent_type') !== 'feature-critique')
return skipped('unsupported_agent_type');

const sessionId = ownDataValue(record, 'session_id');
const promptId = ownDataValue(record, 'prompt_id');
if (!opaqueIdentifier(sessionId) || !opaqueIdentifier(promptId))
return skipped('work_identity_unavailable');
const workId = deriveClaudePlanCritiqueWorkId(sessionId, promptId);

const stopHookActive = ownDataValue(record, 'stop_hook_active');
if (typeof stopHookActive !== 'boolean') return skipped('continuation_state_unavailable');
if (stopHookActive) return { kind: 'work_unbindable', reason: 'hook_continuation', workId };

const agentId = ownDataValue(record, 'agent_id');
if (!opaqueIdentifier(agentId)) return skipped('callback_identity_unavailable');
const derivedCallbackIdentity = callbackIdentity(sessionId, promptId, agentId);
if (
Buffer.byteLength(derivedCallbackIdentity, 'utf8') > PLAN_CRITIQUE_CALLBACK_IDENTITY_MAX_BYTES
)
return skipped('callback_identity_unavailable');

const finalMessage = ownDataValue(record, 'last_assistant_message');
if (typeof finalMessage !== 'string') return skipped('final_message_unavailable');
if (
finalMessage.length > PLAN_CRITIQUE_EXACT_RESPONSE_MAX_BYTES ||
Buffer.byteLength(finalMessage, 'utf8') > PLAN_CRITIQUE_EXACT_RESPONSE_MAX_BYTES
)
return skipped('final_message_too_large');
if (finalMessage.trim().length === 0) return skipped('final_message_unavailable');

return {
kind: 'ready',
callback: {
provider: 'claude',
callbackIdentity: derivedCallbackIdentity,
workId,
repository: { ...context.repository },
model: null,
promptHash: null,
providerCompletedAt: null,
exactResponse: Buffer.from(finalMessage, 'utf8'),
},
};
}
213 changes: 213 additions & 0 deletions gate-engine/critique/provider-adapters/claude-subagent-stop.test.mts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { describe, expect, it } from 'vitest';
import { temporaryRoot } from '../__tests__/evidence-store-fixture.mts';
import { REVIEWED_RESPONSE } from '../__tests__/response-fixture.mts';
import { capturePlanCritiqueCompletedCallback } from '../capture-normalizer.mts';
import { PLAN_CRITIQUE_EXACT_RESPONSE_MAX_BYTES, sha256Bytes } from '../evidence-record.mts';
import {
readPlanCritiqueExactResponse,
readPlanCritiqueProjection,
readPlanCritiqueTranscript,
} from '../evidence-store.mts';
import {
adaptClaudeFeatureCritiqueSubagentStop,
CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES,
deriveClaudePlanCritiqueWorkId,
} from './claude-subagent-stop.mts';

const response = JSON.stringify(REVIEWED_RESPONSE);
const repository = {
fingerprint: sha256Bytes(Buffer.from('repository', 'utf8')),
fingerprintSource: 'canonical_remote' as const,
branch: 'codex/example',
head: 'a'.repeat(40),
};

function payload(overrides: Record<string, unknown> = {}): Record<string, unknown> {
return {
hook_event_name: 'SubagentStop',
session_id: 'session-1',
prompt_id: 'prompt-1',
stop_hook_active: false,
agent_id: 'agent-1',
agent_type: 'feature-critique',
last_assistant_message: response,
transcript_path: '/untrusted/main.jsonl',
agent_transcript_path: '/untrusted/subagent.jsonl',
permission_mode: 'plan',
model: 'forged-model',
...overrides,
};
}

const adapt = (value: unknown) => adaptClaudeFeatureCritiqueSubagentStop(value, { repository });

describe('adaptClaudeFeatureCritiqueSubagentStop', () => {
it('maps only documented capture fields and preserves the final message text as UTF-8', () => {
const result = adapt(payload());
expect(result.kind).toBe('ready');
if (result.kind !== 'ready') throw new Error('expected a ready callback');

expect(result.callback).toMatchObject({
provider: 'claude',
workId: deriveClaudePlanCritiqueWorkId('session-1', 'prompt-1'),
repository,
model: null,
promptHash: null,
providerCompletedAt: null,
});
expect(result.callback.callbackIdentity).toBe(
JSON.stringify(['claude_plan_critique_subagent_stop', 1, 'session-1', 'prompt-1', 'agent-1']),
);
expect(Buffer.from(result.callback.exactResponse).toString('utf8')).toBe(response);
expect(result.callback).not.toHaveProperty('opaqueTranscript');
expect(result.callback).not.toHaveProperty('permission_mode');
});

it('shares work identity across a fresh recheck while keeping callback identity distinct', () => {
const first = adapt(payload({ agent_id: 'agent-1' }));
const second = adapt(payload({ agent_id: 'agent-2' }));
expect(first.kind).toBe('ready');
expect(second.kind).toBe('ready');
if (first.kind !== 'ready' || second.kind !== 'ready')
throw new Error('expected ready callbacks');
expect(second.callback.workId).toBe(first.callback.workId);
expect(second.callback.callbackIdentity).not.toBe(first.callback.callbackIdentity);
});

it('derives domain-separated, delimiter-safe, deterministic work identities', () => {
const workId = deriveClaudePlanCritiqueWorkId('a:b', 'c');
expect(workId).toMatch(/^pcw1_[0-9a-f]{64}$/);
expect(deriveClaudePlanCritiqueWorkId('a:b', 'c')).toBe(workId);
expect(deriveClaudePlanCritiqueWorkId('a', 'b:c')).not.toBe(workId);
expect(() => deriveClaudePlanCritiqueWorkId('session\u0000', 'prompt')).toThrow(
/invalid Claude plan critique work identity/,
);
});

it.each([
{ value: null, reason: 'invalid_payload' },
{ value: [], reason: 'invalid_payload' },
{ value: payload({ hook_event_name: 'Stop' }), reason: 'unsupported_event' },
{ value: payload({ agent_type: 'feature-critique-copy' }), reason: 'unsupported_agent_type' },
{
value: payload({ stop_hook_active: undefined }),
reason: 'continuation_state_unavailable',
},
{ value: payload({ prompt_id: undefined }), reason: 'work_identity_unavailable' },
{ value: payload({ session_id: 'session\n2' }), reason: 'work_identity_unavailable' },
{ value: payload({ agent_id: '' }), reason: 'callback_identity_unavailable' },
{ value: payload({ last_assistant_message: null }), reason: 'final_message_unavailable' },
{ value: payload({ last_assistant_message: ' ' }), reason: 'final_message_unavailable' },
])('skips unsupported or incomplete payloads with $reason', ({ value, reason }) => {
expect(adapt(value)).toEqual({ kind: 'skipped', reason });
});

it('marks the whole work unbindable when another hook continued the subagent', () => {
const continued = payload({ stop_hook_active: true });
for (const key of ['agent_id', 'last_assistant_message'])
Object.defineProperty(continued, key, {
enumerable: true,
get: () => {
throw new Error('continued output must not be inspected');
},
});
expect(adapt(continued)).toEqual({
kind: 'work_unbindable',
reason: 'hook_continuation',
workId: deriveClaudePlanCritiqueWorkId('session-1', 'prompt-1'),
});
});

it('bounds opaque identities and final output before allocating capture bytes', () => {
expect(
adapt(payload({ session_id: 's'.repeat(CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES + 1) })),
).toEqual({ kind: 'skipped', reason: 'work_identity_unavailable' });
expect(
adapt(
payload({
last_assistant_message: 'x'.repeat(PLAN_CRITIQUE_EXACT_RESPONSE_MAX_BYTES + 1),
}),
),
).toEqual({ kind: 'skipped', reason: 'final_message_too_large' });
});

it('bounds the serialized callback tuple after JSON escaping', () => {
expect(
adapt(
payload({
session_id: '\\'.repeat(CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES),
prompt_id: '\\'.repeat(CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES),
agent_id: '\\'.repeat(CLAUDE_PLAN_CRITIQUE_IDENTITY_MAX_BYTES),
}),
),
).toEqual({ kind: 'skipped', reason: 'callback_identity_unavailable' });
});

it('ignores accessors instead of executing untrusted payload code', () => {
const value = Object.create(null) as Record<string, unknown>;
Object.defineProperty(value, 'hook_event_name', {
enumerable: true,
get: () => {
throw new Error('must not run');
},
});
expect(adapt(value)).toEqual({ kind: 'skipped', reason: 'unsupported_event' });
});

it('rejects live and revoked Proxies without executing their traps', () => {
let traps = 0;
const live = new Proxy(payload(), {
getPrototypeOf: () => {
traps += 1;
throw new Error('must not run');
},
});
expect(adapt(live)).toEqual({ kind: 'skipped', reason: 'invalid_payload' });
expect(traps).toBe(0);

const revoked = Proxy.revocable(payload(), {});
revoked.revoke();
expect(adapt(revoked.proxy)).toEqual({ kind: 'skipped', reason: 'invalid_payload' });
});

it('feeds exact valid and malformed responses into the existing capture transaction', () => {
for (const finalMessage of [response, '{']) {
const result = adapt(payload({ last_assistant_message: finalMessage }));
if (result.kind !== 'ready') throw new Error('expected a ready callback');
const root = temporaryRoot();
const captured = capturePlanCritiqueCompletedCallback(result.callback, { root });

expect(
readPlanCritiqueExactResponse(captured.record.critiqueId, { root })?.toString('utf8'),
).toBe(finalMessage);
expect(captured.record.execution).toMatchObject({
provider: 'claude',
model: null,
promptHash: null,
});
expect(readPlanCritiqueTranscript(captured.record.critiqueId, { root })).toBeNull();
if (finalMessage === response) {
expect(captured.record.contract.state).toBe('valid');
expect(readPlanCritiqueProjection(captured.record.critiqueId, { root })).not.toBeNull();
} else {
expect(captured.record.contract).toMatchObject({
state: 'invalid',
error: { code: 'INVALID_JSON', path: '$' },
});
expect(readPlanCritiqueProjection(captured.record.critiqueId, { root })).toBeNull();
}
}
});

it('replays the same provider callback deterministically', () => {
const first = adapt(payload());
const replay = adapt(payload());
if (first.kind !== 'ready' || replay.kind !== 'ready')
throw new Error('expected ready callbacks');
const root = temporaryRoot();
const stored = capturePlanCritiqueCompletedCallback(first.callback, { root });
const repeated = capturePlanCritiqueCompletedCallback(replay.callback, { root });
expect(repeated.record.critiqueId).toBe(stored.record.critiqueId);
expect(repeated.record.execution.callbackHash).toBe(stored.record.execution.callbackHash);
});
});
Loading