Chio/Docs

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 aloneLambda + chio
IAM role-based authorization on the functionCapability-scoped, time-bounded, per-tool authorization per invocation
CloudWatch logs with structured fieldsMerkle-committed, signed receipt log independent of CloudWatch
No tool-level policy surfaceGuard pipeline evaluates each invocation with evidence
Binary allow or deny semantics at the gatewayBudget-aware, scope-narrowing, conditional access
No cross-invocation audit trailReceipt 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.

rendering…
Function handler and chio extension run as co-processes in the Lambda execution environment, communicating over internal HTTP.

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

The extension can buffer receipts in memory during an invocation and flush on SHUTDOWN because Lambda guarantees the extension a draining window before the environment goes away. If durability per-invocation matters (regulated workloads, payment flows), configure the DynamoDB sink for synchronous writes instead.

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:

yaml
# 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

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

typescript
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:

rust
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:

rendering…
A dedicated chio Authorizer Lambda evaluates the capability token before the target function is ever invoked. Denied calls incur no handler cold-start cost.
python
# 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:

yaml
# 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.Arn

SAM 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.

yaml
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:

typescript
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.

text
CloudFront -> Lambda@Edge (chio scope + token check) -> Origin (full chio guard pipeline)

Do not enable WASM guards at the edge

The Lambda@Edge runtime environment cannot host the WASM guard runtime at reasonable cost. Configure the edge profile to rely on pre-evaluated static policy only, and defer dynamic guard evaluation to the origin.

Package Layout

text
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.ts

Open 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 WorkflowGrant model?

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
AWS Lambda · Chio Docs