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
4 changes: 2 additions & 2 deletions docs/docs/explanation/security-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ Credentials stay in the host's authentication layer. They are not fields on `aut

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

1. **Exact binding and canonicalization.** CodeMode rejects positional, missing, duplicate, unknown, incorrectly typed, and out-of-range arguments. It creates the exact registered Go input and a fresh JSON-shaped argument map.
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`.

Expand Down Expand Up @@ -79,7 +79,7 @@ This projection prevents trusted diagnostic detail from becoming model-visible.
- credentials
- source or argument values copied into wrapped errors

The host can log trusted details on its side of the boundary if its authorizer, resolver, and handlers implement that logging. CodeMode's client response remains coarse. Unknown service errors and recovered adapter panics become `internal failure`.
The host can log trusted details on its side of the boundary if its authorizer, resolver, and handlers implement that logging. CodeMode's client response remains coarse. Unknown service errors and recovered adapter panics become `internal failure`. A nil `Server.Execute` context is a caller-contract violation and is currently classified as `ErrInternal`.

## Execution state does not cross calls

Expand Down
14 changes: 7 additions & 7 deletions docs/docs/reference/mcp-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Search the names and summaries of enabled capabilities.
}
```

The raw query is limited by `MaxSearchQueryBytes`. CodeMode then trims surrounding whitespace and normalizes case. Matching is a substring search over capability names and summaries. A blank normalized query returns an empty array.
The raw query is limited by `MaxSearchQueryBytes` before trimming or case normalization. Whitespace padding counts. CodeMode then trims surrounding whitespace and normalizes case. Matching is a substring search over capability names and summaries. A blank normalized query returns an empty array.

Results are sorted by exact dotted name and limited by `MaxSearchResults`. Static filtering happens before search, so disabled capabilities never appear.

Expand Down Expand Up @@ -66,7 +66,7 @@ The structured content itself is the array described above, not an object that w

## `describe_api`

Describe one enabled capability by its exact dotted name.
Describe one enabled capability by the exact dotted `name` returned by `search_api`.

### Input

Expand All @@ -83,7 +83,7 @@ Describe one enabled capability by its exact dotted name.
}
```

Name lookup is exact and case-sensitive. It does not perform search, case normalization, prefix expansion, or fuzzy matching. An unknown or disabled name returns `capability not found`.
Name lookup is exact. It neither trims nor case-folds, and it does not perform search, prefix expansion, or fuzzy matching. Clients must pass the exact `name` returned by `search_api`. An unknown or disabled name returns `capability not found`.

For the site-wide sample, the requested name is `records.lookup`. Its stable ID, `records.entry.lookup`, is intentionally not part of this tool input or output.

Expand Down Expand Up @@ -164,7 +164,7 @@ Execute one bounded Starlark program against the enabled capability namespace.

The source must define `main` as a function with no parameters. Source loading cannot call native capabilities; calls are accepted only while `main` runs. Module loading is disabled.

Capabilities are available by dotted name. The sample native call is `records.lookup(key="alpha", limit=2)`. Native calls accept keyword arguments only. For the sample, `key` is required and `limit` can be omitted, `None`, or an integer in the signed 64-bit range.
Capabilities are available by dotted name. The sample native call is `records.lookup(key="alpha", limit=2)`. Native calls accept keyword arguments only. Duplicate keyword syntax is rejected by the Starlark parser as `invalid program` before authorization or handler dispatch. Positional, unknown, missing, incorrectly typed, and out-of-range arguments reach binding and map to `invalid capability arguments`. For the sample, `key` is required and `limit` can be omitted, `None`, or an integer in the signed 64-bit range.

Each `execute` call gets a fresh interpreter and fresh source, step, elapsed-time, native-call, conversion-depth, and result-size budgets. There is no interpreter state shared between calls.

Expand All @@ -191,7 +191,7 @@ The `result` property is the final converted return value from `main`. Its runti
- an array containing supported values
- an object with string keys and supported values

Nested values are subject to `MaxValueDepth`, and the encoded `result` value is subject to `MaxResultBytes`. Starlark tuples and lists become arrays. `None` becomes `null`. Dictionaries must have string keys.
`MaxValueDepth` is inclusive. A scalar or `None` is depth 1. Each tuple, list, or dictionary wrapper adds one. A scalar with limit 1 succeeds, a one-level container with limit 2 succeeds, and one more wrapper with limit 2 fails. Nested values are subject to that limit, and the encoded `result` value is subject to `MaxResultBytes`. Starlark tuples and lists become arrays. `None` becomes `null`. Dictionaries must have string keys.

Only the final converted value crosses the execution boundary. `print` output is discarded. Globals, source-loading values, intermediate expressions, and native results that are not included in the final return value are not added to structured output. The successful envelope contains only `result`.

Expand All @@ -203,8 +203,8 @@ After a well-formed call reaches the adapter, a resolver or service failure beco
| --- | --- |
| `unauthenticated` | The resolver failed or returned an empty subject ID. |
| `capability not found` | `describe_api` did not find an enabled exact name. |
| `invalid program` | Source, entry point, runtime behavior, or final-value conversion was invalid. |
| `invalid capability arguments` | A native call failed exact argument binding. |
| `invalid program` | Source, including duplicate keyword syntax, entry point, runtime behavior, or final-value conversion was invalid. |
| `invalid capability arguments` | A native call failed binding: positional, unknown, missing, incorrectly typed, or out-of-range arguments. |
| `permission denied` | Policy returned a recognized denial. |
| `authorization policy failure` | Policy evaluation failed. |
| `resource limit exceeded` | A discovery, execution, or conversion budget was exceeded. |
Expand Down
20 changes: 11 additions & 9 deletions docs/docs/reference/public-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ The shipped input matrix is:
| `string` | `str` | Yes | `omitempty` is rejected. |
| `*int64` | `int` or `None` | No | `omitempty` is accepted but is not required for optional binding. |

Inputs accept keyword arguments only. Positional, unknown, duplicate, missing required, incorrectly typed, and out-of-range arguments are rejected. An omitted optional integer and an explicit `None` both produce a nil pointer.
Inputs accept keyword arguments only. Duplicate keyword syntax is rejected by the Starlark parser as `ErrInvalidProgram` before authorization or handler dispatch. Positional, unknown, missing required, incorrectly typed, and out-of-range arguments reach binding and map to `ErrInvalidArguments`. An omitted optional integer and an explicit `None` both produce a nil pointer.

The shipped output matrix is:

Expand Down Expand Up @@ -113,13 +113,15 @@ The filter is deployment configuration, not a per-request policy. Use an `authz.
| `MaxExecutionSteps uint64` | 1,000,000 | Starlark bytecode steps for one execution. |
| `MaxExecutionTime time.Duration` | 5 seconds | Elapsed time for one execution. |
| `MaxNativeCalls uint64` | 100 | Attempted native calls in one execution. |
| `MaxValueDepth int` | 32 | Nesting depth of the converted final value. |
| `MaxValueDepth int` | 32 | Inclusive nesting depth of the converted final value. |
| `MaxResultBytes int` | 1,048,576 bytes (1 MiB) | JSON encoding of the final value. |
| `MaxSearchQueryBytes int` | 256 bytes | Search query before normalization. |
| `MaxSearchQueryBytes int` | 256 bytes | Raw search query before trimming or case normalization. Whitespace padding counts. |
| `MaxSearchResults int` | 20 | Search results returned. |

`Limits.Validate()` returns `ErrInvalidRegistration` if any field is zero or otherwise non-positive. Zero never means unlimited. Passing a zero-value `Limits` does not select defaults; use `DefaultLimits()` explicitly.

`MaxValueDepth` is inclusive. A scalar or `None` is depth 1. Each tuple, list, or dictionary wrapper adds one. A scalar with limit 1 succeeds, a one-level container with limit 2 succeeds, and one more wrapper with limit 2 fails.

Execution limits constrain an in-process interpreter. `MaxExecutionTime` cancels Starlark evaluation, but it cannot forcibly interrupt blocking Go authorizers or handlers. See [Security model](../explanation/security-model.md#cancellation-and-host-code).

### Server operations
Expand All @@ -132,7 +134,7 @@ Execution limits constrain an in-process interpreter. `MaxExecutionTime` cancels
Search(query string) ([]SearchResult, error)
```

`Search` first enforces `MaxSearchQueryBytes`, then trims surrounding whitespace and normalizes case. It performs substring matching against enabled capability names and summaries. Results are sorted by exact capability name and capped by `MaxSearchResults`. A blank normalized query returns an empty, non-nil result.
`Search` first enforces `MaxSearchQueryBytes` on the raw query, then trims surrounding whitespace and normalizes case. Whitespace padding counts toward the byte budget. It performs substring matching against enabled capability names and summaries. Results are sorted by exact capability name and capped by `MaxSearchResults`. A blank normalized query returns an empty, non-nil result.

`SearchResult` contains these JSON fields:

Expand All @@ -150,7 +152,7 @@ An oversized query returns `ErrResourceLimit`. An unexpected server-state failur
Describe(name CapabilityName) (Description, error)
```

`Describe` performs an exact, case-sensitive lookup of an enabled dotted name. An unavailable, unknown, or disabled name returns `ErrNotFound`.
`Describe` performs an exact lookup of an enabled dotted name. It neither trims nor case-folds. Clients must pass the exact `name` returned by `Search`. An unavailable, unknown, or disabled name returns `ErrNotFound`.

`Description` contains:

Expand All @@ -171,7 +173,7 @@ Each field shape has `name` (string), `type` (string), and `required` (boolean).
Execute(ctx context.Context, subject authz.Subject, program Program) (any, error)
```

`Program` is Starlark source. The context must be non-nil and the subject ID must be non-empty.
`Program` is Starlark source. The context must be non-nil and the subject ID must be non-empty. A nil context is a caller-contract violation and is currently classified as `ErrInternal`.

Every call creates a fresh interpreter and fresh budgets. Module loading is disabled. The enabled capability names form the predeclared namespace. Source loading must define a function named `main` that accepts no positional parameters, keyword-only parameters, variadic positional parameters, or variadic keyword parameters. Native capability calls are accepted only while `main` is running.

Expand All @@ -195,13 +197,13 @@ Use `errors.Is` to inspect these exported sentinel errors. Client adapters can r
| `ErrInvalidRegistration` | Invalid capability metadata or types, builder use, limits, static filters, or server construction. |
| `ErrUnauthenticated` | Missing trusted subject. |
| `ErrNotFound` | Unknown, unavailable, or disabled capability. |
| `ErrInvalidProgram` | Invalid source, entry point, runtime behavior, or unsupported final value. |
| `ErrInvalidArguments` | Native arguments rejected before authorization. |
| `ErrInvalidProgram` | Invalid source, including duplicate keyword syntax, invalid entry point, runtime behavior, or unsupported final value. |
| `ErrInvalidArguments` | Native arguments rejected at binding before authorization: positional, unknown, missing required, incorrectly typed, or out-of-range. |
| `ErrPermissionDenied` | Authorizer error that wraps `authz.ErrDenied`. |
| `ErrPolicyFailure` | Other authorizer error or recovered authorizer panic. |
| `ErrResourceLimit` | Source, step, time, native-call, conversion, result, or search budget exceeded. |
| `ErrCapabilityFailure` | Handler error or invalid handler output conversion. |
| `ErrInternal` | Unexpected framework state, recovered handler panic, or other recovered internal failure. |
| `ErrInternal` | Unexpected framework state, recovered handler panic, other recovered internal failure, or a nil `Execute` context. |

Request cancellation returns `context.Canceled`. A deadline is classified as `ErrResourceLimit` and also wraps `context.DeadlineExceeded` at the root API.

Expand Down
6 changes: 3 additions & 3 deletions docs/docs/tutorials/first-server.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ This tutorial builds an in-process MCP server with one typed capability. You wil

## Create a module

CodeMode has not published a release. To follow the tutorial against the current `master` branch, create a module and add CodeMode:
CodeMode has not published a release. To follow the tutorial against the current `master` branch, create a module and add CodeMode and the official MCP Go SDK:

```sh
mkdir codemode-first-server
cd codemode-first-server
go mod init example.com/codemode-first-server
go get github.com/meigma/codemode@master
go get github.com/meigma/codemode@master github.com/modelcontextprotocol/go-sdk/mcp
```

The repository currently requires Go 1.26.6.
Expand Down Expand Up @@ -186,7 +186,7 @@ The resolver reads only the typed, server-side Go context. In a production host,

## Run the server

Resolve the direct dependencies and run the program:
Clean up the module metadata, then run the program:

```sh
go mod tidy
Expand Down
17 changes: 15 additions & 2 deletions internal/binding/output_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,13 +121,26 @@ func TestConvertFinalRejectsUnsupportedAndOverflowingValues(t *testing.T) {
}
}

// TestConvertFinalEnforcesDepthAndEncodedSize proves both conversion budgets are positive hard limits.
// TestConvertFinalEnforcesDepthAndEncodedSize proves inclusive nesting depth and both conversion budgets are positive hard limits.
func TestConvertFinalEnforcesDepthAndEncodedSize(t *testing.T) {
converted, err := ConvertFinal(starlark.String("value"), 1, 1024)
require.NoError(t, err)
assert.Equal(t, "value", converted)

converted, err = ConvertFinal(starlark.None, 1, 1024)
require.NoError(t, err)
assert.Nil(t, converted)

shallow := starlark.NewList([]starlark.Value{starlark.String("value")})
converted, err = ConvertFinal(shallow, 2, 1024)
require.NoError(t, err)
assert.Equal(t, []any{"value"}, converted)

deep := starlark.NewList([]starlark.Value{
starlark.NewList([]starlark.Value{starlark.String("value")}),
})

_, err := ConvertFinal(deep, 2, 1024)
_, err = ConvertFinal(deep, 2, 1024)
require.Error(t, err)
require.ErrorIs(t, err, ErrValueLimit)
assert.Contains(t, err.Error(), "depth")
Expand Down
21 changes: 21 additions & 0 deletions internal/execution/execute_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,27 @@ func TestExecuteRejectsMalformedArgumentsBeforePolicy(t *testing.T) {
assert.Zero(t, handlerCalls.Load())
}

// TestExecuteRejectsDuplicateKeywordSyntaxAsInvalidProgram proves repeated keywords fail at parse time before policy.
func TestExecuteRejectsDuplicateKeywordSyntaxAsInvalidProgram(t *testing.T) {
authorizer := authzmocks.NewMockAuthorizer(t)
var handlerCalls atomic.Int64
capabilityCatalog := buildEngine(t, func(context.Context, authz.Subject, any) (any, error) {
handlerCalls.Add(1)
return testOutput{}, nil
})

_, err := capabilityCatalog.Execute(
t.Context(),
authz.Subject{ID: "subject-1"},
`def main(): return records.lookup(value="alpha", value="beta")`,
authorizer,
defaultExecutionLimits(),
)

require.ErrorIs(t, err, execution.ErrInvalidProgram)
assert.Zero(t, handlerCalls.Load())
}

// TestExecuteCancellationAfterAuthorizationPreventsDispatch proves a stale allow cannot cross the handler boundary.
func TestExecuteCancellationAfterAuthorizationPreventsDispatch(t *testing.T) {
tests := []struct {
Expand Down