@chengchenccc/oh-my-agent 0.2.0-rc.1 → 0.2.0-rc.2

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.
@@ -1,9 +1,14 @@
1
1
  /** The gateway domain's verbs. `oma gateway <verb>` is the CLI surface for
2
2
  * backend + web: fetch their artifact, run it, report on it. */
3
- export declare const GATEWAY_VERBS: readonly ["up", "down", "fetch", "status"];
3
+ export declare const GATEWAY_VERBS: readonly ["up", "down", "fetch", "status", "doctor", "passwd"];
4
4
  export type GatewayCommand = (typeof GATEWAY_VERBS)[number];
5
- export declare const GATEWAY_USAGE = "oma gateway <command>\n\n up start backend + web; stays in the foreground (Ctrl-C stops it)\n up -d same, but detached: returns once it is healthy\n down stop a detached gateway\n fetch download, verify and unpack the gateway artifact for a version\n status which version is installed, is it running, is it answering\n\n --version <v> artifact version for up/fetch (default: oma's own)\n\nNote: 'gateway' is a reserved first word. To send it as a prompt, use\n'oma -p \"gateway ...\"'.";
5
+ export declare const GATEWAY_USAGE = "oma gateway <command>\n\n up start backend + web; stays in the foreground (Ctrl-C stops it)\n up -d same, but detached: returns once it is healthy\n down stop a detached gateway\n fetch download, verify and unpack the gateway artifact for a version\n status which version is installed, is it running, is it answering\n doctor why nothing can run: artifact, tools, catalog, keys, ports, oma bin\n passwd replace the login password with a fresh random one\n\n --version <v> artifact version for up/fetch (default: oma's own)\n\nNote: 'gateway' is a reserved first word. To send it as a prompt, use\n'oma -p \"gateway ...\"'.";
6
6
  export declare function isGatewayCommand(value: string): value is GatewayCommand;
7
+ /** What to tell the operator about logging in. The password is only echoed on
8
+ * an interactive terminal: a detached run's stdout is redirected into a log
9
+ * file, and a secret must not be filed away there. The pointer is always
10
+ * printed, so a missed banner is never a dead end. */
11
+ export declare function credentialLines(home: string, secrets: Record<string, string>, interactive: boolean): string[];
7
12
  export interface GatewayCommandOptions {
8
13
  home?: string;
9
14
  version?: string;
@@ -21,6 +26,13 @@ export declare function runGatewayStatus(opts?: GatewayCommandOptions): Promise<
21
26
  /** `oma gateway up`: fetch the version if it is missing, then run the whole gateway in
22
27
  * the foreground (Ctrl-C stops it, children are signalled in reverse order). */
23
28
  export declare function runGatewayUp(opts?: GatewayCommandOptions): Promise<number>;
29
+ /** `oma gateway passwd`: rotate the login password. Generated, never read from
30
+ * argv — an argument would land in shell history and process listings.
31
+ * Takes effect on the next start, because the web component reads it from its
32
+ * environment. */
33
+ export declare function runGatewayPasswd(opts?: GatewayCommandOptions): Promise<number>;
34
+ /** `oma gateway doctor`: check the deployment and say how to fix what is wrong. */
35
+ export declare function runGatewayDoctor(opts?: GatewayCommandOptions): Promise<number>;
24
36
  /** `oma gateway down`: stop whatever `up -d` started. */
25
37
  export declare function runGatewayDown(opts?: GatewayCommandOptions): Promise<number>;
26
38
  /** `oma gateway <verb>`: single entry point so the CLI owns one code path. */
@@ -1 +1 @@
1
- {"version":3,"file":"gateway-commands.d.ts","sourceRoot":"","sources":["../../src/cli/gateway-commands.ts"],"names":[],"mappings":"AAqBA;iEACiE;AACjE,eAAO,MAAM,aAAa,4CAA6C,CAAC;AACxE,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,eAAO,MAAM,aAAa,ygBAWF,CAAC;AAEzB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc,CAKvE;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAyBD;qEACqE;AACrE,wBAAsB,eAAe,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBvF;AAED;qEACqE;AACrE,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAuDxF;AAED;iFACiF;AACjF,wBAAsB,YAAY,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAkCpF;AAED,yDAAyD;AACzD,wBAAsB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAUtF;AA0DD,8EAA8E;AAC9E,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,MAAM,CAAC,CAKjB"}
1
+ {"version":3,"file":"gateway-commands.d.ts","sourceRoot":"","sources":["../../src/cli/gateway-commands.ts"],"names":[],"mappings":"AAwBA;iEACiE;AACjE,eAAO,MAAM,aAAa,gEAAiE,CAAC;AAC5F,MAAM,MAAM,cAAc,GAAG,CAAC,OAAO,aAAa,CAAC,CAAC,MAAM,CAAC,CAAC;AAE5D,eAAO,MAAM,aAAa,0pBAaF,CAAC;AAEzB,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,KAAK,IAAI,cAAc,CAKvE;AAED;;;uDAGuD;AACvD,wBAAgB,eAAe,CAC7B,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,EAC/B,WAAW,EAAE,OAAO,GACnB,MAAM,EAAE,CAMV;AAED,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,4DAA4D;IAC5D,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAyBD;qEACqE;AACrE,wBAAsB,eAAe,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAmBvF;AAED;qEACqE;AACrE,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAgExF;AAED;iFACiF;AACjF,wBAAsB,YAAY,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAoCpF;AAED;;;mBAGmB;AACnB,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CA0BxF;AA2BD,mFAAmF;AACnF,wBAAsB,gBAAgB,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAiBxF;AAED,yDAAyD;AACzD,wBAAsB,cAAc,CAAC,IAAI,GAAE,qBAA0B,GAAG,OAAO,CAAC,MAAM,CAAC,CAUtF;AA+DD,8EAA8E;AAC9E,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,cAAc,EACpB,IAAI,GAAE,qBAA0B,GAC/B,OAAO,CAAC,MAAM,CAAC,CAOjB"}
@@ -1,12 +1,14 @@
1
1
  import { existsSync } from "node:fs";
2
+ import { writeFile } from "node:fs/promises";
2
3
  import { join, resolve } from "node:path";
3
- import { currentVersion, fetchGatewayArtifact, gatewayPaths, installedVersions, omaHome, omaVersion, readSecrets, } from "../core/gateway/artifact.js";
4
+ import { currentVersion, fetchGatewayArtifact, gatewayPaths, generatePassword, installedVersions, omaHome, omaVersion, readSecrets, } from "../core/gateway/artifact.js";
4
5
  import { isProcessAlive, readDaemon, startDetachedGateway, stopDetachedGateway, tailDaemonLog, } from "../core/gateway/daemon.js";
6
+ import { diagnoseGateway } from "../core/gateway/doctor.js";
5
7
  import { readGatewayManifest } from "../core/gateway/manifest.js";
6
8
  import { runGateway } from "../core/gateway/supervisor.js";
7
9
  /** The gateway domain's verbs. `oma gateway <verb>` is the CLI surface for
8
10
  * backend + web: fetch their artifact, run it, report on it. */
9
- export const GATEWAY_VERBS = ["up", "down", "fetch", "status"];
11
+ export const GATEWAY_VERBS = ["up", "down", "fetch", "status", "doctor", "passwd"];
10
12
  export const GATEWAY_USAGE = `oma gateway <command>
11
13
 
12
14
  up start backend + web; stays in the foreground (Ctrl-C stops it)
@@ -14,6 +16,8 @@ export const GATEWAY_USAGE = `oma gateway <command>
14
16
  down stop a detached gateway
15
17
  fetch download, verify and unpack the gateway artifact for a version
16
18
  status which version is installed, is it running, is it answering
19
+ doctor why nothing can run: artifact, tools, catalog, keys, ports, oma bin
20
+ passwd replace the login password with a fresh random one
17
21
 
18
22
  --version <v> artifact version for up/fetch (default: oma's own)
19
23
 
@@ -26,6 +30,18 @@ export function isGatewayCommand(value) {
26
30
  }
27
31
  return false;
28
32
  }
33
+ /** What to tell the operator about logging in. The password is only echoed on
34
+ * an interactive terminal: a detached run's stdout is redirected into a log
35
+ * file, and a secret must not be filed away there. The pointer is always
36
+ * printed, so a missed banner is never a dead end. */
37
+ export function credentialLines(home, secrets, interactive) {
38
+ const lines = [];
39
+ const password = secrets.MOCK_PASSWORD;
40
+ if (password && interactive)
41
+ lines.push(`login: user-001 / ${password}`);
42
+ lines.push(`login password: oma gateway status (or read ${gatewayPaths(home).secrets})`);
43
+ return lines;
44
+ }
29
45
  /** A source checkout runs the gateway from the repo, so the release artifact it
30
46
  * would download is unused. Only stay quiet there: an explicit CLI call still
31
47
  * fetches. `npm_lifecycle_event` is set while a package's postinstall runs. */
@@ -81,6 +97,13 @@ export async function runGatewayStatus(opts = {}) {
81
97
  log(`gateway home: ${paths.home}`);
82
98
  log(`versions: ${versions.length > 0 ? versions.join(", ") : "(none installed)"}`);
83
99
  log(`current: ${current ?? "(none)"}`);
100
+ // Which catalog the gateway's children will read: product runs deliberately
101
+ // ignore ~/.oma/models.yml (the agent's own bash tool can write there), so a
102
+ // custom provider only counts when OMA_HOME points at its models.yml.
103
+ const catalogHome = process.env.OMA_HOME;
104
+ log(catalogHome
105
+ ? `catalog: ${catalogHome}/models.yml via OMA_HOME`
106
+ : "catalog: built-in providers only (OMA_HOME unset — a custom models.yml is not read)");
84
107
  if (versions.length === 0) {
85
108
  log(`install one: oma gateway fetch (oma ${omaVersion()})`);
86
109
  return 1;
@@ -154,8 +177,9 @@ export async function runGatewayUp(opts = {}) {
154
177
  log("");
155
178
  log(`gateway ${handle.manifest.version} is up (${handle.manifest.components.length} processes)`);
156
179
  log(`open http://127.0.0.1:${web?.port ?? 3001}/login`);
157
- if (handle.secrets.MOCK_PASSWORD)
158
- log(`login: user-001 / ${handle.secrets.MOCK_PASSWORD}`);
180
+ for (const line of credentialLines(home, handle.secrets, process.stdout.isTTY === true)) {
181
+ log(line);
182
+ }
159
183
  log("Ctrl-C stops the gateway");
160
184
  },
161
185
  });
@@ -165,6 +189,73 @@ export async function runGatewayUp(opts = {}) {
165
189
  return 1;
166
190
  }
167
191
  }
192
+ /** `oma gateway passwd`: rotate the login password. Generated, never read from
193
+ * argv — an argument would land in shell history and process listings.
194
+ * Takes effect on the next start, because the web component reads it from its
195
+ * environment. */
196
+ export async function runGatewayPasswd(opts = {}) {
197
+ const home = opts.home ?? omaHome();
198
+ const log = loggerFor(opts, "stdout");
199
+ const secretsPath = gatewayPaths(home).secrets;
200
+ if (!existsSync(secretsPath)) {
201
+ log(`no secrets at ${secretsPath} — run 'oma gateway up' once first`);
202
+ return 1;
203
+ }
204
+ const password = generatePassword();
205
+ const secrets = { ...readSecrets(home), MOCK_PASSWORD: password };
206
+ await writeFile(secretsPath, `${JSON.stringify(secrets, null, 2)}\n`, { mode: 0o600 });
207
+ log(`login password replaced: user-001 / ${password}`);
208
+ // A password set in the console is stored as a hash and wins over this file,
209
+ // so push the new one there too when the gateway is up — otherwise rotating
210
+ // here would silently not take effect.
211
+ const pushed = await pushPasswordToRunningGateway(secrets, password);
212
+ if (pushed) {
213
+ log("the running gateway took it too (console-set password updated)");
214
+ }
215
+ else {
216
+ log("restart to apply: oma gateway down && oma gateway up -d");
217
+ log("note: a password set in the web console wins over this file — change it there if you used that");
218
+ }
219
+ return 0;
220
+ }
221
+ /** Update the console-set password (a hash in the backend's settings) when the
222
+ * gateway is reachable. Returns false when it is not: the file value then
223
+ * applies after the next start. */
224
+ async function pushPasswordToRunningGateway(secrets, password) {
225
+ const token = secrets.BACKEND_AUTH_TOKEN;
226
+ if (!token)
227
+ return false;
228
+ try {
229
+ const res = await fetch(`${process.env.BACKEND_URL ?? "http://127.0.0.1:3000"}/api/auth/password`, {
230
+ method: "PUT",
231
+ headers: { "content-type": "application/json", "x-auth-token": token },
232
+ body: JSON.stringify({ password }),
233
+ signal: AbortSignal.timeout(2000),
234
+ });
235
+ return res.ok;
236
+ }
237
+ catch {
238
+ return false;
239
+ }
240
+ }
241
+ /** `oma gateway doctor`: check the deployment and say how to fix what is wrong. */
242
+ export async function runGatewayDoctor(opts = {}) {
243
+ const home = opts.home ?? omaHome();
244
+ const log = loggerFor(opts, "stdout");
245
+ const checks = await diagnoseGateway({ home });
246
+ log(`oma gateway doctor (home: ${home})`);
247
+ for (const check of checks) {
248
+ log(`${check.ok ? "✓" : "✗"} ${check.id.padEnd(9)} ${check.detail}`);
249
+ if (check.fix)
250
+ log(` ${check.ok ? "note" : "fix"}: ${check.fix}`);
251
+ }
252
+ const failed = checks.filter((check) => !check.ok);
253
+ log("");
254
+ log(failed.length === 0
255
+ ? "no problems found"
256
+ : `${failed.length} problem(s): ${failed.map((c) => c.id).join(", ")}`);
257
+ return failed.length === 0 ? 0 : 1;
258
+ }
168
259
  /** `oma gateway down`: stop whatever `up -d` started. */
169
260
  export async function runGatewayDown(opts = {}) {
170
261
  const home = opts.home ?? omaHome();
@@ -206,12 +297,12 @@ async function startGatewayDetached(opts) {
206
297
  try {
207
298
  const res = await fetch(healthUrl, { signal: AbortSignal.timeout(2000) });
208
299
  if (res.ok) {
209
- const password = readSecrets(opts.home).MOCK_PASSWORD;
210
300
  opts.log("");
211
301
  opts.log(`gateway ${opts.version} is up in the background (pid ${daemon.pid})`);
212
302
  opts.log(`open http://127.0.0.1:${web?.port ?? 3001}/login`);
213
- if (password)
214
- opts.log(`login: user-001 / ${password}`);
303
+ for (const line of credentialLines(opts.home, readSecrets(opts.home), process.stdout.isTTY === true)) {
304
+ opts.log(line);
305
+ }
215
306
  opts.log("stop it with: oma gateway down");
216
307
  return 0;
217
308
  }
@@ -238,5 +329,9 @@ export async function runGatewayCommand(verb, opts = {}) {
238
329
  return runGatewayDown(opts);
239
330
  if (verb === "fetch")
240
331
  return runGatewayFetch(opts);
332
+ if (verb === "doctor")
333
+ return runGatewayDoctor(opts);
334
+ if (verb === "passwd")
335
+ return runGatewayPasswd(opts);
241
336
  return runGatewayStatus(opts);
242
337
  }
@@ -29,7 +29,7 @@ export declare function releaseLocation(version: string, env?: NodeJS.ProcessEnv
29
29
  export declare function sha256File(path: string): Promise<string>;
30
30
  /** Parse the SHA256SUMS entry for one file (GNU coreutils format). */
31
31
  export declare function parseSums(sums: string, fileName: string): string;
32
- /** Read the launcher's secrets without generating anything (status paths). */
32
+ export declare function generatePassword(): string;
33
33
  export declare function readSecrets(home: string): Record<string, string>;
34
34
  /** Read the launcher's secrets, generating any missing one. 0600: they
35
35
  * authenticate the local single-user gateway. */
@@ -39,6 +39,8 @@ export interface FetchOptions {
39
39
  version?: string;
40
40
  env?: NodeJS.ProcessEnv;
41
41
  log?: (line: string) => void;
42
+ /** No-byte allowance before a download is declared dead (test seam). */
43
+ stallMs?: number;
42
44
  }
43
45
  export interface FetchResult {
44
46
  version: string;
@@ -1 +1 @@
1
- {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/artifact.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,eAAe,CAAC;AAE1E,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD,eAAO,MAAM,kBAAkB,4BAA4B,CAAC;AAM5D,wBAAgB,OAAO,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEpE;AAED;;sDAEsD;AACtD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,MAAkB,GAAG,YAAY,CAUnE;AAED;qDACqD;AACrD,wBAAgB,UAAU,IAAI,MAAM,CAQnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;oFACoF;AACpF,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAMjB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMhE;AA2CD,8EAA8E;AAC9E,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBhE;AAED;kDACkD;AAClD,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAgBjC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9B;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;6EAC6E;AAC7E,wBAAsB,oBAAoB,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuDxF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,EAAE,CAMpE;AAED,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,GAAG,SAAS,CAO3E"}
1
+ {"version":3,"file":"artifact.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/artifact.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,eAAe,EAAuB,MAAM,eAAe,CAAC;AAE1E,qBAAa,oBAAqB,SAAQ,KAAK;CAAG;AAElD,eAAO,MAAM,kBAAkB,4BAA4B,CAAC;AAM5D,wBAAgB,OAAO,CAAC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAAG,MAAM,CAEpE;AAED;;sDAEsD;AACtD,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,2CAA2C;IAC3C,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,YAAY,CAAC,IAAI,GAAE,MAAkB,GAAG,YAAY,CAUnE;AAED;qDACqD;AACrD,wBAAgB,UAAU,IAAI,MAAM,CAQnC;AAED,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;CACd;AAED;oFACoF;AACpF,wBAAgB,eAAe,CAC7B,OAAO,EAAE,MAAM,EACf,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,eAAe,CAMjB;AAED,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAI9D;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMhE;AAiFD,wBAAgB,gBAAgB,IAAI,MAAM,CAYzC;AASD,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAgBhE;AAED;kDACkD;AAClD,wBAAsB,aAAa,CACjC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,SAAS,MAAM,EAAE,GACvB,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAiBjC;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7B,wEAAwE;IACxE,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,GAAG,EAAE,MAAM,CAAC;IACZ,UAAU,EAAE,OAAO,CAAC;IACpB,QAAQ,EAAE,eAAe,CAAC;IAC1B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED;6EAC6E;AAC7E,wBAAsB,oBAAoB,CAAC,IAAI,GAAE,YAAiB,GAAG,OAAO,CAAC,WAAW,CAAC,CAuDxF;AAED,wBAAgB,iBAAiB,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,EAAE,CAMpE;AAED,wBAAgB,cAAc,CAAC,IAAI,GAAE,MAAkB,GAAG,MAAM,GAAG,SAAS,CAO3E"}
@@ -56,7 +56,24 @@ export function parseSums(sums, fileName) {
56
56
  }
57
57
  throw new GatewayArtifactError(`SHA256SUMS has no entry for ${fileName}`);
58
58
  }
59
- async function download(url, dest) {
59
+ /** How long a download may go without delivering a byte before it is called
60
+ * dead. A slow-but-moving transfer is fine; a wedged one must not hang an
61
+ * install (a postinstall sat for 10 minutes before this existed). */
62
+ const DEFAULT_STALL_MS = 60_000;
63
+ async function withStallTimeout(work, ms, onStall) {
64
+ let timer;
65
+ const stall = new Promise((_, reject) => {
66
+ timer = setTimeout(() => reject(onStall()), ms);
67
+ });
68
+ try {
69
+ return await Promise.race([work, stall]);
70
+ }
71
+ finally {
72
+ if (timer)
73
+ clearTimeout(timer);
74
+ }
75
+ }
76
+ async function download(url, dest, stallMs) {
60
77
  let res;
61
78
  try {
62
79
  res = await fetch(url, { redirect: "follow" });
@@ -73,11 +90,17 @@ async function download(url, dest) {
73
90
  // also keeps a 60MB artifact out of memory.
74
91
  const writer = Bun.file(dest).writer();
75
92
  const reader = res.body.getReader();
76
- for (;;) {
77
- const { done, value } = await reader.read();
78
- if (done)
79
- break;
80
- writer.write(value);
93
+ try {
94
+ for (;;) {
95
+ const { done, value } = await withStallTimeout(reader.read(), stallMs, () => new GatewayArtifactError(`download stalled for ${Math.round(stallMs / 1000)}s at ${url} — check the connection, then retry`));
96
+ if (done)
97
+ break;
98
+ writer.write(value);
99
+ }
100
+ }
101
+ catch (err) {
102
+ await reader.cancel().catch(() => { });
103
+ throw err;
81
104
  }
82
105
  await writer.end();
83
106
  }
@@ -99,6 +122,34 @@ async function unpack(tarball, dest) {
99
122
  }
100
123
  }
101
124
  /** Read the launcher's secrets without generating anything (status paths). */
125
+ /** Human-typed login password. Entropy comes from length, not from requiring
126
+ * character classes: 22 chars over a 56-symbol alphabet is ~128 bits. The
127
+ * alphabet drops look-alikes (l, 1, I, O, 0) and shell-hostile punctuation,
128
+ * because the person typing it may be reading it off a terminal on another
129
+ * device. */
130
+ const PASSWORD_ALPHABET = "ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789";
131
+ const PASSWORD_LENGTH = 22;
132
+ export function generatePassword() {
133
+ // Rejection sampling keeps the alphabet uniform (256 % 56 is not zero).
134
+ const ceiling = Math.floor(256 / PASSWORD_ALPHABET.length) * PASSWORD_ALPHABET.length;
135
+ let password = "";
136
+ while (password.length < PASSWORD_LENGTH) {
137
+ for (const byte of crypto.getRandomValues(new Uint8Array(PASSWORD_LENGTH))) {
138
+ if (byte >= ceiling)
139
+ continue;
140
+ password += PASSWORD_ALPHABET.charAt(byte % PASSWORD_ALPHABET.length);
141
+ if (password.length === PASSWORD_LENGTH)
142
+ break;
143
+ }
144
+ }
145
+ return password;
146
+ }
147
+ /** Internal tokens (backend auth, session signing): never typed, so the cheap
148
+ * hex encoding is fine. */
149
+ function generateToken() {
150
+ const bytes = crypto.getRandomValues(new Uint8Array(24));
151
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
152
+ }
102
153
  export function readSecrets(home) {
103
154
  const path = gatewayPaths(home).secrets;
104
155
  if (!existsSync(path))
@@ -127,8 +178,9 @@ export async function ensureSecrets(home, names) {
127
178
  for (const name of names) {
128
179
  if (existing[name])
129
180
  continue;
130
- const bytes = crypto.getRandomValues(new Uint8Array(24));
131
- existing[name] = Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join("");
181
+ // MOCK_PASSWORD is the one a human retypes at the login page; the rest are
182
+ // machine-to-machine tokens.
183
+ existing[name] = name === "MOCK_PASSWORD" ? generatePassword() : generateToken();
132
184
  generated = true;
133
185
  }
134
186
  if (generated) {
@@ -163,7 +215,7 @@ export async function fetchGatewayArtifact(opts = {}) {
163
215
  if (!sums.ok)
164
216
  throw new GatewayArtifactError(`${sumsUrl} -> HTTP ${sums.status}`);
165
217
  const expected = parseSums(await sums.text(), `oma-gateway-${version}.tar.zst`);
166
- await download(tarballUrl, tarball);
218
+ await download(tarballUrl, tarball, opts.stallMs ?? DEFAULT_STALL_MS);
167
219
  const actual = await sha256File(tarball);
168
220
  if (actual !== expected) {
169
221
  throw new GatewayArtifactError(`checksum mismatch for ${version}: expected ${expected.slice(0, 12)}…, got ${actual.slice(0, 12)}…`);
@@ -1 +1 @@
1
- {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/daemon.ts"],"names":[],"mappings":"AAaA;mDACmD;AACnD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAGrD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASlE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAED;;;gFAGgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAexD;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;6CAE6C;AAC7C,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CA6B7F;AAED;uBACuB;AACvB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CA4B7F;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,EAAE,CAKhE"}
1
+ {"version":3,"file":"daemon.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/daemon.ts"],"names":[],"mappings":"AAaA;mDACmD;AACnD,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,8EAA8E;AAC9E,MAAM,WAAW,WAAW;IAC1B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;CACb;AAED,wBAAgB,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,WAAW,CAGrD;AAED,4EAA4E;AAC5E,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,GAAG,aAAa,GAAG,SAAS,CASlE;AAED,wBAAgB,cAAc,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CASnD;AAED;;;gFAGgF;AAChF,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAexD;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,OAAO,EAAE,SAAS,MAAM,EAAE,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;6CAE6C;AAC7C,wBAAsB,oBAAoB,CAAC,IAAI,EAAE,oBAAoB,GAAG,OAAO,CAAC,aAAa,CAAC,CA+B7F;AAED;uBACuB;AACvB,wBAAsB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,SAAS,GAAG,OAAO,CAAC,UAAU,CAAC,CA4B7F;AAED,kEAAkE;AAClE,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,SAAK,GAAG,MAAM,EAAE,CAKhE"}
@@ -68,7 +68,9 @@ export async function startDetachedGateway(opts) {
68
68
  const gateway = gatewayPaths(opts.home);
69
69
  await mkdir(gateway.root, { recursive: true });
70
70
  const paths = daemonPaths(opts.home);
71
- const logFd = openSync(paths.log, "a");
71
+ // 0600: the log carries the supervisor's own output, and nothing that runs
72
+ // unattended should be world-readable.
73
+ const logFd = openSync(paths.log, "a", 0o600);
72
74
  let pid;
73
75
  try {
74
76
  const proc = Bun.spawn([...opts.command], {
@@ -0,0 +1,21 @@
1
+ /** One diagnosis line: what was checked, what was found, and how to fix it. */
2
+ export interface GatewayCheck {
3
+ id: string;
4
+ ok: boolean;
5
+ detail: string;
6
+ fix?: string;
7
+ }
8
+ export interface DoctorOptions {
9
+ home?: string;
10
+ /** Seam for tests: replaces the `oma --list-models` probe. */
11
+ probeModels?: () => Promise<{
12
+ providers: string[];
13
+ }>;
14
+ /** Seam for tests: replaces the release HEAD probe. Returns the HTTP status,
15
+ * or undefined when the host could not be reached. */
16
+ probeRelease?: (url: string) => Promise<number | undefined>;
17
+ }
18
+ /** Diagnose why a gateway may not be able to run anything: artifact, tools,
19
+ * catalog source, runnable providers, ports, and the per-run oma binary. */
20
+ export declare function diagnoseGateway(opts?: DoctorOptions): Promise<GatewayCheck[]>;
21
+ //# sourceMappingURL=doctor.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"doctor.d.ts","sourceRoot":"","sources":["../../../src/core/gateway/doctor.ts"],"names":[],"mappings":"AAeA,+EAA+E;AAC/E,MAAM,WAAW,YAAY;IAC3B,EAAE,EAAE,MAAM,CAAC;IACX,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,WAAW,CAAC,EAAE,MAAM,OAAO,CAAC;QAAE,SAAS,EAAE,MAAM,EAAE,CAAA;KAAE,CAAC,CAAC;IACrD;2DACuD;IACvD,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAAC;CAC7D;AAoED;6EAC6E;AAC7E,wBAAsB,eAAe,CAAC,IAAI,GAAE,aAAkB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CA2UvF"}
@@ -0,0 +1,405 @@
1
+ import { existsSync, readFileSync, statfsSync, statSync } from "node:fs";
2
+ import { rm, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { currentVersion, gatewayPaths, omaHome, omaVersion, readSecrets, releaseLocation, } from "./artifact.js";
5
+ import { daemonPaths, isProcessAlive, readDaemon } from "./daemon.js";
6
+ import { readGatewayManifest } from "./manifest.js";
7
+ import { resolveOmaBin } from "./supervisor.js";
8
+ /** Provider env names oma's builtin catalog knows (the product backend
9
+ * forwards every *_API_KEY, but these work with no models.yml at all). */
10
+ const BUILTIN_KEY_ENVS = [
11
+ "ANTHROPIC_API_KEY",
12
+ "ANTHROPIC_AUTH_TOKEN",
13
+ "OPENAI_API_KEY",
14
+ "DEEPSEEK_API_KEY",
15
+ "GROQ_API_KEY",
16
+ "OPENROUTER_API_KEY",
17
+ ];
18
+ /** Hosts that are not reachable from another machine. */
19
+ const LOOPBACK_HOSTS = ["127.0.0.1", "::1", "localhost"];
20
+ async function probeRunnableProviders() {
21
+ const entry = resolveOmaBin();
22
+ const proc = Bun.spawn([entry, "--list-models"], {
23
+ // Exactly the switches the product backend uses for its children, so the
24
+ // answer matches what a Run would see.
25
+ env: { ...process.env, OMA_WORKSPACE_CATALOG: "0" },
26
+ stdin: "ignore",
27
+ stdout: "pipe",
28
+ stderr: "ignore",
29
+ });
30
+ const text = await new Response(proc.stdout).text();
31
+ const code = await proc.exited;
32
+ if (code !== 0)
33
+ return { providers: [] };
34
+ const parsed = JSON.parse(text);
35
+ if (typeof parsed !== "object" || parsed === null || !("models" in parsed)) {
36
+ return { providers: [] };
37
+ }
38
+ const models = parsed.models;
39
+ if (!Array.isArray(models))
40
+ return { providers: [] };
41
+ const seen = {};
42
+ for (const model of models) {
43
+ if (typeof model !== "object" || model === null || !("id" in model))
44
+ continue;
45
+ const id = model.id;
46
+ if (typeof id !== "string")
47
+ continue;
48
+ const provider = id.split("/")[0];
49
+ if (provider)
50
+ seen[provider] = true;
51
+ }
52
+ return { providers: Object.keys(seen).sort() };
53
+ }
54
+ /** `{status:"ok"}` from :3000/health is our backend; anything else on that port
55
+ * is somebody else's process. */
56
+ async function backendAnswers(url) {
57
+ try {
58
+ const res = await fetch(url, { signal: AbortSignal.timeout(1000) });
59
+ if (!res.ok)
60
+ return false;
61
+ const body = await res.json();
62
+ return typeof body === "object" && body !== null && "status" in body;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ }
68
+ async function anyPortAnswers(url) {
69
+ try {
70
+ await fetch(url, { signal: AbortSignal.timeout(1000) });
71
+ return true;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /** Diagnose why a gateway may not be able to run anything: artifact, tools,
78
+ * catalog source, runnable providers, ports, and the per-run oma binary. */
79
+ export async function diagnoseGateway(opts = {}) {
80
+ const home = opts.home ?? omaHome();
81
+ const paths = gatewayPaths(home);
82
+ const checks = [];
83
+ // 1. artifact
84
+ const version = currentVersion(home) ?? omaVersion();
85
+ const dir = join(paths.versions, version);
86
+ const manifestPath = join(dir, "gateway.json");
87
+ let manifest;
88
+ if (existsSync(manifestPath)) {
89
+ manifest = readGatewayManifest(manifestPath);
90
+ checks.push({
91
+ id: "artifact",
92
+ ok: true,
93
+ detail: `${version} installed (${manifest.components.length} components) at ${dir}`,
94
+ });
95
+ }
96
+ else {
97
+ // A source checkout carries the repo's own version, which was never
98
+ // published: pointing at it would 404, so say what to do instead.
99
+ const fromSource = import.meta.filename.endsWith(".ts");
100
+ checks.push({
101
+ id: "artifact",
102
+ ok: false,
103
+ detail: `no gateway artifact for ${version} at ${dir}`,
104
+ fix: fromSource
105
+ ? "running from a checkout: fetch a released version (oma gateway fetch --version <released>), or run the stack from the repo"
106
+ : `oma gateway fetch --version ${version}`,
107
+ });
108
+ }
109
+ // 2. unpack tools (only needed by fetch)
110
+ const missingTools = ["tar", "zstd"].filter((tool) => !Bun.which(tool));
111
+ checks.push({
112
+ id: "tools",
113
+ ok: missingTools.length === 0,
114
+ detail: missingTools.length === 0
115
+ ? "tar and zstd are on PATH"
116
+ : `missing: ${missingTools.join(", ")}`,
117
+ fix: missingTools.length > 0 ? `install: ${missingTools.join(" ")}` : undefined,
118
+ });
119
+ // 3. catalog source — informational, but it is the usual reason a custom
120
+ // provider is invisible: product children read ONLY $OMA_HOME/models.yml.
121
+ const catalogHome = process.env.OMA_HOME;
122
+ const customCatalog = catalogHome ? join(catalogHome, "models.yml") : undefined;
123
+ if (customCatalog) {
124
+ checks.push({
125
+ id: "catalog",
126
+ ok: true,
127
+ detail: existsSync(customCatalog)
128
+ ? `custom catalog: ${customCatalog}`
129
+ : `OMA_HOME is set but ${customCatalog} does not exist (builtin providers only)`,
130
+ });
131
+ }
132
+ else {
133
+ checks.push({
134
+ id: "catalog",
135
+ ok: true,
136
+ detail: "OMA_HOME is unset: no models.yml is read, so only builtin providers can be used",
137
+ fix: "to use a custom provider: put models.yml in a directory and start the gateway with OMA_HOME pointing at it",
138
+ });
139
+ }
140
+ // 4. provider credentials present in THIS environment (what a gateway
141
+ // started from here would hand to the backend).
142
+ const present = BUILTIN_KEY_ENVS.filter((key) => (process.env[key] ?? "").length > 0);
143
+ checks.push({
144
+ id: "keys",
145
+ ok: present.length > 0,
146
+ detail: present.length > 0
147
+ ? `provider keys in this environment: ${present.join(", ")}`
148
+ : "no builtin provider key in this environment",
149
+ ...(present.length === 0
150
+ ? {
151
+ fix: "add one in the web UI (Settings -> Provider keys: stored server-side, no restart), or export it and restart the gateway",
152
+ }
153
+ : {}),
154
+ });
155
+ // 5. the decisive one: what can actually run, asked the way the backend asks.
156
+ let providers;
157
+ try {
158
+ providers = (await (opts.probeModels ?? probeRunnableProviders)()).providers;
159
+ }
160
+ catch (err) {
161
+ providers = [];
162
+ checks.push({
163
+ id: "models",
164
+ ok: false,
165
+ detail: `could not list models: ${err instanceof Error ? err.message : String(err)}`,
166
+ fix: "check that oma runs: oma --list-models",
167
+ });
168
+ }
169
+ if (providers.length > 0) {
170
+ checks.push({ id: "models", ok: true, detail: `runnable providers: ${providers.join(", ")}` });
171
+ }
172
+ else if (checks.every((c) => c.id !== "models")) {
173
+ checks.push({
174
+ id: "models",
175
+ ok: false,
176
+ detail: "no runnable model: an agent Run would fail here",
177
+ fix: "for a builtin provider add its key in the web UI (Settings -> Provider keys); for a custom provider put models.yml where OMA_HOME points and restart",
178
+ });
179
+ }
180
+ // 6. download channel — only matters when the artifact is missing.
181
+ if (!manifest) {
182
+ const { sums } = releaseLocation(version, process.env);
183
+ const probe = opts.probeRelease ??
184
+ (async (url) => {
185
+ try {
186
+ const res = await fetch(url, { method: "HEAD", signal: AbortSignal.timeout(5000) });
187
+ return res.status;
188
+ }
189
+ catch {
190
+ return undefined;
191
+ }
192
+ });
193
+ const status = await probe(sums);
194
+ if (status === undefined) {
195
+ checks.push({
196
+ id: "download",
197
+ ok: false,
198
+ detail: `cannot reach the release host: ${sums}`,
199
+ fix: "check the network/proxy; a slow link shows up as a stalled download when fetching",
200
+ });
201
+ }
202
+ else {
203
+ // Any HTTP answer means the channel works; a 404 just means this version
204
+ // was never published (routine in a source checkout).
205
+ checks.push({
206
+ id: "download",
207
+ ok: true,
208
+ detail: status === 200
209
+ ? `release reachable: ${sums}`
210
+ : `release host reachable, but ${version} has no release (HTTP ${status})`,
211
+ });
212
+ }
213
+ }
214
+ // 7. secrets: present, parseable, and not readable by anyone else.
215
+ const secretsPath = paths.secrets;
216
+ if (!existsSync(secretsPath)) {
217
+ checks.push({
218
+ id: "secrets",
219
+ ok: true,
220
+ detail: `no secrets yet at ${secretsPath} (generated on first up)`,
221
+ });
222
+ }
223
+ else {
224
+ const mode = statSync(secretsPath).mode & 0o777;
225
+ let parsed;
226
+ let parseError;
227
+ try {
228
+ parsed = readSecrets(home);
229
+ }
230
+ catch (err) {
231
+ parseError = err instanceof Error ? err.message : String(err);
232
+ }
233
+ if (parseError) {
234
+ checks.push({
235
+ id: "secrets",
236
+ ok: false,
237
+ detail: `${secretsPath} is unreadable: ${parseError}`,
238
+ fix: `fix or delete ${secretsPath} (a new one is generated on the next up)`,
239
+ });
240
+ }
241
+ else {
242
+ const keys = Object.keys(parsed ?? {});
243
+ checks.push({
244
+ id: "secrets",
245
+ ok: mode === 0o600,
246
+ detail: `${secretsPath} holds ${keys.join(", ") || "nothing"} (mode ${mode.toString(8)})`,
247
+ fix: mode === 0o600 ? undefined : `chmod 600 ${secretsPath}`,
248
+ });
249
+ }
250
+ }
251
+ // 8. data directory: the backend writes its DB, workspaces and workflows here.
252
+ const dataDir = paths.data;
253
+ if (!existsSync(dataDir)) {
254
+ checks.push({
255
+ id: "data-dir",
256
+ ok: true,
257
+ detail: `${dataDir} does not exist yet (created on first up)`,
258
+ });
259
+ }
260
+ else {
261
+ let writable = true;
262
+ const probe = join(dataDir, ".doctor-probe");
263
+ try {
264
+ await writeFile(probe, "");
265
+ await rm(probe, { force: true });
266
+ }
267
+ catch {
268
+ writable = false;
269
+ }
270
+ let freeMb;
271
+ try {
272
+ const fs = statfsSync(dataDir);
273
+ freeMb = Math.round((Number(fs.bavail) * Number(fs.bsize)) / (1024 * 1024));
274
+ }
275
+ catch {
276
+ freeMb = -1;
277
+ }
278
+ const lowSpace = freeMb >= 0 && freeMb < 500;
279
+ checks.push({
280
+ id: "data-dir",
281
+ ok: writable && !lowSpace,
282
+ detail: writable
283
+ ? `${dataDir} (free ${freeMb >= 0 ? `${freeMb}MB` : "unknown"})`
284
+ : `${dataDir} is not writable`,
285
+ fix: writable && !lowSpace ? undefined : `free space or fix ownership: ${dataDir}`,
286
+ });
287
+ }
288
+ // 9. ports: is our stack answering, or is something else sitting on them?
289
+ const backendUp = await backendAnswers("http://127.0.0.1:3000/health");
290
+ const webAnswers = await anyPortAnswers("http://127.0.0.1:3001/login");
291
+ const daemon = readDaemon(home);
292
+ const detached = daemon && isProcessAlive(daemon.pid) ? `detached, pid ${daemon.pid}` : "not detached";
293
+ if (backendUp) {
294
+ checks.push({
295
+ id: "ports",
296
+ ok: true,
297
+ detail: `${detached}; our backend answers on 3000${webAnswers ? " and 3001 answers too" : ""}`,
298
+ });
299
+ }
300
+ else if (webAnswers) {
301
+ checks.push({
302
+ id: "ports",
303
+ ok: false,
304
+ detail: `something answers on 3001 but 3000 is not our backend (${detached})`,
305
+ fix: "stop whatever holds 3001, or start the gateway with different ports",
306
+ });
307
+ }
308
+ else {
309
+ checks.push({ id: "ports", ok: true, detail: `${detached}; ports quiet` });
310
+ }
311
+ // 10. the last start's log: migration failures are the classic first-boot
312
+ // death and they only live here.
313
+ const logPath = daemonPaths(home).log;
314
+ if (existsSync(logPath)) {
315
+ const tail = readFileSync(logPath, "utf8").split("\n").slice(-200);
316
+ const failure = tail.find((line) => line.includes("_journal") ||
317
+ /\bmigrate\b/i.test(line) ||
318
+ line.includes("SQLITE_") ||
319
+ line.includes("no such table"));
320
+ checks.push({
321
+ id: "last-start",
322
+ ok: failure === undefined,
323
+ detail: failure === undefined
324
+ ? `${logPath}: no migration or SQLite errors in the last start`
325
+ : `${logPath} shows: ${failure.trim().slice(0, 120)}`,
326
+ fix: failure === undefined ? undefined : `read ${logPath} for the full start log`,
327
+ });
328
+ }
329
+ else {
330
+ checks.push({ id: "last-start", ok: true, detail: "no detached start log yet" });
331
+ }
332
+ // 11. exposure: the artifact decides what the components bind.
333
+ if (manifest) {
334
+ const hosts = [];
335
+ for (const component of manifest.components) {
336
+ const host = component.env.HOSTNAME ?? component.env.BACKEND_HOST;
337
+ hosts.push(host ?? "unset(0.0.0.0)");
338
+ }
339
+ const exposed = hosts.filter((host) => !LOOPBACK_HOSTS.includes(host));
340
+ const tokenIsDefault = (() => {
341
+ try {
342
+ const token = readSecrets(home).BACKEND_AUTH_TOKEN;
343
+ return token === undefined || token === "dev-token";
344
+ }
345
+ catch {
346
+ return true;
347
+ }
348
+ })();
349
+ if (exposed.length === 0) {
350
+ checks.push({ id: "exposure", ok: true, detail: "all components bind loopback" });
351
+ }
352
+ else {
353
+ checks.push({
354
+ id: "exposure",
355
+ ok: !tokenIsDefault,
356
+ detail: tokenIsDefault
357
+ ? `components bind non-loopback (${exposed.join(", ")}) while the auth token is missing or the documented default`
358
+ : `components bind non-loopback (${exposed.join(", ")})`,
359
+ fix: tokenIsDefault
360
+ ? "set a random token before exposing this gateway, and see docs/architecture/security"
361
+ : undefined,
362
+ });
363
+ }
364
+ }
365
+ // 12. which password wins: the console's hash or the launcher's secret.
366
+ if (backendUp) {
367
+ try {
368
+ const token = readSecrets(home).BACKEND_AUTH_TOKEN ?? "";
369
+ const res = await fetch("http://127.0.0.1:3000/api/auth/password", {
370
+ headers: { "x-auth-token": token },
371
+ signal: AbortSignal.timeout(2000),
372
+ });
373
+ const body = await res.json();
374
+ const configured = typeof body === "object" && body !== null && "configured" in body
375
+ ? body.configured === true
376
+ : undefined;
377
+ if (configured !== undefined) {
378
+ checks.push({
379
+ id: "password",
380
+ ok: true,
381
+ detail: configured
382
+ ? "a console-set password exists (it wins over the launcher's)"
383
+ : "no console-set password: the launcher's secret is the one that logs in",
384
+ });
385
+ }
386
+ }
387
+ catch {
388
+ // The backend answered /health but not this route (older artifact).
389
+ }
390
+ }
391
+ // 13. the binary the backend spawns per Run
392
+ try {
393
+ const entry = resolveOmaBin();
394
+ checks.push({ id: "oma-bin", ok: true, detail: `runs will spawn ${entry}` });
395
+ }
396
+ catch (err) {
397
+ checks.push({
398
+ id: "oma-bin",
399
+ ok: false,
400
+ detail: err instanceof Error ? err.message : String(err),
401
+ fix: "install oma (npm/bun) or run 'bun run build' in the repo",
402
+ });
403
+ }
404
+ return checks;
405
+ }
@@ -365,6 +365,7 @@ export declare const executeCommandSchema: z.ZodObject<{
365
365
  }>;
366
366
  }, "strip", z.ZodTypeAny, {
367
367
  type: "execute";
368
+ id: string;
368
369
  input: {
369
370
  run: {
370
371
  model: {
@@ -399,9 +400,9 @@ export declare const executeCommandSchema: z.ZodObject<{
399
400
  args?: unknown;
400
401
  } | undefined;
401
402
  };
402
- id: string;
403
403
  }, {
404
404
  type: "execute";
405
+ id: string;
405
406
  input: {
406
407
  run: {
407
408
  model: {
@@ -436,7 +437,6 @@ export declare const executeCommandSchema: z.ZodObject<{
436
437
  args?: unknown;
437
438
  } | undefined;
438
439
  };
439
- id: string;
440
440
  }>;
441
441
  export type ExecuteCommand = z.infer<typeof executeCommandSchema>;
442
442
  export declare const steerCommandSchema: z.ZodObject<{
@@ -458,21 +458,21 @@ export declare const steerCommandSchema: z.ZodObject<{
458
458
  }>;
459
459
  }, "strip", z.ZodTypeAny, {
460
460
  type: "steer";
461
+ id: string;
461
462
  input: {
462
463
  message: Record<string, unknown>;
463
464
  inputId: string;
464
465
  productEntryId?: string | undefined;
465
466
  };
466
- id: string;
467
467
  runId: string;
468
468
  }, {
469
469
  type: "steer";
470
+ id: string;
470
471
  input: {
471
472
  message: Record<string, unknown>;
472
473
  inputId: string;
473
474
  productEntryId?: string | undefined;
474
475
  };
475
- id: string;
476
476
  runId: string;
477
477
  }>;
478
478
  export type SteerCommand = z.infer<typeof steerCommandSchema>;
@@ -498,14 +498,14 @@ export declare const resolveApprovalCommandSchema: z.ZodObject<{
498
498
  decision: z.ZodEnum<["allow", "deny"]>;
499
499
  }, "strip", z.ZodTypeAny, {
500
500
  type: "resolve_approval";
501
- decision: "deny" | "allow";
502
501
  id: string;
502
+ decision: "deny" | "allow";
503
503
  callId: string;
504
504
  runId: string;
505
505
  }, {
506
506
  type: "resolve_approval";
507
- decision: "deny" | "allow";
508
507
  id: string;
508
+ decision: "deny" | "allow";
509
509
  callId: string;
510
510
  runId: string;
511
511
  }>;
@@ -682,6 +682,7 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
682
682
  }>;
683
683
  }, "strip", z.ZodTypeAny, {
684
684
  type: "execute";
685
+ id: string;
685
686
  input: {
686
687
  run: {
687
688
  model: {
@@ -716,9 +717,9 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
716
717
  args?: unknown;
717
718
  } | undefined;
718
719
  };
719
- id: string;
720
720
  }, {
721
721
  type: "execute";
722
+ id: string;
722
723
  input: {
723
724
  run: {
724
725
  model: {
@@ -753,7 +754,6 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
753
754
  args?: unknown;
754
755
  } | undefined;
755
756
  };
756
- id: string;
757
757
  }>, z.ZodObject<{
758
758
  id: z.ZodString;
759
759
  type: z.ZodLiteral<"steer">;
@@ -773,21 +773,21 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
773
773
  }>;
774
774
  }, "strip", z.ZodTypeAny, {
775
775
  type: "steer";
776
+ id: string;
776
777
  input: {
777
778
  message: Record<string, unknown>;
778
779
  inputId: string;
779
780
  productEntryId?: string | undefined;
780
781
  };
781
- id: string;
782
782
  runId: string;
783
783
  }, {
784
784
  type: "steer";
785
+ id: string;
785
786
  input: {
786
787
  message: Record<string, unknown>;
787
788
  inputId: string;
788
789
  productEntryId?: string | undefined;
789
790
  };
790
- id: string;
791
791
  runId: string;
792
792
  }>, z.ZodObject<{
793
793
  id: z.ZodString;
@@ -809,14 +809,14 @@ export declare const codingAgentCommandSchema: z.ZodDiscriminatedUnion<"type", [
809
809
  decision: z.ZodEnum<["allow", "deny"]>;
810
810
  }, "strip", z.ZodTypeAny, {
811
811
  type: "resolve_approval";
812
- decision: "deny" | "allow";
813
812
  id: string;
813
+ decision: "deny" | "allow";
814
814
  callId: string;
815
815
  runId: string;
816
816
  }, {
817
817
  type: "resolve_approval";
818
- decision: "deny" | "allow";
819
818
  id: string;
819
+ decision: "deny" | "allow";
820
820
  callId: string;
821
821
  runId: string;
822
822
  }>]>;
@@ -1358,7 +1358,6 @@ export declare const modelCatalogResponseSchema: z.ZodObject<{
1358
1358
  };
1359
1359
  }>, "many">;
1360
1360
  }, "strip", z.ZodTypeAny, {
1361
- backendKind: "omp" | "pi" | "oma" | "claude_code";
1362
1361
  models: {
1363
1362
  id: string;
1364
1363
  reasoning: boolean;
@@ -1374,8 +1373,8 @@ export declare const modelCatalogResponseSchema: z.ZodObject<{
1374
1373
  cacheWrite: number;
1375
1374
  };
1376
1375
  }[];
1377
- }, {
1378
1376
  backendKind: "omp" | "pi" | "oma" | "claude_code";
1377
+ }, {
1379
1378
  models: {
1380
1379
  id: string;
1381
1380
  reasoning: boolean;
@@ -1391,6 +1390,7 @@ export declare const modelCatalogResponseSchema: z.ZodObject<{
1391
1390
  cacheWrite: number;
1392
1391
  };
1393
1392
  }[];
1393
+ backendKind: "omp" | "pi" | "oma" | "claude_code";
1394
1394
  }>;
1395
1395
  export type ModelCatalogResponse = z.infer<typeof modelCatalogResponseSchema>;
1396
1396
  //# sourceMappingURL=transport.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chengchenccc/oh-my-agent",
3
- "version": "0.2.0-rc.1",
3
+ "version": "0.2.0-rc.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "bin": {
@@ -21,23 +21,23 @@
21
21
  "lint": "biome check . && eslint ."
22
22
  },
23
23
  "dependencies": {
24
- "@chengchenccc/adapter-mcp": "^0.2.0-rc.1",
25
- "@chengchenccc/agent-contract": "^0.2.0-rc.1",
26
- "@chengchenccc/ai": "^0.2.0-rc.1",
27
- "@chengchenccc/config": "^0.2.0-rc.1",
28
- "@chengchenccc/message": "^0.2.0-rc.1",
29
- "@chengchenccc/sandbox": "^0.2.0-rc.1",
30
- "@chengchenccc/source-fetch": "^0.2.0-rc.1",
31
- "@chengchenccc/tui": "^0.2.0-rc.1",
24
+ "@chengchenccc/adapter-mcp": "^0.2.0-rc.2",
25
+ "@chengchenccc/agent-contract": "^0.2.0-rc.2",
26
+ "@chengchenccc/ai": "^0.2.0-rc.2",
27
+ "@chengchenccc/config": "^0.2.0-rc.2",
28
+ "@chengchenccc/message": "^0.2.0-rc.2",
29
+ "@chengchenccc/sandbox": "^0.2.0-rc.2",
30
+ "@chengchenccc/source-fetch": "^0.2.0-rc.2",
31
+ "@chengchenccc/tui": "^0.2.0-rc.2",
32
32
  "@modelcontextprotocol/sdk": "^1.12.1",
33
33
  "puppeteer-core": "25.10.0",
34
34
  "zod": "^3.23.0"
35
35
  },
36
36
  "devDependencies": {
37
- "@chengchenccc/test-helpers": "^0.2.0-rc.1",
37
+ "@chengchenccc/test-helpers": "^0.2.0-rc.2",
38
38
  "bun-types": "latest",
39
39
  "typescript": "^5.7.0",
40
- "@chengchenccc/adapter-oma-agent": "^0.2.0-rc.1"
40
+ "@chengchenccc/adapter-oma-agent": "^0.2.0-rc.2"
41
41
  },
42
42
  "files": [
43
43
  "dist",