78e6858751
RegisterStore(reg, StoreDeps) registers the persistent-memory tools over the host's KV and/or File backends: - kv_get/set/list/delete (KVStorage seam) - file_save/get/get_text/get_metadata/list/delete (FileStorage seam), plus file_search (FileSearcher) and create_file_url (FileTokenMinter) when wired. Near-zero-config: Quota defaults to a generous static cap (staticQuota), the per-value/per-file caps default, and the kv vs file groups register independently (a host can take just one). Seams moved clean (interface-only): kv_storage.go, quota_provider.go, file_descendant_grant.go. The default in-memory KV/File backends come with contrib/store at P4. Core go.sum still free of gorm/redis/discordgo/sqlite. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
59 lines
2.7 KiB
Go
59 lines
2.7 KiB
Go
// file_descendant_grant.go — the cross-skill file-access escape hatch
|
|
// for parent → spawned-worker handoff.
|
|
//
|
|
// The blanket rule everywhere in this package is "a file belongs to
|
|
// the skill that saved it; cross-skill refs are rejected". That rule
|
|
// breaks the agent_spawn flow: a worker saves a chart with file_save
|
|
// under ITS ephemeral ID, returns the file_id as text, and the parent
|
|
// (which orchestrated the whole thing) can't attach, read, or host it.
|
|
// Observed live on the second spawn test — the chart never reached
|
|
// Discord; general could only apologise with the file_id.
|
|
//
|
|
// The grant: a caller may access a file whose owning skill/agent
|
|
// PRODUCED A RUN THAT DESCENDS FROM THE CALLER'S CURRENT RUN. In other
|
|
// words: you may touch the artifacts of workers you (transitively)
|
|
// dispatched in this very tree — output you were already entitled to
|
|
// see as their tool results. You may NOT touch files from siblings,
|
|
// ancestors, other trees, or unrelated skills; those still reject.
|
|
//
|
|
// Why an optional interface upgrade (vs a new constructor dep on
|
|
// every file tool): six tools enforce the ownership rule, each with
|
|
// its own narrow storage interface — threading a new dep through all
|
|
// of them churns every signature and test fake. Instead, the
|
|
// production storage adapter (mort.go's skillsFileStorageAdapter,
|
|
// which backs ALL of those interfaces) additionally implements
|
|
// DescendantRunChecker; the tools type-assert at the rejection site.
|
|
// Fakes that don't implement it keep the strict behaviour — the grant
|
|
// is fail-closed everywhere. Same pattern as KVHistoryRecorder (v7).
|
|
package tools
|
|
|
|
import (
|
|
"context"
|
|
|
|
"gitea.stevedudenhoeffer.com/steve/executus/tool"
|
|
)
|
|
|
|
// DescendantRunChecker reports whether ownerSkillID (the file's owning
|
|
// skill or agent ID — e.g. a spawned worker's "eph-…" ID) produced a
|
|
// run that is a DESCENDANT of callerRunID. Production walks the audit
|
|
// parent_run_id chain; see mort_skills_storage_adapters.go.
|
|
type DescendantRunChecker interface {
|
|
IsDescendantProducer(ctx context.Context, ownerSkillID, callerRunID string) (bool, error)
|
|
}
|
|
|
|
// descendantFileGrant is called at a cross-skill rejection site with
|
|
// the tool's storage dep. Returns true only when the dep implements
|
|
// DescendantRunChecker AND the owner's run descends from the caller's
|
|
// run. Any error or missing context keeps the strict rejection.
|
|
func descendantFileGrant(ctx context.Context, dep any, inv tool.Invocation, ownerSkillID string) bool {
|
|
if ownerSkillID == "" || inv.RunID == "" {
|
|
return false
|
|
}
|
|
checker, ok := dep.(DescendantRunChecker)
|
|
if !ok || checker == nil {
|
|
return false
|
|
}
|
|
granted, err := checker.IsDescendantProducer(ctx, ownerSkillID, inv.RunID)
|
|
return err == nil && granted
|
|
}
|