Skip to content

K8s daemon pool — implementation plan and tracking #955

Description

@zfy0701

Implementation plan and tracking issue for the k8s daemon pool: install-wide multi-org daemon members, the duty ledger, and the retirement of the per-org execution envelope. The mechanism design is docs/designs/k8s-daemon-pool.md (#954). The plan is restructured around a walking skeleton: the first milestone is an end-to-end running pool, and every later milestone upgrades a running system.

How to read this

Survey provenance. Counts, paths, and line references come from a code survey originally taken at 09d528a53 and re-verified at 51c317a6b; treat them as pointers to re-verify, not as current facts.

The ordering principle is E2E first: get one pool serving real turns as early as possible, then harden and generalize on top of a system that runs. Perfection items — contract suites, protocol replacements, chart test batteries — attach to the milestone that needs them, never earlier. Three properties hold throughout:

  1. Every milestone ships a running system. M0 runs end to end; each later milestone replaces one piece of scaffolding with the target mechanism while the rest keeps serving.
  2. Nothing lands as a mode flag. Capability knobs and injected drivers, never if (isPool).
  3. The single-org daemon keeps working the entire way. Scaffolding choices are chosen so local/self-hosted code paths are untouched.

Effort labels: S ≈ one PR, days. M ≈ a few PRs, 1–2 weeks. L ≈ a workstream, several weeks.

The trust and tenancy seams (landed)

The pool-facing identity work deliberately landed first, as target seams rather than scaffolding:

  • CP ↔ pool daemon authentication uses an audience-scoped, Pod-bound Kubernetes ServiceAccount token. TokenReview establishes the ServiceAccount subject and Pod UID; each Deployment Pod gets its own org-less daemon row and stable daemonId for that Pod lifetime.
  • The daemon ↔ CP WebSocket is install-wide. orgId is optional in the common frame envelope, but mandatory on every org-scoped frame over an install-wide connection. There is no org room, org-specific connection, or per-org socket.
  • Daemon ↔ shim authentication keeps the existing direct Kubernetes-identity mechanism. The CP does not issue a shim signing key, grant, public key, JWKS document, or key set. This settles the earlier open question outright: no CP-signed shim grant exists or is planned. Audience separation prevents a token for one hop from authenticating at another.

Why a skeleton is reachable early

Two decisions unlock it. The shared-table data plane meant multi-org members did not have to wait for Postgres — the plan allowed one SQLite file per org as scaffolding — but #958 made that moot by putting the complete cloud store on Postgres directly. Fleet identity and frame-scoped organization context land at the first application seam: each Pod authenticates with Kubernetes identity, owns one org-less daemon row, runs one CpClient, and carries orgId on org-scoped frames.

Three survey findings shape the order: dial-in is the prerequisite for tier-shared warm pools (AC_SHIM_ENDPOINT is baked into cloned templates and claim env bypasses adoption), so both land together in M3. Duty-gated ingress is nearly free at the connection layer (all platforms already reconcile per bot at runtime; the filter attaches at transportAgents()) — the real work was the third lifecycle state, since "absent from this.agents" previously meant both not mine and deleted, and the deleted path tears down workspaces; that was M2. Fleet identity is smaller than its daemonId reference count suggests — the WS handlers are daemon-fenced, not org-fenced; only 3 handlers derive org from the connection; the per-Pod identity and common orgId envelope make M4 a bounded threading change rather than a scheduler rewrite.

Milestones

graph LR
    M0[M0 · Walking skeleton<br/>multi-org member, static assignment] --> M1[M1 · Shared state<br/>async store + PG big table]
    M1 --> M2[M2 · Dynamic ownership<br/>duty groups, rendezvous]
    M0 --> M3[M3 · Dial-in, Kubernetes identity,<br/>shared warm pools]
    M0 --> M4[M4 · Frame-scoped org<br/>hardening]
    M2 --> M5[M5 · Envelope + operator<br/>DELETED, not migrated]
    M3 --> M5
    M4 --> M5
Loading

M1 and M3 are independent and can run concurrently.

M0 — Walking skeleton — complete, exit verified 2026-08-15

Goal: two orgs' agents serving real turns from one multi-tenant member, in a three-member Deployment, in the two-namespace layout. This is also the canary: everything after it upgrades a system that demonstrably works.

  • Multi-org boot: one CpClient authenticated by the Pod-bound ServiceAccount token; org-less daemon row; org-scoped frames carry orgId (frame-mode registration)

Member composition: one Pod runs one multi-org Daemon graph, not one graph per organization. Workspace and Git operations route by agent into that agent sandbox volume; workspace module settings and the member root are install-wide. Per-org workspace roots and per-org workspace-service instances are not part of this design.

  • Static assignment: agent.daemonId remains the placement seam for the skeleton's lifetime (no ledger enforcement yet)
  • Minimal chartnot a deliverable of this repository. Every application chart lives on the deployment side (this repository ships no chart for the pool), so the item as written was aimed at the wrong place. What it listed landed there instead: the sandbox→member and member-ingress rules; the runtime SandboxTemplate and warm pool rendered by the chart under release-derived names; and, last, the two-namespace layout — a dedicated agents namespace created by the cluster-admin-owned foundation release under the restricted Pod Security level with a default-deny baseline, the members' claim RBAC bound into it, template, warm pool, and shim-ingress policy rendered into it, the template hardened to pass restricted (deferred until after enforcement was verified so the two changes could not be confounded). Verified 2026-08-15: a cold turn minted its sandbox in the agents namespace, on the pinned node group, under restricted, in ~12 s. kubeconform validation of the rendered chart landed there too; a namespace-wide quota was considered and deliberately not added (see the risk register); a rendered-golden test remains optional.
  • Kubernetes hygiene that cannot wait (shared namespace): removed the orphaned drain/Sandbox watch after its producer retired, made runtime-probe claim names member-unique with bounded UID/resourceVersion-fenced garbage collection, and made the sandbox namespace explicit. fix(daemon): clean up Kubernetes runtime plane hygiene #970 S.

Accepted imperfections, named so nobody mistakes them for the design: per-member sandbox templates are a per-member object (bounded by pool size, closed by M3). Fleet identity, frame-scoped orgId, and direct Kubernetes authentication are target seams from the outset, not temporary scaffolding.

Exit: a Telegram message to org A and a webchat turn for org B are served by the same member process; a second member serves a third org; an idle agent suspends and cold-wakes.

M1 — Shared state: the Postgres store — complete

The plan originally called for an async-store refactor (repository extraction → UnitOfWork → a dedicated async freeze forcing ~145 daemon.ts methods async → a separate PG driver). #958 took a different, cheaper route: a synchronous-facade Postgres store over a worker-thread bridge (store/postgres-sync-database.ts + postgres-store-worker.js), so LocalStore's synchronous surface survives unchanged and the async freeze never happens. As landed:

Exit (met, verified across the test-environment rollouts of 2026-08-15/16): a member restart loses no org state; no per-org SQLite files exist to retire.

M2 — Dynamic ownership: duty groups and the rendezvous — complete; enforced, unconditional, verified on a live rollout

M3 — Dial-in, Kubernetes identity, tier-shared warm pools — complete, minus two checks deferred with per-tier pools

The dial-in half has landed (shim/dialer.ts dials, shim/server.ts listens in the sandbox, generation-fenced; K8sDriverDeps.awaitChannel kept its signature, so the driver's tests stand). Authentication is asymmetric by design (design §7): the shim presents its audience-restricted projected ServiceAccount token and the dialing daemon TokenReviews it (implemented — the dialer's PodIdentityVerifier, including the presented-identity-vs-dialed-pod check); toward the shim the pre-disclosure boundary is the sandbox-namespace NetworkPolicy (one ingress rule: pool namespace → shim port), a single active connection, pre-auth frame-size caps, term fencing, and audience separation — an earlier revision's CP-signed grant / JWKS distribution was removed outright, so there is deliberately no shim-side TokenReview to build. Status:

  • Deprecated: The direct Kubernetes-identity handshake in both directions. Legacy shim→daemon compatibility and the bidirectional compatibility suite are out of scope; M3 retains only the daemon→shim Kubernetes-identity handshake.
  • The NetworkPolicy flip: the single coarse pool→shim ingress rule replaces sandbox→member egress allowances
  • The warm pool is a release-level chart object shared by every member (the per-member-template coupling retired with the chart consolidation; warmPoolRef adoption is live). The per-tier dimension stays skipped until agent resource tiers exist at all — warm pods' resources are fixed at creation, so tiers force per-tier pools, and none of that machinery is worth building for today's single size.
  • The Kubernetes-identity handshake suitetest(daemon): cover the dial-in Kubernetes-identity handshake invariants #1096: the §7 invariants are pinned on the daemon→shim path (identity presented or the dial fails, TokenReview refusal, presented-identity-vs-dialed-pod, audience assertion, single active connection with its queued slot, the fence rules, and the size cap in the direction that is safe today), each new case mutation-checked. It also found that one pre-existing test was passing on its own 1 ms dial deadline rather than on the pod check, and produced two findings: the accept-side WebSocket paths register no 'error' listener, so an oversized frame from an unauthenticated peer takes the shim process down (fix in flight; the shim-side half of the size-cap invariant lands with it), and §7's term is in fact enforced as the generation, with supersession at the dialer rather than at the shim — the design/implementation drift was Dial-in handshake: §7's term fencing is implemented as generation fencing, with supersession at the dialer #1097, closed by docs(designs): the dial-in handshake fences on the generation, not the duty term #1110: §7 now states the shipped shape — bind() carries the generation rules, the listener refuses a second dial outright, supersession happens at the dialer — and records the lingering-socket takeover delay as an accepted trade-off, covered by the duty gate and row ownership rather than by the generation.
  • Two empirical checks, deferred — only relevant if per-tier shared adoption is ever revisited: whether additionalPodMetadata.labels reach an adopted warm pod, and that an adopted pod never returns to the pool (with tier-shared pools that turns from waste into a cross-tenant leak)

Exit (met): the daemon→shim path passes the Kubernetes-identity handshake suite (#1096). (The original "wake adopts from a shared warm pool" criterion is retired as already met in substance: the warm pool is a release-level shared object and adoption is live; per-tier pools wait on resource tiers existing.) Legacy shim→daemon compatibility is deprecated and out of scope.

M4 — Frame-scoped organization hardening — M (needs M0) — complete: lint fence, frame contract, and the reconnect snapshot all landed

The fleet link already exists — every Deployment Pod gets one org-less daemon row resolved from (ServiceAccount subject, Pod UID) and one install-wide WebSocket; a container restart in the same Pod reuses the row, a replacement Pod gets a new daemonId, and Pod IP is metadata, not identity. M4 completes org-threading across the remaining handlers and registries:

  • A frame-mode connection requires orgId on every org-scoped request, event, control frame, and correlated reply; both peers validate the frame org against the targeted resource — feat(protocol): require and validate the frame organization on every org-scoped frame and reply (#955 M4) #1089. One classification of every frame type now lives in protocol/frame-scope.ts (install-wide: auth/register/bootstrap, heartbeat, capabilities, facts/*, relay roster, collaboration routes, drain/restart/upgrade/config-push, duty/* except duty/fetch, agent/exists; org-scoped: everything else including typed replies) and both peers run the same two checks at their decode edge: an org is required on org-scoped and forbidden on install-wide frames, a correlated reply must carry exactly its request's org (a mismatch fails the pending request with SCOPE_DENIED and applies nothing), and uncorrelated ack/error are dropped instead of answered. Gaps it closed on the way: the two peers kept divergent lists; replies bypassed the gate on both sides; hook/report and hook/start were unfenced; stray errors could ping-pong SCOPE_DENIED; the daemon's refusals were unlogged and cron/* controls unscoped.
  • Reconnect state as a combined multi-org snapshot or revision-fenced stream on the same member connection — never subscribe(org), an org room, an org-specific socket, or an org-specific data-plane schema. Do not invent the watermark machinery: Agent.configRevision with the daemon-side stale|conflict|idempotent|apply compare and SessionMeta.visibilityRev/visibilityAckedRev + replay-on-register already exist in production. Closed by the survey + proof in test(control-plane): prove reconnect as a combined multi-org snapshot (#955 M4) #1095: the mechanism already shipped as three pieces on one wire — register/ok is the combined install-wide snapshot (union roster pinned ∪ duty-held across every served org, per-entry orgs, revision-stamped specs applied through the daemon compare, ownership-aware drops), the register-time visibility replay converges visibilityRev per org on org-scoped session/visibility/snapshot frames, and the first beat's duty exchange re-issues missing/stale-term grants revision-stamped. test/protocol/multi-org-reconnect.test.ts pins the end-to-end property (two orgs mutated while a member is away converge through one reconnect); the tests flushed out no production gap.
  • Extend the *Unscoped lint fence to src/ws/**; give the 3 org-deriving handlers explicit arguments — chore(control-plane): fence the daemon WS surface on the frame org #1066 (frameOrgId(frame, conn) is the one place the org is resolved; a dozen handlers now fence their reads on it; three self-row reads and duty/claim allow-listed with a justification). One of them showed up as a live symptom (Pool member: organization suggestion sync fails — handler derives org from an org-less daemon #968, knowledge/suggestions/sync on an org-less member) and is closed by fix(knowledge): scope the organization-suggestion replay to the org that owns it #996; the four org-knowledge read handlers in the same file carry the same agent.daemonId blind spot and are Org-knowledge read handlers still fence on agent.daemonId, so a pool member's agent cannot search or read org knowledge #999. After feat(duty): make the pool heal itself by making placement a target #991 this class recurs wherever a handler still authorizes on agent.daemonId — the fix is always the placement resolver, never a per-handler special case.

Exit: one WS per Pod member; two orgs' scoped frames share it without cross-tenant reads or writes.

M5 — Per-org envelope retirement — done, and it was a deletion, not a migration

M5 was planned as a migration: a durable-state importer, workspace accounting, a rehearsed rollback, then the operator stops. None of that was built, because there was nothing to migrate. No production or staging envelope ever existed; the only consumers were disposable test organizations. A migration path would have been machinery written for an empty set, so the milestone collapsed to deleting the model outright (2026-08-14).

Items the migration plan owned that simply ceased to exist: the durable-state importer, workspace accounting before cutover, the rollback order, the drain-annotation sweep, and named owners for orphaned operator behaviors (suspend quiescing, org offboarding). They are recorded here as not done and not needed rather than dropped silently.

One ordering mistake worth keeping. The cluster objects were deleted before the control plane that writes them, so between the two changes the deployed CP logged a 404 envelope re-apply on every maintenance tick for the four disposable orgs. Noise rather than damage, and it could not be quieted by turning the flag off (see the decoupling item above) — but the consumer should have been retired before the API it consumes.

Daemon groups — designed, first half in flight

The pool is the degenerate case of a member set — a named set of daemons within which an agent's duty may be claimed. Every prerequisite the pool design listed for generalizing it landed with M2, so the "future direction" became a design: docs/designs/daemon-groups.md (#994, six review rounds). One concept, member_set(id, orgId NULL-able, name), where null means cross-org (the pool — one per install, never per org) and an orgId means one organization's set of its own daemons. Tenancy lives in three write-time invariants (which daemons a set may contain; which set an agent may reference), so the read path is a single rule — claimant ∈ agent's set — and the placement resolver ends with fewer branches than it has today. daemon stays its own kind (a pinned machine has no replaceability; it joins a set, it does not become one). Membership changes are two-phase and generation-fenced in the drain/move order — stop and confirm the old authority, then commit — and enrolling a pinned machine is the existing agent move applied N times inside one fence.

Member-replacement audit (2026-08-16) — closed out

After #1017/#1019/#1023/#987 all turned out to be one class — code assuming one daemon owns an agent, sandbox, or session for its lifetime — a systematic audit of per-member state and agent.daemonId reads produced #1025#1041 (three mechanisms: per-member memory nobody re-derives on a duty move; control-plane authorization/targeting on agent.daemonId instead of the placement resolver; tables that were per-process under SQLite and shared by accident on the pool store). All seventeen are closed: #1042 (orchestration deadlines duty-gated + CAS), #1044 (hook-completion outbox owned per member), #1045 (sandbox launches duty-scoped, re-derived on takeover), #1046 (boot recovery scoped to owned rows), #1047/#1061 (hooks and PR review through the placement resolver, completion accepted from the serving member), #1048 (relay actions through the rendezvous), #1049/#1064 (inbox backlog replayed on every duty gain, kept on handoff), #1053 (missed cron/dream fires compensated on handover, definition-fenced), #1054 (memory-capture gate keyed by agent), #1055 (the agent.daemonId sweep for 409s/dropped reports/visibility replay), #1057 (multi-agent webchat + delegated MCP placement), #1058 (model catalog keyed by member), #1063 + #1075 (the low-severity batch, transcript tables org-fenced), #1065 (session TTL/GC sweeps holder-only, purge receipts leased), #1068 (session-metadata outbox owned per member, parked not failed), #1069 (loop guard: atomic counters, member-scoped trip). Follow-ups it produced: #1050 (done, #1064), #1051 (done, #1061), #1073 (done, #1080 — the store suites now run on real Postgres in CI, which immediately caught NUL-joined activation keys that Postgres rejects), #1078 (done, #1081), #1062 (reconciler, done; the store half — orphan rows whose agent is gone — landed as #1085, which also folded the seven scattered prune/expire routines into one declarative retention rule table).

What moved off the critical path

Stated explicitly so it is not mistaken for omission: the store contract suite gates M1, not M0. Chart golden tests start minimal in M0 and grow with M3. Version-aware placement exists nowhere (there is no placement). The *Unscoped fence, full explicit-org hygiene, and the reconnect-snapshot proof gated M4 and have all landed (#1066, #1089, #1095). The connection-pool org-dimension fix (two orgs pasting the same bot token collide on the pool keys — copy the relay's tenant-fence) is required before untrusted tenants share a member, i.e. M5, not M0. And the canary is not a phase: M0 is the canary.

Remaining decisions and checks

Ownership assignments once blocking M5 (suspend quiescing, org offboarding) are moot — those behaviors were deleted with the operator, not reassigned. Empirical checks blocking M3: adoption-label propagation, adopted-pod-never-returns. Authentication is settled: CP ↔ daemon and daemon ↔ shim use direct Kubernetes identity; pool members are per-Pod org-less records; org context travels on the common frame; no CP-signed shim grant or public-key distribution exists. Everything else previously listed as a gate is settled in the design document (isolation unit, cron authority — holder-fired as an ingress edge, quota deferral, term/sessionEpoch independence, the duty-group ledger shape).

Risk register

Risk Milestone Mitigation
Skeleton org state dies with its member (SQLite on member disk) M0 Closed by #958: cloud daemons have no SQLite; durable state is in shared Postgres
Two orgs sharing a bot token collapse onto one connection M5 gate Org dimension on the pool keys; the relay's tenant-fence fix is the template
Duty release runs the removal path and destroys a workspace M2 ✅ The release lifecycle state landed in #948 with a test that release preserves the workspace
Partitioned ex-holder serves a group a successor has claimed M2 The daemon self-fence (above) — the open blocker for the enforcement flip
dispatch() admission race / ACP update reordering after the async flip M1 Moot: #958's sync-over-worker bridge keeps the store surface synchronous, so the async flip never happens
Long-lived async branch vs store churn M1 Moot for the same reason — no async branch exists
A term derived from sessionEpoch churns every duty on each CP deploy M2 ✅ Independent fencing domains; covered by tests in #939
Missing org predicate in a repository (row-tenancy's classic leak) M1 Predicate injected in exactly one layer; contract-suite coverage; RLS as optional enforcement
One member suspends another org's sandboxes M0 Client-side watch filtering via the claim→Sandbox join
Stranded drain annotation leaves a sandbox permanently un-wakeable M5 Moot: the envelope model was deleted, never migrated — no drain annotations exist to strand
Pod killed mid-removal strands a deleted agent's sandbox volume M1 Still open. #962 diagnosed it and proposed an install-wide agent_removal_obligation row unioned with the FS mirrors at boot, but was closed unmerged; its review also found the approach incomplete, since a store-admission failure exits the process after the CP row is already deleted. The branch survives as a starting point
Stale staged-move fence dark-holds an agent after pod replacement M5 gate Scope-key decision from the #962 inventory before pool members carry production agents
Shim listener pre-disclosure M3 Direct Kubernetes identity authenticates the daemon before disclosure; rate/size limits before authentication; projected-token rotation
Data-plane PG availability becomes pool availability live since #958 Accepted; HA posture ≥ the CP's — no longer hypothetical: every cloud daemon hard-depends on the data-plane Postgres
No per-org resource enforcement Namespace-wide quota + member duty budgets; a dedicated daemon as the escape hatch

Issues spawned from this plan

Work that earned its own issue rather than a checkbox here, each because it is a
distinct mechanism with its own gate:

Issue What Gate
#804 / #815 The original k8s-daemon umbrella (D0–D10: remote spawn, shim, workspace-over-shim, sandbox lifecycle, k8s-as-supervisor) — complete, and it assumed one daemon per org, which #964 retired Closed; this issue is its successor
#950 Three enforcement-path findings the reviewer could not submit before #948 merged: duty changes not converging platform connections, agentsLost ignored on replacement, drain not closing connections Closed by #976 and #977 — two of the three were rediscovered independently before the cross-check
#973 Updates and the reconnect roster must follow the duty holder Closed by #978
#975 Duty admission ordering: a late admission resurrecting a withdrawn duty; a refused replacement keeping departed members Closed by #976 and #977
#979 MCP proxy and external-memory definitions did not follow the duty holder Closed by #989 — the duty/fetch bundle carries both, the roster scopes them by the same union as agents; and it found that every MCP reader picked the retiring grant during rotation ([0] on an ascending query), now one currentMcpGrant selector plus an issuedAt fence so a bundle can never regress a fresh key
#987 Webchat could not reach a pool member after a rollout Delivery/readiness by #991; the install-window residual by #1060 (not_ready, pending directory entry, bounded retry). Webchat continuation of a session recorded on a retired member is a content-ownership question handled in #1019
#982 Retire the dutyEnforcement flag itself Closed by #995
#1001 Rename the "cloud daemon" code identifiers to the pool / member-set vocabulary; identity/storage contract names (the pool ServiceAccount, the store schema) deliberately excluded Closed by #1008 (code) and #1012 (POOL_NAMESPACE, a hard rename — no compatibility alias, per the same reasoning as #971)
#1000 Daemon groups, second half: org-scoped member sets (design §6 PR 2) After PR 1 has merged and run
#999 The four org-knowledge read handlers still authorize on agent.daemonId, so a pool member's agent cannot search or read org knowledge Closed by #1004: authorized through the serving member via the placement resolver
#1010 A shim dial that timed out during a sandbox cold start revoked the binding and left the ACP host closed after the retry bound Closed by #1011: the loss window starts when the sandbox pod is up, bounded by the pod-up timeout; verified as a single-generation cold start on test
#1007 The driver reuses an existing SandboxClaim regardless of its warmPoolRef, so a renamed warm pool never migrates existing agents Closed as not needed: a one-time transition on an existing environment, handled by hand; a fresh install never sees it
#1009 Flaky k8s-client watch unit test Closed
#1016 Daemon-pool rolling update: surge-then-drain, capacity never dips, each agent moves at most once Closed: #1021 + #1022 (application), deployment side on maxSurge: 100%, a long grace period, a Helm timeout above the drain budget, AC_POD_TEMPLATE_HASH; two live rollouts verified. Readiness follow-up #1043 closed by #1056
#1023 Pool members retried an event/session-sync outbox row forever when its organization could not be resolved Closed by #1068
#1024 Flaky duty-lease.handler renewal test Closed by #1052
#1025#1041 The member-replacement audit (see the section above) All closed
#1050 Graceful fence purged the agent's shared inbox instead of handing it over Closed by #1064
#1051 Hook completion from the member that now serves the agent was refused Closed by #1061
#1062 Orphan reconciler Closed by #1074/#1079: one-shot reconcile --once CronJob (observer registration, no lease), dry-run by default
#1070 Console: files/memory of a not-running sandbox Files half by #1077 (POST /agents/:id/wake, wake when the tab is active); memory half by #1081 — verified on the test environment at rc.150 (tab wakes the sandbox, a console write lands on the agent's volume)
#1073 Store suites on real Postgres in CI Closed by #1080 (also fixed NUL-joined activation keys the pool rejected)
#1078 Pool agents' managed memory lived on the member's ephemeral state root Closed by #1081 (option A, memory on the agent's sandbox volume; the B/"one home" analysis is recorded on the issue)
#1085 Store-side orphan reaper + retention rule table Merged: reconcile --once also reaps store rows whose agent no longer exists (dry-run behind AC_STORE_ORPHAN_DELETE); the daemon's hourly tick runs the same rules age-only, replacing seven hand-written prune routines (net −93 lines)
#1018 / #1020 Session content reads for pool sessions Reverted at the owner's request; #1019 owns it

Deployment-side counterparts live on the deployment side: the Cloud Daemon
was missing two environment settings the runtime plane made mandatory, which had
been failing every deploy and pinning that environment several releases back, and
the enforcement switch needed plumbing before it could be set at all.

Known issues

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions