Module: Restless::Injection

Defined in:
lib/restless/injection.rb

Overview

CONTRACT.md section 10. What the SDK adds to the customer's own responses.

Class Method Summary collapse

Class Method Details

.apply_body(body, content_type, debug) ⇒ Object

INJECT-003. Merge debug into the response body ONLY when the body is a JSON OBJECT (not an array, not a scalar) and the content type says JSON.

Returns the original body untouched on any parse failure: SAFETY-001 outranks everything here.



98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
# File 'lib/restless/injection.rb', line 98

def apply_body(body, content_type, debug)
  return body if body.nil? || body.empty? || debug.nil?
  return body unless Text.full_lower(content_type.to_s).include?("application/json")

  begin
    parsed = JSON.parse(body, max_nesting: false)
  rescue StandardError
    return body
  end
  return body unless parsed.is_a?(Hash)

  JSON.generate(parsed.merge("debug" => debug))
rescue StandardError
  body
end

.build(status:, request_id:, prefix: nil, recovery: nil, method: nil, path: nil, portal_url: nil) ⇒ Object

INJECT-001..004, INJECT-006. Returns the headers to set plus the debug object to merge into a JSON body (nil when there is nothing to merge).

portal_url is the project's public portal origin, published by the server. It is NOT the ingest base URL, which serves /v1/* and would 404 both paths, and there is deliberately no fallback to it: with no portal origin we emit x-debug alone. A caller cannot tell a broken URL from a missing one, and one fetched 404 teaches an agent to stop following the link.



54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
# File 'lib/restless/injection.rb', line 54

def build(status:, request_id:, prefix: nil, recovery: nil,
          method: nil, path: nil, portal_url: nil)
  hdrs = headers(request_id: request_id, prefix: prefix, portal_url: portal_url)

  # INJECT-001. The body object is 4xx/5xx only: a successful body is the
  # caller's data, not ours to reshape. With no portal origin there is no
  # URL to put in one either (INJECT-006).
  return { headers: hdrs, debug: nil } if status < 400 || portal_url.nil? || portal_url.empty?

  log_url = hdrs["x-log-url"]
  debug_cmd = hdrs["x-debug"]

  # Per-request "dig-in" URL the calling agent (often an AI) can fetch for
  # concrete next steps. Deliberately LEGIBLE: it ends in `<slug>.md` so it
  # reads as documentation rather than a tracking blob. The first segment
  # is the same public request id already in `debug.log`, so the dashboard
  # can correlate the follow-up without any new tracking token.
  slug = recovery_slug(method, path)
  dig_in = "For the accepted parameters and next steps, " \
           "fetch #{portal_url}/p/#{request_id}/#{slug}.md"
  # INJECT-004: a cached recovery message precedes the dig-in line,
  # separated by a blank line.
  recovery_text =
    if recovery.nil? || recovery.empty?
      dig_in
    else
      "#{recovery}\n\n#{dig_in}"
    end

  {
    headers: hdrs,
    debug: {
      "log" => log_url,
      "cli" => debug_cmd,
      "recovery" => recovery_text
    }
  }
end

.headers(request_id:, prefix: nil, portal_url: nil) ⇒ Object

INJECT-002. The debug response headers, which ship on every status.

x-log-url is omitted with no portal origin; x-debug carries no URL, so it always ships.



38
39
40
41
42
43
# File 'lib/restless/injection.rb', line 38

def headers(request_id:, prefix: nil, portal_url: nil)
  display = RequestId.format_request_id(request_id, prefix)
  out = { "x-debug" => "npx api debug #{display}" }
  out["x-log-url"] = "#{portal_url}/logs/#{request_id}" unless portal_url.nil? || portal_url.empty?
  out
end

.recovery_slug(method = nil, path = nil) ⇒ Object

INJECT-005. Legible URL slug for the recovery dig-in path, derived from method + route pattern: GET /car/{id} becomes get-car-id.

The server resolves it back to an OpenAPI operation by matching the same scheme, so this MUST stay in sync with the app's recoverySlug (INJECT-007).



19
20
21
22
23
24
25
26
27
28
29
30
31
32
# File 'lib/restless/injection.rb', line 19

def recovery_slug(method = nil, path = nil)
  m = Text.full_lower(method.to_s)
  # JavaScript's `trim` strips the PRIM-002 set; Ruby's `strip` strips a
  # different one in both directions.
  p = Text.ws_trim(path.to_s)
  return "unknown" if m.empty? || p.empty?

  flat = p.gsub(%r{[/{}:]+}, "-")
          .gsub(/[^a-zA-Z0-9\-]/, "")
          .gsub(/-+/, "-")
  # `\A` / `\z`, not `^` / `$`: Ruby's are line anchors (PRIM-005).
  flat = flat.sub(/\A-/, "").sub(/-\z/, "")
  flat.empty? ? m : "#{m}-#{flat}"
end