@caddis/cli 0.4.76 → 0.4.78
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/bundles/antigravity-plugin/guard_agy.py +131 -1
- package/bundles/antigravity-plugin/plugin.json +1 -1
- package/bundles/antigravity-plugin/scripts/caddis_temp_prune.py +1 -0
- package/bundles/manifest.json +2 -2
- package/dist/claude-deepseek.js +29 -1
- package/dist/claude-glm.js +29 -1
- package/dist/claude-oss.js +29 -1
- package/dist/cli.js +3051 -372
- package/dist/ui/assets/favicon-DtHQyAC1.svg +8 -0
- package/dist/ui/assets/favicon-dot-DHAVYP15.svg +9 -0
- package/dist/ui/assets/index-AaPwxAoF.js +220 -0
- package/dist/ui/assets/index-DWiypj6N.css +1 -0
- package/dist/ui/assets/mark-small-CwCIyjyC.svg +8 -0
- package/dist/ui/assets/wordmark-CD0s2ofS.svg +99 -0
- package/dist/ui/fonts/OFL-Atkinson.txt +93 -0
- package/dist/ui/fonts/OFL-MapleMono.txt +93 -0
- package/dist/ui/fonts/atkinson-400.849ae6f0.woff2 +0 -0
- package/dist/ui/fonts/atkinson-700.3a9cc18a.woff2 +0 -0
- package/dist/ui/fonts/maple-mono-400.071799fd.woff2 +0 -0
- package/dist/ui/fonts/maple-mono-700.1e17b237.woff2 +0 -0
- package/dist/ui/index.html +14 -0
- package/package.json +13 -3
|
@@ -37,6 +37,7 @@ looks like an edit/write and whose args carry a path. Unknown tools -> allow.
|
|
|
37
37
|
"""
|
|
38
38
|
from __future__ import annotations
|
|
39
39
|
|
|
40
|
+
import fnmatch
|
|
40
41
|
import json
|
|
41
42
|
import math
|
|
42
43
|
import os
|
|
@@ -78,6 +79,9 @@ def classify_write(file_path: str) -> tuple[str, str]:
|
|
|
78
79
|
return "deny", f"writes an environment/secret file ({base})"
|
|
79
80
|
if base in _SECRET_BASENAMES or any(base.endswith(s) for s in _SECRET_SUFFIXES):
|
|
80
81
|
return "deny", f"writes a credential/key file ({base})"
|
|
82
|
+
why = hub_dir_reason(file_path)
|
|
83
|
+
if why:
|
|
84
|
+
return "deny", why
|
|
81
85
|
|
|
82
86
|
why = guard_config_reason(file_path)
|
|
83
87
|
if why:
|
|
@@ -126,6 +130,114 @@ def guard_config_reason(path: str) -> str:
|
|
|
126
130
|
return ""
|
|
127
131
|
|
|
128
132
|
|
|
133
|
+
# ── the hub folder (hub design 7a) ───────────────────────────────────────────
|
|
134
|
+
# `~/.caddis/hub/**` (and `$CADDIS_HOME/.caddis/hub/**` when CADDIS_HOME is set) holds the hub key, the repo registry,
|
|
135
|
+
# the owner requests and the audit log. An agent that writes there could forge an owner request, so a write by tool or
|
|
136
|
+
# through the shell (redirection, tee, cp/mv, Set-Content, Out-File, rm, Remove-Item, ...) denies. Reads stay free.
|
|
137
|
+
# A run of `python .../caddis_coordinate.py` stays free: the script writes, not a shell redirection. Known limits (it
|
|
138
|
+
# is not a boundary): an interpreter one-liner, a script that writes the file, a variable other than the home ones.
|
|
139
|
+
# Copied from hooks/guard.py; test_guard_agy.py checks they agree.
|
|
140
|
+
_WHY_HUB = "writes the caddis hub folder (~/.caddis/hub); only the hub and caddis_coordinate.py write there"
|
|
141
|
+
# CADDIS_HOME as a shell spells it. It becomes the marker below before the tokenizer splits the text on `$`.
|
|
142
|
+
_CADDIS_HOME_VAR = re.compile(r"\$\{?env:caddis_home\}?|\$\{caddis_home\}|\$caddis_home\b|%caddis_home%", re.I)
|
|
143
|
+
_CADDIS_HOME_MARK = "~caddis-home"
|
|
144
|
+
_HUB_DELETERS = frozenset({"rm", "del", "erase", "rmdir", "rd", "unlink", "shred", "truncate", "remove-item", "ri",
|
|
145
|
+
"clear-content", "clc"})
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _hub_dirs() -> list[str]:
|
|
149
|
+
"""The hub folders to protect: under the user home, and under CADDIS_HOME when it is set."""
|
|
150
|
+
homes = [os.path.expanduser("~")]
|
|
151
|
+
if os.environ.get("CADDIS_HOME"):
|
|
152
|
+
homes.append(os.environ["CADDIS_HOME"])
|
|
153
|
+
return [os.path.normcase(os.path.realpath(os.path.join(h, _ARTIFACT_DIR, "hub"))) for h in homes]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
# A device prefix that Windows removes before it opens the path: `\\?\`, `\\.\`, `\\?\UNC\`, `\??\` (either slash).
|
|
157
|
+
_HUB_DEVICE_PREFIX = re.compile(r"^[\\/]{2}[?.][\\/](?:unc[\\/])?|^[\\/]\?\?[\\/]", re.I)
|
|
158
|
+
_HUB_GLOB = re.compile(r"[*?\[]")
|
|
159
|
+
# A `$` just before a separator is never a variable: an admin share (`\\localhost\c$\...`). The tokenizer
|
|
160
|
+
# splits on `$`, so the target finders drop it first.
|
|
161
|
+
_SHARE_DOLLAR = re.compile(r"\$(?=[\\/])")
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def _hub_clean(raw: str) -> str:
|
|
165
|
+
"""``raw`` spelled the way Windows opens it, with `/` separators: no device prefix, each segment cut at its first
|
|
166
|
+
`:` (an NTFS stream such as `::$DATA` or `::$INDEX_ALLOCATION`; a drive letter stays), and trailing dots and
|
|
167
|
+
spaces removed from each segment (`hub.` and `hub ` open `hub`)."""
|
|
168
|
+
raw = _HUB_DEVICE_PREFIX.sub("", raw)
|
|
169
|
+
lead = re.match(r"[\\/]*", raw).group(0)
|
|
170
|
+
segs = []
|
|
171
|
+
for i, seg in enumerate(re.split(r"[\\/]+", raw[len(lead):])):
|
|
172
|
+
drive = seg[:2] if i == 0 and not lead and re.match(r"[A-Za-z]:", seg) else ""
|
|
173
|
+
seg = drive + seg[len(drive):].split(":", 1)[0]
|
|
174
|
+
trimmed = seg.rstrip(" .")
|
|
175
|
+
segs.append(trimmed if trimmed or seg.strip(" ") not in (".", "..") else seg.strip(" "))
|
|
176
|
+
return lead.replace("\\", "/") + "/".join(segs)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def _hub_parts(clean: str) -> list[str]:
|
|
180
|
+
"""The lower-case segments of a _hub_clean path, with `.` and `..` applied the way Windows does (by text)."""
|
|
181
|
+
out: list[str] = []
|
|
182
|
+
for seg in clean.lower().split("/"):
|
|
183
|
+
if seg == "..":
|
|
184
|
+
out = out[:-1]
|
|
185
|
+
elif seg and seg != ".":
|
|
186
|
+
out.append(seg)
|
|
187
|
+
return out
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _hub_seg_is(seg: str, name: str, shell: bool) -> bool:
|
|
191
|
+
"""``seg`` names ``name``. In a shell, a glob segment (`hu*`, `h?b`, `[h]ub`) names it when it matches; as in
|
|
192
|
+
bash, only a glob that starts with `.` matches a dot name."""
|
|
193
|
+
if seg == name:
|
|
194
|
+
return True
|
|
195
|
+
return (shell and bool(_HUB_GLOB.search(seg)) and (seg.startswith(".") or not name.startswith("."))
|
|
196
|
+
and fnmatch.fnmatchcase(name, seg))
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _hub_spellings(raw: str, shell: bool) -> list[str]:
|
|
200
|
+
"""The paths a shell can open for ``raw``: quotes removed, then `\\` read as a separator (cmd, PowerShell) and
|
|
201
|
+
`\\` or a backtick read as an escape (bash, PowerShell). A tool path is opened as it is."""
|
|
202
|
+
if not shell:
|
|
203
|
+
return [raw]
|
|
204
|
+
unquoted = raw.replace('"', "").replace("'", "")
|
|
205
|
+
return [unquoted, re.sub(r"[\\`](.)", r"\1", unquoted)]
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def hub_dir_reason(path: str, shell: bool = False) -> str:
|
|
209
|
+
"""The deny reason when ``path`` is the hub folder or a file in it, else ''. ``shell`` is True for a path that a
|
|
210
|
+
shell reads (quotes, escapes and globs apply). A path with a `.caddis` segment and then a `hub` segment denies
|
|
211
|
+
whatever its root (a device prefix, an admin share such as `\\\\localhost\\c$`, another home): the backstop."""
|
|
212
|
+
if not isinstance(path, str):
|
|
213
|
+
return ""
|
|
214
|
+
raw = _CADDIS_HOME_VAR.sub(_CADDIS_HOME_MARK, path.strip().strip("'\""))
|
|
215
|
+
for spelling in _hub_spellings(raw, shell):
|
|
216
|
+
clean = _hub_clean(_HOME_VAR.sub("~", spelling))
|
|
217
|
+
parts = _hub_parts(clean)
|
|
218
|
+
hubs = [i for i, seg in enumerate(parts) if _hub_seg_is(seg, "hub", shell)]
|
|
219
|
+
if any(i and _hub_seg_is(parts[i - 1], _ARTIFACT_DIR, shell) for i in hubs):
|
|
220
|
+
return _WHY_HUB
|
|
221
|
+
if not hubs or clean.startswith("//"): # cheap test first; never resolve a UNC path (a network call)
|
|
222
|
+
continue
|
|
223
|
+
if clean.lower().startswith(_CADDIS_HOME_MARK):
|
|
224
|
+
clean = (os.environ.get("CADDIS_HOME") or "~") + clean[len(_CADDIS_HOME_MARK):]
|
|
225
|
+
full = os.path.normcase(os.path.realpath(os.path.abspath(os.path.normpath(_home_path(clean)))))
|
|
226
|
+
for hub in _hub_dirs():
|
|
227
|
+
if full == hub or full.startswith(hub.rstrip(os.sep) + os.sep):
|
|
228
|
+
return _WHY_HUB
|
|
229
|
+
return ""
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _hub_delete_targets(command: str) -> list[str]:
|
|
233
|
+
"""The paths that rm / del / Remove-Item / Clear-Content name in ``command`` (their non-option words)."""
|
|
234
|
+
command = _SHARE_DOLLAR.sub("", _CADDIS_HOME_VAR.sub(_CADDIS_HOME_MARK, _HOME_VAR.sub("~", command)))
|
|
235
|
+
out: list[str] = []
|
|
236
|
+
for _name, _cwd, _sub, args in _tool_calls(command, None, _HUB_DELETERS):
|
|
237
|
+
out += [a for a in args if not a.startswith(("-", ">", "<"))] # cmd flags (`/s`) never name the hub
|
|
238
|
+
return out
|
|
239
|
+
|
|
240
|
+
|
|
129
241
|
# Copied from hooks/guard.py: the Read rule, used by the shell secret-read rule below.
|
|
130
242
|
def classify_read(file_path: str) -> tuple[str, str]:
|
|
131
243
|
"""Risk tier (deny|allow) + reason for reading ``file_path`` via the Read tool.
|
|
@@ -483,7 +595,8 @@ def _shell_write_targets(command: str) -> list[str]:
|
|
|
483
595
|
targets of the writer programs the config rule knows (cp, mv, Copy-Item, Set-Content, New-Item, Tee-Object, sed -i,
|
|
484
596
|
...). Not the regex's `-Path` group: `Get-Content -Path x` reads. Best-effort by design: a miss degrades to the old
|
|
485
597
|
behaviour, it never invents a denial. guard_agy.py carries a copy; test_guard_agy.py checks they agree."""
|
|
486
|
-
|
|
598
|
+
# The tokenizer splits on `$`, so in `$HOME/x` the home part would be lost.
|
|
599
|
+
command = _SHARE_DOLLAR.sub("", _CADDIS_HOME_VAR.sub(_CADDIS_HOME_MARK, _HOME_VAR.sub("~", command)))
|
|
487
600
|
out = [m.group(g).strip("'\"") for m in _SHELL_WRITE_TARGET.finditer(command)
|
|
488
601
|
for g in ("redir", "tee", "outfile") if m.group(g)]
|
|
489
602
|
try:
|
|
@@ -556,6 +669,18 @@ def _copier_targets(args: list[str]) -> list[str]:
|
|
|
556
669
|
return [dest] + [os.path.join(dest, os.path.basename(src.replace("\\", "/"))) for src in sources]
|
|
557
670
|
|
|
558
671
|
|
|
672
|
+
def _hub_delete_safe(text: str) -> str:
|
|
673
|
+
"""The deny reason when a delete command (rm, Remove-Item, ...) names the hub folder, else ''. An error is
|
|
674
|
+
"no match": one rule must never switch off the rest."""
|
|
675
|
+
try:
|
|
676
|
+
for target in _hub_delete_targets(text):
|
|
677
|
+
if hub_dir_reason(target, shell=True):
|
|
678
|
+
return f"shell command deletes in the caddis hub folder (~/.caddis/hub); {_WHY_HUB.split('; ', 1)[1]}"
|
|
679
|
+
except Exception: # noqa: BLE001 - fail open for this rule only; the other rules still run
|
|
680
|
+
return ""
|
|
681
|
+
return ""
|
|
682
|
+
|
|
683
|
+
|
|
559
684
|
def guard_config_shell_reason(command: str) -> str:
|
|
560
685
|
"""The ask reason when a shell command writes a `.caddis/config.toml` or `~/.caddis/*.toml`, else ''.
|
|
561
686
|
Callers pass heredoc-free text and treat an exception as "no match"."""
|
|
@@ -1181,9 +1306,14 @@ def classify_bash(command: str, root: str | None = None) -> tuple[str, str]:
|
|
|
1181
1306
|
# ── deny: writing a secret through the shell (copied from hooks/guard.py) ──
|
|
1182
1307
|
# The same file must not be denied via a write tool and waved through via `echo ... > .env`.
|
|
1183
1308
|
for _target in _shell_write_targets(data_free):
|
|
1309
|
+
if hub_dir_reason(_target, shell=True): # the shell reads quotes, escapes and globs; a tool path does not
|
|
1310
|
+
return "deny", f"shell command {_WHY_HUB}"
|
|
1184
1311
|
_tier, _why = classify_write(_target)
|
|
1185
1312
|
if _tier == "deny":
|
|
1186
1313
|
return "deny", f"shell command {_why}"
|
|
1314
|
+
_hub = _hub_delete_safe(data_free)
|
|
1315
|
+
if _hub:
|
|
1316
|
+
return "deny", _hub
|
|
1187
1317
|
|
|
1188
1318
|
# ── deny: an inline python/node/etc. one-liner that touches a secret env file ──
|
|
1189
1319
|
if _INLINE_ENV.search(c):
|
|
@@ -55,6 +55,7 @@ CADDIS_TEMP_PREFIXES = {
|
|
|
55
55
|
"caddis-codex-": "cli/test/codex.test.ts mkdtempSync",
|
|
56
56
|
"caddis-doctor-home-": "cli/test/doctor.test.ts mkdtempSync",
|
|
57
57
|
"caddis-doctor-sessions-": "cli/test/doctor.test.ts mkdtempSync",
|
|
58
|
+
"caddis-hub-": "cli/test/hub/*.test.ts mkdtempSync",
|
|
58
59
|
"caddis-keys-": "cli/test/providers.test.ts mkdtempSync",
|
|
59
60
|
"caddis-sessions-": "cli/test/sessions.test.ts mkdtempSync",
|
|
60
61
|
"caddis-stub-": "cli/test/stub.test.ts mkdtempSync",
|
package/bundles/manifest.json
CHANGED
package/dist/claude-deepseek.js
CHANGED
|
@@ -467,6 +467,34 @@ var theme = {
|
|
|
467
467
|
isColorSupported: import_picocolors.default.isColorSupported
|
|
468
468
|
};
|
|
469
469
|
|
|
470
|
+
// ui/src/brand/palette.ts
|
|
471
|
+
var PALETTE = {
|
|
472
|
+
blue: "#0D42FC",
|
|
473
|
+
cyan: "#00CAD5",
|
|
474
|
+
green: "#91D202",
|
|
475
|
+
amber: "#FDAD01",
|
|
476
|
+
orange: "#FC4E23",
|
|
477
|
+
violet: "#8730FB"
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// src/util/banner.ts
|
|
481
|
+
var BANNER_COLORS = {
|
|
482
|
+
mark: PALETTE.green,
|
|
483
|
+
wordmarkTop: PALETTE.cyan,
|
|
484
|
+
wordmarkMid: PALETTE.green,
|
|
485
|
+
wordmarkBottom: PALETTE.violet,
|
|
486
|
+
cliVersion: PALETTE.cyan,
|
|
487
|
+
poolVersion: PALETTE.violet
|
|
488
|
+
};
|
|
489
|
+
var color = {
|
|
490
|
+
mark: hex(BANNER_COLORS.mark),
|
|
491
|
+
top: hex(BANNER_COLORS.wordmarkTop),
|
|
492
|
+
mid: hex(BANNER_COLORS.wordmarkMid),
|
|
493
|
+
bottom: hex(BANNER_COLORS.wordmarkBottom),
|
|
494
|
+
cli: hex(BANNER_COLORS.cliVersion),
|
|
495
|
+
pool: hex(BANNER_COLORS.poolVersion)
|
|
496
|
+
};
|
|
497
|
+
|
|
470
498
|
// src/util/log.ts
|
|
471
499
|
var OK = theme.mint("OK");
|
|
472
500
|
var WARN = theme.amber("!!");
|
|
@@ -483,7 +511,7 @@ function errorLine(text) {
|
|
|
483
511
|
process.stderr.write(`${theme.coral("error")} ${text}
|
|
484
512
|
`);
|
|
485
513
|
}
|
|
486
|
-
var
|
|
514
|
+
var color2 = Object.assign(import_picocolors2.default, {
|
|
487
515
|
mint: theme.mint,
|
|
488
516
|
violet: theme.violet,
|
|
489
517
|
magenta: theme.magenta,
|
package/dist/claude-glm.js
CHANGED
|
@@ -467,6 +467,34 @@ var theme = {
|
|
|
467
467
|
isColorSupported: import_picocolors.default.isColorSupported
|
|
468
468
|
};
|
|
469
469
|
|
|
470
|
+
// ui/src/brand/palette.ts
|
|
471
|
+
var PALETTE = {
|
|
472
|
+
blue: "#0D42FC",
|
|
473
|
+
cyan: "#00CAD5",
|
|
474
|
+
green: "#91D202",
|
|
475
|
+
amber: "#FDAD01",
|
|
476
|
+
orange: "#FC4E23",
|
|
477
|
+
violet: "#8730FB"
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// src/util/banner.ts
|
|
481
|
+
var BANNER_COLORS = {
|
|
482
|
+
mark: PALETTE.green,
|
|
483
|
+
wordmarkTop: PALETTE.cyan,
|
|
484
|
+
wordmarkMid: PALETTE.green,
|
|
485
|
+
wordmarkBottom: PALETTE.violet,
|
|
486
|
+
cliVersion: PALETTE.cyan,
|
|
487
|
+
poolVersion: PALETTE.violet
|
|
488
|
+
};
|
|
489
|
+
var color = {
|
|
490
|
+
mark: hex(BANNER_COLORS.mark),
|
|
491
|
+
top: hex(BANNER_COLORS.wordmarkTop),
|
|
492
|
+
mid: hex(BANNER_COLORS.wordmarkMid),
|
|
493
|
+
bottom: hex(BANNER_COLORS.wordmarkBottom),
|
|
494
|
+
cli: hex(BANNER_COLORS.cliVersion),
|
|
495
|
+
pool: hex(BANNER_COLORS.poolVersion)
|
|
496
|
+
};
|
|
497
|
+
|
|
470
498
|
// src/util/log.ts
|
|
471
499
|
var OK = theme.mint("OK");
|
|
472
500
|
var WARN = theme.amber("!!");
|
|
@@ -483,7 +511,7 @@ function errorLine(text) {
|
|
|
483
511
|
process.stderr.write(`${theme.coral("error")} ${text}
|
|
484
512
|
`);
|
|
485
513
|
}
|
|
486
|
-
var
|
|
514
|
+
var color2 = Object.assign(import_picocolors2.default, {
|
|
487
515
|
mint: theme.mint,
|
|
488
516
|
violet: theme.violet,
|
|
489
517
|
magenta: theme.magenta,
|
package/dist/claude-oss.js
CHANGED
|
@@ -467,6 +467,34 @@ var theme = {
|
|
|
467
467
|
isColorSupported: import_picocolors.default.isColorSupported
|
|
468
468
|
};
|
|
469
469
|
|
|
470
|
+
// ui/src/brand/palette.ts
|
|
471
|
+
var PALETTE = {
|
|
472
|
+
blue: "#0D42FC",
|
|
473
|
+
cyan: "#00CAD5",
|
|
474
|
+
green: "#91D202",
|
|
475
|
+
amber: "#FDAD01",
|
|
476
|
+
orange: "#FC4E23",
|
|
477
|
+
violet: "#8730FB"
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
// src/util/banner.ts
|
|
481
|
+
var BANNER_COLORS = {
|
|
482
|
+
mark: PALETTE.green,
|
|
483
|
+
wordmarkTop: PALETTE.cyan,
|
|
484
|
+
wordmarkMid: PALETTE.green,
|
|
485
|
+
wordmarkBottom: PALETTE.violet,
|
|
486
|
+
cliVersion: PALETTE.cyan,
|
|
487
|
+
poolVersion: PALETTE.violet
|
|
488
|
+
};
|
|
489
|
+
var color = {
|
|
490
|
+
mark: hex(BANNER_COLORS.mark),
|
|
491
|
+
top: hex(BANNER_COLORS.wordmarkTop),
|
|
492
|
+
mid: hex(BANNER_COLORS.wordmarkMid),
|
|
493
|
+
bottom: hex(BANNER_COLORS.wordmarkBottom),
|
|
494
|
+
cli: hex(BANNER_COLORS.cliVersion),
|
|
495
|
+
pool: hex(BANNER_COLORS.poolVersion)
|
|
496
|
+
};
|
|
497
|
+
|
|
470
498
|
// src/util/log.ts
|
|
471
499
|
var OK = theme.mint("OK");
|
|
472
500
|
var WARN = theme.amber("!!");
|
|
@@ -483,7 +511,7 @@ function errorLine(text) {
|
|
|
483
511
|
process.stderr.write(`${theme.coral("error")} ${text}
|
|
484
512
|
`);
|
|
485
513
|
}
|
|
486
|
-
var
|
|
514
|
+
var color2 = Object.assign(import_picocolors2.default, {
|
|
487
515
|
mint: theme.mint,
|
|
488
516
|
violet: theme.violet,
|
|
489
517
|
magenta: theme.magenta,
|