leos-agent 10.7.1 → 10.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # leos-agent
2
2
 
3
- Leo's portable agent operating policy, version **10.7.1**, installable on Claude
3
+ Leo's portable agent operating policy, version **10.7.2**, installable on Claude
4
4
  Code, Codex, Cursor, Hermes, Pi, and OpenCode through each harness's own plugin
5
5
  system.
6
6
 
@@ -119,7 +119,7 @@ their global instruction file.
119
119
  The payload has always said that a subagent dispatch must name a model. Prose
120
120
  alone did not hold: a forgotten dispatch inherits the parent's expensive model
121
121
  and pays a cold cache write per child, which is the single most expensive shape
122
- this policy has. From 10.7.1 that half of the rule is enforced by a hook instead,
122
+ this policy has. From 10.7.2 that half of the rule is enforced by a hook instead,
123
123
  and the prose it replaced came out of the always-loaded payload — enforcement in
124
124
  code costs **zero** context per turn, so the guard paid for itself in bytes
125
125
  before saving a cent.
@@ -143,11 +143,12 @@ is also deliberately narrow: a false block costs one re-dispatch, while a caught
143
143
  inherited fan-out saves the cold prefix of every child, so the margin only holds
144
144
  while the rule refuses to make judgment calls.
145
145
 
146
- Detection is by **argument shape**, not tool name — only Claude Code's dispatch
147
- tool is verified, so an unanticipated one degrades to a no-op rather than a
148
- broken harness. MCP tools are never guarded. Anything that goes wrong inside the
149
- guard allows the call and records `decision: "error"`, kept distinct from a
150
- decision to allow, because a guard that dies quietly is worse than no guard.
146
+ Detection is by **argument shape**, not tool name: a dispatch is a call that
147
+ selects an agent and carries a brief. A shape the guard does not recognise is
148
+ left alone, so a harness it has never met keeps working. MCP tools are never
149
+ guarded. Anything that goes wrong inside the guard allows the call and records
150
+ `decision: "error"`, kept distinct from a decision to allow, because a guard
151
+ that dies quietly is worse than no guard.
151
152
 
152
153
  ```
153
154
  LEOS_AGENT_DISPATCH_GUARD=on block (default)
@@ -157,13 +158,17 @@ LEOS_AGENT_DISPATCH_GUARD=on block (default)
157
158
  LEOS_AGENT_DISPATCH_LOG_PROMPTS=1 debug only: keep 200 chars of brief text in the log
158
159
  ```
159
160
 
160
- **Coverage is honest, not uniform.** Claude Code is verified. Codex, Cursor,
161
- Hermes and OpenCode are wired with the same policy but their dispatch argument
162
- shapes are unconfirmed; there the guard is best-effort and no-ops rather than
163
- misfires. Pi has no hook surface and gets nothing. **Codex hash-pins hooks**, so
164
- upgrading to 10.7.1 — and every later edit to the guard — needs re-approval
165
- through `/hooks` there. Until you do, Codex silently enforces nothing; zero Codex
166
- rows in the report is the symptom.
161
+ **Per harness.** Claude Code dispatches `Agent` with `subagent_type`, and a
162
+ plugin install namespaces it — both forms are tiers. Codex dispatches
163
+ `spawn_agent`, which selects behaviour by `model` and `reasoning_effort` rather
164
+ than by naming an agent, so there the guard requires `model`; its `message` is
165
+ encrypted, so the over-delegation heuristic and block-conversion tracking do not
166
+ apply on Codex. Hermes and OpenCode run the same policy in-process, blocking by
167
+ directive and by throw respectively. Pi has no hook surface.
168
+
169
+ **Codex hash-pins hooks.** A new version of the guard enforces nothing there
170
+ until it is re-approved through `/hooks`, and the symptom is silence — zero
171
+ Codex rows in `dispatch_log.py report`.
167
172
 
168
173
  ### What it records
169
174
 
@@ -190,7 +195,7 @@ gets it through its global instruction file, written by
190
195
  [`scripts/leo-install.py`](scripts/leo-install.py) into a marker block:
191
196
 
192
197
  ```
193
- <leos-agent version="10.7.1">
198
+ <leos-agent version="10.7.2">
194
199
  ...the payload...
195
200
  </leos-agent>
196
201
  ```
@@ -259,7 +264,7 @@ that it is already installed and changes nothing.
259
264
  Run the installer's uninstall first, while the script is still on disk:
260
265
 
261
266
  ```bash
262
- python3 ~/.claude/plugins/cache/leos-agent/leos-agent/10.7.1/scripts/leo-install.py claude --uninstall
267
+ python3 ~/.claude/plugins/cache/leos-agent/leos-agent/10.7.2/scripts/leo-install.py claude --uninstall
263
268
  ```
264
269
 
265
270
  ```bash
@@ -290,7 +295,7 @@ Then run the `install` skill in a Codex session (`$leos-agent`, then `install`),
290
295
  run the script directly:
291
296
 
292
297
  ```bash
293
- python3 ~/.codex/plugins/cache/leos-agent/leos-agent/10.7.1/scripts/leo-install.py codex
298
+ python3 ~/.codex/plugins/cache/leos-agent/leos-agent/10.7.2/scripts/leo-install.py codex
294
299
  ```
295
300
 
296
301
  This writes `~/.codex/AGENTS.md` and installs two economical agents:
@@ -315,7 +320,7 @@ threads only. Re-adding an already-installed plugin is idempotent.
315
320
  **Uninstall**
316
321
 
317
322
  ```bash
318
- python3 ~/.codex/plugins/cache/leos-agent/leos-agent/10.7.1/scripts/leo-install.py codex --uninstall
323
+ python3 ~/.codex/plugins/cache/leos-agent/leos-agent/10.7.2/scripts/leo-install.py codex --uninstall
319
324
  ```
320
325
 
321
326
  ```bash
@@ -464,7 +469,7 @@ Pinned refs are reconciled, never silently advanced — to move to a new tag,
464
469
  install it explicitly:
465
470
 
466
471
  ```bash
467
- pi install git:github.com/foxhatleo/leos-agent@v10.7.1
472
+ pi install git:github.com/foxhatleo/leos-agent@v10.7.2
468
473
  ```
469
474
 
470
475
  Re-run `/skill:install` afterwards.
@@ -684,7 +689,7 @@ claude plugin uninstall leos-agent@leos-agent && claude plugin install leos-agen
684
689
  ```
685
690
 
686
691
  or replace the cachebuster suffix in the Codex manifest with one in the form
687
- `10.7.1+codex.local-YYYYMMDD-HHMMSS` and re-add. Either way, plugin changes only
692
+ `10.7.2+codex.local-YYYYMMDD-HHMMSS` and re-add. Either way, plugin changes only
688
693
  reach a **new** session or thread.
689
694
 
690
695
  `--check` exits non-zero when a file is out of date, and `--force` replaces a
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "leos-agent",
3
- "version": "10.7.1",
3
+ "version": "10.7.2",
4
4
  "description": "Leo's portable agent operating policy: orchestrator main thread, subagent-first execution, cost-tiered model routing.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -49,7 +49,7 @@ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
49
49
 
50
50
  Dispatch = collections.namedtuple(
51
51
  "Dispatch",
52
- "tool agent model prompt_bytes prompt_lines path_count prompt_hash prompt_head",
52
+ "tool agent model prompt_bytes prompt_lines path_count prompt_hash prompt_head opaque",
53
53
  )
54
54
 
55
55
  # The agent-selection field is mandatory, and that is the whole trick. A rule
@@ -71,6 +71,19 @@ SKIP_PREFIXES = ("mcp__",)
71
71
  # Seeded empty on purpose: populate from field reports, not from guesses.
72
72
  SKIP_TOOLS = frozenset()
73
73
 
74
+ # Dispatch tools that select behaviour by model rather than by naming an agent,
75
+ # so the agent-key rule alone would never see them. Exact names only, and only
76
+ # ones observed in a real rollout: Codex's spawn_agent takes
77
+ # {task_name, message, fork_turns, model, reasoning_effort} -- task_name is a
78
+ # free-text label, so `model` is the entire routing decision there.
79
+ DISPATCH_TOOLS = ("spawn_agent",)
80
+
81
+ # Tools whose brief does not arrive as readable text. Codex encrypts `message`,
82
+ # so its length is a proxy at best and its hash changes on every re-send. Both
83
+ # the size heuristic and the conversion hash are suppressed rather than reported
84
+ # as if they meant something.
85
+ OPAQUE_BRIEF_TOOLS = ("spawn_agent",)
86
+
74
87
  ALLOW, BLOCK = "allow", "block"
75
88
 
76
89
  # Path-ish tokens in a brief. Scanning is capped at the first 8 KiB -- the
@@ -131,21 +144,26 @@ def normalize(event, _harness=None):
131
144
 
132
145
  agent = _first_str(args, AGENT_KEYS)
133
146
  prompt = _first_str(args, PROMPT_KEYS)
134
- if not agent or not prompt:
147
+ known = tool in DISPATCH_TOOLS
148
+ if not prompt or not (agent or known):
135
149
  return None
136
150
 
137
151
  from dispatch_log import digest # local: a no-op call must not pay for this
138
152
 
153
+ opaque = tool in OPAQUE_BRIEF_TOOLS
139
154
  head = prompt[:PATH_SCAN_BYTES]
140
155
  return Dispatch(
141
156
  tool=tool or "-",
142
- agent=agent,
157
+ # A model-routed dispatch names no agent; label it by its tool so the log
158
+ # reads sensibly without inventing a tier that was never selected.
159
+ agent=agent or tool,
143
160
  model=_first_str(args, MODEL_KEYS) or None,
144
- prompt_bytes=len(prompt.encode("utf-8", "replace")),
145
- prompt_lines=prompt.count("\n") + 1,
146
- path_count=len(set(PATH_RE.findall(head))),
147
- prompt_hash=digest(prompt),
148
- prompt_head=prompt[:200],
161
+ prompt_bytes=0 if opaque else len(prompt.encode("utf-8", "replace")),
162
+ prompt_lines=0 if opaque else prompt.count("\n") + 1,
163
+ path_count=0 if opaque else len(set(PATH_RE.findall(head))),
164
+ prompt_hash=None if opaque else digest(prompt),
165
+ prompt_head="" if opaque else prompt[:200],
166
+ opaque=opaque,
149
167
  )
150
168
 
151
169
 
@@ -160,7 +178,7 @@ def triviality(dispatch):
160
178
  for a log line and disqualifying for a block, which is why this is only ever
161
179
  a log line.
162
180
  """
163
- if dispatch is None:
181
+ if dispatch is None or dispatch.opaque:
164
182
  return 0
165
183
  score = 0
166
184
  if dispatch.prompt_bytes < 220:
@@ -182,7 +200,9 @@ def routable(name):
182
200
  inherit and say so -- blocking there would demand something the harness
183
201
  cannot do, a false positive by construction.
184
202
  """
185
- if name == "claude":
203
+ # Both take a model per spawn: Claude Code on its dispatch tool, Codex as
204
+ # spawn_agent's `model` argument.
205
+ if name in ("claude", "codex"):
186
206
  return True
187
207
  try:
188
208
  import routing
@@ -215,8 +235,20 @@ def decide(dispatch, name, is_routable):
215
235
  return BLOCK, "no-model"
216
236
 
217
237
 
218
- def render_block(dispatch):
219
- """The refusal. It must name the remedies, or it costs a turn to discover them."""
238
+ def render_block(dispatch, name=None):
239
+ """The refusal. It must name the remedies, or it costs a turn to discover them.
240
+
241
+ The remedies differ by harness: Codex routes by `model` on spawn_agent and
242
+ has no agent to name, so offering it a subagent_type would be advice it
243
+ cannot take.
244
+ """
245
+ if dispatch.tool in DISPATCH_TOOLS or name == "codex":
246
+ return (
247
+ "[leo routing] BLOCKED - %s names no model, so it would silently inherit\n"
248
+ "the parent's. Re-dispatch with `model` set (and `reasoning_effort` with it),\n"
249
+ "naming the economical tier's model for narrow work.\n"
250
+ "Set LEOS_AGENT_DISPATCH_GUARD=off to disable, =warn to log only."
251
+ ) % dispatch.tool
220
252
  return (
221
253
  '[leo routing] BLOCKED - dispatch to agent "%s" names no model, so it would\n'
222
254
  "silently inherit the parent's. Re-dispatch with one of:\n"
@@ -291,7 +323,7 @@ def main(argv=None):
291
323
  pass
292
324
 
293
325
  if action == BLOCK and mode != "warn":
294
- sys.stderr.write(render_block(dispatch) + "\n")
326
+ sys.stderr.write(render_block(dispatch, name) + "\n")
295
327
  return 2
296
328
 
297
329
  # A notice is user-facing only. systemMessage reaches Leo's transcript and