Appearance
PlexdHook Managed Push — opt-in, per-Domain authoritative writes of PlexdHook CRDs
This document is the authoritative bounded-context reference for the opt-in PlexdHook managed-push surface — the inversion of the discovery-only PlexdHook default. Where discovery records what a plexd agent observed in its customer cluster and never holds kubectl credentials, managed push lets a Domain explicitly opt in to having plexsphere become an authoritative writer of PlexdHook custom resources into that customer cluster. It covers the ubiquitous-language pins, the fail-closed opt-in model, the six HTTP operations, the exhaustive kubeconfig validation catalog, the AES-256-GCM sealing envelope and the wrap-key contract, the at-rest sealed-bytea persistence, the Server-Side-Apply authority model that distinguishes GitOps ownership from managed writes, the one-shot rollback semantics, the closed Problem-code taxonomy, the orthogonal relationship to the trust-on-first-use hook-integrity gate, and the four-aspect test map.
Managed push is fail-closed and off by default. A Domain holds no managed-push state until an operator attaches a customer-cluster kubeconfig, and even an attached credential refuses to write until its opt-in flag is enabled. The whole surface stays a 501 scaffold on any deployment that has not provisioned the sealing wrap key. This is the opposite posture from discovery: discovery is always-on and read-only; managed push is opt-in and authoritative.
The domain model lives in the k8s-aware policy bounded context (../../../internal/policy/hooks/push); the HTTP surface lives in a transport package that holds none of the policy module's k8s, sealer, or persistence concerns (../../../internal/transport/http/v1/managedpush); the two are bridged at the composition root (../../../cmd/plexsphere/managed_push_factory_prod.go). The wire contract is pinned in ../../../api/openapi/plexsphere-v1.yaml.
Ubiquitous language
The terms below travel verbatim across the domain value objects, the transport command/view types, the wire schema, and the persistence tables. Internal code never paraphrases them.
| Term | Definition | Code anchor |
|---|---|---|
| Target | The per-Domain managed-push opt-in record: the sealed customer-cluster kubeconfig, the Enabled flag, and the display metadata (KubeconfigSHA256, APIServerURL, timestamps) the read surface echoes. One Target per Domain. The plaintext kubeconfig NEVER lives on a Target — only the sealed KubeconfigCiphertext bytes and the hex fingerprint. | ../../../internal/policy/hooks/push/target.go (Target) |
Opt-in (Enabled) | The fail-closed flag. A Target with Enabled=false has a credential attached but refuses pushes. Target.CanPush is the single seat that enforces "never write into a customer cluster until the Domain opts in". | ../../../internal/policy/hooks/push/target.go (CanPush) |
| Push (Record) | One recorded server-side apply of a PlexdHook into the customer cluster, plus the captured prior cluster state that makes the apply reversible. The id is an app-minted UUIDv7 so the record key is time-ordered and stable across the audit row and the read URL. | ../../../internal/policy/hooks/push/record.go (Record) |
| Status | The closed lifecycle of a Record: applied then optionally a one-shot transition to rolled_back. The DB CHECK mirrors this set. | ../../../internal/policy/hooks/push/record.go (Status, StatusApplied, StatusRolledBack) |
| Prior state | The full JSON of the cluster object as it existed immediately before the apply, or nil when the object did not exist. Drives both the had_prior_state read field and the rollback strategy. NEVER echoed on the wire or on an audit row. | ../../../internal/policy/hooks/push/record.go (PriorState, HadPriorState) |
| Request | A validated push-time command: a DNS-1123 namespace plus the five-field PlexdHookSpec, constructed only through NewRequest, so a value always carries Kubernetes-valid names and a canonical image digest. | ../../../internal/policy/hooks/push/request.go (Request, NewRequest) |
| Sealer / sealed envelope | The AES-256-GCM at-rest cipher for the kubeconfig. The envelope is nonce ‖ ciphertext+tag; the Domain UUID bytes are bound as GCM additional authenticated data so a ciphertext sealed for one Domain cannot be opened under another's. | ../../../internal/policy/hooks/push/sealer.go (Sealer, Seal, Open) |
| Wrap key | The 32-byte AES-256 key the Sealer seals under, sourced from the PLEXSPHERE_MANAGED_PUSH_WRAP_KEY_B64 env var. Its presence is the feature's opt-in gate. | ../../../cmd/plexsphere/managed_push_factory_prod.go (productionManagedPushConfigFromEnv) |
| PlexdHook | The custom-resource kind written into the cluster (group/version/kind hooks.plexd.io / v1alpha1 / PlexdHook), shared with the discovery projection's single PlexdHookGVK declaration. | ../../../internal/policy/hooks/plexdhook.go (PlexdHookGVK, RenderPlexdHook) |
| FieldManager | The pinned Server-Side-Apply field manager plexsphere-managed-push. A stable manager is what lets the apiserver detect a competing owner and return a conflict instead of silently overwriting another controller's fields. | ../../../internal/policy/hooks/push/kube/kube.go (fieldManager) |
| Discovery-only vs managed authority | The two postures of the catalogue. Discovery-only is the always-on default (plexsphere mirrors what the agent observed, no kubectl credentials). Managed authority is the opt-in inversion (plexsphere holds the credential and writes the hook). | ../policy/hooks.md / this document |
The translation is one-directional at attach time (operator supplies a kubeconfig; plexsphere validates, seals, and stores it) and authoritative at push time (plexsphere opens the sealed credential and applies the rendered PlexdHook into the customer cluster).
Opt-in model
Managed push is fail-closed at three independent layers, so a misconfiguration or an un-opted-in Domain can never produce a write into a customer cluster:
- Build gate (the wrap key). The surface is wired only when
PLEXSPHERE_MANAGED_PUSH_WRAP_KEY_B64is set. An unset key leaves the six handlers on their 501managed_push_not_provisionedscaffold. The composition root deliberately gates on the wrap key rather than the sharedPLEXSPHERE_DSN, so a Postgres-wired deployment does not stand managed push up automatically — a deployment must explicitly opt the whole feature in by provisioning the sealing key. - Per-Domain credential gate. Even when the feature is wired, a Domain holds no managed-push state until an operator attaches a kubeconfig. An absent Target (a zero value, signalled by empty ciphertext bytes) yields
ErrNotConfigured. - Per-Domain enable gate. An attached Target with
Enabled=falserefuses pushes withErrDisabled.Target.CanPushis the single, pure, value-receiver decision the application service consults before any cluster work; a zero-value Target can therefore never return a permit.
The push application service enforces the credential and enable gates before it validates the request spec: a Domain that has not opted in learns it is disabled regardless of payload shape, so a spec error never leaks that the credential gate would otherwise have been reached. Rollback deliberately does not consult CanPush — disabling the opt-in is precisely the moment an operator wants to undo what was pushed, so an already-applied hook must remain reversible after the Domain is disabled (only the credential must still be present).
HTTP surface
The surface is six operations under the hooks OpenAPI tag, all scoped to a Domain. The two reads gate on the Domain read permission; the four mutations gate on Domain manage. The ReBAC check runs before any persistence or cluster work, and before the request body is decoded — an unauthorised caller never reaches the JSON decoder, the sealer, or the cluster, and the endpoint cannot be used as a Domain-id existence oracle.
| Method | Path | operationId | ReBAC permission | Audit relation | Body cap |
|---|---|---|---|---|---|
| GET | /v1/domains/{domainId}/managed-push | GetManagedPush | domain read | managed_push.target.read | n/a |
| PUT | /v1/domains/{domainId}/managed-push | PutManagedPush | domain manage | managed_push.attach | 64 KiB |
| DELETE | /v1/domains/{domainId}/managed-push | DeleteManagedPush | domain manage | managed_push.detach | n/a |
| POST | /v1/domains/{domainId}/managed-push/hooks | PushManagedHook | domain manage | managed_push.push | 64 KiB |
| GET | /v1/domains/{domainId}/managed-push/hooks/{pushId} | GetManagedHookPush | domain read | managed_push.push.read | n/a |
| POST | /v1/domains/{domainId}/managed-push/hooks/{pushId}:rollback | RollbackManagedHookPush | domain manage | managed_push.rollback | n/a |
Both bounded operations cap the request body at 64 KiB (MaxManagedPushRequestBodyBytes). The attach (PUT) body carries a base64-encoded kubeconfig — a real one is a few KiB, so 64 KiB leaves generous headroom while refusing the unbounded-body DoS vector. The push (POST) body is a five-field spec and shares the same cap.
Success bodies are display-only. A read or a mutation response never echoes the kubeconfig (neither the plaintext nor the sealed ciphertext) and never echoes the captured prior cluster object. The target response carries only enabled, api_server_url, kubeconfig_sha256, and timestamps; the push response carries the had_prior_state boolean in place of the prior object content. The omission is structural — the transport view types (../../../internal/transport/http/v1/managedpush/wiring.go, TargetView / RecordView) have no field for the credential or the prior object, so the credential cannot leak even by accident.
Kubeconfig validation catalog
At attach time ValidateKubeconfig (../../../internal/policy/hooks/push/kubeconfig.go) runs a static, structural validation pipeline over the supplied kubeconfig. Every rejection arm wraps the domain ErrKubeconfigInvalid sentinel, which the transport surfaces as 422 kubeconfig_invalid. There is no liveness probe of the cluster at attach time — see the authority note below. The validation runs in order:
| # | Rejection arm | Rule |
|---|---|---|
| 1 | Unparseable document | clientcmd.Load must accept the bytes as a kubeconfig document. |
| 2 | Current-context not set | CurrentContext must be non-empty. |
| 3 | Current-context does not resolve | the named context, its cluster, and its authinfo must all resolve to existing entries. |
| 4 | insecure-skip-tls-verify set | the current-context cluster must not set insecure-skip-tls-verify. |
| 5 | Non-https API-server scheme | the cluster server must parse to an https URL with a non-empty host. |
| 6 | Cluster certificate-authority file path | the cluster must NOT set a certificate-authority file path; supply the CA inline as certificate-authority-data. |
| 7 | Cluster proxy-url set | the cluster must NOT set a proxy-url — an operator-supplied proxy is an SSRF vector for the apiserver dial. |
| 8 | Exec credential plugin present | the authinfo must NOT declare an AuthInfo.Exec credential plugin. |
| 9 | AuthProvider present | the authinfo must NOT declare an AuthInfo.AuthProvider. |
| 10 | tokenFile set | the authinfo must NOT set tokenFile; supply the token inline as token. |
| 11 | client-certificate file path | the authinfo must NOT set a client-certificate file path; supply it inline as client-certificate-data. |
| 12 | client-key file path | the authinfo must NOT set a client-key file path; supply it inline as client-key-data. |
| 13 | No usable rest.Config | the document must materialise a rest.Config through clientcmd ClientConfig, proving it is internally coherent. |
Arms 6–12 are the security pivot of the whole feature, and the contract reduces to one rule: managed push accepts only embedded, in-band credentials — a token, or client-certificate-data / client-key-data, with the CA supplied inline as certificate-authority-data. A kubeconfig is operator-supplied data that plexsphere later loads to build a client, so every out-of-process, file-path, and proxy mechanism is refused:
- Exec / AuthProvider (arms 8–9). An
Execcredential plugin instructs client-go to run an external binary inside the plexsphere process to mint credentials, and anAuthProviderplugs in a comparable out-of-process hook. Accepting either would be a remote-code-execution primitive on the control plane. - File-path credentials (arms 6, 10, 11, 12). client-go resolves
tokenFile,client-certificate,client-key, and the clustercertificate-authorityby reading the named path off the control-plane filesystem — the token file duringClientConfigat attach, the cert / CA paths at push time. Accepting any of them would let a Domain operator who holdsmanageon their own Domain make the control plane read an arbitrary local file (a service-account token, another tenant's on-disk secret) and exfiltrate it as a bearer credential to a cluster they control — a local-file-disclosure primitive. proxy-url(arm 7). An operator-supplied proxy would route the apiserver connection through a host the operator controls — a server-side request forgery vector.
There is no safe sandbox for an operator-controlled binary running with the control plane's privileges, and the control plane has no business reading operator-named local paths or dialling an operator-supplied proxy, so "accept exec but sandbox it" — and the file-path / proxy siblings — are refused outright rather than mitigated. The customer embeds the data form instead.
On success the validator returns the cluster server URL as KubeconfigInfo.APIServerURL, the display value the read surface echoes so an operator can confirm which cluster a Target points at without decrypting anything.
Sealing envelope and wrap-key contract
The kubeconfig is sealed at rest with AES-256-GCM (../../../internal/policy/hooks/push/sealer.go):
- The envelope layout is a 12-byte random nonce, then the ciphertext, then the 16-byte GCM tag (
nonce ‖ ciphertext+tag), matching the NSK software-wrapper layout so a reader who knows one adapter recognises the other. A fresh nonce is drawn per seal fromcrypto/rand. - The key is exactly 32 bytes.
NewSealerrejects any other length withErrSealerMisconfiguredat construction (boot) rather than on the first attach. - The Domain UUID bytes are bound as the GCM additional authenticated data (AAD). A ciphertext sealed for Domain A opens under Domain A's 16 UUID bytes and fails the GCM tag check under any other Domain's AAD. A ciphertext copied between rows — a confused-deputy or row-swap attempt — therefore fails to
Openat zero extra cost.
The AAD binding is a deliberate improvement over the NSK precedent. The NSK Unwrapper omits an AAD because its rows are keyed by a possession-of-wrap-key model where a rotation would otherwise force a full re-wrap; managed push has the stronger, cheap-to-enforce invariant that a ciphertext belongs to exactly one Domain row, so binding the Domain UUID as AAD costs nothing and closes the row-swap gap. Reusing the identity context's NSK wrap-key internals was rejected for a second reason: the policy bounded context must not import the identity context (the cross-context depguard rule), and the NSK adapter carries heartbeat-specific concerns (a dev-only boot warning, a kid/version tolerance posture, a fixed 32-byte plaintext size) that are irrelevant to a variable-length kubeconfig. The Sealer is therefore implemented locally with stdlib crypto/aes + cipher.NewGCM.
Wrap-key source and the opt-in gate
The wrap key is sourced from the env var PLEXSPHERE_MANAGED_PUSH_WRAP_KEY_B64, the base64 encoding of exactly 32 bytes (../../../cmd/plexsphere/managed_push_factory_prod.go):
- An empty / unset value means "feature off": the factory returns a nil factory and the six handlers stay on their 501 scaffold. Reading the key inside a presence guard marks it operator-tunable rather than a required dev-cluster knob.
- A value that fails to base64-decode fails the boot loader.
- A decoded value of any length other than 32 bytes fails the factory's build-time validation (mirroring the registration factory's exact-length posture), so a mis-sized key fails closed at boot, never silently at the first attach.
At-rest persistence
Migration ../../../internal/platform/db/migrations/0051_domain_managed_push.sql adds two tables to the core logical database:
plexsphere.domain_managed_push— the per-Domain opt-in record, keyed ondomain_id(one attachment per Domain) andFK-bound toplexsphere.domainsON DELETE CASCADE. The sealed kubeconfig lives inkubeconfig_ciphertext bytea, whoseoctet_lengthCHECKfloors at the GCM envelope minimum (28 bytes = 12-byte nonce + 16-byte tag) and caps at 128 KiB so a malformed or oversized blob is structurally impossible at rest.kubeconfig_sha256is the hex fingerprint of the plaintext (display only);api_server_urlis the cluster server extracted at validation time. The plaintext NEVER lands in this table.plexsphere.domain_hook_push— the managed-push trail, one row per apply, keyed on the app-minted UUIDv7id.pushed_specis the five-field hook shape as jsonb;prior_stateis the full prior object JSON orNULL(theNULLdrives a delete-on-rollback);statusis the closedapplied/rolled_backset, withrolled_back_atnon-NULLiffstatus = 'rolled_back'enforced by an XORCHECK.
The kubeconfig is a sealed bytea column, deliberately not OpenBao. Routing a single per-Domain control-plane credential through OpenBao would add a second external-system dependency and an OpenBao round-trip to every push for no gain over an AES-256-GCM sealed column whose key is the same wrap-key class the NSK adapter already manages; a plaintext bytea column was rejected outright because this is the first surface that holds workload-cluster credentials, and a plaintext column would expose every customer kubeconfig to anyone with a database read.
The migration is irreversible by design: its down arm RAISEs an exception with SQLSTATE 0A000 (feature_not_supported) rather than dropping the tables. Dropping them would silently discard every operator-attached credential and the entire publish history — a downgrade-then-upgrade cycle would restore only empty tables, erasing both the credentials and the trail a security review depends on, with no trace. Operators performing a legitimate wipe-and-reinstall must drop the Postgres database itself; the down path is not a destructive teardown tool.
Server-Side Apply — GitOps-versus-managed authority
Managed push writes the rendered PlexdHook into the customer cluster through Server-Side Apply (SSA) under the pinned field manager plexsphere-managed-push with force: false (../../../internal/policy/hooks/push/kube/kube.go). The kube adapter builds a fresh dynamic client per call from the opened (plaintext) kubeconfig and never retains the plaintext between calls; every method addresses the single PlexdHook GVK the discovery context owns.
This is the authority inversion at the heart of the feature:
- Discovery-only (the default) — plexsphere mirrors what the agent observed and holds no kubectl credentials for a workload cluster. It never writes a
PlexdHookback. - Managed authority (this surface) — plexsphere becomes an authoritative writer for the hook objects a Domain opts in to. Because the field manager is stable and
forceisfalse, a conflict with another field manager — a GitOps controller or another operator that owns a field the apply touches — surfaces as409 managed_push_conflict. The apply deliberately refuses to steal fields; the operator must resolve the ownership overlap rather than have managed push silently overwrite the other owner.
Around the apply, the push captures the prior object: GetHook reads the current object (or records its absence as prior_state=NULL) before the apply, and the trail record is written only after a successful apply, so the trail never claims a push that did not land. DeleteHook (used by rollback) treats a NotFound as already-restored. The kube adapter collapses every apiserver/transport error into exactly two buckets: a field-manager conflict (ErrClusterConflict → 409) and an everything-else "unreachable" bucket (ErrClusterUnreachable → 502), because the transport contract defines only those two cluster-failure codes.
Rollback semantics
Rollback is one-shot and reverses an applied push (../../../internal/policy/hooks/push/record.go, ../../../internal/policy/hooks/push/service.go):
- Only a Record in
appliedstatus may roll back.Record.CanRollBackrefuses any other state withErrAlreadyRolledBack, so a second rollback of the same push can never trigger a duplicate cluster mutation. The transport surfaces this as409 push_already_rolled_back. - The reversal strategy is driven by the captured prior state: when the push had a prior object (
had_prior_state = true) rollback re-applies those captured bytes; when there was no prior state (had_prior_state = false) rollback deletes the object the push created. - The trail record is flipped to
rolled_back(stampingrolled_back_at) only after a successful cluster mutation, so the trail never claims a rollback that did not land. The Record is the audit trail.
As noted in the opt-in model, rollback does not consult the Enabled flag — only that a credential is still attached — so a disabled Domain can still undo a prior push.
Problem-code catalog
The surface emits a closed taxonomy of Problem.code values, pinned as constants in the transport package and translated from the domain sentinels at the composition-root adapter. The codes split by origin: the handler emits the request-shape and not-provisioned codes; the application service emits the invariant and cluster codes (translated across the anti-corruption boundary); the ReBAC denial uses a separate PermissionDenied schema rather than the Problem schema and is emitted audit-first.
| HTTP | code | Origin | Trigger |
|---|---|---|---|
| 400 | invalid_domain_id | handler | domainId is not a non-zero UUID. |
| 400 | invalid_push_id | handler | pushId is not a non-zero UUID. |
| 400 | invalid_body | handler | the request body is not valid JSON for the schema; for PushManagedHook an oversized body ALSO maps here (the push body has no 413). |
| 401 | unauthenticated | handler | the request carries no authenticated principal. |
| 403 | (PermissionDenied) | transport | a ReBAC denial — a distinct PermissionDenied schema, not Problem, emitted audit-first. |
| 404 | managed_push_not_configured | service (ErrNotConfigured) | the Domain has no configured target (GET/DELETE). |
| 404 | domain_not_found | service (ErrDomainNotFound) | a write references an unknown Domain (Postgres FK violation 23503). |
| 404 | push_not_found | service (ErrPushNotFound) | an unknown push id (GET single push / rollback). |
| 409 | managed_push_disabled | service (ErrDisabled) | a push while the target exists but is disabled. |
| 409 | managed_push_conflict | service (ErrClusterConflict) | an apiserver field-manager conflict on the Server-Side Apply. |
| 409 | push_already_rolled_back | service (ErrAlreadyRolledBack) | a second rollback of the same push. |
| 413 | kubeconfig_too_large | handler | the PutManagedPush (attach) body exceeded the 64 KiB cap — this operation ONLY. |
| 422 | kubeconfig_invalid | service (ErrKubeconfigInvalid) | attach-time kubeconfig validation failed (any arm above). |
| 422 | plexd_hook_invalid | service (ErrPlexdHookInvalid) | a push-time PlexdHook spec was invalid. |
| 502 | cluster_unreachable | service (ErrClusterUnreachable) | the customer cluster could not be reached (connection / TLS / timeout). |
| 500 | internal | handler/service | an unexpected error; the underlying text is logged internally and NEVER placed on the wire. |
| 501 | managed_push_not_provisioned | handler | the fail-closed scaffold: the service OR the authorizer is not wired in this build. |
Two sentinel→code renames happen at the composition-root adapter (translateManagedPushErr): the domain's push.ErrSpecInvalid is surfaced as plexd_hook_invalid (422), and the domain's push.ErrKubeconfigInvalid is surfaced as kubeconfig_invalid (422). Every other sentinel maps by the same name. The adapter exists so the transport http module never imports the k8s-aware policy module; an unrecognised error passes through verbatim so the transport's default arm logs it and answers 500 with a generic body.
The 413 asymmetry is deliberate: PutManagedPush distinguishes an oversized attach body (413 kubeconfig_too_large) from a malformed one (400 invalid_body), while PushManagedHook folds both an oversized and a malformed push body onto 400 invalid_body because the push operation has no 413 in the spec.
Hook-integrity (TOFU) interaction
Managed push writes PlexdHook CRDs into the cluster, but it is orthogonal to the trust-on-first-use hook-integrity gate documented in ../policy/integrity.md and the integrity-gating section of the discovery reference. That gate is unchanged by managed push:
- The integrity gate still pins a known-good image-digest baseline from the capability catalog the agent advertises, and still refuses a drifted action dispatch with
hook_integrity_violationonDispatchExecution. - Pushing a hook through managed push does not bypass, pre-seed, or alter integrity pinning. Managed push writes the object into the customer cluster; the integrity baseline is pinned and checked on the capability-update path, exactly as for a hook the agent discovered on its own.
- The digest the agent later reports for a pushed hook is validated on the capability-update path identically to a discovered hook. There is no privileged path: a hook plexsphere pushed is still subject to the same first-seen pin and the same fail-closed dispatch verdict as any other advertised hook.
In short: managed push controls what is written into the cluster; the integrity gate controls what may be dispatched against an advertised hook. The two surfaces share the PlexdHook vocabulary but enforce independent invariants.
Testing
The four-aspect coverage for managed push is held across the test pyramid.
- Unit — the domain value objects, the validation pipelines, the sealer, and the transport handlers in isolation:
../../../internal/policy/hooks/push/target_test.go,../../../internal/policy/hooks/push/sealer_test.go,../../../internal/policy/hooks/push/kubeconfig_test.go,../../../internal/policy/hooks/push/request_test.go,../../../internal/policy/hooks/push/record_test.go,../../../internal/policy/hooks/push/service_test.go, the repo unit test../../../internal/policy/hooks/push/repo/repo_test.go, the render test../../../internal/policy/hooks/plexdhook_render_test.go, and the transport handler tests../../../internal/transport/http/v1/managedpush/target_test.goand../../../internal/transport/http/v1/managedpush/push_test.go. - Integration — the persistence round-trip, the HTTP surface against a real wiring, the kube adapter against an apiserver through envtest, and the migration downgrade refusal:
../../../tests/integration/hooks_managed_push_persistence_test.go,../../../tests/integration/hooks_managed_push_http_test.go,../../../internal/policy/hooks/push/kube/kube_envtest_test.go, and the downgrade-refusal case in../../../tests/integration/db_migrations_test.go. - E2E — the chainsaw suite that stands up the production binary and exercises the surface end-to-end:
../../../tests/e2e/policy/plexdhook-managed-push/chainsaw-test.yaml. The suite isskip-gated until the operator-token mint is available; the workspace fixture gate fails closed ifskip: trueis dropped without the un-skip companion edits, so the green-but-empty fixture cannot ship undetected. - Documentation — this reference and the hooks HTTP API reference.
Cross-references
../../../internal/policy/hooks/push— the managed-push domain package:Target,Record,Request, theSealer,ValidateKubeconfig, the applicationService, the outbound ports, and the closed sentinel set.../../../internal/policy/hooks/push/kube/kube.go— the Server-Side-Apply kube adapter, the pinnedplexsphere-managed-pushfield manager, and the two-bucket cluster-error classification.../../../internal/transport/http/v1/managedpush— the six handlers, the transport-local ports and view types, the closed Problem-code constants, and the audit-first refusal helpers.../../../cmd/plexsphere/managed_push_factory_prod.go— the composition root: the wrap-key opt-in gate, the exact-length validation, thetranslateManagedPushErranti-corruption adapter, and the slog-backed audit sinks.../../../internal/platform/db/migrations/0051_domain_managed_push.sql— the two managed-push tables, the sealed-byteaand CASCADE decisions, and the irreversible-downRAISE.../../../api/openapi/plexsphere-v1.yaml— the six managed-push operations, the request/response schemas, and the Problem-code wire contract.../policy/hooks.md— the discovery-only PlexdHook reference managed push inverts.../policy/integrity.md— the integrity-violation ingest reference the trust-on-first-use gate fans out through.../../reference/api/hooks.md— the hooks HTTP API reference.