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
|
@@ -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()
|