kosa8 docs

Guides / Ruby / API reference

Ruby API reference

Everything public in the kosa8 gem, version 0.1.2: every class, method, keyword argument and default, what each returns and what it raises. Signatures are copied from the source in lib/. Keys in returned hashes are the daemon's own JSON names, as strings.

How this page was checked

Every signature was copied from lib/kosa8/*.rb at tag v0.1.2 of the source (commit 2b5945e), and the gem published on RubyGems (gem install kosa8 -v 0.1.2) was checked to contain the same files. The result shapes and timings quoted were printed by real calls to a kosa8 daemon 0.9.36 on macOS 26.0.1 (Apple silicon), from the system Ruby 2.6.10, with 0.1.1 or 0.1.2 where the behaviour changed in those versions and 0.1.0 where it did not. mounts:, capture:, broker:, encrypt:, allow_tampered: and approvals were not exercised. Differences from 0.1.0 are marked "since 0.1.1" or "since 0.1.2".

What this is, in plain words

This page is the dictionary. The quickstart and the developer guide show how the pieces fit together; this page lists every piece, with the exact names to type and the exact things you get back. You do not need to read it top to bottom: search it for the method you are calling.

A few words used throughout: the daemon is the kosa8 background service on your machine. The gem sends it requests over a unix socket (a file that acts like a network port, only reachable from your own computer). A keyword argument is a Ruby argument passed by name, as in run(…, timeout: 30). A hash is Ruby's dictionary type; most methods return the daemon's answer as one.

The gem

gem "kosa8", "~> 0.1.2"  # Gemfile (or: gem install kosa8 -v 0.1.2)
require "kosa8"           # loads version, errors, transport, checkpoints, client
NameValue
Kosa8::VERSION"0.1.2"
Kosa8::MAX_CHECKPOINTS_PER_SANDBOX10. Checkpoints kept per sandbox; past it the oldest is evicted.
Kosa8::APPROVAL_HEADER"X-Kosa8-Approval"
Kosa8::ACTOR_HEADER"X-Kosa8-Actor"
Kosa8::AGENT_TOKEN_HEADER"X-Kosa8-Agent-Token"
Kosa8::BY_STATUSFrozen hash from HTTP status to error class: 400, 401, 403, 404, 409, 501.
Kosa8::TRANSIENTThe regexp explore uses to decide a branch failure is worth retrying (see explore).

Ruby 2.6 or later (required_ruby_version >= 2.6). No dependencies: the gem uses only the standard library (net/http, json, socket, openssl, base64, uri, fileutils, time). MIT licensed.

Kosa8::Client

Kosa8::Client.new(
  socket_path: nil,
  host: nil,
  token: nil,
  insecure: false,
  actor: "tool",
  agent_token: nil,
  headers: {},
  timeout: nil,
  checkpoint_store: nil,
  env: ENV
)

All arguments are keyword arguments. Constructing a client sends nothing: it works out where to connect (and raises Kosa8::Error for a refused address or a bad actor) and sets up the checkpoint store.

KeywordDefaultMeaning
socket_path:~/.kosa8/run/kosa8.sockThe daemon's unix socket. Used only when no host is set, here or in KOSA8_HOST.
host:ENV["KOSA8_HOST"]A remote worker: https://…, http://…, tcp://h:p, h:p, or unix:///path. An empty string counts as not given. Rules under resolve_endpoint.
token:ENV["KOSA8_TOKEN"], only when the host came from KOSA8_HOSTSent as Authorization: Bearer …, to a TCP endpoint only, never over a unix socket.
insecure:false (or KOSA8_INSECURE=1)Allows plain http:// to a host that is not loopback.
actor:"tool""human", "agent" or "tool"; sent as X-Kosa8-Actor on every request and recorded in the audit log. Anything else raises Kosa8::Error (actor must be human, agent or tool, not "root").
agent_token:ENV["KOSA8_AGENT_TOKEN"]Agent identity, sent as X-Kosa8-Agent-Token when non-empty. A token the daemon does not know is refused with AuthError, never treated as anonymous.
headers:{}Extra headers on every request. Set after Accept and X-Kosa8-Actor (so they can replace those), before Content-Type, X-Kosa8-Approval, X-Kosa8-Agent-Token and Authorization (which win when the gem sets them).
timeout:nilSeconds. Sets Net::HTTP's read_timeout to this and open_timeout to the smaller of this and 30, for every request. A per-call timeout: overrides it. With nil: streaming calls (run, exec, exec_frames, attach) wait for output as long as the command runs (since 0.1.1; 0.1.0 cut them off after 60 silent seconds), and every other call keeps Net::HTTP's defaults, 60 seconds to connect and 60 per read. See Timeouts.
checkpoint_store:FileCheckpointStore.newWhere checkpoint stacks are kept. Any object with load and update { |data| … }; see Checkpoint stores.
env:ENVWhere KOSA8_HOST, KOSA8_TOKEN, KOSA8_INSECURE and KOSA8_AGENT_TOKEN are read. Pass {} to ignore the environment.
ReaderTypeMeaning
transportKosa8::TransportThe HTTP layer. transport.endpoint.target says where requests go, such as unix:///Users/you/.kosa8/run/kosa8.sock.
checkpointsKosa8::CheckpointBookThe checkpoint stack logic over checkpoint_store. (On a Sandbox handle, checkpoints is a different method: the list.)

Conventions for every method

Daemon

SignatureReturnsHTTP
healthHash: status, version, commit, go, uptime_seconds, phaseGET …/health
versionhealth["version"], such as "0.9.36" (nil if absent)GET …/health
pull(ref, approval: nil)The daemon's answer. Pulls into the host's shared engine, not into a sandbox; for a sandbox use pull_into_sandbox.POST …/pull, body {"ref"}

Sandboxes

SignatureReturnsHTTP
list_sandboxesArray of sandbox recordsGET …/sandboxes
get_sandbox(id_or_name)The record whose id matches, else whose name matches, else nil. Never raises for a missing sandbox.GET …/sandboxes, filtered in the client
create_sandbox(name: nil, cpus: nil, mem_mb: nil, disk_gb: nil, mounts: nil, egress: nil, egress_allow: nil, capture: nil, labels: nil, broker: nil, approval: nil)Hash: sandbox (a record), boot_ms, and broker when one was asked forPOST …/sandboxes
start_sandbox(sandbox, approval: nil)Hash: boot_ms, sandbox, warning. Boots a stopped sandbox from its own disk; processes that were running are not brought back.POST …/sandboxes/{id}/up
stop_sandbox(sandbox, approval: nil)Hash: ok, sandbox (state stopped), warning. The disk is kept; running processes end.POST …/sandboxes/{id}/down
delete_sandbox(sandbox, wipe: false, keep_checkpoints: false, approval: nil)The daemon's answer ({"ok"=>true}) merged with "checkpoints_dropped" (an Integer)DELETE …/sandboxes/{id} (?wipe=1). Then, unless keep_checkpoints, removes the sandbox's stack from the checkpoint store and sends DELETE …/snapshots/{ckpt} (with the same wipe) for each, ignoring their errors
pull_into_sandbox(sandbox, ref, approval: nil)Hash: image (the full reference, such as "docker.io/library/alpine:latest"), okPOST …/sandboxes/{id}/pull, body {"ref"}. Fetched on the host and copied in, so it works with egress: "deny"
sandbox_images(sandbox)Array of image references (Strings)GET …/sandboxes/{id}/images
sandbox_containers(sandbox)Array of container hashesGET …/sandboxes/{id}/containers
sandbox_egress(sandbox)Hash: policy, stats, and more when they applyGET …/sandboxes/{id}/egress

create_sandbox keywords

KeywordSent asNotes
name:nameAlso the sandbox's id. Without it the daemon picks one. Left out of the body when nil, as are cpus, mem_mb, disk_gb, mounts, labels and broker.
cpus:, mem_mb:, disk_gb:same namesIntegers. Daemon 0.9.36 defaults seen in tests: 2 CPUs, 32 GB disk.
egress:egressA String passed as is: "deny", "any", "fakenet", "allow:host,host". Or a hash { allow: [...] } (symbol or string key), turned into "allow:a,b" with each host stripped; an empty list raises Kosa8::Error (an egress allow-list needs at least one host; use egress: "deny" for no network). nil sends nothing and the daemon's default applies.
egress_allow:egressAn array of hosts; non-empty, it becomes "allow:a,b" and wins. Combined with an egress: other than nil or "deny" it raises Kosa8::Error before sending (egress_allow already limits the network to the hosts it lists; do not also set egress). An empty array is ignored.
mounts:mountsPassed through unchanged. Not tested for this page; see Native HTTP API for what the daemon accepts.
capture:captureSent whenever it is not nil (so false is sent).
labels:labelsA hash, such as { "team" => "docs" }; checked against the org policy and echoed in the record.
broker:brokerCredential broker configuration, passed through. See Keep API keys out.

Running code

SignatureReturnsHTTP
run(sandbox, image, args = [], env: nil, on_stdout: nil, on_stderr: nil, on_status: nil, timeout: nil, approval: nil)ExecResultPOST …/sandboxes/{id}/run, body {"image","args","env"?}; answer is NDJSON frames. Starts a fresh container, waits for it to exit. The image must already be in the sandbox
start_container(sandbox, image, args = [], approval: nil)The container id (String), such as "k3-bea6352b"POST …/sandboxes/{id}/start, body {"image","args"}. Runs detached
exec(sandbox, container, args, on_stdout: nil, on_stderr: nil, timeout: nil, approval: nil)ExecResultPOST …/sandboxes/{id}/exec, body {"container","args"}; NDJSON frames. No env:: exec takes none
exec_frames(sandbox, container, args, timeout: nil, approval: nil) { |frame| … }nilPOST …/sandboxes/{id}/exec. Yields every frame, including the final one, with data already base64-decoded. Does not raise on an error frame; you see {"done"=>true, "error"=>…}
start_interactive(sandbox, image, args = [], env: nil, tty: false, approval: nil)The container idPOST …/sandboxes/{id}/start-interactive, body {"image","args","env","tty"} (env is null when not given). Stdin is held open for write_stdin
write_stdin(sandbox, container, data, approval: nil){"ok"=>true}POST …/sandboxes/{id}/containers/{cid}/stdin, application/octet-stream. A String is sent with a length; an IO (anything with read) or any other Enumerable of chunks is sent chunked. Once per container per client: a second call raises Kosa8::Error before sending (since 0.1.1)
attach(sandbox, container, on_stdout: nil, on_stderr: nil, timeout: nil, approval: nil)ExecResult when the container exitsPOST …/sandboxes/{id}/containers/{cid}/attach; NDJSON frames

Snapshots and fork

SignatureReturnsHTTP
snapshot(sandbox, id: nil, encrypt: false, approval: nil)Hash: snapshot (a snapshot record), snapshot_msPOST …/sandboxes/{id}/snapshot, body {"id"?, "encrypt"?} (encrypt only when true). Captures memory, processes and disks; the sandbox keeps running. A taken id raises ConflictError
list_snapshotsArray of snapshot records (every sandbox's)GET …/snapshots
restore_snapshot(snapshot_id, name: nil, label: nil, allow_tampered: false, approval: nil)Hash: sandbox (the new record, with parent), restore_msPOST …/snapshots/{id}/restore, body {"name"?, "label"?, "allow_tampered"?}. Boots a new sandbox with processes resumed
delete_snapshot(snapshot_id, wipe: false, force: false, approval: nil)Hash: encrypted, ok, removedDELETE …/snapshots/{id} (?wipe=1, &force=1)
fork(sandbox, n = 1, prefix: nil, label: nil, approval: nil)Hash: count, fork_ms, forks (array of sandbox records), snapshot (the id of the fork's snapshot, a String)POST …/sandboxes/{id}/fork, body {"n", "prefix"?, "label"?}. n is sent as given. With prefix: "p" the copies are named p1, p2, …

In our tests, once every copy made by fork had been deleted, its snapshot had gone too (delete_snapshot on it answered NotFoundError).

explore

explore(sandbox, variants, image: "alpine:latest", keep_all: false, keep: nil, timeout: 120,
        prefix: nil, pull: true, approval: nil)  # => Kosa8::ExploreResult

Best-of-N, as kosa8 explore does it. In order:

  1. variants.to_a; fewer than two raises Kosa8::Error (give at least two variants; with one there is nothing to compare) before anything is sent.
  2. If pull: pull_into_sandbox(sandbox, image).
  3. fork(sandbox, variants.length, prefix: …). The default prefix is "#{sandbox}-explore-#{Time.now.to_i % 100_000}". If fewer branches come back than variants, raises Kosa8::Error (forked N branches for M variants; see list_sandboxes for what was created); the branches made are not deleted.
  4. One Ruby Thread per branch runs run(branch, image, ["sh", "-c", variant], timeout: timeout). A failure whose message matches Kosa8::TRANSIENT (connection refused, EOF, reset, broken pipe, "no such file or directory", stream ended, unreachable) is retried up to four attempts in all, sleeping 0.5, 1 and 1.5 seconds between them (since 0.1.1 there is no sleep after the last). A TimeoutError is never retried. Branch failures are recorded in the branch's error, not raised.
  5. The winner is the successful branch (no error, exit 0) with the smallest duration_ms, or "" if none succeeded. keep: names a branch to keep instead; a name that is not a branch raises Kosa8::Error (keep X is not one of the branches (…); all of them were kept) and nothing is deleted.
  6. Unless keep_all, every branch other than the winner is deleted with DELETE …/sandboxes/{branch} (errors ignored, counted in removed). With no winner, that is every branch. The parent sandbox is never touched.

One quirk worth knowing: TRANSIENT also matches "No such file or directory". Because variants run through sh -c, a command missing from the image is an ordinary exit 127 (in our test, "python3 -V" on Alpine came back as exit=127 error=nil, no retry). But in an image with no sh at all, the run fails with … not found in $PATH: No such file or directory, which the code treats as transient and retries four times (read from the code; not reproduced).

Checkpoints

Checkpoints are kept on the client side, over snapshots, in the same file and format the kosa8 CLI uses (~/.kosa8/checkpoints.json), so kosa8 checkpoint ls and kosa8 rewind see what the gem did and the other way round.

SignatureReturnsHow
checkpoint(sandbox, label: "", approval: nil)Hash: "checkpoint" (the stored record), "evicted" (ids, Array), "snapshot" (the snapshot answer)Picks the next id <sandbox>-ckpt-<n>; POST …/sandboxes/{id}/snapshot with it (on a 409 "already exists", steps to the next id and retries, up to 10 attempts, since 0.1.2); once that exists, DELETE …/snapshots/{old} for each evicted id past 10 (errors ignored); then writes the record to the store
list_checkpoints(sandbox)Array of checkpoint records, oldest firstThe store only; sends nothing
rewind(sandbox, to: nil, approval: nil)Hash: "sandbox" (the restored record), "checkpoint" (the target record), "dropped" (ids), "restore_ms"DELETE …/sandboxes/{id}, then POST …/snapshots/{ckpt}/restore with {"name": sandbox}, then drops every later checkpoint from the store and sends DELETE …/snapshots/{later} for each (errors ignored)

Helpers

SignatureReturns
sandbox(sandbox_id)A Kosa8::Sandbox bound to that id. Sends nothing, and does not check the sandbox exists.
Kosa8::Client.egress_string(egress, allow)The egress string create_sandbox would send, or nil. Raises as described above.
Kosa8::Client.env_list(env)nil, or ["K=V", …] from a hash, or env.to_a.
Kosa8::Client.utf8(bytes)A copy forced to UTF-8 with invalid bytes replaced (scrub). How ExecResult strings are made.

Kosa8::Sandbox

Kosa8::Sandbox.new(client, id)   # usually: client.sandbox("name")
sb.id        # => "doc-rb-demo"
sb.client    # => the Kosa8::Client
sb.inspect   # => "#<Kosa8::Sandbox doc-rb-demo>"

Each method calls the client method with the sandbox id filled in and passes the remaining keyword arguments through, so the keywords, results and errors are the client's.

MethodCalls
infoget_sandbox(id)
start(**kw)start_sandbox(id, **kw)
stop(**kw)stop_sandbox(id, **kw)
delete(**kw)delete_sandbox(id, **kw): wipe:, keep_checkpoints:, approval:
pull(ref, **kw)pull_into_sandbox(id, ref, **kw) (not the client's pull)
imagessandbox_images(id)
containerssandbox_containers(id)
egresssandbox_egress(id)
run(image, args = [], **kw)run(id, image, args, **kw)
start_container(image, args = [], **kw)start_container(id, image, args, **kw)
exec(container, args, **kw)exec(id, container, args, **kw)
snapshot(**kw)snapshot(id, **kw)
fork(n = 1, **kw)fork(id, n, **kw)
explore(variants, **kw)explore(id, variants, **kw)
exec_frames(container, args, **kw) { |frame| … }exec_frames(id, container, args, **kw) (since 0.1.1)
start_interactive(image, args = [], **kw)start_interactive(id, image, args, **kw) (since 0.1.1)
write_stdin(container, data, **kw)write_stdin(id, container, data, **kw) (since 0.1.1)
attach(container, **kw)attach(id, container, **kw) (since 0.1.1)
checkpoint(**kw)checkpoint(id, **kw)
checkpointslist_checkpoints(id)
rewind(**kw)rewind(id, **kw)

Not on the handle: restore_snapshot, delete_snapshot, list_snapshots (they are about snapshots, not this sandbox). Call them on sb.client.

Structs

All three are Structs built with keyword_init: true, so they have readers and writers for each member, to_h, and value equality.

Kosa8::ExecResult

ExecResult = Struct.new(:exit_code, :stdout, :stderr, keyword_init: true)
r.exit_code  # Integer
r.stdout     # String, UTF-8, invalid bytes replaced
r.stderr     # String, the same
r.ok?        # exit_code.zero?

Kosa8::ExploreBranch

ExploreBranch = Struct.new(:name, :variant, :exit, :output, :error, :duration_ms, keyword_init: true)
b.name         # the branch sandbox's id, such as "doc-rb-g2-e2"
b.variant      # the shell command it ran
b.exit         # Integer; -1 if it never ran
b.output       # stdout + stderr, trailing newline removed (chomp)
b.error        # the last error message, or nil
b.duration_ms  # Integer, including retries

Kosa8::ExploreResult

ExploreResult = Struct.new(:parent, :branches, :winner, :removed, keyword_init: true)
x.parent     # the sandbox explored
x.branches   # Array of ExploreBranch, in variant order
x.winner     # the kept branch's name, or "" when none succeeded
x.removed    # Integer: branches deleted

A real one, from three variants ("sleep 2; echo slow", "echo fast", "echo broken; exit 1"):

"doc-rb-g2-e2"
2
#<struct Kosa8::ExploreBranch name="doc-rb-g2-e1", variant="sleep 2; echo slow", exit=0, output="slow", error=nil, duration_ms=2508>
#<struct Kosa8::ExploreBranch name="doc-rb-g2-e2", variant="echo fast", exit=0, output="fast", error=nil, duration_ms=506>
#<struct Kosa8::ExploreBranch name="doc-rb-g2-e3", variant="echo broken; exit 1", exit=1, output="broken", error=nil, duration_ms=484>

Result shapes

Other methods return the daemon's JSON as plain hashes and arrays. The keys you will meet most, as kosa8 0.9.36 sent them in our tests:

ShapeKeys
sandbox recordid, name, created, state (running, stopped, …), cpus, mem_mb, disk_gb, egress, ip, and when set parent, labels, mounts, capture, broker
snapshot recordid, sandbox, created, machine_id, mac, cpus, mem_mb, disk_gb, cmdline, size_bytes, state_path, disk_path, data_path, egress, and optional fields such as sealed, labels, encryption
checkpoint recordid, sandbox, label, created (UTC with nine fraction digits, such as 2026-11-03T06:46:25.129669000Z). Records written by the Go tools may also carry auto.
containerid, image, status, ip, and exit when known
egress statuspolicy, and stats: passed, denied, dropped, dns_answered, dns_refused
exec frame{"stream"=>"stdout"|"stderr"|"status", "data"=>bytes}, then a final {"done"=>true, "ok"=>true, "exit"=>4} or {"done"=>true, "error"=>"…"}

A real sandbox record, from create_sandbox(name: "doc-rb-guide", cpus: 2, mem_mb: 1024, egress_allow: ["example.com"], labels: { "team" => "docs" }):

{"id"=>"doc-rb-guide", "name"=>"doc-rb-guide", "created"=>"2026-11-03T01:39:22.091122-05:00", "state"=>"running", "cpus"=>2, "mem_mb"=>1024, "disk_gb"=>32, "egress"=>"allow:example.com", "ip"=>"192.168.127.2", "labels"=>{"team"=>"docs"}}

Transport and endpoints

Kosa8::Transport

Kosa8::Transport.new(socket_path: nil, host: nil, token: nil, insecure: false, actor: "tool",
                     agent_token: nil, headers: {}, timeout: nil, env: ENV)

The same connection keywords as Client.new, without checkpoint_store:. It opens a new connection for every request (no keep-alive), so one transport can be used from several threads at once.

MemberMeaning
endpointThe resolved Kosa8::Endpoint.
raw(method, path, query: nil, body: nil, raw_body: nil, approval: nil, timeout: nil, stream: false) { |resp| … }stream: true (what frames passes) sets no read timeout when none is given. Sends one request and yields the Net::HTTPResponse with its body unread; returns the block's value. body is sent as JSON; raw_body as application/octet-stream (a String with a length, an IO or Enumerable chunked). For a TCP endpoint the endpoint's base_path is put before path. Raises TimeoutError for Net::ReadTimeout/Net::OpenTimeout, and ConnectionError for any SystemCallError, IOError, SocketError or OpenSSL::SSL::SSLError, adding the "start it with `kosa8 daemon start`" hint for ENOENT and ECONNREFUSED.
json(method, path, **kw)raw, then the body parsed as JSON (nil for an empty body, the text itself if it is not JSON). A status of 400 or more raises Kosa8.error_from_response(…).
frames(method, path, **kw) { |frame| … }raw with stream: true, then yields one frame per NDJSON line, with data base64-decoded. An HTTP error before streaming raises the typed error; a line that is not JSON (or bad base64) raises ProtocolError. Returns nil.

An exception raised by the block given to frames (and so by your callbacks) passes through raw's network rescue unchanged: a Kosa8::Error is re-raised as is, and any other StandardError is carried past the rescue inside the internal Kosa8::CallbackFailure (an Exception subclass) and re-raised as the original object. You never see CallbackFailure yourself. In 0.1.0 such an IOError became a ConnectionError, and a malformed line raised JSON::ParserError.

Kosa8.resolve_endpoint

Kosa8.resolve_endpoint(host: nil, socket_path: nil, token: nil, insecure: false, env: ENV)  # => Kosa8::Endpoint

Where to connect, by the kosa8 CLI's rules:

  1. The address is host if it is non-empty, else env["KOSA8_HOST"], stripped of spaces and trailing slashes. Empty: the unix socket at socket_path or Kosa8.default_socket_path.
  2. The scheme is lower-cased. unix:///path is a socket (and wins over socket_path). tcp:// is read as http://. No scheme (host:port) is read as http://. Any other scheme raises Kosa8::Error (KOSA8_HOST scheme "ftp" is not supported; use http://, https://, tcp:// or unix://). An address with no host name raises Kosa8::Error (KOSA8_HOST "…" names no host).
  3. Plain http to a host other than localhost, ::1 or 127.* raises Kosa8::Error (refusing to talk to 10.0.0.5:7440 over plain http: …) unless insecure or KOSA8_INSECURE=1.
  4. The token is token if non-empty; otherwise env["KOSA8_TOKEN"], but only when the address came from KOSA8_HOST, never with an explicit host:. A path in the URL is kept as a prefix for every request. https uses Net::HTTP's default certificate verification.

Kosa8::Endpoint is a Struct (keyword_init) with kind (:unix or :tcp), socket_path, scheme, host, port, base_path, token, and target: "unix:///path" or "scheme://host:port" (the port always shown, as in https://w.example.com:443).

Other module functions and classes

NameMeaning
Kosa8.default_socket_pathFile.join(Dir.home, ".kosa8", "run", "kosa8.sock")
Kosa8.loopback?(host)True for localhost, ::1 (with or without brackets) and 127.*.
Kosa8.enc(s)Percent-encodes one path segment; a space becomes %20 and / becomes %2F.
Kosa8.decode_frame(hash)Replaces a non-empty String "data" with its base64 decoding, in place, and returns the hash.
Kosa8.validate_sandbox_name(name)Returns name, or raises CheckpointError (invalid sandbox name "../etc").
Kosa8.parse_time(s)Time.iso8601(s), or Time.at(0) if it cannot be parsed. Used to order checkpoints.
Kosa8::UnixHTTPA Net::HTTP subclass that connects to socket_path instead of a TCP port. Requests go to host kosa8.
Kosa8::ChunkReaderAn IO-like over an Enumerable of string chunks (read(len = nil, outbuf = nil)), used for chunked write_stdin bodies.

Checkpoint stores

A store holds { "sandbox-name" => [checkpoint record, …] }. It needs two methods: load (returns the data) and update { |data| … } (runs the block on the data, saves what it leaves, and returns the block's value).

ClassBehaviour
Kosa8::FileCheckpointStore.new(path = nil, lock_timeout: 10)path and lock_timeout (readers). path defaults to ~/.kosa8/checkpoints.json, the CLI's own file. load returns {} when the file is missing or not valid JSON (other errors, such as Errno::EACCES, are raised as they are). update takes a Mutex in the process, creates the directory (mode 0700) if needed, takes an exclusive flock on checkpoints.json.lock (the CLI's lock, so the two never interleave), reloads, runs the block, writes pretty JSON to checkpoints.json.tmp (mode 0600) and renames it over the file. The lock is tried without blocking every 50 ms for up to lock_timeout seconds (since 0.1.1; 0.1.0 waited for ever), then CheckpointError is raised: timed out after 10s waiting for …/checkpoints.json.lock; another kosa8 process is holding it.
Kosa8::MemoryCheckpointStore.newKeeps stacks in memory, under a Mutex, deep-copied with Marshal on every load and update. Nothing is shared with the CLI or other processes, and nothing survives the process. Use it in tests.
Kosa8::CheckpointBook.new(store)The stack logic, as client.checkpoints. See below.
CheckpointBook methodReturns
next_id(sandbox)[id, evict]: the next "<sandbox>-ckpt-<n>", with n one past the larger of the stack's length and the highest number used, and the oldest ids to evict to stay at 10. Validates the name.
record(id, sandbox, label, evicted = [])The new record {"id", "sandbox", "label", "created"}, appended after removing evicted. created is Time.now.utc.iso8601(9). No auto key is written. Validates the name.
list(sandbox)Records sorted by created, oldest first (ties keep file order).
latest(sandbox)The newest record, or raises CheckpointError.
resolve(sandbox, ref)The newest record whose id equals ref or whose label equals it ignoring case, or raises CheckpointError.
pop(sandbox, id)Keeps the stack up to and including id; returns the ids removed after it.
forget(sandbox)Removes the sandbox's stack; returns its ids.
CheckpointBook.seq(id, sandbox)The number in "<sandbox>-ckpt-<n>", or 0.

Errors

StandardError
└─ Kosa8::Error
   ├─ Kosa8::ConnectionError
   ├─ Kosa8::TimeoutError
   ├─ Kosa8::StreamError              #stdout, #stderr
   ├─ Kosa8::CheckpointError
   ├─ Kosa8::ProtocolError            a frame that is not NDJSON (since 0.1.1)
   └─ Kosa8::ApiError                 any HTTP error status without a closer class (500, 503, …)
      ├─ Kosa8::BadRequestError       400
      ├─ Kosa8::AuthError             401
      ├─ Kosa8::PolicyError           403
      │  └─ Kosa8::ApprovalRequiredError   403 with an approval refusal; #approval
      ├─ Kosa8::NotFoundError         404
      ├─ Kosa8::ConflictError         409
      └─ Kosa8::UnsupportedError      501
ReaderOnMeaning
messageallFor a daemon error: "<error> (<remediation>)", as the CLI prints it.
statusApiError and belowThe HTTP status (Integer); nil elsewhere.
daemon_messagedaemon errors, stream error framesThe daemon's own text.
remediationdaemon errorsThe daemon's advice, when it gave one.
http_method, pathdaemon, connection, timeout and protocol errorsThe request, such as "GET" and "/kosa8/v1/health". Named http_method since 0.1.1; in 0.1.0 it was method, which hid Ruby's Object#method. The constructor keyword is still method:.
bodydaemon errorsThe parsed response body.
stdout, stderrStreamErrorOutput produced before the failure (UTF-8 Strings).
approvalApprovalRequiredErrorThe daemon's refusal hash: why and its other fields.
Kosa8::Error.new(message = nil, status: nil, daemon_message: nil, remediation: nil, method: nil, path: nil, body: nil)
Kosa8::ApprovalRequiredError.new(message = nil, approval: {}, **kw)
Kosa8::StreamError.new(message = nil, stdout: "", stderr: "", **kw)

Kosa8.error_from_response(status, body, method:, path:)  # => the typed error (not raised)

error_from_response builds the error the client raises for an HTTP status of 400 or more. daemon_message is the body's error if it is a non-empty String, else its message; for a text body, the stripped text. remediation is the body's non-empty remediation. The message is "<daemon_message> (<remediation>)", the daemon message alone, or "request failed with HTTP <status>". A 403 whose body has an approval hash with a String why becomes ApprovalRequiredError; other statuses use Kosa8::BY_STATUS, falling back to ApiError.

Kosa8::TimeoutError is the gem's own class, not Ruby's Timeout::Error, and does not inherit from it. Real messages and how to handle each class: Ruby errors and troubleshooting.