# Account filesystem schema: seed it, and enforce file↔graph stewardship — design

Source task: `.tasks/1091-account-filesystem-schema-enforcement.md`. Backstop
(out of scope here): `.tasks/1092-account-dir-standing-reconcile-and-quarantine.md`.

## Problem (recap)

The account data dir (`data/accounts/<id>/`) should be a consistent projection of
the graph ontology, but that doctrine lives only inside the `data-manager`
specialist the main agent often routes around, nothing seeds a canonical file
layout, and nothing stops an agent inventing folders. The observed failure: an
agent invented `projects/<name>/deploy|sources|design|archive` with no ontology
basis, justified as "there was no canonical schema to copy from."

## Resolved gating decision

`provision_account_dir` runs **unprivileged** on the Pi target — at install and
at runtime when the `account_create` admin tool calls it via `execFileSync`
inside the `systemctl --user` brand service. `chattr +i` needs root
(`CAP_LINUX_IMMUTABLE`). So the task's Decision C1 (OS-immutable top level)
**cannot take effect on the real target**.

**Decision (operator, 2026-06-23): drop C1 entirely.** No `chattr`/`chflags`
code ships. The PreToolUse Write-hook becomes the sole tool-layer hard guard;
top-level strays created via bash fall to the Task 1092 reconcile. This also
removes the chattr observability line and the upgrade/uninstall-unlock risk from
the task.

## What ships

Three layers, honest about hard-enforceable vs cooperative.

### A. Seeded schema (one standard, every account)

`provision_account_dir` (the function both `setup-account.sh` and the
`account_create` lifecycle tool call) gains an idempotent seed step:

- **Create the operator-data skeleton:** `projects/`, `contacts/`, `documents/`.
- **Create the tool-owned skeleton:** `url-get/`, `output/`, `generated/`,
  `extracted/`, `uploads/` (`agents/`, `specialists/` already created earlier in
  the function).
- **Copy `SCHEMA.md`** to the account root from a shipped template
  (`platform/templates/account-schema/SCHEMA.md`). The template is the **single
  source of truth** for the allowed top-level set (see below) and the flatness
  rule.
- **Log** `[acct-schema] seeded dirs=<n>` once per provision. Idempotent: a
  re-provision over an existing account re-creates nothing and overwrites
  `SCHEMA.md` with the shipped version (Rubytech-controlled, like `IDENTITY.md`).

Operator-data buckets are **flat**: files live directly under one entity folder
(`projects/<name>/file`, `contacts/<name>/file`) or, for `documents/`, directly
in the bucket or one document-folder deep. Flatness is what makes
`projects/<name>/deploy/` a machine-detectable violation.

### B. `DATA_STEWARDSHIP` directive (cooperative layer)

A `DATA_STEWARDSHIP` const beside `PLATFORM_BOUNDARY` in
`platform/services/claude-session-manager/src/system-prompt.ts`, rendered as a
`<data-stewardship>` section in `renderAppendBlock` next to `<platform-boundary>`
— joined into `--append-system-prompt` on **every spawn, every role**. It states:
the account-dir layout is fixed by the seeded `SCHEMA.md`; never author new
top-level folders or nested subtrees under an entity folder; route
reorganisation/moves through `data-manager` and non-trivial graph writes through
`database-operator`; the existing carve-out (a one-line update against an
in-context, unambiguously-classified node) stays direct.

`runSystemPromptSelfTest` is extended to assert `<data-stewardship>` /
`</data-stewardship>` appear in the rendered block.

### C. PreToolUse Write-hook (hard guard, no bash removal)

A new hook `platform/plugins/admin/hooks/fs-schema-guard.sh`, seeded into the
account `.claude/settings.json` as a PreToolUse hook on `Write`, `Edit`, and
`NotebookEdit` (added alongside the existing `archive-ingest-surface-gate.sh`
entries on those three matchers).

Behaviour:

1. Read the stdin JSON envelope. Fail **closed** on terminal stdin (no envelope).
2. Extract the target path: `tool_input.file_path` for Write/Edit,
   `tool_input.notebook_path` for NotebookEdit (via python3, matching the
   existing `extract_tool_input_field` pattern — never jq, never command parsing).
3. Resolve the path against the account dir. If the resolved path is **outside**
   the account data dir, **allow** (exit 0) — this hook governs the account-dir
   schema only; platform-source writes are PLATFORM_BOUNDARY's concern.
4. Compute the path relative to the account dir. Let `seg0` be its first segment.
5. **Top-level check:** if `seg0` is a single path segment (a top-level file or
   dir) not in the allowed top-level set → **block** (exit 2),
   `reason=top-level`.
6. **Over-deep check:** if `seg0 ∈ {projects, contacts}` and the path has more
   than `bucket/entity/file` depth (i.e. a directory component beyond the single
   entity folder), or `seg0 == documents` and the path exceeds
   `documents/folder/file` → **block** (exit 2), `reason=over-deep`.
7. Tool-owned dirs (everything in the allowed set except the three operator-data
   buckets) pass at **any** depth.
8. Otherwise allow (exit 0).
9. Every block logs `[fs-guard] blocked path=<rel> reason=<top-level|over-deep>`
   to stderr (operator-greppable). No task numbers / internal refs in any
   operator-visible string. The exit-2 stderr message cites the schema in plain
   terms.

**Allowed top-level set — single source.** The set lives once, as a fenced
` ```allowed-top-level ` block in `platform/templates/account-schema/SCHEMA.md`
(newline-delimited). The hook parses that fenced block from the **account's**
seeded `SCHEMA.md` at runtime (deterministic structured data, not prose control
flow). Shipped contents:

```
projects
contacts
documents
url-get
output
generated
extracted
uploads
agents
specialists
sites
public
cache
secrets
state
logs
tmp
SCHEMA.md
account.json
AGENTS.md
.claude
.git
.quarantine
```

A unit test asserts the hook's parse of the template equals this documented set,
so the human doc and the enforcement never drift. Adding a future plugin
top-level dir is a one-line edit to this block.

## Out of scope (unchanged from task)

- The standing reconcile / quarantine — Task 1092 (named as the backstop here,
  not built).
- Removing or narrowing the main agent's `Bash`/`Write`/`Edit` tools — rejected.
- Auto-repairing drift — `data-manager` fixes on dispatch.
- Per-account schema variants.
- Any change to the graph schema or the `database-operator` write path.
- `chattr`/`chflags` OS-immutability (Decision C1) — dropped per the gating
  decision above.

## Verification

- **Seed:** a fresh `provision_account_dir` run produces the documented skeleton
  and `SCHEMA.md`; the seeded allowed-top-level set matches the template exactly.
  Re-provision is idempotent.
- **Write-hook:** a `Write` to `projects/Acme/deploy/sources/a.txt` blocks
  (`over-deep`); `projects/Acme/a.txt` and `documents/a.pdf` allow; a write into
  `output/` or `generated/` at any depth allows; a write to a novel top-level dir
  `whim/x` blocks (`top-level`); a write outside the account dir allows.
- **Directive:** `runSystemPromptSelfTest` asserts the `<data-stewardship>` tag.
- **No tool regression:** the main agent retains `Bash`/`Write`/`Edit`; existing
  hook + spawn + specialist tests pass; `platform/ui` (if touched) builds/lints
  clean. The new hook does not interfere with the existing PreToolUse gate on the
  same matchers (both run; either can block).
- **Device:** after publish + upgrade, a real account shows the seeded skeleton,
  `SCHEMA.md`, and a blocked whim-write in the logs.

## Observability

- `provision_account_dir` logs `[acct-schema] seeded dirs=<n>`.
- The Write-hook logs `[fs-guard] blocked path=<rel> reason=<top-level|over-deep>`
  per block.
