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.
92 lines
3.0 KiB
Go
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)
|
|
}
|