@nylorun/studio 0.4.2-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 CHANGED
@@ -1,5 +1,41 @@
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
+
24
+ ## 0.5.0-beta
25
+
26
+ ### Minor Changes
27
+
28
+ - Breaking: Studio reads `manifest.capabilities` (capability id, `kind`, `hasMiddleware`, tool
29
+ schemas). `manifestCapabilities()` still accepts legacy `middleware` / `harness.manifest`
30
+ documents. `StudioMiddlewareManifest` is now `StudioCapabilityManifest`.
31
+ - c5bbb1a: Breaking beta: make Harness `run()` a direct async state-in/state-out executor with
32
+ serializable pauses, application `info`, cancellation signals, awaited recording, and
33
+ agent-level output schemas. Runtime owns session scheduling with memory-default or exclusive
34
+ local storage and imports Harness contracts. Isolate Node adapters under `runtime/node`, stream
35
+ observations incrementally, and add opt-in bounded token previews with Studio reconciliation.
36
+ Migrate consumers and deployment guidance together; legacy event records remain archived, not
37
+ automatically replayed.
38
+
3
39
  ## 0.4.2-beta
4
40
 
5
41
  ### Patch Changes
package/README.md CHANGED
@@ -1,11 +1,13 @@
1
- # `@nylorun/studio`
1
+ # @nylorun/studio
2
2
 
3
- Local developer Studio for Nylorun agents.
3
+ Local session dashboard for the OSS Runtime. Requires Node 24.
4
4
 
5
- See [docs.nylorun.com](https://docs.nylorun.com) for installation, configuration, and usage.
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
- ## Package independence
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
- Studio exposes `startStudio()` and has no engine or Runtime dependency. The `nylorun` executable is supplied by `@nylorun/runtime`; Studio no longer publishes `nylo`. Runtime loads the application-installed Studio for development or dashboard attachment. Studio accepts neutral version-2 manifests as well as legacy `harness.manifest` documents.
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
- Repository development: [contributing](../CONTRIBUTING.md). Package publication: [releasing](../RELEASING.md).
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
- agentServerUrl?: string;
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 agentServerUrl: string;
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
- agentServerUrl?: string;
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 { createServer } from "node:http";
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("--agent-server-url must be an absolute http(s) URL.");
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("--agent-server-url must use http or https.");
30
+ throw new Error("--runtime-url must use http or https.");
30
31
  if (url.username !== "" || url.password !== "")
31
- throw new Error("--agent-server-url must not contain credentials.");
32
+ throw new Error("--runtime-url must not contain credentials.");
32
33
  if (url.search !== "" || url.hash !== "")
33
- throw new Error("--agent-server-url must not contain a query string or fragment.");
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, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store", "x-content-type-options": "nosniff" });
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) { json(response, status, { error: { 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" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
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}`) ? candidate : undefined;
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 ? "public, max-age=31536000, immutable" : "no-store",
65
- "x-content-type-options": "nosniff"
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 && "code" in error && ["EADDRNOTAVAIL", "EAFNOSUPPORT", "EPERM"].includes(String(error.code));
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({ port: address.port, servers: Object.freeze([ipv4, await listenOn("::1", address.port, handle)]) });
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({ port: address.port, servers: Object.freeze([ipv4]) });
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) || !("code" in error) || error.code !== "EADDRINUSE")
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.agentServerUrl === undefined ? undefined : parseAgentServerUrl(options.agentServerUrl);
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 === "" || !loopbackHosts(Number(new URL(origin).port)).has(request.headers.host ?? "")) {
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 === "/_studio" || url.pathname.startsWith("/_studio/")) {
144
- reject(response, 404, "Studio has no API routes.");
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, agentServerUrl === undefined ? {} : { agentServerUrl });
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 agentServerUrl() { return agentServerUrl ?? ""; },
176
- setAgentServerUrl: (url) => { agentServerUrl = url === undefined ? undefined : parseAgentServerUrl(url); },
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 { parseAgentServerUrl, startStaticStudio, startStudio } from "./host.js";
1
+ export { startStudio } from "./host.js";
2
2
  export type { StudioConfig, StudioHost, StudioOptions } from "./host.js";
3
- export type { StudioAgentManifest, StudioDiscoveryDocument, StudioDiscoveryEntry, StudioEndpointSet, StudioMiddlewareManifest } from "./protocol.js";
package/dist/index.js CHANGED
@@ -1 +1 @@
1
- export { parseAgentServerUrl, startStaticStudio, startStudio } from "./host.js";
1
+ export { startStudio } from "./host.js";
@@ -19,13 +19,18 @@ export type StudioEndpointSet = Readonly<{
19
19
  agUi: string;
20
20
  sessions: string;
21
21
  }>;
22
- export type StudioMiddlewareManifest = Readonly<{
22
+ export type StudioManifestTool = Readonly<{
23
+ name: string;
24
+ description?: string;
25
+ inputSchema?: Readonly<Record<string, unknown>>;
26
+ outputSchema?: Readonly<Record<string, unknown>>;
27
+ }>;
28
+ export type StudioCapabilityManifest = Readonly<{
23
29
  id: string;
30
+ kind?: "agent" | "capability" | "middleware";
31
+ hasMiddleware?: boolean;
24
32
  instructions?: readonly string[];
25
- tools?: readonly Readonly<{
26
- name: string;
27
- description?: string;
28
- }>[];
33
+ tools?: readonly StudioManifestTool[];
29
34
  model?: Readonly<{
30
35
  id?: string;
31
36
  controls?: Readonly<{
@@ -34,27 +39,27 @@ export type StudioMiddlewareManifest = Readonly<{
34
39
  }>;
35
40
  }>;
36
41
  }>;
42
+ type StudioInnerManifest = Readonly<{
43
+ id: string;
44
+ name: string;
45
+ outputSchema?: Readonly<Record<string, unknown>>;
46
+ capabilities?: readonly StudioCapabilityManifest[];
47
+ middleware?: readonly StudioCapabilityManifest[];
48
+ }>;
37
49
  /** A generated, JSON-safe description of one direct Harness agent. */
38
50
  export type StudioAgentManifest = Readonly<{
39
51
  protocolVersion: 1 | 2;
40
52
  id: string;
41
53
  name: string;
42
- manifest?: Readonly<{
43
- id: string;
44
- name: string;
45
- middleware?: readonly StudioMiddlewareManifest[];
46
- }>;
54
+ manifest?: StudioInnerManifest;
47
55
  engine?: Readonly<{
48
56
  name: string;
49
57
  details?: unknown;
50
58
  }>;
51
59
  harness?: Readonly<{
52
- manifest: Readonly<{
53
- id: string;
54
- name: string;
55
- middleware: readonly StudioMiddlewareManifest[];
56
- }>;
60
+ manifest: StudioInnerManifest;
57
61
  }>;
58
62
  endpoints: StudioEndpointSet;
59
63
  }>;
60
- export declare function manifestMiddleware(agent: StudioAgentManifest): readonly StudioMiddlewareManifest[];
64
+ export declare function manifestCapabilities(agent: StudioAgentManifest): readonly StudioCapabilityManifest[];
65
+ export {};
package/dist/protocol.js CHANGED
@@ -5,6 +5,10 @@ export function isStudioDiscovery(value) {
5
5
  return ((candidate.protocolVersion === 1 || candidate.protocolVersion === 2) &&
6
6
  Array.isArray(candidate.agents));
7
7
  }
8
- export function manifestMiddleware(agent) {
9
- return agent.manifest?.middleware ?? agent.harness?.manifest.middleware ?? [];
8
+ export function manifestCapabilities(agent) {
9
+ return (agent.manifest?.capabilities ??
10
+ agent.manifest?.middleware ??
11
+ agent.harness?.manifest.capabilities ??
12
+ agent.harness?.manifest.middleware ??
13
+ []);
10
14
  }
@@ -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
+ }