T-FW4Z-sdf-no-ambient-project
State: closed/done · Impact: high · Complexity: large
SDF must never act on a project the user did not name. Today every
project-parameterised read and action resolves through
backend::acting_root(), which is the focused project with a fallback to
the “ambient” checkout (SDF_PROJECT_ROOT, else the nearest sdlc.yaml
above the process cwd). The fallback means a panel or a button can address
a project the operator never selected, and it only works at all when SDF
is launched from inside a checkout. This task removes the ambient concept:
every service fn takes a project_id, resolves it through the registry’s
membership guard, and refuses anything else. It is the rule the SDLC setup
action (T-1D8N) and every later control is built on.
| Location | Role today |
|---|---|
solutions/ontological/desktop/src-tauri/src/backend.rs#project_root | The ambient root: SDF_PROJECT_ROOT, else the sdlc.yaml walk from cwd. |
solutions/ontological/desktop/src-tauri/src/backend.rs#acting_root | Focused project, falling back to the ambient root. The seam every reader and action uses. |
solutions/ontological/desktop/src-tauri/src/backend.rs#ensure | Spawns sdlc dashboard start against the ambient root. |
solutions/ontological/desktop/src-tauri/src/lib.rs#bootstrap_focus | On launch, auto-registers the ambient checkout and sets it as the focus when none is recorded. |
solutions/ontological/desktop/src-tauri/src/lib.rs#run_with | Mints the platform token and audit dir from the ambient root at startup. |
solutions/ontological/desktop/src-tauri/src/api/v1/action.rs | Six action fns (refresh_leases, open_worktree, orchestrate_watch_probe, record, get_log, reveal_token) act on acting_root(); reveal_token reads the ambient-keyed token. |
solutions/ontological/desktop/src-tauri/src/api/v1/control_plane.rs | Four read fns (get_leases, get_orchestrator_log, get_pr_cursors, get_pr_verdicts) read under acting_root(). |
solutions/ontological/desktop/src-tauri/src/api/v1/skill.rs#audit | Suspenders audit appends land under acting_root_opt(). |
solutions/ontological/desktop/src-tauri/src/system_graph/mod.rs | The system graph walks manifests under acting_root(). |
solutions/ontological/desktop/src-tauri/src/actions/token.rs | token_dir / audit_dir are keyed by a digest of the project path (project_key). |
solutions/ontological/desktop/src-tauri/src/actions/guard.rs#denial_log_dir | A 401 is audited under the focused project’s log, falling back to the startup ambient one. |
solutions/ontological/desktop/src-tauri/src/projects/mod.rs#path_of | The membership guard: id to location, no path lookup, no probe. Already the right seam. |
solutions/ontological/desktop/src-tauri/tests/focused_root_guard.rs | Pins “an unresolvable focus refuses”; still assumes an ambient root exists. |
solutions/ontological/desktop/src-tauri/tests/focused_root_actions.rs | Same shape for the action surface. |
solutions/ontological/desktop/src-tauri/tests/actions_http.rs | Token-guard tests stand up a fake project via SDF_PROJECT_ROOT. |
solutions/ontological/desktop/src-tauri/tests/suspenders_audit.rs | Audit-trail tests stand up a fake project via SDF_PROJECT_ROOT. |
solutions/ontological/desktop/src-nuxt/app/composables/useControlPlane.ts | Polls the four control-plane reads with no project argument; relies on the server following the focus. |
solutions/ontological/desktop/src-nuxt/app/composables/useActions.ts | Calls the action fns with no project argument. |
solutions/ontological/desktop/src-nuxt/app/plugins/system-graph.client.ts | Calls get_graph with no project argument. |
solutions/ontological/desktop/docs/ui-consolidation-plan.md | Documents SDF_PROJECT_ROOT as a supported knob. |
Proposed
Section titled “Proposed”- Every project-parameterised service fn in
api/v1/takesproject_id: &stras its first parameter and resolves it withProjectRegistry::path_of. An id the registry did not mint isAppError::RegisteredProjectNotFound. There is no fallback of any kind. backend::project_root,backend::acting_root,backend::acting_root_optandbackend::focused_rootare deleted.SDF_PROJECT_ROOTis no longer read anywhere.bootstrap_focusis deleted. A fresh install has an empty roster and the add form; nothing is registered without the user doing it.- The platform token is per install, not per project: it lives at the suite’s
durable tier (
<durable>/com.sizerllc.ontological/platform-token) and is minted once at startup with no project involved.reveal_tokentakes no project. - Audit trails are keyed by project id, not by a path digest:
logs/projects/<project_id>/actions.jsonl.get_log(project_id)reads that file. Denials, which carry no project, land in a suite-levellogs/denied.jsonl. - The dashboard-server spawn in
backend::ensureno longer needs a project: it runs from the sdlc checkout above SDF’s own cwd (dev-only, the same place the code lives today) untilT-B7GNreplaces it with the CLI locator. - The frontend passes
focusedProjectIdto every call. When no project is focused, the control-plane, work, and system-graph views render a “no project selected” state instead of reading. - The rule is written down: a short “No ambient project” section appended to
D-0018-sdf-control-plane-app under Decision, and a test in
solutions/ontological/desktop/src-tauri/tests/bounded_controls.rsasserting every generated/api/control-planes/*and/api/actions/*route exceptreveal-tokenandlogdenials takes aproject_id.
Approach
Section titled “Approach”- Add
project_idto the ten fns inapi/v1/action.rsandapi/v1/control_plane.rs, tosystem_graph::get_graph, and toskill::audit’s callers. Each resolves through a newcrate::projects::root_of(project_id) -> Result<PathBuf, AppError>that wrapsProjectRegistry::global()+path_ofand produces the not-found error. Runcargo checkso ontogen regenerates HTTP, IPC, MCP and the TS client; commit the generated files. - Move the token:
solutions/ontological/desktop/src-tauri/src/actions/token.rsgainsinstall_token_dir()(suite durable tier, unscoped) and loses the project-keyedtoken_dir.lib.rs#run_withmints from it.reveal_tokenreads it. Deletetoken_rootinaction.rs. - Re-key audit:
actions/token.rs::audit_dir(project_id)scopes the logs tier by id;project_keyand its sha2 digest go away (drop thesha2dependency if nothing else uses it).guard.rs#denial_log_dirwrites to the suite-level denied log; delete its TTL cache since there is nothing to walk.GuardState.audit_dirbecomes that fixed path. - Delete
backend::project_root,focused_root,acting_root,acting_root_opt. Rewritebackend::ensureto find the dev checkout (walk up from cwd forsolutions/ontological/cli/sdlc.ts) and pass no--project-root; log and skip when none is found. - Delete
bootstrap_focusandsame_rootinlib.rs. Keep thefocused_projectsetting as the switcher’s persisted selection only. - Rewrite the four integration tests that set
SDF_PROJECT_ROOT: each now writes aregistered_projects/<id>.mdrecord into theSDF_VAULT_ROOTtempdir and calls the fn with that id. Renamefocused_root_guard.rsandfocused_root_actions.rstoproject_id_guard.rsandproject_id_actions.rs; their assertion becomes “an unregistered id refuses and names the id”. - Add the route-shape assertion to
solutions/ontological/desktop/src-tauri/tests/bounded_controls.rs. - Frontend: thread
focusedProjectIdthroughuseControlPlane.ts,useActions.ts,solutions/ontological/desktop/src-nuxt/app/plugins/system-graph.client.ts, and any page that calls the generated client directly. Add the empty state. Update the vitest suites beside those composables. - Docs: the D-0018 addendum; strip
SDF_PROJECT_ROOTfrom the SDF README andsolutions/ontological/desktop/docs/ui-consolidation-plan.md; update the action table in the README.
| Area | Note |
|---|---|
solutions/ontological/desktop/src-tauri/src/backend.rs | Delete the four root resolvers; ensure finds the dev checkout instead of a project. |
solutions/ontological/desktop/src-tauri/src/lib.rs | Delete bootstrap_focus and same_root; mint the token from the install dir. |
solutions/ontological/desktop/src-tauri/src/projects/mod.rs | Add root_of(project_id) returning the not-found error. |
solutions/ontological/desktop/src-tauri/src/api/v1/action.rs | project_id on every fn except reveal_token; drop project_root / token_root. |
solutions/ontological/desktop/src-tauri/src/api/v1/control_plane.rs | project_id on the four reads; drop project_root. |
solutions/ontological/desktop/src-tauri/src/api/v1/skill.rs | audit takes the project id its callers already hold. |
solutions/ontological/desktop/src-tauri/src/api/v1/system_graph.rs | get_graph takes project_id. |
solutions/ontological/desktop/src-tauri/src/system_graph/mod.rs | Take the root as a parameter instead of resolving it. |
solutions/ontological/desktop/src-tauri/src/actions/token.rs | Install-level token dir; id-keyed audit dir; delete project_key. |
solutions/ontological/desktop/src-tauri/src/actions/guard.rs | Denials to the suite-level log; delete the TTL cache. |
solutions/ontological/desktop/src-tauri/src/api/transport/http/generated.rs | Regenerated. |
solutions/ontological/desktop/src-tauri/src/api/transport/ipc/generated.rs | Regenerated. |
solutions/ontological/desktop/src-tauri/src/api/transport/mcp/generated.rs | Regenerated. |
solutions/ontological/desktop/src-nuxt/app/generated/transport.ts | Regenerated. |
solutions/ontological/desktop/src-nuxt/app/generated/types.ts | Regenerated. |
solutions/ontological/desktop/src-tauri/Cargo.toml | Drop sha2 if unused after project_key goes. |
solutions/ontological/desktop/src-tauri/tests/focused_root_guard.rs | Replaced by project_id_guard.rs. |
solutions/ontological/desktop/src-tauri/tests/project_id_guard.rs | Unregistered id refuses on the read surface. |
solutions/ontological/desktop/src-tauri/tests/focused_root_actions.rs | Replaced by project_id_actions.rs. |
solutions/ontological/desktop/src-tauri/tests/project_id_actions.rs | Unregistered id refuses on the action surface. |
solutions/ontological/desktop/src-tauri/tests/actions_http.rs | Register a project in the vault tempdir instead of SDF_PROJECT_ROOT. |
solutions/ontological/desktop/src-tauri/tests/suspenders_audit.rs | Same. |
solutions/ontological/desktop/src-tauri/tests/bounded_controls.rs | Assert the project_id parameter on every project route. |
solutions/ontological/desktop/src-nuxt/app/composables/useControlPlane.ts | Pass focusedProjectId; empty state when null. |
solutions/ontological/desktop/src-nuxt/app/composables/useControlPlane.test.ts | Cover the id threading and the empty state. |
solutions/ontological/desktop/src-nuxt/app/composables/useActions.ts | Pass focusedProjectId. |
solutions/ontological/desktop/src-nuxt/app/plugins/system-graph.client.ts | Pass focusedProjectId. |
solutions/ontological/desktop/README.md | Remove SDF_PROJECT_ROOT; note the rule in the action table. |
solutions/ontological/desktop/docs/ui-consolidation-plan.md | Remove SDF_PROJECT_ROOT. |
docs/planning/decisions/D-0018-sdf-control-plane-app.md | Append the “No ambient project” rule under Decision. |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
grep -rn "SDF_PROJECT_ROOT\|acting_root\|focused_root\|fn project_root" solutions/ontological/desktopreturns no matches outside git history. - AC-2: Every generated route under
/api/control-planes/*and/api/actions/*other thanreveal-tokentakes aproject_id, asserted by a test insolutions/ontological/desktop/src-tauri/tests/bounded_controls.rs. - AC-3: Calling any project-parameterised fn with an id the registry never minted returns
AppError::RegisteredProjectNotFoundnaming that id, with no filesystem read or spawn, asserted byproject_id_guard.rsandproject_id_actions.rs. - AC-4: Launching SDF with an empty vault registers nothing and records no focus; the roster is empty until the add form is used.
- AC-5: The platform token file exists at the suite durable tier regardless of cwd, and the HTTP
guard accepts it on a mutating route, asserted in
actions_http.rs. - AC-6: An action’s audit record lands in
logs/projects/<project_id>/actions.jsonlandget_log(project_id)returns it; a 401 lands inlogs/denied.jsonl. - AC-7: With no focused project, the control-plane, work, and system-graph views render a “no
project selected” state and issue no reads (vitest on
useControlPlane). - AC-8:
moon run sdf:checkandmoon run sdf:testpass, and the codegen-drift gate is clean.
Out of scope
Section titled “Out of scope”- Resolving the sdlc CLI for a project outside this monorepo (
T-B7GN). - Any new action, including project setup (
T-1D8N). - The dashboard server’s own project scoping (
lib/services/dashboard/), which already takes?project=. - A dev convenience that pre-registers the current checkout; if wanted, it is a later, explicit verb.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Surfaced while surveying what it would take for SDF to run sdlc project setup on a newly opened project (2026-09-20). The survey found that
ambient-root resolution was load-bearing in the action surface, the token,
and the launch bootstrap, so the rule had to land before the setup action.