Merge branch 'fix/g08' into fix/integrated
# Conflicts: # internal/store/migrations.go
This commit is contained in:
@@ -472,3 +472,30 @@ func TestRequirement_Speaker(t *testing.T) {
|
||||
t.Errorf("voice listing speakers = %v; want allowed", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Describing an image is a read. Saving the description is a write of recall
|
||||
// corpus under a source no enrollment owns, so it is held to the same
|
||||
// source-scope rule WriteFact is. Before this, any AuthRead caller could put a
|
||||
// small VLM's guess into what Maven knows.
|
||||
func TestCan_DescribeImage_SaveNoteNeedsScope(t *testing.T) {
|
||||
poller := Scope{Surface: SurfaceTelegram, Module: "poll", SourceScope: []string{"poll:healthcheck"}}
|
||||
web := Scope{Surface: SurfaceAuthedPage, Module: "web", SourceScope: []string{"*"}}
|
||||
|
||||
plain, err := json.Marshal(ipc.DescribeImageReq{Data: []byte("x")})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
noting, err := json.Marshal(ipc.DescribeImageReq{Data: []byte("x"), SaveNote: true})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := Can(ipc.MethodDescribeImage, poller, plain); err != nil {
|
||||
t.Errorf("describing without saving must stay a read: %v", err)
|
||||
}
|
||||
if err := Can(ipc.MethodDescribeImage, poller, noting); !errors.Is(err, ErrForbidden) {
|
||||
t.Errorf("save_note out of scope = %v, want ErrForbidden", err)
|
||||
}
|
||||
if err := Can(ipc.MethodDescribeImage, web, noting); err != nil {
|
||||
t.Errorf("a module scoped to everything must still be allowed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,6 +187,18 @@ func Can(m ipc.Method, scope Scope, params json.RawMessage) error {
|
||||
|
||||
switch Requirement(m) {
|
||||
case AuthRead:
|
||||
// Describing an image is a read. Saving the description as a note is
|
||||
// not: writeNote embeds it, so it comes back in a later turn as
|
||||
// something Maven knows, under the source media:image:<id>, which no
|
||||
// enrollment owns. The rung's own argument was that the method "cannot
|
||||
// write a fact, set a reminder, or touch the tool allowlist" — it can
|
||||
// write recall corpus, and that is what AuthWrite exists to scope. So
|
||||
// the note half is held to the same source-scope rule WriteFact is.
|
||||
if m == ipc.MethodDescribeImage && wantsNote(params) {
|
||||
if !SourceAllowed(scope.SourceScope, ImageNoteSource) {
|
||||
return fmt.Errorf("%w: source %q out of scope", ErrForbidden, ImageNoteSource)
|
||||
}
|
||||
}
|
||||
// Any enrolled module may read. Reads through the surface level the
|
||||
// Enrollment set (voice-L0 wouldn't be enrolled to write at all).
|
||||
return nil
|
||||
@@ -223,6 +235,26 @@ func Can(m ipc.Method, scope Scope, params json.RawMessage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ImageNoteSource is the source scope a caller needs to turn a described image
|
||||
// into a note. The note itself is stored under "media:image:<id-prefix>"; the
|
||||
// scope is checked against this stem, because the id is not known until the
|
||||
// bytes arrive and no enrollment could name it in advance.
|
||||
const ImageNoteSource = "media:image"
|
||||
|
||||
// wantsNote reports whether a DescribeImage call asked for the description to
|
||||
// be remembered. Malformed params read as no: dispatch rejects them a moment
|
||||
// later with a better error.
|
||||
func wantsNote(raw json.RawMessage) bool {
|
||||
if len(raw) == 0 {
|
||||
return false
|
||||
}
|
||||
var p ipc.DescribeImageReq
|
||||
if json.Unmarshal(raw, &p) != nil {
|
||||
return false
|
||||
}
|
||||
return p.SaveNote
|
||||
}
|
||||
|
||||
// SourceAllowed — true iff src is in scope (the wildcard "*" matches all).
|
||||
// Empty scope ⇒ fail closed. The function is pure; we keep it exported so a
|
||||
// future enrollment table can call into the same matching logic.
|
||||
|
||||
Reference in New Issue
Block a user