agent-orchestration-process 0.2.1__tar.gz → 0.2.2__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.
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/PKG-INFO +1 -1
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/pyproject.toml +1 -1
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/pyproject.toml.orig +1 -1
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/cli.py +61 -2
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/models.py +13 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/provider_versions.py +6 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/runner.py +198 -34
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/LICENSE +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/README.md +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/__init__.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/__main__.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/batch.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/codex_auth.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/codex_routes.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/dsh_runner.mjs +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/egress.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/egress_bridge.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/integration.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/isolation.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/locks.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/migration.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/model_catalog.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/model_listing.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/pricing.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/prompt_transport.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/worktrees.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/zcode.py +0 -0
- {agent_orchestration_process-0.2.1 → agent_orchestration_process-0.2.2}/src/agent_orchestration_process/zcode_protocol.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-orchestration-process
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.2
|
|
4
4
|
Summary: Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance
|
|
5
5
|
Keywords: agent-orchestration,ai-agents,autoresearch,coding-agents,git-worktrees
|
|
6
6
|
Author: AOP contributors
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-orchestration-process"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -4,7 +4,7 @@ build-backend = "uv_build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "agent-orchestration-process"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Run bounded, non-interactive jobs across agent harnesses with isolation, exact resume, and provenance"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -23,7 +23,13 @@ from .locks import exclusive_lock, task_lock_path
|
|
|
23
23
|
from .model_catalog import ModelCatalog, ensure_catalog_fresh
|
|
24
24
|
from .model_listing import AGENTS, AvailableModel, list_models
|
|
25
25
|
from .models import PromptPart, RunRequest, RunResult
|
|
26
|
-
from .runner import
|
|
26
|
+
from .runner import (
|
|
27
|
+
AgentRunner,
|
|
28
|
+
RunStore,
|
|
29
|
+
adapter_for,
|
|
30
|
+
plan_inputs,
|
|
31
|
+
provider_runtime_record,
|
|
32
|
+
)
|
|
27
33
|
from .migration import migrate_runs, migration_roots, print_counts
|
|
28
34
|
from .worktrees import (
|
|
29
35
|
AOPError,
|
|
@@ -396,6 +402,25 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
396
402
|
input_names=tuple(Path(path).name for path in args.input_paths),
|
|
397
403
|
).to_dict()
|
|
398
404
|
preview["provider"] = args.agent
|
|
405
|
+
planned = plan_inputs(
|
|
406
|
+
args.input_paths,
|
|
407
|
+
excludes=args.input_excludes,
|
|
408
|
+
skip_external_links=args.skip_external_links,
|
|
409
|
+
protected=(manager.state_dir, manager.sealed_runtime_dir),
|
|
410
|
+
)
|
|
411
|
+
preview["inputs"]["planned"] = [
|
|
412
|
+
{
|
|
413
|
+
"name": plan.alias,
|
|
414
|
+
"source": os.fspath(plan.source),
|
|
415
|
+
"files": len(plan.entries) if plan.entries else 1,
|
|
416
|
+
"size_bytes": sum(path.stat().st_size for _, path in plan.entries)
|
|
417
|
+
if plan.source.is_dir()
|
|
418
|
+
else plan.source.stat().st_size,
|
|
419
|
+
"excluded": list(plan.excluded),
|
|
420
|
+
"links": list(plan.links),
|
|
421
|
+
}
|
|
422
|
+
for plan in planned
|
|
423
|
+
]
|
|
399
424
|
preview["provider_runtime"] = provider_runtime_record(
|
|
400
425
|
adapter_for(args.agent)
|
|
401
426
|
)
|
|
@@ -424,6 +449,8 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
424
449
|
timeout_seconds=args.timeout,
|
|
425
450
|
artifacts=args.artifact,
|
|
426
451
|
input_paths=args.input_paths,
|
|
452
|
+
input_excludes=args.input_excludes,
|
|
453
|
+
skip_external_links=args.skip_external_links,
|
|
427
454
|
system_prompt=_read_system_prompt(args),
|
|
428
455
|
tools=args.tools,
|
|
429
456
|
provider_base_url=args.provider_base_url,
|
|
@@ -465,7 +492,11 @@ def main(argv: Sequence[str] | None = None) -> int:
|
|
|
465
492
|
print(f"aop: {error}", file=sys.stderr)
|
|
466
493
|
return 2
|
|
467
494
|
except FileNotFoundError as error:
|
|
468
|
-
print(
|
|
495
|
+
print(
|
|
496
|
+
f"aop: command not found: {error.filename} "
|
|
497
|
+
f"(PATH={os.environ.get('PATH', '')})",
|
|
498
|
+
file=sys.stderr,
|
|
499
|
+
)
|
|
469
500
|
return 127
|
|
470
501
|
except KeyboardInterrupt:
|
|
471
502
|
print("aop: interrupted", file=sys.stderr)
|
|
@@ -514,6 +545,25 @@ def _add_input_arguments(
|
|
|
514
545
|
metavar="PATH",
|
|
515
546
|
help=help_text,
|
|
516
547
|
)
|
|
548
|
+
if resume:
|
|
549
|
+
return
|
|
550
|
+
parser.add_argument(
|
|
551
|
+
"--input-exclude",
|
|
552
|
+
action="append",
|
|
553
|
+
default=[],
|
|
554
|
+
dest="input_excludes",
|
|
555
|
+
metavar="GLOB",
|
|
556
|
+
help="leave matching paths out of every input directory; a pattern "
|
|
557
|
+
"without '/' matches names at any depth, one with '/' matches paths "
|
|
558
|
+
"from the input's root; repeat for more",
|
|
559
|
+
)
|
|
560
|
+
parser.add_argument(
|
|
561
|
+
"--skip-external-links",
|
|
562
|
+
action="store_true",
|
|
563
|
+
help="leave out symlinks that point outside their input instead of "
|
|
564
|
+
"failing; links that stay inside an input are always copied as their "
|
|
565
|
+
"targets",
|
|
566
|
+
)
|
|
517
567
|
|
|
518
568
|
|
|
519
569
|
def _tool_list(value: str) -> tuple[str, ...]:
|
|
@@ -606,6 +656,15 @@ def _report_policy(
|
|
|
606
656
|
f"{prefix}inputs: {inputs['mode']} ({len(inputs.get('names', []))} declared)",
|
|
607
657
|
file=stream,
|
|
608
658
|
)
|
|
659
|
+
for item in inputs.get("planned", []):
|
|
660
|
+
skipped = sum(link["action"] == "skipped" for link in item["links"])
|
|
661
|
+
print(
|
|
662
|
+
f"{prefix} {item['name']}: {item['files']} files, "
|
|
663
|
+
f"{item['size_bytes'] / 1e6:.1f} MB; {len(item['excluded'])} paths "
|
|
664
|
+
f"excluded; {len(item['links']) - skipped} symlinks followed, "
|
|
665
|
+
f"{skipped} skipped",
|
|
666
|
+
file=stream,
|
|
667
|
+
)
|
|
609
668
|
print(
|
|
610
669
|
f"{prefix}inherited local instructions: {instructions['inherited_local']}; "
|
|
611
670
|
f"provider built-in prompt: {instructions['provider_builtin']}; "
|
|
@@ -28,11 +28,19 @@ class InputSnapshot:
|
|
|
28
28
|
size_bytes: int
|
|
29
29
|
sha256: str
|
|
30
30
|
files: tuple[InputFile, ...]
|
|
31
|
+
# Paths left out of the snapshot, relative to the input: matches of the
|
|
32
|
+
# run's input excludes and AOP's own state directories.
|
|
33
|
+
excluded: tuple[str, ...] = ()
|
|
34
|
+
# Symlinks found in the input: {"path", "target", "action"}, where action
|
|
35
|
+
# is "followed" for a target inside the input and "skipped" otherwise.
|
|
36
|
+
links: tuple[dict[str, str], ...] = ()
|
|
31
37
|
|
|
32
38
|
@classmethod
|
|
33
39
|
def from_dict(cls, value: dict[str, Any]) -> InputSnapshot:
|
|
34
40
|
fields = dict(value)
|
|
35
41
|
fields["files"] = tuple(InputFile(**item) for item in fields.get("files", ()))
|
|
42
|
+
fields["excluded"] = tuple(fields.get("excluded", ()))
|
|
43
|
+
fields["links"] = tuple(fields.get("links", ()))
|
|
36
44
|
return cls(**fields)
|
|
37
45
|
|
|
38
46
|
|
|
@@ -102,6 +110,10 @@ class RunRequest:
|
|
|
102
110
|
# The prompt split into content blocks, in order; their lengths add up to
|
|
103
111
|
# len(prompt). None sends the prompt as one block.
|
|
104
112
|
prompt_parts: tuple[PromptPart, ...] | None = None
|
|
113
|
+
# Glob patterns left out of every input directory, and whether symlinks
|
|
114
|
+
# that leave their input are skipped instead of failing the run.
|
|
115
|
+
input_excludes: tuple[str, ...] = ()
|
|
116
|
+
skip_external_links: bool = False
|
|
105
117
|
# How `aop migrate-results` converted an older record, if it did.
|
|
106
118
|
migration: dict[str, Any] | None = None
|
|
107
119
|
|
|
@@ -118,6 +130,7 @@ class RunRequest:
|
|
|
118
130
|
if fields.get("tools") is not None:
|
|
119
131
|
fields["tools"] = tuple(fields["tools"])
|
|
120
132
|
fields["allowed_urls"] = tuple(fields.get("allowed_urls", ()))
|
|
133
|
+
fields["input_excludes"] = tuple(fields.get("input_excludes", ()))
|
|
121
134
|
if fields.get("prompt_parts") is not None:
|
|
122
135
|
fields["prompt_parts"] = tuple(
|
|
123
136
|
PromptPart(**part) for part in fields["prompt_parts"]
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import re
|
|
6
7
|
import subprocess
|
|
7
8
|
|
|
@@ -24,6 +25,11 @@ def require_supported_agy(binary: str) -> str:
|
|
|
24
25
|
capture_output=True,
|
|
25
26
|
timeout=5,
|
|
26
27
|
)
|
|
28
|
+
except FileNotFoundError as error:
|
|
29
|
+
raise AOPError(
|
|
30
|
+
f"{binary} not found on PATH ({os.environ.get('PATH', '')}); set "
|
|
31
|
+
"AOP_AGY_BIN to its absolute path"
|
|
32
|
+
) from error
|
|
27
33
|
except OSError as error:
|
|
28
34
|
raise AOPError(f"could not query Agy version: {error}") from error
|
|
29
35
|
except subprocess.TimeoutExpired as error:
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
|
|
5
5
|
import ast
|
|
6
6
|
import contextlib
|
|
7
|
+
import fnmatch
|
|
7
8
|
import hashlib
|
|
8
9
|
import json
|
|
9
10
|
import os
|
|
@@ -1075,7 +1076,10 @@ def _capture_process(
|
|
|
1075
1076
|
prompt: str | None,
|
|
1076
1077
|
timeout_seconds: float | None,
|
|
1077
1078
|
is_response: Callable[[str], bool],
|
|
1079
|
+
live_dir: Path | None = None,
|
|
1078
1080
|
) -> _ProcessCapture:
|
|
1081
|
+
"""Run a harness to completion; with `live_dir`, its stdout and stderr also
|
|
1082
|
+
grow in live_dir/events.jsonl and live_dir/stderr.log while it runs."""
|
|
1079
1083
|
started = time.monotonic()
|
|
1080
1084
|
timed_out = False
|
|
1081
1085
|
first_event_seconds = None
|
|
@@ -1106,6 +1110,11 @@ def _capture_process(
|
|
|
1106
1110
|
|
|
1107
1111
|
stdout_parts: list[str] = []
|
|
1108
1112
|
stderr_parts: list[str] = []
|
|
1113
|
+
live = contextlib.ExitStack()
|
|
1114
|
+
live_stdout = live_stderr = None
|
|
1115
|
+
if live_dir is not None:
|
|
1116
|
+
live_stdout = live.enter_context((live_dir / "events.jsonl").open("w"))
|
|
1117
|
+
live_stderr = live.enter_context((live_dir / "stderr.log").open("w"))
|
|
1109
1118
|
|
|
1110
1119
|
def read_stdout() -> None:
|
|
1111
1120
|
nonlocal first_event_seconds, first_response_seconds
|
|
@@ -1117,10 +1126,17 @@ def _capture_process(
|
|
|
1117
1126
|
if first_response_seconds is None and is_response(line):
|
|
1118
1127
|
first_response_seconds = elapsed
|
|
1119
1128
|
stdout_parts.append(line)
|
|
1129
|
+
if live_stdout is not None:
|
|
1130
|
+
live_stdout.write(line)
|
|
1131
|
+
live_stdout.flush()
|
|
1120
1132
|
|
|
1121
1133
|
def read_stderr() -> None:
|
|
1122
1134
|
assert process.stderr is not None
|
|
1123
|
-
|
|
1135
|
+
for line in process.stderr:
|
|
1136
|
+
stderr_parts.append(line)
|
|
1137
|
+
if live_stderr is not None:
|
|
1138
|
+
live_stderr.write(line)
|
|
1139
|
+
live_stderr.flush()
|
|
1124
1140
|
|
|
1125
1141
|
stdout_reader = threading.Thread(target=read_stdout, daemon=True)
|
|
1126
1142
|
stderr_reader = threading.Thread(target=read_stderr, daemon=True)
|
|
@@ -1150,6 +1166,8 @@ def _capture_process(
|
|
|
1150
1166
|
_close_finished_streams(
|
|
1151
1167
|
(stdout_reader, process.stdout), (stderr_reader, process.stderr)
|
|
1152
1168
|
)
|
|
1169
|
+
if not (stdout_reader.is_alive() or stderr_reader.is_alive()):
|
|
1170
|
+
live.close()
|
|
1153
1171
|
return _ProcessCapture(
|
|
1154
1172
|
stdout="".join(stdout_parts),
|
|
1155
1173
|
stderr="".join(stderr_parts),
|
|
@@ -1229,6 +1247,7 @@ class ClaudeAdapter:
|
|
|
1229
1247
|
),
|
|
1230
1248
|
timeout_seconds=request.timeout_seconds,
|
|
1231
1249
|
is_response=self._is_response,
|
|
1250
|
+
live_dir=run_dir,
|
|
1232
1251
|
)
|
|
1233
1252
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
1234
1253
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -1868,6 +1887,7 @@ class GrokAdapter:
|
|
|
1868
1887
|
prompt=None,
|
|
1869
1888
|
timeout_seconds=request.timeout_seconds,
|
|
1870
1889
|
is_response=self._is_response,
|
|
1890
|
+
live_dir=run_dir,
|
|
1871
1891
|
)
|
|
1872
1892
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
1873
1893
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -2201,6 +2221,7 @@ class CursorAdapter:
|
|
|
2201
2221
|
prompt=prompt_transport.stdin_prompt(self.provider, request.prompt),
|
|
2202
2222
|
timeout_seconds=request.timeout_seconds,
|
|
2203
2223
|
is_response=self._is_response,
|
|
2224
|
+
live_dir=run_dir,
|
|
2204
2225
|
)
|
|
2205
2226
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
2206
2227
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -2419,6 +2440,7 @@ class DevinAdapter:
|
|
|
2419
2440
|
prompt=None,
|
|
2420
2441
|
timeout_seconds=request.timeout_seconds,
|
|
2421
2442
|
is_response=lambda line: bool(line.strip()),
|
|
2443
|
+
live_dir=run_dir,
|
|
2422
2444
|
)
|
|
2423
2445
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
2424
2446
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -2680,6 +2702,7 @@ class OpenCodeAdapter:
|
|
|
2680
2702
|
prompt=prompt_transport.stdin_prompt(self.provider, request.prompt),
|
|
2681
2703
|
timeout_seconds=request.timeout_seconds,
|
|
2682
2704
|
is_response=self._is_response,
|
|
2705
|
+
live_dir=run_dir,
|
|
2683
2706
|
)
|
|
2684
2707
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
2685
2708
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -3040,6 +3063,7 @@ class AgyAdapter:
|
|
|
3040
3063
|
prompt=prompt_transport.stdin_prompt(self.provider, request.prompt),
|
|
3041
3064
|
timeout_seconds=request.timeout_seconds,
|
|
3042
3065
|
is_response=self._is_response,
|
|
3066
|
+
live_dir=run_dir,
|
|
3043
3067
|
)
|
|
3044
3068
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
3045
3069
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -3324,6 +3348,7 @@ class HermesAdapter:
|
|
|
3324
3348
|
prompt=None,
|
|
3325
3349
|
timeout_seconds=request.timeout_seconds,
|
|
3326
3350
|
is_response=lambda line: bool(line.strip()),
|
|
3351
|
+
live_dir=run_dir,
|
|
3327
3352
|
)
|
|
3328
3353
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
3329
3354
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -3772,6 +3797,7 @@ class DeepSeekHarnessAdapter:
|
|
|
3772
3797
|
prompt=None,
|
|
3773
3798
|
timeout_seconds=request.timeout_seconds,
|
|
3774
3799
|
is_response=self._is_response,
|
|
3800
|
+
live_dir=run_dir,
|
|
3775
3801
|
)
|
|
3776
3802
|
_atomic_write(run_dir / "events.jsonl", capture.stdout)
|
|
3777
3803
|
_atomic_write(run_dir / "stderr.log", capture.stderr)
|
|
@@ -5786,9 +5812,19 @@ class AgentRunner:
|
|
|
5786
5812
|
writable_workspace: bool = False,
|
|
5787
5813
|
thinking_display: str | None = None,
|
|
5788
5814
|
prompt_parts: Sequence[PromptPart] | None = None,
|
|
5815
|
+
input_excludes: Sequence[str] = (),
|
|
5816
|
+
skip_external_links: bool = False,
|
|
5789
5817
|
) -> RunResult:
|
|
5790
5818
|
self._validate_mode(mode)
|
|
5791
5819
|
prompt_parts = self._validated_prompt_parts(prompt, prompt_parts)
|
|
5820
|
+
input_excludes = tuple(input_excludes)
|
|
5821
|
+
# Every input problem fails here, before any state is created.
|
|
5822
|
+
plan_inputs(
|
|
5823
|
+
input_paths,
|
|
5824
|
+
excludes=input_excludes,
|
|
5825
|
+
skip_external_links=skip_external_links,
|
|
5826
|
+
protected=self._state_roots(),
|
|
5827
|
+
)
|
|
5792
5828
|
if writable_workspace and profile != "sealed":
|
|
5793
5829
|
raise AOPError("--writable-workspace applies only to the sealed profile")
|
|
5794
5830
|
if thinking_display is not None:
|
|
@@ -5835,6 +5871,8 @@ class AgentRunner:
|
|
|
5835
5871
|
writable_workspace=writable_workspace,
|
|
5836
5872
|
thinking_display=thinking_display,
|
|
5837
5873
|
prompt_parts=prompt_parts,
|
|
5874
|
+
input_excludes=input_excludes,
|
|
5875
|
+
skip_external_links=skip_external_links,
|
|
5838
5876
|
)
|
|
5839
5877
|
worktree = (
|
|
5840
5878
|
self._create_sealed_workspace(request.run_id)
|
|
@@ -5968,6 +6006,8 @@ class AgentRunner:
|
|
|
5968
6006
|
writable_workspace=parent_request.writable_workspace,
|
|
5969
6007
|
thinking_display=parent_request.thinking_display,
|
|
5970
6008
|
prompt_parts=prompt_parts,
|
|
6009
|
+
input_excludes=parent_request.input_excludes,
|
|
6010
|
+
skip_external_links=parent_request.skip_external_links,
|
|
5971
6011
|
)
|
|
5972
6012
|
return self._execute(
|
|
5973
6013
|
request,
|
|
@@ -6084,7 +6124,12 @@ class AgentRunner:
|
|
|
6084
6124
|
cache_dir = self.manager.cache_dir
|
|
6085
6125
|
cache_dir.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
6086
6126
|
declared_inputs = _prepare_inputs(
|
|
6087
|
-
|
|
6127
|
+
plan_inputs(
|
|
6128
|
+
input_paths,
|
|
6129
|
+
excludes=request.input_excludes,
|
|
6130
|
+
skip_external_links=request.skip_external_links,
|
|
6131
|
+
protected=self._state_roots() if input_provenance is None else (),
|
|
6132
|
+
),
|
|
6088
6133
|
input_dir,
|
|
6089
6134
|
guest_paths=request.profile != "host",
|
|
6090
6135
|
provenance=input_provenance,
|
|
@@ -6325,6 +6370,8 @@ class AgentRunner:
|
|
|
6325
6370
|
writable_workspace: bool = False,
|
|
6326
6371
|
thinking_display: str | None = None,
|
|
6327
6372
|
prompt_parts: tuple[PromptPart, ...] | None = None,
|
|
6373
|
+
input_excludes: tuple[str, ...] = (),
|
|
6374
|
+
skip_external_links: bool = False,
|
|
6328
6375
|
) -> RunRequest:
|
|
6329
6376
|
return RunRequest(
|
|
6330
6377
|
run_id=str(uuid.uuid4()),
|
|
@@ -6353,8 +6400,14 @@ class AgentRunner:
|
|
|
6353
6400
|
writable_workspace=writable_workspace,
|
|
6354
6401
|
thinking_display=thinking_display,
|
|
6355
6402
|
prompt_parts=prompt_parts,
|
|
6403
|
+
input_excludes=input_excludes,
|
|
6404
|
+
skip_external_links=skip_external_links,
|
|
6356
6405
|
)
|
|
6357
6406
|
|
|
6407
|
+
def _state_roots(self) -> tuple[Path, ...]:
|
|
6408
|
+
"""AOP's own state, which no input snapshot may copy into itself."""
|
|
6409
|
+
return (self.manager.state_dir, self.manager.sealed_runtime_dir)
|
|
6410
|
+
|
|
6358
6411
|
def _validate_provider_base_url(self, url: str | None, profile: str) -> None:
|
|
6359
6412
|
variable = PROVIDER_BASE_URL_VARIABLES.get(self.adapter.provider)
|
|
6360
6413
|
if url is None:
|
|
@@ -6617,18 +6670,39 @@ def normalize_artifacts(artifacts: Sequence[str]) -> tuple[str, ...]:
|
|
|
6617
6670
|
return tuple(normalized)
|
|
6618
6671
|
|
|
6619
6672
|
|
|
6620
|
-
|
|
6673
|
+
@dataclass(frozen=True)
|
|
6674
|
+
class InputPlan:
|
|
6675
|
+
"""One declared input, checked and resolved before anything is copied."""
|
|
6676
|
+
|
|
6677
|
+
source: Path
|
|
6678
|
+
alias: str
|
|
6679
|
+
# (path relative to the input, host file to copy); empty for a file input.
|
|
6680
|
+
entries: tuple[tuple[str, Path], ...]
|
|
6681
|
+
excluded: tuple[str, ...]
|
|
6682
|
+
links: tuple[dict[str, str], ...]
|
|
6683
|
+
|
|
6684
|
+
|
|
6685
|
+
def plan_inputs(
|
|
6621
6686
|
values: Sequence[str | os.PathLike[str]],
|
|
6622
|
-
input_dir: Path,
|
|
6623
6687
|
*,
|
|
6624
|
-
|
|
6625
|
-
|
|
6626
|
-
|
|
6627
|
-
|
|
6628
|
-
|
|
6629
|
-
|
|
6688
|
+
excludes: Sequence[str] = (),
|
|
6689
|
+
skip_external_links: bool = False,
|
|
6690
|
+
protected: Sequence[Path] = (),
|
|
6691
|
+
) -> tuple[InputPlan, ...]:
|
|
6692
|
+
"""Check every input as a run would, without copying.
|
|
6693
|
+
|
|
6694
|
+
Directories under `protected` (AOP's own state) are left out of any input
|
|
6695
|
+
that contains them. A symlink whose target lies inside its input is copied
|
|
6696
|
+
as the target's contents; one that leaves the input, or is dangling, fails
|
|
6697
|
+
the run unless `skip_external_links` is set.
|
|
6698
|
+
"""
|
|
6699
|
+
for pattern in excludes:
|
|
6700
|
+
if not pattern or "\0" in pattern or pattern.startswith("/"):
|
|
6701
|
+
raise AOPError(f"input exclude must be a relative glob pattern: {pattern!r}")
|
|
6702
|
+
protected = tuple(path.resolve() for path in protected)
|
|
6703
|
+
plans: list[InputPlan] = []
|
|
6630
6704
|
aliases: set[str] = set()
|
|
6631
|
-
for
|
|
6705
|
+
for value in values:
|
|
6632
6706
|
raw = os.fspath(value)
|
|
6633
6707
|
if not raw or "\0" in raw:
|
|
6634
6708
|
raise AOPError("input path must be a non-empty filesystem path")
|
|
@@ -6648,46 +6722,136 @@ def _prepare_inputs(
|
|
|
6648
6722
|
if alias in aliases:
|
|
6649
6723
|
raise AOPError(f"input paths have the same basename: {alias}")
|
|
6650
6724
|
aliases.add(alias)
|
|
6651
|
-
|
|
6652
|
-
|
|
6725
|
+
if any(source == root or source.is_relative_to(root) for root in protected):
|
|
6726
|
+
raise AOPError(f"input path is inside AOP's state directory: {source}")
|
|
6653
6727
|
if source.is_dir():
|
|
6654
|
-
|
|
6728
|
+
plans.append(
|
|
6729
|
+
_plan_directory(source, alias, excludes, skip_external_links, protected)
|
|
6730
|
+
)
|
|
6731
|
+
elif source.is_file():
|
|
6732
|
+
plans.append(InputPlan(source, alias, (), (), ()))
|
|
6733
|
+
else:
|
|
6734
|
+
raise AOPError(f"input path is not a regular file or directory: {source}")
|
|
6735
|
+
return tuple(plans)
|
|
6736
|
+
|
|
6737
|
+
|
|
6738
|
+
def _plan_directory(
|
|
6739
|
+
root: Path,
|
|
6740
|
+
alias: str,
|
|
6741
|
+
excludes: Sequence[str],
|
|
6742
|
+
skip_external_links: bool,
|
|
6743
|
+
protected: tuple[Path, ...],
|
|
6744
|
+
) -> InputPlan:
|
|
6745
|
+
entries: list[tuple[str, Path]] = []
|
|
6746
|
+
excluded: list[str] = []
|
|
6747
|
+
links: list[dict[str, str]] = []
|
|
6748
|
+
|
|
6749
|
+
def is_excluded(relative: str) -> bool:
|
|
6750
|
+
name = relative.rsplit("/", 1)[-1]
|
|
6751
|
+
return any(
|
|
6752
|
+
fnmatch.fnmatchcase(relative, pattern)
|
|
6753
|
+
or ("/" not in pattern and fnmatch.fnmatchcase(name, pattern))
|
|
6754
|
+
for pattern in excludes
|
|
6755
|
+
)
|
|
6756
|
+
|
|
6757
|
+
def is_protected(path: Path) -> bool:
|
|
6758
|
+
return any(path == item or path.is_relative_to(item) for item in protected)
|
|
6759
|
+
|
|
6760
|
+
def walk(directory: Path, relative: PurePosixPath, chain: tuple[Path, ...]) -> None:
|
|
6761
|
+
try:
|
|
6762
|
+
children = sorted(directory.iterdir(), key=lambda child: child.name)
|
|
6763
|
+
except OSError as error:
|
|
6764
|
+
raise AOPError(
|
|
6765
|
+
f"could not read input directory {directory}: {error}"
|
|
6766
|
+
) from error
|
|
6767
|
+
for child in children:
|
|
6768
|
+
logical = (relative / child.name).as_posix()
|
|
6769
|
+
if is_excluded(logical):
|
|
6770
|
+
excluded.append(logical)
|
|
6771
|
+
continue
|
|
6772
|
+
status = child.lstat()
|
|
6773
|
+
path = child
|
|
6774
|
+
if stat.S_ISLNK(status.st_mode):
|
|
6775
|
+
target = child.resolve()
|
|
6776
|
+
inside = target.is_relative_to(root) and target.exists()
|
|
6777
|
+
if not inside:
|
|
6778
|
+
if not skip_external_links:
|
|
6779
|
+
raise AOPError(
|
|
6780
|
+
f"input contains a symlink that leaves it: {child} -> "
|
|
6781
|
+
f"{os.readlink(child)}; pass --skip-external-links "
|
|
6782
|
+
"to leave such links out, or --input-exclude to "
|
|
6783
|
+
"exclude them"
|
|
6784
|
+
)
|
|
6785
|
+
links.append(
|
|
6786
|
+
{"path": logical, "target": os.readlink(child), "action": "skipped"}
|
|
6787
|
+
)
|
|
6788
|
+
continue
|
|
6789
|
+
if target in chain:
|
|
6790
|
+
raise AOPError(f"input contains a symlink loop: {child}")
|
|
6791
|
+
links.append(
|
|
6792
|
+
{"path": logical, "target": os.readlink(child), "action": "followed"}
|
|
6793
|
+
)
|
|
6794
|
+
path = target
|
|
6795
|
+
status = target.stat()
|
|
6796
|
+
if stat.S_ISDIR(status.st_mode):
|
|
6797
|
+
resolved = path.resolve()
|
|
6798
|
+
if is_protected(resolved):
|
|
6799
|
+
excluded.append(logical)
|
|
6800
|
+
elif resolved in chain:
|
|
6801
|
+
raise AOPError(f"input contains a symlink loop: {child}")
|
|
6802
|
+
else:
|
|
6803
|
+
walk(path, PurePosixPath(logical), (*chain, resolved))
|
|
6804
|
+
elif stat.S_ISREG(status.st_mode):
|
|
6805
|
+
entries.append((logical, path))
|
|
6806
|
+
else:
|
|
6807
|
+
raise AOPError(f"read path contains a special file: {child}")
|
|
6808
|
+
|
|
6809
|
+
walk(root, PurePosixPath(), (root,))
|
|
6810
|
+
return InputPlan(root, alias, tuple(entries), tuple(excluded), tuple(links))
|
|
6811
|
+
|
|
6812
|
+
|
|
6813
|
+
def _prepare_inputs(
|
|
6814
|
+
plans: Sequence[InputPlan],
|
|
6815
|
+
input_dir: Path,
|
|
6816
|
+
*,
|
|
6817
|
+
guest_paths: bool = True,
|
|
6818
|
+
provenance: Sequence[str] | None = None,
|
|
6819
|
+
) -> tuple[InputSnapshot, ...]:
|
|
6820
|
+
if provenance is not None and len(provenance) != len(plans):
|
|
6821
|
+
raise AOPError("input provenance does not match the input snapshot set")
|
|
6822
|
+
prepared: list[InputSnapshot] = []
|
|
6823
|
+
for index, plan in enumerate(plans):
|
|
6824
|
+
mounted = input_dir / plan.alias
|
|
6825
|
+
if plan.source.is_dir():
|
|
6826
|
+
mounted.mkdir()
|
|
6827
|
+
for relative, path in plan.entries:
|
|
6828
|
+
target = mounted / relative
|
|
6829
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
6830
|
+
shutil.copyfile(path, target)
|
|
6655
6831
|
else:
|
|
6656
|
-
shutil.copyfile(source, mounted)
|
|
6832
|
+
shutil.copyfile(plan.source, mounted)
|
|
6657
6833
|
kind, files = _inspect_read_path(mounted)
|
|
6658
|
-
size_bytes = sum(item.size_bytes for item in files)
|
|
6659
6834
|
prepared.append(
|
|
6660
6835
|
InputSnapshot(
|
|
6661
6836
|
source_path=(
|
|
6662
|
-
provenance[index]
|
|
6837
|
+
provenance[index]
|
|
6838
|
+
if provenance is not None
|
|
6839
|
+
else os.fspath(plan.source)
|
|
6663
6840
|
),
|
|
6664
6841
|
mounted_path=(
|
|
6665
|
-
f"/inputs/{alias}" if guest_paths else os.fspath(mounted)
|
|
6842
|
+
f"/inputs/{plan.alias}" if guest_paths else os.fspath(mounted)
|
|
6666
6843
|
),
|
|
6667
6844
|
kind=kind,
|
|
6668
|
-
size_bytes=size_bytes,
|
|
6845
|
+
size_bytes=sum(item.size_bytes for item in files),
|
|
6669
6846
|
sha256=_read_path_digest(kind, files),
|
|
6670
6847
|
files=files,
|
|
6848
|
+
excluded=plan.excluded,
|
|
6849
|
+
links=plan.links,
|
|
6671
6850
|
)
|
|
6672
6851
|
)
|
|
6673
6852
|
return tuple(prepared)
|
|
6674
6853
|
|
|
6675
6854
|
|
|
6676
|
-
def _copy_input_directory(source: Path, destination: Path) -> None:
|
|
6677
|
-
destination.mkdir()
|
|
6678
|
-
for child in sorted(source.iterdir(), key=lambda item: item.name):
|
|
6679
|
-
status = child.lstat()
|
|
6680
|
-
target = destination / child.name
|
|
6681
|
-
if stat.S_ISLNK(status.st_mode):
|
|
6682
|
-
raise AOPError(f"read path contains a symlink: {child}")
|
|
6683
|
-
if stat.S_ISDIR(status.st_mode):
|
|
6684
|
-
_copy_input_directory(child, target)
|
|
6685
|
-
elif stat.S_ISREG(status.st_mode):
|
|
6686
|
-
shutil.copyfile(child, target)
|
|
6687
|
-
else:
|
|
6688
|
-
raise AOPError(f"read path contains a special file: {child}")
|
|
6689
|
-
|
|
6690
|
-
|
|
6691
6855
|
def _make_snapshot_private(root: Path) -> None:
|
|
6692
6856
|
for directory, names, files in os.walk(root):
|
|
6693
6857
|
path = Path(directory)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|