Files
summercms/modules/pier/filter.go
Jakub Zych e7630e6762 fix(19.7): RA-WR-03 a guard error while re-filtering no longer narrows a session
catalog.allowedReport returns the allowed lists together with the first
provider error. Widening (stateful) and the stateless refresh leave the
cached server and its recorded fingerprint untouched on an error, so the
next request retries instead of the tools staying hidden until the next
rotation or event. A fan-out recompute still applies its fail-closed diff
but records no fingerprint, so the binding's next request re-filters.

TestWideningGuardError fails the guard during a widening on both paths and
during a permissions recompute, then asserts that the next request lists
every tool once the provider answers again.
2026-10-23 20:07:51 +02:00

92 lines
3.0 KiB
Go

package pier
import (
"context"
"fmt"
"git.golem15.com/golem15/summercms/modules/pact"
)
// allowed computes the tools and prompts an identity may see and call: a
// tool needs a subject under its guard, the read/write ceiling hides
// non-read-only tools and prompts declared with Writes from an identity
// without write (D-20), and the
// guard provider's own check decides the rest (D-16, D-23). A provider
// error hides the item and is logged. The same function builds the
// per-identity server and re-checks every list and call for the current
// request's identity (D-08, D-25).
func (c *catalog) allowed(ctx context.Context, id Identity) ([]*tool, []*prompt) {
tools, prompts, _ := c.allowedReport(ctx, id)
return tools, prompts
}
// allowedReport is allowed plus the first provider error, so a caller that
// re-filters a cached server can tell a refusal from a failed check
// (RA-WR-03): widening leaves the server as it is on an error and retries on
// the next request, and a fan-out recompute marks the server for that retry.
// Every failed check is still logged and hides its item.
func (c *catalog) allowedReport(ctx context.Context, id Identity) ([]*tool, []*prompt, error) {
var first error
var tools []*tool
for _, t := range c.tools {
ok, err := c.allowsTool(ctx, id, t)
if err != nil {
logger().Warn("pier: guard check failed", "tool", t.name, "guard", t.guardName, "error", err)
if first == nil {
first = fmt.Errorf("tool %s: %w", t.name, err)
}
continue
}
if ok {
tools = append(tools, t)
}
}
var prompts []*prompt
for _, p := range c.prompts {
ok, err := c.allowsPrompt(ctx, id, p)
if err != nil {
logger().Warn("pier: guard check failed", "prompt", p.name, "guard", p.guardName, "error", err)
if first == nil {
first = fmt.Errorf("prompt %s: %w", p.name, err)
}
continue
}
if ok {
prompts = append(prompts, p)
}
}
return tools, prompts, first
}
func (c *catalog) allowsTool(ctx context.Context, id Identity, t *tool) (bool, error) {
subject := id.subjectFor(t.guardName)
if subject == nil {
return false, nil
}
if !t.readOnly && !id.writes() {
return false, nil
}
return allows(ctx, t.guard, *subject, t.req, t.decl.Permissions, t.method)
}
func (c *catalog) allowsPrompt(ctx context.Context, id Identity, p *prompt) (bool, error) {
// A prompt that directs write tools sits under the read/write ceiling
// like a tool that is not read-only (P-28).
if p.decl.Writes && !id.writes() {
return false, nil
}
subject := id.subjectFor(p.guardName)
if subject == nil {
return false, nil
}
return allows(ctx, p.guard, *subject, pact.MCPRequirements{}, p.decl.Permissions, "")
}
// allows asks the provider, method-aware when it supports it.
func allows(ctx context.Context, g pact.MCPGuard, subject pact.MCPSubject, req pact.MCPRequirements, permissions []string, method string) (bool, error) {
if ma, ok := g.(methodAllower); ok && method != "" {
return ma.MCPAllowsMethod(ctx, subject, req, permissions, method)
}
return g.MCPAllows(ctx, subject, req, permissions)
}