Summary
Propose a CGS-authored, host-enforced preconditions: surface so domain write legality (e.g. “do not pay while an open dispute exists”) is expressed in catalogs and evaluated by plasm-agent-core, instead of living as ad-hoc reject logic in product/shim adapters.
Motivation (τ³-Banking overlay): a SharedDbShim pay-guard correctly rejects pay_credit_card_from_checking when the customer has an open dispute — matching KB / nv_closure_policy. That is the right semantics, wrong home. Soft CGS already has the view (credit_card_closure_eligibility) and gloss; hard enforcement should be catalog + host.
Non-goals / cheating line
- Preconditions deny illegal writes; they must never rewrite agent wire args to gold outcomes.
- Orthogonal to Plan Security (
FlowPolicy AC/IFC) — see plan-flow-policy-spec in the monorepo (or equivalent when vendored).
- Transport hygiene (null-drop,
0 → omit optional fields) stays CML/shim — not policy.
- Harness theatre (e.g. τ³ auto-unlock) stays adapter-local.
Problem
| Layer today |
Soft (teach / view) |
Hard (reject write) |
| CGS |
nv_* policy text, eligibility views, capability descriptions |
— |
| Host |
— |
— (no domain precondition admit) |
| Adapter/shim |
— |
τ³ pay-guard, CLI pending-row checks, … |
Agents can ignore soft policy. Adapters duplicate domain rules that belong in the catalog and apply only to one backend.
Plan Security capability_gates are static capability matches (tenant AC). They cannot express “deny pay when has_open_dispute on this account.”
Proposal
1. Schema (domain.yaml on capabilities)
Optional preconditions: list on kind: action (and optionally other mutating kinds). Each item:
| Field |
Required |
Meaning |
id |
yes |
Stable id for diagnostics / teaching |
view or capability |
one of |
Reuse a composed view or a query/get capability as the readiness source |
bind |
yes |
Map view/query scope params from action params (kind: param) or literals |
deny_when or require |
one of |
Predicate over the evaluated row(s) |
message |
yes |
Human/agent-visible deny reason (policy text, not task-id) |
Predicate shapes (v1):
deny_when: { field, equals } — scalar / bool on the (single) view output or matched row
require: { match: { field, equals: { kind: param, name } } } — at least one row matches (CLI pending request_id)
- Optional later:
any_of / all_of composition; keep v1 minimal
Example — closure pay stop (domain logistics):
CreditCardAccountLocked_pay_from_checking:
kind: action
entity: CreditCardAccountLocked
parameters: [ ... ]
preconditions:
- id: no_open_dispute
view: credit_card_closure_eligibility
bind:
credit_card_account_id:
kind: param
name: credit_card_account_id
user_id:
kind: param
name: user_id
deny_when:
field: has_open_dispute
equals: true
message: >
Open dispute blocks payment toward closure (nv_closure_policy step 1).
Inform the customer and STOP — do not pay on this call.
Example — pending row identity (CLI):
CreditLimitRequest_approve:
preconditions:
- id: pending_request_matches
capability: CreditLimitRequest_list_by_account
bind:
credit_card_account_id:
kind: param
name: credit_card_account_id
status:
kind: literal
value: PENDING
require:
match:
field: request_id
equals:
kind: param
name: request_id
message: No PENDING credit-limit request matches this request_id.
Authoring rules:
- Prefer existing views already used for teaching (DRY with eligibility projections).
- Messages cite policy / KB concepts, not task ids or gold strings.
- Preconditions are catalog domain rules, not tenant FlowPolicy documents.
2. Host enforcement
plasm (dry) → structural validate → [NEW] verify_capability_preconditions
→ plan flow policy (AC/IFC) → mint pcN
plasm_run → [NEW] re-check preconditions (TOCTOU belt) → execute CML
Evaluation:
- For each write node’s capability, load
preconditions[].
- Resolve
bind from plan params / program bindings.
- Evaluate view or run the referenced query against session graph when rows are already materialised; otherwise perform a scoped fetch (same transport as normal reads).
- Apply predicate → on failure: deny with
id + message (dry: plan verdict; execute: hard fail before outbound mutation).
Disposition: deny only (v1). No auto-rewrite of parameters. No silent coerce-to-default for “gold-shaped” fields.
Composition with Plan Security:
- Preconditions (domain legality)
capability_gates + forbidden (tenant AC/IFC)
Either layer may deny. Approving a capability in FlowPolicy does not suppress a failed precondition.
3. Teaching / UX
- Optional TSV gloss: list precondition ids + short messages when the capability is exposed.
- Dry failure should be actionable (
extend / query eligibility view), not “open a new session.”
- Plan UX (optional follow-on): show precondition outcomes on write nodes alongside Flow tab.
4. Explicitly out of scope (v1)
| Item |
Why |
| Shim/backends inventing corrected wire values |
Cheating / write correction |
| Encoding τ³ unlock theatre in CGS |
Harness concern |
| Stateful automata / multi-step effect machines |
Doctrine already marks out of scope for FlowPolicy v1 |
| Tenant-authored preconditions in FlowPolicy JSON |
Wrong layer; keep domain in CGS |
| Soft-only “warnings” |
v1 is hard deny; soft already exists via descriptions/views |
Implementation sketch
| Area |
Work |
plasm-core schema |
Parse/validate preconditions on capabilities; fixture coverage on plasm_language_matrix / prompt matrix — not live apis/ |
plasm-runtime / agent-core |
verify_capability_preconditions on dry + execute; view eval reuse |
| Authoring skill |
Document in plasm-authoring reference: when to use views vs query require.match |
| Migration |
τ³ overlay: delete domain pay-guard once catalog preconditions cover it; keep unlock theatre in shim |
Complexity: medium (view bind + dry path).
Catastrophic risk: low if deny-only and opt-in (preconditions absent ⇒ no change). Wrong predicates could over-block real APIs — mitigate with matrix fixtures + explicit messages.
Acceptance criteria
References
- Monorepo Plan Security:
docs/plan-flow-policy-spec.md, ADR docs/adr/flow-policy-capability-abstraction.md
- Soft precedent: composed eligibility views +
nv_* policy values in catalogs (e.g. closure readiness / CLI eligibility)
- Temporary hard precedent: adapter reject guards that should move here
Summary
Propose a CGS-authored, host-enforced
preconditions:surface so domain write legality (e.g. “do not pay while an open dispute exists”) is expressed in catalogs and evaluated byplasm-agent-core, instead of living as ad-hoc reject logic in product/shim adapters.Motivation (τ³-Banking overlay): a SharedDbShim pay-guard correctly rejects
pay_credit_card_from_checkingwhen the customer has an open dispute — matching KB /nv_closure_policy. That is the right semantics, wrong home. Soft CGS already has the view (credit_card_closure_eligibility) and gloss; hard enforcement should be catalog + host.Non-goals / cheating line
FlowPolicyAC/IFC) — see plan-flow-policy-spec in the monorepo (or equivalent when vendored).0→ omit optional fields) stays CML/shim — not policy.Problem
nv_*policy text, eligibility views, capability descriptionsAgents can ignore soft policy. Adapters duplicate domain rules that belong in the catalog and apply only to one backend.
Plan Security
capability_gatesare static capability matches (tenant AC). They cannot express “deny pay whenhas_open_disputeon this account.”Proposal
1. Schema (
domain.yamlon capabilities)Optional
preconditions:list onkind: action(and optionally other mutating kinds). Each item:idvieworcapabilitybindkind: param) or literalsdeny_whenorrequiremessagePredicate shapes (v1):
deny_when: { field, equals }— scalar / bool on the (single) view output or matched rowrequire: { match: { field, equals: { kind: param, name } } }— at least one row matches (CLI pendingrequest_id)any_of/all_ofcomposition; keep v1 minimalExample — closure pay stop (domain logistics):
Example — pending row identity (CLI):
Authoring rules:
2. Host enforcement
Evaluation:
preconditions[].bindfrom plan params / program bindings.id+message(dry: plan verdict; execute: hard fail before outbound mutation).Disposition: deny only (v1). No auto-rewrite of parameters. No silent coerce-to-default for “gold-shaped” fields.
Composition with Plan Security:
capability_gates+forbidden(tenant AC/IFC)Either layer may deny. Approving a capability in FlowPolicy does not suppress a failed precondition.
3. Teaching / UX
extend/ query eligibility view), not “open a new session.”4. Explicitly out of scope (v1)
Implementation sketch
plasm-coreschemapreconditionson capabilities; fixture coverage onplasm_language_matrix/ prompt matrix — not liveapis/plasm-runtime/ agent-coreverify_capability_preconditionson dry + execute; view eval reuserequire.matchComplexity: medium (view bind + dry path).
Catastrophic risk: low if deny-only and opt-in (
preconditionsabsent ⇒ no change). Wrong predicates could over-block real APIs — mitigate with matrix fixtures + explicit messages.Acceptance criteria
preconditionson actionsdeny_whenmatches; execute re-checkrequire.matchon pending row idReferences
docs/plan-flow-policy-spec.md, ADRdocs/adr/flow-policy-capability-abstraction.mdnv_*policy values in catalogs (e.g. closure readiness / CLI eligibility)