Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2478eddcfd | ||
|
|
3f4c8e8fd9 |
No files matched your search
@@ -740,10 +740,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
// 4.5 Bus wiring — construct the `WebviewBridge` around the HUD's
|
||||
// `WKWebView`, install the `WKUserScript` at document-start in
|
||||
// the isolated `JarvisBusWorld`, start the single-writer
|
||||
// `HudStateCoordinator`, and load the R3F bundle's
|
||||
// `index.html` so the handshake kicks off. See `installBus`
|
||||
// for the seven-step sequence.
|
||||
// the page world (the shared `WKContentWorld.page` used by
|
||||
// Injection.js and the HUD bundle alike since `fb41c5f`),
|
||||
// start the single-writer `HudStateCoordinator`, and load the
|
||||
// R3F bundle's `index.html` so the handshake kicks off. See
|
||||
// `installBus` for the seven-step sequence.
|
||||
installBus()
|
||||
|
||||
// 5. Keychain probe. Banner suppression: when the API key is
|
||||
|
||||
@@ -209,6 +209,7 @@ bash scripts/check-corpus-secrets.sh
|
||||
bash scripts/check-embedding-dim-literal.sh
|
||||
bash scripts/check-install-order.sh
|
||||
bash scripts/check-no-evaluate-javascript.sh
|
||||
bash scripts/check-no-jarvis-bus-world-references.sh
|
||||
bash scripts/check-no-leftover-stubs.sh
|
||||
bash scripts/check-no-modal-presentation.sh
|
||||
bash scripts/check-no-null-voice-adapters.sh
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Injected at document-start in WKContentWorld("JarvisBusWorld").
|
||||
// Installs window.jarvisBus with the minimum surface needed to complete
|
||||
// the handshake.
|
||||
// Injected at document-start in the page world (the default
|
||||
// `WKContentWorld.page` since `fb41c5f` retired the prior isolated named
|
||||
// world; see WebviewBridge.swift). Installs window.jarvisBus with the
|
||||
// minimum surface needed to complete the handshake.
|
||||
//
|
||||
// IMPORTANT — protocolVersion drift hazard: the full TS bundle's installer
|
||||
// (Vy() in the minified output) shape-checks window.jarvisBus and SKIPS
|
||||
|
||||
@@ -6,8 +6,9 @@ import JarvisLogging
|
||||
/// Main-actor-bound bridge between Swift and the webview's JS layer.
|
||||
///
|
||||
/// Responsibilities:
|
||||
/// - Install a `WKScriptMessageHandlerWithReply` in an isolated content world
|
||||
/// (`JarvisBusWorld`) so page scripts cannot observe or post to it.
|
||||
/// - Install a `WKScriptMessageHandlerWithReply` on the page world
|
||||
/// (`WKContentWorld.page`, the default since `fb41c5f`) so page scripts
|
||||
/// in the loaded HUD bundle can address it via `window.webkit.messageHandlers.jarvisBus`.
|
||||
/// - Inject a `WKUserScript` at `.atDocumentStart` in the same world so
|
||||
/// `window.jarvisBus` exists before any page script runs (pitfall 3).
|
||||
/// - Decode inbound JSON into `BusInbound` and either (a) handle the handshake
|
||||
|
||||
@@ -15,7 +15,7 @@ import WebKit
|
||||
/// **Why this exists** — Phase D-Phase-2 / Phase F1 of
|
||||
/// `.planning/AUDIT-AND-FIX-PLAN.md`. The 2026-05-02 audit-and-fix
|
||||
/// session uncovered a `WKContentWorld` isolation bug
|
||||
/// (`Injection.js` ran in `WKContentWorld(name: "JarvisBusWorld")`
|
||||
/// (`Injection.js` ran in a prior isolated named content world
|
||||
/// while the bundle ran in the page world; `window.jarvisBus`
|
||||
/// properties were per-world isolated; bundle's `attachBus()` saw
|
||||
/// `undefined` and created a SECOND fresh bus object; two stubs, two
|
||||
@@ -127,13 +127,13 @@ final class RealWKWebViewIntegrationTests: XCTestCase {
|
||||
|
||||
/// **The test that would have caught the WKContentWorld bug.**
|
||||
///
|
||||
/// On the bug branch, `Injection.js` ran in
|
||||
/// `WKContentWorld(name: "JarvisBusWorld")` while
|
||||
/// `WebviewBridge.sendRaw` invoked `callAsyncJavaScript` against
|
||||
/// the same named world — but the message *handler* was registered
|
||||
/// against `WKContentWorld.page`, so JS-side `postMessage` calls
|
||||
/// from the named-world stub never reached Swift. Handshake hung
|
||||
/// at `.sentHello`, eventually timed out with an `NSAlert`.
|
||||
/// On the bug branch, `Injection.js` ran in the prior named
|
||||
/// content world while `WebviewBridge.sendRaw` invoked
|
||||
/// `callAsyncJavaScript` against the same named world — but the
|
||||
/// message *handler* was registered against `WKContentWorld.page`,
|
||||
/// so JS-side `postMessage` calls from the named-world stub never
|
||||
/// reached Swift. Handshake hung at `.sentHello`, eventually timed
|
||||
/// out with an `NSAlert`.
|
||||
///
|
||||
/// On the fixed branch (fb41c5f), Injection.js auto-acks `hello`
|
||||
/// at the stub layer in the unified `.page` world, the bridge's
|
||||
|
||||
@@ -78,8 +78,8 @@ final class WebviewBridgeOutboundTests: XCTestCase {
|
||||
.decode(BusOutbound.self, from: Data((payload ?? "").utf8))
|
||||
XCTAssertEqual(decoded, .hudState(.idle))
|
||||
// F-A2-01: production WebviewBridge uses `WKContentWorld.page` (the
|
||||
// default since `fb41c5f`); the prior `JarvisBusWorld` named-world
|
||||
// assertion was retired in that commit but this test wasn't updated.
|
||||
// default since `fb41c5f`); the prior named-world assertion was
|
||||
// retired in that commit but this test wasn't updated until #45.
|
||||
XCTAssertEqual(call.contentWorld, .page)
|
||||
}
|
||||
|
||||
|
||||
@@ -160,11 +160,14 @@ final class WebviewBridgeTests: XCTestCase {
|
||||
// Re-adding a handler with the same (contentWorld, name) pair would
|
||||
// raise `NSInvalidArgumentException`. The constructor succeeding is
|
||||
// itself evidence of registration. As a double-check, `remove` on a
|
||||
// registered handler must succeed without throwing.
|
||||
// registered handler must succeed without throwing. We remove from
|
||||
// `WKContentWorld.page` because that is the default world
|
||||
// `WebviewBridge.init` registers the handler against since `fb41c5f`
|
||||
// unified bus and bundle into the same page world.
|
||||
let controller = webView.configuration.userContentController
|
||||
controller.removeScriptMessageHandler(
|
||||
forName: "jarvisBus",
|
||||
contentWorld: WKContentWorld.world(name: "JarvisBusWorld")
|
||||
contentWorld: .page
|
||||
)
|
||||
// No XCTAssert needed — the remove call succeeding on a registered
|
||||
// handler is the assertion.
|
||||
|
||||
@@ -12,7 +12,7 @@ import Replay
|
||||
|
||||
/// Test seam over `Replay.ReplayLog`. The Memory module deliberately does NOT
|
||||
/// depend on a concrete `ReplayLog` instance — `AppDelegate.installMemory`
|
||||
/// (Plan 07-06) hands a wrapper through `setReplayLog`. Tests inject a
|
||||
/// hands a wrapper through `setReplayLog` at startup. Tests inject a
|
||||
/// mock conforming to this protocol.
|
||||
public protocol MemoryReplaySink: Sendable {
|
||||
func record(_ event: ReplayEvent, forTriggerTurnId triggerTurnId: Int64)
|
||||
@@ -34,8 +34,10 @@ public protocol MemoryReplaySink: Sendable {
|
||||
/// memory store the app refuses to launch. This eliminates the silent
|
||||
/// dormancy that hid B-02 / B-04-class substrate failures.
|
||||
///
|
||||
/// The actor is the only writer. Plan 07-02 wires the bounded extraction
|
||||
/// channel; Plan 07-03 wires the search path + ReplayEvent emission.
|
||||
/// The actor is the only writer. The bounded extraction channel writes
|
||||
/// here via `applyOp`; the hybrid (FTS5 + vec0) search path reads through
|
||||
/// `MemoryReadStore` conformance and emits `ReplayEvent.memoryRetrieval`
|
||||
/// via `recordRetrieval`.
|
||||
public actor MemoryStore {
|
||||
|
||||
public static let logChannel = "memory"
|
||||
@@ -331,11 +333,11 @@ public actor MemoryStore {
|
||||
sink.record(.memoryMutation(bytes), forTriggerTurnId: triggerTurnId)
|
||||
}
|
||||
|
||||
// MARK: - Read path (Plan 07-03 — MEM-07 + TEXT-03 + D-02 + D-05)
|
||||
// MARK: - Read path (MEM-07 + TEXT-03 + D-02 + D-05)
|
||||
|
||||
/// SOLE production emission site for ReplayEvent.memoryRetrieval (D-05).
|
||||
/// Mirrors recordMemoryMutation from 07-02. Best-effort; if no replay
|
||||
/// sink has been injected, the call is a silent no-op.
|
||||
/// Mirrors `recordMemoryMutation` on the write path. Best-effort; if no
|
||||
/// replay sink has been injected, the call is a silent no-op.
|
||||
public func recordRetrieval(_ ref: FactRef, triggerTurnId: Int64) {
|
||||
guard let sink = replayLog else { return }
|
||||
let stamp: Int64 = ref.timestamp == 0
|
||||
@@ -538,13 +540,13 @@ public actor MemoryStore {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - HybridSearch + SessionHistory conformance (Plan 07-03 Task 2)
|
||||
// MARK: - HybridSearch + SessionHistory conformance
|
||||
|
||||
extension MemoryStore: MemoryReadStore {}
|
||||
extension MemoryStore: SessionHistoryReading {}
|
||||
extension OllamaEmbeddingClient: EmbeddingProviding {}
|
||||
|
||||
// MARK: - Plan 07-06 / Regression corpus test seams
|
||||
// MARK: - Regression corpus test seams
|
||||
//
|
||||
// These small read-only seams are exercised exclusively by the env-gated
|
||||
// MemoryRegressionCorpusTests suite (JARVIS_REAL_MODELS=1). They wrap raw
|
||||
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
#!/usr/bin/env bash
|
||||
#
|
||||
# check-no-jarvis-bus-world-references.sh
|
||||
#
|
||||
# Issue #45 boundary gate.
|
||||
#
|
||||
# The named `WKContentWorld("JarvisBusWorld")` was retired in `fb41c5f` —
|
||||
# the bus message handler and `Injection.js` user script now both register
|
||||
# against `WKContentWorld.page` (the default page world). Old doc-comments
|
||||
# and test fixtures hung onto the name for months afterward; #45 closed
|
||||
# the cleanup.
|
||||
#
|
||||
# This gate prevents new code or comments from re-introducing the literal
|
||||
# string `JarvisBusWorld` outside the historical reference corpora:
|
||||
#
|
||||
# - `.planning/` archived plans / audit reports / handoffs
|
||||
# - `docs/` spec documents that explain the migration
|
||||
# - `node_modules/` vendored deps (out of repo control anyway)
|
||||
# - `.build/`, build/ SwiftPM + Xcode derived data
|
||||
# - `webview/packages/*/dist/` TypeScript build output (regenerated from src)
|
||||
# - this script itself we have to reference the literal to grep for it
|
||||
# - `.git/` git internals
|
||||
#
|
||||
# Fails CI if any other tracked file contains the literal string.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(dirname "$SCRIPT_DIR")"
|
||||
|
||||
cd "$REPO_ROOT"
|
||||
|
||||
# Use `git grep` so we automatically respect .gitignore (so dist/ etc. that
|
||||
# aren't tracked don't trip the gate even if they exist on disk locally).
|
||||
# `--cached` would miss new working-tree files; default mode greps the
|
||||
# working tree filtered by tracked-or-untracked-not-ignored, which is what
|
||||
# we want before a commit.
|
||||
matches=$(git grep --no-color -n "JarvisBusWorld" -- \
|
||||
':!.planning/' \
|
||||
':!docs/' \
|
||||
':!scripts/check-no-jarvis-bus-world-references.sh' \
|
||||
':!webview/packages/*/dist/' \
|
||||
|| true)
|
||||
|
||||
if [ -n "$matches" ]; then
|
||||
echo "[check-no-jarvis-bus-world-references] FAIL: tracked source still references the retired 'JarvisBusWorld' named content world." >&2
|
||||
echo "" >&2
|
||||
echo "$matches" >&2
|
||||
echo "" >&2
|
||||
echo "[check-no-jarvis-bus-world-references] Rewrite these to 'page world' / 'shared world' / 'defensive double-install guard' — see #45 and the JSDoc in webview/packages/bus/src/bridge.ts for the canonical phrasing." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[check-no-jarvis-bus-world-references] PASS (no tracked source references the retired named world)"
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@jarvis/bus",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"main": "dist/index.js",
|
||||
"types": "dist/index.d.ts",
|
||||
|
||||
@@ -57,27 +57,28 @@ export interface InstallOptions {
|
||||
* - `send(msg)` JSON-encodes and posts to `webkit.messageHandlers.jarvisBus`.
|
||||
* Callers await the returned Promise to observe Swift's reply.
|
||||
*
|
||||
* ## WKContentWorld attach-to-existing path (H-02)
|
||||
* ## Defensive double-install guard (H-02)
|
||||
*
|
||||
* When the HUD bundle runs in the page's default JS world (via
|
||||
* `<script type="module" src="…">`), `webkit.messageHandlers.jarvisBus` is
|
||||
* NOT visible — Swift registered the handler `in: JarvisBusWorld` and
|
||||
* WebKit isolates message handlers per content world. However,
|
||||
* `window.jarvisBus` is a property on the (cross-world-shared) `window`
|
||||
* object, so the bus instance installed by `Injection.js` inside
|
||||
* `JarvisBusWorld` IS observable from the default world.
|
||||
* Both `Injection.js` (a `WKUserScript` injected at `.atDocumentStart`) and
|
||||
* this bundle's `installJarvisBus()` run in the SAME page world — the
|
||||
* default `WKContentWorld.page` since `fb41c5f` retired the prior
|
||||
* isolated named world. Injection.js fires first and stamps a
|
||||
* minimal `window.jarvisBus` with `protocolVersion`, `_handler`, plus the
|
||||
* `send` / `receive` / `onOutbound` surface needed for the handshake.
|
||||
* The bundle's installer fires later (top-level side-effect of `main.tsx`),
|
||||
* and if we naively overwrote `window.jarvisBus` we would orphan
|
||||
* Injection.js's queued early messages and any handshake state already
|
||||
* captured against the original closure.
|
||||
*
|
||||
* If we detect a pre-existing `window.jarvisBus` with the expected
|
||||
* `send` + `onOutbound` + `receive` shape, we attach our options to it
|
||||
* and return it verbatim rather than overwriting it. This routes every
|
||||
* outbound `send()` through the JarvisBusWorld-captured message handler
|
||||
* reference and keeps `onOutbound` hooked to the same `_handler` slot the
|
||||
* Injection.js bus exposes. Without this branch the default-world bundle
|
||||
* replaces `window.jarvisBus` with a bus whose `send()` cannot reach
|
||||
* `webkit.messageHandlers.jarvisBus` and every outbound frame — including
|
||||
* the first `uiReady` — is lost.
|
||||
* The shape check below (`isJarvisBusShape`) is therefore a defensive
|
||||
* double-install guard inside one page world, not a cross-world attach.
|
||||
* When we detect a pre-existing `window.jarvisBus` with the canonical
|
||||
* `send` + `onOutbound` + `receive` surface, we return early and leave
|
||||
* Injection.js's instance in place. Outbound `send()` calls still reach
|
||||
* `window.webkit.messageHandlers.jarvisBus` because that handler lives on
|
||||
* the same page world too.
|
||||
*
|
||||
* bus-harness.html (Phase 2) loads its marker script in a page where
|
||||
* `bus-harness.html` (Phase 2) loads its marker script in a page where
|
||||
* Injection.js has installed the same bus; the harness never called `send()`
|
||||
* so the regression wasn't observed until Phase 3. For that path this
|
||||
* function still returns the pre-installed bus, which is exactly what the
|
||||
@@ -93,10 +94,11 @@ export function installJarvisBus(options: InstallOptions = {}): () => void {
|
||||
console.error("[bus] decode failed:", error, raw);
|
||||
});
|
||||
|
||||
// H-02: attach-to-existing path. Injection.js in JarvisBusWorld installs a
|
||||
// minimal bus with the same surface; if it's already on window we MUST NOT
|
||||
// replace it (that would orphan webkit.messageHandlers.jarvisBus, which is
|
||||
// only visible inside JarvisBusWorld).
|
||||
// H-02: defensive double-install guard. Injection.js runs first (at
|
||||
// document-start in the same page world) and stamps a minimal bus with the
|
||||
// same surface; if it's already on `window`, we MUST NOT replace it — doing
|
||||
// so would orphan Injection.js's queued early messages and captured handshake
|
||||
// state. See the class doc above for the full rationale.
|
||||
const existing: unknown =
|
||||
typeof window !== "undefined"
|
||||
? (window as unknown as { jarvisBus?: unknown }).jarvisBus
|
||||
@@ -156,7 +158,7 @@ export function installJarvisBus(options: InstallOptions = {}): () => void {
|
||||
/**
|
||||
* Duck-type guard for a pre-existing `window.jarvisBus`. Matches the shape
|
||||
* installed by `packages/Bus/Sources/Bus/Resources/Injection.js` at
|
||||
* document-start in JarvisBusWorld. Intentionally narrow: if the shape
|
||||
* document-start in the page world. Intentionally narrow: if the shape
|
||||
* doesn't match (e.g. a left-over test stub), we fall through to the
|
||||
* fresh-install branch and overwrite it.
|
||||
*/
|
||||
|
||||
@@ -74,14 +74,13 @@ describe("installJarvisBus", () => {
|
||||
expect(b).toHaveBeenCalledOnce();
|
||||
});
|
||||
|
||||
// H-02: the HUD bundle (`main.tsx`) runs in the default JS world, but
|
||||
// Swift registered `webkit.messageHandlers.jarvisBus` in JarvisBusWorld,
|
||||
// so a default-world `send()` cannot reach the message handler. The
|
||||
// Injection.js-installed bus in JarvisBusWorld CAN reach it (it captured
|
||||
// the reference at document-start inside that world), and because
|
||||
// `window.jarvisBus` is a cross-world-shared property the default world
|
||||
// observes the Injection.js bus. Therefore `installJarvisBus()` must
|
||||
// attach to — not replace — a pre-existing bus.
|
||||
// H-02: both `Injection.js` (`WKUserScript` at document-start) and this
|
||||
// bundle's `installJarvisBus()` run in the SAME page world since `fb41c5f`
|
||||
// retired the prior isolated named content world. Injection.js fires first
|
||||
// and stamps a minimal `window.jarvisBus`; if `installJarvisBus()` naively
|
||||
// overwrote it, the early handshake state and queued messages captured
|
||||
// against the original closure would be orphaned. Therefore the bundle's
|
||||
// installer must attach to — not replace — a pre-existing bus.
|
||||
describe("H-02 attach-to-existing (pre-installed window.jarvisBus)", () => {
|
||||
it("returns the pre-existing bus unchanged when one is already installed", () => {
|
||||
const preExistingSend = vi.fn().mockResolvedValue(undefined);
|
||||
@@ -96,9 +95,10 @@ describe("installJarvisBus", () => {
|
||||
// @ts-expect-error — test shim: seed a pre-installed bus shape matching Injection.js
|
||||
globalThis.window = {
|
||||
jarvisBus: preExisting,
|
||||
// Note: webkit is intentionally absent — in real JarvisBusWorld the
|
||||
// pre-existing bus captured webkit at install time; the default world
|
||||
// never sees webkit. The fix MUST NOT touch the pre-existing bus.
|
||||
// Note: webkit is intentionally absent — Injection.js's pre-existing
|
||||
// bus captures a reference to `window.webkit` at install time, and the
|
||||
// shape guard must trust the pre-existing bus to route through that
|
||||
// captured reference rather than re-resolving `webkit` here.
|
||||
};
|
||||
|
||||
installJarvisBus();
|
||||
@@ -110,7 +110,7 @@ describe("installJarvisBus", () => {
|
||||
expect(window.jarvisBus.onOutbound).toBe(preExistingOnOutbound);
|
||||
});
|
||||
|
||||
it("routes send() through the pre-existing bus's send (reaches JarvisBusWorld-captured handler)", async () => {
|
||||
it("routes send() through the pre-existing bus's send (reaches Injection.js-captured handler)", async () => {
|
||||
const preExistingSend = vi.fn().mockResolvedValue({ ok: true });
|
||||
const preExisting = {
|
||||
protocolVersion: "2.0.0",
|
||||
|
||||
@@ -5,15 +5,15 @@ import { attachBus } from './bus/client'
|
||||
import './theme/tokens.css'
|
||||
|
||||
// `attachBus()` runs in the page's default JS world (this module loads via
|
||||
// `<script type="module" src="…">` in index.html). Swift installed the real
|
||||
// `window.jarvisBus` inside `JarvisBusWorld` via Injection.js — WebKit
|
||||
// content worlds isolate `webkit.messageHandlers`, so the default-world
|
||||
// bundle can't reach them directly. `window.jarvisBus` IS visible though,
|
||||
// because WebKit keeps `window` as one cross-world object (only *variables*
|
||||
// are world-scoped). The H-02 fix in `@jarvis/bus` detects the pre-existing
|
||||
// `window.jarvisBus` and attaches to it instead of replacing it, which
|
||||
// routes every outbound `send()` back through the JarvisBusWorld-captured
|
||||
// message handler reference. See `packages/bus/src/bridge.ts` for details.
|
||||
// `<script type="module" src="…">` in index.html). Swift's
|
||||
// `WebviewBridge.installScriptHandlers` (since `fb41c5f`) registers the
|
||||
// message handler on the same `WKContentWorld.page`, and Injection.js — also
|
||||
// in the page world — has already stamped a minimal `window.jarvisBus` at
|
||||
// document-start. `installJarvisBus()` here detects that pre-existing
|
||||
// instance via a duck-type shape guard and returns early, leaving the
|
||||
// Injection.js bus untouched so its captured handshake state and queued
|
||||
// early messages are preserved. See `packages/bus/src/bridge.ts` for the
|
||||
// full defensive-double-install rationale.
|
||||
attachBus()
|
||||
|
||||
const rootEl = document.getElementById('root')
|
||||
|
||||
Reference in new issue
Block a user