-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuilder.go
More file actions
215 lines (197 loc) · 7.24 KB
/
Copy pathbuilder.go
File metadata and controls
215 lines (197 loc) · 7.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
package codemode
import (
"context"
"errors"
"fmt"
"reflect"
"slices"
"github.com/meigma/codemode/authz"
"github.com/meigma/codemode/internal/binding"
"github.com/meigma/codemode/internal/catalog"
)
// Options configures one immutable CodeMode server build.
type Options struct {
// Authorizer decides whether each validated native capability call may dispatch.
Authorizer authz.Authorizer
// DisabledCapabilities lists stable capability IDs removed from every live server surface.
DisabledCapabilities []CapabilityID
// Limits contains execution, conversion, and discovery budgets. Build
// replaces each zero-valued field with the corresponding DefaultLimits value.
Limits Limits
}
// Builder collects capability registrations for one immutable Server.
//
// A Builder is single-threaded and one-shot. Its first Build call closes registration even when
// validation fails. Construct another Builder to change configuration or capability visibility.
type Builder struct {
// authorizer is the required authorization port retained by the eventual server.
authorizer authz.Authorizer
// disabledCapabilities is a private copy of the static deployment filter.
disabledCapabilities []string
// limits is the value-copied server budget configuration.
limits Limits
// registrations contains validated, precompiled capability registrations until Build.
registrations []catalog.Registration
// registrationErrors contains capability-specific failures deferred to Build.
registrationErrors []error
// built reports whether Build has closed this builder.
built bool
}
// New creates a mutable one-shot Builder and copies caller-owned option slices.
//
// The final binary must call ServeWorkerAndExit as the first statement of main
// before it calls New or performs ordinary host setup. Test binaries that call
// Build must make the same call as the first statement of TestMain.
func New(options Options) *Builder {
disabled := make([]string, len(options.DisabledCapabilities))
for index, id := range options.DisabledCapabilities {
disabled[index] = string(id)
}
return &Builder{
authorizer: options.Authorizer,
disabledCapabilities: disabled,
limits: options.Limits,
}
}
// Register compiles and retains one typed capability without erasing its
// binding contract first.
//
// Capability-specific failures are accumulated and returned together by Build.
// A name whose first dotted segment collides with a reserved Starlark universe
// root, including standard builtins, sum, json, and math, is recorded as an
// invalid registration; nested leaves such as stats.sum remain legal.
// Register panics when builder is nil or already closed because no future Build
// call can report those lifecycle violations.
func Register[Input, Output any](builder *Builder, capability Capability[Input, Output]) {
if builder == nil {
panic(fmt.Errorf("%w: nil builder", ErrInvalidRegistration))
}
if builder.built {
panic(fmt.Errorf("%w: builder is already closed", ErrInvalidRegistration))
}
if capability.Handler == nil {
builder.recordRegistrationError(capability.Name, errors.New("handler must not be nil"))
return
}
plan, err := binding.CompileFor[Input, Output]()
if err != nil {
builder.recordRegistrationError(capability.Name, err)
return
}
id := capability.ID
if id == "" {
id = CapabilityID(capability.Name)
}
description := capability.Description
if description == "" {
description = capability.Summary
}
handler := capability.Handler
registration := catalog.Registration{
ID: string(id),
Name: string(capability.Name),
Summary: capability.Summary,
Description: description,
SearchTerms: slices.Clone(capability.SearchTerms),
Plan: plan,
Invoke: func(ctx context.Context, subject authz.Subject, input any) (any, error) {
typed, ok := input.(Input)
if !ok {
return nil, catalog.ErrInputTypeMismatch
}
return handler(ctx, subject, typed)
},
}
if err := catalog.ValidateRegistration(registration); err != nil {
builder.recordRegistrationError(capability.Name, err)
return
}
for _, existing := range builder.registrations {
if existing.ID == registration.ID {
builder.recordRegistrationError(
capability.Name,
fmt.Errorf("duplicate capability ID %q", registration.ID),
)
return
}
if existing.Name == registration.Name {
builder.recordRegistrationError(
capability.Name,
fmt.Errorf("duplicate capability name %q", registration.Name),
)
return
}
}
builder.registrations = append(builder.registrations, registration)
}
// recordRegistrationError retains one capability-specific programmer error.
func (builder *Builder) recordRegistrationError(name CapabilityName, err error) {
builder.registrationErrors = append(builder.registrationErrors, fmt.Errorf(
"%w: capability %q: %w",
ErrInvalidRegistration,
name,
err,
))
}
// Build closes the Builder and returns an immutable concurrency-safe Server
// after full validation and a same-executable worker probe.
//
// Build allows up to five seconds for the probe exchange, then kills and reaps
// the probe child; operating-system spawn and kill/reap overhead can extend the
// call beyond that exchange deadline. Build has no context and the probe
// deadline is not configurable.
//
// The final binary must call ServeWorkerAndExit as the first statement of main,
// and a test binary that calls Build must do the same in TestMain. The probe
// detects an absent or nonfunctional worker entry, but it cannot detect ordinary
// host work that completes silently before ServeWorkerAndExit is called.
func (builder *Builder) Build() (*Server, error) {
if builder == nil {
return nil, fmt.Errorf("%w: nil builder", ErrInvalidRegistration)
}
if builder.built {
return nil, fmt.Errorf("%w: builder is already closed", ErrInvalidRegistration)
}
builder.built = true
registrations := slices.Clone(builder.registrations)
builder.registrations = nil
buildErrors := slices.Clone(builder.registrationErrors)
builder.registrationErrors = nil
limits := builder.limits.withDefaults()
if isNilAuthorizer(builder.authorizer) {
buildErrors = append(
buildErrors,
fmt.Errorf("%w: Authorizer must not be nil", ErrInvalidRegistration),
)
}
if err := limits.Validate(); err != nil {
buildErrors = append(buildErrors, err)
}
if len(buildErrors) > 0 {
return nil, errors.Join(buildErrors...)
}
capabilityCatalog, err := catalog.Build(registrations, catalog.Options{
DisabledCapabilities: slices.Clone(builder.disabledCapabilities),
MaxSearchQueryBytes: limits.MaxSearchQueryBytes,
MaxSearchResults: limits.MaxSearchResults,
})
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidRegistration, err)
}
server, err := newServer(capabilityCatalog, builder.authorizer, limits)
if err != nil {
return nil, fmt.Errorf("%w: %w", ErrInvalidRegistration, err)
}
return server, nil
}
// isNilAuthorizer reports whether an authorization interface is nil or contains a typed nil value.
func isNilAuthorizer(authorizer authz.Authorizer) bool {
if authorizer == nil {
return true
}
value := reflect.ValueOf(authorizer)
kind := value.Kind()
nilable := kind == reflect.Chan || kind == reflect.Func || kind == reflect.Map ||
kind == reflect.Pointer || kind == reflect.Slice
return nilable && value.IsNil()
}