@miller-tech/uap 1.179.2 → 1.179.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miller-tech/uap",
3
- "version": "1.179.2",
3
+ "version": "1.179.3",
4
4
  "description": "Autonomous AI agent memory system with CLAUDE.md protocol enforcement",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -211,6 +211,31 @@ def _local_mode() -> str:
211
211
 
212
212
  BASH_OPS = {"Bash", "bash", "run_bash", "shell"}
213
213
 
214
+ # The delivery gate's own state. The pending log is the replay queue for
215
+ # `uap deliver --pending`; the lock and heartbeat are how an in-flight run is
216
+ # found, followed, and reclaimed when wedged.
217
+ #
218
+ # Guarding only `rm <literal path>` would repeat the mistake this whole change
219
+ # exists to fix. `: > .uap/pending-deliver.jsonl` is SHORTER than the command it
220
+ # blocks and just as destructive, and `rm -rf .uap` takes all three at once. So
221
+ # match any destructive verb (or a truncating redirect) against any path under
222
+ # `.uap/` that names deliver state — including the directory itself and globs.
223
+ _DELIVER_STATE_PATH = (
224
+ r"(?:pending-deliver\.jsonl|deliver\.lock|deliver\.heartbeat"
225
+ r"|pending-[^\s'\"|;&]*|deliver\.[^\s'\"|;&]*|\*[^\s'\"|;&]*)"
226
+ )
227
+ _DELIVER_STATE_RM_RE = re.compile(
228
+ r"(?:\b(?:rm|unlink|shred|truncate|mv)\b"
229
+ r"|\bfind\b[^|;&\n]*-(?:delete|exec\s+rm)"
230
+ r"|\bgit\s+clean\b"
231
+ r"|(?<![0-9<>])>(?!>))"
232
+ r"[^|;&\n]*?"
233
+ # The inner path is optional INSIDE the slash group so a bare `.uap/`
234
+ # (trailing slash, nothing after) still matches — `rm -rf .uap/` destroys
235
+ # exactly as much as `rm -rf .uap`.
236
+ r"((?:[^\s'\"|;&]*/)?\.uap(?:/(?:" + _DELIVER_STATE_PATH + r")?)?)(?:\s|$|['\"])"
237
+ )
238
+
214
239
  # A bash command that WRITES a source file: `> f.ts`, `>> f.ts`, `tee f.ts`,
215
240
  # `sed -i ... f.ts`. Without this, Edit/Write gating is trivially bypassable —
216
241
  # `cat > app.js <<EOF` writes source with no deliver run and no validation.
@@ -375,6 +400,44 @@ def _handle_bash(args: dict) -> None:
375
400
  if os.environ.get("UAP_DELIVER_BYPASS") == "1":
376
401
  emit(True, "bash: UAP_DELIVER_BYPASS override set")
377
402
  return
403
+ # Destroying the gate's own state is not a way out of the gate. Observed
404
+ # live 7x on 2026-07-31 (octopus_invaders_v3), interleaved with kill -9 of
405
+ # the running deliver: the queued edit intents were discarded to escape a
406
+ # block rather than completing the work.
407
+ #
408
+ # deliver's own housekeeping is unaffected twice over: it rewrites the
409
+ # pending log in-process (delivery/pending-intents.ts) rather than shelling
410
+ # out, and its subprocesses carry UAP_DELIVER_ACTIVE=1, which returned
411
+ # above. UAP_DELIVER_BYPASS=1 also returned above — operator-set only, since
412
+ # enforcement_self_protect refuses the inline form.
413
+ #
414
+ # This block is deliberately NOT relaxed by UAP_ENFORCE_DELIVERY=advisory:
415
+ # advisory trades verification for speed on an EDIT, but destroying recorded
416
+ # state is not an edit and has no verified-later equivalent.
417
+ sm = _DELIVER_STATE_RM_RE.search(cmd)
418
+ if sm:
419
+ target = sm.group(1)
420
+ if "pending-" in target:
421
+ what = (
422
+ "that is the queue of edit intents deliver replays; removing it "
423
+ "discards recorded work rather than completing it. To apply what "
424
+ "is already queued, run `uap deliver --pending`"
425
+ )
426
+ else:
427
+ what = (
428
+ "that is the single-flight lock/heartbeat; deleting it starts a "
429
+ "SECOND concurrent run on the same tree. A stale lock is "
430
+ "reclaimed automatically by heartbeat age, so it never needs "
431
+ "deleting"
432
+ )
433
+ emit(
434
+ False,
435
+ f"BLOCKED: do not destroy the delivery gate's own state ('{target}') — "
436
+ f"{what}. If a deliver run is in flight, wait for it (deliver tool "
437
+ "with follow:true, or `uap deliver --await-run`).",
438
+ )
439
+ return
440
+
378
441
  m = _BASH_WRITE_RE.search(cmd)
379
442
  if m:
380
443
  target = m.group(1)
@@ -26,13 +26,34 @@ Scope (Bash/bash/run_bash commands only):
26
26
  - broad `-f`/`--full` kill whose pattern is a substring of the stack's argv
27
27
  (uap/llama/qwen/mmproj/nomic/anthropic) or a glob over the python
28
28
  interpreter that runs the proxy.
29
+ - a kill held APART from its target — by a pipe (`ps aux | grep llama-server
30
+ | xargs kill -9`), by an infra-port lookup feeding it (`lsof -t -i:4000 |
31
+ xargs kill -9`), by a variable (`X=$(pgrep -f llama-server); kill -9 $X`),
32
+ or by a `-f` pattern that is an infra port (`pkill -f 8080`) — rule 8.
33
+ Matching is on TEXT, so a token counts wherever it appears; quoted data and
34
+ heredoc bodies are stripped first so prose about a kill is not a kill.
35
+ - a kill whose bare PID resolves to a deliver run or the inference stack
36
+ (`kill -9 3936358`, `kill -9 -3936358`) — rule 9. Resolved from
37
+ .uap/deliver.lock and /proc argv, not from the command text, and only for
38
+ the numbers the command actually names.
29
39
  - systemctl stop/restart/kill/disable of the inference services.
30
40
  - Starting a server that BINDS an infra port (http.server 8080 etc.) —
31
41
  this is how the port got stolen even with kills blocked (the model
32
42
  bound the port inside llama-server's crash/restart window).
33
43
 
34
44
  Killing a SPECIFIC process pattern (e.g. `pkill -f "python3 -m http.server
35
- 8765"`) and serving on non-infra ports stay allowed.
45
+ 8765"`) and serving on non-infra ports stay allowed — unless the pattern itself
46
+ carries a stack token, so `pkill -f 'uap deliver'` is refused; wait for the run
47
+ instead.
48
+
49
+ KNOWN LIMITS, so nobody reads this as airtight. It is a TEXT gate on a language
50
+ with unbounded ways to say the same thing:
51
+ - obfuscated patterns (`grep '[l]lama-server'`, `pkill -f 'llam[a]-server'`)
52
+ match the real process but not these rules;
53
+ - anything routed through `eval`/base64 defeats every text rule by design;
54
+ - the script-body scan is depth-1 and shell-only (`.sh/.bash/.zsh`).
55
+ Rule 9 is the part that cannot be spelled around, because it resolves PIDs
56
+ rather than reading them. Treat 1-8 as raising the cost, not as a boundary.
36
57
  """
37
58
  import os
38
59
  import re
@@ -40,7 +61,7 @@ import sys
40
61
  from pathlib import Path
41
62
 
42
63
  sys.path.insert(0, str(Path(__file__).parent))
43
- from _common import emit, parse_cli # noqa: E402
64
+ from _common import emit, parse_cli, repo_root, scannable_command # noqa: E402
44
65
 
45
66
  BASH_OPS = {"Bash", "bash", "run_bash", "shell", "execute_command"}
46
67
 
@@ -90,6 +111,233 @@ RULES = (
90
111
  ),
91
112
  )
92
113
 
114
+ # --------------------------------------------------------------------------
115
+ # 8) LAUNDERED kills — the verb and its target pulled apart.
116
+ #
117
+ # Rules 1-7 each scan a single shell SEGMENT (`[^|;&\n]*`), so moving the PID
118
+ # list one hop away splits the kill verb from the thing being killed and the
119
+ # whole rule set goes blind. Every one of these reached the stack:
120
+ # ps aux | grep llama-server | awk '{print $2}' | xargs kill -9 (pipe)
121
+ # lsof -t -i:4000 | xargs kill -9 (pipe)
122
+ # PIDS=$(pgrep -f llama-server); kill -9 $PIDS (variable)
123
+ # for p in $(pgrep -f llama-server); do kill -9 $p; done (loop)
124
+ #
125
+ # Observed live (2026-07-31, octopus_invaders_v3, qwen on opencode): the model's
126
+ # `pkill -f "uap deliver"` was refused at 11:13:46Z and EIGHT SECONDS LATER it
127
+ # ran the xargs form and reported "cleaned"; it repeated that at 11:41:53Z. The
128
+ # evasion is found without being sought, so closing only the observed spelling
129
+ # is not closing anything — the `;` twin is the same thought one keystroke away.
130
+ #
131
+ # Three passes, all on ONE logical command:
132
+ # a) statement-local: a kill verb plus a stack token / infra-port lookup.
133
+ # b) cross-statement taint: a LOOKUP that names the stack (pgrep/ps/lsof/...)
134
+ # anywhere, plus a kill verb anywhere. This is what catches the `;` and
135
+ # loop forms, where neither half is damning alone.
136
+ # c) a `-f` kill whose pattern IS an infra port (`pkill -f 8080`), which names
137
+ # no token yet matches llama-server's argv.
138
+ # Statements split on `;`, `&&`, `||`, newline — never on `|`, since a pipeline
139
+ # is one command.
140
+ #
141
+ # Quoted DATA is stripped first via scannable_command(), which removes heredoc
142
+ # bodies and multi-word quoted blobs but keeps single-word quoted arguments, and
143
+ # strips nothing when the command hands text to a shell (`bash <<EOF`, `sh -c`,
144
+ # `eval`, `xargs`). That kills the false-positive class — a commit message or an
145
+ # `echo` describing a kill — WITHOUT opening a heredoc as an evasion. Rules 1-7
146
+ # still read the raw text, so `pkill -f "uap deliver"` stays caught by rule 6.
147
+ _STATEMENT_SPLIT_RE = re.compile(r";|&&|\|\||\n")
148
+ # A kill in COMMAND POSITION, not the word "kill" inside prose or a grep pattern.
149
+ # The `xargs` branch is what keeps the pipe form matching.
150
+ _KILL_VERB_RE = re.compile(
151
+ r"(?:^|[|;&(`]|\|\||&&|\bxargs\b[^|;&\n]*|\bsudo\b\s+|\bexec\b\s+"
152
+ r"|\bthen\b\s+|\bdo\b\s+|\$\()"
153
+ r"\s*(?:\w+=\S*\s+)*(?:[\w./~-]*/)?(?:p?kill(?:all)?[0-9]*|skill)\b"
154
+ )
155
+ _LOOKUP_VERB_RE = re.compile(r"\b(?:pgrep|pidof|ps|lsof|fuser|ss|netstat)\b")
156
+ _INFRA_TOKEN_RE = re.compile(
157
+ r"\b(uap|llama|llama-server|anthropic|anthropic_proxy|nomic|mmproj"
158
+ r"|qwen[0-9.]*|llama-slots|slots?[_-]?save|deliver)\b",
159
+ re.IGNORECASE,
160
+ )
161
+ _PORT_LOOKUP_RE = re.compile(
162
+ r"\b(lsof|fuser|ss|netstat)\b[^\n]*?[:\s=]" + INFRA_PORTS + r"\b"
163
+ )
164
+ # `pkill -f 8080` — the pattern is the port itself; no token, still fatal.
165
+ _KILL_PATTERN_PORT_RE = re.compile(
166
+ r"\b(?:p?kill(?:all)?|skill)\b[^|;&\n]*"
167
+ r"(?:\s-(?:-full|[A-Za-z0-9]*f)\b)[^|;&\n]*\b" + INFRA_PORTS + r"\b"
168
+ )
169
+ # Simple `VAR=value` / `VAR="value"` bindings, substituted before splitting so
170
+ # `X=llama-server; pkill -f "$X"` collapses to the form rule 6 already refuses.
171
+ # Heuristic by design: no scoping, no command-substitution values. It can only
172
+ # ADD matches, never remove one, so it cannot introduce a miss.
173
+ _ASSIGN_RE = re.compile(
174
+ r"\b([A-Za-z_]\w*)=(?:\"([^\"]*)\"|'([^']*)'|([^\s;&|]+))"
175
+ )
176
+
177
+
178
+ def _expand_assignments(cmd: str) -> str:
179
+ bindings = {
180
+ m.group(1): (m.group(2) or m.group(3) or m.group(4) or "")
181
+ for m in _ASSIGN_RE.finditer(cmd)
182
+ }
183
+ for name, value in bindings.items():
184
+ if value:
185
+ cmd = re.sub(r"\$\{?" + re.escape(name) + r"\}?", value, cmd)
186
+ return cmd
187
+
188
+
189
+ def _laundered_infra_kill(cmd: str) -> bool:
190
+ """True when a command kills the stack with the verb held apart from it.
191
+
192
+ The two halves are read from DIFFERENT views of the same command, because
193
+ they fail in opposite directions:
194
+
195
+ * the kill VERB is read from scannable_command() — quoted data removed —
196
+ so `-m "…laundered kill of llama-server"` is not mistaken for a kill;
197
+ * the stack TOKEN is read from the RAW text, because the token routinely
198
+ lives inside the quotes that identify the victim
199
+ (`pgrep -f "uap deliver" | while read p; do kill -9 $p; done`). Reading
200
+ tokens from the blanked view lost exactly that form.
201
+
202
+ Statement counts are compared before pairing the two views; if blanking
203
+ changed the shape (a heredoc removed lines), fall back to the raw text for
204
+ both, which is the conservative direction.
205
+ """
206
+ raw = _expand_assignments(cmd)
207
+ scannable = _expand_assignments(scannable_command(cmd))
208
+ raw_stmts = _STATEMENT_SPLIT_RE.split(raw)
209
+ kill_stmts = _STATEMENT_SPLIT_RE.split(scannable)
210
+ if len(kill_stmts) != len(raw_stmts):
211
+ kill_stmts = raw_stmts
212
+
213
+ kills = [bool(_KILL_VERB_RE.search(s)) for s in kill_stmts]
214
+ for is_kill, stmt in zip(kills, raw_stmts):
215
+ if is_kill and (_INFRA_TOKEN_RE.search(stmt) or _PORT_LOOKUP_RE.search(stmt)):
216
+ return True
217
+ if _KILL_PATTERN_PORT_RE.search(raw):
218
+ return True
219
+ # Cross-statement: a lookup that NAMES the stack, and a kill anywhere.
220
+ if any(kills) and any(
221
+ _LOOKUP_VERB_RE.search(s)
222
+ and (_INFRA_TOKEN_RE.search(s) or _PORT_LOOKUP_RE.search(s))
223
+ for s in raw_stmts
224
+ ):
225
+ return True
226
+ return False
227
+
228
+
229
+ # --------------------------------------------------------------------------
230
+ # 9) Kill by BARE PID.
231
+ #
232
+ # `kill -9 3936358` names nothing, so no text rule can see what it hits. The
233
+ # model killed its own in-flight deliver this way six times in one hour on
234
+ # 2026-07-31, discarding each run's completed work — the direct cause of ~50
235
+ # minutes spent in a ps/sleep/kill loop making no progress.
236
+ #
237
+ # Resolution is SEMANTIC, not textual, so it cannot be spelled around: the
238
+ # numbers the command names are looked up in /proc and compared against the
239
+ # deliver lock and the stack's argv. Only the PIDs the command actually names
240
+ # are resolved (never a full /proc walk), which is both cheaper and narrower.
241
+ #
242
+ # A dead PID is never protected, and a lock PID must ALSO still look like a
243
+ # deliver run: a crashed run leaves a stale lock, and without the identity check
244
+ # whatever process later recycles that number becomes unkillable while the
245
+ # refusal says "wait for the deliver run" — recreating the exact stall this rule
246
+ # exists to prevent. delivery_enforcement._deliver_lock_holder() has taken the
247
+ # same precaution since the PID-reuse incident; this must not diverge from it.
248
+ _PID_TOKEN_RE = re.compile(r"(?<![\w.])(-?\d{1,10})(?![\w.-])")
249
+ _STACK_ARGV_RE = re.compile(
250
+ r"(llama-server|anthropic_proxy|nomic-embed"
251
+ r"|\buap\s+deliver\b|(?:cli\.js|uap)\s+(?:\S+\s+)*deliver\b)",
252
+ re.IGNORECASE,
253
+ )
254
+ # A pathological command full of integers must not become a syscall storm.
255
+ _MAX_PID_CANDIDATES = 32
256
+
257
+
258
+ def _lock_holder_pids() -> dict[str, str]:
259
+ """PIDs claimed by a deliver lock, from the main root AND any worktree.
260
+
261
+ deliver writes its lock under the root it was LAUNCHED from, which under
262
+ this repo's mandated worktree workflow is often `.worktrees/NNN-*/`, while
263
+ the gate resolves repo_root() to the main checkout. Reading only the main
264
+ root would leave rule 9's headline case unprotected in the normal workflow.
265
+ """
266
+ holders: dict[str, str] = {}
267
+ roots = [repo_root()]
268
+ try:
269
+ roots.extend(sorted((repo_root() / ".worktrees").glob("*")))
270
+ except Exception: # noqa: BLE001 - no worktrees dir: main root only
271
+ pass
272
+ for root in roots:
273
+ try:
274
+ text = (root / ".uap" / "deliver.lock").read_text(errors="replace")
275
+ except OSError:
276
+ continue
277
+ m = re.match(r"\s*(\d+)", text)
278
+ if m:
279
+ holders[str(int(m.group(1)))] = "the deliver run in progress"
280
+ return holders
281
+
282
+
283
+ def _identify_pid(pid: str, holders: dict[str, str]) -> str | None:
284
+ """What `pid` actually IS right now, or None if it is nothing to protect."""
285
+ try:
286
+ argv = (
287
+ Path(f"/proc/{pid}/cmdline")
288
+ .read_bytes()
289
+ .replace(b"\0", b" ")
290
+ .decode(errors="replace")
291
+ )
292
+ except OSError:
293
+ return None # dead: a stale lock protects nothing
294
+ m = _STACK_ARGV_RE.search(argv)
295
+ if pid in holders:
296
+ # Confirm identity too — a recycled PID must not inherit the claim.
297
+ return holders[pid] if m else None
298
+ return m.group(1).lower() if m else None
299
+
300
+
301
+ def _protected_pid_hit(text: str) -> tuple[str, str] | None:
302
+ if not (_KILL_VERB_RE.search(text) and _PID_TOKEN_RE.search(text)):
303
+ return None
304
+ holders = _lock_holder_pids()
305
+ seen: set[str] = set()
306
+ for m in _PID_TOKEN_RE.finditer(text):
307
+ # `kill -9 -3936358` kills the process GROUP — strictly more
308
+ # destructive, and invisible if the sign is treated as part of the token.
309
+ pid = str(abs(int(m.group(1))))
310
+ if pid in seen:
311
+ continue
312
+ seen.add(pid)
313
+ if len(seen) > _MAX_PID_CANDIDATES:
314
+ break
315
+ what = _identify_pid(pid, holders)
316
+ if what:
317
+ return pid, what
318
+ return None
319
+
320
+
321
+ DELIVER_PID_REASON = (
322
+ "infra-protect: this command kills the deliver run in progress (pid {pid}). "
323
+ "A deliver run that is still working is NOT stuck — killing it discards the "
324
+ "work it has already done and starts the cycle over. Wait for it instead: "
325
+ "call the deliver tool with follow:true, which answers within about a "
326
+ "minute; a 'STILL RUNNING' answer is normal and means keep polling, not "
327
+ "fail. From a shell, `uap deliver --await-run` blocks until the run ends. "
328
+ "Operator override: set UAP_INFRA_PROTECT_OFF=1 in the launch environment "
329
+ "(not inline on the command)."
330
+ )
331
+
332
+ STACK_PID_REASON = (
333
+ "infra-protect: this command kills {what} (pid {pid}) — the inference stack "
334
+ "answering this session's own requests (llama-server :8080 / UAP proxy "
335
+ ":4000 / embeddings :8081). Killing it ends your own session; it is not a "
336
+ "way to fix a slow response. If the stack genuinely needs restarting, ask "
337
+ "the operator. Operator override: set UAP_INFRA_PROTECT_OFF=1 in the launch "
338
+ "environment (not inline on the command)."
339
+ )
340
+
93
341
  REASON = (
94
342
  "infra-protect: this command would kill or displace the inference stack this "
95
343
  "session runs on (llama-server :8080 / UAP proxy :4000 / embeddings :8081). "
@@ -175,11 +423,24 @@ def main() -> None:
175
423
  for rule in RULES:
176
424
  if rule.search(cmd):
177
425
  emit(False, REASON)
178
- # Same rules, applied to the body of any script the command would run.
179
- for path, body in _referenced_script_bodies(cmd):
180
- for rule in RULES:
181
- if rule.search(body):
182
- emit(False, f"{REASON} (matched inside the invoked script {path})")
426
+ # Rules 8 and 9 apply to the command AND to any script it would run — the
427
+ # script-body pass exists because moving a stack-restart one level down into
428
+ # a file slipped past every text rule once already, and a rule that skips
429
+ # that pass reopens exactly that hole.
430
+ for label, text in [("", cmd), *((f" (matched inside the invoked script {p})", b)
431
+ for p, b in _referenced_script_bodies(cmd))]:
432
+ if text is not cmd:
433
+ for rule in RULES:
434
+ if rule.search(text):
435
+ emit(False, f"{REASON}{label}")
436
+ if _laundered_infra_kill(text):
437
+ emit(False, f"{REASON}{label}")
438
+ hit = _protected_pid_hit(text)
439
+ if hit:
440
+ pid, what = hit
441
+ if what == "the deliver run in progress":
442
+ emit(False, DELIVER_PID_REASON.format(pid=pid) + label)
443
+ emit(False, STACK_PID_REASON.format(what=what, pid=pid) + label)
183
444
  emit(True, "no infra-destructive pattern")
184
445
 
185
446
 
@@ -12,7 +12,9 @@ which drives a model to verified completion against the project's real gates
12
12
  (build, type-check, tests) rather than ad-hoc hand edits.
13
13
 
14
14
  The enforcer fires on `Edit` / `Write` / `MultiEdit` operations targeting
15
- source-code files. It is satisfied when any of the following holds:
15
+ source-code files, and on `Bash` commands that write source through the shell
16
+ (a redirect, heredoc, `tee` or `sed -i`), launch a GUI browser, or destroy the
17
+ gate's own state (see below). It is satisfied when any of the following holds:
16
18
 
17
19
  - the edit runs inside a deliver-driven context (`UAP_DELIVER_ACTIVE=1`),
18
20
  - an explicit operator override is set (`UAP_DELIVER_BYPASS=1`),
@@ -43,3 +45,19 @@ of blocking.
43
45
  Exempt by construction: non-source files; `docs/`, `scripts/`, `policies/`,
44
46
  `src/policies/`, test files (deliver protects those itself); and tooling
45
47
  dot-dirs (`.claude/`, `.uap/`, `.worktrees/`, …).
48
+
49
+ **One carve-out from that dot-dir exemption: the gate's own state.** Destroying
50
+ `.uap/pending-deliver.jsonl`, `.uap/deliver.lock` or `.uap/deliver.heartbeat` —
51
+ by `rm`, `unlink`, `shred`, `truncate`, `mv`, `find -delete`, `git clean`, a
52
+ truncating `>` redirect, or removing `.uap/` itself — is blocked. The pending log
53
+ is the replay queue for `uap deliver --pending`, so deleting it discards recorded
54
+ work rather than completing it; the lock and heartbeat are how an in-flight run
55
+ is found, followed, and reclaimed when wedged, so deleting them starts a second
56
+ concurrent run on the same tree. Observed live 7x on 2026-07-31
57
+ (octopus_invaders_v3), interleaved with `kill -9` of the running deliver.
58
+
59
+ Unlike an edit, this block is **not** relaxed by `UAP_ENFORCE_DELIVERY=advisory`:
60
+ advisory trades verification for speed on a change, but destroying recorded state
61
+ has no verified-later equivalent. deliver's own housekeeping is unaffected — it
62
+ rewrites the pending log in-process, and its subprocesses carry
63
+ `UAP_DELIVER_ACTIVE=1`.
@@ -23,9 +23,38 @@ the session runs on** is blocked:
23
23
  `vite --port 4000`, …) — this is how the port gets stolen even when kills are
24
24
  blocked, during the service's crash/restart window.
25
25
 
26
+ - A broad `-f`/`--full` kill whose pattern is a substring of the stack's own
27
+ argv (`uap`, `llama`, `qwen`, `mmproj`, `nomic`, `anthropic`), or a glob over
28
+ the python interpreter that runs the proxy (`pkill -f "python.*"`).
29
+ - A kill held **apart** from its target, however the two are separated: by a
30
+ pipe (`ps aux | grep llama-server | xargs kill -9`), by an infra-port lookup
31
+ feeding it (`lsof -t -i:4000 | xargs kill -9`), by a variable
32
+ (`P=$(pgrep -f llama-server); kill -9 $P`), by a loop
33
+ (`for p in $(pgrep -f llama-server); do kill -9 $p; done`), or by a `-f`
34
+ pattern that is itself an infra port (`pkill -f 8080`).
35
+ - A kill whose **bare PID** resolves to a deliver run or the inference stack
36
+ (`kill -9 3936358`, and `kill -9 -3936358`, which kills the whole group).
37
+ This is resolved from `.uap/deliver.lock` and `/proc` rather than from the
38
+ command text, so it cannot be spelled around; a dead or recycled PID is never
39
+ protected.
40
+ - The same rules applied to the body of a shell script the command would run —
41
+ moving a stack-restart one level down into a file once slipped past every
42
+ text rule.
43
+
26
44
  **Allowed**: killing a SPECIFIC process by pattern
27
45
  (`pkill -f "python3 -m http.server 8765"`) and serving your app on any non-infra
28
- port (e.g. `8765`).
46
+ port (e.g. `8765`) — **unless the pattern itself carries a stack token**, so
47
+ `pkill -f "uap deliver"` is refused. If a deliver run is in flight, wait for it
48
+ (deliver tool with `follow:true`, or `uap deliver --await-run`) instead of
49
+ killing it: a run that is still working is not stuck, and killing it discards
50
+ the work it has already done.
51
+
52
+ **Known limits.** This is a text gate over a language with unbounded ways to say
53
+ the same thing. Obfuscated patterns (`grep '[l]lama-server'`) match the real
54
+ process but not these rules; anything routed through `eval` or base64 defeats
55
+ every text rule by construction; the script-body scan is depth-1 and shell-only.
56
+ The bare-PID rule is the part that resolves rather than reads. Treat the rest as
57
+ raising the cost, not as a boundary.
29
58
 
30
59
  ## Why
31
60
 
@@ -36,9 +65,24 @@ proxy), `kill $(lsof -t -i:8080)` (killed `llama-server`), then
36
65
  into a systemd bind-failure crash loop and `529`-ing its own session. The model
37
66
  cannot serve requests once it has killed the stack that answers them.
38
67
 
68
+ Observed again (2026-07-31, octopus_invaders_v3): the model spent ~50 minutes in
69
+ a `ps`/`sleep`/`kill` loop, destroying six of its own in-flight `uap deliver`
70
+ runs and restarting them — each kill discarding work the run had already
71
+ completed. When its `pkill -f "uap deliver"` was refused at 11:13:46Z it ran the
72
+ `xargs kill -9` form **eight seconds later** and reported "cleaned". That is why
73
+ the rules above cover the whole family rather than the observed spelling: the
74
+ evasion gets found without being sought, and `SIGKILL` runs no exit handler, so
75
+ none of it appears in `.uap/deliver-exits.log`.
76
+
39
77
  ## Enforcement
40
78
 
41
79
  Python enforcer `enforcement_infra_protect.py` (Bash/run_bash ops only). Emits a
42
80
  specific remediation: kill your own processes by exact pattern and serve on a
43
- port other than `8080`/`4000`/`8081`. Always activethis protects the runtime,
44
- not a per-project opt-in.
81
+ port other than `8080`/`4000`/`8081`, or for a live deliver run wait for it
82
+ rather than killing it.
83
+
84
+ Active by default in every project; this protects the runtime, not a per-project
85
+ opt-in. The only override is `UAP_INFRA_PROTECT_OFF=1` set in the launch
86
+ **environment** by the operator. An inline `UAP_INFRA_PROTECT_OFF=1 <cmd>` is
87
+ deliberately NOT honoured and is separately refused by `enforcement-self-protect`
88
+ — an agent that can grant itself the exemption has no control at all.