Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,78 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased] — Phase 2

### S10.A3 `phase2-runtime-openai-agents` — first non-OpenClaw native runtime

#### Added

- **`plan_openai_agents` producer** in
`controller::reconciler::runtime` — replaces the `AdapterMissing`
short-circuit landed in S10.A2 with a real
`RuntimeDeploymentPlan` for `RuntimeKind::OpenAIAgents`. Resolves
the adapter image via `DEFAULT_OPENAI_AGENTS_IMAGE` (default
`azureclawacr.azurecr.io/azureclaw-runtime-openai-agents:latest`)
with `OPENAI_AGENTS_RUNTIME_IMAGE` env override (whitespace
treated as unset). Propagates `python_version` →
`RUNTIME_PYTHON_VERSION` env (non-reserved prefix so it survives
the deployment builder's reserved-prefix filter), merges user
`extra_env` on top, passes `entrypoint` through as the container
command, round-trips `agent_code` for the eventual
`oci`/`git` mount path.
- **`sandbox-images/openai-agents/` scaffolding** — Dockerfile (Python
3.12 + `openai-agents>=0.1,<0.2`) + `entrypoint.sh` exporting
`OPENAI_BASE_URL=http://127.0.0.1:8443/openai/v1` (router sidecar
is the only LLM endpoint allowed by NetworkPolicy + egress-guard)
and `AZURECLAW_PLATFORM_MCP_URL=http://127.0.0.1:8443/platform/mcp`
(S10.B platform MCP server: every runtime gets the 9 Foundry shim
tools for free). Image declares
`LABEL org.azureclaw.runtime.contract="v1"` so the existing BYO
contract verifier recognises it.
- 8 new controller tests (315/315 green): default image, env-override
image (set / unset / whitespace-as-unset), `python_version` →
`RUNTIME_PYTHON_VERSION`, `extra_env` merge, user-extra-wins on
conflict, `entrypoint` → command propagation, `agent_code`
round-trip, dispatcher arm wiring.

#### Changed

- **Reconciler `is_byo` flag generalised to `is_openclaw`** (positive
polarity). `RuntimeKind::OpenAIAgents` now flows through the same
generic-runtime container shape as BYO: container name `agent`
(not `openclaw`), no OpenClaw-specific env (`OPENCLAW_MODEL`,
`OPENCLAW_GATEWAY_TOKEN`, `FOUNDRY_DEPLOYMENTS`, `FOUNDRY_AGENT_ID`,
`FOUNDRY_AGENT_TOOLS`), no admin-token mount. The OpenClaw vs
generic split established for BYO in S10.A2.b is the single
branching point; adding OpenAIAgents required no parallel flag.
- **`AdapterMissing` log message updated** — track now reads
`BYO=S10.A2.b, OpenAIAgents=S10.A3 (wired), MAF=S10.A4`.
- **`plan_returns_adapter_missing_for_each_unwired_non_openclaw_kind`**
— drops the `OpenAIAgents` case (now wired); four cases remain
(`MicrosoftAgentFramework`, `SemanticKernel`, `LangGraph`,
`Anthropic`).

#### Deferred

- **In-pod adapter Python package** (`azureclaw-runtime-openai-agents`
PyPI) — AAD shim for Azure OpenAI, `AZURE_OPENAI_ENDPOINT`
rewriting based on `InferencePolicy`, AGT-init compat, OTel SDK
wiring. The Dockerfile + entrypoint scaffolding is contract-labelled
but does not yet consume the adapter; immediate follow-up before
the slice closes.
- **Class B mesh / spawn / handoff tools** — blocked on
AgentMesh-Python upstream availability
(`docs/internal/agt-upstream-asks.md` §3). S10.A3 ships
Foundry-shim access only via S10.B; mesh tools deliberately absent
rather than reimplemented.
- **Reference example app + e2e Kind test + negative-egress
assertion** — fold into S10.A4 (MAF) where ≥2 native runtimes
share the e2e harness investment.

#### Audit doc

- `docs/security-audits/2026-04-28-phase2-runtime-openai-agents.md` —
scope, threat model, hard-rule checklist, AGT upstream dependency
note, two sign-off slots.

### S10.B `phase2-platform-mcp-server` — runtime-agnostic Foundry-shim discovery surface

#### Added
Expand Down
33 changes: 18 additions & 15 deletions controller/src/reconciler/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,8 +248,8 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
let msg = format!(
"spec.runtime.kind=`{kind}` has no adapter wired in this controller \
build (S10.A1+A2 spine); skipping Deployment to avoid silently running \
the OpenClaw image. Track adapter rollout: OpenAIAgents=S10.A3, \
MicrosoftAgentFramework=S10.A4, BYO=S10.A2.b; \
the OpenClaw image. Track adapter rollout: BYO=S10.A2.b, \
OpenAIAgents=S10.A3 (wired), MicrosoftAgentFramework=S10.A4; \
SemanticKernel/LangGraph/Anthropic are Tier-2 placeholders pending roadmap"
);
tracing::warn!(sandbox = %name, runtime = %kind, "{msg}");
Expand Down Expand Up @@ -700,11 +700,14 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
.and_then(|b| b.per_request)
.unwrap_or(0);

// S10.A2.b: OpenClaw vs BYO branch the *agent container shape*.
// The router sidecar, init container, NetworkPolicy, SA, seccomp,
// volumes, and security context are runtime-agnostic. Only the
// agent container itself differs.
let is_byo = matches!(runtime_spec.kind, crate::crd::RuntimeKind::BYO);
// S10.A2.b / S10.A3: OpenClaw vs non-OpenClaw branch the *agent
// container shape*. The router sidecar, init container,
// NetworkPolicy, SA, seccomp, volumes, and security context are
// runtime-agnostic. Only the agent container itself differs.
// BYO (S10.A2.b) and OpenAIAgents (S10.A3) both follow the
// generic-runtime shape (different container name, no
// OpenClaw-specific env, no admin-token mount).
let is_openclaw = matches!(runtime_spec.kind, crate::crd::RuntimeKind::OpenClaw);

// Build OpenClaw container env vars.
//
Expand All @@ -724,13 +727,13 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
// and a BYO pod referencing it without `optional: true` would
// ImagePullBackOff-style fail to start.
let mut openclaw_env: Vec<serde_json::Value> = Vec::new();
if !is_byo {
if is_openclaw {
openclaw_env
.push(json!({"name": "OPENCLAW_MODEL", "value": inference_config.model.clone()}));
}
openclaw_env.push(json!({"name": "AZURE_OPENAI_ENDPOINT", "value": &ctx.openai_endpoint}));
openclaw_env.push(json!({"name": "AZURECLAW_AUTH_MODE", "value": "workload-identity"}));
if !is_byo {
if is_openclaw {
openclaw_env.push(json!({
"name": "OPENCLAW_GATEWAY_TOKEN",
"valueFrom": {
Expand All @@ -750,13 +753,13 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
// Foundry deployments list (so plugin shows only deployed models, not full catalog).
// BYO agents bring their own Foundry client (or none); skipped to avoid leaking
// the deployment list into a runtime that doesn't need it.
if !is_byo && !ctx.foundry_deployments.is_empty() {
if is_openclaw && !ctx.foundry_deployments.is_empty() {
openclaw_env
.push(json!({"name": "FOUNDRY_DEPLOYMENTS", "value": &ctx.foundry_deployments}));
}
// Inject Foundry Agent ID if set in status (for tools needing agent runs).
// BYO doesn't go through the OpenClaw plugin path; skipped.
if !is_byo
if is_openclaw
&& let Some(ref agent_id) = sandbox
.status
.as_ref()
Expand All @@ -766,7 +769,7 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
openclaw_env.push(json!({"name": "FOUNDRY_AGENT_ID", "value": agent_id}));
}
// Signal configured Foundry agent tools (OpenClaw plugin reads this).
if !is_byo
if is_openclaw
&& let Some(ref tools) = agent_config.tools
&& !tools.is_empty()
{
Expand Down Expand Up @@ -984,7 +987,7 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
// (env, security context, volumes, probes, resources) is
// identical across runtimes — they're platform contract,
// controller-enforced.
let agent_container_name = if is_byo { "agent" } else { "openclaw" };
let agent_container_name = if is_openclaw { "openclaw" } else { "agent" };
let agent_resources = spec
.resources
.as_ref()
Expand All @@ -1002,7 +1005,7 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
json!({"name": "sandbox-data", "mountPath": "/sandbox"}),
json!({"name": "tmp", "mountPath": "/tmp"}),
];
if !is_byo {
if is_openclaw {
// OpenClaw plugin needs admin token to authenticate trust
// mutations after KNOCK handshakes (pushTrustToRouter).
// BYO does not run the plugin — mount is omitted to keep the
Expand Down Expand Up @@ -1045,7 +1048,7 @@ async fn reconcile(sandbox: Arc<ClawSandbox>, ctx: Arc<Context>) -> Result<Actio
"periodSeconds": 10
}
});
if !is_byo {
if is_openclaw {
// OpenClaw gateway port (used by `azureclaw connect` port-forward).
agent_container["ports"] = json!([{"containerPort": 18789, "name": "gateway"}]);
}
Expand Down
Loading
Loading