Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
File renamed without changes.
Binary file not shown.
37 changes: 37 additions & 0 deletions 10-26 AI Engineering Night/demo/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# copy to .env and fill in. The scripts source it; stackql-deploy reads it with
# --env-file .env; the agent loads it itself. Never commit .env.

# --- the model: OpenAI through the Vercel AI SDK --------------------------------
OPENAI_API_KEY=sk-...
# gpt-6.1-sol keeps a live run short; gpt-6-astra for a more careful report
OPENAI_MODEL=gpt-6.1-sol

# --- the estate: one Cloudflare zone, a throwaway one ---------------------------
# a token scoped to THIS zone only: Zone > Zone WAF > Edit, Zone > Zone > Read,
# Account > Account Analytics > Read (for the traffic query). Nothing else.
CLOUDFLARE_API_TOKEN=
CLOUDFLARE_ZONE_ID=
# printed by every script so the room (and you) can see which zone is in play
CLOUDFLARE_ZONE_NAME=stackql.xyz

# --- the declaration's home on GitHub, for the provenance query ------------------
# public repo, no token needed. owner/repo and the path of the manifest in it
DECLARATION_REPO=stackql-labs/agentic-platform-engineering
DECLARATION_PATH="10-26 AI Engineering Night/demo/stack/stackql_manifest.yml"

# --- the platform: StackQL MCP server -------------------------------------------
# read_only: writes refused by the server. safe: writes pause for operator approval.
# The agent code never starts a full_access server.
STACKQL_MCP_MODE=read_only
# where providers are pulled to; cloudflare and github must be there. Empty: stackql's own
# default (~/.stackql), and no --approot is passed
STACKQL_APPROOT=
# the stackql binary the MCP launcher runs. Empty: `stackql` on the PATH, nothing downloaded
STACKQL_MCP_BIN=
# the server's audit sink (one JSON record per tool call), the transcripts and the last
# proposal. Empty: audit/ under the directory the agent runs from (agent/audit with npm run)
STACKQL_AUDIT_DIR=
# cap on model steps per run
AGENT_MAX_STEPS=12
# the stackql-deploy entry point. Empty: `stackql-deploy` on the Git Bash PATH
STACKQL_DEPLOY=
8 changes: 8 additions & 0 deletions 10-26 AI Engineering Night/demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
.env
runs/
audit/
.stackql/
agent/node_modules/
agent/dist/
*.log
.stackql-deploy-exports
103 changes: 103 additions & 0 deletions 10-26 AI Engineering Night/demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# demo: the 3am rate limit

Demo assets for "Query before mutation: guardrail patterns for agents that touch production infrastructure" (AI Engineering Night, Melbourne, 29 October 2026, 18 minutes including demo and Q&A). The talk makes the argument (the human pause is gone; put the judgement back into the system); this demo is where StackQL appears, as the implementation of the guardrails.

## the scenario

One Cloudflare zone (a throwaway, free plan), with one rate limit rule declared in a `stackql-deploy` manifest and asserted against the live API with no state file: block any client over 100 requests per 10 seconds. At 3am someone saw a burst of bot traffic, tightened the rule in the dashboard to 5, left a note in the description, and went back to bed. Every page load with more than five assets now gets a 429.

That is drift, and it is hurting. A scheduled platform agent reviews the zone with the StackQL MCP server as its only tool surface, reads the live rule, reads when the declaration last changed (GitHub), reads the last hour of traffic, and decides. It can propose exactly one write: the assertion of the declared rule. Whether that write runs is decided by three gates the platform owns and the model cannot change.

## how it maps to the talk

| Guardrail (slide) | Where it shows in the demo |
| --- | --- |
| 01 Query before mutation | The agent's first statements are `SELECT`s against `cloudflare.rulesets.phases`, `github.repos.commits`, `cloudflare.zones.http_requests_adaptive_groups`. It proposes nothing until it has read live state, and it reads the declaration's history too. |
| 02 Idempotent assertions | `scripts/build.sh` is one `stackql-deploy build`: the createorupdate anchor runs the same REPLACE whether the rule is missing, drifted or already right, and the statecheck confirms it. The same command creates the rule and converges it after the drift. The agent's one permitted write is that same assertion, so a retry changes nothing. |
| 03 Policy gates | `agent/src/policy.ts`: four rules, in code, checked before the statement leaves the agent process. Then the server mode: `read_only` refuses, `safe` asks. Then a human, bound to the exact statement. |
| 04 Scoped credentials and mutation surfaces | The token is scoped to one zone and lives in the server process, never in the model's context. The mutation surface is one statement on one resource, given to the model verbatim; the policy declines anything else, field for field. |
| 05 Recovery paths | Blocked: the agent reports the statement and stops; it does not rephrase or retry. A human replays it through the gate (`npm run probe` in `safe`), or the declaration is re-asserted (`scripts/restore.sh`). Failed: re-assert; the build is idempotent. |

## layout

| Path | What it is |
| --- | --- |
| `stack/stackql_manifest.yml`, `stack/resources/rate_limit.iql` | the estate and the declaration: one rate limit rule, a post-build summary query |
| `agent/src/agent.ts` | the agent. Starts the MCP server in the mode the platform chose, renders the prompts with the declared rule and the permitted write, wraps the write tool with the policy, runs the loop, prints every statement, writes a transcript |
| `agent/src/policy.ts` | the policy gate: a pure function, `check(sql, declared)`, with `agent/test/policy.test.ts` |
| `agent/src/declaration.ts` | the declared rule and the permitted write, rendered from the manifest; `agent/test/declaration.test.ts` checks it against the `.iql` anchor field for field |
| `agent/src/console.ts` | the screen: colour, ASCII boxes and tables, the renderer for the model's markdown report |
| `agent/src/audit.ts` | `npm run audit`: every audit sink of the session as one table |
| `recordings/` | the transcript of the dry run, for `npm run replay` |
| `agent/prompts/system.md` | the briefing: the surface, the resource shapes, the one permitted write |
| `agent/prompts/intent.md` | the intent: the scheduled review, with `{{ placeholders }}` filled from `.env` and the manifest |
| `scripts/_env.sh` | sourced by the scripts: `.env`, defaults, refuses to run without a named zone |
| `scripts/build.sh` | `stackql-deploy build`: the rule, and the convergence |
| `scripts/drift.sh`, `scripts/drift.sql` | the 3am change, one statement |
| `scripts/restore.sh` | `build.sh` under the name the running order uses, then the rule read back |
| `scripts/shell.sh`, `scripts/shell.iql` | the human view of the same estate, for `stackql shell` |
| `scripts/denied.sql` | a plausible write that is not the declaration, for the policy beat |

## prerequisites

- `stackql` on PATH ([downloads](https://stackql.io/downloads)), with the `cloudflare` and `github` providers pulled (`stackql exec "REGISTRY PULL cloudflare"`, `stackql exec "REGISTRY PULL github"`). stackql's approot is `.stackql` under the directory it runs from: the first `scripts/build.sh` pulls `cloudflare` into the demo root's, `stackql shell` there needs `github` too, and the agent's MCP server uses `~/.stackql` (the launcher's default), which needs both. Or set `STACKQL_APPROOT` in `.env` and keep one store.
- `stackql-deploy` on the Git Bash PATH (`pip install stackql-deploy`); otherwise set `STACKQL_DEPLOY` in `.env` to the exe
- a Cloudflare zone you do not care about, on the free plan, and an API token scoped to that zone: Zone WAF Edit, Zone Read, Account Analytics Read
- Node 24 or later
- an OpenAI API key

Windows notes: run the scripts from Git Bash. `stackql-deploy` needs `PYTHONUTF8=1` (the scripts set it).

## setup

```bash
cp .env.example .env # keys, zone, model
scripts/build.sh # the rule, from nothing, in a few seconds
scripts/drift.sh # the 3am change
cd agent && npm install
```

## running order

Before the talk: rule built, `scripts/drift.sh` applied, `scripts/shell.sh` rendered, a terminal in `agent/` with a wide window. [runsheet.md](runsheet.md) is the on-screen walkthrough.

| Beat | Command | What the room sees | Guardrail |
| --- | --- | --- | --- |
| The zone at 3am | `stackql shell`, paste from `runs/shell.iql` | threshold 5 (declared 100), the note in the description, `last_updated` at 3am; the declaration's last commit, older; `out_of_band = 1` from one query across two providers; 429s if there has been traffic | 01 |
| The agent, `read_only` | `npm run agent` | the declared table; the statements stream past: rule, commits, traffic; the diagnosis; the one write proposed; the server refuses it; the agent reports and stops | 01, 03, 04, 05 |
| The gate, `safe` | `STACKQL_MCP_MODE=safe npm run probe` | the agent's proposal replayed: policy passes it, the approval box, `y`, the rule is restored | 03, 05 |
| The policy | `npm run probe -- ../scripts/denied.sql` | threshold 1000: declined by policy, `$[0].ratelimit.requests_per_period: 1000 vs declared 100`, before the server or a human is involved | 03, 04 |
| The event log | `npm run audit` | every run of the session in one table: the statement, the mode, the decision, one record each, including the policy decline | 03 |
| Converge (optional) | `scripts/restore.sh` | `stackql-deploy build`: the same REPLACE runs, the statecheck confirms it, the rule is read back where the manifest says | 02, 05 |

Say which mode the server is in before each run.

## fallback

`npm run replay` replays the newest transcript from `agent/audit/` (or `recordings/`) with the pacing of a run, under a banner that says it is a replay. Use it when there is no network or no key. Do not present it as a live run.

## what it proves, and does not

- It demonstrates the five guardrails on one public cloud (Cloudflare) plus one SaaS provider (GitHub), with the policy enforced in code, the mode enforced by the server, and the convergence done by a declarative build.
- A query is an observation, not a lock. The agent re-reads after a change; nothing here is an atomic precondition.
- The approval at the terminal is the MCP elicitation flow answered by whoever is at the keyboard. It binds the answer to the exact statement; it is not an authenticated production approval service.
- The agent's blast radius is the token's: one zone, WAF edit. A broader token would widen it silently; the policy would still hold, the token is the backstop.
- The traffic query needs traffic. Zero rows is a valid answer and the agent is told to say so.
- `safe` trusts the MCP client to show the prompt to a person; against an untrusted client use `read_only` or a narrowed tool set.

## status

Run end to end on 29 October 2026 on Windows 11 (Git Bash for the scripts, PowerShell for the agent) with stackql v0.12.824, `@stackql/mcp-server` 0.12.824, the cloudflare provider v26.07.00424, github v26.08.00448, stackql-deploy 2.4.0, Node 24.19.0, `ai` 7.0.154, `@ai-sdk/openai` 4.0.102, `@ai-sdk/mcp` 2.0.85 and `gpt-6.1-sol`. `npm run typecheck` and `npm test` (18 tests) pass; the `.iql` anchor and the agent's rendered statement are checked field for field.

| Beat | Wall time |
| --- | --- |
| `scripts/build.sh`, create or converge | 15 to 17 s, of which 2 to 3 s is the deploy; the rest is stackql-deploy starting its server |
| `scripts/drift.sh` | about 1 s |
| `npm run tools` | about 1 s |
| `npm run agent`, `read_only` | 25 to 30 s, 5 steps, 4 tool calls, about 46k tokens in |
| `npm run probe`, `safe`, approved | about 7 s plus the pause at the prompt |
| `npm run probe -- ../scripts/denied.sql` | about 2 s |
| `npm run audit` | about 1 s |
| `npm run replay` | about 10 s |

`recordings/agent_dry-run_2026-10-29.txt` is the `read_only` run against the drifted zone, with the GitHub query returning zero rows because the folder was not yet on origin. The zone was left drifted, ready for the talk. The server's three start-up lines on stderr are dropped from the screen; anything else it writes there still shows.
Loading