specs/051-artifact.md
Rama main · réplica de lectura del repositorio original.
# Spec: Artifact Actions
## Status
Partially implemented.
Conformance covers one local write/read/list round trip, path boundaries,
and representative validation errors. Executor tests own option permutations,
metadata details, overwrite behavior, and filesystem edge cases.
This spec defines conformance behavior for the built-in `artifact.write`,
`artifact.read`, and `artifact.list` actions.
## Scope
This spec defines `artifact.write`, `artifact.read`, and `artifact.list`,
which write, read, and list files in a DAG-run's own artifact directory.
This spec covers:
- that using any `artifact.*` action auto-enables artifacts for the DAG,
and that explicitly setting `artifacts.enabled: false` while also using
one is a conflict
- `with.path`, relative to the artifact directory, and that it must stay
within it (no absolute paths, no `~`, no `..` segments)
- `artifact.write`'s required `with.content`, `with.mode`, and
`with.overwrite` (default `false`: writing to an existing path fails)
with `with.atomic` (default `true`; `overwrite: true` requires it)
- `artifact.read`'s required `with.path`, `with.format` (`raw`, the
default, streaming the file's exact bytes; or `json`, wrapping the
content with metadata), and `with.max_bytes`
- `artifact.list`'s `with.pattern` (a glob), `with.recursive` (default
`false`: only direct children), and `with.include_dirs` (default
`false`: files only)
- that every configuration error this spec documents is rejected at
DAG-build-time validation, not only at runtime
- validation and runtime errors
This spec does not define:
- `artifacts.dir`, the DAG-level option to relocate the artifact
directory (this spec only covers its default location)
- symlink-based escape attempts, beyond the same `..`/absolute-path
protections already covered
- direct `type: artifact` authoring
## Goal
Workflow authors persist and retrieve files scoped to a single DAG-run,
without shelling out to a command executor for file management, and
without accidentally writing outside that run's own storage.
## Behavior
### Enabling artifacts
Using any `artifact.write`/`artifact.read`/`artifact.list` step
auto-enables artifact storage for the DAG; no separate configuration is
needed. Setting `artifacts.enabled: false` while a step also uses an
artifact action is rejected (see "Errors").
### Path safety
`with.path` is always relative to the DAG-run's own artifact directory.
An absolute path, a path starting with `~`, or one containing a `..`
segment is rejected, regardless of which action uses it.
### Write
`artifact.write` requires `with.content` (the file's new content) and
`with.path`. `with.mode` sets the file's permission bits (an octal
string such as `"0640"`); it defaults to `0600`. The process umask may
restrict permissions when creating a file. Without
`with.overwrite`, writing to a path that already exists fails; with
`with.overwrite: true`, the existing file is replaced. `with.atomic`
(default `true`) governs how a replacement is written; `with.overwrite:
true` requires it and cannot be combined with `with.atomic: false`.
### Read
`artifact.read` requires `with.path`. `with.format: raw` (the default)
copies the file's exact bytes to stdout. `with.format: json` instead
writes one JSON object with the file's path, type, size, mode,
modification time, and its content as a string. `with.max_bytes`, when
greater than zero, rejects a file larger than that many bytes rather
than reading it. Reading a path that is a directory fails.
### List
`artifact.list` writes one JSON object naming the count of files found
and an array of entries (each with path, type, size, mode, and
modification time), sorted by path. `with.path` (default: the artifact
directory's root) names the directory to list. `with.recursive: true`
descends into subdirectories; otherwise only direct children are listed.
`with.include_dirs: true` includes directory entries in the result;
otherwise only regular files are listed. `with.pattern`, when set, is a
glob matched against each candidate's slash-separated relative path.
### JSON output
The following members define action stdout. Extra members are allowed.
| Action | Members |
| --- | --- |
| `artifact.write` | `operation`: `"write"`; `path`: relative path string; `created`: boolean; `bytes`: integer byte count, omitted when zero. |
| `artifact.read` with `format: json` | `operation`: `"read"`; `path`: relative path string; `exists`: `true`; `type`: file type string; `mode`: permission string; `modTime`: RFC 3339 timestamp, optionally with fractional seconds; `size` and `bytes`: integer byte counts, omitted when zero; `content`: string, omitted when empty. |
| `artifact.list` | `operation`: `"list"`; `path`: relative directory string (`"."` for the root); `files`: integer regular-file count, omitted when zero; `entries`: array, omitted when empty. |
Each list entry has `path`, `type`, and `mode` strings; integer `size`;
`modTime` in the timestamp format above; and boolean `isDir`, `isRegular`,
and `isSymlink`. Paths use `/` separators. The smoke test checks action
identity, content, and listed paths; detailed metadata coverage belongs to
executor tests.
## Errors
### Validation
Every one of these is rejected at DAG-build-time validation (`dagu
validate`), not only when the step runs:
- `artifact.write` without `with.content`.
- `artifact.read` without `with.path`.
- `with.overwrite: true` combined with `with.atomic: false`.
- `artifacts.enabled: false` set while a step uses an artifact action.
### Runtime
- `with.path` is absolute, starts with `~`, or contains a `..` segment:
an error containing `"artifact path must be relative"` or `"artifact
path must not contain .."`.
- `artifact.write` to an existing path without `with.overwrite`: an
error containing `"file exists"`.
- `artifact.read` on a file larger than `with.max_bytes`: an error
containing `"exceeds max_bytes"`.
- `artifact.read` on a directory: an error containing `"cannot read a
directory"`.
## Related Specs
- Run context: [Spec 017: Built-In Run Context](017-built-in-run-context.md)
## Examples
Write a file, then read it back as JSON metadata plus content:
```yaml
steps:
- id: save
action: artifact.write
with:
path: report.json
content: '{"status":"ok"}'
- id: inspect
depends: save
action: artifact.read
with:
path: report.json
format: json
```
List every `.log` file under a subdirectory:
```yaml
steps:
- action: artifact.list
with:
path: logs
pattern: "**/*.log"
recursive: true
```