Skip to content

feat(ai-agent): bootstrap MVP service from scaffold (#420)#546

Open
olaleyeolajide81-sketch wants to merge 2 commits into
ancore-org:mainfrom
olaleyeolajide81-sketch:main
Open

feat(ai-agent): bootstrap MVP service from scaffold (#420)#546
olaleyeolajide81-sketch wants to merge 2 commits into
ancore-org:mainfrom
olaleyeolajide81-sketch:main

Conversation

@olaleyeolajide81-sketch
Copy link
Copy Markdown
Contributor

@olaleyeolajide81-sketch olaleyeolajide81-sketch commented May 26, 2026

  • Add package.json, tsconfig.json, tsconfig.test.json
  • Add src/types.ts: DraftIntentRequest/Response schemas (zod)
  • Add src/guardrail.ts: enforceNoAutonomousExecution guardrail
  • Add src/server.ts: Express app with /health and /agent/draft-intent
  • Add src/server.test.ts: 9 tests covering all endpoints and guardrail
  • Update README.md with security boundaries and limitations

The agent is a draft-only suggestion engine. It never executes financial operations autonomously. All responses carry status='draft' and requiresConfirmation=true, enforced by the guardrail function.

Closes #420

Description

Type of Change

  • 🐛 Bug fix (non-breaking change which fixes an issue)
  • ✨ New feature (non-breaking change which adds functionality)
  • 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • 📝 Documentation update
  • 🔧 Configuration change
  • ♻️ Code refactoring
  • ⚡ Performance improvement
  • ✅ Test addition/improvement

Security Impact

  • This change involves cryptographic operations
  • This change affects account validation logic
  • This change modifies smart contracts
  • This change handles user private keys
  • This change affects authorization/authentication
  • No security impact

Testing

  • Unit tests added/updated
  • Integration tests added/updated
  • Manual testing performed
  • E2E tests added/updated (if applicable)

Test Coverage

  • Current coverage: __%
  • New/modified code coverage: __%

Manual Testing Steps

Breaking Changes

  • This PR introduces breaking changes

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings or errors
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published
  • Any flaky-test quarantine entry includes a linked follow-up issue, owner, expiry, and stable failure signature

For High-Security Changes

  • I have documented all security assumptions
  • I have considered attack vectors
  • I have added security-focused test cases
  • I have reviewed against the threat model

Related Issues

Closes #
Related to #

Additional Context

Reviewer Notes


closes #420

Summary by CodeRabbit

  • New Features

    • Introduced AI Agent service with /agent/draft-intent endpoint for creating payment and invoice drafts.
    • Added OpenAPI 3.1 specification for relayer service endpoints (/relay/execute, /relay/validate, /relay/status).
  • Documentation

    • Updated AI Agent README with endpoint documentation, security guardrails, and limitations.
    • Enhanced relayer README with API specification details and example cURL commands.
  • Tests

    • Added contract tests validating relayer API compliance with OpenAPI specification.

Review Change Stack

- Add package.json, tsconfig.json, tsconfig.test.json
- Add src/types.ts: DraftIntentRequest/Response schemas (zod)
- Add src/guardrail.ts: enforceNoAutonomousExecution guardrail
- Add src/server.ts: Express app with /health and /agent/draft-intent
- Add src/server.test.ts: 9 tests covering all endpoints and guardrail
- Update README.md with security boundaries and limitations

The agent is a draft-only suggestion engine. It never executes
financial operations autonomously. All responses carry status='draft'
and requiresConfirmation=true, enforced by the guardrail function.

Closes ancore-org#420
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 26, 2026

📝 Walkthrough

Walkthrough

This PR establishes two major service components: an AI agent service MVP offering draft-only intent creation with strict guardrails, and a complete OpenAPI specification for the relayer service with contract testing to ensure implementation fidelity. The changes add ~1,100 lines across type definitions, server implementations, test coverage, and API specification artifacts.

Changes

AI Agent Service MVP

Layer / File(s) Summary
Draft intent type contracts
services/ai-agent/src/types.ts
DraftIntentRequestSchema defines Zod-backed validation for prompt and accountId inputs. DraftIntent is a discriminated union of DraftPaymentIntent and DraftInvoiceIntent. DraftIntentResponse enforces status: 'draft' and requiresConfirmation: true.
Guardrail enforcement and server implementation
services/ai-agent/src/guardrail.ts, services/ai-agent/src/server.ts
enforceNoAutonomousExecution() validates that all responses use draft status and require user confirmation. Express app wires /health and POST /agent/draft-intent endpoints; request validation middleware uses Zod safeParse; parseDraftIntent() converts prompts to typed draft objects and applies the guardrail before responding.
API and guardrail test coverage
services/ai-agent/src/server.test.ts
Tests cover health endpoint responses, successful draft creation for both payment and invoice intents, request validation errors (missing/empty fields), and guardrail violations when draft status or confirmation requirement are violated.
Configuration, package setup, and documentation
services/ai-agent/package.json, services/ai-agent/tsconfig.json, services/ai-agent/tsconfig.test.json, services/ai-agent/README.md
Establishes npm scripts (build/test/lint), Jest configuration with ts-jest, TypeScript strict mode targeting ES2020. README documents endpoints, security boundaries (no autonomous execution, key management, fund movement), guardrail invariants, MVP limitations (stub parser, no auth/rate limiting), and build/test commands.

Relayer OpenAPI Specification and Contract Testing

Layer / File(s) Summary
OpenAPI 3.1 specification definition
services/relayer/openapi.yaml
Defines three endpoints: POST /relay/execute (requires BearerAuth, optional idempotency header), POST /relay/validate, and GET /relay/status (no auth). Component schemas include RelayExecuteRequest/Response, RelayValidateRequest, ValidationResult, HealthResponse with dependency status, RelayError code enum, and structured error responses with field-level validation details.
Documentation, type generation script, and README updates
services/relayer/README.md, scripts/generate-openapi-types.ts
README adds OpenAPI specification documentation (location, viewing, regeneration), example cURL commands for /relay/execute and /relay/validate with headers and payloads, and updated project structure. generate-openapi-types.ts dynamically imports openapi-typescript at runtime to read the OpenAPI YAML and write generated types to services/relayer/src/api/openapi-types.ts, with error handling for missing dependency and top-level invocation.
OpenAPI contract test suite
services/relayer/tests/contract/openapi-contract.test.ts
Jest/Supertest suite validates route existence/404 behavior, HTTP status codes across success/validation failure/missing auth/signature failure scenarios, response body shapes (including conditional transactionId), structured validation error format (error: VALIDATION_ERROR with details array), idempotency replay via idempotency-key header, allowed HTTP methods (GET vs POST), and error code compliance within OpenAPI-enumerated RelayError set.

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant validateBody as Validation Middleware
  participant parseDraftIntent as Intent Parser
  participant guardrail as Guardrail Check
  participant Response
  Client->>validateBody: POST /agent/draft-intent<br/>{ prompt, accountId }
  validateBody->>validateBody: Zod safeParse against schema
  validateBody->>parseDraftIntent: Call with validated prompt, accountId
  parseDraftIntent->>parseDraftIntent: Detect intent type<br/>Create DraftPaymentIntent or<br/>DraftInvoiceIntent with defaults
  parseDraftIntent->>guardrail: enforceNoAutonomousExecution(response)
  guardrail->>guardrail: Assert status='draft'<br/>Assert requiresConfirmation=true
  guardrail->>Response: Return 200 DraftIntentResponse
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

The PR introduces two substantial but independent service features: a complete AI agent MVP with guardrails and types, plus a formal OpenAPI spec with contract testing for the relayer. Changes are heterogeneous (types, server logic, tests, specs, scripts) across multiple directories. The core logic density is moderate (intent parsing, validation middleware, guardrail checks), and the contract test suite is comprehensive but methodical. No single file is trivial, but the patterns are consistent and well-scoped within each cohort.

Possibly related issues

  • ancore-org/ancore#420: This PR fully implements the AI Agent MVP acceptance criteria—service skeleton with health endpoint, draft intent endpoint with typed output, explicit non-execution guardrail, and security boundary documentation.

Possibly related PRs

  • ancore-org/ancore#706: Both PRs modify services/ai-agent/src/server.ts to add validation routes and Zod-backed schema handling for request validation.
  • ancore-org/ancore#347: This PR's relayer OpenAPI spec and contract tests directly exercise the /relay/execute, /relay/validate, and /relay/status behaviors (auth, signature, idempotency, error codes) that were introduced in the relayer MVP skeleton.

Poem

🐰 Hop, hop—the rabbits have woven a tale,
Draft intents safe, with guardrails that won't fail,
OpenAPI flows, contracts held true,
No autonomous hops—only humans say "do!"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The PR includes out-of-scope changes: OpenAPI spec and type generation for relayer service are unrelated to the ai-agent MVP bootstrap objective. Remove relayer service changes (openapi.yaml, generate-openapi-types.ts, openapi-contract.test.ts, relayer README updates) as they belong in a separate PR.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main change: bootstrapping the ai-agent MVP service from scaffold.
Linked Issues check ✅ Passed All acceptance criteria from #420 are met: service skeleton with health endpoint, typed draft intent endpoints, guardrails tested, and README documenting security boundaries.
Docstring Coverage ✅ Passed Docstring coverage is 83.33% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch main

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.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@drips-wave
Copy link
Copy Markdown

drips-wave Bot commented May 26, 2026

@olaleyeolajide81-sketch Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@wheval
Copy link
Copy Markdown
Contributor

wheval commented May 28, 2026

@olaleyeolajide81-sketch stick to one pr and resolve conflicts

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (6)
services/ai-agent/src/types.ts (1)

20-34: ⚡ Quick win

Consider adding validation for amount and asset fields.

The amount and asset fields in both DraftPaymentIntent and DraftInvoiceIntent are unvalidated strings. While string is appropriate for amount (to preserve precision), consider adding validation to ensure:

  • amount represents a valid non-negative decimal number
  • asset follows expected conventions (e.g., asset code format)

This would catch malformed data early and improve type safety for downstream consumers.

♻️ Example validation using Zod refinements
export interface DraftPaymentIntent {
  type: 'payment';
  destination: string;
  amount: string; // validated via schema
  asset: string;   // validated via schema
  memo?: string;
}

// Add schema validation
export const DraftPaymentIntentSchema = z.object({
  type: z.literal('payment'),
  destination: z.string().min(1),
  amount: z.string().regex(/^\d+(\.\d+)?$/, 'Amount must be a valid decimal'),
  asset: z.string().min(1).max(12), // Stellar asset codes are max 12 chars
  memo: z.string().optional(),
});
🤖 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 `@services/ai-agent/src/types.ts` around lines 20 - 34, The DraftPaymentIntent
and DraftInvoiceIntent types expose unvalidated amount and asset strings; add
runtime validation schemas (e.g., using Zod or similar) named
DraftPaymentIntentSchema and DraftInvoiceIntentSchema that enforce amount is a
non-negative decimal string (no leading +/- except optional zero, allow
fractional part) and that asset matches your expected asset-code rules (e.g.,
non-empty, max length/regex for allowed chars), validate destination/requestedBy
and optional fields too, export these schemas and use them to parse/validate
incoming data before constructing or passing
DraftPaymentIntent/DraftInvoiceIntent objects so malformed values are rejected
early.
services/ai-agent/package.json (1)

6-10: 💤 Low value

Consider adding a start script for convenience.

The package defines build, test, and lint scripts but no start script. Adding one would improve developer experience when running the service locally.

♻️ Suggested addition
   "scripts": {
     "build": "tsc --project tsconfig.json",
+    "start": "node dist/server.js",
     "test": "jest --coverage",
     "lint": "eslint src/"
   },
🤖 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 `@services/ai-agent/package.json` around lines 6 - 10, Add a "start" script to
the package.json "scripts" section so developers can run the built service
easily; update the "scripts" object to include a "start" entry (for production:
e.g. "start": "node dist/index.js" or combined build+run "start": "npm run build
&& node dist/index.js", or for local dev use "start:dev": "ts-node
src/index.ts") depending on your chosen entrypoint, and ensure the chosen
entrypoint (e.g., dist/index.js or src/index.ts) matches the project's compiled
output and existing entry module.
services/ai-agent/src/server.ts (1)

14-14: ⚡ Quick win

Consider making parseDraftIntent async now.

The comment indicates this will be replaced with LLM/NLP integration, which will require async/await. Making this function async in the MVP avoids a breaking change later and aligns the signature with the expected future implementation.

♻️ Refactor to async pattern
-function parseDraftIntent(prompt: string, accountId: string): DraftIntentResponse {
+async function parseDraftIntent(prompt: string, accountId: string): Promise<DraftIntentResponse> {

Then update the route handler at line 78:

-    const draft = parseDraftIntent(prompt, accountId);
+    const draft = await parseDraftIntent(prompt, accountId);
🤖 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 `@services/ai-agent/src/server.ts` at line 14, Change parseDraftIntent to an
async function signature (async function parseDraftIntent(...):
Promise<DraftIntentResponse>) and ensure its body returns a Promise (use return
values or Promise.resolve as needed); then update the route handler that calls
it (the handler referenced at line 78) to be async and await the call to
parseDraftIntent. Keep the function name parseDraftIntent and the
DraftIntentResponse type the same so references remain valid.
services/relayer/openapi.yaml (3)

319-404: ⚡ Quick win

Consider consolidating duplicate schemas using $ref.

RelayExecuteRequest and RelayValidateRequest are structurally identical. Consolidating them into a single shared schema (e.g., RelayRequestPayload) would improve maintainability and ensure validation rules stay synchronized.

♻️ Proposed refactor using shared schema

Add a shared base schema in components:

   schemas:
+    RelayRequestPayload:
+      type: object
+      required:
+        - sessionKey
+        - operation
+        - parameters
+        - signature
+        - nonce
+      properties:
+        sessionKey:
+          type: string
+          pattern: '^[0-9a-fA-F]{64}$'
+          minLength: 64
+          maxLength: 64
+          description: Hex-encoded Ed25519 session public key (64 characters)
+          example: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
+        operation:
+          type: string
+          enum: ['relay_execute', 'add_session_key', 'revoke_session_key']
+          description: Type of operation
+          example: "relay_execute"
+        parameters:
+          type: object
+          additionalProperties: true
+          description: Operation-specific parameters
+          example:
+            accountAddress: "GBBM6BKZPEBWYY3A3YR4IK7T7XZM5JC5K7NYGR7KDCXYBCJVPQYV5YAA"
+            to: "GD7OEZ2NYNQXK7FLTLQZZCNY2DZV5C7M3F4TNZBAYEBQKVU5RQV6SRQQ"
+            functionName: "transfer"
+            args: ["base64_encoded_xdr"]
+        signature:
+          type: string
+          pattern: '^[0-9a-fA-F]{128}$'
+          minLength: 128
+          maxLength: 128
+          description: Hex-encoded Ed25519 signature over canonical payload (128 characters)
+          example: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
+        nonce:
+          type: integer
+          minimum: 0
+          description: Replay-protection nonce (non-negative integer)
+          example: 1
+
     RelayExecuteRequest:
-      type: object
-      required:
-        - sessionKey
-        - operation
-        - parameters
-        - signature
-        - nonce
-      properties:
-        sessionKey:
-          type: string
-          pattern: '^[0-9a-fA-F]{64}$'
-          minLength: 64
-          maxLength: 64
-          description: Hex-encoded Ed25519 session public key (64 characters)
-          example: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-        operation:
-          type: string
-          enum: ['relay_execute', 'add_session_key', 'revoke_session_key']
-          description: Type of operation to execute
-          example: "relay_execute"
-        parameters:
-          type: object
-          additionalProperties: true
-          description: Operation-specific parameters
-          example:
-            accountAddress: "GBBM6BKZPEBWYY3A3YR4IK7T7XZM5JC5K7NYGR7KDCXYBCJVPQYV5YAA"
-            to: "GD7OEZ2NYNQXK7FLTLQZZCNY2DZV5C7M3F4TNZBAYEBQKVU5RQV6SRQQ"
-            functionName: "transfer"
-            args: ["base64_encoded_xdr"]
-        signature:
-          type: string
-          pattern: '^[0-9a-fA-F]{128}$'
-          minLength: 128
-          maxLength: 128
-          description: Hex-encoded Ed25519 signature over canonical payload (128 characters)
-          example: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
-        nonce:
-          type: integer
-          minimum: 0
-          description: Replay-protection nonce (non-negative integer)
-          example: 1
+      allOf:
+        - $ref: '`#/components/schemas/RelayRequestPayload`'
+        - description: Request payload for relay execution
 
     RelayValidateRequest:
-      type: object
-      required:
-        - sessionKey
-        - operation
-        - parameters
-        - signature
-        - nonce
-      properties:
-        sessionKey:
-          type: string
-          pattern: '^[0-9a-fA-F]{64}$'
-          minLength: 64
-          maxLength: 64
-          description: Hex-encoded Ed25519 session public key (64 characters)
-          example: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
-        operation:
-          type: string
-          enum: ['relay_execute', 'add_session_key', 'revoke_session_key']
-          description: Type of operation to validate
-          example: "relay_execute"
-        parameters:
-          type: object
-          additionalProperties: true
-          description: Operation-specific parameters
-          example:
-            accountAddress: "GBBM6BKZPEBWYY3A3YR4IK7T7XZM5JC5K7NYGR7KDCXYBCJVPQYV5YAA"
-            to: "GD7OEZ2NYNQXK7FLTLQZZCNY2DZV5C7M3F4TNZBAYEBQKVU5RQV6SRQQ"
-            functionName: "transfer"
-            args: ["base64_encoded_xdr"]
-        signature:
-          type: string
-          pattern: '^[0-9a-fA-F]{128}$'
-          minLength: 128
-          maxLength: 128
-          description: Hex-encoded Ed25519 signature over canonical payload (128 characters)
-          example: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
-        nonce:
-          type: integer
-          minimum: 0
-          description: Replay-protection nonce (non-negative integer)
-          example: 1
+      allOf:
+        - $ref: '`#/components/schemas/RelayRequestPayload`'
+        - description: Request payload for relay validation
🤖 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 `@services/relayer/openapi.yaml` around lines 319 - 404, RelayExecuteRequest
and RelayValidateRequest are identical; create a single shared schema (e.g.,
RelayRequestPayload) that defines sessionKey, operation, parameters, signature,
and nonce, then replace the RelayExecuteRequest and RelayValidateRequest
definitions with $ref entries pointing to RelayRequestPayload so both use the
same canonical validation rules (update schema names in components and
references to RelayExecuteRequest/RelayValidateRequest accordingly).

531-545: ⚡ Quick win

Add maxItems constraint to validation details array.

The details array lacks a maxItems constraint, which could allow unbounded error responses if a request fails validation on many fields. This creates a potential DoS vector.

🛡️ Proposed fix to add maxItems
         details:
           type: array
+          maxItems: 50
           items:
             type: object
🤖 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 `@services/relayer/openapi.yaml` around lines 531 - 545, The "details" array
schema is missing a maxItems constraint allowing unbounded items; update the
array definition for the "details" property (the array whose items are objects
with required "field" and "message" properties) to include a reasonable maxItems
(e.g., 10) to limit returned validation errors and prevent potential DoS; keep
the existing item schema (with "field" and "message") intact and add the
maxItems attribute directly on the array node in openapi.yaml.

417-417: transactionId hex case matches current relayer implementation.

The relayer’s mock transactionId is generated as randomBytes(32).toString('hex').toUpperCase(), producing 64 uppercase hex chars—so transactionId using ^[0-9A-F]{64}$ aligns with the implementation. Session key/signature validation in code accepts both cases (^[0-9a-fA-F]+$), so optionally make transactionId case-insensitive in OpenAPI (^[0-9a-fA-F]{64}$) for consistency and to tolerate lowercase from real sources.

🤖 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 `@services/relayer/openapi.yaml` at line 417, The OpenAPI schema for
transactionId currently restricts to uppercase hex via pattern '^[0-9A-F]{64}$';
update the pattern on the transactionId property in
services/relayer/openapi.yaml to accept both cases (e.g. '^[0-9a-fA-F]{64}$') so
it matches session key/signature validation and tolerates lowercase transaction
IDs generated by external sources while keeping the 64-byte length constraint.
🤖 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 `@services/ai-agent/src/server.ts`:
- Around line 66-67: The express.json() middleware call
(app.use(express.json())) lacks an explicit body size limit; update the
middleware to include a conservative limit (e.g., app.use(express.json({ limit:
'50kb' })) — pick the exact value appropriate for your intents) and add the same
explicit limit to express.urlencoded if used (app.use(express.urlencoded({
extended: true, limit: '50kb' }))); also add a short inline comment near app to
document why the limit is set (prevent large-payload DoS and protect sensitive
financial intent requests).
- Around line 73-81: The POST /agent/draft-intent route currently calls
parseDraftIntent synchronously and will crash the process if that function
throws; wrap the handler logic in a try-catch (or convert to async and await
inside try-catch) so errors from parseDraftIntent are caught and a 4xx/5xx JSON
error response is returned, or forward the error to next(err) for Express error
middleware; ensure validateBody(DraftIntentRequestSchema) stays and reference
parseDraftIntent, DraftIntentRequestSchema and the '/agent/draft-intent' route
when applying the fix.
- Around line 54-56: The current handler returns result.error.flatten() directly
(in the block where result.success is false), which leaks internal
schema/validation details; replace that with a sanitized payload—either a
generic message and an optional list of invalid field names only. Update the
response in the res.status(400).json call to call a sanitizer (e.g.,
sanitizeValidationError(result.error)) or inline logic that extracts only field
paths (e.g., map error details to their field names) and exclude messages,
schema types, or stack info; keep the status and shape ({ error: 'Invalid
request', fields: [...] }) so callers get minimal actionable info without
exposing internal rules.
- Around line 14-16: The parseDraftIntent function accepts prompt without
validation which risks DoS and memory issues; add input validation at the start
of parseDraftIntent(prompt: string, accountId: string): DraftIntentResponse to:
1) assert prompt is a non-empty string and trim it, 2) enforce a reasonable max
length (e.g. configurable constant) and reject or return an error
DraftIntentResponse when exceeded, 3) optionally sanitize/control characters
(e.g. remove excessive whitespace/newlines) and reject binary/invalid input, and
4) ensure failures return a safe, descriptive DraftIntentResponse or throw a
controlled error rather than proceeding with parsing.

In `@services/ai-agent/src/types.ts`:
- Line 9: The accountId schema currently allows any non-empty string; change the
zod validator for accountId (where it's defined as z.string().min(1)) to enforce
Stellar public key format by using a regex check (e.g., require a leading 'G'
and 55 base32 characters A-Z2-7 for a total length of 56) or
z.string().length(56). Replace z.string().min(1) with
z.string().regex(/^G[A-Z2-7]{55}$/, "Invalid Stellar public key") (or an
equivalent zod refine) so invalid Stellar addresses are rejected at validation
time.

---

Nitpick comments:
In `@services/ai-agent/package.json`:
- Around line 6-10: Add a "start" script to the package.json "scripts" section
so developers can run the built service easily; update the "scripts" object to
include a "start" entry (for production: e.g. "start": "node dist/index.js" or
combined build+run "start": "npm run build && node dist/index.js", or for local
dev use "start:dev": "ts-node src/index.ts") depending on your chosen
entrypoint, and ensure the chosen entrypoint (e.g., dist/index.js or
src/index.ts) matches the project's compiled output and existing entry module.

In `@services/ai-agent/src/server.ts`:
- Line 14: Change parseDraftIntent to an async function signature (async
function parseDraftIntent(...): Promise<DraftIntentResponse>) and ensure its
body returns a Promise (use return values or Promise.resolve as needed); then
update the route handler that calls it (the handler referenced at line 78) to be
async and await the call to parseDraftIntent. Keep the function name
parseDraftIntent and the DraftIntentResponse type the same so references remain
valid.

In `@services/ai-agent/src/types.ts`:
- Around line 20-34: The DraftPaymentIntent and DraftInvoiceIntent types expose
unvalidated amount and asset strings; add runtime validation schemas (e.g.,
using Zod or similar) named DraftPaymentIntentSchema and
DraftInvoiceIntentSchema that enforce amount is a non-negative decimal string
(no leading +/- except optional zero, allow fractional part) and that asset
matches your expected asset-code rules (e.g., non-empty, max length/regex for
allowed chars), validate destination/requestedBy and optional fields too, export
these schemas and use them to parse/validate incoming data before constructing
or passing DraftPaymentIntent/DraftInvoiceIntent objects so malformed values are
rejected early.

In `@services/relayer/openapi.yaml`:
- Around line 319-404: RelayExecuteRequest and RelayValidateRequest are
identical; create a single shared schema (e.g., RelayRequestPayload) that
defines sessionKey, operation, parameters, signature, and nonce, then replace
the RelayExecuteRequest and RelayValidateRequest definitions with $ref entries
pointing to RelayRequestPayload so both use the same canonical validation rules
(update schema names in components and references to
RelayExecuteRequest/RelayValidateRequest accordingly).
- Around line 531-545: The "details" array schema is missing a maxItems
constraint allowing unbounded items; update the array definition for the
"details" property (the array whose items are objects with required "field" and
"message" properties) to include a reasonable maxItems (e.g., 10) to limit
returned validation errors and prevent potential DoS; keep the existing item
schema (with "field" and "message") intact and add the maxItems attribute
directly on the array node in openapi.yaml.
- Line 417: The OpenAPI schema for transactionId currently restricts to
uppercase hex via pattern '^[0-9A-F]{64}$'; update the pattern on the
transactionId property in services/relayer/openapi.yaml to accept both cases
(e.g. '^[0-9a-fA-F]{64}$') so it matches session key/signature validation and
tolerates lowercase transaction IDs generated by external sources while keeping
the 64-byte length constraint.
🪄 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

Run ID: 1083fc96-f5a5-4675-9799-c8cc73602e56

📥 Commits

Reviewing files that changed from the base of the PR and between dca3264 and b3c3911.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • scripts/generate-openapi-types.ts
  • services/ai-agent/README.md
  • services/ai-agent/package.json
  • services/ai-agent/src/guardrail.ts
  • services/ai-agent/src/server.test.ts
  • services/ai-agent/src/server.ts
  • services/ai-agent/src/types.ts
  • services/ai-agent/tsconfig.json
  • services/ai-agent/tsconfig.test.json
  • services/relayer/README.md
  • services/relayer/openapi.yaml
  • services/relayer/tests/contract/openapi-contract.test.ts

Comment on lines +14 to +16
function parseDraftIntent(prompt: string, accountId: string): DraftIntentResponse {
const lower = prompt.toLowerCase();
const isInvoice = lower.includes('invoice') || lower.includes('request');
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 | 🟠 Major | ⚡ Quick win

Add input validation on prompt parameter.

The prompt parameter is accepted without any length or content validation. For a financial service, unbounded input poses DoS and memory consumption risks. Consider adding validation before processing.

🛡️ Proposed fix to add prompt validation
 function parseDraftIntent(prompt: string, accountId: string): DraftIntentResponse {
+  if (!prompt || prompt.length > 5000) {
+    throw new Error('Invalid prompt: must be 1-5000 characters');
+  }
   const lower = prompt.toLowerCase();
   const isInvoice = lower.includes('invoice') || lower.includes('request');
📝 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
function parseDraftIntent(prompt: string, accountId: string): DraftIntentResponse {
const lower = prompt.toLowerCase();
const isInvoice = lower.includes('invoice') || lower.includes('request');
function parseDraftIntent(prompt: string, accountId: string): DraftIntentResponse {
if (!prompt || prompt.length > 5000) {
throw new Error('Invalid prompt: must be 1-5000 characters');
}
const lower = prompt.toLowerCase();
const isInvoice = lower.includes('invoice') || lower.includes('request');
🤖 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 `@services/ai-agent/src/server.ts` around lines 14 - 16, The parseDraftIntent
function accepts prompt without validation which risks DoS and memory issues;
add input validation at the start of parseDraftIntent(prompt: string, accountId:
string): DraftIntentResponse to: 1) assert prompt is a non-empty string and trim
it, 2) enforce a reasonable max length (e.g. configurable constant) and reject
or return an error DraftIntentResponse when exceeded, 3) optionally
sanitize/control characters (e.g. remove excessive whitespace/newlines) and
reject binary/invalid input, and 4) ensure failures return a safe, descriptive
DraftIntentResponse or throw a controlled error rather than proceeding with
parsing.

Comment on lines +54 to +56
if (!result.success) {
res.status(400).json({ error: 'Invalid request', details: result.error.flatten() });
return;
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 | 🟠 Major | ⚡ Quick win

Sanitize validation error details before exposing to clients.

Returning result.error.flatten() directly exposes internal schema structure and validation details. For a financial service, this could leak information about expected request formats and business logic constraints that should remain internal.

🔒 Proposed fix to sanitize validation errors
     if (!result.success) {
-      res.status(400).json({ error: 'Invalid request', details: result.error.flatten() });
+      res.status(400).json({ error: 'Invalid request', message: 'Request body validation failed' });
       return;
     }

Alternatively, if some detail is needed for debugging, provide only field names:

     if (!result.success) {
+      const fieldErrors = result.error.flatten().fieldErrors;
+      const fields = Object.keys(fieldErrors);
-      res.status(400).json({ error: 'Invalid request', details: result.error.flatten() });
+      res.status(400).json({ error: 'Invalid request', invalidFields: fields });
       return;
     }
📝 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 (!result.success) {
res.status(400).json({ error: 'Invalid request', details: result.error.flatten() });
return;
if (!result.success) {
res.status(400).json({ error: 'Invalid request', message: 'Request body validation failed' });
return;
🤖 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 `@services/ai-agent/src/server.ts` around lines 54 - 56, The current handler
returns result.error.flatten() directly (in the block where result.success is
false), which leaks internal schema/validation details; replace that with a
sanitized payload—either a generic message and an optional list of invalid field
names only. Update the response in the res.status(400).json call to call a
sanitizer (e.g., sanitizeValidationError(result.error)) or inline logic that
extracts only field paths (e.g., map error details to their field names) and
exclude messages, schema types, or stack info; keep the status and shape ({
error: 'Invalid request', fields: [...] }) so callers get minimal actionable
info without exposing internal rules.

Comment on lines +66 to +67
const app = express();
app.use(express.json());
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 | 🟠 Major | ⚡ Quick win

Configure explicit request body size limit.

The express.json() middleware uses a default limit (100kb), but for a financial service handling sensitive intent requests, the limit should be explicitly configured and documented. This prevents potential DoS attacks via large payloads.

🛡️ Proposed fix to add explicit limit
   const app = express();
-  app.use(express.json());
+  app.use(express.json({ limit: '10kb' }));
📝 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
const app = express();
app.use(express.json());
const app = express();
app.use(express.json({ limit: '10kb' }));
🤖 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 `@services/ai-agent/src/server.ts` around lines 66 - 67, The express.json()
middleware call (app.use(express.json())) lacks an explicit body size limit;
update the middleware to include a conservative limit (e.g.,
app.use(express.json({ limit: '50kb' })) — pick the exact value appropriate for
your intents) and add the same explicit limit to express.urlencoded if used
(app.use(express.urlencoded({ extended: true, limit: '50kb' }))); also add a
short inline comment near app to document why the limit is set (prevent
large-payload DoS and protect sensitive financial intent requests).

Comment on lines +73 to +81
app.post(
'/agent/draft-intent',
validateBody(DraftIntentRequestSchema),
(req: Request, res: Response) => {
const { prompt, accountId } = req.body;
const draft = parseDraftIntent(prompt, accountId);
res.status(200).json(draft);
}
);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Add error handling to the route handler.

If parseDraftIntent throws an error (e.g., from the guardrail enforcement), the current implementation will crash the process. Wrap the logic in try-catch or add Express error-handling middleware.

🛡️ Proposed fix to add try-catch
   app.post(
     '/agent/draft-intent',
     validateBody(DraftIntentRequestSchema),
     (req: Request, res: Response) => {
+      try {
         const { prompt, accountId } = req.body;
         const draft = parseDraftIntent(prompt, accountId);
         res.status(200).json(draft);
+      } catch (error) {
+        res.status(500).json({ error: 'Internal server error' });
+      }
     }
   );

Alternatively, add a global error handler after all routes:

   return app;
+
+  // Error handling middleware
+  app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
+    console.error('Unhandled error:', err);
+    res.status(500).json({ error: 'Internal server error' });
+  });
 }
📝 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
app.post(
'/agent/draft-intent',
validateBody(DraftIntentRequestSchema),
(req: Request, res: Response) => {
const { prompt, accountId } = req.body;
const draft = parseDraftIntent(prompt, accountId);
res.status(200).json(draft);
}
);
app.post(
'/agent/draft-intent',
validateBody(DraftIntentRequestSchema),
(req: Request, res: Response) => {
try {
const { prompt, accountId } = req.body;
const draft = parseDraftIntent(prompt, accountId);
res.status(200).json(draft);
} catch (error) {
res.status(500).json({ error: 'Internal server error' });
}
}
);
🤖 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 `@services/ai-agent/src/server.ts` around lines 73 - 81, The POST
/agent/draft-intent route currently calls parseDraftIntent synchronously and
will crash the process if that function throws; wrap the handler logic in a
try-catch (or convert to async and await inside try-catch) so errors from
parseDraftIntent are caught and a 4xx/5xx JSON error response is returned, or
forward the error to next(err) for Express error middleware; ensure
validateBody(DraftIntentRequestSchema) stays and reference parseDraftIntent,
DraftIntentRequestSchema and the '/agent/draft-intent' route when applying the
fix.

/** Natural-language description of the intended operation */
prompt: z.string().min(1).max(1000),
/** Stellar account address of the initiating user */
accountId: z.string().min(1),
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 | 🟠 Major | ⚡ Quick win

Add Stellar address format validation to accountId.

The accountId field accepts any non-empty string but should validate that it matches Stellar's public key format (56 characters, starting with 'G', base32-encoded). Without this validation, invalid addresses can enter the system and cause downstream errors or security issues.

🛡️ Proposed fix to add Stellar address validation
 export const DraftIntentRequestSchema = z.object({
   /** Natural-language description of the intended operation */
   prompt: z.string().min(1).max(1000),
   /** Stellar account address of the initiating user */
-  accountId: z.string().min(1),
+  accountId: z.string().regex(/^G[A-Z2-7]{55}$/, 'Invalid Stellar account address'),
   /** Optional context for the intent (e.g. invoice ID, session key) */
   context: z.record(z.unknown()).optional(),
 });
🤖 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 `@services/ai-agent/src/types.ts` at line 9, The accountId schema currently
allows any non-empty string; change the zod validator for accountId (where it's
defined as z.string().min(1)) to enforce Stellar public key format by using a
regex check (e.g., require a leading 'G' and 55 base32 characters A-Z2-7 for a
total length of 56) or z.string().length(56). Replace z.string().min(1) with
z.string().regex(/^G[A-Z2-7]{55}$/, "Invalid Stellar public key") (or an
equivalent zod refine) so invalid Stellar addresses are rejected at validation
time.

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.

[AI] Bootstrap ai-agent service MVP from scaffold

2 participants