Skip to content

142: EnvironmentProvider port + Cloudflare ephemeral-environment provider (Workers for Platforms + Containers); K8s/Daytona optional - #194

Closed
agent-relay-code[bot] wants to merge 3 commits into
mainfrom
factory/142-agentworkforce-factory-5a9a545c
Closed

142: EnvironmentProvider port + Cloudflare ephemeral-environment provider (Workers for Platforms + Containers); K8s/Daytona optional#194
agent-relay-code[bot] wants to merge 3 commits into
mainfrom
factory/142-agentworkforce-factory-5a9a545c

Conversation

@agent-relay-code

@agent-relay-code agent-relay-code Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Part of Epic: Factory Verification Environments (#141). This is the core provisioning primitive.

Why

The Factory needs to create and destroy isolated environments on demand to verify features. Follow the repo's existing port + provider seam (src/ports/fleet.ts with RelayFleetClient/InternalFleetClient implementations) so the environment substrate is swappable and testable. The primary provider is Cloudflare (the org's substrate — see #146); K8s/Daytona are optional providers behind the same port for stacks that can't run on Cloudflare.

What

Add an EnvironmentProvider port (src/ports/environment.ts) and a Cloudflare implementation (src/environments/cloudflare-provider.ts):

  • provision(spec): Promise<Environment> — create a Workers for Platforms dispatch namespace for the environment, wire per-env bindings/secrets, and (per the stack descriptor Verification-stack descriptor contract + deployer: repos declare their full stack, Factory brings it up #143) deploy the environment's Workers and Cloudflare Containers into it.
  • Environment carries { id, dispatchNamespace, endpoints, bindings, status, createdAt, ttl }.
  • status(id) — readiness of the namespace's Workers/Containers.
  • endpoints(id) — resolve the environment's reachable URLs (Worker preview URL / Container ingress) so callers (E2E driver, load harness) can hit them.
  • destroy(id) — delete the dispatch namespace + Container instances; idempotent.
  • TTL reaper — mirror src/orchestrator/reaper.ts (identity-checked) to delete environments past TTL or whose owning run died, so namespaces/Containers can't leak.
  • Config schema entry (mirror src/config/schema.ts) for the Cloudflare account/API-token (read via a Resource.*-style secret, not process.env), default limits, TTL.
  • Keep the Cloudflare API/wrangler dependency isolated behind the provider (the port stays substrate-agnostic). Leave a seam for an optional KubernetesEnvironmentProvider / DaytonaEnvironmentProvider implementing the same port later.
  • Export from src/index.ts.

Acceptance

  • EnvironmentProvider port with a Cloudflare provider; unit tests with a faked Cloudflare API/miniflare.
  • Provision creates a dispatch namespace + per-env bindings; destroy is idempotent (namespace + Containers gone).
  • Reaper deletes past-TTL / orphaned environments.
  • Exported from src/index.ts; config schema covers account/token + limits + TTL; port leaves room for K8s/Daytona providers.

✅ End-to-end verification — Factory success criteria (REQUIRED)

"Compiles" is not done. Run the provider against a REAL Cloudflare dispatch namespace and observe an environment come up, be reachable, and get reaped. CI uses miniflare for the fast path and a real dispatch namespace for the E2E.

Run it

npm run build
npm test                                   # unit (faked Cloudflare API / miniflare)

# E2E against a real dispatch namespace (creds from factory-test-infra)
CLOUDFLARE_API_TOKEN=... npx tsx test/e2e/environment-provider.e2e.ts

The E2E must (against real Cloudflare):

  1. provision() → assert the dispatch namespace exists, per-env bindings are set, and status() reports ready.
  2. Deploy a trivial Worker (or Container) into it and assert endpoints() resolves to a reachable URL (curl returns 200).
  3. destroy() → assert the namespace + Container instances are gone.
  4. Provision with a short TTL, run the reaper, assert the environment is deleted without an explicit destroy (proves leaks are cleaned).

Deliverable

  • test/e2e/environment-provider.e2e.ts running provision→reachable→destroy→reap against a real dispatch namespace in CI, one command.
  • Fails if provisioning, endpoint resolution, or reaping is broken (no false green).

Anchor files

  • src/ports/fleet.ts (port pattern) → new src/ports/environment.ts
  • src/fleet/relay-fleet-client.ts (provider-impl pattern) → new src/environments/cloudflare-provider.ts (+ later optional kubernetes-provider.ts / daytona-provider.ts)
  • src/orchestrator/reaper.ts (reaper pattern), src/config/schema.ts, src/index.ts
  • Cloudflare/miniflare posture precedent: relaycast-cloud, cloud's miniflare-sage + test:integration

Fixes #142


Summary by cubic

Adds a Cloudflare-backed EnvironmentProvider for ephemeral verification environments (Workers for Platforms + Containers), with endpoint resolution and a full provision → ready → destroy → reap lifecycle. Fixes #142.

  • New Features

    • CloudflareEnvironmentProvider with provision, status, endpoints, and destroy; per-environment bindings/secrets, Workers for Platforms dispatch namespace, Container ingress; idempotent teardown and a TTL reaper for past-TTL/orphaned envs.
    • CloudflareEnvironmentConfigSchema (Resource.*-only credentials, defaults/limits); wired into FactoryConfigSchema.environments.cloudflare; provider, types, and constants exported from src/environments/index.ts and main index.
    • Tests: fake Cloudflare API, Miniflare runtime, and real E2E at test/e2e/environment-provider.e2e.ts; new workflow cloudflare-environment-provider.yml; run via npm run test:e2e:cloudflare with CLOUDFLARE_API_TOKEN and optional CLOUDFLARE_ACCOUNT_ID.
  • Dependencies

    • Added wrangler@4.113.0, @cloudflare/containers@0.3.7, miniflare@4.20260721.0; Node engine set to >=22 and CI updated to Node 22; overrides include undici@7.28.0 and esbuild@0.28.1.

Written for commit 987427a. Summary will update on new commits.

Review in cubic

@agent-relay-code

Copy link
Copy Markdown
Contributor Author

Verdict: Clean. Preserves the EnvironmentProvider port contract, verifies owned metadata identity before every destroy/reap, fails closed on ambiguous owner/namespace state, and registers all three features with executable tier-1 and tier-5 procedures.

maintainability review · .agentworkforce/workforce/personas/maintainability.md

@khaliqgant

Copy link
Copy Markdown
Member

Stale-PR triage: REVIVE — and this is the Cloudflare implementation to keep

Triage sweep of the 6 open PRs, evaluated against origin/main @ f13600f. This PR and #182 are two independent attempts at overlapping Cloudflare ephemeral-environment ground; my recommendation is to keep this one and close #182.

The port half of the title has already landed; the Cloudflare half has not

src/ports/environment.ts is byte-identical on main, on this branch, and on #182's branch:

$ diff <(git show origin/main:src/ports/environment.ts) <(git show origin/<this-branch>:src/ports/environment.ts)
(no output — identical, 98 lines each)

It landed on 2026-07-21 via #173 (Verification gate: provision to teardown in the merge path) together with c4b545c feat: add Kubernetes verification environments. So the EnvironmentProvider port and the K8s provider — the "K8s/Daytona optional" arm of #142 — are already on main. Only the Cloudflare provider is outstanding, and it is genuinely absent:

$ git grep -i -l cloudflare origin/main -- src docs .github
origin/main:src/environments/verification-stack-deployer.test.ts

That single hit is an incidental target: { type: 'cloudflare' } literal in a deployer test — no implementation. #142 is still OPEN, consistent with this.

No later work superseded this. src/environments/ has had zero commits since 2026-07-21:

$ git log --oneline --since=2026-07-20 origin/main -- src/environments | head -1
18297f3 2026-07-21 Verification gate: provision to teardown in the merge path (#173)

The subsystem went quiet; it did not move on without this PR.

The E2E "failure" is an unprovisioned secret, not a code defect

The Cloudflare environment provider E2E failure on the current head is worth reading precisely, because it looks worse than it is. The job failed on its first step:

Run test -n "$CLOUDFLARE_API_TOKEN"
env:
  CLOUDFLARE_API_TOKEN:            <-- empty
  CLOUDFLARE_ACCOUNT_ID: ***       <-- set
##[error]Process completed with exit code 1.

The account ID is populated and the API token is not. The guard did its job and no Cloudflare provider code ever executed. This is a missing repository/environment secret — the same org-owner provisioning request raised on #142/#146 back in July and never answered. It is not evidence that the implementation is broken; it is evidence the implementation has never been exercised end to end. Whoever revives this needs the token provisioned before the E2E means anything.

CI itself is success on the same sha.

Rebase cost: small

Tested in an isolated detached worktree (never the shared checkout, no force-push):

$ git rebase f13600f...
CONFLICT — 2 files, 1 conflict block each:
  .agentworkforce/features/manifest.yaml
  src/config/schema.test.ts

Two conflict blocks total. mergeable: CONFLICTING overstates this considerably — none of the provider source conflicts.

Why this one rather than #182

Both branches diverged from the same base (2079ae0, 2026-07-21) and neither is an ancestor of the other — they are independent parallel builds of the same port, not complementary pieces. They collide directly: both define src/environments/cloudflare-provider.ts exporting the same names (CLOUDFLARE_ENVIRONMENT_TAG, CloudflareProvisionSpec, CloudflareDispatchNamespace). Only one can land.

This PR is the strict superset:

#194 (this) #182
cloudflare-provider.ts 1405 lines 758 lines
concrete HTTP client FetchCloudflareApi implements CloudflareApi HttpCloudflareEnvironmentClient
limits/guardrails config cloudflare-config.ts (quotas, CPU, subrequests)
credential indirection ResourceCloudflareCredentialResolver (Resource.* refs, no literals)
container fixtures + Miniflare test yes
draft no yes
tracking issue #142, open #146, closed on a false signal

#182's unique content is the factory-test-infra/ scaffolding and docs/cloudflare-environment-provider.md — worth salvaging as a follow-up, but it does not require keeping that branch's provider.

Recommendation only — I do not close or merge. Posted by an automated triage lane.

@khaliqgant

Copy link
Copy Markdown
Member

Triage: CONVERT-TO-ISSUE — the port landed without this PR, and the Cloudflare half has never been executed once

Stale-PR triage pass. This is a recommendation, not an action; no ruling has been made yet. Evidence below is against origin/main at f6050e0, fetched today.

1. Is the work already on main by another route? — Half of it, yes.

The PR body's headline deliverable is "Add an EnvironmentProvider port (src/ports/environment.ts)". That port is already on main:

  • origin/main:src/ports/environment.ts:93-97 declares EnvironmentProvider with exactly the four methods this issue specifies — provision(spec), status(id), endpoints(id), destroy(id) — plus Environment at :72 carrying id / endpoints / status.
  • It was added by Verification gate: provision to teardown in the merge path #173 (18297f3, "Verification gate: provision to teardown in the merge path") on 2026-07-21 — one day before this PR was opened.

To this branch's credit it does not re-add the port: 0 of its 18 files touch src/ports/environment.ts. So the branch already sits correctly on the landed seam. What remains unique here is the Cloudflare implementationsrc/environments/cloudflare-provider.ts (1405 lines) — which is not on main (git grep -il cloudflare origin/main -- src/ returns one unrelated test file).

2. Does the parent issue still describe something we want? — Partly, but its central premise was reversed on main.

#142 says: "The primary provider is Cloudflare (the org's substrate); K8s/Daytona are optional providers behind the same port."

Main shipped the opposite ordering. origin/main:src/environments/kubernetes-provider.ts:107 is export class KubernetesEnvironmentProvider implements EnvironmentProvider — a full implementation of this very port, with provision/status/endpoints/destroy at :136/:270/:286/:312 and a KubernetesEnvironmentReaper at :546. It is supported by ~14 other files under src/environments/ and by four green E2E workflows on this PR's own CI: kubernetes-provider-e2e, verification-gate-e2e, verification-stack-e2e, load-e2e.

So the "optional escape-hatch" provider is the one that shipped, is tested, and is wired into the merge path. Cloudflare is the one that did not. That is a product decision that has already been taken by what landed — it should be made explicit rather than resolved by rebasing this branch.

3. How big is the conflict? — Small. 4 files, 81 commits behind.

git merge-tree --write-tree origin/main origin/factory/142-agentworkforce-factory-5a9a545c → exit 1, conflicting:

.agentworkforce/features/manifest.yaml
package.json
package-lock.json
src/config/schema.test.ts

git rev-list --left-right --count origin/main...<branch>81 behind, 3 ahead.

The conflict is genuinely cheap. Conflict size is not what should decide this PR — see below.

4. What is the failing check actually telling us? — That the real path has never run. Not once.

Real dispatch namespace and Container fails in 6 seconds, which is too fast to be a test failure. Reading the job log (run 29929804013, job 88956396664) rather than inferring from the name, it dies on the job's first step:

##[group]Run test -n "$CLOUDFLARE_API_TOKEN"
env:
  CLOUDFLARE_API_TOKEN:            <- empty
  CLOUDFLARE_ACCOUNT_ID: ***
##[error]Process completed with exit code 1.

That step is Require the protected Cloudflare Resource secret in .github/workflows/cloudflare-environment-provider.yml. It is a correct fail-closed guard and it is doing its job. The job never reached npm ci, docker info, or npm run test:e2e:cloudflare.

Checked today: the factory-test-infra GitHub environment exists but has zero secrets configured, and there are zero repo-level Actions secrets. So this cannot be re-run green today by any code change.

Why this matters more than a red check. The pattern the lead flagged — fake path green, real path red — reads exactly as it looks. Miniflare binding fast path passes because miniflare is a local simulator that needs no credential. The consequence is that the entire 1405-line Cloudflare provider has never been validated against Cloudflare. Provisioning a real dispatch namespace, wiring per-env bindings, resolving Container ingress, and idempotent teardown are precisely the behaviours a local fake cannot vouch for, and they are the whole substance of the PR. This branch's own acceptance evidence does not exist and never has.

Recommendation

CONVERT-TO-ISSUE. Close this PR and carry the design + the 1405-line provider forward as an issue, because the open question is not mergeability — it is two decisions that only the principal can make:

  1. Do we still want a Cloudflare provider at all, now that Kubernetes shipped behind the same port and is green in the merge path?
  2. If yes, who provisions a Cloudflare API token into the factory-test-infra environment? Until that exists, this code cannot be proven to work, and merging unproven provisioning code that creates and destroys real cloud resources is the risky direction, not the safe one.

Rebasing first would answer neither, and would land 1405 lines whose only passing evidence comes from a fake. The branch is preserved in the issue and rebases cheaply (4 files) whenever question 1 is answered yes.

Holding for a ruling — not closing anything on my own.

@khaliqgant

Copy link
Copy Markdown
Member

Closing under the accepted stale-PR ruling and converting the remaining work back to its existing parent, #142.

The port half is already on origin/main; #142 now records the exact landed paths and the still-unlanded Cloudflare half. This branch preserves the historical provider candidate, but its real protected Cloudflare lifecycle job never executed because the required token was absent. The active factory-cloud singleton Container/Durable Object hosting work does not consume this per-run dispatch-namespace provider.

No duplicate issue was created. Continue the remaining design, credential provisioning, and real provision → reachable → destroy → reap proof in #142.

@khaliqgant khaliqgant closed this Aug 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[factory] EnvironmentProvider port + Cloudflare ephemeral-environment provider (Workers for Platforms + Containers); K8s/Daytona optional

2 participants