Skip to content

Commit 091f32f

Browse files
committed
fix: validate create-thread evidence boundaries
1 parent 9a92cae commit 091f32f

5 files changed

Lines changed: 62 additions & 4 deletions

File tree

docs/superpowers/specs/2026-07-25-create-thread-capability-evidence-design.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# TFD create_thread Capability Evidence Design
22

3-
**Status:** Approved design for implementation planning
4-
**Date:** 2026-07-25
3+
**Status:** Approved design for implementation planning
4+
**Date:** 2026-07-25
55
**Scope:** Add current-session `create_thread` tool-schema evidence to the live model scanner without weakening the existing account-catalog or `spawn_agent` evidence boundaries.
66

77
## 1. Problem
@@ -18,7 +18,7 @@ TFD 2.1.0 correctly scans the active Codex account model catalog and accepts the
1818
}
1919
```
2020

21-
Live acceptance on 2026-07-25 proved that the current `create_thread` schema advertises and successfully executes all seven visible account models. The scanner cannot represent that evidence, so its machine-readable backend result diverges from the Coordinator's live tool evidence.
21+
Live acceptance on 2026-07-25 proved that current `create_thread` tool metadata advertises seven visible account-model identifiers and that supplying those identifiers produces a seven-model account/backend metadata intersection. It did not prove seven completed invocations. The scanner cannot represent that evidence, so its machine-readable backend result diverges from the Coordinator's live tool evidence.
2222

2323
## 2. Goals
2424

plugins/tfd/scripts/model_catalog.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,6 +333,7 @@ def normalize_catalog(
333333
runtime_allowed: tuple[str, ...],
334334
create_thread_allowed: tuple[str, ...] | None = None,
335335
) -> dict[str, object]:
336+
create_thread_allowed = parse_create_thread_allowed(create_thread_allowed)
336337
by_id: dict[str, dict[str, object]] = {}
337338
field_map = {
338339
"displayName": "display_name",
@@ -503,6 +504,7 @@ def scan_catalog(
503504
runtime_allowed: tuple[str, ...],
504505
create_thread_allowed: tuple[str, ...] | None = None,
505506
) -> dict[str, object]:
507+
create_thread_allowed = parse_create_thread_allowed(create_thread_allowed)
506508
deadline = time.monotonic() + DEFAULT_TIMEOUT_SECONDS
507509
with AppServerSession(codex_bin, deadline) as session:
508510
session.initialize()

plugins/tfd/scripts/test_model_catalog.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -366,6 +366,21 @@ def test_scanner_returns_a_sanitized_blocker_when_the_server_cannot_start(self)
366366

367367
self.assertEqual(raised.exception.code, "launch_failure")
368368

369+
def test_scan_rejects_blank_create_thread_allowlist_before_launch(self) -> None:
370+
with tempfile.TemporaryDirectory() as directory:
371+
missing_bin = str(Path(directory) / "missing-codex")
372+
with self.assertRaises(model_catalog.CatalogBlocker) as raised:
373+
model_catalog.scan_catalog(
374+
missing_bin,
375+
("model-spawn",),
376+
(" ",),
377+
)
378+
379+
self.assertEqual(
380+
raised.exception.code,
381+
"invalid_create_thread_allowlist",
382+
)
383+
369384
def test_post_launch_selector_setup_failure_reaps_the_child(self) -> None:
370385
stdin = mock.Mock()
371386
stdin.closed = False
@@ -936,6 +951,19 @@ def test_scan_deadline_covers_notification_stream(self) -> None:
936951

937952

938953
class ModelCatalogNormalizationTests(unittest.TestCase):
954+
def test_normalize_catalog_rejects_blank_create_thread_allowlist(self) -> None:
955+
with self.assertRaises(model_catalog.CatalogBlocker) as raised:
956+
model_catalog.normalize_catalog(
957+
({"id": "model-account", "hidden": False},),
958+
("model-account",),
959+
(" ",),
960+
)
961+
962+
self.assertEqual(
963+
raised.exception.code,
964+
"invalid_create_thread_allowlist",
965+
)
966+
939967
def test_create_thread_backend_is_available_and_preferred(self) -> None:
940968
payload = model_catalog.normalize_catalog(
941969
(
@@ -984,6 +1012,30 @@ def test_create_thread_backend_is_available_and_preferred(self) -> None:
9841012
"UNSUPPORTED",
9851013
)
9861014

1015+
def test_non_overlapping_create_thread_models_fall_back_to_spawn_agent(
1016+
self,
1017+
) -> None:
1018+
payload = model_catalog.normalize_catalog(
1019+
({"id": "model-spawn", "hidden": False},),
1020+
("model-spawn",),
1021+
(" model-backend-only ",),
1022+
)
1023+
1024+
self.assertEqual(
1025+
payload["backend_capabilities"]["create_thread"]["models"],
1026+
["model-backend-only"],
1027+
)
1028+
self.assertEqual(payload["create_thread_eligible_models"], [])
1029+
self.assertEqual(
1030+
payload["execution_readiness"],
1031+
{
1032+
"status": "PASS",
1033+
"code": "verified_execution_backend",
1034+
"backend": "spawn_agent",
1035+
"models": ["model-spawn"],
1036+
},
1037+
)
1038+
9871039
def test_recommendation_catalog_contains_every_visible_model_with_evidence(
9881040
self,
9891041
) -> None:

plugins/tfd/scripts/test_team_skill_contract.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ def test_team_requires_fresh_live_model_evidence_before_roster_proposal(
9090
self.assertIn("<plugin-root>/scripts/model_catalog.py", self.content)
9191
self.assertIn("--create-thread-model", self.content)
9292
self.assertIn("--runtime-allowed-model", self.content)
93+
self.assertIn(
94+
"pass each exact create_thread model identifier once with its own repeated `--create-thread-model` flag",
95+
self.normalized,
96+
)
9397
self.assertIn("omit `--create-thread-model`", self.content)
9498
self.assertIn("must not pass cached identifiers", self.normalized)
9599
self.assertIn(

plugins/tfd/skills/team/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ Choose no fixed roster size. Include a role only when it adds a required special
5252

5353
## Verify live model evidence
5454

55-
Before proposing a roster, the Coordinator must read the current create_thread model allowlist from live tool metadata and the current spawn_agent model allowlist from live tool metadata. Pass every exact current-session identifier to `<plugin-root>/scripts/model_catalog.py` once for its backend. For spawn_agent, pass every exact model name to `<plugin-root>/scripts/model_catalog.py` as a repeated `--runtime-allowed-model` argument. Use this pattern:
55+
Before proposing a roster, the Coordinator must read the current create_thread model allowlist from live tool metadata and the current spawn_agent model allowlist from live tool metadata. Pass each exact create_thread model identifier once with its own repeated `--create-thread-model` flag. For spawn_agent, pass every exact model name to `<plugin-root>/scripts/model_catalog.py` as a repeated `--runtime-allowed-model` argument. Use this pattern:
5656

5757
```bash
5858
python3 <plugin-root>/scripts/model_catalog.py \

0 commit comments

Comments
 (0)