Independent post-merge review of #19 surfaced two more findings.
Both shipped here.
Q3 — abstract intermediates + multiple-concrete-classes.
The class-discovery filter from O1 (#19) only excluded BaseAdapter
itself. Two failure modes slipped through:
(a) A locally-defined abstract intermediate
`class FrameworkAdapter(BaseAdapter): @abstractmethod ...`
passed the filter, falsely satisfying "at least one
concrete subclass" while still being non-instantiable at
workspace boot.
(b) A template defining BOTH `class FrameworkAdapter(BaseAdapter)`
AND `class ConcreteAdapter(FrameworkAdapter)` had both pass
the filter, producing a silent ambiguity where the runtime's
class-discovery picks one per its resolution rules — wrong
class loaded after a future runtime refactor.
Fixes:
- Add `not inspect.isabstract(obj)` to the discovery filter so
abstract intermediates are excluded.
- Hard-error if `len(adapter_classes) > 1` listing both names so
the contributor knows exactly which classes are competing.
Three new tests pin the behaviors:
- test_abstract_intermediate_alone_does_not_count
- test_abstract_plus_concrete_passes_with_concrete_only
- test_multiple_concrete_baseadapter_subclasses_errors
Identity-based deduplication.
Caught against the real langgraph template during smoke-testing
the Q3 fix: production adapters often do
`Adapter = ConcreteAdapter` as a module-level alias for the
runtime's discovery convention. `vars(mod)` returns BOTH bindings
pointing at the same class object, so the new
multiple-concrete-classes error fired falsely on every aliased
template.
Fix: deduplicate by `id(obj)` BEFORE counting, so the same class
object under multiple bindings counts once. New regression test
test_aliased_concrete_class_is_deduplicated pins this against
any future filter regression.
Existing tests updated to use fully-concrete BaseAdapter subclasses
(matching production templates) since the new abstract-filter
correctly rejects partial stubs that don't override every abstract
method BaseAdapter declares (5 methods: name, display_name,
description, setup, create_executor).
Q5 — GITHUB_TOKEN scope lockdown.
validate-workspace-template.yml runs untrusted-by-design code from
the calling template repo: pip post-install hooks, adapter.py
imports, Dockerfile RUN steps. Each of those primitives executes
with GITHUB_TOKEN in env. The workflow had no `permissions:`
block, defaulting to whatever the calling repo grants — often
contents: write.
Add `permissions: contents: read` at the workflow level. Worst-
case-with-token now drops to "read public repo state" — no write
to issues, no push to branches, no comment-spam, no workflow
re-trigger. Partial mitigation; the deeper `pull_request_target`
discipline is bigger scope (tracked separately).
Verification:
- 47/47 tests pass (was 43; +3 abstract/multi-concrete + +1 alias)
- All 8 production templates pass the full updated validator
end-to-end with 0 warnings / 0 errors
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>