maka-agent 0.2.0-dev.3.20260830 → 0.2.0-dev.5.20260830
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.
- package/dist/cli-core.js +1 -0
- package/dist/eval-bundle-path.js +1 -1
- package/dist/pi-tui-runner.js +3 -1
- package/dist/runtime-host-cli.js +37 -3
- package/dist/runtime-host-onboarding.js +10 -1
- package/dist/runtime-host-peer-mesh-management-command.js +17 -0
- package/dist/runtime-host-setup-command.js +33 -15
- package/dist/runtime-host-systemd-service.js +9 -3
- package/dist/runtime-host-tui-context.js +2 -6
- package/native/runtime-host-peer/prebuilds/darwin-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-arm64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/linux-x64/maka_runtime_host_peer.node +0 -0
- package/native/runtime-host-peer/prebuilds/win32-x64/maka_runtime_host_peer.node +0 -0
- package/node_modules/@maka/eval/dist/external-subject.js +1 -1
- package/node_modules/@maka/eval/dist/harbor-external-subject.js +2 -2
- package/node_modules/@maka/runtime/dist/sandbox/macos-seatbelt.js +68 -9
- package/node_modules/@maka/runtime-host/dist/client/peer-client.js +22 -3
- package/node_modules/@maka/runtime-host/dist/operator/peer-mesh-management-frame.js +14 -0
- package/{packages/eval/dist/index.js → node_modules/@maka/runtime-host/dist/peer-mesh/display-name.js} +12 -10
- package/node_modules/@maka/runtime-host/dist/peer-mesh/model.js +32 -21
- package/node_modules/@maka/runtime-host/dist/peer-mesh/node.js +207 -47
- package/node_modules/@maka/runtime-host/dist/peer-mesh/owner.js +2 -0
- package/node_modules/@maka/runtime-host/dist/peer-mesh/store.js +18 -5
- package/node_modules/@maka/runtime-host/dist/protocol/index.js +5 -1
- package/node_modules/@maka/runtime-host/dist/protocol/peer-mesh.js +59 -2
- package/node_modules/@maka/runtime-host/dist/server/access-authority.js +11 -7
- package/node_modules/@maka/runtime-host/dist/server/connection-session.js +3 -0
- package/node_modules/@maka/runtime-host/dist/server/execution-service.js +1 -0
- package/node_modules/@maka/runtime-host/dist/server/host-kernel.js +1 -1
- package/node_modules/@maka/runtime-host/dist/server/peer-mesh-authority.js +19 -0
- package/package.json +1 -2
- package/packages/eval/dist/attempt-store.js +0 -138
- package/packages/eval/dist/cli.js +0 -119
- package/packages/eval/dist/experiment-directory.js +0 -59
- package/packages/eval/dist/experiment.js +0 -49
- package/packages/eval/dist/external-subject.js +0 -488
- package/packages/eval/dist/harbor-external-subject.js +0 -929
- package/packages/eval/dist/harbor-maka-subject.js +0 -83
- package/packages/eval/dist/harness-environment.js +0 -80
- package/packages/eval/dist/harness-executor.js +0 -1013
- package/packages/eval/dist/install-preflight.js +0 -201
- package/packages/eval/dist/maka-artifacts.js +0 -126
- package/packages/eval/dist/maka-runtime-policy.js +0 -42
- package/packages/eval/dist/maka-subject.js +0 -268
- package/packages/eval/dist/metering-checkpoint.js +0 -80
- package/packages/eval/dist/provider-admission.js +0 -39
- package/packages/eval/dist/provider-metering.js +0 -67
- package/packages/eval/dist/provider-web-tool-surface.js +0 -74
- package/packages/eval/dist/relay-result-frame.js +0 -38
- package/packages/eval/dist/result.js +0 -94
- package/packages/eval/dist/runner.js +0 -290
- package/packages/eval/dist/spec.js +0 -135
- package/packages/eval/dist/toolchain-verification.js +0 -130
- package/packages/eval/harbor/deepseek-codex-models.json +0 -69
- package/packages/eval/harbor/deepseek-harness-profile/cordis.patch.yml +0 -140
- package/packages/eval/harbor/deepseek-harness-profile/cordis.yml +0 -18
- package/packages/eval/harbor/deepseek-harness-profile/package.json +0 -10
- package/packages/eval/harbor/docker-compose-egress-proxy.yaml +0 -57
- package/packages/eval/harbor/egress-proxy/Dockerfile +0 -25
- package/packages/eval/harbor/egress-proxy/entrypoint.sh +0 -62
- package/packages/eval/harbor/egress-proxy/network-policy +0 -148
- package/packages/eval/harbor/egress_filter.py +0 -435
- package/packages/eval/harbor/eval_framework.py +0 -40
- package/packages/eval/harbor/relay_agent.py +0 -686
- package/packages/eval/harbor/run_trial.py +0 -223
- package/packages/eval/package.json +0 -20
|
@@ -1,686 +0,0 @@
|
|
|
1
|
-
# Licensed to the Apache Software Foundation (ASF) under one
|
|
2
|
-
# or more contributor license agreements. See the NOTICE file
|
|
3
|
-
# distributed with this work for additional information
|
|
4
|
-
# regarding copyright ownership. The ASF licenses this file
|
|
5
|
-
# to you under the Apache License, Version 2.0 (the
|
|
6
|
-
# "License"); you may not use this file except in compliance
|
|
7
|
-
# with the License. You may obtain a copy of the License at
|
|
8
|
-
#
|
|
9
|
-
# http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
-
#
|
|
11
|
-
# Unless required by applicable law or agreed to in writing,
|
|
12
|
-
# software distributed under the License is distributed on an
|
|
13
|
-
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
|
14
|
-
# KIND, either express or implied. See the License for the
|
|
15
|
-
# specific language governing permissions and limitations
|
|
16
|
-
# under the License.
|
|
17
|
-
|
|
18
|
-
"""One-shot Harbor/Pier Agent that delegates one subject execution to @maka/eval."""
|
|
19
|
-
|
|
20
|
-
from __future__ import annotations
|
|
21
|
-
|
|
22
|
-
import asyncio
|
|
23
|
-
import base64
|
|
24
|
-
import contextlib
|
|
25
|
-
import hashlib
|
|
26
|
-
import json
|
|
27
|
-
import os
|
|
28
|
-
import re
|
|
29
|
-
import shlex
|
|
30
|
-
import tempfile
|
|
31
|
-
from pathlib import Path
|
|
32
|
-
from typing import Any
|
|
33
|
-
|
|
34
|
-
from eval_framework import selected
|
|
35
|
-
|
|
36
|
-
framework = selected()
|
|
37
|
-
if framework == "harbor":
|
|
38
|
-
from harbor.agents.base import BaseAgent
|
|
39
|
-
else:
|
|
40
|
-
from pier.agents.base import BaseAgent
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
class RelayTransportClosed(RuntimeError):
|
|
44
|
-
pass
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
RESULT_FRAME_PREFIX = "MAKA-EVAL-RESULT-V1"
|
|
48
|
-
SCOPE_ERROR_PREFIX = "MAKA-EVAL-SCOPE-ERROR-V1"
|
|
49
|
-
RESULT_PAYLOAD_LIMIT_BYTES = 2 * 1024
|
|
50
|
-
RESULT_CARRIER_LIMIT_BYTES = 64 * 1024
|
|
51
|
-
SUBJECT_STDOUT_PATH = "/logs/artifacts/maka-subject.stdout.txt"
|
|
52
|
-
SUBJECT_STDERR_PATH = "/logs/artifacts/maka-subject.stderr.txt"
|
|
53
|
-
CAPABILITY_PREFIX = "MAKA-EVAL-CAPABILITIES-V1 "
|
|
54
|
-
NAMESPACE_PREFIX = "MAKA-EVAL-POLICY-NAMESPACE-V1 "
|
|
55
|
-
# The egress policy only constrains what the IP output hooks can see. NET_RAW
|
|
56
|
-
# grants an AF_PACKET socket that writes beneath them, and NET_ADMIN grants the
|
|
57
|
-
# ability to delete the ruleset outright. The overlay drops NET_RAW, but a
|
|
58
|
-
# task's own compose can add either back, and a `cap_add` wins over an
|
|
59
|
-
# overlay's `cap_drop`.
|
|
60
|
-
BYPASS_CAPABILITIES = {"NET_RAW": 1 << 13, "NET_ADMIN": 1 << 12}
|
|
61
|
-
# Every set is read, not just the effective one: a non-root subject reports an
|
|
62
|
-
# empty effective set while a file-capability executable still reacquires
|
|
63
|
-
# anything the bounding set kept, and the permitted, inheritable and ambient
|
|
64
|
-
# sets each raise into the effective set without an exec at all.
|
|
65
|
-
CAPABILITY_FIELDS = ("CapEff", "CapPrm", "CapBnd")
|
|
66
|
-
# Harbor installs the policy by running `network-policy` inside this service, so
|
|
67
|
-
# its network namespace is what "the namespace the policy was applied to" means.
|
|
68
|
-
# Reading the identity from both sides answers that question directly, rather
|
|
69
|
-
# than inferring it from something only the shared namespace would expose.
|
|
70
|
-
POLICY_SERVICE = "harbor-docker-egress-control-sidecar"
|
|
71
|
-
NAMESPACE_PROBE = f"printf %s {shlex.quote(NAMESPACE_PREFIX)}; readlink /proc/self/ns/net"
|
|
72
|
-
PROXY_IPV4_PATH = "/opt/maka-egress/proxy-ipv4"
|
|
73
|
-
PROXY_HOSTS_PREFIX = "MAKA-EVAL-PROXY-HOST-V1 "
|
|
74
|
-
# The kernel's own form for a namespace link target. Matching it keeps an
|
|
75
|
-
# unexpected answer from being compared as if it were an identity.
|
|
76
|
-
NAMESPACE_IDENTITY = re.compile(r"^net:\[[0-9]+\]$")
|
|
77
|
-
|
|
78
|
-
_host_teardown_requested = False
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
def request_host_teardown() -> None:
|
|
82
|
-
global _host_teardown_requested
|
|
83
|
-
_host_teardown_requested = True
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
class RelayAgent(BaseAgent):
|
|
87
|
-
def __init__(
|
|
88
|
-
self,
|
|
89
|
-
*args: Any,
|
|
90
|
-
relay_host: str,
|
|
91
|
-
relay_port: int,
|
|
92
|
-
relay_token: str,
|
|
93
|
-
teardown_timeout_ms: int,
|
|
94
|
-
**kwargs: Any,
|
|
95
|
-
):
|
|
96
|
-
super().__init__(*args, **kwargs)
|
|
97
|
-
self._host = relay_host
|
|
98
|
-
self._port = relay_port
|
|
99
|
-
self._token = relay_token
|
|
100
|
-
if not isinstance(teardown_timeout_ms, int) or teardown_timeout_ms <= 0:
|
|
101
|
-
raise RuntimeError("Maka Eval teardown timeout is invalid")
|
|
102
|
-
self._teardown_timeout = teardown_timeout_ms / 1000
|
|
103
|
-
|
|
104
|
-
@staticmethod
|
|
105
|
-
def name() -> str:
|
|
106
|
-
return "maka-eval-relay"
|
|
107
|
-
|
|
108
|
-
def version(self) -> str:
|
|
109
|
-
return "1"
|
|
110
|
-
|
|
111
|
-
async def setup(self, environment: Any) -> None:
|
|
112
|
-
return None
|
|
113
|
-
|
|
114
|
-
async def run(self, instruction: str, environment: Any, context: Any) -> None:
|
|
115
|
-
reader, writer = await asyncio.open_connection(self._host, self._port)
|
|
116
|
-
execution: asyncio.Task[Any] | None = None
|
|
117
|
-
decision: asyncio.Task[dict[str, Any]] | None = None
|
|
118
|
-
request: dict[str, Any] | None = None
|
|
119
|
-
execution_reported = False
|
|
120
|
-
scope_path = f"/logs/agent/.maka-eval-{self._token}.pid"
|
|
121
|
-
environment_path = f"/tmp/maka-eval-{self._token}.env"
|
|
122
|
-
try:
|
|
123
|
-
cwd_prefix = f"MAKA-EVAL-CWD-V1 {self._token} "
|
|
124
|
-
working_directory = await environment.exec(
|
|
125
|
-
f"printf {shlex.quote(cwd_prefix)}; pwd 2>/dev/null"
|
|
126
|
-
)
|
|
127
|
-
cwd_lines = [
|
|
128
|
-
line[len(cwd_prefix) :]
|
|
129
|
-
for line in str(working_directory.stdout or "").splitlines()
|
|
130
|
-
if line.startswith(cwd_prefix)
|
|
131
|
-
]
|
|
132
|
-
cwd = cwd_lines[0] if len(cwd_lines) == 1 else ""
|
|
133
|
-
if working_directory.return_code != 0 or not cwd.startswith("/") or "\x00" in cwd:
|
|
134
|
-
raise RuntimeError("Maka Eval could not resolve the task working directory")
|
|
135
|
-
await _require_constrained_subject(environment)
|
|
136
|
-
if not await _send(
|
|
137
|
-
writer,
|
|
138
|
-
{
|
|
139
|
-
"token": self._token,
|
|
140
|
-
"kind": "ready",
|
|
141
|
-
"instruction": instruction,
|
|
142
|
-
"cwd": cwd,
|
|
143
|
-
},
|
|
144
|
-
):
|
|
145
|
-
raise RelayTransportClosed("Maka Eval relay transport closed before ready")
|
|
146
|
-
request = await _receive(reader)
|
|
147
|
-
_require_message(request, self._token, "execute")
|
|
148
|
-
command = await _prepare_command(environment, request, self._token, scope_path)
|
|
149
|
-
execution = asyncio.create_task(environment.exec(command, cwd=cwd))
|
|
150
|
-
decision = asyncio.create_task(_receive(reader))
|
|
151
|
-
done, _ = await asyncio.wait({execution, decision}, return_when=asyncio.FIRST_COMPLETED)
|
|
152
|
-
if decision in done and execution not in done:
|
|
153
|
-
decision.result()
|
|
154
|
-
raise RelayTransportClosed("Maka Eval relay received control before execution")
|
|
155
|
-
result = execution.result()
|
|
156
|
-
await _persist_subject_outputs(environment, result)
|
|
157
|
-
stdout, diagnostic = _project_result(result, request)
|
|
158
|
-
# A subject that exited on its own leaves the shared environment as
|
|
159
|
-
# it left it, and the verifier reads that environment. Nothing is
|
|
160
|
-
# waiting on those processes here — `environment.exec` has already
|
|
161
|
-
# returned — so tearing them down would not unblock anything; it
|
|
162
|
-
# would only edit the thing about to be measured, and edit it for
|
|
163
|
-
# some subjects and not others. Cancellation still quiesces, because
|
|
164
|
-
# there the subject has not stopped and the trial is being abandoned.
|
|
165
|
-
if not await _send(
|
|
166
|
-
writer,
|
|
167
|
-
{
|
|
168
|
-
"token": self._token,
|
|
169
|
-
"kind": "executed",
|
|
170
|
-
"termination": "exited",
|
|
171
|
-
"exitCode": result.return_code,
|
|
172
|
-
"stdout": stdout,
|
|
173
|
-
"diagnostic": diagnostic,
|
|
174
|
-
},
|
|
175
|
-
):
|
|
176
|
-
raise RelayTransportClosed("Maka Eval relay transport closed before result")
|
|
177
|
-
execution_reported = True
|
|
178
|
-
_require_message(
|
|
179
|
-
decision.result() if decision.done() else await decision,
|
|
180
|
-
self._token,
|
|
181
|
-
"verify",
|
|
182
|
-
)
|
|
183
|
-
except asyncio.CancelledError:
|
|
184
|
-
if request is not None and execution is not None:
|
|
185
|
-
execution_terminal = execution.done() and not execution.cancelled()
|
|
186
|
-
terminal_projection = None
|
|
187
|
-
if execution_terminal:
|
|
188
|
-
terminal_result = execution.result()
|
|
189
|
-
terminal_projection = _project_result(terminal_result, request)
|
|
190
|
-
# A subject that already exited has nothing left to settle, and
|
|
191
|
-
# tearing its scope down here would remove what the verifier is
|
|
192
|
-
# about to score -- the same environment edit this relay stopped
|
|
193
|
-
# making on the ordinary path. Only a subject still running is
|
|
194
|
-
# brought to a stop.
|
|
195
|
-
if terminal_projection is not None:
|
|
196
|
-
result = terminal_result
|
|
197
|
-
else:
|
|
198
|
-
result = await _settle_or_destroy(
|
|
199
|
-
environment, cwd, scope_path, execution, self._teardown_timeout
|
|
200
|
-
)
|
|
201
|
-
if result is not None:
|
|
202
|
-
await _persist_subject_outputs(environment, result)
|
|
203
|
-
if (
|
|
204
|
-
result is not None
|
|
205
|
-
and not execution_reported
|
|
206
|
-
and (execution_terminal or not _host_teardown_requested)
|
|
207
|
-
):
|
|
208
|
-
stdout, diagnostic = terminal_projection or _project_result(result, request)
|
|
209
|
-
with contextlib.suppress(Exception):
|
|
210
|
-
await _send(
|
|
211
|
-
writer,
|
|
212
|
-
{
|
|
213
|
-
"token": self._token,
|
|
214
|
-
"kind": "executed",
|
|
215
|
-
"termination": "exited" if execution_terminal else "framework_timeout",
|
|
216
|
-
"exitCode": result.return_code if execution_terminal else 124,
|
|
217
|
-
"stdout": stdout,
|
|
218
|
-
"diagnostic": diagnostic,
|
|
219
|
-
},
|
|
220
|
-
)
|
|
221
|
-
elif not execution_reported and not _host_teardown_requested:
|
|
222
|
-
with contextlib.suppress(Exception):
|
|
223
|
-
await _send(
|
|
224
|
-
writer,
|
|
225
|
-
{
|
|
226
|
-
"token": self._token,
|
|
227
|
-
"kind": "executed",
|
|
228
|
-
"termination": "framework_timeout",
|
|
229
|
-
"exitCode": 124,
|
|
230
|
-
"stdout": "",
|
|
231
|
-
"diagnostic": (
|
|
232
|
-
_carrier_diagnostic("result-frame-missing", b"")
|
|
233
|
-
if request.get("captureStdout", True)
|
|
234
|
-
else {"category": "none"}
|
|
235
|
-
),
|
|
236
|
-
},
|
|
237
|
-
)
|
|
238
|
-
raise
|
|
239
|
-
except RelayTransportClosed:
|
|
240
|
-
if request is not None and execution is not None:
|
|
241
|
-
result = await _settle_or_destroy(
|
|
242
|
-
environment, cwd, scope_path, execution, self._teardown_timeout
|
|
243
|
-
)
|
|
244
|
-
if result is not None:
|
|
245
|
-
with contextlib.suppress(Exception):
|
|
246
|
-
await _persist_subject_outputs(environment, result)
|
|
247
|
-
except BaseException:
|
|
248
|
-
if request is not None and execution is not None:
|
|
249
|
-
result = await _settle_or_destroy(
|
|
250
|
-
environment, cwd, scope_path, execution, self._teardown_timeout
|
|
251
|
-
)
|
|
252
|
-
if result is not None:
|
|
253
|
-
with contextlib.suppress(Exception):
|
|
254
|
-
await _persist_subject_outputs(environment, result)
|
|
255
|
-
raise
|
|
256
|
-
finally:
|
|
257
|
-
if decision is not None and not decision.done():
|
|
258
|
-
decision.cancel()
|
|
259
|
-
with contextlib.suppress(BaseException):
|
|
260
|
-
await decision
|
|
261
|
-
if request is not None:
|
|
262
|
-
with contextlib.suppress(Exception):
|
|
263
|
-
await asyncio.wait_for(
|
|
264
|
-
environment.exec(
|
|
265
|
-
f"rm -f -- {shlex.quote(scope_path)} {shlex.quote(environment_path)}",
|
|
266
|
-
cwd=cwd,
|
|
267
|
-
timeout_sec=1,
|
|
268
|
-
),
|
|
269
|
-
timeout=1,
|
|
270
|
-
)
|
|
271
|
-
with contextlib.suppress(BrokenPipeError, ConnectionError, RuntimeError, TimeoutError):
|
|
272
|
-
writer.close()
|
|
273
|
-
await asyncio.wait_for(writer.wait_closed(), timeout=1)
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
async def _prepare_command(
|
|
277
|
-
environment: Any,
|
|
278
|
-
request: dict[str, Any],
|
|
279
|
-
token: str,
|
|
280
|
-
scope_path: str,
|
|
281
|
-
) -> str:
|
|
282
|
-
credentials = request.get("credentials")
|
|
283
|
-
public_environment = request.get("environment", {})
|
|
284
|
-
if not isinstance(credentials, dict) or not all(
|
|
285
|
-
isinstance(key, str) and isinstance(value, str) for key, value in credentials.items()
|
|
286
|
-
):
|
|
287
|
-
raise RuntimeError("invalid Maka Eval credentials")
|
|
288
|
-
if not isinstance(public_environment, dict) or not all(
|
|
289
|
-
isinstance(key, str) and isinstance(value, str)
|
|
290
|
-
for key, value in public_environment.items()
|
|
291
|
-
):
|
|
292
|
-
raise RuntimeError("invalid Maka Eval environment")
|
|
293
|
-
if set(credentials) & set(public_environment):
|
|
294
|
-
raise RuntimeError("Maka Eval environment overlaps credentials")
|
|
295
|
-
capture_stdout = request.get("captureStdout", True)
|
|
296
|
-
if not isinstance(capture_stdout, bool):
|
|
297
|
-
raise RuntimeError("invalid Maka Eval stdout policy")
|
|
298
|
-
result_token = request.get("resultToken")
|
|
299
|
-
if not isinstance(result_token, str) or re.fullmatch(r"[0-9a-f]{32}", result_token) is None:
|
|
300
|
-
raise RuntimeError("invalid Maka Eval result token")
|
|
301
|
-
if "MAKA_EVAL_RESULT_TOKEN" in credentials or "MAKA_EVAL_RESULT_TOKEN" in public_environment:
|
|
302
|
-
raise RuntimeError("Maka Eval environment contains a reserved name")
|
|
303
|
-
for label, values in (("environment", public_environment), ("credential", credentials)):
|
|
304
|
-
if any(re.fullmatch(r"[A-Za-z_][A-Za-z0-9_]*", key) is None for key in values):
|
|
305
|
-
raise RuntimeError(f"invalid Maka Eval {label} name")
|
|
306
|
-
|
|
307
|
-
container_path = f"/tmp/maka-eval-{token}.env"
|
|
308
|
-
secret_path = None
|
|
309
|
-
try:
|
|
310
|
-
with tempfile.NamedTemporaryFile("w", encoding="utf-8", delete=False) as secret:
|
|
311
|
-
secret_path = Path(secret.name)
|
|
312
|
-
os.chmod(secret_path, 0o600)
|
|
313
|
-
for key, value in {
|
|
314
|
-
**public_environment,
|
|
315
|
-
**credentials,
|
|
316
|
-
"MAKA_EVAL_RESULT_TOKEN": result_token,
|
|
317
|
-
}.items():
|
|
318
|
-
secret.write(f"export {key}={shlex.quote(value)}\n")
|
|
319
|
-
await environment.upload_file(secret_path, container_path)
|
|
320
|
-
finally:
|
|
321
|
-
if secret_path is not None:
|
|
322
|
-
secret_path.unlink(missing_ok=True)
|
|
323
|
-
subject = shlex.join([request["command"], *request["args"]])
|
|
324
|
-
output_redirect = "" if capture_stdout else " >/dev/null"
|
|
325
|
-
scope_error = shlex.quote(f"{SCOPE_ERROR_PREFIX} {result_token}\\n")
|
|
326
|
-
inner = (
|
|
327
|
-
"umask 077; "
|
|
328
|
-
f"{{ echo $$ > {shlex.quote(scope_path)}; }} 2>/dev/null || "
|
|
329
|
-
f"{{ printf {scope_error}; exit 111; }}; "
|
|
330
|
-
f". {shlex.quote(container_path)}; command -p rm -f {shlex.quote(container_path)}; "
|
|
331
|
-
f"exec {subject}{output_redirect}"
|
|
332
|
-
)
|
|
333
|
-
return f"setsid --wait sh -c {shlex.quote(inner)}"
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
async def _require_constrained_subject(environment: Any) -> None:
|
|
337
|
-
"""Refuse to start the subject unless the egress policy actually governs it.
|
|
338
|
-
|
|
339
|
-
Runs after the policy is applied and before the subject exists, so a task
|
|
340
|
-
that keeps a capability the policy cannot see, or that keeps the subject out
|
|
341
|
-
of the namespace the policy was applied to, fails the attempt instead of
|
|
342
|
-
producing a result the isolation contract never actually covered.
|
|
343
|
-
"""
|
|
344
|
-
if os.environ.get("MAKA_EVAL_EGRESS_REQUIRED") != "1":
|
|
345
|
-
return
|
|
346
|
-
probe = await environment.exec(
|
|
347
|
-
f"printf %s {shlex.quote(CAPABILITY_PREFIX)}; "
|
|
348
|
-
r"sed -n 's/^\(Cap[A-Za-z]*\):[[:space:]]*/\1=/p' /proc/self/status | tr '\n' ' '; "
|
|
349
|
-
"echo; " + NAMESPACE_PROBE
|
|
350
|
-
)
|
|
351
|
-
policy = await environment.service_exec(NAMESPACE_PROBE, service=POLICY_SERVICE)
|
|
352
|
-
if probe.return_code != 0 or policy.return_code != 0:
|
|
353
|
-
raise RuntimeError("Maka Eval could not read the subject isolation evidence")
|
|
354
|
-
capabilities = _sole_probe_line(probe, CAPABILITY_PREFIX)
|
|
355
|
-
namespace = _sole_probe_line(probe, NAMESPACE_PREFIX)
|
|
356
|
-
policy_namespace = _sole_probe_line(policy, NAMESPACE_PREFIX)
|
|
357
|
-
if not all(NAMESPACE_IDENTITY.match(value) for value in (namespace, policy_namespace)):
|
|
358
|
-
raise RuntimeError("Maka Eval could not read the subject isolation evidence")
|
|
359
|
-
reported: dict[str, int] = {}
|
|
360
|
-
for field in capabilities.split():
|
|
361
|
-
name, _, value = field.partition("=")
|
|
362
|
-
try:
|
|
363
|
-
reported[name] = int(value, 16)
|
|
364
|
-
except ValueError:
|
|
365
|
-
raise RuntimeError("Maka Eval could not read the subject capability set") from None
|
|
366
|
-
if not all(field in reported for field in CAPABILITY_FIELDS):
|
|
367
|
-
raise RuntimeError("Maka Eval could not read the subject capability set")
|
|
368
|
-
granted = 0
|
|
369
|
-
for value in reported.values():
|
|
370
|
-
granted |= value
|
|
371
|
-
held = sorted(name for name, bit in BYPASS_CAPABILITIES.items() if granted & bit)
|
|
372
|
-
if held:
|
|
373
|
-
raise RuntimeError(
|
|
374
|
-
"the subject holds "
|
|
375
|
-
+ ", ".join(held)
|
|
376
|
-
+ ", which bypasses the Eval egress policy; remove it from the task"
|
|
377
|
-
)
|
|
378
|
-
if namespace != policy_namespace:
|
|
379
|
-
raise RuntimeError(
|
|
380
|
-
"the subject does not share the network namespace the Eval egress policy "
|
|
381
|
-
"was applied to; remove the task's own networking on the subject service"
|
|
382
|
-
)
|
|
383
|
-
await _pin_proxy_hostname(environment)
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
HOSTS_ALIAS_AWK = r"""
|
|
387
|
-
/^[[:space:]]*#/ { print; next }
|
|
388
|
-
{
|
|
389
|
-
for (i = 2; i <= NF; i++) {
|
|
390
|
-
if ($i == host) next
|
|
391
|
-
}
|
|
392
|
-
print
|
|
393
|
-
}
|
|
394
|
-
"""
|
|
395
|
-
|
|
396
|
-
# Four decimal octets 0-255, no empty fields and no leading zeros. The same
|
|
397
|
-
# program is embedded in egress-proxy/network-policy; the contract test
|
|
398
|
-
# requires the two copies to stay identical.
|
|
399
|
-
IPV4_OCTET_AWK = r"""
|
|
400
|
-
BEGIN { FS = "." }
|
|
401
|
-
NF != 4 { exit 1 }
|
|
402
|
-
{
|
|
403
|
-
for (i = 1; i <= 4; i++) {
|
|
404
|
-
if ($i !~ /^(0|[1-9][0-9]*)$/ || $i + 0 > 255) exit 1
|
|
405
|
-
}
|
|
406
|
-
}
|
|
407
|
-
"""
|
|
408
|
-
|
|
409
|
-
_PUBLISHED_IPV4 = re.compile(
|
|
410
|
-
r"^(?:(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)\.){3}"
|
|
411
|
-
r"(?:25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)$"
|
|
412
|
-
)
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
def _valid_egress_proxy_host(host: str) -> bool:
|
|
416
|
-
if not host or host.startswith(".") or host.endswith(".") or ".." in host:
|
|
417
|
-
return False
|
|
418
|
-
# Match the pin script's `*[!A-Za-z0-9.-]*` class. str.isalnum() also
|
|
419
|
-
# accepts Unicode letters, which the shell would later reject as a pin
|
|
420
|
-
# failure instead of an invalid host.
|
|
421
|
-
return all(
|
|
422
|
-
(character.isalnum() and character.isascii()) or character in ".-"
|
|
423
|
-
for character in host
|
|
424
|
-
)
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
def _valid_published_ipv4(ip: str) -> bool:
|
|
428
|
-
return bool(_PUBLISHED_IPV4.fullmatch(ip))
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
async def _pin_proxy_hostname(environment: Any) -> None:
|
|
432
|
-
"""Point the proxy hostname at the published IPv4 so Docker DNS can be refused.
|
|
433
|
-
|
|
434
|
-
The subject only needs that one name, because the HTTP proxy does remote
|
|
435
|
-
resolution itself. Pinning it in /etc/hosts lets the namespace policy reject
|
|
436
|
-
127.0.0.11:53 without breaking HTTPS_PROXY.
|
|
437
|
-
"""
|
|
438
|
-
host = os.environ.get("MAKA_EVAL_EGRESS_ALLOWED_HOST") or "maka-eval-mitmproxy"
|
|
439
|
-
if not _valid_egress_proxy_host(host):
|
|
440
|
-
raise RuntimeError("Eval egress proxy host is invalid")
|
|
441
|
-
script = f"""
|
|
442
|
-
set -eu
|
|
443
|
-
path={shlex.quote(PROXY_IPV4_PATH)}
|
|
444
|
-
host={shlex.quote(host)}
|
|
445
|
-
case "$host" in
|
|
446
|
-
""|.*|*..*|*.) exit 1 ;;
|
|
447
|
-
*[!A-Za-z0-9.-]*) exit 1 ;;
|
|
448
|
-
esac
|
|
449
|
-
test -f "$path"
|
|
450
|
-
ip=$(tr -d ' \\t\\r\\n' < "$path")
|
|
451
|
-
case "$ip" in
|
|
452
|
-
""|*.*.*.*.*|*[!0-9.]*) exit 1 ;;
|
|
453
|
-
esac
|
|
454
|
-
if ! printf '%s\\n' "$ip" | awk {shlex.quote(IPV4_OCTET_AWK)}; then
|
|
455
|
-
exit 1
|
|
456
|
-
fi
|
|
457
|
-
tmp=$(mktemp)
|
|
458
|
-
trap 'rm -f "$tmp"' EXIT
|
|
459
|
-
if [ -f /etc/hosts ]; then
|
|
460
|
-
awk -v host="$host" {shlex.quote(HOSTS_ALIAS_AWK)} /etc/hosts > "$tmp"
|
|
461
|
-
fi
|
|
462
|
-
printf '%s %s\\n' "$ip" "$host" >> "$tmp"
|
|
463
|
-
cat "$tmp" > /etc/hosts
|
|
464
|
-
printf %s {shlex.quote(PROXY_HOSTS_PREFIX)}
|
|
465
|
-
printf '%s %s\\n' "$ip" "$host"
|
|
466
|
-
"""
|
|
467
|
-
# Harbor 0.20.0 BaseEnvironment.exec and DockerEnvironment.exec take user=;
|
|
468
|
-
# DockerEnvironment._compose_exec forwards it as `docker compose exec -u`.
|
|
469
|
-
probe = await environment.exec(script, user="root")
|
|
470
|
-
if probe.return_code != 0:
|
|
471
|
-
raise RuntimeError("Maka Eval could not pin the Eval egress proxy hostname")
|
|
472
|
-
reported = _sole_probe_line(probe, PROXY_HOSTS_PREFIX)
|
|
473
|
-
ip, _, pinned = reported.partition(" ")
|
|
474
|
-
if pinned != host or not _valid_published_ipv4(ip):
|
|
475
|
-
raise RuntimeError("Maka Eval could not pin the Eval egress proxy hostname")
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
def _sole_probe_line(probe: Any, prefix: str) -> str:
|
|
479
|
-
reported = [
|
|
480
|
-
line[len(prefix) :]
|
|
481
|
-
for line in str(probe.stdout or "").splitlines()
|
|
482
|
-
if line.startswith(prefix)
|
|
483
|
-
]
|
|
484
|
-
if len(reported) != 1:
|
|
485
|
-
raise RuntimeError("Maka Eval could not read the subject isolation evidence")
|
|
486
|
-
return reported[0].strip()
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
async def _persist_subject_outputs(environment: Any, result: Any) -> None:
|
|
490
|
-
with tempfile.TemporaryDirectory() as directory:
|
|
491
|
-
root = Path(directory)
|
|
492
|
-
stdout = root / "stdout"
|
|
493
|
-
stderr = root / "stderr"
|
|
494
|
-
stdout.write_text(str(getattr(result, "stdout", "") or ""), encoding="utf-8")
|
|
495
|
-
stderr.write_text(str(getattr(result, "stderr", "") or ""), encoding="utf-8")
|
|
496
|
-
prepared = await environment.exec("mkdir -p /logs/artifacts && chmod 700 /logs/artifacts")
|
|
497
|
-
if prepared.return_code != 0:
|
|
498
|
-
raise RuntimeError("Maka Eval could not prepare subject artifact output")
|
|
499
|
-
await environment.upload_file(stdout, SUBJECT_STDOUT_PATH)
|
|
500
|
-
await environment.upload_file(stderr, SUBJECT_STDERR_PATH)
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
def _decode_result_carrier(carrier: str, token: str) -> tuple[str, dict[str, Any]]:
|
|
504
|
-
raw = carrier.encode("utf-8", errors="replace")
|
|
505
|
-
if len(raw) > RESULT_CARRIER_LIMIT_BYTES:
|
|
506
|
-
return "", _carrier_diagnostic("result-frame-oversize", raw)
|
|
507
|
-
prefix = f"{RESULT_FRAME_PREFIX} {token} "
|
|
508
|
-
candidates = [line for line in carrier.splitlines(keepends=True) if line.startswith(prefix)]
|
|
509
|
-
if len(candidates) != 1:
|
|
510
|
-
category = "result-frame-missing" if not candidates else "result-frame-ambiguous"
|
|
511
|
-
return "", _carrier_diagnostic(category, raw)
|
|
512
|
-
frame = candidates[0]
|
|
513
|
-
fields = frame.rstrip("\r\n").split(" ", 4)
|
|
514
|
-
if len(fields) != 5:
|
|
515
|
-
return "", _carrier_diagnostic("result-frame-invalid", raw)
|
|
516
|
-
_, framed_token, length_text, digest, encoded = fields
|
|
517
|
-
try:
|
|
518
|
-
length = int(length_text)
|
|
519
|
-
padding = "=" * (-len(encoded) % 4)
|
|
520
|
-
payload = base64.b64decode(encoded + padding, altchars=b"-_", validate=True)
|
|
521
|
-
except (ValueError, base64.binascii.Error):
|
|
522
|
-
return "", _carrier_diagnostic("result-frame-invalid", raw)
|
|
523
|
-
if (
|
|
524
|
-
framed_token != token
|
|
525
|
-
or length < 0
|
|
526
|
-
or length > RESULT_PAYLOAD_LIMIT_BYTES
|
|
527
|
-
or len(payload) != length
|
|
528
|
-
or not re.fullmatch(r"[0-9a-f]{64}", digest)
|
|
529
|
-
or hashlib.sha256(payload).hexdigest() != digest
|
|
530
|
-
):
|
|
531
|
-
return "", _carrier_diagnostic("result-frame-invalid", raw)
|
|
532
|
-
noise = carrier.replace(frame, "", 1).encode("utf-8", errors="replace")
|
|
533
|
-
diagnostic = (
|
|
534
|
-
{"category": "none"}
|
|
535
|
-
if not noise
|
|
536
|
-
else _carrier_diagnostic("unstructured-output", noise)
|
|
537
|
-
)
|
|
538
|
-
try:
|
|
539
|
-
decoded = payload.decode("utf-8", errors="strict")
|
|
540
|
-
except UnicodeDecodeError:
|
|
541
|
-
return "", _carrier_diagnostic("result-frame-invalid", raw)
|
|
542
|
-
return decoded, diagnostic
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
def _project_result(result: Any, request: dict[str, Any]) -> tuple[str, dict[str, Any]]:
|
|
546
|
-
carrier = str(getattr(result, "stdout", "") or "")
|
|
547
|
-
scope_error = f"{SCOPE_ERROR_PREFIX} {request['resultToken']}"
|
|
548
|
-
if carrier == f"{scope_error}\n":
|
|
549
|
-
return "", _carrier_diagnostic("execution-scope-unavailable", carrier.encode())
|
|
550
|
-
if not request.get("captureStdout", True):
|
|
551
|
-
return "", {"category": "none"}
|
|
552
|
-
return _decode_result_carrier(carrier, request["resultToken"])
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
def _carrier_diagnostic(category: str, value: bytes) -> dict[str, Any]:
|
|
556
|
-
return {
|
|
557
|
-
"category": category,
|
|
558
|
-
"bytes": len(value),
|
|
559
|
-
"sha256": hashlib.sha256(value).hexdigest(),
|
|
560
|
-
}
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
async def _settle(environment: Any, cwd: str, scope_path: str, execution: Any) -> Any:
|
|
564
|
-
if execution.cancelled():
|
|
565
|
-
raise RuntimeError("Maka Eval subject execution was cancelled")
|
|
566
|
-
if execution.done():
|
|
567
|
-
result = execution.result()
|
|
568
|
-
else:
|
|
569
|
-
result = None
|
|
570
|
-
for signal, timeout in (("TERM", 20), ("KILL", 10)):
|
|
571
|
-
await _signal(environment, cwd, scope_path, signal)
|
|
572
|
-
try:
|
|
573
|
-
result = await asyncio.wait_for(asyncio.shield(execution), timeout=timeout)
|
|
574
|
-
break
|
|
575
|
-
except asyncio.CancelledError:
|
|
576
|
-
if execution.cancelled():
|
|
577
|
-
raise RuntimeError("Maka Eval subject execution was cancelled") from None
|
|
578
|
-
raise
|
|
579
|
-
except TimeoutError:
|
|
580
|
-
pass
|
|
581
|
-
if result is None:
|
|
582
|
-
raise RuntimeError("Maka Eval subject did not settle")
|
|
583
|
-
await _quiesce_scope(environment, cwd, scope_path)
|
|
584
|
-
return result
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
async def _settle_or_destroy(
|
|
588
|
-
environment: Any,
|
|
589
|
-
cwd: str,
|
|
590
|
-
scope_path: str,
|
|
591
|
-
execution: Any,
|
|
592
|
-
timeout: float,
|
|
593
|
-
) -> Any | None:
|
|
594
|
-
loop = asyncio.get_running_loop()
|
|
595
|
-
deadline = loop.time() + timeout
|
|
596
|
-
stop_reserve = min(20.0, timeout * 0.2)
|
|
597
|
-
try:
|
|
598
|
-
return await asyncio.wait_for(
|
|
599
|
-
_settle(environment, cwd, scope_path, execution),
|
|
600
|
-
timeout=max(0.001, deadline - loop.time() - stop_reserve),
|
|
601
|
-
)
|
|
602
|
-
except Exception:
|
|
603
|
-
try:
|
|
604
|
-
remaining = max(0.001, deadline - loop.time())
|
|
605
|
-
await asyncio.wait_for(environment.stop(delete=True), timeout=remaining)
|
|
606
|
-
except Exception:
|
|
607
|
-
pass
|
|
608
|
-
finally:
|
|
609
|
-
if not execution.done():
|
|
610
|
-
execution.cancel()
|
|
611
|
-
with contextlib.suppress(asyncio.CancelledError, Exception):
|
|
612
|
-
remaining = max(0.001, deadline - loop.time())
|
|
613
|
-
await asyncio.wait_for(execution, timeout=remaining)
|
|
614
|
-
return None
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
async def _signal(environment: Any, cwd: str, scope_path: str, signal: str) -> None:
|
|
618
|
-
command = (
|
|
619
|
-
f"pgid=$(cat {shlex.quote(scope_path)} 2>/dev/null) || exit 0; "
|
|
620
|
-
"case $pgid in ''|0|*[!0-9]*) exit 0;; esac; "
|
|
621
|
-
f"kill -{signal} -- \"-$pgid\""
|
|
622
|
-
)
|
|
623
|
-
with contextlib.suppress(Exception):
|
|
624
|
-
await environment.exec(
|
|
625
|
-
command,
|
|
626
|
-
cwd=cwd,
|
|
627
|
-
timeout_sec=5,
|
|
628
|
-
)
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
async def _quiesce_scope(environment: Any, cwd: str, scope_path: str) -> None:
|
|
632
|
-
if not await _scope_active(environment, cwd, scope_path):
|
|
633
|
-
return
|
|
634
|
-
for signal, timeout in (("TERM", 10), ("KILL", 5)):
|
|
635
|
-
await _signal(environment, cwd, scope_path, signal)
|
|
636
|
-
deadline = asyncio.get_running_loop().time() + timeout
|
|
637
|
-
while asyncio.get_running_loop().time() < deadline:
|
|
638
|
-
if not await _scope_active(environment, cwd, scope_path):
|
|
639
|
-
return
|
|
640
|
-
await asyncio.sleep(0.1)
|
|
641
|
-
raise RuntimeError("Maka Eval execution scope did not quiesce")
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
async def _scope_active(environment: Any, cwd: str, scope_path: str) -> bool:
|
|
645
|
-
result = await environment.exec(
|
|
646
|
-
f"pgid=$(cat {shlex.quote(scope_path)} 2>/dev/null) || exit 4; "
|
|
647
|
-
"case $pgid in ''|0|*[!0-9]*) exit 4;; esac; "
|
|
648
|
-
"kill -0 -- \"-$pgid\" 2>/dev/null; status=$?; "
|
|
649
|
-
"if [ $status -eq 0 ]; then exit 0; fi; exit 3",
|
|
650
|
-
cwd=cwd,
|
|
651
|
-
timeout_sec=5,
|
|
652
|
-
)
|
|
653
|
-
if result.return_code == 0:
|
|
654
|
-
return True
|
|
655
|
-
if result.return_code == 3:
|
|
656
|
-
return False
|
|
657
|
-
raise RuntimeError("Maka Eval execution scope evidence was unavailable")
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
def _require_message(value: dict[str, Any], token: str, kind: str) -> None:
|
|
661
|
-
if value.get("token") != token or value.get("kind") != kind:
|
|
662
|
-
raise RuntimeError("invalid Maka Eval relay message")
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
async def _receive(reader: asyncio.StreamReader) -> dict[str, Any]:
|
|
666
|
-
try:
|
|
667
|
-
raw = await reader.readline()
|
|
668
|
-
if not raw:
|
|
669
|
-
raise RelayTransportClosed("Maka Eval relay peer closed")
|
|
670
|
-
value = json.loads(raw)
|
|
671
|
-
except (ConnectionError, json.JSONDecodeError, ValueError) as error:
|
|
672
|
-
raise RelayTransportClosed("Maka Eval relay message was unavailable") from error
|
|
673
|
-
if not isinstance(value, dict):
|
|
674
|
-
raise RelayTransportClosed("Maka Eval relay message was invalid")
|
|
675
|
-
return value
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
async def _send(writer: asyncio.StreamWriter, value: object) -> bool:
|
|
679
|
-
if writer.is_closing():
|
|
680
|
-
return False
|
|
681
|
-
try:
|
|
682
|
-
writer.write((json.dumps(value, separators=(",", ":")) + "\n").encode())
|
|
683
|
-
await writer.drain()
|
|
684
|
-
return True
|
|
685
|
-
except (BrokenPipeError, ConnectionError, RuntimeError):
|
|
686
|
-
return False
|