v5 design: staged patch rollout (socket.yml + scan limit) - #290
Mikola Lysenko (mikolalysenko) merged 6 commits into
Conversation
Adds the design for rolling Socket patches out gradually: a `patches:` block in socket.yml that narrows what scan may patch (paths, ecosystems, packages, severity floor, on/off), and a severity-ordered per-run cap on new patches so each scan lands the next few most critical fixes. The plan splits the work into two parallel items with a frozen interface, lists every hard-coded filter and where it belongs, and covers the depscan autopatch follow-up. configuration.md now records that socket-patch reads socket.yml for selection policy only, with the trust boundary unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Three independent reviews (ambiguity, churn, trust boundary) found gaps that would have let the two implementations disagree or let a repo file widen or stall the rollout. The plan now: - matches paths against marker files with the backend's top-down gitignore rules, so projectIgnorePaths means the same everywhere - uses one data source for severity, supersession and ordering - spends the budget only on patches the planning pass proves can land, and admits nothing new when a lookup failed - uses the merged recorded view in every mode and engine - has depscan read the policy from the base commit for PR jobs - hardens file handling (regular files, aliases, size, encoding, trusted repo root) and reports what a policy hides Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A final consistency pass found places where the two work items would have produced incompatible code: base purls admitted in one directory being charged again in the next, no defined hand-off of the unfiltered offers from the severity filter to classification, no shared repo-relative path helper, and override sources the JSON must report but the interface could not carry. The shared contract now defines each of these, and the parity tests match each engine's budget scope. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The plan said both that the selector returns the shared offers struct (work item A) and that it returns admitted/deferred rows (work item B). The selector now returns the offers, and B adds the rollout stage after it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[agent] plan final Summary. The staged-rollout plan is final at
Updated at Below are the work-item specs A and B in full (§9, verbatim). The sections they cite (§3–§5 semantics, §7.2 engine API) are normative; read them in Work items A and B (§9, verbatim)9.0 Shared contract (frozen by this plan)Scan pipeline, in order (disk and memory):
// crates/socket-patch-core/src/policy/mod.rs — OWNER A
pub struct SelectionPolicy { /* private fields */ }
pub enum PolicySource { None, File { path: String, sha256: String }, Bypassed }
pub enum FilterReason {
Disabled, PathExcluded { pattern: String, list: &'static str }, PathNotIncluded,
Ecosystem, PackageNotListed, PackageIgnored { spec: String },
Severity { found: Option<String>, floor: String },
}
impl FilterReason { pub fn code(&self) -> &'static str; pub fn detail(&self) -> String; }
pub enum PolicyError {
Invalid { file: String, key: String, message: String },
Ambiguous { files: [String; 2] },
}
impl PolicyError { pub fn code(&self) -> &'static str; } // socket_yml_invalid | socket_yml_ambiguous
pub enum RootFile { Absent, Present(Vec<u8>), PresentWithoutContent }
pub trait PolicyFs { fn read_root_file(&self, name: &str, cap: usize) -> std::io::Result<RootFile>; }
pub enum OverrideSource { Flag, Env }
pub struct PolicyOverrides { pub bypass: bool, pub min_severity: Option<(Option<u8>, OverrideSource)> } // (None, _) = "none"
pub struct PolicyWarning { pub code: &'static str, pub detail: String }
pub struct Root<'a> { pub rel_dir: &'a str, pub markers: &'a [String], pub explicit: bool }
impl SelectionPolicy {
pub fn unrestricted() -> Self; // built-in default ignores only
pub fn load(fs: &dyn PolicyFs, o: &PolicyOverrides) -> Result<(Self, Vec<PolicyWarning>), PolicyError>;
pub fn source(&self) -> &PolicySource;
pub fn enabled(&self) -> bool;
pub fn admits_root(&self, root: &Root) -> Result<(), FilterReason>;
pub fn admits_purl(&self, purl: &str) -> Result<(), FilterReason>; // ecosystem + packages
pub fn admits_severity(&self, severity_order: u8) -> Result<(), FilterReason>;
pub fn max_new_patches(&self) -> Option<u32>; // the file's value; None when source() is None or Bypassed, or the key is absent
}
pub fn package_spec_matches(spec: &str, purl: &str) -> bool; // moved from cli scan/mod.rs:383
pub fn find_repo_root(cwd: &Path) -> PathBuf; // 4.5
pub fn repo_relative(repo_root: &Path, dir: &Path) -> String; // "" for the repo root, `/` separators
// crates/socket-patch-core/src/policy/mod.rs — OWNER A (the step 5 → 7 seam)
pub struct Offers {
pub unfiltered: BTreeMap<String, Vec<PatchSearchResult>>, // purl → every offer (after tier)
pub selected: BTreeMap<String, PatchSearchResult>, // purl → winner among floor-admitted offers
}
// crates/socket-patch-core/src/rollout.rs — OWNER B
pub enum Recorded { None, Same, Kept { uuid: String }, Superseded { old_uuid: String } }
pub struct Candidate {
pub project: String, pub purl: String, pub base_purl: String, pub uuid: String,
pub ecosystem: &'static str, pub severity_order: u8, pub advisory_count: usize,
pub recorded: Recorded, pub eligible: bool, pub in_flight: bool,
}
pub enum MaxNewSource { Flag, Env, File, Default, Cap }
pub struct MaxNew { pub value: Option<u32>, pub source: MaxNewSource }
pub fn resolve_max_new(flag: Option<Option<u32>>, env: Option<Option<u32>>,
file: Option<u32>, cap: Option<u32>) -> MaxNew;
pub fn canonical_base_purl(purl: &str) -> String;
pub fn rollout_cmp(a: &Candidate, b: &Candidate) -> std::cmp::Ordering;
pub struct RolloutCounts { pub new: u32, pub deferred: u32, pub upgrade: u32, pub already: u32 }
pub struct RolloutPlan {
pub admitted: Vec<Candidate>, pub deferred: Vec<(Candidate, u32)>,
pub counts: RolloutCounts,
pub remaining: Option<u32>, // carried to the next directory
pub admitted_base_purls: BTreeSet<String>, // carried too
}
// Rows whose base_purl is in `already_admitted` are admitted without spending budget.
pub fn plan_rollout(candidates: Vec<Candidate>, max_new: &MaxNew, incomplete: bool,
already_admitted: &BTreeSet<String>) -> RolloutPlan; // pure
// crates/socket-patch-core/src/api/ranking.rs — OWNER B (addition)
pub fn search_result_supersedes(candidate: &PatchSearchResult, recorded: &PatchSearchResult) -> bool;Rules both items follow:
9.1 Work item A — socket.yml loading and filteringScope:
Tests:
Docs (A): 9.2 Work item B — limit, ordering, reportingScope:
Tests:
Docs (B): 9.3 Integration (B, after rebasing on A)
Generated by Claude Code |
|
[agent] CI note: This PR changes only three Markdown files under I don't know of an existing fix to port. I'll re-run the failed job once, after this CI run finishes. Generated by Claude Code |
|
#283 landed on release/v5-prerelease as 06437d2; please merge origin/release/v5-prerelease again, resolve conflicts, get green, and keep it ready. Generated by Claude Code |
…taged-rollout-plan
|
[agent] The same target failed on Windows in base PR #283's own last CI run (job 108911094705), and #283 is now in Generated by Claude Code |
|
[agent] ready to land The plan is final and CI is green except Generated by Claude Code |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix is ON. A cloud agent has been kicked off to fix the reported issue.
Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit c434039. Configure here.
Bugbot on #290: the plan had the in-memory engine's path selector apply only the built-in test/fixture ignores, because it runs before socket.yml is read. A negation such as `!/e2e/tests/` could then never bring those trees back in depscan, while it works on disk. Selection is now two-phase: the caller fetches the root policy file(s) first and passes their text to selectHostedScanPaths, which applies the full path policy. A listed policy file that is not passed fails closed. A new memory test covers the negation case. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[agent] Plan amendment for work item A, at In-memory path selection is now two-phase. depscan fetches the root Generated by Claude Code |
0f2de18
into
release/v5-prerelease
…etup-and-ui Take the base's configuration.md deferred-defaults paragraph, which already accounts for `setup` being removed in v5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PQaKzoW5dSw9u5pgAAvVRj
…ed-engine Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KHZ8uzdXfkG2zH8ZYDG8ju
Brings in the squashed staged-rollout plan (#290), the e2e-tier fix (#288: Windows path separators in a scan test, CI cancel rules) and the docs-only design PRs. The plan doc's later edits (two-phase policy selection for the in-memory engine) merge with this branch's implementation notes unchanged. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Brings in the merged staged-rollout plan (#290), the Windows scan test fix (#288) and the docs-only design PRs. The plan's late two-phase memory selection is implemented in the next commit. Claude-Session: https://claude.ai/code/session_01BKsyzefGhAnPkYmXCwq3H3 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
[agent] Work item B status: #294 (
Generated by Claude Code |
|
[agent] Work item A (socket.yml patch config and filtering) is ready to land as #293. It targets What it adds
Decisions where the plan left a gap: recorded in
CI: green except failures the base branch already has:
No fix exists yet for either. Generated by Claude Code |

Design only; no code. This PR adds
docs/design/staged-rollout.md, updatesdocs/design/configuration.md(socket-patch now reads socket.yml for selection policy, with the trust boundary kept), and adds a WS9 entry todocs/design/v5-plan.md.What the plan proposes
patches:block in socket.yml (version: 2, new top-level key; every existing parser strips or ignores it). Keys:enabled,includePaths,ignorePaths,ecosystems,packages,ignorePackages,minSeverity,maxNewPatches. Paths use the same gitignore semantics asprojectIgnorePaths, which scan now also honors.test/tests/fixtures/__fixtures__/testdataroot exclusions become overridable default ignores, applied on disk too. Every other hard-coded skip is safety, entitlement or correctness and stays (inventory in section 2.3, both repos).scan --max-new-patches <N|none>(and themaxNewPatcheskey): caps patches added to packages that have none yet. Order: real severity, then advisory count, ecosystem, purl, uuid. Upgrades are exempt. Patches that can't land this run don't hold a slot. Repeated runs converge. Deferred patches are reported in a newrolloutJSON block; exit code unchanged.socket_yml_invalid, exit 1). Narrowing never removes or upgrades an already-applied patch.Candidate designs came from three angles (minimal, safety, user journey). Three independent judges scored them, and the result is a synthesis. Section 8 records each decision and why.
🤖 Generated with Claude Code
Generated by Claude Code
Note
Low Risk
Documentation-only; no runtime or behavioral changes until follow-up implementation PRs land.
Overview
Design-only — no implementation in this PR. It adds the v5.0 staged rollout spec and aligns configuration docs with that direction.
New:
docs/design/staged-rollout.mddefines repo-owned gradual patching: a top-levelpatches:block insocket.yml(underversion: 2) for paths, ecosystems, packages, severity floor,enabled, andmaxNewPatches;scan --max-new-patchesfor severity-ordered caps on new patches (upgrades exempt); fail-closed validation;policy/rolloutJSON blocks; and two parallel work items (A policy, B limit) with a frozen pipeline contract and depscan follow-ups.Updated
configuration.md: Reverses the v3.5 rule that socket-patch never readssocket.yml— v5 will readprojectIgnorePathspluspatchesonly, with a trust boundary that repo files may narrow or pace but never widen or carry credentials/endpoints. Invalidpatchesblocks fail scan (exit 1); manifestsetup.defaultsis dropped from the deferred plan in favor ofsocket.yml.Updated
v5-plan.md: Adds WS9 (branchesv5/rollout-policy/v5/rollout-limit, merge A then B).Reviewed by Cursor Bugbot for commit c434039. Configure here.