From 8e95d499ca7e8784237f37d28952908c7fa16d07 Mon Sep 17 00:00:00 2001 From: Brian O'Kelley Date: Sat, 5 Sep 2026 19:02:57 +0000 Subject: [PATCH] feat(addie): add dormant direct replay contract --- server/src/addie/bolt-app.ts | 38 +- server/src/addie/claude-client.ts | 347 +++++++++++++++++- server/src/addie/config-version.ts | 2 +- .../unit/addie/direct-replay-contract.test.ts | 261 +++++++++++++ 4 files changed, 630 insertions(+), 18 deletions(-) create mode 100644 server/tests/unit/addie/direct-replay-contract.test.ts diff --git a/server/src/addie/bolt-app.ts b/server/src/addie/bolt-app.ts index 85cafa8c69..64bb6fb958 100644 --- a/server/src/addie/bolt-app.ts +++ b/server/src/addie/bolt-app.ts @@ -33,7 +33,14 @@ import { deliverAndRecordDirectMessage, prepareSlackDirectMessagePost } from './ const logger = createLogger('addie-bolt-app'); import { sanitizeSpeakerName } from './prompts.js'; import { captureEvent } from '../utils/posthog.js'; -import { AddieClaudeClient, ADMIN_MAX_ITERATIONS, CERTIFICATION_MAX_ITERATIONS, type AddieResponse, type ProcessMessageOptions, type UserScopedToolsResult } from './claude-client.js'; +import { + AddieClaudeClient, + ADMIN_MAX_ITERATIONS, + CERTIFICATION_MAX_ITERATIONS, + type AddieResponse, + type ProcessMessageOptions, + type UserScopedToolsResult, +} from './claude-client.js'; import { buildSlackCostOptions, SLACK_COST_CHANNEL_INFO_MAX_AGE_MS, @@ -3776,9 +3783,7 @@ export async function buildChannelResponseInvocation(input: { } } - return { - requestTools: profileTools, - processOptions: { + const processOptions: ProcessMessageOptions = { ...(userIsAdmin ? { maxIterations: ADMIN_MAX_ITERATIONS } : {}), ...(modelOverride ? { modelOverride } : {}), requestContext, @@ -3800,7 +3805,30 @@ export async function buildChannelResponseInvocation(input: { maxIterations: 4, } : {}), - }, + ...(officialDocsProfile && channelContext?.viewing_channel_is_private === false + ? { + // The private client assembly will mint an opaque capability from + // these authenticated channel facts. It has no dispatch consumer + // in this PR and is deliberately absent from persisted traces. + directReplayContractFacts: { + surface: 'slack_channel', + isAdmin: userIsAdmin, + threadId, + channelPrivacy: 'public', + replayPrincipal: userId, + caseId: threadId, + requestId: `${threadId}:${userId}`, + selectedToolSetNames: selectedToolSets, + selectedToolNames: OFFICIAL_DOCS_ALLOWED_TOOLS, + expiresAt: Date.now() + 60_000, + }, + } + : {}), + }; + + return { + requestTools: profileTools, + processOptions, effectiveModel, selectedToolSets, isAdmin: userIsAdmin, diff --git a/server/src/addie/claude-client.ts b/server/src/addie/claude-client.ts index cda104779f..83fdb8de01 100644 --- a/server/src/addie/claude-client.ts +++ b/server/src/addie/claude-client.ts @@ -525,6 +525,236 @@ export interface RequestTools { handlers: Map; } +/** + * The immutable policy label carried by an in-memory direct-replay contract. + * This is deliberately separate from evaluator provenance: a digest is useful + * for audit, but never establishes authority to replay a production request. + */ +export const DIRECT_REPLAY_ASSEMBLY_POLICY_VERSION = 'direct-replay-assembly:v1' as const; + +/** Facts authenticated by the Slack/channel assembly before a dormant replay capability is minted. */ +export interface DirectReplayContractFacts { + surface: 'slack_channel'; + isAdmin: boolean; + threadId: string; + channelPrivacy: 'public'; + replayPrincipal: string; + caseId: string; + requestId: string; + selectedToolSetNames: readonly string[]; + selectedToolNames: readonly string[]; + expiresAt: number; + abortSignal?: AbortSignal; +} + +/** Evidence only. None of these values are consulted when admitting a contract. */ +export interface DirectReplayContractAudit { + assemblyPolicyVersion: typeof DIRECT_REPLAY_ASSEMBLY_POLICY_VERSION; + definitionSha256: string; + factsSha256: string; +} + +/** + * Opaque in-memory capability. Runtime authority is private WeakMap + * membership below; this visible audit shape is intentionally forgeable. + */ +export interface DirectReplayContract { + readonly audit: DirectReplayContractAudit; +} + +export type DirectReplayContractConsumption = + | { admitted: true } + | { + admitted: false; + reason: 'unknown_contract' | 'already_consumed' | 'expired' | 'aborted' | 'assembly_drift'; + }; + +interface DirectToolRegistryAssembly { + definitions: AddieTool[]; + handlers: Map; +} + +interface DirectReplayContractRecord { + consumed: boolean; + assemblyPolicyVersion: typeof DIRECT_REPLAY_ASSEMBLY_POLICY_VERSION; + selectedToolSetNames: readonly string[]; + selectedToolSetsAreCurrent: () => boolean; + noProviderToolsAreCurrent: () => boolean; + facts: DirectReplayContractFacts; + factSnapshot: Omit; + definitionSnapshots: readonly AddieTool[]; + handlerSlots: ReadonlyMap; + sourceRegistriesAreCurrent: () => boolean; + assembleCurrent: () => DirectToolRegistryAssembly; +} + +// No brand, constructor, serialized hash, or public registrar can create +// membership in this map. A copied audit object is evidence only. +const directReplayContracts = new WeakMap(); + +function hasDuplicateNames(tools: readonly AddieTool[]): boolean { + const names = new Set(); + for (const tool of tools) { + if (names.has(tool.name)) return true; + names.add(tool.name); + } + return false; +} + +function hasOnlyPlainData(value: unknown, seen = new WeakSet()): boolean { + if (value === null || ['string', 'number', 'boolean'].includes(typeof value)) return true; + if (typeof value !== 'object') return false; + const object = value as object; + if (seen.has(object)) return false; + seen.add(object); + try { + const prototype = Object.getPrototypeOf(object); + if (prototype !== Object.prototype && prototype !== null && !Array.isArray(object)) return false; + for (const key of Reflect.ownKeys(object)) { + if (typeof key !== 'string') return false; + const descriptor = Object.getOwnPropertyDescriptor(object, key); + if (!descriptor || !('value' in descriptor) || !hasOnlyPlainData(descriptor.value, seen)) return false; + } + return true; + } catch { + // Proxies and hostile accessors must not become a replay capability. + return false; + } +} + +function immutableDefinitionSnapshots(definitions: readonly AddieTool[]): readonly AddieTool[] | null { + if (!hasOnlyPlainData(definitions)) return null; + try { + return Object.freeze(definitions.map((definition) => Object.freeze(structuredClone(definition)))); + } catch { + return null; + } +} + +function sameDefinitionSnapshots( + expected: readonly AddieTool[], + actual: readonly AddieTool[], +): boolean { + const snapshots = immutableDefinitionSnapshots(actual); + if (!snapshots || snapshots.length !== expected.length) return false; + return snapshots.every((snapshot, index) => JSON.stringify(snapshot) === JSON.stringify(expected[index])); +} + +function sameStringList(expected: readonly string[], actual: readonly string[]): boolean { + return expected.length === actual.length && expected.every((value, index) => value === actual[index]); +} + +function factsAreCurrent(expected: DirectReplayContractFacts): boolean { + return expected.surface === 'slack_channel' + && expected.isAdmin === false + && expected.channelPrivacy === 'public' + && expected.threadId.trim().length > 0 + && expected.replayPrincipal.trim().length > 0 + && expected.caseId.trim().length > 0 + && expected.requestId.trim().length > 0 + && Number.isSafeInteger(expected.expiresAt) + && expected.expiresAt > 0; +} + +function snapshotFacts(facts: DirectReplayContractFacts): Omit { + return Object.freeze({ + surface: facts.surface, + isAdmin: facts.isAdmin, + threadId: facts.threadId, + channelPrivacy: facts.channelPrivacy, + replayPrincipal: facts.replayPrincipal, + caseId: facts.caseId, + requestId: facts.requestId, + selectedToolSetNames: Object.freeze([...facts.selectedToolSetNames]), + selectedToolNames: Object.freeze([...facts.selectedToolNames]), + expiresAt: facts.expiresAt, + }); +} + +function sameFacts( + expected: Omit, + actual: DirectReplayContractFacts, +): boolean { + return expected.surface === actual.surface + && expected.isAdmin === actual.isAdmin + && expected.threadId === actual.threadId + && expected.channelPrivacy === actual.channelPrivacy + && expected.replayPrincipal === actual.replayPrincipal + && expected.caseId === actual.caseId + && expected.requestId === actual.requestId + && expected.expiresAt === actual.expiresAt + && sameStringList(expected.selectedToolSetNames, actual.selectedToolSetNames) + && sameStringList(expected.selectedToolNames, actual.selectedToolNames); +} + +function contractAudit( + definitions: readonly AddieTool[], + facts: DirectReplayContractFacts, +): DirectReplayContractAudit { + // These hashes are deliberately never read by consumeDirectReplayContract. + // Definition data excludes handlers; function identity is bound by reference. + return Object.freeze({ + assemblyPolicyVersion: DIRECT_REPLAY_ASSEMBLY_POLICY_VERSION, + definitionSha256: createHash('sha256').update(JSON.stringify(definitions), 'utf8').digest('hex'), + factsSha256: createHash('sha256').update(JSON.stringify({ + surface: facts.surface, + isAdmin: facts.isAdmin, + threadId: facts.threadId, + channelPrivacy: facts.channelPrivacy, + replayPrincipal: facts.replayPrincipal, + caseId: facts.caseId, + requestId: facts.requestId, + selectedToolSetNames: facts.selectedToolSetNames, + selectedToolNames: facts.selectedToolNames, + expiresAt: facts.expiresAt, + }), 'utf8').digest('hex'), + }); +} + +/** + * Consume a capability once. This intentionally does not expose handlers or + * dispatch a provider/tool: connecting it to replay is a later, reviewed step. + */ +export function consumeDirectReplayContract( + contract: unknown, + now: number = Date.now(), +): DirectReplayContractConsumption { + if (!contract || typeof contract !== 'object') return { admitted: false, reason: 'unknown_contract' }; + const record = directReplayContracts.get(contract); + if (!record) return { admitted: false, reason: 'unknown_contract' }; + if (record.consumed) return { admitted: false, reason: 'already_consumed' }; + // A valid member is consumed before any mutable-state check, so repairing a + // changed registry after a failed attempt cannot resurrect the capability. + record.consumed = true; + if (!Number.isFinite(now) || now >= record.facts.expiresAt) return { admitted: false, reason: 'expired' }; + if (record.facts.abortSignal?.aborted) return { admitted: false, reason: 'aborted' }; + if ( + record.assemblyPolicyVersion !== DIRECT_REPLAY_ASSEMBLY_POLICY_VERSION + || !record.noProviderToolsAreCurrent() + || !record.selectedToolSetsAreCurrent() + || !sameStringList(record.selectedToolSetNames, record.facts.selectedToolSetNames) + || !factsAreCurrent(record.facts) + || !sameFacts(record.factSnapshot, record.facts) + ) { + return { admitted: false, reason: 'assembly_drift' }; + } + let current: DirectToolRegistryAssembly; + try { + current = record.assembleCurrent(); + } catch { + return { admitted: false, reason: 'assembly_drift' }; + } + if ( + hasDuplicateNames(current.definitions) + || !record.sourceRegistriesAreCurrent() + || !sameStringList(record.facts.selectedToolNames, current.definitions.map((tool) => tool.name)) + || !sameDefinitionSnapshots(record.definitionSnapshots, current.definitions) + || current.definitions.some((tool) => current.handlers.get(tool.name) !== record.handlerSlots.get(tool.name)) + || current.handlers.size !== record.handlerSlots.size + ) return { admitted: false, reason: 'assembly_drift' }; + return { admitted: true }; +} + /** * Result from createUserScopedTools including admin status */ @@ -541,6 +771,14 @@ export interface ProcessMessageOptions { executionMode?: AddieExecutionMode; /** Exclude provider-managed tools such as web search for this request only. */ disableServerTools?: boolean; + /** + * Slack's authenticated channel assembly may supply facts for the dormant + * direct-replay capability. The capability itself is minted privately only + * while this client's normal request assembly resolves definitions/handlers. + */ + directReplayContractFacts?: DirectReplayContractFacts; + /** Internal observer for an opaque, already-issued contract; it cannot mint one. */ + onDirectReplayContract?: (contract: DirectReplayContract) => void; /** * Exact request-local custom-tool allowlist. When present, global and * request-scoped tools outside this list are omitted before prompt sizing, @@ -1191,6 +1429,90 @@ export class AddieClaudeClient { return toolNames.every((name) => definitions.has(name) && this.toolHandlers.has(name)); } + /** + * Resolve the provider-neutral custom-tool intersection for one request. + * The order and same-name winner semantics intentionally match the existing + * live request path: request-local definitions and handlers win globally + * registered entries of the same name. + */ + private assembleDirectToolRegistry( + requestTools: RequestTools | undefined, + allowedToolNames: readonly string[] | undefined, + ): DirectToolRegistryAssembly { + const allowed = allowedToolNames ? new Set(allowedToolNames) : null; + return { + definitions: mergeAddieToolDefinitions(this.tools, requestTools?.tools, allowedToolNames), + handlers: new Map( + [...this.toolHandlers, ...(requestTools?.handlers || [])] + .filter(([name]) => !allowed || allowed.has(name)), + ), + }; + } + + /** + * Mint an opaque, in-memory capability after the exact production registry + * merge. This remains private so no caller can register arbitrary + * definitions/handlers as production replay authority. + */ + private mintDirectReplayContract( + requestTools: RequestTools | undefined, + options: ProcessMessageOptions, + facts: DirectReplayContractFacts, + assembly: DirectToolRegistryAssembly, + requestWebSearchEnabled: boolean, + ): DirectReplayContract | undefined { + const definitionNames = assembly.definitions.map((tool) => tool.name); + const snapshots = immutableDefinitionSnapshots(assembly.definitions); + const globalSnapshots = immutableDefinitionSnapshots(this.tools); + const requestSnapshots = immutableDefinitionSnapshots(requestTools?.tools ?? []); + const selectedSets = options.selectedToolSetNames ?? []; + const selectedSetSnapshot = Object.freeze([...selectedSets]); + const noProviderTools = options.disableServerTools === true && !requestWebSearchEnabled; + if ( + !snapshots + || !globalSnapshots + || !requestSnapshots + || hasDuplicateNames(assembly.definitions) + || hasDuplicateNames(this.tools) + || hasDuplicateNames(requestTools?.tools ?? []) + || (options.executionMode ?? 'production') !== 'production' + || !noProviderTools + || !factsAreCurrent(facts) + || !sameStringList(facts.selectedToolSetNames, selectedSets) + || !sameStringList(facts.selectedToolNames, definitionNames) + || assembly.definitions.some((tool) => typeof assembly.handlers.get(tool.name) !== 'function') + || assembly.handlers.size !== assembly.definitions.length + || [...assembly.handlers.keys()].some((name) => !definitionNames.includes(name)) + ) return undefined; + + const contract: DirectReplayContract = Object.freeze({ + audit: contractAudit(snapshots, facts), + }); + directReplayContracts.set(contract, { + consumed: false, + assemblyPolicyVersion: DIRECT_REPLAY_ASSEMBLY_POLICY_VERSION, + selectedToolSetNames: selectedSetSnapshot, + selectedToolSetsAreCurrent: () => sameStringList( + selectedSetSnapshot, + options.selectedToolSetNames ?? [], + ), + noProviderToolsAreCurrent: () => options.disableServerTools === true, + facts, + factSnapshot: snapshotFacts(facts), + definitionSnapshots: snapshots, + handlerSlots: new Map(assembly.definitions.map((tool) => [ + tool.name, + assembly.handlers.get(tool.name)!, + ])), + sourceRegistriesAreCurrent: () => !hasDuplicateNames(this.tools) + && !hasDuplicateNames(requestTools?.tools ?? []) + && sameDefinitionSnapshots(globalSnapshots, this.tools) + && sameDefinitionSnapshots(requestSnapshots, requestTools?.tools ?? []), + assembleCurrent: () => this.assembleDirectToolRegistry(requestTools, options.allowedToolNames), + }); + return contract; + } + /** * Copy the registered tool surface into a provider-isolated client. This is * deliberately not a production selector: alternate providers remain @@ -1221,18 +1543,19 @@ export class AddieClaudeClient { && !isIsolatedExecution(options) && options?.disableServerTools !== true; const effectiveModel = options?.modelOverride ?? this.model; - const allowedToolNames = options?.allowedToolNames - ? new Set(options.allowedToolNames) - : null; - const allTools = mergeAddieToolDefinitions( - this.tools, - requestTools?.tools, - options?.allowedToolNames, - ); - const allHandlers = new Map( - [...this.toolHandlers, ...(requestTools?.handlers || [])] - .filter(([name]) => !allowedToolNames || allowedToolNames.has(name)), - ); + const directTools = this.assembleDirectToolRegistry(requestTools, options?.allowedToolNames); + const allTools = directTools.definitions; + const allHandlers = directTools.handlers; + const directReplayContract = options?.directReplayContractFacts + ? this.mintDirectReplayContract( + requestTools, + options, + options.directReplayContractFacts, + directTools, + requestWebSearchEnabled, + ) + : undefined; + if (directReplayContract) options?.onDirectReplayContract?.(directReplayContract); const promptStart = Date.now(); const systemBlocks = this.buildSystemBlocks( diff --git a/server/src/addie/config-version.ts b/server/src/addie/config-version.ts index a6b5310520..f7c7580e0e 100644 --- a/server/src/addie/config-version.ts +++ b/server/src/addie/config-version.ts @@ -30,7 +30,7 @@ import { loadRules, loadResponseStyle } from './rules/index.js'; * Format: YYYY.MM.N where N is incremented for multiple changes in a month * Example: 2025.01.1, 2025.01.2, 2025.02.1 */ -export const CODE_VERSION = '2026.09.38'; +export const CODE_VERSION = '2026.09.39'; // Types export interface ConfigVersion { diff --git a/server/tests/unit/addie/direct-replay-contract.test.ts b/server/tests/unit/addie/direct-replay-contract.test.ts new file mode 100644 index 0000000000..819c115642 --- /dev/null +++ b/server/tests/unit/addie/direct-replay-contract.test.ts @@ -0,0 +1,261 @@ +import { describe, expect, it, vi } from 'vitest'; +import { + AddieClaudeClient, + consumeDirectReplayContract, + type DirectReplayContract, + type DirectReplayContractFacts, + type RequestTools, +} from '../../../src/addie/claude-client.js'; +import { OFFICIAL_DOCS_ALLOWED_TOOLS } from '../../../src/addie/jobs/shadow-replay-cohort.js'; +import { KNOWLEDGE_TOOLS } from '../../../src/addie/mcp/knowledge-search.js'; +import type { ModelRequest } from '../../../src/addie/model-providers/model-provider.js'; +import type { AddieTool } from '../../../src/addie/types.js'; + +function tool(name: string, description = `${name} description`): AddieTool { + return { + name, + description, + replaySafety: 'pure_local', + input_schema: { type: 'object', properties: {} }, + }; +} + +function facts(names: readonly string[], overrides: Partial = {}): DirectReplayContractFacts { + return { + surface: 'slack_channel', + isAdmin: false, + threadId: 'thread-1', + channelPrivacy: 'public', + replayPrincipal: 'U_REPLAY', + caseId: 'case-1', + requestId: 'request-1', + selectedToolSetNames: ['knowledge'], + selectedToolNames: names, + expiresAt: Date.now() + 60_000, + ...overrides, + }; +} + +function mintContract( + client: AddieClaudeClient, + local: RequestTools, + contractFacts: DirectReplayContractFacts, + options: { + disableServerTools: true; + selectedToolSetNames: readonly string[]; + allowedToolNames?: readonly string[]; + }, +): DirectReplayContract { + let contract: DirectReplayContract | undefined; + // This is the real private first-invocation assembly, not a test factory. + // prepareMessageInvocation stops before SDK dispatch and invokes no handler. + client.prepareMessageInvocation('authenticated Slack request', undefined, local, undefined, { + ...options, + directReplayContractFacts: contractFacts, + onDirectReplayContract: (issued) => { contract = issued; }, + }); + if (!contract) throw new Error('fixture failed to mint a production-assembled contract'); + return contract; +} + +function contractFixture(): { + client: AddieClaudeClient; + local: RequestTools; + localDefinition: AddieTool; + localHandler: ReturnType; + contract: DirectReplayContract; + contractFacts: DirectReplayContractFacts; + options: { + disableServerTools: true; + selectedToolSetNames: string[]; + allowedToolNames: string[]; + }; +} { + const client = new AddieClaudeClient('unused'); + client.registerTool(tool('global_only'), vi.fn(async () => 'global')); + const localDefinition = tool('local_only'); + const localHandler = vi.fn(async () => 'local'); + const local: RequestTools = { + tools: [localDefinition, tool('second_local')], + handlers: new Map([ + ['local_only', localHandler], + ['second_local', vi.fn(async () => 'second')], + ]), + }; + const contractFacts = facts(['global_only', 'local_only', 'second_local']); + const options = { + disableServerTools: true, + selectedToolSetNames: ['knowledge'], + allowedToolNames: ['global_only', 'local_only', 'second_local'], + } satisfies { + disableServerTools: true; + selectedToolSetNames: string[]; + allowedToolNames: string[]; + }; + const contract = mintContract(client, local, contractFacts, options); + return { client, local, localDefinition, localHandler, contract, contractFacts, options }; +} + +function capturingClient(): { client: AddieClaudeClient; prepared: ModelRequest[] } { + const prepared: ModelRequest[] = []; + const provider = { + id: 'anthropic', + capabilities: { + streaming: false, + structuredOutput: true, + reasoning: true, + reasoningEfforts: ['provider_default', 'none', 'low', 'medium', 'high'], + customTools: true, + providerWebSearch: false, + imageInput: false, + documentInput: false, + }, + prepare: vi.fn((request: ModelRequest) => { + prepared.push(request); + return { + provider: 'anthropic', + model: request.model, + capabilities: provider.capabilities, + providerRequest: request, + }; + }), + respond: vi.fn(), + }; + return { + client: new AddieClaudeClient('unused', undefined, undefined, { provider: provider as never }), + prepared, + }; +} + +describe('direct replay contract', () => { + it('preserves the live direct request-local winner while remaining dormant', () => { + const { client, prepared } = capturingClient(); + const globalHandler = vi.fn(async () => 'global handler'); + const localHandler = vi.fn(async () => 'local handler'); + client.registerTool(tool('same_name', 'global definition'), globalHandler); + const local: RequestTools = { + tools: [tool('same_name', 'request-local definition'), tool('local_only')], + handlers: new Map([['same_name', localHandler], ['local_only', vi.fn(async () => 'local')]]), + }; + const options = { + disableServerTools: true, + selectedToolSetNames: ['knowledge'], + allowedToolNames: ['same_name', 'local_only'], + } as const; + const before = client.prepareMessageInvocation('live direct Slack message', undefined, local, undefined, options); + const contract = mintContract(client, local, facts(['same_name', 'local_only']), options); + const after = client.prepareMessageInvocation('live direct Slack message', undefined, local, undefined, options); + + expect(contract).toBeDefined(); + expect(before.tool_schemas).toEqual(after.tool_schemas); + expect(after.tool_schemas.map(({ name }) => name)).toEqual(['same_name', 'local_only']); + expect(prepared).toHaveLength(3); + expect(prepared.every((request) => request.tools.map(({ name }) => name).join(',') === 'same_name,local_only')).toBe(true); + expect(prepared[0]?.tools[0]).toMatchObject({ name: 'same_name', description: 'request-local definition' }); + expect(consumeDirectReplayContract(contract)).toEqual({ admitted: true }); + // The assembly/capability path is intentionally non-dispatching. + expect(globalHandler).not.toHaveBeenCalled(); + expect(localHandler).not.toHaveBeenCalled(); + }); + + it('admits the exact official-docs two-tool global profile only', () => { + const client = new AddieClaudeClient('unused'); + for (const name of OFFICIAL_DOCS_ALLOWED_TOOLS) { + const definition = KNOWLEDGE_TOOLS.find((candidate) => candidate.name === name); + if (!definition) throw new Error(`Missing ${name}`); + client.registerTool(definition, vi.fn(async () => 'not dispatched')); + } + const contract = mintContract(client, { tools: [], handlers: new Map() }, facts(OFFICIAL_DOCS_ALLOWED_TOOLS), { + disableServerTools: true, + selectedToolSetNames: ['knowledge'], + allowedToolNames: OFFICIAL_DOCS_ALLOWED_TOOLS, + }); + + expect(contract).toBeDefined(); + expect(consumeDirectReplayContract(contract)).toEqual({ admitted: true }); + }); + + it.each([ + ['missing definitions', (fixture: ReturnType) => fixture.local.tools.pop()], + ['extra definitions', (fixture: ReturnType) => fixture.local.tools.push(tool('extra'))], + ['reordered definitions', (fixture: ReturnType) => fixture.local.tools.reverse()], + ['duplicate definitions', (fixture: ReturnType) => fixture.local.tools.push(fixture.localDefinition)], + ['mutated definitions', (fixture: ReturnType) => { fixture.localDefinition.description = 'mutated'; }], + ['swapped handlers', (fixture: ReturnType) => fixture.local.handlers.set('local_only', vi.fn(async () => 'swapped'))], + ['missing handlers', (fixture: ReturnType) => fixture.local.handlers.delete('local_only')], + ['restamped facts', (fixture: ReturnType) => { fixture.contractFacts.caseId = 'case-2'; }], + ['restamped policy', (fixture: ReturnType) => fixture.options.selectedToolSetNames.push('admin')], + ] as const)('fails closed for post-mint %s', (_name, mutate) => { + const fixture = contractFixture(); + mutate(fixture); + expect(consumeDirectReplayContract(fixture.contract)).toEqual({ admitted: false, reason: 'assembly_drift' }); + }); + + it('rejects accessor and Proxy definitions before any capability is minted', () => { + const client = new AddieClaudeClient('unused'); + const accessor = tool('accessor'); + Object.defineProperty(accessor, 'description', { + enumerable: true, + get: () => 'must not be accepted', + }); + const accessorTools: RequestTools = { + tools: [accessor], + handlers: new Map([['accessor', vi.fn(async () => 'never')]]), + }; + const proxied = new Proxy(tool('proxied'), {}); + const proxyTools: RequestTools = { + tools: [proxied], + handlers: new Map([['proxied', vi.fn(async () => 'never')]]), + }; + const options = { disableServerTools: true, selectedToolSetNames: ['knowledge'] } as const; + + let issued: DirectReplayContract | undefined; + client.prepareMessageInvocation('accessor', undefined, accessorTools, undefined, { + ...options, + directReplayContractFacts: facts(['accessor']), + onDirectReplayContract: (contract) => { issued = contract; }, + }); + expect(issued).toBeUndefined(); + client.prepareMessageInvocation('proxy', undefined, proxyTools, undefined, { + ...options, + directReplayContractFacts: facts(['proxied']), + onDirectReplayContract: (contract) => { issued = contract; }, + }); + expect(issued).toBeUndefined(); + }); + + it('treats visible brands, prototypes, hashes, and serialized copies as evidence only', () => { + const { contract } = contractFixture(); + class CopiedProductionLookingContract {} + const copied = Object.assign(Object.create(CopiedProductionLookingContract.prototype), contract); + const serialized = JSON.parse(JSON.stringify(contract)); + const sameHashes = { audit: { ...contract.audit } }; + + expect(consumeDirectReplayContract(copied)).toEqual({ admitted: false, reason: 'unknown_contract' }); + expect(consumeDirectReplayContract(serialized)).toEqual({ admitted: false, reason: 'unknown_contract' }); + expect(consumeDirectReplayContract(sameHashes)).toEqual({ admitted: false, reason: 'unknown_contract' }); + }); + + it('enforces expiry, abort, and one-use consumption without dispatching a provider, tool, budget, credential, mutation, or output', () => { + const expired = contractFixture(); + expect(consumeDirectReplayContract(expired.contract, expired.contractFacts.expiresAt)).toEqual({ admitted: false, reason: 'expired' }); + expect(consumeDirectReplayContract(expired.contract)).toEqual({ admitted: false, reason: 'already_consumed' }); + + const controller = new AbortController(); + const aborted = contractFixture(); + const abortable = mintContract(aborted.client, aborted.local, facts(['global_only', 'local_only', 'second_local'], { + abortSignal: controller.signal, + }), { + disableServerTools: true, + selectedToolSetNames: ['knowledge'], + allowedToolNames: ['global_only', 'local_only', 'second_local'], + }); + controller.abort(); + expect(consumeDirectReplayContract(abortable)).toEqual({ admitted: false, reason: 'aborted' }); + + const once = contractFixture(); + expect(consumeDirectReplayContract(once.contract)).toEqual({ admitted: true }); + expect(consumeDirectReplayContract(once.contract)).toEqual({ admitted: false, reason: 'already_consumed' }); + expect(once.localHandler).not.toHaveBeenCalled(); + }); +});