Repository navigation
Description
Summary
The app-packaging design (doc/2026-10-29T12-50-36Z_design_id_app_packaging §2) states the model in one line: "Everything is relative to the app folder. No absolute paths, no references outside the folder" — the folder is copyable as a unit, its name must equal the app id, and discovery (§3) validates and registers the app, degrading per-app with a named error. Discovery was built under #2324 (closed) and the gate in pkgs/id/src/app_manifest.rs has been tightened field by field since (#4063 / PR #4082). Six filings in two days show it is still partial in a patterned way — every one is a place where the gate checks one field, one list, or one level and trusts the rest:
- id(app_manifest): [resources].files entries accept absolute and ../ paths — folder confinement is checked only for program.source #4059 —
[resources].filesis never inspected:files = ["../../etc/passwd", "/etc/passwd", "assets/**"]is accepted, whileprogram.sourceis component-checked. The pack step will package and content-hash exactly these entries as a set. - id(app_manifest): discover_apps follows symlinked app folders — an app outside the apps root registers as a local app #4067 —
discover_appsfollows symlinks (is_dir,join("app.toml")), and the id-match uses the symlink's name, so an app folder symlinked to a directory outside the apps root registers as a local app, errors=0. - id(app_manifest): the #4082 duplicate gate covers only capabilities.request — duplicate cli.commands pass, and padded capability entries evade the duplicate check and are stored untrimmed #4085 — the fix(id): validate app manifest field values (#4063) #4082 duplicate gate covers only
capabilities.request: duplicatecli.commandspass, and the capability duplicate check compares raw strings while the blank check trims, so" store.read "is stored verbatim as a distinct request — a capability name no exact-match check will ever match. - id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168 —
[resources].fileshas no blank/duplicate gate at all (blank, whitespace-only, duplicate, padded-duplicate all parse). Its acceptance explicitly defers its duplicate rule to "the same normalisation rule the id(app_manifest): the #4082 duplicate gate covers only capabilities.request — duplicate cli.commands pass, and padded capability entries evade the duplicate check and are stored untrimmed #4085 fix chooses". - id(app_manifest): interfaces.web route/socket are barely validated and discovery accepts duplicate routes #4060 —
[interfaces.web]checks onlymodeandroute.starts_with('/'):route = "/../escape path"andsocket = "https://evil.example/ws"are accepted, anddiscover_appsregisters two apps on the same route with zero errors. id: implement the [interfaces.web] adapter binding — parameterise /world + /ws/world from app.toml #2325 (open) will parameterise the host's/worldroute and/ws/worldchannel from these fields. - id(serve): app discovery is silent when the apps root does not exist — a missing or mispointed root looks exactly like zero apps installed #4061 —
discover_appsspecial-cases a missing root (ErrorKind::NotFound→ default discovery): zero apps, zero errors, and serve never logs the root it scanned — a mispointedID_APPS_DIRis indistinguishable from zero apps installed.
Three origins leave a ruling open (#4067's symlink stance, #4085's trim-vs-reject, #4061's layer) and #4168 defers its ruling to #4085's. This design states the contract once and records those rulings. It was the standing candidate named at design-architect pass 35; the currency_poc cluster filed the same evening was assessed and not originated (heterogeneous mechanisms, complete per-issue acceptance, no unrecorded shared rule — recorded in the design doc).
Full design doc: files/designs/code-id-app-manifest-folder-contract.md in the keep-the-four-repos-healthy goal workspace. Audit baseline: origin/main cc5effd8 (the whole of app_manifest.rs read verbatim at HEAD this pass; all six defects present at the cited sites).
The contract
A discovered app is exactly its folder. Every path a manifest names resolves inside the folder, under the same component rule
program.sourcealready carries. The folder itself is a directory under the apps root — a symlink is followed only when its resolved target stays inside the root, otherwise it is a named per-app error. Every list field obeys one entry rule (non-blank, unpadded, no duplicates — the gate names authoring mistakes, it does not absorb or rewrite them). Routing keys are path-shaped and unique across the discovered set, first folder in sort order wins, the loser is a named error. And discovery reports what it could not do: a root it cannot read — missing included — is a named error entry, so "no apps" always means the scan actually looked.
Recorded decisions
- Path fields share
program.source's rule (Phase 1, id(app_manifest): [resources].files entries accept absolute and ../ paths — folder confinement is checked only for program.source #4059).[resources].filesentries get the same component test (non-empty, every componentComponent::Normal); glob metacharacters live inside otherwise-normal components, so the design example (assets/**,web/templates/**) passes; absolute/../nested-escape entries are refused naming the entry. Glob semantics belong to the future pack step, not the gate. - Symlinked app folders: resolve and contain (Phase 1, id(app_manifest): discover_apps follows symlinked app folders — an app outside the apps root registers as a local app #4067). Canonicalize each candidate folder; resolution failure or a resolved path outside the canonicalized root → not registered + a named
AppDiscovery.errorsentry; a target inside the root registers normally. This generalises the fleet's landed pm-dispatch(sessions): directory confinement is lexical only — committed symlinks in the clone (nixpkgs, worktrees) escape it (residual of #2903) #3999 precedent (pm-dispatch symlink confinement: resolve, require the resolved form inside the boundary) over skip-all-links — an inside-root link breaks nothing in the folder model, and double-registration is self-defeating because the id-match compares the manifestidto the entry name. Rule stated in the module doc. Symlinks inside a folder are not a phase; Phase 4 dispositions them. - One entry rule for every list field; padding is refused, not trimmed (Phase 2, id(app_manifest): the #4082 duplicate gate covers only capabilities.request — duplicate cli.commands pass, and padded capability entries evade the duplicate check and are stored untrimmed #4085 + id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168). For
cli.commands,capabilities.requestand[resources].files: non-blank, entry equals its own trim, no duplicates — refused naming field and entry. Trim-and-store loses: the manifest is "the strict record of what an app asks for" (fix(id): validate app manifest field values (#4063) #4082 gate's own words), and silently rewriting a capability name hides the authoring mistake the gate exists to name — the same rationale the code pins for duplicates (rejected, not deduplicated). This supplies the normalisation rule id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168's acceptance defers to. An emptyfiles = []stays legal (optional section; packages nothing) and is dispositioned explicitly by Phase 4. - Routing keys are path-shaped and unique across the set (Phase 3, id(app_manifest): interfaces.web route/socket are barely validated and discovery accepts duplicate routes #4060).
route, andsocketwhen present: starts with/, no..segment, no whitespace, no?, no#. Uniqueness is discovery-level: first in folder sort order keeps the key; a second app on the same route is not registered and gets a named error. Socket uniqueness is pinned the same way — a duplicate socket is the same shadowing defect on the live channel id: implement the [interfaces.web] adapter binding — parameterise /world + /ws/world from app.toml #2325 parameterises; this extends id(app_manifest): interfaces.web route/socket are barely validated and discovery accepts duplicate routes #4060's letter by its own logic and is recorded here so its triage does not re-derive it. Uniqueness against the host's own routes belongs to id: implement the [interfaces.web] adapter binding — parameterise /world + /ws/world from app.toml #2325's binding (it owns the host route table); Phase 4 records the disposition. - A missing root is a named error, recorded by discovery (Phase 3, id(serve): app discovery is silent when the apps root does not exist — a missing or mispointed root looks exactly like zero apps installed #4061). The
NotFoundspecial case goes away; a missing root produces the same named error entry otherread_dirfailures produce, naming the root. Serve's existing warn-per-error loop surfaces it unchanged, every caller gets the signal, and root-missing / present-empty / present-with-N are distinguishable. This pins id(serve): app discovery is silent when the apps root does not exist — a missing or mispointed root looks exactly like zero apps installed #4061's "triage picks the layer": the layer is discovery, where the error channel already exists.
Alternatives that lose (in the doc): six independent fixes (the rulings interact and #4168 already defers to #4085); skip-all-symlinks (against the #3999 precedent); trim-and-store (decision 3); serve-side missing-root warning (leaves discover_apps lying to other callers); deferring confinement to pack/serve consumers (re-derives the rule per consumer); folding into #3068 (JS/TS parsed-shape contract — different gate, the pass-28 disposition).
Phased breakdown
- Phase 1 — the folder is the unit (adopted: id(app_manifest): [resources].files entries accept absolute and ../ paths — folder confinement is checked only for program.source #4059 + id(app_manifest): discover_apps follows symlinked app folders — an app outside the apps root registers as a local app #4067).
[resources].filesunderprogram.source's component rule; symlinked folders resolved and contained, outside-root refused with a named error, rule in the module doc. Lands first: the trust boundary every consumer relies on. - Phase 2 — one entry rule for every list (adopted: id(app_manifest): the #4082 duplicate gate covers only capabilities.request — duplicate cli.commands pass, and padded capability entries evade the duplicate check and are stored untrimmed #4085 + id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168). Duplicate
cli.commandsrefused; padded entries refused on all three list fields; the missing blank/duplicate gate for[resources].filesunder the same rule (decision 3). Independent of Phase 1 in code. (id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168 is already claimed/blocked in the fleet — consistent with waiting on exactly this rule.) - Phase 3 — routing keys and scan visibility (adopted: id(app_manifest): interfaces.web route/socket are barely validated and discovery accepts duplicate routes #4060 + id(serve): app discovery is silent when the apps root does not exist — a missing or mispointed root looks exactly like zero apps installed #4061). Route/socket segment sanity; route + socket uniqueness in
discover_apps(first wins, loser named); missing root as a named error entry. Independent of Phases 1–2 in code. id: implement the [interfaces.web] adapter binding — parameterise /world + /ws/world from app.toml #2325 is the named downstream consumer. - Phase 4 — closing audit (new issue, gated on Phases 1–3). Field-by-field disposition of every
AppManifestfield and everydiscover_appsbehaviour against this contract, recorded in theapp_manifest.rsmodule doc. Required dispositions: in-folder symlinks (program.sourceexistence check + pack-time resource resolution); emptyfiles = []; route edge spellings (trailing slash,//,route = "/"); app-iduniqueness (structural via folder-name equality — stated);[program].kind's free-string vocabulary; uniqueness against host routes (id: implement the [interfaces.web] adapter binding — parameterise /world + /ws/world from app.toml #2325's disposition).
Open questions stay in the design doc (in-folder symlinks at pack time; glob semantics for the pack step; ID_APPS_DIR's CWD-relative default beyond the new visibility; a [program].kind vocabulary once a second kind exists).
Related: packaging design doc/2026-10-29T12-50-36Z_design_id_app_packaging (#2026 / epic #2056) — this contract operationalises its §2/§3 at the gate without re-opening its schema; #2324 (closed, built discovery); #2325 (open, Phase 3's consumer); #4063 / PR #4082 (the partial gate); #3999 (symlink precedent); #2898 / #3231 / #3455 (same crate, validate-at-the-gate family, no field overlap).
🤖 Design architect pass 36 (2026-11-01), audit at origin/main cc5effd8.
Acceptance criteria
- Every
AppManifestfield anddiscover_appsbehaviour named in the design has an explicit disposition — gated by a named rule, or deliberately ungated with the reason recorded — in the pkgs/id module documentation. - Phases 1–3 (issues id(app_manifest): [resources].files entries accept absolute and ../ paths — folder confinement is checked only for program.source #4059, id(app_manifest): discover_apps follows symlinked app folders — an app outside the apps root registers as a local app #4067, id(app_manifest): the #4082 duplicate gate covers only capabilities.request — duplicate cli.commands pass, and padded capability entries evade the duplicate check and are stored untrimmed #4085, id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168, id(app_manifest): interfaces.web route/socket are barely validated and discovery accepts duplicate routes #4060, id(serve): app discovery is silent when the apps root does not exist — a missing or mispointed root looks exactly like zero apps installed #4061) have landed or been dispositioned, and the Phase 4 audit (issue Phase 4 (audit) of #4202 — closing field-by-field disposition for pkgs/id app_manifest #4203) records the closing field-by-field disposition.
- A manifest naming an absolute path, or a path escaping its app folder, is rejected with a named error at discovery — for every field that can name a path.
- Duplicate, blank, or whitespace-padded entries in any list field are rejected under the one shared entry rule.
- Each dispositioned rule is covered by tests in pkgs/id.
Activity
dezren39 commentedon Nov 2, 2026
🤖 Agent note · design originated, pass 36
status/blocked).[resources].filesunderprogram.source's component rule (glob components pass lexically); symlinked app folders resolve-and-contain per the pm-dispatch(sessions): directory confinement is lexical only — committed symlinks in the clone (nixpkgs, worktrees) escape it (residual of #2903) #3999 precedent (outside-root resolution = named error, not registered); one list-entry rule forcli.commands/capabilities.request/[resources].files— non-blank, unpadded, no duplicates, padding REFUSED not trimmed (this supplies the normalisation rule id(app_manifest): [resources].files has no blank/duplicate gate — blank, whitespace-only, duplicate and padded-duplicate entries all parse, unlike cli.commands and capabilities.request #4168's acceptance defers to); route AND socket are path-shaped and unique across the discovered set (socket uniqueness extends id(app_manifest): interfaces.web route/socket are barely validated and discovery accepts duplicate routes #4060's letter by its own logic); a missing apps root is a named error recorded bydiscover_apps, not a serve-side warning.files/designs/code-id-app-manifest-folder-contract.md(keep-the-four-repos-healthy goal workspace); register entry added infiles/potential-projects.md. Nouser/review— every origin carries explicit acceptance and the open rulings are the architect's recorded decisions above.3 remaining items
developing-today commentedon Nov 3, 2026
agent-note:v1
state: triaged (coverage pass)
Coverage pass 2026-11-03: reviewed in the fleet-wide per-issue pass (drift / overlap / already-handled / stale / blockers / ordering). Verdict: still live; queued in repo-health-code-pilot as triage at priority 40 (priority/normal). If this is already handled at current main, the triage closes it with evidence; if it needs splitting, the triage files the split.
Found via: coverage pass 2026-11-03
dezren39 commentedon Nov 3, 2026
🤖 Agent note · picked up (queue task 8374, lane 7b47536b)
dezren39 commentedon Nov 3, 2026
🤖 Agent note · triage verdict (queue task 8374, lane 7b47536b)
Verdict: still live — verified at origin/main
3e363c9(2026-11-03).Children states (fresh): #4067, #4085, #4168, #4060, #4061 CLOSED; #4059 OPEN (Phase 1 remainder); #4203 OPEN
status/blocked(Phase 4 audit, gated on Phases 1–3).Code evidence at
3e363c9(pkgs/id/src/app_manifest.rs):ensure_distinct_entries, parse sites ~L141-143; tests ~L614-641), route/socket path-shape gate (is_safe_web_path, ~L171-186), symlink resolve-and-contain indiscover_apps+ module doc (~L194-255, test ~L706).[resources].filesgets only the entry rule — noComponent::Normalconfinement check.program.sourcehas it (~L153-159);filesdoes not. id(app_manifest): [resources].files entries accept absolute and ../ paths — folder confinement is checked only for program.source #4059 still reproduces by code reading.Queue (no new tasks needed): #4059 triage task 8342 is claimed-live elsewhere — no parallel work started. #4203 triage task 8745 is queued. This epic needs no further routing.
Ordering / blockers: land #4059 (files confinement under
program.sourcerule) → then run #4203 audit against the final module. Nothing in this epic is blocked on a Drew decision.Done: triage complete, no code change (read-only; no branch/worktree), no PR, no split filed.
dezren39 commentedon Nov 4, 2026
🤖 Agent note · Phase 4 summary for #4203
Phase 4 audit of #4202 is complete in PR #5323. The
app_manifest.rsmodule doc now carries the field-by-field disposition table (everyAppManifestfield and everydiscover_appsbehaviour: gated by a named rule / deliberately ungated with reason / routed to a named owner), audited against current main0dab47d5f.The six required dispositions:
resources.filesandprogram.sourceare component-checked strings, and theprogram.sourceexistence check follows symlinks without resolving them, because discovery opens neither path. Pack-time resolution confinement is assigned to the pack step as named owner: no pack step exists inpkgs/idyet, and the one that is built must resolve each named file inside the app folder before packaging/hashing it.files = []— deliberately legal, recorded as a decision (optional section, packages nothing; no consumer reads it as a defect).is_safe_web_pathnow rejects empty segments, so/, trailing slash, and//spellings fail the gate for route and socket; new testweb_route_edge_spellings_are_rejected.iduniqueness — structural via folder-name equality (including the two-symlinks-one-target case); no redundant check added.program.kindvocabulary — disposition only; free string, one checked-in value (roc-world), no enumeration without a second kind.web::app_interfacerejects exact and structural collisions withHOST_RESERVED_ROUTES(id(web): web_interface_from_app_toml accepts route/socket values that panic in create_router_for_app (no leading-slash or host-route collision check) #4779, id(web): web_interface_from_app_toml still accepts route/socket shapes that panic axum — invalid capture syntax and renamed-capture conflicts with host routes pass the #4779 checks #4795). Discovery keeps only cross-app uniqueness. No follow-up filed, because the owed check exists today at the layer that owns the host route table.Findings: no other confirmed same-shape gaps. Audit evidence: the only
discover_appscaller today iscommands::serve(logs id/path, warns per error); nothing in the crate yet readsresources.files, executesprogram.source, or binds a discovered app's route. Tests: laptopcargo test --lib app_manifest::inpkgs/id— 22 passed, 0 failed.dezren39 commentedon Nov 4, 2026
Program tending — epic completion proposal (2026-11-04, via #1352)
All 7 sub-issues of this epic are now closed:
With the full set complete, this epic's acceptance appears met. Proposing closure as completed — if any follow-up work remains (e.g. under the app-manifest gate theme), it can be filed as its own issue rather than keeping this epic open.
— program tender (#1352)