@oharness/engine 0.8.0 → 0.9.1

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.
@@ -0,0 +1,228 @@
1
+ import { createHash } from "node:crypto";
2
+ import { execFile } from "node:child_process";
3
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
4
+ import { mkdir, mkdtemp, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
5
+ import path from "node:path";
6
+ import { promisify } from "node:util";
7
+ /** Plain releases only: a pre-release is not something to hand a user unasked. */
8
+ function parse(version) {
9
+ const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(version);
10
+ return match ? [Number(match[1]), Number(match[2]), Number(match[3])] : undefined;
11
+ }
12
+ export function compareVersions(a, b) {
13
+ const left = parse(a) ?? [0, 0, 0];
14
+ const right = parse(b) ?? [0, 0, 0];
15
+ for (let i = 0; i < 3; i++)
16
+ if (left[i] !== right[i])
17
+ return left[i] - right[i];
18
+ return 0;
19
+ }
20
+ function sameMajor(a, b) {
21
+ const left = parse(a);
22
+ const right = parse(b);
23
+ return !!left && !!right && left[0] === right[0];
24
+ }
25
+ /** The version a package directory says it is, or nothing if it cannot say. */
26
+ export function packageVersion(dir) {
27
+ try {
28
+ const version = JSON.parse(readFileSync(path.join(dir, "package.json"), "utf8")).version;
29
+ return typeof version === "string" ? version : undefined;
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ }
35
+ /**
36
+ * The downloaded copies that are whole.
37
+ *
38
+ * A directory counts only when its entry exists and its manifest names the
39
+ * version the directory is called: anything else is a download that was
40
+ * interrupted or tampered with, and running it would fail in a way that looks
41
+ * like the gateway's fault.
42
+ */
43
+ export function cachedCopies(root) {
44
+ let names;
45
+ try {
46
+ names = readdirSync(root);
47
+ }
48
+ catch {
49
+ return [];
50
+ }
51
+ const copies = [];
52
+ for (const version of names) {
53
+ if (!parse(version))
54
+ continue;
55
+ const dir = path.join(root, version, "package");
56
+ const entry = path.join(dir, "mcp", "server.mjs");
57
+ if (existsSync(entry) && packageVersion(dir) === version)
58
+ copies.push({ version, entry });
59
+ }
60
+ return copies.sort((a, b) => compareVersions(b.version, a.version));
61
+ }
62
+ /**
63
+ * Which copy to run: the newest cached one the bundled copy is compatible
64
+ * with, or the bundled copy itself.
65
+ *
66
+ * Without a bundled copy there is nothing to measure compatibility against,
67
+ * so nothing is run — the same outcome as an install that dropped the package.
68
+ */
69
+ export function pickCopy(bundled, cached) {
70
+ if (!bundled)
71
+ return undefined;
72
+ let best = bundled;
73
+ for (const copy of cached) {
74
+ if (sameMajor(copy.version, bundled.version) && compareVersions(copy.version, best.version) > 0)
75
+ best = copy;
76
+ }
77
+ return best;
78
+ }
79
+ /**
80
+ * Whether to look for updates at all.
81
+ *
82
+ * A CI run or a test is not a user, and a network call from one is either
83
+ * noise or a flaky test — so both are off unless asked for by name.
84
+ */
85
+ export function autoUpdateEnabled(env = process.env) {
86
+ const flag = env["OHARNESS_MEDIA_AUTO_UPDATE"];
87
+ if (flag === "0" || flag === "false")
88
+ return false;
89
+ if (flag === "1" || flag === "true")
90
+ return true;
91
+ return !env["CI"] && !env["NODE_TEST_CONTEXT"];
92
+ }
93
+ /** Enough time to ask; the registry answering slowly is no reason to wait on it. */
94
+ const REGISTRY_TIMEOUT_MS = 5_000;
95
+ /** The package is about a hundred kilobytes; a minute is generous for it. */
96
+ const DOWNLOAD_TIMEOUT_MS = 60_000;
97
+ /** The running version plus the one before it, in case the newest is bad. */
98
+ const KEEP = 2;
99
+ const run = promisify(execFile);
100
+ /**
101
+ * `tar`, as the system ships it.
102
+ *
103
+ * macOS and Linux have always had it, and Windows has shipped bsdtar as
104
+ * `tar.exe` since 10 — which is everything the desktop app supports. A JS
105
+ * implementation would be a dependency for a job the OS already does.
106
+ */
107
+ async function systemTar(archive, into) {
108
+ await run("tar", ["-xzf", archive, "-C", into]);
109
+ }
110
+ /**
111
+ * Whether these bytes are the ones the registry vouched for.
112
+ *
113
+ * Only sha512 is accepted. The registry has published it for every package
114
+ * for years, and an entry offering only something weaker is not one to run as
115
+ * a subprocess holding the user's key.
116
+ */
117
+ export function matchesIntegrity(bytes, integrity) {
118
+ const digest = createHash("sha512").update(bytes).digest("base64");
119
+ const claimed = integrity
120
+ .split(/\s+/)
121
+ .filter((part) => part.startsWith("sha512-"))
122
+ .map((part) => part.slice("sha512-".length).split("?")[0]);
123
+ return claimed.length > 0 && claimed.includes(digest);
124
+ }
125
+ function timed(signal, ms) {
126
+ return signal ? AbortSignal.any([signal, AbortSignal.timeout(ms)]) : AbortSignal.timeout(ms);
127
+ }
128
+ /**
129
+ * Fetch the newest compatible release into the cache, if there is one.
130
+ *
131
+ * Resolves rather than throws in every case: the caller fires this and moves
132
+ * on, and a failure here must never be noticed as anything but the bundled
133
+ * server continuing to run.
134
+ */
135
+ export async function updateOttoport(options) {
136
+ if (!options.force && !autoUpdateEnabled(options.env))
137
+ return { status: "disabled" };
138
+ const doFetch = options.fetch ?? fetch;
139
+ const registry = options.registry ?? "https://registry.npmjs.org";
140
+ let staging;
141
+ try {
142
+ const response = await doFetch(`${registry}/ottoport/latest`, {
143
+ signal: timed(options.signal, REGISTRY_TIMEOUT_MS),
144
+ headers: { accept: "application/json" },
145
+ });
146
+ if (!response.ok)
147
+ throw new Error(`registry answered HTTP ${response.status}`);
148
+ const latest = (await response.json());
149
+ const version = typeof latest.version === "string" ? latest.version : "";
150
+ // Also what makes the version safe to use as a directory name.
151
+ if (!parse(version))
152
+ throw new Error(`registry offered an unusable version: ${String(latest.version)}`);
153
+ if (compareVersions(version, options.current) <= 0)
154
+ return { status: "current", version };
155
+ if (!sameMajor(version, options.current))
156
+ return { status: "incompatible", version };
157
+ if (cachedCopies(options.root).some((copy) => copy.version === version))
158
+ return { status: "cached", version };
159
+ const tarball = latest.dist?.tarball;
160
+ const integrity = latest.dist?.integrity;
161
+ if (typeof tarball !== "string" || !/^https:\/\//.test(tarball))
162
+ throw new Error("registry entry has no https tarball");
163
+ if (typeof integrity !== "string")
164
+ throw new Error("registry entry has no integrity");
165
+ const download = await doFetch(tarball, { signal: timed(options.signal, DOWNLOAD_TIMEOUT_MS) });
166
+ if (!download.ok)
167
+ throw new Error(`tarball download answered HTTP ${download.status}`);
168
+ const bytes = Buffer.from(await download.arrayBuffer());
169
+ // Before a single byte is unpacked: a tarball that is not the one the
170
+ // registry published is not opened at all.
171
+ if (!matchesIntegrity(bytes, integrity))
172
+ throw new Error(`integrity mismatch for ottoport ${version}`);
173
+ await mkdir(options.root, { recursive: true });
174
+ // Inside the cache root, so the final rename never crosses a filesystem
175
+ // and a directory named for a version is only ever a complete one.
176
+ staging = await mkdtemp(path.join(options.root, ".staging-"));
177
+ const archive = path.join(staging, "package.tgz");
178
+ await writeFile(archive, bytes);
179
+ await (options.extract ?? systemTar)(archive, staging);
180
+ await rm(archive, { force: true });
181
+ const dir = path.join(staging, "package");
182
+ if (!existsSync(path.join(dir, "mcp", "server.mjs")))
183
+ throw new Error("the package has no mcp/server.mjs");
184
+ if (packageVersion(dir) !== version)
185
+ throw new Error("the package is not the version the registry named");
186
+ const target = path.join(options.root, version);
187
+ try {
188
+ await rename(staging, target);
189
+ staging = undefined;
190
+ }
191
+ catch (err) {
192
+ // Another session got there first, which is the same outcome.
193
+ if (cachedCopies(options.root).some((copy) => copy.version === version))
194
+ return { status: "cached", version };
195
+ throw err;
196
+ }
197
+ await prune(options.root);
198
+ return { status: "installed", version };
199
+ }
200
+ catch (err) {
201
+ return { status: "failed", error: err instanceof Error ? err.message : String(err) };
202
+ }
203
+ finally {
204
+ if (staging)
205
+ await rm(staging, { recursive: true, force: true }).catch(() => { });
206
+ }
207
+ }
208
+ /**
209
+ * Drop all but the newest few versions, and staging left by a crash.
210
+ *
211
+ * Staging is only cleared once it is an hour old, since a younger one may be
212
+ * another session's download still in progress.
213
+ */
214
+ async function prune(root) {
215
+ const keep = new Set(cachedCopies(root).slice(0, KEEP).map((copy) => copy.version));
216
+ for (const name of await readdir(root).catch(() => [])) {
217
+ const full = path.join(root, name);
218
+ if (name.startsWith(".staging-")) {
219
+ const age = Date.now() - (await stat(full).then((s) => s.mtimeMs).catch(() => Date.now()));
220
+ if (age > 3_600_000)
221
+ await rm(full, { recursive: true, force: true }).catch(() => { });
222
+ continue;
223
+ }
224
+ if (parse(name) && !keep.has(name))
225
+ await rm(full, { recursive: true, force: true }).catch(() => { });
226
+ }
227
+ }
228
+ //# sourceMappingURL=ottoport-updates.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ottoport-updates.js","sourceRoot":"","sources":["../../src/mcp/ottoport-updates.ts"],"names":[],"mappings":"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"}
@@ -1,33 +1,104 @@
1
+ import type { MediaAccess } from "../../mcp/builtin.ts";
2
+ import type { AuthManager } from "../../auth/manager.ts";
1
3
  import type { McpToolDefinition } from "../../mcp/protocol.ts";
2
4
  import type { Tool, ToolContext } from "../../tools/types.ts";
3
5
  import type { RuntimePlugin } from "../types.ts";
4
6
  export interface StudioPluginOptions {
5
- /** The gateway credential, from the same sources completions resolve. */
6
- secret(): Promise<string | undefined>;
7
+ /** Who calls are made as, and where they go. Asked before every call. */
8
+ access(): Promise<MediaAccess | undefined>;
7
9
  }
10
+ /**
11
+ * The credential media calls go out on: the OHarness account, and nothing else.
12
+ *
13
+ * Media is billed only through our gateway, which charges the account (or the
14
+ * team it bills to) in OHarness credits the way it charges chat. A vendor key
15
+ * the user happens to hold for chat is deliberately not used here: two ways to
16
+ * pay for the same image would be two balances to explain, and a generation
17
+ * the user expected on their plan would land on a different bill.
18
+ *
19
+ * The team rides in the path because the server offers nowhere else: all a
20
+ * caller sets is a base URL and a key.
21
+ */
22
+ export declare function mediaAccess(auth: AuthManager): Promise<MediaAccess | undefined>;
8
23
  export declare function createStudioPlugin(options: StudioPluginOptions): RuntimePlugin;
24
+ /** What the model is told when media exists but the user is not signed in. */
25
+ export declare const SIGN_IN_PROMPT: string;
26
+ /** One upstream tool: what it is called here, and what it produces. */
27
+ export interface FacadeEntry {
28
+ upstream: string;
29
+ name: string;
30
+ /**
31
+ * What generated files are named after; empty for a tool that only reads,
32
+ * which is also what makes it skip confirmation and download nothing.
33
+ */
34
+ kind: string;
35
+ note: string;
36
+ }
9
37
  /**
10
- * What each upstream tool is called here, and what it produces.
38
+ * The tools this file knows by name, and what to say about each.
11
39
  *
12
- * The rename is the whole of the mapping. Parameters are not restated: the
13
- * schema comes from the server, so a model can send anything that server
40
+ * The rename and the note are the whole of it. Parameters are not restated:
41
+ * the schema comes from the server, so a model can send anything that server
14
42
  * accepts and a parameter added upstream works the day it ships. Writing them
15
43
  * out here would be a second, staler copy of somebody else's list — and the
16
44
  * failure mode is silent, since a parameter this file has not heard of would
17
45
  * simply never reach the gateway.
46
+ *
47
+ * A tool missing from here is still offered; see `facadeEntry`.
18
48
  */
19
- export declare const FACADE: Array<{
20
- upstream: string;
21
- name: string;
22
- kind: string;
23
- note: string;
24
- }>;
49
+ export declare const FACADE: FacadeEntry[];
50
+ /**
51
+ * The name a tool is offered under: the known one, or one derived from the
52
+ * upstream name with the vendor taken off — `ottoport_remove_background`
53
+ * becomes `RemoveBackground`.
54
+ */
55
+ export declare function facadeName(upstream: string): string;
56
+ /**
57
+ * How to present a tool, whether or not this file has heard of it.
58
+ *
59
+ * An unknown tool is assumed to spend money unless its name says it only
60
+ * looks something up. Guessing the other way would let a new paid tool skip
61
+ * confirmation, and the cost of that guess is a charge the user never agreed
62
+ * to; guessing this way costs one extra click on a lookup.
63
+ */
64
+ export declare function facadeEntry(upstream: string): FacadeEntry;
65
+ /**
66
+ * A server-written description, in this harness's words.
67
+ *
68
+ * The vendor's name is not part of the product, and the server's text is full
69
+ * of it — "through an OttoPort image model", "call ottoport_list_models".
70
+ * Left alone, the second is worse than cosmetic: it names a tool the model
71
+ * does not have, under the name it is offered as here. So tool references
72
+ * become the names they are offered under, the vendor's name is dropped, and
73
+ * a sentence about something withheld or refused is dropped with it.
74
+ */
75
+ export declare function rebrand(text: string): string;
76
+ /**
77
+ * The server's input schema, as this harness offers it: descriptions in its
78
+ * words at every depth, and without the parameters the gateway refuses.
79
+ */
80
+ export declare function facadeSchema(inputSchema: unknown): {
81
+ properties?: Record<string, unknown>;
82
+ [k: string]: unknown;
83
+ };
25
84
  /**
26
85
  * One upstream tool, wearing this harness's name and manners.
27
86
  *
28
- * `inputSchema` is the server's own, with `path` added. Nothing validates it
29
- * here: the server validates its own inputs, and a second opinion derived from
30
- * its JSON Schema would only be a less accurate one — the same reasoning the
31
- * MCP tool adapter states.
87
+ * `inputSchema` is the server's own, rebranded, with `path` added. Nothing
88
+ * validates it here: the server validates its own inputs, and a second
89
+ * opinion derived from its JSON Schema would only be a less accurate one —
90
+ * the same reasoning the MCP tool adapter states.
91
+ */
92
+ export declare function facadeTool(entry: FacadeEntry, def: McpToolDefinition, call: (tool: string, args: Record<string, unknown>, ctx: ToolContext) => Promise<string>): Tool;
93
+ /**
94
+ * Where one output goes: into a folder, or under a name.
95
+ *
96
+ * A model asked for "a kitten picture" passes `media/kitten.png` as often as
97
+ * `media/` — and treating every path as a folder saved that one as
98
+ * `media/kitten.png/image-….jpg`, a folder named like an image with the image
99
+ * inside it. A path whose last part has an extension is a file name. The
100
+ * extension kept is the one the bytes have, because renaming a JPEG to .png
101
+ * makes a file that some viewers refuse; the name asked for is kept. A second
102
+ * output of the same call is numbered beside the first.
32
103
  */
33
- export declare function facadeTool(entry: (typeof FACADE)[number], def: McpToolDefinition, call: (tool: string, args: Record<string, unknown>, ctx: ToolContext) => Promise<string>): Tool;
104
+ export declare function destination(into: string | undefined, kind: string, index: number, extension: string): string;