Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,8 @@ gv-team.md
.windsurf/
.zencoder/
skills/
!packages/sardis-openclaw/skills/
!packages/sardis-openclaw/skills/**
skills-lock.json
AGENTS.md

Expand Down
3 changes: 0 additions & 3 deletions packages/sardis-js/src/core/engine.ts
Original file line number Diff line number Diff line change
Expand Up @@ -340,7 +340,6 @@ export class Engine {
// ───────────────────────────────────────────────────────── helpers

private buildUrl(path: string, params?: Record<string, unknown>): string {
const url = new URL(path, this.baseURL + '/');
// Preserve full path (URL collapses "/" if baseURL has no path).
const full = path.startsWith('http') ? path : `${this.baseURL}${path.startsWith('/') ? '' : '/'}${path}`;
const u = new URL(full);
Expand All @@ -354,8 +353,6 @@ export class Engine {
}
}
}
// `url` is intentionally unused — left for static analysis.
void url;
return u.toString();
}

Expand Down
26 changes: 26 additions & 0 deletions packages/sardis-openclaw/skills/audit/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
name: sardis-audit
description: Teach an OpenClaw agent to inspect Sardis transactions, preserve evidence, and verify authority before reporting payment state.
homepage: https://sardis.sh
user-invocable: false
---

# Sardis Audit

Use this skill only when `SARDIS_API_KEY` is present. If it is missing, state that Sardis audit data is unavailable.

## Audit Flow

Use `sardis_list_transactions` to inspect the activity ledger before summarizing payment history, card activity, or budget usage.

Use `sardis_check_balance` before claiming available spend capacity.

Use `sardis_check_policy` before saying a future spend is allowed.

## Reporting Rules

Report Sardis transaction identifiers, status, merchant, amount, currency, and evidence references when available.

Distinguish `pending`, `requires_approval`, `denied`, `settled`, and `revoked` states.

Never claim a payment completed from intent text alone. The ledger status is the source of truth.
32 changes: 32 additions & 0 deletions packages/sardis-openclaw/skills/payments/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
---
name: sardis-payments
description: Teach an OpenClaw agent to use Sardis payment verbs with policy checks, reversibility awareness, and fail-closed handling.
homepage: https://sardis.sh
user-invocable: false
---

# Sardis Payments

Use this skill only when `SARDIS_API_KEY` is present. If it is missing, do not create wallets, issue cards, or attempt payments.

## Payment Verbs

Use `sardis_give_wallet` to provision the agent payment identity.

Use `sardis_spend` only after a successful `sardis_check_policy` result.

Use `sardis_pay_invoice` for invoice-shaped requests instead of manually constructing a payment when invoice metadata is available.

Use `sardis_issue_card` only when the user has explicitly requested a card and the policy result permits it.

Use `sardis_freeze_card` when a card is revoked, compromised, outside mandate, or no longer needed.

## Fail-Closed Rules

If Sardis returns `requires_approval`, surface the approval requirement and wait.

If Sardis returns `deny`, stop the payment path.

If Sardis is unavailable, treat the action as denied until the service is reachable again.

Never expose raw card data, private keys, API keys, signing secrets, or provider tokens.
26 changes: 26 additions & 0 deletions packages/sardis-openclaw/skills/spending-policy/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
name: sardis-spending-policy
description: Teach an OpenClaw agent to set budgets, check policy before spend, and honor Sardis allow / requires_approval / deny outcomes.
homepage: https://sardis.sh
user-invocable: false
---

# Sardis Spending Policy

Use this skill only when `SARDIS_API_KEY` is present. If it is missing, stop and ask the operator to configure Sardis before attempting any money movement.

## Required Flow

1. Create or update a budget with `sardis_set_budget` before the first spend.
2. Run `sardis_check_policy` before every `sardis_spend`.
3. Continue only when the policy result is `allow`.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize MCP policy approvals

The skill pack advertises that these instructions work with the Sardis MCP server (packages/sardis-openclaw/README.md lines 17-19), but this flow only permits spending when sardis_check_policy returns the literal agent-tools outcome allow. The MCP implementation serializes policy results as allowed: true/false and decision.outcome: 'APPROVED' | 'BLOCKED' (packages/sardis-mcp-server/src/tools/policy.ts lines 64 and 412), so an OpenClaw agent using the supported MCP server will fail closed even for allowed payments because step 3 never matches. Include the MCP result shape, or scope the skill to agent-tools outcomes, before requiring the spend step.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recognize MCP policy approvals

The skill pack advertises that these instructions work with the Sardis MCP server (packages/sardis-openclaw/README.md lines 17-19), but this flow only permits spending when sardis_check_policy returns the literal agent-tools outcome allow. The MCP implementation serializes policy results as allowed: true/false and decision.outcome: 'APPROVED' | 'BLOCKED' (packages/sardis-mcp-server/src/tools/policy.ts lines 64 and 412), so an OpenClaw agent using the supported MCP server will fail closed even for allowed payments because step 3 never matches. Include the MCP result shape, or scope the skill to agent-tools outcomes, before requiring the spend step.

Useful? React with 👍 / 👎.

4. Pause when the result is `requires_approval`.
5. Refuse when the result is `deny`.

## Guardrails

Never infer authority from user wording alone. The Sardis policy result is the authority boundary.

Never split one payment into smaller payments to bypass a budget, approval threshold, merchant block, token block, or time window.

Never retry a denied action with changed fields unless the operator explicitly changes the budget or mandate first.
15 changes: 0 additions & 15 deletions packages/sardis/src/sardis/cli/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,18 +83,3 @@ def close(self) -> None:
if self._client:
self._client.close()
self._client = None


def get_client(ctx) -> SardisAPIClient:
"""Get API client from context."""
config = ctx.obj["config"]

api_key = config.get("api_key")
if not api_key:
raise click.ClickException("Not authenticated. Run 'sardis login' first.")

return SardisAPIClient(
base_url=config.get("api_base_url", "https://api.sardis.sh"),
api_key=api_key,
)

138 changes: 0 additions & 138 deletions packages/sardis/src/sardis/cli/commands/demo.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,141 +204,3 @@ def demo(ctx, chain: str, port: int):

console.print(Syntax(code_example, "python", theme="monokai"))
console.print()


# ---------------------------------------------------------------------------
# Post-payment aha moment
# ---------------------------------------------------------------------------

def show_post_payment_experience(
*,
tx_id: str = "tx_demo_00001",
tx_hash: str = "0xabcd1234...ef567890",
amount: float = 10.0,
merchant: str = "localhost:8402",
chain: str = "tempo_moderato",
blocked: bool = False,
) -> None:
"""Display the guided post-payment experience after a demo payment.

Called after ``sardis pay`` succeeds or is blocked in demo mode.
"""
if blocked:
# Red: blocked payment
console.print(Panel(
f"[bold red]Payment Blocked[/bold red]\n\n"
f" Amount: [red]${amount:.2f} USDC[/red]\n"
f" Merchant: [red]{merchant}[/red]\n"
f" Reason: [yellow]Exceeds spending mandate limit[/yellow]\n\n"
" The spending policy correctly prevented this transaction.\n"
" This is Sardis in action — agents can reason, but they\n"
" cannot be trusted with money without guardrails.",
border_style="red",
title="Policy Enforcement",
))
else:
# Green: successful payment
explorer_base = {
"tempo_moderato": "https://moderato.explorer.caldera.xyz/tx",
"base_sepolia": "https://sepolia.basescan.org/tx",
"base": "https://basescan.org/tx",
}
explorer_url = f"{explorer_base.get(chain, explorer_base['base_sepolia'])}/{tx_hash}"

console.print(Panel(
f"[bold green]Payment Successful[/bold green]\n\n"
f" Amount: [green]${amount:.2f} USDC[/green]\n"
f" Merchant: [cyan]{merchant}[/cyan]\n"
f" TX ID: [cyan]{tx_id}[/cyan]\n"
f" TX Hash: [cyan]{tx_hash}[/cyan]\n"
f" Chain: [cyan]{chain}[/cyan]\n"
f" Explorer: [link={explorer_url}]{explorer_url}[/link]\n"
" Audit: Run [cyan]sardis ledger list[/cyan] to inspect the local evidence trail.",
border_style="green",
title="Transaction Receipt",
))

# What's next menu
console.print()
console.print("[bold]What's next?[/bold]\n")

next_steps = Table(show_header=False, box=None, padding=(0, 2))
next_steps.add_column("Num", style="bold cyan", width=3)
next_steps.add_column("Action", style="white")
next_steps.add_column("Command", style="dim")

next_steps.add_row("1", "Try a blocked payment (exceeds limit)", "sardis pay --to binance.com --amount 50000")
next_steps.add_row("2", "Set a custom spending limit", "sardis mandates create --per-tx 50 --daily 200")
next_steps.add_row("3", "Add wallet to an AI agent", "sardis agents create --name my-agent")
next_steps.add_row("4", "View audit trail", "sardis ledger list")
next_steps.add_row("5", "Read the public API guide", "https://sardis.sh/docs")

console.print(next_steps)
console.print()


# ---------------------------------------------------------------------------
# Degraded state handlers
# ---------------------------------------------------------------------------

def handle_wallet_creation_failure(chain: str) -> dict[str, str]:
"""Fallback when MPC wallet creation fails — use pre-funded EOA pool."""
console.print("[yellow]Warning: MPC wallet creation failed. Using pre-funded EOA fallback.[/yellow]")
fallback_id = f"wal_fallback_{int(time.time()) % 100000:05d}"
fallback_addr = "0xFa11...bAcK0001"
console.print(f"[yellow] Fallback wallet: {fallback_id} ({fallback_addr})[/yellow]")
console.print("[yellow] This wallet has limited signing capabilities.[/yellow]")
return {"wallet_id": fallback_id, "address": fallback_addr}


def handle_faucet_empty(chain: str) -> None:
"""Handle sponsor/faucet being empty."""
console.print(f"[yellow]Warning: Testnet faucet on {chain} appears to be empty.[/yellow]")
if chain != "base_sepolia":
console.print("[yellow] Suggestion: Try --chain base_sepolia which has a more reliable faucet.[/yellow]")
console.print("[yellow] Run: sardis demo --chain base_sepolia[/yellow]")
else:
console.print("[yellow] The Base Sepolia faucet is temporarily unavailable.[/yellow]")
console.print("[yellow] You can still explore mandates and policy features without a funded wallet.[/yellow]")


def handle_port_in_use(preferred_port: int) -> int | None:
"""Handle preferred port being in use — auto-increment through range."""
actual = _find_free_port(preferred_port, preferred_port + 8)
if actual is None:
console.print(f"[yellow]Warning: All ports {preferred_port}-{preferred_port + 8} are in use.[/yellow]")
console.print("[yellow] Mock merchant server could not be started.[/yellow]")
console.print("[yellow] Free up a port or specify a different one: sardis demo --port 9000[/yellow]")
return None
if actual != preferred_port:
console.print(f"[yellow]Warning: Port {preferred_port} in use, using {actual} instead.[/yellow]")
return actual


def handle_rpc_down(chain: str, max_retries: int = 3) -> bool:
"""Handle mid-session RPC being down — retry with backoff."""
for attempt in range(1, max_retries + 1):
console.print(f"[yellow] RPC retry {attempt}/{max_retries}...[/yellow]")
time.sleep(min(attempt * 0.5, 2.0))
console.print(f"[red]Testnet RPC on {chain} appears to be down.[/red]")
console.print("[red] This is a known issue with public testnets.[/red]")
console.print("[red] The demo sandbox is still usable for policy and mandate exploration.[/red]")
return False


def handle_no_network() -> None:
"""Handle complete network unavailability (ConnectionError)."""
console.print(Panel(
"[bold yellow]No Network Connection[/bold yellow]\n\n"
" Sardis demo requires network access for:\n"
" - Testnet wallet creation\n"
" - Faucet funding\n"
" - On-chain transactions\n\n"
" The mock merchant server and policy engine work offline.\n"
" To run the full demo, check your internet connection.\n\n"
" For offline exploration, use the Python SDK in simulation mode:\n"
" from sardis import SardisClient\n"
" client = SardisClient() # auto simulation mode",
border_style="yellow",
title="Offline Mode",
))
6 changes: 2 additions & 4 deletions packages/sardis/src/sardis/resources/treasury.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,7 @@ async def list_financial_accounts(
refresh: bool = False,
timeout: float | TimeoutConfig | None = None,
) -> list[FinancialAccount]:
params: dict[str, object] = {"refresh": refresh}
params["refresh"] = str(refresh).lower()
params: dict[str, object] = {"refresh": str(refresh).lower()}
if account_token:
params["account_token"] = account_token
data = await self._get("treasury/financial-accounts", params=params, timeout=timeout)
Expand Down Expand Up @@ -123,8 +122,7 @@ def list_financial_accounts(
refresh: bool = False,
timeout: float | TimeoutConfig | None = None,
) -> list[FinancialAccount]:
params: dict[str, object] = {"refresh": refresh}
params["refresh"] = str(refresh).lower()
params: dict[str, object] = {"refresh": str(refresh).lower()}
if account_token:
params["account_token"] = account_token
data = self._get("treasury/financial-accounts", params=params, timeout=timeout)
Expand Down
3 changes: 0 additions & 3 deletions scripts/pr_maintenance.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,8 @@ def non_success_check_count(status_check_rollup: list[dict[str, Any]]) -> int:
count = 0
for check in status_check_rollup:
conclusion = check.get("conclusion")
status = check.get("status")
if conclusion in {"SUCCESS", "SKIPPED"}:
continue
if status in {"COMPLETED"} and conclusion in {"SUCCESS", "SKIPPED"}:
continue
count += 1
return count

Expand Down
Loading