Parent
#1633 — DIAL Folder As Resource (EPIC)
What to build
The foundational tracer bullet for folder-as-resource: a user can upload an agent skill as a folder and download it back as a ZIP, end-to-end.
- Introduce a new, isolated
SKILL resource type with URL group skills, stored under its own blob prefix. It must be invisible to the v1 files API — only the new v2 handler may read or write it.
- Wire a new
v2 route family: PUT /v2/skills/{bucket}/{path} and GET /v2/skills/{bucket}/{path} (whole resource, trailing slash). Use a reluctant path component. Reserve the path segments files and v/ as structural tokens (neither a folder nor a resource may be named files).
- Introduce a generic engine (
FolderResourceController + FolderResourceService) and the .dial-resource marker as a small self-describing JSON document holding type, schemaVersion, state, currentVersion, etag (aggregate), timestamps, author, and type-specific metadata.
- Adopt the versioned-prefix layout: file content lives under
v/{versionId}/...; the marker is a pointer to the current immutable version.
- Introduce a per-type handler keyed by group. The first handler is
SkillHandler: validate(files) requires SKILL.md at root with parseable YAML frontmatter (name + description required); buildMarkerMetadata(files) extracts name/description/version into the marker.
- Whole-resource PUT: accept
multipart/form-data, one part per file, each part's filename = relative path inside the resource. Write all parts under a fresh v/{versionId}/ prefix, run the handler, compute the aggregate etag, then commit with a single putResource of .dial-resource (guarded by If-Match). The marker is synthesized server-side (type derived from the URL group) so clients can never corrupt it.
- Whole-resource GET: stream an
application/zip produced on a worker thread (never buffer the whole archive); the .dial-resource marker is stripped from the archive.
Scope: root-level skills only (grouping folders, structural-invariant walks, single-file ops, delete, listing come in later slices). Reuse ResourceService, BlobStorage, EtagHeader/If-Match, LockService, and ResourceEvent propagation.
Acceptance criteria
Blocked by
None - can start immediately.
Parent
#1633 — DIAL Folder As Resource (EPIC)
What to build
The foundational tracer bullet for folder-as-resource: a user can upload an agent skill as a folder and download it back as a ZIP, end-to-end.
SKILLresource type with URL groupskills, stored under its own blob prefix. It must be invisible to the v1filesAPI — only the new v2 handler may read or write it.v2route family:PUT /v2/skills/{bucket}/{path}andGET /v2/skills/{bucket}/{path}(whole resource, trailing slash). Use a reluctant path component. Reserve the path segmentsfilesandv/as structural tokens (neither a folder nor a resource may be namedfiles).FolderResourceController+FolderResourceService) and the.dial-resourcemarker as a small self-describing JSON document holdingtype,schemaVersion,state,currentVersion,etag(aggregate), timestamps, author, and type-specificmetadata.v/{versionId}/...; the marker is a pointer to the current immutable version.SkillHandler:validate(files)requiresSKILL.mdat root with parseable YAML frontmatter (name+descriptionrequired);buildMarkerMetadata(files)extractsname/description/versioninto the marker.multipart/form-data, one part per file, each part's filename = relative path inside the resource. Write all parts under a freshv/{versionId}/prefix, run the handler, compute the aggregate etag, then commit with a singleputResourceof.dial-resource(guarded byIf-Match). The marker is synthesized server-side (type derived from the URL group) so clients can never corrupt it.application/zipproduced on a worker thread (never buffer the whole archive); the.dial-resourcemarker is stripped from the archive.Scope: root-level skills only (grouping folders, structural-invariant walks, single-file ops, delete, listing come in later slices). Reuse
ResourceService,BlobStorage,EtagHeader/If-Match,LockService, andResourceEventpropagation.Acceptance criteria
SKILLresource type exists with groupskillsand an isolated blob prefix; v1filesroutes cannot read or write it.PUT /v2/skills/{bucket}/{path}/with a valid multipart body creates the skill: parts written underv/{versionId}/,.dial-resourcemarker committed in a single write, response returns200with the aggregateETag.PUTof a skill missingSKILL.md, or with unparseable/incomplete frontmatter (name/description), is rejected with400and writes nothing observable.GET /v2/skills/{bucket}/{path}/streams a ZIP of the current version's files with the marker stripped; round-tripping PUT→GET reproduces the uploaded file set.If-Matchon the whole-resource PUT is honored against the marker's aggregate etag.Blocked by
None - can start immediately.