# Feature 0004: Scriptorium Source Installation and Configuration Status: ready ## Summary Provide a small, independently usable `rakestraw.scriptorium` role that installs Scriptorium from a pinned Git tag, builds its executable on the managed host, and manages its native YAML configuration. Preserve Scriptorium's usefulness inside and outside the D&D transcript pipeline by keeping application configuration in the standalone role and leaving prompt library content and suite policy to their eventual owners. ## Problem or Opportunity The `ansible-role-scriptorium` repository contains only its initial README and license. Scriptorium is one of the five applications required by the pipeline, but it is also a generally useful Promptkit-based application with independent consumers. It needs a complete standalone installation and configuration role before the pipeline role can compose it without owning Scriptorium-specific behavior. The completed Seriatim, Audita, and Notarius roles establish the concise source-build pattern. Scriptorium can reuse that pattern while exposing its own configuration contract through field-specific, nullable variables. ## Governing Policies - [The Ansible house style](../../ansible.md) governs the role implementation, public configuration interface, and template. - [The testing house style](../../testing.md) governs repository-local validation. Tests exercise role-owned behavior through public inputs and native application outcomes without snapshotting defaults, tracing tasks, or rechecking trusted-module postconditions. - [The MVP roadmap](../mvp.md) fixes the common source-build architecture, supported platform, filesystem layout, and standalone-role boundary. ## Scope - Implement `ansible-role-scriptorium` as a conventional standalone role. - Support Ubuntu 26.04 on native `amd64` and `arm64` hosts. - Install Git and the official Go Snap with classic confinement when build dependency management is enabled. - Default the Go Snap channel to `1.26/stable`. - Check out immutable Scriptorium tag `v0.13.0` by default under `/usr/local/src/scriptorium`. - Build the native executable with Scriptorium's release build contract and install it at `/usr/local/bin/scriptorium`. - Rebuild when the Git checkout changes or the binary is absent. - Converge the source tree to `root:root` ownership and the installed binary to `root:root` ownership and mode `0755`. - Manage `/usr/local/etc/scriptorium/config.yml` as `root:root` with mode `0644` using one role-owned template. - Render `{}` when no application settings are selected, allowing Scriptorium to use its built-in defaults without copying them into Ansible. - Expose every application configuration field through a field-specific role variable. Default each field to null and omit it from the rendered document until a caller supplies a non-null value. - Expose the native open-ended `backends` mapping as one nullable variable and serialize the caller's mapping without inventing a second backend schema or merge mechanism. - Keep defaults, argument specifications, README, metadata, collection dependency declaration, template, and focused integration tests aligned with this contract. ## Non-goals - D&D-specific prompts, profiles, schemas, model choices, paths, or other pipeline policy. - Cloning, copying, or otherwise populating prompt, profile, or schema libraries. - A role-managed Scriptorium service, HTTP listener lifecycle, scheduler, or persistent runtime account. - A raw mapping for the complete configuration document, a template override, or parallel configuration interfaces. - Copying Scriptorium's documented built-in defaults into role defaults merely to render them back into YAML. - Decomposing the open-ended `backends` mapping into variables for each backend ID or backend-specific option. - Secret values, environment files, or another credential-delivery mechanism. - Installation or configuration of Promptkit, another pipeline application, or the complete D&D suite. - Installation from Scriptorium's published binary artifacts, cross-compiling, or supporting platforms outside the MVP. - Production execution of Scriptorium to validate its binary or configuration. - Detection of out-of-band binary modification, compiler changes, or fixed build-command changes. - Build fingerprints, checksums, sidecar state, staged replacement, manual Git cleanup, or special recovery for malformed or adversarial host state. ## Target End State On a supported host, applying the role with defaults installs or confirms Git and Go, checks out Scriptorium `v0.13.0` at `/usr/local/src/scriptorium`, builds `/usr/local/bin/scriptorium`, and manages `/usr/local/etc/scriptorium/config.yml`. The managed configuration is the empty mapping `{}`, so Scriptorium retains ownership of all application defaults. When a caller sets an optional configuration variable, the corresponding native YAML field appears in the correct section. Null fields are absent, as are parent mappings with no selected descendants. Explicit false, zero, empty string, empty collection, and other non-null values are serialized rather than silently omitted, leaving their native semantics to Scriptorium. `scriptorium_backends` represents the complete native `backends` field as a mapping keyed by backend ID; the role does not rewrite its nested definitions or combine fragments. Callers and composing roles may construct the final mapping with ordinary inventory and Jinja expressions. Prompt, profile, and schema directories remain caller-provided application inputs. The standalone role exposes their configuration paths but does not populate them. A later pipeline feature will deploy the maintained prompt library and pass its selected locations through this public interface. An immediate repeated application reports no changes. A changed checkout or a missing binary causes the build command to run. Configuration changes converge through the template without becoming build triggers. ## Repository Ownership - `ansible-role-scriptorium` owns the role implementation, public variables, native configuration serialization, metadata, dependency declaration, documentation, and repository-local tests. - `../scriptorium` is the read-only authority for release, build, CLI, and configuration contracts. - `rakestraw-ansible` is a read-only style and historical deployment reference. - `ansible-role-dnd-pipeline` will later own D&D-specific Scriptorium values and prompt-library deployment through the standalone role's public interface. - `ansible-workspace-dnd` owns this roadmap, the eventual staged implementation plan, and aggregate status. - All other repositories remain unchanged. ## Constraints and Settled Decisions - The selected default release is annotated tag `v0.13.0`, resolving to commit `f33af205242cbaeeb689c179c0465bd10beaa1a3`. - The release declares Go 1.25.5. The suite-standard `1.26/stable` Go Snap channel satisfies that requirement. - The native build uses package `./cmd/scriptorium`, `CGO_ENABLED=0`, `GOWORK=off`, `GOTOOLCHAIN=local`, `-trimpath`, and linker flags `-s -w`. - Scriptorium does not embed the selected application tag and does not expose a native application-version command. Production convergence does not add version interrogation, and tests do not inspect the Git checkout merely to manufacture version evidence. - Scriptorium accepts an empty configuration and resolves all omitted settings from built-in defaults. The role renders `{}` so the managed file is an explicit YAML mapping while selecting no application policy. - Configuration discovery checks `/usr/local/etc/scriptorium/config.yml` before `/etc/scriptorium/config.yml`; the role manages the first path. - Optional variables use YAML null defaults and template conditions use `is not none`. Parent mappings are emitted only when at least one descendant is non-null. - Strings and mappings are serialized with Ansible or Jinja filters rather than hand-written YAML quoting. - `scriptorium_backends` is a native open-ended mapping. Caller-side mapping composition is permitted, but the role supplies no merge or precedence behavior. - Backend configuration may name a credential environment variable through `api_key_env`; secret values themselves remain outside the role and managed file. - Tasks installing dependencies or managing root-owned state use task-level `become: true`. - `community.general` supplies Snap management and is pinned to version `13.3.0` in the role's collection requirements. ## Fixed Public Interface Provide these installation variables: | Variable | Default | Purpose | | --- | --- | --- | | `scriptorium_version` | `v0.13.0` | Selected Git revision | | `scriptorium_repository_url` | `https://gitea.maximumdirect.net/eric/scriptorium.git` | Source repository | | `scriptorium_source_dir` | `/usr/local/src/scriptorium` | Managed source checkout | | `scriptorium_binary_path` | `/usr/local/bin/scriptorium` | Installed executable | | `scriptorium_manage_build_dependencies` | `true` | Whether to install Git and Go | | `scriptorium_go_snap_channel` | `1.26/stable` | Go Snap channel | | `scriptorium_go_binary` | `/snap/bin/go` | Go executable used for the build | | `scriptorium_config_path` | `/usr/local/etc/scriptorium/config.yml` | Managed configuration file | Provide these nullable configuration variables: | Variable | Native field | | --- | --- | | `scriptorium_prompt_dir` | `prompt_dir` | | `scriptorium_profile_dir` | `profile_dir` | | `scriptorium_schema_dir` | `schema_dir` | | `scriptorium_server_addr` | `server.addr` | | `scriptorium_server_artifact_root` | `server.artifact_root` | | `scriptorium_server_max_request_bytes` | `server.max_request_bytes` | | `scriptorium_server_max_artifact_bytes` | `server.max_artifact_bytes` | | `scriptorium_server_max_response_bytes` | `server.max_response_bytes` | | `scriptorium_defaults_render_format` | `defaults.render_format` | | `scriptorium_backends` | `backends` | Argument specifications type paths and strings according to their native shape, the three server limits as integers, and `scriptorium_backends` as a mapping. They do not reproduce semantic validation owned by Scriptorium. The role preserves non-null values while the application decides whether a selected value is valid or effective. ## Simplest Viable Implementation The expected production implementation is approximately nine ordinary tasks in one readable task file: 1. Assert Ubuntu 26.04 and architecture `x86_64` or `aarch64`. 2. Install Git with `ansible.builtin.apt`, including `cache_valid_time: 3600`, when dependency management is enabled. 3. Install the classic Go Snap from `scriptorium_go_snap_channel` with `community.general.snap` when dependency management is enabled. 4. Create the source, binary, and configuration parent directories with one `ansible.builtin.file` loop. 5. Check out `scriptorium_version` with `ansible.builtin.git`, `force: true`, and register the module result. 6. Inspect whether `scriptorium_binary_path` exists with `ansible.builtin.stat`. 7. Run the native Go build through `scriptorium_go_binary` when the checkout changed or the binary is absent. 8. Converge recursive source ownership and binary ownership and mode with one item-driven `ansible.builtin.file` loop. 9. Render the field-specific configuration with one `ansible.builtin.template` task. Use `argv` for the build command, declare its change semantics, and put its two conditions directly on the task. Do not add a rebuild fact, handler, executable discovery, compiler or application interrogation, configuration assembly facts, preliminary configuration probes, or post-operation validation to production tasks. ## Rationale and Alternatives A standalone role is valuable beyond the D&D pipeline and prevents the suite role from becoming the owner of Scriptorium's native application interface. The suite can later provide opinionated directory paths and backend choices without reducing the role's usefulness to independent consumers. Field-specific nullable variables preserve a documented and typed inventory interface while allowing upstream defaults to evolve. Rendering `{}` is preferable to copying those defaults into Ansible and is more explicit than an empty file. Conditional sections distinguish omission from intentional falsey values. The `backends` mapping is the deliberate exception to leaf-level variables. Its keys are caller-defined backend IDs and its values follow an extensible native schema. Passing that one native field through as a mapping is simpler and more useful than inventing variables for arbitrary IDs, while still avoiding a raw mapping for the complete configuration document. The role exposes server configuration because it is part of Scriptorium's native interface, but it does not infer that callers want a managed service. Consumers may invoke the CLI directly or arrange their own process lifecycle. Scriptorium now publishes binary release artifacts, but the MVP deliberately uses the common source-build model across all five applications. Artifact-based installation remains a later cross-suite decision. ## Acceptance Criteria ### Required Role Behavior - The role rejects unsupported platforms before modifying application state. - On a supported host, it installs the selected Scriptorium source release and builds the binary with the upstream release contract. - Source, binary, and configuration state converge to the paths, ownership, and modes selected above through the responsible Ansible modules. - With no configuration variables selected, the template renders `{}` and Scriptorium accepts the managed file while retaining its built-in defaults. - Each non-null configuration variable renders its corresponding native field. Null fields and empty parent sections are omitted, while intentional falsey values are retained. - A caller-supplied backend mapping is rendered without structural rewriting, and independently defined mappings can be composed before assignment. - A changed checkout or missing binary causes the imperative build to run. An immediate repeated application with unchanged inputs reports no changes. - Dependency opt-out accepts a caller-supplied `scriptorium_go_binary`. - Root-owned operations use task-level privilege escalation; play-level escalation is not required. - Defaults, argument specifications, template behavior, README, and metadata remain aligned with the public interface. - Production tasks contain no role-private state, redundant probes, application self-tests, or speculative guarantees. - No repository other than `ansible-role-scriptorium` and the workspace control repository is modified. ### Required Validation - Syntax checking and production-profile ansible-lint pass. - A default invocation exercises the installed application with the managed configuration, followed immediately by a role application reporting no changes. It does not snapshot the role's current default tag or complete default document. - One recovery case removes the binary, disables dependency management, supplies a caller-selected Go executable, confirms that executable is used, and finishes with a clean repeated application. - One representative configuration case verifies selected scalar and nested values, selected omissions, intentional zero values, and a caller-supplied backend mapping. An offline native Scriptorium command accepts and uses the managed configuration, and an immediate repeated application reports no changes. - Tests do not assert task names or order, individual task results, exact current defaults, complete configuration snapshots, public-variable counts, trusted-module file or package postconditions, or an exhaustive negative platform matrix. - Because Scriptorium exposes no native application release identity, the test suite does not substitute a Git-checkout assertion or version-specific behavior snapshot for a native version-selection test. ## Dependencies and Follow-up Work The managed host requires access to the Scriptorium Git repository, Ubuntu Snap infrastructure, and Go module sources. Useful prompt execution also needs prompt definitions and, depending on the chosen configuration, profiles, schemas, backend endpoints, and credential environment variables. Those inputs remain caller responsibilities for this standalone feature. After this feature, the Narratio standalone role can be designed against all four downstream application-role interfaces. The later pipeline role will install maintained prompt-library content and supply D&D-specific Scriptorium configuration through this role. ## Potential Future Hardening - Build into a temporary path before replacing the installed executable. - Verify source signatures or release artifacts. - Detect compiler, build-command, or out-of-band binary changes. - Validate configuration during production convergence. - Manage a service, runtime account, secret environment, or network controls if later operating requirements select them. - Support Scriptorium's published binary artifacts as part of a future suite-wide installation-model change. - Add broader platform coverage only when supported-platform behavior expands.