Skip to content

core.file

← Module catalog

soul-sidesoulstack.filesfilesroot required

Manage files and directories: content/permissions/owner, template rendering, directory creation, deletion.

core.file brings a file or directory to the desired shape with four states: present (content inline via content or copied from the host via src), rendered (renders a text/template template), directory (a directory with attributes), and absent. Idempotent by content (SHA-256 comparison) and by the mode/owner/group attributes — if everything matches, changed=false. Replaces the separate core.copy and core.template (ADR-015). Soul-side: writing outside /var/lib/soul-stack/ requires fs_write_root, and system paths in practice require root.

Requirements

  • Rootrequired
  • Sidesoul-side
  • Collectionsoulstack.files
  • Categoryfiles
Capabilitiesfs_write_root

States

core.file.present — The file exists with the given content (inline content or an src copy) plus mode/owner/group.

Changes when

The file did not exist, or the content (compared by SHA-256; for src — sha256 of the src bytes), mode, or owner/group differs.

Stays unchanged when

The content, mode, and owner/group already match.

Parameters

ParamTypeRequired / defaultDescription
pathstringrequiredTarget file path.
contentstringoptionalFile content (inline). Mutually exclusive with src; neither — an empty file.
srcstringoptionalAbsolute path to a regular file on the host; its content is copied into path (typically the result of core.archive.extracted). Sets the content only, not the source's attributes. Mutually exclusive with content.
modestringoptionalPermissions in octal form, e.g. "0640".
ownerstringoptionalOwner (user name).
groupstringoptionalOwning group (group name).

Example — present with inline content (the core.copy role)

- name: Drop a static marker file
module: core.file.present
params:
path: /etc/soul-stack/marker
content: "managed by soul-stack"
mode: "0644"
owner: root
group: root

Output

FieldTypeDescription
pathstring
sha256string
modestring
installedtrue

Reference

Template context root (rendered)

What text/template sees when core.file.rendered renders. A missing referenced variable is a render error (strict-mode missingkey=error), not a silent empty substitution.

KeyWhat it is
.vars.*derived variables from vars: (computed by the CEL phase)
.input.*the resolved operator-input of the pass (Variant B — directly, without passthrough via vars:; Keeper sets the key only if the template actually references .input.*)
.self.*a projection of soulprint, snake_case: .self.os.pkg_mgr, .self.network.primary_ip
.rolethe host's declared role
.essence.*effective essence

Notes

  • present sets content with exactly one of content (inline) or src (a copy of a regular file from the host); both together are an error (a conflict on key presence, even content: "" together with src:). Neither one — an empty file (legacy behavior).
  • src sets only the content (typically the result of core.archive.extracted); mode/owner/group are taken from explicit params, the attributes of src are not inherited. The src path must be absolute, a symlink is rejected via Lstat (the source is not followed via the symlink).
  • rendered: on the wire Keeper must deliver both template_content and render_context — without either one the step fails (a golden-path prod invariant, not optional).
  • The .self.* keys are snake_case (.self.os.pkg_mgr, .self.os.init_system), like soulprint.self.*; camelCase will not work. register on a rendered task typically serves as an onchanges: anchor to restart a service when the config changes.
  • present/absent/rendered/directory spawn no subprocesses — rendering, the write, and mkdir/chmod/chown happen in-process, without a shell. recurse (recursive permissions on directory contents) is unsupported in the MVP.

Security & defaults

  • A direct write to an arbitrary FS path, including system ones (/etc/...), is the main risk: path/content/mode/owner/group must come from the Destiny author, not from untrusted input (otherwise a swap of /etc/passwd, /etc/sudoers, or a unit file).
  • Atomicity differs: rendered and the src branch of present write via temp + rename (an observer does not see a partially written config), while the content branch of present goes through os.WriteFile directly, without temp+rename: on a failure mid-write the file may be left truncated. For configs under a running daemon prefer rendered/src or restart the consumer via onchanges:.
  • src is a copy of a regular file from the host itself (a symlink/directory/device/relative path is rejected), but the source does not become trusted: src, like path, is set by the Destiny author, otherwise the contents of an arbitrary host file end up copied into path.
  • Set mode/owner explicitly for secrets (0600/0640): without mode the os.WriteFile default is used (depends on umask), the existing mode is not compared and not fixed — a secret may end up world-readable.
  • rendered renders in a sandbox (sprig-allowlist, no access to FS/network/environment — three sandbox barriers), but secrets (${ vault(...) }, passwords via vars:) land in render_context and in the resulting file — hence the requirement of an explicit mode for configs with secrets.
  • Privileges: the manifest declares fs_write_root (writing outside /var/lib/soul-stack/), but not run_as_root — the module runs with the soul agent process's privileges without elevating them internally; writing to system paths in practice requires root.

See also