diff --git a/.codex-plugin/plugin.json b/.codex-plugin/plugin.json index 7c2a144..e86452a 100644 --- a/.codex-plugin/plugin.json +++ b/.codex-plugin/plugin.json @@ -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", diff --git a/README.md b/README.md index 77c29ac..1ec485a 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. diff --git a/SECURITY.md b/SECURITY.md index 51a5d2c..58dd4ff 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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. diff --git a/docs/TESTERS.md b/docs/TESTERS.md index e98e6a8..bc23a9b 100644 --- a/docs/TESTERS.md +++ b/docs/TESTERS.md @@ -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 @@ -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=''`. | | 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` | diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index d83d560..f8fe278 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -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: @@ -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=''`. | | **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 `` before listing custom tools" | | Want to start fresh | n/a | `node scripts/install.mjs --uninstall && node scripts/install.mjs` — clean re-install | diff --git a/hooks/before-shell-execution.mjs b/hooks/before-shell-execution.mjs index 4e650ee..53bba08 100755 --- a/hooks/before-shell-execution.mjs +++ b/hooks/before-shell-execution.mjs @@ -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) // @@ -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, @@ -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)) { diff --git a/hooks/check-lua-auth.mjs b/hooks/check-lua-auth.mjs index 3daaabd..cfcf474 100644 --- a/hooks/check-lua-auth.mjs +++ b/hooks/check-lua-auth.mjs @@ -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.', }; } diff --git a/lib/permissions-template.json b/lib/permissions-template.json index 89d4331..af143c4 100644 --- a/lib/permissions-template.json +++ b/lib/permissions-template.json @@ -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*)", @@ -44,6 +41,7 @@ "Bash(lua deploy*)", "Bash(lua * --auto-deploy*)", "Bash(lua push * --auto-deploy*)", + "Bash(lua auth configure*)", "Bash(lua auth key*)" ] } diff --git a/mcp/lua-platform/README.md b/mcp/lua-platform/README.md index eff429e..615e0c4 100644 --- a/mcp/lua-platform/README.md +++ b/mcp/lua-platform/README.md @@ -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 @@ -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='' node dist/server.js ``` ## Architecture diff --git a/mcp/lua-platform/package-lock.json b/mcp/lua-platform/package-lock.json index 69d7df3..681659c 100644 --- a/mcp/lua-platform/package-lock.json +++ b/mcp/lua-platform/package-lock.json @@ -1,12 +1,12 @@ { "name": "@lua/claude-plugin-mcp", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@lua/claude-plugin-mcp", - "version": "1.0.0", + "version": "1.1.0", "dependencies": { "@modelcontextprotocol/sdk": "~0.6.0" }, diff --git a/mcp/lua-platform/package.json b/mcp/lua-platform/package.json index fc1596e..ec5cd3b 100644 --- a/mcp/lua-platform/package.json +++ b/mcp/lua-platform/package.json @@ -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", diff --git a/mcp/lua-platform/src/api-client.mjs b/mcp/lua-platform/src/api-client.mjs index 930b817..1df8a8c 100644 --- a/mcp/lua-platform/src/api-client.mjs +++ b/mcp/lua-platform/src/api-client.mjs @@ -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, diff --git a/mcp/lua-platform/src/server.mjs b/mcp/lua-platform/src/server.mjs index 20dd49a..318ae13 100644 --- a/mcp/lua-platform/src/server.mjs +++ b/mcp/lua-platform/src/server.mjs @@ -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: {} } } ); @@ -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(), @@ -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(), diff --git a/mcp/lua-platform/tests/api-client.test.mjs b/mcp/lua-platform/tests/api-client.test.mjs index 98b663b..65844fb 100644 --- a/mcp/lua-platform/tests/api-client.test.mjs +++ b/mcp/lua-platform/tests/api-client.test.mjs @@ -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) => { @@ -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 })); diff --git a/package-lock.json b/package-lock.json index 66e43b0..b1cfdb4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "codex-lua-plugin", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "codex-lua-plugin", - "version": "1.0.0", + "version": "1.1.0", "devDependencies": { "@types/node": "^20.11.0", "eslint": "^8.57.0", diff --git a/package.json b/package.json index 0d5e123..13f9c9b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "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", @@ -8,7 +8,7 @@ "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" diff --git a/scripts/lint-cli-flags.mjs b/scripts/lint-cli-flags.mjs index 06eeed4..489caf6 100644 --- a/scripts/lint-cli-flags.mjs +++ b/scripts/lint-cli-flags.mjs @@ -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: @@ -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; @@ -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.`); diff --git a/scripts/lint-release-version.mjs b/scripts/lint-release-version.mjs new file mode 100644 index 0000000..09a2caf --- /dev/null +++ b/scripts/lint-release-version.mjs @@ -0,0 +1,32 @@ +#!/usr/bin/env node + +import { readFile } from 'node:fs/promises'; + +const readJson = async (path) => JSON.parse(await readFile(path, 'utf8')); +const releaseVersion = (await readJson('package.json')).version; +let failed = false; + +function fail(message) { + console.error(`✗ ${message}`); + failed = true; +} + +for (const [path, version] of [ + ['.codex-plugin/plugin.json', (await readJson('.codex-plugin/plugin.json')).version], + ['mcp/lua-platform/package.json', (await readJson('mcp/lua-platform/package.json')).version], +]) { + if (version !== releaseVersion) fail(`${path} has version ${version}; expected ${releaseVersion}.`); +} + +for (const [path, expected, count = 1] of [ + ['mcp/lua-platform/src/api-client.mjs', `'X-Lua-Client': 'codex-plugin/${releaseVersion}'`], + ['mcp/lua-platform/src/server.mjs', `{ name: 'lua-platform', version: '${releaseVersion}' }`], + ['mcp/lua-platform/src/server.mjs', `plugin_version: '${releaseVersion}'`, 2], +]) { + const source = await readFile(path, 'utf8'); + const matches = source.split(expected).length - 1; + if (matches !== count) fail(`${path} must contain ${count} occurrence(s) of ${expected}.`); +} + +if (failed) process.exit(1); +console.log(`✓ Release version ${releaseVersion} is consistent across plugin and MCP metadata.`); diff --git a/skills/lua-auth/SKILL.md b/skills/lua-auth/SKILL.md index dad4fd7..02f277d 100644 --- a/skills/lua-auth/SKILL.md +++ b/skills/lua-auth/SKILL.md @@ -1,46 +1,45 @@ --- name: lua-auth -description: Authenticate with Lua via email+OTP or paste an existing API key. Stores credentials in ~/.lua-cli/credentials. Run this once after installing the plugin. +description: Authenticate with Lua through lua-cli's private interactive login. Existing credentials from LUA_API_KEY, .env, or the credentials file remain valid. --- You are `/lua-auth`. The user wants to authenticate with the Lua platform. -## Step 1 — pick the auth path +## Step 1: keep a working credential -ask-user-question **once**: +Run `Bash(lua agents --json --ci)`. If the command succeeds, summarize the accessible organizations and agents, then stop. Do not replace, rotate, print, or rewrite the credential that worked. -- "How do you want to authenticate?" (options: `Email + OTP`, `Paste API key`, `Cancel`) +The probe can use `LUA_API_KEY`, `~/.lua-cli/credentials`, or the project's `.env` file. Existing non-dotted legacy keys remain supported. -If the user picks Cancel, print "Auth cancelled." and stop. +## Step 2: choose without collecting a secret -## Step 2 — run the chosen flow +ask-user-question once: "Do you need a new Lua credential, or do you already have one?" Use the options `New login`, `Use an existing credential privately`, and `Cancel`. -The follow-up "what's your email/OTP/key?" prompts are **information collection**, exempt from the §3.7 single-permission contract per the permission-vs-information distinction. +If the user has an existing credential, tell them to use `lua auth configure` in a private terminal and choose the existing-key option. They can also set `LUA_API_KEY` or a project `.env` value outside this conversation. Do not require an upgrade, rotation, or new login. Continue to Step 4 after they confirm. -### Path A — Email + OTP +For a new login, run `Bash(lua --version)`. New typed issuance requires `lua-cli` 3.28.0 or newer. If the installed version is older, tell the user to run `/lua-update`, then stop. Do not fall back to the old non-interactive OTP commands. -1. Ask: "Email for your Lua account?" (free-text — don't validate format; lua-cli does that). Store as ``. -2. Run `Bash(lua auth configure --email --ci)`. This sends a 6-digit OTP to the user's inbox. The command prints a confirmation; surface that to the user verbatim so they know to check their email. -3. Ask: "Enter the 6-digit code from the email." (free-text). Store as ``. -4. Run `Bash(lua auth configure --email --otp --ci)`. This verifies the OTP and writes the API key to `~/.lua-cli/credentials` (mode 0600, plain text per `lua-cli/src/services/auth.ts:65-67`). +## Step 3: hand off secret input to the terminal -### Path B — Paste API key +Tell the user to open a terminal outside Codex and run: -1. Ask: "Paste your API key (starts with `lk_`). Get one from https://admin.heylua.ai if you don't have one." (free-text). Store as ``. -2. Run `Bash(lua auth configure --api-key --ci)`. +```bash +lua auth configure +``` -## Step 3 — verify +For a new login, tell the user to choose the email option. The CLI handles the email and OTP in the terminal, then requires the user to select an organization, one or more exact agents, and an assignable role. The role picker defaults to Builder. The server limits the available roles to the user's authority ceiling. -Run `Bash(lua agents --json --ci)`. The exit code tells us whether auth worked (the JSON body is org/agent metadata — fine to surface a one-line summary like "✓ Authenticated as ; access to org(s) and agent(s)."). +The CLI writes the issued typed personal credential to `~/.lua-cli/credentials` with mode `0600`. Never ask the user to paste an email, OTP, or credential into the Codex conversation. Never run `lua auth configure` on the user's behalf. + +Ask the user to confirm when the terminal flow finishes. This question must not collect account details or credentials. -If the exit is non-zero, print the CLI's error verbatim and tell the user to re-run `/lua-auth`. Common causes: +## Step 4: verify + +Run `Bash(lua agents --json --ci)`. The exit code tells us whether auth worked (the JSON body is org/agent metadata — fine to surface a one-line summary like "✓ Authenticated as ; access to org(s) and agent(s)."). -- OTP expired or mistyped (re-run `/lua-auth` and pick Email + OTP again) -- API key invalid (re-run `/lua-auth` and pick Paste API key) -- Network error (check connection and retry) +If the exit is non-zero, tell the user to rerun `lua auth configure` in the private terminal. Do not ask them to copy terminal output that contains a credential. ## Notes -- This slash uses 2-3 ask-user-question interactions per the §3.7 permission-vs-information distinction. The frontmatter marker `x-lua-multi-step: true` exempts it from the single-permission lint per the same convention as `/lua-doctor`. -- For a full environment diagnostic (Node, npm, lua-cli version, auth, permission rules), use `/lua-doctor` instead — it includes Step 4 which runs the same OTP flow as this slash. -- The `Bash(lua auth configure --email * --ci)` and `--email * --otp * --ci` and `--api-key * --ci` permission rules are auto-allowed (added in iteration-12), so no Bash prompts during the OTP flow itself. +- For a full environment diagnostic, use `/lua-doctor`. +- The plugin continues to resolve `LUA_API_KEY`, `~/.lua-cli/credentials`, and project `.env` files in the existing order. A working stored credential never triggers this setup flow. diff --git a/skills/lua-doctor/SKILL.md b/skills/lua-doctor/SKILL.md index 80edfba..bdb4832 100644 --- a/skills/lua-doctor/SKILL.md +++ b/skills/lua-doctor/SKILL.md @@ -26,13 +26,9 @@ Run `Bash(lua --version)`. If not installed, ask-user-question to install via `n Run `Bash(lua agents --json --ci)`. The exit code tells us if auth works (0 = authenticated; non-zero = no/invalid key); the JSON body lists the user's orgs and agents (metadata, no secrets). -**Do NOT use `lua auth key --force`** as the auth probe — it prints the raw API key to stdout, which would land in the Claude conversation transcript, the model's context, and Anthropic's request logs. The §3.7 single-permission contract values include "never leak credentials into the transcript." +**Do NOT use `lua auth key --force`** as the auth probe. It prints the raw credential to stdout, which would put the credential in the Codex conversation and model context. -If exit non-zero, run the OTP orchestration: - -- ask-user-question: "Email for your Lua account, or paste an existing API key?" with options `[Email + OTP, API key, Cancel]`. -- If Email: ask for the email, run `lua auth configure --email --ci`, then ask for the OTP code, run `lua auth configure --email --otp --ci`. -- If API key: ask for the key, run `lua auth configure --api-key --ci`. +If the command fails, invoke `/lua-auth`. That skill preserves any working legacy credential. For a new login, it requires `lua-cli` 3.28.0 or newer and sends the user to `lua auth configure` in a private terminal. Do not collect an email, an OTP, or a credential in this conversation. Do not recreate the old flag-based OTP flow here. ## Step 5 — permission rules @@ -46,6 +42,6 @@ Plugin-level `settings.json` is silently ignored by Cursor for `permissions` key If the user skips: print a one-line warning that every Bash invocation will trigger a permission prompt and the deploy-deny rule won't fire (the §3.3 hook still gates, but it's the only line of defence rather than the second). -Per §3.7, each step asks AT MOST one permission interaction. Information collection (email, OTP code, settings.json contents) is exempt from the single-permission rule per §3.7's permission-vs-information distinction. +Per §3.7, each step asks at most one permission interaction. Account details and credentials never enter this conversation. After all five steps green, print "✓ Lua plugin ready. Try `/lua-init` to start a new agent project." diff --git a/skills/lua-init/SKILL.md b/skills/lua-init/SKILL.md index 7fdbe48..69bd494 100644 --- a/skills/lua-init/SKILL.md +++ b/skills/lua-init/SKILL.md @@ -1,16 +1,16 @@ --- name: lua-init -description: Initialize a new Lua agent project. Wraps `lua init --ci` after collecting agent name, org, model, and optional promo code. Auto-resolves missing auth or stale lua-cli before running. +description: Initialize a new Lua agent project. Wraps `lua init --ci` after collecting agent name, org, model, and optional promo code. Routes missing authentication through private CLI setup. --- You are `/lua-init`. The user wants to create a new Lua agent project in the current directory. -## Step 0 — preflight (auto-resolve dependencies — DO NOT punt back to the user) +## Step 0: preflight -Iteration-13 audit: when the user says "let's go" or invokes `/lua-init` after the architect proposes a plan, they expect the build to proceed autonomously. Your job is to **auto-invoke** the dependency-resolving slashes via the agent invocation, NOT to ask the user to run them. +When the user invokes `/lua-init`, resolve safe dependencies through agent invocation. Authentication is different because account details and credentials must stay outside the conversation. 1. **Auth probe**: Run `Bash(lua agents --json --ci)`. If exit is non-zero: - - Auto-invoke the auth slash: use the **agent invocation** with `agent: "lua-auth"`. Do NOT ask the user "want me to run /lua-auth?" — they implicitly authorized by running `/lua-init`. + - Invoke the auth skill with `agent: "lua-auth"`. It preserves a working legacy credential. A new login pauses while the user runs `lua auth configure` in a private terminal. - After `/lua-auth` returns, re-probe with `Bash(lua agents --json --ci)`. - If still non-zero, abort: "Authentication didn't complete. Re-run `/lua-auth` then `/lua-init`." diff --git a/test/hooks/before-shell-execution.test.mjs b/test/hooks/before-shell-execution.test.mjs index 9a46352..0f7ff3d 100644 --- a/test/hooks/before-shell-execution.test.mjs +++ b/test/hooks/before-shell-execution.test.mjs @@ -15,8 +15,16 @@ describe('before-shell-execution decide() — Cursor safety hook', () => { expect(r?.reason).toContain('DEPLOY_DENIED_AUTH_KEY'); }); - test('does NOT block `lua auth configure`', () => { - expect(decide({ tool_input: { command: 'lua auth configure --email a@b.com' } })).toBeNull(); + test('blocks `lua auth configure` so secrets stay in a private terminal', () => { + const r = decide({ tool_input: { command: 'lua auth configure' } }); + expect(r?.block).toBe(true); + expect(r?.reason).toContain('AUTH_INPUT_DENIED'); + expect(r?.reason).toContain('private terminal'); + }); + + test('blocks old flag-based auth commands', () => { + expect(decide({ tool_input: { command: 'lua auth configure --email a@b.com' } })?.block).toBe(true); + expect(decide({ tool_input: { command: 'lua auth configure --api-key secret' } })?.block).toBe(true); }); }); diff --git a/test/hooks/check-lua-auth.test.mjs b/test/hooks/check-lua-auth.test.mjs index cf8b5f5..6ed628f 100644 --- a/test/hooks/check-lua-auth.test.mjs +++ b/test/hooks/check-lua-auth.test.mjs @@ -35,12 +35,13 @@ describe('check-lua-auth decide()', () => { expect(result?.warn).toContain('/lua-auth'); }); - test('warning message mentions both Email + OTP and API key paths', () => { + test('warning keeps the new login outside the conversation', () => { const versionOK = { exitCode: 0, stdout: '3.12.3\n', stderr: '' }; const authFail = { exitCode: 1, stdout: '', stderr: '' }; const result = decide(versionOK, authFail); - expect(result?.warn).toContain('Email + OTP'); - expect(result?.warn).toContain('API key'); + expect(result?.warn).toContain('typed credential'); + expect(result?.warn).toContain('private terminal'); + expect(result?.warn).not.toMatch(/paste.*key/i); }); test('warning explains the user-visible consequence', () => {