Skip to content

Artifacts

ollikurki edited this page May 20, 2026 · 1 revision

Artifacts

Artifacts are the core abstraction in HomePilot. Every change the AI proposes to your infrastructure is captured as an artifact — a structured Markdown file with YAML frontmatter that describes what to do, what target to do it on, and how to verify it worked.

Philosophy

The AI never mutates infrastructure directly. It drafts a fully-specified plan (an artifact); you review it, approve it, and then HomePilot executes it. This gives you:

  • Review before execution — you see exactly what will happen
  • Audit trail — every transition is recorded
  • Rollback — Proxmox snapshots and rollback specs let you undo changes
  • Survivability — artifacts are plain Markdown; readable without HomePilot

File Format

Location

~/.hp/artifacts/YYYY/MM/YYYY-MM-DD-<slug>[-<hash6>].md

Examples:

  • 2026-05-06-deploy-jellyfin-lxc.md
  • 2026-05-06-deploy-jellyfin-lxc-a3f201.md (collision)
  • 2026-05-06-pve-authentik-oidc.md

Structure

Every artifact is a Markdown file with YAML frontmatter:

---
id: 2026-05-06-deploy-jellyfin-lxc
kind: proxmox-api-sequence
intent: "Deploy Jellyfin LXC on Proxmox"
status: proposed
mutating: true
target:
  kind: lxc
  vmid: 142
  node: pve1
  host: jellyfin-lxc
idempotence: via-precheck
produced_by:
  session: chat-2026-05-06-abc
  agent: claude-opus
  user: admin
  at: "2026-05-06T14:32:00Z"
hash: sha256:a3f201...
---

# Deploy Jellyfin LXC on Proxmox

## Plan
Create a new LXC container running Debian 13, install Jellyfin,
configure hardware transcoding, and start the service.

## Spec

```yaml proxmox-api-spec
steps:
  - id: create-lxc
    method: POST
    path: /nodes/{{ target.node }}/lxc
    body:
      vmid: "{{ target.vmid }}"
      ostemplate: "local:vztmpl/debian-13-standard_13.0-1_amd64.tar.zst"
      hostname: "{{ target.host }}"
      memory: 4096
      cores: 4
      net0: "name=eth0,bridge=vmbr1,ip=dhcp"
    precheck:
      method: GET
      path: /nodes/{{ target.node }}/lxc/{{ target.vmid }}/status/current
      skip_if: "response.status_code == 200"
    on_error: halt

  - id: start-lxc
    method: POST
    path: /nodes/{{ target.node }}/lxc/{{ target.vmid }}/status/start
    precheck:
      method: GET
      path: /nodes/{{ target.node }}/lxc/{{ target.vmid }}/status/current
      skip_if: "response.json['data']['status'] == 'running'"
    on_error: halt

Rollback

steps:
  - id: stop-lxc
    method: POST
    path: /nodes/{{ target.node }}/lxc/{{ target.vmid }}/status/stop
    on_error: continue
  - id: delete-lxc
    method: DELETE
    path: /nodes/{{ target.node }}/lxc/{{ target.vmid }}
    on_error: continue

## Frontmatter Schema

### Always Required

| Field | Type | Description |
|-------|------|-------------|
| `id` | string | Matches filename basename |
| `kind` | enum | See Artifact Kinds below |
| `intent` | string | One-line description, max 200 chars |
| `status` | enum | See Lifecycle below |
| `mutating` | bool | `true` for all except `kb-note` |
| `produced_by` | object | `{ session, agent, user, at }` |
| `hash` | string | `sha256:<hex>` of the body |

### Required for Mutating Kinds

| Field | Type | Description |
|-------|------|-------------|
| `target` | object | See Target Object below |
| `idempotence` | enum | `via-precheck`, `declared-natural`, or `replay-only` |

### Set by Lifecycle Transitions

| Field | Set When |
|-------|----------|
| `approved_by` | On approve: `{ user, at, reason }` |
| `applied_at` | On apply (success) |
| `failed_at`, `failure_reason` | On apply (failure) |
| `supersedes` | At propose time (references older artifacts) |
| `superseded_by` | When superseded |
| `rejected_by` | On reject |
| `revoked_by` | On revoke |

## Target Object

```yaml
target:
  kind: vm | lxc | node | cluster | service | network | global
  host: <hostname>           # optional, inventory name
  vmid: <int>                # required for vm/lxc
  node: <pve-node-name>      # required for vm/lxc/node; forbidden for cluster
  service: <service-name>    # required for service
  network: <bridge-or-vlan>  # required for network
kind Required Optional
vm vmid, node host
lxc vmid, node host
node node —
cluster (none; node MUST NOT be set) —
service service host
network network node
global (none) —

Lifecycle

                    ┌─────────┐
              ┌────►│ proposed │
              │     └────┬────┘
              │          │ approve / reject
              │     ┌────┴──────┐
              │     ▼           ▼
              │  approved    rejected  ──── (terminal)
              │     │
              │     │ apply
              │  ┌──┴───┐
              │  ▼       ▼
              │ applied  failed ──► approve (retry)
              │  │
              │  │ supersede
              │  ▼
              │ superseded ──── (terminal)
              │
              └── revoke ──► revoked ──── (terminal, triggers rollback)
From To Trigger
(none) proposed propose_artifact(spec)
proposed approved hp artifacts approve <id>
proposed rejected hp artifacts reject <id> (terminal)
approved applied hp artifacts apply <id> (executor success)
approved failed Executor error during apply
failed approved hp artifacts approve <id> (retry, only if idempotent)
applied superseded Another artifact supersedes it (terminal)
applied revoked hp artifacts revoke <id> (triggers rollback)

kb-note shortcut

kind: kb-note artifacts skip the approval gate entirely — they auto-apply on propose. No approved_by, no executor run, no apply log.

Rejection is terminal

A rejected artifact stays for history but cannot be revived. Create a new artifact to re-do the work.

Artifact Kinds

ansible-playbook

Runs an Ansible playbook on the target host via the SSH jump server.

  • Idempotence: declared-natural (Ansible's promise)
  • Spec block: ```yaml ansible-spec ``` — valid Ansible playbook list
  • Rollback block: ```yaml ansible-rollback ``` — optional

proxmox-api-sequence

Executes an ordered sequence of Proxmox REST API calls. Each mutating step requires a precheck to verify idempotence.

  • Idempotence: via-precheck (default, required for mutating steps)
  • Spec block: ```yaml proxmox-api-spec ```
  • Rollback block: ```yaml proxmox-api-rollback ```
steps:
  - id: step-name
    method: POST | GET | PUT | DELETE
    path: /nodes/{{ target.node }}/lxc/{{ target.vmid }}/...
    body: { ... }               # optional
    precheck:                    # required for mutating steps
      method: GET
      path: /nodes/...
      skip_if: "response.status_code == 200"
    on_error: halt | continue   # default: halt

http-sequence

Same shape as proxmox-api-sequence but targets adopted services (e.g., Authentik). Each step resolves credentials from the vault by name.

  • Idempotence: via-precheck (required)
  • Spec block: ```yaml http-spec ```
  • Rollback block: ```yaml http-rollback ```

shell-script

Run a bash script on a managed host via SSH. Last resort for operations that don't fit structured kinds.

  • Required: ## Idempotence preamble section explaining how the script is idempotent
  • Spec block: ```bash shell-spec ```
  • Rollback block: ```bash shell-rollback ``` — optional but encouraged
  • Restriction: target host cannot be a Proxmox node itself

composite

References other artifacts and runs them in topological order. Used when a single intent spans multiple kinds.

  • Spec block: ```yaml composite-spec ```
  • Steps reference sub-artifact IDs
  • Approving a composite cascades to its sub-artifacts
  • Editing a sub-artifact invalidates composite approval

kb-note

Knowledge base entry. Non-mutating. Auto-applies on proposal.

  • Note kinds: note (durable fact), policy (preference), decision (recorded choice)

Idempotence

Value Meaning Required for
via-precheck Spec includes prechecks that verify current state before each step proxmox-api-sequence, http-sequence
declared-natural Agent claims the spec is naturally idempotent (e.g., Ansible modules) ansible-playbook, shell-script
replay-only Not idempotent; safe to apply once only. Executor refuses replay without --force Any kind, rare

Hash and Tamper Detection

The hash field covers the artifact body (everything after the frontmatter ---). It ensures the content you approved is exactly what gets applied.

  • Computed at propose_artifact
  • Re-verified at approve — mismatch refuses approval
  • Re-verified at apply — mismatch aborts with tampered error
  • Line endings normalized to LF via .gitattributes

CLI Reference

hp artifacts list                    # list all artifacts
hp artifacts show <id>               # show artifact detail
hp artifacts approve <id>            # approve a proposed artifact
hp artifacts reject <id>             # reject a proposed artifact
hp artifacts apply <id>              # apply an approved artifact
hp artifacts apply <id> --approve    # approve + apply atomically
hp artifacts edit <id>               # open in editor
hp artifacts revoke <id>             # revoke an applied artifact (triggers rollback)
hp artifacts drift <id>             # check drift for an applied artifact
hp artifacts push                    # push artifact git repo to remote
hp artifacts pull                    # pull artifact git repo from remote

See Also

Clone this wiki locally