motionloom 2.6.0 → 2.7.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 +3 -1
- package/CHANGELOG.md +68 -0
- package/CONTRIBUTING.md +3 -1
- package/README.md +20 -4
- package/SECURITY.md +5 -5
- package/SKILL.md +15 -5
- package/agent-card.json +77 -14
- package/agent-surfaces.json +30 -7
- package/artifact-adapter-registry.json +568 -20
- package/bin/motionloom.mjs +25 -2
- package/capability-registry.json +58 -234
- package/dev-lab/public/devlab.js +36 -3
- package/dev-lab/public/index.html +6 -2
- package/docs/ACTION-SEPARATION.md +104 -0
- package/docs/ASSET-GENERATION-PLANNER.md +101 -0
- package/docs/BRANCH-PROTECTION.md +44 -0
- package/docs/EXTERNAL-CORPUS.md +26 -0
- package/docs/STATUS.md +3 -2
- package/docs/audits/field-test-after-hardening-2026-08-21.md +25 -0
- package/docs/releases/2.6.1.md +57 -0
- package/docs/releases/2.7.0.md +98 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.00.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.01.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.02.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/envelopes/walk.03.json +32 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/hero-walk-action-manifest.json +81 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.00.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.01.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.02.json +26 -0
- package/examples/agent-consumer/asset-consistency/action-sequence/verifier-evidence/walk.03.json +26 -0
- package/examples/agent-consumer/asset-planning/pixellab-hero-256x448-request.json +43 -0
- package/examples/agent-consumer/devlab-live-sprite/README.md +30 -0
- package/examples/agent-consumer/devlab-live-sprite/devlab-runtime.json +61 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-00.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-01.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/idle-02.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-00.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-01.png +0 -0
- package/examples/agent-consumer/devlab-live-sprite/frames/reverse-02.png +0 -0
- package/examples/agent-consumer/frame-generation-lock/hero-walk-lock.json +97 -0
- package/package.json +23 -9
- package/references/multi-frame-asset-generation.md +144 -0
- package/schemas/action-separation-verifier-evidence.schema.json +43 -0
- package/schemas/action-sequence-manifest.schema.json +48 -0
- package/schemas/artifact-adapter-registry.schema.json +1 -1
- package/schemas/asset-adaptation.schema.json +21 -0
- package/schemas/asset-generation-plan.schema.json +77 -0
- package/schemas/asset-generation-request.schema.json +104 -0
- package/schemas/frame-envelope.schema.json +62 -0
- package/schemas/frame-generation-lock.schema.json +189 -0
- package/scripts/action-separation.py +410 -0
- package/scripts/asset-adapt.mjs +92 -0
- package/scripts/asset-generation-plan.py +613 -0
- package/scripts/browser_review_consistency.py +64 -0
- package/scripts/fetch-project-corpus.py +91 -0
- package/scripts/frame-generation-lock.py +358 -0
- package/scripts/frame-set-preflight.py +264 -0
- package/scripts/package-consumer-smoke.mjs +20 -0
- package/scripts/quality-gate.py +7 -0
- package/scripts/release-verify.py +25 -0
- package/scripts/report-contract.py +1 -1
- package/scripts/report.py +19 -4
- package/scripts/resolve-task-bundle.py +11 -3
- package/scripts/review-hook.py +51 -2
- package/scripts/skill-doctor.py +42 -0
- package/src/output/browser-review-smoke/browser-review.json +6 -6
- package/tests/scripts/run_tests.py +45 -2
- package/tests/scripts/test_asset_adapt.py +47 -0
- package/tests/scripts/test_asset_consistency.py +77 -4
- package/tests/scripts/test_asset_generation_plan.py +250 -0
- package/tests/scripts/test_attestation.py +24 -8
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema_version": "1.0",
|
|
3
|
-
"candidate_id": "
|
|
4
|
-
"task_id": "
|
|
3
|
+
"candidate_id": "956caeb2f56397430c4f",
|
|
4
|
+
"task_id": "browser-review-smoke-task",
|
|
5
5
|
"scene": "browser-review-smoke",
|
|
6
|
-
"url": "
|
|
6
|
+
"url": "http://127.0.0.1:3300/?scene=browser-review-smoke&task_id=browser-review-smoke-task&candidate_id=956caeb2f56397430c4f",
|
|
7
7
|
"status": "approved",
|
|
8
8
|
"context_sha256": "cbbd43a53a0ddd5cf5452df5f6d521f06829465bd58c6d764a59d501ed1710fa",
|
|
9
9
|
"source_sha256": "c5ab312427678b17ae903d280d89db9d202670f031f10ad6fe774fc9aaecee8a",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
],
|
|
16
16
|
"review_artifact": "review.json",
|
|
17
17
|
"requires_user_approval": true,
|
|
18
|
-
"prepared_at": "2026-08-
|
|
19
|
-
"expires_at": "
|
|
20
|
-
"reviewed_at": "2026-08-
|
|
18
|
+
"prepared_at": "2026-08-12T14:29:17Z",
|
|
19
|
+
"expires_at": "2099-01-01T00:00:00Z",
|
|
20
|
+
"reviewed_at": "2026-08-12T14:32:51Z"
|
|
21
21
|
}
|
|
@@ -511,6 +511,9 @@ def test_approved_browser_review_e2e_contract():
|
|
|
511
511
|
candidate = json.loads(candidate_path.read_text())
|
|
512
512
|
candidate["expires_at"] = "2099-01-01T00:00:00Z"
|
|
513
513
|
candidate_path.write_text(json.dumps(candidate, indent=2) + "\n")
|
|
514
|
+
scene_candidate_path = root / "src/output/browser-review-smoke/browser-review.json"
|
|
515
|
+
scene_candidate = dict(candidate)
|
|
516
|
+
scene_candidate_path.write_text(json.dumps(scene_candidate, indent=2) + "\n")
|
|
514
517
|
|
|
515
518
|
review = json.loads((task_dir / "review.json").read_text())
|
|
516
519
|
task = json.loads((task_dir / "task.json").read_text())
|
|
@@ -524,7 +527,7 @@ def test_approved_browser_review_e2e_contract():
|
|
|
524
527
|
|
|
525
528
|
review_hook = subprocess.run([
|
|
526
529
|
sys.executable, str(ROOT / "scripts/review-hook.py"), "validate",
|
|
527
|
-
"--task-dir", str(task_dir), "--require-approved",
|
|
530
|
+
"--task-dir", str(task_dir), "--root", str(root), "--require-approved",
|
|
528
531
|
], capture_output=True, text=True)
|
|
529
532
|
check("e2e review hook accepts approved candidate", review_hook.returncode == 0, review_hook.stdout.strip())
|
|
530
533
|
|
|
@@ -536,7 +539,7 @@ def test_approved_browser_review_e2e_contract():
|
|
|
536
539
|
check("e2e quality gate accepts task evidence", quality.returncode == 0, quality.stdout.strip())
|
|
537
540
|
|
|
538
541
|
report_check = subprocess.run([
|
|
539
|
-
sys.executable, str(ROOT / "scripts/report.py"), "check", "--task-dir", str(task_dir),
|
|
542
|
+
sys.executable, str(ROOT / "scripts/report.py"), "check", "--task-dir", str(task_dir), "--root", str(root),
|
|
540
543
|
], capture_output=True, text=True)
|
|
541
544
|
check("e2e report contract accepts confirmed task", report_check.returncode == 0, report_check.stdout.strip())
|
|
542
545
|
|
|
@@ -1162,6 +1165,46 @@ if __name__ == "__main__":
|
|
|
1162
1165
|
test_category_coverage()
|
|
1163
1166
|
test_observability_contract()
|
|
1164
1167
|
test_quality_workflow_rebuilds_replay_after_generated_artifacts()
|
|
1168
|
+
browser_review_consistency_tests = subprocess.run(
|
|
1169
|
+
[sys.executable, str(ROOT / "tests/scripts/test_browser_review_consistency.py")],
|
|
1170
|
+
capture_output=True,
|
|
1171
|
+
text=True,
|
|
1172
|
+
)
|
|
1173
|
+
check(
|
|
1174
|
+
"browser review scene/task candidate consistency is fail-closed",
|
|
1175
|
+
browser_review_consistency_tests.returncode == 0 and "browser review consistency tests: PASS" in browser_review_consistency_tests.stdout,
|
|
1176
|
+
browser_review_consistency_tests.stdout.strip() or browser_review_consistency_tests.stderr.strip(),
|
|
1177
|
+
)
|
|
1178
|
+
action_separation_tests = subprocess.run(
|
|
1179
|
+
[sys.executable, str(ROOT / "tests/scripts/test_action_separation.py")],
|
|
1180
|
+
capture_output=True,
|
|
1181
|
+
text=True,
|
|
1182
|
+
)
|
|
1183
|
+
check(
|
|
1184
|
+
"action-scoped frame manifests reject cross-action and ambiguous frames",
|
|
1185
|
+
action_separation_tests.returncode == 0 and "action separation tests: PASS" in action_separation_tests.stdout,
|
|
1186
|
+
action_separation_tests.stdout.strip() or action_separation_tests.stderr.strip(),
|
|
1187
|
+
)
|
|
1188
|
+
asset_generation_plan_tests = subprocess.run(
|
|
1189
|
+
[sys.executable, str(ROOT / "tests/scripts/test_asset_generation_plan.py")],
|
|
1190
|
+
capture_output=True,
|
|
1191
|
+
text=True,
|
|
1192
|
+
)
|
|
1193
|
+
check(
|
|
1194
|
+
"asset planner recommends explicit provider and canvas adaptations",
|
|
1195
|
+
asset_generation_plan_tests.returncode == 0 and "asset generation planner contract tests: PASS" in asset_generation_plan_tests.stdout,
|
|
1196
|
+
asset_generation_plan_tests.stdout.strip() or asset_generation_plan_tests.stderr.strip(),
|
|
1197
|
+
)
|
|
1198
|
+
asset_adapt_tests = subprocess.run(
|
|
1199
|
+
[sys.executable, str(ROOT / "tests/scripts/test_asset_adapt.py")],
|
|
1200
|
+
capture_output=True,
|
|
1201
|
+
text=True,
|
|
1202
|
+
)
|
|
1203
|
+
check(
|
|
1204
|
+
"asset adaptation preserves target geometry without crop or stretch",
|
|
1205
|
+
asset_adapt_tests.returncode == 0 and "asset adaptation contract tests: PASS" in asset_adapt_tests.stdout,
|
|
1206
|
+
asset_adapt_tests.stdout.strip() or asset_adapt_tests.stderr.strip(),
|
|
1207
|
+
)
|
|
1165
1208
|
print()
|
|
1166
1209
|
if FAILED:
|
|
1167
1210
|
print(f"{len(FAILED)} test(s) FAILED: {', '.join(FAILED)}")
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import subprocess
|
|
5
|
+
import sys
|
|
6
|
+
import tempfile
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
10
|
+
SCRIPT = ROOT / "scripts" / "asset-adapt.mjs"
|
|
11
|
+
SOURCE = ROOT / "examples/agent-consumer/asset-consistency/assets/hero-frame-00.png"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def check(condition: bool, message: str) -> None:
|
|
15
|
+
if not condition:
|
|
16
|
+
raise AssertionError(message)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def main() -> int:
|
|
20
|
+
with tempfile.TemporaryDirectory() as td:
|
|
21
|
+
root = Path(td)
|
|
22
|
+
output = root / "padded.png"
|
|
23
|
+
report = root / "adaptation.json"
|
|
24
|
+
result = subprocess.run([
|
|
25
|
+
"node", str(SCRIPT), "pad", "--input", str(SOURCE), "--output", str(output),
|
|
26
|
+
"--width", "16", "--height", "16", "--scale", "1", "--anchor", "center",
|
|
27
|
+
"--report", str(report), "--json",
|
|
28
|
+
], capture_output=True, text=True)
|
|
29
|
+
check(result.returncode == 0, result.stderr)
|
|
30
|
+
doc = json.loads(report.read_text())
|
|
31
|
+
check(doc["approval"] is False and doc["production_approved"] is False, "adaptation cannot grant approval")
|
|
32
|
+
check(doc["source"]["canvas"] == [8, 8] and doc["output"]["canvas"] == [16, 16], "adaptation must bind source/output geometry")
|
|
33
|
+
check(doc["transform"]["crop"] is False and doc["transform"]["stretch"] is False, "crop/stretch must remain false")
|
|
34
|
+
check(doc["transform"]["interpolation"] == "nearest-neighbour", "pixel art must use nearest-neighbour")
|
|
35
|
+
check(output.is_file() and output.stat().st_size > 0, "adapter must emit target PNG")
|
|
36
|
+
|
|
37
|
+
blocked = subprocess.run([
|
|
38
|
+
"node", str(SCRIPT), "pad", "--input", str(SOURCE), "--output", str(root / "blocked.png"),
|
|
39
|
+
"--width", "4", "--height", "4", "--json",
|
|
40
|
+
], capture_output=True, text=True)
|
|
41
|
+
check(blocked.returncode != 0 and "crop is forbidden" in blocked.stderr, "oversized source must fail closed")
|
|
42
|
+
print("asset adaptation contract tests: PASS")
|
|
43
|
+
return 0
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == "__main__":
|
|
47
|
+
raise SystemExit(main())
|
|
@@ -18,19 +18,21 @@ from pathlib import Path
|
|
|
18
18
|
ROOT = Path(__file__).resolve().parents[2]
|
|
19
19
|
FIXTURE_ROOT = ROOT / "examples/agent-consumer/asset-consistency"
|
|
20
20
|
ANALYZER_PATH = ROOT / "scripts/asset-consistency.py"
|
|
21
|
+
PREFLIGHT_PATH = ROOT / "scripts/frame-set-preflight.py"
|
|
21
22
|
|
|
22
23
|
|
|
23
|
-
def
|
|
24
|
-
spec = importlib.util.spec_from_file_location(
|
|
24
|
+
def load_module(path: Path, name: str):
|
|
25
|
+
spec = importlib.util.spec_from_file_location(name, path)
|
|
25
26
|
if spec is None or spec.loader is None:
|
|
26
|
-
raise RuntimeError("cannot load
|
|
27
|
+
raise RuntimeError(f"cannot load {path.name}")
|
|
27
28
|
module = importlib.util.module_from_spec(spec)
|
|
28
29
|
sys.modules[spec.name] = module
|
|
29
30
|
spec.loader.exec_module(module)
|
|
30
31
|
return module
|
|
31
32
|
|
|
32
33
|
|
|
33
|
-
AC =
|
|
34
|
+
AC = load_module(ANALYZER_PATH, "asset_consistency")
|
|
35
|
+
PREFLIGHT = load_module(PREFLIGHT_PATH, "frame_set_preflight")
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
def read_json(name: str) -> dict:
|
|
@@ -112,6 +114,65 @@ def test_frame_contamination_and_pivot_drift() -> None:
|
|
|
112
114
|
check(not result["ready"] and any(item["code"] == "pivot_drift" for item in result["errors"]), "pivot drift must block")
|
|
113
115
|
|
|
114
116
|
|
|
117
|
+
def test_generated_frame_set_preflight() -> None:
|
|
118
|
+
geometry = read_json("hero-walk-frame-geometry.json")
|
|
119
|
+
result = PREFLIGHT.validate(geometry, FIXTURE_ROOT)
|
|
120
|
+
check(result["ready"], f"isolated generated frame fixture must pass preflight: {result}")
|
|
121
|
+
check(result.get("approval") is False, "preflight must never emit approval")
|
|
122
|
+
|
|
123
|
+
shared = read_json("hero-walk-frame-geometry.json")
|
|
124
|
+
shared["frames"][1]["image"] = shared["frames"][0]["image"]
|
|
125
|
+
shared["frames"][1]["sha256"] = shared["frames"][0]["sha256"]
|
|
126
|
+
result = PREFLIGHT.validate(shared, FIXTURE_ROOT)
|
|
127
|
+
check(
|
|
128
|
+
not result["ready"] and any(item["code"] == "shared_source_image" for item in result["errors"]),
|
|
129
|
+
"generated multi-frame source must reject a shared pose-sheet image",
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
guard = read_json("hero-walk-frame-geometry.json")
|
|
133
|
+
guard["frames"][0]["safe_rect"] = {"x": 2, "y": 1, "width": 4, "height": 5}
|
|
134
|
+
guard["frames"][0]["bleed_margin_px"] = 1
|
|
135
|
+
result = PREFLIGHT.validate(guard, FIXTURE_ROOT)
|
|
136
|
+
check(
|
|
137
|
+
not result["ready"] and any(item["code"] == "guard_band_violation" for item in result["errors"]),
|
|
138
|
+
"generated frame alpha must preserve the declared transparent guard band",
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
with tempfile.TemporaryDirectory() as td:
|
|
142
|
+
root = Path(td)
|
|
143
|
+
geometry = read_json("hero-walk-frame-geometry.json")
|
|
144
|
+
geometry["frames"] = [copy.deepcopy(geometry["frames"][0]), copy.deepcopy(geometry["frames"][1])]
|
|
145
|
+
geometry["invariants"]["bbox_drift_tolerance_px"] = 0
|
|
146
|
+
for frame in geometry["frames"]:
|
|
147
|
+
frame["rect"] = {"x": 0, "y": 0, "width": 8, "height": 8}
|
|
148
|
+
frame["safe_rect"] = {"x": 0, "y": 0, "width": 8, "height": 8}
|
|
149
|
+
frame["bleed_margin_px"] = 0
|
|
150
|
+
|
|
151
|
+
first_pixels = solid(8, 8, (0, 0, 0, 0))
|
|
152
|
+
for y in range(1, 6):
|
|
153
|
+
for x in range(2, 6):
|
|
154
|
+
first_pixels[y * 8 + x] = (120, 160, 220, 255)
|
|
155
|
+
second_pixels = solid(8, 8, (0, 0, 0, 0))
|
|
156
|
+
for y in range(1, 6):
|
|
157
|
+
for x in range(1, 7):
|
|
158
|
+
second_pixels[y * 8 + x] = (120, 160, 220, 255)
|
|
159
|
+
write_rgba_png(root / "frame-00.png", 8, 8, first_pixels)
|
|
160
|
+
write_rgba_png(root / "frame-01.png", 8, 8, second_pixels)
|
|
161
|
+
|
|
162
|
+
geometry["frames"][0]["image"] = "frame-00.png"
|
|
163
|
+
geometry["frames"][0]["alpha_bbox"] = {"x": 2, "y": 1, "width": 4, "height": 5}
|
|
164
|
+
geometry["frames"][0]["sha256"] = hashlib.sha256((root / "frame-00.png").read_bytes()).hexdigest()
|
|
165
|
+
geometry["frames"][1]["image"] = "frame-01.png"
|
|
166
|
+
geometry["frames"][1]["alpha_bbox"] = {"x": 1, "y": 1, "width": 6, "height": 5}
|
|
167
|
+
geometry["frames"][1]["sha256"] = hashlib.sha256((root / "frame-01.png").read_bytes()).hexdigest()
|
|
168
|
+
|
|
169
|
+
result = PREFLIGHT.validate(geometry, root)
|
|
170
|
+
check(
|
|
171
|
+
not result["ready"] and any(item["code"] == "bbox_drift" for item in result["errors"]),
|
|
172
|
+
"generated frame apparent-size drift beyond tolerance must block instead of warning",
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
|
|
115
176
|
def test_atlas_overlap_and_contamination() -> None:
|
|
116
177
|
atlas = read_json("hero-atlas-contract.json")
|
|
117
178
|
atlas["regions"][1]["rect"]["x"] = 4
|
|
@@ -179,11 +240,23 @@ def test_missing_file_and_cli_surface() -> None:
|
|
|
179
240
|
data = json.loads(cli.stdout)
|
|
180
241
|
check(cli.returncode == 0 and data.get("ready") is True and data.get("contract") == "layered_map", "CLI must expose the consistency validator")
|
|
181
242
|
|
|
243
|
+
preflight_cli = subprocess.run([
|
|
244
|
+
sys.executable, str(PREFLIGHT_PATH),
|
|
245
|
+
"--input", str(FIXTURE_ROOT / "hero-walk-frame-geometry.json"),
|
|
246
|
+
"--root", str(FIXTURE_ROOT), "--json",
|
|
247
|
+
], cwd=ROOT, capture_output=True, text=True)
|
|
248
|
+
preflight_data = json.loads(preflight_cli.stdout)
|
|
249
|
+
check(
|
|
250
|
+
preflight_cli.returncode == 0 and preflight_data.get("ready") is True and preflight_data.get("approval") is False,
|
|
251
|
+
"generated frame-set preflight CLI must pass isolated fixture without granting approval",
|
|
252
|
+
)
|
|
253
|
+
|
|
182
254
|
|
|
183
255
|
def main() -> int:
|
|
184
256
|
test_pass_fixtures()
|
|
185
257
|
test_identity_and_action_fail_closed()
|
|
186
258
|
test_frame_contamination_and_pivot_drift()
|
|
259
|
+
test_generated_frame_set_preflight()
|
|
187
260
|
test_atlas_overlap_and_contamination()
|
|
188
261
|
test_layered_map_order_seam_and_bounds()
|
|
189
262
|
test_missing_file_and_cli_surface()
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import importlib.util
|
|
5
|
+
import json
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import tempfile
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
ROOT = Path(__file__).resolve().parents[2]
|
|
12
|
+
MODULE_PATH = ROOT / "scripts" / "asset-generation-plan.py"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def load_module():
|
|
16
|
+
spec = importlib.util.spec_from_file_location("asset_generation_plan", MODULE_PATH)
|
|
17
|
+
if spec is None or spec.loader is None:
|
|
18
|
+
raise RuntimeError("cannot load asset generation planner")
|
|
19
|
+
module = importlib.util.module_from_spec(spec)
|
|
20
|
+
sys.modules[spec.name] = module
|
|
21
|
+
spec.loader.exec_module(module)
|
|
22
|
+
return module
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MODULE = load_module()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def check(condition: bool, message: str) -> None:
|
|
29
|
+
if not condition:
|
|
30
|
+
raise AssertionError(message)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def request(canvas=(256, 448), frame_count=8, isolation="required", preferences=None):
|
|
34
|
+
value = {
|
|
35
|
+
"schema_version": "0.1",
|
|
36
|
+
"request_id": "test-request",
|
|
37
|
+
"asset_id": "test-hero-attack",
|
|
38
|
+
"asset_kind": "frame_sequence",
|
|
39
|
+
"target": {
|
|
40
|
+
"canvas": {"width": canvas[0], "height": canvas[1]},
|
|
41
|
+
"frame_count": frame_count,
|
|
42
|
+
"fps": 8,
|
|
43
|
+
"alpha_mode": "straight",
|
|
44
|
+
"pixel_art": True,
|
|
45
|
+
"anchor": "footline",
|
|
46
|
+
},
|
|
47
|
+
"generation_policy": {
|
|
48
|
+
"frame_isolation": isolation,
|
|
49
|
+
"allow_crop": False,
|
|
50
|
+
"allow_stretch": False,
|
|
51
|
+
"allow_silent_resize": False,
|
|
52
|
+
"allow_provider_batch_as_provisional": True,
|
|
53
|
+
"integer_scale_only": True,
|
|
54
|
+
},
|
|
55
|
+
"actions": [{"action_id": "attack", "positive_cues": ["sword swing"], "negative_cues": ["walk", "jump"]}],
|
|
56
|
+
}
|
|
57
|
+
if preferences is not None:
|
|
58
|
+
value["provider_preferences"] = preferences
|
|
59
|
+
return value
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def verified_adapter(canvas=(256, 448)):
|
|
63
|
+
return {
|
|
64
|
+
"adapter_id": "verified.single-frame",
|
|
65
|
+
"kind": "external_provider",
|
|
66
|
+
"status": "verified",
|
|
67
|
+
"adapter_version": "0.1",
|
|
68
|
+
"invocation_mode": "api",
|
|
69
|
+
"cost_class": "external",
|
|
70
|
+
"inputs": ["reference"],
|
|
71
|
+
"outputs": ["frame_sequence"],
|
|
72
|
+
"compatibility": {"os": ["linux"]},
|
|
73
|
+
"availability": {"status": "available", "environment": "test"},
|
|
74
|
+
"capabilities": {
|
|
75
|
+
"canvas": {"shapes": ["portrait" if canvas[1] > canvas[0] else "landscape" if canvas[0] > canvas[1] else "square"], "allowed_sizes": [[canvas[0], canvas[1]]], "max_width": canvas[0], "max_height": canvas[1]},
|
|
76
|
+
"frame_behavior": {"mode": "single_frame", "single_frame": True, "max_frames_per_request": 1},
|
|
77
|
+
"adaptation": [],
|
|
78
|
+
},
|
|
79
|
+
"evidence": [{"path": "evidence.md", "sha256": "a" * 64, "kind": "static"}],
|
|
80
|
+
"limitations": [],
|
|
81
|
+
"risk_level": "low",
|
|
82
|
+
"side_effect_level": "read",
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def registry_with(adapters, require_verified=True, allow_scaffold_only=False):
|
|
87
|
+
return {
|
|
88
|
+
"schema_version": "0.1",
|
|
89
|
+
"registry_id": "test-registry",
|
|
90
|
+
"generated_at": "2026-08-22T00:00:00Z",
|
|
91
|
+
"selection_policy": {"require_verified": require_verified, "allow_scaffold_only": allow_scaffold_only},
|
|
92
|
+
"adapters": adapters,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def by_id(plan, adapter_id):
|
|
97
|
+
return next(item for item in plan["providers"] if item["adapter_id"] == adapter_id)
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def main() -> int:
|
|
101
|
+
production_registry = json.loads((ROOT / "artifact-adapter-registry.json").read_text())
|
|
102
|
+
plan_schema = json.loads((ROOT / "schemas" / "asset-generation-plan.schema.json").read_text())
|
|
103
|
+
check(plan_schema["properties"]["producer"]["const"] == "MotionLoom" and plan_schema["properties"]["schema_version"]["const"] == "0.2", "published plan schema must bind MotionLoom identity and version")
|
|
104
|
+
with tempfile.TemporaryDirectory() as td:
|
|
105
|
+
root = Path(td)
|
|
106
|
+
request_path = root / "request.json"
|
|
107
|
+
request_path.write_text(json.dumps(request()))
|
|
108
|
+
registry_path = root / "registry.json"
|
|
109
|
+
registry_path.write_text(json.dumps(production_registry))
|
|
110
|
+
|
|
111
|
+
plan = MODULE.build_plan(request(), production_registry, root)
|
|
112
|
+
check(plan["contract"] == "motionloom-asset-generation-plan" and plan["schema_version"] == "0.2", "plan contract must identify MotionLoom plan 0.2")
|
|
113
|
+
check(plan["producer"] == "MotionLoom" and plan["identity"]["product"] == "MotionLoom", "structured plan must preserve MotionLoom identity")
|
|
114
|
+
check(plan["approval"] is False and plan["production_approved"] is False, "planner must never grant approval")
|
|
115
|
+
check(plan["decision"] == "recommendations_available", "normal planning must remain useful without verified execution provider")
|
|
116
|
+
check(plan["execution_decision"] == "no_execution_eligible_route", "default verified-only execution must remain fail-closed")
|
|
117
|
+
check(plan["selection"]["eligible_count"] == 0, "default registry must have no eligible execution provider")
|
|
118
|
+
check(plan["selection"]["provisional_count"] >= 4, "scaffold/manual routes must remain visible as provisional")
|
|
119
|
+
check(plan["selection"]["recommendation_count"] >= 3, "normal planning must expose useful ranked recommendations")
|
|
120
|
+
check("fixture.local-artifact-intake" not in plan["selection"]["recommendation_adapter_ids"], "regression fixture must not be recommended as a generation route")
|
|
121
|
+
check(plan["recommendations"], "normal planning must not collapse to no provider available")
|
|
122
|
+
check(all(item["recommendation_status"] in {"recommended", "acceptable"} for item in plan["recommendations"]), "recommendation list must exclude not-recommended routes")
|
|
123
|
+
check(all(item["approval"] is False for item in plan["recommendations"]), "recommendations must preserve approval=false")
|
|
124
|
+
check(plan["agent_guidance"]["recommended_by"] == "MotionLoom", "plan-level guidance must identify MotionLoom")
|
|
125
|
+
check(plan["project"]["runtime"] is None and plan["project"]["framework"] is None, "synthetic test request without project context should remain explicit")
|
|
126
|
+
check(any("MotionLoom" in step for step in plan["next_steps"]), "next steps must use MotionLoom workflow identity")
|
|
127
|
+
|
|
128
|
+
pix = by_id(plan, "pixellab.animate-skeleton")
|
|
129
|
+
check(pix["recommendation_status"] == "recommended" and pix["execution_status"] == "provisional", "PixelLab may be recommended while execution remains provisional")
|
|
130
|
+
check(pix["execution_eligible"] is False, "PixelLab must not be execution-eligible under verified-only policy")
|
|
131
|
+
check(pix["canvas"]["status"] == "adaptation_required", "PixelLab skeleton must reject non-square native target")
|
|
132
|
+
check(pix["frames"]["status"] == "provisional_batch_only", "batch PixelLab output must remain provisional under required isolation")
|
|
133
|
+
pad = next(option for option in pix["adaptation_options"] if option["id"] == "deterministic-pad-to-target")
|
|
134
|
+
check(pad["source_canvas"] == [256, 256] and pad["target_canvas"] == [256, 448], "planner must propose explicit 256x256 to 256x448 padding")
|
|
135
|
+
check(pad["crop"] is False and pad["stretch"] is False, "planner must forbid crop and stretch")
|
|
136
|
+
check("action-separation" in pad["requires_validation"], "adaptation must require action separation validation")
|
|
137
|
+
check(pix["availability"]["status"] == "unknown", "PixelLab availability must not be fabricated")
|
|
138
|
+
check(pix["agent_guidance"]["recommended_by"] == "MotionLoom", "route guidance must identify MotionLoom")
|
|
139
|
+
|
|
140
|
+
manual = by_id(plan, "manual.import-frame-sequence")
|
|
141
|
+
check(manual["recommendation_status"] in {"recommended", "acceptable"}, "manual fallback must remain a normal planning option")
|
|
142
|
+
check(manual["execution_status"] == "provisional", "manual route remains provisional without runtime evidence")
|
|
143
|
+
check(manual["availability"]["status"] == "known", "manual route availability should be explicit")
|
|
144
|
+
|
|
145
|
+
create_text = by_id(plan, "pixellab.create-animated-object-character")
|
|
146
|
+
check(create_text["frames"]["frame_count_policy"] == "depends_on_canvas_size", "PixelLab create-from-text frame policy must be dynamic")
|
|
147
|
+
create_text_adapter = next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.create-animated-object-character")
|
|
148
|
+
small_canvas_frames = MODULE.frame_assessment(create_text_adapter, MODULE.target_summary(request(canvas=(128, 128), frame_count=4)), request()["generation_policy"])
|
|
149
|
+
check(small_canvas_frames["max_frames_per_request"] == 4 and small_canvas_frames["limits_by_canvas"], "planner must resolve declared dynamic frame limit for an exact PixelLab canvas")
|
|
150
|
+
existing_text_adapter = next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.animate-with-text")
|
|
151
|
+
existing_text = by_id(plan, "pixellab.animate-with-text")
|
|
152
|
+
check(existing_text["frames"]["frame_count_policy"] == "fixed" and existing_text["frames"]["max_frames_per_request"] == 4, "PixelLab animate-existing-reference route must model its documented four-frame output")
|
|
153
|
+
|
|
154
|
+
preferred_request = request(preferences={"preferred_adapter_ids": ["pixellab.animate-skeleton"]})
|
|
155
|
+
preferred_plan = MODULE.build_plan(preferred_request, production_registry, root)
|
|
156
|
+
preferred_pix = by_id(preferred_plan, "pixellab.animate-skeleton")
|
|
157
|
+
check(preferred_pix["user_preference"] == {"state": "preferred", "requested": True}, "explicit user preference must be visible")
|
|
158
|
+
check(preferred_pix["recommendation_status"] == "recommended", "preferred provisional provider may be recommended")
|
|
159
|
+
check(preferred_pix["execution_status"] == "provisional" and preferred_pix["execution_eligible"] is False, "preference must not promote provisional execution")
|
|
160
|
+
check(any("preferred by the user" in reason for reason in preferred_pix["rationale"]), "rationale must explain preferred route")
|
|
161
|
+
check(preferred_pix["ranking_score"] >= 8 and any(item["adapter_id"] == "pixellab.animate-skeleton" for item in preferred_plan["recommendations"]), "preferred compatible provisional route should receive a strong explainable ranking signal without overriding a safer native route")
|
|
162
|
+
|
|
163
|
+
incompatible_request = request(preferences={"preferred_adapter_ids": ["internal.imagegen"]})
|
|
164
|
+
incompatible_plan = MODULE.build_plan(incompatible_request, production_registry, root)
|
|
165
|
+
incompatible = by_id(incompatible_plan, "internal.imagegen")
|
|
166
|
+
check(incompatible["user_preference"]["state"] == "preferred", "incompatible preference must remain visible")
|
|
167
|
+
check(incompatible["recommendation_status"] == "not_recommended" and incompatible["execution_status"] == "blocked", "hard-incompatible preferred route must be blocked")
|
|
168
|
+
check("internal.imagegen" not in incompatible_plan["selection"]["recommendation_adapter_ids"], "incompatible preferred route must not be recommended")
|
|
169
|
+
check(incompatible_plan["recommendations"], "safer alternatives must remain available")
|
|
170
|
+
check(any("does not declare canvas capability" in reason for reason in incompatible["rationale"]), "incompatibility rationale must explain missing canvas capability")
|
|
171
|
+
|
|
172
|
+
excluded_request = request(preferences={"excluded_adapter_ids": ["manual.import-frame-sequence"]})
|
|
173
|
+
excluded_plan = MODULE.build_plan(excluded_request, production_registry, root)
|
|
174
|
+
excluded = by_id(excluded_plan, "manual.import-frame-sequence")
|
|
175
|
+
check(excluded["user_preference"]["state"] == "excluded" and excluded["recommendation_status"] == "not_recommended", "excluded preference must be honored without affecting other routes")
|
|
176
|
+
check("manual.import-frame-sequence" not in excluded_plan["selection"]["recommendation_adapter_ids"], "excluded route must not be recommended")
|
|
177
|
+
|
|
178
|
+
unavailable_registry = registry_with([copy.deepcopy(next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.animate-skeleton"))], require_verified=False, allow_scaffold_only=True)
|
|
179
|
+
unavailable_registry["adapters"][0]["availability"] = {"status": "unavailable", "reason": "connector not configured"}
|
|
180
|
+
unavailable_plan = MODULE.build_plan(request(), unavailable_registry, root)
|
|
181
|
+
unavailable = unavailable_plan["providers"][0]
|
|
182
|
+
check(unavailable["recommendation_status"] == "recommended" and unavailable["execution_status"] == "blocked", "unavailable compatible route may remain a recommendation but cannot execute")
|
|
183
|
+
check(unavailable_plan["recommendations"][0]["route"] == "resolve_availability_first", "unavailable route must instruct the Agent to resolve availability")
|
|
184
|
+
|
|
185
|
+
unknown_preference = request(preferences={"preferred_adapter_ids": ["future.provider"]})
|
|
186
|
+
unknown_plan = MODULE.build_plan(unknown_preference, production_registry, root)
|
|
187
|
+
check(unknown_plan["selection"]["unknown_preferred_adapter_ids"] == ["future.provider"], "unknown preferred provider must be surfaced without fabrication")
|
|
188
|
+
check(any("future.provider" in warning for warning in unknown_plan["warnings"]), "unknown preference warning must be human-readable")
|
|
189
|
+
|
|
190
|
+
cli = subprocess.run([
|
|
191
|
+
sys.executable, str(MODULE_PATH), "plan", "--request", str(request_path), "--registry", str(registry_path), "--project-root", str(root), "--json", "--strict"
|
|
192
|
+
], capture_output=True, text=True)
|
|
193
|
+
strict_output = json.loads(cli.stdout)
|
|
194
|
+
check(cli.returncode == 2 and strict_output["execution_decision"] == "no_execution_eligible_route", "strict mode must fail without execution-eligible provider")
|
|
195
|
+
|
|
196
|
+
human = subprocess.run([
|
|
197
|
+
sys.executable, str(MODULE_PATH), "plan", "--request", str(request_path), "--registry", str(registry_path), "--project-root", str(root)
|
|
198
|
+
], capture_output=True, text=True)
|
|
199
|
+
check(human.returncode == 0 and "MotionLoom Project Assessment" in human.stdout and "MotionLoom Recommendations" in human.stdout and "MotionLoom Agent Guidance" in human.stdout, "human CLI must expose MotionLoom identity and guidance")
|
|
200
|
+
|
|
201
|
+
permitted = copy.deepcopy(production_registry)
|
|
202
|
+
permitted["selection_policy"] = {"require_verified": False, "allow_scaffold_only": True}
|
|
203
|
+
next(item for item in permitted["adapters"] if item["adapter_id"] == "pixellab.animate-skeleton")["availability"] = {"status": "available", "environment": "test-connector"}
|
|
204
|
+
permitted_plan = MODULE.build_plan(request(), permitted, root)
|
|
205
|
+
permitted_pix = by_id(permitted_plan, "pixellab.animate-skeleton")
|
|
206
|
+
check(permitted_pix["execution_status"] == "provisional" and permitted_pix["execution_eligible"] is True, "policy may authorize scaffold execution while preserving provisional status")
|
|
207
|
+
check(any(item["adapter_id"] == "pixellab.animate-skeleton" for item in permitted_plan["recommendations"]), "permitted scaffold provider may be ranked")
|
|
208
|
+
|
|
209
|
+
verified = verified_adapter()
|
|
210
|
+
verified_plan = MODULE.build_plan(request(), registry_with([verified]), root)
|
|
211
|
+
check(verified_plan["decision"] == "compatible_execution_route_available", "verified native provider must produce execution route")
|
|
212
|
+
check(verified_plan["selection"]["eligible_adapter_ids"] == ["verified.single-frame"], "verified provider must be execution-eligible")
|
|
213
|
+
check(verified_plan["recommendations"][0]["execution_status"] == "verified" and verified_plan["recommendations"][0]["route"] == "use_native", "verified native route must be recommended with verified status")
|
|
214
|
+
|
|
215
|
+
unknown = copy.deepcopy(production_registry)
|
|
216
|
+
unknown["adapters"] = [next(item for item in production_registry["adapters"] if item["adapter_id"] == "internal.imagegen")]
|
|
217
|
+
unknown_plan = MODULE.build_plan(request(), unknown, root)
|
|
218
|
+
unknown_item = unknown_plan["providers"][0]
|
|
219
|
+
check(unknown_plan["decision"] == "no_safe_recommendation_available", "unknown-only provider set must not produce a false recommendation")
|
|
220
|
+
check(unknown_item["execution_status"] == "blocked" and any("does not declare canvas capability" in warning for warning in unknown_item["warnings"]), "unknown capabilities must be blocked and surfaced")
|
|
221
|
+
|
|
222
|
+
oversized = copy.deepcopy(next(item for item in production_registry["adapters"] if item["adapter_id"] == "pixellab.animate-skeleton"))
|
|
223
|
+
oversized["adapter_id"] = "provider.oversized-square"
|
|
224
|
+
oversized["capabilities"]["canvas"]["allowed_sizes"] = [[384, 384]]
|
|
225
|
+
oversized_plan = MODULE.build_plan(request(), registry_with([oversized], require_verified=False, allow_scaffold_only=True), root)
|
|
226
|
+
oversized_assessment = oversized_plan["providers"][0]
|
|
227
|
+
check(oversized_assessment["execution_status"] == "blocked", "source exceeding target width must be blocked")
|
|
228
|
+
check(not oversized_assessment["adaptation_options"] and "no declared safe canvas adaptation strategy" in oversized_assessment["hard_failures"], "384x384 source must not be recommended for 256x448 padding")
|
|
229
|
+
|
|
230
|
+
landscape_plan = MODULE.build_plan(request(canvas=(448, 256), frame_count=2), registry_with([existing_text_adapter], require_verified=False, allow_scaffold_only=True), root)
|
|
231
|
+
landscape = landscape_plan["providers"][0]
|
|
232
|
+
check(any(option["target_canvas"] == [448, 256] for option in landscape["adaptation_options"]), "landscape target must receive explicit adaptation assessment")
|
|
233
|
+
|
|
234
|
+
request_path.write_text(json.dumps(request(canvas=(128, 128), frame_count=1)))
|
|
235
|
+
contextual = copy.deepcopy(request())
|
|
236
|
+
contextual["project_context"] = {"runtime": "game-runtime", "framework": "canvas", "existing_assets": ["assets/hero.png"], "rig_requirements": ["footline"], "provenance_requirements": ["receipt"], "validation_requirements": ["MotionLoom Dev Lab"]}
|
|
237
|
+
contextual_plan = MODULE.build_plan(contextual, production_registry, root)
|
|
238
|
+
check(contextual_plan["project"]["runtime"] == "game-runtime" and contextual_plan["project"]["framework"] == "canvas", "project context must flow into MotionLoom assessment")
|
|
239
|
+
check(contextual_plan["project"]["requested_existing_assets"] == ["assets/hero.png"] and contextual_plan["project"]["validation_requirements"] == ["MotionLoom Dev Lab"], "project asset and validation requirements must remain visible")
|
|
240
|
+
|
|
241
|
+
cli_native = subprocess.run([
|
|
242
|
+
sys.executable, str(MODULE_PATH), "plan", "--request", str(request_path), "--registry", str(registry_path), "--project-root", str(root), "--json"
|
|
243
|
+
], capture_output=True, text=True)
|
|
244
|
+
check(cli_native.returncode == 0 and json.loads(cli_native.stdout)["approval"] is False, "CLI must preserve approval=false")
|
|
245
|
+
print("asset generation planner contract tests: PASS")
|
|
246
|
+
return 0
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
if __name__ == "__main__":
|
|
250
|
+
raise SystemExit(main())
|
|
@@ -6,6 +6,8 @@ from __future__ import annotations
|
|
|
6
6
|
import base64
|
|
7
7
|
import hashlib
|
|
8
8
|
import json
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
9
11
|
import subprocess
|
|
10
12
|
import sys
|
|
11
13
|
import tempfile
|
|
@@ -134,19 +136,33 @@ def main() -> int:
|
|
|
134
136
|
|
|
135
137
|
scene_dir = ROOT / "src" / "output" / "browser-review-smoke"
|
|
136
138
|
task_dir = ROOT / "artifacts" / "browser-review-smoke-task"
|
|
139
|
+
smoke_task_dir = temp / "professional-review-e2e-attestation"
|
|
140
|
+
shutil.copytree(task_dir, smoke_task_dir)
|
|
141
|
+
shutil.copy(
|
|
142
|
+
ROOT / "artifacts" / "browser-review-smoke-task" / "project-context.json",
|
|
143
|
+
smoke_task_dir / "project-context.json",
|
|
144
|
+
)
|
|
145
|
+
shutil.copy(
|
|
146
|
+
ROOT / "artifacts" / "browser-review-smoke-task" / "evidence-verifier-report.json",
|
|
147
|
+
smoke_task_dir / "evidence-verifier-report.json",
|
|
148
|
+
)
|
|
149
|
+
candidate_path = smoke_task_dir / "browser-review.json"
|
|
150
|
+
candidate = json.loads(candidate_path.read_text(encoding="utf-8"))
|
|
151
|
+
candidate["expires_at"] = "2099-01-01T00:00:00Z"
|
|
152
|
+
candidate_path.write_text(json.dumps(candidate, indent=2) + "\n", encoding="utf-8")
|
|
137
153
|
task_statement = make_statement(
|
|
138
154
|
task_id="browser-review-smoke-task",
|
|
139
155
|
scene="browser-review-smoke",
|
|
140
|
-
context_hash=sha256_file(
|
|
156
|
+
context_hash=sha256_file(smoke_task_dir / "project-context.json"),
|
|
141
157
|
source_sha256=sha256_file(scene_dir / "animation.json"),
|
|
142
158
|
manifest_sha256=sha256_file(scene_dir / "manifest.json"),
|
|
143
|
-
motion_ir_sha256=sha256_file(
|
|
159
|
+
motion_ir_sha256=sha256_file(smoke_task_dir / "motion-ir.json"),
|
|
144
160
|
evidence={
|
|
145
|
-
"runtime_evidence_sha256": sha256_file(
|
|
146
|
-
"runtime_telemetry_sha256": telemetry_bundle_sha256(
|
|
147
|
-
"verifier_report_sha256": sha256_file(
|
|
161
|
+
"runtime_evidence_sha256": sha256_file(smoke_task_dir / "runtime-adapters" / "runtime-evidence.json"),
|
|
162
|
+
"runtime_telemetry_sha256": telemetry_bundle_sha256(smoke_task_dir),
|
|
163
|
+
"verifier_report_sha256": sha256_file(smoke_task_dir / "evidence-verifier-report.json"),
|
|
148
164
|
},
|
|
149
|
-
provenance_chain_hash=sha256_file(
|
|
165
|
+
provenance_chain_hash=sha256_file(smoke_task_dir / "provenance.json"),
|
|
150
166
|
)
|
|
151
167
|
smoke_statement_file = temp / "smoke-statement.json"
|
|
152
168
|
smoke_statement_file.write_text(json.dumps(task_statement, indent=2) + "\n", encoding="utf-8")
|
|
@@ -157,8 +173,8 @@ def main() -> int:
|
|
|
157
173
|
sys.executable,
|
|
158
174
|
str(ROOT / "scripts" / "quality-gate.py"),
|
|
159
175
|
"--scene", "browser-review-smoke",
|
|
160
|
-
"--context", str(
|
|
161
|
-
"--task-dir", str(
|
|
176
|
+
"--context", str(smoke_task_dir / "project-context.json"),
|
|
177
|
+
"--task-dir", str(smoke_task_dir),
|
|
162
178
|
"--require-attestation",
|
|
163
179
|
"--attestation", str(smoke_bundle_file),
|
|
164
180
|
"--trust-policy", str(policy_file),
|