Skip to content

Merge main into demo (after #52, #88, #89 and #85) - #96

Merged
sauagarwa merged 70 commits into
validatedpatterns-sandbox:demofrom
sauagarwa:sync/main-into-demo
Oct 12, 2026
Merged

sauagarwa merged 70 commits into
validatedpatterns-sandbox:demofrom
sauagarwa:sync/main-into-demo

Conversation

@sauagarwa

Copy link
Copy Markdown
Collaborator

Merge main into demo

demo was based on main at #79. This merge brings in current main (4fc1785), including #84, #85, #88, #89 and #52. It adds no new demo content: the next PR adds the LaunchPad doc and doc fixes.

Please merge with "Create a merge commit", not squash or rebase. That way demo keeps main's commits, and later main → demo (or demo → main) merges stay conflict-free.

Conflicts

Checks

  • Content: the result is identical to the rebased feat/demo content that has been tested.
  • Tests: 1151 pass locally. The only failures are the same ones main has in my environment: harness bundle-chart tests that my Helm stand-in can't render, ssh-keygen, and one NetworkPolicy render. CI covers those.
  • Egress and network policy: the parity tests pass with the Slack and Gmail profiles, because main's egress.allow already lists their hosts.
  • Live: main's content runs on OpenShift (OpenShell 0.1.2-rhaiv.0, engine interceptor). A portal workspace installs and applies cleanly, with NemoClaw images from GHCR.

Hadar301 and others added 30 commits October 7, 2026 16:18
Consolidate Make targets and safe lifecycle scripts.
Add deployment checks and update guides and tests.
Merge upstream/main after PR validatedpatterns-sandbox#79.
Keep the clean Make workflow and remove obsolete prepare Job code.
…, execute H9 revocation drill

Three gaps from PR validatedpatterns-sandbox#53 review (round 7) left open on main after merge:

- verify_harness_image was only called from image_tree, which only runs
  when the harness volume gets (re)filled. A sandbox whose volume already
  holds the image never had the signature rechecked, so revoking the
  cosign identity had no effect on it. Moved the check into
  prepare_harness so it runs on every apply, regardless of whether the
  volume is current.

- verify_harness only confirmed the OpenClaw bundle loaded (plugins list
  --json, bundleCapabilities flag), never that the individual MCP servers
  mcp.json declares are actually live. describe_harness_tree now returns
  mcpServers (declared names); verify_harness cross-checks them against
  `openclaw mcp status`, WARN not fail when the agent has no mcp status
  subcommand.

- scripts/e2e-harness.sh's H9 revocation step only printed manual
  instructions and skipped. It now executes the drill: helm upgrade
  --set harnessEnabled=false, wait for the sandbox to come back without
  /sandbox/harness and a clean plugins.load.paths, then flip back to true
  and wait for the mount to return, restoring harnessEnabled=true on exit
  (including ^C). H3 gained the same live MCP server-name check.

Rollback (HarnessVolume.fill backing up and restoring the previous tree
on a failed import) was already correct on main; untouched here.

Tests: tests/installer -k harness, 114 passed, 1 pre-existing unrelated
failure (test_a_running_gateway_is_kept_when_nothing_changed, fails
identically on clean main).
A harness volume reaches a sandbox through caller driver config, and
allow_driver_config is a fleet-wide gateway switch: once it is on, any
workspace user may submit a driver_config envelope, and OpenShell's
resource admission never inspects who sent it or what mount mode it asks
for (crates/openshell-core/src/resource_admission.rs knows nothing about
the caller). That is the create-time window the harness review left open:
the installer can detect a writable remount and recreate the sandbox, but
nothing stopped the mount being created writable in the first place.

The interceptor does see both, since the gateway hands it the whole
CreateSandbox request and driver_config carries no secret field option, so
it is not stripped from the interceptor view. Two rules go in through the
same patch-the-upstream-source mechanism the profile-annotation workaround
already uses:

- only a platform admin identity may submit driver config at all;
- every mount it carries must set read_only: true.

CreateSandboxTemplate is bound as well, because workload_template is
resolved after the interceptor seam, so a template would otherwise smuggle
a writable mount past the CreateSandbox check.

tests/charts/test_governance_interceptor_image.py keeps the Dockerfile and
the BuildConfig's inlined copy in step, since the two must patch upstream
identically.
…2e dedup

Follow-up to 439f826:

- verify_harness's MCP server check used plain substring containment
  (`s not in listed.out`), so a declared server named `search` would be
  wrongly treated as present if the output merely contained an unrelated
  server named `search-internal`. Switched to a word-boundary regex match.
  scripts/e2e-harness.sh already used `grep -qw` for the same check, so no
  change needed there.

- Added test_a_revoked_signature_is_caught_on_a_later_apply: fills the
  volume once, marks the image unsigned, then re-applies with a fresh
  installer instance against the same (already current) ledger-backed
  volume. Confirms the signature check in 439f826 actually fires when no
  pull/export happens, not just on first fill.

- Removed a redundant BOM_RELEASE default assignment inside the H9 drill
  block (already set at the top of the script).

Tests: tests/installer -k harness, 115 passed, 1 pre-existing unrelated
failure (test_a_running_gateway_is_kept_when_nothing_changed, fails
identically on clean main).
…or GC

prepare_harness added an image to self.harness_images before calling
verify_harness_image on it. If the signature check then raised (revoked
identity, no longer signed), the image was already in the keep-set for
this apply, so cleanup_harness_images() never removed it -- a revoked
image stayed on disk indefinitely instead of being garbage-collected.

Swapped the order: verify first, then add to the keep-set.

Tests: test_an_image_whose_signature_is_revoked_is_garbage_collected.
…t mcp status

verify_harness's live MCP-server check called `openclaw mcp status`, which
only lists servers added via `mcp add`/`mcp set`; it never reflects ones a
plugin bundle declares in its own mcp.json. Caught live on a real cluster:
the shipped ds-default bundle failed verify on every single apply, crash-
looping saw-apply.service forever, even though the bundle loaded correctly.

`openclaw plugins inspect <bundle> --runtime` is OpenClaw's actual documented
way to check a bundle's live MCP servers (confirmed against docs.openclaw.ai);
--runtime imports the module so it reflects real load state, not just
declared config.

No test exercised this code path before (the fake openshell had no handler
for the command at all), which is why the bug shipped with "115 passed".
Added a plugins inspect handler to the fake and a test proving verify still
fails when a declared server is missing from the live listing.
…sion

verify_harness's live MCP-server match used \bserver\b, but `-` is a
non-word character in regex, so \bsearch\b still matches inside
"search-internal" -- exactly the false positive 87d1e0d's word-boundary fix
claimed to close. Server names here are kebab-case (saw-mcp-echo), so a
hyphen must count as part of the token, not a boundary. Switched to a
negative lookaround that treats [\w-] as the token's own characters.

No test existed for the word-boundary behavior itself (87d1e0d only added
a signature-revocation test); added one with a declared server name that is
a prefix of a different name in the live listing.
Require the CLI API version pinned by the gateway BOM.

Show Pattern VMs in saw-list and recover absent local gateway entries.

Pass the sandbox password to the OpenClaw TUI.

Document browser pairing for the older CUDA image.
Keep Pattern checks focused on tools and cluster capacity. Require CNV
and RHBK only for the manual quickstart path.

Remove an orphaned CNV CRD after owned teardown when no operator or
HyperConverged resource remains. Document test user password access.
Correct Keycloak help, clarify SAW listing, and move internal
checks after public commands. Keep designer approval pending.
Build UI route values from the selected SAW-BOM profiles and set the
cluster domain for the VM proxy. Use uv for local Keycloak user tasks.

Document the routes and test the profile selection.
Use the default Helm release list for SAW and Keycloak cleanup.
Helm v4 includes all release states and rejects the --all flag.
…pr53-nvidia-gaps

fix(harness): close signature/MCP/revocation and driver-config admission gaps
Mark only the CSV installed by the pattern-owned subscription.
After uninstall, remove it only when no Pipelines subscription
remains. This lets OLM resolve the operator on reinstall.
…ssuer

The gateway fetches the OIDC issuer (Keycloak's route) at startup with the
system trust store and exits when it cannot verify it. On a cluster whose
apps certificate comes from its own ingress CA (found on an arm64
LaunchPad cluster), the gateway never started: "OIDC discovery request
failed", and the install timed out.

oidc.caBundle (PEM) goes into config.json; the installer writes it to
/etc/pki/ca-trust/source/anchors on every run (cloud-init would run once
per VM), runs update-ca-trust, and restarts the gateway when the trust
store changed. Emptying the setting removes it.

Co-Authored-By: Claude
… trust store

docs/deployment-guide.md, "The issuer's certificate": when it is needed
(self-signed *.apps or a private-CA external issuer), how to check from a
VM, and how to set oidc.caBundle for the pattern and the quickstart.

The dashboard and sandbox-UI oauth2-proxies get the VM's trust store, so
setting dashboard.insecureSkipIssuerTlsVerify: false verifies the issuer
with the configured CA. The default is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e fix

make check-oidc-ca (also in check-prereqs) tells, before
./pattern.sh make install, whether workspace VMs will trust the issuer:
OpenShift's self-signed *.apps default, or a custom certificate that does
not verify against public CAs, means oidc.caBundle is needed. It saves the
cluster's ingress CA and prints the overrides/saw-users.yaml snippet.
ISSUER=<url> checks an external issuer.

If it was missed, install now checks the issuer with the VM's trust store
before starting the gateway and fails with the same instructions, instead
of timing out on the gateway's port.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…luster Keycloak

The saw-ingress-ca imperative job copies openshift-config-managed/
default-ingress-cert (public data) to Vault at
secret/data/hub/cluster-ingress-ca when it changes. pattern-secrets syncs
it into every user namespace as the Secret saw-ingress-ca (saw-users
defaults.clusterCaSecret), and the VM mounts it like a provider Secret;
the installer trusts its ca-bundle.crt. Only for the in-cluster Keycloak
(oidc.issuerUrl empty); an explicit oidc.caBundle wins.

With the issuer now trusted, saw-users no longer turns off the oauth2-
proxies' issuer verification (insecureSkipIssuerTlsVerify).

check-oidc-ca exits 3 for this case (handled by the pattern, the
quickstart needs oidc.caBundle); the installer's error and the docs
describe both paths.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pattern uninstall removes the managed gateway image stream.
Restore the tag before the live test runner starts reinstall.
Found on the GB200: Argo CD's client-side apply copies each object into
its kubectl.kubernetes.io/last-applied-configuration annotation, and
annotations may hold at most 262144 bytes. The installer ConfigMap
(apply_bom.py and friends) is about 261 KB rendered, so the sync failed
with "metadata.annotations: Too long" and the VM kept its old installer.
Main is already about 255 KB, so any growth hits it. Server-side apply
keeps no such annotation; a ConfigMap itself may hold 1 MiB. The portal's
per-user apps render saw-users too, so they get it as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Mark the HyperConverged CRD while its pattern-owned namespace exists.
Cleanup then checks the marker even if framework uninstall has already
removed the namespace. Cover both deletion orders and unowned cases.
Use the CI Python environment for Keycloak user script tests. Keep uv for local runs. Set the harness test namespace so Make exports do not alter its fixture.
With ServerSideApply there is no last-applied annotation to diff
against, so the fields KubeVirt defaults on the VM (firmware serial and
uuid, machine type) kept saw-<user> OutOfSync. Found on the GB200;
argocd.argoproj.io/compare-options: ServerSideDiff=true on the
Application made it Synced.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: in the system trust store the issuer CA (the cluster's ingress
CA, no name constraints) was a trust anchor for every TLS client on the
VM, podman pulls included. Now:

- The installer writes the CA to the runtime user's
  ~/.config/openshell/issuer-ca.pem and, with the public CAs, to
  issuer-trust.pem, and gives the gateway SSL_CERT_FILE=issuer-trust.pem
  in a drop-in (openshell-gateway.service.d/issuer-ca.conf). The system
  store is untouched; the anchor older builds wrote there is removed.
- The oauth2-proxies mount issuer-ca.pem and use it only for the issuer
  (OAUTH2_PROXY_PROVIDER_CA_FILES) instead of getting the system bundle
  over /etc/ssl/certs; the dashboard BFF gets nothing.
- A bundle with a private key is refused by the chart and the installer,
  one without a CA certificate by the installer; leaf certificates are
  dropped. The issuer pre-check verifies with issuer-trust.pem, so a CA
  that does not sign the issuer fails install.
- saw-ingress-ca verifies Vault's certificate (service CA) before sending
  the root token, and skips a ConfigMap with a key in it.
- Docs: the trust scope, and that a user's values are admin input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: the pre-check verified with the public CAs plus the extra CA, so
on a cluster whose *.apps certificate is public the automatic ingress CA
was still added to the gateway's trust although it signed nothing.
select_issuer_ca now connects to the issuer and verifies with the extra
CA alone: installed when it signs the issuer, skipped when the issuer is
publicly trusted anyway, and install fails with the fix when neither
verifies it. An unreachable issuer keeps the configured CA (the gateway
reports it). Tests use a real HTTPS issuer on 127.0.0.1.

saw-ingress-ca: stop with a clear message, before reading the root token,
if the service CA file is missing. OpenShift puts service-ca.crt in every
pod's default service-account volume, and the GB200 run reached Vault
with verification, so this only guards against a different platform.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CI (Python 3.14) failed the real-TLS issuer tests with "CA cert does not
include key usage extension": since 3.13 ssl.create_default_context()
sets VERIFY_X509_STRICT. The test CA now has keyUsage and a subject key
identifier and the issuer certificate an authority key identifier,
keyUsage and serverAuth, like OpenShift's ingress CA and certificate
(which is why the same code passed on the GB200's Fedora 44 guest).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…idc-ca-trust

Trust a private-CA OIDC issuer: oidc.caBundle, with manual steps
LinoyBitan1 and others added 28 commits October 11, 2026 15:46
…ch-ci-images-to-ghcr

ci: share image build recipes and switch project images to GHCR
…ster-policy

feat(security): enforce intra-cluster NetworkPolicy and profile allowlist parity (Phase 2 Isolation)
global.governance.engine (values-global.yaml, or the Pattern's
extraParameters) picks the service behind
governance-interceptor.openshell-agents.svc:18081: the OpenShell
governance interceptor (default, unchanged) or NVIDIA Agent Policy
Fabric, which serves the same policy as a signed bundle, stamps
provenance on every sandbox, refuses runtime policy changes and writes
an audit trail.

- charts/governance-interceptor: with apf, the signed bundle and trust
  root ConfigMaps (files/apf, generated per deployment), ExternalSecrets
  from Vault for the GHCR pull secret (toJson, trimmed), the signing
  seed and the Argo CD OCI repository, and an Argo CD Application for
  the private APF chart, released as governance-interceptor so gateways
  keep their endpoint. The NetworkPolicy selects the APF pod. The app
  prunes and the APF Application carries the resources finalizer, so
  switching engines either way removes the other engine's objects.
- charts/openshell-saw: per-engine binding policy. APF declares one
  binding per phase, which allowlist rejects, so APF uses dynamic (its
  own 14 bindings, fail-open post-commit). Interceptor rendering is
  byte-identical, so existing VMs do not restart. saw-users passes
  global.governance to each user.
- scripts/apf-bundle.py (make apf-keys, apf-bundle, apf-bundle-check):
  translates charts/governance-policy into APF SandboxPolicy/Provider
  documents using only the fields APF accepts, leaves out profiles APF
  cannot express (query auth: gemini), and signs exactly as
  apf-compile genfixture. The repository ships no key and no bundle.
- Quickstart: make governance-apf and GOVERNANCE_ENGINE=apf.
- values-secret.yaml.template: ghcr and apf-signing entries.
- docs/apf.md, README setup steps, deployment guide; chart and script
  tests.

Validated live on OpenShift: signature verified, policy injected with
apf.nvidia.com provenance, runtime policy mutation refused, audit
events recorded.

Co-Authored-By: Claude
- Golden byte comparison of the interceptor-engine gateway.toml (default
  and with OIDC), rendered from main before the switch, so the "existing
  VMs don't restart" guarantee holds byte for byte, not only after
  parsing (review: mkhaas).
- apf-bundle.py: note that apf-dev is only the chart's mount label; the key
  behind it is the operator's own (review: mkhaas).
- Rebased on main with OpenShell 0.1.2 (validatedpatterns-sandbox#56): the engine switch sits in
  the schema-v2 gateway.toml, and the interceptor image stays the ghcr
  v0.1.2 one. The openai profile now names api.openai.com, which the APF
  bundle test expects.
- docs/apf.md: what to check with OpenShell 0.1.x (extension protocol
  negotiation, NVIDIA/OpenShell#3929 annotations, no inference routing).

Co-Authored-By: Claude
validatedpatterns-sandbox#78 lets a sandbox mount its harness volume through caller driver config,
and relies on the patched governance interceptor to admit only the
installer's read-only harness mount from an mTLS admin. APF has no such
guard, so with engine apf any caller could attach the volume, writable.
The chart now stops with a message when allowDriverConfig is set on the
apf engine, instead of rendering a gateway without the guard. Harness
bundles stay off by default, so default workspaces are unaffected.

Co-Authored-By: Claude
The APF chart (0.2.0) pins images built against OpenShell 0.0.x. A 0.1.2
SAW gateway refuses them at startup: "gateway-interceptor extension
'governance' did not provide protocol metadata" (seen live), and with
fail_closed the gateway stays down. APF 0.1.0 is the first build for
OpenShell 0.1.x: its Describe returns the extension protocol metadata
(PeerMetadata, protocol 1.0), and it no longer misreads the bundle's
default SandboxPolicy as a provider profile. The chart stays at 0.2.0;
only the gateway, policy-service and compile images move to 0.1.0, through
apf.values (so the Argo CD Application and `make governance-apf` both get
them). The images are multi-arch.

Co-Authored-By: Claude
… 0.1.0

From the live test on OpenShift with OpenShell 0.1.2-rhaiv.0: APF puts
the policy on a sandbox at creation and refuses changes afterwards, so a
re-signed bundle reaches sandboxes created after it, not the existing
ones. Adds the network_policies example that was verified (git clone from
github.com refused, then allowed for a new sandbox after re-signing), how
to find the real binaries to list, and what the provenance annotations
record.

Co-Authored-By: Claude
The portal rebuilt `global` for its saw-users values without `governance`,
so portal-created workspaces rendered interceptor `allowlist` bindings
even with the pattern on APF. Forward it like sshPublicKey; saw-users
already passes it on to openshell-saw (machineGlobals).

Co-Authored-By: Claude
- build: an unchanged policy is "up to date" only when the bundle still
  verifies against apf.pub. After `make apf-keys --force` it re-signs with
  a new revision instead of leaving a bundle APF rejects.
- APF's anti-rollback counter is an emptyDir in the APF chart, and every
  bundle change rolls the pod, so a new pod accepts any signed revision.
  verify (make apf-bundle-check, and the committed-bundle test) now fails
  when policy_revision is not above every earlier commit of the bundle,
  so a git revert of the bundle is caught before Argo CD deploys it.
  build picks a revision above all of git history.
- keygen --force tightens the seed to 0600 when the file already existed.

Co-Authored-By: Claude
KubeVirt does not restart a running VM when its template changes; it
marks it RestartRequired and the gateway keeps its old bindings. The
docs said VMs restart on their own. `make saw-restart-pending` lists
gateway VMs with RestartRequired and restarts them with CONFIRM=yes.
docs/apf.md says to run it in both directions, how quickstart SAWs
switch, how to re-sign after a new key, and where rollback is enforced.

Co-Authored-By: Claude
validatedpatterns-sandbox#88 made networkPolicy.clusterNetworkCIDRs required (make saw-create and
the pattern discover them), so the APF gateway tests' direct renders of
openshell-saw need the same offline fixture main's chart tests use.

Co-Authored-By: Claude
…pf-governance

feat(governance): switch between the governance interceptor and APF
…tterns-sandbox#88, validatedpatterns-sandbox#89 and validatedpatterns-sandbox#85)

Conflicts, resolved as in the feat/demo rebase:
- values-secret.yaml.template: main's APF entries, then the demo's Slack
  and Gmail entries under its "Personal assistant" heading.
- tests/charts/test_openshell_saw_chart.py: main (validatedpatterns-sandbox#88) moved the profile
  copy check to test_egress_profile_parity.py; it now lists the demo's
  gmail and slack copies there.
- Makefile-quickstart: main's layout (validatedpatterns-sandbox#85); keycloak-add-user added to it.
  Main's Keycloak script no longer prints passwords, so its help says the
  password is kept in a Secret.

Co-Authored-By: Claude
@sauagarwa
sauagarwa force-pushed the sync/main-into-demo branch from d0d3c09 to 88846fe Compare October 12, 2026 16:37
@sauagarwa
sauagarwa merged commit 54d27c6 into validatedpatterns-sandbox:demo Oct 12, 2026
11 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants