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
18 changes: 14 additions & 4 deletions .github/workflows/release-mtmcanvas.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ concurrency:

env:
PACKAGE_NAME: mtmcanvas
PACKAGE_DIR: packages/mtmcanvas
TAG_PREFIX: mtmcanvas-v

jobs:
Expand Down Expand Up @@ -63,7 +62,7 @@ jobs:
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
printf 'PACKAGE_VERSION=%s\n' "$package_version" >> "$GITHUB_ENV"

- name: Check mtmcanvas
- name: Check mtmcanvas browser extension
run: pnpm --filter mtmcanvas run check

- name: Pack release tarball
Expand All @@ -79,8 +78,19 @@ jobs:
printf 'tarball=%s\n' "$tarball" >> "$GITHUB_OUTPUT"
printf 'LOCAL_INTEGRITY=%s\n' "$local_integrity" >> "$GITHUB_ENV"

- name: Verify tarball contract
run: node "$PACKAGE_DIR/scripts/verify-package.mjs" "${{ steps.pack.outputs.tarball }}"
- name: Verify browser extension tarball
run: |
set -euo pipefail
entries="$(tar -tzf "${{ steps.pack.outputs.tarball }}")"
! grep -Eq '^package/(cordis\.patch\.yml|lib/(index|client)\.cjs)$' <<<"$entries"
grep -qx 'package/lib/client.js' <<<"$entries"
unpack="$RUNNER_TEMP/mtmcanvas-unpack"
mkdir -p "$unpack"
tar -xzf "${{ steps.pack.outputs.tarball }}" -C "$unpack"
for declaration in package/lib/types/index.d.ts package/lib/types/client/index.d.ts; do test -f "$unpack/$declaration"; done
export UNPACK_ROOT="$unpack"
node --input-type=module -e 'import fs from "node:fs"; import path from "node:path"; const root = process.env.UNPACK_ROOT; const pkg = JSON.parse(fs.readFileSync(path.join(root, "package/package.json"), "utf8")); if (pkg.dsh !== undefined || JSON.stringify(pkg.mtmharness?.secondary) !== JSON.stringify({ id: "mtmcanvas", apiVersion: 1, client: "./lib/client.js" }) || pkg.exports?.["."]?.import !== "./lib/client.js" || pkg.exports?.["./client"]?.import !== "./lib/client.js") process.exit(1);'
node --input-type=module -e 'import fs from "node:fs"; import path from "node:path"; const source = fs.readFileSync(path.join(process.env.UNPACK_ROOT, "package/lib/client.js"), "utf8"); if (!source.includes("export") || /^\s*import\s/m.test(source)) process.exit(1);'

- name: Lint package metadata
run: pnpm dlx publint run "${{ steps.pack.outputs.tarball }}" --strict
Expand Down
19 changes: 17 additions & 2 deletions .github/workflows/release-mtmharness.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,23 @@ jobs:
git merge-base --is-ancestor "$GITHUB_SHA" origin/main
printf 'PACKAGE_VERSION=%s\n' "$package_version" >> "$GITHUB_ENV"

- name: Check workspace
run: pnpm --filter mtmharness run check
- name: Check workspace and secondary client contract
run: |
set -euo pipefail
pnpm --filter mtmharness run check
pnpm --filter mtmcanvas run check
# The pinned Canvas artifact must already be published before mtmharness.
canvas_version="$(node -p "require('./packages/mtmcanvas/package.json').version")"
expected_integrity="$(sed -n 's/.*clientIntegrity: \"\([^\"]*\)\".*/\1/p' packages/mtmharness/src/features/secondary/manifest.ts)"
local_integrity="sha256-$(openssl dgst -sha256 -binary packages/mtmcanvas/lib/client.js | base64 -w0)"
test -n "$expected_integrity"
test "$local_integrity" = "$expected_integrity"
test "$(npm view "mtmcanvas@${canvas_version}" version --json | jq -r .)" = "$canvas_version"
remote="$RUNNER_TEMP/mtmcanvas-client.js"
curl --fail --silent --show-error --output "$remote" "https://unpkg.com/mtmcanvas@${canvas_version}/lib/client.js"
remote_integrity="sha256-$(openssl dgst -sha256 -binary "$remote" | base64 -w0)"
test "$remote_integrity" = "$expected_integrity"
cmp "packages/mtmcanvas/lib/client.js" "$remote"

- name: Pack release tarball
id: pack
Expand Down
29 changes: 10 additions & 19 deletions packages/mtmcanvas/README.md
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
# mtmcanvas

Experimental independent DSH Web Canvas plugin extracted from the Canvas domain currently hosted by `mtmharness`.
Experimental browser-only frontend extension for `mtmharness`.

The package publishes two DSH faces:
The package is not a standard DSH plugin: it has no `dsh` manifest or profile patch. `mtmharness` owns its runtime loading, version pin, integrity check, and lifecycle.

- Host half: `lib/index.js`, which mounts the existing file-backed Canvas RPC through DSH's abstract `ctx.fs` service.
- Browser half: `lib/client.cjs`, a `window.__ModuleLoader__.load` bundle that registers the Canvas sidebar action.
## Extension contract

This package is the independent Canvas implementation for the dynamic-loading experiment. The published `mtmharness` package no longer statically mounts Canvas; install both packages when the Canvas surface is required.
The package publishes one self-contained native ESM artifact at `lib/client.js`. It exports `mount(context)`, where `context` contains the extension id, version, an owned DOM root, the host `Document`, an `AbortSignal`, a cleanup-registration callback, and `apiVersion: 1`. The function returns an optional cleanup function. The extension does not receive DSH or Node.js internals.

## Install Into DSH Web
The first experiment keeps Canvas data in browser memory. File persistence and host capabilities are intentionally deferred until the frontend ABI is proven. The artifact runs in the host page; SHA-256 integrity identifies the reviewed bytes but does not sandbox the code.

```bash
dsh plugin --profile web add mtmcanvas
dsh --profile web --dump-config
## Use Through mtmharness

# Restart the Web profile after changing Bundle membership.
```
Install only `mtmharness` into the DSH Web profile. The Dynamic Canvas setting loads the exact Canvas artifact at runtime; it does not modify the profile or create another DSH Loader entry.

The package patch inserts the `mtmcanvas` Loader row. The official Web client module table discovers the `dsh.client` declaration and serves the exported `./client` bundle.

The separate `standalone/` client in `mtmharness` is not part of this plugin. It is the cloud multi-user client and does not receive the file-backed Host implementation.
The default experiment uses the published URL on unpkg, but the loader accepts any exact HTTPS static-host URL with CORS enabled. The host CSP must allow `connect-src` to the artifact origin and `script-src blob:` for the fetched ESM. The artifact is fetched, checked against its SHA-256 integrity value, imported as native ESM, and mounted into an owned root.

## Development

```bash
pnpm --filter mtmcanvas run check
pnpm --filter mtmcanvas run pack:check
```
pnpm --filter mtmcanvas run check

The browser artifact uses DSH's React singleton and dynamic module table. It does not create a React root, router, authentication flow, or local filesystem. `ctx.fs` is the Host capability boundary and can be backed by a remote implementation later.
The package metadata under `mtmharness.secondary` is consumed by the owning harness runtime. It is not a `dsh.bundle` or `dsh.client` declaration.
4 changes: 0 additions & 4 deletions packages/mtmcanvas/cordis.patch.yml

This file was deleted.

62 changes: 14 additions & 48 deletions packages/mtmcanvas/package.json
Original file line number Diff line number Diff line change
@@ -1,48 +1,37 @@
{
"name": "mtmcanvas",
"version": "0.1.0",
"description": "Experimental Canvas DSH Web plugin extracted from mtmharness.",
"version": "0.2.0",
"description": "Experimental browser-only frontend extension for mtmharness.",
"type": "module",
"engines": {
"node": ">=22.19.0",
"pnpm": ">=11.7.0"
},
"main": "./lib/index.js",
"main": "./lib/client.js",
"types": "./lib/types/index.d.ts",
"exports": {
".": {
"types": "./lib/types/index.d.ts",
"default": "./lib/index.js"
"import": "./lib/client.js",
"default": "./lib/client.js"
},
"./client": {
"types": "./lib/types/client/index.d.ts",
"default": "./lib/client.cjs"
"import": "./lib/client.js",
"default": "./lib/client.js"
},
"./cordis.patch.yml": "./cordis.patch.yml",
"./package.json": "./package.json"
},
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
},
"client": {
"platform": "web",
"inject": [
"@deepseek-ai/dsh-client-connection",
"@deepseek-ai/dsh-client-runtime",
"@deepseek-ai/dsh-client-ui-primitives",
"@deepseek-ai/dsh-client-ui-sidebar",
"@deepseek-ai/dsh-client-ui-layout",
"@deepseek-ai/dsh-client-ui-slots"
]
"mtmharness": {
"secondary": {
"id": "mtmcanvas",
"apiVersion": 1,
"client": "./lib/client.js"
}
},
"files": [
"lib/index.js",
"lib/client.js",
"lib/client.cjs",
"lib/types/**/*.d.ts",
"cordis.patch.yml",
"README.md",
"package.json",
"LICENSE"
Expand All @@ -51,10 +40,8 @@
"build": "node scripts/build.mjs",
"typecheck": "tsc --noEmit",
"test": "vitest run",
"verify:package": "node scripts/verify-package.mjs",
"check": "pnpm run typecheck && pnpm run test && pnpm run build && pnpm run verify:package",
"pack:check": "node scripts/pack-check.mjs",
"prepack": "pnpm run build && pnpm run verify:package"
"check": "pnpm run typecheck && pnpm run test && pnpm run build",
"prepack": "pnpm run build"
},
"license": "MIT",
"publishConfig": {
Expand All @@ -65,28 +52,7 @@
"url": "git+https://github.com/codeh007/mtmdsh.git",
"directory": "packages/mtmcanvas"
},
"peerDependencies": {
"@deepseek-ai/cordis": "^4.0.1",
"@deepseek-ai/dsh-client-connection": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-runtime": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-sidebar": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-layout": "^0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "^0.1.1-rc.2",
"@deepseek-ai/dsh-fs": "^0.1.1-rc.2",
"@deepseek-ai/dsh-host-directory-picker": "^0.1.1-rc.2",
"react": ">=18.2.0 <20"
},
"devDependencies": {
"@deepseek-ai/cordis": "4.0.1",
"@deepseek-ai/dsh-client-connection": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-runtime": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-primitives": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-sidebar": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-layout": "0.1.1-rc.2",
"@deepseek-ai/dsh-client-ui-slots": "0.1.1-rc.2",
"@deepseek-ai/dsh-fs": "0.1.1-rc.2",
"@deepseek-ai/dsh-host-directory-picker": "0.1.1-rc.2",
"@types/node": "22.20.1",
"@types/react": "~18.3.1",
"@types/react-dom": "~18.3.0",
Expand Down
50 changes: 10 additions & 40 deletions packages/mtmcanvas/scripts/build.mjs
Original file line number Diff line number Diff line change
@@ -1,64 +1,34 @@
#!/usr/bin/env node

import { execFileSync } from "node:child_process";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, rmSync } from "node:fs";
import { resolve } from "node:path";
import { fileURLToPath } from "node:url";
import { build } from "esbuild";

const packageRoot = fileURLToPath(new URL("..", import.meta.url));
const libRoot = resolve(packageRoot, "lib");
const tsc = resolve(packageRoot, "node_modules/.bin/tsc");
const clientTemp = resolve(libRoot, "client.bundle.cjs");
const packageName = "mtmcanvas";

rmSync(libRoot, { recursive: true, force: true });
mkdirSync(libRoot, { recursive: true });
if (!existsSync(tsc)) throw new Error("mtmcanvas build: local TypeScript executable is missing");
execFileSync(tsc, ["--project", resolve(packageRoot, "tsconfig.json")], { cwd: packageRoot, stdio: "inherit" });

await build({
entryPoints: [resolve(packageRoot, "src/index.ts")],
outfile: resolve(libRoot, "index.js"),
bundle: true,
format: "esm",
platform: "node",
packages: "external",
target: "es2022",
logLevel: "info",
});
for (const declaration of ["lib/types/index.d.ts", "lib/types/client/index.d.ts"]) {
if (!existsSync(resolve(packageRoot, declaration))) throw new Error("mtmcanvas build: missing " + declaration);
}

await build({
entryPoints: [resolve(packageRoot, "src/client/index.ts")],
outfile: clientTemp,
outfile: resolve(libRoot, "client.js"),
bundle: true,
format: "cjs",
format: "esm",
platform: "browser",
target: "es2020",
external: ["react", "react/*", "@deepseek-ai/*"],
target: "es2022",
legalComments: "none",
logLevel: "info",
});
const artifact = await import(resolve(libRoot, "client.js"));
if (typeof artifact.mount !== "function") throw new Error("mtmcanvas build: client artifact must export mount(context)");

const clientSource = readFileSync(clientTemp, "utf8");
const indented = clientSource.split("\n").map((line) => " " + line).join("\n");
const artifact = [
"window.__ModuleLoader__.load({",
" id: " + JSON.stringify(packageName) + ",",
" factory: (require) => {",
" var module = { exports: {} };",
" var exports = module.exports;",
indented,
" return module.exports;",
" }",
"});",
"",
].join("\n");
if (!artifact.includes("window.__ModuleLoader__.load") || !artifact.includes("id: \"" + packageName + "\"")) {
throw new Error("mtmcanvas build: generated client artifact does not have the DSH loader contract");
}
writeFileSync(resolve(libRoot, "client.js"), artifact);
writeFileSync(resolve(libRoot, "client.cjs"), artifact);
rmSync(clientTemp, { force: true });

console.log("built mtmcanvas Host and Client artifacts");
console.log("built mtmcanvas browser ESM artifact");
18 changes: 0 additions & 18 deletions packages/mtmcanvas/scripts/pack-check.mjs

This file was deleted.

55 changes: 0 additions & 55 deletions packages/mtmcanvas/scripts/verify-package.mjs

This file was deleted.

4 changes: 2 additions & 2 deletions packages/mtmcanvas/src/client/CanvasView.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { useEffect, useRef, useState, type DragEvent, type ReactElement } from "react";
import { useEffect, useRef, useState, type DragEvent, type FormEvent, type ReactElement } from "react";
import type { CanvasNode } from "../contract/canvas.ts";
import type { CanvasActions, CanvasViewState } from "./runtime.ts";

Expand Down Expand Up @@ -31,7 +31,7 @@ export function CanvasView({ state, actions }: { state: CanvasViewState; actions
if (selected?.kind === "prompt") setDraft(selected.prompt);
}, [selected]);

function create(event: React.FormEvent<HTMLFormElement>): void {
function create(event: FormEvent<HTMLFormElement>): void {
event.preventDefault();
const name = filename.endsWith(".canvas") ? filename : filename + ".canvas";
actions.create(name);
Expand Down
Loading
Loading