@nylorun/studio 0.5.0-beta → 0.6.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -0
- package/README.md +8 -6
- package/dist/host.d.ts +5 -4
- package/dist/host.js +70 -25
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -1
- package/dist/proxy.d.ts +7 -0
- package/dist/proxy.js +90 -0
- package/dist/web/assets/index-D8GMjOxw.js +102 -0
- package/dist/web/assets/index-XVwY7Mtj.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +6 -9
- package/dist/web/assets/index-B9Unrv4d.css +0 -2
- package/dist/web/assets/index-mtdvyCDZ.js +0 -58
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.0-beta
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 41e613c: Ship the local SDK registry workflow with an independent SQLite Runtime, connected tool executor, authenticated Studio proxy, and a text-and-tool starter. Replace the legacy Hono starter and AG-UI transport. Require Node 24 and include the SDK in exact release compatibility pins.
|
|
8
|
+
|
|
9
|
+
Break the Harness execution import from `/engine` to `/run` and rename hosted execution APIs to durable execution APIs, including RunBinding, BoundRunOptions, and createRunState. Update all consumers without compatibility aliases; retain persisted checkpoint fields and version pins.
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- 2898d02: Extract shared definitions and contracts into core and local orchestration into
|
|
14
|
+
CLI. Harness becomes execution-only; the SDK no longer installs the engine and
|
|
15
|
+
Runtime no longer depends on the SDK. Author applications through agents and
|
|
16
|
+
install cli for the unchanged nylorun commands. See the package architecture and
|
|
17
|
+
migration guide. Cloud installs published packages from npm independently.
|
|
18
|
+
- Pin agents to the tested release.
|
|
19
|
+
- Updated dependencies [41e613c]
|
|
20
|
+
- Updated dependencies [2898d02]
|
|
21
|
+
- Updated dependencies
|
|
22
|
+
- @nylorun/agents@0.2.0-beta
|
|
23
|
+
|
|
3
24
|
## 0.5.0-beta
|
|
4
25
|
|
|
5
26
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @nylorun/studio
|
|
2
2
|
|
|
3
|
-
Local
|
|
3
|
+
Local session dashboard for the OSS Runtime. Requires Node 24.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
The starter's `npm run dev` starts Studio automatically. To attach to an already running local project, use `npm run studio`, or `nylorun studio --runtime-url http://127.0.0.1:8787`.
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
Studio lists registered agents and sessions, sends text, displays completed assistant responses and tool inputs/results, restores history, observes canonical SSE events, and cancels a turn. Each session shows chat beside an **Events** inspector (history `/items` + live SSE `/events`) and an optional Agent Manifest tab. Responses appear when complete; token streaming, media, approvals UI, and remote deployment are deferred.
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
The local Node host proxies a small allowlist of Runtime HTTP/SSE routes. It holds the server credential, stamps local ownership, and excludes executor claims/results. The browser never receives Runtime or executor credentials. Studio binds loopback and accepts only a loopback Runtime destination.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
Programmatic hosts use `startStudio({ runtimeUrl, serverKey, open: false })` and await the returned handle's `close()`. Studio uses `@nylorun/agents/client`; its browser bundle contains no harness engine. The CLI belongs to `@nylorun/cli`.
|
|
12
|
+
|
|
13
|
+
For repository development and publication, see [CONTRIBUTING](../CONTRIBUTING.md) and [RELEASING](../RELEASING.md).
|
package/dist/host.d.ts
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
export type StudioOptions = Readonly<{
|
|
2
|
-
|
|
2
|
+
runtimeUrl?: string;
|
|
3
|
+
serverKey?: string;
|
|
3
4
|
port?: number;
|
|
4
5
|
open?: boolean;
|
|
5
6
|
}>;
|
|
6
7
|
export type StudioHost = Readonly<{
|
|
7
8
|
address: string;
|
|
8
|
-
readonly
|
|
9
|
-
setAgentServerUrl(url: string | undefined): void;
|
|
9
|
+
readonly runtimeUrl: string;
|
|
10
10
|
open(): void;
|
|
11
11
|
close(): Promise<void>;
|
|
12
12
|
}>;
|
|
13
13
|
export type StudioConfig = Readonly<{
|
|
14
|
-
|
|
14
|
+
runtimeUrl: string;
|
|
15
|
+
local: true;
|
|
15
16
|
}>;
|
|
16
17
|
export declare function parseAgentServerUrl(value: string): string;
|
|
17
18
|
/** Serves the packaged React distribution and a non-secret in-memory runtime configuration. */
|
package/dist/host.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { proxyRuntime } from "./proxy.js";
|
|
2
|
+
import { createServer, } from "node:http";
|
|
2
3
|
import { readFile } from "node:fs/promises";
|
|
3
4
|
import { spawn } from "node:child_process";
|
|
4
5
|
import { dirname, extname, join, resolve, sep } from "node:path";
|
|
@@ -15,7 +16,7 @@ const MIME_TYPES = Object.freeze({
|
|
|
15
16
|
".webmanifest": "application/manifest+json; charset=utf-8",
|
|
16
17
|
".svg": "image/svg+xml",
|
|
17
18
|
".webp": "image/webp",
|
|
18
|
-
".woff2": "font/woff2"
|
|
19
|
+
".woff2": "font/woff2",
|
|
19
20
|
});
|
|
20
21
|
export function parseAgentServerUrl(value) {
|
|
21
22
|
let url;
|
|
@@ -23,23 +24,33 @@ export function parseAgentServerUrl(value) {
|
|
|
23
24
|
url = new URL(value);
|
|
24
25
|
}
|
|
25
26
|
catch {
|
|
26
|
-
throw new Error("--
|
|
27
|
+
throw new Error("--runtime-url must be an absolute http(s) URL.");
|
|
27
28
|
}
|
|
28
29
|
if (url.protocol !== "http:" && url.protocol !== "https:")
|
|
29
|
-
throw new Error("--
|
|
30
|
+
throw new Error("--runtime-url must use http or https.");
|
|
30
31
|
if (url.username !== "" || url.password !== "")
|
|
31
|
-
throw new Error("--
|
|
32
|
+
throw new Error("--runtime-url must not contain credentials.");
|
|
32
33
|
if (url.search !== "" || url.hash !== "")
|
|
33
|
-
throw new Error("--
|
|
34
|
+
throw new Error("--runtime-url must not contain a query string or fragment.");
|
|
34
35
|
return url.href.replace(/\/$/u, "");
|
|
35
36
|
}
|
|
36
37
|
function json(response, status, value) {
|
|
37
|
-
response.writeHead(status, {
|
|
38
|
+
response.writeHead(status, {
|
|
39
|
+
"content-type": "application/json; charset=utf-8",
|
|
40
|
+
"cache-control": "no-store",
|
|
41
|
+
"x-content-type-options": "nosniff",
|
|
42
|
+
});
|
|
38
43
|
response.end(`${JSON.stringify(value)}\n`);
|
|
39
44
|
}
|
|
40
|
-
function reject(response, status, message) {
|
|
45
|
+
function reject(response, status, message) {
|
|
46
|
+
json(response, status, { error: { message } });
|
|
47
|
+
}
|
|
41
48
|
function browser(address) {
|
|
42
|
-
const command = process.platform === "darwin"
|
|
49
|
+
const command = process.platform === "darwin"
|
|
50
|
+
? "open"
|
|
51
|
+
: process.platform === "win32"
|
|
52
|
+
? "cmd"
|
|
53
|
+
: "xdg-open";
|
|
43
54
|
const args = process.platform === "win32" ? ["/c", "start", "", address] : [address];
|
|
44
55
|
const child = spawn(command, args, { detached: true, stdio: "ignore" });
|
|
45
56
|
child.unref();
|
|
@@ -55,14 +66,18 @@ function staticPath(root, pathname) {
|
|
|
55
66
|
if (decoded.includes("\0"))
|
|
56
67
|
return undefined;
|
|
57
68
|
const candidate = resolve(root, decoded.replace(/^\/+/, "") || "index.html");
|
|
58
|
-
return candidate === root || candidate.startsWith(`${root}${sep}`)
|
|
69
|
+
return candidate === root || candidate.startsWith(`${root}${sep}`)
|
|
70
|
+
? candidate
|
|
71
|
+
: undefined;
|
|
59
72
|
}
|
|
60
73
|
function staticHeaders(file) {
|
|
61
74
|
const immutable = file.includes(`${sep}assets${sep}`);
|
|
62
75
|
return {
|
|
63
76
|
"content-type": MIME_TYPES[extname(file)] ?? "application/octet-stream",
|
|
64
|
-
"cache-control": immutable
|
|
65
|
-
|
|
77
|
+
"cache-control": immutable
|
|
78
|
+
? "public, max-age=31536000, immutable"
|
|
79
|
+
: "no-store",
|
|
80
|
+
"x-content-type-options": "nosniff",
|
|
66
81
|
};
|
|
67
82
|
}
|
|
68
83
|
async function sendFile(response, method, file) {
|
|
@@ -92,7 +107,9 @@ async function listenOn(host, port, handle) {
|
|
|
92
107
|
return server;
|
|
93
108
|
}
|
|
94
109
|
function ipv6Optional(error) {
|
|
95
|
-
return error instanceof Error &&
|
|
110
|
+
return (error instanceof Error &&
|
|
111
|
+
"code" in error &&
|
|
112
|
+
["EADDRNOTAVAIL", "EAFNOSUPPORT", "EPERM"].includes(String(error.code)));
|
|
96
113
|
}
|
|
97
114
|
async function bindLoopback(handle, port) {
|
|
98
115
|
const ipv4 = await listenOn("127.0.0.1", port, handle);
|
|
@@ -102,11 +119,20 @@ async function bindLoopback(handle, port) {
|
|
|
102
119
|
throw new Error("Studio did not report a TCP address.");
|
|
103
120
|
}
|
|
104
121
|
try {
|
|
105
|
-
return Object.freeze({
|
|
122
|
+
return Object.freeze({
|
|
123
|
+
port: address.port,
|
|
124
|
+
servers: Object.freeze([
|
|
125
|
+
ipv4,
|
|
126
|
+
await listenOn("::1", address.port, handle),
|
|
127
|
+
]),
|
|
128
|
+
});
|
|
106
129
|
}
|
|
107
130
|
catch (error) {
|
|
108
131
|
if (ipv6Optional(error))
|
|
109
|
-
return Object.freeze({
|
|
132
|
+
return Object.freeze({
|
|
133
|
+
port: address.port,
|
|
134
|
+
servers: Object.freeze([ipv4]),
|
|
135
|
+
});
|
|
110
136
|
ipv4.close();
|
|
111
137
|
throw error;
|
|
112
138
|
}
|
|
@@ -121,7 +147,9 @@ async function listenForStudio(handle, port) {
|
|
|
121
147
|
}
|
|
122
148
|
catch (error) {
|
|
123
149
|
lastError = error;
|
|
124
|
-
if (!(error instanceof Error) ||
|
|
150
|
+
if (!(error instanceof Error) ||
|
|
151
|
+
!("code" in error) ||
|
|
152
|
+
error.code !== "EADDRINUSE")
|
|
125
153
|
throw error;
|
|
126
154
|
}
|
|
127
155
|
}
|
|
@@ -130,22 +158,33 @@ async function listenForStudio(handle, port) {
|
|
|
130
158
|
/** Serves the packaged React distribution and a non-secret in-memory runtime configuration. */
|
|
131
159
|
export async function startStudio(options = {}) {
|
|
132
160
|
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..", "dist", "web");
|
|
133
|
-
const requestedAgentServerUrl = options.
|
|
161
|
+
const requestedAgentServerUrl = options.runtimeUrl === undefined
|
|
162
|
+
? undefined
|
|
163
|
+
: parseAgentServerUrl(options.runtimeUrl);
|
|
164
|
+
if (!requestedAgentServerUrl || !options.serverKey)
|
|
165
|
+
throw new Error("Studio requires runtimeUrl and a serverKey");
|
|
166
|
+
if (!["localhost", "127.0.0.1", "[::1]"].includes(new URL(requestedAgentServerUrl).hostname))
|
|
167
|
+
throw new Error("This Studio release connects only to a loopback Runtime");
|
|
134
168
|
let origin = "";
|
|
135
169
|
let agentServerUrl = requestedAgentServerUrl;
|
|
136
170
|
const handle = (request, response) => {
|
|
137
171
|
void (async () => {
|
|
138
172
|
const url = new URL(request.url ?? "/", origin || "http://localhost");
|
|
139
|
-
if (origin === "" ||
|
|
173
|
+
if (origin === "" ||
|
|
174
|
+
!loopbackHosts(Number(new URL(origin).port)).has(request.headers.host ?? "")) {
|
|
140
175
|
reject(response, 421, "Studio only accepts its own loopback Host header.");
|
|
141
176
|
return;
|
|
142
177
|
}
|
|
143
|
-
if (url.pathname
|
|
144
|
-
|
|
178
|
+
if (url.pathname.startsWith("/_studio/runtime/")) {
|
|
179
|
+
await proxyRuntime(request, response, {
|
|
180
|
+
origin: `http://${request.headers.host}`,
|
|
181
|
+
runtimeUrl: agentServerUrl,
|
|
182
|
+
serverKey: options.serverKey,
|
|
183
|
+
});
|
|
145
184
|
return;
|
|
146
185
|
}
|
|
147
186
|
if (url.pathname === CONFIG_PATH && request.method === "GET") {
|
|
148
|
-
json(response, 200,
|
|
187
|
+
json(response, 200, { runtimeUrl: "/_studio/runtime", local: true });
|
|
149
188
|
return;
|
|
150
189
|
}
|
|
151
190
|
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
@@ -165,22 +204,28 @@ export async function startStudio(options = {}) {
|
|
|
165
204
|
}
|
|
166
205
|
if (!(await sendFile(response, request.method, join(root, "index.html"))))
|
|
167
206
|
reject(response, 500, "Studio distribution is missing. Reinstall or rebuild @nylorun/studio.");
|
|
168
|
-
})()
|
|
207
|
+
})().catch(() => {
|
|
208
|
+
if (!response.headersSent)
|
|
209
|
+
reject(response, 500, "Studio request failed");
|
|
210
|
+
else
|
|
211
|
+
response.end();
|
|
212
|
+
});
|
|
169
213
|
};
|
|
170
214
|
const bound = await listenForStudio(handle, options.port);
|
|
171
215
|
origin = `http://localhost:${bound.port}`;
|
|
172
216
|
const address = origin;
|
|
173
217
|
const host = Object.freeze({
|
|
174
218
|
address,
|
|
175
|
-
get
|
|
176
|
-
|
|
219
|
+
get runtimeUrl() {
|
|
220
|
+
return agentServerUrl;
|
|
221
|
+
},
|
|
177
222
|
open: () => browser(address),
|
|
178
223
|
close: async () => {
|
|
179
224
|
await Promise.all(bound.servers.map((server) => new Promise((done, reject) => {
|
|
180
225
|
server.closeAllConnections();
|
|
181
226
|
server.close((error) => error === undefined ? done() : reject(error));
|
|
182
227
|
})));
|
|
183
|
-
}
|
|
228
|
+
},
|
|
184
229
|
});
|
|
185
230
|
if (options.open !== false)
|
|
186
231
|
host.open();
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,2 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { startStudio } from "./host.js";
|
|
2
2
|
export type { StudioConfig, StudioHost, StudioOptions } from "./host.js";
|
|
3
|
-
export type { StudioAgentManifest, StudioCapabilityManifest, StudioDiscoveryDocument, StudioDiscoveryEntry, StudioEndpointSet, StudioManifestTool, } from "./protocol.js";
|
package/dist/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { startStudio } from "./host.js";
|
package/dist/proxy.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
/** Local tooling proxy: credentials stay in this process, not the browser. */
|
|
3
|
+
export declare function proxyRuntime(request: IncomingMessage, response: ServerResponse, options: {
|
|
4
|
+
origin: string;
|
|
5
|
+
runtimeUrl: string;
|
|
6
|
+
serverKey: string;
|
|
7
|
+
}): Promise<void>;
|
package/dist/proxy.js
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/** Local tooling proxy: credentials stay in this process, not the browser. */
|
|
2
|
+
export async function proxyRuntime(request, response, options) {
|
|
3
|
+
const incoming = new URL(request.url, options.origin);
|
|
4
|
+
const path = incoming.pathname.slice("/_studio/runtime".length);
|
|
5
|
+
const method = request.method ?? "GET";
|
|
6
|
+
const fail = (status, message) => {
|
|
7
|
+
response.writeHead(status, { "content-type": "application/json" });
|
|
8
|
+
response.end(JSON.stringify({ message }));
|
|
9
|
+
};
|
|
10
|
+
const read = method === "GET" &&
|
|
11
|
+
(/^\/v1\/(agents|sessions)$/.test(path) ||
|
|
12
|
+
/^\/v1\/sessions\/[^/]+(?:\/(items|events))?$/.test(path));
|
|
13
|
+
const write = (method === "PUT" && /^\/v1\/sessions\/[^/]+$/.test(path)) ||
|
|
14
|
+
(method === "POST" && /^\/v1\/sessions\/[^/]+\/commands$/.test(path));
|
|
15
|
+
if (!read && !write)
|
|
16
|
+
return fail(404, "Unsupported Studio operation");
|
|
17
|
+
if (write && request.headers.origin !== options.origin)
|
|
18
|
+
return fail(403, "Studio mutations require a same-origin request");
|
|
19
|
+
let body;
|
|
20
|
+
if (write) {
|
|
21
|
+
if (!request.headers["content-type"]?.startsWith("application/json"))
|
|
22
|
+
return fail(415, "JSON required");
|
|
23
|
+
let text = "";
|
|
24
|
+
for await (const chunk of request) {
|
|
25
|
+
text += chunk;
|
|
26
|
+
if (Buffer.byteLength(text) > 1024 * 1024)
|
|
27
|
+
return fail(413, "Request too large");
|
|
28
|
+
}
|
|
29
|
+
let value;
|
|
30
|
+
try {
|
|
31
|
+
value = JSON.parse(text);
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
return fail(400, "Invalid JSON");
|
|
35
|
+
}
|
|
36
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
37
|
+
return fail(400, "JSON object required");
|
|
38
|
+
if (method === "PUT")
|
|
39
|
+
value.ownerUserId = "local-developer";
|
|
40
|
+
else if (!["message", "cancel"].includes(value.type))
|
|
41
|
+
return fail(400, "This Studio release supports message and cancel commands");
|
|
42
|
+
body = JSON.stringify(value);
|
|
43
|
+
}
|
|
44
|
+
const controller = new AbortController();
|
|
45
|
+
response.on("close", () => controller.abort());
|
|
46
|
+
try {
|
|
47
|
+
const upstream = await fetch(options.runtimeUrl + path + incoming.search, {
|
|
48
|
+
method,
|
|
49
|
+
body,
|
|
50
|
+
redirect: "error",
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
headers: {
|
|
53
|
+
authorization: `Bearer ${options.serverKey}`,
|
|
54
|
+
...(body ? { "content-type": "application/json" } : {}),
|
|
55
|
+
...(request.headers["last-event-id"]
|
|
56
|
+
? { "last-event-id": String(request.headers["last-event-id"]) }
|
|
57
|
+
: {}),
|
|
58
|
+
accept: request.headers.accept ?? "application/json",
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
response.writeHead(upstream.status, {
|
|
62
|
+
"content-type": upstream.headers.get("content-type") ?? "application/json",
|
|
63
|
+
"cache-control": "no-store",
|
|
64
|
+
"x-accel-buffering": "no",
|
|
65
|
+
});
|
|
66
|
+
response.flushHeaders();
|
|
67
|
+
if (upstream.body)
|
|
68
|
+
for await (const chunk of upstream.body) {
|
|
69
|
+
if (!response.write(chunk))
|
|
70
|
+
await new Promise((resolve) => {
|
|
71
|
+
const done = () => {
|
|
72
|
+
response.off("drain", done);
|
|
73
|
+
response.off("close", done);
|
|
74
|
+
resolve();
|
|
75
|
+
};
|
|
76
|
+
response.once("drain", done);
|
|
77
|
+
response.once("close", done);
|
|
78
|
+
});
|
|
79
|
+
if (controller.signal.aborted)
|
|
80
|
+
break;
|
|
81
|
+
}
|
|
82
|
+
response.end();
|
|
83
|
+
}
|
|
84
|
+
catch {
|
|
85
|
+
if (!response.headersSent)
|
|
86
|
+
fail(502, "Local Runtime is unavailable");
|
|
87
|
+
else
|
|
88
|
+
response.end();
|
|
89
|
+
}
|
|
90
|
+
}
|