@meistrari/agent-core 0.2.0-experimental.1 → 0.2.0-experimental.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +50 -0
  2. package/docs/consumer-capability-matrix.md +374 -0
  3. package/package.json +17 -10
  4. package/scripts/generate-codex-protocol.ts +711 -0
  5. package/src/agents/agent-operation.ts +8 -0
  6. package/src/agents/agent-provider.ts +3 -0
  7. package/src/agents/agent-run.ts +14 -2
  8. package/src/agents/agent-runtime-error.ts +28 -0
  9. package/src/agents/agent-session-events.ts +33 -0
  10. package/src/agents/agent-tool-runner.ts +12 -11
  11. package/src/agents/agent-tool.ts +12 -4
  12. package/src/agents/author-context.ts +1 -1
  13. package/src/agents/claude/claude-auth.ts +40 -0
  14. package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
  15. package/src/agents/claude/claude-code-process.ts +98 -0
  16. package/src/agents/claude/claude-command-mapper.ts +63 -3
  17. package/src/agents/claude/claude-desktop.ts +55 -0
  18. package/src/agents/claude/claude-event-mapper.ts +864 -135
  19. package/src/agents/claude/claude-file-change.ts +128 -0
  20. package/src/agents/claude/claude-provider.ts +344 -29
  21. package/src/agents/claude/claude-run.ts +893 -130
  22. package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
  23. package/src/agents/claude/claude-skill-catalog.ts +75 -0
  24. package/src/agents/claude/claude-steered-notifications.ts +180 -0
  25. package/src/agents/claude/claude-tool-mapper.ts +81 -2
  26. package/src/agents/claude/claude-workspace-skills.ts +87 -0
  27. package/src/agents/claude/index.ts +2 -0
  28. package/src/agents/codex/codex-auth-mapper.ts +27 -0
  29. package/src/agents/codex/codex-auth.ts +19 -0
  30. package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
  31. package/src/agents/codex/codex-browser-auth-status.ts +55 -0
  32. package/src/agents/codex/codex-command-mapper.ts +38 -3
  33. package/src/agents/codex/codex-event-mapper.ts +375 -64
  34. package/src/agents/codex/codex-json-rpc-client.ts +66 -21
  35. package/src/agents/codex/codex-provider.ts +118 -236
  36. package/src/agents/codex/codex-run-auth.ts +113 -0
  37. package/src/agents/codex/codex-run.ts +423 -53
  38. package/src/agents/codex/codex-tool-mapper.ts +2 -2
  39. package/src/agents/codex/codex.errors.ts +8 -0
  40. package/src/agents/codex/generated/meta.gen.ts +144 -6
  41. package/src/agents/codex/generated/namespaces.gen.ts +92 -3
  42. package/src/agents/codex/generated/schema.gen.ts +24698 -15638
  43. package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
  44. package/src/agents/index.ts +5 -0
  45. package/src/agents/input-attachment-preparation.ts +13 -0
  46. package/src/agents/mcp-tool-content.ts +82 -0
  47. package/src/agents/model-rebinding-run.ts +14 -3
  48. package/src/agents/selected-repository-instructions.ts +202 -0
  49. package/src/agents/session-context.ts +41 -0
  50. package/src/agents/subscription-token-refresh.ts +73 -0
  51. package/src/agents/user-input-request.ts +19 -20
  52. package/src/errors/index.ts +1 -1
  53. package/src/errors/validation-error.ts +6 -0
  54. package/src/protocol/agent-catalog.ts +159 -0
  55. package/src/protocol/agent-command.ts +52 -7
  56. package/src/protocol/agent-content.ts +36 -16
  57. package/src/protocol/agent-event.ts +210 -73
  58. package/src/protocol/agent-identity.ts +29 -0
  59. package/src/protocol/agent-json.ts +122 -8
  60. package/src/protocol/agent-metadata.ts +2 -2
  61. package/src/protocol/agent-model.ts +13 -1
  62. package/src/protocol/agent-notification.ts +110 -0
  63. package/src/protocol/agent-presentation.ts +71 -20
  64. package/src/protocol/agent-tool.ts +66 -10
  65. package/src/protocol/agent-usage.ts +3 -1
  66. package/src/protocol/agent-user-input.ts +4 -4
  67. package/src/protocol/agent-work.ts +12 -10
  68. package/src/protocol/index.ts +2 -0
  69. package/src/protocol/rejected-keys.ts +11 -0
  70. package/src/provenance.gen.ts +3 -3
  71. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
@@ -0,0 +1,128 @@
1
+ import type { FileEditOutput, FileWriteOutput } from '@anthropic-ai/claude-agent-sdk/sdk-tools'
2
+ import { isAbsolute, relative } from 'node:path'
3
+ import z from 'zod'
4
+ import { CANONICAL_TOOL } from '../../protocol/agent-tool-name'
5
+
6
+ type ClaudePatchHunk = FileEditOutput['structuredPatch'][number]
7
+ // `staged` is typed only by newer SDKs (0.3.280); older pinned runtimes omit it, so it stays optional.
8
+ type ClaudeEditDiffEvidence = Pick<FileEditOutput, 'filePath' | 'oldString' | 'newString' | 'originalFile' | 'structuredPatch'> & { staged?: boolean }
9
+ type ClaudeWriteDiffEvidence = Pick<FileWriteOutput, 'type' | 'filePath' | 'content' | 'structuredPatch' | 'originalFile'> & { staged?: boolean }
10
+ interface DiffHeaderPaths { old: string, new: string }
11
+
12
+ const patchHunkSchema = z.object({
13
+ oldStart: z.number().int().nonnegative(),
14
+ oldLines: z.number().int().nonnegative(),
15
+ newStart: z.number().int().nonnegative(),
16
+ newLines: z.number().int().nonnegative(),
17
+ lines: z.array(z.string()),
18
+ }).passthrough() satisfies z.ZodType<ClaudePatchHunk>
19
+
20
+ const editDiffEvidenceSchema = z.object({
21
+ filePath: z.string().min(1),
22
+ oldString: z.string(),
23
+ newString: z.string(),
24
+ originalFile: z.string().nullable(),
25
+ structuredPatch: z.array(patchHunkSchema),
26
+ staged: z.boolean().optional(),
27
+ }).passthrough() satisfies z.ZodType<ClaudeEditDiffEvidence>
28
+
29
+ const writeDiffEvidenceSchema = z.object({
30
+ type: z.enum(['create', 'update']),
31
+ filePath: z.string().min(1),
32
+ content: z.string(),
33
+ structuredPatch: z.array(patchHunkSchema),
34
+ originalFile: z.string().nullable(),
35
+ staged: z.boolean().optional(),
36
+ }).passthrough() satisfies z.ZodType<ClaudeWriteDiffEvidence>
37
+
38
+ /**
39
+ * Renders the host `tool_use_result` of a successful Claude Edit or Write call as a unified diff,
40
+ * the same output shape Codex reports for a file change. Returns undefined when the result does
41
+ * not prove a diff, so the caller keeps the model-facing text.
42
+ */
43
+ export function claudeFileChangeText({ toolName, toolUseResult, cwd }: { toolName: string | undefined, toolUseResult: unknown, cwd: string | undefined }): string | undefined {
44
+ if (toolName === CANONICAL_TOOL.edit) {
45
+ const parsed = editDiffEvidenceSchema.safeParse(toolUseResult)
46
+ if (!parsed.success)
47
+ return undefined
48
+ const path = displayPath({ filePath: parsed.data.filePath, cwd })
49
+ const { oldString, newString, originalFile, structuredPatch, staged } = parsed.data
50
+ if (staged === true)
51
+ return noChangesText(path)
52
+ // The CLI accepts an empty old_string only for a missing or empty file ("Cannot create new
53
+ // file - file already exists." otherwise) and reports its original as "" (or null when the
54
+ // result is reconstructed), so this Edit created the file.
55
+ if (oldString === '' && !originalFile) {
56
+ const hunks = structuredPatch.length > 0 ? structuredPatch : createdFileHunks(newString)
57
+ return unifiedDiff({ paths: createdFilePaths(path), hunks })
58
+ }
59
+ // An empty Edit patch cannot tell an unchanged file from a diff the CLI gave up computing.
60
+ if (structuredPatch.length === 0)
61
+ return undefined
62
+ return unifiedDiff({ paths: gitHeaderPaths(path), hunks: structuredPatch })
63
+ }
64
+
65
+ if (toolName === CANONICAL_TOOL.write) {
66
+ const parsed = writeDiffEvidenceSchema.safeParse(toolUseResult)
67
+ if (!parsed.success)
68
+ return undefined
69
+ const { type, content, structuredPatch, originalFile, staged } = parsed.data
70
+ const path = displayPath({ filePath: parsed.data.filePath, cwd })
71
+ if (staged === true)
72
+ return noChangesText(path)
73
+ if (type === 'create') {
74
+ const hunks = structuredPatch.length > 0 ? structuredPatch : createdFileHunks(content)
75
+ return unifiedDiff({ paths: createdFilePaths(path), hunks })
76
+ }
77
+ if (structuredPatch.length > 0)
78
+ return unifiedDiff({ paths: gitHeaderPaths(path), hunks: structuredPatch })
79
+ // An empty update patch also covers a timed-out diff or an original too large to include.
80
+ return originalFile === content ? noChangesText(path) : undefined
81
+ }
82
+
83
+ return undefined
84
+ }
85
+
86
+ function displayPath({ filePath, cwd }: { filePath: string, cwd: string | undefined }): string {
87
+ if (!cwd || !isAbsolute(filePath))
88
+ return filePath
89
+ const relativePath = relative(cwd, filePath)
90
+ return relativePath && !relativePath.startsWith('..') && !isAbsolute(relativePath) ? relativePath : filePath
91
+ }
92
+
93
+ function gitHeaderPaths(path: string): DiffHeaderPaths {
94
+ return isAbsolute(path) ? { old: path, new: path } : { old: `a/${path}`, new: `b/${path}` }
95
+ }
96
+
97
+ function createdFilePaths(path: string): DiffHeaderPaths {
98
+ return { old: '/dev/null', new: gitHeaderPaths(path).new }
99
+ }
100
+
101
+ function createdFileHunks(content: string): ClaudePatchHunk[] {
102
+ if (content === '')
103
+ return []
104
+ const endsWithNewline = content.endsWith('\n')
105
+ const lines = (endsWithNewline ? content.slice(0, -1) : content).split('\n')
106
+ const hunkLines = lines.map(line => `+${line}`)
107
+ if (!endsWithNewline)
108
+ hunkLines.push('\')
109
+ return [{ oldStart: 1, oldLines: 0, newStart: 1, newLines: lines.length, lines: hunkLines }]
110
+ }
111
+
112
+ function unifiedDiff({ paths, hunks }: { paths: DiffHeaderPaths, hunks: ClaudePatchHunk[] }): string {
113
+ const lines = [`--- ${paths.old}`, `+++ ${paths.new}`]
114
+ for (const hunk of hunks)
115
+ lines.push(`@@ -${hunkRange(hunk.oldStart, hunk.oldLines)} +${hunkRange(hunk.newStart, hunk.newLines)} @@`, ...hunk.lines)
116
+ return `${lines.join('\n')}\n`
117
+ }
118
+
119
+ // The CLI's structuredPatch is jsdiff's, which starts an empty range at the following line; a
120
+ // unified diff names the preceding line instead (so an empty original is `-0,0`), as jsdiff's own
121
+ // formatPatch does.
122
+ function hunkRange(start: number, count: number): string {
123
+ return `${count === 0 ? Math.max(start - 1, 0) : start},${count}`
124
+ }
125
+
126
+ function noChangesText(path: string): string {
127
+ return `No changes applied to ${path}.`
128
+ }
@@ -1,46 +1,171 @@
1
- import type { Options, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
1
+ import type { McpStdioServerConfig, Options, Query, SDKControlInitializeResponse, SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
2
2
  import type { ClaudeModelId } from '../../protocol'
3
3
  import type { AgentOperationOptions } from '../agent-operation'
4
4
  import type { AgentProvider, AgentSessionOpenInput, AgentSessionResumeInput } from '../agent-provider'
5
5
  import type { AgentTool } from '../agent-tool'
6
6
  import type { InstructionComposer } from '../instructions'
7
+ import type { SessionContextComposer } from '../session-context'
8
+ import type { ClaudeProviderAuth } from './claude-auth'
9
+ import type { ClaudeDesktopConfig } from './claude-desktop'
7
10
  import type { ClaudeStartupTiming } from './claude-startup-timing'
11
+ import type { ClaudeNativeToolActor } from './claude-tool-mapper'
12
+ import { randomUUID } from 'node:crypto'
8
13
  import { homedir } from 'node:os'
9
14
  import { join } from 'node:path'
10
15
  import { query as queryClaude } from '@anthropic-ai/claude-agent-sdk'
11
16
  import { claudeModelIdSchema } from '../../protocol'
12
- import { AgentRunStateError } from '../agent-runtime-error'
17
+ import { AgentProviderLaunchError, AgentRunStateError } from '../agent-runtime-error'
13
18
  import { selectAgentTools } from '../agent-tool'
19
+ import { CodexBrowserAuthBroker } from '../codex/codex-browser-auth-broker'
20
+ import { LazyCodexRunAuth } from '../codex/lazy-codex-run-auth'
14
21
  import { emptyInstructionComposer } from '../instructions'
15
22
  import { removeUndefined } from '../normalize'
23
+ import { pendingSessionContext } from '../session-context'
16
24
  import { UserInputRequestStore, userInputRequestStoreRootFromTranscriptPath } from '../user-input-request'
25
+ import { ClaudeBrowserTurnMetadata, claudeBrowserTurnMetadataHooks } from './claude-browser-turn-metadata'
26
+ import { ClaudeCodeProcess } from './claude-code-process'
27
+ import { claudeDesktopMcpServers } from './claude-desktop'
17
28
  import { createClaudeEventMapperState } from './claude-event-mapper'
18
29
  import { ClaudeRun } from './claude-run'
30
+ import { ClaudeSelectedRepositoryInstructions } from './claude-selected-repository-instructions'
31
+ import { ClaudeSkillCatalog } from './claude-skill-catalog'
19
32
  import { createClaudeStartupTiming, observeClaudeControlInitialization } from './claude-startup-timing'
20
- import { claudeAllowedToolNames, claudeToolActorHooks, createClaudeToolRuntimeRef, toClaudeMcpServers } from './claude-tool-mapper'
33
+ import { claudeSteeredNotificationHooks, ClaudeSteeredNotificationStore } from './claude-steered-notifications'
34
+ import { claudeAllowedToolNames, claudeNativeToolActorHooks, claudeToolActorHooks, createClaudeToolRuntimeRef, mergeClaudeToolHooks, toClaudeMcpServers } from './claude-tool-mapper'
35
+ import { ClaudeWorkspaceSkills, claudeWorkspaceSkillsPluginName } from './claude-workspace-skills'
36
+
37
+ export type { ClaudeNativeToolActor } from './claude-tool-mapper'
38
+
39
+ export type ClaudeNativeToolActorRun = Readonly<{
40
+ mcpServer: Readonly<{
41
+ name: 'mcp_repl'
42
+ config: McpStdioServerConfig
43
+ }>
44
+ toolNames: readonly string[]
45
+ register: (input: { toolName: string, toolUseId: string, actor: ClaudeNativeToolActor, signal: AbortSignal }) => Promise<void>
46
+ finish: (input: { toolUseId: string }) => Promise<void>
47
+ close: () => Promise<void>
48
+ }>
49
+
50
+ export type ClaudeNativeToolActorBridge = Readonly<{
51
+ openRun: (input: { sessionId: string, signal: AbortSignal }) => Promise<ClaudeNativeToolActorRun>
52
+ }>
53
+
54
+ /**
55
+ * Skill sources for every run. `imagePlugin` is the image-baked local plugin whose `skills` links to
56
+ * the image skills directory (`null` outside the sandbox image); `workspacePluginDirectory` is where
57
+ * each run generates its workspace-repository skills plugin.
58
+ */
59
+ export type ClaudeSkillsConfig = Readonly<{
60
+ imagePlugin: Readonly<{ path: string, name: string }> | null
61
+ workspacePluginDirectory: string
62
+ }>
21
63
 
22
64
  export interface ClaudeProviderConfig {
65
+ // Routes model traffic through a credential proxy. Without it, the configured environment's own
66
+ // credentials (for example ANTHROPIC_API_KEY) authenticate Claude Code unchanged.
67
+ auth?: ClaudeProviderAuth
68
+ // Skill plugins for every run. Without it, runs load no generated skills plugin.
69
+ skills?: ClaudeSkillsConfig
70
+ // The sandbox desktop (Computer Use / Browser Use); omitted or `null` where no desktop exists.
71
+ desktop?: ClaudeDesktopConfig | null
23
72
  tools?: readonly AgentTool[]
24
73
  executable?: string
25
74
  environment?: Record<string, string | undefined>
75
+ nativeToolActorBridge?: ClaudeNativeToolActorBridge
76
+ startupDeadlineMs?: number
77
+ // Product instructions appended to the Claude Code preset system prompt.
26
78
  instructions?: InstructionComposer
79
+ // Per-session context delivered once with the first prompt of a session that has a web URL.
80
+ sessionContext?: SessionContextComposer
81
+ // Product context published with the selected-repository instructions, even with no repository.
82
+ selectedRepositoryContext?: string
27
83
  onStartupTiming?: (timing: ClaudeStartupTiming) => void
84
+ // Defaults to project and local settings; `sdk-default` leaves the SDK option unset.
85
+ settingSources?: 'sdk-default' | NonNullable<Options['settingSources']>
86
+ // Reads persisted tool results back from the transcript (default true).
87
+ hydratePersistedToolOutput?: boolean
88
+ // Native tools unavailable to the model; defaults to the interactive and out-of-session tools.
89
+ disallowedTools?: readonly string[]
28
90
  }
29
91
 
92
+ // Bounds the SDK `initialize` handshake. Measured Claude Code startup in the E2B image is about
93
+ // 0.4 to 2.7 s (locally about 0.13 s); 30 s is roughly ten times the slowest observation and matches
94
+ // the 30 s deadline the Codex browser-auth broker applies to its native startup.
95
+ export const CLAUDE_STARTUP_DEADLINE_MS = 30_000
96
+
97
+ // Credentials that must never reach Claude Code from the inherited environment: the run's only
98
+ // credential is the configured auth route, so an ambient key cannot silently take precedence.
99
+ const inheritedClaudeCredentialKeys = [
100
+ 'ANTHROPIC_API_KEY',
101
+ 'ANTHROPIC_AUTH_TOKEN',
102
+ 'ANTHROPIC_BASE_URL',
103
+ 'CLAUDE_CODE_OAUTH_TOKEN',
104
+ 'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
105
+ 'CLAUDE_CODE_OAUTH_REFRESH_TOKEN',
106
+ // Account facts for the env token; only the auth route may state them.
107
+ 'CLAUDE_CODE_SUBSCRIPTION_TYPE',
108
+ 'CLAUDE_CODE_RATE_LIMIT_TIER',
109
+ ] as const
110
+
111
+ // Claude Code 2.1.280 gives an env OAuth token the subscription type CLAUDE_CODE_SUBSCRIPTION_TYPE
112
+ // (the variable it sets itself for the env-token child processes it spawns), else null. With
113
+ // _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL, a null or team/enterprise type makes initialize fetch
114
+ // remote managed settings and policy limits from the hard-coded https://api.anthropic.com with
115
+ // the placeholder bearer (a 401 when reachable, ~210 ms locally; ~18 s of retries when not). For a
116
+ // Pro or Max account the CLI skips both as it does for a stored /login (`unsupported_subscription`,
117
+ // `prosumer_oauth`), so the environment states only those two real types; see
118
+ // docs/packages/agents.md#claude-authentication.
119
+ const claudeFetchFreeSubscriptionTypes = new Set(['pro', 'max'])
120
+
121
+ // Interactive native tools that a headless run cannot serve.
122
+ export const defaultClaudeDisallowedToolNames: readonly string[] = [
123
+ 'AskUserQuestion',
124
+ 'EnterPlanMode',
125
+ 'ExitPlanMode',
126
+ 'NotebookEdit',
127
+ 'EnterWorktree',
128
+ 'ExitWorktree',
129
+ ]
130
+
131
+ // Claude Code 2.1.263 and 2.1.280 omit these native work tools from headless preset sessions unless
132
+ // they are named in allowedTools. Keep them loaded so their lifecycle maps to work.observed.
133
+ const claudeNativeWorkToolNames = ['TaskCreate', 'TaskGet', 'TaskList', 'TaskUpdate'] as const
134
+
30
135
  export class ClaudeProvider implements AgentProvider<'claude'> {
31
136
  readonly id = 'claude' as const
137
+ private readonly auth: ClaudeProviderAuth | undefined
138
+ private readonly skills: ClaudeSkillsConfig | undefined
139
+ private readonly desktop: ClaudeDesktopConfig | null
32
140
  private readonly tools: readonly AgentTool[]
33
141
  private readonly executable: string | undefined
34
142
  private readonly environment: Record<string, string | undefined>
143
+ private readonly nativeToolActorBridge: ClaudeNativeToolActorBridge | undefined
144
+ private readonly startupDeadlineMs: number
35
145
  private readonly instructions: InstructionComposer
146
+ private readonly sessionContext: SessionContextComposer | undefined
147
+ private readonly selectedRepositoryContext: string | undefined
36
148
  private readonly onStartupTiming: ClaudeProviderConfig['onStartupTiming']
149
+ private readonly settingSources: Options['settingSources']
150
+ private readonly hydratePersistedToolOutput: boolean
151
+ private readonly disallowedTools: readonly string[]
37
152
 
38
153
  private constructor(config: ClaudeProviderConfig) {
154
+ this.auth = config.auth
155
+ this.skills = config.skills
156
+ this.desktop = config.desktop ?? null
39
157
  this.tools = config.tools ?? []
40
158
  this.executable = config.executable
41
159
  this.environment = config.environment ?? {}
160
+ this.nativeToolActorBridge = config.nativeToolActorBridge
161
+ this.startupDeadlineMs = config.startupDeadlineMs ?? CLAUDE_STARTUP_DEADLINE_MS
42
162
  this.instructions = config.instructions ?? emptyInstructionComposer
163
+ this.sessionContext = config.sessionContext
164
+ this.selectedRepositoryContext = config.selectedRepositoryContext
43
165
  this.onStartupTiming = config.onStartupTiming
166
+ this.settingSources = config.settingSources === 'sdk-default' ? undefined : config.settingSources ?? ['project', 'local']
167
+ this.hydratePersistedToolOutput = config.hydratePersistedToolOutput ?? true
168
+ this.disallowedTools = config.disallowedTools ?? defaultClaudeDisallowedToolNames
44
169
  }
45
170
 
46
171
  static create(config: ClaudeProviderConfig = {}): ClaudeProvider {
@@ -62,6 +187,8 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
62
187
  session: { sessionId?: string, resume?: string }
63
188
  expectedSessionId: string
64
189
  operationOptions: AgentOperationOptions
190
+ // Set on the one re-spawn after a spawn declared a subscription type the credential lacks.
191
+ respawnedForSubscriptionType?: true
65
192
  }): Promise<ClaudeRun> {
66
193
  const { input, session, expectedSessionId, operationOptions } = params
67
194
  operationOptions.signal?.throwIfAborted()
@@ -73,11 +200,25 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
73
200
  const tools = selectAgentTools(this.tools, input.tools)
74
201
  const state = createClaudeEventMapperState()
75
202
  const runtime = createClaudeToolRuntimeRef({ turnId: () => state.currentTurnId })
203
+ // The transcript location is a pre-input fact; session-local stores live beside it.
204
+ const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
205
+ const steeredNotifications = ClaudeSteeredNotificationStore.open({ sessionId: input.sessionId, transcriptPath })
206
+ const resources = await this.openRunResources({ input, expectedSessionId, signal: abortController.signal })
207
+ const { nativeToolActorRun, workspaceSkills, selectedRepositoryInstructions, browserAuthBroker, browserTurnMetadata } = resources
208
+ const claudeProcess = new ClaudeCodeProcess()
209
+ const declaredSubscriptionType = this.auth ? fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) : undefined
210
+ const runtimeMcpServers = toClaudeMcpServers({
211
+ tools,
212
+ runtime,
213
+ signal: abortController.signal,
214
+ })
76
215
  const sdkOptions: Options = removeUndefined({
77
216
  abortController,
78
217
  cwd: input.cwd,
79
- settingSources: ['project', 'local'],
80
- env: this.environment,
218
+ // Spawn is not delayed for the credential: before the first acquisition the declared type
219
+ // is the bootstrap's hint, which is re-checked below once the credential is acquired.
220
+ env: this.auth ? claudeRunEnvironment(this.environment, this.auth, declaredSubscriptionType) : this.environment,
221
+ settingSources: this.settingSources,
81
222
  model: input.model,
82
223
  thinking: { type: 'adaptive', display: 'summarized' },
83
224
  effort: input.reasoningEffort,
@@ -89,29 +230,62 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
89
230
  permissionMode: 'bypassPermissions',
90
231
  allowDangerouslySkipPermissions: true,
91
232
  tools: { type: 'preset', preset: 'claude_code' },
92
- systemPrompt: { type: 'preset', preset: 'claude_code', append: this.instructions({ provider: 'claude', tools }) },
93
- disallowedTools: ['AskUserQuestion', 'EnterPlanMode', 'ExitPlanMode', 'NotebookEdit', 'EnterWorktree', 'ExitWorktree'],
94
- mcpServers: toClaudeMcpServers({
95
- tools,
96
- runtime,
97
- signal: abortController.signal,
98
- }),
99
- allowedTools: ['Bash', ...claudeAllowedToolNames(tools)],
100
- hooks: claudeToolActorHooks(tools),
233
+ systemPrompt: { type: 'preset', preset: 'claude_code', append: input.instructions ?? this.instructions({ provider: 'claude', tools }) },
234
+ disallowedTools: [...this.disallowedTools],
235
+ mcpServers: {
236
+ ...(this.desktop && browserAuthBroker && browserTurnMetadata
237
+ ? claudeDesktopMcpServers({ desktop: this.desktop, environment: this.environment, browserAuthSocketPath: browserAuthBroker.socketPath, turnMetadataPath: browserTurnMetadata.path })
238
+ : {}),
239
+ ...runtimeMcpServers,
240
+ ...(nativeToolActorRun ? { [nativeToolActorRun.mcpServer.name]: nativeToolActorRun.mcpServer.config } : {}),
241
+ },
242
+ plugins: workspaceSkills
243
+ ? [
244
+ ...(this.skills?.imagePlugin ? [{ type: 'local' as const, path: this.skills.imagePlugin.path }] : []),
245
+ { type: 'local' as const, path: workspaceSkills.pluginPath },
246
+ ]
247
+ : undefined,
248
+ allowedTools: ['Bash', ...claudeNativeWorkToolNames, ...claudeAllowedToolNames(tools), ...(nativeToolActorRun?.toolNames ?? [])],
249
+ hooks: mergeClaudeToolHooks(
250
+ claudeToolActorHooks(tools),
251
+ nativeToolActorRun
252
+ ? claudeNativeToolActorHooks({
253
+ expectedSessionId,
254
+ toolNames: nativeToolActorRun.toolNames,
255
+ register: nativeToolActorRun.register,
256
+ finish: nativeToolActorRun.finish,
257
+ })
258
+ : undefined,
259
+ claudeSteeredNotificationHooks(steeredNotifications),
260
+ selectedRepositoryInstructions.hooks(),
261
+ this.desktop && browserTurnMetadata
262
+ ? claudeBrowserTurnMetadataHooks({
263
+ serverName: this.desktop.browserAuth.serverName,
264
+ record: async () => await browserTurnMetadata.record({ sessionId: expectedSessionId, turnId: state.currentTurnId }),
265
+ })
266
+ : undefined,
267
+ ),
101
268
  pathToClaudeCodeExecutable: this.executable,
269
+ spawnClaudeCodeProcess: claudeProcess.spawn,
102
270
  })
103
271
 
104
272
  const recordStartup = createClaudeStartupTiming(this.onStartupTiming)
105
- const query = queryClaude({ prompt: inputQueue, options: sdkOptions })
106
- recordStartup('query_created')
107
- if (this.onStartupTiming)
108
- observeClaudeControlInitialization(query.initializationResult(), recordStartup)
273
+ let query: Query
274
+ try {
275
+ query = queryClaude({ prompt: inputQueue, options: sdkOptions })
276
+ recordStartup('query_created')
277
+ if (this.onStartupTiming)
278
+ observeClaudeControlInitialization(query.initializationResult(), recordStartup)
279
+ }
280
+ catch (error) {
281
+ await closeRunResources(resources)
282
+ throw error
283
+ }
109
284
  try {
110
285
  // Binding metadata comes from pre-input facts only: the live runtime emits system/init
111
286
  // only after the first user message, so the run validates identity/config on the first
112
287
  // input (init barrier). Observed runtime configuration arrives via session.started /
113
288
  // session.configured after that validation.
114
- const transcriptPath = join(configDir, 'projects', encodeClaudeProjectPath(input.cwd), `${expectedSessionId}.jsonl`)
115
289
  const metadata = {
116
290
  provider: 'claude' as const,
117
291
  sessionId: input.sessionId,
@@ -120,40 +294,181 @@ export class ClaudeProvider implements AgentProvider<'claude'> {
120
294
  model: firstClaudeModelId(input.model),
121
295
  transcriptPath,
122
296
  }
123
- const userInputRequests = await UserInputRequestStore.open({
124
- sessionId: metadata.sessionId,
125
- rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
126
- })
297
+ // The SDK spawns the CLI and sends the `initialize` control request inside query(); its
298
+ // response needs no user input. Awaiting it means an opened run, including a supervisor
299
+ // prewarm, holds a CLI that has finished startup, so the first prompt skips that stall.
300
+ // This is not system/init, which still arrives only after the first input. The
301
+ // credential is acquired alongside, so an account needing reauth fails the open.
302
+ const [userInputRequests, initialization, , sessionContext] = await Promise.all([
303
+ UserInputRequestStore.open({
304
+ sessionId: metadata.sessionId,
305
+ rootDir: userInputRequestStoreRootFromTranscriptPath(metadata.transcriptPath),
306
+ }),
307
+ awaitClaudeInitialization({ query, signal: abortController.signal, deadlineMs: this.startupDeadlineMs }),
308
+ this.auth?.acquire({ signal: abortController.signal }),
309
+ pendingSessionContext({ compose: this.sessionContext, sessionWebUrl: input.sessionWebUrl, resumedTranscriptPath: session.resume === undefined ? undefined : transcriptPath }),
310
+ ])
311
+ // A CLI told a Pro/Max type that the acquired account does not have (a stale bootstrap
312
+ // hint, or an account replaced since) would skip that account's policy fetches for its
313
+ // lifetime; it has had no input yet, so it is replaced once before the run is published.
314
+ if (this.auth && declaredSubscriptionType !== undefined && declaredSubscriptionType !== fetchFreeSubscriptionType(this.auth.knownSubscriptionType()) && !params.respawnedForSubscriptionType) {
315
+ query.close()
316
+ inputQueue.close()
317
+ await claudeProcess.terminate()
318
+ await closeRunResources(resources)
319
+ return await this.openRun({ ...params, respawnedForSubscriptionType: true })
320
+ }
127
321
 
128
- const run = new ClaudeRun({
322
+ let unsubscribeAuthFailures = (): void => undefined
323
+ let run: ClaudeRun | undefined
324
+ const skillCatalog = new ClaudeSkillCatalog({
325
+ pluginScopes: new Map([
326
+ ...(this.skills?.imagePlugin ? [[this.skills.imagePlugin.name, 'user'] as const] : []),
327
+ ...(workspaceSkills ? [[claudeWorkspaceSkillsPluginName, 'repository'] as const] : []),
328
+ ]),
329
+ emit: draft => run?.pushEvent(draft),
330
+ })
331
+ skillCatalog.publish(initialization.commands)
332
+ run = new ClaudeRun({
129
333
  metadata,
130
334
  query,
131
335
  userInputRequests,
132
336
  input: inputQueue,
133
337
  eventMapperState: state,
134
- recordStartup,
135
338
  init: {
136
339
  expectedSessionId,
137
340
  requestedModel: input.model,
138
341
  reasoningEffort: input.reasoningEffort,
139
342
  environment: this.environment,
140
343
  },
344
+ steeredNotifications,
345
+ sessionContext,
346
+ process: claudeProcess,
347
+ workspace: {
348
+ skills: workspaceSkills,
349
+ repositoryRoots: input.workspaceRepositoryRoots,
350
+ catalog: skillCatalog,
351
+ instructions: selectedRepositoryInstructions,
352
+ },
353
+ onFinished: async () => {
354
+ unsubscribeAuthFailures()
355
+ await closeRunResources(resources)
356
+ },
357
+ recordStartup,
358
+ hydratePersistedToolOutput: this.hydratePersistedToolOutput,
141
359
  })
360
+ const openedRun = run
361
+ // The proxy owns the credential; a failure it cannot retry away ends this run typed.
362
+ if (this.auth)
363
+ unsubscribeAuthFailures = this.auth.failures.subscribe(failure => openedRun.failActiveTurnAndEndRun(failure))
142
364
  runtime.attach({
143
- metadata: run.metadata,
144
- requestUserInput: async request => await run.requestUserInput(request),
365
+ metadata: openedRun.metadata,
366
+ requestUserInput: async request => await openedRun.requestUserInput(request),
145
367
  })
146
- run.begin()
147
- return run
368
+ openedRun.begin()
369
+ return openedRun
148
370
  }
149
371
  catch (error) {
150
372
  query.close()
151
373
  inputQueue.close()
374
+ // A failed open (for example a missed initialize deadline) must not leave its CLI behind.
375
+ await claudeProcess.terminate()
376
+ await closeRunResources(resources)
377
+ throw error
378
+ }
379
+ }
380
+
381
+ // Run-owned resources opened before the CLI spawns; every failure path and run end closes them.
382
+ private async openRunResources(input: {
383
+ input: AgentSessionOpenInput<'claude'> | AgentSessionResumeInput<'claude'>
384
+ expectedSessionId: string
385
+ signal: AbortSignal
386
+ }): Promise<ClaudeRunResources> {
387
+ const opened: Partial<ClaudeRunResources> = {}
388
+ try {
389
+ if (this.skills) {
390
+ const workspaceSkills = opened.workspaceSkills = await ClaudeWorkspaceSkills.create(join(this.skills.workspacePluginDirectory, randomUUID()))
391
+ // Repositories may still be initializing; prepareWorkspace re-syncs after the Git baseline.
392
+ await workspaceSkills.sync(input.input.workspaceRepositoryRoots)
393
+ }
394
+ // Published before the CLI spawns: Claude Code reads instruction files once per process.
395
+ const selectedRepositoryInstructions = await ClaudeSelectedRepositoryInstructions.open({ cwd: input.input.cwd, repositoryRoots: input.input.workspaceRepositoryRoots, sharedContext: this.selectedRepositoryContext })
396
+ if (this.desktop) {
397
+ // The broker listens now but binds a Codex account only when Browser Use first asks.
398
+ opened.browserAuthBroker = await CodexBrowserAuthBroker.open({
399
+ executable: this.desktop.browserAuth.codexExecutable,
400
+ cwd: input.input.cwd,
401
+ environment: this.environment,
402
+ auth: new LazyCodexRunAuth(this.desktop.browserAuth.codexAuth),
403
+ })
404
+ opened.browserTurnMetadata = await ClaudeBrowserTurnMetadata.open()
405
+ }
406
+ if (this.nativeToolActorBridge)
407
+ opened.nativeToolActorRun = await this.nativeToolActorBridge.openRun({ sessionId: input.expectedSessionId, signal: input.signal })
408
+ return { workspaceSkills: opened.workspaceSkills, selectedRepositoryInstructions, browserAuthBroker: opened.browserAuthBroker, browserTurnMetadata: opened.browserTurnMetadata, nativeToolActorRun: opened.nativeToolActorRun }
409
+ }
410
+ catch (error) {
411
+ await closeRunResources(opened)
152
412
  throw error
153
413
  }
154
414
  }
155
415
  }
156
416
 
417
+ interface ClaudeRunResources {
418
+ nativeToolActorRun: ClaudeNativeToolActorRun | undefined
419
+ workspaceSkills: ClaudeWorkspaceSkills | undefined
420
+ selectedRepositoryInstructions: ClaudeSelectedRepositoryInstructions
421
+ browserAuthBroker: CodexBrowserAuthBroker | undefined
422
+ browserTurnMetadata: ClaudeBrowserTurnMetadata | undefined
423
+ }
424
+
425
+ async function closeRunResources(resources: Partial<ClaudeRunResources>): Promise<void> {
426
+ await Promise.all([
427
+ resources.nativeToolActorRun?.close().catch(() => undefined),
428
+ resources.browserAuthBroker?.close().catch(() => undefined),
429
+ resources.browserTurnMetadata?.close().catch(() => undefined),
430
+ resources.workspaceSkills?.remove().catch(() => undefined),
431
+ ])
432
+ }
433
+
434
+ async function awaitClaudeInitialization(input: { query: Query, signal: AbortSignal, deadlineMs: number }): Promise<SDKControlInitializeResponse> {
435
+ input.signal.throwIfAborted()
436
+ let onAbort: (() => void) | undefined
437
+ let deadline: ReturnType<typeof setTimeout> | undefined
438
+ try {
439
+ return await Promise.race([
440
+ input.query.initializationResult(),
441
+ new Promise<never>((_, reject) => {
442
+ onAbort = () => reject(input.signal.reason)
443
+ input.signal.addEventListener('abort', onAbort, { once: true })
444
+ deadline = setTimeout(() => reject(new AgentProviderLaunchError({
445
+ message: `Claude Code did not finish its initialize handshake within ${input.deadlineMs}ms.`,
446
+ retryable: true,
447
+ })), input.deadlineMs)
448
+ }),
449
+ ])
450
+ }
451
+ finally {
452
+ clearTimeout(deadline)
453
+ if (onAbort)
454
+ input.signal.removeEventListener('abort', onAbort)
455
+ }
456
+ }
457
+
458
+ function claudeRunEnvironment(environment: Record<string, string | undefined>, auth: ClaudeProviderAuth, subscriptionType: string | undefined): Record<string, string | undefined> {
459
+ const runEnvironment = { ...environment }
460
+ for (const key of inheritedClaudeCredentialKeys) delete runEnvironment[key]
461
+ runEnvironment.ANTHROPIC_BASE_URL = auth.baseUrl
462
+ runEnvironment.CLAUDE_CODE_OAUTH_TOKEN = auth.oauthToken
463
+ if (subscriptionType !== undefined)
464
+ runEnvironment.CLAUDE_CODE_SUBSCRIPTION_TYPE = subscriptionType
465
+ return runEnvironment
466
+ }
467
+
468
+ function fetchFreeSubscriptionType(subscriptionType: string | null): string | undefined {
469
+ return subscriptionType !== null && claudeFetchFreeSubscriptionTypes.has(subscriptionType) ? subscriptionType : undefined
470
+ }
471
+
157
472
  function firstClaudeModelId(...values: Array<string | null | undefined>): ClaudeModelId | undefined {
158
473
  for (const value of values) {
159
474
  const parsed = claudeModelIdSchema.safeParse(value)