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
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"name": "lua-agent-builder",
"source": "./plugins/lua-agent-builder",
"description": "Build, test, and deploy Lua AI agents from inside Claude Code",
"version": "1.17.2",
"version": "1.18.0",
"homepage": "https://github.com/lua-ai-global/claude-code-lua-plugin#readme",
"repository": "https://github.com/lua-ai-global/claude-code-lua-plugin.git",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion plugins/lua-agent-builder/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "lua-agent-builder",
"version": "1.17.2",
"version": "1.18.0",
"description": "Build, test, and deploy Lua AI agents from inside Claude Code",
"author": {
"name": "Lua AI",
Expand Down
7 changes: 7 additions & 0 deletions plugins/lua-agent-builder/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,13 @@

All notable changes to the `lua-agent-builder` plugin. Versions follow the tag `release-prod.yml` cuts from `package.json` (`v<version>`). lua-cli is a TypeScript SDK/CLI; it is unrelated to the Lua programming language.

## 1.18.0 — 2026-10-29

### New

- **App Viewers in web apps** (`lib/knowledge/web-apps.md` §3, lua-cli 3.64.0+): `auth.accessLevel` (`'full'`, `'view_only'`, `'use'`), and `defineWebApp({ viewers: { write, chat, mode } })`, what an app asks for the people who sign in by phone or email to use only that app. With `write: true` they reach every write route as `'use'` with empty roles and scopes, so the builder is told, with an example, that each write route checks the record is the caller's (their `userId`, or their scope in the app's own roster) and answers 404 otherwise.
- **`/lua-audit`** (`agents/lua-auditor.md`): lists `viewers` and `auth.accessLevel` branches under custom access control, and a new finding, **App Viewer writes without ownership** (14), always HIGH: `viewers.write: true` with a write route that changes a record without checking it is the caller's.

## 1.17.2 — 2026-10-29

### Fixed
Expand Down
4 changes: 3 additions & 1 deletion plugins/lua-agent-builder/agents/lua-auditor.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ Skip `node_modules/`, build output, images and fonts.

The **gateway** decides who may open the app at all: every request comes from a signed-in person of the organization with access to the agent, so `auth` is never anonymous. **Custom access control** is any condition in the app's own code that further decides who may see or do something. Find every one:

- route or `fetch` handlers branching on `auth.*` (`roles`, `scopes`, `email`, `userId`, `orgId`, `name`);
- route or `fetch` handlers branching on `auth.*` (`roles`, `scopes`, `email`, `userId`, `orgId`, `name`, `accessLevel`);
- what the app asks for its App Viewers in `defineWebApp({ viewers })` (`write`, `chat`, `mode`): with `write: true` App Viewers (people outside the agent's team) reach every write route as `accessLevel: 'use'`;
- allow/deny lists — a JSON or data file, a constant array, an `env()` value, or `Data` rows (e.g. an `admins` collection) checked against the viewer;
- per-record rules — "only the creator may edit", "only rows of my shop";
- page code that shows, hides or disables UI by who is viewing (`lua.api('GET', '/me')` results, role or email checks in components or page routes).
Expand All @@ -44,6 +45,7 @@ For each: `file:line`, the route or page, the rule in plain words ("only people
11. **SSRF** — outbound `fetch` to a URL or host taken from the request.
12. **Untestable role gate** — a check on `auth.roles` / `auth.scopes`, which are empty under `lua test webapp`, so it cannot be verified locally and may deny everyone (or no one) once deployed.
13. **`Data.shared` writes** — a route writing organization-wide data, reachable by every viewer of this app.
14. **App Viewer writes without ownership** — `viewers.write: true` and a `POST`/`PUT`/`PATCH`/`DELETE` route that changes a record without checking it belongs to `auth.userId` or their scope in the app's roster (and answering 404 otherwise): every App Viewer can change anyone's records by changing an id. Always **HIGH**.

Then anything else that lets a viewer see or change what they should not.

Expand Down
18 changes: 17 additions & 1 deletion plugins/lua-agent-builder/lib/knowledge/web-apps.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,23 @@ export default defineWebApp({

- **Keys** are `'<METHOD> /<path>'`, methods `GET POST PUT PATCH DELETE`, exactly one space, `:param` segments. A bad key, a route without `handler`, a nav item whose `path` doesn't start with `/` or lacks `label`, a bad name, a missing `pages.root`, or **no routes and no `fetch`** all fail at `lua compile` with a `defineWebApp(<name>): …` message.
- **`ctx`**: `params`, `query` (repeated key → array), `body`, `headers` (lower-cased, **allow-listed**: `accept`, `accept-language`, `content-type`, `if-none-match`, `idempotency-key`, `x-lua-*` — cookies never arrive), `method`, `path`, `route`, `auth`, `app` (`{ id, name, version, deployment, origin, preview }`), `request` (a Web `Request`).
- **`auth`**: `{ userId, orgId, agentId, subjectType: 'user', credentialType: 'firstPartySession', sessionId?, roles, scopes, name?, email?, expiresAt? }` — always a signed-in person, never an API key, never anonymous. Use `auth.userId` for "who did this" fields. **Locally `roles` and `scopes` are empty** (the gateway decides access) — never gate logic on them without a fallback you can test.
- **`auth`**: `{ userId, orgId, agentId, subjectType: 'user', credentialType: 'firstPartySession', sessionId?, roles, scopes, name?, email?, expiresAt?, accessLevel? }` — always a signed-in person, never an API key, never anonymous. Use `auth.userId` for "who did this" fields. **Locally `roles` and `scopes` are empty** (the gateway decides access) — never gate logic on them without a fallback you can test. `accessLevel`: `'full'` (can read the agent: every route), `'view_only'` (a view shared with them, or org-wide: `GET` routes only, every other method answers 403 `VIEW_ONLY` before your handler) or `'use'` (an App Viewer of an app that asks for writes, below: every route, with empty `roles` and `scopes`).
- **App Viewers** (⏳ lua-cli 3.64.0+): people who sign in by phone or email to use only this app. They only read unless the app asks for more in `defineWebApp`: `viewers: { write?: boolean, chat?: boolean, mode?: 'people' | 'app' }`. `write: true` lets them call your `POST`/`PUT`/`PATCH`/`DELETE` routes as `accessLevel: 'use'`; `chat` asks for the agent pane (honoured once the platform ships App Viewer chat); `mode` says how they join (`'people'`, the default: an admin invites them; `'app'`: your code adds and removes them, ⏳ not available yet). It is an ask: the deploy shows what it opens and the workspace can turn writes or chat off. `viewers` must be a compile-time object of those keys (no spread, no computed key), or `lua compile` refuses it. An older lua-cli drops it silently. **With `write: true`, every write route must check the record is the caller's** — `auth.userId`, or their scope in your own roster collection (person → territory, team, shop) — **and answer 404 when it is not**; the platform cannot tell whose record it is:

```ts
'POST /visits/:id/reason': route({
params: z.object({ id: z.string() }),
body: z.object({ reason: z.string().max(200) }),
handler: async ({ params, body, auth }) => {
const visit = await Data.getEntry('visits', params.id).catch(() => null); // a missing id throws
// inScopeOf: your own check against your roster collection (is this area theirs?)
const mine = visit && (visit.data.repUserId === auth.userId || (await inScopeOf(auth.userId, visit.data.area)));
if (!mine) return json({ code: 'NOT_FOUND', message: 'No such visit' }, 404); // never 403: do not confirm it exists
await Data.patch('visits', visit.id, { set: { reason: body.reason, by: auth.userId } });
return json(undefined, 204);
},
}),
```
- **Schemas** (`params`, `query`, `body`) are Zod; a failure answers **400 `VALIDATION`** before the handler runs. `responses` is for the typed client only, not enforced. `Data.get(collection, filter?, page?, limit?)` takes the filter itself as the second argument (`{ status: 'open' }`), not `{ filter: … }`. On lua-cli **3.49.0 and earlier** a field with `.default()` is typed `T | undefined` in the handler and a `.transform()` schema is refused by `tsc`; write `query.status ?? 'open'` there. The release after 3.49.0 types the schema output.
- **Returns**: any JSON value → 200; `undefined` → 204; a `Response` as-is (`json(data, status, headers)` builds one). Response headers other than `content-type`, `cache-control`, `etag`, `location`, `content-disposition`, `content-language`, `x-lua-*` are dropped; **`set-cookie` is refused** — there are no cookie sessions, the client holds the session.
- **Platform errors** all carry `{ code, message, issues?, executionId? }` with `code` one of `VALIDATION NOT_FOUND METHOD_NOT_ALLOWED HANDLER_ERROR FORBIDDEN UNAUTHENTICATED APP_DISABLED CONCURRENCY_LIMIT WALL_TIMEOUT RESPONSE_TOO_LARGE PLATFORM_ERROR`. A handler's own error body passes through untouched — return `json({ code: 'NOT_FOUND', message }, 404)` rather than throwing (a throw surfaces as `HANDLER_ERROR`).
Expand Down
6 changes: 3 additions & 3 deletions plugins/lua-agent-builder/mcp/lua-platform/dist/server.js

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions plugins/lua-agent-builder/mcp/lua-platform/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion plugins/lua-agent-builder/mcp/lua-platform/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@lua/claude-plugin-mcp",
"version": "1.17.2",
"version": "1.18.0",
"description": "Read-only MCP server for the lua-agent-builder Claude Code plugin",
"type": "module",
"main": "dist/server.js",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ export async function apiRequest(path, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'X-Lua-Client': 'claude-plugin/1.17.2',
'X-Lua-Client': 'claude-plugin/1.18.0',
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
Expand Down
6 changes: 3 additions & 3 deletions plugins/lua-agent-builder/mcp/lua-platform/src/server.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as tools from './tools/index.mjs';
const TOOL_REGISTRY = Object.values(tools);

const server = new Server(
{ name: 'lua-platform', version: '1.17.2' },
{ name: 'lua-platform', version: '1.18.0' },
{ capabilities: { tools: {} } }
);

Expand Down Expand Up @@ -46,7 +46,7 @@ process.on('uncaughtException', (err) => {
kind: 'uncaughtException',
message: err?.message,
stack: err?.stack,
plugin_version: '1.17.2',
plugin_version: '1.18.0',
lua_cli_version: process.env.LUA_CLI_VERSION ?? null,
platform: process.platform,
ts: new Date().toISOString(),
Expand All @@ -59,7 +59,7 @@ process.on('unhandledRejection', (reason) => {
kind: 'unhandledRejection',
reason: String(reason),
stack: /** @type {any} */ (reason)?.stack,
plugin_version: '1.17.2',
plugin_version: '1.18.0',
lua_cli_version: process.env.LUA_CLI_VERSION ?? null,
platform: process.platform,
ts: new Date().toISOString(),
Expand Down
2 changes: 1 addition & 1 deletion plugins/lua-agent-builder/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "claude-code-lua-plugin",
"version": "1.17.2",
"version": "1.18.0",
"description": "Plugin assets for the lua-agent-builder Claude Code plugin",
"private": true,
"type": "module",
Expand Down
Loading