Skip to content

feat: ANS identity extension for NVIDIA OpenShell - #1

Merged
kperry-godaddy merged 5 commits into
mainfrom
initial
Oct 20, 2026
Merged

kperry-godaddy merged 5 commits into
mainfrom
initial

Conversation

@kperry-godaddy

@kperry-godaddy kperry-godaddy commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

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-owned sandbox_identities map.
  • ans-openshell-interceptor: OpenShell gateway interceptor (validate) that vets the hosts of proposed network policies and custom provider profiles against ANS registration at CreateSandbox, UpdateConfig, SubmitPolicyAnalysis, EditDraftChunk, ApproveDraftChunk, ApproveAllDraftChunks, ImportProviderProfiles and UpdateProviderProfiles, in observe or require mode. Draft approvals resolve their stored rules from the evaluation's current_state, which the pinned OpenShell gateway does not populate yet, so require_registered refuses 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 (collected OCSF-JSON stderr lines or JSONL, enabled with ocsf_json_enabled) into signed Trust Index observations.
  • ans-reference-callee: demo callee that verifies ANS-6 Method B callers with the SDK's pop.Middleware.
  • Vendored OpenShell protocol definitions (Apache-2.0, 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/VERSION and ANS SDK v0.1.19. OpenShell's extension contracts are themselves a research preview upstream.

Testing

  • Two multi-perspective reviews after the import: a scoped re-review of the files the earlier review's fixes touched (2 HIGH blockers found and fixed here), then a verification review of those fixes (0 critical, 0 high)
  • go build ./..., go vet ./...: clean
  • go test -race ./...: 13 packages pass; library coverage 98.5% (make cover, gate in CI is 95%)
  • golangci-lint run ./...: 0 issues
  • make 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_registered denial; observe-mode finding; cross-sandbox credential denied; unauthenticated call rejected)
  • helm lint and helm template pass; buf generate produces no diff against gen/
  • govulncheck ./... under Go 1.26.9: no vulnerabilities
  • GoReleaser snapshot build of all four binaries succeeds

AI assistance

Assisted-by: Claude Code (claude-fable-5-1)

Checklist

  • The PR title follows Conventional Commits — release notes are generated from it
  • Tests cover the change
  • The linked issue above uses a closing keyword
  • Every commit is signed off (git commit -s) certifying the DCO

kperry-godaddy and others added 3 commits October 19, 2026 09:05
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 csnitker-godaddy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Requesting changes for the four P2 issues detailed in the inline comments.

Comment on lines +33 to +36
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),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread deploy/helm/ans-openshell/values.yaml Outdated
Comment on lines +73 to +81
# 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

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/ans-openshell-middleware/main.go Outdated
Comment on lines +171 to +175
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())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread cmd/ans-ocsf-hydrator/main.go Outdated
Comment on lines +116 to +117
if report.Parse.Events > 0 && report.Observations == 0 && !f.allowUnmapped {
return errNothingAttributed

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

kperry-godaddy and others added 2 commits October 19, 2026 14:43
… 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 csnitker-godaddy left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +172 to +174
installable := func(c *openshellv1.PolicyChunk) bool {
return c.GetStatus() == draftStatusPending && (includeFlagged || c.GetSecurityNotes() == "")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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.

@kperry-godaddy
kperry-godaddy merged commit 2096d09 into main Oct 20, 2026
8 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.

Initial import of the OpenShell ANS extension (v0.1)

2 participants