assay-lua 0.9.0

General-purpose enhanced Lua runtime. Batteries-included scripting, automation, and web services.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
---
name: assay
description: Infrastructure scripting runtime — 51 modules for Kubernetes, ArgoCD, Vault, Prometheus, HTTP servers, AI agents, databases. Replaces kubectl, Python, Node.js, curl, jq in one 9 MB binary.
metadata:
  author: developerinlondon
  version: "0.6.1"
---

# Assay Skill — LLM Agent Guide

Assay is a single ~9 MB static binary that runs Lua scripts in Kubernetes. It replaces 50-250 MB
Python/Node/kubectl containers in K8s Jobs. One binary, two modes: run a `.lua` script directly, or
run a `.yaml` check config with retry/backoff/structured output.

The image is `ghcr.io/developerinlondon/assay:latest` (~9 MB compressed). Install locally with
`cargo install assay-lua` or download from GitHub Releases.

## Quick Start

```bash
# Run a Lua script
assay script.lua

# Run YAML check orchestration
assay checks.yaml

# Test Lua inline (great for quick experiments)
assay exec -e 'log.info("hello from assay")'

# Discover modules by keyword
assay context "vault"

# List all available modules
assay modules
```

## CLI Commands

| Command                     | What it does                                  |
| --------------------------- | --------------------------------------------- |
| `assay script.lua`          | Auto-detect and run Lua script                |
| `assay checks.yaml`         | Auto-detect and run YAML check config         |
| `assay run script.lua`      | Explicit run (same as auto-detect)            |
| `assay exec -e 'lua code'`  | Evaluate Lua inline                           |
| `assay exec script.lua`     | Run Lua file via exec subcommand              |
| `assay context "<keyword>"` | Find modules matching keyword, shows quickref |
| `assay modules`             | List all 51 modules (34 stdlib + 17 builtins) |

## Discovering Modules

When you need to interact with a service, use `assay context` to find the right module:

```
1. Run: assay context "<what you need>"
2. Read the output — it shows matching modules and their methods
3. Use require("assay.<module>") in your script
4. Call client methods shown in the quickref
```

Example:

```bash
$ assay context "grafana"
# Assay Module Context

## Matching Modules

### assay.grafana
Grafana monitoring and dashboards. Health, datasources, annotations, alerts, folders.
Methods:
  c:health() -> {database, version, commit} | Check Grafana health
  c:datasources() -> [{id, name, type, url}] | List all datasources
  ...
```

The output is prompt-ready Markdown. Paste it into your context or read it to know exactly which
methods exist and what they return.

## Writing Lua Scripts

All stdlib modules follow the same three-step pattern:

```lua
-- 1. Require the module
local grafana = require("assay.grafana")

-- 2. Create a client
local c = grafana.client("http://grafana:3000", { api_key = "glsa_..." })

-- 3. Call methods
local h = c:health()
assert.eq(h.database, "ok", "Grafana database unhealthy")
log.info("Grafana version: " .. h.version)
```

Auth options vary by service:

```lua
-- Token auth
local c = vault.client(url, { token = "hvs...." })

-- API key auth
local c = grafana.client(url, { api_key = "glsa_..." })

-- Username/password
local c = grafana.client(url, { username = "admin", password = "secret" })
```

## Builtins (no require needed)

These are always available in every `.lua` script.

### HTTP

| Function                       | Description                                    |
| ------------------------------ | ---------------------------------------------- |
| `http.get(url, opts?)`         | GET request, returns `{status, body, headers}` |
| `http.post(url, body, opts?)`  | POST request (auto-JSON if body is table)      |
| `http.put(url, body, opts?)`   | PUT request                                    |
| `http.patch(url, body, opts?)` | PATCH request                                  |
| `http.delete(url, opts?)`      | DELETE request                                 |
| `http.serve(port, routes)`     | Start HTTP server (async handlers)             |

Options: `{ headers = { ["X-Key"] = "value" } }`

`http.serve` response handlers accept array values for headers to emit the same header name
multiple times — required for `Set-Cookie` with multiple cookies, and useful for `Link`, `Vary`,
`Cache-Control`, etc.:

```lua
return {
  status = 200,
  headers = {
    ["Set-Cookie"] = {
      "session=abc; Path=/; HttpOnly",
      "csrf=xyz; Path=/",
    },
  },
  body = "ok",
}
```

String header values still work as before.

### Serialization

| Function             | Description                     |
| -------------------- | ------------------------------- |
| `json.parse(str)`    | Parse JSON string to Lua table  |
| `json.encode(tbl)`   | Encode Lua table to JSON string |
| `yaml.parse(str)`    | Parse YAML string to Lua table  |
| `yaml.encode(tbl)`   | Encode Lua table to YAML string |
| `toml.parse(str)`    | Parse TOML string to Lua table  |
| `toml.encode(tbl)`   | Encode Lua table to TOML string |
| `base64.encode(str)` | Base64 encode                   |
| `base64.decode(str)` | Base64 decode                   |

### Filesystem

| Function            | Description          |
| ------------------- | -------------------- |
| `fs.read(path)`     | Read file to string  |
| `fs.write(path, s)` | Write string to file |

### Cryptography

| Function                             | Description                                                   |
| ------------------------------------ | ------------------------------------------------------------- |
| `crypto.jwt_sign(claims, key, alg)`  | Sign JWT — alg: HS256, RS256/384/512, ES256/384               |
| `crypto.jwt_decode(token)`           | Decode `{header, claims}` WITHOUT verifying — trusted channel |
| `crypto.hash(str, alg)`              | Hash string (sha256, sha384, sha512, md5)                     |
| `crypto.hmac(key, data, alg?, raw?)` | HMAC (sha256 default, raw=true for binary)                    |
| `crypto.random(len)`                 | Secure random hex string of length `len`                      |

### Regex

| Function                         | Description              |
| -------------------------------- | ------------------------ |
| `regex.match(pattern, str)`      | Test if pattern matches  |
| `regex.find(pattern, str)`       | Find first match         |
| `regex.find_all(pattern, str)`   | Find all matches (array) |
| `regex.replace(pattern, str, r)` | Replace matches          |

### Database

| Function                         | Description                                   |
| -------------------------------- | --------------------------------------------- |
| `db.connect(url)`                | Connect (Postgres, MySQL, SQLite)             |
| `db.query(conn, sql, params?)`   | Execute query, return rows as array of tables |
| `db.execute(conn, sql, params?)` | Execute statement, return affected row count  |
| `db.close(conn)`                 | Close connection                              |

URLs: `postgres://user:pass@host:5432/db`, `mysql://...`, `sqlite:///path/to/file.db`

### WebSocket and Templates

| Function                          | Description                   |
| --------------------------------- | ----------------------------- |
| `ws.connect(url)`                 | Connect to WebSocket server   |
| `ws.send(conn, msg)`              | Send message                  |
| `ws.recv(conn)`                   | Receive message (blocking)    |
| `ws.close(conn)`                  | Close connection              |
| `template.render(path, vars)`     | Render Jinja2 template file   |
| `template.render_string(tmpl, v)` | Render Jinja2 template string |

### Async

| Function                       | Description                        |
| ------------------------------ | ---------------------------------- |
| `async.spawn(fn)`              | Spawn async task, returns handle   |
| `async.spawn_interval(fn, ms)` | Spawn recurring task every `ms` ms |
| `handle:await()`               | Wait for task completion           |
| `handle:cancel()`              | Cancel recurring task              |

### Assertions

| Function                          | Description              |
| --------------------------------- | ------------------------ |
| `assert.eq(a, b, msg?)`           | Assert equal             |
| `assert.ne(a, b, msg?)`           | Assert not equal         |
| `assert.gt(a, b, msg?)`           | Assert greater than      |
| `assert.lt(a, b, msg?)`           | Assert less than         |
| `assert.contains(str, sub, msg?)` | Assert substring present |
| `assert.not_nil(val, msg?)`       | Assert not nil           |
| `assert.matches(str, pat, msg?)`  | Assert regex match       |

### Logging and Utilities

| Function         | Description              |
| ---------------- | ------------------------ |
| `log.info(msg)`  | Info log                 |
| `log.warn(msg)`  | Warning log              |
| `log.error(msg)` | Error log                |
| `env.get(key)`   | Get environment variable |
| `sleep(secs)`    | Sleep for N seconds      |
| `time()`         | Unix timestamp (integer) |

### Temporal gRPC (optional feature)

Available when built with `--features temporal`. Native gRPC client for Temporal workflow engine.

| Function                                     | Description                                           |
| -------------------------------------------- | ----------------------------------------------------- |
| `temporal.connect({ url, namespace? })`      | Connect to Temporal server, returns client             |
| `temporal.start({ url, namespace?, ... })`   | One-shot: connect + start workflow                     |
| `client:start_workflow({ task_queue, workflow_type, workflow_id, input? })` | Start a workflow execution |
| `client:signal_workflow({ workflow_id, signal_name, input? })`             | Signal a running workflow  |
| `client:query_workflow({ workflow_id, query_type, input? })`               | Query workflow state       |
| `client:describe_workflow(workflow_id)`       | Get workflow status and metadata                       |
| `client:get_result({ workflow_id })`          | Wait for workflow completion and get result             |
| `client:cancel_workflow(workflow_id)`         | Request workflow cancellation                           |
| `client:terminate_workflow(workflow_id)`      | Forcefully terminate a workflow                         |

## Stdlib Modules Quick Reference

All 34 modules follow `require("assay.<name>")` then `M.client(url, opts)`.

| Module                | Description                                                                |
| --------------------- | -------------------------------------------------------------------------- |
| `assay.prometheus`    | PromQL queries, alerts, targets, rules, label values, series               |
| `assay.alertmanager`  | Manage alerts, silences, receivers, config                                 |
| `assay.loki`          | Push logs, query with LogQL, labels, series, tail                          |
| `assay.grafana`       | Health, dashboards, datasources, annotations, alert rules, folders         |
| `assay.k8s`           | 30+ resource types, CRDs, readiness checks, pod logs, rollouts             |
| `assay.argocd`        | Apps, sync, health, projects, repositories, clusters                       |
| `assay.kargo`         | Stages, freight, promotions, warehouses, pipeline status                   |
| `assay.flux`          | GitRepositories, Kustomizations, HelmReleases, notifications               |
| `assay.traefik`       | Routers, services, middlewares, entrypoints, TLS status                    |
| `assay.vault`         | KV secrets, policies, auth, transit, PKI, token management                 |
| `assay.openbao`       | Alias for vault (OpenBao API-compatible)                                   |
| `assay.certmanager`   | Certificates, issuers, ACME orders and challenges                          |
| `assay.eso`           | ExternalSecrets, SecretStores, ClusterSecretStores sync status             |
| `assay.dex`           | OIDC discovery, JWKS, health, configuration validation                     |
| `assay.zitadel`       | OIDC identity management with JWT machine auth                             |
| `assay.ory.kratos`        | Ory Kratos — login/registration/recovery/settings flows, identities, sessions |
| `assay.ory.hydra`         | Ory Hydra OAuth2/OIDC — clients, authorize URLs, tokens, login/consent, JWKs |
| `assay.ory.keto`          | Ory Keto ReBAC — relation tuples, permission checks, expand                |
| `assay.ory.rbac`          | Capability-based RBAC engine over Keto — roles + capabilities, separation of duties |
| `assay.ory`           | Convenience wrapper — `ory.connect()` builds kratos/hydra/keto clients together; also re-exports `rbac` |
| `assay.crossplane`    | Providers, XRDs, compositions, managed resources                           |
| `assay.velero`        | Backups, restores, schedules, storage locations                            |
| `assay.temporal`      | Workflows, task queues, schedules, signals + native gRPC (temporal feature)|
| `assay.harbor`        | Projects, repositories, artifacts, vulnerability scanning                  |
| `assay.healthcheck`   | HTTP checks, JSON path, body matching, latency, multi-check                |
| `assay.s3`            | S3-compatible storage (AWS, R2, MinIO) with Sig V4 auth                    |
| `assay.postgres`      | Postgres helpers: users, databases, grants, Vault integration              |
| `assay.unleash`       | Feature flags: projects, environments, features, strategies, tokens        |
| `assay.openclaw`      | OpenClaw AI agent — invoke tools, state, diff, approve, LLM tasks          |
| `assay.github`        | GitHub REST API — PRs, issues, actions, repos, GraphQL                     |
| `assay.gmail`         | Gmail REST API with OAuth2 — search, read, reply, send, labels             |
| `assay.gcal`          | Google Calendar REST API with OAuth2 — events CRUD, calendar list          |
| `assay.oauth2`        | Google OAuth2 token management — credentials, auto-refresh, persistence    |
| `assay.email_triage`  | Email classification — deterministic rules + LLM-assisted triage           |

## Common Patterns

### HTTP Health Check

```lua
#!/usr/bin/assay
local resp = http.get("http://grafana.monitoring:80/api/health")
assert.eq(resp.status, 200, "Grafana not responding")

local data = json.parse(resp.body)
assert.eq(data.database, "ok", "Grafana database unhealthy")
log.info("Grafana healthy: version=" .. data.version)
```

### JWT Auth and API Call

```lua
#!/usr/bin/assay
-- Read RSA private key from mounted secret
local key = fs.read("/secrets/jwt-key.pem")

local token = crypto.jwt_sign({
  iss = "assay-client",
  sub = "admin@example.com",
  exp = time() + 3600
}, key, "RS256")

local resp = http.get("https://api.example.com/users", {
  headers = { Authorization = "Bearer " .. token }
})

assert.eq(resp.status, 200, "API call failed")
local users = json.parse(resp.body)
log.info("Found " .. #users .. " users")
```

### Vault Secret Retrieval

```lua
#!/usr/bin/assay
local vault = require("assay.vault")

local token = env.get("VAULT_TOKEN")
local c = vault.client("http://vault:8200", { token = token })

-- Read KV v2 secret
local secret = c:kv_get("secret", "myapp/config")
assert.not_nil(secret, "Secret not found")

log.info("db_password: " .. secret.data.db_password)
```

### Kubernetes Pod Readiness Check

```lua
#!/usr/bin/assay
local k8s = require("assay.k8s")

local c = k8s.client("https://kubernetes.default.svc", {
  token = fs.read("/var/run/secrets/kubernetes.io/serviceaccount/token"),
  ca_cert = fs.read("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt"),
})

-- Wait for deployment to be ready
local deploy = c:deployment("my-namespace", "my-app")
assert.not_nil(deploy, "Deployment not found")
assert.eq(deploy.status.readyReplicas, deploy.spec.replicas, "Not all replicas ready")
log.info("Deployment ready: " .. deploy.metadata.name)
```

### Prometheus Metric Query

```lua
#!/usr/bin/assay
local prom = require("assay.prometheus")

local c = prom.client("http://prometheus.monitoring:9090")

-- Check targets are up
local targets = c:targets()
local up_count = 0
for _, t in ipairs(targets.activeTargets) do
  if t.health == "up" then up_count = up_count + 1 end
end
assert.gt(up_count, 0, "No Prometheus targets are up")

-- Query a metric
log.info("Active targets: " .. up_count .. ", up query: " .. tostring(c:query("up")))
```

### Temporal Workflow (gRPC)

```lua
#!/usr/bin/assay
-- Native gRPC client (requires --features temporal)
local client = temporal.connect({
  url = "temporal-frontend.infra:7233",
  namespace = "my-namespace",
})

-- Start a workflow
local handle = client:start_workflow({
  task_queue = "my-queue",
  workflow_type = "ProcessOrder",
  workflow_id = "order-12345",
  input = { item = "widget", quantity = 3 },
})
log.info("Started workflow: " .. handle.run_id)

-- Check status
local info = client:describe_workflow("order-12345")
log.info("Status: " .. info.status)

-- Signal a running workflow
client:signal_workflow({
  workflow_id = "order-12345",
  signal_name = "approve",
  input = { approved_by = "admin" },
})
```

## Error Handling

Errors from stdlib methods follow the format: `"<module>: <METHOD> <path> HTTP <status>: <body>"`

Use `pcall` to catch errors without crashing the script:

```lua
local vault = require("assay.vault")

local ok, err = pcall(function()
  local c = vault.client("http://vault:8200", { token = env.get("VAULT_TOKEN") })
  return c:kv_get("secret", "myapp/config")
end)

if not ok then
  log.error("Vault read failed: " .. tostring(err))
  -- handle gracefully or re-raise
  error(err)
end
```

For 404 responses, stdlib modules return `nil` rather than raising an error:

```lua
local secret = c:kv_get("secret", "maybe/exists")
if secret == nil then
  log.warn("Secret not found, using defaults")
else
  log.info("Secret found")
end
```

## YAML Check Mode

For structured orchestration with retry, backoff, and JSON output:

```yaml
timeout: 120s
retries: 3
backoff: 5s
parallel: false

checks:
  - name: grafana-healthy
    type: http
    url: http://grafana.monitoring:80/api/health
    expect:
      status: 200
      json: ".database == \"ok\""

  - name: prometheus-targets
    type: prometheus
    url: http://prometheus.monitoring:9090
    query: "count(up)"
    expect:
      min: 1

  - name: custom-check
    type: script
    file: verify.lua
```

Check types: `http`, `prometheus`, `script`. Exit code 0 = all pass, 1 = any fail.

## Tips for LLM Agents

**Finding the right module**: Run `assay context "<service name>"` before writing any script. The
output shows exact method signatures and return types. Don't guess.

**Testing snippets**: Use `assay exec -e 'log.info(json.encode({a=1}))'` to test individual
expressions before putting them in a full script.

**In-cluster auth**: K8s service account tokens are at
`/var/run/secrets/kubernetes.io/serviceaccount/token`. Read them with `fs.read()`.

**Environment variables**: Pass secrets via env vars, read with `env.get("MY_SECRET")`. Never
hardcode credentials in scripts.

**Shebang scripts**: Add `#!/usr/bin/assay` as the first line and `chmod +x script.lua` to run
scripts directly without the `assay` prefix.

**Module not found**: All 34 stdlib modules are embedded in the binary. If `require("assay.foo")`
fails, run `assay modules` to see the exact module names.

**Lua 5.5 specifics**: Assay uses Lua 5.5 (not LuaJIT). Integer division is `//`, bitwise ops use
`&`, `|`, `~`, `<<`, `>>`. The `#` operator on tables counts only the sequence part.

**Debugging**: Add `log.info(json.encode(some_table))` to inspect table contents. The `json.encode`
builtin handles nested tables.

**Temporal gRPC vs HTTP**: The `temporal` builtin (gRPC) and `assay.temporal` stdlib (HTTP) are
complementary. Use `temporal.connect()` for starting/signaling workflows (gRPC, more reliable).
Use `require("assay.temporal").client()` for querying the Temporal Web UI (HTTP REST API).

## MCP Replacement

Assay replaces MCP (Model Context Protocol) servers with embedded Lua modules. Instead of running
separate Docker containers for each MCP server, you write one Lua script with
`require("assay.<module>")`.

| MCP Server                                | Stars | Assay Module                     | Coverage |
| ----------------------------------------- | ----- | -------------------------------- | -------- |
| modelcontextprotocol/servers (filesystem) | 79K   | `fs.read/write` builtin          | ✅ Full  |
| modelcontextprotocol/servers (fetch)      | 79K   | `http.*` builtins                | ✅ Full  |
| punkpeye/mcp-postgres                     | 3K+   | `assay.postgres`                 | ✅ Full  |
| wong2/mcp-grafana                         | 2K+   | `assay.grafana`                  | ✅ Full  |
| prometheus-community/mcp-prometheus       | 500+  | `assay.prometheus`               | ✅ Full  |
| [42 MCP servers total]                    | —     | See assay.rs/mcp-comparison.html | —        |

Key insight: MCP servers require persistent processes, auth config, and container overhead. Assay
modules are embedded Lua — zero process overhead, same binary, same auth pattern.

Run `assay context "grafana"` to get prompt-ready method signatures for any module.

## AI Agent Integration

Assay integrates with all major AI coding agents via `assay context <query>` (today) or
`assay mcp-serve` (v0.6.0).

### Claude Code

Add to `.mcp.json` (Coming Soon — v0.6.0):

```json
{
  "mcpServers": {
    "assay": {
      "command": "assay",
      "args": ["mcp-serve"]
    }
  }
}
```

Today — add to your AGENTS.md or .cursorrules:

```
Run `assay context <query>` to get accurate Lua method signatures before writing assay scripts.
Example: `assay context "grafana"` returns all grafana client methods with types.
```

### Cursor

Add to `.cursor/mcp.json` (Coming Soon — v0.6.0):

```json
{
  "mcpServers": {
    "assay": { "command": "assay", "args": ["mcp-serve"] }
  }
}
```

### Windsurf

Add to `~/.codeium/windsurf/mcp_config.json` (Coming Soon — v0.6.0):

```json
{
  "mcpServers": {
    "assay": { "command": "assay", "args": ["mcp-serve"] }
  }
}
```

### Cline / OpenCode

Same pattern — `assay mcp-serve` exposes all modules as MCP tools (v0.6.0).

Today: use `assay context <query>` from terminal and paste output into agent context.

## MCP-Serve Vision (v0.6.0)

`assay mcp-serve` will expose all 51 modules (34 stdlib + 17 builtins) as MCP tools over stdio/SSE transport:

- Each stdlib module becomes an MCP tool (e.g., `grafana_health`, `k8s_pods`)
- Each builtin becomes an MCP tool (e.g., `http_get`, `crypto_jwt_sign`)
- Agents call tools directly — no Lua scripting required for simple queries
- Lua scripting still available for complex multi-step workflows

Until v0.6.0: use `assay context <query>` + paste into agent context window. See
https://assay.rs/agent-guides.html for complete integration examples.