Handbook
Design: Fleet-distributed campaign dispatch
Status: signed off (Program D, P1). Author: Fable (orchestrator) with Architecture + Security Versona review notes inline. Implementation: Program D P2–P5. Prerequisite: the lmeta bridge (docs/design/LMETA-BRIDGE.md) is…
Updated
Purpose
Let a PDCA campaign fan independent items out to a prioritized list of Forge Fleet servers as docker_argv jobs, run each item inside a container with the dark-factory worker stack, return artifacts to the host, and keep every trust gate (assay, freeze) host-side. Rollback: FORGE_DARK_FACTORY_DISPATCH unset → the existing sequential local path runs unchanged.
Dispatch contract — forge_lcdl.dispatch.fleet
New leaf module src/forge_lcdl/dispatch/fleet.py (+ empty-ish dispatch/__init__.py). Boundary rules (Architecture Versona):
- stdlib
urllib/json/timeonly; no new dependencies. - No imports from
forge_lcdl.flow,runner,tasks,transport— and nothing in forge-lcdl core importsdispatch. Consumers inject it (mirrorsRunLcdlFninjection); the lmeta flow reaches it only through a consumer hook the host binds.
API surface (shapes mirror forge-lenses/lenses/sandbox/fleet_client.py and forge-certificators/.../fleet_client.py):
@dataclass(frozen=True)
class FleetNode: # id, base_url, bearer_token, enabled, priority, max_cpu_percent, max_memory_percent
@dataclass(frozen=True)
class FleetJobRef: # node_id, base_url, job_id (NO token stored)
load_fleet_servers(path=None, environ=None) -> list[FleetNode]
select_node(nodes, *, exclude_ids=()) -> tuple[FleetNode, dict] | None # priority asc, /v1/health probe, ceiling skip
submit_docker_argv_job(node, argv, *, session_id="", meta=None) -> FleetJobRef
upload_workspace(node, job_id, tar_gz_bytes) -> dict # PUT /v1/jobs/{id}/workspace + sha256 header
poll_job(node, job_id, *, timeout_s, interval_s) -> dict # GET /v1/jobs/{id} until terminal
cancel_job(node, job_id) -> dict
Config resolution order for load_fleet_servers: FORGE_FLEET_SERVERS_FILE env → <cwd>/config/fleet-servers.json → single-node fallback from FORGE_FLEET_BASE_URL + FORGE_FLEET_BEARER_TOKEN (priority 0). Node JSON shape is identical to Lenses fleet-settings.json nodes[] entries so Studio-configured nodes can be pasted verbatim:
{"nodes": [{"id": "granite-host", "base_url": "http://host:18766", "bearer_token": "…",
"enabled": true, "priority": 100, "max_cpu_percent": null, "max_memory_percent": null}]}
Executor integration
No new lmeta ref. The host (pdca.py fan-out mode) performs dispatch outside the flow: the container runs the full lmeta driver loop for one item; the host submits/polls/ingests. A future per-state dispatch (single flow state remoted) would use a df.dispatch consumer hook bound to this module — the hook name is reserved, not implemented in Program D.
Job shape
{"kind": "docker_argv",
"argv": ["docker", "run", "--rm", "--network", "host",
"-v", "{fleet_workspace}:/workspace",
"forge-agent-worker:latest",
"python", "-m", "forge_dark_factory.container_worker",
"--item-spec", "/workspace/item-spec.json", "--out", "/workspace/out"],
"session_id": "dfd-{campaign_id}-{item_id}",
"meta": {"container_class": "forge-agent-worker",
"workspace_upload_required": true,
"use_fleet_template_image": true,
"requirements": ["forge-agent-worker"],
"build_template_if_missing": true}}
The Fleet runner substitutes its workspace mount; container_worker reads the bundle, runs one campaign item via the lmeta driver path (FORGE_DARK_FACTORY_VIA_LMETA=1 inside the container), posts progress via POST /v1/jobs/{id}/workspace-worker-progress (header X-Workspace-Worker-Token, token delivered inside the workspace bundle by Fleet — never by us), and writes results to /workspace/out.
Workspace tarball (host → node)
Contents (built by a packer helper in dark-factory):
target/— target repo snapshot: tracked files only, excluding.git, virtualenvs,runs/, and any*.env/*secrets*paths (deny-glob list in the packer, tested).item-spec.json— serialized campaign item (goal, level, sublevel, worker_ladder minuscursor, allowed_files, patch_units, fixture inline if small).llm-env.json— LLM endpoint config for the container:LLM_BASE_URL,LLM_MODEL,LLM_API_KEYonly (Security Versona: LLM key is the only secret that enters the container; it is required for drafting; it is never echoed into artifacts — existingmodel_report.jsonrecords env file name, not contents).
Explicitly excluded: Fleet bearer tokens (host-only), Cursor credentials (the cursor ladder rung is stripped from remote item specs; cursor-backend items are not eligible for dispatch), git credentials, .ssh.
Result retrieval and host-side gates
- Terminal job → host reads
worker_resultJSON (small summary: final_status, assay verdict, artifact manifest) and downloads the out-bundle (Fleet workspace round-trip; same channel as certificators source-ingest). - Host unpacks
machine/+human/trace intoruns/campaigns/<cid>/<item>/run-*/exactly where the local path would write them. - Host re-runs
check_assayon the returned evidence andfreeze_gate.verifyon the run dir — a remote worker cannot self-certify (trust boundary; Security Versona requirement). machine/run.jsongainsdispatch: {"mode": "fleet", "node_id": "...", "job_id": "...", "base_host": "host:port"}— no token, no full URL with credentials; H report renders one dispatch line.
Fan-out semantics (pdca.py)
- Flags:
FORGE_DARK_FACTORY_DISPATCH=fleet(default unset = local),FORGE_DARK_FACTORY_DISPATCH_MAX_PARALLEL(default 2),FORGE_DARK_FACTORY_DISPATCH_REQUIRE_REMOTE=1→ zero eligible nodes is a hard error; unset → documented local fallback with a warning line. - Scheduler: items are independent by campaign contract; submit up to N in flight; re-select node per item (
exclude_idsof busy nodes when possible) so healthy nodes share load; poll round-robin; aggregate into the existing campaign summary shape. - Item eligibility:
worker_laddermust not requirecursor;promote_liveitems run promote host-side after ingest, never in-container.
Container image (docker/agent-worker/)
Single Dockerfile: python:3.12-slim, non-root worker user, editable installs of vendored forge-lcdl, forge-workcells, and forge_dark_factory from the build context, no secrets baked in. Registered as Fleet container template id forge-agent-worker (PUT /v1/container-templates, POST /v1/container-templates/build — operator step documented in docs/FLEET-DISPATCH.md). No forge-fleet server changes.
Security posture (Security Versona summary)
| Concern | Decision |
|---|---|
| Fleet bearer | Host env/config only; never in tarball, argv, traces, or logs; FleetJobRef carries no token |
| LLM key | Enters container via llm-env.json; excluded from out-bundle and traces |
| Cursor auth | Never containerized; cursor rung stripped from remote specs |
| Worker self-certification | Impossible — assay + freeze re-run host-side on returned evidence |
| Workspace leakage | Packer deny-globs (.git, *.env, *secrets*, .ssh) with a unit test asserting exclusion |
| Node trust | Nodes are operator-configured (same trust level as Lenses fleet-settings); no auto-discovery in dispatch path |
Rejected alternatives
- New lmeta
forgeDispatchref — dispatch is a consumer capability, not an orchestration primitive; hooks/host-side scheduling suffice (lmeta boundary rule). - Fleet server API changes (job groups, node federation) — client-side priority selection over N single-orchestrator hosts is proven in Lenses and sufficient for campaign fan-out.
- Remoting the Granite HTTP call only (no container) — would distribute the cheapest part (LLM call) while keeping verify/apply local; containerizing the whole item run is what actually parallelizes campaigns.