@geostack/arc
v0.1.3
Published
Arc SDK and CLI for guarding high-risk AI actions.
Maintainers
Readme
@geostack/arc
Arc makes high-risk AI actions safe with permissions, approvals, signed execution, and audit.
Arc does not run your app logic. Your app owns the action implementation. Arc verifies authority, asks for approval when policy requires it, signs the execution request, delivers it to your app, and records the audit trail.
Define Actions
import { arc } from "@geostack/arc";
export const actions = arc.defineActions({
issue_refund: {
name: "Issue refund",
risk: "high",
defaultDecision: "ask",
input: {
type: "object",
required: ["amount", "customerId"],
properties: {
amount: { type: "number" },
customerId: { type: "string" }
}
}
}
});risk must be low, medium, high, or critical. defaultDecision must be allow, ask, or block.
Handle Signed Execution
import express from "express";
import { arc } from "@geostack/arc";
import { actions } from "./actions.js";
import { invocationStore, nonceStore } from "./arc-stores.js";
const app = express();
app.use(express.json());
app.post("/arc/execute", arc.handleAction(actions, {
issue_refund: async ({ input, appUserId, invocationId }) => {
// Enforce app-side idempotency by invocationId before side effects.
// Store the in-progress/completed record in durable storage shared by all app instances.
if (await refundAlreadyHandled(invocationId)) {
return getStoredRefundResult(invocationId);
}
return issueRefund(appUserId, input);
}
}, {
apiUrl: "https://app.geostack.xyz",
nonceStore,
invocationStore
}));handleAction() verifies Arc's ES256 JWS, body hash, timestamp freshness, nonce replay hook, invocation idempotency, and signature claims before dispatching your handler.
For lower-level integrations, call verifyArcRequest(req, { jwks, nonceStore }) directly. The nonce store must return false for replayed nonces. Verification fails with nonce_store_required when no store is supplied.
Invocation idempotency is fail-closed the same way: handleAction() / verifyArcExecution() / validateArcInvocation() require an invocationStore that returns false for already-processed invocation ids, and fail with invocation_store_required when no store is supplied. This matters because Arc's automatic delivery retries re-sign with a fresh nonce but keep the same invocation_id — a nonce store alone cannot stop a retry (or a replayed envelope hitting another instance) from double-executing a refund.
For production, use durable nonce replay and invocation idempotency storage shared by every app instance. The SDK's memory stores are local development helpers only and must be enabled explicitly with unsafeAllowInMemoryNonceStore / unsafeAllowInMemoryInvocationStore. A production app that verifies without durable nonce and invocation storage is not safely integrated with Arc.
Your app should also treat invocation_id as an idempotency key before performing side effects, because Arc may retry delivery after network failures or 5xx responses. A robust handler writes an idempotency row before the side effect and stores the final result when the side effect completes. If the process crashes after the app side effect but before Arc records success, Arc will mark the invocation outcome unknown rather than blindly retrying; your app's idempotency record is the source of truth for reconciliation.
Minimal nonce store contract:
export const nonceStore = {
async useNonce(nonce: string, expiresAt: Date) {
// Atomically insert nonce with TTL. Return false when it already exists.
const result = await redis.set(`arc:nonce:${nonce}`, "1", {
NX: true,
PXAT: expiresAt.getTime()
});
return result === "OK";
}
};Minimal invocation idempotency store contract:
export const invocationStore = {
async useInvocation(invocationId: string) {
// Atomically record the invocation id. Return false when it was already processed.
const result = await redis.set(`arc:invocation:${invocationId}`, "1", { NX: true });
return result === "OK";
}
};Input Schema Validation
Pass inputSchema to verifyArcExecution / validateArcInvocation to validate the
signed input against your action's JSON Schema before your handler runs.
The SDK validates the input with full JSON Schema validation via
ajv (and ajv-formats), exactly as the Arc server does — so the
SDK rejects precisely what the server rejects. Every JSON Schema feature is enforced:
nested objects, array items, nested required, deeply nested property type, enum,
format, pattern, minimum/maximum, additionalProperties, allOf/anyOf/oneOf,
$ref, and so on. There is no lightweight fast-path that could under-validate and
silently accept input the server would reject.
ajv and ajv-formats ship as dependencies of this package, so validation works out of the
box — there is nothing extra to install. The validator is still lazily imported only when
you actually pass an inputSchema, so handlers that never validate input never load it; an
empty schema ({}) imposes no constraints and is accepted without loading the validator.
Validation failures throw ArcRequestVerificationError with code invalid_input and never
echo the offending input value into the error message; a stored schema that is itself
uncompilable throws invalid_action_schema (also fail-closed). In the unlikely event the
validator cannot be loaded at all, the SDK fails closed with code
schema_validator_unavailable rather than accepting unvalidated input.
await verifyArcExecution(req, {
jwks,
nonceStore,
invocationStore,
inputSchema: actions.issue_refund.input
});
strictSchemais deprecated and now a no-op: full Ajv validation is always applied to every non-empty schema, so there is nothing left to opt into.
Sync Actions
arc config set --api-url https://app.geostack.xyz
arc app create "Refund App" --execute-url https://your-service.example.com/arc/execute
arc actions sync ./src/actions.tsThe CLI stores local configuration in ~/.arc/config.json. It never stores app API keys. Dev agent tokens are only stored when you create them with arc agent dev-token, and the CLI labels them as local development credentials.
Invoke Locally
arc agent dev-token
arc invoke issue_refund --app refund-app --input '{"amount":480,"customerId":"cus_123"}'
arc approvals list
arc approvals approve <approval_id>
arc audit tailAllowed invocations are queued for signed delivery. Asked invocations create approvals. Blocked invocations never execute.
Drive the CLI from an AI Agent
Every command is non-interactive (no stdin, no TTY) and supports a global --json flag: stdout carries exactly one JSON object/array, errors go to stderr as {"error":{"code","message"[,"status"]}}, and exit codes are 0 success, 1 usage/validation or 4xx, 2 transport/5xx.
export ARC_API_URL="https://app.geostack.xyz/api" # hosted API base (or your self-hosted URL)
export ARC_AGENT_TOKEN="arc_agent_..." # console -> Agents
arc agent whoami --json
arc agent apps --json
arc agent invoke --app <id-or-slug> --action <key> --input '{"amount":50}' --jsonarc setup-prompt (alias arc agent-setup) prints a copy-paste prompt that walks Claude Code/Codex through the full setup: install, env vars, guarding your highest-risk action, and verifying one invocation end to end. See AGENTS.md for the machine-oriented guide, and https://geostack.xyz/docs/ai-setup for the docs page.
HTTP Clients
import { ArcDeveloperClient, ArcAgentClient } from "@geostack/arc";
const developer = new ArcDeveloperClient({ baseUrl: "https://app.geostack.xyz" });
await developer.devLogin({ email: "[email protected]" });
const agent = new ArcAgentClient({
baseUrl: "https://app.geostack.xyz",
agentToken: "arc_agent_..."
});The clients return Arc API JSON directly and throw ArcHttpError with status, code, and message for non-2xx responses.
Production Notes
This SDK is V1 developer tooling, not a claim that the whole Arc stack is ready for public production traffic. Before public launch, run Arc with a production signing key, persistent app-side nonce storage, app-side idempotency by invocation_id, observability around failed/unknown executions, and network egress controls. The local Docker stack and its runtime-generated development signing key are for local development only — production config rejects the dev signing-key id and refuses to boot without a real key.
