Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f0cc20bbf | ||
|
|
f14846d758 |
@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"slices"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -97,10 +98,8 @@ func TestCaptchaVerifiedReadsTrustedHeader(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAllTrustedHeadersIncludesCaptcha(t *testing.T) {
|
func TestAllTrustedHeadersIncludesCaptcha(t *testing.T) {
|
||||||
for _, h := range AllTrustedHeaders() {
|
if slices.Contains(AllTrustedHeaders(), HeaderVerifiedCaptcha) {
|
||||||
if h == HeaderVerifiedCaptcha {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
|
||||||
t.Fatalf("AllTrustedHeaders() missing %s — hosts strip-then-stamp from this list, so omitting it makes the header client-forgeable", HeaderVerifiedCaptcha)
|
t.Fatalf("AllTrustedHeaders() missing %s — hosts strip-then-stamp from this list, so omitting it makes the header client-forgeable", HeaderVerifiedCaptcha)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
# Register sees the current guest host services
|
||||||
|
|
||||||
|
Go guest plugins can declare capability-backed runtime surfaces from their
|
||||||
|
`Register` callback. Bridge providers are the first such surface: the provider
|
||||||
|
stores its invokable value in the guest-local bridge stub while the stub tells
|
||||||
|
the CMS host that the named service exists.
|
||||||
|
|
||||||
|
`Serve` previously assigned the package-level guest runtime only after
|
||||||
|
`newGuest` returned. Because `newGuest` runs `Register` synchronously,
|
||||||
|
`HostServices()` returned an empty `CoreServices` value during registration.
|
||||||
|
Plugins that correctly nil-checked the bridge silently skipped registration.
|
||||||
|
The CMS could therefore mark a Wasm plugin loaded while consumers failed with
|
||||||
|
`bridge: no service registered` during a later load hook.
|
||||||
|
|
||||||
|
`newGuest` now makes the guest under construction current for the synchronous
|
||||||
|
registration pass and restores the previous runtime before returning. `Serve`
|
||||||
|
then installs the completed guest as before. This keeps construction isolated
|
||||||
|
for native tests while making the documented `HostServices()` escape hatch
|
||||||
|
truthful during `Register` on every pooled Wasm instance.
|
||||||
|
|
||||||
|
Alternatives rejected were moving bridge registration into `Load`, which runs
|
||||||
|
on only one pooled instance, and adding bridge names only to the manifest,
|
||||||
|
which would advertise host availability without installing the guest-local
|
||||||
|
`BridgeInvokable` value needed by `HOOK_BRIDGE_CALL`.
|
||||||
|
|
||||||
|
Consequences:
|
||||||
|
|
||||||
|
- Bridge services registered from `Register` exist on every pooled guest
|
||||||
|
instance and remain callable after CMS startup and hot swap.
|
||||||
|
- A regression test exercises the observable `Serve` plus
|
||||||
|
`HOOK_BRIDGE_CALL` contract instead of relying on initialization internals.
|
||||||
|
- No ABI or protobuf change is required; consumers need a plugin SDK release
|
||||||
|
containing the corrected Go guest runtime.
|
||||||
|
|
||||||
|
Keywords: wasmguest.Serve, newGuest, HostServices, PluginRegistration.Register,
|
||||||
|
plugin bridge, RegisterService, BridgeInvokable, HOOK_BRIDGE_CALL, wiki content
|
||||||
@ -166,12 +166,12 @@ func validHostname(h string) bool {
|
|||||||
if len(h) > 253 {
|
if len(h) > 253 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, label := range strings.Split(h, ".") {
|
for label := range strings.SplitSeq(h, ".") {
|
||||||
if label == "" || len(label) > 63 {
|
if label == "" || len(label) > 63 {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
for _, r := range label {
|
for _, r := range label {
|
||||||
if !(r >= 'a' && r <= 'z') && !(r >= '0' && r <= '9') && r != '-' {
|
if (r < 'a' || r > 'z') && (r < '0' || r > '9') && r != '-' {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -68,7 +68,7 @@ func checkRoutePath(r ModPublicRoute) string {
|
|||||||
if strings.HasSuffix(p, "/") {
|
if strings.HasSuffix(p, "/") {
|
||||||
return "no trailing slash; set prefix = true to claim the subtree"
|
return "no trailing slash; set prefix = true to claim the subtree"
|
||||||
}
|
}
|
||||||
for _, seg := range strings.Split(p[1:], "/") {
|
for seg := range strings.SplitSeq(p[1:], "/") {
|
||||||
switch {
|
switch {
|
||||||
case seg == "":
|
case seg == "":
|
||||||
return "empty path segment"
|
return "empty path segment"
|
||||||
|
|||||||
@ -100,7 +100,15 @@ func newGuest(reg plugin.PluginRegistration) *guest {
|
|||||||
// Pool whose calls fail cleanly, matching the capability stubs.
|
// Pool whose calls fail cleanly, matching the capability stubs.
|
||||||
g.services.Pool = bnwasm.NewPool(bnwasm.Transport(capTransport))
|
g.services.Pool = bnwasm.NewPool(bnwasm.Transport(capTransport))
|
||||||
g.rag, _ = g.services.RAGService.(*caps.RAGStub)
|
g.rag, _ = g.services.RAGService.(*caps.RAGStub)
|
||||||
|
// Register callbacks may use HostServices for capability-backed surfaces
|
||||||
|
// that are not passed as Register parameters, such as bridge services.
|
||||||
|
// Publish this partially initialized guest only for the synchronous
|
||||||
|
// registration pass, then restore the previous runtime. Serve installs g
|
||||||
|
// permanently after newGuest returns.
|
||||||
|
previous := current
|
||||||
|
current = g
|
||||||
g.registerErr = runRegister(reg, g.templates, g.blocks)
|
g.registerErr = runRegister(reg, g.templates, g.blocks)
|
||||||
|
current = previous
|
||||||
return g
|
return g
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -130,6 +130,43 @@ func TestServeInstallsRuntime(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestServeExposesHostServicesDuringRegister(t *testing.T) {
|
||||||
|
previous := current
|
||||||
|
t.Cleanup(func() { current = previous })
|
||||||
|
|
||||||
|
Serve(plugin.PluginRegistration{
|
||||||
|
Name: "bridge-provider",
|
||||||
|
Version: "0.1.0",
|
||||||
|
Register: func(_ templates.TemplateRegistry, _ blocks.BlockRegistry) error {
|
||||||
|
bridge := HostServices().Bridge
|
||||||
|
if bridge == nil {
|
||||||
|
return fmt.Errorf("HostServices bridge is unavailable during Register")
|
||||||
|
}
|
||||||
|
bridge.RegisterService("bridge-provider", "content", bridgeInvokableFunc(
|
||||||
|
func(_ context.Context, _ string, _ []byte) ([]byte, error) {
|
||||||
|
return []byte("registered"), nil
|
||||||
|
},
|
||||||
|
))
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
resp := invokeHook(t, current, abiv1.Hook_HOOK_BRIDGE_CALL, &abiv1.BridgeCallRequest{
|
||||||
|
ServiceName: "content",
|
||||||
|
Method: "apply",
|
||||||
|
})
|
||||||
|
if resp.GetError() != nil {
|
||||||
|
t.Fatalf("bridge call returned error: %v", resp.GetError())
|
||||||
|
}
|
||||||
|
bridgeResp := &abiv1.BridgeCallResponse{}
|
||||||
|
if err := proto.Unmarshal(resp.GetPayload(), bridgeResp); err != nil {
|
||||||
|
t.Fatalf("unmarshal BridgeCallResponse: %v", err)
|
||||||
|
}
|
||||||
|
if got := string(bridgeResp.GetPayload()); got != "registered" {
|
||||||
|
t.Fatalf("bridge payload = %q, want registered", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBridgeCallProvidesAuthenticatedCallerContext(t *testing.T) {
|
func TestBridgeCallProvidesAuthenticatedCallerContext(t *testing.T) {
|
||||||
g := newGuest(fixtureRegistration())
|
g := newGuest(fixtureRegistration())
|
||||||
g.services.Bridge.RegisterService("fixture", "content", bridgeInvokableFunc(
|
g.services.Bridge.RegisterService("fixture", "content", bridgeInvokableFunc(
|
||||||
|
|||||||
@ -74,8 +74,8 @@ func methodDocumentation(method protoreflect.MethodDescriptor) (description, doc
|
|||||||
}
|
}
|
||||||
paragraph = strings.TrimSpace(strings.ReplaceAll(paragraph, "\n", " "))
|
paragraph = strings.TrimSpace(strings.ReplaceAll(paragraph, "\n", " "))
|
||||||
methodName := string(method.Name())
|
methodName := string(method.Name())
|
||||||
if strings.HasPrefix(paragraph, methodName+" ") {
|
if after, ok := strings.CutPrefix(paragraph, methodName+" "); ok {
|
||||||
paragraph = strings.TrimSpace(strings.TrimPrefix(paragraph, methodName+" "))
|
paragraph = strings.TrimSpace(after)
|
||||||
if paragraph != "" {
|
if paragraph != "" {
|
||||||
runes := []rune(paragraph)
|
runes := []rune(paragraph)
|
||||||
runes[0] = unicode.ToUpper(runes[0])
|
runes[0] = unicode.ToUpper(runes[0])
|
||||||
|
|||||||
@ -65,30 +65,30 @@ func registerMCPFixture(t *testing.T) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
|
file, err := protodesc.NewFile(&descriptorpb.FileDescriptorProto{
|
||||||
Name: proto.String("test/mcpfixture.proto"),
|
Name: new("test/mcpfixture.proto"),
|
||||||
Package: proto.String("mcpfixture.v1"),
|
Package: new("mcpfixture.v1"),
|
||||||
Syntax: proto.String("proto3"),
|
Syntax: new("proto3"),
|
||||||
Dependency: []string{"google/protobuf/timestamp.proto"},
|
Dependency: []string{"google/protobuf/timestamp.proto"},
|
||||||
MessageType: []*descriptorpb.DescriptorProto{{
|
MessageType: []*descriptorpb.DescriptorProto{{
|
||||||
Name: proto.String("ListArticlesRequest"),
|
Name: new("ListArticlesRequest"),
|
||||||
Field: []*descriptorpb.FieldDescriptorProto{
|
Field: []*descriptorpb.FieldDescriptorProto{
|
||||||
{Name: proto.String("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
|
{Name: new("search"), Number: proto.Int32(1), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_STRING.Enum()},
|
||||||
{Name: proto.String("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
|
{Name: new("page"), Number: proto.Int32(2), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_INT32.Enum()},
|
||||||
{Name: proto.String("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: proto.String(".google.protobuf.Timestamp")},
|
{Name: new("published_after"), Number: proto.Int32(3), Label: descriptorpb.FieldDescriptorProto_LABEL_OPTIONAL.Enum(), Type: descriptorpb.FieldDescriptorProto_TYPE_MESSAGE.Enum(), TypeName: new(".google.protobuf.Timestamp")},
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
Name: proto.String("ListArticlesResponse"),
|
Name: new("ListArticlesResponse"),
|
||||||
}},
|
}},
|
||||||
Service: []*descriptorpb.ServiceDescriptorProto{{
|
Service: []*descriptorpb.ServiceDescriptorProto{{
|
||||||
Name: proto.String("WikiService"),
|
Name: new("WikiService"),
|
||||||
Method: []*descriptorpb.MethodDescriptorProto{{
|
Method: []*descriptorpb.MethodDescriptorProto{{
|
||||||
Name: proto.String("ListArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"),
|
Name: new("ListArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"),
|
||||||
}, {
|
}, {
|
||||||
Name: proto.String("StreamArticles"), InputType: proto.String(".mcpfixture.v1.ListArticlesRequest"), OutputType: proto.String(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: proto.Bool(true),
|
Name: new("StreamArticles"), InputType: new(".mcpfixture.v1.ListArticlesRequest"), OutputType: new(".mcpfixture.v1.ListArticlesResponse"), ServerStreaming: new(true),
|
||||||
}},
|
}},
|
||||||
}},
|
}},
|
||||||
SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{
|
SourceCodeInfo: &descriptorpb.SourceCodeInfo{Location: []*descriptorpb.SourceCodeInfo_Location{{
|
||||||
Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: proto.String("ListArticles lists Wiki articles.\n\nSupports pagination."),
|
Path: []int32{6, 0, 2, 0}, Span: []int32{1, 0, 1, 1}, LeadingComments: new("ListArticles lists Wiki articles.\n\nSupports pagination."),
|
||||||
}}},
|
}}},
|
||||||
}, protoregistry.GlobalFiles)
|
}, protoregistry.GlobalFiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user