mirror of
https://github.com/dtzp555-max/ocp.git
synced 2026-07-19 09:44:07 +00:00
feat(tui): OCP_TUI_FULL_TOOLS gate — -p-equivalent tools for single-user TUI (#135)
* feat(tui): OCP_TUI_FULL_TOOLS gate — -p-equivalent tool surface for single-user TUI Lets a SINGLE-USER / trusted TUI deployment run a tool-using / MCP agent (e.g. an OpenClaw assistant) on the subscription pool. When OCP_TUI_FULL_TOOLS=1, buildTuiCmd grants the interactive session the SAME tool surface as the -p A-path — --allowedTools (+ optional --mcp-config / --dangerously-skip-permissions, read from the same CLAUDE_ALLOWED_TOOLS / CLAUDE_MCP_CONFIG / CLAUDE_SKIP_PERMISSIONS env as buildCliArgs) — instead of the default MCP-walled, built-in-tools-only set. Motivation: the default TUI tool wall (--strict-mcp-config --disallowedTools mcp__*) exists for multi-tenant safety, but it also blocks a trusted single-operator agent from its MCP tools — forcing tool-using agents onto the metered -p pool after 2026-06-15. This gate resolves that for the single-user case. Safe to gate ON only because TUI is hard-incompatible with AUTH_MODE=multi (server.mjs refuses to boot, see existing guard), so this can NEVER widen a guest's tool surface. Default (gate unset) is unchanged: MCP wall + built-in tools only. Verified live on PI231 through the real OCP TUI path (spike 2026-06-02): - built-in tools: claude created + read a file on the host (2 tool_use entries); - MCP: claude invoked a custom MCP server's tool (mcp__spike__echo_marker) and returned its output; - billing: entrypoint=cli (subscription pool) WITH full tools + MCP; - completion: end_turn, no permission stall, no hang. Tests: +1 (full-tools branch: --allowedTools present + MCP wall dropped; skip-permissions supersedes; mcp-config threaded). 195 pass. README: new OCP_TUI_FULL_TOOLS env var entry. ALIGNMENT.md: changes lib/tui/session.mjs, not server.mjs — server.mjs hard requirements do not trigger. Mirrors buildCliArgs() permissions logic; the flags are documented Claude Code CLI flags, not invented endpoints. cli.js citation N/A under Rule 2. ADR 0007 (A-path). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(tui): shq operator tool tokens in full-tools shell string (reviewer Finding 2) Independent reviewer (APPROVE WITH CHANGES) caught that buildTuiCmd returns a SHELL STRING (run by tmux via sh -c), unlike buildCliArgs which returns an argv array to spawn(). Operator-supplied CLAUDE_ALLOWED_TOOLS can be a scoped specifier such as "Bash(npm run test:*)" or "Read(~/**)", whose ( ) * ~ would break / inject the shell command if pasted bare. Now shq() each allowed-tool token (operator-self-injection only — guests cannot reach TUI per the multi-mode boot guard, but it's a real correctness bug). Also: tightened the README OCP_TUI_FULL_TOOLS wording per reviewer Finding 1 — the precise safety property is "no guest key can reach the TUI path (multi-mode boot is a hard exit)", and noted the AUTH_MODE=shared + OCP_TUI_ALLOW_LAN trust model is unchanged. Test: +scoped-specifier assertions (token shq'd; never appears unquoted). 195 pass. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: dtzp555 <dtzp555@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -1506,6 +1506,50 @@ test("buildTuiCmd keeps version pin + entrypoint label + MCP wall", () => {
|
||||
assert.ok(/-u CLAUDE_CODE_ENTRYPOINT/.test(auto), "auto mode unsets any inherited entrypoint");
|
||||
});
|
||||
|
||||
test("buildTuiCmd OCP_TUI_FULL_TOOLS=1 grants -p-equivalent tool surface (single-user opt-in)", () => {
|
||||
const save = { ...process.env };
|
||||
const restore = () => {
|
||||
for (const k of ["OCP_TUI_FULL_TOOLS", "CLAUDE_SKIP_PERMISSIONS", "CLAUDE_MCP_CONFIG", "CLAUDE_ALLOWED_TOOLS"]) {
|
||||
if (k in save) process.env[k] = save[k]; else delete process.env[k];
|
||||
}
|
||||
};
|
||||
try {
|
||||
// default (gate off) keeps the MCP wall, no --allowedTools
|
||||
delete process.env.OCP_TUI_FULL_TOOLS;
|
||||
const off = buildTuiCmd("/usr/bin/claude", "m", "s", "/home/u", "cli");
|
||||
assert.ok(off.includes("--strict-mcp-config") && !off.includes("--allowedTools"), "gate off = MCP wall");
|
||||
|
||||
// gate on: --allowedTools (default set incl Bash), MCP wall dropped
|
||||
process.env.OCP_TUI_FULL_TOOLS = "1";
|
||||
delete process.env.CLAUDE_SKIP_PERMISSIONS;
|
||||
delete process.env.CLAUDE_MCP_CONFIG;
|
||||
delete process.env.CLAUDE_ALLOWED_TOOLS;
|
||||
const full = buildTuiCmd("/usr/bin/claude", "m", "s", "/home/u", "cli");
|
||||
assert.ok(full.includes("--allowedTools") && full.includes("Bash"), "full-tools grants --allowedTools incl Bash");
|
||||
assert.ok(!full.includes("--strict-mcp-config") && !/--disallowedTools/.test(full), "full-tools drops the MCP wall");
|
||||
|
||||
// skip-permissions supersedes --allowedTools
|
||||
process.env.CLAUDE_SKIP_PERMISSIONS = "true";
|
||||
const skip = buildTuiCmd("/usr/bin/claude", "m", "s", "/home/u", "cli");
|
||||
assert.ok(skip.includes("--dangerously-skip-permissions") && !skip.includes("--allowedTools"), "skip-permissions honored");
|
||||
|
||||
// mcp-config threaded through
|
||||
delete process.env.CLAUDE_SKIP_PERMISSIONS;
|
||||
process.env.CLAUDE_MCP_CONFIG = "/tmp/mcp.json";
|
||||
const mcp = buildTuiCmd("/usr/bin/claude", "m", "s", "/home/u", "cli");
|
||||
assert.ok(/--mcp-config '\/tmp\/mcp.json'/.test(mcp), "mcp-config passed through (shq'd)");
|
||||
|
||||
// operator-supplied scoped tool specifiers must be shell-quoted (no injection via ()*~)
|
||||
delete process.env.CLAUDE_MCP_CONFIG;
|
||||
process.env.CLAUDE_ALLOWED_TOOLS = "Bash(npm run test:*),Read";
|
||||
const scoped = buildTuiCmd("/usr/bin/claude", "m", "s", "/home/u", "cli");
|
||||
assert.ok(scoped.includes("'Bash(npm run test:*)'"), "scoped tool tokens are shq'd in the shell string");
|
||||
assert.ok(!/--allowedTools Bash\(npm/.test(scoped), "scoped token must NOT appear unquoted");
|
||||
} finally {
|
||||
restore();
|
||||
}
|
||||
});
|
||||
|
||||
test("reaper kills ONLY ocp-tui- sessions, never olp-tui-", () => {
|
||||
const killed = [];
|
||||
const fakeTmux = (args) => {
|
||||
|
||||
Reference in New Issue
Block a user