OpenErrand 0.1: an errand on a website with no API, with the human steps kept human (#227)

* OpenErrand 0.1: an errand on a website with no API, with the human steps kept human

docs/openerrand.md mints OpenErrand: one JSON file per errand (register an
account, download a transcript) naming the site, the inputs with a
sensitivity class and ordered sources (document, vault, prompt, generate,
derive, candidate, literal), field rules matched by id then label, page and
wait steps, five human gates a runner never performs (declare,
identity-proofing, code, mail, captcha), outcomes, the never-retried shared
secret, vault and download outputs, hand-off cards that may name only public
inputs, the publisher index at /.well-known/openerrand.json, and thirteen
runner rules. The worked example is the MyFTB business registration that
cli-tools `ftb` performs (profullstack/cli-tools#125), with no personal data.

- @logicsrc/schemas: openerrand + openerrand-index schemas and fixtures
- @logicsrc/validators: semantic checks (references, templates, no personal
  or secret input on a card) and tests that validate the spec's own examples
- logicsrc-web: registry entry (process family), /openerrand landing page,
  the example and the index served as static files, contract tests

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* OpenErrand: hand-off cards stay on the surface that owns the data

Anthony's ruling: tax and finance data never touches a social or promotion
tool, and nothing is sent to a CPA or preparer.

- Hand-off cards are delivered only on the surface that owns the errand's
  data (for a tax or finance errand, the principal's finance app through its
  CLI, PWA, MCP server or API, such as CoinPay, or the runner's terminal),
  never a social, promotion or third-party posting service, and never to
  anyone but the principal. A card for an errand with personal or secret
  inputs does not leave that surface. Runner rule 9 says the same.
- The run record and the sample run name the card by an opaque id
  (pin-letter/7f3k2q) instead of a mynaposter.com URL; the myna mention is gone.
- `principal: represented` no longer cites a preparer with a power of attorney.
- The FTB card's last step no longer suggests sending the PIN to someone else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* OpenErrand: user-agent rule, captcha solver policy, reference runner note

Anthony's answers on #227 ("go with your recommendations"):

- Rule 11: a runner may run headless with a normal desktop browser user agent
  (dropping HeadlessChrome) and nothing more: no fingerprint spoofing beyond
  the UA string, no stealth plugins, no solving or evading a bot challenge.
  A challenge the browser completes itself is a wait step; any other is a
  captcha gate.
- Captcha solvers: new site.sector and captcha step `solver`
  (forbidden by default | allowed). Never allowed on government, tax,
  financial, healthcare or identity-provider sites, nor on any errand with a
  declare or identity-proofing step or a secret input; elsewhere only when the
  file says so, with every use logged. The validator rejects `allowed` in the
  forbidden set or without a stated sector; six new tests. The FTB example
  states sector "tax".
- Reference runner: @logicsrc/openerrand / `logicsrc errand run`, marked in
  progress; ftb stays the runner the example was taken from.
- Name stays OpenErrand; family stays Agents and process.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Anthony Ettinger 2026-10-04 08:57:10 -07:00 • committed by GitHub
parent d38db62e8b
commit 1d69dc3804
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 2231 additions and 3 deletions

View file

@ -18,6 +18,11 @@ Schema families include:
- **OpenRental draft** — the `openrental` export describes listings of OpenAgent
profiles and OpenSwarm file-key references, with metadata and CoinPay rental
offers. `@logicsrc/validators` also checks membership and rental references.
- **OpenErrand** - the `openerrand` and `openerrand-index` exports describe an
errand a runner performs on a website with no API, and a publisher's list of
them. Fixtures are in `fixtures/openerrand`. `@logicsrc/validators` also checks
references, templates, and that no hand-off card names a personal or secret
input. See `docs/openerrand.md`.
## Install

View file

@ -0,0 +1,236 @@
{
"type": "logicsrc.openerrand",
"version": "0.1",
"id": "https://logicsrc.com/examples/openerrand/ftb-register-business.json",
"name": "ftb-register-business",
"title": "Register a MyFTB business account",
"description": "Creates a MyFTB account for a California S corporation at the Franchise Tax Board, proving the business with a figure from a filed Form 100S. FTB then mails a PIN; activation is a second errand.",
"publisher": "https://logicsrc.com/.well-known/openprofile.md",
"updated": "2026-10-04T00:00:00Z",
"reference": "https://github.com/profullstack/cli-tools/pull/125",
"principal": "self",
"site": {
"name": "California Franchise Tax Board (MyFTB)",
"sector": "tax",
"origins": ["https://webapp.ftb.ca.gov"],
"start": ["https://webapp.ftb.ca.gov/MyFTBAccess/Registration/NewAccount"]
},
"limits": { "pages": 15, "page_timeout": "PT30S", "same_page": 2 },
"inputs": {
"email": {
"label": "Email address FTB writes to",
"type": "email",
"sensitivity": "personal",
"required": true,
"sources": [{ "from": "prompt" }]
},
"phone": {
"label": "Mobile number FTB texts a verification code to",
"type": "string",
"pattern": "^[0-9]{10}$",
"sensitivity": "personal",
"required": true,
"sources": [{ "from": "prompt" }]
},
"first_name": {
"label": "Representative's first name",
"type": "string",
"max_length": 11,
"sensitivity": "personal",
"sources": [{ "from": "document", "form": "CA 540", "field": "first name", "pick": "newest" }]
},
"last_name": {
"label": "Representative's last name",
"type": "string",
"max_length": 13,
"sensitivity": "personal",
"sources": [{ "from": "document", "form": "CA 540", "field": "last name", "pick": "newest" }]
},
"street": {
"label": "Street address on the newest return",
"type": "string",
"sensitivity": "personal",
"sources": [{ "from": "document", "form": "CA 540", "field": "street address", "pick": "newest" }]
},
"address_numbers": {
"label": "The numbers in the address on file",
"type": "string",
"sensitivity": "personal",
"sources": [{ "from": "derive", "input": "street", "transform": "digits" }]
},
"zip": {
"label": "ZIP code on file",
"type": "string",
"sensitivity": "personal",
"sources": [{ "from": "document", "form": "CA 540", "field": "ZIP code", "pick": "newest", "transform": "first:5" }]
},
"corp_id": {
"label": "California corporation number",
"type": "string",
"sensitivity": "public",
"sources": [{ "from": "document", "form": "CA 100S", "field": "California corporation number", "pick": "newest" }]
},
"tax_year": {
"label": "Tax year of the return the shared secret comes from",
"type": "integer",
"sensitivity": "public",
"sources": [{ "from": "candidate", "input": "net_income", "part": "year" }]
},
"net_income": {
"label": "Net income for tax purposes, whole dollars",
"type": "integer",
"sensitivity": "secret",
"role": "shared-secret",
"sources": [
{ "from": "document", "form": "CA 100S", "field": "line 20", "match": "net income for tax purposes", "years": { "back": 5, "current": false }, "transform": "whole" },
{ "from": "document", "form": "CA 100S", "field": "line 15", "match": "net income \\(loss\\) for state purposes", "years": { "back": 5, "current": false }, "transform": "whole" }
]
},
"username": {
"label": "MyFTB user name",
"type": "string",
"sensitivity": "personal",
"role": "credential",
"sources": [
{ "from": "vault", "key": "FTB_BUSINESS_USERNAME" },
{ "from": "generate", "length": 15, "classes": ["lower", "digit"] }
]
},
"password": {
"label": "MyFTB password",
"type": "string",
"sensitivity": "secret",
"role": "credential",
"sources": [
{ "from": "vault", "key": "FTB_BUSINESS_PASSWORD" },
{ "from": "generate", "length": 24, "classes": ["lower", "upper", "digit", "special"], "special": "!#$*@" }
]
},
"security": {
"label": "Three security questions and their answers",
"type": "qa-set",
"count": 3,
"sensitivity": "secret",
"role": "credential",
"sources": [
{ "from": "vault", "key": "FTB_BUSINESS_SECURITY_ANSWERS" },
{ "from": "generate", "length": 10, "classes": ["lower", "digit"] }
]
}
},
"rules": [
{ "name": "read terms", "id": "^ReadTerms$", "types": ["checkbox"], "do": { "check": true } },
{ "name": "accept terms", "id": "^AcceptTerms$", "types": ["checkbox"], "do": { "check": true } },
{ "name": "first name", "id": "^FstName$", "do": { "text": "{{first_name}}" } },
{ "name": "middle initial", "id": "^MInitial$", "do": { "skip": true } },
{ "name": "last name", "id": "^LstName$", "do": { "text": "{{last_name}}" } },
{ "name": "suffix", "id": "^Sffx$", "do": { "skip": true } },
{ "name": "user name again", "id": "^ReUserName$", "do": { "text": "{{username}}" } },
{ "name": "user name", "id": "^UserName$", "do": { "text": "{{username}}" } },
{ "name": "email again", "id": "^ReEmail$", "do": { "text": "{{email}}" } },
{ "name": "email", "id": "^Email$", "do": { "text": "{{email}}" } },
{ "name": "password again", "id": "^RePassword$", "do": { "text": "{{password}}" } },
{ "name": "password", "id": "^Password$", "do": { "text": "{{password}}" } },
{ "name": "foreign number", "id": "^Phone_Foreign$", "do": { "skip": true } },
{ "name": "foreign address", "id": "^Address_Foreign$|^Address_No(MailAddress|PostalCode)$", "do": { "skip": true } },
{ "name": "security question", "label": "question", "types": ["select-one"], "do": { "choose": "security" } },
{ "name": "security answer", "label": "question|answer", "types": ["text", "password"], "do": { "answer": "security" } },
{ "name": "role", "label": "individual|business representative", "types": ["radio"], "do": { "check": { "label": "^\\s*business representative" } } },
{ "name": "zip", "label": "zip|postal", "types": ["text", "tel", "number"], "do": { "text": "{{zip}}" } },
{ "name": "address numbers", "label": "numbers in (the |your )?(business )?(mailing )?address", "types": ["text", "tel", "number"], "do": { "text": "{{address_numbers}}" } },
{ "name": "tax year", "label": "year (of|on) the tax return|tax year", "types": ["select-one"], "do": { "select": ["^\\s*{{tax_year}}\\s*$"] } },
{ "name": "tax year", "label": "year (of|on) the tax return|tax year", "types": ["text", "tel", "number"], "do": { "text": "{{tax_year}}" } },
{ "name": "net income", "label": "net income|income \\(loss\\)", "types": ["text", "tel", "number"], "do": { "text": "{{net_income}}" } },
{ "name": "company type", "label": "type of company|company type|entity type", "types": ["select-one"], "do": { "select": ["^\\s*corporation\\s*$", "corporation"] } },
{ "name": "account number", "label": "account number|entity id|corporation (id|number)", "types": ["text", "tel", "number"], "do": { "text": "{{corp_id}}" } },
{ "name": "form type", "label": "form type|type of (tax )?(return|form)", "types": ["select-one"], "do": { "select": ["100\\s*S\\b"] } },
{ "name": "declaration", "label": "perjury|i declare|under penalty", "types": ["checkbox"], "do": { "gate": "declaration" } },
{ "name": "phone", "label": "phone number", "types": ["text", "tel", "number"], "do": { "text": "{{phone}}" } },
{ "name": "send a text", "label": "send me a text|text message", "types": ["radio"], "do": { "check": true } },
{ "name": "verification code", "label": "verification code|security code|one[- ]time|passcode|access code|enter (the )?code", "types": ["text", "tel", "number", "password"], "do": { "gate": "text-code" } }
],
"steps": [
{
"id": "bot-check",
"kind": "wait",
"match": { "title": "^Challenge Validation$", "selector": "#sec-cpt-if" },
"timeout": "PT90S",
"poll": "PT3S",
"say": "FTB's bot check is a proof of work the page's own script solves; the runner waits for it."
},
{
"id": "form",
"kind": "page",
"unmatched": "stop",
"say": "Every MyFTB registration page: terms, profile, security questions, role, address, shared secret, phone."
},
{
"id": "declaration",
"kind": "declare",
"statement": "perjury|i declare|under penalty",
"why": "Ticking this box is the representative stating, under penalty of perjury, that what was entered is true. Only that person can make the statement."
},
{
"id": "text-code",
"kind": "code",
"channel": "sms",
"relay": ["terminal", "file"],
"pattern": "^\\w{4,10}$",
"timeout": "PT15M",
"why": "FTB texts a code to the phone number given. Whoever holds the phone reads it out."
},
{
"id": "pin-letter",
"kind": "mail",
"what": "MyFTB PIN letter",
"arrives": "5 to 10 business days, to the address FTB has on file",
"expires": "P21D",
"resume": "ftb-activate-business",
"input": "pin",
"handoff": "pin-letter",
"why": "FTB activates a new account with a PIN it sends by US Mail. Nobody but the addressee can read it."
}
],
"submit": {
"labels": "^(submit|continue|next|log ?in|login|activate|send( code| me a code)?|verify|confirm)$",
"never": "^(back|cancel|end session|previous)$",
"ignore": "#timer, .modal"
},
"outcomes": [
{
"name": "rejected",
"kind": "rejected",
"text": "does not match our records|there is a problem|unable to (verify|process) your|account (is|has been) locked"
},
{
"name": "registered",
"kind": "success",
"text": "registration confirmation|successfully (registered|created)|we will (mail|send) you a (letter|pin)|pin .*(mail|letter)",
"then": "pin-letter"
}
],
"retry": { "shared_secret": "never", "page_errors": "rejected" },
"outputs": {
"vault": {
"when": "registered",
"keys": {
"FTB_BUSINESS_USERNAME": "{{username}}",
"FTB_BUSINESS_PASSWORD": "{{password}}",
"FTB_BUSINESS_EMAIL": "{{email}}",
"FTB_BUSINESS_SECURITY_ANSWERS": "{{security}}"
}
}
},
"handoffs": {
"pin-letter": {
"title": "FTB PIN letter: business MyFTB account",
"open": "https://webapp.ftb.ca.gov/MyFTBAccess/",
"steps": [
"Watch the mail at the address FTB has on file for the MyFTB PIN letter (5 to 10 business days).",
"Activate before {{expires_on}}: the PIN expires 21 days after registration.",
"Run the command below yourself, with the PIN from the letter."
],
"command": "ftb activate business --pin <PIN from the letter>"
}
}
}

View file

@ -0,0 +1,16 @@
{
"type": "logicsrc.openerrand-index",
"version": "0.1",
"publisher": "https://logicsrc.com/.well-known/openprofile.md",
"updated": "2026-10-04T00:00:00Z",
"errands": [
{
"url": "https://logicsrc.com/examples/openerrand/ftb-register-business.json",
"name": "ftb-register-business",
"site": "https://webapp.ftb.ca.gov",
"title": "Register a MyFTB business account",
"gates": ["declare", "code", "mail"],
"updated": "2026-10-04T00:00:00Z"
}
]
}

View file

@ -23,6 +23,8 @@
"opencontext",
"opencreds",
"openrental",
"openerrand",
"browser-automation",
"openontology",
"password-manager",
"standards",
@ -67,6 +69,8 @@
"./opencreds-manifest": "./schemas/logicsrc-opencreds-manifest.schema.json",
"./opencreds-vault-meta": "./schemas/logicsrc-opencreds-vault-meta.schema.json",
"./openrental": "./schemas/logicsrc-openrental.schema.json",
"./openerrand": "./schemas/logicsrc-openerrand.schema.json",
"./openerrand-index": "./schemas/logicsrc-openerrand-index.schema.json",
"./openontology-action": "./schemas/logicsrc-openontology-action.schema.json",
"./openontology-approval": "./schemas/logicsrc-openontology-approval.schema.json",
"./openontology-changeset": "./schemas/logicsrc-openontology-changeset.schema.json",

View file

@ -0,0 +1,35 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-openerrand-index.schema.json",
"title": "OpenErrand index",
"description": "The list of errand files a publisher serves at /.well-known/openerrand.json.",
"type": "object",
"additionalProperties": false,
"required": ["type", "version", "errands"],
"properties": {
"type": { "const": "logicsrc.openerrand-index" },
"version": { "const": "0.1" },
"publisher": { "type": "string", "format": "uri", "pattern": "^https://" },
"updated": { "type": "string", "format": "date-time" },
"errands": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["url", "site", "title"],
"properties": {
"url": { "type": "string", "format": "uri", "pattern": "^https://" },
"name": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,63}$" },
"site": { "type": "string", "pattern": "^https://[a-z0-9.-]+(:[0-9]+)?$" },
"title": { "type": "string", "minLength": 1 },
"gates": {
"type": "array",
"uniqueItems": true,
"items": { "enum": ["declare", "identity-proofing", "code", "mail", "captcha"] }
},
"updated": { "type": "string", "format": "date-time" }
}
}
}
}
}

View file

@ -0,0 +1,434 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://schemas.logicsrc.com/logicsrc-openerrand.schema.json",
"title": "OpenErrand",
"description": "A declarative description of one errand a person runs on a website that has no API: the site, the inputs and where each may come from, the field rules, the human gates a runner must never automate, the outcomes, the retry policy, the outputs and the hand-off cards.",
"type": "object",
"additionalProperties": false,
"required": ["type", "version", "name", "title", "site", "steps", "outcomes"],
"properties": {
"type": { "const": "logicsrc.openerrand" },
"version": { "const": "0.1" },
"id": { "$ref": "#/$defs/httpsUrl" },
"name": { "$ref": "#/$defs/slug" },
"title": { "type": "string", "minLength": 1, "maxLength": 160 },
"description": { "type": "string" },
"publisher": { "$ref": "#/$defs/httpsUrl" },
"updated": { "type": "string", "format": "date-time" },
"reference": { "$ref": "#/$defs/httpsUrl" },
"principal": { "enum": ["self", "represented"] },
"site": {
"type": "object",
"additionalProperties": false,
"required": ["name", "origins", "start"],
"properties": {
"name": { "type": "string", "minLength": 1 },
"sector": { "enum": ["government", "tax", "financial", "healthcare", "identity-provider", "commercial", "other"] },
"origins": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/origin" } },
"start": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/httpsUrl" } },
"terms": { "$ref": "#/$defs/httpsUrl" }
}
},
"limits": {
"type": "object",
"additionalProperties": false,
"properties": {
"pages": { "type": "integer", "minimum": 1, "maximum": 200 },
"page_timeout": { "$ref": "#/$defs/duration" },
"same_page": { "type": "integer", "minimum": 1, "maximum": 10 }
}
},
"inputs": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/inputName" },
"additionalProperties": { "$ref": "#/$defs/input" }
},
"rules": { "type": "array", "items": { "$ref": "#/$defs/rule" } },
"steps": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/step" } },
"submit": {
"type": "object",
"additionalProperties": false,
"required": ["labels"],
"properties": {
"labels": { "$ref": "#/$defs/pattern" },
"never": { "$ref": "#/$defs/pattern" },
"ignore": { "type": "string", "minLength": 1 }
}
},
"outcomes": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/outcome" } },
"retry": {
"type": "object",
"additionalProperties": false,
"properties": {
"shared_secret": { "const": "never" },
"page_errors": { "enum": ["rejected", "continue"] }
}
},
"outputs": {
"type": "object",
"additionalProperties": false,
"properties": {
"vault": {
"type": "object",
"additionalProperties": false,
"required": ["keys"],
"properties": {
"when": { "type": "string", "minLength": 1 },
"keys": {
"type": "object",
"minProperties": 1,
"propertyNames": { "pattern": "^[A-Z][A-Z0-9_]*$" },
"additionalProperties": { "type": "string", "minLength": 1 }
}
}
},
"downloads": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["match", "to"],
"properties": {
"match": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"url": { "$ref": "#/$defs/pattern" },
"filename": { "$ref": "#/$defs/pattern" },
"type": { "type": "string", "minLength": 1 }
}
},
"to": { "type": "string", "minLength": 1 },
"when": { "type": "string", "minLength": 1 }
}
}
}
}
},
"handoffs": {
"type": "object",
"propertyNames": { "$ref": "#/$defs/slug" },
"additionalProperties": { "$ref": "#/$defs/handoff" }
},
"metadata": { "type": "object" }
},
"$defs": {
"httpsUrl": { "type": "string", "format": "uri", "pattern": "^https://" },
"origin": { "type": "string", "pattern": "^https://[a-z0-9.-]+(:[0-9]+)?$" },
"slug": { "type": "string", "pattern": "^[a-z0-9][a-z0-9-]{0,63}$" },
"inputName": { "type": "string", "pattern": "^[a-z][a-z0-9_]{0,63}$" },
"pattern": { "type": "string", "minLength": 1, "format": "regex" },
"duration": { "type": "string", "format": "duration" },
"transform": { "type": "string", "pattern": "^(digits|whole|upper|lower|trim|first:[0-9]+|last:[0-9]+)$" },
"fieldType": { "enum": ["text", "password", "email", "tel", "number", "date", "textarea", "select-one", "radio", "checkbox"] },
"input": {
"type": "object",
"additionalProperties": false,
"required": ["type", "sensitivity", "sources"],
"properties": {
"label": { "type": "string", "minLength": 1 },
"type": { "enum": ["string", "integer", "number", "email", "date", "boolean", "qa-set"] },
"sensitivity": { "enum": ["public", "personal", "secret"] },
"role": { "enum": ["shared-secret", "credential", "identifier"] },
"required": { "type": "boolean" },
"pattern": { "$ref": "#/$defs/pattern" },
"max_length": { "type": "integer", "minimum": 1 },
"count": { "type": "integer", "minimum": 1, "maximum": 10 },
"sources": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/source" } }
}
},
"source": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["from"],
"properties": { "from": { "const": "prompt" }, "ask": { "type": "string", "minLength": 1 } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["from", "key"],
"properties": { "from": { "const": "vault" }, "key": { "type": "string", "pattern": "^[A-Z][A-Z0-9_]*$" } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["from", "form", "field"],
"properties": {
"from": { "const": "document" },
"form": { "type": "string", "minLength": 1 },
"field": { "type": "string", "minLength": 1 },
"match": { "$ref": "#/$defs/pattern" },
"pick": { "enum": ["newest", "oldest", "each"] },
"years": {
"type": "object",
"additionalProperties": false,
"properties": {
"back": { "type": "integer", "minimum": 1, "maximum": 50 },
"current": { "type": "boolean" }
}
},
"transform": { "$ref": "#/$defs/transform" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["from", "input", "transform"],
"properties": {
"from": { "const": "derive" },
"input": { "$ref": "#/$defs/inputName" },
"transform": { "$ref": "#/$defs/transform" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["from", "input", "part"],
"properties": {
"from": { "const": "candidate" },
"input": { "$ref": "#/$defs/inputName" },
"part": { "enum": ["year", "form", "field", "source"] }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["from", "length", "classes"],
"properties": {
"from": { "const": "generate" },
"length": { "type": "integer", "minimum": 1, "maximum": 256 },
"classes": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": { "enum": ["lower", "upper", "digit", "special"] }
},
"special": { "type": "string", "minLength": 1 }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["from", "value"],
"properties": { "from": { "const": "literal" }, "value": { "anyOf": [{ "type": "string" }, { "type": "number" }, { "type": "boolean" }] } }
}
]
},
"rule": {
"type": "object",
"additionalProperties": false,
"required": ["name", "do"],
"anyOf": [{ "properties": { "id": true }, "required": ["id"] }, { "properties": { "label": true }, "required": ["label"] }],
"properties": {
"name": { "type": "string", "minLength": 1 },
"id": { "$ref": "#/$defs/pattern" },
"label": { "$ref": "#/$defs/pattern" },
"types": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/fieldType" } },
"do": { "$ref": "#/$defs/action" }
}
},
"action": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["text"],
"properties": {
"text": { "type": "string" },
"split": { "type": "array", "minItems": 2, "items": { "type": "integer", "minimum": 1 } }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["select"],
"properties": { "select": { "type": "array", "minItems": 1, "items": { "$ref": "#/$defs/pattern" } } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["check"],
"properties": {
"check": {
"oneOf": [
{ "const": true },
{
"type": "object",
"additionalProperties": false,
"required": ["label"],
"properties": { "label": { "$ref": "#/$defs/pattern" } }
}
]
}
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["skip"],
"properties": { "skip": { "const": true } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["gate"],
"properties": { "gate": { "$ref": "#/$defs/slug" } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["choose"],
"properties": { "choose": { "$ref": "#/$defs/inputName" } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["answer"],
"properties": { "answer": { "$ref": "#/$defs/inputName" } }
}
]
},
"match": {
"type": "object",
"additionalProperties": false,
"minProperties": 1,
"properties": {
"url": { "$ref": "#/$defs/pattern" },
"title": { "$ref": "#/$defs/pattern" },
"text": { "$ref": "#/$defs/pattern" },
"selector": { "type": "string", "minLength": 1 }
}
},
"step": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "page" },
"match": { "$ref": "#/$defs/match" },
"rules": { "type": "array", "items": { "$ref": "#/$defs/rule" } },
"unmatched": { "enum": ["stop", "ask"] },
"say": { "type": "string" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "match", "timeout"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "wait" },
"match": { "$ref": "#/$defs/match" },
"timeout": { "$ref": "#/$defs/duration" },
"poll": { "$ref": "#/$defs/duration" },
"say": { "type": "string" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "statement", "why"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "declare" },
"statement": { "$ref": "#/$defs/pattern" },
"why": { "type": "string", "minLength": 1 },
"say": { "type": "string" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "provider", "origins", "why"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "identity-proofing" },
"provider": { "type": "string", "minLength": 1 },
"origins": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "$ref": "#/$defs/origin" } },
"match": { "$ref": "#/$defs/match" },
"timeout": { "$ref": "#/$defs/duration" },
"handoff": { "$ref": "#/$defs/slug" },
"why": { "type": "string", "minLength": 1 },
"say": { "type": "string" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "channel", "relay", "timeout"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "code" },
"channel": { "enum": ["sms", "email", "voice", "app"] },
"relay": { "type": "array", "minItems": 1, "uniqueItems": true, "items": { "enum": ["terminal", "file", "page"] } },
"pattern": { "$ref": "#/$defs/pattern" },
"timeout": { "$ref": "#/$defs/duration" },
"why": { "type": "string" },
"say": { "type": "string" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "what"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "mail" },
"what": { "type": "string", "minLength": 1 },
"arrives": { "type": "string" },
"expires": { "$ref": "#/$defs/duration" },
"resume": { "$ref": "#/$defs/slug" },
"input": { "$ref": "#/$defs/inputName" },
"handoff": { "$ref": "#/$defs/slug" },
"why": { "type": "string" },
"say": { "type": "string" }
}
},
{
"type": "object",
"additionalProperties": false,
"required": ["id", "kind", "match"],
"properties": {
"id": { "$ref": "#/$defs/slug" },
"kind": { "const": "captcha" },
"match": { "$ref": "#/$defs/match" },
"solver": { "enum": ["forbidden", "allowed"] },
"timeout": { "$ref": "#/$defs/duration" },
"why": { "type": "string" },
"say": { "type": "string" }
}
}
]
},
"outcome": {
"type": "object",
"additionalProperties": false,
"required": ["name", "kind"],
"anyOf": [{ "properties": { "text": true }, "required": ["text"] }, { "properties": { "url": true }, "required": ["url"] }],
"properties": {
"name": { "$ref": "#/$defs/slug" },
"kind": { "enum": ["success", "rejected", "waiting"] },
"text": { "$ref": "#/$defs/pattern" },
"url": { "$ref": "#/$defs/pattern" },
"then": { "$ref": "#/$defs/slug" }
}
},
"handoff": {
"type": "object",
"additionalProperties": false,
"required": ["title", "steps"],
"properties": {
"title": { "type": "string", "minLength": 1 },
"open": { "$ref": "#/$defs/httpsUrl" },
"steps": { "type": "array", "minItems": 1, "items": { "type": "string", "minLength": 1 } },
"command": { "type": "string" }
}
}
}
}