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,157 @@
1
+ """Validate Grok's single native-generation final object without publishing diagnostics."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import math
8
+ import re
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+ from .base import (
13
+ MAX_METADATA_NUMBER,
14
+ BaseParser,
15
+ ParsedCLIResponse,
16
+ ParserError,
17
+ bounded_number,
18
+ cli_failure,
19
+ load_json,
20
+ model_identifier,
21
+ )
22
+
23
+ MAX_STREAM_BYTES = 10 * 1024 * 1024
24
+ OUTPUT_BYTES = 131072
25
+ _SESSION = re.compile(r"[0-9a-fA-F]{8}(?:-[0-9a-fA-F]{4}){3}-[0-9a-fA-F]{12}\Z")
26
+ _PERMISSION_REFUSAL = re.compile(
27
+ r"permission required: [^\r\n]+ was not run, as a headless run cannot ask for approval\Z"
28
+ )
29
+ _MODEL_USAGE_COUNTS = frozenset({"inputTokens", "outputTokens", "cacheReadInputTokens", "cacheCreationInputTokens"})
30
+ PARSER_CONTRACT = (
31
+ "grok-native-generation-json/v1;strict-utf8-json;no-duplicate-keys-or-nonfinite-numbers;"
32
+ "max-stream-bytes=10485760;one-final-object;session-uuid;stopReason=end_turn;"
33
+ "answer=exact-nonblank-text;max-final-utf8-bytes=131072;overflow-refused;"
34
+ "actual-model=one-valid-modelUsage-entry-with-valid-recognized-usage-counts-and-no-incomplete-ledger;"
35
+ "errors-incomplete-and-source-supported-headless-refusal-refuse;no-raw-metadata;"
36
+ "identity=sha256(canonical-json(contract,source-sha256(grok_native_generation.py,base.py)))"
37
+ )
38
+
39
+
40
+ def _protocol() -> ParserError:
41
+ return ParserError("Grok native generation output violates its protocol", code="protocol_error")
42
+
43
+
44
+ def parser_contract_digest() -> str:
45
+ sources = {}
46
+ try:
47
+ for name in ("grok_native_generation.py", "base.py"):
48
+ with Path(__file__).with_name(name).open("rb") as stream:
49
+ content = stream.read(1024 * 1024 + 1)
50
+ if len(content) > 1024 * 1024:
51
+ raise _protocol()
52
+ sources[name] = hashlib.sha256(content).hexdigest()
53
+ except OSError:
54
+ raise _protocol() from None
55
+ identity = json.dumps(
56
+ {"contract": PARSER_CONTRACT, "source_sha256": sources}, sort_keys=True, separators=(",", ":")
57
+ )
58
+ return hashlib.sha256(identity.encode()).hexdigest()
59
+
60
+
61
+ def _validate_json(value: Any) -> None:
62
+ pending = [value]
63
+ while pending:
64
+ part = pending.pop()
65
+ if isinstance(part, dict):
66
+ pending.extend(part.keys())
67
+ pending.extend(part.values())
68
+ elif isinstance(part, list):
69
+ pending.extend(part)
70
+ elif isinstance(part, str):
71
+ try:
72
+ part.encode("utf-8")
73
+ except UnicodeError:
74
+ raise _protocol() from None
75
+ elif isinstance(part, float) and not math.isfinite(part):
76
+ raise _protocol()
77
+
78
+
79
+ def _actual_model(value: object) -> str | None:
80
+ if not isinstance(value, dict) or len(value) != 1:
81
+ return None
82
+ model, usage = next(iter(value.items()))
83
+ model = model_identifier(model)
84
+ if model is None or model == "unknown" or not isinstance(usage, dict):
85
+ return None
86
+ counts = {key: value for key, value in usage.items() if key in _MODEL_USAGE_COUNTS}
87
+ if not counts or any(type(count) is not int or not 0 <= count <= MAX_METADATA_NUMBER for count in counts.values()):
88
+ return None
89
+ if "modelCalls" in usage and (
90
+ type(usage["modelCalls"]) is not int or not 1 <= usage["modelCalls"] <= MAX_METADATA_NUMBER
91
+ ):
92
+ return None
93
+ if not any(counts.values()) and usage.get("modelCalls", 0) == 0:
94
+ return None
95
+ if "costUSD" in usage and bounded_number(usage["costUSD"]) is None:
96
+ return None
97
+ return model
98
+
99
+
100
+ class GrokNativeGenerationParser(BaseParser):
101
+ """Publish one successful final text, preserving bytes and refusing truncation."""
102
+
103
+ name = "grok_native_generation_json"
104
+
105
+ def parse(self, stdout: str, stderr: str = "") -> ParsedCLIResponse:
106
+ try:
107
+ if len(stdout.encode("utf-8")) > MAX_STREAM_BYTES:
108
+ raise _protocol()
109
+ stderr.encode("utf-8")
110
+ except (AttributeError, UnicodeError):
111
+ raise _protocol() from None
112
+ if any(_PERMISSION_REFUSAL.fullmatch(line) for line in stderr.splitlines()):
113
+ raise ParserError("Grok native generation requires permission", code="permission_needed")
114
+ if not stdout.strip():
115
+ raise cli_failure(stderr, message="Grok native generation omitted its final output", code="protocol_error")
116
+ try:
117
+ payload = load_json(stdout)
118
+ except ParserError:
119
+ raise _protocol() from None
120
+ _validate_json(payload)
121
+ if not isinstance(payload, dict):
122
+ raise _protocol()
123
+ if any(name in payload for name in ("event", "status")) or ("type" in payload and payload["type"] != "error"):
124
+ raise _protocol()
125
+ if "is_error" in payload and type(payload["is_error"]) is not bool:
126
+ raise _protocol()
127
+ if "errors" in payload and payload["errors"] is not None and not isinstance(payload["errors"], list):
128
+ raise _protocol()
129
+ if (
130
+ payload.get("type") == "error"
131
+ or payload.get("is_error") is True
132
+ or payload.get("error") is not None
133
+ or payload.get("errors")
134
+ ):
135
+ raise cli_failure(payload, stderr, message="Grok native generation did not complete", code="protocol_error")
136
+ session = payload.get("sessionId")
137
+ if not isinstance(session, str) or not _SESSION.fullmatch(session) or payload.get("stopReason") != "end_turn":
138
+ raise _protocol()
139
+ text = payload.get("text")
140
+ if not isinstance(text, str):
141
+ raise _protocol()
142
+ if not text.strip():
143
+ raise ParserError("Grok native generation final response was empty", code="empty_response")
144
+ if len(text.encode("utf-8")) > OUTPUT_BYTES:
145
+ raise ParserError("Grok native generation final response exceeded its byte limit", code="output_limit")
146
+ return ParsedCLIResponse(
147
+ content=text,
148
+ metadata={
149
+ "actual_model": (
150
+ _actual_model(payload.get("modelUsage"))
151
+ if payload.get("usage_is_incomplete", False) is False
152
+ else None
153
+ ),
154
+ "tool_error_count": 0,
155
+ "permission_denial_count": 0,
156
+ },
157
+ )
clink/policy.py ADDED
@@ -0,0 +1,180 @@
1
+ """Adapter-owned advisory arguments and content-addressed verification helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import os
8
+ import stat
9
+ from pathlib import Path
10
+
11
+ # No operator/caller option can replace or weaken these arguments.
12
+ ENVIRONMENT_KEYS = (
13
+ "PATH",
14
+ "HOME",
15
+ "USER",
16
+ "LOGNAME",
17
+ "TMPDIR",
18
+ "TMP",
19
+ "TEMP",
20
+ "LANG",
21
+ "LC_ALL",
22
+ "LC_CTYPE",
23
+ "TZ",
24
+ "CODEX_HOME",
25
+ "CLAUDE_CONFIG_DIR",
26
+ )
27
+ CODEX_FEATURES_OFF = (
28
+ "hooks",
29
+ "plugin_hooks",
30
+ "plugins",
31
+ "apps",
32
+ "multi_agent",
33
+ "multi_agent_v2",
34
+ "shell_tool",
35
+ "unified_exec",
36
+ "code_mode",
37
+ "code_mode_only",
38
+ "code_mode_host",
39
+ "deferred_executor",
40
+ "browser_use",
41
+ "browser_use_external",
42
+ "computer_use",
43
+ "in_app_browser",
44
+ "image_generation",
45
+ "remote_plugin",
46
+ "plugin_discovery_prefetch",
47
+ "skill_search",
48
+ "skill_mcp_dependency_install",
49
+ "workspace_dependencies",
50
+ "request_permissions_tool",
51
+ )
52
+ CODEX_OVERRIDES = (
53
+ 'approval_policy="never"',
54
+ 'sandbox_mode="read-only"',
55
+ "agents.enabled=false",
56
+ "orchestrator.mcp.enabled=false",
57
+ "skills.bundled.enabled=false",
58
+ "skills.include_instructions=false",
59
+ "cloud.skills.enabled=false",
60
+ "project_doc_max_bytes=0",
61
+ 'web_search="live"',
62
+ "suppress_unstable_features_warning=true",
63
+ 'model_catalog_json="@CLINK_CODEX_MODEL_CATALOG@"',
64
+ "notify=[]",
65
+ )
66
+
67
+
68
+ CODEX_CATALOG_PATH = "clink/resources/codex-0.166.0-models-direct.json"
69
+ CODEX_CATALOG_SHA256 = "69d3e68d0e78a684c56b1818f1351149902fc61c398e4b69d371dfdea08e4e12"
70
+
71
+
72
+ def rendered_policy_arguments(adapter: str) -> tuple[str, ...]:
73
+ """Resolve only the trusted bundled-resource placeholder, independently of caller cwd."""
74
+ catalog = Path(__file__).resolve().parent.parent / CODEX_CATALOG_PATH
75
+ return tuple(
76
+ (
77
+ "model_catalog_json=" + json.dumps(str(catalog))
78
+ if argument == 'model_catalog_json="@CLINK_CODEX_MODEL_CATALOG@"'
79
+ else argument
80
+ )
81
+ for argument in policy_arguments(adapter)
82
+ )
83
+
84
+
85
+ def policy_arguments(adapter: str) -> tuple[str, ...]:
86
+ if adapter == "codex":
87
+ controls = []
88
+ for feature in CODEX_FEATURES_OFF:
89
+ controls.extend(("--disable", feature))
90
+ controls.extend(("--enable", "skip_host_skill_discovery"))
91
+ for override in CODEX_OVERRIDES:
92
+ controls.extend(("-c", override))
93
+ return (
94
+ "--no-daemon",
95
+ "--ask-for-approval",
96
+ "never",
97
+ "exec",
98
+ "--ignore-user-config",
99
+ "--ignore-rules",
100
+ "--ephemeral",
101
+ "--strict-config",
102
+ "--sandbox",
103
+ "read-only",
104
+ *controls,
105
+ "--skip-git-repo-check",
106
+ "--json",
107
+ )
108
+ if adapter == "claude":
109
+ return (
110
+ "--restricted",
111
+ "--safe-mode",
112
+ "--no-chrome",
113
+ "--disable-slash-commands",
114
+ "--tools",
115
+ "Read,Glob,Grep,WebSearch,WebFetch",
116
+ "--strict-mcp-config",
117
+ "--mcp-config",
118
+ '{"mcpServers":{}}',
119
+ "--permission-prompts",
120
+ "none",
121
+ "--no-session-persistence",
122
+ "--settings",
123
+ '{"disableAllHooks":true}',
124
+ "--print",
125
+ "--output-format",
126
+ "json",
127
+ )
128
+ return ()
129
+
130
+
131
+ def policy_recipe(adapter: str) -> dict:
132
+ arguments = policy_arguments(adapter)
133
+ result = {
134
+ "adapter": adapter,
135
+ "argv": [*arguments, "-"] if adapter == "codex" else list(arguments),
136
+ "environment_allowlist": list(ENVIRONMENT_KEYS),
137
+ "fixed_environment": {"CLINK_DELEGATION_DEPTH": "1"},
138
+ "native_auth_location_preserved": True,
139
+ "prompt_channel": "stdin",
140
+ }
141
+ if adapter == "codex":
142
+ result["resources"] = [{"path": CODEX_CATALOG_PATH, "sha256": CODEX_CATALOG_SHA256}]
143
+ return result
144
+
145
+
146
+ def recipe_digest(adapter: str) -> str:
147
+ """Bind evidence to exact arguments, environment keys and operational guarantees."""
148
+ return hashlib.sha256(
149
+ json.dumps(policy_recipe(adapter), sort_keys=True, separators=(",", ":")).encode()
150
+ ).hexdigest()
151
+
152
+
153
+ def executable_digest(path: str) -> str:
154
+ """Hash the actual executable without retaining its bytes or following a later path swap."""
155
+ digest = hashlib.sha256()
156
+ descriptor = os.open(path, os.O_RDONLY | os.O_NONBLOCK | os.O_NOFOLLOW | os.O_CLOEXEC)
157
+ with os.fdopen(descriptor, "rb") as stream:
158
+ initial = stream.fileno()
159
+ if not stat.S_ISREG(os.fstat(initial).st_mode):
160
+ return "not_a_regular_executable"
161
+ before = os.fstat(initial)
162
+ for block in iter(lambda: stream.read(1024 * 1024), b""):
163
+ digest.update(block)
164
+ after = os.fstat(initial)
165
+ if (before.st_dev, before.st_ino, before.st_size, before.st_mtime_ns) != (
166
+ after.st_dev,
167
+ after.st_ino,
168
+ after.st_size,
169
+ after.st_mtime_ns,
170
+ ):
171
+ return "changed_during_verification"
172
+ current = Path(path).stat()
173
+ if (after.st_dev, after.st_ino, after.st_size, after.st_mtime_ns) != (
174
+ current.st_dev,
175
+ current.st_ino,
176
+ current.st_size,
177
+ current.st_mtime_ns,
178
+ ):
179
+ return "changed_during_verification"
180
+ return digest.hexdigest()