mandateclaw-dsl 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0f8133897021b79b33b7140c81735e0c436e2edeb0648543a341be427b5a8584
4
+ data.tar.gz: db3bc78360c6e8723e23af8d7fc16fc6a4f9390e08462a4deccadeadd9e01e3e
5
+ SHA512:
6
+ metadata.gz: 72f783bf5b600e8e8b70f5224ffc08fbad39dea6f00bd943d094b8112ac87a3c5380ff85e04a8296713282116a8c0c04afcca1774b32cfc71411835443589d0d
7
+ data.tar.gz: 5dbacb76099e47104c1f0f9084877f6f0269bdc2216842c95a8fc092597029f96e45d6070018b69d9e3d701dccd01285df2a5d0a6587ad131cb9280a18dcc454
data/CHANGELOG.md ADDED
@@ -0,0 +1,19 @@
1
+ # Changelog
2
+
3
+ All notable changes to MandateClaw-DSL are documented here.
4
+ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
5
+
6
+ ## [0.0.1] - 2026-05-21
7
+
8
+ ### Added
9
+ - `MandateClaw::DSL::Contract` — base class for contract definitions
10
+ - `party` DSL: declare human, organisation, and autonomous agent parties
11
+ - `obligation` DSL: positive duties with temporal constraints and breach consequences
12
+ - `permission` DSL: rights a party may exercise
13
+ - `prohibition` DSL: explicit denials; attempts are breach events
14
+ - `agent_bounds` DSL: capability envelope for autonomous agent parties (`may`, `must_not`, `must_log_to`)
15
+ - `attestation` DSL: signing requirements (Ed25519, HMAC-SHA256)
16
+ - `rule` DSL: named decision rules with ternary truth (true / false / `:unknown`)
17
+ - `MandateClaw::Validators::ContractValidator` — validates contract structure at definition time
18
+ - `MandateClaw::Renderers::MarkdownRenderer` — renders contract as human-readable Markdown
19
+ - `MandateClaw.define` convenience method for inline contract definition
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Abhishek Parolkar
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # MandateClaw-DSL
2
+
3
+ > **The Employment Contract Between You and Your Agent.**
4
+
5
+ MandateClaw-DSL is a Ruby gem for defining programmatic, human-readable contracts that govern what an AI agent is — and is not — allowed to do on behalf of a human or organisation.
6
+
7
+ It gives your agent a signed scope. Not a system prompt. A contract.
8
+
9
+ [![Gem Version](https://badge.fury.io/rb/mandateclaw-dsl.svg)](https://rubygems.org/gems/mandateclaw-dsl)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
11
+
12
+ ---
13
+
14
+ ## The Problem
15
+
16
+ Every serious deployment of an AI agent in a consequential workflow — financial, legal, medical, regulatory — hits the same wall:
17
+
18
+ - **System prompts are unenforceable.** The model may ignore them.
19
+ - **Tool allowlists are opaque.** They are deployer-side, invisible to counterparties and regulators.
20
+ - **Audit logs are held by the deployer.** Exactly the wrong party.
21
+ - **Smart contracts are unreadable** to the lawyers and compliance officers who need to sign off.
22
+
23
+ What is missing is a layer that is:
24
+ - Signed **before** work begins
25
+ - Deterministic in what it permits
26
+ - Readable by humans
27
+ - Enforceable at runtime
28
+ - Auditable by regulators
29
+
30
+ That is MandateClaw.
31
+
32
+ ---
33
+
34
+ ## Installation
35
+
36
+ ```ruby
37
+ # Gemfile
38
+ gem "mandateclaw-dsl"
39
+ ```
40
+
41
+ ```bash
42
+ bundle install
43
+ ```
44
+
45
+ Or install directly:
46
+
47
+ ```bash
48
+ gem install mandateclaw-dsl
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Quick Start
54
+
55
+ ```ruby
56
+ require "mandate_claw"
57
+
58
+ class InvoiceContract < MandateClaw::DSL::Contract
59
+ contract :invoice do
60
+ # ── Parties ──────────────────────────────────────────────────────────
61
+ party :buyer, identifies_by: :customer_id
62
+ party :seller, identifies_by: :merchant_id
63
+ party :ai_agent, identifies_by: :agent_did, kind: :autonomous
64
+
65
+ # ── Obligations: what a party MUST do ────────────────────────────────
66
+ obligation :pay_invoice,
67
+ on: :buyer,
68
+ within: 30.days,
69
+ breach: :late_payment_penalty
70
+
71
+ # ── Permissions: what a party MAY do ─────────────────────────────────
72
+ permission :dispute,
73
+ on: :buyer,
74
+ within: 7.days,
75
+ event: :raise_dispute
76
+
77
+ # ── Prohibitions: what a party MUST NOT do ───────────────────────────
78
+ prohibition :unilateral_amend,
79
+ on: :ai_agent,
80
+ breach: :void_transition
81
+
82
+ # ── Agent capability envelope ─────────────────────────────────────────
83
+ agent_bounds :ai_agent do
84
+ may :issue_invoice, :send_reminder
85
+ must_not :modify_amount, :waive_penalty
86
+ must_log_to :contract_registry
87
+ end
88
+
89
+ # ── Decision rules (ternary: true / false / :unknown) ─────────────────
90
+ rule :payment_overdue do
91
+ decide :overdue,
92
+ when: ->(ctx) { ctx.days_since_issued > 30 },
93
+ unknown_when: ->(ctx) { ctx.issued_at.nil? }
94
+ end
95
+
96
+ # ── Attestation: who must sign before the contract is in force ────────
97
+ attestation do
98
+ require_signature_from :buyer, :seller, :ai_agent
99
+ sign_with :ed25519
100
+ end
101
+ end
102
+ end
103
+ ```
104
+
105
+ ### Validate the contract definition
106
+
107
+ ```ruby
108
+ InvoiceContract.validate!
109
+ # => true (raises MandateClaw::ValidationError if malformed)
110
+ ```
111
+
112
+ ### Render as human-readable Markdown
113
+
114
+ ```ruby
115
+ puts InvoiceContract.to_markdown
116
+ ```
117
+
118
+ Output:
119
+
120
+ ```markdown
121
+ # Contract: Invoice
122
+
123
+ > Generated by MandateClaw-DSL v0.0.1
124
+
125
+ ## Parties
126
+
127
+ | Name | Kind | Identified By |
128
+ |----------|--------------|---------------|
129
+ | buyer | Human | customer_id |
130
+ | seller | Human | merchant_id |
131
+ | ai_agent | Autonomous | agent_did |
132
+
133
+ ## Obligations
134
+
135
+ - **Buyer** MUST `pay_invoice` within **30 days**. Breach: `late_payment_penalty`.
136
+
137
+ ## Permissions
138
+
139
+ - **Buyer** MAY `dispute` within **7 days**.
140
+
141
+ ## Prohibitions
142
+
143
+ - **Ai_agent** MUST NOT `unilateral_amend`. Breach: `void_transition`.
144
+
145
+ ## Agent Capability Bounds
146
+
147
+ ### Agent: `ai_agent`
148
+
149
+ - **May do:** `issue_invoice`, `send_reminder`
150
+ - **Must not do:** `modify_amount`, `waive_penalty`
151
+ - **Must log to:** `contract_registry`
152
+
153
+ ## Attestation
154
+
155
+ This contract comes into force only when all required parties have signed.
156
+
157
+ **Required signatories:**
158
+ - `buyer`
159
+ - `seller`
160
+ - `ai_agent`
161
+
162
+ **Signing algorithm:** `ed25519`
163
+ ```
164
+
165
+ ---
166
+
167
+ ## Core Concepts
168
+
169
+ ### Parties
170
+
171
+ ```ruby
172
+ party :buyer, identifies_by: :customer_id # human (default)
173
+ party :seller, identifies_by: :merchant_id, kind: :organisation
174
+ party :ai_agent, identifies_by: :agent_did, kind: :autonomous
175
+ ```
176
+
177
+ Party kinds: `:human`, `:organisation`, `:autonomous`.
178
+
179
+ ### Obligations, Permissions, Prohibitions
180
+
181
+ The three deontic modalities of contract law:
182
+
183
+ | DSL method | Meaning | Breach on failure? |
184
+ |---------------|----------------------|--------------------|
185
+ | `obligation` | Party MUST do this | Yes |
186
+ | `permission` | Party MAY do this | No |
187
+ | `prohibition` | Party MUST NOT do this | Yes (on attempt) |
188
+
189
+ ### Agent Bounds
190
+
191
+ The `agent_bounds` block is the contract clause that directly constrains an autonomous agent. The agent is given a capability envelope it cannot exceed:
192
+
193
+ ```ruby
194
+ agent_bounds :ai_agent do
195
+ may :read_invoice, :send_reminder # explicit allowlist
196
+ must_not :delete_invoice, :modify_amount # explicit denylist
197
+ must_log_to :contract_registry # mandatory audit targets
198
+ end
199
+ ```
200
+
201
+ ### Decision Rules (Ternary Truth)
202
+
203
+ Borrowed from [SMU's L4 language](https://l4-documentation.readthedocs.io/): a rule evaluates to `true`, `false`, or `:unknown` — never silently collapses an unknown value to false:
204
+
205
+ ```ruby
206
+ rule :payment_overdue do
207
+ decide :overdue,
208
+ when: ->(ctx) { ctx.days_since_issued > 30 },
209
+ unknown_when: ->(ctx) { ctx.issued_at.nil? }
210
+ end
211
+
212
+ result = InvoiceContract._rules.first.evaluate(my_context)
213
+ # => true | false | :unknown
214
+ ```
215
+
216
+ ### Inline Definition
217
+
218
+ ```ruby
219
+ PaymentContract = MandateClaw.define(:payment) do
220
+ party :payer, identifies_by: :user_id
221
+ party :payee, identifies_by: :merchant_id
222
+
223
+ obligation :transfer_funds, on: :payer, within: 1.day, breach: :payment_failed
224
+ end
225
+ ```
226
+
227
+ ---
228
+
229
+ ## Architecture
230
+
231
+ MandateClaw is intentionally split into focused components:
232
+
233
+ | Gem | Role | License |
234
+ |-----|------|---------|
235
+ | **mandateclaw-dsl** (this gem) | Contract definition DSL + Markdown renderer | MIT |
236
+ | [mandateclaw-registry](https://github.com/parolkar/MandateClaw-Registry) | Signing, storage, audit ledger | FSL-1.1-Apache-2.0 |
237
+ | [MandateClaw Cloud](https://mandateclaw.com) | Hosted registry, regulator webhooks, compliance dashboards | Commercial |
238
+
239
+ The DSL has no runtime enforcement — that is the registry's job. The DSL's output is a validated, human-readable contract definition that the registry stores, signs, and monitors.
240
+
241
+ ---
242
+
243
+ ## Roadmap
244
+
245
+ - [x] `0.0.1` — Core DSL: parties, obligations, permissions, prohibitions, agent bounds, attestation, decision rules, Markdown renderer
246
+ - [ ] `0.1.0` — `fosm-rails` integration: lifecycle events validated against contract definitions
247
+ - [ ] `0.2.0` — JSON Schema renderer for machine-readable contract exchange
248
+ - [ ] `0.3.0` — Contract diffing: show what changed between two versions of a contract
249
+
250
+ ---
251
+
252
+ ## Contributing
253
+
254
+ Bug reports and pull requests are welcome at [github.com/parolkar/MandateClaw-DSL](https://github.com/parolkar/MandateClaw-DSL).
255
+
256
+ Please read [CONTRIBUTING.md](CONTRIBUTING.md) before opening a PR. All contributions are expected to have specs.
257
+
258
+ ---
259
+
260
+ ## License
261
+
262
+ [MIT](LICENSE) — Copyright (c) 2026 Abhishek Parolkar
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # Defines the capability envelope for an autonomous agent party.
6
+ # The agent is constrained to only the actions listed under +may+
7
+ # and is explicitly barred from those under +must_not+.
8
+ #
9
+ # agent_bounds :ai_agent do
10
+ # may :issue_invoice, :send_reminder
11
+ # must_not :modify_amount, :waive_penalty
12
+ # must_log_to :contract_registry
13
+ # end
14
+ #
15
+ class AgentBounds
16
+ attr_reader :party, :permitted_actions, :prohibited_actions, :log_targets
17
+
18
+ def initialize(party)
19
+ @party = party
20
+ @permitted_actions = []
21
+ @prohibited_actions = []
22
+ @log_targets = []
23
+ end
24
+
25
+ def may(*actions)
26
+ @permitted_actions.concat(actions.flatten)
27
+ end
28
+
29
+ def must_not(*actions)
30
+ @prohibited_actions.concat(actions.flatten)
31
+ end
32
+
33
+ def must_log_to(*targets)
34
+ @log_targets.concat(targets.flatten)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # Defines signing requirements for the contract.
6
+ # A contract is not in force until all required parties have signed.
7
+ #
8
+ # attestation do
9
+ # require_signature_from :buyer, :seller, :ai_agent
10
+ # sign_with :ed25519
11
+ # end
12
+ #
13
+ class Attestation
14
+ SUPPORTED_ALGORITHMS = %i[ed25519 hmac_sha256].freeze
15
+
16
+ attr_reader :required_signatories, :algorithm
17
+
18
+ def initialize
19
+ @required_signatories = []
20
+ @algorithm = :ed25519
21
+ end
22
+
23
+ def require_signature_from(*parties)
24
+ @required_signatories.concat(parties.flatten)
25
+ end
26
+
27
+ def sign_with(algorithm)
28
+ unless SUPPORTED_ALGORITHMS.include?(algorithm)
29
+ raise ArgumentError, "Unsupported signing algorithm: #{algorithm}. " \
30
+ "Supported: #{SUPPORTED_ALGORITHMS.join(', ')}"
31
+ end
32
+ @algorithm = algorithm
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
5
+ module MandateClaw
6
+ module DSL
7
+ # Base class for all MandateClaw contracts.
8
+ #
9
+ # Usage:
10
+ #
11
+ # class InvoiceContract < MandateClaw::DSL::Contract
12
+ # contract do
13
+ # party :buyer, identifies_by: :customer_id
14
+ # party :seller, identifies_by: :merchant_id
15
+ # party :ai_agent, identifies_by: :agent_did, kind: :autonomous
16
+ #
17
+ # obligation :pay_invoice, on: :buyer, within: 30.days, breach: :late_payment_penalty
18
+ # permission :dispute, on: :buyer, within: 7.days, event: :raise_dispute
19
+ # prohibition :unilateral_amend, on: :ai_agent, breach: :void_transition
20
+ #
21
+ # agent_bounds :ai_agent do
22
+ # may :issue_invoice, :send_reminder
23
+ # must_not :modify_amount, :waive_penalty
24
+ # must_log_to :contract_registry
25
+ # end
26
+ #
27
+ # attestation do
28
+ # require_signature_from :buyer, :seller, :ai_agent
29
+ # sign_with :ed25519
30
+ # end
31
+ # end
32
+ # end
33
+ #
34
+ class Contract
35
+ class << self
36
+ attr_reader :_parties, :_obligations, :_permissions,
37
+ :_prohibitions, :_agent_bounds, :_attestation,
38
+ :_rules, :_contract_name
39
+
40
+ def contract(name = nil, &block)
41
+ @_contract_name = name || self.name&.underscore&.to_sym || :unnamed
42
+ @_parties = {}
43
+ @_obligations = []
44
+ @_permissions = []
45
+ @_prohibitions = []
46
+ @_agent_bounds = {}
47
+ @_attestation = nil
48
+ @_rules = []
49
+
50
+ DSL::ContractBuilder.new(self).instance_eval(&block) if block_given?
51
+ self
52
+ end
53
+
54
+ def validate!
55
+ MandateClaw::Validators::ContractValidator.new(self).validate!
56
+ end
57
+
58
+ def to_markdown
59
+ MandateClaw::Renderers::MarkdownRenderer.new(self).render
60
+ end
61
+
62
+ def autonomous_parties
63
+ _parties.select { |_, p| p.kind == :autonomous }.keys
64
+ end
65
+ end
66
+ end
67
+
68
+ # Internal builder — keeps the Contract class clean
69
+ class ContractBuilder
70
+ def initialize(target)
71
+ @target = target
72
+ end
73
+
74
+ def party(name, identifies_by:, kind: :human)
75
+ @target._parties[name] = DSL::Party.new(name, identifies_by: identifies_by, kind: kind)
76
+ end
77
+
78
+ def obligation(name, on:, within: nil, breach: nil)
79
+ @target._obligations << DSL::Obligation.new(name, on: on, within: within, breach: breach)
80
+ end
81
+
82
+ def permission(name, on:, within: nil, event: nil)
83
+ @target._permissions << DSL::Permission.new(name, on: on, within: within, event: event)
84
+ end
85
+
86
+ def prohibition(name, on:, breach: nil)
87
+ @target._prohibitions << DSL::Prohibition.new(name, on: on, breach: breach)
88
+ end
89
+
90
+ def agent_bounds(party_name, &block)
91
+ bounds = DSL::AgentBounds.new(party_name)
92
+ bounds.instance_eval(&block) if block_given?
93
+ @target._agent_bounds[party_name] = bounds
94
+ end
95
+
96
+ def rule(name, &block)
97
+ dr = DSL::DecisionRule.new(name)
98
+ dr.instance_eval(&block) if block_given?
99
+ @target._rules << dr
100
+ end
101
+
102
+ def attestation(&block)
103
+ att = DSL::Attestation.new
104
+ att.instance_eval(&block) if block_given?
105
+ @target._attestation = att
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # A named decision rule with ternary truth: true / false / :unknown.
6
+ # Borrowed from L4's model: an unknown value propagates uncertainty
7
+ # rather than defaulting to false.
8
+ #
9
+ # rule :payment_due_today do
10
+ # decide :due, when: ->(ctx) { ctx.issued_at + ctx.net_terms <= Date.today }
11
+ # unknown_when: ->(ctx) { ctx.issued_at.nil? }
12
+ # end
13
+ #
14
+ class DecisionRule
15
+ TRUTH_VALUES = [true, false, :unknown].freeze
16
+
17
+ attr_reader :name, :outcome, :condition, :unknown_condition
18
+
19
+ def initialize(name)
20
+ @name = name
21
+ end
22
+
23
+ def decide(outcome, when: nil, unknown_when: nil)
24
+ @outcome = outcome
25
+ @condition = binding.local_variable_get(:when)
26
+ @unknown_condition = unknown_when
27
+ end
28
+
29
+ # Evaluate against a context object.
30
+ # Returns true, false, or :unknown — never nil.
31
+ def evaluate(context)
32
+ return :unknown if @unknown_condition&.call(context)
33
+ return @condition.call(context) if @condition
34
+
35
+ :unknown
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # A positive duty: the named party MUST perform this action.
6
+ # Failure to perform within +within+ triggers the +breach+ consequence.
7
+ #
8
+ # obligation :pay_invoice, on: :buyer, within: 30.days, breach: :late_payment_penalty
9
+ #
10
+ Obligation = Struct.new(:name, :on, :within, :breach, keyword_init: true)
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ Party = Struct.new(:name, :identifies_by, :kind, keyword_init: true) do
6
+ KINDS = %i[human organisation autonomous].freeze
7
+
8
+ def autonomous?
9
+ kind == :autonomous
10
+ end
11
+
12
+ def valid?
13
+ KINDS.include?(kind) && name.is_a?(Symbol) && identifies_by.is_a?(Symbol)
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # A right: the named party MAY perform this action.
6
+ # Not performing is not a breach.
7
+ #
8
+ # permission :dispute, on: :buyer, within: 7.days, event: :raise_dispute
9
+ #
10
+ Permission = Struct.new(:name, :on, :within, :event, keyword_init: true)
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # An explicit denial: the named party MUST NOT perform this action.
6
+ # The attempt itself is a breach event, even if the action is blocked.
7
+ #
8
+ # prohibition :unilateral_amend, on: :ai_agent, breach: :void_transition
9
+ #
10
+ Prohibition = Struct.new(:name, :on, :breach, keyword_init: true)
11
+ end
12
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module DSL
5
+ # Represents a temporal constraint on an obligation or permission.
6
+ # Anchored to a named event or an absolute duration.
7
+ #
8
+ # Used internally; surface API is:
9
+ # obligation :pay, on: :buyer, within: 30.days
10
+ #
11
+ TemporalConstraint = Struct.new(:duration, :anchor, keyword_init: true) do
12
+ def deadline_from(base_time)
13
+ base_time + duration
14
+ end
15
+
16
+ def breached?(base_time, now = Time.current)
17
+ now > deadline_from(base_time)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module Renderers
5
+ # Renders a contract class as a human-readable Markdown document.
6
+ # This is the "bidirectional transpilation" — the same DSL that
7
+ # the Ruby runtime enforces can be read by lawyers and regulators.
8
+ class MarkdownRenderer
9
+ def initialize(contract_class)
10
+ @c = contract_class
11
+ end
12
+
13
+ def render
14
+ sections = []
15
+ sections << header
16
+ sections << parties_section
17
+ sections << obligations_section if @c._obligations.any?
18
+ sections << permissions_section if @c._permissions.any?
19
+ sections << prohibitions_section if @c._prohibitions.any?
20
+ sections << agent_bounds_section if @c._agent_bounds.any?
21
+ sections << rules_section if @c._rules.any?
22
+ sections << attestation_section if @c._attestation
23
+ sections << footer
24
+ sections.compact.join("\n\n")
25
+ end
26
+
27
+ private
28
+
29
+ def header
30
+ <<~MD
31
+ # Contract: #{contract_title}
32
+
33
+ > Generated by [MandateClaw-DSL](https://mandateclaw.com) v#{MandateClaw::VERSION}
34
+ > This document is the human-readable rendering of a programmatic contract.
35
+ > The Ruby definition is the authoritative source of enforcement.
36
+ MD
37
+ end
38
+
39
+ def contract_title
40
+ @c._contract_name.to_s.humanize.titleize
41
+ rescue StandardError
42
+ @c._contract_name.to_s
43
+ end
44
+
45
+ def parties_section
46
+ rows = @c._parties.map do |name, party|
47
+ "| `#{name}` | #{party.kind.to_s.capitalize} | `#{party.identifies_by}` |"
48
+ end
49
+
50
+ <<~MD
51
+ ## Parties
52
+
53
+ | Name | Kind | Identified By |
54
+ |------|------|---------------|
55
+ #{rows.join("\n")}
56
+ MD
57
+ end
58
+
59
+ def obligations_section
60
+ items = @c._obligations.map do |ob|
61
+ deadline = ob.within ? " within **#{humanize_duration(ob.within)}**" : ""
62
+ breach = ob.breach ? " Breach: `#{ob.breach}`." : ""
63
+ "- **#{ob.on.to_s.capitalize}** MUST `#{ob.name}`#{deadline}.#{breach}"
64
+ end
65
+
66
+ "## Obligations\n\n#{items.join("\n")}"
67
+ end
68
+
69
+ def permissions_section
70
+ items = @c._permissions.map do |pr|
71
+ deadline = pr.within ? " within **#{humanize_duration(pr.within)}**" : ""
72
+ "- **#{pr.on.to_s.capitalize}** MAY `#{pr.name}`#{deadline}."
73
+ end
74
+
75
+ "## Permissions\n\n#{items.join("\n")}"
76
+ end
77
+
78
+ def prohibitions_section
79
+ items = @c._prohibitions.map do |pr|
80
+ breach = pr.breach ? " Breach: `#{pr.breach}`." : ""
81
+ "- **#{pr.on.to_s.capitalize}** MUST NOT `#{pr.name}`.#{breach}"
82
+ end
83
+
84
+ "## Prohibitions\n\n#{items.join("\n")}"
85
+ end
86
+
87
+ def agent_bounds_section
88
+ sections = @c._agent_bounds.map do |name, bounds|
89
+ permitted = bounds.permitted_actions.map { |a| "`#{a}`" }.join(", ")
90
+ prohibited = bounds.prohibited_actions.map { |a| "`#{a}`" }.join(", ")
91
+ log_targets = bounds.log_targets.map { |t| "`#{t}`" }.join(", ")
92
+
93
+ <<~MD
94
+ ### Agent: `#{name}`
95
+
96
+ - **May do:** #{permitted.empty? ? "_none specified_" : permitted}
97
+ - **Must not do:** #{prohibited.empty? ? "_none specified_" : prohibited}
98
+ - **Must log to:** #{log_targets.empty? ? "_none specified_" : log_targets}
99
+ MD
100
+ end
101
+
102
+ "## Agent Capability Bounds\n\n#{sections.join("\n")}"
103
+ end
104
+
105
+ def rules_section
106
+ items = @c._rules.map do |rule|
107
+ "- **#{rule.name}**: evaluates to `true`, `false`, or `:unknown` (ternary)"
108
+ end
109
+
110
+ "## Decision Rules\n\n#{items.join("\n")}"
111
+ end
112
+
113
+ def attestation_section
114
+ att = @c._attestation
115
+ sigs = att.required_signatories.map { |s| "- `#{s}`" }.join("\n")
116
+
117
+ <<~MD
118
+ ## Attestation
119
+
120
+ This contract comes into force only when all required parties have signed.
121
+
122
+ **Required signatories:**
123
+ #{sigs}
124
+
125
+ **Signing algorithm:** `#{att.algorithm}`
126
+ MD
127
+ end
128
+
129
+ def footer
130
+ "---\n_MandateClaw — mandateclaw.com_"
131
+ end
132
+
133
+ def humanize_duration(duration)
134
+ return duration.inspect unless duration.respond_to?(:to_i)
135
+
136
+ seconds = duration.to_i
137
+ if seconds >= 86_400
138
+ "#{seconds / 86_400} days"
139
+ elsif seconds >= 3_600
140
+ "#{seconds / 3_600} hours"
141
+ else
142
+ "#{seconds} seconds"
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end
@@ -0,0 +1,83 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ module Validators
5
+ class ContractValidator
6
+ def initialize(contract_class)
7
+ @contract = contract_class
8
+ @errors = []
9
+ end
10
+
11
+ def validate!
12
+ validate_parties
13
+ validate_obligations
14
+ validate_prohibitions
15
+ validate_agent_bounds
16
+ validate_attestation
17
+
18
+ raise MandateClaw::ValidationError, @errors.join("; ") if @errors.any?
19
+
20
+ true
21
+ end
22
+
23
+ private
24
+
25
+ def validate_parties
26
+ if @contract._parties.empty?
27
+ @errors << "Contract must define at least one party"
28
+ return
29
+ end
30
+
31
+ @contract._parties.each do |name, party|
32
+ @errors << "Party #{name} has invalid kind: #{party.kind}" unless party.valid?
33
+ end
34
+ end
35
+
36
+ def validate_obligations
37
+ @contract._obligations.each do |ob|
38
+ unless @contract._parties.key?(ob.on)
39
+ @errors << "Obligation #{ob.name}: unknown party #{ob.on}"
40
+ end
41
+ end
42
+ end
43
+
44
+ def validate_prohibitions
45
+ @contract._prohibitions.each do |pr|
46
+ unless @contract._parties.key?(pr.on)
47
+ @errors << "Prohibition #{pr.name}: unknown party #{pr.on}"
48
+ end
49
+ end
50
+ end
51
+
52
+ def validate_agent_bounds
53
+ @contract._agent_bounds.each do |party_name, bounds|
54
+ unless @contract._parties.key?(party_name)
55
+ @errors << "agent_bounds references unknown party: #{party_name}"
56
+ end
57
+
58
+ party = @contract._parties[party_name]
59
+ unless party&.autonomous?
60
+ @errors << "agent_bounds can only be applied to autonomous parties; " \
61
+ "#{party_name} has kind: #{party&.kind}"
62
+ end
63
+
64
+ overlap = bounds.permitted_actions & bounds.prohibited_actions
65
+ if overlap.any?
66
+ @errors << "agent_bounds for #{party_name}: actions #{overlap.join(', ')} " \
67
+ "appear in both may and must_not"
68
+ end
69
+ end
70
+ end
71
+
72
+ def validate_attestation
73
+ return unless @contract._attestation
74
+
75
+ @contract._attestation.required_signatories.each do |sig|
76
+ unless @contract._parties.key?(sig)
77
+ @errors << "Attestation requires signature from unknown party: #{sig}"
78
+ end
79
+ end
80
+ end
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MandateClaw
4
+ VERSION = "0.0.1"
5
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support"
4
+ require "active_support/core_ext"
5
+
6
+ require "mandate_claw/version"
7
+ require "mandate_claw/dsl/contract"
8
+ require "mandate_claw/dsl/party"
9
+ require "mandate_claw/dsl/obligation"
10
+ require "mandate_claw/dsl/permission"
11
+ require "mandate_claw/dsl/prohibition"
12
+ require "mandate_claw/dsl/agent_bounds"
13
+ require "mandate_claw/dsl/attestation"
14
+ require "mandate_claw/dsl/temporal"
15
+ require "mandate_claw/dsl/decision_rule"
16
+ require "mandate_claw/validators/contract_validator"
17
+ require "mandate_claw/renderers/markdown_renderer"
18
+
19
+ module MandateClaw
20
+ class Error < StandardError; end
21
+ class ValidationError < Error; end
22
+ class BreachError < Error; end
23
+
24
+ # Convenience: define a contract class inline
25
+ #
26
+ # MandateClaw.define(:invoice_contract) do
27
+ # party :buyer, identifies_by: :customer_id
28
+ # ...
29
+ # end
30
+ #
31
+ def self.define(name, &block)
32
+ contract = Class.new(MandateClaw::DSL::Contract)
33
+ contract.contract(&block)
34
+ contract
35
+ end
36
+ end
metadata ADDED
@@ -0,0 +1,120 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mandateclaw-dsl
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Abhishek Parolkar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-05-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '7.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '7.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3.12'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3.12'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.60'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.60'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop-rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.25'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.25'
69
+ description: MandateClaw-DSL is a Ruby DSL for defining programmatic, signed, and
70
+ auditable contracts that govern what an AI agent is and isn't allowed to do on behalf
71
+ of a human or organisation.
72
+ email:
73
+ - abhishek@parolkar.com
74
+ executables: []
75
+ extensions: []
76
+ extra_rdoc_files: []
77
+ files:
78
+ - CHANGELOG.md
79
+ - LICENSE
80
+ - README.md
81
+ - lib/mandate_claw.rb
82
+ - lib/mandate_claw/dsl/agent_bounds.rb
83
+ - lib/mandate_claw/dsl/attestation.rb
84
+ - lib/mandate_claw/dsl/contract.rb
85
+ - lib/mandate_claw/dsl/decision_rule.rb
86
+ - lib/mandate_claw/dsl/obligation.rb
87
+ - lib/mandate_claw/dsl/party.rb
88
+ - lib/mandate_claw/dsl/permission.rb
89
+ - lib/mandate_claw/dsl/prohibition.rb
90
+ - lib/mandate_claw/dsl/temporal.rb
91
+ - lib/mandate_claw/renderers/markdown_renderer.rb
92
+ - lib/mandate_claw/validators/contract_validator.rb
93
+ - lib/mandate_claw/version.rb
94
+ homepage: https://mandateclaw.com
95
+ licenses:
96
+ - MIT
97
+ metadata:
98
+ homepage_uri: https://mandateclaw.com
99
+ source_code_uri: https://github.com/parolkar/MandateClaw-DSL
100
+ changelog_uri: https://github.com/parolkar/MandateClaw-DSL/blob/main/CHANGELOG.md
101
+ post_install_message:
102
+ rdoc_options: []
103
+ require_paths:
104
+ - lib
105
+ required_ruby_version: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 3.1.0
110
+ required_rubygems_version: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - ">="
113
+ - !ruby/object:Gem::Version
114
+ version: '0'
115
+ requirements: []
116
+ rubygems_version: 3.5.22
117
+ signing_key:
118
+ specification_version: 4
119
+ summary: The Employment Contract Between You and Your Agent
120
+ test_files: []