@arbiterforge/ca-pi 0.21.4 → 0.21.5

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
@@ -10,7 +10,7 @@ and durable project context. You decide. codeArbiter enforces.
10
10
  <img alt="Claude Code plugin" src="https://img.shields.io/badge/Claude_Code-plugin-d97757">
11
11
  <img alt="Codex plugin" src="https://img.shields.io/badge/OpenAI_Codex-plugin-10a37f">
12
12
  <img alt="Pi Feature Forge preview" src="https://img.shields.io/badge/ca--pi-Feature_Forge_preview-d97757">
13
- <img alt="version 3.2.4" src="https://img.shields.io/badge/version-3.2.4-2b7489">
13
+ <img alt="version 3.2.5" src="https://img.shields.io/badge/version-3.2.5-2b7489">
14
14
  <img alt="core lanes" src="https://img.shields.io/badge/core_lanes-18-555">
15
15
  <img alt="skills" src="https://img.shields.io/badge/skills-22-555">
16
16
  <img alt="agents" src="https://img.shields.io/badge/agents-20-555">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.21.4",
3
+ "version": "0.21.5",
4
4
  "license": "AGPL-3.0-only",
5
5
  "repository": {
6
6
  "type": "git",
@@ -4,6 +4,12 @@ All notable changes to `ca-pi` are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.21.5] - 2026-10-19
8
+
9
+ ### Fixed
10
+
11
+ - The H-05 (audit logs), H-11 (ADRs) and H-18 (`CONTEXT.md`) shell guards blocked any interpreter command that merely named a protected path, so a read-only `python -c` read, an issue body citing the path, or an `ArtifactClient` request whose record text named an audit log was refused as a write. Their interpreter legs now block only when the command shows write intent after the interpreter (a write, delete, copy, move or in-place edit call, a non-read `open` mode, PowerShell content cmdlets, or dynamic execution such as `exec`, `eval` or `subprocess`), or when the interpreter is handed a script or module argument. H-18 also lets a `sed` without `-i` or a `w` command read `CONTEXT.md` (#997, #911).
12
+
7
13
  ## [0.21.4] - 2026-10-19
8
14
 
9
15
  ### Fixed
@@ -37,12 +37,12 @@
37
37
  },
38
38
  "context_qualification": {
39
39
  "darwin/amd64": {
40
- "source_commit": "4e33a9a7ef7ad22eeec2d925305164ccb9179922",
40
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
41
41
  "workflow": ".github/workflows/ci.yml",
42
- "run_id": "39285460301",
42
+ "run_id": "39337812494",
43
43
  "binary_sha256": "e501ca2b817c882feec23bc3560223f20d88ca372400f950679bfe01ba47cebd",
44
- "observation_sha256": "fe57f74d7cda25cf36e845fcb7b74226d0651120f9129bd7abf9e394d10091ca",
45
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
44
+ "observation_sha256": "c832f3aff1d1c2b553173e16bb4fcd5b7ddf1cbf45c2a57e32840f58232c5a7a",
45
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
46
46
  "native_cases": [
47
47
  "TestContextBoundedRender",
48
48
  "TestContextDocumentContract",
@@ -53,12 +53,12 @@
53
53
  ]
54
54
  },
55
55
  "darwin/arm64": {
56
- "source_commit": "4e33a9a7ef7ad22eeec2d925305164ccb9179922",
56
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
57
57
  "workflow": ".github/workflows/ci.yml",
58
- "run_id": "39285460301",
58
+ "run_id": "39337812494",
59
59
  "binary_sha256": "0fe9c4672486a28db1340c77d9eebf37bc33774fc34b5c055728f135afe4d7e8",
60
- "observation_sha256": "0c070e5db7e5296d7492d374ebff59d0da38167d746b29d924fc9508cafaf365",
61
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
60
+ "observation_sha256": "8007bb64505d6f4163df70b58d0a52d950f718caf284846b3a79b9936c759dee",
61
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
62
62
  "native_cases": [
63
63
  "TestContextBoundedRender",
64
64
  "TestContextDocumentContract",
@@ -69,12 +69,12 @@
69
69
  ]
70
70
  },
71
71
  "linux/amd64": {
72
- "source_commit": "4e33a9a7ef7ad22eeec2d925305164ccb9179922",
72
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
73
73
  "workflow": ".github/workflows/ci.yml",
74
- "run_id": "39285460301",
74
+ "run_id": "39337812494",
75
75
  "binary_sha256": "f98d2647144640b226d0fa3fae6369eefbcee477e9d2bb818f767711e6a3e506",
76
- "observation_sha256": "26ca308b11f988606f2ad007560010c863aec29af92d277fb5aa65f80dcdad13",
77
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
76
+ "observation_sha256": "d7c40f95cd3189dee10f26db25d63fbc2891181280efc2403dda058912f80236",
77
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
78
78
  "native_cases": [
79
79
  "TestContextBoundedRender",
80
80
  "TestContextDocumentContract",
@@ -85,12 +85,12 @@
85
85
  ]
86
86
  },
87
87
  "linux/arm64": {
88
- "source_commit": "4e33a9a7ef7ad22eeec2d925305164ccb9179922",
88
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
89
89
  "workflow": ".github/workflows/ci.yml",
90
- "run_id": "39285460301",
90
+ "run_id": "39337812494",
91
91
  "binary_sha256": "7abc7f188851bde8c082c2b812b74378e9e1848541aeb9029ee8551f4b68fdaa",
92
- "observation_sha256": "dcee8721dfb207509ae3e487fa45e73f27ee1cb8740ad6e4b6f157302a21a3f9",
93
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
92
+ "observation_sha256": "1bd885c3ecb6efacb789404d0d97db91596943de630c1cecbcee25482f2bf26f",
93
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
94
94
  "native_cases": [
95
95
  "TestContextBoundedRender",
96
96
  "TestContextDocumentContract",
@@ -101,12 +101,12 @@
101
101
  ]
102
102
  },
103
103
  "windows/amd64": {
104
- "source_commit": "4e33a9a7ef7ad22eeec2d925305164ccb9179922",
104
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
105
105
  "workflow": ".github/workflows/ci.yml",
106
- "run_id": "39285460301",
106
+ "run_id": "39337812494",
107
107
  "binary_sha256": "3fe0a4b867fbcc8a278248f92697b649f4b9b208d51c5168885ba92fcbb73961",
108
- "observation_sha256": "169b76151423856386be0bed088ebde2684fcb46faef49c354cf1e4db77136dc",
109
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
108
+ "observation_sha256": "8439eeadb753215c8e5e983091ff61e1920ac12480d9406e9cc6a837faa7725f",
109
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
110
110
  "native_cases": [
111
111
  "TestContextBoundedRender",
112
112
  "TestContextDocumentContract",
@@ -117,12 +117,12 @@
117
117
  ]
118
118
  },
119
119
  "windows/arm64": {
120
- "source_commit": "4e33a9a7ef7ad22eeec2d925305164ccb9179922",
120
+ "source_commit": "0fc246614c8b0a85188ffcee9284f1942b4b3567",
121
121
  "workflow": ".github/workflows/ci.yml",
122
- "run_id": "39285460301",
122
+ "run_id": "39337812494",
123
123
  "binary_sha256": "339210645436d11bca4129f74d4a7885ad12a9489f449669b5f46a6f9cd1056d",
124
- "observation_sha256": "bcb30a463547d0ee70cccf0063a08c337f371a9f2a206fe7775e9de59c55bb68",
125
- "module_sha256": "896150db84c968920a4397b61b5be1d4d0d3775de7816d172f0bf4885e9e7133",
124
+ "observation_sha256": "062e23804043c352163576ced022881e88e849f1d9b202b14c08958ec9c63ae8",
125
+ "module_sha256": "537452f67f1105572c4fa3829cec4af7354260a96e1614b66daa8fec7eefc256",
126
126
  "native_cases": [
127
127
  "TestContextBoundedRender",
128
128
  "TestContextDocumentContract",
@@ -516,6 +516,147 @@ CONTEXT_INTERP_RE = re.compile(
516
516
  _INTERP_EXECUTABLE + r"[\s\S]*" + CONTEXT_MD, re.I,
517
517
  )
518
518
 
519
+ # #997/#911: the three legs above match an interpreter token followed ANYWHERE
520
+ # by a protected name, so a read-only `python -c "print(open(...).read())"` or
521
+ # an issue body that merely cites the path blocked like a write. They are now a
522
+ # pre-filter: `_interp_protected_write` blocks only when the command shows
523
+ # write intent after the interpreter, or hands the interpreter a script or
524
+ # module argument (script argv stays conservative: its writes are invisible
525
+ # here). Write intent is lexical and deliberately broad; dynamic execution
526
+ # (`exec`, `eval`, `system`, `subprocess`) counts, because it hides the write.
527
+ # The guard stays defense in depth behind the Write/Edit tool guards; a path
528
+ # assembled at runtime (`'.codearbiter/' + 'CONTEXT.md'`) was never seen.
529
+ _PS_WRITE_ALIASES = (r"sc|ac|ni|ri|mi|cpi|rni|ren|clc|del|erase|rd|rmdir|rm|mv|cp"
530
+ r"|move|copy|tee")
531
+ INTERP_WRITE_INTENT_RE = re.compile(
532
+ # Calls whose name carries a write/destroy verb: write_text, writeFileSync,
533
+ # WriteAllText, fwrite, appendFile, os.remove, unlink, shutil.move/copy,
534
+ # rmtree, Path.touch/chmod, os.truncate, rename/replace, File.Delete.
535
+ r"[\w$]*(?:write|append|unlink|remove|rename|replace|truncate|rmtree|rmdir"
536
+ r"|delete|move|copy|symlink|touch|chmod|chown|mkfifo|put_contents|fputs)"
537
+ r"[\w$]*\s*\("
538
+ r"|(?:\b|\.)(?:rm|cp|link)(?:Sync)?\s*\("
539
+ r"|\w*Writer\b|\w*WriteStream\b|\bFileStream\b|\bFileUtils\b"
540
+ r"|::\s*(?:Create\w*|Open(?:Write)?)\s*\("
541
+ # open() with a second positional argument that is not a literal read
542
+ # mode (a write mode, a flag, or a variable), or a non-read mode= keyword.
543
+ r"|\b\w*open(?:Sync)?\s*\(\s*[^,()]+(?:\([^()]*\))?[^,()]*,"
544
+ r"(?!\s*['\"](?:[rbtU]*|<)['\"]\s*[,)]|\s*[A-Za-z_]+\s*=)"
545
+ r"|\bmode\s*=\s*(?!['\"][rbtU]*['\"])"
546
+ r"|\b(?:Set-Content|Add-Content|Out-File|Clear-Content|Remove-Item|Move-Item"
547
+ r"|Copy-Item|Rename-Item|New-Item|Tee-Object|Invoke-Expression|Start-Process"
548
+ r"|Invoke-Command)\b"
549
+ r"|(?:^|[;&|({\n\"'`])\s*(?:" + _PS_WRITE_ALIASES + r")\s"
550
+ r"|\b(?:exec\w*|eval|system|popen|spawn\w*|check_call|check_output|getattr"
551
+ r"|setattr|__import__|compile)\s*\("
552
+ r"|\bsubprocess\b|\bmethodcaller\b|\bDeno\.(?:run|Command)\b|\.Invoke\w*\s*\("
553
+ # Reflection hides the called name: dunders (`os.__dict__['remove']`),
554
+ # vars()/globals(), and a computed member call (`fs['write' + x](...)`).
555
+ r"|__\w+__|\b(?:vars|globals|locals)\s*\(|\]\s*\("
556
+ # A write-mode literal passed or assigned anywhere: `o = open; o(p, 'w')`.
557
+ r"|(?<=[,(=\s])['\"][rbtU]*[wax+][rbtUwax+]*['\"](?=\s*[,);\]])",
558
+ re.I,
559
+ )
560
+ _INTERP_ARG_RE = re.compile(r"\s+([^\s|;&]+)")
561
+ # Words that precede a command without ending a shell word list.
562
+ _COMMAND_PREFIXES = frozenset({"env", "sudo", "call", "exec", "nohup", "time",
563
+ "command", "xargs", "then", "do", "else", "&"})
564
+
565
+
566
+ def _interp_operand(cmd, end):
567
+ """Classify what an interpreter token ending at `end` is handed:
568
+ 'inline' (an inline-code switch or a heredoc), 'stdin' (`-`, code that
569
+ may arrive from a pipe), 'script' (a positional argument or `<` input
570
+ shaped like a path), 'word' (any other positional argument, such as a
571
+ `-m` module or a dotless script name) or 'bare' (nothing before the
572
+ command ends)."""
573
+ rest = cmd[end:]
574
+ suffix = re.match(r"[\w.-]*[\"'`]?", rest)
575
+ pos = suffix.end()
576
+ while True:
577
+ m = _INTERP_ARG_RE.match(rest, pos)
578
+ if not m:
579
+ return "bare"
580
+ token = m.group(1).rstrip("\"'`")
581
+ inline = re.match(_INTERP_INLINE_CODE, token, re.I)
582
+ if inline and inline.end() == len(token):
583
+ return "inline"
584
+ if token.startswith("<<"):
585
+ return "inline"
586
+ if token == "-":
587
+ return "stdin"
588
+ if not token.startswith("-"):
589
+ return "script" if re.search(r"[./\\]", token) else "word"
590
+ pos = m.end()
591
+
592
+
593
+ def _at_command_position(cmd, start):
594
+ """True when the interpreter token at `start` sits where a command starts,
595
+ rather than inside prose such as "each node in the graph"."""
596
+ before = re.sub(r"[^\s;&|()`\"'{}]*$", "", cmd[:start]).rstrip()
597
+ if not before or before[-1] in ";&|({\n`\"'" or before.endswith("$("):
598
+ return True
599
+ word = before.split()[-1]
600
+ return word.lower() in _COMMAND_PREFIXES or "=" in word
601
+
602
+
603
+ def _interp_protected_write(cmd, interp_re, name_re):
604
+ """True when an interpreter invocation can write the protected `name_re`
605
+ target: `interp_re` (token then name) matched, and after some interpreter
606
+ token that precedes the name either write intent appears, or the code it
607
+ runs is not visible on the command line (a script, module or file, piped
608
+ stdin, or a dotless argument in command position)."""
609
+ if not interp_re.search(cmd):
610
+ return False
611
+ names = [m.start() for m in name_re.finditer(cmd)]
612
+ for m in re.finditer(_INTERP_EXECUTABLE, cmd, re.I):
613
+ if not any(n >= m.end() for n in names):
614
+ continue
615
+ # Perl and Ruby calls need no parentheses (`open F, ">", p`,
616
+ # `File.write p, ""`), which the lexical intent scan cannot see, so
617
+ # they keep the conservative pre-#997 behavior.
618
+ if m.group(1).lower() in ("perl", "ruby"):
619
+ return True
620
+ if INTERP_WRITE_INTENT_RE.search(cmd, m.start()):
621
+ return True
622
+ operand = _interp_operand(cmd, m.end())
623
+ if operand == "script":
624
+ return True
625
+ if operand == "stdin" and "<<" not in cmd[m.end():]:
626
+ return True
627
+ if operand in ("word", "bare") and _at_command_position(cmd, m.start()):
628
+ return True
629
+ return False
630
+
631
+
632
+ LOG_NAME_RE = re.compile(LOG_NAMES, re.I)
633
+ DECISIONS_NAME_RE = re.compile(DECISIONS, re.I)
634
+ CONTEXT_NAME_RE = re.compile(CONTEXT_MD, re.I)
635
+
636
+ # #997: `sed` writes a file only in place (`-i`, `--in-place`) or through a
637
+ # `w`/`W` script command; `sed -n 1,20p .codearbiter/CONTEXT.md` is a read.
638
+ _SED_WRITES_RE = re.compile(
639
+ r"(?<![\w-])-[a-zA-Z]*i|--in-place|(?:^|[\s'\";{}/\d$,])[wW]\s+\S")
640
+
641
+
642
+ def _context_shell_write(cmd):
643
+ """CONTEXT_WRITE_RE, except a `sed` invocation that cannot write."""
644
+ text = cmd
645
+ while True:
646
+ m = CONTEXT_WRITE_RE.search(text)
647
+ if not m:
648
+ return False
649
+ if m.group(1).lower() != "sed":
650
+ return True
651
+ # GNU sed accepts options after operands (`sed s/a/b/ FILE -i`), so
652
+ # scan the whole command segment, not only the text before the path.
653
+ segment = re.match(r"[^|;&]*", text[m.end(1):]).group(0)
654
+ if _SED_WRITES_RE.search(segment):
655
+ return True
656
+ # Blank this read-only `sed` and rescan, so another verb later in the
657
+ # same segment is still seen.
658
+ text = text[:m.start(1)] + " " + text[m.end(1):]
659
+
519
660
  # H-22's shell flank: the protected-state registry (B1/#564) — Write/Edit are
520
661
  # guarded via classify_protected's "state" class (pre-write.py/pre-edit.py,
521
662
  # T-05a/T-06/T-07); this closes the shell flank the SAME way the four
@@ -1430,7 +1571,8 @@ def _check_h05_audit_log(cmd):
1430
1571
  if enrolled_name and (
1431
1572
  LOG_TRUNC_RE.search(cmd) or LOG_DESTROY_RE.search(cmd)
1432
1573
  or LOG_GIT_RESTORE_RE.search(cmd)
1433
- or LOG_INTERP_RE.search(cmd) and not _is_direct_taskwrite(cmd)):
1574
+ or _interp_protected_write(cmd, LOG_INTERP_RE, LOG_NAME_RE)
1575
+ and not _is_direct_taskwrite(cmd)):
1434
1576
  block("H-05", "The .codearbiter audit logs (overrides.log, triage.log, sprint-log.md, "
1435
1577
  "gate-events.log, decisions/decision-log.md) are append-only "
1436
1578
  "(ORCHESTRATOR §7). Truncating, overwriting, or deleting the audit "
@@ -1457,7 +1599,7 @@ def _check_h11_decisions(cmd):
1457
1599
  cmd = DECISION_LOG_SHELL_RE.sub(" ", cmd)
1458
1600
  cmd = ADR_LIFECYCLE_LOG_SHELL_RE.sub(" ", cmd)
1459
1601
  if (DECISIONS_REDIRECT_RE.search(cmd) or DECISIONS_WRITE_RE.search(cmd)
1460
- or DECISIONS_INTERP_RE.search(cmd)):
1602
+ or _interp_protected_write(cmd, DECISIONS_INTERP_RE, DECISIONS_NAME_RE)):
1461
1603
  block("H-11", "ADR files under .codearbiter/decisions/ are authored only via "
1462
1604
  "/adr and are immutable history (ORCHESTRATOR §6) — shell writes, "
1463
1605
  "edits, and deletions there are prohibited.")
@@ -1469,9 +1611,13 @@ def _check_h18_context_md(cmd):
1469
1611
  Write tool, so nothing legitimate is blocked. Reads pass.
1470
1612
 
1471
1613
  #574: CONTEXT_INTERP_RE closes the interpreter-one-liner flank the same
1472
- way LOG_INTERP_RE/DECISIONS_INTERP_RE do for H-05/H-11."""
1473
- if (CONTEXT_REDIRECT_RE.search(cmd) or CONTEXT_WRITE_RE.search(cmd)
1474
- or CONTEXT_INTERP_RE.search(cmd)):
1614
+ way LOG_INTERP_RE/DECISIONS_INTERP_RE do for H-05/H-11.
1615
+
1616
+ #997: reads pass on both flanks. `sed` without in-place or a `w` command
1617
+ is a read, and an interpreter needs write intent or a script argument
1618
+ (`_interp_protected_write`)."""
1619
+ if (CONTEXT_REDIRECT_RE.search(cmd) or _context_shell_write(cmd)
1620
+ or _interp_protected_write(cmd, CONTEXT_INTERP_RE, CONTEXT_NAME_RE)):
1475
1621
  block("H-18", ".codearbiter/CONTEXT.md is the activation switch every enforcement "
1476
1622
  "hook reads (#159) — shell rewrites, edits, or deletions that could flip "
1477
1623
  "`arbiter: enabled` off or corrupt its frontmatter are prohibited. Edit it "
@@ -11,7 +11,7 @@ import hostapi # noqa: E402
11
11
  class PiHost(hostapi.Host):
12
12
  name = "pi"
13
13
  adapter_name = "@arbiterforge/ca-pi"
14
- adapter_version = "0.21.4"
14
+ adapter_version = "0.21.5"
15
15
  update_target = "ca-pi"
16
16
  update_tag_prefix = "ca-pi-v"
17
17
  update_command = "pi update npm:@arbiterforge/ca-pi"
@@ -313,7 +313,7 @@ class Host:
313
313
 
314
314
  name = "claude"
315
315
  adapter_name = "ca"
316
- adapter_version = "3.2.4"
316
+ adapter_version = "3.2.5"
317
317
 
318
318
  # Update-notifier descriptor. Each independently versioned host overrides
319
319
  # these three values in its per-plugin _host.py. Keeping the target,
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@arbiterforge/ca-pi",
3
- "version": "0.21.4",
3
+ "version": "0.21.5",
4
4
  "private": true,
5
5
  "license": "AGPL-3.0-only",
6
6
  "type": "module",