Skip to content

Gap: no pre-sign policy validation between "authorized to act" and "should this action execute" #96

Closed

Description

@Eras256

Right now x402Serve() checks that a request carries a valid, correctly signed payment. It doesn't check anything about whether that specific action should go through in the first place — who's acting, how much value is at risk, whether the destination is one the account owner would actually approve, or any rule beyond "is this signature valid."

Those are two different questions. Holding a valid signing key proves you can request an action. It doesn't prove that action should execute under the rules the account owner actually wants.

We don't have an answer for this yet, just the gap. If you're interested, a design doc or a set of adversarial test cases for a pre-sign policy layer (something like an allow/warn/wait/deny model, evaluated against actor identity, value, destination, and exposure, before the payment settles) would be genuinely useful here. No commitment on our side or yours — just an open problem worth working through in public.


🤖 Drafted with Claude Code

Activity

  1. CodeDeityX commented on Sep 29, 2026

    @CodeDeityX

    Agent-side pre-sign policy — first integrated result

    I mapped the three cases we discussed fragmentation, concurrent requests against the same observed capacity, and self-escalation against Nirium’s actual agent-side x402 signing path.

    The scope of this first pass is intentionally narrow: owner-defined policy, spending authority separated from policy administration, and enforcement at the agent-side pre-sign boundary. x402Serve() is out of scope here, and Nirium is not treated as the policy owner or custodian.

    I think it is important to distinguish the guarantees from the beginning.

    G1 — Pre-sign integrity

    Before Nirium emits an authorization, the exact authorization being considered must be evaluated against the owner’s policy.

    For the cases tested here:

    • ALLOW may reach the signer only for the authorization that was evaluated.
    • DENY must not reach the signer.
    • WAIT must not reach the signer.
    • policy failure must not silently fail open.
    • a late decision must not produce a signature after the decision deadline.
    • a decision produced for one authorization must not authorize another.

    G2 — Account enforcement

    Even if Nirium is bypassed entirely, the spending credential must still be unable to execute consequences outside the owner’s policy.

    G2 is a different security claim and is not proven by G1.

    Integrated G1 result

    G1 has now been exercised against the real Nirium/x402 signing path in a controlled harness.

    Nirium upstream baseline: 58/58 PASS
    G1 integration:           25/25 PASS
    U0 isolated adapter:      24/24 PASS
    

    The integrated run exercised:

    Agent.initX402()
    → Agent.x402Fetch()
    → @x402/fetch
    → @x402/stellar
    → Stellar authorization construction
    → pre-sign policy adapter
    → instrumented signer
    

    HTTP transport, RPC, ledger timing, and policy state were deliberately simulated so the adversarial cases could be controlled and reproduced.

    No real key was used.

    No transaction was broadcast.

    No production Nirium source change was required for the integration.

    The U0 adapter tests remain separate from the G1 integration result; they are not being counted as proof of Nirium integration.

    Adversarial cases

    1. Fragmentation

    An individually acceptable payment should not automatically be treated as economically independent from previous payments.

    Example:

    per-action limit:        500
    aggregate exposure cap:  2000
    
    499
    499
    499
    499
    ----
    1996
    
    next request: 499
    

    The next request is still below the per-action limit, but would push aggregate exposure above the owner’s policy.

    In the G1 harness, when the controlled policy rejected that next fragmented request, the authorization did not reach the signer.

    This demonstrates the pre-sign policy path.

    It does not yet prove that an account enforces the same aggregate exposure rule if Nirium is bypassed.

    2. Concurrent requests

    Two requests can observe the same apparent remaining capacity at nearly the same time.

    For example:

    remaining capacity = 400
    
    request A = 300
    request B = 300
    

    A simple read of 400 available does not reserve that capacity.

    The G1 tests demonstrated that concurrent requests remain bound to their own policy decisions, authorization inputs, and signer results.

    No cross-request decision reuse was observed.

    However, this does not solve shared pending-capacity reservation.

    Two requests may still observe the same available capacity before either one consumes it.

    That limitation remains explicit rather than being hidden inside a G1 PASS.

    3. Self-escalation

    A spending authority should not automatically inherit the authority to modify the policy that constrains it.

    In the G1 harness, a spending credential requesting a policy-expansion / administrative action was rejected before signing.

    That means the pre-sign integration can distinguish the economic action from the authority required to change policy.

    This remains a G1 boundary result only.

    It does not yet prove that the same spending credential cannot attempt the administrative action through another client or execution path.

    That belongs to account-level enforcement.

    Explicitly unresolved

    L01_PENDING_CAPACITY_RESERVATION = NOT_SOLVED
    
    L02_OUT_OF_PROCESS_SIGNER_BYPASS = NOT_SOLVED
    
    G2_ONCHAIN_ENFORCEMENT = NOT_RUN
    

    The distinction matters:

    A pre-sign policy adapter can prevent Nirium from emitting an authorization.

    That is not the same claim as making the policy unavoidable at the account layer.

    Next adversarial case: stale ALLOW / TOCTOU

    One additional case I think belongs directly in the pre-sign model is a stale ALLOW.

    For example:

    T0:
    policy v7 evaluates request A
    → ALLOW
    
    T1:
    owner changes policy
    or revokes the spending authority
    or relevant policy state changes
    
    T2:
    the old ALLOW arrives at the signing boundary
    

    The question is:

    Can an approval produced under an old policy/state still produce a signature after that policy/state is no longer authoritative?

    I do not think ALLOW should behave like an indefinite reusable approval.

    A future version of the interface should therefore be able to bind a decision not only to the exact authorization context, but also to the policy/state under which that decision was produced and to an explicit validity boundary.

    The exact API fields are still a design question; the invariant is more important than the field names.

    Possible economic-authority conformance profile

    Longer term, I think #96 could define a small conformance model so that different policy engines do not all mean something different when they claim an agent is “policy protected.”

    For example:

    G1 — PRE-SIGN
    
    Forbidden authorization is not emitted by the Nirium signing path.
    
    
    G2 — ACCOUNT ENFORCEMENT
    
    Bypassing Nirium still cannot execute the forbidden consequence.
    
    
    G3 — PENDING CAPACITY
    
    Outstanding authorizations cannot collectively oversubscribe
    the authority/budget the owner intended to expose.
    
    
    G4 — GOVERNANCE
    
    Spending authority cannot modify the rules that constrain it.
    

    Only G1 is claimed/tested in this result.

    The purpose of separating these levels is to prevent a local pre-sign check from being described as if it were equivalent to account-level or concurrency-safe enforcement.

    Optional policy-decision receipt

    I also think it may be useful to keep room for a small, non-secret policy-decision receipt.

    For example:

    decisionId
    contextHash
    policyRef
    policyVersion
    decision
    reason
    observedStateRef
    issuedAt
    expiresAt
    

    This would not be a reusable payment authorization and should not contain private keys or secrets.

    Its purpose would be evidence:

    what exact context was evaluated, under which policy/state, what decision was returned, and why.

    That could make policy decisions easier to reproduce, debug, audit, and explain without making Nirium the policy authority.

    Design boundary

    The policy integration should remain engine-neutral.

    Nirium’s responsibility would be to expose and preserve a trustworthy pre-sign integration point.

    The owner chooses the policy engine and authority model.

    A future account-level implementation can provide G2 independently of Nirium’s servers, while a stronger admission model can address G3 separately.

    That keeps:

    signing integration
    ≠
    policy ownership
    ≠
    account enforcement
    

    and prevents those guarantees from being collapsed into one ambiguous “protected” claim.

    If this direction matches what you had in mind for #96, my next step can be to clean up the design and adversarial harness for review, while keeping G2/G3/G4 as explicit follow-on guarantees rather than silently expanding the G1 claim.

  2. Eras256 commented on Sep 29, 2026

    @Eras256
    CollaboratorAuthor

    Thanks @CodeDeityX, this is the shape I hoped #96 would take. Keeping G1 separate from G2, and writing L01/L02/G2 down as NOT_SOLVED/NOT_RUN instead of folding them into a PASS, is what makes this useful.

    My vote for the next step is to clean up the harness for review first. Right now the 58/58 and 25/25 can't be reproduced by anyone else, and that's what turns this from a claim into evidence.

    To keep this issue finishable, I'd scope it to three things for now:

    1. The harness, public and runnable by anyone.
    2. Stale ALLOW / TOCTOU inside G1: an ALLOW bound to the policy version it was produced under, with an expiry.
    3. On Nirium's side, exposing the pre-sign point as a supported, documented hook, so the adapter doesn't depend on instrumenting the signer. We'll pick this up once the harness is public.

    G2 to G4 and the decision receipt stay written down here as follow-ups. Not dropped, just not started yet.

    Disclosure: drafted with AI assistance under my direction and reviewed by hand.

  3. guptakumarranjeet150 commented on Sep 30, 2026

    @guptakumarranjeet150

    Hello! I would like to work on this issue.

    I would love to take this on. My planned approach:

    • Implement Gap: no pre-sign policy validation between "authorized to act" and "should this action execute" with modular design, clear documentation, and clean commit history.
    • Ensure the change passes all existing lint, format, and CI verification pipelines.

    I am ready to start immediately—please feel free to assign this to me!

  4. CodeDeityX commented on Sep 30, 2026

    @CodeDeityX

    Public reproducibility harness — G1 closure candidate

    Following the scope we agreed on, the first two items are now ready for maintainer reproduction.

    1. Public / reproducible harness

    The harness is now public:

    https://github.com/CodeDeityX/nirium-96-agent-policy-harness

    Canonical publication commit:

    65c7ca2ffc18811c28e79f0fd74b054407aa4b09

    A clean reproduction exercises the pinned Nirium/x402 path and currently produces:

    Nirium upstream baseline: 58/58 PASS
    G1 integration:           35/35 PASS
    

    The original 25 G1 cases are preserved. The additional 10 cases cover the freshness / stale-ALLOW boundary discussed below.

    No real keys are used, no transaction is broadcast, no KINE dependency is required, and no Nirium production source is modified by the harness.

    2. Stale ALLOW / TOCTOU inside G1

    The G1 candidate now binds an ALLOW to:

    • the exact request context;
    • the policy version under which it was produced;
    • an explicit expiry.

    At the final tested pre-sign freshness check:

    • current + unexpired ALLOW may reach the signer;
    • stale policy version fails closed;
    • expired decision fails closed;
    • unavailable / invalid freshness information fails closed;
    • a concurrent request that becomes stale before its own final check does not reach the signer.

    The claim remains deliberately bounded:

    Within the tested Nirium/x402 pre-sign path, ALLOW is bound to the exact request, policy version and explicit expiry, and fails closed at the final tested freshness check when stale, expired or freshness cannot be established.

    This does not claim that the freshness observation is atomic with cryptographic signing, nor does it establish account-level / on-chain enforcement.

    The unresolved boundaries therefore remain:

    L01_PENDING_CAPACITY_RESERVATION = NOT_SOLVED
    L02_OUT_OF_PROCESS_SIGNER_BYPASS = NOT_SOLVED
    G2_ONCHAIN_ENFORCEMENT           = NOT_RUN
    G3_PENDING_CAPACITY              = NOT_SOLVED
    G4_GOVERNANCE                    = NOT_RUN
    

    This should close the first two items from the scope you proposed.

    The remaining item is the supported/documented pre-sign hook you mentioned on the Nirium side. Once that exists, I can move the adapter from signer instrumentation onto that supported boundary and rerun the same evidence contract against it.

    I would especially value an independent reproduction of the public harness before we treat this phase as closed.

  5. rupesh-kumar-sah commented on Oct 2, 2026

    @rupesh-kumar-sah

    Hi team,

    Happy to tackle this. Here is how I plan to implement it:

    Implementation approach:

    1. Audit authorization boundaries and enforce defensive validation barriers.
    2. Implement multi-role permission checks and invocation validation.
    3. Write adversarial unit tests simulating edge failure modes and invalid callers.
    4. Ensure zero regressions across existing test suites and CI workflows.

    Could you please assign this issue to me? Looking forward to delivering a clean PR.

  6. ranjeet150 commented on Oct 2, 2026

    @ranjeet150

    Hi,

    I have experience with similar implementations and would like to take this on.

    Plan:

    1. Audit authorization boundaries and enforce defensive validation barriers.
    2. Implement multi-role permission checks and invocation validation.
    3. Write adversarial unit tests simulating edge failure modes and invalid callers.
    4. Ensure zero regressions across existing test suites and CI workflows.

    Could you please assign this issue to me? Looking forward to delivering a clean PR.

  7. Ranjeet2063 commented on Oct 2, 2026

    @Ranjeet2063
    Contributor

    Hi maintainers,

    I reviewed this issue and would like to contribute the fix.

    Implementation approach:

    1. Audit authorization boundaries and enforce defensive validation barriers.
    2. Implement multi-role permission checks and invocation validation.
    3. Write adversarial unit tests simulating edge failure modes and invalid callers.
    4. Ensure zero regressions across existing test suites and CI workflows.

    Please assign this to me if the plan looks good. Thanks!

  8. shobhamerabacha-star commented on Oct 2, 2026

    @shobhamerabacha-star

    Hi maintainers,

    I reviewed this issue and would like to contribute the fix.

    Plan:

    1. Audit authorization boundaries and enforce defensive validation barriers.
    2. Implement multi-role permission checks and invocation validation.
    3. Write adversarial unit tests simulating edge failure modes and invalid callers.
    4. Ensure zero regressions across existing test suites and CI workflows.

    Ready to implement immediately upon assignment. Please feel free to assign!

  9. ravimahatonp commented on Oct 2, 2026

    @ravimahatonp

    Hi maintainers,

    I reviewed this issue and would like to contribute the fix.

    Implementation approach:

    1. Audit authorization boundaries and enforce defensive validation barriers.
    2. Implement multi-role permission checks and invocation validation.
    3. Write adversarial unit tests simulating edge failure modes and invalid callers.
    4. Ensure zero regressions across existing test suites and CI workflows.

    I can deliver a clean, tested PR within 24 hours. Please assign!

  10. self-assigned this
    on Oct 9, 2026
  11. Eras256 commented on Oct 10, 2026

    @Eras256
    CollaboratorAuthor

    Resolved by #98 (merged 2026-10-03), which added the pre-sign policy hook: initX402({ policy }) lets a caller run an evaluate/decide step (allow/warn/wait/deny) against actor identity, value, and destination before x402Fetch signs anything — exactly the "should this execute" layer this issue asked for, on top of the existing "is this signature valid" check.

    Closing as completed rather than leaving it open for the design doc originally requested; the implementation shipped instead.

    🤖 Generated with Claude Code

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

Metadata

Metadata

Assignees

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