cdx-manager 0.25.8 → 0.25.9
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 +31 -6
- package/VERSION +1 -1
- package/changelogs/CHANGELOGS_0_25_9.md +22 -0
- package/changelogs/CHANGELOGS_UNRELEASED.md +27 -1
- package/checksums/release-archives.json +11 -0
- package/docs/cli.md +69 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/src/antigravity_status.py +161 -0
- package/src/claude_remote_token.py +45 -0
- package/src/cli.py +2 -0
- package/src/cli_args.py +2 -0
- package/src/commands/auth.py +25 -0
- package/src/commands/maintenance.py +4 -1
- package/src/commands/runs.py +16 -3
- package/src/interactive_usage.py +6 -12
- package/src/ollama_remote.py +175 -0
- package/src/provider_runtime.py +15 -0
- package/src/remote_run.py +69 -25
- package/src/remote_worker.py +85 -33
- package/src/run_command.py +9 -0
- package/src/run_registry.py +5 -0
- package/src/run_usage.py +63 -2
- package/src/session_service.py +4 -0
- package/src/session_status.py +16 -0
- package/src/status_view.py +1 -0
- package/src/update_all.py +9 -3
- package/src/workspace_sandbox.py +126 -6
package/src/run_usage.py
CHANGED
|
@@ -10,6 +10,10 @@ definitions without saying so.
|
|
|
10
10
|
|
|
11
11
|
The definitions below are therefore normative, not a suggestion:
|
|
12
12
|
|
|
13
|
+
Antigravity exception: the native input/cache/thinking relationship is not
|
|
14
|
+
established by the measured CLI output. Preserve its counters and reported
|
|
15
|
+
total rather than inventing a subset relationship or recomputing that total.
|
|
16
|
+
|
|
13
17
|
input_tokens Uncached input only. Claude reports it this way
|
|
14
18
|
natively; Codex does not, so its cache-inclusive count
|
|
15
19
|
is reduced here rather than left for a reader to guess.
|
|
@@ -64,9 +68,9 @@ TOKEN_USAGE_KEYS = (
|
|
|
64
68
|
"total_tokens",
|
|
65
69
|
)
|
|
66
70
|
USAGE_KEYS = (*TOKEN_USAGE_KEYS, "cache_hit_ratio")
|
|
67
|
-
#: Derived values are never read from a provider.
|
|
71
|
+
#: Derived values are never read from a provider, except Antigravity's native total.
|
|
68
72
|
DERIVED_USAGE_KEYS = ("cached_input_tokens", "total_tokens", "cache_hit_ratio")
|
|
69
|
-
SUPPORTED_PROVIDERS = {"claude", "codex"}
|
|
73
|
+
SUPPORTED_PROVIDERS = {"claude", "codex", "ollama", "antigravity"}
|
|
70
74
|
|
|
71
75
|
|
|
72
76
|
def empty_usage():
|
|
@@ -456,6 +460,8 @@ def coerce_usage(usage, provider=None):
|
|
|
456
460
|
reasoning_tokens=usage.get("reasoning_tokens"),
|
|
457
461
|
cache_creation_1h_tokens=usage.get("cache_creation_1h_tokens"),
|
|
458
462
|
)
|
|
463
|
+
if provider == "antigravity":
|
|
464
|
+
record["total_tokens"] = _first_int(usage.get("total_tokens"))
|
|
459
465
|
if record["cached_input_tokens"] is None:
|
|
460
466
|
# A legacy record carrying only the fused figure: keep it visible rather
|
|
461
467
|
# than dropping a number someone already recorded, but leave the split
|
|
@@ -635,10 +641,25 @@ def _usage_from_dict(value, provider=None):
|
|
|
635
641
|
tokens. The fix is not arithmetic in this function: it is that the split
|
|
636
642
|
parts go to `normalize_usage`, which owns every derived figure.
|
|
637
643
|
"""
|
|
644
|
+
if provider == "ollama":
|
|
645
|
+
prompt = _first_int(value.get("prompt_eval_count"))
|
|
646
|
+
output = _first_int(value.get("eval_count"))
|
|
647
|
+
return {**normalize_usage(input_tokens=prompt, output_tokens=output),
|
|
648
|
+
"prompt_eval_count": prompt, "eval_count": output}
|
|
638
649
|
usage = value.get("usage") if isinstance(value.get("usage"), dict) else value
|
|
639
650
|
if not isinstance(usage, dict):
|
|
640
651
|
return empty_usage()
|
|
641
652
|
|
|
653
|
+
if provider == "antigravity":
|
|
654
|
+
# agy reports thinking separately. Without a documented cache subset
|
|
655
|
+
# relationship, retain its input count and its measured total verbatim.
|
|
656
|
+
record = normalize_usage(
|
|
657
|
+
input_tokens=usage.get("input_tokens"), output_tokens=usage.get("output_tokens"),
|
|
658
|
+
cache_read_tokens=usage.get("cache_read_tokens"), reasoning_tokens=usage.get("thinking_tokens"),
|
|
659
|
+
)
|
|
660
|
+
record["total_tokens"] = _first_int(usage.get("total_tokens"))
|
|
661
|
+
return record
|
|
662
|
+
|
|
642
663
|
cache_creation = _first_int(usage.get("cache_creation_input_tokens"))
|
|
643
664
|
if cache_creation is None:
|
|
644
665
|
cache_creation = _first_int(usage.get("cache_write_input_tokens"))
|
|
@@ -720,3 +741,43 @@ def _int_value(*values):
|
|
|
720
741
|
|
|
721
742
|
def _has_usage(usage):
|
|
722
743
|
return isinstance(usage, dict) and any(usage.get(key) is not None for key in USAGE_KEYS)
|
|
744
|
+
|
|
745
|
+
|
|
746
|
+
def antigravity_outcome(info):
|
|
747
|
+
"""Fail closed on missing completion, authentication errors and refused tools."""
|
|
748
|
+
if info.get("timed_out") or info.get("budget_exceeded"):
|
|
749
|
+
return info
|
|
750
|
+
try:
|
|
751
|
+
with open(info["stdout_path"], encoding="utf-8") as handle:
|
|
752
|
+
records = _parse_json_records(handle.read())
|
|
753
|
+
with open(info["stderr_path"], encoding="utf-8") as handle:
|
|
754
|
+
error_text = handle.read().lower()
|
|
755
|
+
except OSError:
|
|
756
|
+
records, error_text = [], ""
|
|
757
|
+
def objects(value):
|
|
758
|
+
if isinstance(value, dict):
|
|
759
|
+
yield value
|
|
760
|
+
for child in value.values():
|
|
761
|
+
yield from objects(child)
|
|
762
|
+
elif isinstance(value, list):
|
|
763
|
+
for child in value:
|
|
764
|
+
yield from objects(child)
|
|
765
|
+
|
|
766
|
+
statuses = [record.get("status") for record in records if isinstance(record, dict) and "status" in record]
|
|
767
|
+
errors = [record.get("error") for record in objects(records) if record.get("error")]
|
|
768
|
+
text = json.dumps(errors if statuses and statuses[-1] == "SUCCESS" else records).lower() + error_text
|
|
769
|
+
expired = any(word in text for word in ("invalid_grant", "unauthenticated", "session expired", "token expired",
|
|
770
|
+
"login required", "sign in", "not authenticated", "authentication required"))
|
|
771
|
+
refused = any(record.get(key) for record in objects(records)
|
|
772
|
+
for key in ("permission_denials", "refused_actions", "denied_actions"))
|
|
773
|
+
refused = refused or any(str(record.get("status", "")).upper() in ("REFUSED", "DENIED", "PERMISSION_DENIED")
|
|
774
|
+
for record in objects(records))
|
|
775
|
+
if expired:
|
|
776
|
+
code, message = "antigravity_session_expired", "Antigravity worker session has expired; sign in again on the worker."
|
|
777
|
+
elif refused:
|
|
778
|
+
code, message = "antigravity_action_refused", "Antigravity refused one or more requested actions."
|
|
779
|
+
elif not statuses or statuses[-1] != "SUCCESS":
|
|
780
|
+
code, message = "antigravity_status_failed", "Antigravity did not report JSON status SUCCESS."
|
|
781
|
+
else:
|
|
782
|
+
return info
|
|
783
|
+
return {**info, "returncode": info.get("returncode") or 1, "provider_error": code, "provider_message": message}
|
package/src/session_service.py
CHANGED
|
@@ -644,6 +644,10 @@ def create_session(base_dir, env, store, name, provider=DEFAULT_PROVIDER):
|
|
|
644
644
|
_seed_codex_auth_from_global(auth_home, env=env)
|
|
645
645
|
if normalized_provider == PROVIDER_CLAUDE:
|
|
646
646
|
_ensure_claude_attribution_disabled(auth_home)
|
|
647
|
+
if normalized_provider == "antigravity":
|
|
648
|
+
from .antigravity_status import install
|
|
649
|
+
|
|
650
|
+
install(auth_home)
|
|
647
651
|
now = _local_now_iso()
|
|
648
652
|
session = {
|
|
649
653
|
"name": name,
|
package/src/session_status.py
CHANGED
|
@@ -359,6 +359,12 @@ def session_paths(session):
|
|
|
359
359
|
|
|
360
360
|
def _status_row_from_session(store, s):
|
|
361
361
|
status = s.get("lastStatus")
|
|
362
|
+
antigravity = None
|
|
363
|
+
if s["provider"] == "antigravity":
|
|
364
|
+
from .antigravity_status import read_status
|
|
365
|
+
|
|
366
|
+
antigravity = read_status(s.get("authHome") or s.get("sessionRoot") or "")
|
|
367
|
+
status = antigravity
|
|
362
368
|
enabled = s.get("enabled", True) is not False
|
|
363
369
|
row_status = status if enabled else None
|
|
364
370
|
runtime = _session_runtime(store, s["name"])
|
|
@@ -401,6 +407,13 @@ def _status_row_from_session(store, s):
|
|
|
401
407
|
"updated_at": _to_local_iso(s.get("lastStatusAt")),
|
|
402
408
|
"last_launched_at": _to_local_iso(s.get("lastLaunchedAt")),
|
|
403
409
|
}
|
|
410
|
+
if antigravity is not None:
|
|
411
|
+
row.update({key: antigravity[key] for key in (
|
|
412
|
+
"quota", "quota_status", "quota_stale", "reading_age_seconds", "plan_tier", "updated_at",
|
|
413
|
+
"reset_5h_at_iso", "reset_week_at_iso",
|
|
414
|
+
)})
|
|
415
|
+
if enabled and antigravity["quota_stale"]:
|
|
416
|
+
row["status"] = "stale"
|
|
404
417
|
row["runs_out_at_iso"] = _runs_out(row)
|
|
405
418
|
return row
|
|
406
419
|
|
|
@@ -434,6 +447,9 @@ def _resolve_session_status(store, base_dir, env, fetch_codex_status, session,
|
|
|
434
447
|
cache_only=False,
|
|
435
448
|
status_timeout_seconds=None,):
|
|
436
449
|
current_status = session.get("lastStatus")
|
|
450
|
+
if session["provider"] == "antigravity":
|
|
451
|
+
# The local statusLine file is the only Antigravity quota source.
|
|
452
|
+
return None
|
|
437
453
|
if session.get("enabled", True) is False:
|
|
438
454
|
return current_status
|
|
439
455
|
if cache_only:
|
package/src/status_view.py
CHANGED
|
@@ -369,6 +369,7 @@ def _format_status_detail(row, use_color=False):
|
|
|
369
369
|
f"{_style('Session:', '1', use_color)} {_format_session_name(row)}",
|
|
370
370
|
f"{_style('Provider:', '1', use_color)} {row.get('provider') or 'n/a'}",
|
|
371
371
|
f"{_style('Status:', '1', use_color)} {_style(row.get('status') or ('enabled' if row.get('enabled', True) else 'disabled'), '2' if row.get('enabled', True) is False else '32', use_color)}",
|
|
372
|
+
*([f"{_style('Plan:', '1', use_color)} {row['plan_tier']}"] if row.get("plan_tier") else []),
|
|
372
373
|
f"{_style('Auth:', '1', use_color)} {_style(auth, auth_color, use_color)}",
|
|
373
374
|
*([f"{_style('Auth type:', '1', use_color)} {_auth_type_label(row)[0]}"] if row.get("auth_type") else []),
|
|
374
375
|
f"{_style('Available:', '1', use_color)} {_style_pct(row.get('available_pct'), use_color)}",
|
package/src/update_all.py
CHANGED
|
@@ -241,9 +241,15 @@ def apply_update_all_plan(plan, service, env=None, runner=None, progress=None):
|
|
|
241
241
|
advanced = (_newer(after, item["version"]) and
|
|
242
242
|
(not item["latest_version"] or after == item["latest_version"] or _newer(after, item["latest_version"])))
|
|
243
243
|
if not (advanced or (after and after == item["version"] and reports_current)):
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
244
|
+
if item["name"] == PROVIDER_ANTIGRAVITY and not _newer(item["latest_version"], item["version"]):
|
|
245
|
+
result["warning"] = {
|
|
246
|
+
"code": "update_unverified",
|
|
247
|
+
"message": "agy update exited successfully, but currency could not be verified: no expected newer CLI version is known. Status remains unknown.",
|
|
248
|
+
}
|
|
249
|
+
else:
|
|
250
|
+
result["returncode"] = 1
|
|
251
|
+
result["reason"] = "Update verification failed: installed version did not advance to the expected release and updater did not confirm it is current."
|
|
252
|
+
result["stderr"] = (result["stderr"] + "\n" + result["reason"]).strip()
|
|
247
253
|
results.append(result)
|
|
248
254
|
if result["returncode"] == 0 and step.get("id"):
|
|
249
255
|
completed.add(step["id"])
|
package/src/workspace_sandbox.py
CHANGED
|
@@ -5,9 +5,12 @@ Provider authentication is the only credential deliberately passed to the provid
|
|
|
5
5
|
"""
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
import ipaddress
|
|
8
9
|
import json
|
|
9
10
|
import os
|
|
10
11
|
import shutil
|
|
12
|
+
import socket
|
|
13
|
+
import stat
|
|
11
14
|
import tempfile
|
|
12
15
|
from contextlib import contextmanager
|
|
13
16
|
from pathlib import Path
|
|
@@ -28,7 +31,17 @@ NETWORK_RULES = """table inet mission_egress {
|
|
|
28
31
|
PUBLIC_RESOLV_CONF = "nameserver 1.1.1.1\nnameserver 9.9.9.9\n"
|
|
29
32
|
|
|
30
33
|
# Keep the trusted setup outside the agent's filesystem/PID/user namespaces.
|
|
31
|
-
|
|
34
|
+
CLAUDE_STDIN_EXEC = """
|
|
35
|
+
import json
|
|
36
|
+
import os
|
|
37
|
+
import sys
|
|
38
|
+
token = json.loads(sys.stdin.buffer.readline())
|
|
39
|
+
os.environ["CLAUDE_CODE_OAUTH_TOKEN"] = token
|
|
40
|
+
os.dup2(os.open(os.devnull, os.O_RDONLY), 0)
|
|
41
|
+
os.execv(sys.argv[1], sys.argv[1:])
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
NETWORK_SETUP = r"""
|
|
32
45
|
import pathlib
|
|
33
46
|
import subprocess
|
|
34
47
|
import sys
|
|
@@ -68,6 +81,61 @@ def private_auth_home(auth):
|
|
|
68
81
|
yield directory
|
|
69
82
|
|
|
70
83
|
|
|
84
|
+
ANTIGRAVITY_TOKEN_PATH = ".gemini/antigravity-cli/antigravity-oauth-token"
|
|
85
|
+
# Candidate endpoints extracted from agy 1.4.12; live acceptance is pending.
|
|
86
|
+
ANTIGRAVITY_DESTINATIONS = ("oauth2.googleapis.com", "cloudcode-pa.googleapis.com",
|
|
87
|
+
"daily-cloudcode-pa.googleapis.com")
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def antigravity_token():
|
|
91
|
+
path = Path.home() / ANTIGRAVITY_TOKEN_PATH
|
|
92
|
+
try:
|
|
93
|
+
info = path.lstat()
|
|
94
|
+
except FileNotFoundError:
|
|
95
|
+
raise RuntimeError("Antigravity worker is not signed in; sign in once on the worker.") from None
|
|
96
|
+
if (not stat.S_ISREG(info.st_mode) or info.st_uid != os.getuid()
|
|
97
|
+
or stat.S_IMODE(info.st_mode) != 0o600 or not info.st_size
|
|
98
|
+
or any(parent.is_symlink() for parent in path.parents)):
|
|
99
|
+
raise RuntimeError("Antigravity worker session file is unsafe; restore an owner-only regular token file.")
|
|
100
|
+
return path
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def antigravity_secrets():
|
|
104
|
+
text = antigravity_token().read_text().strip()
|
|
105
|
+
|
|
106
|
+
def values(value):
|
|
107
|
+
if isinstance(value, dict):
|
|
108
|
+
return [item for child in value.values() for item in values(child)]
|
|
109
|
+
if isinstance(value, list):
|
|
110
|
+
return [item for child in value for item in values(child)]
|
|
111
|
+
return [value] if isinstance(value, str) and value else []
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
return [text, *values(json.loads(text))]
|
|
115
|
+
except ValueError:
|
|
116
|
+
return [text]
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
@contextmanager
|
|
120
|
+
def antigravity_attempt(parent="/dev/shm"):
|
|
121
|
+
import fcntl
|
|
122
|
+
|
|
123
|
+
location = Path(parent).resolve()
|
|
124
|
+
mounts = [row.split() for row in Path('/proc/mounts').read_text().splitlines()]
|
|
125
|
+
enclosing = [row for row in mounts if Path(row[1]) == location or Path(row[1]) in location.parents]
|
|
126
|
+
if not enclosing or max(enclosing, key=lambda row: len(row[1]))[2] != 'tmpfs':
|
|
127
|
+
raise RuntimeError('Antigravity attempts require memory-backed storage')
|
|
128
|
+
token = antigravity_token()
|
|
129
|
+
# Lock the file itself without modifying it; every cdx attempt uses this inode.
|
|
130
|
+
with token.open("rb") as lock:
|
|
131
|
+
try:
|
|
132
|
+
fcntl.flock(lock, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
133
|
+
except BlockingIOError:
|
|
134
|
+
raise RuntimeError("Antigravity worker session is busy with another attempt.") from None
|
|
135
|
+
with tempfile.TemporaryDirectory(prefix="cdx-agy-", dir=parent) as directory:
|
|
136
|
+
yield directory
|
|
137
|
+
|
|
138
|
+
|
|
71
139
|
def clean_env(source: dict) -> dict:
|
|
72
140
|
allowed = ("PATH", "LANG", "LC_ALL", "TERM", "TZ")
|
|
73
141
|
env = {key: source[key] for key in allowed if key in source}
|
|
@@ -85,7 +153,7 @@ def clean_env(source: dict) -> dict:
|
|
|
85
153
|
|
|
86
154
|
|
|
87
155
|
def sandbox_command(command: list[str], tree: Path, *, network: bool, writable: bool = True,
|
|
88
|
-
source: dict | None = None, auth_home: str = "", provider: str = "", observation_home: str = "") -> tuple[list[str], dict]:
|
|
156
|
+
source: dict | None = None, auth_home: str = "", provider: str = "", observation_home: str = "", ollama_relay: str = "", claude_token_stdin: bool = False) -> tuple[list[str], dict]:
|
|
89
157
|
source = os.environ if source is None else source
|
|
90
158
|
env = clean_env(source)
|
|
91
159
|
binary = shutil.which(command[0], path=env["PATH"])
|
|
@@ -98,7 +166,7 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
|
|
|
98
166
|
CACHE.mkdir(parents=True, exist_ok=True)
|
|
99
167
|
(CACHE / "rtk").mkdir(exist_ok=True)
|
|
100
168
|
# Provider transport uses the same restricted egress, even for read-only tools.
|
|
101
|
-
online = bool(network or provider)
|
|
169
|
+
online = bool(network or (provider and provider != "ollama"))
|
|
102
170
|
if online:
|
|
103
171
|
pasta = shutil.which("pasta", path=source.get("PATH"))
|
|
104
172
|
nft = shutil.which("nft", path=os.defpath + ":/usr/sbin:/sbin")
|
|
@@ -112,6 +180,10 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
|
|
|
112
180
|
if online else ["--unshare-all"])
|
|
113
181
|
args += ["--cap-drop", "ALL"]
|
|
114
182
|
args += ["--proc", "/proc", "--dev", "/dev", "--tmpfs", "/tmp", "--dir", "/home/agent"]
|
|
183
|
+
if (provider == "claude" and claude_token_stdin) or provider == "antigravity":
|
|
184
|
+
if not observation_home:
|
|
185
|
+
raise RuntimeError("Provider requires a volatile attempt home")
|
|
186
|
+
args += ["--bind", observation_home, "/home/agent"]
|
|
115
187
|
# An allowlist avoids overlooking another secret file or a credential symlink.
|
|
116
188
|
paths = [Path(p) for p in ("/usr", "/bin", "/sbin", "/lib", "/lib64")]
|
|
117
189
|
home = Path.home()
|
|
@@ -170,15 +242,63 @@ def sandbox_command(command: list[str], tree: Path, *, network: bool, writable:
|
|
|
170
242
|
args += ["--ro-bind", str(Path(auth_home) / "auth.json"), "/home/agent/.codex/auth.json"]
|
|
171
243
|
elif provider == "claude":
|
|
172
244
|
token = source.get("CLAUDE_CODE_OAUTH_TOKEN")
|
|
173
|
-
if not token:
|
|
245
|
+
if not token and not claude_token_stdin:
|
|
174
246
|
raise RuntimeError("Isolated Claude runs require a cdx OAuth setup token")
|
|
175
|
-
|
|
247
|
+
if claude_token_stdin:
|
|
248
|
+
env["XDG_CACHE_HOME"] = "/home/agent/.cache"
|
|
249
|
+
python = shutil.which("python3", path=os.defpath)
|
|
250
|
+
if not python:
|
|
251
|
+
raise RuntimeError("Remote Claude requires Python 3")
|
|
252
|
+
command = [python, "-I", "-c", CLAUDE_STDIN_EXEC, binary, *command[1:]]
|
|
253
|
+
binary = python
|
|
254
|
+
else:
|
|
255
|
+
env["CLAUDE_CODE_OAUTH_TOKEN"] = token
|
|
256
|
+
env.update(CLAUDE_CODE_SUBPROCESS_ENV_SCRUB="1",
|
|
176
257
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC="1")
|
|
258
|
+
elif provider == "antigravity":
|
|
259
|
+
token = antigravity_token()
|
|
260
|
+
args += ["--ro-bind", str(token), "/home/agent/" + ANTIGRAVITY_TOKEN_PATH]
|
|
261
|
+
env["AGY_CLI_DISABLE_AUTO_UPDATE"] = "true"
|
|
262
|
+
env["XDG_CACHE_HOME"] = "/home/agent/.cache"
|
|
263
|
+
elif provider == "ollama":
|
|
264
|
+
if not ollama_relay:
|
|
265
|
+
raise RuntimeError("Isolated Ollama requires a supervisor-owned relay")
|
|
266
|
+
args += ["--ro-bind", ollama_relay, "/run/cdx-ollama"]
|
|
177
267
|
elif provider:
|
|
178
268
|
raise RuntimeError("Unsupported isolated provider: " + provider)
|
|
179
269
|
args += ["--chdir", str(tree), "--", binary, *command[1:]]
|
|
180
270
|
if online:
|
|
181
|
-
|
|
271
|
+
rules = NETWORK_RULES
|
|
272
|
+
if provider == "claude" and claude_token_stdin and not network:
|
|
273
|
+
# Resolve on the trusted worker before pasta replaces the DNS namespace.
|
|
274
|
+
addresses = {entry[4][0] for entry in socket.getaddrinfo("api.anthropic.com", 443, type=socket.SOCK_STREAM)}
|
|
275
|
+
inference = []
|
|
276
|
+
for family, values in (("ip", sorted(address for address in addresses if ":" not in address)),
|
|
277
|
+
("ip6", sorted(address for address in addresses if ":" in address))):
|
|
278
|
+
if values:
|
|
279
|
+
inference.append(f" {family} daddr {{ {', '.join(values)} }} tcp dport 443 accept")
|
|
280
|
+
if not inference:
|
|
281
|
+
raise RuntimeError("No Anthropic inference addresses")
|
|
282
|
+
rules = rules.replace("policy accept", "policy drop").replace(
|
|
283
|
+
" oif", " ct state established,related accept\n oif")
|
|
284
|
+
rules = rules.replace(" }\n}", " ip daddr { 1.1.1.1, 9.9.9.9 } udp dport 53 accept\n"
|
|
285
|
+
" ip daddr { 1.1.1.1, 9.9.9.9 } tcp dport 53 accept\n"
|
|
286
|
+
+ "\n".join(inference) + "\n }\n}")
|
|
287
|
+
if provider == "antigravity":
|
|
288
|
+
addresses = {entry[4][0] for host in ANTIGRAVITY_DESTINATIONS
|
|
289
|
+
for entry in socket.getaddrinfo(host, 443, type=socket.SOCK_STREAM)}
|
|
290
|
+
if not addresses or any(not ipaddress.ip_address(address).is_global for address in addresses):
|
|
291
|
+
raise RuntimeError("Antigravity destinations must resolve to public addresses")
|
|
292
|
+
rules = NETWORK_RULES.replace("policy accept", "policy drop")
|
|
293
|
+
inference = []
|
|
294
|
+
for family in ("ip", "ip6"):
|
|
295
|
+
values = sorted(a for a in addresses if (":" in a) == (family == "ip6"))
|
|
296
|
+
if values:
|
|
297
|
+
inference.append(f" {family} daddr {{ {', '.join(values)} }} tcp dport 443 accept")
|
|
298
|
+
rules = rules.replace(" }\n}", " ip daddr { 1.1.1.1, 9.9.9.9 } udp dport 53 accept\n"
|
|
299
|
+
" ip daddr { 1.1.1.1, 9.9.9.9 } tcp dport 53 accept\n"
|
|
300
|
+
+ "\n".join(inference) + "\n }\n}")
|
|
301
|
+
setup = "RULES = " + repr(rules) + "\nRESOLVER = " + repr(PUBLIC_RESOLV_CONF) + "\n" + NETWORK_SETUP
|
|
182
302
|
# Disable both directions explicitly: automatic host-loopback splicing
|
|
183
303
|
# bypasses output filtering unless -T/-U are also disabled.
|
|
184
304
|
args = [pasta, "--config-net", "--no-map-gw", "-t", "none", "-u", "none",
|