mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-10-05 06:05:28 +00:00
OpenErrand reference runner: @logicsrc/openerrand and logicsrc errand (#228)
* 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> * OpenErrand reference runner: @logicsrc/openerrand and logicsrc errand Ship the runner docs/openerrand.md promised. `logicsrc errand run <file>` reads an OpenErrand 0.1 file, validates it with @logicsrc/validators, and drives headless Chrome through it under the spec's thirteen rules; `errand validate` shows what a file will ask of you and `errand status` shows the last run of each errand, its card and any lockout. The engine is generalised from cli-tools `ftb` (PR #125) with no dependency on cli-tools: the CDP client and Chrome finder from wcag.ts, the page reader, native-setter fill and forward-button picker from ftb-run.ts, and the rule matcher, throttle and outcome logic from ftb.ts, all now driven by the file. - Inputs: document (an extractor hook; the one shipped runs a local command that reads JSON requests and prints records), vault (teams or OpenCreds), prompt (no echo for secrets), generate, derive, candidate, literal. `--input name=value` wins. Shared-secret candidates are ranked as the spec says, one is submitted, and a rejection lists the others for --candidate. - Rules: id before label, step rules first, choices before text, an id match final, an unmatched required field stops the run naming it. - Gates: declare only with --declare after the values are shown; identity proofing never touched (URL only) and handed over or stopped on; code from the terminal or a code file, used once, a wrong code waits for the next; mail ends the run waiting with the card; captcha is the person's, and a CaptchaSolver interface is called only where the spec permits (no solver is bundled); wait steps are polled, never solved. - Throttle: 2 runs per errand and account in 30 minutes, 4 a day, 2 minutes between runs on a site, lockouts from metadata.lockout or a default, held per site and account, never lifted by --force. - Outputs: credentials written before success to a teams vault by pull, merge, push (metadata.vault or --vault), else a 0600 file said aloud; downloads type-checked and never overwritten with different bytes; cards only in the local run record. - The user agent is Chrome's own with HeadlessChrome replaced, given at launch: a CDP override did not reach a navigation the page's own script started, which is exactly the proof-of-work interstitial case. Tests: 85 in the package (rule engine, inputs, gates, throttle, outcomes, captcha gating, vaults, outputs, commands) including an integration test that runs the published FTB example unchanged in real headless Chrome against a local HTTPS fake site (Chrome maps webapp.ftb.ca.gov to it and every other host to NOTFOUND; all data fictional), and 2 in the CLI. CLI 0.6.0 -> 0.7.0; @logicsrc/schemas and @logicsrc/validators 0.3.0 -> 0.4.0 (the OpenErrand schemas, and the vocabularies now exported for runners); PRD 0009; the spec's Reference runner section and the landing page say it ships. 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:
parent
1d69dc3804
commit
dde596b276
46 changed files with 5263 additions and 14 deletions
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@logicsrc/cli",
|
||||
"version": "0.6.0",
|
||||
"version": "0.7.0",
|
||||
"description": "LogicSRC CLI: every LogicSRC standard and tool as one command.",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
|
|
@ -18,6 +18,7 @@
|
|||
"@logicsrc/account-core": "file:../account-core",
|
||||
"@logicsrc/opencontext": "file:../opencontext",
|
||||
"@logicsrc/opencreds": "file:../opencreds",
|
||||
"@logicsrc/openerrand": "file:../openerrand",
|
||||
"@logicsrc/openfleet": "file:../openfleet",
|
||||
"@logicsrc/openmcp": "^0.3.1",
|
||||
"@logicsrc/openontology": "file:../openontology",
|
||||
|
|
|
|||
42
packages/cli/src/errand.test.ts
Normal file
42
packages/cli/src/errand.test.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { Command } from "commander";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { registerErrandCommands } from "./errand.js";
|
||||
|
||||
/** A program shaped like the real one: positional options on, no process.exit. */
|
||||
function program(): Command {
|
||||
const p = new Command();
|
||||
p.name("logicsrc").enablePositionalOptions().exitOverride();
|
||||
return p;
|
||||
}
|
||||
|
||||
const example = fileURLToPath(new URL("../../schemas/fixtures/openerrand/ftb-register-business.json", import.meta.url));
|
||||
|
||||
describe("logicsrc errand", () => {
|
||||
const homes: string[] = [];
|
||||
afterEach(() => {
|
||||
for (const home of homes.splice(0)) rmSync(home, { recursive: true, force: true });
|
||||
process.exitCode = 0;
|
||||
});
|
||||
|
||||
it("mounts run, validate and status", () => {
|
||||
const p = program();
|
||||
registerErrandCommands(p);
|
||||
const errand = p.commands.find((c) => c.name() === "errand")!;
|
||||
expect(errand.commands.map((c) => c.name()).sort()).toEqual(["run", "status", "validate"]);
|
||||
});
|
||||
|
||||
it("validates the spec's worked example through the umbrella", async () => {
|
||||
const out: string[] = [];
|
||||
const home = mkdtempSync(join(tmpdir(), "logicsrc-errand-cli-"));
|
||||
homes.push(home);
|
||||
const p = program();
|
||||
registerErrandCommands(p, { out: (l) => out.push(l), say: () => undefined, env: { LOGICSRC_ERRAND_HOME: home } });
|
||||
await p.parseAsync(["node", "logicsrc", "errand", "validate", example]);
|
||||
expect(process.exitCode).toBe(0);
|
||||
expect(out.at(-1)).toMatch(/^valid OpenErrand 0\.1/);
|
||||
});
|
||||
});
|
||||
35
packages/cli/src/errand.ts
Normal file
35
packages/cli/src/errand.ts
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
import { spawnSync } from "node:child_process";
|
||||
import type { Command } from "commander";
|
||||
import { registerErrandCommands as registerRunner, type Deps, type LogicsrcExec } from "@logicsrc/openerrand/commands";
|
||||
|
||||
/**
|
||||
* `logicsrc errand …`
|
||||
*
|
||||
* The runner lives in `@logicsrc/openerrand`, the reference implementation of
|
||||
* OpenErrand; this file only mounts it. A team vault named by an errand
|
||||
* (`teams:<team>/<project>/<env>`) is read and written through this same CLI's
|
||||
* `teams pull` / `teams push`, re-entered as a child process so the runner
|
||||
* never needs the CLI's session code and the plaintext lives for one call.
|
||||
*
|
||||
* `deps` is injectable so the umbrella test drives the group without a
|
||||
* terminal, a browser or a vault.
|
||||
*/
|
||||
export function registerErrandCommands(program: Command, deps: Partial<Deps> = {}): void {
|
||||
const errand = program
|
||||
.command("errand")
|
||||
.description(
|
||||
"OpenErrand: run an errand file on a website with no API, in headless Chrome, stopping at every step " +
|
||||
"that belongs to a person (declarations, identity proofing, codes, letters, captchas).",
|
||||
);
|
||||
|
||||
const self: LogicsrcExec = (args, options) => {
|
||||
const entry = process.argv[1];
|
||||
const result = entry
|
||||
? spawnSync(process.execPath, [entry, ...args], { encoding: "utf8", stdio: [options?.inheritStdin ? "inherit" : "ignore", "pipe", "pipe"] })
|
||||
: spawnSync("logicsrc", args, { encoding: "utf8", stdio: [options?.inheritStdin ? "inherit" : "ignore", "pipe", "pipe"] });
|
||||
if (result.error) return { status: 1, stdout: "", stderr: result.error.message };
|
||||
return { status: result.status ?? 1, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
|
||||
};
|
||||
|
||||
registerRunner(errand, { logicsrc: self, ...deps });
|
||||
}
|
||||
|
|
@ -41,6 +41,7 @@ import { exportOpenSpecSummary, importOpenSpec, writeOpenSpecChange } from "./op
|
|||
import { registerOpenContextCommands } from "./context.js";
|
||||
import { registerOpenCredsCommands } from "./creds.js";
|
||||
import { registerFleetCommands } from "./fleet.js";
|
||||
import { registerErrandCommands } from "./errand.js";
|
||||
import { registerOntologyCommands } from "./ontology.js";
|
||||
import { registerPrdCommands } from "./prd.js";
|
||||
import { registerOpenMcpCommands } from "./openmcp.js";
|
||||
|
|
@ -1176,6 +1177,7 @@ registerOpenCredsCommands(program);
|
|||
registerOntologyCommands(program);
|
||||
registerPrdCommands(program);
|
||||
registerFleetCommands(program);
|
||||
registerErrandCommands(program);
|
||||
registerOpenMcpCommands(program);
|
||||
registerMcpCommands(program);
|
||||
// Every other word under `logicsrc openspec` is OpenSpec.dev's own CLI.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue