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 .codex-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
"displayName": "Lua Agent Builder",
"description": "Build, test, and deploy Lua AI agents (heylua.ai) from inside Codex CLI — 14 skills, 5 specialised subagents, MCP-first integrations to 250+ third-party services via Unified.to, sandbox-then-prod deploys with safety gates.",
"shortDescription": "Build, test, and deploy Lua AI agents directly from Codex CLI.",
"version": "1.0.0",
"version": "1.1.0",
"author": {
"name": "Lua AI",
"email": "support@heylua.ai",
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ To uninstall: `node scripts/install.mjs --uninstall` (removes the plugin AND the

To update: `cd ~/codex-lua-plugin && git pull && node scripts/install.mjs` — idempotent; Codex re-reads the manifest and updates components.

In Codex, type `/` to see the `lua-*` skills appear in autocomplete. Run `/lua-doctor` first to verify the environment, then `/lua-auth` to authenticate.
In Codex, type `/` to see the `lua-*` skills in autocomplete. Run `/lua-doctor` to verify the environment, then `/lua-auth`. A new login uses `lua auth configure` in a private terminal and requires lua-cli 3.28.0 or newer.

## What's inside

Expand Down Expand Up @@ -89,7 +89,7 @@ The plugin enforces the same gates as the Claude Code and Cursor versions:

- **§3.3 deploy gate** — bare `lua deploy` is denied by `hooks/before-shell-execution.mjs` unless prefixed with `LUA_DEPLOY_CONFIRMED=1` (the `/lua-deploy` skill sets this after walking the user through the gated 5-step ship sequence).
- **`--auto-deploy` block** — denied for any command containing `--auto-deploy`.
- **Credential isolation** — `lua auth key*` is denied to prevent the API key from being printed into the chat transcript.
- **Credential isolation** — model-run `lua auth configure` and `lua auth key*` commands are denied. Account details, OTPs, and credentials stay in a private terminal.
- **Single-permission contract** — preserved in the skill bodies.

See [`SECURITY.md`](./SECURITY.md) for disclosure path.
Expand Down
2 changes: 1 addition & 1 deletion SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ The plugin enforces several safety contracts. Bypasses count as security issues.
|---|---|
| **§3.3 deploy gate**: bare `lua deploy` is denied | `hooks/before-shell-execution.mjs` (umbrella) + `hooks/confirm-deploy.mjs` (dedicated) |
| **§3.3 auto-deploy block**: `--auto-deploy` is denied | `hooks/before-shell-execution.mjs` + `hooks/block-auto-deploy.mjs` |
| **Credential isolation**: API key never enters chat transcript | `hooks/before-shell-execution.mjs` denies `lua auth key*` invocations + `skills/lua-doctor/SKILL.md` Step 4 uses an authenticated metadata probe (`lua agents --json --ci`), not a key-printing command |
| **Credential isolation**: account details, OTPs, and credentials never enter the Codex conversation | `skills/lua-auth/SKILL.md` sends new login to a private terminal; `hooks/before-shell-execution.mjs` denies model-run `lua auth configure` and `lua auth key*` commands |
| **§3.7 single-permission contract**: each skill asks at most one prompt per invocation | Convention enforced in skill bodies |

If you find a way to bypass any of these without an explicit user prompt, please report.
Expand Down
4 changes: 2 additions & 2 deletions docs/TESTERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ If any of those four checks fails, see [Common gotchas](#5-common-gotchas) below
Once the sanity check passes, walk a full agent build to verify the integration works in your own environment:

```
/lua-auth # email+OTP, takes ~30s
/lua-auth # private typed login through lua-cli 3.28.0+
/lua-doctor # 5-step env diagnostic
/lua-architect Build me an agent that summarises my Stripe refund history
# produces a structured plan
Expand Down Expand Up @@ -81,7 +81,7 @@ This is a fresh port from the Cursor and Claude Code plugins, so the things most
| `node scripts/install.mjs` says "Codex CLI not on PATH" | Codex isn't installed | `npm install -g @openai/codex` |
| `node scripts/install.mjs` says "MCP server bundle not found" | Build step skipped | `cd mcp/lua-platform && npm ci && npm run build` then re-run install |
| `/lua-` doesn't autocomplete | Plugin installed but Codex needs reload | Fully exit and reopen Codex |
| MCP tools missing | `LUA_API_KEY` not set | `/lua-auth` or `export LUA_API_KEY=lk_...` |
| MCP tools missing | No working credential | Run `/lua-auth`. Existing automation can keep `LUA_API_KEY='<existing-credential>'`. |
| Every shell command rejected with `DEPLOY_DENIED_BARE` | Stale install (pre-fix bug from earlier ports) | `git pull && node scripts/install.mjs` |
| `lua agents --json --ci` returns nothing in `/lua-doctor` | Auth state out of sync | Re-run `/lua-auth` |

Expand Down
11 changes: 7 additions & 4 deletions docs/USER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,13 @@ If any of this fails, see the [Troubleshooting](#7-troubleshooting) section belo
/lua-auth
```

The skill asks how you want to authenticate:
The skill first runs `lua agents --json --ci`. If a credential from `LUA_API_KEY`, `~/.lua-cli/credentials`, or the project's `.env` file works, the plugin leaves it unchanged. Existing non-dotted legacy keys remain supported.

- **Email + OTP** (recommended for first-time users) — enter your email; you'll receive a 6-digit code; enter it back. The CLI generates and stores an API key for you.
- **Existing API key** — paste it. (The plugin's `before-shell-execution.mjs` hook denies `lua auth key*` invocations specifically to prevent your stored key from being printed back into the chat transcript.)
For a new login, install lua-cli 3.28.0 or newer. Open a terminal outside Codex and run `lua auth configure`, then choose the email option. The CLI handles your email and OTP, then requires an organization, one or more exact agents, and an assignable role. Builder is the default role. The CLI writes the typed personal credential to `~/.lua-cli/credentials` with mode `0600`.

If you already have a credential that is not configured, choose the existing-key option in the private terminal. Existing automation can keep using `LUA_API_KEY` or `.env`.

Never paste an email, an OTP, or a credential into the Codex conversation. The safety hook denies model-run `lua auth configure` and `lua auth key*` commands.

Verify with:

Expand Down Expand Up @@ -206,7 +209,7 @@ The `/lua-deploy` skill sets this env var automatically after walking you throug
| `node scripts/install.mjs` says "Codex CLI not on PATH" | Codex CLI not installed | `npm install -g @openai/codex`, then re-run install |
| `node scripts/install.mjs` says "MCP server bundle not found" | Build step skipped | `cd mcp/lua-platform && npm ci && npm run build`, then re-run install |
| `/lua-` doesn't autocomplete in Codex | Plugin install reported success but Codex didn't reload | Restart Codex completely (close all sessions) |
| MCP tools missing from agent | `LUA_API_KEY` not set | Run `/lua-auth` to set credentials, or `export LUA_API_KEY=lk_...` |
| MCP tools missing from agent | No working credential | Run `/lua-auth`. Existing automation can keep using `LUA_API_KEY='<existing-credential>'`. |
| **All shell commands rejected with `DEPLOY_DENIED_BARE`** | Stale install (pre-bug-fix `confirm-deploy.mjs`) | `cd ~/codex-lua-plugin && git pull && node scripts/install.mjs` |
| Architect proposes custom tools that duplicate an integration's API | Rare, but if seen: the architect didn't run the MCP discovery step | Manually attach `@integrations`, then re-prompt with: "Verify the MCP surface for `<integration>` before listing custom tools" |
| Want to start fresh | n/a | `node scripts/install.mjs --uninstall && node scripts/install.mjs` — clean re-install |
Expand Down
20 changes: 16 additions & 4 deletions hooks/before-shell-execution.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
//
// Coverage:
// 1. `lua auth key*` — would print API key to stdout (transcript leak)
// 2. `--auto-deploy` — bypasses the §3.3 confirmation contract
// 3. bare `lua deploy` — must be prefixed with LUA_DEPLOY_CONFIRMED=1
// 2. `lua auth configure` — account and credential input belongs in a private terminal
// 3. `--auto-deploy` — bypasses the §3.3 confirmation contract
// 4. bare `lua deploy` — must be prefixed with LUA_DEPLOY_CONFIRMED=1
// (set by confirm-deploy.mjs after the user OKs the
// 5-step gated ship via /lua-deploy)
//
Expand Down Expand Up @@ -34,7 +35,18 @@ export function decide(input) {
};
}

// 2. --auto-deploy — never. Bypasses the §3.3 confirmation contract.
// 2. Interactive auth belongs in a private terminal. Even the flagless
// command prompts for account details and an OTP.
if (/\blua\s+auth\s+configure\b/.test(command)) {
return {
block: true,
reason:
'AUTH_INPUT_DENIED: Run `lua auth configure` yourself in a private terminal. ' +
'Do not enter your email, OTP, or credential in the Codex conversation.',
};
}

// 3. --auto-deploy — never. Bypasses the §3.3 confirmation contract.
if (/--auto-deploy\b/.test(command)) {
return {
block: true,
Expand All @@ -45,7 +57,7 @@ export function decide(input) {
};
}

// 3. Bare `lua deploy` — only allowed when LUA_DEPLOY_CONFIRMED=1 is set
// 4. Bare `lua deploy` — only allowed when LUA_DEPLOY_CONFIRMED=1 is set
// inline (which the confirm-deploy.mjs hook does after the user OKs the
// /lua-deploy gated flow).
if (/\blua\s+deploy\b/.test(command) && !/\bLUA_DEPLOY_CONFIRMED=1\b/.test(command)) {
Expand Down
4 changes: 2 additions & 2 deletions hooks/check-lua-auth.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ export function decide(versionResult, authResult) {

return {
warn:
'🔐 Lua plugin loaded but you\'re not authenticated. Run `/lua-auth` to set up ' +
'pick `Email + OTP` (we\'ll send a 6-digit code to your inbox) or paste an existing API key. ' +
'🔐 Lua plugin loaded but you\'re not authenticated. Run `/lua-auth` to set up a typed credential. ' +
'The setup keeps your email, OTP, and credential in a private terminal. ' +
'Until then, every `/lua-*` slash that needs the platform will fail.',
};
}
Expand Down
4 changes: 1 addition & 3 deletions lib/permissions-template.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,6 @@
"Bash(lua agents *)",
"Bash(lua skills view --ci*)",
"Bash(lua skills versions --ci*)",
"Bash(lua auth configure --api-key * --ci)",
"Bash(lua auth configure --email * --ci)",
"Bash(lua auth configure --email * --otp * --ci)",
"Bash(lua push * --ci --force*)",
"Bash(LUA_DEPLOY_CONFIRMED=1 lua deploy*)",
"Bash(env LUA_DEPLOY_CONFIRMED=1 lua deploy*)",
Expand All @@ -44,6 +41,7 @@
"Bash(lua deploy*)",
"Bash(lua * --auto-deploy*)",
"Bash(lua push * --auto-deploy*)",
"Bash(lua auth configure*)",
"Bash(lua auth key*)"
]
}
Expand Down
6 changes: 3 additions & 3 deletions mcp/lua-platform/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @lua/claude-plugin-mcp

Read-only MCP server for the lua-agent-builder Claude Code plugin.
Read-only MCP server bundled with the Lua Agent Builder plugin for Codex.

Per tech spec §3.4 / §6.3: this server exposes 6 read-only tools that let
Claude Code query lua-platform state mid-conversation without using slash
Expand Down Expand Up @@ -28,11 +28,11 @@ plugin assets repo at `mcp/lua-platform/dist/server.js` per §3.2.

## Running standalone

The server speaks MCP over stdio. Normally invoked by Claude Code via
The server speaks MCP over stdio. Codex normally invokes it through
`.mcp.json`; for manual testing:

```bash
LUA_API_KEY=lk_... node dist/server.js
LUA_API_KEY='<existing-credential>' node dist/server.js
```

## Architecture
Expand Down
4 changes: 2 additions & 2 deletions 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 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.0.0",
"version": "1.1.0",
"description": "Read-only MCP server for the lua-agent-builder Claude Code plugin",
"type": "module",
"main": "dist/server.js",
Expand Down
1 change: 1 addition & 0 deletions mcp/lua-platform/src/api-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export async function apiRequest(path, {
headers: {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
'X-Lua-Client': 'codex-plugin/1.1.0',
},
body: body ? JSON.stringify(body) : undefined,
signal: controller.signal,
Expand Down
6 changes: 3 additions & 3 deletions 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.0.0' },
{ name: 'lua-platform', version: '1.1.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.0.0',
plugin_version: '1.1.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: reason?.stack,
plugin_version: '1.0.0',
plugin_version: '1.1.0',
lua_cli_version: process.env.LUA_CLI_VERSION ?? null,
platform: process.platform,
ts: new Date().toISOString(),
Expand Down
29 changes: 29 additions & 0 deletions mcp/lua-platform/tests/api-client.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,21 @@
// 401 / 403 / generic-error paths, and query-string handling.

import { describe, test, expect, beforeEach, afterEach } from '@jest/globals';
import { readFileSync, readdirSync } from 'node:fs';
import { join, relative } from 'node:path';
import { fileURLToPath } from 'node:url';
import { apiRequest } from '../src/api-client.mjs';

const SOURCE_DIRECTORY = fileURLToPath(new URL('../src/', import.meta.url));
const PLUGIN_PACKAGE = JSON.parse(readFileSync(new URL('../../../package.json', import.meta.url), 'utf8'));

function sourceFiles(directory) {
return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => {
const path = join(directory, entry.name);
return entry.isDirectory() ? sourceFiles(path) : [path];
});
}

function mockFetch(scripted) {
const calls = [];
const fn = async (url, init) => {
Expand Down Expand Up @@ -51,6 +64,22 @@ describe('apiRequest', () => {
expect(fetchFn.calls[0].init.headers['Content-Type']).toBe('application/json');
});

test('identifies direct requests as the versioned Codex plugin', async () => {
const fetchFn = mockFetch(jsonResponse({ ok: true }));
await apiRequest('/agents', { fetchFn });
expect(fetchFn.calls[0].init.headers['X-Lua-Client']).toBe(`codex-plugin/${PLUGIN_PACKAGE.version}`);
});

test('keeps every direct Lua API call behind the identified wrapper', () => {
const directCallers = sourceFiles(SOURCE_DIRECTORY)
.filter((path) => path.endsWith('.mjs'))
.filter((path) => /\b(?:fetch|fetchFn)\s*\(/.test(readFileSync(path, 'utf8')))
.map((path) => relative(SOURCE_DIRECTORY, path))
.sort();

expect(directCallers).toEqual(['api-client.mjs']);
});

test('uses LUA_API_URL env override when set', async () => {
process.env.LUA_API_URL = 'https://api-staging.heylua.ai';
const fetchFn = mockFetch(jsonResponse({ ok: true }));
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

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

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"name": "codex-lua-plugin",
"version": "1.0.0",
"version": "1.1.0",
"description": "Codex CLI plugin for building, testing, and deploying Lua AI agents (heylua.ai)",
"private": true,
"type": "module",
"engines": {
"node": ">=18.0.0"
},
"scripts": {
"lint": "eslint . && node scripts/lint-paths.mjs && node scripts/lint-mcp-refs.mjs && node scripts/lint-pinned-version.mjs && node scripts/lint-knowledge-commands.mjs && node scripts/lint-monorepo-paths.mjs && node scripts/lint-log-field-names.mjs && node scripts/lint-chat-thread-flag.mjs && node scripts/lint-cli-flags.mjs && node scripts/lint-codex-manifest.mjs && node scripts/lint-codex-mcp-config.mjs && node scripts/lint-codex-no-claude-root.mjs",
"lint": "eslint . && node scripts/lint-paths.mjs && node scripts/lint-mcp-refs.mjs && node scripts/lint-pinned-version.mjs && node scripts/lint-knowledge-commands.mjs && node scripts/lint-monorepo-paths.mjs && node scripts/lint-log-field-names.mjs && node scripts/lint-chat-thread-flag.mjs && node scripts/lint-cli-flags.mjs && node scripts/lint-codex-manifest.mjs && node scripts/lint-codex-mcp-config.mjs && node scripts/lint-codex-no-claude-root.mjs && node scripts/lint-release-version.mjs",
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
"test:coverage": "node --experimental-vm-modules node_modules/jest/bin/jest.js --coverage && node scripts/check-coverage.mjs",
"test:watch": "node --experimental-vm-modules node_modules/jest/bin/jest.js --watch"
Expand Down
36 changes: 24 additions & 12 deletions scripts/lint-cli-flags.mjs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#!/usr/bin/env node
// Denylist of known-wrong lua-cli flag combinations that have shipped to the
// plugin in the past. Standalone-repo friendly: doesn't need lua-cli source
// Denylist of known-wrong or unsafe lua-cli command references that have shipped
// in the plugin. Standalone-repo friendly: doesn't need lua-cli source
// (unlike lint-knowledge-commands.mjs, which is skipped without it).
//
// History:
Expand All @@ -20,9 +20,13 @@ const DENY = [
// Pattern → reason
{ pattern: 'lua sync --pull', reason: 'real flag is `lua sync --accept` (server → local)' },
{ pattern: 'sync --pull', reason: 'permission rule must allow `--accept`, not `--pull`' },
{ pattern: 'lua auth configure --email', reason: 'email and OTP input must stay in a private terminal', authFlow: true },
{ pattern: 'lua auth configure --api-key', reason: 'credentials must stay out of the model conversation', authFlow: true },
];

const SCAN_DIRS = ['commands', 'agents', 'hooks', 'lib', 'scripts', 'mcp'];
const SCAN_DIRS = ['skills', 'agents', 'hooks', 'lib', 'scripts', 'mcp'];
const AUTH_DOC_DIRS = ['docs'];
const AUTH_DOC_FILES = ['README.md', 'SECURITY.md'];
const SCAN_EXT = new Set(['.md', '.json', '.mjs', '.js', '.ts']);

let failed = false;
Expand All @@ -40,23 +44,31 @@ async function* walk(dir) {
}

let scanned = 0;
async function scan(path, { authOnly = false } = {}) {
const content = await readFile(path, 'utf8');
for (const { pattern, reason, authFlow } of DENY) {
if (authOnly && !authFlow) continue;
if (content.includes(pattern)) {
fail(`${path}: contains denylisted CLI reference \`${pattern}\` — ${reason}`);
}
}
scanned++;
}

for (const dir of SCAN_DIRS) {
for await (const path of walk(dir)) {
// Don't lint this script itself — it has to mention the deny patterns.
if (path.endsWith('lint-cli-flags.mjs')) continue;
const content = await readFile(path, 'utf8');
for (const { pattern, reason } of DENY) {
if (content.includes(pattern)) {
fail(`${path}: contains denylisted CLI reference \`${pattern}\` — ${reason}`);
}
}
scanned++;
await scan(path);
}
}
for (const dir of AUTH_DOC_DIRS) {
for await (const path of walk(dir)) await scan(path, { authOnly: true });
}
for (const path of AUTH_DOC_FILES) await scan(path, { authOnly: true });

if (failed) {
console.error(`\nFix the references above. These flags do not exist in lua-cli; shipping them ` +
`breaks the user's first attempt to use the slash/agent that referenced them.`);
console.error('\nFix the references above. These commands are wrong or unsafe in a model-run plugin flow.');
process.exit(1);
}
console.log(`✓ CLI flag denylist: ${scanned} file(s) scanned, no known-wrong flags found.`);
Loading
Loading