Repository navigation
feat(core): run Code Mode modules through verify and run, with stable outcomes - #387
Merged
Merged
Conversation
nickruigrok
force-pushed
the
chore/pi-agent-core-1-1
branch
from
October 11, 2026 02:41
d1536dc to
b3dd385
Compare
nickruigrok
force-pushed
the
feat/code-mode-module-contract
branch
from
October 11, 2026 02:46
8dfae1d to
17e3dfd
Compare
nickruigrok
force-pushed
the
chore/pi-agent-core-1-1
branch
from
October 11, 2026 03:06
b3dd385 to
df18454
Compare
nickruigrok
force-pushed
the
feat/code-mode-module-contract
branch
from
October 11, 2026 03:06
17e3dfd to
711c4a5
Compare
… outcomes The agent code is a complete module whose default export takes (self, env, ctx): self is null and ctx is the run own context. The harness verifies the module first, so a module that fails to load is told apart from a function that throws. Source over 200000 characters is refused before it loads, input fields besides code are refused, and the wall clock starts before the module loads. Every run ends with a status the host sets: completed, threw, refused, load_failed, cpu_exceeded, crashed, wall_expired or cancelled.
The harness takes every built-in it uses before the code loads, freezes its entrypoint class, and returns every outcome of the code, describing whatever was thrown without ever throwing itself. A call that rejects is then always the runtime, so the host reads cpu_exceeded, wall_expired and crashed only from the runtime own messages, matched whole. A run whose function is gone ends as load_failed.
verify and run now return a single JSON string, put together by hand from primitives, and the logs are kept as the JSON of their lines. No array or object of the harness that a prototype the code changed could reach is ever filled or sent, so the code cannot make a call reject with a message it chose. The host parses and checks the string.
nickruigrok
force-pushed
the
feat/code-mode-module-contract
branch
from
October 11, 2026 03:54
b4ae7c2 to
e41f072
Compare
nickruigrok
marked this pull request as ready for review
October 11, 2026 03:54
Contributor
Author
|
@greptileai review |
5 similar comments
Contributor
Author
|
@greptileai review |
Contributor
Author
|
@greptileai review |
Contributor
Author
|
@greptileai review |
Contributor
Author
|
@greptileai review |
Contributor
Author
|
@greptileai review |
|
The outcome string was capped at eight times the room, but JSON escapes a character to as many as six, so output within both decoded limits could pass the cap and the run was taken as thrown. The cap is now the worst escaped size of the logs and result with their punctuation; the decoded limits are still checked once the string is parsed.
Contributor
Author
|
@greptileai review |
The wall-clock test lowered CODE_RUN_TIMEOUT_MS to a second in the env the Workspace objects share, and never set it back, so every later test in the file ran its code under a one-second limit. A run of 150 API calls took longer than that on a loaded runner and ended wall_expired.
Contributor
Author
|
@greptileai review |
nickruigrok
added a commit
that referenced
this pull request
Oct 11, 2026
…tion (#389) Third PR of the GRA-349 stack, on top of #387. **Security-relevant:** it changes how long the agent's code holds authority. It needs a person's review before merge. ## Threat model (written before the code) Each row is a way stale or stolen authority could act, and what stops it now. | # | Failure or attack | What stops it | Test | |---|---|---|---| | T1 | Code keeps running after its run ended (cancelled, past the wall clock, or detached work after it returned) and calls an API | The lease closes as the run ends; every call is counted and checked against it first (`requireOpenRun`) | existing cancel test; new wall-clock write test | | T2 | A call admitted while the lease was open writes **after** the lease closed (a slow write that races the end of the run) | Admitted again when the write commits. Draft writes, discards and App creates check the lease **inside the Workspace object's write transaction**. Commits elsewhere (propose, permission and dependency requests, connection requests and calls, USER.md saves) call `admitEffect` right before the effect. | DO-level fence test | | T3 | A permission of the agent is revoked while code is under way | Every call and every write admission checks for any revocation of the agent's permissions since the run opened (one indexed D1 read). The first one that finds it closes the lease as `revoked`, and the run ends `revoked` whatever its code returned. Its result isn't handed to the model, since it may rest on what was revoked. | revocation test | | T4 | A core restart mid-run leaves a usable lease, or the module runs again | Leases live only in memory. After a restart the run is unknown and answers `ended`. The record is marked `interrupted` on wake. Turns are never replayed, so a follow-up is a new run with a new ID. | restart test (the object is aborted with `ctx.abort`) | | T5 | Forged or guessed run IDs, or one chat's run reaching another chat | The run ID is a random UUID in props the code can't read or change, and leases are keyed `<chat>/<run>` | existing env/import tests; unknown-run case | | T6 | Flooding the audit log by calling after the end | One refusal is audited per run. The call that finds a revocation audits it, and later calls don't. | existing 100-call test; revocation test | | T7 | Erasing what already happened | Effects admitted before the close keep their receipts (pending actions, proposed versions). Nothing is rolled back or described as undone. | (by construction) | Accepted residual: revoking **any** permission of the org agent ends every run of that agent under way, not only runs that used it. A run lasts at most 30 s, so this errs safe and costs little. It follows the posture of allowing and auditing, without per-permission bookkeeping. ## What changes - **`chat_code_runs`**, a new Workspace DO SQLite table. It is the chat's existing persistence, not a separate Code Mode store. Each row holds: - the run ID, tool call ID, SHA-256 and length of the module (never the module itself); - `agent` (`@earendil-works/pi-agent-core@<exact version>`, imported from the package; pi-ai is pinned to the same version in the catalog) and the compatibility date; - `runtime` and `cpu_limit`; - `status` (`running`, then the terminal status, or `interrupted`) and its times. - Rows are deleted with the chat. The migration is `0008_chat_code_runs`; it is new, so this is a hard cut. - **D2.** Only `RUNTIME_PROFILE=cloudflare` records `enforced`. Anything else, including unset or a typo, records `cpu_limit_unsupported`. Tests, `vp run dev` and the on-prem smoke config set `workerd`. The CPU proof is GRA-382's. **Follow-up:** the console must set `RUNTIME_PROFILE=cloudflare` on Cloudflare deployments. Until it does, their records say `cpu_limit_unsupported`, which errs safe. - **Leases.** They now carry `openedAt`. New `revokeCodeRun` and `admitFromCodeRun` close a lease and check one without counting a call. Draft writes and discards now name their run: `saveDraft(..., runId)` and `dropDraft(..., { runId })`. A proposal's follow-through drop goes by `{ revision }`, because its commit was already admitted. `takeCreate(..., runId)` also names its run. - **New error code** `agent.run_revoked`. ## Evidence New tests in `apps/core/test/agent.test.ts` ("chat agent code run leases"), through the real Workspace DO, Worker Loader, Pi loop and D1: - **The record:** call ID, source hash and length, the exact Pi version, compatibility date, `workerd` with `cpu_limit_unsupported`, and status `completed`. - **The DO fence:** - a write while the run is open is admitted; - after `closeCodeRun` (`wall_expired`), a write and a discard are refused with `agent.run_ended`; - after `revokeCodeRun`, a write and a create are refused with `agent.run_revoked`; - an unknown run is refused; - the revoked run's close returns `revoked`; - the draft holds only the admitted write, and the records read `wall_expired` and `revoked`. - **Revocation mid-run:** the code waits, an admin revokes the agent's grant, and the next call is refused. The tool result is `Error (revoked)`, the record says `revoked`, and exactly one `agent.run_revoked` audit refusal is written. - **Restart mid-run:** `ctx.abort("restart")` while the code waits. The turn fails, and on wake the record says `interrupted` and the run's ID answers `ended`. 6 s later there is still one tool call, no result, one run record and one model request: nothing ran again. In `apps/core/test/agent-builds.test.ts`: code that waits 2 s past its 1 s wall-clock limit and then writes the draft is refused once (`agent.run_ended`), and the draft stays empty. Commands: - `vp check`: passes. - `vp test run test/agent.test.ts test/agent-connections.test.ts test/chat-connections.test.ts test/agent-instructions.test.ts test/agent-knowledge.test.ts test/chats.test.ts test/memory.test.ts test/run-fixes.test.ts test/packages.test.ts test/dependencies.test.ts`: 184 passed. - `vp test run -c vite.screens.config.ts test/agent-builds.test.ts test/preview-repairs.test.ts test/app-preview.test.ts test/agent-apps.test.ts`: 46 passed. ## Security review fixes (cc14ff5) **M1. A revocation with no later call went unnoticed.** As a run ends, `#endCodeRun` now reads revocations once more. If it finds one, it closes the run as `revoked` and records a single `agent.run_revoked` (`method: executeCode`). **M2. A revoked run's logs still reached the model.** A revoked run now hands on neither its result nor its logs. In #390, output that no watcher has had yet is dropped when the lease is revoked. **M3. Any revoked row of the org agent ended every run.** `revokedSince` now counts only revoked permissions with `granted_at IS NOT NULL AND (chat_id IS NULL OR chat_id = <this chat>)`. A declined request that was never granted, or a revocation in another chat, leaves the run alone. The comparison is `revoked_at >= opened_at`. - I added no skew margin. With a 2 s margin, a run opened just after an unrelated revoke ended as revoked; the existing held-outcome connection test showed it. Both timestamps come from Workers clocks. **S1.** `saveDraft`, `dropDraft({ runId })` and `takeCreate` now run `admitEffect` first, and keep the lease check inside their own transaction. **S2.** `admitEffect` takes three steps in this order: - a cheap lease read, to get `openedAt` and fail fast; - the D1 revocation read; - the lease check, last, right before the effect. **S3.** - `connections.call` and `memory.saveUser` record their `admitEffect` refusals with `admitEffect(env, scope, method)`. Calls wrapped in `auditedCall` are recorded by it. - A refusal is recorded only by the call that closed the lease (`revocationOf` returns `closed` or `closedBefore`), which fixes the double audit. - Remaining case: a call admitted at its start, then refused at commit because another call had already revoked the lease, is recorded by `auditedCall` too. - The `admitEffect` in `memory.saveUser` stays. It is the commit-time check, which `requireOpenRun` at call start doesn't cover. **S4. New tests:** - M1: the code calls once and waits, an admin revokes, and the code returns without another call. The result and logs are withheld, the status is `revoked`, and one refusal is recorded. - M3: a declined, never-granted request and another chat's revocation, stored mid-run, leave the run `completed`. - `admitEffect` end to end, using a slow outside dependency: every read of core's files is delayed 2 s against a 1 s run limit. - A proposal whose run expired while it was checked commits nothing, and `build.propose` is refused as `agent.run_ended`. - A draft write let in at once outlives its run while it reads the App's files, and is refused at commit. - Mutation checks: - an `admitEffect` that does nothing fails the proposal test; - dropping the granted and chat filters fails the M3 test; - dropping the end-of-run revocation read fails the M1 test. **S5.** `RUNTIME_PROFILE` now fails closed (see D2 above). There is a test of how each profile value maps. **S6.** New migration `0056_permissions_revoked_idx` adds `permissions(subject_type, subject_id, status, revoked_at)`. **Nits:** - `recordSources` refuses a revoked lease with `agent.run_revoked`. - `interrupted` is a `CodeRunStatus`, and the statuses live in `code-run-statuses.ts`. - `chat_code_runs` (migration `0008`, regenerated since it isn't on main) gains a CHECK on `status`, a CHECK on `tool_call_id` length (1 to 200, cut to fit before insert), and a partial index `WHERE status = 'running'`. A chat keeps its latest 1000 records. - Not recorded: pi-ai's version, which exports no `package.json` to read (it is pinned to pi-agent-core's version in the catalog), and the workerd build version, which the runtime doesn't expose. - `RUNTIME_PROFILE` is added to the platform's reserved binding names. **Evidence at the top of the stack:** - `vp check`: passes. - `vp test run` on agent, connections, chat-connections, instructions, knowledge, chats, memory, run-fixes, permissions, packages and dependencies: 220 passed. - `vp test run -c vite.screens.config.ts` on agent-builds, preview-repairs, app-preview and agent-apps: 47 passed. ## Delta review fixes (d14824f) **Must-fix 1, fail open on T1:** the end-of-run revocation read could throw before the lease closed. **Must-fix 2, regression:** the lease stayed open through that D1 round trip after wall expiry or cancel. - `#endCodeRun` now closes the lease and records the status synchronously and unconditionally first. Only then does it read revocations. - If a revocation is found, the record is revised to `revoked`, one `agent.run_revoked` is recorded, and the result and logs are withheld. - A read that **throws** counts as revoked (fail closed). It is logged as `agent.revocation_read_failed`. New tests (only the revocation query is faulted, through a database proxy; everything else is real): - **A throwing revocation read:** the result and logs are withheld (`Error (revoked)`), the record says `revoked`, and the lease reads `ended`. - **A call during the end-of-run read** (the read is slowed 2 s; the run ends at its 1 s wall clock while the code waits, then calls): refused once as `agent.run_ended`, with the run recorded as `wall_expired`. - Mutation check: against the previous `#endCodeRun`, both tests fail. **Nits:** - A comment now states that cross-machine clock skew is the accepted residual of the margin-free comparison. - The `admitEffect` doc now says refusals are recorded per refused call. - A comment ties `granted_at IS NOT NULL` to how permissions become active: both grant paths set it, so only a request declined before it was granted lacks it. Evidence at the top of the stack: - `vp check`: passes. - Core test files: 226 passed (11 files). - Screen-config test files: 47 passed (4 files). - `smoke:code-mode`: passes. ## Greptile fix (1b7fd82): admission right before the write `admitEffect` ran before slow preparation, not right before the write, so work that outlasted the run, or a revocation meanwhile, still let the write land after the lease closed. I confirmed this with a probe for dependency resolution. Each write helper now takes an admission callback and calls it right before its batch, after any slow preparation. The early check stays as a cheap refusal. Where the late check runs: | Helper | Late check runs | |---|---| | `resolveDependencies` | after `resolveGraph` and `inspectGraph`, before `proposeResolved` | | `proposeDraft` | after `storeTree`, before `auditedBatch` | | `saveUserMemory` | as `writeVersion`'s `lastCheck` | | `createApp`, `createFromBlueprint`, `requestPermission`, `askInChat` | before their batch | Unchanged: - `saveDraft`, `dropDraft` and `takeCreate` already check the lease inside the Workspace transaction. - `connections.call` is admitted right before `CONNECT.call`. New tests, each slowing an outside step past a 1 s run limit: | Slowed step | Result | |---|---| | the registry metadata fetch | no dependency request or lock is stored, and the refusal is recorded as `build.resolveDependencies` | | the upload of the version's tree | no pending version | | the knowledge reads | no USER.md is saved, and one `memory.saveUser` refusal is recorded | Mutation check: removing the late admission from any one of the three helpers makes its test fail. Evidence: - `vp check`: passes. - Agent, connections, chat-connections, knowledge, chats, memory, permissions, dependencies, packages, app-blueprints and apps tests: 236 passed. - `agent-builds` and `agent-apps` (screen config): 34 passed.
nickruigrok
added a commit
that referenced
this pull request
Oct 11, 2026
…get (#390) Fourth PR of the GRA-349 stack, on top of #389. It adds bounded output and output that streams before the tool completes (section 21.1, step 5). ## What changes - **One output budget:** 64 KiB of UTF-8, shared by logs, streamed progress, result and error. - 1 KiB is kept for how the run ended, so a flood of logs never hides the error. - Cut text ends with an explicit marker: `… (cut: over 65536 bytes of output)`. - This replaces the 32 K-character model view. - Text is cut with `TextEncoder.encodeInto`, so a cut never lands inside a character. - **Streaming:** - The harness sends each logged line to core's callback in order, one after another, and waits for every line to arrive before a call returns. The code never sees the callback. - The callback is handed to `verify` and to `run` separately, because an RPC stub only works in the call that brought it. - Core checks each line like the run's result (a string, bounded) and drops it once the run has ended. - Each line becomes a Pi tool update (`onUpdate`; Pi ignores updates after the step settles). The turn hands it to the Workspace DO. - The DO takes a line only while the run's lease is open. It counts and bounds the lines itself (1,000 lines, 128 KiB of raw text a run), whatever the harness claims. - A line is shown only after a read of revocations that **began after the line arrived** finds none. Those reads happen at most once a second a run and fail closed. So nothing logged after a revocation is ever shown, even if the code makes no further call. - The shown text is the whole output redacted at once, then cut at the budget less the reserve, so a credential split across lines is caught. Watchers get it through `ChatUpdate.output`, which grows incrementally like `partial` and is left out when unchanged. - The output stays until the step's result is stored, so it doesn't flicker away first. A revocation, or the end of the turn, drops it at once. - **Redaction.** Bearer and basic credentials, `sk-`/`pk-`/`rk-` keys, JWTs, PEM private keys and AWS access keys become `[redacted]`. This applies to every streamed line and to what the model reads and the transcript stores. A run holds no secret of its own; this guards against an outside system's data carrying one. - **Correlation.** The tool result's `details` now carry the run ID along with the status, tying the transcript to the run's record. - **Web.** - `ChatView.output` holds the running step's output, built with `applyOutput`. - The code step shows an "Output" section while it runs, replaced by its result. - Translations are added for nl, de, fr and es. ## Evidence New tests in `apps/core/test/agent.test.ts` ("chat agent code output"), through the real DO watch, Loader and Pi loop: - **Streaming:** - before the step's result message reaches the watcher, it has seen `first\nkey [redacted]` and then `…\nafter the wait`; - the output goes back to `null` once the step ends; - the model's text is redacted the same way; - a line sent through `codeRunOutput` after the run closed shows nowhere. - **Budget:** a flood of 5,000 lines of two-byte characters followed by a throw: - the final text is at most 64 KiB, is cut with the marker and still ends with `Error (threw): Error: the end`; - the streamed output never passes 64 KiB less the 1 KiB reserve. - **Pure helpers:** byte cuts with `é` and emoji, and the redaction patterns. - **Updated tests:** the existing flood and forged-prototype tests now check the 64 KiB budget and the new marker. In `apps/web/src/chat/follow-chat.test.ts`: the output grows, starts afresh for another step, and drops when no step runs. Commands: - `vp check`: passes. - `vp test run test/agent.test.ts test/chats.test.ts test/run-fixes.test.ts test/agent-connections.test.ts`: 90 passed. - `vp test run -c vite.screens.config.ts test/agent-builds.test.ts test/agent-apps.test.ts`: 32 passed. - In apps/web, `vp test run src/chat/follow-chat.test.ts src/locales/locales.test.ts`: 12 passed. - `vp build` (web, React Compiler): passes. ## Restacked on the #387 and #389 review fixes (daf0cb4) - The harness's streaming is merged into the hardened harness. The output callback, captured `Promise.prototype.then` and `Promise.resolve`, and the in-order delivery are unchanged, and every outcome is still returned rather than thrown. - A lease closed by a revocation drops output that no watcher has had yet. The model gets no logs of a revoked run (#389, M2). ## Review fixes (c7d3cdd) **Must-fix: output kept streaming after a mid-run revocation.** A line is now shown only once a read of revocations begun after it arrived finds none. Those reads run at most once a second a run, through the same `revokedSince` read, and fail closed. A revocation found there: - closes the lease as `revoked`; - drops the output, shown or pending; - records one `agent.run_revoked` (`method: output`); - streams nothing more. **Should-fix:** - **Redaction now also covers** Stripe `sk_`/`pk_`/`rk_` live and test keys, GitHub `ghp_`/`gho_`/`ghs_`/`ghu_`/`ghr_`/`github_pat_`, Slack `xox[abpr]-`, Google `AIza`, and the value of `api_key`, `apikey`, `token`, `secret`, `password` or `authorization` written as `name=value` or `name: value`. The docstring lists exactly what is covered and says it is a net, not a guarantee. - **The stream redacts like the model view:** core keeps each run's lines and redacts all of them as one text. - **Core counts lines and raw bytes itself**, and doesn't trust the harness's totals. **Nits:** - The output stays until the step's result is stored (no flicker). - `output` is left out of an update when it didn't change (`applyOutput` keeps what was shown). - `aria-live` is dropped. **New tests:** - **Revocation with no call:** the code logs, waits and logs again with no call; the admin revokes in between. No line logged after the revoke ever reaches the chat, and the run ends `revoked`. - Mutation check: with the stream's revocation read disabled, this test fails. - **Clearing when a call finds the revocation** (`revokeCodeRun`): the shown output goes to `null` before the step's result. - **A PEM private key logged line by line** never shows its body in any streamed text, and the model reads `Logs:\n[redacted]\ndone`. - **1,500 lines sent straight to `codeRunOutput`** show as 1,000. - **The redaction table covers the new shapes.** The token-shaped fixtures are put together at runtime, so secretlint finds no token in the repository. Evidence (top of the stack): - `vp check`: passes. - Core: 230 passed. - Screen-config tests: 47 passed. - Web: 12 passed, and the web build passes. ## Delta review fix (2815b1e) **Must-fix: quadratic JWT redaction.** The pattern started at `\b`, and a flood like `eyJ-eyJ-…` has a word boundary at every step. Redacting about 128K characters of that took about 4 s, inside the Workspace object, which re-redacts output about once a second. - The pattern now starts at `(?<![\w-])`. A real JWT still matches. - A new test redacts floods of about 128K characters for every credential shape, each with tails of `""`, `"x"`, `"-"` and `"."`. All must finish within 250 ms. - With the old pattern, that test fails (13.8 s). With the fix, it passes. No other pattern was slow under the same floods. **Nits:** - A revocation read that fails is now recorded with its own reason, `agent.revocation_read_failed`, both by the output check and at the end of a run. - New tests for the output check: - **Failed read:** nothing is shown, the run ends `revoked`, and one `agent.revocation_read_failed` is recorded. - **Slow read:** the read takes 3 s, longer than the run, so the line is never shown before the step's result, which shows it. Evidence: `vp check` passes; `vp test run test/agent.test.ts test/chats.test.ts` passes (103).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Second PR of the GRA-349 stack, on top of #385. It brings the existing
executeCodeandrunCodeup to the module contract in section 21.1. The executor and the tool are the same ones as before; nothing new runs beside them.What changes
{ code }and nothing else. The declaration setsadditionalProperties: false, andprepareArgumentsrefuses anything else with a short message. Pi's own validation error would repeat the whole input, which can be 200 KB, back to the model and into the transcript.refusedand a bounded diagnostic.(self, env, ctx):selfisnull;envholds the same seven scoped stubs as before (decision D1);ctxis a frozen object with only the run's ownwaitUntil, so nothing of core's and noexports.waitUntilextends no lease, because the host closes the run's stubs when the run ends.verifyimports the module and checks its default export. Whatever the module does while it loads runs inside the isolate's limits. A failure there isload_failed, and what the module logged before failing is kept.runthen calls the function.completed,threw,refused,load_failed,cpu_exceeded,crashed,wall_expiredandcancelled.isErrorresult. The status is kept in the tool result'sdetails, and the model reads it asError (<status>):.wall_expired.CODE_RUN_TIMEOUT_MSshortens the wall clock in tests only, likeAPP_CALL_TIMEOUT_MS. It can only lower the limit, and the model can't set it.Not here
cpu_exceededmaps the runtime's message, but plain workerd doesn't enforcelimits.cpuMs. The CPU proof is GRA-382's; PR 5 of this stack recordscpu_limit_unsupportedon plain workerd.Evidence
New tests in
apps/core/test/agent.test.ts, all through the real Workspace DO, Worker Loader and Pi loop, with only the model provider faked:self === null,ctxkeys["waitUntil"], and two parallel native calls.refusedbefore loading. It would log while loading, and it doesn't.load_failed.threw, after the module's load-time log.wall_expired.wall_expiredwithin 10 s.Error (cancelled).Commands:
vp check: passes.vp test run test/agent.test.ts test/run-fixes.test.ts: 54 passed.vp test run test/chat-connections.test.ts test/agent-knowledge.test.ts test/agent-instructions.test.ts test/agent-connections.test.ts test/run-fixes.test.ts, and the same withtest/packages.test.ts test/dependencies.test.ts test/chats.test.ts: all pass.vp test run -c vite.screens.config.ts test/agent-apps.test.ts test/agent-builds.test.ts test/preview-repairs.test.ts: 38 passed.Review fixes (711c4a5)
Must-fix: the code could forge the host-set statuses. It could make
cpu_exceeded,wall_expiredorcrashedappear by making a harness call reject with a message it chose. Three routes did it: a thrown Proxy, built-ins patched after load, and replacingHarness.prototype.runthrough an import of the harness.Object.keys,Proxy,Reflect,Error, the array and string helpers,JSON.stringifyandString.render,showanddescribeare total. They catch everything, fall back to fixed text, and read a thrown value'sstackwithReflect.getinstead ofinstanceof.verifyandrunreturn every JS outcome and never reject from JS. A rejection is therefore always the runtime's own.cpu_exceededonly from the runtime's exact message, matched whole, andwall_expiredonly from its hang message (prefix).Nits:
run()with no function to call ends asload_failed(the outcome carriesloaded).crasheddoc now says a rejection while loading ends asload_failedand one while running ascrashed.load_failedfrom verify andthrewfrom run; this is documented.New tests:
threw.threw.threw.completed.load_failed.load_failed.ctx.waitUntilwork calling env after the run returned is never answered. On workerd, that work either reaches core and is refused at the lease, or never reaches core at all; the test accepts both, so it doesn't prove which.Evidence:
vp test run test/agent.test.ts(57 passed at this commit), plus the connection, knowledge, chat and run-fix test files (42 passed).Delta review fix (b4ae7c2)
Must-fix: forgery through structured clone. A setter on
Array.prototype[0]fired when the harness pushed to its logs array, installed a throwing getter, and made the RPC serialisation of the outcome reject with the code's own message.verifyandrunnow return one string: the outcome's JSON, put together by hand from primitives (two booleans, the logs, and one result or error string), with a fixed fallback.logsJson), built by string concatenation. No array or object of the harness's is ever filled through [[Set]] or sent.Array.prototype[0]set up while the module loads and inside the function, each throwing the CPU and the hang message. They end ascompletedorthrew.Evidence:
vp test run test/agent.test.tspasses (61), and so do the connection, knowledge, chat and run-fix test files (42).