KVS namespace tree & permission resolution
Corpo da especificação
Spec — KVS namespace tree & permission resolution (resolves kvs-RFC-002 OQ-1)
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 a higher 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 (internal/host/auth) 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 clones/refs/MCP
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 = ''; writes/updates/deletes 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 discovera/b. - T4 — move re-parents permissions: move
a/bunderx→ grants resolved via the 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).
Referências
rfcs/kvs-RFC-002-concurrent-ai-first-forge-on-kdb.kmdspecs/multi-tenancy/contract.kmdpolicies/multi-tenant-by-default.kmdrfcs/stack-RFC-001-kdb-as-unified-data-plane.kmd