# How it works Pomerado has three parts. The minter builds an integration, Guardian reviews the minter's work while it builds, and the runtime runs the result. A job model ties them to your MCP client. This page also covers the library interfaces, the line between this repository and other hosts, and the source layout. ## The minter - The minter is a model agent with a local workspace and a real Chromium browser, driven through native Playwright. - It reads the site, writes operation modules under `src/`, and runs them against the live site to check them. - A build turn may send several tool calls in one response, so a step's edit and the `execute` that runs it go together, and reads go together. The host decides how each call runs. Reads, `read_source` and `captcha_state`, sent together run at the same time. Every other call, edits, `exec_command`, `execute` and the other host tools, runs alone in the model's order once the calls before it returned, and a read after one of them waits for it. A call after an edit in its response waits until the response's edits land, then runs with no model call between, so Guardian reviews and the step runs what the edit wrote. Such a call runs only when every edit in its response came before it and applied. Otherwise it comes back `not_run` and sends nothing. Once an `execute` or `exec_command` does not return a `completed` receipt, because it failed, review refused it, it timed out or it may have dispatched, no later call of its response runs: each comes back `not_run`, and the minter decides again with that result. Reads already started before it are unaffected. Nothing in a response runs after `finish_build` or `report_blocked` ends the build. - It reads source, skills and captures with `read_source`, one file per call or up to 10 in one call. Each file reads as a call naming only it would, and the results come back in order. Once a call's results reach 200,000 UTF-16 code units, the files left come back unread for another call. - Every minter request starts with the same prefix for a release: the tool definitions, then one developer message holding the instructions and the host's static context (`MintDependencies.staticContext`), whose last part carries an explicit prompt-cache breakpoint with a 12-hour lifetime. Jobs of the same release share that prefix in the provider's prompt cache. A takeover's first request sends the same bytes, since the new process records the installed skill files in its workspace manifest without mounting them again. What differs per job comes after the breakpoint: the job's request, then a tools message that lists the host tools this build offers (`hostTools.offered` and `hostTools.notOffered`) and how its site signs in (`signIn`, `host_autofill` or `managed`). The rest of the conversation is cached at the provider's implicit breakpoint, with its default 30-minute lifetime. - Every build turn sends every host tool, in one order, with one description and one schema each. `execute` has one strict schema in which each optional field is required and nullable, and `signInStep` is null on a managed sign-in. Which tools a build may call is the request's `tool_choice` (`allowed_tools`), which is not part of the cached prefix, and the host refuses a call to a tool the build does not offer as `tool_not_offered`, and a `signInStep` on a managed sign-in as `sign_in_step_not_offered`, running nothing. The effect question turn keeps its own short request with only `request_input`. - A host sets `MintDependencies.promptCacheKey` to send `prompt_cache_key` on every minter request, such as one key per environment, and calls `prewarmMinterPrefix` from `pomerado/core/mint/openai` at deploy or startup to write the static prefix to the cache before the first job. The prewarm is the minter's own first request, built from the same instructions, skills, static context, tool descriptions and key, with `prompt_cache_options.prewarm` and only the prefix as input. The provider generates nothing and bills the write at the cache-write rate. Each call's `model_usage` trace splits its cache writes by lifetime, `cacheWrite30mTokens` and `cacheWrite12hTokens`. Every request after a run's first sets `prompt_cache_options.comparison_response_id` to the last turn's response, which asks the provider only for diagnostics, and the trace carries the response's verdict as `promptCacheDiagnostics`: its `type`, `reason`, `cacheMissedTokens` and `comparisonReusableTokens`, known enums and counts only. - The minter's conversation is compacted once a request reaches 250,000 input tokens, in the background while a turn's tools run, and the provider's own compaction at 268,000 tokens stays as the backstop. One compaction runs or waits to apply at a time: a turn that went out before a compaction landed starts none. A request whose estimated input, the last request's tokens plus what was added since at three characters a token, reaches 260,000 waits for the running compaction, or compacts the last request first, so no request reaches the model's 272,000-token long-context price boundary while a compaction is on its way. Either way, the skill pages it read are no longer in view, so the request after the compaction item carries a host note naming the skills a successful `read_source` of `.agents//SKILL.md` returned before the compaction, for skills in the build's catalog, and inviting the minter to read any of them again. A failed read, a page outside the catalog and the model's own claim of a read don't count, and with no skill read there is no note. The note is fixed per compaction, so every later request sends the same bytes and its cached prefix holds. A host words it itself with `MintDependencies.compactionSkillsNote`. - An `execute` result comes back whole while its screened text fits in 16 KiB (`resultDigestThresholdBytes` in `typescript/src/mint/result-digest.ts`). A larger one is saved whole, as indented JSON, to the read-only `captures/results/.json`, and the minter gets a digest instead: its size, its shape with every array's length, its first items and the file's path, which it reads with `read_source` or searches with `rg`. A completed result's error and stack fields, and a failed result's error, message and reason fields, stay whole up to 16 KiB, so the digest never hides why something failed; a failed result that names none keeps its first 16 KiB of text. The host saves the file through `MintDependencies.saveResult`; a host without it, or a save that fails, returns the result whole. A command's output is never digested. Guardian's view is unchanged: each execution review already gets the last steps' results cut at 4 KiB, never the whole result. - A live example, a live test and a write's first step start over at the site's root, after other tabs close. They keep the browser's cookies and site storage, so a site that trusts a browser session keeps trusting it, and the script must set every value its input controls itself. A build whose sign-in started and was never verified clears them instead, so a half-finished sign-in never reaches a step. - A read designs a few live tests of its own, each covering as much as it can. As soon as it knows the page and its inputs, the minter writes its cases in `test/cases.json` from what it saw on the site, each with its purpose, following the testing skill. The first is one combined case that sets every optional input and filter the example left unset, with values the site shows, such as a picker's options or values in the example's own rows, each chosen so at least one example row fails it while some rows still pass, so a filter the tool ignores shows up as a row that breaks it; the minter checks every returned row satisfies each input it set and the excluded rows are gone, and the case runs page two when the tool pages. The host fails a list case whose page two holds only rows page one already returned, as when the tool ignores its cursor. A read signed out adds a handful more: the example's input once more, no results, and another case only where a different page structure or real variant needs it. A read whose answer is a count or total answers an input that matches nothing with 0, which the judge reads as a result: its no-results case expects `result`, sets `noMatch: true` and returns the count or total as 0. The publication record keeps the mark, and the review counts it as the no-match case only against a capture of the site applying that input and showing zero or no records. A signed-in read, on a host that declares `LiveTestHost.signedInBatches` (the local host does), runs the combined case first, then the cases that cover the most, such as one made-up key for an input that takes a record's own key: the harness allows it at most `signedInTestInputLimit` (four) distinct inputs in a build or repair, counted across `live_tests` batches and `execute` test inputs and kept across a takeover, and refuses a batch or step that would add another as `limit`; running a counted input again, after a fix or on the HTTP version, is free. On a host without it, a signed-in read runs no live tests and its example alone proves the tool. The minter lists what it chose not to test, with why, and runs the cases in batches with `live_tests`. Guardian reviews each batch once, and each case starts like the example, keeping its browser's site data. Each result records whether the case ran cold, first on a new browser with an empty profile, or warm, on a browser that kept what earlier steps left. The local host runs a batch one case at a time on its one browser, so every case runs warm. A host with more browsers may run several at once, and runs at least one case cold. A signed-in read's batch is marked `signedIn`, and its host runs every case on the build's own signed-in browser, one at a time. A batch with less time left before its deadline than one case may take on that host (`LiveTestHost.caseTimeoutMs`, 60 s by default) is refused as `no_time` and runs nothing. A host that caps its batches may keep one more in reserve past its cap (`LiveTestHost.reservedCaseTimeoutMs`): once no batch fits, a batch runs on it only when every case it names is one a failure is pending on (`pendingFailureCases`), a case whose input, expectation and next page are a failing result's, its own latest or a retired one, that has not passed since on the current source and did not fail on it, since a pass on the source a case failed on excuses nothing, and the reserve's case time is left before the batch's deadline. The `no_time` refusal of any other batch names those cases (`reservedFor`), and says the attempt has run every batch its host allows when the cap, not the attempt's end, stopped it. A `no_time` answer whose host's reserve is closed also says that when publication review then denies for a failure only a new batch could clear, the minter prefers `report_blocked` with reason `host_refused`. One whose reserve is open with no case pending yet says the reserve remains, and names the cases that failed on the current source, which join it once the code changes. Once no regular batch fits, the record says time ran out, whether or not the reserve runs. A next-page case runs page two from the cursor page one returned, which the host signs and checks as it does a run's. The minter sees every result, with the source line a failure threw from. It runs the same cases as a batch on the HTTP version, and each version keeps its own results: the publication record shows each case's result on both, and the review judges each version from its own. A write designs no tests: its act session is the one run that proves the tool, and the HTTP version is checked once against it. - A read's publication goes to review only when at least 70% of its counted cases pass on the version it publishes, the `finish_build` entrypoint's (`reviewPassPercent` and `liveTestsShortfall` in `typescript/src/mint/live-tests.ts`). The counted cases are the cases in `test/cases.json` whose latest result on that version's published source is a pass or a fail. An inconclusive result, such as a challenge, a site block, a host fault or the deadline, a stale or not-run case and a retired case count neither way, and a version with no counted case reaches the bar. An HTTP version beside a published Playwright version is not held to it: the review judges it from its own results, as before. Below the bar, `finish_build` is refused before review as `live_tests_below_threshold`, with recovery `new_observation`, and the minter is told how many passed. The refusal grants no `host_refused` ending. Once the attempt ran out of time for live tests, a batch refused as `no_time` or a case stopped by the attempt's deadline, or when no further batch fits in what is left, by the same check that refuses a batch as `no_time`, as after a host's batch cap, no case can run again, so the bar does not apply and the review decides as before. The bar holds for a host's repair of a published read that runs live tests as for a new build. A signed-in read is held to it on the cases it ran signed in, the only ones its record holds. It is not 100% so that a case that is itself wrong does not hold back a working tool while the minter corrects it. - A host that sets `LiveTestHost.holdReviewOnFailingCases` also starts no review while any counted case of that version fails on its source (`failingLiveTestCases` in `typescript/src/mint/live-tests.ts`). `finish_build` is then refused before review as `live_test_cases_failing`, with recovery `new_observation`, naming the failing cases, up to ten, and the minter fixes the code, or retires a case that is itself wrong by changing or deleting it. Below the bar the bar's refusal stands. It lifts with the bar, once no case can run again, and grants no `host_refused` ending. The local host does not set it. - Otherwise test results never refuse publication by themselves. The publication review reads the host's record of the cases on the source it publishes, `publication/tests.json`, and judges the cases against the tool's claims and the captures: a failing case, including one the minter deleted or changed, or a claimed capability no passing case tests. A retired failure stops counting only on a fact the host checks: `outsideSchema`, when the version's published input schema bounds a value its input supplied, never for a field the schema newly requires (checked with Ajv; the review then judges that bound against the site), or `excusedBy`, a case that passes the same input after the source changed with a result the failure's own expectation accepts under `judgeCase`, such as `InvalidInput` for an expectation of `error`. A failing case the minter changed to expect another answer for the same input is excused only when the captures show the site's own answer for that input matching the new expectation, reached through the site's own controls: its no-results state for `empty`, its own results for `result`, or its own form refusing that input, with its validation message or a 400, 413, 414 or 422 response to the form's request, for `invalid_input` or `error`, never a refusal of the request the tool made; the record names the passing case as `changedExpectationBy`. Each retired failure that still counts carries the host's `clearedBy`, the ways that can clear it and the site answer its lead needs, and a review finding about the record names the case, input or schema change that clears it, or the evidence that is missing. A finding about the record is the minter's to fix through its cases, source or coverage text, never `host_owned`. When the attempt ran out of time for tests, a case could only end inconclusive, or no case passed cold, the gaps go into the review's rationale, not its findings. A signed-in read's record, marked `signedIn`, holds only the cases it ran signed in, and appears only once one ran; the review judges a failing case there as for any read, but a signed-in read, like a write on its one run, gets no finding for what its runs did not test. - The host opens the URL you gave before the build starts, once the build's read or write is known, and the build's first request tells it whether the page opened, its status and where it landed. The host waits for the page's document, not every resource, for up to 30 seconds. A page that fails to load, or that the site answers with an error status, doesn't stop the build: it runs on whatever page the tab shows. Its first live step starts there, unless that step is one of those resets. Explorations then continue on whatever page the last step left. - A build counts as signed in only after its own sign-in steps sent the login's identifier and a password or code, or you completed an approval, and the page then shows the account. A screen counts as sent once the host clicked its submit after filling it, or the page sent what was typed itself, or the agent went on from it after the host's click failed. A code the site sent for that sign-in counts too once the agent asked you for it and an explore made a completed `fill`, `type` or `pressSequentially` call with it on a page, frame, locator or keyboard, in a frame on the site or on one of the sign-in origins you configured. A frame with no address of its own, such as `about:srcdoc`, isn't the site. Keys typed with nothing focused count only when every frame of the page is on the site or one of those origins. Waiting for the code field before typing the code makes sure the field is there when the typing starts. A page that already showed an account proves nothing. A check after a confirmed sign-in starts a new sign-in. - A sign-in form whose fields or submit go to another origin than the site's or one of the sign-in origins you configured is refused before anything is typed, as `credential_target_refused`. Pass a sign-in origin you trust in `authenticationOrigins`. A sign-in form whose own action is a script that does nothing (`javascript:`, `javascript:void(0)` or `javascript:;`) goes to no origin by itself, since the page's own code sends what it holds, so it is judged by where its fields and submit sit and by its other destinations. A sign-in submit link whose own address is such a script, such as a header Sign In link that opens the sign-in panel, likewise goes nowhere by itself, so it is judged by where it sits and by its other destinations. A form or link whose script does anything else, and a payment card step's script action or link, are still refused. - The host reads a phone field back before it clicks the submit. When the field kept other digits than the number, as when the site's country picker is set to another country and its mask shifts or cuts the number, the host types nothing more and clicks nothing, and the agent hears the number's calling code, or the site's own country for a number written without one, so it can set the picker and send the step again. Nobody is asked to correct a number that was right. - A code screen that asks you to do something first, such as call a number or approve in an app, shows its own instruction in the code question when the sign-in step names it as the code field's `questionSelector`. A plain code screen asks for the code the site sent. - A rejection the agent reports counts only where the error message the screen recorded for that field (`rejectedMarkers`) shows on the page, or showed right after the host's submit of the latest screen, and a code's always does. A rejected username or password is corrected in place, twice per field at most, and each question says how many tries are left. A value the site rejected is never typed again. - After a reset that keeps a verified sign-in's session, the host checks the signed-in marker on the root. When the page reads signed out, it signs in again with the login it holds before the step runs and resets once more. - A missing marker alone can't tell, since a site may show a screen of its own before the marker, such as an account picker, a "remember this device" prompt or its terms. A page reads signed out only on the sign-in's own evidence. A marker that names a path must be missing on that path. A marker with no path must be missing where the sign-in's own password field shows again, or on one of the sign-in's recorded screens. An off-site page reads signed out only when it is one of those screens. - A marker that names a path reads as unknown on the root, so it costs nothing. So does a root that shows neither the marker nor the sign-in's form. - A marker with a page to open costs a load of that page and one more reset when the root reads signed out. - On a site whose root shows the sign-in's form, and whose sign-in page shows its form to a signed-in browser too, the check that types nothing can't decide, so the host signs in. The reopened root still reads signed out, so the first reset signs in twice and takes the session as one the root's load loses. Each later reset signs in once, and the step starts on the page the sign-in left. The fourth such reset fails with the session not kept. - Only a read build's sign-in again may end on a page that can't tell, as a read's may in a run. A write build's counts only once its marker shows. So does a build whose request names no effect or leaves it to the build. - Before it sends a signed-in marker, the minter can test it with `check_signed_in_marker`. - The host compares the marker with the pages it saw signed out: the page of the build's first sign-in screen, just before the host types into it, and each page a reset left signed out: one that cleared cookies and site storage, or any reset before a sign-in started. It reads a page once it loaded, its network went quiet and its document went 1.5 seconds without a change: about 2 seconds after its load, for a fast page. It waits 3 seconds at most, and reads a page still busy then as it is. It keeps only a page on the site's origin that shows something. A page still busy at 3 seconds, or one that showed something only after its document went quiet, counts only where it shows the marker, never to show it absent. The host doesn't see what a page renders after it read it, so a signed-out header that renders more than 1.5 seconds after the page went quiet is missed. Once the host typed a sign-in value in the session, it keeps no sign-in screen's page, since builds in a session share cookies. - It then checks the page as it is, the marker's `openPath` or the site's root loaded again, and the newest other page the build explored once its sign-in sent the login. It loads that other page only when the current page shows the marker. It checks each page it loaded again every half second, without loading it again, until the page shows the marker, the check fails for another reason, or its next check would start more than 10 seconds after its first check there. - The loads move the tab. The host opens the agent's page again when that page showed the marker and isn't the direct answer to a form. Otherwise the tab stays where the loads left it. - Once a write session started, the check loads no page and reports itself unavailable. - The `signedIn` step refuses a marker that one of those signed-out pages shows, and a path alone that is one of the sign-in's pages, before it reads the page. The sign-in stays open for another marker. It then waits up to 10 seconds for a marker the page draws late. - A failed act step Guardian labelled a write tells the minter to read the site back before any further write, unless its commit marks show it entered no commit step. The rule and its text come from `readBackFirst` in `pomerado/core/mint/write-session`, the same for every host. The local host records no requests that would show a write the page sent before any mark was entered, so it skips the read-back on those marks only when the step also made no browser call. - A step whose entrypoint does not parse is refused before review, naming only the line and column, never the source. The check is `mintSourceSyntaxFailure` in `pomerado/core/mint/operation-source`, and a sign-in step, which runs the host's own code, skips it. - The local host publishes nothing that rests on a live step it ran after a sign-in sent the login and before any check verified it, unless the build publishes a verified sign-in. A write rests on every act step of its session, and a read on the example or explore it publishes. `finish_build` refuses such a build as `autofill_recipe_not_verified` before any review. - The local host refuses to publish a saved file that holds a provider API key or an AWS access key ID, by the shapes error messages mask (`pomerado/core/privacy/credential-policy`). `finish_build` refuses it as `source_screening` with a `provider_credential` publication block naming the file, line and column, never the key. Guardian's publication review still reads every published file for other hard-coded credentials. - That refusal is deliberate and fails closed. Such a step may have run signed in, as an explore on the page the sign-in left can, and the tool's runs couldn't sign in. A read is refused even when its example ran signed out, since the build signed in because it took the task to need the account. - Publishing a write first checks that its session may have sent the write. A step that recorded a confirmation, entered a commit mark or made a browser call counts. Only then is the composed contract read, reviewed and checked against the session. - A write build has one write session, so its first act step declares the whole path that session and the published tool take, as `writePath`: each step that saves or submits on the site, in order, by the commit mark its helper enters, and what the last step leaves the caller with. The host refuses a first act step without one before review. Guardian's execution review judges the path by the rule its publication review judges a write's outcome by, and denies one that ends before the requested outcome, such as at a saved draft when the request asks for a quote, or one no single session can reach, such as a step that waits for another person's approval. The host records the path Guardian allowed. - A step that records a confirmation before the session entered every mark of the path leaves the session open, and its receipt's `writePath` lists the marks not yet settled: unentered, or entered only by a step that lost its result, which a read-back confirming the write settles. Its text never asks to run a step that finished or may have again. A later act step may revise the path when the page shows it must change, keeping every mark the session entered, in the same order. - Publishing refuses a session that has not entered every mark of the path as `write_path_unfinished`, before it reads the composed contract, and a composed script that does not declare them all as `write_path_undeclared`. Both name the marks in `writePathMarks`. The publication review judges the script against the same path. - The rules and texts come from `writePathRefusal`, `writePathRemaining` and `writePathNotice` in `pomerado/core/mint/write-session`, the same for every host. A session a host started before it kept paths runs on without one. - The composed script must then name each step where you accepted a confirm popup. A run matches a recorded confirm by its step, so a renamed step is refused. - It finishes by publishing an entrypoint with JSON Schemas for the input and the output. - The saved integration holds every file under `src/`, the entrypoint, and the files under `explore/`, `test/` or `scratch/` that they import. Every file under those four folders is saved instead when the workspace has a `package.json` or one of the folders holds `node_modules`, when a saved module reads or loads files another way, such as through `fs`, `createRequire`, a `#` import or Playwright's internal modules, or when one of the files they import is a WebAssembly module, a native addon, or an extensionless file that isn't JavaScript. Paths match in any letter case, as macOS loads files. - It asks you questions through the job when it needs a login, a code or a choice. - A step's script asks you only the questions its entrypoint declares as a plain literal in its one `defineOperation` call. The host reads them from the source, not from the running script. A declaration held in a variable, imported or computed declares nothing, so its ask fails as `Undeclared` and reaches neither Guardian nor you. - When a value you gave isn't available on the site, such as a sold-out date or an option the site doesn't list, it asks you whether to change it or stop. The question names your value and offers what the site has. It never picks another value for you. - A write keeps each native confirm you accept during its act steps on an https page. It saves up to 32 of them in `pomerado.json` as `acceptedConfirms`, each a digest of the message, the origin and the step. No page text is saved. When the message names a value of the step's input, or an identifier in the page's URL, the digest is taken with each such value replaced by where it came from, so a run about another record can match it. - It gets 20 minutes of active work. Time spent waiting for your answers doesn't count. - Its prompts and examples come from `typescript/authoring/`. Every host reads the same text, except at the named sections where a host puts its own. - Its `AGENTS.md` starts with a list of features the prompts describe that another host supplies and the local host lacks. The minter ignores them. `typescript/src/hosted-features.ts` holds the list, one line per feature. - Its workspace also holds `reference/publication-review.md`: Guardian's publication rules for live tests and output, the same text the review reads, so the minter can gather that evidence before it publishes. ### Failures a build survives - A diagnostic copy the host can't keep, such as an execution's diagnostics or a readable model transcript, is a recorded gap in the outcome's diagnostics. The build goes on, however many gaps there are. - The raw record of each model call is the one required trace. A host that keeps one through `retainRuntimeRecord` must store it before the next model call. If it can't, the attempt stops with `hostFailure: "diagnostic_retention"`. A host without `retainRuntimeRecord`, such as the local host, has no required trace. - A review whose evidence the host can't keep is a review outage. The minter may resubmit until reviews have been unavailable for the review outage budget, 15 minutes by default. A review that failed `InputTooLarge` is no outage: resubmitting it would fail the same way, so the attempt ends at once, as past the budget, and a blocked explanation's review ends blocked with the reason alone. - An execution whose capture the host can't produce or screen comes back as a capture gap: its result is withheld and its effect is possible. A publication whose capture evidence is unavailable comes back `not_published` with `retryable: true` until the review outage budget runs out. Neither runs a write again. - A publication dependency that stays unavailable, such as the registry, its source store, or a screening step or source read that names no file to fix, comes back with `retryable: true` until the review outage budget runs out. - `report_blocked` with reason `policy` needs a refusal on record in the attempt: a Guardian deny or escalation of an execution that Guardian marked `refusal: "final"`, a Guardian publication denial or escalation for privacy or authority or with a `private_literal`, `credential`, `customer_data`, `exfiltration`, `unsafe_logging` or `safety_default` finding, or Guardian's fifth refusal in a row of a write build's `act` step, deny or escalate, correctable ones included, with no `act` step Guardian allowed between. An explore between refusals leaves the count, and a takeover starts it again. A publication denial or escalation that names a correction or missing evidence, such as `source_correction`, `unsupported_claim`, `evidence` or an `example_value` finding, is not one, whatever its outcome: the minter fixes it, or gathers the missing evidence the rationale names, until the build's other limits stop it. Without one the host refuses it as `policy_not_refused` and the build goes on, unless the host's `MintDependencies.policyBlockAllowed` allows that explanation, an ending the host's own guidance names. A `policy` report that stands only on the owner's no or on an unanswered question ends as `owner_declined` or `question_unanswered` instead. - An execution review's deny or escalate carries `refusal`: `final` when no change to the source, input or evidence within the request's authority would make the step acceptable, and `correctable` otherwise. The minter reads it in `review_rejected`. Only a final refusal lets a later `report_blocked` use `policy`. A refusal without one, from a reviewer that predates the field, or with a value Guardian may not return, counts as `final`, as every execution refusal did before. The checkpoint keeps the basis as `guardianRefused`; one from an older worker, set on any execution refusal, is restored as it is. - Guardian's third correctable denial in a row of one unchanged sign-in step ends sign-in in the build as `sign_in_unavailable`, with `spentSignIn: "guardian_denials_repeated"`, whatever the rationale's wording. A changed step, or an authenticate that ends any other way, starts the count again. An explore or a question between denials leaves it. The diagnostic that records sign-in as unavailable keeps that denial's `reviewId` and screened `rationale`. - An execution deny or escalate may name the host fact it rests on as `refusalGround`: `retry_permission`, `committed_write` or `write_authority`. The host checks it against its own record. A retry-permission refusal while `repeatableRead` is true, a committed-write refusal of a step the review labels a read, or a write-authority refusal of a step with `PendingExecution.writeAuthority`, goes back once in the same review, and a refusal that keeps resting on it counts as `correctable`. After a committed-write refusal goes back, only an allow labelled `read` stands; any other allow keeps the original refusal. The decision the host returns drops the field. - An execution or recovery allow may carry `advice`, Guardian's note to the minter, such as a doubt about which page value the request's output means. It never blocks the step: the minter reads it in the execution's `review`, and only publication review judges a tool's output and capability claims. The host drops advice on a deny or escalate and cuts it to 2,000 characters. - The `intent` of the minter's `execute` call reaches that step's execution review as `submitted_call.agentNote`: the agent's own description of the step, untrusted and never authority, cut to 500 characters. A host passes `reviewAndExecute`'s `agentNote` into `PendingExecution.agentNote`, screened like the input. - `report_blocked` with reason `host_refused` needs a host check's refusal on record in the attempt, which the host records itself: a publication the host's own checks refused (`PublicationUnavailable` that is no outage, no unavailable example output, no one-time login URL and no untested or stale HTTP version, such as a sign-in that fails on a new browser), never Guardian's denial or the harness's refusal of the request itself, a sign-in the host refused to send where credentials may not go (`CredentialTargetRefused`, `CredentialTargetUnverified`, or `AutofillRefused` by its `destination` check), a sign-in screen the host refused as `credential_already_filled` because it already filled the password twice with none verified since, while the password stays spent (`signInRefillRefused`), a sign-in screen the host refused at its `destination` check and returned as the step's result rather than a failure, while no later screen that takes a password or code passed that check and no sign-in verified since (`signInDestinationRefused`), or a `mint_update` the host's `applyTaskUpdate` refused, such as a site change. It also stands on live tests the host refuses while Guardian's denial of the latest publication stands: the attempt ran out of live tests (a batch refused as `no_time`, a case the attempt's deadline stopped, or the host's reserved batch run), no batch fits now, and the host's reserve is closed (it ran, the host keeps none, or too little time is left for it), so nothing left in the attempt can clear a failure the denial rests on. While the reserve is open, the report is refused as `live_tests_reserved` and the build goes on, even when no case is pending until the code changes: it names the cases the reserve would run now as `reservedFor` and those that failed on the current source, which it takes once the code changes, as `failedOnSource`. When it names neither, it adds that a denial with no failure still counting in `publication/tests.json` rests on something else, so the minter fixes what the denial names. The cases file is read only to name them, so one that cannot be read still refuses the report. A report that stands only on refused live tests records `basis: "live_tests_spent"` in its `blocked` diagnostic and says so in the summary, and the outcome keeps the denial as `publicationDenial`. Without one the host refuses it as `host_not_refused` and the build goes on. A later `finish_build` that gets past the host's checks to Guardian's review clears a refusal the harness recorded (`hostRefused`), since what Guardian decided is then what stops the build, and a `host_refused` report with no host refusal since is refused as `host_refusal_passed` and the build goes on. It leaves the host's two sign-in screen answers as the host gives them: `signInRefillRefused` stands while the password stays spent, and `signInDestinationRefused` until a later screen that takes a password or code passes the check or a sign-in verifies. A refused preflight, a selector the fill could not use and the harness's own refusal of an update are the minter's to fix, so none counts. Guardian reviews its explanation like any blocked ending. The checkpoint keeps the basis as `hostRefused`, a cleared one as `hostRefusalPassed`, and the host's latest `signInDestinationRefused` answer, read after each authenticate, as `signInDestinationRefused`. - Once sign-in is unavailable in the build, any `report_blocked`, reason `policy` included, ends the build with the sign-in's own result instead of the reason it names. - An unanswered question the harness does not hand back to the minter ends the build as `no_response`: the read-or-write and capability questions, any question in a write repair, any asked while the build is signed in or holds a login, a sign-in's own question, and any after the hand-back limit. A host may suspend such a build instead, to resume it later, through `MintDependencies.suspendUnanswered`. The harness offers it the question (`UnansweredQuestion`: where it was asked, its id when known and whether the build may have changed the site), never one it hands back and never one the owner cancelled. Once the host answers `suspended`, having made the state it resumes from durable, the run stops at once: the call that asked never returns, so the minter's checkpoint keeps it started, no further model call starts, no outcome is recorded, and the run ends interrupted. Any other answer, or a failure, ends the build as `no_response`, as without the hook, and so does `suspended` when the model run ended while the hook ran. A question it hands back goes to `MintDependencies.suspendHandedBack` instead, and only when the minter then ends the build on it with `report_blocked` reason `question_unanswered`, the minter's own question with no other asked or handed back since, and never one that asks whether to end the build. The report is then the call that never returns: a resumed attempt rejoins it through `recoverTool` with the owner's answer as `input`, and the minter reads that answer as the report's result and goes on. Without that hook, or on any other answer, the build ends blocked as before. The local host has no checkpoint store and does not suspend. - A host resumes a suspended build, or continues one a takeover restored, from `agentRecovery.initial`. Its optional `resumeMessage` is the host's own text, such as why the build stopped and that the page may have changed, and the restored minter reads it once as a user message before its next model call. A snapshot that holds a saved model response replays that response first, and `recoverTool` rejoins each call it left started, such as a question with the answer the owner gave meanwhile; the message follows their results, so every call keeps its result. A question that `recoverTool` finds unanswered, failing it with `noResponse`, such as one that expired while no worker held the build, goes on as a live one: back to the minter as no answer where the harness would hand it back, and otherwise offered to `suspendUnanswered` without its id, which the harness never learned, and ended as `no_response` unless the host suspends it. The message joins the conversation with that model call, so only checkpoints from that call on hold it: a takeover that restores an earlier checkpoint passes it again, and one that restores a later checkpoint does not. - `report_blocked` with reason `owner_declined` needs the owner's no to a `confirm` question or a declined login in the build, `question_unanswered` needs a question handed back as no answer, and `owner_ended` needs the owner's latest answer, not a secret or sign-in, with no other call through the turn's actions since, and `caller_input` needs its `intent` to cite the request, by a field name its input holds or four words in a row of its text, or a task update the owner confirmed in an answer, by its revision or four words in a row of its text or of that answer; the host refuses each without it (`owner_not_declined`, `no_question_unanswered`, `owner_not_ended`, `caller_input_not_cited`). - Every unpublished outcome the harness can name carries `stopReason`, such as `final_text_without_tool`, `turn_budget_spent`, `sign_in_failed` or `account_locked`. When the minter stops after three answers without a tool call, its last answer goes through the blocked explanation's screening and Guardian review, and the outcome keeps it as `stopExplanation` only when Guardian allows it; a screen or review that fails, dies or outlasts `MintDependencies.stopExplanationReviewMs` (one minute by default) keeps nothing. A publication denial may carry Guardian's screened `publicReason` for the caller, dropped when the screen fails; its rationale stays private. - When the owner declines a login (`no_account` or `public_only`), the minter hears it as a `login_declined` result and builds the part of the request the site shows signed out. No further sign-in runs and no further login request is raised in the build. - A sign-in step may report that the site refused the account itself (`rejected: { account: "locked" | "suspended", selector }`). The local host checks that the element shows, then ends the sign-in as `AccountLocked`: sign-in is unavailable, and the build ends `account_locked`. - Where the host offers `MintDependencies.abandonSignIn`, the minter's `abandon_sign_in` gives up signing in, and an applied `mint_update` with `login` change `signed_out` does the same with the owner's confirmation: the unresolved sign-in no longer holds the build back, and it goes on signed out. The host decides whether a build may abandon its sign-in; Guardian does not review it. From then on the build stays signed out: `authenticate` runs nothing and comes back `sign_in_abandoned`, and a question routed to sign-in asks for no login, until a later applied `login` change `sign_in` or `different_login` signs the build back in. The checkpoint keeps this as `signInAbandoned`. - A publication denial with reason `host_owned`, whose every finding is in a file the host wrote, ends the attempt at once as the host's own failure (`hostFailure: "publication_unavailable"`), with the denial in `publicationDenial`. No source edit fixes it, so it is neither the minter's to correct nor a `policy` block. It recurs on every build until the host fixes its file, so a host treats it as final rather than requeuing it like other host failures. The host sets that reason itself on a denial no correction gets past (`publicationRefusalUnfixable`), one for privacy or authority or with a finding such as `private_literal`, whose every finding is in a bundle file under `operation/` that the review's index marks `owner: host` (`hostWritten` in `publicationScope`), such as the host's sign-in recipe: `decodePublicationDecision` decodes it as `host_owned` whatever reason the review gave, and its rationale names that reason. Any other denial keeps the review's reason, since much of the recipe records the minter's own sign-in choices, such as its selectors and signed-in marker, which it fixes by signing in again. A host that marks no bundle file as its own, as the local host, keeps the review's reason. - A temporary, narrow exception covers choices on a site's own sign-in pages, such as a location, workspace or portal the site asks for before it signs in, which the host's sign-in makes and keeps in its sign-in recipe `operation/auth-fill.json`, where the minter cannot edit them. Until such choices become declared sign-in inputs, `decodePublicationDecision` sets an `input_option` or `example_value` finding in that recipe aside, in the decision's `excusedSignInChoices`, which the completion diagnostic records, when the decision is a denial for a source correction, input feedback or a host-owned file. The rest of the decision stands; with nothing left the decision allows, and with only input findings left it is input feedback. Nothing else is excused: any other finding in the recipe or another host-written file, any refusal for privacy, authority or evidence, and anything about the tool's own behavior, output or source blocks as before. `publicationSignInChoicePolicy`, part of `publicationDecisionPolicy`, tells the review the same. - A `finish_build` that changed nothing since Guardian denied a publication is refused before review as `unchanged_since_denial`, and the denial stays the build's `publicationDenial`. The host compares the request, the entrypoint's and the HTTP version's import closures, each file the denial names, and the executions, live test results, task revision, answers and captures the attempt holds. It keeps the comparison in memory, so a takeover drops it. - `report_blocked` with reason `host_unavailable` ends the attempt as the host's own failure (`hostFailure: "host_unavailable"`), with no `blocked` outcome and no explanation review. The host refuses it as `host_available` while live execution is still open. The minter uses it when a retained receipt cannot publish because it needs a source correction, which needs a fresh example. - `report_blocked` ends the build only when Guardian allows the explanation, or when its review stays unavailable past the review outage budget, which leaves the caller only the reason's fixed sentence. An unavailable review comes back with `retryable: true` before that. When Guardian asks for a reword, the minter gets the rationale, and may revise the explanation, which Guardian reviews again, or withdraw it and go on. - A `contract_input_mismatch` or `contract_output_mismatch` refusal's recovery is `correct_source` in a read build and `write_completion` in a write build. ### Publication decisions - `path_screening` stays a refusal the minter fixes, since it can mean a source path holds a credential. - Each `finish_build` decision, refused or published, is a `PublicationDecision`: its code, reason, execution, time, failed checks and recovery path. The tool result carries its `decisionId`. The harness's own refusals before publication runs, such as an invalid request, and a host fallback's publication after unresolved input feedback are decisions too. - A host keeps them as evidence through `MintDependencies.publicationDecisions`, which records each decision and lists the build's decisions. The local host keeps them in memory for the request. - A question review and a blocked-explanation review get the latest refusals as `question_review.publicationDecisions`, and a task update review as `update_review.publicationDecisions`, so Guardian reads what the host refused, not only the minter's account of it. - A refusal's decision keeps the instruction `finish_build` gave the minter as `instruction`, screened and capped, so those reviews also read what the refusal requires. Guardian's policy treats it as a host fact. - A blocked-explanation review also gets the harness's decision ledger as `question_review.decisionLedger`: the build's Guardian denials of executions, live test batches and publications, with the rationale and findings Guardian wrote, and the live test cases whose latest result did not pass, with what their tool threw marked `toolReported`. It is bounded, a takeover keeps it, and it does not depend on what Guardian's compacted conversation still holds. - Both reviews also get the build's effect, once the owner or the request decided it, as `question_review.buildEffect`. Their `allowedEffects` stays empty on purpose, so this is how Guardian knows a write build publishes only after it does the requested task once, for real. ### Sign-in versions `checkSignInVersion` from `pomerado/core/mint/sign-in-version` checks a sign-in recipe and the address its runs start from as one version, deterministically, before a host publishes it. - The recipe is decoded as a host reads it first; one that does not decode, such as one with no steps or a workspace page that is more than a path, is refused as `invalid_recipe`. - The checks then run over the entry URL, every step's page and selectors, the signed-in marker and the recipe's workspace relation. Every exact page runs on an https origin of the site itself, and a workspace step, named by path only, on each login's own host on the site's registrable domain. - They refuse the recording account's identity values and masked contacts or numbers anywhere, host labels included, sign-out steps and a marker account page that signs out, a one-time or credentialed entry URL, a marker a signed-out page shows, a workspace relation for another domain, a page that is not https (`insecure_origin`), a page on any other domain (`foreign_origin`), and a step that writes a path segment as `*` off the site's own origin and its workspace hosts, or on the page the entry URL opens (`wildcard_placement`). - A refusal names each rule and the part's path, never the value. - Off-site sign-in pages are always reported under their own rule, `foreign_origin`; a host that permits sign-in on another site's origin drops that rule's findings itself and still gets the https rule. A host says whether it already asked about a one-time entry URL, which then passes with only its path read for masked numbers, and the identity rules apply to the identity values a caller passes. - The local host runs the check before it publishes a build's sign-in and refuses `finish_build` as `sign_in_version_refused` when it fails, sign-in pages on another domain included, even one on a declared authentication origin. It passes no identity values, which it applied screen by screen while recording. ## Guardian - Guardian is a second model that reviews the minter's work before it takes effect. - While minting, it reviews each browser call before it runs, each question before it reaches you, and the finished source before Pomerado saves it. - A denied call never reaches the website. - Guardian doesn't review a saved integration's runs. It approved the source while minting, so running an integration makes no model request and needs no model key. - Its policy in `typescript/src/guardian/upstream-policy.md` is adapted from OpenAI Codex under the Apache License 2.0. - Its execution policy ends with the same list of hosted features, as protections it doesn't count on. ### Review requests Guardian reviews five built-in kinds of request: execution, question, task update, browser recovery and publication. A host can add its own kinds. With a session (`makeGuardian`'s third argument), all of a mint's reviews are turns of one conversation. - **One request layout.** Every kind sends the same instructions, the same `read_source` tool and the same strict output format, which is the union of all kinds' fields. A kind's own policy goes in its user message as `trusted_review.policy`, beside its evidence, so moving from one kind to another keeps the conversation's cached prefix, and the shared instructions hold no kind's policy: Guardian reads a review's policy with the review it governs. The host drops fields a kind doesn't use. A host registers its own policy for each kind once, in `GuardianModelOptions.policies`, which each review of that kind carries after the core policy; a registered publication policy replaces the core one. Per review, it adds input and a turn limit through `specialize`, and can pick the review's reasoning effort, `low` or `medium` (the default), on the same model. A `policy` returned from `specialize` is deprecated: it still reaches that review's `trusted_review.policy`, after the policy `policies` holds for the kind unless it is the same text. - **Prompt cache.** The instructions travel as developer messages, since top-level instructions cannot hold a cache breakpoint. The first holds the instructions every build shares and ends in a 12-hour explicit breakpoint, so every build under the same host reads them from the cache. The second is the build's own: a session's continuity policy and the build's request. Every request caches implicitly from there (`prompt_cache_options.mode` `implicit`), so each request reads the conversation the last one wrote: a session's earlier reviews, and a review's own earlier calls, with or without a session. A host's `promptCacheKey` goes on every request as `prompt_cache_key`. `makeOpenAIReviewer(...).prewarm()` prepares the shared instructions' cache with a prewarm request that is a session review's own first request up to that breakpoint, for a host to call at startup or deploy. A review's `guardian.usage` splits `cacheWriteTokens` into `cacheWrite30mTokens` and `cacheWrite12hTokens`. Each session request after the conversation's first, a compaction request included, sets `prompt_cache_options.comparison_response_id` to the conversation's latest response, which the session snapshot keeps as `latestResponseId` so a restored session compares too. The provider then reports whether the request reused that response's prefix, and `guardian.usage` lists each call's report in `promptCacheDiagnostics`: its `type`, a known miss `reason`, `cacheMissedTokens` and `comparisonReusableTokens`, enums and counts only. - **Reasoning effort.** Every request keeps the default effort, since changing the request's effort changes the provider's hidden instructions and loses the whole cached prefix. A review at another effort than the conversation's adds a `configuration_update` item before its request, which holds for later responses until another one; a review without a session adds one when it runs at another effort than the default. Two updates never stand next to each other. The provider's automatic compaction does not accept a conversation that holds one, so such a request goes without it, and the session's own compaction re-adds the effort in force after the compaction item. A request that would reach the threshold when the session could not compact first keeps the provider's compaction instead: it goes without its updates and carries the effort in force as its own, and the effort follows the provider's compaction item again as an update. - **The build's request.** The build's own developer message holds the build's request, `trusted_request.intent`: the caller's task as the host received it, which every policy calls trusted intent. It is the same for every review of a build and comes before the conversation on every request, so it stays in the cached prefix, survives a compaction and is not repeated in each review's user message. It comes after the instructions every build shares, so builds still share that prefix. Guardian reads it as context about the task's scope and the authority the caller gave, never as instructions: an action that serves a capability the request states is in scope, and Guardian judges its safety as usual, under the same privacy, write, payment, credential and supplied-value rules. - **Outcomes per kind.** `trusted_review.outcomes` lists the outcomes the host accepts from that review: `allow`, `deny` or `escalate` for an execution, recovery or publication review, `allow_business`, `authentication` or `reword` for a question, `allow`, `clarify`, `reword` or `new_mint` for a task update, and a host-defined kind's own subset. Only execution, recovery and publication policies ask for `allow`, `deny` or `escalate`. The output format stays shared, so its enum still holds every kind's outcomes. - A decision whose outcome is not in the list gets one follow-up round in the same review that names the outcome it refused and the outcomes it may return. For a built-in kind, it also says which of them holds a change the reviewer wants first: a deny or an escalate, as its policy says, for an execution review, a deny for a recovery or publication review, a reword for a question review, and, for a task update review, a clarify when the update needs the caller's confirmation and lacks it or gives it ambiguously, or a reword when it is wrong as written. A private host kind gets none, since its exchange must stay out of readable records. - If the outcome is still not in the list, the review fails with `InvalidOutcome`. Reviewing the same input again would repeat it, so it is never retried as an outage. It is not an outage for the harness either: it starts no outage budget and counts as no outage retry. The first such failure in an attempt comes back as `review_invalid_outcome` without `retryable`, and the minter revises or withdraws what it submitted instead of resubmitting it unchanged; an execution whose live execution has closed goes on through its retained receipt instead. The second ends the attempt incomplete with no host failure, since running it again would repeat it. Its ending is the typed stop `host_unavailable`, the host's and not the task's, and a `review` diagnostic (`reason: review_unavailable`, `code: InvalidOutcome`) names the review kind, so a host never reads it as a missing publication; for a `report_blocked` explanation, the build ends `blocked` with the reason's fixed sentence. The count survives a takeover. A host that became unavailable during the review ends the attempt with its own cause. - Diagnostics name such a failure `code: "InvalidOutcome"`, and its message names the refused outcome when it is one of the shared output format's, never a private host kind's. A publication's `PublicationDecision` keeps the code `ReviewUnavailable`, lists `InvalidOutcome` in `failedChecks` and has the recovery `correct_source`, or `ended` when it ended the attempt, never `retry`. - A decision that doesn't decode, such as one missing a field or with a finding outside the index, gets one follow-up round in the same review that names the decode error and, in a publication review, lists the indexed paths. A private host kind gets none. If the answer still doesn't decode, the review fails with `InvalidDecision`, which is not retried as an outage, since the model already saw why. A review that got no correction, a private host kind's or one whose reviewer sends no follow-up, is reviewed once more, since a fresh sample may decode. A host's own `decodePublication` must be pure: it may run more than once per output. - **Host-defined kinds.** `reviewHostKind(pending, request, readSource?)` runs a review of a kind the host defines, as one more turn of the same conversation, with the same instructions, tool and output format. - The `request` gives the kind's name, its policy (sent as `trusted_review.policy`, after the policy `policies.hostKinds` registers for the kind when that is other text), its evidence (sent as `host_review`) and the subset of the shared outcomes it may return. - It can also give the `labels` its decision may carry, `outcomeLabels` to tie some of those labels to an outcome, and a `private` flag. - A decision whose label its outcome does not carry gets one follow-up round in the same review that names the labels its outcome carries, as for an outcome. A private kind gets none. If the label still doesn't fit, the review fails with `InvalidDecision`. - It returns `{ outcome, rationale, label? }`. The host refuses any other outcome or label. - A private kind's evidence, transcript and rationale reach no readable diagnostic. Later reviews' readable model records show its exchange only as a placeholder, including after a compaction. - **Action label.** An execution review's decision carries `action`: `read`, `write` or `authentication`, by what the code does to the site's persistent state, not by HTTP method or button names. A write includes intermediate changes, such as adding to a cart or saving a draft. - An allow without a label is a decision that doesn't decode, and gets that follow-up round. - An allowed `write` on a step without `PendingExecution.writeAuthority` becomes a denial, as out of authority. The local host gives write authority only to a write build's live `act` steps. - The host passes the allow and its label to the dispatch fence, `beforeDispatch(allowed)`, and the execution's receipt carries it as `review.action`. - **Required read.** `PendingExecution.entrypoint` is the agent's own file. A host that runs it through a wrapper describes the wrapper in `hostWrapper`; Guardian may read it but needn't. An execution review puts the entrypoint's first chunk in its request, so a typical review takes one model call. - If the host's read fails, the source is left out and Guardian reads it itself. - If only keeping the screened copy fails, the source stays in the request and the gap is recorded as `guardian.source_failed`. - An allow counts only while the entrypoint is in view, so after a compaction during the review, Guardian must read it again. `./x` and `x` name the same file. - If an allow still lacks the read after two follow-up rounds in the same review, the review fails with `EntrypointNotRead`. That failure is a verdict, so it is never retried. - **Resume note.** A host that restores a session for a resumed build may pass `GuardianSessionOptions.resumeNote`. The session adds it once, as a message before its next request, and keeps it in the history like any other item. Absent, a restored session continues as before. - **Compaction.** A session compacts its conversation once a request would reach 240,000 input tokens. It sends the last request again, the same in every byte, with a compaction trigger after its input, so the compaction reads the prefix that request cached. The returned compaction item replaces that request's input, and the last response's output and the new items follow it. The provider's own compaction at the same threshold stays as the backstop, for a request the session could not compact first, such as the first one after a takeover; it runs inside the request and reads none of the cached prefix. A request whose conversation holds a configuration update goes without the backstop while it stays below the threshold. One that reaches it uncompacted, after a failed compaction or as a restored session's first request, whose size the session estimates from its content, keeps the backstop and carries its effort as its own instead (see Reasoning effort). Both developer messages stay ahead of the compaction item. The review's `guardian.usage` counts the compaction call. - **Incremental review.** Before an execution review, the host compares each executed source Guardian already read in this conversation since its last compaction with the current bytes. It lists the identical ones in `trusted_review.unchangedSources`, and Guardian needn't read them again. The entrypoint is still always included. - **Publication findings.** A publication finding quotes the text at fault, and the host finds its byte range in the file Guardian read, so Guardian never rereads a file to count offsets. A quote the file doesn't hold puts the finding on the whole file. A finding path names the indexed file it matches exactly, then the file its part before a `#`, such as a JSON pointer, matches exactly. A path with a leading directory missing or added names the one indexed file that shares the most trailing path segments with it, its file name at least, a current file before a historical one. A path that names no file, or ties between files, gets the decode follow-up. - **Several files per call.** `read_source` reads one file, or up to 10 listed in `paths`, each with its own offset and `match`. Each file is its own host read and counts as Guardian's read of it, and a file that fails fails alone. Once a call's results reach 200,000 characters, its remaining files come back `not_read`. A call the host cannot read, such as one naming more than 10 files, reads nothing and tells Guardian what to change. The shared instructions ask Guardian to request the files it already knows it needs in as few calls as possible, up to 10 files each. - **Large files.** `read_source` with a `match` returns only the slices of the whole file around each occurrence, with their byte offsets. A publication review queries captures this way instead of reading them whole. - **Bounds.** A review has no deadline: a slow review runs to its decision. Three bounds keep it finite, and none starts it over. - **Turns.** A review gets 12 turns, a core publication review 32, or the host's own limit through `specialize`. A review that uses them gets one more turn that offers no source read, with a note to decide from the evidence already in view, and returns that decision. If it still returns none, it fails `TurnLimitExceeded`. - **Each model call.** A call with no response for five minutes is aborted and sent again with the same input, at most twice (`guardianModelCallBound`, or the reviewer's own `modelCallBound`). The review's reads and earlier calls stand, and each such retry emits `guardian.model_call_retried`. A call that stalls every time fails the review `ModelCallStalled`. The bound also covers a call in which the provider compacts the conversation. - **Outages.** A host's `ReviewRetry`, such as `guardianOutageRetry`, runs a new attempt after a provider error or a required source read that failed. Its `budget` counts the outage from the first failure, not the review before it, so a long review that then meets a provider error is still retried. `TurnLimitExceeded`, `ModelCallStalled` and `InputTooLarge`, a request the provider refused as over the model's context window, are not outages, so they are never retried. `InvalidDecision` is retried once, and only when its review got no decode follow-up. - **Diagnostics.** Each review emits `guardian.usage` with its model calls, its input, cached, cache-write, output and reasoning token counts, and the reasoning effort it ran at. Model diagnostics are reported with or without a session. The wait for a session is emitted as the `guardian.session_wait` interval, and waits between outage retries happen outside the session. - Each attempt is one review with its own ID. Its `guardian.started` and its closing `guardian.completed` or `guardian.failed` carry a `timing` with the attempt number, which counts outage retries from 1, and the interval as `performance.now()` offsets. The started record adds that attempt's session permit wait, and the closing record counts any `followUpRounds`. Follow-up rounds for a refused outcome, a decision that doesn't decode or a skipped entrypoint read stay inside one attempt. - `guardian.review_retried` carries the scheduled backoff interval and the failed attempt's review ID. A failed source read, the host's own entrypoint read included, records its duration on `guardian.source_failed`. - A private host kind keeps no transcript, so its finite model and tool timing goes to `observeModelTrace` instead. ### Task updates - The minter changes its task's settings with `mint_update` once the caller confirms the change: input values, a requirement, constraint or prerequisite (added, dropped or revised), the purpose, a read becoming a write, the target site or the login. It asks with `request_input` first unless the request already settles the change, and names the answered questions in `confirmedBy`. The caller's pick of an option the minter wrote confirms what that option says, as do the caller's own words. - `recommend` is the minter's own judgment: `update` for the same task and workflow, or `new_mint`, with a `suggestedRequest`, for a different task or another product's workflow. A changed site origin alone decides neither. - Guardian reviews the update as an `update` review (`reviewTaskUpdate`), against the effective task and the caller's recorded answers. It allows it, asks for clarification, asks for a reword, or finds that the change belongs in a new build. - Only the minter's own `new_mint` recommendation, which needs a suggested request, ends the build. When Guardian finds that a `recommend: "update"` belongs in a new build, the result is Guardian's `clarification_required`, and the minter asks the caller to confirm the change. When Guardian finds that again before an update applies, whatever the retry's wording, the result is `reword`: the caller was already asked, so the minter revises the change rather than asking them again. Guardian still reviews every update. - Results: `updated`; `clarification_required` (from Guardian, or from the host when, for example, the new site needs a login the caller hasn't given); `reword`, which never ends the build; `new_mint_recommended`, which ends the build `blocked` with the summary and suggested request; `review_unavailable`, under the usual review outage budget; `review_invalid_outcome` when the review returned an outcome its kind does not accept, which the minter revises or withdraws rather than resubmits unchanged; `update_refused` when this build can't take the change, such as a site change while a write session is open, once the build can't run another live example, or on a host that can't move builds (`site_move_unsupported`); and `update_invalid` for a request that doesn't decode, a `new_mint` recommendation without a suggested request, or one whose suggested request screening made longer than 500 characters (`suggested_request_too_long`), which the minter shortens before any review. Nothing changes on any result but `updated`. A site, login or effect change widens what the build may do, so it always needs an answer in `confirmedBy`. After a site change, `finish_build` publishes only an execution that ran on the new site. A read build that already ran a live read example may still become a write; the write session runs its own example, and only what it did can be published. - In maintenance, a repair changes only the requirement, the purpose or one output field of the published tool's contract: `{ setting: "output", field, change, text, reason? }`. - `add` adds a field and `tighten` makes one stricter: optional or nullable to required, or a narrower type. A repair uses them to fix a gap it finds, such as a fact the page shows that the output lacks, or a needed value the schema lets be null. Callers keep everything they already received, so an update whose every change is an `add` or `tighten` needs nobody's confirmation, even when the host fact `maintenance: { confirmer }` that Guardian reviews it with is `none`. A tightened field can fail a run where the old one returned null, which is the point: a missing needed value is a failure, not an answer. - `optional`, `nullable` and `widen` loosen a field the site no longer shows as before, or one the owner asked to change. `remove` drops it and has the highest bar: it needs `reason`, why the field must go from what the site shows now, or the host refuses it as `output_removal_reason` before review, and Guardian judges that reason explicitly against the captures. - Any other setting is refused as `maintenance_setting`, an output change outside maintenance as `output_outside_maintenance`, both before review. The owner confirms every other maintenance update: the host's `MintDependencies.taskUpdateConfirmer` answers whether the person answering the repair's questions owns the tool, and without it, or when it answers `none`, the update is refused as `owner_unavailable`. Guardian reviews an owner's update with the host fact `maintenance: { confirmer: "owner" }`. A change to an output field an earlier accepted update of the same build added keeps the registered contract too, since the contract never had that field, so a repair nobody can confirm may remove a field it added for a part of its request it leaves out. The `owner_unavailable` refusal prefers leaving out the part of the request that needs the change, removing any code or field added only for it, publishing the rest and naming the left-out part in coverage, and ends blocked only when no part can be published. - When the maintenance is the owner's own request to change the tool, the confirmer answers `owner_request`. An output change that request plainly names, such as a new shape for a field or fields left out of a smaller output the caller opts into, is then confirmed by the request itself: the update may cite no answer, and Guardian needs no evidence that the site stopped showing the value, since the owner chose the change. Guardian returns `clarify` when the request leaves the change open or is ambiguous, and its question review allows a question asking the owner to confirm such a change. The local host runs no repairs and has no confirmer. - A host that publishes repairs refuses one that loosens the registered output contract with no applied output change for that field and change, as the publication reason `output_obligation_weakened` with `weakenedOutputs`, each `{ field, change }`. A change to a field an earlier applied update added exempts nothing: the field owes nothing when the registered contract lacks it, and a registered field the build "added" keeps the owner's confirmation. The harness has no registered output schema, so this host check is what stops a registered field from being added and then removed unconfirmed. `finish_build` hands that to the minter as data, neither a review nor an outage: it keeps the field and fixes the extraction, then runs the example again (a write repair publishes the same execution and never runs its write again), or proposes the output change through `mint_update` and, once it is updated, publishes the same example again. - The host applies an allowed update through `MintDependencies.applyTaskUpdate`. All or nothing, it stores the harness checkpoint it is given, with the update applied, together with its own bindings, rebinds everything that depends on the site for a site change, reruns intake screening and the duplicate check where it has them, and resolves the login. It may answer `clarification_required` or `refused` instead. A host that can't move a build to another site sets `MintDependencies.siteMoveUnsupported`: the harness then refuses a site change as `site_move_unsupported` when the minter proposes it, before the host asks for the caller's confirmation or Guardian reviews it, and points the minter to a new build. Every update review reads the host fact `siteMoveUnsupported: true`, so Guardian takes a change of site as a reason for a new build. A takeover restores the whole update or none of it, and the same `mint_update` again is answered `updated` without another review. The local host has no intake screen, duplicate check, saved logins or checkpoint store: it rebinds the site, the input and the effect, leaves the earlier site's sign-in origins and sign-ins behind, and a sign-in on the new site asks you as any sign-in does. - An option the minter wrote is the caller's confirmation of what it says once they pick it or type it back. A link in it names where the caller's work lives (`trusted_authority.ownerNamedOrigins`), as the caller's own words do. - Every later review reads the effective task: the original intent and `trusted_authority.taskUpdates`, with the rebound `allowedOrigins`. Each recorded execution keeps the task revision it ran under (`taskRevision`). No update removes the requested action itself, allows repeating a write that may have committed, or overturns a Guardian decision. ### Publication review - `finish_build` runs one publication review after its own checks. Guardian reads the files that would ship, the public definition the host writes from the build's name, description and schemas, and the evidence they are judged against. - For a read, the evidence is the example's output as compact JSON, with the build's secrets masked, and the source the example ran, under `executed/`. Each field of the record and each record of a top-level list is on its own line, so a later round diffs a rerun of the same output in a few hundred bytes. A changed record's diff also carries the records beside it. Past 96 KiB the output keeps the first whole records of its largest list, or else a prefix of its text. `publication/output-digest.json` summarizes every field of each list in the output across all its records. - For a write, it is each act step's source under `publication/session/`, in order, and the output of the step the build names. - `trusted_publication` indexes those files: whether each ships, whether it is current and who wrote it, the host or the minter. Its `references` list, by byte length, the files the review may read beside its evidence, such as the minter's skills. A host that repairs a published tool may add `publishedInputs`, the inputs the published revision declares, each `{ name, description? }`, from its own record of that revision. The local host keeps no published revisions and adds none. Its `inline` carries the small files the review reads first, so it needs no turn for them: the public definition, the entrypoint, the output digest and a read's example output, each up to 16 KiB, exactly as `read_source` returns it, a JSON file parsed. A file whose read fails or takes more than one chunk is left for Guardian to read, and the review's notes say which files need no read. A publication review gets 32 turns, then the one final turn that must decide. - Input feedback, such as an account's own number listed as an enum member or a write's add-on the script settles by itself, goes back to the minter, which gets two rounds to fix it. An `exampleInput` key that the input schema doesn't list comes back the same way, and so does a top-level key of the caller's own input that it doesn't list, which the run would otherwise drop. An output field a task update declared that the output schema lacks comes back as a source correction (`schema_mismatch`). Each says to support the name or end the build blocked, never to drop it. The request's prose declares no names the host checks; the publication review judges it. A host that runs its own review gets the same check from `missingRequestedNames`, `requestedOutputFields` and `requestedNamesFeedback` in `pomerado/core/mint/publication-review`. A choice the request or the owner's answer settled as how the tool behaves, such as a sort order, is the tool's default: published as an optional input whose schema `default` is the answered value (`defaultedInput` in `pomerado/runtime`), and a required input, a run-time question or a missing default for it is a publication finding. If the feedback remains after that, the build ends unpublished with Guardian's categories and rationale, and `pomerado mint` exits 1. An input narrowed to the example's value is not feedback: it blocks publication like any other finding. - Any other denial goes back to the minter with Guardian's reason and findings. Each finding carries Guardian's explanation of what is wrong, the evidence and the fix, and its file as the minter's workspace names it; the minter fixes every finding and calls `finish_build` again. A finding whose byte range runs past its file or is empty is fitted to the file, so the denial still reaches the minter. - A publication submitted again in the same build is reviewed as a delta of the round before it. Once a publication review completes, Guardian keeps its decision, each indexed file's screened text digest, the text of files up to 128 KiB (512 KiB a round), and which files a review read whole. It keeps the round in its session beside the conversation, so the record outlasts a compaction that folds the earlier request away and a takeover that resumes the saved session. The session stores the round loosely and decodes it when used: a round this release cannot read, such as one with a finding category a later release added, is dropped, and the next round is reviewed as a first one. A failed review leaves the previous round in place. A Guardian without a session keeps the round in memory. - Every publication review reads every indexed file once through the host's reader, up to 4 MiB each and four at a time, since the index carries no digest. These reads are not Guardian's own. - The next round's request adds `publication_delta`: `priorReview`, that round's outcome, reason, rationale and findings, each with its byte range and the quoted text from the earlier file; `files`, every indexed file as `unchanged` (by digest, with no content, and read whole by a review in this build while it had that text), `unchanged_unread` (by digest, but no review read that text whole; a `read_source` search, which shows only slices around each match, never counts as reading it), `changed` (a unified diff with three lines of context, or the whole file when that is shorter or the earlier text was not kept), `added` (its content), `removed` or `not_compared` (a file the host could not read whole within 4 MiB in this round or the last), with any change to its index entry; and `changedContext`, the other request fields that changed, such as `trusted_authority.answeredQuestions`, `submitted_call.entrypoint` or `untrusted_step_results`. A field a host adds through `specialize` is not compared. A diff or content past 32 KiB, or past 96 KiB across the request, is left out, and Guardian reads that file. - The review's policy, the core one or a host's own, is followed by Guardian's `publicationDeltaPolicy`: check each prior finding against the change, review what changed for new problems, judge `unchanged_unread` and `not_compared` files as in a first review, and leave `unchanged` files alone unless a change makes them relevant. Every file stays readable, and the decision and its outcomes are the same as a first round's. A first round's request is unchanged. - `guardian.started` for a later round counts its files by state under `delta`. - The publication policy blocks these output and input faults in every tool: an output set to a constant where the page shows the value, an input the code never applies (or a search filter the tool leaves out unsaid), an input claimed applied without reading the site's committed state, a supplied filter, sort or date the tool can report as not applied while still returning results (it throws `InputNotApplied` instead), or a location, store or other input that tailors the answer whose results hide the value the site applied or present it as the caller's, a throw where an empty list or null was right or a placeholder where a value was needed, Playwright source that builds a URL from caller values off a parameter or route the site was not seen to produce, iterates on URL variants, or opens a caller's URL or one built from caller values without reading the page's identity and every input back (a caller-supplied URL on the tool's site opened unchanged, or a stable identifier route the site uses, is fine with that read-back, and so is a results URL the site's controls produced, refilled from caller values only in the parts two runs showed carry each input, when the tool runs the page's controls once instead whenever the landing or a read-back is off), and a write that does not read the state it changes before and after. It also holds cart tools to running signed in, stating whether quantity adds or sets, saving nothing to the account unasked and never adding items to meet a minimum. Source that turns off a safety default, a preselected option whose only effect is sharing the caller's data with another company, opting into tracking or signing up for marketing, is part of a good tool and never an input finding; a later edit that drops one is a `safety_default` finding with reason `privacy`. Hosts with their own publication policy include these rules from `publicationOutputPolicy`, `publicationCartPolicy`, `publicationSafetyDefaultPolicy` and `publicationFindingFeedback` in `pomerado/core/guardian/publication`. - An input that tailors the answer may have a limited use, such as a postal code that only lists nearby stores, when the caller chose it. Its description starts "By design:", then states how many values it takes and the use, and Guardian counts that use as applied only when `trusted_authority.answeredQuestions` holds the caller's answer to the input's design question, matched by the answer's `id`, `input_use_` with the input's name in lowercase snake_case. On a repair, `trusted_publication.publishedInputs` showing the same input with the same description word for word also backs it, as the caller's earlier answer. A filter, sort or date never gets a limited use. - A caller may settle how the tool behaves on every run, in the build's request where it says so explicitly, in an answer to a question about the tool's behavior or in a task update, such as blank fields returned as empty strings, an output shape or an option fixed for every run. The publication review honours that decision as stated, as an exception to the API-design and output-contract rules (a loosened needed value, lost information, a constant output, an unapplied input, how the output reports an absent value or no results, `input_option`, `example_value` and a cart's quantity semantics), with the field's or input's description saying what the caller chose (`publicationCallerDecisionPolicy`, part of `publicationOutputPolicy`). It never excuses a privacy, credential or security finding, such as a fixed personal or account-specific value, nor the correctness and write-safety rules: a read-back, a restricting filter applied, built URLs, dates, pop-ups, a write's confirmation and reconciliation, authority and claims, or a value the page shows that the code failed to read. An answer that gives one run's value stays one instance of the caller's input. Only the request, an answer or a caller-confirmed task update establishes a decision, never a description or note that claims one. On a write, a decision may have the tool decline or turn off a paid add-on, a paid upsell, a payment method or a delivery address, never accept, keep or choose one for every caller (`publicationPaidChoiceLimit`, in both this rule and the input schema review). - Before the review, publication refuses a sign-in whose login URL holds a value the build was given, and a name, description or site naming that holds one. The refusal names the part, never the value. - A login URL that is one authorization request, such as an identity provider's authorize URL or one carrying `state`, `nonce` or `SAMLRequest`, is asked about once. The first `finish_build` fails `login_url_one_time` with its one-time parameters, and finishing again with the same URL publishes it. Parameters in a hash-routed route query, such as `https://example.com/#/oauth/callback?code_challenge=xyz123`, are detected as well. - A host that compares a publication's input schema with the published tool's may refuse one that changes an input the tool already declares, as the publication reason `input_contract_changed` with `brokenInputs`, each `{ field, change }` with `change` one of `removed`, `required` or `changed`. `finish_build` hands the minter exactly its fixed instruction with that list, neither a review nor an outage: it restores each input to accept at least what the published one does, up to a limit the site itself states, and keeps every new input optional. A read runs the example again with the edited code and publishes that execution, which needs a host that grants `repeatableRead`; a write calls `finish_build` again with the same `executionId`, never running the write again, and the refusal is recorded as `correct_source`. The local host refuses none. - A host that registers its own publication policy in `policies.publication`, or returns one from `specialize` (deprecated), keeps exactly the policy, input and turn limit it sends. The core policy, `trusted_publication` and the 32 turns apply only without one. A host can also decode publication decisions itself with `decodePublication`, and end unresolved input feedback its own way with an `InputFeedbackFallback`. ## The outcome reviewer - Guardian judges each action before it runs and never sees its result. A separate model session, the outcome reviewer, judges afterwards whether each write Guardian labelled actually changed the site: `done`, `not_done` or `unknown`, with a short explanation and the evidence it rests on. - A write's own commit request is `done` evidence when the step that clicked the final commit control lists it in `stateChangingRequests` on the route the session's commit used, with a 2xx or 3xx status and no error after it in its readable response or on the page. A 202, or a 2xx whose body says the work is queued, counts too. A 200 whose body reports an error, as GraphQL can, did not go through. A later stage the site can still refuse needs its own request. The local host lists no requests, so its reviewer relies on confirmations and readbacks. - It runs beside the minter as its own continuing conversation, on GPT-6 Luna, with Guardian's session building blocks: provider compaction, and an interrupted turn closed before the next. Its instructions, the same for every mint, are a developer message with a 12-hour cache breakpoint, and the conversation after them caches implicitly. `makeOpenAIOutcomeReviewer(...).prewarm()` prepares their cache, `OutcomeReviewerModelOptions.promptCacheKey` goes on every request, and each request after the conversation's first compares its prompt cache with the latest response, as Guardian's do. - Its tools only read. It searches and reads the minter's whole history, the turns a compaction replaced in later requests included. It lists and reads the host's records of executions, generated source, screened captures and publication decisions, and reads the original request, the accepted answers and the task's current state. It records an assessment, and it can ask the minter for a readback, which the minter runs through its normal reviewed execution. It has no browser, shell or website access. - It wakes when a write returns, fails or loses its result, when a later live execution may hold a readback, when an answer or an applied `mint_update` changes the remaining work (a refused update does not), and when `finish_build` runs with a write unresolved. - It takes one turn at a time per mint. Events that arrive during a turn are coalesced into the next. Its turns are not minter activity. - A failed turn is retried after a wait, without limit, and never invents an outcome. The newest assessment of a write replaces the older one. - The minter keeps working meanwhile. The only step that waits for the reviewer is one that would run an earlier write's step again: the same entrypoint, or the same source under another name (a digest of the contents of its import closure in `src/`, `explore/`, `test/` and `scratch/`, without the host's SDK). It runs only once the reviewer, caught up without an outage and with no readback it asked for still unanswered, found every such write `not_done`. A host with no outcome reviewer never clears such a step: its refusal says the step stays refused for the rest of the attempt and never to run a copy of it, where a host with one says to run the write's own step again once the review finds it `not_done`. - Guardian's execution review sees the build's earlier writes with the review's newest finding for each (`trusted_execution_context.writes`), and denies a step that would commit the same change again, however its code is written. A host passes them through `OutcomeReviewHost.bindWrites` and `MintReviewHost.writes`. - Guardian's execution review sees the attempt's latest live test batches, and any older one holding a failure that still stands on its source (`trusted_execution_context.liveTests`): each batch's id, entrypoint and source digest, whether that source is unchanged since the batch ran, and each case's verdict and whether it ran cold, so it can confirm from host facts that a suite passed on the source it judges. A host passes them through `LiveTestHost.bindBatches` and `MintReviewHost.liveTests`. - The harness's recovery checkpoint keeps the tracked writes with their entrypoints and digests (`MintHarnessSnapshot.outcomeWrites`), so a takeover tracks them, and refuses their repeats, even when the reviewer's own save failed. A write whose result a takeover recovers is tracked with the entrypoint its call named, from the evidence the host's `recoverTool` returns: a host's execution journal must keep each execution's `review.action`, or a recovered write is not tracked. - Publication never waits. When a published build leaves a write unresolved, the reviewer gets one final turn as the attempt ends, and its answer stands. A refused publication leaves the reviewer on its ordinary turns. The outcome's `writes` lists every write with its newest assessment: `applied`, `not_applied`, or `may_have_applied` when the review found it `unknown` or never settled it. - Every dispatch carries a label. A sign-in screen the local host fills itself passes the host's own `authentication` allow to the dispatch fence. - The local host runs the reviewer and decides whether a write session submitted its write from the labels and assessments. Commit marks and declared confirmations remain the write contract's evidence. - The minter's whole history stays readable. The SDK's `RunState.history`, which a new run segment starts from, drops every item before the latest compaction. So before a segment starts, the minter stores those items in a history archive, and its recovery checkpoint records where the run state starts in the whole history (`historyOffset`). After a takeover, the reviewer reads the earlier items from the archive and the later ones from the restored run state. - A host supplies the reviewer through `MintDependencies.outcomeReview`: the model (`makeOpenAIOutcomeReviewer` from `pomerado/core/mint/outcome-review-openai`), a `save` hook for the reviewer's state beside the minter's run state, a `recordAssessment` journal hook, and optionally a durable `historyArchive` and its own records around the harness's evidence. Without an archive, the harness keeps the history in memory for the attempt, as the local host does. The archive can grow to millions of tokens, so it is never part of a checkpoint. A range the archive could not store stays in memory, is recorded as a `history_archive_gap` diagnostic and is stored again later; a storage failure never ends the build. A range lost with its attempt reads as explicit gap items, so later offsets stay right and the reviewer sees what is missing. The contracts are in `pomerado/core/mint/outcome-review-contracts`. ## The runtime - A saved integration runs as its own MCP stdio server. Its `mcp.mjs` launcher loads the Pomerado installation that minted it and serves the integration's folder, as `pomerado-mcp serve --artifact` does. - The server validates each call's input against the integration's input schema before it runs anything. - A read tool is marked read-only and idempotent, so a client may run it without asking and run several at once. The tool of a write, `provide_input`, `cancel_job` and `mint` are marked as writes, and `get_job` as read-only. - A call whose run failed returns an error result (`isError`) with its job, `structuredContent` included, a write that may have applied as well. - The tool's input carries its own description. A summary of its fields is added only when they aren't listed beside it, as for an input that is a reference. - It serves that schema in one canonical form. A reference is inlined where it can be, and an input that recurses keeps its references, with its definitions at the call schema's root. - A run asks only the questions publication reviewed, which `pomerado.json` keeps, even when there are none. Any other ask fails as `Undeclared` and asks nobody. An integration saved by an earlier release has none recorded, so it asks only what its entrypoint declares as a plain literal. - Each run starts at the site's root, as the integration's example did. The path of the configured URL isn't loaded. An operation that needs a deeper page opens it itself. - `pomerado run` and each served call open a new browser context, so they start with no cookies or storage. A library caller's runs share the browser context of their `createPomerado` scope, and a run doesn't clear it. - The operation's output is validated against the output schema before it is returned. It comes back without secret redaction. A write's output that fails the schema still comes back, as the failed jobs below say. - A write's run accepts a confirm from its `acceptedConfirms` once, at the same step on the same origin with the same message. A message that named the build's own values matches when the same wording names the run's values from the same input fields or URL parts. A value counts only as a whole word or number, of two or more characters when it holds a digit and three or more otherwise. Only a value of four or more characters matches in any letter case. A URL part counts when it holds a digit, except a query value that is an amount such as `50.00`. Any other confirm in a write's run asks the caller one yes or no question. A read run accepts nothing from the record. - Every other popup gets the safe default without a question. A beforeunload is accepted, since leaving a page sends nothing, and an alert, a prompt and a read run's confirm are dismissed. - A confirm whose question's window ends unanswered is dismissed, and the run goes on. If the run then fails, it reports `no_response`, as the failed jobs below say. A cancel at the terminal still stops the step. No confirm is accepted without an answer or a record. - A run reads a popup's origin from the page address the script reports. A frame from another site inside that page counts as the page. - Files never travel as bytes in an input, an output, page code or a page code result. - An input field typed `FileInput` holds a reference, and its JSON Schema carries `format: "file"`. The local host takes a `file:` URL of a file on your machine. The script passes it to `files.place` with the field as data, its label (`{ label: "Receipt" }`) or a selector, never page code. In one page call the host finds that one element, checks that it is an `` in a frame on the tool's site whose `accept` takes the file, sets that same element and reads the choice back. - `files.collect` runs the script's call that starts a download and returns a `FileOutput`, `{ "$file": { id, name, media_type, size, sha256, download_url?, expires_at? } }`, with `format: "file"` in the output schema. Its `media_type` comes from the file's first bytes when they prove a type, else from the server's `Content-Type`, never from its name. - The local host keeps a download for 30 minutes, in this process's temporary directory or under `PomeradoOptions.files.downloads`, and `download_url` is its `file:` URL. A sweep on each keep and every minute deletes expired files, and the temporary directory goes when the process exits. The host can't see a caller read a `file:` URL, so it doesn't delete a file on its first read as a hosted download does. - A file is refused over the caps, 25 MiB each and 10 files or 50 MiB per run by default (`PomeradoOptions.files.limits`), when its first bytes don't match its type, when the input's `accept` doesn't take it, or when it is a program. Those refusals reach the caller as invalid input naming the reason. A local file that grows while it is read is refused. Nothing opens, decompresses or renders a file. - A run may place only a file its input names, and an output may name only a file the run collected. - While minting, the minter sees each of your files as a `{{file.fN}}` handle with its name, type and size, and a download as its metadata and sha256. Source holding a handle is refused before it runs or publishes, as is source that reads a file's contents, and in a build with files source that sets a file input itself, takes a download itself, reads an input's files (`FormData`, `DataTransfer`, a request body) or routes the page's requests. That check reads source text and misses names built at run time, so Guardian's file-handle and off-site rules deny the same reads. - Another host implements `FileHostHook` from `pomerado/core/runtime/file-transfer`, such as with a hosted browser's filesystem API, and `makeRunFiles` adds the checks. - This package has no general privacy screening service. Error messages mask values that look like credentials. - Operations run in child processes. Page code runs in native Playwright workers. - Authored tools import the SDK as `pomerado/runtime`, like a standard library, or through the workspace's `../../runtime/index.js`, which re-exports it. Both give the same modules the host runs. - The local host stages the authored source beside its own `node_modules/pomerado`, which exports only `./runtime` and re-exports the running package's runtime. So `pomerado/runtime` resolves the same way from a source checkout and from any install layout, and an import of the package's internal modules, such as `pomerado/core/runtime/operation`, fails to resolve. - Guardian's `read_source` reads `pomerado/runtime` as the module it names, and every module that one loads at its own path, such as `runtime/operation.js`. The minter's workspace holds the same files at the same paths. - A path under an SDK folder that names no SDK file, such as a guessed `runtime/form-controls.js`, reads as a `host_sdk_reference` naming the runtime entry, `pomerado/browser/index.js`. A missing authored file still fails. A failed read of an SDK file the host provides is the host's gap: Guardian's deny or escalate after one fails the review as that read's `SourceUnavailable`, an outage, never a refusal (`refusalsOnHostGapsFail`). The local host always holds its SDK files, and its secret check, which refuses a file holding a value you gave on every read, is no such gap, so a refusal after it stands. - Authored code, offline commands and page-code workers run with your user account's file and network access. Guardian review and file checks are not an operating system sandbox. Clearing a worker's `process.env` hides environment variables from that API but doesn't isolate host credentials. Generated code keeps the Kernel SDK's browser call shape, so the same source also runs on a host that uses Kernel. ```js const response = await kernel.browsers.playwright.execute(sessionId, { code: "return await page.title();", timeout_sec: 30, }); ``` - Here `kernel` is a compatibility object that forwards calls to native Playwright over local process IPC. It doesn't load the Kernel SDK or call Kernel. - The native executor launches Chromium, attaches to a Playwright browser server (`endpoint`), or attaches over CDP to the own page of a running Chromium a host owns (`cdp`). It drives the browser with Playwright, or with Patchright when asked (`library`). Hosts compose it through `pomerado/core/execution/playwright-execute` and `pomerado/core/runtime/kernel-compatibility`. The local host doesn't attach over CDP, since its runs close other tabs and reset site data. - A script's result may take up to 1 MiB as JSON, and a larger one fails the call. A host whose scripts read large page state raises that with `resultLimitBytes`, a whole number of bytes up to 64 MiB; the failure then names the raised limit. Console output keeps its 1 MiB limit, and the local host keeps the 1 MiB result limit. - Narrow credential-keyboard and browser-ownership checks still use Chromium's low-level CDP primitives where required. - The local host doesn't mint HTTP versions, record network traffic or produce `captures/routes.json`, and it has no capture replay. Requests made inside the browser still work. - A list tool returns one page per call. It spreads `listInputFields` (`limit`, 1 to 50, default 20, and `cursor`) and `listOutputFields` (`next_cursor`, `next_cursor_expires_at`, `has_more`, `total_results` and `list_changed`) from `pomerado/runtime` into its schemas. Its script reads its position with `startList` and reads the page with `readList`, giving it the site actions as a reader: `open` reaches where the call starts, `more` reads more of the segment already open, and `next` moves to the next site page, API response or "Load more" step. `readList` asks for exactly the rows the window needs, so a call's `limit` and the site's page size need not match: a window can end inside a site page, span two or more, or end on a short last one. It picks the rows after the previous page with `selectRows`, keying each row by the item's own ID. With the reader's `promoted`, a sponsored copy of an item the rows read show unpromoted, or an earlier page returned, is dropped and never sets `list_changed`, so an item comes back at its organic place, unless its sponsored copy was returned before its organic row was read, when both come back; a promoted row whose item the list shows only promoted stays in place. It writes the list fields with `finishList`, which leaves an unsigned draft of the next position in `next_cursor`, or null with `next_cursor_unavailable` saying why when the list has more. The position names the segment that holds the last returned row, by the site's own link and page number, the API's token, or the fewest steps that load it, and the offset after that row there; a site link that carries a session or a credential is left out, so the next call reaches that page by its number. A later page that returned no rows and stayed at the position it started from, such as a "Load more" list that still stalls, ends with `no_progress` rather than handing back the same cursor. No browser stays open between calls: the next call is a fresh run, started and signed in as the first was, that rebuilds the position. Most list tools give `readList` the runtime's pager as their reader (`pagedListReader`, with `paginationCode` in each Kernel call): the pager finds the list's page numbers, Next, "Load more" control or scroll near its rows, steps it, waits for each step by what changed and reads the rows, and a tool writes only its selectors and how its list starts. - The host signs the draft into the cursor the caller sees with `sealListOutput`, and checks a cursor the caller sends back with `admitListCursor` before the run signs in or loads a page, both from `pomerado/core/runtime/list-cursor`. The cursor is HMAC-SHA256 under the host's key (`ListCursorKeys`, a current key and previous ones for rotation), bound to the tool, the inputs other than `limit`, `cursor` and `include`, the caller the host names as `subject`, a ten-minute lifetime (`listCursorTtlMs`), and a site link on the tool's site. - The script gets the checked position on a host channel beside its input (`list` on `runLocalOperation`, `executeKernelOperation` and `executeOperation`), never in the input, and never sees the key. A run whose host passes no `list` refuses any cursor that would continue a runtime list and returns no next cursor, so a host that skips the check fails closed. Tools never make or read a cursor of their own: `admitListCursor` refuses any non-empty cursor its host did not sign as `malformed`, before the run starts, and `sealListOutput` drops a next cursor that is not a runtime draft, so the caller never gets a cursor the host would refuse. Its `next_cursor` becomes null, `has_more` stays as the tool gave it, and while the list has more, `next_cursor_unavailable` is `not_a_draft`. An empty, null or absent cursor starts at the first page. - A cursor's body is signed, not encrypted. Whoever holds it can read the site's own link, token or scope for the next page; the tool, inputs and caller appear only as keyed digests. A site link that redirects off the site is the run's navigation policy's to stop, as with any page the script opens. - A refused cursor ends the run as `input_rejected` with `details.field: "cursor"` and `details.kind`, one of `malformed`, `altered`, `unknown_key`, `version`, `other_tool`, `other_account`, `inputs_changed`, `expired` and `off_site` from the host, or `mechanism_changed` and `site_expired` from the script. The local host signs with `PomeradoOptions.listCursors.keys`, or a random key per process. - An HTTP version is `defineOperation`'s object form whose only network capability is `SiteHttp`, usually written with `defineHttpOperation`, `readText` and `readJson`. All of these are in `pomerado/runtime`, with `HttpFailure` and the offline-test services `SavedHttpFixtures`, `SavedCaptureEvidence` and `parseSavedHttp`. - The local child runs one when its caller passes `runLocalOperation` an `HttpTransport` as `http`. Each request goes to that transport through the host and marks the run as possibly dispatched just before it is sent. Without a transport, an HTTP version fails before it sends anything. The local MCP and terminal pass none. - `readText`, `readJson` and `requestPastChallenge` act on a bot-protection challenge page only when the host recognizes one (`isBotChallenge`). The local host recognizes none, so every answer is the site's. - `readEmbeddedJson`, `embeddedJson` and `parseHtml` parse a page's embedded JSON (state blocks, JSON attributes and state a script assigns) and markup, on `htmlparser2` and `css-select`. When a curl answer to a safe read lacks the embedded block, `readEmbeddedJson` reads it once more over the page's fetch where the transport offers `page-environment`, never from an offline replay. - `SiteHttp` checks each request before sending it. A refusal fails `invalid_request`, or `unsupported_capability` for a capability the transport lacks, with dispatch `not_sent` and a `refusal` naming the rule it broke, such as `body_on_get_or_head` or `header_name_invalid`. `maxResponseBytes` alone sets a response limit, at most 8 MiB. ## Jobs - A tool call that starts work creates a job in the server's memory and returns its ID. - `get_job` waits for a change, 20 seconds by default and at most 30. It never starts work again. - A call to a generated integration waits 20 seconds. It returns the output if the run finished, and a job ID if not. - A pending question expires after 10 minutes. - The server runs one job at a time and keeps at most 32 job records. Finished records expire after 15 minutes. - Each job owns a fresh browser context and closes it when the job ends. A minted integration doesn't inherit the mint's signed-in session. - A build that signed in saves its sign-in screens and check, with no value, in `auth-fill.json`. Each call replays them before the tool runs. - It first checks without values, and asks nothing when the session already shows the account. A served call starts in a fresh browser context, so it asks. - Otherwise it asks for the login, and for any code, date of birth, ZIP code or security answer a screen needs. It keeps them in memory for that call only. - A code screen the build never recorded gets the same code question. Once the recorded screens ran with no code sent and the account doesn't show, the host looks once for a one-time-code field and its form's submit, on a recorded screen's origin. It doesn't look while any field of the screen it last submitted still shows, since that screen is the site's answer to it. A field marked `autocomplete="one-time-code"` always counts. On a page with none, a field counts when its name or id says otp, totp, 2fa or mfa, or says code or verification on a box that takes 4 to 8 digits. One named for another value, such as a password, voucher, CAPTCHA, recovery code or card, doesn't. - The replay never opens the recipe's account page to check the session while it waits for a screen. Only the host's last check may open it, after the marker wait, so a write whose landing page shows no marker waits that out first. - A library caller's run on a browser its scope already typed a sign-in into, such as a run after the build in the same scope, first checks the recipe's account page when it has one. It waits up to 10 seconds for a marker drawn late, and types nothing when the marker shows there. A recipe with a workspace relation always replays. - The check without values holds a sign-in screen it found for 1.5 seconds before it asks for the login, since a site's login page may show a signed-in browser its form a moment before it sends the browser on. A fill whose screen went before anything was sent waits once more for the page, which may show the account. - A value counts as sent once the host clicked the screen's submit after filling it, or the page sent the screen itself. A click whose wait for the next page outlasts its 5 s, as one through a slow chain of redirects does, still counts once a request for the page's main frame went out, whatever the site answered, and the host then waits for that page as after any click. A click that runs out of time with no such request sent nothing (`submit_failed`). - A rejected username or password is asked again at most twice per field, with the tries left, and a rejected value is never sent again. Those tries are counted across the call's sign-ins until one verifies. The site rejects it by showing the recipe's recorded error marker. A login screen that comes back with no such marker fails the call, since the tool's sign-in no longer matches the site. - The same password goes out at most twice per call. A screen that asks for it again is a rejection, unless the build recorded that screen asking for it again beside a code (a version 5 recipe's `refills`), as a site that wants the password re-entered with a one-time code does: there the host enters it once more, with the code, while no code went out yet. - A later screen whose path holds each account's own segment, such as `/team//password`, may record that segment as `*` (a version 7 recipe's `anySegments`), keeping at least one other segment literal, on the site's own origin or the login's workspace host, never on the first screen or another site's. Runs never open a recorded screen. They only recognize it, so a page is that screen on the same origin with as many segments, any one non-empty segment where the recipe writes `*` and every other segment the same. The marker's paths never hold `*`. The local host doesn't record such segments when it builds a tool. - A recorded approval, where you approve the sign-in on another device or by an emailed link, is asked about once per sign-in. Its screen is the recorded page, or any other page on that page's origin once the screen before it was submitted, since a site's approval page often holds a code of its own in its path and a recording may have saved the page shown after you approved. Such a page is never one of the recorded screens or the entry page, and never shows a password field or the login form the replay just submitted. A page on any other origin is never the approval's screen. A page that already shows the marker signed in with no approval: the replay asks nothing there, and the sign-in's own check verifies it. - A screen a cookie or consent notice covered records the notice's own close, reject or accept control (a version 6 recipe's `dismiss`). The host clicks it before it types, when the page shows it, and when a notice drawn later catches the submit's click, clicks it then and the submit once more, typing nothing again. A notice control that sits in the sign-in form, or would submit a form, is clicked only when that notice covers one of the screen's controls: the host empties the screen's fields first, cancels the click's submission and any submit event while it runs, and types the fields again afterwards, so nothing typed goes out with it. A click another element caught sent nothing (`submit_blocked`). - A screen whose submit another element covers, such as a fixed footer, is pressed from the keyboard: when the host's click is caught, it focuses that same submit and presses Enter, only once focus landed on it, and never clicks the element over it. The recipe records that screen's submit so (a version 8 recipe's `submitVia`), and runs press it the same way first, falling back to the click when focus lands elsewhere. - A read's sign-in that sent its values, saw no rejection and ends on a page that can't tell counts as signed in. That page shows no marker, no password field of the sign-in and none of its screens, as an account picker does. So does one that types nothing because the sign-in page sends a signed-in browser on to such a page. Once the login went out, such a page also ends the wait for a recorded screen the site skips this time, such as a code screen. The tool goes on from there, and makes the pick itself. A page is one of the screens only on the screen's own origin and path, a trailing slash on a path other than `/` aside, so the login page reached with or without one still reads as the login page. - A read's check without values may also count as signed in on the login page itself, when that page stays unchanged for 1.5 seconds showing no screen, marker or password field. It never does in a browser that started empty, or right after a check found the page signed out. The local host sets `startedEmpty` on every sign-in, so this never happens on a local call. - A write's sign-in counts only once its marker shows, since nothing checks the session again before the write commits. A write call that would end on such a page fails its sign-in instead. So does a call that names no effect, as `pomerado run` does. - When the tool's `ensureSignedIn` finds the page signed out in the middle of the call, by the same evidence as after a reset, the host signs in again on the same page with the login it asked for, at most 3 times per call. The call's deadline pauses meanwhile. Past that, or when that sign-in fails, the call fails as below. - A recipe the host can't read, or a sign-in that fails, stops the call before the tool runs. Its job's error then carries no warning that a website action may have taken effect. - A version 4 recipe may say that the sign-in moves each account to its own workspace host on the site's registrable domain (`workspace`), with workspace screens recorded by path only. A workspace host is a host name on the site's own port, so a site on the default port has no workspace host with an explicit port. A run types on a workspace host only after it sent the login's identifier on the shared sign-in host, on that screen's own recorded origin, and the site itself moved there, and it learns at most one such host per sign-in. A workspace host that shows before that, another one than the login's pinned host included, or a second one after the host it learned, is refused as `workspace_unverified`: the host clears the site's own cookies and storage, never another site's, and starts again once, then fails the call. The workspace the sign-in reached is where the account check opens and where the tool runs, for that call only, since the local host keeps no login between calls. A sign-in pins a workspace host to the login only when the login's own sign-in explains it: the pinned host, the host it learned after the identifier, or a host the site moved to after the password or code went out on the shared host. A session that already shows a workspace host, with nothing sent, is checked there but pins nothing, so the local host runs the tool on the site's own origin. A tool whose recipe has a workspace relation never opens a login URL whose path, query or fragment names another address, such as `?next=https://...`, a redirect path like `/r/evil.example.com/`, or a host on the site's domain, even percent-encoded, written in fullwidth or other compatibility forms (read as plain text after NFKC) or with another full stop such as U+3002 (read as "."); the URL's own host is never read, and the call fails before the page opens. It doesn't record workspace relations when it builds a tool yet. - A run trusts `auth-fill.json` as it trusts `src/`, and edits to either aren't reviewed. An edited recipe still sends values only to the site and its sign-in origins, the ones in `deployment.json` and the ones `pomerado.json` saved with the sign-in. There it can pick a form that sends a value in the page address, as a form that submits with GET does, where the site's logs may keep it. - A write tool takes an optional `idempotency_key`. A call that repeats the key and input rejoins the first job and acts on nothing, even while that job still runs. The same key with other input is refused, and nothing runs. - A served integration keeps each keyed job's record in its folder's `.jobs` for a day. The record holds the key, a digest of the input, the job ID, its status, and a failed run's outcome and commit marks. It never holds the input or the output. - Two servers on one integration folder share those records, so one key starts one job between them. - `.jobs` is for servers on one machine. A record names the process that runs its job, and a server on another machine or in another container can't tell whether that process still runs. - Restarting the server stops running jobs and keeps saved integrations and keyed job records. After a restart, `get_job` and a repeated call find a keyed job's status and a failed run's outcome, but not its output. A keyed job the restart stopped reads as failed and is never run again. - A failed job is never replayed. - A failed run's job names its `code`, `write_status`, `possible_commit` and `retry` class. Its `error` says the same in one sentence. - A write that returned without recording its confirmation fails as `outcome_unknown` with `may_have_applied`. Its job keeps the script's output, unconfirmed. - A write's output that fails the output schema still comes back, since the write already happened: as the result of a confirmed write or one that sent nothing, and as the unconfirmed output of an `outcome_unknown` write. One whose declared commit steps were never entered fails as `invalid_output` with `not_applied`, unless the caller left a popup's question unanswered during the run. A read's output that fails the schema fails as `invalid_output`. - A write may declare refusals in its output schema: a branch that requires `status` `"refused"` and a `reason` from a listed set of stable lowercase codes, such as `over_price_limit`, beside the values the caller needs. Refusal branches may sit in a union of their own inside the output's union, and the schema declares none when another branch could also have `status` `"refused"`, as a free-text `status` could; a `status` that lists other strings, or a string whose `pattern` does not match `refused`, cannot. A declared refusal the script returns before it enters any of its declared commit steps, with no confirmation recorded, completes the run with that output, and the write is `not_applied` with no possible commit. The same refusal returned after a commit step was entered, by a write that declares no commit steps, or in a shape the schema does not declare fails as `outcome_unknown` like any other unconfirmed write. `declaredRefusalReasons`, `declaredRefusalOf` and `returnedRefusal` in `pomerado/core/runtime/run-outcome` decide it for any host. A refusal thrown as `InvalidInput` before the commit steps still reports `input_rejected`. - Publishing a write refuses a composed output schema that lists a `refused` status the host can't read as a declared refusal as `refusal_unreadable`, before review, since a run that returned it would end as `outcome_unknown`. `unreadableRefusal` in `pomerado/core/runtime/run-outcome` decides it, and `checkWriteSession` runs it when the host passes the composed script's `outputSchema`. A republish whose published revision's output schema has an unreadable refusal too is not refused for it. - A failed write whose child process reported none of its declared commit steps entered reports `not_applied`, whatever failed it, unless it already recorded its confirmation. `unenteredWrite` in `pomerado/core/runtime/run-outcome` decides it for any host. - Only that report proves it. A failure the host raised without it, such as its deadline or the child process ending, keeps `may_have_applied`, and so does an error the child sent while a host request was still out. A refused input or login with no commit step entered reports `not_applied` too. - A script that fails on its own unanswered question, or a run that fails after a popup's question went unanswered, reports `no_response`. A script that catches its own unanswered question and then fails another way reports that failure instead. A `no_response` write is never `not_applied`, since the local host keeps no log of what the page sent: it reads `applied` once it recorded its confirmation, `not_attempted` when it sent nothing, and otherwise `may_have_applied`, even with none of its declared commit steps entered. - Any other failure after a browser step ran reports `may_have_applied`, because that step may have changed the website. - A call that can't sign in again, because its sign-ins are spent or the sign-in failed, reports `website_sign_in_unavailable` with the retry class `same_key`. That holds for a read, and for a write that sent nothing (`not_attempted`) or entered none of its declared commit steps (`not_applied`). A call whose journal shows a commit step entered, or commit marks it can't read, never reports it. Any other write keeps the outcome its journal shows, `outcome_unknown` while it may have applied. - A valid value the caller supplied that the page did not apply, such as a location, store, filter, sort or date it ignored or reset, reports `input_not_applied` with the retry class `same_key` only for a read or a site left untouched. A write after a browser step ran keeps `outcome_unknown`, since setting a store signed in can change the account, and a confirmed write keeps `execution_failed`. A record an earlier release kept as `location_not_applied` reports the same. - A read never reports a possible website change. - A write that recorded its confirmation is a result: whatever failed after it, the run reports `applied` with the retry class `never`, and its code says what failed. A script that threw or returned output that failed its schema reports `execution_failed` or `invalid_output`, and one that then failed on an unanswered question, a refused input or a rejected login keeps that code, with `possible_commit` true because the website did change. A browser call after `verified()` reopens the write, so a failure after it reports `may_have_applied`. - Only `possible_commit: true` tells the caller to read the site back before any retry. - `retry` is one of four classes. `never`: don't repeat the call as is. `fix_input`: correct the input or the login, then call again. `new_key`: calling again is a new run, after reading the site back when `possible_commit` is true. `same_key`: the request itself may be repeated. - A repeated `idempotency_key` always answers the job it named, even a failed one. To run a failed call again, call with a new key or none. - A failed mint's job still warns that a website action it already sent may have taken effect. - The integration's folder is reserved before the mint starts, so a name collision can't run the task and then fail to save it. An unpublished mint removes the folder. ## Library and terminal The package also works as a library. A library session can mint and run while it keeps the same signed-in browser context. ```js import { Effect } from "effect"; import { createPomerado, makeTerminalAsker } from "pomerado"; await Effect.runPromise( Effect.scoped( Effect.gen(function* () { const session = yield* createPomerado({ ask: makeTerminalAsker() }); const request = { url: "https://example.com", intent: "Read the main heading", input: {}, effect: "read", }; const result = yield* session.mint(request); if (result.artifact === undefined) throw new Error(result.summary); console.log(yield* session.run(result.artifact, request)); }), ), ); ``` - `makeInputAsker` adapts your own chat callback. The callback receives an `InputRequest` and returns raw answers keyed by question ID. - `makePomeradoMcp` and `makeIntegrationMcp` expose the two local MCP modes as library functions. Their Effect scopes own cleanup. - `minterProvider`, `guardianProvider` and `outcomeReviewerProvider` take Agents SDK `ModelProvider` implementations in place of the default OpenAI provider. - Importing a core module starts no browser, MCP listener or workspace. The package has these entry points. - `pomerado`, `pomerado/runtime` and `pomerado/mcp` serve local sessions, the authored browser runtime and local MCP composition. - `pomerado/runtime` only adds exports. A release never removes or renames one, because published tools import it, and a tool may run on a later release than the one that minted it. - Explicit `pomerado/core/*` subpaths, such as `pomerado/core/mint/harness`, `pomerado/core/guardian/review` and `pomerado/core/runtime/host-execute`, let other hosts compose the library. The export map lists the supported modules. - `pomerado/testing/*` holds reusable test helpers and fixtures. Vitest is an optional peer for helpers that need it. - `submitJob` from `pomerado/core/runtime/job-store` is the retry-key rule every host shares. A host passes its own `JobStore`, and runs `describeJobStoreContract` from `pomerado/testing/job-store-contract` to check that store. - `getAuthoringDirectory` and `getGuardianPolicyPath` from `pomerado/assets` return the installed prompt and policy paths. - `getRuntimeSources` from `pomerado/assets` returns every module `pomerado/runtime` loads, as `[path, JavaScript text]` with paths from the package's source root, the entry `runtimeSourceEntry` (`browser/index.js`) first. The set follows the SDK's imports. A host places the files under one folder for Guardian's source reads and the minter's workspace, and maps a bare `pomerado/runtime` to the entry. - The build writes the same set as data to `dist/runtime-sources.json` (export `pomerado/runtime-sources.json`): `root`, the folder the paths are relative to from the package root, `entry`, and `modules`, each with its `path` and `text`. A host reads it from a package without running the package's code. A test checks that it matches the set the built package computes. - `pomerado/core/runtime/same-site` is the one place site identity is computed: which site a host belongs to, for Guardian's authorized site, where a sign-in's values may be typed, the tab storage a saved session keeps, a file input's frame, a list cursor's site link, workspace hosts and every other same-site check. A site is a host's registrable domain under the public suffix list, its private section included, so each user's page on a hosting suffix such as github.io is its own site. A host may add its own private suffixes with `configureSiteIdentity({ privateSuffixes })`, once at startup before any site check, in each process (and each bundle with its own copy of the module) that runs site checks: under each, every label directly below the suffix is its own site in the same way, and where several suffixes match the longest wins. An entry that is empty, not a lower-case host name, or a name with no registrable domain of its own such as `com`, `co.uk` or `github.io` throws `InvalidPrivateSuffix` and leaves the identity in force. With no added suffix every result is the public list's alone. `siteOf`, `registrableDomain`, `siteDomain` and `sameSite` read the configured identity; `makeSiteIdentity` returns an identity as a value for a caller that passes it explicitly. Page code has no suffix list, so the host builds its same-site test with `hostOnSiteSource` from the site it computed. Guardian's `allowedSites` entry lists the added suffixes below the site's domain as `addedPrivateSuffixes`, which its policy's rule for hosts the list gives a registrable domain of their own then keeps off the site; without one the entry is unchanged. A script's `siteDomain` is the host's result, so a site under an added suffix reaches scripts as its own domain; a script's own suffix check on a parent domain, and the wait helper's request tracking, still count hosts under an added suffix below it as the site's. Exact origin checks, such as a configured sign-in origin or a payment frame, stay origin checks. - `makeSiteHttp` from `pomerado/core/runtime/site-http-host` builds an HTTP version's `SiteHttp` over a host's `HttpTransport`, with the execution context's deadline, journal and events, and a capture hook. Its optional `isChallenge` is the host's test for a bot-protection challenge page; a result it finds carries `challenge: true`. This package ships no such test. - `loadAuthoringSkills` and `loadWorkspaceGuide` from `pomerado/core/mint/skills` render each named authoring section's standalone text by default. A host that supplies its own text for those sections composes the directory first, then passes its own `render` function to load it. `loadWorkspaceGuide` also adds `reference/publication-review.md`, built from Guardian's policy text, which no `render` touches. Neither adds the list of hosted features. Only `loadStandaloneAuthoring`, the local host's loader, puts it on top of `AGENTS.md`. Both take an optional third argument, `{ searchUrlFastPath: true }`, that renders the search URL fast path: the workspace guide enters at a results URL with filters, sort or a location in it when that mapping is reliable, and the search skill loads that URL before it reads the page's own data. It is off when absent, and the local host leaves it off. - `makeOpenAIReviewer` from `pomerado/core/guardian/openai` takes the host's `GuardianExecutionEnvironment`, the texts that tell Guardian how that host runs code. The local host passes `nativeExecutionEnvironment`. - `dataVendor` says when a read may carry the caller's input to the site's own data vendor on another domain, and what evidence shows the site's page making that call. - `absentProtections` is optional. It names protections other hosts supply that this host lacks, as the policy's last line. - `executeKernelOperation` from `pomerado/core/runtime/kernel-operation-run` runs an operation's script under the execution context's deadline, capture, events and journal, as the local child process does. A host with its own implementation of an operation passes it as the optional `first` runner, which runs in place of the script and gets the script's run as its fallback. - `checkWriteSession` from `pomerado/core/mint/write-session` runs a write session's publication checks. The session sent its write when a step recorded a confirmation, or an act step Guardian labelled a write is `possiblySent` and the outcome review has not found it `not_done`. The local host marks each step its effect journal can't rule out as `possiblySent`. Given the session's recorded `writePath`, it also checks that the session entered, and the composed script declares, every mark of the path. - `replayAutofillRecipe` from `pomerado/core/runtime/autofill-replay` is the run sign-in every host replays a recipe with. A host passes its page hooks (`AutofillHooks` from `pomerado/core/runtime/autofill-state`), its asker and its masking. What it keeps besides the login plugs in through the optional `hosted` set (`HostedSignIn` from `pomerado/core/runtime/autofill-replay-steps`), which the local host leaves out. `configureSignInTelemetry` from `pomerado/core/runtime/sign-in-telemetry` sets where the engine's reports go; unset, they go nowhere. - A build's sign-in follows the rules under `pomerado/core/mint`, which each host runs in its own screen order. They are `autofill-results` (the `AutofillHost` state the rules read and update, their results and notices), `autofill-guards`, `held-rejections`, `autofill-rejected`, `autofill-filled`, `autofill-account-rejected` and `signed-in-marker`. A host keeps that state in its own refs, and may pass an `EventSink` as `diagnostics` to hear each step. The local host's order is `makeSignInRecorder` in `typescript/src/mint/sign-in-recorder.ts`. - `makeCredentialKeyboard` from `pomerado/core/destinations/credential-keyboard` takes an optional `bindingWorld` function that returns the execution context a credential field resolves in. Without it, the field resolves in the page's main world. When it fails, the insertion refuses as `binding_unresolved` and types nothing. - `makeRunDialogDecision` from `pomerado/core/browser/dialogs/expected` decides a run's native dialogs from the tool's `acceptedConfirms`. It takes an `IncidentStore` from `pomerado/core/runtime/incidents` and records each decision it makes on its own there. The local host passes `noIncidents`, which records nothing. - A host may pass `judgeConfirm` to judge a write's confirm that matched no record before the caller is asked, such as one whose wording first appears in that run. It gets the popup as observed, with a `template` naming where each of the run's values in its message came from, and answers `accept` or `ask`. An accepted one is recorded as `dialog_judged_confirm_accepted`. A judgment that fails counts as `ask`, and a confirm with no known step or on a page that isn't https is never judged. The message it gets is the page's own text, unmasked, so a host screens it before it sends it anywhere. The local host judges none, so it asks. - `askDialogCaller` from the same module puts a dialog the policy leaves to the caller as one yes or no question, worded for its type, and turns the answer into the choice. A host passes the message as the caller may see it, and optionally the dialog's own end as `AskBounds`. - `readHints`, `writeHints` and `nestedInput` from `pomerado/core/mcp/schema` give a host's MCP tools the same hints and nested input as the local server. - `scriptAnswers` from `pomerado/core/destinations/card-fill` hands a script its answers with each card answer replaced by a handle the run's `fillCard` names. - `lintOutput` from `pomerado/core/runtime/output-lint` is the one output check every host uses: after a run, at publication and on hosted runs. `outputNotes` from `pomerado/core/mint/output-notes` applies it at publication with the minter's overrides, returns what still blocks and writes `publication/output-notes.json` for the publication review. Advisory `duplicate_entries` also flags an item ID, SKU or link field that repeats within one list. `empty_field` also takes `alsoTested`, the `fieldFill` of the tool's other tested runs, as paths and counts with no value: a field empty in every record of the run and of those runs, at least one of which holds it, is flagged, outside a list even in one record; with no other tested run only the per-list check runs. The harness passes each live test case the other cases' fills on the same source, and gives `publish` every case's fill as `hostEvidence.testedFields`, which the local host passes to `outputNotes`. `lintSchemas` adds advisory checks of the declared schemas there, `loose_text` and `looser_than_input`, which never block. Two advisory checks name a value in another notation than its field promises: `format_mismatch`, a field that declares `format: "date"` holding anything but `YYYY-MM-DD`, or `format: "date-time"` anything but ISO 8601 with an offset, and `case_variant`, one short value written in two letter cases across a list's records, such as `paid` and `Paid`. - `pomerado/testing/confirm-popups-contract` holds a fixture page with eight confirm cases and `confirmPopupContractFailures`, which checks a host's run dialog handling against them. `npx -y -p pomerado pomerado --help` shows the terminal interface for minting and running. Terminal mint keeps its original source-artifact format. Use `pomerado-mcp mint` for generated MCP packaging. ## Other hosts - This repository is the only source for the shared core, the portable tests, the authoring assets and the local MCP adapters. - Another host installs the same core as a pinned library package and calls it directly, through the hook interfaces above. Its own frontend, accounts, storage, providers and authoring text live in its own code. - This package holds the code the local host runs, those hook interfaces, and the signed-in marker checks in `destinations/signed-in-marker.ts`, which a host uses to implement `MintDependencies.checkSignedInMarker`. Code that only another host runs stays in that host. - A host adopts a tested release through an exact dependency pin with locked integrity, and rolls back by restoring its previous pin. Public commits don't update any host. - `package.json` declares `"pomerado": { "hostProtocol": N }`, the version of the interface hosts compose, the `./core/*` exports. A release that removes a `./core/*` export hosts use, or changes one incompatibly, raises N by one. A host reads it as data from the package it installs, without running the package. A package without the field is protocol 1. `pnpm test` checks a removed `./core/*` export against the versions on npm, as [RELEASING.md](RELEASING.md) describes. - Contributors can test a host against a locally built package before a version is published. ## Source layout | Path | Responsibility | | --- | --- | | `typescript/src/mint/` | Shared minter loop, source tools and completion | | `typescript/src/guardian/` | Shared review loop, source inspection and policy | | `typescript/src/runtime/` | Shared operation SDK, schemas, browser call contract, the page each live step starts from, the re-sign-in rules, the JobStore hook, the shared retry-key rule and the local job stores | | `typescript/src/browser/` | Shared browser helpers used by authored operations | | `typescript/src/destinations/` | Shared sign-in inspection, autofill and trusted credential entry | | `typescript/src/inputs/` | Input validation, terminal collection and per-session secrets | | `typescript/src/execution/` | Local workspaces, child processes and native Playwright adapter | | `typescript/src/standalone/` | Local library, terminal and MCP composition | | `typescript/src/hosted-features.ts` | Features another host supplies that the local minter ignores and its Guardian doesn't count on | | `typescript/src/mcp/schema.ts` | Pure schema adapter shared with the production MCP | | `typescript/authoring/` | Shared prompts and examples, with sections a host can replace |