diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index b4b04e6..fc9ce24 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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.0.0", + "version": "1.1.0", "homepage": "https://docs.heylua.ai/claude-code-plugin", "repository": "https://github.com/lua-ai-global/claude-code-lua-plugin.git", "license": "MIT", diff --git a/README.md b/README.md index ae55d23..4ff8642 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ A [Claude Code](https://code.claude.com) marketplace + plugin for building, test /reload-plugins ``` -Then `/lua-auth` to authenticate (email + OTP, or paste an existing API key from [admin.heylua.ai](https://admin.heylua.ai)), and `/lua-doctor` to verify the full environment. +Then run `/lua-auth`. For a new login, the plugin sends you to `lua auth configure` in a private terminal. The CLI issues a typed credential bound to the organization, agents, and role you select. Once it's on the official Anthropic marketplace, install will simplify to: @@ -26,7 +26,7 @@ This repo is a **marketplace catalog** that ships one plugin: | Plugin | Description | |---|---| -| [`lua-agent-builder`](./plugins/lua-agent-builder/) | The full Lua agent toolchain — 14 slash commands, 5 subagents, 9 hooks, MCP server with 5 read-only platform tools | +| [`lua-agent-builder`](./plugins/lua-agent-builder/) | The full Lua agent toolchain: 14 slash commands, 5 subagents, 10 hooks, and an MCP server with 5 read-only platform tools | See [`plugins/lua-agent-builder/README.md`](./plugins/lua-agent-builder/README.md) for the plugin's own docs (layout, hooks list, slash commands, design rationale). @@ -42,7 +42,7 @@ claude-code-lua-plugin/ │ │ └── plugin.json ← plugin manifest │ ├── commands/ ← 14 slash commands │ ├── agents/ ← 5 subagents -│ ├── hooks/ ← 9 hooks +│ ├── hooks/ ← 10 hooks │ ├── lib/ ← shared utilities + permissions template │ ├── mcp/lua-platform/ ← MCP server source + bundled dist/ │ ├── scripts/ ← 16 lints + 2 check scripts @@ -76,7 +76,7 @@ The plugin enforces several gates that show up at install time via `/lua-doctor` - **§3.3 deploy gate** — bare `lua deploy` is denied at the permissions layer; defense-in-depth via the `confirm-deploy.mjs` PreToolUse hook. - **`--auto-deploy` block** — denied at permissions + blocked at the hook layer. - **§3.7 single-permission contract** — each slash asks at most one prompt (multi-step diagnostic slashes use the documented `x-lua-multi-step: true` opt-out). -- **Credential isolation** — API key never enters the Claude conversation transcript; `/lua-doctor` Step 4 uses an authenticated metadata probe (`lua agents --json --ci`), not a key-printing command. +- **Credential isolation** — new login runs in a private terminal. Hooks deny model-run `lua auth configure` and `lua auth key*` commands. See [`plugins/lua-agent-builder/SECURITY.md`](./plugins/lua-agent-builder/SECURITY.md) for the disclosure path and a fuller scope statement. diff --git a/docs/USER_GUIDE.md b/docs/USER_GUIDE.md index aa46cbc..9d36932 100644 --- a/docs/USER_GUIDE.md +++ b/docs/USER_GUIDE.md @@ -37,7 +37,7 @@ Two ways to think about it: - **The CLI guide layer**: every `lua` command becomes accessible via a `/lua-*` slash command that knows the right flags, prompts you for missing inputs, and surfaces errors in a friendlier shape than the raw CLI. - **The agent collaborator**: 5 specialised subagents (architect, skill-builder, debug, deploy-pilot, qa) handle the heavyweight tasks — designing an agent's architecture, scaffolding new primitives, diagnosing compile/runtime failures, gating production deploys, running conversational QA — and only ask you for input at the explicit decision points. -Plus 9 hooks that run automatically (auth-state probes, deploy-safety gates, smoke tests, context injection) and an MCP server that exposes 5 read-only platform tools so Claude can answer "what's deployed?" without you typing anything. +Plus 10 hooks that run automatically (auth-state probes, credential-input isolation, deploy-safety gates, smoke tests, context injection) and an MCP server that exposes 5 read-only platform tools so Claude can answer "what's deployed?" without you typing anything. --- @@ -70,7 +70,7 @@ Before installing the plugin you need: | **Node.js ≥ 18** | The plugin's hooks and MCP server are Node ESM | macOS: `brew install node@20` · Windows: `winget install OpenJS.NodeJS.LTS` · Linux: NodeSource APT or `nvm install 20` | | **lua-cli ≥ 3.12.3** | Every slash wraps a `lua` command | `npm install -g lua-cli` | | **Claude Code** | The plugin host | https://claude.com/claude-code | -| **A Lua account + API key** | To talk to `api.heylua.ai` | Sign up at https://admin.heylua.ai (or the plugin's `/lua-auth` will walk you through email + OTP) | +| **A Lua account** | To talk to `api.heylua.ai` | Sign up at https://admin.heylua.ai. `/lua-auth` guides new credential setup. | The plugin's `/lua-doctor` slash will check all of these (Node, npm/pnpm, lua-cli, auth, permission rules) and offer to install or fix anything missing — see [Installation](#installation) for the canonical first-run sequence. @@ -108,8 +108,8 @@ This runs a 5-step diagnostic: 1. **Node ≥ 18** — probes `node --version`. Offers to install if missing. 2. **Package manager** — probes `npm`, falls back to `pnpm`. Offers to install via `corepack enable`. 3. **lua-cli ≥ 3.12.3** — probes `lua --version`. Offers `npm install -g lua-cli` if missing or `/lua-update` if too old. -4. **Authentication** — probes `lua agents --json --ci`. If it fails, kicks off the OTP flow inline (see [Authentication](#authentication)). -5. **Permission rules** — reads the plugin's `lib/permissions-template.json` (29 allow/ask/deny rules) and offers to merge them into your project's `.claude/settings.json`. **Accept this merge** — it's what stops every `lua` invocation from triggering a permission prompt. +4. **Authentication** — probes `lua agents --json --ci`. If it fails, `/lua-auth` sends new login to a private terminal. +5. **Permission rules** — reads the plugin's `lib/permissions-template.json` and offers to merge it into your project's `.claude/settings.json`. Accept the merge to avoid a permission prompt for each safe `lua` invocation. All 5 steps green = you're ready. @@ -117,32 +117,33 @@ All 5 steps green = you're ready. ## Authentication -You need a Lua API key. Two options: +The plugin first checks for a working credential. It uses the existing lookup order: `LUA_API_KEY`, `~/.lua-cli/credentials`, then the project's `.env` file. -### Option A — `/lua-auth` (single-purpose, fastest) +### Set up a new login ``` /lua-auth ``` -Pick one of: +If an existing credential works, `/lua-auth` leaves it unchanged. Existing non-dotted legacy keys remain supported and do not require rotation or a new login. -- **Email + OTP**: enter your email → check your inbox → enter the 6-digit code → done. The OTP is verified server-side and an API key is generated and stored at `~/.lua-cli/credentials` (mode 0600, owner-readable only). -- **Paste API key**: paste a key from [admin.heylua.ai](https://admin.heylua.ai). Stored at the same path. +For a new login, install `lua-cli` 3.28.0 or newer. `/lua-auth` asks you to open a terminal outside Claude Code and run: -Once stored, every `lua-*` slash and the MCP server pick it up automatically. You can re-run `/lua-auth` any time to switch accounts. +```bash +lua auth configure +``` -### Option B — `/lua-doctor` Step 4 +Choose the email option for a new login. The CLI handles your email and OTP in the terminal. You then select an organization, one or more exact agents, and a role. Builder is the default role, but you can select another role that the server allows. The CLI stores the typed personal credential in `~/.lua-cli/credentials` with mode `0600`. -`/lua-doctor` runs the same OTP flow as part of the full diagnostic. Use this if you want to validate the whole environment (Node, lua-cli, permissions) at the same time as auth. +Never paste an email, an OTP, or a credential into the Claude conversation. -### Option C — manual +### Keep an existing credential -Set `LUA_API_KEY` in your shell environment, or create `.env` in your project with `LUA_API_KEY=lk_…`. Both are detected by the plugin's credential resolver. Useful for CI/CD. +Existing `LUA_API_KEY`, `.env`, and `~/.lua-cli/credentials` values keep working. If you already have a credential that is not configured, choose the existing-key option in the private terminal. For CI, set `LUA_API_KEY=''` in the process environment or a protected secret store. ### What's NEVER done -The plugin never uses `lua auth key --force` to read the stored key — that command prints the raw API key to stdout, which would land in the Claude conversation transcript. The auth-state probe used everywhere is `lua agents --json --ci` (returns metadata, not credentials). This is enforced by `lib/permissions-template.json`'s deny rule on `lua auth key*`. +The plugin never runs `lua auth configure` or `lua auth key --force` in the model session. The first command collects account details. The second prints the stored credential. The auth probe is `lua agents --json --ci`, which returns metadata instead of credentials. --- @@ -289,7 +290,7 @@ That's the full loop. | Slash | What it does | |---|---| | `/lua-doctor` | 5-step environment diagnostic: Node, npm/pnpm, lua-cli, auth, permission rules. Offers fixes for each. | -| `/lua-auth` | Standalone authentication: email + OTP or paste API key. Stores in `~/.lua-cli/credentials`. | +| `/lua-auth` | Keeps a working credential or guides typed login through `lua auth configure` in a private terminal. | | `/lua-update` | Updates lua-cli to latest via `npm install -g lua-cli@latest`. | | `/lua-docs ` | Fetches lua-cli documentation from `docs.heylua.ai/` via WebFetch. | @@ -342,7 +343,7 @@ The minimal toolsets are intentional — a debug agent doesn't need Write; a dep ## Hooks — what runs automatically -9 hooks fire on specific Claude Code events. You don't invoke them; they run as subprocesses. +10 hooks fire on specific Claude Code events. You don't invoke them; they run as subprocesses. ### `SessionStart` — once per Claude session @@ -364,6 +365,7 @@ The minimal toolsets are intentional — a debug agent doesn't need Write; a dep |---|---| | `confirm-deploy` | Fires on `lua deploy` invocations. Blocks bare `lua deploy` (must use `LUA_DEPLOY_CONFIRMED=1` prefix from the deploy-pilot subagent). | | `block-auto-deploy` | Fires on commands containing `--auto-deploy`. Always blocks — `--auto-deploy` is never appropriate from inside Claude Code. | +| `block-auth-configure` | Blocks model-run `lua auth configure`. Run interactive login in a private terminal. | | `warn-version-zero` | Fires on `lua push --set-version 0.x.y`. Soft-warns that 0.x versions don't deploy to existing 1.x stacks. | ### `PostToolUse` (matcher: Bash) — after successful bash commands @@ -503,19 +505,20 @@ The plugin enforces several gates that show up at install time via `/lua-doctor` ### `permissions.allow` — runs without prompting -29 explicit `lua-cli` patterns covering safe read operations, the canonical `--ci`/`--force` push form, the env-prefixed deploy form, common version probes, and read-only git commands the deploy-pilot uses. +Explicit `lua-cli` patterns cover safe read operations, the canonical `--ci` and `--force` push form, the env-prefixed deploy form, common version probes, and read-only git commands the deploy-pilot uses. **Per Claude Code's documented precedence (deny → ask → allow), the ask rules win when they overlap with allow rules.** So `lua sync --pull --force` (matches both ask and allow) prompts the user; `lua sync --pull` (only matches allow) runs silently. ### The §3.7 single-permission contract -Every slash asks **at most one** permission interaction. Information collection (asking for an email, an OTP code, a project name) doesn't count as a permission interaction — only `AskUserQuestion` calls that gate behaviour do. +Every slash asks at most one permission interaction. Account details and credentials stay outside the conversation. Slashes that legitimately need multi-step interaction (`/lua-doctor`, `/lua-auth`) declare `x-lua-multi-step: true` in their frontmatter — a private extension marker that the plugin's `lint-single-permission.mjs` script uses to skip those files. Claude Code itself ignores the marker (it's not a documented frontmatter field). ### What the plugin never does - Auto-deploy to production without an explicit prompt +- Collect your email, OTP, or Lua credential in the conversation - Print your API key to stdout - Run `--auto-deploy` even if the model asks - Mutate server state via the MCP server (all 5 MCP tools are read-only) @@ -583,13 +586,7 @@ For deeper diagnosis, run `claude --debug` — every hook invocation shows stdin ### "Authentication failed" after running `/lua-auth` -Check that `~/.lua-cli/credentials` exists and contains a string starting with `lk_`: - -```bash -cat ~/.lua-cli/credentials -``` - -If empty or wrong format, re-run `/lua-auth` and pick "Paste API key" instead — the OTP path may have failed silently. +Run `lua agents --json --ci` in a private terminal. If the command fails, run `lua auth configure` there. Do not print or paste the contents of `~/.lua-cli/credentials`. ### "Lua plugin loaded but you're not authenticated" appears every session @@ -659,7 +656,7 @@ The plugin's hooks and MCP server make HTTPS calls to `api.heylua.ai` only — n /reload-plugins ``` -Auto-updates happen at session start if you've enabled them in your Claude Code settings. The plugin's `version` field in `marketplace.json` controls when users receive updates — currently pinned to `1.0.0`, will bump on each release. +Auto-updates happen at session start if you enable them in Claude Code. The plugin's `version` field in `marketplace.json` controls when users receive updates. Release 1.1.0 adds the private typed login flow for lua-cli 3.28.0 and later. ### Can I customize the slash commands? diff --git a/plugins/lua-agent-builder/.claude-plugin/plugin.json b/plugins/lua-agent-builder/.claude-plugin/plugin.json index 28d025c..009b983 100644 --- a/plugins/lua-agent-builder/.claude-plugin/plugin.json +++ b/plugins/lua-agent-builder/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "name": "lua-agent-builder", - "version": "1.0.0", + "version": "1.1.0", "description": "Build, test, and deploy Lua AI agents from inside Claude Code", "author": { "name": "Lua AI", diff --git a/plugins/lua-agent-builder/README.md b/plugins/lua-agent-builder/README.md index d6db981..1990d5e 100644 --- a/plugins/lua-agent-builder/README.md +++ b/plugins/lua-agent-builder/README.md @@ -9,7 +9,7 @@ Once approved on the official Anthropic marketplace: ``` /plugin install lua-agent-builder@claude-plugins-official /reload-plugins -/lua-auth # email + OTP, or paste an API key +/lua-auth # private typed login through lua-cli 3.28.0+ ``` After auth, `/lua-doctor` to verify the environment, then `/lua-init` to scaffold a new agent project. @@ -108,18 +108,18 @@ itself only — no integration with lua-cli or lua-api in CI.** ## M1 status -- 9 hooks, 100% coverage on every metric (statements, branches, functions, lines) +- 10 hooks, 100% coverage on every metric (statements, branches, functions, lines) - 5 subagents, 14 slash commands ### First-run After installing and activating the plugin: -1. Start any Claude Code session — the `check-lua-auth` SessionStart hook detects you have lua-cli installed but no API key, and surfaces a "run `/lua-auth`" prompt. -2. Run `/lua-auth` to authenticate. Pick **Email + OTP** (we send a 6-digit code to your inbox) or **Paste API key** (from https://admin.heylua.ai). The slash stores credentials in `~/.lua-cli/credentials`. +1. Start any Claude Code session. The `check-lua-auth` SessionStart hook detects lua-cli without a working credential and shows a "run `/lua-auth`" prompt. +2. Run `/lua-auth`. A working `LUA_API_KEY`, `.env`, or credentials-file value stays unchanged. For a new login, use lua-cli 3.28.0 or newer and run `lua auth configure` in a private terminal. Select the organization, exact agents, and role there. 3. From here on, every Lua slash works: `/lua-init`, `/lua-new`, `/lua-test`, `/lua-deploy`, etc. -For the full environment diagnostic (Node version, npm/pnpm, lua-cli version, auth, permission rules) use `/lua-doctor` instead — its Step 4 runs the same OTP flow as `/lua-auth`. +For the full environment diagnostic, use `/lua-doctor`. Its authentication step delegates new login to `/lua-auth`. - 3 GitHub Actions workflows (ci, release-beta, release-prod) - 9 lint scripts: `lint-permissions`, `lint-paths`, `lint-single-permission`, `lint-mcp-refs`, `lint-mcp-config`, `lint-pinned-version`, `lint-hooks-json`, @@ -155,4 +155,4 @@ resolved both gaps **without any new lua-api work**: | ~~`check_drift`~~ | **Deleted from MCP.** Deploy-pilot uses `Bash(lua sync --check)` directly. | **Net for M4: zero new lua-api endpoints needed.** All work happens in the -plugin repo. \ No newline at end of file +plugin repo. diff --git a/plugins/lua-agent-builder/SECURITY.md b/plugins/lua-agent-builder/SECURITY.md index 4ae17b0..377a656 100644 --- a/plugins/lua-agent-builder/SECURITY.md +++ b/plugins/lua-agent-builder/SECURITY.md @@ -37,7 +37,7 @@ The plugin enforces several safety contracts. Bypasses count as security issues: | §3.3 deploy gate: bare `lua deploy` is denied | `lib/permissions-template.json` `deny` list + `hooks/confirm-deploy.mjs` | | §3.3 auto-deploy block: `--auto-deploy` is denied | same | | §3.7 single-permission contract: each slash asks at most one prompt | `scripts/lint-single-permission.mjs` | -| Credential isolation: API key never enters Claude conversation transcript | `commands/lua-doctor.md` Step 4 + `lib/permissions-template.json` deny rule on `lua auth key*` (bug 41) | +| Credential isolation: account details, OTPs, and credentials never enter the Claude conversation | `commands/lua-auth.md` sends new login to a private terminal; `hooks/block-auth-configure.mjs` and `lib/permissions-template.json` deny model-run login; `lua auth key*` remains denied | If you find a way to bypass any of these without an explicit user prompt, please report. diff --git a/plugins/lua-agent-builder/commands/lua-auth.md b/plugins/lua-agent-builder/commands/lua-auth.md index e6652af..c5c07d7 100644 --- a/plugins/lua-agent-builder/commands/lua-auth.md +++ b/plugins/lua-agent-builder/commands/lua-auth.md @@ -1,46 +1,45 @@ --- -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. x-lua-multi-step: true --- 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 -AskUserQuestion **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 +AskUserQuestion 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 Claude Code 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 Claude 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 AskUserQuestion 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/plugins/lua-agent-builder/commands/lua-doctor.md b/plugins/lua-agent-builder/commands/lua-doctor.md index 032c02d..467ac03 100644 --- a/plugins/lua-agent-builder/commands/lua-doctor.md +++ b/plugins/lua-agent-builder/commands/lua-doctor.md @@ -28,11 +28,7 @@ Run `Bash(lua agents --json --ci)`. The exit code tells us if auth works (0 = au **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." -If exit non-zero, run the OTP orchestration: - -- AskUserQuestion: "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 command 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 Claude Code for `permissions 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/plugins/lua-agent-builder/commands/lua-init.md b/plugins/lua-agent-builder/commands/lua-init.md index 6651a46..339360e 100644 --- a/plugins/lua-agent-builder/commands/lua-init.md +++ b/plugins/lua-agent-builder/commands/lua-init.md @@ -1,16 +1,16 @@ --- -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. x-lua-multi-step: true --- 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 Skill tool, NOT to ask the user to run them. +When the user invokes `/lua-init`, resolve safe dependencies through the Skill tool. 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 **Skill tool** with `skill: "lua-auth"`. Do NOT ask the user "want me to run /lua-auth?" — they implicitly authorized by running `/lua-init`. + - Invoke the auth slash with the **Skill tool** and `skill: "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/plugins/lua-agent-builder/hooks/block-auth-configure.mjs b/plugins/lua-agent-builder/hooks/block-auth-configure.mjs new file mode 100644 index 0000000..db7d96b --- /dev/null +++ b/plugins/lua-agent-builder/hooks/block-auth-configure.mjs @@ -0,0 +1,19 @@ +import { runHook, checkNodeVersion, isMainScript } from '../lib/hook-runtime.mjs'; + +export function decide(input) { + const command = input?.tool_input?.command ?? ''; + if (!/\blua\s+auth\s+configure\b/.test(command)) return null; + + 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 Claude conversation.', + }; +} + +/* istanbul ignore next */ +if (isMainScript(import.meta.url)) { + checkNodeVersion(); + await runHook('block-auth-configure', decide); +} diff --git a/plugins/lua-agent-builder/hooks/check-lua-auth.mjs b/plugins/lua-agent-builder/hooks/check-lua-auth.mjs index 3daaabd..cfcf474 100644 --- a/plugins/lua-agent-builder/hooks/check-lua-auth.mjs +++ b/plugins/lua-agent-builder/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/plugins/lua-agent-builder/hooks/hooks.json b/plugins/lua-agent-builder/hooks/hooks.json index 0c0ebcb..d838d69 100644 --- a/plugins/lua-agent-builder/hooks/hooks.json +++ b/plugins/lua-agent-builder/hooks/hooks.json @@ -48,6 +48,12 @@ "if": "Bash(*--auto-deploy*)", "timeout": 5 }, + { + "type": "command", + "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/block-auth-configure.mjs", + "if": "Bash(*lua auth configure*)", + "timeout": 5 + }, { "type": "command", "command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/warn-version-zero.mjs", diff --git a/plugins/lua-agent-builder/lib/permissions-template.json b/plugins/lua-agent-builder/lib/permissions-template.json index 89d4331..af143c4 100644 --- a/plugins/lua-agent-builder/lib/permissions-template.json +++ b/plugins/lua-agent-builder/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/plugins/lua-agent-builder/mcp/lua-platform/README.md b/plugins/lua-agent-builder/mcp/lua-platform/README.md index eff429e..316d6f2 100644 --- a/plugins/lua-agent-builder/mcp/lua-platform/README.md +++ b/plugins/lua-agent-builder/mcp/lua-platform/README.md @@ -32,7 +32,7 @@ The server speaks MCP over stdio. Normally invoked by Claude Code via `.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/plugins/lua-agent-builder/mcp/lua-platform/package-lock.json b/plugins/lua-agent-builder/mcp/lua-platform/package-lock.json index 69d7df3..681659c 100644 --- a/plugins/lua-agent-builder/mcp/lua-platform/package-lock.json +++ b/plugins/lua-agent-builder/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/plugins/lua-agent-builder/mcp/lua-platform/package.json b/plugins/lua-agent-builder/mcp/lua-platform/package.json index fc1596e..ec5cd3b 100644 --- a/plugins/lua-agent-builder/mcp/lua-platform/package.json +++ b/plugins/lua-agent-builder/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/plugins/lua-agent-builder/mcp/lua-platform/src/api-client.mjs b/plugins/lua-agent-builder/mcp/lua-platform/src/api-client.mjs index 930b817..bb07669 100644 --- a/plugins/lua-agent-builder/mcp/lua-platform/src/api-client.mjs +++ b/plugins/lua-agent-builder/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': 'claude-plugin/1.1.0', }, body: body ? JSON.stringify(body) : undefined, signal: controller.signal, diff --git a/plugins/lua-agent-builder/mcp/lua-platform/src/server.mjs b/plugins/lua-agent-builder/mcp/lua-platform/src/server.mjs index 20dd49a..318ae13 100644 --- a/plugins/lua-agent-builder/mcp/lua-platform/src/server.mjs +++ b/plugins/lua-agent-builder/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/plugins/lua-agent-builder/mcp/lua-platform/tests/api-client.test.mjs b/plugins/lua-agent-builder/mcp/lua-platform/tests/api-client.test.mjs index 98b663b..ccd2b4e 100644 --- a/plugins/lua-agent-builder/mcp/lua-platform/tests/api-client.test.mjs +++ b/plugins/lua-agent-builder/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 Claude plugin', async () => { + const fetchFn = mockFetch(jsonResponse({ ok: true })); + await apiRequest('/agents', { fetchFn }); + expect(fetchFn.calls[0].init.headers['X-Lua-Client']).toBe(`claude-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/plugins/lua-agent-builder/package-lock.json b/plugins/lua-agent-builder/package-lock.json index 66d7b5d..b808112 100644 --- a/plugins/lua-agent-builder/package-lock.json +++ b/plugins/lua-agent-builder/package-lock.json @@ -1,12 +1,12 @@ { "name": "claude-code-lua-plugin", - "version": "1.0.0", + "version": "1.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "claude-code-lua-plugin", - "version": "1.0.0", + "version": "1.1.0", "devDependencies": { "@types/node": "^20.11.0", "eslint": "^8.57.0", diff --git a/plugins/lua-agent-builder/package.json b/plugins/lua-agent-builder/package.json index 163918d..c8460d0 100644 --- a/plugins/lua-agent-builder/package.json +++ b/plugins/lua-agent-builder/package.json @@ -1,6 +1,6 @@ { "name": "claude-code-lua-plugin", - "version": "1.0.1", + "version": "1.1.0", "description": "Plugin assets for the lua-agent-builder Claude Code plugin", "private": true, "type": "module", @@ -8,7 +8,7 @@ "node": ">=18.0.0" }, "scripts": { - "lint": "eslint . && node scripts/lint-permissions.mjs && node scripts/lint-paths.mjs && node scripts/lint-single-permission.mjs && node scripts/lint-mcp-refs.mjs && node scripts/lint-mcp-config.mjs && node scripts/lint-pinned-version.mjs && node scripts/lint-hooks-json.mjs && node scripts/lint-knowledge-commands.mjs && node scripts/lint-plugin-manifest.mjs && node scripts/lint-frontmatter.mjs && node scripts/lint-subagent-dispatch.mjs && node scripts/lint-agent-bash-allowlist.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", + "lint": "eslint . && node scripts/lint-permissions.mjs && node scripts/lint-paths.mjs && node scripts/lint-single-permission.mjs && node scripts/lint-mcp-refs.mjs && node scripts/lint-mcp-config.mjs && node scripts/lint-pinned-version.mjs && node scripts/lint-hooks-json.mjs && node scripts/lint-knowledge-commands.mjs && node scripts/lint-plugin-manifest.mjs && node scripts/lint-frontmatter.mjs && node scripts/lint-subagent-dispatch.mjs && node scripts/lint-agent-bash-allowlist.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-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/plugins/lua-agent-builder/scripts/lint-cli-flags.mjs b/plugins/lua-agent-builder/scripts/lint-cli-flags.mjs index 06eeed4..53ed111 100644 --- a/plugins/lua-agent-builder/scripts/lint-cli-flags.mjs +++ b/plugins/lua-agent-builder/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 AUTH_DOC_DIRS = ['../../docs']; +const AUTH_DOC_FILES = ['README.md', 'SECURITY.md', '../../README.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/plugins/lua-agent-builder/scripts/lint-permissions.mjs b/plugins/lua-agent-builder/scripts/lint-permissions.mjs index f312e56..6b414d4 100644 --- a/plugins/lua-agent-builder/scripts/lint-permissions.mjs +++ b/plugins/lua-agent-builder/scripts/lint-permissions.mjs @@ -94,12 +94,10 @@ if (!hasAutoDeployDeny) { fail('settings.json must deny patterns containing `--auto-deploy` — required by §3.3 hooks.'); } -// Iteration-13 audit: `lua auth key` prints the raw API key to stdout. If -// it's auto-allowed, the key lands in the Claude conversation transcript on -// every invocation. Must be denied (or at minimum not auto-allowed) so -// Claude Code prompts the user before printing the credential. +// Credential display and interactive login both belong in a private terminal. const credentialPrinters = [ { pattern: 'lua auth key', reason: 'prints the raw API key to stdout' }, + { pattern: 'lua auth configure', reason: 'collects account details and an OTP' }, ]; for (const { pattern, reason } of credentialPrinters) { for (const allow of allowSet) { @@ -115,8 +113,8 @@ for (const { pattern, reason } of credentialPrinters) { // to a real bash invocation in commands/*.md. // // Found and fixed in iteration-2 audit (2026-05-02): missing entries for -// `lua init --ci`, `lua auth configure --`, and `lua skills view --ci` -// caused the doctor / init / test slashes to prompt unexpectedly. +// `lua init --ci` and `lua skills view --ci` caused the doctor / init / test +// slashes to prompt unexpectedly. const REQUIRED_ALLOW_PREFIXES = [ // Critical loop slashes 'Bash(lua --version', @@ -132,7 +130,6 @@ const REQUIRED_ALLOW_PREFIXES = [ // prints the API key to stdout and so leaked it into the conversation // transcript every time /lua-doctor ran). 'Bash(lua agents', - 'Bash(lua auth configure --', // Deploy gate: only the env-prefixed form is allowed 'Bash(LUA_DEPLOY_CONFIRMED=1 lua deploy', // Read-only git probes used by deploy-pilot pre-flight (`git status --short`) diff --git a/plugins/lua-agent-builder/scripts/lint-release-version.mjs b/plugins/lua-agent-builder/scripts/lint-release-version.mjs new file mode 100644 index 0000000..0f05b0a --- /dev/null +++ b/plugins/lua-agent-builder/scripts/lint-release-version.mjs @@ -0,0 +1,33 @@ +#!/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 [ + ['.claude-plugin/plugin.json', (await readJson('.claude-plugin/plugin.json')).version], + ['../../.claude-plugin/marketplace.json', (await readJson('../../.claude-plugin/marketplace.json')).plugins[0].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': 'claude-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/plugins/lua-agent-builder/test/hooks/block-auth-configure.test.mjs b/plugins/lua-agent-builder/test/hooks/block-auth-configure.test.mjs new file mode 100644 index 0000000..a179948 --- /dev/null +++ b/plugins/lua-agent-builder/test/hooks/block-auth-configure.test.mjs @@ -0,0 +1,24 @@ +import { describe, expect, test } from '@jest/globals'; +import { decide } from '../../hooks/block-auth-configure.mjs'; + +describe('block-auth-configure decide()', () => { + test.each([ + 'lua auth configure', + 'lua auth configure --email person@example.com', + 'lua auth configure --api-key secret', + ])('blocks %s', (command) => { + const result = decide({ tool_input: { command } }); + expect(result?.block).toBe(true); + expect(result?.reason).toContain('AUTH_INPUT_DENIED'); + expect(result?.reason).toContain('private terminal'); + }); + + test.each(['lua agents --json --ci', 'lua auth logout', 'lua auth configuration'])('allows %s', (command) => { + expect(decide({ tool_input: { command } })).toBeNull(); + }); + + test('allows missing input', () => { + expect(decide(null)).toBeNull(); + expect(decide({})).toBeNull(); + }); +}); diff --git a/plugins/lua-agent-builder/test/hooks/check-lua-auth.test.mjs b/plugins/lua-agent-builder/test/hooks/check-lua-auth.test.mjs index cf8b5f5..6ed628f 100644 --- a/plugins/lua-agent-builder/test/hooks/check-lua-auth.test.mjs +++ b/plugins/lua-agent-builder/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', () => { diff --git a/plugins/lua-agent-builder/test/lib/hook-runtime.test.mjs b/plugins/lua-agent-builder/test/lib/hook-runtime.test.mjs index 9fdedae..d4047a4 100644 --- a/plugins/lua-agent-builder/test/lib/hook-runtime.test.mjs +++ b/plugins/lua-agent-builder/test/lib/hook-runtime.test.mjs @@ -1,6 +1,8 @@ import { jest, describe, test, expect, beforeEach, afterEach } from '@jest/globals'; import { Readable } from 'node:stream'; -import { fileURLToPath } from 'node:url'; +import { pathToFileURL } from 'node:url'; +import { join as pathJoin } from 'node:path'; +import { tmpdir } from 'node:os'; import { readStdin, log, @@ -133,8 +135,9 @@ describe('isMainScript', () => { test('returns true when import.meta.url matches process.argv[1]', () => { const original = process.argv[1]; - const url = 'file:///tmp/test-script.mjs'; - process.argv[1] = fileURLToPath(url); + const real = pathJoin(tmpdir(), 'test-script.mjs'); + const url = pathToFileURL(real).href; + process.argv[1] = real; try { expect(isMainScript(url)).toBe(true); } finally { @@ -143,7 +146,7 @@ describe('isMainScript', () => { }); test('returns false when import.meta.url differs from process.argv[1]', () => { - expect(isMainScript('file:///tmp/different.mjs')).toBe(false); + expect(isMainScript(pathToFileURL(pathJoin(tmpdir(), 'different.mjs')).href)).toBe(false); }); test('returns false on invalid file URL (catches gracefully)', () => { diff --git a/plugins/lua-agent-builder/test/lib/lua-cli-spawn.test.mjs b/plugins/lua-agent-builder/test/lib/lua-cli-spawn.test.mjs index 04e6f71..df57154 100644 --- a/plugins/lua-agent-builder/test/lib/lua-cli-spawn.test.mjs +++ b/plugins/lua-agent-builder/test/lib/lua-cli-spawn.test.mjs @@ -62,7 +62,9 @@ describe('spawnLua', () => { const callEnv = spawnMock.mock.calls[0][2].env; expect(callEnv.LUA_API_KEY).toBe('k'); - expect(callEnv.PATH).toBe(process.env.PATH); // process.env spread + const pathKey = Object.keys(callEnv).find((key) => key.toLowerCase() === 'path'); + expect(pathKey).toBeDefined(); + expect(callEnv[pathKey]).toBe(process.env[pathKey]); }); test('returns clean result on normal exit', async () => {