Files
executus/tools/file_descendant_grant.go
T
steve ac961e1539
executus CI / test (pull_request) Successful in 58s
Adversarial Review (Gadfly) / review (pull_request) Successful in 10m10s
P3: store group — kv_* + file_* tools (agent memory)
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>
2026-06-26 22:06:46 -04:00

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
}