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
8 changes: 6 additions & 2 deletions AGENT_RULES.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,8 @@ Use it only after both status and the current tool schema expose it.
- `three_studio_job` `sceneExport` writes a Three.js-loadable glTF/GLB of the
active scene or an exact entity subtree. Shader-graph looks export as authored
PBR factors (and a bound albedo map when present), not compiled TSL.
- A configured `three_studio_job` `rehearsalRun` is not a command runner:
provide only its repository-relative run-spec path.
- Create with `template: "blank"` for an empty scene. Use `scene.clear` with
the inspected `sceneHash` to wipe the current scene without the starter
composition. `template: "starter"` remains the optional lit primitive stage.
Expand Down Expand Up @@ -377,8 +379,10 @@ operations.

- Use project-relative logical asset IDs, never arbitrary absolute paths in
scene documents.
- Asset import jobs are unavailable in the current slice; do not place
arbitrary absolute paths into a project as a workaround.
- General asset import jobs are unavailable. The project tool may exchange a
schema- and hash-guarded JSON artifact beneath the configured rehearsal root;
do not place absolute paths into a project or use this seam for arbitrary
files.
- Record source, license/provenance, colour role, and importer settings.
- Do not overwrite a user-authored script or asset without inspecting it and
making the replacement explicit in the transaction label.
Expand Down
17 changes: 11 additions & 6 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -624,11 +624,15 @@ backward. Dedicated diff and revision-compare actions are not exposed.

### 9. `three_studio_job`

This is an explicit reserved slot so the nine-tool topology need not change.
It currently exposes no start/inspect/cancel action and returns
`job_not_implemented`; `status.capabilities.jobs` is false. Asset import,
texture/mesh bakes, reconstruction, lightmaps, and packaging are future job
kinds, not callable operations.
The ninth slot exposes bounded, typed jobs without admitting a generic process
runner. `textureBake` deterministically compiles one procedural graph output
through the guarded kernel. `sceneExport` writes an active scene or exact
entity subtree as glTF/GLB. When a machine-local rehearsal root is configured,
`rehearsalRun` invokes exactly `tools/rehearsal-sidecar/src/cli.mjs` with
`shell: false`, a minimal environment, and a repository-relative run-spec path;
the response is reduced to verified bundle evidence. Arbitrary commands,
reconstruction, lightmaps, mesh bakes, whole-project/application export, and
packaging remain unavailable.

## Shared request and response contract

Expand All @@ -637,7 +641,8 @@ an idempotency key, and a human-readable label. Undo/redo, non-query Play
controls, and project mutations carry the correlation fields required by their
schemas; project create/open have no meaningful prior project revision. Read
actions accept the bounded connection/target fields declared by their own
schemas. There is currently no long-running job request.
schemas. Jobs carry the same exact project, revision, and idempotency guards as
other stateful requests and honor bridge cancellation.

Responses are action-specific. Committed `apply`/undo/redo responses include
the revision, transaction ID, resolved IDs, changed/deleted IDs, compact diff,
Expand Down
22 changes: 15 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,18 +63,24 @@ The nine-tool shape is stable, but each tool advertises only its live slice:
- `play`: deterministic Action animation and timeline-driven Ocean displacement
enter/stop/pause/resume/seek/step plus recorded input, without script,
blueprint, physics, or game-logic execution;
- `project`: list, create, open, and atomic save; and
- `project`: list, create, open, atomic save, and guarded import/export of one
configured external JSON artifact as canonical project data;
- `history`: list, inspect, undo, and redo.

The jobs tool remains as an explicit reserved ninth slot and always reports
`job_not_implemented`. Shader and texture graph resources are authored through
ordinary resource operations: the live subset compiles to TSL/WebGPU and the
procedural subset can be deterministically CPU-baked. Bounded inline
The jobs tool exposes deterministic procedural texture baking and scene or
entity-subtree glTF/GLB export. When a machine-local rehearsal root is
configured, it also exposes `rehearsalRun`: Studio accepts only a
repository-relative run-spec path and invokes the fixed sidecar with
`shell: false` and a minimal environment. It never accepts a command. Shader
and texture graph resources are authored through ordinary resource operations:
the live subset compiles to TSL/WebGPU and the procedural subset can be
deterministically CPU-baked. Bounded inline
`dataTexture` resources compile once to shared RGBA8 WebGPU textures and bind
to supported raster material map slots. Existing format-v1 generic texture
placeholders remain valid/indexable but are not live raster inputs. Script operations,
layout generators, scoped or code/asset/render validation, diagnostic passes,
export, and RTX evidence remain absent. Status capabilities are authoritative.
whole-project/application export, and RTX evidence remain absent. Status
capabilities are authoritative.

## Lean dependency boundary

Expand Down Expand Up @@ -180,7 +186,9 @@ sibling checkout at `..\ThreeBrowser\ThreeBrowserRuntime`. For another
location, set `THREEBROWSER_RUNTIME_ROOT` to the absolute
`ThreeBrowserRuntime` directory, or put
`{ "runtimeRoot": "C:\\path\\to\\ThreeBrowserRuntime" }` in the ignored
`.studio-local.json` file. Machine-local paths never enter a saved project.
`.studio-local.json` file. A controlled external rehearsal can additionally set
`"rehearsalRoot"` there (or `THREE_STUDIO_REHEARSAL_ROOT`). Machine-local paths
never enter a saved project.

`npm run launch` opens one native WebGPU window and restores the last opened
project (falling back to `projects/live`). A genuinely new fallback project is
Expand Down
80 changes: 73 additions & 7 deletions schemas/tools-v1.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -2841,6 +2841,38 @@
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
"op": {
"type": "string",
"const": "artifact.json.patch"
},
"artifactId": {
"$ref": "#/$defs/toolSchemas/three_studio_apply/$defs/__schema6"
},
"pointer": {
"type": "string",
"minLength": 1,
"maxLength": 1024,
"pattern": "^(?:\\/(?:[^~/]|~[01])*)+$"
},
"value": {
"$ref": "#/$defs/toolSchemas/three_studio_apply/$defs/__schema2"
},
"expectedArtifactHash": {
"$ref": "#/$defs/toolSchemas/three_studio_apply/$defs/__schema7"
}
},
"required": [
"op",
"artifactId",
"pointer",
"value",
"expectedArtifactHash"
],
"additionalProperties": false
},
{
"type": "object",
"properties": {
Expand Down Expand Up @@ -7305,7 +7337,8 @@
"type": "string",
"enum": [
"textureBake",
"sceneExport"
"sceneExport",
"rehearsalRun"
]
},
"projectId": {
Expand Down Expand Up @@ -7348,6 +7381,11 @@
"gltf"
]
},
"runSpecReference": {
"type": "string",
"minLength": 1,
"maxLength": 1024
},
"name": {
"type": "string",
"minLength": 1,
Expand Down Expand Up @@ -7406,14 +7444,13 @@
"list",
"create",
"open",
"save"
"save",
"artifactImport",
"artifactExport"
]
},
"projectId": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?(?:\\/[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)*$"
"$ref": "#/$defs/toolSchemas/three_studio_project/$defs/__schema0"
},
"path": {
"type": "string",
Expand All @@ -7432,6 +7469,21 @@
"starter"
]
},
"artifactId": {
"$ref": "#/$defs/toolSchemas/three_studio_project/$defs/__schema0"
},
"schemaId": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[A-Za-z0-9][A-Za-z0-9._/@-]*$"
},
"expectedFileSha256": {
"$ref": "#/$defs/toolSchemas/three_studio_project/$defs/__schema1"
},
"expectedArtifactHash": {
"$ref": "#/$defs/toolSchemas/three_studio_project/$defs/__schema1"
},
"baseRevision": {
"type": "integer",
"minimum": 0,
Expand All @@ -7452,7 +7504,21 @@
"required": [
"action"
],
"additionalProperties": false
"additionalProperties": false,
"$defs": {
"__schema0": {
"type": "string",
"minLength": 1,
"maxLength": 160,
"pattern": "^[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?(?:\\/[a-z0-9](?:[a-z0-9._-]*[a-z0-9])?)*$"
},
"__schema1": {
"type": "string",
"minLength": 64,
"maxLength": 64,
"pattern": "^[a-f0-9]{64}$"
}
}
},
"three_studio_play": {
"type": "object",
Expand Down
5 changes: 4 additions & 1 deletion scripts/launch.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,15 @@ import { spawn } from "node:child_process";
import { rm } from "node:fs/promises";
import path from "node:path";
import { fileURLToPath } from "node:url";
import { resolveRuntimeRoot } from "./runtime-path.mjs";
import { readStudioLocalConfig, resolveRuntimeRoot } from "./runtime-path.mjs";
import { defaultSessionMarkerPath, readSessionMarker } from "../src/bridge/index.mjs";

const studioRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url)));
const entry = path.join(studioRoot, "site-entry.mjs");
const localConfig = await readStudioLocalConfig({ studioRoot });
const runtime = await resolveRuntimeRoot({ studioRoot });
const projectArgument = process.argv.slice(2).find(argument => !argument.startsWith("--"));
const rehearsalRoot = process.env.THREE_STUDIO_REHEARSAL_ROOT || localConfig.rehearsalRoot;

console.error(`[ThreeBrowser Studio] runtime: ${runtime.root}`);
console.error(`[ThreeBrowser Studio] entry: ${entry}`);
Expand All @@ -19,6 +21,7 @@ const child = spawn(process.execPath, [runtime.launcher, entry], {
...process.env,
THREE_STUDIO_ROOT: studioRoot,
...(projectArgument ? { THREE_STUDIO_PROJECT: path.resolve(projectArgument) } : {}),
...(rehearsalRoot ? { THREE_STUDIO_REHEARSAL_ROOT: rehearsalRoot } : {}),
},
stdio: "inherit",
windowsHide: false,
Expand Down
30 changes: 21 additions & 9 deletions scripts/runtime-path.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@ function cleanRoot(value) {
return text ? path.resolve(text) : null;
}

export async function readStudioLocalConfig({
studioRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))),
} = {}) {
const localConfigPath = path.join(studioRoot, ".studio-local.json");
if (!(await exists(localConfigPath))) return Object.freeze({});
try {
const config = JSON.parse(await readFile(localConfigPath, "utf8"));
const normalized = {};
const runtimeRoot = cleanRoot(config.runtimeRoot);
const rehearsalRoot = cleanRoot(config.rehearsalRoot);
if (runtimeRoot) normalized.runtimeRoot = runtimeRoot;
if (rehearsalRoot) normalized.rehearsalRoot = rehearsalRoot;
return Object.freeze(normalized);
} catch (error) {
throw new Error(`Invalid ${localConfigPath}: ${error.message}`);
}
}

export async function resolveRuntimeRoot({
studioRoot = path.dirname(path.dirname(fileURLToPath(import.meta.url))),
environment = process.env,
Expand All @@ -25,15 +43,9 @@ export async function resolveRuntimeRoot({
const configuredEnvironment = cleanRoot(environment.THREEBROWSER_RUNTIME_ROOT);
if (configuredEnvironment) candidates.push({ source: "environment", root: configuredEnvironment });

const localConfigPath = path.join(studioRoot, ".studio-local.json");
if (await exists(localConfigPath)) {
try {
const config = JSON.parse(await readFile(localConfigPath, "utf8"));
const configuredFile = cleanRoot(config.runtimeRoot);
if (configuredFile) candidates.push({ source: ".studio-local.json", root: configuredFile });
} catch (error) {
throw new Error(`Invalid ${localConfigPath}: ${error.message}`);
}
const localConfig = await readStudioLocalConfig({ studioRoot });
if (localConfig.runtimeRoot) {
candidates.push({ source: ".studio-local.json", root: localConfig.runtimeRoot });
}

const packaged = path.resolve(studioRoot, "..", "host");
Expand Down
9 changes: 7 additions & 2 deletions scripts/studio-call.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import path from 'node:path';
import { LiveBridgeClient, defaultSessionMarkerPath, readSessionMarker } from '../src/bridge/index.mjs';
import {
LiveBridgeClient,
defaultSessionMarkerPath,
readSessionMarker,
requestTimeoutMsForMethod,
} from '../src/bridge/index.mjs';

const args = process.argv.slice(2);
let markerPath = defaultSessionMarkerPath();
Expand All @@ -19,7 +24,7 @@ if (args[0]) {
const client = new LiveBridgeClient(marker);
try {
await client.connect();
const result = await client.request(method, params);
const result = await client.request(method, params, { timeoutMs: requestTimeoutMsForMethod(method) });
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} finally {
await client.close();
Expand Down
1 change: 1 addition & 0 deletions src/bridge/protocol.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export const COMPILE_HEAVY_METHODS = Object.freeze(new Set([
'three_studio_render',
'three_studio_project',
'three_studio_history',
'three_studio_job',
]));

export function requestTimeoutMsForMethod(method, fallback = DEFAULT_REQUEST_TIMEOUT_MS) {
Expand Down
Loading