Drive Bicep Gate from your pipeline
Everything the web page does is available over HTTP. Base URL:
https://api.skillsafe.ai/v1/app-api. Every response is the same envelope —
{"ok": true, "data": {...}} on success,
{"ok": false, "error": {"code": "...", "message": "...", "details": {...}}} on failure.
Check ok before you touch data.
The task field comes first
Bicep Gate is one app with five lanes over one work object. Every request must carry a
task field naming the lane. The lane decides which fourth section comes back
and what the review is about; the other six sections are identical across all five.
task | Fourth section | What the lane answers | Source skill |
|---|---|---|---|
readiness | DEPLOY ORDER | Will this deploy unattended? Parameters that prompt, secrets in outputs, azd service tags, scope, API versions, naming rules. | @microsoft/azure-prepare |
capacity | QUOTA REQUESTS | Which allowance runs out first. Per SKU family per region. Never what you have — only what the template draws. | @microsoft/azure-quotas |
cost | COST DRIVERS | The shape of the bill: standing floor vs consumption vs unbounded. No figures, ever. | @microsoft/azure-cost |
rightsize | SIZING TABLE | Families, burstable credits, orchestration mode, disk tier, zones, scaling rules. A Resource/Current/Suggested/Why table. | @microsoft/azure-compute |
aks | DAY-0 CHECKLIST | The AKS decisions that cannot be changed after the cluster exists. | @microsoft/azure-kubernetes |
If task is absent or is not one of those five, the reviewer picks the closest lane and
names its choice in the first line of ## SUMMARY rather than blending two contracts.
Do not rely on that — send the field.
The input object
These are the exact fields app.js submits. Anything else is ignored.
| Field | Type | Meaning |
|---|---|---|
task | string | One of the five lane ids above. Required. |
template | string | The pasted deployment definition. Clipped at 60,000 characters, with an inline marker where content was removed. |
template_clipped | boolean | True when the app clipped it. |
template_clip_note | string | Human-readable note about what was cut. |
environment | string | unstated, dev, staging or production. |
context | string | The user's free-text note. May be empty. |
masked | boolean | True when identifiers were replaced with placeholders in EVERY field, this one included. |
prescan | object | The deterministic in-browser scan. See below. |
The prescan object, and why unknown matters
prescan is what the browser computed from the same text, before any model saw it. It
carries format, target_scope, counts, resources,
regions, quota, unresolved_modules, tally,
checks and coverage.
Every checks[].verdict is "pass", "fail" or
"unknown", and every coverage value is "yes",
"no" or "unknown". Those are three different claims, not two:
"no"/"fail"— the scan saw the whole surface where the thing would appear, and it was not there. You may act on it."unknown"— the scan could not see the whole surface, usually because amodulereference points at a file that was not pasted. The thing may well be declared there. Reporting it as absent would be a fabrication, and the system prompt forbids it.
unresolved_modules lists exactly which files were missing. If you are calling this API
from a pipeline, you have the whole repository — concatenate every .bicep file
with ==== path/to/file.bicep ==== separators and the unknowns turn into answers.
The output contract
data.output.text is Markdown with exactly seven ## headings, in order.
The parser in app.js finds them with
/^[ \t]*#{2,3}[ \t]*([A-Z0-9][A-Z0-9 \-'/]*?)[ \t]*:?[ \t]*$/m, so headings are
ALL CAPS on their own line.
## VERDICT one line: GO | GO WITH CHANGES | STOP, then one sentence
## SUMMARY two to four sentences
## FINDINGS - [critical|high|medium|low] Title — detail (resource: x, line: N)
## ...or the single word None
## <LANE SECTION> the per-lane section from the table above
## UNKNOWNS bullets, or None
## NEXT STEPS numbered, at most six
## GROUNDING - claim → where it came from
A section body of None is complete, not missing. The app counts
recovery against the seven sections this lane must return, so a clean template that
legitimately returns None in ## FINDINGS reports 7 of 7, never 6 of 7.
Step 1 — get a token
Tokens are per-app. The easiest way to get one for Bicep Gate is the token page: it reads the token this browser already holds, shows it masked, and copies it as a shell export. Sign in there first if you want the run billed to your own credits rather than to a guest wallet.
Programmatically, mint a guest token:
curl -s -X POST https://api.skillsafe.ai/v1/app-api/guest \
-H "Content-Type: application/json" \
-d '{"slug": "bicep-gate"}'
Then send it as Authorization: Bearer YOUR_TOKEN on every call below.
Step 2 — confirm the session
GET /me returns subject_type (user or guest) and credits. Call it once to prove the token works before spending anything.
curl -s https://api.skillsafe.ai/v1/app-api/me \
-H "Authorization: Bearer YOUR_TOKEN"import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(path, body=None):
data = json.dumps(body).encode() if body is not None else None
req = urllib.request.Request(BASE + path, data=data, method="POST" if data else "GET")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
with urllib.request.urlopen(req) as r:
payload = json.loads(r.read())
if not payload.get("ok"):
raise RuntimeError(payload["error"]["code"] + ": " + payload["error"]["message"])
return payload["data"]
print(call("/me"))const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
async function call(path, body) {
const res = await fetch(BASE + path, {
method: body ? "POST" : "GET",
headers: { "Authorization": `Bearer ${TOKEN}`, "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined
});
const payload = await res.json();
if (!payload.ok) throw new Error(`${payload.error.code}: ${payload.error.message}`);
return payload.data;
}
console.log(await call("/me"));package main
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
)
const base = "https://api.skillsafe.ai/v1/app-api"
const token = "YOUR_TOKEN"
func call(path string, body any) (map[string]any, error) {
var rdr io.Reader
method := "GET"
if body != nil {
b, _ := json.Marshal(body)
rdr = bytes.NewReader(b)
method = "POST"
}
req, _ := http.NewRequest(method, base+path, rdr)
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Content-Type", "application/json")
res, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer res.Body.Close()
var p struct {
Ok bool `json:"ok"`
Data map[string]any `json:"data"`
Error struct{ Code, Message string } `json:"error"`
}
json.NewDecoder(res.Body).Decode(&p)
if !p.Ok {
return nil, fmt.Errorf("%s: %s", p.Error.Code, p.Error.Message)
}
return p.Data, nil
}
func main() {
me, err := call("/me", nil)
fmt.Println(me, err)
}import java.net.URI;
import java.net.http.*;
public class BicepGate {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static final String TOKEN = "YOUR_TOKEN";
static final HttpClient CLIENT = HttpClient.newHttpClient();
static String call(String path, String jsonBody) throws Exception {
HttpRequest.Builder b = HttpRequest.newBuilder(URI.create(BASE + path))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json");
HttpRequest req = (jsonBody == null)
? b.GET().build()
: b.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
HttpResponse<String> res = CLIENT.send(req, HttpResponse.BodyHandlers.ofString());
return res.body();
}
public static void main(String[] args) throws Exception {
System.out.println(call("/me", null));
}
}require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN"
def call(path, body = nil)
uri = URI(BASE + path)
req = body ? Net::HTTP::Post.new(uri) : Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{TOKEN}"
req["Content-Type"] = "application/json"
req.body = JSON.dump(body) if body
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |h| h.request(req) }
payload = JSON.parse(res.body)
raise "#{payload['error']['code']}: #{payload['error']['message']}" unless payload["ok"]
payload["data"]
end
puts call("/me")<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
const TOKEN = "YOUR_TOKEN";
function call(string $path, ?array $body = null): array {
$ch = curl_init(BASE . $path);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
]);
if ($body !== null) {
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
}
$payload = json_decode(curl_exec($ch), true);
curl_close($ch);
if (empty($payload["ok"])) {
throw new RuntimeException($payload["error"]["code"] . ": " . $payload["error"]["message"]);
}
return $payload["data"];
}
print_r(call("/me"));using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
const string Base = "https://api.skillsafe.ai/v1/app-api";
const string Token = "YOUR_TOKEN";
var http = new HttpClient();
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Token);
async Task<JsonElement> Call(string path, object? body = null)
{
HttpResponseMessage res = body is null
? await http.GetAsync(Base + path)
: await http.PostAsync(Base + path,
new StringContent(JsonSerializer.Serialize(body), Encoding.UTF8, "application/json"));
JsonDocument doc = JsonDocument.Parse(await res.Content.ReadAsStringAsync());
if (!doc.RootElement.GetProperty("ok").GetBoolean())
throw new Exception(doc.RootElement.GetProperty("error").GetProperty("code").GetString());
return doc.RootElement.GetProperty("data");
}
Console.WriteLine(await Call("/me"));Step 3 — build the input and price it
A complete run_input, abbreviated in prescan for readability:
{
"task": "readiness",
"template": "targetScope = 'resourceGroup'\n\nparam location string = 'westeurope'\n\nresource plan 'Microsoft.Web/serverfarms@2024-04-01' = {\n name: 'app-plan'\n location: location\n sku: { name: 'P1v3' }\n}",
"template_clipped": false,
"template_clip_note": "",
"environment": "production",
"context": "EU only, deploying from a pipeline",
"masked": false,
"prescan": {
"format": "bicep",
"target_scope": "resourceGroup",
"counts": {
"resources": 1
},
"coverage": {
"r_modules_resolved": "yes",
"r_managed_identity": "unknown"
},
"unresolved_modules": []
}
}
POST /estimate is free and starts no job. It returns
hold_credits (what is reserved, priced against the full output cap),
min_credits (the floor below which the run is refused), model,
model_alias and markup_bps. hold_credits differs per lane
— re-estimate when you change task, and never show one lane's hold for another's
run.
curl -s https://api.skillsafe.ai/v1/app-api/estimate \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-d '{"input": {"task": "capacity", "template": "resource aks ...", "environment": "production", "context": "", "masked": false, "prescan": {}}}' est = call("/estimate", {"input": run_input})
print(est["hold_credits"], est["min_credits"], est["model"], est["model_alias"], est["markup_bps"])const est = await call("/estimate", { input: runInput });
console.log(est.hold_credits, est.min_credits, est.model, est.model_alias, est.markup_bps);est, err := call("/estimate", map[string]any{"input": runInput})
fmt.Println(est["hold_credits"], est["model_alias"], err)String est = call("/estimate", "{\"input\": " + runInputJson + "}");
System.out.println(est);est = call("/estimate", { "input" => run_input })
puts est["hold_credits"], est["model_alias"]$est = call("/estimate", ["input" => $runInput]);
echo $est["hold_credits"], " ", $est["model_alias"];JsonElement est = await Call("/estimate", new { input = runInput });
Console.WriteLine(est.GetProperty("hold_credits"));Step 4 — run it, and poll
POST /run returns a job_id; poll GET /jobs/{id} until
status is succeeded, failed or cancelled. This
is the metered call — it charges credits.
Always send an Idempotency-Key. The app builds it as
bicep-gate:<task>:<hash of the input>:<nonce>. The lane is in the key
because two lanes over one template are two distinct runs and must not collide; the nonce is minted
once per deliberate run, so a network retry reuses the key and cannot double-bill, while a genuine
re-run of identical input is a new run rather than a replayed answer.
If truncated comes back true, the balance sat between
min_credits and hold_credits and the output cap was reduced. Treat the
answer as partial and say so — do not present it as complete.
# submit
JOB=$(curl -s https://api.skillsafe.ai/v1/app-api/run \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: bicep-gate:capacity:9f2ab130:n1" \
-d '{"input": {"task": "capacity", "template": "...", "environment": "production", "context": "", "masked": false, "prescan": {}}}' \
| python3 -c 'import sys,json; print(json.load(sys.stdin)["data"]["job_id"])')
# poll to a terminal state
until curl -s https://api.skillsafe.ai/v1/app-api/jobs/$JOB -H "Authorization: Bearer YOUR_TOKEN" \
| grep -q '"status":"succeeded"'; do sleep 2; done
curl -s https://api.skillsafe.ai/v1/app-api/jobs/$JOB -H "Authorization: Bearer YOUR_TOKEN"import time
job = call("/run", {"input": run_input}) # header: Idempotency-Key
job_id = job["job_id"]
while True:
j = call("/jobs/" + job_id)
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
print(j["status"], j.get("charged_credits"), j.get("truncated"))
print(j["output"]["text"])const job = await call("/run", { input: runInput }); // header: Idempotency-Key
let j;
do {
await new Promise(r => setTimeout(r, 2000));
j = await call(`/jobs/${job.job_id}`);
} while (!["succeeded", "failed", "cancelled"].includes(j.status));
console.log(j.status, j.charged_credits, j.truncated);
console.log(j.output.text);job, _ := call("/run", map[string]any{"input": runInput})
id := job["job_id"].(string)
for {
j, _ := call("/jobs/"+id, nil)
s, _ := j["status"].(string)
if s == "succeeded" || s == "failed" || s == "cancelled" {
fmt.Println(s, j["charged_credits"], j["truncated"])
break
}
time.Sleep(2 * time.Second)
}String job = call("/run", "{\"input\": " + runInputJson + "}");
// parse job_id, then poll GET /jobs/{id} until status is
// succeeded, failed or cancelled.job = call("/run", { "input" => run_input })
loop do
j = call("/jobs/#{job['job_id']}")
break puts(j["output"]["text"]) if %w[succeeded failed cancelled].include?(j["status"])
sleep 2
end$job = call("/run", ["input" => $runInput]);
do {
sleep(2);
$j = call("/jobs/" . $job["job_id"]);
} while (!in_array($j["status"], ["succeeded", "failed", "cancelled"], true));
echo $j["output"]["text"];JsonElement job = await Call("/run", new { input = runInput });
string id = job.GetProperty("job_id").GetString()!;
JsonElement j;
do {
await Task.Delay(2000);
j = await Call($"/jobs/{id}");
} while (j.GetProperty("status").GetString() is not ("succeeded" or "failed" or "cancelled"));
Console.WriteLine(j.GetProperty("output").GetProperty("text").GetString());Step 5 — stream it instead
POST /run-stream is the same billed run over SSE. Each line is
data: {...}; type: "delta" carries text to append, and
type: "job" is the terminal event carrying status,
charged_credits and truncated. The web app uses this one so it can show a
staged progress card and keep partial sections when a stream drops.
curl -N https://api.skillsafe.ai/v1/app-api/run-stream \
-H "Authorization: Bearer YOUR_TOKEN" \
-H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-H "Idempotency-Key: bicep-gate:cost:9f2ab130:n1" \
-d '{"input": {"task": "cost", "template": "...", "environment": "production", "context": "", "masked": false, "prescan": {}}}' import urllib.request, json
req = urllib.request.Request(BASE + "/run-stream",
data=json.dumps({"input": run_input}).encode(), method="POST")
req.add_header("Authorization", "Bearer " + TOKEN)
req.add_header("Content-Type", "application/json")
req.add_header("Accept", "text/event-stream")
req.add_header("Idempotency-Key", "bicep-gate:cost:9f2ab130:n1")
answer = []
with urllib.request.urlopen(req) as r:
for raw in r:
line = raw.decode().strip()
if not line.startswith("data:"):
continue
evt = json.loads(line[5:].strip())
if evt.get("type") == "delta":
answer.append(evt["text"])
elif evt.get("type") == "job":
print("terminal:", evt["status"], evt.get("charged_credits"))
print("".join(answer))const res = await fetch(BASE + "/run-stream", {
method: "POST",
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Content-Type": "application/json",
"Accept": "text/event-stream",
"Idempotency-Key": "bicep-gate:cost:9f2ab130:n1"
},
body: JSON.stringify({ input: runInput })
});
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buf = "", answer = "";
for (;;) {
const { value, done } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const lines = buf.split("\n");
buf = lines.pop();
for (const line of lines) {
if (!line.startsWith("data:")) continue;
const evt = JSON.parse(line.slice(5).trim());
if (evt.type === "delta") answer += evt.text;
}
}
console.log(answer);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("Accept", "text/event-stream")
req.Header.Set("Idempotency-Key", "bicep-gate:cost:9f2ab130:n1")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
sc := bufio.NewScanner(res.Body)
for sc.Scan() {
line := sc.Text()
if !strings.HasPrefix(line, "data:") {
continue
}
var evt struct {
Type, Text, Status string
}
json.Unmarshal([]byte(strings.TrimPrefix(line, "data:")), &evt)
if evt.Type == "delta" {
fmt.Print(evt.Text)
}
}HttpRequest req = HttpRequest.newBuilder(URI.create(BASE + "/run-stream"))
.header("Authorization", "Bearer " + TOKEN)
.header("Content-Type", "application/json")
.header("Accept", "text/event-stream")
.header("Idempotency-Key", "bicep-gate:cost:9f2ab130:n1")
.POST(HttpRequest.BodyPublishers.ofString("{\"input\": " + runInputJson + "}"))
.build();
CLIENT.send(req, HttpResponse.BodyHandlers.ofLines())
.body()
.filter(l -> l.startsWith("data:"))
.forEach(System.out::println);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"] = "bicep-gate:cost:9f2ab130:n1"
req.body = JSON.dump({ "input" => run_input })
Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) do |http|
http.request(req) do |res|
res.read_body do |chunk|
chunk.each_line do |line|
next unless line.start_with?("data:")
evt = JSON.parse(line[5..].strip)
print evt["text"] if evt["type"] == "delta"
end
end
end
end$ch = curl_init(BASE . "/run-stream");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["input" => $runInput]));
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer " . TOKEN,
"Content-Type: application/json",
"Accept: text/event-stream",
"Idempotency-Key: bicep-gate:cost:9f2ab130:n1",
]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) {
foreach (explode("\n", $chunk) as $line) {
if (!str_starts_with($line, "data:")) continue;
$evt = json_decode(substr($line, 5), true);
if (($evt["type"] ?? "") === "delta") echo $evt["text"];
}
return strlen($chunk);
});
curl_exec($ch);var req = new HttpRequestMessage(HttpMethod.Post, Base + "/run-stream");
req.Headers.Add("Accept", "text/event-stream");
req.Headers.Add("Idempotency-Key", "bicep-gate:cost:9f2ab130:n1");
req.Content = new StringContent(JsonSerializer.Serialize(new { input = runInput }),
Encoding.UTF8, "application/json");
using var res = await http.SendAsync(req, HttpCompletionOption.ResponseHeadersRead);
using var reader = new StreamReader(await res.Content.ReadAsStreamAsync());
while (await reader.ReadLineAsync() is string line)
{
if (!line.StartsWith("data:")) continue;
JsonDocument evt = JsonDocument.Parse(line[5..]);
if (evt.RootElement.GetProperty("type").GetString() == "delta")
Console.Write(evt.RootElement.GetProperty("text").GetString());
}Errors
| Code | HTTP | What to do |
|---|---|---|
UNAUTHORIZED | 401 | The token is missing, malformed or expired. Mint a new one — see step 1. |
FORBIDDEN | 403 | The token is valid but not for this app. Tokens are per-app. |
VALIDATION_ERROR | 400 | The input did not match the app's shape. `error.details` names the field. |
INSUFFICIENT_CREDITS | 402 | Your balance is below `min_credits`. Top up, or expect a truncated answer. |
RATE_LIMITED | 429 | Back off and retry. Never tight-loop. |
JOB_FAILED | 200 | The envelope is ok but `data.status` is `failed`; read `data.error`. |
INTERNAL | 500 | Retry once with the SAME Idempotency-Key so a retry cannot double-bill. |
One worked example per lane
The same template, five lanes, five different fourth sections. Only the task field changes.
task: "readiness"
Request body input:
{"task": "readiness", "template": "...", "environment": "production", "context": "", "masked": false, "prescan": {}}
Abridged data.output.text:
## VERDICT
STOP — an output returns the SQL admin password, so deploying writes a secret into readable deployment history.
## SUMMARY
...
## FINDINGS
- [critical] Output returns a secret — `output sqlPassword` hands back the value of the `sqlAdminPassword` parameter (resource: sqlPassword, line: 62)
## DEPLOY ORDER
1. `logs` (Microsoft.OperationalInsights/workspaces) — no declared dependencies
2. ...
## UNKNOWNS
- Whether a diagnostic setting exists — `modules/network.bicep` was not pasted
## NEXT STEPS
1. Remove the `sqlPassword` output.
## GROUNDING
- The output returns a secret → `output sqlPassword string = sqlAdminPassword` in the pasted template
task: "capacity"
Request body input:
{"task": "capacity", ...}
Abridged data.output.text:
## VERDICT
STOP — the GPU family starts at zero on a new subscription and needs a reviewed request before this can deploy.
## SUMMARY
...
## FINDINGS
- [high] GPU family needs a reviewed quota request — Standard NCADS_A100_v4 starts at zero (resource: aks, line: 46)
## QUOTA REQUESTS
- Standard NCADS_A100_v4 Family vCPUs — westeurope — 48 vCPU across 2 instances — request before deploying: yes
- Standard BS Family vCPUs — westeurope — 24 vCPU across 6 instances — request before deploying: unknown
## UNKNOWNS
- ...
## NEXT STEPS
1. ...
## GROUNDING
- 48 vCPU of NCADS_A100_v4 → prescan.quota.families
task: "cost"
Request body input:
{"task": "cost", ...}
Abridged data.output.text:
## VERDICT
GO WITH CHANGES — three resources bill continuously and one accrues log retention without a ceiling.
## SUMMARY
...
## FINDINGS
- [medium] Log retention is set to 730 days — retention above the included period accrues per GB per month (resource: logs, line: 71)
## COST DRIVERS
- aks gpu pool (Standard_NC24ads_A100_v4 ×2) — standing — GPU nodes bill while the pool exists, running or idle
- plan (P2v3, capacity 3, zoneRedundant) — standing — three instances, multiplied again by zone redundancy
- logs (retentionInDays 730) — unbounded — retention accrues for two years with no ceiling on volume
## UNKNOWNS
- ...
## NEXT STEPS
1. ...
## GROUNDING
- ...
task: "rightsize"
Request body input:
{"task": "rightsize", ...}
Abridged data.output.text:
## VERDICT
GO WITH CHANGES — a burstable family carries a steady job queue and the scale set uses the legacy orchestration mode.
## SUMMARY
...
## FINDINGS
- [medium] Burstable family under steady load — B-series throttles hard once its credits run out (resource: jobs, line: 36)
## SIZING TABLE
| Resource | Current | Suggested | Why |
| --- | --- | --- | --- |
| jobs | Standard_B4ms ×6 | Standard_D4s_v5 | B-series banks credits and throttles under a steady queue; D-series holds a flat rate |
| aks/system | Standard_D4s_v5 ×3 | keep | Right for a system pool of three |
## UNKNOWNS
- ...
## NEXT STEPS
1. ...
## GROUNDING
- ...
task: "aks"
Request body input:
{"task": "aks", ...}
Abridged data.output.text:
## VERDICT
STOP — networkPolicy is unset and cannot be added after the cluster is created.
## SUMMARY
...
## FINDINGS
- [high] No network policy — with networkPlugin azure and no policy every pod can reach every other pod (resource: aks, line: 46)
## DAY-0 CHECKLIST
- [change] Network policy — networkPlugin is azure with no networkPolicy; set it now, it is a create-time decision
- [change] API server exposure — neither enablePrivateCluster nor authorizedIPRanges is set
- [change] Kubernetes version — pinned to patch 1.29.4; pin the minor and let AKS pick the patch
## UNKNOWNS
- ...
## NEXT STEPS
1. ...
## GROUNDING
- ...
What the API will not do
- It never prices anything. There is no price list behind this API and it cannot fetch one.
- It never contacts Azure. It cannot read your real quota, usage, agreement or regional SKU availability.
- It does not compile, deploy or run what-if. A clean result is not a deployment.
- It reports
unknownrather than guessing. That is the point of the app.
Derived from and crediting @microsoft/azure-prepare, @microsoft/azure-quotas,
@microsoft/azure-cost, @microsoft/azure-compute and
@microsoft/azure-kubernetes. Not affiliated with or endorsed by Microsoft.