Skip to content

The app folder is the unit of trust — one gate for what an app manifest may name (pkgs/id app_manifest) #4202

Open
7 of 7 issues completed

Description

@dezren39

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:

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.source already 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

  1. 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].files entries get the same component test (non-empty, every component Component::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.
  2. 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.errors entry; 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 manifest id to the entry name. Rule stated in the module doc. Symlinks inside a folder are not a phase; Phase 4 dispositions them.
  3. 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.request and [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 empty files = [] stays legal (optional section; packages nothing) and is dispositioned explicitly by Phase 4.
  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, and socket when 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.
  5. 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 NotFound special case goes away; a missing root produces the same named error entry other read_dir failures 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

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

Activity

  1. added
    priority/normalOrdinary priority (the default)
    app/idPackage/app: id (pkgs/ or apps/ in this repo)
    epicMeta-issue grouping multiple items (org Project Type=Epic)
    effort/lLarge: multi-day or open-ended; expect staged check-ins
    kind/designA design proposal (design-architect): multi-step, phased, decision-bearing
    on Nov 2, 2026
  2. dezren39 commented on Nov 2, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · design originated, pass 36

  3. 3 remaining items

  4. added
    queue/queuedHas a queued task in the Supabase agent queue
    task/triageQueue packet kind: verify, reproduce, and route this item
    on Nov 3, 2026
  5. developing-today commented on Nov 3, 2026

    @developing-today
    Contributor

    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

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Nov 3, 2026
  7. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up (queue task 8374, lane 7b47536b)

  8. dezren39 commented on Nov 3, 2026

    @dezren39
    MemberAuthor

    🤖 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):

    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.source rule) → 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.

  9. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · Phase 4 summary for #4203

    Phase 4 audit of #4202 is complete in PR #5323. The app_manifest.rs module doc now carries the field-by-field disposition table (every AppManifest field and every discover_apps behaviour: gated by a named rule / deliberately ungated with reason / routed to a named owner), audited against current main 0dab47d5f.

    The six required dispositions:

    1. In-folder symlinks — discovery-side rule is syntactic confinement only: resources.files and program.source are component-checked strings, and the program.source existence 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 in pkgs/id yet, and the one that is built must resolve each named file inside the app folder before packaging/hashing it.
    2. Empty files = [] — deliberately legal, recorded as a decision (optional section, packages nothing; no consumer reads it as a defect).
    3. Route edge spellings — tightened in PR docs(id): Phase 4 app_manifest disposition table; reject empty web-path segments (#4203) #5323: is_safe_web_path now rejects empty segments, so /, trailing slash, and // spellings fail the gate for route and socket; new test web_route_edge_spellings_are_rejected.
    4. App-id uniqueness — structural via folder-name equality (including the two-symlinks-one-target case); no redundant check added.
    5. program.kind vocabulary — disposition only; free string, one checked-in value (roc-world), no enumeration without a second kind.
    6. Host-route uniqueness — NOT left as a gap: id: implement the [interfaces.web] adapter binding — parameterise /world + /ws/world from app.toml #2325's scope deferred it, but the binding layer it created (PR feat(id): parameterise web adapter binding from [interfaces.web] (#2325) #3894) has since gained the check — web::app_interface rejects exact and structural collisions with HOST_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_apps caller today is commands::serve (logs id/path, warns per error); nothing in the crate yet reads resources.files, executes program.source, or binds a discovered app's route. Tests: laptop cargo test --lib app_manifest:: in pkgs/id — 22 passed, 0 failed.

  10. dezren39 commented on Nov 4, 2026

    @dezren39
    MemberAuthor

    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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

app/idPackage/app: id (pkgs/ or apps/ in this repo)effort/lLarge: multi-day or open-ended; expect staged check-insepicMeta-issue grouping multiple items (org Project Type=Epic)kind/designA design proposal (design-architect): multi-step, phased, decision-bearingmeta/enrichedIntake review done: acceptance criteria, links, and normalized labels addedpriority/normalOrdinary priority (the default)source/design-architectFiled by the repo design architect jobtask/triageQueue packet kind: verify, reproduce, and route this item

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions