- S1: Client#inspect redacts @token (default ruby inspect walks ivars); to_s aliased and pretty_print overridden so PP doesn't bypass it. - S2: AppToken#inspect/#to_s/#pretty_print redact :token member; nil token still rendered as token=nil for list-row clarity. - S3: validate token name vs [a-z0-9_-]+ in revoke_token and create_token; drops URI.encode_www_form_component path-encoding dependency. - C1: upload_timeout_secs parameter on upload_file (default 60), decoupled from default_timeout/http_timeout_margin so big uploads aren't capped by the run-subprocess timeout. - Q6: clearer multipart filename escape via gsub block form. - C7: dropped unused @uri ivar. - A3: YARD note clarifying http_client: bypasses base_url host/port routing. Test gaps closed: Client/AppToken inspect+pp redaction, AppToken nil-token inspect, revoke_token name validation (path traversal, uppercase, empty, valid), create_token name validation, upload_timeout_secs independence from default_timeout (incl. default==60), Array-form ip_cidrs round-trip, non-JSON 5xx error body kept as String, empty 200 body raises Error. 35 runs / 104 assertions / 0 failures. Audit: memory/clawdforge-audits/ruby-b1d6e3f.md
85 lines
2.8 KiB
Ruby
85 lines
2.8 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
module Clawdforge
|
|
# Result of a successful `POST /run`.
|
|
#
|
|
# `result` is whatever clawdforge parsed out of `claude -p --output-format
|
|
# json`: a Hash if the model returned valid JSON, otherwise the raw String.
|
|
# `ok` is always true for a RunResult — failures raise APIError instead.
|
|
RunResult = Struct.new(:ok, :result, :duration_ms, :stop_reason, keyword_init: true) do
|
|
def self.from_response(payload)
|
|
new(
|
|
ok: payload.fetch("ok", true) ? true : false,
|
|
result: payload["result"],
|
|
duration_ms: payload.fetch("duration_ms", 0).to_i,
|
|
stop_reason: payload["stop_reason"],
|
|
)
|
|
end
|
|
end
|
|
|
|
# Result of a successful `POST /files`. Pass `file_token` to
|
|
# `Client#run(files: [...])` to attach the upload to a prompt.
|
|
FileToken = Struct.new(:file_token, :ttl_secs, :size, keyword_init: true) do
|
|
def self.from_response(payload)
|
|
new(
|
|
file_token: payload.fetch("file_token"),
|
|
ttl_secs: payload.fetch("ttl_secs").to_i,
|
|
size: payload.fetch("size").to_i,
|
|
)
|
|
end
|
|
end
|
|
|
|
# A row from `GET /admin/tokens` or the result of `POST /admin/tokens`.
|
|
# `token` is the plaintext bearer string, returned ONLY at create time.
|
|
# On list responses it is nil; the server stores only a sha256 hash.
|
|
AppToken = Struct.new(
|
|
:name, :token, :ip_cidrs, :created_at, :last_used, :enabled,
|
|
keyword_init: true,
|
|
) do
|
|
def self.from_create_response(payload)
|
|
new(
|
|
name: payload.fetch("name"),
|
|
token: payload["token"],
|
|
ip_cidrs: Array(payload["ip_cidrs"]),
|
|
created_at: nil,
|
|
last_used: nil,
|
|
enabled: true,
|
|
)
|
|
end
|
|
|
|
def self.from_list_row(row)
|
|
raw = row["ip_cidrs"]
|
|
cidrs =
|
|
case raw
|
|
when String then raw.split(",").reject(&:empty?)
|
|
when Array then raw.dup
|
|
else []
|
|
end
|
|
new(
|
|
name: row.fetch("name"),
|
|
token: nil,
|
|
ip_cidrs: cidrs,
|
|
created_at: row["created_at"],
|
|
last_used: row["last_used"],
|
|
enabled: row.fetch("enabled", 1).to_i != 0,
|
|
)
|
|
end
|
|
|
|
# Override the Struct-autogenerated `inspect`/`to_s`, which would dump
|
|
# every member including the plaintext `:token` from create-time
|
|
# responses. We still want to see whether a token field is *present* —
|
|
# useful when distinguishing create-response rows from list rows —
|
|
# without leaking the bearer itself.
|
|
def inspect
|
|
tok_state = self[:token].nil? ? "nil" : "[REDACTED]"
|
|
"#<#{self.class} name=#{self[:name].inspect} token=#{tok_state} " \
|
|
"ip_cidrs=#{self[:ip_cidrs].inspect} created_at=#{self[:created_at].inspect} " \
|
|
"last_used=#{self[:last_used].inspect} enabled=#{self[:enabled].inspect}>"
|
|
end
|
|
alias_method :to_s, :inspect
|
|
|
|
def pretty_print(pp)
|
|
pp.text(inspect)
|
|
end
|
|
end
|
|
end
|