From c16dd29d29f7b39f047a8894d646dc6add4cf18e Mon Sep 17 00:00:00 2001 From: Phodal Huang Date: Thu, 27 Aug 2026 20:45:12 +0800 Subject: [PATCH 1/4] feat(artifacts): add production AgentReact runtime Introduce a spec-backed AgentReact pipeline across semantic compilation, confined linking, immutable snapshots, opaque iframe staging, Host-owned state and Actions, and Studio registration. The implementation covers AR-AC-1 through AR-AC-30 in docs/specs/2026-08-27-agent-react-artifact-runtime-poc.md. It was validated with npm run check, the 47-test Chromium suite, and Intel macOS npm install-plan closure. Co-authored-by: Codex (GPT 5.6 Sol) --- .gitignore | 3 + ...-08-27-agent-react-artifact-runtime-poc.md | 630 +++++++++++++++ package-lock.json | 740 +++++++++++++++++- packages/harness-studio/README.md | 41 + packages/harness-studio/package.json | 2 + .../src/agent-react/contracts/addressing.ts | 70 ++ .../src/agent-react/contracts/build.ts | 47 ++ .../src/agent-react/contracts/compile.ts | 91 +++ .../src/agent-react/contracts/diagnostics.ts | 46 ++ .../src/agent-react/contracts/host.ts | 115 +++ .../src/agent-react/contracts/index.ts | 17 + .../src/agent-react/contracts/revision.ts | 29 + .../src/agent-react/contracts/versions.ts | 12 + .../src/agent-react/host/action-gateway.ts | 92 +++ .../src/agent-react/host/build-coordinator.ts | 301 +++++++ .../src/agent-react/host/capability.ts | 123 +++ .../agent-react/host/compiler-worker-entry.ts | 23 + .../host/compiler-worker-protocol.ts | 39 + .../src/agent-react/host/data-ownership.ts | 40 + .../src/agent-react/host/digest.ts | 32 + .../host/frames/frame-controller.ts | 222 ++++++ .../agent-react/host/frames/frame-protocol.ts | 101 +++ .../host/frames/local-frame-factory.ts | 249 ++++++ .../src/agent-react/host/index.ts | 75 ++ .../agent-react/host/observation-bridge.ts | 92 +++ .../src/agent-react/host/project-loader.ts | 146 ++++ .../src/agent-react/host/state-store.ts | 156 ++++ .../src/agent-react/host/stream-assembler.ts | 93 +++ .../agent-react/host/worker-oxc-compiler.ts | 146 ++++ .../harness-studio/src/agent-react/index.ts | 39 + .../src/agent-react/kernel/abi.ts | 260 ++++++ .../src/agent-react/kernel/ast.ts | 122 +++ .../src/agent-react/kernel/compiler.ts | 162 ++++ .../src/agent-react/kernel/index.ts | 18 + .../src/agent-react/kernel/profile.ts | 171 ++++ .../src/agent-react/kernel/semantic-index.ts | 139 ++++ .../agent-react/linker/allowed-packages.ts | 79 ++ .../src/agent-react/linker/esbuild-linker.ts | 172 ++++ .../src/agent-react/linker/index.ts | 18 + .../agent-react/runtime/address-registry.ts | 50 ++ .../src/agent-react/runtime/bridge.ts | 47 ++ .../src/agent-react/runtime/browser-bridge.ts | 135 ++++ .../src/agent-react/runtime/index.ts | 111 +++ .../agent-react/runtime/jsx-dev-runtime.ts | 90 +++ .../app/artifacts/AgentReactHostServices.ts | 100 +++ .../app/artifacts/AgentReactPreviewHost.tsx | 415 ++++++++++ .../app/artifacts/ArtifactSurfaceRegistry.tsx | 8 + .../src/app/styles/workbench.css | 7 + .../agent-react-production-runtime.ts | 206 +++++ .../registry/artifact-build-runtimes.ts | 6 + .../artifacts/registry/artifact-catalog.ts | 2 + .../registry/artifact-compile-runtime.ts | 80 +- .../registry/artifact-plugin-registry.ts | 25 + .../agent-react/build-coordinator.test.ts | 232 ++++++ .../capability-and-actions.test.ts | 205 +++++ .../test/agent-react/frame-controller.test.ts | 388 +++++++++ .../test/agent-react/frame-protocol.test.ts | 68 ++ .../test/agent-react/host-services.test.ts | 75 ++ .../test/agent-react/layering.test.ts | 278 +++++++ .../agent-react/observation-bridge.test.ts | 122 +++ .../test/agent-react/oxc-compiler.test.ts | 251 ++++++ .../test/agent-react/pipeline-fixture.ts | 110 +++ .../agent-react/runtime-addressing.test.ts | 377 +++++++++ .../test/agent-react/state-store.test.ts | 177 +++++ .../test/agent-react/stream-assembler.test.ts | 105 +++ .../agent-react/worker-oxc-compiler.test.ts | 86 ++ .../test/artifact-compile-runtime.test.ts | 85 ++ .../test/artifact-view-registry.test.ts | 2 + .../test/artifact-viewers.test.ts | 1 + .../test/browser/artifact-host.spec.mjs | 194 +++++ packages/harness/src/artifacts/model.ts | 36 + packages/harness/src/artifacts/provider.ts | 1 + packages/harness/test/artifacts.test.ts | 30 + 73 files changed, 9047 insertions(+), 11 deletions(-) create mode 100644 docs/specs/2026-08-27-agent-react-artifact-runtime-poc.md create mode 100644 packages/harness-studio/src/agent-react/contracts/addressing.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/build.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/compile.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/diagnostics.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/host.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/index.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/revision.ts create mode 100644 packages/harness-studio/src/agent-react/contracts/versions.ts create mode 100644 packages/harness-studio/src/agent-react/host/action-gateway.ts create mode 100644 packages/harness-studio/src/agent-react/host/build-coordinator.ts create mode 100644 packages/harness-studio/src/agent-react/host/capability.ts create mode 100644 packages/harness-studio/src/agent-react/host/compiler-worker-entry.ts create mode 100644 packages/harness-studio/src/agent-react/host/compiler-worker-protocol.ts create mode 100644 packages/harness-studio/src/agent-react/host/data-ownership.ts create mode 100644 packages/harness-studio/src/agent-react/host/digest.ts create mode 100644 packages/harness-studio/src/agent-react/host/frames/frame-controller.ts create mode 100644 packages/harness-studio/src/agent-react/host/frames/frame-protocol.ts create mode 100644 packages/harness-studio/src/agent-react/host/frames/local-frame-factory.ts create mode 100644 packages/harness-studio/src/agent-react/host/index.ts create mode 100644 packages/harness-studio/src/agent-react/host/observation-bridge.ts create mode 100644 packages/harness-studio/src/agent-react/host/project-loader.ts create mode 100644 packages/harness-studio/src/agent-react/host/state-store.ts create mode 100644 packages/harness-studio/src/agent-react/host/stream-assembler.ts create mode 100644 packages/harness-studio/src/agent-react/host/worker-oxc-compiler.ts create mode 100644 packages/harness-studio/src/agent-react/index.ts create mode 100644 packages/harness-studio/src/agent-react/kernel/abi.ts create mode 100644 packages/harness-studio/src/agent-react/kernel/ast.ts create mode 100644 packages/harness-studio/src/agent-react/kernel/compiler.ts create mode 100644 packages/harness-studio/src/agent-react/kernel/index.ts create mode 100644 packages/harness-studio/src/agent-react/kernel/profile.ts create mode 100644 packages/harness-studio/src/agent-react/kernel/semantic-index.ts create mode 100644 packages/harness-studio/src/agent-react/linker/allowed-packages.ts create mode 100644 packages/harness-studio/src/agent-react/linker/esbuild-linker.ts create mode 100644 packages/harness-studio/src/agent-react/linker/index.ts create mode 100644 packages/harness-studio/src/agent-react/runtime/address-registry.ts create mode 100644 packages/harness-studio/src/agent-react/runtime/bridge.ts create mode 100644 packages/harness-studio/src/agent-react/runtime/browser-bridge.ts create mode 100644 packages/harness-studio/src/agent-react/runtime/index.ts create mode 100644 packages/harness-studio/src/agent-react/runtime/jsx-dev-runtime.ts create mode 100644 packages/harness-studio/src/app/artifacts/AgentReactHostServices.ts create mode 100644 packages/harness-studio/src/app/artifacts/AgentReactPreviewHost.tsx create mode 100644 packages/harness-studio/src/server/artifacts/registry/agent-react-production-runtime.ts create mode 100644 packages/harness-studio/test/agent-react/build-coordinator.test.ts create mode 100644 packages/harness-studio/test/agent-react/capability-and-actions.test.ts create mode 100644 packages/harness-studio/test/agent-react/frame-controller.test.ts create mode 100644 packages/harness-studio/test/agent-react/frame-protocol.test.ts create mode 100644 packages/harness-studio/test/agent-react/host-services.test.ts create mode 100644 packages/harness-studio/test/agent-react/layering.test.ts create mode 100644 packages/harness-studio/test/agent-react/observation-bridge.test.ts create mode 100644 packages/harness-studio/test/agent-react/oxc-compiler.test.ts create mode 100644 packages/harness-studio/test/agent-react/pipeline-fixture.ts create mode 100644 packages/harness-studio/test/agent-react/runtime-addressing.test.ts create mode 100644 packages/harness-studio/test/agent-react/state-store.test.ts create mode 100644 packages/harness-studio/test/agent-react/stream-assembler.test.ts create mode 100644 packages/harness-studio/test/agent-react/worker-oxc-compiler.test.ts diff --git a/.gitignore b/.gitignore index 54599e7..0db67f6 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,9 @@ dev/terminal-demo/*.cast /harness-qoder-profile-evidence/ design-qa.md +# Linked AgentReact bundles written by the Artifact Runtime tests. +packages/harness-studio/test/.artifacts/ + # Local Artifact View scratch decks and their inspection dumps. /*.pptx /*.pptx.inspect.ndjson diff --git a/docs/specs/2026-08-27-agent-react-artifact-runtime-poc.md b/docs/specs/2026-08-27-agent-react-artifact-runtime-poc.md new file mode 100644 index 0000000..8bd6eec --- /dev/null +++ b/docs/specs/2026-08-27-agent-react-artifact-runtime-poc.md @@ -0,0 +1,630 @@ +# AgentReact Artifact Runtime production foundation + +## Traceability + +- Spec ID: agent-react-artifact-runtime-poc +- Status: Implemented locally; cross-platform CI and publication pending +- ADR: [Harness Studio Artifact runtime and provider architecture](../adrs/studio-artifact-runtime-and-providers.md) +- Related spec: [Stabilize the Artifact View host lifecycle](2026-08-23-studio-artifact-view-host-lifecycle.md) + +## Intent + +Prove that a React Artifact can travel the whole AgentReact pipeline as an +addressable, verifiable, transactionally committed build: + +```text +Agent Source Stream +→ Artifact Revision +→ Oxc Semantic Compile +→ esbuild Link +→ Immutable Build Snapshot +→ Opaque Sandbox Staging +→ Atomic View Commit +→ Observation +``` + +The proof of concept only strengthens the existing ADR-0007 Code-backed +lifecycle. It does not add a third lifecycle, and it does not touch the +Data-backed adapter path. + +The value being proved is not "TSX can be bundled" — Studio already bundles TSX +through `artifact-compile-runtime.ts`. The new claims are: + +1. a compile stage can *refuse* code that leaves the AgentReact language + profile, before any bundle exists; +2. a default export can declare its state and capability requests as static + data, and the Host can grant strictly less than the code asked for; +3. every rendered DOM node can carry a deterministic address back to its source + span; and +4. a new build can be verified in a separate staging frame and committed + atomically, so a failing revision never replaces a working view. + +The pipeline above is the target architecture. The first increment established +the production-eligible data and Host foundations. The production increment +continues in this same spec: it registers an explicit AgentReact format, moves +Oxc behind a restartable deadline-enforced Worker, and binds the build to +Studio's existing opaque-origin iframe/CSP/MessageChannel security surface. + +## Production Readiness + +Production readiness is assessed per boundary rather than inherited from a +passing end-to-end POC: + +| Boundary | Readiness in this increment | Production rule | +| --- | --- | --- | +| Revision, compile/link contracts, Build Snapshot identity | Production-eligible foundation | Identity-owned records are deeply immutable and every effective compiler/runtime policy participates in the build identity | +| Oxc Node compiler | Implemented; macOS arm64 runtime and extracted-install verified; Intel macOS dependency closure checked | Native parsing may process untrusted source only inside a restartable Worker with an enforceable per-request deadline; direct in-process use remains verification-only | +| Profile validator | Advisory production diagnostic | It improves refusal quality but is never a security boundary; execution isolation and Host Action validation remain mandatory | +| State, capability, Action, and observation Host services | Production-eligible foundation | The Host owns immutable state copies, tokens are unguessable, approval revocation is effective immediately, and Actions are revalidated per dispatch | +| Transaction controller | Production-eligible only with an isolated FrameFactory | Concurrent stages are generation-fenced; a rejected, superseded, timed-out, or disposed frame can never act on newer controller state | +| `LocalFrameFactory` | Verification-only | It may prove compile/link/address wiring with explicit opt-in, but it is not an origin boundary and cannot support a production execution claim | +| Opaque iframe transport and Studio registration | Implemented; Chromium wide/compact/narrow verified | Reuse the existing Studio sandbox only after AgentReact adds transferred-port identity, two-frame commit, state/Action validation, cancellation, browser mount/error evidence, and responsive browser QA | + +The controller therefore rejects an in-process `FrameFactory` by default. +Tests and local experiments must opt in explicitly, making it impossible to +register the verification transport accidentally as a production runtime. + +## Terminology + +The document under implementation renames several overloaded words. This spec +uses the same vocabulary, and the code uses it verbatim: + +| Term | Meaning | +| --- | --- | +| `Artifact Revision` | Digest-addressed, immutable set of module sources | +| `Build Generation` | One monotonically numbered build attempt | +| `Build Snapshot` | Frozen, replayable compile+link result | +| `Artifact View Definition` | The `defineArtifactView` default export | +| `Artifact Surface` | Unchanged ADR-0007 presentation kind | + +## Decisions + +### D-1: Four layers, one narrow compiler port + +The POC keeps the four-layer split from the source document, and each layer is one +directory whose barrel is its public face: + +| Directory | Layer | Owns | +| --- | --- | --- | +| `contracts/` | (shared) | Layer-crossing types, plus the addressing algorithm | +| `kernel/` | Oxc Semantic Kernel | Parse, admit, extract ABI, index, erase types | +| `linker/` | esbuild Linker | Resolve modules, externalize Bootstrap, emit one bundle | +| `runtime/` | React Artifact Runtime | Render, state/action hooks, node addressing | +| `host/` | Artifact Host | Revision, state, grants, actions, commit, observation | + +Dependencies point one way: `host → {kernel, linker, runtime} → contracts`. Two of +those edges are load-bearing rather than tidy: + +- **`runtime` may not reach the kernel.** The runtime layer is what loads inside + the sandbox frame. One import of `kernel/` pulls `oxc-parser`'s native binding + into a browser bundle, which then fails to load at all. +- **`contracts` may not use Node built-ins or any package.** The runtime + re-exports contracts into the frame, so a single `node:crypto` there breaks the + same load. This is why hashing enters the pipeline as an injected `DigestFn`, + and why `contracts/addressing.ts` inlines a 64-bit FNV-1a instead of importing + one. + +`contracts/addressing.ts` is the only contract module with executable code, and +that is the point: the kernel computes a JSX element's id at compile time and the +sandbox `jsxDEV` computes it again at render time. Two copies of "the same" hash +in two layers is exactly how those drift, so both layers import this one. + +Business code never sees an Oxc AST; it depends only on `OxcCompilerPort`: + +```ts +interface OxcCompilerPort { + readonly compilerVersion: string; + readonly profileVersion: string; + compileModule(input: CompileModuleInput): Promise; +} +``` + +Oxc answers *what the code is and whether it obeys the contract*. The Host +answers *what the code is allowed to do*. The Profile validator is therefore +documented and tested as a Semantic Firewall, not as the security authority: the +Action Gateway re-validates every single call at runtime even if a bundle +forged its declaration. + +### D-2: Oxc runs through the Node bindings only behind the compiler port + +The direct `createOxcCompiler()` adapter remains the deterministic test and +fixture implementation. Production uses the same port through a Node +`worker_threads` adapter because Studio's server owns the confined source root, +the exact revision route, and the immutable build cache. Moving compilation to +the browser would duplicate that authority and transfer the whole source graph +over a second protocol without improving the iframe execution boundary. + +The Worker has a per-request deadline. Timeout, crash, invalid response, or +disposal terminates it; the next request starts a clean Worker. The compiler +policy fingerprint includes the deadline and Oxc limits. The stable +`limit/compile-timeout` diagnostic is produced without caching a partial build. + +### D-3: The Profile is a closed list of refusals + +`AGENT_REACT_PROFILE_VERSION = "1"` refuses, with a stable diagnostic code: + +| Code | Refusal | +| --- | --- | +| `profile/commonjs` | `require`, `module.exports`, `exports.x` | +| `profile/node-builtin` | `node:*` and bare Node built-in imports | +| `profile/dynamic-import` | any `import()` | +| `profile/package-not-allowed` | any bare import outside the allowlist | +| `profile/react-dom-root` | `createRoot`, `ReactDOM.render`, `hydrateRoot` | +| `profile/dynamic-eval` | `eval`, `new Function` | +| `profile/worker` | `Worker`, `SharedWorker`, `ServiceWorker`, `navigator.serviceWorker` | +| `profile/network` | `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource`, `sendBeacon` | +| `profile/class-component` | `class X extends React.Component/PureComponent` | +| `profile/top-level-effect` | a top-level statement that is not a declaration, an import/export, or a directive | + +`profile/top-level-effect` is deliberately a hard rule rather than a budget: a +top-level `VariableDeclaration`, `FunctionDeclaration`, type declaration, import, +export, or string directive is allowed, and every other top-level statement — +expression statements, loops, conditionals, `try`, `throw` — is refused. "Some +side effects allowed" has no test that separates pass from fail, and violations +inside an allowed declaration's initializer are still caught by the other rules. + +A refusal is reported as a diagnostic with module path, line, and column, and no +code is emitted for that module. + +### D-4: The ABI is extracted, never inferred + +`extractArtifactViewDeclaration` accepts exactly one shape: + +```tsx +export default defineArtifactView({ id, state, capabilities, component }); +``` + +- `id` must be a static string literal; +- `state` must be an object literal whose keys are `/`-rooted paths and whose + values are `{ schema: string, version: integer }` literals; +- `capabilities` must be an array literal of string literals; +- `component` must be an identifier bound to a module-local function declaration + or function-valued `const`, or a named/default import from another module in + the same Revision. Package and namespace imports cannot be the root component. + +Anything else — a spread, a computed key, an identifier reference for `id`, a +capability built by `.map()` — is `abi/not-static`. The validator never derives a +capability from a call the code happens to make, because a permission inferred +from behaviour would grant exactly what an attacker writes. + +### D-4b: The linker re-applies the allowlist and externalizes the Bootstrap + +`AllowedPackageResolver` answers three questions: is this an internal Revision +module, is it a trusted runtime package, or is it refused. A trusted package +resolves to an **external** Bootstrap specifier in the validation bundle. That +keeps the source-owned link step independent of installed package layout and +makes every trusted mapping part of `buildPolicyDigest`; the production packager +owns the later resolution into the self-contained iframe bundle. + +The resolver also maps the TypeScript `./panel.js` → `./panel.tsx` convention, +since that is what a TS-aware agent writes. + +The linker refuses a non-allowlisted package independently of the Profile, so +defeating one check does not produce a bundle. Note that Oxc elides an unused +import the way TypeScript does, so only a *used* forbidden import reaches the +linker at all. The production packager then resolves those trusted externals +from Studio's installed dependency closure and emits one self-contained iframe +bundle; Artifact source never controls that resolution step. + +### D-5: Grants are an intersection, and the frame token carries them + +```text +Granted = Declared Requests ∩ Host Policy ∩ Session Approval +``` + +`CapabilityBroker.computeGrant()` returns the intersection plus the two reasons a +request was dropped (`not-in-policy`, `awaiting-approval`), so the UI can explain +a missing control instead of silently omitting it. The broker issues a +frame-scoped token; `ActionGateway.dispatch()` validates the token, the +capability, and the frame's action mode on *every* call and revokes on frame +disposal. A revoked token denies even a previously granted capability. + +### D-6: Addresses are deterministic within a Revision, and only within it + +`@studio/agent-react/jsx-dev-runtime` accepts Oxc's development-shaped JSX call +for its source span, then delegates element creation to React's production-safe +`jsx`/`jsxs`. The automatic development transform supplies `fileName`, +`lineNumber`, and `columnNumber`, so: + +```text +SourceNodeId = fnv1a64(modulePath | line | column | elementType) → 16 hex chars +InstanceAddress = artifactDigest | SourceNodeId | React key | parent instance +``` + +Intrinsic elements receive `data-artifact-node`; catalog components receive the +reserved `artifactNode` prop and are expected to forward it. The runtime never +reads React internals or Fiber fields. + +Cross-Revision continuity is *not* claimed from spans. The `ReactSemanticIndex` +exists for that purpose and is explicitly excluded from authorization. + +### D-7: Observations reuse the AG-UI CUSTOM envelope, under their own name + +The source document routes observations through `HARNESS_PROTOCOL_EVENT`. That +constant's payload type is `HarnessProtocolEvidence` with `protocol: "acp"`, and +an artifact render is not ACP traffic. The POC therefore keeps the AG-UI +`CUSTOM` envelope but introduces `HARNESS_ARTIFACT_OBSERVATION_EVENT` +(`"harness.artifact-observation"`) so a consumer cannot mistake a render failure +for a protocol receipt. Recorded kinds are the ten versioned values exported by +the AgentReact Host contract. + +### D-8: Commit is transactional, and staging cannot act + +`SandboxFrameController` holds at most one Current and one Staging frame. +Staging is created with `actionMode: "dry-run"` and a frozen state snapshot; +`activate()` promotes it only after `renderCompleted`. On `renderFailed`, on +timeout, or on a blocking dry-run attempt under a `blockOnDeniedAction` policy, +the Staging frame is disposed and Current is untouched. + +Verification performs module load, mount, error boundary, a post-mount paint +boundary with a bounded visibility-independent fallback, and the timeout — it +never synthesizes clicks and never runs a real Action. +Promotion is a Host act, not a frame act: `activate()` revokes the dry-run token, +issues a `live` token for the same build digest, and hands it to the frame. A +frame therefore cannot grant itself live mode. + +### D-8b: The transport is a port; verification and production have distinct implementations + +`SandboxFrameController` depends on a `FrameFactory`, and the POC ships two +implementations of the frame side: + +- `host/frames/frame-protocol.ts` — the transport-free handshake: the init message, + identity matching, and the render reports. A frame must match protocol version, + artifact digest, build digest, and frame token before mounting, because an + opaque origin cannot serve as an identity and a superseded build racing a stage + is the common case. +- `host/frames/local-frame-factory.ts` — an in-process frame that **really + executes** the linked bundle: it loads the module, installs the runtime bridge + into that bundle's own runtime instance, renders the component, and returns + resolvable node addresses. + +Production reuses the existing Studio sandbox response and host invariants: +`