Skip to content

Commit 6f897f1

Browse files
Mauricio GomesMauricio Gomes
authored andcommitted
Rescan scoped capability roots after method calls
1 parent c4b46b2 commit 6f897f1

3 files changed

Lines changed: 103 additions & 22 deletions

File tree

vibes/capability_contracts.go

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,15 @@ func validateCapabilityDataOnlyValue(label string, val Value) error {
3131

3232
func bindCapabilityContracts(
3333
val Value,
34-
contractsByName map[string]CapabilityMethodContract,
34+
scope *capabilityContractScope,
3535
target map[*Builtin]CapabilityMethodContract,
36-
scopes map[*Builtin]map[string]CapabilityMethodContract,
36+
scopes map[*Builtin]*capabilityContractScope,
3737
) {
38-
if len(contractsByName) == 0 {
38+
if scope == nil || len(scope.contracts) == 0 {
3939
return
4040
}
4141
scanner := newCapabilityContractScanner()
42-
scanner.bindContracts(val, contractsByName, target, scopes)
42+
scanner.bindContracts(val, scope, target, scopes)
4343
}
4444

4545
func (s *capabilityContractScanner) containsCallable(val Value) bool {
@@ -83,17 +83,17 @@ func (s *capabilityContractScanner) containsCallable(val Value) bool {
8383

8484
func (s *capabilityContractScanner) bindContracts(
8585
val Value,
86-
contractsByName map[string]CapabilityMethodContract,
86+
scope *capabilityContractScope,
8787
target map[*Builtin]CapabilityMethodContract,
88-
scopes map[*Builtin]map[string]CapabilityMethodContract,
88+
scopes map[*Builtin]*capabilityContractScope,
8989
) {
9090
switch val.Kind() {
9191
case KindBuiltin:
9292
builtin := val.Builtin()
9393
if _, seen := scopes[builtin]; !seen {
94-
scopes[builtin] = contractsByName
94+
scopes[builtin] = scope
9595
}
96-
if contract, ok := contractsByName[builtin.Name]; ok {
96+
if contract, ok := scope.contracts[builtin.Name]; ok {
9797
if _, seen := target[builtin]; !seen {
9898
target[builtin] = contract
9999
}
@@ -110,7 +110,7 @@ func (s *capabilityContractScanner) bindContracts(
110110
}
111111
s.seenArrays[id] = struct{}{}
112112
for _, item := range values {
113-
s.bindContracts(item, contractsByName, target, scopes)
113+
s.bindContracts(item, scope, target, scopes)
114114
}
115115
case KindHash, KindObject:
116116
entries := val.Hash()
@@ -120,7 +120,7 @@ func (s *capabilityContractScanner) bindContracts(
120120
}
121121
s.seenMaps[ptr] = struct{}{}
122122
for _, item := range entries {
123-
s.bindContracts(item, contractsByName, target, scopes)
123+
s.bindContracts(item, scope, target, scopes)
124124
}
125125
case KindClass:
126126
classDef := val.Class()
@@ -132,7 +132,7 @@ func (s *capabilityContractScanner) bindContracts(
132132
}
133133
s.seenClasses[classDef] = struct{}{}
134134
for _, item := range classDef.ClassVars {
135-
s.bindContracts(item, contractsByName, target, scopes)
135+
s.bindContracts(item, scope, target, scopes)
136136
}
137137
case KindInstance:
138138
instance := val.Instance()
@@ -144,10 +144,10 @@ func (s *capabilityContractScanner) bindContracts(
144144
}
145145
s.seenInstances[instance] = struct{}{}
146146
for _, item := range instance.Ivars {
147-
s.bindContracts(item, contractsByName, target, scopes)
147+
s.bindContracts(item, scope, target, scopes)
148148
}
149149
if instance.Class != nil {
150-
s.bindContracts(NewClass(instance.Class), contractsByName, target, scopes)
150+
s.bindContracts(NewClass(instance.Class), scope, target, scopes)
151151
}
152152
}
153153
}

vibes/capability_contracts_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,47 @@ func (c legacyFooCapability) Bind(binding CapabilityBinding) (map[string]Value,
256256
}, nil
257257
}
258258

259+
type siblingMutationContractCapability struct {
260+
invokeCount *int
261+
}
262+
263+
func (c siblingMutationContractCapability) Bind(binding CapabilityBinding) (map[string]Value, error) {
264+
peer := NewInstance(&Instance{
265+
Class: &ClassDef{
266+
Name: "PeerHost",
267+
Methods: map[string]*ScriptFunction{},
268+
ClassMethods: map[string]*ScriptFunction{},
269+
ClassVars: map[string]Value{},
270+
},
271+
Ivars: map[string]Value{},
272+
})
273+
return map[string]Value{
274+
"publisher": NewObject(map[string]Value{
275+
"install": NewBuiltin("publisher.install", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) {
276+
peer.Instance().Ivars["call"] = NewBuiltin("peer.call", func(exec *Execution, receiver Value, args []Value, kwargs map[string]Value, block Value) (Value, error) {
277+
*c.invokeCount = *c.invokeCount + 1
278+
return NewString("ok"), nil
279+
})
280+
return NewString("installed"), nil
281+
}),
282+
}),
283+
"peer": peer,
284+
}, nil
285+
}
286+
287+
func (c siblingMutationContractCapability) CapabilityContracts() map[string]CapabilityMethodContract {
288+
return map[string]CapabilityMethodContract{
289+
"peer.call": {
290+
ValidateArgs: func(args []Value, kwargs map[string]Value, block Value) error {
291+
if len(args) != 1 || args[0].Kind() != KindInt {
292+
return fmt.Errorf("peer.call expects int")
293+
}
294+
return nil
295+
},
296+
},
297+
}
298+
}
299+
259300
func TestCapabilityContractRejectsInvalidArguments(t *testing.T) {
260301
engine := MustNewEngine(Config{})
261302
script, err := engine.Compile(`def run()
@@ -491,3 +532,30 @@ end`)
491532
t.Fatalf("unexpected result: %#v", result)
492533
}
493534
}
535+
536+
func TestCapabilityContractsBindAfterSiblingScopeMutation(t *testing.T) {
537+
engine := MustNewEngine(Config{})
538+
script, err := engine.Compile(`def run()
539+
publisher.install()
540+
peer.call("bad")
541+
end`)
542+
if err != nil {
543+
t.Fatalf("compile failed: %v", err)
544+
}
545+
546+
invocations := 0
547+
_, err = script.Call(context.Background(), "run", nil, CallOptions{
548+
Capabilities: []CapabilityAdapter{
549+
siblingMutationContractCapability{invokeCount: &invocations},
550+
},
551+
})
552+
if err == nil {
553+
t.Fatalf("expected sibling-mutation contract validation error")
554+
}
555+
if got := err.Error(); !strings.Contains(got, "peer.call expects int") {
556+
t.Fatalf("unexpected error: %s", got)
557+
}
558+
if invocations != 0 {
559+
t.Fatalf("sibling mutation capability should not execute when contract fails")
560+
}
561+
}

vibes/execution.go

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,14 +55,19 @@ type Execution struct {
5555
moduleLoadStack []string
5656
moduleStack []moduleContext
5757
capabilityContracts map[*Builtin]CapabilityMethodContract
58-
capabilityContractScopes map[*Builtin]map[string]CapabilityMethodContract
58+
capabilityContractScopes map[*Builtin]*capabilityContractScope
5959
capabilityContractsByName map[string]CapabilityMethodContract
6060
receiverStack []Value
6161
envStack []*Env
6262
strictEffects bool
6363
allowRequire bool
6464
}
6565

66+
type capabilityContractScope struct {
67+
contracts map[string]CapabilityMethodContract
68+
roots []Value
69+
}
70+
6671
type moduleContext struct {
6772
key string
6873
path string
@@ -625,14 +630,19 @@ func (exec *Execution) invokeCallable(callee Value, receiver Value, args []Value
625630
return NewNil(), exec.wrapError(err, pos)
626631
}
627632
}
628-
scopeContracts := exec.capabilityContractScopes[builtin]
629-
if len(scopeContracts) > 0 {
633+
scope := exec.capabilityContractScopes[builtin]
634+
if scope != nil && len(scope.contracts) > 0 {
630635
// Capability methods can lazily publish additional builtins at runtime
631636
// (e.g. through factory return values or receiver mutation). Re-scan
632637
// these values so future calls still enforce declared contracts.
633-
bindCapabilityContracts(result, scopeContracts, exec.capabilityContracts, exec.capabilityContractScopes)
638+
bindCapabilityContracts(result, scope, exec.capabilityContracts, exec.capabilityContractScopes)
634639
if receiver.Kind() != KindNil {
635-
bindCapabilityContracts(receiver, scopeContracts, exec.capabilityContracts, exec.capabilityContractScopes)
640+
bindCapabilityContracts(receiver, scope, exec.capabilityContracts, exec.capabilityContractScopes)
641+
}
642+
// Methods can mutate sibling scope roots via captured references; refresh
643+
// all adapter roots so newly exposed builtins also get bound.
644+
for _, root := range scope.roots {
645+
bindCapabilityContracts(root, scope, exec.capabilityContracts, exec.capabilityContractScopes)
636646
}
637647
}
638648
return result, nil
@@ -3413,7 +3423,7 @@ func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallO
34133423
moduleLoadStack: make([]string, 0, 8),
34143424
moduleStack: make([]moduleContext, 0, 8),
34153425
capabilityContracts: make(map[*Builtin]CapabilityMethodContract),
3416-
capabilityContractScopes: make(map[*Builtin]map[string]CapabilityMethodContract),
3426+
capabilityContractScopes: make(map[*Builtin]*capabilityContractScope),
34173427
capabilityContractsByName: make(map[string]CapabilityMethodContract),
34183428
receiverStack: make([]Value, 0, 8),
34193429
envStack: make([]*Env, 0, 8),
@@ -3427,7 +3437,9 @@ func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallO
34273437
if adapter == nil {
34283438
continue
34293439
}
3430-
adapterContracts := map[string]CapabilityMethodContract{}
3440+
scope := &capabilityContractScope{
3441+
contracts: map[string]CapabilityMethodContract{},
3442+
}
34313443
if provider, ok := adapter.(CapabilityContractProvider); ok {
34323444
for methodName, contract := range provider.CapabilityContracts() {
34333445
name := strings.TrimSpace(methodName)
@@ -3438,7 +3450,7 @@ func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallO
34383450
return NewNil(), fmt.Errorf("duplicate capability contract for %s", name)
34393451
}
34403452
exec.capabilityContractsByName[name] = contract
3441-
adapterContracts[name] = contract
3453+
scope.contracts[name] = contract
34423454
}
34433455
}
34443456
globals, err := adapter.Bind(binding)
@@ -3448,7 +3460,8 @@ func (s *Script) Call(ctx context.Context, name string, args []Value, opts CallO
34483460
for name, val := range globals {
34493461
rebound := rebinder.rebindValue(val)
34503462
root.Define(name, rebound)
3451-
bindCapabilityContracts(rebound, adapterContracts, exec.capabilityContracts, exec.capabilityContractScopes)
3463+
scope.roots = append(scope.roots, rebound)
3464+
bindCapabilityContracts(rebound, scope, exec.capabilityContracts, exec.capabilityContractScopes)
34523465
}
34533466
}
34543467
}

0 commit comments

Comments
 (0)