@meistrari/agent-core 0.2.0-experimental.0 → 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 (90) hide show
  1. package/README.md +82 -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 +876 -124
  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 +922 -91
  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 +428 -48
  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 +54 -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 +4 -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/steps/contract.ts +4 -1
  72. package/src/steps/projector.ts +17 -1
  73. package/src/supervisor/agent-provider-factory.ts +60 -5
  74. package/src/supervisor/bootstrap-binder.ts +3 -2
  75. package/src/supervisor/index.ts +1 -1
  76. package/src/supervisor/persistence/supervisor-store.ts +22 -8
  77. package/src/supervisor/runtime-handler.ts +30 -2
  78. package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
  79. package/src/supervisor-protocol/bootstrap.ts +4 -1
  80. package/src/supervisor-protocol/command-body.ts +4 -1
  81. package/src/supervisor-protocol/envelopes/supervisor-to-control-plane.ts +1 -0
  82. package/src/testing/in-memory-runtime-control-plane.ts +118 -12
  83. package/src/testing/index.ts +1 -1
  84. package/src/worker-runtime-client/command-pump.ts +85 -10
  85. package/src/worker-runtime-client/connection-attempt.ts +95 -20
  86. package/src/worker-runtime-client/frame-processor.ts +242 -58
  87. package/src/worker-runtime-client/index.ts +8 -2
  88. package/src/worker-runtime-client/ports.ts +34 -2
  89. package/src/worker-runtime-client/rpc-request-manager.ts +152 -107
  90. package/src/worker-runtime-client/sandbox-connection-runtime.ts +72 -35
@@ -8,4 +8,12 @@ export interface AgentOperationOptions {
8
8
  signal?: AbortSignal
9
9
  inputAttachmentPreparation?: AgentInputAttachmentPreparation
10
10
  senderContext?: AgentSenderContext
11
+ // Trusted product-rendered context for a person-authored prompt, replacing the default author block.
12
+ authorContext?: string
13
+ }
14
+
15
+ export type AgentPromptOperationOptions = AgentOperationOptions & {
16
+ // `in-order` (the default) defers a `next` prompt while a turn is active. `bypass` is for a
17
+ // steer the application delivers ahead of queued work; it requires an active turn.
18
+ submissionOrder?: 'in-order' | 'bypass'
11
19
  }
@@ -9,6 +9,9 @@ import type { AgentRun } from './agent-run'
9
9
 
10
10
  interface AgentSessionConfigInput {
11
11
  sessionId: string
12
+ sessionWebUrl?: string
13
+ // An explicit per-session prompt overrides the provider composer; omission preserves its default.
14
+ instructions?: string
12
15
  cwd: string
13
16
  workspaceRepositoryRoots: readonly string[]
14
17
  reasoningEffort?: AgentReasoningEffort
@@ -1,17 +1,26 @@
1
1
  import type {
2
+ AgentContextCompactionRequestResult,
2
3
  AgentEvent,
4
+ AgentNotifyCommand,
3
5
  AgentRunMetadata,
6
+ CompactContextAgentCommand,
4
7
  InterruptAgentCommand,
5
8
  RespondUserInputAgentCommand,
6
9
  SendPromptCommand,
7
10
  StopAgentCommand,
8
11
  } from '../protocol'
9
- import type { AgentOperationOptions } from './agent-operation'
12
+ import type { AgentOperationOptions, AgentPromptOperationOptions } from './agent-operation'
10
13
 
11
14
  // Whether respond-user-input submitted provider input. `already_delivered` is the durable-store
12
15
  // idempotence fact for a redriven duplicate, not a provider prompt-acceptance receipt.
13
16
  export type AgentUserInputSubmission = 'submitted' | 'already_delivered'
14
17
 
18
+ export interface AgentNotificationAcceptance {
19
+ kind: 'turn'
20
+ turnId: string
21
+ placement: 'started' | 'steered'
22
+ }
23
+
15
24
  // Positive prompt-submission receipt. `turn` carries the provider's real turn identity (Codex: nested
16
25
  // `turn.id` from turn/start, flat `turnId` from turn/steer). `submitted` is the stream-submission arm
17
26
  // for providers whose input boundary carries no turn identity.
@@ -25,7 +34,10 @@ export interface AgentRun {
25
34
  prepareWorkspace: (options?: AgentOperationOptions) => Promise<void>
26
35
  // Providers without root registration accept the root without provider I/O.
27
36
  addWorkspaceRepositoryRoot: (root: string, options?: AgentOperationOptions) => Promise<void>
28
- sendPrompt: (command: SendPromptCommand, options?: AgentOperationOptions) => Promise<AgentPromptAcceptance>
37
+ sendPrompt: (command: SendPromptCommand, options?: AgentPromptOperationOptions) => Promise<AgentPromptAcceptance>
38
+ // Optional capabilities: the Claude and Codex runs implement both; other runs may omit them.
39
+ notify?: (command: AgentNotifyCommand, options?: AgentOperationOptions) => Promise<AgentNotificationAcceptance>
40
+ compactContext?: (command: CompactContextAgentCommand, options?: AgentOperationOptions) => Promise<AgentContextCompactionRequestResult>
29
41
  respondUserInput: (command: RespondUserInputAgentCommand, options?: AgentOperationOptions) => Promise<AgentUserInputSubmission>
30
42
  interrupt: (command: InterruptAgentCommand, options?: AgentOperationOptions) => Promise<void>
31
43
  stop: (command: StopAgentCommand, options?: AgentOperationOptions) => Promise<void>
@@ -98,6 +98,23 @@ export class AgentProviderMetadataError extends AgentProviderRuntimeError {
98
98
  }
99
99
  }
100
100
 
101
+ /**
102
+ * A provider that violated its protocol or omitted required metadata fails the same way on every
103
+ * attempt: a sandbox keeps its image-baked agent code for its lifetime, so redriving the same input
104
+ * cannot heal it. Walks the cause chain, so a delivery-unknown outcome caused by one qualifies.
105
+ */
106
+ export function isDeterministicProviderFailure(error: unknown): boolean {
107
+ const seen = new Set<unknown>()
108
+ let current: unknown = error
109
+ while (current instanceof Error && !seen.has(current)) {
110
+ if (current instanceof AgentProviderProtocolError || current instanceof AgentProviderMetadataError)
111
+ return true
112
+ seen.add(current)
113
+ current = current.cause
114
+ }
115
+ return false
116
+ }
117
+
101
118
  export class AgentRuntimeValidationError extends ValidationError {
102
119
  constructor(input: RuntimeValidationErrorInput) {
103
120
  const { code = 'agents.validation-failed', ...rest } = input
@@ -136,6 +153,17 @@ export class AgentRunStateError extends AgentRuntimeValidationError {
136
153
  }
137
154
  }
138
155
 
156
+ export class AgentNotificationStoreError extends InfrastructureError {
157
+ constructor(input: Omit<RuntimeInfrastructureErrorInput, 'code'>) {
158
+ super({
159
+ code: 'agents.notification-store-error',
160
+ publicMessage: 'Agent notification delivery state is unavailable.',
161
+ retryable: false,
162
+ ...input,
163
+ })
164
+ }
165
+ }
166
+
139
167
  export class AgentUserInputRequestStoreError extends InfrastructureError {
140
168
  constructor(input: Omit<RuntimeInfrastructureErrorInput, 'code'>) {
141
169
  super({
@@ -1,8 +1,10 @@
1
1
  import type { AgentReasoningEffort, AgentToolDefinition, SessionConfiguredAgentEvent } from '../protocol'
2
2
  import type { AgentEventDraft } from './agent-event-stream'
3
+ import { currentTurnUnavailableReasonMaxLength } from '../protocol'
3
4
 
4
5
  type SessionConfiguredPayload = SessionConfiguredAgentEvent['payload']
5
6
  type SessionConfiguredEventDraft = Extract<AgentEventDraft, { type: 'session.configured' }>
7
+ type SessionSettingsChangedEventDraft = Extract<AgentEventDraft, { type: 'session.settings.changed' }>
6
8
 
7
9
  export type SessionConfiguredEventInput = Pick<SessionConfiguredPayload, 'model' | 'cwd'> & {
8
10
  reasoningEffort?: AgentReasoningEffort
@@ -27,6 +29,37 @@ export function sessionConfiguredEvent(input: SessionConfiguredEventInput): Sess
27
29
  }
28
30
  }
29
31
 
32
+ // Where the provider applied a settings change: `current_turn` when the turn that handles the prompt
33
+ // runs on it (a switched running turn or the turn the prompt started); `next_turn` only when a steer
34
+ // could not switch the running turn. Only that miss carries the provider's reason, when it gave one.
35
+ export type AgentSettingsPlacement
36
+ = | { appliesFrom: 'current_turn' }
37
+ | { appliesFrom: 'next_turn', currentTurnUnavailableReason?: string }
38
+
39
+ export type SessionSettingsChangedEventInput = AgentSettingsPlacement & {
40
+ model: string
41
+ reasoningEffort?: AgentReasoningEffort
42
+ commandId: string
43
+ turnId?: string
44
+ }
45
+
46
+ // Emitted by a provider run once a prompt's settings change is applied and its input accepted.
47
+ export function sessionSettingsChangedEvent(input: SessionSettingsChangedEventInput): SessionSettingsChangedEventDraft {
48
+ return {
49
+ type: 'session.settings.changed',
50
+ payload: {
51
+ model: input.model,
52
+ reasoningEffort: input.reasoningEffort,
53
+ commandId: input.commandId,
54
+ turnId: input.turnId,
55
+ appliesFrom: input.appliesFrom,
56
+ ...(input.appliesFrom === 'next_turn' && input.currentTurnUnavailableReason !== undefined
57
+ ? { currentTurnUnavailableReason: input.currentTurnUnavailableReason.slice(0, currentTurnUnavailableReasonMaxLength) }
58
+ : {}),
59
+ },
60
+ }
61
+ }
62
+
30
63
  export function redactedEnvironment(environment: Record<string, string | undefined>): SessionConfiguredPayload['environment'] {
31
64
  return Object.fromEntries(Object.keys(environment).sort().map(key => [key, '[REDACTED]']))
32
65
  }
@@ -1,16 +1,16 @@
1
1
  import type { AppError } from '../errors'
2
- import type { AgentToolResult, JsonValue } from '../protocol'
3
- import type { AgentTool, AgentToolActor, AgentToolContext } from './agent-tool'
2
+ import type { JsonValue } from '../protocol'
3
+ import type { AgentTool, AgentToolActor, AgentToolContext, AgentToolExecutionResult } from './agent-tool'
4
4
  import { isAppError } from '../errors'
5
5
  import { AgentOperationAbortedError, AgentToolValidationError } from './agent-runtime-error'
6
- import { agentToolActorDenialMessage, agentToolAllowsActor } from './agent-tool'
6
+ import { agentToolActorDenialMessage, agentToolAllowsActor, agentToolExecutionResultSchema } from './agent-tool'
7
7
 
8
8
  export async function runAgentTool(input: {
9
9
  tool: AgentTool
10
10
  actor: AgentToolActor
11
11
  rawInput: JsonValue
12
12
  context: AgentToolContext
13
- }): Promise<AgentToolResult> {
13
+ }): Promise<AgentToolExecutionResult> {
14
14
  if (!agentToolAllowsActor(input.tool, input.actor))
15
15
  throw new AgentToolValidationError({ message: agentToolActorDenialMessage(input.tool) })
16
16
 
@@ -30,7 +30,7 @@ export async function runAgentTool(input: {
30
30
  abort()
31
31
 
32
32
  try {
33
- const result = await Promise.race([
33
+ const result: unknown = await Promise.race([
34
34
  input.tool.execute({
35
35
  input: parsed.data,
36
36
  context: {
@@ -41,8 +41,7 @@ export async function runAgentTool(input: {
41
41
  waitForAbort(abortController.signal, input.tool.name, input.tool.timeoutMs),
42
42
  ])
43
43
 
44
- validateToolResult(result)
45
- return result
44
+ return parseToolResult(result)
46
45
  }
47
46
  catch (error) {
48
47
  if (!isAppError(error))
@@ -68,7 +67,7 @@ async function waitForAbort(signal: AbortSignal, toolName: string, timeoutMs: nu
68
67
  })
69
68
  }
70
69
 
71
- function appErrorToolResult(error: AppError): AgentToolResult {
70
+ function appErrorToolResult(error: AppError): AgentToolExecutionResult {
72
71
  const serializedDetails = error.details === undefined ? '' : `\n\nDetails:\n${JSON.stringify(error.details, null, 2)}`
73
72
  return {
74
73
  success: false,
@@ -76,8 +75,10 @@ function appErrorToolResult(error: AppError): AgentToolResult {
76
75
  }
77
76
  }
78
77
 
79
- function validateToolResult(result: AgentToolResult): void {
80
- if (typeof result.success !== 'boolean' || !Array.isArray(result.content)) {
78
+ function parseToolResult(result: unknown): AgentToolExecutionResult {
79
+ const parsed = agentToolExecutionResultSchema.safeParse(result)
80
+ if (!parsed.success)
81
81
  throw new AgentToolValidationError({ message: 'Tool result must include success and content array.' })
82
- }
82
+
83
+ return parsed.data
83
84
  }
@@ -1,7 +1,7 @@
1
1
  import type { ZodRawShape } from 'zod'
2
- import type { AgentProviderId, AgentToolDefinition, AgentToolResult, AgentUserInputQuestion } from '../protocol'
2
+ import type { AgentProviderId, AgentToolDefinition, AgentUserInputQuestion } from '../protocol'
3
3
  import { z } from 'zod'
4
- import { jsonObjectSchema } from '../protocol'
4
+ import { agentToolImageContentSchema, agentToolResultSchema, agentToolTextContentSchema, jsonObjectSchema } from '../protocol'
5
5
  import { AgentToolValidationError } from './agent-runtime-error'
6
6
 
7
7
  export interface AgentToolContext {
@@ -23,6 +23,14 @@ export interface AgentToolExecuteInput<TShape extends ZodRawShape = ZodRawShape>
23
23
  export type AgentToolActor = 'main' | 'subagent'
24
24
  export type AgentToolActorScope = 'all' | 'main'
25
25
 
26
+ export const agentToolExecutionResultSchema = agentToolResultSchema.omit({ structuredContent: true, reportedSuccess: true }).extend({
27
+ content: z.array(z.discriminatedUnion('type', [
28
+ agentToolTextContentSchema.strict(),
29
+ agentToolImageContentSchema.strict(),
30
+ ])),
31
+ }).strict()
32
+ export type AgentToolExecutionResult = z.infer<typeof agentToolExecutionResultSchema>
33
+
26
34
  export interface AgentTool {
27
35
  readonly name: string
28
36
  readonly description: string
@@ -30,7 +38,7 @@ export interface AgentTool {
30
38
  readonly inputShape: ZodRawShape
31
39
  readonly inputSchema: z.ZodObject<ZodRawShape>
32
40
  readonly timeoutMs: number
33
- execute: (input: AgentToolExecuteInput) => Promise<AgentToolResult>
41
+ execute: (input: AgentToolExecuteInput) => Promise<AgentToolExecutionResult>
34
42
  definition: () => AgentToolDefinition
35
43
  }
36
44
 
@@ -40,7 +48,7 @@ export interface CreateAgentToolInput<TShape extends ZodRawShape> {
40
48
  actorScope?: AgentToolActorScope
41
49
  inputSchema: z.ZodObject<TShape>
42
50
  timeoutMs?: number
43
- execute: (input: AgentToolExecuteInput<TShape>) => Promise<AgentToolResult>
51
+ execute: (input: AgentToolExecuteInput<TShape>) => Promise<AgentToolExecutionResult>
44
52
  }
45
53
 
46
54
  const DEFAULT_TOOL_TIMEOUT_MS = 60_000
@@ -4,7 +4,7 @@ import type { AgentSenderContext } from './agent-operation'
4
4
  const OPEN_TAG = '<coding-agent-context>'
5
5
  const CLOSE_TAG = '</coding-agent-context>'
6
6
 
7
- const AUTHOR_CONTEXT_PATTERN = /^<coding-agent-context>[\s\S]*?<\/coding-agent-context>\n*/
7
+ const AUTHOR_CONTEXT_PATTERN = /^<coding-agent-context(?: [^>]*)?>[\s\S]*?<\/coding-agent-context>\n*/
8
8
  const slackMentionPattern = /^<@([\w-]+)(?:\|[^>]*)?>$/u
9
9
 
10
10
  export function renderAuthorContext(author: AgentAuthor, senderContext: AgentSenderContext = { kind: 'application' }): string {
@@ -0,0 +1,40 @@
1
+ import type { AgentError } from '../../protocol'
2
+
3
+ export type ClaudeAuthFailureReason = 'claude_auth_acquisition_failed' | 'claude_auth_refresh_failed'
4
+
5
+ /** A credential failure that needs outside action or is terminal; retrying the request cannot fix it. */
6
+ export type ClaudeAuthFailure = Readonly<{
7
+ reason: ClaudeAuthFailureReason
8
+ error: AgentError
9
+ }>
10
+
11
+ export type ClaudeAuthFailureSource = Readonly<{
12
+ /** Returns the unsubscribe function. */
13
+ subscribe: (listener: (failure: ClaudeAuthFailure) => void) => () => void
14
+ }>
15
+
16
+ /**
17
+ * How Claude Code reaches the Messages API. In the sandbox, `baseUrl` is the supervisor's loopback
18
+ * provider proxy and `oauthToken` is a fixed placeholder that the proxy exchanges for the
19
+ * server-issued access token, so no real credential enters the Claude process environment.
20
+ * `failures` reports credential failures that end a run with a typed reason once it has input.
21
+ */
22
+ export type ClaudeProviderAuth = Readonly<{
23
+ baseUrl: string
24
+ oauthToken: string
25
+ /**
26
+ * Makes a credential available before an opened run is published, as Codex acquires auth per
27
+ * open. Rejects with `AgentNotAcceptedError` when the credential needs external action, so the
28
+ * open fails instead of publishing a run whose first model request cannot authenticate.
29
+ */
30
+ acquire: (input: { signal: AbortSignal }) => Promise<void>
31
+ /**
32
+ * The subscription type (for example `max`) of the account the run's credential belongs to, as
33
+ * far as it is known without waiting: the installed credential's, or before the first
34
+ * acquisition the control plane's declared type; otherwise `null`. Read when a run spawns
35
+ * Claude Code, whose environment is then fixed, and again after `acquire` to detect a spawn
36
+ * that declared the wrong type.
37
+ */
38
+ knownSubscriptionType: () => string | null
39
+ failures: ClaudeAuthFailureSource
40
+ }>
@@ -0,0 +1,70 @@
1
+ import type { Options } from '@anthropic-ai/claude-agent-sdk'
2
+ import { randomUUID } from 'node:crypto'
3
+ import { mkdtemp, rename, rm, writeFile } from 'node:fs/promises'
4
+ import { tmpdir } from 'node:os'
5
+ import { join } from 'node:path'
6
+
7
+ // Named in the image's Claude node_repl definition; its relay reads the file before each tools/call.
8
+ export const turnMetadataPathVariable = 'CODING_AGENT_TURN_METADATA_PATH'
9
+
10
+ /**
11
+ * The Codex-shaped turn metadata that Browser Use's first-party runtime requires on every node_repl
12
+ * call (`session_id`, `turn_id`). Codex's MCP client sends it itself; for Claude, the run records
13
+ * its own provider session id and current turn id here, and the image's relay adds them to the
14
+ * call. The directory is private to the run and removed with it.
15
+ */
16
+ export class ClaudeBrowserTurnMetadata {
17
+ private constructor(private readonly directory: string, readonly path: string) {}
18
+
19
+ static async open(): Promise<ClaudeBrowserTurnMetadata> {
20
+ // mkdtemp creates the directory with mode 0700.
21
+ const directory = await mkdtemp(join(tmpdir(), 'claude-turn-metadata-'))
22
+ return new ClaudeBrowserTurnMetadata(directory, join(directory, 'turn-metadata.json'))
23
+ }
24
+
25
+ /** Atomically replaces the recorded metadata; without an active turn, removes it. */
26
+ async record(input: { sessionId: string, turnId: string | undefined }): Promise<void> {
27
+ if (input.turnId === undefined) {
28
+ await rm(this.path, { force: true })
29
+ return
30
+ }
31
+ const staged = `${this.path}.${randomUUID()}`
32
+ await writeFile(staged, JSON.stringify({ session_id: input.sessionId, turn_id: input.turnId }), { mode: 0o600, flag: 'wx' })
33
+ await rename(staged, this.path)
34
+ }
35
+
36
+ async close(): Promise<void> {
37
+ await rm(this.directory, { recursive: true, force: true })
38
+ }
39
+ }
40
+
41
+ /**
42
+ * Records the metadata before each call to the browser server's tools. Claude Code runs PreToolUse
43
+ * hooks before it sends the MCP tools/call, so the relay reads the metadata of the call's own turn.
44
+ * A call whose metadata cannot be recorded is denied rather than sent with a previous turn's ids.
45
+ */
46
+ export function claudeBrowserTurnMetadataHooks(input: { serverName: string, record: () => Promise<void> }): NonNullable<Options['hooks']> {
47
+ const prefix = `mcp__${input.serverName}__`
48
+ return {
49
+ PreToolUse: [{
50
+ matcher: `${prefix}.*`,
51
+ hooks: [async (hookInput) => {
52
+ if (hookInput.hook_event_name !== 'PreToolUse' || !hookInput.tool_name.startsWith(prefix))
53
+ return {}
54
+ try {
55
+ await input.record()
56
+ return {}
57
+ }
58
+ catch {
59
+ return {
60
+ hookSpecificOutput: {
61
+ hookEventName: 'PreToolUse' as const,
62
+ permissionDecision: 'deny' as const,
63
+ permissionDecisionReason: 'Browser Use turn metadata could not be recorded for this call.',
64
+ },
65
+ }
66
+ }
67
+ }],
68
+ }],
69
+ }
70
+ }
@@ -0,0 +1,98 @@
1
+ import type { SpawnedProcess, SpawnOptions } from '@anthropic-ai/claude-agent-sdk'
2
+ import type { ChildProcess } from 'node:child_process'
3
+ import { spawn } from 'node:child_process'
4
+
5
+ // After the SDK closes the CLI's stdin, Claude Code normally exits within milliseconds; the SDK
6
+ // itself sends SIGTERM after a 2 s grace (SDK 0.3.280). Past this bound the run kills the CLI's
7
+ // whole process group, including tool subprocesses. It stays below the supervisor's 10 s shutdown
8
+ // drain budget and the unit's 15 s TimeoutStopSec.
9
+ export const CLAUDE_PROCESS_EXIT_TIMEOUT_MS = 5_000
10
+ // SIGKILL cannot be caught; this only bounds the wait for the kernel to report the exit.
11
+ const killedExitWaitMs = 1_000
12
+ // Matches the SDK's own stderr tail (2048 characters) for process-exit errors.
13
+ const stderrTailLimit = 2_048
14
+
15
+ /**
16
+ * The one Claude Code process of a run. The SDK's `close()` and `return()` do not wait for the CLI
17
+ * to exit (`return()` gives up after 2 s) and signal only the CLI's pid, so a stopped run could
18
+ * leave a CLI still writing its transcript while a resume starts a second writer. The run spawns
19
+ * the CLI through `spawnClaudeCodeProcess` as its own process group so it can await the exit and,
20
+ * past the bound, kill the group.
21
+ */
22
+ export class ClaudeCodeProcess {
23
+ private child: ChildProcess | undefined
24
+ private exit: Promise<void> | undefined
25
+ private termination: Promise<void> | undefined
26
+ private stderr = ''
27
+
28
+ constructor(private readonly exitTimeoutMs = CLAUDE_PROCESS_EXIT_TIMEOUT_MS) {}
29
+
30
+ /** The SDK `spawnClaudeCodeProcess` hook. A run spawns at most one process. */
31
+ readonly spawn = (options: SpawnOptions): SpawnedProcess => {
32
+ if (this.child)
33
+ throw new Error('Claude Code process already spawned for this run.')
34
+ // `detached` makes the CLI a process-group leader. The forwarded signal aborts only after the
35
+ // SDK's graceful stdin-EOF window, as for the SDK's own spawn.
36
+ const child = spawn(options.command, options.args, { cwd: options.cwd, env: options.env, signal: options.signal, stdio: ['pipe', 'pipe', 'pipe'], detached: true, windowsHide: true })
37
+ this.child = child
38
+ this.exit = new Promise((resolve) => {
39
+ child.once('exit', () => resolve())
40
+ // A failed spawn emits `error` without `exit`; there is no process to wait for.
41
+ child.once('error', () => {
42
+ if (child.pid === undefined)
43
+ resolve()
44
+ })
45
+ })
46
+ // The SDK reads only stdin/stdout of a custom process; stderr must be drained here.
47
+ child.stderr?.setEncoding('utf8')
48
+ child.stderr?.on('data', (chunk: string) => {
49
+ this.stderr = (this.stderr + chunk).slice(-stderrTailLimit)
50
+ })
51
+ return child
52
+ }
53
+
54
+ /** The CLI's last stderr output, which a custom spawn keeps out of the SDK's exit errors. */
55
+ stderrTail(): string {
56
+ return this.stderr.trim()
57
+ }
58
+
59
+ /**
60
+ * Resolves once the CLI has exited. Call after `query.close()`, which sends stdin EOF. Past the
61
+ * exit bound the process group is killed. Idempotent; resolves at once if nothing was spawned.
62
+ */
63
+ async terminate(): Promise<void> {
64
+ this.termination ??= this.awaitExit()
65
+ await this.termination
66
+ }
67
+
68
+ private async awaitExit(): Promise<void> {
69
+ const child = this.child
70
+ const exit = this.exit
71
+ if (!child || !exit || child.pid === undefined)
72
+ return
73
+ if (await settlesWithin(exit, this.exitTimeoutMs))
74
+ return
75
+ try {
76
+ process.kill(-child.pid, 'SIGKILL')
77
+ }
78
+ catch {
79
+ child.kill('SIGKILL')
80
+ }
81
+ await settlesWithin(exit, killedExitWaitMs)
82
+ }
83
+ }
84
+
85
+ async function settlesWithin(promise: Promise<void>, ms: number): Promise<boolean> {
86
+ let timer: ReturnType<typeof setTimeout> | undefined
87
+ try {
88
+ return await Promise.race([
89
+ promise.then(() => true),
90
+ new Promise<boolean>((resolve) => {
91
+ timer = setTimeout(() => resolve(false), ms)
92
+ }),
93
+ ])
94
+ }
95
+ finally {
96
+ clearTimeout(timer)
97
+ }
98
+ }
@@ -1,9 +1,11 @@
1
1
  import type { SDKUserMessage } from '@anthropic-ai/claude-agent-sdk'
2
2
  import type { MessageParam } from '@anthropic-ai/sdk/resources'
3
- import type { AgentAuthor, AgentInputBlock, AgentPrompt } from '../../protocol'
3
+ import type { UUID } from 'node:crypto'
4
+ import type { AgentAuthor, AgentInputBlock, AgentNotifyCommand, AgentPrompt } from '../../protocol'
4
5
  import type { AgentSenderContext } from '../agent-operation'
5
6
  import type { AgentInputAttachmentPreparation, AgentInputAttachmentProjection, MaterializedAgentInputAttachment } from '../materialized-input-attachment'
6
7
  import { Buffer } from 'node:buffer'
8
+ import { serializeAgentNotificationToolOutput } from '../../protocol'
7
9
  import { AgentUnsupportedPromptBlockError } from '../agent-runtime-error'
8
10
  import { renderAuthorContext } from '../author-context'
9
11
  import { filesystemFallbackLabel, nativeAttachmentLabel, nativeImageMimeType, readAttachmentBytes } from '../input-attachment-preparation'
@@ -23,6 +25,9 @@ interface ClaudeUserMessageInput {
23
25
  origin?: 'user' | 'supervisor'
24
26
  author?: AgentAuthor
25
27
  senderContext?: AgentSenderContext
28
+ authorContext?: string
29
+ // The session's one-time context block, carried by its first prompt only.
30
+ sessionContext?: string
26
31
  }
27
32
  type ClaudeAttachmentProjection
28
33
  = | {
@@ -40,6 +45,59 @@ export function toClaudeUserMessage(input: ClaudeUserMessageInput): SDKUserMessa
40
45
  return claudeUserMessage(input, claudeMessageContent(input))
41
46
  }
42
47
 
48
+ export function toClaudeCompactContextMessage(commandId: string): SDKUserMessage {
49
+ return claudeUserMessage({
50
+ prompt: { blocks: [{ type: 'text', text: '/compact' }] },
51
+ commandId,
52
+ origin: 'supervisor',
53
+ }, [{ type: 'text', text: '/compact' }])
54
+ }
55
+
56
+ export const claudeNotificationTag = 'coding_agent_session_notifications'
57
+
58
+ /**
59
+ * Claude has no standalone tool-output input, so an application notification is a synthetic
60
+ * user-role message whose only content is the same bounded `{ commandId, instruction,
61
+ * notifications }` envelope Codex sends as the `coding_agent/session_notifications` tool output.
62
+ * The `task-notification` origin makes Claude Code frame it for the model as a non-user message
63
+ * ("[MESSAGE FROM NON-USER SOURCE - NOT USER INPUT]", CLI 2.1.280). `<`, `>`, and `&` are
64
+ * JSON-escaped, so an untrusted notification value cannot close the tag and forge a trusted
65
+ * envelope. Priority `next`: an idle session starts a turn with it.
66
+ */
67
+ export function toClaudeNotificationMessage(command: Pick<AgentNotifyCommand, 'commandId' | 'instruction' | 'notifications'>): SDKUserMessage {
68
+ return toClaudeTaskNotificationMessage({ text: claudeNotificationText(command), uuid: deriveMessageId(command.commandId) })
69
+ }
70
+
71
+ /**
72
+ * The same non-user framing for already-tagged envelope text, such as steered notifications whose
73
+ * turn ended before a hook delivered them. The caller owns the input uuid.
74
+ */
75
+ export function toClaudeTaskNotificationMessage(input: { text: string, uuid: UUID }): SDKUserMessage {
76
+ return {
77
+ type: 'user',
78
+ message: { role: 'user', content: [{ type: 'text', text: input.text }] },
79
+ parent_tool_use_id: null,
80
+ priority: 'next',
81
+ uuid: input.uuid,
82
+ isSynthetic: true,
83
+ origin: { kind: 'task-notification' },
84
+ }
85
+ }
86
+
87
+ /** The tagged, escaped envelope; also the hook payload for a notification steered into a turn. */
88
+ export function claudeNotificationText(command: Pick<AgentNotifyCommand, 'commandId' | 'instruction' | 'notifications'>): string {
89
+ const envelope = serializeAgentNotificationToolOutput({
90
+ commandId: command.commandId,
91
+ instruction: command.instruction,
92
+ notifications: command.notifications,
93
+ })
94
+ return `<${claudeNotificationTag}>\n${escapeForTaggedJson(envelope)}\n</${claudeNotificationTag}>`
95
+ }
96
+
97
+ function escapeForTaggedJson(json: string): string {
98
+ return json.replaceAll('<', '\\u003c').replaceAll('>', '\\u003e').replaceAll('&', '\\u0026')
99
+ }
100
+
43
101
  export async function mapClaudeUserMessage(input: ClaudeUserMessageInput, preparation?: AgentInputAttachmentPreparation): Promise<SDKUserMessage> {
44
102
  return preparation
45
103
  ? await prepareClaudeUserMessage({ ...input, preparation })
@@ -78,10 +136,12 @@ function claudeUserMessage(input: ClaudeUserMessageInput, content: ClaudeContent
78
136
  }
79
137
  }
80
138
 
81
- function claudeMessageContent({ prompt, origin = 'user', author, senderContext }: ClaudeUserMessageInput): ClaudeContentBlock[] {
139
+ function claudeMessageContent({ prompt, origin = 'user', author, senderContext, authorContext, sessionContext }: ClaudeUserMessageInput): ClaudeContentBlock[] {
82
140
  const blocks = prompt.blocks.map(toClaudeContentBlock)
141
+ if (sessionContext)
142
+ blocks.unshift({ type: 'text', text: sessionContext })
83
143
  return origin === 'user' && author
84
- ? [{ type: 'text', text: renderAuthorContext(author, senderContext) }, ...blocks]
144
+ ? [{ type: 'text', text: authorContext ?? renderAuthorContext(author, senderContext) }, ...blocks]
85
145
  : blocks
86
146
  }
87
147
 
@@ -0,0 +1,55 @@
1
+ import type { McpStdioServerConfig } from '@anthropic-ai/claude-agent-sdk'
2
+ import type { CodexAuthProvider } from '../codex/codex-auth'
3
+ import { turnMetadataPathVariable } from './claude-browser-turn-metadata'
4
+
5
+ /** One image-owned desktop MCP server, as generated by the sandbox desktop configuration. */
6
+ export type ClaudeDesktopMcpServer = Readonly<{
7
+ name: string
8
+ command: string
9
+ args: readonly string[]
10
+ env: Readonly<Record<string, string>>
11
+ // Values the run supplies from its environment (or the browser-auth broker) at spawn time.
12
+ runtimeEnv: readonly string[]
13
+ }>
14
+
15
+ /**
16
+ * The sandbox desktop for Claude: the same Computer Use / Browser Use MCP servers Codex receives,
17
+ * plus the Codex credential source that Browser Use's first-party runtime requires. The server
18
+ * named `browserAuth.serverName` receives the run's broker socket as
19
+ * `CODING_AGENT_BROWSER_AUTH_SOCKET` and its turn metadata file as
20
+ * `CODING_AGENT_TURN_METADATA_PATH`; the credential itself never enters the Claude environment.
21
+ */
22
+ export type ClaudeDesktopConfig = Readonly<{
23
+ servers: readonly ClaudeDesktopMcpServer[]
24
+ browserAuth: Readonly<{
25
+ serverName: string
26
+ codexExecutable: string
27
+ codexAuth: CodexAuthProvider
28
+ }>
29
+ }>
30
+
31
+ export const browserAuthSocketVariable = 'CODING_AGENT_BROWSER_AUTH_SOCKET'
32
+
33
+ export function claudeDesktopMcpServers(input: {
34
+ desktop: ClaudeDesktopConfig
35
+ environment: Readonly<Record<string, string | undefined>>
36
+ browserAuthSocketPath: string
37
+ turnMetadataPath: string
38
+ }): Record<string, McpStdioServerConfig> {
39
+ // Run-owned values reach only the browser server, never another desktop server that names them.
40
+ const browserRunValues = new Map([
41
+ [browserAuthSocketVariable, input.browserAuthSocketPath],
42
+ [turnMetadataPathVariable, input.turnMetadataPath],
43
+ ])
44
+ return Object.fromEntries(input.desktop.servers.map((server) => {
45
+ const runtime: Record<string, string> = {}
46
+ for (const name of server.runtimeEnv) {
47
+ const value = server.name === input.desktop.browserAuth.serverName && browserRunValues.has(name)
48
+ ? browserRunValues.get(name)
49
+ : input.environment[name]
50
+ if (value !== undefined)
51
+ runtime[name] = value
52
+ }
53
+ return [server.name, { type: 'stdio', command: server.command, args: [...server.args], env: { ...server.env, ...runtime } }]
54
+ }))
55
+ }