← Developers

01.1 — Meta-kernel & tenant isolation

Spec matching SPEC-DX-01

The Invariant

The edge runtime is the only writer of business data. No route handler, script or plugin writes biz_* tables directly — every mutation goes through a manifest-declared trigger, which bundles RBAC, constraint, FSM enforcement, data-link mapping, audit and outbox emission in a single transaction.

Second invariant: tenants cannot see each other. PostgreSQL Row-Level Security scopes every biz_*/ax_* row to the session’s tenant_id — isolation is enforced by the database, not by application code remembering a WHERE clause.

The Contract

A manifest declares the engines — shape from manifests/LayoutManifest.production-line.example.json:

{
  "action_engine": {
    "transitions": [{ "from": "VERIFICATO", "to": "ACCETTATO", "trigger": "ACCEPT_LOT" }]
  },
  "data_link_engine": {
    "queries": {
      "ACCEPT_LOT": { "type": "INSERT", "table": "biz_lotti",
                      "mapping": { "lotto": "$payload.lot_id",
                                   "operator_id": "$context.operatorId" } } }
  },
  "rbac_engine": { "permissions": { "MAGAZZINIERE": ["ACCEPT_LOT", "…"] } },
  "constraint_engine": { "pre_commit_rules": [{ "id": "peso_coerente",
      "expression": "$formula.weight_delta_pct <= 0.5 && $formula.weight_delta_pct >= -0.5",
      "triggers": ["ACCEPT_LOT"] }] }
}

Session context injected by the runtime — payload cannot override it:

interface EdgeContext {
  tenantId: string;     // from session, enforced by RLS
  deviceCode: string;   // $context.deviceCode — device identity
  userRole: string;     // MAGAZZINIERE | OPERATORE | QUALITA | ADMIN | …
  operatorId: string;   // who actually touched the machine
}

The Flow

POST /api/v1/actions/:trigger
  → session auth (ax_sessions, expiry enforced)
  → RBAC check (role ∈ permissions)          → 403 ACTION_FAILED
  → constraint check (pre_commit_rules)      → 400 ACTION_FAILED
  → FSM check (from → to declared)           → 409 ACTION_FAILED
  → data-link write + audit row + outbox     [same transaction]
  → 200 { committed, to_state, … }

Payload Example

# Accept a lot — the weight constraint must hold or nothing commits
curl -X POST http://edge:8291/api/v1/actions/ACCEPT_LOT \
  -H "Authorization: Bearer $TOKEN" -H "X-Idempotency-Key: $(uuidgen)" \
  -d '{"payload":{"lot_id":"LOT-001","peso_dichiarato":100.0,
        "peso_misurato":100.3}}'

Known Failures

Symptom Cause → fix
401 AUTH_REQUIRED Session TTL lapsed or revoked → login again
409 ACTION_FAILED FSM transition not declared from current state → inspect manifest transitions
403 ACTION_FAILED Role lacks the trigger → manifest permissions are the law
400 ACTION_FAILED pre_commit_rule failed → the error message names the rule
Empty LIST_* result that “should have rows” RLS: you are looking at a different tenant/device than you think
Write works but cloud never sees it Outbox drains asynchronously — check 02-cdc-outbox, not the biz table