claude-agent-sdk 1.0.0 → 1.2.0
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.
- checksums.yaml +4 -4
- data/.yardopts +10 -0
- data/CHANGELOG.md +110 -0
- data/README.md +43 -31
- data/docs/cli-installer.md +26 -4
- data/docs/client.md +40 -11
- data/docs/configuration.md +206 -1
- data/docs/errors.md +32 -2
- data/docs/hooks-and-permissions.md +30 -10
- data/docs/mcp-servers.md +30 -9
- data/docs/observability.md +61 -10
- data/docs/options.md +232 -0
- data/docs/rails.md +263 -18
- data/docs/sessions.md +40 -12
- data/docs/subagents.md +1 -1
- data/docs/types.md +100 -11
- data/lib/claude_agent_sdk/cli_installer.rb +140 -19
- data/lib/claude_agent_sdk/command_builder.rb +84 -27
- data/lib/claude_agent_sdk/fiber_boundary.rb +45 -2
- data/lib/claude_agent_sdk/instrumentation/otel.rb +90 -28
- data/lib/claude_agent_sdk/query.rb +547 -132
- data/lib/claude_agent_sdk/railtie.rb +27 -2
- data/lib/claude_agent_sdk/sdk_mcp_server.rb +78 -26
- data/lib/claude_agent_sdk/session_mutations.rb +112 -92
- data/lib/claude_agent_sdk/session_resume.rb +356 -39
- data/lib/claude_agent_sdk/session_store.rb +31 -2
- data/lib/claude_agent_sdk/sessions.rb +720 -138
- data/lib/claude_agent_sdk/subprocess_cli_transport.rb +252 -29
- data/lib/claude_agent_sdk/testing/session_store_conformance.rb +18 -7
- data/lib/claude_agent_sdk/transcript_mirror_batcher.rb +45 -37
- data/lib/claude_agent_sdk/transport.rb +28 -12
- data/lib/claude_agent_sdk/types/attributes.rb +9 -0
- data/lib/claude_agent_sdk/types/base.rb +85 -15
- data/lib/claude_agent_sdk/types/hooks.rb +73 -0
- data/lib/claude_agent_sdk/types/mcp.rb +37 -1
- data/lib/claude_agent_sdk/types/messages.rb +7 -1
- data/lib/claude_agent_sdk/types/option_values.rb +186 -4
- data/lib/claude_agent_sdk/types/options.rb +104 -17
- data/lib/claude_agent_sdk/types/permissions.rb +18 -9
- data/lib/claude_agent_sdk/version.rb +1 -1
- data/lib/claude_agent_sdk.rb +111 -53
- data/lib/generators/claude_agent_sdk/install/templates/claude_agent_sdk.rb.tt +6 -0
- data/sig/claude_agent_sdk/types/hooks.rbs +6 -3
- data/sig/claude_agent_sdk/types/option_values.rbs +23 -6
- data/sig/claude_agent_sdk/types/options.rbs +20 -7
- data/sig/claude_agent_sdk/types/permissions.rbs +4 -2
- metadata +6 -4
|
@@ -1,42 +1,58 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module ClaudeAgentSDK
|
|
4
|
-
# Abstract
|
|
4
|
+
# Abstract base class for transports: the channel between the SDK and a
|
|
5
|
+
# Claude Code CLI process.
|
|
5
6
|
#
|
|
6
|
-
#
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
7
|
+
# Public API, covered by this gem's SemVer promise like the rest of the
|
|
8
|
+
# documented surface. {SubprocessCLITransport}, the default, spawns the CLI
|
|
9
|
+
# locally. To run the CLI somewhere else (a container, an SSH host, a
|
|
10
|
+
# sandbox VM), subclass this class, or write any object with the same
|
|
11
|
+
# methods, and pass it to {Client#initialize} as `transport_class:` or to
|
|
12
|
+
# {ClaudeAgentSDK.query} / {ClaudeAgentSDK.ask} as `transport:`. See
|
|
13
|
+
# docs/client.md, "Custom Transport".
|
|
14
|
+
#
|
|
15
|
+
# The SDK calls five methods: {#connect}, {#write}, {#read_messages},
|
|
16
|
+
# {#end_input} and {#close}. It never calls {#ready?}, so that one is
|
|
17
|
+
# optional. Every method here raises NotImplementedError until a subclass
|
|
18
|
+
# overrides it.
|
|
10
19
|
class Transport
|
|
11
|
-
#
|
|
20
|
+
# Establish the connection (spawn or reach the CLI) and prepare for IO.
|
|
12
21
|
def connect
|
|
13
22
|
raise NotImplementedError, 'Subclasses must implement #connect'
|
|
14
23
|
end
|
|
15
24
|
|
|
16
|
-
# Write raw data to the
|
|
25
|
+
# Write raw data to the CLI's stdin.
|
|
17
26
|
# @param data [String] Raw string data to write (typically JSON + newline)
|
|
18
27
|
def write(data)
|
|
19
28
|
raise NotImplementedError, 'Subclasses must implement #write'
|
|
20
29
|
end
|
|
21
30
|
|
|
22
|
-
# Read and
|
|
23
|
-
#
|
|
31
|
+
# Read the CLI's stdout and yield each line as a parsed message, blocking
|
|
32
|
+
# until the stream ends. Parse each line with
|
|
33
|
+
# `JSON.parse(line, symbolize_names: true)`: the SDK reads Symbol keys.
|
|
34
|
+
# The SDK always passes a block.
|
|
35
|
+
# @yield [Hash{Symbol => Object}] Each parsed message
|
|
24
36
|
def read_messages
|
|
25
37
|
raise NotImplementedError, 'Subclasses must implement #read_messages'
|
|
26
38
|
end
|
|
27
39
|
|
|
28
|
-
#
|
|
40
|
+
# Terminate the CLI and clean up resources. Must be idempotent: the SDK
|
|
41
|
+
# can call it more than once, and calls it after a failed {#connect} too.
|
|
29
42
|
def close
|
|
30
43
|
raise NotImplementedError, 'Subclasses must implement #close'
|
|
31
44
|
end
|
|
32
45
|
|
|
33
|
-
# Check if transport is ready for communication
|
|
46
|
+
# Check if transport is ready for communication. Optional: the SDK never
|
|
47
|
+
# calls it.
|
|
34
48
|
# @return [Boolean] True if transport is ready to send/receive messages
|
|
35
49
|
def ready?
|
|
36
50
|
raise NotImplementedError, 'Subclasses must implement #ready?'
|
|
37
51
|
end
|
|
38
52
|
|
|
39
|
-
# End the input stream (close stdin for process transports)
|
|
53
|
+
# End the input stream (close stdin for process transports). The SDK
|
|
54
|
+
# calls it when a one-shot query's run is over and when a streamed prompt
|
|
55
|
+
# is exhausted.
|
|
40
56
|
def end_input
|
|
41
57
|
raise NotImplementedError, 'Subclasses must implement #end_input'
|
|
42
58
|
end
|
|
@@ -184,6 +184,10 @@ module ClaudeAgentSDK
|
|
|
184
184
|
end
|
|
185
185
|
|
|
186
186
|
def read_attribute(name)
|
|
187
|
+
# A writer's name (`session_id=`) is not readable. Checked before the
|
|
188
|
+
# cache, which also holds the camelCase writers method_missing resolved.
|
|
189
|
+
return if writer_name?(name)
|
|
190
|
+
|
|
187
191
|
reader = self.class.cached_attribute_reader(name)
|
|
188
192
|
return public_send(reader) if reader
|
|
189
193
|
|
|
@@ -198,6 +202,11 @@ module ClaudeAgentSDK
|
|
|
198
202
|
end
|
|
199
203
|
end
|
|
200
204
|
|
|
205
|
+
def writer_name?(name)
|
|
206
|
+
name = name.to_s unless name.is_a?(Symbol) || name.is_a?(String)
|
|
207
|
+
name.end_with?('=')
|
|
208
|
+
end
|
|
209
|
+
|
|
201
210
|
# Whether a normalized reader, writer or predicate name belongs to an
|
|
202
211
|
# attribute: a declared one (`session_id`, `session_id=`,
|
|
203
212
|
# `fork_session?`), or a method user code defined on its own subclass, a
|
|
@@ -122,8 +122,19 @@ module ClaudeAgentSDK
|
|
|
122
122
|
# dumps every ivar recursively — those (SDK MCP server instances, store
|
|
123
123
|
# adapters, observers) show as `#<ClassName>`. For display only: nothing
|
|
124
124
|
# sent to the CLI goes through #inspect or #to_s (wire output uses #to_h).
|
|
125
|
+
#
|
|
126
|
+
# Attributes that carry credentials print as `[FILTERED]` (a Hash keeps
|
|
127
|
+
# its keys): `env`, `settings` and `extra_args` of ClaudeAgentOptions,
|
|
128
|
+
# and `env`, `args` and `headers` of the MCP server configs. An MCP
|
|
129
|
+
# server `url` prints its scheme and host only, and a Hash server config
|
|
130
|
+
# (in `mcp_servers`, or echoed by the CLI in McpServerStatus#config) is
|
|
131
|
+
# filtered like a typed one. `mcp_servers` given as a String prints as
|
|
132
|
+
# `[FILTERED]`.
|
|
133
|
+
#
|
|
134
|
+
# It does not raise: a value that fails while it is rendered shows as
|
|
135
|
+
# `#<ClassName>` (`#<?>` when even its class cannot be asked).
|
|
125
136
|
def inspect
|
|
126
|
-
|
|
137
|
+
inspect_bounded(self, 0, {}.compare_by_identity)
|
|
127
138
|
end
|
|
128
139
|
|
|
129
140
|
# Object#to_s ignores instance variables, so `puts message` would print
|
|
@@ -134,9 +145,10 @@ module ClaudeAgentSDK
|
|
|
134
145
|
inspect
|
|
135
146
|
end
|
|
136
147
|
|
|
137
|
-
# Declares attributes that carry credentials (env vars, auth headers
|
|
138
|
-
#
|
|
139
|
-
#
|
|
148
|
+
# Declares attributes that carry credentials (env vars, auth headers,
|
|
149
|
+
# settings, extra CLI arguments, an MCP server's args). Objects get
|
|
150
|
+
# logged, so #inspect shows them filtered; #to_h and everything sent to
|
|
151
|
+
# the CLI are unaffected. Inherited by subclasses.
|
|
140
152
|
#
|
|
141
153
|
# @api private
|
|
142
154
|
def self.inspect_filtered(*names)
|
|
@@ -182,7 +194,7 @@ module ClaudeAgentSDK
|
|
|
182
194
|
filtered = self.class.inspect_filtered_attributes
|
|
183
195
|
instance_variables.filter_map do |ivar|
|
|
184
196
|
value = instance_variable_get(ivar)
|
|
185
|
-
next if value.nil
|
|
197
|
+
next if nil.equal?(value) # not value.nil?: a BasicObject has no such method
|
|
186
198
|
|
|
187
199
|
name = ivar.to_s.delete_prefix('@')
|
|
188
200
|
[name, filtered.include?(name) ? inspect_filter(value) : value]
|
|
@@ -191,15 +203,65 @@ module ClaudeAgentSDK
|
|
|
191
203
|
|
|
192
204
|
# A credential-bearing Hash keeps its keys (useful when debugging which
|
|
193
205
|
# variables are set) with every value replaced; anything else is replaced
|
|
194
|
-
# outright
|
|
206
|
+
# outright, and so is a value that fails while it is asked for its keys.
|
|
207
|
+
# Builds a new Hash; the object itself is never touched.
|
|
195
208
|
def inspect_filter(value)
|
|
196
209
|
value.respond_to?(:each_key) ? value.each_key.to_h { |key| [key, '[FILTERED]'] } : '[FILTERED]'
|
|
210
|
+
rescue StandardError
|
|
211
|
+
'[FILTERED]'
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
INSPECT_URL_ORIGIN = %r{
|
|
215
|
+
\A(https?|wss?):// # scheme
|
|
216
|
+
(?:[^/?\#\\]*@)? # userinfo, up to the last @ before the path
|
|
217
|
+
(\[[0-9a-f:.]+\]|[[:alnum:]._-]+) # host: an IPv6 literal or a name
|
|
218
|
+
(:\d{1,5})? # port
|
|
219
|
+
(?=[/?\#]|\z) # then the path, the query, the fragment or the end
|
|
220
|
+
}ix
|
|
221
|
+
private_constant :INSPECT_URL_ORIGIN
|
|
222
|
+
|
|
223
|
+
# A URL can carry a credential in its userinfo, path, query or fragment,
|
|
224
|
+
# so only where it points is shown: `https://mcp.example.com/[FILTERED]`
|
|
225
|
+
# (scheme, host and port). Anything else is replaced outright: a value
|
|
226
|
+
# that is not a String, another scheme, a host or port with a character
|
|
227
|
+
# that does not belong there.
|
|
228
|
+
def inspect_filter_url(value)
|
|
229
|
+
origin = value.is_a?(String) && INSPECT_URL_ORIGIN.match(value)
|
|
230
|
+
origin ? "#{origin[1]}://#{origin[2]}#{origin[3]}/[FILTERED]" : '[FILTERED]'
|
|
231
|
+
rescue StandardError
|
|
232
|
+
'[FILTERED]'
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# Keys of a raw Hash MCP server config whose values #inspect shows as they are.
|
|
236
|
+
INSPECT_MCP_CONFIG_KEYS = %w[type command name instance].freeze
|
|
237
|
+
private_constant :INSPECT_MCP_CONFIG_KEYS
|
|
238
|
+
|
|
239
|
+
# An MCP server config held as a raw Hash (in ClaudeAgentOptions#mcp_servers,
|
|
240
|
+
# or echoed by the CLI in McpServerStatus#config) carries the same
|
|
241
|
+
# credentials as a typed one: env, headers, args, a token in the url. It
|
|
242
|
+
# keeps its keys and shows what identifies the server: its type, its
|
|
243
|
+
# command (or the SDK server's name and instance) and the scheme and host
|
|
244
|
+
# of its url. Every other value is filtered here rather than left to the
|
|
245
|
+
# nesting limit of #inspect.
|
|
246
|
+
def inspect_mcp_server_config(config)
|
|
247
|
+
config.to_h do |key, value|
|
|
248
|
+
name = key.to_s
|
|
249
|
+
next [key, value] if INSPECT_MCP_CONFIG_KEYS.include?(name)
|
|
250
|
+
|
|
251
|
+
[key, name == 'url' ? inspect_filter_url(value) : '[FILTERED]']
|
|
252
|
+
end
|
|
197
253
|
end
|
|
198
254
|
|
|
199
255
|
def inspect_class_name
|
|
200
256
|
self.class.name || self.class.inspect
|
|
201
257
|
end
|
|
202
258
|
|
|
259
|
+
# Printing must never raise (it runs inside loggers and `puts`). The
|
|
260
|
+
# value's own methods run here (a String subclass's #length, a Hash
|
|
261
|
+
# subclass's #size, a Type subclass's #inspect_attributes), so a value
|
|
262
|
+
# that fails while it is rendered becomes a placeholder and the rest of
|
|
263
|
+
# the object still prints. Only StandardError is rescued: Interrupt,
|
|
264
|
+
# SystemExit and the like pass through.
|
|
203
265
|
def inspect_bounded(value, depth, seen)
|
|
204
266
|
case value
|
|
205
267
|
when Type then value.inspect_with(depth, seen)
|
|
@@ -212,6 +274,16 @@ module ClaudeAgentSDK
|
|
|
212
274
|
when Proc, Method, UnboundMethod then inspect_callable(value)
|
|
213
275
|
else inspect_leaf(value)
|
|
214
276
|
end
|
|
277
|
+
rescue StandardError
|
|
278
|
+
inspect_placeholder(value)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# `#<ClassName>` for a value that could not be rendered; a BasicObject
|
|
282
|
+
# cannot even be asked for its class.
|
|
283
|
+
def inspect_placeholder(value)
|
|
284
|
+
inspect_truncated_text("#<#{value.class}>")
|
|
285
|
+
rescue StandardError
|
|
286
|
+
'#<?>'
|
|
215
287
|
end
|
|
216
288
|
|
|
217
289
|
def inspect_container(value, open, close, depth, seen, &)
|
|
@@ -231,7 +303,10 @@ module ClaudeAgentSDK
|
|
|
231
303
|
# Rendered by hand rather than via Hash#inspect, whose format differs
|
|
232
304
|
# between Ruby 3.3 (`{:a=>1}`) and 3.4 (`{a: 1}`).
|
|
233
305
|
def inspect_hash_key(key, depth, seen)
|
|
234
|
-
|
|
306
|
+
case key # not key.is_a?: a BasicObject key has no such method
|
|
307
|
+
when Symbol
|
|
308
|
+
return "#{key.name}: " if key.inspect.match?(/\A:\w+[?!]?\z/)
|
|
309
|
+
end
|
|
235
310
|
|
|
236
311
|
"#{inspect_bounded(key, depth, seen)} => "
|
|
237
312
|
end
|
|
@@ -265,19 +340,14 @@ module ClaudeAgentSDK
|
|
|
265
340
|
"#{rendered[0, INSPECT_MAX_STRING]}…(+#{rendered.length - INSPECT_MAX_STRING} chars)"
|
|
266
341
|
end
|
|
267
342
|
|
|
268
|
-
#
|
|
269
|
-
#
|
|
270
|
-
# to a placeholder.
|
|
343
|
+
# An object whose #inspect raises, or a BasicObject without one, falls
|
|
344
|
+
# back to a placeholder.
|
|
271
345
|
def inspect_leaf(value)
|
|
272
346
|
return "#<#{value.class}>" if kernel_inspect_only?(value)
|
|
273
347
|
|
|
274
348
|
inspect_truncated_text(value.inspect)
|
|
275
349
|
rescue StandardError
|
|
276
|
-
|
|
277
|
-
"#<#{value.class}>"
|
|
278
|
-
rescue StandardError
|
|
279
|
-
'#<?>'
|
|
280
|
-
end
|
|
350
|
+
inspect_placeholder(value)
|
|
281
351
|
end
|
|
282
352
|
|
|
283
353
|
def kernel_inspect_only?(value)
|
|
@@ -637,4 +637,77 @@ module ClaudeAgentSDK
|
|
|
637
637
|
result
|
|
638
638
|
end
|
|
639
639
|
end
|
|
640
|
+
|
|
641
|
+
# The spellings a hook callback's return value may use for a field of the
|
|
642
|
+
# typed output classes above, mapped to the key the CLI reads. A Hash a
|
|
643
|
+
# callback returns stands for the typed output with the same fields: its
|
|
644
|
+
# keys may be Symbols or Strings, the attribute names (snake_case) or what
|
|
645
|
+
# #to_h emits (camelCase), at the top level and inside hook_specific_output.
|
|
646
|
+
#
|
|
647
|
+
# Only names that differ from their wire key are listed. A key that is not
|
|
648
|
+
# listed is sent as written, so a field of a newer CLI that the typed
|
|
649
|
+
# classes do not model still gets through, in the CLI's own spelling.
|
|
650
|
+
# spec/unit/hook_output_normalization_spec.rb walks every typed output
|
|
651
|
+
# class and fails when an attribute and these tables disagree.
|
|
652
|
+
#
|
|
653
|
+
# @api private
|
|
654
|
+
module HookOutputKeys
|
|
655
|
+
# SyncHookJSONOutput and AsyncHookJSONOutput attributes, plus the
|
|
656
|
+
# Ruby-safe spellings of the two keywords.
|
|
657
|
+
TOP_LEVEL = {
|
|
658
|
+
'continue_' => 'continue',
|
|
659
|
+
'async_' => 'async',
|
|
660
|
+
'suppress_output' => 'suppressOutput',
|
|
661
|
+
'stop_reason' => 'stopReason',
|
|
662
|
+
'system_message' => 'systemMessage',
|
|
663
|
+
'hook_specific_output' => 'hookSpecificOutput',
|
|
664
|
+
'async_timeout' => 'asyncTimeout'
|
|
665
|
+
}.freeze
|
|
666
|
+
|
|
667
|
+
# Attributes of the *HookSpecificOutput classes.
|
|
668
|
+
HOOK_SPECIFIC = {
|
|
669
|
+
'hook_event_name' => 'hookEventName',
|
|
670
|
+
'permission_decision' => 'permissionDecision',
|
|
671
|
+
'permission_decision_reason' => 'permissionDecisionReason',
|
|
672
|
+
'updated_input' => 'updatedInput',
|
|
673
|
+
'additional_context' => 'additionalContext',
|
|
674
|
+
'updated_tool_output' => 'updatedToolOutput',
|
|
675
|
+
'updated_mcp_tool_output' => 'updatedMCPToolOutput',
|
|
676
|
+
'watch_paths' => 'watchPaths'
|
|
677
|
+
}.freeze
|
|
678
|
+
|
|
679
|
+
# The hook output Hash as the CLI reads it: String keys in wire
|
|
680
|
+
# spelling, at the top level and one level down, inside
|
|
681
|
+
# hookSpecificOutput. Values are never rewritten: updatedInput and the
|
|
682
|
+
# tool outputs are the tool's own payloads, and a PermissionRequest
|
|
683
|
+
# decision goes out as the caller wrote it.
|
|
684
|
+
def self.normalize(output)
|
|
685
|
+
normalized = rename(output, TOP_LEVEL)
|
|
686
|
+
specific = normalized['hookSpecificOutput']
|
|
687
|
+
normalized['hookSpecificOutput'] = rename(specific, HOOK_SPECIFIC) if specific.is_a?(Hash)
|
|
688
|
+
normalized
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
# Every key ends up as one String, so a Symbol and a String spelling the
|
|
692
|
+
# same field cannot both reach JSON.generate (json 3.x raises on that;
|
|
693
|
+
# 2.x emits the key twice). When a Hash carries both spellings of one
|
|
694
|
+
# field the wire spelling wins, whichever comes first; between two keys
|
|
695
|
+
# in the same spelling the later one does.
|
|
696
|
+
def self.rename(hash, table)
|
|
697
|
+
renamed = {}
|
|
698
|
+
wire_spelled = {}
|
|
699
|
+
hash.each do |key, value|
|
|
700
|
+
name = key.to_s
|
|
701
|
+
wire = table.fetch(name, name)
|
|
702
|
+
if wire == name
|
|
703
|
+
wire_spelled[wire] = true
|
|
704
|
+
elsif wire_spelled.key?(wire)
|
|
705
|
+
next
|
|
706
|
+
end
|
|
707
|
+
renamed[wire] = value
|
|
708
|
+
end
|
|
709
|
+
renamed
|
|
710
|
+
end
|
|
711
|
+
private_class_method :rename
|
|
712
|
+
end
|
|
640
713
|
end
|
|
@@ -109,6 +109,25 @@ module ClaudeAgentSDK
|
|
|
109
109
|
config
|
|
110
110
|
end
|
|
111
111
|
end
|
|
112
|
+
|
|
113
|
+
private
|
|
114
|
+
|
|
115
|
+
# The CLI echoes a server's config back with its credentials (headers, a
|
|
116
|
+
# token in the url, args). A Hash config prints like a raw Hash config in
|
|
117
|
+
# ClaudeAgentOptions#mcp_servers; the typed proxy and SDK configs print
|
|
118
|
+
# as they are.
|
|
119
|
+
def inspect_attributes
|
|
120
|
+
super.map { |name, value| [name, name == 'config' ? inspect_config(value) : value] }
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def inspect_config(config)
|
|
124
|
+
case config # not config.is_a?: a BasicObject has no such method
|
|
125
|
+
when Hash then inspect_mcp_server_config(config)
|
|
126
|
+
else config
|
|
127
|
+
end
|
|
128
|
+
rescue StandardError
|
|
129
|
+
'[FILTERED]'
|
|
130
|
+
end
|
|
112
131
|
end
|
|
113
132
|
|
|
114
133
|
# Response from get_mcp_status containing all server statuses
|
|
@@ -138,7 +157,8 @@ module ClaudeAgentSDK
|
|
|
138
157
|
attr_accessor :command, :args, :env
|
|
139
158
|
attr_reader :type
|
|
140
159
|
|
|
141
|
-
|
|
160
|
+
# `args` carries flags such as `--api-key <key>`.
|
|
161
|
+
inspect_filtered :env, :args
|
|
142
162
|
|
|
143
163
|
def initialize(attributes = {})
|
|
144
164
|
super
|
|
@@ -173,6 +193,14 @@ module ClaudeAgentSDK
|
|
|
173
193
|
result[:headers] = @headers if @headers
|
|
174
194
|
result
|
|
175
195
|
end
|
|
196
|
+
|
|
197
|
+
private
|
|
198
|
+
|
|
199
|
+
# The url can carry a token (userinfo, path or query): #inspect shows
|
|
200
|
+
# its scheme and host.
|
|
201
|
+
def inspect_attributes
|
|
202
|
+
super.map { |name, value| [name, name == 'url' ? inspect_filter_url(value) : value] }
|
|
203
|
+
end
|
|
176
204
|
end
|
|
177
205
|
|
|
178
206
|
class McpHttpServerConfig < Type
|
|
@@ -195,6 +223,14 @@ module ClaudeAgentSDK
|
|
|
195
223
|
result[:headers] = @headers if @headers
|
|
196
224
|
result
|
|
197
225
|
end
|
|
226
|
+
|
|
227
|
+
private
|
|
228
|
+
|
|
229
|
+
# The url can carry a token (userinfo, path or query): #inspect shows
|
|
230
|
+
# its scheme and host.
|
|
231
|
+
def inspect_attributes
|
|
232
|
+
super.map { |name, value| [name, name == 'url' ? inspect_filter_url(value) : value] }
|
|
233
|
+
end
|
|
198
234
|
end
|
|
199
235
|
|
|
200
236
|
class McpSdkServerConfig < Type
|
|
@@ -192,7 +192,13 @@ module ClaudeAgentSDK
|
|
|
192
192
|
:outcome # "success", "error", or "cancelled"
|
|
193
193
|
end
|
|
194
194
|
|
|
195
|
-
# Session state changed system message
|
|
195
|
+
# Session state changed system message.
|
|
196
|
+
#
|
|
197
|
+
# Reaches your code only when you opt in with
|
|
198
|
+
# `CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS=1` in ClaudeAgentOptions#env. The
|
|
199
|
+
# SDK also asks the CLI for these frames on its own behalf (to tell when a
|
|
200
|
+
# query() run is over), but those arrive marked `sdk_host_only` and are
|
|
201
|
+
# dropped before the message stream.
|
|
196
202
|
class SessionStateChangedMessage < SystemMessage
|
|
197
203
|
attr_accessor :uuid, :session_id,
|
|
198
204
|
:state # "idle", "running", or "requires_action"
|
|
@@ -158,7 +158,19 @@ module ClaudeAgentSDK
|
|
|
158
158
|
end
|
|
159
159
|
end
|
|
160
160
|
|
|
161
|
-
# Sandbox settings for isolated command execution
|
|
161
|
+
# Sandbox settings for isolated command execution.
|
|
162
|
+
#
|
|
163
|
+
# +network+ and +filesystem+ take a SandboxNetworkConfig /
|
|
164
|
+
# SandboxFilesystemConfig or a Hash. A Hash may spell the fields of those
|
|
165
|
+
# classes as their attributes (+denied_domains+) or as the CLI does
|
|
166
|
+
# (+deniedDomains+), with Symbol or String keys; any other key is sent as
|
|
167
|
+
# written. The same holds for a Hash given as +sandbox:+ in place of a
|
|
168
|
+
# SandboxSettings.
|
|
169
|
+
#
|
|
170
|
+
# A snake_case key in such a Hash is sent under the CLI's name only when
|
|
171
|
+
# its value has the shape the CLI accepts there (an Array of Strings for
|
|
172
|
+
# +denied_domains+, +true+ or +false+ for +allow_local_binding+, ...).
|
|
173
|
+
# With any other value it is sent as written, and the CLI ignores it.
|
|
162
174
|
class SandboxSettings < Type
|
|
163
175
|
include Type::OptionValue
|
|
164
176
|
|
|
@@ -169,15 +181,15 @@ module ClaudeAgentSDK
|
|
|
169
181
|
:ignore_violations, :enable_weaker_nested_sandbox,
|
|
170
182
|
:enable_weaker_network_isolation, :ripgrep
|
|
171
183
|
|
|
172
|
-
def to_h
|
|
184
|
+
def to_h
|
|
173
185
|
result = {}
|
|
174
186
|
result[:enabled] = @enabled unless @enabled.nil?
|
|
175
187
|
result[:failIfUnavailable] = @fail_if_unavailable unless @fail_if_unavailable.nil?
|
|
176
188
|
result[:autoAllowBashIfSandboxed] = @auto_allow_bash_if_sandboxed unless @auto_allow_bash_if_sandboxed.nil?
|
|
177
189
|
result[:excludedCommands] = @excluded_commands if @excluded_commands
|
|
178
190
|
result[:allowUnsandboxedCommands] = @allow_unsandboxed_commands unless @allow_unsandboxed_commands.nil?
|
|
179
|
-
result[:network] =
|
|
180
|
-
result[:filesystem] =
|
|
191
|
+
result[:network] = SandboxKeys.network(@network) if @network
|
|
192
|
+
result[:filesystem] = SandboxKeys.filesystem(@filesystem) if @filesystem
|
|
181
193
|
result[:ignoreViolations] = @ignore_violations if @ignore_violations
|
|
182
194
|
result[:enableWeakerNestedSandbox] = @enable_weaker_nested_sandbox unless @enable_weaker_nested_sandbox.nil?
|
|
183
195
|
unless @enable_weaker_network_isolation.nil?
|
|
@@ -188,6 +200,176 @@ module ClaudeAgentSDK
|
|
|
188
200
|
end
|
|
189
201
|
end
|
|
190
202
|
|
|
203
|
+
# The spellings a sandbox Hash may use for a field of the three sandbox
|
|
204
|
+
# classes above, mapped to the key their #to_h emits, which is the key the
|
|
205
|
+
# CLI reads. A Hash given as the +sandbox:+ option, or as the network /
|
|
206
|
+
# filesystem of a SandboxSettings, stands for the typed value with the same
|
|
207
|
+
# fields: its keys may be Symbols or Strings, the attribute names
|
|
208
|
+
# (snake_case) or the wire keys (camelCase).
|
|
209
|
+
#
|
|
210
|
+
# Every attribute is listed, also the ones whose name is their wire key. A
|
|
211
|
+
# key that is not listed is sent as written, so a field of the CLI that the
|
|
212
|
+
# typed classes do not model (allowAppleEvents, network.strictAllowlist,
|
|
213
|
+
# filesystem.disabled, ...) still gets through, in the CLI's own spelling.
|
|
214
|
+
# spec/unit/sandbox_hash_keys_spec.rb walks the attributes of the three
|
|
215
|
+
# classes and fails when one of them and these tables disagree.
|
|
216
|
+
#
|
|
217
|
+
# @api private
|
|
218
|
+
module SandboxKeys
|
|
219
|
+
# SandboxSettings attribute => wire key.
|
|
220
|
+
TOP_LEVEL = {
|
|
221
|
+
'enabled' => :enabled,
|
|
222
|
+
'fail_if_unavailable' => :failIfUnavailable,
|
|
223
|
+
'auto_allow_bash_if_sandboxed' => :autoAllowBashIfSandboxed,
|
|
224
|
+
'excluded_commands' => :excludedCommands,
|
|
225
|
+
'allow_unsandboxed_commands' => :allowUnsandboxedCommands,
|
|
226
|
+
'network' => :network,
|
|
227
|
+
'filesystem' => :filesystem,
|
|
228
|
+
'ignore_violations' => :ignoreViolations,
|
|
229
|
+
'enable_weaker_nested_sandbox' => :enableWeakerNestedSandbox,
|
|
230
|
+
'enable_weaker_network_isolation' => :enableWeakerNetworkIsolation,
|
|
231
|
+
'ripgrep' => :ripgrep
|
|
232
|
+
}.freeze
|
|
233
|
+
|
|
234
|
+
# SandboxNetworkConfig attribute => wire key.
|
|
235
|
+
NETWORK = {
|
|
236
|
+
'allowed_domains' => :allowedDomains,
|
|
237
|
+
'denied_domains' => :deniedDomains,
|
|
238
|
+
'allow_managed_domains_only' => :allowManagedDomainsOnly,
|
|
239
|
+
'allow_unix_sockets' => :allowUnixSockets,
|
|
240
|
+
'allow_all_unix_sockets' => :allowAllUnixSockets,
|
|
241
|
+
'allow_local_binding' => :allowLocalBinding,
|
|
242
|
+
'allow_mach_lookup' => :allowMachLookup,
|
|
243
|
+
'http_proxy_port' => :httpProxyPort,
|
|
244
|
+
'socks_proxy_port' => :socksProxyPort
|
|
245
|
+
}.freeze
|
|
246
|
+
|
|
247
|
+
# SandboxFilesystemConfig attribute => wire key.
|
|
248
|
+
FILESYSTEM = {
|
|
249
|
+
'allow_write' => :allowWrite,
|
|
250
|
+
'deny_write' => :denyWrite,
|
|
251
|
+
'deny_read' => :denyRead,
|
|
252
|
+
'allow_read' => :allowRead,
|
|
253
|
+
'allow_managed_read_paths_only' => :allowManagedReadPathsOnly
|
|
254
|
+
}.freeze
|
|
255
|
+
|
|
256
|
+
# Wire key => the kind of value CLI 2.1.287's sandbox schema accepts
|
|
257
|
+
# under it, for every wire key an attribute spells differently (read from
|
|
258
|
+
# the schema in the CLI binary and checked against the running CLI):
|
|
259
|
+
#
|
|
260
|
+
# :boolean true or false
|
|
261
|
+
# :strings an Array of Strings
|
|
262
|
+
# :mach_services an Array of Strings; a "*" only as the last character
|
|
263
|
+
# :port an Integer from 0 to 65535
|
|
264
|
+
# :string_lists a Hash whose values are Arrays of Strings
|
|
265
|
+
#
|
|
266
|
+
# One value outside its kind makes the CLI discard the whole --settings
|
|
267
|
+
# value, the sandbox and the permissions next to it. It lists the error
|
|
268
|
+
# in its get_settings response only, so the SDK is not told. A key in
|
|
269
|
+
# snake_case is one the CLI does not know and ignores, so it is renamed
|
|
270
|
+
# only when its value is of the kind listed here (see #rename). A kind
|
|
271
|
+
# that is too strict leaves a key without effect, as it was before the
|
|
272
|
+
# renaming existed; one that is too loose can cost a session its sandbox.
|
|
273
|
+
#
|
|
274
|
+
# :port is the strict side of what the CLI does. It takes any number it
|
|
275
|
+
# can read as a port, but an Integer too large for that (2**1024 and up,
|
|
276
|
+
# say 10**400) reaches it as a non-finite value and fails its schema.
|
|
277
|
+
SHAPES = {
|
|
278
|
+
boolean: %i[failIfUnavailable autoAllowBashIfSandboxed allowUnsandboxedCommands enableWeakerNestedSandbox
|
|
279
|
+
enableWeakerNetworkIsolation allowManagedDomainsOnly allowAllUnixSockets allowLocalBinding
|
|
280
|
+
allowManagedReadPathsOnly],
|
|
281
|
+
strings: %i[excludedCommands allowedDomains deniedDomains allowUnixSockets
|
|
282
|
+
allowWrite denyWrite denyRead allowRead],
|
|
283
|
+
mach_services: %i[allowMachLookup],
|
|
284
|
+
port: %i[httpProxyPort socksProxyPort],
|
|
285
|
+
string_lists: %i[ignoreViolations]
|
|
286
|
+
}.flat_map { |kind, wire_keys| wire_keys.map { |wire| [wire, kind] } }.to_h.freeze
|
|
287
|
+
|
|
288
|
+
# A Hash +sandbox:+ as the CLI reads it: its known keys under their wire
|
|
289
|
+
# keys, at the top level and inside its network / filesystem, which may
|
|
290
|
+
# each be a Hash or the typed config. Other values are not rewritten:
|
|
291
|
+
# ignore_violations and ripgrep hold structures of the CLI's own.
|
|
292
|
+
def self.normalize(sandbox)
|
|
293
|
+
normalized = rename(sandbox, TOP_LEVEL)
|
|
294
|
+
normalized[:network] = network(normalized[:network]) if normalized.key?(:network)
|
|
295
|
+
normalized[:filesystem] = filesystem(normalized[:filesystem]) if normalized.key?(:filesystem)
|
|
296
|
+
normalized
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# A network section as the CLI reads it: the typed config's #to_h, or a
|
|
300
|
+
# Hash with its known keys renamed. Any other value is returned as it is.
|
|
301
|
+
def self.network(section)
|
|
302
|
+
case section
|
|
303
|
+
when SandboxNetworkConfig then section.to_h
|
|
304
|
+
when Hash then rename(section, NETWORK)
|
|
305
|
+
else section
|
|
306
|
+
end
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
# A filesystem section as the CLI reads it (see .network).
|
|
310
|
+
def self.filesystem(section)
|
|
311
|
+
case section
|
|
312
|
+
when SandboxFilesystemConfig then section.to_h
|
|
313
|
+
when Hash then rename(section, FILESYSTEM)
|
|
314
|
+
else section
|
|
315
|
+
end
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# A known key ends up as the one Symbol #to_h emits, so the spellings of
|
|
319
|
+
# a field (attribute name or wire key, Symbol or String) cannot reach
|
|
320
|
+
# JSON.generate side by side: json 3.x raises on a key given as a Symbol
|
|
321
|
+
# and as a String, 2.x writes it twice. When a Hash carries both
|
|
322
|
+
# spellings of a field the wire spelling wins, whichever comes first;
|
|
323
|
+
# between two keys in the same spelling the later one does.
|
|
324
|
+
#
|
|
325
|
+
# A known key holding nil is left out, as #to_h leaves out a nil
|
|
326
|
+
# attribute. The CLI rejects null under every one of these keys, and
|
|
327
|
+
# when it does it drops the whole --settings value, sandbox included.
|
|
328
|
+
#
|
|
329
|
+
# A key in snake_case is renamed only when the CLI accepts its value
|
|
330
|
+
# under the wire key (SHAPES). Otherwise it is sent as written, which
|
|
331
|
+
# the CLI ignores, as it ignored every snake_case key before: renaming
|
|
332
|
+
# must not be what makes the CLI drop the settings. A key the caller
|
|
333
|
+
# wrote in wire spelling is not looked at; it goes out as it always did.
|
|
334
|
+
def self.rename(hash, table)
|
|
335
|
+
renamed = {}
|
|
336
|
+
wire_spelled = {}
|
|
337
|
+
hash.each do |key, value|
|
|
338
|
+
name = key.to_s
|
|
339
|
+
wire = table[name] || table.each_value.find { |candidate| candidate.name == name }
|
|
340
|
+
next if wire && value.nil?
|
|
341
|
+
|
|
342
|
+
if wire && wire.name == name
|
|
343
|
+
wire_spelled[wire] = true
|
|
344
|
+
renamed[wire] = value
|
|
345
|
+
elsif wire && well_shaped?(wire, value)
|
|
346
|
+
renamed[wire] = value unless wire_spelled.key?(wire)
|
|
347
|
+
else
|
|
348
|
+
renamed[key] = value
|
|
349
|
+
end
|
|
350
|
+
end
|
|
351
|
+
renamed
|
|
352
|
+
end
|
|
353
|
+
|
|
354
|
+
# Whether the CLI accepts +value+ under +wire+. A key without a kind is
|
|
355
|
+
# answered false, the side that cannot cost the sandbox.
|
|
356
|
+
def self.well_shaped?(wire, value)
|
|
357
|
+
case SHAPES[wire]
|
|
358
|
+
when :boolean then [true, false].include?(value)
|
|
359
|
+
when :strings then strings?(value)
|
|
360
|
+
when :mach_services then strings?(value) && value.none? { |name| name.delete_suffix('*').include?('*') }
|
|
361
|
+
when :port then value.is_a?(Integer) && value.between?(0, 65_535)
|
|
362
|
+
when :string_lists then value.is_a?(Hash) && value.each_value.all? { |list| strings?(list) }
|
|
363
|
+
else false
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def self.strings?(value)
|
|
368
|
+
value.is_a?(Array) && value.all?(String)
|
|
369
|
+
end
|
|
370
|
+
private_class_method :rename, :well_shaped?, :strings?
|
|
371
|
+
end
|
|
372
|
+
|
|
191
373
|
# API-side task budget in tokens.
|
|
192
374
|
# When set, the model is made aware of its remaining token budget so it can
|
|
193
375
|
# pace tool use and wrap up before the limit.
|