clink-cli 0.2.1__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.
- clink/__init__.py +19 -0
- clink/agents/__init__.py +30 -0
- clink/agents/base.py +70 -0
- clink/agents/claude.py +7 -0
- clink/agents/codex.py +7 -0
- clink/agents/gemini.py +7 -0
- clink/allowed_roots.py +356 -0
- clink/backend.py +765 -0
- clink/cli.py +594 -0
- clink/constants.py +78 -0
- clink/contracts.py +167 -0
- clink/files.py +70 -0
- clink/generation.py +274 -0
- clink/generation_capabilities.py +207 -0
- clink/generation_contracts.py +214 -0
- clink/generation_policy.py +174 -0
- clink/models.py +243 -0
- clink/native.py +409 -0
- clink/native_adapters.py +244 -0
- clink/native_contracts.py +68 -0
- clink/native_generation.py +421 -0
- clink/native_generation_claude_policy.py +59 -0
- clink/native_generation_contracts.py +264 -0
- clink/native_generation_families.py +218 -0
- clink/native_generation_family_policy.py +14 -0
- clink/native_generation_files.py +153 -0
- clink/native_generation_policy.py +82 -0
- clink/native_policy.py +86 -0
- clink/parsers/__init__.py +34 -0
- clink/parsers/agy.py +53 -0
- clink/parsers/agy_native.py +161 -0
- clink/parsers/agy_native_generation.py +189 -0
- clink/parsers/base.py +209 -0
- clink/parsers/claude.py +104 -0
- clink/parsers/claude_native.py +113 -0
- clink/parsers/claude_native_generation.py +346 -0
- clink/parsers/codex.py +172 -0
- clink/parsers/codex_generation.py +212 -0
- clink/parsers/codex_native.py +236 -0
- clink/parsers/codex_native_generation.py +367 -0
- clink/parsers/gemini.py +110 -0
- clink/parsers/grok.py +63 -0
- clink/parsers/grok_native.py +91 -0
- clink/parsers/grok_native_generation.py +157 -0
- clink/policy.py +180 -0
- clink/registry.py +544 -0
- clink/resources/CODEX-LICENSE +201 -0
- clink/resources/CODEX-NOTICE +6 -0
- clink/resources/PROVENANCE.md +16 -0
- clink/resources/codex-0.166.0-models-direct.json +1284 -0
- clink/resources/codex-0.166.0-models-generation.json +1269 -0
- clink/resources/native-agy-1.4.12-admission.json +746 -0
- clink/resources/native-claude-2.1.304-admission.json +99 -0
- clink/resources/native-generation-claude-2.1.304-admission.json +277 -0
- clink/resources/native-generation-codex-0.166.0-admission.json +309 -0
- clink/resources/native-generation-grok-1.0.70-admission.json +329 -0
- clink/resources/native-grok-1.0.70-admission.json +644 -0
- clink/runner.py +303 -0
- clink/setup.py +399 -0
- clink/tasks.py +695 -0
- clink_cli-0.2.1.dist-info/METADATA +225 -0
- clink_cli-0.2.1.dist-info/RECORD +141 -0
- clink_cli-0.2.1.dist-info/WHEEL +5 -0
- clink_cli-0.2.1.dist-info/entry_points.txt +3 -0
- clink_cli-0.2.1.dist-info/licenses/LICENSE +197 -0
- clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-LICENSE +201 -0
- clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-NOTICE +6 -0
- clink_cli-0.2.1.dist-info/top_level.txt +7 -0
- conf/__init__.py +1 -0
- conf/cli_clients/agy.json +40 -0
- conf/cli_clients/claude.json +39 -0
- conf/cli_clients/codex.json +40 -0
- conf/cli_clients/gemini.json +23 -0
- conf/cli_clients/grok.json +40 -0
- config.py +40 -0
- server.py +896 -0
- systemprompts/__init__.py +35 -0
- systemprompts/analyze_prompt.py +87 -0
- systemprompts/chat_prompt.py +57 -0
- systemprompts/clink/codex_codereviewer.txt +8 -0
- systemprompts/clink/default.txt +8 -0
- systemprompts/clink/default_codereviewer.txt +8 -0
- systemprompts/clink/default_planner.txt +7 -0
- systemprompts/clink/native_codereviewer.txt +1 -0
- systemprompts/clink/native_default.txt +1 -0
- systemprompts/clink/native_planner.txt +1 -0
- systemprompts/codereview_prompt.py +91 -0
- systemprompts/consensus_prompt.py +120 -0
- systemprompts/debug_prompt.py +150 -0
- systemprompts/docgen_prompt.py +252 -0
- systemprompts/generate_code_prompt.py +181 -0
- systemprompts/planner_prompt.py +127 -0
- systemprompts/precommit_prompt.py +179 -0
- systemprompts/refactor_prompt.py +323 -0
- systemprompts/secaudit_prompt.py +420 -0
- systemprompts/testgen_prompt.py +133 -0
- systemprompts/thinkdeep_prompt.py +55 -0
- systemprompts/tracer_prompt.py +145 -0
- tools/__init__.py +43 -0
- tools/analyze.py +583 -0
- tools/apilookup.py +97 -0
- tools/challenge.py +198 -0
- tools/chat.py +187 -0
- tools/clink.py +130 -0
- tools/codereview.py +724 -0
- tools/consensus.py +790 -0
- tools/debug.py +573 -0
- tools/docgen.py +577 -0
- tools/listmodels.py +48 -0
- tools/models.py +373 -0
- tools/planner.py +533 -0
- tools/precommit.py +776 -0
- tools/refactor.py +645 -0
- tools/secaudit.py +685 -0
- tools/shared/__init__.py +19 -0
- tools/shared/base_models.py +167 -0
- tools/shared/base_tool.py +659 -0
- tools/shared/exceptions.py +20 -0
- tools/shared/schema_builders.py +159 -0
- tools/simple/__init__.py +18 -0
- tools/simple/base.py +539 -0
- tools/testgen.py +513 -0
- tools/thinkdeep.py +586 -0
- tools/tracer.py +763 -0
- tools/version.py +53 -0
- tools/workflow/__init__.py +22 -0
- tools/workflow/base.py +448 -0
- tools/workflow/schema_builders.py +166 -0
- tools/workflow/workflow_mixin.py +1195 -0
- utils/__init__.py +21 -0
- utils/call_state.py +349 -0
- utils/client_info.py +293 -0
- utils/consensus_manifest.py +131 -0
- utils/conversation_memory.py +582 -0
- utils/env.py +111 -0
- utils/file_types.py +271 -0
- utils/file_utils.py +803 -0
- utils/image_utils.py +94 -0
- utils/security_config.py +163 -0
- utils/storage_backend.py +177 -0
- utils/token_utils.py +54 -0
clink/models.py
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
"""Strict manifest models and frozen resolved clink configuration."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Any, Literal
|
|
8
|
+
|
|
9
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator
|
|
10
|
+
|
|
11
|
+
_NAME_PATTERN = re.compile(r"^[A-Za-z][A-Za-z0-9_-]{0,63}$")
|
|
12
|
+
_ENVIRONMENT_NAME = re.compile(r"^[A-Za-z_][A-Za-z0-9_]{0,127}$")
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _validate_name(value: str) -> str:
|
|
16
|
+
if not _NAME_PATTERN.fullmatch(value):
|
|
17
|
+
raise ValueError("must start with a letter and contain only letters, digits, underscores, or hyphens")
|
|
18
|
+
return value
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _validate_model(value: str | None) -> str | None:
|
|
22
|
+
if value is None or value == "auto":
|
|
23
|
+
return None
|
|
24
|
+
if (
|
|
25
|
+
not value
|
|
26
|
+
or value != value.strip()
|
|
27
|
+
or value.startswith("-")
|
|
28
|
+
or len(value) > 256
|
|
29
|
+
or any(ord(char) < 32 or ord(char) == 127 for char in value)
|
|
30
|
+
):
|
|
31
|
+
raise ValueError("model must be a nonempty native identifier without control characters")
|
|
32
|
+
return value
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class OutputCaptureConfig(BaseModel):
|
|
36
|
+
"""Legacy import compatibility; generic output capture is not admitted by manifests."""
|
|
37
|
+
|
|
38
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
39
|
+
|
|
40
|
+
flag_template: str
|
|
41
|
+
cleanup: bool = True
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class CLIRoleConfig(BaseModel):
|
|
45
|
+
"""Operator-owned role manifest with no argument or type coercion."""
|
|
46
|
+
|
|
47
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
48
|
+
|
|
49
|
+
prompt_path: str | None = None
|
|
50
|
+
role_args: list[str] = Field(default_factory=list)
|
|
51
|
+
description: str | None = None
|
|
52
|
+
model: str | None = None
|
|
53
|
+
|
|
54
|
+
_validate_native_model = field_validator("model")(_validate_model)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class NativeTaskConfig(BaseModel):
|
|
58
|
+
"""Opt-in task profile; inherited environment is configured by name only."""
|
|
59
|
+
|
|
60
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
61
|
+
|
|
62
|
+
enabled: bool = False
|
|
63
|
+
inherit_env: list[str] = Field(default_factory=list, max_length=128)
|
|
64
|
+
required_env: list[str] = Field(default_factory=list, max_length=128)
|
|
65
|
+
roles: dict[str, CLIRoleConfig] = Field(default_factory=dict, max_length=64)
|
|
66
|
+
default_model: str | None = None
|
|
67
|
+
|
|
68
|
+
_validate_native_model = field_validator("default_model")(_validate_model)
|
|
69
|
+
|
|
70
|
+
@field_validator("inherit_env", "required_env")
|
|
71
|
+
@classmethod
|
|
72
|
+
def _environment_names(cls, names: list[str]) -> list[str]:
|
|
73
|
+
for name in names:
|
|
74
|
+
if not _ENVIRONMENT_NAME.fullmatch(name) or name.startswith("CLINK_"):
|
|
75
|
+
raise ValueError("native environment entries must be valid names outside the server namespace")
|
|
76
|
+
if len(names) != len(set(names)):
|
|
77
|
+
raise ValueError("native environment names must be unique")
|
|
78
|
+
return names
|
|
79
|
+
|
|
80
|
+
@field_validator("roles")
|
|
81
|
+
@classmethod
|
|
82
|
+
def _role_names(cls, roles: dict[str, CLIRoleConfig]) -> dict[str, CLIRoleConfig]:
|
|
83
|
+
for name in roles:
|
|
84
|
+
_validate_name(name)
|
|
85
|
+
return roles
|
|
86
|
+
|
|
87
|
+
@model_validator(mode="after")
|
|
88
|
+
def _required_are_inherited(self) -> NativeTaskConfig:
|
|
89
|
+
if not set(self.required_env).issubset(self.inherit_env):
|
|
90
|
+
raise ValueError("required_env names must also appear in inherit_env")
|
|
91
|
+
return self
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class GenerationNativeConfig(BaseModel):
|
|
95
|
+
"""Operator-owned environment names for separate native generation only."""
|
|
96
|
+
|
|
97
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
98
|
+
|
|
99
|
+
inherit_env: list[str] = Field(default_factory=list, max_length=128)
|
|
100
|
+
required_env: list[str] = Field(default_factory=list, max_length=128)
|
|
101
|
+
|
|
102
|
+
@field_validator("inherit_env", "required_env")
|
|
103
|
+
@classmethod
|
|
104
|
+
def _environment_names(cls, names: list[str]) -> list[str]:
|
|
105
|
+
for name in names:
|
|
106
|
+
if not _ENVIRONMENT_NAME.fullmatch(name) or name.startswith("CLINK_"):
|
|
107
|
+
raise ValueError("native environment entries must be valid names outside the server namespace")
|
|
108
|
+
if len(names) != len(set(names)):
|
|
109
|
+
raise ValueError("native environment names must be unique")
|
|
110
|
+
return names
|
|
111
|
+
|
|
112
|
+
@model_validator(mode="after")
|
|
113
|
+
def _required_are_inherited(self) -> GenerationNativeConfig:
|
|
114
|
+
if not set(self.required_env).issubset(self.inherit_env):
|
|
115
|
+
raise ValueError("required_env names must also appear in inherit_env")
|
|
116
|
+
return self
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
class CLIClientConfig(BaseModel):
|
|
120
|
+
"""Raw version-one manifest; every recognized field is validated explicitly."""
|
|
121
|
+
|
|
122
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
123
|
+
|
|
124
|
+
schema_version: int = Field(default=1, ge=1, le=1)
|
|
125
|
+
name: str
|
|
126
|
+
adapter: str | None = None
|
|
127
|
+
command: str | None = None
|
|
128
|
+
working_dir: str | None = None
|
|
129
|
+
allowed_roots: list[str] | None = None
|
|
130
|
+
additional_args: list[str] = Field(default_factory=list)
|
|
131
|
+
env: dict[str, str] = Field(default_factory=dict)
|
|
132
|
+
timeout_seconds: int | None = Field(default=None, gt=0, le=1800)
|
|
133
|
+
roles: dict[str, CLIRoleConfig] = Field(default_factory=dict)
|
|
134
|
+
default_model: str | None = None
|
|
135
|
+
native_task: NativeTaskConfig | None = None
|
|
136
|
+
generation_native: GenerationNativeConfig | None = None
|
|
137
|
+
|
|
138
|
+
_validate_client_name = field_validator("name")(_validate_name)
|
|
139
|
+
_validate_native_model = field_validator("default_model")(_validate_model)
|
|
140
|
+
|
|
141
|
+
@field_validator("roles")
|
|
142
|
+
@classmethod
|
|
143
|
+
def _validate_role_names(cls, value: dict[str, CLIRoleConfig]) -> dict[str, CLIRoleConfig]:
|
|
144
|
+
for name in value:
|
|
145
|
+
_validate_name(name)
|
|
146
|
+
return value
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class ConsumerCLIConfig(BaseModel):
|
|
150
|
+
"""Closed generation-only manifest for one independently bound consumer."""
|
|
151
|
+
|
|
152
|
+
model_config = ConfigDict(extra="forbid", strict=True)
|
|
153
|
+
|
|
154
|
+
schema_version: int = Field(ge=1, le=1)
|
|
155
|
+
name: Literal["claude", "codex", "agy", "grok"]
|
|
156
|
+
adapter: Literal["claude", "codex", "agy", "grok"]
|
|
157
|
+
command: str
|
|
158
|
+
allowed_roots: list[str]
|
|
159
|
+
generation_native: GenerationNativeConfig
|
|
160
|
+
|
|
161
|
+
@model_validator(mode="after")
|
|
162
|
+
def _matching_adapter(self) -> ConsumerCLIConfig:
|
|
163
|
+
if self.name != self.adapter:
|
|
164
|
+
raise ValueError("consumer name must match its adapter")
|
|
165
|
+
return self
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
class ResolvedCLIRole(BaseModel):
|
|
169
|
+
"""Role snapshot, including prompt text read once at registry initialization."""
|
|
170
|
+
|
|
171
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
172
|
+
|
|
173
|
+
name: str
|
|
174
|
+
prompt_path: Path
|
|
175
|
+
prompt_text: str = ""
|
|
176
|
+
role_args: tuple[str, ...] = ()
|
|
177
|
+
description: str | None = None
|
|
178
|
+
model: str | None = None
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
class ResolvedNativeTask(BaseModel):
|
|
182
|
+
"""Native capability profile, separate from advisory configuration."""
|
|
183
|
+
|
|
184
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
185
|
+
|
|
186
|
+
enabled: bool = False
|
|
187
|
+
inherit_env: tuple[str, ...] = ()
|
|
188
|
+
required_env: tuple[str, ...] = ()
|
|
189
|
+
roles: dict[str, ResolvedCLIRole] = Field(default_factory=dict)
|
|
190
|
+
default_model: str | None = None
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
class ResolvedGenerationNative(BaseModel):
|
|
194
|
+
"""Freeze environment names independently of native-task roles and authority."""
|
|
195
|
+
|
|
196
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
197
|
+
|
|
198
|
+
inherit_env: tuple[str, ...] = ()
|
|
199
|
+
required_env: tuple[str, ...] = ()
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class ResolvedCLIClient(BaseModel):
|
|
203
|
+
"""Frozen runtime fields; backend targets additionally snapshot nested mappings."""
|
|
204
|
+
|
|
205
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
|
206
|
+
|
|
207
|
+
name: str
|
|
208
|
+
adapter: str
|
|
209
|
+
executable: tuple[str, ...]
|
|
210
|
+
working_dir: Path | None
|
|
211
|
+
allowed_roots: tuple[Path, ...] = ()
|
|
212
|
+
allowed_roots_explicit: bool = False
|
|
213
|
+
configuration_scope: str = "unknown"
|
|
214
|
+
internal_args: tuple[str, ...] = ()
|
|
215
|
+
config_args: tuple[str, ...] = ()
|
|
216
|
+
env: dict[str, str] = Field(default_factory=dict)
|
|
217
|
+
timeout_seconds: int
|
|
218
|
+
parser: str
|
|
219
|
+
runner: str | None = None
|
|
220
|
+
roles: dict[str, ResolvedCLIRole]
|
|
221
|
+
default_model: str | None = None
|
|
222
|
+
native_task: ResolvedNativeTask | None = None
|
|
223
|
+
generation_native: ResolvedGenerationNative | None = None
|
|
224
|
+
output_to_file: None = None
|
|
225
|
+
|
|
226
|
+
@model_validator(mode="before")
|
|
227
|
+
@classmethod
|
|
228
|
+
def _legacy_adapter_default(cls, value: Any) -> Any:
|
|
229
|
+
# Older callers construct built-in resolved clients directly in tests and integrations.
|
|
230
|
+
if isinstance(value, dict) and "adapter" not in value:
|
|
231
|
+
value = dict(value)
|
|
232
|
+
value["adapter"] = str(value.get("name", "")).lower()
|
|
233
|
+
return value
|
|
234
|
+
|
|
235
|
+
def list_roles(self) -> list[str]:
|
|
236
|
+
return list(self.roles)
|
|
237
|
+
|
|
238
|
+
def get_role(self, role_name: str | None) -> ResolvedCLIRole:
|
|
239
|
+
key = role_name or "default"
|
|
240
|
+
if key not in self.roles:
|
|
241
|
+
available = ", ".join(sorted(self.roles))
|
|
242
|
+
raise KeyError(f"Role '{role_name}' not configured for CLI '{self.name}'. Available roles: {available}")
|
|
243
|
+
return self.roles[key]
|
clink/native.py
ADDED
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
"""One-shot native task attempts through the backend's shared process limits."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import math
|
|
9
|
+
import os
|
|
10
|
+
import platform
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
import time
|
|
14
|
+
from collections.abc import Callable
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
from clink.backend import CLIBackend, _directory, _model, get_backend
|
|
19
|
+
from clink.contracts import AdapterPolicy, BackendError, BackendTarget, ClientSnapshot, CLIRequest, RoleSnapshot
|
|
20
|
+
from clink.native_adapters import get_native_adapter, prepare_native_launch
|
|
21
|
+
from clink.native_contracts import FrozenNativeContext, NativeRunOutcome
|
|
22
|
+
from clink.native_policy import native_environment_names, native_recipe_digest
|
|
23
|
+
from clink.parsers.base import ParserError
|
|
24
|
+
from clink.policy import executable_digest
|
|
25
|
+
|
|
26
|
+
PROMPT_BYTES = 1024 * 1024
|
|
27
|
+
# This profile has its own genuine native evidence. Advisory receipts cannot admit it.
|
|
28
|
+
_VERIFIED_NATIVE_POLICIES: dict[str, AdapterPolicy] = {
|
|
29
|
+
"claude": AdapterPolicy(
|
|
30
|
+
adapter="claude",
|
|
31
|
+
version="2.1.304 (Claude Code)",
|
|
32
|
+
verified=True,
|
|
33
|
+
verification_status="verified_native_lifecycle",
|
|
34
|
+
file_references=True,
|
|
35
|
+
repository_instructions="native user/project instructions retained",
|
|
36
|
+
read_scope="native permissions; approved roots constrain supplied context only",
|
|
37
|
+
evidence="clink/resources/native-claude-2.1.304-admission.json",
|
|
38
|
+
evidence_sha256="ed3cd9bf385ac533b8f24c06cc14a431c0dbac0100f1eb23f24953eab6ca4de2",
|
|
39
|
+
executable_sha256="b9ac5374839f14abf3f1f45610637c332b5daee5ce0d1634726cef87aab78e6a",
|
|
40
|
+
recipe_sha256="de885baaadfdf5810321e32992c7e961f3a360dda90b903ab98ffc243564885e",
|
|
41
|
+
),
|
|
42
|
+
"grok": AdapterPolicy(
|
|
43
|
+
adapter="grok",
|
|
44
|
+
version="grok 1.0.70 (f50904eaee6c)",
|
|
45
|
+
verified=True,
|
|
46
|
+
verification_status="verified_native_lifecycle",
|
|
47
|
+
file_references=True,
|
|
48
|
+
repository_instructions="native user/project instructions retained",
|
|
49
|
+
read_scope="native permissions; approved roots constrain supplied context only",
|
|
50
|
+
evidence="clink/resources/native-grok-1.0.70-admission.json",
|
|
51
|
+
evidence_sha256="809f49238544d7fa317110c403952ba3fa791b9627066fa51d256ad16fc46ec1",
|
|
52
|
+
executable_sha256="5c8a688251bf61e35370b828b6a353b0a20fcc3f64000de8a91d426039f29cec",
|
|
53
|
+
recipe_sha256="b2e236f6490d3d00b51dac2a634c8b4f845b649ad9386c6510c6ae902f730cd3",
|
|
54
|
+
),
|
|
55
|
+
"agy": AdapterPolicy(
|
|
56
|
+
adapter="agy",
|
|
57
|
+
version="1.4.12",
|
|
58
|
+
verified=True,
|
|
59
|
+
verification_status="verified_native_lifecycle",
|
|
60
|
+
file_references=True,
|
|
61
|
+
repository_instructions="native user/project instructions retained",
|
|
62
|
+
read_scope="native permissions; approved roots constrain supplied context only",
|
|
63
|
+
evidence="clink/resources/native-agy-1.4.12-admission.json",
|
|
64
|
+
evidence_sha256="7c845424c38d57c3b5027eb2f91b159bf00d633f3ffa8de772f69f2c49c36381",
|
|
65
|
+
executable_sha256="34423fc9b0af3841e7646aa72a2d40ffeab8218b22d1cb99eac7355f9eba31e3",
|
|
66
|
+
recipe_sha256="da740ab92a35b480344a4dc07a517681dfbd5768f18ea01fbb65074337659a4f",
|
|
67
|
+
),
|
|
68
|
+
}
|
|
69
|
+
_VERIFIED_NATIVE_PLATFORMS = {"grok": ("Darwin", "arm64"), "agy": ("Darwin", "arm64")}
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class NativeRuntime:
|
|
73
|
+
"""Freeze native configuration without granting capabilities beyond native policy."""
|
|
74
|
+
|
|
75
|
+
def __init__(self, backend: CLIBackend | None = None):
|
|
76
|
+
self.backend = backend or get_backend()
|
|
77
|
+
self.pool = self.backend.pool
|
|
78
|
+
|
|
79
|
+
@staticmethod
|
|
80
|
+
def _policy(adapter: str) -> AdapterPolicy:
|
|
81
|
+
policy = _VERIFIED_NATIVE_POLICIES.get(adapter, AdapterPolicy(adapter))
|
|
82
|
+
scope = _VERIFIED_NATIVE_PLATFORMS.get(adapter)
|
|
83
|
+
if scope is not None and (platform.system(), platform.machine()) != scope:
|
|
84
|
+
return AdapterPolicy(adapter, verification_status="platform_unverified")
|
|
85
|
+
return policy
|
|
86
|
+
|
|
87
|
+
def describe(self) -> list[dict[str, Any]]:
|
|
88
|
+
"""Report native profiles without authentication, inference, or executable probes."""
|
|
89
|
+
descriptions = []
|
|
90
|
+
for name in self.backend.registry.list_clients():
|
|
91
|
+
client = self.backend.registry.get_client(name)
|
|
92
|
+
profile = client.native_task
|
|
93
|
+
if profile is None:
|
|
94
|
+
continue
|
|
95
|
+
policy = self._policy(client.adapter)
|
|
96
|
+
adapter = get_native_adapter(client.adapter)
|
|
97
|
+
installed = bool(client.executable and shutil.which(client.executable[0]))
|
|
98
|
+
descriptions.append(
|
|
99
|
+
{
|
|
100
|
+
"cli_name": client.name,
|
|
101
|
+
"adapter": client.adapter,
|
|
102
|
+
"enabled": profile.enabled,
|
|
103
|
+
"installed": installed,
|
|
104
|
+
"available": profile.enabled and installed and policy.verified,
|
|
105
|
+
"verification_status": policy.verification_status,
|
|
106
|
+
"version": policy.version,
|
|
107
|
+
"version_origin": "admission_receipt",
|
|
108
|
+
"launch_validation": "exact executable, recipe and receipt rechecked for each attempt",
|
|
109
|
+
"authentication": "unknown",
|
|
110
|
+
"roles": sorted(profile.roles),
|
|
111
|
+
"default_model": profile.default_model,
|
|
112
|
+
"default_model_origin": "native_profile" if profile.default_model else "native_cli_configuration",
|
|
113
|
+
"environment_names": list(profile.inherit_env),
|
|
114
|
+
"required_environment_names": list(profile.required_env),
|
|
115
|
+
"permission_handling": adapter.permission_handling,
|
|
116
|
+
"prompt_transport": adapter.prompt_channel,
|
|
117
|
+
"read_scope": "native policy; approved roots only constrain server-supplied context",
|
|
118
|
+
}
|
|
119
|
+
)
|
|
120
|
+
return descriptions
|
|
121
|
+
|
|
122
|
+
def _check_depth(self) -> None:
|
|
123
|
+
depth = os.environ.get("CLINK_DELEGATION_DEPTH", "0")
|
|
124
|
+
if not re.fullmatch(r"[0-9]+", depth):
|
|
125
|
+
raise BackendError("invalid_config", "CLINK_DELEGATION_DEPTH must be a nonnegative integer.")
|
|
126
|
+
if self.backend._nested or depth.strip("0"):
|
|
127
|
+
raise BackendError("recursion_blocked", "A known nested clink server cannot submit native tasks.")
|
|
128
|
+
|
|
129
|
+
@staticmethod
|
|
130
|
+
def _check_environment_names(context: FrozenNativeContext) -> None:
|
|
131
|
+
missing = [name for name in context.required_environment_names if not os.environ.get(name)]
|
|
132
|
+
if missing:
|
|
133
|
+
raise BackendError(
|
|
134
|
+
"missing_environment",
|
|
135
|
+
"Required native environment names are unavailable in the server process.",
|
|
136
|
+
metadata={"missing_environment_names": missing},
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
def prepare(
|
|
140
|
+
self,
|
|
141
|
+
cli_name: str,
|
|
142
|
+
role: str | None,
|
|
143
|
+
model: str | None,
|
|
144
|
+
project_dir: str,
|
|
145
|
+
files: tuple[str, ...] = (),
|
|
146
|
+
images: tuple[str, ...] = (),
|
|
147
|
+
absolute_deadline: float | None = None,
|
|
148
|
+
) -> FrozenNativeContext:
|
|
149
|
+
"""Validate and freeze names/context before reserving a task, without CLI calls."""
|
|
150
|
+
self._check_depth()
|
|
151
|
+
if absolute_deadline is not None and (
|
|
152
|
+
isinstance(absolute_deadline, bool)
|
|
153
|
+
or not isinstance(absolute_deadline, (int, float))
|
|
154
|
+
or not math.isfinite(absolute_deadline)
|
|
155
|
+
):
|
|
156
|
+
raise BackendError("invalid_config", "A native deadline must be a finite monotonic timestamp.")
|
|
157
|
+
if not isinstance(cli_name, str) or not cli_name:
|
|
158
|
+
raise BackendError("invalid_config", "A native task requires an explicit configured CLI name.")
|
|
159
|
+
try:
|
|
160
|
+
client = self.backend.registry.get_client(cli_name)
|
|
161
|
+
except KeyError as exc:
|
|
162
|
+
raise BackendError("invalid_config", "The requested native CLI client is not configured.") from exc
|
|
163
|
+
profile = client.native_task
|
|
164
|
+
if profile is None or not profile.enabled:
|
|
165
|
+
raise BackendError("unsupported_capability", "This client has no enabled native task profile.")
|
|
166
|
+
adapter = get_native_adapter(client.adapter)
|
|
167
|
+
selected_role = profile.roles.get(role or "default")
|
|
168
|
+
if selected_role is None:
|
|
169
|
+
raise BackendError("invalid_config", "The requested role is not configured in the native task profile.")
|
|
170
|
+
policy = self._policy(client.adapter)
|
|
171
|
+
if not policy.verified:
|
|
172
|
+
raise BackendError(
|
|
173
|
+
"policy_unverified", "The native task profile has no admitted version-specific evidence."
|
|
174
|
+
)
|
|
175
|
+
command = tuple(client.executable)
|
|
176
|
+
found = shutil.which(command[0]) if command else None
|
|
177
|
+
if not found:
|
|
178
|
+
raise BackendError("cli_missing", "The configured native CLI executable is unavailable.")
|
|
179
|
+
executable = (str(Path(found).resolve()), *command[1:])
|
|
180
|
+
roots = tuple(Path(root) for root in client.allowed_roots)
|
|
181
|
+
cwd = _directory(project_dir, roots)
|
|
182
|
+
requested_model = _model(model)
|
|
183
|
+
role_model = _model(selected_role.model)
|
|
184
|
+
effective_model = requested_model or role_model or _model(profile.default_model)
|
|
185
|
+
target = BackendTarget(
|
|
186
|
+
client=ClientSnapshot(
|
|
187
|
+
name=client.name,
|
|
188
|
+
adapter=client.adapter,
|
|
189
|
+
executable=executable,
|
|
190
|
+
working_dir=Path(client.working_dir),
|
|
191
|
+
allowed_roots=roots,
|
|
192
|
+
config_args=tuple(client.config_args),
|
|
193
|
+
env=tuple(sorted(client.env.items())),
|
|
194
|
+
timeout_seconds=client.timeout_seconds,
|
|
195
|
+
parser=f"{adapter.name}_native",
|
|
196
|
+
default_model=profile.default_model,
|
|
197
|
+
),
|
|
198
|
+
role=RoleSnapshot(selected_role.name, selected_role.prompt_text, model=selected_role.model),
|
|
199
|
+
requested_model=requested_model,
|
|
200
|
+
effective_model=effective_model,
|
|
201
|
+
working_directory=cwd,
|
|
202
|
+
policy=policy,
|
|
203
|
+
default_origin=(
|
|
204
|
+
"request" if requested_model else "role" if role_model else "client" if effective_model else "native"
|
|
205
|
+
),
|
|
206
|
+
)
|
|
207
|
+
validated_files = self.validate_context(target, files, images)
|
|
208
|
+
identity = {
|
|
209
|
+
"client": client.name,
|
|
210
|
+
"adapter": client.adapter,
|
|
211
|
+
"executable": executable,
|
|
212
|
+
"project": str(cwd),
|
|
213
|
+
"roots": [str(root) for root in roots],
|
|
214
|
+
"model": effective_model,
|
|
215
|
+
"role": selected_role.name,
|
|
216
|
+
"role_sha256": hashlib.sha256(selected_role.prompt_text.encode()).hexdigest(),
|
|
217
|
+
"environment_names": profile.inherit_env,
|
|
218
|
+
"required_environment_names": profile.required_env,
|
|
219
|
+
"configured_environment_names": sorted(client.env),
|
|
220
|
+
"configured_environment_sha256": hashlib.sha256(
|
|
221
|
+
json.dumps(sorted(client.env.items()), separators=(",", ":")).encode()
|
|
222
|
+
).hexdigest(),
|
|
223
|
+
"config_args": client.config_args,
|
|
224
|
+
"recipe": native_recipe_digest(client.adapter),
|
|
225
|
+
}
|
|
226
|
+
profile_id = hashlib.sha256(json.dumps(identity, sort_keys=True, separators=(",", ":")).encode()).hexdigest()
|
|
227
|
+
context = FrozenNativeContext(
|
|
228
|
+
target=target,
|
|
229
|
+
profile_id=profile_id,
|
|
230
|
+
files=validated_files,
|
|
231
|
+
deadline=absolute_deadline,
|
|
232
|
+
environment_names=tuple(profile.inherit_env),
|
|
233
|
+
required_environment_names=tuple(profile.required_env),
|
|
234
|
+
)
|
|
235
|
+
self._check_environment_names(context)
|
|
236
|
+
return context
|
|
237
|
+
|
|
238
|
+
def validate_context(
|
|
239
|
+
self, target: BackendTarget, files: tuple[str, ...], images: tuple[str, ...] = ()
|
|
240
|
+
) -> tuple[str, ...]:
|
|
241
|
+
if images:
|
|
242
|
+
raise BackendError("unsupported_capability", "Native image input has not been verified for this adapter.")
|
|
243
|
+
validated, _ = self.backend._context(CLIRequest(prompt="", target=target, files=files))
|
|
244
|
+
return validated
|
|
245
|
+
|
|
246
|
+
@staticmethod
|
|
247
|
+
def _compose(context: FrozenNativeContext, prompt: str) -> bytes:
|
|
248
|
+
if not isinstance(prompt, str) or not prompt.strip():
|
|
249
|
+
raise BackendError("invalid_config", "A native task prompt must be nonempty text.")
|
|
250
|
+
sections = [
|
|
251
|
+
"Execute the assigned scoped task using your configured native capabilities and permissions. "
|
|
252
|
+
"Preserve project/user instructions. Report changes, checks, unfinished work, and possible partial effects. "
|
|
253
|
+
"Do not claim independent verification for checks you did not perform.",
|
|
254
|
+
"<subordinate_role_expertise_json>\n"
|
|
255
|
+
+ json.dumps(context.target.role.prompt_text, ensure_ascii=False)
|
|
256
|
+
+ "\n</subordinate_role_expertise_json>",
|
|
257
|
+
"<declared_context_paths_json>\n"
|
|
258
|
+
+ json.dumps({"files": context.files}, ensure_ascii=False)
|
|
259
|
+
+ "\n</declared_context_paths_json>",
|
|
260
|
+
"<assigned_task_json>\n" + json.dumps(prompt, ensure_ascii=False) + "\n</assigned_task_json>",
|
|
261
|
+
]
|
|
262
|
+
content = "\n\n".join(sections).encode("utf-8")
|
|
263
|
+
if len(content) > PROMPT_BYTES:
|
|
264
|
+
raise BackendError("input_limit", "The assembled native prompt exceeds 1 MiB UTF-8.")
|
|
265
|
+
return content
|
|
266
|
+
|
|
267
|
+
@staticmethod
|
|
268
|
+
def _command(
|
|
269
|
+
context: FrozenNativeContext, session_id: str | None, prompt_path: str | None = None
|
|
270
|
+
) -> tuple[str, ...]:
|
|
271
|
+
return get_native_adapter(context.target.client.adapter).command(context, session_id, prompt_path)
|
|
272
|
+
|
|
273
|
+
@staticmethod
|
|
274
|
+
def _environment(context: FrozenNativeContext) -> dict[str, str]:
|
|
275
|
+
baseline_names = native_environment_names(context.target.client.adapter)
|
|
276
|
+
names = frozenset((*baseline_names, *context.environment_names))
|
|
277
|
+
result = {name: value for name, value in os.environ.items() if name in names}
|
|
278
|
+
additions = dict(context.target.client.env)
|
|
279
|
+
if any(name not in baseline_names for name in additions):
|
|
280
|
+
raise BackendError("invalid_config", "Native static environment additions must use baseline names.")
|
|
281
|
+
result.update(additions)
|
|
282
|
+
result["CLINK_DELEGATION_DEPTH"] = "1"
|
|
283
|
+
return result
|
|
284
|
+
|
|
285
|
+
def _validate_admission(self, context: FrozenNativeContext) -> None:
|
|
286
|
+
target = context.target
|
|
287
|
+
policy = self._policy(target.client.adapter)
|
|
288
|
+
if not policy.verified or policy != target.policy:
|
|
289
|
+
raise BackendError("policy_unverified", "The native admission snapshot is unavailable or changed.")
|
|
290
|
+
if not policy.version or not policy.executable_sha256 or not policy.recipe_sha256:
|
|
291
|
+
raise BackendError("policy_unverified", "Native admission must bind an exact binary/version and recipe.")
|
|
292
|
+
if native_recipe_digest(target.client.adapter) != policy.recipe_sha256:
|
|
293
|
+
raise BackendError("policy_unverified", "The native launch recipe changed after verification.")
|
|
294
|
+
if not policy.evidence or not policy.evidence_sha256:
|
|
295
|
+
raise BackendError("policy_unverified", "Native admission requires a pinned verification receipt.")
|
|
296
|
+
project = Path(__file__).resolve().parent.parent
|
|
297
|
+
path = (project / policy.evidence).resolve()
|
|
298
|
+
evidence_roots = (project / "clink" / "resources", project / "docs" / "verification")
|
|
299
|
+
if not any(root in path.parents for root in evidence_roots):
|
|
300
|
+
raise BackendError("policy_unverified", "Native evidence is outside the verification directory.")
|
|
301
|
+
try:
|
|
302
|
+
with path.open("rb") as stream:
|
|
303
|
+
evidence = stream.read(PROMPT_BYTES + 1)
|
|
304
|
+
except OSError as exc:
|
|
305
|
+
raise BackendError("policy_unverified", "Native admission evidence is unavailable.") from exc
|
|
306
|
+
if len(evidence) > PROMPT_BYTES or hashlib.sha256(evidence).hexdigest() != policy.evidence_sha256:
|
|
307
|
+
raise BackendError("policy_unverified", "Native admission evidence changed after verification.")
|
|
308
|
+
|
|
309
|
+
async def run(
|
|
310
|
+
self,
|
|
311
|
+
context: FrozenNativeContext,
|
|
312
|
+
prompt: str,
|
|
313
|
+
session_id: str | None = None,
|
|
314
|
+
*,
|
|
315
|
+
attempt_id: str,
|
|
316
|
+
started: Callable[[], None] | None = None,
|
|
317
|
+
) -> NativeRunOutcome:
|
|
318
|
+
"""Submit exactly once; process cleanup completes before cancellation propagates."""
|
|
319
|
+
started_at = time.monotonic()
|
|
320
|
+
configured_deadline = started_at + context.target.client.timeout_seconds
|
|
321
|
+
deadline = min(context.deadline, configured_deadline) if context.deadline is not None else configured_deadline
|
|
322
|
+
self._check_depth()
|
|
323
|
+
self._check_environment_names(context)
|
|
324
|
+
self._validate_admission(context)
|
|
325
|
+
content = self._compose(context, prompt)
|
|
326
|
+
adapter = get_native_adapter(context.target.client.adapter)
|
|
327
|
+
with prepare_native_launch(adapter, context, content, session_id) as launch:
|
|
328
|
+
try:
|
|
329
|
+
observed = await asyncio.wait_for(
|
|
330
|
+
asyncio.to_thread(executable_digest, launch.command[0]), max(0.0, deadline - time.monotonic())
|
|
331
|
+
)
|
|
332
|
+
except asyncio.TimeoutError as exc:
|
|
333
|
+
raise BackendError(
|
|
334
|
+
"timeout", "The native attempt deadline expired during executable verification."
|
|
335
|
+
) from exc
|
|
336
|
+
except OSError as exc:
|
|
337
|
+
raise BackendError("cli_missing", "The admitted native executable is unavailable.") from exc
|
|
338
|
+
if observed != context.target.policy.executable_sha256:
|
|
339
|
+
raise BackendError(
|
|
340
|
+
"policy_unverified", "The installed native binary differs from its admission evidence."
|
|
341
|
+
)
|
|
342
|
+
environment: dict[str, str] = {}
|
|
343
|
+
|
|
344
|
+
def prelaunch() -> None:
|
|
345
|
+
self._check_depth()
|
|
346
|
+
self._check_environment_names(context)
|
|
347
|
+
self._validate_admission(context)
|
|
348
|
+
if executable_digest(launch.command[0]) != context.target.policy.executable_sha256:
|
|
349
|
+
raise BackendError("policy_unverified", "The admitted native executable changed before launch.")
|
|
350
|
+
cwd = _directory(context.target.working_directory, context.target.client.allowed_roots)
|
|
351
|
+
if cwd != context.target.working_directory:
|
|
352
|
+
raise BackendError("path_not_allowed", "The native project changed before launch.")
|
|
353
|
+
self.validate_context(context.target, context.files, context.images)
|
|
354
|
+
launch.validate_input()
|
|
355
|
+
environment.update(self._environment(context))
|
|
356
|
+
|
|
357
|
+
output = await self.pool.execute(
|
|
358
|
+
launch.command,
|
|
359
|
+
content=launch.stdin,
|
|
360
|
+
cwd=str(context.target.working_directory),
|
|
361
|
+
env=environment,
|
|
362
|
+
deadline=deadline,
|
|
363
|
+
request_id=attempt_id,
|
|
364
|
+
metadata={"client": context.target.cli_name, "role": context.target.role.name, "tool": "clink"},
|
|
365
|
+
prelaunch=prelaunch,
|
|
366
|
+
on_started=started,
|
|
367
|
+
)
|
|
368
|
+
metadata: dict[str, Any] = {
|
|
369
|
+
"exit_status": output.returncode,
|
|
370
|
+
"stdout_bytes": output.stdout_bytes,
|
|
371
|
+
"stderr_bytes": output.stderr_bytes,
|
|
372
|
+
"queue_seconds": output.queue_seconds,
|
|
373
|
+
"claims_verification": "agent_reported_unverified",
|
|
374
|
+
"goal_achieved": "unverified",
|
|
375
|
+
"configuration_sources": ["native_user", "native_project", "operator_environment_names"],
|
|
376
|
+
"verification_status": context.target.policy.verification_status,
|
|
377
|
+
}
|
|
378
|
+
try:
|
|
379
|
+
parsed = adapter.parse(output.stdout, output.stderr)
|
|
380
|
+
except ParserError as exc:
|
|
381
|
+
state = (
|
|
382
|
+
"failed"
|
|
383
|
+
if output.returncode != 0 or exc.code in {"authentication_required", "rate_limited"}
|
|
384
|
+
else "uncertain"
|
|
385
|
+
)
|
|
386
|
+
return NativeRunOutcome(state, metadata={**metadata, "error_code": exc.code})
|
|
387
|
+
if session_id is not None and parsed.session_id is not None and session_id != parsed.session_id:
|
|
388
|
+
return NativeRunOutcome("uncertain", metadata={**metadata, "error_code": "native_session_mismatch"})
|
|
389
|
+
state = parsed.state
|
|
390
|
+
if output.returncode != 0 and state == "completed":
|
|
391
|
+
state = "failed"
|
|
392
|
+
metadata["error_code"] = "nonzero_exit"
|
|
393
|
+
return NativeRunOutcome(
|
|
394
|
+
state,
|
|
395
|
+
content=parsed.content,
|
|
396
|
+
metadata={**metadata, **parsed.metadata},
|
|
397
|
+
session_id=parsed.session_id,
|
|
398
|
+
partial_effects=True,
|
|
399
|
+
)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
_RUNTIME: NativeRuntime | None = None
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def get_native_runtime() -> NativeRuntime:
|
|
406
|
+
global _RUNTIME
|
|
407
|
+
if _RUNTIME is None:
|
|
408
|
+
_RUNTIME = NativeRuntime()
|
|
409
|
+
return _RUNTIME
|