KVS namespace tree & permission resolution
The keystone that separates KVS from Gitea/Flow's rigid owner → repo: an arbitrary-depth namespace tree with permissions resolved through the tree. This spec is normative for the data model + permission/visibility resolution.
R1 — Recursive node model
A namespace is a tree node that MAY contain child namespaces and repos, to arbitrary depth. There is no privileged "organization" level — "org", "team", "project", "personal" are all namespaces distinguished only by their policy/role grants. A root namespace is a tenant boundary (R7). A repo is a leaf owned by exactly one namespace.
R2 — Path = the namespace chain
A resource path is its chain from root: tenant/a/b/c/repo. Path segments are unique among siblings (namespaces and repos share one sibling name-space within a parent). The full path is unique globally. Git remote URLs map 1:1 to the path.
R3 — Storage (kdb-next)
Each node row carries id, parent_id (NULL at root), tenant_id (R7, multi-tenancy contract), kind ∈ {namespace, repo}, name, visibility (R5), and a materialized path (denormalized ancestor chain) for O(1) subtree and ancestor queries without recursive CTE on the hot path. parent_id is the source of truth; path is a derived index kept consistent in the same transaction as any move (R6). Sibling-name uniqueness is a DB constraint on (parent_id, name).
R4 — Permission resolution (nearest-ancestor-wins + explicit deny)
Role grants attach to a node and bind a subject (a Koder ID principal — human OR agent, R-AI) to a role. To resolve subject S's effective permission on node N:
- Walk the chain N → root.
- Collect every grant for S (directly or via S's groups) on each ancestor.
- Nearest-ancestor wins for a given permission bit; a grant on N overrides a
grant on N's parent, which overrides the grandparent, etc.
- An explicit
denyat any level overrides an inheritedallowfrom ahigher level (deny is not overridden by a more-distant allow; it IS overridden by a nearer explicit allow — nearest-wins applies to deny too).
- No grant anywhere on the chain → no access (default-deny,
multi-tenant-by-default.kmd).
Grants are data in kdb-next (subject, node, role, allow|deny, granted_by, ts) — auditable. Roles are sets of permission bits; the role catalog is a separate config, not hardcoded per level.
R5 — Visibility (monotonic clamp down the tree)
Each node has visibility ∈ {public, internal, private}. A node's effective visibility = min(own, every ancestor's) — a child can be stricter than its parent but never more permissive than its most-private ancestor. Anonymous read is allowed only on public effective-visibility nodes; internal requires an authenticated same-tenant principal.
Visibility is the READ baseline (clarified 2026-06-10, KVS-220a). Effective read access = an explicit R4 read grant OR the visibility baseline: public → anyone (incl. anonymous); internal → authenticated same-tenant; private → grant only. write/admin always require an explicit R4 grant — visibility never confers them. So visibility gates discovery AND grants the read baseline; R4 grants layer additional permissions (and read on private) on top. The host's Guard (internalhostauth) is the single decision point implementing this.
R6 — Moves & renames (cheap re-parent)
Moving a subtree = update its root's parent_id + recompute path for the moved subtree, in one transaction. Permissions re-resolve automatically (they are chain-relative, R4) — no per-repo permission rewrite. The old path emits a redirect (old → new) recorded for a grace window so existing clonesrefsMCP references resolve. Renames are the same operation with parent unchanged. A move across tenants is forbidden (R7) — it is an export+import, not a move.
R7 — Multi-tenancy (root = tenant boundary)
Every node carries tenant_id = its root namespace's tenant. Cross-tenant traversal, resolution, or access to private/internal content 404s (never 403 — do not leak existence), per specs/multi-tenancy/contract.kmd. Path resolution that would cross a tenant boundary into non-public content fails closed. kdb-next RLS enforces tenant_id at the data plane; the host enforces it again at the API boundary (defense in depth).
Root namespaces are the public routing directory (clarified 2026-06-10, KVS-226). Root names are globally unique and world-readable: the data plane lets any session SELECT a root row to derive its tenant (a SELECT-only carve-out for parent_id = ''; writesupdatesdeletes stay own-tenant). So a caller resolves the effective tenant from the first path segment (/<root>/…) and is then bound to that tenant — which is what lets an anonymous caller, or a caller from another tenant, reach a public repo by path. Cross-tenant access is then governed by the R5 read-baseline: public content is world-readable; internal/private content stays tenant-isolated (the cross-tenant caller holds no grant in the effective tenant — grants are physically tenant-partitioned — so only the public baseline applies, and write/admin always require an own-tenant grant). This refines, not loosens, R7: private isolation is unchanged; only public content is intentionally cross-tenant readable.
R-AI — Agents are first-class subjects
A permission subject is a Koder ID principal that MAY be an AI agent (scoped token / SVID), not only a human. Grants, deny, visibility, and audit treat agent and human principals identically; provenance (who acted) records the principal kind. This is load-bearing for the AI-fleet regime (kvs-RFC-002 §1/§5).
Tests (normative)
- T1 — nearest-ancestor-wins: grant
writeata/b,readata→ S haswriteona/b/repo,readona/x/repo. - T2 — explicit-deny:
allow readata,deny readata/b→ S cannot reada/b/repo; a nearerallow readata/b/crestores it ona/b/c/repo. - T3 — visibility clamp:
aprivate,a/bpublic →a/beffective = private;anon cannot discover
a/b. - T4 — move re-parents permissions: move
a/bunderx→ grants resolved viathe new chain (
x/...), old grants underano longer apply; old path redirects. - T5 — cross-tenant 404: principal of tenant T1 resolving a T2 path gets 404,
not 403, at API and data plane.
- T6 — default-deny: no grant on the chain → access denied (not implicit-allow).
- T7 — sibling-name uniqueness: creating a repo named
bwhere namespacebexists under the same parent is rejected.
Anti-patterns
- A privileged hardcoded "organization" tier (defeats the recursive model).
- Per-repo permission copies (breaks chain-relative resolution; rewrites on move).
- 403 on cross-tenant (leaks existence — MUST be 404).
- Recursive-CTE per request on the hot path (use the materialized
path, R3).