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/native_adapters.py
ADDED
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"""Small native transport/parser boundary; shared admission and process limits stay central."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import hashlib
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import stat
|
|
9
|
+
import tempfile
|
|
10
|
+
from abc import ABC, abstractmethod
|
|
11
|
+
from collections.abc import Callable, Iterator
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from dataclasses import dataclass
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
|
|
16
|
+
from clink.contracts import BackendError
|
|
17
|
+
from clink.native_contracts import FrozenNativeContext, ParsedNativeResponse
|
|
18
|
+
from clink.native_policy import NATIVE_PROMPT_FILE, native_arguments
|
|
19
|
+
|
|
20
|
+
PROMPT_BYTES = 1024 * 1024
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class NativeAdapter(ABC):
|
|
24
|
+
"""Own argv, session validation and parsing for one documented native interface."""
|
|
25
|
+
|
|
26
|
+
name: str
|
|
27
|
+
prompt_channel: str
|
|
28
|
+
permission_handling: str
|
|
29
|
+
|
|
30
|
+
@abstractmethod
|
|
31
|
+
def command(
|
|
32
|
+
self, context: FrozenNativeContext, session_id: str | None, prompt_path: str | None = None
|
|
33
|
+
) -> tuple[str, ...]:
|
|
34
|
+
"""Build protected direct argv without embedding task text."""
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def validate_session(self, session_id: str | None) -> str | None:
|
|
38
|
+
"""Reject identities that are ambiguous or unsafe for native resume."""
|
|
39
|
+
|
|
40
|
+
@abstractmethod
|
|
41
|
+
def parse(self, stdout: str, stderr: str) -> ParsedNativeResponse:
|
|
42
|
+
"""Return bounded claims and a separate private native session identity."""
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ClaudeNativeAdapter(NativeAdapter):
|
|
46
|
+
name = "claude"
|
|
47
|
+
prompt_channel = "stdin_text"
|
|
48
|
+
permission_handling = "native policy; unattended prompts denied and reported"
|
|
49
|
+
|
|
50
|
+
def validate_session(self, session_id: str | None) -> str | None:
|
|
51
|
+
from clink.parsers.claude_native import validate_session_id
|
|
52
|
+
|
|
53
|
+
return validate_session_id(session_id)
|
|
54
|
+
|
|
55
|
+
def command(
|
|
56
|
+
self, context: FrozenNativeContext, session_id: str | None, prompt_path: str | None = None
|
|
57
|
+
) -> tuple[str, ...]:
|
|
58
|
+
self.validate_session(session_id)
|
|
59
|
+
target = context.target
|
|
60
|
+
command = [*target.client.executable, *native_arguments(self.name), *target.client.config_args]
|
|
61
|
+
if target.effective_model is not None:
|
|
62
|
+
command.extend(("--model", target.effective_model))
|
|
63
|
+
if session_id is not None:
|
|
64
|
+
command.extend(("--resume", session_id))
|
|
65
|
+
return tuple(command)
|
|
66
|
+
|
|
67
|
+
def parse(self, stdout: str, stderr: str) -> ParsedNativeResponse:
|
|
68
|
+
from clink.parsers.claude_native import ClaudeNativeParser
|
|
69
|
+
|
|
70
|
+
return ClaudeNativeParser().parse(stdout, stderr)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class GrokNativeAdapter(NativeAdapter):
|
|
74
|
+
name = "grok"
|
|
75
|
+
prompt_channel = "private_file"
|
|
76
|
+
permission_handling = "native policy; headless permission requests refused and reported"
|
|
77
|
+
|
|
78
|
+
def validate_session(self, session_id: str | None) -> str | None:
|
|
79
|
+
from clink.parsers.grok_native import validate_session_id
|
|
80
|
+
|
|
81
|
+
return validate_session_id(session_id)
|
|
82
|
+
|
|
83
|
+
def command(
|
|
84
|
+
self, context: FrozenNativeContext, session_id: str | None, prompt_path: str | None = None
|
|
85
|
+
) -> tuple[str, ...]:
|
|
86
|
+
self.validate_session(session_id)
|
|
87
|
+
if prompt_path is None:
|
|
88
|
+
raise BackendError("invalid_config", "Grok native execution requires a private prompt file.")
|
|
89
|
+
target = context.target
|
|
90
|
+
arguments = tuple(prompt_path if item == NATIVE_PROMPT_FILE else item for item in native_arguments(self.name))
|
|
91
|
+
command = [*target.client.executable, *arguments, *target.client.config_args]
|
|
92
|
+
if target.effective_model is not None:
|
|
93
|
+
command.extend(("--model", target.effective_model))
|
|
94
|
+
if session_id is not None:
|
|
95
|
+
command.extend(("--resume", session_id))
|
|
96
|
+
return tuple(command)
|
|
97
|
+
|
|
98
|
+
def parse(self, stdout: str, stderr: str) -> ParsedNativeResponse:
|
|
99
|
+
from clink.parsers.grok_native import GrokNativeParser
|
|
100
|
+
|
|
101
|
+
return GrokNativeParser().parse(stdout, stderr)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class AgyNativeAdapter(NativeAdapter):
|
|
105
|
+
name = "agy"
|
|
106
|
+
prompt_channel = "stdin_user_ndjson"
|
|
107
|
+
permission_handling = "native policy; headless confirmation requests soft-denied and reported"
|
|
108
|
+
|
|
109
|
+
def validate_session(self, session_id: str | None) -> str | None:
|
|
110
|
+
from clink.parsers.agy_native import validate_session_id
|
|
111
|
+
|
|
112
|
+
return validate_session_id(session_id)
|
|
113
|
+
|
|
114
|
+
def command(
|
|
115
|
+
self, context: FrozenNativeContext, session_id: str | None, prompt_path: str | None = None
|
|
116
|
+
) -> tuple[str, ...]:
|
|
117
|
+
self.validate_session(session_id)
|
|
118
|
+
target = context.target
|
|
119
|
+
command = [*target.client.executable, *native_arguments(self.name), *target.client.config_args]
|
|
120
|
+
if target.effective_model is not None:
|
|
121
|
+
command.extend(("--model", target.effective_model))
|
|
122
|
+
if session_id is not None:
|
|
123
|
+
command.extend(("--conversation", session_id))
|
|
124
|
+
return tuple(command)
|
|
125
|
+
|
|
126
|
+
def parse(self, stdout: str, stderr: str) -> ParsedNativeResponse:
|
|
127
|
+
from clink.parsers.agy_native import AgyNativeParser
|
|
128
|
+
|
|
129
|
+
return AgyNativeParser().parse(stdout, stderr)
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class CodexNativeAdapter(NativeAdapter):
|
|
133
|
+
name = "codex"
|
|
134
|
+
prompt_channel = "stdin_text"
|
|
135
|
+
permission_handling = "native exec approval semantics; interactive approvals unavailable"
|
|
136
|
+
|
|
137
|
+
def validate_session(self, session_id: str | None) -> str | None:
|
|
138
|
+
from clink.parsers.codex_native import validate_session_id
|
|
139
|
+
|
|
140
|
+
return validate_session_id(session_id)
|
|
141
|
+
|
|
142
|
+
def command(
|
|
143
|
+
self, context: FrozenNativeContext, session_id: str | None, prompt_path: str | None = None
|
|
144
|
+
) -> tuple[str, ...]:
|
|
145
|
+
self.validate_session(session_id)
|
|
146
|
+
target = context.target
|
|
147
|
+
if session_id is None:
|
|
148
|
+
arguments = [*native_arguments(self.name), "--cd", str(target.working_directory)]
|
|
149
|
+
else:
|
|
150
|
+
arguments = ["exec", "--cd", str(target.working_directory), "resume", "--json"]
|
|
151
|
+
command = [*target.client.executable, *arguments, *target.client.config_args]
|
|
152
|
+
if target.effective_model is not None:
|
|
153
|
+
command.extend(("--model", target.effective_model))
|
|
154
|
+
if session_id is not None:
|
|
155
|
+
command.append(session_id)
|
|
156
|
+
command.append("-")
|
|
157
|
+
return tuple(command)
|
|
158
|
+
|
|
159
|
+
def parse(self, stdout: str, stderr: str) -> ParsedNativeResponse:
|
|
160
|
+
from clink.parsers.codex_native import CodexNativeParser
|
|
161
|
+
|
|
162
|
+
return CodexNativeParser().parse(stdout, stderr)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
_ADAPTERS: dict[str, NativeAdapter] = {
|
|
166
|
+
adapter.name: adapter
|
|
167
|
+
for adapter in (ClaudeNativeAdapter(), GrokNativeAdapter(), AgyNativeAdapter(), CodexNativeAdapter())
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def get_native_adapter(name: str) -> NativeAdapter:
|
|
172
|
+
if name not in _ADAPTERS:
|
|
173
|
+
raise BackendError("unsupported_capability", "This adapter has no native task contract.")
|
|
174
|
+
return _ADAPTERS[name]
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
@dataclass(frozen=True)
|
|
178
|
+
class NativeLaunch:
|
|
179
|
+
"""Private launch resources remain alive until shared process cleanup completes."""
|
|
180
|
+
|
|
181
|
+
command: tuple[str, ...]
|
|
182
|
+
stdin: bytes
|
|
183
|
+
validate_input: Callable[[], None]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@contextmanager
|
|
187
|
+
def prepare_native_launch(
|
|
188
|
+
adapter: NativeAdapter, context: FrozenNativeContext, content: bytes, session_id: str | None
|
|
189
|
+
) -> Iterator[NativeLaunch]:
|
|
190
|
+
"""Keep task text off argv; dispose private prompt files on every exit path."""
|
|
191
|
+
if len(content) > PROMPT_BYTES:
|
|
192
|
+
raise BackendError("input_limit", "The native input exceeds the 1 MiB byte limit.")
|
|
193
|
+
adapter.validate_session(session_id)
|
|
194
|
+
if adapter.prompt_channel == "private_file":
|
|
195
|
+
with tempfile.TemporaryDirectory(prefix="clink-native-input-") as directory:
|
|
196
|
+
root = Path(directory)
|
|
197
|
+
os.chmod(root, 0o700)
|
|
198
|
+
path = root / "prompt.txt"
|
|
199
|
+
descriptor = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL | os.O_NOFOLLOW | os.O_CLOEXEC, 0o600)
|
|
200
|
+
with os.fdopen(descriptor, "wb") as stream:
|
|
201
|
+
os.fchmod(stream.fileno(), 0o600)
|
|
202
|
+
stream.write(content)
|
|
203
|
+
parent_identity = root.stat()
|
|
204
|
+
file_identity = path.stat()
|
|
205
|
+
digest = hashlib.sha256(content).digest()
|
|
206
|
+
|
|
207
|
+
def validate_input() -> None:
|
|
208
|
+
try:
|
|
209
|
+
parent = root.lstat()
|
|
210
|
+
if (
|
|
211
|
+
not stat.S_ISDIR(parent.st_mode)
|
|
212
|
+
or stat.S_IMODE(parent.st_mode) != 0o700
|
|
213
|
+
or (parent.st_dev, parent.st_ino) != (parent_identity.st_dev, parent_identity.st_ino)
|
|
214
|
+
):
|
|
215
|
+
raise OSError
|
|
216
|
+
descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_NONBLOCK | os.O_CLOEXEC)
|
|
217
|
+
with os.fdopen(descriptor, "rb") as stream:
|
|
218
|
+
observed = os.fstat(stream.fileno())
|
|
219
|
+
if (
|
|
220
|
+
not stat.S_ISREG(observed.st_mode)
|
|
221
|
+
or stat.S_IMODE(observed.st_mode) != 0o600
|
|
222
|
+
or (observed.st_dev, observed.st_ino, observed.st_size)
|
|
223
|
+
!= (file_identity.st_dev, file_identity.st_ino, len(content))
|
|
224
|
+
):
|
|
225
|
+
raise OSError
|
|
226
|
+
data = stream.read(PROMPT_BYTES + 1)
|
|
227
|
+
if hashlib.sha256(data).digest() != digest:
|
|
228
|
+
raise OSError
|
|
229
|
+
except OSError:
|
|
230
|
+
raise BackendError(
|
|
231
|
+
"invalid_config", "The private native input is unavailable or changed."
|
|
232
|
+
) from None
|
|
233
|
+
|
|
234
|
+
yield NativeLaunch(adapter.command(context, session_id, str(path)), b"", validate_input)
|
|
235
|
+
return
|
|
236
|
+
if adapter.prompt_channel == "stdin_user_ndjson":
|
|
237
|
+
stdin = (
|
|
238
|
+
json.dumps({"event": "user", "message": {"content": content.decode("utf-8")}}, ensure_ascii=False) + "\n"
|
|
239
|
+
).encode("utf-8")
|
|
240
|
+
if len(stdin) > PROMPT_BYTES:
|
|
241
|
+
raise BackendError("input_limit", "The native input envelope exceeds the 1 MiB byte limit.")
|
|
242
|
+
else:
|
|
243
|
+
stdin = content
|
|
244
|
+
yield NativeLaunch(adapter.command(context, session_id), stdin, lambda: None)
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Focused contracts for native jobs, separate from advisory CLI execution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass, field
|
|
7
|
+
from typing import Any, Protocol
|
|
8
|
+
|
|
9
|
+
from clink.contracts import BackendTarget
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass(frozen=True)
|
|
13
|
+
class ParsedNativeResponse:
|
|
14
|
+
"""Keep private session identity separate from bounded public metadata."""
|
|
15
|
+
|
|
16
|
+
state: str
|
|
17
|
+
content: str = ""
|
|
18
|
+
session_id: str | None = None
|
|
19
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@dataclass(frozen=True)
|
|
23
|
+
class FrozenNativeContext:
|
|
24
|
+
"""Pin native identity and names, without caller text or resolved environment values."""
|
|
25
|
+
|
|
26
|
+
target: BackendTarget
|
|
27
|
+
profile_id: str
|
|
28
|
+
files: tuple[str, ...] = ()
|
|
29
|
+
images: tuple[str, ...] = ()
|
|
30
|
+
deadline: float | None = None
|
|
31
|
+
environment_names: tuple[str, ...] = ()
|
|
32
|
+
required_environment_names: tuple[str, ...] = ()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class NativeRunOutcome:
|
|
37
|
+
"""Separate terminal execution state from agent claims and private native identity."""
|
|
38
|
+
|
|
39
|
+
state: str
|
|
40
|
+
content: str = ""
|
|
41
|
+
metadata: dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
session_id: str | None = None
|
|
43
|
+
partial_effects: bool = True
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class NativeRuntime(Protocol):
|
|
47
|
+
"""Execute once and finish child cleanup before returning or propagating cancellation."""
|
|
48
|
+
|
|
49
|
+
def prepare(
|
|
50
|
+
self,
|
|
51
|
+
cli_name: str,
|
|
52
|
+
role: str | None,
|
|
53
|
+
model: str | None,
|
|
54
|
+
project_dir: str,
|
|
55
|
+
files: tuple[str, ...] = (),
|
|
56
|
+
images: tuple[str, ...] = (),
|
|
57
|
+
absolute_deadline: float | None = None,
|
|
58
|
+
) -> FrozenNativeContext: ...
|
|
59
|
+
|
|
60
|
+
async def run(
|
|
61
|
+
self,
|
|
62
|
+
context: FrozenNativeContext,
|
|
63
|
+
prompt: str,
|
|
64
|
+
session_id: str | None = None,
|
|
65
|
+
*,
|
|
66
|
+
attempt_id: str,
|
|
67
|
+
started: Callable[[], None] | None = None,
|
|
68
|
+
) -> NativeRunOutcome: ...
|