Repository navigation
feat: ANS identity extension for NVIDIA OpenShell - #1
Conversation
Initial import of the ans-openshell module: a supervisor middleware that signs allowed outbound requests as the sandbox's registered ANS agent using ANS-6 Method B (DPoP proof, transparency-log receipt and status token), a gateway interceptor that vets destination hosts against ANS registration before a policy is committed, a hydrator that turns OpenShell's OCSF audit export into signed Trust Index observations, and a reference callee that verifies ANS-6 callers with the SDK's pop middleware. Includes the vendored OpenShell protocol definitions (Apache-2.0, see third_party/openshell) with generated Go bindings, an in-process end-to-end demo (make demo), a Helm chart and Dockerfile, CI with a coverage gate, lint and proto-drift checks, and release automation (release-please and GoReleaser) matching the other agentnameservice repositories. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: kperry <kperry@godaddy.com>
buf-action installs the latest buf by default; 1.74.0 formats generated doc comments differently from the 1.73.0 used for gen/, so the drift check failed on the PR. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: kperry <kperry@godaddy.com>
The committed bindings were produced by a protoc-gen-go built with Go 1.27, whose go/format lays out doc-comment lists differently from the Go 1.26.9 that CI builds the plugin with, so the drift check failed. make proto now builds both plugins with the CI toolchain at the pinned versions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: kperry <kperry@godaddy.com>
csnitker-godaddy
left a comment
There was a problem hiding this comment.
Requesting changes for the four P2 issues detailed in the inline comments.
| newBinding("create-sandbox", openshellv1.OpenShell_CreateSandbox_FullMethodName, createSandboxHosts), | ||
| newBinding("update-config", openshellv1.OpenShell_UpdateConfig_FullMethodName, updateConfigHosts), | ||
| newBinding("submit-policy-analysis", openshellv1.OpenShell_SubmitPolicyAnalysis_FullMethodName, submitPolicyAnalysisHosts), | ||
| newBinding("edit-draft-chunk", openshellv1.OpenShell_EditDraftChunk_FullMethodName, editDraftChunkHosts), |
There was a problem hiding this comment.
[P2] Check registration when stored drafts are approved
These bindings check draft submission and editing, but omit ApproveDraftChunk and ApproveAllDraftChunks, which install stored proposals into active policy. A draft accepted in observe mode can therefore be approved after switching to require_registered without checking its destinations. OpenShell merges the stored rule directly, so the UpdateConfig binding does not cover this path. Please cover both approval operations and vet the stored rules before installation; require mode should reject approval when those rules cannot be resolved.
There was a problem hiding this comment.
Agreed, and bound in the next push: ApproveDraftChunk and ApproveAllDraftChunks now have bindings whose extractors resolve the stored rules from the evaluation's current_state (expected in GetDraftPolicyResponse shape, a chunks list), vet their destinations like any other operation, and the gateway example registers both RPCs. At the pinned OpenShell commit the server sets validate_current_state: None for every evaluation (crates/openshell-server/src/multiplex.rs), and the docs list "the gateway does not yet populate current_state with method-specific state" as a current limitation, so the interceptor cannot see the stored rule today. Per your note, that is handled as fail-closed: under require_registered an approval whose rules cannot be resolved is refused (PERMISSION_DENIED, outcome draft_unresolved), and under observe it is allowed and annotated so the gap shows up in the gateway log. ApproveAllDraftChunks without explicit approvals resolves every pending chunk, skipping security-flagged ones unless include_security_flagged. Tests cover resolved, missing and absent state for both RPCs; the README and the design note call out the dependency on the gateway populating current_state, which becomes an ask to NVIDIA.
| # The hydrator runs as a CronJob over the gateway's OCSF JSONL. Mount the file with extraVolumes | ||
| # and extraVolumeMounts (a hostPath or the gateway's log PVC) and point input at it. | ||
| hydrator: | ||
| enabled: false | ||
| image: | ||
| repository: ghcr.io/agentnameservice/ans-ocsf-hydrator | ||
| tag: "" | ||
| schedule: "*/15 * * * *" | ||
| input: /var/log/openshell/gateway-ocsf.jsonl |
There was a problem hiding this comment.
[P2] Feed the hydrator supervisor audit records
In the pinned OpenShell version, gateway JSONL contains gateway-origin events and explicitly does not archive supervisors’ structured records. For Linux sandboxes, the HTTP/network events this hydrator aggregates are emitted to supervisor JSONL or OCSF-JSON stderr output. Following this recipe therefore misses the sandbox traffic needed to produce behavioral observations. Please configure a collected supervisor feed and document enabling ocsf_json_enabled, updating the README and hydrator example consistently.
There was a problem hiding this comment.
You are right that the gateway's ocsf_log JSONL carries gateway-origin events only and never archives the supervisors' records, so the recipe as written would have produced no behavioral observations. Reworked in the next push: the README, the example config and the chart now describe enabling ocsf_json_enabled (openshell settings set --global --key ocsf_json_enabled --value true), collecting the supervisor containers' OCSF-JSON stderr lines or their /var/log/openshell-ocsf.*.jsonl files with the cluster's log collector into a volume the CronJob can read, and pointing hydrator.input at that feed (default path renamed to supervisor-ocsf.jsonl). The reader already accepts both the raw supervisor stderr line format and plain JSONL. The gateway JSONL is called out explicitly as not being the input.
| state := healthState{} | ||
| for { | ||
| next := healthState{authReady: auth.Ready(), unhealthy: registry.Unhealthy(), total: len(registry.Names())} | ||
| if next.ready() != state.ready() || !slices.Equal(next.unhealthy, state.unhealthy) { | ||
| srv.SetHealthy(next.ready()) |
There was a problem hiding this comment.
[P2] Publish the initial readiness state unconditionally
With jwks_url configured and no jwks_file, the verifier can initially hold no gateway keys while every identity’s artifacts are healthy. Both state.ready() and next.ready() are then false, and both unhealthy lists are empty, so this condition skips SetHealthy(false). However, grpcserver.New initializes readiness to true. The instance consequently reports ready while rejecting authenticated RPCs, indefinitely if the JWKS fetch keeps failing. Please publish the first computed health state unconditionally before comparing subsequent transitions.
There was a problem hiding this comment.
Fixed in c49a707, which landed after your review: grpcserver.New now starts not-ready (gRPC health NOT_SERVING, /readyz 503) and readiness is monotone once draining starts, and both binaries publish their first probe result unconditionally through Server.WatchReadiness before comparing later transitions. The case you describe is covered end to end: TestServeNotReadyWithoutGatewayKeys serves the middleware with a jwks_url-only verifier and asserts /readyz answers 503, and the probe is table-tested for the no-key, all-healthy case.
| if report.Parse.Events > 0 && report.Observations == 0 && !f.allowUnmapped { | ||
| return errNothingAttributed |
There was a problem hiding this comment.
[P2] Distinguish missing attribution from missing observations
Zero observations does not mean that no events mapped to an agent. A correctly attributed batch containing only process, lifecycle, or detection events intentionally produces no network observations. I reproduced this with one mapped process event: the CLI logged unmapped=0, agents=1, and observations=0, then exited 1 claiming that nothing could be attributed. This makes normal quiet batches fail and retry. Please base the mapping-failure check on attribution and allow mapped batches without network/HTTP activity to succeed as no-ops.
There was a problem hiding this comment.
Good catch; the exit rule conflated the two. The report now carries Attributed, the count of in-window events that mapped to a configured agent, and the CLI fails only when at least one event mapped to no agent and none mapped to any (Unmapped > 0 && Attributed == 0). A batch of mapped process, lifecycle or detection events with no network or HTTP activity succeeds as a no-op with observations=0, and a batch whose events all fall outside an explicit window also exits 0. The summary log includes attributed, and the rule is table-tested, including your single-mapped-process-event case.
… back off failed refreshes Readiness: grpcserver.New now starts not-ready and stays not-ready once Serve begins draining; Server.WatchReadiness applies an owner-supplied probe immediately and on every tick. Both binaries wire their probes through it, so a middleware replica that holds no gateway verification key at startup answers /readyz with 503 instead of advertising Ready while rejecting every RPC. Destination vetting: the interceptor binds ImportProviderProfiles and UpdateProviderProfiles. Custom provider profiles carry endpoints that the gateway composes into every attached sandbox's effective policy, so they are vetted like policy rules; the gateway example registers both RPCs and the README notes that built-in profiles cannot be vetted. Vetting cache: a refresh that fails while a valid verdict is cached defers the next refresh by the error TTL instead of retrying on every hit. Binaries: the config-to-service mapping is a tested function in each main; the interceptor rejects vetting.request_budget, which it never honoured. Release and CI: release.yml publishes the three chart images to ghcr.io at the released version, which the chart's defaults already reference, for linux/amd64 and linux/arm64; the Dockerfile cross-compiles for the target platform. The coverage gate is raised to 95%. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: kperry <kperry@godaddy.com>
…ttribution not observations Interceptor: ApproveDraftChunk and ApproveAllDraftChunks are bound. They install stored rules the request does not carry, so the extractors resolve the chunks from the evaluation's current_state (GetDraftPolicyResponse shape, which must carry the chunks field) following the gateway's own rules: pending chunks only, security-flagged ones only when requested. The pinned OpenShell gateway sends no state for these RPCs, so under require_registered an approval whose rules cannot be resolved is refused and under observe it is allowed and annotated draft_unresolved. The gateway example registers both RPCs. Hydrator: the exit rule failed any batch without observations, including correctly attributed batches of process, lifecycle or detection events. The report now counts attributed events; the job fails only when events mapped to no configured agent at all, or when a non-empty input yielded no OCSF events, which is what an export left disabled looks like. A new --window-last flag lets a scheduled run post only the trailing window, and the chart's CronJob uses it. Hydrator feed: the gateway's ocsf_log JSONL carries gateway-origin events only, so the README, example and chart now describe enabling ocsf_json_enabled and collecting the supervisors' OCSF-JSON stderr lines or JSONL files into the volume the CronJob reads. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Signed-off-by: kperry <kperry@godaddy.com>
csnitker-godaddy
left a comment
There was a problem hiding this comment.
Re-reviewed cb7c025. The four requested changes are addressed. Targeted race tests, the original regression probes, CLI checks, build, vet, and Helm validation pass; all eight CI checks are green. As documented, require-mode draft approvals remain unavailable until OpenShell supplies current_state.
| installable := func(c *openshellv1.PolicyChunk) bool { | ||
| return c.GetStatus() == draftStatusPending && (includeFlagged || c.GetSecurityNotes() == "") | ||
| } |
There was a problem hiding this comment.
[P3] Handle rejected chunks before enabling populated current_state
Before enabling populated current_state, handle rejected chunks in single-chunk approval. Upstream ApproveDraftChunk accepts both pending and rejected chunks, but this shared predicate only vets pending ones. With supplied state, I reproduced a rejected chunk being allowed with zero host lookups. This is not reachable through the pinned gateway, which supplies no state and is correctly denied today. Please include rejected chunks for single approval while retaining pending-only behavior for bulk approval.
Related issue
Fixes #2
Summary
Initial import of
ans-openshell: ANS identity for agents running in NVIDIA OpenShell.ans-openshell-middleware: OpenShell supervisor middleware (PRE_CREDENTIALS) that signs every allowed outbound request as the sandbox's registered ANS agent using ANS-6 Method B (DPoP proof plus the transparency-log receipt and status token). Callers are authenticated with the OpenShell extension JWT; identities are selected by policy and authorized per sandbox by the operator-ownedsandbox_identitiesmap.ans-openshell-interceptor: OpenShell gateway interceptor (validate) that vets the hosts of proposed network policies and custom provider profiles against ANS registration atCreateSandbox,UpdateConfig,SubmitPolicyAnalysis,EditDraftChunk,ApproveDraftChunk,ApproveAllDraftChunks,ImportProviderProfilesandUpdateProviderProfiles, in observe or require mode. Draft approvals resolve their stored rules from the evaluation'scurrent_state, which the pinned OpenShell gateway does not populate yet, sorequire_registeredrefuses them until it does. Both services start not-ready and assert readiness only once they hold a gateway verification key (and, for the middleware, a usable identity).ans-ocsf-hydrator: aggregates the sandbox supervisors' OCSF records (collectedOCSF-JSONstderr lines or JSONL, enabled withocsf_json_enabled) into signed Trust Index observations.ans-reference-callee: demo callee that verifies ANS-6 Method B callers with the SDK'spop.Middleware.third_party/openshell) with generated Go bindings, an in-process end-to-end demo (make demo), a Helm chart and Dockerfile, CI (coverage gate, lint, image and chart build, proto drift) and release automation (release-please and GoReleaser) matching the other agentnameservice repositories.Status: v0.1 research preview, built against the OpenShell protos recorded in
third_party/openshell/VERSIONand ANS SDK v0.1.19. OpenShell's extension contracts are themselves a research preview upstream.Testing
go build ./...,go vet ./...: cleango test -race ./...: 13 packages pass; library coverage 98.5% (make cover, gate in CI is 95%)golangci-lint run ./...: 0 issuesmake demo: 11 of 11 expectations hold (signed call accepted; replay, tampered body and unsigned call rejected; strip mode; fail-closed on a missing status token;require_registereddenial; observe-mode finding; cross-sandbox credential denied; unauthenticated call rejected)helm lintandhelm templatepass;buf generateproduces no diff againstgen/govulncheck ./...under Go 1.26.9: no vulnerabilitiesAI assistance
Assisted-by: Claude Code (claude-fable-5-1)
Checklist
git commit -s) certifying the DCO