# Feature 0005: Narratio Source Installation and Configuration Status: ready ## Summary Provide a small, independently usable `rakestraw.narratio` role that installs Narratio from a pinned Git tag, builds its executable on the managed host, creates the application account and runtime directories, and manages its native pipeline configuration. Keep the role application-specific so the later suite role can supply D&D policy through its public interface without making the Narratio role responsible for the other applications or campaign content. ## Problem or Opportunity The `ansible-role-narratio` repository contains only its initial README and license. Narratio is the command-line orchestrator for the transcript pipeline, so the complete suite cannot be composed until Narratio has an independently usable installation and configuration role. The four completed downstream application roles establish the concise source-build pattern. Narratio can reuse that pattern, but its native contract also requires a deployment account, shared writable runtime areas, a campaign registry root, and a substantially larger pipeline configuration interface. This feature fixes those boundaries before suite composition begins. ## 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-narratio` 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 Narratio tag `v2.0.0` by default under `/usr/local/src/narratio`. - Build the native executable with Narratio's release build contract and install it at `/usr/local/bin/narratio`. - Rebuild when the Git checkout changes or the binary is absent. - Converge the source tree and installed binary to root ownership, with the binary at mode `0755`. - Create the `narratio` system group and system account with `/var/lib/narratio` as its home and a non-login shell. - Create the workspace, spool, and cache roots as group-writable, setgid directories owned by `narratio:narratio`. - Create the campaign registry as a root-managed, operator-readable content root; leave its contents to the suite role or another caller. - Manage `/usr/local/etc/narratio/pipeline.yml` as `root:narratio` with mode `0640` using one role-owned template. - Require the caller to supply the deployment-specific WhisperX transcription URL because Narratio has no valid universal value for this required field. - Give filesystem roots concrete role-owned defaults. Default other optional application settings to null and omit them until a caller supplies a non-null value, allowing Narratio to retain ownership of its built-in defaults. - Expose scalar fields individually. Expose native open-ended collections as one variable each, including publish outputs, Notarius references and outputs, and Scriptorium artifacts and artifact families. - Permit callers to compose those collection variables in inventory before assigning the final value; do not add role-owned merge or precedence logic. - Expose ConfigKit's explicit import list in the root document without taking ownership of the imported fragments or named profile overlays. - Keep defaults, argument specifications, README, metadata, collection dependency declaration, template, and focused integration tests aligned with this contract. ## Non-goals - Installing or configuring Audita, Notarius, Seriatim, Scriptorium, WhisperX, or the complete D&D suite. - Populating campaign data, prompt libraries, profiles, schemas, or session documents. - Selecting D&D-specific pipelines, campaigns, models, prompts, artifacts, extraction lanes, publish rules, storage providers, or application paths. - Managing ConfigKit import fragments or named profile overlay files. - Managing credentials, secret values, a secrets directory, environment injection, or the pipeline-wide secret-delivery policy. - Adding human operator accounts to the `narratio` group; the suite role owns operator selection and membership. - A Narratio daemon, service unit, scheduler, or timer. - A raw mapping for the complete pipeline document, a template override, or a second parallel configuration interface. - Copying Narratio's built-in defaults into role defaults merely to render them back into YAML. - Decomposing open-ended artifact, artifact-family, reference, or output maps into variables for caller-defined keys. - Installing from published binary artifacts, cross-compiling, or supporting platforms outside the MVP. - Production execution of Narratio 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 the required `narratio_whisperx_transcribe_url` installs or confirms Git and Go, creates the Narratio account and filesystem roots, checks out Narratio `v2.0.0` at `/usr/local/src/narratio`, builds `/usr/local/bin/narratio`, and manages `/usr/local/etc/narratio/pipeline.yml`. The baseline managed pipeline document contains only the caller's required WhisperX URL and the filesystem roots the role deliberately owns. Narratio continues to supply all other application defaults. When a caller sets an optional configuration variable, the corresponding native YAML field appears in the correct section. Null fields and parent mappings with no selected descendants are absent. Explicit false, zero, empty string, empty list, empty mapping, and other non-null values are serialized rather than silently omitted, leaving their native semantics to Narratio. Native open-ended collections pass through without structural rewriting. Callers and composing roles may define fragments in separate inventory files and combine them with ordinary Ansible/Jinja expressions before assigning the final collection variable. The role does not merge fragments or define precedence among them. The `narratio` account owns the mutable workspace, spool, and cache roots. Their setgid group-write permissions support later direct invocation by human operators whom the suite role adds to the `narratio` group. The root-managed campaign registry remains readable but not writable by those operators. An immediate repeated application reports no changes. A changed checkout or a missing binary causes the build command to run. Account, directory, and configuration changes converge through their responsible Ansible modules and do not become build triggers. ## Repository Ownership - `ansible-role-narratio` owns the role implementation, application account, runtime and campaign-root state, public variables, native root pipeline serialization, metadata, dependency declaration, documentation, and repository-local tests. - `../narratio` is the read-only authority for release, build, CLI, configuration, filesystem, and integration contracts. - `rakestraw-ansible` is a read-only style and historical deployment reference. - `ansible-role-dnd-pipeline` will later own compatible application versions, D&D-specific Narratio values, campaign and prompt content, secret delivery, and human operator membership through the standalone roles' public interfaces. - `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 lightweight tag `v2.0.0`, resolving to commit `ed4623bc415901134c765fe9cf06d59aa7891500`. - The release declares Go 1.26.0. The suite-standard `1.26/stable` Go Snap channel satisfies that requirement. - The native build uses package `./cmd/narratio`, `CGO_ENABLED=0`, `GOWORK=off`, `GOTOOLCHAIN=local`, `-trimpath`, and linker flags `-s -w -X gitea.maximumdirect.net/eric/narratio/internal/buildinfo.Version=`. - `narratio version` reports the embedded selected tag and provides the native observable for an explicitly requested version in integration tests. - Narratio discovers `/usr/local/etc/narratio/pipeline.yml` before `/etc/narratio/pipeline.yml`; the role manages the first path by default. - `whisperx.transcribe_url` is the only pipeline field without an application default that is required for pipeline validation. The role therefore exposes `narratio_whisperx_transcribe_url` as a required public input rather than inventing a deployment endpoint. - Workspace, campaign, spool, and cache roots are concrete role defaults because the role creates and owns the corresponding filesystem state. These are intentional deployment policy, not a duplicate database of application defaults. - All other optional configuration 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 collections are serialized with Ansible or Jinja filters rather than hand-written YAML quoting. - `narratio_publish_outputs`, `narratio_notarius_references`, `narratio_notarius_outputs`, `narratio_scriptorium_artifacts`, and `narratio_scriptorium_artifact_families` represent their complete native fields. Caller-side composition is permitted, but the role supplies no merge behavior. - Credential configuration contains environment-variable names only. Secret values remain outside the role and managed pipeline document. - The configuration directory is `root:narratio` mode `0750`; the pipeline file is `root:narratio` mode `0640`. - Mutable runtime roots are `narratio:narratio` mode `2775`. The campaign root is `root:narratio` mode `0755` because version-controlled content is populated by a separate owner and is not operator-edited on the host. - Tasks installing dependencies or managing privileged 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 ### Installation and Managed Filesystem | Variable | Default | Purpose | | --- | --- | --- | | `narratio_version` | `v2.0.0` | Selected Git revision and embedded build version | | `narratio_repository_url` | `https://gitea.maximumdirect.net/eric/narratio.git` | Source repository | | `narratio_source_dir` | `/usr/local/src/narratio` | Managed source checkout | | `narratio_binary_path` | `/usr/local/bin/narratio` | Installed executable | | `narratio_manage_build_dependencies` | `true` | Whether to install Git and Go | | `narratio_go_snap_channel` | `1.26/stable` | Go Snap channel | | `narratio_go_binary` | `/snap/bin/go` | Go executable used for the build | | `narratio_user` | `narratio` | System account owning mutable runtime state | | `narratio_group` | `narratio` | System group sharing mutable runtime state | | `narratio_config_path` | `/usr/local/etc/narratio/pipeline.yml` | Managed root pipeline document | | `narratio_workspace_root` | `/var/lib/narratio` | Managed workspace root and account home | | `narratio_campaigns_root` | `/usr/local/share/narratio/campaigns` | Managed campaign registry root | | `narratio_spool_root` | `/var/spool/narratio` | Managed spool root | | `narratio_cache_root` | `/var/cache/narratio` | Managed cache root | Changing a managed root changes both its rendered pipeline value and the directory the role manages. The role does not infer additional arbitrary paths embedded inside caller-supplied open-ended collections. ### Pipeline Structure and Storage Except where a default is shown, every variable below defaults to null and is omitted when null. | Variable | Default | Native field | | --- | --- | --- | | `narratio_configkit_imports` | `null` | `configkit.imports` | | `narratio_workspace_cleanup_after_publish` | `null` | `workspace.cleanup_after_publish` | | `narratio_campaigns_default_campaign_id` | `null` | `campaigns.default_campaign_id` | | `narratio_storage_backend` | `null` | `storage.backend` | | `narratio_storage_s3_bucket` | `null` | `storage.s3.bucket` | | `narratio_storage_s3_root_prefix` | `null` | `storage.s3.root_prefix` | | `narratio_storage_s3_region` | `null` | `storage.s3.region` | | `narratio_storage_s3_endpoint` | `null` | `storage.s3.endpoint` | | `narratio_storage_s3_force_path_style` | `null` | `storage.s3.force_path_style` | | `narratio_storage_s3_access_key_id` | `null` | `storage.s3.access_key_id` environment-variable name | | `narratio_storage_s3_secret_access_key` | `null` | `storage.s3.secret_access_key` environment-variable name | | `narratio_spool_delete_audio_after_publish` | `null` | `spool.delete_audio_after_publish` | | `narratio_cache_s3_audio` | `null` | `cache.s3_audio` | | `narratio_publish_enabled` | `null` | `publish.enabled` | | `narratio_publish_upload_run` | `null` | `publish.upload_run` | | `narratio_publish_outputs` | `null` | Complete native `publish.outputs` list | ### WhisperX, Seriatim, and Audita | Variable | Default | Native field | | --- | --- | --- | | `narratio_whisperx_transcribe_url` | required | `whisperx.transcribe_url` | | `narratio_whisperx_language` | `null` | `whisperx.language` | | `narratio_whisperx_timeout` | `null` | `whisperx.timeout` | | `narratio_whisperx_retries` | `null` | `whisperx.retries` | | `narratio_whisperx_retry_delay` | `null` | `whisperx.retry_delay` | | `narratio_whisperx_concurrency` | `null` | `whisperx.concurrency` | | `narratio_seriatim_binary` | `null` | `seriatim.binary` | | `narratio_seriatim_timeout` | `null` | `seriatim.timeout` | | `narratio_seriatim_output_schema` | `null` | `seriatim.output_schema` | | `narratio_seriatim_coalesce_gap` | `null` | `seriatim.coalesce_gap` | | `narratio_seriatim_report` | `null` | `seriatim.report` | | `narratio_seriatim_env_overlap_word_run_gap` | `null` | `seriatim.env.overlap_word_run_gap` | | `narratio_seriatim_env_overlap_word_run_reorder_window` | `null` | `seriatim.env.overlap_word_run_reorder_window` | | `narratio_seriatim_env_backchannel_max_duration` | `null` | `seriatim.env.backchannel_max_duration` | | `narratio_seriatim_env_filler_max_duration` | `null` | `seriatim.env.filler_max_duration` | | `narratio_audita_binary` | `null` | `audita.binary` | | `narratio_audita_timeout` | `null` | `audita.timeout` | | `narratio_audita_llm_api_key` | `null` | `audita.llm_api_key` environment-variable name | | `narratio_audita_modules` | `null` | `audita.modules` | | `narratio_audita_base_url` | `null` | `audita.base_url` | | `narratio_audita_model` | `null` | `audita.model` | | `narratio_audita_total_llm_concurrency` | `null` | `audita.total_llm_concurrency` | | `narratio_audita_proposal_llm_concurrency` | `null` | `audita.proposal_llm_concurrency` | | `narratio_audita_validation_model` | `null` | `audita.validation_model` | | `narratio_audita_validation_llm_concurrency` | `null` | `audita.validation_llm_concurrency` | | `narratio_audita_transcript_description` | `null` | `audita.transcript_description` | | `narratio_audita_config_path` | `null` | `audita.config_path` | | `narratio_audita_output_schema` | `null` | `audita.output_schema` | | `narratio_audita_work_dir_retention` | `null` | `audita.work_dir_retention` | | `narratio_audita_report` | `null` | `audita.report` | ### Transcript Stages and Rendering | Variable | Default | Native field | | --- | --- | --- | | `narratio_normalize_output_path` | `null` | `normalize.output_path` | | `narratio_normalize_output_schema` | `null` | `normalize.output_schema` | | `narratio_normalize_report` | `null` | `normalize.report` | | `narratio_trim_enabled` | `null` | `trim.enabled` | | `narratio_trim_output_path` | `null` | `trim.output_path` | | `narratio_trim_bounds_prompt_id` | `null` | `trim.bounds.prompt_id` | | `narratio_trim_bounds_profile_id` | `null` | `trim.bounds.profile_id` | | `narratio_trim_bounds_transcript_input_name` | `null` | `trim.bounds.transcript_input_name` | | `narratio_trim_bounds_output_path` | `null` | `trim.bounds.output_path` | | `narratio_trim_bounds_timeout` | `null` | `trim.bounds.timeout` | | `narratio_trim_bounds_render_debug` | `null` | `trim.bounds.render_debug` | | `narratio_trim_bounds_render_output_path` | `null` | `trim.bounds.render_output_path` | | `narratio_trim_seriatim_report` | `null` | `trim.seriatim.report` | | `narratio_render_enabled` | `null` | `render.enabled` | | `narratio_render_format` | `null` | `render.format` | | `narratio_render_title` | `null` | `render.title` | | `narratio_render_include_timestamps` | `null` | `render.include_timestamps` | | `narratio_render_include_segment_ids` | `null` | `render.include_segment_ids` | | `narratio_render_include_metadata` | `null` | `render.include_metadata` | ### Scriptorium, Notarius, and Notification | Variable | Default | Native field | | --- | --- | --- | | `narratio_scriptorium_binary` | `null` | `scriptorium.binary` | | `narratio_scriptorium_config_path` | `null` | `scriptorium.config_path` | | `narratio_scriptorium_timeout` | `null` | `scriptorium.timeout` | | `narratio_scriptorium_render_debug` | `null` | `scriptorium.render_debug` | | `narratio_scriptorium_artifacts` | `null` | Complete native `scriptorium.artifacts` map | | `narratio_scriptorium_artifact_families` | `null` | Complete native `scriptorium.artifact_families` map | | `narratio_notarius_enabled` | `null` | `notarius.enabled` | | `narratio_notarius_binary` | `null` | `notarius.binary` | | `narratio_notarius_config_path` | `null` | `notarius.config_path` | | `narratio_notarius_pipeline_id` | `null` | `notarius.pipeline_id` | | `narratio_notarius_timeout` | `null` | `notarius.timeout` | | `narratio_notarius_working_directory` | `null` | `notarius.working_directory` | | `narratio_notarius_references` | `null` | Complete native `notarius.references` map | | `narratio_notarius_outputs` | `null` | Complete native `notarius.outputs` map | | `narratio_notification_mode` | `null` | `notification.mode` | ## Simplest Viable Implementation The production task flow is: 1. Assert the supported platform and architecture. 2. Install Git and Go when dependency management is enabled. 3. Create the Narratio system group and account. 4. Create the source and configuration parent directories. 5. Create the mutable runtime roots and read-only campaign registry root. 6. Check out the selected Narratio revision with `ansible.builtin.git`. 7. Inspect only whether the installed binary exists, because that result is needed to decide whether the imperative build command must run. 8. Build directly to the selected binary path when the checkout changed or the binary is absent. 9. Converge binary ownership and mode with `ansible.builtin.file`. 10. Render the root pipeline document with one `ansible.builtin.template` task. The build command uses `narratio_go_binary` and runs only under the two stated conditions. Ordinary modules own all other state checks. No role-private state, pre-build version interrogation, post-build validation, configuration validation, or redundant filesystem inspection belongs in production tasks. The template is necessarily larger than those of the downstream roles because Narratio's native pipeline schema is larger. Its logic remains mechanical: serialize the required URL and role-owned roots, test each optional field with `is not none`, emit a parent section only when needed, and pass native collections through unchanged. ## Rationale and Alternatives Narratio's configuration is broad, but a field-specific interface preserves discoverability, argument validation, stable inventory names, and a clean composition boundary for the suite role. A raw complete-document variable would make the template shorter but would discard those benefits and conflict with the established configuration policy. Open-ended native collections are kept whole because decomposing caller-defined keys would create a second schema and make inventory composition harder. Requiring the WhisperX URL is preferable to installing a configuration that Narratio cannot validate or inventing an endpoint that might silently direct work to the wrong service. The baseline integration invocation supplies a non-contacted test URL; production callers supply their actual endpoint. The standalone role creates the application account and directory permissions because those are Narratio runtime requirements. It does not select human operators, because membership is deployment-wide policy and belongs in the suite role. ConfigKit imports and profiles remain available to advanced callers, but the role manages only the canonical root document. This avoids a parallel raw-file deployment subsystem. A caller that selects imports or profiles is responsible for installing those additional files by another role or play. ## Acceptance Criteria ### Required Role Behavior - The role rejects unsupported platforms before modifying application state. - On a supported host, it installs the selected Narratio source release and builds the binary with the upstream release contract. - The system account, source, binary, configuration, runtime roots, and campaign root converge to the paths, ownership, and modes selected above through the responsible Ansible modules. - With only `narratio_whisperx_transcribe_url` supplied, the rendered pipeline contains that URL and the four role-owned roots, omits unselected optional policy, and is accepted by Narratio's native configuration validator. - Each non-null configuration variable renders its corresponding native field. Null fields and empty parent sections are omitted, while intentional falsey values are retained. - Caller-supplied open-ended collections are rendered without structural rewriting, and independently defined values 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 `narratio_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-narratio` and the workspace control repository is modified. ### Required Validation - Syntax checking and production-profile ansible-lint pass. - A baseline invocation supplies only the required WhisperX URL, exercises the installed application by validating the managed pipeline as the Narratio account, and is followed immediately by a role application reporting no changes. It does not snapshot the role's current default tag or complete default document. - One explicitly selected-version case requests `v2.0.0` and verifies that `narratio version` reports `v2.0.0`. The assertion proves caller-controlled version selection, not that `v2.0.0` remains the role default. - 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 falsey values, and composed open-ended collections. Narratio's native `config validate` accepts the managed pipeline, 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 account, directory, file, package, checkout, or template postconditions, or an exhaustive negative platform matrix. ## Dependencies and Follow-up Work The managed host requires access to the Narratio Git repository, Ubuntu Snap infrastructure, and Go module sources. Useful pipeline execution also requires a reachable WhisperX service, the four downstream application binaries and their configuration, campaign and session inputs, prompt content, and any selected object-storage or model credentials. Those remain caller responsibilities for this standalone feature. After this feature, all five standalone application roles will have stable public interfaces. The next unit can design the suite-level pipeline role, including dependency composition, compatible version selection, D&D pipeline policy, campaign and prompt deployment, secrets, and operator group membership. ## 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 imported fragments or profile overlays if a concrete standalone-role use case requires them. - Add a scheduler, service, or non-interactive execution environment if later operating requirements select one. - Support Narratio's published binary artifacts as part of a future suite-wide installation-model change. - Add broader platform coverage only when supported-platform behavior expands.