Lyber
Request access
DocsSecurity review

Workflows

Markdown

Security review API

Run an API, web or code audit through Lyber’s security-review endpoint, from intake to a report with findings checked against the full flow.

Tell it what to audit. Supply the code. Review the findings after a deeper pass and a check of the complete application flow.

POST /v1/workflows/security-review

  1. Intake
  2. Scan repo
  3. Find vulnerabilities
  4. Deep review
  5. Recheck full flow
  6. Report

Start with one request.

Use curl and Python 3 for this example. No SDK install is needed. Keep your API key in your server or terminal environment.

1. Set your connection

Use the Router URL supplied with your approved access. LYBER_BASE_URL includes /v1. Choose an enabled model from your Router’s GET /v1/models response.

Get API access · Your API keys

Connection · shell
export LYBER_BASE_URL='https://lyber-paid-acceptance-gateway-production.up.railway.app/v1'
export LYBER_API_KEY='YOUR_LYBER_API_KEY'
export LYBER_MODEL='minimax-m3.1-flash-abliterated'

2. Ask for the audit details

The first reply asks for the audit type (API, web, infrastructure or another type), the repository and revision or scope, and a live-server URL only when needed. This saves the reply in intake.json for the next request.

Start intake · curl
# Replace only this example's generated intake file.
rm -f intake.json
curl --max-time 600 --fail-with-body --silent --show-error \
  "$LYBER_BASE_URL/workflows/security-review" \
  -H "Authorization: Bearer $LYBER_API_KEY" \
  -H "Content-Type: application/json" \
  --data "{\"model\":\"$LYBER_MODEL\",\"stage\":\"intake\",\"messages\":[],\"max_tokens\":16384,\"max_cost_micro_usdc\":1000000}" \
  --output intake-pending.json &&
python3 - <<'PY'
import json
from pathlib import Path

pending = Path("intake-pending.json")
response = json.loads(pending.read_text(encoding="utf-8"))
reply = response["choices"][0]
message = reply["message"]
text = message.get("content")
if (reply.get("finish_reason") != "stop" or message.get("tool_calls")
        or message.get("role") != "assistant" or not isinstance(text, str) or not text.strip()
        or any(marker in text for marker in ("<tool_call", "<|DSML|", "[TOOL_CALL]", "<|tool▁"))):
    raise SystemExit("Resolve the incomplete reply or tool call before continuing.")
pending.replace("intake.json")
print(message["content"])
PY

Read the reply in choices[0].message.content. It is a normal Chat Completions response.

3. Answer and supply the code

Your app supplies repository files or real tool results. A repository link identifies the target; it does not give the endpoint access to the code. Supply live-server observations when relevant, through your own tools.

Create audit-context.txt in your working directory. Answer the intake questions, then paste the relevant files with their paths. Use actual source, including the surrounding calls and guards needed to trace the flow.

Context file template · replace the brackets before sending

audit-context.txt
Audit type: API
Repository: [repository URL]
Revision: [frozen commit SHA and snapshot ID]
Scope: [routes or features to review]
Live server: [URL only if relevant; otherwise omit this line]

Repository tree:
[paste the relevant file tree]

--- path/to/route.ext @ [snapshot, actual lines] ---
[paste the complete relevant source]

--- path/to/auth.ext @ [snapshot, actual lines] ---
[paste authentication and authorization source]

--- path/to/service.ext @ [snapshot, actual lines] ---
[paste the internal calls, storage code and relevant configuration/tests]

Send the next turn

This preserves the real intake reply and starts the repository scan. It reads audit-context.txt and saves the successful request and response together in scan-state.json. Read the printed reply before continuing.

Send source and history · Python + curl
if python3 - <<'PY'
import json
import os
from pathlib import Path

Path("scan-state.json").unlink(missing_ok=True)
reply = json.loads(Path("intake.json").read_text(encoding="utf-8"))["choices"][0]
message = reply["message"]
text = message.get("content")
if (reply.get("finish_reason") != "stop" or message.get("tool_calls")
        or message.get("role") != "assistant" or not isinstance(text, str) or not text.strip()
        or any(marker in text for marker in ("<tool_call", "<|DSML|", "[TOOL_CALL]", "<|tool▁"))):
    raise SystemExit("Resolve the incomplete reply or tool call before continuing.")
context = Path("audit-context.txt").read_text(encoding="utf-8")
if not context.strip():
    raise SystemExit("Add your audit details and actual source to audit-context.txt.")
body = {
    "model": os.environ["LYBER_MODEL"], "stage": "scan",
    "messages": [message, {"role": "user", "content": context}],
    "max_tokens": 16384, "max_cost_micro_usdc": 1000000,
}
Path("scan-request-pending.json").write_text(json.dumps(body), encoding="utf-8")
PY
then
  curl --max-time 600 --fail-with-body --silent --show-error \
    "$LYBER_BASE_URL/workflows/security-review" \
    -H "Authorization: Bearer $LYBER_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @scan-request-pending.json \
    --output scan-response-pending.json &&
  python3 - <<'PY'
import json
from pathlib import Path

request = json.loads(Path("scan-request-pending.json").read_text(encoding="utf-8"))
response = json.loads(Path("scan-response-pending.json").read_text(encoding="utf-8"))
choice = response["choices"][0]
message = choice["message"]
text = message.get("content")
if (choice.get("finish_reason") != "stop" or message.get("tool_calls")
        or message.get("role") != "assistant" or not isinstance(text, str) or not text.strip()
        or any(marker in text for marker in ("<tool_call", "<|DSML|", "[TOOL_CALL]", "<|tool▁"))):
    raise SystemExit("Incomplete reply: inspect private pending files; no stage completed. Do not auto-replay.")
state = Path("scan-state-pending.json")
state.write_text(json.dumps({"request": request, "response": response}), encoding="utf-8")
state.replace("scan-state.json")
print(message["content"])
PY
else
  false
fi

One request. One model call.

Each explicit stage requests only its own artifact. After a completed stage, archive the raw transcript privately and start the next stage with a fresh evidence packet in an ordinary user message. Full history remains supported. Keep complete assistant messages and matching tool results inside an active tool loop.

stageWhat it does
intakeCollect only missing audit type, repository/revision/scope and a live URL when relevant; proceed when supplied.
scanReturn the runtime map, exact methods/routes, principal–object–operation boundaries, sinks, coverage ledger and ranked queue. No verdicts.
discoverReturn finite candidate cards for the assigned property/module: invariant, evidence, guards, missing links and stable IDs. Queue uncovered work.
deep_reviewReturn an independent verdict per candidate, strongest counterevidence, effective guards, an allowed scenario and decisive missing facts.
recheckReturn checked and unknown flow segments, including feature gates, canonical identity, transactions, failure/retry/cancellation and reconciliation.
reportReport adjudicated results, impact and fixes, proposed versus executed verification, rejected claims and ledger-derived coverage. No new findings.

Omit stage or use auto to infer the next conversational pass from actual context. It asks only missing intake questions and never dispatches hidden passes. Every HTTP request makes one inference dispatch; a caller tool continuation or another stage is another billed request.

Continue from your saved scan

Read and check the scan artifact, then create review-packet.txt with scope/snapshot, map, neutral candidate cards, actual decisive source chunks and frozen ranges, guards/counterevidence, coverage and open questions. Remove the discoverer’s severity/confidence and persuasive narrative for independent review. Reuse this block for discover → deep_review, deep_review → recheck, then recheck → report. Fresh packets are the default between stages; REVIEW_CONTEXT_MODE=history retains the compatible full-history option. Within a stage, supply missing source in REVIEW_REPLY_FILE. Inspect every artifact before advancing. Run sequentially in one private working directory per audit.

Next pass · Python + curl
export PREVIOUS_STAGE="scan"
export REVIEW_STAGE="discover"
export REVIEW_REPLY_FILE="review-packet.txt"
export REVIEW_CONTEXT_MODE="fresh" # Use history to keep full completed history.

if python3 - <<'PY'
import json
import os
from pathlib import Path

previous, stage = os.environ["PREVIOUS_STAGE"], os.environ["REVIEW_STAGE"]
allowed = {"scan", "discover", "deep_review", "recheck", "report"}
if previous not in allowed or stage not in allowed:
    raise SystemExit("Use a documented review stage.")
if stage != previous:
    Path(f"{stage}-state.json").unlink(missing_ok=True)
state = json.loads(Path(f"{previous}-state.json").read_text(encoding="utf-8"))
body, reply = state["request"], state["response"]["choices"][0]
message = reply["message"]
text = message.get("content")
if (reply.get("finish_reason") != "stop" or message.get("tool_calls")
        or message.get("role") != "assistant" or not isinstance(text, str) or not text.strip()
        or any(marker in text for marker in ("<tool_call", "<|DSML|", "[TOOL_CALL]", "<|tool▁"))):
    raise SystemExit("Resolve the incomplete reply or tool call before continuing.")
reply_file = os.environ.get("REVIEW_REPLY_FILE")
if not reply_file:
    raise SystemExit("Set REVIEW_REPLY_FILE to your checked evidence packet or missing source.")
answer = Path(reply_file).read_text(encoding="utf-8")
if not answer.strip():
    raise SystemExit("The evidence packet must include actual source and counterevidence, not only IDs.")
mode = os.environ.get("REVIEW_CONTEXT_MODE", "fresh")
if mode not in {"fresh", "history"}:
    raise SystemExit("REVIEW_CONTEXT_MODE must be fresh or history.")
# Compact only after a completed stage. Never split an active tool loop.
base = [] if mode == "fresh" and stage != previous else [*body["messages"], message]
body["messages"] = [*base, {"role": "user", "content": answer}]
body["stage"] = stage
Path(f"{stage}-request-pending.json").write_text(json.dumps(body), encoding="utf-8")
PY
then
  curl --max-time 600 --fail-with-body --silent --show-error \
    "$LYBER_BASE_URL/workflows/security-review" \
    -H "Authorization: Bearer $LYBER_API_KEY" \
    -H "Content-Type: application/json" \
    --data-binary @"$REVIEW_STAGE-request-pending.json" \
    --output "$REVIEW_STAGE-response-pending.json" &&
  python3 - <<'PY'
import json
import os
from pathlib import Path

stage = os.environ["REVIEW_STAGE"]
request = json.loads(Path(f"{stage}-request-pending.json").read_text(encoding="utf-8"))
response = json.loads(Path(f"{stage}-response-pending.json").read_text(encoding="utf-8"))
choice = response["choices"][0]
message = choice["message"]
text = message.get("content")
if (choice.get("finish_reason") != "stop" or message.get("tool_calls")
        or message.get("role") != "assistant" or not isinstance(text, str) or not text.strip()
        or any(marker in text for marker in ("<tool_call", "<|DSML|", "[TOOL_CALL]", "<|tool▁"))):
    raise SystemExit("Incomplete reply: inspect private pending files; no stage completed. Do not auto-replay.")
state = Path(f"{stage}-state-pending.json")
state.write_text(json.dumps({"request": request, "response": response}), encoding="utf-8")
state.replace(f"{stage}-state.json")
print(message["content"])
PY
else
  false
fi

Keep evidence and limits in your app.

This server-side helper uses built-in fetch. Pass { fresh: true } with a reviewed evidence packet to begin a completed next stage. Omit it to extend the active history. Optional readFile supplies a caller-owned, read-only source tool; constrain it to your authorized frozen snapshot.

JavaScript · server side · Node.js 20+

workflow.mjs
// Run on your server. Never bundle the API key into a web page.
const base = process.env.LYBER_BASE_URL?.replace(/\/+$/, "");
const key = process.env.LYBER_API_KEY;
const model = process.env.LYBER_MODEL;
if (!base || !key || !model) throw new Error("Set the three environment variables first.");

const messages = []; // One private history per audit. Call sequentially.
// Provisional caller policy. Set these for your authorized workload.
const limits = { calls: 24, timeoutMs: 600000, costPerCall: 1000000 };
let calls = 0; // Each dispatch reserves one whole per-call ceiling.
const readFileTool = {
  type: "function", function: { name: "read_file", description: "Read a file in the frozen authorized source snapshot.",
    parameters: { type: "object", properties: { path: { type: "string" } }, required: ["path"], additionalProperties: false } },
};
const nativeSyntax = text => /<tool_call|<|DSML||\[TOOL_CALL\]|<|tool▁/.test(text ?? "");
function recovery(message, response, next) {
  const error = new Error(message); // Do not include raw source, provider errors or credentials.
  Object.defineProperties(error, {
    response: { value: response },
    requestMessages: { value: structuredClone(next) },
  });
  return error;
}
async function request(stage, next, tools) {
  if (calls >= limits.calls) throw recovery("unresolved: caller call/cost ceiling; retain missing evidence.", null, next);
  calls += 1; // Transport failures may still incur usage; never replay automatically.
  let response;
  try {
    response = await fetch(base + "/workflows/security-review", {
      method: "POST", redirect: "error", signal: AbortSignal.timeout(limits.timeoutMs),
      headers: { Authorization: "Bearer " + key, "Content-Type": "application/json" },
      body: JSON.stringify({ model, stage, messages: next, max_tokens: 16384,
        max_cost_micro_usdc: limits.costPerCall, ...(tools ? { tools } : {}) }),
    });
  } catch {
    throw recovery("Transport/deadline failure; usage uncertain. Inspect privately before another request.", null, next);
  }
  let raw;
  try { raw = await response.text(); }
  catch { throw recovery("Response transport failure; usage uncertain. Do not auto-replay.", null, next); }
  if (!response.ok) throw recovery("HTTP " + response.status + "; inspect private response before retrying.", raw, next);
  try { return JSON.parse(raw); }
  catch { throw recovery("Invalid JSON response; usage uncertain.", raw, next); }
}
async function review(stage, userText, { fresh = false, readFile } = {}) {
  if (fresh && (typeof userText !== "string" || !userText.trim())) throw new Error("Fresh stage requires a checked evidence packet.");
  let next = fresh ? [] : structuredClone(messages);
  if (userText !== undefined) next.push({ role: "user", content: userText });
  let invalidCalls = 0;
  const seenIds = new Set();
  for (let round = 0; round <= 8; round += 1) {
    const data = await request(stage, next, readFile ? [readFileTool] : undefined);
    const choice = data?.choices?.[0];
    const assistant = choice?.message;
    if (assistant?.role !== "assistant" || nativeSyntax(assistant.content)) {
      throw recovery("unresolved: malformed reply or leaked native tool syntax.", data, next);
    }
    if (choice.finish_reason === "tool_calls" && readFile && round < 8) {
      const tools = assistant.tool_calls;
      if (!Array.isArray(tools) || !tools.length || tools.some(call =>
          !call || typeof call !== "object" || typeof call.id !== "string" || !call.id || seenIds.has(call.id)) || new Set(tools.map(call => call.id)).size !== tools.length) {
        throw recovery("unresolved: invalid or duplicate tool IDs.", data, next);
      }
      next.push(assistant); // Preserve ALL fields, including required reasoning fields.
      let stop = false;
      for (const call of tools) {
        seenIds.add(call.id);
        let args;
        try { args = JSON.parse(call.function?.arguments); } catch { /* Return an actionable schema error below. */ }
        const valid = call.type === "function" && call.function?.name === "read_file"
          && args && typeof args === "object" && !Array.isArray(args)
          && typeof args.path === "string" && args.path.trim()
          && Object.keys(args).length === 1;
        let result;
        if (stop) result = { error: "unresolved: tool batch stopped after repeated malformed arguments; not executed." };
        else if (!valid) {
          invalidCalls += 1;
          result = { error: "read_file requires only nonempty string field path; example: {\"path\":\"src/routes.ts\"}. No arguments were inferred or executed." };
          stop = invalidCalls > 1;
        } else {
          try { result = { path: args.path, source: await readFile(args.path) }; }
          catch { result = { error: "Source read failed; supply the missing authorized frozen source." }; }
        }
        next.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
      }
      if (stop) throw recovery("unresolved: repeated malformed tool arguments; investigation stopped.", data, next);
      continue;
    }
    if (choice.finish_reason !== "stop" || assistant.tool_calls ||
        typeof assistant.content !== "string" || !assistant.content.trim()) {
      throw recovery("unresolved: incomplete output or tool round limit. Inspect partials; do not auto-replay.", data, next);
    }
    // Atomic publication. Human checks the stage artifact before starting the next stage.
    messages.splice(0, messages.length, ...next, assistant);
    return assistant.content;
  }
}

console.log(await review("intake"));
// Collect the user's answer and actual source, then:
// console.log(await review("scan", answerAndSource));
// Inspect each artifact, prepare a neutral packet with decisive source/counterevidence:
// console.log(await review("discover", discoveryPacket, { fresh: true }));
// console.log(await review("deep_review", neutralVerificationPacket, { fresh: true }));
// console.log(await review("recheck", fullFlowPacket, { fresh: true }));
// console.log(await review("report", adjudicatedPacket, { fresh: true }));
// Optional: { readFile: authorizedSnapshotReader }; the caller owns source access.

After scan, prepare a fresh evidence packet for discover, deep_review, recheck and report. A stop reply is transport completion, not proof that the required stage artifact is complete: check it before advancing. A listing, failed read or model summary is not source review. Keep statuses candidate, supported_static, reproduced, rejected and unresolved distinct; severity, confidence, category and verification are separate. A proposed reproduction is not an executed one.

Length-truncated, native-tool-syntax and malformed replies stop without advancing history. Read non-enumerable error.response and error.requestMessages privately to recover complete assistant fields and matched tool results. HTTP/transport errors never replay automatically and may have incurred usage. Shell pending files retain attempts; only a complete text reply publishes an atomic state pair. Do not log these files, source, keys or recovery fields.

The optional tool loop uses flat read_file(path), validates before execution and returns a precise missing-field error with an example. One correction is allowed; another malformed call stops unresolved without guessing arguments. Tool IDs stay matched and assistant reasoning fields stay intact. Batching needs separate qualification against the exact model/runtime. The caller enforces configurable call/time/cost limits, records stop reasons and missing evidence, and preserves partials and accounting uncertainty. The sample’s timeoutMs bounds each HTTP exchange; separately bound the readFile callback and the whole-audit deadline. Defaults are provisional caller policy, not measured optima. Preserve adequate reasoning/output settings; reduce irrelevant context instead.

The request contract

Use your normal Bearer API key and Content-Type: application/json. The response is standard Chat Completions JSON; set stream: true for normal SSE events.

Also available at /api/v1/workflows/security-review.

FieldBehavior
modelRequired explicit model ID. The workflow does not choose or substitute a checkpoint.
messagesRequired array, including [] for intake. Send 0–255 caller messages with the current evidence packet or protocol-correct active history. The model’s context limit still applies.
stageOptional: auto, intake, scan, discover, deep_review, recheck or report.
max_tokensNormal output limit. Examples request 16384; input plus output must fit the selected model’s limits.
max_cost_micro_usdcNormal per-request spending ceiling, in millionths of a USDC. Increase it only if the current quote requires it and you accept that ceiling.
tools / stream / reasoning_effortNormal chat settings, subject to the selected model’s capabilities. The examples use non-streaming text replies.

The workflow supplies its system prompt. Caller system and developer messages are rejected on this endpoint. Conversation state is held by your app; there is no workflow-session ID to resume.

The final report derives coverage from actual source chunks and retrieval results bound to the frozen revision. It keeps rejected and unresolved outcomes, separates static support from reproduction, and adds no new findings. Record exact model, prompt version/hash, reasoning/output settings and per-call ceilings in comparisons. Historical success does not establish v2 precision or recall.

If a request fails

Read the returned error before sending another paid request. The examples do not retry automatically.

StatusWhat to check
400Check stage, message roles, history length and the model’s input/output limits.
401 / 403Check the API key and approved access to the selected model.
402Check available credit and whether the per-request ceiling covers the current quote.
404Check the URL and that your Router deployment includes this workflow endpoint. Also inspect any model-specific error.
429 / 503Check admission, capacity and model readiness. Retry only after checking the response and request status.