Skip to content

fix: fail closed when spending policy check is unreachable#200

Open
baedboibidex-cmyk wants to merge 2 commits into
Stellar-Ecosystem:mainfrom
baedboibidex-cmyk:fix/fail-closed-spend-policy
Open

fix: fail closed when spending policy check is unreachable#200
baedboibidex-cmyk wants to merge 2 commits into
Stellar-Ecosystem:mainfrom
baedboibidex-cmyk:fix/fail-closed-spend-policy

Conversation

@baedboibidex-cmyk

@baedboibidex-cmyk baedboibidex-cmyk commented Jun 22, 2026

Copy link
Copy Markdown

close #126

Summary

This PR fixes the agent-side spending policy flow so that any failed or unreachable can-spend check now blocks the payment flow by default instead of silently allowing it.

What changed

  • Updated the agent to treat failed or invalid policy-check responses as deny-by-default, while still allowing an explicit development-only fail-open override.
  • Added structured warnings whenever the policy check fails or returns an unexpected payload.
  • Added a per-process local fallback spending guard so the agent still enforces MAX_PER_TX / MAX_PER_DAY bounds during backend outages.
  • Documented the new env behavior in agent/.env.example and README.md.
  • Added backend coverage for the relevant can-spend route behavior.

Acceptance criteria checklist

  • Policy-check failures stop the payment flow by default.
  • Unreachable policy checks are logged with actionable context.
  • Operators can explicitly opt into fail-open behavior via AGENT_FAIL_OPEN=true.
  • Local fallback spending limits provide a defense-in-depth guard when the backend is unavailable.
  • Relevant tests were updated/added.

Verification

  • cd /workspaces/lodestar/backend && npm run test
    • Result: 10/10 test files passed, 111/111 tests passed
  • node --check /workspaces/lodestar/agent/agent.js
    • Result: syntax check passed

Notes

  • The agent now logs a warning whenever the backend policy check is unreachable and, by default, refuses to continue.
  • The AGENT_FAIL_OPEN setting is documented as development-only and should not be used in production.

Summary by CodeRabbit

  • New Features

    • Added clearer guidance for spending controls, including what happens when policy checks are unavailable and how development-only bypass behavior works.
  • Bug Fixes

    • Improved payment safety by defaulting to a secure blocked state when policy checks fail, with a local fallback limit to prevent overspending during outages.
    • Added clearer warnings and logging around policy-check failures and payment updates.

@coderabbitai

coderabbitai Bot commented Jun 22, 2026

Copy link
Copy Markdown

Review Change Stack

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3438cf8-71f3-48e0-99bf-816fa779aef5

📥 Commits

Reviewing files that changed from the base of the PR and between 8963e8d and 331855e.

📒 Files selected for processing (4)
  • README.md
  • agent/.env.example
  • agent/agent.js
  • backend/src/routes/agents.test.js
✅ Files skipped from review due to trivial changes (1)
  • README.md

📝 Walkthrough

Walkthrough

The agent gains configurable fail-open/fail-closed spending-policy enforcement via AGENT_FAIL_OPEN. checkSpend is refactored to validate response payloads and apply consistent behavior on failures. runTask adds a local per-session USDC fallback when the remote check is unreachable. Backend agent route tests switch to async per-test initialization.

Agent spending policy enforcement

Layer / File(s) Summary
Config, session state, and docs
agent/agent.js, agent/.env.example, README.md
Parses AGENT_FAIL_OPEN and USDC limit env vars at startup; documents the new variable in .env.example; updates README to describe policy-check ordering, fail-closed default, and AGENT_FAIL_OPEN=true dev override; emits startup warning when fail-open is enabled.
checkSpend refactor
agent/agent.js
Builds a single policy-check URL, validates data.allowed as a boolean, and applies uniform fail-open/closed returns across HTTP non-OK responses, invalid payloads, and fetch/network errors — removing the prior silent { allowed: true } fallback.
runTask integration and session spend tracking
agent/agent.js
Detects fail-open-unreachable reason in runTask and enforces local per-tx/per-session USDC fallback limits; on successful payment increments sessionSpentUsdc and logs the updated session total.

Backend agents route test updates

Layer / File(s) Summary
agents.test.js mock wiring and async beforeEach
backend/src/routes/agents.test.js
Adds mockListAgentsPage declaration and wires it into the contract.js mock; replaces beforeAll+sync beforeEach with an async beforeEach that clears mocks, resets module state and idempotency store, and re-imports the router per test; fixes whitespace at the flag test suite boundary.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • Stellar-Ecosystem/lodestar#208: Modifies agent/agent.js's spending-limit logic to run checkSpend against the demanded USDC amount from the 402 response, overlapping with the checkSpend refactor in this PR.
  • Stellar-Ecosystem/lodestar#218: Modifies runTask's spending-policy enforcement and blocking behavior in the same control-flow region as the local fallback added here.
  • Stellar-Ecosystem/lodestar#223: Rewrites runTask flow and integrates checkSpend into scoring/service selection, directly overlapping with both the checkSpend refactor and runTask changes in this PR.

Suggested reviewers

  • ritik4ever

🐇 No more silent "allowed: true" in the night,
fail-closed stands guard when the backend's out of sight.
Session spend tracked, a local limit in place,
AGENT_FAIL_OPEN warns from its startup space.
The rabbit hops safe — no budget lost by mistake! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main change: making spending policy checks fail closed when unreachable.
Linked Issues check ✅ Passed The PR appears to implement the linked requirements: fail-closed checks, fail-open override, fallback spend tracking, warnings, and docs/tests updates.
Out of Scope Changes check ✅ Passed The changed files align with the spending-policy fix and related test/doc updates, with no clear unrelated additions.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 Biome (2.5.0)
agent/agent.js

File contains syntax errors that prevent linting: Line 44: Illegal return statement outside of a function; Line 58: Illegal return statement outside of a function; Line 83: Illegal return statement outside of a function; Line 90: Illegal return statement outside of a function; Line 98: Illegal return statement outside of a function; Line 99: Expected a statement but instead found '}

async'.; Line 107: await is only allowed within async functions and at the top levels of modules.; Line 115: await is only allowed within async functions and at the top levels of modules.; Line 302: A continue statement can only be used within an enclosing for, while or do while statement.; Line 322: A continue statement can only be used within an enclosing for, while or do while statement.; Line 402: Expected a statement but instead found '}'.

backend/src/routes/agents.test.js

File contains syntax errors that prevent linting: Line 145: Expected a statement but instead found '})'.

🔧 markdownlint-cli2 (0.22.1)
README.md

markdownlint-cli2 wrapper config was not available before execution


Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
backend/src/routes/agents.test.js (1)

172-174: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Zero-agents stats test is mocking the wrong contract function.

This test still stubs mockListAgents, but the stats route now reads through getAgentCount + listAgentsPage. Please mock mockGetAgentCount as 0 (and optionally assert mockListAgentsPage is not called) so the test validates the current integration path.

Suggested fix
   it('should return zero stats when no agents', async () => {
-    mockListAgents.mockResolvedValueOnce([]);
+    mockGetAgentCount.mockResolvedValueOnce(0);

     const res = await request(app).get('/api/agents/stats');

     expect(res.status).toBe(200);
     expect(res.body.totalAgents).toBe(0);
     expect(res.body.avgScore).toBe(0);
+    expect(mockListAgentsPage).not.toHaveBeenCalled();
   });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/agents.test.js` around lines 172 - 174, The test "should
return zero stats when no agents" is mocking the outdated function
mockListAgents, but the stats route now uses mockGetAgentCount and
mockListAgentsPage for its integration. Replace the
mockListAgents.mockResolvedValueOnce([]) call with
mockGetAgentCount.mockResolvedValueOnce(0) to correctly test the current code
path. Optionally, add an assertion to verify that mockListAgentsPage is not
called when the agent count is zero.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/agent.js`:
- Around line 254-271: The policy enforcement logic gated by the `if
(scoringEnabled)` condition is being skipped entirely when scoring is disabled,
bypassing deny-by-default policy checks. Decouple policy enforcement from the
scoringEnabled state by moving the checkSpend() call and associated policy
validation logic (including the checkLocalSpendLimit() fallback) outside or
independent of the `if (scoringEnabled)` block so that spending policy checks
are always performed regardless of the scoring state to ensure policies are
enforced consistently.
- Around line 299-301: Before updating sessionSpentUsdc by adding
Number(best.price_usdc), validate that the converted price is a valid number and
not NaN. If the conversion results in NaN or an invalid value, handle it as an
error case by either logging a warning, rejecting the transaction, or taking an
appropriate fail-closed action that prevents the invalid amount from poisoning
the sessionSpentUsdc accumulator and disabling subsequent daily-limit checks.

In `@README.md`:
- Line 180: The documentation is directing users to modify the `.env.example`
template file instead of the actual runtime configuration file. Change the
reference from `agent/.env.example` to `agent/.env` in the README.md line that
instructs users to set the AGENT_FAIL_OPEN environment variable. This ensures
users modify the correct file that actually controls runtime behavior,
preventing confusion during setup and verification.

---

Outside diff comments:
In `@backend/src/routes/agents.test.js`:
- Around line 172-174: The test "should return zero stats when no agents" is
mocking the outdated function mockListAgents, but the stats route now uses
mockGetAgentCount and mockListAgentsPage for its integration. Replace the
mockListAgents.mockResolvedValueOnce([]) call with
mockGetAgentCount.mockResolvedValueOnce(0) to correctly test the current code
path. Optionally, add an assertion to verify that mockListAgentsPage is not
called when the agent count is zero.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: f1a5eff8-4ca6-40d4-882a-d7cc6a6d80dc

📥 Commits

Reviewing files that changed from the base of the PR and between 5bbe6bd and 8963e8d.

📒 Files selected for processing (5)
  • README.md
  • agent/.env.example
  • agent/agent.js
  • backend/src/lib/contract.test.js
  • backend/src/routes/agents.test.js

Comment thread agent/agent.js Outdated
Comment on lines +254 to +271
if (scoringEnabled) {
const check = await checkSpend(best.price_usdc, category);

if (!check.allowed) {
logger.warn(`${tag()} Payment blocked by spending policy: ${check.reason}`);
return;
}
logger.info(`${tag()} Spending policy check passed`);

if (check.reason === 'Policy check unreachable (fail-open enabled)') {
const localCheck = checkLocalSpendLimit(best.price_usdc);
if (!localCheck.allowed) {
logger.warn(`${tag()} Payment blocked by local fallback spending policy: ${localCheck.reason}`);
return;
}
logger.warn(`${tag()} Spending policy check unreachable; using local fallback guard for this session`);
} else {
logger.info(`${tag()} Spending policy check passed`);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Policy enforcement is bypassed when scoringEnabled is false.

checkSpend() is currently gated by if (scoringEnabled). When registration/policy backend is unreachable (or contract is not configured), this block is skipped and payments proceed without deny-by-default or local fallback checks.

Suggested fix (decouple policy enforcement from scoring state)
-  // Spending policy check
-  if (scoringEnabled) {
-    const check = await checkSpend(best.price_usdc, category);
+  // Spending policy check (always enforce)
+  {
+    const check = await checkSpend(best.price_usdc, category);

     if (!check.allowed) {
       logger.warn(`${tag()} Payment blocked by spending policy: ${check.reason}`);
       return;
     }

     if (check.reason === 'Policy check unreachable (fail-open enabled)') {
       const localCheck = checkLocalSpendLimit(best.price_usdc);
       if (!localCheck.allowed) {
         logger.warn(`${tag()} Payment blocked by local fallback spending policy: ${localCheck.reason}`);
         return;
       }
       logger.warn(`${tag()} Spending policy check unreachable; using local fallback guard for this session`);
     } else {
       logger.info(`${tag()} Spending policy check passed`);
     }
   }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (scoringEnabled) {
const check = await checkSpend(best.price_usdc, category);
if (!check.allowed) {
logger.warn(`${tag()} Payment blocked by spending policy: ${check.reason}`);
return;
}
logger.info(`${tag()} Spending policy check passed`);
if (check.reason === 'Policy check unreachable (fail-open enabled)') {
const localCheck = checkLocalSpendLimit(best.price_usdc);
if (!localCheck.allowed) {
logger.warn(`${tag()} Payment blocked by local fallback spending policy: ${localCheck.reason}`);
return;
}
logger.warn(`${tag()} Spending policy check unreachable; using local fallback guard for this session`);
} else {
logger.info(`${tag()} Spending policy check passed`);
}
// Spending policy check (always enforce)
{
const check = await checkSpend(best.price_usdc, category);
if (!check.allowed) {
logger.warn(`${tag()} Payment blocked by spending policy: ${check.reason}`);
return;
}
if (check.reason === 'Policy check unreachable (fail-open enabled)') {
const localCheck = checkLocalSpendLimit(best.price_usdc);
if (!localCheck.allowed) {
logger.warn(`${tag()} Payment blocked by local fallback spending policy: ${localCheck.reason}`);
return;
}
logger.warn(`${tag()} Spending policy check unreachable; using local fallback guard for this session`);
} else {
logger.info(`${tag()} Spending policy check passed`);
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/agent.js` around lines 254 - 271, The policy enforcement logic gated by
the `if (scoringEnabled)` condition is being skipped entirely when scoring is
disabled, bypassing deny-by-default policy checks. Decouple policy enforcement
from the scoringEnabled state by moving the checkSpend() call and associated
policy validation logic (including the checkLocalSpendLimit() fallback) outside
or independent of the `if (scoringEnabled)` block so that spending policy checks
are always performed regardless of the scoring state to ensure policies are
enforced consistently.

Comment thread agent/agent.js Outdated
Comment thread README.md
- Pay via x402 on Stellar
- Log the data received and update on-chain reputation

If you explicitly want development-only fail-open behavior, set `AGENT_FAIL_OPEN=true` in [agent/.env.example](agent/.env.example). The agent will still enforce a per-process local fallback limit for the current run.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Point fail-open setup to the runtime .env, not .env.example.

This currently instructs users to edit the template file, which can leave runtime behavior unchanged and cause confusion during verification.

Suggested doc fix
-If you explicitly want development-only fail-open behavior, set `AGENT_FAIL_OPEN=true` in [agent/.env.example](agent/.env.example). The agent will still enforce a per-process local fallback limit for the current run.
+If you explicitly want development-only fail-open behavior, set `AGENT_FAIL_OPEN=true` in `agent/.env` (copied from [agent/.env.example](agent/.env.example)). The agent will still enforce a per-process local fallback limit for the current run.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
If you explicitly want development-only fail-open behavior, set `AGENT_FAIL_OPEN=true` in [agent/.env.example](agent/.env.example). The agent will still enforce a per-process local fallback limit for the current run.
If you explicitly want development-only fail-open behavior, set `AGENT_FAIL_OPEN=true` in `agent/.env` (copied from [agent/.env.example](agent/.env.example)). The agent will still enforce a per-process local fallback limit for the current run.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@README.md` at line 180, The documentation is directing users to modify the
`.env.example` template file instead of the actual runtime configuration file.
Change the reference from `agent/.env.example` to `agent/.env` in the README.md
line that instructs users to set the AGENT_FAIL_OPEN environment variable. This
ensures users modify the correct file that actually controls runtime behavior,
preventing confusion during setup and verification.

@ritik4ever ritik4ever 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.

Thanks for the contribution! The fail-closed approach is a good improvement, but before we merge could you please address the critical review comment?

It appears the spending policy check is still gated by scoringEnabled, which may allow payments to proceed without policy enforcement when scoring is disabled or unavailable. Since this PR is intended to enforce fail-closed behavior, that path should also be protected.

Additionally, there are currently merge conflicts that need to be resolved before we can merge.

Once those are addressed, I'll take another look. Thanks!

@ritik4ever ritik4ever 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.

Thanks for the contribution. Before we can merge, I still need to see the critical review comment addressed.

The spending-policy enforcement appears to remain gated by scoringEnabled, which means payments may proceed without policy checks when scoring is disabled or unavailable. Since this PR is intended to enforce fail-closed behavior, that path also needs to be protected.

There are also unresolved merge conflicts in:

  • agent/.env.example
  • agent/agent.js
  • backend/src/routes/agents.test.js

Additionally, please consider hardening the session spend tracking against invalid/NaN payment amounts so the local fallback limits cannot be bypassed.

Once those items are addressed, I'll review again.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
agent/agent.js (1)

37-96: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the ensureRegistered() wrapper around this block.

This try/return sequence is sitting at module scope, which is why the parser is flagging illegal top-level returns here. Re-wrap the registration flow in its async function before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/agent.js` around lines 37 - 96, The registration flow in the agent
module is currently sitting at module scope, which leaves the `try`/`return`
sequence outside any function. Move this whole block back inside the async
`ensureRegistered()` wrapper, and keep the existing fetch/error handling and
logger calls (`EVENT.AGENT_REGISTERED`, `logger.info`, `logger.warn`) within
that function so the top-level `return`s are no longer illegal.

Source: Linters/SAST tools

backend/src/routes/agents.test.js (1)

71-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mount the freshly imported router onto a new app instance.

After vi.resetModules(), Line 76 imports a new router, but the shown setup never reassigns app or mounts that router. That means these tests either keep exercising a stale router instance from earlier setup or request(app) runs against undefined, so the per-test reset is ineffective.

Suggested fix
 beforeEach(async () => {
   vi.clearAllMocks();
   vi.resetModules();
   resetIdempotencyStore();

   const router = (await import('./agents.js')).default;
+  app = makeApp();
+  app.use(router);
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/agents.test.js` around lines 71 - 78, The test setup in
agents.test.js imports a fresh router after vi.resetModules(), but it never
mounts that router onto the app used by request(app), so the per-test reset is
ineffective. Update the beforeEach setup to rebuild the app instance and attach
the newly imported default router from agents.js before each test, using the
existing app/request test harness so each test runs against the current router
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/agent.js`:
- Around line 274-352: The payment handling in runTask() is missing the
enclosing candidate-attempt retry/selection loop, so the existing references to
selected, failed, and continue are invalid and break control flow. Restore the
loop that iterates over candidate services before the payment request block, and
keep the current success/failure branches inside it so selected is always
defined and failed tracking works correctly. Use runTask(), selected, failed,
and the payment success/error logging blocks as the anchors when reintroducing
the retry logic.

In `@backend/src/routes/agents.test.js`:
- Around line 80-84: The POST /admin/agents/:address/flag test suite is being
closed too early by a stray describe terminator, which leaves the flag tests
outside the suite and breaks parsing. Remove the extra closing bracket in the
agents.test.js flag block so the describe for POST /admin/agents/:address/flag
stays open until its intended it(...) cases, and keep the suite structure
consistent with the surrounding test blocks.
- Around line 7-35: The test mock for contract.js is missing the admin-specific
exports that agents.js imports, so the router setup breaks before the tests run.
Add dedicated mocks for flagAgentOnChain and adminDeactivateAgentOnChain in this
vi.mock block, and update the related assertions to use those same mock symbols
instead of the unused deactivateAgentOnChain mock so the import surface matches
what agents.js actually consumes.

---

Outside diff comments:
In `@agent/agent.js`:
- Around line 37-96: The registration flow in the agent module is currently
sitting at module scope, which leaves the `try`/`return` sequence outside any
function. Move this whole block back inside the async `ensureRegistered()`
wrapper, and keep the existing fetch/error handling and logger calls
(`EVENT.AGENT_REGISTERED`, `logger.info`, `logger.warn`) within that function so
the top-level `return`s are no longer illegal.

In `@backend/src/routes/agents.test.js`:
- Around line 71-78: The test setup in agents.test.js imports a fresh router
after vi.resetModules(), but it never mounts that router onto the app used by
request(app), so the per-test reset is ineffective. Update the beforeEach setup
to rebuild the app instance and attach the newly imported default router from
agents.js before each test, using the existing app/request test harness so each
test runs against the current router state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3438cf8-71f3-48e0-99bf-816fa779aef5

📥 Commits

Reviewing files that changed from the base of the PR and between 8963e8d and 331855e.

📒 Files selected for processing (4)
  • README.md
  • agent/.env.example
  • agent/agent.js
  • backend/src/routes/agents.test.js
✅ Files skipped from review due to trivial changes (1)
  • README.md

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
agent/agent.js (1)

37-96: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Restore the ensureRegistered() wrapper around this block.

This try/return sequence is sitting at module scope, which is why the parser is flagging illegal top-level returns here. Re-wrap the registration flow in its async function before merge.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/agent.js` around lines 37 - 96, The registration flow in the agent
module is currently sitting at module scope, which leaves the `try`/`return`
sequence outside any function. Move this whole block back inside the async
`ensureRegistered()` wrapper, and keep the existing fetch/error handling and
logger calls (`EVENT.AGENT_REGISTERED`, `logger.info`, `logger.warn`) within
that function so the top-level `return`s are no longer illegal.

Source: Linters/SAST tools

backend/src/routes/agents.test.js (1)

71-78: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Mount the freshly imported router onto a new app instance.

After vi.resetModules(), Line 76 imports a new router, but the shown setup never reassigns app or mounts that router. That means these tests either keep exercising a stale router instance from earlier setup or request(app) runs against undefined, so the per-test reset is ineffective.

Suggested fix
 beforeEach(async () => {
   vi.clearAllMocks();
   vi.resetModules();
   resetIdempotencyStore();

   const router = (await import('./agents.js')).default;
+  app = makeApp();
+  app.use(router);
 });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/agents.test.js` around lines 71 - 78, The test setup in
agents.test.js imports a fresh router after vi.resetModules(), but it never
mounts that router onto the app used by request(app), so the per-test reset is
ineffective. Update the beforeEach setup to rebuild the app instance and attach
the newly imported default router from agents.js before each test, using the
existing app/request test harness so each test runs against the current router
state.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@agent/agent.js`:
- Around line 274-352: The payment handling in runTask() is missing the
enclosing candidate-attempt retry/selection loop, so the existing references to
selected, failed, and continue are invalid and break control flow. Restore the
loop that iterates over candidate services before the payment request block, and
keep the current success/failure branches inside it so selected is always
defined and failed tracking works correctly. Use runTask(), selected, failed,
and the payment success/error logging blocks as the anchors when reintroducing
the retry logic.

In `@backend/src/routes/agents.test.js`:
- Around line 80-84: The POST /admin/agents/:address/flag test suite is being
closed too early by a stray describe terminator, which leaves the flag tests
outside the suite and breaks parsing. Remove the extra closing bracket in the
agents.test.js flag block so the describe for POST /admin/agents/:address/flag
stays open until its intended it(...) cases, and keep the suite structure
consistent with the surrounding test blocks.
- Around line 7-35: The test mock for contract.js is missing the admin-specific
exports that agents.js imports, so the router setup breaks before the tests run.
Add dedicated mocks for flagAgentOnChain and adminDeactivateAgentOnChain in this
vi.mock block, and update the related assertions to use those same mock symbols
instead of the unused deactivateAgentOnChain mock so the import surface matches
what agents.js actually consumes.

---

Outside diff comments:
In `@agent/agent.js`:
- Around line 37-96: The registration flow in the agent module is currently
sitting at module scope, which leaves the `try`/`return` sequence outside any
function. Move this whole block back inside the async `ensureRegistered()`
wrapper, and keep the existing fetch/error handling and logger calls
(`EVENT.AGENT_REGISTERED`, `logger.info`, `logger.warn`) within that function so
the top-level `return`s are no longer illegal.

In `@backend/src/routes/agents.test.js`:
- Around line 71-78: The test setup in agents.test.js imports a fresh router
after vi.resetModules(), but it never mounts that router onto the app used by
request(app), so the per-test reset is ineffective. Update the beforeEach setup
to rebuild the app instance and attach the newly imported default router from
agents.js before each test, using the existing app/request test harness so each
test runs against the current router state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b3438cf8-71f3-48e0-99bf-816fa779aef5

📥 Commits

Reviewing files that changed from the base of the PR and between 8963e8d and 331855e.

📒 Files selected for processing (4)
  • README.md
  • agent/.env.example
  • agent/agent.js
  • backend/src/routes/agents.test.js
✅ Files skipped from review due to trivial changes (1)
  • README.md
🛑 Comments failed to post (3)
agent/agent.js (1)

274-352: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Reintroduce the missing retry/selection block in runTask().

The payment path still references selected, failed, and continue, but there is no enclosing candidate-attempt loop after Line 273. This is the root cause of the parse errors here and the broken success/failure control flow.

🧰 Tools
🪛 Biome (2.5.0)

[error] 302-302: A continue statement can only be used within an enclosing for, while or do while statement.

(parse)


[error] 322-322: A continue statement can only be used within an enclosing for, while or do while statement.

(parse)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@agent/agent.js` around lines 274 - 352, The payment handling in runTask() is
missing the enclosing candidate-attempt retry/selection loop, so the existing
references to selected, failed, and continue are invalid and break control flow.
Restore the loop that iterates over candidate services before the payment
request block, and keep the current success/failure branches inside it so
selected is always defined and failed tracking works correctly. Use runTask(),
selected, failed, and the payment success/error logging blocks as the anchors
when reintroducing the retry logic.

Source: Linters/SAST tools

backend/src/routes/agents.test.js (2)

7-35: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Mock the admin contract symbols the router actually imports.

agents.js imports flagAgentOnChain and adminDeactivateAgentOnChain, but this mock only wires deactivateAgentOnChain, and the file never defines mockFlagAgentOnChain / mockAdminDeactivateAgentOnChain. Line 76 will fail when ./agents.js is imported, and the assertions later in this file also dereference undefined mocks.

Suggested fix
 const mockListAgentsPage = vi.fn();
 const mockRegisterAgentOnChain = vi.fn();
 const mockRecordPaymentOnChain = vi.fn();
+const mockFlagAgentOnChain = vi.fn();
 const mockDeactivateAgentOnChain = vi.fn();
+const mockAdminDeactivateAgentOnChain = vi.fn();
 const mockUpdatePolicyOnChain = vi.fn();
 const mockBuildUnsignedAgentTx = vi.fn();
 const mockSubmitSignedAgentTx = vi.fn();

 vi.mock('../lib/contract.js', () => ({
   getAgent: (...args) => mockGetAgent(...args),
   getAgentPolicy: (...args) => mockGetAgentPolicy(...args),
   getAgentScore: (...args) => mockGetAgentScore(...args),
   getAgentCount: (...args) => mockGetAgentCount(...args),
   isAgentEligible: (...args) => mockIsAgentEligible(...args),
   checkSpendingAllowed: (...args) => mockCheckSpendingAllowed(...args),
   listAgentsPage: (...args) => mockListAgentsPage(...args),
   registerAgentOnChain: (...args) => mockRegisterAgentOnChain(...args),
   recordPaymentOnChain: (...args) => mockRecordPaymentOnChain(...args),
+  flagAgentOnChain: (...args) => mockFlagAgentOnChain(...args),
   deactivateAgentOnChain: (...args) => mockDeactivateAgentOnChain(...args),
+  adminDeactivateAgentOnChain: (...args) => mockAdminDeactivateAgentOnChain(...args),
   updatePolicyOnChain: (...args) => mockUpdatePolicyOnChain(...args),
   buildUnsignedAgentTx: (...args) => mockBuildUnsignedAgentTx(...args),
   submitSignedAgentTx: (...args) => mockSubmitSignedAgentTx(...args),
 }));
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

const mockGetAgent = vi.fn();
const mockGetAgentPolicy = vi.fn();
const mockGetAgentScore = vi.fn();
const mockGetAgentCount = vi.fn();
const mockIsAgentEligible = vi.fn();
const mockCheckSpendingAllowed = vi.fn();
const mockListAgentsPage = vi.fn();
const mockRegisterAgentOnChain = vi.fn();
const mockRecordPaymentOnChain = vi.fn();
const mockFlagAgentOnChain = vi.fn();
const mockDeactivateAgentOnChain = vi.fn();
const mockAdminDeactivateAgentOnChain = vi.fn();
const mockUpdatePolicyOnChain = vi.fn();
const mockBuildUnsignedAgentTx = vi.fn();
const mockSubmitSignedAgentTx = vi.fn();

vi.mock('../lib/contract.js', () => ({

  getAgent: (...args) => mockGetAgent(...args),
  getAgentPolicy: (...args) => mockGetAgentPolicy(...args),
  getAgentScore: (...args) => mockGetAgentScore(...args),
  getAgentCount: (...args) => mockGetAgentCount(...args),
  isAgentEligible: (...args) => mockIsAgentEligible(...args),
  checkSpendingAllowed: (...args) => mockCheckSpendingAllowed(...args),
  listAgentsPage: (...args) => mockListAgentsPage(...args),
  registerAgentOnChain: (...args) => mockRegisterAgentOnChain(...args),
  recordPaymentOnChain: (...args) => mockRecordPaymentOnChain(...args),
  flagAgentOnChain: (...args) => mockFlagAgentOnChain(...args),
  deactivateAgentOnChain: (...args) => mockDeactivateAgentOnChain(...args),
  adminDeactivateAgentOnChain: (...args) => mockAdminDeactivateAgentOnChain(...args),
  updatePolicyOnChain: (...args) => mockUpdatePolicyOnChain(...args),
  buildUnsignedAgentTx: (...args) => mockBuildUnsignedAgentTx(...args),
  submitSignedAgentTx: (...args) => mockSubmitSignedAgentTx(...args),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/agents.test.js` around lines 7 - 35, The test mock for
contract.js is missing the admin-specific exports that agents.js imports, so the
router setup breaks before the tests run. Add dedicated mocks for
flagAgentOnChain and adminDeactivateAgentOnChain in this vi.mock block, and
update the related assertions to use those same mock symbols instead of the
unused deactivateAgentOnChain mock so the import surface matches what agents.js
actually consumes.

80-84: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Remove the stray }); that closes the flag suite early.

Line 84 closes the describe before any it(...) blocks. That matches the parse failure reported later in the file and leaves the flag tests outside the suite.

Suggested fix
 describe('POST /admin/agents/:address/flag', () => {
   const ADDRESS = 'GAMASX3TLJIDO42FO3GTX7IQAYN7RJ4U4CXJOROTB7RSV3NGPUEIEQH3';
-
-  });
 
   it('flags an agent with valid admin key', async () => {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

describe('POST /admin/agents/:address/flag', () => {
  const ADDRESS = 'GAMASX3TLJIDO42FO3GTX7IQAYN7RJ4U4CXJOROTB7RSV3NGPUEIEQH3';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@backend/src/routes/agents.test.js` around lines 80 - 84, The POST
/admin/agents/:address/flag test suite is being closed too early by a stray
describe terminator, which leaves the flag tests outside the suite and breaks
parsing. Remove the extra closing bracket in the agents.test.js flag block so
the describe for POST /admin/agents/:address/flag stays open until its intended
it(...) cases, and keep the suite structure consistent with the surrounding test
blocks.

Source: Linters/SAST tools

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Title: checkSpend in agent.js fails open — a failed can-spend request defaults to allowed: true, silently bypassing all spending policy enforcement

2 participants