Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ jobs:
- name: Checkout NodeRoom
uses: actions/checkout@v4
with:
ref: ${{ github.event.pull_request.head.sha || github.sha }}
path: noderoom

- name: Resolve immutable NodeSlide producer
Expand Down Expand Up @@ -115,12 +116,15 @@ jobs:
NODESLIDE_PACKAGE_ARTIFACT: ${{ runner.temp }}/nodeslide-packages
run: npm run nodeslide:consumer:proof

- name: Verify immutable mounted release and exact producer checkout
- name: Verify immutable mounted release, exact producer, and full mounted journey
working-directory: noderoom
env:
NODESLIDE_ROOT: ${{ github.workspace }}/nodeslide
run: npm run nodeslide:mounted:release:proof
run: npm run nodeslide:mounted:release:proof -- --json-out nodeslide-mounted-release-proof.json

- name: Run smallest mounted Memory and Convex journeys
working-directory: noderoom
run: npm test -- --run tests/nodeSlideMountedMemoryJourney.test.ts tests/nodeSlideMountedConvexJourney.test.ts tests/nodeSlideStudioMount.test.tsx
- name: Upload mounted release receipt
uses: actions/upload-artifact@v4
with:
name: nodeslide-mounted-release-${{ github.run_id }}
path: noderoom/nodeslide-mounted-release-proof.json
if-no-files-found: error
9 changes: 9 additions & 0 deletions artifacts/nodeslide-mounted-ui-proof-20260720/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Mounted NodeSlide UI proof — 2026-07-20 PDT

This folder is the literal browser-camera companion to the deterministic mounted-component journey. It records a real NodeRoom memory sample because the live Convex deployment correctly rejected the disposable unauthenticated sample with `production_identity_required`; no identity check was bypassed.

- `mounted-artifact-command-surface-clean.jpg` shows the persisted `Q3 diligence readout` after editing `Capture Notebook — mounted v0.2.2`, clicking **Make live**, reopening the artifact, and observing the mounted NodeSlide command controls inside the real NodeRoom Artifacts surface.
- `memory-write-fails-closed-clean.jpg` shows the same mounted surface after **Apply title through NodeSlide** correctly refused a write without a live verified room session.
- `receipt.json` records the a11y/DOM assertions, camera classification, console result, and SHA-256 digests.

The camera proves the literal mounted React surface and its fail-closed memory behavior. ActorProof, membership, request-digest binding, exact-byte acceptance, grant replay rejection, durable proposal acceptance, reload, presenter/PPTX, and credential-free receipts are proved separately by the deterministic Convex and repository journeys named in `receipt.json`.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
74 changes: 74 additions & 0 deletions artifacts/nodeslide-mounted-ui-proof-20260720/receipt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
{
"schema": "noderoom.nodeslide-mounted-ui-proof/v1",
"capturedAt": "2026-07-21T02:16:14.6459902Z",
"capturedOnLocalDate": "2026-07-20",
"route": "http://127.0.0.1:5287/?demo=1&name=CodexUI&surface=desktop",
"acceptanceClass": "literal-browser-a11y",
"camera": {
"authorityMode": "memory",
"authenticatedConvexCamera": false,
"authenticatedConvexDeterministicProof": true,
"reason": "The live Convex deployment correctly rejected the disposable unauthenticated sample with production_identity_required; no identity check was bypassed."
},
"journey": [
"Open the real NodeRoom Artifacts surface.",
"Open the generated Q3 diligence readout deck.",
"Edit the first slide title to Capture Notebook — mounted v0.2.2.",
"Persist it with Make live.",
"Reopen the persisted deck artifact.",
"Observe the literal mounted NodeSlide region, package boundary, and command controls.",
"Attempt the mounted title command without a live identity and observe the fail-closed alert."
],
"accessibleTree": {
"region": "NodeSlide studio mounted in NodeRoom",
"workbench": "Deck storyboard workbench",
"deckHeading": "Q3 diligence readout",
"persistedSlideHeading": "Capture Notebook — mounted v0.2.2",
"controls": [
"NodeSlide title command",
"Apply title through NodeSlide",
"NodeSlide purpose proposal",
"Propose purpose for review"
],
"failClosedAlert": "The mounted NodeSlide write path requires a live, verified room session."
},
"domAttributes": {
"aria-label": "NodeSlide studio mounted in NodeRoom",
"data-nodeslide-package-version": "0.2.2",
"data-nodeslide-authority": "noderoom-artifact-cas",
"data-nodeslide-surface": "noderoom-deck-storyboard"
},
"browserConsole": {
"localApplicationErrors": 0,
"localApplicationWarnings": 0,
"excludedExtensionEntries": 10,
"observation": "A fresh tab completed the full camera journey and remained stable for eight seconds after reopen. Extension-origin entries were excluded from the local application count."
},
"screenshots": [
{
"file": "mounted-artifact-command-surface-clean.jpg",
"mediaType": "image/jpeg",
"width": 2560,
"height": 1431,
"bytes": 279084,
"sha256": "0a0022343073ff4a6edf506ba0268f97591118bb41214bdcea87974058b79ac7"
},
{
"file": "memory-write-fails-closed-clean.jpg",
"mediaType": "image/jpeg",
"width": 2560,
"height": 1431,
"bytes": 275492,
"sha256": "6fc76fec4ffa1920baeeb3a0d56afe93e0ed016174865ac7e4a7ea05274ec409"
}
],
"deterministicProof": {
"script": "scripts/nodeslide-mounted-release-proof.ts",
"isolatedComponentJourney": "tests/nodeSlideMountedIsolatedComponentJourney.test.ts",
"memoryJourney": "tests/nodeSlideMountedMemoryJourney.test.ts",
"convexJourney": "tests/nodeSlideMountedConvexJourney.test.ts",
"reactMountJourney": "tests/nodeSlideStudioMount.test.tsx",
"producerReleaseRun": "https://github.com/HomenShum/NodeSlide/actions/runs/29787121559",
"producerReleaseTag": "https://github.com/HomenShum/NodeSlide/releases/tag/v0.2.2"
}
}
148 changes: 147 additions & 1 deletion convex/nodeslideHost.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
import { v } from "convex/values";
import {
NODESLIDE_COMPONENT_GRANT_VERSION,
nodeSlideComponentPatchDigest,
type NodeSlideComponentGrant,
} from "@nodeslide/convex/component";
import type { FunctionReference } from "convex/server";
import type { MutationCtx, QueryCtx } from "./_generated/server";
import { internalQuery, mutation, query } from "./_generated/server";
import { internalMutation, internalQuery, mutation, query } from "./_generated/server";
import { components } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import { applyCellEditCore, resolveProposalCore } from "./artifacts";
import { actorProofV, requireActorProof, requireArtifactInRoom, sha256Hex, type ActorValue } from "./lib";
Expand Down Expand Up @@ -34,6 +41,23 @@ const mountedDeckArgs = {
requester: actorProofV,
};

// The packaged component carries its exact repository declarations, but the
// current Convex app codegen narrows this mounted property to `{}`. Keep the
// runtime reference from generated `components` and spell the three consumed
// functions structurally until the upstream codegen preserves that shape.
const nodeSlideComponentRepository = (components.nodeslide as unknown as {
repository: {
initializeDeck: FunctionReference<"mutation", "public", {
snapshot: unknown;
grant: NodeSlideComponentGrant;
}, unknown, "nodeslide">;
getDeck: FunctionReference<"query", "public", {
deckId: string;
grant: NodeSlideComponentGrant;
}, unknown, "nodeslide">;
};
}).repository;

export const getMountedDeck = query({
args: mountedDeckArgs,
handler: async (ctx, args) => {
Expand Down Expand Up @@ -315,6 +339,62 @@ export const storeMountedReceipt = mutation({
},
});

/**
* Server-only bridge into the package-owned isolated component namespace.
* NodeRoom authenticates ActorProof and room membership before minting the
* one-time component grant; the component never receives the ActorProof.
*/
export const initializeMountedComponent = internalMutation({
args: mountedDeckArgs,
handler: async (ctx, args) => {
const deck = await loadDeckArtifact(ctx, args.roomId, args.artifactId);
const translated = translateNodeRoomArtifactToNodeSlide(deck);
const grant = await componentGrantForAuthorizedRequest(ctx, {
...args,
action: "deck.initialize",
resource: { kind: "deck", id: String(args.artifactId) },
recordEvidence: true,
});
const snapshot = await ctx.runMutation(nodeSlideComponentRepository.initializeDeck, {
snapshot: translated.snapshot,
grant,
});
return {
snapshot,
translationReceipt: translated.receipt,
authorization: componentAuthorizationReceipt(grant),
};
},
});

export const issueMountedComponentPatchGrant = internalQuery({
args: { ...mountedDeckArgs, patch: v.any() },
handler: async (ctx, args) => {
const patch = parsePatchCommand(args.patch);
return componentGrantForAuthorizedRequest(ctx, {
...args,
action: "patch.apply",
resource: { kind: "patch", id: patch.id },
patch,
});
},
});

export const getMountedComponentDeck = internalQuery({
args: mountedDeckArgs,
handler: async (ctx, args) => {
const grant = await componentGrantForAuthorizedRequest(ctx, {
...args,
action: "deck.read",
resource: { kind: "deck", id: String(args.artifactId) },
});
return ctx.runQuery(nodeSlideComponentRepository.getDeck, {
deckId: String(args.artifactId),
grant,
});
},
});

async function authorizeOperation(
ctx: DbCtx,
args: {
Expand Down Expand Up @@ -367,6 +447,72 @@ async function authorizeOperation(
};
}

async function componentGrantForAuthorizedRequest(
ctx: DbCtx,
args: {
roomId: Id<"rooms">;
artifactId: Id<"artifacts">;
requester: Requester;
action: "deck.initialize" | "deck.read" | "patch.apply";
resource: NodeSlideComponentGrant["resource"];
patch?: NodeRoomNodeSlidePatchCommand;
recordEvidence?: boolean;
},
): Promise<NodeSlideComponentGrant> {
if (args.action === "patch.apply" && !args.patch) {
throw new Error("nodeslide_component_patch_required");
}
const authorization = await authorizeOperation(ctx, {
roomId: args.roomId,
artifactId: args.artifactId,
requester: args.requester,
// Component initialization is a host-only write. Reuse the same strongest
// NodeRoom write-policy decision without pretending it is a repository
// receipt action understood by older NodeSlide consumers.
action: args.action === "deck.read" ? "deck.read" : "patch.apply",
resourceId: args.resource.id,
recordEvidence: args.recordEvidence,
});
const requestDigest = args.patch
? await nodeSlideComponentPatchDigest(args.patch)
: undefined;
const authorizedAt = Date.now();
const grantFingerprint = await sha256Hex([
authorization.principal.userId,
String(args.artifactId),
args.action,
args.resource.kind,
args.resource.id,
requestDigest ?? "no-request-digest",
authorization.evidence.evidenceId ?? "no-evidence-id",
String(authorizedAt),
].join("\n"));
return {
schemaVersion: NODESLIDE_COMPONENT_GRANT_VERSION,
id: `grant:noderoom:${grantFingerprint}`,
principalId: authorization.principal.userId,
deckId: String(args.artifactId),
action: args.action,
resource: args.resource,
...(requestDigest ? { requestDigest } : {}),
authorizedAt,
evidence: authorization.evidence,
};
}

function componentAuthorizationReceipt(grant: NodeSlideComponentGrant) {
return {
schemaVersion: "nodeslide.authorization/v1" as const,
id: grant.id,
principalId: grant.principalId,
deckId: grant.deckId,
action: grant.action,
resource: grant.resource,
authorizedAt: grant.authorizedAt,
evidence: grant.evidence,
};
}

async function loadDeckArtifact(
ctx: DbCtx,
roomId: Id<"rooms">,
Expand Down
36 changes: 28 additions & 8 deletions docs/NEXT_SESSION.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ producer is NodeSlide main commit
[`v0.2.2`](https://github.com/HomenShum/NodeSlide/releases/tag/v0.2.2). The
public clean-install, immutable-upgrade, exact-rebuild, tamper, and mixed-release
proof passed in [run 29787121559](https://github.com/HomenShum/NodeSlide/actions/runs/29787121559).
The release is bound to annotated tag object
`ec4870300e1ad7ddd74209aada3a47a26779b4bb`.

## Shipped integration

Expand All @@ -23,13 +25,25 @@ proof passed in [run 29787121559](https://github.com/HomenShum/NodeSlide/actions
- `NodeRoomNodeSlideStudioMount.tsx` mounts the packed
`@nodeslide/react` controlled shell inside the real deck workbench. Members
can propose; only hosts can patch or decide.
- `artifacts/nodeslide-mounted-ui-proof-20260720/` contains the literal browser
camera and a11y receipt for the real Artifacts surface. It proves edit,
persistence via **Make live**, reopen, the 0.2.2 mounted DOM boundary, command
controls, and fail-closed memory behavior with zero local application console
errors or warnings on the final fresh-tab pass.
- The memory journey runs a real NodeAgent tool call, host review, competing
stale proposal, repository reconstruction, presenter/PPTX generation, archive
reopen, snapshot revalidation, and credential-free receipt checks.
- The Convex journey proves the same authorization, CAS, proposal, reload,
version-history, room-activity, and receipt behavior on durable tables.
- The isolated-component journey starts from real NodeRoom ActorProof and room
membership, initializes the package namespace, computes the canonical patch
digest, rejects substituted bytes without consuming the grant, accepts the
exact command, rejects replay, and proves `requestDigest` never enters a
NodeRoom receipt. It asserts semantic parity because NodeRoom intentionally
re-derives brief/source timestamps and owns a separate slide-object clock.
- `vendor/nodeslide/release-lock.json` binds the complete 11-package v0.2.2 set,
the 0.1.0 to 0.2.2 upgrade receipt, and all package/manifest digests to exact
the public 0.1.0 to 0.2.2 upgrade receipt, and all package/manifest digests to
exact
NodeSlide main. `package-lock.json` integrity-pins the six runtime packages.
- CI reads that lock, checks out the exact NodeSlide commit, runs the legacy
portable consumer proof, verifies the complete immutable release, and runs
Expand All @@ -42,7 +56,7 @@ npm run typecheck
npm run design:audit
npm run nodeagent:frame:smoke
npm run omnigent:nodeagent:smoke
npm test -- --run tests/nodeSlideMountedMemoryJourney.test.ts tests/nodeSlideMountedConvexJourney.test.ts tests/nodeSlideStudioMount.test.tsx
npm test -- --run tests/nodeSlideMountedMemoryJourney.test.ts tests/nodeSlideMountedConvexJourney.test.ts tests/nodeSlideMountedIsolatedComponentJourney.test.ts tests/nodeSlideStudioMount.test.tsx
$env:NODESLIDE_ROOT = "D:\path\to\NodeSlide-at-a88fb57f"
npm run nodeslide:mounted:release:proof
npm run floor
Expand All @@ -51,16 +65,22 @@ npm run prod:gate

The mounted release proof is fail-closed on manifest/proof digests, every
tarball digest, lockstep versions, NodeRoom's package-lock integrities, package
exports, component governance/grant exports, and the exact producer checkout.
exports, component governance/grant exports, fresh isolated-component
digest/replay behavior, the full mounted NodeRoom journey, and the exact
producer checkout. CI uploads the JSON receipt for the exact tested SHA.

## Honest boundary

This closes the NodeSlide I4 host-authorizer work and the repository, runtime,
package, and CI portions of I7/I8. The tests prove the mounted React boundary and
the product workbench's command wiring; they are not a recorded real-browser
accessibility/camera acceptance. If that stronger evidence is requested, capture
it as a separate browser proof without weakening or relabeling the deterministic
journeys above.
package, isolated-component, literal mounted-UI, and CI portions of I7/I8.
`v0.2.0` (pre-final review) and `v0.2.1` (cross-platform byte-rebuild mismatch)
remain immutable, superseded history and must not be rebound. The camera proof
is deliberately classified as a memory-room browser pass: the live Convex
deployment correctly rejected a disposable unauthenticated sample with
`production_identity_required`, and no identity check was bypassed. Do not call
the screenshots an authenticated production journey; use the deterministic
Convex and isolated-component tests for ActorProof, authorization, digest,
replay, durability, and receipt claims.

Leave unrelated `.qa/` and `.proofloop/` state uncommitted. Before changing
NodeAgent, continue to run both required smokes from `AGENTS.md`.
Loading
Loading