Ctrl K
ComunidadGuías…

specs/051-artifact.md

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# 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
```