Cairnloop.Tools.InternalNote (cairnloop v0.1.0)

Copy Markdown View Source

Example governed-write tool: appends an operator-only internal note to the host-owned cairnloop_messages store.

Usage

This is the Phase 16 proof-of-concept (ACT-01, D16-01) and the reference implementation for host developers building governed-write tools. Copy this module and adapt the schema, changeset, and run/3 body.

Design Notes

  • risk_tier: :low_write → derives approval_mode: :requires_approval automatically (D-09/D-10).
  • scope/0 returns [] — no special scopes required (D16-01: operator-only, low blast radius).
  • authorize/2 overrides the deny-by-default to :ok — any authenticated operator may propose this action; the approval gate provides the safety barrier.
  • run/3 is the ONLY place an actual side effect occurs. It is called only by Cairnloop.Workers.ToolExecutionWorker after the full approval + re-validation chain (D16-03).
  • Idempotency is implemented via an indexed run_key column existence check (D16-05). NEVER use a JSONB metadata: containment query — Ecto does not emit @> for map equality, and such a query would bypass the index entirely.
  • The note row carries role: :internal_note so it is distinguishable from customer-visible messages and can be filtered by host queries (D16-01 "never customer-visible").
  • Repo indirection: Application.fetch_env!(:cairnloop, :repo) — never Cairnloop.Repo directly. The host owns the repo; the library is a guest (D-02 / D16-02).

Run key idempotency

The worker passes :run_idempotency_key in the execution context. run/3 does an indexed existence check before inserting. If the row already exists, returns {:ok, %{idempotent: true}} without inserting — safe under Oban job replay (T-16-01).

Atomicity precondition (WR-01)

The run_idempotency_key passed to run/3 is attempt-scoped: it is derived from the proposal's idempotency_key and the current attempt number, so a Oban retry gets a different key. This design allows a retry to proceed cleanly if a prior attempt left no evidence row.

IMPORTANT for host tool authors copying this module: your run/3 MUST be a single atomic write keyed on run_key. If your tool performs multiple writes (e.g. row A then row B), a transient failure between them would be retried with a new run_idempotency_key — the existence check for the new key finds nothing, and the partial prior write (row A) is not rolled back. This could result in duplicate or inconsistent state. Rule: one run/3 invocation = one atomic operation (one INSERT, one Ecto.Multi inside a single transaction, etc.).

Summary

Functions

Appends an internal_note role row to cairnloop_messages.

Functions

run(internal_note, actor_id, context)

Appends an internal_note role row to cairnloop_messages.

Idempotent on context[:run_idempotency_key]: if a row with that run_key already exists, returns {:ok, %{idempotent: true}} without inserting (T-16-01, D16-05).

Returns:

  • {:ok, %{message_id: id}} — note inserted successfully
  • {:ok, %{idempotent: true}} — duplicate key; note already written
  • {:error, changeset} — insert failed (propagated for Oban retry logic)