* add documentation for ai agents * docs: consolidate CLAUDE.md and CONTRIBUTING.md, add style guidelines - Refactor CLAUDE.md to reference CONTRIBUTING.md for build/test/format info - Expand CONTRIBUTING.md with comprehensive build targets, env vars, and testing - Add code style guidelines section with conventional commits - Standardize SDK prettier config to use single quotes (matching web) - Add project-level Claude Code settings to disable co-author attribution * style(sdk): apply prettier with single quotes Run prettier across sdk/base and sdk/package to apply the standardized quote style (single quotes matching web). * docs: add USER.md for per-developer TODO filtering - Add agents/USER.md to .gitignore (contains user identifier) - Document session startup flow in CLAUDE.md: - Create USER.md if missing, prompting for identifier - Filter TODOs by @username tags - Offer relevant TODOs on session start * docs: add i18n documentation task to agent TODOs * docs: document i18n ID patterns in core/ Add agents/i18n-patterns.md covering rust-i18n setup, translation file format, t!() macro usage, key naming conventions, and locale selection. Remove completed TODO item and add reference in CLAUDE.md. * chore: clarify that all builds work on any OS with Docker
7.0 KiB
rpc-toolkit
StartOS uses rpc-toolkit for its JSON-RPC API. This document covers the patterns used in this codebase.
Overview
The API is JSON-RPC (not REST). All endpoints are RPC methods organized in a hierarchical command structure.
Handler Functions
There are four types of handler functions, chosen based on the function's characteristics:
from_fn_async - Async handlers
For standard async functions. Most handlers use this.
pub async fn my_handler(ctx: RpcContext, params: MyParams) -> Result<MyResponse, Error> {
// Can use .await
}
from_fn_async(my_handler)
from_fn_async_local - Non-thread-safe async handlers
For async functions that are not Send (cannot be safely moved between threads). Use when working with non-thread-safe types.
pub async fn cli_download(ctx: CliContext, params: Params) -> Result<(), Error> {
// Non-Send async operations
}
from_fn_async_local(cli_download)
from_fn_blocking - Sync blocking handlers
For synchronous functions that perform blocking I/O or long computations.
pub fn query_dns(ctx: RpcContext, params: DnsParams) -> Result<DnsResponse, Error> {
// Blocking operations (file I/O, DNS lookup, etc.)
}
from_fn_blocking(query_dns)
from_fn - Sync non-blocking handlers
For pure functions or quick synchronous operations with no I/O.
pub fn echo(ctx: RpcContext, params: EchoParams) -> Result<String, Error> {
Ok(params.message)
}
from_fn(echo)
ParentHandler
Groups related RPC methods into a hierarchy:
use rpc_toolkit::{Context, HandlerExt, ParentHandler, from_fn_async};
pub fn my_api<C: Context>() -> ParentHandler<C> {
ParentHandler::new()
.subcommand("list", from_fn_async(list_handler).with_call_remote::<CliContext>())
.subcommand("create", from_fn_async(create_handler).with_call_remote::<CliContext>())
}
Handler Extensions
Chain methods to configure handler behavior.
Ordering rules:
with_about()must come AFTER other CLI modifiers (no_display(),with_custom_display_fn(), etc.)with_call_remote()must be the LAST adapter in the chain
| Method | Purpose |
|---|---|
.with_metadata("key", Value) |
Attach metadata for middleware |
.no_cli() |
RPC-only, not available via CLI |
.no_display() |
No CLI output |
.with_display_serializable() |
Default JSON/YAML output for CLI |
.with_custom_display_fn(|_, res| ...) |
Custom CLI output formatting |
.with_about("about.description") |
Add help text (i18n key) - after CLI modifiers |
.with_call_remote::<CliContext>() |
Enable CLI to call remotely - must be last |
Correct ordering example:
from_fn_async(my_handler)
.with_metadata("sync_db", Value::Bool(true)) // metadata early
.no_display() // CLI modifier
.with_about("about.my-handler") // after CLI modifiers
.with_call_remote::<CliContext>() // always last
Metadata by Middleware
Metadata tags are processed by different middleware. Group them logically:
Auth Middleware (middleware/auth/mod.rs)
| Metadata | Default | Description |
|---|---|---|
authenticated |
true |
Whether endpoint requires authentication. Set to false for public endpoints. |
Session Auth Middleware (middleware/auth/session.rs)
| Metadata | Default | Description |
|---|---|---|
login |
false |
Special handling for login endpoints (rate limiting, cookie setting) |
get_session |
false |
Inject session ID into params as __Auth_session |
Signature Auth Middleware (middleware/auth/signature.rs)
| Metadata | Default | Description |
|---|---|---|
get_signer |
false |
Inject signer public key into params as __Auth_signer |
Registry Auth (extends Signature Auth)
| Metadata | Default | Description |
|---|---|---|
admin |
false |
Require admin privileges (signer must be in admin list) |
get_device_info |
false |
Inject device info header for hardware filtering |
Database Middleware (middleware/db.rs)
| Metadata | Default | Description |
|---|---|---|
sync_db |
false |
Sync database after mutation, add X-Patch-Sequence header |
Context Types
Different contexts for different execution environments:
RpcContext- Web/RPC requests with full service accessCliContext- CLI operations, calls remote RPCInitContext- During system initializationDiagnosticContext- Diagnostic/recovery modeRegistryContext- Registry daemon contextEffectContext- Service effects context (container-to-host calls)
Parameter Structs
Parameters use derive macros for JSON-RPC, CLI parsing, and TypeScript generation:
#[derive(Deserialize, Serialize, Parser, TS)]
#[serde(rename_all = "camelCase")] // JSON-RPC uses camelCase
#[command(rename_all = "kebab-case")] // CLI uses kebab-case
#[ts(export)] // Generate TypeScript types
pub struct MyParams {
pub package_id: PackageId,
}
Middleware Injection
Auth middleware can inject values into params using special field names:
#[derive(Deserialize, Serialize, Parser, TS)]
pub struct MyParams {
#[ts(skip)]
#[serde(rename = "__Auth_session")] // Injected by session auth
session: InternedString,
#[ts(skip)]
#[serde(rename = "__Auth_signer")] // Injected by signature auth
signer: AnyVerifyingKey,
#[ts(skip)]
#[serde(rename = "__Auth_userAgent")] // Injected during login
user_agent: Option<String>,
}
Common Patterns
Adding a New RPC Endpoint
- Define params struct with
Deserialize, Serialize, Parser, TS - Choose handler type based on sync/async and thread-safety
- Write handler function taking
(Context, Params) -> Result<Response, Error> - Add to parent handler with appropriate extensions (display modifiers before
with_about) - TypeScript types auto-generated via
make ts-bindings
Public (Unauthenticated) Endpoint
from_fn_async(get_info)
.with_metadata("authenticated", Value::Bool(false))
.with_display_serializable()
.with_about("about.get-info")
.with_call_remote::<CliContext>() // last
Mutating Endpoint with DB Sync
from_fn_async(update_config)
.with_metadata("sync_db", Value::Bool(true))
.no_display()
.with_about("about.update-config")
.with_call_remote::<CliContext>() // last
Session-Aware Endpoint
from_fn_async(logout)
.with_metadata("get_session", Value::Bool(true))
.no_display()
.with_about("about.logout")
.with_call_remote::<CliContext>() // last
File Locations
- Handler definitions: Throughout
core/src/modules - Main API tree:
core/src/lib.rs(main_api(),server(),package()) - Auth middleware:
core/src/middleware/auth/ - DB middleware:
core/src/middleware/db.rs - Context types:
core/src/context/