Class: MandateClaw::Registry::TaskContract

Inherits:
Object
  • Object
show all
Defined in:
lib/mandate_claw/registry/task_contract.rb

Overview

A TaskContract is an instantiated, signable contract for a specific scope.

Usage:

tc = MandateClaw::Registry::TaskContract.new(
  template: InvoiceContract,
  scope:    invoice,
  parties:  { buyer: current_user, seller: merchant, ai_agent: agent },
  validity: 24.hours
)

tc.sign_as(:buyer,    key: user.signing_key_hex)
tc.sign_as(:ai_agent, key: agent.capability_key_hex,
                      attests: agent.capability_manifest)
tc.anchor!   # persists to MandateClaw::Registry::SignedContract

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(template:, scope:, parties:, validity: 24.hours) ⇒ TaskContract

Returns a new instance of TaskContract.



28
29
30
31
32
33
34
35
36
# File 'lib/mandate_claw/registry/task_contract.rb', line 28

def initialize(template:, scope:, parties:, validity: 24.hours)
  @template   = template
  @scope      = scope
  @parties    = parties
  @validity   = validity
  @signatures = {}
  @created_at = Time.current
  @contract_id = nil
end

Instance Attribute Details

#contract_idObject (readonly)

Returns the value of attribute contract_id.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def contract_id
  @contract_id
end

#created_atObject (readonly)

Returns the value of attribute created_at.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def created_at
  @created_at
end

#partiesObject (readonly)

Returns the value of attribute parties.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def parties
  @parties
end

#scopeObject (readonly)

Returns the value of attribute scope.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def scope
  @scope
end

#signaturesObject (readonly)

Returns the value of attribute signatures.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def signatures
  @signatures
end

#templateObject (readonly)

Returns the value of attribute template.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def template
  @template
end

#validityObject (readonly)

Returns the value of attribute validity.



25
26
27
# File 'lib/mandate_claw/registry/task_contract.rb', line 25

def validity
  @validity
end

Instance Method Details

#anchor!Object

Persists this contract to the registry database. Raises unless fully signed.



71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
# File 'lib/mandate_claw/registry/task_contract.rb', line 71

def anchor!
  raise Registry::SignatureError, "Contract is not fully signed" unless fully_signed?

  record = SignedContract.create!(
    contract_digest:   digest,
    template_name:     template._contract_name.to_s,
    scope_type:        scope.class.name,
    scope_id:          scope.id.to_s,
    parties_json:      parties.transform_values(&:to_s).to_json,
    signatures_json:   signatures.to_json,
    rendered_markdown: template.to_markdown,
    expires_at:        created_at + validity,
    status:            :active
  )

  @contract_id = record.id
  record
end

#digestObject

The canonical digest is the authoritative fingerprint of this contract instance. All parties sign this exact string.



40
41
42
# File 'lib/mandate_claw/registry/task_contract.rb', line 40

def digest
  @digest ||= Digest::SHA256.hexdigest(canonical_payload.to_json)
end

#expired?Boolean

Returns:

  • (Boolean)


90
91
92
# File 'lib/mandate_claw/registry/task_contract.rb', line 90

def expired?
  created_at + validity < Time.current
end

#fully_signed?Boolean

Returns:

  • (Boolean)


64
65
66
67
# File 'lib/mandate_claw/registry/task_contract.rb', line 64

def fully_signed?
  required = template._attestation&.required_signatories || []
  required.all? { |p| signatures.key?(p) }
end

#sign_as(party_name, key:, attests: nil, algorithm: :ed25519) ⇒ Object

Raises:

  • (ArgumentError)


44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
# File 'lib/mandate_claw/registry/task_contract.rb', line 44

def sign_as(party_name, key:, attests: nil, algorithm: :ed25519)
  raise ArgumentError, "Unknown party: #{party_name}" unless parties.key?(party_name)

  sig = case algorithm
        when :ed25519
          Signing::Ed25519Signer.sign(digest, key)
        when :hmac_sha256
          Signing::HmacSigner.sign(digest, key)
        else
          raise ArgumentError, "Unsupported algorithm: #{algorithm}"
        end

  @signatures[party_name] = {
    signature:  sig,
    algorithm:  algorithm,
    attests:    attests,
    signed_at:  Time.current
  }
end