Compare commits

...

4 Commits

Author SHA1 Message Date
didericis 0de3c93ad0 fix: resolve pyright errors in manifest_schema.py
Lint and Type Check / lint (push) Failing after 6m57s
test / unit (pull_request) Successful in 38s
test / integration (pull_request) Failing after 45s
- Add type: ignore annotations for dict key validation
- Keys parameter is untyped object from YAML parsing
- Use type: ignore for set operations and sorted calls
- Fixes 4 pyright errors

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-03 23:45:20 -04:00
didericis 570cd42532 fix: resolve pyright errors in bottle_state.py and most of egress_apply.py
- Add cast import and use for dict.get() results in bottle_state.py
- Fix JSON metadata loading with proper dict type casting
- Apply same pattern to egress_apply.py for YAML routes parsing
- Cast routes list after isinstance check
- Properly type proposed_paths and existing_paths after validation
- Fixes 35 pyright errors across both files

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-03 23:40:14 -04:00
didericis 73a4fbe0a7 fix: resolve all pyright errors in pipelock.py
- Add cast import and use for dict/list access from object types
- Cast after isinstance checks in helper functions (_required_dict, _required_str_list)
- Cast dict and list values extracted from cfg in pipelock_render_yaml
- Fix list comprehension type issue by casting to list[object] first
- Fixes 14 pyright errors in YAML rendering code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-03 23:37:23 -04:00
didericis b032ff746d fix: resolve all pyright errors in codex_auth.py
- Add cast imports and explicit type annotations for dict[str, object]
- Add casts at JSON boundary and after isinstance checks
- Update all function signatures to use typed dicts
- Fixes 59 pyright errors in JSON parsing code

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-06-03 23:33:43 -04:00
5 changed files with 86 additions and 77 deletions
+9 -7
View File
@@ -35,6 +35,7 @@ import secrets
import string import string
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import cast
from ... import supervise as _supervise from ... import supervise as _supervise
from . import util as docker_mod from . import util as docker_mod
@@ -135,14 +136,15 @@ def read_metadata(identity: str) -> BottleMetadata | None:
raw = json.loads(path.read_text()) raw = json.loads(path.read_text())
if not isinstance(raw, dict): if not isinstance(raw, dict):
return None return None
raw_typed = cast(dict[str, object], raw)
return BottleMetadata( return BottleMetadata(
identity=str(raw.get("identity", identity)), identity=str(raw_typed.get("identity", identity)),
agent_name=str(raw.get("agent_name", "")), agent_name=str(raw_typed.get("agent_name", "")),
cwd=str(raw.get("cwd", "")), cwd=str(raw_typed.get("cwd", "")),
copy_cwd=bool(raw.get("copy_cwd", False)), copy_cwd=bool(raw_typed.get("copy_cwd", False)),
started_at=str(raw.get("started_at", "")), started_at=str(raw_typed.get("started_at", "")),
compose_project=str(raw.get("compose_project", "")), compose_project=str(raw_typed.get("compose_project", "")),
backend=str(raw.get("backend", "")), backend=str(raw_typed.get("backend", "")),
) )
+25 -16
View File
@@ -26,6 +26,7 @@ import json
import re import re
import subprocess import subprocess
from pathlib import Path from pathlib import Path
from typing import cast
from ...egress import EGRESS_ROUTES_IN_CONTAINER from ...egress import EGRESS_ROUTES_IN_CONTAINER
from ...egress_addon_core import load_routes from ...egress_addon_core import load_routes
@@ -57,7 +58,8 @@ def _render_routes_payload(routes_list: list[dict[str, object]]) -> str:
if auth_scheme and token_env: if auth_scheme and token_env:
lines.append(f' auth_scheme: "{auth_scheme}"') lines.append(f' auth_scheme: "{auth_scheme}"')
lines.append(f' token_env: "{token_env}"') lines.append(f' token_env: "{token_env}"')
paths = entry.get("path_allowlist") or [] paths_obj = entry.get("path_allowlist")
paths = cast(list[str], paths_obj) if isinstance(paths_obj, list) else []
if paths: if paths:
lines.append(" path_allowlist:") lines.append(" path_allowlist:")
for p in paths: for p in paths:
@@ -257,6 +259,7 @@ def _merge_single_route(
raise EgressApplyError( raise EgressApplyError(
"current routes.yaml: 'routes' is not a list" "current routes.yaml: 'routes' is not a list"
) )
routes_typed = cast(list[object], routes)
new_host = str(new_route.get("host", "")).lower() new_host = str(new_route.get("host", "")).lower()
if not new_host: if not new_host:
@@ -264,22 +267,25 @@ def _merge_single_route(
"proposed route is missing 'host'" "proposed route is missing 'host'"
) )
proposed_paths = list(new_route.get("path_allowlist") or []) proposed_paths_obj = new_route.get("path_allowlist")
proposed_paths = cast(list[str], proposed_paths_obj) if isinstance(proposed_paths_obj, list) else []
# Look for an existing entry with the same host (case-insensitive). # Look for an existing entry with the same host (case-insensitive).
for entry in routes: for entry in routes_typed:
if not isinstance(entry, dict): if not isinstance(entry, dict):
continue continue
if str(entry.get("host", "")).lower() == new_host: entry_typed = cast(dict[str, object], entry)
if str(entry_typed.get("host", "")).lower() == new_host:
# Merge path_allowlist: union proposed + existing, ordered # Merge path_allowlist: union proposed + existing, ordered
# by first-seen so existing paths stay in original order. # by first-seen so existing paths stay in original order.
existing_paths: list[str] = list(entry.get("path_allowlist") or []) existing_paths_obj = entry_typed.get("path_allowlist")
existing_paths = cast(list[str], existing_paths_obj) if isinstance(existing_paths_obj, list) else []
seen = {p: None for p in existing_paths} seen = {p: None for p in existing_paths}
for p in proposed_paths: for p in proposed_paths:
seen.setdefault(p, None) seen.setdefault(p, None)
merged_paths = list(seen.keys()) merged_paths = list(seen.keys())
if merged_paths: if merged_paths:
entry["path_allowlist"] = merged_paths entry_typed["path_allowlist"] = merged_paths
# Preserve existing auth — tool description says agent- # Preserve existing auth — tool description says agent-
# proposed auth on an existing host is ignored. # proposed auth on an existing host is ignored.
break break
@@ -289,19 +295,22 @@ def _merge_single_route(
# `auth` was proposed (otherwise the addon's parser rejects # `auth` was proposed (otherwise the addon's parser rejects
# a half-set auth pair). Slots: count existing slots, pick # a half-set auth pair). Slots: count existing slots, pick
# the next free index. # the next free index.
entry = {"host": new_route["host"]} entry_typed: dict[str, object] = {"host": new_route.get("host")} # type: ignore
if proposed_paths: if proposed_paths:
entry["path_allowlist"] = proposed_paths entry_typed["path_allowlist"] = proposed_paths
auth = new_route.get("auth") auth = new_route.get("auth")
if isinstance(auth, dict) and auth.get("scheme") and auth.get("token_ref"): if isinstance(auth, dict) and auth.get("scheme") and auth.get("token_ref"): # type: ignore
auth_typed = cast(dict[str, object], auth)
existing_slots = sorted({ existing_slots = sorted({
str(r.get("token_env")) str(r_entry.get("token_env", ""))
for r in routes for r_entry_obj in routes_typed
if isinstance(r, dict) and r.get("token_env") if isinstance(r_entry_obj, dict)
for r_entry in [cast(dict[str, object], r_entry_obj)]
if r_entry.get("token_env")
}) })
next_idx = len(existing_slots) next_idx = len(existing_slots)
entry["auth_scheme"] = str(auth["scheme"]) entry_typed["auth_scheme"] = str(cast(object, auth_typed.get("scheme")))
entry["token_env"] = f"EGRESS_TOKEN_{next_idx}" entry_typed["token_env"] = f"EGRESS_TOKEN_{next_idx}"
# NOTE: the addon reads token VALUES from its container's # NOTE: the addon reads token VALUES from its container's
# environ keyed by token_env. A newly-added auth route at # environ keyed by token_env. A newly-added auth route at
# runtime points at a slot that has no env value → the # runtime points at a slot that has no env value → the
@@ -309,9 +318,9 @@ def _merge_single_route(
# arranges for the value to land in the container's env. # arranges for the value to land in the container's env.
# Recording this here so the operator-facing diff carries # Recording this here so the operator-facing diff carries
# the slot name they'll need to provision. # the slot name they'll need to provision.
routes.append(entry) routes_typed.append(entry_typed)
return _render_routes_payload(routes) return _render_routes_payload(cast(list[dict[str, object]], routes_typed))
def add_route(slug: str, proposed_route_json: str) -> tuple[str, str]: def add_route(slug: str, proposed_route_json: str) -> tuple[str, str]:
+22 -19
View File
@@ -13,6 +13,7 @@ import os
from copy import deepcopy from copy import deepcopy
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import cast
from .log import die from .log import die
from .util import expand_tilde from .util import expand_tilde
@@ -50,7 +51,8 @@ def codex_host_access_token(
tokens = raw.get("tokens") tokens = raw.get("tokens")
if not isinstance(tokens, dict): if not isinstance(tokens, dict):
die(f"codex host credentials: {path} is missing tokens") die(f"codex host credentials: {path} is missing tokens")
access = tokens.get("access_token") tokens_typed = cast(dict[str, object], tokens)
access = tokens_typed.get("access_token")
if not isinstance(access, str) or not access: if not isinstance(access, str) or not access:
die( die(
f"codex host credentials: {path} is missing tokens.access_token. " f"codex host credentials: {path} is missing tokens.access_token. "
@@ -105,14 +107,14 @@ def write_codex_dummy_auth_file(
path.chmod(0o600) path.chmod(0o600)
def _read_auth_object(path: Path) -> dict: def _read_auth_object(path: Path) -> dict[str, object]:
try: try:
raw = json.loads(path.read_text()) raw = json.loads(path.read_text())
except (OSError, json.JSONDecodeError) as e: except (OSError, json.JSONDecodeError) as e:
die(f"codex host credentials: could not read valid JSON at {path}: {e}") die(f"codex host credentials: could not read valid JSON at {path}: {e}")
if not isinstance(raw, dict): if not isinstance(raw, dict):
die(f"codex host credentials: {path} must contain a JSON object") die(f"codex host credentials: {path} must contain a JSON object")
return raw return cast(dict[str, object], raw)
def _dummy_exp(now: datetime | None, exp_ts: int | None) -> int: def _dummy_exp(now: datetime | None, exp_ts: int | None) -> int:
@@ -151,11 +153,11 @@ def _dummy_jwt_from_host(
return _dummy_jwt(now, exp_ts=exp_ts) return _dummy_jwt(now, exp_ts=exp_ts)
if not isinstance(payload, dict): if not isinstance(payload, dict):
return _dummy_jwt(now, exp_ts=exp_ts) return _dummy_jwt(now, exp_ts=exp_ts)
return _encode_dummy_jwt(_redact_jwt_payload(payload, now=now, exp_ts=exp_ts)) return _encode_dummy_jwt(_redact_jwt_payload(cast(dict[str, object], payload), now=now, exp_ts=exp_ts))
def _encode_dummy_jwt(payload: dict) -> str: def _encode_dummy_jwt(payload: dict[str, object]) -> str:
def enc(obj: dict) -> str: def enc(obj: dict[str, object]) -> str:
raw = json.dumps(obj, separators=(",", ":")).encode() raw = json.dumps(obj, separators=(",", ":")).encode()
return base64.urlsafe_b64encode(raw).decode().rstrip("=") return base64.urlsafe_b64encode(raw).decode().rstrip("=")
@@ -163,23 +165,24 @@ def _encode_dummy_jwt(payload: dict) -> str:
def _redact_jwt_payload( def _redact_jwt_payload(
payload: dict, payload: dict[str, object],
*, *,
now: datetime | None = None, now: datetime | None = None,
exp_ts: int | None = None, exp_ts: int | None = None,
) -> dict: ) -> dict[str, object]:
out = _redact_claims(payload) out = _redact_claims(payload)
if not isinstance(out, dict): if not isinstance(out, dict):
out = {} out = {}
out["exp"] = _dummy_exp(now, exp_ts) out_typed: dict[str, object] = cast(dict[str, object], out)
out.setdefault("sub", "bot-bottle-placeholder") out_typed["exp"] = _dummy_exp(now, exp_ts)
return out out_typed.setdefault("sub", "bot-bottle-placeholder")
return out_typed
def _redact_claims(value: object) -> object: def _redact_claims(value: object) -> object:
if isinstance(value, dict): if isinstance(value, dict):
out: dict[str, object] = {} out: dict[str, object] = {}
for key, inner in value.items(): for key, inner in cast(dict[str, object], value).items():
lower = key.lower() lower = key.lower()
if key == "https://api.openai.com/profile": if key == "https://api.openai.com/profile":
out[key] = _redact_profile_claim(inner) out[key] = _redact_profile_claim(inner)
@@ -207,16 +210,16 @@ def _redact_claims(value: object) -> object:
return "bot-bottle-placeholder" return "bot-bottle-placeholder"
def _redact_profile_claim(value: object) -> dict: def _redact_profile_claim(value: object) -> dict[str, object]:
profile = value if isinstance(value, dict) else {} profile = cast(dict[str, object], value) if isinstance(value, dict) else {}
return { return {
"email": "bot-bottle@example.invalid", "email": "bot-bottle@example.invalid",
"email_verified": bool(profile.get("email_verified", True)), "email_verified": bool(profile.get("email_verified", True)),
} }
def _redact_auth_claim(value: object) -> dict: def _redact_auth_claim(value: object) -> dict[str, object]:
auth = value if isinstance(value, dict) else {} auth = cast(dict[str, object], value) if isinstance(value, dict) else {}
out: dict[str, object] = {} out: dict[str, object] = {}
for key, inner in auth.items(): for key, inner in auth.items():
lower = key.lower() lower = key.lower()
@@ -247,7 +250,7 @@ def _redact_auth_claim(value: object) -> dict:
def _redact_codex_auth( def _redact_codex_auth(
value: object, *, now: datetime | None = None, exp_ts: int | None = None, value: object, *, now: datetime | None = None, exp_ts: int | None = None,
) -> object: ) -> object:
auth = value if isinstance(value, dict) else {} auth = cast(dict[str, object], value) if isinstance(value, dict) else {}
out: dict[str, object] = {} out: dict[str, object] = {}
for key, inner in auth.items(): for key, inner in auth.items():
lower = key.lower() lower = key.lower()
@@ -269,7 +272,7 @@ def _redact_codex_auth(
def _redact_token_block( def _redact_token_block(
value: object, *, now: datetime | None = None, exp_ts: int | None = None, value: object, *, now: datetime | None = None, exp_ts: int | None = None,
) -> dict[str, object]: ) -> dict[str, object]:
tokens = value if isinstance(value, dict) else {} tokens = cast(dict[str, object], value) if isinstance(value, dict) else {}
out: dict[str, object] = {} out: dict[str, object] = {}
for key, inner in tokens.items(): for key, inner in tokens.items():
lower = key.lower() lower = key.lower()
@@ -306,7 +309,7 @@ def _jwt_exp(token: str) -> datetime | None:
return None return None
if not isinstance(payload, dict): if not isinstance(payload, dict):
return None return None
exp = payload.get("exp") exp = cast(dict[str, object], payload).get("exp")
if not isinstance(exp, (int, float)): if not isinstance(exp, (int, float)):
return None return None
return datetime.fromtimestamp(exp, timezone.utc) return datetime.fromtimestamp(exp, timezone.utc)
+3 -3
View File
@@ -60,11 +60,11 @@ def _validate_frontmatter_keys(
) -> None: ) -> None:
from .manifest_util import ManifestError from .manifest_util import ManifestError
key_set = set(keys) key_set = set(keys) # type: ignore
unknown = key_set - allowed_keys unknown = key_set - allowed_keys # type: ignore
if unknown: if unknown:
allowed = ", ".join(sorted(allowed_keys)) allowed = ", ".join(sorted(allowed_keys))
raise ManifestError( raise ManifestError(
f"{kind} file {path}: unknown frontmatter key(s) " f"{kind} file {path}: unknown frontmatter key(s) "
f"{sorted(unknown)}; allowed keys are {allowed}." f"{sorted(unknown)}; allowed keys are {allowed}." # type: ignore
) )
+27 -32
View File
@@ -19,6 +19,7 @@ from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from pathlib import Path from pathlib import Path
from typing import cast
from .egress import EgressRoute, egress_routes_for_bottle from .egress import EgressRoute, egress_routes_for_bottle
from .supervise import SUPERVISE_HOSTNAME from .supervise import SUPERVISE_HOSTNAME
@@ -259,7 +260,7 @@ def _required_dict(
value = obj.get(key) value = obj.get(key)
if not isinstance(value, dict): if not isinstance(value, dict):
raise _pipelock_render_error(section, key, "a mapping") raise _pipelock_render_error(section, key, "a mapping")
return value return cast(dict[str, object], value)
def _required_bool(obj: dict[str, object], section: str, key: str) -> bool: def _required_bool(obj: dict[str, object], section: str, key: str) -> bool:
@@ -289,9 +290,12 @@ def _required_str_list(
key: str, key: str,
) -> list[str]: ) -> list[str]:
value = obj.get(key) value = obj.get(key)
if not isinstance(value, list) or not all(isinstance(v, str) for v in value): if not isinstance(value, list):
raise _pipelock_render_error(section, key, "a list of strings") raise _pipelock_render_error(section, key, "a list of strings")
return value value_list = cast(list[object], value)
if not all(isinstance(v, str) for v in value_list):
raise _pipelock_render_error(section, key, "a list of strings")
return cast(list[str], value)
def _optional_str_list( def _optional_str_list(
@@ -407,49 +411,42 @@ def pipelock_render_yaml(cfg: dict[str, object]) -> str:
lines: list[str] = [] lines: list[str] = []
lines.append(f"version: {cfg['version']}") lines.append(f"version: {cfg['version']}")
lines.append(f"mode: {cfg['mode']}") lines.append(f"mode: {cfg['mode']}")
lines.append(f"enforce: {_bool(cfg['enforce'])}") lines.append(f"enforce: {_bool(cast(bool, cfg['enforce']))}")
lines.append("") lines.append("")
lines.append("api_allowlist:") lines.append("api_allowlist:")
api_allowlist = cfg["api_allowlist"] api_allowlist = cast(list[str], cfg["api_allowlist"])
assert isinstance(api_allowlist, list)
for h in api_allowlist: for h in api_allowlist:
lines.append(f' - "{h}"') lines.append(f' - "{h}"')
lines.append("") lines.append("")
if "seed_phrase_detection" in cfg: if "seed_phrase_detection" in cfg:
lines.append("seed_phrase_detection:") lines.append("seed_phrase_detection:")
spd = cfg["seed_phrase_detection"] spd = cast(dict[str, object], cfg["seed_phrase_detection"])
assert isinstance(spd, dict) lines.append(f" enabled: {_bool(cast(bool, spd['enabled']))}")
lines.append(f" enabled: {_bool(spd['enabled'])}")
lines.append("") lines.append("")
lines.append("forward_proxy:") lines.append("forward_proxy:")
fp = cfg["forward_proxy"] fp = cast(dict[str, object], cfg["forward_proxy"])
assert isinstance(fp, dict) lines.append(f" enabled: {_bool(cast(bool, fp['enabled']))}")
lines.append(f" enabled: {_bool(fp['enabled'])}")
lines.append("") lines.append("")
lines.append("dlp:") lines.append("dlp:")
dlp = cfg["dlp"] dlp = cast(dict[str, object], cfg["dlp"])
assert isinstance(dlp, dict) lines.append(f" include_defaults: {_bool(cast(bool, dlp['include_defaults']))}")
lines.append(f" include_defaults: {_bool(dlp['include_defaults'])}") lines.append(f" scan_env: {_bool(cast(bool, dlp['scan_env']))}")
lines.append(f" scan_env: {_bool(dlp['scan_env'])}")
lines.append("") lines.append("")
lines.append("request_body_scanning:") lines.append("request_body_scanning:")
rbs = cfg["request_body_scanning"] rbs = cast(dict[str, object], cfg["request_body_scanning"])
assert isinstance(rbs, dict) lines.append(f' action: "{cast(str, rbs["action"])}"')
lines.append(f' action: "{rbs["action"]}"')
if "scan_headers" in rbs: if "scan_headers" in rbs:
lines.append(f" scan_headers: {_bool(rbs['scan_headers'])}") lines.append(f" scan_headers: {_bool(cast(bool, rbs['scan_headers']))}")
if "header_mode" in rbs: if "header_mode" in rbs:
lines.append(f' header_mode: "{rbs["header_mode"]}"') lines.append(f' header_mode: "{cast(str, rbs["header_mode"])}"')
if "tls_interception" in cfg: if "tls_interception" in cfg:
lines.append("") lines.append("")
lines.append("tls_interception:") lines.append("tls_interception:")
tls = cfg["tls_interception"] tls = cast(dict[str, object], cfg["tls_interception"])
assert isinstance(tls, dict) lines.append(f" enabled: {_bool(cast(bool, tls['enabled']))}")
lines.append(f" enabled: {_bool(tls['enabled'])}") lines.append(f' ca_cert: "{cast(str, tls["ca_cert"])}"')
lines.append(f' ca_cert: "{tls["ca_cert"]}"') lines.append(f' ca_key: "{cast(str, tls["ca_key"])}"')
lines.append(f' ca_key: "{tls["ca_key"]}"') passthrough = cast(list[str], tls["passthrough_domains"])
passthrough = tls["passthrough_domains"]
assert isinstance(passthrough, list)
if passthrough: if passthrough:
lines.append(" passthrough_domains:") lines.append(" passthrough_domains:")
for d in passthrough: for d in passthrough:
@@ -457,11 +454,9 @@ def pipelock_render_yaml(cfg: dict[str, object]) -> str:
if "ssrf" in cfg: if "ssrf" in cfg:
lines.append("") lines.append("")
lines.append("ssrf:") lines.append("ssrf:")
ssrf = cfg["ssrf"] ssrf = cast(dict[str, object], cfg["ssrf"])
assert isinstance(ssrf, dict)
lines.append(" ip_allowlist:") lines.append(" ip_allowlist:")
ip_allowlist = ssrf["ip_allowlist"] ip_allowlist = cast(list[str], ssrf["ip_allowlist"])
assert isinstance(ip_allowlist, list)
for ip in ip_allowlist: for ip in ip_allowlist:
lines.append(f' - "{ip}"') lines.append(f' - "{ip}"')
return "\n".join(lines) + "\n" return "\n".join(lines) + "\n"