maf-sandbox-codeact 0.21.4__tar.gz → 0.22.0__tar.gz
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.
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/PKG-INFO +8 -6
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/README.md +6 -4
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/pyproject.toml +2 -2
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/pyproject.toml.orig +2 -2
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/src/maf_sandbox_codeact/_tool.py +184 -135
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/LICENSE +0 -0
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/src/maf_sandbox_codeact/__init__.py +0 -0
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/src/maf_sandbox_codeact/_runtime.py +0 -0
- {maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/src/maf_sandbox_codeact/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: maf-sandbox-codeact
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.22.0
|
|
4
4
|
Summary: CodeAct as a Microsoft Agent Framework tool — the model writes a short Python program and it runs inside a sandbox — written against maf-sandbox so it runs on any sandbox backend.
|
|
5
5
|
Keywords: codeact,code-execution,sandbox,agent-framework,microsoft-agent-framework
|
|
6
6
|
Author: SOKOLAI BV
|
|
@@ -15,7 +15,7 @@ Classifier: Programming Language :: Python :: 3.12
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3.13
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.14
|
|
17
17
|
Classifier: Topic :: Software Development :: Interpreters
|
|
18
|
-
Requires-Dist: maf-sandbox>=0.
|
|
18
|
+
Requires-Dist: maf-sandbox>=0.47.0,<0.48
|
|
19
19
|
Requires-Dist: agent-framework-core>=1.19.0,<1.20
|
|
20
20
|
Requires-Python: >=3.12, <3.15
|
|
21
21
|
Project-URL: Homepage, https://www.sokol.ai
|
|
@@ -52,7 +52,7 @@ tools = make_codeact_tools(
|
|
|
52
52
|
|
|
53
53
|
The host supplies the router and `CallerContext`. With no configured backend the factory returns `[]`. Unsupported requirements are refused before attachment.
|
|
54
54
|
|
|
55
|
-
The default path
|
|
55
|
+
The default path requests a backend-owned `python-portable-v1` channel, preferring exec. This profile promises Python 3.11+ with `json`, `math`, `re`, `sys` and `types`; it promises no other imports or retained state. The exec channel verifies CPython and owns `program.py`, the interpreter and any host-tool transport. Source text is file content, never part of the command line. See [channel selection and migration](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/program-channels.md).
|
|
56
56
|
|
|
57
57
|
Use the [Docker sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/06_docker_codeact), [ACAS sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/03_acas_codeact) or [WSLC sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/04_wslc_codeact) for complete applications.
|
|
58
58
|
|
|
@@ -64,7 +64,7 @@ All four channels below are off by default. The host enables them when building
|
|
|
64
64
|
|---|---|
|
|
65
65
|
| `file_store=store` | A `files` argument selecting caller-visible input files. |
|
|
66
66
|
| `output_sink=sink`, `outputs=...` | Collection and delivery of named output files. |
|
|
67
|
-
| `host_tools=registry` | Calls from guest Python to registered host functions
|
|
67
|
+
| `host_tools=registry` | Calls from guest Python to registered host functions through a supporting program channel. |
|
|
68
68
|
| `egress_allow=(...)` | Network requests to named destinations. |
|
|
69
69
|
|
|
70
70
|
These channels operate during `execute_code`. Hiding its final report does not undo host calls, network requests or artifact delivery.
|
|
@@ -101,10 +101,12 @@ tools = make_codeact_tools(
|
|
|
101
101
|
)
|
|
102
102
|
```
|
|
103
103
|
|
|
104
|
-
`files_in` and `files_out` bound file count, individual bytes and total bytes. A missing declared output is reported. Artifacts carry no guest-selected media type; the host decides how to handle them.
|
|
104
|
+
`files_in` and `files_out` bound shared-file count, individual bytes and total bytes. Shared inputs default to 8 MiB per file, 24 MiB total and 63 files, leaving room for the default 8 MiB program and its staging file within a 32 MiB/64-file backend ceiling. Explicit limits are preserved and the complete channel demand is checked without clamping; larger programs or host-tool transport traffic can require a backend with larger ceilings. On the automatic channel path, `program=ProgramRequirements(max_program_bytes=8 * 1024 * 1024, host_tool_timeout_seconds=30)` from `maf_sandbox` configures source bytes and callback time separately. A missing declared output is reported. Artifacts carry no guest-selected media type; the host decides how to handle them.
|
|
105
105
|
|
|
106
106
|
Keep the output sink separate from the agent's writable input store. Otherwise guest code could overwrite files through a channel that bypasses the host's file-write approval. See the [files sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/08_docker_codeact_files).
|
|
107
107
|
|
|
108
|
+
With `withhold_guest_output=True`, a sink can supply `locate=lambda name: f".tool-results/execute_code/{name}"` to report a fixed landing folder in the Saved and not-written lists. Have `deliver` write to the same location and return it in `LandedArtifact.name` or `handle`. The pure synchronous locator receives only validated declared names, normalized as the sink requests; it must never depend on guest content or delivery state. A mismatched receipt falls back to the declared name and logs a warning. The default locator preserves existing results, and `per_call` sinks retain their folder-only reporting. See the [withholding contract](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/kinds/codeact.md#withholding-guest-output) for the information this mode still reveals.
|
|
109
|
+
|
|
108
110
|
## Host functions and network access
|
|
109
111
|
|
|
110
112
|
Register host functions before passing the registry to the factory. Reading its combined policy seals it against later registration.
|
|
@@ -125,7 +127,7 @@ registry.register(exchange_rate)
|
|
|
125
127
|
|
|
126
128
|
`exchange_rate` is a host-defined function. Its body runs with host authority and bypasses ordinary agent middleware. Configure declarations, permitted identities and approval policy before exposing it. Registering a user-authority function makes the enclosing tool approval-gated.
|
|
127
129
|
|
|
128
|
-
A nonempty registry requires `HOST_TOOLS` and
|
|
130
|
+
A nonempty registry requires `HOST_TOOLS` and an explicit supporting program channel. Docker, ACAS and Docker Sandboxes supply the exec channel, which adds `EXEC`, `FILES_IN` and `FILES_OUT` internally. WSLC supports programs without host tools. The exec image needs the POSIX launcher utilities, including `sh` and `nohup`. Hyperlight native host tools remain pending.
|
|
129
131
|
|
|
130
132
|
Transport traffic counts toward backend transfer limits. Set response limits to fit your functions; broad defaults can make the tool fail attachment. Model-named files live separately from transport files. Without a registry, `program.py` is reserved.
|
|
131
133
|
|
|
@@ -27,7 +27,7 @@ tools = make_codeact_tools(
|
|
|
27
27
|
|
|
28
28
|
The host supplies the router and `CallerContext`. With no configured backend the factory returns `[]`. Unsupported requirements are refused before attachment.
|
|
29
29
|
|
|
30
|
-
The default path
|
|
30
|
+
The default path requests a backend-owned `python-portable-v1` channel, preferring exec. This profile promises Python 3.11+ with `json`, `math`, `re`, `sys` and `types`; it promises no other imports or retained state. The exec channel verifies CPython and owns `program.py`, the interpreter and any host-tool transport. Source text is file content, never part of the command line. See [channel selection and migration](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/program-channels.md).
|
|
31
31
|
|
|
32
32
|
Use the [Docker sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/06_docker_codeact), [ACAS sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/03_acas_codeact) or [WSLC sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/04_wslc_codeact) for complete applications.
|
|
33
33
|
|
|
@@ -39,7 +39,7 @@ All four channels below are off by default. The host enables them when building
|
|
|
39
39
|
|---|---|
|
|
40
40
|
| `file_store=store` | A `files` argument selecting caller-visible input files. |
|
|
41
41
|
| `output_sink=sink`, `outputs=...` | Collection and delivery of named output files. |
|
|
42
|
-
| `host_tools=registry` | Calls from guest Python to registered host functions
|
|
42
|
+
| `host_tools=registry` | Calls from guest Python to registered host functions through a supporting program channel. |
|
|
43
43
|
| `egress_allow=(...)` | Network requests to named destinations. |
|
|
44
44
|
|
|
45
45
|
These channels operate during `execute_code`. Hiding its final report does not undo host calls, network requests or artifact delivery.
|
|
@@ -76,10 +76,12 @@ tools = make_codeact_tools(
|
|
|
76
76
|
)
|
|
77
77
|
```
|
|
78
78
|
|
|
79
|
-
`files_in` and `files_out` bound file count, individual bytes and total bytes. A missing declared output is reported. Artifacts carry no guest-selected media type; the host decides how to handle them.
|
|
79
|
+
`files_in` and `files_out` bound shared-file count, individual bytes and total bytes. Shared inputs default to 8 MiB per file, 24 MiB total and 63 files, leaving room for the default 8 MiB program and its staging file within a 32 MiB/64-file backend ceiling. Explicit limits are preserved and the complete channel demand is checked without clamping; larger programs or host-tool transport traffic can require a backend with larger ceilings. On the automatic channel path, `program=ProgramRequirements(max_program_bytes=8 * 1024 * 1024, host_tool_timeout_seconds=30)` from `maf_sandbox` configures source bytes and callback time separately. A missing declared output is reported. Artifacts carry no guest-selected media type; the host decides how to handle them.
|
|
80
80
|
|
|
81
81
|
Keep the output sink separate from the agent's writable input store. Otherwise guest code could overwrite files through a channel that bypasses the host's file-write approval. See the [files sample](https://github.com/sokolaidev/maf-extensions/tree/main/samples/08_docker_codeact_files).
|
|
82
82
|
|
|
83
|
+
With `withhold_guest_output=True`, a sink can supply `locate=lambda name: f".tool-results/execute_code/{name}"` to report a fixed landing folder in the Saved and not-written lists. Have `deliver` write to the same location and return it in `LandedArtifact.name` or `handle`. The pure synchronous locator receives only validated declared names, normalized as the sink requests; it must never depend on guest content or delivery state. A mismatched receipt falls back to the declared name and logs a warning. The default locator preserves existing results, and `per_call` sinks retain their folder-only reporting. See the [withholding contract](https://github.com/sokolaidev/maf-extensions/blob/main/docs/sandbox/kinds/codeact.md#withholding-guest-output) for the information this mode still reveals.
|
|
84
|
+
|
|
83
85
|
## Host functions and network access
|
|
84
86
|
|
|
85
87
|
Register host functions before passing the registry to the factory. Reading its combined policy seals it against later registration.
|
|
@@ -100,7 +102,7 @@ registry.register(exchange_rate)
|
|
|
100
102
|
|
|
101
103
|
`exchange_rate` is a host-defined function. Its body runs with host authority and bypasses ordinary agent middleware. Configure declarations, permitted identities and approval policy before exposing it. Registering a user-authority function makes the enclosing tool approval-gated.
|
|
102
104
|
|
|
103
|
-
A nonempty registry requires `HOST_TOOLS` and
|
|
105
|
+
A nonempty registry requires `HOST_TOOLS` and an explicit supporting program channel. Docker, ACAS and Docker Sandboxes supply the exec channel, which adds `EXEC`, `FILES_IN` and `FILES_OUT` internally. WSLC supports programs without host tools. The exec image needs the POSIX launcher utilities, including `sh` and `nohup`. Hyperlight native host tools remain pending.
|
|
104
106
|
|
|
105
107
|
Transport traffic counts toward backend transfer limits. Set response limits to fit your functions; broad defaults can make the tool fail attachment. Model-named files live separately from transport files. Without a registry, `program.py` is reserved.
|
|
106
108
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "maf-sandbox-codeact"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.22.0"
|
|
4
4
|
description = "CodeAct as a Microsoft Agent Framework tool — the model writes a short Python program and it runs inside a sandbox — written against maf-sandbox so it runs on any sandbox backend."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<3.15"
|
|
@@ -24,7 +24,7 @@ classifiers = [
|
|
|
24
24
|
"Topic :: Software Development :: Interpreters",
|
|
25
25
|
]
|
|
26
26
|
dependencies = [
|
|
27
|
-
"maf-sandbox>=0.
|
|
27
|
+
"maf-sandbox>=0.47.0,<0.48",
|
|
28
28
|
"agent-framework-core>=1.19.0,<1.20",
|
|
29
29
|
]
|
|
30
30
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "maf-sandbox-codeact"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.22.0"
|
|
4
4
|
description = "CodeAct as a Microsoft Agent Framework tool — the model writes a short Python program and it runs inside a sandbox — written against maf-sandbox so it runs on any sandbox backend."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.12,<3.15"
|
|
@@ -35,7 +35,7 @@ dependencies = [
|
|
|
35
35
|
# import, and that one fails at the call, where no import check is looking. The number
|
|
36
36
|
# itself is below, and `scripts/set_dependents_range.py` moves it — so this says which
|
|
37
37
|
# release the floor is for, never which release that is.
|
|
38
|
-
"maf-sandbox>=0.
|
|
38
|
+
"maf-sandbox>=0.47.0,<0.48",
|
|
39
39
|
# The `@tool` decorator. MAF is a genuine dependency of the tool itself, not of the host
|
|
40
40
|
# application. Both ends of the range are measured: 1.19 made an item's embedded label
|
|
41
41
|
# restrict-only, which the result wrapper now works with rather than against.
|
|
@@ -35,8 +35,6 @@ from typing import TYPE_CHECKING, Any, cast
|
|
|
35
35
|
|
|
36
36
|
from maf_sandbox import (
|
|
37
37
|
DEFAULT_TRANSFER_LIMITS,
|
|
38
|
-
SHIM_MODULE,
|
|
39
|
-
WORK_DIRECTORY,
|
|
40
38
|
CallerContext,
|
|
41
39
|
Capability,
|
|
42
40
|
Cleanup,
|
|
@@ -50,6 +48,7 @@ from maf_sandbox import (
|
|
|
50
48
|
ListedFile,
|
|
51
49
|
NameNormalization,
|
|
52
50
|
OutputSink,
|
|
51
|
+
ProgramRequirements,
|
|
53
52
|
SandboxArtifactNameInvalid,
|
|
54
53
|
SandboxOutputError,
|
|
55
54
|
SandboxProgramTimeout,
|
|
@@ -61,9 +60,6 @@ from maf_sandbox import (
|
|
|
61
60
|
collect_outputs,
|
|
62
61
|
echoed_name,
|
|
63
62
|
error_detail,
|
|
64
|
-
guest_run_layout,
|
|
65
|
-
host_tool_calls_over_exec,
|
|
66
|
-
host_tool_shim,
|
|
67
63
|
validate_artifact_name,
|
|
68
64
|
)
|
|
69
65
|
from maf_sandbox.maf import (
|
|
@@ -81,10 +77,12 @@ if TYPE_CHECKING:
|
|
|
81
77
|
|
|
82
78
|
from agent_framework import AgentFileStore
|
|
83
79
|
from maf_sandbox import (
|
|
80
|
+
Artifact,
|
|
84
81
|
FileStoreProvenance,
|
|
85
82
|
HostToolAggregate,
|
|
86
83
|
HostToolRegistry,
|
|
87
84
|
LandedArtifact,
|
|
85
|
+
ProgramRequirements,
|
|
88
86
|
Sandbox,
|
|
89
87
|
SandboxKey,
|
|
90
88
|
)
|
|
@@ -136,10 +134,10 @@ _SMALLEST_MANIFEST = len(
|
|
|
136
134
|
#: time and cannot un-deliver, so "could not be saved" is not the same as "nothing was saved".
|
|
137
135
|
_MAY_HAVE_LANDED = "Some of them may already have been saved; do not assume none were."
|
|
138
136
|
|
|
139
|
-
_INTERPRETER = "python3"
|
|
140
|
-
|
|
141
137
|
#: Eight artifacts is a generous single call and a cap that actually bounds something; the
|
|
142
138
|
#: byte ceilings are the shared defaults, so only the count is this workload's own opinion.
|
|
139
|
+
# Shared inputs leave room for the default program within a 32 MiB / 64-file backend.
|
|
140
|
+
_DEFAULT_FILES_IN = replace(DEFAULT_TRANSFER_LIMITS, max_total_bytes=24 * 1024 * 1024, max_files=63)
|
|
143
141
|
_DEFAULT_FILES_OUT = replace(DEFAULT_TRANSFER_LIMITS, max_files=8)
|
|
144
142
|
|
|
145
143
|
#: Writing an expression and expecting a REPL to echo it is the commonest way a first CodeAct
|
|
@@ -201,11 +199,12 @@ def codeact_sandbox_spec(
|
|
|
201
199
|
image_id: str | None = None,
|
|
202
200
|
*,
|
|
203
201
|
outputs: CodeactOutputs = CodeactOutputs.NONE,
|
|
204
|
-
files_in: TransferLimits =
|
|
202
|
+
files_in: TransferLimits = _DEFAULT_FILES_IN,
|
|
205
203
|
files_out: TransferLimits = _DEFAULT_FILES_OUT,
|
|
206
204
|
host_tools: HostToolRegistry | None = None,
|
|
207
205
|
egress_allow: Sequence[str | EgressRule] = (),
|
|
208
206
|
runtime: CodeactRuntime | None = None,
|
|
207
|
+
program: ProgramRequirements = ProgramRequirements(),
|
|
209
208
|
takes_files: bool = False,
|
|
210
209
|
credential_retention_seconds: int | None = None,
|
|
211
210
|
) -> SandboxSpec:
|
|
@@ -269,6 +268,7 @@ def codeact_sandbox_spec(
|
|
|
269
268
|
surface=_host_tools(host_tools),
|
|
270
269
|
egress_allow=egress_allow,
|
|
271
270
|
runtime=runtime,
|
|
271
|
+
program=program,
|
|
272
272
|
takes_files=takes_files,
|
|
273
273
|
credential_retention_seconds=credential_retention_seconds,
|
|
274
274
|
)
|
|
@@ -288,10 +288,11 @@ def make_codeact_tools(
|
|
|
288
288
|
image: str | None = None,
|
|
289
289
|
image_id: str | None = None,
|
|
290
290
|
exec_timeout_seconds: int = 120,
|
|
291
|
-
files_in: TransferLimits =
|
|
291
|
+
files_in: TransferLimits = _DEFAULT_FILES_IN,
|
|
292
292
|
files_out: TransferLimits = _DEFAULT_FILES_OUT,
|
|
293
293
|
egress_allow: Sequence[str | EgressRule] = (),
|
|
294
294
|
runtime: CodeactRuntime | None = None,
|
|
295
|
+
program: ProgramRequirements = ProgramRequirements(),
|
|
295
296
|
credential_retention_seconds: int | None = None,
|
|
296
297
|
file_store_provenance: FileStoreProvenance | None = None,
|
|
297
298
|
requires_file_integrity: SourceIntegrity | None = None,
|
|
@@ -323,7 +324,7 @@ def make_codeact_tools(
|
|
|
323
324
|
:data:`CodeactOutputs.NONE`, and refused at attach without one.
|
|
324
325
|
outputs: How a program's output files are named. See :class:`CodeactOutputs`.
|
|
325
326
|
withhold_guest_output: Keep what the program printed out of the tool result, and answer
|
|
326
|
-
with whether it exited cleanly and the
|
|
327
|
+
with whether it exited cleanly and the declared names or their sink locations. No
|
|
327
328
|
guest-authored text survives into the result — but the values that replace it were
|
|
328
329
|
still chosen by a program the model wrote. The workload therefore claims
|
|
329
330
|
``SourceIntegrity.UNTRUSTED`` for its derived report in both modes. The result
|
|
@@ -358,8 +359,9 @@ def make_codeact_tools(
|
|
|
358
359
|
``docs/sandbox/information-flow.md``.
|
|
359
360
|
|
|
360
361
|
**What withholding gets you, exactly.** The prose and the shape are this package's,
|
|
361
|
-
and
|
|
362
|
-
to choose, and it is a channel rather than a
|
|
362
|
+
and artifact locations depend only on declared names and host configuration — but
|
|
363
|
+
what fills them is the program's to choose, and it is a channel rather than a
|
|
364
|
+
leak-free boundary. Whether the program
|
|
363
365
|
exited cleanly is one bit and whether it finished in time is another; **each
|
|
364
366
|
declared output is one further bit**, since the program decides whether to write it
|
|
365
367
|
and the result says of every declared name whether it landed — up to
|
|
@@ -453,36 +455,8 @@ def make_codeact_tools(
|
|
|
453
455
|
f"a non-empty host_tools registry makes it the guest's patience as well as the "
|
|
454
456
|
f"run's bound, so it must be a finite positive number of seconds."
|
|
455
457
|
)
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
# never answered while the host-tool call it asked for goes on to act.
|
|
459
|
-
host_tool_call = _HostToolCall(
|
|
460
|
-
host_tools, host_tool_shim(host_tools.names(), call_timeout=exec_timeout_seconds)
|
|
461
|
-
)
|
|
462
|
-
if configured and runtime is None and files_in.max_files < 1:
|
|
463
|
-
# `program.py` is one inbound file on every call, so a cap below one refuses all of
|
|
464
|
-
# them. Every impossible pairing below is caught here rather than per call: a tool the
|
|
465
|
-
# model can see and can never use successfully is worse than one that never attached.
|
|
466
|
-
raise ValueError(
|
|
467
|
-
f"{EXECUTE_CODE_TOOL_NAME}: files_in.max_files is {files_in.max_files}, and the "
|
|
468
|
-
f"program itself is one file written into the sandbox on every call, so no call "
|
|
469
|
-
f"could succeed."
|
|
470
|
-
)
|
|
471
|
-
if host_tool_call is not None:
|
|
472
|
-
if files_in.max_files < 2:
|
|
473
|
-
raise ValueError(
|
|
474
|
-
f"{EXECUTE_CODE_TOOL_NAME}: files_in.max_files is {files_in.max_files}, and a "
|
|
475
|
-
f"non-empty host_tools registry puts the guest's host-tool module beside the "
|
|
476
|
-
f"program on every call, so no call could succeed."
|
|
477
|
-
)
|
|
478
|
-
crossing = len(host_tool_call.shim.encode())
|
|
479
|
-
room = min(files_in.max_bytes_per_file, files_in.max_total_bytes)
|
|
480
|
-
if crossing > room:
|
|
481
|
-
raise ValueError(
|
|
482
|
-
f"{EXECUTE_CODE_TOOL_NAME}: the guest's host-tool module is {crossing} bytes and "
|
|
483
|
-
f"crosses beside the program on every call, and this host's files_in allows "
|
|
484
|
-
f"{room}, so no call could succeed."
|
|
485
|
-
)
|
|
458
|
+
host_tool_call = _HostToolCall(host_tools)
|
|
459
|
+
|
|
486
460
|
if configured and outputs is CodeactOutputs.DECLARED and files_out.max_files < 1:
|
|
487
461
|
raise ValueError(
|
|
488
462
|
f"{EXECUTE_CODE_TOOL_NAME}: outputs={str(CodeactOutputs.DECLARED)!r} shows the "
|
|
@@ -544,6 +518,7 @@ def make_codeact_tools(
|
|
|
544
518
|
surface=surface,
|
|
545
519
|
egress_allow=egress_allow if configured else (),
|
|
546
520
|
runtime=runtime if configured else None,
|
|
521
|
+
program=program if configured else ProgramRequirements(),
|
|
547
522
|
takes_files=file_store is not None,
|
|
548
523
|
credential_retention_seconds=credential_retention_seconds if configured else None,
|
|
549
524
|
)
|
|
@@ -694,24 +669,24 @@ def _codeact_spec(
|
|
|
694
669
|
surface: HostToolAggregate | None,
|
|
695
670
|
egress_allow: Sequence[str | EgressRule] = (),
|
|
696
671
|
runtime: CodeactRuntime | None = None,
|
|
672
|
+
program: ProgramRequirements = ProgramRequirements(),
|
|
697
673
|
takes_files: bool = False,
|
|
698
674
|
credential_retention_seconds: int | None = None,
|
|
699
675
|
) -> SandboxSpec:
|
|
700
676
|
""":func:`codeact_sandbox_spec`, over a host-tool surface the caller has already derived."""
|
|
701
677
|
_validate_runtime(runtime, takes_files=takes_files, outputs=outputs)
|
|
678
|
+
if runtime is None and program.profile != "python-portable-v1":
|
|
679
|
+
raise ValueError("CodeAct requires the python-portable-v1 execution profile")
|
|
702
680
|
if runtime is not None and surface is not None:
|
|
703
681
|
raise ValueError("CodeAct runtime host tools require a native channel; not supported")
|
|
704
682
|
collects = outputs is not CodeactOutputs.NONE
|
|
705
|
-
requires =
|
|
683
|
+
requires: set[Capability] = set() if runtime is None else {Capability.RUN_CODE}
|
|
706
684
|
if takes_files:
|
|
707
685
|
requires.add(Capability.FILES_IN)
|
|
708
686
|
if collects:
|
|
709
687
|
requires.add(Capability.FILES_OUT)
|
|
710
688
|
if surface is not None:
|
|
711
|
-
|
|
712
|
-
# and reads its request files and the exit marker back over the pull surface, so even a
|
|
713
|
-
# stdout-only program that can call a host function needs one.
|
|
714
|
-
requires |= {Capability.HOST_TOOLS, Capability.FILES_OUT}
|
|
689
|
+
requires.add(Capability.HOST_TOOLS)
|
|
715
690
|
# CodeAct runs model-written code, so it accepts only two postures and never UNRESTRICTED:
|
|
716
691
|
# CLOSED to compute offline, ALLOWLIST to reach the deployment's named sources. The mode is
|
|
717
692
|
# derived from whether any host was named — an allowlist with hosts is ALLOWLIST, an empty
|
|
@@ -738,7 +713,10 @@ def _codeact_spec(
|
|
|
738
713
|
max_identity_scope=IdentityScope.PER_SANDBOX if authority else None,
|
|
739
714
|
max_identity_retention_seconds=credential_retention_seconds,
|
|
740
715
|
work_dir=runtime.guest_work_dir if runtime is not None else None,
|
|
741
|
-
execution_contract=runtime_contract(runtime)
|
|
716
|
+
execution_contract=runtime_contract(runtime)
|
|
717
|
+
if runtime is not None
|
|
718
|
+
else f"codeact:{program.profile}",
|
|
719
|
+
program=program if runtime is None else None,
|
|
742
720
|
# Model-written code can write outside the call path and leave processes running.
|
|
743
721
|
confined_to_guest_call_path=False,
|
|
744
722
|
# And read whatever a sibling call put in the sandbox, so calls run one at a time.
|
|
@@ -873,8 +851,7 @@ _DESCRIPTION_HOST_TOOLS = """**To call a host tool, ``import maf_host_tools`` an
|
|
|
873
851
|
The tools you may call this way are: {names}. A refusal raises
|
|
874
852
|
``maf_host_tools.HostToolError``, whose message says why."""
|
|
875
853
|
|
|
876
|
-
_DESCRIPTION_ARG_CODE = """code: The Python source to run
|
|
877
|
-
whatever the sandbox image ships."""
|
|
854
|
+
_DESCRIPTION_ARG_CODE = """code: The Python source to run using the profile described above."""
|
|
878
855
|
|
|
879
856
|
_DESCRIPTION_ARG_FILES = """files: Store-relative paths to share into the sandbox, or
|
|
880
857
|
omit for none. Only files in your file store listing can be shared."""
|
|
@@ -935,6 +912,19 @@ _DESCRIPTION_IN_PLACE_WITHHELD = """Naming a file in both ``files`` and ``output
|
|
|
935
912
|
|
|
936
913
|
_DESCRIPTION_RETURNS_SAVED_WITHHELD = """ A call that saved files also names each one."""
|
|
937
914
|
|
|
915
|
+
_DESCRIPTION_DECLARED_WITHHELD_LOCATED = """**To produce files, name them in ``outputs`` and
|
|
916
|
+
write them into the working directory.** They are saved to host storage after the
|
|
917
|
+
program exits. The result names their host-configured landing paths when delivery
|
|
918
|
+
confirms them, otherwise their declared names. A name you declare and do not write
|
|
919
|
+
is reported at its configured landing path. A file you write without declaring is
|
|
920
|
+
not saved. **A program that fails still saves what it wrote.** Saved files have not
|
|
921
|
+
been read back; do not claim to know their contents from this result."""
|
|
922
|
+
|
|
923
|
+
_DESCRIPTION_IN_PLACE_WITHHELD_LOCATED = """Naming a file in both ``files`` and ``outputs``
|
|
924
|
+
edits the supplied copy; the saved copy goes to the sink's configured location.
|
|
925
|
+
The supplied copy is already present, so it cannot be reported as not written.
|
|
926
|
+
A failed program still saves whatever it had written by then."""
|
|
927
|
+
|
|
938
928
|
#: The withholding pair again, for a host whose sink lands each call under a folder of its own.
|
|
939
929
|
#: Two promises above stop being true: nothing names which files landed, so nothing reports a
|
|
940
930
|
#: name declared and not written either. It names the folder without promising a reader for it,
|
|
@@ -966,6 +956,7 @@ def _tool_description(
|
|
|
966
956
|
egress_allow: Sequence[str | EgressRule] = (),
|
|
967
957
|
withhold: bool,
|
|
968
958
|
lands_per_call: bool = False,
|
|
959
|
+
locates_outputs: bool = False,
|
|
969
960
|
runtime: CodeactRuntime | None = None,
|
|
970
961
|
) -> str:
|
|
971
962
|
"""The description the model reads, for the channels this host actually wired.
|
|
@@ -992,7 +983,19 @@ def _tool_description(
|
|
|
992
983
|
"*this* call is in it.",
|
|
993
984
|
"Use only the runtime facilities described below.",
|
|
994
985
|
)
|
|
986
|
+
else:
|
|
987
|
+
head = head.replace("as ``python3 program.py``", "with the ``python-portable-v1`` profile")
|
|
988
|
+
head = head.replace(
|
|
989
|
+
"Each call gets a fresh working\n directory: nothing you did not pass in to "
|
|
990
|
+
"*this* call is in it.",
|
|
991
|
+
"Do not rely on state from earlier calls.",
|
|
992
|
+
)
|
|
995
993
|
body = [head.format(network=network)]
|
|
994
|
+
if runtime is None:
|
|
995
|
+
body.append(
|
|
996
|
+
"Python 3.11 or newer is available with ``json``, ``math``, ``re``, ``sys`` and "
|
|
997
|
+
"``types``. Other standard-library modules and third-party packages are not promised."
|
|
998
|
+
)
|
|
996
999
|
if runtime is not None:
|
|
997
1000
|
if runtime.guest_work_dir is not None:
|
|
998
1001
|
body.append(
|
|
@@ -1027,8 +1030,18 @@ def _tool_description(
|
|
|
1027
1030
|
body.append(_DESCRIPTION_DECLARED_WITHHELD_PER_CALL)
|
|
1028
1031
|
in_place = _DESCRIPTION_IN_PLACE_WITHHELD_PER_CALL
|
|
1029
1032
|
else:
|
|
1030
|
-
body.append(
|
|
1031
|
-
|
|
1033
|
+
body.append(
|
|
1034
|
+
_DESCRIPTION_DECLARED_WITHHELD_LOCATED
|
|
1035
|
+
if locates_outputs
|
|
1036
|
+
else _DESCRIPTION_DECLARED_WITHHELD
|
|
1037
|
+
)
|
|
1038
|
+
in_place = (
|
|
1039
|
+
_DESCRIPTION_IN_PLACE_WITHHELD_LOCATED
|
|
1040
|
+
if locates_outputs
|
|
1041
|
+
else _DESCRIPTION_IN_PLACE_WITHHELD
|
|
1042
|
+
)
|
|
1043
|
+
if withhold and lands_per_call and locates_outputs:
|
|
1044
|
+
in_place = _DESCRIPTION_IN_PLACE_WITHHELD_LOCATED
|
|
1032
1045
|
if takes_files:
|
|
1033
1046
|
body.append(in_place)
|
|
1034
1047
|
arguments.append(_DESCRIPTION_ARG_OUTPUTS)
|
|
@@ -1083,14 +1096,9 @@ def _tool_description(
|
|
|
1083
1096
|
|
|
1084
1097
|
@dataclass(frozen=True)
|
|
1085
1098
|
class _HostToolCall:
|
|
1086
|
-
"""
|
|
1087
|
-
|
|
1088
|
-
Generated once and written into every run, which the registry's sealing is what makes
|
|
1089
|
-
honest: the names it spells cannot change after the factory has read them.
|
|
1090
|
-
"""
|
|
1099
|
+
"""The sealed registry from which each program receives a fresh policy run."""
|
|
1091
1100
|
|
|
1092
1101
|
registry: HostToolRegistry
|
|
1093
|
-
shim: str
|
|
1094
1102
|
|
|
1095
1103
|
|
|
1096
1104
|
def _execute_code_tool(
|
|
@@ -1167,6 +1175,9 @@ def _execute_code_tool(
|
|
|
1167
1175
|
egress_allow=session.spec.egress_allow,
|
|
1168
1176
|
withhold=withhold,
|
|
1169
1177
|
lands_per_call=lands_per_call,
|
|
1178
|
+
locates_outputs=(
|
|
1179
|
+
session.output_sink is not None and session.output_sink.locate is not OutputSink.locate
|
|
1180
|
+
),
|
|
1170
1181
|
runtime=runtime,
|
|
1171
1182
|
)
|
|
1172
1183
|
return body
|
|
@@ -1227,9 +1238,7 @@ async def _execute(
|
|
|
1227
1238
|
# writes. One sentence for both would be false about one of them.
|
|
1228
1239
|
reserved: dict[str, str] = {}
|
|
1229
1240
|
if host_tool_call is None and runtime is None:
|
|
1230
|
-
reserved[_PROGRAM_FILENAME] =
|
|
1231
|
-
"this tool writes a file of that name into every call's directory"
|
|
1232
|
-
)
|
|
1241
|
+
reserved[_PROGRAM_FILENAME] = "this name is reserved for program execution"
|
|
1233
1242
|
if outputs is CodeactOutputs.MANIFEST:
|
|
1234
1243
|
reserved[_MANIFEST_FILENAME] = (
|
|
1235
1244
|
"this tool reads a file of that name from every call's directory as its manifest"
|
|
@@ -1248,12 +1257,21 @@ async def _execute(
|
|
|
1248
1257
|
# from, and what is collected out — so the three cannot disagree about one call's layout.
|
|
1249
1258
|
# It is longer when calling a host tool, which is why the name checks below take it rather than
|
|
1250
1259
|
# `call_id`: five more bytes of the 255 a guest path gets, spent before the name is.
|
|
1251
|
-
guest_prefix =
|
|
1260
|
+
guest_prefix = call_id
|
|
1252
1261
|
if runtime is not None and not runtime.use_call_directory:
|
|
1253
1262
|
guest_prefix = ""
|
|
1254
1263
|
|
|
1255
1264
|
names: list[str] = []
|
|
1256
1265
|
if outputs is CodeactOutputs.DECLARED:
|
|
1266
|
+
if runtime is None:
|
|
1267
|
+
prospective = session.prospective_program_channel(key)
|
|
1268
|
+
if prospective is not None:
|
|
1269
|
+
guest_prefix = _channel_output_prefix(
|
|
1270
|
+
call_directory,
|
|
1271
|
+
prospective.guest_working_directory(
|
|
1272
|
+
call_directory, host_tools=host_tool_call is not None
|
|
1273
|
+
),
|
|
1274
|
+
)
|
|
1257
1275
|
checked = _validated_output_names(
|
|
1258
1276
|
declared,
|
|
1259
1277
|
max_files=session.spec.files_out.max_files,
|
|
@@ -1268,35 +1286,26 @@ async def _execute(
|
|
|
1268
1286
|
return _stopped("Error: the outputs argument is invalid.", detail=checked)
|
|
1269
1287
|
names = checked
|
|
1270
1288
|
|
|
1271
|
-
# Cap before acquiring anything, and cap *as we go*: a bound that answers only once
|
|
1272
|
-
# everything is in memory has already spent what it exists to bound. Every check below
|
|
1273
|
-
# therefore happens before the read it would have prevented — the count before the listing,
|
|
1274
|
-
# the program's own bytes before the store is touched at all, and each file's as it arrives.
|
|
1275
|
-
# The tally covers the submitted program, an exec shim where wired, and each shared file.
|
|
1276
|
-
# The transport's launcher and responses have their own limits.
|
|
1277
1289
|
limits = session.spec.files_in
|
|
1278
1290
|
tally = _InboundTally(limits)
|
|
1279
1291
|
shared: list[tuple[str, str, str]] = []
|
|
1280
|
-
inbound = len(files) + (0 if runtime is not None else 2 if host_tool_call is not None else 1)
|
|
1281
1292
|
program = code
|
|
1282
1293
|
if runtime is not None:
|
|
1283
1294
|
refusal = _InboundTally(limits).add("", code, named="the program", program=True)
|
|
1284
1295
|
if refusal is not None:
|
|
1285
1296
|
return _stopped("Error: the program could not be staged.", detail=refusal)
|
|
1286
1297
|
program = runtime_program(runtime, code, call_directory)
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
)
|
|
1298
|
-
if over_cap is None and host_tool_call is not None:
|
|
1299
|
-
over_cap = tally.add(SHIM_MODULE, host_tool_call.shim)
|
|
1298
|
+
refusal = tally.add("", program, named="the program", program=True)
|
|
1299
|
+
if refusal is not None:
|
|
1300
|
+
return _stopped("Error: the program could not be staged.", detail=refusal)
|
|
1301
|
+
else:
|
|
1302
|
+
try:
|
|
1303
|
+
program_bytes = len(code.encode("utf-8"))
|
|
1304
|
+
except UnicodeEncodeError:
|
|
1305
|
+
return _stopped("Error: the program is not valid UTF-8.")
|
|
1306
|
+
if session.spec.program is None or program_bytes > session.spec.program.max_program_bytes:
|
|
1307
|
+
return _stopped("Error: the program exceeds its declared byte limit.")
|
|
1308
|
+
over_cap = _over_file_count(len(files), limits, calls_host_tool=False, program_is_file=False)
|
|
1300
1309
|
if over_cap is not None:
|
|
1301
1310
|
return _stopped("Error: the call's inputs could not be staged.", detail=over_cap)
|
|
1302
1311
|
if store is not None:
|
|
@@ -1317,18 +1326,28 @@ async def _execute(
|
|
|
1317
1326
|
if isinstance(sandbox, str):
|
|
1318
1327
|
return _stopped("Error: the sandbox could not be acquired.", detail=sandbox)
|
|
1319
1328
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
# than defaulted, so this kind's constants and the transport's cannot drift apart.
|
|
1326
|
-
layout = (
|
|
1327
|
-
guest_run_layout(call_directory, program=_PROGRAM_FILENAME)
|
|
1328
|
-
if host_tool_call is not None
|
|
1329
|
-
else None
|
|
1329
|
+
channel = session.program_channel(key, sandbox) if runtime is None else None
|
|
1330
|
+
shared_dir = (
|
|
1331
|
+
channel.guest_working_directory(call_directory, host_tools=host_tool_call is not None)
|
|
1332
|
+
if channel is not None
|
|
1333
|
+
else call_directory
|
|
1330
1334
|
)
|
|
1331
|
-
|
|
1335
|
+
if channel is not None:
|
|
1336
|
+
guest_prefix = _channel_output_prefix(call_directory, shared_dir)
|
|
1337
|
+
if runtime is None and outputs is CodeactOutputs.DECLARED:
|
|
1338
|
+
checked = _validated_output_names(
|
|
1339
|
+
declared,
|
|
1340
|
+
max_files=session.spec.files_out.max_files,
|
|
1341
|
+
reserved=reserved,
|
|
1342
|
+
guest_prefix=guest_prefix,
|
|
1343
|
+
normalization=_normalization(session),
|
|
1344
|
+
named_by=_OUTPUTS_ARGUMENT,
|
|
1345
|
+
argument=_OUTPUTS_ARGUMENT,
|
|
1346
|
+
candidates=rewritten,
|
|
1347
|
+
)
|
|
1348
|
+
if isinstance(checked, str):
|
|
1349
|
+
return _stopped("Error: the outputs argument is invalid.", detail=checked)
|
|
1350
|
+
names = checked
|
|
1332
1351
|
|
|
1333
1352
|
for name, named, content in shared:
|
|
1334
1353
|
refusal = await _write_shared(
|
|
@@ -1339,45 +1358,20 @@ async def _execute(
|
|
|
1339
1358
|
"Error: an input file could not be written into the sandbox.", detail=refusal
|
|
1340
1359
|
)
|
|
1341
1360
|
|
|
1342
|
-
program_path = layout.program if layout is not None else f"{call_directory}/{_PROGRAM_FILENAME}"
|
|
1343
1361
|
try:
|
|
1344
|
-
if runtime is None:
|
|
1345
|
-
await sandbox.write_file(
|
|
1346
|
-
posixpath.relpath(
|
|
1347
|
-
program_path, layout.directory if layout is not None else call_directory
|
|
1348
|
-
),
|
|
1349
|
-
code,
|
|
1350
|
-
working_directory=layout.directory if layout is not None else call_directory,
|
|
1351
|
-
)
|
|
1352
|
-
except Exception as exc: # noqa: BLE001
|
|
1353
|
-
logger.warning(
|
|
1354
|
-
"execute_code: could not write the program into the sandbox: %s", error_detail(exc)
|
|
1355
|
-
)
|
|
1356
|
-
return _stopped("Error: could not write the program into the sandbox")
|
|
1357
|
-
|
|
1358
|
-
try:
|
|
1359
|
-
# The two are built together above and are never one without the other; both are named
|
|
1360
|
-
# here so neither has to be narrowed from the other.
|
|
1361
1362
|
if runtime is not None:
|
|
1362
1363
|
result = await sandbox.run_code(program, timeout=timeout)
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
host_tool_call.shim,
|
|
1367
|
-
working_directory=layout.directory,
|
|
1368
|
-
)
|
|
1369
|
-
# A fresh run per call: the host-tool-call cap and the ledger bound one program.
|
|
1370
|
-
result = await host_tool_calls_over_exec(
|
|
1364
|
+
else:
|
|
1365
|
+
assert channel is not None and session.spec.program is not None
|
|
1366
|
+
result = await channel.run(
|
|
1371
1367
|
sandbox,
|
|
1372
|
-
|
|
1373
|
-
|
|
1368
|
+
code,
|
|
1369
|
+
requirements=session.spec.program,
|
|
1370
|
+
guest_call_path=call_directory,
|
|
1374
1371
|
timeout=timeout,
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
# An argv sequence, never a command line: the model's source never reaches a shell.
|
|
1379
|
-
result = await sandbox.exec(
|
|
1380
|
-
[_INTERPRETER, _PROGRAM_FILENAME], working_directory=call_directory, timeout=timeout
|
|
1372
|
+
policy=HostToolRun(host_tool_call.registry, logger=logger, key=key)
|
|
1373
|
+
if host_tool_call
|
|
1374
|
+
else None,
|
|
1381
1375
|
)
|
|
1382
1376
|
except SandboxQueuedTimeout:
|
|
1383
1377
|
logger.warning("execute_code: the deadline expired before the queued program started")
|
|
@@ -1740,6 +1734,13 @@ async def _write_shared(
|
|
|
1740
1734
|
# --- Files out -----------------------------------------------------------------------------
|
|
1741
1735
|
|
|
1742
1736
|
|
|
1737
|
+
def _channel_output_prefix(guest_call_path: str, guest_working_directory: str) -> str:
|
|
1738
|
+
call_id = guest_call_path.rsplit("/", 1)[-1]
|
|
1739
|
+
return posixpath.normpath(
|
|
1740
|
+
f"{call_id}/{posixpath.relpath(guest_working_directory, guest_call_path)}"
|
|
1741
|
+
)
|
|
1742
|
+
|
|
1743
|
+
|
|
1743
1744
|
def _normalization(session: SandboxToolSession) -> NameNormalization:
|
|
1744
1745
|
"""What the attached sink does to a name, which decides the spelling to judge."""
|
|
1745
1746
|
sink = session.output_sink
|
|
@@ -1877,7 +1878,11 @@ async def _collect(
|
|
|
1877
1878
|
DeclaredOutput(path=posixpath.join(guest_prefix, name), name=name, required=False)
|
|
1878
1879
|
for name in declared
|
|
1879
1880
|
)
|
|
1881
|
+
locations: dict[str, str] = {}
|
|
1882
|
+
delivered_names: list[str] | None = None
|
|
1880
1883
|
try:
|
|
1884
|
+
if withhold and not sink.per_call and sink.locate is not OutputSink.locate:
|
|
1885
|
+
sink, locations, delivered_names = _located_sink(sink, declared)
|
|
1881
1886
|
landed = await collect_outputs(
|
|
1882
1887
|
sandbox,
|
|
1883
1888
|
spec,
|
|
@@ -1911,9 +1916,40 @@ async def _collect(
|
|
|
1911
1916
|
named_by=_MANIFEST_FILENAME if outputs is CodeactOutputs.MANIFEST else _OUTPUTS_ARGUMENT,
|
|
1912
1917
|
argument=None if outputs is CodeactOutputs.MANIFEST else _OUTPUTS_ARGUMENT,
|
|
1913
1918
|
candidates=candidates,
|
|
1919
|
+
locations=locations,
|
|
1920
|
+
delivered_names=delivered_names,
|
|
1914
1921
|
)
|
|
1915
1922
|
|
|
1916
1923
|
|
|
1924
|
+
def _located_sink(
|
|
1925
|
+
sink: OutputSink, declared: Sequence[str]
|
|
1926
|
+
) -> tuple[OutputSink, dict[str, str], list[str]]:
|
|
1927
|
+
"""Calculate locations before reading guest bytes, then check each delivery receipt."""
|
|
1928
|
+
locations = {
|
|
1929
|
+
name: sink.locate(
|
|
1930
|
+
unicodedata.normalize("NFC", name)
|
|
1931
|
+
if sink.normalization is NameNormalization.NFC
|
|
1932
|
+
else name
|
|
1933
|
+
)
|
|
1934
|
+
for name in declared
|
|
1935
|
+
}
|
|
1936
|
+
originals = {unicodedata.normalize("NFC", name): name for name in declared}
|
|
1937
|
+
delivered_names: list[str] = []
|
|
1938
|
+
|
|
1939
|
+
async def deliver(artifact: Artifact) -> LandedArtifact:
|
|
1940
|
+
accepted = await sink.deliver(artifact)
|
|
1941
|
+
name = originals[unicodedata.normalize("NFC", artifact.name)]
|
|
1942
|
+
if locations[name] not in (accepted.name, accepted.handle):
|
|
1943
|
+
logger.warning(
|
|
1944
|
+
"execute_code: output sink locate disagrees with delivery; using declared name"
|
|
1945
|
+
)
|
|
1946
|
+
locations[name] = name
|
|
1947
|
+
delivered_names.append(artifact.name)
|
|
1948
|
+
return accepted
|
|
1949
|
+
|
|
1950
|
+
return replace(sink, deliver=deliver), locations, delivered_names
|
|
1951
|
+
|
|
1952
|
+
|
|
1917
1953
|
async def _read_manifest(
|
|
1918
1954
|
session: SandboxToolSession, sandbox: Sandbox, guest_prefix: str
|
|
1919
1955
|
) -> tuple[list[str], int] | str:
|
|
@@ -2000,6 +2036,8 @@ def _format_landed(
|
|
|
2000
2036
|
named_by: str = _OUTPUTS_ARGUMENT,
|
|
2001
2037
|
argument: str | None = None,
|
|
2002
2038
|
candidates: frozenset[str] | None = None,
|
|
2039
|
+
locations: Mapping[str, str] | None = None,
|
|
2040
|
+
delivered_names: Sequence[str] | None = None,
|
|
2003
2041
|
) -> str:
|
|
2004
2042
|
"""What the model is told about the files: what landed, and what is absent.
|
|
2005
2043
|
|
|
@@ -2015,14 +2053,20 @@ def _format_landed(
|
|
|
2015
2053
|
before it: without it a declared name equal to hidden content renders as a position, and a
|
|
2016
2054
|
caller watching which way its own spelling comes back learns that the guess was right.
|
|
2017
2055
|
|
|
2018
|
-
``withhold`` drops ``display`` in favour of the name
|
|
2056
|
+
``withhold`` drops ``display`` in favour of the declared name or its checked location. The sink
|
|
2019
2057
|
composes ``display`` from an :class:`~maf_sandbox.Artifact` whose ``content`` is the guest's
|
|
2020
2058
|
bytes, and nothing in the protocol requires the two to be independent — so a sink that puts
|
|
2021
2059
|
any of that content in the string would be putting guest-authored text back into a result
|
|
2022
|
-
rendered to hold none.
|
|
2023
|
-
|
|
2060
|
+
rendered to hold none. Locations are calculated from validated declarations before delivery;
|
|
2061
|
+
substituted declarations still render only as argument positions.
|
|
2024
2062
|
"""
|
|
2025
|
-
delivered = {
|
|
2063
|
+
delivered = {
|
|
2064
|
+
unicodedata.normalize("NFC", name)
|
|
2065
|
+
for name in (
|
|
2066
|
+
delivered_names if delivered_names is not None else [item.name for item in landed]
|
|
2067
|
+
)
|
|
2068
|
+
}
|
|
2069
|
+
locations = locations or {}
|
|
2026
2070
|
lines: list[str] = []
|
|
2027
2071
|
# Answered once, above the two renderings that need it: a name that landed is reported as
|
|
2028
2072
|
# surely as one that did not, and a name the framework substituted must not be repeated
|
|
@@ -2039,10 +2083,11 @@ def _format_landed(
|
|
|
2039
2083
|
if withhold:
|
|
2040
2084
|
for position, name in enumerate(declared):
|
|
2041
2085
|
if unicodedata.normalize("NFC", name) in delivered:
|
|
2042
|
-
#
|
|
2043
|
-
# files, not a refusal, so a name of the model's own is what it wants back.
|
|
2086
|
+
# Only model-authored declarations may reveal their location.
|
|
2044
2087
|
lines.append(
|
|
2045
|
-
f"- {named_by}[{position}]"
|
|
2088
|
+
f"- {named_by}[{position}]"
|
|
2089
|
+
if position in rewritten
|
|
2090
|
+
else f"- {locations.get(name, name)}"
|
|
2046
2091
|
)
|
|
2047
2092
|
else:
|
|
2048
2093
|
for item in landed:
|
|
@@ -2058,7 +2103,11 @@ def _format_landed(
|
|
|
2058
2103
|
# spelling where it is the caller's, and the position where the framework put something
|
|
2059
2104
|
# else there. It is the one line here that names a file which does not exist.
|
|
2060
2105
|
missing = [
|
|
2061
|
-
echoed_name(
|
|
2106
|
+
echoed_name(
|
|
2107
|
+
locations.get(name, name),
|
|
2108
|
+
at=f"{named_by}[{position}]",
|
|
2109
|
+
hidden=position in rewritten,
|
|
2110
|
+
)
|
|
2062
2111
|
for position, name in enumerate(declared)
|
|
2063
2112
|
if unicodedata.normalize("NFC", name) not in delivered
|
|
2064
2113
|
]
|
|
File without changes
|
{maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/src/maf_sandbox_codeact/__init__.py
RENAMED
|
File without changes
|
{maf_sandbox_codeact-0.21.4 → maf_sandbox_codeact-0.22.0}/src/maf_sandbox_codeact/_runtime.py
RENAMED
|
File without changes
|
|
File without changes
|