Bicep Gate

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.

taskFourth sectionWhat the lane answersSource skill
readinessDEPLOY ORDERWill this deploy unattended? Parameters that prompt, secrets in outputs, azd service tags, scope, API versions, naming rules.@microsoft/azure-prepare
capacityQUOTA REQUESTSWhich allowance runs out first. Per SKU family per region. Never what you have — only what the template draws.@microsoft/azure-quotas
costCOST DRIVERSThe shape of the bill: standing floor vs consumption vs unbounded. No figures, ever.@microsoft/azure-cost
rightsizeSIZING TABLEFamilies, burstable credits, orchestration mode, disk tier, zones, scaling rules. A Resource/Current/Suggested/Why table.@microsoft/azure-compute
aksDAY-0 CHECKLISTThe 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.

FieldTypeMeaning
taskstringOne of the five lane ids above. Required.
templatestringThe pasted deployment definition. Clipped at 60,000 characters, with an inline marker where content was removed.
template_clippedbooleanTrue when the app clipped it.
template_clip_notestringHuman-readable note about what was cut.
environmentstringunstated, dev, staging or production.
contextstringThe user's free-text note. May be empty.
maskedbooleanTrue when identifiers were replaced with placeholders in EVERY field, this one included.
prescanobjectThe 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:

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.

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.

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.

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.

Errors

CodeHTTPWhat to do
UNAUTHORIZED401The token is missing, malformed or expired. Mint a new one — see step 1.
FORBIDDEN403The token is valid but not for this app. Tokens are per-app.
VALIDATION_ERROR400The input did not match the app's shape. `error.details` names the field.
INSUFFICIENT_CREDITS402Your balance is below `min_credits`. Top up, or expect a truncated answer.
RATE_LIMITED429Back off and retry. Never tight-loop.
JOB_FAILED200The envelope is ok but `data.status` is `failed`; read `data.error`.
INTERNAL500Retry 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

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.