devlyn-cli 4.1.0 → 4.2.0
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/AGENTS.md +10 -94
- package/CLAUDE.md +7 -160
- package/LICENSE +21 -0
- package/README.md +65 -110
- package/bin/devlyn.js +95 -116
- package/bin/instruction-templates.json +43 -2
- package/bin/instructions.js +29 -8
- package/config/skills/_shared/adapters/claude.md +8 -85
- package/config/skills/_shared/adapters/codex.md +14 -34
- package/config/skills/_shared/adapters/omp.md +1 -11
- package/config/skills/_shared/codex-monitored.sh +4 -54
- package/config/skills/_shared/engine-doctor.sh +18 -58
- package/config/skills/_shared/engine-preflight.md +15 -47
- package/config/skills/_shared/expected-contract.py +242 -0
- package/config/skills/_shared/expected.schema.json +10 -95
- package/config/skills/_shared/invocation-receipt.py +26 -757
- package/config/skills/_shared/platform-support.py +37 -28
- package/config/skills/_shared/role-config.py +100 -421
- package/config/skills/_shared/runtime-principles.md +17 -93
- package/config/skills/_shared/task-complete.py +490 -285
- package/config/skills/_shared/task-completion.md +235 -0
- package/config/skills/devlyn-engines/SKILL.md +9 -11
- package/config/skills/devlyn-ideate/SKILL.md +57 -107
- package/config/skills/devlyn-ideate/references/elicitation.md +18 -99
- package/config/skills/devlyn-ideate/references/loop.md +110 -0
- package/config/skills/devlyn-ideate/references/package-format.md +128 -0
- package/config/skills/devlyn-ideate/scripts/acceptance.py +795 -0
- package/config/skills/devlyn-ideate/scripts/queue.py +1780 -0
- package/package.json +2 -3
- package/config/commit-conventions.md +0 -28
- package/config/skills/_shared/adapters/README.md +0 -74
- package/config/skills/_shared/adapters/grok.md +0 -100
- package/config/skills/_shared/archive_run.py +0 -969
- package/config/skills/_shared/codex-config.md +0 -81
- package/config/skills/_shared/collect-codex-findings.py +0 -505
- package/config/skills/_shared/finish-gate.py +0 -521
- package/config/skills/_shared/grok-anchor-guard.py +0 -78
- package/config/skills/_shared/judge-output-parser.py +0 -200
- package/config/skills/_shared/judge-role-evidence.py +0 -320
- package/config/skills/_shared/phase-prompt-render.py +0 -183
- package/config/skills/_shared/process-evidence.py +0 -926
- package/config/skills/_shared/resolve-bootstrap.py +0 -1410
- package/config/skills/_shared/resolve-stop-hook.py +0 -359
- package/config/skills/_shared/run-bounded.py +0 -58
- package/config/skills/_shared/spec-verify-check.py +0 -5083
- package/config/skills/_shared/state-phase-write.py +0 -5989
- package/config/skills/_shared/terminal-claim-check.py +0 -777
- package/config/skills/_shared/verify-merge-findings.py +0 -3528
- package/config/skills/devlyn-design-ui/SKILL.md +0 -650
- package/config/skills/devlyn-ideate/references/from-spec-mode.md +0 -61
- package/config/skills/devlyn-ideate/references/project-mode.md +0 -80
- package/config/skills/devlyn-ideate/references/spec-template.md +0 -121
- package/config/skills/devlyn-ideate/references/templates/decision.md +0 -48
- package/config/skills/devlyn-ideate/references/templates/roadmap.md +0 -50
- package/config/skills/devlyn-ideate/references/templates/vision.md +0 -44
- package/config/skills/devlyn-queue/SKILL.md +0 -66
- package/config/skills/devlyn-queue/scripts/append.py +0 -94
- package/config/skills/devlyn-resolve/SKILL.md +0 -379
- package/config/skills/devlyn-resolve/references/free-form-mode.md +0 -71
- package/config/skills/devlyn-resolve/references/outer-loop.md +0 -23
- package/config/skills/devlyn-resolve/references/phases/build-gate.md +0 -75
- package/config/skills/devlyn-resolve/references/phases/cleanup.md +0 -33
- package/config/skills/devlyn-resolve/references/phases/implement.md +0 -44
- package/config/skills/devlyn-resolve/references/phases/plan.md +0 -39
- package/config/skills/devlyn-resolve/references/phases/probe-derive.md +0 -261
- package/config/skills/devlyn-resolve/references/phases/surface-close.md +0 -34
- package/config/skills/devlyn-resolve/references/phases/verify.md +0 -310
- package/config/skills/devlyn-resolve/references/state-schema.md +0 -152
- package/config/skills/devlyn-resolve/references/task-completion.md +0 -160
- package/config/templates/prompt-templates.md +0 -71
- package/config/templates/template-feature.spec.md +0 -255
- package/config/templates/template-product-spec.md +0 -680
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""Dispatch `codex exec` for codex-monitored.sh: exact argv, an optional sealed prompt file, native process ownership."""
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
5
|
import runpy
|
|
@@ -18,16 +18,10 @@ import tempfile
|
|
|
18
18
|
import time
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
SCHEMA_VERSION = "2.0"
|
|
22
|
-
PHASES = {"plan", "implement", "build_gate", "cleanup"}
|
|
23
|
-
SHA256_RE = re.compile(r"^[0-9a-f]{64}$")
|
|
24
|
-
SAFE_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
25
|
-
DANGEROUS_FLAGS = {"--dangerously-bypass-approvals-and-sandbox", "--yolo"}
|
|
26
|
-
NETWORK_ACCESS_CONFIG = "sandbox_workspace_write.network_access"
|
|
27
21
|
PLATFORM = runpy.run_path(pathlib.Path(__file__).with_name("platform-support.py"))
|
|
28
22
|
|
|
29
23
|
|
|
30
|
-
class
|
|
24
|
+
class TransportError(ValueError):
|
|
31
25
|
pass
|
|
32
26
|
|
|
33
27
|
|
|
@@ -69,62 +63,6 @@ def atomic_write(path: pathlib.Path, value: object) -> None:
|
|
|
69
63
|
raise
|
|
70
64
|
|
|
71
65
|
|
|
72
|
-
def relative_file(work: pathlib.Path, raw_path: str, label: str, *, require: bool) -> tuple[pathlib.Path, str]:
|
|
73
|
-
path = pathlib.Path(raw_path)
|
|
74
|
-
candidate = path if path.is_absolute() else work / path
|
|
75
|
-
try:
|
|
76
|
-
if require and candidate.is_symlink():
|
|
77
|
-
raise ReceiptError(f"{label} must not be a symlink: {raw_path}")
|
|
78
|
-
resolved = candidate.resolve(strict=require)
|
|
79
|
-
relative = resolved.relative_to(work.resolve()).as_posix()
|
|
80
|
-
except (OSError, ValueError) as exc:
|
|
81
|
-
raise ReceiptError(f"{label} is missing or escapes the worktree: {raw_path}") from exc
|
|
82
|
-
if require and not resolved.is_file():
|
|
83
|
-
raise ReceiptError(f"{label} is not a regular file: {raw_path}")
|
|
84
|
-
return resolved, relative
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
def option_values(argv: list[str], short: str, long: str) -> list[str]:
|
|
88
|
-
values = []
|
|
89
|
-
index = 0
|
|
90
|
-
while index < len(argv):
|
|
91
|
-
token = argv[index]
|
|
92
|
-
if token in {short, long}:
|
|
93
|
-
if index + 1 >= len(argv):
|
|
94
|
-
raise ReceiptError(f"{token} requires a value")
|
|
95
|
-
values.append(argv[index + 1])
|
|
96
|
-
index += 2
|
|
97
|
-
continue
|
|
98
|
-
if token.startswith(long + "="):
|
|
99
|
-
values.append(token.split("=", 1)[1])
|
|
100
|
-
elif token.startswith(short + "="):
|
|
101
|
-
values.append(token.split("=", 1)[1])
|
|
102
|
-
index += 1
|
|
103
|
-
return values
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def option_value(argv: list[str], short: str, long: str) -> str | None:
|
|
107
|
-
values = option_values(argv, short, long)
|
|
108
|
-
if len(values) > 1:
|
|
109
|
-
raise ReceiptError(f"duplicate {long} option")
|
|
110
|
-
return values[0] if values else None
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
def sandbox_network_access(argv: list[str], phase: str) -> bool:
|
|
114
|
-
enabled = phase == "build_gate"
|
|
115
|
-
expected = f"{NETWORK_ACCESS_CONFIG}={'true' if enabled else 'false'}"
|
|
116
|
-
related = [
|
|
117
|
-
raw for raw in option_values(argv, "-c", "--config")
|
|
118
|
-
if "sandbox_workspace_write" in raw or "network_access" in raw
|
|
119
|
-
]
|
|
120
|
-
if related != [expected]:
|
|
121
|
-
raise ReceiptError(
|
|
122
|
-
f"Codex {phase} requires exactly one -c {expected}; "
|
|
123
|
-
"alternate, table, missing, and duplicate network overrides are forbidden"
|
|
124
|
-
)
|
|
125
|
-
return enabled
|
|
126
|
-
|
|
127
|
-
|
|
128
66
|
def file_prompt_args(argv):
|
|
129
67
|
"""File mode accepts one explicit stdin prompt and no positional competitors."""
|
|
130
68
|
values = {"-C", "--cd", "-s", "--sandbox", "-m", "--model", "-c", "--config",
|
|
@@ -138,7 +76,7 @@ def file_prompt_args(argv):
|
|
|
138
76
|
token = argv[index]
|
|
139
77
|
if token in values:
|
|
140
78
|
if index + 1 >= len(argv):
|
|
141
|
-
raise
|
|
79
|
+
raise TransportError(f"{token} requires a value")
|
|
142
80
|
index += 2
|
|
143
81
|
elif any(token.startswith(key + "=") for key in values) or token in flags:
|
|
144
82
|
index += 1
|
|
@@ -149,22 +87,22 @@ def file_prompt_args(argv):
|
|
|
149
87
|
positional.append(token)
|
|
150
88
|
index += 1
|
|
151
89
|
else:
|
|
152
|
-
raise
|
|
90
|
+
raise TransportError(f"unsupported Codex file-transport option: {token}")
|
|
153
91
|
if positional != ["-"]:
|
|
154
|
-
raise
|
|
92
|
+
raise TransportError("file transport requires the sole prompt argument '-'; competing prompts are forbidden")
|
|
155
93
|
|
|
156
94
|
|
|
157
|
-
def prepare_transport(prompt_path, command, argv, seconds
|
|
95
|
+
def prepare_transport(prompt_path, command, argv, seconds):
|
|
158
96
|
path = pathlib.Path(prompt_path).resolve(strict=True)
|
|
159
97
|
with PLATFORM["open_stdin"](path) as source:
|
|
160
98
|
raw = source.read()
|
|
161
99
|
record = {"schema_version": 1, "transport": "stdin-file",
|
|
162
100
|
"prompt": {"path": str(path), "sha256": sha256(raw), "bytes": len(raw)},
|
|
163
|
-
"command": command, "argv": argv, "timeout_sec": seconds,
|
|
101
|
+
"command": command, "argv": argv, "timeout_sec": seconds,
|
|
164
102
|
"status": "started", "exit_code": None}
|
|
165
103
|
carrier = path.with_name(path.name + ".transport.json")
|
|
166
104
|
if carrier.exists():
|
|
167
|
-
raise
|
|
105
|
+
raise TransportError(f"prompt transport already exists: {carrier}")
|
|
168
106
|
stream = tempfile.TemporaryFile("w+b")
|
|
169
107
|
try:
|
|
170
108
|
stream.write(raw)
|
|
@@ -181,330 +119,23 @@ def write_transport(path, record):
|
|
|
181
119
|
|
|
182
120
|
|
|
183
121
|
def finish_transport(path, exit_code):
|
|
184
|
-
record =
|
|
122
|
+
record = read_transport(path)
|
|
185
123
|
if record.get("status") != "started":
|
|
186
|
-
raise
|
|
124
|
+
raise TransportError("prompt transport is not open")
|
|
187
125
|
record.update(status="completed", exit_code=exit_code)
|
|
188
126
|
atomic_write(path, record)
|
|
189
127
|
|
|
190
128
|
|
|
191
|
-
def
|
|
192
|
-
record = read_receipt(path)
|
|
193
|
-
if set(record) != {"schema_version", "transport", "prompt", "command", "argv", "timeout_sec", "isolated", "status", "exit_code"}:
|
|
194
|
-
raise ReceiptError("invalid prompt transport shape")
|
|
195
|
-
prompt = record["prompt"]
|
|
196
|
-
if not isinstance(prompt, dict) or set(prompt) != {"path", "sha256", "bytes"}:
|
|
197
|
-
raise ReceiptError("invalid delivered prompt binding")
|
|
198
|
-
if (record["schema_version"] != 1 or record["transport"] != "stdin-file"
|
|
199
|
-
or record["status"] != "completed" or type(record["exit_code"]) is not int
|
|
200
|
-
or type(record["timeout_sec"]) is not int or record["timeout_sec"] < 0
|
|
201
|
-
or type(record["isolated"]) is not bool
|
|
202
|
-
or prompt["sha256"] != sha256(prompt_raw) or type(prompt["bytes"]) is not int
|
|
203
|
-
or prompt["bytes"] != len(prompt_raw)):
|
|
204
|
-
raise ReceiptError("delivered prompt/transport mismatch")
|
|
205
|
-
original = pathlib.Path(prompt["path"])
|
|
206
|
-
# Custody may relocate this sealed bundle; rehash its local prompt, not a live source tree.
|
|
207
|
-
if (not original.is_absolute() or path.name != original.name + ".transport.json"
|
|
208
|
-
or path.with_name(original.name).read_bytes() != prompt_raw):
|
|
209
|
-
raise ReceiptError("transport prompt path/bytes mismatch")
|
|
210
|
-
command, actual = record["command"], record["argv"]
|
|
211
|
-
if not all(isinstance(args, list) and args and all(isinstance(arg, str) for arg in args)
|
|
212
|
-
for args in (command, actual)) or len(actual) < len(command) or actual[len(actual) - len(command) + 1:] != command[1:]:
|
|
213
|
-
raise ReceiptError("transport actual argv mismatch")
|
|
214
|
-
return record
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def start_receipt(
|
|
218
|
-
work: pathlib.Path,
|
|
219
|
-
receipt_path: pathlib.Path,
|
|
220
|
-
run_id: str,
|
|
221
|
-
phase: str,
|
|
222
|
-
round_: int,
|
|
223
|
-
prompt_path: str,
|
|
224
|
-
session_path: str,
|
|
225
|
-
argv: list[str],
|
|
226
|
-
*, transport: dict | None = None,
|
|
227
|
-
) -> None:
|
|
228
|
-
if receipt_path.exists():
|
|
229
|
-
raise ReceiptError(f"invocation receipt already exists: {receipt_path}")
|
|
230
|
-
if SAFE_ID_RE.fullmatch(run_id) is None:
|
|
231
|
-
raise ReceiptError("run id is invalid")
|
|
232
|
-
if phase not in PHASES:
|
|
233
|
-
raise ReceiptError(f"unsupported invocation phase: {phase}")
|
|
234
|
-
if round_ < 0:
|
|
235
|
-
raise ReceiptError("round must be non-negative")
|
|
236
|
-
evidence_manifest = (
|
|
237
|
-
work / ".devlyn" / "process-evidence" / run_id / phase
|
|
238
|
-
/ f"round-{round_}" / "manifest.json"
|
|
239
|
-
)
|
|
240
|
-
if evidence_manifest.is_file():
|
|
241
|
-
manifest = loads_strict_json(evidence_manifest.read_text(encoding="utf-8"))
|
|
242
|
-
entries = manifest.get("entries") if isinstance(manifest, dict) else None
|
|
243
|
-
if not isinstance(entries, list):
|
|
244
|
-
raise ReceiptError("existing process-evidence manifest is malformed")
|
|
245
|
-
if any(
|
|
246
|
-
isinstance(entry, dict)
|
|
247
|
-
and isinstance(entry.get("classification"), dict)
|
|
248
|
-
and entry["classification"].get("kind") == "capability_denied"
|
|
249
|
-
for entry in entries
|
|
250
|
-
):
|
|
251
|
-
raise ReceiptError(
|
|
252
|
-
"capability-denied phase/round cannot launch another Codex invocation"
|
|
253
|
-
)
|
|
254
|
-
forbidden = sorted(DANGEROUS_FLAGS.intersection(argv))
|
|
255
|
-
if forbidden:
|
|
256
|
-
raise ReceiptError(f"forbidden Codex bypass flag: {forbidden[0]}")
|
|
257
|
-
if "--json" not in argv[:-1]:
|
|
258
|
-
raise ReceiptError("receipt-bound Codex invocation requires --json before the prompt")
|
|
259
|
-
model = option_value(argv, "-m", "--model")
|
|
260
|
-
sandbox = option_value(argv, "-s", "--sandbox")
|
|
261
|
-
invocation_workdir = option_value(argv, "-C", "--cd")
|
|
262
|
-
if not model:
|
|
263
|
-
raise ReceiptError("Codex invocation requires an explicit model")
|
|
264
|
-
if not sandbox:
|
|
265
|
-
raise ReceiptError("Codex invocation requires an explicit sandbox")
|
|
266
|
-
if sandbox != "workspace-write":
|
|
267
|
-
raise ReceiptError(
|
|
268
|
-
f"Codex {phase} sandbox must remain workspace-write, got {sandbox}"
|
|
269
|
-
)
|
|
270
|
-
network_access = sandbox_network_access(argv, phase)
|
|
271
|
-
if not invocation_workdir:
|
|
272
|
-
raise ReceiptError("Codex invocation requires an explicit workdir")
|
|
273
|
-
try:
|
|
274
|
-
if pathlib.Path(invocation_workdir).resolve(strict=True) != work.resolve():
|
|
275
|
-
raise ReceiptError("Codex invocation workdir does not match receipt workdir")
|
|
276
|
-
except OSError as exc:
|
|
277
|
-
raise ReceiptError("Codex invocation workdir is missing") from exc
|
|
278
|
-
|
|
279
|
-
prompt_file, prompt_relative = relative_file(work, prompt_path, "invocation prompt", require=True)
|
|
280
|
-
session_file, session_relative = relative_file(work, session_path, "worker session", require=False)
|
|
281
|
-
receipt_file, receipt_relative = relative_file(work, str(receipt_path), "invocation receipt", require=False)
|
|
282
|
-
expected_receipt = f".devlyn/{phase}.invocation.{round_}.json"
|
|
283
|
-
expected_session = f".devlyn/{phase}.worker-session.{round_}.jsonl"
|
|
284
|
-
expected_prompt = f".devlyn/{phase}.prompt.{round_}"
|
|
285
|
-
if receipt_relative != expected_receipt:
|
|
286
|
-
raise ReceiptError(
|
|
287
|
-
f"invocation receipt path expected {expected_receipt}, got {receipt_relative}"
|
|
288
|
-
)
|
|
289
|
-
if session_relative != expected_session:
|
|
290
|
-
raise ReceiptError(
|
|
291
|
-
f"worker session path expected {expected_session}, got {session_relative}"
|
|
292
|
-
)
|
|
293
|
-
if prompt_relative != expected_prompt:
|
|
294
|
-
raise ReceiptError(
|
|
295
|
-
f"invocation prompt path expected {expected_prompt}, got {prompt_relative}"
|
|
296
|
-
)
|
|
297
|
-
prompt_raw = prompt_file.read_bytes()
|
|
298
|
-
try:
|
|
299
|
-
prompt_argument = prompt_raw.decode("utf-8").rstrip("\n")
|
|
300
|
-
except UnicodeError as exc:
|
|
301
|
-
raise ReceiptError("invocation prompt is not UTF-8") from exc
|
|
302
|
-
if transport is not None:
|
|
303
|
-
file_prompt_args(argv)
|
|
304
|
-
if (transport["prompt"] != {"path": str(prompt_file), "sha256": sha256(prompt_raw), "bytes": len(prompt_raw)}
|
|
305
|
-
or transport["command"][1:] != ["exec", *argv]):
|
|
306
|
-
raise ReceiptError("delivered prompt file/bytes or actual argv does not match the canonical invocation")
|
|
307
|
-
elif not argv or argv[-1] == "-" or argv[-1] != prompt_argument:
|
|
308
|
-
raise ReceiptError("Codex prompt argument does not match the canonical prompt file")
|
|
309
|
-
|
|
310
|
-
atomic_write(receipt_file, {
|
|
311
|
-
"schema_version": SCHEMA_VERSION,
|
|
312
|
-
"run_id": run_id,
|
|
313
|
-
"phase": phase,
|
|
314
|
-
"round": round_,
|
|
315
|
-
"engine": "codex",
|
|
316
|
-
"model": model,
|
|
317
|
-
"sandbox": sandbox,
|
|
318
|
-
"sandbox_network_access": network_access,
|
|
319
|
-
"prompt": {"path": prompt_relative, "sha256": sha256(prompt_raw)},
|
|
320
|
-
"session": {"path": session_relative, "sha256": None, "bytes": None},
|
|
321
|
-
"argv_sha256": sha256(json.dumps(argv, separators=(",", ":")).encode("utf-8")),
|
|
322
|
-
"status": "started",
|
|
323
|
-
"exit_code": None,
|
|
324
|
-
**({"transport": {"path": prompt_relative + ".transport.json", "sha256": None}} if transport is not None else {}),
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
def read_receipt(path: pathlib.Path) -> dict:
|
|
129
|
+
def read_transport(path: pathlib.Path) -> dict:
|
|
329
130
|
try:
|
|
330
131
|
value = loads_strict_json(path.read_text(encoding="utf-8"))
|
|
331
132
|
except (OSError, UnicodeError, ValueError) as exc:
|
|
332
|
-
raise
|
|
133
|
+
raise TransportError(f"prompt transport is invalid: {exc}") from exc
|
|
333
134
|
if not isinstance(value, dict):
|
|
334
|
-
raise
|
|
135
|
+
raise TransportError("prompt transport must contain an object")
|
|
335
136
|
return value
|
|
336
137
|
|
|
337
138
|
|
|
338
|
-
def finish_receipt(work: pathlib.Path, receipt_path: pathlib.Path, exit_code: int) -> None:
|
|
339
|
-
receipt = read_receipt(receipt_path)
|
|
340
|
-
if receipt.get("status") != "started" or receipt.get("exit_code") is not None:
|
|
341
|
-
raise ReceiptError("invocation receipt is not open")
|
|
342
|
-
session = receipt.get("session")
|
|
343
|
-
session_path = session.get("path") if isinstance(session, dict) else None
|
|
344
|
-
if not isinstance(session_path, str):
|
|
345
|
-
raise ReceiptError("invocation receipt session path is invalid")
|
|
346
|
-
session_file, _ = relative_file(work, session_path, "worker session", require=True)
|
|
347
|
-
raw = session_file.read_bytes()
|
|
348
|
-
receipt["session"] = {
|
|
349
|
-
"path": session_path,
|
|
350
|
-
"sha256": sha256(raw),
|
|
351
|
-
"bytes": len(raw),
|
|
352
|
-
}
|
|
353
|
-
receipt["status"] = "completed"
|
|
354
|
-
receipt["exit_code"] = exit_code
|
|
355
|
-
if "transport" in receipt:
|
|
356
|
-
carrier, _ = relative_file(work, receipt["transport"]["path"], "prompt transport", require=True)
|
|
357
|
-
receipt["transport"]["sha256"] = sha256(carrier.read_bytes())
|
|
358
|
-
atomic_write(receipt_path, receipt)
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
def validate_receipt_artifacts(
|
|
362
|
-
work: pathlib.Path,
|
|
363
|
-
receipt_path: pathlib.Path,
|
|
364
|
-
*,
|
|
365
|
-
run_id: str,
|
|
366
|
-
phase: str,
|
|
367
|
-
) -> tuple[dict, pathlib.Path, pathlib.Path, pathlib.Path]:
|
|
368
|
-
"""Validate self-contained receipt identity and every referenced byte carrier."""
|
|
369
|
-
receipt_file, receipt_relative = relative_file(
|
|
370
|
-
work, str(receipt_path), "invocation receipt", require=True,
|
|
371
|
-
)
|
|
372
|
-
receipt = read_receipt(receipt_file)
|
|
373
|
-
if set(receipt) - {"transport"} != {
|
|
374
|
-
"schema_version", "run_id", "phase", "round", "engine", "model",
|
|
375
|
-
"sandbox", "sandbox_network_access", "prompt", "session", "argv_sha256",
|
|
376
|
-
"status", "exit_code",
|
|
377
|
-
}:
|
|
378
|
-
raise ReceiptError("invocation receipt has an invalid shape")
|
|
379
|
-
round_ = receipt.get("round")
|
|
380
|
-
if isinstance(round_, bool) or not isinstance(round_, int) or round_ < 0:
|
|
381
|
-
raise ReceiptError("invocation receipt round is invalid")
|
|
382
|
-
expected_path = f".devlyn/{phase}.invocation.{round_}.json"
|
|
383
|
-
if receipt_relative != expected_path:
|
|
384
|
-
raise ReceiptError(f"invocation receipt path expected {expected_path}")
|
|
385
|
-
expected_identity = (SCHEMA_VERSION, run_id, phase, round_, "codex", "completed")
|
|
386
|
-
actual_identity = (
|
|
387
|
-
receipt["schema_version"], receipt["run_id"], receipt["phase"], receipt["round"],
|
|
388
|
-
receipt["engine"], receipt["status"],
|
|
389
|
-
)
|
|
390
|
-
if actual_identity != expected_identity:
|
|
391
|
-
raise ReceiptError("invocation receipt identity/status mismatch")
|
|
392
|
-
if not isinstance(receipt["model"], str) or not receipt["model"]:
|
|
393
|
-
raise ReceiptError("invocation receipt model is invalid")
|
|
394
|
-
if receipt["sandbox"] != "workspace-write":
|
|
395
|
-
raise ReceiptError("invocation receipt sandbox must be workspace-write")
|
|
396
|
-
expected_network_access = phase == "build_gate"
|
|
397
|
-
if receipt["sandbox_network_access"] is not expected_network_access:
|
|
398
|
-
raise ReceiptError("invocation receipt sandbox network-access capability mismatch")
|
|
399
|
-
if not isinstance(receipt["exit_code"], int) or isinstance(receipt["exit_code"], bool):
|
|
400
|
-
raise ReceiptError("invocation receipt exit_code is invalid")
|
|
401
|
-
if not isinstance(receipt["argv_sha256"], str) or SHA256_RE.fullmatch(receipt["argv_sha256"]) is None:
|
|
402
|
-
raise ReceiptError("invocation receipt argv digest is invalid")
|
|
403
|
-
prompt = receipt["prompt"]
|
|
404
|
-
if not isinstance(prompt, dict) or set(prompt) != {"path", "sha256"}:
|
|
405
|
-
raise ReceiptError("invocation receipt prompt binding is invalid")
|
|
406
|
-
prompt_file, _ = relative_file(work, prompt["path"], "invocation prompt", require=True)
|
|
407
|
-
if prompt["path"] != f".devlyn/{phase}.prompt.{round_}":
|
|
408
|
-
raise ReceiptError("invocation receipt prompt path mismatch")
|
|
409
|
-
if (
|
|
410
|
-
not isinstance(prompt["sha256"], str)
|
|
411
|
-
or SHA256_RE.fullmatch(prompt["sha256"]) is None
|
|
412
|
-
or sha256(prompt_file.read_bytes()) != prompt["sha256"]
|
|
413
|
-
):
|
|
414
|
-
raise ReceiptError("invocation prompt digest mismatch")
|
|
415
|
-
if "transport" in receipt:
|
|
416
|
-
binding = receipt["transport"]
|
|
417
|
-
if not isinstance(binding, dict) or set(binding) != {"path", "sha256"} or binding["path"] != prompt["path"] + ".transport.json":
|
|
418
|
-
raise ReceiptError("invocation transport binding is invalid")
|
|
419
|
-
carrier, _ = relative_file(work, binding["path"], "prompt transport", require=True)
|
|
420
|
-
if sha256(carrier.read_bytes()) != binding["sha256"]:
|
|
421
|
-
raise ReceiptError("invocation transport digest mismatch")
|
|
422
|
-
transport = validate_transport(carrier, prompt_file.read_bytes())
|
|
423
|
-
argv = transport["command"][2:]
|
|
424
|
-
file_prompt_args(argv)
|
|
425
|
-
if (transport["command"][1:2] != ["exec"] or transport["exit_code"] != receipt["exit_code"]
|
|
426
|
-
or sha256(json.dumps(argv, separators=(",", ":")).encode("utf-8")) != receipt["argv_sha256"]
|
|
427
|
-
or option_value(argv, "-m", "--model") != receipt["model"]
|
|
428
|
-
or option_value(argv, "-s", "--sandbox") != receipt["sandbox"]
|
|
429
|
-
or sandbox_network_access(argv, phase) != receipt["sandbox_network_access"]):
|
|
430
|
-
raise ReceiptError("invocation actual argv/capability mismatch")
|
|
431
|
-
session = receipt["session"]
|
|
432
|
-
if not isinstance(session, dict) or set(session) != {"path", "sha256", "bytes"}:
|
|
433
|
-
raise ReceiptError("invocation receipt session binding is invalid")
|
|
434
|
-
if not isinstance(session["path"], str):
|
|
435
|
-
raise ReceiptError("invocation receipt session path is invalid")
|
|
436
|
-
session_file, relative_session = relative_file(
|
|
437
|
-
work, session["path"], "worker session", require=True,
|
|
438
|
-
)
|
|
439
|
-
if relative_session != f".devlyn/{phase}.worker-session.{round_}.jsonl":
|
|
440
|
-
raise ReceiptError("invocation receipt session path mismatch")
|
|
441
|
-
raw = session_file.read_bytes()
|
|
442
|
-
if (
|
|
443
|
-
not isinstance(session["sha256"], str)
|
|
444
|
-
or SHA256_RE.fullmatch(session["sha256"]) is None
|
|
445
|
-
or isinstance(session["bytes"], bool)
|
|
446
|
-
or not isinstance(session["bytes"], int)
|
|
447
|
-
or session["bytes"] < 0
|
|
448
|
-
or session["sha256"] != sha256(raw)
|
|
449
|
-
or session["bytes"] != len(raw)
|
|
450
|
-
):
|
|
451
|
-
raise ReceiptError("invocation worker-session digest mismatch")
|
|
452
|
-
return receipt, receipt_file, prompt_file, session_file
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
def validate_receipt(
|
|
456
|
-
work: pathlib.Path,
|
|
457
|
-
receipt_path: pathlib.Path,
|
|
458
|
-
*,
|
|
459
|
-
run_id: str,
|
|
460
|
-
phase: str,
|
|
461
|
-
round_: int,
|
|
462
|
-
model: str,
|
|
463
|
-
prompt_sha256: str,
|
|
464
|
-
session_path: pathlib.Path,
|
|
465
|
-
) -> dict:
|
|
466
|
-
receipt, receipt_file, _prompt_file, receipt_session = validate_receipt_artifacts(
|
|
467
|
-
work, receipt_path, run_id=run_id, phase=phase,
|
|
468
|
-
)
|
|
469
|
-
if receipt["round"] != round_ or receipt["model"] != model:
|
|
470
|
-
raise ReceiptError("invocation receipt round/model mismatch")
|
|
471
|
-
if receipt["prompt"]["sha256"] != prompt_sha256:
|
|
472
|
-
raise ReceiptError("invocation prompt digest mismatch")
|
|
473
|
-
supplied_session, _ = relative_file(
|
|
474
|
-
work, str(session_path), "worker session", require=True,
|
|
475
|
-
)
|
|
476
|
-
if supplied_session != receipt_session:
|
|
477
|
-
raise ReceiptError("invocation receipt session path mismatch")
|
|
478
|
-
raw = receipt_session.read_bytes()
|
|
479
|
-
if sha256(raw) != receipt["session"]["sha256"]:
|
|
480
|
-
raise ReceiptError("invocation worker-session digest mismatch")
|
|
481
|
-
for number, line in enumerate(raw.decode("utf-8").split("\n"), start=1):
|
|
482
|
-
if not line.strip():
|
|
483
|
-
continue
|
|
484
|
-
try:
|
|
485
|
-
event = loads_strict_json(line)
|
|
486
|
-
except ValueError as exc:
|
|
487
|
-
raise ReceiptError(f"invalid worker-session JSONL at line {number}: {exc}") from exc
|
|
488
|
-
if not isinstance(event, dict):
|
|
489
|
-
raise ReceiptError(f"worker-session event at line {number} must be an object")
|
|
490
|
-
item = event.get("item")
|
|
491
|
-
if (event.get("type") == "item.completed" and isinstance(item, dict)
|
|
492
|
-
and item.get("type") == "error" and isinstance(item.get("message"), str)
|
|
493
|
-
and item["message"].startswith("model rerouted: ")):
|
|
494
|
-
raise ReceiptError(
|
|
495
|
-
f"Codex reported model reroute at worker-session line {number}; "
|
|
496
|
-
"requested model was not preserved; inspect the retained session"
|
|
497
|
-
)
|
|
498
|
-
return {
|
|
499
|
-
"path": receipt_file.relative_to(work.resolve()).as_posix(),
|
|
500
|
-
"sha256": sha256(receipt_file.read_bytes()),
|
|
501
|
-
"sandbox": receipt["sandbox"],
|
|
502
|
-
"sandbox_network_access": receipt["sandbox_network_access"],
|
|
503
|
-
"argv_sha256": receipt["argv_sha256"],
|
|
504
|
-
"exit_code": receipt["exit_code"],
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
|
|
508
139
|
def monitor_descendant_regression() -> None:
|
|
509
140
|
if os.name == "nt":
|
|
510
141
|
print("SKIP POSIX shell process-group fixture; test-windows-portability.py exercises native Node/Python trees")
|
|
@@ -547,8 +178,7 @@ def monitor_descendant_regression() -> None:
|
|
|
547
178
|
env = os.environ.copy()
|
|
548
179
|
for key in tuple(env):
|
|
549
180
|
if (
|
|
550
|
-
key.startswith("
|
|
551
|
-
or key.startswith("DEVLYN_WATCHDOG_TEST_")
|
|
181
|
+
key.startswith("DEVLYN_WATCHDOG_TEST_")
|
|
552
182
|
or key.startswith("CODEX_MONITORED_")
|
|
553
183
|
or key in {"CODEX_BLOCKED", "CODEX_REAL_BIN", "DEVLYN_CODEX_PROMPT_FILE"}
|
|
554
184
|
):
|
|
@@ -783,342 +413,23 @@ def monitor_descendant_regression() -> None:
|
|
|
783
413
|
|
|
784
414
|
def self_test() -> int:
|
|
785
415
|
monitor_descendant_regression()
|
|
416
|
+
if os.name == "nt":
|
|
417
|
+
print("SKIP POSIX executable-shell wrapper fixture; native npm transport is covered by test-windows-portability.py")
|
|
418
|
+
return 0
|
|
786
419
|
with tempfile.TemporaryDirectory() as raw_tmp:
|
|
787
420
|
work = pathlib.Path(raw_tmp)
|
|
788
|
-
devlyn = work / ".devlyn"
|
|
789
|
-
devlyn.mkdir()
|
|
790
|
-
prompt = devlyn / "implement.prompt.0"
|
|
791
|
-
prompt.write_text("do the task\n", encoding="utf-8")
|
|
792
|
-
session = devlyn / "implement.worker-session.0.jsonl"
|
|
793
|
-
session.write_text('{"type":"thread.started"}\n', encoding="utf-8")
|
|
794
|
-
receipt = devlyn / "implement.invocation.0.json"
|
|
795
|
-
argv = [
|
|
796
|
-
"--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-test",
|
|
797
|
-
"-c", "sandbox_workspace_write.network_access=false", "do the task",
|
|
798
|
-
]
|
|
799
|
-
start_receipt(work, receipt, "rs-receipt", "implement", 0, str(prompt), str(session), argv)
|
|
800
|
-
finish_receipt(work, receipt, 0)
|
|
801
|
-
bound = validate_receipt(
|
|
802
|
-
work, receipt, run_id="rs-receipt", phase="implement", round_=0,
|
|
803
|
-
model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session,
|
|
804
|
-
)
|
|
805
|
-
assert bound["sandbox"] == "workspace-write" and bound["exit_code"] == 0
|
|
806
|
-
original_session = session.read_bytes()
|
|
807
|
-
original_receipt = receipt.read_bytes()
|
|
808
|
-
reroute = "model rerouted: gpt-test -> another-model (Policy)"
|
|
809
|
-
cases = [
|
|
810
|
-
({"type": "item.completed", "item": {"type": "error", "message": reroute}}, True),
|
|
811
|
-
({"type": "item.completed", "item": {"type": "agent_message", "text": reroute}}, False),
|
|
812
|
-
({"type": "item.completed", "item": {"type": "command_execution", "aggregated_output": reroute}}, False),
|
|
813
|
-
({"type": "item.completed", "item": {"type": "error", "message": "recoverable tool error"}}, False),
|
|
814
|
-
]
|
|
815
|
-
cases.append(({"type": "item.completed", "item": {
|
|
816
|
-
"type": "agent_message", "text": "ordinary \u2028 \u2029 \u0085 message",
|
|
817
|
-
}}, False))
|
|
818
|
-
for event, blocked in cases:
|
|
819
|
-
raw = original_session + (json.dumps(event, ensure_ascii=False) + '\n{"type":"turn.completed"}\n').encode()
|
|
820
|
-
session.write_bytes(raw)
|
|
821
|
-
receipt.unlink()
|
|
822
|
-
start_receipt(work, receipt, "rs-receipt", "implement", 0, str(prompt), str(session), argv)
|
|
823
|
-
finish_receipt(work, receipt, 0)
|
|
824
|
-
sealed = receipt.read_bytes()
|
|
825
|
-
try:
|
|
826
|
-
validate_receipt(work, receipt, run_id="rs-receipt", phase="implement", round_=0,
|
|
827
|
-
model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session)
|
|
828
|
-
except ReceiptError as exc:
|
|
829
|
-
assert blocked and "reported model reroute" in str(exc), str(exc)
|
|
830
|
-
else:
|
|
831
|
-
assert not blocked, "native reroute followed by success was accepted"
|
|
832
|
-
validate_receipt_artifacts(work, receipt, run_id="rs-receipt", phase="implement")
|
|
833
|
-
assert receipt.read_bytes() == sealed and session.read_bytes() == raw
|
|
834
|
-
for malformed in (b'{broken\n', b'null\n', b'{"type":"item.completed","type":"ignored"}\n'):
|
|
835
|
-
session.write_bytes(malformed)
|
|
836
|
-
receipt.unlink()
|
|
837
|
-
start_receipt(work, receipt, "rs-receipt", "implement", 0, str(prompt), str(session), argv)
|
|
838
|
-
finish_receipt(work, receipt, 0)
|
|
839
|
-
try:
|
|
840
|
-
validate_receipt(work, receipt, run_id="rs-receipt", phase="implement", round_=0,
|
|
841
|
-
model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session)
|
|
842
|
-
except ReceiptError:
|
|
843
|
-
pass
|
|
844
|
-
else:
|
|
845
|
-
raise AssertionError("malformed worker event was accepted")
|
|
846
|
-
validate_receipt_artifacts(work, receipt, run_id="rs-receipt", phase="implement")
|
|
847
|
-
session.write_bytes(original_session)
|
|
848
|
-
receipt.write_bytes(original_receipt)
|
|
849
|
-
plan_prompt = devlyn / "plan.prompt.0"
|
|
850
|
-
plan_prompt.write_text("plan exactly\n", encoding="utf-8")
|
|
851
|
-
plan_session = devlyn / "plan.worker-session.0.jsonl"
|
|
852
|
-
plan_session.write_text('{"type":"thread.started"}\n', encoding="utf-8")
|
|
853
|
-
plan_receipt = devlyn / "plan.invocation.0.json"
|
|
854
|
-
plan_argv = [
|
|
855
|
-
"--json", "-C", str(work), "-s", "workspace-write",
|
|
856
|
-
"-m", "gpt-plan", "-c",
|
|
857
|
-
"sandbox_workspace_write.network_access=false", "plan exactly",
|
|
858
|
-
]
|
|
859
|
-
start_receipt(
|
|
860
|
-
work, plan_receipt, "rs-receipt", "plan", 0,
|
|
861
|
-
str(plan_prompt), str(plan_session), plan_argv,
|
|
862
|
-
)
|
|
863
|
-
finish_receipt(work, plan_receipt, 0)
|
|
864
|
-
plan_bound = validate_receipt(
|
|
865
|
-
work, plan_receipt, run_id="rs-receipt", phase="plan", round_=0,
|
|
866
|
-
model="gpt-plan", prompt_sha256=sha256(plan_prompt.read_bytes()),
|
|
867
|
-
session_path=plan_session,
|
|
868
|
-
)
|
|
869
|
-
assert plan_bound["sandbox"] == "workspace-write"
|
|
870
|
-
try:
|
|
871
|
-
loads_strict_json('{"run_id":"a","run_id":"b"}')
|
|
872
|
-
except ValueError as exc:
|
|
873
|
-
assert "duplicate JSON key" in str(exc)
|
|
874
|
-
else:
|
|
875
|
-
raise AssertionError("duplicate invocation receipt key was accepted")
|
|
876
|
-
try:
|
|
877
|
-
start_receipt(
|
|
878
|
-
work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
|
|
879
|
-
str(prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
|
|
880
|
-
["--json", "--dangerously-bypass-approvals-and-sandbox", "-s", "danger-full-access",
|
|
881
|
-
"-m", "gpt-test", "do the task"],
|
|
882
|
-
)
|
|
883
|
-
except ReceiptError as exc:
|
|
884
|
-
assert "forbidden Codex bypass flag" in str(exc)
|
|
885
|
-
else:
|
|
886
|
-
raise AssertionError("Codex bypass flag was accepted")
|
|
887
|
-
cleanup_prompt = devlyn / "cleanup.prompt.0"
|
|
888
|
-
cleanup_prompt.write_text("do the task\n", encoding="utf-8")
|
|
889
|
-
try:
|
|
890
|
-
start_receipt(
|
|
891
|
-
work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
|
|
892
|
-
str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
|
|
893
|
-
["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-test",
|
|
894
|
-
"do the task"],
|
|
895
|
-
)
|
|
896
|
-
except ReceiptError as exc:
|
|
897
|
-
assert "requires exactly one -c sandbox_workspace_write.network_access=false" in str(exc)
|
|
898
|
-
else:
|
|
899
|
-
raise AssertionError("Codex cleanup accepted implicit network capability")
|
|
900
|
-
try:
|
|
901
|
-
start_receipt(
|
|
902
|
-
work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
|
|
903
|
-
str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
|
|
904
|
-
["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-test",
|
|
905
|
-
"-c", "sandbox_workspace_write.network_access=true", "do the task"],
|
|
906
|
-
)
|
|
907
|
-
except ReceiptError as exc:
|
|
908
|
-
assert "requires exactly one -c sandbox_workspace_write.network_access=false" in str(exc)
|
|
909
|
-
else:
|
|
910
|
-
raise AssertionError("Codex cleanup accepted enabled network capability")
|
|
911
|
-
try:
|
|
912
|
-
start_receipt(
|
|
913
|
-
work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
|
|
914
|
-
str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
|
|
915
|
-
["--json", "-s", "danger-full-access", "-m", "gpt-test", "do the task"],
|
|
916
|
-
)
|
|
917
|
-
except ReceiptError as exc:
|
|
918
|
-
assert "must remain workspace-write" in str(exc)
|
|
919
|
-
else:
|
|
920
|
-
raise AssertionError("widened Codex sandbox was accepted")
|
|
921
|
-
try:
|
|
922
|
-
start_receipt(
|
|
923
|
-
work, devlyn / "cleanup.invocation.0.json", "rs-receipt", "cleanup", 0,
|
|
924
|
-
str(cleanup_prompt), str(devlyn / "cleanup.worker-session.0.jsonl"),
|
|
925
|
-
["--json", "-C", str(devlyn), "-s", "workspace-write", "-m", "gpt-test",
|
|
926
|
-
"-c", "sandbox_workspace_write.network_access=false", "do the task"],
|
|
927
|
-
)
|
|
928
|
-
except ReceiptError as exc:
|
|
929
|
-
assert "workdir does not match" in str(exc)
|
|
930
|
-
else:
|
|
931
|
-
raise AssertionError("Codex invocation accepted a different working directory")
|
|
932
|
-
session.write_text('{"type":"thread.started","mutated":true}\n', encoding="utf-8")
|
|
933
|
-
try:
|
|
934
|
-
validate_receipt(
|
|
935
|
-
work, receipt, run_id="rs-receipt", phase="implement", round_=0,
|
|
936
|
-
model="gpt-test", prompt_sha256=sha256(prompt.read_bytes()), session_path=session,
|
|
937
|
-
)
|
|
938
|
-
except ReceiptError as exc:
|
|
939
|
-
assert "worker-session digest mismatch" in str(exc)
|
|
940
|
-
else:
|
|
941
|
-
raise AssertionError("mutated worker session was accepted")
|
|
942
|
-
|
|
943
|
-
build_prompt = devlyn / "build_gate.prompt.0"
|
|
944
|
-
build_prompt.write_text("verify the task\n", encoding="utf-8")
|
|
945
|
-
build_session = devlyn / "build_gate.worker-session.0.jsonl"
|
|
946
|
-
build_receipt = devlyn / "build_gate.invocation.0.json"
|
|
947
|
-
try:
|
|
948
|
-
start_receipt(
|
|
949
|
-
work, build_receipt, "rs-wrapper", "build_gate", 0,
|
|
950
|
-
str(build_prompt), str(build_session),
|
|
951
|
-
["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
|
|
952
|
-
"verify the task"],
|
|
953
|
-
)
|
|
954
|
-
except ReceiptError as exc:
|
|
955
|
-
assert "requires exactly one -c sandbox_workspace_write.network_access=true" in str(exc)
|
|
956
|
-
else:
|
|
957
|
-
raise AssertionError("Codex build_gate accepted missing network capability")
|
|
958
|
-
try:
|
|
959
|
-
start_receipt(
|
|
960
|
-
work, build_receipt, "rs-wrapper", "build_gate", 0,
|
|
961
|
-
str(build_prompt), str(build_session),
|
|
962
|
-
["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
|
|
963
|
-
"-c", "sandbox_workspace_write.network_access=true",
|
|
964
|
-
"-c", "sandbox_workspace_write.network_access=false",
|
|
965
|
-
"verify the task"],
|
|
966
|
-
)
|
|
967
|
-
except ReceiptError as exc:
|
|
968
|
-
assert "duplicate network overrides are forbidden" in str(exc)
|
|
969
|
-
else:
|
|
970
|
-
raise AssertionError("Codex build_gate accepted duplicate network capabilities")
|
|
971
|
-
for alternate in (
|
|
972
|
-
"sandbox_workspace_write={network_access=true}",
|
|
973
|
-
'"sandbox_workspace_write".network_access=true',
|
|
974
|
-
):
|
|
975
|
-
try:
|
|
976
|
-
start_receipt(
|
|
977
|
-
work, build_receipt, "rs-wrapper", "build_gate", 0,
|
|
978
|
-
str(build_prompt), str(build_session),
|
|
979
|
-
["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
|
|
980
|
-
"-c", "sandbox_workspace_write.network_access=true",
|
|
981
|
-
"--config=" + alternate, "verify the task"],
|
|
982
|
-
)
|
|
983
|
-
except ReceiptError as exc:
|
|
984
|
-
assert "alternate, table, missing, and duplicate" in str(exc)
|
|
985
|
-
else:
|
|
986
|
-
raise AssertionError(f"Codex build_gate accepted alternate override: {alternate}")
|
|
987
|
-
glued_prompt = devlyn / "build_gate.prompt.1"
|
|
988
|
-
glued_prompt.write_text("verify glued config\n", encoding="utf-8")
|
|
989
|
-
glued_session = devlyn / "build_gate.worker-session.1.jsonl"
|
|
990
|
-
glued_session.write_text('{"type":"thread.started"}\n', encoding="utf-8")
|
|
991
|
-
glued_receipt = devlyn / "build_gate.invocation.1.json"
|
|
992
|
-
start_receipt(
|
|
993
|
-
work, glued_receipt, "rs-glued", "build_gate", 1,
|
|
994
|
-
str(glued_prompt), str(glued_session),
|
|
995
|
-
["--json", "-C", str(work), "-s", "workspace-write", "-m", "gpt-wrapper",
|
|
996
|
-
"-c=sandbox_workspace_write.network_access=true", "verify glued config"],
|
|
997
|
-
)
|
|
998
|
-
finish_receipt(work, glued_receipt, 0)
|
|
999
|
-
assert validate_receipt(
|
|
1000
|
-
work, glued_receipt, run_id="rs-glued", phase="build_gate", round_=1,
|
|
1001
|
-
model="gpt-wrapper", prompt_sha256=sha256(glued_prompt.read_bytes()),
|
|
1002
|
-
session_path=glued_session,
|
|
1003
|
-
)["sandbox_network_access"] is True
|
|
1004
|
-
if os.name == "nt":
|
|
1005
|
-
print("SKIP POSIX executable-shell wrapper fixtures; native npm worker/judge transport is covered by test-windows-portability.py")
|
|
1006
|
-
return 0
|
|
1007
421
|
fake_codex = work / "fake-codex"
|
|
1008
|
-
fake_codex.write_text(
|
|
1009
|
-
"#!/usr/bin/env bash\n"
|
|
1010
|
-
"touch native-started\n"
|
|
1011
|
-
"printf '%s\\n' '{\"type\":\"thread.started\"}'\n",
|
|
1012
|
-
encoding="utf-8",
|
|
1013
|
-
)
|
|
422
|
+
fake_codex.write_text("#!/usr/bin/env bash\ntouch native-started\n", encoding="utf-8")
|
|
1014
423
|
fake_codex.chmod(0o755)
|
|
1015
424
|
wrapper = pathlib.Path(__file__).with_name("codex-monitored.sh")
|
|
1016
425
|
widened = subprocess.run(
|
|
1017
|
-
[
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
],
|
|
1021
|
-
cwd=work,
|
|
1022
|
-
env={**os.environ, "CODEX_BIN": str(fake_codex)},
|
|
1023
|
-
stdout=subprocess.PIPE,
|
|
1024
|
-
stderr=subprocess.PIPE,
|
|
1025
|
-
check=False,
|
|
1026
|
-
)
|
|
1027
|
-
assert widened.returncode == 64
|
|
1028
|
-
assert b"forbidden Codex sandbox" in widened.stderr
|
|
1029
|
-
assert widened.stdout == b""
|
|
1030
|
-
env = os.environ.copy()
|
|
1031
|
-
env.pop("DEVLYN_CODEX_PROMPT_FILE", None)
|
|
1032
|
-
env.update({
|
|
1033
|
-
"CODEX_BIN": str(fake_codex),
|
|
1034
|
-
"CODEX_MONITORED_HEARTBEAT": "1",
|
|
1035
|
-
"DEVLYN_INVOCATION_RUN_ID": "rs-wrapper",
|
|
1036
|
-
"DEVLYN_INVOCATION_PHASE": "build_gate",
|
|
1037
|
-
"DEVLYN_INVOCATION_ROUND": "0",
|
|
1038
|
-
"DEVLYN_INVOCATION_WORKDIR": str(work),
|
|
1039
|
-
"DEVLYN_INVOCATION_PROMPT_FILE": str(build_prompt),
|
|
1040
|
-
"DEVLYN_INVOCATION_SESSION_FILE": str(build_session),
|
|
1041
|
-
"DEVLYN_INVOCATION_RECEIPT": str(build_receipt),
|
|
1042
|
-
})
|
|
1043
|
-
with build_session.open("wb") as stdout:
|
|
1044
|
-
missing_json = subprocess.run(
|
|
1045
|
-
["bash", str(wrapper), "-C", str(work), "-s", "workspace-write",
|
|
1046
|
-
"-m", "gpt-wrapper", "-c",
|
|
1047
|
-
"sandbox_workspace_write.network_access=true", "verify the task"],
|
|
1048
|
-
cwd=work, env=env, stdout=stdout, stderr=subprocess.PIPE,
|
|
1049
|
-
check=False,
|
|
1050
|
-
)
|
|
1051
|
-
assert missing_json.returncode == 64 and b"requires --json" in missing_json.stderr, missing_json.stderr
|
|
1052
|
-
assert build_session.read_bytes() == b"" and not (work / "native-started").exists()
|
|
1053
|
-
assert not build_receipt.exists()
|
|
1054
|
-
with build_session.open("wb") as stdout:
|
|
1055
|
-
wrapped = subprocess.run(
|
|
1056
|
-
[
|
|
1057
|
-
"bash", str(wrapper), "--json", "-C", str(work), "-s", "workspace-write",
|
|
1058
|
-
"-m", "gpt-wrapper", "-c",
|
|
1059
|
-
"sandbox_workspace_write.network_access=true", "verify the task",
|
|
1060
|
-
],
|
|
1061
|
-
cwd=work,
|
|
1062
|
-
env=env,
|
|
1063
|
-
stdout=stdout,
|
|
1064
|
-
stderr=subprocess.PIPE,
|
|
1065
|
-
check=False,
|
|
1066
|
-
)
|
|
1067
|
-
assert wrapped.returncode == 0, wrapped.stderr.decode("utf-8", errors="replace")
|
|
1068
|
-
wrapper_bound = validate_receipt(
|
|
1069
|
-
work, build_receipt, run_id="rs-wrapper", phase="build_gate", round_=0,
|
|
1070
|
-
model="gpt-wrapper", prompt_sha256=sha256(build_prompt.read_bytes()),
|
|
1071
|
-
session_path=build_session,
|
|
1072
|
-
)
|
|
1073
|
-
assert wrapper_bound["exit_code"] == 0
|
|
1074
|
-
assert wrapper_bound["sandbox_network_access"] is True
|
|
1075
|
-
|
|
1076
|
-
wrapped_plan_prompt = devlyn / "plan.prompt.1"
|
|
1077
|
-
wrapped_plan_prompt.write_text("plan through wrapper\n", encoding="utf-8")
|
|
1078
|
-
wrapped_plan_session = devlyn / "plan.worker-session.1.jsonl"
|
|
1079
|
-
wrapped_plan_receipt = devlyn / "plan.invocation.1.json"
|
|
1080
|
-
plan_env = os.environ.copy()
|
|
1081
|
-
plan_env.pop("DEVLYN_CODEX_PROMPT_FILE", None)
|
|
1082
|
-
plan_env.update({
|
|
1083
|
-
"CODEX_BIN": str(fake_codex),
|
|
1084
|
-
"CODEX_MONITORED_HEARTBEAT": "1",
|
|
1085
|
-
"DEVLYN_INVOCATION_RUN_ID": "rs-plan-wrapper",
|
|
1086
|
-
"DEVLYN_INVOCATION_PHASE": "plan",
|
|
1087
|
-
"DEVLYN_INVOCATION_ROUND": "1",
|
|
1088
|
-
"DEVLYN_INVOCATION_WORKDIR": str(work),
|
|
1089
|
-
"DEVLYN_INVOCATION_PROMPT_FILE": str(wrapped_plan_prompt),
|
|
1090
|
-
"DEVLYN_INVOCATION_SESSION_FILE": str(wrapped_plan_session),
|
|
1091
|
-
"DEVLYN_INVOCATION_RECEIPT": str(wrapped_plan_receipt),
|
|
1092
|
-
})
|
|
1093
|
-
with wrapped_plan_session.open("wb") as stdout:
|
|
1094
|
-
wrapped_plan = subprocess.run(
|
|
1095
|
-
[
|
|
1096
|
-
"bash", str(wrapper), "--json", "-C", str(work),
|
|
1097
|
-
"-s", "workspace-write", "-m", "gpt-plan-wrapper",
|
|
1098
|
-
"-c", "sandbox_workspace_write.network_access=false",
|
|
1099
|
-
"plan through wrapper",
|
|
1100
|
-
],
|
|
1101
|
-
cwd=work,
|
|
1102
|
-
env=plan_env,
|
|
1103
|
-
stdout=stdout,
|
|
1104
|
-
stderr=subprocess.PIPE,
|
|
1105
|
-
check=False,
|
|
1106
|
-
)
|
|
1107
|
-
assert wrapped_plan.returncode == 0, wrapped_plan.stderr.decode(
|
|
1108
|
-
"utf-8", errors="replace",
|
|
1109
|
-
)
|
|
1110
|
-
plan_wrapper_bound = validate_receipt(
|
|
1111
|
-
work, wrapped_plan_receipt, run_id="rs-plan-wrapper", phase="plan",
|
|
1112
|
-
round_=1, model="gpt-plan-wrapper",
|
|
1113
|
-
prompt_sha256=sha256(wrapped_plan_prompt.read_bytes()),
|
|
1114
|
-
session_path=wrapped_plan_session,
|
|
1115
|
-
)
|
|
1116
|
-
assert plan_wrapper_bound["exit_code"] == 0
|
|
1117
|
-
print(
|
|
1118
|
-
"PASS invocation receipt identity, prompt/session digest, bypass guard, "
|
|
1119
|
-
"phase-scoped BUILD_GATE network capability, and monitored-wrapper "
|
|
1120
|
-
"integration including PLAN"
|
|
426
|
+
["bash", str(wrapper), "-C", str(work), "-s", "danger-full-access", "-m", "gpt-wrapper", "verify the task"],
|
|
427
|
+
cwd=work, env={**os.environ, "CODEX_BIN": str(fake_codex)},
|
|
428
|
+
stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False,
|
|
1121
429
|
)
|
|
430
|
+
assert widened.returncode == 64 and b"forbidden Codex sandbox" in widened.stderr, widened.stderr
|
|
431
|
+
assert widened.stdout == b"" and not (work / "native-started").exists()
|
|
432
|
+
print("PASS monitored-wrapper descendant cleanup and authority-widening refusal")
|
|
1122
433
|
return 0
|
|
1123
434
|
|
|
1124
435
|
|
|
@@ -1127,10 +438,6 @@ def complete_dispatch(exit_code):
|
|
|
1127
438
|
if prompt:
|
|
1128
439
|
path = pathlib.Path(prompt).resolve()
|
|
1129
440
|
finish_transport(path.with_name(path.name + ".transport.json"), exit_code)
|
|
1130
|
-
if os.environ.get("DEVLYN_INVOCATION_RECEIPT"):
|
|
1131
|
-
work = pathlib.Path(os.environ["DEVLYN_INVOCATION_WORKDIR"]).resolve()
|
|
1132
|
-
path = pathlib.Path(os.environ["DEVLYN_INVOCATION_RECEIPT"])
|
|
1133
|
-
finish_receipt(work, path if path.is_absolute() else work / path, exit_code)
|
|
1134
441
|
|
|
1135
442
|
|
|
1136
443
|
def dispatch_codex(args):
|
|
@@ -1139,23 +446,12 @@ def dispatch_codex(args):
|
|
|
1139
446
|
actual = PLATFORM["native_argv"](command)
|
|
1140
447
|
prompt = os.environ.get("DEVLYN_CODEX_PROMPT_FILE")
|
|
1141
448
|
if prompt == "":
|
|
1142
|
-
raise
|
|
449
|
+
raise TransportError("DEVLYN_CODEX_PROMPT_FILE must name a readable prompt file")
|
|
1143
450
|
stream, carrier, transport = None, None, None
|
|
1144
451
|
try:
|
|
1145
452
|
if prompt:
|
|
1146
453
|
file_prompt_args(argv)
|
|
1147
|
-
stream, carrier, transport = prepare_transport(
|
|
1148
|
-
prompt, command, actual, args.timeout,
|
|
1149
|
-
isolated=bool(os.environ.get("CODEX_MONITORED_ISOLATED")),
|
|
1150
|
-
)
|
|
1151
|
-
if os.environ.get("DEVLYN_INVOCATION_RECEIPT"):
|
|
1152
|
-
env = os.environ
|
|
1153
|
-
work = pathlib.Path(env["DEVLYN_INVOCATION_WORKDIR"]).resolve()
|
|
1154
|
-
path = pathlib.Path(env["DEVLYN_INVOCATION_RECEIPT"])
|
|
1155
|
-
start_receipt(work, path if path.is_absolute() else work / path,
|
|
1156
|
-
env["DEVLYN_INVOCATION_RUN_ID"], env["DEVLYN_INVOCATION_PHASE"],
|
|
1157
|
-
int(env["DEVLYN_INVOCATION_ROUND"]), env["DEVLYN_INVOCATION_PROMPT_FILE"],
|
|
1158
|
-
env["DEVLYN_INVOCATION_SESSION_FILE"], argv, transport=transport)
|
|
454
|
+
stream, carrier, transport = prepare_transport(prompt, command, actual, args.timeout)
|
|
1159
455
|
if transport is not None:
|
|
1160
456
|
write_transport(carrier, transport)
|
|
1161
457
|
if os.name != "nt":
|
|
@@ -1176,19 +472,6 @@ def main() -> int:
|
|
|
1176
472
|
parser = argparse.ArgumentParser(description=__doc__)
|
|
1177
473
|
parser.add_argument("--self-test", action="store_true")
|
|
1178
474
|
subparsers = parser.add_subparsers(dest="action")
|
|
1179
|
-
start = subparsers.add_parser("start")
|
|
1180
|
-
start.add_argument("--workdir", required=True)
|
|
1181
|
-
start.add_argument("--receipt", required=True)
|
|
1182
|
-
start.add_argument("--run-id", required=True)
|
|
1183
|
-
start.add_argument("--phase", choices=sorted(PHASES), required=True)
|
|
1184
|
-
start.add_argument("--round", type=int, required=True)
|
|
1185
|
-
start.add_argument("--prompt-file", required=True)
|
|
1186
|
-
start.add_argument("--session-file", required=True)
|
|
1187
|
-
start.add_argument("argv", nargs=argparse.REMAINDER)
|
|
1188
|
-
finish = subparsers.add_parser("finish")
|
|
1189
|
-
finish.add_argument("--workdir", required=True)
|
|
1190
|
-
finish.add_argument("--receipt", required=True)
|
|
1191
|
-
finish.add_argument("--exit-code", type=int, required=True)
|
|
1192
475
|
dispatch = subparsers.add_parser("dispatch")
|
|
1193
476
|
dispatch.add_argument("--binary", required=True)
|
|
1194
477
|
dispatch.add_argument("--timeout", type=int, required=True)
|
|
@@ -1204,20 +487,6 @@ def main() -> int:
|
|
|
1204
487
|
return dispatch_codex(args)
|
|
1205
488
|
elif args.action == "complete-dispatch":
|
|
1206
489
|
complete_dispatch(args.exit_code)
|
|
1207
|
-
elif args.action == "start":
|
|
1208
|
-
argv = args.argv[1:] if args.argv[:1] == ["--"] else args.argv
|
|
1209
|
-
start_receipt(
|
|
1210
|
-
pathlib.Path(args.workdir).resolve(), pathlib.Path(args.receipt), args.run_id,
|
|
1211
|
-
args.phase, args.round, args.prompt_file, args.session_file, argv,
|
|
1212
|
-
)
|
|
1213
|
-
elif args.action == "finish":
|
|
1214
|
-
work = pathlib.Path(args.workdir).resolve()
|
|
1215
|
-
receipt_path = pathlib.Path(args.receipt)
|
|
1216
|
-
if not receipt_path.is_absolute():
|
|
1217
|
-
receipt_path = work / receipt_path
|
|
1218
|
-
finish_receipt(
|
|
1219
|
-
work, receipt_path, args.exit_code,
|
|
1220
|
-
)
|
|
1221
490
|
else:
|
|
1222
491
|
parser.error("an action is required")
|
|
1223
492
|
except (OSError, UnicodeError, ValueError) as exc:
|