AWS Lambda
Serverless is a natural home for agent tool servers: each invocation is already a well-bounded unit of work, scaling is automatic, and there is no long-lived container to manage. The one thing you lose when you move to Lambda is the persistent sidecar that the standard chio deployment pattern relies on. The fix is the AWS Lambda Extension API. chio ships as an external extension that runs in the same execution environment as the function, persists across warm invocations, and exposes the same localhost endpoint your handler already knows how to call. It works with Python, Node.js, and Rust Lambda runtimes, adds roughly 50ms on a cold start, and under 1ms on a warm one.
Why Lambda Through chio
Lambda's native authorization model is IAM: coarse, role-based, and attached to the function rather than the invocation. That is useful for permissions the function itself needs to act in AWS, but it does not answer the agent governance question: does this specific caller, with this specific capability token, have scope to invoke this specific tool right now, within their remaining budget, and passing all guards? chio layers that question on top of IAM without replacing it.
| Lambda alone | Lambda + chio |
|---|---|
| IAM role-based authorization on the function | Capability-scoped, time-bounded, per-tool authorization per invocation |
| CloudWatch logs with structured fields | Merkle-committed, signed receipt log independent of CloudWatch |
| No tool-level policy surface | Guard pipeline evaluates each invocation with evidence |
| Binary allow or deny semantics at the gateway | Budget-aware, scope-narrowing, conditional access |
| No cross-invocation audit trail | Receipt chain links related invocations in a workflow |
The Extension Model
Lambda Extensions run as co-processes in the same execution environment as the function. They start during the environment's INIT phase, receive lifecycle events during every INVOKE, and get one last hook on SHUTDOWN to drain buffered state. An extension does not replace the handler; it sits beside it. For chio, that is exactly the pattern we want: a sidecar that preloads policy and signing keys on cold start, answers evaluate calls on a local UNIX socket or localhost port during invocations, and flushes buffered receipts before the environment is torn down.
Extension Lifecycle
The extension participates in three lifecycle phases, and each one does a specific job.
INIT
When a cold start fires, the extension loads policy from its configured source (bundled in the layer, S3, SSM Parameter Store), initializes the chio kernel, loads the receipt signing keypair, and opens its local listener on port 9090. Every subsequent invocation on this execution environment reuses the warm state.
INVOKE
For every function invocation, the extension receives the event from the Lambda runtime before the handler is called. In transparent mode the extension pre-evaluates the invocation against policy so that a denied call never reaches your handler code. In explicit mode, the handler calls chio.evaluate() itself over the local endpoint and gets fine-grained control of the decision.
SHUTDOWN
When Lambda reclaims the execution environment, the extension gets one last window to drain any buffered receipts to durable storage: DynamoDB, S3, SQS, or the chio control plane. Batched flush on SHUTDOWN avoids per-invocation durability latency while still guaranteeing that no receipt is lost when the environment is torn down.
In-memory buffering is acceptable because of SHUTDOWN
Cold-Start Optimization
Cold start is the dominant latency concern with any Lambda Extension. The chio kernel is a small Rust binary (~15ms to initialize), compiled for arm64 and x86_64. Pre-publishing the extension as a Layer avoids the per-cold-start download. Policy source choice dominates the rest of the budget: bundling policy in the layer keeps the extra delta around 25-50ms; loading from S3 on first cold start is 65-125ms including the AWS SDK round-trip. Guard WASM init adds 10-30ms per guard. Warm-path evaluate calls are under 1ms loopback.
Transparent Mode
Transparent mode is the zero-code-change path. The extension intercepts every invocation automatically and denies before the handler ever runs. Configuration is a single YAML file bundled in the layer or loaded from S3:
# chio-policy.yaml
mode: transparent
evaluation:
# How to project a Lambda event into a chio evaluation context
identity_field: "requestContext.authorizer.principalId"
tool_field: "resource" # e.g. API Gateway resource path
scope_field: "httpMethod" # GET -> read, POST -> write
arguments_field: "body"
policy:
default: deny
rules:
- tool: "/api/search"
scopes: ["tools:search:invoke"]
guards: ["rate-limit", "pii-filter"]
- tool: "/api/write"
scopes: ["tools:write:invoke"]
guards: ["approval-required"]Explicit Mode
For fine-grained control inside the handler, call chio directly. The SDK talks to the extension over the local UNIX socket or localhost HTTP, so latency is loopback-bound and no network hop is involved.
Python
from chio_lambda import ChioLambda
chio = ChioLambda() # auto-connects to the extension on :9090
async def handler(event, context):
verdict = await chio.evaluate(
tool="database-query",
scope="db:read",
arguments=event["body"],
identity=event["requestContext"]["authorizer"],
)
if verdict.denied:
return {
"statusCode": 403,
"body": json.dumps({
"error": "capability_denied",
"reason": verdict.reason,
"receipt_id": verdict.receipt_id,
}),
}
result = execute_query(event["body"])
receipt = await chio.record(
verdict=verdict,
result_hash=sha256(json.dumps(result)),
)
return {
"statusCode": 200,
"body": json.dumps(result),
"headers": {"X-Chio-Receipt": receipt.receipt_id},
}TypeScript / Node.js
import { ChioLambda } from "@chio-protocol/lambda";
const chio = new ChioLambda();
export const handler = async (event: APIGatewayProxyEvent) => {
const verdict = await chio.evaluate({
tool: "database-query",
scope: "db:read",
arguments: JSON.parse(event.body ?? "{}"),
identity: event.requestContext.authorizer,
});
if (verdict.denied) {
return {
statusCode: 403,
body: JSON.stringify({ error: verdict.reason }),
};
}
const result = await executeQuery(event.body);
const receipt = await chio.record({
verdict,
resultHash: sha256(JSON.stringify(result)),
});
return {
statusCode: 200,
body: JSON.stringify(result),
headers: { "X-Chio-Receipt": receipt.receiptId },
};
};Rust
Rust Lambda runtimes ship with lambda_runtime. The chio SDK for Rust exposes a wrapping adapter that evaluates before the handler closure runs:
use chio_lambda::{ChioLambda, Verdict};
use lambda_runtime::{service_fn, Error, LambdaEvent};
async fn handler(event: LambdaEvent<Value>) -> Result<Value, Error> {
let chio = ChioLambda::connect_default().await?;
let verdict = chio.evaluate()
.tool("database-query")
.scope("db:read")
.arguments(&event.payload)
.evaluate()
.await?;
match verdict {
Verdict::Allow(v) => {
let result = execute_query(&event.payload).await?;
chio.record(&v, sha256_hex(&result)).await?;
Ok(result)
}
Verdict::Deny(d) => Ok(json!({
"statusCode": 403,
"body": { "error": d.reason, "receipt_id": d.receipt_id },
})),
}
}
#[tokio::main]
async fn main() -> Result<(), Error> {
lambda_runtime::run(service_fn(handler)).await
}API Gateway Authorizer Mode
For HTTP-fronted tool servers, chio can run as a dedicated Lambda Authorizer attached to API Gateway. The authorizer evaluates the capability token before the target function is ever invoked, which means denied calls incur no handler cold-start cost:
# chio_authorizer.py - deployed as its own Lambda
from chio_lambda import ChioAuthorizer
authorizer = ChioAuthorizer(
policy_source="s3://my-bucket/chio-policy.yaml",
)
def handler(event, context):
# Returns an IAM policy document (allow or deny)
return authorizer.evaluate(event)Receipt Persistence
Lambda execution environments are ephemeral, so receipts need a durable home before the environment is recycled. The extension supports four sinks with different tradeoffs: DynamoDB (~5ms per-write, good for synchronous per-invocation writes), S3 (~50ms, best batched on SHUTDOWN as one object per environment lifecycle), SQS (~10ms, useful for async aggregation into a Merkle tree by a downstream processor Lambda), and the chio control plane directly (~20-50ms, managed). A common production layout writes DynamoDB per-invocation and flushes S3 on SHUTDOWN.
IAM Integration
The extension shares the function's task role. Policy bucket reads, receipt table writes, and control-plane calls all run with the same IAM identity the function has. There is no separate credential pathway and no extra secret to manage. Grant the minimum permissions required by the sinks you configure:
# Permissions the chio extension needs (attached to function role)
- Effect: Allow
Action:
- s3:GetObject # policy source
Resource: arn:aws:s3:::my-policy-bucket/chio-policy.yaml
- Effect: Allow
Action:
- dynamodb:PutItem # per-invocation receipt sink
- dynamodb:BatchWriteItem
Resource: !GetAtt ReceiptTable.Arn
- Effect: Allow
Action:
- sqs:SendMessage # optional async aggregation
Resource: !GetAtt ReceiptQueue.ArnSAM Template
Reference the chio extension layer ARN and attach it to any function that should be governed. The ARN follows a predictable pattern by region and architecture, for example arn:aws:lambda:us-east-1:000000000000:layer:chio-kernel-extension-arm64:42.
Resources:
ChioExtensionLayer:
Type: AWS::Serverless::LayerVersion
Properties:
LayerName: chio-kernel-extension
ContentUri: layers/chio-extension/
CompatibleRuntimes:
- python3.13
- nodejs22.x
- provided.al2023 # Rust runtimes
CompatibleArchitectures:
- arm64
- x86_64
ToolFunction:
Type: AWS::Serverless::Function
Properties:
Handler: handler.handler
Runtime: python3.13
Architectures: [arm64]
Layers:
- !Ref ChioExtensionLayer
Environment:
Variables:
CHIO_POLICY_SOURCE: !Sub "s3://${PolicyBucket}/chio-policy.yaml"
CHIO_RECEIPT_TABLE: !Ref ReceiptTable
Policies:
- S3ReadPolicy: { BucketName: !Ref PolicyBucket }
- DynamoDBCrudPolicy: { TableName: !Ref ReceiptTable }CDK
The @chio-protocol/cdk construct hides the layer wiring behind a single type:
import { ChioExtensionLayer } from "@chio-protocol/cdk";
const chioLayer = new ChioExtensionLayer(this, "ChioExtension", {
policySource: PolicySource.s3(policyBucket, "chio-policy.yaml"),
receiptSink: ReceiptSink.dynamodb(receiptTable),
});
const toolFn = new lambda.Function(this, "ToolFunction", {
runtime: lambda.Runtime.PYTHON_3_13,
handler: "handler.handler",
layers: [chioLayer],
});Lambda@Edge and CloudFront Functions
Lambda@Edge and CloudFront Functions have tighter constraints: 5s viewer-request timeout, no VPC access, limited package size. chio runs here in a restricted profile. The edge evaluates scope checks and token validity only, and receipts buffer to CloudWatch for asynchronous flush. Full guard evaluation still happens at the origin, where a standard chio sidecar runs unconstrained.
CloudFront -> Lambda@Edge (chio scope + token check) -> Origin (full chio guard pipeline)Do not enable WASM guards at the edge
Package Layout
sdks/lambda/
chio-lambda-extension/ # Rust binary, compiles to Lambda Extension
src/main.rs # Extension lifecycle (INIT/INVOKE/SHUTDOWN)
src/evaluator.rs # HTTP server on :9090
src/receipt_sink.rs # DynamoDB/S3/SQS flush
chio-lambda-python/ # deps: chio-py
src/chio_lambda/client.py # ChioLambda, ChioAuthorizer
src/chio_lambda/transparent.py # Event-to-evaluation projection
chio-lambda-node/ # deps: @chio-protocol/node-http
src/client.ts # ChioLambda class
chio-lambda-cdk/ # CDK constructs
src/extension-layer.ts
src/constructs.tsOpen Questions
- Provisioned concurrency. With provisioned concurrency, the extension is always warm. Should it prefetch policy updates on a background timer, or continue to pull on a miss-driven cache basis only?
- SnapStart (Java). Lambda SnapStart checkpoints the JVM after INIT. The extension state must be checkpoint-safe: no open sockets, no time-dependent state at checkpoint time.
- Function URLs vs API Gateway. Function URLs bypass API Gateway, so the Authorizer pattern does not apply. Should the extension switch to transparent mode automatically when invoked via a Function URL?
- Multi-function workflows. For Step Functions orchestrating multiple Lambdas, should each function carry a grant token the orchestrator acquired, similar to the Temporal
WorkflowGrantmodel?
Next Steps
- Envoy ext_authz · front any HTTP service with chio via the Envoy filter
- Kafka · governance for event-driven Lambda fan-ins via the streaming adapter
- Receipt Dashboard · visualize receipts flushed from DynamoDB or S3