# Lab Spec — DD-11: OpenAI Agents SDK (2-Layer Harness/Compute Split)

**Course**: Master Course · **Deep-Dive**: DD-11 · **Duration**: 90 minutes · **Difficulty**: Senior
**Prerequisites**: Modules 0–12, DD-01–10, especially Module 5 (Sandboxing) and DD-09 (NemoClaw)

> *Simulate the OpenAI Agents SDK's defining architecture in pure Python: a harness layer (loop, credentials) and a compute layer (sandbox with swappable providers). Confirm credentials cannot cross the boundary, swap providers by configuration, and demonstrate both subagent patterns (handoffs + agents-as-tools).*

---

## Objective

Build a minimal simulation of the OpenAI Agents SDK's 2-layer harness/compute split. You will implement a `Harness` class (holds credentials, drives the loop, routes subagent delegation) and a `Sandbox` class (executes tools via a swappable provider). You will prove three architectural properties: (1) credentials never enter the sandbox, (2) providers swap by configuration, and (3) both Module 1.3 subagent patterns work.

This lab does not call any real model API. The "agent" is a deterministic loop that decides actions from a scripted task. The point is the architecture, not the intelligence.

---

## Setup

**Language**: Python 3.10+
**Dependencies**: none (standard library only)
**File**: `lab_dd11.py` (single file, run with `python3 lab_dd11.py`)

```python
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Callable, Optional
import json
```

---

## Phase 1 — The 2-Layer Split and Credential Isolation

Implement the core classes. The `Harness` holds credentials; the `Sandbox` executes tools via a provider. The boundary between them is the load-bearing architectural decision.

```python
class Provider(str, Enum):
    LOCAL_BASH = "local_bash"
    DOCKER = "docker"
    E2B = "e2b"
    MODAL = "modal"
    DAYTONA = "daytona"
    CLOUDFLARE = "cloudflare"
    VERCEL = "vercel"

@dataclass
class Harness:
    """The harness layer. Holds credentials. Drives the loop. NEVER exposes credentials to the sandbox."""
    model_api_key: str
    db_credentials: dict[str, str]
    sandbox: "Sandbox"
    trace: list[str] = field(default_factory=list)

    def run_task(self, task: str) -> str:
        # The loop runs HERE, in the harness layer.
        self.trace.append(f"[harness] received task: {task}")
        self.trace.append(f"[harness] credentials held: model_api_key={'*' * 8}, db_creds={ {k: '*' * 4 for k in self.db_credentials} }")
        # Dispatch tool execution to the sandbox. NO credentials cross.
        result = self.sandbox.execute(tool="read_file", args={"path": "auth.ts"})
        self.trace.append(f"[harness] sandbox returned: {result}")
        return result

@dataclass
class Sandbox:
    """The compute layer. Executes tools. Has NO access to credentials."""
    provider: Provider

    def execute(self, tool: str, args: dict) -> str:
        # Tool execution happens HERE. Credentials are NOT in this scope.
        return f"[compute:{self.provider.value}] executed {tool}({json.dumps(args)})"
```

**Deliverable 1**: Instantiate a `Harness` with a fake `model_api_key="sk-1234"` and `db_credentials={"user": "admin", "password": "secret"}`. Run a task. Confirm the trace shows credentials held in the harness but the sandbox output contains no credential values.

**Property to verify**: Write an assertion that searches the sandbox's execution output (and the `execute` method's scope) for any substring of the credentials. The assertion must pass — credentials never crossed the boundary.

---

## Phase 2 — The 7-Provider Abstraction (Swap by Configuration)

Demonstrate that the same task runs in different isolation regimes by changing only the `provider` config. The harness code does not change.

```python
def run_with_provider(provider: Provider, task: str) -> str:
    """The harness code is identical; only the provider config differs."""
    sandbox = Sandbox(provider=provider)
    harness = Harness(
        model_api_key="sk-NEVER-IN-SANDBOX",
        db_credentials={"user": "admin", "password": "secret"},
        sandbox=sandbox,
    )
    return harness.run_task(task)
```

**Deliverable 2**: Loop over all 7 providers. For each, run the same task (`"read auth.ts and refactor"`). Print the sandbox output line for each. Confirm:
- The output line differs only in the provider name (`[compute:local_bash] ...`, `[compute:docker] ...`, etc.).
- The harness code (`run_with_provider`) is identical across iterations — only the `provider` argument changes.
- No credentials appear in any of the 7 outputs.

**Property to verify**: Write an assertion that the 7 outputs are identical except for the provider name substring. This proves the harness code is provider-agnostic.

---

## Phase 3 — Agents-as-Tools (Delegation by Query)

Implement the first Module 1.3 pattern. The parent agent calls a subagent like a tool, gets a structured result, and retains control.

```python
@dataclass
class Agent:
    name: str
    instructions: str
    tools: list[str]
    handoff_targets: list[str] = field(default_factory=list)

    def invoke_as_tool(self, query: str) -> dict:
        """Called by a parent agent like a tool. Returns a structured result. Parent retains control."""
        return {"agent": self.name, "query": query, "result": f"answer from {self.name}"}

@dataclass
class Orchestrator:
    """A parent agent that retains control and calls subagents as tools."""
    subagents: dict[str, Agent]

    def run(self, task: str) -> str:
        # Parent retains control throughout.
        log = [f"[parent] received task: {task}"]
        # Call the DB agent as a tool — synchronous, structured result.
        db_result = self.subagents["db_agent"].invoke_as_tool("what is the auth schema?")
        log.append(f"[parent] db_agent returned: {db_result['result']}")
        # Parent CONTINUES orchestrating — it did not transfer control.
        log.append(f"[parent] continuing orchestration with db_result")
        log.append(f"[parent] task complete — parent retained control throughout")
        return "\n".join(log)
```

**Deliverable 3**: Instantiate a parent `Orchestrator` with a `db_agent` subagent. Run a task. Confirm the log shows the parent calling the DB agent, receiving a result, and continuing — the parent retained control.

**Property to verify**: The log contains `[parent] continuing orchestration` AFTER the subagent call. This proves delegation by query — the parent did not stop.

---

## Phase 4 — Handoffs (Delegation by Transfer)

Implement the second Module 1.3 pattern. The parent agent transfers control to a subagent. The transfer is terminal — the parent stops.

```python
@dataclass
class HandoffOrchestrator:
    """A parent agent that TRANSFERS control to a subagent. The handoff is terminal."""
    subagents: dict[str, Agent]

    def run(self, task: str) -> str:
        log = [f"[parent] received task: {task}"]
        log.append(f"[parent] this is a DB task — handing off to db_agent (TERMINAL)"])
        # Transfer control. The parent STOPS here.
        sub = self.subagents["db_agent"]
        log.append(f"[{sub.name}] took ownership of task: {task}")
        log.append(f"[{sub.name}] executing task to completion...")
        log.append(f"[{sub.name}] task complete")
        # NOTE: the parent does NOT continue. The handoff was terminal.
        return "\n".join(log)
```

**Deliverable 4**: Instantiate a `HandoffOrchestrator` with a `db_agent` subagent. Run the same task as Phase 3. Confirm the log shows the parent handing off and STOPPING — the subagent owns the task to completion.

**Property to verify**: The log does NOT contain `[parent] continuing orchestration`. Compare the Phase 3 and Phase 4 logs side by side. The difference IS the Module 1.3 distinction: agents-as-tools retains control; handoffs transfer it.

---

## Phase 5 — The NemoClaw Connection (Same Principle, Different Layer)

Write a function that prints the architectural parallel between the SDK's credential boundary and NemoClaw's governance gate. Both place enforcement outside the principal's reach.

```python
def demonstrate_nemoclaw_parallel():
    """Show that the SDK (credentials outside sandbox's reach) and NemoClaw
    (governance gate outside agent's reach) apply the same principle to different layers."""
    print("=== Architectural Parallel: Same Principle, Different Layer ===\n")
    print("NEMOCLAW (DD-09):")
    print("  Principal: the agent")
    print("  Enforcement: governance gate")
    print("  Location: OUTSIDE the agent's address space (in the harness layer)")
    print("  Property: agent cannot bypass the gate — it is structurally unreachable\n")
    print("OPENAI AGENTS SDK (DD-11):")
    print("  Principal: the sandbox")
    print("  Enforcement: credential boundary")
    print("  Location: OUTSIDE the sandbox's address space (in the harness layer)")
    print("  Property: sandbox cannot reach credentials — they are structurally inaccessible\n")
    print("SHARED PRINCIPLE: enforcement outside the principal's reach.")
    print("Both convert a discipline you maintain into a structural property you inherit.")
```

**Deliverable 5**: Call this function. Confirm the output clearly states the shared principle. Add a comment in your own words explaining why "structural" is stronger than "policy-based" for trust boundaries.

---

## Deliverables Checklist

- [ ] `Harness` and `Sandbox` classes implemented with the 2-layer split
- [ ] Phase 1: credential isolation property verified (credentials never appear in sandbox output)
- [ ] Phase 2: all 7 providers run the same task; harness code identical across providers
- [ ] Phase 3: agents-as-tools log shows parent retaining control after subagent call
- [ ] Phase 4: handoffs log shows parent stopping after transfer
- [ ] Phase 5: NemoClaw parallel function prints the shared principle
- [ ] `lab_dd11.py` runs end-to-end with `python3 lab_dd11.py` (no errors, all assertions pass)

---

## Solution Key (Summary)

The solution implements six properties:

1. **Credential isolation is structural**: The `Sandbox.execute` method's scope contains no reference to credentials. They are fields on the `Harness`, not passed to `execute`. An assertion searches the trace and confirms no credential substring appears in sandbox output.
2. **Provider swap is config-only**: `run_with_provider` takes a `Provider` enum and constructs an identical `Harness`/`Sandbox` pair. The 7 outputs differ only in the provider name substring.
3. **Agents-as-tools retains control**: The `Orchestrator.run` log contains `[parent] continuing orchestration` AFTER the `invoke_as_tool` call. The parent never stopped.
4. **Handoffs transfer control**: The `HandoffOrchestrator.run` log contains `[db_agent] took ownership` and does NOT contain `[parent] continuing`. The parent stopped.
5. **Both patterns are first-class**: The same `Agent` dataclass participates in both patterns. The SDK formalizes both rather than hand-rolling one.
6. **The NemoClaw parallel**: Both the SDK credential boundary and NemoClaw governance gate place enforcement outside the principal's reach. The `demonstrate_nemoclaw_parallel` function prints this clearly.

---

## Stretch Goals

1. **Add a trace layer (Module 11)**: Modify `Harness.run_task` to emit structured trace events (JSON lines) for each loop step, handoff, and sandbox dispatch. This is the bring-your-own observability the SDK does not provide. Confirm you can reconstruct the full execution path from the trace alone.
2. **Add a governance gate (the NemoClaw fix)**: Modify the `Sandbox.execute` to route through a governance gate that checks the tool name against an allowlist. Demonstrate that a disallowed tool is rejected before execution. This is NemoClaw's principle applied to the SDK's compute layer — proof the two patterns compose.
3. **Multi-tenant config**: Extend `run_with_provider` to accept a tenant ID and look up the provider per-tenant from a config dict. Demonstrate two tenants running the same task with different isolation regimes simultaneously.
4. **Verify the 2-layer split blocks a credential-leak attempt**: Write a malicious tool that tries to read `harness.model_api_key` from the sandbox scope. Confirm it raises an `AttributeError` — the sandbox has no reference to the harness. This is the structural impossibility made concrete.

---

## References

1. **Module 5** — outside-sandbox architecture; the 7-provider model; credential isolation as architectural property.
2. **Module 1.3** — handoffs + agents-as-tools; the two subagent patterns this lab implements.
3. **Module 10** — subagents; the SDK's handoffs + agents-as-tools as the Module 1.3 realization.
4. **DD-09 (NemoClaw)** — the same-principle-different-layer connection (Phase 5).
5. **DD-03 (OpenCode)** — the Docker-or-nothing contrast to the 7-provider abstraction (Phase 2 discussion).
6. **DD-04 (Codex CLI)** — the finished-product vs SDK contrast (the OpenAI camp).
