Skip to content

fix(policy): refresh pending proposals when the sandbox policy changes - #3923

Merged
johntmyers merged 4 commits into
NVIDIA:mainfrom
fede-kamel:fix/3884-refresh-pending-proposals
Oct 1, 2026
Merged

johntmyers merged 4 commits into
NVIDIA:mainfrom
fede-kamel:fix/3884-refresh-pending-proposals

Conversation

@fede-kamel

Copy link
Copy Markdown
Contributor

Summary

Approving pending policy proposals one after another always failed once with FAILED_PRECONDITION: proposal inputs changed; evaluation refreshed. After a policy change the gateway only reconciled proposals the new policy covered; the rest kept their old prover result and review token, so rule get showed a stale evaluation and the next approval refreshed it and failed. This PR re-evaluates the remaining pending proposals at every policy change, and makes the CLI explain the refresh when it still happens.

Related Issue

Closes #3884

Changes

  • openshell-server (grpc/policy.rs): new refresh_pending_chunk_evaluations re-evaluates every pending proposal against live inputs and persists the result. It reuses the cached prover result and reruns the prover only when the proposal's review token changed.
  • The refresh runs at every sandbox policy change: approve, approve-all and auto-approve (via reconcile_pending_chunks_after_policy_change), removal of an approved rule, undo, and both UpdateConfig policy writes.
  • Approval still rejects a review token that doesn't match the stored evaluation, so a reviewer (e.g. the TUI) holding a pre-refresh evaluation must refetch it. The re-review guarantee is unchanged; only the spurious first failure goes away.
  • openshell-cli: rule approve and rule approve-all turn the refresh precondition into an actionable error with a openshell rule get <sandbox> --status pending hint instead of the raw gRPC status.

Testing

  • mise run pre-commit passes
  • Unit tests added/updated
    • approval_refreshes_other_pending_proposals_for_sequential_review: approve A, then B's refreshed token (as rule get returns it) approves first time, and B's pre-refresh token is rejected with "refetch and review again".
    • The existing draft_receipts_replay_after_chunk_state_and_review_tokens_change covers the undo path (it failed until undo refreshed too).
    • draft_approval_error_explains_refreshed_evaluation for the CLI message.
  • cargo test -p openshell-server --features bundled-z3: 1861 passed. cargo test -p openshell-cli: all pass except ssh::tests::launch_editor_returns_friendly_error_when_binary_missing, which fails identically on unmodified main in this environment.
  • E2E tests added/updated (if applicable)

Checklist

  • Follows Conventional Commits
  • Commits are signed off (DCO)
  • Architecture docs updated (if applicable)

Approving, removing, or undoing a rule, or updating the sandbox policy,
changes the inputs every other pending proposal was evaluated against.
Only proposals the new policy covered were reconciled; the rest kept
their old prover result and review token. The review surface
(GetDraftPolicy) therefore showed a stale evaluation, and the first
approval of the next proposal refreshed it and failed with
FAILED_PRECONDITION, so approving proposals one after another always
failed once.

Re-evaluate the remaining pending proposals at each policy change,
reusing the cached prover result unless the proposal's inputs changed.
Approval still rejects a review token that does not match the stored
evaluation, so a reviewer holding a pre-refresh evaluation must still
refetch it.

When a refresh does happen at approval time (inputs changed between
fetch and approve), the CLI now explains that the rule was re-evaluated
and how to review it, instead of printing the raw gRPC status.

Closes NVIDIA#3884

Signed-off-by: fede-kamel <fkamelhar@gmail.com>
@copy-pr-bot

copy-pr-bot Bot commented Sep 30, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@johntmyers johntmyers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

PR Review Status

This is a valid, focused fix for #3884, but the new refresh path can lose a concurrent proposal edit and can turn an agent-controlled pending queue into unbounded synchronous work during policy mutations. The user-facing refresh guidance also needs the corresponding Fern documentation update described by the linked issue.

Action required: @fede-kamel, please address the two inline Warnings and update the relevant docs/ workflow pages (including navigation only if needed).

Blocking findings:

  • GATOR-64662972-01: stale refresh persistence can overwrite a newer proposal edit.
  • GATOR-64662972-02: an unbounded pending queue can amplify policy-mutation latency and the global mutation-lock hold time.
  • GATOR-64662972-03: the direct CLI workflow change is missing its Fern documentation update.

Carried findings:

  • None

Non-blocking suggestions:

  • None
Gator metadata
  • Validation: Project-valid concentrated policy correctness and CLI UX fix linked to reproducible issue #3884; no duplicate work found.
  • Docs: Missing for the direct rule approve / rule approve-all UX change; update the relevant Fern pages under docs/.
  • Checks: DCO and vouch gates pass; required branch, Helm, Trivy, and E2E workflows are not yet all dispatched for this head.
  • E2E: test:e2e will be required because the PR changes policy enforcement and gateway behavior, but dispatch waits until blocking review feedback is resolved.
  • Head SHA: 6466297265f699b1817f70a5d552c09acbff109e
  • Base SHA: 252882f37f2daa63ef4078b3bbb89adbcf43b409
  • Merge base SHA: 0ea0d3102089ebaa4e891a12389056f545b48415
  • Patch ID: c372705fa20f61d8c7182fb423feafab0b776380
  • Gator payload: 9
  • Review mode: initial
  • Previous reviewed SHA: none
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

Comment thread crates/openshell-server/src/grpc/policy.rs Outdated
Comment thread crates/openshell-server/src/grpc/policy.rs Outdated
@johntmyers johntmyers added the gator:in-review Gator is reviewing or awaiting PR review feedback label Sep 30, 2026
Store refreshed evaluations with a compare-and-swap: the store re-reads
the proposal, refuses when its rule name, proposed rule, or review token
changed since the evaluation read it, copies only the evaluation fields
onto the stored record, and updates only if the payload is still the one
it read. A refresh can no longer revert a concurrent edit or observation,
and the edit path uses the same guard against a concurrent refresh.

Bound each refresh to the 32 newest pending proposals; the rest keep the
approval-time recheck, which still refuses a stale review token. Operator
decisions (approve, approve-all, remove, undo) refresh before responding.
UpdateConfig, which holds the gateway-wide sandbox sync guard, and
agent-driven auto-approval refresh in a background task instead, one per
sandbox with later changes coalesced into a single rerun.

Refs NVIDIA#3884

Signed-off-by: fede-kamel <fkamelhar@gmail.com>
Explain that approving, removing, or undoing a rule rechecks the other
pending proposals so they can be approved one after another, when the
recheck is deferred or bounded, and what rule approve reports when a
proposal changed after it was listed. Show rule approve-all in Run Your
First Agent with its security-flag behavior.

Refs NVIDIA#3884

Signed-off-by: fede-kamel <fkamelhar@gmail.com>
@fede-kamel

Copy link
Copy Markdown
Contributor Author

Addressed all three gator findings:

  • GATOR-64662972-01 (stale refresh vs edit): compare-and-swap evaluation writes, 16efca8. Details in the inline thread.
  • GATOR-64662972-02 (unbounded mutation work): refresh bounded to 32 proposals and moved off the UpdateConfig / auto-approval request paths into a coalesced per-sandbox background task, 16efca8. Details in the inline thread.
  • GATOR-64662972-03 (docs): 2373b2b updates docs/how-it-works/policies/advisor.mdx (when proposals are rechecked, the bound, and what rule approve reports when a proposal changed after it was listed) and docs/about/run-your-first-agent.mdx step 4 (sequential approval and rule approve-all with its security-flag behavior). No navigation change needed.

mise run pre-commit passes; cargo test -p openshell-server --features bundled-z3: 1864 passed; mise run docs: 0 errors.

@johntmyers johntmyers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

Re-check After Author Update

Thanks @fede-kamel. I checked the compare-and-swap persistence, bounded/background refresh paths, regression coverage, and both Fern updates on the latest head. The stale-write and unbounded synchronous-work findings are resolved, and I resolved their Gator threads. One carried documentation/correctness obligation remains: the guide now says other policy updates refresh pending proposals in the background, but the full-policy UpdateConfig path used by openshell policy set returns after the matching atomic write before reaching the new background-refresh call. That can still leave rule get showing the stale candidate and make the next approval fail with the refresh precondition.

Action required: @fede-kamel, either schedule the background refresh before the full-policy matching-write return and add a regression test for that path, or narrow the documentation so it accurately preserves the approval-time recheck guidance for full policy replacements.

Blocking findings:

  • GATOR-64662972-03: the published proposal-review workflow is materially false for full sandbox policy replacement.

Carried findings:

  • GATOR-64662972-03: still open; no replacement finding or duplicate thread was created.
Gator metadata
  • Validation: Project-valid concentrated policy correctness and CLI UX fix linked to #3884.
  • Docs: Updated, but the background-refresh guarantee is inaccurate for full-policy UpdateConfig.
  • Checks: Current-head branch and Helm gates remain pending; required E2E dispatch waits until blocking review feedback is resolved.
  • E2E: test:e2e remains required because this changes policy enforcement and gateway behavior.
  • Head SHA: 2373b2b9e1ed6c02070cd944a3022f08db507950
  • Base SHA: 252882f37f2daa63ef4078b3bbb89adbcf43b409
  • Merge base SHA: 0ea0d3102089ebaa4e891a12389056f545b48415
  • Patch ID: 3ca02bc7184f1790f4a2ca81461d100567fa6698
  • Gator payload: 9
  • Review mode: follow_up
  • Previous reviewed SHA: 6466297265f699b1817f70a5d552c09acbff109e
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

A full policy UpdateConfig (openshell policy set) re-reads the latest
revision after its atomic write, finds the revision it just committed,
and returns before reaching the pending-proposal refresh at the end of
the handler. Pending proposals kept their stale evaluation, so rule get
showed the old candidate and the next approval failed with the refresh
precondition. Schedule the background refresh right after the commit.

Refs NVIDIA#3884

Signed-off-by: fede-kamel <fkamelhar@gmail.com>
@fede-kamel

Copy link
Copy Markdown
Contributor Author

Thanks, confirmed: after the full-policy atomic write, the handler re-reads the revision it just committed and returns through the matching-revision branch, so the refresh at the end of the handler was unreachable for openshell policy set.

GATOR-64662972-03 fixed in aea952b by scheduling the background refresh immediately after the commit, before that return. The dedup return earlier in the handler (policy unchanged) correctly schedules nothing. The later call stays for the fallthrough case, and the per-sandbox coalescing folds any duplicate into one rerun, so the docs' "other policy updates recheck pending proposals in the background" is now accurate.

Regression test full_policy_update_refreshes_pending_proposals drives handle_update_config with a full policy replacement and waits for the pending proposal's review token to change. I confirmed it fails with the new call removed (no refresh within 10 s) and passes with it.

mise run pre-commit passes; cargo test -p openshell-server --features bundled-z3: 1865 passed.

@fede-kamel

Copy link
Copy Markdown
Contributor Author

Probed live on Windows 11 + WSL 2 with Docker Desktop 4.71, using the reproduction from #3884. A sandbox opens two blocked connections (example.com:443, example.org:443), so two proposals are pending. Then A is approved, then B. Candidate is B's value in openshell rule get <sandbox> --status pending.

Step Released 0.1.2 This PR
B's candidate before approving A 85fb05cd5ee3 85fb05cd5ee3
rule approve A OK, policy v2 OK, policy v2
B's candidate after approving A 85fb05cd5ee3 (stale) 9d6e1ce484a0 (refreshed)
rule approve B Fails: FAILED_PRECONDITION … proposal inputs changed; evaluation refreshed, refetch and review again OK, policy v3, hash 9d6e1ce484a0
rule approve B again, identical command OK, policy v3 not needed

On 0.1.2, the identical retry succeeds without anyone looking at the refreshed proposal. With this PR, the applied policy hash equals the candidate rule get showed just before the approval.

This run predates aea952b, the policy set refresh fix, which is covered by full_policy_update_refreshes_pending_proposals.

@johntmyers johntmyers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

gator-agent

Re-check After Author Update

Thanks @fede-kamel. I checked your fix for the full-policy replacement path against the prior carried obligation: the background refresh is now scheduled immediately after the atomic policy commit and before the matching-revision return, and the new regression test exercises handle_update_config and observes the pending proposal token change. GATOR-64662972-03 is resolved, so no blocking findings remain.

Blocking findings:

  • No blocking findings remain.

Carried findings:

  • GATOR-64662972-03: resolved by the current-head scheduling fix and regression coverage.
Gator metadata
  • Validation: Project-valid concentrated policy correctness and CLI UX fix linked to #3884.
  • Docs: Fern guidance is now accurate for full-policy replacements and the sequential approval workflow.
  • Checks: Current-head DCO and Trivy gates are green; branch, Helm, and required E2E dispatch remain to be confirmed.
  • E2E: test:e2e is required because the PR changes policy enforcement and gateway behavior; dispatch follows this review.
  • Head SHA: aea952bc77d8abc6409cddccf5000b8a3886c637
  • Base SHA: 252882f37f2daa63ef4078b3bbb89adbcf43b409
  • Merge base SHA: 0ea0d3102089ebaa4e891a12389056f545b48415
  • Patch ID: b116e533cb08da488b231a2f38d515e889292dc9
  • Gator payload: 9
  • Review mode: follow_up
  • Previous reviewed SHA: 2373b2b9e1ed6c02070cd944a3022f08db507950
  • Review budget exhausted: no
  • Maintainer decision required: no
  • Next state: gator:in-review

@johntmyers johntmyers added the test:e2e Requires end-to-end coverage label Sep 30, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied, but pull-request/3923 does not exist yet. A maintainer needs to comment /ok to test aea952bc77d8abc6409cddccf5000b8a3886c637 to mirror this PR. Once the mirror exists, re-apply the label or re-run Branch E2E Checks from the Actions tab.

@johntmyers

Copy link
Copy Markdown
Collaborator

/ok to test aea952b

@johntmyers johntmyers added gator:watch-pipeline Gator is monitoring PR CI/CD status and removed gator:in-review Gator is reviewing or awaiting PR review feedback labels Sep 30, 2026
@fede-kamel

Copy link
Copy Markdown
Contributor Author

Went through the E2E failures across the three attempts of run 36726439249. Each attempt failed a different job, each time while starting a sandbox, and each failed job passed on another attempt with the same head (aea952b):

Attempt Failed job Failure
1 Kubernetes E2E, Agent Sandbox v1beta1 sandbox create: timed out waiting for gated workload Pod
2 Kubernetes E2E, Agent Sandbox v1alpha1 Same timeout (v1beta1 passed)
2 Integration, fedora-podman-rootless, e2e-podman sandbox_from_explicit_nvidia_ubuntu_image: relay open timed out
3 Conformance, ubuntu-docker-rootful file-transfer/git-filtering: first sandbox create of the run failed after 34.5 s, still pulling nvcr.io/nvidia/base/ubuntu:24.04

On attempt 3 both Kubernetes jobs and the Podman job passed.

Why I don't think this PR causes them:

  • The Kubernetes timeout is wait_for_bootstrap_workload_pod (crates/openshell-driver-kubernetes/src/driver.rs:2028): the driver polls for the Pod the Agent Sandbox controller creates from the Sandbox CR, for KUBE_API_TIMEOUT (30 s). This PR doesn't touch the Kubernetes driver or openshell-core, and the proposal refresh takes no locks and doesn't run on the create path. mechanistic-proposal/create was just the second sandbox the suite creates, before any proposal logic ran.
  • The Podman and Docker failures are plain sandbox startup and image pull, with no policy or proposal involvement.
  • mechanistic_proposal, the conformance test that exercises this change, passed in the same Docker job that failed git_filtering, and in every other job that ran it.

Could someone rerun the failed conformance job?

@johntmyers johntmyers added test:e2e Requires end-to-end coverage and removed test:e2e Requires end-to-end coverage labels Sep 30, 2026
@github-actions

Copy link
Copy Markdown

Label test:e2e applied for aea952b. Open the existing run and click Re-run all jobs to execute with the label set. The run will execute the standard E2E suite after building the required gateway, sandbox, and supervisor images once. The matching required CI gate status on this PR will flip green automatically once the run finishes.

@johntmyers johntmyers added gator:approval-needed Gator completed review; maintainer approval needed and removed gator:watch-pipeline Gator is monitoring PR CI/CD status labels Sep 30, 2026
@fede-kamel

Copy link
Copy Markdown
Contributor Author

Follow-up to the CI analysis above: I also ran the full conformance suite against this PR's head outside CI, and attempt 4 has since passed here too.

Setup: a fresh Ubuntu 24.04 VM (12 vCPU, 24 GB, Docker 29.1.3), a release build of aea952b (openshell 0.1.3-dev.28+gaea952bc7), the Docker driver with the published :dev supervisor and sandbox images, and openshell-conformance run for five rounds back to back. Round 1 started with nothing cached, matching the job that timed out while pulling images.

Round Duration smoke sandbox-lifecycle file-transfer mechanistic-proposal new-hostname-proposal policy-local
1 (cold) 67 s pass pass pass pass pass pass
2 60 s pass pass pass pass pass pass
3 60 s pass pass pass pass pass pass
4 60 s pass pass pass pass pass pass
5 59 s pass pass pass pass pass pass

30 of 30 scenario runs passed, including file-transfer (attempt 3's failure) and both proposal scenarios.

@johntmyers
johntmyers added this pull request to the merge queue Oct 1, 2026
Merged via the queue into NVIDIA:main with commit 71440b2 Oct 1, 2026
290 of 304 checks passed
@johntmyers

Copy link
Copy Markdown
Collaborator

gator-agent

Monitoring Complete

Monitoring is complete because this PR has merged.

Final status: Gator review feedback was resolved, maintainer approval was present, and the required branch, Helm, Trivy, and E2E checks passed before merge.

I removed the active gator:* label because there is nothing left for gator to monitor on this PR.

@johntmyers johntmyers removed the gator:approval-needed Gator completed review; maintainer approval needed label Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

test:e2e Requires end-to-end coverage

Projects

None yet

Development

Successfully merging this pull request may close these issues.

ux(cli): sequential rule approve fails with raw FAILED_PRECONDITION after proposals refresh

2 participants