Skip to content

RFC: CGS capability preconditions (domain write guards) #42

Description

@ryan-s-roberts

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:

  1. For each write node’s capability, load preconditions[].
  2. Resolve bind from plan params / program bindings.
  3. 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).
  4. 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:

  1. Preconditions (domain legality)
  2. 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

  • Schema + validation for preconditions on actions
  • Dry deny when deny_when matches; execute re-check
  • Fixture test: eligibility view blocks a write without adapter logic
  • Fixture test: require.match on pending row id
  • Doc: layering vs Plan Security; cheating line (deny ≠ rewrite)
  • No dependency on product shims for the fixture path

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

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions