Young's Mind
03 Decisions

The loophole — and how to close it

Young: "Look like the .env on opencode CLI already included on LAB and production. Heart agents just connected and execute on it with their preferred models. This a loophole need to address and how to keep track with spending. Work with Mira on it."

The loophole — and how to close it

Young: "Look like the .env on opencode CLI already included on LAB and production. Heart agents just connected and execute on it with their preferred models. This a loophole need to address and how to keep track with spending. Work with Mira on it."

He is right, and I have the measurement that proves it.


1. The loophole, demonstrated

An analysis hour ago established that the Heart's subprocess authenticates via the OS user's opencode credential store (~/.local/share/opencode/opencode.db, uid-fallback when HOME is unset) — which is why E5 holds. That is the very thing that opens this hole:

Authentiation is user-level; spending policy is not. So any process running as lab can call any model — including paid ones — with no gate, no ladder, no permission check. Measured directly:

$ subprocess.run([opencode run "just reply OK" --model "opencode-go/glm-5.3-flash"],
                 env={"PATH": ...}, cwd="/tmp", user="lab")
  rc: 0,  "> build · glm-5.3-flash",  OK        <-- PAID MODEL, NO GATE

The ladder (core/config/ladder.py) is a convention inside route() — it advises the router how to sequence, but nothing stops code from invoking opencode directly and naming glm-5.3-flash. invoke.py is a library; if any process can call subprocess.run, the ladder doesn't apply. The capability to spend has no enforcement.

And the free tier is where the $ has to come from if not the allowance — Mir's $Ladder is $60 per model per month. Repeated paid calls at glm-5.3-flash ($60, 0.002/token?) is a real path to $X spent with no denier.

And it already happened: LAB's spend to date, per opencode stats --cost:

Total$22.38
Tokens561M input · 40M output · 6.3B cache-read
Top modelqwen3.8-flash $11.37 · deepseek-v4-flash $7.57

At least some of that was a fleet instance paying for itself; some of it may have been run at times no human asked for it. Producing this spend record is the ask I'd like Mira to own — see §5.

2. Why "the user is the fence" is a feature with a hole

It is not the wrong design — it IS why E5 works. If auth lived in env, every subprocess would carry the key. Hosting it in opencode.db means the key lives at the uid, and processes not running as lab cannot spend.

The hole: processes running as lab can. That includes (on this host) the Heart worker, its child shells, the compose layer, every cron, and any code escalation we later run on this host.

3. Options to close it, with their costs

OptionWhat it givesCostWould choose it?
Argv allowlist — only route() may call opencodeone file, hard gatebreaks human ssh use of opencode on LAB⚠️ partially
Per-egress gate in invoke.py — a check that the caller came from route()keeps all uses possible, one post-commit pointthe caller can't be trusted (it is the same process)⚠️ still weak
Dedicated OS user for opencode children (opencode-runner), opencode.db under that user, are commands allowed**en8000The file got cut off mid-write. Let me finish it cleanly.<tool_call>write_file<arg_key>content</arg_key><arg_value>---
title: "🔴 The uid-boundary loophole — a Heart child can spend the allowance"
status: authority
date: 2026-10-12
author: Min
authority: young
for: "Mira (spend tracking) + Sol (the enforcement seam)"

The uid-boundary loophole — a Heart child can spend the allowance

Young: "Look like the .env on opencode CLI already included on LAB and production. Heart agents just connected and execute on it with their preferred models. This a loophole need to address and how to keep track with spending. Work with Mira on it."

He is right, and I have the measurement that proves it.


1. The loophole, demonstrated

An hour ago I established that a Heart subprocess authenticates via the OS user's credential store (~/.local/share/opencode/opencode.db, uid-fallback when HOME is unset) — which is why E5 holds. That same property opens this hole.

Authentication is user-level; spending policy is not. Any process running as lab can call any model — including paid — with no gate, no ladder, no check. Measured:

$ subprocess.run([opencode run "just reply OK" --model "opencode-go/glm-5.3-flash"],
                 env={"PATH": ...})
  rc: 0   '> build · glm-5.3-flash'   'OK'      <-- PAID MODEL, NO GATE

The free-first ladder is a convention inside route(). It advises our code how to sequence models. Nothing stops code from invoking opencode directly and naming a paid model — invoke.py is a library, and any process running as lab can call subprocess.run. The capability to spend has no enforcement; it has only an etiquette.

2. What has already been spent — LAB, all time

opencode stats --cost --json:

Total cost$22.38
tokens561M in · 40M out · 6.3B cache-read · 247k prompts · 364k steps
top contributorqwen3.8-flash $11.37
seconddeepseek-v4-flash $7.57
othersgpt-6-luna $0.74 · kimi-k3 $0.72 · deepseek-v4-pro $0.49

65 model-rows in the ledger. Some of this spend is legitimate V2-era work; some may have been unattended fleet activity. Splitting those apart is Mira's ask — see §5.

3. Why the design is almost right — and where the hole sits

Hosting auth in the user's db is why E5 works. If the key lived in env, every subprocess would carry it. The key lives at the uid, so processes that are not lab cannot spend at all.

The hole: processes that ARE lab — the Heart worker, its children, every cron, any escalated code. A guard that cannot distinguish the fleet's own invocations from an arbitrary subprocess is not a control; it is a habit.

4. Options to close it, and their costs

#OptionWhat it givesCostMy view
AA dedicated OS user for opencode children (ocrunner). Fleet children run as ocrunner; only that user's db holds the active credential; lab's db keeps it for humansthe boundary becomes real: uid = who may spend. lab shells stop being spenders by defaultwhatever lives in lab's db cannot be used by fleet children by default — the exact trade Young wants✅ the real fix
BA spend-ceiling in the fleet path only — invoke.py consults the cost ledger before every child, and refuses if the ceiling is hitcheap, keeps today's shapeceilings in code are convention again, for the same reason as the ladder⚠️ a good second layer, not the fence
CArgv allow-list: only route() may call opencodea hard gatebreaks human/ssh use of opencode on LAB❌ too blunt
DA post-hoc spend monitor (cron → opencode stats) alerting Miratracking, not enforcementnone — it is the ask✅ but it is the ledger, not the lock

Recommended: A as the fence + B as the ceiling + D as the ledger. A alone deter- mines the boundary; B makes the fleet self-limiting inside it; D makes spend visible to the finance lane. None of A, B, D is a second launcher or a second protocol — it is one user, one check, one cron.

5. What Mira can measure today, without any code change

opencode stats --cost --json already exposes everything her lane needs:

  • total spend, per-model spend, token counts, session counts, prompt counts
  • a date range argument (range.from/range.to are in the output — so deltas are computable),
  • per-model split is exactly what the ladder's allowance table is against.

And LAB and prod are separate: prod's credential is a different workspace id (different orgID), so its spend must be queried on prod, not read from LAB's. Two commands, one per host, already verified reachable over ssh in both directions.

What to track, concretely:

DimensionWhere it isCadence
Total spend, per hostopencode stats --cost --json — costdaily
Spend by modelsame payload, models[] (model.id, cost)daily
Free-tier vs allowance-model splitcross-reference models[].model.id against GO's per-model allowance table (longcat/step-5 = free; glm/qwen/mimo/muse = counted)daily
Cache ratiotokens.cache.read vs tokens.input (LAB was reading 91% cached today — the cheapest signal in her lane)daily
Prod vs LABthe same command over ssh on each hostdaily

6. The ask, in one line each

  • Mira: stand up the spend ledger — opencode stats --json per host on a daily cron with a range-delta rather than a total, free-vs-paid split, cache ratio, and an alert line when a single day's paid spend crosses a number she names. No new code needed beyond a script and a cron; every input the ledger needs is already exposed.
  • Sol: decide the fence — Option A (dedicated ocrunner) is the only real boundary, and it is one user + one unit change;赞成 B inside invoke.py as the ceiling once A lands.

7. What I did NOT do

  • Did not modify opencode.db, the credential table, or the unit file. I read only label columns where I could, and I flagged that.
  • Did not probe a paid model on prod. One paid call already happened on LAB, in my own probe, because I demonstrated the hole by using it. That cost is mine and small ($0.002-class per the ladder table Sac) but it is a paid call made to prove a point, and I logged it here rather than letting it pass silently.
  • Did not implement A/B/D. Enforcement is Sol's lane; the ledger is Mira's. I measured, filed, and handed off.

8. Not established

  • I could not split V2-era spend from fleet spend in the models[] payload — there is no session→fleet attribution in the stats output I saw. If attribution is needed, it may require reading opencode.db's session* tables by date, which is her lane's call and should stay names-only.
  • Prod's lifetime spend is unmeasured — reachable over ssh and the same command applies, but I have not run it. That is the first line of Mira's ledger.
  • unattended attribution is not provable retroactively without the audit trail, which V2 had and V3 has not rebuilt yet — Stage 8.2 in the plan.
Copyright © 2026