diff --git a/README.md b/README.md index 5de9b9a..fed1afc 100644 --- a/README.md +++ b/README.md @@ -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() { @@ -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`. @@ -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) diff --git a/SECURITY.md b/SECURITY.md index b05fff9..7616b38 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -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 diff --git a/docs/docs/how-to/disable-capabilities.md b/docs/docs/how-to/disable-capabilities.md index 1b65d82..4e48cc0 100644 --- a/docs/docs/how-to/disable-capabilities.md +++ b/docs/docs/how-to/disable-capabilities.md @@ -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 @@ -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. diff --git a/docs/docs/how-to/use-rego-authorization.md b/docs/docs/how-to/use-rego-authorization.md index 5d97957..020684b 100644 --- a/docs/docs/how-to/use-rego-authorization.md +++ b/docs/docs/how-to/use-rego-authorization.md @@ -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 @@ -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: @@ -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. diff --git a/docs/docs/reference/mcp-tools.md b/docs/docs/reference/mcp-tools.md index 8f217df..e1ab1f5 100644 --- a/docs/docs/reference/mcp-tools.md +++ b/docs/docs/reference/mcp-tools.md @@ -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" } + } } } } @@ -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 @@ -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(): @@ -284,7 +291,7 @@ shared between calls. "required": ["result"], "additionalProperties": false, "properties": { - "result": {} + "result": true } } ``` @@ -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. diff --git a/docs/docs/reference/public-api.md b/docs/docs/reference/public-api.md index 48ddb27..a40efc8 100644 --- a/docs/docs/reference/public-api.md +++ b/docs/docs/reference/public-api.md @@ -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. | @@ -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 @@ -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 diff --git a/docs/docs/tutorials/first-server.md b/docs/docs/tutorials/first-server.md index f6feb5b..4277f0a 100644 --- a/docs/docs/tutorials/first-server.md +++ b/docs/docs/tutorials/first-server.md @@ -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 @@ -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 { @@ -120,10 +127,13 @@ 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"}} @@ -131,11 +141,11 @@ is equivalent to: `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