@nylorun/runtime 0.7.0-beta → 0.9.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.
Files changed (72) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +7 -5
  3. package/dist/adapters/sandbox/microsandbox.d.ts +2 -0
  4. package/dist/adapters/sandbox/microsandbox.js +238 -0
  5. package/dist/adapters/sandbox/virtual.d.ts +4 -0
  6. package/dist/adapters/sandbox/virtual.js +125 -0
  7. package/dist/configuration.d.ts +1 -1
  8. package/dist/configuration.js +1 -1
  9. package/dist/core/executors.d.ts +32 -0
  10. package/dist/core/executors.js +60 -0
  11. package/dist/core/main.js +7 -12
  12. package/dist/core/runtime.d.ts +24 -1
  13. package/dist/core/runtime.js +364 -43
  14. package/dist/core/store.d.ts +14 -1
  15. package/dist/core/store.js +59 -3
  16. package/dist/index.d.ts +0 -1
  17. package/dist/index.js +0 -1
  18. package/dist/mcp/pool.d.ts +13 -1
  19. package/dist/mcp/pool.js +53 -19
  20. package/dist/mcp/snapshot.d.ts +4 -0
  21. package/dist/model/catalog.d.ts +12 -0
  22. package/dist/model/catalog.js +29 -0
  23. package/dist/model/models.d.ts +3 -1
  24. package/dist/model/models.js +4 -2
  25. package/dist/model/pi-model.d.ts +9 -3
  26. package/dist/model/pi-model.js +47 -11
  27. package/dist/model/settings.d.ts +0 -2
  28. package/dist/model/settings.js +0 -47
  29. package/dist/node/index.d.ts +1 -1
  30. package/dist/node/index.js +1 -1
  31. package/dist/sandbox/manager.d.ts +61 -0
  32. package/dist/sandbox/manager.js +239 -0
  33. package/dist/sandbox/policy.d.ts +13 -0
  34. package/dist/sandbox/policy.js +67 -0
  35. package/dist/sandbox/select.d.ts +35 -0
  36. package/dist/sandbox/select.js +76 -0
  37. package/dist/sandbox/tools.d.ts +25 -0
  38. package/dist/sandbox/tools.js +165 -0
  39. package/dist/sandbox/types.d.ts +66 -0
  40. package/dist/vault/service.d.ts +41 -1
  41. package/dist/vault/service.js +291 -4
  42. package/dist/version.d.ts +1 -0
  43. package/dist/version.js +3 -0
  44. package/package.json +9 -4
  45. package/dist/config.d.ts +0 -20
  46. package/dist/model/defaults.d.ts +0 -17
  47. package/dist/model/defaults.js +0 -21
  48. package/dist/model/http-model.d.ts +0 -12
  49. package/dist/model/http-model.js +0 -299
  50. package/dist/node/local-sessions.d.ts +0 -5
  51. package/dist/node/local-sessions.js +0 -174
  52. package/dist/server/ag-ui.d.ts +0 -8
  53. package/dist/server/ag-ui.js +0 -76
  54. package/dist/server/delivery.d.ts +0 -24
  55. package/dist/server/delivery.js +0 -107
  56. package/dist/server/digests.d.ts +0 -13
  57. package/dist/server/digests.js +0 -39
  58. package/dist/server/host.d.ts +0 -70
  59. package/dist/server/host.js +0 -647
  60. package/dist/session/api.d.ts +0 -10
  61. package/dist/session/api.js +0 -15
  62. package/dist/session/default.d.ts +0 -5
  63. package/dist/session/default.js +0 -30
  64. package/dist/session/handle.d.ts +0 -27
  65. package/dist/session/handle.js +0 -199
  66. package/dist/session/index.d.ts +0 -2
  67. package/dist/session/index.js +0 -2
  68. package/dist/sessions/host.d.ts +0 -39
  69. package/dist/sessions/host.js +0 -359
  70. package/dist/sessions/store.d.ts +0 -41
  71. package/dist/sessions/store.js +0 -33
  72. /package/dist/{config.js → sandbox/types.js} +0 -0
@@ -1,6 +1,14 @@
1
1
  import { DatabaseSync } from "node:sqlite";
2
2
  import type { LiveEvent } from "@nylorun/core/contracts";
3
3
  export declare function canonical(value: unknown): string;
4
+ export interface ExecutorRow {
5
+ agentId: string;
6
+ tokenHash: string;
7
+ implementationVersion: string;
8
+ manifestHash?: string;
9
+ createdAt: string;
10
+ updatedAt: string;
11
+ }
4
12
  export declare class Store {
5
13
  readonly db: DatabaseSync;
6
14
  constructor(path: string);
@@ -8,9 +16,14 @@ export declare class Store {
8
16
  get<T = any>(table: string, id: string): T | undefined;
9
17
  all<T = any>(table: string): T[];
10
18
  put(table: string, id: string, body: unknown): void;
19
+ delete(table: string, id: string): void;
20
+ allExecutors(): ExecutorRow[];
21
+ putExecutor(row: Omit<ExecutorRow, "createdAt">): void;
22
+ deleteExecutor(agentId: string): void;
11
23
  event(sessionId: string, turnId: string | null, type: string, payload: unknown): LiveEvent;
12
24
  credentialCount(): number;
13
- history(sessionId: string, cursor?: string): {
25
+ /** `agent` keeps only events of one agent used as a tool, by delegationId or path. */
26
+ history(sessionId: string, cursor?: string, agent?: string): {
14
27
  items: LiveEvent[];
15
28
  cursor: string | null;
16
29
  };
@@ -25,6 +25,7 @@ export class Store {
25
25
  CREATE TABLE IF NOT EXISTS checkpoints(id TEXT PRIMARY KEY, body TEXT NOT NULL);
26
26
  CREATE TABLE IF NOT EXISTS effects(id TEXT PRIMARY KEY, body TEXT NOT NULL);
27
27
  CREATE TABLE IF NOT EXISTS actions(id TEXT PRIMARY KEY, body TEXT NOT NULL);
28
+ CREATE TABLE IF NOT EXISTS sandboxes(id TEXT PRIMARY KEY, body TEXT NOT NULL);
28
29
  CREATE TABLE IF NOT EXISTS events(sequence INTEGER PRIMARY KEY AUTOINCREMENT, session_id TEXT NOT NULL, body TEXT NOT NULL);
29
30
  CREATE INDEX IF NOT EXISTS events_session ON events(session_id, sequence);
30
31
  CREATE TABLE IF NOT EXISTS vaults(
@@ -32,7 +33,8 @@ export class Store {
32
33
  name TEXT NOT NULL,
33
34
  owner_user_id TEXT NOT NULL,
34
35
  metadata_json TEXT,
35
- created_at TEXT NOT NULL
36
+ created_at TEXT NOT NULL,
37
+ scope TEXT NOT NULL DEFAULT 'user'
36
38
  );
37
39
  CREATE INDEX IF NOT EXISTS vaults_owner ON vaults(owner_user_id);
38
40
  CREATE TABLE IF NOT EXISTS vault_credentials(
@@ -65,7 +67,16 @@ export class Store {
65
67
  id TEXT PRIMARY KEY,
66
68
  body_hash TEXT NOT NULL,
67
69
  response TEXT NOT NULL
70
+ );
71
+ CREATE TABLE IF NOT EXISTS executors(
72
+ agent_id TEXT PRIMARY KEY,
73
+ token_hash TEXT NOT NULL UNIQUE,
74
+ implementation_version TEXT NOT NULL,
75
+ manifest_hash TEXT,
76
+ created_at TEXT NOT NULL,
77
+ updated_at TEXT NOT NULL
68
78
  );`);
79
+ migrateVaultScope(this.db);
69
80
  }
70
81
  tx(fn) {
71
82
  this.db.exec("BEGIN IMMEDIATE");
@@ -94,6 +105,38 @@ export class Store {
94
105
  .prepare(`INSERT INTO ${table}(id,body) VALUES(?,?) ON CONFLICT(id) DO UPDATE SET body=excluded.body`)
95
106
  .run(id, JSON.stringify(body));
96
107
  }
108
+ delete(table, id) {
109
+ this.db.prepare(`DELETE FROM ${table} WHERE id=?`).run(id);
110
+ }
111
+ // Executors carry a bearer secret, so they use typed columns with a database-enforced unique
112
+ // token hash instead of flowing through the generic JSON document helpers above.
113
+ allExecutors() {
114
+ return this.db
115
+ .prepare("SELECT agent_id,token_hash,implementation_version,manifest_hash,created_at,updated_at FROM executors")
116
+ .all()
117
+ .map((r) => ({
118
+ agentId: String(r.agent_id),
119
+ tokenHash: String(r.token_hash),
120
+ implementationVersion: String(r.implementation_version),
121
+ manifestHash: r.manifest_hash == null ? undefined : String(r.manifest_hash),
122
+ createdAt: String(r.created_at),
123
+ updatedAt: String(r.updated_at),
124
+ }));
125
+ }
126
+ putExecutor(row) {
127
+ this.db
128
+ .prepare(`INSERT INTO executors(agent_id,token_hash,implementation_version,manifest_hash,created_at,updated_at)
129
+ VALUES(?,?,?,?,?,?)
130
+ ON CONFLICT(agent_id) DO UPDATE SET
131
+ token_hash=excluded.token_hash,
132
+ implementation_version=excluded.implementation_version,
133
+ manifest_hash=excluded.manifest_hash,
134
+ updated_at=excluded.updated_at`)
135
+ .run(row.agentId, row.tokenHash, row.implementationVersion, row.manifestHash ?? null, row.updatedAt, row.updatedAt);
136
+ }
137
+ deleteExecutor(agentId) {
138
+ this.db.prepare("DELETE FROM executors WHERE agent_id=?").run(agentId);
139
+ }
97
140
  event(sessionId, turnId, type, payload) {
98
141
  const base = {
99
142
  eventId: randomUUID(),
@@ -121,7 +164,8 @@ export class Store {
121
164
  .get();
122
165
  return Number(row.n);
123
166
  }
124
- history(sessionId, cursor) {
167
+ /** `agent` keeps only events of one agent used as a tool, by delegationId or path. */
168
+ history(sessionId, cursor, agent) {
125
169
  let sequence = 0;
126
170
  if (cursor) {
127
171
  const decoded = Buffer.from(cursor, "base64url").toString();
@@ -134,7 +178,8 @@ export class Store {
134
178
  const items = this.db
135
179
  .prepare("SELECT body FROM events WHERE session_id=? AND sequence>? ORDER BY sequence")
136
180
  .all(sessionId, sequence)
137
- .map((r) => JSON.parse(String(r.body)));
181
+ .map((r) => JSON.parse(String(r.body)))
182
+ .filter((event) => agent === undefined || belongsTo(event, agent));
138
183
  const last = this.db
139
184
  .prepare("SELECT body FROM events WHERE session_id=? ORDER BY sequence DESC LIMIT 1")
140
185
  .get(sessionId);
@@ -144,3 +189,14 @@ export class Store {
144
189
  };
145
190
  }
146
191
  }
192
+ function belongsTo(event, agent) {
193
+ const ref = event.payload
194
+ ?.agent;
195
+ return ref?.delegationId === agent || ref?.path === agent;
196
+ }
197
+ function migrateVaultScope(db) {
198
+ const columns = db.prepare("PRAGMA table_info(vaults)").all();
199
+ if (!columns.some((column) => column.name === "scope"))
200
+ db.exec("ALTER TABLE vaults ADD COLUMN scope TEXT NOT NULL DEFAULT 'user'");
201
+ db.exec("CREATE UNIQUE INDEX IF NOT EXISTS vaults_one_host ON vaults(scope) WHERE scope = 'host'");
202
+ }
package/dist/index.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  export { CoreRuntime, createRuntime, startRuntime, type RuntimeOptions } from "./core/runtime.js";
2
2
  export { scriptedModel, gatewayModel, type ModelProvider } from "./core/provider.js";
3
- export { httpModel, type HttpModelOptions, type ModelEnvironment } from "./model/http-model.js";
4
3
  export type { RuntimeAgent } from "./contracts.js";
5
4
  export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
6
5
  export type { RuntimeModelAdapter } from "./contracts.js";
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { CoreRuntime, createRuntime, startRuntime } from "./core/runtime.js";
2
2
  export { scriptedModel, gatewayModel } from "./core/provider.js";
3
- export { httpModel } from "./model/http-model.js";
4
3
  export { IMAGE_MEDIA_TYPES, MAX_IMAGE_BYTES, decodeImageBase64, validateImageBytes } from "./media.js";
@@ -1,4 +1,4 @@
1
- import type { AgentManifest } from "@nylorun/core/define";
1
+ import type { AgentManifest, McpServerManifest } from "@nylorun/core/define";
2
2
  import type { AuthorizeResult } from "../vault/service.js";
3
3
  import { callMcpTool } from "./connect.js";
4
4
  import { type McpDiagnostic, type McpSnapshot, type McpToolRecord } from "./snapshot.js";
@@ -31,6 +31,7 @@ export declare class McpPool {
31
31
  }): Promise<McpDiagnostic[]>;
32
32
  call(input: {
33
33
  sessionId: string;
34
+ agentId?: string;
34
35
  capabilityId: string;
35
36
  serverName: string;
36
37
  serverToolName: string;
@@ -43,3 +44,14 @@ export declare class McpPool {
43
44
  private liveKey;
44
45
  private connectDeclared;
45
46
  }
47
+ interface DeclaredServer {
48
+ /** Absent for the session's root agent. */
49
+ readonly agentId?: string;
50
+ /** The manifest of the agent that declares the server. */
51
+ readonly manifest: AgentManifest;
52
+ readonly capabilityId: string;
53
+ readonly server: McpServerManifest;
54
+ }
55
+ /** Servers of the root agent, then of each agent it uses as a tool. */
56
+ export declare function serversOf(manifest: AgentManifest): DeclaredServer[];
57
+ export {};
package/dist/mcp/pool.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { join } from "node:path";
2
+ import { delegatesOf } from "@nylorun/core/define";
2
3
  import { callMcpTool, diagnosticFromError, listMcpTools, openMcpServer, } from "./connect.js";
3
4
  import { declaredToolNames, } from "./snapshot.js";
4
5
  export class McpPool {
@@ -8,7 +9,8 @@ export class McpPool {
8
9
  this.options = options;
9
10
  }
10
11
  async discover(input) {
11
- const taken = declaredToolNames(input.manifest);
12
+ // Each agent names its own tools, so collisions are checked per agent.
13
+ const taken = new Map();
12
14
  const tools = [];
13
15
  const diagnostics = [];
14
16
  for (const declared of serversOf(input.manifest)) {
@@ -19,13 +21,16 @@ export class McpPool {
19
21
  continue;
20
22
  }
21
23
  try {
24
+ if (!taken.has(declared.agentId))
25
+ taken.set(declared.agentId, declaredToolNames(declared.manifest));
22
26
  const listed = await listMcpTools(opened.connection.client, {
23
27
  capabilityId: declared.capabilityId,
24
28
  serverName: declared.server.name,
25
- taken,
29
+ taken: taken.get(declared.agentId),
26
30
  });
27
- tools.push(...listed.tools);
31
+ tools.push(...listed.tools.map((tool) => owned(declared, tool)));
28
32
  diagnostics.push({
33
+ ...ownerOf(declared),
29
34
  capabilityId: declared.capabilityId,
30
35
  serverName: declared.server.name,
31
36
  outcome: "connected",
@@ -37,7 +42,7 @@ export class McpPool {
37
42
  }
38
43
  catch (error) {
39
44
  await opened.connection.close().catch(() => { });
40
- diagnostics.push(diagnosticFromError(declared.capabilityId, declared.server.name, error));
45
+ diagnostics.push(owned(declared, diagnosticFromError(declared.capabilityId, declared.server.name, error)));
41
46
  }
42
47
  }
43
48
  return {
@@ -53,16 +58,17 @@ export class McpPool {
53
58
  const diagnostics = [];
54
59
  const seen = new Set();
55
60
  for (const tool of input.tools) {
56
- const key = `${tool.capabilityId}\0${tool.serverName}`;
61
+ const key = this.liveKey(input.sessionId, tool.agentId, tool.capabilityId, tool.serverName);
57
62
  if (seen.has(key))
58
63
  continue;
59
64
  seen.add(key);
60
- if (this.live.has(this.liveKey(input.sessionId, tool.capabilityId, tool.serverName)))
65
+ if (this.live.has(key))
61
66
  continue;
62
67
  input.signal?.throwIfAborted();
63
- const declared = serversOf(input.manifest).find((item) => item.capabilityId === tool.capabilityId && item.server.name === tool.serverName);
68
+ const declared = findServer(input.manifest, tool.agentId, tool.capabilityId, tool.serverName);
64
69
  if (!declared) {
65
70
  diagnostics.push({
71
+ ...(tool.agentId === undefined ? {} : { agentId: tool.agentId }),
66
72
  capabilityId: tool.capabilityId,
67
73
  serverName: tool.serverName,
68
74
  outcome: "failed",
@@ -80,10 +86,10 @@ export class McpPool {
80
86
  return diagnostics;
81
87
  }
82
88
  async call(input) {
83
- const key = this.liveKey(input.sessionId, input.capabilityId, input.serverName);
89
+ const key = this.liveKey(input.sessionId, input.agentId, input.capabilityId, input.serverName);
84
90
  let live = this.live.get(key);
85
91
  if (!live) {
86
- const declared = serversOf(input.manifest).find((item) => item.capabilityId === input.capabilityId && item.server.name === input.serverName);
92
+ const declared = findServer(input.manifest, input.agentId, input.capabilityId, input.serverName);
87
93
  if (!declared)
88
94
  throw new Error(`MCP server '${input.serverName}' is not declared`);
89
95
  const opened = await this.connectDeclared(input, declared);
@@ -102,21 +108,21 @@ export class McpPool {
102
108
  await Promise.all(connections.map((item) => item.connection.close().catch(() => { })));
103
109
  }
104
110
  remember(sessionId, declared, connection) {
105
- this.live.set(this.liveKey(sessionId, declared.capabilityId, declared.server.name), {
111
+ this.live.set(this.liveKey(sessionId, declared.agentId, declared.capabilityId, declared.server.name), {
106
112
  capabilityId: declared.capabilityId,
107
113
  serverName: declared.server.name,
108
114
  connection,
109
115
  });
110
116
  }
111
- liveKey(sessionId, capabilityId, serverName) {
112
- return JSON.stringify([sessionId, capabilityId, serverName]);
117
+ liveKey(sessionId, agentId, capabilityId, serverName) {
118
+ return JSON.stringify([sessionId, agentId ?? null, capabilityId, serverName]);
113
119
  }
114
120
  async connectDeclared(input, declared) {
115
121
  try {
116
122
  const connection = await openMcpServer({
117
123
  server: declared.server,
118
- pluginRoot: input.pluginRoots[declared.capabilityId],
119
- pluginData: join(this.options.dataDir, "plugin-data", declared.capabilityId),
124
+ pluginRoot: input.pluginRoots[pluginKey(declared)],
125
+ pluginData: join(this.options.dataDir, "plugin-data", ...pluginKey(declared).split("/")),
120
126
  authorize: declared.server.type === "stdio"
121
127
  ? undefined
122
128
  : (url) => this.options.authorize(input.sessionId, {
@@ -129,15 +135,43 @@ export class McpPool {
129
135
  catch (error) {
130
136
  return {
131
137
  ok: false,
132
- diagnostic: diagnosticFromError(declared.capabilityId, declared.server.name, error),
138
+ diagnostic: owned(declared, diagnosticFromError(declared.capabilityId, declared.server.name, error)),
133
139
  };
134
140
  }
135
141
  }
136
142
  }
137
- function serversOf(manifest) {
143
+ /** Servers of the root agent, then of each agent it uses as a tool. */
144
+ export function serversOf(manifest) {
138
145
  const servers = [];
139
- for (const capability of manifest.capabilities)
140
- for (const server of Object.values(capability.mcpServers ?? {}))
141
- servers.push({ capabilityId: capability.id, server });
146
+ const add = (agent, agentId) => {
147
+ for (const capability of agent.capabilities)
148
+ for (const server of Object.values(capability.mcpServers ?? {}))
149
+ servers.push({
150
+ ...(agentId === undefined ? {} : { agentId }),
151
+ manifest: agent,
152
+ capabilityId: capability.id,
153
+ server,
154
+ });
155
+ };
156
+ add(manifest);
157
+ for (const child of delegatesOf(manifest))
158
+ add(child.manifest, child.manifest.id);
142
159
  return servers;
143
160
  }
161
+ function findServer(manifest, agentId, capabilityId, serverName) {
162
+ return serversOf(manifest).find((item) => item.agentId === agentId &&
163
+ item.capabilityId === capabilityId &&
164
+ item.server.name === serverName);
165
+ }
166
+ /** Plugin roots of agents used as tools are registered as `<agent>/<capability>`. */
167
+ function pluginKey(declared) {
168
+ return declared.agentId === undefined
169
+ ? declared.capabilityId
170
+ : `${declared.agentId}/${declared.capabilityId}`;
171
+ }
172
+ function ownerOf(declared) {
173
+ return declared.agentId === undefined ? {} : { agentId: declared.agentId };
174
+ }
175
+ function owned(declared, value) {
176
+ return { ...ownerOf(declared), ...value };
177
+ }
@@ -1,6 +1,8 @@
1
1
  import type { JsonObject } from "@nylorun/core/define";
2
2
  import type { AgentManifest } from "@nylorun/core/define";
3
3
  export interface McpToolRecord {
4
+ /** The agent used as a tool that declares the server; absent for the session's root agent. */
5
+ readonly agentId?: string;
4
6
  readonly capabilityId: string;
5
7
  readonly serverName: string;
6
8
  readonly serverToolName: string;
@@ -15,6 +17,8 @@ export interface McpSnapshot {
15
17
  readonly mcpTools: readonly McpToolRecord[];
16
18
  }
17
19
  export interface McpDiagnostic {
20
+ /** The agent used as a tool that declares the server; absent for the session's root agent. */
21
+ readonly agentId?: string;
18
22
  readonly capabilityId: string;
19
23
  readonly serverName: string;
20
24
  readonly outcome: "connected" | "refused" | "failed";
@@ -0,0 +1,12 @@
1
+ export type HostModelCatalog = {
2
+ providers: {
3
+ id: string;
4
+ name: string;
5
+ models: {
6
+ id: string;
7
+ name: string;
8
+ }[];
9
+ }[];
10
+ };
11
+ /** Public provider and model names. No credentials. */
12
+ export declare function hostModelCatalog(): HostModelCatalog;
@@ -0,0 +1,29 @@
1
+ import { modelsFor } from "./models.js";
2
+ const emptyStore = {
3
+ async read() {
4
+ return undefined;
5
+ },
6
+ async list() {
7
+ return [];
8
+ },
9
+ async modify(_providerId, fn) {
10
+ return fn(undefined);
11
+ },
12
+ async delete() { },
13
+ };
14
+ /** Public provider and model names. No credentials. */
15
+ export function hostModelCatalog() {
16
+ const models = modelsFor({ provider: "", model: "" }, emptyStore, {
17
+ environment: false,
18
+ });
19
+ return {
20
+ providers: models.getProviders().map((provider) => ({
21
+ id: provider.id,
22
+ name: provider.name,
23
+ models: models.getModels(provider.id).map((model) => ({
24
+ id: model.id,
25
+ name: model.name,
26
+ })),
27
+ })),
28
+ };
29
+ }
@@ -6,4 +6,6 @@ export type Selection = Readonly<{
6
6
  baseUrl: string;
7
7
  }>;
8
8
  }>;
9
- export declare function modelsFor(selection: Selection, credentials: CredentialStore): import("@earendil-works/pi-ai").MutableModels;
9
+ export declare function modelsFor(selection: Selection, credentials: CredentialStore, options?: {
10
+ environment?: boolean;
11
+ }): import("@earendil-works/pi-ai").MutableModels;
@@ -1,7 +1,7 @@
1
1
  import { createProvider, defaultProviderAuthContext, envApiKeyAuth, } from "@earendil-works/pi-ai";
2
2
  import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
3
3
  import { builtinModels } from "@earendil-works/pi-ai/providers/all";
4
- export function modelsFor(selection, credentials) {
4
+ export function modelsFor(selection, credentials, options = {}) {
5
5
  const environmentFirst = {
6
6
  async read(providerId, options) {
7
7
  const explicit = process.env.MODEL_PROVIDER_API_KEY;
@@ -23,7 +23,9 @@ export function modelsFor(selection, credentials) {
23
23
  modify: (id, fn, options) => credentials.modify(id, fn, options),
24
24
  delete: (id, options) => credentials.delete(id, options),
25
25
  };
26
- const models = builtinModels({ credentials: environmentFirst });
26
+ const models = builtinModels({
27
+ credentials: options.environment === false ? credentials : environmentFirst,
28
+ });
27
29
  if (!selection.custom)
28
30
  return models;
29
31
  const model = {
@@ -1,11 +1,17 @@
1
+ import type { Credential } from "@earendil-works/pi-ai";
1
2
  import type { RuntimeModelAdapter } from "../contracts.js";
2
3
  import type { RuntimeMedia } from "../adapters/media.js";
3
- import { type Selection } from "./models.js";
4
+ import type { HostModelSecret } from "../vault/service.js";
5
+ export interface ModelPreview {
6
+ readonly invocationId: string;
7
+ readonly text: string;
8
+ }
4
9
  export interface PiModelOptions {
5
10
  readonly root?: string;
6
- readonly onPreview?: (preview: import("./defaults.js").ModelPreview) => void;
7
- readonly selection?: Selection;
11
+ readonly onPreview?: (preview: ModelPreview) => void;
8
12
  readonly media?: Pick<RuntimeMedia, "dataUrl">;
13
+ readonly readHostModel?: () => HostModelSecret | undefined;
14
+ readonly writeHostCredential?: (credential: Credential) => void;
9
15
  }
10
16
  /** Node model adapter. Local provider configuration is read only when invoked. */
11
17
  export declare function piModel(options?: PiModelOptions): RuntimeModelAdapter;
@@ -1,8 +1,6 @@
1
- import { join } from "node:path";
2
1
  import { scrub } from "../redact.js";
3
- import { ProjectCredentialStore } from "./auth-store.js";
4
2
  import { modelsFor } from "./models.js";
5
- import { modelSelection, projectSecrets } from "./settings.js";
3
+ import { projectSecrets } from "./settings.js";
6
4
  const emptyUsage = () => ({
7
5
  input: 0,
8
6
  output: 0,
@@ -16,15 +14,18 @@ export function piModel(options = {}) {
16
14
  return async (call, context) => {
17
15
  context.signal.throwIfAborted();
18
16
  const root = options.root ?? process.cwd();
19
- const configured = options.selection ?? modelSelection(root);
17
+ const stored = options.readHostModel?.();
18
+ if (!stored)
19
+ throw new Error("Model provider is not configured. Start nylorun dev in a terminal, or set it in Studio.");
20
20
  const requested = call.model?.id;
21
21
  const selection = {
22
- ...configured,
23
- model: requested?.startsWith(`${configured.provider}/`)
24
- ? requested.slice(configured.provider.length + 1)
25
- : (requested ?? configured.model),
22
+ provider: stored.provider,
23
+ model: requested?.startsWith(`${stored.provider}/`)
24
+ ? requested.slice(stored.provider.length + 1)
25
+ : (requested ?? stored.model),
26
+ ...(stored.baseUrl ? { custom: { baseUrl: stored.baseUrl } } : {}),
26
27
  };
27
- const registry = modelsFor(selection, new ProjectCredentialStore(join(root, ".nylorun", "auth.json"), join(root, ".env", "auth.json")));
28
+ const registry = modelsFor(selection, hostCredentialStore(stored, options.writeHostCredential), { environment: false });
28
29
  const selected = registry.getModel(selection.provider, selection.model);
29
30
  if (!selected)
30
31
  throw new Error("Unknown model. Run nylorun configure.");
@@ -176,7 +177,10 @@ export function piModel(options = {}) {
176
177
  messages,
177
178
  tools,
178
179
  };
179
- const secrets = projectSecrets(root);
180
+ const secrets = [
181
+ ...projectSecrets(root),
182
+ ...credentialSecrets(stored.credential),
183
+ ];
180
184
  // Publish only portable references, never the materialized provider image bytes.
181
185
  context.reportPreparedCall?.({
182
186
  adapter: "runtime.pi-ai",
@@ -265,7 +269,39 @@ export function piModel(options = {}) {
265
269
  };
266
270
  }
267
271
  catch (error) {
268
- throw new Error(String(scrub(error instanceof Error ? error.message : String(error), projectSecrets(root))));
272
+ throw new Error(String(scrub(error instanceof Error ? error.message : String(error), secrets)));
269
273
  }
270
274
  };
271
275
  }
276
+ function hostCredentialStore(stored, write) {
277
+ let current = stored.credential;
278
+ return {
279
+ async read(providerId) {
280
+ if (providerId !== stored.provider)
281
+ return undefined;
282
+ return current;
283
+ },
284
+ async list() {
285
+ return [{ providerId: stored.provider, type: current.type }];
286
+ },
287
+ async modify(providerId, fn) {
288
+ if (providerId !== stored.provider)
289
+ return fn(undefined);
290
+ const next = await fn(current);
291
+ if (next === undefined)
292
+ return current;
293
+ write?.(next);
294
+ current = next;
295
+ return next;
296
+ },
297
+ async delete() {
298
+ throw new Error("Replace the model provider through nylorun dev or Studio.");
299
+ },
300
+ };
301
+ }
302
+ function credentialSecrets(credential) {
303
+ const values = [credential.key, credential.access, credential.refresh];
304
+ if (credential.env)
305
+ values.push(...Object.values(credential.env));
306
+ return values.filter((value) => Boolean(value));
307
+ }
@@ -1,3 +1 @@
1
- import type { Selection } from "./models.js";
2
- export declare function modelSelection(root?: string): Selection;
3
1
  export declare function projectSecrets(root?: string): readonly string[];
@@ -1,52 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- export function modelSelection(root = process.cwd()) {
4
- const { MODEL_PROVIDER: provider, MODEL: model, MODEL_PROVIDER_BASE_URL: baseUrl, } = process.env;
5
- if (provider !== undefined || model !== undefined || baseUrl !== undefined) {
6
- if (!provider?.trim() || !model?.trim())
7
- throw new Error("Set both MODEL_PROVIDER and MODEL, or run nylorun configure.");
8
- if (provider === "custom" && !baseUrl?.trim())
9
- throw new Error("Set MODEL_PROVIDER_BASE_URL for MODEL_PROVIDER=custom.");
10
- if (baseUrl && provider !== "custom")
11
- throw new Error("MODEL_PROVIDER_BASE_URL requires MODEL_PROVIDER=custom.");
12
- if (baseUrl) {
13
- let url;
14
- try {
15
- url = new URL(baseUrl);
16
- }
17
- catch {
18
- throw new Error("MODEL_PROVIDER_BASE_URL must be an HTTP(S) URL.");
19
- }
20
- if (!["http:", "https:"].includes(url.protocol))
21
- throw new Error("MODEL_PROVIDER_BASE_URL must be an HTTP(S) URL.");
22
- }
23
- return { provider, model, ...(baseUrl ? { custom: { baseUrl } } : {}) };
24
- }
25
- try {
26
- let contents;
27
- try {
28
- contents = readFileSync(join(root, ".env", "model.json"), "utf8");
29
- }
30
- catch (error) {
31
- if (!(error instanceof Error) ||
32
- !("code" in error) ||
33
- !["ENOENT", "ENOTDIR"].includes(String(error.code)))
34
- throw error;
35
- contents = readFileSync(join(root, "config", "model.json"), "utf8");
36
- }
37
- const value = JSON.parse(contents);
38
- if (typeof value.provider === "string" &&
39
- value.provider &&
40
- typeof value.model === "string" &&
41
- value.model &&
42
- (value.custom === undefined || typeof value.custom.baseUrl === "string"))
43
- return value;
44
- }
45
- catch {
46
- /* Report one actionable setup error without file or credential contents. */
47
- }
48
- throw new Error("Run nylorun configure to connect a model provider.");
49
- }
50
3
  export function projectSecrets(root = process.cwd()) {
51
4
  const values = Object.entries(process.env)
52
5
  .filter(([key]) => /key|token|secret|password|credential/i.test(key))
@@ -1,5 +1,5 @@
1
- export { localSessions } from "./local-sessions.js";
2
1
  export { localMedia, MediaStore } from "../adapters/media.js";
3
2
  export { jsonlObserver } from "../adapters/observe.js";
4
3
  export { piModel, type PiModelOptions } from "../model/pi-model.js";
5
4
  export { projectAsset } from "../assets.js";
5
+ export { probeSandboxBackends, type SandboxSelectionReport, } from "../sandbox/select.js";
@@ -1,5 +1,5 @@
1
- export { localSessions } from "./local-sessions.js";
2
1
  export { localMedia, MediaStore } from "../adapters/media.js";
3
2
  export { jsonlObserver } from "../adapters/observe.js";
4
3
  export { piModel } from "../model/pi-model.js";
5
4
  export { projectAsset } from "../assets.js";
5
+ export { probeSandboxBackends, } from "../sandbox/select.js";