@nyte-ai/host 0.0.0-stage → 0.0.19
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/LICENSE +21 -0
- package/README.md +6 -2
- package/THIRD-PARTY-NOTICES.md +866 -0
- package/dist/chunks/canvas-preview-0cmbkp2j.js +65 -0
- package/dist/chunks/canvas-preview-0nq75h9k.js +7 -0
- package/dist/chunks/canvas-preview-11nadadc.js +241 -0
- package/dist/chunks/canvas-preview-1sq7z9jq.js +17 -0
- package/dist/chunks/canvas-preview-2jwbe14k.js +130 -0
- package/dist/chunks/canvas-preview-2zdyh9ny.js +26 -0
- package/dist/chunks/canvas-preview-31w7xm8z.js +34 -0
- package/dist/chunks/canvas-preview-4b6sayhg.js +295 -0
- package/dist/chunks/canvas-preview-4e0pxa32.js +63 -0
- package/dist/chunks/canvas-preview-518dh8by.js +169 -0
- package/dist/chunks/canvas-preview-55s0y68g.js +387 -0
- package/dist/chunks/canvas-preview-5ekjx6sw.js +613 -0
- package/dist/chunks/canvas-preview-5mex725c.js +14 -0
- package/dist/chunks/canvas-preview-5z63am63.js +295 -0
- package/dist/chunks/canvas-preview-65xqjw7f.js +20 -0
- package/dist/chunks/canvas-preview-69fkk9yn.js +95 -0
- package/dist/chunks/canvas-preview-8xc0vn7v.js +458 -0
- package/dist/chunks/canvas-preview-9aqczacs.js +73 -0
- package/dist/chunks/canvas-preview-af86jz8t.js +141 -0
- package/dist/chunks/canvas-preview-aq5j4p25.js +240 -0
- package/dist/chunks/canvas-preview-b2v8wfnz.js +36 -0
- package/dist/chunks/canvas-preview-befkjea2.js +60 -0
- package/dist/chunks/canvas-preview-bxnqt8cd.js +103 -0
- package/dist/chunks/canvas-preview-cawy6vbz.js +352 -0
- package/dist/chunks/canvas-preview-crmzxxt8.js +518 -0
- package/dist/chunks/canvas-preview-drctegz4.js +591 -0
- package/dist/chunks/canvas-preview-e3vnkkc5.js +134 -0
- package/dist/chunks/canvas-preview-ec2mt7es.js +108 -0
- package/dist/chunks/canvas-preview-fxa0rfb0.js +61 -0
- package/dist/chunks/canvas-preview-g5fkjs34.js +598 -0
- package/dist/chunks/canvas-preview-jeddjex8.js +166 -0
- package/dist/chunks/canvas-preview-k1yg4fgp.js +571 -0
- package/dist/chunks/canvas-preview-kdqgrstc.js +194 -0
- package/dist/chunks/canvas-preview-kn7kvbfj.js +12 -0
- package/dist/chunks/canvas-preview-kp75gb63.js +141 -0
- package/dist/chunks/canvas-preview-mgcx2b2p.js +271 -0
- package/dist/chunks/canvas-preview-n2a35zv5.js +81 -0
- package/dist/chunks/canvas-preview-pkzqg96v.js +63 -0
- package/dist/chunks/canvas-preview-rp4x6y1h.js +144 -0
- package/dist/chunks/canvas-preview-s94ebwf5.js +112 -0
- package/dist/chunks/canvas-preview-skzwvw4d.js +23 -0
- package/dist/chunks/canvas-preview-srgfpdmf.js +60 -0
- package/dist/chunks/canvas-preview-tjhabynd.js +252 -0
- package/dist/chunks/canvas-preview-tnqe7k0b.js +374 -0
- package/dist/chunks/canvas-preview-vaatfznm.js +189 -0
- package/dist/chunks/canvas-preview-vm9zdp2t.js +398 -0
- package/dist/chunks/canvas-preview-w54ymtb3.js +272 -0
- package/dist/chunks/canvas-preview-wg80rbc2.js +46 -0
- package/dist/chunks/canvas-preview-y5y6ghaw.js +125 -0
- package/dist/chunks/canvas-preview-ycepb05s.js +176 -0
- package/dist/chunks/canvas-preview-yjz0f7n4.js +50 -0
- package/dist/chunks/canvas-preview-zaaj99wd.js +244 -0
- package/dist/src/canvas-preview.d.ts +29 -0
- package/dist/src/canvas-preview.js +271 -0
- package/dist/src/credentials.d.ts +44 -0
- package/dist/src/credentials.js +10 -0
- package/dist/src/deploy/cloudflare/worker.d.ts +50 -0
- package/dist/src/deploy/cloudflare/worker.js +196 -0
- package/dist/src/deploy/vercel/nitro.config.d.ts +2 -0
- package/dist/src/deploy/vercel/nitro.config.js +30 -0
- package/dist/src/deploy/vercel/server/routes/[...].d.ts +2 -0
- package/dist/src/deploy/vercel/server/routes/[...].js +18 -0
- package/dist/src/deploy/vercel/server/utils/host.d.ts +13 -0
- package/dist/src/deploy/vercel/server/utils/host.js +7 -0
- package/dist/src/deploy/vercel/server/utils/repair.d.ts +1 -0
- package/dist/src/deploy/vercel/server/utils/repair.js +6 -0
- package/dist/src/deploy/vercel/server/utils/step.d.ts +10 -0
- package/dist/src/deploy/vercel/server/utils/step.js +6 -0
- package/dist/src/deploy/vercel/server/workflows/drive.d.ts +14 -0
- package/dist/src/deploy/vercel/server/workflows/drive.js +13 -0
- package/dist/src/durable-object.d.ts +14 -0
- package/dist/src/durable-object.js +231 -0
- package/dist/src/hash.d.ts +1 -0
- package/dist/src/hash.js +6 -0
- package/dist/src/identity-key.d.ts +11 -0
- package/dist/src/identity-key.js +9 -0
- package/dist/src/identity.d.ts +12 -0
- package/dist/src/identity.js +6 -0
- package/dist/src/images/exif.d.ts +1 -0
- package/dist/src/images/exif.js +6 -0
- package/dist/src/images/node.d.ts +37 -0
- package/dist/src/images/node.js +12 -0
- package/dist/src/images/photon.d.ts +3 -0
- package/dist/src/images/photon.js +7 -0
- package/dist/src/images/worker.d.ts +1 -0
- package/dist/src/images/worker.js +23 -0
- package/dist/src/index.d.ts +20 -0
- package/dist/src/index.js +121 -0
- package/dist/src/key-file.d.ts +1 -0
- package/dist/src/key-file.js +7 -0
- package/dist/src/listen-fetch.d.ts +8 -0
- package/dist/src/listen-fetch.js +9 -0
- package/dist/src/listen.d.ts +4 -0
- package/dist/src/listen.js +10 -0
- package/dist/src/local/command.d.ts +20 -0
- package/dist/src/local/command.js +12 -0
- package/dist/src/local/feed.d.ts +7 -0
- package/dist/src/local/feed.js +6 -0
- package/dist/src/local/files.d.ts +18 -0
- package/dist/src/local/files.js +13 -0
- package/dist/src/local/git-watch.d.ts +15 -0
- package/dist/src/local/git-watch.js +7 -0
- package/dist/src/local/git.d.ts +5 -0
- package/dist/src/local/git.js +13 -0
- package/dist/src/local/github.d.ts +30 -0
- package/dist/src/local/github.js +9 -0
- package/dist/src/local/identity.d.ts +3 -0
- package/dist/src/local/identity.js +7 -0
- package/dist/src/local/jobs.d.ts +7 -0
- package/dist/src/local/jobs.js +7 -0
- package/dist/src/local/legacy-trust.d.ts +10 -0
- package/dist/src/local/legacy-trust.js +6 -0
- package/dist/src/local/patch.d.ts +16 -0
- package/dist/src/local/patch.js +17 -0
- package/dist/src/local/provider.d.ts +11 -0
- package/dist/src/local/provider.js +20 -0
- package/dist/src/local/snapshots.d.ts +7 -0
- package/dist/src/local/snapshots.js +11 -0
- package/dist/src/local/text.d.ts +1 -0
- package/dist/src/local/text.js +6 -0
- package/dist/src/local/tree.d.ts +50 -0
- package/dist/src/local/tree.js +16 -0
- package/dist/src/local/workspace-migration.d.ts +2 -0
- package/dist/src/local/workspace-migration.js +7 -0
- package/dist/src/local/worktrees.d.ts +46 -0
- package/dist/src/local/worktrees.js +14 -0
- package/dist/src/mcp/config.d.ts +75 -0
- package/dist/src/mcp/config.js +12 -0
- package/dist/src/mcp/connect.d.ts +15 -0
- package/dist/src/mcp/connect.js +9 -0
- package/dist/src/mcp/hub.d.ts +19 -0
- package/dist/src/mcp/hub.js +11 -0
- package/dist/src/mcp/oauth.d.ts +46 -0
- package/dist/src/mcp/oauth.js +12 -0
- package/dist/src/mcp/tools.d.ts +29 -0
- package/dist/src/mcp/tools.js +12 -0
- package/dist/src/open.d.ts +14 -0
- package/dist/src/open.js +46 -0
- package/dist/src/plugins/loader.d.ts +21 -0
- package/dist/src/plugins/loader.js +11 -0
- package/dist/src/postgres.d.ts +19 -0
- package/dist/src/postgres.js +8 -0
- package/dist/src/providers.d.ts +16 -0
- package/dist/src/providers.js +112 -0
- package/dist/src/secrets.d.ts +12 -0
- package/dist/src/secrets.js +8 -0
- package/dist/src/serve/binding.d.ts +39 -0
- package/dist/src/serve/binding.js +6 -0
- package/dist/src/serve/handler.d.ts +15 -0
- package/dist/src/serve/handler.js +8 -0
- package/dist/src/serve/stream-server.d.ts +6 -0
- package/dist/src/serve/stream-server.js +6 -0
- package/dist/src/session-index.d.ts +11 -0
- package/dist/src/session-index.js +11 -0
- package/dist/src/shell-env.d.ts +14 -0
- package/dist/src/shell-env.js +108 -0
- package/dist/src/sqlite-workers.d.ts +6 -0
- package/dist/src/sqlite-workers.js +6 -0
- package/dist/src/sqlite.d.ts +8 -0
- package/dist/src/sqlite.js +11 -0
- package/dist/src/store-rows.d.ts +13 -0
- package/dist/src/store-rows.js +26 -0
- package/dist/src/store-usage.d.ts +25 -0
- package/dist/src/store-usage.js +19 -0
- package/dist/src/usage-history.d.ts +54 -0
- package/dist/src/usage-history.js +14 -0
- package/dist/src/usage-scan.d.ts +24 -0
- package/dist/src/usage-scan.js +65 -0
- package/dist/src/usage-service.d.ts +23 -0
- package/dist/src/usage-service.js +16 -0
- package/dist/src/usage-summary.d.ts +9 -0
- package/dist/src/usage-summary.js +16 -0
- package/dist/src/usage-types.d.ts +86 -0
- package/dist/src/usage-types.js +0 -0
- package/dist/src/usage-worker.d.ts +1 -0
- package/dist/src/usage-worker.js +29 -0
- package/dist/src/usage.d.ts +195 -0
- package/dist/src/usage.js +23 -0
- package/package.json +140 -3
- package/src/canvas-preview.ts +403 -0
- package/src/credentials.ts +294 -0
- package/src/deploy/cloudflare/README.md +3 -0
- package/src/deploy/cloudflare/worker.ts +306 -0
- package/src/deploy/cloudflare/wrangler.jsonc +12 -0
- package/src/deploy/vercel/README.md +3 -0
- package/src/deploy/vercel/nitro.config.ts +29 -0
- package/src/deploy/vercel/server/routes/[...].ts +10 -0
- package/src/deploy/vercel/server/utils/host.ts +104 -0
- package/src/deploy/vercel/server/utils/repair.ts +35 -0
- package/src/deploy/vercel/server/utils/step.ts +25 -0
- package/src/deploy/vercel/server/workflows/drive.ts +46 -0
- package/src/deploy/vercel/tsconfig.json +9 -0
- package/src/deploy/vercel/types/bun.d.ts +2 -0
- package/src/durable-object.ts +386 -0
- package/src/hash.ts +5 -0
- package/src/identity-key.ts +20 -0
- package/src/identity.ts +66 -0
- package/src/images/exif.ts +69 -0
- package/src/images/node.ts +163 -0
- package/src/images/photon.ts +141 -0
- package/src/images/worker.ts +29 -0
- package/src/index.ts +39 -0
- package/src/key-file.ts +53 -0
- package/src/listen-fetch.ts +409 -0
- package/src/listen.ts +228 -0
- package/src/local/command.ts +161 -0
- package/src/local/feed.ts +70 -0
- package/src/local/files.ts +817 -0
- package/src/local/git-watch.ts +199 -0
- package/src/local/git.ts +919 -0
- package/src/local/github.ts +691 -0
- package/src/local/identity.ts +21 -0
- package/src/local/jobs.ts +573 -0
- package/src/local/legacy-trust.ts +105 -0
- package/src/local/patch.ts +334 -0
- package/src/local/provider.ts +184 -0
- package/src/local/snapshots.ts +778 -0
- package/src/local/text.ts +9 -0
- package/src/local/tree.ts +398 -0
- package/src/local/workspace-migration.ts +95 -0
- package/src/local/worktrees.ts +239 -0
- package/src/mcp/config.ts +330 -0
- package/src/mcp/connect.ts +92 -0
- package/src/mcp/hub.ts +836 -0
- package/src/mcp/oauth.ts +436 -0
- package/src/mcp/tools.ts +333 -0
- package/src/open.ts +449 -0
- package/src/plugins/loader.ts +186 -0
- package/src/postgres.ts +492 -0
- package/src/providers.ts +135 -0
- package/src/secrets.ts +31 -0
- package/src/serve/binding.ts +629 -0
- package/src/serve/handler.ts +362 -0
- package/src/serve/stream-server.ts +176 -0
- package/src/session-index.ts +99 -0
- package/src/shell-env.ts +153 -0
- package/src/sqlite-workers.ts +283 -0
- package/src/sqlite.ts +553 -0
- package/src/store-rows.ts +110 -0
- package/src/store-usage.ts +347 -0
- package/src/usage-history.ts +219 -0
- package/src/usage-scan.ts +81 -0
- package/src/usage-service.ts +82 -0
- package/src/usage-summary.ts +79 -0
- package/src/usage-types.ts +87 -0
- package/src/usage-worker.ts +24 -0
- package/src/usage.ts +776 -0
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { asId, SdkError } from "@nyte-ai/schema";
|
|
2
|
+
import type { AdvanceReport, HeadName, Owned, SessionId } from "@nyte-ai/schema";
|
|
3
|
+
import type { Models } from "@nyte-ai/ai";
|
|
4
|
+
import { createModels } from "@nyte-ai/ai/models";
|
|
5
|
+
import { composeHost } from "@nyte-ai/core/server";
|
|
6
|
+
import type { Host, Listener, OpenOptions } from "@nyte-ai/core";
|
|
7
|
+
import type { ServedHost, ServerHost } from "@nyte-ai/core/server";
|
|
8
|
+
import { encodeError, errorStatus } from "@nyte-ai/protocol";
|
|
9
|
+
import { Settings } from "typebox/system";
|
|
10
|
+
import { credentialStore } from "@nyte-ai/host/credentials";
|
|
11
|
+
import { durableObjectStore } from "@nyte-ai/host/durable-object";
|
|
12
|
+
import type { DurableObjectSql, SqlValue } from "@nyte-ai/host/durable-object";
|
|
13
|
+
import { seedSigner } from "@nyte-ai/host/identity";
|
|
14
|
+
import { listenFetch } from "@nyte-ai/host/listen-fetch";
|
|
15
|
+
import { chatOnlyProvider } from "@nyte-ai/host/providers";
|
|
16
|
+
import manifest from "../../../package.json" with { type: "json" };
|
|
17
|
+
|
|
18
|
+
Settings.Set({ useAcceleration: false });
|
|
19
|
+
|
|
20
|
+
/** Returns the parts of a Durable Object's `state` `NyteHost` uses, structurally: the storage `durableObjectStore` reads with the object's one alarm, and `blockConcurrencyWhile` to hold requests until the host is composed. */
|
|
21
|
+
export type WorkerState = {
|
|
22
|
+
readonly storage: DurableObjectSql & {
|
|
23
|
+
getAlarm(): Promise<number | null>;
|
|
24
|
+
setAlarm(scheduledTime: number): Promise<void>;
|
|
25
|
+
deleteAlarm(): Promise<void>;
|
|
26
|
+
};
|
|
27
|
+
blockConcurrencyWhile<T>(callback: () => Promise<T>): Promise<T>;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/** Returns the Worker's bindings: the `NyteHost` namespace `wrangler.jsonc` binds, the listener token it requires as a secret (`secrets.required`, set with `wrangler secret put NYTE_TOKEN`, at least 16 characters; §42l), the optional base64 secrets key (decision 10; absent, see `secretsFrom`), and the optional base64 identity seed (§50a; absent, the host answers no identity challenge). */
|
|
31
|
+
export type WorkerEnv = {
|
|
32
|
+
readonly NYTE_HOST: { getByName(name: string): { fetch(request: Request): Promise<Response> } };
|
|
33
|
+
readonly NYTE_TOKEN: string;
|
|
34
|
+
readonly NYTE_SECRET_KEY?: string;
|
|
35
|
+
readonly NYTE_IDENTITY_KEY?: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type PassBudget = {
|
|
39
|
+
readonly heads: number;
|
|
40
|
+
readonly ms: number;
|
|
41
|
+
readonly repair: boolean;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export type PassHost = Pick<Host, "execution"> & {
|
|
45
|
+
readonly server: Pick<ServerHost, "tick">;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const PASS_BUDGET = { heads: 32, ms: 10_000 } as const;
|
|
49
|
+
|
|
50
|
+
const REPAIR_EVERY = 60;
|
|
51
|
+
|
|
52
|
+
const RETRY_MS = 30_000;
|
|
53
|
+
|
|
54
|
+
const PAGE = 100;
|
|
55
|
+
|
|
56
|
+
export function scheduleFrom(report: AdvanceReport, now: number): number | null {
|
|
57
|
+
switch (report.kind) {
|
|
58
|
+
case "continue":
|
|
59
|
+
return now;
|
|
60
|
+
case "waiting":
|
|
61
|
+
return report.until;
|
|
62
|
+
case "retry":
|
|
63
|
+
case "busy":
|
|
64
|
+
return report.until;
|
|
65
|
+
case "idle":
|
|
66
|
+
case "fenced":
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export async function pass(
|
|
72
|
+
host: PassHost,
|
|
73
|
+
storage: Pick<DurableObjectSql, "sql">,
|
|
74
|
+
budget: PassBudget,
|
|
75
|
+
): Promise<number | null> {
|
|
76
|
+
const began = Date.now();
|
|
77
|
+
const schedule = openSchedule(storage);
|
|
78
|
+
await host.server.tick(began);
|
|
79
|
+
|
|
80
|
+
if (budget.repair) {
|
|
81
|
+
let cursor: Parameters<Host["execution"]["repair"]>[0]["cursor"];
|
|
82
|
+
|
|
83
|
+
do {
|
|
84
|
+
cursor = (await host.execution.repair({ cursor, limit: PAGE })).next ?? undefined;
|
|
85
|
+
} while (cursor !== undefined);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const advances: Promise<void>[] = [];
|
|
89
|
+
|
|
90
|
+
for (const entry of schedule.due(Date.now())) {
|
|
91
|
+
if (advances.length >= budget.heads || Date.now() - began >= budget.ms) {
|
|
92
|
+
schedule.write(entry.session, entry.head, began);
|
|
93
|
+
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
advances.push(
|
|
98
|
+
host.execution.advance({ session: entry.session, head: entry.head }).then(
|
|
99
|
+
(report) => schedule.settle(entry, scheduleFrom(report, Date.now())),
|
|
100
|
+
(error: unknown) => {
|
|
101
|
+
console.error(`Advancing ${entry.session}/${entry.head} failed:`, error);
|
|
102
|
+
schedule.settle(entry, Date.now() + RETRY_MS);
|
|
103
|
+
},
|
|
104
|
+
),
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await Promise.all(advances);
|
|
109
|
+
|
|
110
|
+
return schedule.earliest();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** The Durable Object that is one Nyte host, importing only the node-free subpaths (§41j): composed once per object over its own SQLite storage with a chat-only workspace, served through `listenFetch`, and driven by its alarm. */
|
|
114
|
+
export class NyteHost {
|
|
115
|
+
readonly #state: WorkerState;
|
|
116
|
+
readonly #env: WorkerEnv;
|
|
117
|
+
readonly #models: Models | undefined;
|
|
118
|
+
#served: Promise<Served> | null = null;
|
|
119
|
+
#passes = 0;
|
|
120
|
+
|
|
121
|
+
constructor(state: WorkerState, env: WorkerEnv, options?: { readonly models?: Models }) {
|
|
122
|
+
this.#state = state;
|
|
123
|
+
this.#env = env;
|
|
124
|
+
this.#models = options?.models;
|
|
125
|
+
void state.blockConcurrencyWhile(async () => {
|
|
126
|
+
await this.#serve().catch(() => undefined);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Answers one request through the listener once the host is composed; an object whose composition failed answers its encoded `SdkError` (`unavailable`, 503) and composes again on the next request. */
|
|
131
|
+
async fetch(request: Request): Promise<Response> {
|
|
132
|
+
let served: Served;
|
|
133
|
+
|
|
134
|
+
try {
|
|
135
|
+
served = await this.#serve();
|
|
136
|
+
} catch (error) {
|
|
137
|
+
const failure = new SdkError(
|
|
138
|
+
"unavailable",
|
|
139
|
+
`The host did not start: ${error instanceof Error ? error.message : String(error)}`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return new Response(JSON.stringify(encodeError(failure)), {
|
|
143
|
+
status: errorStatus[failure.code],
|
|
144
|
+
headers: { "Content-Type": "application/json" },
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return served.listener.fetch(request);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async alarm(): Promise<void> {
|
|
152
|
+
const served = await this.#serve();
|
|
153
|
+
const repair = this.#passes % REPAIR_EVERY === 0;
|
|
154
|
+
this.#passes += 1;
|
|
155
|
+
const next = await pass(served.host, this.#state.storage, { ...PASS_BUDGET, repair });
|
|
156
|
+
|
|
157
|
+
if (next === null) await this.#state.storage.deleteAlarm();
|
|
158
|
+
else await this.#state.storage.setAlarm(next);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
#serve(): Promise<Served> {
|
|
162
|
+
const served = (this.#served ??= compose(this.#state, this.#env, this.#models));
|
|
163
|
+
|
|
164
|
+
served.catch(() => {
|
|
165
|
+
if (this.#served === served) this.#served = null;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
return served;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** The Worker: routes every request to the one `NyteHost` object, `env.NYTE_HOST.getByName("host")`. */
|
|
173
|
+
export default {
|
|
174
|
+
async fetch(request: Request, env: WorkerEnv): Promise<Response> {
|
|
175
|
+
return env.NYTE_HOST.getByName("host").fetch(request);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
type Served = {
|
|
180
|
+
readonly host: ServedHost;
|
|
181
|
+
readonly listener: Listener;
|
|
182
|
+
readonly coverage: Owned;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
async function compose(
|
|
186
|
+
state: WorkerState,
|
|
187
|
+
env: WorkerEnv,
|
|
188
|
+
models: Models | undefined,
|
|
189
|
+
): Promise<Served> {
|
|
190
|
+
const { storage } = state;
|
|
191
|
+
const store = durableObjectStore(storage);
|
|
192
|
+
const secrets = secretsFrom(env.NYTE_SECRET_KEY);
|
|
193
|
+
const description = { name: "nyte-host", platform: "cloudflare", version: manifest.version };
|
|
194
|
+
|
|
195
|
+
const host = await composeHost({
|
|
196
|
+
store,
|
|
197
|
+
models: models ?? createModels({ credentials: credentialStore(store, secrets) }),
|
|
198
|
+
plugins: [{ id: "chat-only", environment: chatOnlyProvider() }],
|
|
199
|
+
workspace: { kind: "chat-only", locator: null },
|
|
200
|
+
execution: { kind: "admit-only" },
|
|
201
|
+
secrets,
|
|
202
|
+
home: null,
|
|
203
|
+
description,
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const coverage = await host.execution.cover({ kind: "all" });
|
|
208
|
+
const schedule = openSchedule(storage);
|
|
209
|
+
|
|
210
|
+
host.server.onWake((session, head) => {
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
schedule.write(session, head, now);
|
|
213
|
+
void storage.setAlarm(now);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
const listener = await listenFetch(host, {
|
|
217
|
+
transport: { kind: "fetch" },
|
|
218
|
+
auth: { kind: "token", token: env.NYTE_TOKEN },
|
|
219
|
+
execution: { kind: "admit-only" },
|
|
220
|
+
describe: () => description,
|
|
221
|
+
...(env.NYTE_IDENTITY_KEY === undefined || env.NYTE_IDENTITY_KEY === ""
|
|
222
|
+
? {}
|
|
223
|
+
: {
|
|
224
|
+
identity: await seedSigner(host.hostId, Uint8Array.fromBase64(env.NYTE_IDENTITY_KEY)),
|
|
225
|
+
}),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
await storage.setAlarm(Date.now());
|
|
229
|
+
|
|
230
|
+
return { host, listener, coverage };
|
|
231
|
+
} catch (error) {
|
|
232
|
+
await host.close();
|
|
233
|
+
|
|
234
|
+
throw error;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function secretsFrom(key: string | undefined): NonNullable<OpenOptions["secrets"]> {
|
|
239
|
+
if (key === undefined || key === "")
|
|
240
|
+
return {
|
|
241
|
+
key: async () => {
|
|
242
|
+
throw new SdkError("unavailable", "Set NYTE_SECRET_KEY to a base64 32-byte key.");
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
const bytes = Uint8Array.from(atob(key), (character) => character.charCodeAt(0));
|
|
246
|
+
|
|
247
|
+
return { key: async () => bytes };
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function openSchedule(storage: Pick<DurableObjectSql, "sql">) {
|
|
251
|
+
const rows = (query: string, ...bindings: SqlValue[]): Record<string, SqlValue>[] =>
|
|
252
|
+
storage.sql.exec(query, ...bindings).toArray();
|
|
253
|
+
|
|
254
|
+
rows(
|
|
255
|
+
"CREATE TABLE IF NOT EXISTS nyte_schedule (session TEXT NOT NULL, head TEXT NOT NULL, at INTEGER NOT NULL, PRIMARY KEY (session, head))",
|
|
256
|
+
);
|
|
257
|
+
|
|
258
|
+
const readAt = (session: SessionId, head: HeadName): number | null => {
|
|
259
|
+
const at = rows("SELECT at FROM nyte_schedule WHERE session = ? AND head = ?", session, head)[0]
|
|
260
|
+
?.at;
|
|
261
|
+
|
|
262
|
+
return typeof at === "number" ? at : null;
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
const write = (session: SessionId, head: HeadName, at: number): void => {
|
|
266
|
+
rows(
|
|
267
|
+
"INSERT INTO nyte_schedule (session, head, at) VALUES (?, ?, ?) ON CONFLICT (session, head) DO UPDATE SET at = excluded.at",
|
|
268
|
+
session,
|
|
269
|
+
head,
|
|
270
|
+
at,
|
|
271
|
+
);
|
|
272
|
+
};
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
write,
|
|
276
|
+
due(
|
|
277
|
+
now: number,
|
|
278
|
+
): readonly { readonly session: SessionId; readonly head: HeadName; readonly at: number }[] {
|
|
279
|
+
return rows(
|
|
280
|
+
"SELECT session, head, at FROM nyte_schedule WHERE at <= ? ORDER BY at",
|
|
281
|
+
now,
|
|
282
|
+
).flatMap((row) =>
|
|
283
|
+
typeof row.session === "string" &&
|
|
284
|
+
typeof row.head === "string" &&
|
|
285
|
+
typeof row.at === "number"
|
|
286
|
+
? [{ session: asId<"session">(row.session), head: asId<"head">(row.head), at: row.at }]
|
|
287
|
+
: [],
|
|
288
|
+
);
|
|
289
|
+
},
|
|
290
|
+
settle(
|
|
291
|
+
entry: { readonly session: SessionId; readonly head: HeadName; readonly at: number },
|
|
292
|
+
next: number | null,
|
|
293
|
+
): void {
|
|
294
|
+
if (readAt(entry.session, entry.head) !== entry.at) return;
|
|
295
|
+
|
|
296
|
+
if (next === null)
|
|
297
|
+
rows("DELETE FROM nyte_schedule WHERE session = ? AND head = ?", entry.session, entry.head);
|
|
298
|
+
else write(entry.session, entry.head, next);
|
|
299
|
+
},
|
|
300
|
+
earliest(): number | null {
|
|
301
|
+
const at = rows("SELECT MIN(at) AS at FROM nyte_schedule")[0]?.at;
|
|
302
|
+
|
|
303
|
+
return typeof at === "number" ? at : null;
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nyte-host",
|
|
3
|
+
"main": "worker.ts",
|
|
4
|
+
"compatibility_date": "2026-10-02",
|
|
5
|
+
"compatibility_flags": ["nodejs_compat"],
|
|
6
|
+
"durable_objects": {
|
|
7
|
+
"bindings": [{ "name": "NYTE_HOST", "class_name": "NyteHost" }],
|
|
8
|
+
},
|
|
9
|
+
"migrations": [{ "tag": "v1", "new_sqlite_classes": ["NyteHost"] }],
|
|
10
|
+
"secrets": { "required": ["NYTE_TOKEN"] },
|
|
11
|
+
"observability": { "enabled": true },
|
|
12
|
+
}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Vercel
|
|
2
|
+
|
|
3
|
+
This directory is a Nitro app template for running a Nyte host on Vercel; the deployer builds it with `nitro build --preset vercel` (§42m). `nitro.config.ts` serves `server/`, compiles the workflow in `server/workflows` through the `workflow/nitro` integration, lets functions run up to 300 s, and schedules the repair cron. `server/routes/[...].ts` sends every path to the listener of a host composed once per function instance (`server/utils/host.ts`): sessions in Postgres (`DATABASE_URL`), tools in the sandbox named by `NYTE_SANDBOX`, booted by the `boot` you supply in `server/utils/host.ts` over your sandbox SDK (the template carries none; until you do, that workspace answers `unavailable`), and the store re-read every second so each instance sees the others' writes. The host's execution is `dispatch`: after each commit it hands every head a wake makes owed a step to `driveHead` in `server/workflows/drive.ts`, which advances it one step at a time, each step covering its session, sleeps durably between steps until nothing is owed, and ends when another instance holds the head. The cron calls `GET /.nyte/repair` every minute (`NYTE_REPAIR_SCHEDULE` overrides; Hobby plans allow daily only) to dispatch again any head a failed dispatch left owed; that route answers only `Authorization: Bearer <CRON_SECRET>` (compared in constant time; Vercel sends it when `CRON_SECRET` is set), anything else `unauthenticated`, and `unknown` when `CRON_SECRET` is unset (`server/utils/repair.ts`). Jobs run per instance: a client reads another instance's job only through its exit report. Install `nitro` and `workflow` (optional peers of `@nyte-ai/host`; the route reaches h3 through `nitro/h3`) and your sandbox SDK, and set `NYTE_TOKEN` (at least 16 characters), `CRON_SECRET`, to enable `secrets.set`, `NYTE_SECRET_KEY` (base64), and, to answer identity challenges, `NYTE_IDENTITY_KEY` (a base64 32-byte Ed25519 seed; without it `/.nyte/identity` answers `unknown`).
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { defineConfig } from "nitro";
|
|
3
|
+
import type { ModuleOptions } from "workflow/nitro";
|
|
4
|
+
|
|
5
|
+
const config = {
|
|
6
|
+
serverDir: "./server",
|
|
7
|
+
modules: ["workflow/nitro"],
|
|
8
|
+
workflow: {
|
|
9
|
+
runtime: "nodejs24.x",
|
|
10
|
+
dirs: ["./server/workflows"],
|
|
11
|
+
},
|
|
12
|
+
vercel: {
|
|
13
|
+
functions: {
|
|
14
|
+
runtime: "nodejs24.x",
|
|
15
|
+
maxDuration: 300,
|
|
16
|
+
},
|
|
17
|
+
config: {
|
|
18
|
+
version: 3,
|
|
19
|
+
crons: [
|
|
20
|
+
{
|
|
21
|
+
path: "/.nyte/repair",
|
|
22
|
+
schedule: process.env.NYTE_REPAIR_SCHEDULE ?? "* * * * *",
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
} satisfies Parameters<typeof defineConfig>[0] & { workflow: ModuleOptions };
|
|
28
|
+
|
|
29
|
+
export default defineConfig(config);
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { defineHandler } from "nitro/h3";
|
|
2
|
+
import type { H3Event } from "nitro/h3";
|
|
3
|
+
import { vercelHost } from "../utils/host.ts";
|
|
4
|
+
import { dispatchHead } from "../workflows/drive.ts";
|
|
5
|
+
|
|
6
|
+
async function serve(event: H3Event): Promise<Response> {
|
|
7
|
+
return (await vercelHost(dispatchHead)).listener.fetch(event.req);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export default defineHandler(serve);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
import { createModels } from "@nyte-ai/ai/models";
|
|
3
|
+
import { SdkError } from "@nyte-ai/schema";
|
|
4
|
+
import { composeHost } from "@nyte-ai/core/server";
|
|
5
|
+
import type { Host, Listener, OpenOptions } from "@nyte-ai/core";
|
|
6
|
+
import type { ServedHost } from "@nyte-ai/core/server";
|
|
7
|
+
import { postgresStore } from "@nyte-ai/host";
|
|
8
|
+
import { credentialStore } from "@nyte-ai/host/credentials";
|
|
9
|
+
import { seedSigner } from "@nyte-ai/host/identity";
|
|
10
|
+
import { listenFetch } from "@nyte-ai/host/listen-fetch";
|
|
11
|
+
import { sandboxProvider } from "@nyte-ai/host/providers";
|
|
12
|
+
import manifest from "../../../../../package.json" with { type: "json" };
|
|
13
|
+
import { answerRepair } from "./repair.ts";
|
|
14
|
+
|
|
15
|
+
type Served = {
|
|
16
|
+
readonly host: ServedHost;
|
|
17
|
+
readonly listener: Listener;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/** Starts the workflow that drives a head; the host hands each wake to it. */
|
|
21
|
+
type Dispatch = Extract<
|
|
22
|
+
NonNullable<OpenOptions["execution"]>,
|
|
23
|
+
{ readonly kind: "dispatch" }
|
|
24
|
+
>["dispatch"];
|
|
25
|
+
|
|
26
|
+
const PAGE = 100;
|
|
27
|
+
|
|
28
|
+
const boot: Parameters<typeof sandboxProvider>[0]["boot"] = async () => {
|
|
29
|
+
throw new SdkError("unavailable", "Supply a boot over your sandbox SDK in server/utils/host.ts.");
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
let building: Promise<Served> | null = null;
|
|
33
|
+
|
|
34
|
+
/** This process's host, composed once, handing each wake to `dispatch`. */
|
|
35
|
+
export function vercelHost(dispatch: Dispatch): Promise<Served> {
|
|
36
|
+
const built = (building ??= build(dispatch));
|
|
37
|
+
|
|
38
|
+
built.catch(() => {
|
|
39
|
+
if (building === built) building = null;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
return built;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function build(dispatch: Dispatch): Promise<Served> {
|
|
46
|
+
const connectionString = process.env.DATABASE_URL;
|
|
47
|
+
|
|
48
|
+
if (connectionString === undefined || connectionString === "")
|
|
49
|
+
throw new SdkError("unavailable", "Set DATABASE_URL to the Postgres database the host keeps.");
|
|
50
|
+
const identityKey = process.env.NYTE_IDENTITY_KEY;
|
|
51
|
+
const store = postgresStore({ connectionString });
|
|
52
|
+
const secrets = secretsFrom(process.env.NYTE_SECRET_KEY);
|
|
53
|
+
const description = { name: "nyte-host", platform: "vercel", version: manifest.version };
|
|
54
|
+
|
|
55
|
+
const host = await composeHost({
|
|
56
|
+
store,
|
|
57
|
+
models: createModels({ credentials: credentialStore(store, secrets) }),
|
|
58
|
+
plugins: [{ id: "sandbox", environment: sandboxProvider({ boot }) }],
|
|
59
|
+
workspace: { kind: "sandbox", locator: { name: process.env.NYTE_SANDBOX ?? "nyte" } },
|
|
60
|
+
execution: { kind: "dispatch", dispatch },
|
|
61
|
+
poll: 1000,
|
|
62
|
+
secrets,
|
|
63
|
+
home: null,
|
|
64
|
+
description,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const repair = async (): Promise<void> => {
|
|
68
|
+
let cursor: Parameters<Host["execution"]["repair"]>[0]["cursor"];
|
|
69
|
+
|
|
70
|
+
do {
|
|
71
|
+
const page = await host.execution.repair({ cursor, limit: PAGE });
|
|
72
|
+
cursor = page.next ?? undefined;
|
|
73
|
+
} while (cursor !== undefined);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const listener = await listenFetch(host, {
|
|
77
|
+
transport: { kind: "fetch" },
|
|
78
|
+
auth: { kind: "token", token: process.env.NYTE_TOKEN ?? "" },
|
|
79
|
+
execution: { kind: "admit-only" },
|
|
80
|
+
describe: () => description,
|
|
81
|
+
...(identityKey === undefined || identityKey === ""
|
|
82
|
+
? {}
|
|
83
|
+
: { identity: await seedSigner(host.hostId, Uint8Array.fromBase64(identityKey)) }),
|
|
84
|
+
before: (request) => answerRepair(request, process.env.CRON_SECRET, repair),
|
|
85
|
+
}).catch(async (error: unknown) => {
|
|
86
|
+
await host.close();
|
|
87
|
+
|
|
88
|
+
throw error;
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
return { host, listener };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function secretsFrom(key: string | undefined): NonNullable<OpenOptions["secrets"]> {
|
|
95
|
+
if (key === undefined || key === "")
|
|
96
|
+
return {
|
|
97
|
+
key: async () => {
|
|
98
|
+
throw new SdkError("unavailable", "Set NYTE_SECRET_KEY to a base64 32-byte key.");
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
const bytes = new Uint8Array(Buffer.from(key, "base64"));
|
|
102
|
+
|
|
103
|
+
return { key: async () => bytes };
|
|
104
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import { SdkError } from "@nyte-ai/schema";
|
|
3
|
+
import { encodeError, errorStatus } from "@nyte-ai/protocol";
|
|
4
|
+
|
|
5
|
+
export async function answerRepair(
|
|
6
|
+
request: Request,
|
|
7
|
+
secret: string | undefined,
|
|
8
|
+
repair: () => Promise<void>,
|
|
9
|
+
): Promise<Response | undefined> {
|
|
10
|
+
if (request.method !== "GET" || new URL(request.url).pathname !== "/.nyte/repair")
|
|
11
|
+
return undefined;
|
|
12
|
+
|
|
13
|
+
if (secret === undefined || secret === "")
|
|
14
|
+
return failure(new SdkError("unknown", "This deployment sets no CRON_SECRET."));
|
|
15
|
+
|
|
16
|
+
const given = /^Bearer\s+(\S+)\s*$/i.exec(request.headers.get("Authorization") ?? "")?.[1] ?? "";
|
|
17
|
+
|
|
18
|
+
if (!timingSafeEqual(digest(given), digest(secret)))
|
|
19
|
+
return failure(new SdkError("unauthenticated", "Send Authorization: Bearer <CRON_SECRET>."));
|
|
20
|
+
|
|
21
|
+
await repair();
|
|
22
|
+
|
|
23
|
+
return new Response(null, { status: 204 });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function digest(text: string): Buffer {
|
|
27
|
+
return createHash("sha256").update(text).digest();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function failure(error: SdkError): Response {
|
|
31
|
+
return new Response(JSON.stringify(encodeError(error)), {
|
|
32
|
+
status: errorStatus[error.code],
|
|
33
|
+
headers: { "Content-Type": "application/json" },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { AdvanceReport } from "@nyte-ai/schema";
|
|
2
|
+
|
|
3
|
+
export type DriveStep =
|
|
4
|
+
| { readonly kind: "advance" }
|
|
5
|
+
| { readonly kind: "sleep"; readonly ms: number }
|
|
6
|
+
| { readonly kind: "stop" };
|
|
7
|
+
|
|
8
|
+
export function stepFrom(report: AdvanceReport, now: number): DriveStep {
|
|
9
|
+
switch (report.kind) {
|
|
10
|
+
case "continue":
|
|
11
|
+
return { kind: "advance" };
|
|
12
|
+
case "waiting":
|
|
13
|
+
return report.until === null ? { kind: "stop" } : untilStep(report.until, now);
|
|
14
|
+
case "retry":
|
|
15
|
+
return untilStep(report.until, now);
|
|
16
|
+
case "busy":
|
|
17
|
+
case "idle":
|
|
18
|
+
case "fenced":
|
|
19
|
+
return { kind: "stop" };
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function untilStep(until: number, now: number): DriveStep {
|
|
24
|
+
return until > now ? { kind: "sleep", ms: until - now } : { kind: "advance" };
|
|
25
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { sleep } from "workflow";
|
|
2
|
+
import { start } from "workflow/api";
|
|
3
|
+
import type { AdvanceReport, HeadName, SessionId } from "@nyte-ai/schema";
|
|
4
|
+
import { vercelHost } from "../utils/host.ts";
|
|
5
|
+
import { stepFrom } from "../utils/step.ts";
|
|
6
|
+
|
|
7
|
+
/** Hands a head's wake to the workflow that drives it. */
|
|
8
|
+
export async function dispatchHead(target: {
|
|
9
|
+
readonly session: SessionId;
|
|
10
|
+
readonly head: HeadName;
|
|
11
|
+
}): Promise<void> {
|
|
12
|
+
await start(driveHead, [target]);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function driveHead(input: {
|
|
16
|
+
readonly session: SessionId;
|
|
17
|
+
readonly head: HeadName;
|
|
18
|
+
}): Promise<void> {
|
|
19
|
+
"use workflow";
|
|
20
|
+
|
|
21
|
+
for (;;) {
|
|
22
|
+
const next = stepFrom(await advanceHead(input), Date.now());
|
|
23
|
+
|
|
24
|
+
if (next.kind === "stop") return;
|
|
25
|
+
|
|
26
|
+
if (next.kind === "sleep") await sleep(next.ms);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function advanceHead(input: {
|
|
31
|
+
readonly session: SessionId;
|
|
32
|
+
readonly head: HeadName;
|
|
33
|
+
}): Promise<AdvanceReport> {
|
|
34
|
+
"use step";
|
|
35
|
+
|
|
36
|
+
const { host } = await vercelHost(dispatchHead);
|
|
37
|
+
const coverage = await host.execution.cover({ kind: "sessions", sessions: [input.session] });
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
await host.server.tick(Date.now());
|
|
41
|
+
|
|
42
|
+
return await host.execution.advance(input);
|
|
43
|
+
} finally {
|
|
44
|
+
await coverage.close();
|
|
45
|
+
}
|
|
46
|
+
}
|