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
56 changes: 56 additions & 0 deletions authz/authz.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package authz

import (
"context"
"errors"
)

// SubjectID is a stable, non-secret authenticated identity.
type SubjectID string

// Subject identifies the authenticated caller used for authorization decisions.
type Subject struct {
// ID is the caller's stable, non-secret identity.
ID SubjectID
}

// AuthorizationInput contains the complete trusted input for one authorization decision.
type AuthorizationInput struct {
// Subject is the authenticated identity resolved from trusted host context.
Subject Subject

// CapabilityID is the capability's stable policy identity.
CapabilityID string

// CapabilityName is the model-facing dotted capability name.
CapabilityName string

// Arguments is a fresh canonical JSON-shaped projection of validated arguments.
Arguments map[string]any
}

// Authorizer decides whether a subject may perform one validated native invocation.
type Authorizer interface {
// Authorize returns nil to allow the invocation, an error wrapping ErrDenied for a recognized denial,
// or another error when policy evaluation fails.
Authorize(context.Context, AuthorizationInput) error
}

// ErrDenied classifies a recognized authorization denial.
//
// An authorizer may wrap ErrDenied with trusted diagnostic context. Callers must not expose
// the wrapped error text to untrusted clients.
var ErrDenied = errors.New("authorization denied")

// AllowAllAuthorizer explicitly permits every authorization input.
type AllowAllAuthorizer struct{}

// AllowAll returns an explicit authorizer that permits every invocation.
func AllowAll() AllowAllAuthorizer {
return AllowAllAuthorizer{}
}

// Authorize permits the invocation without inspecting its input.
func (AllowAllAuthorizer) Authorize(context.Context, AuthorizationInput) error {
return nil
}
50 changes: 50 additions & 0 deletions authz/authz_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package authz_test

import (
"context"
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/meigma/codemode/authz"
)

// TestAllowAllRequiresExplicitConstruction proves the unrestricted policy is explicit and always permits.
func TestAllowAllRequiresExplicitConstruction(t *testing.T) {
var authorizer authz.Authorizer = authz.AllowAll()
input := authz.AuthorizationInput{
Subject: authz.Subject{ID: "subject-1"},
CapabilityID: "records.lookup",
CapabilityName: "records.lookup",
Arguments: map[string]any{"id": "alpha"},
}

require.NoError(t, authorizer.Authorize(t.Context(), input))
}

// TestDeniedClassificationKeepsTrustedDetailWrapped proves callers can classify denials without string matching.
func TestDeniedClassificationKeepsTrustedDetailWrapped(t *testing.T) {
trustedErr := fmt.Errorf("account suspended: %w", authz.ErrDenied)

require.ErrorIs(t, trustedErr, authz.ErrDenied)
assert.NotEqual(t, authz.ErrDenied.Error(), trustedErr.Error())
}

// TestAuthorizerRemainsContextAware proves the port accepts request cancellation state.
func TestAuthorizerRemainsContextAware(t *testing.T) {
ctx, cancel := context.WithCancel(t.Context())
cancel()
authorizer := contextAuthorizer{}

require.ErrorIs(t, authorizer.Authorize(ctx, authz.AuthorizationInput{}), context.Canceled)
}

// contextAuthorizer returns the request context error for contract testing.
type contextAuthorizer struct{}

// Authorize returns the current request context error.
func (contextAuthorizer) Authorize(ctx context.Context, _ authz.AuthorizationInput) error {
return ctx.Err()
}
2 changes: 2 additions & 0 deletions authz/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package authz defines the trusted authorization boundary for native capability calls.
package authz
34 changes: 34 additions & 0 deletions capability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package codemode

import (
"context"

"github.com/meigma/codemode/authz"
)

// CapabilityID is a stable deployment and authorization identity for a capability.
type CapabilityID string

// CapabilityName is the dotted name exposed to Starlark programs and model-facing discovery.
type CapabilityName string

// Handler executes one capability with trusted subject identity and typed input.
type Handler[Input, Output any] func(context.Context, authz.Subject, Input) (Output, error)

// Capability describes one typed native operation available to CodeMode.
type Capability[Input, Output any] struct {
// ID is the stable identity used by deployment filtering and authorization policy.
ID CapabilityID

// Name is the dotted Starlark name exposed to programs and discovery.
Name CapabilityName

// Summary is a compact description used by capability search.
Summary string

// Description explains the capability behavior for exact description requests.
Description string

// Handler executes the capability after binding and authorization succeed.
Handler Handler[Input, Output]
}
2 changes: 1 addition & 1 deletion doc.go
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
// Package codemode is the root package of the CodeMode library.
// Package codemode defines capability and runtime configuration contracts for code-native MCP servers.
package codemode
35 changes: 35 additions & 0 deletions errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package codemode

import "errors"

var (
// ErrInvalidRegistration classifies invalid capability registration, limits, or server construction.
ErrInvalidRegistration = errors.New("invalid registration")

// ErrUnauthenticated classifies failure to resolve a trusted invocation subject.
ErrUnauthenticated = errors.New("unauthenticated")

// ErrNotFound classifies an unavailable or disabled capability.
ErrNotFound = errors.New("capability not found")

// ErrInvalidProgram classifies invalid Starlark source or entrypoint behavior.
ErrInvalidProgram = errors.New("invalid program")

// ErrInvalidArguments classifies capability arguments rejected before authorization.
ErrInvalidArguments = errors.New("invalid capability arguments")

// ErrPermissionDenied classifies a recognized authorization denial.
ErrPermissionDenied = errors.New("permission denied")

// ErrPolicyFailure classifies an authorization evaluation failure.
ErrPolicyFailure = errors.New("authorization policy failure")

// ErrResourceLimit classifies a configured execution or conversion limit.
ErrResourceLimit = errors.New("resource limit exceeded")

// ErrCapabilityFailure classifies a native capability handler failure.
ErrCapabilityFailure = errors.New("capability failed")

// ErrInternal classifies an unexpected framework failure.
ErrInternal = errors.New("internal failure")
)
7 changes: 6 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ module github.com/meigma/codemode
go 1.26.6

require (
github.com/modelcontextprotocol/go-sdk v1.7.0
github.com/stretchr/testify v1.12.1
go.starlark.net v0.0.0-20260708150628-5395d018f003
)

require (
go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/sys v0.42.0 // indirect
)
12 changes: 11 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,12 @@
github.com/modelcontextprotocol/go-sdk v1.7.0/go.mod h1:dL7u98E/zjJTGzEq+j30jQ8K2k1mb6LeAH4inEcSGts=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/stretchr/testify v1.12.1 h1:EuwCh5fleGS7H32xRwO3wRGT7DxrDhLAT6FF8MpWDWE=
github.com/stretchr/testify v1.12.1/go.mod h1:MDEgiDPPsNp5cuIrHPPCyornHKgEVbtFUmoNlxoYthg=
go.starlark.net v0.0.0-20260708150628-5395d018f003 h1:cAxcqHgW8fnmT0cEBU3TzvVYHIFt8IIGDMWUF6rImk4=
go.starlark.net v0.0.0-20260708150628-5395d018f003/go.mod h1:Iue6g6iirlfLoVi/DYCi5/x0h/bAOuWF3dULTKpt2Vo=
go.yaml.in/yaml/v3 v3.0.5 h1:N6y/pJk8buWs9NY5ERU2HSMfm+IuD/OtfdAnq6kESPw=
go.yaml.in/yaml/v3 v3.0.5/go.mod h1:HVTZu1O7/Vkt2N+BFy8Zza+lnLsABggaTM2ZpNIGuKg=
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
2 changes: 2 additions & 0 deletions internal/binding/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package binding compiles restricted Go input and output types into immutable Starlark conversion plans.
package binding
114 changes: 114 additions & 0 deletions internal/binding/input.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package binding

import (
"fmt"
"reflect"

"go.starlark.net/starlark"
)

const keywordTupleSize = 2

// BindAs binds keyword arguments to the plan's exact generic input type and canonical arguments.
func BindAs[Input any](plan *Plan, args starlark.Tuple, kwargs []starlark.Tuple) (Input, map[string]any, error) {
var zero Input
if plan == nil || plan.inputType != reflect.TypeFor[Input]() {
return zero, nil, fmt.Errorf("%w: requested input type does not match the compiled plan", ErrInvalidPlan)
}
bound, canonical, err := plan.Bind(args, kwargs)
if err != nil {
return zero, nil, err
}
typed, ok := bound.(Input)
if !ok {
return zero, nil, fmt.Errorf("%w: bound input does not match its compiled type", ErrInvalidPlan)
}
return typed, canonical, nil
}

// Bind validates keyword-only Starlark arguments and constructs an exact Go input plus fresh canonical map.
func (plan *Plan) Bind(args starlark.Tuple, kwargs []starlark.Tuple) (any, map[string]any, error) {
if plan == nil {
return nil, nil, fmt.Errorf("%w: nil plan", ErrInvalidPlan)
}
if len(args) != 0 {
return nil, nil, fmt.Errorf("%w: positional arguments are not accepted", ErrInvalidArguments)
}

input := reflect.New(plan.inputType).Elem()
canonical := make(map[string]any, len(kwargs))
seen := make(map[string]struct{}, len(kwargs))
for _, keyword := range kwargs {
name, err := plan.bindKeyword(input, canonical, keyword)
if err != nil {
return nil, nil, err
}
if _, duplicate := seen[name]; duplicate {
return nil, nil, fmt.Errorf("%w: duplicate argument %q", ErrInvalidArguments, name)
}
seen[name] = struct{}{}
}
if err := plan.requireArguments(seen); err != nil {
return nil, nil, err
}
return input.Interface(), canonical, nil
}

// bindKeyword validates and converts one keyword into both typed and canonical representations.
func (plan *Plan) bindKeyword(input reflect.Value, canonical map[string]any, keyword starlark.Tuple) (string, error) {
if len(keyword) != keywordTupleSize {
return "", fmt.Errorf("%w: malformed keyword argument", ErrInvalidArguments)
}
name, ok := starlark.AsString(keyword[0])
if !ok {
return "", fmt.Errorf("%w: keyword name must be a string", ErrInvalidArguments)
}
fieldIndex, known := plan.inputByName[name]
if !known {
return "", fmt.Errorf("%w: unknown argument %q", ErrInvalidArguments, name)
}

field := plan.inputFields[fieldIndex]
value := keyword[1]
switch field.kind {
case fieldString:
converted, ok := starlark.AsString(value)
if !ok {
return "", fmt.Errorf("%w: argument %q must be a string", ErrInvalidArguments, name)
}
input.Field(field.index).SetString(converted)
canonical[name] = converted
case fieldOptionalInt64:
if value == starlark.None {
return name, nil
}
integer, ok := value.(starlark.Int)
if !ok {
return "", fmt.Errorf("%w: argument %q must be an integer or None", ErrInvalidArguments, name)
}
converted, ok := integer.Int64()
if !ok {
return "", fmt.Errorf("%w: argument %q overflows int64", ErrInvalidArguments, name)
}
pointer := reflect.New(input.Field(field.index).Type().Elem())
pointer.Elem().SetInt(converted)
input.Field(field.index).Set(pointer)
canonical[name] = converted
case fieldInt64, fieldBool, fieldFloat64:
return "", fmt.Errorf("%w: argument %q has an invalid compiled kind", ErrInvalidPlan, name)
}
return name, nil
}

// requireArguments verifies that every required compiled field was present.
func (plan *Plan) requireArguments(seen map[string]struct{}) error {
for _, field := range plan.inputFields {
if !field.required {
continue
}
if _, ok := seen[field.name]; !ok {
return fmt.Errorf("%w: missing required argument %q", ErrInvalidArguments, field.name)
}
}
return nil
}
Loading