Operating from the portal, not the cluster
Maintainer directive, 2026-09-08: "do all the operations through the memex api" Β· "no direct access of aks etc" Β· "build the api in a way that you don't need any az access".
An instance is a record (Deployments/<name>, a Hosting/Deployment node on the control
instance memex.systemorph.com, GitSynced to the private Systemorph/Memex repo), and every
change to it is an action node (Hosting/InstanceAction) that the control instance's operator
executes in-cluster under its own service account. The operator's credential lives in the
cluster; the person or agent who asks holds none. That is the whole design: an operator who never
had az, kubectl or a Loki endpoint can still roll, restart, suspend, audit and reconcile an
instance β and can read what it is running.
π¨ Which portal am I talking to? The MCP server named memex is NOT the instance named memex
Measured 2026-09-10, and it cost two sessions an hour on the same morning:
| MCP server | Host | What it is |
|---|---|---|
systemorph |
memex.systemorph.com |
the control instance β the live, GitSynced Deployments space |
memex |
memex.meshweaver.cloud |
the public portal and the plugin registry |
The instance named memex is memex.systemorph.com, so the server named after it is the other
one. Both portals hold nodes at Deployments/<name>, and only the control instance's copy is
authoritative: on 2026-09-10 the control instance's Deployments/memex-cloud was version 60,
createdDate 2026-08-10, lastModifiedBy system-security, written 64 s after the config-repo
merge; memex.meshweaver.cloud's was version 9, createdDate 2026-08-30, and 40 versions behind.
That second copy is not unsynced β an earlier revision of this page called it "not a sync
target at all", and that was wrong. memex.meshweaver.cloud carries its own Deployments/_GitSync,
a GitHubSyncConfig for the SAME folder (Systemorph/Memex, mesh/Deployments, twoWay: true,
created 2026-08-30). Measured 2026-09-11:
| Field | Reading |
|---|---|
lastSyncOutcome |
Imported |
lastSyncCommitSha Β· lastSyncedAt |
one commit Β· 2026-09-09 |
lastAttemptedCommitSha Β· lastSyncAttemptAt |
a DIFFERENT commit Β· 2026-09-11 |
lastAttemptWasFinal |
true |
Imported beside a lastSyncCommitSha that does not move is not by itself a stalled sync.
GitHubSyncService.MayAdvanceBaseline deliberately holds that baseline whenever an import preserved
server-newer nodes or failed any, and lastAttemptWasFinal is the discriminator: true means
the verdict at lastAttemptedCommitSha is settled β a preserved node or a content-verdict refusal,
which re-attempting the same commit cannot change β while false means a transient failure the next
delivery retries. Here it is true: a SETTLED divergence. Either this portal's two-way import kept
its own server-newer Deployments/* nodes over the repository's, or it refused some of the
repository's content; the sync's activity log says which. So the copy is synced AND disagrees with
the record, by design β which is exactly why it is not the record. It is scheduled for retirement
with the GitHub App split; until then read Deployments/* on the control instance only. Three facts settle which portal is which without guessing: the Deployments/memex
record's own host and purpose, and every other
instance's Hosting__ReportTo, which points at https://memex.systemorph.com.
So confirm the portal before drawing any conclusion from a read of it β /api/version, or the
MCP server's configured URL. This is the same class as the namespace: memex confusion in #3883,
where the word named a Kubernetes namespace rather than an instance.
The rest of this doc tree still carries az aks command invoke β¦ / kubectl β¦ recipes. They are
evidence, not procedure: each one is either a measurement that was taken through the cluster
before the API surface existed (kept because the war story is the reason the rule exists), or a
break-glass read for the case where the control plane itself cannot act. Read them that way.
What exists today (measured on the control instance, 2026-09-08)
The Hosting package (MeshWeaver.Plugins, Hosting/**; its own manual is the in-mesh page
Hosting/Guide, source
Hosting/Guide.md)
gives you:
| Surface | What it answers / does |
|---|---|
/Hosting/Console β the Fleet Console |
every instance at a glance: recorded state, the version it is RUNNING, sampled health with the sample's age, per-instance log links. It reports; it does not command. |
Deployments/<name> (Hosting/Deployment) |
the record β host, namespace, cluster, database, image repository, key-vault classes, env precedence, operator settings. The record's image pin is the roll. |
Hosting/InstanceAction kinds |
Provision, Teardown, Backup, Restore, Suspend, Reactivate, HelmRelease (helmAction: capture\|adopt\|deploy β dispatches the config repo's helm-release.yml and follows it), Roll (set image to imageTag, else the record's pin, then WAIT for the rollout), Restart (rolling restart, then WAIT), InstallAddOn, Audit, RotateRegistryKey (two-phase, at the registry that holds the instance β Registry-key rotation), RevokeRegistryKey (a key named by the Secret it sits in stops authenticating, nobody reading it), Reconcile (converge a drifted instance back onto its record β the reconcile loop). Each run carries phases, a log, the invoker's identity and the name-the-instance confirmation. |
Hosting/DeploymentStatus, Hosting/LogEntry, Hosting/Issue (types) |
the designed observation surfaces β one status sample per deployment (ready/desired replicas, restarts, health, RUNNING image, last activity), structured log records pulled from the logging backend, filed issues. |
Sample and Logs (Hosting/InstanceAction kinds β Systemorph/MeshWeaver.Plugins#1521, the delivery of the paragraphs below) |
READ-ONLY, no operator job, no confirmation: Sample writes Ops/Status/<id> with replicas[] β per pod the image, ready, restarts, started, phase, terminating, generation, and what the pod's OWN /health says (verdict, detail, version, frameworkIdentity, pluginCount) β plus generations, converged and warnings[] (unknown is never zero); Logs + query/sinceMinutes/limit/pod lands a Loki window as Hosting/LogEntry nodes under Ops/Logs with the exact LogQL, the count and whether it was CUT on the run. Both read Prometheus and Loki from the control instance's own pod, where they are credential-free. |
An action is one node:
{ "id": "roll-memex", "namespace": "Ops/Actions", "name": "Roll memex",
"nodeType": "Hosting/InstanceAction",
"content": { "$type": "InstanceActionContent",
"deployment": "memex", "requestedAction": "Roll", "imageTag": "3.0.0-ci.8079",
"confirmation": "memex", "reason": "β¦", "dryRun": true } }
Start with dryRun: true β it renders the exact commands and changes nothing. Then watch the same
node: state goes Requested β Running β Done, or Failed naming the phase, or Refused with
the question you did not answer. Through MCP that is create / get; through the portal it is the
node's own page.
What did NOT exist on 2026-09-08 β and what delivers it
Measured 2026-09-08 10:40Z on the control instance: the DeploymentStatus, LogEntry and
Issue types existed and had ZERO instances, Ops itself did not exist, and the maintainer's
own identity was refused Create on Ops/Actions/β¦. Nothing sampled status in-cluster, nothing
answered a log query, and no replica reported what it could load. The delivery is
Systemorph/MeshWeaver.Plugins#1521 (Hosting module) β read this table as "before / after it
lands on the control instance":
| Question | Before (break-glass read) | After #1521 (one node, no credential) |
|---|---|---|
| What image / how many restarts / how old is each replica β and is an old process still a cluster member? | az aks command invoke β¦ kubectl -n <ns> get pods -o wide |
{ "requestedAction": "Sample" } β Ops/Status/<id>: replicas[] with image, ready, restarts, started, phase, terminating, generation; generations and converged on the node. A roll (Roll/Restart/Reconcile/Reactivate) now ends with Verify one generation and refuses Done while a previous-generation pod is still a member β the 8059-after-8079 measurement. |
| What did the process log at time T? | az aks command invoke β¦ curl loki.monitoring.svc.cluster.local:3100/loki/api/v1/query_range β¦ β the invoke shell has curl but no sed/python3 |
{ "requestedAction": "Logs", "query": "β¦", "sinceMinutes": 60, "limit": 300 } β logQl, entryCount, truncated on the run; lines under Ops/Logs, the Deployment page's Logs area. π¨ Zero entries with a logQl is an answer only once a same-text positive control passes β measured 2026-09-11, three in-window zeros for lines LogWatch held samples of from the same namespace under three hours earlier (Log entries are a query result). |
| Can THIS replica load NodeType X? | kubectl exec β¦ ls /tmp/MeshWeaver/.mesh-cache/<Type>* on EACH replica β a live compile is pod-local (local = FileSystemAssemblyStore), see NodeTypeCompilation |
the core half has landed. /health is a system-side census, public and past RLS, and the Sample keeps each pod's whole body: content-types names every type this replica could not TYPE; bake-report (census-tagged, so it prints CLEAN too) carries total/baked/pending, the per-state breakdown and ClassifiedFromLocalAdoption; source-discovery carries the fold's chunk count and largest inter-chunk gap. Still partial, and say so: content-types records a degradation only when a read degrades, so a type nobody has opened on this replica is in neither list β for that, the boot log remains the only source. A Ready process reporting 0 plugins already degrades the sample, which is the shape that outage wore. |
| Who may create the first action? | nobody β Ops did not exist and no grant path existed |
the Hosting module provisions Ops and mirrors every Admin on Admin/_Access as Admin on Ops/_Access at start (OperationalSpaceProvisioning, on the always-activated Hosting/PlatformBuilds hub). |
Until #1521 is on the control instance, the "before" column is what you have; see MeasuringALivePortalReadOnly for the safe, read-only shapes. A break-glass read is fine; a break-glass write is half an operation. The Hosting manual's "Break glass β when the control plane cannot act" section says exactly which half the cluster command leaves undone (paywall, backup question, the record's stamp, the audit) and how to reconcile it in the same session. Do not re-derive that list here β read it there.
How to read a cluster recipe in this doc tree
- Is there an action kind for it?
set image+ rollout βRoll;rollout restartβRestart;scale --replicas=0βSuspend;helm upgradeβHelmRelease deploy(today) / the record pin +Roll; "make the cluster match the record" βReconcile; "what is drifting?" βAudit;patch pvc β¦ storageβvolumes[].sizeon the record +Reconcile(the operator'shosting-pv-resize, which never shrinks and reads the capacity back);set env deploy/<name> <KEY>-βretiredByon the record'sinlineEnventry +Reconcile(the operator'shosting-inline-env-retire). Use the action. The kubectl line is what the operator runs for you. - Is it a read the API does not answer yet? (the three above) Take it read-only, name it as break-glass in what you write down, and file the gap against the Hosting package rather than leaving the recipe as the procedure.
- Is it a measurement in a war story? Leave it β it is the reason the rule on the page exists. Do not run it to "check"; ask the API question the page's rule now points at.
Retiring an inline env: entry β Reconcile, driven by the record's retiredBy
Until 2026-09-11 this was the one routine WRITE with no action kind. No repository change and no
InstanceAction could remove an inline env: entry from a Deployment; only a break-glass
kubectl set env deploy/<name> <KEY>- could. The chart never rendered one (it emits four
unconditional portal entries β the DOTNET_Dbg* crash-dump set β plus AZURE_CLIENT_ID when
selfUpdate.azureClientId is set, and two per gate sidecar; every name is fixed and no values key
extends the list), the record's inlineEnv is declarative by contract β dropping an entry does not
delete one β and ReapplyRecord is a helm upgrade, whose three-way merge removes only what helm
previously owned. Audit detected the drift precisely, under envLiveOnly and
plainSecretEntries, and no remedy could act on it.
The lane is now Reconcile (MeshWeaver.Plugins#1593). Its RetireInlineEnv remedy reads the one statement the
record could always make and nothing read β inlineEnv[].retiredBy β and removes exactly the
entries that carry it:
- Mark the entry retired on the record:
retiredBynames the issue that ends it,shadowsnames theenvFromsource it falls through to, and a credential also needsagreesWithShadowed: true. The plan REFUSES, naming the entry, a sole source (shadowsblank β removal would blank the key), a credential whose equality is not recordedtrue, and any entry the record says disagrees with its shadow. One refused entry refuses the wholeReconcile. - File a
Reconcile. The retirement runs afterReapplyRecordβ so the key's declared home is on the pod first β and before any roll. The operator'shosting-inline-env-retirerefuses while a rollout is in progress; walks the portal container'senvFromin order and requires the LAST source carrying the key to be the one the record names; re-measures in-cluster that the inline value and that source are EQUAL (lengths and a verdict, never a value); and only then removes the key from every container that carries it, the gate sidecars included, in one patch guarded on the Deployment'sresourceVersion. The run waits for the rollout and ends in the re-audit and Verify one generation, like everyReconcile. - Drop the entry from the record once the closing audit no longer lists it. Until then the
record still describes a shadow, and
RotateRegistryKeyrefuses while the record lists an inline entry for any key the registry key lands as;hosting-kv-rotaterefuses the same thing in-cluster, before it mints.
π¨ It works only once the control instance has BOTH halves: an operator image that carries
hosting-inline-env-retire (the operator image is control-instance configuration, moved by a
helm-release deploy, not by any record) and the Hosting module version that plans the remedy
(1.17). On an older operator image the step fails by name (command not found) after the re-apply
and nothing is removed. And it retires a shadow; it does not remediate a disclosure β a
credential that sat in plaintext in a Deployment spec still needs rotating at its issuer, which is a
separate act (MeshWeaver#3201 is the worked example).
Related rules decided the same day
- The Deployment record is the ONE input. Aspire and Helm render from it; the image receives
it as configuration (
Deployment:Record); Aspire emits a record, never a chart. The record is built fluently (AddMemex("memex").WithImage(β¦).WithPluginRepo(β¦)), the adapter's own copy of it (MemexOptions) is gone, and generating the chart from Aspire (#3646) is retired β ConfiguringAnInstanceFromAspire. - Volume capacity is a record property:
volumes[].sizeon the Deployment record is what a claim holds.ProvisionandReconcilegrow every declared claim to it throughhosting-pv-resize(grow-only, read back from the claim's status, refuses a class that cannot expand);Auditreports a claim that has fallen below its record. The 16Gi/datashare that measured FULL onmemex.systemorph.comat 13:51Z that day is the case β DeploymentAKS β "Volume capacity is a record property". - A platform roll must not need every satellite re-baked first:
Modules:VersionStrictness(Exact/Family/Minimum, dev =Minimum) and "a sealed publication syncs its own sources" β ModuleVersioning, SealedPublicationReads. - Self-update takes clean releases by default; a
-ci.<n>build is opted into by a version pattern onAdmin/UpdatePolicyβ ReleaseProcess. The version scheme isX.Y.Z-ci.<n>(temporary) β cleanX.Y.Z(final);rcis retired. - A cancelled delivery run with zero jobs is a superseded queue entry, not a lost seal β ReadingCiSignals.
- The operator's ClusterRole follows the chart:
deploy/aks/manifests/hosting-operator/operator-rbac.yamlis applied by the config repository's helm-release lane on everyadopt/deploy, from the chart at the pin; the operator test suite refuses a script that names akubectlverb+resource the role does not grant (check-rbac-coverage.sh). Measured 2026-09-09: the first Reconcile through the fixed operator stopped at step 1/6 withstorageclasses β¦ is forbiddenβ the grant had been onmainfor hours, the cluster's role predated it, and only a laptop could have moved it. A control-plane input with no lane is a defect, whichever file it lives in. /api/versionnames the delivery run's RESOLVED target commit, not the set's queue tip.3.0.0-ci.8131is CD run 34260408777 with heade1039813e; its "Resolve the target commit" job picked3853374f7(the merge one second earlier in the same queue group), and that is what the image answers. Compare a portal's commit against the run's resolve job, never against the run's head ormainβ read as "not 8131", it cost an hour on 2026-09-09.- Steady state is self-update and CD, never a hand roll β ReleaseStrategy, DeploymentAKS (the bootstrap runbook, now read under rule 1 above).