Drive SBOM Desk from your own code
Everything the web page does is available over HTTP. The difference from the page is where the SBOM
gets read. In the browser a real CycloneDX / SPDX reader parses the document locally and derives a
compact text digest; only that digest and a set of prescan flags are sent to the
model. Over the API the contract is the same: you send the digest, never the whole
document. The natural uses are a release gate that runs the conformance lane on every
build and fails when the publish decision regresses to hold, a nightly job that
re-rules the licence position after a dependency bump, and a script that drafts the customer
statement straight from the gate it just ran.
The task field decides everything else
SBOM Desk is one endpoint with four lanes. task is the router: it selects the section
of the system prompt that applies, the output contract, the credit hold and the progress markers.
It is the first field you should decide and the first field in every example on this page. A missing
or unrecognised task does not fail the run — the model picks the closest lane,
names it in lane, and says which it chose in the opening sentence of
exec_summary — but never rely on that. Send one of the four ids.
| task | the question it answers | keys it adds to the common envelope |
|---|---|---|
conformance |
Is this document fit to publish, for the audience you named? Twelve named checks and the seven NTIA minimum elements, each decided in the browser first and then re-read for that audience. | checks[] — exactly twelve, in a fixed order — ntia[]
(exactly seven, N1..N7) and publish_gate. |
licenses |
What is the licence position of this inventory, against your policy and the way you ship? One ruling per distinct licence expression, not one per component. | rulings[], obligations[], notice_gaps[],
policy_exceptions[]. |
triage |
Where should a real scanner be pointed first? Exposure reasoning from graph position, pinning and identifier coverage — never an invented CVE list. | triage[], blind_spots[], monitoring[],
embedded_vulnerabilities. |
remediation |
What is the fix plan, and what goes to the customer? Ordered steps with real commands plus a
sendable statement. Runs best on a lane you already have, passed in source. |
steps[], verification[], statement,
residual[]. |
Every lane returns the same common envelope — lane,
title, posture, verdict, headline,
exec_summary, findings, reconciliation,
assumptions, open_questions, summary — and then adds
its own keys on top. A lane never blends another lane's contract into its reply, so you can switch
on lane and trust the shape.
Two lanes over the same document are two runs. They are priced separately, held
separately and billed separately; the app's Idempotency-Key carries the lane id for
exactly this reason. There is no combined call that returns all four.
One worked request per lane
These are the four bodies as the app itself submits them, with the digest abbreviated — the
real sbom_digest runs to a few thousand characters and is capped at 26,000. The full
input object is documented field by field in step 4.
conformance — a container image going to a customer's security team:
{
"task": "conformance",
"distribution": "container",
"policy": "permissive",
"audience": "customer-security",
"context": "Generated by cdxgen in CI with default flags. We ship this as a container image.",
"sbom_digest": "SBOM DIGEST - computed in the browser from the whole parsed document\n\n## Document\nformat: CycloneDX 1.5\n... (see step 4)",
"prescan_facts": {
"flags": [
{"id": "F-NO-SUPPLIER", "severity": "high",
"title": "13 components have no supplier",
"detail": "NTIA minimum element 1. This is the element SBOM generators most often skip."}
],
"checks": [
{"id": "C-FORMAT", "name": "Format and specification version",
"status": "pass", "evidence": "CycloneDX 1.5"}
],
"ntia": [
{"id": "N1", "element": "Supplier name", "status": "fail",
"evidence": "1 of 14 components (7%)"}
],
"stats": {"components": 14, "supplier_coverage": 7, "licence_coverage": 93, "edges": 9}
}
}
licenses — the same inventory under a permissive-only policy. Note there is no checks or ntia block: those are sent only for the conformance lane.
{
"task": "licenses",
"distribution": "container",
"policy": "permissive",
"audience": "customer-security",
"context": "Our written policy is permissive licences only.",
"sbom_digest": "... the same digest ...",
"prescan_facts": {
"flags": [
{"id": "F-COPYLEFT", "severity": "medium",
"title": "1 component under strong copyleft terms",
"detail": "Distributing a binary that links these obliges you to offer corresponding source."}
],
"stats": {
"components": 14, "unlicensed": 1, "deprecated_licences": 1,
"licence_classes": {"permissive": 9, "strong-copyleft": 1, "source-available": 2,
"unrecognised": 1, "unknown": 1}
}
}
}
triage — same shape again; the lane is the only difference:
{
"task": "triage",
"distribution": "container",
"policy": "permissive",
"audience": "customer-security",
"context": "",
"sbom_digest": "... the same digest ...",
"prescan_facts": {
"flags": [{"id": "F-NO-ID", "severity": "high",
"title": "1 component has no purl and no CPE",
"detail": "Without a machine-readable coordinate this row can only be matched by name."}],
"stats": {"components": 14, "identifier_coverage": 93, "edges": 9, "max_depth": 2,
"unreachable": 5, "embedded_vulnerabilities": 0}
}
}
remediation — with a previous lane's output carried in
source. That field is optional; without it the plan is written from the prescan alone,
which is a legitimate case the prompt covers.
{
"task": "remediation",
"distribution": "container",
"policy": "permissive",
"audience": "customer-security",
"context": "cdxgen 10.4.3, default flags, GitHub Actions.",
"sbom_digest": "... the same digest ...",
"source": "Lane: conformance\nPosture: conditional\nVerdict: ...\n\nFindings:\n- [high] 13 components have no supplier ...",
"prescan_facts": {
"flags": [{"id": "F-NO-SUPPLIER", "severity": "high", "title": "...", "detail": "..."}],
"stats": {"components": 14, "supplier_coverage": 7}
}
}
The envelope and the error codes
Every response from https://api.skillsafe.ai/v1/app-api is one of two shapes:
{"ok": true, "data": { ... }}
{"ok": false, "error": {"code": "...", "message": "...", "details": { ... }}}
| code | HTTP | what it means and what to do |
|---|---|---|
unauthorized | 401 | No token, a malformed token, or one that has expired. Mint a new one (step 2). |
forbidden | 403 | A guest token on a metered path. /me and /estimate work as a guest;
/run and /run-stream need a personal token. |
payment_required | 402 | Balance below min_credits for this lane. Call /estimate first and
compare against /me — a 402 after submit is a bug in your client, not in
the service. |
validation_error | 400 | The body was not accepted. Note that /estimate performs no body
validation at all, so it will happily price a malformed body — see the warning
in step 4. |
rate_limited | 429 | Shared limit. Back off with jitter; never tight-loop. |
not_found | 404 | A job id that does not exist, or one belonging to a different subject. |
internal | 500 | Retry once with the same Idempotency-Key. A failed run is not billed. |
1. A tiny client helper
Three things never change: the base URL, the bearer token, and reading ok before
touching data. Everything after this step assumes the helper below.
# Every call is the same three things: the base URL, your bearer token,
# and a JSON body. Keep the token in an environment variable your shell
# already holds rather than pasting it into a script.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from /tokens.html, or step 2 below
call() { # call <path> [json-body]
if [ -n "$2" ]; then
curl -sS -X POST "$BASE/$1" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d "$2"
else
curl -sS "$BASE/$1" -H "Authorization: Bearer $TOKEN"
fi
}
# ok is the first thing to read; data is only meaningful when it is true.
call me | python3 -c 'import sys,json; r=json.load(sys.stdin); print(r["data"] if r["ok"] else r["error"])'
import json, time, urllib.request, urllib.error
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
class SbomDeskError(RuntimeError):
def __init__(self, code, message, details=None, status=None):
super().__init__("%s: %s" % (code, message))
self.code, self.message, self.details, self.status = code, message, details or {}, status
def call(path, body=None, headers=None, token=None):
"""POST when body is given, GET otherwise. Raises on ok=false."""
url = "%s/%s" % (BASE, path)
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(url, data=data, method="POST" if data else "GET")
req.add_header("Authorization", "Bearer %s" % (token or TOKEN))
if data:
req.add_header("Content-Type", "application/json")
for k, v in (headers or {}).items():
req.add_header(k, v)
try:
with urllib.request.urlopen(req, timeout=180) as r:
payload = json.loads(r.read())
status = r.status
except urllib.error.HTTPError as e:
payload = json.loads(e.read() or b"{}")
status = e.code
if not payload.get("ok"):
err = payload.get("error") or {}
raise SbomDeskError(err.get("code", "unknown"), err.get("message", "request failed"),
err.get("details"), status)
return payload["data"]
print(call("me"))
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
class SbomDeskError extends Error {
constructor(code, message, details, status) {
super(`${code}: ${message}`);
Object.assign(this, { code, details: details || {}, status });
}
}
async function call(path, body, headers) {
const res = await fetch(`${BASE}/${path}`, {
method: body === undefined ? "GET" : "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
...(body === undefined ? {} : { "Content-Type": "application/json" }),
...(headers || {})
},
body: body === undefined ? undefined : JSON.stringify(body)
});
const payload = await res.json().catch(() => ({}));
if (!payload.ok) {
const e = payload.error || {};
throw new SbomDeskError(e.code || "unknown", e.message || "request failed", e.details, res.status);
}
return payload.data;
}
console.log(await call("me"));
package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
)
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from /tokens.html, or step 2 below
type envelope struct {
OK bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct {
Code string `json:"code"`
Message string `json:"message"`
Details json.RawMessage `json:"details"`
} `json:"error"`
}
var client = &http.Client{Timeout: 180 * time.Second}
// call POSTs when body is non-nil, GETs otherwise. Returns data or an error.
func call(path string, body any, headers map[string]string) (json.RawMessage, error) {
method := http.MethodGet
var rdr io.Reader
if body != nil {
method = http.MethodPost
b, err := json.Marshal(body)
if err != nil {
return nil, err
}
rdr = bytes.NewReader(b)
}
req, err := http.NewRequest(method, base+"/"+path, rdr)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
for k, v := range headers {
req.Header.Set(k, v)
}
res, err := client.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var env envelope
if err := json.NewDecoder(res.Body).Decode(&env); err != nil {
return nil, err
}
if !env.OK {
if env.Error != nil {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return nil, fmt.Errorf("request failed with status %d", res.StatusCode)
}
return env.Data, nil
}
func main() {
data, err := call("me", nil, nil)
if err != nil {
panic(err)
}
fmt.Println(string(data))
}
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
import java.util.Map;
public class SbomDesk {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static final HttpClient http = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(20)).build();
/** POSTs when body is non-null, GETs otherwise. Returns the raw JSON response text. */
static String call(String path, String jsonBody, Map<String, String> headers) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + "/" + path))
.timeout(Duration.ofSeconds(180))
.header("Authorization", "Bearer " + token);
if (jsonBody == null) {
b.GET();
} else {
b.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(jsonBody));
}
if (headers != null) headers.forEach(b::header);
HttpResponse<String> res = http.send(b.build(), HttpResponse.BodyHandlers.ofString());
// Read "ok" before touching "data" - use your own JSON library here.
if (!res.body().contains("\"ok\":true")) {
throw new RuntimeException("request failed (" + res.statusCode() + "): " + res.body());
}
return res.body();
}
public static void main(String[] args) throws Exception {
System.out.println(call("me", null, null));
}
}
require "json"
require "net/http"
require "uri"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from /tokens.html, or step 2 below
class SbomDeskError < StandardError
attr_reader :code, :details, :status
def initialize(code, message, details, status)
super("#{code}: #{message}")
@code, @details, @status = code, details || {}, status
end
end
# POSTs when body is given, GETs otherwise. Raises on ok=false.
def call(path, body = nil, headers = {})
uri = URI("#{BASE}/#{path}")
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
if body
req["Content-Type"] = "application/json"
req.body = JSON.generate(body)
end
headers.each { |k, v| req[k] = v }
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 180) do |h|
h.request(req)
end
payload = JSON.parse(res.body) rescue {}
unless payload["ok"]
e = payload["error"] || {}
raise SbomDeskError.new(e["code"] || "unknown", e["message"] || "request failed",
e["details"], res.code.to_i)
end
payload["data"]
end
pp call("me")
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
class SbomDeskError extends RuntimeException {
public function __construct(
public string $code,
string $message,
public array $details = [],
public int $status = 0
) { parent::__construct("$code: $message"); }
}
/** POSTs when $body is given, GETs otherwise. Throws on ok=false. */
function call(string $path, ?array $body = null, array $headers = []): array {
global $TOKEN;
$h = ["Authorization: Bearer $TOKEN"];
foreach ($headers as $k => $v) { $h[] = "$k: $v"; }
$ch = curl_init(BASE . "/" . $path);
curl_setopt_array($ch, [CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 180]);
if ($body !== null) {
$h[] = "Content-Type: application/json";
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
curl_setopt($ch, CURLOPT_HTTPHEADER, $h);
$raw = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
$payload = json_decode($raw ?: "{}", true) ?: [];
if (empty($payload["ok"])) {
$e = $payload["error"] ?? [];
throw new SbomDeskError($e["code"] ?? "unknown", $e["message"] ?? "request failed",
$e["details"] ?? [], $status);
}
return $payload["data"];
}
print_r(call("me"));
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
public static class SbomDesk {
const string Base = "https://api.skillsafe.ai/v1/app-api";
static string Token = "YOUR_TOKEN"; // from /tokens.html, or step 2 below
static readonly HttpClient Http = new HttpClient {
Timeout = TimeSpan.FromSeconds(180)
};
/// POSTs when jsonBody is non-null, GETs otherwise. Returns the "data" element.
public static async Task<JsonElement> Call(string path, string jsonBody = null,
(string, string)[] headers = null) {
var req = new HttpRequestMessage(
jsonBody == null ? HttpMethod.Get : HttpMethod.Post, $"{Base}/{path}");
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
if (jsonBody != null)
req.Content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
if (headers != null)
foreach (var (k, v) in headers) req.Headers.TryAddWithoutValidation(k, v);
var res = await Http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.TryGetProperty("ok", out var ok) || !ok.GetBoolean()) {
var e = doc.RootElement.GetProperty("error");
throw new Exception($"{e.GetProperty("code")}: {e.GetProperty("message")}");
}
return doc.RootElement.GetProperty("data");
}
public static async Task Main() => Console.WriteLine(await Call("me"));
}
2. Get a token
The shortest path is /tokens.html — it shows the token this browser
already holds for sbom-desk, with a Reveal button and a copy-as-shell-export button, so
you never open the developer console. For an unattended job, mint a guest token: it
is enough for /me and /estimate, which is enough to build a release gate
that prices a lane and checks the balance without ever spending a credit. Running a lane needs a
personal token, which comes from signing in.
# A guest token is enough for /me and /estimate.
BASE="https://api.skillsafe.ai/v1/app-api"
curl -sS -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"sbom-desk"}'
# -> {"ok":true,"data":{"token":"aut_...","subject_type":"guest"}}
# Keep it in a shell variable. Never commit it; never echo it into a log.
TOKEN=$(curl -sS -X POST "$BASE/guest" -H "Content-Type: application/json" \
-d '{"slug":"sbom-desk"}' | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["token"])')
def guest_token():
"""Enough for /me and /estimate. Running a lane needs a personal token."""
data = call("guest", {"slug": "sbom-desk"}, token="none")
return data["token"]
TOKEN = guest_token()
print("subject:", call("me", token=TOKEN)["subject_type"])
async function guestToken() {
// Enough for /me and /estimate. Running a lane needs a personal token.
const res = await fetch(`${BASE}/guest`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "sbom-desk" })
});
const payload = await res.json();
if (!payload.ok) throw new Error("could not mint a guest token");
return payload.data.token;
}
TOKEN = await guestToken();
console.log("subject:", (await call("me")).subject_type);
// guestToken mints a guest token, which is enough for /me and /estimate.
func guestToken() (string, error) {
saved := token
token = "none"
data, err := call("guest", map[string]string{"slug": "sbom-desk"}, nil)
token = saved
if err != nil {
return "", err
}
var out struct{ Token string `json:"token"` }
if err := json.Unmarshal(data, &out); err != nil {
return "", err
}
return out.Token, nil
}
/** Mints a guest token: enough for /me and /estimate, not for running a lane. */
static String guestToken() throws Exception {
String saved = token;
token = "none";
String body = call("guest", "{\"slug\":\"sbom-desk\"}", null);
token = saved;
// Extract data.token with your JSON library.
int i = body.indexOf("\"token\":\"") + 9;
return body.substring(i, body.indexOf('"', i));
}
# Enough for /me and /estimate. Running a lane needs a personal token.
def guest_token
uri = URI("#{BASE}/guest")
res = Net::HTTP.post(uri, JSON.generate({slug: "sbom-desk"}),
"Content-Type" => "application/json")
JSON.parse(res.body).dig("data", "token")
end
TOKEN_GUEST = guest_token
<?php
/** Enough for /me and /estimate. Running a lane needs a personal token. */
function guest_token(): string {
$ch = curl_init(BASE . "/guest");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => ["Content-Type: application/json"],
CURLOPT_POSTFIELDS => json_encode(["slug" => "sbom-desk"]),
]);
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
return $payload["data"]["token"];
}
$TOKEN = guest_token();
/// Enough for /me and /estimate. Running a lane needs a personal token.
public static async Task<string> GuestToken() {
var req = new HttpRequestMessage(HttpMethod.Post, $"{Base}/guest") {
Content = new StringContent("{\"slug\":\"sbom-desk\"}",
Encoding.UTF8, "application/json")
};
var res = await Http.SendAsync(req);
var doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
return doc.RootElement.GetProperty("data").GetProperty("token").GetString();
}
3. /me — who you are and what you can afford
Free, and the only place a balance comes from. The response carries exactly three fields:
subject_type (user or guest), subject_id and
credits. Signed in means subject_type == "user" —
there is no username and no email here, so do not test for one.
{"ok": true, "data": {"subject_type": "user", "subject_id": "usr_...", "credits": 184320}}
Pair it with /estimate before every run: compare credits against the
lane's min_credits and hold_credits. A 402 after submit means your client
skipped this step.
BASE="https://api.skillsafe.ai/v1/app-api"
curl -sS "$BASE/me" -H "Authorization: Bearer $TOKEN"
# -> {"ok":true,"data":{"subject_type":"user","subject_id":"usr_...","credits":184320}}
# A release gate that needs a real account:
SUBJECT=$(curl -sS "$BASE/me" -H "Authorization: Bearer $TOKEN" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["subject_type"])')
[ "$SUBJECT" = "user" ] || { echo "guest token cannot run a lane"; exit 1; }
me = call("me")
signed_in = me["subject_type"] == "user" # not me.get("username")
print("credits:", me["credits"], "signed in:", signed_in)
if not signed_in:
raise SystemExit("a guest token can price a lane but cannot run one")
const me = await call("me");
const signedIn = me.subject_type === "user"; // not me.username
console.log("credits:", me.credits, "signed in:", signedIn);
if (!signedIn) throw new Error("a guest token can price a lane but cannot run one");
type Me struct {
SubjectType string `json:"subject_type"`
SubjectID string `json:"subject_id"`
Credits int64 `json:"credits"`
}
func whoAmI() (Me, error) {
var me Me
data, err := call("me", nil, nil)
if err != nil {
return me, err
}
err = json.Unmarshal(data, &me)
return me, err
}
// signedIn := me.SubjectType == "user"
// /me returns exactly subject_type, subject_id and credits.
// Signed in means subject_type equals "user".
static boolean signedIn() throws Exception {
return call("me", null, null).contains("\"subject_type\":\"user\"");
}
me = call("me")
signed_in = me["subject_type"] == "user" # not me["username"]
puts "credits: #{me["credits"]}, signed in: #{signed_in}"
abort "a guest token can price a lane but cannot run one" unless signed_in
<?php
$me = call("me");
$signedIn = ($me["subject_type"] ?? "") === "user"; // not $me["username"]
printf("credits: %d, signed in: %s\n", $me["credits"], $signedIn ? "yes" : "no");
if (!$signedIn) { exit("a guest token can price a lane but cannot run one\n"); }
var me = await Call("me");
var signedIn = me.GetProperty("subject_type").GetString() == "user";
Console.WriteLine($"credits: {me.GetProperty("credits").GetInt64()}, signed in: {signedIn}");
if (!signedIn) throw new Exception("a guest token can price a lane but cannot run one");
4. Price the lane — free
POST /estimate with the run input returns model
(gpt-5.6-terra), model_alias (gpt-terra),
markup_bps, hold_credits, min_credits and
sponsor_enabled. No job is created and nothing is billed.
The hold differs per lane, because the prompt sections and output caps differ, so
re-estimate when you change task. The hold is a reservation priced against the full
output cap; the settled figure is usually far lower.
Warning — /estimate validates nothing. A bare string, a number,
null and [] all return ok: true with a well-formed estimate
and a correct model binding. So a successful estimate proves the app, the token and the model
binding — and says nothing whatever about whether your body is the right shape. There is no
server-side signal for that, ever. Assert client-side that you are sending an object with a
task string before you spend anything; the web app ships exactly that guard.
The run input, field by field
| field | type | what it does |
|---|---|---|
task | string, required | conformance | licenses | triage |
remediation. The router. |
distribution | string | How the software reaches its users, and the single field that changes the most answers:
saas, container, on-prem, library,
embedded, internal. A strong-copyleft component is a live obligation
in an on-premise binary and a much narrower one in a SaaS backend; an AGPL component is a live
obligation in both, because network copyleft triggers on network use. |
policy | string | The licence allow-list to rule against: permissive,
permissive-weak, osi, case-by-case. |
audience | string | Who receives the document: customer-security, questionnaire,
federal, cra, release-gate. Drives the publish
gate. |
context | string, up to 8,000 chars | Which generator produced the document and with what flags, whether the product is actually distributed, how the copyleft components are linked, what the customer asked for. Optional, and it changes the rulings. |
sbom_digest | string, required, up to 26,000 chars | The derived digest, never the document. Format below. |
prescan_facts | object, required | {flags[], checks[], ntia[], stats{}}. checks and ntia
are sent only for the conformance lane. Every flag id you send must come back
reconciled exactly once. |
source | string, optional, up to 12,000 chars | A previous lane's output, for the remediation lane to plan from. Omit it and the
plan is written from the prescan alone. |
retry_note | string, optional | Set only by the app's one automatic reformat retry, which reuses the same idempotency base so a malformed first reply cannot double-bill. |
sbom_digest: what to send instead of the document
The digest is plain text with named sections. Produce it however you like — the app's own
reader is in /bomscan.js and BomScan.digest(BomScan.scan(text)) returns
it — but keep the section names and the two rules the prompt depends on: every count in it is
computed over the whole document, and the sample block ends with a
sample_completeness: line reading COMPLETE or PARTIAL. That
line is what tells the model whether counting over the sampled rows is legitimate. Drop it and the
model will happily total a subset and present it as the inventory.
SBOM DIGEST - computed in the browser from the whole parsed document
## Document
format: CycloneDX 1.5
detected_as: cyclonedx-json
name: orders-api
identifier: urn:uuid:6f2a1c94-3b7e-4d21-9c58-0e1f4a7b3d6e
document_version: 1
created: 2026-05-04T09:12:44Z
authored_by_tools: cdxgen 10.4.3
authored_by_people: (none)
data_licence: (none)
subject: pkg:npm/orders-api@3.2.0 version 3.2.0
input_truncated: no
## Inventory (whole document)
components: 14
named: 14 (100%)
versioned: 13 (93%), of which 1 are a range or placeholder
with_purl: 14 (100%), parsing: 13, malformed: 1
with_cpe: 0
with_any_identifier: 14 (100%)
with_supplier: 1 (7%)
with_licence: 13 (93%), unlicensed: 1
with_checksum: 2 (14%), sha-256 or better: 1, weak only: 1
with_copyright: 0 (0%)
with_download_location: 0 (0%)
nested_components: 0
duplicate_coordinates: 0
## Dependency graph (whole document)
edges: 9, max_depth: 2
reachable_from_subject: 9 of 14 (64%)
unreachable: 5, isolated: 4, dangling_references: 1
## By ecosystem (whole document)
JavaScript: 10
Java: 1
...
## Licence classes (whole document)
permissive: 9
source-available: 2
strong-copyleft: 1
...
## Licence identifiers present (whole document, top 30)
MIT: 7
GPL-2.0: 1
SSPL-1.0: 1
BUSL-1.1: 1
MIT OR Apache-2.0: 1
BSD: 1
ISC: 1
## NTIA minimum elements (computed locally)
N1 Supplier name: FAIL - 1 of 14 components (7%)
N2 Component name: PASS - 14 of 14 components (100%)
... N3 through N7 ...
## Conformance checks (computed locally)
C-FORMAT Format and specification version: PASS - CycloneDX 1.5
... the other eleven ...
## Prescan flags (computed locally - reconcile every one of these)
F-NO-SUPPLIER [high] 13 components have no supplier :: express@4.19.2, body-parser@1.20.2, ...
... the rest ...
## Sample component rows
sample_size: 14 of 14
- express@4.19.2 | type=library | purl=pkg:npm/express@4.19.2 | licence=MIT[permissive] | supplier=- | hash=strong
...
sample_completeness: COMPLETE
When the sample is a subset the block instead ends with
sample_completeness: PARTIAL followed by an explicit instruction not to aggregate over
those rows. Keep that sentence: it is load-bearing.
# sbom_digest is the DERIVED digest, never the document itself.
BASE="https://api.skillsafe.ai/v1/app-api"
cat > /tmp/sbom-desk-input.json <<'JSON'
{
"task": "conformance",
"distribution": "container",
"policy": "permissive",
"audience": "customer-security",
"context": "cdxgen 10.4.3, default flags, GitHub Actions.",
"sbom_digest": "SBOM DIGEST - computed in the browser from the whole parsed document\n\n## Document\nformat: CycloneDX 1.5\n...",
"prescan_facts": {"flags": [], "checks": [], "ntia": [], "stats": {"components": 14}}
}
JSON
curl -sS -X POST "$BASE/estimate" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
--data-binary @/tmp/sbom-desk-input.json
# -> {"ok":true,"data":{"model":"gpt-5.6-terra","model_alias":"gpt-terra",
# "markup_bps":1000,"hold_credits":...,"min_credits":...}}
# The hold differs per lane. Price the one you are about to run.
for LANE in conformance licenses triage remediation; do
python3 - "$LANE" <<'PY' > /tmp/sbom-desk-lane.json
import json, sys
body = json.load(open("/tmp/sbom-desk-input.json"))
body["task"] = sys.argv[1]
json.dump(body, open("/tmp/sbom-desk-lane.json", "w"))
PY
printf "%-12s " "$LANE"
curl -sS -X POST "$BASE/estimate" -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" --data-binary @/tmp/sbom-desk-lane.json \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["hold_credits"])'
done
LANES = ("conformance", "licenses", "triage", "remediation")
def must_be_object(body):
"""The ONLY place a malformed body can be caught: /estimate validates nothing."""
if not isinstance(body, dict):
raise ValueError("the run body must be an object, not %s" % type(body).__name__)
if body.get("task") not in LANES:
raise ValueError("task must be one of %s, got %r" % (LANES, body.get("task")))
if not body.get("sbom_digest"):
raise ValueError("sbom_digest is required - send the digest, not the document")
return body
def build_input(task, digest, prescan, *, distribution="container", policy="permissive",
audience="customer-security", context="", source=None):
body = {
"task": task,
"distribution": distribution,
"policy": policy,
"audience": audience,
"context": context,
"sbom_digest": digest,
"prescan_facts": prescan,
}
if source:
body["source"] = source
return must_be_object(body)
body = build_input("conformance", digest, prescan)
est = call("estimate", body)
assert est["model"] == "gpt-5.6-terra" and est["model_alias"] == "gpt-terra"
print("reserves", est["hold_credits"], "minimum", est["min_credits"])
# The hold differs per lane; never show lane A's hold for lane B.
for lane in LANES:
e = call("estimate", build_input(lane, digest, prescan))
print("%-12s %s" % (lane, e["hold_credits"]))
const LANES = ["conformance", "licenses", "triage", "remediation"];
// The ONLY place a malformed body can be caught: /estimate validates nothing.
function mustBeObject(body) {
if (!body || typeof body !== "object" || Array.isArray(body)) {
throw new Error("the run body must be an object");
}
if (!LANES.includes(body.task)) throw new Error(`task must be one of ${LANES}`);
if (!body.sbom_digest) throw new Error("sbom_digest is required - send the digest, not the document");
return body;
}
function buildInput(task, digest, prescan, opts = {}) {
const body = {
task,
distribution: opts.distribution || "container",
policy: opts.policy || "permissive",
audience: opts.audience || "customer-security",
context: opts.context || "",
sbom_digest: digest,
prescan_facts: prescan
};
if (opts.source) body.source = opts.source;
return mustBeObject(body);
}
const est = await call("estimate", buildInput("conformance", digest, prescan));
console.assert(est.model === "gpt-5.6-terra" && est.model_alias === "gpt-terra");
console.log("reserves", est.hold_credits, "minimum", est.min_credits);
for (const lane of LANES) {
const e = await call("estimate", buildInput(lane, digest, prescan));
console.log(lane.padEnd(12), e.hold_credits);
}
type RunInput struct {
Task string `json:"task"`
Distribution string `json:"distribution"`
Policy string `json:"policy"`
Audience string `json:"audience"`
Context string `json:"context"`
SbomDigest string `json:"sbom_digest"`
PrescanFacts map[string]any `json:"prescan_facts"`
Source string `json:"source,omitempty"`
}
type Estimate struct {
Model string `json:"model"`
ModelAlias string `json:"model_alias"`
MarkupBps int `json:"markup_bps"`
Hold int64 `json:"hold_credits"`
Min int64 `json:"min_credits"`
Sponsored bool `json:"sponsor_enabled"`
}
var lanes = []string{"conformance", "licenses", "triage", "remediation"}
// validate is the only place a malformed body can be caught: /estimate validates nothing.
func (in RunInput) validate() error {
ok := false
for _, l := range lanes {
if in.Task == l {
ok = true
}
}
if !ok {
return fmt.Errorf("task must be one of %v, got %q", lanes, in.Task)
}
if in.SbomDigest == "" {
return fmt.Errorf("sbom_digest is required - send the digest, not the document")
}
return nil
}
func estimate(in RunInput) (Estimate, error) {
var est Estimate
if err := in.validate(); err != nil {
return est, err
}
data, err := call("estimate", in, nil)
if err != nil {
return est, err
}
err = json.Unmarshal(data, &est)
return est, err
}
static final String[] LANES = {"conformance", "licenses", "triage", "remediation"};
/** The only place a malformed body can be caught: /estimate validates nothing. */
static void mustBeValid(String task, String digest) {
boolean ok = false;
for (String l : LANES) if (l.equals(task)) ok = true;
if (!ok) throw new IllegalArgumentException("task must be one of the four lane ids");
if (digest == null || digest.isEmpty())
throw new IllegalArgumentException("sbom_digest is required - send the digest, not the document");
}
/** Build the body with your JSON library; this shows the shape. */
static String estimate(String task, String digest, String prescanJson) throws Exception {
mustBeValid(task, digest);
String body = "{"
+ "\"task\":\"" + task + "\","
+ "\"distribution\":\"container\","
+ "\"policy\":\"permissive\","
+ "\"audience\":\"customer-security\","
+ "\"context\":\"\","
+ "\"sbom_digest\":" + jsonString(digest) + ","
+ "\"prescan_facts\":" + prescanJson
+ "}";
return call("estimate", body, null);
}
LANES = %w[conformance licenses triage remediation].freeze
# The only place a malformed body can be caught: /estimate validates nothing.
def must_be_object(body)
raise ArgumentError, "the run body must be a Hash" unless body.is_a?(Hash)
raise ArgumentError, "task must be one of #{LANES}" unless LANES.include?(body[:task])
raise ArgumentError, "sbom_digest is required" if body[:sbom_digest].to_s.empty?
body
end
def build_input(task, digest, prescan, distribution: "container", policy: "permissive",
audience: "customer-security", context: "", source: nil)
body = {task: task, distribution: distribution, policy: policy, audience: audience,
context: context, sbom_digest: digest, prescan_facts: prescan}
body[:source] = source if source
must_be_object(body)
end
est = call("estimate", build_input("conformance", digest, prescan))
raise "wrong model binding" unless est["model"] == "gpt-5.6-terra"
puts "reserves #{est["hold_credits"]}, minimum #{est["min_credits"]}"
LANES.each { |lane| puts format("%-12s %s", lane, call("estimate", build_input(lane, digest, prescan))["hold_credits"]) }
<?php
const LANES = ["conformance", "licenses", "triage", "remediation"];
/** The only place a malformed body can be caught: /estimate validates nothing. */
function must_be_object(array $body): array {
if (!in_array($body["task"] ?? null, LANES, true)) {
throw new InvalidArgumentException("task must be one of the four lane ids");
}
if (empty($body["sbom_digest"])) {
throw new InvalidArgumentException("sbom_digest is required - send the digest, not the document");
}
return $body;
}
function build_input(string $task, string $digest, array $prescan,
string $distribution = "container", string $policy = "permissive",
string $audience = "customer-security", string $context = "",
?string $source = null): array {
$body = ["task" => $task, "distribution" => $distribution, "policy" => $policy,
"audience" => $audience, "context" => $context,
"sbom_digest" => $digest, "prescan_facts" => $prescan];
if ($source !== null) { $body["source"] = $source; }
return must_be_object($body);
}
$est = call("estimate", build_input("conformance", $digest, $prescan));
assert($est["model"] === "gpt-5.6-terra" && $est["model_alias"] === "gpt-terra");
printf("reserves %d, minimum %d\n", $est["hold_credits"], $est["min_credits"]);
static readonly string[] Lanes = {"conformance", "licenses", "triage", "remediation"};
/// The only place a malformed body can be caught: /estimate validates nothing.
static string BuildInput(string task, string digest, string prescanJson,
string distribution = "container", string policy = "permissive",
string audience = "customer-security", string context = "",
string source = null) {
if (Array.IndexOf(Lanes, task) < 0)
throw new ArgumentException("task must be one of the four lane ids");
if (string.IsNullOrEmpty(digest))
throw new ArgumentException("sbom_digest is required - send the digest, not the document");
var body = new {
task, distribution, policy, audience, context,
sbom_digest = digest,
prescan_facts = JsonDocument.Parse(prescanJson).RootElement,
source
};
return JsonSerializer.Serialize(body,
new JsonSerializerOptions { DefaultIgnoreCondition =
System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull });
}
// var est = await Call("estimate", BuildInput("conformance", digest, prescanJson));
5. /run and poll
POST /run returns {"job_id": "job_..."} immediately.
GET /jobs/{id} polls it to a terminal status of succeeded,
failed or cancelled. The reply text is at
data.output.output — a JSON string you then parse. charged_credits
is the real cost and is usually far below the hold. truncated: true means the balance
sat between min_credits and hold_credits, so the run executed with a
reduced output cap; render what parsed and say it was cut short rather than presenting a clipped
answer as complete.
Send an Idempotency-Key on every run. Include the lane in it: two
lanes over one document are two distinct runs and must not collide on one key. The app uses
sbom-desk:{lane}:{hash of the input}:a{attempt}, and its one automatic reformat retry
reuses the same hash with the attempt bumped — so a network blip or a malformed first reply
can never double-bill.
# The key carries the LANE, so two lanes over one document are two jobs, not a 409.
BASE="https://api.skillsafe.ai/v1/app-api"
LANE=conformance
HASH=$(shasum -a 256 /tmp/sbom-desk-input.json | cut -c1-16)
KEY="sbom-desk:$LANE:$HASH:a1"
JOB=$(curl -sS -X POST "$BASE/run" \
-H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: $KEY" \
--data-binary @/tmp/sbom-desk-input.json \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["job_id"])')
# Poll to a terminal state. Back off; do not tight-loop.
for i in $(seq 1 90); do
RES=$(curl -sS "$BASE/jobs/$JOB" -H "Authorization: Bearer $TOKEN")
STATUS=$(printf '%s' "$RES" | python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["status"])')
[ "$STATUS" = "succeeded" ] && break
[ "$STATUS" = "failed" ] && { echo "run failed"; exit 1; }
sleep 2
done
# The reply text is a JSON string at data.output.output.
printf '%s' "$RES" \
| python3 -c 'import sys,json;print(json.load(sys.stdin)["data"]["output"]["output"])' \
> /tmp/sbom-desk-reply.json
import hashlib
def idempotency_key(body, attempt=1):
"""Include the lane: two lanes over one document are two runs."""
src = "||".join([body["task"], body.get("distribution", ""), body.get("policy", ""),
body.get("audience", ""), body["sbom_digest"], body.get("context", ""),
body.get("source", "")])
h = hashlib.sha256(src.encode()).hexdigest()[:16]
return "sbom-desk:%s:%s:a%d" % (body["task"], h, attempt)
def run_and_wait(body, attempt=1, timeout=300):
must_be_object(body)
job = call("run", body, headers={"Idempotency-Key": idempotency_key(body, attempt)})
job_id = job["job_id"]
deadline = time.time() + timeout
while time.time() < deadline:
state = call("jobs/%s" % job_id)
if state["status"] == "succeeded":
return state
if state["status"] in ("failed", "cancelled"):
raise SbomDeskError("job_" + state["status"],
state.get("error") or "the run did not complete")
time.sleep(2)
raise TimeoutError("job %s did not finish in %ss" % (job_id, timeout))
state = run_and_wait(build_input("conformance", digest, prescan))
reply_text = state["output"]["output"]
if state.get("truncated"):
print("WARNING: cut short by the available balance - render what parsed, do not present it as complete")
print("charged", state.get("charged_credits"), "credits")
import { createHash } from "node:crypto";
// Include the lane: two lanes over one document are two runs.
function idempotencyKey(body, attempt = 1) {
const src = [body.task, body.distribution || "", body.policy || "", body.audience || "",
body.sbom_digest, body.context || "", body.source || ""].join("||");
const h = createHash("sha256").update(src).digest("hex").slice(0, 16);
return `sbom-desk:${body.task}:${h}:a${attempt}`;
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
async function runAndWait(body, attempt = 1, timeoutMs = 300000) {
mustBeObject(body);
const { job_id } = await call("run", body, { "Idempotency-Key": idempotencyKey(body, attempt) });
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const state = await call(`jobs/${job_id}`);
if (state.status === "succeeded") return state;
if (state.status === "failed" || state.status === "cancelled") {
throw new Error(`job ${state.status}: ${state.error || "the run did not complete"}`);
}
await sleep(2000);
}
throw new Error(`job ${job_id} did not finish in time`);
}
const state = await runAndWait(buildInput("conformance", digest, prescan));
const replyText = state.output.output;
if (state.truncated) console.warn("cut short by the available balance - render what parsed");
console.log("charged", state.charged_credits, "credits");
import (
"crypto/sha256"
"encoding/hex"
"strings"
)
// idempotencyKey includes the lane: two lanes over one document are two runs.
func idempotencyKey(in RunInput, attempt int) string {
src := strings.Join([]string{in.Task, in.Distribution, in.Policy, in.Audience,
in.SbomDigest, in.Context, in.Source}, "||")
sum := sha256.Sum256([]byte(src))
return fmt.Sprintf("sbom-desk:%s:%s:a%d", in.Task, hex.EncodeToString(sum[:])[:16], attempt)
}
type JobState struct {
Status string `json:"status"`
Output struct{ Output string `json:"output"` } `json:"output"`
Charged int64 `json:"charged_credits"`
Truncated bool `json:"truncated"`
Error string `json:"error"`
}
func runAndWait(in RunInput, attempt int, timeout time.Duration) (JobState, error) {
var st JobState
if err := in.validate(); err != nil {
return st, err
}
data, err := call("run", in, map[string]string{"Idempotency-Key": idempotencyKey(in, attempt)})
if err != nil {
return st, err
}
var job struct{ JobID string `json:"job_id"` }
if err := json.Unmarshal(data, &job); err != nil {
return st, err
}
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
d, err := call("jobs/"+job.JobID, nil, nil)
if err != nil {
return st, err
}
if err := json.Unmarshal(d, &st); err != nil {
return st, err
}
if st.Status == "succeeded" {
return st, nil
}
if st.Status == "failed" || st.Status == "cancelled" {
return st, fmt.Errorf("job %s: %s", st.Status, st.Error)
}
time.Sleep(2 * time.Second)
}
return st, fmt.Errorf("job %s did not finish in %s", job.JobID, timeout)
}
import java.security.MessageDigest;
/** Include the lane: two lanes over one document are two runs. */
static String idempotencyKey(String task, String digest, int attempt) throws Exception {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] h = md.digest((task + "||" + digest).getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (int i = 0; i < 8; i++) sb.append(String.format("%02x", h[i]));
return "sbom-desk:" + task + ":" + sb + ":a" + attempt;
}
static String runAndWait(String body, String task, String digest, int attempt) throws Exception {
String started = call("run", body,
Map.of("Idempotency-Key", idempotencyKey(task, digest, attempt)));
String jobId = extract(started, "job_id"); // your JSON library
long deadline = System.currentTimeMillis() + 300_000;
while (System.currentTimeMillis() < deadline) {
String state = call("jobs/" + jobId, null, null);
if (state.contains("\"status\":\"succeeded\"")) return state;
if (state.contains("\"status\":\"failed\"") || state.contains("\"status\":\"cancelled\"")) {
throw new RuntimeException("the run did not complete: " + state);
}
Thread.sleep(2000);
}
throw new RuntimeException("job " + jobId + " did not finish in time");
}
require "digest"
# Include the lane: two lanes over one document are two runs.
def idempotency_key(body, attempt = 1)
src = [body[:task], body[:distribution], body[:policy], body[:audience],
body[:sbom_digest], body[:context], body[:source]].map(&:to_s).join("||")
"sbom-desk:#{body[:task]}:#{Digest::SHA256.hexdigest(src)[0, 16]}:a#{attempt}"
end
def run_and_wait(body, attempt: 1, timeout: 300)
must_be_object(body)
job = call("run", body, {"Idempotency-Key" => idempotency_key(body, attempt)})
deadline = Time.now + timeout
while Time.now < deadline
state = call("jobs/#{job["job_id"]}")
return state if state["status"] == "succeeded"
raise "job #{state["status"]}" if %w[failed cancelled].include?(state["status"])
sleep 2
end
raise "job #{job["job_id"]} did not finish in #{timeout}s"
end
state = run_and_wait(build_input("conformance", digest, prescan))
reply_text = state.dig("output", "output")
warn "cut short by the available balance - render what parsed" if state["truncated"]
puts "charged #{state["charged_credits"]} credits"
<?php
/** Include the lane: two lanes over one document are two runs. */
function idempotency_key(array $body, int $attempt = 1): string {
$src = implode("||", [$body["task"], $body["distribution"] ?? "", $body["policy"] ?? "",
$body["audience"] ?? "", $body["sbom_digest"],
$body["context"] ?? "", $body["source"] ?? ""]);
return sprintf("sbom-desk:%s:%s:a%d", $body["task"],
substr(hash("sha256", $src), 0, 16), $attempt);
}
function run_and_wait(array $body, int $attempt = 1, int $timeout = 300): array {
must_be_object($body);
$job = call("run", $body, ["Idempotency-Key" => idempotency_key($body, $attempt)]);
$deadline = time() + $timeout;
while (time() < $deadline) {
$state = call("jobs/" . $job["job_id"]);
if ($state["status"] === "succeeded") { return $state; }
if (in_array($state["status"], ["failed", "cancelled"], true)) {
throw new RuntimeException("the run did not complete: " . $state["status"]);
}
sleep(2);
}
throw new RuntimeException("job did not finish in {$timeout}s");
}
$state = run_and_wait(build_input("conformance", $digest, $prescan));
$replyText = $state["output"]["output"];
if (!empty($state["truncated"])) { fwrite(STDERR, "cut short - render what parsed\n"); }
using System.Security.Cryptography;
/// Include the lane: two lanes over one document are two runs.
static string IdempotencyKey(string task, string digest, int attempt = 1) {
using var sha = SHA256.Create();
var h = Convert.ToHexString(sha.ComputeHash(Encoding.UTF8.GetBytes(task + "||" + digest)))
.ToLowerInvariant()[..16];
return $"sbom-desk:{task}:{h}:a{attempt}";
}
public static async Task<JsonElement> RunAndWait(string body, string task, string digest,
int attempt = 1, int timeoutSeconds = 300) {
var started = await Call("run", body,
new[] { ("Idempotency-Key", IdempotencyKey(task, digest, attempt)) });
var jobId = started.GetProperty("job_id").GetString();
var deadline = DateTime.UtcNow.AddSeconds(timeoutSeconds);
while (DateTime.UtcNow < deadline) {
var state = await Call($"jobs/{jobId}");
var status = state.GetProperty("status").GetString();
if (status == "succeeded") return state;
if (status is "failed" or "cancelled")
throw new Exception($"the run did not complete: {status}");
await Task.Delay(2000);
}
throw new TimeoutException($"job {jobId} did not finish in time");
}
6. /run-stream — server-sent events
Same body, same Idempotency-Key, but the reply arrives as it is written. Each
delta event carries a chunk of the reply JSON; a job event carries the
terminal state. This is what the web page uses, and it is what lets the progress card advance on
real signals: the app watches the delta stream for that lane's contract markers — for
conformance those are "checks", "ntia",
"publish_gate", "reconciliation" and "summary" — and
moves a named stage forward as each one appears, rather than counting characters.
Accumulate the deltas and keep them on error. If the stream dies mid-flight you still have partial JSON; closing it with the right brackets recovers the sections that did arrive. The app does exactly that and labels the result as partial rather than discarding a run it paid for.
# Server-sent events. Each `delta` carries a chunk of the reply JSON; the final
# `job` event carries the terminal state.
BASE="https://api.skillsafe.ai/v1/app-api"
curl -sSN -X POST "$BASE/run-stream" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: sbom-desk:conformance:$HASH:a1" \
-H "Accept: text/event-stream" \
--data-binary @/tmp/sbom-desk-input.json \
| while IFS= read -r line; do
case "$line" in
"event: delta") read -r data; printf '%s\n' "${data#data: }" ;;
"event: job") read -r data; printf 'JOB %s\n' "${data#data: }" ;;
esac
done
def run_stream(body, attempt=1, on_delta=None):
"""Accumulate the deltas; keep them if the stream dies."""
must_be_object(body)
req = urllib.request.Request("%s/run-stream" % BASE,
data=json.dumps(body).encode(), method="POST")
req.add_header("Authorization", "Bearer %s" % TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "text/event-stream")
req.add_header("Idempotency-Key", idempotency_key(body, attempt))
raw = []
final = None
with urllib.request.urlopen(req, timeout=600) as r:
event = None
for line in r:
line = line.decode("utf-8", "replace").rstrip("\n")
if line.startswith("event: "):
event = line[7:]
elif line.startswith("data: "):
payload = line[6:]
if event == "delta":
chunk = json.loads(payload).get("text", payload)
raw.append(chunk)
if on_delta:
on_delta(chunk)
elif event == "job":
final = json.loads(payload)
return "".join(raw), final
# Advance a named stage as each contract marker arrives - not a character counter.
MARKERS = {
"conformance": ['"checks"', '"ntia"', '"publish_gate"', '"reconciliation"', '"summary"'],
"licenses": ['"rulings"', '"obligations"', '"notice_gaps"', '"policy_exceptions"', '"summary"'],
"triage": ['"triage"', '"blind_spots"', '"monitoring"', '"embedded_vulnerabilities"', '"summary"'],
"remediation": ['"steps"', '"verification"', '"statement"', '"residual"', '"summary"'],
}
seen = []
buf = []
def progress(chunk):
buf.append(chunk)
text = "".join(buf)
for m in MARKERS["conformance"]:
if m in text and m not in seen:
seen.append(m)
print("stage:", m)
raw, final = run_stream(build_input("conformance", digest, prescan), on_delta=progress)
async function runStream(body, { attempt = 1, onDelta } = {}) {
mustBeObject(body);
const res = await fetch(`${BASE}/run-stream`, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
Accept: "text/event-stream",
"Idempotency-Key": idempotencyKey(body, attempt)
},
body: JSON.stringify(body)
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "";
let raw = "";
let final = null;
let event = null;
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += dec.decode(value, { stream: true });
let nl;
while ((nl = buf.indexOf("\n")) >= 0) {
const line = buf.slice(0, nl);
buf = buf.slice(nl + 1);
if (line.startsWith("event: ")) event = line.slice(7).trim();
else if (line.startsWith("data: ")) {
const payload = line.slice(6);
if (event === "delta") {
let chunk = payload;
try { chunk = JSON.parse(payload).text ?? payload; } catch {}
raw += chunk;
onDelta?.(chunk, raw);
} else if (event === "job") {
try { final = JSON.parse(payload); } catch {}
}
}
}
}
return { raw, final };
}
const MARKERS = {
conformance: ['"checks"', '"ntia"', '"publish_gate"', '"reconciliation"', '"summary"'],
licenses: ['"rulings"', '"obligations"', '"notice_gaps"', '"policy_exceptions"', '"summary"'],
triage: ['"triage"', '"blind_spots"', '"monitoring"', '"embedded_vulnerabilities"', '"summary"'],
remediation: ['"steps"', '"verification"', '"statement"', '"residual"', '"summary"']
};
const { raw, final } = await runStream(buildInput("conformance", digest, prescan), {
onDelta: (_chunk, all) => {
const reached = MARKERS.conformance.filter((m) => all.includes(m)).length;
process.stderr.write(`\rstage ${reached}/${MARKERS.conformance.length}`);
}
});
import "bufio"
// runStream accumulates the deltas and keeps them if the stream dies.
func runStream(in RunInput, attempt int, onDelta func(string, string)) (string, *JobState, error) {
if err := in.validate(); err != nil {
return "", nil, err
}
b, err := json.Marshal(in)
if err != nil {
return "", nil, err
}
req, err := http.NewRequest(http.MethodPost, base+"/run-stream", bytes.NewReader(b))
if err != nil {
return "", nil, err
}
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", idempotencyKey(in, attempt))
res, err := client.Do(req)
if err != nil {
return "", nil, err
}
defer res.Body.Close()
var raw strings.Builder
var final *JobState
event := ""
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 0, 64*1024), 4*1024*1024)
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event: "):
event = strings.TrimSpace(line[7:])
case strings.HasPrefix(line, "data: "):
payload := line[6:]
if event == "delta" {
var d struct{ Text string `json:"text"` }
chunk := payload
if json.Unmarshal([]byte(payload), &d) == nil && d.Text != "" {
chunk = d.Text
}
raw.WriteString(chunk)
if onDelta != nil {
onDelta(chunk, raw.String())
}
} else if event == "job" {
var st JobState
if json.Unmarshal([]byte(payload), &st) == nil {
final = &st
}
}
}
}
// A scanner error still leaves raw populated - keep the partial reply.
return raw.String(), final, sc.Err()
}
/** Accumulate the deltas; keep them if the stream dies. */
static String runStream(String body, String task, String digest, int attempt) throws Exception {
HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.timeout(Duration.ofMinutes(10))
.header("Authorization", "Bearer " + token)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", idempotencyKey(task, digest, attempt))
.POST(HttpRequest.BodyPublishers.ofString(body))
.build();
StringBuilder raw = new StringBuilder();
HttpResponse<java.util.stream.Stream<String>> res =
http.send(req, HttpResponse.BodyHandlers.ofLines());
final String[] event = {""};
res.body().forEach(line -> {
if (line.startsWith("event: ")) {
event[0] = line.substring(7).trim();
} else if (line.startsWith("data: ") && event[0].equals("delta")) {
raw.append(extract(line.substring(6), "text")); // your JSON library
}
});
return raw.toString();
}
# Accumulate the deltas; keep them if the stream dies.
def run_stream(body, attempt: 1)
must_be_object(body)
uri = URI("#{BASE}/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Accept"] = "text/event-stream"
req["Idempotency-Key"] = idempotency_key(body, attempt)
req.body = JSON.generate(body)
raw = +""
final = nil
event = nil
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true, read_timeout: 600) do |http|
http.request(req) do |res|
buf = +""
res.read_body do |part|
buf << part
while (nl = buf.index("\n"))
line = buf.slice!(0..nl).chomp
if line.start_with?("event: ")
event = line[7..].strip
elsif line.start_with?("data: ")
payload = line[6..]
if event == "delta"
chunk = (JSON.parse(payload)["text"] rescue payload)
raw << chunk
yield chunk, raw if block_given?
elsif event == "job"
final = (JSON.parse(payload) rescue nil)
end
end
end
end
end
end
[raw, final]
end
<?php
/** Accumulate the deltas; keep them if the stream dies. */
function run_stream(array $body, int $attempt = 1, ?callable $onDelta = null): array {
global $TOKEN;
must_be_object($body);
$raw = "";
$final = null;
$event = "";
$buf = "";
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => json_encode($body),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer $TOKEN",
"Content-Type: application/json",
"Accept: text/event-stream",
"Idempotency-Key: " . idempotency_key($body, $attempt),
],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$raw, &$final, &$event, &$buf, $onDelta) {
$buf .= $chunk;
while (($nl = strpos($buf, "\n")) !== false) {
$line = rtrim(substr($buf, 0, $nl), "\r");
$buf = substr($buf, $nl + 1);
if (str_starts_with($line, "event: ")) {
$event = trim(substr($line, 7));
} elseif (str_starts_with($line, "data: ")) {
$payload = substr($line, 6);
if ($event === "delta") {
$d = json_decode($payload, true);
$text = is_array($d) && isset($d["text"]) ? $d["text"] : $payload;
$raw .= $text;
if ($onDelta) { $onDelta($text, $raw); }
} elseif ($event === "job") {
$final = json_decode($payload, true);
}
}
}
return strlen($chunk);
},
]);
curl_exec($ch);
curl_close($ch);
return [$raw, $final];
}
/// Accumulate the deltas; keep them if the stream dies.
public static async Task<(string Raw, JsonElement? Final)> RunStream(
string body, string task, string digest, int attempt = 1,
Action<string, string> onDelta = null) {
var req = new HttpRequestMessage(HttpMethod.Post, $"{Base}/run-stream") {
Content = new StringContent(body, Encoding.UTF8, "application/json")
};
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", Token);
req.Headers.TryAddWithoutValidation("Accept", "text/event-stream");
req.Headers.TryAddWithoutValidation("Idempotency-Key",
IdempotencyKey(task, digest, attempt));
using var res = await Http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var stream = await res.Content.ReadAsStreamAsync();
using var reader = new System.IO.StreamReader(stream);
var raw = new StringBuilder();
JsonElement? final = null;
var evt = "";
string line;
while ((line = await reader.ReadLineAsync()) != null) {
if (line.StartsWith("event: ")) {
evt = line[7..].Trim();
} else if (line.StartsWith("data: ")) {
var payload = line[6..];
if (evt == "delta") {
var chunk = payload;
try { chunk = JsonDocument.Parse(payload).RootElement
.GetProperty("text").GetString(); } catch { }
raw.Append(chunk);
onDelta?.Invoke(chunk, raw.ToString());
} else if (evt == "job") {
try { final = JsonDocument.Parse(payload).RootElement.Clone(); } catch { }
}
}
}
return (raw.ToString(), final);
}
The output contract
data.output.output is a JSON string. Parse it, then switch on
lane. The app's own parser is brace-balanced rather than a regex, because a licence
expression or the customer statement can legitimately contain a brace inside a string and a lazy
match closes the object in the wrong place.
The common envelope — every lane
{
"lane": "conformance",
"title": "orders-api 3.2.0 SBOM - publish gate",
"posture": "conditional", // ready | conditional | blocked
"verdict": "One sentence: the thing that decides the posture.",
"headline": {"label": "NTIA elements passed", "value": "6 of 7"},
"exec_summary": "2-4 sentences of plain prose.",
"findings": [
{"id": "SBD-001", "severity": "high", "area": "supplier",
"title": "One line",
"detail": "What is wrong and what it costs downstream in THIS distribution model",
"evidence": "1 of 14 components carry a supplier (7%)",
"action": "The specific next step, addressed to whoever must do it"}
],
"reconciliation": [
{"flag_id": "F-NO-SUPPLIER", "status": "confirmed", "note": "Specific."}
],
"assumptions": ["..."],
"open_questions": ["..."],
"summary": "One closing paragraph."
}
severity is critical | high | medium |
low; ids run SBD-001 upward in severity order.
reconciliation.status is confirmed | refined |
not_material | disputed, and there is exactly one entry per flag
id you sent — the web page renders a table naming any the model skipped, and you
should do the equivalent check in your own client. assumptions,
open_questions and every task array are [] when empty, never
null.
conformance adds
"checks": [
{"id": "C-FORMAT", "name": "Format and specification version",
"status": "pass", // pass | partial | fail | unknown
"evidence": "CycloneDX 1.5",
"comment": "What it means for THIS audience"}
], // EXACTLY twelve, in this order:
// C-FORMAT, C-IDENTITY, C-AUTHOR, C-TIMESTAMP,
// C-SUBJECT, C-NAMES, C-VERSIONS, C-IDS,
// C-SUPPLIER, C-LICENSES, C-HASHES, C-GRAPH
"ntia": [
{"id": "N1", "element": "Supplier name", "status": "fail",
"gap": "13 of 14 components carry no supplier",
"fix": "Set metadata.supplier and pass --author on the generator"}
], // EXACTLY seven, N1 through N7, in order
"publish_gate": {
"decision": "publish_with_caveats", // publish | publish_with_caveats | hold
"caveats": ["What must be said out loud if it goes out as it is"],
"blockers": ["What must change before it goes out at all"]
}
decision: "hold" with an empty blockers array is rejected by the
app's parser and triggers its one reformat retry: telling somebody to stop without telling
them what to fix is not an answer. Assert the same thing in your own client.
licenses adds
"rulings": [
{"licence": "GPL-2.0", // exactly as the document states it
"licence_class": "strong-copyleft", // permissive | weak-copyleft | strong-copyleft |
// network-copyleft | source-available | restricted |
// public-domain | license-ref | unrecognised | unknown
"components": 1,
"examples": ["mysql-connector-java@8.0.33"],
"ruling": "resolve", // allow | review | deny | resolve
"obligation": "What shipping this way actually requires you to do",
"rationale": "Why this ruling under this policy and this distribution model"}
],
"obligations": [
{"obligation": "Ship a written offer of corresponding source",
"triggered_by": "strong-copyleft, 1 component",
"owner": "release", // engineering | legal | release | product
"artifact": "A NOTICE file and a source mirror URL in the image labels"}
],
"notice_gaps": [
{"gap": "No component carries a copyright line",
"components": 14,
"fix": "Read each licence file from the package itself"}
],
"policy_exceptions": [
{"licence": "BUSL-1.1", "components": 1,
"ask": "The exception that would have to be granted, stated as a decision",
"alternative": "What to do instead if it is refused"}
]
One ruling per distinct licence expression, not one per component. The app
cross-checks the set of rulings[].licence values against the licence expressions its
own reader found and paints a row saying "present in the document but not ruled on" or "ruled on but
not present in the document"; it also compares each components count against its own
and prints both numbers when they disagree. And note that resolve is not a softer
deny: it means the information is missing — NOASSERTION, a free-text
string, a LicenseRef, or a retired identifier such as GPL-2.0 that does
not say whether later versions may be used.
triage adds
"triage": [
{"id": "T-1",
"target": "The 1 component with no purl and no CPE",
"exposure": "No scanner can match it, so it is not clean - it is unscanned",
"priority": "this-sprint", // now | this-sprint | this-quarter | accept
"why": "Reasoning, from the digest",
"action": "The specific next step",
"verify": "How the user will know it is done"}
],
"blind_spots": [
{"blind_spot": "Components with no identifier",
"components": 1,
"consequence": "What gets missed",
"close_it": "What would make these components visible"}
],
"monitoring": [
{"signal": "What to watch", "where": "Which part of the pipeline", "cadence": "How often"}
],
"embedded_vulnerabilities": {
"present": false,
"count": 0,
"handling": "What to do with the records the document carries, and why they are not a current status",
"unanalysed": 0
}
This lane has no vulnerability database and the prompt forbids inventing one. It
will not tell you whether a component is affected by anything: no CVE identifiers, no CVSS scores.
What it gives you is exposure reasoning from graph position, pinning and identifier coverage. The
app additionally compares embedded_vulnerabilities.count against the number of
vulnerabilities[] records its own reader found in the document and prints both numbers
when they disagree — so a fabricated count is visible rather than plausible.
remediation adds
"steps": [
{"id": "R-1",
"phase": "generator", // generator | build | dependency | policy | document
"step": "What to do, in the imperative",
"why": "The finding or flag it closes",
"command": "cdxgen --author 'Acme Release Engineering' --spec-version 1.6 -o sbom.json",
"effort": "minutes", // minutes | hours | days
"risk": "low", // none | low | medium | high
"closes": ["F-NO-AUTHOR", "N6", "C-AUTHOR"]}
],
"verification": [
{"check": "What to re-run", "expect": "The figure that proves it worked"}
],
"statement": "A complete, sendable paragraph for the customer's security team ...",
"residual": ["What will still be true after every step above is done"]
closes may only name flag ids you sent, one of the twelve check ids, or one of
N1..N7. The app prints "N steps claim to close something the prescan never
raised" naming each offender, and separately prints "no step closes X, which the prescan rated
critical or high" for anything left neither closed nor listed in residual. A missing or
empty statement is rejected by the parser.
Route on the lane
# REPLY holds data.output.output, already extracted in step 5.
python3 - /tmp/sbom-desk-reply.json <<'PY'
import json, sys
r = json.load(open(sys.argv[1]))
print(r["lane"], r["posture"], "|", r["verdict"])
if r["lane"] == "conformance":
print("gate:", r["publish_gate"]["decision"])
fails = [c["id"] for c in r["checks"] if c["status"] == "fail"]
print("failing checks:", ", ".join(fails) or "none")
# A release gate: stop the pipeline when the document is not fit to send.
if r["publish_gate"]["decision"] == "hold":
sys.exit(1)
elif r["lane"] == "licenses":
for x in r["rulings"]:
if x["ruling"] in ("deny", "resolve"):
print(x["ruling"].upper(), x["licence"], "x", x["components"])
elif r["lane"] == "triage":
for t in r["triage"]:
if t["priority"] == "now":
print("NOW", t["target"])
elif r["lane"] == "remediation":
print(r["statement"])
PY
LANE_KEYS = {
"conformance": ("checks", "ntia", "publish_gate"),
"licenses": ("rulings", "obligations", "notice_gaps", "policy_exceptions"),
"triage": ("triage", "blind_spots", "monitoring", "embedded_vulnerabilities"),
"remediation": ("steps", "verification", "statement", "residual"),
}
def parse_reply(text, expected_lane, sent_flag_ids):
"""Brace-balanced, then checked against the two things that actually break."""
start = text.index("{")
depth = 0
in_str = esc = False
for i in range(start, len(text)):
c = text[i]
if in_str:
if esc:
esc = False
elif c == "\\":
esc = True
elif c == '"':
in_str = False
continue
if c == '"':
in_str = True
elif c == "{":
depth += 1
elif c == "}":
depth -= 1
if depth == 0:
obj = json.loads(text[start:i + 1])
break
else:
raise ValueError("the JSON object never closed")
if obj.get("lane") != expected_lane:
print("WARNING: asked for %s, got %s" % (expected_lane, obj.get("lane")))
for key in LANE_KEYS[obj["lane"]]:
if key not in obj:
raise ValueError("lane %s is missing %s" % (obj["lane"], key))
got = [r["flag_id"] for r in obj.get("reconciliation", [])]
missed = [f for f in sent_flag_ids if f not in got]
extra = [g for g in got if g not in sent_flag_ids]
if missed:
print("NOT RECONCILED:", ", ".join(missed))
if extra:
print("INVENTED FLAG IDS:", ", ".join(extra))
return obj
result = parse_reply(reply_text, "conformance", [f["id"] for f in prescan["flags"]])
if result["lane"] == "conformance":
gate = result["publish_gate"]
print("gate:", gate["decision"])
if gate["decision"] == "hold":
raise SystemExit("not fit to send: " + "; ".join(gate["blockers"]))
elif result["lane"] == "licenses":
for r in result["rulings"]:
if r["ruling"] in ("deny", "resolve"):
print(r["ruling"].upper(), r["licence"], "x", r["components"])
const LANE_KEYS = {
conformance: ["checks", "ntia", "publish_gate"],
licenses: ["rulings", "obligations", "notice_gaps", "policy_exceptions"],
triage: ["triage", "blind_spots", "monitoring", "embedded_vulnerabilities"],
remediation: ["steps", "verification", "statement", "residual"]
};
// Brace-balanced: a licence expression or the statement can contain a brace in a string.
function parseReply(text, expectedLane, sentFlagIds) {
const start = text.indexOf("{");
if (start < 0) throw new Error("no JSON object in the reply");
let depth = 0, inStr = false, esc = false, obj = null;
for (let i = start; i < text.length; i++) {
const c = text[i];
if (inStr) {
if (esc) esc = false;
else if (c === "\\") esc = true;
else if (c === '"') inStr = false;
continue;
}
if (c === '"') inStr = true;
else if (c === "{") depth++;
else if (c === "}" && --depth === 0) { obj = JSON.parse(text.slice(start, i + 1)); break; }
}
if (!obj) throw new Error("the JSON object never closed");
if (obj.lane !== expectedLane) console.warn(`asked for ${expectedLane}, got ${obj.lane}`);
for (const k of LANE_KEYS[obj.lane]) {
if (!(k in obj)) throw new Error(`lane ${obj.lane} is missing ${k}`);
}
const got = (obj.reconciliation || []).map((r) => r.flag_id);
const missed = sentFlagIds.filter((f) => !got.includes(f));
const extra = got.filter((g) => !sentFlagIds.includes(g));
if (missed.length) console.warn("NOT RECONCILED:", missed.join(", "));
if (extra.length) console.warn("INVENTED FLAG IDS:", extra.join(", "));
return obj;
}
const result = parseReply(replyText, "conformance", prescan.flags.map((f) => f.id));
if (result.lane === "conformance" && result.publish_gate.decision === "hold") {
throw new Error("not fit to send: " + result.publish_gate.blockers.join("; "));
}
// Common envelope plus the union of the lane bodies. Unmarshal once, switch on Lane.
type Reply struct {
Lane string `json:"lane"`
Title string `json:"title"`
Posture string `json:"posture"`
Verdict string `json:"verdict"`
Headline struct {
Label string `json:"label"`
Value string `json:"value"`
} `json:"headline"`
ExecSummary string `json:"exec_summary"`
Findings []struct {
ID, Severity, Area, Title, Detail, Evidence, Action string
} `json:"findings"`
Reconciliation []struct {
FlagID string `json:"flag_id"`
Status string `json:"status"`
Note string `json:"note"`
} `json:"reconciliation"`
Summary string `json:"summary"`
// conformance
Checks []struct{ ID, Name, Status, Evidence, Comment string } `json:"checks"`
Ntia []struct{ ID, Element, Status, Gap, Fix string } `json:"ntia"`
PublishGate *struct {
Decision string `json:"decision"`
Caveats []string `json:"caveats"`
Blockers []string `json:"blockers"`
} `json:"publish_gate"`
// licenses
Rulings []struct {
Licence string `json:"licence"`
LicenceClass string `json:"licence_class"`
Components int `json:"components"`
Examples []string `json:"examples"`
Ruling string `json:"ruling"`
Obligation string `json:"obligation"`
Rationale string `json:"rationale"`
} `json:"rulings"`
// triage
Triage []struct {
ID, Target, Exposure, Priority, Why, Action, Verify string
} `json:"triage"`
// remediation
Steps []struct {
ID, Phase, Step, Why, Command, Effort, Risk string
Closes []string `json:"closes"`
} `json:"steps"`
Statement string `json:"statement"`
Residual []string `json:"residual"`
}
func routeReply(text string, sentFlagIDs []string) (Reply, error) {
var r Reply
start := strings.Index(text, "{")
if start < 0 {
return r, fmt.Errorf("no JSON object in the reply")
}
if err := json.Unmarshal([]byte(text[start:]), &r); err != nil {
return r, err
}
got := map[string]bool{}
for _, x := range r.Reconciliation {
got[x.FlagID] = true
}
for _, id := range sentFlagIDs {
if !got[id] {
fmt.Println("NOT RECONCILED:", id)
}
}
switch r.Lane {
case "conformance":
if r.PublishGate == nil {
return r, fmt.Errorf("conformance lane returned no publish_gate")
}
if r.PublishGate.Decision == "hold" {
return r, fmt.Errorf("not fit to send: %s", strings.Join(r.PublishGate.Blockers, "; "))
}
case "remediation":
if r.Statement == "" {
return r, fmt.Errorf("remediation lane returned no customer statement")
}
}
return r, nil
}
/** Route on `lane` and assert the two things that actually break. */
static void routeReply(String replyText, java.util.List<String> sentFlagIds) {
// Parse with your JSON library; the shape is documented above.
String lane = extract(replyText, "lane");
switch (lane) {
case "conformance" -> {
// publish_gate.decision is the answer; "hold" always carries blockers.
if (replyText.contains("\"decision\":\"hold\"")) {
throw new RuntimeException("not fit to send - read publish_gate.blockers");
}
}
case "licenses" -> { /* rulings[], obligations[], notice_gaps[], policy_exceptions[] */ }
case "triage" -> { /* triage[], blind_spots[], monitoring[], embedded_vulnerabilities */ }
case "remediation" -> { /* steps[], verification[], statement, residual[] */ }
default -> throw new IllegalStateException("unknown lane: " + lane);
}
// Every flag id you sent must appear exactly once in reconciliation[].
for (String id : sentFlagIds) {
if (!replyText.contains("\"" + id + "\"")) System.out.println("NOT RECONCILED: " + id);
}
}
LANE_KEYS = {
"conformance" => %w[checks ntia publish_gate],
"licenses" => %w[rulings obligations notice_gaps policy_exceptions],
"triage" => %w[triage blind_spots monitoring embedded_vulnerabilities],
"remediation" => %w[steps verification statement residual],
}.freeze
def parse_reply(text, expected_lane, sent_flag_ids)
obj = JSON.parse(text[text.index("{")..])
warn "asked for #{expected_lane}, got #{obj["lane"]}" if obj["lane"] != expected_lane
LANE_KEYS.fetch(obj["lane"]).each do |k|
raise "lane #{obj["lane"]} is missing #{k}" unless obj.key?(k)
end
got = obj.fetch("reconciliation", []).map { |r| r["flag_id"] }
(sent_flag_ids - got).each { |id| warn "NOT RECONCILED: #{id}" }
(got - sent_flag_ids).each { |id| warn "INVENTED FLAG ID: #{id}" }
obj
end
result = parse_reply(reply_text, "conformance", prescan[:flags].map { |f| f[:id] })
if result["lane"] == "conformance" && result.dig("publish_gate", "decision") == "hold"
abort "not fit to send: #{result.dig("publish_gate", "blockers").join("; ")}"
end
<?php
const LANE_KEYS = [
"conformance" => ["checks", "ntia", "publish_gate"],
"licenses" => ["rulings", "obligations", "notice_gaps", "policy_exceptions"],
"triage" => ["triage", "blind_spots", "monitoring", "embedded_vulnerabilities"],
"remediation" => ["steps", "verification", "statement", "residual"],
];
function parse_reply(string $text, string $expectedLane, array $sentFlagIds): array {
$start = strpos($text, "{");
if ($start === false) { throw new RuntimeException("no JSON object in the reply"); }
$obj = json_decode(substr($text, $start), true);
if (!is_array($obj)) { throw new RuntimeException("the reply did not parse as JSON"); }
if (($obj["lane"] ?? "") !== $expectedLane) {
fwrite(STDERR, "asked for $expectedLane, got {$obj["lane"]}\n");
}
foreach (LANE_KEYS[$obj["lane"]] as $k) {
if (!array_key_exists($k, $obj)) {
throw new RuntimeException("lane {$obj["lane"]} is missing $k");
}
}
$got = array_column($obj["reconciliation"] ?? [], "flag_id");
foreach (array_diff($sentFlagIds, $got) as $id) { fwrite(STDERR, "NOT RECONCILED: $id\n"); }
foreach (array_diff($got, $sentFlagIds) as $id) { fwrite(STDERR, "INVENTED FLAG ID: $id\n"); }
return $obj;
}
$result = parse_reply($replyText, "conformance", array_column($prescan["flags"], "id"));
if ($result["lane"] === "conformance" && $result["publish_gate"]["decision"] === "hold") {
exit("not fit to send: " . implode("; ", $result["publish_gate"]["blockers"]) . "\n");
}
static readonly Dictionary<string, string[]> LaneKeys = new() {
["conformance"] = new[] {"checks", "ntia", "publish_gate"},
["licenses"] = new[] {"rulings", "obligations", "notice_gaps", "policy_exceptions"},
["triage"] = new[] {"triage", "blind_spots", "monitoring", "embedded_vulnerabilities"},
["remediation"] = new[] {"steps", "verification", "statement", "residual"},
};
public static JsonElement ParseReply(string text, string expectedLane, string[] sentFlagIds) {
var start = text.IndexOf('{');
if (start < 0) throw new Exception("no JSON object in the reply");
var root = JsonDocument.Parse(text[start..]).RootElement;
var lane = root.GetProperty("lane").GetString();
if (lane != expectedLane) Console.Error.WriteLine($"asked for {expectedLane}, got {lane}");
foreach (var k in LaneKeys[lane])
if (!root.TryGetProperty(k, out _)) throw new Exception($"lane {lane} is missing {k}");
var got = new HashSet<string>();
foreach (var r in root.GetProperty("reconciliation").EnumerateArray())
got.Add(r.GetProperty("flag_id").GetString());
foreach (var id in sentFlagIds)
if (!got.Contains(id)) Console.Error.WriteLine($"NOT RECONCILED: {id}");
if (lane == "conformance" &&
root.GetProperty("publish_gate").GetProperty("decision").GetString() == "hold") {
throw new Exception("not fit to send - read publish_gate.blockers");
}
return root;
}
Rate limits, cost and the one thing to design around
- 30 requests/minute per IP on the app-api endpoints; the platform's
/v1/creator/*paths are separate. Back off with jitter on a 429. /estimate,/meand/guestare free. A release gate that only prices lanes and checks a balance never spends a credit.- The hold is not the price. It reserves against the full output cap; the settled
charged_creditsis usually far lower. Quote the hold as reserved and the settled figure only after the fact. - Failed runs are not billed. Retry a
500once with the sameIdempotency-Key. - The digest is the design constraint. A 4,000-component SBOM does not fit, and
truncating it silently is the one failure mode that produces confident wrong answers. Compute
every count over the whole document, sample the rows, and keep the
sample_completeness:line honest — the prompt reads it and changes its behaviour accordingly.
What this API will not do
It will not tell you whether a component is vulnerable. There is no vulnerability database behind it, the prompt forbids inventing a CVE identifier or a CVSS score, and the triage lane is exposure reasoning rather than vulnerability reporting. It will not validate your document against the CycloneDX or SPDX schema — use the projects' own validators for that; what it does is judge whether the document is useful to the person you are sending it to. And nothing it returns is legal advice: the licence lane classifies obligations and names the decisions somebody has to make, and never states that a particular use is or is not permitted.