Pular para o conteúdo

KVS namespace tree & permission resolution

kvs specs/kvs/namespace-tree.kmd

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:

  1. Walk the chain N → root.
  2. Collect every grant for S (directly or via S's groups) on each ancestor.
  3. Nearest-ancestor wins for a given permission bit; a grant on N overrides a grant on N's parent, which overrides the grandparent, etc.
  4. An explicit deny at any level overrides an inherited allow from 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).
  5. 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 write at a/b, read at a → S has write on a/b/repo, read on a/x/repo.
  • T2 — explicit-deny: allow read at a, deny read at a/b → S cannot read a/b/repo; a nearer allow read at a/b/c restores it on a/b/c/repo.
  • T3 — visibility clamp: a private, a/b public → a/b effective = private; anon cannot discover a/b.
  • T4 — move re-parents permissions: move a/b under x → grants resolved via the new chain (x/...), old grants under a no 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 b where namespace b exists 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