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.
Files changed (141) hide show
  1. clink/__init__.py +19 -0
  2. clink/agents/__init__.py +30 -0
  3. clink/agents/base.py +70 -0
  4. clink/agents/claude.py +7 -0
  5. clink/agents/codex.py +7 -0
  6. clink/agents/gemini.py +7 -0
  7. clink/allowed_roots.py +356 -0
  8. clink/backend.py +765 -0
  9. clink/cli.py +594 -0
  10. clink/constants.py +78 -0
  11. clink/contracts.py +167 -0
  12. clink/files.py +70 -0
  13. clink/generation.py +274 -0
  14. clink/generation_capabilities.py +207 -0
  15. clink/generation_contracts.py +214 -0
  16. clink/generation_policy.py +174 -0
  17. clink/models.py +243 -0
  18. clink/native.py +409 -0
  19. clink/native_adapters.py +244 -0
  20. clink/native_contracts.py +68 -0
  21. clink/native_generation.py +421 -0
  22. clink/native_generation_claude_policy.py +59 -0
  23. clink/native_generation_contracts.py +264 -0
  24. clink/native_generation_families.py +218 -0
  25. clink/native_generation_family_policy.py +14 -0
  26. clink/native_generation_files.py +153 -0
  27. clink/native_generation_policy.py +82 -0
  28. clink/native_policy.py +86 -0
  29. clink/parsers/__init__.py +34 -0
  30. clink/parsers/agy.py +53 -0
  31. clink/parsers/agy_native.py +161 -0
  32. clink/parsers/agy_native_generation.py +189 -0
  33. clink/parsers/base.py +209 -0
  34. clink/parsers/claude.py +104 -0
  35. clink/parsers/claude_native.py +113 -0
  36. clink/parsers/claude_native_generation.py +346 -0
  37. clink/parsers/codex.py +172 -0
  38. clink/parsers/codex_generation.py +212 -0
  39. clink/parsers/codex_native.py +236 -0
  40. clink/parsers/codex_native_generation.py +367 -0
  41. clink/parsers/gemini.py +110 -0
  42. clink/parsers/grok.py +63 -0
  43. clink/parsers/grok_native.py +91 -0
  44. clink/parsers/grok_native_generation.py +157 -0
  45. clink/policy.py +180 -0
  46. clink/registry.py +544 -0
  47. clink/resources/CODEX-LICENSE +201 -0
  48. clink/resources/CODEX-NOTICE +6 -0
  49. clink/resources/PROVENANCE.md +16 -0
  50. clink/resources/codex-0.166.0-models-direct.json +1284 -0
  51. clink/resources/codex-0.166.0-models-generation.json +1269 -0
  52. clink/resources/native-agy-1.4.12-admission.json +746 -0
  53. clink/resources/native-claude-2.1.304-admission.json +99 -0
  54. clink/resources/native-generation-claude-2.1.304-admission.json +277 -0
  55. clink/resources/native-generation-codex-0.166.0-admission.json +309 -0
  56. clink/resources/native-generation-grok-1.0.70-admission.json +329 -0
  57. clink/resources/native-grok-1.0.70-admission.json +644 -0
  58. clink/runner.py +303 -0
  59. clink/setup.py +399 -0
  60. clink/tasks.py +695 -0
  61. clink_cli-0.2.1.dist-info/METADATA +225 -0
  62. clink_cli-0.2.1.dist-info/RECORD +141 -0
  63. clink_cli-0.2.1.dist-info/WHEEL +5 -0
  64. clink_cli-0.2.1.dist-info/entry_points.txt +3 -0
  65. clink_cli-0.2.1.dist-info/licenses/LICENSE +197 -0
  66. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-LICENSE +201 -0
  67. clink_cli-0.2.1.dist-info/licenses/clink/resources/CODEX-NOTICE +6 -0
  68. clink_cli-0.2.1.dist-info/top_level.txt +7 -0
  69. conf/__init__.py +1 -0
  70. conf/cli_clients/agy.json +40 -0
  71. conf/cli_clients/claude.json +39 -0
  72. conf/cli_clients/codex.json +40 -0
  73. conf/cli_clients/gemini.json +23 -0
  74. conf/cli_clients/grok.json +40 -0
  75. config.py +40 -0
  76. server.py +896 -0
  77. systemprompts/__init__.py +35 -0
  78. systemprompts/analyze_prompt.py +87 -0
  79. systemprompts/chat_prompt.py +57 -0
  80. systemprompts/clink/codex_codereviewer.txt +8 -0
  81. systemprompts/clink/default.txt +8 -0
  82. systemprompts/clink/default_codereviewer.txt +8 -0
  83. systemprompts/clink/default_planner.txt +7 -0
  84. systemprompts/clink/native_codereviewer.txt +1 -0
  85. systemprompts/clink/native_default.txt +1 -0
  86. systemprompts/clink/native_planner.txt +1 -0
  87. systemprompts/codereview_prompt.py +91 -0
  88. systemprompts/consensus_prompt.py +120 -0
  89. systemprompts/debug_prompt.py +150 -0
  90. systemprompts/docgen_prompt.py +252 -0
  91. systemprompts/generate_code_prompt.py +181 -0
  92. systemprompts/planner_prompt.py +127 -0
  93. systemprompts/precommit_prompt.py +179 -0
  94. systemprompts/refactor_prompt.py +323 -0
  95. systemprompts/secaudit_prompt.py +420 -0
  96. systemprompts/testgen_prompt.py +133 -0
  97. systemprompts/thinkdeep_prompt.py +55 -0
  98. systemprompts/tracer_prompt.py +145 -0
  99. tools/__init__.py +43 -0
  100. tools/analyze.py +583 -0
  101. tools/apilookup.py +97 -0
  102. tools/challenge.py +198 -0
  103. tools/chat.py +187 -0
  104. tools/clink.py +130 -0
  105. tools/codereview.py +724 -0
  106. tools/consensus.py +790 -0
  107. tools/debug.py +573 -0
  108. tools/docgen.py +577 -0
  109. tools/listmodels.py +48 -0
  110. tools/models.py +373 -0
  111. tools/planner.py +533 -0
  112. tools/precommit.py +776 -0
  113. tools/refactor.py +645 -0
  114. tools/secaudit.py +685 -0
  115. tools/shared/__init__.py +19 -0
  116. tools/shared/base_models.py +167 -0
  117. tools/shared/base_tool.py +659 -0
  118. tools/shared/exceptions.py +20 -0
  119. tools/shared/schema_builders.py +159 -0
  120. tools/simple/__init__.py +18 -0
  121. tools/simple/base.py +539 -0
  122. tools/testgen.py +513 -0
  123. tools/thinkdeep.py +586 -0
  124. tools/tracer.py +763 -0
  125. tools/version.py +53 -0
  126. tools/workflow/__init__.py +22 -0
  127. tools/workflow/base.py +448 -0
  128. tools/workflow/schema_builders.py +166 -0
  129. tools/workflow/workflow_mixin.py +1195 -0
  130. utils/__init__.py +21 -0
  131. utils/call_state.py +349 -0
  132. utils/client_info.py +293 -0
  133. utils/consensus_manifest.py +131 -0
  134. utils/conversation_memory.py +582 -0
  135. utils/env.py +111 -0
  136. utils/file_types.py +271 -0
  137. utils/file_utils.py +803 -0
  138. utils/image_utils.py +94 -0
  139. utils/security_config.py +163 -0
  140. utils/storage_backend.py +177 -0
  141. utils/token_utils.py +54 -0
@@ -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: ...