AST Validator Reference¶
The specstar.validator.DeclarativeASTValidator enforces that _generated.py (the skill-maintained file in spec-driven projects) contains declarative Python only — no I/O, no orchestration, no side effects.
specstar verify runs the validator on every *_generated.py it finds in spec.lock.json. CI fails on the first violation.
This page is the canonical reference for what the validator accepts and rejects.
Mental model¶
Three layers cooperate to keep generated code safe:
- API design — declarative slots (
spec.add_model, permission expressions, query filters) accept proxy types whose operator overloading produces expression trees instead of computing values. - AST validation — the file as a whole passes through an allow / block list before any of it runs.
- String references — anything that needs I/O lives in your own module and is referenced by name (
"my_app.logic.fn"), not imported.
If the validator rejects something in _generated.py, the fix is almost always: move the offending logic into your own module, then reference it by string from spec.md.
Block-list — rejected outright¶
Imports¶
These top-level module names cause import and from … import … statements to fail:
| Module | Reason |
|---|---|
os |
filesystem, env, process |
subprocess |
shell |
socket |
network |
requests, urllib, urllib2, urllib3, httpx, aiohttp, http |
HTTP |
pathlib |
filesystem |
shutil, tempfile |
filesystem |
threading, multiprocessing, asyncio |
concurrency |
ctypes |
foreign function interface |
Statements¶
| AST node | Why blocked |
|---|---|
Try / TryStar |
error handling implies failures, which implies I/O |
With / AsyncWith |
context managers usually wrap I/O |
Raise |
raising errors belongs in user logic, not declarative wiring |
While |
non-declarative control flow |
AsyncFunctionDef / Await / Yield / YieldFrom |
concurrency / generators |
Global / Nonlocal |
mutation patterns |
Builtin calls¶
Direct calls to these names are blocked (they bypass the sandbox):
exec, eval, compile, open, __import__, input, breakpoint,
getattr, setattr, delattr
getattr / setattr / delattr are blocked because runtime indirection
defeats static analysis — getattr(obj, "system") would otherwise slip
past the call-name check.
Dunder attributes¶
Reading any attribute whose name starts and ends with two underscores
(__class__, __bases__, __subclasses__, __globals__, __builtins__,
__dict__, etc.) is blocked. These are the canonical Python sandbox-escape
vectors:
().__class__.__bases__[0].__subclasses__() # rejected (multiple violations)
some_func.__globals__["os"] # rejected (__globals__)
Two dunders are explicitly safe-listed because they appear in legitimate metadata reads:
__name____doc__
Allow-list — accepted¶
Statements¶
Import/ImportFrom— for any module not in the block-list (typicallyspecstar,msgspec,typing,enum,datetime,decimal, your own user package as a string ref; never your user package as a Pythonimport)FunctionDef— for declarative bodies (e.g. pure migration row-transforms)ClassDef— formsgspec.StructdefinitionsAssign,AnnAssign,ReturnIf,For,IfExp— declarative wiring may legitimately loop or branch (e.g. registering a list of similar resources)Pass
Expressions¶
Lambda, Call, Attribute, BinOp, BoolOp, UnaryOp, Compare, IfExp, list / dict / set / generator comprehensions, all literal types.
Builtins (when called directly)¶
len, min, max, sum, sorted, tuple, list, dict, set, frozenset, range, enumerate, zip, bool, int, float, str.
(Other names, including any user-defined function in scope, are also allowed by the current skeleton — Phase 1.5 will tighten this with positive call-target tracking.)
The escape hatch — # specstar: allow¶
Some violations are legitimate. Annotate the offending line with a comment to suppress the validator on that line:
The comment may include a free-text reason after allow. Pattern is case-insensitive and tolerates extra whitespace:
Bypassed lines are recorded in spec.lock.json's validation block so reviewers can audit waivers.
Use sparingly. Each bypass is technical debt. If you find yourself reaching for it more than once or twice per file, move that code into a hand-written module and reference it from spec.md instead.
Same-line semantics only — above-line / block-scope exemption is intentionally not supported, to keep audit diffs unambiguous.
When verify fails¶
specstar verify exits with code 1 and prints lines like:
AST: my_app/_generated.py:3:0 [blocked_import] import of 'os' is not allowed in declarative Python
verify: 1 issue(s) detected
The fix:
- Read the line cited in the error.
- Decide whether the offending code is declaration (move it to spec.md) or logic (move it to your own Python module and reference it by string).
- Re-run the skill to regenerate
_generated.pywith the corrected source-of-truth.
Do not silence violations with # specstar: allow to make CI green without thinking about why the validator flagged it.
Reference¶
- Implementation:
specstar.validator.ast - Public API:
DeclarativeASTValidator,ValidationError,BLOCKED_IMPORTS,BLOCKED_STATEMENTS,BLOCKED_BUILTINS - Design rationale:
docs/design/spec-driven-architecture.md§3.2