Skip to content

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

Every search_api, describe_api, and execute call runs an mcpserver.InvocationResolver. In a single-user stdio deployment, mcpserver.StaticSubject treats process ownership as the authentication boundary. A multi-user host must not use StaticSubject; authentication middleware must validate each request, store the non-secret identity with authz.WithSubject, and use mcpserver.ContextSubject.

Typed, host-owned Go context gives a multi-user host an identity channel outside model-visible data. These values are untrusted and cannot establish or replace a subject:

  • MCP tool arguments
  • Starlark source or values
  • MCP request _meta
  • other client-controlled request metadata

Credentials stay in the host's authentication layer. They are not fields on authz.Subject, authorization arguments, capability inputs, or Starlark values. The resolver returns only a stable, non-secret subject ID. A resolver error or empty subject ID produces the coarse unauthenticated failure before discovery or execution starts.

mcpserver.New creates an SDK server, not a complete network service. The host owns authentication, official MCP transport creation, listeners, request cancellation, connection lifecycle, and shutdown. CodeMode does not take ownership of those resources and does not provide a generic downstream MCP forwarding path.

Validation precedes authority

A native call passes four stages in a fixed order:

  1. Worker binding. The worker validates exact keyword arguments against the registered input shape and sends normalized values to the parent. Duplicate keyword syntax is rejected by the Starlark parser as ErrInvalidProgram. Positional, missing, unknown, incorrectly typed, and out-of-range arguments map to ErrInvalidArguments.
  2. Parent rebinding and canonicalization. The parent reconstructs the exact registered Go input and creates a fresh JSON-shaped authorization map.
  3. Authorization. CodeMode passes the trusted subject, stable capability ID, dotted capability name, and canonical arguments to authz.Authorizer.
  4. 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 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.

Authorization is evaluated for every attempted native call whose arguments bind successfully. A prior allowed call does not grant authority to a later call.

AllowAll is an explicit policy choice

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

The optional authz/rego adapter prepares trusted Rego module source as an OPA library inside the CodeMode host. It does not contact a remote OPA service. Module source must come from trusted deployment configuration, such as a compiled-in string or go:embed file. The adapter's restrictions reduce the policy evaluator's capabilities; they do not make policy from an untrusted author safe to run in the host process.

The adapter starts with OPA's Rego v1 capabilities and removes every builtin that OPA marks nondeterministic. That removal is what takes away runtime network-capable builtins, including http.send, plus DNS, runtime, random, time, and UUID builtins, before policy preparation.

The adapter also sets AllowNet to a non-nil empty slice. That empty list is a deny-all host allowlist and defense in depth. It is not the mechanism that removes http.send. If a remaining code path still tried to reach a network host, the empty list would deny it.

CodeMode installs no schema set and no schema resolver. Metadata schema["https://example.invalid/schema.json"] names a schema in a set that was never installed, so the annotation is accepted but ignored: there is no validation and no fetch. Metadata with an external $ref: "https://example.invalid/schema.json" asks OPA to load a remote schema and is rejected because remote reference loading is disabled.

StrictBuiltinErrors(true) makes builtin errors fatal. A failing builtin cannot become an undefined rule branch while another branch allows the call. EnablePrintStatements(false) erases print calls during compilation. The adapter installs no print hook, tracer, custom builtin, data store, or other policy hook.

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 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 for configuration and the authz/rego API reference for the exact input and result contracts.

Static filtering reduces the exposed catalog

Options.DisabledCapabilities removes capabilities by stable CapabilityID when the immutable server is built. Disabled entries are absent from search results, exact description, the Starlark namespace, and execution. An unknown disabled ID fails the build rather than silently leaving a capability exposed.

Static filtering is useful for deployment-wide availability, but it is not dynamic authorization. It cannot express subject-specific or argument-specific decisions. Conversely, authorization alone does not hide a capability's metadata from discovery. Use static filtering to remove a capability from the deployment surface and authorization to decide whether an enabled native call may dispatch.

Discovery metadata is observable

Search indexes each enabled capability's name, SearchTerms, summary, and description. A search response returns only the exact name, signature, and summary, but omission is not secrecy. A caller can submit different queries and infer whether particular vocabulary changes the ranked results.

Treat SearchTerms as model-visible discovery metadata even though the terms are not returned directly. Do not put secrets, credentials, policy facts, tenant identifiers, or sensitive examples in them. Search terms do not create callable aliases and are not accepted by exact Describe or by execution.

Static filtering removes a disabled capability before the search index is built, so its metadata does not contribute tokens or ranking. Authorization is different: search does not run the per-capability authorizer and does not hide enabled discovery metadata based on the resolved subject.

Client errors are intentionally coarse

Detailed causes exist only on the trusted side of the public boundary. Internal packages and host authorizers, resolvers, and handlers can hold or log those causes. A direct call to authz/rego.Authorize can return an ordinary error that identifies an undefined or non-Boolean decision, or carries an OPA evaluation or builtin failure.

codemode.Server.Execute removes those trusted causes. It returns the documented public sentinel for execution, policy, handler, resource, and internal failures. Request cancellation returns context.Canceled. A deadline returns ErrResourceLimit and preserves context.DeadlineExceeded for errors.Is. Root Error() strings stay exactly coarse. Approved model-derived parser, resolver, and binding detail may travel with the sentinel for MCP formatting, but it is not part of the root error text.

The MCP adapter narrows the boundary again. It emits the nine fixed error texts in the MCP tool reference, plus two stable prefixes that may append approved CodeMode execution detail: invalid program: ... for parse and resolve positions and messages, and invalid capability arguments: ... for binding diagnostics. Resolver and custom-service details and recovered panic values become coarse responses. SDK input-schema errors are different: they occur before trusted subject resolution and can identify malformed client-owned fields or values.

This projection prevents host-derived diagnostic detail from becoming model-visible. MCP responses do not expose budget values, filtered capability identities, unknown requested names, host-derived argument values, Rego decision paths or rule names, handler messages, credentials, panic values, or stack details. The only MCP exceptions are parse or resolve positions and messages and binding argument diagnostics produced by the program that the service executed. With the shipped *codemode.Server, that program is the submitted source.

If a host needs detailed diagnostics, its trusted authorizer, resolver, or handler must record them before returning. CodeMode cannot recover a discarded cause after the root or MCP projection. Apply the host's normal access controls and redaction rules to those logs.

An allowed or denied call proves only the result for that subject, capability, and canonical argument set. It does not show whether the policy is default-open, default-deny, complete, or incomplete. Treat an unexpected denial or policy failure as a reason to contact the host, not as evidence about the policy's rules or defaults.

Execution state does not cross calls

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. The predeclared environment is the fixed language surface (sum, json, math, and the standard Starlark builtins) plus 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 normalized native call crosses the private protocol, is rebound to the exact registered input type in the parent, is authorized there, and is then dispatched to the parent handler. The parent converts the handler output to a bounded process-neutral value. Its encoded native-result body crosses back to the worker, which converts that value to Starlark so the program can continue.

These rules isolate interpreter state and make Starlark execution killable. They do not confine registered Go code: authorizers, the optional Rego evaluator, and handlers run in the host process with the privileges the host gave that process.

Cancellation and host code

CodeMode derives one execution context from MaxExecutionTime and the request context. The elapsed budget starts before waiting for a worker slot and covers spawn, protocol exchange, Starlark execution, and parent dispatch. Killing and reaping the worker can add operating-system overhead beyond the budget. Source, bytecode-step, attempted-native-call, crossing-value depth, and per-crossing encoded-size limits apply independently.

MaxIntermediateValueBytes additionally bounds the request-scoped sum of successful parent-to-child native-result value bodies. A result is encoded before its body length is checked and debited. This cumulative budget is independent of MaxValueBytes, starts fresh for every Execute, and includes successful native results that the program later discards. It does not measure handler allocations, Starlark object memory, process RSS, or an operating-system memory quota.

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, including a monolithic built-in that does not observe interpreter cancellation.

Cancellation remains cooperative after a native call reaches parent Go code. Parent dispatch runs asynchronously so Server.Execute can return after canceling and reaping its worker, but CodeMode cannot forcibly stop an authorizer or handler goroutine or undo its side effects. A non-cooperative authorizer or handler may continue consuming host resources after Execute returns.

The Rego adapter passes the context into OPA evaluation and checks cancellation both before and after that call. The second check preserves context.Canceled or context.DeadlineExceeded when cancellation races with an OPA error. Cancellation does not forcibly interrupt arbitrary parent Go code.

Authorizers and handlers must:

  • honor the supplied context for I/O, locks, waits, and downstream calls
  • return promptly after cancellation
  • bound their own memory, network, storage, and retry behavior
  • avoid exposing credentials or trusted diagnostics in returned values or client-visible errors
  • be safe for concurrent calls when the immutable server is shared

CodeMode recovers panics at selected boundaries and returns a coarse classification. Panic recovery does not replace normal error handling, cancellation, or resource control in host code.

Worker processes are not tenant isolation

The worker boundary separates Starlark interpreter state from the parent and permits process kill and reap. It does not establish a complete security boundary between mutually untrusted tenants. A worker runs as the host 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 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 and every call returns to the parent authorization boundary. Those language restrictions reduce reachability; they do not replace operating-system containment.

A host that needs a hard tenant, CPU, heap, filesystem, credential, or network boundary must add container or workload isolation and operating-system resource controls. Keep credentials and other parent-only setup after ServeWorkerAndExit so worker processes do not initialize resources they do not need.

See Public API reference for the Go contracts and MCP tool reference for the exact client-visible surface.