Task Breakdown: Ever Works Apps — isolated hosting tier (launch gate)
Ordered tasks derived from
plan.md. Each is small enough to land in one PR and ships with tests per Constitution VI. Schema tasks ship their migrations in the same PR per Constitution V.
Epic ID: APW-10-apps-hosting-tier
Spec: ./spec.md · Plan: ./plan.md
Status: Draft
Last updated: 2026-09-17
How to use
- Tasks are sequential by default.
(parallel)means it may run alongside its predecessor. - Every task names the files to create or modify. new marks a file that does not exist yet; every other path was
checked with
git ls-filesondevelop@ee45946e5. - Every task has a Test line (file + assertion, or the run command for a test task) and a Done when line that is checkable without reading the diff.
- Add new tasks at the bottom rather than renumbering.
- Program audit resolutions (CONTRACTS.md §0) applied here: R-1 (shared types in
packages/contracts/src/apps/), R-2 (ActivityactionTypeapp_tier, dottedaction), R-5 (the platform writes desired state only; this epic ownsAppsTierPolicy), R-15 (delete semantics), R-20 (Quarantine naming), R-22 (no suite underapps/api/test/), R-24 (sandboxed runtime in Wave 2, sandboxed builds in Wave 3). - Public-repository hygiene applies to every task: no address, host name, cluster or node name, vendor account id, or assessment of an existing system in code, tests, fixtures, comments, commit messages or PR text. Probe targets, zone values and drill evidence live in the private operations repository.
- Infrastructure work (building the zone, admission products, edge account, data servers) is not in this file; it is tracked in the private operations plan. Tasks here that need a zone say so. Where a Test line is an operator attestation, it names the automated self-check run it relies on and the private evidence artefact by name only (plan §10.5).
- Run commands, from the monorepo root:
- contracts:
pnpm --filter @ever-works/contracts test - plugin contracts:
pnpm --filter @ever-works/plugin test - controller:
pnpm --filter ever-works-hosting-operator test(kind lane:test:integration) - tier plugin:
pnpm --filter @ever-works/ever-works-apps-plugin test - agent:
pnpm --filter @ever-works/agent test -- apps-tier - tasks:
pnpm --filter @ever-works/trigger-tasks test - API:
cd apps/api && pnpm test -- apps-tier - web unit:
pnpm --filter ever-works-web test -- apps-tier; web e2e:pnpm --filter ever-works-web test:e2e admin-apps-tier
- contracts:
Phase P1 — The gate can be evaluated and the brakes work (Wave 2)
Delivers spec FR-1…FR-29, FR-41…FR-46, FR-52, FR-53. The tier stays Closed.
P1.1 — Shared contracts
-
T1. Launch-gate ids, outcomes and reason codes. Create
packages/contracts/src/apps/apps-tier.ts(new, Resolution R-1) —LAUNCH_GATE_ITEM_IDS(LG-01…LG-25),LaunchGateKind(automated/attested/both),LaunchGatePhase(P2/P3),LaunchGateOutcome(passed/failed/inconclusive/error), every reason code from plan §3.7 and §5.4, tier states and scopes,Workphases (incl.removed), quarantine categories and sources, signal kinds and severities, owner eligibility reasons (emailUnverified,planRequired,ownerQuarantined,capReached,tierClosed), and the numeric constants (GATE_MAX_AGE_HOURS = 24,SELF_CHECK_BUDGET_MS = 900_000,PROBE_CONNECT_TIMEOUT_MS = 3_000,PROBE_ATTEMPTS = 2,SELF_CHECK_INTERVAL_HOURS = 6,ATTESTATION_TTL_DAYS = 90,ATTESTATION_NOTICE_DAYS = [14, 1],HEARTBEAT_MAX_AGE_MS = 120_000,GATE_WATCH_INTERVAL_MIN = 5,QUARANTINE_ISOLATE_MS = 15_000,QUARANTINE_SCALE_MS = 60_000,QUARANTINE_EDGE_MS = 120_000,RELEASE_RESTORE_MS = 180_000,DETECTOR_QUARANTINE_MS = 60_000,IMAGE_ALLOWANCE_MAX_DAYS = 30,PULL_CREDENTIAL_MAX_MS = 900_000,REMOVED_DATA_RETENTION_DAYS = 30). Modify APW-03's barrelpackages/contracts/src/apps/index.ts—export * from './apps-tier.js';(create the barrel, andexport * from './apps/index.js';inpackages/contracts/src/index.ts, only if APW-03 has not landed). Test:packages/contracts/src/apps/__tests__/apps-tier.spec.ts(new) — pins the 25 ids in order, every union (a member cannot be added without editing the test) and every constant, incl. the 6-hourSELF_CHECK_INTERVAL_HOURS(ACC-10-01, ACC-10-07); runpnpm --filter @ever-works/contracts test. Done when:import { LAUNCH_GATE_ITEM_IDS } from '@ever-works/contracts'resolves fromapps/api,apps/webandapps/hosting-operator, andpackages/contracts/src/__tests__/index.barrel.spec.tspasses. -
T2 (parallel with T1).
apps-tiercapability contract. Createpackages/plugin/src/contracts/capabilities/apps-tier.interface.tsandapps-tier.types.ts(new) exactly as plan §5.1, incl.removeWork(workId, { deleteData }). Modifypackages/plugin/src/contracts/capabilities/index.ts(export both) andpackages/plugin/src/contracts/facade-capabilities.ts(APPS_TIER: 'apps-tier'). Test:packages/plugin/src/contracts/__tests__/apps-tier.types.spec.ts(new) —submitBuildandgetBuildstay optional,removeWorkrequires thedeleteDataoption,PLUGIN_CAPABILITIES.APPS_TIERequals'apps-tier'; runpnpm --filter @ever-works/plugin test. Done when:pnpm --filter @ever-works/plugin buildis clean and no existing capability constant changed.
P1.2 — The zone controller
- T3. Controller package and CRDs.
Create
apps/hosting-operator/(new):package.json(nameever-works-hosting-operator, private, ESM,tsup,vitest, scriptstestandtest:integration, dependency@kubernetes/client-nodematchingpackages/plugins/k8s/package.json),tsconfig.json,vitest.config.ts,src/crds/{work,selfcheck,usagereport,abusesignal,appbuild}.ts(schemas from plan §3.1–3.2, incl.spec.desiredState: removed,spec.dataDeletion,status.removal,status.dependencies),scripts/generate-crds.ts→deploy/crds/*.yaml.pnpm-workspace.yamlalready globsapps/*. Test:packages/hosting-crds/src/crds/__tests__/crds.spec.ts(new) — generated YAML equals committed YAML;Workrejects anamespacefield (ACC-10-26), an image without@sha256:, and an object over 512 KiB (ACC-10-27); runpnpm --filter ever-works-hosting-operator test. Done when:pnpm --filter ever-works-hosting-operator build testpasses and CI fails if generated CRDs drift.
T3 landed differently, and deliberately — two owner rulings. (1) 2026-09-20: T3 shipped as written, then the schema half was split out, because both ends of the tier import it and
apps/*here means a process, which a CRD-only package was not. (2) 2026-09-21: the owner rejected the nameapps-tier-controlleroutright. The process is nowapps/hosting-operator(ever-works-hosting-operator) and the contract ispackages/hosting-crds(@ever-works/hosting-crds) — “operator” is the Kubernetes word for a controller that owns custom resources, andhostingmatches the API group these CRDs actually serve,hosting.ever.works/v1alpha1. The drift gate ispnpm --filter @ever-works/hosting-crds test(47 tests); the process’s bootstrap, config and reconciler port landed on 2026-09-20 and it refuses to start until T6 registers a reconciler. TheAPPS_TIER_*names in@ever-works/contractsare deliberately unchanged: they are operator-facing env-var names and the tier’s own product vocabulary, not this package’s name. Seeapps/hosting-operator/README.md.
-
T4. Tenant template and pod overlays. Create
apps/hosting-operator/src/template/tenant-template.tsandsrc/template/pod-overlays.ts(new) per plan §3.4 (pure functions ofWork, profile and zone info; StatefulSet PVC retention pinned toRetain). Test:apps/hosting-operator/src/template/__tests__/tenant-template.spec.tsandpod-overlays.spec.ts(new) with golden files insrc/template/__tests__/fixtures/— a hostile rendered pod (privileged,hostNetwork,runAsUser: 0, token automount, no runtime class, extra capabilities) comes out fully overwritten (ACC-10-15 unit half); the egressexceptlist contains all 11 standard ranges; ports 25/465/587 absent (ACC-10-13 unit half); the Starter and StandardResourceQuotaobjects equal spec FR-47 incl. 0 load balancers and 0 node ports (ACC-10-14 unit half, ACC-10-41). Createapps/hosting-operator/src/template/__tests__/canary-parity.spec.ts(new) — the template for aWorklabelledhosting.ever.works/canary: "true"equals the template for a realWorkobject by object once names, ids and the canary label are masked (ACC-10-06). Done when: no overlay field can be influenced by anyWork.specvalue and the three specs pass. -
T5. Validation, fingerprints and unsealing. Create
apps/hosting-operator/src/validate/work-spec.validator.ts,src/validate/credential-fingerprints.ts,src/seal/unseal.ts(new). Test:apps/hosting-operator/src/validate/__tests__/work-spec.validator.spec.ts— every FR-26 limit at N and N+1 (ACC-10-27), each refusal code of plan §3.1, a host already referenced by anotherWorkrefusedHOST_CLAIMED(ACC-10-31 unit half);src/validate/__tests__/credential-fingerprints.spec.ts— an env value equal to a configured fingerprint refuses theWorkwithPLATFORM_CREDENTIAL_IN_ENVand zero workloads, a one-character change passes (ACC-10-28);src/seal/__tests__/unseal.spec.ts— rejects a tampered tag and a wrong key id. Done when: the three specs pass and no refusal message contains the env value. -
T6. Work reconciler, heartbeat, leader election. Create
apps/hosting-operator/src/main.ts,src/reconcile/work.reconciler.ts(new) — informers onWork, level-triggered reconcile with 300 s resync, Leaseever-works-apps-controllerrenewed every 30 s (FR-29), status phases andobservedGeneration, namespaceewa-<first 20 hex>, template applied before any workload, rendering through APW-06's App renderer library (import frompackages/plugins/k8s/src/app/as APW-06 exports it — the platform never sends workload objects, Resolution R-5), overlays, server-side apply by the controller with field managerever-works-apps-controller. Removal without data deletion (FR-28) delegates to T39'sremoval.reconciler.ts. Added by the 2026-09-17 fix pass. The reconciler uses only APW-06's workload builders (componentDeployments,Services,Ingress, command/runnerJobs,CronJobs,PVCs, envSecret) and discards the renderer'sNamespace,ServiceAccount,LimitRange,ResourceQuotaandNetworkPolicyobjects — tenancy objects come only from T4'stenant-template.ts(APW10-G03). Createsrc/render/work-to-render-input.ts(new) for the reverse mapping (Work.spec→ APW-06AppRenderInput, includingenv.valuesafter T44's substitution,policy,ingress,hosts.previous,deploymentShort) with a golden round-trip test against T26's forward mapper. It runs T43's dependency reconciler before reporting a phase, and it performs T52's P1 image promotion at startup (APW10-G08). Test:apps/hosting-operator/src/reconcile/__tests__/work.reconciler.spec.ts(new) with a fake API — ordering (template before workloads); refusal leaves zero workload objects; twoWorks get two namespaces (ACC-10-26); a removedWorkstill has its PVCs and its dependency references after a simulated 30 days (ACC-10-29); the Lease renew interval is 30 s. Done when: the spec passes and the reconciler's API call log contains no create/patch from any identity other than the controller's. -
T7. Quarantine sequencer. Create
apps/hosting-operator/src/reconcile/quarantine.sequencer.ts(new) per plan §2.5: isolate → record counts → scale 0 + suspend CronJobs/Jobs → hosts to unavailable backend, each with a status timestamp; release in reverse from recorded counts; detector quarantine withsource: detector. Test:apps/hosting-operator/src/reconcile/__tests__/quarantine.sequencer.spec.ts(new) — order asserted from the fake API call log with each timestamp inside the LG-18 budgets on a healthy fake (ACC-10-32 unit half); release restores{ web: 2, worker: 1 }exactly and removes the isolation policy (ACC-10-33 unit half); a quarantine arriving mid-rollout stops further apply steps and sets conditionCancelledwith reasonQuarantined(ACC-10-35 controller half); no PVC, Secret or image is modified. Done when: the spec passes; the quarantine timing evidence on a real zone isapw10-quarantine-drill(T22). -
T8. Probe entrypoint and library. Create
apps/hosting-operator/src/probe/main.ts,net.ts,kernel.ts,token.ts,marker.ts,report.ts(new) — theprobecommand reads its plan from a mounted ConfigMap, runs TCP/HTTPS checks with the T1 timeouts, writes a ≤ 4 KiB JSON result to the termination message, never prints a target address. Test:apps/hosting-operator/src/probe/__tests__/net.spec.tsandreport.spec.ts(new) — fake sockets: refused, timeout, accepted; a failed control turns dependentsinconclusive(ACC-10-04 unit half); an accepted connection to a private-range sentinel yieldsPRIVATE_RANGE_REACHABLE(ACC-10-08 unit half), to a metadata addressMETADATA_REACHABLE(ACC-10-12 unit half), to tenant BCROSS_TENANT_REACHABLE(ACC-10-11 unit half);kernel.spec.ts— no sandbox signature yieldsSANDBOX_KERNEL_NOT_DETECTED; output never contains an input address. Done when: the specs pass and the result JSON of every fixture is ≤ 4 KiB. -
T9. SelfCheck reconciler (zone side). Create
apps/hosting-operator/src/reconcile/selfcheck.reconciler.ts,src/policy/drift.ts(new) — ensurescanary-a/canary-bWorks from zone config through the samework.reconciler.tspath (plan D-F), runs probe Jobs for LG-02…LG-11, LG-13, LG-14, LG-19 (zone part), LG-22; server-side dry-runs as theapps-tier-proberservice account; zone part of the LG-18 Tenant quarantine drill; writesSelfCheck.status.results; enforces FR-7 minimums (misconfigured→error). Test:apps/hosting-operator/src/reconcile/__tests__/selfcheck.reconciler.spec.ts(new) — a run with every probe passing; one with a failing control (dependentsinconclusive, ACC-10-04); one with one sentinel fewer than the FR-7 minimum per private range, 2 platform endpoints or 1 public control, each turning its dependent itemserrorwith reasonmisconfigured(ACC-10-05); a dry-run admitting abaselinepod yieldsPRIVILEGED_POD_ADMITTED(ACC-10-10 unit half); an unsigned digest admitted yieldsUNSIGNED_IMAGE_ADMITTED(ACC-10-17 unit half); canaries are created throughwork.reconciler.ts, never a special path (ACC-10-06);src/policy/__tests__/drift.spec.ts(new) — a one-field change in a policy object yieldsPOLICY_DRIFT(ACC-10-18 unit half). Done when: the specs pass; the weakened-zone proof of ACC-10-05, 06, 08…18 is recorded inapw10-weakened-zone-drill(T22). -
T10. Image, manifests and CI. Create
apps/hosting-operator/Dockerfile(distroless Node 22, non-root, read-only root filesystem),deploy/rbac/controller.yaml,deploy/rbac/platform-role.yaml(exactly plan §3.3),deploy/rbac/prober.yaml,deploy/controller.yaml,deploy/README.md(public-safe: what the zone's GitOps must supply, no values). Create.github/workflows/hosting-operator.yml(new) — build, test, push image to the organisation registry with:sha-<full sha>and a branch alias; runs on GitHub-hosted runners. Test:apps/hosting-operator/deploy/__tests__/platform-role.spec.ts(new) — parsesdeploy/rbac/platform-role.yamland asserts its rules equal plan §3.3 exactly (no extra resource, verb or ClusterRole; ACC-10-16 static half); the workflow runspnpm --filter ever-works-hosting-operator test. Done when: the workflow publishes an image whose digest is recorded in the run summary. -
T11. kind integration job. Create
apps/hosting-operator/test/integration/tenant-template.int.spec.ts,credential-scope.int.spec.ts,sandbox-control.int.spec.ts(new) and a job in the T10 workflow that starts a kind cluster, installs CRDs + RBAC + controller, applies a fixtureWork, and asserts: tenant template objects exist; overlays present on pods; LG-04 reportsSANDBOX_KERNEL_NOT_DETECTED(the known-dirty control — kind has no sandbox runtime; ACC-10-09); LG-11 passes with no token file (ACC-10-15); LG-12 fails withCREDENTIAL_TOO_BROADwhen the test addssecrets getto the platform Role (ACC-10-16). Test: runpnpm --filter ever-works-hosting-operator test:integrationin the job. Done when: the job is green on a clean run and red when any of those three assertions is inverted.
P1.3 — Platform plugin and domain
-
T12.
ever-works-appsplugin — control client and sealing. Createpackages/plugins/ever-works-apps/(new):package.json(name@ever-works/ever-works-apps-plugin,everworks.pluginidever-works-apps, categorydeployment, capabilities["apps-tier","deployment"]),vitest.config.ts,src/index.ts,src/control-client.ts,src/seal.ts,src/apps-tier.provider.tsimplementing the P1 methods ofIAppsTierProvider(zoneInfo,setDesiredState,startSelfCheck,getSelfCheck,reviewCredentialScope,getHeartbeat,removeWork), exportEVER_WORKS_APPS_PLUGIN_ID. The client writes onlyhosting.ever.worksobjects in the control namespace. Modifydocs/plugin-system/built-in-plugins.md— one row (Constitution VIII). Test:packages/plugins/ever-works-apps/src/__tests__/control-client.spec.ts(new) — with a fakeCustomObjectsApievery call is namespaced to the control namespace and targets onlyhosting.ever.workskinds; the credential never appears in a thrown error;src/__tests__/seal.spec.ts(new) — seal/unseal round trip against T5, tamper refused, key id change forces a re-read; runpnpm --filter @ever-works/ever-works-apps-plugin test. Done when: plugin discovery listsever-works-appsdisabled by default and both specs pass. -
T13. Entities and migrations 1–2. Create in
packages/agent/src/entities/(new):apps-tier-gate-run.entity.ts,apps-tier-attestation.entity.ts,apps-tier-state-event.entity.ts,apps-tier-quarantine.entity.ts,apps-tier-abuse-signal.entity.ts,apps-tier-image-allowance.entity.tsper plan §4; Modifypackages/agent/src/entities/index.ts,packages/agent/src/database/_entity-names.ts,packages/agent/src/database/_entities-inventory.tsto register them. Createapps/api/src/migrations/1792100000000-CreateAppsTierGate.ts(seeds oneclosedstate event) and1792100100000-CreateAppsTierQuarantineAndSignals.ts(new). Test:apps/api/src/migrations/__tests__/CreateAppsTierGate.spec.ts,CreateAppsTierQuarantineAndSignals.spec.ts(new) — seed present; on SQLite the unique expression index refuses a second active quarantine for one Work; the Postgres branch's SQL contains the partial unique indexWHERE "state" IN ('requested','active','releasing');down()drops only whatup()created. Done when:packages/agent/src/database/database.module.spec.tsanddatabase.config.spec.tspass without a magic-number edit. -
T14. Gate registry and state service. Create
packages/agent/src/apps-tier/gate/launch-gate.registry.ts,packages/agent/src/apps-tier/gate/apps-tier-state.service.ts,packages/agent/src/facades/apps-tier.facade.ts,packages/agent/src/apps-tier/apps-tier.module.ts,packages/agent/src/apps-tier/index.ts(new) —evaluate()exactly as plan §5.4 with a 30 s cache,open(),close(),recordTransitions(). LG-18's title key resolves to Tenant quarantine drill. Modifypackages/agent/src/facades/facades.module.ts(FACADES) andpackages/agent/src/facades/index.ts. Test:packages/agent/src/apps-tier/__tests__/launch-gate.registry.spec.ts(new) — 25 items with kind and phase exactly spec FR-2 (ACC-10-01);apps-tier-state.evaluate.spec.ts(new) — the full matrix of plan §10.1: a heartbeat older than 120 s yieldsCONTROLLER_STALEand closed (ACC-10-19), a red run closes (ACC-10-22), a stale-only closure reopens after a green run (ACC-10-23), scopeanywithout LG-24/LG-25 passed stays closed (ACC-10-25);apps-tier-state.open.spec.ts(new) — red, 25-hour-old or missing run, expired attestation and ceiling off are each refused with every reason listed and no event written (ACC-10-20); a green 3-hour-old run with current attestations opens and records run id, actor and reason (ACC-10-21). Done when: no test can open the tier with a missing state row, a missing run, a stale heartbeat or the ceiling off. -
T15. Attestations. Create
packages/agent/src/apps-tier/gate/apps-tier-attestation.service.ts(new) — create (evidence 20–2,000 chars, ref ≤ 500), revoke,expiresAt = attestedAt + 90 d, notices at 14 d and 1 d throughpackages/agent/src/facades/email.facade.tsto everyisPlatformAdminuser. Test:packages/agent/src/apps-tier/__tests__/apps-tier-attestation.service.spec.ts(new) — boundaries 19/20 and 2,000/2,001 chars; one notice per threshold; a revoked or expired attestation makes the nextevaluate()returnATTESTATION_EXPIREDorNOT_ATTESTEDwith its item id (ACC-10-20). Done when: the spec passes and no email body contains the private evidence reference. -
T16. Self-check orchestration. Create
packages/agent/src/apps-tier/gate/apps-tier-self-check.service.ts(new) — single-flight request,execute(runId): createSelfCheck, poll 10 s ≤ 15 min, run platform-side probes (LG-12 access review, LG-15 PSL fetch 8 s, LG-16 canary TLS, LG-18 public reachability timings via the T17 service path, LG-20, LG-21, LG-23), merge, writeapps_tier_gate_runswith policy revision and controller version. Createpackages/agent/src/tasks/apps-tier-self-check-dispatcher.ts(new,Symbol()token); modifypackages/agent/src/tasks/_tasks-symbols.tsandpackages/agent/src/tasks/index.ts. Createpackages/tasks/src/tasks/trigger/apps-tier-self-check.task.ts(new; one-shot +schedules.taskcron23 */6 * * *,maxDuration20 min, retries 0); modifypackages/tasks/src/tasks/trigger/index.ts; bind the dispatcher through the job-runtime provider like every other*_DISPATCHER. Test:packages/agent/src/apps-tier/__tests__/apps-tier-self-check.service.spec.ts(new) — a second request while running returns the same run withreused: true(ACC-10-03); a finished run records outcome, reason code and duration per item, policy revision and controller version, and items still pending at 15 min areerror(ACC-10-02); a failed control keeps the gate not green (ACC-10-04);packages/agent/src/tasks/__tests__/apps-tier-dispatchers.spec.ts(new) — the dispatcher is registered and the schedule starts a run with triggerscheduleand no actor (ACC-10-07). Done when:packages/agent/src/tasks/tasks.spec.tspasses without a magic-number edit; the 15-minute budget on a real zone is recorded inapw10-p1-ship-gate(T22, ACC-10-02 manual half). -
T17. Quarantine service. Create
packages/agent/src/apps-tier/apps-tier-quarantine.service.ts(new) — request (row then synchronoussetDesiredState,202semantics), release, pause-all (batch id), release-paused (only that batch), mirror of zone timestamps and detector quarantines; Activity with owner-safe category. Modifypackages/agent/src/entities/activity-log.types.ts— append one memberAPP_TIER = 'app_tier'(Resolution R-2); every tier row uses it withactionapp.tier.quarantined,app.tier.released,app.tier.deploy_refused,app.tier.egress_thresholdorapp.tier.usage_daily. Test:packages/agent/src/apps-tier/__tests__/apps-tier-quarantine.service.spec.ts(new) — the rows of plan §10.1:setDesiredStateis awaited before the method resolves and no job is dispatched (ACC-10-34); a second request on an active quarantine →APPS_TIER_QUARANTINE_ACTIVE; release restores (ACC-10-33); Pause all creates one batch, Release all paused releases only that batch and leaves abuse quarantines (ACC-10-36); Activity rows carryactionType: 'app_tier', the category and never the operator's reason, and no owner-callable method releases (ACC-10-37); timings mirrored from zone status (ACC-10-32). Done when: the spec passes;packages/agent/src/entities/__tests__/activity-log.types.spec.tspasses with the one new member. -
T18. Gate watch task. Create
packages/agent/src/apps-tier/gate/apps-tier-gate-watch.service.ts(new) —recordTransitions(), attestation notices, stuckrequestedquarantines re-patched,runningruns older than 20 min markederror, operator email on every automatic close; each step in its own try/catch. Createpackages/tasks/src/tasks/trigger/apps-tier-gate-watch.task.ts(new, cron*/5 * * * *, 4 min budget) calling it; modifypackages/tasks/src/tasks/trigger/index.ts. Test:packages/agent/src/apps-tier/__tests__/apps-tier-gate-watch.service.spec.ts(new) — an open tier with a red scheduled run is closed by one tick and a later green run does not reopen it (ACC-10-22); a heartbeat silent 3 minutes closes it withCONTROLLER_STALE(ACC-10-19); one failing step does not abort the others;packages/tasks/src/__tests__/apps-tier-tasks.task.spec.ts(new) — ids, crons, budgets and retries of the four tier tasks. Done when: both specs pass andpnpm --filter @ever-works/trigger-tasks buildlists the task.
P1.4 — API and operator UI
-
T19. Operator API. Create
apps/api/src/apps-tier/guards/apps-tier-admin.guard.ts,apps/api/src/apps-tier/apps-tier-admin.controller.ts,apps/api/src/apps-tier/dto/apps-tier-admin.dto.ts,apps/api/src/apps-tier/apps-tier.module.ts(new) — P1 routes of plan §6.1 (status, self-checks, attestations, open/close, state events, works, quarantine, release, pause-all, release-paused); modifyapps/api/src/api.module.tsto import the module. Test:apps/api/src/apps-tier/apps-tier-admin.controller.spec.ts(new) — table-driven404for a non-admin on every route in the controller's route list (ACC-10-45); throttle metadata (self-check 6/hour, quarantine 60/min);409 APPS_TIER_OPEN_REFUSEDwith every reason (ACC-10-20);400 CONFIRMATION_MISMATCHunlessconfirmis exactlyPAUSE ALL(ACC-10-36); every other error code of plan §6.3. Done when:cd apps/api && pnpm test -- apps-tier-adminis green and the route table in the spec is built from the controller's metadata, so a route added later without a 404 case fails it. -
T20. Operator pages. Create
apps/web/src/app/[locale]/(dashboard)/admin/apps-tier/page.tsx,apps/web/src/app/[locale]/(dashboard)/admin/apps-tier/self-checks/[runId]/page.tsx,apps/web/src/app/[locale]/(dashboard)/admin/apps-tier/works/page.tsx, and inapps/web/src/components/admin/apps-tier/GateBoard.tsx,GateItemRow.tsx,OpenTierDialog.tsx,AttestDialog.tsx,SelfCheckRunDetail.tsx,TierWorksTable.tsx,QuarantineDialog.tsx,PauseAllDialog.tsx;apps/web/src/lib/api/admin-apps-tier.ts,apps/web/src/app/actions/admin/apps-tier.ts(new), followingapps/web/src/app/[locale]/(dashboard)/admin/usage/page.tsx(notFound()on error). Copy exactly spec §6.1–6.2; polling 10 s during a run or quarantine, otherwise 60 s. Test:apps/web/src/components/admin/apps-tier/GateBoard.unit.spec.tsx(25 rows with kind and phase, ACC-10-01),OpenTierDialog.unit.spec.tsx(every refusal reason rendered),QuarantineDialog.unit.spec.tsx,PauseAllDialog.unit.spec.tsx(button disabled untilPAUSE ALLtyped, ACC-10-36) (new);apps/web/e2e/admin-apps-tier-gate.spec.tsandapps/web/e2e/admin-apps-tier-quarantine.spec.ts(new) with a mocked API; runpnpm --filter ever-works-web test:e2e admin-apps-tier. Done when: both e2e specs pass and a non-admin visiting/admin/apps-tiergets the not-found page. -
T21. i18n (P1 keys). Modify
apps/web/messages/en.jsonand the 20 sibling locale files inapps/web/messages/—admin.appsTier.board.*,admin.appsTier.items.lg01Title…lg25Title(lg18Title= "Tenant quarantine drill", Resolution R-20),admin.appsTier.outcomes.*,admin.appsTier.reasons.*,admin.appsTier.quarantine.*. Leaf keys camelCase, no literal dot. No value says "kill switch", "stop flag" or "Pause everything" — those name the platform stop flag and the AW-24 workspace pause (plan §1.3). Test:apps/web/src/components/admin/apps-tier/apps-tier-messages.unit.spec.ts(new) — iterates every reason code exported by T1 and finds a message key for it in all 21 locale files; assertslg18Titleinen.jsonequals "Tenant quarantine drill" and that noadmin.appsTiervalue in any locale matches/kill switch|stop flag|pause everything/i. Done when: the spec passes andpnpm --filter ever-works-web buildreports no missing message. -
T22. P1 ship gate. (operator attestation) Root
pnpm format && pnpm lint && pnpm type-check && pnpm test && pnpm build; controller image published; with a staging zone provided by the private operations plan: run a self-check and a tenant quarantine drill on canaries; run the weakened-zone drills; record evidence in the private repository only. Modifydocs/specs/features/app-works/TRACKER.md— APW-10 P1 row (status and the gate run id, no zone detail). Test: the automated self-check run (apps-tier-self-check, triggermanual) whose run id is cited by the private artefactsapw10-p1-ship-gate(ACC-10-01…07),apw10-quarantine-drill(ACC-10-32…34, ten consecutive drills) andapw10-weakened-zone-drill(ACC-10-05, 06, 08…19). Done when: ACC-10-01…07 and ACC-10-32…37 pass on stage, the weakened-zone artefact shows each of ACC-10-08…19 turning its item Failed, and the tier is still Closed.
Phase P2 — Open for verified Blueprints (Wave 2)
Delivers spec FR-30…FR-40, FR-47…FR-51. Coordinated with APW-06 P2 (T44–T50), APW-07 P2 and APW-03.
-
T23. Promotion: copy, scan, sign; allowances. Create
apps/hosting-operator/src/promote/promotion-job.ts(new) — one Job per new digest (tools named in zone config),status.promotion[]with{ critical, criticalFixable, high }, refuse oncriticalFixable > 0unless an allowance annotation for that digest is present and unexpired; single-use pull credential ≤ 15 min. Createpackages/agent/src/apps-tier/apps-tier-image-allowance.service.ts(new, FR-31). Modifyapps/api/src/apps-tier/apps-tier-admin.controller.ts—POST image-allowances(≤ 30 days); modifypackages/plugins/ever-works-apps/src/control-client.tsto project allowances onto theWork. Test:apps/hosting-operator/src/promote/__tests__/promotion-job.spec.ts(new) — refusalIMAGE_SCAN_BLOCKEDatcriticalFixable = 1, pass at 0, pass with an unexpired allowance for that digest, refusal again once it expired (ACC-10-30);packages/agent/src/apps-tier/__tests__/apps-tier-image-allowance.service.spec.ts(new) — 30 days accepted, 31 refused, reason recorded (ACC-10-30); extendapps/api/src/apps-tier/apps-tier-admin.controller.spec.tswith the new route in the 404 table (ACC-10-45) and extendpackages/plugins/ever-works-apps/src/__tests__/control-client.spec.tswith the projection. Done when: the four specs pass and an expired allowance is never projected. -
T24 (parallel with T23).
edge-hostnamescapability. Createpackages/plugin/src/contracts/capabilities/edge-hostnames.interface.ts(new) per plan §5.6; modifypackages/plugin/src/contracts/capabilities/index.tsandpackages/plugin/src/contracts/facade-capabilities.ts(EDGE_HOSTNAMES: 'edge-hostnames'); modifypackages/plugins/cloudflare-dns/package.json(add capabilityedge-hostnames) and addpackages/plugins/cloudflare-dns/src/edge-hostnames.provider.ts(new) with its own operator credential setting (x-secret), separate from the platform zone token. Createpackages/agent/src/facades/edge-hostnames.facade.ts(new); modifypackages/agent/src/facades/facades.module.tsandpackages/agent/src/facades/index.ts. Test:packages/plugins/cloudflare-dns/src/__tests__/edge-hostnames.provider.spec.ts(new) — fake HTTP client: status mapping, idempotent delete, token never logged;packages/agent/src/facades/__tests__/edge-hostnames.facade.spec.ts(new) — a host is handed toWork.spec.hosts[]only whenstatusandcertificateStatusare bothactive(ACC-10-31; the duplicate-host half is T5'sHOST_CLAIMED, the live half is ACC-E2E-10 (b)). Done when:packages/plugins/cloudflare-dns/src/__tests__/cloudflare-dns.plugin.spec.tspasses unchanged and no plugin id string appears in the facade. -
T25. Quota profiles. Create
packages/agent/src/entities/apps-tier-quota-profile.entity.ts,packages/agent/src/entities/apps-tier-usage-window.entity.ts(new; register both inpackages/agent/src/entities/index.ts,packages/agent/src/database/_entity-names.ts,packages/agent/src/database/_entities-inventory.ts) andapps/api/src/migrations/1792100200000-CreateAppsTierQuotaAndMetering.ts(new; seedsstarterandstandardexactly as spec FR-47; addsworks.appsTierQuotaProfile). Createpackages/agent/src/apps-tier/apps-tier-quota-profile.service.ts(new; ceilings 16 CPU, 32 GiB, 100 pods, 500 GiB); routesGET/PUT quota-profiles,PUT works/:workId/quota-profileinapps/api/src/apps-tier/apps-tier-admin.controller.ts; pageapps/web/src/app/[locale]/(dashboard)/admin/apps-tier/quota-profiles/page.tsxandapps/web/src/components/admin/apps-tier/QuotaProfileEditor.tsx,ImageAllowanceDialog.tsx(new). Test:packages/agent/src/apps-tier/__tests__/apps-tier-quota-profile.service.spec.ts(new) —QUOTA_CEILING_EXCEEDEDat 16.1 CPU, 32.1 GiB, 101 pods and 501 GiB; accepted at each ceiling (ACC-10-41);apps/api/src/migrations/__tests__/CreateAppsTierQuotaAndMetering.spec.ts(new) — seeded values equal FR-47 (ACC-10-41); extendapps-tier-admin.controller.spec.ts(422body and the new routes in the 404 table, ACC-10-45);apps/web/src/components/admin/apps-tier/QuotaProfileEditor.unit.spec.tsx(new) — the save button shows the ceiling error. Done when: the specs pass and T4's quota golden files still equal the seeded profiles. -
T26. Desired-state mapper and App additions. Create
packages/plugins/ever-works-apps/src/desired-state.mapper.ts(new); modifypackages/plugins/ever-works-apps/src/apps-tier.provider.tsto implementapplyWork,getWork,setEgressThrottle,setDependencies/releaseDependencies/getDependencies(T45) and APW-06'sIDeploymentPluginApp additions (supportsApps,deployApp,getAppStatus,runAppJob,scaleApp→desiredState: pausedwithpausedReplicas,getAppLogs→pods/logthrough the platform Role,checkAppCluster→zoneInfo(),prepareAppNamespace/publishAppHosts→ no-ops returning the zone's observed values,destroyApp→removeWork({ deleteData: deleteVolumes })). The plugin writes theWorkdesired-state object only (Resolution R-5). The managed behaviour of every member is the table in plan §5.2; leaving one out makes the tier a second-class target (added, APW10-G04). Test:packages/plugins/ever-works-apps/src/__tests__/desired-state.mapper.spec.ts(new) — every §3.1 limit (ACC-10-27), non-digest image refused before any network call, unknown render-input fields dropped, generation monotonic, smoke checks andcron[].http.authSchemeand jobwhencarried through (ACC-10-53);src/__tests__/apps-tier.provider.status.spec.ts(new) — aQuarantinedphase withobservedGeneration < generationmaps to a cancelled result with reasonquarantined(ACC-10-35 plugin half), each refusal code of plan §3.1 maps to the APW-06 outcome in the plan's mapping table,scaleApp('pause')writesdesiredState: pausedwith the recorded counts andscaleApp('resume')reverses it,getAppLogsreadspods/logand answerslogs_unavailable_on_tieron a temporary refusal, anddestroyApppassesdeleteVolumesthrough unchanged; the fake API sees no write outsideworks. Done when: both specs pass and APW-06's deploy job, run against the plugin with a fake zone, records a Deployment whose state reads Cancelled — quarantined (cross-epic check with APW-06). -
T27.
AppsTierPolicyimplementation and eligibility. (Resolution R-5 — this epic owns the policy) Createpackages/agent/src/apps-tier/apps-tier-policy.impl.ts,packages/agent/src/apps-tier/apps-tier-eligibility.service.ts(new) per plan §5.5 and spec FR-35 (User.emailVerified,SubscriptionService.getActiveSubscriptioninpackages/agent/src/subscriptions/subscription.service.ts, no active abuse/security quarantine, per-person cap).isOpen()is the method consumers call;isManagedEnabled()is an alias defined only on this implementation (not on APW-06's port) that returnsisOpen()and that no consumer calls;resolveClusterCredential()returns the control-namespace credential. Modifypackages/agent/src/app-runtime/ports.ts(created by APW-06) only if it lacksisOpen()oreligibility(userId)— add them toAppsTierPolicyand to its disabled default (additive). Cross-epic PR (with APW-06 T44): bindAPPS_TIER_POLICYto this implementation; APW-06 preconditions callisOpen()andeligibility(userId); APW-06's facade hands the render input to theapps-tier-capable plugin for target Ever Works Apps. Test:packages/agent/src/apps-tier/__tests__/apps-tier-policy.impl.spec.ts(new) — with the env ceilingtrueand the tier Closed,isOpen()isfalseand deployment admission is refused while an already-ReadyWorkreceives no desired-state write (ACC-10-24); the aliasisManagedEnabled() === isOpen()in every state;apps-tier-eligibility.service.spec.ts(new) — each FR-35 condition alone yields its own reason code, in order (ACC-10-38); APW-13'sapps/web/e2e/sec-pin-app-works-managed-gate.spec.ts(ACC-NEG-03) passes with the tier Closed and the env ceiling on. Done when: no file outsidepackages/agent/src/apps-tier/andpackages/agent/src/config/readsEVER_WORKS_APPS_MANAGED_ENABLED(grep asserted inapps-tier-policy.impl.spec.ts). -
T28. Usage, metering import, receipts. Create
apps/hosting-operator/src/usage/usage-reporter.ts(new, hourlyUsageReports, canaries excluded). Createpackages/agent/src/apps-tier/apps-tier-metering.service.ts(new);packages/tasks/src/tasks/trigger/apps-tier-metering-import.task.ts(cron7 * * * *) andpackages/tasks/src/tasks/trigger/apps-tier-daily-receipts.task.ts(cron15 0 * * *) (new); modifypackages/tasks/src/tasks/trigger/index.ts. Modifypackages/agent/src/entities/plugin-usage-event.entity.ts—PluginUsageCapability.HOSTING = 'hosting';packages/agent/src/usage/credit-price-list.ts— the fivehosting.*price keys at 0. Test:packages/agent/src/apps-tier/__tests__/apps-tier-metering.service.spec.ts(new) — importing the same hour twice creates no duplicate usage row orPluginUsageEvent(ACC-10-42); one daily receipt per App Work with non-zero CPU, with quantities and no host (ACC-10-43); 80 % notification and 100 %egressThrottlefire once per month (ACC-10-44);apps/hosting-operator/src/usage/__tests__/usage-reporter.spec.ts(new) — hourly windows, canaries excluded (FR-10); extendpackages/tasks/src/__tests__/apps-tier-tasks.task.spec.tswith the two crons. Done when: the specs pass and a price list without hosting prices still produces a receipt with 0 credits. -
T29. Abuse signals and detector quarantine. Create
apps/hosting-operator/src/signals/signal-rules.ts(new) — FR-38 heuristics, runtime sensor intake adapter (sensor named in zone config), detector quarantine ≤ 60 s. Createpackages/agent/src/apps-tier/apps-tier-signals.service.ts(new); routesGET signals,POST signals/:id/dismiss|quarantine,POST works/:workId/reportsinapps/api/src/apps-tier/apps-tier-admin.controller.ts; pageapps/web/src/app/[locale]/(dashboard)/admin/apps-tier/signals/page.tsxandapps/web/src/components/admin/apps-tier/SignalsTable.tsx(new); T18's watch service imports signals. Test:apps/hosting-operator/src/signals/__tests__/signal-rules.spec.ts(new) — boundary values 89/90 %, 29/30 min, 9/10 attempts; a sustained 90 % CPU for 30 min with 10 refused mining-port attempts raises High and requests a detector quarantine in the same reconcile (ACC-10-39); 49 refused mail-port attempts raise nothing and 50 raise Medium with no quarantine (ACC-10-40);packages/agent/src/apps-tier/__tests__/apps-tier-signals.service.spec.ts(new) — dedupe byzoneName, detector quarantine mirrored within one watch tick,test: truenever quarantines a non-canary Work; extendapps-tier-admin.controller.spec.ts(new routes in the 404 table, ACC-10-45);apps/web/src/components/admin/apps-tier/SignalsTable.unit.spec.tsx(new). Done when: the specs pass and no signal summary longer than 500 characters is stored. -
T30. Owner routes, banner, eligibility copy. Create
apps/api/src/apps-tier/apps-tier-user.controller.ts(new) —GET api/me/apps-tier,GET api/works/:id/apps-tierper plan §6.2 (abuse/security/legal→review). Createapps/web/src/components/works/detail/deploy/AppsTierQuarantineBanner.tsx,apps/web/src/lib/api/apps-tier.ts(new); mount the banner in APW-06's App deploy page. Modifyapps/web/messages/en.jsonand the 20 sibling locale files —dashboard.appsTier.eligibility.*,dashboard.workDetail.appsTier.*, remainingadmin.appsTier.*keys (spec §6.3 copy). Test:apps/api/src/apps-tier/apps-tier-user.controller.spec.ts(new) — reasons never contain an operator reason code;abusemaps toreview(ACC-10-37); another account's Work →404(ACC-NEG-13);apps/web/src/components/works/detail/deploy/AppsTierQuarantineBanner.unit.spec.tsx(new) — both banner variants, no operator reason shown, no release control (ACC-10-37). Done when: both specs pass and T21's messages spec still passes with the new keys. -
T31. APW-03 availability wiring. (cross-epic PR with the APW-03 owner, Resolution R-5) Modify
packages/agent/src/apps-catalog/apps-catalog.mapper.ts(created by APW-03 T23) — makemanagedHostingAvailabilityreadAppsTierPolicy.isOpen()andmanagedScope(); the env var alone becomes the ceiling only. Test: extendpackages/agent/src/apps-catalog/__tests__/apps-catalog.mapper.spec.ts— with the env vartrueand the tier Closed, availability reportsmanagedTierDisabled; with the tier open forverified-blueprints, an unverified entry reportsblueprintNotVerified. Done when:apps-catalog.mapper.tsno longer readsEVER_WORKS_APPS_MANAGED_ENABLED. -
T32. P2 ship gate. (operator attestation) On stage with the zone from the private operations plan: all P2 attestations recorded; a green self-check; open for verified Blueprints; walk ACC-10-01…ACC-10-48 except ACC-10-25 (weakened-zone drills ACC-10-08…19 run against the staging copy per the private runbook); ACCEPTANCE.md ACC-E2E-10 (b) and ACC-NEG-03 green. Modify
docs/specs/features/app-works/ACCEPTANCE.md— the APW-10 rows (public-safe wording, sent to the program owner of that file) anddocs/specs/features/app-works/TRACKER.md— APW-10 P2 row. Test: the greenapps-tier-self-checkrun the open action cites; private artefactsapw10-p2-ship-gate,apw10-attestations,apw10-weakened-zone-drillandapw10-hygiene-drill(ACC-10-46). Done when: the owner approves opening production (spec §9 open questions on location, apex domain and prices answered).
Phase P3 — Open for all App Works (Wave 3)
Sandboxed in-zone builds (LG-24) and build limits (LG-25) — Resolution R-24.
-
T33.
AppBuildreconcile. Createapps/hosting-operator/src/reconcile/appbuild.reconciler.ts(new) — one sandboxed build workload perAppBuildon build-dedicated capacity, allow-listed egress, caps, push only tot-<id>, workload deleted ≤ 10 minutes after completion, status fields APW-05 expects (scanSummary,signatureState,blockedEgressHosts). Modifypackages/plugins/ever-works-apps/src/apps-tier.provider.ts—submitBuild,getBuild. Test:apps/hosting-operator/src/reconcile/__tests__/appbuild.reconciler.spec.ts(new) — a 60 s cap on a 120 s fixture terminates the build at 60 s and records it as cap-exceeded in status; cross-tenant push refused; the build pod carries the sandbox runtime class; extendpackages/plugins/ever-works-apps/src/__tests__/apps-tier.provider.status.spec.tswithgetBuildstatus mapping. Done when: both specs pass and a finished build workload is gone within 10 minutes on the fake clock. -
T34. Probes LG-24, LG-25. Modify
apps/hosting-operator/src/reconcile/selfcheck.reconciler.tsandpackages/agent/src/apps-tier/gate/launch-gate.registry.tsscope logic soanyrequires them. Test:apps/hosting-operator/src/reconcile/__tests__/selfcheck.reconciler.p3.spec.ts(new) —BUILD_UNSANDBOXED,BUILD_EGRESS_OPEN,BUILD_PUSH_CROSS_TENANT,BUILD_CAP_NOT_ENFORCED; extendpackages/agent/src/apps-tier/__tests__/apps-tier-state.open.spec.ts— a run missing LG-24 cannot open scopeany(ACC-10-25). Done when: both specs pass. -
T35. Scope
any. Cross-epic PRs: Modifypackages/agent/src/facades/build.facade.ts(created by APW-05; its T16 — this file used to cite its own T35 here) soapps-builderresolves onisOpen() && managedScope() === 'any'; modifypackages/agent/src/apps-catalog/apps-catalog.mapper.ts(APW-03) so the verified-only rule derives frommanagedScope(); APW-06 T51. SetEVER_WORKS_APPS_MAX_SCOPE=anyon stage only (operator configuration, not a repository file). Test: extendpackages/agent/src/apps-tier/__tests__/apps-tier-policy.impl.spec.ts—managedScope()isanyonly when the last open event isopen-anyandEVER_WORKS_APPS_MAX_SCOPE=any; stage evidence inapw10-p3-ship-gate. Done when: a provisioned (non-Blueprint) App Work deploys on stage with scopeanyand is refused with scopeverified-blueprints. -
T36. P3 ship gate. (operator attestation) ACC-10-25 plus a full re-walk of ACC-10-01…48 on stage. Modify
docs/specs/features/app-works/TRACKER.md— APW-10 P3 row. Test: the greenapps-tier-self-checkrun covering LG-24 and LG-25; private artefactapw10-p3-ship-gate. Done when: the tier is opened for all App Works on stage by an operator action citing that run.
Closing tasks
-
T37. Program documents. Modify
docs/specs/features/app-works/README.md§1 — confirm the Launch gate and Quarantine rows (already present) and add Self-check, Attestation, Quota profile, Abuse signal (spec §5.2); confirm the CONTRACTS.md rows named in the plan header match merged code. Createdocs/features/ever-works-apps.md(new) — owner-facing: eligibility, limits, quarantine and receipts; no infrastructure detail. Modifyapps/docs/sidebarsPlatform.ts. Test: runpnpm --filter ever-works-docs build. Done when: the docs build has no broken link anddocs/features/ever-works-apps.mdcontains no address, host or vendor account name. -
T38. Statuses. Modify
docs/specs/features/app-works/APW-10-apps-hosting-tier/tasks.md,plan.mdandspec.md—StatustoImplemented; confirm every item in plan §12. Test:grep -n "Status" docs/specs/features/app-works/APW-10-apps-hosting-tier/*.mdshowsImplementedon all three. Done when: TRACKER.md shows APW-10 implemented and plan §12 has no unticked item. -
T39. Delete semantics on the tier (Resolution R-15). Create
apps/hosting-operator/src/reconcile/removal.reconciler.ts(new) per plan §2.6: isolate; delete Deployments, StatefulSets (PVCs retained), Services, Ingresses, Jobs, CronJobs, NetworkPolicies other thandefault-denyandquarantine, and the env Secret; drop hosts; labelretained-until; phaseRemoved; only withspec.dataDeletionand everystatus.dependencies[]released, delete PVCs then the namespace and setstatus.removal.dataDeletedAt. Modifypackages/plugins/ever-works-apps/src/apps-tier.provider.ts—removeWork(workId, { deleteData })setsdesiredState: removedand, only whendeleteData,dataDeletion; and, added by APW10-G01, callsreleaseDependencies(workId, { deleteData })and waits for everystatus.dependencies[]entry to reportreleasedbeforedataDeletionis written — otherwise nothing ever sets the phase this reconciler waits for and ACC-10-47 can never pass. Test:apps/hosting-operator/src/reconcile/__tests__/removal.reconciler.spec.ts(new) — after removal the fake API holds no Deployment, Service, Ingress, Job, CronJob or env Secret for the Work and still holds its PVCs (ACC-10-29, ACC-10-47); withdataDeletionbut one dependency notreleased, zero PVC deletes; once all arereleased, PVCs are deleted before the namespace (ACC-10-47); withoutdataDeletionno PVC or namespace delete is ever issued; extendpackages/plugins/ever-works-apps/src/__tests__/apps-tier.provider.status.spec.ts—destroyApp({ deleteVolumes: false })writes nodataDeletion. Done when: both specs pass and a repository-wide search finds no PVC delete call outsideremoval.reconciler.ts. -
T40. Stop-flag independence (Resolution R-20). Create
packages/agent/src/apps-tier/__tests__/apps-tier-stop-independence.spec.ts(new) — no code change unless it fails. Test: that spec — (1) statically, no file underpackages/agent/src/apps-tier/importspackages/agent/src/agents/run-kill-switch.ts,agent-brake.service.tsorpackages/agent/src/safety/workspace-pause.service.ts, and none of those three importsapps-tier; (2) with a fake plugin, setting the platform stop flag, braking an Agent and pausing a workspace issue zerosetDesiredStatecalls and write zeroapps_tier_quarantinesrows while a tier App Work staysReady(ACC-10-48); runpnpm --filter @ever-works/agent test -- apps-tier-stop-independence. Done when: the spec passes ondevelop. -
T41. Redaction guard (FR-53). Create
packages/agent/src/apps-tier/__tests__/apps-tier-redaction.spec.ts(new). Test: that spec — plants a sentinel env value, sealed payload, control credential and probe address, runs self-check merge, quarantine, signals import, metering and receipts through fakes, and asserts none of the sentinels appears in any logger call, Activity metadata, controller response body or telemetry payload (ACC-10-46 automated half); the full-drill scan is the private artefactapw10-hygiene-drill(T32). Done when: the spec passes and removing one redaction in a service makes it fail. -
T42 (lands with T13 and T25). Classify new tables for workspace backup (R-25). Modify
packages/agent/src/account-transfer/backup/redaction.ts—BACKUP_DROPPED_ENTITIESgainsAppsTierGateRun,AppsTierAttestation,AppsTierStateEvent,AppsTierQuotaProfile,AppsTierQuarantine,AppsTierAbuseSignalandAppsTierImageAllowance, under one comment giving the reason: operator launch-gate, moderation and quota state that is not stored on a workspace's behalf (detector rules, operator reasons and operator ids never reach a tenant); the quarantine an owner sees already travels as the Deployment's cancelled state inworks/deployments.jsonl. Modifypackages/agent/src/account-transfer/backup/collectors/domain-specs.ts— append to therunsdomain, besideplugin-usage-events.jsonl:{ file: 'apps-tier-usage-windows.jsonl', entity: 'AppsTierUsageWindow', scope: { by: 'parent', column: 'workId', from: 'workIds' }, trim: 'pluginUsageEvents' }— metering is a record, not restorable state, and still reached through the parent Work ids. The tier's credential fingerprints live only in a zone Secret (plan §3.6), never in a platform table, so there is no row to drop;Work.appsTierQuotaProfileridesworks/works.jsonl. Test: extendpackages/agent/src/account-transfer/backup/collectors/collectors.spec.ts— the seven operator entities are inBACKUP_DROPPED_ENTITIESand referenced by no domain;AppsTierUsageWindowis referenced exactly once, inruns, scopedparentonworkIdfromworkIds, with thepluginUsageEventstrim. Done when:pnpm --filter @ever-works/agent test -- collectors redactionis green and a backup of a workspace with a tier App Work listsdata/runs/apps-tier-usage-windows.jsonland noapps_tier_*operator row.
Fix-pass additions (added 2026-09-17)
Closes APW10-G01 and GAP-22 (blockers), APW10-G02…G08 and GAP-25. T43–T46 are P2 and pair with APW-07's T36/T37; T47, T48 and T52 land with the P1 tasks they modify; T49–T51 are P2.
-
T43 (P2). In-zone dependency reconciler (APW10-G01, GAP-22). Create
apps/hosting-operator/src/dependencies/dependency.reconciler.ts(new) — readsWork.spec.dependencies[], and for each kind: creates the tenant database withbuildTenantPostgresDdl(input)frompackages/contracts/src/apps/tenant-postgres-ddl.tson the data server named in the new zone Secret (plan §3.6), creates the App Work's own Redis instance, creates its prefixed buckets with a user limited toaw-<hex12>-*and the 10 GiB quota, and obtains the mail credential from the relay's credential API (T46). Writesstatus.dependencies[]withphase∈pending|ready|failed|released,lastBackupAtand adetailthat names a reason and never a value, and schedules a backup at most every 24 hours. Modifysrc/reconcile/work.reconciler.ts(T6) to run it before reporting a tenant phase, andsrc/reconcile/removal.reconciler.ts(T39) to release rather than delete unlessspec.dataDeletionis set. Test:apps/hosting-operator/src/dependencies/__tests__/dependency.reconciler.spec.ts(new) — each kind reachesreadywith a freshlastBackupAt; an unreachable data server yieldsfailedwith a reason and no value; an unknown kind is refused; a release setsreleasedwithout deleting data and a delete happens only afterwards (ACC-10-49, ACC-10-51). The live properties (another App Work's role refused, 21st connection refused, 70-second statement cancelled at 60 s, bucket prefix, quota) are LG-14's, extended by T45. Done when: the spec passes, APW-07's T36 reads a realWork.status.dependencies[], and ACC-E2E-10 (b)'s dependency cards are green. -
T44 (P2). Dependency token substitution (APW10-G01). Create
apps/hosting-operator/src/seal/dep-token-substitution.ts(new) — after unsealing and before the tenant env Secret is written, replaces everyew-dep://<kind>/<output>token with the value T43 resolved, and refuses an unknown token or an unresolved kind with the new refusal codeDEPENDENCY_TOKEN_UNKNOWN. Modifysrc/seal/unseal.ts(call it) andsrc/crds/work.ts(the code). Test:apps/hosting-operator/src/seal/__tests__/dep-token-substitution.spec.ts(new) — a known token is replaced in a plain value and inside a template; an unknown token, an unknown kind and a missing output each refuse theWorkwithDEPENDENCY_TOKEN_UNKNOWN; no placeholder ever reaches the rendered env Secret — the assertion is on the applied Secret's bytes, not on the intermediate string (ACC-10-49, FR-55). Done when: the spec passes and a repository-wide search finds no code path that writes the env Secret before substitution. -
T45 (P2).
IAppsTierProviderdependency methods and the extended LG-14 probe (APW10-G01). Modifypackages/plugin/src/contracts/capabilities/apps-tier.interface.ts— addsetDependencies,releaseDependenciesandgetDependenciesexactly as plan §5.1; modifypackages/plugins/ever-works-apps/src/apps-tier.provider.ts(T26) to implement them by patching onlyspec.dependencieson theWork(never the whole object — that would restart the app's workloads for a dependency change). Modifyapps/hosting-operator/src/reconcile/selfcheck.reconciler.tsand the LG-14 row of plan §3.7 — the probe additionally asserts, on the canary's own database, that a 21st connection is refused (CONNECTION_LIMIT_NOT_ENFORCED) and that a 70-second statement is cancelled at 60 s (STATEMENT_TIMEOUT_NOT_ENFORCED), because APW-07 relies on this probe for ACC-07-26. Test:packages/plugins/ever-works-apps/src/__tests__/apps-tier.provider.dependencies.spec.ts(new) — the fake API sees exactly one patch tospec.dependencies, theWork'sspec.generationand every other field are untouched, andreleaseDependencieswaits forreleased; extendapps/hosting-operator/src/reconcile/__tests__/selfcheck.reconciler.spec.tswith both new LG-14 reasons. Done when: both specs pass and the live LG-14 run on stage records the two new outcomes. -
T46 (P2). Managed mail on the tier (GAP-22). Modify
apps/hosting-operator/src/dependencies/dependency.reconciler.ts(T43) andsrc/crds/work.ts—smtpjoinsWork.spec.dependencies, and its credential comes from the relay's credential API named in the zone Secret (smtpRelay.credentialApiUrl,adminSecretRef), never from a tenant-reachable mail port. Modifypackages/plugins/apps-tier-dependencies/src/managed-smtp.provider.ts(APW-07 T36) — the platform side writes{ kind: 'smtp', ref: 'dep-smtp' }and reports readiness fromstatus.dependencies[]. LG-08 is unchanged: ports 25, 465 and 587 remain blocked, and the relay is reached over its own HTTPS endpoint. Test: extenddependency.reconciler.spec.ts— declaringsmtpyields areadydependency whose outputs carry the relay endpoint, credential and from-address, and a credential-API refusal yieldsfailedwith a reason; extendpackages/plugins/hosting-operator/src/template/__tests__/tenant-template.spec.ts— the egress policy still omits 25/465/587 while the relay endpoint is reachable on 443 (ACC-10-50, and ACC-07-32 on APW-07's side). Done when: the specs pass andapp-fixture-hello'ssmtp: { required: true }reachesreadyon a stage tier, so ACC-E2E-10 (b) can pass. -
T47 (P1, lands with T4/T6). Deployment phase order, smoke checks and status (GAP-25). Modify
packages/hosting-crds/src/crds/work.ts—spec.smoke[],spec.cron[].http.authScheme,status.jobs[],status.smoke[]andstatus.deployPhaseexactly as plan §3.1; modifysrc/reconcile/work.reconciler.ts— the phases are normative and ordered: pre-deploy jobs → rollout → first-deploy jobs → in-cluster smoke → publish hosts at the edge → post-deploy jobs → CronJobs, each written tostatus.deployPhase, withstatus.smoke[]populated by the in-cluster runs and a first-deploy job never running after the first Deployment; modifysrc/render/work-to-render-input.tsandpackages/plugins/ever-works-apps/src/desired-state.mapper.ts(T26) to carry all three. Test: extendwork.reconciler.spec.ts— the phase order is asserted from the fake API call log, an in-cluster smoke failure stops before hosts are published, and no first-deploy job exists on a second Deployment; extendpackages/plugins/ever-works-apps/src/__tests__/apps-tier.provider.status.spec.ts— APW-06'shooks.onPhasesees eachstatus.deployPhasein order andstatus.smoke[]reaches its smoke result (ACC-10-53). Done when: both specs pass and APW-06's Deployment on the managed target shows job and smoke results. -
T48 (P1, lands with T4/T7/T9). Quarantine isolation that blocks (APW10-G02). Modify
src/template/tenant-template.ts— everyallow-*policy carriespodSelector.matchExpressions: [{ key: hosting.ever.works/quarantined, operator: NotIn, values: ['true'] }], anddefault-deny/quarantinekeep selecting all pods; modifysrc/reconcile/quarantine.sequencer.ts— step (1) labels every pod template, appliesquarantine, recordsstatus.quarantine.policiesSuspended[], and release re-applies the template's policies and clears the label; modifysrc/probe/net.tsand the LG-18 row of plan §3.7 — the drill's canary pod, kept alive through the window, must observe the public control and the edge path refused within 15 s (QUARANTINE_NOT_ISOLATING). Test: extendtenant-template.spec.tsandpod-overlays.spec.ts— golden files carry the selector on everyallow-*policy; extendquarantine.sequencer.spec.ts— the label, the policy andpoliciesSuspended[]are set in step (1) and cleared in reverse; extendprobe/__tests__/net.spec.ts— a reachable public control during quarantine yieldsQUARANTINE_NOT_ISOLATINGeven whennetworkIsolatedAtis fresh (ACC-10-52). Modify T11's kind job to install a CNI that enforces NetworkPolicy and to assert egress is refused there. Done when: the specs pass and the LG-18 drill on a real zone fails when the label or the selector is removed. -
T49 (P2). Custom hostnames on the tier (APW10-G06). Create
packages/agent/src/entities/apps-tier-custom-hostname.entity.ts,packages/agent/src/apps-tier/apps-tier-custom-hostname.service.tsandapps/api/src/migrations/1792100300000-CreateAppsTierCustomHostnames.ts(new) per plan §4; routesPOST /api/works/:workId/custom-hostnames,GET(with the validation record and the CNAME target) andDELETEon the user controller; aapps-tier-gate-watchstep that polls pending hostnames until both statuses areactive. Modifypackages/plugins/ever-works-apps/src/desired-state.mapper.ts—customHostnameRefreachesWork.spec.hosts[]only when the statuses areactive. Test:packages/agent/src/apps-tier/__tests__/apps-tier-custom-hostname.service.spec.ts(new) — creation stores id, status and validation; a pending hostname never reachesWork.spec.hosts[]; the owner view carries the TXT record and the fallback target and no credential; removal deletes the edge hostname and the row. Extendapps-tier-user.controller.spec.ts(another account's Work → 404, ACC-10-31). Done when: the specs pass, the new table is classified under R-25 with the other operator tables (T42), and ACC-10-56 is walked. -
T50 (P2, lands with T28). Hosting prices in the pricebook and a real debit (APW10-G07). Modify
packages/agent/src/subscriptions/billing/credit-pricebook.ts— a newVERSION_2with aneffectiveFromdate (VERSION_1 stays frozen), carryinghosting.*andrelay.messageskeys in whole billing units; modifypackages/contracts/src/billing/meter.types.ts—hostingjoinsCREDIT_PRICE_GROUPS; modifypackages/agent/src/apps-tier/apps-tier-metering.service.ts— the integer conversions (core-seconds ÷ 3600, MiB-hours ÷ 1024, MiB ÷ 1024, GiB-hours ÷ 720) with the remainder carried onAppsTierUsageWindow.carriedRemainder,PluginUsageService.recordcalled withoperationandpayer: 'platform', and the daily receipt debiting the credit ledger once with idempotency keyapps-tier:<workId>:<YYYY-MM-DD>. Modifypackages/tasks/.../apps-tier-daily-receipts.task.tsaccordingly. Test:packages/agent/src/subscriptions/billing/__tests__/credit-pricebook.spec.ts— VERSION_2 is additive, VERSION_1 is byte-identical,hostingis a valid group, and each key resolves; extendapps-tier-metering.service.spec.ts— the conversions and the carried remainder are exact, a re-run of the same day debits once, and a price list without hosting keys still records quantities with 0 credits (ACC-10-55). Done when: the specs pass and a day's receipt reconciles against the ledger's single debit. -
T51 (P2). Credits, caps and the billing quarantine (XC-12). Modify
packages/agent/src/apps-tier/apps-tier-eligibility.service.tsandapps-tier-metering.service.ts— notify the owner at 80 % and 100 % of available credits, enforce an optional per-App-Work monthly cap, and on zero credits or a lapsed subscription quarantine the App Work with category Billing after a 7-day grace period, releasing it automatically when payment resumes; modifyapps/api/src/apps-tier/apps-tier-user.controller.ts— the monthly cost estimate from the quota profile. Test: extendapps-tier-metering.service.spec.tsandapps-tier-quarantine.service.spec.ts— both notifications fire once per month, the cap refuses a scale that would exceed it, the grace period is exact, a Billing quarantine is released automatically and never by an owner (ACC-10-54). Done when: the specs pass and the owner banner shows the billing variant of plan §6.3. -
T52 (P1, lands with T6/T10). P1 promotion of the canary and controller images (APW10-G08). Create
apps/hosting-operator/canary/(new) — the canary application image (HTTPS client for LG-16, a Postgres client for LG-14, a marker writer/reader for LG-18) built by T10's workflow — andsrc/promote/canary-promotion.ts(new) performing the copy/scan/sign of the controller's own image and the canary image into each canary'st-<id>registry space at startup. Modifysrc/main.tsto run it before the first canary reconcile, andsrc/reconcile/selfcheck.reconciler.tsto report P2-only items (LG-13's promotion half, LG-16, LG-17, LG-20) asinconclusivewith reasonPHASE_NOT_ENABLEDin a P1 run. Test:apps/hosting-operator/src/promote/__tests__/canary-promotion.spec.ts(new) — both images land signed in each canary's space, admission then admits the canary pods, and a P1 run reports the P2-only itemsinconclusiverather thanpassedor skipped (ACC-10-57). Done when: a P1 self-check run and quarantine drill complete on a real zone (T22) with the probe Jobs admitted. -
T53 (P2). Hosting terms and abuse policy (EXT-18). Create
docs/specs/features/app-works/policies-draft/(new) — a hosting terms addendum, an acceptable-use policy, an abuse and copyright takedown process (intake, response targets, mapping to FR-41's quarantine categories), a trademark display policy and a names-only subprocessor delta, each marked for counsel review. Modifypackages/agent/src/apps-tier/apps-tier-eligibility.service.ts(T27) — the hosting terms join the platform's existing required-documents set (migration1785000000000-CreateTermsAcceptance) and must be accepted before an owner's first deployment to the tier, withtermsNotAcceptedadded to FR-35's reason codes. Test: extendapps-tier-eligibility.service.spec.ts— an owner who has not accepted the hosting terms is refused withtermsNotAccepted, and accepting them clears it (ACC-10-38). Done when: the drafts exist, the documents are registered, and the reason code has owner copy.
Definition of Done
- Every checkbox above is ticked.
pnpm format:check,pnpm lint,pnpm type-check,pnpm test,pnpm buildgreen; the controller's kind integration job green.- Every acceptance criterion in spec §8 (ACC-10-01…48) walked on stage, with drill evidence stored only in the private operations repository and referred to here by artefact name.
- A repository-wide search of the merged diff finds no address, internal host name, cluster or node name.
- The tier opened on production only by an operator action that cites a green self-check less than 24 hours old.