Skip to content

A Persona is a declared identity construct. It establishes a named participant role that may be referenced in operation allowed_personas sets, flow step persona fields, handoff steps, compensation steps, and escalation targets. Personas are the authority namespace of the contract -- they define who can act.

DSL Syntax

hcl
persona <PersonaId>

A persona declaration is a single line. There are no attributes, no metadata, no delegation, no hierarchies, and no semantic content beyond identity.

hcl
persona buyer
persona seller
persona escrow_agent
persona compliance_officer

PersonaId is a non-empty UTF-8 string, unique within the contract. The set of all declared personas is finite, fixed at contract definition time, and statically enumerable.

Personas Are Roles, Not Users

A persona represents a role -- warehouse_manager, compliance_officer, billing_system -- not an individual user. If Alice and Bob are both warehouse managers, they share the warehouse_manager persona.

The mapping from concrete identities to personas happens outside the contract, in the executor's identity layer. This separation is deliberate:

  • The contract declares authority boundaries and reasons about what roles can do.
  • The executor handles identity-to-persona mapping at runtime.

This is an executor concern, governed by obligations E15 and E16. The contract never sees user ids, API keys, or session tokens.

Usage in Operations

Personas appear in the allowed_personas field of operations, controlling who can invoke each operation:

hcl
operation release_escrow {
  personas: [escrow_agent]
  require:  verdict_present(can_auto_release)
  effects:  [EscrowAccount: held -> released]
  outcomes: [released]
}

operation release_escrow_with_compliance {
  personas: [compliance_officer]
  require:  verdict_present(delivery_ok)
  effects:  [EscrowAccount: held -> released]
  outcomes: [released]
}

When an operation is invoked, the first step in the execution sequence is persona check: persona in op.allowed_personas as a simple set membership test.

Usage in Flows

Personas appear throughout flow steps:

hcl
flow escrow_release {
  snapshot: at_initiation
  entry:    step_confirm

  steps: {
    // OperationStep -- persona executes the operation
    step_confirm: OperationStep {
      op:      confirm_delivery
      persona: seller
      outcomes: { confirmed: step_check }
      on_failure: Terminate(outcome: failure)
    }

    // BranchStep -- persona evaluates the condition
    step_check: BranchStep {
      condition: verdict_present(within_threshold)
      persona:   escrow_agent
      if_true:   step_release
      if_false:  step_handoff
    }

    // HandoffStep -- authority transfers between personas
    step_handoff: HandoffStep {
      from_persona: escrow_agent
      to_persona:   compliance_officer
      next:         step_compliance_release
    }

    step_release: OperationStep {
      op:      release_escrow
      persona: escrow_agent
      outcomes: { released: Terminal(success) }
      on_failure: Terminate(outcome: failure)
    }

    step_compliance_release: OperationStep {
      op:      release_escrow_with_compliance
      persona: compliance_officer
      outcomes: { released: Terminal(success) }
      on_failure: Terminate(outcome: failure)
    }
  }
}

Personas also appear in:

  • CompensationStep persona -- who executes the compensation operation
  • Escalate to_persona -- who receives the escalation
  • SubFlowStep persona -- who initiates the sub-flow

Authority Topology (S4)

Static analysis S4 derives the complete authority topology from the contract. For any declared persona P and entity state S, the set of operations P can invoke in S is statically derivable. Whether a persona can cause a transition from S to S' is answerable without executing anything.

Consider a contract with four personas:

hcl
persona customs_officer
persona quality_inspector
persona port_authority
persona shipping_agent

And three operations affecting Shipment:

Operationallowed_personasShipment effect
begin_inspection[customs_officer]arrived -> inspecting
release_shipment[port_authority]inspecting -> cleared
hold_shipment[customs_officer]inspecting -> held

The proof that customs_officer cannot release a shipment is visible in the contract text: release_shipment lists [port_authority], not customs_officer. No runtime check needed. No log analysis. The authority topology is a structural fact.

Running tenor check produces:

Authority (S4): 4 personas, 8 authority entries

Unreferenced Personas

A declared persona that is never referenced in any operation or flow is not an error. The elaborator does not reject unused personas. Static analysis tooling may optionally warn about them, but this is advisory, not normative.

This is useful when:

  • A persona is defined for a system composition that uses it in shared_personas across contracts.
  • A persona is declared for future use as the contract evolves.

Full Working Example

A complete contract demonstrating persona authority boundaries:

hcl
// --- Personas ---
persona requestor
persona department_head
persona finance_controller
persona procurement_admin

// --- Entity ---
entity PurchaseOrder {
  states:  [draft, submitted, dept_approved, finance_approved, rejected, fulfilled]
  initial: draft
  transitions: [
    (draft, submitted),
    (submitted, dept_approved),
    (submitted, rejected),
    (dept_approved, finance_approved),
    (dept_approved, rejected),
    (finance_approved, fulfilled)
  ]
}

// --- Facts ---
fact order_amount {
  type:   Money(currency: "USD")
  source: "procurement_service.order_amount"
}

fact department_budget_available {
  type:    Bool
  source:  "budget_service.dept_has_funds"
  default: false
}

// --- Rules ---
rule budget_check {
  stratum: 0
  when:    department_budget_available = true
  produce: verdict budget_ok { payload: Bool = true }
}

// --- Operations ---
// requestor can ONLY submit drafts
operation submit_order {
  personas: [requestor]
  require:  order_amount > 0
  effects:  [PurchaseOrder: draft -> submitted]
  outcomes: [submitted]
}

// department_head approves or rejects submitted orders
operation dept_approve {
  personas: [department_head]
  require:  verdict_present(budget_ok)
  effects:  [PurchaseOrder: submitted -> dept_approved]
  outcomes: [approved]
}

// finance_controller approves or rejects dept-approved orders
operation finance_approve {
  personas: [finance_controller]
  require:  verdict_present(budget_ok)
  effects:  [PurchaseOrder: dept_approved -> finance_approved]
  outcomes: [approved]
}

// both department_head and finance_controller can reject
operation reject_order {
  personas: [department_head, finance_controller]
  require:  true
  effects:  [
    PurchaseOrder: submitted -> rejected,
    PurchaseOrder: dept_approved -> rejected
  ]
  outcomes: [rejected]
}

// procurement_admin fulfills approved orders
operation fulfill_order {
  personas: [procurement_admin]
  require:  true
  effects:  [PurchaseOrder: finance_approved -> fulfilled]
  outcomes: [fulfilled]
}

Authority topology for this contract:

PersonaCan invokeCannot invoke
requestorsubmit_orderEverything else
department_headdept_approve, reject_ordersubmit_order, finance_approve, fulfill_order
finance_controllerfinance_approve, reject_ordersubmit_order, dept_approve, fulfill_order
procurement_adminfulfill_orderEverything else

No persona can bypass the approval chain. The requestor cannot approve their own order. The procurement_admin cannot approve or reject. These are structural facts, provable by tenor check.

Constraints

  • Persona identifiers are unique within a contract. Duplicates are elaboration errors.
  • Persona ids occupy a distinct namespace from other construct kinds. A Persona named Foo does not conflict with an Entity named Foo.
  • Every persona reference in an operation allowed_personas set must resolve to a declared Persona. Unresolved references are Pass 5 errors: "undeclared persona '<id>'".
  • Every persona reference in flow steps (OperationStep, BranchStep, SubFlowStep persona fields), HandoffStep from_persona/to_persona, CompensationStep persona, and Escalate to_persona must resolve to a declared Persona.
  • Unreferenced persona declarations are NOT errors. Tooling may optionally warn.

Common Mistakes

Creating a persona for every user. Personas represent roles, not individuals. If Alice and Bob are both warehouse managers, they share warehouse_manager. The executor maps identities to personas.

Undeclared persona references. If an operation references logistics_admin but no persona logistics_admin exists, elaboration fails. Declare all personas explicitly.

Confusing personas with hierarchies. Personas have no inheritance. A senior_manager persona does not automatically inherit the authority of manager. If both should approve orders, list both in allowed_personas.

Missing handoffs in flows. If a flow requires two different personas at different steps, there must be a HandoffStep between them. A flow cannot silently assume authority it was not granted.

How Personas Connect to Other Constructs

  • Operations reference personas in allowed_personas. The persona check is the first step of the execution sequence.
  • Flows reference personas at every step: who executes an operation, who evaluates a branch, who hands off to whom.
  • Systems can share personas across contracts via shared_personas. A shared persona creates identity equivalence: the same role has authority in multiple contracts.
  • Rules do NOT reference personas. Verdict production is persona-independent.
  • Facts do NOT reference personas. Fact values are identity-neutral.

Static Analysis Properties

  • S4 (Authority topology) is the primary analysis consuming personas. It produces a complete map of (persona, entity state) -> available operations.
  • The complete set of declared persona identifiers is statically known from the contract (enumerable at Pass 2).
  • Authority boundaries are derivable without execution, enabling automated security auditing.