fix(addons): deny a stale generation before the credential check

A read of an add-on that declares credentials looked up its capabilities
before it compared the generation. A stale generation made that lookup
fail, and the failure was reported as an outage of the owner (503)
instead of a denial (403). Classify the lookup error like the tool check
does.

Assisted-by: Claude:claude-sonnet-5-5 go-test
This commit is contained in:
Ettore Di Giacinto committed 2026-11-04 00:09:39 +00:00
1 parent 48bc71c38a
commit e351118f85
2 files changed
+12 -2

No files matched your search

+3 -2
View File
@@ -114,8 +114,9 @@ func (g *Gateway) credentialize(ctx context.Context, a contract.Actor, r *contra
caps, err := src.DeclaredCapabilities(ctx, r.AddonID, r.Generation)
if err != nil {
// Not knowing whether the add-on declares credentials is not a reason
// to run its tool without the check.
return ErrUnavailable
// to run its tool without the check. A stale generation is a denial,
// not an outage of the owner.
return toolSourceError(err)
}
if len(caps.Credentials) == 0 {
return nil
@@ -136,6 +136,15 @@ var _ = Describe("a read of an add-on that declares credentials", Label("tools")
Expect(err).To(MatchError(ErrUnavailable))
})
It("denies a stale generation as a denial, never as an outage of the owner", func() {
g.Resolver = resolved{}
_, err := g.Dispatch(ctx, contract.Actor{UserID: "alice", CredentialID: "c", Mode: "named-user"},
contract.Request{AddonID: "sample", ResourceID: "r-0123456789", Generation: 7, OperationID: "op", Action: "read", Arguments: json.RawMessage(`{"q":"plain"}`)})
Expect(err).To(MatchError(ErrForbidden))
Expect(err).NotTo(MatchError(ErrUnavailable))
Expect(client.reads.Load()).To(Equal(int32(0)))
})
It("does not touch an add-on that declares no credentials", func() {
plain := capSet{toolSet: toolSet{gen: 1, tools: map[string][]string{"sample": {"whoami"}}}, defs: defs[1:]}
g.Tools = plain