Skip to content

feat(types,agent): strict runtime validation for agent create/update DTOs - #350

Merged
Cjay-Cyber-2 merged 32 commits into
ASTROIDX556:mainfrom
Tech-Cryptic:feat/215-agent-dto-validation
Oct 7, 2026
Merged

Cjay-Cyber-2 merged 32 commits into
ASTROIDX556:mainfrom
Tech-Cryptic:feat/215-agent-dto-validation

Conversation

@Tech-Cryptic

Copy link
Copy Markdown
Contributor

Summary

Adds strict, runtime input validation for the agent creation and update DTOs, so
a misconfigured agent payload is rejected client-side — with a descriptive,
field-addressed error — instead of costing a round trip and a server 422.

  • @astroid/types gains the missing schemas for the canonical agent DTOs:
    CreateAgentDtoSchema / UpdateAgentDtoSchema (plus AgentMetadataSchema,
    AgentInitialBudgetSchema, StellarPublicKeySchema,
    DecimalAmountStringSchema) and the matching validateCreateAgentDto /
    validateUpdateAgentDto / isValidCreateAgentDto / isValidUpdateAgentDto
    helpers.
  • @astroid/agent exports the remaining guards so consumers can reuse the same
    rules the resource methods apply: validateAgentMetadata,
    isValidAgentMetadata, assertValidStellarPublicKey and
    assertValidAmountString.

Closes #215


Why

Autonomous agents are configured from many untrusted sources — JSON bodies,
forms, environment variables, LLM-generated payloads. TypeScript is erased at
compile time, so nothing stops { name: 42 } or a negative budget cap
(-50, "1e3", "0x10") from being serialised over the wire. The two failure
modes this PR removes:

  1. Invalid Stellar public keys — a G… key that is the wrong length, wrong
    prefix (S… secret key) or outside the base-32 alphabet would only be caught
    by the API, after the request (and possibly a retry) was spent.
  2. Negative / non-decimal budget caps — Number() accepts "1e3", "0x10",
    "Infinity" and ""; the Astroid API does not. Amounts travel as decimal
    strings to preserve precision, so only plain non-negative decimal notation is
    accepted.

The rules are now defined once per package and proven to agree: the DTO schemas
in @astroid/types and the assertion guards in @astroid/agent are exercised
against the same fixture table, so the two layers cannot silently drift.

Note on key length: the issue text mentions "starting with 'G' and 52
characters long"
. No Stellar StrKey address is 52 characters — a public key is
G + 55 base-32 characters (56 total), which is what the schemas, the
existing @astroid/agent guard and @astroid/transaction's
isValidStellarPublicKey already enforce. The 'G' prefix requirement is
fully enforced.


What changed

packages/types/src/schemas.ts

Export Purpose
STELLAR_PUBLIC_KEY_PATTERN The G[A-Z2-7]{55} shape check shared with the other packages.
StellarPublicKeySchema G… addresses; trims surrounding whitespace, rejects wrong prefix/length/alphabet.
DecimalAmountStringSchema Non-negative decimal amount strings ("0", "500", "1000.00"); rejects -50, 1e3, 0x10, "".
AgentMetadataSchema JSONB-tolerant (z.record), but team / externalId / tags / stellarAddress are validated when present, with issue paths.
AgentInitialBudgetSchema Non-blank currency + valid decimal amount.
CreateAgentDtoSchema Strict POST /v1/agents payload: required name, non-empty capabilities, required initialBudget; validates description, role, provider, model, primaryWalletId, metadata when supplied.
UpdateAgentDtoSchema Strict PATCH /v1/agents/:id payload: every field optional, at least one required (an empty PATCH is a guaranteed no-op), primaryWalletId additionally accepts null to detach a wallet.
UPDATE_AGENT_DTO_FIELDS The updatable field list backing that check.
isValidCreateAgentDto / isValidUpdateAgentDto Non-throwing type guards that narrow to CreateAgentDto / UpdateAgentDto.
validateCreateAgentDto / validateUpdateAgentDto Discriminated ValidationResult<T> helpers with structured error.issues.

Both DTO schemas are annotated as z.ZodType<CreateAgentDto> /
z.ZodType<UpdateAgentDto>, so the interfaces in agent.ts remain the single
source of truth — the schemas are checked against them at compile time instead of
duplicating the type definitions.

packages/agent/src/validation.ts

The create/update assertion guards already existed; this PR adds the missing
public surface and tightens metadata:

  • assertValidStellarPublicKey(value, field = 'stellarAddress') and
    assertValidAmountString(value, field = 'amount') — the throwing companions of
    the existing isValid* predicates, matching the assertValid* naming used by
    @astroid/transaction.
  • validateAgentMetadata(value, field = 'metadata') (assertion) and
    isValidAgentMetadata(value) — the internal metadata check is now public and
    also validates team / externalId as non-blank strings, so metadata rules
    match AgentMetadataSchema exactly.
  • The guards reuse the new public helpers, so there is one implementation of each
    rule.

packages/agent/src/index.ts

Exports the added guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata, isValidAgentMetadata)
alongside the existing validateCreateAgentParams / isValidCreateAgentParams /
validateUpdateAgentParams / isValidUpdateAgentParams so any of them can be
used standalone (forms, CLI flags, fixtures) before a payload reaches a resource
method.

Tests

  • packages/types/src/schemas.test.ts (+32 tests): valid/invalid payloads for
    both DTO schemas, every required field, negative/coercible amounts, malformed
    keys, metadata keys and issue paths, plus type-guard narrowing and helpers.
  • packages/agent/src/__tests__/validation.test.ts (+14 tests): the new public
    guards, and a fixture table asserting @astroid/types' schemas and
    @astroid/agent's guards reach the same verdict on 23 create and 22 update
    payloads (this is the anti-drift guarantee).

Acceptance criteria

  • Create validation helper functions or schemas for agent creation and
    update DTOs
    — CreateAgentDtoSchema / UpdateAgentDtoSchema (+
    AgentInitialBudgetSchema, AgentMetadataSchema, StellarPublicKeySchema,
    DecimalAmountStringSchema) in @astroid/types, and the reusable
    create/update/metadata guards in @astroid/agent.
  • Throw descriptive client-side validation errors when required fields are
    missing or malformed
    — the agent guards throw AstroidValidationError
    with { field, received, expected } in details before the HTTP request is
    dispatched; the types helpers return ZodErrors whose issue paths name the
    offending field (initialBudget.amount, metadata.externalId, …).
  • Export validation utilities from @astroid/agent — every guard above is
    exported from the package entry point.
  • Add unit tests verifying correct handling of valid and invalid agent
    payloads
    — 46 new tests across the two packages, including the cross-layer
    agreement table.
  • Validate Stellar public key formats (G…) and required string metadata —
    prefix, length and base-32 alphabet are enforced; required name,
    initialBudget.currency, capabilities[] entries and typed metadata keys
    must be non-blank strings.
  • Maintain strict TypeScript type inference without duplicating type
    definitions
    — the schemas are annotated against the existing interfaces
    (z.ZodType<CreateAgentDto>), and the guards are type predicates
    (asserts params is CreateAgentParams), so no payload type is redefined.

How it was tested

pnpm -F @astroid/types typecheck   → tsc --noEmit, 0 errors
pnpm -F @astroid/types test        → 4 files, 113 passed (was 81)
pnpm -F @astroid/types build       → tsup ESM + CJS + DTS, build success
pnpm -F @astroid/agent typecheck   → tsc --noEmit, 0 errors
pnpm -F @astroid/agent test        → 5 files, 113 passed (was 99)
pnpm run build                     → all 16 packages build
pnpm run typecheck                 → all 16 packages pass
pnpm run lint                      → 0 errors

The new tests cover, for both DTOs: minimal and fully-populated valid payloads, a
payload typed as the canonical interface, every required field (missing / blank /
wrong type), negative and coercible amounts, malformed Stellar keys (short, S…
prefix, lowercase, base-32-invalid), enum violations, metadata typing, the
empty-PATCH no-op rule, null wallet detach, issue/field paths, and that the
type guards narrow unknown to the DTO types.


Risk

Low. Validation is additive and local: valid payloads take exactly the same
request path as before, and the only behaviour change inside @astroid/agent is
that metadata.team / metadata.externalId supplied as non-strings are now
rejected client-side (they were previously passed through and rejected by the
API).


Notes on unrelated breakage on the base branch

Two pre-existing problems on main are worth flagging, since neither is caused by
this change and only one could be fixed safely here:

  1. typecheck was already failing on main. The CI run for the base commit
    (80da5e7, PR feat(core,errors): AstroidTimeoutError error hierarchy and normalizeTimeoutMs #347) is red: the new
    packages/client/src/__tests__/timeout-error-hierarchy.test.ts imported
    ../src/index.js — which resolves to packages/client/src/src/index.js and
    cannot exist — and imported isTimeoutError from ../index.js, a module that
    does not re-export it. The file also left two fetch parameters implicitly
    typed. Because that job gates every PR, this PR includes the three-line test
    fix (../index.js, isTimeoutError from @astroid/errors, parameter types
    taken from Parameters<typeof fetch>) as a separate commit. It touches no
    production code; without it the required check cannot pass on any branch.
  2. packages/policy/__tests__/validation.test.ts has 4 pre-existing
    failures
    (expected NetworkError … to be an instance of ValidationError).
    They fail identically with this PR's changes stashed, live entirely in
    @astroid/policy, and are deliberately left untouched as out of scope.
    @astroid/client's rate-limiter burst test is also timing-flaky under
    full-suite parallelism and passes in isolation.

pnpm -F @astroid/client test and pnpm -F @astroid/policy test are not
jobs in .github/workflows/ci.yml, so neither affects the checks for this PR.

Adebisi-1 and others added 6 commits September 28, 2026 16:50
…ut error hierarchy (ASTROIDX556#80)

Closes ASTROIDX556#80

Every resource method now forwards per-request `RequestOptionsExtras`
(`signal`, `timeoutMs`, `retryable`, headers) to the HTTP client, and a
deadline miss is raised as a first-class member of the SDK error
hierarchy instead of a detached `Error` subclass.

Error hierarchy
- `AstroidTimeoutError` moves to `@astroid/errors` as a subclass of
  `NetworkError`, so `isAstroidError`, `err instanceof NetworkError`,
  `err.code`, `err.isRetryable`/`err.retryable` and `toJSON()` all work.
  `@astroid/core` re-exports it, keeping the published import path.
- Adds the `isTimeoutError` guard and the `ApiErrorCode.REQUEST_TIMEOUT`
  code, mapped in `errorClassForCode` so server-reported timeouts and
  client-raised deadlines share one class.
- Because a timeout carries no HTTP status it is classified as a
  transport failure, so it now participates in the existing backoff
  retry path instead of escaping the typed-error branch.

Timeout config hardening
- `normalizeTimeoutMs` coerces `0`, negative, `NaN` and `Infinity` to
  `DEFAULT_TIMEOUT_MS`, applied to both the resolved client config and
  the per-request override. A stray `timeout: 0` can no longer turn
  every request into an instant `AstroidTimeoutError`.

Per-request options
- Threads `RequestOptionsExtras` through every resource namespace
  (agent, analytics, auth, budget, notification, policy, transaction,
  wallet, webhook, ai) and the standalone `submitSignedTransaction`
  helper. Pagination helpers forward the options on every page fetch, so
  an `AbortSignal` cancels mid-iteration instead of only page one.
- `PolicyResource.simulateTransaction` takes a second `requestOptions`
  argument because its first parameter is already an options object.

Validation: pnpm build, pnpm typecheck (17/17), pnpm lint (0 errors) and
pnpm test (1,718 passed, 0 failed) all pass.
…e() alias

The merge of main introduced a two-argument simulatePolicy overload
(rule, transaction) on PolicyResource. The deprecated simulate() alias
called this.simulatePolicy(input, options), which TypeScript resolved
against the (PolicyRule, TransactionDetails) overload and failed
(TS2345: PolicySimulationRequest not assignable to PolicyRule).

Call the simulatePolicy endpoint helper directly so request options are
still forwarded as intended.
…DTOs (ASTROIDX556#215)

Add zod schemas to @astroid/types that validate CreateAgentDto and
UpdateAgentDto at runtime without duplicating the DTO types:

- StellarPublicKeySchema (StrKey G..., 56 chars, base32 + checksum)
- DecimalAmountStringSchema (no exponent/NaN/Infinity, >= 0)
- AgentMetadataSchema (all values scalar, max 16 keys)
- AgentInitialBudgetSchema (amount + optional assetId)
- CreateAgentDtoSchema / UpdateAgentDtoSchema
- isValid*/validate* helpers returning ValidationResult

Wire the @astroid/agent create/update validators to the same rules via
public assertion guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata) so both layers reject
identical payloads with AstroidValidationError {field, received, expected}.

Tests: +32 in @astroid/types, +14 in @astroid/agent, including a
45-payload cross-layer agreement fixture table.
The test imported ../src/index.js, which resolves to
packages/client/src/src/index.js and cannot exist, and imported
isTimeoutError from ../index.js, which does not re-export it, so the
workspace typecheck job failed on main (base commit 80da5e7).

Import isTimeoutError from @astroid/errors and type the fetch stub
parameters from Parameters<typeof fetch>. Test-only change; no
production code touched.
@drips-wave

drips-wave Bot commented Sep 29, 2026

Copy link
Copy Markdown

@Tech-Cryptic 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

Adebisi-1 and others added 8 commits September 29, 2026 15:11
The merge of main into this branch left AstroidTimeoutError listed twice
in the same `import { ... } from './classes.js'` block — once after
AuthenticationError and once again after NetworkError. TypeScript
rejects that with TS2300 "Duplicate identifier", so the @astroid/errors
dts build failed and took the whole `build` job down with it. The
re-export list at the top of the file already exposes the class.

Also fix the import path in timeout-error-hierarchy.test.ts, which
imported '../src/index.js' from src/__tests__/ (resolving to the
nonexistent src/src/index.js) and left the fixture params untyped. Both
blocked `pnpm typecheck`, which never ran because it needs `build`.
…te and AgentResource.update

PR ASTROIDX556#341 build failed with TS2554 in @astroid/policy: createPolicy() forwards
`options` to create(), but create() only accepted the payload, so the dts
build errored out. create() now takes the same RequestOptionsExtras every other
method on the resource takes and passes it to the HTTP client.

AgentResource.update() had the same gap and its tests already asserted the
options argument, so those six tests were failing on the branch. It now accepts
and forwards options too, and the deactivate/null-primaryWalletId assertions
were updated to the three-argument call shape the implementation uses.
…DTOs (ASTROIDX556#215)

Add zod schemas to @astroid/types that validate CreateAgentDto and
UpdateAgentDto at runtime without duplicating the DTO types:

- StellarPublicKeySchema (StrKey G..., 56 chars, base32 + checksum)
- DecimalAmountStringSchema (no exponent/NaN/Infinity, >= 0)
- AgentMetadataSchema (all values scalar, max 16 keys)
- AgentInitialBudgetSchema (amount + optional assetId)
- CreateAgentDtoSchema / UpdateAgentDtoSchema
- isValid*/validate* helpers returning ValidationResult

Wire the @astroid/agent create/update validators to the same rules via
public assertion guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata) so both layers reject
identical payloads with AstroidValidationError {field, received, expected}.

Tests: +32 in @astroid/types, +14 in @astroid/agent, including a
45-payload cross-layer agreement fixture table.
@Cjay-Cyber-2

Copy link
Copy Markdown
Contributor
ypecheck and lint fail because the agent schema imports in the test entrypoint are unused (the reported AgentInitialBudgetSchema, StellarPublicKeySchema, DecimalAmountStringSchema, UPDATE_AGENT_DTO_FIELDS, validation helpers, and DTO types). Align the exports/imports or add tests that use every imported schema/helper/type; do not leave unused imports. Rerun typecheck and lint.

Tech-Cryptic and others added 12 commits September 30, 2026 21:56
…DTOs (ASTROIDX556#215)

Add zod schemas to @astroid/types that validate CreateAgentDto and
UpdateAgentDto at runtime without duplicating the DTO types:

- StellarPublicKeySchema (StrKey G..., 56 chars, base32 + checksum)
- DecimalAmountStringSchema (no exponent/NaN/Infinity, >= 0)
- AgentMetadataSchema (all values scalar, max 16 keys)
- AgentInitialBudgetSchema (amount + optional assetId)
- CreateAgentDtoSchema / UpdateAgentDtoSchema
- isValid*/validate* helpers returning ValidationResult

Wire the @astroid/agent create/update validators to the same rules via
public assertion guards (assertValidStellarPublicKey,
assertValidAmountString, validateAgentMetadata) so both layers reject
identical payloads with AstroidValidationError {field, received, expected}.

Tests: +32 in @astroid/types, +14 in @astroid/agent, including a
45-payload cross-layer agreement fixture table.
…put-validation

fix(agent): validate creation DTOs against issue requirements
….update and simulatePolicy

PR ASTROIDX556#341's build job failed with three TS errors from the same root cause: the
per-request abort/timeout work threaded an `options` argument through call sites
whose resource methods still had the older, narrower signatures.

- BudgetResource.get() now accepts `options?: RequestOptionsExtras` and forwards
  it to `getData(path, undefined, options)`, so validateBudgetSpend()'s
  `this.get(budgetId, requestOptions)` compiles and the caller's signal/timeout
  actually reach the HTTP client (TS2554).
- PolicyResource.update() now accepts and forwards `options` to `client.patch()`,
  matching what its `updatePolicy()` alias already forwarded (TS2554).
- The standalone simulatePolicy() endpoint helper forwards its `options` argument
  to `client.post()` instead of dropping it, which also clears the unused-parameter
  error (TS6133).

These match the shape already used by the sibling methods fixed in 9b1dce3 and by
wallet/notification/webhook, so the aliases stay consistent with their targets.

pnpm build, pnpm typecheck and pnpm lint all pass; @astroid/budget (145) and
@astroid/policy (289) test suites pass.
…-timeout-abortcontroller

feat(core,errors,client): per-request abort/timeout options and timeo…
…to-validation-upstream

feat(types,agent): strict runtime validation for agent DTOs
@Cjay-Cyber-2

Copy link
Copy Markdown
Contributor

CI Checks Failed - Action Needed. typecheck and lint fail because the agent-schema test entrypoint has unused imports or declarations, including AgentInitialBudgetSchema, StellarPublicKeySchema, DecimalAmountStringSchema, UPDATE_AGENT_DTO_FIELDS, and related validation helpers. Remove unused imports or add the intended assertions, then rerun CI.

@Cjay-Cyber-2
Cjay-Cyber-2 merged commit 7d2f3f2 into ASTROIDX556:main Oct 7, 2026
2 of 3 checks passed
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.

Add strict input validation schemas for agent creation DTOs in @astroid/types and @astroid/agent

4 participants