daybook_sessions_deploy¶
Deploy the Daybook agent-session shipper on macOS as a periodic launchd
LaunchDaemon. The role validates an existing uv binary, keeps a pinned
Daybook checkout on the target, runs uv sync, installs the trufflehog
Homebrew package, renders a secret environment file, and schedules
daybook sessions ship. It can also install the optional Archive quote
classifier launchd job that runs daybook archive classify-quotes from the same
checkout and environment. A separately gated Studio system LaunchDaemon can run
the draft-only daybook weeknotes reconcile command at 07:40 and 19:40 local
time. Its schedule is disabled and unloaded by default.
Run the role with facts enabled. It validates the macOS target, rejects placeholder credentials, and expects real MinIO credentials to come from a private control repo such as ops-control SOPS.
What This Role Manages¶
a pinned Daybook source checkout
an existing
uvbinary atdaybook_sessions_uv_binthe
trufflehogHomebrew package/etc/daybook-sessions/sessions.env/etc/daybook-sessions/ship-sessions.sh/Library/LaunchDaemons/de.wersdoerfer.daybook.sessions.plistoptional
/etc/daybook-sessions/archive-quotes.envoptional
/etc/daybook-sessions/classify-archive-quotes.shoptional
/Library/LaunchDaemons/de.wersdoerfer.daybook.archive-quotes.plistoptional
/etc/daybook-sessions/weeknotes-reconcile.env(mode0600)optional root-owned
/etc/daybook-sessions/weeknotes-reconcile.env.sha256optional
/etc/daybook-sessions/reconcile-weeknotes.shoptional
/etc/daybook-sessions/weeknotes-identity-ops.pyand/etc/daybook-sessions/weeknotes-identity-ops.shcontent-free operator railoptional
/Library/LaunchDaemons/de.wersdoerfer.daybook.weeknotes-reconcile.plistoptional dedicated
~/.daybook/weeknotes-reconcile/runtime (mode0700), withstate.json,.state.json.lock, and an auth-onlypi-agent/optional dedicated
~/.daybook/archive-quote-browserprofile directory (never a user’s live Helium profile)launchd stdout/stderr logs under
/var/log/daybook-sessions/the default shipper watermark state path at
~/.daybook/sessions-shipped.json
The role removes the legacy by-hand ~/daybook-ship.sh runner by default. It
does not delete the watermark state file; keeping it lets the first timed run
skip sessions that were already shipped manually.
Important Defaults¶
daybook_sessions_repo_url: "https://github.com/ephes/daybook.git"
daybook_sessions_repo_ref: "CHANGEME"
daybook_sessions_checkout_path: "{{ daybook_sessions_service_home }}/projects/daybook"
daybook_sessions_repo_update: true
daybook_sessions_path: "s3://agent-sessions"
daybook_sessions_schedule_interval_seconds: 1800
daybook_sessions_launchd_label: "de.wersdoerfer.daybook.sessions"
daybook_sessions_launchd_scope: "system"
daybook_sessions_state_path: "{{ daybook_sessions_service_home }}/.daybook/sessions-shipped.json"
daybook_sessions_brew_packages:
- "trufflehog"
daybook_sessions_redact_files: []
daybook_archive_quote_classifier_enabled: false
daybook_archive_quote_classifier_state: "s3://agent-sessions/archive-quote-classifier.json"
daybook_archive_quote_classifier_env_file: "{{ daybook_sessions_config_dir }}/archive-quotes.env"
daybook_archive_quote_classifier_cmd: "CHANGEME"
daybook_archive_quote_classifier_probe_enabled: true
daybook_archive_quote_classifier_probe_prompt: "Return exactly OK."
daybook_archive_quote_classifier_probe_timeout_seconds: 120
daybook_archive_quote_classifier_throttle_interval_seconds: 60
daybook_archive_url: "CHANGEME"
daybook_archive_token: "CHANGEME"
daybook_quotes_enabled: false
daybook_quotes_unused_path: "CHANGEME"
daybook_quotes_used_path: "CHANGEME"
daybook_quote_lifecycle_state: "s3://agent-sessions/weeknotes-quote-lifecycle.json"
daybook_archive_browser_executable: "/Applications/Helium.app/Contents/MacOS/Helium"
daybook_archive_browser_user_data_dir: "{{ daybook_sessions_service_home }}/.daybook/archive-quote-browser"
daybook_archive_browser_headless: true
daybook_archive_browser_launch_timeout_seconds: 30
daybook_archive_browser_navigation_timeout_seconds: 30
daybook_archive_browser_render_wait_seconds: 1
daybook_weeknotes_reconcile_enabled: false
daybook_weeknotes_reconcile_launchd_enabled: false
daybook_weeknotes_reconcile_launchd_label: de.wersdoerfer.daybook.weeknotes-reconcile
daybook_weeknotes_reconcile_start_calendar:
- {Hour: 7, Minute: 40}
- {Hour: 19, Minute: 40}
daybook_weeknotes_reconcile_runtime_dir: "{{ daybook_sessions_service_home }}/.daybook/weeknotes-reconcile"
daybook_weeknotes_reconcile_state_path: "{{ daybook_weeknotes_reconcile_runtime_dir }}/state.json"
daybook_weeknotes_reconcile_pi_agent_dir: "{{ daybook_weeknotes_reconcile_runtime_dir }}/pi-agent"
daybook_weeknotes_reconcile_pi_auth_source_root: "{{ daybook_sessions_service_home }}/.config/daybook-weeknotes-reconcile"
daybook_weeknotes_reconcile_pi_auth_source: "{{ daybook_weeknotes_reconcile_pi_auth_source_root }}/auth.json"
daybook_weeknotes_reconcile_pi_auth_rotate: false
daybook_weeknotes_public_repositories_json: "CHANGEME"
daybook_weeknotes_post_identity_epoch: ""
daybook_weeknotes_identity_required_repo_ref: ""
daybook_weeknotes_identity_seed_moves: []
daybook_weeknotes_identity_seed_path: "{{ daybook_weeknotes_reconcile_runtime_dir }}/identity-migration-seed.json"
daybook_weeknotes_identity_plan_path: "{{ daybook_weeknotes_reconcile_runtime_dir }}/identity-migration-plan.json"
daybook_weeknotes_identity_attestation_path: "{{ daybook_weeknotes_reconcile_runtime_dir }}/identity-migration-attestation.json"
daybook_weeknotes_identity_activation_proof_path: "{{ daybook_sessions_config_dir }}/weeknotes-identity-activation-proof.json"
These values must be supplied by the private control repo:
daybook_sessions_service_userdaybook_sessions_repo_refdaybook_sessions_aws_endpoint_url_s3daybook_sessions_aws_access_key_iddaybook_sessions_aws_secret_access_keydaybook_archive_url,daybook_archive_token, anddaybook_archive_quote_classifier_cmdwhen the optional classifier is enabled; anddaybook_quotes_unused_pathplusdaybook_quotes_used_pathwhen either quote lifecycle or classification is enabled. Thedaybook_quote_lifecycle_statedefault is the exact production location,s3://agent-sessions/weeknotes-quote-lifecycle.json; override it together with the Markdown paths for local or non-production lifecycles. The role probes the classifier command by default during deploy, which verifies both the executable and provider/subscription authentication before launchd is enabled.
The three quote lifecycle settings must be distinct locations on one backend.
The unused and used locations must be safe absolute local .md paths or safe,
exact s3://bucket/key.md object URIs; lifecycle state must be a safe absolute
local .json path or exact s3://bucket/key.json URI. Mixing local and S3
locations is rejected, as is reusing either Markdown location for state. All
three variables are written to sessions.env on handoff/delivery machines and
to the classifier environment when classification is enabled.
The role deliberately does not stat local quote files, contact S3, or create
files/object keys. Daybook reads the exact existing Markdown and JSON locations
and fails closed if any are missing or malformed. For an S3 lifecycle, the
configured MinIO credentials therefore need GetObject and PutObject for all
three objects in addition to the existing session scope; delete remains
unnecessary. The configured browser executable must be a real executable file
accessible to that user. The browser profile must be an absent/empty directory
or an already Daybook-marked directory directly below ~/.daybook/; the role
validates its real path and rejects symlinks, unsafe marker/lock files, wrong
ownership, and non-empty unmarked profiles. Never configure or copy a normal
Helium/Chromium user-data directory. A fresh empty dedicated profile is
sufficient for public X pages that do not require authentication.
uv sync --frozen installs the Python Playwright dependency from uv.lock with
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1. Daybook launches the configured Helium
binary, so this role does not install Playwright’s bundled Chromium.
Use a dedicated least-privilege MinIO account scoped to the existing session
objects and production lifecycle state object in agent-sessions, plus exactly
the two configured Obsidian quote objects. Do not grant quote-bucket
listing/delete access and do not pass the MinIO admin key to this role.
Set daybook_sessions_repo_update: false only when the private control repo has
pre-staged a service-user-owned checkout on the target and
daybook_sessions_repo_ref is the full 40-character commit id already present
there. The role verifies the checkout and commit locally, always rejects
untracked files, rejects tracked changes when force is disabled, and uses a
local detached checkout without any remote operation. A missing checkout or
object fails instead of cloning or fetching.
Launchd Scope¶
The default daybook_sessions_launchd_scope: "system" installs a LaunchDaemon
under /Library/LaunchDaemons and runs as daybook_sessions_service_user.
Use daybook_sessions_launchd_scope: "user" for laptops or hosts where the
control repo deploys as the logged-in service user without passwordless sudo.
Headed browser mode is accepted only in this GUI LaunchAgent scope and the plist
is restricted to an Aqua session; system LaunchDaemons must remain headless. In
user mode, override the managed paths to user-writable locations, for example:
daybook_sessions_launchd_scope: "user"
daybook_sessions_config_dir: "/Users/jochen/.config/daybook-sessions"
daybook_sessions_env_file: "/Users/jochen/.config/daybook-sessions/sessions.env"
daybook_sessions_launcher_path: "/Users/jochen/.config/daybook-sessions/ship-sessions.sh"
daybook_sessions_log_dir: "/Users/jochen/Library/Logs/daybook-sessions"
daybook_sessions_stdout_log: "/Users/jochen/Library/Logs/daybook-sessions/ship.log"
daybook_sessions_stderr_log: "/Users/jochen/Library/Logs/daybook-sessions/ship.err.log"
daybook_sessions_launchd_plist_path: "/Users/jochen/Library/LaunchAgents/de.wersdoerfer.daybook.sessions.plist"
daybook_archive_quote_classifier_launcher_path: "/Users/jochen/.config/daybook-sessions/classify-archive-quotes.sh"
daybook_archive_quote_classifier_env_file: "/Users/jochen/.config/daybook-sessions/archive-quotes.env"
daybook_archive_quote_classifier_stdout_log: "/Users/jochen/Library/Logs/daybook-sessions/archive-quotes.log"
daybook_archive_quote_classifier_stderr_log: "/Users/jochen/Library/Logs/daybook-sessions/archive-quotes.err.log"
daybook_archive_quote_classifier_launchd_plist_path: "/Users/jochen/Library/LaunchAgents/de.wersdoerfer.daybook.archive-quotes.plist"
Redaction Notes¶
daybook sessions ship harvests secret values from its process environment,
~/.aws/credentials, ~/.envrc, and any colon-separated
DAYBOOK_REDACT_FILES. The launchd job sources the rendered environment file
before running Daybook, so the injected MinIO credentials are present in the
process environment and can be redacted from transcripts before upload. When
quote classification is enabled, the role automatically adds
archive-quotes.env to DAYBOOK_REDACT_FILES, allowing Daybook’s secret-name
harvester to redact the Archive token without logging or exposing its value.
Set daybook_sessions_redact_files to host-local files that contain other API
keys worth redacting, for example project .envrc files:
daybook_sessions_redact_files:
- "/Users/jochen/projects/daybook/.envrc"
- "/Users/jochen/projects/ops-control/.envrc"
Dedicated Weeknotes Reconciler¶
Set daybook_weeknotes_reconcile_enabled: true to install the dedicated
reconciler files. This requires a system LaunchDaemon. Installation renders
the plist but daybook_weeknotes_reconcile_launchd_enabled defaults to false;
the role boots out a loaded copy and persists launchd’s disabled state. It never
kickstarts or otherwise invokes reconcile during deployment. Ordinary role
deployment rejects a true launchd flag. After reviewed migration and draft
inspection, private control must perform a fresh idempotent apply and then
include tasks_from: weeknotes_reconcile_activate in that same play to enable
and bootstrap the calendar schedule without an immediate run. In Ansible check
mode, read-only launchctl inspection still runs, all launchctl mutations and
mutation-dependent filesystem postconditions are skipped, and placeholder
validation remains fail-closed.
The calendar is fixed at 07:40 and 19:40 local time, twenty minutes after the
quote classifier’s 07:20 and 19:20 runs. StartCalendarInterval lets launchd
coalesce a missed occurrence after wake; there is deliberately no
StartInterval catch-up loop. Daybook’s non-blocking flock makes overlap a
content-free exit-1 result, and its idempotent state/fencing handles duplicate
calendar delivery safely. RunAtLoad is always false. The job is background,
low-I/O, draft-only, and has separate stdout/stderr logs.
The mode-0600 managed environment supplies these names:
DAYBOOK_PI_AGENT_DIR,DAYBOOK_PI_PUBLIC_TIMEOUT_SECONDS, andDAYBOOK_PI_FAMILY_TIMEOUT_SECONDSDAYBOOK_WEEKNOTES_RECONCILE_STATE,DAYBOOK_WEEKNOTES_PATH, andDAYBOOK_SESSIONS_PATHArchive GET URL (no Archive token), all three quote lifecycle locations, and the S3 endpoint, regions, and credentials
django-cast base URL/token and distinct public/family blog ids
weeknotes.home URL/token
the strict nonempty
DAYBOOK_WEEKNOTES_PUBLIC_REPOSITORIES_JSONmappingDAYBOOK_WEEKNOTES_POST_IDENTITY_EPOCH=monday-after-v1only after the exact private plan/attestation gate has passed
All credential and public-policy defaults are CHANGEME and are rejected.
Policy and credential values never enter logs, the plist, or argv; the launcher
sources the owner-only file and executes exactly
uv run --isolated --frozen --no-dev --no-config daybook weeknotes reconcile.
It never passes a
publish argument or prompt/content in argv.
The public policy is an editorial allowlist supplied by private control. Daybook validates it as a strict, nonempty alias-to-canonical-GitHub mapping from the process environment. The public collection contains no environment-specific aliases or repository URLs.
Attested identity epoch and operator rail¶
The epoch default is empty, and ordinary role deployment rejects every request
to enable the schedule, so a code install cannot gather corrected
Monday-after identities. Before any source, dependency, or configuration
mutation, the role disables and unloads the exact reconcile LaunchDaemon.
Requesting monday-after-v1 additionally requires a clean exact pinned Daybook
checkout, the exact private control seed, matching service-user-owned mode-0600
seed/plan/attestation files, and a root-owned mode-0600 activation proof binding
their plan hash to that Daybook revision. The installed helper and launcher must
match the current root-owned templates. Only then is the epoch rendered or the
schedule allowed to activate. Private control must perform a fresh idempotent
apply and include tasks_from: weeknotes_reconcile_activate in the same play;
that task binds the freshly verified plan hash to the installed epoch before it
enables and bootstraps without kickstart.
The installed operator launcher sources the existing mode-0600 reconcile
environment and exposes only fixed prepare, dry-run, apply, verify, and
status actions. It never accepts IDs, credentials, policy, post content, or
arbitrary command arguments. Preparation reads a private content-free seed,
GETs only those exact posts, verifies draft/live/parent/revision/old identity and
the previous_revision_id response contract, computes Daybook’s protected-field
hash in memory, and publishes the mode-0600 plan through a fsynced temporary
file and atomic no-replace hard link. A crash after the durable link is safely
completed on retry; a partial file never occupies the final path. It never
prints bodies or metadata. If a plan already exists it must match the
seed and is reused without GET regeneration, preserving crash recovery after a
partial migration.
Operator actions and the scheduled launcher require the exact clean
service-owned checkout, an isolated frozen uv environment, and byte-identical
root-owned rail files. Both root-owned launchers verify the owner-readable
environment against a root-controlled SHA-256 manifest before sourcing it. Normal role
deployment only installs this rail; it never invokes it. Private
control must call tasks_from: weeknotes_identity_operator for one explicit
action. Apply additionally requires the exact confirmation phrase
APPLY monday-after-v1 draft identity migration. Dry-run is Daybook’s
zero-mutation migration mode; apply is a separate command and holds the existing
reconcile flock. No action publishes.
django-cast commit 80b80928 is a hard prerequisite. Its editor response adds
the content-free, page-local previous_revision_id that lets Daybook prove an
already-renamed post is exactly one page revision after the planned source. A
later edit fails that proof; global revision IDs must never be decremented or
assumed consecutive.
Static install, migration, epoch, and activation flow¶
# Install/update while forcing the new unit disabled and unloaded.
ansible-playbook ... -e daybook_weeknotes_reconcile_enabled=true \
-e daybook_weeknotes_reconcile_launchd_enabled=false
# Through a separate private operator playbook/tasks_from invocation:
# prepare -> dry-run -> explicit apply -> verify -> idempotent apply.
# The original runtime plan is retained and reused after partial progress.
# Install the epoch while still disabled. The role verifies the exact
# plan/attestation and pinned ref before rendering it.
ansible-playbook ... -e daybook_weeknotes_reconcile_enabled=true \
-e daybook_weeknotes_reconcile_launchd_enabled=false \
-e daybook_weeknotes_post_identity_epoch=monday-after-v1
# Only after independent inspection, use a private activation play that runs a
# fresh apply and then includes tasks_from=weeknotes_reconcile_activate in that
# same play. The normal role rejects launchd_enabled=true.
Status remains content-free:
launchctl print system/de.wersdoerfer.daybook.weeknotes-reconcile
set -a; . /etc/daybook-sessions/weeknotes-reconcile.env; set +a
cd /Users/USER/projects/daybook
/opt/homebrew/bin/uv run --isolated --frozen --no-dev --no-config \
daybook weeknotes reconcile-status \
--state "$DAYBOOK_WEEKNOTES_RECONCILE_STATE"
A normal exit is 0. Lock contention or an audience failure returns 1 with
Daybook’s bounded, content-free JSON report; launchd exposes the last exit status.
A safe manual rerun is the launcher with no arguments or one explicit
--audience. It remains draft-only and idempotent.
To roll back, first deploy with the launchd flag false and the identity epoch
empty. Preserve the runtime seed, plan, attestation, root activation proof,
state, lock, drafts, quotes,
and comments so interrupted work remains recoverable. Then remove only the new
plist, launcher, environment, and its two logs if desired. Preserve the reconcile
~/.daybook/weeknotes-reconcile/state.json, its .state.json.lock, drafts,
quote assignments/lifecycle, and managed pi auth unless an operator separately
rotates authorization. Reverting ops commits
is safe after the unit is unloaded. This role does not configure log rotation;
that matches the existing Daybook launchd jobs.
Example¶
- name: Deploy Daybook session shipper
hosts: mac_session_feeders
become: true
roles:
- role: local.ops_library.daybook_sessions_deploy
vars:
daybook_sessions_service_user: "jochen"
daybook_sessions_brew_user: "jochen"
daybook_sessions_repo_ref: "main"
daybook_sessions_aws_endpoint_url_s3: "{{ daybook_sessions_secrets.aws_endpoint_url_s3 }}"
daybook_sessions_aws_access_key_id: "{{ daybook_sessions_secrets.aws_access_key_id }}"
daybook_sessions_aws_secret_access_key: "{{ daybook_sessions_secrets.aws_secret_access_key }}"
The classifier job runs as a background, low-I/O launchd process with a throttle interval and fixed calendar schedule. The profile lock prevents overlapping manual and scheduled browser runs; headless mode is the safe unattended default.
Verify on the target with:
launchctl print system/de.wersdoerfer.daybook.sessions
tail -f /var/log/daybook-sessions/ship.log