← Linux Triage / API
Token panel

Driving Linux Triage from your own code

Linux Triage is a metered SkillSafe app. Everything the page does over the network, you can do from a script: mint a token, price a lane for free, submit a paste, and read back the same JSON envelope the page renders.

Base URL https://api.skillsafe.ai/v1/app-api · slug linux-triage · model gpt-terra (resolves to gpt-5.6-terra)

The task field comes first

Every run input carries task, and it is the field that decides everything else. It selects one of four lanes over the same work object — one console paste — and each lane has its own allowed verdicts and its own emphasis inside an otherwise identical envelope. If task is missing or unrecognised the model picks the closest lane and names its choice in summary rather than blending two contracts.

taskLaneAllowed verdictsWhat differs
triageThe fault chainroot-cause-identified | narrowed | insufficient-evidencechain[] is populated and is the spine of the answer: one entry per link, earliest cause first, each quoting the line that proves it.
denialWhat was deniedpolicy-change | relabel | not-the-cause | no-denials-presentchain[] is [] by contract. One findings[] entry per denial, each quoting its audit record. permissive=1 denials are reported as logged-and-allowed, which is not-the-cause.
packageThe package staterecoverable-in-place | needs-intervention | no-package-failurechain[] is [] by contract. The package manager is taken from the error grammar (dpkg:, nothing provides, failed to commit transaction), never from distro.
runbookThe runbookready-to-run | needs-approval | blocked-on-evidencechain[] here is the preconditions list, not a causal chain. Every step carries verify, and every risk: "high" step carries rollback. At least one findings entry with severity: "crit" is the stop condition.

The endpoints

The envelope, and what can go wrong

Success is {"ok":true,"data":{...}}. Failure is {"ok":false,"error":{"code":"...","message":"...","details":{...}}} with a matching HTTP status.

codeHTTPWhat it means, and what to do
UNAUTHORIZED401No token, or a token that has expired or been revoked. Mint a new guest token or sign in again.
FORBIDDEN403The token is valid but not for this app, or the call needs a personal token and you sent a guest one.
NOT_FOUND404Wrong slug, or a job id that does not belong to this subject.
VALIDATION_ERROR400The body is malformed. error.details names the offending field.
PAYMENT_REQUIRED402The balance is below min_credits. Compare them in step 3 and this never happens.
RATE_LIMITED429Back off and retry with the SAME Idempotency-Key — a retry on the same key cannot double-bill.
INTERNAL500Transient. Retry once on the same key before treating it as a failure.

1. A tiny client helper

Every endpoint returns the same envelope: {"ok":true,"data":{...}} on success and {"ok":false,"error":{"code":"...","message":"..."}} on failure. Write the unwrapping once.

2. Get a token

A guest token is minted with one call and is enough for /me and /estimate. Running a lane is metered, so it needs a personal token — sign in on the app page or copy one from the token panel.

3. Check the session and the balance

Do this before every run. Comparing credits here against min_credits from step 4 is what stops a submit turning into a 402.

4. Price the run — free, and per lane

/estimate creates no job and costs nothing. It is also the authoritative check that the app is bound to the model you expect. Re-estimate whenever you change task: the four lanes have different prompts and different output caps, so their reservations differ.

5. Run it and poll

Use this when you do not need progress. Always send an Idempotency-Key: a retry with the same key returns the same job rather than billing a second run.

6. Or stream it

The app itself uses this. Concatenate every delta payload and parse the concatenation once at the end — an individual delta is a fragment, not valid JSON.

The run input, field by field

FieldTypeMeaning
taskstringtriage, denial, package or runbook. Required in practice.
console_outputstringThe paste. Send it already masked if it carries identities — the page masks in the browser and so should you.
distrostringdebian, rhel, fedora, arch or unknown.
distro_sourcestringread-from-paste or stated-by-operator. The latter overrules the paste.
posturestringbalanced, cautious or fast.
recent_changesstringWhat changed and what was already tried. The highest-value optional field.
definition_of_fixedstringWhat a good outcome is, and what is off the table. Binds the runbook.
identities_maskedbooleanWhether pseudonyms are in play, so the model knows not to guess at what HOST-1 stands for.
scanobjectThe structured read: units[], status[], denials[], packages[], oom[], disks[], flags[]. You may send null, but the reply loses its grounding and its coverage check.
clipobjectcharacters_cut, head_lines, tail_lines — so the model knows it is reading an excerpt of an excerpt.

The output envelope — identical on every lane

One JSON object, no fence, no prose around it. Every key is present on every reply; an empty array is a complete answer.

{
  "lane": "triage",
  "title": "nginx.service cannot bind 443 because caddy already holds it",
  "headline": "One sentence an on-call engineer could act on.",
  "verdict": "root-cause-identified",
  "confidence": "high",
  "summary": "Two to five sentences: what broke, what it means, what is least certain.",
  "chain": [
    {"order": 1, "what": "caddy.service started at 22:29 and took :443",
     "evidence": "Aug 14 22:29:58 HOST-1 systemd[1]: Started caddy.service - Caddy web server.",
     "line": 29, "certainty": "definite"}
  ],
  "steps": [
    {"order": 1, "action": "Identify the process holding 443", "command": "ss -ltnp | grep ':443'",
     "why": "Names the conflicting listener before anything is stopped.",
     "risk": "low", "reversible": "yes", "rollback": "", "verify": "one line naming a pid and a program"}
  ],
  "findings": [
    {"title": "The restart loop is a consequence, not the cause", "severity": "warn",
     "why": "systemd restarted three times in twelve seconds because the bind failed each time.",
     "evidence": "nginx.service: Start request repeated too quickly, start-limit-hit"}
  ],
  "artifact": "# Triage note\n\n...markdown...",
  "coverage_check": [
    {"flag_id": "restart-loop", "answered": "yes", "note": "Explained as downstream of the bind failure."}
  ],
  "questions": ["Was caddy installed deliberately, or pulled in as a dependency?"]
}

One worked example per lane

task: "triage" — The fault chain

chain[] is populated and is the spine of the answer: one entry per link, earliest cause first, each quoting the line that proves it. Verdicts: root-cause-identified | narrowed | insufficient-evidence.

{
  "task": "triage",
  "console_output": "<the systemctl status block and the journal tail>",
  "distro": "debian",
  "posture": "balanced",
  "recent_changes": "Rebooted at 22:29 after an unattended kernel upgrade.",
  "definition_of_fixed": "Production node behind an LB; one node may leave rotation.",
  "scan": { "...": "the browser scan, see the shape above" }
}

task: "denial" — What was denied

chain[] is [] by contract. One findings[] entry per denial, each quoting its audit record. permissive=1 denials are reported as logged-and-allowed, which is not-the-cause. Verdicts: policy-change | relabel | not-the-cause | no-denials-present.

{
  "task": "denial",
  "console_output": "<the httpd status block plus the type=AVC records>",
  "distro": "rhel",
  "posture": "cautious",
  "definition_of_fixed": "SELinux must stay enforcing.",
  "scan": { "denials": [{"lsm": "selinux", "perms": ["name_bind"], "comm": "httpd",
                         "scontext_type": "httpd_t", "tcontext_type": "unreserved_port_t",
                         "tclass": "tcp_socket", "permissive": false}] }
}

task: "package" — The package state

chain[] is [] by contract. The package manager is taken from the error grammar (dpkg:, nothing provides, failed to commit transaction), never from distro. Verdicts: recoverable-in-place | needs-intervention | no-package-failure.

{
  "task": "package",
  "console_output": "<the dpkg --configure -a output plus df -h and df -i>",
  "distro": "debian",
  "posture": "balanced",
  "scan": { "packages": [{"mgr": "apt", "kind": "interrupted", "pkg": "", "detail": ""}],
            "disks": [{"fs": "/dev/vda2", "mount": "/", "size": "20G", "used": "20G",
                       "stated_pct": 100, "computed_pct": 100, "inode_table": false}] }
}

task: "runbook" — The runbook

chain[] here is the preconditions list, not a causal chain. Every step carries verify, and every risk: "high" step carries rollback. At least one findings entry with severity: "crit" is the stop condition. Verdicts: ready-to-run | needs-approval | blocked-on-evidence.

{
  "task": "runbook",
  "console_output": "<the same paste>",
  "recent_changes": "Carried over from the fault chain lane: ...",
  "definition_of_fixed": "No reboot before Sunday.",
  "scan": { "...": "as above" }
}

Notes that will save you a round trip