Status: ✅ implemented (both phases). Built engine-first: a deterministic core (Phase 1, no
LLM) with the LLM composer + work loop on top (Phase 2). The governing principle throughout:
the LLM proposes; the deterministic engine decides what is allowed and executes — a model can
never override a gate (proven in tests/test_redteam_gate_override.py).
Phase 1 — deterministic engine (no LLM) · engine.py · toolcards.py
ToolCardRegistry—create_task,update_project_status,route_approval,draft_internal_note,schedule_meeting,edit_document(each aToolCardwithside_effect+input_schema).ActionValidationEngine.validate_action(action, bundle, recipe=None, *, approvals=None)— re-derivesblocked_reasonfrom five gates (model-supplied values are ignored):- permission — every
action.sourcesobject must be accessible (not in the bundle'spermission_boundary, and ACL-readable by the actor). - mosaic / information-barrier — block when sources combine
public-side+private-side. - injection — scan source content for hidden-instruction patterns; strip + block.
- missing-evidence — block status-advancing writes while a blocking
MissingEvidenceStateis unresolved (routing for sign-off is still allowed). - approval — a
write/route_approvalwith arequired_approveris held until that approver is present.
- permission — every
build_diff/DryRunExecutor.dry_run— previewable before/after diffs, zero side effects.WorkspaceExecutor.execute(plan, approved_indices)(core.pipeline.Executor) — applies ONLY approved, non-blocked actions to an in-memory workspace; records skipped/blocked.rollback(audit_event)/build_rollback_plan— inverse diff restores prior state.
SafeActionComposer.compose(brief, bundle)(composer.py,core.pipeline.ActionComposer) — an injectable proposer mapsbrief.next_stepsonto ToolCards; every candidate is then run throughvalidate_action+build_diff.summarize_plangives the "N follow-ups — X draftable, Y need approval routing, Z blocked" split, derived entirely from engine validation. Proposers:HeuristicActionProposer(offline default) ·LLMActionProposer(routed viaPLANNER_MODEL).SafeActionComposer.compose_staged_remediation(remediation, brief, bundle)— validates one Decision Brief readiness-row remediation into one drawer card. This is the anti-drift seam for staged row actions: the card is rebuilt from the row descriptor and re-gated by the engine.ControlledWorkLoop.run(brief, bundle)(loop.py) — five pure(state)->statenodes: distribute → collect → escalate → schedule → close. A human-approval step (approvercallback, defaultapprove_nonblocked) precedes execution; the loop callsExecutor.executeonly on approved, non-blocked actions and emits the audit dossier (LoopState).- Personas (
personas.py) — seeded counterparties (credit officer, legal, analyst, compliance). Client is injectable:StubPersonaClient(offline) ·LLMPersonaClient(routed viaPERSONA_MODEL). The persona only supplies reply text; control flow stays deterministic.
A hostile proposer returns mosaic / injection / missing-evidence actions while claiming they are
fine (blocked_reason=None). The composer re-validates → the block stands; the executor refuses to
run a blocked action even when its index is approved. A gate is never overridden by model output.
This mirrors WS-D's synthesis-layer guarantee — the same property provable at both layers.
python -m actions.loop # demo the full work loop over fixtures.acme (stub personas)
pytest actions/tests/ -q # 39 tests, no network / API key
make test && make lint # full suite green + ruff cleanfrom actions.composer import SafeActionComposer, summarize_plan
from actions.loop import ControlledWorkLoop, run_acme_loop_demo
plan = SafeActionComposer().compose(brief, bundle) # core.pipeline.ActionComposer
print(summarize_plan(plan).headline)
state = run_acme_loop_demo() # full loop on fixtures.acme
state = ControlledWorkLoop().run(brief, bundle) # distribute→collect→escalate→schedule→closeVerified end-to-end against the live upstream stages (real context.assembler → verification.engine
→ brief.synthesizer → WS-E): every executed action is engine-verified non-blocked. The loop's five
nodes are pure (state)->state functions, so they map 1:1 onto a LangGraph StateGraph; the
hand-rolled run keeps the suite offline and fast. The composer/executor back the
/actions/compose + /actions/execute surfaces (action-diff drawer).
Phase 1 fully tested (every ToolCard + gate blocks the right thing; rollback + ordered audit work) ·
Phase 2 compose() + full loop run on the fixture with personas, every execution path engine-gated
and human-approved · gate-override red-team test passes · LLM mockable so CI is green offline ·
make test (239) + make lint clean · stays within actions/, no core/ edits.
Built single-stream (engine + loop together) on main, per the consolidated WS-E brief. The
deterministic engine (engine.py, toolcards.py) is the Codex-owned
lane; the composer/loop/personas are the Claude lane. The internal split is preserved
(deterministic engine first, LLM on top) so it rebases cleanly onto a separate engine PR if needed.