Skip to content

About

Integration layer for Stellar Agent Guard (Phase 2 complete, published to npm). Pre-flight simulation, zero-broadcast fee estimation, and framework middleware.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

 
 

Repository files navigation

Stellar Agent Guard

CI License: MIT Node 24+

Stellar Agent Guard — SDK

Non-custodial TypeScript SDK and pre-flight policy interception firewall for AI agents on Stellar.

An autonomous agent holding a wallet has a single point of failure: one prompt-injection or one buggy loop can drain it. Stellar Agent Guard makes that impossible on-chain — the agent's funds stay in its own smart account, and every transaction the account must authorize is intercepted by the contract's and rejected pre-broadcast unless it satisfies the operator's installed policy: per-transaction spend caps, a rolling-window spend limit, recipient/asset allowlists, protocol allowlists, a pause switch, and a dead-man switch. This SDK provides the integration layer: pre-flight simulation interception, zero-broadcast fee estimation, agent-auth transaction signing, and dual-stream event telemetry for AI agent frameworks (LangChain, ElizaOS).

Status: Phase 2 complete — and the package is published. stellar-agent-guard-sdk@0.1.1 is live on the npm registry (npm install stellar-agent-guard-sdk). All five enforcement scenarios were proven against live Stellar testnet (protocol 28) with real contract IDs, transaction hashes, and diagnostic events — evidence is recorded in tests/fixtures/integration-evidence.md. Phase 2 code is merged into main with green CI (ci status check). For historical release notes and publish pipeline reconciliation, see docs/publishing-history.md.

🎯 What makes this different

Enforcement happens inside the account itself, via Soroban's native Custom Account Abstraction — not in a wrapper contract in front of funds, and not in an off-chain service.

  • Pre-flight simulation without broadcast: The SDK evaluates guard approval against Soroban RPC before a single byte hits the network. If the transaction violates policy, it is rejected client-side with the contract's own reason code, incurring zero network fees.
  • Dual-stream telemetry: Blocked decisions never commit to the ledger because Soroban rolls back failed authorizations. A listener that only tails committed ledger events sees a guard that appears to approve everything. The SDK extracts event_auth_checked from simulation diagnostics as well as committed blocks.
  • In-process simulation pricing: CostPreChecker computes network resource and inclusion fees directly from the enforced simulation, avoiding dependencies on external profiling tools.
  • Framework middleware: Plug-and-play middleware for LangChain and validators for ElizaOS halt execution before external tool calls run.

⚠️ Disclaimer: This is unaudited security tooling that gates real fund access. Do not deploy to mainnet without an independent audit. See the contracts repo's SECURITY.md.

What it does

  • Pre-flight policy interception (PreFlightInterceptor): Intercepts contract calls before broadcast, simulates auth authorization, and returns a discriminated admissible, blocked, or undetermined verdict. Never throws on policy refusal; an opt-in short-lived cache can reduce repeated simulation RPC calls within the current ledger.
  • In-process cost pre-checking (CostPreChecker): Prices transaction execution from simulation results, reporting resource fees, inclusion fees, and total fees against an optional ceiling.
  • Autonomous transaction execution (invoke()): Executes the full Soroban lifecycle: probe simulation, auth signing for custom accounts, enforced simulation, and broadcast with bounded exponential-backoff retry for stale ledger resource limits (scecExceededLimit).
  • Framework adapters:
    • createLangChainGuardMiddleware: Halts tool execution if the interceptor blocks the planned action.
    • createGuardValidator: ElizaOS action validator returning boolean verdicts before actions run.
  • Telemetry listener (GuardTelemetryListener): Tails both committed events and diagnostic streams, decoding contract topics and reason codes.

Quick Start

Installation

npm install stellar-agent-guard-sdk

Module Format & Environment Note: stellar-agent-guard-sdk is published strictly as pure ESM ("type": "module") targeting Node.js >= 24.0.0 (declared in engines).

If your project or toolchain runs in CommonJS (e.g. legacy LangChain setups, Jest configs, or .cjs scripts), load the SDK using the dynamic await import() pattern:

// CommonJS (.cjs or package without "type": "module")
async function run() {
  const { PreFlightInterceptor, CostPreChecker } = await import("stellar-agent-guard-sdk");
  // use interceptor, cost pre-checker, etc.
}

(Or build locally from source with Node 24+)

git clone https://github.com/aigbagbobila/stellar-agent-guard-sdk.git
cd stellar-agent-guard-sdk
npm ci
npm run build

Live testnet suite (.env.phase2)

The unit suite needs no credentials. The live suite (npm run test:integration) runs real transactions against the deployed Phase 2 testnet instance and reads its signing keys from a gitignored .env.phase2. Start from the committed template, which documents every key and what each one unlocks without carrying a value:

cp .env.phase2.example .env.phase2   # then fill in the values
npm run deploy:phase2                # or provision a fresh instance and write it for you
npm run test:integration

tests/integration/harness.ts validates the file up front. When it is incomplete it fails once with every missing key named — not one key per run, which would make setup a guessing game of five round trips:

.env.phase2 is incomplete: 3 required key(s) are missing:
  - PHASE2_ADMIN_SECRET
  - PHASE2_RECIPIENT_SECRET
  - PHASE2_OUTSIDER_SECRET

Copy the documented template and fill it in:  cp .env.phase2.example .env.phase2
Or provision a fresh instance (writes the file, including PHASE2_ISSUER_SECRET):  npm run deploy:phase2

The required keys are PHASE2_GUARD, PHASE2_TOKEN, PHASE2_ADMIN_SECRET, PHASE2_AGENT_SECRET, PHASE2_RECIPIENT_SECRET and PHASE2_OUTSIDER_SECRET, plus optionally PHASE2_RPC_URL; PHASE2_ISSUER_SECRET is additionally required to (re)deploy. .env.phase2 is gitignored (as are all .env.* values files — only *.example templates are committable); never commit the filled-in copy.

Pre-flight Policy Interception

import { Keypair, rpc } from "@stellar/stellar-sdk";
import { PreFlightInterceptor } from "stellar-agent-guard-sdk";

const interceptor = new PreFlightInterceptor({
  server: new rpc.Server("https://soroban-testnet.stellar.org"),
  networkPassphrase: "Test SDF Network ; September 2015",
  guard: "CAPADGEK457RHKN4RYVUMDJTFHDSG7R5HREQONKLYK7MFKC5WFENPP44",
  agent: Keypair.fromSecret(process.env.AGENT_SECRET!),
  source: Keypair.fromSecret(process.env.SOURCE_SECRET!),
});

const decision = await interceptor.check({
  contract: "CDCYDGBGS5AZ5BZS6XY2SK2PHJHSOEGTN3N4INCK34KF6GU2BGC7Z6MB",
  fn: "transfer",
  args: [/* from, to, amount */],
});

if (decision.kind === "admissible") {
  console.log("Allowed! Resource fee:", decision.estimatedResourceFee);
} else if (decision.kind === "blocked") {
  console.log("Blocked by guard:", decision.reason);
} else {
  console.log("Undetermined (fails closed)");
}

Throw vs. Verdict Contract

Pre-flight policy interception makes an intentional asymmetric distinction between programmer errors and policy outcomes:

  • Input validation throws InvalidInputError (synchronous): If a ContractCall is malformed (invalid StrKey contract ID, missing or non-symbol-shaped function name, invalid arguments array, or non-i128 amount), interceptor.check() throws InvalidInputError synchronously without dispatching any network RPC request.
  • Policy refusals return a verdict (kind: "blocked"): When input is valid but policy disallows the action (spend cap exceeded, recipient not allowlisted, account paused), this represents expected guardrail operation. check() returns { allowed: false, kind: "blocked", reason, explanation, ... } instead of throwing.
  • Callers requiring a throw-on-refusal flow can use interceptor.assertAllowed(call), which throws GuardBlockedError on blocked and PreFlightUndeterminedError on undetermined.

Optional simulation-result cache

PreFlightInterceptor always performs a fresh simulation by default. For agent loops that repeatedly check the same call, caching can be enabled explicitly:

const interceptor = new PreFlightInterceptor({
  server,
  networkPassphrase,
  guard,
  agent,
  source,
  cache: {
    ttlLedgers: 1, // maximum one approximate five-second ledger window
    policyRevision: () => readPolicyRevision(),
  },
});

const first = await interceptor.check(call);
const second = await interceptor.check(call); // may reuse the first verdict
interceptor.invalidate();                       // clear all entries
interceptor.invalidate(call);                  // clear one call's entries

The cache is disabled unless cache is supplied. It stores only actual admissible and blocked decisions; transient undetermined results are not cached. A cache key includes the contract, function, canonical XDR argument fingerprint, interceptor identity, and the supplied policy revision. The cache is discarded when the observed ledger advances, when the TTL expires, or when invalidate() is called. ttlMs and ttlLedgers are both capped at one approximate ledger-close interval; if both are supplied, ttlMs takes precedence.

A cached verdict can be staler than one admitted transfer. The rolling spend window can change after a simulation while a cached result is still being reused, so callers that cannot tolerate that tradeoff should leave caching off, use a shorter TTL, provide a policy revision, and invalidate after policy or account-state changes.

Pipeline step observability (onStep)

invoke() accepts an optional onStep callback. When omitted, behavior is exactly as before — the hook is pure observability and the SDK itself never logs anything (and takes no logger dependency; what you do with the events is up to you):

const outcome = await invoke({
  server,
  source,
  call,
  networkPassphrase,
  guardAuth,
  onStep(step) {
    // consumer decides how to display/log the event
    console.log(`[${step.attempt}] ${step.name} ${step.status} in ${step.durationMs}ms`);
  },
});

Event shape (InvokeStepEvent):

Field Meaning
name Pipeline stage: probe → sign → simulate → broadcast (the shared TRACE_STEP_NAMES vocabulary).
status start (emitted immediately before the stage runs), then ok or fail.
durationMs Elapsed time of this stage attempt in milliseconds — not the total invoke() duration. Always 0 on start.
attempt 0-based retry index. 0 for the first pass; 1 on the built-in stale-ledger re-run. Always present.

The callback is optional, receives every stage attempt (a retried invoke emits a full probe → sign → simulate → broadcast sequence per attempt, each tagged with its attempt index), and callback exceptions are isolated: a throwing onStep never breaks the pipeline, never turns a successful invoke into a failure, and never masks the original pipeline error — callback errors are swallowed silently, since the SDK is logger-agnostic and has no sink to report them to. Step names come from the same shared vocabulary the dry-run trace uses (TRACE_STEP_NAMES), so consumers of either see identical stage names.

The LangChain adapter exposes the same capability:

const middleware = createLangChainGuardMiddleware({
  interceptor,
  toContractCall: (request) => (/* ... */),
  onStep(step) {
    // enforcement-stage events (probe → sign → simulate; never broadcast)
  },
});

Framework Middleware (LangChain & ElizaOS)

When deploying fleets of hundreds or thousands of autonomous agents derived from identical templates or scheduled loops, fixed poll intervals (e.g. exactly every 5s) cause all instances to poll RPC nodes in lockstep phase. This creates synchronized traffic spikes (thundering herds) against public Soroban RPC endpoints, triggering aggressive HTTP 429 rate limits and cascade backpressure errors.

GuardTelemetryListener.watch() defaults to jitter: 'full', which uniformly randomizes each poll delay in [intervalMs * (1 - j), intervalMs] with j = 0.2 (a 20% variance window). This breaks lockstep fleet synchronization while keeping polling responsive and bounded. Deterministic fixed interval cadence can be restored when needed by specifying jitter: 'none'.

// Follow event telemetry with full jitter (default)
for await (const events of listener.watch({
  pollIntervalMs: 5_000,
  jitter: "full", // uniformly distributed in [4000ms, 5000ms]
})) {
  console.log(`Received ${events.length} guard event(s)`);
}

Framework Middleware (LangChain & ElizaOS)

Plug-and-play middleware intercepts agent actions before tools are executed:

Policy encode/decode round trip

decodePolicy is the canonical read-side counterpart to policyToScVal. It returns bigint for every integer field, keeps ProtocolRule.fns: null distinct from an empty array, normalizes Stellar addresses, and rejects missing, duplicate, unknown, unsorted, or wrongly typed fields with a path-bearing PolicyDecodeError.

import {
  decodePolicy,
  policyToScVal,
  type PolicyConfig,
} from "stellar-agent-guard-sdk";

const policy: PolicyConfig = {
  per_tx_cap: 1_000n,
  window_secs: 60n,
  window_cap: 150n,
  assets: ["CDCYDGBGS5AZ5BZS6XY2SK2PHJHSOEGTN3N4INCK34KF6GU2BGC7Z6MB"],
  protocols: [
    {
      contract: "CDCYDGBGS5AZ5BZS6XY2SK2PHJHSOEGTN3N4INCK34KF6GU2BGC7Z6MB",
      fns: ["transfer"],
    },
  ],
  recipients: ["GAOBCRXTCO4ZCBNHALJUMJJ5JDXNOUZ7U6VZJX4UBTXAHQEO66IPU6PH"],
  allow_any_recipient: false,
  active_from: 0n,
  active_until: 0n,
  paused: false,
  dms_grace_secs: 0n,
};

const encoded = policyToScVal(policy);
const decoded = decodePolicy(encoded); // exactly equal to policy

The decoder accepts the direct ScVal::Map returned by the deployed policy() read and also the one-element Vec representation used by some RPC/host surfaces for Some(PolicyConfig). ScVal::Void means no policy is installed and therefore throws rather than fabricating a default-deny config. Canonical u64/i128 variants are range checked exactly; base-10 ScVal::String integers are accepted as a deliberate compatibility path for stringly-typed RPC/telemetry payloads and normalized to bigint.

Debug a blocked transfer with invoke({ dryRun: true })

Dry run executes the real probe, authorization signing, and enforced-simulation path, then returns the verdict, diagnostics, network-derived fees, and per-stage timings. It stops before final transaction assembly and cannot call sendTransaction, so its result has no transaction hash or submission object.

import { invoke } from "stellar-agent-guard-sdk";

const debug = await invoke({
  server,
  source,
  call: blockedTransferCall,
  networkPassphrase,
  guardAuth: { guard, agent },
  dryRun: true,
});

if (debug.kind === "dry_run") {
  console.log({
    admissible: debug.admissible,
    verdict: debug.verdict,
    reason: debug.reason,
    fees: debug.fees,
  });
  console.table(debug.steps);
}

A blocked or undetermined dry run reports an explicit all-zero charged fee breakdown; an admissible dry run reports the simulation's resource fee plus the SDK's 100-stroop inclusion floor. Missing, negative, malformed, unsafe-number, or out-of-u64-range fee payloads are ContractResponseError failures and remain undetermined—never free. steps[].ok describes whether a stage completed, not whether policy approved the call; the separate verdict field is the policy answer. The probe → sign → simulate stages are measured through the same onStep hook a live invocation uses, so a dry-run trace and an onStep trace are the same measurement of the same code.

Troubleshooting agent authentication

verifyAgentSignature lets an agent runtime check that a signature belongs to the key it believes is registered before entering an agent loop. The helper is verify-only: it never accepts, signs with, stores, or derives a private key. Other SDK APIs continue to accept caller-created Keypair objects for transaction/authorization signing as before.

import { verifyAgentSignature } from "stellar-agent-guard-sdk";

function signerMatches(
  registeredPublicKey: string | Uint8Array,
  hostSignaturePayload: Uint8Array,
  signature: Uint8Array,
): boolean {
  return verifyAgentSignature(
    registeredPublicKey,
    hostSignaturePayload,
    signature,
  );
}

hostSignaturePayload must be the exact 32-byte host digest covered by the signature; the helper verifies those bytes without re-hashing and is not SEP-53 message signing. A successful result proves only the key/payload/signature relationship—it does not validate network ID, invocation, nonce, expiration ledger, or transaction freshness.

Typed errors and 0.1.x migration

All SDK-owned errors now share a GuardError base. invoke() remains result-oriented: inspect outcome.error with instanceof when outcome.kind === "error".

import {
  BroadcastError,
  GuardError,
  SigningError,
  SimulationError,
} from "stellar-agent-guard-sdk";

if (outcome.kind === "error") {
  if (outcome.error instanceof SigningError) {
    console.error("agent signer is wrong or unavailable");
  } else if (outcome.error instanceof SimulationError) {
    console.error("enforcement could not be determined");
  } else if (outcome.error instanceof BroadcastError) {
    console.error("submission failed", outcome.error.transactionHash);
  } else if (outcome.error instanceof GuardError) {
    console.error(outcome.error.message);
  }
}

A pipeline stage that throws — a dropped RPC connection, say — is reported the same way: as kind: "error" carrying the original error as the typed error's cause, never as a rejection. A guardrail that could not reach a verdict has no business throwing at its caller, and the original error object stays reachable for diagnostics.

If ledger state changes after enforced simulation and the included transaction is then refused by the guard, invoke() still returns kind: "blocked" with the contract reason and diagnostics. That charged outcome additionally carries transactionHash and charged: true; it is not flattened into a technical BroadcastError.

GuardBlockedError keeps its published name, message, fields, and instanceof behavior and now extends GuardError; PreFlightUndeterminedError extends SimulationError. The hierarchy, decodePolicy, and verifyAgentSignature are additive to 0.1.x callers. The one intentional behavior change is for callers already using dryRun: true: the old success sentinel (kind: "error") is replaced by the structured kind: "dry_run" result documented above. Non-dry-run callers keep their existing outcome shapes.

API Reference

Interception & Execution

  • PreFlightInterceptor
    • constructor(options: PreFlightInterceptorOptions) — Pass cache: { ttlMs } or cache: { ttlLedgers } to opt into the short-lived cache; omit it for fresh simulations.
    • check(call: ContractCall): Promise<PreFlightDecision> — Returns admissible | blocked | undetermined without throwing or broadcasting.
    • assertAllowed(call: ContractCall): Promise<AdmissibleDecision> — Asserts allowed or throws GuardBlockedError.
    • invalidate(call?: ContractCall): void — Clears all cached decisions or only entries for one call.
  • CostPreChecker
    • constructor(options: CostPreCheckerOptions)
    • check(call: ContractCall): Promise<CostPreCheckResult> — Returns within_budget | over_budget | blocked | undetermined.
    • checkWithCost(call: ContractCall): Promise<{ decision, cost }> — Returns the interceptor verdict and the cost of the same single simulation. Prefer this over calling check() on both classes.
  • invoke(options: InvokeOptions): Promise<InvokeResult> — End-to-end pipeline: probe, sign auth, simulate, and broadcast. Accepts an optional onStep(step: InvokeStepEvent) hook reporting per-stage start/ok/fail timing events with a 0-based retry attempt index; callback exceptions are isolated and omitting the hook changes nothing.

Fee units: stroops and XLM

CostPreChecker reports fees as exact integer stroops — the raw value is the source of truth, and it is what a maxFeeStroops ceiling is compared against. formatFee() renders the same number in XLM, the unit operators think in, using integer math only (XLM has 7 decimal places; float rounding on money-adjacent output in a security tool is not acceptable) and with no trailing zeros:

import { formatFee } from "stellar-agent-guard-sdk";

cost.totalFeeStroops;            // 12345n           — stroops (exact, source of truth)
formatFee(cost.totalFeeStroops); // "0.0012345"      — same value in XLM

formatFee(1n);             // "0.0000001" — one stroop
formatFee(9_999_999n);     // "0.9999999" — largest sub-XLM value

One simulation per check: prefer checkWithCost

PreFlightInterceptor.check() answers may this proceed? and CostPreChecker.check() answers what will it cost? — but calling both runs the enforced simulation twice, against two ledger snapshots. The extra RPC is the lesser problem: the fee reported for a call can then differ from the fee implied by the verdict that was actually enforced, so the price no longer corresponds to the approved decision.

CostPreChecker.checkWithCost() returns both from a single simulation:

const { decision, cost } = await costChecker.checkWithCost(call);

if (decision.kind === "blocked") {
  console.log("refused:", decision.reason);        // nothing was charged
} else if (cost.kind === "over_budget") {
  console.log("too expensive:", formatFee(cost.totalFeeStroops), "XLM");
} else if (decision.allowed) {
  console.log("approved at", formatFee(cost.totalFeeStroops), "XLM");
}

Prefer this over calling interceptor.check(call) and costChecker.check(call) in sequence. That two-call pattern still works and its types are unchanged, but it carries the fee-drift caveat above. precheckCostWithDecision() is the one-shot form.

Telemetry & Helpers

  • docs/event-schema.md — every telemetry event and field, each labelled with its stability tier: Stable (relied on), Append-only (new values may appear, existing ones will not be removed or renamed), Best-effort (may change in any release), Internal (implementation detail, not a contract).
  • GuardTelemetryListener
    • constructor(options: GuardTelemetryListenerOptions)
    • watch(signal?: AbortSignal): AsyncIterable<GuardEventPage> — Tails on-chain and uncommitted events.
  • policyToScVal(policy: PolicyConfig): xdr.ScVal — Encodes a policy as the contract's canonical sorted ScVal struct.
  • decodePolicy(scVal: xdr.ScVal): PolicyConfig — Strictly decodes policy()/set-policy ScVal data, including Option/Vec and numeric normalization.
  • policyFromScVal(scVal) — Alias for callers using the issue's original function name.
  • invoke(options) / invoke({ ...options, dryRun: true }) — Broadcasts an admissible result, or returns the full pre-broadcast dry-run trace.
  • verifyAgentSignature(publicKey, payload, signature): boolean — Verify-only Ed25519 check against a strkey or raw 32-byte key.
  • GuardError, SimulationError, SigningError, BroadcastError, PolicyDecodeError, and ContractResponseError — Typed failure hierarchy.
  • decodeCheckResult(raw): CheckResult — Decodes Allowed or Blocked(reason).
  • decodeAuthDecision(event: SorobanRpc.Api.GetEventsResponse.Event): AuthDecisionEvent | null
  • guardEventsFromDiagnostics(events: xdr.DiagnosticEvent[]): GuardEvent[] — Each decoded GuardEvent carries a stable id: ledger:<txHash>:<topic> for committed events, diag:<sha256> for blocked ones (which are rolled back and have no hash to anchor on). Same event re-parsed → same id; two different blocks in one simulation → different ids. Format and collision notes: docs/event-schema.md.
  • explainReason(reason: string | number): string — Human-readable explanation of contract reason codes.
  • isDeadManFrozen(status: GuardStatus): boolean
  • deadManRemaining(status: GuardStatus, policy: PolicyConfig | null): bigint | null

Architecture

Stellar Agent Guard operates across three dedicated repositories:

┌─────────────────────────────────────────────────────────────────────────┐
│                      Operator (Browser / Freighter)                     │
│                                     │                                   │
│                                     ▼                                   │
│              stellar-agent-guard-dashboard (Next.js / UI)               │
└─────────────────────────────────────┬───────────────────────────────────┘
                                      │
                                      ▼
┌─────────────────────────────────────────────────────────────────────────┐
│                   AI Agent Runtime (LangChain / ElizaOS)                │
│                                     │                                   │
│                                     ▼                                   │
│                stellar-agent-guard-sdk (TypeScript / RPC)               │
│               • Pre-flight policy check  • Cost pre-checks              │
│               • Agent-auth tx signing    • Event telemetry              │
└─────────────────────────────────────┬───────────────────────────────────┘
                                      │
                                      ▼ Soroban RPC
┌─────────────────────────────────────────────────────────────────────────┐
│               stellar-agent-guard-contracts (Soroban / Rust)             │
│            • CustomAccount interface (`__check_auth`)                   │
│            • Spend caps, rolling window, allowlists, dead-man switch    │
└─────────────────────────────────────────────────────────────────────────┘
Repository Role Documentation
stellar-agent-guard-contracts Soroban smart contracts implementing Custom Account Abstraction and spending policy firewall GitBook Docs
stellar-agent-guard-sdk (this repo) TypeScript SDK for pre-flight interception, simulation pricing, and AI agent framework integration GitHub
stellar-agent-guard-dashboard Client-side operator dashboard for policy deployment, inspection, and emergency panic-button freeze GitHub

✅ Verified against live testnet

Proven against a real deployed instance on Stellar testnet (protocol 28, Test SDF Network ; September 2015):

  • Guard (custom account): CAPADGEK457RHKN4RYVUMDJTFHDSG7R5HREQONKLYK7MFKC5WFENPP44
  • SAC Token: CDCYDGBGS5AZ5BZS6XY2SK2PHJHSOEGTN3N4INCK34KF6GU2BGC7Z6MB
  • WASM bytecode hash: f47919f92e78fdd034836aa61955fc338dd56a218c448c37df1867a8c3da0f63 (identical to Phase 1 artifact)

5/5 Live Enforcement Scenarios

Scenario Condition Result Evidence
1. Within caps Transfer 100 within caps (cap: 1000, window: 150) Allowed Tx hash f8f5b3c51b85c8777c956d71330d015fcf72fa57548077d8b32969f8ba9c762e at ledger 4704849
2. Per-tx cap Transfer 1001 > 1000 cap Blocked (per_tx_cap_exceeded) Diagnostic event event_auth_checked, blocked, per_tx_cap_exceeded, pre-broadcast, 0 fees
3. Rolling window Transfer 76 + 76 = 152 > 150 window cap Blocked (window_cap_exceeded) Rolling window accumulation refusal, balances untouched
4. Recipient allowlist Transfer to unlisted recipient Blocked (recipient_not_allowed) Default-deny address check refusal
5. Account paused Call while paused = true Blocked (paused) Account-state refusal cleanly distinguished from policy caps

Complete run output and assertion logs are preserved in tests/fixtures/integration-evidence.md.

Honest limitations

  • Enforcement boundary for arbitrary calls: Full amount/recipient limits are native to SAC token transfers. Arbitrary Soroban contract calls are enforced via protocol/function allowlists, active window, pause, and dead-man switches; per-call amount limits are not available generically from host auth contexts (tracked as v2).
  • Single-key agent signing today, multi-key prepared: A guard account registers one Ed25519 agent key, and buildGuardAuthEntry signs the authorization digest with it. The signing path is now a seam (AgentSigner: sign a 32-byte digest, return signature bytes) and every public config accepts either an AgentSigner or a plain Keypair — so the current single-key behaviour is unchanged, and threshold/multi-key agent signing lands behind the same interface when the contracts repo's v2 decision does. Research, the recommended wire shape, and the revisit trigger: docs/concepts/multi-key-agent-signing.md.
  • AutoGPT integration: AutoGPT lacks an extensible pre-execution interceptor hook at the surveyed revision; findings and future integration paths are documented in docs/integration-hooks.md.
  • Testnet signing credentials: Running npm run test:integration requires .env.phase2 populated with funded testnet keypairs. The live suite is not run on every PR: it is (a) required locally before any PR that touches the enforcement path (src/tx.ts, src/invoke.ts, src/policy.ts, src/preflight.ts), with fresh evidence committed to tests/fixtures/integration-evidence.md and CI-verified as present, and (b) run automatically on a weekly schedule (.github/workflows/live-suite.yml) to catch host/testnet drift. A green ci therefore means the required checks ran — not that the live suite ran against this change.

Enforcement scope — read this before relying on the caps

Full recipient/amount enforcement — spend caps, allowlists, per-transaction limits — is native and automatic for SAC token transfers (transfer/transfer_from), since these are the calls whose arguments the Soroban auth context exposes for inspection. For other Soroban contract calls made by the guarded account (arbitrary DEX/lending/protocol calls), the policy engine still enforces window and pause state, but per-call amount/recipient limits are not yet enforced — extending fine-grained enforcement to arbitrary calls is tracked as a v2 item, not implied as already covered.

This boundary is an inherent property of the platform (the auth context does not expose arbitrary call arguments generically), not a gap this project hides or overclaims. The classification that produces this boundary (AssetTransfer vs Protocol vs Unknown default-deny) is spelled out in SPEC §6.

Maintainers

Name GitHub Telegram
Hybrid @aigbagbobila @aigbagbobila

Socials

Contact

License

Licensed under MIT. This is unaudited security tooling that gates real fund access — see the contracts repo's SECURITY.md before considering mainnet use.

Contributing

See CONTRIBUTING.md for details on coding standards, PR process, and project structure — including the strict one-commit-per-logical-unit rule.

Looking for something to work on? The issue backlog holds scoped issues with Summary / Acceptance Criteria / Tech Stack — good first tasks for the Drips Stellar Wave contributor sprints.

Contributors

About

Integration layer for Stellar Agent Guard (Phase 2 complete, published to npm). Pre-flight simulation, zero-broadcast fee estimation, and framework middleware.

Resources

Contributing

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages