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
17 changes: 10 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,14 @@ The module currently requires Go 1.26.6.

Follow [Build your first CodeMode server](docs/docs/tutorials/first-server.md)
to register `records.lookup`, run a real stdio MCP server, and add it to an
agent. The server assembly is:
agent.

`authz.AllowAll()` is deliberate in the simple examples. CodeMode never
defaults authorization to allow. `mcpserver.StaticSubject` is only for
single-user transports where process ownership is the authentication boundary;
multi-user hosts must resolve each authenticated request separately.

The server assembly is:

```go
func main() {
Expand All @@ -45,14 +52,9 @@ func main() {

The repository also contains shorter, compile-checked examples:

- [`example_test.go`](example_test.go) — typed registration with default identity and limits, plus direct execution
- [`example_test.go`](example_test.go) — typed registration with default limits and an explicit subject, plus direct execution
- [`mcpserver/example_test.go`](mcpserver/example_test.go) — a fixed single-user subject and the official in-memory MCP transport

`authz.AllowAll()` is deliberate in the simple examples. CodeMode never
defaults authorization to allow. `mcpserver.StaticSubject` is only for
single-user transports where process ownership is the authentication boundary;
multi-user hosts must resolve each authenticated request separately.

`codemode.ServeWorkerAndExit()` must remain the first statement of `main`,
before flag parsing or any other setup. Test binaries that call `Builder.Build`
must make the same call from `TestMain` before `m.Run`.
Expand All @@ -62,6 +64,7 @@ must make the same call from `TestMain` before `m.Run`.
- [Documentation home](docs/docs/index.md)
- [First-server tutorial](docs/docs/tutorials/first-server.md)
- [Disable capabilities for a deployment](docs/docs/how-to/disable-capabilities.md)
- [Use Rego for authorization](docs/docs/how-to/use-rego-authorization.md)
- [Public Go API](docs/docs/reference/public-api.md)
- [MCP tools](docs/docs/reference/mcp-tools.md)
- [Security model](docs/docs/explanation/security-model.md)
Expand Down
9 changes: 6 additions & 3 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,15 @@ 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 source loading. A program must define a
zero-argument `main()` function, and only its converted return value is
exposed.
zero-argument `main()` function. Only `main`'s final converted value is
exposed to the caller.

Configured limits bound source bytes, interpreter steps, elapsed time,
attempted native calls, concurrent workers, and the depth and encoded
size of every value that crosses the worker boundary. Search query bytes and
size of every value that crosses the worker boundary.
`MaxIntermediateValueBytes` is the cumulative encoded size of successful
parent-to-child native-result value bodies per execution, independent of
the per-value `MaxValueBytes` bound. Search query bytes and
search result counts are bounded separately in the parent. An elapsed deadline
or request cancellation kills and reaps the worker. Each native call whose
arguments bind successfully is rebound in the parent and passes through the
Expand Down
35 changes: 28 additions & 7 deletions docs/docs/how-to/disable-capabilities.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ its deployment identity.

## Add the ID to the filter

The following block retains the tutorial's `authz.AllowAll()` only to isolate
the filtering change. In an existing deployment, keep its current
`authz.Authorizer`.

Replace the builder initialization:

```go
Expand All @@ -47,15 +51,32 @@ 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. Static filtering fixes the deployment-wide surface; the authorizer
Static filtering fixes the deployment-wide surface; the authorizer
decides whether a trusted subject may make an enabled native call.

## Replace and verify the server
## Replace the server

1. Build the server:

```sh
go build -o codemode-first-server .
```

2. Reload the binary in the agent.

## Verify the filter

1. Call `search_api` with `{"query":"records.lookup"}`. The result is `[]` and no longer lists `records.lookup`.

2. Call `describe_api` with `{"name":"records.lookup"}`. The call returns the tool error `capability not found`.

3. Call `execute` with this zero-argument program as the `source` argument:

```python
def main():
return records.lookup(key="alpha", limit=2)
```

Build and replace the existing server process, then reload the server in your
agent. `search_api` no longer returns `records.lookup`, `describe_api` reports
`capability not found`, and an `execute` program that references
`records.lookup` reports `invalid program`.
The call returns the tool error `invalid program`.

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.
58 changes: 33 additions & 25 deletions docs/docs/how-to/use-rego-authorization.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
title: Use Rego for authorization
description: Replace AllowAll with a prepared in-process Rego authorization decision.
description: Replace the allow-all authorizer with a prepared in-process Rego authorization decision.
---

# Use Rego for authorization
Expand All @@ -26,9 +26,19 @@ allow if {
Use `input.capability.id` for policy identity. In this example, `records.entry.lookup` is the stable capability ID. `input.capability.name` is the dotted discovery and Starlark name, `records.lookup`; it can change independently of the policy identity.

Set `ID: "records.entry.lookup"` in the tutorial's capability registration
before writing this policy. An omitted ID defaults to `records.lookup`; explicit
policy identity prevents a later capability rename from changing the decision
input.
before writing this policy:

```go
codemode.Register(builder, codemode.Capability[lookupInput, lookupOutput]{
ID: "records.entry.lookup",
Name: "records.lookup",
Summary: "Look up one record by key.",
Handler: lookup,
})
```

An omitted ID defaults to `records.lookup`. Explicit policy identity
prevents a later capability rename from changing the decision input.

The adapter supplies only this input shape:

Expand Down Expand Up @@ -126,37 +136,35 @@ Do not use an undefined decision as denial behavior. Keep `default allow := fals

## Verify the policy

Run the adapted first-server program with the tutorial's `execute` source, which calls `records.lookup(key="alpha", limit=2)`:
Rebuild the server binary:

```sh
go run .
go build -o codemode-first-server .
```

The `execute` line is:

```
execute: {"result":{"count":2,"key":"alpha"}}
```
Reload the server in the configured agent.

Change the `execute` source to `return records.lookup(key="forbidden", limit=2)`.
### Verify an allowed call

The tutorial's `callTool` helper formats a tool error with `%v` and stops the program, so the denial text is not visible as written. To print it, replace the `result.IsError` branch in `callTool` with:
Ask the agent to run this program through `execute` (the program text is the tool's `source` argument):

```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
}
```python
def main():
return records.lookup(key="alpha", limit=2)
```

Run `go run .` again. `search_api` and `describe_api` still succeed, and the `execute` line is:
The structured result is `{"result":{"count":2,"key":"alpha"}}`.

### Verify a denied call

Ask the agent to run this program through `execute`:

```python
def main():
return records.lookup(key="forbidden", limit=2)
```
execute: permission denied
```

`execute` returns a tool error whose text is `permission denied`.
`search_api` and `describe_api` still succeed.

See the [`authz/rego` API reference](../reference/public-api.md#authzrego) for constructor validation and exact result semantics. See [Understanding CodeMode's security model](../explanation/security-model.md#rego-policy-runs-in-process) for the policy trust boundary and runtime restrictions.
43 changes: 25 additions & 18 deletions docs/docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,22 +110,28 @@ For the site-wide sample, the requested name is `records.lookup`. Its stable ID,
"description": { "type": "string" },
"input": {
"type": "array",
"items": { "$ref": "#/$defs/fieldShape" }
"items": {
"type": "object",
"required": ["name", "type", "required"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"type": { "type": "string" },
"required": { "type": "boolean" }
}
}
},
"output": {
"type": "array",
"items": { "$ref": "#/$defs/fieldShape" }
}
},
"$defs": {
"fieldShape": {
"type": "object",
"required": ["name", "type", "required"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"type": { "type": "string" },
"required": { "type": "boolean" }
"items": {
"type": "object",
"required": ["name", "type", "required"],
"additionalProperties": false,
"properties": {
"name": { "type": "string" },
"type": { "type": "string" },
"required": { "type": "boolean" }
}
}
}
}
Expand Down Expand Up @@ -204,7 +210,7 @@ For example, one flat `output` array can contain:
]
```

The output universe includes nested structs, arrays and slices, string-keyed
Supported output types include nested structs, arrays and slices, string-keyed
maps, pointers, named scalars, all signed and unsigned integer kinds, finite
`float32` and `float64` values, and byte slices or arrays as integer lists from
0 through 255. Integers are projected through signed 64-bit values, so a
Expand Down Expand Up @@ -254,8 +260,9 @@ Capabilities are available by dotted name. The sample native call is `records.lo

For example, a capability described with the signature
`records.search(*, count: int, active: bool, score: float, label: str | None)`
and output type `list[{id: str, active: bool, score: float}]` can be composed
inside one program:
has a `describe_api.output` field `items` of type
`list[{id: str, active: bool, score: float}]`. It can be composed inside one
program:

```python
def main():
Expand Down Expand Up @@ -284,7 +291,7 @@ shared between calls.
"required": ["result"],
"additionalProperties": false,
"properties": {
"result": {}
"result": true
}
}
```
Expand Down Expand Up @@ -348,7 +355,7 @@ After a well-formed call reaches the adapter, a resolver or service failure beco
| `resource limit exceeded` | A discovery, execution, depth, per-value, or aggregate intermediate-value budget was exceeded. |
| `capability failed` | A handler failed or returned an invalid value, including a non-finite float or an unsigned integer above `math.MaxInt64`. |
| `context canceled` | The request context was canceled. |
| `context deadline exceeded` | A service returned a bare deadline error. Root CodeMode execution deadlines are normally projected as `resource limit exceeded`. |
| `context deadline exceeded` | A service returned a bare deadline error. Root CodeMode execution deadlines are projected as `resource limit exceeded`. |
| `internal failure` | Any unknown service error or recovered adapter failure. |

Malformed MCP arguments are rejected by the SDK's input-schema validation and do not call the invocation resolver. These errors can identify malformed client-owned fields or values because validation occurs before trusted resolution. For well-formed inputs, resolver failure stops the request before search, description, or execution and becomes `unauthenticated` without resolver detail.
Expand Down
16 changes: 3 additions & 13 deletions docs/docs/reference/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ to perform that setup; `IsWorker` does not serve worker mode.

| Field | Contract |
| --- | --- |
| `ID CapabilityID` | Stable deployment and policy identity. An empty ID defaults to `Name`; explicit IDs must have no surrounding whitespace and must be unique. Set it before writing policy or deployment filters that must survive a rename. |
| `ID CapabilityID` | Stable deployment and policy identity. An empty ID defaults to `Name`; explicit IDs must have no surrounding whitespace and must be unique. An explicit `ID` preserves policy and filter identity across `Name` changes. |
| `Name CapabilityName` | A unique dotted Starlark name. A complete capability name cannot also be another capability's namespace. |
| `Summary string` | Non-empty compact text searched by `Search`, with no surrounding whitespace. |
| `Description string` | Detail returned by `Describe`. An empty value defaults to `Summary`; an explicit value must have no surrounding whitespace. |
Expand Down Expand Up @@ -165,16 +165,7 @@ Output-depth, per-value byte, and aggregate intermediate-byte exhaustion map to
| `DisabledCapabilities []CapabilityID` | Stable IDs removed when the immutable catalog is built. `New` copies the slice. |
| `Limits Limits` | Execution and discovery budgets. `New` copies the value; `Build` replaces each zero-valued field with its `DefaultLimits()` value. |

`New(options)` returns a mutable `*Builder`. A builder is single-threaded and one-shot:

1. Call `Register` for each capability.
2. Call `Build` once.
3. Use the returned immutable `*Server` concurrently.

The first `Build` call closes the builder before full validation. This remains
true when the build fails. A later `Build` returns `ErrInvalidRegistration`, and
a later `Register` panics. Create another builder to change registrations,
options, or capability visibility.
`New(options)` returns a mutable `*Builder`. A builder is single-threaded and one-shot. It accepts registrations until its single `Build` call. `Build` validates the accumulated registrations and returns an immutable, concurrency-safe `*Server`. The first `Build` call closes the builder before full validation. This remains true when the build fails. A later `Build` returns `ErrInvalidRegistration`, and a later `Register` panics. A later change to registrations, options, or capability visibility requires a new builder.

`Build` returns all capability-specific registration failures as one joined
error. It also rejects a missing authorizer, negative signed limits, namespace
Expand All @@ -198,9 +189,8 @@ ordering is a host obligation. The fixed probe deadline is independent of
- handler dispatch

When a capability omits `ID`, its dotted `Name` is also its filter identity.
Set `ID` before writing a filter that must remain stable across name changes.

The filter is deployment configuration, not a per-request policy. Use an `authz.Authorizer` for decisions that depend on the subject or validated arguments.
Static filtering is build-scoped deployment configuration. Subject- or argument-dependent decisions belong to an `authz.Authorizer`.

### Limits

Expand Down
30 changes: 20 additions & 10 deletions docs/docs/tutorials/first-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,15 @@ the server to an agent, the agent can discover and call `records.lookup`.

The repository currently requires Go 1.26.6.

This tutorial builds a local, single-user stdio server. Process ownership is
the authentication boundary, and every capability is allowed for that one
subject.

## Create a module

CodeMode has not published a release. Create a module and add CodeMode and the
official MCP Go SDK from their current default branches:
CodeMode has not published a release. Create a module, add CodeMode from
`master`, and add the official MCP Go SDK. The SDK command resolves to its
latest release:

```sh
mkdir codemode-first-server
Expand Down Expand Up @@ -107,8 +112,10 @@ go mod tidy
go build -o codemode-first-server .
```

Add the absolute binary path to your agent's MCP configuration. For agents that
use the common `mcpServers` shape:
The binary speaks MCP over stdin/stdout and is not useful to run directly.
Configure it in an agent as shown next.

Agents that use the `mcpServers` configuration shape accept:

```json
{
Expand All @@ -120,22 +127,25 @@ use the common `mcpServers` shape:
}
```

Restart or reload the agent's MCP servers. Ask the agent to search for a record
capability and call `records.lookup` with `key="alpha"` and `limit=2`. The agent
can use `search_api`, `describe_api`, and `execute`; the final structured result
is equivalent to:
Restart or reload the agent's MCP servers.

Ask the agent to search for a record capability.

Ask the agent to call `records.lookup` with `key="alpha"` and `limit=2`. The
agent can use `search_api`, `describe_api`, and `execute`. The final structured
result is equivalent to:

```json
{"result":{"count":2,"key":"alpha"}}
```

`ServeWorkerAndExit` must be the first statement in `main`, before flag parsing,
credential loading, client construction, or other setup. `execute` first binds
the keyword arguments in the worker. The parent rebinds them to `lookupInput`,
the keyword arguments in the worker process. The parent rebinds them to `lookupInput`,
creates a fresh canonical authorization map, authorizes the native call, and
then dispatches `lookup`. The parent converts the handler output to a
process-neutral value, and the worker converts it to Starlark. Each `execute`
call runs a fresh bounded interpreter in a re-executed worker process. Only the
call runs a fresh bounded interpreter in a re-executed worker. Only the
final converted value returned by a zero-argument `main()` is exposed in the
successful MCP result; printed text, globals, and interpreter-local intermediate
values are not returned. Each native-call argument map, native result, and final
Expand Down