You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The buyer already has a fail-closed path-scope gate on the pay path, but it is a per-home setting, not a per-offer one. A buyer running two contribution jobs with different scopes cannot express either scope at post_job time. Measured at main = c1338504.
What exists today
crates/maxplayer-core/src/contribution.rs:275-282 — ContentPolicy { allowed_paths, forbidden_paths, max_diff_bytes }, evaluated fail-closed against the fork-vs-base changed paths (evaluate, :291). Empty/out-of-scope/forbidden/too-large ⇒ refuse.
crates/maxplayer-core/src/authorize_pay.rs:797-806 — contribution_policy(home) resolves it from home.config.contribution, else the FLOOR (refuse only empty diffs). Buyer-side, never seller-influenced. Runs before spend.
crates/maxplayer-core/src/home.rs:1433-1445 — ContributionPolicyConfig, the [contribution] table in the buyer home config; env seam MAXPLAYER_CONTRIBUTION__ALLOWED_PATHS (home.rs:46).
contribution.rs:271-272, the doc comment says it outright: "Path-scope lives here (the offer table has NO paths tag)".
crates/maxplayer/src/mcp.rs:198-262 — the post_job input schema: task, output, amount_sats, max_sats, harness, model, harness_family, capabilities, seller_pubkey, untargeted, deadline_unix, repo, branch, target_repo_owner, target_repo_url, base_branch, base_oid, accepts. additionalProperties: false. No path field of any kind.
So the only way to scope a job's paths is to edit the buyer home's config before posting, and that setting then applies to every contribution job that buyer settles until it is edited again.
Why it matters — the job that surfaced it
#956 is a docs-only fix whose scope is "touch crates/buzz/** comments and NOTICE; never touch web/network/test/fixtures/**" (signed wire fixtures — one changed byte breaks id and sig). That is exactly a forbidden_paths rule. Today it can only be stated as prose in task, which nothing enforces before payment: a seller that "helpfully" rewrites the fixtures is paid in full, and the refusal happens at human review, after the sats are gone.
With a second contribution job live on the same buyer (e.g. one under crates/maxplayer/**), a home-wide allowed_paths for #956 would refuse the other job's honest delivery. Per-home is the wrong granularity once a buyer runs more than one job.
Proposed change
Three optional fields on post_job (and the CLI/daemon post path that feeds OfferDraft): allowed_paths: [string], forbidden_paths: [string], max_diff_bytes: integer. Same semantics as ContentPolicy, prefix match on changed paths.
In authorize_pay, resolve the effective policy as home policy tightened by offer policy: allowed_paths = intersection semantics (offer list narrows, never widens, the home list when the home list is non-empty); forbidden_paths = union; max_diff_bytes = min. The offer can only make the gate stricter than the operator's floor, so a mis-authored offer can never loosen a home rule.
Absent fields ⇒ today's behaviour exactly. Existing offers and existing homes unchanged.
Refusal surfaces as the existing ContentRefusal::OutOfScope / Forbidden / TooLarge (contribution.rs:344+), zero spend, journaled — no new gate class. This is the gate that already exists, given a per-job knob.
It is not a quality gate and does not claim to be. An in-scope worthless diff still passes; the grader owns quality.
I have not run the buyer daemon to prove the home-level policy fires end to end; the claim that it does is from reading authorize_pay.rs, not from a settled job with [contribution] set. Someone with a checkout should confirm with the existing tests at contribution.rs:766 and delivery_git.rs:1383 before sizing this.
Filed by maxie (forge v2 domain lead) under the shared maxy-player credential, on Bob's order 2026-09-01 21:39 PDT.
The buyer already has a fail-closed path-scope gate on the pay path, but it is a per-home setting, not a per-offer one. A buyer running two contribution jobs with different scopes cannot express either scope at
post_jobtime. Measured atmain=c1338504.What exists today
crates/maxplayer-core/src/contribution.rs:275-282—ContentPolicy { allowed_paths, forbidden_paths, max_diff_bytes }, evaluated fail-closed against the fork-vs-base changed paths (evaluate,:291). Empty/out-of-scope/forbidden/too-large ⇒ refuse.crates/maxplayer-core/src/authorize_pay.rs:797-806—contribution_policy(home)resolves it fromhome.config.contribution, else the FLOOR (refuse only empty diffs). Buyer-side, never seller-influenced. Runs before spend.crates/maxplayer-core/src/home.rs:1433-1445—ContributionPolicyConfig, the[contribution]table in the buyer home config; env seamMAXPLAYER_CONTRIBUTION__ALLOWED_PATHS(home.rs:46).contribution.rs:271-272, the doc comment says it outright: "Path-scope lives here (the offer table has NO paths tag)".crates/maxplayer/src/mcp.rs:198-262— thepost_jobinput schema:task, output, amount_sats, max_sats, harness, model, harness_family, capabilities, seller_pubkey, untargeted, deadline_unix, repo, branch, target_repo_owner, target_repo_url, base_branch, base_oid, accepts.additionalProperties: false. No path field of any kind.So the only way to scope a job's paths is to edit the buyer home's config before posting, and that setting then applies to every contribution job that buyer settles until it is edited again.
Why it matters — the job that surfaced it
#956 is a docs-only fix whose scope is "touch
crates/buzz/**comments and NOTICE; never touchweb/network/test/fixtures/**" (signed wire fixtures — one changed byte breaksidandsig). That is exactly aforbidden_pathsrule. Today it can only be stated as prose intask, which nothing enforces before payment: a seller that "helpfully" rewrites the fixtures is paid in full, and the refusal happens at human review, after the sats are gone.With a second contribution job live on the same buyer (e.g. one under
crates/maxplayer/**), a home-wideallowed_pathsfor #956 would refuse the other job's honest delivery. Per-home is the wrong granularity once a buyer runs more than one job.Proposed change
post_job(and the CLI/daemon post path that feedsOfferDraft):allowed_paths: [string],forbidden_paths: [string],max_diff_bytes: integer. Same semantics asContentPolicy, prefix match on changed paths.OfferDraft::to_event_draftand parsed intoParsedOffer(same pattern buyer-stated acceptance block as a LENIENT SHAPE FLOOR, checked before spend (under the grader, not instead of it) #822 proposes for its acceptance tags). The seller can read them — that is a feature: a seller can decline a job whose scope it cannot honour, instead of delivering out-of-scope work nobody will pay for.authorize_pay, resolve the effective policy as home policy tightened by offer policy:allowed_paths= intersection semantics (offer list narrows, never widens, the home list when the home list is non-empty);forbidden_paths= union;max_diff_bytes= min. The offer can only make the gate stricter than the operator's floor, so a mis-authored offer can never loosen a home rule.ContentRefusal::OutOfScope/Forbidden/TooLarge(contribution.rs:344+), zero spend, journaled — no new gate class. This is the gate that already exists, given a per-job knob.Bounds
REVIEW.mdarrive). This is path scope on the diff, and the evaluator for it is already shipped; only its input source is missing.authorize_pay.rs, not from a settled job with[contribution]set. Someone with a checkout should confirm with the existing tests atcontribution.rs:766anddelivery_git.rs:1383before sizing this.Filed by maxie (forge v2 domain lead) under the shared
maxy-playercredential, on Bob's order 2026-09-01 21:39 PDT.