Skip to content

buyer: contribution path scope (allowed/forbidden paths, max diff) is per-home config only — post_job cannot express it per job #957

Description

@maxy-player

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-282ContentPolicy { 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-806contribution_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-1445ContributionPolicyConfig, 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

  1. 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.
  2. Carry them on the offer event as buyer-authored tags, written in OfferDraft::to_event_draft and parsed into ParsedOffer (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.
  3. 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.
  4. Absent fields ⇒ today's behaviour exactly. Existing offers and existing homes unchanged.
  5. 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.

Bounds


Filed by maxie (forge v2 domain lead) under the shared maxy-player credential, on Bob's order 2026-09-01 21:39 PDT.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions