Skip to content

squad_state_list fails for root on adapter-backed backends: StateBackendStorageAdapter.toRelative() returns "." for squadDir #2231

Open

Description

@jpapiez

Summary

squad_state_list with dir omitted (or "", or ".squad") fails on any backend routed through StateBackendStorageAdapter (observed with stateBackend: "two-layer"):

Failed to list state: State key must not contain . or .. path segments

squad_state_list {dir: "decisions"} works.

Environment

  • squad CLI v1.0.1 (WinGet bradygaster.Squad), Windows
  • .squad/config.json → stateBackend: "two-layer"; squad_state_health → State backend storage: StateBackendStorageAdapter
  • Source references below are from main @ 89d6302 (bug still present)

Minimal repro

  1. Repo with stateBackend: "two-layer" (or orphan / git-notes).
  2. Run squad state-mcp, call squad_state_list {}.
  3. Observe the error above.

Unit-level repro:

const adapter = new StateBackendStorageAdapter(new OrphanBranchBackend(repo), squadDir);
adapter.listSync(squadDir); // throws: State key must not contain . or .. path segments

Root cause

  1. packages/squad-sdk/src/tools/index.ts normalizeStateToolDir() correctly maps an omitted dir to "", then calls this.storage.listSync(path.join(this.squadRoot, "")), i.e. listSync(squadRoot).
  2. packages/squad-sdk/src/state-backend.ts StateBackendStorageAdapter.toRelative() has:
    if (fileCmp === squadCmp) {
      return '.';
    }
  3. Every backend list() runs normalizeKey(relativeDir). normalizeKey explicitly allows "" as root ("Empty string after normalization means root — valid for list() operations"), but "." is non-empty, so validateStateKey('.') throws.

The adapter emits a root sentinel the backends do not accept.

Suggested fix (keeps traversal rejection)

Do not relax validateStateKey. Map squadDir to "" only for list-shaped reads:

  • list / listSync / isDirectory / isDirectorySync: when the path resolves to squadDir, pass "".
  • Keep "." (and therefore rejection) for file operations and especially deleteDir/deleteDirSync. Returning "" there would turn deleteDir(squadDir) into a wipe of all state, and read("") on git backends could resolve to a tree object.

For example, add private toRelativeDir(p) { const r = this.toRelative(p); return r === '.' ? '' : r; } and use it in the four list methods.

Regression tests

  • adapter.listSync(squadDir) returns root entries for the orphan, git-notes, and two-layer backends.
  • squad_state_list {} and {dir: ""} succeed.
  • {dir: ".."}, {dir: "a/../b"}, and {dir: "."} still fail with the . or .. error.
  • adapter.deleteDirSync(squadDir) and adapter.readSync(squadDir) still throw.

Downstream report: OlyForge3D/PrintFarmer#3372

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    wave1:P1Wave 1 PR group P1wave:1-nextWave 1 — immediate next sprint

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions