acp-sdk 0.6.0 → 0.7.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/CHANGELOG.md +13 -0
- data/README.md +3 -1
- data/lib/acp/client_connection.rb +128 -1
- data/lib/acp/request_error.rb +3 -2
- data/lib/acp/transport/stdio.rb +21 -6
- data/lib/acp/version.rb +1 -1
- data/sig/generated/acp/client_connection.rbs +63 -12
- data/sig/generated/acp/request_error.rbs +2 -1
- data/sig/generated/acp/transport/stdio.rbs +13 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 978b245a94b3cd60d3858ee42c3deaf9b687c8a6a1701ce54c684a5e434b2bce
|
|
4
|
+
data.tar.gz: b83adfe811bc7873455f9f81993061e7b2ea66d0a75b78645807fc44e08cd58b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2907a71594882b7afd3ed8fe89b2e9520f139dc1c235a4e43bf836a2f127ae6264ce948d86885577daa6cd320078c6da831f4b8df89225201287276e17710c4e
|
|
7
|
+
data.tar.gz: 9191c0362932d37aaeb55666fb0b892505e448949f13e4173d0d9a0196ccebd16e56324f42db33f6c0d47fbec658bce6aac6e1bec4e4e9d4bdd4d6a1237ffc11
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.7.0](https://github.com/bottrall/acp-sdk/compare/v0.6.0...v0.7.0) (2026-10-05)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* terminal handlers on ACP::ClientConnection ([#77](https://github.com/bottrall/acp-sdk/issues/77)) ([8a42829](https://github.com/bottrall/acp-sdk/commit/8a428295d5cd7c92b56ec1b3ed1c900b485bbf01)), closes [#40](https://github.com/bottrall/acp-sdk/issues/40)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* attach data to method-not-found and invalid-request replies ([#75](https://github.com/bottrall/acp-sdk/issues/75)) ([9bc201c](https://github.com/bottrall/acp-sdk/commit/9bc201c53d86f506bedb5b49183b865cf9b8f4fe)), closes [#29](https://github.com/bottrall/acp-sdk/issues/29)
|
|
14
|
+
* don't send handler exception messages to the peer ([#74](https://github.com/bottrall/acp-sdk/issues/74)) ([af66b48](https://github.com/bottrall/acp-sdk/commit/af66b48cbab4a99244c28a9c6f6a12c9ea23efc7)), closes [#28](https://github.com/bottrall/acp-sdk/issues/28)
|
|
15
|
+
|
|
3
16
|
## [0.6.0](https://github.com/bottrall/acp-sdk/compare/v0.5.0...v0.6.0) (2026-10-04)
|
|
4
17
|
|
|
5
18
|
|
data/README.md
CHANGED
|
@@ -50,6 +50,7 @@ The agent's contract:
|
|
|
50
50
|
|
|
51
51
|
- It defines `new_session`, `prompt` and `cancel`. `load_session`, `list_sessions`, `resume_session`, `close_session` and `delete_session` are required only when `capabilities` advertises `load_session` or `session_capabilities.list`, `.resume`, `.close` or `.delete`, and `authenticate` only when `auth_methods` is non-empty, and `logout` only when `capabilities` advertises `auth.logout`; `start` raises `ArgumentError` when one is advertised but missing, and a method that is not advertised is answered with `-32601`. `change_session_mode` and `change_session_config_option` are optional, since modes and config options are offered per session rather than in `initialize`; `session/set_mode` and `session/set_config_option` are answered with `-32601` when the agent does not define them. `session_created`, if defined, runs right after the `session/new` reply is sent.
|
|
52
52
|
- Each request method returns its response type or an `ACP::RequestError`, which is sent as the error reply. Params that do not match the schema are answered with `-32602` before the agent sees them.
|
|
53
|
+
- A handler that raises is answered with `-32603` and the message `Internal error`. Nothing from the exception reaches the peer — its class and message are logged to the transport's logger instead, since messages can interpolate paths, queries or credentials. Return an `ACP::RequestError` from the handler when the peer should see a real error message.
|
|
53
54
|
- `cancel` runs on the transport's reader thread, so it must return quickly: set a flag and let the prompt notice it.
|
|
54
55
|
- After a cancel, the agent must itself end the turn with `stopReason: cancelled`. `ACP::AgentConnection` does not enforce it.
|
|
55
56
|
- `client.capabilities` is `nil` until the client sends `initialize`.
|
|
@@ -63,7 +64,7 @@ Extensions are `_`-prefixed methods outside the spec. `AgentConnection` takes `e
|
|
|
63
64
|
|
|
64
65
|
## Driving an agent
|
|
65
66
|
|
|
66
|
-
`ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply.
|
|
67
|
+
`ACP::ClientConnection` is the other side of the same transport. Each method takes the request's generated `ACP::Types` object and returns its response type or the agent's `ACP::RequestError`. `connect` sends `initialize`, since Ruby reserves that name for the constructor, and `authenticate` and `logout` send their namesake methods. `session_set_mode` sends `session/set_mode`, and `session_set_config_option` sends `session/set_config_option` and returns the session's complete option list, which replaces any state held from an earlier reply. A handler that raises is answered with `-32603` and the message `Internal error`, and the exception is only logged to the transport's logger.
|
|
67
68
|
|
|
68
69
|
`ACP::AgentProcess.spawn` starts a command with args, env and cwd, wires its stdio to a new `ACP::ClientConnection` and starts it. The block receives the connection and the process handle (`pid`, plus `stderr` when captured), and the child is terminated — after its stdin is closed, with TERM and then KILL if it will not exit — when the block exits, so a raised block cannot leak the process. Without a block, `spawn` returns the process and calling `terminate` is yours. `stderr` is `:inherit` by default, so the child writes to your stderr; pass `:capture` to collect it on the process instead.
|
|
69
70
|
|
|
@@ -105,6 +106,7 @@ connection = ACP::ClientConnection.new(
|
|
|
105
106
|
- `session_prompt` and `session_load` yield the session's updates on the calling thread as they arrive and return once the agent replies. Updates outside those calls, such as the available commands after `session/new`, go to `updates`, which runs on the reader thread and must return quickly. `session_resume` does not take a block: the agent must not replay history on resume, so unlike `session_load` there is nothing to stream. `session_close` and `session_delete` end the session, discarding it on the agent, and return once the agent replies.
|
|
106
107
|
- `permission` answers `session/request_permission` with an `ACP::Types::RequestPermissionResponse` or an `ACP::RequestError`. It runs on its own thread, so it may block while the user decides, and `session_cancel` can be sent meanwhile.
|
|
107
108
|
- `read_text_file` and `write_text_file` answer the agent's `fs/read_text_file` and `fs/write_text_file` with an `ACP::Types::ReadTextFileResponse`, an `ACP::Types::WriteTextFileResponse` or an `ACP::RequestError`. They are routed by the `fs` capabilities `connect` sends: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises a capability that has no handler.
|
|
109
|
+
- `create_terminal`, `terminal_output`, `wait_for_terminal_exit`, `kill_terminal` and `release_terminal` answer the agent's `terminal/create`, `terminal/output`, `terminal/wait_for_exit`, `terminal/kill` and `terminal/release` with the matching `ACP::Types` response or an `ACP::RequestError`. They are routed by the `terminal` capability `connect` sends, the same way as `fs`: a method the client does not advertise is answered with `-32601` without reaching the handler, and `connect` raises `ArgumentError` — before sending `initialize` — when the request advertises `terminal` with any of the five missing. The transport serves each request on its own thread, so `wait_for_terminal_exit` may block without holding up the others.
|
|
108
110
|
|
|
109
111
|
Extensions work the same way in both directions: `extension_requests` and `extension_notifications` are hashes keyed by the raw `_`-prefixed wire name, `ext_request` sends one to the agent and returns its result as-is, and `ext_notify` sends an extension notification. A handler name without the `_` prefix raises `ArgumentError`, and an extension request nothing answers gets `-32601`.
|
|
110
112
|
|
|
@@ -5,8 +5,15 @@ class ACP::ClientConnection
|
|
|
5
5
|
# @rbs @permission: ACP::ClientConnection::_PermissionHandler
|
|
6
6
|
# @rbs @read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
7
7
|
# @rbs @write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
8
|
+
# @rbs @create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
9
|
+
# @rbs @terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
10
|
+
# @rbs @wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
|
|
11
|
+
# (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
|
|
12
|
+
# @rbs @kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
|
|
13
|
+
# @rbs @release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
8
14
|
# @rbs @updates: ACP::ClientConnection::_UpdateHandler
|
|
9
15
|
# @rbs @fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
16
|
+
# @rbs @terminal: bool?
|
|
10
17
|
# @rbs @extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
11
18
|
# @rbs @extension_notifications: Hash[String, ^(untyped) -> void]
|
|
12
19
|
# @rbs @lock: Thread::Mutex
|
|
@@ -26,6 +33,12 @@ class ACP::ClientConnection
|
|
|
26
33
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
27
34
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
28
35
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
36
|
+
# @rbs create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
37
|
+
# @rbs terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
38
|
+
# @rbs wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
|
|
39
|
+
# (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
|
|
40
|
+
# @rbs kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
|
|
41
|
+
# @rbs release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
29
42
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
30
43
|
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
31
44
|
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
@@ -36,6 +49,11 @@ class ACP::ClientConnection
|
|
|
36
49
|
permission:,
|
|
37
50
|
read_text_file: nil,
|
|
38
51
|
write_text_file: nil,
|
|
52
|
+
create_terminal: nil,
|
|
53
|
+
terminal_output: nil,
|
|
54
|
+
wait_for_terminal_exit: nil,
|
|
55
|
+
kill_terminal: nil,
|
|
56
|
+
release_terminal: nil,
|
|
39
57
|
updates: IGNORE,
|
|
40
58
|
extension_requests: {},
|
|
41
59
|
extension_notifications: {},
|
|
@@ -45,8 +63,14 @@ class ACP::ClientConnection
|
|
|
45
63
|
@permission = permission
|
|
46
64
|
@read_text_file = read_text_file
|
|
47
65
|
@write_text_file = write_text_file
|
|
66
|
+
@create_terminal = create_terminal
|
|
67
|
+
@terminal_output = terminal_output
|
|
68
|
+
@wait_for_terminal_exit = wait_for_terminal_exit
|
|
69
|
+
@kill_terminal = kill_terminal
|
|
70
|
+
@release_terminal = release_terminal
|
|
48
71
|
@updates = updates
|
|
49
72
|
@fs_capabilities = nil
|
|
73
|
+
@terminal = nil
|
|
50
74
|
@extension_requests = extension_requests
|
|
51
75
|
@extension_notifications = extension_notifications
|
|
52
76
|
extension_requests.each_key { |name| ACP::Extensions.validate_name(name) }
|
|
@@ -63,7 +87,12 @@ class ACP::ClientConnection
|
|
|
63
87
|
requests: {
|
|
64
88
|
'session/request_permission' => method(:request_permission),
|
|
65
89
|
'fs/read_text_file' => method(:read_text_file),
|
|
66
|
-
'fs/write_text_file' => method(:write_text_file)
|
|
90
|
+
'fs/write_text_file' => method(:write_text_file),
|
|
91
|
+
'terminal/create' => method(:create_terminal),
|
|
92
|
+
'terminal/output' => method(:terminal_output),
|
|
93
|
+
'terminal/wait_for_exit' => method(:wait_for_terminal_exit),
|
|
94
|
+
'terminal/kill' => method(:kill_terminal),
|
|
95
|
+
'terminal/release' => method(:release_terminal)
|
|
67
96
|
}.merge(@extension_requests),
|
|
68
97
|
notifications: { 'session/update' => method(:dispatch) }.merge(@extension_notifications)
|
|
69
98
|
)
|
|
@@ -81,7 +110,14 @@ class ACP::ClientConnection
|
|
|
81
110
|
raise ArgumentError, "initialize advertises fs methods no handler serves: #{unserved.join(', ')}"
|
|
82
111
|
end
|
|
83
112
|
|
|
113
|
+
terminal = request.client_capabilities&.terminal
|
|
114
|
+
unserved = unserved_terminal_methods(terminal)
|
|
115
|
+
unless unserved.empty?
|
|
116
|
+
raise ArgumentError, "initialize advertises terminal methods no handler serves: #{unserved.join(', ')}"
|
|
117
|
+
end
|
|
118
|
+
|
|
84
119
|
@fs_capabilities = capabilities
|
|
120
|
+
@terminal = terminal
|
|
85
121
|
parse(ACP::Types::InitializeResponse, @transport.request('initialize', request.to_h)).then do |response|
|
|
86
122
|
next response if response.is_a?(ACP::RequestError)
|
|
87
123
|
|
|
@@ -349,6 +385,82 @@ class ACP::ClientConnection
|
|
|
349
385
|
handler.call(request)
|
|
350
386
|
end
|
|
351
387
|
|
|
388
|
+
# The terminal routes are registered at start, before connect records the
|
|
389
|
+
# advertised capability, so each one answers -32601 until then.
|
|
390
|
+
#
|
|
391
|
+
# @rbs params: untyped
|
|
392
|
+
# @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
393
|
+
def create_terminal(params)
|
|
394
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
395
|
+
|
|
396
|
+
request = ACP::Types::CreateTerminalRequest.from_h(params)
|
|
397
|
+
rescue KeyError, TypeError, NoMethodError
|
|
398
|
+
ACP::RequestError.invalid_params
|
|
399
|
+
else
|
|
400
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
401
|
+
handler = @create_terminal #: ^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
402
|
+
handler.call(request)
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
# @rbs params: untyped
|
|
406
|
+
# @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
407
|
+
def terminal_output(params)
|
|
408
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
409
|
+
|
|
410
|
+
request = ACP::Types::TerminalOutputRequest.from_h(params)
|
|
411
|
+
rescue KeyError, TypeError, NoMethodError
|
|
412
|
+
ACP::RequestError.invalid_params
|
|
413
|
+
else
|
|
414
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
415
|
+
handler = @terminal_output #: ^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
416
|
+
handler.call(request)
|
|
417
|
+
end
|
|
418
|
+
|
|
419
|
+
# The handler may block for as long as the command runs, so the transport
|
|
420
|
+
# must serve each request on its own thread for this not to hold up others.
|
|
421
|
+
#
|
|
422
|
+
# @rbs params: untyped
|
|
423
|
+
# @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
424
|
+
def wait_for_terminal_exit(params)
|
|
425
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
426
|
+
|
|
427
|
+
request = ACP::Types::WaitForTerminalExitRequest.from_h(params)
|
|
428
|
+
rescue KeyError, TypeError, NoMethodError
|
|
429
|
+
ACP::RequestError.invalid_params
|
|
430
|
+
else
|
|
431
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
432
|
+
handler = @wait_for_terminal_exit #: ^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
433
|
+
handler.call(request)
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# @rbs params: untyped
|
|
437
|
+
# @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
438
|
+
def kill_terminal(params)
|
|
439
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
440
|
+
|
|
441
|
+
request = ACP::Types::KillTerminalRequest.from_h(params)
|
|
442
|
+
rescue KeyError, TypeError, NoMethodError
|
|
443
|
+
ACP::RequestError.invalid_params
|
|
444
|
+
else
|
|
445
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
446
|
+
handler = @kill_terminal #: ^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
447
|
+
handler.call(request)
|
|
448
|
+
end
|
|
449
|
+
|
|
450
|
+
# @rbs params: untyped
|
|
451
|
+
# @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
452
|
+
def release_terminal(params)
|
|
453
|
+
return ACP::RequestError.unadvertised('terminal') unless @terminal
|
|
454
|
+
|
|
455
|
+
request = ACP::Types::ReleaseTerminalRequest.from_h(params)
|
|
456
|
+
rescue KeyError, TypeError, NoMethodError
|
|
457
|
+
ACP::RequestError.invalid_params
|
|
458
|
+
else
|
|
459
|
+
# Safe: connect refuses an advertised capability no handler serves.
|
|
460
|
+
handler = @release_terminal #: ^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
461
|
+
handler.call(request)
|
|
462
|
+
end
|
|
463
|
+
|
|
352
464
|
# The fs capabilities initialize advertises that no injected handler serves.
|
|
353
465
|
#
|
|
354
466
|
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
@@ -359,4 +471,19 @@ class ACP::ClientConnection
|
|
|
359
471
|
'fs.writeTextFile' => capabilities&.write_text_file && !@write_text_file
|
|
360
472
|
}.select { |_, unserved| unserved }.keys
|
|
361
473
|
end
|
|
474
|
+
|
|
475
|
+
# The terminal capabilities initialize advertises that no injected handler
|
|
476
|
+
# serves.
|
|
477
|
+
#
|
|
478
|
+
# @rbs capabilities: bool?
|
|
479
|
+
# @rbs return: Array[String]
|
|
480
|
+
def unserved_terminal_methods(capabilities)
|
|
481
|
+
{
|
|
482
|
+
'terminal.create' => capabilities && !@create_terminal,
|
|
483
|
+
'terminal.output' => capabilities && !@terminal_output,
|
|
484
|
+
'terminal.wait_for_exit' => capabilities && !@wait_for_terminal_exit,
|
|
485
|
+
'terminal.kill' => capabilities && !@kill_terminal,
|
|
486
|
+
'terminal.release' => capabilities && !@release_terminal
|
|
487
|
+
}.select { |_, unserved| unserved }.keys
|
|
488
|
+
end
|
|
362
489
|
end
|
data/lib/acp/request_error.rb
CHANGED
|
@@ -48,9 +48,10 @@ class ACP::RequestError
|
|
|
48
48
|
new(code: INVALID_REQUEST, message: 'Invalid request')
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
+
# @rbs method: String?
|
|
51
52
|
# @rbs return: ACP::RequestError
|
|
52
|
-
def self.method_not_found
|
|
53
|
-
new(code: METHOD_NOT_FOUND, message: 'Method not found')
|
|
53
|
+
def self.method_not_found(method = nil)
|
|
54
|
+
new(code: METHOD_NOT_FOUND, message: 'Method not found', data: method && { method: method })
|
|
54
55
|
end
|
|
55
56
|
|
|
56
57
|
# @rbs return: ACP::RequestError
|
data/lib/acp/transport/stdio.rb
CHANGED
|
@@ -92,7 +92,7 @@ class ACP::Transport::Stdio
|
|
|
92
92
|
method = message['method']
|
|
93
93
|
error = message['error']
|
|
94
94
|
if method.is_a?(String) && message.key?('id')
|
|
95
|
-
serve(requests[method], message['id'], message['params'])
|
|
95
|
+
serve(requests[method], message['id'], message['params'], method)
|
|
96
96
|
elsif method.is_a?(String)
|
|
97
97
|
# Run on the reader thread so notifications (session/update) keep their
|
|
98
98
|
# order; a JSON-RPC notification has no reply to carry a handler error.
|
|
@@ -104,10 +104,21 @@ class ACP::Transport::Stdio
|
|
|
104
104
|
code, text, data = error.values_at('code', 'message', 'data')
|
|
105
105
|
settle(message['id'], ACP::RequestError.new(code:, message: text, data:))
|
|
106
106
|
else
|
|
107
|
-
reply(
|
|
107
|
+
reply(detectable_id(message), ACP::RequestError.invalid_request)
|
|
108
108
|
end
|
|
109
109
|
end
|
|
110
110
|
|
|
111
|
+
# JSON-RPC 2.0 says an error reply echoes the request id whenever it can be
|
|
112
|
+
# detected; ids outside the spec's String/Number/Null types are not, so
|
|
113
|
+
# those replies keep id null.
|
|
114
|
+
#
|
|
115
|
+
# @rbs message: Hash[String, untyped]
|
|
116
|
+
# @rbs return: untyped
|
|
117
|
+
def detectable_id(message)
|
|
118
|
+
id = message['id']
|
|
119
|
+
id.is_a?(String) || id.is_a?(Numeric) ? id : nil
|
|
120
|
+
end
|
|
121
|
+
|
|
111
122
|
# @rbs line: String
|
|
112
123
|
# @rbs return: Hash[String, untyped] | ACP::RequestError
|
|
113
124
|
def parse(line)
|
|
@@ -122,10 +133,11 @@ class ACP::Transport::Stdio
|
|
|
122
133
|
# @rbs handler: _Handler?
|
|
123
134
|
# @rbs id: untyped
|
|
124
135
|
# @rbs params: untyped
|
|
136
|
+
# @rbs method: String
|
|
125
137
|
# @rbs return: void
|
|
126
|
-
def serve(handler, id, params)
|
|
138
|
+
def serve(handler, id, params, method)
|
|
127
139
|
Thread.new do
|
|
128
|
-
outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found
|
|
140
|
+
outcome = handler ? invoke(handler, params) : ACP::RequestError.method_not_found(method)
|
|
129
141
|
case outcome
|
|
130
142
|
when ACP::Transport::Reply
|
|
131
143
|
reply(id, outcome.result)
|
|
@@ -149,7 +161,9 @@ class ACP::Transport::Stdio
|
|
|
149
161
|
end
|
|
150
162
|
|
|
151
163
|
# Handlers are application code at the protocol boundary: an exception
|
|
152
|
-
# becomes an error response instead of killing the thread serving it.
|
|
164
|
+
# becomes an error response instead of killing the thread serving it. The
|
|
165
|
+
# exception is only logged, not sent: its message can carry paths, SQL or
|
|
166
|
+
# anything else the handler interpolated, which must not reach the peer.
|
|
153
167
|
#
|
|
154
168
|
# @rbs handler: _Handler
|
|
155
169
|
# @rbs params: untyped
|
|
@@ -157,7 +171,8 @@ class ACP::Transport::Stdio
|
|
|
157
171
|
def invoke(handler, params)
|
|
158
172
|
handler.call(params)
|
|
159
173
|
rescue StandardError => e
|
|
160
|
-
|
|
174
|
+
@logger.error("request handler raised #{e.class}: #{e.message}")
|
|
175
|
+
ACP::RequestError.internal_error
|
|
161
176
|
end
|
|
162
177
|
|
|
163
178
|
# The success arm is whatever the handler returned, which the transport only
|
data/lib/acp/version.rb
CHANGED
|
@@ -1,29 +1,41 @@
|
|
|
1
1
|
# Generated from lib/acp/client_connection.rb with RBS::Inline
|
|
2
2
|
|
|
3
3
|
class ACP::ClientConnection
|
|
4
|
-
@
|
|
4
|
+
@transport: ACP::AgentConnection::_Transport
|
|
5
5
|
|
|
6
|
-
@
|
|
6
|
+
@permission: ACP::ClientConnection::_PermissionHandler
|
|
7
7
|
|
|
8
|
-
@
|
|
8
|
+
@read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
9
9
|
|
|
10
|
-
@
|
|
10
|
+
@write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
11
11
|
|
|
12
|
-
@
|
|
12
|
+
@create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
13
13
|
|
|
14
|
-
@
|
|
14
|
+
@terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
15
15
|
|
|
16
|
-
@
|
|
16
|
+
@wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
|
|
17
|
+
|
|
18
|
+
@kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
|
|
19
|
+
|
|
20
|
+
@release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
17
21
|
|
|
18
22
|
@updates: ACP::ClientConnection::_UpdateHandler
|
|
19
23
|
|
|
20
|
-
@
|
|
24
|
+
@fs_capabilities: ACP::Types::FileSystemCapabilities?
|
|
21
25
|
|
|
22
|
-
@
|
|
26
|
+
@terminal: bool?
|
|
23
27
|
|
|
24
|
-
@
|
|
28
|
+
@extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
25
29
|
|
|
26
|
-
@
|
|
30
|
+
@extension_notifications: Hash[String, ^(untyped) -> void]
|
|
31
|
+
|
|
32
|
+
@lock: Thread::Mutex
|
|
33
|
+
|
|
34
|
+
@streams: Hash[String, Thread::Queue]
|
|
35
|
+
|
|
36
|
+
@pending_permissions: Hash[String, Array[Thread::Queue]]
|
|
37
|
+
|
|
38
|
+
@logger: ACP::Transport::_Logger
|
|
27
39
|
|
|
28
40
|
PROTOCOL_VERSION: Integer
|
|
29
41
|
|
|
@@ -35,12 +47,18 @@ class ACP::ClientConnection
|
|
|
35
47
|
# @rbs permission: ACP::ClientConnection::_PermissionHandler
|
|
36
48
|
# @rbs read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?
|
|
37
49
|
# @rbs write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?
|
|
50
|
+
# @rbs create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?
|
|
51
|
+
# @rbs terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?
|
|
52
|
+
# @rbs wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) ->
|
|
53
|
+
# (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?
|
|
54
|
+
# @rbs kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?
|
|
55
|
+
# @rbs release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?
|
|
38
56
|
# @rbs updates: ACP::ClientConnection::_UpdateHandler
|
|
39
57
|
# @rbs extension_requests: Hash[String, ^(untyped) -> untyped]
|
|
40
58
|
# @rbs extension_notifications: Hash[String, ^(untyped) -> void]
|
|
41
59
|
# @rbs logger: ACP::Transport::_Logger
|
|
42
60
|
# @rbs return: void
|
|
43
|
-
def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
|
|
61
|
+
def initialize: (transport: ACP::AgentConnection::_Transport, permission: ACP::ClientConnection::_PermissionHandler, ?read_text_file: (^(ACP::Types::ReadTextFileRequest) -> (ACP::Types::ReadTextFileResponse | ACP::RequestError))?, ?write_text_file: (^(ACP::Types::WriteTextFileRequest) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError))?, ?create_terminal: (^(ACP::Types::CreateTerminalRequest) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError))?, ?terminal_output: (^(ACP::Types::TerminalOutputRequest) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError))?, ?wait_for_terminal_exit: (^(ACP::Types::WaitForTerminalExitRequest) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError))?, ?kill_terminal: (^(ACP::Types::KillTerminalRequest) -> (ACP::Types::KillTerminalResponse | ACP::RequestError))?, ?release_terminal: (^(ACP::Types::ReleaseTerminalRequest) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError))?, ?updates: ACP::ClientConnection::_UpdateHandler, ?extension_requests: Hash[String, ^(untyped) -> untyped], ?extension_notifications: Hash[String, ^(untyped) -> void], ?logger: ACP::Transport::_Logger) -> void
|
|
44
62
|
|
|
45
63
|
# @rbs return: Thread
|
|
46
64
|
def start: () -> Thread
|
|
@@ -182,9 +200,42 @@ class ACP::ClientConnection
|
|
|
182
200
|
# @rbs return: (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
183
201
|
def write_text_file: (untyped params) -> (ACP::Types::WriteTextFileResponse | ACP::RequestError)
|
|
184
202
|
|
|
203
|
+
# The terminal routes are registered at start, before connect records the
|
|
204
|
+
# advertised capability, so each one answers -32601 until then.
|
|
205
|
+
#
|
|
206
|
+
# @rbs params: untyped
|
|
207
|
+
# @rbs return: (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
208
|
+
def create_terminal: (untyped params) -> (ACP::Types::CreateTerminalResponse | ACP::RequestError)
|
|
209
|
+
|
|
210
|
+
# @rbs params: untyped
|
|
211
|
+
# @rbs return: (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
212
|
+
def terminal_output: (untyped params) -> (ACP::Types::TerminalOutputResponse | ACP::RequestError)
|
|
213
|
+
|
|
214
|
+
# The handler may block for as long as the command runs, so the transport
|
|
215
|
+
# must serve each request on its own thread for this not to hold up others.
|
|
216
|
+
#
|
|
217
|
+
# @rbs params: untyped
|
|
218
|
+
# @rbs return: (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
219
|
+
def wait_for_terminal_exit: (untyped params) -> (ACP::Types::WaitForTerminalExitResponse | ACP::RequestError)
|
|
220
|
+
|
|
221
|
+
# @rbs params: untyped
|
|
222
|
+
# @rbs return: (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
223
|
+
def kill_terminal: (untyped params) -> (ACP::Types::KillTerminalResponse | ACP::RequestError)
|
|
224
|
+
|
|
225
|
+
# @rbs params: untyped
|
|
226
|
+
# @rbs return: (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
227
|
+
def release_terminal: (untyped params) -> (ACP::Types::ReleaseTerminalResponse | ACP::RequestError)
|
|
228
|
+
|
|
185
229
|
# The fs capabilities initialize advertises that no injected handler serves.
|
|
186
230
|
#
|
|
187
231
|
# @rbs capabilities: ACP::Types::FileSystemCapabilities?
|
|
188
232
|
# @rbs return: Array[String]
|
|
189
233
|
def unserved_fs_methods: (ACP::Types::FileSystemCapabilities? capabilities) -> Array[String]
|
|
234
|
+
|
|
235
|
+
# The terminal capabilities initialize advertises that no injected handler
|
|
236
|
+
# serves.
|
|
237
|
+
#
|
|
238
|
+
# @rbs capabilities: bool?
|
|
239
|
+
# @rbs return: Array[String]
|
|
240
|
+
def unserved_terminal_methods: (bool? capabilities) -> Array[String]
|
|
190
241
|
end
|
|
@@ -44,8 +44,9 @@ class ACP::RequestError
|
|
|
44
44
|
# @rbs return: ACP::RequestError
|
|
45
45
|
def self.invalid_request: () -> ACP::RequestError
|
|
46
46
|
|
|
47
|
+
# @rbs method: String?
|
|
47
48
|
# @rbs return: ACP::RequestError
|
|
48
|
-
def self.method_not_found: () -> ACP::RequestError
|
|
49
|
+
def self.method_not_found: (?String? method) -> ACP::RequestError
|
|
49
50
|
|
|
50
51
|
# @rbs return: ACP::RequestError
|
|
51
52
|
def self.invalid_params: () -> ACP::RequestError
|
|
@@ -62,6 +62,14 @@ class ACP::Transport::Stdio
|
|
|
62
62
|
# @rbs return: void
|
|
63
63
|
def route: (Hash[String, untyped] message, Hash[String, _Handler] requests, Hash[String, _NotificationHandler] notifications) -> void
|
|
64
64
|
|
|
65
|
+
# JSON-RPC 2.0 says an error reply echoes the request id whenever it can be
|
|
66
|
+
# detected; ids outside the spec's String/Number/Null types are not, so
|
|
67
|
+
# those replies keep id null.
|
|
68
|
+
#
|
|
69
|
+
# @rbs message: Hash[String, untyped]
|
|
70
|
+
# @rbs return: untyped
|
|
71
|
+
def detectable_id: (Hash[String, untyped] message) -> untyped
|
|
72
|
+
|
|
65
73
|
# @rbs line: String
|
|
66
74
|
# @rbs return: Hash[String, untyped] | ACP::RequestError
|
|
67
75
|
def parse: (String line) -> (Hash[String, untyped] | ACP::RequestError)
|
|
@@ -69,8 +77,9 @@ class ACP::Transport::Stdio
|
|
|
69
77
|
# @rbs handler: _Handler?
|
|
70
78
|
# @rbs id: untyped
|
|
71
79
|
# @rbs params: untyped
|
|
80
|
+
# @rbs method: String
|
|
72
81
|
# @rbs return: void
|
|
73
|
-
def serve: (_Handler? handler, untyped id, untyped params) -> void
|
|
82
|
+
def serve: (_Handler? handler, untyped id, untyped params, String method) -> void
|
|
74
83
|
|
|
75
84
|
# For code with no reply to carry an error: a notification handler, or a
|
|
76
85
|
# Reply's after once the reply is on the wire. An exception must not kill
|
|
@@ -82,7 +91,9 @@ class ACP::Transport::Stdio
|
|
|
82
91
|
def quietly: (String label) { () -> void } -> void
|
|
83
92
|
|
|
84
93
|
# Handlers are application code at the protocol boundary: an exception
|
|
85
|
-
# becomes an error response instead of killing the thread serving it.
|
|
94
|
+
# becomes an error response instead of killing the thread serving it. The
|
|
95
|
+
# exception is only logged, not sent: its message can carry paths, SQL or
|
|
96
|
+
# anything else the handler interpolated, which must not reach the peer.
|
|
86
97
|
#
|
|
87
98
|
# @rbs handler: _Handler
|
|
88
99
|
# @rbs params: untyped
|