mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-02 21:49:42 +02:00
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
103 lines
5.6 KiB
TypeScript
103 lines
5.6 KiB
TypeScript
/**
|
|
* @fileoverview The env-var half of the multi-user privilege clamp.
|
|
*
|
|
* A session's `envOverrides` can hand back privilege that the per-CLI config
|
|
* clamp removed, so a non-granted owner's overrides get the privileged keys
|
|
* 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 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
|
|
* precedent by importing `canUsernameRunPrivilegedCommands` from `user-store.ts`
|
|
* directly and re-resolving the owner's grant when a job fires. Every caller here
|
|
* re-resolves the grant at the moment it builds a session, for the same reason.
|
|
*
|
|
* @dependencies user-store (canUsernameRunPrivilegedCommands), config/cli-registry
|
|
* @consumedby web/routes/session-routes, web/routes/reboot-restore-routes
|
|
*
|
|
* @module session-env-clamp
|
|
*/
|
|
|
|
import { canUsernameRunPrivilegedCommands } from './user-store.js';
|
|
import { enabledClis } from './config/cli-registry/registry.js';
|
|
|
|
/**
|
|
* Env-var keys a non-granted owner must not be able to set, because each one
|
|
* hands back privilege `clampExternalCliBypassForOwner()` just removed, or redirects a
|
|
* credential-resolution endpoint.
|
|
*
|
|
* The DeepSeek three are reachable because `DSH_*` and `DEEPSEEK_*` are
|
|
* allowlisted `envOverrides` prefixes (schemas.ts) — which they have to be, since
|
|
* that is also how a user configures the harness's non-privileged knobs.
|
|
*
|
|
* - `DSH_PERMISSION_MODE` IS the harness's permission switch. Every other CLI's
|
|
* bypass is a command-line FLAG, reachable only through the per-CLI config the
|
|
* clamp already owns; this one is an env var, so the config clamp alone is
|
|
* half a gate.
|
|
* - `DSH_HOME` points the launcher at a profile tree, and a profile's plugin code
|
|
* executes at BOOT, before any approval row can apply. A user who can write a
|
|
* workspace can put a profile in it, so this is the wider of the two.
|
|
* - `DEEPSEEK_BASE_URL` aims the provider endpoint, and `_configureCliEnv()`
|
|
* forwards the SERVER's own `DEEPSEEK_API_KEY` into every dsh pane before
|
|
* `applyEnvOverrides()` runs — so a non-granted owner who could set the base
|
|
* URL would have the operator's API key sent as a bearer credential to a host
|
|
* of their choosing. (`DEEPSEEK_API_KEY` itself stays overridable: supplying
|
|
* your OWN key removes privilege rather than granting it.)
|
|
* - `OMP_AUTH_BROKER_URL`/`OMP_AUTH_BROKER_TOKEN` are where omp resolves
|
|
* credentials from — the same shape as `DEEPSEEK_BASE_URL` above, reachable
|
|
* because `OMP_*` is an allowlisted prefix. Unlike DeepSeek, Codeman does not
|
|
* forward any operator-held key into an omp pane today (omp's provider
|
|
* credentials live in `~/.omp` config files, not env vars), so there is no
|
|
* known concrete exfiltration path yet — clamped defensively anyway, since a
|
|
* non-granted owner redirecting where a shared multi-tenant deployment
|
|
* resolves auth from is not something to allow silently (found in
|
|
* Ark0N/Codeman#353 review; omp's own knobs are otherwise mostly `PI_*`,
|
|
* already allowlisted for pi and not addressed here — see resolveOmpHome()).
|
|
*/
|
|
export function ownerClampedEnvKeys(): string[] {
|
|
return enabledClis().flatMap((entry) => entry.capabilities.privilegedEnvKeys);
|
|
}
|
|
|
|
/**
|
|
* Env-var half of the multi-user bypass clamp.
|
|
*
|
|
* `clampExternalCliBypassForOwner()` in `web/routes/session-routes.ts` clamps the
|
|
* per-CLI CONFIG, and for every CLI
|
|
* but DeepSeek that is the whole story. Here it is not: `applyEnvOverrides()` runs
|
|
* AFTER `_configureCliEnv()` in tmux-manager, so an override sent on the SAME
|
|
* request lands last and wins, and a non-granted owner could restore
|
|
* `danger-full-access` on the very request the config clamp downgraded.
|
|
*
|
|
* Keys are DROPPED rather than rewritten: dropping falls through to what
|
|
* `_configureCliEnv()` exports, which is the clamped config and the server's own
|
|
* `DSH_HOME`, i.e. exactly the intended state. No-op in single-user mode and for a
|
|
* granted owner, like every other clamp here
|
|
* (`canUsernameRunPrivilegedCommands()` returns true when `!isMultiUserMode()`),
|
|
* and it returns the caller's own object untouched when there is nothing to strip.
|
|
*/
|
|
export async function clampEnvOverridesForOwner(
|
|
owner: string | undefined,
|
|
envOverrides: Record<string, string> | undefined
|
|
): Promise<Record<string, string> | undefined> {
|
|
if (!envOverrides) return envOverrides;
|
|
const keys = ownerClampedEnvKeys();
|
|
if (!keys.some((key) => key in envOverrides)) return envOverrides;
|
|
if (await canUsernameRunPrivilegedCommands(owner)) return envOverrides;
|
|
const clamped = { ...envOverrides };
|
|
for (const key of keys) delete clamped[key];
|
|
return clamped;
|
|
}
|