@nylorun/cli 0.2.1-beta → 0.4.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 +83 -0
- package/README.md +66 -42
- package/dist/baseline.d.ts +2 -0
- package/dist/baseline.js +10 -0
- package/dist/cli.d.ts +1 -1
- package/dist/cli.js +73 -242
- package/dist/doctor.d.ts +15 -4
- package/dist/doctor.js +44 -46
- package/dist/environment.d.ts +4 -1
- package/dist/environment.js +12 -3
- package/dist/home.d.ts +6 -0
- package/dist/home.js +15 -0
- package/dist/model/configure.d.ts +30 -0
- package/dist/model/configure.js +117 -11
- package/dist/model/host-model.d.ts +5 -3
- package/dist/model/host-model.js +37 -16
- package/dist/project/create-tenant.d.ts +28 -0
- package/dist/project/create-tenant.js +66 -0
- package/dist/project/credentials.d.ts +18 -0
- package/dist/project/credentials.js +64 -0
- package/dist/project/env.d.ts +3 -0
- package/dist/project/env.js +21 -0
- package/dist/project/link.d.ts +20 -0
- package/dist/project/link.js +77 -0
- package/dist/project/root.d.ts +9 -0
- package/dist/project/root.js +52 -0
- package/dist/project/seed.d.ts +25 -0
- package/dist/project/seed.js +96 -0
- package/dist/tenant/commands.d.ts +4 -0
- package/dist/tenant/commands.js +321 -0
- package/package.json +6 -6
- package/dist/dev-entry.d.ts +0 -1
- package/dist/dev-entry.js +0 -6
- package/dist/dev.d.ts +0 -6
- package/dist/dev.js +0 -65
- package/dist/launcher.d.ts +0 -5
- package/dist/launcher.js +0 -4
- package/dist/project-runner.d.ts +0 -20
- package/dist/project-runner.js +0 -141
- package/dist/runtime-host.d.ts +0 -39
- package/dist/runtime-host.js +0 -355
- package/dist/scope.d.ts +0 -65
- package/dist/scope.js +0 -191
package/dist/doctor.js
CHANGED
|
@@ -1,61 +1,59 @@
|
|
|
1
1
|
import { release } from "node:os";
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
2
|
+
import { createClient, resolveConnection } from "@nylorun/agents";
|
|
3
|
+
import { CliError } from "./errors.js";
|
|
4
|
+
function platformLine() {
|
|
5
|
+
const os = process.platform === "darwin"
|
|
6
|
+
? "macOS"
|
|
7
|
+
: process.env.WSL_DISTRO_NAME
|
|
8
|
+
? `Linux (WSL: ${process.env.WSL_DISTRO_NAME})`
|
|
9
|
+
: process.platform;
|
|
10
|
+
return `${os} (kernel ${release()}) · ${process.arch}`;
|
|
11
|
+
}
|
|
12
|
+
async function fetchSandboxReport(options) {
|
|
13
|
+
const connection = await resolveConnection(options);
|
|
14
|
+
const client = createClient({
|
|
15
|
+
url: connection.url,
|
|
16
|
+
key: connection.key,
|
|
17
|
+
tenant: connection.tenant,
|
|
18
|
+
});
|
|
19
|
+
return client.transport.json("/v1/tenant/sandbox", "GET", undefined);
|
|
20
|
+
}
|
|
21
|
+
/** `nylo doctor sandbox`: Tenant sandbox report via the Tenant API (F2-4). */
|
|
8
22
|
export async function doctorSandbox(options) {
|
|
9
|
-
|
|
23
|
+
let report;
|
|
24
|
+
try {
|
|
25
|
+
report = await fetchSandboxReport();
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
throw new CliError(error instanceof Error
|
|
29
|
+
? error.message
|
|
30
|
+
: `Could not read Tenant sandbox status: ${String(error)}`, 1);
|
|
31
|
+
}
|
|
10
32
|
if (options.json) {
|
|
11
|
-
console.log(JSON.stringify({ platform:
|
|
33
|
+
console.log(JSON.stringify({ platform: platformLine(), ...report }, null, 2));
|
|
12
34
|
return;
|
|
13
35
|
}
|
|
14
|
-
const rows = [["platform",
|
|
36
|
+
const rows = [["platform", platformLine()]];
|
|
15
37
|
for (const probe of report.probes)
|
|
16
38
|
rows.push([
|
|
17
39
|
probe.name,
|
|
18
40
|
`${probe.available ? "✓" : "✗"} ${probe.reason ?? ""}${probe.version ? ` · ${probe.version}` : ""}`,
|
|
19
41
|
]);
|
|
20
|
-
rows.push([
|
|
21
|
-
|
|
42
|
+
rows.push([
|
|
43
|
+
"preference",
|
|
44
|
+
report.preference === "auto"
|
|
45
|
+
? "auto (seed Tenant sandbox.backend via nylo tenant create / .env NYLORUN_SANDBOX)"
|
|
46
|
+
: report.preference,
|
|
47
|
+
]);
|
|
48
|
+
rows.push([
|
|
49
|
+
"selected",
|
|
50
|
+
report.backend
|
|
51
|
+
? `${report.backend} (${report.isolation} isolation)`
|
|
52
|
+
: `none: ${report.reason}`,
|
|
53
|
+
]);
|
|
22
54
|
const width = Math.max(...rows.map(([key]) => key.length)) + 2;
|
|
23
55
|
for (const [key, value] of rows)
|
|
24
56
|
console.log(` ${key.padEnd(width)}${value}`);
|
|
25
57
|
if (report.backend === "virtual")
|
|
26
|
-
console.log("\n The virtual shell emulates bash in the Runtime process; it is not a VM boundary
|
|
27
|
-
" For hardware isolation use macOS on Apple Silicon or Linux with KVM.");
|
|
28
|
-
}
|
|
29
|
-
/** One line for the dev banner, or undefined when no connected agent declares a sandbox. */
|
|
30
|
-
export async function sandboxBanner(runtimeUrl, serverKey, manifests) {
|
|
31
|
-
const capability = manifests
|
|
32
|
-
.flatMap((manifest) => manifest.capabilities)
|
|
33
|
-
.find((item) => item.sandbox);
|
|
34
|
-
if (!capability)
|
|
35
|
-
return undefined;
|
|
36
|
-
let report;
|
|
37
|
-
try {
|
|
38
|
-
const response = await fetch(`${runtimeUrl}/v1/host/sandbox`, {
|
|
39
|
-
headers: { authorization: `Bearer ${serverKey}` },
|
|
40
|
-
signal: AbortSignal.timeout(10_000),
|
|
41
|
-
});
|
|
42
|
-
if (response.ok)
|
|
43
|
-
report = await response.json();
|
|
44
|
-
}
|
|
45
|
-
catch { }
|
|
46
|
-
if (!report)
|
|
47
|
-
return undefined;
|
|
48
|
-
const doctor = "run `npx nylorun doctor sandbox` for options";
|
|
49
|
-
if (!report.backend)
|
|
50
|
-
return `sandbox: unavailable (${report.reason}) · ${doctor}`;
|
|
51
|
-
const image = capability.sandbox?.image ?? report.defaultImage;
|
|
52
|
-
const network = capability.sandbox?.network?.preset ?? "dev";
|
|
53
|
-
const fellBack = report.preference === "auto" && report.backend !== report.probes[0]?.name;
|
|
54
|
-
return fellBack
|
|
55
|
-
? `sandbox: ${LABEL[report.backend] ?? report.backend} (${report.reason}) · ${doctor}`
|
|
56
|
-
: `sandbox: ${LABEL[report.backend] ?? report.backend}${report.backend === "microsandbox" && image ? ` · image ${image}` : ""} · network: ${network}`;
|
|
57
|
-
}
|
|
58
|
-
function platform() {
|
|
59
|
-
const os = process.platform === "darwin" ? "macOS" : process.platform === "win32" ? "Windows" : process.platform;
|
|
60
|
-
return `${os} (kernel ${release()}) · ${process.arch}`;
|
|
58
|
+
console.log("\n The virtual shell emulates bash in the Runtime process; it is not a VM boundary.");
|
|
61
59
|
}
|
package/dist/environment.d.ts
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Read the Project `.env` into a value map. Does not mutate `process.env` (F3).
|
|
3
|
+
*/
|
|
4
|
+
export declare function loadProjectEnvironment(root?: string): Record<string, string>;
|
|
2
5
|
export declare function saveEnvironment(root: string, updates: Record<string, string | undefined>, signal?: AbortSignal): Promise<void>;
|
package/dist/environment.js
CHANGED
|
@@ -2,7 +2,10 @@ import { readFileSync, statSync } from "node:fs";
|
|
|
2
2
|
import { writeFile, rename, rm } from "node:fs/promises";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
|
-
import {
|
|
5
|
+
import { parseEnv } from "node:util";
|
|
6
|
+
/**
|
|
7
|
+
* Read the Project `.env` into a value map. Does not mutate `process.env` (F3).
|
|
8
|
+
*/
|
|
6
9
|
export function loadProjectEnvironment(root = process.cwd()) {
|
|
7
10
|
const file = join(root, ".env");
|
|
8
11
|
try {
|
|
@@ -11,10 +14,16 @@ export function loadProjectEnvironment(root = process.cwd()) {
|
|
|
11
14
|
}
|
|
12
15
|
catch (error) {
|
|
13
16
|
if (error.code === "ENOENT")
|
|
14
|
-
return;
|
|
17
|
+
return {};
|
|
15
18
|
throw error;
|
|
16
19
|
}
|
|
17
|
-
|
|
20
|
+
const parsed = parseEnv(readFileSync(file, "utf8"));
|
|
21
|
+
const result = {};
|
|
22
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
23
|
+
if (value !== undefined)
|
|
24
|
+
result[key] = value;
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
18
27
|
}
|
|
19
28
|
// Match complete dotenv assignments, including quoted multiline values.
|
|
20
29
|
const assignment = /^(?:export\s+)?([\w]+)[\t ]*=[\t ]*(?:"[^"]*"|'[^']*'|`[^`]*`|[^#\r\n]*)([^\r\n]*)(?:\r?\n|$)/gm;
|
package/dist/home.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Host root: an explicit home, `NYLORUN_HOME`, or `~/.nylorun`. The stack
|
|
3
|
+
* bind-mounts it into the Runtime and Studio containers, and `@nylorun/admin`
|
|
4
|
+
* reads `host.json` and the admin key from it.
|
|
5
|
+
*/
|
|
6
|
+
export declare function resolveHome(home?: string, env?: Readonly<Record<string, string | undefined>>): string;
|
package/dist/home.js
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
/**
|
|
4
|
+
* The Host root: an explicit home, `NYLORUN_HOME`, or `~/.nylorun`. The stack
|
|
5
|
+
* bind-mounts it into the Runtime and Studio containers, and `@nylorun/admin`
|
|
6
|
+
* reads `host.json` and the admin key from it.
|
|
7
|
+
*/
|
|
8
|
+
export function resolveHome(home, env = process.env) {
|
|
9
|
+
if (home !== undefined && home.trim() !== "")
|
|
10
|
+
return resolve(home);
|
|
11
|
+
const fromEnv = env.NYLORUN_HOME?.trim();
|
|
12
|
+
if (fromEnv)
|
|
13
|
+
return resolve(fromEnv);
|
|
14
|
+
return resolve(join(homedir(), ".nylorun"));
|
|
15
|
+
}
|
|
@@ -11,9 +11,39 @@ export type PromptedModel = {
|
|
|
11
11
|
baseUrl?: string;
|
|
12
12
|
auth: Credential;
|
|
13
13
|
};
|
|
14
|
+
export type ModelCatalog = {
|
|
15
|
+
providers: {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
models: {
|
|
19
|
+
id: string;
|
|
20
|
+
name: string;
|
|
21
|
+
}[];
|
|
22
|
+
auth?: {
|
|
23
|
+
apiKey?: unknown;
|
|
24
|
+
oauth?: unknown;
|
|
25
|
+
};
|
|
26
|
+
}[];
|
|
27
|
+
};
|
|
28
|
+
export type Selection = Readonly<{
|
|
29
|
+
provider: string;
|
|
30
|
+
model: string;
|
|
31
|
+
custom?: Readonly<{
|
|
32
|
+
baseUrl: string;
|
|
33
|
+
}>;
|
|
34
|
+
}>;
|
|
35
|
+
/** Fetch GET /v1/tenant/models when a connection is available. */
|
|
36
|
+
export declare function fetchModelCatalog(options: {
|
|
37
|
+
url: string;
|
|
38
|
+
key: string;
|
|
39
|
+
tenantId: string;
|
|
40
|
+
fetchImpl?: typeof fetch;
|
|
41
|
+
}): Promise<ModelCatalog>;
|
|
14
42
|
export declare function configureProvider(options?: {
|
|
15
43
|
signal?: AbortSignal;
|
|
16
44
|
root?: string;
|
|
17
45
|
input?: Readable;
|
|
18
46
|
output?: Writable;
|
|
47
|
+
/** Tenant API catalog from GET /v1/tenant/models. */
|
|
48
|
+
catalog?: ModelCatalog;
|
|
19
49
|
}): Promise<PromptedModel>;
|
package/dist/model/configure.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { createInterface } from "node:readline/promises";
|
|
2
|
-
import {
|
|
2
|
+
import { builtinModels } from "@earendil-works/pi-ai/providers/all";
|
|
3
|
+
import { createProvider, } from "@earendil-works/pi-ai";
|
|
4
|
+
import { stream, streamSimple, } from "@earendil-works/pi-ai/api/openai-completions";
|
|
3
5
|
export class ConfigurationCancelled extends Error {
|
|
4
6
|
signal;
|
|
5
7
|
exitCode;
|
|
@@ -9,6 +11,112 @@ export class ConfigurationCancelled extends Error {
|
|
|
9
11
|
this.exitCode = signal === "SIGINT" ? 130 : 143;
|
|
10
12
|
}
|
|
11
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* Build a pi-ai registry for interactive login. Catalog listing prefers the
|
|
16
|
+
* Tenant API payload when provided (F2-4).
|
|
17
|
+
*/
|
|
18
|
+
function modelsFor(selection, credentials, catalog) {
|
|
19
|
+
const models = builtinModels({ credentials });
|
|
20
|
+
if (selection.custom) {
|
|
21
|
+
const model = {
|
|
22
|
+
id: selection.model,
|
|
23
|
+
name: selection.model,
|
|
24
|
+
api: "openai-completions",
|
|
25
|
+
provider: "custom",
|
|
26
|
+
baseUrl: selection.custom.baseUrl,
|
|
27
|
+
reasoning: false,
|
|
28
|
+
input: ["text", "image"],
|
|
29
|
+
cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 },
|
|
30
|
+
contextWindow: 128000,
|
|
31
|
+
maxTokens: 16384,
|
|
32
|
+
};
|
|
33
|
+
models.setProvider(createProvider({
|
|
34
|
+
id: "custom",
|
|
35
|
+
name: "Custom OpenAI-compatible",
|
|
36
|
+
baseUrl: selection.custom.baseUrl,
|
|
37
|
+
auth: {
|
|
38
|
+
apiKey: {
|
|
39
|
+
label: "Custom API key",
|
|
40
|
+
schemas: [],
|
|
41
|
+
async resolve() {
|
|
42
|
+
const credential = await credentials.read("custom");
|
|
43
|
+
if (!credential || credential.type !== "api_key")
|
|
44
|
+
return undefined;
|
|
45
|
+
const key = credential.key;
|
|
46
|
+
if (!key)
|
|
47
|
+
return undefined;
|
|
48
|
+
return { type: "api_key", auth: { apiKey: key } };
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
models: [model],
|
|
53
|
+
api: { stream, streamSimple },
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
return {
|
|
57
|
+
models,
|
|
58
|
+
listProviders() {
|
|
59
|
+
if (catalog?.providers.length) {
|
|
60
|
+
return catalog.providers.map((provider) => {
|
|
61
|
+
const live = models.getProvider(provider.id);
|
|
62
|
+
return {
|
|
63
|
+
id: provider.id,
|
|
64
|
+
name: provider.name,
|
|
65
|
+
auth: {
|
|
66
|
+
...(live?.auth?.apiKey || provider.auth?.apiKey
|
|
67
|
+
? { apiKey: live?.auth?.apiKey ?? {} }
|
|
68
|
+
: {}),
|
|
69
|
+
...(live?.auth?.oauth || provider.auth?.oauth
|
|
70
|
+
? { oauth: live?.auth?.oauth ?? {} }
|
|
71
|
+
: {}),
|
|
72
|
+
...(!live?.auth?.apiKey &&
|
|
73
|
+
!live?.auth?.oauth &&
|
|
74
|
+
!provider.auth?.apiKey &&
|
|
75
|
+
!provider.auth?.oauth
|
|
76
|
+
? { apiKey: {} }
|
|
77
|
+
: {}),
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
return models.getProviders().map((provider) => ({
|
|
83
|
+
id: provider.id,
|
|
84
|
+
name: provider.name,
|
|
85
|
+
auth: {
|
|
86
|
+
...(provider.auth?.apiKey ? { apiKey: provider.auth.apiKey } : {}),
|
|
87
|
+
...(provider.auth?.oauth ? { oauth: provider.auth.oauth } : {}),
|
|
88
|
+
},
|
|
89
|
+
}));
|
|
90
|
+
},
|
|
91
|
+
listModels(providerId) {
|
|
92
|
+
const fromCatalog = catalog?.providers.find((p) => p.id === providerId);
|
|
93
|
+
if (fromCatalog?.models.length)
|
|
94
|
+
return fromCatalog.models;
|
|
95
|
+
return models.getModels(providerId).map((model) => ({
|
|
96
|
+
id: model.id,
|
|
97
|
+
name: model.name,
|
|
98
|
+
}));
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/** Fetch GET /v1/tenant/models when a connection is available. */
|
|
103
|
+
export async function fetchModelCatalog(options) {
|
|
104
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
105
|
+
const { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER } = await import("@nylorun/agents");
|
|
106
|
+
const response = await fetchImpl(`${options.url.replace(/\/$/, "")}/v1/tenant/models`, {
|
|
107
|
+
headers: {
|
|
108
|
+
authorization: `Bearer ${options.key}`,
|
|
109
|
+
[TENANT_HEADER]: options.tenantId,
|
|
110
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
111
|
+
Accept: "application/json",
|
|
112
|
+
},
|
|
113
|
+
signal: AbortSignal.timeout(10_000),
|
|
114
|
+
});
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
throw new Error(`Runtime model catalog returned ${response.status}`);
|
|
117
|
+
}
|
|
118
|
+
return (await response.json());
|
|
119
|
+
}
|
|
12
120
|
// Internal options also allow isolated prompt tests without changing process globals.
|
|
13
121
|
export async function configureProvider(options = {}) {
|
|
14
122
|
const output = options.output ?? process.stdout;
|
|
@@ -34,10 +142,8 @@ export async function configureProvider(options = {}) {
|
|
|
34
142
|
return next ?? captured;
|
|
35
143
|
},
|
|
36
144
|
};
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
});
|
|
40
|
-
const providers = models.getProviders();
|
|
145
|
+
const registry = modelsFor({ provider: "", model: "" }, store, options.catalog);
|
|
146
|
+
const providers = registry.listProviders();
|
|
41
147
|
const prompt = createInterface({
|
|
42
148
|
input: options.input ?? process.stdin,
|
|
43
149
|
output,
|
|
@@ -72,21 +178,21 @@ export async function configureProvider(options = {}) {
|
|
|
72
178
|
model,
|
|
73
179
|
custom: { baseUrl },
|
|
74
180
|
};
|
|
75
|
-
const
|
|
76
|
-
if (!(await
|
|
77
|
-
await
|
|
181
|
+
const custom = modelsFor(selection, store, options.catalog);
|
|
182
|
+
if (!(await custom.models.checkAuth("custom", { signal })))
|
|
183
|
+
await custom.models.login("custom", "api_key", interaction());
|
|
78
184
|
return prompted(selection);
|
|
79
185
|
}
|
|
80
186
|
else {
|
|
81
187
|
const chosen = providers[choice - 1];
|
|
82
188
|
if (!chosen)
|
|
83
189
|
throw new Error("Choose a listed provider.");
|
|
84
|
-
const available =
|
|
190
|
+
const available = registry.listModels(chosen.id);
|
|
85
191
|
available.forEach((model, index) => output.write(`${index + 1}. ${model.name} (${model.id})\n`));
|
|
86
192
|
const model = available[Number(await question("Choose a model: ")) - 1];
|
|
87
193
|
if (!model)
|
|
88
194
|
throw new Error("Choose a listed model.");
|
|
89
|
-
if (!(await models.checkAuth(chosen.id, { signal }))) {
|
|
195
|
+
if (!(await registry.models.checkAuth(chosen.id, { signal }))) {
|
|
90
196
|
let method = chosen.auth.apiKey
|
|
91
197
|
? "api_key"
|
|
92
198
|
: "oauth";
|
|
@@ -97,7 +203,7 @@ export async function configureProvider(options = {}) {
|
|
|
97
203
|
if (answer === "2")
|
|
98
204
|
method = "oauth";
|
|
99
205
|
}
|
|
100
|
-
await models.login(chosen.id, method, interaction());
|
|
206
|
+
await registry.models.login(chosen.id, method, interaction());
|
|
101
207
|
}
|
|
102
208
|
return prompted({ provider: chosen.id, model: model.id });
|
|
103
209
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PromptedModel } from "./configure.js";
|
|
2
|
-
/** Wire shape of GET/PUT `/v1/
|
|
2
|
+
/** Wire shape of GET/PUT `/v1/tenant/model` — local copy so CLI stays off `@nylorun/core`. */
|
|
3
3
|
type HostModelView = {
|
|
4
4
|
readonly configured: false;
|
|
5
5
|
} | {
|
|
@@ -9,12 +9,14 @@ type HostModelView = {
|
|
|
9
9
|
readonly authType: "api_key" | "oauth";
|
|
10
10
|
readonly baseUrl?: string;
|
|
11
11
|
};
|
|
12
|
-
export declare function getHostModel(runtimeUrl: string, serverKey: string): Promise<HostModelView>;
|
|
13
|
-
export declare function putHostModel(runtimeUrl: string, serverKey: string, model: PromptedModel): Promise<HostModelView>;
|
|
12
|
+
export declare function getHostModel(runtimeUrl: string, serverKey: string, tenantId?: string): Promise<HostModelView>;
|
|
13
|
+
export declare function putHostModel(runtimeUrl: string, serverKey: string, model: PromptedModel, tenantId?: string): Promise<HostModelView>;
|
|
14
14
|
export declare function ensureHostModel(options: {
|
|
15
15
|
runtimeUrl: string;
|
|
16
16
|
serverKey: string;
|
|
17
|
+
tenantId: string;
|
|
17
18
|
root?: string;
|
|
19
|
+
env?: Readonly<Record<string, string>>;
|
|
18
20
|
signal?: AbortSignal;
|
|
19
21
|
}): Promise<void>;
|
|
20
22
|
export {};
|
package/dist/model/host-model.js
CHANGED
|
@@ -1,20 +1,38 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { PROTOCOL_HEADER, PROTOCOL_VERSION, TENANT_HEADER, } from "@nylorun/agents";
|
|
4
5
|
import { configureProvider } from "./configure.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
import { loadProjectEnvironment } from "../environment.js";
|
|
7
|
+
function tenantHeaders(serverKey, tenantId) {
|
|
8
|
+
return {
|
|
9
|
+
authorization: `Bearer ${serverKey}`,
|
|
10
|
+
[TENANT_HEADER]: tenantId,
|
|
11
|
+
[PROTOCOL_HEADER]: String(PROTOCOL_VERSION),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
function resolveTenantId(explicit) {
|
|
15
|
+
const tenant = explicit?.trim() || process.env.NYLORUN_TENANT?.trim();
|
|
16
|
+
if (!tenant) {
|
|
17
|
+
throw new Error("Set NYLORUN_TENANT (or pass tenant via Project link) before calling Tenant model routes");
|
|
18
|
+
}
|
|
19
|
+
return tenant;
|
|
20
|
+
}
|
|
21
|
+
export async function getHostModel(runtimeUrl, serverKey, tenantId) {
|
|
22
|
+
const tenant = resolveTenantId(tenantId);
|
|
23
|
+
const response = await fetch(`${runtimeUrl}/v1/tenant/model`, {
|
|
24
|
+
headers: tenantHeaders(serverKey, tenant),
|
|
8
25
|
});
|
|
9
26
|
if (!response.ok)
|
|
10
27
|
throw new Error(`Runtime model status returned ${response.status}`);
|
|
11
28
|
return response.json();
|
|
12
29
|
}
|
|
13
|
-
export async function putHostModel(runtimeUrl, serverKey, model) {
|
|
14
|
-
const
|
|
30
|
+
export async function putHostModel(runtimeUrl, serverKey, model, tenantId) {
|
|
31
|
+
const tenant = resolveTenantId(tenantId);
|
|
32
|
+
const response = await fetch(`${runtimeUrl}/v1/tenant/model`, {
|
|
15
33
|
method: "PUT",
|
|
16
34
|
headers: {
|
|
17
|
-
|
|
35
|
+
...tenantHeaders(serverKey, tenant),
|
|
18
36
|
"content-type": "application/json",
|
|
19
37
|
},
|
|
20
38
|
body: JSON.stringify({
|
|
@@ -32,12 +50,15 @@ export async function putHostModel(runtimeUrl, serverKey, model) {
|
|
|
32
50
|
return body;
|
|
33
51
|
}
|
|
34
52
|
export async function ensureHostModel(options) {
|
|
35
|
-
const current = await getHostModel(options.runtimeUrl, options.serverKey);
|
|
53
|
+
const current = await getHostModel(options.runtimeUrl, options.serverKey, options.tenantId);
|
|
36
54
|
if (current.configured)
|
|
37
55
|
return;
|
|
38
|
-
const
|
|
56
|
+
const env = options.env ?? loadProjectEnvironment(options.root ?? process.cwd());
|
|
57
|
+
if (env.NYLORUN_DEV_MODEL?.trim() === "fixture")
|
|
58
|
+
return;
|
|
59
|
+
const seeded = seedFromEnv(env, options.root ?? process.cwd());
|
|
39
60
|
if (seeded) {
|
|
40
|
-
await putHostModel(options.runtimeUrl, options.serverKey, seeded);
|
|
61
|
+
await putHostModel(options.runtimeUrl, options.serverKey, seeded, options.tenantId);
|
|
41
62
|
return;
|
|
42
63
|
}
|
|
43
64
|
if (process.stdin.isTTY && process.stdout.isTTY) {
|
|
@@ -45,18 +66,18 @@ export async function ensureHostModel(options) {
|
|
|
45
66
|
root: options.root,
|
|
46
67
|
signal: options.signal,
|
|
47
68
|
});
|
|
48
|
-
await putHostModel(options.runtimeUrl, options.serverKey, prompted);
|
|
69
|
+
await putHostModel(options.runtimeUrl, options.serverKey, prompted, options.tenantId);
|
|
49
70
|
return;
|
|
50
71
|
}
|
|
51
|
-
throw new Error("Model provider is not configured. Run
|
|
72
|
+
throw new Error("Model provider is not configured. Run nylo configure in a terminal to set it up, or open Studio.");
|
|
52
73
|
}
|
|
53
|
-
function
|
|
54
|
-
const provider =
|
|
55
|
-
const model =
|
|
74
|
+
function seedFromEnv(env, root) {
|
|
75
|
+
const provider = env.MODEL_PROVIDER?.trim();
|
|
76
|
+
const model = env.MODEL?.trim();
|
|
56
77
|
if (!provider || !model)
|
|
57
78
|
return undefined;
|
|
58
|
-
const baseUrl =
|
|
59
|
-
const key =
|
|
79
|
+
const baseUrl = env.MODEL_PROVIDER_BASE_URL?.trim();
|
|
80
|
+
const key = env.MODEL_PROVIDER_API_KEY;
|
|
60
81
|
const auth = key
|
|
61
82
|
? { type: "api_key", key }
|
|
62
83
|
: legacyCredential(root, provider);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Admin } from "@nylorun/admin";
|
|
2
|
+
import { type ProjectCredentials } from "./credentials.js";
|
|
3
|
+
import { type ProjectLink } from "./link.js";
|
|
4
|
+
export interface CreateTenantOptions {
|
|
5
|
+
/** Prefer an already-resolved Admin (local Host or explicit). */
|
|
6
|
+
admin: Admin;
|
|
7
|
+
hostId: string;
|
|
8
|
+
projectRoot: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
/** Override Host URL written into the link (defaults to admin.url). */
|
|
11
|
+
hostUrl?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface CreateTenantResult {
|
|
14
|
+
link: ProjectLink;
|
|
15
|
+
credentials: ProjectCredentials;
|
|
16
|
+
envelope: {
|
|
17
|
+
id: string;
|
|
18
|
+
name: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
/** The Project's `package.json` name, else its directory name. */
|
|
22
|
+
export declare function defaultTenantName(projectRoot: string): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Create a Tenant via `@nylorun/admin` and write format-1 link + credentials.
|
|
25
|
+
*/
|
|
26
|
+
export declare function createProjectTenant(options: CreateTenantOptions): Promise<CreateTenantResult>;
|
|
27
|
+
/** Resolve Admin from the local Host root (host.json and the admin key). */
|
|
28
|
+
export declare function localAdmin(home?: string): Admin;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import { basename, join } from "node:path";
|
|
3
|
+
import { createAdmin } from "@nylorun/admin";
|
|
4
|
+
import { newPrincipalId } from "@nylorun/agents";
|
|
5
|
+
import { CliError } from "../errors.js";
|
|
6
|
+
import { writeCredentials, } from "./credentials.js";
|
|
7
|
+
import { writeLink } from "./link.js";
|
|
8
|
+
/** The Project's `package.json` name, else its directory name. */
|
|
9
|
+
export async function defaultTenantName(projectRoot) {
|
|
10
|
+
try {
|
|
11
|
+
const pkg = JSON.parse(await readFile(join(projectRoot, "package.json"), "utf8"));
|
|
12
|
+
if (typeof pkg.name === "string" && pkg.name.trim())
|
|
13
|
+
return pkg.name.trim();
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
/* fall through */
|
|
17
|
+
}
|
|
18
|
+
const base = basename(projectRoot);
|
|
19
|
+
return base && base.length > 0 ? base : "project";
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a Tenant via `@nylorun/admin` and write format-1 link + credentials.
|
|
23
|
+
*/
|
|
24
|
+
export async function createProjectTenant(options) {
|
|
25
|
+
const name = options.name ?? (await defaultTenantName(options.projectRoot));
|
|
26
|
+
const admin = options.admin;
|
|
27
|
+
let created;
|
|
28
|
+
try {
|
|
29
|
+
created = await admin.createTenant({ name });
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
throw new CliError(error instanceof Error
|
|
33
|
+
? error.message
|
|
34
|
+
: `Could not create Tenant: ${String(error)}`, 1);
|
|
35
|
+
}
|
|
36
|
+
const hostUrl = (options.hostUrl ?? admin.url).replace(/\/$/, "");
|
|
37
|
+
// CLIENTS-CCR: admin.createTenant should also return principalId (D§4.2 /
|
|
38
|
+
// credentials.json). Until then, mint a local id for the credentials file;
|
|
39
|
+
// agents resolveConnection only needs applicationKey.
|
|
40
|
+
const principalId = "principalId" in created &&
|
|
41
|
+
typeof created.principalId === "string"
|
|
42
|
+
? created.principalId
|
|
43
|
+
: newPrincipalId();
|
|
44
|
+
const credentials = {
|
|
45
|
+
format: 1,
|
|
46
|
+
applicationKey: created.applicationKey,
|
|
47
|
+
principalId,
|
|
48
|
+
};
|
|
49
|
+
const link = {
|
|
50
|
+
format: 1,
|
|
51
|
+
hostUrl,
|
|
52
|
+
hostId: options.hostId,
|
|
53
|
+
tenantId: created.tenant.id,
|
|
54
|
+
};
|
|
55
|
+
await writeCredentials(options.projectRoot, credentials);
|
|
56
|
+
await writeLink(options.projectRoot, link);
|
|
57
|
+
return {
|
|
58
|
+
link,
|
|
59
|
+
credentials,
|
|
60
|
+
envelope: { id: created.tenant.id, name: created.tenant.name },
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
/** Resolve Admin from the local Host root (host.json and the admin key). */
|
|
64
|
+
export function localAdmin(home) {
|
|
65
|
+
return createAdmin(home ? { home } : undefined);
|
|
66
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Project-local credentials (mode 0600). Format 1 drops `executors`.
|
|
3
|
+
* Format 0 may still contain an executors map; readers ignore it (D§3.7).
|
|
4
|
+
*/
|
|
5
|
+
export interface ProjectCredentials {
|
|
6
|
+
format: 0 | 1;
|
|
7
|
+
applicationKey: string;
|
|
8
|
+
principalId: string;
|
|
9
|
+
/** Present only on format 0 files; ignored by version 1. */
|
|
10
|
+
executors?: Record<string, string>;
|
|
11
|
+
}
|
|
12
|
+
export declare function readCredentials(projectRoot: string): Promise<ProjectCredentials | undefined>;
|
|
13
|
+
export declare function writeCredentials(projectRoot: string, credentials: {
|
|
14
|
+
applicationKey: string;
|
|
15
|
+
principalId: string;
|
|
16
|
+
format?: 0 | 1;
|
|
17
|
+
}): Promise<void>;
|
|
18
|
+
export declare function removeCredentials(projectRoot: string): Promise<void>;
|