@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.
- package/README.md +50 -0
- package/docs/consumer-capability-matrix.md +374 -0
- package/package.json +17 -10
- package/scripts/generate-codex-protocol.ts +711 -0
- package/src/agents/agent-operation.ts +8 -0
- package/src/agents/agent-provider.ts +3 -0
- package/src/agents/agent-run.ts +14 -2
- package/src/agents/agent-runtime-error.ts +28 -0
- package/src/agents/agent-session-events.ts +33 -0
- package/src/agents/agent-tool-runner.ts +12 -11
- package/src/agents/agent-tool.ts +12 -4
- package/src/agents/author-context.ts +1 -1
- package/src/agents/claude/claude-auth.ts +40 -0
- package/src/agents/claude/claude-browser-turn-metadata.ts +70 -0
- package/src/agents/claude/claude-code-process.ts +98 -0
- package/src/agents/claude/claude-command-mapper.ts +63 -3
- package/src/agents/claude/claude-desktop.ts +55 -0
- package/src/agents/claude/claude-event-mapper.ts +864 -135
- package/src/agents/claude/claude-file-change.ts +128 -0
- package/src/agents/claude/claude-provider.ts +344 -29
- package/src/agents/claude/claude-run.ts +893 -130
- package/src/agents/claude/claude-selected-repository-instructions.ts +91 -0
- package/src/agents/claude/claude-skill-catalog.ts +75 -0
- package/src/agents/claude/claude-steered-notifications.ts +180 -0
- package/src/agents/claude/claude-tool-mapper.ts +81 -2
- package/src/agents/claude/claude-workspace-skills.ts +87 -0
- package/src/agents/claude/index.ts +2 -0
- package/src/agents/codex/codex-auth-mapper.ts +27 -0
- package/src/agents/codex/codex-auth.ts +19 -0
- package/src/agents/codex/codex-browser-auth-broker.ts +320 -0
- package/src/agents/codex/codex-browser-auth-status.ts +55 -0
- package/src/agents/codex/codex-command-mapper.ts +38 -3
- package/src/agents/codex/codex-event-mapper.ts +375 -64
- package/src/agents/codex/codex-json-rpc-client.ts +66 -21
- package/src/agents/codex/codex-provider.ts +118 -236
- package/src/agents/codex/codex-run-auth.ts +113 -0
- package/src/agents/codex/codex-run.ts +423 -53
- package/src/agents/codex/codex-tool-mapper.ts +2 -2
- package/src/agents/codex/codex.errors.ts +8 -0
- package/src/agents/codex/generated/meta.gen.ts +144 -6
- package/src/agents/codex/generated/namespaces.gen.ts +92 -3
- package/src/agents/codex/generated/schema.gen.ts +24698 -15638
- package/src/agents/codex/lazy-codex-run-auth.ts +47 -0
- package/src/agents/index.ts +5 -0
- package/src/agents/input-attachment-preparation.ts +13 -0
- package/src/agents/mcp-tool-content.ts +82 -0
- package/src/agents/model-rebinding-run.ts +14 -3
- package/src/agents/selected-repository-instructions.ts +202 -0
- package/src/agents/session-context.ts +41 -0
- package/src/agents/subscription-token-refresh.ts +73 -0
- package/src/agents/user-input-request.ts +19 -20
- package/src/errors/index.ts +1 -1
- package/src/errors/validation-error.ts +6 -0
- package/src/protocol/agent-catalog.ts +159 -0
- package/src/protocol/agent-command.ts +52 -7
- package/src/protocol/agent-content.ts +36 -16
- package/src/protocol/agent-event.ts +210 -73
- package/src/protocol/agent-identity.ts +29 -0
- package/src/protocol/agent-json.ts +122 -8
- package/src/protocol/agent-metadata.ts +2 -2
- package/src/protocol/agent-model.ts +13 -1
- package/src/protocol/agent-notification.ts +110 -0
- package/src/protocol/agent-presentation.ts +71 -20
- package/src/protocol/agent-tool.ts +66 -10
- package/src/protocol/agent-usage.ts +3 -1
- package/src/protocol/agent-user-input.ts +4 -4
- package/src/protocol/agent-work.ts +12 -10
- package/src/protocol/index.ts +2 -0
- package/src/protocol/rejected-keys.ts +11 -0
- package/src/provenance.gen.ts +3 -3
- package/src/supervisor-protocol/agent-event-wrapper.ts +30 -0
|
@@ -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
|
package/src/agents/agent-run.ts
CHANGED
|
@@ -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?:
|
|
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 {
|
|
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<
|
|
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
|
-
|
|
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):
|
|
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
|
|
80
|
-
|
|
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
|
}
|
package/src/agents/agent-tool.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ZodRawShape } from 'zod'
|
|
2
|
-
import type { AgentProviderId, AgentToolDefinition,
|
|
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<
|
|
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<
|
|
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
|
|
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 {
|
|
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
|
+
}
|