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

@ -0,0 +1,198 @@
import Link from "next/link";
import type { ReactNode } from "react";
import type { Metadata } from "next";
import { specMetadata } from "@/lib/page-meta";
import { SiteShell } from "@/components/site-shell";
import { mono, pre, table, td, th } from "../openontology/ui";
import { SENSITIVITY, SOURCES, STEPS } from "./data";
export const metadata: Metadata = specMetadata(
"/openerrand",
"OpenErrand is one JSON file that describes an errand on a website with no API: the inputs and where each comes from, the rules that fill each field, the human gates a runner must never automate (declarations, identity proofing, codes, letters, captchas), the outcomes, and a hand-off card that never carries a secret."
);
const EXCERPT = `"rules": [
{ "name": "first name", "id": "^FstName$", "do": { "text": "{{first_name}}" } },
{ "name": "net income", "label": "net income|income \\\\(loss\\\\)",
"types": ["text", "tel", "number"], "do": { "text": "{{net_income}}" } },
{ "name": "declaration", "label": "perjury|i declare|under penalty",
"types": ["checkbox"], "do": { "gate": "declaration" } }
],
"steps": [
{ "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." }
]`;
const RULES: Array<[string, string]> = [
["Shows the errand first", "Title, publisher, every gate with its reason, every input with its sensitivity and source. A file whose hash changed is shown again before it runs."],
["Never guesses", "Fields are matched by id first and label second. A required field no rule fills stops the run."],
["Never performs a gate", "Declarations need the person's consent for this run. Identity proofing and letters are theirs, and so are captchas on any sensitive site. Codes come only through a declared relay."],
["One shared secret per run", "A figure from a return is submitted once. A rejection ends the run and lists the other candidates; a person picks the next one."],
["Keeps documents and values in place", "Extraction is local. Values go to the site's fields and the vault; logs hold fields, never values; cards hold steps, never secrets."]
];
export default function OpenErrandPage(): ReactNode {
return (
<SiteShell active="OpenErrand">
<div className="band">
<div className="section-head">
<p className="eyebrow">LogicSRC standards surface</p>
<h2>OpenErrand</h2>
<p>
One JSON file that describes an errand a person runs on a website that has no API:
registering for a tax account, downloading a transcript, renewing a licence. A person
reads it and knows every value it will send and every statement it will ask them to
make. An agent runs the same file in a headless browser and stops exactly where a
person is needed.
</p>
</div>
<p style={{ color: "#41505d" }}>
People already automate these sites, with a script nobody else can read or an agent
left to guess. The script hides what it sends. The agent ticks a penalty-of-perjury box
because the form would not submit without it, and tries a second figure when the first
is rejected. OpenErrand writes the errand down: the fields it fills and with what, the
values that are secret, the steps that belong to a person, and the point where it stops.
</p>
<p style={{ color: "#5b6b7a" }}>
Status: 0.1. A generic runner, <code style={mono}>logicsrc errand run</code> from{" "}
<code style={mono}>@logicsrc/openerrand</code>, is in progress. The worked example
transcribes the rule table of <code style={mono}>ftb</code> in cli-tools, which registers
and activates MyFTB accounts at the California Franchise Tax Board. An excerpt:
</p>
<pre style={pre}>{EXCERPT}</pre>
</div>
<div className="band">
<div className="section-head">
<h2>Steps and human gates</h2>
<p>
Five of the seven step kinds are gates: steps only a person can take. A runner hands
them over and never performs them, and each carries a <code style={mono}>why</code> a
person reads.
</p>
</div>
<table style={table}>
<tbody>
{STEPS.map(([kind, what]) => (
<tr key={kind}>
<td style={td}>
<code style={mono}>{kind}</code>
</td>
<td style={td}>{what}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="band">
<div className="section-head">
<h2>Inputs</h2>
<p>Every input has a sensitivity class, which decides where its value may appear.</p>
</div>
<table style={table}>
<thead>
<tr>
<th style={th}>class</th>
<th style={th}>where it may appear</th>
</tr>
</thead>
<tbody>
{SENSITIVITY.map(([name, where]) => (
<tr key={name}>
<td style={td}>
<code style={mono}>{name}</code>
</td>
<td style={td}>{where}</td>
</tr>
))}
</tbody>
</table>
<p style={{ color: "#41505d" }}>And an ordered list of sources, tried until one yields a value:</p>
<table style={table}>
<tbody>
{SOURCES.map(([name, what]) => (
<tr key={name}>
<td style={td}>
<code style={mono}>{name}</code>
</td>
<td style={td}>{what}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="band">
<div className="section-head">
<h2>What a runner promises</h2>
<p>Thirteen rules in the specification; these are the ones that keep a person safe.</p>
</div>
<table style={table}>
<tbody>
{RULES.map(([rule, meaning]) => (
<tr key={rule}>
<td style={td}>
<strong>{rule}</strong>
</td>
<td style={td}>{meaning}</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="band">
<div className="section-head">
<h2>Discovery</h2>
<p>
A publisher lists its errands at <code style={mono}>/.well-known/openerrand.json</code>,
each with the gates it will ask of a person. An errand is verified when it comes from
that publisher&apos;s origin, and site-endorsed only when the site itself serves the
index. This site publishes the worked example at{" "}
<a href="/.well-known/openerrand.json">
<code style={mono}>/.well-known/openerrand.json</code>
</a>
.
</p>
</div>
</div>
<div className="band">
<div className="section-head">
<h2>Not</h2>
</div>
<p style={{ color: "#41505d" }}>
Not a way around a check: a runner may present a normal browser user agent and nothing more, with no fingerprint spoofing, no stealth plugins and no challenge solving.
Not for someone else&apos;s account. Not an API: when a site has one, use{" "}
<Link href="/openconnection">OpenConnection</Link>,{" "}
<Link href="/openaccess">OpenAccess</Link> or <Link href="/opensaas">OpenSaaS</Link>. Not a
scraper, not a test framework, not a credential store, not legal or tax advice.
</p>
</div>
<div className="band">
<div className="section-head">
<h2>Where everything lives</h2>
</div>
<ul style={{ color: "#41505d", lineHeight: 1.9, paddingLeft: "1.1rem" }}>
<li>
<Link href="/docs/openerrand">Specification</Link>: the file, inputs, field rules,
steps, the five gates, outcomes and retry, outputs, hand-off cards, discovery, thirteen
runner rules, and the MyFTB business registration as a worked example
</li>
<li>
JSON Schemas <code style={mono}>@logicsrc/schemas/openerrand</code> and{" "}
<code style={mono}>openerrand-index</code>, checked by{" "}
<code style={mono}>@logicsrc/validators</code>
</li>
<li>
<Link href="/opencreds">OpenCreds</Link>, the vault an errand writes logins to;{" "}
<Link href="/openfleet">OpenFleet</Link>, the record an agent runs an errand under
</li>
</ul>
</div>
</SiteShell>
);
}