pinesandbox 0.3.18 → 0.3.19

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.
@@ -1,6 +1,7 @@
1
1
  require "faraday"
2
2
  require "json"
3
3
  require "securerandom"
4
+ require "tempfile"
4
5
  require "uri"
5
6
  require_relative "../clock"
6
7
  require_relative "../errors"
@@ -14,8 +15,9 @@ require_relative "../response_handler"
14
15
  module PineSandbox
15
16
  module Adapters
16
17
  # @api private — the SDK's HTTP client for the Computer API, reached at the
17
- # runtime's own host `https://<runtime_id>.computer.<zone>`. The subdomain
18
- # selects the runtime and the PATH selects the role (the API at root);
18
+ # Computer sandbox's own host `https://<sandbox_id>.computer.<zone>`. The
19
+ # subdomain selects the Computer sandbox and the PATH selects the role (the
20
+ # API at root);
19
21
  # the host is port-less and header-less — no routing header.
20
22
  #
21
23
  # The bearer goes in `Authorization: Bearer`. The Computer gates every
@@ -36,24 +38,28 @@ module PineSandbox
36
38
  DEFAULT_OPEN_TIMEOUT = 5
37
39
  # A non-2xx stream answer keeps this much of its body for the error.
38
40
  STREAM_ERROR_BODY_CAP = 64 * 1024
39
-
40
- # The runtime is reached at its own host `https://<runtime_id>.computer.<zone>`
41
- # (derived from `zone`), port-less and header-less: the subdomain selects the
42
- # runtime and the PATH selects the role. `zone` is a PineSandbox::Zone.
43
- # `host` is the per-runtime data-plane host `<runtime_id>.computer.<zone>`
41
+ # Chunk size for passing on a body that was spooled to a temp file.
42
+ SPOOL_READ_SIZE = 64 * 1024
43
+ # An event stream silent this long (three missed keepalives) is reopened.
44
+ STREAM_IDLE_TIMEOUT = 60
45
+
46
+ # The Computer sandbox is reached at its own host
47
+ # `https://<sandbox_id>.computer.<zone>` (derived from `zone`), port-less and
48
+ # header-less: the subdomain selects the Computer sandbox and the PATH
49
+ # selects the role. `zone` is a PineSandbox::Zone.
50
+ # `host` is the Computer sandbox's data-plane host `<sandbox_id>.computer.<zone>`
44
51
  # (scheme-less). `scheme` is http for a .localhost zone (local-dev loop),
45
52
  # https otherwise. `clock` paces retries and reconnects.
46
- attr_reader :runtime_id, :host, :scheme, :clock
53
+ attr_reader :sandbox_id, :host, :scheme, :clock
47
54
 
48
55
  # discovery_token is the Computer token that reads the capability
49
56
  # manifest when a request's own token, such as a session's, cannot. An
50
57
  # adopted binding passes it; #bind records the one it issues.
51
- # `sandbox_id:` is the deprecated spelling of `runtime_id:`.
52
- def initialize(zone:, runtime_id: nil, sandbox_id: nil, discovery_token: nil, clock: PineSandbox::SYSTEM_CLOCK)
53
- @runtime_id = runtime_id || sandbox_id
54
- raise ArgumentError, "runtime_id required" if @runtime_id.to_s.empty?
58
+ def initialize(zone:, sandbox_id: nil, discovery_token: nil, clock: PineSandbox::SYSTEM_CLOCK)
59
+ @sandbox_id = sandbox_id
60
+ raise ArgumentError, "sandbox_id required" if @sandbox_id.to_s.empty?
55
61
 
56
- @host = zone.data_host(@runtime_id)
62
+ @host = zone.data_host(@sandbox_id)
57
63
  @scheme = zone.http_scheme # http for a .localhost zone (local-dev loop), https otherwise
58
64
  @discovery_token = discovery_token
59
65
  @clock = clock
@@ -63,13 +69,7 @@ module PineSandbox
63
69
  @zone = zone
64
70
  end
65
71
 
66
- # Deprecated: use #runtime_id.
67
- def sandbox_id
68
- PineSandbox.deprecated_name("CoordinatorAdapter#sandbox_id", "#runtime_id")
69
- @runtime_id
70
- end
71
-
72
- # The full data-plane origin `<scheme>://<runtime_id>.computer.<zone>` — the
72
+ # The full data-plane origin `<scheme>://<sandbox_id>.computer.<zone>` — the
73
73
  # DelegatedConnection's `computer_host` is a complete URI per the Computer API,
74
74
  # so the web SDK derives the desktop's ws/wss scheme from it (no guessing).
75
75
  def base_url
@@ -77,7 +77,7 @@ module PineSandbox
77
77
  end
78
78
 
79
79
  # Bind handshake. Posts the portal-issued bind_token JWS to
80
- # /v1/coord/bind and returns the runtime's `ct_` from the response.
80
+ # /v1/coord/bind and returns the Computer token (`ct_`) from the response.
81
81
  #
82
82
  # Auth model: bind_token's Ed25519 signature IS the auth — no
83
83
  # bearer token is presented here (the bind endpoint refuses
@@ -85,7 +85,7 @@ module PineSandbox
85
85
  #
86
86
  # @param bind_token [String] compact JWS Ed25519 token minted by the
87
87
  # portal attach-credentials endpoint (AttachCredentialsSource).
88
- # @param pod_uid [String] echoed runtime identity (must match the token).
88
+ # @param pod_uid [String] echoed Computer sandbox identity (must match the token).
89
89
  # @param coord_boot_id [String] echoed process boot id.
90
90
  # @param ciphertext [String] base64url HPKE wire (enc || aead_ct).
91
91
  # @return [Hash] { "computer_token" => ct_, "epoch" => Integer }.
@@ -109,7 +109,7 @@ module PineSandbox
109
109
  end
110
110
 
111
111
  # Fetches the Computer's per-process ephemeral HPKE public key and
112
- # runtime identity. Public route — no token. The SDK seals the bind
112
+ # Computer sandbox identity. Public route — no token. The SDK seals the bind
113
113
  # payload to this key and echoes pod_uid/coord_boot_id back on bind.
114
114
  #
115
115
  # @return [Hash] { "pod_uid", "coord_boot_id", "ephem_pub_x25519"
@@ -192,22 +192,6 @@ module PineSandbox
192
192
  true
193
193
  end
194
194
 
195
- # Orphan downloads: browser downloads no session owns (ct_ only).
196
- def list_orphan_downloads(token:)
197
- get("downloads/orphans", token: token).body
198
- end
199
-
200
- def claim_orphan_download(token:, guid:, session_name:, filename: nil)
201
- body = { session_name: session_name }
202
- body[:filename] = filename if filename
203
- post("downloads/orphans/#{escape(guid)}/claim", token: token, json: body).body
204
- end
205
-
206
- def discard_orphan_download(token:, guid:)
207
- delete("downloads/orphans/#{escape(guid)}", token: token)
208
- true
209
- end
210
-
211
195
  def create_session(token:, name: nil, label: nil, browser: false, blind: false)
212
196
  # Omit :name when blank so the coordinator mints a friendly name; only
213
197
  # send :label when the caller set one. Name stays first so the on-wire
@@ -300,7 +284,7 @@ module PineSandbox
300
284
  # ----- Artifacts: list / read / zip / upload -----
301
285
 
302
286
  # GET /v1/sessions/{name}/artifacts?turn_id= — the session's artifacts
303
- # (uploads + claimed downloads + every completed turn's durably-captured
287
+ # (uploads + browser downloads + every completed turn's durably-captured
304
288
  # agent outputs).
305
289
  def list_artifacts(token:, name:, turn_id: nil)
306
290
  route = "v1/sessions/#{escape(name)}/artifacts"
@@ -525,7 +509,7 @@ module PineSandbox
525
509
  event.data["exitCode"] || event.data[:exitCode]
526
510
  exit_code = raw unless raw.nil?
527
511
  end
528
- block.call(event) if block
512
+ pass_to_block(block, event) if block
529
513
  end
530
514
  # Clamp the HTTP timeout above timeout_ms so the client waits for
531
515
  # the server's own timeout to fire an execution_complete event
@@ -541,7 +525,13 @@ module PineSandbox
541
525
  req.headers["Accept"] = "text/event-stream"
542
526
  req.body = body.to_json
543
527
  req.options.on_data = proc do |chunk, _received, env|
544
- if env.nil? || env.status.to_i.between?(200, 299)
528
+ if env.nil?
529
+ # faraday-net_http before 3.0 passes no env, so the status is
530
+ # known only at the end: parse the chunk and keep it in case
531
+ # the answer is an error.
532
+ parser.parse(chunk)
533
+ keep_error_body(error_body, chunk)
534
+ elsif env.status.to_i.between?(200, 299)
545
535
  parser.parse(chunk)
546
536
  else
547
537
  keep_error_body(error_body, chunk)
@@ -568,6 +558,8 @@ module PineSandbox
568
558
  end
569
559
  ExecResult.new(stdout: stdout, stderr: stderr, exit_code: exit_code,
570
560
  error_message: error_msg)
561
+ rescue BlockError => e
562
+ raise e.error
571
563
  end
572
564
 
573
565
  # Minimal struct surfaced by #exec. Matches the shape tests expect:
@@ -701,7 +693,7 @@ module PineSandbox
701
693
  end
702
694
 
703
695
  # Explicit qualification for a binding. No cache, downgrade or mutation.
704
- # A missing discovery route is an old runtime; the gateway's typed
696
+ # A missing discovery route is an old Computer sandbox; the gateway's typed
705
697
  # sandbox-not-found response remains a lifecycle failure.
706
698
  def require_agent_protocol!(token:)
707
699
  manifest = capabilities(token: token)
@@ -709,7 +701,7 @@ module PineSandbox
709
701
 
710
702
  self
711
703
  rescue NotFoundError => e
712
- raise if e.is_a?(RuntimeGoneError)
704
+ raise if e.is_a?(SandboxGoneError)
713
705
 
714
706
  raise AgentProtocolUnsupported
715
707
  end
@@ -899,7 +891,13 @@ module PineSandbox
899
891
  req.headers["Accept"] = "text/event-stream"
900
892
  req.headers["Last-Event-ID"] = last_event_id.to_s unless last_event_id.to_s.empty?
901
893
  req.options.on_data = proc do |chunk, _bytes, env|
902
- if env.nil? || env.status.to_i.between?(200, 299)
894
+ if env.nil?
895
+ # faraday-net_http before 3.0 passes no env, so the status is
896
+ # known only at the end: parse the chunk and keep it in case the
897
+ # answer is an error.
898
+ parser.feed(chunk) { |frame| yield frame }
899
+ keep_error_body(error_body, chunk)
900
+ elsif env.status.to_i.between?(200, 299)
903
901
  parser.feed(chunk) { |frame| yield frame }
904
902
  else
905
903
  keep_error_body(error_body, chunk)
@@ -927,6 +925,16 @@ module PineSandbox
927
925
  end
928
926
  private_constant :BlockError
929
927
 
928
+ # Runs the caller's block on one piece of a streamed answer from inside
929
+ # Faraday's read loop, which would turn an IOError or EPIPE from the
930
+ # block into a dropped connection. The streaming method raises the
931
+ # block's error again as it was.
932
+ def pass_to_block(block, value)
933
+ block.call(value)
934
+ rescue StandardError => e
935
+ raise BlockError.new(e)
936
+ end
937
+
930
938
  # The resuming event-stream engine. Each connection opens from the last
931
939
  # event id seen; frames advance the cursor before any decode, so a
932
940
  # skipped frame is never replayed. A dropped connection (a transport
@@ -1008,24 +1016,45 @@ module PineSandbox
1008
1016
  # raises, before any chunk. A transport fault after the first chunk
1009
1017
  # raises too, so the caller knows the body is incomplete. The default
1010
1018
  # connection's read timeout bounds each wait for the next chunk.
1011
- def stream_bytes(path, token:, accept:)
1019
+ def stream_bytes(path, token:, accept:, &block)
1012
1020
  error_body = +""
1021
+ # faraday-net_http before 3.0 passes no env and learns the status only
1022
+ # after the body, so a chunk can't be told from an error body until the
1023
+ # response ends. Those chunks go to a temp file, not memory, and are
1024
+ # passed on once the status is known.
1025
+ spool = nil
1013
1026
  with_transport_norm("GET", path) do
1014
1027
  response = connection.get(route_path(path)) do |req|
1015
1028
  apply_token(req, token)
1016
1029
  req.headers["Accept"] = accept
1017
1030
  req.options.on_data = proc do |chunk, _received, env|
1018
- if env.status.to_i.between?(200, 299)
1019
- yield chunk.b unless chunk.empty?
1031
+ if env.nil?
1032
+ (spool ||= Tempfile.new("pinesandbox-read", binmode: true)).write(chunk)
1033
+ elsif env.status.to_i.between?(200, 299)
1034
+ pass_to_block(block, chunk.b) unless chunk.empty?
1020
1035
  else
1021
1036
  keep_error_body(error_body, chunk)
1022
1037
  end
1023
1038
  end
1024
1039
  end
1040
+ if spool
1041
+ spool.rewind
1042
+ if response.success?
1043
+ while (chunk = spool.read(SPOOL_READ_SIZE))
1044
+ yield chunk
1045
+ end
1046
+ else
1047
+ keep_error_body(error_body, spool.read(STREAM_ERROR_BODY_CAP).to_s)
1048
+ end
1049
+ end
1025
1050
  response.env.body = error_body if !response.success? && response.body.to_s.empty?
1026
1051
  ensure_success!(response, token: token) unless response.success?
1027
1052
  end
1028
1053
  nil
1054
+ rescue BlockError => e
1055
+ raise e.error
1056
+ ensure
1057
+ spool&.close!
1029
1058
  end
1030
1059
 
1031
1060
  def get(path, token:, accept: "application/json", headers: nil)
@@ -1105,7 +1134,7 @@ module PineSandbox
1105
1134
  end
1106
1135
 
1107
1136
  def normalize_transport_fault(error, method, path)
1108
- suffix = context_suffix(@runtime_id, op_from_route(method, path), nil)
1137
+ suffix = context_suffix(@sandbox_id, op_from_route(method, path), nil)
1109
1138
  if error.is_a?(Faraday::TimeoutError)
1110
1139
  PineSandbox::TimeoutError.new("Computer request timed out: #{error.message}#{suffix}")
1111
1140
  else
@@ -1123,7 +1152,7 @@ module PineSandbox
1123
1152
 
1124
1153
  # The coord route path on the gateway host. The gateway routes by subdomain
1125
1154
  # and selects the role by PATH, so the path is just the coord route — no
1126
- # proxy prefix, no runtime id, no port.
1155
+ # proxy prefix, no sandbox id, no port.
1127
1156
  def route_path(suffix)
1128
1157
  "/#{suffix}"
1129
1158
  end
@@ -1151,12 +1180,12 @@ module PineSandbox
1151
1180
  def classify_failure!(response, token)
1152
1181
  check_success!(response)
1153
1182
  rescue NotFoundError => original
1154
- raise if original.is_a?(RuntimeGoneError) || !original.problem_type.to_s.empty?
1183
+ raise if original.is_a?(SandboxGoneError) || !original.problem_type.to_s.empty?
1155
1184
 
1156
- # The session list is served by every live runtime, so an untyped 404
1185
+ # The session list is served by every live Computer sandbox, so an untyped 404
1157
1186
  # there means the gateway no longer routes to it.
1158
1187
  operation = Operations.match(:computer, response.env.method.to_s.upcase, response.env.url.path)
1159
- raise RuntimeGoneError.new(original), cause: original if operation&.id == "listSessions"
1188
+ raise SandboxGoneError.new(original), cause: original if operation&.id == "listSessions"
1160
1189
 
1161
1190
  # Healthy V2 calls need no discovery round trip. Qualify only an
1162
1191
  # ambiguous 404, without retrying the original operation.
@@ -1185,7 +1214,7 @@ module PineSandbox
1185
1214
  # edge / the ingress), fall back to the older shape so
1186
1215
  # existing callers that rescue ApiError / UnauthorizedError /
1187
1216
  # NotFoundError keep working unchanged.
1188
- # The RESOURCE spine for every error this adapter raises: WHICH Computer (@runtime_id,
1217
+ # The RESOURCE spine for every error this adapter raises: WHICH Computer (@sandbox_id,
1189
1218
  # held by the adapter) and WHICH operation ("<METHOD> <path>", off the Faraday env). We
1190
1219
  # attach it to EVERY raised path so an integrator's generic handler sees the resource +
1191
1220
  # op with no extra plumbing. request_id is the secondary precision handle.
@@ -1200,9 +1229,9 @@ module PineSandbox
1200
1229
  begin
1201
1230
  raise_for_status!(response)
1202
1231
  rescue PineSandbox::ApiError => e
1203
- e.attach_context!(runtime_id: @runtime_id, op: op)
1232
+ e.attach_context!(sandbox_id: @sandbox_id, op: op)
1204
1233
  if e.problem_type == "/errors/sandbox-not-found"
1205
- raise PineSandbox::RuntimeGoneError.new(e), cause: e
1234
+ raise PineSandbox::SandboxGoneError.new(e), cause: e
1206
1235
  end
1207
1236
  raise
1208
1237
  end
@@ -1222,7 +1251,7 @@ module PineSandbox
1222
1251
  if response.status == 408 || response.status == 504
1223
1252
  message = parse_legacy_error_message(response.body)
1224
1253
  timeout = PineSandbox::TimeoutError.new(
1225
- "Computer request failed: status=#{response.status} message=#{message}#{context_suffix(@runtime_id, op, rid)}"
1254
+ "Computer request failed: status=#{response.status} message=#{message}#{context_suffix(@sandbox_id, op, rid)}"
1226
1255
  )
1227
1256
  timeout.retry_after = retry_after_seconds(response)
1228
1257
  raise timeout
@@ -1240,7 +1269,7 @@ module PineSandbox
1240
1269
  else PineSandbox::ApiError
1241
1270
  end
1242
1271
  ext = {}
1243
- ext["sandbox_id"] = @runtime_id if @runtime_id && !@runtime_id.to_s.empty?
1272
+ ext["sandbox_id"] = @sandbox_id if @sandbox_id && !@sandbox_id.to_s.empty?
1244
1273
  ext["op"] = op if op && !op.to_s.empty?
1245
1274
  ext["request_id"] = rid if rid
1246
1275
  raise klass.new(
@@ -1274,11 +1303,11 @@ module PineSandbox
1274
1303
  end
1275
1304
 
1276
1305
  # Resource-first troubleshooting suffix for the message-only error paths (TimeoutError
1277
- # has no problem_extensions): runtime (WHICH Computer runtime) → op (WHICH operation) →
1306
+ # has no problem_extensions): sandbox (WHICH Computer sandbox) → op (WHICH operation) →
1278
1307
  # request_id. Each part only when present; "" when all absent.
1279
- def context_suffix(runtime_id, op, rid)
1308
+ def context_suffix(sandbox_id, op, rid)
1280
1309
  parts = []
1281
- parts << "runtime=#{runtime_id}" if runtime_id && !runtime_id.to_s.empty?
1310
+ parts << "sandbox=#{sandbox_id}" if sandbox_id && !sandbox_id.to_s.empty?
1282
1311
  parts << "op=#{op}" if op && !op.to_s.empty?
1283
1312
  parts << "request_id=#{rid}" if rid && !rid.to_s.empty?
1284
1313
  parts.empty? ? "" : " (#{parts.join(', ')})"
@@ -1297,7 +1326,7 @@ module PineSandbox
1297
1326
  end
1298
1327
 
1299
1328
  # Faraday connection to the Computer's gateway host. The base URL sets the
1300
- # Host (and TLS SNI) to `<runtime_id>.computer.<zone>` — covered by the
1329
+ # Host (and TLS SNI) to `<sandbox_id>.computer.<zone>` — covered by the
1301
1330
  # gateway wildcard cert `*.computer.<zone>`. Port-less and header-less: the
1302
1331
  # gateway routes the subdomain to the pod and the PATH selects the role; no
1303
1332
  # API key (coord gates every route on the bearer).
@@ -1310,18 +1339,14 @@ module PineSandbox
1310
1339
  end
1311
1340
  end
1312
1341
 
1313
- # Long-lived stream connection — disables the read timeout. The default
1314
- # `connection` above ships a 30s `options.timeout`; for SSE that's a
1315
- # papercut (a model-thinking turn or sparse control-event stream
1316
- # legitimately exceeds 30s of silence). Coord ships periodic `: ka`
1317
- # keepalive comments (every 20s) which reset Net::HTTP's read timer,
1318
- # so even with the default this rarely fires in practice — but the
1319
- # principled fix is to make the streaming socket actually long-lived.
1342
+ # Long-lived stream connection. Its read timeout is an idle watchdog: coord
1343
+ # sends a `: ka` keepalive comment every 20s, and each read resets the
1344
+ # timer, so a quiet but healthy stream never trips it. A connection that
1345
+ # sends nothing for STREAM_IDLE_TIMEOUT is half-open; the timeout raises
1346
+ # Faraday::TimeoutError and follow_events reconnects from the cursor.
1320
1347
  #
1321
- # Faraday 2.x trap: `req.options.timeout = nil` does NOT mean "no
1322
- # timeout" on the Net::HTTP adapter. faraday-net_http 3.x only sets
1323
- # `http.read_timeout` when the option is truthy; nil falls through to
1324
- # Ruby's 60s default. The fix is to set it explicitly via the adapter
1348
+ # Faraday 2.x trap: `req.options.timeout` is not honored the way it reads
1349
+ # on the Net::HTTP adapter, so the read timeout is set in the adapter
1325
1350
  # block, which the adapter calls AFTER applying request_timeout.
1326
1351
  def streaming_connection
1327
1352
  @streaming_connection ||= @zone.faraday(@host) do |conn|
@@ -1329,7 +1354,7 @@ module PineSandbox
1329
1354
  conn.options.open_timeout = DEFAULT_OPEN_TIMEOUT
1330
1355
  # DELIBERATELY no conn.options.timeout — adapter block is authoritative.
1331
1356
  conn.adapter Faraday.default_adapter do |http|
1332
- http.read_timeout = nil
1357
+ http.read_timeout = STREAM_IDLE_TIMEOUT
1333
1358
  end
1334
1359
  end
1335
1360
  end
@@ -165,11 +165,11 @@ module PineSandbox
165
165
  end
166
166
 
167
167
  # How the turn ended, including with a question (a `result` frame): the
168
- # same Hash the turn's "result" holds, or nil for another event.
168
+ # same AgentResult the turn's #result returns, or nil for another event.
169
169
  def result
170
170
  return nil unless type == "result" && payload.is_a?(Hash)
171
171
 
172
- payload
172
+ AgentResult.wrap(payload)
173
173
  end
174
174
 
175
175
  private
@@ -1,6 +1,28 @@
1
1
  require "time"
2
2
 
3
3
  module PineSandbox
4
+ # How a turn ended (spec PublicTurnResult), from AgentTurn#result and
5
+ # AgentEvent#result: the result's own string-keyed Hash, extended with these
6
+ # readers. It stays a plain Hash, so a change to it is kept and YAML and JSON
7
+ # store it as they did before the readers existed. A frozen result can't be
8
+ # extended, so it reads as a frozen copy.
9
+ module AgentResult
10
+ def self.wrap(value)
11
+ return value unless value.is_a?(Hash)
12
+ return value if value.is_a?(self)
13
+ return value.dup.extend(self).freeze if value.frozen?
14
+
15
+ value.extend(self)
16
+ end
17
+
18
+ def terminal_reason = self["terminal_reason"]
19
+ # The agent's closing message; the question for "needs_input".
20
+ def summary = self["summary"]
21
+ def artifacts = self["artifacts"]
22
+ def author = self["author"]
23
+ def usage = self["usage"]
24
+ end
25
+
4
26
  # One unit of agent work (spec PublicTurn), returned by AgentMode#run,
5
27
  # #wait, #answer, #steer and #cancel. A turn starts when a run or an answer is
6
28
  # accepted and ends when the agent finishes, fails, is canceled or asks a
@@ -25,8 +47,8 @@ module PineSandbox
25
47
  def state = self["state"]
26
48
  # The turn this turn's answer opened; present once it is "answered".
27
49
  def next_turn_id = self["next_turn_id"]
28
- # How the turn ended, with "usage"; nil while it runs.
29
- def result = self["result"]
50
+ # How the turn ended, an AgentResult with its usage; nil while it runs.
51
+ def result = AgentResult.wrap(self["result"])
30
52
  def first_event_id = self["first_event_id"]
31
53
  def last_event_id = self["last_event_id"]
32
54
 
@@ -12,17 +12,17 @@ module PineSandbox
12
12
  # * POST /v1/computers — optional legacy
13
13
  # pre-registration helper. Normal attach does not require it.
14
14
  # * POST /v1/computers/:computer_id/attach-credentials — body carries the
15
- # runtime coordinates {pod_uid, coord_boot_id}; returns {bind_token,
15
+ # start's coordinates {pod_uid, coord_boot_id}; returns {bind_token,
16
16
  # broker_grant, key_assertion, binding_revision}. The portal authenticates
17
17
  # the pk_ and atomically creates/rechecks its minimal authorization row,
18
18
  # advances the key floor, supersedes the prior binding, and mints.
19
19
  # * DELETE /v1/computers/:computer_id — permanent delete.
20
20
  # 204 for deleted, already deleted, unknown, and cross-project ids alike.
21
21
  #
22
- # This source caches nothing. Computer#run_bind calls it exactly once for a
23
- # sandbox, then convergence retries re-send the held envelope verbatim
24
- # (the bind is idempotent on jti + ciphertext). A runtime-identity shift
25
- # requires a fresh sandbox; it never authorizes the same sandbox twice.
22
+ # This source caches nothing. Computer#run_bind calls it exactly once for each
23
+ # start, then convergence retries re-send the held envelope verbatim
24
+ # (the bind is idempotent on jti + ciphertext). A change of pod_uid or
25
+ # coord_boot_id requires a new start; it never authorizes the same start twice.
26
26
  # The pk_ never appears in #inspect.
27
27
  class AttachCredentialsSource
28
28
  REGISTER_PATH = "/v1/computers"
@@ -97,7 +97,7 @@ module PineSandbox
97
97
  payload
98
98
  end
99
99
 
100
- # One authorization-registry read before Start; never a runtime status poll.
100
+ # One authorization-registry read before Start; never a Computer status poll.
101
101
  def current_binding(computer_id)
102
102
  resp = get("#{REGISTER_PATH}/#{computer_id}", op: "computer binding lookup")
103
103
  if resp.status == 404
@@ -134,9 +134,9 @@ module PineSandbox
134
134
  end
135
135
  end
136
136
 
137
- # Mint the per-attach credentials for one runtime + boot. `sandbox_id` is the
138
- # provisioned sandbox this attach targets — the portal records the
139
- # sandbox→computer attribution at mint (its renewal gate keys off it).
137
+ # Mint the per-attach credentials for one Computer sandbox and boot. `sandbox_id`
138
+ # is the sandbox id this attach targets — the portal records which Computer it
139
+ # belongs to at mint (its renewal gate keys off it).
140
140
  # Returns a Hash with string keys: "bind_token", "broker_grant",
141
141
  # "key_assertion", "binding_revision", and the effective "location"
142
142
  # (plus expiry metadata).
@@ -214,7 +214,7 @@ module PineSandbox
214
214
  problem["current_binding_revision"], problem["current_sandbox_id"]
215
215
  ),
216
216
  current_binding_revision: problem["current_binding_revision"],
217
- current_runtime_id: problem["current_sandbox_id"],
217
+ current_sandbox_id: problem["current_sandbox_id"],
218
218
  **error_attributes(resp)
219
219
  )
220
220
  else
@@ -347,12 +347,12 @@ module PineSandbox
347
347
 
348
348
  # The attach revision is a fence against two live attaches of one
349
349
  # Computer, so the SDK never retries past it; the message says what to do.
350
- def binding_conflict_message(message, current_revision, current_runtime_id)
350
+ def binding_conflict_message(message, current_revision, current_sandbox_id)
351
351
  return message unless current_revision.is_a?(Integer)
352
352
 
353
- holder = current_runtime_id.to_s.empty? ? "" : " (runtime #{current_runtime_id})"
353
+ holder = current_sandbox_id.to_s.empty? ? "" : " (Computer sandbox #{current_sandbox_id})"
354
354
  "#{message}. Another start changed the Computer binding#{holder}. " \
355
- "Do not retry automatically. Check the current runtime before starting again."
355
+ "Do not retry automatically. Check whether the Computer is still running before starting again."
356
356
  end
357
357
 
358
358
  def error_message(resp, default)
@@ -15,7 +15,7 @@ module PineSandbox
15
15
  module_function
16
16
 
17
17
  # info binds the sealed payload to the receiving process identity so a
18
- # ciphertext sealed for one runtime/boot cannot be opened by another.
18
+ # ciphertext sealed for one Computer sandbox/boot cannot be opened by another.
19
19
  # MUST byte-match the receiver's info string.
20
20
  #
21
21
  # @param pod_uid [String]
@@ -26,8 +26,8 @@ module PineSandbox
26
26
  end
27
27
 
28
28
  # restore_info is the two-round v3 restore's round-2 domain: the
29
- # integrator-unwrapped component secrets sealed back to the same runtime
30
- # key. Same suite, DISTINCT domain — a bind ciphertext can never be
29
+ # integrator-unwrapped component secrets sealed back to the same Computer
30
+ # sandbox key. Same suite, DISTINCT domain — a bind ciphertext can never be
31
31
  # replayed as a secrets payload or vice versa. MUST byte-match the
32
32
  # receiver's restore info string.
33
33
  def restore_info(pod_uid, coord_boot_id)
@@ -36,7 +36,7 @@ module PineSandbox
36
36
 
37
37
  # seal encrypts plaintext to the recipient's raw 32-byte X25519 public
38
38
  # key. The AAD is fixed empty (the portal attach path carries no nonce —
39
- # payload-to-runtime binding lives in the info string).
39
+ # payload-to-sandbox binding lives in the info string).
40
40
  #
41
41
  # @param recipient_pub_raw [String] 32 raw bytes (NOT base64).
42
42
  # @param plaintext [String] bytes to seal.