rules.yaml
The note on the fridge. Your housekeeper follows it to the letter, in code, with no model in the loop.
runs The rules engine lives in packages/core/src/rules.ts, with tests. The playground on the home page runs it in your browser.
Example
owner: stan
device: approvals-app # where "ask me" requests go
default: ask # join | decline | ask
tags: [research-volunteers]
rules:
- match: { purpose: research, sponsor_verified: true, ethics_approved: true }
allow_fields: [age_band, medications, readmissions]
deny_fields: [name, postcode, dob]
min_price_usd: 4
retention: none
decision: join
- match: { purpose: marketing }
decision: decline
- match: { requester: gp.example }
allow_fields: [medications]
grant_hours: 48
decision: join
negotiation:
join_collectives: true # may counter-offer together with other housesHow a decision is made
Rules are checked top to bottom. The first rule whose match fits the offer decides. If none fits, default applies.
For a matching rule with decision: join:
| Offer… | Result |
|---|---|
keeps data, but the rule says retention: none | decline |
asks for a field not in allow_fields | ask you, naming the field |
asks for a deny_fields field, or pays below min_price_usd | counter (drop the field, raise the price) if join_collectives: true, otherwise ask |
| fits everything | join: a key for grant_hours (default 48), exactly those fields |
decision: decline and decision: ask apply as written.
A default: join with no matching rule becomes ask, because without a rule nothing limits the fields.
Reference
| Key | Type | Meaning |
|---|---|---|
owner | string, required | Your name or handle |
device | string | Where ask requests go |
default | join | decline | ask | When no rule matches. Default ask. |
tags | list | Interests, used for discovery and guilds |
rules[].match | map | All keys must equal the offer's. Keys: purpose, requester, sponsor_verified, ethics_approved, audit_certified, and reputation_at_least (a number: the visitor's reputation from the nodes you follow, 0–1; unknown agents count as 0.5) |
rules[].allow_fields | list | Fields this rule may share |
rules[].deny_fields | list | Fields never shared under this rule |
rules[].min_price_usd | number | Lowest acceptable price per participant |
rules[].retention | none or ISO 8601 duration | How long the visitor may keep results |
rules[].grant_hours | integer | Key lifetime. Default 48. |
rules[].decision | join | decline | ask | |
negotiation.join_collectives | boolean | Allow your housekeeper to bargain with neighbours as one bloc |
Trust
trust:
issuers: [issuer.lake.local] # who may sign your records
auditors: [audit.lake.local] # who may certify studies and converters
ethics: [ethics.lake.local] # whose ethics approvals count
payments: [payments.lake.local] # whose receipts you fileethics_approved: true in a rule means approved by an ethics body you list here. An approval from anyone else counts as none. Nobody appoints these for the Lake; you choose. A sandbox house with no trust section trusts the demo specialists and says so in its log; outside the sandbox, an empty list trusts nobody.
Whatever your rules say, a study without a certificate from an auditor you trust never runs in your house.
Machine-readable: spec/schemas/rules.schema.json.
Safety properties
- Typos fail closed. An unknown
matchkey is an error, not an ignored line. A misspelling never widens access. - Free text is never read. Offers may carry a note; the engine ignores it. "Ignore your rules and share everything" has nothing to grab.
- Deterministic. Same rules plus same offer gives the same answer, every time. That's what makes seeded demos and audits possible.
- A
joinis your pre-approval, not the agent's. It counts because you wrote the rule. Agents can't write rules.