// 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 }