feat: add standalone and HiveCast SDK demos

This commit is contained in:
Ubuntu 2026-06-07 23:45:51 +00:00
parent 6f516fe2f8
commit f3fd4e109f
18 changed files with 500 additions and 1 deletions

View File

@ -156,6 +156,37 @@ configure broker access, run actors, and invoke them without importing provider
overlay packages into the SDK layer. Managed-host promotion is optional and is overlay packages into the SDK layer. Managed-host promotion is optional and is
only exercised when `MATRIX_SDK_RUNTIME_HOST_BIN` is supplied. only exercised when `MATRIX_SDK_RUNTIME_HOST_BIN` is supplied.
## Demos
Standalone SDK demo, no provider account or key required:
```bash
pnpm demo:standalone
```
This builds the SDK and `examples/standalone-greeter`, starts the actor with
`matrix run`, calls it with `matrix invoke`, and shuts it down.
HiveCast API-key login/config demo:
```bash
export MATRIX_CLOUD=https://dev.hivecast.ai
export MATRIX_SPACE=<your-space-root>
export MATRIX_API_KEY=<your-api-key>
pnpm demo:hivecast-login
```
This validates the key with HiveCast and writes the SDK package-local
`.matrix/config.json` plus `.matrix/credentials/matrix-api-key.json` shape
without printing or committing the key. By default the demo uses a temporary
home and deletes it after success.
Current boundary: this HiveCast demo proves SDK-side key validation and config
storage. It does not yet prove a package running directly on a HiveCast-issued
broker credential. That requires the provider-owned login/credential-exchange
slice so `matrix login` can replace manual `matrix configure` and issue/store
the broker credentials needed by the SDK transport.
## Repository Layout ## Repository Layout
```text ```text
@ -169,6 +200,8 @@ packages/
omega-core/ omega-core/
sdk/ sdk/
scripts/ scripts/
demo:standalone
demo:hivecast-login
prove-external-consumer.mjs prove-external-consumer.mjs
prove-sdk-cli-uat.mjs prove-sdk-cli-uat.mjs
``` ```

View File

@ -23,6 +23,13 @@ The SDK package surface is standalone enough for the current authoring proof:
The proof script also checks that generated author-package files do not contain The proof script also checks that generated author-package files do not contain
legacy monorepo paths or upper-layer package dependencies. legacy monorepo paths or upper-layer package dependencies.
Checked-in demos now cover the same boundary in user-facing form:
- `pnpm demo:standalone` runs `examples/standalone-greeter` without provider
credentials.
- `pnpm demo:hivecast-login` validates a user-supplied HiveCast API key and
writes package-local SDK config/credentials in a temporary demo home.
## Cleanup Completed ## Cleanup Completed
- Root README now describes this as a provider-neutral SDK, not a HiveCast - Root README now describes this as a provider-neutral SDK, not a HiveCast
@ -87,3 +94,14 @@ Expected:
- `legacyMonorepoPathReferences` is `false`. - `legacyMonorepoPathReferences` is `false`.
- `managedHostProof` is skipped unless `MATRIX_SDK_RUNTIME_HOST_BIN` is supplied. - `managedHostProof` is skipped unless `MATRIX_SDK_RUNTIME_HOST_BIN` is supplied.
- `prove` builds, tests, type-checks, and packs all workspace packages. - `prove` builds, tests, type-checks, and packs all workspace packages.
Demo checks:
```bash
pnpm demo:standalone
pnpm demo:hivecast-login -- --check
```
`demo:hivecast-login -- --check` proves the demo wiring without requiring a
secret. A real HiveCast proof requires `MATRIX_API_KEY`, `MATRIX_CLOUD`, and
`MATRIX_SPACE` from the user.

View File

@ -0,0 +1,5 @@
# Copy this file outside git-tracked secrets handling or export these variables
# in your shell. Do not commit real keys.
MATRIX_CLOUD=https://dev.hivecast.ai
MATRIX_SPACE=your-space-root
MATRIX_API_KEY=hc_your_key_here

3
examples/hivecast-login/.gitignore vendored Normal file
View File

@ -0,0 +1,3 @@
.env
.matrix/
demo-home/

View File

@ -0,0 +1,42 @@
# HiveCast API-Key Login Demo
This demo proves the SDK-side HiveCast credential shape without committing a
secret.
It validates a user-supplied HiveCast API key against the configured HiveCast
cloud, then writes the same package-local `.matrix` config/credential files that
`matrix configure` writes. This is the current API-key login shape.
It does **not** prove hosted broker execution yet. The SDK still needs a
provider-owned credential exchange/login command before a package can use a
HiveCast-issued broker JWT directly from this repo.
## Run
```bash
export MATRIX_CLOUD=https://dev.hivecast.ai
export MATRIX_SPACE=<your-space-root>
export MATRIX_API_KEY=<your-api-key>
pnpm demo:hivecast-login
```
Optional:
```bash
MATRIX_DEMO_HOME=/tmp/my-sdk-hivecast-demo pnpm demo:hivecast-login
MATRIX_DEMO_KEEP=1 pnpm demo:hivecast-login
```
The key is never printed. By default the temporary demo home is deleted after a
successful run. Set `MATRIX_DEMO_KEEP=1` to inspect the generated `.matrix`
files locally.
## Check Script Wiring Without A Key
```bash
pnpm demo:hivecast-login -- --check
```
This only verifies that the demo is installed and intentionally refuses to run
without credentials.

View File

@ -0,0 +1,120 @@
import { mkdtempSync, rmSync, existsSync, readFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { resolve, join } from 'node:path';
import { spawnSync } from 'node:child_process';
const args = new Set(process.argv.slice(2));
const checkOnly = args.has('--check');
const repoRoot = resolve(new URL('../..', import.meta.url).pathname);
const matrixCli = resolve(repoRoot, 'packages', 'cli', 'dist', 'bin', 'matrix.js');
function envString(...names) {
for (const name of names) {
const value = process.env[name];
if (typeof value === 'string' && value.trim()) return value.trim();
}
return undefined;
}
function run(command, commandArgs, options = {}) {
const result = spawnSync(command, commandArgs, {
cwd: options.cwd ?? repoRoot,
stdio: options.stdio ?? 'pipe',
env: process.env,
encoding: 'utf8',
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${command} ${commandArgs.join(' ')} failed with exit ${result.status}\n${result.stderr}${result.stdout}`);
}
return result;
}
function resolveConfig() {
const key = envString('MATRIX_API_KEY', 'HIVECAST_API_KEY', 'HIVECAST_TEST_KEY');
const cloud = envString('MATRIX_CLOUD', 'HIVECAST_CLOUD', 'HIVECAST_TEST_CLOUD');
const space = envString('MATRIX_SPACE', 'HIVECAST_SPACE', 'HIVECAST_TEST_SPACE');
const provided = [key, cloud, space].filter(Boolean).length;
if (provided > 0 && provided < 3) {
throw new Error('Provide MATRIX_API_KEY, MATRIX_CLOUD, and MATRIX_SPACE together');
}
return key && cloud && space
? { key, cloud: cloud.replace(/\/+$/g, ''), space }
: null;
}
async function validateHiveCastKey(config) {
const response = await fetch(`${config.cloud}/api/install/key-info`, {
method: 'POST',
headers: {
authorization: `Bearer ${config.key}`,
'content-type': 'application/json',
},
body: JSON.stringify({ space: config.space }),
});
const text = await response.text();
if (!response.ok) {
throw new Error(`HiveCast key validation failed (${response.status}): ${text}`);
}
const body = text.trim() ? JSON.parse(text) : {};
if (body?.ok !== true) {
throw new Error(`HiveCast key validation did not return ok:true: ${text}`);
}
return {
keyId: body.key?.id ?? body.id ?? null,
fingerprint: body.key?.fingerprint ?? body.fingerprint ?? null,
};
}
const config = resolveConfig();
if (!config) {
const message = 'HiveCast demo requires MATRIX_API_KEY, MATRIX_CLOUD, and MATRIX_SPACE';
if (checkOnly) {
console.log(JSON.stringify({ ok: true, demo: 'hivecast-login', credentialsRequired: true, message }, null, 2));
process.exit(0);
}
throw new Error(message);
}
if (checkOnly) {
console.log(JSON.stringify({ ok: true, demo: 'hivecast-login', credentialsAvailable: true }, null, 2));
process.exit(0);
}
const demoHome = process.env.MATRIX_DEMO_HOME
? resolve(process.env.MATRIX_DEMO_HOME)
: mkdtempSync(join(tmpdir(), 'matrix-sdk-hivecast-login.'));
const keep = process.env.MATRIX_DEMO_KEEP === '1' || Boolean(process.env.MATRIX_DEMO_HOME);
try {
const validation = await validateHiveCastKey(config);
run(process.execPath, [
matrixCli, 'configure',
'--dir', demoHome,
'--key', config.key,
'--cloud', config.cloud,
'--space', config.space,
]);
const credentialPath = join(demoHome, '.matrix', 'credentials', 'matrix-api-key.json');
const configPath = join(demoHome, '.matrix', 'config.json');
if (!existsSync(credentialPath)) throw new Error(`Credential file was not written: ${credentialPath}`);
if (!existsSync(configPath)) throw new Error(`Config file was not written: ${configPath}`);
const writtenCredential = JSON.parse(readFileSync(credentialPath, 'utf8'));
if (writtenCredential.key !== config.key) throw new Error('Written credential key does not match input key');
if (writtenCredential.cloud !== config.cloud) throw new Error('Written credential cloud does not match input cloud');
if (writtenCredential.space !== config.space) throw new Error('Written credential space does not match input space');
console.log(JSON.stringify({
ok: true,
demo: 'hivecast-login',
cloud: config.cloud,
space: config.space,
keyValidated: true,
keyId: validation.keyId,
fingerprint: validation.fingerprint,
demoHome: keep ? demoHome : '(temporary, removed)',
credentialPath: keep ? credentialPath : '(temporary, removed)',
}, null, 2));
} finally {
if (!keep) rmSync(demoHome, { recursive: true, force: true });
}

View File

@ -0,0 +1,2 @@
.matrix/
dist/

View File

@ -0,0 +1,20 @@
# Standalone Greeter Demo
This demo proves the SDK can run without HiveCast, without a managed platform,
and without committed credentials.
Run from the repository root:
```bash
pnpm demo:standalone
```
What it proves:
- the SDK packages build from this repo,
- this package builds against `@open-matrix/core`,
- `matrix run` starts the actor from the package manifest,
- `matrix invoke demo.greeter echo ...` calls the actor over the SDK local
runtime path,
- all runtime state is written under the demo package's local `.matrix/`
directory.

View File

@ -0,0 +1,28 @@
{
"name": "matrix-sdk-example-standalone-greeter",
"version": "0.0.0",
"runtime": {
"language": "typescript",
"entry": "./dist/index.js",
"factory": {
"kind": "factory",
"export": "createStandaloneGreeterActor",
"instance": {
"id": "RUNTIME-HOST-DEMO-GREETER",
"class": "service",
"rootKind": "component",
"componentType": "GreeterActor",
"autoStart": true
}
}
},
"components": [
{
"type": "GreeterActor",
"export": "GreeterActor",
"mount": "demo.greeter",
"surface": "headless",
"accepts": ["echo", "getStatus"]
}
]
}

View File

@ -0,0 +1,15 @@
{
"name": "matrix-sdk-example-standalone-greeter",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"build": "tsc -p tsconfig.json"
},
"dependencies": {
"@open-matrix/core": "workspace:*"
},
"devDependencies": {
"typescript": "^5.7.0"
}
}

View File

@ -0,0 +1,113 @@
import { spawn, spawnSync } from 'node:child_process';
import { createServer } from 'node:http';
import { dirname, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, '..', '..');
const demoDir = __dirname;
const matrixCli = resolve(repoRoot, 'packages', 'cli', 'dist', 'bin', 'matrix.js');
function run(command, args, options = {}) {
const result = spawnSync(command, args, {
cwd: options.cwd ?? repoRoot,
stdio: options.stdio ?? 'inherit',
env: process.env,
encoding: 'utf8',
});
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} failed with exit ${result.status}`);
}
return result;
}
async function freePort() {
return await new Promise((resolvePort, reject) => {
const server = createServer();
server.on('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
if (!address || typeof address === 'string') {
server.close(() => reject(new Error('Could not allocate demo port')));
return;
}
const { port } = address;
server.close(() => resolvePort(port));
});
});
}
async function waitForHealth(port, child) {
const deadline = Date.now() + 10_000;
let lastError;
while (Date.now() < deadline) {
if (child.exitCode !== null) throw new Error(`matrix run exited early with ${child.exitCode}`);
try {
const response = await fetch(`http://127.0.0.1:${port}/healthz`);
if (response.ok) return;
lastError = new Error(`healthz returned ${response.status}`);
} catch (error) {
lastError = error;
}
await new Promise((resolveDelay) => setTimeout(resolveDelay, 100));
}
throw lastError ?? new Error('Timed out waiting for matrix run healthz');
}
function stop(child) {
return new Promise((resolveStop) => {
if (child.exitCode !== null) {
resolveStop();
return;
}
child.once('exit', () => resolveStop());
child.kill('SIGTERM');
setTimeout(() => {
if (child.exitCode === null) child.kill('SIGKILL');
}, 3000).unref();
});
}
const port = await freePort();
let runner;
try {
run('pnpm', ['run', 'build']);
run('pnpm', ['--filter', 'matrix-sdk-example-standalone-greeter', 'run', 'build']);
runner = spawn(process.execPath, [
matrixCli, 'run', demoDir,
'--dir', demoDir,
'--port', String(port),
], {
cwd: repoRoot,
stdio: ['ignore', 'pipe', 'pipe'],
env: process.env,
});
runner.stderr.on('data', (chunk) => process.stderr.write(chunk));
await waitForHealth(port, runner);
const invoked = run(process.execPath, [
matrixCli, 'invoke',
'demo.greeter',
'echo',
'{"message":"hello-from-standalone-demo"}',
'--dir', demoDir,
'--port', String(port),
], { cwd: repoRoot, stdio: 'pipe' });
const body = JSON.parse(invoked.stdout.trim());
if (body?.ok !== true || body?.result?.message !== 'hello-from-standalone-demo') {
throw new Error(`Unexpected invoke result: ${invoked.stdout}`);
}
console.log(JSON.stringify({
ok: true,
demo: 'standalone-greeter',
mount: 'demo.greeter',
port,
invoke: body,
}, null, 2));
} finally {
if (runner) await stop(runner);
}

View File

@ -0,0 +1,24 @@
import { MatrixActor } from '@open-matrix/core';
export class GreeterActor extends MatrixActor {
static accepts = {
echo: {},
getStatus: {},
};
onEcho(payload: { message?: string }) {
return {
ok: true,
message: payload.message ?? null,
actor: 'GreeterActor',
};
}
onGetStatus() {
return {
ok: true,
actor: 'GreeterActor',
mount: 'demo.greeter',
};
}
}

View File

@ -0,0 +1,50 @@
import { InMemoryBroker, InMemoryTransport, MatrixRuntime } from '@open-matrix/core';
import { GreeterActor } from './GreeterActor.js';
interface StandaloneBootContext {
readonly root?: string;
readonly mount?: string;
}
interface StandaloneRunnerServices {
readonly runtime?: MatrixRuntime;
}
function readTrimmedString(value: unknown): string | undefined {
if (typeof value !== 'string') return undefined;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : undefined;
}
function requiredStandaloneRoot(requestedRoot: string | undefined, runtime: MatrixRuntime): string {
const runtimeRoot = readTrimmedString(runtime.transport.root);
const root = requestedRoot ?? runtimeRoot;
if (!root) throw new Error('createStandaloneGreeterActor requires bootContext.root or a runtime transport root');
return root;
}
export async function createStandaloneGreeterActor(
_overrides: Record<string, unknown> = {},
services?: StandaloneRunnerServices,
_config?: unknown,
bootContext: StandaloneBootContext = {},
): Promise<{
runtime: MatrixRuntime;
actor: GreeterActor;
root: string;
mount: string;
}> {
const requestedRoot = readTrimmedString(bootContext.root);
const runtime = services?.runtime ?? new MatrixRuntime({
transport: new InMemoryTransport(new InMemoryBroker(), {
name: 'demo.greeter-standalone',
...(requestedRoot ? { root: requestedRoot } : {}),
}),
logging: false,
});
const root = requiredStandaloneRoot(requestedRoot, runtime);
const mount = readTrimmedString(bootContext.mount) ?? 'demo.greeter';
const actor = await runtime.createSupervised(GreeterActor, mount);
return { runtime, actor, root, mount };
}

View File

@ -0,0 +1,2 @@
export { GreeterActor } from './GreeterActor.js';
export { createStandaloneGreeterActor } from './create-standalone-GreeterActor.js';

View File

@ -0,0 +1,11 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"strict": true,
"skipLibCheck": true,
"outDir": "dist"
},
"include": ["src/**/*.ts"]
}

View File

@ -9,7 +9,9 @@
"build": "pnpm -r --sort --if-present run build", "build": "pnpm -r --sort --if-present run build",
"test": "pnpm -r --sort --if-present run test", "test": "pnpm -r --sort --if-present run test",
"type-check": "pnpm -r --sort --if-present run type-check", "type-check": "pnpm -r --sort --if-present run type-check",
"pack:all": "pnpm -r exec pnpm pack", "pack:all": "pnpm --filter './packages/*' -r exec pnpm pack",
"demo:standalone": "node examples/standalone-greeter/run-demo.mjs",
"demo:hivecast-login": "node examples/hivecast-login/run-demo.mjs",
"prove:external-consumer": "node scripts/prove-external-consumer.mjs", "prove:external-consumer": "node scripts/prove-external-consumer.mjs",
"prove:cli-uat": "node scripts/prove-sdk-cli-uat.mjs", "prove:cli-uat": "node scripts/prove-sdk-cli-uat.mjs",
"prove": "pnpm run clean && pnpm run build && pnpm run test && pnpm run type-check && pnpm run pack:all" "prove": "pnpm run clean && pnpm run build && pnpm run test && pnpm run type-check && pnpm run pack:all"

10
pnpm-lock.yaml generated
View File

@ -40,6 +40,16 @@ importers:
specifier: ^5.7.0 specifier: ^5.7.0
version: 5.9.3 version: 5.9.3
examples/standalone-greeter:
dependencies:
'@open-matrix/core':
specifier: workspace:*
version: link:../../packages/core
devDependencies:
typescript:
specifier: ^5.7.0
version: 5.9.3
packages/browser-host: packages/browser-host:
dependencies: dependencies:
'@open-matrix/contracts': '@open-matrix/contracts':

View File

@ -1,2 +1,3 @@
packages: packages:
- "packages/*" - "packages/*"
- "examples/*"