mandateclaw-registry 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 +7 -0
- data/CHANGELOG.md +14 -0
- data/LICENSE +45 -0
- data/README.md +179 -0
- data/app/controllers/mandate_claw/api/v1/breach_events_controller.rb +48 -0
- data/app/controllers/mandate_claw/api/v1/contracts_controller.rb +77 -0
- data/app/models/mandate_claw/breach_event.rb +31 -0
- data/app/models/mandate_claw/signed_contract.rb +58 -0
- data/app/models/mandate_claw/transition_link.rb +17 -0
- data/db/migrate/001_create_mandate_claw_signed_contracts.rb +25 -0
- data/db/migrate/002_create_mandate_claw_breach_events.rb +21 -0
- data/db/migrate/003_create_mandate_claw_transition_links.rb +21 -0
- data/lib/mandate_claw/registry/engine.rb +14 -0
- data/lib/mandate_claw/registry/signing/ed25519_signer.rb +42 -0
- data/lib/mandate_claw/registry/signing/hmac_signer.rb +24 -0
- data/lib/mandate_claw/registry/task_contract.rb +110 -0
- data/lib/mandate_claw/registry/version.rb +7 -0
- data/lib/mandate_claw/registry.rb +17 -0
- metadata +148 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 1a92d4ec897ce54f6d0db4ce8227822743ed0bac629753be7e7c876d6c163a8b
|
|
4
|
+
data.tar.gz: 17cc935df9338d2d142d2d47e223385eef96ba8d5059da90b55fa2935e65b28c
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 76ba21b99c177fc54759168903296da3c6aef0747cf99c5973ed761f9368c51c5d3bbad86b9b9f0356a63f3577e35ade56e8ee315dd7fdabe498195e67c03264
|
|
7
|
+
data.tar.gz: ad75a26f15c5c02edb1061c034ed555b84c13008d28fcc971ad5e6fb99a80ace5eec69f24a84182e83d609a61706e9b0e139ad6f79e68e97bd9ea7080d6f2dd1
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.0.1] - 2026-05-21
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
- `MandateClaw::SignedContract` — immutable record of a signed, anchored contract
|
|
7
|
+
- `MandateClaw::BreachEvent` — append-only log of prohibited action attempts
|
|
8
|
+
- `MandateClaw::TransitionLink` — audit link between a FOSM transition and a contract
|
|
9
|
+
- `MandateClaw::Registry::TaskContract` — instantiation and signing workflow
|
|
10
|
+
- `MandateClaw::Registry::Signing::Ed25519Signer` — Ed25519 keypair generation and verification
|
|
11
|
+
- `MandateClaw::Registry::Signing::HmacSigner` — HMAC-SHA256 for service-to-service contexts
|
|
12
|
+
- JSON API v1: `POST /contracts`, `GET /contracts/:id`, `POST /contracts/:id/verify`
|
|
13
|
+
- Breach events API: `GET` and `POST` under `/contracts/:id/breach_events`
|
|
14
|
+
- Three database migrations (signed_contracts, breach_events, transition_links)
|
data/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
Functional Source License, Version 1.1, Apache 2.0 Future License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Abhishek Parolkar
|
|
4
|
+
|
|
5
|
+
Parameters
|
|
6
|
+
|
|
7
|
+
Licensor: Abhishek Parolkar
|
|
8
|
+
Licensed Work: MandateClaw-Registry
|
|
9
|
+
Additional Use Grant: None
|
|
10
|
+
Change Date: Four years from the date the Licensed Work is published.
|
|
11
|
+
Change License: Apache License, Version 2.0
|
|
12
|
+
|
|
13
|
+
For information about alternative licensing arrangements, contact abhishek@parolkar.com
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
Business Source License 1.1
|
|
18
|
+
|
|
19
|
+
Terms
|
|
20
|
+
|
|
21
|
+
The Licensor hereby grants you the right to copy, modify, create derivative works,
|
|
22
|
+
redistribute, and make non-production use of the Licensed Work.
|
|
23
|
+
|
|
24
|
+
The Licensor may make an Additional Use Grant, above, permitting limited production use.
|
|
25
|
+
|
|
26
|
+
Effective on the Change Date, or the fourth anniversary of the first publicly available
|
|
27
|
+
distribution of a specific version of the Licensed Work under this License, whichever
|
|
28
|
+
comes first, the Licensor hereby grants you rights under the terms of the Change License,
|
|
29
|
+
and the rights granted in the paragraph above terminate.
|
|
30
|
+
|
|
31
|
+
If your use of the Licensed Work does not comply with the requirements currently in effect
|
|
32
|
+
as described in this License, you must purchase a commercial license from the Licensor,
|
|
33
|
+
its affiliated entities, or authorized resellers, or you must refrain from using the
|
|
34
|
+
Licensed Work.
|
|
35
|
+
|
|
36
|
+
All copies of the original and modified Licensed Work, and derivative works of the
|
|
37
|
+
Licensed Work, are subject to this License.
|
|
38
|
+
|
|
39
|
+
This License does not grant you any right in any trademark or logo of Licensor or its
|
|
40
|
+
affiliates.
|
|
41
|
+
|
|
42
|
+
TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS"
|
|
43
|
+
BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED,
|
|
44
|
+
INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
45
|
+
PURPOSE, NON-INFRINGEMENT, AND TITLE.
|
data/README.md
ADDED
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# MandateClaw-Registry
|
|
2
|
+
|
|
3
|
+
> The signing, storage, and audit layer for MandateClaw contracts.
|
|
4
|
+
|
|
5
|
+
MandateClaw-Registry is a Rails engine that receives signed MandateClaw contracts, stores them in an append-only ledger, links them to transition logs, and records breach events — even when the action was blocked.
|
|
6
|
+
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## What it does
|
|
12
|
+
|
|
13
|
+
| Responsibility | How |
|
|
14
|
+
|---|---|
|
|
15
|
+
| Store signed contracts | `mandate_claw_signed_contracts` — immutable after creation |
|
|
16
|
+
| Sign with Ed25519 | `MandateClaw::Registry::Signing::Ed25519Signer` |
|
|
17
|
+
| Sign with HMAC-SHA256 | `MandateClaw::Registry::Signing::HmacSigner` (v0 deployments) |
|
|
18
|
+
| Record breach attempts | `mandate_claw_breach_events` — append-only, even blocked actions |
|
|
19
|
+
| Link transitions to contracts | `mandate_claw_transition_links` — FOSM transition ↔ contract |
|
|
20
|
+
| Verify contract validity | `POST /api/v1/contracts/:id/verify` |
|
|
21
|
+
| Revoke contracts | `POST /api/v1/contracts/:id/revoke` |
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Installation
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
# Gemfile
|
|
29
|
+
gem "mandateclaw-registry"
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Run the migrations:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
bundle exec rails mandate_claw_registry:install:migrations
|
|
36
|
+
bundle exec rails db:migrate
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Mount the engine in your `config/routes.rb`:
|
|
40
|
+
|
|
41
|
+
```ruby
|
|
42
|
+
mount MandateClaw::Registry::Engine => "/mandate_claw"
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
---
|
|
46
|
+
|
|
47
|
+
## Signing and Anchoring a Contract
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
require "mandate_claw/registry"
|
|
51
|
+
|
|
52
|
+
# 1. Instantiate a task contract from a DSL template
|
|
53
|
+
tc = MandateClaw::Registry::TaskContract.new(
|
|
54
|
+
template: InvoiceContract, # a MandateClaw::DSL::Contract subclass
|
|
55
|
+
scope: invoice, # the Rails object this contract governs
|
|
56
|
+
parties: {
|
|
57
|
+
buyer: current_user,
|
|
58
|
+
seller: merchant,
|
|
59
|
+
ai_agent: agent
|
|
60
|
+
},
|
|
61
|
+
validity: 24.hours
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
# 2. Each party signs the canonical contract digest
|
|
65
|
+
tc.sign_as(:buyer,
|
|
66
|
+
key: current_user.signing_key_hex, # Ed25519 signing key
|
|
67
|
+
algorithm: :ed25519)
|
|
68
|
+
|
|
69
|
+
tc.sign_as(:ai_agent,
|
|
70
|
+
key: agent.capability_key_hex,
|
|
71
|
+
attests: agent.capability_manifest, # what the agent claims it can do
|
|
72
|
+
algorithm: :ed25519)
|
|
73
|
+
|
|
74
|
+
tc.sign_as(:seller,
|
|
75
|
+
key: merchant.signing_key_hex)
|
|
76
|
+
|
|
77
|
+
# 3. Anchor — persists to the registry database
|
|
78
|
+
record = tc.anchor!
|
|
79
|
+
|
|
80
|
+
# record.id is now the contract_id to attach to all subsequent transitions
|
|
81
|
+
puts record.contract_digest # SHA-256 of the canonical payload
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
## Recording a Breach Event
|
|
87
|
+
|
|
88
|
+
When the runtime blocks a prohibited action, record the attempt:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
contract = MandateClaw::SignedContract.find(contract_id)
|
|
92
|
+
|
|
93
|
+
contract.breach_events.create!(
|
|
94
|
+
breach_type: :prohibition_attempted,
|
|
95
|
+
party: "ai_agent",
|
|
96
|
+
action: "modify_amount",
|
|
97
|
+
occurred_at: Time.current,
|
|
98
|
+
context_json: { invoice_id: invoice.id, attempted_value: 9999 }.to_json
|
|
99
|
+
)
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
The breach is stored even though the action was blocked. This is the audit evidence.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## JSON API
|
|
107
|
+
|
|
108
|
+
### Create a contract
|
|
109
|
+
```
|
|
110
|
+
POST /mandate_claw/api/v1/contracts
|
|
111
|
+
Content-Type: application/json
|
|
112
|
+
|
|
113
|
+
{
|
|
114
|
+
"contract": {
|
|
115
|
+
"contract_digest": "abc123...",
|
|
116
|
+
"template_name": "invoice",
|
|
117
|
+
"scope_type": "Invoice",
|
|
118
|
+
"scope_id": "42",
|
|
119
|
+
"parties_json": "{...}",
|
|
120
|
+
"signatures_json": "{...}",
|
|
121
|
+
"rendered_markdown": "# Contract: Invoice\n...",
|
|
122
|
+
"expires_at": "2026-05-22T09:00:00Z"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Verify a contract
|
|
128
|
+
```
|
|
129
|
+
POST /mandate_claw/api/v1/contracts/:id/verify
|
|
130
|
+
|
|
131
|
+
→ { "valid": true, "status": "active", "expires_at": "..." }
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### List breach events
|
|
135
|
+
```
|
|
136
|
+
GET /mandate_claw/api/v1/contracts/:id/breach_events
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
---
|
|
140
|
+
|
|
141
|
+
## Database Schema
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
mandate_claw_signed_contracts
|
|
145
|
+
id, contract_digest (unique), template_name, scope_type, scope_id,
|
|
146
|
+
parties_json, signatures_json, rendered_markdown,
|
|
147
|
+
status (active/expired/revoked), expires_at, revoked_at, revocation_reason,
|
|
148
|
+
created_at, updated_at
|
|
149
|
+
|
|
150
|
+
mandate_claw_breach_events
|
|
151
|
+
id, signed_contract_id (nullable), breach_type, party, action,
|
|
152
|
+
context_json, occurred_at, created_at, updated_at
|
|
153
|
+
|
|
154
|
+
mandate_claw_transition_links
|
|
155
|
+
id, signed_contract_id, transition_log_id (unique),
|
|
156
|
+
transition_model, actor_party, event_name, transitioned_at,
|
|
157
|
+
created_at, updated_at
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
## Architecture
|
|
163
|
+
|
|
164
|
+
```
|
|
165
|
+
mandateclaw-dsl → defines the contract
|
|
166
|
+
mandateclaw-registry → signs, stores, audits (this gem)
|
|
167
|
+
MandateClaw Cloud → Merkle ledger, regulator webhooks, dashboards
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
The registry is intentionally minimal — a single Postgres schema and a JSON API. All advanced features (Merkle anchoring, multi-tenant, regulator integrations, compliance dashboards) are MandateClaw Cloud.
|
|
171
|
+
|
|
172
|
+
---
|
|
173
|
+
|
|
174
|
+
## License
|
|
175
|
+
|
|
176
|
+
[FSL-1.1-Apache-2.0](LICENSE) — converts to Apache 2.0 four years after publication.
|
|
177
|
+
Commercial use before the Change Date requires a license from abhishek@parolkar.com.
|
|
178
|
+
|
|
179
|
+
Copyright (c) 2026 Abhishek Parolkar
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MandateClaw
|
|
4
|
+
module Api
|
|
5
|
+
module V1
|
|
6
|
+
class BreachEventsController < ActionController::API
|
|
7
|
+
before_action :find_contract
|
|
8
|
+
|
|
9
|
+
# GET /mandate_claw/api/v1/contracts/:contract_id/breach_events
|
|
10
|
+
def index
|
|
11
|
+
events = @contract.breach_events.order(occurred_at: :desc)
|
|
12
|
+
render json: events.map { |e| serialize(e) }
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# POST /mandate_claw/api/v1/contracts/:contract_id/breach_events
|
|
16
|
+
# Called by the runtime when a prohibited action is attempted.
|
|
17
|
+
def create
|
|
18
|
+
event = @contract.breach_events.create!(breach_event_params)
|
|
19
|
+
render json: serialize(event), status: :created
|
|
20
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
21
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def find_contract
|
|
27
|
+
@contract = SignedContract.find(params[:contract_id])
|
|
28
|
+
rescue ActiveRecord::RecordNotFound
|
|
29
|
+
render json: { error: "Contract not found" }, status: :not_found
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def breach_event_params
|
|
33
|
+
params.require(:breach_event).permit(:breach_type, :party, :action, :context_json)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def serialize(event)
|
|
37
|
+
{
|
|
38
|
+
id: event.id,
|
|
39
|
+
breach_type: event.breach_type,
|
|
40
|
+
party: event.party,
|
|
41
|
+
action: event.action,
|
|
42
|
+
occurred_at: event.occurred_at
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MandateClaw
|
|
4
|
+
module Api
|
|
5
|
+
module V1
|
|
6
|
+
class ContractsController < ActionController::API
|
|
7
|
+
before_action :find_contract, only: %i[show verify revoke]
|
|
8
|
+
|
|
9
|
+
# POST /mandate_claw/api/v1/contracts
|
|
10
|
+
# Body: { contract_digest, template_name, scope_type, scope_id,
|
|
11
|
+
# parties_json, signatures_json, rendered_markdown, expires_at }
|
|
12
|
+
def create
|
|
13
|
+
contract = SignedContract.create!(contract_params)
|
|
14
|
+
render json: serialize(contract), status: :created
|
|
15
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
16
|
+
render json: { error: e.message }, status: :unprocessable_entity
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# GET /mandate_claw/api/v1/contracts/:id
|
|
20
|
+
def show
|
|
21
|
+
render json: serialize(@contract)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# POST /mandate_claw/api/v1/contracts/:id/verify
|
|
25
|
+
# Returns whether the contract is active and not expired.
|
|
26
|
+
def verify
|
|
27
|
+
render json: {
|
|
28
|
+
contract_id: @contract.id,
|
|
29
|
+
digest: @contract.contract_digest,
|
|
30
|
+
valid: @contract.active? && !@contract.expired?,
|
|
31
|
+
status: @contract.status,
|
|
32
|
+
expires_at: @contract.expires_at
|
|
33
|
+
}
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# POST /mandate_claw/api/v1/contracts/:id/revoke
|
|
37
|
+
def revoke
|
|
38
|
+
@contract.revoke!(reason: params[:reason])
|
|
39
|
+
render json: { revoked: true, contract_id: @contract.id }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def find_contract
|
|
45
|
+
@contract = SignedContract.find(params[:id])
|
|
46
|
+
rescue ActiveRecord::RecordNotFound
|
|
47
|
+
render json: { error: "Contract not found" }, status: :not_found
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def contract_params
|
|
51
|
+
params.require(:contract).permit(
|
|
52
|
+
:contract_digest, :template_name, :scope_type, :scope_id,
|
|
53
|
+
:parties_json, :signatures_json, :rendered_markdown, :expires_at
|
|
54
|
+
)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def serialize(contract)
|
|
58
|
+
{
|
|
59
|
+
id: contract.id,
|
|
60
|
+
contract_digest: contract.contract_digest,
|
|
61
|
+
template_name: contract.template_name,
|
|
62
|
+
scope_type: contract.scope_type,
|
|
63
|
+
scope_id: contract.scope_id,
|
|
64
|
+
parties: contract.parties,
|
|
65
|
+
status: contract.status,
|
|
66
|
+
expires_at: contract.expires_at,
|
|
67
|
+
created_at: contract.created_at,
|
|
68
|
+
_links: {
|
|
69
|
+
self: mandate_claw_api_v1_contract_url(contract),
|
|
70
|
+
verify: verify_mandate_claw_api_v1_contract_url(contract)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MandateClaw
|
|
4
|
+
# Records an attempted prohibited action — even if it was blocked.
|
|
5
|
+
# The attempt itself is evidence for regulatory audit.
|
|
6
|
+
#
|
|
7
|
+
# Breach events are append-only: they are never updated or deleted.
|
|
8
|
+
class BreachEvent < ApplicationRecord
|
|
9
|
+
self.table_name = "mandate_claw_breach_events"
|
|
10
|
+
|
|
11
|
+
belongs_to :signed_contract, optional: true
|
|
12
|
+
|
|
13
|
+
enum :breach_type, {
|
|
14
|
+
prohibition_attempted: "prohibition_attempted",
|
|
15
|
+
obligation_missed: "obligation_missed",
|
|
16
|
+
temporal_violation: "temporal_violation",
|
|
17
|
+
capability_exceeded: "capability_exceeded"
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
validates :breach_type, presence: true
|
|
21
|
+
validates :party, presence: true
|
|
22
|
+
validates :action, presence: true
|
|
23
|
+
validates :occurred_at, presence: true
|
|
24
|
+
|
|
25
|
+
before_create { self.occurred_at ||= Time.current }
|
|
26
|
+
|
|
27
|
+
# Breach events are append-only
|
|
28
|
+
before_update { raise ActiveRecord::ReadOnlyRecord, "BreachEvents are immutable" }
|
|
29
|
+
before_destroy { raise ActiveRecord::ReadOnlyRecord, "BreachEvents cannot be deleted" }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MandateClaw
|
|
4
|
+
# Immutable record of a fully-signed, anchored task contract.
|
|
5
|
+
# Once created, only +status+ and +revoked_at+ may change.
|
|
6
|
+
class SignedContract < ApplicationRecord
|
|
7
|
+
self.table_name = "mandate_claw_signed_contracts"
|
|
8
|
+
|
|
9
|
+
has_many :breach_events, foreign_key: :signed_contract_id, dependent: :nullify
|
|
10
|
+
has_many :transition_links, foreign_key: :signed_contract_id, dependent: :nullify
|
|
11
|
+
|
|
12
|
+
enum :status, { active: "active", expired: "expired", revoked: "revoked" }
|
|
13
|
+
|
|
14
|
+
validates :contract_digest, presence: true, uniqueness: true
|
|
15
|
+
validates :template_name, presence: true
|
|
16
|
+
validates :scope_type, presence: true
|
|
17
|
+
validates :scope_id, presence: true
|
|
18
|
+
validates :parties_json, presence: true
|
|
19
|
+
validates :signatures_json, presence: true
|
|
20
|
+
validates :rendered_markdown, presence: true
|
|
21
|
+
validates :expires_at, presence: true
|
|
22
|
+
|
|
23
|
+
before_create :freeze_immutable_fields
|
|
24
|
+
after_create :schedule_expiry
|
|
25
|
+
|
|
26
|
+
scope :active_for, ->(scope_type, scope_id) {
|
|
27
|
+
where(scope_type: scope_type, scope_id: scope_id.to_s, status: :active)
|
|
28
|
+
.where("expires_at > ?", Time.current)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
def parties
|
|
32
|
+
JSON.parse(parties_json)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def signatures
|
|
36
|
+
JSON.parse(signatures_json)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def expired?
|
|
40
|
+
expires_at < Time.current || status == "expired"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def revoke!(reason: nil)
|
|
44
|
+
update!(status: :revoked, revoked_at: Time.current, revocation_reason: reason)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def freeze_immutable_fields
|
|
50
|
+
# Core fields are set once on create — prevent accidental mutation
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def schedule_expiry
|
|
54
|
+
# In production, hook into an ActiveJob to flip status to :expired
|
|
55
|
+
# when expires_at is reached. Registry Cloud handles this automatically.
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MandateClaw
|
|
4
|
+
# Links a FOSM transition log entry to the signed contract that authorised it.
|
|
5
|
+
# Provides the direct audit trail: "this AI action was bounded by contract #X."
|
|
6
|
+
class TransitionLink < ApplicationRecord
|
|
7
|
+
self.table_name = "mandate_claw_transition_links"
|
|
8
|
+
|
|
9
|
+
belongs_to :signed_contract
|
|
10
|
+
|
|
11
|
+
validates :transition_log_id, presence: true, uniqueness: true
|
|
12
|
+
validates :transition_model, presence: true
|
|
13
|
+
validates :actor_party, presence: true
|
|
14
|
+
validates :event_name, presence: true
|
|
15
|
+
validates :transitioned_at, presence: true
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateMandateClawSignedContracts < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :mandate_claw_signed_contracts do |t|
|
|
6
|
+
t.string :contract_digest, null: false, index: { unique: true }
|
|
7
|
+
t.string :template_name, null: false
|
|
8
|
+
t.string :scope_type, null: false
|
|
9
|
+
t.string :scope_id, null: false
|
|
10
|
+
t.text :parties_json, null: false
|
|
11
|
+
t.text :signatures_json, null: false
|
|
12
|
+
t.text :rendered_markdown, null: false
|
|
13
|
+
t.string :status, null: false, default: "active"
|
|
14
|
+
t.datetime :expires_at, null: false
|
|
15
|
+
t.datetime :revoked_at
|
|
16
|
+
t.text :revocation_reason
|
|
17
|
+
|
|
18
|
+
t.timestamps
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
add_index :mandate_claw_signed_contracts, %i[scope_type scope_id status]
|
|
22
|
+
add_index :mandate_claw_signed_contracts, :template_name
|
|
23
|
+
add_index :mandate_claw_signed_contracts, :expires_at
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateMandateClawBreachEvents < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :mandate_claw_breach_events do |t|
|
|
6
|
+
t.references :signed_contract, foreign_key: { to_table: :mandate_claw_signed_contracts },
|
|
7
|
+
null: true # null allowed — breach may occur before a contract is located
|
|
8
|
+
t.string :breach_type, null: false
|
|
9
|
+
t.string :party, null: false
|
|
10
|
+
t.string :action, null: false
|
|
11
|
+
t.text :context_json
|
|
12
|
+
t.datetime :occurred_at, null: false
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :mandate_claw_breach_events, :breach_type
|
|
18
|
+
add_index :mandate_claw_breach_events, :party
|
|
19
|
+
add_index :mandate_claw_breach_events, :occurred_at
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
class CreateMandateClawTransitionLinks < ActiveRecord::Migration[7.1]
|
|
4
|
+
def change
|
|
5
|
+
create_table :mandate_claw_transition_links do |t|
|
|
6
|
+
t.references :signed_contract, null: false,
|
|
7
|
+
foreign_key: { to_table: :mandate_claw_signed_contracts }
|
|
8
|
+
t.bigint :transition_log_id, null: false
|
|
9
|
+
t.string :transition_model, null: false
|
|
10
|
+
t.string :actor_party, null: false
|
|
11
|
+
t.string :event_name, null: false
|
|
12
|
+
t.datetime :transitioned_at, null: false
|
|
13
|
+
|
|
14
|
+
t.timestamps
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
add_index :mandate_claw_transition_links, :transition_log_id, unique: true
|
|
18
|
+
add_index :mandate_claw_transition_links, :event_name
|
|
19
|
+
add_index :mandate_claw_transition_links, :actor_party
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MandateClaw
|
|
4
|
+
module Registry
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
isolate_namespace MandateClaw::Registry
|
|
7
|
+
|
|
8
|
+
config.generators do |g|
|
|
9
|
+
g.test_framework :rspec
|
|
10
|
+
g.fixture_replacement :factory_bot
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "ed25519"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module MandateClaw
|
|
7
|
+
module Registry
|
|
8
|
+
module Signing
|
|
9
|
+
# Ed25519 signing for MandateClaw task contracts.
|
|
10
|
+
# Each party signs the canonical contract digest.
|
|
11
|
+
# The signature is stored alongside the contract record.
|
|
12
|
+
class Ed25519Signer
|
|
13
|
+
# Generate a new Ed25519 keypair.
|
|
14
|
+
# Returns { signing_key: <hex>, verify_key: <hex> }
|
|
15
|
+
def self.generate_keypair
|
|
16
|
+
signing_key = ::Ed25519::SigningKey.generate
|
|
17
|
+
{
|
|
18
|
+
signing_key: signing_key.to_bytes.unpack1("H*"),
|
|
19
|
+
verify_key: signing_key.verify_key.to_bytes.unpack1("H*")
|
|
20
|
+
}
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Sign a message (typically the contract digest) with a hex signing key.
|
|
24
|
+
def self.sign(message, signing_key_hex)
|
|
25
|
+
key = ::Ed25519::SigningKey.new([signing_key_hex].pack("H*"))
|
|
26
|
+
signature = key.sign(message.to_s)
|
|
27
|
+
Base64.strict_encode64(signature)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Verify a base64 signature against a message using a hex verify key.
|
|
31
|
+
def self.verify(message, signature_b64, verify_key_hex)
|
|
32
|
+
key = ::Ed25519::VerifyKey.new([verify_key_hex].pack("H*"))
|
|
33
|
+
sig = Base64.strict_decode64(signature_b64)
|
|
34
|
+
key.verify(sig, message.to_s)
|
|
35
|
+
true
|
|
36
|
+
rescue ::Ed25519::VerifyError
|
|
37
|
+
false
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "openssl"
|
|
4
|
+
require "base64"
|
|
5
|
+
|
|
6
|
+
module MandateClaw
|
|
7
|
+
module Registry
|
|
8
|
+
module Signing
|
|
9
|
+
# HMAC-SHA256 signing — a simpler alternative to Ed25519
|
|
10
|
+
# suitable for trusted service-to-service contexts (v0 deployments).
|
|
11
|
+
class HmacSigner
|
|
12
|
+
def self.sign(message, secret_key)
|
|
13
|
+
digest = OpenSSL::HMAC.digest("SHA256", secret_key, message.to_s)
|
|
14
|
+
Base64.strict_encode64(digest)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def self.verify(message, signature_b64, secret_key)
|
|
18
|
+
expected = sign(message, secret_key)
|
|
19
|
+
ActiveSupport::SecurityUtils.secure_compare(expected, signature_b64)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module MandateClaw
|
|
7
|
+
module Registry
|
|
8
|
+
# A TaskContract is an instantiated, signable contract for a specific scope.
|
|
9
|
+
#
|
|
10
|
+
# Usage:
|
|
11
|
+
#
|
|
12
|
+
# tc = MandateClaw::Registry::TaskContract.new(
|
|
13
|
+
# template: InvoiceContract,
|
|
14
|
+
# scope: invoice,
|
|
15
|
+
# parties: { buyer: current_user, seller: merchant, ai_agent: agent },
|
|
16
|
+
# validity: 24.hours
|
|
17
|
+
# )
|
|
18
|
+
#
|
|
19
|
+
# tc.sign_as(:buyer, key: user.signing_key_hex)
|
|
20
|
+
# tc.sign_as(:ai_agent, key: agent.capability_key_hex,
|
|
21
|
+
# attests: agent.capability_manifest)
|
|
22
|
+
# tc.anchor! # persists to MandateClaw::Registry::SignedContract
|
|
23
|
+
#
|
|
24
|
+
class TaskContract
|
|
25
|
+
attr_reader :template, :scope, :parties, :validity,
|
|
26
|
+
:signatures, :created_at, :contract_id
|
|
27
|
+
|
|
28
|
+
def initialize(template:, scope:, parties:, validity: 24.hours)
|
|
29
|
+
@template = template
|
|
30
|
+
@scope = scope
|
|
31
|
+
@parties = parties
|
|
32
|
+
@validity = validity
|
|
33
|
+
@signatures = {}
|
|
34
|
+
@created_at = Time.current
|
|
35
|
+
@contract_id = nil
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# The canonical digest is the authoritative fingerprint of this contract
|
|
39
|
+
# instance. All parties sign this exact string.
|
|
40
|
+
def digest
|
|
41
|
+
@digest ||= Digest::SHA256.hexdigest(canonical_payload.to_json)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def sign_as(party_name, key:, attests: nil, algorithm: :ed25519)
|
|
45
|
+
raise ArgumentError, "Unknown party: #{party_name}" unless parties.key?(party_name)
|
|
46
|
+
|
|
47
|
+
sig = case algorithm
|
|
48
|
+
when :ed25519
|
|
49
|
+
Signing::Ed25519Signer.sign(digest, key)
|
|
50
|
+
when :hmac_sha256
|
|
51
|
+
Signing::HmacSigner.sign(digest, key)
|
|
52
|
+
else
|
|
53
|
+
raise ArgumentError, "Unsupported algorithm: #{algorithm}"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
@signatures[party_name] = {
|
|
57
|
+
signature: sig,
|
|
58
|
+
algorithm: algorithm,
|
|
59
|
+
attests: attests,
|
|
60
|
+
signed_at: Time.current
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def fully_signed?
|
|
65
|
+
required = template._attestation&.required_signatories || []
|
|
66
|
+
required.all? { |p| signatures.key?(p) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Persists this contract to the registry database.
|
|
70
|
+
# Raises unless fully signed.
|
|
71
|
+
def anchor!
|
|
72
|
+
raise Registry::SignatureError, "Contract is not fully signed" unless fully_signed?
|
|
73
|
+
|
|
74
|
+
record = SignedContract.create!(
|
|
75
|
+
contract_digest: digest,
|
|
76
|
+
template_name: template._contract_name.to_s,
|
|
77
|
+
scope_type: scope.class.name,
|
|
78
|
+
scope_id: scope.id.to_s,
|
|
79
|
+
parties_json: parties.transform_values(&:to_s).to_json,
|
|
80
|
+
signatures_json: signatures.to_json,
|
|
81
|
+
rendered_markdown: template.to_markdown,
|
|
82
|
+
expires_at: created_at + validity,
|
|
83
|
+
status: :active
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
@contract_id = record.id
|
|
87
|
+
record
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def expired?
|
|
91
|
+
created_at + validity < Time.current
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
private
|
|
95
|
+
|
|
96
|
+
def canonical_payload
|
|
97
|
+
{
|
|
98
|
+
template: template._contract_name,
|
|
99
|
+
scope_type: scope.class.name,
|
|
100
|
+
scope_id: scope.id.to_s,
|
|
101
|
+
parties: parties.transform_values(&:to_s),
|
|
102
|
+
created_at: created_at.iso8601,
|
|
103
|
+
expires_at: (created_at + validity).iso8601,
|
|
104
|
+
obligations: template._obligations.map { |o| { name: o.name, on: o.on } },
|
|
105
|
+
prohibitions: template._prohibitions.map { |p| { name: p.name, on: p.on } }
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "mandate_claw"
|
|
4
|
+
require "mandate_claw/registry/version"
|
|
5
|
+
require "mandate_claw/registry/engine"
|
|
6
|
+
require "mandate_claw/registry/signing/ed25519_signer"
|
|
7
|
+
require "mandate_claw/registry/signing/hmac_signer"
|
|
8
|
+
require "mandate_claw/registry/task_contract"
|
|
9
|
+
|
|
10
|
+
module MandateClaw
|
|
11
|
+
module Registry
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
class SignatureError < Error; end
|
|
14
|
+
class ContractNotFoundError < Error; end
|
|
15
|
+
class ContractExpiredError < Error; end
|
|
16
|
+
end
|
|
17
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mandateclaw-registry
|
|
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: rails
|
|
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: mandateclaw-dsl
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 0.0.1
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 0.0.1
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: ed25519
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '1.3'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '1.3'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec-rails
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '6.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '6.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: factory_bot_rails
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: sqlite3
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - "~>"
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '1.7'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - "~>"
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '1.7'
|
|
97
|
+
description: A Rails engine that stores signed MandateClaw contracts, links them to
|
|
98
|
+
transition logs, records breach events, and exposes a JSON API for contract verification.
|
|
99
|
+
Self-hostable open-source foundation for MandateClaw Cloud.
|
|
100
|
+
email:
|
|
101
|
+
- abhishek@parolkar.com
|
|
102
|
+
executables: []
|
|
103
|
+
extensions: []
|
|
104
|
+
extra_rdoc_files: []
|
|
105
|
+
files:
|
|
106
|
+
- CHANGELOG.md
|
|
107
|
+
- LICENSE
|
|
108
|
+
- README.md
|
|
109
|
+
- app/controllers/mandate_claw/api/v1/breach_events_controller.rb
|
|
110
|
+
- app/controllers/mandate_claw/api/v1/contracts_controller.rb
|
|
111
|
+
- app/models/mandate_claw/breach_event.rb
|
|
112
|
+
- app/models/mandate_claw/signed_contract.rb
|
|
113
|
+
- app/models/mandate_claw/transition_link.rb
|
|
114
|
+
- db/migrate/001_create_mandate_claw_signed_contracts.rb
|
|
115
|
+
- db/migrate/002_create_mandate_claw_breach_events.rb
|
|
116
|
+
- db/migrate/003_create_mandate_claw_transition_links.rb
|
|
117
|
+
- lib/mandate_claw/registry.rb
|
|
118
|
+
- lib/mandate_claw/registry/engine.rb
|
|
119
|
+
- lib/mandate_claw/registry/signing/ed25519_signer.rb
|
|
120
|
+
- lib/mandate_claw/registry/signing/hmac_signer.rb
|
|
121
|
+
- lib/mandate_claw/registry/task_contract.rb
|
|
122
|
+
- lib/mandate_claw/registry/version.rb
|
|
123
|
+
homepage: https://mandateclaw.com
|
|
124
|
+
licenses:
|
|
125
|
+
- FSL-1.1-Apache-2.0
|
|
126
|
+
metadata:
|
|
127
|
+
homepage_uri: https://mandateclaw.com
|
|
128
|
+
source_code_uri: https://github.com/parolkar/MandateClaw-Registry
|
|
129
|
+
post_install_message:
|
|
130
|
+
rdoc_options: []
|
|
131
|
+
require_paths:
|
|
132
|
+
- lib
|
|
133
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
134
|
+
requirements:
|
|
135
|
+
- - ">="
|
|
136
|
+
- !ruby/object:Gem::Version
|
|
137
|
+
version: 3.1.0
|
|
138
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
139
|
+
requirements:
|
|
140
|
+
- - ">="
|
|
141
|
+
- !ruby/object:Gem::Version
|
|
142
|
+
version: '0'
|
|
143
|
+
requirements: []
|
|
144
|
+
rubygems_version: 3.5.22
|
|
145
|
+
signing_key:
|
|
146
|
+
specification_version: 4
|
|
147
|
+
summary: MandateClaw contract registry — signing, storage, and audit trail
|
|
148
|
+
test_files: []
|