Skip to content
Merged
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
8 changes: 5 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,16 +34,18 @@ Keep the root [README](README.md) concise and link to the detailed pages instead
mise install
```

2. Keep the change focused. Add or update behavior tests when a public contract changes. Update the user documentation when behavior visible through the root, `authz`, or `mcpserver` packages changes.
3. Validate compile-checked examples and documentation directly when either changes:
2. Keep the change focused.
3. When a public contract changes, add or update behavior tests.
4. When behavior visible through the root, `authz`, or `mcpserver` packages changes, update the user documentation.
5. When compile-checked examples or documentation change, validate them directly:

```sh
go test ./... -count=1
moon run docs:build
```

`go test` compiles and runs the Go examples with the package tests. `docs:build` runs the strict MkDocs build with the locked documentation environment.
4. Before requesting review, run the complete repository check:
6. Before requesting review, run the complete repository check:

```sh
moon run root:check
Expand Down
11 changes: 7 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ The repository also contains shorter, compile-checked examples:
- [`example_test.go`](example_test.go) — typed capability registration, explicit `authz.AllowAll()`, default limits, and direct execution
- [`mcpserver/example_test.go`](mcpserver/example_test.go) — trusted typed-context resolution and the official in-memory MCP transport

`authz.AllowAll()` is deliberate in the simple examples and permits every validated invocation. Production hosts normally supply an authorization policy.
`authz.AllowAll()` is deliberate in the simple examples and permits every native call whose arguments bind successfully. Use `authz.AllowAll()` only when every resolved subject may call every enabled capability; otherwise supply an `authz.Authorizer`.

The final host binary must call `codemode.ServeWorkerAndExit()` as the first
statement of `main`, before flag parsing or any other setup. Test binaries that
Expand All @@ -40,10 +40,13 @@ call `Builder.Build` must do the same from `TestMain` before `m.Run`.

## Security boundary

Each `execute` request runs Starlark in a fresh process created by re-executing
Each `execute` request runs Starlark in a fresh worker process created by re-executing
the host binary. The submitted program must define a zero-argument `main()`,
and only its final converted value crosses the execution boundary. Prints,
globals, and intermediate values do not.
and only its final converted value is exposed to the caller. Printed text,
globals, and interpreter-local intermediate values are not returned. During
execution, each native-call argument map and each validated native result
crosses the private worker protocol, and each crossing value is independently
subject to `MaxValueDepth` and `MaxValueBytes`.

Capabilities are bound and canonicalized in the parent before authorization,
and authorization completes before handler dispatch. Trusted subjects come
Expand Down
14 changes: 7 additions & 7 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ Include the following details when available:
CodeMode executes each submitted program in a fresh worker process created by
re-executing the host binary. Module loading is disabled, native capabilities
are limited to the immutable set registered and enabled by the host, and native
calls are rejected during top-level program loading. A program must define a
calls are rejected during top-level source loading. A program must define a
zero-argument `main()` function, and only its converted return value is
exposed.

Configured limits bound source bytes, interpreter steps, elapsed time,
attempted native calls, concurrent worker processes, and the depth and encoded
attempted native calls, concurrent workers, and the depth and encoded
size of every value that crosses the worker boundary. Search query bytes and
search result counts are bounded separately in the parent. An elapsed deadline
or request cancellation kills and reaps the worker process. Each validated
native call is rebound in the parent and passes through the host-supplied
`authz.Authorizer` before its handler runs. The MCP adapter resolves the subject
from trusted Go context and ignores identity or credential claims in tool
arguments and request `_meta`.
or request cancellation kills and reaps the worker. Each native call whose
arguments bind successfully is rebound in the parent and passes through the
host-supplied `authz.Authorizer` before its handler runs. The MCP adapter
resolves the subject from trusted Go context and ignores identity or
credential claims in tool arguments and request `_meta`.

The worker boundary prevents Starlark from sharing interpreter state with the
host and permits hard preemption of Starlark execution. It is not a hard tenant
Expand Down
52 changes: 26 additions & 26 deletions docs/docs/explanation/security-model.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
---
title: Security model
title: Understanding CodeMode's security model
description: Trust boundaries, authorization order, cancellation, and worker-process containment in CodeMode.
---

# Security model
# Understanding CodeMode's security model

CodeMode separates client-controlled Starlark from host-controlled identity, policy, and Go handlers. The separation depends on the host establishing a trusted request context, installing the worker entry point, and ensuring authorizers and handlers follow their contracts. CodeMode does not authenticate clients or provide operating-system tenant quotas.
CodeMode separates client-controlled Starlark from host-controlled identity, policy, and Go handlers. The separation depends on the host establishing a trusted request context, installing the worker process entry point, and ensuring authorizers and handlers follow their contracts. CodeMode does not authenticate clients or provide operating-system tenant quotas.

## The host establishes identity

Expand All @@ -24,13 +24,13 @@ Credentials stay in the host's authentication layer. They are not fields on `aut

## Validation precedes authority

A native capability call crosses three boundaries in a fixed order:
A native call crosses three boundaries in a fixed order:

1. **Exact binding and canonicalization.** Duplicate keyword syntax is rejected by the Starlark parser as `ErrInvalidProgram` before this step. Positional, missing, unknown, incorrectly typed, and out-of-range arguments reach binding and map to `ErrInvalidArguments`. Successful binding creates the exact registered Go input and a fresh JSON-shaped argument map.
2. **Authorization.** CodeMode passes the trusted subject, stable capability ID, dotted capability name, and canonical arguments to `authz.Authorizer`.
3. **Handler dispatch.** CodeMode calls the typed handler only if authorization returns `nil`.

This order prevents policy from interpreting malformed Starlark values and prevents a handler from running before policy has evaluated the exact input it will receive. The canonical map is separate from the typed handler input, so policy cannot rewrite the handler's arguments by mutating the map.
This order prevents policy from interpreting malformed Starlark values and prevents a handler from running before policy has evaluated the exact input the handler will receive. The canonical map is separate from the typed handler input, so policy cannot rewrite the handler's arguments by mutating the map.

An authorizer reports a recognized denial with an error that wraps `authz.ErrDenied`. CodeMode classifies that outcome as `permission denied` and does not dispatch the handler. Any other authorizer error, and an authorizer panic recovered at the boundary, becomes `authorization policy failure`. Policy diagnostic text does not cross the MCP boundary.

Expand All @@ -40,7 +40,7 @@ Authorization is evaluated for every attempted native call whose arguments bind

`authz.AllowAll()` returns an authorizer because the server never treats a missing authorizer as permission. The simple example uses it deliberately so that a minimal server is complete and the absence of policy logic is visible.

`AllowAll` approves every valid native call for every resolved subject. It is not authentication and does not inspect capability identity or arguments. Production hosts normally supply an authorizer that evaluates the resolved subject, the stable capability ID, and the canonical arguments. A production host should use `AllowAll` only when unrestricted access to every enabled capability is the intended policy.
`AllowAll` approves every native call for every resolved subject. It is not authentication and does not inspect capability identity or arguments. Use `authz.AllowAll()` only when every resolved subject may call every enabled capability; otherwise supply an `authz.Authorizer` that evaluates the resolved subject, the stable capability ID, and the canonical arguments.

## Rego policy runs in process

Expand All @@ -56,7 +56,7 @@ CodeMode installs no schema set and no schema resolver. Metadata `schema["https:

The configured decision is one direct, ground `data` reference. Construction validates that reference syntax and prepares the policy; it cannot prove that the decision is defined and Boolean for every future input. A ground decision is either undefined or yields one value. That value must be Boolean. Boolean `true` allows a call. Boolean `false` is a recognized denial. Undefined and non-Boolean decisions are policy failures, as are evaluation and builtin errors. A total decision with `default allow := false` turns unmatched input into an intentional denial while still failing closed when the policy contract is broken.

These controls restrict policy inputs and evaluator capabilities, not resource consumption or process authority. OPA, authorizers, and handlers run inside the host process; moving Starlark to a worker does not isolate Rego policy. A host that does not trust its policy authors needs an external process or container boundary for policy evaluation.
These controls restrict policy inputs and evaluator capabilities, not resource consumption or process authority. OPA, authorizers, and handlers run inside the host process; moving Starlark to a worker process does not isolate Rego policy. A host that does not trust its policy authors needs an external process or container boundary for policy evaluation.

See [Use Rego for authorization](../how-to/use-rego-authorization.md) for configuration and the [`authz/rego` API reference](../reference/public-api.md#authzrego) for the exact input and result contracts.

Expand All @@ -82,20 +82,20 @@ An allowed or denied call proves only the result for that subject, capability, a

## Execution state does not cross calls

Every `Server.Execute` call starts a fresh process by re-executing the host
binary. The child receives only the immutable enabled-capability manifest,
positive execution limits, and one submitted program through CodeMode's private
protocol. It constructs a fresh Starlark interpreter. Module loading is
disabled, and the only predeclared application functions are the enabled
capability namespace. Native calls are rejected while top-level source is
loading and are accepted only while the required zero-argument `main()`
function runs.

After `main` returns, the worker converts only its final value to
type-preserving wire data under the depth and encoded-size limits. Printed text
is discarded. Globals, source-loading values, intermediate expressions, and
unrelated native results do not cross the boundary. No Starlark globals or
mutable interpreter state carry into the next execute call.
Every `Server.Execute` call starts a fresh worker process by re-executing the
host binary. The worker process is a child of the host process. The worker
receives only the immutable enabled-capability manifest, positive execution
limits, and one submitted program through CodeMode's private protocol. It
constructs a fresh Starlark interpreter. Module loading is disabled, and the
only predeclared application functions are the enabled capability namespace.
Native calls are rejected during top-level source loading and are accepted
only while the required zero-argument `main()` function runs.

After `main` returns, only the final converted value is exposed to the caller.
Printed text, globals, and interpreter-local intermediate values are not
returned. Unrelated native results are not exposed in that final caller
result. No Starlark globals or mutable interpreter state carry into the next
execute call.

Capability handlers do not run in the worker. A native call crosses the private
protocol, is rebound to the exact registered input type in the parent, is
Expand All @@ -111,10 +111,10 @@ gave that process.

CodeMode derives one execution context from `MaxExecutionTime` and the request
context. The elapsed budget starts before waiting for a worker slot and covers
process startup, protocol exchange, Starlark execution, parent authorization
and dispatch, and worker cleanup. The interpreter separately enforces source,
bytecode-step, attempted-native-call, crossing-value depth, and crossing-value
size limits.
spawn, protocol exchange, Starlark execution, and parent dispatch. Killing and
reaping the worker can add operating-system overhead beyond the budget. The
interpreter separately enforces source, bytecode-step, attempted-native-call,
crossing-value depth, and crossing-value size limits.

When the execution context ends, the parent closes the worker pipes, kills the
worker if necessary, and reaps it exactly once. This hard-preempts Starlark,
Expand Down Expand Up @@ -153,7 +153,7 @@ operating-system user and re-executes the host binary. CodeMode supplies an
environment containing only its private worker marker and passes no extra file
descriptors, but it provides no operating-system CPU or memory quota. Package
initialization and any setup placed before the worker entry point still run in
the child with that user's filesystem and network authority.
the worker with that user's filesystem and network authority.

The restricted Starlark environment exposes no file, network, environment, or
process built-ins. Native access is limited to the enabled capability manifest
Expand Down
32 changes: 21 additions & 11 deletions docs/docs/how-to/disable-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,33 @@ Use the stable ID, `records.entry.lookup`, not the model-facing dotted name, `re

Continue to call `codemode.Register` for `records.entry.lookup` before `builder.Build()`. CodeMode validates the complete registration set before applying the filter, and `Build` rejects a disabled ID that does not identify a registered capability. Static filtering cannot hide invalid or conflicting registrations.

The explicit `authz.AllowAll()` remains deliberate only for the simple tutorial. Production hosts normally supply an authorization policy. Static filtering and per-call authorization solve different problems: filtering fixes the deployment-wide surface, while the authorizer decides whether a trusted subject may make an enabled native call.
The explicit `authz.AllowAll()` remains deliberate only for the simple tutorial. Use `authz.AllowAll()` only when every resolved subject may call every enabled capability; otherwise supply an `authz.Authorizer`. Static filtering and per-call authorization solve different problems: filtering fixes the deployment-wide surface, while the authorizer decides whether a trusted subject may make an enabled native call.

## Replace the built server

Call `Build` after registering all capabilities, then pass the resulting server to `mcpserver.New` as usual. An existing immutable server does not change when the builder options change. Replace the server instance and its host-owned transport lifecycle to deploy a different enabled set.
After registering all capabilities, call `Build`. Pass the returned server to `mcpserver.New`. To deploy a different enabled set, replace the existing server instance and its host-owned transport lifecycle.

## Check the resulting surface
## Verify the filtered surface

With the tutorial's unchanged call order, `search_api` prints `[]`, then `describe_api` returns the expected tool error and the helper exits. The later `execute` call is not reached in that run. The list below describes the complete contract of the built server, not the output of one sequential client run.
The tutorial's `callTool` helper returns on the first tool error, so `describe_api` would stop the program before `execute`. Replace the `result.IsError` branch in `callTool` with:

After `records.entry.lookup` is disabled:
```go
if result.IsError {
text, ok := result.Content[0].(*mcp.TextContent)
if !ok {
return fmt.Errorf("%s returned a tool error: %v", params.Name, result.Content)
}
fmt.Printf("%s: %s\n", params.Name, text.Text)
return nil
}
```

- `search_api` does not return `records.lookup`, even when the query matches its name or summary.
- `describe_api` for `records.lookup` returns the same not-found classification used for any unavailable name.
- The generated Starlark namespace has no `records.lookup` callable. If no other enabled capability starts with `records.`, the `records` namespace is absent; otherwise only `lookup` is absent.
- An `execute` program cannot call the disabled capability. The request fails before authorization or handler dispatch.
Run `go run .`. The program prints:

These effects come from one filtered catalog used by discovery and execution. The filter does not leave a hidden execution path.
```
search_api: []
describe_api: capability not found
execute: invalid program
```

See the [public API reference](../reference/public-api.md) for `Options` and build validation, and the [MCP tools reference](../reference/mcp-tools.md) for discovery and execution results.
See the [public API reference](../reference/public-api.md#static-capability-filtering) for the complete filter contract, and the [MCP tools reference](../reference/mcp-tools.md) for discovery and execution results.
Loading