Drive Controller Pinning Audit from your own code
What this app does
Controller Pinning Audit reviews one unit of a Laravel application — a controller, the routes
that reach it, the Eloquent models it writes, its form requests and policies — and does one of two
jobs over it. The audit lane is a security review; the tests lane writes
the feature-test file that pins each finding, one test that fails on the pasted code and passes
once the fix lands. The app never runs your code; every answer is derived from the text you send.
The task field comes first
Every request carries task, and it decides the whole contract of the reply.
task | What it does | lane_detail carries | artifact |
|---|---|---|---|
audit | Authorization and IDOR, mass assignment, SQL and column-name injection, validation, unescaped Blade, CSRF, uploads, rate limits, password hashing, secrets and configuration. | surface, mass_assignment, owasp (ten rows, A01–A10) | kind: "none" |
tests | A complete Pest or PHPUnit feature-test file. With a handoff from an audit, one test per fixable finding. | test_plan, factories, pinning, run_command | kind: "test", the whole .php file in body |
An unknown or missing task is not an error: the model picks the closer lane, does only
that one, and names it in the first sentence of exec_summary. Check lane
in the reply rather than assuming.
The envelope and the error codes
Every endpoint lives under https://api.skillsafe.ai/v1/app-api and every response uses
the same two-shape envelope:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "...", "details": { ... } } }
Authentication is one header on every call except the guest mint:
Authorization: Bearer <token>. There is no slug header — the token is minted for
controller-pin-audit and carries the app with it.
| Code | HTTP | What causes it | What to do |
|---|---|---|---|
VALIDATION_ERROR | 400 | A body that is not a JSON object. | Send one flat object. Nothing is charged. |
UNAUTHORIZED | 401 | Missing, malformed or expired token. | Get a fresh one from the token page. |
FORBIDDEN | 403 | A guest token calling /run, or a token minted for another app. | Sign in and use a personal token. |
PAYMENT_REQUIRED | 402 | Balance below the run's min_credits. | Compare /estimate with /me before running; top up. |
NOT_FOUND | 404 | A job_id that belongs to a different token. | Poll with the token that created the job. |
RATE_LIMITED | 429 | A poll loop with no sleep. | Poll at most once a second and back off. |
INTERNAL | 500 | A failure on our side. | Retry with the same Idempotency-Key. |
/estimate also returns a warnings array. It names fields the app does not
declare (the common one is wrapping the body as {"input": {...}}) and required fields
you left out. A warning is not a rejection — the run would still happen and still be charged — so
treat a non-empty warnings as a bug in your request.
The input contract
One flat JSON object. Every field is a string.
| Field | Required | Meaning |
|---|---|---|
task | yes | audit or tests. |
code | yes | The Laravel source. Separate files with a marker line // file: app/Http/Controllers/PostController.php; findings quote those paths and line numbers counted from the line after each marker. The page sends at most 60,000 characters and cuts over-long files in the middle, leaving a // [... N characters cut from the middle of this file ...] marker the model is told to respect. |
context | no | Free text: who can sign up, which records are private, what is about to change. |
laravel_version | no | "12", "11", "10", "9" or "". It changes facts such as where CSRF exceptions live and whether $this->authorize() exists on the base controller. |
auth_stack | no | breeze, jetstream, fortify, sanctum, passport, custom or "". |
test_framework | no | pest (default) or phpunit; used by the tests lane. |
prescan_facts | no | A JSON string of the browser prescan (routes, controllers, models, form requests, flags). When present, the model must reconcile every flags[].id exactly once in coverage_check. Omit it from your own client and coverage_check comes back empty. |
handoff | no | A JSON string {"from":"audit","findings":[{"id":"LD-001","severity":"critical","category":"injection","rule":"...","location":"...","problem":"...","fix":"..."}]}. Send it with task: "tests" to get one pinning test per finding. |
retry_note | no | Only on a reformat retry: tells the model its previous reply did not parse. |
The output contract
The reply, at job.output.output, is one JSON object with the same outer envelope in
both lanes. Every array is always present, even when empty.
{
"lane": "audit",
"unit_title": "PostController + routes/web.php",
"verdict": "blocks-deploy | needs-work | ready",
"headline": "one sentence",
"exec_summary": "two to four sentences",
"facts": {
"files": 3,
"controllers": 1,
"routes": 5,
"models": 1,
"form_requests": 0,
"raw_queries": 1,
"laravel_version": "11",
"auth_stack": "breeze"
},
"findings": [
{
"id": "LD-001",
"severity": "critical | high | medium | low",
"category": "authz | mass-assignment | injection | xss | csrf | validation | upload | secrets | config | rate-limit | auth | other",
"rule": "kebab-case",
"location": "app/Http/Controllers/PostController.php:42",
"problem": "...",
"impact": "...",
"fix": "...",
"snippet": "corrected PHP or \"\""
}
],
"metrics": [
{
"label": "...",
"value": "3",
"target": "0",
"status": "ok | warn | fail | unknown",
"note": "..."
}
],
"lane_detail": {},
"artifact": {
"kind": "test | none",
"filename": "tests/Feature/PostControllerTest.php",
"language": "php",
"body": "",
"note": ""
},
"coverage_check": [
{
"flag_id": "PS-001",
"status": "confirmed | cleared | out-of-scope",
"note": "..."
}
],
"assumptions": [],
"open_questions": [],
"next_steps": [],
"summary": "..."
}
verdict is blocks-deploy when any finding is critical,
needs-work when the worst is high or medium, and
ready otherwise. In the tests lane it describes the code under test.
lane_detail for audit
{
"surface": [
{
"route": "PUT /posts/{post}",
"action": "PostController@update",
"middleware": "web, auth",
"authz": "none | policy | gate | form-request | scoped-query | inline",
"validation": "none | inline | form-request | partial",
"risk": "critical | high | medium | low | none",
"note": "..."
}
],
"mass_assignment": [
{
"model": "Post",
"protection": "guarded-empty | fillable | guarded | default",
"exposed_fields": [
"user_id"
],
"reached_from": "PostController@update via $request->all()"
}
],
"owasp": [
{
"category": "A01 Broken Access Control",
"status": "fail | pass | not-applicable | unknown",
"evidence": "..."
}
]
}
lane_detail for tests
{
"test_plan": [
{
"test_name": "it forbids updating another user's post",
"kind": "happy | authz | validation | guest | security | edge",
"pins": "LD-003",
"route": "PUT /posts/{post}",
"expect_today": "fails | passes | unknown",
"setup": "...",
"asserts": [
"..."
]
}
],
"factories": [
{
"model": "Post",
"states": [],
"note": "needs user_id via ->for($owner)"
}
],
"pinning": {
"pinned": [
"LD-001"
],
"unpinned": [
{
"finding_id": "LD-010",
"why": "..."
}
]
},
"run_command": "php artisan test --filter=PostControllerTest"
}
Every test_name appears in artifact.body (Pest: as the it()
description; PHPUnit: as the method name), and every handed-off finding id appears in exactly one
of pinning.pinned or pinning.unpinned. The page checks both; your client
should too.
Step by step
1. Get a token
The token page shows the token this browser holds, signs you in for a
personal one, and copies a ready-made shell export. A guest token is enough for
/me and /estimate; running a lane is metered and needs a personal token.
# Easiest: open /tokens.html on this app, sign in, press "Copy shell export":
export SKILLSAFE_TOKEN="YOUR_TOKEN"
# A guest token (enough for /me and /estimate, not for /run):
curl -s -X POST "https://api.skillsafe.ai/v1/app-api/guest" \
-H "Content-Type: application/json" \
-d '{"slug":"controller-pin-audit"}' | jq -r '.data.token'
import json, urllib.request
def mint_guest():
req = urllib.request.Request(
"https://api.skillsafe.ai/v1/app-api/guest",
data=json.dumps({"slug": "controller-pin-audit"}).encode(),
headers={"Content-Type": "application/json"}, method="POST")
with urllib.request.urlopen(req) as r:
return json.load(r)["data"]["token"]
TOKEN = "YOUR_TOKEN" # personal token from /tokens.html; mint_guest() for a guest
// Node 18+ or a browser. A personal token comes from /tokens.html.
const TOKEN = "YOUR_TOKEN";
async function mintGuest() {
const r = await fetch("https://api.skillsafe.ai/v1/app-api/guest", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ slug: "controller-pin-audit" }),
});
return (await r.json()).data.token;
}
package main
// A personal token comes from /tokens.html; read it from your own secret store.
const token = "YOUR_TOKEN"
// Guest token (for /me and /estimate only):
// POST https://api.skillsafe.ai/v1/app-api/guest {"slug":"controller-pin-audit"} -> data.token
// A personal token comes from /tokens.html.
static final String TOKEN = "YOUR_TOKEN";
// Guest token (for /me and /estimate only):
// POST https://api.skillsafe.ai/v1/app-api/guest {"slug":"controller-pin-audit"} -> data.token
require "net/http"
require "json"
TOKEN = "YOUR_TOKEN" # personal token from /tokens.html
def mint_guest
uri = URI("https://api.skillsafe.ai/v1/app-api/guest")
res = Net::HTTP.post(uri, { slug: "controller-pin-audit" }.to_json, "Content-Type" => "application/json")
JSON.parse(res.body)["data"]["token"]
end
<?php
$token = 'YOUR_TOKEN'; // personal token from /tokens.html
function mint_guest(): string {
$ch = curl_init('https://api.skillsafe.ai/v1/app-api/guest');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json'],
CURLOPT_POSTFIELDS => json_encode(['slug' => 'controller-pin-audit']),
]);
return json_decode(curl_exec($ch), true)['data']['token'];
}
// A personal token comes from /tokens.html.
const string Token = "YOUR_TOKEN";
// Guest token (for /me and /estimate only):
// POST https://api.skillsafe.ai/v1/app-api/guest {"slug":"controller-pin-audit"} -> data.token
2. A tiny client helper
One function that sends the token and unwraps the envelope.
BASE="https://api.skillsafe.ai/v1/app-api"
api() { # api METHOD PATH [BODY_FILE] -> prints .data, or the error and returns 1
local out
if [ -n "$3" ]; then
out=$(curl -s -X "$1" "$BASE$2" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" --data-binary @"$3")
else
out=$(curl -s -X "$1" "$BASE$2" -H "Authorization: Bearer $SKILLSAFE_TOKEN")
fi
if [ "$(printf '%s' "$out" | jq -r '.ok')" = "true" ]; then
printf '%s' "$out" | jq '.data'
else
printf '%s' "$out" | jq '.error' >&2; return 1
fi
}
BASE = "https://api.skillsafe.ai/v1/app-api"
def api(method, path, body=None, key=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode())
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
if key:
req.add_header("Idempotency-Key", key)
try:
with urllib.request.urlopen(req) as r:
env = json.load(r)
except urllib.error.HTTPError as e:
env = json.load(e)
if not env.get("ok"):
raise RuntimeError(env["error"]["code"] + ": " + env["error"]["message"])
return env["data"]
const BASE = "https://api.skillsafe.ai/v1/app-api";
async function api(method, path, body, key) {
const headers = { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json" };
if (key) headers["Idempotency-Key"] = key;
const r = await fetch(BASE + path, { method, headers, body: body ? JSON.stringify(body) : undefined });
const env = await r.json();
if (!env.ok) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
type envelope struct {
Ok bool `json:"ok"`
Data json.RawMessage `json:"data"`
Error *struct{ Code, Message string } `json:"error"`
}
func api(method, path string, body any, key string) (json.RawMessage, error) {
var rdr *bytes.Reader
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
} else {
rdr = bytes.NewReader(nil)
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
if key != "" {
req.Header.Set("Idempotency-Key", key)
}
res, err := http.DefaultClient.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 {
return nil, fmt.Errorf("%s: %s", env.Error.Code, env.Error.Message)
}
return env.Data, nil
}
import java.net.URI;
import java.net.http.*;
import com.fasterxml.jackson.databind.*;
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final HttpClient HTTP = HttpClient.newHttpClient();
static final ObjectMapper JSON = new ObjectMapper();
static JsonNode api(String method, String path, String body, String key) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.method(method, body == null ? HttpRequest.BodyPublishers.noBody()
: HttpRequest.BodyPublishers.ofString(body));
if (key != null) b.header("Idempotency-Key", key);
JsonNode env = JSON.readTree(HTTP.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (!env.path("ok").asBoolean())
throw new RuntimeException(env.at("/error/code").asText() + ": " + env.at("/error/message").asText());
return env.get("data");
}
BASE = "https://api.skillsafe.ai/v1/app-api"
def api(method, path, body = nil, key = nil)
uri = URI(BASE + path)
req = Net::HTTP.const_get(method.capitalize).new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key if key
req.body = body.to_json if body
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }
env = JSON.parse(res.body)
raise "#{env['error']['code']}: #{env['error']['message']}" unless env["ok"]
env["data"]
end
<?php
const BASE = 'https://api.skillsafe.ai/v1/app-api';
function api(string $method, string $path, ?array $body = null, ?string $key = null): array {
global $token;
$headers = ['Authorization: Bearer ' . $token, 'Content-Type: application/json'];
if ($key) $headers[] = 'Idempotency-Key: ' . $key;
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => $headers,
]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (empty($env['ok'])) throw new RuntimeException($env['error']['code'] . ': ' . $env['error']['message']);
return $env['data'];
}
using System.Net.Http;
using System.Text;
using System.Text.Json;
static readonly HttpClient Http = new();
const string Base = "https://api.skillsafe.ai/v1/app-api";
static async Task<JsonElement> Api(HttpMethod method, string path, string? body = null, string? key = null)
{
var req = new HttpRequestMessage(method, Base + path);
req.Headers.Add("Authorization", "Bearer " + Token);
if (key != null) req.Headers.Add("Idempotency-Key", key);
if (body != null) req.Content = new StringContent(body, Encoding.UTF8, "application/json");
var res = await Http.SendAsync(req);
var env = JsonDocument.Parse(await res.Content.ReadAsStringAsync()).RootElement;
if (!env.GetProperty("ok").GetBoolean())
throw new Exception(env.GetProperty("error").GetProperty("code").GetString());
return env.GetProperty("data");
}
3. Check the session and the balance
GET /me returns subject_type (user or guest) and, for a user, credits.
api GET /me | jq '{subject_type, username, credits}'
# {"subject_type":"user","username":"you","credits":48210}
me = api("GET", "/me")
print(me["subject_type"], me.get("credits")) # "user" 48210
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits); // "user" 48210
me, err := api("GET", "/me", nil, "")
if err != nil { panic(err) }
fmt.Println(string(me)) // {"subject_type":"user","credits":48210,...}
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type").asText() + " " + me.path("credits").asLong());
me = api("GET", "/me")
puts "#{me['subject_type']} #{me['credits']}" # user 48210
$me = api('GET', '/me');
echo $me['subject_type'], ' ', $me['credits'] ?? 0, PHP_EOL; // user 48210
var me = await Api(HttpMethod.Get, "/me");
Console.WriteLine($"{me.GetProperty("subject_type")} {me.GetProperty("credits")}");
4. Price the lane — free, no job
POST /estimate takes the same body as /run and returns
hold_credits (the reservation, priced at the full output cap), min_credits,
the resolved model and markup_bps. The two lanes hold different amounts,
so estimate the lane you are about to run. You are charged for what the run actually uses, which is
usually far less than the hold.
# Write the body once. The quoted 'JSON' delimiter stops the shell expanding the
# $request and $post inside the PHP source.
cat > body.json <<'JSON'
{"task":"audit",
"code":"// file: routes/web.php\n<?php\nRoute::middleware('auth')->group(function () {\n Route::put('/posts/{post}', [PostController::class, 'update']);\n});\n\n// file: app/Http/Controllers/PostController.php\n<?php\nclass PostController extends Controller\n{\n public function update(Request $request, Post $post)\n {\n $post->update($request->all());\n return back();\n }\n}",
"context":"Community blog; any signed-in member can post.",
"laravel_version":"11","auth_stack":"breeze","test_framework":"pest"}
JSON
api POST /estimate body.json | jq '{hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
# {"hold_credits":2140,"min_credits":180,"model":"gpt-5.6-terra","model_alias":"gpt-terra","markup_bps":1000,"warnings":[]}
body = json.load(open("body.json"))
for lane in ("audit", "tests"):
body["task"] = lane
est = api("POST", "/estimate", body)
print(lane, est["hold_credits"], est.get("min_credits"), est.get("warnings"))
import { readFileSync } from "node:fs";
const body = JSON.parse(readFileSync("body.json", "utf8"));
for (const lane of ["audit", "tests"]) {
const est = await api("POST", "/estimate", { ...body, task: lane });
console.log(lane, est.hold_credits, est.min_credits, est.warnings);
}
raw, _ := os.ReadFile("body.json")
var body map[string]any
json.Unmarshal(raw, &body)
for _, lane := range []string{"audit", "tests"} {
body["task"] = lane
est, err := api("POST", "/estimate", body, "")
if err != nil { panic(err) }
fmt.Println(lane, string(est))
}
ObjectNode body = (ObjectNode) JSON.readTree(java.nio.file.Files.readString(java.nio.file.Path.of("body.json")));
for (String lane : new String[]{"audit", "tests"}) {
body.put("task", lane);
JsonNode est = api("POST", "/estimate", JSON.writeValueAsString(body), null);
System.out.println(lane + " holds " + est.get("hold_credits").asLong());
}
body = JSON.parse(File.read("body.json"))
%w[audit tests].each do |lane|
est = api("POST", "/estimate", body.merge("task" => lane))
puts "#{lane} holds #{est['hold_credits']} (min #{est['min_credits']})"
end
$body = json_decode(file_get_contents('body.json'), true);
foreach (['audit', 'tests'] as $lane) {
$est = api('POST', '/estimate', ['task' => $lane] + $body);
echo $lane, ' holds ', $est['hold_credits'], PHP_EOL;
}
var body = JsonNode.Parse(File.ReadAllText("body.json"))!.AsObject();
foreach (var lane in new[] { "audit", "tests" })
{
body["task"] = lane;
var est = await Api(HttpMethod.Post, "/estimate", body.ToJsonString());
Console.WriteLine($"{lane} holds {est.GetProperty("hold_credits")}");
}
5. Run it, then poll the job
POST /run returns {"job_id": "...", "status": "queued"}. Poll
GET /jobs/{job_id} until status is succeeded,
failed or cancelled. Always send an
Idempotency-Key built from the lane plus a hash of the input: the audit and the
tests over the same paste are two different runs, and a retry after a timeout must reuse the key or
you pay twice.
Idempotency-Key: controller-pin-audit:<task>:<12 hex of sha256(task, code, context, versions, handoff)>:a<attempt>
LANE=$(jq -r '.task' body.json)
DIGEST=$(shasum -a 256 body.json | cut -c1-12)
KEY="controller-pin-audit:$LANE:$DIGEST:a1" # reuse this exact key on any retry
JOB=$(curl -s -X POST "$BASE/run" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" -H "Idempotency-Key: $KEY" \
--data-binary @body.json | jq -r '.data.job_id')
while :; do
OUT=$(api GET "/jobs/$JOB")
case "$(printf '%s' "$OUT" | jq -r '.status')" in succeeded|failed|cancelled) break ;; esac
sleep 2
done
printf '%s' "$OUT" | jq -r '.output.output' > review.json
jq '.verdict, .headline' review.json
import hashlib, time
def idem_key(body, attempt=1):
seed = "\x00".join([body["task"], body["code"], body.get("context", ""),
body.get("laravel_version", ""), body.get("auth_stack", ""),
body.get("test_framework", ""), body.get("handoff", "")])
return "controller-pin-audit:%s:%s:a%d" % (body["task"], hashlib.sha256(seed.encode()).hexdigest()[:12], attempt)
body["task"] = "audit"
key = idem_key(body)
job = api("POST", "/run", body, key)
while job["status"] not in ("succeeded", "failed", "cancelled"):
time.sleep(2)
job = api("GET", "/jobs/" + job["job_id"])
review = json.loads(job["output"]["output"])
print(review["verdict"], review["headline"], job.get("charged_credits"))
import { createHash } from "node:crypto";
function idemKey(b, attempt = 1) {
const seed = [b.task, b.code, b.context, b.laravel_version, b.auth_stack, b.test_framework, b.handoff || ""].join("\0");
return `controller-pin-audit:${b.task}:${createHash("sha256").update(seed).digest("hex").slice(0, 12)}:a${attempt}`;
}
const runBody = { ...body, task: "audit" };
let job = await api("POST", "/run", runBody, idemKey(runBody));
while (!["succeeded", "failed", "cancelled"].includes(job.status)) {
await new Promise((r) => setTimeout(r, 2000));
job = await api("GET", "/jobs/" + job.job_id);
}
const review = JSON.parse(job.output.output);
console.log(review.verdict, review.headline, job.charged_credits);
sum := sha256.Sum256([]byte(fmt.Sprint(body["task"], body["code"], body["context"])))
key := fmt.Sprintf("controller-pin-audit:%s:%x:a1", body["task"], sum[:6])
raw, err := api("POST", "/run", body, key)
if err != nil { panic(err) }
var job struct {
JobID string `json:"job_id"`
Status string `json:"status"`
Output struct{ Output string `json:"output"` } `json:"output"`
}
json.Unmarshal(raw, &job)
for job.Status != "succeeded" && job.Status != "failed" && job.Status != "cancelled" {
time.Sleep(2 * time.Second)
raw, _ = api("GET", "/jobs/"+job.JobID, nil, "")
json.Unmarshal(raw, &job)
}
fmt.Println(job.Output.Output[:200])
String payload = JSON.writeValueAsString(body);
String digest = java.util.HexFormat.of().formatHex(
java.security.MessageDigest.getInstance("SHA-256").digest(payload.getBytes())).substring(0, 12);
String key = "controller-pin-audit:" + body.get("task").asText() + ":" + digest + ":a1";
JsonNode job = api("POST", "/run", payload, key);
while (!java.util.Set.of("succeeded", "failed", "cancelled").contains(job.get("status").asText())) {
Thread.sleep(2000);
job = api("GET", "/jobs/" + job.get("job_id").asText(), null, null);
}
JsonNode review = JSON.readTree(job.at("/output/output").asText());
System.out.println(review.get("verdict").asText() + " - " + review.get("headline").asText());
require "digest"
body["task"] = "audit"
seed = body.values_at("task", "code", "context", "laravel_version", "auth_stack", "test_framework").join("\0")
key = "controller-pin-audit:#{body['task']}:#{Digest::SHA256.hexdigest(seed)[0, 12]}:a1"
job = api("POST", "/run", body, key)
until %w[succeeded failed cancelled].include?(job["status"])
sleep 2
job = api("GET", "/jobs/#{job['job_id']}")
end
review = JSON.parse(job["output"]["output"])
puts review["verdict"], review["headline"]
$body['task'] = 'audit';
$seed = implode("\0", [$body['task'], $body['code'], $body['context'] ?? '', $body['laravel_version'] ?? '']);
$key = 'controller-pin-audit:' . $body['task'] . ':' . substr(hash('sha256', $seed), 0, 12) . ':a1';
$job = api('POST', '/run', $body, $key);
while (!in_array($job['status'], ['succeeded', 'failed', 'cancelled'], true)) {
sleep(2);
$job = api('GET', '/jobs/' . $job['job_id']);
}
$review = json_decode($job['output']['output'], true);
echo $review['verdict'], ' - ', $review['headline'], PHP_EOL;
body["task"] = "audit";
var payload = body.ToJsonString();
var digest = Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(payload)))[..12].ToLower();
var key = $"controller-pin-audit:audit:{digest}:a1";
var job = await Api(HttpMethod.Post, "/run", payload, key);
while (job.GetProperty("status").GetString() is not ("succeeded" or "failed" or "cancelled"))
{
await Task.Delay(2000);
job = await Api(HttpMethod.Get, "/jobs/" + job.GetProperty("job_id").GetString());
}
var review = JsonDocument.Parse(job.GetProperty("output").GetProperty("output").GetString()!).RootElement;
Console.WriteLine(review.GetProperty("verdict"));
6. Or stream it
POST /run-stream takes the same body and key and answers with server-sent events:
event: job first (keep the job_id so you can fall back to polling),
optional event: delta frames with a text fragment, then
event: done carrying the settled job — status, output,
charged_credits, truncated. Treat done as authoritative;
deltas may be absent entirely.
curl -s -N -X POST "$BASE/run-stream" -H "Authorization: Bearer $SKILLSAFE_TOKEN" \
-H "Content-Type: application/json" -H "Idempotency-Key: $KEY" --data-binary @body.json
# event: job
# data: {"job_id":"job_7Kq2","status":"running"}
#
# event: delta (may be absent, or arrive as one frame)
# data: {"text":"{\"lane\":\"audit\",..."}
#
# event: done
# data: {"job_id":"job_7Kq2","status":"succeeded","charged_credits":1180,"truncated":false,"output":{"output":"{...}"}}
req = urllib.request.Request(BASE + "/run-stream", data=json.dumps(body).encode(), method="POST")
for h, v in (("Authorization", "Bearer " + TOKEN), ("Content-Type", "application/json"), ("Idempotency-Key", key)):
req.add_header(h, v)
event, text, done = None, "", None
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().rstrip("\n")
if line.startswith("event:"):
event = line[6:].strip()
elif line.startswith("data:"):
data = json.loads(line[5:].strip())
if event == "delta":
text += data.get("text", "")
elif event == "done":
done = data
elif event == "error":
raise RuntimeError(data.get("message"))
reply = done["output"]["output"] if done else text # done is authoritative
const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: { Authorization: "Bearer " + TOKEN, "Content-Type": "application/json", "Idempotency-Key": key },
body: JSON.stringify(body),
});
const reader = res.body.getReader();
const dec = new TextDecoder();
let buf = "", text = "", done = null;
for (;;) {
const { value, done: end } = await reader.read();
if (end) break;
buf += dec.decode(value, { stream: true });
let i;
while ((i = buf.indexOf("\n\n")) >= 0) {
const frame = buf.slice(0, i); buf = buf.slice(i + 2);
const ev = (frame.match(/^event:\s*(.*)$/m) || [])[1];
const data = (frame.match(/^data:\s*(.*)$/m) || [])[1];
if (!data) continue;
const d = JSON.parse(data);
if (ev === "delta") text += d.text || "";
if (ev === "done") done = d;
if (ev === "error") throw new Error(d.message);
}
}
const reply = done ? done.output.output : text;
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(payload))
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", key)
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
sc.Buffer(make([]byte, 1<<20), 1<<24)
event, reply := "", ""
for sc.Scan() {
line := sc.Text()
switch {
case strings.HasPrefix(line, "event:"):
event = strings.TrimSpace(line[6:])
case strings.HasPrefix(line, "data:") && event == "done":
var d struct{ Output struct{ Output string } }
json.Unmarshal([]byte(strings.TrimSpace(line[5:])), &d)
reply = d.Output.Output
}
}
var req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN).header("Content-Type", "application/json")
.header("Idempotency-Key", key).POST(HttpRequest.BodyPublishers.ofString(payload)).build();
String event = null, reply = null;
for (String line : (Iterable<String>) HTTP.send(req, HttpResponse.BodyHandlers.ofLines()).body()::iterator) {
if (line.startsWith("event:")) event = line.substring(6).trim();
else if (line.startsWith("data:") && "done".equals(event))
reply = JSON.readTree(line.substring(5).trim()).at("/output/output").asText();
}
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req["Idempotency-Key"] = key
req.body = body.to_json
reply, event, buf = nil, nil, +""
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) do |res|
res.read_body do |chunk|
buf << chunk
while (line = buf.slice!(/\A.*\n/))
line = line.chomp
event = line[6..].strip if line.start_with?("event:")
reply = JSON.parse(line[5..].strip)["output"]["output"] if line.start_with?("data:") && event == "done"
end
end
end
end
$event = null; $reply = null; $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', 'Idempotency-Key: ' . $key],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) use (&$event, &$reply, &$buf) {
$buf .= $chunk;
while (($i = strpos($buf, "\n")) !== false) {
$line = rtrim(substr($buf, 0, $i)); $buf = substr($buf, $i + 1);
if (str_starts_with($line, 'event:')) $event = trim(substr($line, 6));
elseif (str_starts_with($line, 'data:') && $event === 'done')
$reply = json_decode(trim(substr($line, 5)), true)['output']['output'];
}
return strlen($chunk);
},
]);
curl_exec($ch);
var sreq = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream")
{ Content = new StringContent(payload, Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Token);
sreq.Headers.Add("Idempotency-Key", key);
using var sres = await Http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rd = new StreamReader(await sres.Content.ReadAsStreamAsync());
string? ev = null, reply = null, line;
while ((line = await rd.ReadLineAsync()) != null)
{
if (line.StartsWith("event:")) ev = line[6..].Trim();
else if (line.StartsWith("data:") && ev == "done")
reply = JsonDocument.Parse(line[5..].Trim()).RootElement.GetProperty("output").GetProperty("output").GetString();
}
7. Parse the reply
Take the text from the first { to the last }, parse it, check
lane is the one you asked for, and — if you sent prescan_facts — check every
flag id came back in coverage_check. If truncated is true the balance
capped the output; render what arrived and say it was cut short.
jq '{lane, verdict, headline,
findings: [.findings[] | {id, severity, rule, location}],
unreconciled: (.coverage_check | length)}' review.json
# tests lane: write the generated file where it belongs
jq -r '.artifact.body' review.json > tests/Feature/PostControllerTest.php
review = json.loads(reply[reply.index("{"):reply.rindex("}") + 1])
assert review["lane"] == body["task"], "the router picked a different lane"
flags = json.loads(body["prescan_facts"])["flags"] if body.get("prescan_facts") else []
seen = {c["flag_id"] for c in review["coverage_check"]}
missing = [f["id"] for f in flags if f["id"] not in seen]
print(review["verdict"], len(review["findings"]), "findings; unreconciled flags:", missing)
if review["lane"] == "tests" and review["artifact"]["kind"] == "test":
open(review["artifact"]["filename"].split("/")[-1], "w").write(review["artifact"]["body"])
const review = JSON.parse(reply.slice(reply.indexOf("{"), reply.lastIndexOf("}") + 1));
if (review.lane !== body.task) throw new Error("router picked " + review.lane);
const flags = body.prescan_facts ? JSON.parse(body.prescan_facts).flags : [];
const seen = new Set(review.coverage_check.map((c) => c.flag_id));
console.log(review.verdict, review.findings.length, "findings; unreconciled:",
flags.filter((f) => !seen.has(f.id)).map((f) => f.id));
start, end := strings.Index(reply, "{"), strings.LastIndex(reply, "}")
var review struct {
Lane, Verdict, Headline string
Findings []struct{ ID, Severity, Rule, Location string }
Artifact struct{ Kind, Filename, Body string }
}
if err := json.Unmarshal([]byte(reply[start:end+1]), &review); err != nil { panic(err) }
fmt.Println(review.Verdict, len(review.Findings))
JsonNode review = JSON.readTree(reply.substring(reply.indexOf('{'), reply.lastIndexOf('}') + 1));
System.out.println(review.get("verdict").asText() + ", " + review.get("findings").size() + " findings");
if ("test".equals(review.at("/artifact/kind").asText()))
java.nio.file.Files.writeString(java.nio.file.Path.of("GeneratedTest.php"), review.at("/artifact/body").asText());
review = JSON.parse(reply[reply.index("{")..reply.rindex("}")])
raise "router picked #{review['lane']}" unless review["lane"] == body["task"]
puts "#{review['verdict']}: #{review['findings'].size} findings"
File.write(File.basename(review["artifact"]["filename"]), review["artifact"]["body"]) if review["artifact"]["kind"] == "test"
$review = json_decode(substr($reply, strpos($reply, '{'), strrpos($reply, '}') - strpos($reply, '{') + 1), true);
if ($review['lane'] !== $body['task']) throw new RuntimeException('router picked ' . $review['lane']);
echo $review['verdict'], ': ', count($review['findings']), ' findings', PHP_EOL;
if ($review['artifact']['kind'] === 'test') {
file_put_contents(basename($review['artifact']['filename']), $review['artifact']['body']);
}
var json = reply![reply.IndexOf('{')..(reply.LastIndexOf('}') + 1)];
var rv = JsonDocument.Parse(json).RootElement;
Console.WriteLine($"{rv.GetProperty("verdict")}: {rv.GetProperty("findings").GetArrayLength()} findings");
Two worked examples, one per lane
Lane 1 — audit
The blog controller bundled with the app, with its prescan attached (shortened here).
{
"task": "audit",
"code": "// file: routes/web.php\n<?php\n\nuse App\\Http\\Controllers\\PostController;\nuse App\\Http\\Controllers\\Auth\\RegisterController;\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('/', [PostController::class, 'index']);\nRoute::post('/register', [RegisterController::class, 'store']);\nRoute::post('/login', [RegisterController::class, 'login']);\n\nRoute::middleware('auth')->group(function () {\n Route::post('/posts', [PostCont\n// ... the rest of the paste ...",
"context": "Small community blog on Laravel 11 with Breeze. Any signed-in member can post. We are about to open registration to the public and want to know what breaks first.",
"laravel_version": "11",
"auth_stack": "breeze",
"test_framework": "pest",
"prescan_facts": "{\"title\": \"PostController + RegisterController + routes/web.php\", \"flags\": [{\"id\": \"PS-001\", \"rule\": \"raw-sql-interpolated\", \"severity\": \"critical\", \"category\": \"injection\", \"title\": \"DB::select() builds SQL from interpolated or concatenated values\", \"where\": \"app/Http/Controllers/PostController.php:16\"}, {\"id\": \"PS-002\", \"rule\": \"missing-authz\", \"severity\": \"critical\", \"category\": \"authz\", \"title\": \"PostController@destroy changes a record without an authorization check\", \"where\": \"app/Http/Controllers/PostController.php:35\"}]} (shortened)"
}
Lane 2 — tests
The same paste with the audit's findings handed over, so the reply pins each one.
{
"task": "tests",
"code": "// file: routes/web.php\n<?php\n\nuse App\\Http\\Controllers\\PostController;\nuse App\\Http\\Controllers\\Auth\\RegisterController;\nuse Illuminate\\Support\\Facades\\Route;\n\nRoute::get('/', [PostController::class, 'index']);\nRoute::post('/register', [RegisterController::class, 'store']);\nRoute::post('/login', [RegisterController::class, 'login']);\n\nRoute::middleware('auth')->group(function () {\n Route::post('/posts', [PostCont\n// ... the rest of the paste ...",
"context": "Small community blog on Laravel 11 with Breeze. Any signed-in member can post. We are about to open registration to the public and want to know what breaks first.",
"laravel_version": "11",
"auth_stack": "breeze",
"test_framework": "pest",
"prescan_facts": "{\"title\": \"PostController + RegisterController + routes/web.php\", \"flags\": [{\"id\": \"PS-001\", \"rule\": \"raw-sql-interpolated\", \"severity\": \"critical\", \"category\": \"injection\", \"title\": \"DB::select() builds SQL from interpolated or concatenated values\", \"where\": \"app/Http/Controllers/PostController.php:16\"}, {\"id\": \"PS-002\", \"rule\": \"missing-authz\", \"severity\": \"critical\", \"category\": \"authz\", \"title\": \"PostController@destroy changes a record without an authorization check\", \"where\": \"app/Http/Controllers/PostController.php:35\"}]} (shortened)",
"handoff": "{\"from\": \"audit\", \"findings\": [{\"id\": \"LD-001\", \"severity\": \"critical\", \"category\": \"injection\", \"rule\": \"raw-sql-interpolated\", \"location\": \"app/Http/Controllers/PostController.php:16\", \"problem\": \"PostController@index interpolates both $term (the q parameter) and $sort (the sort parameter) straight into the DB::select string: \\\"... LIKE '%$term%' ORDER BY $sort DESC\\\". Neither value is bound or validated, and GET / sits outside the auth group.\", \"fix\": \"Validate q as a short string and sort against an allow-list, and replace the raw DB::select with the query builder so the search term is a bound parameter. One code path then serves both branches and always returns a paginator.\"}, {\"id\": \"LD-002\", \"severity\": \"critical\", \"category\": \"authz\", \"rule\": \"state-changing-get-unauthenticated\", \"location\": \"routes/web.php:18\", \"problem\": \"Route::get('/posts/{post}/delete', [PostController::class, 'destroy']) is declared outside the Route::middleware('auth') group and on GET. It reaches PostController@destroy, which deletes the route-model-bound Post with no check of any kind.\", \"fix\": \"Delete the GET route. Deletion already has the DELETE /posts/{post} route inside the auth group; submit it from a form with @csrf and @method('DELETE'), and add the ownership check from LD-003.\"}]} (shortened)"
}
Both examples replay for free on the app page — load "the blog controller that leaks" and run either lane.