How to provide additional dev tools? #231
Labels
No labels
Agent
Ignore
Agent
In Progress
Agent
Planned
Agent
Planning
Agent
Ready
Agent
Triage
Compat
Breaking
Kind
Bug
Kind
Documentation
Kind
Enhancement
Kind
Feature
Kind
Security
Kind
Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
lucas/omp-deck#231
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
When starting to use omp-deck with more and more projects, the amount of different dev tools required keeps increasing. Installing all of them in the Dockerfile is annoying for multiple reasons:
The agreed answer is mise: one small static binary in the image, with each project declaring its own tools in a file it owns. The deck integrates it so the agent mostly does not have to think about it — tools resolve per directory, installs persist across container recreation, and the deck provisions a workspace's toolchain without being asked.
Two constraints shape the whole design, both verified in this repository:
InProcessAgentBridge.createSession(apps/server/src/bridge/in-process.ts:290) never passesenvtocreateAgentSession, and the onlyPATHin the repo isDockerfile:68,151. There is no per-session environment, somise activate(one env per shell) is not available as the integration path. mise shims are, because a shim resolves the tool version from the invocation's own directory: a single globalPATHentry gives two concurrent sessions in two workspaces two different toolchains.forgejo/checkout.tsputs them in<dataDir>/checkouts/<owner>/<repo>/<kind>-<n>, detached atrefs/pull/<n>/head. mise's own docs state thatmise install,mise execandmise runautomatically trust the config they load, so naively runningmise installin a session's cwd would execute whatever a pull-request author pushed, and silently mark it trusted. Installs must therefore never be driven by a session's cwd.Scope of this issue: the image/persistence layer, deck-driven provisioning, a per-workspace config pasted in Settings, and the session context that tells the agent how tools are provided. The "detect a missing tool and open a PR adjusting the mise config" loop is deliberately not part of it. Nor is shrinking the image (
chromium,build-essential,/opt/pytools, the npm language-server globals); every tool baked in today stays baked in.Plan
1. Put
misein the image, with shims first onPATHdocker/entrypoint.sh:93runs the deck asenv HOME=/data bun src/index.ts, and/datais the bind-mounted volume. Every mise directory defaults to a location under$HOME— data and shims in~/.local/share/mise, global config in~/.config/mise, trust decisions and tracked config paths in~/.local/state/mise, cache in~/.cache/mise— so noMISE_*_DIRoverride is needed: everything mise installs or records already persists on the volume. Do not add any.In
Dockerfile, add a build stage after thejuststage (line 84), mirroring that stage exactly — separate so the download caches independently of runtime changes:MISE_VERSIONandMISE_INSTALL_PATHare both honoured by that installer (verified against the script itself). Bump the pin through the existing.renovatercmachinery if it picks it up; otherwise it is a manual bump like theoven/buntag.In the
runtimestage, next to the other stage copies atDockerfile:143-146:Replace the
ENVblock atDockerfile:149-151so the shim directory leadsPATH:MISE_NOT_FOUND_AUTO_INSTALL=0is load-bearing, not tidiness: with mise's default, a shim running inside a PR checkout resolves that checkout'smise.tomland installs whatever version it names, which is exactly the PR-driven install this design rules out.exec_auto_installstays at its defaulttrue, so the agent can still deliberately runmise exec <tool>@<ver> -- …and have it resolve. Set this as an imageENVrather than a managed global config file: it is a security property, and anENVcannot be overridden by anything the deck or a project writes under/data.Leave
not_found_system_fallbackat its defaulttrue. Turning it off would make an unresolvable shim fail — but a shim exists for every executable of every tool mise has ever installed, so once any project installsnode, a hard-failingnodeshim would break every other project that relies on the image's Debiannodejs. The cost of keeping the fallback is that a declared-but-uninstalled tool can silently run a system binary of a different version; section 5's context block tells the agent to check withmise whichrather thancommand -v.No change to
docker/entrypoint.sh. mise creates its own directories under/data, which the deck UID already owns.2. Store a per-workspace mise config
A workspace may carry a config pasted in Settings, used on top of (or instead of) whatever the repo declares. Follow the
victorialogs_enabledchain end to end; the next free migration number is026(025-forgejo-trigger-quota-notice.sqlis the latest).apps/server/src/db/migrations/026-workspace-mise-config.sql:apps/server/src/db/workspaces.ts: addmise_config: string | nullandmise_config_mode: stringtoWorkspaceRow(line 22), both names toWORKSPACE_COLUMNS(line 44), and torowToWorkspace(line 32)miseConfig: r.mise_config ?? ""andmiseConfigMode: r.mise_config_mode === "replace" ? "replace" : "override". Add next tosetWorkspaceVictoriaLogsEnabled:storing
config.trim() === "" ? null : configandmode, returninggetWorkspace(workspaceId), and returningundefinedwhen the workspace does not exist — identical in shape to the VictoriaLogs setter.packages/protocol/src/index.ts: addexport type MiseConfigMode = "override" | "replace";and extendWorkspace(line 54) withmiseConfig: string;(empty string = none) andmiseConfigMode: MiseConfigMode;WorkspaceDetails(line 96) with the same two fields plusmise: MiseStatus;(section 4)UpdateWorkspaceRequest(line 124) withmiseConfig?: string;andmiseConfigMode?: MiseConfigMode;apps/server/src/routes-workspaces.ts, inPATCH /workspaces/:id(line 104): extend the required-field guard at line 112 to mentionmiseConfig, validatemiseConfigis a string andmiseConfigModeis"override" | "replace"(400 otherwise), and add a branch after the VictoriaLogs one callingsetWorkspaceMiseConfig(id, body.miseConfig ?? ws.miseConfig, body.miseConfigMode ?? ws.miseConfigMode). Either field alone is a valid update. After a successful write, callvoid provisionWorkspace(ws)(section 3) so the change takes effect without reopening a session, thennotifyWorkspacesChanged()as the existing branches do.apps/server/src/workspace-details.ts: addmiseConfig,miseConfigModeandmise: miseStatus(ws.id)beside the existingvictorialogsEnabledfield at line 78.apps/web/src/lib/api.ts, besidesetWorkspaceVictoriaLogs(line 63):PATCHing
{ miseConfig, miseConfigMode }.apps/web/src/components/settings/WorkspacesPanel.tsx: add a<dt>mise</dt>/<dd>pair to the<dl>after the VictoriaLogs row (ends line 322). Unlike the two checkboxes it needs an explicit commit, so: a<textarea>bound to local state seeded fromws.miseConfig, a<select>withoverride/replace, a Save button disabled whilebusyor unchanged, and the lineApplies immediately; the workspace is re-provisioned in the background.Renderws.miseunderneath asphaseplus, whenphase === "failed", the recorded error. Label the modes in the UI asoverride — merged over the repo's own configandreplace — the repo's own config is ignored.3. Provision a workspace's toolchain
New directory
apps/server/src/mise/. Nothing here may ever take instructions from a session's cwd.3a.
apps/server/src/mise/materialize.tsWrites the deck-managed files that make mise resolve the right versions in a directory. Exports:
Behaviour:
cfg === nullorcfg.text.trim() === "": delete<dir>/mise.local.tomland<dir>/.miserc.toml, but only if the file exists and its first line isMANAGED_HEADER. Return.Otherwise write
<dir>/mise.local.tomlasMANAGED_HEADER + "\n" + cfg.text. mise readsmise.local.tomlahead ofmise.tomlin the same directory and documents it as the file for personal, uncommitted overrides, which is exactly this.[tools]merges per tool, so a pastednodeentry replaces the repo'snodeand leaves the repo's other tools in force.cfg.mode === "replace": additionally write<dir>/.miserc.toml:override_config_filenamesmakes mise load only files with those names in each directory, andoverride_tool_versions_filenames = ["none"]stops.tool-versionsbeing read; both are settable in a project.miserc.toml(rc = truein mise's settings schema), and.miserc.tomlcan only select which config files load, so it carries no code and needs no trust.cfg.mode === "override": delete a managed.miserc.tomlif present.Before overwriting either file, if it exists and does not start with
MANAGED_HEADER, throwMiseConfigConflictnaming the path, and write nothing. A user's ownmise.local.tomlis never clobbered.When
<dir>/.gitexists, ensure.git/info/excludecontains a blockappended once, keyed on that comment line, so the managed files never show up as untracked. Create
.git/info/if missing.3b.
apps/server/src/mise/provision.tsState lives in two module-level maps keyed by workspace id: the
MiseStatusrecord, and the in-flightPromise<void>so concurrent sessions in one workspace never run two installs (return the existing promise instead of starting a second). Both are in-memory; after a deck restart a workspace reportsidleuntil its next session, which is correct — provisioning is idempotent and cheap once warm.provisionWorkspace(ws)steps, in order:Set phase
running.await materializeMiseConfig(ws.path, ws.miseConfig ? { text: ws.miseConfig, mode: ws.miseConfigMode } : null). Do this first, so version resolution in the workspace is correct even while the install is still running. AMiseConfigConflicthere sets phasefailedwith the thrown message and stops.Build a scratch config root at
<getDataDir()>/mise/workspaces/<ws.id>:rm -rfthenmkdir -p. Installs are driven from this directory and never fromws.pathor a checkout, because the working tree may sit on any branch and a checkout holds a pull request's code.Unless the mode is
replace, copy the default branch's config into the scratch root. Resolve a ref by trying, in order,origin/HEADandorigin/<default branch>; userunGitfromapps/server/src/bridge/git-tools/run.tswithreadOnly: true— it spawns git as an argv array with no shell, which is what every other git caller in the server uses. For each candidate relative pathrun
git show <ref>:<rel>and, on exit 0, write the output into the scratch root at the same relative path. Copy every one that exists rather than picking a winner: that keeps mise's own precedence rules intact instead of inventing one. If neither ref resolves — a workspace with no remote, or not a git work tree at all — fall back to copying the same paths fromws.pathon disk; there is no pull-request risk in a repo that has no remote. Record the copied paths assources.If the workspace has a pasted config, write it into the scratch root as
mise.local.toml(header included, same as the materialized one). Inreplacemode step 4 copied nothing, so this is the only config present and no scratch.miserc.tomlis needed.If the scratch root is empty, set phase
idlewith emptysourcesand return — nothing to provision.Run
mise install --yeswithcwd= the scratch root,MISE_CEILING_PATHS=<getDataDir()>/mise/workspacesin the environment so the directory walk cannot pick up stray config from/data, and a 30-minute timeout. Spawn it as an argv array viaBun.spawn, matching howgit-tools/run.tsspawns git; do not go through a shell. Runningmise installhere auto-trusts the scratch config, which is intended and safe: its content came from the repository's default branch or from the user's own paste.Exit 0 → phase
ok. Non-zero, timeout, or a spawn failure → phasefailed,errorset to the last 2000 characters of stderr, and onelog.warnnaming the workspace. A failed provision never throws to the caller and never blocks a session.kickoffMiseProvisioning(workspaceId)looks the workspace up and callsvoid provisionWorkspace(ws)without awaiting. Anullid is a no-op.3c. Wire it into session creation
In
apps/server/src/bridge/in-process.ts,integrationWorkspaceIdis already resolved at line 312 — the trigger path throughopts.placement.workspaceId, the interactive path throughgetWorkspaceByPath(opts.cwd). Immediately after it, addkickoffMiseProvisioning(integrationWorkspaceId). Do the same at the corresponding line inresumeSession(around line 464). Not awaited:POST /sessions(apps/server/src/routes.ts:101) awaitscreateSession, and the web client awaits that HTTP call (apps/web/src/lib/store.ts:382), so a cold Rust or Python toolchain would otherwise freeze "New session" for minutes.createSessionis also reached fromforgejo/dispatcher.ts:2049,:2156,:2936andforgejo/plan-issue.ts:199; all of them resolve their workspace through the same variable, so this one call site covers every path.3d. Apply the workspace config inside trigger checkouts
A checkout lives outside
workspace.path, so nothing materialized in the workspace reaches it. Inapps/server/src/forgejo/checkout.ts, after the local-branch cleanup at the end ofcreateCheckoutand at the same point inrestoreCheckout, resolve the workspace withgetWorkspaceByPath(opts.workspacePath)— no signature change is needed — and callmaterializeMiseConfig(dest, …)with its pasted config, or withnullwhen there is none.createEmptyCheckouthas no workspace and is left alone. Inreplacemode this also makes the pull request's ownmise.tomlinert inside the clone, which is the stronger behaviour, not a weaker one. Swallow and log aMiseConfigConflicthere rather than failing the checkout: a clone is disposable and a broken toolchain must not block a trigger session from opening.4. Tell the agent where tools come from
The guidance must not go into
DEFAULT_PRELUDE:getEffectivePrelude()(apps/server/src/orientation-store.ts) returns<dataDir>/prelude.mdwhenever it exists, so anyone who has customised their prelude would never see an edit to the default. Add it as its own entry in the system-prompt array instead, the wayforgejoContextalready is.New
apps/server/src/mise/system-context.tsexportingmiseSessionContext(workspaceId: string | null): string | null, returningnullwhen there is no workspace. Otherwise the static block below, followed by a status line built frommiseStatus(workspaceId)—Workspace tool provisioning: <phase>., plusDriven from: <sources joined>.when non-empty, plus the recorded error whenfailed.Wire it into both
systemPromptarrays inin-process.ts— line 334 and the resume copy near line 480 — directly after theforgejoContextentry:Call
miseSessionContext(integrationWorkspaceId)inside the closure, not before it, so the status refreshes as provisioning progresses: the resolver is invoked per turn preparation (node_modules/@oh-my-pi/pi-coding-agent/src/sdk.ts:4411), which is also why the existingideaBridgeRef.current?.isEnabled()check in that array works despite the ref beingnullat create time. Unverified — confirm first: if the closure turns out to run once at creation, the status cannot update that way; in that case keep the static block in the closure and move the status line into a small extension registered alongsidegatein theextensions:array (line 343) that handlesbefore_agent_startand returns{ systemPrompt: [...event.systemPrompt, statusLine] }. Do not add a push-to-live-session mechanism for this.5. Verification
Everything below runs against a container built from this branch. Prerequisite for all of it:
docker build -t omp-deck .and a run with-v <host-dir>:/dataas theDockerfileheader describes.mise --versionprints the pinned2026.11.7, andmise doctorreports its data directory as/data/.local/share/miseand its shims directory as/data/.local/share/mise/shims.echo $PATHshows that shims path first. Recreate the container (docker rm -f+ run again) and confirmmise ls --globalstill lists anything installed before the recreate.mise.tomldeclaring a tool that is not installed, running the tool directly must not install it;mise exec <tool>@<version> -- <tool> --versionmust install and print a version.mise.tomlwith, say,[tools]\njq = "1.7.1". Open a session in it. Within the install's runtime,mise which jqinside the workspace resolves to a path under/data/.local/share/mise/installs/jq/1.7.1, andjq --versionprintsjq-1.7.1rather than the image's Debianjq.GET /api/workspaces/detailsshows that workspace'smise.phaseasokwithmise.tomlinsources.mise.tomlto a version that is not installed, open a pull request, and let a trigger session start. In its checkout, the tool must not be installed:ls /data/.local/share/mise/installs/jqstill shows only the default branch's version, and the deck log contains nomise installfor the checkout path. This is the security property the design exists for; check it explicitly.[tools]\njq = "1.7"for a workspace whose repo declaresjqand something else, leave the mode onoverride, Save.<workspace>/mise.local.tomlappears carrying the managed header,git statusin the workspace reports a clean tree (the file is excluded),mise which jqresolves to the pasted version, and the repo's other tool still resolves.replaceand Save.<workspace>/.miserc.tomlappears withoverride_config_filenames = ["mise.local.toml"],mise config lsin the workspace lists onlymise.local.toml, and the tool the repo declared but the paste does not no longer resolves. Clear the textarea and Save: both managed files are gone and the repo's config resolves again.mise.local.tomlexists in<dataDir>/checkouts/<owner>/<repo>/pr-<n>/with the managed header.mise.local.tomlwithout the managed header into a workspace, then Save a paste for it. The file must be untouched, and that workspace'smise.phasemust befailedwith an error naming the path.[tools]\nnot-a-real-tool = "1"), Save, and open a session. Session creation must succeed promptly, the session's system context must carryWorkspace tool provisioning: failedwith the error text, and the deck log must carry the warning.bun testin the repo root passes, includingapps/server/src/forgejo/dispatcher.test.ts, whose bridge fake exercises thecreateSessionpath the provisioning kickoff was added to.Assumptions & contingencies
.miserc.tomlhonoursoverride_config_filenamesfrom a project directory. Both settings are markedrc = truein mise's settings schema, which is what permits them in a.miserc.toml. If step 6 of Verification shows the repo'smise.tomlstill loading inreplacemode, fall back toignored_config_pathsin the same.miserc.toml, listing the repo config paths the deck found in step 4 of provisioning; it isrc = trueas well and its relative paths resolve from the directory holding the file.git show origin/HEAD:…resolves in a workspace. This depends onrefs/remotes/origin/HEADexisting, which a plaingit clonecreates but a repo set up by hand may lack. The ordered fallback toorigin/<default branch>and then to the on-disk files covers it, so no step stalls; iforigin/HEADturns out to be missing often in practice, read the default branch name from the existing Forgejo repo metadata rather than adding agit remote set-headcall — the deck must not mutate the user's repository.git fetchthe user runs.@omp-deck What are some ideas to provide dev tools in a way other than installing them in
DockerfileIdeas, grouped by the mechanism they use. Most of them are complementary rather than exclusive — I'd expect the end state to be two or three of these combined.
First, one structural fact that makes several of these cheap: the entrypoint runs the deck (and therefore the agent's shell) with
HOME=/data, and/datais the bind-mounted volume. So anything a tool installs under$HOMEalready persists across container recreation, without touching the image. The current image fights that:CARGO_HOME=/usr/local/cargoand the pytools venv in/opt/pytoolsare baked into layers, socargo install/pip installby the agent lands in the ephemeral upper layer and is lost on recreate. Moving the mutable toolchain roots under/datais a precondition for most of the "install on demand" options below.1. Per-project toolchain manager (
mise/devbox/nix)Install exactly one small bootstrapper in the image; projects declare their tools in a file they own (
mise.toml,.tool-versions,devbox.json,flake.nix). Cache under/data(MISE_DATA_DIR=/data/.local/share/mise) so the first session pays the download and later ones don't.miseis the cheapest to adopt (single static binary, Debian-friendly, has a large builtin registry plus ubi/cargo/npm/pipx backends);nixis the most reproducible but is a much bigger conceptual and disk commitmentmise activatein the shell init, ormise x --wrappers). A deck-side "runmise installwhen a session's cwd changes" hook would make this invisible.2. Ephemeral per-invocation runners
Lean on the runners that are already there (or nearly):
bunx/npx,uvx/pipx run,go run,cargo-binstall.uvx ruff checkreplaces the entirepytoolsstage;npxalready replaces the globaltypescript-language-server/pyrightinstalls./data(UV_CACHE_DIR,npm_config_cache,XDG_CACHE_HOME) or every invocation re-downloadsAGENTS.mdproblem as much as an infra one.3. Host-managed tool volume
Mount a host directory of static binaries read-only at e.g.
/opt/host-toolsand prepend it toPATHvia compose. Host stays the single place you install things.oven/bun. Dynamically linked Arch binaries will fail against bookworm's older glibc. This only works reliably for statically linked tools (Go, most Rust,jq,just, …), which is a real but limited subset. Mounting host/usrwholesale is not viable.4. Derived image instead of upstream image
Keep the published
omp-deckimage minimal (bun + git + ripgrep + the runtime essentials) and document a one-line local Dockerfile:plus optional
--build-arg/build targets for the common bundles (rust,python,web). Doesn't solve per-project granularity, but it does solve "requires a change in omp-deck" and lets the published image shrink a lot (build-essential+chromium+ rustup dominate the current size).5. Tools as containers (docker-out-of-docker)
Give the agent a
docker/podmanCLI and have it run tools as throwaway containers against the bind-mounted workspace. Projects declare a toolbox image; omp-deck ships nothing.-v $PWD:/winside the container needs the host path, not the container path.6. Full devcontainer per project
Let the project's existing
.devcontainer/define the environment, and have the deck run the session inside that container rather than in its own.7. Project setup hook
A conventional per-project script (
.omp-deck/setup.sh, or a routine triggered on first session in a workspace) that the deck runs once per workspace and caches a marker for. Not a tool-provisioning mechanism itself — it's the trigger that makes 1/2/3 ergonomic, and it's the smallest thing that gives projects control without an omp-deck change.8. Reach back to the host over SSH
The deck already ships
openssh-clientand the agent hasssh://support. A project could declare "run builds on the host". Avoids duplication entirely, but it's a shell on the dev machine from inside the sandbox — it throws away the containment that the container was for. Mentioning for completeness; I wouldn't.What I'd actually do
/data(CARGO_HOME,UV_CACHE_DIR, npm cache,MISE_DATA_DIR,XDG_*). Cheap, no downside, and every other option depends on it.pytoolsvenv in favour ofnpx/uvx.chromiumandbuild-essentialdeserve a hard look; both are large and probably not universally needed.mise(oruv+mise) as the single provisioning primitive, plus a setup hook (7) so entering a workspace with amise.tomlprovisions it automatically into the persisted volume.That leaves omp-deck with one mechanism to maintain, keeps the image boring, and makes the tool list a property of each project's repo.
Open questions worth deciding before implementing: whether tool caches are shared across projects or isolated per workspace (shared is smaller and faster, isolated avoids cross-project version fights); whether first-session provisioning may block or must be backgrounded; and whether sessions are allowed network access for on-demand installs at all.
Mise sounds like a good solution, especially when integrated into the session handling in a way that the harness handles it mostly transparently. Including things like "if a tool is missing and evidence suggests the user wants it available, open a pr to adjust the mise config"
Agreed on mise. Here's what "mostly transparent" has to mean concretely, given how sessions actually work in this repo, plus a design for the PR-opening loop.
The constraint that shapes the whole design
Sessions run in-process in the deck server (
InProcessAgentBridge.createSession), so every session shares oneprocess.env. There is no per-session environment today, and adding one means reaching into how the SDK spawns the bash tool. That rules outmise activate(shell-hook based, one env per shell) as the integration path.Use shims instead. A shim resolves the tool version from the invocation's current directory, not from an ambient activated env (mise docs). So one global
PATHentry —$MISE_DATA_DIR/shims, prepended ahead of/usr/local/cargo/binand friends — gives two concurrent sessions in two different workspaces two different toolchains, with zero per-session plumbing. That's the piece that makes this cheap.Known shim limitations, worth accepting explicitly rather than discovering later:
mise.toml's[env]into the ambient environment for processes that aren't themselves shimmedPATHor expectGOROOT/VIRTUAL_ENV-shaped discovery can behave oddly behind a shimmise exec --/mise run, which the prelude should teach as the fallbackLayering
0 — Image + persistence.
misebinary in the image (one static binary).MISE_DATA_DIR/MISE_CACHE_DIRunder/dataso installs survive container recreation. Shims first onPATH. This alone is a small change and is a prerequisite for everything else.1 — Provision on session create.
createSessionalready hasopts.cwd. If the workspace hasmise.toml/.tool-versions, runmise install --locked.Two things to get right here:
session_startinject the status into context ("provisioning node 24, go 1.24 — in progress") so the agent doesn't start a build that fails for a reason it can't see.mise.tomlcan run arbitrary code (tasks, hooks, source-building backends). Auto-runningmise installon any workspace the deck can see is remote code execution from whatever repo got checked out — including, notably, trigger-session checkouts of PR branches from the Forgejo dispatcher. mise has its ownmise trustmodel; the deck needs to mirror it as an explicit per-workspace opt-in stored with the workspace record and visible in the UI, and trigger checkouts should default to untrusted regardless.2 — Context, not probing.
session_startinjects the resolved tool list. Prelude guidance: a missing tool meansmise, neverapt-get(which won't persist anyway). Cheap, and it removes a whole class ofwhich fooround-trips.3 — Missing tool → PR. This is the interesting part, and the failure mode is obvious: an agent that opens a PR because it fat-fingered
pyhton. So the loop needs real evidence gating.Signal. The extension API already has what's needed — the
tool_callhook (seestarter-extensions/maintenance-gate) can watchbashresults for exit 127 /command not foundand record(workspace, binary, command)into a small ledger.Filters, all of which must pass:
PATHor in the registrypackage.jsonscripts,justfile,Makefile, CI workflow. A project that runsgolangci-lintin CI clearly wants it; a speculativewhich fzfdoes not.Escalating response, so the PR is the last resort rather than the first:
mise x <tool>@latest -- …, change nothing/data— persists, still no PRmise use --pin <tool>@<version>and open a PR via the existinggit_*+ Forgejo chainTwo judgement calls that decide whether this is useful or annoying:
mise.toml; personal preferences (fzf,delta, a pager) belong in the global config. Conflating them produces PRs nobody wants. My proposed rule: a PR against the project only when the tool appears in the project's own files.turn_end, never one PR per discovery.Pin versions, don't float — a PR that writes
node = "latest"is worse than no PR.What mise doesn't cover
System-level
aptpackages (libssl-dev, headless chromium) and the C toolchain. mise'scargo:/npm:backends build from source and therefore needbuild-essential, which is the main argument for keeping it in the image despite the size. So the derived-image escape hatch from my earlier comment stays necessary; mise shrinks the problem, it doesn't delete it. Same for air-gapped or offline sessions, which would need a pre-warmed/data.Smallest thing worth shipping
Layers 0 and 2, plus Layer 1 behind an explicit per-workspace trust toggle. That already delivers the original three complaints — small image, project-owned tool list, no omp-deck change per project.
I'd hold Layer 3 until there's a ledger of what actually goes missing in practice. The detector is only as good as its evidence rules, and designing those from imagination rather than from a few weeks of recorded
command not foundevents is how you get a bot that opens PRs for typos. The ledger from Layer 3's signal half is cheap to build and useful on its own — ship the observation, defer the action.Open questions I'd want settled before implementation: trust model for trigger-session checkouts specifically; whether tool installs are shared across workspaces or isolated; whether provisioning may ever block session start; and whether
mise.lockis committed per project or left out.Happy to split these into separate issues (image/persistence, session provisioning + trust, missing-tool ledger) if you want them tracked independently.