Safe, Governed AI Data Engineering: working paper

Working paper · internal

Safe, Governed AI Data Engineering on Spark

Can you let an AI agent write your production data pipelines without trusting it? This paper measures the risk, draws the boundary that makes an agent safe to run untrusted, and builds an open platform that enforces it, with per-tenant isolation demonstrated end to end on a live cluster.

Updated 2026-09· git 1ec5a94· 4 sections + 2 reference appendices demonstrated configured, unrun frontier

Overview

The one-minute summary and the headline findings per section. Start here, then follow any card into the full text below.

What this is, in one minute

AI coding agents now write real data pipelines. The failure that matters is not a crash, it is a pipeline that runs green and silently corrupts the data. So: can you get the productivity of an agent writing your pipelines without trusting the agent? This paper answers in four parts, building from a measurement to a running system.

  1. The risk, measured (§1): a controlled study of the same agent writing pipelines imperatively versus in a declarative framework (SDP).
  2. The agent-native dev loop (§2): a new inner loop, propose, gate, reconcile, that closes before any data; the control boundary that enables it lets a declarative agent run fully untrusted.
  3. The platform, built and demonstrated (§3): an open governed stack that isolates every tenant from every other, on a live cluster.
  4. Running it at fleet scale (§4): orchestration that holds credentials so the agent never sees one.

Where to start: the cards below give the headline findings and metrics per section and link into the full text; §1 carries the study evidence, §3 the architecture and the live isolation proof. The two reference-spec appendices are executable build targets.

Sections at a glance

Section 1Complete

The risk, measured

A controlled, pre-registered study of the same agent writing Spark pipelines two ways, imperative vs declarative (SDP).

  • 79 vs 0 structural defects caught at the gate before any data moves (SDP vs bare imperative)
  • ~34x the compute burned by imperative (~1000x on failed runs): a broken pipeline never starts under SDP
  • ~half the code (-49% lines) at ~2.3x tokens, comparable task completion
  • the raw "SDP looks less safe" gap is skill-induced, not paradigm-inherent (timezone defects 7→0 once taught)
  • 528-run powered study, frozen instrument
Read Section 1 →
Section 2Demonstrated

The agent-native dev loop

A new inner loop for agents, propose, gate, reconcile, that closes before any data; the control boundary that enables it is what lets the agent run fully untrusted.

  • a new inner loop: propose → dry-run gate → reconcile, closing before any data, not the imperative write-run-find-out
  • the enabling boundary: the agent emits only inert desired-state, never a session or credential, so it can be run fully untrusted
  • demonstrated across hosts to L3; dev→prod is a one-URL change (Spark Connect)
Read Section 2 →
Section 3Demonstrated on live EKS

The platform, built and demonstrated

An open governed stack that isolates every tenant from every other, proven layer by layer.

  • five per-tenant isolation layers, all demonstrated on a live EKS cluster
  • an agent as tenant A cannot reach tenant B by any path: routing · token custody · execution · catalog authz · storage
  • cross-tenant AccessDenied both directions; fleet role makes 0 warehouse data calls (CloudTrail); un-granted principal 403
  • only multi-tenant scale remains frontier
Read Section 3 →
Section 4Proposed

Running it at fleet scale

The orchestration layer over a fleet of governed agents.

  • credential custody, demonstrated (S4.3): one custodian holds + rotates every credential, a credential-free fleet, §3 isolation preserved
  • governed fleet, demonstrated on the platform (S4.5): Polly built medallions for three isolated customers over their own live tenants, cross-vendor routed + reviewed, credential-free via the custodian, repaired to pass under each customer's contextual policy, every cross-tenant read denied
  • the pattern this paper was built with: heterogeneous agents, adversarial cross-review, one shared governed skill set
  • the quantitative fleet study (cost / quality numbers) is a separate experiment (S4.6)
Read Section 4 →

Two reference-spec appendices (S2-A, S3-A) are executable build targets, skim unless you are building against them. Full paper below.

Safe, governed AI data engineering on Spark · a working paper in four parts

The question. Agents already write production data pipelines. The failure that actually hurts isn't a crash. It's a job that runs green and quietly ships wrong data. So: can you get the productivity without trusting the agent?

The answer, short. (1) A controlled 528-run study: the same agent wrote pipelines two ways. Declarative (SDP), it had a structural dry-run that caught 79 defects before data moved, vs 0 for bare imperative — and because broken pipelines get rejected before executors start, imperative burned ~34× the cluster compute (~1000× on failed runs). (2) A dev loop built around what the agent can and can't touch: propose, gate, reconcile. The agent only ever emits an inert plan. It never holds a session, credentials, or data. (3) The platform that makes this real for many tenants: Spark Connect, Kubernetes, and a governed catalog, with all five per-tenant isolation layers live on EKS. Tenant A can't reach tenant B. (4) Omnigent, the fleet layer: one custodian holds every credential so no agent ever sees one. Demonstrated: one brief, one autonomous agent, three isolated tenants, governed medallion pipelines on the live platform. The quantitative fleet study is a separate paper.

Each section carries a maturity label. §1 and §3 are the measured evidence; §2 and §4's core are demonstrated; §4's numbers are deferred.

SECTION 1: Imperative vs SDP Complete

A Safety-and-Cost Study of AI Agents Writing Spark Pipelines

Abstract (Section 1)

The expensive failure isn't a crash. It's the silent defect: a pipeline that completes, passes its checks, and ships data that's quietly wrong. We asked whether the authoring paradigm changes how safely an agent writes Spark pipelines, and at what cost. One manipulation, everything else frozen: 22 tasks, 12 seeds, same model, same prompt, same decoding. Arm A wrote bare imperative PySpark. Arm B wrote Spark Declarative Pipelines (SDP) with the framework's built-in dry-run and a minimal API skill. 528 runs, 264 per arm. SDP's dry-run caught 79 structural defects before any data was processed; imperative caught 0 and met the same bugs at runtime. On the semantic residue no gate can see, the raw number made SDP look worse (0.326 vs 0.277 silent-defect rate). A controlled skill-swap traced that to one missing idiom, not the paradigm: teaching the skill how to bucket a UTC day took timezone defects from 7 to 0. SDP wrote about half the code (−49% lines, −44% AST) for ~2.3× the tokens, and because a dry-run rejection starts no executors, SDP can't burn cluster compute on a broken pipeline at all. Imperative used ~34× the total compute, ~1000× on failed attempts. Declarative structure buys a real, early safety margin on structural faults, and it doesn't make semantic faults worse as long as the skill actually teaches the paradigm.

Introduction

Pipelines populate warehouses. They feed dashboards, reports, and numbers somebody eventually signs off on. A pipeline that throws an exception is one you fix. The dangerous one runs to completion and ships data that's quietly wrong: a dropped currency, a mis-bucketed day, a dedup that changes the numbers run to run. Nothing announces those. They surface downstream, long after the agent moved on.

Whether the agent writes them depends on more than the model. It depends on the paradigm it's asked to write in. With imperative PySpark the agent owns a live SparkSession and executes transformations directly. With SDP the agent declares the pipeline as desired state, a set of materialized views, and the framework assembles the graph and dry-runs it before data moves. The hypothesis: because SDP inspects the whole graph up front, it should catch a class of defects (unresolved columns, broken dependencies) that imperative only discovers at runtime, or never.

Root causes, operational definitions, and the pre-registered protocol live in SUPPLEMENT-Section1.md as §SM1–§SM7.

Reader's map

Terms, once:

Platform terms arrive from §2 on: Spark Connect (session-less client/server front end; the client ships a plan, not code), driver/executors (planner vs worker pods), mTLS (identity from certificates, not claims), IRSA (keyless AWS auth from Kubernetes), vend (short-lived prefix-scoped credentials issued on demand), reconcile (a controller pushes infrastructure to match the declared spec), Lakekeeper + OpenFGA (governed Iceberg-REST catalog + Zanzibar-style authorization).

Background

Imperative is what the base model knows natively: acquire a SparkSession, read inputs, transform, run. The agent is in full control and fully responsible. SDP inverts it. The agent writes decorated transformation functions, and the framework assembles the graph, resolves dependencies, and runs the pipeline. The agent never calls .start(), and in the governed setting later it never holds a session at all.

The property that matters is the dry-run. SDP resolves every view against the catalog and rejects structurally invalid pipelines — a column that doesn't exist, a view on a missing upstream table, an attempt to mutate immutable config — before a single executor touches data. Imperative PySpark has nothing like it, so the same faults become runtime exceptions after work already began. We treat the gate as intrinsic to the paradigm, not a bolted-on feature (F1, §1.4.2).

Defects split three ways (§SM3.2). Structural ones the gate can see. Semantic ones it can't, because the pipeline is well-formed and just computes the wrong answer. That semantic family is the silent-defect surface: the runs that complete and ship corruption. State defects need a live stream to manifest, so this study's offline oracle can't grade them; they're seeded in some tasks and left to future work. One consequence worth sitting with: a paradigm effect can appear only where the gate acts (structural defects), or in how well the agent was taught to handle semantic ones.

Section 1: defect taxonomy and what a structural gate can catch Defects split into three families. Structural defects (D1, D4, D5) are caught at the dry-run gate before any data. Semantic defects (D2, D6, D7, D8) are un-gateable by construction and ship as silent defects in completed output. State defects (D3, D9) are not scored offline. DEFECT TAXONOMY · WHAT A STRUCTURAL GATE CAN AND CANNOT CATCH StructuralD1 unresolved column · D4 broken DAGD5 immutable-config mutation Semantic / silentD2 timestamp · D6 dedup · D7 timezoneD8 silent row-drop StateD3 unwatermarked dedup · D9 unbounded state Dry-run gatecaught before any data Ships in output= silent defect Not scored offlineout of scope A paradigm effect appears only where the gate acts, or in how well the agent was taught the semantics.
What a structural gate can and cannot catch: structural defects are caught pre-data; semantic defects are un-gateable by construction and ship as silent defects.

Arm B gets the pyspark-sdp skill because the base model barely knows SDP's newer API. It's a 164-line API reference that teaches mechanics only and, in its own words, says nothing about what your pipeline should compute. An earlier spark-safety skill was scrapped after it moved the silent-defect rate by 0.000. The one idiom this minimal skill happens not to teach — bucketing a UTC calendar day — is exactly what the residual gap in §1.4.1 traces to.

Safety, token, and code-size results are substrate-independent, so they ran local. The compute question (H3) needs both paradigms on one uniform cluster, so it ran separately on Spark Connect/EKS (§SM6.5). One model everywhere: claude-opus-4-8, identical decoding (§SM7.4).

What we measure, and why

Safety and cost together, because a paradigm that's safer but slower to converge isn't automatically better. Five outcomes, pre-registered (§SM6.2):

Two choices keep this trustworthy. N1 and N2 are separated because a gate changes them differently. And "silent defect" plus the detection stage are defined against the instrument code before anyone looked at results, so the endpoints can't be redefined to fit the data.

Experimental setup at a glance

Manipulation one variable, the authoring paradigm. Arm A = bare imperative PySpark; Arm B = SDP + dry-run gate + pyspark-sdp skill
Scale 22 frozen tasks (7 Low / 8 Med / 7 High) × 12 seeds × 2 arms = 528 runs; 264 per arm, statistically powered
Model claude-opus-4-8, identical decoding; blind grading: the grader sees output, never the fix
Data deterministic synthetic event streams per (task, seed), with defect traps deliberately injected
Substrate: safety / tokens / code local: imperative on local Spark, SDP on local Spark Connect
Substrate: compute (H3) a real EKS Spark Connect cluster: client-mode driver pod + dynamically-allocated executor pods, Iceberg on S3, mTLS ingress
Instrument frozen (instrument-v3.2-frozen); every reported number cites a committed results file

The loop for one cell: generate the seeded data → agent proposes a pipeline → gate dry-runs the graph → execute → blind grade → record cost, repeat to a fixed iteration cap.

Section 1: the study run-loop for one cell For each (task, arm, seed): generate seeded data, the agent proposes a pipeline, arm B passes a structural SDP dry-run gate before execution while arm A has no gate, execute on Spark, blind-grade against ground truth, and record cost. Failures feed back to the agent up to an iteration cap. THE STUDY RUN-LOOP, FOR ONE CELL arm A (imperative) · no gate Generateseeded data Proposepipeline code Dry-runarm B · pre-data Executelocal / EKS Gradeblind · record cost failure feeds back · retry to the iteration cap
The study run-loop for one cell. Arm B passes a structural SDP dry-run before execution; arm A has no gate. Failures feed back to the agent up to an iteration cap.

Where the artifacts live, in open-lakehouse/safe-spark-agents-paper:

Links with line numbers pin the paper-v1 tag so they resolve against the exact revision the numbers came from. The raw generated data and transcripts ship as study/raw/sasa_raw_data_20260628.tar.gz.

1.1 Research question

Does forcing an agent into Spark Declarative Pipelines instead of imperative PySpark produce safer pipelines, and at what cost? The manipulation is paradigm, only paradigm. The gate and the skill are part of the controlled environment, not the treatment.

1.2 Design: one manipulation

Everything a reader might suspect of driving the difference — model, tasks, seeds, prompt, decoding — is held fixed, so any difference is attributable to paradigm alone. One deliberate asymmetry: the structural gate stays with arm B, because it comes with declarative authoring and can't be given to imperative without making it something other than imperative. That asymmetry is the finding (F1; F2 in §1.4.2).

Arm Paradigm Gate Skill
A imperative none (bare) none
B SDP (declarative) framework dry-run (built in) pyspark-sdp

Retired arms (imperative+gate+skill, SDP without skill) live in study/arms/supplementary/; the spark-safety skill was dropped everywhere after moving the silent-defect rate by 0.000 in pilot.

1.4 Results

Headline first, so it isn't missed: SDP's dry-run catches 79 structural defects before any data is processed; bare imperative catches 0. That's the load-bearing result. The rest of this section: the silent semantic residue around it (§1.4.1), gate validity (§1.4.2), cost (§1.4.3), and what each paradigm hallucinates (§1.4.4).

Every number below comes from one powered run: 528 cells on the frozen instrument, 0 instrument-fault rows, mixed-effects logistic inference with Holm correction and bootstrap CIs. Recomputation spec: §SM6.

1.4.1 Silent-defect rate: SDP looks worse until the skill swap

On the one thing a structural gate can't see, the raw powered run has arm B slightly higher. Don't stop at the raw number: this subsection shows the gap is one missing skill idiom, and it closes once the skill teaches it.

arm silent-defect rate k/n 95% CI
A (bare imperative) 0.277 73/264 [0.223, 0.330]
B (SDP) 0.326 86/264 [0.269, 0.383]

Δ = −0.049 [−0.098, +0.000]; OR = 1.97 (B vs A); GLMM p = 0.0033, Holm-adjusted. [src: study/results/results.powered.AB.n12.final.jsonl · silent_defect · recompute §SM6]

Which bugs carry the gap.

class A ships B ships read
D2 timestamp misparse 1 3 negligible
D6 nondeterministic dedup 38 39 a wash, not SDP-specific
D7 timezone / day-bucket 0 7 SDP-specific: imperative never ships it
D8 silent row-drop / bad currency 51 57 B worse by +6, task-concentrated

The whole gap is D7 (+7) and D8 (+6); the largest class, D6, is tied. A three-agent code audit found the mechanism (full forensics in §SM1): SDP's immutable-config rule removes the one-line session.timeZone = UTC fix imperative can use, and the base skill was silent on the replacement idiom. So the controlled skill-swap taught arm B the UTC day-bucketing idiom and re-ran: D7 went from 7 to 0 (study/results/results.tzfix.jsonl), matching imperative. The residue is skill-induced, not paradigm-inherent. Structure isn't unsafe. It needs a skill that teaches the paradigm-matched idiom, and then the arms reach parity. (A 3-seed pilot pointed the same way: A 18/66, B 23/66.)

§1.4.1: silent-defect composition by class, arm A vs arm B Silent semantic defects shipped, per class, comparing bare imperative (A) and SDP (B). D6 nondeterministic dedup is the largest class and is tied, 38 versus 39. D7 timezone/day-bucket is SDP-specific: imperative ships 0, SDP 7, and it closes to 0 once the skill teaches the idiom. D8 silent row-drop is B worse by 6 (51 vs 57). D2 timestamp misparse is negligible (1 vs 3). The whole A-minus-B gap is D7 plus D8; D6 is tied. SILENT-DEFECT COMPOSITION BY CLASS · WHO SHIPS WHAT A · bare imperative B · SDP D6 nondeterministic dedup 38 39 largest class · tied (a wash) not SDP-specific D7 timezone / day-bucket 0 7 SDP-only · imperative never ships it closes to 0 once the skill is taught D8 silent row-drop / bad currency 51 57 B worse by +6 · task-concentrated D2 timestamp misparse 1 3 negligible The whole A−B gap is D7 (+7) and D8 (+6); the largest class, D6, is tied.
The honest counter-signal, decomposed. Silent semantic defects shipped, per class, arm A vs arm B. The whole A−B gap is D7 (+7) and D8 (+6); the largest class, D6 dedup, is tied (a wash, not SDP-specific). D7 timezone is the sharp, skill-attributable driver, imperative ships zero and closes SDP to zero once the skill teaches the day-bucket idiom (§SM1).

1.4.2 Structural defects at the gate

Where structural defects (D1/D4/D5) get caught — defect-level, across all iterations; a gate-caught-then-fixed bug still counts:

arm at gate (dry-run) at runtime shipped
A (bare, no gate) 0 4 0
B (SDP) 79 30 0

SDP's framework dry-run intercepted 79 structural defects (353 iteration-level error events) before any data moved. Bare imperative has no gate, so its 4 structural catches surface at runtime after compute was spent. The gate wasn't a harness gift: it's what declarative pipelines do by construction. Bolting an artificial gate onto imperative (F2) was explicitly rejected — it would contaminate imperative with a feature it would never naturally have. The retired A2 arm's gate audit and design history are in §SM2.

§1.4.2: where structural defects are caught, SDP's gate vs bare imperative Structural defects (D1, D4, D5) reach a boundary marked "before any data is processed." Bare imperative has no structural gate, so zero are caught before data; four surface later at runtime, after executors have run and compute is spent. SDP runs a framework dry-run at that boundary and catches 79 defects before any data is processed; 30 more surface at runtime; neither arm ships a structural defect. WHERE STRUCTURAL DEFECTS (D1 / D4 / D5) ARE CAUGHT · THE LOAD-BEARING RESULT ◀ before any data after: executors run ▶ the dry-run gate sits on this line Imperative (bare)no structural gate 0 nothing caught early Runtime, over data4 surface · 0 shipped SDP (declarative)framework dry-run 79 caught before any data Runtime, over data30 surface · 0 shipped Neither arm ships a structural defect. The dry-run is intrinsic to declarative authoring, not a harness gift.
The load-bearing result. Structural defects (D1/D4/D5) meet a boundary, before any data is processed. Bare imperative has no structural gate, so zero are caught early; four surface later at runtime, after compute is spent. SDP's framework dry-run catches 79 at that boundary before any executor starts; 30 more surface at runtime. Neither arm ships a structural defect.

1.4.3 Cost: half the code, 2.3× the tokens, and a structural compute asymmetry

Conciseness (H4). Paired over (task, seed) on the final accepted program (Δ = A − B, positive means B wrote less; *_body excludes scaffolding):

metric B (SDP) A (imperative) Δ (A−B) 95% CI
final_program_loc 67.9 134.0 +66.1 [+61.9, +70.4]
ast_node_count 614.9 1105.5 +490.6 [+453, +530]

All CIs clear of zero: B is ~49% fewer LOC and ~44% smaller AST. [src: study/results/results.powered.AB.n12.final.jsonl · final_program_loc / ast_node_count]

Tokens (N1). Medians to a correct pipeline, both arms fully populated (264/264):

arm input output total vs A
A 1,436 9,964 11,524 1.0×
B 7,295 18,499 26,480 ~2.3×

SDP iterates more against its gate, and that shows up as tokens. [src: same results file · input_tokens / output_tokens · per arm]

Cluster compute (N2), the sharpest cost result. This one isn't a matter of degree. It's structural. Imperative runs spark-submit, spends executor time, and only then discovers the pipeline is wrong. SDP's dry-run rejects a structurally-invalid pipeline before any executor starts, so a failed SDP attempt costs ≈ 0 cluster compute by construction. 69.5% of arm B's attempts were intercepted at the gate, before touching data. Scope note: a semantically-wrong SDP pipeline passes the dry-run and burns compute like imperative — that's the un-gateable residue of §1.4.1.

Measured on one uniform EKS Connect cluster, imperative spent ~34× the total executor-seconds and roughly 1000× on failed attempts.

Dollar version (a projection from the measured mechanism, not a measured result): a real pipeline over ~100 GB whose failed attempt burns ~10 minutes across a 20-executor cluster wastes ≈ $0.60 per failed attempt. An imperative agent that fails ~2× before converging wastes ≈ $1.20 per pipeline, so at ~1,000 pipelines/week that's ≈ $5,000/month of cluster compute SDP never spends. Against that, SDP's ~15k extra tokens per pipeline cost ≈ $0.73 at Opus-class rates (~$15/$75 per million input/output tokens), ≈ $3,000/month at the same scale. Big pipelines favor the gate; small ones flip. All of it recomputes from the token deltas above.

§1.4.3 (N2): wasted compute on failed attempts, imperative vs SDP Executor-seconds spent on attempts that ultimately failed, measured as real Spark stage-diff executor-seconds and confirmed on EKS by a 48-cell sweep. Bare imperative wastes 521 executor-seconds ($0.028) because spark-submit executes over the data and only then discovers the fault; SDP wastes about 0.5 ($≈0) because its dry-run rejects the pipeline before any executor starts — roughly 1000× on failed attempts. Total compute runs ~34× (A $0.032 vs B $0.0009). At production scale a fleet authoring ~1,000 pipelines a week projects to ~$5,000/month of compute SDP's gate never spends. WASTED COMPUTE ON FAILED ATTEMPTS · MEASURED, 48-CELL SWEEP · CONFIRMED ON EKS Imperative 521 exec-s · $0.028 spark-submit runs over the data; the fault surfaces only after compute is spent SDP ≈0.5 exec-s · ≈$0 the dry-run rejects the bad pipeline before any executor starts, so ≈0 is wasted ≈ 1000× more wasted compute on failures · finite vs ≈0 also ≈ 34× total compute (A $0.032 vs B $0.0009) At production scale (projection from the measured mechanism): a fleet authoring ~1,000 pipelines/week wastes ≈ $5,000/month of compute that SDP's gate never spends.
The sharpest cost result (N2). Executor-seconds spent on attempts that ultimately failed: bare imperative burns 521 because spark-submit runs over the data before the fault surfaces; SDP burns ≈0.5 because its dry-run rejects the pipeline before any executor starts. Roughly 1000×; total compute ~34×. The projection scales the measured mechanism to production-sized tasks.
Cost of SDP relative to imperative: conciseness and tokens Arm B (SDP) expressed as a percentage of arm A (imperative = 100%). Mean lines of code: 51% (134 to 68). Mean AST nodes: 56% (1106 to 615). Median tokens: 230% (11.5k to 26.5k). SDP writes about half the code but spends roughly 2.3 times the tokens. SDP writes less SDP costs more 250% 200% 100% imperative (A) = 100% 0 51% 56% 230% Lines of code 134 → 68 AST nodes 1106 → 615 Tokens 11.5k → 26.5k
The cost of the declarative paradigm, arm B relative to arm A (= 100%): SDP writes far less code and spends more tokens. Bars show B as a percentage of A; absolute medians beneath.

1.4.4 What each paradigm hallucinates

Beyond whether an agent fails, what it invents differs sharply by paradigm, and the difference maps onto where the gate can act. (A qualitative characterization over the exploratory sweeps, not a powered magnitude. "Hallucination" = inventing something that doesn't exist, or writing code for the wrong paradigm; §1.4.1's silent defects are a separate axis.)

hallucination imperative (A) SDP (B)
invents an I/O path (nonexistent input/output location) 51 0
imperative session control (spark.conf.set) inside a declarative pipeline 0 76
invented / undeclared table or view 1 40
eager action (.collect()) inside a declarative query function 0 27
invented column name 3 21
invented / unsupported API 4 0

Imperative invents where the data lives: a hard-coded path that never resolves. Nothing structural can know a path is wrong until storage is touched, so 96% of imperative error-iterations surface at runtime and the agent loops re-guessing — a direct source of the wasted compute in §1.4.3. SDP's hallucinations are imperative habits leaking into a declarative frame, and the gate eats them: 80 of 96 spark.conf.set occurrences were rejected at the dry-run, 39% of SDP error-iterations die cheaply at the gate vs 3% for imperative. The same spark.conf.set line is legitimate in imperative (63 final programs keep it, no error): an identical keystroke, caught as hallucination in one paradigm and correct in the other. Neither arm hallucinated Databricks DLT (import dlt, @dlt.table): 0 in both — the SDP arm's governed skill keeps it on the OSS API. [src: study/raw/raw_20260628/all_results.jsonl · per_iteration error_class × arm × stage]

Section 1.4.4: what each paradigm hallucinates, and where it is caught The imperative arm and the SDP arm hallucinate different things. The imperative agent's dominant hallucination is inventing nonexistent I/O paths, which nothing structural can catch, so it surfaces only at runtime (96% of its error iterations) and drives fail-loops and wasted compute. The SDP agent's hallucinations are imperative habits written into a declarative pipeline (spark.conf.set, eager actions in a query function) plus invented undeclared tables and columns; 39% are caught at the cheap dry-run gate before compute. Neither arm hallucinated Databricks DLT. WHAT EACH PARADIGM HALLUCINATES, AND WHERE IT IS CAUGHT IMPERATIVE (A) · invents where the data lives invents a nonexistent I/O path51 invented / unsupported API4 caught only at runtime · 96% of error-itersun-gateable → re-guessing loops, wasted compute SDP (B) · confuses the paradigm spark.conf.set() inside a declarative pipeline76 invented / undeclared table or view40 eager .collect() inside a query function27 39% caught at the cheap dry-run gate, before compute the same spark.conf.set line — legitimate in imperative, a gate-caught hallucination in SDP Databricks DLT hallucination (import dlt, @dlt): 0 in BOTH arms. Imperative has no framework to confuse; the SDP arm's governed skill keeps it on the OSS API. The gate catches a whole class of hallucination cheaply; imperative's dominant one is un-gateable. The arms hallucinate different things — the gate moves SDP's from runtime to a cheap pre-data check.
What each paradigm hallucinates and where it is caught: imperative invents I/O paths (un-gateable, runtime); SDP writes imperative habits into the declarative pipeline, a large share caught at the cheap dry-run gate. Databricks DLT hallucination is zero in both arms.

1.5 Threats to validity

1.8 Conclusions

The study isolates the paradigm, and the safety result is clean. SDP's dry-run intercepted 79 structural defects before any executor started; bare imperative met the same faults only at runtime. That gate isn't a gift we handed SDP — it's a property declarative authoring has by construction and imperative can't have without ceasing to be imperative.

The counter-signal gets reported without softening. On semantic residue, SDP is slightly worse in the raw data (0.326 vs 0.277; OR 1.97, p = 0.0033). The gap is mostly timezone/day-bucket bugs carried by a missing skill idiom, and teaching it closed D7 7 → 0. The honest headline isn't "structure is unsafe". It's that structure alone isn't enough: it needs a skill that teaches the paradigm-matched idiom, and once it has one, the arms reach parity.

Cost is coherent. SDP wrote ~half the code for ~2.3× the tokens while completing at a comparable rate (65.2% vs 68.9%, a gap that tracks the skill-attributable D7 residue). Whether the token premium is worth an early structural margin and half the code is a judgment call; the study makes the inputs explicit either way. Two limits bound the claims: the compute comparison requires one uniform cluster (§1.4.3, §SM6.5), and the study fixes a single model while giving B an API skill A doesn't need.

The safety result motivates what's next. If the valuable thing about declarative authoring is catching faults before data moves, can we build a system where the agent never holds a live session at all? That's Section 2.


§1 methods. The methods appendix is a separate file, SUPPLEMENT-Section1.md: root-cause forensics (§SM1), gate history and retired arms (§SM2), operational definitions (§SM3), the pre-registered run protocol (§SM6), full materials and system (§SM7).

SECTION 2: The Agent-Native Development Loop Demonstrated

Abstract (Section 2)

§1 showed a declarative agent writes safer, shorter pipelines. This section shows why, and turns it into a loop. The agent's whole job is to declare the tables and stop. No session to open, no endpoint to point at, no credential to hold, no cluster to configure — so the loop can be built around a fallible, untrusted author. The agent proposes inert desired state; a gate rejects bad structure up front; a controller the agent doesn't control holds the credentials and reconciles. Authoring and execution get separated by construction, and that separation is what lets the same agent run fully untrusted. We built it and ran real agents through it, end to end and across hosts on a live EKS cluster.

Introduction

One developer, one pipeline. §2 walks the loop step by step: why run-to-debug is wrong for an agent (§2.1), what the agent writes (§2.2), the loop itself (§2.3), what it buys (§2.4), dev vs prod (§2.5), and where it honestly stands (§2.6). The multi-tenant, production-grade version of this boundary is §3's problem.

2.1 The problem: building a pipeline by running it is wrong for an agent

The normal way to build a Spark pipeline is imperative: write code, run it, learn what's wrong from what breaks. Every mistake costs a run. That loop was designed for a trusted human at a keyboard, and it assumes the author holds a live session, warehouse credentials, and a cluster endpoint. An untrusted agent can safely be given none of those.

An agent holding a SparkSession is a governance problem with no gate before data, no line between what it authors and what it runs, and nothing to audit or contain. It can mutate config, read or overwrite arbitrary tables, burn compute on pipelines that never worked, or ship §1's silently-wrong data. This section shows the alternative: give the agent a full production platform without ever handing it the runtime keys.

The agent-native dev loop vs the traditional developer loop Two loops, contrasted. The traditional loop is built for a trusted developer: work in an IDE holding a live session and credentials, submit with a tool (spark-submit, Jobs, Airflow), execute over real data, and hit the error only at runtime, so every retry happens after data is touched. The agent-native loop is built for an untrusted agent that holds none of that: it proposes inert desired-state, a dry-run gate rejects bad structure before data, then a controller (never the agent) reconciles and executes; the retry happens at the gate, with no data touched. TRADITIONAL LOOP · a trusted developer submits and runs; the error surfaces at runtime IDE / notebooklive session + creds Submit to platformspark-submit / Jobs / Airflow Executeover real data Runtime erroror wrong data retry after data is touched and compute is spent · every step assumes a trusted author AGENT-NATIVE LOOP · an untrusted agent proposes; the error is caught at the gate, before data Propose (PR)inert spec, no session Dry-run gate (CI)structural check, before data Controllerreconcile & execute Materialized dataover Spark Connect retry at the gate, no data touched, ≈0 compute · the agent holds only the inert spec Same shape, reimagined: the submission tool becomes a reviewed PR, and the controller runs it.
The agent-native loop vs the traditional developer loop. The traditional loop is built for a trusted developer: work in an IDE holding a live session and credentials, submit to the platform with a tool (spark-submit, Jobs, or Airflow), execute over real data, and hit the error only at runtime, so every retry follows touched data and spent compute. The agent-native loop is built for an untrusted agent that holds none of that: it proposes an inert spec, a dry-run gate catches structure before any data, then a controller (never the agent) reconciles and executes; the retry happens at the gate, no data touched. Same shape, the submission tool becomes a reviewed PR.

2.2 What the agent writes

One property does all the work: the agent authors an inert description of desired state and can touch nothing else. The two paradigms make fundamentally different artifacts.

# Imperative: the agent owns and runs the session; authoring IS execution.
spark = SparkSession.builder.getOrCreate()
orders = spark.read.parquet("s3a://warehouse/orders/")
orders.filter(...).write.saveAsTable("orders_clean")   # runs live data ops now

# Declarative (SDP): the agent writes only inert desired-state; the framework runs it.
@dp.materialized_view
def orders_clean():
    return dp.read_table("orders").filter(...)          # a declaration, nothing executes

2.3 The dev loop, step by step

Author → PR → gate → reconcile. The agent writes the transform and the spec and opens a PR. CI dry-runs the changed specs against the real catalog, so a structural bug is rejected before merge, before any data moves: a spec with a missing upstream fails the gate with [TABLE_OR_VIEW_NOT_FOUND] … SQLSTATE 42P01, a valid one dry-runs to Run is COMPLETED. On merge, the reconciler — never the agent — runs it over Spark Connect. The agent that authored it holds no credentials and never ran it. That's review plus a real integration gate plus controller-owned execution plus a full audit trail, with the agent strictly outside the runtime.

The control boundary: the imperative agent crosses it, the SDP agent stops at it One control boundary separates authoring from execution. The obvious way: an imperative agent holds a live SparkSession and credentials and calls df.write.saveAsTable, so authoring IS execution and the write crosses the boundary to touch data. The agent-native way: an SDP agent writes only @dp transforms, uses SparkSession.active() read-only, and produces an inert spec that stops at the boundary; only the platform crosses, carrying the spec through the dry-run gate and controller to the same execution engine. THE CONTROL BOUNDARY · THE IMPERATIVE AGENT CROSSES IT, THE SDP AGENT STOPS AT IT AUTHORING · what the agent writes EXECUTION CONTROL BOUNDARY EXECUTIONdriver + executor podsthe only things thattouch data THE OBVIOUS WAY · imperative agent (uncontained) Imperative agentholds a live SparkSession + credentialsdf.write.saveAsTable("gold") the write CROSSES the boundary authoring IS execution · data touched AGENT-NATIVE · the SDP agent writes, then stops SDP agent (untrusted)writes only @dp transformsSession.active() · read-only inert specstops at the wall gate +controller only the platform crosses: the spec goes via the gate and controller, never the agent Absent from the agent's surface (I1/I2): no session · no credentials · no endpoint · no cluster config · no run lifecycle
The control boundary, two authors. The obvious way: an imperative agent holds a live SparkSession and credentials and calls df.write.saveAsTable, so its authoring IS execution, the write crosses the boundary and touches data. The agent-native way: the SDP agent writes only @dp transforms (SparkSession.active() is a read-only handle) and produces an inert spec that stops at the boundary; absent from its surface are any owned session, credentials, endpoint, cluster config, or run lifecycle (I1/I2). Only the platform crosses, carrying the spec across through the dry-run gate and controller to the same execution engine.
The agent-native dev loop: author, open a PR, gate on CI, reconcile over Connect An untrusted agent authors an inert SDP spec (no session, credentials, or config) and its only action is to open a pull request. CI gates the PR with a spark-pipelines dry-run that validates the dataflow graph without touching data: valid reports Run is COMPLETED, broken fails with TABLE_OR_VIEW_NOT_FOUND and feeds back to the agent. Only on merge does a controller, never the agent, run spark-pipelines run over Spark Connect, shipping plans over mTLS to driver and executor pods that materialize tables to the governed catalog and S3 with the controller's credential. The same loop targets a local Connect server in dev or the cluster in prod by changing SPARK_REMOTE. THE AGENT-NATIVE DEV LOOP · AUTHOR → PR → GATE → RECONCILE OVER CONNECT UNTRUSTED AUTHOR Agentno session, no creds, no config SDP specinert @dp transforms Pull requestthe only interface in control boundary GOVERNED PLATFORM CI gatespark-pipelines dry-run --spec orders/spark-pipeline.ymlbuilds + validates the graph · touches no datavalid: Run is COMPLETEDbroken: TABLE_OR_VIEW_NOT_FOUND feedback, no data touched on merge (valid only) Controller (never the agent)spark-pipelines run --speca Connect client, ships plans not code Connectplans · mTLS Clusterdriver + exec pods materialized tables · governed catalog + S3read with the controller's credential, never the agent's Same loop, two endpoints: local Connect (dev) or the cluster (prod) — change SPARK_REMOTE only.
The agent-native dev loop. An untrusted agent authors an inert SDP spec (no session, credentials, or config); its only action is to open a pull request. CI gates the PR with spark-pipelines dry-run, which validates the dataflow graph without touching data (valid: Run is COMPLETED; broken: TABLE_OR_VIEW_NOT_FOUND, fed back to the agent). Only on merge does a controller, never the agent, run spark-pipelines run over Spark Connect, shipping plans to a cluster that materializes tables with the controller's credential. The same loop runs locally or on the cluster by changing SPARK_REMOTE.

Honest seams, named up front: the loop runs locally and the gate targets runner-local Connect, not the EKS endpoint (study/gitops_demo/PRODUCTION_EKS.md). No captured run of a real agent-opened PR exists in public. Wiring the CI controller's identity to the per-tenant runtime certificate is design-only. And the gate checks structure: data-quality/expectation tests — the natural place to catch §1's silent semantic bugs — aren't built.

2.4 What this buys

The agent's surface is declare-the-tables, so there's no session lifecycle, endpoint, or credential to get right, and effort goes to the transform instead of plumbing. That's part of why it writes ~half the code of imperative (§1.4.3) and still completes (§1.4, H5). The containment properties follow. It's untrusted by design: no session, no credentials, nothing to leak. Structural mistakes die at the gate (79, §1.4.2) instead of at runtime. A broken pipeline can't burn cluster compute, because it never runs (§1.4.3). And the output is an inert, reviewable artifact, so the loop audits cleanly and — since nothing the agent writes is ever run by the agent — scales to many tenants (§3). The price: ~2.3× the tokens from iterating against the gate. Cheap model calls to save cluster compute, wrong data, and blast radius. For an untrusted author on production data, that's the trade you want.

2.5 Develop locally, promote by changing one endpoint

The controller is a plan-builder pointed at a URL, so the same agent, spec, and gate run against a local Connect server while iterating — fast and free — and promote to the EKS cluster by changing one variable, SPARK_REMOTE, with no code change. The swap happens entirely outside the agent's surface: the agent authors one inert spec, byte-identical in dev and prod, and never sees or sets the variable. Where the pipeline runs isn't the agent's concern, and can't be.

Spark Connect: one client, one plan, two endpoints (dev-to-prod by changing SPARK_REMOTE) The client side is identical in dev and prod: the agent authors inert @dp transforms, a CI dry-run gate checks them, and a controller builds one serialized plan, holding no engine. That plan ships over gRPC to whatever SPARK_REMOTE points at. In dev the URL is a local Connect server with driver and executors in one in-process JVM. In prod the same plan goes to the EKS Spark Connect cluster, where a driver pod schedules executor pods that read the S3 warehouse through the Lakekeeper catalog behind mTLS. Promoting dev to prod changes only the URL; the server alone touches data. ONE CLIENT · ONE PLAN · TWO ENDPOINTS THE CLIENTidentical in dev and prod Agentauthors inert @dp transforms SDP specone inert artifact CI dry-run gatestructural check, no data Controllerbuilds ONE serialized planno SparkContext, no engine same agent · spec · gate · plan in both modes plan gRPC SPARK_REMOTEthe one thingthat changes DEV · local Connect serverdriver + executors in ONE in-process JVMthe whole engine in one JVMsc://localhost:15002fast · free · ungoverned (dev only) PROD · EKS Spark Connect clusterdriverpod S3 Lakekeeper · mTLS · vended creds sc://<cluster-nlb>:15009 executors alone touch data Same client, same plan, same gate; only the URL changes, and the client never holds an engine. The server (driver + executors) runs it and alone touches data; dev → prod is one SPARK_REMOTE change.
One client, one plan, two endpoints. The client side (agent, SDP spec, dry-run gate, controller) is identical in dev and prod and holds no engine; it ships one serialized plan over gRPC to whatever SPARK_REMOTE points at. In dev that is a local Connect server (driver + executors in one in-process JVM, fast and ungoverned); in prod it is the EKS cluster (driver pod + executor pods reading the S3 warehouse through the Lakekeeper catalog behind mTLS). The executors are the only thing that touches data. Promote dev→prod by changing the URL alone.

There's a precise reason SDP keeps this promise where imperative only mostly does. Connect refuses to hand a client the engine: SparkContext, the JVM, RDDs, and static cluster config stay server-side and error if you reach for them (JVM_ATTRIBUTE_NOT_SUPPORTED, CANNOT_CONFIGURE_SPARK_CONNECT_MASTER). So there's a privilege ladder: the cluster admin owns the engine, the controller may submit plans but never touch it, and the agent holds not even a session. SDP sits on the bottom rung by construction; imperative runs over Connect too, but agents routinely reach for sparkContext or _jvm, so imperative only happens to stay in bounds when Connect stops it.

The Spark Connect privilege ladder: agent, controller, cluster admin, and who can touch the engine Three privilege tiers separated by the Spark Connect boundary. The agent is an untrusted author holding only an inert @dp transform — no session, credentials, or endpoint — and cannot run anything, misconfigure, or reach a credential or SparkContext. The controller is a Connect client that submits serialized plans and holds the S3 credential, but cannot reach SparkContext, the JVM, RDDs, or cluster config; Connect errors if it tries. Below the boundary is the server side: the cluster admin owns the engine itself, SparkContext, the JVM, the executor pods, and cluster config. SparkContext never leaves the cluster admin's side. THE SPARK CONNECT PRIVILEGE LADDER · WHO CAN TOUCH THE ENGINE AGENTuntrusted authorleast privilegedHOLDSan inert @dp transformno session · no credentials · no endpointCANNOTrun anything · misconfigure · reach credsor the SparkContext CONTROLLERthe Connect client(never the agent)HOLDSa Connect session · submits serialized plans+ the S3 credential that reads the warehouseCANNOTSparkContext · JVM · RDDs · cluster configConnect: JVM_ATTRIBUTE_NOT_SUPPORTED SPARK CONNECT BOUNDARY CLUSTER ADMINserver side / operatorowns the engineOWNS THE ENGINESparkContext · JVM · executor pods · cluster configthe only tier that touches the running engine and dataSparkContextlives only here A plan can cross the line; the engine cannot be touched by a client. SparkContext never leaves the cluster admin's side: SDP never asks, imperative is stopped when it does.
The Spark Connect privilege ladder. The agent holds only an inert @dp transform (no session, credentials, or endpoint) and cannot reach the SparkContext. The controller, as a Connect client, submits serialized plans and holds the S3 credential but cannot reach SparkContext, the JVM, RDDs, or cluster config, Connect errors if it tries. Both are the client side. Below the Spark Connect boundary the cluster admin owns the engine, SparkContext, JVM, executor pods, and cluster config. A client can submit a plan but cannot cross into the engine, so SparkContext never leaves the cluster admin's side.

2.6 Where this stands, and what §3 takes on

This ran for real: a live agent (claude-opus-4-8) over §1's full corpus, and separately across hosts on a real EKS Connect cluster (ssa-spark-eks), where the controller submitted over Connect, driver and executors ran in Kubernetes pods, and SDP pipelines materialized to Iceberg on S3. Authoring and execution were genuinely separate machines. Two gaps are named so nothing reads as more finished than it is: those remote runs reached Connect through a socat tunnel instead of native mTLS, and the reconciler still runs alongside the agent rather than split onto the platform (live.py:675-689). Neither changes the boundary. The invariants and layer-by-layer build state are Appendix S2-A.

§3 is what an infrastructure engineer stands up to make this boundary real and multi-tenant: governed catalog, Kubernetes execution, per-tenant credential vending, and the trust model that keeps one agent's blast radius off every other tenant.


SECTION 3: The Open Reference Architecture Demonstrated

Abstract (Section 3)

§2's boundary, seen from the infrastructure side. §2 gave one developer a loop where the agent's only artifact is an inert PR; §3 takes that as given and stands up the platform underneath it, adding four pieces §2 didn't have: a governed catalog (Iceberg-REST, demonstrated on Lakekeeper + OpenFGA), Kubernetes execution (a client-mode Connect driver spawning executor pods), credential vending (the catalog mints short-lived, prefix-scoped credentials, never the agent), and per-tenant trust. The center of gravity is a five-layer isolation stack — L1 ingress routing, L2 token custody, L3 execution isolation, L4 catalog authorization, L5 storage scoping — whose one job is to make it impossible for a tenant-A agent to reach tenant-B's data by any of five paths. All five run on a live EKS cluster today. The one capability still ahead is multi-tenant scale.

Introduction

From the infrastructure engineer's seat, §3 is a separation of concerns: each layer owns exactly one thing and delegates the rest (§3.0). §3.1 is the GitOps/CI boundary that tests and reconciles every change; §3.2 is Connect-on-Kubernetes, the governed ingress and the elastic execution behind it; §3.3 is the section's core, the five-layer isolation stack, walked link by link; §3.4 states where each pillar stands; §3.5 hands credential custody at fleet scale to §4/Omnigent.

Because §2's agent only emits inert desired state, §3's platform can trust it with nothing: SDP for authoring, a GitOps/CI layer for integration, Spark Connect as the identity-pinned door, Kubernetes for execution, and a governed catalog for authorization and credential vending. The isolation was re-verified on 2026-07-14 (paper/notes/proof_2026-07-14_live_isolation.log); §3.4 maps what runs versus what's still ahead.

Section 3: the open governed reference architecture Three trust zones: untrusted agent authoring (Zone U), governed control plane (Zone C), and the EKS data plane (Zone D), connected by a GitOps loop. Spark Connect is the single identity-pinned door; executor elasticity is configured but unrun; per-tenant governance is demonstrated on live EKS with multi-tenant scale the remaining frontier. THE OPEN GOVERNED REFERENCE ARCHITECTURE demonstrated configured, unrun frontier ZONE U · UNTRUSTED AUTHORING (agent holds no creds) AI agentauthors spec SDP spec (inert)pipeline.yml + code Pull requestagent's only output ZONE C · GOVERNED CONTROL PLANE (holds creds) CI dry-run gatechecks vs the real catalog Reconcilerapplies desired-state ZONE D · DATA PLANE (EKS · the only place data is touched) Connect ingressmTLS · pins principal Spark driverclient-mode pod Executor podsdynamic 0 → 10 Governed catalog (Lakekeeper)per-principal authz + scoped vend S3 warehouseIceberg tables Tenant governance · five isolation layers, demonstrated on live EKSonly multi-tenant scale remains the frontier (see the isolation-chain figure)
The open governed reference architecture: three trust zones, untrusted agent authoring, a governed control plane, and the EKS data plane, joined by a GitOps loop, with Spark Connect as the single identity-pinned door and per-tenant isolation demonstrated on live EKS. Solid = demonstrated · dashed = configured but unrun · dotted = frontier.

3.0 One layer, one job

Layer Owns Delegates / does NOT own
SDP declarative authoring: what the pipeline is execution, identity, tenant authz
GitOps / CI continuous test + integration against a real target catalog; reconciliation defining the pipeline; enforcing grants
Spark Connect the single governed, identity-pinned ingress the data work itself (executors)
Kubernetes elastic execution: client-mode driver + executor pods the governance boundary (stays at the ingress)
Catalog (governed Iceberg-REST; Lakekeeper + OpenFGA, vendor-neutral) per-principal authorization, grants, credential vending being reinvented by SDP or GitOps

3.1 GitOps / CI: tested integration, not blind submission

The naive setup lets an agent submit code straight to a cluster: a live session in the agent's hands, untested and unreviewed changes in prod, no gate, no audit. The fix: the agent's only output is a PR. CI triggers on any PR touching pipeline-definitions/**, stands up a real Connect server, resolves the changed specs, and dry-runs each against actual catalog and schema state. Broken DAGs and missing upstream tables are caught before merge, before any data moves. On merge, reconcile.py does controller-owned execution. The agent that authored it holds no credentials and never ran it.

Honest scoping: the full author→PR→gate→reconcile loop runs locally against runner-local Connect; the production-EKS GitOps path is documented but not wired (study/gitops_demo/PRODUCTION_EKS.md). No public run of a real agent-opened PR is captured. The authoring→runtime bridge — the CI controller presenting a per-tenant client cert (SAN spiffe://safe-spark-agents/tenant_a) to the gateway — is design-only, named here rather than left implicit. And the gate is structural: data-quality/expectation tests, the obvious extension, aren't built.

3.2 Connect on Kubernetes: governed ingress, elastic execution

Spark Connect is Spark's session-less front end: a client sends a query plan to a shared server, never code to execute. The driver plans and coordinates; executor pods do the work. Scale happens behind the governed door, never around it.

The boundary lives in three facts. One external port, mTLS only: TCP 15009 is the sole externally reachable endpoint; raw Spark Connect (15002) is never exposed. Envoy terminates that mTLS, reads identity from the client certificate — discarding any identity the client tried to assert — and stamps the verified principal on the request. And the Connect server listens only on loopback, so the proxy is the only way in: every session arrives with a cryptographically pinned principal no matter how execution scales behind it.

Driver + executors: the Connect pod is a client-mode driver (spark.master=k8s://…, deployMode=client) that creates executor pods through in-cluster RBAC scoped to its own namespace, pinned via a pod template to executor-labeled nodes with topology spread. Dynamic allocation is configured 0→10.

Connect on Kubernetes: one governed mTLS door, elastic executor pods on a separate node pool behind it A Connect client reaches the cluster only through one external mTLS endpoint on port 15009. That endpoint is an Envoy sidecar in the Spark Connect driver pod, which pins the caller's principal and forwards over loopback to the Connect server on 127.0.0.1:15002 (never externally reachable). The driver pod is a singleton on the untainted system node pool and is the client-mode Spark driver: through an RBAC service account it schedules executor pods on a separate tainted executor pool (spark-role=executor), scaling 0 to 10 by dynamic allocation. Executors read the S3 Iceberg warehouse with the vended, prefix-scoped credentials. CONNECT ON KUBERNETES · ONE GOVERNED mTLS DOOR, ELASTIC EXECUTORS BEHIND IT Connect clientcontroller / agent mTLS :15009 SYSTEM NODE POOL · untainted Spark Connect driver pod (replicas: 1) Envoy mTLS:15009 · externalpins principal Connect server127.0.0.1:15002loopback only client-mode driver · spark.master=k8s://… deployMode=client · driver.host=$(POD_IP) RBAC: create / watch / delete pods (own ns) only :15009 is external · :15002 is Envoy-only kube API · kubernetes.default.svc EXECUTOR POOL taint: spark-role=executor executor pod2 cores / 2g executor pod2 cores / 2g executor pod …up to 10 · dynamic alloc dynamic 0 → 10 · spread S3 warehouse (Iceberg)vended prefix-scoped creds creates One shared Connect driver serves many sessions; execution scales elastically on a separate, tainted node pool. Scaling happens behind the governed mTLS door (:15009), never around it; the two pools keep driver and executors apart. Demonstrated with a spark.range(80M).sum() probe on the cluster's executors: 1.246 exec-s · 0.878 cpu-s — a small probe, not a parallelism benchmark.
Connect on Kubernetes. A client reaches the cluster only through one external mTLS port (15009), an Envoy sidecar in the Spark Connect driver pod that pins the caller's principal and forwards over loopback to the Connect server on 127.0.0.1:15002 (never externally reachable). The driver pod is a singleton on the untainted system node pool and is the client-mode Spark driver: through an RBAC service account it schedules executor pods on a separate, tainted executor node pool (spark-role=executor), scaling 0->10 by dynamic allocation. Executors read the S3 Iceberg warehouse with the vended, prefix-scoped credentials of §3.3. Execution scales behind the governed door, never around it, and the two node pools keep driver and executors physically separated.

What actually ran: a real EKS cluster, one shared long-lived driver (a singleton, replicas:1 — Connect sessions are server-local), and a spark.range(80_000_000).sum() probe that registered real Spark stages on the cluster's executor pods, not the local process (executor-seconds 1.246, cpu-seconds 0.878). That confirms execution genuinely left the client. It's a small probe, not a parallelism benchmark. The substrate is terraform-applied and CI/OIDC-gated as of 2026-07-09, with no long-lived AWS keys; elastic 0→10→0 and node autoscaling (Karpenter) are configured, not reproduced.

3.3 Tenant governance: the multi-tenancy stack, built and demonstrated

Why co-tenant at all instead of one cluster per tenant? Consolidation — one elastic compute pool and one governed catalog beat a cluster per team on cost and ops surface. But consolidation is exactly what puts a hostile or hallucinating tenant-A agent one misconfiguration from tenant-B's data, so co-tenancy has to be safe by construction, not by trusting the agent.

The tenancy model, decided before any mechanism:

Model Shape L1/L2/L3 L4/L5 Scales?
A · server per tenant (shown below) one server + driver + executors per tenant static token; disjoint pods (physical) per-principal no
B · shared servers tenants multiplex a few servers per-request pin + vend; shared executors (logical) per-principal yes
C · hybrid (chosen) shared by default + optional dedicated tier per-request pin + vend; shared, physical optional per-principal yes

Chosen C because of a §2 property. The risk of a shared executor depends on what runs there: an imperative agent could scavenge a co-resident tenant's in-memory or shuffle data, but an SDP agent emits inert declarative transforms that the framework runs — never agent code. Shared execution is safe for declarative agents in a way it isn't for imperative ones, so physical separation drops from requirement to optional defense-in-depth. Two more properties make this production-shaped rather than a lab convenience: a Connect server pool needn't be homogeneous (it can span AWS, on-prem, Databricks, routing to the best fit), and the pressure moves off Connect onto the catalog — a metastore that owns metadata, authz, and vending, not the write path. Catalog-side scale is itself left to future work.

Proven vs frontier: the five links below run on Model A — one server per tenant, the strongest-isolation end of the spectrum. Two tenants sharing one Connect server who still can't touch each other is the hybrid's headline, and it's named as frontier rather than claimed.

The adversary, and the five paths. The threat is §2's: a tenant-A agent that is fully untrusted — it may emit code that executes on the cluster, hallucinate, or actively try to reach tenant B's data. Reaching tenant B decomposes into exactly five paths, the ledger below. Each is closed by a different layer, each layer is independently defeatable, so all five must hold. And the later layers can't backstop failure of the earlier ones: a routing or custody failure produces a legitimately issued tenant-B identity, not a forgery, which the catalog and storage gates would then correctly serve. The early links keep the identity honest; the late links bound what an honest identity may do. That's why the chain is non-redundant.

Section 3.3: the adversary and the five paths to tenant B A fully-untrusted tenant-A agent could reach tenant B's data along exactly five paths: connect to tenant B's server, be handed tenant B's credential, execute in a shared process, ask the catalog for tenant B, or hit storage with a broad credential. Each path is closed by one of the five isolation layers, so all five must hold. AN UNTRUSTED TENANT-A AGENT COULD REACH TENANT B FIVE WAYS · EACH IS CLOSED BY ONE LAYER THE PATH IT WOULD TAKE THE LAYER THAT CLOSES IT ① connect straight to tenant B's server L1 · mTLS ingress routing ② be handed tenant B's credential L2 · token custody ③ execute in a process shared with B L3 · execution isolation ④ ask the catalog for B's tables L4 · catalog authorization ⑤ reach B's bytes with a broad cred L5 · storage scoping Each layer is independently defeatable, so all five must hold, and none is redundant.
The adversary, and the five paths to tenant B. Each attack route is closed by exactly one of the five isolation layers, so all five must hold.
Path Layer Enforced by Blast radius if this layer alone fails
connect L1 ingress routing route by the certificate's authenticated principal a tenant-A cert reaches tenant-B's server
be handed L2 token custody inject only the tenant's own token, never client-held a session presents tenant-B's credential
execute L3 execution isolation own pods, no shared JVM a co-resident session reads tenant-B's in-process data
ask catalog L4 catalog authz authorize every request, including the vend an agent is handed tenant-B's tables or credential
hit storage L5 storage scoping downscope the vended credential to the tenant's prefix a credential reaches tenant-B's bytes

The default open stack stops at identity. Envoy pins an unspoofable principal from the client-cert SAN, and the interceptor rejects a mismatched user_id. The platform knows who each session is. But authorization stays fleet-scoped: one shared Iceberg catalog and one fleet-wide IAM role (IRSA) with read/write to the whole warehouse. Per-principal schema isolation exists by convention, not enforcement, and the stock open-source catalog can't express per-user grants at all — audit and fleet-wide grants only, no per-user grants, no row/column masking. Execution is shared too: one long-lived driver, shared executors, no per-tenant pool. That's the baseline the five links close.

Section 3.3: the five-layer per-tenant isolation chain An agent authenticated as tenant A is routed to its own Connect server, handed only its own credential, run on its own executor pods, authorized at the catalog only for itself, and prefix-scoped at storage. Tenant B travels a fully separate lane. Five layers, all demonstrated on live EKS; an agent for tenant A cannot reach tenant B by any path. FIVE LAYERS, ALL DEMONSTRATED ON LIVE EKS tenant A cannot reach tenant B by any path agent · tenant Acert SAN spiffe://…/tenant_a agent · tenant Bcert SAN spiffe://…/tenant_b 1 · INGRESS ROUTING mTLS gateway routes by cert principal to that tenant's own servertenant_a cert → server A · tenant_b cert → server B · un-granted → 403 · no cert → refused 2 · TOKEN CUSTODY · each server injects only its own tenant's token spark-connect-tenant-ainjects token A spark-connect-tenant-binjects token B 3 · EXECUTION ISOLATION · disjoint pods, never a shared JVM tenant_a executor podsown driver app tenant_b executor podsseparate driver app 4 · CATALOG AUTHORIZATION Lakekeeper + OpenFGA · grants each identity only its own warehousetenant_a asking for tenant_b's table or vend → 404 · its own → 200 5 · STORAGE SCOPING S3 · prefix-scopeds3://…/tenant_a/s3://…/tenant_b/ cross-tenant AccessDenied, both directions
The five-layer per-tenant isolation chain, all demonstrated on live EKS, link by link. An agent authenticated as tenant A is routed to its own server, handed only its own credential, run on its own executor pods, authorized only for itself, and prefix-scoped at storage; tenant B travels a fully separate lane. (Proven per link; one request composing all five remains a seam, see §3.3.)

The five links, outside in. Each is demonstrated on live EKS (2026-07-10 unless noted), each names the residual threat it alone closes and the attack that still works with that layer removed but the other four intact. The interactive figure paper/figures/isolation-architecture.html renders the whole path.

  1. The gateway routes by who you are, not what you ask for. Envoy terminates the client's mTLS, derives the principal from the certificate's URI-SAN (spiffe://safe-spark-agents/<tenant>), and routes on that. A tenant-A cert reaches only spark-connect-tenant-a; tenant-B only its own server; an un-granted principal gets 403; no cert never clears TLS. The authenticated identity, not the client's choice, selects the server, so no route can send a tenant-A cert to tenant-B's server. (paper/notes/proof_2026-07-10_ingress_routing.log)
  2. A tenant only ever holds its own token, and never sees it. Each tenant's Connect server injects its own catalog token in config, never exposed to the client. A session on tenant-A's server operates only as tenant-A; configuring tenant-B, it holds no credential and is refused (NotAuthorized: Missing Authorization Header). No token to redirect or replay. (paper/notes/proof_2026-07-10_multiserver.log) Full custody — holding and rotating the vended credential — is §4/Omnigent's job; §3 shows the per-tenant server binding.
  3. Each tenant's work runs on its own pods. Connect sessions are server-local, so tenant-A runs on its own driver's executor pods (2 pods, distinct IPs), tenant-B on another driver app and pods. Never a shared JVM: no in-memory or shuffle reads across tenants, no scavenged process credential. (paper/notes/proof_2026-07-10_multiserver.log)
  4. The catalog authorizes every request per principal (Lakekeeper + OpenFGA). A tenant-A identity is denied at the catalog for tenant-B across warehouse resolution and namespace ops (404, existence hidden for a zero-relation principal); tenant-B and admin get 200, unauthenticated gets 401. Toggling a describe grant flips 404↔200, proving it's authorization, not nonexistence. The vend path is probed directly, not inferred: tenant-B's own loadTable carrying the vended-credentials delegation returns 200 with credentials, while tenant-A gets 404 and nothing is vended. (paper/notes/proof_2026-07-10_perprincipal_authz.log, sections A–D)
  5. Even a leaked credential reaches only its own prefix. The catalog vends keylessly — IRSA assumes a downscoping role, external-id-pinned — per tenant prefix. Replaying the vended credential against the other tenant's prefix is AccessDenied in both directions; an ablation confirms a whole-bucket credential would cross, so the deny is the downscoping vend, not the base policy. CloudTrail settles that compute uses the vend and nothing else: every warehouse object call rides the vended session, and the fleet IRSA role makes zero data calls. (paper/notes/cloudtrail_vend_evidence.md, paper/notes/proof_2026-07-10_delta_and_frontier.log)

The vend is configured, not narrated — each tenant's storage profile names its own prefix, sts-enabled, and the external-id-pinned vending role (deploy/eks/terraform/lakekeeper-vending.tf):

// warehouse-tenant_a.aws.json  --  tenant_a's storage profile (Lakekeeper)
{
  "warehouse-name": "tenant_a",
  "storage-credential": { "type": "s3", "credential-type": "aws-system-identity",
                          "external-id": "<external-id>" },
  "storage-profile": {
    "type": "s3", "bucket": "<warehouse-bucket>", "key-prefix": "tenant_a",
    "sts-enabled": true,
    "assume-role-arn": "arn:aws:iam::<ACCT>:role/…-lakekeeper-vending"
  }
}

Implementation notes for link 1: over gRPC the no-route deny surfaces as an HTTP-200 carrying x-routed-to=DENIED and grpc-message: no tenant route (plain HTTP gets a real 403). Two server-side checks bar dialing in around the gateway: the PSK bearer the gateway injects (an infrastructure secret, not per-tenant) and the principal-pinning interceptor (rejects a user_id that doesn't match the gateway-derived x-connect-principal). The per-tenant servers are ClusterIP-only but bind 0.0.0.0:15002, so those two secrets — not topology — are today's door; a NetworkPolicy is shipped as defense-in-depth but not yet applied.

Proven link by link, not one composed request. Links 2–5 already compose: a write through tenant-A's server draws a tenant-scoped vend and runs on tenant-A's executors. The per-link probes are narrower than they look. The storage probe (a 12-task per-write delta) shows only that execution left the driver onto a dedicated pod; per-tenant pod disjointness is the separate multi-server result (link 3, distinct IPs). And the cross-tenant deny is shown by replaying the vended credential against the other prefix, not by an executor refused in-cluster (CloudTrail: every warehouse call rides the vend; fleet IRSA, zero data calls). Two seams stay named rather than hidden: the storage-scoping forensic predates the authorization-enabled catalog, and one request traversing all five links hasn't been captured as one job. Full narrative: paper/notes/PLATFORM_LAB_NOTEBOOK.md.

Credential custody: the credential's path never touches the agent The agent is untrusted and holds no credential; it only emits an inert spec and receives a pass/fail. On the governed side the catalog vends a short-lived scoped credential, the custodian (Section 4 / Omnigent) holds and injects it per session, and the Connect server and executors use it to reach only this tenant's data. The credential never crosses the trust boundary to the agent. UNTRUSTED GOVERNED · THE CREDENTIAL LIVES HERE AND NEVER CROSSES LEFT §2 trust boundary AI agent (untrusted)holds no credential Its only exchange with the platform: emits an inert spec + PR → ← receives a pass / fail Governed catalog (Lakekeeper + OpenFGA)authorizes, then vends a short-lived scoped credential Custodian · §4 / Omnigentholds + rotates the vended credential, injects it per session Connect server → executorsuse it to reach only this tenant's data S3 · this tenant's prefix only credential
Credential custody, the §3↔§4 line. The credential is vended by the catalog, held and injected by the §4 custodian, and used by the Connect server and executors; it never crosses the §2 trust boundary to the untrusted agent, which only emits an inert spec and receives a pass/fail.

Catalog binding: Lakekeeper and Unity Catalog OSS

The governed catalog is a swappable slot, not a Lakekeeper dependency. (Disclosure: a co-author works on open-source Unity Catalog at Databricks; this evaluation names UC OSS's gaps as plainly as its strengths, and every UC claim is verified against v0.5.0 source or reproduced live.)

L1 ingress routing, L2 token custody, and L3 execution isolation are catalog-independent — they live at the ingress, the per-tenant servers, and the executor pods, and carry a Unity Catalog token exactly as they carry a Lakekeeper one. L4 per-principal authorization and L5 prefix-scoped vending are the catalog's job, and both bindings enforce them.

Where Unity Catalog OSS is genuinely weaker (and it is), stamped to v0.5.0:

Gap Kind Detail
Iceberg-REST path can't do per-principal L4 hard every Iceberg-REST route authorizes at metastore-OWNER, read-only/UniForm-only; per-principal only via the native plugin
Authorization off by default posture ships server.authorization=disable; isolation is opt-in, not default
No RLS / column masking / ABAC hard coarse object-level RBAC only
RBAC, not relationship-based design JCasbin ACL/RBAC vs OpenFGA (Zanzibar); single, non-thread-safe on concurrent policy reload
Identity mapping setup maps OIDC email to a pre-provisioned user (no default JIT); per-user grant work per tenant
Authz maturity maturity standing up per-principal v0.5.0 needed two authz bug fixes
Operational weight ops server + Postgres + OIDC token-exchange + JCasbin vs one Rust binary

Takeaway. Per-principal authorization and prefix-scoped vending — the two things §3's isolation rests on — are achievable on both bindings. The full isolation proof demonstrates on Lakekeeper; the catalog-integrated path (token custody, per-principal authz, downscoped vending, executor data-read isolation) reproduces end to end through Spark on UC OSS, with UC's weaker spots named above so the choice is informed.

3.4 Where each pillar stands

Pillar Running on live EKS Configured but not yet run Still ahead
GitOps/CI PR-author session denial; dry-run + reconcile workflows EKS-target reconcile (none)
Connect-on-k8s topology; small-scale distributed exec elastic 0→10 executors node autoscaler
Tenant governance per-principal mTLS ingress routing (Envoy routes by cert SAN); token custody + execution isolation (two per-tenant servers, server-injected tokens, disjoint pods); per-principal catalog authz (Lakekeeper+OpenFGA+OIDC, both directions, grants toggle); per-tenant storage isolation (vended creds, cross-tenant AccessDenied, fleet IRSA 0 data calls) (none) multi-tenant scale (many tenants + node autoscaling)

Frontier. Three things: multi-tenant scale — many tenants and node autoscaling, the one unbuilt capability; a single request composing all five links end to end; and unifying the storage-scoping forensic onto the authorization-enabled catalog. Two design-only seams sit under GitOps: the gate targets runner-local Connect (§3.1), and the controller's identity isn't wired to the per-tenant runtime certificate (§3.3). Everything else in this section runs on the live cluster.

Reproduce it: deploy/eks/lakekeeper/SETUP.md, four sub-deployments built inside-out, each writing the proof log cited above.

Section 3: how to reproduce the five-layer isolation stack from SETUP.md Once the prerequisites are in place (EKS substrate, IRSA vending roles, per-tenant mTLS certs, the spark-connect image, warehouse external-id), four sub-deployments run inside-out, storage first and ingress last: spike/eks stands up layer 5 (storage proof 13/13 + cloudtrail_vend); authz/ stands up layer 4 (catalog proof); authz/ multi-server stands up layers 2+3 (custody proof); authz/ gateway stands up layer 1 (ingress proof). The five proof logs are the per-layer evidence; one composed request through all five links remains the frontier. REPRODUCE THE FIVE-LAYER STACK · SETUP.md BUILD ORDER built inside-out: storage first, ingress last (the reverse of the outside-in reading order) PREREQUISITES (ONCE) EKS substrate (terraform) · IRSA vending roles · external-id · mTLS certs · connect image WHAT YOU RUN LAYER IT STANDS UP PROOF IT WRITES spike/eksapply -k · run_spike.py Layer 5storage scoping storage proof13/13 + cloudtrail_vend authz/ (catalog)IdP + OpenFGA + grants Layer 4catalog authorization catalog proofperprincipal_authz.log authz/ (multi-server)gen_connect_server.py Layers 2 + 3custody + execution custody proofmultiserver.log authz/ (gateway)Envoy gateway-envoy.yaml Layer 1mTLS ingress routing ingress proofingress_routing.log The five proof logs are the per-layer evidence cited in §3.3. Frontier: one composed request through all five links, not yet captured.
How to reproduce the five-layer stack, from SETUP.md. Once the prerequisites are in place, four sub-deployments run inside-out (storage first, ingress last): each column maps what you run to the layer it stands up to the proof log it writes. The five logs are the per-layer evidence cited in §3.3; one composed request through all five links remains the frontier.

3.5 Where §3 hands off

§3 governs one agent — routing, custody, execution, authorization, storage — per tenant, live on EKS. That governed substrate is the precondition for orchestrating a whole fleet of agents, which is §4.


SECTION 4, Omnigent: Governed Multi-Agent Orchestration for Data Engineering Proposed

The thesis. §3 governs one agent safely. Omnigent — an orchestration layer enacted at runtime by one orchestrator driving credential-free sub-agent workers — governs a fleet. A fleet isn't "more agents in parallel"; an orchestration layer makes many data-engineering agents cheaper, higher quality, governed, and collectively knowledgeable in ways N independent sessions can't be by construction, having no shared coordination layer. The load-bearing one is governance: one custodian holds every per-tenant credential from §3 and enforces each tenant's contextual data policy at submit time, so the fleet stays credential-free and policy-bound, §2's boundary preserved at fleet scale. The keystone and the orchestration pattern run in the capstone below; the quantitative cost/quality numbers are the separate pre-registered study (S4.6).

Section 4: Omnigent governed multi-agent orchestration The Omnigent orchestrator provides four capabilities over a heterogeneous, credential-free agent fleet feeding the Section 3 governed platform: model routing for cost, cross-vendor review for quality, credential custody for governance (the demonstrated keystone), and a shared skill library for knowledge. All four mechanisms run in the capstone; only the quantitative cost and quality numbers are a separate, pre-registered study. OMNIGENT · ONE ORCHESTRATOR, A CREDENTIAL-FREE FLEET demonstrated mechanism numbers separate Omnigent orchestrator · one custodian atop §3 (not N raw sessions) Model routingcost · cheap vs strong model Cross-vendor reviewquality · catches blind spots Credential custodygovernance · the demonstrated keystone Shared skill libraryknowledge · fleet-wide floor claude_codecredential-free codexcredential-free picredential-free a heterogeneous, credential-free agent fleet §3 governed platform · the substrate Omnigent sits on GitOps gatePR dry-run vs catalog Connect ingresssingle mTLS door k8s executiondriver + executor pods
Omnigent: one custodian over a credential-free heterogeneous fleet. Credential custody is the keystone, now demonstrated (S4.3).
Section 4 (S4.3): the Omnigent credential custodian at fleet scale A fleet of untrusted, credential-free agents submits inert specs to one Omnigent custodian and receives only pass or fail. The custodian holds and rotates every per-tenant credential, minting a fresh short-lived token per job and running the work over the Section 3 governed catalog and per-tenant storage. Credentials never cross the trust boundary back to the agents, and Section 3's per-tenant isolation is preserved under custody. UNTRUSTED FLEET · NO CREDENTIAL OMNIGENT CUSTODIAN · HOLDS + ROTATES EVERY CREDENTIAL §2 boundary agent A1 · tenant Aemits spec + PR agent B1 · tenant Bemits spec + PR agent A2 · tenant Aemits spec + PR each receives only a pass / fail Custodianmints a fresh short-livedtoken per job (rotation)vault, never handed back:credA#3 · credB#2 · credA#1 …held only here spec → ← pass/fail mint + vend credential §3 catalogper-principal authz per-tenantstorage (scoped) One custodian, N credential-free agents. Demonstrated: 1 custodian governed 2 tenants, minted + rotated 3 short-lived credentials; agents held 0. An agent's cross-tenant read was refused (PERMISSION_DENIED), so §3's isolation holds under custody. Frontier: scale-out (many tenants + node autoscaling) and rotation under live long-running jobs.
The credential custodian at fleet scale (S4.3), demonstrated. A fleet of credential-free agents submits inert specs and receives only pass or fail; one custodian holds and rotates every per-tenant credential, minting a fresh short-lived token per job and running the work over the §3 catalog. Credentials never cross back to the agents, and §3's per-tenant isolation holds under custody (a cross-tenant read is refused).

S4.5 The core, on the live platform

Two things ran on the live §3 platform, both working mechanism rather than measured number. Custody: one custodian holds and rotates every per-tenant credential while a credential-free fleet submits specs and gets pass/fail. Orchestration: model routing, cross-vendor review, and skill injection driven natively by a single Omnigent agent.

The capstone ran in two stages: a deterministic wrapper first (the orchestrator decomposes, a script drives routing, custody, review, repair), then the same build native and autonomous (one agent, one custodian tool, the whole loop). From one brief, Omnigent's orchestrator (Polly) built end-to-end medallions (bronze to silver to gold) for three isolated tenants:

This demonstrates that the mechanism runs, not a numbers claim. Runnable at deploy/omnigent/sdp-capstone/ (paper/notes/proof_2026-07-12_sp4_capstone.log). The paper's own production used the same shape: one orchestrator fanned claude_code, codex, and pi sub-agents, adversarial verifiers tried to refute each finding, and only the survivors got through.

Section 4 (S4.5): Polly builds a governed medallion fleet for three customers over live per-tenant isolation Polly decomposes one data-engineering brief into per-customer bronze/silver/gold subtasks and fans out a cross-vendor fleet of credential-free sub-agents. Each is routed to a model by difficulty, cross-reviewed by a different vendor, and submitted to one custodian that holds every credential. The custodian runs each medallion over that customer's own Section 3 tenant, enforces contextual policy, and returns pass/fail; on failure the fleet repairs and converges. Cross-tenant reads are denied. FLEET · credential-free · Omnigent-routed CUSTODIAN LIVE §3 PER-TENANT ISOLATION (EKS) Polly (Omnigent) decomposes1 brief → 3-customer medallion DAG dispatches sub-agents §2 boundary retail agentqwen-coder:30b (local) → opus repairLow saas agentopus → opus repairHigh payments agentdeepseek → opus repairMed cross-vendor review · gpt-5caught silent defects the authors missed Custodianholds PSK +every tenant tokensubmit(customer, code)→ pass / failagents get onlypass/fail + policy tenant_a · retailown Connect · authz · vend · prefix standard ✓ tenant_b · saasown Connect · authz · vend · prefix PII-mask ✓ tenant_c · paymentsown Connect · authz · vend · prefix financial ✓ cross-tenant reads DENIED · isolation holds Polly decomposed one brief; a cross-vendor fleet built three medallions over live isolation. S4.1 routed vendors by difficulty (qwen / deepseek / opus) · S4.2 gpt-5 review caught silent defects. S4.3 the custodian held every credential, the 3 agents held 0; routed cost $0.35. §3 isolation preserved: each medallion materialized in its own tenant; every cross-tenant probe was denied. Demonstrates the mechanism (S4.5 core), not a powered numbers claim (SP4.2 is separate). Stage 1 = wrapper (Polly decomposes) · Stage 2 = native in Omnigent (custodian as a first-class tool).
The demonstrated core on the platform's own domain (S4.5). Polly decomposes one brief into per-customer medallions, routes authoring across three vendors by difficulty, has a different vendor review, and submits each through the custodian, which runs it over that customer's own live tenant and enforces its contextual data policy. On failure the fleet repairs and converges (the §2 dev loop at fleet scale); every cross-tenant read is denied, so §3 isolation holds. The numbers are S4.6's separate study.

Contained deployment. For a client, the same layer deploys inside their own cluster: Omnigent server, custodian, and credential-free agent fleet as pods on their EKS over the §3 platform, one IdP governing both (deploy/kubernetes/).

Section 4: Omnigent contained as pods in the client's EKS, over the Section 3 platform A fully containerized Omnigent delivery layer runs as pods in the client's EKS cluster: an Omnigent server (control plane), a credential-free agent fleet (claude_code, codex, pi), and a custodian that holds the Connect PSK and per-tenant catalog tokens. The custodian is the fleet's only path to a catalog; it submits into the Section 3 governed platform (ingress gateway, per-tenant Connect servers, Lakekeeper authz and vend, S3). One IdP over OIDC governs both planes. OMNIGENT CONTAINED · THE DELIVERY LAYER AS PODS IN THE CLIENT'S EKS, OVER §3 Client IdP (OIDC) OIDC authn CLIENT EKS CLUSTER · ALL CONTAINERIZED §4 OMNIGENT DELIVERY · ns: omnigent Omnigent servercontrol planehosts/sandbox runners credential-free fleetclaude_codeAnthropiccodexOpenAIpigateway CUSTODIANholds PSK + tokensonly path to catalog submit(customer, code) only ingress into §3 §3 GOVERNED PLATFORM · ns: spark · per-tenant isolation Ingress gatewayEnvoy mTLSroutes by principal Per-tenant Connect serverstenant-a / -b / -c · token-injecteddisjoint executor pods LakekeeperOpenFGA authzprefix-scoped vend S3 warehouseIceberg tablesper-tenant prefixes OIDC: the same issuer authorizes here (§3) Contained: server + custodian + credential-free fleet as pods in the client's EKS. No laptop, no shim. The custodian alone holds the PSK and tokens; claude_code / codex / pi hold zero credentials. One IdP (OIDC) governs both planes; §2 + §3 unchanged. Deploy: kustomize + overlays.
The contained deployment shape (architecture): the Omnigent server, the custodian, and the credential-free agent fleet as pods in the client's EKS, over the Section 3 platform, with one IdP governing both. S4.5's capstone demonstrates the mechanism; this is how it is packaged for a client via the official Omnigent Kubernetes path.

Frontier. Scale-out (many tenants, node autoscaling), credential rotation under live long-running jobs, and a learned fleet memory.

S4.6 The fleet numbers (separate study, SP4.2)

S4.1 and S4.2's numbers are a separate pre-registered experiment, its own paper, not part of this run and not retrofitted. Design, for completeness: routed vs single-model fleets on dollars-per-correct-pipeline; cross-vendor vs same-vendor defect catch-rate on a seeded corpus; §1's frozen, blind, provenance-tracked instrument at fleet scale. No claim in this paper depends on it.


Appendix S2-A, Reference Architecture: The Control Boundary Reference spec

Executable spec for implementing agents. This is the SSOT §2 is measured against; build work lives in SECTION2_eks_connect_demo_checklist.md. If a component, step, or claim can't trace to an invariant below, it's drift. Cites are file:line on origin/dev.

North star. The agent proposes inert desired state. A governed control plane, on a separate host, validates and executes. The agent never holds a live session, credentials, or data. The dev loop (propose → dry-run gate → reconcile/execute) is that boundary. Declarative makes it expressible; Spark Connect enforces it.

The invariant. The boundary holds iff all of these are true, each a checkable predicate:

A demonstration that violates any I-rule is a simulation of the boundary, not the boundary.

Trust zones. Zone U: untrusted authoring — the agent and its workspace, no creds, no session. Zone C: governed control plane — the reconciler/controller, holds creds, runs the Connect client, drives propose→gate→execute→grade; the agent can't run code here. Zone D: data plane (remote EKS) — Connect service (client-mode driver pod) + executor pods + catalog + warehouse, the only place data is touched. Boundary U│C hands the inert artifact (enforces I1); boundary C│D submits plans over authenticated mTLS/PSK Connect (enforces I2/I3/I6).

Components (role · zone · trust).

Component Zone Role Holds creds?
Agent (LLM) U proposes SDP spec + transforms as text No
Per-cell workspace U inert spark-pipeline.yml + transform code No
Reconciler / controller C runs SDP CLI client; drives gate + execute; blind-grades Yes
Dry-run gate C→D structural validation before data via controller
Connect channel C│D mTLS + bearer PSK + x-connect-principal via Envoy yes (controller-held)
Connect server (driver) D client-mode driver pod; builds/runs graph cluster identity
Executor pods D do the data work cluster (IRSA)
Catalog D Iceberg JDBC / HMS cluster
Warehouse (S3) D s3a://… via IRSA cluster
Blind oracle C grades output without seeing paradigm n/a

End-to-end flow, each arrow tagged with its invariant. Propose (agent emits spec+code, zone U) → I1. Materialize (inert artifact written, handed across U│C) → I1, I6. Dry-run gate (controller submits a structural dry-run to D; no data touched; defects returned as feedback) → I4. Reconcile/execute (controller in zone C ships DefineOutput/DefineFlow/StartRun plans over the authenticated Connect channel) → I5, I6, I2. Execute in D (driver + executor pods run against the warehouse/catalog) → I3. Telemetry back, blind grade → governance closure.

The authenticated submission path, the crux. The controller runs the stock SDP CLI as a Connect client: it imports the agent's transform Python, builds the graph, and sends protobuf plans over gRPC, so the server needs no raw files. Channel: sc://<NLB>:15009/ + bearer PSK + x-connect-principal, terminated by Envoy mTLS. The controller — not a side tunnel — must hold and present this identity (I2).

Known leaks, named not hidden.

Working backwards: dependency-ordered layers. Each layer depends on the one below; claiming a layer while a lower invariant is unproven is drift.

Layer Claim it licenses Enforces Depends on
L0 Substrate EKS Connect server + Envoy + catalog + S3 reachable — —
L1 Authenticated channel controller → Connect via native mTLS/PSK, no tunnel I2 (C│D) L0
L2 Off-host execution a trivial plan runs in D, driver + executors in pods I3 L1
L3 SDP submission green agent-authored spec from C completes + grades green in D I1, I6 L2
L4 Gate before data dry-run rejects structural defects pre-execution I4 L2
L5 Governance split reconciler in C, agent in U, agent holds no creds I5, I2, (R2) L3, L4
L6 Negative control imperative cannot traverse C│D thesis support L1

Reverse-engineering map (reference → as-built).

Ref element Target As-built Evidence / delta
Inert artifact I1, I6 IMPLEMENTED runner.py:369-405; spec + code only
Plan-not-files submission I6 IMPLEMENTED (PySpark) cli.py:221-263
L0 substrate — APPLIED via terraform + CI (2026-07-09), S3-backed state deploy/eks/terraform
L1 native mTLS/PSK I2 PARTIAL: socat tunnel, native client unproven (R3) study.config.live.json:2
L2 off-host execution I3 runs off-host; driver + executors in pods, tables materialize study/repro/h3_eks/
L3 SDP green remote I1, I6 Arm B completes + grades green remotely (2026-07-06), submitted session-less study/repro/h3_eks/
L4 gate before data I4 IMPLEMENTED locally; not re-proven remote sdp_dryrun.py:462-484
L5 governance split I5, I2, R2 GAP: reconciler co-located on agent host live.py:675-689
L6 imperative-can't-cross thesis CORROBORATED, not captured DEVIATIONS.md:516-522

Highest honestly claimable: ~L3 (both arms run the full loop remotely; arm B completes + grades green, 2026-07-06). L5 is the real remaining work; L1 needs native-mTLS de-risking; L6 needs capturing as a clean artifact. Claim only up to the highest proven layer, and state the next gap plainly.


Appendix S3-A, Reference Architecture: The Open Governed Platform Reference spec

Executable target for the multi-tenant platform §3 builds toward; build work lives in SECTION3_platform_build_checklist.md. As in S2-A: claim only up to the highest proven layer; anything above it is a build task, not a result.

G: Invariants (what the platform must satisfy).

P: Build/claim layers (bottom-up; the highest proven layer is the honest claim ceiling).

Layer Requirement Status
P0 substrate EKS cluster applied via terraform, CI/OIDC-gated APPLIED (2026-07-09)
P1 governed ingress single mTLS door; no raw Connect exposure runs today
P2 authoring boundary agent-as-PR-author + CI dry-run gate + reconcile runs locally today
P4 integration testing structural dry-run against the real catalog runs today; data-quality tests next
P5 tenant isolation credential scoping + per-principal authz + per-tenant execution runs today, link by link
P6 multi-tenant scale multiple Connect servers + node autoscaling per-tenant servers + routing run today (2 proof tenants; a third for the §4 capstone); N-tenant scale + autoscaling next

P5 detail, since it's the load: storage scoping (Lakekeeper vends, replay cross-tenant AccessDenied both directions, 12-task per-write delta on a dedicated executor pod, CloudTrail: FileIO via the vend, fleet IRSA 0 data calls, ablation confirms the vend is load-bearing); per-principal catalog authz (tenant-A denied for tenant-B across warehouse resolution, namespace, and the direct vend path — loadTable delegation: tenant-B 200+creds, tenant-A 404 — grants toggle, proof_2026-07-10_perprincipal_authz.log); token custody + execution isolation (two per-tenant servers, server-injected tokens, tenant-A session refused on tenant-B, disjoint executor pods, proof_2026-07-10_multiserver.log); ingress routing by identity (Envoy by cert URI-SAN, un-granted 403, no-cert refused, proof_2026-07-10_ingress_routing.log).

Highest honestly claimable ≈ the full per-tenant isolation path, link by link on EKS (2026-07-09/10). Tenant A is routed to tenant-A's server, handed tenant-A's token, run on tenant-A's executors, authorized at the catalog only for tenant-A, prefix-scoped at storage. What remains: multi-tenant scale (many tenants + node autoscaling), a single request composing all five links, and unifying the storage-scoping forensic onto the authorization-enabled catalog.

R: Reverse-engineering map (reference → as-built → SALVAGE / GAP).

Component Target As-built SALVAGE (keep) GAP (build)
GitOps loop agent→PR→CI gate→reconcile to prod runs locally PR authoring, dry-run + reconcile workflows, unit tests wire to EKS Connect; capture a real agent PR
Connect ingress one governed mTLS endpoint + per-principal routing built; Envoy routes by cert-SAN to per-tenant servers Envoy mTLS, principal interceptor, gateway, deployment, image capture deploy artifacts
Elastic execution driver + dyn executors, autoscaling runs at small scale dyn-alloc config, pod template, node group, image prove 0→10→0; add Karpenter/autoscaler
Catalog authz per-tenant grants runs via Lakekeeper + OpenFGA + OIDC governed Iceberg-REST catalog, per-tenant vending, OIDC identities OSS HMS/Iceberg still can't enforce, hence the governed catalog
Tenant exec isolation per-tenant Connect / pools runs today: per-tenant servers, gateway routing, disjoint pods per-tenant servers, routing gateway, token injection scale to N tenants + node autoscaling
Integration testing structural + data-quality gate structural only the CI dry-run gate data-quality / expectation tests in CI
Substrate (IaC) reproducible cluster terraform-applied + CI/OIDC-gated, live (2026-07-09) terraform stack, k8s manifests, HMS, image keep capturing run evidence per deploy

Read R top-to-bottom to build: P0→P4 is mostly salvage + wiring; P5/P6 is genuine new construction (a governed catalog + multi-server Connect).

↑ top