Skip to content

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.

LocationRole today
solutions/ontological/desktop/src-tauri/src/backend.rs#project_rootThe ambient root: SDF_PROJECT_ROOT, else the sdlc.yaml walk from cwd.
solutions/ontological/desktop/src-tauri/src/backend.rs#acting_rootFocused project, falling back to the ambient root. The seam every reader and action uses.
solutions/ontological/desktop/src-tauri/src/backend.rs#ensureSpawns sdlc dashboard start against the ambient root.
solutions/ontological/desktop/src-tauri/src/lib.rs#bootstrap_focusOn 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_withMints the platform token and audit dir from the ambient root at startup.
solutions/ontological/desktop/src-tauri/src/api/v1/action.rsSix 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.rsFour 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#auditSuspenders audit appends land under acting_root_opt().
solutions/ontological/desktop/src-tauri/src/system_graph/mod.rsThe system graph walks manifests under acting_root().
solutions/ontological/desktop/src-tauri/src/actions/token.rstoken_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_dirA 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_ofThe membership guard: id to location, no path lookup, no probe. Already the right seam.
solutions/ontological/desktop/src-tauri/tests/focused_root_guard.rsPins “an unresolvable focus refuses”; still assumes an ambient root exists.
solutions/ontological/desktop/src-tauri/tests/focused_root_actions.rsSame shape for the action surface.
solutions/ontological/desktop/src-tauri/tests/actions_http.rsToken-guard tests stand up a fake project via SDF_PROJECT_ROOT.
solutions/ontological/desktop/src-tauri/tests/suspenders_audit.rsAudit-trail tests stand up a fake project via SDF_PROJECT_ROOT.
solutions/ontological/desktop/src-nuxt/app/composables/useControlPlane.tsPolls the four control-plane reads with no project argument; relies on the server following the focus.
solutions/ontological/desktop/src-nuxt/app/composables/useActions.tsCalls the action fns with no project argument.
solutions/ontological/desktop/src-nuxt/app/plugins/system-graph.client.tsCalls get_graph with no project argument.
solutions/ontological/desktop/docs/ui-consolidation-plan.mdDocuments SDF_PROJECT_ROOT as a supported knob.
  • Every project-parameterised service fn in api/v1/ takes project_id: &str as its first parameter and resolves it with ProjectRegistry::path_of. An id the registry did not mint is AppError::RegisteredProjectNotFound. There is no fallback of any kind.
  • backend::project_root, backend::acting_root, backend::acting_root_opt and backend::focused_root are deleted. SDF_PROJECT_ROOT is no longer read anywhere.
  • bootstrap_focus is 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_token takes 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-level logs/denied.jsonl.
  • The dashboard-server spawn in backend::ensure no longer needs a project: it runs from the sdlc checkout above SDF’s own cwd (dev-only, the same place the code lives today) until T-B7GN replaces it with the CLI locator.
  • The frontend passes focusedProjectId to 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.rs asserting every generated /api/control-planes/* and /api/actions/* route except reveal-token and log denials takes a project_id.
  1. Add project_id to the ten fns in api/v1/action.rs and api/v1/control_plane.rs, to system_graph::get_graph, and to skill::audit’s callers. Each resolves through a new crate::projects::root_of(project_id) -> Result<PathBuf, AppError> that wraps ProjectRegistry::global() + path_of and produces the not-found error. Run cargo check so ontogen regenerates HTTP, IPC, MCP and the TS client; commit the generated files.
  2. Move the token: solutions/ontological/desktop/src-tauri/src/actions/token.rs gains install_token_dir() (suite durable tier, unscoped) and loses the project-keyed token_dir. lib.rs#run_with mints from it. reveal_token reads it. Delete token_root in action.rs.
  3. Re-key audit: actions/token.rs::audit_dir(project_id) scopes the logs tier by id; project_key and its sha2 digest go away (drop the sha2 dependency if nothing else uses it). guard.rs#denial_log_dir writes to the suite-level denied log; delete its TTL cache since there is nothing to walk. GuardState.audit_dir becomes that fixed path.
  4. Delete backend::project_root, focused_root, acting_root, acting_root_opt. Rewrite backend::ensure to find the dev checkout (walk up from cwd for solutions/ontological/cli/sdlc.ts) and pass no --project-root; log and skip when none is found.
  5. Delete bootstrap_focus and same_root in lib.rs. Keep the focused_project setting as the switcher’s persisted selection only.
  6. Rewrite the four integration tests that set SDF_PROJECT_ROOT: each now writes a registered_projects/<id>.md record into the SDF_VAULT_ROOT tempdir and calls the fn with that id. Rename focused_root_guard.rs and focused_root_actions.rs to project_id_guard.rs and project_id_actions.rs; their assertion becomes “an unregistered id refuses and names the id”.
  7. Add the route-shape assertion to solutions/ontological/desktop/src-tauri/tests/bounded_controls.rs.
  8. Frontend: thread focusedProjectId through useControlPlane.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.
  9. Docs: the D-0018 addendum; strip SDF_PROJECT_ROOT from the SDF README and solutions/ontological/desktop/docs/ui-consolidation-plan.md; update the action table in the README.
AreaNote
solutions/ontological/desktop/src-tauri/src/backend.rsDelete the four root resolvers; ensure finds the dev checkout instead of a project.
solutions/ontological/desktop/src-tauri/src/lib.rsDelete bootstrap_focus and same_root; mint the token from the install dir.
solutions/ontological/desktop/src-tauri/src/projects/mod.rsAdd root_of(project_id) returning the not-found error.
solutions/ontological/desktop/src-tauri/src/api/v1/action.rsproject_id on every fn except reveal_token; drop project_root / token_root.
solutions/ontological/desktop/src-tauri/src/api/v1/control_plane.rsproject_id on the four reads; drop project_root.
solutions/ontological/desktop/src-tauri/src/api/v1/skill.rsaudit takes the project id its callers already hold.
solutions/ontological/desktop/src-tauri/src/api/v1/system_graph.rsget_graph takes project_id.
solutions/ontological/desktop/src-tauri/src/system_graph/mod.rsTake the root as a parameter instead of resolving it.
solutions/ontological/desktop/src-tauri/src/actions/token.rsInstall-level token dir; id-keyed audit dir; delete project_key.
solutions/ontological/desktop/src-tauri/src/actions/guard.rsDenials to the suite-level log; delete the TTL cache.
solutions/ontological/desktop/src-tauri/src/api/transport/http/generated.rsRegenerated.
solutions/ontological/desktop/src-tauri/src/api/transport/ipc/generated.rsRegenerated.
solutions/ontological/desktop/src-tauri/src/api/transport/mcp/generated.rsRegenerated.
solutions/ontological/desktop/src-nuxt/app/generated/transport.tsRegenerated.
solutions/ontological/desktop/src-nuxt/app/generated/types.tsRegenerated.
solutions/ontological/desktop/src-tauri/Cargo.tomlDrop sha2 if unused after project_key goes.
solutions/ontological/desktop/src-tauri/tests/focused_root_guard.rsReplaced by project_id_guard.rs.
solutions/ontological/desktop/src-tauri/tests/project_id_guard.rsUnregistered id refuses on the read surface.
solutions/ontological/desktop/src-tauri/tests/focused_root_actions.rsReplaced by project_id_actions.rs.
solutions/ontological/desktop/src-tauri/tests/project_id_actions.rsUnregistered id refuses on the action surface.
solutions/ontological/desktop/src-tauri/tests/actions_http.rsRegister a project in the vault tempdir instead of SDF_PROJECT_ROOT.
solutions/ontological/desktop/src-tauri/tests/suspenders_audit.rsSame.
solutions/ontological/desktop/src-tauri/tests/bounded_controls.rsAssert the project_id parameter on every project route.
solutions/ontological/desktop/src-nuxt/app/composables/useControlPlane.tsPass focusedProjectId; empty state when null.
solutions/ontological/desktop/src-nuxt/app/composables/useControlPlane.test.tsCover the id threading and the empty state.
solutions/ontological/desktop/src-nuxt/app/composables/useActions.tsPass focusedProjectId.
solutions/ontological/desktop/src-nuxt/app/plugins/system-graph.client.tsPass focusedProjectId.
solutions/ontological/desktop/README.mdRemove SDF_PROJECT_ROOT; note the rule in the action table.
solutions/ontological/desktop/docs/ui-consolidation-plan.mdRemove SDF_PROJECT_ROOT.
docs/planning/decisions/D-0018-sdf-control-plane-app.mdAppend the “No ambient project” rule under Decision.
  • AC-1: grep -rn "SDF_PROJECT_ROOT\|acting_root\|focused_root\|fn project_root" solutions/ontological/desktop returns no matches outside git history.
  • AC-2: Every generated route under /api/control-planes/* and /api/actions/* other than reveal-token takes a project_id, asserted by a test in solutions/ontological/desktop/src-tauri/tests/bounded_controls.rs.
  • AC-3: Calling any project-parameterised fn with an id the registry never minted returns AppError::RegisteredProjectNotFound naming that id, with no filesystem read or spawn, asserted by project_id_guard.rs and project_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.jsonl and get_log(project_id) returns it; a 401 lands in logs/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:check and moon run sdf:test pass, and the codegen-drift gate is clean.
  • 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.
  • none

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.


← Back to Tasks