Plan the standalone Narratio role

This commit is contained in:
2026-09-09 21:33:44 +00:00
parent 860651966a
commit 7c2c9fe287
4 changed files with 910 additions and 23 deletions

View File

@@ -0,0 +1,461 @@
# 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=<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.