From fa70c9b64f792fdf14699a9e43b74a02d6adf637 Mon Sep 17 00:00:00 2001 From: ScriptedAlchemy Date: Tue, 1 Sep 2026 23:34:01 +0000 Subject: [PATCH] chore: collapse literal Object.freeze pyramids into deepFreeze Nested freeze stacks on fresh object literals were readability noise. Shared-reference snapshots stay shallow-frozen so detach semantics do not change. --- packages/agent-bundle/src/adapters/claude.ts | 18 ++++---- packages/agent-bundle/src/adapters/codex.ts | 12 ++--- .../src/adapters/hook-contract.ts | 44 ++++++++++--------- packages/agent-bundle/src/adapters/plugin.ts | 20 +++++---- .../agent-bundle/src/adapters/portable.ts | 20 +++++---- .../agent-bundle/src/adapters/registry.ts | 14 +++--- packages/agent-bundle/src/adapters/types.ts | 6 ++- packages/agent-bundle/src/api.ts | 40 +++++++++-------- packages/agent-bundle/src/build/build.ts | 6 ++- packages/agent-bundle/src/build/emit.ts | 6 ++- packages/agent-bundle/src/build/entries.ts | 3 +- packages/agent-bundle/src/build/hook-index.ts | 4 +- .../agent-bundle/src/build/inspect-bundler.ts | 6 ++- packages/agent-bundle/src/core/diagnostics.ts | 4 +- packages/agent-bundle/src/core/freeze.ts | 7 +++ packages/agent-bundle/src/dev/agent-api.ts | 4 +- .../src/dev/artifacts/artifact-service.ts | 6 ++- .../src/dev/diagnostic-service.ts | 8 ++-- packages/agent-bundle/src/dev/epoch-store.ts | 4 +- .../agent-bundle/src/dev/eval/eval-service.ts | 6 ++- .../src/dev/mcp-app-profile-descriptors.ts | 16 ++++--- .../src/dev/mcp-apps/mcp-app-sandbox.ts | 4 +- .../src/dev/mcp-session/mcp-session-types.ts | 4 +- .../dev/playground/hook-playground-service.ts | 22 +++++----- .../src/dev/playground/playground-store.ts | 26 ++++++----- .../src/dev/playground/playground-values.ts | 24 +++++----- .../agent-bundle/src/dev/project-service.ts | 8 ++-- .../src/dev/skill-document-service.ts | 4 +- .../agent-bundle/src/dev/workbench-server.ts | 10 +++-- .../agent-bundle/src/eval/codex-errors.ts | 40 +++++++++-------- .../agent-bundle/src/eval/codex-plugins.ts | 14 +++--- packages/agent-bundle/src/eval/fixtures.ts | 10 +++-- packages/agent-bundle/src/eval/graders.ts | 4 +- packages/agent-bundle/src/eval/harness.ts | 12 ++--- packages/agent-bundle/src/events/project.ts | 22 +++++----- .../agent-bundle/src/services/hook-service.ts | 4 +- packages/agent-bundle/src/skills/parse-ir.ts | 4 +- packages/agent-bundle/src/skills/tokens.ts | 16 ++++--- .../tests/artifact-routes.test.ts | 6 ++- .../agent-bundle/tests/dev-server.test.ts | 6 ++- .../tests/eval-claude-harness.test.ts | 4 +- .../agent-bundle/tests/eval-harness.test.ts | 3 +- .../agent-bundle/tests/eval-routes.test.ts | 32 +++++++------- .../tests/hook-playground-routes.test.ts | 44 ++++++++++--------- .../agent-bundle/tests/mcp-app-routes.test.ts | 4 +- .../tests/native-playground-service.test.ts | 6 ++- .../playground-orchestration-service.test.ts | 10 +++-- .../tests/runtime-mcp-registry.test.ts | 13 +++--- .../tests/runtime-mcp-routes.test.ts | 4 +- .../agent-bundle/tests/support/manifest.ts | 6 ++- .../tests/support/packed-native-smoke.ts | 8 ++-- .../src/artifacts/artifacts-model.ts | 28 +++++------- .../src/comparisons/comparisons-model.ts | 4 +- packages/workbench/src/evals/evals-model.ts | 20 +++++---- packages/workbench/src/freeze.ts | 16 +++++++ packages/workbench/src/hooks/hooks-model.ts | 6 ++- packages/workbench/src/hooks/hooks-page.tsx | 20 +++++---- packages/workbench/src/logs/log-client.ts | 22 +++++----- .../workbench/src/mcp/mcp-session-model.ts | 20 +++++---- .../src/playground/playground-model.ts | 6 ++- packages/workbench/tests/evals-page.test.ts | 6 ++- .../tests/packed-release.e2e.test.ts | 10 +++-- 62 files changed, 459 insertions(+), 327 deletions(-) create mode 100644 packages/workbench/src/freeze.ts diff --git a/packages/agent-bundle/src/adapters/claude.ts b/packages/agent-bundle/src/adapters/claude.ts index bbb8ef069..2d828d5a3 100644 --- a/packages/agent-bundle/src/adapters/claude.ts +++ b/packages/agent-bundle/src/adapters/claude.ts @@ -57,6 +57,8 @@ import { type TargetArtifactPlan, } from './types.ts'; import { withInstallSurface } from '../install/surface.ts'; +import { deepFreeze } from '../core/freeze.ts'; + /** * One Claude Code plugin LSP server. The binary is never vendored: Claude @@ -146,21 +148,21 @@ const metadata = Object.freeze({ }); const evidence = capabilityEvidence(claudeName, metadata); -const artifactValidation = Object.freeze({ - documents: Object.freeze([ +const artifactValidation = deepFreeze({ + documents: [ Object.freeze({ path: 'hooks/hooks.json', required: false, schema: 'hooks' }), Object.freeze({ path: claudeArtifactPaths.lsp, required: false, schema: 'lsp' }), Object.freeze({ path: '.claude-plugin/marketplace.json', required: false, schema: 'marketplace' }), Object.freeze({ path: '.mcp.json', required: false, schema: 'mcp' }), Object.freeze({ path: '.claude-plugin/plugin.json', required: true, schema: 'plugin' }), - ]), - schemas: Object.freeze([ + ], + schemas: [ Object.freeze({ name: 'hooks', validate: validateJsonSchemaDocument(validateHooks) }), Object.freeze({ name: 'lsp', validate: validateJsonSchemaDocument(validateLsp) }), Object.freeze({ name: 'marketplace', validate: validateJsonSchemaDocument(validateMarketplace) }), Object.freeze({ name: 'mcp', validate: validateModernMcpDocument(validateJsonSchemaDocument(validateMcp)) }), Object.freeze({ name: 'plugin', validate: validateJsonSchemaDocument(validatePlugin) }), - ]), + ], }); const mcpRuntime = createTargetMcpRuntime({ @@ -364,9 +366,9 @@ interface ClaudeLspPlan { readonly sourceInputs: readonly string[]; } -const noLspPlan: ClaudeLspPlan = Object.freeze({ - diagnostics: Object.freeze([]), - sourceInputs: Object.freeze([]), +const noLspPlan: ClaudeLspPlan = deepFreeze({ + diagnostics: [], + sourceInputs: [], }); /** diff --git a/packages/agent-bundle/src/adapters/codex.ts b/packages/agent-bundle/src/adapters/codex.ts index fb4e2580d..1189bb369 100644 --- a/packages/agent-bundle/src/adapters/codex.ts +++ b/packages/agent-bundle/src/adapters/codex.ts @@ -51,6 +51,8 @@ import { type TargetArtifactPlan, } from './types.ts'; import { withInstallSurface } from '../install/surface.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface CodexConfigExtension { codex?: AgentBundleHostConfig; @@ -128,19 +130,19 @@ const metadata = Object.freeze({ }); const evidence = capabilityEvidence(codexName, metadata); -const artifactValidation = Object.freeze({ - documents: Object.freeze([ +const artifactValidation = deepFreeze({ + documents: [ Object.freeze({ path: 'hooks/hooks.json', required: false, schema: 'hooks' }), Object.freeze({ path: '.agents/plugins/marketplace.json', required: false, schema: 'marketplace' }), Object.freeze({ path: '.mcp.json', required: false, schema: 'mcp' }), Object.freeze({ path: '.codex-plugin/plugin.json', required: true, schema: 'plugin' }), - ]), - schemas: Object.freeze([ + ], + schemas: [ Object.freeze({ name: 'hooks', validate: validateJsonSchemaDocument(validateHooks) }), Object.freeze({ name: 'marketplace', validate: validateJsonSchemaDocument(validateMarketplace) }), Object.freeze({ name: 'mcp', validate: validateJsonSchemaDocument(validateMcp) }), Object.freeze({ name: 'plugin', validate: validateJsonSchemaDocument(validatePlugin) }), - ]), + ], }); const mcpRuntime = createTargetMcpRuntime({ diff --git a/packages/agent-bundle/src/adapters/hook-contract.ts b/packages/agent-bundle/src/adapters/hook-contract.ts index 7c65ebbfe..f9dadadfc 100644 --- a/packages/agent-bundle/src/adapters/hook-contract.ts +++ b/packages/agent-bundle/src/adapters/hook-contract.ts @@ -9,6 +9,8 @@ import type { NormalizedNativeHook, NormalizedPlugin, } from '../core/types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface TargetHookWrapper { readonly event: CanonicalHookEvent; @@ -172,26 +174,26 @@ export const readCursorNativeHookCommands = (document: unknown): TargetNativeHoo return Object.freeze({ commands: Object.freeze(commands), status: 'found' }); }; -const nativeHookInputFields = Object.freeze([ - Object.freeze({ canonical: 'agentId', native: 'agent_id' }), - Object.freeze({ canonical: 'agentTranscriptPath', native: 'agent_transcript_path' }), - Object.freeze({ canonical: 'agentType', native: 'agent_type' }), - Object.freeze({ canonical: 'cwd', native: 'cwd' }), - Object.freeze({ canonical: 'effort', native: 'effort' }), - Object.freeze({ canonical: 'hookEventName', native: 'hook_event_name' }), - Object.freeze({ canonical: 'lastAssistantMessage', native: 'last_assistant_message' }), - Object.freeze({ canonical: 'model', native: 'model' }), - Object.freeze({ canonical: 'permissionMode', native: 'permission_mode' }), - Object.freeze({ canonical: 'promptId', native: 'prompt_id' }), - Object.freeze({ canonical: 'sessionId', native: 'session_id' }), - Object.freeze({ canonical: 'source', native: 'source' }), - Object.freeze({ canonical: 'stopHookActive', native: 'stop_hook_active' }), - Object.freeze({ canonical: 'toolInput', native: 'tool_input' }), - Object.freeze({ canonical: 'toolName', native: 'tool_name' }), - Object.freeze({ canonical: 'toolResponse', native: 'tool_response' }), - Object.freeze({ canonical: 'toolUseId', native: 'tool_use_id' }), - Object.freeze({ canonical: 'transcriptPath', native: 'transcript_path' }), - Object.freeze({ canonical: 'turnId', native: 'turn_id' }), +const nativeHookInputFields = deepFreeze([ + { canonical: 'agentId', native: 'agent_id' }, + { canonical: 'agentTranscriptPath', native: 'agent_transcript_path' }, + { canonical: 'agentType', native: 'agent_type' }, + { canonical: 'cwd', native: 'cwd' }, + { canonical: 'effort', native: 'effort' }, + { canonical: 'hookEventName', native: 'hook_event_name' }, + { canonical: 'lastAssistantMessage', native: 'last_assistant_message' }, + { canonical: 'model', native: 'model' }, + { canonical: 'permissionMode', native: 'permission_mode' }, + { canonical: 'promptId', native: 'prompt_id' }, + { canonical: 'sessionId', native: 'session_id' }, + { canonical: 'source', native: 'source' }, + { canonical: 'stopHookActive', native: 'stop_hook_active' }, + { canonical: 'toolInput', native: 'tool_input' }, + { canonical: 'toolName', native: 'tool_name' }, + { canonical: 'toolResponse', native: 'tool_response' }, + { canonical: 'toolUseId', native: 'tool_use_id' }, + { canonical: 'transcriptPath', native: 'transcript_path' }, + { canonical: 'turnId', native: 'turn_id' }, ]); const defined = (value: Record): Record => @@ -719,7 +721,7 @@ export const planHooks = ( return eventComparison !== 0 ? eventComparison : left.id.localeCompare(right.id); }); if (selected.length === 0) { - return Object.freeze({ diagnostics: Object.freeze(diagnostics), hookEntries: Object.freeze([]) }); + return deepFreeze({ diagnostics: diagnostics, hookEntries: [] }); } const groups: Record = Object.create(null) as Record; diff --git a/packages/agent-bundle/src/adapters/plugin.ts b/packages/agent-bundle/src/adapters/plugin.ts index 38404d479..2a60db67d 100644 --- a/packages/agent-bundle/src/adapters/plugin.ts +++ b/packages/agent-bundle/src/adapters/plugin.ts @@ -54,6 +54,8 @@ import { type TargetArtifactPlan, type TargetHookEntry, } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + const pluginName = 'plugin'; @@ -149,8 +151,8 @@ const hostValidation = (adapter: TargetAdapter, name: string) => { const claudeValidation = hostValidation(claudeAdapter, 'Claude'); const codexValidation = hostValidation(codexAdapter, 'Codex'); -const artifactValidation = Object.freeze({ - documents: Object.freeze([ +const artifactValidation = deepFreeze({ + documents: [ // One shared Claude-format hook document serves both hosts; the pinned // Codex hooks schema is byte-identical apart from its $id. Object.freeze({ path: bundleHookContract.manifestPath, required: false, schema: 'claude-hooks' }), @@ -165,8 +167,8 @@ const artifactValidation = Object.freeze({ Object.freeze({ path: cursorPaths.marketplace, required: false, schema: 'cursor-marketplace' }), Object.freeze({ path: cursorPaths.mcp, required: false, schema: 'cursor-mcp' }), Object.freeze({ path: cursorPaths.plugin, required: false, schema: 'cursor-plugin' }), - ]), - schemas: Object.freeze([ + ], + schemas: [ ...prefixedSchemas('claude', claudeValidation.schemas), ...prefixedSchemas('codex', codexValidation.schemas, 'plugin').filter((schema) => schema.name !== 'codex-hooks'), // The bundle's Codex manifest points at the relocated MCP document, so its @@ -176,7 +178,7 @@ const artifactValidation = Object.freeze({ Object.freeze({ name: 'cursor-marketplace', validate: validateJsonSchemaDocument(cursorMarketplaceValidator) }), Object.freeze({ name: 'cursor-mcp', validate: validateJsonSchemaDocument(cursorMcpValidator) }), Object.freeze({ name: 'cursor-plugin', validate: validateJsonSchemaDocument(cursorPluginValidator) }), - ]), + ], }); const metadata = Object.freeze({ @@ -497,10 +499,10 @@ const plan = (model: NormalizedPlugin): TargetArtifactPlan => { }); }; -const eventCapabilityTables = Object.freeze([ - Object.freeze({ name: 'Claude', routes: claudeCapabilityTable.hooks.eventRoutes }), - Object.freeze({ name: 'Codex', routes: codexCapabilityTable.hooks.eventRoutes }), - Object.freeze({ name: 'Cursor', routes: cursorCapabilityTable.hooks.eventRoutes }), +const eventCapabilityTables = deepFreeze([ + { name: 'Claude', routes: claudeCapabilityTable.hooks.eventRoutes }, + { name: 'Codex', routes: codexCapabilityTable.hooks.eventRoutes }, + { name: 'Cursor', routes: cursorCapabilityTable.hooks.eventRoutes }, ]); const compositeEventNames = new Set(eventCapabilityTables.flatMap(({ routes }) => Object.keys(routes))); for (const event of compositeEventNames) { diff --git a/packages/agent-bundle/src/adapters/portable.ts b/packages/agent-bundle/src/adapters/portable.ts index d313387f5..72680217e 100644 --- a/packages/agent-bundle/src/adapters/portable.ts +++ b/packages/agent-bundle/src/adapters/portable.ts @@ -37,6 +37,8 @@ import { type TargetArtifactPlan, } from './types.ts'; import { withInstallSurface } from '../install/surface.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface PortableConfigExtension { portable?: AgentBundlePortableConfig; @@ -64,15 +66,15 @@ const metadata = Object.freeze({ }); const evidence = capabilityEvidence(portableName, metadata); -const artifactValidation = Object.freeze({ - documents: Object.freeze([ +const artifactValidation = deepFreeze({ + documents: [ Object.freeze({ path: 'mcp.json', required: false, schema: 'mcp' }), Object.freeze({ path: 'plugin.json', required: true, schema: 'plugin' }), - ]), - schemas: Object.freeze([ + ], + schemas: [ Object.freeze({ name: 'mcp', validate: validateModernMcpDocument(validateJsonSchemaDocument(validateMcp)) }), Object.freeze({ name: 'plugin', validate: validateJsonSchemaDocument(validatePlugin) }), - ]), + ], }); const mcpRuntime = createTargetMcpRuntime({ @@ -311,10 +313,10 @@ const plan = (model: NormalizedPlugin): TargetArtifactPlan => { } } - return withInstallSurface(Object.freeze({ - diagnostics: Object.freeze(diagnostics), - entries: Object.freeze(entries), - hookEntries: Object.freeze([]), + return withInstallSurface(deepFreeze({ + diagnostics: diagnostics, + entries: entries, + hookEntries: [], }), model, 'portable'); }; diff --git a/packages/agent-bundle/src/adapters/registry.ts b/packages/agent-bundle/src/adapters/registry.ts index 655155605..4546deda3 100644 --- a/packages/agent-bundle/src/adapters/registry.ts +++ b/packages/agent-bundle/src/adapters/registry.ts @@ -26,13 +26,15 @@ import type { TargetSchemaDescriptor, } from './types.ts'; import type { TargetMcpRuntimeContract } from '../services/mcp-runtime.ts'; +import { deepFreeze } from '../core/freeze.ts'; + const sha256Pattern = /^[0-9a-f]{64}$/; type NativeHookSource = NonNullable; -const emptyArtifactValidation: TargetArtifactValidationContract = Object.freeze({ - documents: Object.freeze([]), - schemas: Object.freeze([]), +const emptyArtifactValidation: TargetArtifactValidationContract = deepFreeze({ + documents: [], + schemas: [], }); const emptyArtifactLayout: TargetArtifactLayout = Object.freeze({}); @@ -297,9 +299,9 @@ const snapshotArtifactValidation = ( if ([...schemaNames].some((name) => !referencedSchemas.has(name))) { throw new Error(`Target adapter "${adapter.name}" must assign every artifact schema contract to a document.`); } - return Object.freeze({ - documents: Object.freeze(documents.sort((left, right) => left.path.localeCompare(right.path))), - schemas: Object.freeze(schemas.sort((left, right) => left.name.localeCompare(right.name))), + return deepFreeze({ + documents: documents.sort((left, right) => left.path.localeCompare(right.path)), + schemas: schemas.sort((left, right) => left.name.localeCompare(right.name)), }); }; diff --git a/packages/agent-bundle/src/adapters/types.ts b/packages/agent-bundle/src/adapters/types.ts index 1ddd2f5dd..94ab58823 100644 --- a/packages/agent-bundle/src/adapters/types.ts +++ b/packages/agent-bundle/src/adapters/types.ts @@ -15,6 +15,8 @@ import { } from '../core/types.ts'; import type { TargetHookContract, TargetHookEntry } from './hook-contract.ts'; import type { TargetMcpRuntimeContract } from '../services/mcp-runtime.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export type { TargetHookEntry, TargetHookWrapper } from './hook-contract.ts'; @@ -397,10 +399,10 @@ export interface TargetArtifactOutputLayout { } const noArtifactDocumentIssues: readonly TargetArtifactDocumentIssue[] = Object.freeze([]); -const invalidMcpDocumentIssues: readonly TargetArtifactDocumentIssue[] = Object.freeze([Object.freeze({ +const invalidMcpDocumentIssues: readonly TargetArtifactDocumentIssue[] = deepFreeze([{ instancePath: '', message: 'MCP document must be a detached finite JSON value.', -})]); +}]); /** * Target-owned compiler namespaces, separate from target-native schema documents. diff --git a/packages/agent-bundle/src/api.ts b/packages/agent-bundle/src/api.ts index dcde4dcf3..7b9ecfd43 100644 --- a/packages/agent-bundle/src/api.ts +++ b/packages/agent-bundle/src/api.ts @@ -11,6 +11,8 @@ import { isInsideOrEqual } from './core/paths.ts'; import { emptyCompiledRouteGraph } from './routes/graph.ts'; import { inspectRouteGraph, type RouteGraphInspection } from './routes/inspect.ts'; import { mcpServerStateDirectory, runMcpForeground } from './services/mcp-run.ts'; +import { deepFreeze } from './core/freeze.ts'; + export { compileRouteGraph, emptyCompiledRouteGraph, isEmptyRouteGraph } from './routes/graph.ts'; export { canonicalAgentEvents } from './routes/public.ts'; export type { @@ -651,43 +653,43 @@ export const build = async (options: BuildOptions): Promise const evalDiagnostics: Readonly>> = Object.freeze({ - EVAL_ARTIFACT_NOT_FOUND: Object.freeze({ +}>>> = deepFreeze({ + EVAL_ARTIFACT_NOT_FOUND: { code: 'AB9009', recovery: 'Select raw evidence that the recorded eval trial persisted.', - }), - EVAL_ARTIFACT_UNAVAILABLE: Object.freeze({ + }, + EVAL_ARTIFACT_UNAVAILABLE: { code: 'AB9010', recovery: 'Regenerate the recorded eval run before reading its raw evidence.', - }), - EVAL_EVENTS_CURSOR_INVALID: Object.freeze({ + }, + EVAL_EVENTS_CURSOR_INVALID: { code: 'AB9011', recovery: 'Reconnect from a non-negative cursor no later than the durable event sequence.', - }), - EVAL_HARNESS_UNSUPPORTED: Object.freeze({ + }, + EVAL_HARNESS_UNSUPPORTED: { code: 'AB9001', recovery: 'Use deterministic, claude, or codex, or correct an unknown harness name.', - }), - EVAL_RUN_NOT_FOUND: Object.freeze({ + }, + EVAL_RUN_NOT_FOUND: { code: 'AB9003', recovery: 'Read a run that this project recorded, or start a new one.', - }), - EVAL_SELECTION_EMPTY: Object.freeze({ + }, + EVAL_SELECTION_EMPTY: { code: 'AB9002', recovery: 'Select a suite or case that "agent-bundle eval --json" reports as discovered.', - }), - EVAL_SEMANTIC_GRADER_UNSUPPORTED: Object.freeze({ + }, + EVAL_SEMANTIC_GRADER_UNSUPPORTED: { code: 'AB9008', recovery: 'Run the configured semantic grader with "--harness claude" and a Claude-pinned eval case.', - }), - EVAL_TARGET_MISSING: Object.freeze({ + }, + EVAL_TARGET_MISSING: { code: 'AB9004', recovery: 'Select the targets the pinned eval hosts name, then evaluate again.', - }), - EVAL_TRIALS_INVALID: Object.freeze({ + }, + EVAL_TRIALS_INVALID: { code: 'AB9005', recovery: 'Request an integer trial count between 1 and 100.', - }), + }, }); const evalDiagnostic = (error: EvalServiceError): Diagnostic => { diff --git a/packages/agent-bundle/src/build/build.ts b/packages/agent-bundle/src/build/build.ts index 9a6145ff1..c1cd54b5c 100644 --- a/packages/agent-bundle/src/build/build.ts +++ b/packages/agent-bundle/src/build/build.ts @@ -40,6 +40,8 @@ import { type ArtifactOutputProvenance, } from './provenance.ts'; import { validateArtifact, validateArtifactFiles } from './validate-artifact.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface BuildResult { readonly compiledEntries: readonly CompiledEntry[]; @@ -380,8 +382,8 @@ export const build = async (options: BuildOptions): Promise => { ...tools, }))); } - const publishedCompiledEntries = Object.freeze(compiledEntries.map((entry) => - Object.freeze({ + const publishedCompiledEntries = deepFreeze(compiledEntries.map((entry) => + ({ ...entry, output: publishedOutput(entry), }), diff --git a/packages/agent-bundle/src/build/emit.ts b/packages/agent-bundle/src/build/emit.ts index e040ff8b4..fb61ba2ed 100644 --- a/packages/agent-bundle/src/build/emit.ts +++ b/packages/agent-bundle/src/build/emit.ts @@ -28,6 +28,8 @@ import { type ArtifactManifest, } from './manifest.ts'; import type { ArtifactOutputProvenance } from './provenance.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export type ManifestFile = ArtifactManifestFile; @@ -132,10 +134,10 @@ const inspectArtifactDirectory = async ( const directoryMetadata = await lstat(directoryPath); if (!directoryMetadata.isDirectory()) { return { - entries: Object.freeze([Object.freeze({ + entries: deepFreeze([{ kind: filesystemEntryKind(directoryMetadata), path: prefix === '' ? '.' : normalizeRelativePath(prefix), - })]), + }]), files: Object.freeze([]), }; } diff --git a/packages/agent-bundle/src/build/entries.ts b/packages/agent-bundle/src/build/entries.ts index e7868b348..33fe42242 100644 --- a/packages/agent-bundle/src/build/entries.ts +++ b/packages/agent-bundle/src/build/entries.ts @@ -14,6 +14,7 @@ import { mcpEntryAliasPattern } from '../config/normalize.ts'; import { stableJson } from '../core/digest.ts'; import { emitPlanEntries, resolveArtifactDestination } from './emit.ts'; import { scanEntryExports } from './entry-exports.ts'; +import { deepFreeze } from '../core/freeze.ts'; import { cliEntryRuntimePath, cliEntryRuntimeSpecifier, @@ -447,7 +448,7 @@ export const compileMcpEntries = async ( export const planCompiledHooks = ( entries: readonly TargetHookEntry[], options: { readonly outDir: string }, -): readonly CompiledHookEntry[] => Object.freeze(entries.map((entry) => Object.freeze({ +): readonly CompiledHookEntry[] => deepFreeze(entries.map((entry) => ({ event: entry.event, id: entry.hook.id, ...(entry.indexed === false ? { indexed: false as const } : {}), diff --git a/packages/agent-bundle/src/build/hook-index.ts b/packages/agent-bundle/src/build/hook-index.ts index acf6e79c1..8b2725002 100644 --- a/packages/agent-bundle/src/build/hook-index.ts +++ b/packages/agent-bundle/src/build/hook-index.ts @@ -2,6 +2,8 @@ import { posix } from 'node:path'; import { stableJson } from '../core/digest.ts'; import { isPlainRecord, parseJsonWithoutDuplicateKeys } from '../core/strict-json.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface ArtifactHook { readonly event: string; @@ -92,6 +94,6 @@ export const parseArtifactHookIndex = (bytes: string): ArtifactHookIndex | undef previous = hook; hooks.push(hook); } - const index: ArtifactHookIndex = Object.freeze({ hooks: Object.freeze(hooks) }); + const index: ArtifactHookIndex = deepFreeze({ hooks: hooks }); return bytes === `${stableJson(index)}\n` ? index : undefined; }; diff --git a/packages/agent-bundle/src/build/inspect-bundler.ts b/packages/agent-bundle/src/build/inspect-bundler.ts index 66e3bf6f5..2aee65d7d 100644 --- a/packages/agent-bundle/src/build/inspect-bundler.ts +++ b/packages/agent-bundle/src/build/inspect-bundler.ts @@ -16,6 +16,8 @@ import { planCompiledMcpEntries } from './entries.ts'; import { composeMcpAppsRsbuildConfig, planCompiledMcpApps } from './mcp-apps.ts'; import { planPackageEntries } from './package-build.ts'; import { composeEntryLibConfig, type RslibEntry } from './rslib.ts'; +import { deepFreeze } from '../core/freeze.ts'; + /** * `agent-bundle inspect --bundler` (RFC #50 §3.4): surfaces the internal @@ -331,7 +333,7 @@ export const composeBundlerInspection = async (options: { ); } entries.push(...(await packageBuildEntries(options.model, options.tools))); - return Object.freeze({ - entries: Object.freeze(entries.sort(entryOrder)), + return deepFreeze({ + entries: entries.sort(entryOrder), }); }; diff --git a/packages/agent-bundle/src/core/diagnostics.ts b/packages/agent-bundle/src/core/diagnostics.ts index 9b1a364a2..af072ca9e 100644 --- a/packages/agent-bundle/src/core/diagnostics.ts +++ b/packages/agent-bundle/src/core/diagnostics.ts @@ -1,3 +1,5 @@ +import { deepFreeze } from './freeze.ts'; + export type DiagnosticSeverity = 'error' | 'warning' | 'info'; export interface Diagnostic { @@ -130,7 +132,7 @@ export class DiagnosticBag { } export const freezeDiagnostics = (diagnostics: readonly Diagnostic[]): readonly Diagnostic[] => - Object.freeze(diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))); + deepFreeze(diagnostics.map((diagnostic) => ({ ...diagnostic }))); export const hasErrors = (diagnostics: readonly Diagnostic[]): boolean => diagnostics.some((diagnostic) => diagnostic.severity === 'error'); diff --git a/packages/agent-bundle/src/core/freeze.ts b/packages/agent-bundle/src/core/freeze.ts index 7d46319d6..2cb805ab9 100644 --- a/packages/agent-bundle/src/core/freeze.ts +++ b/packages/agent-bundle/src/core/freeze.ts @@ -1,6 +1,13 @@ +const isPlainObjectOrArray = (value: object): boolean => { + if (Array.isArray(value)) return true; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +}; + /** Freezes a value tree in place; cycle-safe and symbol-aware. Freezing is idempotent. */ export const deepFreeze = (value: Value, seen = new WeakSet()): Value => { if (typeof value !== 'object' || value === null || seen.has(value)) return value; + if (!isPlainObjectOrArray(value)) return value; seen.add(value); for (const property of Reflect.ownKeys(value)) { deepFreeze(Reflect.get(value, property), seen); diff --git a/packages/agent-bundle/src/dev/agent-api.ts b/packages/agent-bundle/src/dev/agent-api.ts index 6f92ca6f5..1d4907a83 100644 --- a/packages/agent-bundle/src/dev/agent-api.ts +++ b/packages/agent-bundle/src/dev/agent-api.ts @@ -21,6 +21,8 @@ import { parseJsonWithoutDuplicateKeys, snapshotStrictJsonValue } from '../core/ import type { EvalService } from './eval/eval-service.ts'; import { runtimeAppFiniteOrdinaryJsonByteLength } from './runtime-app-message-limits.ts'; import type { ProjectStatus } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export const agentApiToolNames = Object.freeze([ 'project_status', @@ -602,7 +604,7 @@ const safeToolResult = (value: unknown) => { }; const failedToolResult = (error: unknown) => { - const structured = Object.freeze({ error: Object.freeze({ code: safeErrorCode(error), message: 'The requested operation could not be completed.' }) }); + const structured = deepFreeze({ error: { code: safeErrorCode(error), message: 'The requested operation could not be completed.' } }); return { content: [{ text: stableJson(structured), type: 'text' as const }], isError: true, diff --git a/packages/agent-bundle/src/dev/artifacts/artifact-service.ts b/packages/agent-bundle/src/dev/artifacts/artifact-service.ts index 0b24f6238..2d291663e 100644 --- a/packages/agent-bundle/src/dev/artifacts/artifact-service.ts +++ b/packages/agent-bundle/src/dev/artifacts/artifact-service.ts @@ -27,6 +27,8 @@ import { } from '../playground/native-playground-service.ts'; import type { PreparedProject } from '../project-service.ts'; import { freezeArtifactEpoch, type ArtifactEpoch, type DiagnosticSummary } from '../types.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + export interface SucceededArtifactEpochResult { readonly diagnostics: readonly Diagnostic[]; @@ -77,12 +79,12 @@ const failureDiagnostics = ( const first = diagnostics[0]; if (first !== undefined) return Object.freeze([first, ...diagnostics.slice(1)]); } - return Object.freeze([Object.freeze({ + return deepFreeze([{ code: 'AB7100', message: `Unable to compile the build: ${errorMessage(error)}`, severity: 'error' as const, sourcePath: configPath, - })]); + }]); }; const targetDigests = async ( diff --git a/packages/agent-bundle/src/dev/diagnostic-service.ts b/packages/agent-bundle/src/dev/diagnostic-service.ts index 38269659c..aa4759745 100644 --- a/packages/agent-bundle/src/dev/diagnostic-service.ts +++ b/packages/agent-bundle/src/dev/diagnostic-service.ts @@ -3,6 +3,8 @@ import { resolve } from 'node:path'; import { Rslint } from '@rslint/core'; import type { Diagnostic, DiagnosticSeverity } from '../core/diagnostics.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface RslintMessage { readonly column?: number; @@ -43,9 +45,9 @@ const defaultRslint = ({ cwd }: Readonly<{ readonly cwd: string }>): RslintEngin const freezeReport = ( diagnostics: readonly Diagnostic[], paths: readonly string[], -): DiagnosticReport => Object.freeze({ - diagnostics: Object.freeze(diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic }))), - paths: Object.freeze([...paths]), +): DiagnosticReport => deepFreeze({ + diagnostics: diagnostics.map((diagnostic) => Object.freeze({ ...diagnostic })), + paths: [...paths], }); const diagnosticSeverity = (severity: number): DiagnosticSeverity => diff --git a/packages/agent-bundle/src/dev/epoch-store.ts b/packages/agent-bundle/src/dev/epoch-store.ts index 484b81378..a0f53a1e6 100644 --- a/packages/agent-bundle/src/dev/epoch-store.ts +++ b/packages/agent-bundle/src/dev/epoch-store.ts @@ -10,6 +10,8 @@ import { parseJsonWithoutDuplicateKeys } from '../core/strict-json.ts'; import { runPromise, runSync } from '../effect/boundary.ts'; import { liftPromise, liftTry } from '../effect/lift.ts'; import { freezeArtifactEpoch, type ArtifactEpoch } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface EpochStoreOptions { /** @internal Deterministic cleanup-failure seam. */ @@ -72,7 +74,7 @@ export class EpochCleanupError extends Error { constructor(failures: readonly EpochCleanupFailure[]) { super('One or more epoch cleanup operations failed.'); this.name = 'EpochCleanupError'; - this.failures = Object.freeze(failures.map((failure) => Object.freeze({ ...failure }))); + this.failures = deepFreeze(failures.map((failure) => ({ ...failure }))); Object.freeze(this); } } diff --git a/packages/agent-bundle/src/dev/eval/eval-service.ts b/packages/agent-bundle/src/dev/eval/eval-service.ts index 1186b0b8b..91faf4830 100644 --- a/packages/agent-bundle/src/dev/eval/eval-service.ts +++ b/packages/agent-bundle/src/dev/eval/eval-service.ts @@ -36,6 +36,8 @@ import type { EvalAssertionKind, EvalCase, EvalInvocation } from '../../eval/typ import type { DevLogKindFor, DevLogSink } from '../logs/dev-log-service.ts'; import { isInsideOrEqual } from '../../core/paths.ts'; import { isErrno } from '../../core/errors.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + export type EvalServiceErrorCode = | 'EVAL_ARTIFACT_NOT_FOUND' @@ -280,11 +282,11 @@ const suiteSummary = (projectRoot: string, discovered: DiscoveredEvalSuite): Eva const selectEvalCases = ( discovered: readonly DiscoveredEvalSuite[], selection: EvalRunSelection, -): readonly SelectedEvalCase[] => Object.freeze(discovered +): readonly SelectedEvalCase[] => deepFreeze(discovered .filter((entry) => selection.suites === undefined || selection.suites.includes(entry.suite.name)) .flatMap((entry) => entry.suite.cases .filter((evalCase) => selection.caseIds === undefined || selection.caseIds.includes(evalCase.id)) - .map((evalCase): SelectedEvalCase => Object.freeze({ + .map((evalCase): SelectedEvalCase => ({ evalCase, suite: entry.suite.name, suiteDir: dirname(entry.sourcePath), diff --git a/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts b/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts index 14440b605..ed9135e76 100644 --- a/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts +++ b/packages/agent-bundle/src/dev/mcp-app-profile-descriptors.ts @@ -1,3 +1,5 @@ +import { deepFreeze } from '../core/freeze.ts'; + /** Browser-safe MCP App profile identity and descriptor registry. */ export const MCP_APP_PROTOCOL_VERSION = '2026-01-26'; @@ -19,26 +21,26 @@ export interface McpAppProfileDescriptor { const portableProfileVersion = (`agent-bundle:mcp-apps:${MCP_APP_PROTOCOL_VERSION}`) as McpAppProfileDescriptor['version']; -export const MCP_APP_PROFILE_DESCRIPTORS: Readonly> = Object.freeze({ - chatgpt: Object.freeze({ +export const MCP_APP_PROFILE_DESCRIPTORS: Readonly> = deepFreeze({ + chatgpt: { claimsRealHostParity: false, evidence: 'simulated', id: 'chatgpt', label: 'ChatGPT Simulation', version: 'agent-bundle:chatgpt-sim:1', - }), - claude: Object.freeze({ + }, + claude: { claimsRealHostParity: false, evidence: 'simulated', id: 'claude', label: 'Claude Simulation', version: 'agent-bundle:claude-sim:1', - }), - portable: Object.freeze({ + }, + portable: { claimsRealHostParity: false, evidence: 'simulated', id: 'portable', label: 'Portable MCP Apps', version: portableProfileVersion, - }), + }, }); diff --git a/packages/agent-bundle/src/dev/mcp-apps/mcp-app-sandbox.ts b/packages/agent-bundle/src/dev/mcp-apps/mcp-app-sandbox.ts index cc02d93dd..9971f31bc 100644 --- a/packages/agent-bundle/src/dev/mcp-apps/mcp-app-sandbox.ts +++ b/packages/agent-bundle/src/dev/mcp-apps/mcp-app-sandbox.ts @@ -3,6 +3,8 @@ import { createServer, type Server } from 'node:http'; import { isIP, type Socket } from 'node:net'; import type { McpAppJsonValue } from './mcp-app-binding-service.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + const JSON_RPC_VERSION = '2.0'; const SANDBOX_NOTIFICATION_PREFIX = 'ui/notifications/sandbox-'; @@ -554,7 +556,7 @@ const cspSources = (sources: readonly string[] | undefined): Readonly<{ accepted warnings.push(Object.freeze({ code: 'csp-source-rejected', value: source })); } } - return Object.freeze({ accepted: Object.freeze([...accepted].slice(0, 32)), warnings: Object.freeze(warnings) }); + return deepFreeze({ accepted: [...accepted].slice(0, 32), warnings: warnings }); }; const sourceList = (sources: readonly string[], fallback: string): string => sources.length > 0 ? sources.join(' ') : fallback; diff --git a/packages/agent-bundle/src/dev/mcp-session/mcp-session-types.ts b/packages/agent-bundle/src/dev/mcp-session/mcp-session-types.ts index 8820299d8..fb8901267 100644 --- a/packages/agent-bundle/src/dev/mcp-session/mcp-session-types.ts +++ b/packages/agent-bundle/src/dev/mcp-session/mcp-session-types.ts @@ -19,6 +19,8 @@ import type { McpSessionReplayOverflow, } from './mcp-session-protocol.ts'; import type { McpSessionTraceSink } from './mcp-session-trace.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + export interface McpRequestOptions { readonly signal?: AbortSignal; @@ -161,6 +163,6 @@ export class McpSessionServiceCloseError extends Error { constructor(failures: readonly McpSessionServiceCloseFailure[]) { super('MCP session service could not close every lifecycle resource.'); this.name = 'McpSessionServiceCloseError'; - this.failures = Object.freeze(failures.map((failure) => Object.freeze({ ...failure }))); + this.failures = deepFreeze(failures.map((failure) => ({ ...failure }))); } } diff --git a/packages/agent-bundle/src/dev/playground/hook-playground-service.ts b/packages/agent-bundle/src/dev/playground/hook-playground-service.ts index b7faedd11..e670f554e 100644 --- a/packages/agent-bundle/src/dev/playground/hook-playground-service.ts +++ b/packages/agent-bundle/src/dev/playground/hook-playground-service.ts @@ -13,6 +13,8 @@ import { isRecord, snapshotStrictJsonValue } from '../../core/strict-json.ts'; import { HookService } from '../../services/hook-service.ts'; import { EpochStore, type EpochReference } from '../epoch-store.ts'; import type { DevLogKindFor, DevLogSink } from '../logs/dev-log-service.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + type CanonicalHookInput = Readonly>; type CanonicalHookResult = Readonly>; @@ -117,34 +119,34 @@ const inputFor = (input: HookPlaygroundInput): CanonicalHookInput => { return cloneRecord(candidate); }; -const unsupportedTarget = (target: string, event: string): HookPlaygroundDiagnosticResult => Object.freeze({ - diagnostics: Object.freeze([Object.freeze({ +const unsupportedTarget = (target: string, event: string): HookPlaygroundDiagnosticResult => deepFreeze({ + diagnostics: [Object.freeze({ code: 'hook.playground.target.unsupported', event, message: `Hook playground cannot map target ${JSON.stringify(target)} for canonical event ${JSON.stringify(event)}.`, severity: 'error', target, - })]), + })], }); -const unsupportedEvent = (target: string, event: string): HookPlaygroundDiagnosticResult => Object.freeze({ - diagnostics: Object.freeze([Object.freeze({ +const unsupportedEvent = (target: string, event: string): HookPlaygroundDiagnosticResult => deepFreeze({ + diagnostics: [Object.freeze({ code: 'hook.playground.event.unsupported', event, message: `Hook playground target ${JSON.stringify(target)} cannot map canonical event ${JSON.stringify(event)}.`, severity: 'error', target, - })]), + })], }); -const missingManifest = (target: string, event: string, manifestPath: string): HookPlaygroundDiagnosticResult => Object.freeze({ - diagnostics: Object.freeze([Object.freeze({ +const missingManifest = (target: string, event: string, manifestPath: string): HookPlaygroundDiagnosticResult => deepFreeze({ + diagnostics: [Object.freeze({ code: 'hook.playground.manifest.missing', event, message: `Hook playground target ${JSON.stringify(target)} is missing hook manifest ${JSON.stringify(manifestPath)} for canonical event ${JSON.stringify(event)}.`, severity: 'error', target, - })]), + })], }); const matcherFor = async ( @@ -257,7 +259,7 @@ export class HookPlaygroundService { artifact, ...(options.target === undefined ? {} : { target: options.target }), }); - return Object.freeze(hooks.map((hook) => Object.freeze({ + return deepFreeze(hooks.map((hook) => ({ binding: Object.freeze({ epochId: options.epochId, hook: hook.id, target: hook.target }), hook: cloneRecord(hook), }))); diff --git a/packages/agent-bundle/src/dev/playground/playground-store.ts b/packages/agent-bundle/src/dev/playground/playground-store.ts index 24b972543..221f893bc 100644 --- a/packages/agent-bundle/src/dev/playground/playground-store.ts +++ b/packages/agent-bundle/src/dev/playground/playground-store.ts @@ -8,6 +8,8 @@ import { isErrno } from '../../core/errors.ts'; import { isInsideOrEqual } from '../../core/paths.ts'; import { parseJsonWithoutDuplicateKeys } from '../../core/strict-json.ts'; import type { DevLogSink } from '../logs/dev-log-service.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + export type PlaygroundJsonPrimitive = boolean | null | number | string; export type PlaygroundJsonArray = readonly PlaygroundJsonValue[]; @@ -563,27 +565,27 @@ const assertNoProviderCredentials = (value: PlaygroundJsonValue): void => { }; const normalizeIdentity = (value: PlaygroundSessionIdentity): PlaygroundSessionIdentity => { - const identity = Object.freeze({ - epoch: Object.freeze({ + const identity = deepFreeze({ + epoch: { digest: nonempty(value?.epoch?.digest, 'Playground epoch digest'), id: nonempty(value?.epoch?.id, 'Playground epoch id'), - }), - fixture: Object.freeze({ + }, + fixture: { digest: nonempty(value?.fixture?.digest, 'Playground fixture digest'), id: nonempty(value?.fixture?.id, 'Playground fixture id'), - }), - invocation: Object.freeze({ + }, + invocation: { intent: jsonObject(value?.invocation?.intent, 'Playground invocation intent'), kind: nonempty(value?.invocation?.kind, 'Playground invocation kind'), - }), - target: Object.freeze({ + }, + target: { ...(value?.target?.digest === undefined ? {} : { digest: nonempty(value.target.digest, 'Playground target digest') }), name: nonempty(value?.target?.name, 'Playground target name'), - }), - task: Object.freeze({ + }, + task: { id: nonempty(value?.task?.id, 'Playground task id'), text: nonempty(value?.task?.text, 'Playground task text'), - }), + }, }); assertNoProviderCredentials(json(identity, 'Playground identity')); return identity; @@ -626,7 +628,7 @@ const snapshotEvent = (value: PlaygroundTraceEvent): PlaygroundTraceEvent => Obj }); const snapshotCleanupFailures = (value: readonly PlaygroundCleanupFailure[]): readonly PlaygroundCleanupFailure[] => - Object.freeze(value.map((failure) => Object.freeze({ message: failure.message, operation: failure.operation }))); + deepFreeze(value.map((failure) => ({ message: failure.message, operation: failure.operation }))); const snapshotSession = (record: SessionRecord): PlaygroundSession => Object.freeze({ cleanupFailures: snapshotCleanupFailures(record.cleanupFailures), diff --git a/packages/agent-bundle/src/dev/playground/playground-values.ts b/packages/agent-bundle/src/dev/playground/playground-values.ts index 6c5899c66..41908ec9d 100644 --- a/packages/agent-bundle/src/dev/playground/playground-values.ts +++ b/packages/agent-bundle/src/dev/playground/playground-values.ts @@ -12,6 +12,8 @@ import { type PlaygroundTraceEvent, type PlaygroundTraceSource, } from './playground-protocol.ts'; +import { deepFreeze } from '../../core/freeze.ts'; + const pathSegment = /^[a-z0-9][a-z0-9._-]*$/iu; const traceSources: ReadonlySet = new Set([ @@ -105,27 +107,27 @@ export const assertNoProviderCredentials = (value: PlaygroundJsonValue): void => }; export const normalizeIdentity = (value: PlaygroundSessionIdentity): PlaygroundSessionIdentity => { - const identity = Object.freeze({ - epoch: Object.freeze({ + const identity = deepFreeze({ + epoch: { digest: nonempty(value?.epoch?.digest, 'Playground epoch digest'), id: nonempty(value?.epoch?.id, 'Playground epoch id'), - }), - fixture: Object.freeze({ + }, + fixture: { digest: nonempty(value?.fixture?.digest, 'Playground fixture digest'), id: nonempty(value?.fixture?.id, 'Playground fixture id'), - }), - invocation: Object.freeze({ + }, + invocation: { intent: jsonObject(value?.invocation?.intent, 'Playground invocation intent'), kind: nonempty(value?.invocation?.kind, 'Playground invocation kind'), - }), - target: Object.freeze({ + }, + target: { ...(value?.target?.digest === undefined ? {} : { digest: nonempty(value.target.digest, 'Playground target digest') }), name: nonempty(value?.target?.name, 'Playground target name'), - }), - task: Object.freeze({ + }, + task: { id: nonempty(value?.task?.id, 'Playground task id'), text: nonempty(value?.task?.text, 'Playground task text'), - }), + }, }); assertNoProviderCredentials(identity as PlaygroundJsonValue); return identity; diff --git a/packages/agent-bundle/src/dev/project-service.ts b/packages/agent-bundle/src/dev/project-service.ts index 8ee0582eb..d67577305 100644 --- a/packages/agent-bundle/src/dev/project-service.ts +++ b/packages/agent-bundle/src/dev/project-service.ts @@ -33,6 +33,8 @@ import { writeRouteTypes } from '../routes/typegen.ts'; import type { CompiledRouteGraph } from '../routes/types.ts'; import type { DevRuntimePreparedMcpApp, DevRuntimePreparedMcpServer, DevRuntimePreparedProject } from './runtime-provider.ts'; import { freezeJsonValue, type JsonObject, type JsonValue, type SourceStatus } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export type ProjectCommand = 'build' | 'dev' | 'inspect' | 'validate'; @@ -88,9 +90,9 @@ export interface ProjectSourceSnapshot { readonly revision: string; } -const freezeDiagnostics = (diagnostics: readonly Diagnostic[]): readonly Diagnostic[] => Object.freeze( +const freezeDiagnostics = (diagnostics: readonly Diagnostic[]): readonly Diagnostic[] => deepFreeze( deduplicateDiagnostics(diagnostics.map(withDiagnosticRecovery)) - .map((diagnostic) => Object.freeze({ ...diagnostic })), + .map((diagnostic) => ({ ...diagnostic })), ); const hasErrors = (diagnostics: readonly Diagnostic[]): boolean => @@ -375,7 +377,7 @@ const runtimeDeclaration = ( if (provider === undefined || !('value' in provider) || typeof provider.value !== 'string' || provider.value.trim().length === 0) { return Object.freeze({ diagnostic: sourceDiagnostic('Development runtime provider must be a nonempty project-relative module path.', configPath) }); } - return Object.freeze({ declaration: Object.freeze({ provider: provider.value }) }); + return deepFreeze({ declaration: { provider: provider.value } }); }; const agentApiEnabled = (config: AgentBundleConfig): boolean => { diff --git a/packages/agent-bundle/src/dev/skill-document-service.ts b/packages/agent-bundle/src/dev/skill-document-service.ts index 13e7c0095..4f5539839 100644 --- a/packages/agent-bundle/src/dev/skill-document-service.ts +++ b/packages/agent-bundle/src/dev/skill-document-service.ts @@ -9,6 +9,8 @@ import type { NormalizedSkill, SourceProvenance } from '../core/types.ts'; import { EpochStore } from './epoch-store.ts'; import { ProjectService } from './project-service.ts'; import { isInsideOrEqual } from '../core/paths.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export type SkillDocumentErrorCode = | 'SKILL_DOCUMENT_UNAVAILABLE' @@ -274,7 +276,7 @@ export class SkillDocumentService { .filter((entry) => entry.isDirectory() && !entry.isSymbolicLink() && safeSegment(entry.name)) .sort((left, right) => left.name.localeCompare(right.name)) .map(async (entry) => this.#generatedDocument(epochId, target, `skill:${entry.name}`, targetRoot))); - return Object.freeze({ diagnostics: Object.freeze([]), skills: Object.freeze(documents) }); + return deepFreeze({ diagnostics: [], skills: documents }); }); } diff --git a/packages/agent-bundle/src/dev/workbench-server.ts b/packages/agent-bundle/src/dev/workbench-server.ts index 4e1bbfc7d..cfba107c8 100644 --- a/packages/agent-bundle/src/dev/workbench-server.ts +++ b/packages/agent-bundle/src/dev/workbench-server.ts @@ -54,6 +54,8 @@ import { ScriptPlaygroundService } from './playground/script-playground-service. import { SkillDocumentService } from './skill-document-service.ts'; import { createWorkbenchAssetSource } from './workbench-assets.ts'; import type { Invalidation, ProjectStatus } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface DevServerSession { close(): Promise; @@ -528,10 +530,10 @@ export const startDevServer = async (options: StartDevServerOptions): Promise ProjectStatus = () => Object.freeze({ - artifact: Object.freeze({ state: 'missing' }), - build: Object.freeze({ state: 'idle' }), - source: Object.freeze({ diagnostics: Object.freeze([]), state: 'unknown' }), + let status: () => ProjectStatus = () => deepFreeze({ + artifact: { state: 'missing' }, + build: { state: 'idle' }, + source: { diagnostics: Object.freeze([]), state: 'unknown' }, }); // A provider can start in `compiling`; event delivery retries the no-op // placeholder only after the Workbench has installed its App lifecycle. diff --git a/packages/agent-bundle/src/eval/codex-errors.ts b/packages/agent-bundle/src/eval/codex-errors.ts index 9b072ec6f..bb0932072 100644 --- a/packages/agent-bundle/src/eval/codex-errors.ts +++ b/packages/agent-bundle/src/eval/codex-errors.ts @@ -1,5 +1,7 @@ import { CodedError } from '../core/errors.ts'; import type { EvalHarnessFailure, EvalHarnessFailureCode, EvalHarnessFailureStage } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export type CodexEvalHarnessErrorCode = | 'CODEX_ARTIFACT_INVALID' @@ -22,52 +24,52 @@ export class CodexEvalHarnessError extends CodedError const failureShapes: Readonly ->> = Object.freeze({ - CODEX_ARTIFACT_INVALID: Object.freeze({ +>> = deepFreeze({ + CODEX_ARTIFACT_INVALID: { code: 'EVAL_ARTIFACT_UNAVAILABLE', message: 'The Codex candidate artifact is unavailable.', stage: 'artifact', - }), - CODEX_CLI_INCOMPATIBLE: Object.freeze({ + }, + CODEX_CLI_INCOMPATIBLE: { code: 'EVAL_PROCESS_UNAVAILABLE', message: 'The installed Codex CLI is not compatible with this eval harness.', stage: 'preflight', - }), - CODEX_CLI_MISSING: Object.freeze({ + }, + CODEX_CLI_MISSING: { code: 'EVAL_PROCESS_UNAVAILABLE', message: 'The Codex CLI is not installed.', stage: 'preflight', - }), - CODEX_CLI_UNAUTHENTICATED: Object.freeze({ + }, + CODEX_CLI_UNAUTHENTICATED: { code: 'EVAL_PROCESS_UNAVAILABLE', message: 'The Codex CLI has no signed-in session.', stage: 'preflight', - }), - CODEX_FIXTURE_UNAVAILABLE: Object.freeze({ + }, + CODEX_FIXTURE_UNAVAILABLE: { code: 'EVAL_FIXTURE_UNAVAILABLE', message: 'The trial fixture could not be materialized.', stage: 'fixture', - }), - CODEX_HOME_MUTATED: Object.freeze({ + }, + CODEX_HOME_MUTATED: { code: 'EVAL_PROCESS_UNAVAILABLE', message: 'The normal Codex session state changed during the trial.', stage: 'preflight', - }), - CODEX_PLUGIN_UNAVAILABLE: Object.freeze({ + }, + CODEX_PLUGIN_UNAVAILABLE: { code: 'EVAL_PROCESS_UNAVAILABLE', message: 'The candidate is unavailable in the temporary Codex environment.', stage: 'preflight', - }), - CODEX_TRACE_INVALID: Object.freeze({ + }, + CODEX_TRACE_INVALID: { code: 'EVAL_TRACE_UNAVAILABLE', message: 'The Codex event stream could not be verified.', stage: 'trace', - }), - CODEX_TRIAL_CANCELLED: Object.freeze({ + }, + CODEX_TRIAL_CANCELLED: { code: 'EVAL_TRACE_UNAVAILABLE', message: 'The Codex trial was cancelled before a complete trace was recorded.', stage: 'trace', - }), + }, }); /** Everything the native harness can go wrong at is a harness failure, so no trial blames the plugin. */ diff --git a/packages/agent-bundle/src/eval/codex-plugins.ts b/packages/agent-bundle/src/eval/codex-plugins.ts index 11a5d715e..249c29585 100644 --- a/packages/agent-bundle/src/eval/codex-plugins.ts +++ b/packages/agent-bundle/src/eval/codex-plugins.ts @@ -3,6 +3,8 @@ import { join } from 'node:path'; import { isRecord, parseJsonWithoutDuplicateKeys } from '../core/strict-json.ts'; import { CodexEvalHarnessError } from './codex-errors.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface CodexCandidatePlugin { readonly marketplace: string; @@ -75,16 +77,16 @@ export const readCodexCandidatePlugin = async ( export const codexPluginInstallPlan = ( candidate: CodexCandidatePlugin, candidateDirectory: string, -): readonly CodexInstallStep[] => Object.freeze([ - Object.freeze({ +): readonly CodexInstallStep[] => deepFreeze([ + { args: Object.freeze(['plugin', 'marketplace', 'add', candidateDirectory]), id: 'marketplace.add' as const, - }), - Object.freeze({ + }, + { args: Object.freeze(['plugin', 'add', `${candidate.plugin}@${candidate.marketplace}`]), id: 'plugin.add' as const, - }), - Object.freeze({ args: Object.freeze(['plugin', 'list', '--json']), id: 'plugin.list' as const }), + }, + { args: Object.freeze(['plugin', 'list', '--json']), id: 'plugin.list' as const }, ]); /** Plugin availability is read back from the temporary home's own state, so it is observed. */ diff --git a/packages/agent-bundle/src/eval/fixtures.ts b/packages/agent-bundle/src/eval/fixtures.ts index bd87229a9..6c366ac0a 100644 --- a/packages/agent-bundle/src/eval/fixtures.ts +++ b/packages/agent-bundle/src/eval/fixtures.ts @@ -10,6 +10,8 @@ import { EvalFixtureError } from './errors.ts'; import type { EvalFixture } from './types.ts'; import { isInside } from '../core/paths.ts'; import { isErrno } from '../core/errors.ts'; +import { deepFreeze } from '../core/freeze.ts'; + const runCommand = promisify(execFile); @@ -127,10 +129,10 @@ const initializeGitBaseline = async (workspace: string): Promise => { GIT_CONFIG_GLOBAL: '/dev/null', GIT_CONFIG_SYSTEM: '/dev/null', }; - const commands: readonly (readonly string[])[] = Object.freeze([ - Object.freeze(['-c', 'init.defaultBranch=main', 'init', '--quiet']), - Object.freeze(['add', '--all']), - Object.freeze(['commit', '--no-gpg-sign', '--quiet', '--allow-empty', '--message', 'Eval fixture baseline']), + const commands: readonly (readonly string[])[] = deepFreeze([ + ['-c', 'init.defaultBranch=main', 'init', '--quiet'], + ['add', '--all'], + ['commit', '--no-gpg-sign', '--quiet', '--allow-empty', '--message', 'Eval fixture baseline'], ]); for (const args of commands) { try { diff --git a/packages/agent-bundle/src/eval/graders.ts b/packages/agent-bundle/src/eval/graders.ts index fca0e228e..d853b69d0 100644 --- a/packages/agent-bundle/src/eval/graders.ts +++ b/packages/agent-bundle/src/eval/graders.ts @@ -14,6 +14,8 @@ import type { EvalScriptOutcome, } from './types.ts'; import { isErrno } from '../core/errors.ts'; +import { deepFreeze } from '../core/freeze.ts'; + const runCommand = promisify(execFile); @@ -229,7 +231,7 @@ export const runEvalGraders = async ( results[spec.id] = outcome('inconclusive', evalGraderFailureMessage); } } - return Object.freeze({ failures: Object.freeze(failures), results: Object.freeze(results) }); + return deepFreeze({ failures: failures, results: results }); }; export const evalScriptGraderSpec = (script: string, suiteDir: string): EvalScriptGraderSpec => diff --git a/packages/agent-bundle/src/eval/harness.ts b/packages/agent-bundle/src/eval/harness.ts index 9f419fa75..e9e03dd9c 100644 --- a/packages/agent-bundle/src/eval/harness.ts +++ b/packages/agent-bundle/src/eval/harness.ts @@ -21,6 +21,8 @@ import type { EvalPluginFailure, EvalTrialEvidence, } from './types.ts'; +import { deepFreeze } from '../core/freeze.ts'; + export interface EvalHarness { readonly kind: 'deterministic' | 'native-claude' | 'native-codex'; @@ -81,11 +83,11 @@ const unavailableActivation: EvalActivationEvidence = Object.freeze({ }); /** Every channel unavailable: the shape a harness reports when it observed nothing at all. */ -export const unavailableEvidence: EvalTrialEvidence = Object.freeze({ - mcp: Object.freeze({ calls: Object.freeze([]), level: 'unavailable' }), - process: Object.freeze({ level: 'unavailable', timedOut: false }), - scripts: Object.freeze({ level: 'unavailable', results: Object.freeze({}) }), - skillActivation: Object.freeze({ activated: Object.freeze([]), level: 'unavailable' }), +export const unavailableEvidence: EvalTrialEvidence = deepFreeze({ + mcp: { calls: Object.freeze([]), level: 'unavailable' }, + process: { level: 'unavailable', timedOut: false }, + scripts: { level: 'unavailable', results: Object.freeze({}) }, + skillActivation: { activated: Object.freeze([]), level: 'unavailable' }, }); const harnessKinds: Readonly> = Object.freeze({ diff --git a/packages/agent-bundle/src/events/project.ts b/packages/agent-bundle/src/events/project.ts index ff45f002d..6eecc90bc 100644 --- a/packages/agent-bundle/src/events/project.ts +++ b/packages/agent-bundle/src/events/project.ts @@ -9,6 +9,8 @@ import type { AgentEventRouteProps, CanonicalAgentEvent, } from '../routes/public.ts'; +import { deepFreeze } from '../core/freeze.ts'; + const resultValueSchema = z.object({ outcome: z.enum(['continue', 'deny']).optional(), @@ -133,11 +135,11 @@ export const projectEventDocument = ( if (additionalContext === undefined) return undefined; return target === 'cursor' ? Object.freeze({ additional_context: additionalContext }) - : Object.freeze({ - hookSpecificOutput: Object.freeze({ + : deepFreeze({ + hookSpecificOutput: { additionalContext, hookEventName: nativeEvent, - }), + }, }); } if (event === 'agent/stop') { @@ -156,11 +158,11 @@ export const projectEventDocument = ( } return target === 'cursor' ? Object.freeze({ additional_context: additionalContext }) - : Object.freeze({ - hookSpecificOutput: Object.freeze({ + : deepFreeze({ + hookSpecificOutput: { additionalContext, hookEventName: nativeEvent, - }), + }, }); } if (event === 'tool/before') { @@ -183,17 +185,17 @@ export const projectEventDocument = ( ...(parsedValue?.reason === undefined ? {} : { permissionDecisionReason: parsedValue.reason }), ...(parsedValue?.updatedInput === undefined ? {} : { updatedInput: parsedValue.updatedInput }), }; - return Object.freeze({ hookSpecificOutput: Object.freeze(output) }); + return deepFreeze({ hookSpecificOutput: output }); } if (event === 'session/start' || event === 'tool/after') { if (additionalContext === undefined) return undefined; return target === 'cursor' ? Object.freeze({ additional_context: additionalContext }) - : Object.freeze({ - hookSpecificOutput: Object.freeze({ + : deepFreeze({ + hookSpecificOutput: { additionalContext, hookEventName: nativeEvent, - }), + }, }); } return undefined; diff --git a/packages/agent-bundle/src/services/hook-service.ts b/packages/agent-bundle/src/services/hook-service.ts index 662a325b4..6623b39c0 100644 --- a/packages/agent-bundle/src/services/hook-service.ts +++ b/packages/agent-bundle/src/services/hook-service.ts @@ -12,6 +12,8 @@ import { import { validateArtifact } from '../build/validate-artifact.ts'; import { parseArtifactHookIndex } from '../build/hook-index.ts'; import { taskkill, terminateProcessTree, type ProcessTreeTaskkill } from './process-tree.ts'; +import { deepFreeze } from '../core/freeze.ts'; + const defaultTimeoutMs = 5_000; const maxStreamBytes = 1_000_000; @@ -263,7 +265,7 @@ export class HookService { const hooks = index.hooks.filter((hook) => { return options.target === undefined || hook.target === options.target; }); - return Object.freeze(hooks.map((hook) => Object.freeze({ ...hook }))); + return deepFreeze(hooks.map((hook) => ({ ...hook }))); } async simulate(options: HookSimulationOptions): Promise { diff --git a/packages/agent-bundle/src/skills/parse-ir.ts b/packages/agent-bundle/src/skills/parse-ir.ts index b1d1a94a2..1913a3621 100644 --- a/packages/agent-bundle/src/skills/parse-ir.ts +++ b/packages/agent-bundle/src/skills/parse-ir.ts @@ -205,7 +205,7 @@ const codexFrom = (value: unknown): CodexSkillExtension | undefined => { : { allowImplicitInvocation: pickBoolean(policy, 'allowImplicitInvocation', 'allow_implicit_invocation') }), }), }), - ...(tools === undefined ? {} : { dependencies: Object.freeze({ tools: Object.freeze(tools) }) }), + ...(tools === undefined ? {} : { dependencies: deepFreeze({ tools: tools }) }), }; return Object.keys(extension).length === 0 ? undefined : Object.freeze(extension); }; @@ -403,7 +403,7 @@ export const parseSkillIr = (document: SkillDocument): SkillIr => { passThrough, placeholders: Object.freeze(placeholders), portable: portableFrom(frontmatter), - resources: Object.freeze(document.resources.map((resource) => Object.freeze({ + resources: deepFreeze(document.resources.map((resource) => ({ bytes: resource.bytes, relativePath: resource.relativePath, source: resource.source, diff --git a/packages/agent-bundle/src/skills/tokens.ts b/packages/agent-bundle/src/skills/tokens.ts index f36594188..5f434c112 100644 --- a/packages/agent-bundle/src/skills/tokens.ts +++ b/packages/agent-bundle/src/skills/tokens.ts @@ -1,3 +1,5 @@ +import { deepFreeze } from '../core/freeze.ts'; + /** * Canonical Skill / plugin-surface tokens. Build-time lowering substitutes * host syntax only; runtime values are never resolved here. @@ -158,13 +160,13 @@ export const classifySkillToken = ( table[host][document]?.[token] ?? none(token, host, document, noSkillMarkdown[host]); /** Host-native spellings that parse as a canonical token. Longest match wins. */ -export const skillTokenAliases: Readonly> = Object.freeze({ - arguments: Object.freeze(['$ARGUMENTS']), - pluginData: Object.freeze(['${CLAUDE_PLUGIN_DATA}', '${PLUGIN_DATA}']), - pluginRoot: Object.freeze(['${CLAUDE_PLUGIN_ROOT}', '${CURSOR_PLUGIN_ROOT}', '${PLUGIN_ROOT}']), - projectRoot: Object.freeze(['${CLAUDE_PROJECT_DIR}', '${workspaceFolder}']), - sessionIdentity: Object.freeze(['${CLAUDE_SESSION_ID}']), - skillRoot: Object.freeze(['${CLAUDE_SKILL_DIR}']), +export const skillTokenAliases: Readonly> = deepFreeze({ + arguments: ['$ARGUMENTS'], + pluginData: ['${CLAUDE_PLUGIN_DATA}', '${PLUGIN_DATA}'], + pluginRoot: ['${CLAUDE_PLUGIN_ROOT}', '${CURSOR_PLUGIN_ROOT}', '${PLUGIN_ROOT}'], + projectRoot: ['${CLAUDE_PROJECT_DIR}', '${workspaceFolder}'], + sessionIdentity: ['${CLAUDE_SESSION_ID}'], + skillRoot: ['${CLAUDE_SKILL_DIR}'], }); const aliasEntries = (Object.entries(skillTokenAliases) as [SkillTokenId, readonly string[]][]) diff --git a/packages/agent-bundle/tests/artifact-routes.test.ts b/packages/agent-bundle/tests/artifact-routes.test.ts index 43d789640..c13e47e9d 100644 --- a/packages/agent-bundle/tests/artifact-routes.test.ts +++ b/packages/agent-bundle/tests/artifact-routes.test.ts @@ -13,6 +13,8 @@ import { startRoutes as startRouteServer, type StartedRoutes, } from './support/route-harness.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const startRoutes = async (service?: ArtifactRouteService): Promise> => startRouteServer(new ArtifactRoutes({ authorize, ...(service === undefined ? {} : { service }) })); @@ -93,12 +95,12 @@ it('rejects diff queries that omit, duplicate, or smuggle a parameter', async () it('surfaces artifact validation diagnostics instead of one opaque failure', async () => { const service = new RecordingService(); - const diagnostics = Object.freeze([Object.freeze({ + const diagnostics = deepFreeze([{ code: 'AB4300', generatedPath: 'claude/hooks/guard.mjs', message: 'Emitted hook wrapper is not executable.', severity: 'error' as const, - })]); + }]); service.failure = new ArtifactInspectionServiceError( 'ARTIFACT_INSPECTION_INVALID', '/private/epochs/epoch-a failed validation', diff --git a/packages/agent-bundle/tests/dev-server.test.ts b/packages/agent-bundle/tests/dev-server.test.ts index d552b59f7..6761f76cc 100644 --- a/packages/agent-bundle/tests/dev-server.test.ts +++ b/packages/agent-bundle/tests/dev-server.test.ts @@ -18,6 +18,8 @@ import { import { ArtifactInspectionServiceError } from '../src/dev/artifacts/artifact-inspection-service.ts'; import type { EvalRouteService } from '../src/dev/eval/eval-routes.ts'; import type { McpSessionService } from '../src/dev/mcp-session/mcp-session-service.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const status = (): ProjectStatus => ({ artifact: { state: 'missing' }, @@ -833,12 +835,12 @@ it('accepts headerless browser same-origin fetch provenance with the exact token }); it('forwards structured artifact validation diagnostics through the foreground error boundary', async () => { - const diagnostics = Object.freeze([Object.freeze({ + const diagnostics = deepFreeze([{ code: 'AB4300', generatedPath: 'claude/hooks/guard.mjs', message: 'Emitted hook wrapper is not executable.', severity: 'error' as const, - })]); + }]); const server = await startForegroundServer({ artifacts: { diff: () => Promise.reject(new Error('unused')), diff --git a/packages/agent-bundle/tests/eval-claude-harness.test.ts b/packages/agent-bundle/tests/eval-claude-harness.test.ts index 9d96cb616..ee059609a 100644 --- a/packages/agent-bundle/tests/eval-claude-harness.test.ts +++ b/packages/agent-bundle/tests/eval-claude-harness.test.ts @@ -28,10 +28,12 @@ import type { NativeClaudeProcessResult, } from '../src/host-contracts/native-claude-contract.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const nativeIt = process.env.AGENT_BUNDLE_NATIVE_CLAUDE_SMOKE === '1' ? it : it.skip; -const hosts = Object.freeze({ claude: Object.freeze({ model: 'claude-sonnet-4-5' }) }); +const hosts = deepFreeze({ claude: { model: 'claude-sonnet-4-5' } }); const hostileEnvironment: Readonly = Object.freeze({ ANTHROPIC_API_KEY: 'must-not-reach-child', diff --git a/packages/agent-bundle/tests/eval-harness.test.ts b/packages/agent-bundle/tests/eval-harness.test.ts index 7f05d7bc6..98bc54ec2 100644 --- a/packages/agent-bundle/tests/eval-harness.test.ts +++ b/packages/agent-bundle/tests/eval-harness.test.ts @@ -5,6 +5,7 @@ import { join } from 'node:path'; import { expect, it } from '@rstest/core'; import { build } from '../src/api.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; import { aggregateEvalTrials, createEvalHarness, @@ -28,7 +29,7 @@ import { } from '../src/eval/index.ts'; import { createProjectFixture } from './helpers/project-fixture.ts'; -const hosts = Object.freeze({ portable: Object.freeze({ model: 'deterministic' }) }); +const hosts = deepFreeze({ portable: { model: 'deterministic' } }); const suiteFor = (assertions: EvalCase['assertions']) => defineEvalSuite({ cases: [{ diff --git a/packages/agent-bundle/tests/eval-routes.test.ts b/packages/agent-bundle/tests/eval-routes.test.ts index 56effc1ea..df59c85e1 100644 --- a/packages/agent-bundle/tests/eval-routes.test.ts +++ b/packages/agent-bundle/tests/eval-routes.test.ts @@ -15,6 +15,8 @@ import { } from '../src/dev/eval/eval-service.ts'; import type { EvalComparison } from '../src/eval/compare.ts'; import type { EvalRunEvent, EvalRunRecord, EvalTrialRecord } from '../src/eval/run-store.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + interface StartedRoutes { readonly close: () => Promise; @@ -99,9 +101,9 @@ const runResult: EvalRunResult = Object.freeze({ trials: Object.freeze([trialRecord]), }); -const suiteListing: EvalSuiteListing = Object.freeze({ - diagnostics: Object.freeze([]), - suites: Object.freeze([Object.freeze({ +const suiteListing: EvalSuiteListing = deepFreeze({ + diagnostics: [], + suites: [Object.freeze({ cases: Object.freeze([Object.freeze({ assertions: Object.freeze([Object.freeze({ id: 'outcome:0123456789abcdef', kind: 'outcome' as const })]), digest: 'a'.repeat(64), @@ -114,15 +116,15 @@ const suiteListing: EvalSuiteListing = Object.freeze({ digest: 'e'.repeat(64), name: 'review-change', sourcePath: 'evals/review.eval.ts', - })]), + })], }); -const comparisonRecord = Object.freeze({ - baseline: Object.freeze({ runId: 'run-a' }), - candidate: Object.freeze({ runId: 'run-b' }), - rows: Object.freeze([]), +const comparisonRecord = deepFreeze({ + baseline: { runId: 'run-a' }, + candidate: { runId: 'run-b' }, + rows: [], sampleSize: 3, - summary: Object.freeze({ comparable: 0, nonComparable: 0, reliability: 0, smoke: 0 }), + summary: { comparable: 0, nonComparable: 0, reliability: 0, smoke: 0 }, }) as unknown as EvalComparison; class RecordingService implements EvalRouteService { @@ -155,13 +157,13 @@ class RecordingService implements EvalRouteService { async events(runId: string, afterSequence: number) { this.calls.push({ afterSequence, kind: 'events', runId }); if (this.failure !== undefined) throw this.failure; - const events = Object.freeze([ - Object.freeze({ kind: 'run.started', payload: Object.freeze({}), sequence: 1, timestamp: '2026-08-17T00:00:00.000Z' }), - Object.freeze({ kind: 'trial.completed', payload: Object.freeze({}), sequence: 2, timestamp: '2026-08-17T00:00:01.000Z' }), + const events = deepFreeze([ + { kind: 'run.started', payload: Object.freeze({}), sequence: 1, timestamp: '2026-08-17T00:00:00.000Z' }, + { kind: 'trial.completed', payload: Object.freeze({}), sequence: 2, timestamp: '2026-08-17T00:00:01.000Z' }, ]); - return Object.freeze({ - cursor: Object.freeze({ afterSequence: 2 }), - events: Object.freeze(events.filter((event) => event.sequence > afterSequence)), + return deepFreeze({ + cursor: { afterSequence: 2 }, + events: events.filter((event) => event.sequence > afterSequence), }); } diff --git a/packages/agent-bundle/tests/hook-playground-routes.test.ts b/packages/agent-bundle/tests/hook-playground-routes.test.ts index 70e8a72bd..5547e212c 100644 --- a/packages/agent-bundle/tests/hook-playground-routes.test.ts +++ b/packages/agent-bundle/tests/hook-playground-routes.test.ts @@ -20,6 +20,8 @@ import { startRoutes as startRouteServer, type StartedRoutes, } from './support/route-harness.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const startRoutes = async ( service?: HookPlaygroundRouteService, @@ -32,49 +34,49 @@ const startRoutes = async ( ...(service === undefined ? {} : { service }), }), { closeMode: 'awaited' }); -const hookFixture: HookPlaygroundHook = Object.freeze({ - binding: Object.freeze({ epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }), - hook: Object.freeze({ +const hookFixture: HookPlaygroundHook = deepFreeze({ + binding: { epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }, + hook: { event: 'sessionStart', id: 'hook-a', name: 'guard', path: 'hooks/guard.mjs', target: 'claude', - }), + }, }); -const simulationFixture: HookPlaygroundSimulation = Object.freeze({ - binding: Object.freeze({ epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }), - canonicalIntent: Object.freeze({ +const simulationFixture: HookPlaygroundSimulation = deepFreeze({ + binding: { epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }, + canonicalIntent: { event: 'sessionStart', hook: 'hook-a', input: Object.freeze({ prompt: 'hello' }), - }), - canonicalResult: Object.freeze({ decision: 'allow' }), - hostMapping: Object.freeze({ + }, + canonicalResult: { decision: 'allow' }, + hostMapping: { canonicalEvent: 'sessionStart', nativeEvent: 'SessionStart', nativeProjection: 'deterministic', nativeSelector: 'hooks.SessionStart[0]', target: 'claude', wrapperPath: 'hooks/guard.mjs', - }), - nativeInput: Object.freeze({ prompt: 'hello' }), - nativeOutput: Object.freeze({ decision: 'approve' }), - replay: Object.freeze({ + }, + nativeInput: { prompt: 'hello' }, + nativeOutput: { decision: 'approve' }, + replay: { binding: Object.freeze({ epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }), input: Object.freeze({ prompt: 'hello' }), - }), + }, }); -const diagnosticFixture: HookPlaygroundDiagnosticResult = Object.freeze({ - diagnostics: Object.freeze([Object.freeze({ +const diagnosticFixture: HookPlaygroundDiagnosticResult = deepFreeze({ + diagnostics: [Object.freeze({ code: 'hook.playground.target.unsupported' as const, event: 'sessionStart', message: 'Target "codex" does not support this hook event.', severity: 'error' as const, target: 'codex', - })]), + })], }); class RecordingService implements HookPlaygroundRouteService { @@ -511,9 +513,9 @@ const simulationBody = Object.freeze({ target: 'claude', }); -const replayBody = Object.freeze({ - binding: Object.freeze({ epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }), - input: Object.freeze({ prompt: 'hello' }), +const replayBody = deepFreeze({ + binding: { epochId: 'epoch-a', hook: 'hook-a', target: 'claude' }, + input: { prompt: 'hello' }, }); /** Sends a hook simulation whose body is completed only when the test releases it. */ diff --git a/packages/agent-bundle/tests/mcp-app-routes.test.ts b/packages/agent-bundle/tests/mcp-app-routes.test.ts index dafabc933..1d4af1fdc 100644 --- a/packages/agent-bundle/tests/mcp-app-routes.test.ts +++ b/packages/agent-bundle/tests/mcp-app-routes.test.ts @@ -12,6 +12,8 @@ import { runtimeAppMessageLimits } from '../src/dev/runtime-app-message-limits.t import { McpAppRuntimePreviewError } from '../src/dev/mcp-app-runtime-preview-service.ts'; import type { McpAppRuntimeRoutePreviewService } from '../src/dev/mcp-app-runtime-preview-service.ts'; import type { McpAppBridgeLifecycle, McpAppBridgeMessage } from '../src/dev/mcp-apps/mcp-app-bridge.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + interface StartedRoutes { readonly close: () => Promise; @@ -374,7 +376,7 @@ it('forwards one request-owned abort signal to each admitted runtime App operati : undefined, operate: async (_bindingId, _operation, options?: Readonly<{ readonly signal?: AbortSignal }>) => { signals.push(options?.signal); - return Object.freeze({ result: Object.freeze({ content: Object.freeze([]) }) }) as never; + return deepFreeze({ result: { content: Object.freeze([]) } }) as never; }, }; const started = await startRoutes(Object.assign(new RecordingPreviewService(), { runtime })); diff --git a/packages/agent-bundle/tests/native-playground-service.test.ts b/packages/agent-bundle/tests/native-playground-service.test.ts index 927f09f24..614427798 100644 --- a/packages/agent-bundle/tests/native-playground-service.test.ts +++ b/packages/agent-bundle/tests/native-playground-service.test.ts @@ -15,6 +15,8 @@ import { import type { DiscoveredEvalSuite } from '../src/eval/discovery.ts'; import type { EvalFixturePlan } from '../src/eval/fixtures.ts'; import { defineEvalSuite, normalizeEvalCase } from '../src/eval/suite.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const epoch = (id: string, root: string, target?: 'claude' | 'codex') => Object.freeze({ close: async () => undefined, @@ -126,7 +128,7 @@ it('persists a canonical suite whose authored case order differs from digest ord const cases = ['alpha-case', 'beta-case'].map((id) => normalizeEvalCase({ assertions: Object.freeze([expectExitCode(0)]), fixture: Object.freeze({ git: false, include: Object.freeze(['**/*']), path: './fixture' }), - hosts: Object.freeze({ claude: Object.freeze({ model: 'pinned-claude-model' }) }), + hosts: deepFreeze({ claude: { model: 'pinned-claude-model' } }), id, invocation: Object.freeze({ mode: 'automatic' as const }), prompt: `Review ${id}.`, @@ -473,7 +475,7 @@ it('requires every persisted fixture sha256 to be exactly 64 lowercase hexadecim const fixtureDir = join(suiteDir, 'fixture'); await mkdir(fixtureDir, { recursive: true }); await writeFile(join(fixtureDir, 'input.txt'), 'fixture bytes\n'); - const entries = Object.freeze([Object.freeze({ executable: false, path: 'input.txt', sha256 })]); + const entries = deepFreeze([{ executable: false, path: 'input.txt', sha256 }]); const service = new NativePlaygroundService({ catalogDirectory: join(root, 'catalog'), discover: async () => nativeSuite('claude', join(suiteDir, 'review.eval.ts')), diff --git a/packages/agent-bundle/tests/playground-orchestration-service.test.ts b/packages/agent-bundle/tests/playground-orchestration-service.test.ts index 1470510eb..2e907c629 100644 --- a/packages/agent-bundle/tests/playground-orchestration-service.test.ts +++ b/packages/agent-bundle/tests/playground-orchestration-service.test.ts @@ -31,6 +31,8 @@ import type { import { PlaygroundStore } from '../src/dev/playground/playground-store.ts'; import type { ProjectStatus } from '../src/dev/types.ts'; import { eventuallyPasses } from './support/eventually.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const activeEpoch = Object.freeze({ configDigest: 'config-sha256', @@ -43,10 +45,10 @@ const activeEpoch = Object.freeze({ targetDigests: Object.freeze({ codex: 'target-sha256' }), }); -const currentStatus = (): ProjectStatus => Object.freeze({ - artifact: Object.freeze({ activeEpoch, currentSourceRevision: 'revision-sha256', state: 'active' as const }), - build: Object.freeze({ state: 'idle' as const }), - source: Object.freeze({ diagnostics: Object.freeze([]), state: 'ready' as const }), +const currentStatus = (): ProjectStatus => deepFreeze({ + artifact: { activeEpoch, currentSourceRevision: 'revision-sha256', state: 'active' as const }, + build: { state: 'idle' as const }, + source: { diagnostics: Object.freeze([]), state: 'ready' as const }, }); const eventually = (assertion: () => void): Promise => diff --git a/packages/agent-bundle/tests/runtime-mcp-registry.test.ts b/packages/agent-bundle/tests/runtime-mcp-registry.test.ts index 33aa05d3e..7c10f3f7f 100644 --- a/packages/agent-bundle/tests/runtime-mcp-registry.test.ts +++ b/packages/agent-bundle/tests/runtime-mcp-registry.test.ts @@ -3,6 +3,7 @@ import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { expect, it } from '@rstest/core'; +import { deepFreeze } from '../src/core/freeze.ts'; import { RuntimeGenerationStore, @@ -270,9 +271,9 @@ const createRegistry = (input: Readonly<{ return () => `session-${++next}`; })(), emit: (event) => { input.events?.push(event); }, - executor: input.executor ?? (async (context) => Object.freeze({ + executor: input.executor ?? (async (context) => deepFreeze({ stateVersion: 7, - value: Object.freeze({ kind: context.request.kind, generation: context.generation.id }), + value: { kind: context.request.kind, generation: context.generation.id }, })), generationStore: input.store, initialRegistry: input.initialRegistry ?? registryInput(), @@ -344,7 +345,7 @@ it('leases a generation per blocked operation while implementation updates prese entered.resolve(); await release.promise; } - return Object.freeze({ stateVersion: 3, value: Object.freeze({ generation: context.generation.id }) }); + return deepFreeze({ stateVersion: 3, value: { generation: context.generation.id } }); }, store: fixture.store, }); @@ -382,7 +383,7 @@ it('returns static lists and complete leased vectors for every MCP operation', a const registry = createRegistry({ executor: async (context) => { observed.push(context); - return Object.freeze({ stateVersion: 42, value: Object.freeze({ result: context.request.kind }) }); + return deepFreeze({ stateVersion: 42, value: { result: context.request.kind } }); }, store: fixture.store, }); @@ -1151,7 +1152,7 @@ it('does not execute an already cancelled Runtime MCP operation after leasing it const registry = createRegistry({ executor: async () => { executions += 1; - return Object.freeze({ stateVersion: 1, value: Object.freeze({ unexpected: true }) }); + return deepFreeze({ stateVersion: 1, value: { unexpected: true } }); }, store: fixture.store, }); @@ -1187,7 +1188,7 @@ it('prepares private activation without public visibility, commits synchronously entered.resolve(); await release.promise; } - return Object.freeze({ stateVersion: 9, value: Object.freeze({ generation: context.generation.id }) }); + return deepFreeze({ stateVersion: 9, value: { generation: context.generation.id } }); }, store: fixture.store, }); diff --git a/packages/agent-bundle/tests/runtime-mcp-routes.test.ts b/packages/agent-bundle/tests/runtime-mcp-routes.test.ts index bb3b5659e..2dd18d028 100644 --- a/packages/agent-bundle/tests/runtime-mcp-routes.test.ts +++ b/packages/agent-bundle/tests/runtime-mcp-routes.test.ts @@ -7,6 +7,8 @@ import { RuntimeMcpRoutes } from '../src/dev/runtime-mcp-routes.ts'; import type { DevRuntimeSession } from '../src/dev/runtime-provider.ts'; import { ProjectEventHub, startForegroundServer } from '../src/dev/index.ts'; import type { ProjectStatus } from '../src/dev/types.ts'; +import { deepFreeze } from '../src/core/freeze.ts'; + const authorize = (request: IncomingMessage): void => { if (request.headers.origin !== 'http://127.0.0.1:4567' || request.headers['x-agent-bundle-session'] !== 'runtime-token') { @@ -140,7 +142,7 @@ it('claims manual runtime MCP routes before the generic runtime browser API', as }, }, } as unknown as DevRuntimeSession; - const status: ProjectStatus = Object.freeze({ artifact: Object.freeze({ state: 'missing' }), build: Object.freeze({ state: 'idle' }), source: Object.freeze({ diagnostics: Object.freeze([]), state: 'unknown' }) }); + const status: ProjectStatus = deepFreeze({ artifact: { state: 'missing' }, build: { state: 'idle' }, source: { diagnostics: Object.freeze([]), state: 'unknown' } }); const server = await startForegroundServer({ coordinator: Object.freeze({ close: async () => undefined, rebuild: async () => undefined, start: async () => undefined, status: () => status }), eventHub: new ProjectEventHub(), diff --git a/packages/agent-bundle/tests/support/manifest.ts b/packages/agent-bundle/tests/support/manifest.ts index 3f4ba2bec..b41c266d6 100644 --- a/packages/agent-bundle/tests/support/manifest.ts +++ b/packages/agent-bundle/tests/support/manifest.ts @@ -3,12 +3,14 @@ import { listArtifactFiles, writeHookIndex, writeManifest } from '../../src/buil import type { ArtifactManifest } from '../../src/build/manifest.ts'; import { digest } from '../../src/core/digest.ts'; import { agentSkillsSchemaRevision } from '../../src/schemas/agent-skills/contract.ts'; +import { deepFreeze } from '../../src/core/freeze.ts'; + const fixtureConfigDigest = 'a'.repeat(64); -const fixtureSourceInputs = Object.freeze([Object.freeze({ +const fixtureSourceInputs = deepFreeze([{ path: 'agent-bundle.config.ts', sha256: fixtureConfigDigest, -})]); +}]); export const writeFixtureManifest = async (options: { readonly artifactRoot: string; diff --git a/packages/agent-bundle/tests/support/packed-native-smoke.ts b/packages/agent-bundle/tests/support/packed-native-smoke.ts index c88413503..08efbd53b 100644 --- a/packages/agent-bundle/tests/support/packed-native-smoke.ts +++ b/packages/agent-bundle/tests/support/packed-native-smoke.ts @@ -18,6 +18,8 @@ import { promisify } from 'node:util'; // The native smoke installs the production closure a real consumer would get, // so it stays on npm's default metadata staleness checks. import { npmInstallArguments, sharedPackedTarball } from './shared-pack.ts'; +import { deepFreeze } from '../../src/core/freeze.ts'; + const execFile = promisify(executeFile); const workspaceRoot = process.cwd(); @@ -378,9 +380,9 @@ export const runPackedNativeSmoke = async (options: { } } - return Object.freeze({ - hosts: Object.freeze(reports), - package: Object.freeze({ externalBinary: true, productionOnly: true, tarballs: 1 }), + return deepFreeze({ + hosts: reports, + package: { externalBinary: true, productionOnly: true, tarballs: 1 }, }); } finally { await rm(root, { force: true, recursive: true }); diff --git a/packages/workbench/src/artifacts/artifacts-model.ts b/packages/workbench/src/artifacts/artifacts-model.ts index 0f1c72a85..55503095b 100644 --- a/packages/workbench/src/artifacts/artifacts-model.ts +++ b/packages/workbench/src/artifacts/artifacts-model.ts @@ -11,6 +11,8 @@ import type { ArtifactInspectionTarget, ArtifactInspectionTreeNode, } from '../../../agent-bundle/src/contracts/artifacts.ts'; +import { deepFreeze } from '../freeze.ts'; + export type ArtifactDiffChange = 'added' | 'changed' | 'removed' | 'unchanged'; @@ -192,9 +194,9 @@ export const artifactTreeRowsFor = (target: ArtifactInspectionTarget): readonly export const artifactTargetOptionsFor = ( targets: readonly ArtifactInspectionTarget[], -): readonly ArtifactTargetOption[] => Object.freeze( +): readonly ArtifactTargetOption[] => deepFreeze( targets - .map((target): ArtifactTargetOption => Object.freeze({ key: target.name, label: target.name, name: target.name })) + .map((target): ArtifactTargetOption => ({ key: target.name, label: target.name, name: target.name })) .sort((left, right) => left.key.localeCompare(right.key)), ); @@ -208,9 +210,8 @@ export const artifactEpochIdentityRowsFor = (inspection: ArtifactInspection): re row('Emitted files', String(inspection.files.length)), ]); -export const artifactRuntimeViewFor = (runtime: ArtifactInspectionRuntime): ArtifactRuntimeView => Object.freeze({ - executables: Object.freeze( - runtime.executables +export const artifactRuntimeViewFor = (runtime: ArtifactInspectionRuntime): ArtifactRuntimeView => deepFreeze({ + executables: runtime.executables .map((file): ArtifactExecutableRow => Object.freeze({ bytes: file.bytes, key: file.path, @@ -220,9 +221,7 @@ export const artifactRuntimeViewFor = (runtime: ArtifactInspectionRuntime): Arti sha256: file.sha256, })) .sort((left, right) => left.key.localeCompare(right.key)), - ), - hooks: Object.freeze( - runtime.hooks + hooks: runtime.hooks .map((hook): ArtifactHookRow => Object.freeze({ bytes: hook.file.bytes, event: hook.event, @@ -234,9 +233,7 @@ export const artifactRuntimeViewFor = (runtime: ArtifactInspectionRuntime): Arti ...(hook.timeout === undefined ? {} : { timeout: hook.timeout }), })) .sort((left, right) => left.key.localeCompare(right.key)), - ), - mcpServers: Object.freeze( - runtime.mcpServers + mcpServers: runtime.mcpServers .map((server): ArtifactMcpServerRow => Object.freeze({ entryPaths: Object.freeze([...server.entryPaths].sort((left, right) => left.localeCompare(right))), key: `${server.target}/${server.name}`, @@ -246,14 +243,13 @@ export const artifactRuntimeViewFor = (runtime: ArtifactInspectionRuntime): Arti target: server.target, })) .sort((left, right) => left.key.localeCompare(right.key)), - ), }); export const artifactProvenanceRowsFor = ( provenance: readonly ArtifactInspectionProvenance[], -): readonly ArtifactProvenanceRow[] => Object.freeze( +): readonly ArtifactProvenanceRow[] => deepFreeze( provenance - .map((entry): ArtifactProvenanceRow => Object.freeze({ + .map((entry): ArtifactProvenanceRow => ({ key: entry.outputPath, outputPath: entry.outputPath, sourceInputs: Object.freeze( @@ -272,9 +268,9 @@ const diffGroup = ( readonly path: string; }>[], ): ArtifactDiffGroup => { - const rows = Object.freeze( + const rows = deepFreeze( entries - .map((entry): ArtifactDiffRow => Object.freeze({ + .map((entry): ArtifactDiffRow => ({ ...(entry.after === undefined ? {} : { afterBytes: entry.after.bytes, afterSha256: entry.after.sha256 }), ...(entry.before === undefined ? {} : { beforeBytes: entry.before.bytes, beforeSha256: entry.before.sha256 }), change, diff --git a/packages/workbench/src/comparisons/comparisons-model.ts b/packages/workbench/src/comparisons/comparisons-model.ts index 8f8d84bb8..b4d73a9dd 100644 --- a/packages/workbench/src/comparisons/comparisons-model.ts +++ b/packages/workbench/src/comparisons/comparisons-model.ts @@ -8,6 +8,8 @@ import type { EvalNonComparableReason, EvalRunRecord, } from '../../../agent-bundle/src/contracts/eval.ts'; +import { deepFreeze } from '../freeze.ts'; + export type ComparisonsState = 'compared' | 'empty' | 'insufficient-runs' | 'ready'; @@ -139,7 +141,7 @@ const usageDelta = ( }; export const comparisonRunOptionsFor = (runs: readonly EvalRunRecord[]): readonly ComparisonRunOption[] => - Object.freeze(runs.map((run) => Object.freeze({ + deepFreeze(runs.map((run) => ({ key: run.id, label: `${run.id} · ${run.harness} · ${run.createdAt}`, }))); diff --git a/packages/workbench/src/evals/evals-model.ts b/packages/workbench/src/evals/evals-model.ts index 418520771..8a9566adf 100644 --- a/packages/workbench/src/evals/evals-model.ts +++ b/packages/workbench/src/evals/evals-model.ts @@ -16,6 +16,8 @@ import type { } from '../../../agent-bundle/src/contracts/eval.ts'; import type { EvalRunStart } from './eval-client.ts'; import { jsonEquivalent } from '../client-helpers.ts'; +import { deepFreeze } from '../freeze.ts'; + export type EvalPageState = 'empty' | 'loading' | 'ran' | 'ready'; @@ -260,8 +262,8 @@ export const evalOutcomeLabel = (outcome: EvalAssertionOutcome): string => outco export const evalSuiteOptionsFor = ( suites: readonly EvalSuiteSummary[], -): readonly EvalSuiteOption[] => Object.freeze(suites - .map((suite): EvalSuiteOption => Object.freeze({ +): readonly EvalSuiteOption[] => deepFreeze(suites + .map((suite): EvalSuiteOption => ({ cases: suite.cases.length, key: suite.name, label: `${suite.name} · ${suite.cases.length} case(s) · ${suite.sourcePath}`, @@ -270,8 +272,8 @@ export const evalSuiteOptionsFor = ( })) .sort((left, right) => left.key.localeCompare(right.key))); -export const evalCaseRowsFor = (cases: readonly EvalCaseSummary[]): readonly EvalCaseRow[] => Object.freeze( - cases.map((entry): EvalCaseRow => Object.freeze({ +export const evalCaseRowsFor = (cases: readonly EvalCaseSummary[]): readonly EvalCaseRow[] => deepFreeze( + cases.map((entry): EvalCaseRow => ({ assertions: entry.assertions.length, hosts: entry.hosts.join(', '), id: entry.id, @@ -327,8 +329,8 @@ const provenanceFor = (provenance: EvalTrialProvenance): EvalTrialProvenance => export const evalTrialRowsFor = ( trials: readonly EvalTrialRecord[], -): readonly EvalTrialRow[] => Object.freeze(trials.map((trial): EvalTrialRow => Object.freeze({ - assertions: Object.freeze(trial.assertions.map((assertion): EvalAssertionRow => Object.freeze({ +): readonly EvalTrialRow[] => deepFreeze(trials.map((trial): EvalTrialRow => ({ + assertions: deepFreeze(trial.assertions.map((assertion): EvalAssertionRow => ({ detail: assertion.detail, evidence: assertion.evidence, id: assertion.assertionId, @@ -403,8 +405,8 @@ const outcomeCountsFor = (trials: readonly EvalTrialRow[]): EvalOutcomeCounts => pass: trials.filter((trial) => trial.outcome === 'pass').length, }); -const hostModelsFor = (trials: readonly EvalTrialRow[]): readonly EvalHostModelRow[] => Object.freeze(trials - .map((trial): EvalHostModelRow => Object.freeze({ +const hostModelsFor = (trials: readonly EvalTrialRow[]): readonly EvalHostModelRow[] => deepFreeze(trials + .map((trial): EvalHostModelRow => ({ host: trial.host, model: trial.model, outcome: trial.outcome, @@ -505,7 +507,7 @@ export const evalRunSelectionFor = (view: EvalRunView, trials: string): EvalRunS const suite = view.selected; if (suite === undefined) return undefined; const requested = trials.trim(); - if (requested.length === 0) return Object.freeze({ suites: Object.freeze([suite.name]) }); + if (requested.length === 0) return deepFreeze({ suites: [suite.name] }); if (!/^\d+$/u.test(requested)) return undefined; const count = Number(requested); if (!Number.isSafeInteger(count) || count < 1 || count > maximumTrials) return undefined; diff --git a/packages/workbench/src/freeze.ts b/packages/workbench/src/freeze.ts new file mode 100644 index 000000000..2cb805ab9 --- /dev/null +++ b/packages/workbench/src/freeze.ts @@ -0,0 +1,16 @@ +const isPlainObjectOrArray = (value: object): boolean => { + if (Array.isArray(value)) return true; + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +}; + +/** Freezes a value tree in place; cycle-safe and symbol-aware. Freezing is idempotent. */ +export const deepFreeze = (value: Value, seen = new WeakSet()): Value => { + if (typeof value !== 'object' || value === null || seen.has(value)) return value; + if (!isPlainObjectOrArray(value)) return value; + seen.add(value); + for (const property of Reflect.ownKeys(value)) { + deepFreeze(Reflect.get(value, property), seen); + } + return Object.freeze(value); +}; diff --git a/packages/workbench/src/hooks/hooks-model.ts b/packages/workbench/src/hooks/hooks-model.ts index d3e63f6b5..4b718e925 100644 --- a/packages/workbench/src/hooks/hooks-model.ts +++ b/packages/workbench/src/hooks/hooks-model.ts @@ -9,6 +9,8 @@ import type { HookPlaygroundSimulation, } from '../../../agent-bundle/src/contracts/hooks.ts'; import { deeplyFrozenHookValue } from './hook-client.ts'; +import { deepFreeze } from '../freeze.ts'; + export type HookPlaygroundResult = HookPlaygroundDiagnosticResult | HookPlaygroundSimulation | undefined; @@ -66,9 +68,9 @@ const readableHookLabel = (value: string): string => { export const hookOptionKeyFor = (binding: HookPlaygroundBinding): string => `${binding.target}/${binding.hook}`; -export const hookOptionsFor = (hooks: readonly HookPlaygroundHook[]): readonly HookOption[] => Object.freeze( +export const hookOptionsFor = (hooks: readonly HookPlaygroundHook[]): readonly HookOption[] => deepFreeze( hooks - .map((entry): HookOption => Object.freeze({ + .map((entry): HookOption => ({ binding: Object.freeze({ epochId: entry.binding.epochId, hook: entry.binding.hook, target: entry.binding.target }), event: entry.hook.event, key: hookOptionKeyFor(entry.binding), diff --git a/packages/workbench/src/hooks/hooks-page.tsx b/packages/workbench/src/hooks/hooks-page.tsx index f27beb208..ef09dc9ad 100644 --- a/packages/workbench/src/hooks/hooks-page.tsx +++ b/packages/workbench/src/hooks/hooks-page.tsx @@ -20,6 +20,8 @@ import { type HookPlaygroundView, } from './hooks-model.ts'; import './hooks-page.css'; +import { deepFreeze } from '../freeze.ts'; + export interface HookSimulationViewProps { readonly view: HookPlaygroundView; @@ -34,8 +36,8 @@ const draftError = 'Canonical hook input must be a JSON object.'; type CanonicalHookEvent = HookPlaygroundHook['hook']['event']; -const canonicalHookInputs: Readonly> = Object.freeze({ - afterTool: Object.freeze({ +const canonicalHookInputs: Readonly> = deepFreeze({ + afterTool: { cwd: '/workspace', sessionId: 'workbench-preview', toolInput: Object.freeze({}), @@ -43,28 +45,28 @@ const canonicalHookInputs: Readonly diff --git a/packages/workbench/src/mcp/mcp-session-model.ts b/packages/workbench/src/mcp/mcp-session-model.ts index 4faab3201..dd7c97d46 100644 --- a/packages/workbench/src/mcp/mcp-session-model.ts +++ b/packages/workbench/src/mcp/mcp-session-model.ts @@ -6,6 +6,8 @@ import type { McpSessionTraceReplayGap, } from '../../../agent-bundle/src/contracts/mcp-session.ts'; import type { DevRuntimeMcpAppRunBinding, RuntimeVector } from '../../../agent-bundle/src/contracts/runtime.ts'; +import { deepFreeze } from '../freeze.ts'; + export type McpBrowserSessionBinding = | McpSessionBinding @@ -118,11 +120,11 @@ export type McpBrowserSessionEvent = type McpBrowserSessionEventType = McpBrowserSessionEvent['type']; -const emptyCatalogs = Object.freeze({ - prompts: Object.freeze([]), - resourceTemplates: Object.freeze([]), - resources: Object.freeze([]), - tools: Object.freeze([]), +const emptyCatalogs = deepFreeze({ + prompts: [], + resourceTemplates: [], + resources: [], + tools: [], }); const emptyActiveRequests: Readonly> = Object.freeze(Object.create(null)); @@ -201,9 +203,9 @@ const viewsFor = ( ): Pick => { const cached = derivedViews.get(entries); if (cached !== undefined) return cached; - const views = Object.freeze({ - logs: Object.freeze(entries.filter(isLoggingEntry)), - progress: Object.freeze(entries.filter(isProgressEntry)), + const views = deepFreeze({ + logs: entries.filter(isLoggingEntry), + progress: entries.filter(isProgressEntry), }); derivedViews.set(entries, views); return views; @@ -320,7 +322,7 @@ export const createMcpBrowserSessionModel = (sessionId: string): McpBrowserSessi diagnostics: Object.freeze([]), phase: 'idle', sessionId, - timeline: Object.freeze({ droppedThroughSequence: 0, entries: Object.freeze([]), lastSequence: 0 }), + timeline: deepFreeze({ droppedThroughSequence: 0, entries: [], lastSequence: 0 }), }); export const reduceMcpBrowserSession = ( diff --git a/packages/workbench/src/playground/playground-model.ts b/packages/workbench/src/playground/playground-model.ts index 4183c4772..483eccecc 100644 --- a/packages/workbench/src/playground/playground-model.ts +++ b/packages/workbench/src/playground/playground-model.ts @@ -10,6 +10,8 @@ import type { PlaygroundTraceEvent, PlaygroundTraceSource, } from '../../../agent-bundle/src/contracts/playground.ts'; +import { deepFreeze } from '../freeze.ts'; + export type PlaygroundState = 'finalized' | 'no-epoch' | 'no-session' | 'open'; @@ -137,8 +139,8 @@ export const mergePlaygroundEvents = ( export const playgroundTraceRowsFor = ( epoch: PlaygroundEpochIdentity, events: readonly PlaygroundTraceEvent[], -): readonly PlaygroundTraceRow[] => Object.freeze( - [...events].sort(bySequence).map((event): PlaygroundTraceRow => Object.freeze({ +): readonly PlaygroundTraceRow[] => deepFreeze( + [...events].sort(bySequence).map((event): PlaygroundTraceRow => ({ epochDigest: epoch.digest, epochId: epoch.id, key: event.rawEventRef, diff --git a/packages/workbench/tests/evals-page.test.ts b/packages/workbench/tests/evals-page.test.ts index f42e4c479..5b339e9e8 100644 --- a/packages/workbench/tests/evals-page.test.ts +++ b/packages/workbench/tests/evals-page.test.ts @@ -22,6 +22,8 @@ import { prepareEvalArtifactDisplay, startEvalRun, } from '../src/evals/evals-page.tsx'; +import { deepFreeze } from '../src/freeze.ts'; + const targetDigest = 'c'.repeat(64); @@ -235,8 +237,8 @@ it('renders the persisted timeline, server evidence channels, host/model matrix, }); it('does not paint held prior-run events while a replacement run waits for replay', () => { - const priorRunEvents = Object.freeze([ - Object.freeze({ kind: 'run.started', payload: Object.freeze({ trials: 3 }), schemaVersion: 1, sequence: 1, timestamp: '2026-08-17T00:00:00.000Z' }), + const priorRunEvents = deepFreeze([ + { kind: 'run.started', payload: Object.freeze({ trials: 3 }), schemaVersion: 1, sequence: 1, timestamp: '2026-08-17T00:00:00.000Z' }, ]); expect(eventsForActiveEvalRun('run-b', 'run-a', priorRunEvents)).toEqual([]); diff --git a/packages/workbench/tests/packed-release.e2e.test.ts b/packages/workbench/tests/packed-release.e2e.test.ts index 83e0c8840..36a87c163 100644 --- a/packages/workbench/tests/packed-release.e2e.test.ts +++ b/packages/workbench/tests/packed-release.e2e.test.ts @@ -32,6 +32,8 @@ import { } from './support/packed-release-harness.ts'; import { replaceWatchedSource } from './support/watched-files.ts'; import { workbenchUrl } from './support/workbench-e2e.ts'; +import { deepFreeze } from '../src/freeze.ts'; + const fixtureRoot = join(workspaceRoot, 'fixtures', 'integration', 'packed-release'); const browserTimeout = 12_000 * timeScale; @@ -1029,14 +1031,14 @@ e2e('runs every Agent API tool from the installed tarball', { timeout: 360_000 * oldSessionId: oldBrowserMcpSessionId, origin, outageStartedAt, - postRecovery: Object.freeze({ - freshMcpSession: Object.freeze({ + postRecovery: deepFreeze({ + freshMcpSession: { closeCompletedAt: browserMcpSessionBCloseCompletedAt, closeStartedAt: browserMcpSessionBCloseStartedAt, id: browserMcpSessionBId, openedAt: browserMcpSessionBOpenedAt, - }), - navigation: Object.freeze(postRecoveryNavigation), + }, + navigation: postRecoveryNavigation, }), recoveredAt, requests: browserRequests,