chore(release): v3.21.0 — TUI cleanup + client-tools/ToS docs + promotion plan (#145)

* refactor(tui): dead-code / footgun cleanup (A1/A2/A3)

ALIGNMENT.md Rule 2: infra-only, no new cli.js wire behavior.
No protocol, no endpoint, no credential changes.

A1 (session.mjs): delete resolveTuiEntrypointEnv() and its call site
+ the redundant env-strip block around the spawnSync call. The function
mutated a {env} object passed to spawnSync (tmux itself), but tmux does
NOT forward that env to the pane; the pane's claude gets its env ONLY
from the `env` prefix string built inside buildTuiCmd (verified live
2026-06-01). The spawnSync {env} is intentionally minimal now; only
env.HOME is retained (tmux binary reads it). All claude-specific vars
go via the buildTuiCmd prefix string, unchanged. Tests for the now-
deleted function removed; test count drops by 7 (expected).

A2 (transcript.mjs): delete encodeCwd() and transcriptPath() exports.
Production resolves transcripts exclusively via findTranscriptPath()
(glob by session-id); these two helpers carried a fragile path-encoding
rule used only by their own tests. grep confirms zero non-test importers.
Added a TODO comment near findTranscriptPath() noting a CI fixture-
contract test would make claude-schema drift fail loudly. Tests removed;
count drops by 2.

A3 (session.mjs + README): remove the CLAUDE_SKIP_PERMISSIONS branch
that pushed --dangerously-skip-permissions when OCP_TUI_FULL_TOOLS=1.
OCP_TUI_FULL_TOOLS=1 now always takes the --allowedTools path. Rationale:
claude v2.1.x shows an interactive bypass-acceptance screen that a
headless tmux TUI cannot answer — bricks the turn (tui_paste_not_landed
/ wallclock cap), not recoverable without a human. The working path is
--allowedTools + scratch-home settings.json additionalDirectories.
README OCP_TUI_FULL_TOOLS row updated to document the removal and the
correct alternative; CLAUDE_SKIP_PERMISSIONS row for the -p path is
unchanged (still used in server.mjs). Test updated: skip-permissions
case replaced with an assertion that --dangerously-skip-permissions is
absent from the full-tools command.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* docs: client-tools boundary, ToS honesty, promotion plan (B1/B2/B3)

ALIGNMENT.md Rule 2: docs-only, no new cli.js wire behavior.
No protocol, endpoint, or credential changes.

B1 (README): add 'Client-tools boundary' subsection under 'How It
Works'. Documents that OCP is a text-prompt bridge only — it does not
pass OpenAI tools/functions or Anthropic tool_use blocks to the client.
Clients receive assistant TEXT only; client-local tool execution is
not supported by design (bypassing cli.js = out of scope per
ALIGNMENT.md).

B2 (README): two updates to 'Why OCP?' and the LAN-sharing section.
(a) New bullet: OCP drives the official claude CLI as-is — no OAuth
token extraction, no binary patching, no protocol invention — so
traffic looks like genuine Claude Code (cc_entrypoint=cli).
(b) LAN-sharing paragraph strengthened: pooling one Claude subscription
across multiple distinct people may violate Anthropic's Consumer ToS
and risk account suspension by the abuse classifier. The defensible
framing is 'one person, your own devices'; friends/team sharing is
not. Replaces the softer 'account terms are your call' language.
Feature and auth-mode docs are unchanged.

B3 (docs/PROMOTION.md): new promotion strategy doc. Covers: goal
(polish + low-key OSS visibility, NOT growth-hacking given the live
ToS/billing risk), pre-requisites (stability first), honest ToS
disclosure requirement, items explicitly skipped (multi-backend
routing, gateway model-discovery — delegated to OLP; raw API
passthrough — ALIGNMENT.md scope), TUI toggle as billing-split
insurance, and low-key visibility actions. Framed as a recommendation
for the maintainer to review, not a committed plan.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* chore(release): v3.21.0 — TUI cleanup + docs honesty + promotion plan

ALIGNMENT.md Rule 2: release prep; no new cli.js wire behavior.

Bump version 3.20.1 → 3.21.0. CHANGELOG entry covers:
- A1/A2/A3 TUI dead-code removals (inert entrypoint-env path,
  test-only transcript helpers, headless-unusable skip-permissions)
- B1/B2/B3 docs (client-tools boundary, ToS honesty, Why-OCP posture,
  promotion plan)
- Previously-shipped v3.20.x items documented for completeness:
  spawn-home isolation, bounded concurrency queue + 429, ocp restart,
  ocp-plugin OpenClaw compat.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: dtzp555 <dtzp555@gmail.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
dtzp555-max
2026-06-25 11:50:06 +10:00
committed by GitHub
co-authored by taodeng Claude Sonnet 4.6
parent 5aaab5ea28
commit 38da104b97
7 changed files with 161 additions and 132 deletions
+20 -44
View File
@@ -244,23 +244,6 @@ export function prepareTuiHome(realHome, tuiHome, cwd, { envTokenMode = false }
ensureTuiCwdTrusted(tuiHome, cwd);
}
// ── Billing-classifier labeling ─────────────────────────────────────────
// Resolve CLAUDE_CODE_ENTRYPOINT on the spawn env per mode. ALWAYS deletes any
// inherited value first (so a stray entrypoint from OCP's own parent env can never
// leak into / mislabel the billing header). Then:
// "cli" (default) → set "cli": deterministic subscription-pool classification.
// HONEST ONLY because OCP's spawn is a genuine interactive PTY (tmux pane,
// no -p, stdout not redirected). Never set "cli" on a non-interactive spawn.
// "auto" → leave unset → claude self-classifies via its t$A (TTY → cli). Use to
// observe/diagnose the real TTY-derived value.
// "off" → leave the env exactly as inherited (diagnostics / honesty audit).
export function resolveTuiEntrypointEnv(env, mode = "cli") {
if (mode === "off") return env;
delete env.CLAUDE_CODE_ENTRYPOINT;
if (mode === "cli") env.CLAUDE_CODE_ENTRYPOINT = "cli";
return env;
}
// Build interactive claude argv: NO -p, NO --output-format (=> cc_entrypoint=cli).
// MCP hard-disabled: --strict-mcp-config (no --mcp-config) is the only mechanism
// that stops account-attached managed MCP from connecting (spec §5.2 / T6),
@@ -321,27 +304,24 @@ export function buildTuiCmd(claudeBin, model, sessionId, ehome, entrypointMode)
// DEFAULT (safe): hard-disable MCP (--strict-mcp-config + --disallowedTools mcp__*);
// built-in tools stay on, acceptable for single-user A-path.
// OCP_TUI_FULL_TOOLS=1: grant the SAME tool surface as the -p A-path
// (--allowedTools [+ --mcp-config] [+ --dangerously-skip-permissions]), so a
// SINGLE-USER / trusted TUI deployment can run a tool-using agent (e.g. an OpenClaw
// assistant that needs Bash/Read/Write/MCP) on the subscription pool. This mirrors
// buildCliArgs() in server.mjs. Safe to gate ON only because TUI is hard-incompatible
// with AUTH_MODE=multi (server.mjs refuses to boot), so it can never widen a guest's
// surface. Env mirrors server.mjs's CLAUDE_ALLOWED_TOOLS / _SKIP_PERMISSIONS / _MCP_CONFIG.
// (--allowedTools [+ --mcp-config]), so a SINGLE-USER / trusted TUI deployment can
// run a tool-using agent (e.g. an OpenClaw assistant that needs Bash/Read/Write/MCP)
// on the subscription pool. ALWAYS uses --allowedTools (CLAUDE_SKIP_PERMISSIONS /
// --dangerously-skip-permissions is intentionally removed: claude v2.1.x shows an
// interactive bypass-acceptance screen in headless tmux that nothing can answer →
// the turn hangs until the wallclock cap, bricks the pane; not recoverable without a
// human at a keyboard). Use scratch-home settings.json additionalDirectories instead.
let toolArgs;
if (process.env.OCP_TUI_FULL_TOOLS === "1") {
toolArgs = [];
if (process.env.CLAUDE_SKIP_PERMISSIONS === "true") {
toolArgs.push("--dangerously-skip-permissions");
} else {
const allowed = (process.env.CLAUDE_ALLOWED_TOOLS ||
"Bash,Read,Write,Edit,Glob,Grep,WebSearch,WebFetch,Agent")
.split(",").map((s) => s.trim()).filter(Boolean);
// shq EACH token: buildTuiCmd returns a SHELL STRING (run by tmux via sh -c), unlike
// buildCliArgs which returns an argv array to spawn(). claude accepts scoped specifiers
// like "Bash(npm run test:*)" / "Read(~/**)" whose ( ) * ~ would break/inject the shell
// command if pasted bare. (operator-self-injection only — guests can't reach TUI.)
if (allowed.length) toolArgs.push("--allowedTools", ...allowed.map(shq));
}
const allowed = (process.env.CLAUDE_ALLOWED_TOOLS ||
"Bash,Read,Write,Edit,Glob,Grep,WebSearch,WebFetch,Agent")
.split(",").map((s) => s.trim()).filter(Boolean);
// shq EACH token: buildTuiCmd returns a SHELL STRING (run by tmux via sh -c), unlike
// buildCliArgs which returns an argv array to spawn(). claude accepts scoped specifiers
// like "Bash(npm run test:*)" / "Read(~/**)" whose ( ) * ~ would break/inject the shell
// command if pasted bare. (operator-self-injection only — guests can't reach TUI.)
if (allowed.length) toolArgs.push("--allowedTools", ...allowed.map(shq));
if (process.env.CLAUDE_MCP_CONFIG) toolArgs.push("--mcp-config", shq(process.env.CLAUDE_MCP_CONFIG));
} else {
toolArgs = ["--strict-mcp-config", "--disallowedTools", shq("mcp__*")];
@@ -405,15 +385,11 @@ export async function runTuiTurn({
const promptFile = `${tmpDir}/prompt.txt`;
writeFileSync(promptFile, prompt, { mode: 0o600 });
// Build the env: disable marketplace auto-install, strip any Anthropic / CC
// env vars that might interfere with interactive-mode classification.
const env = { ...process.env, CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL: "1" };
delete env.CLAUDECODE;
delete env.ANTHROPIC_API_KEY;
delete env.ANTHROPIC_BASE_URL;
delete env.ANTHROPIC_AUTH_TOKEN;
env.HOME = ehome; // claude reads credentials + writes the transcript under this HOME
resolveTuiEntrypointEnv(env, entrypointMode);
// Minimal env for spawnSync (tmux itself). The pane's claude env comes exclusively
// from the `env` prefix string built inside buildTuiCmd — tmux does NOT forward the
// spawning process's env to the pane, so the {env} here is intentionally minimal.
const env = { ...process.env };
env.HOME = ehome; // tmux needs HOME; all claude-specific vars go via buildTuiCmd prefix
try {
// 1. Boot the interactive session inside tmux, rooted at the scratch cwd.
+2 -15
View File
@@ -9,24 +9,11 @@ import { readFileSync, existsSync, readdirSync } from "node:fs";
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
// Project-dir encoding: claude replaces every "/" AND every "." with "-".
// Verified live (claude v2.1.158): cwd /home/u/.ocp-tui/work is stored under
// projects/-home-u--ocp-tui-work/ (the "." in ".ocp-tui" becomes "-", yielding
// the double dash). The earlier "/"-only rule was wrong for dotted paths; the
// fixture cwd /tmp/tui-test happened to have no dots so it never surfaced.
// NOTE: prefer findTranscriptPath() (glob by session-id) for resolution — it is
// immune to the exact encoding rule. This helper is kept for the known-path case.
export function encodeCwd(cwd) {
return cwd.replace(/[/.]/g, "-");
}
export function transcriptPath(home, cwd, sessionId) {
return `${home}/.claude/projects/${encodeCwd(cwd)}/${sessionId}.jsonl`;
}
// Locate a session's transcript by its UUID across every projects subdir, without
// reconstructing the encoded cwd. Robust to whatever encoding claude applies.
// Returns the path, or null if not present yet (it appears once the turn starts).
// TODO: add a CI fixture-contract test (a captured real transcript) so schema drift
// in the claude JSONL format fails loudly rather than silently degrading.
export function findTranscriptPath(home, sessionId) {
if (!home || !sessionId) return null;
const root = `${home}/.claude/projects`;