This directory is a worked example, not a bundle dd ships to users. Every directory here is one easyenclave workload. Together they describe a complete DD agent VM: the minimum infra to boot podman, register with a control plane, and optionally expose workload ports on stable hostnames.
The goal is to be the shortest legible "agent VM from scratch" that you can copy and adapt. For orchestrating many workloads, assembling them from templates, and the run / teardown lifecycle, see slopandmop.
apps/
<name>/
workload.json # literal spec
workload.json.tmpl # spec with ${VAR} placeholders (baked at deploy time)
_infra/ # host-side scripts; not a deployable workload
A workload is a JSON object consumed by easyenclave's DeployRequest (see
src/easyenclave/src/workload.rs). Minimum shape:
{
"app_name": "myapp",
"cmd": ["myapp"]
}Add github_release to fetch a binary asset directly from a GitHub release —
no OCI registry, no Dockerfile. The asset lands in /var/lib/easyenclave/bin/
and is spawned by cmd:
{
"app_name": "cloudflared",
"github_release": {
"repo": "cloudflare/cloudflared",
"asset": "cloudflared-linux-amd64",
"rename": "cloudflared"
}
}EE does not ship a rootfs shell. If a workload wants shell scripting, fetch a shell/toolbox as an explicit workload asset first and run that asset:
{
"app_name": "busybox",
"github_release": {
"repo": "devopsdefender/dd",
"asset": "busybox",
"tag": "${DD_RELEASE_TAG}"
}
}Then dependent recipes can use "cmd": ["busybox", "sh", "-c", "..."].
That keeps EE shell-less while letting workload bundles bring their own
userspace when they need scripting.
Add env to inject config:
{
"env": ["MY_ENDPOINT=https://api.example.com", "DEBUG=1"]
}Add expose to ask DD to route a public hostname to a workload's port:
{
"app_name": "web",
"expose": { "hostname_label": "web", "port": 8081 },
"cmd": [...]
}At agent boot, apps/_infra/local-agents.sh collects every expose entry
into DD_EXTRA_INGRESS. dd-agent forwards them on /register and the CP
prepends them to the agent's cloudflared tunnel ingress. A workload declaring
{"hostname_label": "web", "port": 8081} becomes reachable at
web.<agent-hostname> — in addition to the default dashboard at
<agent-hostname>. easyenclave itself ignores the field; it's a DD-level
hint about tunnel routing.
Add oracle to declare that an exposed workload is a read-only oracle endpoint:
{
"app_name": "human-readonly",
"expose": { "hostname_label": "oracle", "port": 8082 },
"oracle": {
"title": "Human read-only oracle",
"path": "/oracle.json",
"interval_secs": 10
},
"cmd": [...]
}At agent boot, DD also extracts these oracle hints into DD_ORACLES_B64.
dd-agent scrapes http://127.0.0.1:<port><path>, publishes the current oracle
state on /health and /api/oracles, and lists it in both the agent dashboard
and CP fleet detail page. The vanity URL uses the expose.hostname_label
(oracle.<agent-hostname> in the example). This is observation-only metadata:
it does not create a read-write terminal or any input path into the workload.
DD separates terminal access by capability:
- Read-only workload terminals show workload logs in the terminal UI. They are for oracle-style services where an operator should be able to inspect output without sending input, resizing a PTY, interrupting, or closing the process. Opening a read-only terminal is observation only, so it leaves the workload's user-facing integrity state clean.
- Read-write PTY sessions are owned by
dd-sessiond. They are for confidential shells and ZDR coding agents such as Codex or Claude. These sessions are reconnectable and write encrypted transcript records. A read-write PTY is controlled as soon as it exists because the holder can send stdin, resize the terminal, and deliver terminal signals.
The native client treats both as terminal views, but only read-write sessions
get input, resize, and close controls. Workloads do not opt into read-write
access by putting metadata in workload.json; the boundary is the session
protocol exposed by dd-agent over Noise.
Internally DD may still call this taint tracking, but the API/UI should speak in
integrity terms: clean for observed-only logs, controlled for interactive PTYs
or other human control paths.
Native clients may recognize WezTerm-compatible notification escapes:
printf '\033]9;%s\033\\' 'job finished'
printf '\033]777;notify;%s;%s\033\\' 'oracle' 'new result available'The native desktop/mobile app is the target for shell inbox workflows: read-only workload cards, read-write Codex/Claude session cards, and notifications for long-running jobs.
Per-workload ingress is boot-time only today. Workloads POSTed later via
/deploy don't get auto-exposed — declare your exposure on boot workloads in
this tree.
Files ending in .json.tmpl carry ${VAR} placeholders. At bake time:
envsubstsubstitutes every uppercase${VAR}that appears in the template using the caller's environment.jqdrops env-array entries whose value ended up empty (so you can make OAuth creds / optional secrets conditional by just leaving them unset).- The result is a plain
workload.jsonready for EE.
Only uppercase placeholders get substituted — shell locals like $i or
$((n+1)) inside cmd strings are left alone. The bake helper is duplicated
inline in two places so both lifecycle points behave identically:
.github/workflows/deploy-cp.yml(CI, for CP workloads)apps/_infra/local-agents.sh(tdx2 host, for agent VMs)
| workload | CP VM | agent VM (preview) | agent VM (prod / dogfood) |
|---|---|---|---|
busybox |
✅ | ✅ | |
cloudflared |
✅ | ✅ | ✅ |
dd-agent |
✅ | ✅ | |
dd-sessiond |
✅ | ✅ | ✅ |
dd-shell |
✅ | ✅ | ✅ |
human-readonly |
✅ | ||
dd-management |
✅ | ||
podman-static |
✅ | ✅ | |
ca-certificates |
✅ | ✅ | |
podman-bootstrap |
✅ | ✅ |
Additional examples:
apps/human-readonly: tiny preview-only read-only oracle. It emits logs for native clients, serves/oracle.jsonon port 8082, gets a vanityoracle.<agent-hostname>address, and appears in the dashboards. It is a shell workload recipe, not adevopsdefenderbinary subcommand.apps/oracle-readonly: standalone oracle example with the same scraper and vanity-address metadata; copy this shape into real oracle app repos.apps/confidential-shell: legacy standalone shell workload for deployments that still run the browser shell and PTY supervisor in one process. Scheduled for removal once all clients usedd-sessiondover Noise.apps/codex-podman-shell: legacy read-write shell workload. It exposes the normal-shelllabel and carries an older self-contained Codex recipe path. Scheduled for removal; new deployments should usedd-sessiond.
CP stays slim: cloudflared + dd-management + static/web client assets as
needed. It must not carry shell, log, transcript, or PTY bytes.
Preview agent VMs run a small read-only oracle plus agent + podman for CI to
prove registration, scraping, vanity ingress, and dashboards end-to-end. Prod
agent VMs use the
same CPU-only boot shape without demo workloads for now. dd-local-dogfood
uses that same prod boot chain but is manually managed, sized larger by
default, and not relaunched by CI.
Use apps/_infra/dd-dogfood.sh when you want a real, long-lived local VM
registered to production for Codex/Podman development:
export DD_ITA_API_KEY="$(cat ~/.secrets/ita_api_key)"
export DD_AUTH_COOKIE_SECRET="$(cat ~/.secrets/dd_auth_cookie_secret)"
export EE_OWNER="posix4e" # or an org/repo principal
./apps/_infra/dd-dogfood.shThe script defines and starts dd-local-dogfood against
https://app.devopsdefender.com. By default it reuses the already-synced local
EasyEnclave base tag, falling back to the EasyEnclave staging channel only when
no local tag exists. It uses the latest DD release by default. It preserves
/var/lib/libvirt/images/dd-local-dogfood-workload.qcow2 across runs, so
Podman images, shell transcript storage, and Codex login state survive explicit
operator refreshes. Production deploys do not call this script and do not
destroy the dogfood VM.
Optional sizing knobs:
DD_DOGFOOD_DISK_SIZE=1024G DD_DOGFOOD_MEM_KIB=67108864 DD_DOGFOOD_VCPUS=12 \
./apps/_infra/dd-dogfood.shEasyEnclave spawns boot workloads concurrently — there's no declared dependency graph. Dependents self-sequence by polling for their prerequisites.
podman-bootstrapwaits forpodman-static's tarball, copies theca-certificatesrelease asset into Podman's persistent config, then stagespodman,conmon,crun, config, and thepodmanwrapper script.- Shell-based recipes wait for
busyboxby depending on the fetch-onlybusyboxworkload in the boot set.
Costs seconds of wasted polling at boot; easy to reason about; no workload-runner changes needed.
- Copy an existing folder as a starting point:
cp -r apps/cloudflared apps/myapp $EDITOR apps/myapp/workload.json - Decide where it runs:
- CP VM: add a
bake apps/myapp/workload.jsonline to the workload-buildingrun:step in.github/workflows/deploy-cp.yml. - Agent VM: add the same call to
apps/_infra/local-agents.shinbuild_config_iso(). - Ad-hoc, runtime-only: POST the baked JSON to
/deployon a running agent. The endpoint is gated in-code by a GitHub Actions OIDC JWT. From inside a GitHub Actions workflow running in theDD_OWNERorg:OIDC=$(curl -fsSL \ -H "Authorization: Bearer ${ACTIONS_ID_TOKEN_REQUEST_TOKEN}" \ "${ACTIONS_ID_TOKEN_REQUEST_URL}&audience=dd-agent" | jq -r .value) curl -fsS -X POST https://<agent-host>/deploy \ -H "Authorization: Bearer ${OIDC}" \ -H "Content-Type: application/json" \ -d @apps/myapp/workload.json
- CP VM: add a
- Schema source of truth:
src/easyenclave/src/workload.rs— theDeployRequeststruct EE deserializes on/deploy.exposeis not in this struct; EE silently ignores it. DD reads it at the bake + register boundary. - CP deploy caller:
.github/workflows/deploy-cp.yml— inlinebake()+ CP workload set. - Agent VM builder:
apps/_infra/local-agents.sh— inlinebake()+ agent workload set per kind. - Ingress and oracle plumbing:
src/cf.rs(create()takes per-workload ingress),src/cp.rs(registerhandler acceptsextra_ingress, dashboard renders scraped oracle status),src/agent.rs(readsDD_EXTRA_INGRESSandDD_ORACLES_B64, forwards ingress on/register, scrapes oracle endpoints).