ace-hitl 0.11.0 → 0.11.1
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 +12 -0
- data/docs/usage.md +6 -1
- data/lib/ace/hitl/lifecycle/otp_vault.rb +8 -2
- data/lib/ace/hitl/lifecycle/service.rb +58 -17
- data/lib/ace/hitl/lifecycle/store.rb +17 -0
- data/lib/ace/hitl/providers/lab/daemon_binding.rb +1 -1
- data/lib/ace/hitl/providers/lab.rb +1 -1
- data/lib/ace/hitl/providers/providers.rb +1 -9
- data/lib/ace/hitl/version.rb +1 -1
- data/lib/ace/hitl.rb +1 -0
- metadata +17 -5
- data/lib/ace/hitl/providers/errors.rb +0 -24
- data/lib/ace/hitl/providers/ref.rb +0 -62
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2529e6b24197617c3554da5d1ff7348b19681769562259b05f6d88f469527cf1
|
|
4
|
+
data.tar.gz: 76b205cac4da0c4d7a34f391ebf569b8c997af87d47edf8b607d602727f82582
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 977f6398c3f1e6a10e198c267c9b65af6f6d3ff0a2e668c3e80e4a0ad79a0fc332b9f03081fd0f4bfcd7f995a8c6b3458d5afc1bb39063dafaa4516f2b51d04c
|
|
7
|
+
data.tar.gz: be81c083da2b5af0a5c97646b621ec2b71a6950025e0da4a608c25a2d28142ec67bba8dab032ccf06c004c3021d1e3d660a2c384a58f4925f4b4690895ea53cb
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.11.1] - 2026-10-04
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- Enforce the persisted OTP challenge deadline at locked consumption, including vault reads that cross expiry. Expired secrets are discarded without a success receipt; memory retention is bounded by both vault TTL and challenge expiry.
|
|
14
|
+
|
|
15
|
+
- Preserve the authenticated service endpoint when another HITL startup is refused. Serialize listener ownership and remove only the socket acquired by the stopping invocation; stale recovery requires a protected service-owned socket.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- Load shared provider references, results, and errors from `ace-hitl-contract`; retain provider registration and locked assignment authority in HITL.
|
|
20
|
+
|
|
10
21
|
## [0.11.0] - 2026-10-04
|
|
11
22
|
|
|
12
23
|
### Added
|
|
@@ -17,6 +28,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
17
28
|
- **OTP exact-operation challenges**: an `otp` request exists only with its non-secret publisher evidence (`{operation, result_ref, input_digest, expires_at}`, bounded to 24h); consumption requires the challenge's authorized operation; duplicate consumes replay the receipt WITHOUT the secret bytes. OTP effects are forbidden (the authorized operation consumes the OTP transiently). The service holds OTP bytes in a bounded, TTL-bound memory vault (`OtpVault::MemoryVault`) — secret bytes persist nowhere, transfer exactly once, and a restart deliberately loses the challenge (prompt again).
|
|
18
29
|
|
|
19
30
|
### Changed
|
|
31
|
+
- **Hardened privileged lifecycle boundary handling**: boundary-facing provider flows no longer trust ambient test fixtures and the lab-side authorizer validates the same privileged transition rules the service enforces.
|
|
20
32
|
- **BREAKING (pre-1.0, ADR-024)**: `ace-hitl ask/deliver/consume/cancel/pending/states/duty` run through the boundary client — the CLI no longer touches shared store files directly, and broker operations no longer require root (the configured transport identity from the grants document does). The admin binding bypass is removed: EVERY request validates against its live attempt. The `LAB_ATTEMPT_ID` environment default is gone (environment is never attempt authority); `--attempt` is required, and binding requires `--assignment` (managed) or `--work` (legacy path, kept until vs2 switches consumers). Answers and store files are service-owned `0600` — requesters receive bytes over the authenticated boundary, not through file ownership. The multi-UID acceptance fixture (`test/edge/lifecycle/`) proves requester isolation, protected ownership, forged-identity rejection, and one-terminal-transition races under real distinct OS accounts (requires root; CI records the run as advisory evidence).
|
|
21
33
|
|
|
22
34
|
|
data/docs/usage.md
CHANGED
|
@@ -251,7 +251,12 @@ A consume timeout bounds ONLY the local wait — the request stays
|
|
|
251
251
|
pending and answerable. OTP consumption requires the challenge's
|
|
252
252
|
authorized operation (`--operation <name>`); the secret transfers
|
|
253
253
|
exactly once and a consumed retry replays the receipt WITHOUT the
|
|
254
|
-
bytes.
|
|
254
|
+
bytes. The persisted challenge deadline is checked at the locked handoff;
|
|
255
|
+
consumption at or after that deadline refuses with an expired error and
|
|
256
|
+
discards the pending secret. Memory retention ends at the earlier of the
|
|
257
|
+
challenge deadline and the vault TTL. Delivery, duplicate delivery, waits,
|
|
258
|
+
and service restarts cannot extend the authorized window. Request a fresh
|
|
259
|
+
authorized challenge after expiry. Cancel is the ONLY way to abandon a request, requester-only;
|
|
255
260
|
it records `cancelled_by` and the `reason` in the public projection,
|
|
256
261
|
and late answers/consumes fail closed against the committed receipt.
|
|
257
262
|
|
|
@@ -90,7 +90,10 @@ module Ace
|
|
|
90
90
|
def write(_store, value, answer)
|
|
91
91
|
key = key_for(value)
|
|
92
92
|
@entries.shift while @entries.length >= @max_entries
|
|
93
|
-
|
|
93
|
+
retention_deadline = @clock.now.to_i + @ttl_seconds
|
|
94
|
+
challenge_deadline = value.dig("otp", "expires_at")
|
|
95
|
+
deadline = challenge_deadline ? [retention_deadline, Integer(challenge_deadline)].min : retention_deadline
|
|
96
|
+
@entries[key] = Entry.new(answer.dup.freeze, deadline)
|
|
94
97
|
nil
|
|
95
98
|
end
|
|
96
99
|
|
|
@@ -101,7 +104,10 @@ module Ace
|
|
|
101
104
|
def read(store, value)
|
|
102
105
|
entry = @entries[key_for(value)]
|
|
103
106
|
raise MissError, "no OTP is pending for this request" unless entry
|
|
104
|
-
|
|
107
|
+
if entry.expires_at <= @clock.now.to_i
|
|
108
|
+
@entries.delete(key_for(value))
|
|
109
|
+
raise ExpiredError, "the OTP challenge has expired; request a new one"
|
|
110
|
+
end
|
|
105
111
|
|
|
106
112
|
@entries.delete(key_for(value))
|
|
107
113
|
entry.answer.dup
|
|
@@ -50,6 +50,8 @@ module Ace
|
|
|
50
50
|
@connections_mutex = Mutex.new
|
|
51
51
|
@stopping = false
|
|
52
52
|
@server = nil
|
|
53
|
+
@endpoint_identity = nil
|
|
54
|
+
@endpoint_lock = nil
|
|
53
55
|
end
|
|
54
56
|
|
|
55
57
|
# Serve until #stop. Installs TERM/INT handlers for a clean
|
|
@@ -125,9 +127,10 @@ module Ace
|
|
|
125
127
|
def prepare!
|
|
126
128
|
begin
|
|
127
129
|
verify_socket_directory!
|
|
128
|
-
|
|
129
|
-
|
|
130
|
+
acquire_endpoint_lock!
|
|
131
|
+
recover_stale_endpoint!
|
|
130
132
|
@server = UNIXServer.open(@socket_path)
|
|
133
|
+
@endpoint_identity = endpoint_identity
|
|
131
134
|
rescue SystemCallError, IOError => e
|
|
132
135
|
raise TransportError, "the HITL boundary endpoint is not usable (#{e.class}: #{e.message})"
|
|
133
136
|
end
|
|
@@ -148,8 +151,20 @@ module Ace
|
|
|
148
151
|
end
|
|
149
152
|
|
|
150
153
|
def shutdown!
|
|
151
|
-
|
|
152
|
-
|
|
154
|
+
begin
|
|
155
|
+
@server&.close
|
|
156
|
+
rescue SystemCallError, IOError
|
|
157
|
+
nil
|
|
158
|
+
end
|
|
159
|
+
begin
|
|
160
|
+
File.unlink(@socket_path) if @endpoint_identity && endpoint_identity == @endpoint_identity
|
|
161
|
+
rescue Errno::ENOENT
|
|
162
|
+
nil
|
|
163
|
+
ensure
|
|
164
|
+
@endpoint_identity = nil
|
|
165
|
+
@endpoint_lock&.close
|
|
166
|
+
@endpoint_lock = nil
|
|
167
|
+
end
|
|
153
168
|
log("stopped")
|
|
154
169
|
end
|
|
155
170
|
|
|
@@ -229,27 +244,53 @@ module Ace
|
|
|
229
244
|
->(limit) { answer[0, limit] }
|
|
230
245
|
end
|
|
231
246
|
|
|
232
|
-
#
|
|
233
|
-
#
|
|
234
|
-
#
|
|
235
|
-
|
|
236
|
-
|
|
247
|
+
# Keep one stable lock inode for the endpoint. Holding the lock
|
|
248
|
+
# through shutdown prevents simultaneous starters from recovering
|
|
249
|
+
# or replacing a socket another invocation is still acquiring.
|
|
250
|
+
# Never unlink the lock file: waiters must all use the same inode.
|
|
251
|
+
def acquire_endpoint_lock!
|
|
252
|
+
@endpoint_lock = File.open("#{@socket_path}.lock", File::RDWR | File::CREAT | File::NOFOLLOW, 0o600)
|
|
253
|
+
stat = @endpoint_lock.stat
|
|
254
|
+
unless stat.file? && stat.uid == Process.uid && stat.nlink == 1 && (stat.mode & 0o077).zero?
|
|
255
|
+
raise TransportError, "HITL boundary endpoint lock is not protected"
|
|
256
|
+
end
|
|
257
|
+
return if @endpoint_lock.flock(File::LOCK_EX | File::LOCK_NB)
|
|
258
|
+
|
|
259
|
+
raise TransportError,
|
|
260
|
+
"a HITL boundary service is already serving #{@socket_path}; stop it before starting another"
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def endpoint_identity
|
|
264
|
+
stat = File.lstat(@socket_path)
|
|
265
|
+
[stat.dev, stat.ino] if stat.socket?
|
|
266
|
+
rescue Errno::ENOENT
|
|
267
|
+
nil
|
|
268
|
+
end
|
|
237
269
|
|
|
270
|
+
# Only an owned socket whose connection is explicitly refused is
|
|
271
|
+
# stale. Permission, type and other probe failures cannot grant
|
|
272
|
+
# authority to delete an endpoint.
|
|
273
|
+
def recover_stale_endpoint!
|
|
274
|
+
stat = File.lstat(@socket_path)
|
|
275
|
+
unless stat.socket? && stat.uid == Process.uid && (stat.mode & 0o002).zero?
|
|
276
|
+
raise TransportError, "HITL boundary endpoint is not a protected service-owned socket"
|
|
277
|
+
end
|
|
278
|
+
identity = [stat.dev, stat.ino]
|
|
238
279
|
begin
|
|
239
280
|
probe = UNIXSocket.open(@socket_path)
|
|
240
|
-
rescue
|
|
281
|
+
rescue Errno::ECONNREFUSED
|
|
282
|
+
unless endpoint_identity == identity
|
|
283
|
+
raise TransportError, "HITL boundary endpoint changed during stale socket recovery"
|
|
284
|
+
end
|
|
285
|
+
File.unlink(@socket_path)
|
|
241
286
|
return
|
|
242
|
-
end
|
|
243
|
-
begin
|
|
244
|
-
probe.write(Protocol.encode_request("ping"))
|
|
245
|
-
probe.flush
|
|
246
|
-
rescue SystemCallError, IOError
|
|
247
|
-
nil
|
|
248
287
|
ensure
|
|
249
|
-
probe
|
|
288
|
+
probe&.close
|
|
250
289
|
end
|
|
251
290
|
raise TransportError,
|
|
252
291
|
"a HITL boundary service is already serving #{@socket_path}; stop it before starting another"
|
|
292
|
+
rescue Errno::ENOENT
|
|
293
|
+
nil
|
|
253
294
|
end
|
|
254
295
|
|
|
255
296
|
def read_frame(socket)
|
|
@@ -235,11 +235,18 @@ module Ace
|
|
|
235
235
|
# fails closed instead of handing over an answer.
|
|
236
236
|
with_live_authority!(value, requester: @identity.username) do
|
|
237
237
|
verify_operation!(value, operation)
|
|
238
|
+
verify_otp_deadline!(value)
|
|
238
239
|
answer = read_answer(value)
|
|
239
240
|
unless answer
|
|
240
241
|
next
|
|
241
242
|
end
|
|
242
243
|
|
|
244
|
+
begin
|
|
245
|
+
verify_otp_deadline!(value)
|
|
246
|
+
rescue OtpVault::ExpiredError
|
|
247
|
+
answer.clear if value["sensitive"] == true && !answer.frozen?
|
|
248
|
+
raise
|
|
249
|
+
end
|
|
243
250
|
commit_terminal!(value, "consumed",
|
|
244
251
|
answer: value["sensitive"] == true ? nil : answer)
|
|
245
252
|
update_public(value, "consumed")
|
|
@@ -341,6 +348,7 @@ module Ace
|
|
|
341
348
|
# under the lock; report it without a second write or effect.
|
|
342
349
|
return delivered_result(value) if answer_present?(value)
|
|
343
350
|
with_live_authority!(value, requester: value["requester"]) do
|
|
351
|
+
verify_otp_deadline!(value)
|
|
344
352
|
write_answer(value, answer)
|
|
345
353
|
update_public(value, "answer-delivered")
|
|
346
354
|
unless value["sensitive"] == true
|
|
@@ -839,6 +847,15 @@ module Ace
|
|
|
839
847
|
answer
|
|
840
848
|
end
|
|
841
849
|
|
|
850
|
+
# The persisted authorization window applies to every vault and
|
|
851
|
+
# is checked under the request and live-authority locks at handoff.
|
|
852
|
+
def verify_otp_deadline!(value)
|
|
853
|
+
return unless value["otp"] && Integer(value["otp"]["expires_at"]) <= Time.now.to_i
|
|
854
|
+
|
|
855
|
+
@vault.discard(self, value)
|
|
856
|
+
raise OtpVault::ExpiredError, "the OTP challenge has expired; request a new one"
|
|
857
|
+
end
|
|
858
|
+
|
|
842
859
|
# Persist the answer service-owned (0600): requesters never touch
|
|
843
860
|
# store files — consumption transfers the bytes over the
|
|
844
861
|
# authenticated boundary. The temporary is unique per invocation
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
require_relative "ref"
|
|
3
|
+
require "ace/hitl/contract"
|
|
5
4
|
require_relative "lab"
|
|
6
5
|
|
|
7
6
|
module Ace
|
|
@@ -10,13 +9,6 @@ module Ace
|
|
|
10
9
|
# transports behind one interface. Agent-facing ace-hitl paths resolve
|
|
11
10
|
# adapters through this registry only — never a concrete transport.
|
|
12
11
|
module Providers
|
|
13
|
-
# ask(question:, ...) result: local event id + relay request id.
|
|
14
|
-
AskResult = Struct.new(:event_id, :request_id, keyword_init: true)
|
|
15
|
-
|
|
16
|
-
# deliver(ref, answer) result; state is :delivered, :retryable
|
|
17
|
-
# (safe to re-push identical content) or :failed.
|
|
18
|
-
DeliverResult = Struct.new(:ref, :state, keyword_init: true)
|
|
19
|
-
|
|
20
12
|
REGISTRY = {
|
|
21
13
|
Lab::PROVIDER_NAME => -> { Lab.new }
|
|
22
14
|
}.freeze
|
data/lib/ace/hitl/version.rb
CHANGED
data/lib/ace/hitl.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ace-hitl
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.11.
|
|
4
|
+
version: 0.11.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Michal Czyz
|
|
@@ -9,6 +9,20 @@ bindir: exe
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 2026-10-04 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: ace-hitl-contract
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.1'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.1'
|
|
12
26
|
- !ruby/object:Gem::Dependency
|
|
13
27
|
name: ace-support-core
|
|
14
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -99,14 +113,14 @@ dependencies:
|
|
|
99
113
|
requirements:
|
|
100
114
|
- - "~>"
|
|
101
115
|
- !ruby/object:Gem::Version
|
|
102
|
-
version:
|
|
116
|
+
version: 0.63.1
|
|
103
117
|
type: :runtime
|
|
104
118
|
prerelease: false
|
|
105
119
|
version_requirements: !ruby/object:Gem::Requirement
|
|
106
120
|
requirements:
|
|
107
121
|
- - "~>"
|
|
108
122
|
- !ruby/object:Gem::Version
|
|
109
|
-
version:
|
|
123
|
+
version: 0.63.1
|
|
110
124
|
description: Provides the dedicated ace-hitl package surface for human in the loop
|
|
111
125
|
(HITL) semantics and CLI workflows.
|
|
112
126
|
email:
|
|
@@ -179,13 +193,11 @@ files:
|
|
|
179
193
|
- lib/ace/hitl/molecules/resume_dispatcher.rb
|
|
180
194
|
- lib/ace/hitl/molecules/worktree_scope_resolver.rb
|
|
181
195
|
- lib/ace/hitl/organisms/hitl_manager.rb
|
|
182
|
-
- lib/ace/hitl/providers/errors.rb
|
|
183
196
|
- lib/ace/hitl/providers/lab.rb
|
|
184
197
|
- lib/ace/hitl/providers/lab/assignment_binding.rb
|
|
185
198
|
- lib/ace/hitl/providers/lab/composite_binding.rb
|
|
186
199
|
- lib/ace/hitl/providers/lab/daemon_binding.rb
|
|
187
200
|
- lib/ace/hitl/providers/providers.rb
|
|
188
|
-
- lib/ace/hitl/providers/ref.rb
|
|
189
201
|
- lib/ace/hitl/version.rb
|
|
190
202
|
homepage: https://github.com/cs3b/ace
|
|
191
203
|
licenses:
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Ace
|
|
4
|
-
module Hitl
|
|
5
|
-
module Providers
|
|
6
|
-
# Error model for HITL provider adapters (spec 8wm.t.vrz §2).
|
|
7
|
-
class Error < StandardError; end
|
|
8
|
-
|
|
9
|
-
# The provider name is not in the registry.
|
|
10
|
-
class UnknownProviderError < Error; end
|
|
11
|
-
|
|
12
|
-
# The reverse address (herdr session + pane) is absent or invalid.
|
|
13
|
-
# Ask fails closed before any event or transport state is created.
|
|
14
|
-
class InvalidRefError < Error; end
|
|
15
|
-
|
|
16
|
-
# The transport send failed (binary missing, non-zero exit,
|
|
17
|
-
# unparseable output) after the local event may already exist.
|
|
18
|
-
class ProviderUnavailableError < Error; end
|
|
19
|
-
|
|
20
|
-
# The provider does not implement the requested operation.
|
|
21
|
-
class UnsupportedOperationError < Error; end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative "errors"
|
|
4
|
-
|
|
5
|
-
module Ace
|
|
6
|
-
module Hitl
|
|
7
|
-
module Providers
|
|
8
|
-
# Typed reverse address of the asking agent: herdr session + pane
|
|
9
|
-
# (spec 8wm.t.vrz §3). Versioned schema ace.hitl.ref/v1; captured
|
|
10
|
-
# fail-closed from the herdr environment at ask time. ace-herdr
|
|
11
|
-
# exports both variables when it bootstraps an agent pane.
|
|
12
|
-
#
|
|
13
|
-
# The ref identifies WHERE the answer goes; it never carries answer
|
|
14
|
-
# content.
|
|
15
|
-
class Ref
|
|
16
|
-
SCHEMA = "ace.hitl.ref/v1"
|
|
17
|
-
SESSION_ENV = "HERDR_SESSION"
|
|
18
|
-
PANE_ENV = "HERDR_PANE"
|
|
19
|
-
MAX_LENGTH = 128
|
|
20
|
-
TOKEN_PATTERN = /\A[A-Za-z0-9._:-]+\z/.freeze
|
|
21
|
-
|
|
22
|
-
attr_reader :session, :pane
|
|
23
|
-
|
|
24
|
-
def initialize(session:, pane:)
|
|
25
|
-
@session = session
|
|
26
|
-
@pane = pane
|
|
27
|
-
end
|
|
28
|
-
|
|
29
|
-
# Fail closed: raises InvalidRefError naming the offending variable
|
|
30
|
-
# and rule when the reverse address is absent or invalid.
|
|
31
|
-
def self.from_env(env = ENV)
|
|
32
|
-
session = validate!(env[SESSION_ENV], SESSION_ENV)
|
|
33
|
-
pane = validate!(env[PANE_ENV], PANE_ENV)
|
|
34
|
-
new(session: session, pane: pane)
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def self.validate!(value, source)
|
|
38
|
-
token = value.to_s.strip
|
|
39
|
-
if token.empty?
|
|
40
|
-
raise InvalidRefError,
|
|
41
|
-
"#{source} is required (herdr reverse address, #{SCHEMA})"
|
|
42
|
-
end
|
|
43
|
-
if token.length > MAX_LENGTH
|
|
44
|
-
raise InvalidRefError,
|
|
45
|
-
"#{source} exceeds #{MAX_LENGTH} characters"
|
|
46
|
-
end
|
|
47
|
-
unless token.match?(TOKEN_PATTERN)
|
|
48
|
-
raise InvalidRefError,
|
|
49
|
-
"#{source} contains invalid characters " \
|
|
50
|
-
"(allowed: letters, digits, '.', '_', ':', '-')"
|
|
51
|
-
end
|
|
52
|
-
|
|
53
|
-
token
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def to_h
|
|
57
|
-
{schema: SCHEMA, session: session, pane: pane}
|
|
58
|
-
end
|
|
59
|
-
end
|
|
60
|
-
end
|
|
61
|
-
end
|
|
62
|
-
end
|