Run Key Shape from your own code
Both metered lanes are available over HTTP and each returns one JSON object. The division of labour: the page checks the key format (prefix, environment marker, entropy, checksum, scanner detectability), verifies the user's test keys in the browser - they are never part of the input - runs the storage, scoping, rotation, dashboard, ownership, governance and OWASP API2 checks, and ranks the storage and scoping rungs, then hands it all to the run as facts. The run writes the design or the runbook; the page then holds it to its own facts (keyreply.js): the ranked rungs, an environment prefix, 120+ bits, a checksum, an invented example key that follows the format, verifies and matches its pattern, the rotation numbers, the four self-service actions, a service-account tier; a rotation that creates before it revokes and waits for zero traffic, a leaked key revoked at once, and no key printed.
Base URL and the response shape
Every endpoint lives under https://api.skillsafe.ai/v1/app-api, and every response has one shape:
{ "ok": true, "data": { ... } }
{ "ok": false, "error": { "code": "...", "message": "..." } }
The token is minted for this app (the guest call takes {"slug":"key-shape"}), so no slug header is needed afterwards. Send it as Authorization: Bearer YOUR_TOKEN. The run body is the input object, sent bare.
| Status | Code | Meaning |
|---|---|---|
| 400 | validation_error | The body is not a JSON object, or a required key (task, platform, keys, facts) is missing. The page always sends all five of task, platform, keys, carry, facts; carry is an empty string for design. |
| 401 | unauthorized | No token, or a stale one. Mint a guest token or sign in again. |
| 402 | insufficient_credits | The balance is under min_credits. Price with /estimate first. |
| 403 | forbidden | A guest token tried to run: both lanes are metered and need a personal sign-in. |
| 404 | not_found | Unknown job id. |
| 429 | rate_limited | Back off and retry. |
| 5xx | server_error | Transient. Retry with the same Idempotency-Key so a retry never double-bills. |
The task field comes first
Two lanes, chosen by task, in the order the work runs - the keys, then the bad day:
design- the key design: the format with an invented example and a scanner pattern, the storage and scoping rungs, rotation, the self-service dashboard, ownership tiers and offboarding, governance, and where OAuth takes over.runbook- the runbook: a scheduled rotation without downtime, the response to a leaked key, offboarding, what to tell customers, and what to track afterwards.
A missing or unknown task is answered as design. Both lanes send the same five fields; no pasted key is ever one of them. The facts are the page's checks, one row per line, and a line starting "must:" is a rule the reply has to hold. The page's handoff carries the design's decisions into carry for the runbook lane.
The input
| Field | Required | What |
|---|---|---|
task | yes | design or runbook. If it is missing or neither, do design and set "task": "design". |
platform | yes | the platform's name and purpose, and the interview: greenfield or retrofit, who holds keys, CI or agent consumers, third-party apps, compliance, existing RBAC, teams, lost keys, timing; "not given" means not given. |
keys | yes | the key format as described (prefixes, body, checksum), storage, scoping, rotation, the dashboard, offboarding, audit, expiry and the OWASP answers, and how many of the user's test keys matched - never the keys. |
carry | send it (empty for design) | runbook task only: the design's decisions, when the design task ran first; may be empty. |
facts | yes | the page's checks, one line each as area | id | status | evidence, and the rungs the page ranked. A line starting "must:" applies to your reply. |
retry_note | no | optional; if present, your previous reply was unusable - follow it. |
The rules the model applies
From the source skill, @samber/api-auth-key-management - the only standard the run applies:
- A static key says which app is calling; OAuth says what a user granted. Choose per surface: OAuth for third-party apps acting for users and for short-lived cross-org machine traffic on sensitive data; keys for server-to-server identity, sandboxes and rate limiting. Never ship a long-lived key to a browser or app - mint short-lived tokens server-side; CI and AI agents are moving to workload identity.
- Three ownership tiers, all designed or one explicitly deferred: individual keys for experiments, team or organization keys with admin approval and review, service-account keys for anything unattended - production must not run on a person's key.
- Format: a type-and-environment prefix ending in _ (sk_live_, sk_test_), a random body of at least 120 bits (32 or more characters; a 30-character base62 body is about 178 bits), and a checksum so validators reject malformed keys without a lookup and scanners filter noise. A distinctive prefix plus a checksum makes keys scannable; register the pattern with secret-scanning programs once leaks justify it.
- Storage, ranked: irretrievable hash-only by default (show the key once, keep its SHA-256 - enough for 120+ bit machine secrets; bcrypt ignores input past 72 bytes); retrievable only when consumers routinely lose low-stakes keys - AES-256-GCM with the key-encryption key in a KMS plus a SHA-256 for lookups, never plaintext or app-managed keys; workload identity when the consumers are CI or AI agents. A hard ship date promotes the cheap rungs.
- Scoping, ranked: one key per service per environment plus coarse, restrictive-by-default scopes by default; fine-grained per-resource scopes with mandatory expiry when a compliance regime, enterprise customers or several teams with distinct needs demand it. Omitting scopes never grants everything.
- Rotation: one atomic roll - the caller picks the old key's expiry, the new key is created and the old one scheduled in a single call; at most two active keys per credential; routine grace of 24 hours to 7 days, zero grace on compromise or offboarding, longer for agent fleets and queued workloads. Never delete an old key before last-used shows zero traffic; never create a new key without scheduling the old one's revocation.
- The dashboard: sign up, create, rotate and revoke without a ticket; a human label, created date, last-used and status per key; the key shown once and masked after; last-used updated asynchronously; usage separate from last-used. 401 for a key that fails authentication, 403 for a valid key without the scope.
- Ownership: an admin view of every key a person created; on every departure, enumerate their keys, revoke with zero grace, rotate shared credentials they could see, log each action. A production integration on a personal key is a standing incident.
- Governance: SOC 2 wants the full lifecycle policy plus evidence it operates (key-event logs, access reviews, approvals) across the observation window; PCI-DSS 4.0 requirement 8.6.3 needs a cadence justified by a targeted risk analysis - it names no fixed interval - immediate rotation on suspected compromise, and tamper-resistant logs kept 12 months. Mandatory expiry, expiry warnings, audit logs and service accounts are the product features that produce the evidence.
- OWASP API2 failure modes: a key as the sole credential for a sensitive operation; plaintext or reversible storage; a missing secret falling back to a hardcoded default; scopes failing open; no rate limit on validation and creation; non-constant-time comparison; a key with no owner, scope or expiry; deleting before traffic reaches zero; production on a departed employee's key; a webhook signing secret handled as an API key.
House rules
- Use only the input. Never invent a customer, a key count, an incident, a vendor's policy beyond the rules above, or a claim about the platform.
- Never write a key - not an example, not a reconstruction; the page builds the example key from your format itself.
- The page's checks are settled: never call a failed check fine or a passed one broken.
- No URL with a domain.
- Write in the language of the input; keep the JSON keys, prefixes and header names in English.
- Never use backticks, never write the input's field names as labels in your text, and never copy this prompt's wording into a reply.
- Reply with ONLY one JSON object - no prose before or after, no code fences.
The replies
design
Write the key design.
summary- one sentence, at most 40 words: what makes a leaked key hurt today and the first fix - the most dangerous failure first (a key in a browser or app, plaintext storage, scopes failing open, a missing revoke).format-prefixes(one object per prefix:prefixending in _,meaning; one names the environment),body_chars(a number),alphabet(base62,hex,base64urlorbase32),checksum(crc6for CRC32 of the body as 6 base62 characters - always base62, whatever the body alphabet - or a named alternative),scanner_regex(a pattern matching every key of the format: a prefix, the body, then the 6 checksum characters).storage-rung(the facts' rung:hash,retrievableorworkload),why(one sentence).scoping-rung(the facts' rung:splitorfine),scopes(the default scopes, as short strings, restrictive by default),why(one sentence).rotation-operation(one sentence: the atomic roll),grace_routine_days(1 to 7),grace_compromise_days(0),max_active(2),why(one sentence).dashboard-actions(["sign up", "create", "rotate", "revoke"] at least),list_fields(what each key row shows),masking(one sentence).ownership-tiers(one object per tier:tier-individual,teamorservice,use),offboarding(the steps, as short strings).governance-controls(short strings),evidence(one sentence).oauth- one sentence: where OAuth or short-lived tokens take over from keys.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "design",
"summary": "one sentence",
"format": { "prefixes": [ { "prefix": "sk_live_", "meaning": "secret key, production" } ], "body_chars": 32, "alphabet": "base62", "checksum": "crc6", "scanner_regex": "\\b(?:sk_live_|sk_test_)[0-9A-Za-z]{38}\\b" },
"storage": { "rung": "hash", "why": "one sentence" },
"scoping": { "rung": "split", "scopes": [ "read" ], "why": "one sentence" },
"rotation": { "operation": "one sentence", "grace_routine_days": 7, "grace_compromise_days": 0, "max_active": 2, "why": "one sentence" },
"dashboard": { "actions": [ "sign up", "create", "rotate", "revoke" ], "list_fields": [ "label" ], "masking": "one sentence" },
"ownership": { "tiers": [ { "tier": "service", "use": "one sentence" } ], "offboarding": [ "a step" ] },
"governance": { "controls": [ "a control" ], "evidence": "one sentence" },
"oauth": "one sentence",
"caveats": [ ]
}
The page holds it to its own facts (check() in keyreply.js; any warning makes the reply flagged): the reply answers the task it was sent; no URL; no backticks; no real-looking key but the one invented example, and never a key the user pasted; prefixes of letters ending in _, one naming the environment; a known alphabet and at least 120 bits; a checksum; an example key that starts with a prefix, has the right length and alphabet, verifies its crc6 checksum and matches the scanner pattern, which compiles and is not too broad; the ranked storage and scoping rungs; restrictive default scopes; 1 to 7 days of routine grace, none on compromise, at most two active keys; sign up, create, rotate and revoke; label, created and last-used per key; a service-account tier; offboarding with revocation; governance controls (an audit log when compliance is in scope); and a word on OAuth (OAuth when third-party apps act for users).
runbook
Write the rotation and leak-response runbook.
summary- one sentence, at most 40 words.rotation- the scheduled rotation, in order: objects withstep,owner(a role),check(how you know the step is done). The roll creates the new key and schedules the old one's revocation in one step; the old key is deleted later, only once last-used shows zero traffic.leak- the response to a leaked or suspected key, in order: objects withstep,owner,within(how soon, as a duration).offboarding- the departure steps, in order, as short strings.comms- one or two sentences: what affected customers are told and when.metrics- two to four strings: what to track afterwards.caveats- at most 4 short strings;[]when there is nothing to say.
{
"task": "runbook",
"summary": "one sentence",
"rotation": [ { "step": "one sentence", "owner": "integration owner", "check": "one sentence" } ],
"leak": [ { "step": "one sentence", "owner": "on-call security", "within": "15 minutes" } ],
"offboarding": [ "a step" ],
"comms": "one or two sentences",
"metrics": [ "time from detection to revocation" ],
"caveats": [ ]
}
The page requires: the reply answers the task it was sent; no URL; no backticks; no real-looking key; at least four rotation steps that create the new key before revoking the old, wait for zero traffic or last-used before deleting, and give each step an owner and a check; at least four leak steps that revoke at once, review the logs, and give each step an owner and a deadline; offboarding that enumerates, revokes and rotates shared credentials; what customers are told; and a metric for the time from detection to revocation.
Worked examples
Three runs from the page's free samples (invented platforms; every example key is a visible fake): the design for a payments API in production, the runbook for the same API carrying that design, and the design for a platform whose callers are CI pipelines and AI agents. Each input is exactly what the page sends - with no pasted key in it; each reply is the saved reply the sample replays, shipped unedited - but the facts and long strings are shortened in this display only, and every cut is listed under the example. The last line is the page's own check of the full reply.
design - A payments API in production
The page read it as: a leaked key would hurt (12 fail, 6 warn, 2 pass); entropy 96 bits; storage by rank: hash; scoping by rank: split; failing checks: env, entropy, storage, scoping, failopen, rotation, selfserve, offboard, audit, browser, ratelimit, fallback.
Input (facts is 2262 characters; the first 1397 are shown):
{
"task": "design",
"platform": "Platform: Quillmint Payments\nWhat it does: Card payments for small online shops; merchants call it from their servers and, for in-app checkout, from their mobile apps.\nMode: retrofit\nWho holds keys: Individual developers\nCI or AI-agent consumers: no\nThird-party apps act on behalf of users: no\nCompliance: PCI-DSS (cardholder data)\nExisting RBAC: no\nSeveral teams with distinct permission needs: no\nConsumers routinely lose keys: no\nTiming: Must ship within weeks",
"keys": "Prefixes:\n- key_ | the only key type\nBody: 24 hex characters (96 bits); checksum: None\nStorage: Plaintext; scoping: Every key can do everything; failing open without scopes: yes\nRotation: No rotation; routine grace: not given; active keys per credential: not given\nDashboard: sign up yes, create yes, rotate no, revoke yes, masked after creation no, last-used shown no\nOffboarding: Nothing happens; audit log: no; expiry: Keys never expire\nKeys in browsers or apps: yes; validation rate-limited: no; constant-time compare: unknown; hardcoded fallback: yes\nSample test keys checked in the page: 1 of 1 match (the keys themselves are never sent)",
"carry": "",
"facts": "The page's checks (12 fail, 6 warn, 2 pass, 2 n/a):\n- Format | prefix | pass | key_\n- Format | env | fail | no prefix names an environment - a test key and a live key look alike\n- Format | scanner | warn | generic prefixes (key_) - add a short vendor mark\n- Format | entropy | fail | 24 hex characters = 96 bits\n- Format | checksum | warn | None\n- Format | samples | pass | all 1 sample match\n- Storage and scoping | storage | fail | Plaintext\n- Storage and scoping | scoping | fail | Every key can do everything\n- Storage and scoping | failopen | fail | a key created without scopes can do everything\n- Rotation and dashboard | rotation | fail | No rotation\n- Rotation and dashboard | grace | na | not given\n- Rotation and dashboard | maxactive | na | not given\n- Rotation and dashboard | selfserve | fail | missing: rotate\n- Rotation and dashboard | mask | warn | shown in full after creation, no last-used\n- Ownership and governance | service | warn | only individual keys - production runs on someone's personal credential\n- Ownership and governance | offboard | fail | Nothing happens\n- Ownership and governance | audit | fail | no audit log - PCI-DSS (cardholder data) asks for the evidence\n- Ownership and governance | expiry | warn | Keys never expire\n- OWASP API2 failure modes | browser | fail | long-lived keys ship to browsers or apps - mint short-lived tokens server-side\n- OWASP API2\n\n[... 865 more characters cut from this display ...]"
}
Reply:
{
"task": "design",
"summary": "A leaked key today is plaintext, unscoped, never expires and also ships inside mobile apps, so one copy can move money forever; first take keys out of the apps and store only their SHA-256 hash.",
"format": {
"prefixes": [
{
"prefix": "cpsk_live_",
"meaning": "Quillmint secret key, live mode - moves real card payments"
},
{
"prefix": "cpsk_test_",
"meaning": "Quillmint secret key, test mode - sandbox cards only, never real money"
}
],
"body_chars": 32,
"alphabet": "base62",
"checksum": "crc6",
"scanner_regex": "\\bcpsk_(?:live|test)_[0-9A-Za-z]{38}\\b"
},
"storage": {
"rung": "hash",
"why": "Merchants do not routinely lose keys and the ship date is weeks away, so show each key once and keep only its SHA-256 hash - never the plaintext, so a database dump stops being a key dump."
},
"scoping": {
"rung": "split",
"scopes": [
"charges:create",
"charges:read"
],
"why": "One key per merchant server per environment with coarse scopes is the rung the deadline allows; a key created without scopes gets only these two, and refunds:create or payouts:read must be granted explicitly."
},
"rotation": {
"operation": "A single roll call creates the replacement key and schedules the old key's revocation at an expiry the merchant picks, so no rotation can leave the old key alive by accident.",
"grace_routine_days": 7,
"grace_compromise_days": 0,
"max_active": 2,
"why": "Seven days lets a merchant redeploy its servers on a normal release cycle, while a suspected compromise revokes at once as PCI-DSS 4.0 requires."
},
"dashboard": {
"actions": [
"sign up",
"create",
"rotate",
"revoke",
"set expiry",
"view usage"
],
"list_fields": [
"label",
"prefix and last 4 characters",
"environment",
"scopes",
"created date and creator",
"last used",
"expiry",
"status"
],
"masking": "The full key is shown once at creation with a copy button, then only its prefix and last 4 characters; validation answers 401 for a bad key and 403 for a valid key missing the scope."
},
"ownership": {
"tiers": [
{
"tier": "individual",
"use": "Test-mode keys a developer creates to try the API; they cannot be created in live mode."
},
{
"tier": "team",
"use": "Live keys owned by the merchant account rather than a person, created and reviewed by the account owner."
},
{
"tier": "service",
"use": "Service-account keys for each merchant server that runs unattended, so production never runs on a departed developer's personal key."
}
],
"offboarding": [
"List every key the leaver created or could see from the merchant account's key view",
"Revoke their personal keys with zero grace",
"Roll any shared live key they could see and move it to a service account",
"Log each revocation and roll with who did it and when"
]
},
"governance": {
"controls": [
"Tamper-resistant audit log of every create, roll, revoke and failed validation, kept 12 months",
"Rotation cadence set by a targeted risk analysis, with immediate rotation on suspected compromise",
"Mandatory expiry on live keys with warnings before it lands",
"Rate limits on validation and key creation, treated like a login endpoint",
"Constant-time hash comparison and no hardcoded fallback key"
],
"evidence": "The audit log, the written risk analysis behind the cadence and the expiry and revocation records are the evidence a PCI-DSS 4.0 assessor asks for under requirement 8.6.3, kept tamper-resistant for 12 months."
},
"oauth": "Mobile in-app checkout stops carrying any key: the merchant server mints a short-lived, single-payment token and the app uses only that, while static keys stay on servers.",
"caveats": [
"Every key already shipped in a mobile app must be treated as exposed and rolled once the app no longer needs it."
]
}
What step 5 prints for it: cpsk_live_ | cpsk_test_ | hash | split
The page's check: kept - nothing to flag
runbook - A payments API in production
The input carries the saved design's decisions (R.laneForm fills carry from the design reply), as the page's handoff does.
Input (facts is 2181 characters; the first 1397 are shown):
{
"task": "runbook",
"platform": "Platform: Quillmint Payments\nWhat it does: Card payments for small online shops; merchants call it from their servers and, for in-app checkout, from their mobile apps.\nMode: retrofit\nWho holds keys: Individual developers\nCI or AI-agent consumers: no\nThird-party apps act on behalf of users: no\nCompliance: PCI-DSS (cardholder data)\nExisting RBAC: no\nSeveral teams with distinct permission needs: no\nConsumers routinely lose keys: no\nTiming: Must ship within weeks",
"keys": "Prefixes:\n- key_ | the only key type\nBody: 24 hex characters (96 bits); checksum: None\nStorage: Plaintext; scoping: Every key can do everything; failing open without scopes: yes\nRotation: No rotation; routine grace: not given; active keys per credential: not given\nDashboard: sign up yes, create yes, rotate no, revoke yes, masked after creation no, last-used shown no\nOffboarding: Nothing happens; audit log: no; expiry: Keys never expire\nKeys in browsers or apps: yes; validation rate-limited: no; constant-time compare: unknown; hardcoded fallback: yes\nSample test keys checked in the page: 1 of 1 match (the keys themselves are never sent)",
"carry": "Prefixes: cpsk_live_, cpsk_test_\nStorage: hash\nScoping: split (charges:create, charges:read)\nRotation: A single roll call creates the replacement key and schedules the old key's revocation at an expiry the merchant picks, so no rotation can leave the old key alive by accident. Routine grace 7 days, compromise 0, at most 2 active.\nTiers: individual, team, service\nDashboard: sign up, create, rotate, revoke, set expiry, view usage",
"facts": "The page's checks (12 fail, 6 warn, 2 pass, 2 n/a):\n- Format | prefix | pass | key_\n- Format | env | fail | no prefix names an environment - a test key and a live key look alike\n- Format | scanner | warn | generic prefixes (key_) - add a short vendor mark\n- Format | entropy | fail | 24 hex characters = 96 bits\n- Format | checksum | warn | None\n- Format | samples | pass | all 1 sample match\n- Storage and scoping | storage | fail | Plaintext\n- Storage and scoping | scoping | fail | Every key can do everything\n- Storage and scoping | failopen | fail | a key created without scopes can do everything\n- Rotation and dashboard | rotation | fail | No rotation\n- Rotation and dashboard | grace | na | not given\n- Rotation and dashboard | maxactive | na | not given\n- Rotation and dashboard | selfserve | fail | missing: rotate\n- Rotation and dashboard | mask | warn | shown in full after creation, no last-used\n- Ownership and governance | service | warn | only individual keys - production runs on someone's personal credential\n- Ownership and governance | offboard | fail | Nothing happens\n- Ownership and governance | audit | fail | no audit log - PCI-DSS (cardholder data) asks for the evidence\n- Ownership and governance | expiry | warn | Keys never expire\n- OWASP API2 failure modes | browser | fail | long-lived keys ship to browsers or apps - mint short-lived tokens server-side\n- OWASP API2\n\n[... 784 more characters cut from this display ...]"
}
Reply:
{
"task": "runbook",
"summary": "Roll each live key on the cadence the PCI-DSS risk analysis sets, retire the old one only after its traffic reaches zero, and revoke a leaked key within minutes with zero grace before reviewing every charge it made.",
"rotation": [
{
"step": "List the live keys that reach the rotation date set by the targeted risk analysis, with each key's owner and service account.",
"owner": "security lead",
"check": "Every key on the list has a named owner and a rotation ticket."
},
{
"step": "Run the roll call for each key: it creates the replacement cpsk_live_ key and schedules the old key's revocation 7 days out.",
"owner": "merchant integration owner",
"check": "The dashboard shows exactly two active keys for the credential, the old one with its scheduled date."
},
{
"step": "Put the new key in the merchant server's secret store and redeploy every server that calls the API.",
"owner": "merchant integration owner",
"check": "Last-used on the new key advances and charges succeed with it."
},
{
"step": "Watch the old key's last-used and usage until it shows zero traffic for a full day; extend the schedule if anything still calls it.",
"owner": "merchant integration owner",
"check": "Last-used on the old key is more than 24 hours old and its request count is zero."
},
{
"step": "Let the scheduled revocation land, then delete the old key's record only once it has stayed at zero traffic.",
"owner": "platform engineer",
"check": "A request with the old key returns 401 and no merchant error rate rises."
},
{
"step": "Write the rotation to the audit log with who rolled the key, when, and the risk-analysis reference.",
"owner": "security lead",
"check": "The audit log entry exists and is in the 12-month tamper-resistant store."
}
],
"leak": [
{
"step": "Confirm the report: match the prefix and checksum, then find the key by its SHA-256 hash and identify its merchant and owner.",
"owner": "on-call security",
"within": "15 minutes"
},
{
"step": "Revoke the key immediately with zero grace and roll a replacement for the merchant, without waiting for the merchant to redeploy.",
"owner": "on-call security",
"within": "15 minutes"
},
{
"step": "Review the audit and request logs for everything the key did since it could have leaked: charges, refunds, source addresses and failed validations.",
"owner": "security analyst",
"within": "4 hours"
},
{
"step": "Find and remove the exposed copy - repository, log, or mobile app build - and if it shipped in an app, move that app to server-minted short-lived tokens.",
"owner": "merchant integration owner",
"within": "24 hours"
},
{
"step": "Decide with compliance whether cardholder data was reachable and start the PCI-DSS incident process if so.",
"owner": "compliance lead",
"within": "24 hours"
},
{
"step": "Hold a review of how the key escaped and record the fix and the timings in the audit log.",
"owner": "security lead",
"within": "5 business days"
}
],
"offboarding": [
"List every key the leaver created or could see, from the account's key view and the audit log",
"Revoke their personal keys with zero grace",
"Roll every shared live key they could see and move it onto a service account",
"Reassign any service account they owned to a current owner",
"Log each revocation and roll with who did it and when"
],
"comms": "The affected merchant is told as soon as the key is revoked: which key (by label and last 4 characters), that it no longer works, the replacement to deploy, and once the log review ends, any charges or refunds made with it.",
"metrics": [
"Time from detection to revocation",
"Live keys still running on a personal key instead of a service account",
"Keys past the rotation date the risk analysis set",
"Old keys still carrying traffic when their scheduled revocation arrives"
],
"caveats": []
}
What step 5 prints for it: 15 minutes: Confirm the report: match the prefix and | 15 minutes: Revoke the key immediately with zero gra | 4 hours: Review the audit and request logs for ev | 24 hours: Find and remove the exposed copy - repos | 24 hours: Decide with compliance whether cardholde | 5 business days: Hold a review of how the key escaped and
The page's check: kept - nothing to flag
design - A platform for CI and AI agents
The page read it as: a leaked key would hurt (1 fail, 1 warn, 20 pass); entropy 190.5 bits; storage by rank: workload; scoping by rank: fine; failing checks: samples.
Input (facts is 2085 characters; the first 1399 are shown):
{
"task": "design",
"platform": "Platform: Nebbleworks\nWhat it does: A build and deploy platform; most keys are used by CI pipelines and AI coding agents, the rest by developers trying things.\nMode: retrofit\nWho holds keys: A mix of people, teams and machines\nCI or AI-agent consumers: yes\nThird-party apps act on behalf of users: no\nCompliance: SOC 2 (enterprise buyers)\nExisting RBAC: yes\nSeveral teams with distinct permission needs: no\nConsumers routinely lose keys: no\nTiming: A platform asset for years",
"keys": "Prefixes:\n- nbl_live_ | secret key, production\n- nbl_test_ | secret key, test mode\nBody: 32 base62 characters (190.5 bits); checksum: CRC32 of the body, 6 base62 characters (this page's scheme)\nStorage: SHA-256 hash only; scoping: Per-environment keys with coarse scopes; failing open without scopes: no\nRotation: One atomic roll with an overlap; routine grace: 7 days; active keys per credential: 2\nDashboard: sign up yes, create yes, rotate yes, revoke yes, masked after creation yes, last-used shown yes\nOffboarding: A runbook run on every departure; audit log: yes; expiry: Optional expiry\nKeys in browsers or apps: no; validation rate-limited: yes; constant-time compare: yes; hardcoded fallback: no\nSample test keys checked in the page: 1 of 2 match (the keys themselves are never sent)",
"carry": "",
"facts": "The page's checks (1 fail, 1 warn, 20 pass, 0 n/a):\n- Format | prefix | pass | nbl_live_, nbl_test_\n- Format | env | pass | nbl_live_, nbl_test_\n- Format | scanner | pass | distinctive\n- Format | entropy | pass | 32 base62 characters = 190.5 bits\n- Format | checksum | pass | CRC32 of the body, 6 base62 characters (this page's scheme)\n- Format | samples | fail | 1 of 2 samples do not match (see the sample table)\n- Storage and scoping | storage | pass | SHA-256 hash only\n- Storage and scoping | scoping | pass | Per-environment keys with coarse scopes\n- Storage and scoping | failopen | pass | no (as stated)\n- Rotation and dashboard | rotation | pass | One atomic roll with an overlap\n- Rotation and dashboard | grace | pass | 7 days\n- Rotation and dashboard | maxactive | pass | 2 active at once\n- Rotation and dashboard | selfserve | pass | all four\n- Rotation and dashboard | mask | pass | masked, last-used shown\n- Ownership and governance | service | pass | A mix of people, teams and machines\n- Ownership and governance | offboard | pass | A runbook run on every departure\n- Ownership and governance | audit | pass | logged\n- Ownership and governance | expiry | warn | Optional expiry\n- OWASP API2 failure modes | browser | pass | no (as stated)\n- OWASP API2 failure modes | ratelimit | pass | yes\n- OWASP API2 failure modes | consttime | pass | yes\n- OWASP API2 failure modes | fallback |\n\n[... 686 more characters cut from this display ...]"
}
Reply:
{
"task": "design",
"summary": "Most keys sit unattended in CI pipelines and AI agents with optional expiry, so a leaked one stays valid indefinitely; first move those callers to workload identity and make expiry mandatory on every key left.",
"format": {
"prefixes": [
{
"prefix": "nbl_live_",
"meaning": "Nebbleworks secret key, production - can build and deploy real projects"
},
{
"prefix": "nbl_test_",
"meaning": "Nebbleworks secret key, test mode - sandbox projects only"
}
],
"body_chars": 32,
"alphabet": "base62",
"checksum": "crc6",
"scanner_regex": "\\bnbl_(?:live|test)_[0-9A-Za-z]{38}\\b"
},
"storage": {
"rung": "workload",
"why": "CI systems and AI agents exchange their own signed workload identity for a short-lived token, so nothing long-lived is stored for them, and the remaining developer keys stay hash-only as today."
},
"scoping": {
"rung": "fine",
"scopes": [
"builds:read on one project",
"builds:trigger on one project",
"deploys:write on one named environment"
],
"why": "SOC 2 buyers expect least privilege they can review, so every key and token names its project and environment and must carry an expiry of 90 days at most, and a key created without scopes can only read builds."
},
"rotation": {
"operation": "One roll call creates the new key and schedules the old key's expiry at a time the caller picks, keeping at most two keys alive for that credential.",
"grace_routine_days": 7,
"grace_compromise_days": 0,
"max_active": 2,
"why": "Seven days covers pipelines and agent fleets that redeploy slowly and queued jobs still holding the old key, while compromise and departures revoke at once."
},
"dashboard": {
"actions": [
"sign up",
"create",
"rotate",
"revoke",
"set expiry",
"view usage"
],
"list_fields": [
"label",
"prefix and last 4 characters",
"owner or service account",
"project and environment scopes",
"created date",
"last used",
"expiry date",
"status"
],
"masking": "The key appears once at creation and afterwards only as its prefix and last 4 characters; last-used is updated asynchronously and usage charts sit apart from it."
},
"ownership": {
"tiers": [
{
"tier": "individual",
"use": "Short-lived test-mode keys for developers trying things, never allowed on a production deploy."
},
{
"tier": "team",
"use": "Team keys for shared tooling, created with an organization admin's approval and listed in the quarterly access review."
},
{
"tier": "service",
"use": "Service accounts for every pipeline and agent fleet, authenticated by workload identity where possible and by a scoped, expiring key where not."
}
],
"offboarding": [
"List every key and token the leaver created from the admin view",
"Revoke their personal keys with zero grace",
"Roll any team or service-account key they could see",
"Reassign service accounts they owned to a current owner",
"Log each step for the SOC 2 evidence trail"
]
},
"governance": {
"controls": [
"Written key lifecycle policy: creation, scoping, expiry, rotation, revocation",
"Mandatory expiry with warnings 14 and 3 days before",
"Audit log of every create, roll, revoke and failed validation",
"Quarterly access review of team and service-account keys",
"Admin approval for team keys and live-mode scopes"
],
"evidence": "Key-event logs, access-review sign-offs and approval records show SOC 2 auditors the policy operating across the whole observation window, not only on paper."
},
"oauth": "CI pipelines and AI coding agents stop holding static keys and trade workload identity for tokens that last minutes; static keys remain for developers experimenting in test mode and for the few callers that cannot federate.",
"caveats": [
"One of the two test keys pasted into the page does not match the format - find which generator produced it before trusting it."
]
}
What step 5 prints for it: nbl_live_ | nbl_test_ | workload | fine
The page's check: kept - nothing to flag
Build the input yourself
Load the engine in node (global.window = global; require("./keykit.js"); require("./keyreply.js")), then:
const K = KeyKit, R = KeyReply;
const f = K.form(Object.assign(K.blank(), { name: "Acme", prefixes: "acme_live_ | secret, production\nacme_test_ | secret, test",
bodylen: "32", alphabet: "base62", checksum: "crc6", storage: "sha256", scoping: "coarse", rotation: "roll" }));
// f.rows (every check: area, id, label, status, evidence), f.bits, f.regex (the scanner pattern), f.store, f.scope
// K.generate(f, "acme_test_", n => crypto.getRandomValues(new Uint8Array(n))) - a fresh test key; K.readSample(f, key) - check one
const b = K.buildInput(f, "design"); // or "runbook" (R.laneForm(form, "runbook", design) fills carry)
const wire = K.toWire(b.input); // POST this to /estimate and /run - it never carries a key
// then: const reply = R.normalize(R.parseReply(job.output.output), "design");
// R.check(reply, b) // {status: kept or flagged, notes}
Get a token for the steps below from /tokens.html.
Step 1 — a tiny client helper
One function that sends JSON and unwraps the response. Paste a personal token from /tokens.html, or leave it empty and mint a guest token in step 2.
BASE="https://api.skillsafe.ai/v1/app-api"
TOKEN="YOUR_TOKEN" # from https://key-shape.skillsafe.ai/tokens.html
api() { # api METHOD PATH [JSON-BODY]
curl -s -X "$1" "$BASE$2" -H "Content-Type: application/json" \
${TOKEN:+-H "Authorization: Bearer $TOKEN"} ${3:+--data "$3"}
}
import json, urllib.request
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://key-shape.skillsafe.ai/tokens.html
def api(method, path, body=None, headers=None):
req = urllib.request.Request(BASE + path, method=method,
data=None if body is None else json.dumps(body).encode(),
headers={"Content-Type": "application/json", **({"Authorization": "Bearer " + TOKEN} if TOKEN else {}), **(headers or {})})
with urllib.request.urlopen(req) as r:
env = json.load(r)
if env.get("error"):
raise RuntimeError(env["error"])
return env["data"]
// key-shape.mjs -- Node 18+ (global fetch)
const BASE = "https://api.skillsafe.ai/v1/app-api";
let TOKEN = "YOUR_TOKEN"; // from https://key-shape.skillsafe.ai/tokens.html
async function api(method, path, body, headers = {}) {
const r = await fetch(BASE + path, { method, headers: { "Content-Type": "application/json",
...(TOKEN ? { Authorization: "Bearer " + TOKEN } : {}), ...headers },
body: body === undefined ? undefined : JSON.stringify(body) });
const env = await r.json();
if (env.error) throw new Error(env.error.code + ": " + env.error.message);
return env.data;
}
package main
import ("bytes"; "encoding/json"; "fmt"; "net/http")
const base = "https://api.skillsafe.ai/v1/app-api"
var token = "YOUR_TOKEN" // from https://key-shape.skillsafe.ai/tokens.html
func api(method, path string, body any, headers map[string]string) (map[string]any, error) {
var buf bytes.Buffer
if body != nil { json.NewEncoder(&buf).Encode(body) }
req, _ := http.NewRequest(method, base+path, &buf)
req.Header.Set("Content-Type", "application/json")
if token != "" { req.Header.Set("Authorization", "Bearer "+token) }
for k, v := range headers { req.Header.Set(k, v) }
res, err := http.DefaultClient.Do(req)
if err != nil { return nil, err }
defer res.Body.Close()
var env struct { Data map[string]any; Error map[string]any }
json.NewDecoder(res.Body).Decode(&env)
if env.Error != nil { return nil, fmt.Errorf("%v", env.Error) }
return env.Data, nil
}
// Key Shape.java -- Java 17+, with com.fasterxml.jackson.core:jackson-databind
import com.fasterxml.jackson.databind.*;
import java.net.URI; import java.net.http.*; import java.util.*;
public class Key Shape {
static final String BASE = "https://api.skillsafe.ai/v1/app-api";
static String token = "YOUR_TOKEN"; // from https://key-shape.skillsafe.ai/tokens.html
static final ObjectMapper M = new ObjectMapper();
static final HttpClient C = HttpClient.newHttpClient();
static JsonNode api(String method, String path, Object body, Map<String, String> headers) throws Exception {
var b = HttpRequest.newBuilder(URI.create(BASE + path)).header("Content-Type", "application/json");
if (!token.isEmpty()) b.header("Authorization", "Bearer " + token);
if (headers != null) headers.forEach(b::header);
b.method(method, body == null ? HttpRequest.BodyPublishers.noBody() : HttpRequest.BodyPublishers.ofString(M.writeValueAsString(body)));
JsonNode env = M.readTree(C.send(b.build(), HttpResponse.BodyHandlers.ofString()).body());
if (env.hasNonNull("error")) throw new RuntimeException(env.get("error").toString());
return env.get("data");
}
}
require "json"
require "net/http"
BASE = "https://api.skillsafe.ai/v1/app-api"
TOKEN = "YOUR_TOKEN" # from https://key-shape.skillsafe.ai/tokens.html
def api(method, path, body = nil, headers = {})
uri = URI(BASE + path)
req = Net::HTTPGenericRequest.new(method, !body.nil?, true, uri.request_uri,
{ "Content-Type" => "application/json" }.merge(TOKEN.empty? ? {} : { "Authorization" => "Bearer #{TOKEN}" }).merge(headers))
req.body = JSON.generate(body) unless body.nil?
env = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
raise env["error"].to_s if env["error"]
env["data"]
end
<?php
const BASE = "https://api.skillsafe.ai/v1/app-api";
$TOKEN = "YOUR_TOKEN"; // from https://key-shape.skillsafe.ai/tokens.html
function api(string $method, string $path, $body = null, array $headers = []) {
global $TOKEN;
$h = ["Content-Type: application/json"];
if ($TOKEN) $h[] = "Authorization: Bearer $TOKEN";
foreach ($headers as $k => $v) $h[] = "$k: $v";
$ch = curl_init(BASE . $path);
curl_setopt_array($ch, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $h, CURLOPT_RETURNTRANSFER => true]);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body));
$env = json_decode(curl_exec($ch), true);
if (!empty($env["error"])) throw new Exception(json_encode($env["error"]));
return $env["data"];
}
// Program.cs -- .NET 6+ console app
using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Nodes;
static class Api {
public const string Base = "https://api.skillsafe.ai/v1/app-api";
public static string Token = "YOUR_TOKEN"; // from https://key-shape.skillsafe.ai/tokens.html
static readonly HttpClient C = new();
public static async Task<JsonNode?> Call(string method, string path, object? body = null, Dictionary<string, string>? headers = null) {
var req = new HttpRequestMessage(new HttpMethod(method), Base + path);
if (Token != "") req.Headers.Add("Authorization", "Bearer " + Token);
if (headers != null) foreach (var h in headers) req.Headers.Add(h.Key, h.Value);
if (body != null) req.Content = JsonContent.Create(body);
var env = JsonNode.Parse(await (await C.SendAsync(req)).Content.ReadAsStringAsync())!;
if (env["error"] is JsonNode e && e.GetValueKind() != JsonValueKind.Null) throw new Exception(e.ToJsonString());
return env["data"];
}
}
Step 2 — a token
A guest token is enough for /me and /estimate. A run is metered, so it needs a personal token: sign in on /tokens.html and copy it from there.
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN=$(curl -s -X POST "$BASE/guest" -H "Content-Type: application/json" -d '{"slug":"key-shape"}' | jq -r '.data.token')
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = ""
TOKEN = api("POST", "/guest", {"slug": "key-shape"})["token"]
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN = "";
TOKEN = (await api("POST", "/guest", { slug: "key-shape" })).token;
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = ""
g, err := api("POST", "/guest", map[string]string{"slug": "key-shape"}, nil)
if err != nil { panic(err) }
token = g["token"].(string)
// inside main: a guest token (enough for /me and /estimate); a run needs your personal token
token = "";
token = api("POST", "/guest", Map.of("slug", "key-shape"), null).get("token").asText();
# a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
TOKEN.replace("")
TOKEN.replace(api("POST", "/guest", { slug: "key-shape" })["token"])
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
$TOKEN = "";
$TOKEN = api("POST", "/guest", ["slug" => "key-shape"])["token"];
// a guest token (enough for /me and /estimate); a run needs your personal token from /tokens.html
Api.Token = "";
Api.Token = (await Api.Call("POST", "/guest", new { slug = "key-shape" }))!["token"]!.GetValue<string>();
Step 3 — GET /me
Who the token is (subject_type: user or guest) and the credit balance.
api GET /me | jq '.data | {subject_type, credits}'
me = api("GET", "/me")
print(me["subject_type"], me.get("credits"))
const me = await api("GET", "/me");
console.log(me.subject_type, me.credits);
me, _ := api("GET", "/me", nil, nil)
fmt.Println(me["subject_type"], me["credits"])
JsonNode me = api("GET", "/me", null, null);
System.out.println(me.get("subject_type") + " " + me.get("credits"));
me = api("GET", "/me")
puts [me["subject_type"], me["credits"]].inspect
$me = api("GET", "/me");
echo $me["subject_type"], " ", $me["credits"] ?? "", "\n";
var me = await Api.Call("GET", "/me");
Console.WriteLine($"{me!["subject_type"]} {me["credits"]}");
Step 4 — POST /estimate
Free. The body is the run input itself, the bare object (never wrapped in {"input": ...}). hold_credits is a reservation, not the price; min_credits is what the balance must cover; model_alias reads gpt-terra.
# input.json: the run input object - {"task": "design", "platform": "Platform: Quillmint Payments\nMode: retrofit\n...", "keys": "Prefixes:\n- key_ | the only key type\n...", "carry": "", "facts": "The page's checks (12 fail, 6 warn, ...):\n- Format | env | fail | ..."}
# - see the input table
api POST /estimate "$(cat input.json)" | jq '.data | {hold_credits, min_credits, model, model_alias, markup_bps, warnings}'
inp = json.load(open("input.json")) # {"task": "design", "platform": "Platform: Quillmint Payments\nMode: retrofit\n...", "keys": "Prefixes:\n- key_ | the only key type\n...", "carry": "", "facts": "The page's checks (12 fail, 6 warn, ...):\n- Format | env | fail | ..."}
# - see the input table
est = api("POST", "/estimate", inp)
print(est["hold_credits"], est["min_credits"], est["model"], est.get("warnings"))
import { readFileSync } from "node:fs";
const input = JSON.parse(readFileSync("input.json", "utf8")); // {task: "design" or "runbook", platform, keys, carry, facts} (see the input table)
const est = await api("POST", "/estimate", input);
console.log(est.hold_credits, est.min_credits, est.model, est.warnings);
raw, _ := os.ReadFile("input.json") // add "os" to the imports
var input map[string]any
json.Unmarshal(raw, &input)
est, _ := api("POST", "/estimate", input, nil)
fmt.Println(est["hold_credits"], est["min_credits"], est["model"])
Map<String, Object> input = M.readValue(new java.io.File("input.json"), Map.class);
JsonNode est = api("POST", "/estimate", input, null);
System.out.println(est.get("hold_credits") + " " + est.get("min_credits") + " " + est.get("model"));
input = JSON.parse(File.read("input.json"))
est = api("POST", "/estimate", input)
puts [est["hold_credits"], est["min_credits"], est["model"]].inspect
$input = json_decode(file_get_contents("input.json"), true);
$est = api("POST", "/estimate", $input);
echo $est["hold_credits"], " ", $est["min_credits"], " ", $est["model"], "\n";
var input = JsonNode.Parse(File.ReadAllText("input.json"))!;
var est = await Api.Call("POST", "/estimate", input);
Console.WriteLine($"{est!["hold_credits"]} {est["min_credits"]} {est["model"]}");
Step 5 — POST /run, then poll the job
Send an Idempotency-Key built from the lane and the input: a retry with the same key never bills twice. The page uses key-shape:<task>:<inputHash>:a<attempt> - the lane is part of the key, retry_note is left out of the hash - and its one automatic reformat retry is a new attempt number, named in the page as one extra run. A design reply carries summary, format, storage, scoping, rotation, dashboard, ownership, governance, oauth and caveats; a runbook reply carries summary, rotation, leak, offboarding, comms, metrics and caveats. The code below prints the gist of either: the prefixes and the two rungs, or each leak-response step with its deadline.
KEY="key-shape:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1"
JOB=$(curl -s -X POST "$BASE/run" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $KEY" --data @input.json | jq -r '.data.job_id')
while :; do
J=$(api GET "/jobs/$JOB"); S=$(echo "$J" | jq -r '.data.status')
case "$S" in succeeded|failed|cancelled) break;; esac; sleep 2
done
echo "$J" | jq -r '.data.output.output' > reply.json # one JSON object, the reply contract
echo "$J" | jq '.data | {charged_credits, truncated}'
jq -r 'if .task == "runbook" then (.leak[] | "\(.within): \(.step)")
else "\(.format.prefixes | map(.prefix) | join(", ")) - \(.storage.rung), \(.scoping.rung)" end' reply.json
import hashlib, time
key = "key-shape:" + inp["task"] + ":" + hashlib.sha256(json.dumps(inp).encode()).hexdigest()[:16] + ":a1"
job = api("POST", "/run", inp, {"Idempotency-Key": key})
while True:
j = api("GET", "/jobs/" + job["job_id"])
if j["status"] in ("succeeded", "failed", "cancelled"):
break
time.sleep(2)
reply = json.loads(j["output"]["output"]) # the reply as one JSON object
print(j["charged_credits"], j.get("truncated"), reply["task"])
if reply["task"] == "runbook":
for x in reply["leak"]:
print(x["within"], x["step"])
else:
print([p["prefix"] for p in reply["format"]["prefixes"]], reply["storage"]["rung"], reply["scoping"]["rung"])
import { createHash } from "node:crypto";
const key = `key-shape:${input.task}:${createHash("sha256").update(JSON.stringify(input)).digest("hex").slice(0, 16)}:a1`;
const job = await api("POST", "/run", input, { "Idempotency-Key": key });
let j;
do { await new Promise((r) => setTimeout(r, 2000)); j = await api("GET", "/jobs/" + job.job_id); }
while (!["succeeded", "failed", "cancelled"].includes(j.status));
const reply = JSON.parse(j.output.output);
console.log(j.charged_credits, j.truncated, reply.task);
if (reply.task === "runbook") for (const x of reply.leak) console.log(x.within, x.step);
else console.log(reply.format.prefixes.map((p) => p.prefix).join(", "), reply.storage.rung, reply.scoping.rung);
sum := sha256.Sum256(raw) // add "crypto/sha256", "encoding/hex", "time" to the imports
key := "key-shape:" + input["task"].(string) + ":" + hex.EncodeToString(sum[:8]) + ":a1"
job, _ := api("POST", "/run", input, map[string]string{"Idempotency-Key": key})
var j map[string]any
for {
time.Sleep(2 * time.Second)
j, _ = api("GET", "/jobs/"+job["job_id"].(string), nil, nil)
if s := j["status"]; s == "succeeded" || s == "failed" || s == "cancelled" { break }
}
var reply map[string]any
json.Unmarshal([]byte(j["output"].(map[string]any)["output"].(string)), &reply)
fmt.Println(j["charged_credits"], j["truncated"], reply["task"])
if reply["task"] == "runbook" {
ls, _ := reply["leak"].([]any)
for _, x := range ls {
xx := x.(map[string]any)
fmt.Println(xx["within"], xx["step"])
}
} else {
st, _ := reply["storage"].(map[string]any)
sc, _ := reply["scoping"].(map[string]any)
fmt.Println(st["rung"], sc["rung"])
}
var digest = java.security.MessageDigest.getInstance("SHA-256").digest(M.writeValueAsBytes(input));
String key = "key-shape:" + input.get("task") + ":" + HexFormat.of().formatHex(digest, 0, 8) + ":a1";
JsonNode job = api("POST", "/run", input, Map.of("Idempotency-Key", key)), j;
do { Thread.sleep(2000); j = api("GET", "/jobs/" + job.get("job_id").asText(), null, null); }
while (!Set.of("succeeded", "failed", "cancelled").contains(j.get("status").asText()));
JsonNode reply = M.readTree(j.get("output").get("output").asText());
System.out.println(j.get("charged_credits") + " " + reply.path("task").asText());
if (reply.path("task").asText().equals("runbook")) {
for (JsonNode x : reply.path("leak")) System.out.println(x.path("within").asText() + " " + x.path("step").asText());
} else {
System.out.println(reply.path("storage").path("rung").asText() + " " + reply.path("scoping").path("rung").asText());
}
require "digest"
key = "key-shape:#{input["task"]}:#{Digest::SHA256.hexdigest(JSON.generate(input))[0, 16]}:a1"
job = api("POST", "/run", input, { "Idempotency-Key" => key })
j = nil
loop do
sleep 2
j = api("GET", "/jobs/#{job["job_id"]}")
break if %w[succeeded failed cancelled].include?(j["status"])
end
reply = JSON.parse(j["output"]["output"])
puts [j["charged_credits"], j["truncated"], reply["task"]].inspect
if reply["task"] == "runbook"
reply["leak"].each { |x| puts "#{x["within"]} #{x["step"]}" }
else
puts "#{reply.dig("storage", "rung")} #{reply.dig("scoping", "rung")}"
end
$key = "key-shape:" . $input["task"] . ":" . substr(hash("sha256", json_encode($input)), 0, 16) . ":a1";
$job = api("POST", "/run", $input, ["Idempotency-Key" => $key]);
do { sleep(2); $j = api("GET", "/jobs/" . $job["job_id"]); }
while (!in_array($j["status"], ["succeeded", "failed", "cancelled"]));
$reply = json_decode($j["output"]["output"], true);
echo $j["charged_credits"], " ", $reply["task"], "\n";
if ($reply["task"] === "runbook") { foreach ($reply["leak"] as $x) echo $x["within"], " ", $x["step"], "\n"; }
else { echo $reply["storage"]["rung"], " ", $reply["scoping"]["rung"], "\n"; }
var digest = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(input.ToJsonString()));
var key = $"key-shape:{input["task"]}:{Convert.ToHexString(digest, 0, 8).ToLowerInvariant()}:a1";
var job = await Api.Call("POST", "/run", input, new() { ["Idempotency-Key"] = key });
JsonNode j;
do { await Task.Delay(2000); j = (await Api.Call("GET", "/jobs/" + job!["job_id"]))!; }
while (!new[] { "succeeded", "failed", "cancelled" }.Contains(j["status"]!.GetValue<string>()));
var reply = JsonNode.Parse(j["output"]!["output"]!.GetValue<string>())!;
var task = (string?)reply["task"];
Console.WriteLine($"{j["charged_credits"]} {task}");
if (task == "runbook") { foreach (var x in reply["leak"]!.AsArray()) Console.WriteLine($"{x!["within"]} {x["step"]}"); }
else Console.WriteLine($"{reply["storage"]?["rung"]} {reply["scoping"]?["rung"]}");
Step 6 — POST /run-stream
The same run as Server-Sent Events, with the same input.json. Browsers receive progress ticks rather than text deltas; the final done event carries the reply.
# Server-Sent Events: tick/delta events while the job runs, then a final event with the job
curl -N -s -X POST "$BASE/run-stream" -H "Content-Type: application/json" -H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: key-shape:$(jq -r .task input.json):$(shasum -a 256 input.json | cut -c1-16):a1" --data @input.json
rev = json.load(open("input.json")) # the same run input as input.json: {"task": "design", "platform": "Platform: Quillmint Payments\nMode: retrofit\n...", "keys": "Prefixes:\n- key_ | the only key type\n...", "carry": "", "facts": "The page's checks (12 fail, 6 warn, ...):\n- Format | env | fail | ..."}
# - see the input table
req = urllib.request.Request(BASE + "/run-stream", method="POST", data=json.dumps(rev).encode(),
headers={"Content-Type": "application/json", "Authorization": "Bearer " + TOKEN,
"Idempotency-Key": "key-shape:" + rev["task"] + ":" + hashlib.sha256(json.dumps(rev).encode()).hexdigest()[:16] + ":a1"})
with urllib.request.urlopen(req) as r:
for line in r:
line = line.decode().strip()
if line.startswith("data:"):
print(line[5:].strip()[:200])
const rev = JSON.parse(readFileSync("input.json", "utf8"));
const r = await fetch(BASE + "/run-stream", { method: "POST", body: JSON.stringify(rev), headers: {
"Content-Type": "application/json", Authorization: "Bearer " + TOKEN,
"Idempotency-Key": `key-shape:${rev.task}:${createHash("sha256").update(JSON.stringify(rev)).digest("hex").slice(0, 16)}:a1` } });
const dec = new TextDecoder();
for await (const chunk of r.body) for (const line of dec.decode(chunk).split("\n"))
if (line.startsWith("data:")) console.log(line.slice(5).trim().slice(0, 200));
rraw, _ := os.ReadFile("input.json") // add "bufio", "strings" to the imports
rs := sha256.Sum256(rraw)
req, _ := http.NewRequest("POST", base+"/run-stream", bytes.NewReader(rraw))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", "Bearer "+token)
req.Header.Set("Idempotency-Key", "key-shape:"+input["task"].(string)+":"+hex.EncodeToString(rs[:8])+":a1")
res, _ := http.DefaultClient.Do(req)
sc := bufio.NewScanner(res.Body)
for sc.Scan() { if l := sc.Text(); strings.HasPrefix(l, "data:") { fmt.Println(l[5:]) } }
String rev = java.nio.file.Files.readString(java.nio.file.Path.of("input.json"));
var rd = java.security.MessageDigest.getInstance("SHA-256").digest(rev.getBytes());
var sreq = HttpRequest.newBuilder(URI.create(BASE + "/run-stream")).header("Content-Type", "application/json")
.header("Authorization", "Bearer " + token).header("Idempotency-Key", "key-shape:" + input.get("task") + ":" + HexFormat.of().formatHex(rd, 0, 8) + ":a1")
.POST(HttpRequest.BodyPublishers.ofString(rev)).build();
C.send(sreq, HttpResponse.BodyHandlers.ofLines()).body().filter(l -> l.startsWith("data:")).forEach(System.out::println);
rev = File.read("input.json")
uri = URI(BASE + "/run-stream")
req = Net::HTTP::Post.new(uri, "Content-Type" => "application/json", "Authorization" => "Bearer #{TOKEN}",
"Idempotency-Key" => "key-shape:#{JSON.parse(rev)["task"]}:#{Digest::SHA256.hexdigest(rev)[0, 16]}:a1")
req.body = rev
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |h|
h.request(req) { |res| res.read_body { |chunk| chunk.each_line { |l| puts l if l.start_with?("data:") } } }
end
$rev = file_get_contents("input.json");
$ch = curl_init(BASE . "/run-stream");
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_POSTFIELDS => $rev, CURLOPT_HTTPHEADER => [
"Content-Type: application/json", "Authorization: Bearer $TOKEN",
"Idempotency-Key: key-shape:" . json_decode($rev, true)["task"] . ":" . substr(hash("sha256", $rev), 0, 16) . ":a1"],
CURLOPT_WRITEFUNCTION => function ($ch, $chunk) { foreach (explode("\n", $chunk) as $l) if (str_starts_with($l, "data:")) echo $l, "\n"; return strlen($chunk); }]);
curl_exec($ch);
var rev = File.ReadAllText("input.json");
var rd = System.Security.Cryptography.SHA256.HashData(System.Text.Encoding.UTF8.GetBytes(rev));
var sreq = new HttpRequestMessage(HttpMethod.Post, Api.Base + "/run-stream") { Content = new StringContent(rev, System.Text.Encoding.UTF8, "application/json") };
sreq.Headers.Add("Authorization", "Bearer " + Api.Token);
sreq.Headers.Add("Idempotency-Key", $"key-shape:{input["task"]}:{Convert.ToHexString(rd, 0, 8).ToLowerInvariant()}:a1");
using var http = new HttpClient();
using var res = await http.SendAsync(sreq, HttpCompletionOption.ResponseHeadersRead);
using var rdr = new StreamReader(await res.Content.ReadAsStreamAsync());
for (string? l; (l = await rdr.ReadLineAsync()) != null;) if (l.StartsWith("data:")) Console.WriteLine(l);