fix(custom-model): address fourth pre-merge review + merge upstream master (Ark0N)

Merged upstream/master (22 commits: reboot-restore recovery feature,
terminal keycode229 recovery work, install.sh/CLI-catalog generator
changes, CHANGELOG/version bump to 1.30.0) into this branch. No
conflicts; git auto-merged every overlapping file (CLAUDE.md,
docs/api-reference.md, app.js, index.html, styles.css, routes/index.ts,
session-routes.ts, schemas.ts, server.ts).

Two required fixes from the latest review:

1. privilegedEnvKeys widening (stock.ts) changes behaviour outside this
   feature. The reviewer decided to keep both CLAUDE_CODE_MAX_CONTEXT_TOKENS
   and CLAUDE_CONFIG_DIR listed (types.ts's rule that every traffic-
   redirecting var this feature introduces must appear there stays
   literally true), and asked for the real consequences documented
   instead of hidden:
   - Corrected session-env-clamp.ts's fileoverview, which stated the
     opposite of what the code now does (reboot-restore's clamp call
     used to be able to strip nothing for claude; it now strips a
     persisted CLAUDE_CONFIG_DIR for a non-granted owner).
   - Corrected the rationale comments in stock.ts: privilegedEnvKeys
     has exactly one consumer (ownerClampedEnvKeys, feeding the
     generic envOverrides clamp on create/quick-start/reboot-restore),
     not the custom-model routes.
   - Added a CLAUDE.md line to the CLAUDE_CONFIG_DIR gotcha covering
     the admin-only-in-multi-user-mode and reboot-restore-strips-it
     consequences.
   - Added a "Claude multi-user clamp" test next to the existing
     DeepSeek/OMP ones, pinning the new stripping behaviour.

2. GET .../running-status (custom-model-routes.ts) no longer passes
   the raw llama-swap `cmd` field (the literal launch line, which can
   carry model paths and --api-key) to the browser -- the frontend
   only ever reads model/state, cmd exists solely for server-side
   parseCtxFromCmd() during discovery. Added a test asserting the
   response never contains cmd or a planted secret.

Also regenerated config/clis.stock.json and install.sh's catalogue
block (npm run generate:cli-catalog) to clear drift introduced by the
upstream merge, since it was failing the sync check.

Left to the reviewer, as they said they'd take at merge: the two
"comments pointing at removed code" cleanups, the two stale CLAUDE.md
counts, and the small items list (mode==='claude' frontend branch,
isCliAvailable() unknown-id gap, shared confirmed flag ordering,
one-shot cancel toast severity, pumpLlamaSwapLogTail buffer cap).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ea59JhUmHBm1gRCsiYF33R
This commit is contained in:
Devvyn
2026-09-19 18:05:51 +08:00
co-authored by Claude Sonnet 5
parent 56209e7829
commit 5fc391a47c
6 changed files with 108 additions and 16 deletions
+15 -5
View File
@@ -228,9 +228,11 @@ const CLAUDE: CliEntry = {
privilegedParams: [],
// ANTHROPIC_* is NOT in allowedPrefixes/allowedKeys above (deliberately — see the
// allowedPrefixes comment nearby), so these are unreachable via plain envOverrides
// today; listed here only so the dedicated custom-model route (docs/custom-model-endpoints-plan.md
// chunk 5) clamps them for a non-granted multi-user owner the same way every other
// CLI's injection vars are clamped, the day that route widens who can set them.
// today. privilegedEnvKeys has exactly one consumer, ownerClampedEnvKeys() in
// session-env-clamp.ts, which feeds the generic envOverrides clamp on
// POST /api/sessions, POST /api/quick-start and reboot-restore — no custom-model
// route reads this field at all, and the values it injects are merged in AFTER
// that clamp runs regardless of what's listed here.
privilegedEnvKeys: [
'ANTHROPIC_BASE_URL',
'ANTHROPIC_API_KEY',
@@ -239,8 +241,16 @@ const CLAUDE: CliEntry = {
'ANTHROPIC_DEFAULT_OPUS_MODEL',
// CLAUDE_CODE_MAX_CONTEXT_TOKENS already matches the CLAUDE_CODE_* allowedPrefix, and
// CLAUDE_CONFIG_DIR is already an allowed exact key (docs/wiki/Agent-CLIs.md), so both
// were already reachable via plain envOverrides before this pair existed — listed here
// only so the custom-model route clamps them the same way as every other injected var.
// were already reachable via plain envOverrides before this pair existed and this
// feature does not strictly need either listed. They stay listed anyway, because
// types.ts's rule ("every traffic-redirecting var this feature introduces MUST also
// appear in privilegedEnvKeys") is meant to hold literally, not with an exception
// carved out for the two vars that happen not to need it today. The real
// consequence lands on the GENERIC envOverrides clamp above, not on this feature:
// a non-granted multi-user owner can no longer set CLAUDE_CONFIG_DIR through
// envOverrides at all (the per-client-account override, #255), and a PERSISTED one
// is now stripped on reboot-restore for such an owner too — see
// session-env-clamp.ts's own fileoverview.
'CLAUDE_CODE_MAX_CONTEXT_TOKENS',
'CLAUDE_CONFIG_DIR',
],
+12 -7
View File
@@ -6,13 +6,18 @@
* stripped before the session is built. The create and resume routes are what
* this bites on: they clamp what a request asked for.
*
* The reboot-restore route calls it as defence in depth, and today it can strip
* nothing. `Session.getEnvOverridesForPersist()` keeps only `CLAUDE_CODE_*` and
* `CLAUDE_CONFIG_DIR` out of a session's overrides, claude's `privilegedEnvKeys`
* are the five `ANTHROPIC_*` names, and that pass admits claude alone — so a
* persisted record cannot carry a clamped key. The call is there for the day the
* persisted set widens. The grant re-resolution that does bite on that path is
* `resolveClaudeModeForUsername`, which recomputes the permission mode.
* The reboot-restore route calls it as defence in depth, and it CAN strip
* something today: `Session.getEnvOverridesForPersist()` keeps only
* `CLAUDE_CODE_*` and `CLAUDE_CONFIG_DIR` out of a session's overrides, and
* claude's `privilegedEnvKeys` now includes both `CLAUDE_CODE_MAX_CONTEXT_TOKENS`
* and `CLAUDE_CONFIG_DIR` (Custom Model Endpoint Profiles, since both can
* redirect a claude session's traffic — see stock.ts's own comment on why they
* are listed despite not needing the clamp for that feature). So a non-granted
* owner's persisted `CLAUDE_CONFIG_DIR` (the per-client-account override, #255)
* is now stripped on reboot-restore, silently returning that session to the
* default Claude account rather than the account it was pointed at. The grant
* re-resolution that ALSO bites on that path is `resolveClaudeModeForUsername`,
* which recomputes the permission mode.
*
* This lives outside `web/routes` on purpose. The question it answers is about
* session privilege rather than about HTTP, and `cron/cron-service.ts` sets the
+15 -2
View File
@@ -790,7 +790,15 @@ export function registerCustomModelRoutes(app: FastifyInstance): void {
// can equally ask what it currently has loaded, before or while that apply is pending.
app.get(
'/api/model-endpoints/:id/running-status',
async (req): Promise<ApiResponse<LlamaSwapStatus & { logLine?: string }>> => {
async (
req
): Promise<
ApiResponse<{
isLlamaSwap: boolean;
running: Array<Pick<LlamaSwapRunningModel, 'model' | 'state'>>;
logLine?: string;
}>
> => {
const { id } = req.params as { id: string };
const hosts = await readCustomModelHosts(CODEMAN_CONFIG_DIR);
const host = hosts.find((item) => item.id === id);
@@ -802,7 +810,12 @@ export function registerCustomModelRoutes(app: FastifyInstance): void {
// Only worth tailing /logs once llama-swap is actually confirmed — a plain
// llama.cpp/OpenAI-compatible server has no such endpoint at all.
const logLine = status.isLlamaSwap ? getLatestLlamaSwapLogLine(host) : undefined;
return { success: true, data: { ...status, logLine } };
// `cmd` (the literal llama-server launch line, which can carry model paths and
// --api-key) exists only so parseCtxFromCmd() can read it server-side during
// discovery — this un-gated, polled-every-second route has no reason to hand it
// to the browser, which only ever reads `model`/`state`.
const running = status.running.map(({ model, state }) => ({ model, state }));
return { success: true, data: { isLlamaSwap: status.isLlamaSwap, running, logLine } };
}
);
}