@nylorun/agents 0.12.0-beta → 0.14.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.
@@ -5,8 +5,8 @@
5
5
  * person, so the Runtime keeps one person out of another's threads and does all the protocol
6
6
  * work. The browser never sees the Tenant key, the Runtime's URL or the Tenant id.
7
7
  *
8
- * The same threads are reachable directly from a browser with a subject token: each thread is
9
- * one session per subject, agent and thread on both paths.
8
+ * The same threads are reachable directly from a browser with a trusted issuer's token (through
9
+ * the operator's proxy): each thread is one session per subject, agent and thread on both paths.
10
10
  */
11
11
  import { type SubjectScope } from "@nylorun/core/contracts";
12
12
  import type { BuiltWorkflow } from "@nylorun/core/define";
@@ -27,10 +27,7 @@ export interface AgUiHandlerOptions {
27
27
  basePath?: string;
28
28
  /** Application client. Default: `createClient()`, from the environment or the Project link. */
29
29
  client?: AgentsClient | Promise<AgentsClient>;
30
- /**
31
- * What each subject may do through this endpoint. Default `["sessions:own"]`; add
32
- * `vaults:own` if `session()` attaches the person's vaults.
33
- */
30
+ /** What each subject may do through this endpoint. Default `["sessions:own"]`. */
34
31
  scopes?: readonly SubjectScope[];
35
32
  /** Optional per-session parameters, e.g. the person's vault for connected accounts. */
36
33
  session?(subject: string, agentId: string): AgUiSessionOptions | Promise<AgUiSessionOptions>;
@@ -5,8 +5,8 @@
5
5
  * person, so the Runtime keeps one person out of another's threads and does all the protocol
6
6
  * work. The browser never sees the Tenant key, the Runtime's URL or the Tenant id.
7
7
  *
8
- * The same threads are reachable directly from a browser with a subject token: each thread is
9
- * one session per subject, agent and thread on both paths.
8
+ * The same threads are reachable directly from a browser with a trusted issuer's token (through
9
+ * the operator's proxy): each thread is one session per subject, agent and thread on both paths.
10
10
  */
11
11
  import { parseSubjectHeaders } from "@nylorun/core/contracts";
12
12
  import { createClient, } from "../client.js";
@@ -0,0 +1,116 @@
1
+ /**
2
+ * Artifacts (protocol 6): `client.artifacts`. A file is uploaded in one streamed request and
3
+ * kept as an artifact with numbered versions; a session's artifacts appear in its history as
4
+ * `artifact.*` events. Download through the Runtime (with Range), or mint a capability link: a
5
+ * short-lived URL that needs no credential, for `<img>` tags and sharing. A message names an
6
+ * artifact in a file part (`session.inputParts`), and the model reads it.
7
+ *
8
+ * Folders (F8.2): at the end of each turn the Runtime exports `/workspace/outputs` of the
9
+ * session's sandbox as a version of its `outputs` folder. Read one as a tree, one file by path,
10
+ * a diff between versions or a zip.
11
+ *
12
+ * Acting for a person (`client.as`, or a trusted issuer's token), only the artifacts of their own sessions.
13
+ */
14
+ import type { ArtifactDiff, ArtifactLink, ArtifactTree, ArtifactView, DeleteArtifactResponse, UploadArtifactResponse } from "@nylorun/core/contracts";
15
+ import { type Transport } from "./http.js";
16
+ /** What an upload sends: bytes, a `Blob` or `File`, text, or a stream of bytes. */
17
+ export type ArtifactBody = Uint8Array | ArrayBuffer | Blob | string | ReadableStream<Uint8Array>;
18
+ export interface UploadArtifactOptions {
19
+ /** The file's name, e.g. `brief.pdf`. */
20
+ readonly name: string;
21
+ /** The session it belongs to; required when acting for a person. */
22
+ readonly sessionId?: string;
23
+ /** The media type. Default: a `Blob`'s type, else what the name implies. */
24
+ readonly contentType?: string;
25
+ readonly labels?: Readonly<Record<string, string>>;
26
+ readonly signal?: AbortSignal;
27
+ }
28
+ /** A capability link, with the absolute URL to open on this Runtime. */
29
+ export type ArtifactLinkWithUrl = ArtifactLink & {
30
+ readonly url: string;
31
+ };
32
+ export declare class ArtifactsClient {
33
+ private readonly transport;
34
+ constructor(transport: Transport);
35
+ private send;
36
+ /**
37
+ * Uploads a file as a new artifact (`POST /v1/artifacts`), in one streamed request. A file past
38
+ * the Tenant's per-file limit, or past its total, is refused with `413 limit_exceeded`.
39
+ */
40
+ upload(body: ArtifactBody, options: UploadArtifactOptions): Promise<UploadArtifactResponse>;
41
+ /** Uploads the artifact's next version. */
42
+ uploadVersion(artifactId: string, body: ArtifactBody, options?: {
43
+ contentType?: string;
44
+ signal?: AbortSignal;
45
+ }): Promise<UploadArtifactResponse>;
46
+ /** The artifacts of one session, or of every session this client reaches; oldest first. */
47
+ list(options?: {
48
+ sessionId?: string;
49
+ signal?: AbortSignal;
50
+ }): Promise<ArtifactView[]>;
51
+ /** The artifact, with every version. */
52
+ get(artifactId: string, options?: {
53
+ signal?: AbortSignal;
54
+ }): Promise<ArtifactView>;
55
+ /**
56
+ * A version's bytes as the Runtime's response, to stream (`response.body`) or read. With
57
+ * `range`, only those bytes (`206`, `Content-Range`). Default: the latest version.
58
+ */
59
+ download(artifactId: string, options?: {
60
+ version?: number;
61
+ /** Bytes `start` to `end`, both inclusive; without `end`, to the end. */
62
+ range?: {
63
+ start: number;
64
+ end?: number;
65
+ };
66
+ signal?: AbortSignal;
67
+ }): Promise<Response>;
68
+ /**
69
+ * A capability link to one version (default: the latest): anyone holding the URL may download
70
+ * it, with no credential, until it expires (default 5 minutes, at most 15) or the artifact is
71
+ * deleted. A folder's link opens its zip, or with `file` that one file.
72
+ */
73
+ link(artifactId: string, options?: {
74
+ version?: number;
75
+ expiresIn?: number;
76
+ file?: string;
77
+ signal?: AbortSignal;
78
+ }): Promise<ArtifactLinkWithUrl>;
79
+ /** A folder version's files (default: the latest), sorted by path. */
80
+ tree(artifactId: string, options?: {
81
+ version?: number;
82
+ signal?: AbortSignal;
83
+ }): Promise<ArtifactTree>;
84
+ /**
85
+ * One file of a folder version (default: the latest) by its path in the folder, as the
86
+ * Runtime's response; with `range`, only those bytes (`206`).
87
+ */
88
+ file(artifactId: string, path: string, options?: {
89
+ version?: number;
90
+ /** Bytes `start` to `end`, both inclusive; without `end`, to the end. */
91
+ range?: {
92
+ start: number;
93
+ end?: number;
94
+ };
95
+ signal?: AbortSignal;
96
+ }): Promise<Response>;
97
+ /**
98
+ * What changed in a folder from version `from` (default: the one before) to `version`
99
+ * (default: the latest): files added, removed and changed.
100
+ */
101
+ diff(artifactId: string, options?: {
102
+ version?: number;
103
+ from?: number;
104
+ signal?: AbortSignal;
105
+ }): Promise<ArtifactDiff>;
106
+ /** A folder version (default: the latest) as a zip archive, streamed: the Runtime's response. */
107
+ zip(artifactId: string, options?: {
108
+ version?: number;
109
+ signal?: AbortSignal;
110
+ }): Promise<Response>;
111
+ private versionPath;
112
+ /** Deletes the artifact, every version and its bytes; its links stop working. */
113
+ delete(artifactId: string, options?: {
114
+ signal?: AbortSignal;
115
+ }): Promise<DeleteArtifactResponse>;
116
+ }
@@ -0,0 +1,116 @@
1
+ import { segment } from "./http.js";
2
+ function contentTypeOf(body, declared) {
3
+ if (declared)
4
+ return declared;
5
+ if (typeof Blob !== "undefined" && body instanceof Blob && body.type)
6
+ return body.type;
7
+ return typeof body === "string" ? "text/plain; charset=utf-8" : "application/octet-stream";
8
+ }
9
+ export class ArtifactsClient {
10
+ transport;
11
+ constructor(transport) {
12
+ this.transport = transport;
13
+ }
14
+ async send(path, body, contentType, signal) {
15
+ const response = await this.transport.request(path, {
16
+ method: "POST",
17
+ headers: { "content-type": contentType },
18
+ body: body,
19
+ // A stream is sent as it is read (Node's fetch needs this said).
20
+ ...(typeof ReadableStream !== "undefined" && body instanceof ReadableStream
21
+ ? { duplex: "half" }
22
+ : {}),
23
+ ...(signal ? { signal } : {}),
24
+ });
25
+ return (await response.json());
26
+ }
27
+ /**
28
+ * Uploads a file as a new artifact (`POST /v1/artifacts`), in one streamed request. A file past
29
+ * the Tenant's per-file limit, or past its total, is refused with `413 limit_exceeded`.
30
+ */
31
+ upload(body, options) {
32
+ const query = new URLSearchParams({ name: options.name });
33
+ if (options.sessionId !== undefined)
34
+ query.set("sessionId", options.sessionId);
35
+ for (const [key, value] of Object.entries(options.labels ?? {}))
36
+ query.append("label", `${key}=${value}`);
37
+ return this.send(`/v1/artifacts?${query}`, body, contentTypeOf(body, options.contentType), options.signal);
38
+ }
39
+ /** Uploads the artifact's next version. */
40
+ uploadVersion(artifactId, body, options = {}) {
41
+ return this.send(`/v1/artifacts/${segment(artifactId)}/versions`, body, contentTypeOf(body, options.contentType), options.signal);
42
+ }
43
+ /** The artifacts of one session, or of every session this client reaches; oldest first. */
44
+ async list(options = {}) {
45
+ const query = options.sessionId === undefined ? "" : `?sessionId=${segment(options.sessionId)}`;
46
+ const reply = await this.transport.json(`/v1/artifacts${query}`, "GET", undefined, options.signal);
47
+ return reply.artifacts;
48
+ }
49
+ /** The artifact, with every version. */
50
+ get(artifactId, options = {}) {
51
+ return this.transport.json(`/v1/artifacts/${segment(artifactId)}`, "GET", undefined, options.signal);
52
+ }
53
+ /**
54
+ * A version's bytes as the Runtime's response, to stream (`response.body`) or read. With
55
+ * `range`, only those bytes (`206`, `Content-Range`). Default: the latest version.
56
+ */
57
+ download(artifactId, options = {}) {
58
+ const range = options.range;
59
+ return this.transport.request(`/v1/artifacts/${segment(artifactId)}/versions/${options.version ?? "latest"}/content`, {
60
+ method: "GET",
61
+ ...(range ? { headers: { range: `bytes=${range.start}-${range.end ?? ""}` } } : {}),
62
+ ...(options.signal ? { signal: options.signal } : {}),
63
+ });
64
+ }
65
+ /**
66
+ * A capability link to one version (default: the latest): anyone holding the URL may download
67
+ * it, with no credential, until it expires (default 5 minutes, at most 15) or the artifact is
68
+ * deleted. A folder's link opens its zip, or with `file` that one file.
69
+ */
70
+ async link(artifactId, options = {}) {
71
+ const link = await this.transport.json(`/v1/artifacts/${segment(artifactId)}/links`, "POST", {
72
+ ...(options.version === undefined ? {} : { version: options.version }),
73
+ ...(options.expiresIn === undefined ? {} : { expiresIn: options.expiresIn }),
74
+ ...(options.file === undefined ? {} : { file: options.file }),
75
+ }, options.signal);
76
+ return { ...link, url: `${this.transport.url}${link.path}` };
77
+ }
78
+ /** A folder version's files (default: the latest), sorted by path. */
79
+ tree(artifactId, options = {}) {
80
+ return this.transport.json(`${this.versionPath(artifactId, options.version)}/tree`, "GET", undefined, options.signal);
81
+ }
82
+ /**
83
+ * One file of a folder version (default: the latest) by its path in the folder, as the
84
+ * Runtime's response; with `range`, only those bytes (`206`).
85
+ */
86
+ file(artifactId, path, options = {}) {
87
+ const range = options.range;
88
+ return this.transport.request(`${this.versionPath(artifactId, options.version)}/files/${encodeURIComponent(path)}`, {
89
+ method: "GET",
90
+ ...(range ? { headers: { range: `bytes=${range.start}-${range.end ?? ""}` } } : {}),
91
+ ...(options.signal ? { signal: options.signal } : {}),
92
+ });
93
+ }
94
+ /**
95
+ * What changed in a folder from version `from` (default: the one before) to `version`
96
+ * (default: the latest): files added, removed and changed.
97
+ */
98
+ diff(artifactId, options = {}) {
99
+ const query = options.from === undefined ? "" : `?from=${options.from}`;
100
+ return this.transport.json(`${this.versionPath(artifactId, options.version)}/diff${query}`, "GET", undefined, options.signal);
101
+ }
102
+ /** A folder version (default: the latest) as a zip archive, streamed: the Runtime's response. */
103
+ zip(artifactId, options = {}) {
104
+ return this.transport.request(`${this.versionPath(artifactId, options.version)}/zip`, {
105
+ method: "GET",
106
+ ...(options.signal ? { signal: options.signal } : {}),
107
+ });
108
+ }
109
+ versionPath(artifactId, version) {
110
+ return `/v1/artifacts/${segment(artifactId)}/versions/${version ?? "latest"}`;
111
+ }
112
+ /** Deletes the artifact, every version and its bytes; its links stop working. */
113
+ delete(artifactId, options = {}) {
114
+ return this.transport.json(`/v1/artifacts/${segment(artifactId)}`, "DELETE", undefined, options.signal);
115
+ }
116
+ }
@@ -5,9 +5,9 @@
5
5
  * WebCrypto's ECDSA verifies.
6
6
  */
7
7
  import { PROTOCOL_HEADER, PROTOCOL_VERSION, isTenantId } from "@nylorun/core/compatibility";
8
- import { DELIVERY_TOKEN_TYPE, subjectTokenIssuer } from "@nylorun/core/contracts";
8
+ import { DELIVERY_TOKEN_TYPE, tenantTokenIssuer } from "@nylorun/core/contracts";
9
9
  /** The issuer prefix of a Tenant's tokens: `urn:nylorun:tenant:<tenantId>`. */
10
- const ISSUER_PREFIX = subjectTokenIssuer("");
10
+ const ISSUER_PREFIX = tenantTokenIssuer("");
11
11
  /** Why a delivery was refused; `code` is sent back to the Runtime. */
12
12
  export class DeliveryVerificationError extends Error {
13
13
  code;
@@ -60,7 +60,7 @@ export async function verifyDeliveryToken(token, options) {
60
60
  if (!claims.iss.startsWith(ISSUER_PREFIX) ||
61
61
  !isTenantId(claims.iss.slice(ISSUER_PREFIX.length)))
62
62
  throw invalid("The token was not issued by a Tenant");
63
- if (options.tenantId !== undefined && claims.iss !== subjectTokenIssuer(options.tenantId))
63
+ if (options.tenantId !== undefined && claims.iss !== tenantTokenIssuer(options.tenantId))
64
64
  throw invalid("The token is for another Tenant");
65
65
  if (options.audience !== undefined && claims.aud !== options.audience)
66
66
  throw invalid(`The token is for ${claims.aud}, not ${options.audience}`);
package/dist/http.d.ts CHANGED
@@ -1,8 +1,9 @@
1
1
  import { type Compatibility, type ProtocolRange } from "@nylorun/core/compatibility";
2
2
  /**
3
- * Subject tokens for a client that holds no Tenant key (a browser or an app; Host feature
4
- * `subject-tokens`). `get` returns a current token and may fetch a new one; `invalidate`
5
- * drops the cached one after the Runtime answered `401 token_expired`.
3
+ * Bearer tokens for a client that holds no Tenant key: a trusted issuer's tokens (Host feature
4
+ * `trusted-issuers`), from the operator's identity provider. `get` returns a current token and
5
+ * may fetch a new one; `invalidate` drops the cached one after the Runtime answered
6
+ * `401 token_expired`.
6
7
  */
7
8
  export interface TokenSource {
8
9
  get(signal?: AbortSignal): Promise<string>;
@@ -16,10 +17,8 @@ export interface Destination {
16
17
  url?: string;
17
18
  key?: string;
18
19
  fetch?: typeof fetch;
19
- /** Instead of `key`: subject tokens. The client then skips the `/health` check. */
20
+ /** Instead of `key`: a trusted issuer's tokens. The client then skips the `/health` check. */
20
21
  token?: TokenSource;
21
- /** A publishable key (Host feature `browser-access`): names the app and its origins. */
22
- publishableKey?: string;
23
22
  }
24
23
  export declare function env(name: string): string | undefined;
25
24
  export declare class RuntimeError extends Error {
@@ -39,10 +38,9 @@ export declare class IncompatibleRuntimeError extends Error {
39
38
  }
40
39
  export declare class Transport {
41
40
  readonly url: string;
42
- /** The Tenant key; empty for a client that uses subject tokens. */
41
+ /** The Tenant key; empty for a client that uses issuer tokens. */
43
42
  readonly key: string;
44
43
  readonly token: TokenSource | undefined;
45
- readonly publishableKey: string | undefined;
46
44
  readonly fetcher: typeof fetch;
47
45
  /** Sent on every request, e.g. `Nylorun-Subject` and `Nylorun-Scopes` (`withHeaders`). */
48
46
  readonly headers: Readonly<Record<string, string>>;
package/dist/http.js CHANGED
@@ -1,4 +1,4 @@
1
- import { PROTOCOL_FEATURES, PROTOCOL_HEADER, PROTOCOL_VERSION, PUBLISHABLE_KEY_HEADER, checkCompatibility, } from "@nylorun/core/compatibility";
1
+ import { PROTOCOL_FEATURES, PROTOCOL_HEADER, PROTOCOL_VERSION, checkCompatibility, } from "@nylorun/core/compatibility";
2
2
  export function env(name) {
3
3
  return typeof process !== "undefined" ? process.env[name] : undefined;
4
4
  }
@@ -54,10 +54,9 @@ function parseProtocolRange(value) {
54
54
  }
55
55
  export class Transport {
56
56
  url;
57
- /** The Tenant key; empty for a client that uses subject tokens. */
57
+ /** The Tenant key; empty for a client that uses issuer tokens. */
58
58
  key;
59
59
  token;
60
- publishableKey;
61
60
  fetcher;
62
61
  /** Sent on every request, e.g. `Nylorun-Subject` and `Nylorun-Scopes` (`withHeaders`). */
63
62
  headers = {};
@@ -69,14 +68,13 @@ export class Transport {
69
68
  if (!url || (!token && !key))
70
69
  throw new Error("Set Runtime url and server key explicitly or via NYLORUN_RUNTIME_URL / NYLORUN_SERVER_KEY");
71
70
  if (token && options.key)
72
- throw new Error("Use a Tenant key or subject tokens, not both");
71
+ throw new Error("Use a Tenant key or issuer tokens, not both");
73
72
  const parsed = new URL(url);
74
73
  if (!["http:", "https:"].includes(parsed.protocol))
75
74
  throw new Error("Runtime requires an HTTP(S) URL");
76
75
  this.url = url.replace(/\/$/, "");
77
76
  this.key = key;
78
77
  this.token = token;
79
- this.publishableKey = options.publishableKey;
80
78
  // Browsers may not call `/health` (it refuses `Origin`); token clients rely on `426`.
81
79
  if (token)
82
80
  this.check.compatible = true;
@@ -165,10 +163,9 @@ export class Transport {
165
163
  ? await this.token.get(init.signal ?? undefined)
166
164
  : this.key;
167
165
  headers.set("Authorization", `Bearer ${bearer}`);
168
- if (this.publishableKey)
169
- headers.set(PUBLISHABLE_KEY_HEADER, this.publishableKey);
170
166
  headers.set(PROTOCOL_HEADER, String(PROTOCOL_VERSION));
171
- if (init.body)
167
+ // JSON unless the caller says otherwise (an artifact upload sends the file's type).
168
+ if (init.body && !headers.has("content-type"))
172
169
  headers.set("Content-Type", "application/json");
173
170
  return headers;
174
171
  }
@@ -179,7 +176,7 @@ export class Transport {
179
176
  headers: await this.authHeaders(init),
180
177
  redirect: "error",
181
178
  });
182
- // An expired or revoked subject token: get a new one and try once more.
179
+ // An expired token: get a new one and try once more.
183
180
  if (response.status === 401 && this.token && !options.retried401) {
184
181
  const body = (await response.clone().json().catch(() => undefined));
185
182
  if (body?.code === "token_expired") {
package/dist/index.d.ts CHANGED
@@ -4,14 +4,14 @@ export { Agent, AgentBuilder } from "./builder.js";
4
4
  export { AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, CapabilityBuilder, flow, tool, defineSchema, ToolError, } from "@nylorun/core/define";
5
5
  export type { FlowAgentBuilder, Flow, Named, StageArgs, NestedStageArgs, CapabilityIdentity, } from "@nylorun/core/define";
6
6
  export type { AgentOptions, AgentManifest, BuiltAgent, BuiltWorkflow, LoopOptions, LoopDecideArgs, LoopVerifyArgs, LoopDecision, ChainOptions, SwitchOptions, ParallelOptions, MapOptions, Verdict, WorkflowBinding, WorkflowManifest, ToolDefinition, ToolExecutionContext, ToolOutcome, Patch, Decision, TurnDecision, HookScope, BeforeHook, AfterHook, JsonValue, JsonObject, } from "@nylorun/core/define";
7
- export type { LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, Action, ActionOutcome, CredentialInfo, CredentialSelection, VaultInfo, SubjectScope, } from "@nylorun/core/contracts";
7
+ export type { LiveEvent, SessionEvent, SessionEventOf, EventType, SessionCommand, Action, ActionOutcome, CredentialInfo, CredentialSelection, VaultInfo, SubjectScope, SandboxView, SandboxEvent, SandboxKind, MessagePart, ArtifactView, ArtifactVersionView, ArtifactLink, UploadArtifactResponse, ArtifactTree, ArtifactDiff, FolderEntry, } from "@nylorun/core/contracts";
8
8
  export { SUBJECT_SCOPES } from "@nylorun/core/contracts";
9
9
  export { AgentsClient, SessionClient, createClient } from "./client.js";
10
- export type { ActAsOptions, AgentSource, SessionView, CommandOptions, SessionSandbox, } from "./client.js";
11
- export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
12
- export type { CreateTokenOptions } from "./access.js";
13
- export { createTokenEndpoint } from "./token-endpoint.js";
14
- export type { TokenEndpointOptions } from "./token-endpoint.js";
10
+ export { ArtifactsClient } from "./artifacts.js";
11
+ export type { ArtifactBody, ArtifactLinkWithUrl, UploadArtifactOptions, } from "./artifacts.js";
12
+ export type { ActAsOptions, AgentSource, SessionView, CommandOptions, CreateSessionOptions, SessionSandbox, ForSessionOptions, SandboxSpec, SessionSandboxHandle, } from "./client.js";
13
+ export { SandboxesClient } from "./client.js";
14
+ export { AccessClient, SigningKeysClient } from "./access.js";
15
15
  export { createActionHandler } from "./action-handler.js";
16
16
  export type { ActionHandler, ActionHandlerOptions, RegisterOptions, } from "./action-handler.js";
17
17
  export type { ExecuteActionOptions, ExecutableDefinition, } from "./execute-action.js";
package/dist/index.js CHANGED
@@ -4,8 +4,9 @@ export { Agent, AgentBuilder } from "./builder.js";
4
4
  export { AgentBuildError, AgentLifecycleError, Loop, Chain, Switch, Parallel, Map, VerdictSchema, isVerdict, withInstructions, withoutTools, WorkflowBuildError, isBuiltWorkflow, capability, CapabilityBuilder, flow, tool, defineSchema, ToolError, } from "@nylorun/core/define";
5
5
  export { SUBJECT_SCOPES } from "@nylorun/core/contracts";
6
6
  export { AgentsClient, SessionClient, createClient } from "./client.js";
7
- export { AccessClient, PublishableKeysClient, SigningKeysClient, TokensClient, } from "./access.js";
8
- export { createTokenEndpoint } from "./token-endpoint.js";
7
+ export { ArtifactsClient } from "./artifacts.js";
8
+ export { SandboxesClient } from "./client.js";
9
+ export { AccessClient, SigningKeysClient } from "./access.js";
9
10
  export { createActionHandler } from "./action-handler.js";
10
11
  export { resolveConnection, ConnectionError } from "./connection.js";
11
12
  export { RuntimeError, IncompatibleRuntimeError } from "./http.js";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Sandboxes as a resource (Host feature `sandboxes`): `client.sandboxes`. A sandbox has its own
3
+ * id and outlives the sessions attached to it; whether it serves one session, one person or a
4
+ * project is the app's choice, made here:
5
+ *
6
+ * - `sandboxes.forSession(...)`: one sandbox for one session. It creates the sandbox, opens the
7
+ * session attached to it, and `release()` deletes the sandbox when the session is done. It
8
+ * replaces sharing through another session (`sandbox: { session }`, `sandboxOwnerId`).
9
+ * - `sandboxes.ensure(id, spec)`: one sandbox per person or per project, by a deterministic id
10
+ * (`user-42`, `team-a/proj-42`); get-or-create in one call. Attach sessions to it with
11
+ * `createSession({ sandbox: { id } })`.
12
+ *
13
+ * Ids may hold `/`; they are sent percent-encoded as one path segment.
14
+ */
15
+ import { type DeleteSandboxResponse, type PutSandboxRequest, type SandboxEvent, type SandboxView } from "@nylorun/core/contracts";
16
+ import { type Transport } from "./http.js";
17
+ import type { CreateSessionOptions, SessionClient } from "./session-client.js";
18
+ /** What a sandbox is made of: fixed once it exists, except its labels. */
19
+ export type SandboxSpec = Omit<PutSandboxRequest, "requestId">;
20
+ /** A session opened on a sandbox of its own (`sandboxes.forSession`). */
21
+ export interface SessionSandboxHandle {
22
+ readonly session: SessionClient;
23
+ readonly sandbox: SandboxView;
24
+ /** Deletes the sandbox and its files; call it when the session is done. */
25
+ release(options?: {
26
+ signal?: AbortSignal;
27
+ }): Promise<DeleteSandboxResponse>;
28
+ }
29
+ export interface ForSessionOptions {
30
+ /** The session to open, as `createSession` takes it, without `sandbox`. */
31
+ readonly session: Omit<CreateSessionOptions, "sandbox">;
32
+ /** The sandbox's spec and labels. Default: a virtual sandbox within the Tenant's defaults. */
33
+ readonly spec?: SandboxSpec;
34
+ /** The sandbox's id. Default: `sessions/<session id>`. */
35
+ readonly sandboxId?: string;
36
+ readonly signal?: AbortSignal;
37
+ }
38
+ export declare class SandboxesClient {
39
+ private readonly transport;
40
+ /** `AgentsClient.createSession`, bound. */
41
+ private readonly createSession;
42
+ constructor(transport: Transport,
43
+ /** `AgentsClient.createSession`, bound. */
44
+ createSession: (options: CreateSessionOptions) => Promise<SessionClient>);
45
+ private path;
46
+ /**
47
+ * Creates the sandbox, or finds the one with this id (`PUT /v1/sandboxes/{id}`). Its spec is
48
+ * fixed once it exists: a different one is a 409. Labels, when given, replace its labels.
49
+ */
50
+ ensure(sandboxId: string, spec?: SandboxSpec, options?: {
51
+ signal?: AbortSignal;
52
+ }): Promise<SandboxView>;
53
+ get(sandboxId: string, options?: {
54
+ signal?: AbortSignal;
55
+ }): Promise<SandboxView>;
56
+ /** The sandboxes with every label in `labels` that this client reaches. */
57
+ list(options?: {
58
+ labels?: Readonly<Record<string, string>>;
59
+ signal?: AbortSignal;
60
+ }): Promise<SandboxView[]>;
61
+ /** Deletes the sandbox and its files; `deleted` is false when there was none. */
62
+ delete(sandboxId: string, options?: {
63
+ signal?: AbortSignal;
64
+ }): Promise<DeleteSandboxResponse>;
65
+ /** The sandbox's lifecycle events, from `from` on. */
66
+ events(sandboxId: string, options?: {
67
+ from?: number;
68
+ signal?: AbortSignal;
69
+ }): Promise<SandboxEvent[]>;
70
+ /**
71
+ * One sandbox for one session: creates the sandbox (`sessions/<session id>` unless
72
+ * `sandboxId` names another), opens the session attached to it, and returns both with
73
+ * `release()`, which deletes the sandbox. If the session cannot be opened, the sandbox is
74
+ * deleted again.
75
+ */
76
+ forSession(options: ForSessionOptions): Promise<SessionSandboxHandle>;
77
+ }
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Sandboxes as a resource (Host feature `sandboxes`): `client.sandboxes`. A sandbox has its own
3
+ * id and outlives the sessions attached to it; whether it serves one session, one person or a
4
+ * project is the app's choice, made here:
5
+ *
6
+ * - `sandboxes.forSession(...)`: one sandbox for one session. It creates the sandbox, opens the
7
+ * session attached to it, and `release()` deletes the sandbox when the session is done. It
8
+ * replaces sharing through another session (`sandbox: { session }`, `sandboxOwnerId`).
9
+ * - `sandboxes.ensure(id, spec)`: one sandbox per person or per project, by a deterministic id
10
+ * (`user-42`, `team-a/proj-42`); get-or-create in one call. Attach sessions to it with
11
+ * `createSession({ sandbox: { id } })`.
12
+ *
13
+ * Ids may hold `/`; they are sent percent-encoded as one path segment.
14
+ */
15
+ import { isSandboxId, } from "@nylorun/core/contracts";
16
+ import { id, segment } from "./http.js";
17
+ const FEATURE = "sandboxes";
18
+ export class SandboxesClient {
19
+ transport;
20
+ createSession;
21
+ constructor(transport,
22
+ /** `AgentsClient.createSession`, bound. */
23
+ createSession) {
24
+ this.transport = transport;
25
+ this.createSession = createSession;
26
+ }
27
+ path(sandboxId) {
28
+ if (!isSandboxId(sandboxId))
29
+ throw new TypeError(`${sandboxId} is not a sandbox id: up to 200 characters, /-separated segments of letters, digits, '.', '_' and '-'`);
30
+ return `/v1/sandboxes/${segment(sandboxId)}`;
31
+ }
32
+ /**
33
+ * Creates the sandbox, or finds the one with this id (`PUT /v1/sandboxes/{id}`). Its spec is
34
+ * fixed once it exists: a different one is a 409. Labels, when given, replace its labels.
35
+ */
36
+ async ensure(sandboxId, spec = {}, options = {}) {
37
+ const path = this.path(sandboxId);
38
+ await this.transport.requireFeature(FEATURE, options.signal);
39
+ return this.transport.json(path, "PUT", { requestId: id(), ...spec }, options.signal);
40
+ }
41
+ async get(sandboxId, options = {}) {
42
+ const path = this.path(sandboxId);
43
+ await this.transport.requireFeature(FEATURE, options.signal);
44
+ return this.transport.json(path, "GET", undefined, options.signal);
45
+ }
46
+ /** The sandboxes with every label in `labels` that this client reaches. */
47
+ async list(options = {}) {
48
+ await this.transport.requireFeature(FEATURE, options.signal);
49
+ const query = new URLSearchParams();
50
+ for (const [key, value] of Object.entries(options.labels ?? {}))
51
+ query.append("label", `${key}=${value}`);
52
+ const search = query.toString();
53
+ const reply = await this.transport.json(`/v1/sandboxes${search ? `?${search}` : ""}`, "GET", undefined, options.signal);
54
+ return reply.sandboxes;
55
+ }
56
+ /** Deletes the sandbox and its files; `deleted` is false when there was none. */
57
+ async delete(sandboxId, options = {}) {
58
+ const path = this.path(sandboxId);
59
+ await this.transport.requireFeature(FEATURE, options.signal);
60
+ return this.transport.json(path, "DELETE", undefined, options.signal);
61
+ }
62
+ /** The sandbox's lifecycle events, from `from` on. */
63
+ async events(sandboxId, options = {}) {
64
+ const path = this.path(sandboxId);
65
+ await this.transport.requireFeature(FEATURE, options.signal);
66
+ const reply = await this.transport.json(`${path}/events${options.from === undefined ? "" : `?from=${options.from}`}`, "GET", undefined, options.signal);
67
+ return reply.events;
68
+ }
69
+ /**
70
+ * One sandbox for one session: creates the sandbox (`sessions/<session id>` unless
71
+ * `sandboxId` names another), opens the session attached to it, and returns both with
72
+ * `release()`, which deletes the sandbox. If the session cannot be opened, the sandbox is
73
+ * deleted again.
74
+ */
75
+ async forSession(options) {
76
+ const sessionId = options.session.id ?? id();
77
+ const sandboxId = options.sandboxId ??
78
+ (isSandboxId(`sessions/${sessionId}`) ? `sessions/${sessionId}` : `sessions/${id()}`);
79
+ const sandbox = await this.ensure(sandboxId, options.spec ?? {}, {
80
+ ...(options.signal ? { signal: options.signal } : {}),
81
+ });
82
+ let session;
83
+ try {
84
+ session = await this.createSession({
85
+ ...options.session,
86
+ id: sessionId,
87
+ sandbox: { id: sandboxId },
88
+ });
89
+ }
90
+ catch (error) {
91
+ await this.delete(sandboxId).catch(() => undefined);
92
+ throw error;
93
+ }
94
+ return {
95
+ session,
96
+ sandbox,
97
+ release: (release = {}) => this.delete(sandboxId, release),
98
+ };
99
+ }
100
+ }