terok-executor 0.1.0__py3-none-any.whl

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.
Files changed (110) hide show
  1. terok_executor/__init__.py +179 -0
  2. terok_executor/_tree.py +39 -0
  3. terok_executor/_util/__init__.py +18 -0
  4. terok_executor/_util/_timezone.py +49 -0
  5. terok_executor/_util/_yaml.py +24 -0
  6. terok_executor/acp/__init__.py +69 -0
  7. terok_executor/acp/cache.py +81 -0
  8. terok_executor/acp/daemon.py +250 -0
  9. terok_executor/acp/endpoint.py +35 -0
  10. terok_executor/acp/model_options.py +159 -0
  11. terok_executor/acp/probe.py +156 -0
  12. terok_executor/acp/proxy.py +640 -0
  13. terok_executor/acp/roster.py +271 -0
  14. terok_executor/cli.py +91 -0
  15. terok_executor/commands.py +955 -0
  16. terok_executor/config.py +31 -0
  17. terok_executor/config_schema.py +150 -0
  18. terok_executor/container/__init__.py +9 -0
  19. terok_executor/container/build.py +1133 -0
  20. terok_executor/container/cache.py +127 -0
  21. terok_executor/container/env.py +599 -0
  22. terok_executor/container/inject.py +41 -0
  23. terok_executor/container/runner.py +1230 -0
  24. terok_executor/container/sidecar.py +166 -0
  25. terok_executor/credentials/__init__.py +10 -0
  26. terok_executor/credentials/auth.py +1027 -0
  27. terok_executor/credentials/extractors.py +217 -0
  28. terok_executor/credentials/vault_commands.py +202 -0
  29. terok_executor/credentials/vault_config.py +612 -0
  30. terok_executor/credentials/vendor_files.py +257 -0
  31. terok_executor/doctor.py +325 -0
  32. terok_executor/integrations/__init__.py +17 -0
  33. terok_executor/integrations/sandbox.py +125 -0
  34. terok_executor/krun.py +369 -0
  35. terok_executor/paths.py +40 -0
  36. terok_executor/preflight.py +447 -0
  37. terok_executor/provider/__init__.py +12 -0
  38. terok_executor/provider/agents.py +562 -0
  39. terok_executor/provider/instructions.py +126 -0
  40. terok_executor/provider/providers.py +496 -0
  41. terok_executor/provider/wrappers.py +541 -0
  42. terok_executor/py.typed +0 -0
  43. terok_executor/resources/__init__.py +4 -0
  44. terok_executor/resources/agents/__init__.py +4 -0
  45. terok_executor/resources/agents/blablador.yaml +68 -0
  46. terok_executor/resources/agents/caddy.yaml +29 -0
  47. terok_executor/resources/agents/claude.yaml +115 -0
  48. terok_executor/resources/agents/coderabbit.yaml +33 -0
  49. terok_executor/resources/agents/codex.yaml +89 -0
  50. terok_executor/resources/agents/copilot.yaml +39 -0
  51. terok_executor/resources/agents/gh.yaml +58 -0
  52. terok_executor/resources/agents/glab.yaml +65 -0
  53. terok_executor/resources/agents/kisski.yaml +62 -0
  54. terok_executor/resources/agents/opencode.yaml +65 -0
  55. terok_executor/resources/agents/openrouter.yaml +68 -0
  56. terok_executor/resources/agents/pi.yaml +63 -0
  57. terok_executor/resources/agents/sonar.yaml +55 -0
  58. terok_executor/resources/agents/toad.yaml +40 -0
  59. terok_executor/resources/agents/vibe.yaml +82 -0
  60. terok_executor/resources/instructions/__init__.py +4 -0
  61. terok_executor/resources/instructions/default.md +54 -0
  62. terok_executor/resources/scripts/Caddyfile +39 -0
  63. terok_executor/resources/scripts/__init__.py +4 -0
  64. terok_executor/resources/scripts/allthethings.sh +40 -0
  65. terok_executor/resources/scripts/hilfe +72 -0
  66. terok_executor/resources/scripts/init-ssh-and-repo.sh +445 -0
  67. terok_executor/resources/scripts/mistral-model-sync.py +292 -0
  68. terok_executor/resources/scripts/opencode-provider +390 -0
  69. terok_executor/resources/scripts/opencode-provider-acp +34 -0
  70. terok_executor/resources/scripts/opencode-session-plugin.mjs +26 -0
  71. terok_executor/resources/scripts/opencode-toad +63 -0
  72. terok_executor/resources/scripts/pi-env.sh +34 -0
  73. terok_executor/resources/scripts/pi-vault-routes.mjs +46 -0
  74. terok_executor/resources/scripts/setup-codex-auth.sh +54 -0
  75. terok_executor/resources/scripts/terok-acp-env.sh +23 -0
  76. terok_executor/resources/scripts/terok-bash-banner.sh +14 -0
  77. terok_executor/resources/scripts/terok-claude-acp +30 -0
  78. terok_executor/resources/scripts/terok-codex-acp +34 -0
  79. terok_executor/resources/scripts/terok-copilot-acp +37 -0
  80. terok_executor/resources/scripts/terok-env-git-identity.sh +61 -0
  81. terok_executor/resources/scripts/terok-env.sh +96 -0
  82. terok_executor/resources/scripts/terok-opencode-acp +24 -0
  83. terok_executor/resources/scripts/terok-toad-entry +79 -0
  84. terok_executor/resources/scripts/terok-trust-workspace.py +104 -0
  85. terok_executor/resources/scripts/terok-vibe-acp +91 -0
  86. terok_executor/resources/scripts/toad +128 -0
  87. terok_executor/resources/scripts/update-all-the-things +94 -0
  88. terok_executor/resources/scripts/vibe-model-sync.sh +37 -0
  89. terok_executor/resources/templates/__init__.py +4 -0
  90. terok_executor/resources/templates/l0.dev.Dockerfile.template +142 -0
  91. terok_executor/resources/templates/l1.agent-cli.Dockerfile.template +129 -0
  92. terok_executor/resources/templates/l1.sidecar.Dockerfile.template +38 -0
  93. terok_executor/resources/tmux/__init__.py +4 -0
  94. terok_executor/resources/tmux/container-tmux.conf +21 -0
  95. terok_executor/resources/toad-agents/__init__.py +4 -0
  96. terok_executor/resources/toad-agents/blablador.helmholtz.de.toml +31 -0
  97. terok_executor/resources/toad-agents/kisski.academiccloud.de.toml +31 -0
  98. terok_executor/roster/__init__.py +18 -0
  99. terok_executor/roster/loader.py +643 -0
  100. terok_executor/roster/schema.py +490 -0
  101. terok_executor/roster/types.py +181 -0
  102. terok_executor/sandbox.py +53 -0
  103. terok_executor/storage.py +110 -0
  104. terok_executor/vault_addr.py +41 -0
  105. terok_executor-0.1.0.dist-info/METADATA +160 -0
  106. terok_executor-0.1.0.dist-info/RECORD +110 -0
  107. terok_executor-0.1.0.dist-info/WHEEL +4 -0
  108. terok_executor-0.1.0.dist-info/entry_points.txt +3 -0
  109. terok_executor-0.1.0.dist-info/licenses/LICENSE +177 -0
  110. terok_executor-0.1.0.dist-info/licenses/LICENSES/Apache-2.0.txt +202 -0
@@ -0,0 +1,179 @@
1
+ # SPDX-FileCopyrightText: 2025 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """terok-executor: single-agent task runner for hardened Podman containers.
5
+
6
+ Builds agent images, launches instrumented containers, and manages the
7
+ lifecycle of one AI coding agent at a time. Designed for standalone use
8
+ (``terok-executor run claude .``) and as a library for terok orchestration.
9
+
10
+ The public surface is ``__all__`` below. Key entry points:
11
+
12
+ - [`AgentRunner`][terok_executor.AgentRunner] — launch agents in containers
13
+ - [`Authenticator`][terok_executor.Authenticator] — credential flow
14
+ - [`ImageBuilder`][terok_executor.ImageBuilder] — image construction
15
+ - [`AgentRoster.shared`][terok_executor.AgentRoster.shared] — YAML agent registry (process-wide cache)
16
+
17
+ Implementation-detail types (raw config schema fragments, ACP error
18
+ classes, internal result types, sidecar image / inject helpers) stay
19
+ in their submodules; reach into ``terok_executor.<sub>`` when you
20
+ need them.
21
+ """
22
+
23
+ __version__: str = "0.1.0" # placeholder; replaced at build time
24
+
25
+ from importlib.metadata import PackageNotFoundError, version as _meta_version
26
+
27
+ try:
28
+ __version__ = _meta_version("terok-executor")
29
+ except PackageNotFoundError:
30
+ pass # editable install or running from source without metadata
31
+
32
+ # -- terok-util shared types (re-exported for convenience) --------------------
33
+ from terok_util import ConfigStack
34
+
35
+ # -- terok-sandbox protocol types (re-exported for convenience) ----------------
36
+ from terok_executor.integrations.sandbox import ConfigScope
37
+
38
+ # -- Commands + CLI surface ----------------------------------------------------
39
+ from ._tree import COMMANDS
40
+
41
+ # -- ACP host-proxy (per-task multi-agent aggregator) -------------------------
42
+ from .acp import ACPEndpointStatus, acp_socket_is_live, list_authenticated_agents
43
+ from .commands import COMMANDS as AGENT_COMMANDS
44
+
45
+ # -- Config schema + read/write accessors for the executor-owned image: section --
46
+ from .config_schema import ExecutorConfigView, RawImageSection
47
+
48
+ # -- Container (build, env assembly, runner) -----------------------------------
49
+ from .container.build import (
50
+ AGENTS_LABEL,
51
+ DEFAULT_BASE_IMAGE,
52
+ BuildError,
53
+ ImageBuilder,
54
+ ImageSet,
55
+ build_project_image,
56
+ )
57
+ from .container.cache import seed_workspace_from_clone_cache
58
+ from .container.env import ContainerEnvSpec, assemble_container_env
59
+ from .container.inject import inject_prompt
60
+ from .container.runner import AgentRunner
61
+
62
+ # -- Credentials (auth flows, extractors, vault commands) ----------------------
63
+ from .credentials.auth import (
64
+ AUTH_PROVIDERS,
65
+ Authenticator,
66
+ AuthSession,
67
+ prepare_oauth_session,
68
+ store_api_key,
69
+ )
70
+ from .credentials.vault_commands import VAULT_COMMANDS, scan_leaked_credentials
71
+
72
+ # -- Krun (KVM-microVM) provisioning + runtime factory -----------------------
73
+ from .krun import KrunHost, KrunHostKeypair, ensure_krun_host_keypair
74
+
75
+ # -- Provider (descriptor + headless behaviour, instructions, agent config) ----
76
+ from .provider.agents import AgentConfigSpec, parse_md_agent, prepare_agent_config_dir
77
+ from .provider.instructions import bundled_default_instructions, resolve_instructions
78
+ from .provider.providers import (
79
+ AGENT_PROVIDERS,
80
+ PROVIDER_NAMES,
81
+ AgentProvider,
82
+ CLIOverrides,
83
+ get_provider,
84
+ resolve_provider_value,
85
+ )
86
+
87
+ # -- Roster (agent catalog + config resolution) --------------------------------
88
+ from .roster import AgentRoster
89
+
90
+ # -- Sandbox bootstrap composition ---------------------------------------------
91
+ from .sandbox import ensure_sandbox_ready
92
+
93
+ # -- Storage queries (filesystem footprint measurement) -------------------------
94
+ from .storage import SharedMountStorageInfo, TaskStorageInfo
95
+
96
+ # -- Bootstrap YAML roster into module-level dicts ---------------------------
97
+ # AGENT_PROVIDERS and AUTH_PROVIDERS are empty dicts populated here to avoid
98
+ # circular imports (roster → auth/providers → roster).
99
+
100
+
101
+ def _bootstrap_roster() -> None:
102
+ """Populate module-level provider dicts from the YAML roster."""
103
+ global PROVIDER_NAMES # noqa: PLW0603 — tuple requires rebind
104
+
105
+ import terok_executor.provider.providers as _reg
106
+
107
+ roster = AgentRoster.shared()
108
+ AGENT_PROVIDERS.update(roster.providers)
109
+ AUTH_PROVIDERS.update(roster.auth_providers)
110
+ PROVIDER_NAMES = _reg.PROVIDER_NAMES = roster.agent_names
111
+
112
+
113
+ _bootstrap_roster()
114
+
115
+ __all__ = [
116
+ "__version__",
117
+ # ACP host-proxy
118
+ "ACPEndpointStatus",
119
+ "acp_socket_is_live",
120
+ "list_authenticated_agents",
121
+ # Provider registry + behaviour
122
+ "AGENT_PROVIDERS",
123
+ "AgentProvider",
124
+ "CLIOverrides",
125
+ "PROVIDER_NAMES",
126
+ "get_provider",
127
+ "resolve_provider_value",
128
+ # Agent config preparation
129
+ "AgentConfigSpec",
130
+ "parse_md_agent",
131
+ "prepare_agent_config_dir",
132
+ # Auth
133
+ "AUTH_PROVIDERS",
134
+ "Authenticator",
135
+ "AuthSession",
136
+ "prepare_oauth_session",
137
+ "store_api_key",
138
+ # Instructions
139
+ "bundled_default_instructions",
140
+ "resolve_instructions",
141
+ # Config stack
142
+ "ConfigScope",
143
+ "ConfigStack",
144
+ # Config schema (executor-owned slice of the shared config.yml)
145
+ "ExecutorConfigView",
146
+ "RawImageSection",
147
+ # Build: image construction + resource staging
148
+ "AGENTS_LABEL",
149
+ "DEFAULT_BASE_IMAGE",
150
+ "BuildError",
151
+ "ImageBuilder",
152
+ "ImageSet",
153
+ "build_project_image",
154
+ # Vault credential scanning
155
+ "scan_leaked_credentials",
156
+ # Roster
157
+ "AgentRoster",
158
+ # Command registry
159
+ "AGENT_COMMANDS",
160
+ "COMMANDS",
161
+ "VAULT_COMMANDS",
162
+ # Storage queries
163
+ "SharedMountStorageInfo",
164
+ "TaskStorageInfo",
165
+ # Runner facade
166
+ "AgentRunner",
167
+ # Container environment assembly
168
+ "ContainerEnvSpec",
169
+ "assemble_container_env",
170
+ # Clone cache + injection helpers
171
+ "inject_prompt",
172
+ "seed_workspace_from_clone_cache",
173
+ # Sandbox bootstrap composition
174
+ "ensure_sandbox_ready",
175
+ # Krun (KVM-microVM) provisioning + runtime factory
176
+ "KrunHost",
177
+ "KrunHostKeypair",
178
+ "ensure_krun_host_keypair",
179
+ ]
@@ -0,0 +1,39 @@
1
+ # SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Composes executor's full [`CommandTree`][terok_util.cli_types.CommandTree].
5
+
6
+ Lives below the CLI surface so the package init can re-export the
7
+ composed tree as ``terok_executor.COMMANDS`` (the cli module is at
8
+ the top of the dependency graph; nothing below it may import it).
9
+
10
+ Three views over one underlying ``SANDBOX_TREE`` instance:
11
+
12
+ - ``terok-executor <own-verb>`` — executor's verbs (run, auth, …)
13
+ - ``terok-executor sandbox <verb>`` — full sandbox tree, deep path
14
+ - ``terok-executor vault <verb>`` — shortcut sharing identity with
15
+ the corresponding subtree under ``sandbox``
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ from terok_util import CommandDef, CommandTree
21
+
22
+ from .commands import COMMANDS as OWN_COMMANDS
23
+ from .credentials.vault_commands import SANDBOX_TREE, VAULT_COMMANDS
24
+
25
+ #: Executor's top-level command tree. See module docstring.
26
+ COMMANDS: CommandTree = CommandTree(
27
+ OWN_COMMANDS
28
+ + (
29
+ CommandDef(
30
+ name="sandbox",
31
+ help="Sandbox subsystem (full deep tree — same verbs as terok-sandbox)",
32
+ children=SANDBOX_TREE.roots,
33
+ ),
34
+ )
35
+ + VAULT_COMMANDS
36
+ )
37
+
38
+
39
+ __all__ = ["COMMANDS"]
@@ -0,0 +1,18 @@
1
+ # SPDX-FileCopyrightText: 2025 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Re-exports executor-only utilities (timezone, YAML loader) for internal use.
5
+
6
+ Standalone — no terok-executor domain imports, safe to use from any layer.
7
+ Cross-package helpers (``ensure_dir``, ``podman_userns_args``, ...) live in
8
+ the shared [`terok_util`][terok_util] package and are imported from there
9
+ directly at every call site.
10
+ """
11
+
12
+ from ._timezone import detect_host_timezone
13
+ from ._yaml import load as yaml_load
14
+
15
+ __all__ = [
16
+ "detect_host_timezone",
17
+ "yaml_load",
18
+ ]
@@ -0,0 +1,49 @@
1
+ # SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Detects the host's IANA timezone for propagation into containers.
5
+
6
+ Returned as a plain string (``"Europe/Prague"``, ``"UTC"``, …) suitable
7
+ for use as a ``TZ`` env var inside the container — glibc resolves it
8
+ against ``/usr/share/zoneinfo`` without needing the host's filesystem.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import os
14
+ from pathlib import Path
15
+
16
+ _ZONEINFO_MARKER = "/zoneinfo/"
17
+
18
+
19
+ def detect_host_timezone() -> str | None:
20
+ """Return the host's IANA timezone name, or ``None`` if it can't be detected.
21
+
22
+ Tried in order:
23
+
24
+ 1. ``$TZ`` — the user's explicit override.
25
+ 2. ``/etc/timezone`` — Debian/Ubuntu convention, single-line zone name.
26
+ 3. ``/etc/localtime`` symlink — systemd-family hosts (and macOS) symlink
27
+ this into the zoneinfo database; the zone name is the path suffix
28
+ after the ``zoneinfo/`` component.
29
+
30
+ Returns ``None`` on hosts that expose none of the above (containers with
31
+ only a copied-in ``/etc/localtime`` file, for instance), letting the
32
+ caller fall back to the image default rather than guessing.
33
+ """
34
+ if tz := os.environ.get("TZ"):
35
+ return tz
36
+
37
+ try:
38
+ if zone := Path("/etc/timezone").read_text(encoding="utf-8").strip():
39
+ return zone
40
+ except OSError:
41
+ pass
42
+
43
+ try:
44
+ target = Path("/etc/localtime").resolve().as_posix()
45
+ except OSError:
46
+ return None
47
+ if _ZONEINFO_MARKER in target:
48
+ return target.split(_ZONEINFO_MARKER, 1)[1]
49
+ return None
@@ -0,0 +1,24 @@
1
+ # SPDX-FileCopyrightText: 2025 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Loads YAML strings with round-trip fidelity (comments, order, quotes preserved).
5
+
6
+ Used for frontmatter parsing and config stack loading. Vendored from
7
+ terok.lib.util.yaml — only the load path is needed here.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import Any
13
+
14
+ from ruamel.yaml import YAML
15
+
16
+ __all__ = ["load"]
17
+
18
+ _yaml = YAML(typ="rt")
19
+ _yaml.preserve_quotes = True
20
+
21
+
22
+ def load(text: str) -> Any:
23
+ """Round-trip load from a YAML string, preserving comments and order."""
24
+ return _yaml.load(text)
@@ -0,0 +1,69 @@
1
+ # SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Per-task host-side ACP (Agent Client Protocol) aggregator.
5
+
6
+ Bridges a single ACP client (Zed, Toad, …) to one of several
7
+ in-container agents (claude, codex, copilot, …) by namespacing models
8
+ as ``agent:model`` (e.g. ``claude:opus-4.6``) under ACP's standard
9
+ ``category: "model"`` configOption.
10
+
11
+ Module map:
12
+
13
+ - [`daemon`][terok_executor.acp.daemon] — Unix-socket server, container
14
+ lifecycle supervision, and the standalone ``terok-executor acp``
15
+ entry point. Owns [`serve_acp`][terok_executor.acp.daemon.serve_acp]
16
+ and the [`acp_socket_is_live`][terok_executor.acp.daemon.acp_socket_is_live]
17
+ probe used to distinguish live daemons from stale socket files.
18
+ - [`roster`][terok_executor.acp.roster] — per-task aggregation: walks
19
+ the image's ``ai.terok.agents`` label, probes each agent, and answers
20
+ "what models does this container offer?" Owns
21
+ [`ACPRoster`][terok_executor.acp.roster.ACPRoster] and the
22
+ vault-side [`list_authenticated_agents`][terok_executor.acp.roster.list_authenticated_agents].
23
+ - [`proxy`][terok_executor.acp.proxy] — the typed bidirectional ACP
24
+ mediator: implements both `acp.Agent` (toward the connected
25
+ client) and `acp.Client` (toward the bound backend wrapper)
26
+ on one object. Drives the bind handshake on first model pick.
27
+ - [`probe`][terok_executor.acp.probe] — the minimal ``initialize +
28
+ session/new`` handshake that extracts an agent's model roster.
29
+ - [`cache`][terok_executor.acp.cache] — thread-safe per-agent model
30
+ cache; survives reconnects, invalidated on credential rotation.
31
+ - [`endpoint`][terok_executor.acp.endpoint] — the
32
+ [`ACPEndpointStatus`][terok_executor.acp.endpoint.ACPEndpointStatus]
33
+ enum the host CLI uses to classify endpoints in ``terok acp list``.
34
+ - [`model_options`][terok_executor.acp.model_options] — the
35
+ ``agent:model`` namespace vocabulary and the typed builders +
36
+ rewriter that keep the proxy's frames schema-valid.
37
+
38
+ Bind-trigger surfaces: explicit ``session/set_model`` /
39
+ ``session/set_config_option(configId="model")``, or — for clients
40
+ that trust the advertised ``currentModelId`` — lazily on the first
41
+ backend-needing method (e.g. ``session/prompt``). Cross-agent
42
+ switching mid-session is out of scope for v1; subsequent picks against
43
+ a different agent are rejected at the protocol level.
44
+
45
+ The exports below are re-exported from ``terok_executor`` so the
46
+ host-side caller (terok) doesn't have to reach into the submodules.
47
+ """
48
+
49
+ from __future__ import annotations
50
+
51
+ from .cache import AgentRosterCache, CacheKey
52
+ from .daemon import acp_socket_is_live, serve_acp
53
+ from .endpoint import ACPEndpointStatus
54
+ from .probe import ProbeError, probe_agent_models
55
+ from .proxy import AgentBindError
56
+ from .roster import ACPRoster, list_authenticated_agents
57
+
58
+ __all__ = [
59
+ "ACPEndpointStatus",
60
+ "ACPRoster",
61
+ "AgentBindError",
62
+ "AgentRosterCache",
63
+ "CacheKey",
64
+ "ProbeError",
65
+ "acp_socket_is_live",
66
+ "list_authenticated_agents",
67
+ "probe_agent_models",
68
+ "serve_acp",
69
+ ]
@@ -0,0 +1,81 @@
1
+ # SPDX-FileCopyrightText: 2026 Jiri Vyskocil
2
+ # SPDX-License-Identifier: Apache-2.0
3
+
4
+ """Per-agent model roster cache for the ACP host-proxy.
5
+
6
+ Probing an agent (initialize + session/new + read configOptions) is
7
+ expensive and the result is stable for the lifetime of an authenticated
8
+ session. The cache is keyed ``(image_id, auth_identity, agent_id)``:
9
+ same image, same auth, same agent ⇒ same model list.
10
+
11
+ The cache is populated lazily on the first ``session/new`` after a new
12
+ auth, and never re-probed mid-session. ``invalidate_auth`` lets workflows
13
+ flush an entire identity's worth of entries when credentials change (today
14
+ auth is global so this is rarely useful; the hook exists for future
15
+ per-project auth).
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import threading
21
+ from dataclasses import dataclass
22
+
23
+
24
+ @dataclass(frozen=True)
25
+ class CacheKey:
26
+ """Composite key for one agent's roster within one auth scope.
27
+
28
+ ``auth_identity`` is the constant ``"global"`` today (terok auth is
29
+ process-wide); the field exists from day one so per-project auth can
30
+ slot in without a key-schema migration.
31
+ """
32
+
33
+ image_id: str
34
+ auth_identity: str
35
+ agent_id: str
36
+
37
+
38
+ class AgentRosterCache:
39
+ """Thread-safe map from [`CacheKey`][terok_executor.acp.cache.CacheKey] to a tuple of model ids.
40
+
41
+ Models are stored as a tuple so cache entries are immutable once
42
+ inserted — callers can return them directly without defensive copying.
43
+ Empty tuples are valid and signal "probe ran but yielded nothing"
44
+ (saved to avoid hammering a misconfigured agent on every session).
45
+ """
46
+
47
+ def __init__(self) -> None:
48
+ self._models: dict[CacheKey, tuple[str, ...]] = {}
49
+ self._lock = threading.Lock()
50
+
51
+ def get(self, key: CacheKey) -> tuple[str, ...] | None:
52
+ """Return cached models for *key*, or ``None`` if not yet probed."""
53
+ with self._lock:
54
+ return self._models.get(key)
55
+
56
+ def put(self, key: CacheKey, models: tuple[str, ...]) -> None:
57
+ """Store *models* under *key*, replacing any existing entry."""
58
+ with self._lock:
59
+ self._models[key] = models
60
+
61
+ def invalidate_auth(self, auth_identity: str) -> None:
62
+ """Drop every entry tied to *auth_identity*.
63
+
64
+ Used when credentials for an identity rotate — the next
65
+ ``session/new`` re-probes affected agents.
66
+ """
67
+ with self._lock:
68
+ self._models = {
69
+ k: v for k, v in self._models.items() if k.auth_identity != auth_identity
70
+ }
71
+
72
+ def __len__(self) -> int:
73
+ """Return the number of cached entries (for tests / introspection)."""
74
+ with self._lock:
75
+ return len(self._models)
76
+
77
+
78
+ # Module-level singleton: most callers get this implicitly via
79
+ # [`ACPRoster`][terok_executor.acp.roster.ACPRoster]'s default. Tests inject a fresh
80
+ # [`AgentRosterCache`][terok_executor.acp.cache.AgentRosterCache] via the constructor.
81
+ GLOBAL_CACHE = AgentRosterCache()