Repository navigation
The opaque paths want a rendered-id type, not WorkspaceId: what happens to an id that fails the check? #535
Description
Where this comes from
#316 asked to thread WorkspaceId through ~69 signatures taking
workspace_id: &str, so an unvalidated string could no longer satisfy them.
#534 migrated 5 and found the
ticket has a boundary through the middle of it. Two independent traces of every
production call site agree:
| build agent | reviewer (independent audit) | |
|---|---|---|
| TRIPLE (a triple is in hand) | 2 | 3 |
| OPAQUE (only a rendered id) | 60 | 59 |
| MIXED | 0 | 0 |
The reviewer established something stronger than absence: 0 MIXED is
unreachable, not merely unobserved. There are exactly two places a triple could
survive downward (launch::place_triple, dl::target::triple) and each reduces
its WorkspaceId to a String inside its own body. No production call site
anywhere passes workspace.value() into any of the 62.
And three of the opaque paths would actively regress if migrated.
listing::unsaved_work_in, reached from the dl <ws> rm guard, looks records up
by recorded id; demanding a triple would miss every old-scheme record and
silently disarm the unsaved-work guard. remove_workspace_by_id has that exact
regression written in its own doc comment.
So #316 as literally written is not achievable, and it closed with #534 rather
than parking an impossible target on map #299. This ticket is where the finding
lives instead.
The question
The 59 opaque paths do not want a WorkspaceId. They want a rendered-id
newtype — a validated id, as against a validated triple — whose
constructors are the only legitimate producers of one.
The id reaching those paths is often not a triple's derivation at all. It can be:
- a triple's derivation (
WorkspaceId::value), - a ref-less URL's hashed id,
- a path spec's directory leaf,
- a bare name the user typed,
- an id
metadata.jsonrecorded under a derivation that has since moved.
Placement already carries a String for exactly this reason, and the comment
beside it says a triple is not recoverable from an id. dl --purge and
dl --prune are defined over devpod workspaces and clone directories with no
record at all; dl <ws> kill skips resolution deliberately.
The design decision is not the type, it is the behaviour at each boundary —
above all: what should happen to a devpod list entry that fails the check? Today
nothing checks, so nothing can fail. That is the question this ticket exists to
settle, and it is why #534 did not answer it in passing.
The small mechanical half, which is not blocked on that
Two triple-shaped signatures survive and are invisible to a workspace_id: &str
grep, so they escaped #316's census:
workspace_clone::prepare_cold— taking loose strings makes
PrepareColdError::UnsafeTripleproduction-unreachable.lifecycle::state::recorded_devpod_workspace_id— reached from a call site
Derive a workspace id once, and make the triple the way in #534 itself wrote, flattening a&WorkspaceIdinto three strings two lines
after removing the same shape next door.
Both can be migrated the way #534 migrated its five, independently of the design
question above.
Three smaller residues from #534's review
WorktreeInfo::as_an_older_dl_recorded_it(a#[cfg(test)]builder for the
real state where a record's triple and id disagree) is used at three sites where
the halves do agree — the escape hatch spent as the general-purpose builder.- Three new hardcoded golden ids sit outside
domain::workspace_id, against a
rule the repo states verbatim in a sibling module. - The sibling-clone test added beside the caching test does not discriminate: it
passes on main too. The pointer-identity test is the one doing the work.
What #534 did land, for context
WorkspaceId derives once at new() instead of on every value() call (a
SHA-256 over the folded triple, three slug passes and a truncation budget, which a
launch asks for at every layer it hands the id down through), value() lends
&str, and the three signatures that genuinely had a triple in hand stopped
flattening it. Deliberately no Deref<Target = str>: it would let a validated
triple be spent as a plain string by coercion at every call site, which is the
erasure the work is undoing.
Activity
blooop commentedon Aug 30, 2026
Not an answer, a view from #472, which is the ticket most likely to have one: reclaiming the tagged derivatives meant deciding what to do with things devlaunch cannot attribute, and the open half here is the same question one type over.
A
devpod listentry that fails the check must stand, be named, and never be dropped from the enumeration. Three claims, and the map has already paid for each of them.It must not disappear from the report. A thing devlaunch cannot account for that vanishes from the listing is the invisible straggler #446 §5(b) and #463 both measured one level down. A workspace whose rendered id will not parse is still a workspace holding a container, a volume and possibly somebody's afternoon.
dl --lsshould print the row with the raw string exactly as devpod gave it, plus why the check refused. Silence about it is the failure mode, not the safe default.It must not shrink the domain a destructive command enumerates. This is R1 on map #444, and it is where a validated-id type is most likely to do real harm.
dl --purgeanddl --pruneare defined over devpod workspaces and clone directories with no record at all, so a check applied as afilter_mapoverdevpod listremoves precisely the population those commands exist for. If the check ever sits between the listing and the enumeration it has to be a partition and not a filter: both arms come out, and the refusing arm is carried rather than discarded.The refusing arm has to be a value, not an absence.
Option<RenderedId>puts devpod named no id and devpod named something the check would not take in oneNone, which is the two-meanings-one-value shape this map has now refused three times: #171, #459 for the purge figure, and #468 §7 forOption<Derivative>. The shape that has worked is #446's: two arms, the refusing one carrying the raw string and the reason, and every reader matching it exhaustively so a new refusal reason is a compile error at each site rather than a branch nobody notices.What that buys per boundary, concretely.
dl --lsprints the row and says why.dl --purgeanddl --pruneleave it standing and name it, the way a site that could not be proved is left standing and named.dl <ws> rmrefuses by name, with the string the user typed and the string devpod holds, rather than reporting no such workspace. All four are one rule, which is the point of having the type at all.And one thing the check is not for. #472 settled the analogous question by asking what the record is a statement about. A rendered id is devpod's, not devlaunch's, for every entry devlaunch did not create, so the check can honestly say this is one of ours and it parses and cannot honestly say this is a legal workspace id. Written as the second, its refusals will mostly be about workspaces somebody else made, and refusing to act on those is right while refusing to show them is exactly the leak #426 was.