mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-04 14:39:42 +02:00
3f8c8e99d1facecc9dd18739db0c9efb02f0ff31
10
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3f8c8e99d1 |
feat(grok): add Grok Build (xAI) as a seventh CLI run mode
SessionMode gains 'grok', a first-class backend alongside Claude Code,
shell, OpenCode, Codex, Gemini, Antigravity and Pi: its own PTY, tmux
session, charcoal tab identity ('gk' badge), welcome button, run-mode
entry, cron agentType, Docker and remote-SSH command defaults, and
clone-repo Brain option. Flag surface verified live against grok 1.0.5.
Grok mixes two existing shapes and the wiring follows from that:
- Codex-shaped on permissions: the bypass switch is GrokConfig.alwaysApprove
(--always-approve, grok's bypassPermissions mode; config-level deny rules
still apply on top). The Run button sends it true, like runAntigravity(),
and clampExternalCliBypassForOwner() puts grok in the only-if-sent branch:
a bare grok spawn is grok's own ask-mode default, which is already safe,
so only a sent config needs the flag forced off. Cron needs nothing for
the same reason.
- OpenCode-shaped on rendering: grok is a fullscreen alternate-screen TUI
with mouse support, so it stays OUT of isAltScreenStripMode() and lands
on the narrow tmux-attach strip and the 'buffer' local-echo fallthrough
(unmeasured against an authenticated composer; documented fallback is the
'off' branch).
- Pi-shaped on resolution: 'grok' has npm squatters (@vibe-kit/grok-cli
also installs a grok bin), so grok-cli-resolver.ts version-probes every
candidate (grok --version, killSignal SIGKILL, VITEST-gated) and
GET /api/grok/status surfaces path AND version; GROK_VERSION_REGEX is
shared with the dependency registry so doctor and run mode cannot drift.
Env allowlist gains GROK_* plus the XAI_* vendor namespace (XAI_API_KEY is
grok's documented headless auth var), the same narrow-vendor reasoning as
GOOGLE_* for gemini. Resume is id-regexed on purpose: grok's own --resume
also matches session titles, which are arbitrary user strings that must
never reach the bash -c spawn line.
Docker: grok is not on npm, so the agent image installs it in its own step
(xAI's installer has no --dir override; the binary is copied to
/usr/local/bin and root's ~/.grok dropped in the same layer), and
credentials are seeded per-file (auth.json, config.toml, pager.toml; the
dir also holds sessions/, memory/ and the ~160MB binary). Remote SSH routes
through the login-shell wrapper like the other agent CLIs.
Verified end to end on an isolated CODEMAN_INSTANCE with grok 1.0.5
installed: /api/grok/status resolves and reports the probed version,
quick-start spawns a pane whose command line ends in 'grok
--always-approve', the real TUI renders (OAuth device screen on an
unauthenticated box), and grokConfig round-trips through state.json.
Docs: docs/grok-integration.md (user guide) + docs/grok-integration-plan.md
(decisions, verification record, follow-ups).
Tests: test/grok-mode.test.ts, test/grok-cli-resolver.test.ts, plus
extended clamp/system-routes/render-index-html/run-mode-ui/mobile-overview/
local-echo-gating coverage. npm test (the CI gate) green: 5910 tests.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
||
|
|
f44d597450 |
review fixes: every claude create path routes through the workspace-hooks decision
Post-#304 follow-ups. The install-vs-refresh decision (workspaceHooksEnabled, default ON) moved from a session-routes-local helper into hooks-config.ts as applyWorkspaceHooks(workspace, install?), and the claude session-create sites that bypassed it now go through it: cron job fires (cron-service), legacy scheduled-run iterations (runScheduledLoop), and the plan-orchestrator research and planner one-shots. A cron or scheduled run firing in a linked case that never had an interactive session ran hook-blind (no stop for completion detection, no tab alert on a blocking dialog). The shared core also carries the two guards every caller needs: a workspace that no longer exists is skipped (ensureCodemanHooks mkdir -p's, so the boot recovery sweep used to resurrect a deleted repo as an empty tree holding only .claude/settings.local.json), and all errors are swallowed since a create must never fail on hooks. Route handlers keep resolving the setting through their ConfigPort and pass it in; non-route callers omit it and the core reads settings.json itself (absent key or unreadable file = ON). Two adjacent gates tightened in session-routes: - the docker quick-start hooks branch excluded the five external CLIs but let `shell` through, contradicting its own rule that only claude reads .claude hooks; it is now gated on mode === 'claude' - the statusLine exporter call in POST /api/sessions got the same !remote && body.workingDir guard the hooks call got in |
||
|
|
f4dcfbe6ca |
fix(pi): close four mode-list gaps in the pi run mode
Review follow-ups on #282. All four are the same failure shape: a list that enumerates run modes, missed by the sweep that added 'pi'. 1. Cron ignored pi's project-trust clamp. The PR widened CronJobBaseSchema's agentType to accept 'pi' but not the matching clamp beside gemini's, so a non-granted multi-user owner's cron pi job spawned bare `pi` (pi's own defaultProjectTrust, an interactive prompt they can answer "yes" to, which loads and EXECUTES repo-local .pi/extensions TypeScript) while the same user's UI/API launch was forced to --no-approve. The clamp is now a pure exported helper, clampCronExternalCliConfigs(), so both it and gemini's previously untested materialization are pinned. 2. POST /api/sessions/:id/interactive auto-enabled the Ralph tracker for pi: its denylist covered opencode/codex/gemini/antigravity only. The tracker is never fed for an external CLI (_processExpensiveParsers returns early), so a pi session reported ralphEnabled and Ralph UI state no sibling backend shows. 3. REMOTE_CLI_BIN had no pi entry, so buildRemoteCliVersionProbeCommand() returned null and Session.cliVersion stayed blank for every remote-SSH pi session, even though the PR wired the remote launch command and the per-mode override schema field. 4. The desktop home rail's badge map had no pi entry, and its lookup falls back to '', which is what claude renders. A pi session read as Claude there while the tab strip and phone overview badged it correctly. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> |
||
|
|
ccb3afc9ee |
fix(multiuser): close cross-user web-layer scoping holes found in review
The opt-in multi-user feature's only enforcement is web-layer scoping (all sessions share one OS account). An adversarial review found 8 critical + 7 high cross-user holes that defeated it, plus mediums; all fixed here. Single-user (flag-off) behavior stays byte-identical apart from documented consistency deltas. Ownership / confinement: - DELETE /api/sessions (bulk) + /:id now owner-scope / findSessionOrFail - quick-start, cron (create+fire), scheduled runs confine workingDir to the owner's space; case link/docker-link/docker-import confine the host path - resolveCasePath no longer resolves linked cases for non-admins; foreign remote/docker cases are skipped (fall through to the caller's own local case) - history, subagents/workflows, mux-sessions, orchestrator, cron run-history, away-digest, and remote/docker host reads are owner- or admin-scoped Permission policy (section 6.3): - non-granted users are downgraded at every spawn site incl. legacy /api/scheduled, PlanOrchestrator one-shots, remote launch, and the cron-fire gemini/codex bypass switches; resolveClaudeModeForUsername now fails closed Auth / store: - verify-first login throttle (a correct password is never locked out), /ws terminal subject to the change-password lockbox, cookie fast-path re-validates identity live, role/grant changes revoke sessions, admin delete runs the last-admin guard before any teardown - users.json: distinguish missing (ENOENT) from corrupt/unreadable so a bad read can't overwrite all accounts; unique per-process temp write path Event streams: - debounced session:updated + batched task:updated, clipboard, and push notifications route by owner (fail closed); getLightState hides machine-wide globalStats from non-admins Tests: two suites updated to assert the fixed (secure) behavior. tsc, eslint, and test:ci all green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
453605a58f |
feat(multiuser): phase 3, ownership threading + scoping
Threads per-user ownership through sessions, cases, cron, and the permission policy. All scoping is a no-op in single-user mode (isMultiUserMode() guards). Sessions - Session.owner stamped at every create path from req.authUser / job.owner: POST /api/sessions, /api/run, /api/quick-start, ralph start, cron launch, plan generation. Round-trips through recovery (MuxSession.owner mirror, read muxSession.owner ?? savedState?.owner) and the mux layer. - findSessionOrFail(ctx, id, req) now does a NOT_FOUND owner check (never 403, so other users' session existence is not leaked); wired at ~50 call sites. - List endpoints filtered by owner: GET /api/sessions, /api/sessions/unified (live+persisted+lifecycle scoped, host-wide transcripts admin-only), cron jobs. Permission policy (section 6.3) - resolveClaudeModeForUsername wraps getClaudeModeConfig at every spawn site so a non-granted user is forced to --permission-mode auto (bypass -> auto), including recovery (or a reboot would un-downgrade). buildPromptArgs now respects the session's claudeMode, closing the one-shot (runPrompt) bypass hole. - Shell mode and cron launchCommand require canBypassPermissions: 403 at POST /api/sessions, /api/quick-start create, cron job create, AND cron fire time (re-checked against the owner's current grant). Cases - resolveCasesDir(user): per-user ~/codeman-users/<name>/cases in multi-user, the shared ~/codeman-cases otherwise. All case CRUD + ralph + plan + quick-start resolve through it. resolveCasePath is owner-aware. - GET /api/cases scoped per user (own folders; legacy linked cases admin-only; remote/docker cases owner-filtered). RemoteCase/DockerCase gain owner, stamped at link/quickcreate/import. - Remote + Docker host CRUD is admin-only. - Non-admin workingDir confinement (the linchpin): realpath must resolve inside the user's space, enforced at POST /api/sessions and /api/run BEFORE any disk write. Limits - sessionCapacityState / sessionCapacityMessage centralize the global + per-user cap (CODEMAN_MAX_SESSIONS_PER_USER, default global/2), replacing the 6 copy-pasted MAX_CONCURRENT_SESSIONS checks. Tests: test/ownership-scoping.test.ts (case isolation, host-CRUD gate, workingDir + shell gates, and the scoping helpers). Deferred to phase 4: WS owner gate, SSE fan-out filtering, file-route preview/thumbnail helper scoping, push routing. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
84ab4ff07b |
fix(review): harden cron security, session lifecycle, skip policy (PR #141)
- Reject multi-line prompts end-to-end: schema refines on promptText/ launchCommand, runtime check in resolvePrompt (prompt-file content; trailing newlines tolerated), matching cron-ui form validation — delivery is single-line only, so multi-line was silently corrupted (typed mode fused lines, paste mode submitted partials) - Close the workingDir confinement bypass (arbitrary server-side file read, e.g. workingDir=/proc + /proc/self/environ): realpath-resolve workingDir before the containment check, reject '/' and blocked/pseudo-fs trees (/proc, /sys, /dev + the attachment-guard blocklist) at fire time AND at job create/update (workingDir must exist and be a directory) - Session lifecycle: new per-job autoClosePreviousSession (default true, recurring schedules only; ignored for 'once') — the previous run's still-open session is closed via the normal cleanupSession path when the next run fires; UI switch added; 50-session cap math documented in docs/cron-guide.md §8 - skip_if_same_agent_running: count only live sessions (exclude stopped/error dead tabs), exclude sessions created by this job's own runs (fixes the fire-once-then-skip-forever self-deadlock), and a skipped 'once' job stays armed and retries next tick instead of being consumed; liveness filter mirrored in cron-ui _countActiveAgents - Wire launchCommand (was accepted+documented but dead): shell mode sends it via writeViaMux as the first input line after startShell readiness (single-line, schema-enforced); form field shown for shell agent type - Record delivery failures: a false writeViaMux result now fails the run instead of recording a false 'prompt_sent' - Cap saved jobs at MAX_CRON_JOBS (100) to bound state.json growth - Surface field-specific schema messages (drop parseBody custom errorMessage on cron create/update) - Tests: workingDir create/update validation, /proc bypass regression, single-line enforcement (schema+runtime+trailing-newline tolerance), live/own-session skip filtering, once-skip re-arm, auto-close on/off/once, job-count cap Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> |
||
|
|
d9c2c6420d |
fix(cron): harden cron fixes against adversarial-review findings
Two blind adversarial reviewers found real holes in the prior cron commits:
SECURITY (was CRITICAL): the prompt-file guard was blocklist-only by default,
so promptFilePath:/proc/self/environ leaked the SERVER PROCESS's entire
environment (every secret) into the agent session, and /dev/zero or a FIFO
caused an unbounded readFile → OOM/hang DoS. A denylist is the wrong posture
for an exfil-into-LLM sink. resolveSafePromptPath now:
- confines the realpath-resolved file to the job's working dir (ALLOWLIST) —
closes /proc, /dev, other homes, modern cloud-cred paths, and symlink escapes
- requires a regular file (rejects dirs/FIFOs/char devices)
- caps the read at MAX_PROMPT_FILE_BYTES (1 MiB)
- keeps the /etc,/root,secrets blocklist as defense-in-depth
LOGIC:
- once-rearm (was MED, defeated in prod): the edit UI round-trips the full
job, so the field-PRESENCE re-arm check always fired → a renamed fired
once-job could be resurrected via edit→re-enable. Now compares schedule
VALUES; an unchanged schedule never re-arms.
- skipped-run history (was HIGH): recording a skip every tick was unbounded
state.json growth. Now coalesces consecutive skips (one record per streak)
and prunes global run history to MAX_CRON_RUN_HISTORY (500), covering the
launch path too.
- skip bookkeeping (was MED): a skip no longer advances lastRunAt (nothing
ran); lastStatus still reflects 'skipped'.
Regression tests added/updated (43 pass): /proc/self/environ + outside-workspace
+ symlink-escape + non-regular + oversized all blocked, in-workspace file
passes; UI-path once resurrection blocked; consecutive skips coalesce to one
record; skip leaves lastRunAt null.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU
|
||
|
|
6082bceee6 |
fix(cron): close three MED cron-job defects
1. once-rearm on edit: editing any field of a finished one-time job reset completedOnce, silently resurrecting it. Now only a SCHEDULE edit (scheduleType/runAt/interval/daily/weekly) re-arms a completed once job; cosmetic edits (rename/notes) leave completedOnce intact. 2. update-validation gap: CronJobUpdateSchema = .partial() drops the cross-field superRefine, so a PUT switching scheduleType without its dependent field produced a dead enabled job (nextRunAt:null). updateJob now re-validates the MERGED job against the full CronJobSchema and throws 400 on inconsistency, leaving the stored job untouched. 3. concurrency-skip silent starvation: skip_if_same_agent_running advanced the schedule but wrote no run record, so a perpetually-skipped job had empty history. Now records a 'skipped' run (new CronJobRunStatus) + lastStatus. Tests updated/added in cron-service.test.ts (37 pass): once non-schedule edit preserves completedOnce, schedule edit re-arms, inconsistent partial update is rejected with the stored job untouched, and the skip path records a skipped run. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU |
||
|
|
40e26c5422 |
fix(cron): confine cron prompt-file reads to block sensitive paths
A cron job's promptFilePath is user-supplied via the API and was read with an unconfined readFile of any absolute path, so a hostile job config could exfil arbitrary host files (e.g. /etc/passwd, SSH keys) into a Claude session. Guard the read in resolvePrompt by mirroring the attachment-serving guard (resolveServableAttachmentPath in file-routes): realpath-resolve the path, then reject via the shared blocklist (/etc, /root, secret locations) plus the optional workspace-confinement toggle before reading. Regression tests in cron-service.test.ts: blocks /etc/passwd (the live repro) and /root/*, fails cleanly on a missing file, and still allows an ordinary prompt file outside the blocklist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU |
||
|
|
9feaa0d6e5 |
refactor(cron): rename scheduler feature to cron
Rename the recurring-jobs feature scheduler->cron to disambiguate from the legacy ScheduledRun system (/api/scheduled), which is left untouched: - ScheduledJob->CronJob, SchedulerService->CronService - /api/scheduler/jobs -> /api/cron/jobs; SSE scheduler:* -> cron:* - state keys cronJobs/cronJobRuns - files moved to src/cron/, cron-routes.ts, cron-port.ts, types/cron.ts - frontend cron-ui.js, #cronModal, menu "Cron" - docs moved to docs/cron-discovery.md + docs/cron-build-brief.md, README guides - new tests: cron-service.test.ts, cron-time.test.ts Green: tsc, lint, frontend-syntax, format, 30 cron + 9 legacy scheduled-runs tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PmvZR12aX2v8K7YhqxPUAU |