--- id: rein-openweights-openrouter-approle demand_source: glas-harness/workplans/GLAS-WP-0002-T02 consumer_repo: rein-openweights credential_type: openbao-approle-kv status: approved approved_by: "Bernd Worsch" approved_at: "2026-07-27" created: "2026-07-27" updated: "2026-07-27" --- # Construction plan: rein-openweights OpenBao AppRole ## 1. Demand `rein-openweights` (the OpenRouter-driven rein in the glas-harness family) needs to authenticate to OpenBao **non-interactively** — no operator OIDC session, no human in the loop at run time — to fetch its OpenRouter API key when it runs unattended (e.g. via a future scheduled trigger, or a glas-harness gateway invocation with no operator present). This was scoped as `glas-harness/GLAS-WP-0002-T02` ("Option B" — see that workplan's discussion) and is currently blocked: the only credential path exercised so far is the `OPENROUTER_API_KEY` env-var short-circuit in `rein_openweights/credentials.py`; the AppRole/vault branch (`_acquire_token`, `bao kv get`) is real code, live-verifiable, but has never actually talked to OpenBao. ## 2. Existing-structure survey **Checked:** `ops-warden/registry/routing/catalog.yaml` (readable directly — no OpenBao session needed for this). **Not checked:** live `bao policy list` / `bao auth list` / `bao secrets list` — this session has no valid OpenBao token (`bao token lookup` returns `403`). Whoever executes phase 4 must re-verify against live state before applying anything below; this survey is current as of what's on disk, not as of live OpenBao ACL state. **Relevant existing catalog entry: `openrouter-llm-connect`** (`ops-warden/registry/routing/catalog.yaml:283`) — an OpenRouter key already exists at `platform/workloads/activity-core/llm-connect/llm-connect-provider-secrets` (field `OPENROUTER_API_KEY`), gated by policy `workload-kv-read-llm-connect-provider-secrets`, `auth_method: caller's own OpenBao token (operator OIDC via key-cape, or a token carrying [that policy])`, marked `risk: high` ("provider API key with spend impact"). `rein-aharness`'s own mail-triage/brief-daily already reuse this exact lane (see `binky-control/integrations/executor-worker-secrets.md`, Lane 1 — "REUSE (verified)"). **Does this existing lane satisfy the demand? Partially, by design, not fully — reuse considered and declined:** - The existing lane's `auth_method` is interactive-caller-shaped (OIDC / "a token carrying the policy") — it was never scoped for a bare AppRole login the way `agent-harness-binky-mail`'s Lane 3 was. Binding a new AppRole to the *existing* policy is mechanically possible (Option A from the original discussion) but would give `rein-openweights` standing, non-interactive access to a policy explicitly flagged `risk: high` for spend impact, shared with `activity-core`'s broader usage — a wider blast radius than a single rein needs. - **Recommendation: do not reuse.** Build a dedicated, narrowly-scoped AppRole + KV path for `rein-openweights` specifically (matches `credentials.py`'s existing default `reins/rein-openweights/openrouter` path already in the code) — same reasoning `agent-harness-binky-mail` used for its own dedicated lane rather than widening an existing one. This keeps a leak scoped to exactly one consumer, one credential. ## 3. Proposed changes | # | Action | Object | Reuse-vs-new rationale | |---|---|---|---| | 1 | create | KV v2 secret path `reins/rein-openweights/openrouter` (field `api_key`) | New, narrowly scoped — matches `credentials.py`'s existing default path; declined reuse of the shared `activity-core` path (see §2) | | 2 | create | Policy `workload-kv-read-rein-openweights-openrouter` — read-only, scoped to exactly path #1 | New — mirrors the shape of `workload-kv-read-llm-connect-provider-secrets` but scoped to one path, not shared | | 3 | create | AppRole `rein-openweights`, bound to policy #2 | New — mirrors `agent-harness-binky-mail`'s AppRole shape: `token_ttl=15m`, `token_max_ttl=30m`, bounded `token_num_uses`, `secret_id_ttl` per current posture (build-phase; see `ops-warden/wiki/WorkloadSecurityPosture.md`) | | 4 | deliver | `role_id`/`secret_id` files, `0600`, to wherever the live verification runs (workstation or a future Railiance deployment) | New — matches `agent-harness-binky-mail`'s delivery pattern (`~/.local/agent-harness/approle-binky-mail`), analogous path for this consumer | | 5 | propose | ops-warden catalog entry `rein-openweights-openrouter-approle` — pointer-only, `warden_executes: false`, `status: draft` | New — no existing entry covers this AppRole; `openrouter-llm-connect` entry stays as-is, untouched | No tear-down in this plan — no existing lane is being retired or compacted; the existing `openrouter-llm-connect` lane is explicitly kept as-is for its current interactive-caller consumers. ## 4. Review notes (phase 2) - **Naming convention check:** follows `-` shape used by `agent-harness-binky-mail` — proposed `rein-openweights-openrouter-approle` for the AppRole/catalog id, `workload-kv-read-rein-openweights-openrouter` for the policy (matches the `workload-kv-read-` convention already visible in the existing catalog). - **TTL/scoping check:** `token_ttl=15m`/`token_max_ttl=30m`/bounded `token_num_uses` matches `agent-harness-binky-mail` exactly — no reason to diverge, this consumer's access pattern (one-shot credential fetch per run) is identical in shape. - **Redundancy check:** confirmed no second existing AppRole or policy already scoped to `reins/rein-openweights/*` — this is genuinely new structure, not a duplicate. - **Compaction opportunity:** none identified — nothing existing becomes redundant once this lands; the shared `activity-core` lane keeps its own consumers. - **Ease of use for the consumer:** matches directly — `credentials.py` already reads `REIN_OPENWEIGHTS_APPROLE_DIR` (defaults to a `role_id`/`secret_id` file pair) and a KV path override via `REIN_OPENWEIGHTS_OPENROUTER_KV_PATH` (defaulting to exactly `reins/rein-openweights/openrouter`, field `api_key`) — this plan's path #1 and delivery #4 need **zero code changes** in `rein-openweights` to consume; the code was already written expecting this exact shape. - **Posture check:** build phase (one founder-operator, pre-revenue) per `ops-warden/wiki/WorkloadSecurityPosture.md` — `agent-harness-binky-mail` used `secret_id_ttl=0` (no expiry) at this same posture; carrying that forward here rather than adding a rotation schedule neither `agent-harness-binky-mail` nor current posture requires. **Explicit assumption, not a silent default** — flagged again in the executive summary below for the approval decision to confirm or override. ## 5. Executive summary (phase 3) **One-line ask:** create a narrowly-scoped, non-interactive credential lane so `rein-openweights` can fetch its own OpenRouter API key at run time without an operator present. **Who/what gets access:** a new OpenBao AppRole named `rein-openweights`. Nothing else — no existing role, human, or service gains anything new. **To what:** read-only access to exactly one KV path, `reins/rein-openweights/openrouter` (field `api_key`) — a path that does not exist today and holds nothing else. The AppRole cannot read the existing shared `activity-core`/`llm-connect` secret or any other path. **For how long:** each login is a 15-minute token, renewable up to a 30-minute cap, bounded number of uses per token — matches the existing `agent-harness-binky-mail` lane exactly. The AppRole credential itself (`role_id`/`secret_id`) does not expire on a schedule at current (build-phase) posture, same choice already made for `agent-harness-binky-mail`. **Blast radius if the credential leaks:** whoever holds a valid `role_id`/`secret_id` pair can mint a short-lived token that reads one OpenRouter API key — nothing else in OpenBao. Actual damage from the OpenRouter key itself is real but bounded (API spend on that key, no data-store or infra access) and independent of this plan (same exposure `openrouter-llm-connect`'s existing consumers already carry). **Cost to reverse:** delete one AppRole, one policy, one KV path — three `bao` commands, no other lane or consumer affected. Fully isolated blast/reversal radius by construction (that was the point of declining reuse in §2). **Decision needed:** approve as proposed, reject, or send back to phase 1 with feedback (e.g. different TTL, different path name, or "actually reuse the shared lane instead"). **Decision: approved as proposed** (Bernd Worsch, 2026-07-27). Ready for phase 4 — still blocked on a real OpenBao session existing in whatever environment executes the build (`bao token lookup` from this workstation returns `403`; unchanged since `GLAS-WP-0002-T02` first flagged it). ## 6. Build result (phase 4)