Skip to content

feat(subagents): artifact_read + workspace staging, artifact counts, docs (M2+M3) - #160

Merged
jkyberneees merged 3 commits into
mainfrom
feat/artifact-read
Aug 30, 2026
Merged

feat(subagents): artifact_read + workspace staging, artifact counts, docs (M2+M3)#160
jkyberneees merged 3 commits into
mainfrom
feat/artifact-read

Conversation

@jkyberneees

@jkyberneees jkyberneees commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Part of the SUBAGENT_RESULT_ARTIFACTS_PLAN.md workstream. M1 shipped in #159 (v1.32.0); this PR completes the plan: M2 content access + M3 staging/relocation, events, docs.

M2 — artifact_read (parent-only content access)

Artifacts larger than the 32 KiB inline budget were metadata-only after M1. Now the parent reads full content on demand, by id:

artifact report (text/markdown, 18432 bytes, sha256:ab12cd34ef56) — bytes 0..65536 — TRUNCATED, call again with offset to continue
  • Session-scoped registry: delegate_tasks registers every ref that passed fail-closed validation at collation, with the validated path. 512 live entries, oldest-first eviction with lazy slots (a last-wins re-registration can never evict a live entry).
  • No model input reaches the filesystem as a path — the model supplies only the id; resolution is internal. Traversal-shaped ids are simply unknown.
  • Re-verified at read time (janitor/cleanup may have removed the subtree) · default 64 KiB per call, 256 KiB hard cap, offset paging with explicit TRUNCATED flag · content inside wrapUntrusted, recorded by the per-call audit ingest.
  • Parent-only: gated on SelfTrust == "" in builtinTools.
  • Duplicate ids across tasks: last-wins + explicit note line.

M3 — staging + relocation, events, docs

The gap M1 left: children could not actually deliver artifacts — ~/.odek/artifacts is doubly protected from child writes (confineToCWD rejects absolute paths; the classifier escalates ~/.odek writes to system_write, denied for approval-less children). M3 resolves it without touching either gate:

  • Children stage deliverables inside the workspace (.odek-artifacts/<task_id>/ — an ordinary local_write), pointed there by a trusted runner note (workspace-relative path only).
  • The trusted child runner relocates them to the canonical dir before the exit scan (rename, cross-device copy fallback), then hashes/sizes there. Wire format unchanged (artifact_root still names the canonical dir — v1.32.0 compatible).
  • subagent_completed events carry artifact_count (count only — hash-only event policy preserved).
  • Docs in the same change: CONFIG.md + MAINTENANCE.md (maintenance.artifacts_max_age_hours, ODEK_MAINTENANCE_ARTIFACTS_MAX_AGE_HOURS, explicit 0 = keep forever), SECURITY.md (artifact invariants), SUBAGENTS.md (Result artifacts section), EXTENSIONS.md (sub-agent reuse of odek.artifact-ref/v1).

Also carries the CI fixes from review: maintenance.artifacts_max_age_hours properly wired (config field + overlay + env + want-literals) and runTask's empty-result error now carries waitErr/scannerErr for diagnosability.

Verification

  • RED first: artifact_read_test.go (9 tests: registry mechanics incl. eviction/last-wins, no-path-leak, untrusted wrap, paging/clamping, unknown-id listing, traversal id, vanished file, parent-only gate, duplicate notes) and subagent_staging_test.go (staging path, relocation + staging removal, copy fallback, no-op, event count, no canonical-path leak).
  • Full cmd/odek suite: ok (darwin 73s · linux container 62s) · -race scoped: ok · golangci-lint: 0 issues

Plan status

With this, the SUBAGENT_RESULT_ARTIFACTS_PLAN.md milestones are complete (M0/M1 in #159+v1.32.0, M2/M3 here).

Sub-agent result artifacts larger than the 32 KiB inline budget were
metadata-only after M1; artifact_read completes the channel: the parent
reads full content on demand, by id.

- session-scoped registry: delegate_tasks registers every ref that
  passed fail-closed validation at collation, with the validated path
  (512 live entries, oldest-first eviction, lazy slots so last-wins
  re-registration never evicts a live entry)
- artifact_read tool (parent-only, gated on SelfTrust=="" via
  builtinTools): resolves ids internally — the model supplies only the
  id, never a path — re-verifies the file at read time (janitor/session
  cleanup may have removed it), returns bytes [offset, offset+limit)
  (default 64 KiB, hard cap 256 KiB) inside the untrusted boundary with
  full metadata header; unknown ids list the registered ids; traversal-
  shaped ids are simply unknown
- duplicate ids across tasks: last-wins with an explicit note line in
  the collated summary

RED first: artifact_read_test.go — registry
register/lookup/evict/last-wins, happy path with no-path-leak +
untrusted wrap, offset/limit slice + truncation flag + cap clamp,
unknown-id listing, traversal id, vanished file, parent-only gate,
duplicate-registration note.
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Aug 30, 2026

Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
odek cc5b3c3 Commit Preview URL

Branch Preview URL
Aug 30 2026, 03:46 PM

…s (M3)

M1 shipped the artifact channel but children could not actually deliver:
the canonical dir (~/.odek/artifacts) is doubly protected from child
writes — confineToCWD rejects absolute paths and the danger classifier
escalates ~/.odek writes to system_write (denied for approval-less
children). M3 resolves this without touching either gate:

- children stage deliverables INSIDE the workspace
  (.odek-artifacts/<task_id>/ — an ordinary local_write); the trusted
  child runner relocates them to the canonical dir before the exit scan
  (rename with cross-device copy fallback), then hashes/sizes there
- wire format unchanged: artifact_root still names the canonical dir
  (v1.32.0 compatible); the child request carries the workspace-relative
  staging path via childArtifactNote
- subagent_completed events carry artifact_count (count only — hash-only
  event policy preserved)
- docs: CONFIG.md + MAINTENANCE.md (artifacts_max_age_hours knob + env),
  SECURITY.md (artifact invariants paragraph), SUBAGENTS.md (Result
  artifacts section), EXTENSIONS.md (sub-agent artifact reuse of
  odek.artifact-ref/v1)

RED first: subagent_staging_test.go — staging path, relocation
(move + staging removal), copy fallback via rename-failure hook,
missing-staging no-op, event artifact_count presence/absence, note
leaks no canonical path.
@jkyberneees jkyberneees changed the title feat(subagents): artifact_read — validated on-demand content access (M2) feat(subagents): artifact_read + workspace staging, artifact counts, docs (M2+M3) Aug 30, 2026
@jkyberneees
jkyberneees merged commit 434144d into main Aug 30, 2026
10 checks passed
@jkyberneees
jkyberneees deleted the feat/artifact-read branch August 30, 2026 15:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant