feat(extension): deterministic, identity-pinned resident extension artifacts - #2804
feat(extension): deterministic, identity-pinned resident extension artifacts#2804miguelg719 wants to merge 1 commit into
Conversation
|
There was a problem hiding this comment.
4 issues found across 14 files
Confidence score: 3/5
- In
packages/sdk-ts/extensionArtifactPackaging.ts, a regular file named__proto__can be omitted from the digest because assignment triggers the object prototype setter, weakening artifact integrity coverage; store entries in a null-prototype map. - In
packages/protocol/tests/browser-runtime/resident-browser-proxy-smoke.test.ts, a stalled Chrome/json/versionrequest can leave the smoke test hanging despite the client lifecycle signal; add a bounded abort signal to the upstream fetch. - In
packages/sdk-ts/tests/extensionArtifactPackaging.test.ts, the ordering assertion does not exercise the stated code-point behavior because JavaScript compares UTF-16 code units; add an astral/BMP filename pair and derive the expected order with the UTF-8 comparator. - In
packages/sdk-ts/extensionArtifactPackaging.ts, Windows packaging errors can expose backslash-separated paths inconsistent with normalized artifact paths; normalize the display path in both error messages.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/sdk-ts/extensionArtifactPackaging.ts">
<violation number="1" location="packages/sdk-ts/extensionArtifactPackaging.ts:65">
P2: When the unpacked extension contains a regular file named `__proto__`, the assignment invokes the object prototype setter, so `Object.keys(files)` omits that file from the digest. Store entries in a null-prototype map or `Map` in the shared traversal helper.</violation>
<violation number="2" location="packages/sdk-ts/extensionArtifactPackaging.ts:70">
P3: On Windows, these packaging errors expose `relativePath` with backslashes, making diagnostics inconsistent with the normalized artifact paths. Normalize the display path before both error messages.
(Based on your team's feedback about normalized display paths.)</violation>
</file>
<file name="packages/sdk-ts/tests/extensionArtifactPackaging.test.ts">
<violation number="1" location="packages/sdk-ts/tests/extensionArtifactPackaging.test.ts:54">
P2: This assertion does not test the stated code-point ordering because JavaScript `<` compares UTF-16 code units. Add an astral/BMP filename pair and compute the expected order with the UTF-8 comparator so cross-runtime digest regressions fail.</violation>
</file>
<file name="packages/protocol/tests/browser-runtime/resident-browser-proxy-smoke.test.ts">
<violation number="1" location="packages/protocol/tests/browser-runtime/resident-browser-proxy-smoke.test.ts:173">
P2: When Chrome's `/json/version` request stalls, this proxy fetch has no deadline, so the smoke test can hang despite the client lifecycle signal. Add a bounded abort signal to the upstream fetch.</violation>
</file>
Architecture diagram
sequenceDiagram
participant Build as Build System (Vite/Turbo)
participant FS as Artifact Store (dist/ & artifacts/)
participant SDK as SDK Packaging (TS/PY/GO)
participant Chrome as Chrome Runtime
participant Proxy as Resident Browser Proxy
Note over Build,FS: Extension Artifact Generation
Build->>Build: NEW: Validate VITE_STAGEHAND_BROWSER_PROXY_URL (loopback only)
Build->>FS: CHANGED: Generate manifest.json (Pinned Key/Chrome ID)
Build->>Build: NEW: Deterministic ZIP (Sorted entries, fixed mtime)
Build->>FS: NEW: Write stagehand-extension.metadata.json (SHA256, Commit, Config)
Note over FS,SDK: SDK Packaging & Validation Logic
SDK->>FS: Load ZIP and Metadata
SDK->>SDK: NEW: Verify residentGatewayConfigured is FALSE (Public builds only)
SDK->>SDK: NEW: Verify ZIP/Unpacked SHA256 matches Metadata
alt Metadata Mismatch or Private Config
SDK-->>SDK: Fail SDK Build
else Integrity Verified
SDK-->>SDK: Package Extension into SDK distribution
end
Note over Chrome,Proxy: Runtime Execution (Smoke Test Flow)
Chrome->>Chrome: Identify Extension by Pinned ID: hgibf...
Chrome->>Proxy: NEW: Extension connects to Loopback Proxy (Resident Mode)
Proxy->>Chrome: Proxy forwards to CDP Port
Chrome-->>Proxy: Return CDP Response
Proxy-->>Chrome: Return to Extension Worker
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| directory: string, | ||
| relativeDirectory = "", | ||
| ): Promise<Record<string, Buffer>> { | ||
| const files: Record<string, Buffer> = {}; |
There was a problem hiding this comment.
P2: When the unpacked extension contains a regular file named __proto__, the assignment invokes the object prototype setter, so Object.keys(files) omits that file from the digest. Store entries in a null-prototype map or Map in the shared traversal helper.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/extensionArtifactPackaging.ts, line 65:
<comment>When the unpacked extension contains a regular file named `__proto__`, the assignment invokes the object prototype setter, so `Object.keys(files)` omits that file from the digest. Store entries in a null-prototype map or `Map` in the shared traversal helper.</comment>
<file context>
@@ -0,0 +1,88 @@
+ directory: string,
+ relativeDirectory = "",
+): Promise<Record<string, Buffer>> {
+ const files: Record<string, Buffer> = {};
+ const entries = await readdir(path.join(directory, relativeDirectory), { withFileTypes: true });
+ for (const entry of entries) {
</file context>
| await writeFile(path.join(directory, relativePath), contents); | ||
| } | ||
| const expected = createHash("sha256"); | ||
| for (const [relativePath, contents] of files.toSorted(([left], [right]) => |
There was a problem hiding this comment.
P2: This assertion does not test the stated code-point ordering because JavaScript < compares UTF-16 code units. Add an astral/BMP filename pair and compute the expected order with the UTF-8 comparator so cross-runtime digest regressions fail.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/tests/extensionArtifactPackaging.test.ts, line 54:
<comment>This assertion does not test the stated code-point ordering because JavaScript `<` compares UTF-16 code units. Add an astral/BMP filename pair and compute the expected order with the UTF-8 comparator so cross-runtime digest regressions fail.</comment>
<file context>
@@ -0,0 +1,97 @@
+ await writeFile(path.join(directory, relativePath), contents);
+ }
+ const expected = createHash("sha256");
+ for (const [relativePath, contents] of files.toSorted(([left], [right]) =>
+ left < right ? -1 : left > right ? 1 : 0,
+ )) {
</file context>
| return; | ||
| } | ||
| try { | ||
| const upstream = await fetch(`http://127.0.0.1:${chromePort}/json/version`); |
There was a problem hiding this comment.
P2: When Chrome's /json/version request stalls, this proxy fetch has no deadline, so the smoke test can hang despite the client lifecycle signal. Add a bounded abort signal to the upstream fetch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/protocol/tests/browser-runtime/resident-browser-proxy-smoke.test.ts, line 173:
<comment>When Chrome's `/json/version` request stalls, this proxy fetch has no deadline, so the smoke test can hang despite the client lifecycle signal. Add a bounded abort signal to the upstream fetch.</comment>
<file context>
@@ -0,0 +1,252 @@
+ return;
+ }
+ try {
+ const upstream = await fetch(`http://127.0.0.1:${chromePort}/json/version`);
+ response.writeHead(upstream.status, {
+ "content-type": upstream.headers.get("content-type") ?? "application/json",
</file context>
| @@ -0,0 +1,88 @@ | |||
| import { createHash } from "node:crypto"; | |||
There was a problem hiding this comment.
P3: On Windows, these packaging errors expose relativePath with backslashes, making diagnostics inconsistent with the normalized artifact paths. Normalize the display path before both error messages.
(Based on your team's feedback about normalized display paths.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/sdk-ts/extensionArtifactPackaging.ts, line 70:
<comment>On Windows, these packaging errors expose `relativePath` with backslashes, making diagnostics inconsistent with the normalized artifact paths. Normalize the display path before both error messages.
(Based on your team's feedback about normalized display paths.) </comment>
<file context>
@@ -0,0 +1,88 @@
+ for (const entry of entries) {
+ const relativePath = path.join(relativeDirectory, entry.name);
+ if (entry.isSymbolicLink()) {
+ throw new Error(`Stagehand extension cannot contain symbolic links: ${relativePath}`);
+ }
+ if (entry.isDirectory()) {
</file context>
… build provenance Port of #2466 onto the main-based resident stack (packages/server -> packages/extension). - manifest.json: stable public `key` (Chrome ID hgibfbbnmoigailpmgihnbaokiinpmij); permissions trimmed to `debugger` + `offscreen` (tabs.query/update with id/windowId and tabs.onCreated do not need `tabs`; nothing uses `scripting`). - vite.config.ts: OXC minification with keepNames; loadEnv-backed VITE_STAGEHAND_BROWSER_PROXY_URL that must be a loopback http(s) origin (same rules as the worker's parseBrowserProxyUrl) or the build fails; `stagehand-extension.metadata.json` sidecar (chromeExtensionId, extensionVersion, stagehandProtocolVersion, residentGatewayConfigured, sha256 of the ZIP, unpackedSha256 of dist, serviceWorkerPath, sourceCommit); stale artifacts are removed at buildStart and dist + artifacts are removed on a failed closeBundle; explicit `root`; exported `stagehandExtensionBuildConfig` factory for tests. - turbo.json: extension#build hashes VITE_STAGEHAND_BROWSER_PROXY_URL and the gitignored .env/.env.* files; outputs/inputs cover artifacts/**. GITHUB_SHA is passThroughEnv, not env, so it reaches the build (for `sourceCommit`) without invalidating the cache on every CI commit; vite.config.ts documents that Turbo may therefore replay byte-identical artifacts carrying an older sourceCommit. - Packaging gates fail closed on private or mismatched artifacts: tsdown verifies residentGatewayConfigured === false plus ZIP sha256 and unpacked dist digest; Python build.py verifies the flag and the unpacked digest; Go extensionpack verifies the flag and ZIP sha256. The ZIP and STAGEHAND_EXTENSION_ARCHIVE_PATH export are kept. - Tests: metadata schema + literal Chrome ID, independent digest recomputation, build-time URL validation, and two builds from identical inputs being byte-identical (and equal to the canonical ZIP); sdk-ts packaging helper tests; Python/Go gate tests; real-Chrome smoke that attaches a resident extension through a loopback proxy on a URL-less stagehand.init and reattaches on a second init. - Regenerated the embedded Go ZIP via `go run ./internal/extensionpack`.
91d03b8 to
1c1d6dd
Compare
745dc5d to
400822a
Compare
Summary
Stack D/5:
-a-transport→-b-target-safety→-c-gateway-runtime→feat/resident-main-d-artifacts(this PR) →-e-browserbase-opt-in. Supersedes #2466.Deterministic, identity-pinned extension artifacts for the Browserbase image:
manifest.jsonpins the publickey→ Chrome IDhgibfbbnmoigailpmgihnbaokiinpmij; permissions reduced todebugger+offscreen; version 1.0.1.vite.config.ts: OXC minification with names kept; deterministic ZIP (sorted entries, fixed mtime/attrs/compression); metadata sidecar withsha256(zip),unpackedSha256(content digest ofdist),sourceCommit,chromeExtensionId,extensionVersion,stagehandProtocolVersion,residentGatewayConfigured.VITE_STAGEHAND_BROWSER_PROXY_URLmust be a loopback http(s) origin (fail closed); private builds never enter public packaging: tsdown, the Python build and Goextensionpackall refuse a private or digest-mismatcheddist..env*andVITE_STAGEHAND_BROWSER_PROXY_URLare hashed;GITHUB_SHAispassThroughEnv(reaches the build forsourceCommit, not hashed — hashing it made every CI commit a cache miss for the extension build and everything downstream).sourceCommittherefore means "commit whose inputs produced these bytes"; pin on the hashes for byte identity.STAGEHAND_EXTENSION_ARCHIVE_PATH(previews/evals/eve consume it). Regenerates the Go embedded zip. Two builds from identical inputs are asserted byte-identical.The private artifact for the Core image (#10960) is built from the stack tip with
VITE_STAGEHAND_BROWSER_PROXY_URL=http://127.0.0.1:9224.Validation
pnpm check; extension tests (metadata schema, literal Chrome ID, independent digest recomputation, double-build identity); sdk-ts unit + package contract;extensionpack --check; Python build tests; real-Chrome protocol smoke — green.Summary by cubic
Deterministic, identity‑pinned Stagehand extension artifacts with SDK packaging gated on build provenance. Previously the extension ID and bytes could vary and packaging didn’t verify origin; now the manifest pins a public key (Chrome ID hgibfbbnmoigailpmgihnbaokiinpmij), builds are reproducible, and private or mismatched artifacts fail packaging.
key; permissions reduced to ["debugger","offscreen"].vite(OXC with keepNames) emits a deterministic ZIP plusstagehand-extension.metadata.json(chromeExtensionId, extensionVersion, stagehandProtocolVersion, residentGatewayConfigured, sha256, unpackedSha256, serviceWorkerPath, sourceCommit). ValidatesVITE_STAGEHAND_BROWSER_PROXY_URLas loopback http(s); build fails otherwise.turbo.jsonhashes.env*andVITE_STAGEHAND_BROWSER_PROXY_URL, treatsGITHUB_SHAas pass‑through (provenance without cache invalidation), and outputsartifacts/**.sdk-tsverifies residentGatewayConfigured === false and both archive/unpacked digests;sdk-pythonverifies public metadata and unpacked digest;sdk-goverifies public metadata and ZIP sha256; embedded ZIP refreshed. Keeps the ZIP andSTAGEHAND_EXTENSION_ARCHIVE_PATH.Rollout and reviewer notes
VITE_STAGEHAND_BROWSER_PROXY_URLis non‑loopback or includes path/credentials/query/fragment.Written for commit 400822a. Summary will update on new commits.