@genkaku/dsh-runtime 0.0.0-stage → 0.1.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.
- package/LICENSE +21 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/runtime.d.ts +272 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +726 -0
- package/package.json +59 -3
- package/README.md +0 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GenKaKu Labs
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness adapter for the GenKaKu protocol.
|
|
3
|
+
*
|
|
4
|
+
* One runtime, two plugin identities: `@genkaku/dsh-worker-plugin` pins a
|
|
5
|
+
* serving role and `@genkaku/dsh-user-plugin` pins the consuming role. Keeping
|
|
6
|
+
* the implementation here means the two packages cannot drift apart.
|
|
7
|
+
*/
|
|
8
|
+
export { Config, DEFAULT_SETTINGS_NS, GenkakuRuntime, defineGenkakuPlugin } from './runtime.ts';
|
|
9
|
+
export type { Config as GenkakuConfig } from './runtime.ts';
|
|
10
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,OAAO,EAAE,MAAM,EAAE,mBAAmB,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAA;AAC/F,YAAY,EAAE,MAAM,IAAI,aAAa,EAAE,MAAM,cAAc,CAAA"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeepSeek Harness adapter for the GenKaKu protocol.
|
|
3
|
+
*
|
|
4
|
+
* One runtime, two plugin identities: `@genkaku/dsh-worker-plugin` pins a
|
|
5
|
+
* serving role and `@genkaku/dsh-user-plugin` pins the consuming role. Keeping
|
|
6
|
+
* the implementation here means the two packages cannot drift apart.
|
|
7
|
+
*/
|
|
8
|
+
export { Config, DEFAULT_SETTINGS_NS, GenkakuRuntime, defineGenkakuPlugin } from "./runtime.js";
|
|
9
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import type { Context, Logger, Volatile } from '@deepseek-ai/cordis';
|
|
2
|
+
import z from '@deepseek-ai/schemastery';
|
|
3
|
+
import { type KeyVault } from '@genkaku/protocol';
|
|
4
|
+
import { ModeManager, type RoleMode } from '@genkaku/protocol';
|
|
5
|
+
import { type NetEnvironment } from '@genkaku/protocol';
|
|
6
|
+
/** Settings document this runtime reads; each adapter passes its own id so the
|
|
7
|
+
* worker and user plugins can be installed side by side. */
|
|
8
|
+
export declare const DEFAULT_SETTINGS_NS = "genkaku";
|
|
9
|
+
/** One (model, price) row the worker publishes on the offer board. */
|
|
10
|
+
export type CatalogEntry = {
|
|
11
|
+
/** Ollama model name, e.g. "llama3:8b" */
|
|
12
|
+
model: string;
|
|
13
|
+
/** 6-decimal USDC per hour, as a decimal string */
|
|
14
|
+
pricePerHour: string;
|
|
15
|
+
/** coarse, self-reported city (privacy: never street-level) */
|
|
16
|
+
city?: string;
|
|
17
|
+
/** 0–100 self-reported credit heuristic */
|
|
18
|
+
creditScore?: number;
|
|
19
|
+
/** completed sessions (joins into creditScore on-chain later) */
|
|
20
|
+
sessionsServed?: number;
|
|
21
|
+
/** parallel sessions the worker accepts */
|
|
22
|
+
maxConcurrent?: number;
|
|
23
|
+
};
|
|
24
|
+
export interface Config {
|
|
25
|
+
/** user = consume remote workers; localPlusSharing / workerDedicated = serve */
|
|
26
|
+
mode: Volatile<RoleMode>;
|
|
27
|
+
/** data-plane chain the receipts bind to */
|
|
28
|
+
env: NetEnvironment;
|
|
29
|
+
/** local OpenAI bridge port (0 = ephemeral; user mode only) */
|
|
30
|
+
port: Volatile<number>;
|
|
31
|
+
/** worker multiaddrs; empty = discover through the baked seed peers */
|
|
32
|
+
workers: Volatile<string[]>;
|
|
33
|
+
/** open/settle real escrows (Base Sepolia only). Default ON: a worker's
|
|
34
|
+
* paywall is ON by default, so a free-session client would always be
|
|
35
|
+
* refused. Set false only for an offline/dev pair. */
|
|
36
|
+
settle: boolean;
|
|
37
|
+
/** refuse to serve unless the client's escrow pays this worker (P4.9d).
|
|
38
|
+
* Defaults ON: a directory entry with the gate off is free GPU for anyone
|
|
39
|
+
* who scans the chain. Dev machines opt out explicitly. */
|
|
40
|
+
paywall: boolean;
|
|
41
|
+
/** directory for the encrypted key vault (default ~/.dsh/genkaku) */
|
|
42
|
+
keyDir: string;
|
|
43
|
+
/** worker-side offer rows */
|
|
44
|
+
catalog: CatalogEntry[];
|
|
45
|
+
/** worker-side libp2p listen addresses */
|
|
46
|
+
listen: string[];
|
|
47
|
+
/** publish this worker's public address to the on-chain directory. OFF by
|
|
48
|
+
* default: a global IP is permanent once written (privacy decision §25). */
|
|
49
|
+
publishAddress: boolean;
|
|
50
|
+
/** worker name shown on the offer board */
|
|
51
|
+
workerName: string;
|
|
52
|
+
/** start the local `ollama serve` before the worker sidecar starts */
|
|
53
|
+
spawnOllama: boolean;
|
|
54
|
+
/** local Ollama base URL */
|
|
55
|
+
ollamaHost: string;
|
|
56
|
+
}
|
|
57
|
+
/** Validated plugin configuration. */
|
|
58
|
+
export declare const Config: z<Schemastery.ObjectS<NoInfer<{
|
|
59
|
+
mode: z<"user" | "localPlusSharing" | "workerDedicated", "user" | "localPlusSharing" | "workerDedicated", "volatile-defined">;
|
|
60
|
+
env: z<"base-sepolia" | "base", "base-sepolia" | "base", "defined">;
|
|
61
|
+
port: z<number, number, "volatile-defined">;
|
|
62
|
+
workers: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
63
|
+
settle: z<boolean, boolean, "defined">;
|
|
64
|
+
paywall: z<boolean, boolean, "defined">;
|
|
65
|
+
keyDir: z<string, string, "defined">;
|
|
66
|
+
catalog: z<({
|
|
67
|
+
model?: string | null | undefined;
|
|
68
|
+
pricePerHour?: string | null | undefined;
|
|
69
|
+
city?: string | null | undefined;
|
|
70
|
+
creditScore?: number | null | undefined;
|
|
71
|
+
sessionsServed?: number | null | undefined;
|
|
72
|
+
maxConcurrent?: number | null | undefined;
|
|
73
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
|
|
74
|
+
model: z<string, string, "defined">;
|
|
75
|
+
pricePerHour: z<string, string, "defined">;
|
|
76
|
+
city: z<string, string, "plain">;
|
|
77
|
+
creditScore: z<number, number, "defined">;
|
|
78
|
+
sessionsServed: z<number, number, "defined">;
|
|
79
|
+
maxConcurrent: z<number, number, "plain">;
|
|
80
|
+
}>>[], "defined">;
|
|
81
|
+
listen: z<string[], string[], "defined">;
|
|
82
|
+
publishAddress: z<boolean, boolean, "defined">;
|
|
83
|
+
workerName: z<string, string, "defined">;
|
|
84
|
+
spawnOllama: z<boolean, boolean, "defined">;
|
|
85
|
+
ollamaHost: z<string, string, "defined">;
|
|
86
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
87
|
+
mode: z<"user" | "localPlusSharing" | "workerDedicated", "user" | "localPlusSharing" | "workerDedicated", "volatile-defined">;
|
|
88
|
+
env: z<"base-sepolia" | "base", "base-sepolia" | "base", "defined">;
|
|
89
|
+
port: z<number, number, "volatile-defined">;
|
|
90
|
+
workers: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
91
|
+
settle: z<boolean, boolean, "defined">;
|
|
92
|
+
paywall: z<boolean, boolean, "defined">;
|
|
93
|
+
keyDir: z<string, string, "defined">;
|
|
94
|
+
catalog: z<({
|
|
95
|
+
model?: string | null | undefined;
|
|
96
|
+
pricePerHour?: string | null | undefined;
|
|
97
|
+
city?: string | null | undefined;
|
|
98
|
+
creditScore?: number | null | undefined;
|
|
99
|
+
sessionsServed?: number | null | undefined;
|
|
100
|
+
maxConcurrent?: number | null | undefined;
|
|
101
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
|
|
102
|
+
model: z<string, string, "defined">;
|
|
103
|
+
pricePerHour: z<string, string, "defined">;
|
|
104
|
+
city: z<string, string, "plain">;
|
|
105
|
+
creditScore: z<number, number, "defined">;
|
|
106
|
+
sessionsServed: z<number, number, "defined">;
|
|
107
|
+
maxConcurrent: z<number, number, "plain">;
|
|
108
|
+
}>>[], "defined">;
|
|
109
|
+
listen: z<string[], string[], "defined">;
|
|
110
|
+
publishAddress: z<boolean, boolean, "defined">;
|
|
111
|
+
workerName: z<string, string, "defined">;
|
|
112
|
+
spawnOllama: z<boolean, boolean, "defined">;
|
|
113
|
+
ollamaHost: z<string, string, "defined">;
|
|
114
|
+
}>>, "plain">;
|
|
115
|
+
/**
|
|
116
|
+
* Start the side that this machine's role allows. The controller reads the
|
|
117
|
+
* volatile mode once at startup; startup is awaited directly so a failure
|
|
118
|
+
* rejects the plugin, and only a successful start registers teardown.
|
|
119
|
+
*/
|
|
120
|
+
/**
|
|
121
|
+
* Build one DeepSeek Harness plugin from this runtime.
|
|
122
|
+
*
|
|
123
|
+
* The worker and user adapters are separate packages, but they share one
|
|
124
|
+
* implementation: what differs is the plugin id, the settings namespace, and
|
|
125
|
+
* the role it is allowed to run. Returns the exact shape dsh expects from a
|
|
126
|
+
* plugin module (`name`, `inject`, `Config`, `apply`).
|
|
127
|
+
*
|
|
128
|
+
* @param options.pluginName - cordis plugin name, e.g. `genkaku-worker`.
|
|
129
|
+
* @param options.settingsNs - settings document id this adapter owns.
|
|
130
|
+
* @param options.role - serving or consuming; overrides any stored mode.
|
|
131
|
+
*/
|
|
132
|
+
export declare function defineGenkakuPlugin(options: {
|
|
133
|
+
pluginName: string;
|
|
134
|
+
settingsNs: string;
|
|
135
|
+
role?: RoleMode;
|
|
136
|
+
/** Extra cordis services the adapter half needs (e.g. `llm`). */
|
|
137
|
+
injectExtra?: readonly string[];
|
|
138
|
+
/** Runs once the runtime is up; where an adapter registers its own services. */
|
|
139
|
+
onStarted?: (ctx: Context, config: Config) => void;
|
|
140
|
+
}): {
|
|
141
|
+
name: string;
|
|
142
|
+
inject: readonly string[];
|
|
143
|
+
Config: z<Schemastery.ObjectS<NoInfer<{
|
|
144
|
+
mode: z<"user" | "localPlusSharing" | "workerDedicated", "user" | "localPlusSharing" | "workerDedicated", "volatile-defined">;
|
|
145
|
+
env: z<"base-sepolia" | "base", "base-sepolia" | "base", "defined">;
|
|
146
|
+
port: z<number, number, "volatile-defined">;
|
|
147
|
+
workers: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
148
|
+
settle: z<boolean, boolean, "defined">;
|
|
149
|
+
paywall: z<boolean, boolean, "defined">;
|
|
150
|
+
keyDir: z<string, string, "defined">;
|
|
151
|
+
catalog: z<({
|
|
152
|
+
model?: string | null | undefined;
|
|
153
|
+
pricePerHour?: string | null | undefined;
|
|
154
|
+
city?: string | null | undefined;
|
|
155
|
+
creditScore?: number | null | undefined;
|
|
156
|
+
sessionsServed?: number | null | undefined;
|
|
157
|
+
maxConcurrent?: number | null | undefined;
|
|
158
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
|
|
159
|
+
model: z<string, string, "defined">;
|
|
160
|
+
pricePerHour: z<string, string, "defined">;
|
|
161
|
+
city: z<string, string, "plain">;
|
|
162
|
+
creditScore: z<number, number, "defined">;
|
|
163
|
+
sessionsServed: z<number, number, "defined">;
|
|
164
|
+
maxConcurrent: z<number, number, "plain">;
|
|
165
|
+
}>>[], "defined">;
|
|
166
|
+
listen: z<string[], string[], "defined">;
|
|
167
|
+
publishAddress: z<boolean, boolean, "defined">;
|
|
168
|
+
workerName: z<string, string, "defined">;
|
|
169
|
+
spawnOllama: z<boolean, boolean, "defined">;
|
|
170
|
+
ollamaHost: z<string, string, "defined">;
|
|
171
|
+
}>>, Schemastery.ObjectT<NoInfer<{
|
|
172
|
+
mode: z<"user" | "localPlusSharing" | "workerDedicated", "user" | "localPlusSharing" | "workerDedicated", "volatile-defined">;
|
|
173
|
+
env: z<"base-sepolia" | "base", "base-sepolia" | "base", "defined">;
|
|
174
|
+
port: z<number, number, "volatile-defined">;
|
|
175
|
+
workers: z<NoInfer<string[]>, NoInfer<string[]>, "volatile-defined">;
|
|
176
|
+
settle: z<boolean, boolean, "defined">;
|
|
177
|
+
paywall: z<boolean, boolean, "defined">;
|
|
178
|
+
keyDir: z<string, string, "defined">;
|
|
179
|
+
catalog: z<({
|
|
180
|
+
model?: string | null | undefined;
|
|
181
|
+
pricePerHour?: string | null | undefined;
|
|
182
|
+
city?: string | null | undefined;
|
|
183
|
+
creditScore?: number | null | undefined;
|
|
184
|
+
sessionsServed?: number | null | undefined;
|
|
185
|
+
maxConcurrent?: number | null | undefined;
|
|
186
|
+
} & import("@deepseek-ai/cosmokit").Dict)[], Schemastery.ObjectT<NoInfer<{
|
|
187
|
+
model: z<string, string, "defined">;
|
|
188
|
+
pricePerHour: z<string, string, "defined">;
|
|
189
|
+
city: z<string, string, "plain">;
|
|
190
|
+
creditScore: z<number, number, "defined">;
|
|
191
|
+
sessionsServed: z<number, number, "defined">;
|
|
192
|
+
maxConcurrent: z<number, number, "plain">;
|
|
193
|
+
}>>[], "defined">;
|
|
194
|
+
listen: z<string[], string[], "defined">;
|
|
195
|
+
publishAddress: z<boolean, boolean, "defined">;
|
|
196
|
+
workerName: z<string, string, "defined">;
|
|
197
|
+
spawnOllama: z<boolean, boolean, "defined">;
|
|
198
|
+
ollamaHost: z<string, string, "defined">;
|
|
199
|
+
}>>, "plain">;
|
|
200
|
+
apply: (ctx: Context, config: Config) => Promise<void>;
|
|
201
|
+
};
|
|
202
|
+
/** Owns the running halves so the mode starter and teardown share one state. */
|
|
203
|
+
export declare class GenkakuRuntime {
|
|
204
|
+
private readonly ctx;
|
|
205
|
+
private readonly config;
|
|
206
|
+
private readonly vault;
|
|
207
|
+
private readonly modeManager;
|
|
208
|
+
private readonly log;
|
|
209
|
+
private readonly keyDir;
|
|
210
|
+
/**
|
|
211
|
+
* Mode this adapter is allowed to run. The worker plugin pins a serving
|
|
212
|
+
* mode and the user plugin pins `user`, so installing both cannot make
|
|
213
|
+
* them fight over the same peer identity and listen ports.
|
|
214
|
+
*/
|
|
215
|
+
private readonly forcedMode?;
|
|
216
|
+
/** Settings document id; defaults to the single-plugin namespace. */
|
|
217
|
+
private readonly settingsNs;
|
|
218
|
+
private node;
|
|
219
|
+
private ollama;
|
|
220
|
+
private stop;
|
|
221
|
+
private statusServer;
|
|
222
|
+
private applied;
|
|
223
|
+
/** Worker-side address watcher: advertise only when the announced set moves. */
|
|
224
|
+
private advertiseState;
|
|
225
|
+
private advertiseTimer;
|
|
226
|
+
private reconfigureQueue;
|
|
227
|
+
private modeTicker;
|
|
228
|
+
private pendingMode;
|
|
229
|
+
private modeUpdating;
|
|
230
|
+
constructor(ctx: Context, config: Config, vault: KeyVault, modeManager: ModeManager, log: Logger, keyDir: string,
|
|
231
|
+
/**
|
|
232
|
+
* Mode this adapter is allowed to run. The worker plugin pins a serving
|
|
233
|
+
* mode and the user plugin pins `user`, so installing both cannot make
|
|
234
|
+
* them fight over the same peer identity and listen ports.
|
|
235
|
+
*/
|
|
236
|
+
forcedMode?: RoleMode | undefined,
|
|
237
|
+
/** Settings document id; defaults to the single-plugin namespace. */
|
|
238
|
+
settingsNs?: string);
|
|
239
|
+
/** Read the configured mode once, switch the guard, and start its half. */
|
|
240
|
+
applyMode(): Promise<void>;
|
|
241
|
+
/** Store the latest requested mode; the runtime ticker applies it. */
|
|
242
|
+
private scheduleMode;
|
|
243
|
+
/** Persistent 300 ms pump, created outside request contexts. */
|
|
244
|
+
private startModeTicker;
|
|
245
|
+
private stopModeTicker;
|
|
246
|
+
/** Apply the latest pending mode, retrying only mid-reload failures. */
|
|
247
|
+
private flushMode;
|
|
248
|
+
/** Serialize settings-driven restarts; each call waits for the previous one. */
|
|
249
|
+
reconfigure(): Promise<void>;
|
|
250
|
+
/** Restart only when a startup field changed; otherwise keep serving. */
|
|
251
|
+
private reconfigureNow;
|
|
252
|
+
private startupState;
|
|
253
|
+
/** User half: a bare libp2p dial node plus the OpenAI-compatible bridge. */
|
|
254
|
+
startUser(): Promise<void>;
|
|
255
|
+
/** Worker half: the libp2p sidecar, its status dashboard, and Ollama. */
|
|
256
|
+
startWorker(): Promise<void>;
|
|
257
|
+
/** Poll the announce set; advertise only after it has been stable for two
|
|
258
|
+
* checks. A worker's listen addrs flap while interfaces come up (Wi-Fi,
|
|
259
|
+
* VPN, sleep/wake) and every advertise is a chain write — this turns a
|
|
260
|
+
* flap into zero transactions instead of a burst of them. */
|
|
261
|
+
private startAdvertiseWatch;
|
|
262
|
+
private stopAdvertiseWatch;
|
|
263
|
+
private checkAdvertise;
|
|
264
|
+
/** Stop the running halves without touching the lifetime mode ticker. */
|
|
265
|
+
private stopServices;
|
|
266
|
+
/** Shut down: stop services and the one lifetime mode ticker. */
|
|
267
|
+
stopAll(): Promise<void>;
|
|
268
|
+
/** Plugin-lifecycle shutdown alias. */
|
|
269
|
+
dispose(): Promise<void>;
|
|
270
|
+
}
|
|
271
|
+
/** Best-effort local Ollama service; a failed spawn leaves an existing daemon alone. */
|
|
272
|
+
//# sourceMappingURL=runtime.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runtime.d.ts","sourceRoot":"","sources":["../src/runtime.ts"],"names":[],"mappings":"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"}
|
package/dist/runtime.js
ADDED
|
@@ -0,0 +1,726 @@
|
|
|
1
|
+
// GENKAKU for dsh — Cordis plugin entry.
|
|
2
|
+
// User mode starts the OpenAI-compatible local bridge (dsh dials it as a plain
|
|
3
|
+
// model provider); worker modes start the libp2p sidecar that serves the local
|
|
4
|
+
// Ollama instance. The mode manager enforces the D-block (never consume and
|
|
5
|
+
// serve on one machine at the same time).
|
|
6
|
+
import { appendFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
7
|
+
import { homedir } from 'node:os';
|
|
8
|
+
import { join } from 'node:path';
|
|
9
|
+
import z from '@deepseek-ai/schemastery';
|
|
10
|
+
import { startOpenAiBridge } from '@genkaku/protocol';
|
|
11
|
+
import { advertiseWorker, deactivateWorker, hasPublicAddr, locatorOf, publicClientFor, readWorker, readWalletBalances, readWorkerLocators, registerWorkerWithLocator, walletAddress, walletClientFor, } from '@genkaku/protocol';
|
|
12
|
+
import { fetchSeedPeers } from '@genkaku/protocol';
|
|
13
|
+
import { loadDirectory, syncDirectory, workerAddrs } from '@genkaku/protocol';
|
|
14
|
+
import { createKeyVault } from '@genkaku/protocol';
|
|
15
|
+
import { createOllamaChatSource, createOllamaSource } from '@genkaku/protocol';
|
|
16
|
+
import { createOllamaSupervisor } from '@genkaku/protocol';
|
|
17
|
+
import { ModeManager } from '@genkaku/protocol';
|
|
18
|
+
import { probeRpc } from '@genkaku/protocol';
|
|
19
|
+
import { HARDCODED_SEEDS } from '@genkaku/protocol';
|
|
20
|
+
import { toWire } from '@genkaku/protocol';
|
|
21
|
+
import { CreditTracker } from '@genkaku/protocol';
|
|
22
|
+
import { createPaywall } from '@genkaku/protocol';
|
|
23
|
+
import { readPublishOptIn, writePublishOptIn } from '@genkaku/protocol';
|
|
24
|
+
import { createSidecar, createUserNode } from '@genkaku/protocol';
|
|
25
|
+
import { POLICY_VERSION, peerIdDigestOf, policyHashOf } from '@genkaku/protocol';
|
|
26
|
+
import { startStatusServer } from '@genkaku/protocol';
|
|
27
|
+
/** Settings document this runtime reads; each adapter passes its own id so the
|
|
28
|
+
* worker and user plugins can be installed side by side. */
|
|
29
|
+
export const DEFAULT_SETTINGS_NS = 'genkaku';
|
|
30
|
+
const catalogEntry = z.object({
|
|
31
|
+
model: z.string().min(1).required(),
|
|
32
|
+
pricePerHour: z.string().pattern(/^\d+$/).default('0'),
|
|
33
|
+
city: z.string(),
|
|
34
|
+
creditScore: z.natural().max(100).default(50),
|
|
35
|
+
sessionsServed: z.natural().default(0),
|
|
36
|
+
maxConcurrent: z.natural().min(1),
|
|
37
|
+
});
|
|
38
|
+
/** Validated plugin configuration. */
|
|
39
|
+
export const Config = z.object({
|
|
40
|
+
mode: z.union(['user', 'localPlusSharing', 'workerDedicated']).default('user').volatile(),
|
|
41
|
+
env: z.union(['base-sepolia', 'base']).default('base-sepolia'),
|
|
42
|
+
port: z.natural().max(65535).default(47821).volatile(),
|
|
43
|
+
workers: z.array(z.string().min(1)).default([]).volatile(),
|
|
44
|
+
settle: z.boolean().default(true),
|
|
45
|
+
paywall: z.boolean().default(true),
|
|
46
|
+
keyDir: z.string().min(1).default(join(homedir(), '.dsh', 'genkaku')),
|
|
47
|
+
catalog: z.array(catalogEntry).default([]),
|
|
48
|
+
// IPv4 and IPv6 need separate binds (a /ip6/::/ bind is v6-only), and a
|
|
49
|
+
// global IPv6 address means a directly dialable worker with no NAT at all —
|
|
50
|
+
// the cheapest way for a dorm/public-wifi peer to be reachable (P4.9j).
|
|
51
|
+
listen: z.array(z.string().min(1)).default(['/ip4/0.0.0.0/tcp/4001', '/ip6/::/tcp/4002']),
|
|
52
|
+
publishAddress: z.boolean().default(false),
|
|
53
|
+
workerName: z.string().min(1).default('dsh-genkaku'),
|
|
54
|
+
spawnOllama: z.boolean().default(true),
|
|
55
|
+
ollamaHost: z.string().pattern(/^https?:\/\//).default('http://127.0.0.1:11434'),
|
|
56
|
+
});
|
|
57
|
+
/**
|
|
58
|
+
* Start the side that this machine's role allows. The controller reads the
|
|
59
|
+
* volatile mode once at startup; startup is awaited directly so a failure
|
|
60
|
+
* rejects the plugin, and only a successful start registers teardown.
|
|
61
|
+
*/
|
|
62
|
+
/**
|
|
63
|
+
* Build one DeepSeek Harness plugin from this runtime.
|
|
64
|
+
*
|
|
65
|
+
* The worker and user adapters are separate packages, but they share one
|
|
66
|
+
* implementation: what differs is the plugin id, the settings namespace, and
|
|
67
|
+
* the role it is allowed to run. Returns the exact shape dsh expects from a
|
|
68
|
+
* plugin module (`name`, `inject`, `Config`, `apply`).
|
|
69
|
+
*
|
|
70
|
+
* @param options.pluginName - cordis plugin name, e.g. `genkaku-worker`.
|
|
71
|
+
* @param options.settingsNs - settings document id this adapter owns.
|
|
72
|
+
* @param options.role - serving or consuming; overrides any stored mode.
|
|
73
|
+
*/
|
|
74
|
+
export function defineGenkakuPlugin(options) {
|
|
75
|
+
const { pluginName, settingsNs, role, injectExtra = [], onStarted } = options;
|
|
76
|
+
async function apply(ctx, config) {
|
|
77
|
+
ctx.inject(['settings'], (child) => { child.effect(() => child.settings.configure({ auto: false }, ctx.fiber)); });
|
|
78
|
+
const log = ctx.logger(pluginName);
|
|
79
|
+
const keyDir = config.keyDir;
|
|
80
|
+
// rpc.ts keeps its probe cache next to the rest of the GenKaKu state; the
|
|
81
|
+
// module reads this once per call, so exporting the resolved dir is enough.
|
|
82
|
+
process.env.GENKAKU_KEY_DIR = keyDir;
|
|
83
|
+
// dsh exposes no safeStorage/keyring codec, so createKeyVault falls back to a
|
|
84
|
+
// plaintext pass-through. Say so at startup: signing keys stay unencrypted.
|
|
85
|
+
log.warn('%s key vault at %s is PLAINTEXT (no key codec available on this host); '
|
|
86
|
+
+ 'user/worker signing keys are stored unencrypted — restrict the directory permissions', pluginName, keyDir);
|
|
87
|
+
const vault = createKeyVault({ dir: keyDir });
|
|
88
|
+
const modeManager = new ModeManager();
|
|
89
|
+
const runtime = new GenkakuRuntime(ctx, config, vault, modeManager, log, keyDir, role, settingsNs);
|
|
90
|
+
try {
|
|
91
|
+
await runtime.applyMode();
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
await runtime.dispose();
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
// Live edits to the volatile startup fields restart the owning half on demand.
|
|
98
|
+
ctx.effect(() => ctx.on('settings/document-updated', (ns) => {
|
|
99
|
+
if (String(ns) === settingsNs)
|
|
100
|
+
void runtime.reconfigure();
|
|
101
|
+
}), `${pluginName}: settings updates`);
|
|
102
|
+
// Startup is complete, so the disposer body stays synchronous: it returns
|
|
103
|
+
// the async teardown for Cordis to await at unload.
|
|
104
|
+
ctx.effect(() => () => runtime.dispose(), `${pluginName}: bridge and sidecar lifecycle`);
|
|
105
|
+
onStarted?.(ctx, config);
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
name: pluginName,
|
|
109
|
+
inject: ['settings', ...injectExtra],
|
|
110
|
+
Config,
|
|
111
|
+
apply,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** Owns the running halves so the mode starter and teardown share one state. */
|
|
115
|
+
export class GenkakuRuntime {
|
|
116
|
+
ctx;
|
|
117
|
+
config;
|
|
118
|
+
vault;
|
|
119
|
+
modeManager;
|
|
120
|
+
log;
|
|
121
|
+
keyDir;
|
|
122
|
+
forcedMode;
|
|
123
|
+
settingsNs;
|
|
124
|
+
node = null;
|
|
125
|
+
ollama = null;
|
|
126
|
+
stop = null;
|
|
127
|
+
statusServer = null;
|
|
128
|
+
applied = null;
|
|
129
|
+
/** Worker-side address watcher: advertise only when the announced set moves. */
|
|
130
|
+
advertiseState = null;
|
|
131
|
+
advertiseTimer = null;
|
|
132
|
+
reconfigureQueue = Promise.resolve();
|
|
133
|
+
modeTicker = null;
|
|
134
|
+
pendingMode = null;
|
|
135
|
+
modeUpdating = false;
|
|
136
|
+
constructor(ctx, config, vault, modeManager, log, keyDir,
|
|
137
|
+
/**
|
|
138
|
+
* Mode this adapter is allowed to run. The worker plugin pins a serving
|
|
139
|
+
* mode and the user plugin pins `user`, so installing both cannot make
|
|
140
|
+
* them fight over the same peer identity and listen ports.
|
|
141
|
+
*/
|
|
142
|
+
forcedMode,
|
|
143
|
+
/** Settings document id; defaults to the single-plugin namespace. */
|
|
144
|
+
settingsNs = DEFAULT_SETTINGS_NS) {
|
|
145
|
+
this.ctx = ctx;
|
|
146
|
+
this.config = config;
|
|
147
|
+
this.vault = vault;
|
|
148
|
+
this.modeManager = modeManager;
|
|
149
|
+
this.log = log;
|
|
150
|
+
this.keyDir = keyDir;
|
|
151
|
+
this.forcedMode = forcedMode;
|
|
152
|
+
this.settingsNs = settingsNs;
|
|
153
|
+
}
|
|
154
|
+
/** Read the configured mode once, switch the guard, and start its half. */
|
|
155
|
+
async applyMode() {
|
|
156
|
+
this.startModeTicker();
|
|
157
|
+
const mode = this.forcedMode ?? this.config.mode.get();
|
|
158
|
+
this.modeManager.switch(mode);
|
|
159
|
+
if (mode === 'user')
|
|
160
|
+
await this.startUser();
|
|
161
|
+
else
|
|
162
|
+
await this.startWorker();
|
|
163
|
+
this.applied = this.startupState(mode);
|
|
164
|
+
}
|
|
165
|
+
/** Store the latest requested mode; the runtime ticker applies it. */
|
|
166
|
+
scheduleMode(mode) {
|
|
167
|
+
this.pendingMode = { mode, retries: 4 };
|
|
168
|
+
modeLog('queued mode=' + mode);
|
|
169
|
+
}
|
|
170
|
+
/** Persistent 300 ms pump, created outside request contexts. */
|
|
171
|
+
startModeTicker() {
|
|
172
|
+
if (this.modeTicker != null)
|
|
173
|
+
return;
|
|
174
|
+
this.modeTicker = setInterval(() => { this.flushMode(); }, 300);
|
|
175
|
+
this.modeTicker.unref();
|
|
176
|
+
}
|
|
177
|
+
stopModeTicker() {
|
|
178
|
+
if (this.modeTicker != null) {
|
|
179
|
+
clearInterval(this.modeTicker);
|
|
180
|
+
this.modeTicker = null;
|
|
181
|
+
}
|
|
182
|
+
this.pendingMode = null;
|
|
183
|
+
}
|
|
184
|
+
/** Apply the latest pending mode, retrying only mid-reload failures. */
|
|
185
|
+
flushMode() {
|
|
186
|
+
if (this.modeUpdating || this.pendingMode == null)
|
|
187
|
+
return;
|
|
188
|
+
const pending = this.pendingMode;
|
|
189
|
+
this.pendingMode = null;
|
|
190
|
+
this.modeUpdating = true;
|
|
191
|
+
void this.ctx.settings.update(this.settingsNs, { mode: pending.mode }).then(() => {
|
|
192
|
+
this.modeUpdating = false;
|
|
193
|
+
modeLog('applied mode=' + pending.mode);
|
|
194
|
+
}).catch((error) => {
|
|
195
|
+
this.modeUpdating = false;
|
|
196
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
197
|
+
if (/nested|hmr/i.test(message) && pending.retries > 0 && this.pendingMode == null) {
|
|
198
|
+
this.pendingMode = { mode: pending.mode, retries: pending.retries - 1 };
|
|
199
|
+
modeLog('retry ' + (5 - pending.retries) + ' after ' + message);
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
modeLog('mode switch error ' + message);
|
|
203
|
+
this.log.warn('mode switch failed (%s)', message);
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
/** Serialize settings-driven restarts; each call waits for the previous one. */
|
|
207
|
+
reconfigure() {
|
|
208
|
+
modeLog('request');
|
|
209
|
+
const run = this.reconfigureQueue.then(() => this.reconfigureNow());
|
|
210
|
+
this.reconfigureQueue = run.catch((error) => {
|
|
211
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
212
|
+
modeLog('error ' + message);
|
|
213
|
+
this.log.warn('reconfigure failed (%s)', message);
|
|
214
|
+
});
|
|
215
|
+
return this.reconfigureQueue;
|
|
216
|
+
}
|
|
217
|
+
/** Restart only when a startup field changed; otherwise keep serving. */
|
|
218
|
+
async reconfigureNow() {
|
|
219
|
+
const mode = this.forcedMode ?? this.config.mode.get();
|
|
220
|
+
const next = this.startupState(mode);
|
|
221
|
+
modeLog('attempt mode=' + mode + ' class=' + next.modeClass + ' port=' + next.port);
|
|
222
|
+
const changed = [];
|
|
223
|
+
if (this.applied == null) {
|
|
224
|
+
changed.push('initial');
|
|
225
|
+
}
|
|
226
|
+
else {
|
|
227
|
+
if (this.applied.modeClass !== next.modeClass)
|
|
228
|
+
changed.push('modeClass');
|
|
229
|
+
if (this.applied.port !== next.port)
|
|
230
|
+
changed.push('port');
|
|
231
|
+
if (this.applied.workers !== next.workers)
|
|
232
|
+
changed.push('workers');
|
|
233
|
+
}
|
|
234
|
+
if (changed.length === 0) {
|
|
235
|
+
modeLog('skip unchanged class=' + next.modeClass + ' port=' + next.port);
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
modeLog('restart changed=' + changed.join(',') + ' to=' + next.modeClass + ':' + next.port);
|
|
239
|
+
this.log.info('settings changed; restarting the %s half', next.modeClass);
|
|
240
|
+
await this.stopServices();
|
|
241
|
+
this.modeManager.switch(mode);
|
|
242
|
+
if (mode === 'user')
|
|
243
|
+
await this.startUser();
|
|
244
|
+
else
|
|
245
|
+
await this.startWorker();
|
|
246
|
+
this.applied = next;
|
|
247
|
+
modeLog('restarted class=' + next.modeClass + ' port=' + next.port);
|
|
248
|
+
}
|
|
249
|
+
startupState(mode) {
|
|
250
|
+
return {
|
|
251
|
+
modeClass: mode === 'user' ? 'user' : 'worker',
|
|
252
|
+
port: this.config.port.get(),
|
|
253
|
+
workers: [...this.config.workers.get()].join('\u0000'),
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/** User half: a bare libp2p dial node plus the OpenAI-compatible bridge. */
|
|
257
|
+
async startUser() {
|
|
258
|
+
const userNode = await createUserNode();
|
|
259
|
+
this.node = userNode;
|
|
260
|
+
void probeRpc(this.config.env, this.log);
|
|
261
|
+
const configuredWorkers = [...this.config.workers.get()];
|
|
262
|
+
const env = this.config.env;
|
|
263
|
+
const publicC = publicClientFor(env);
|
|
264
|
+
// Chain directory first (P4.9b): a cold client with an empty config needs
|
|
265
|
+
// nothing but a public Base RPC. A read failure falls back to the cached
|
|
266
|
+
// directory, then to the seed list only if one was explicitly configured.
|
|
267
|
+
let discovered = [];
|
|
268
|
+
try {
|
|
269
|
+
await syncDirectory(publicC, env, this.keyDir, this.log);
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
this.log.warn('directory: chain scan failed (%s) — using the cached directory', error instanceof Error ? error.message : String(error));
|
|
273
|
+
}
|
|
274
|
+
discovered = workerAddrs(loadDirectory(this.keyDir, env));
|
|
275
|
+
if (discovered.length === 0 && HARDCODED_SEEDS.length > 0) {
|
|
276
|
+
this.log.info('directory: empty — falling back to the configured bootstrap seed(s)');
|
|
277
|
+
discovered = await discoverWorkers(userNode, this.log);
|
|
278
|
+
}
|
|
279
|
+
const workers = [...new Set([...configuredWorkers, ...discovered])];
|
|
280
|
+
if (workers.length === 0) {
|
|
281
|
+
this.log.warn('no workers configured or discovered; /v1/models will stay empty');
|
|
282
|
+
}
|
|
283
|
+
// P4.9h: behavioural credit from escrow events. Boot refresh is
|
|
284
|
+
// fire-and-forget — an empty index only means offers show their own claim
|
|
285
|
+
// until the first scan lands.
|
|
286
|
+
const credit = new CreditTracker(publicC, env);
|
|
287
|
+
void credit.refresh().catch((error) => {
|
|
288
|
+
this.log.warn('credit: initial scan failed (%s)', error instanceof Error ? error.message : String(error));
|
|
289
|
+
});
|
|
290
|
+
const creditTimer = setInterval(() => {
|
|
291
|
+
void credit.refresh().catch(() => { });
|
|
292
|
+
}, 60_000);
|
|
293
|
+
// Funding facts for the user marketplace panel. The status page polls every
|
|
294
|
+
// few seconds, so the two reads are cached briefly and a failure only omits
|
|
295
|
+
// the balance instead of failing the whole status response.
|
|
296
|
+
const userWallet = walletAddress(this.vault.userKey());
|
|
297
|
+
let balanceCache = null;
|
|
298
|
+
const balances = async () => {
|
|
299
|
+
if (balanceCache != null && Date.now() - balanceCache.at < 20_000)
|
|
300
|
+
return balanceCache;
|
|
301
|
+
try {
|
|
302
|
+
const value = await readWalletBalances(publicC, env, userWallet);
|
|
303
|
+
balanceCache = { at: Date.now(), ...value };
|
|
304
|
+
return balanceCache;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
this.log.warn('balances: read failed (%s)', error instanceof Error ? error.message : String(error));
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const bridge = await startOpenAiBridge({
|
|
312
|
+
workers,
|
|
313
|
+
port: this.config.port.get(),
|
|
314
|
+
env: this.config.env,
|
|
315
|
+
modeManager: this.modeManager,
|
|
316
|
+
getNode: async () => userNode,
|
|
317
|
+
userKey: () => this.vault.userKey(),
|
|
318
|
+
settle: this.config.settle,
|
|
319
|
+
onMode: (mode) => { this.scheduleMode(mode); },
|
|
320
|
+
credit: (wallets) => {
|
|
321
|
+
const out = new Map();
|
|
322
|
+
for (const wallet of wallets) {
|
|
323
|
+
const snap = credit.snapshot(wallet);
|
|
324
|
+
if (snap != null) {
|
|
325
|
+
out.set(wallet.toLowerCase(), { score: snap.score, sessions: snap.sessions });
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
return out;
|
|
329
|
+
},
|
|
330
|
+
log: line => this.log.info('%s', line),
|
|
331
|
+
status: async () => {
|
|
332
|
+
const value = await balances();
|
|
333
|
+
return {
|
|
334
|
+
env,
|
|
335
|
+
settle: this.config.settle,
|
|
336
|
+
...(value == null ? {} : { balances: { eth: value.eth.toString(), usdc: value.usdc.toString() } }),
|
|
337
|
+
};
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
this.log.info('openai bridge listening at %s', bridge.url);
|
|
341
|
+
this.stop = async () => {
|
|
342
|
+
clearInterval(creditTimer);
|
|
343
|
+
await bridge.close();
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
/** Worker half: the libp2p sidecar, its status dashboard, and Ollama. */
|
|
347
|
+
async startWorker() {
|
|
348
|
+
this.ollama = createOllamaSupervisor(this.config.ollamaHost, line => this.log.info('%s', line));
|
|
349
|
+
if (this.config.spawnOllama) {
|
|
350
|
+
const state = await this.ollama.ensureRunning();
|
|
351
|
+
if (state.running)
|
|
352
|
+
this.log.info('ollama %s ready at %s', state.version ?? '', state.host);
|
|
353
|
+
else {
|
|
354
|
+
this.log.warn('ollama is %s — the worker can start, but it cannot serve until onboarding finishes', state.installed ? `installed but not running (${state.error ?? 'no response'})` : 'not installed');
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
const workerKey = this.vault.workerKey();
|
|
358
|
+
const workerWallet = this.vault.addresses().worker;
|
|
359
|
+
let workerCatalog = loadCatalog(this.keyDir, this.config.catalog, this.log);
|
|
360
|
+
const paywall = this.config.paywall
|
|
361
|
+
? createPaywall({
|
|
362
|
+
publicC: publicClientFor(this.config.env),
|
|
363
|
+
env: this.config.env,
|
|
364
|
+
worker: workerWallet,
|
|
365
|
+
log: this.log,
|
|
366
|
+
})
|
|
367
|
+
: undefined;
|
|
368
|
+
if (paywall == null) {
|
|
369
|
+
this.log.warn('paywall DISABLED — anyone who can dial this worker gets free inference');
|
|
370
|
+
}
|
|
371
|
+
const sidecar = await createSidecar({
|
|
372
|
+
paywall,
|
|
373
|
+
modeManager: this.modeManager,
|
|
374
|
+
source: createOllamaSource(this.config.ollamaHost),
|
|
375
|
+
// Serves open/1 sessions: chat messages plus tool schemas.
|
|
376
|
+
chatSource: createOllamaChatSource(this.config.ollamaHost),
|
|
377
|
+
listen: this.config.listen,
|
|
378
|
+
catalog: catalogOf(this.config.workerName, workerCatalog),
|
|
379
|
+
workerKey,
|
|
380
|
+
sessionEvents: (event) => {
|
|
381
|
+
if (event.type === 'session-error')
|
|
382
|
+
this.log.warn('%s', event.message);
|
|
383
|
+
else if (event.type === 'session-end')
|
|
384
|
+
this.log.info('session %s ended (%d tokens)', event.sessionId, event.tokens);
|
|
385
|
+
},
|
|
386
|
+
});
|
|
387
|
+
const bootAddrs = sidecar.peerAddrs();
|
|
388
|
+
// The settings page writes a per-machine opt-in; the deployment default
|
|
389
|
+
// (config.publishAddress) still forces it on when a release says so.
|
|
390
|
+
const publish = this.config.publishAddress || readPublishOptIn(this.keyDir);
|
|
391
|
+
const canPublish = publish && hasPublicAddr(bootAddrs);
|
|
392
|
+
if (canPublish) {
|
|
393
|
+
void registerBinding(this.config.env, this.keyDir, workerWallet, workerKey, sidecar.peerIdStr(), locatorOf(bootAddrs), this.log);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
// §39: a LAN-only peer cannot be dialed from the internet, so publishing
|
|
397
|
+
// it into the global directory only creates dead entries. It can still
|
|
398
|
+
// serve peers on the same network, and settlement is unaffected.
|
|
399
|
+
this.log.warn(hasPublicAddr(bootAddrs)
|
|
400
|
+
? 'worker has a public address but publishAddress=false — staying out of the chain directory (privacy; enable publishAddress to be found)'
|
|
401
|
+
: 'worker has no public address (%s) — LAN-only: staying out of the chain directory; only peers on this network can dial it', hasPublicAddr(bootAddrs) ? undefined : bootAddrs.join(', '));
|
|
402
|
+
void retireStaleEntry(this.config.env, workerWallet, workerKey, sidecar.peerIdStr(), this.log);
|
|
403
|
+
}
|
|
404
|
+
void probeRpc(this.config.env, this.log);
|
|
405
|
+
this.statusServer = await startStatusServer(this.config.port.get(), () => {
|
|
406
|
+
const liveAddrs = sidecar.peerAddrs();
|
|
407
|
+
return {
|
|
408
|
+
role: this.modeManager.current().mode,
|
|
409
|
+
peerId: sidecar.peerIdStr(),
|
|
410
|
+
peerAddrs: liveAddrs,
|
|
411
|
+
wallet: workerWallet,
|
|
412
|
+
offers: catalogOf(this.config.workerName, workerCatalog).map(offer => ({
|
|
413
|
+
addr: liveAddrs[0],
|
|
414
|
+
offer: { ...toWire(offer), workerWallet },
|
|
415
|
+
})),
|
|
416
|
+
activeSessions: sidecar.activeSessions(),
|
|
417
|
+
reachable: hasPublicAddr(liveAddrs),
|
|
418
|
+
publishAddress: publish,
|
|
419
|
+
};
|
|
420
|
+
}, (catalog) => {
|
|
421
|
+
workerCatalog = entriesFromInput(catalog);
|
|
422
|
+
sidecar.setCatalog(catalogOf(this.config.workerName, workerCatalog));
|
|
423
|
+
persistCatalog(this.keyDir, workerCatalog, this.log);
|
|
424
|
+
this.log.info('catalog: saved %d offer(s) to %s', workerCatalog.length, join(this.keyDir, 'catalog.json'));
|
|
425
|
+
}, (mode) => { this.scheduleMode(mode); }, this.ollama, (next) => {
|
|
426
|
+
writePublishOptIn(this.keyDir, next);
|
|
427
|
+
this.log.info('directory publishing %s; restarting the worker half', next ? 'enabled' : 'disabled');
|
|
428
|
+
this.scheduleMode(this.modeManager.current().mode);
|
|
429
|
+
});
|
|
430
|
+
this.advertiseState = {
|
|
431
|
+
worker: sidecar,
|
|
432
|
+
wallet: workerWallet,
|
|
433
|
+
key: workerKey,
|
|
434
|
+
env: this.config.env,
|
|
435
|
+
publish,
|
|
436
|
+
applied: canPublish ? locatorOf(bootAddrs) : '',
|
|
437
|
+
pending: null,
|
|
438
|
+
retracting: false,
|
|
439
|
+
};
|
|
440
|
+
this.startAdvertiseWatch();
|
|
441
|
+
this.log.info('worker %s listening on %s', sidecar.peerIdStr(), sidecar.peerAddrs().join(', '));
|
|
442
|
+
this.log.info('worker dashboard at %s', this.statusServer.url);
|
|
443
|
+
this.stop = sidecar.stop;
|
|
444
|
+
}
|
|
445
|
+
/** Poll the announce set; advertise only after it has been stable for two
|
|
446
|
+
* checks. A worker's listen addrs flap while interfaces come up (Wi-Fi,
|
|
447
|
+
* VPN, sleep/wake) and every advertise is a chain write — this turns a
|
|
448
|
+
* flap into zero transactions instead of a burst of them. */
|
|
449
|
+
startAdvertiseWatch() {
|
|
450
|
+
this.stopAdvertiseWatch();
|
|
451
|
+
const timer = setInterval(() => { void this.checkAdvertise(); }, 30_000);
|
|
452
|
+
timer.unref?.();
|
|
453
|
+
this.advertiseTimer = timer;
|
|
454
|
+
}
|
|
455
|
+
stopAdvertiseWatch() {
|
|
456
|
+
if (this.advertiseTimer != null)
|
|
457
|
+
clearInterval(this.advertiseTimer);
|
|
458
|
+
this.advertiseTimer = null;
|
|
459
|
+
this.advertiseState = null;
|
|
460
|
+
}
|
|
461
|
+
async checkAdvertise() {
|
|
462
|
+
const state = this.advertiseState;
|
|
463
|
+
if (state == null)
|
|
464
|
+
return;
|
|
465
|
+
let current = '';
|
|
466
|
+
try {
|
|
467
|
+
const addrs = state.worker.peerAddrs();
|
|
468
|
+
// Either LAN-only (public wifi, CGNAT, UPnP off) or publishing is off:
|
|
469
|
+
// nothing on-chain should point here. If it used to, retract once.
|
|
470
|
+
if (!state.publish || !hasPublicAddr(addrs)) {
|
|
471
|
+
state.pending = null;
|
|
472
|
+
if (state.applied !== '' && !state.retracting) {
|
|
473
|
+
state.retracting = true;
|
|
474
|
+
try {
|
|
475
|
+
const tx = await deactivateWorker(walletClientFor(state.env, state.key), state.env);
|
|
476
|
+
registryLog('deactivate wallet=' + state.wallet + ' tx=' + tx);
|
|
477
|
+
this.log.info('worker lost public reachability — removed from the chain directory (tx %s)', tx);
|
|
478
|
+
state.applied = '';
|
|
479
|
+
}
|
|
480
|
+
catch (error) {
|
|
481
|
+
this.log.warn('worker retract failed (%s)', error instanceof Error ? error.message : String(error));
|
|
482
|
+
}
|
|
483
|
+
finally {
|
|
484
|
+
state.retracting = false;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
current = locatorOf(addrs);
|
|
490
|
+
}
|
|
491
|
+
catch {
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
if (current === '' || current === state.applied) {
|
|
495
|
+
state.pending = null;
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
498
|
+
// first sighting: remember it; second identical sighting: write
|
|
499
|
+
if (state.pending !== current) {
|
|
500
|
+
state.pending = current;
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
try {
|
|
504
|
+
const tx = await advertiseWorker(walletClientFor(state.env, state.key), state.env, current);
|
|
505
|
+
state.applied = current;
|
|
506
|
+
state.pending = null;
|
|
507
|
+
registryLog('advertise wallet=' + state.wallet + ' tx=' + tx + ' locator=' + current);
|
|
508
|
+
this.log.info('worker locator updated on-chain (tx %s)', tx);
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
state.pending = null;
|
|
512
|
+
this.log.warn('worker advertise failed (%s)', error instanceof Error ? error.message : String(error));
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
/** Stop the running halves without touching the lifetime mode ticker. */
|
|
516
|
+
async stopServices() {
|
|
517
|
+
this.stopAdvertiseWatch();
|
|
518
|
+
await Promise.resolve(this.statusServer?.close()).catch(() => { });
|
|
519
|
+
await Promise.resolve(this.stop?.()).catch(() => { });
|
|
520
|
+
await Promise.resolve(this.node?.stop()).catch(() => { });
|
|
521
|
+
this.ollama?.dispose();
|
|
522
|
+
this.ollama = null;
|
|
523
|
+
}
|
|
524
|
+
/** Shut down: stop services and the one lifetime mode ticker. */
|
|
525
|
+
async stopAll() {
|
|
526
|
+
this.stopModeTicker();
|
|
527
|
+
await this.stopServices();
|
|
528
|
+
}
|
|
529
|
+
/** Plugin-lifecycle shutdown alias. */
|
|
530
|
+
async dispose() {
|
|
531
|
+
await this.stopAll();
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/** Seed peers are matchmakers: their peer list is the fallback worker set. */
|
|
535
|
+
async function discoverWorkers(node, log) {
|
|
536
|
+
const peers = new Set();
|
|
537
|
+
await Promise.all(HARDCODED_SEEDS.map(async (seed) => {
|
|
538
|
+
try {
|
|
539
|
+
for (const peer of await fetchSeedPeers(node, seed))
|
|
540
|
+
peers.add(peer);
|
|
541
|
+
}
|
|
542
|
+
catch (error) {
|
|
543
|
+
log.warn('seed %s unreachable (%s)', seed, error instanceof Error ? error.message : String(error));
|
|
544
|
+
}
|
|
545
|
+
}));
|
|
546
|
+
return [...peers];
|
|
547
|
+
}
|
|
548
|
+
const REGISTRY_LOG = '/tmp/genkaku-registry.log';
|
|
549
|
+
/** Best-effort registry diagnostics; a log failure never affects registration. */
|
|
550
|
+
function registryLog(line) {
|
|
551
|
+
try {
|
|
552
|
+
appendFileSync(REGISTRY_LOG, new Date().toISOString() + ' ' + line + String.fromCharCode(10));
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
// best effort
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
const MODE_LOG = '/tmp/genkaku-mode.log';
|
|
559
|
+
/** Best-effort mode-switch diagnostics; a log failure never affects reconfigure. */
|
|
560
|
+
function modeLog(line) {
|
|
561
|
+
try {
|
|
562
|
+
appendFileSync(MODE_LOG, new Date().toISOString() + ' ' + line + String.fromCharCode(10));
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
// best effort
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/** Best-effort WorkerRegistry pin: read/register failures only log, never stop serving. */
|
|
569
|
+
const workerDigest = (peerId) => peerIdDigestOf(peerId);
|
|
570
|
+
/** Boot-time cleanup: a worker that is now LAN-only may still own a directory
|
|
571
|
+
* entry from a previous network. Retract it — but only when the on-chain
|
|
572
|
+
* binding is OURS, never someone else's. */
|
|
573
|
+
async function retireStaleEntry(env, wallet, workerKey, peerId, log) {
|
|
574
|
+
try {
|
|
575
|
+
const record = await readWorker(publicClientFor(env), env, wallet);
|
|
576
|
+
if (record == null || !record.active)
|
|
577
|
+
return;
|
|
578
|
+
if (record.peerId !== workerDigest(peerId))
|
|
579
|
+
return;
|
|
580
|
+
const tx = await deactivateWorker(walletClientFor(env, workerKey), env);
|
|
581
|
+
registryLog('retire wallet=' + wallet + ' tx=' + tx);
|
|
582
|
+
log.info('worker is LAN-only — stale directory entry retired (tx %s)', tx);
|
|
583
|
+
}
|
|
584
|
+
catch (error) {
|
|
585
|
+
registryLog('retire-error wallet=' + wallet + ' ' + (error instanceof Error ? error.message : String(error)));
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
async function registerBinding(env, keyDir, wallet, workerKey, peerId, locator, log) {
|
|
589
|
+
registryLog('start wallet=' + wallet + ' peer=' + peerId + ' locator=' + locator);
|
|
590
|
+
try {
|
|
591
|
+
const peerIdDigest = peerIdDigestOf(peerId);
|
|
592
|
+
const publicC = publicClientFor(env);
|
|
593
|
+
const existing = await readWorker(publicC, env, wallet);
|
|
594
|
+
registryLog('record wallet=' + wallet + ' peer=' + (existing == null ? 'none' : existing.peerId));
|
|
595
|
+
// active=false means a previous selfDeactivate tombstoned this wallet;
|
|
596
|
+
// advertise() would revert (NotRegistered), so re-register instead.
|
|
597
|
+
if (existing != null && existing.peerId === peerIdDigest && existing.active) {
|
|
598
|
+
// Binding is already right; only the address may have moved. The local
|
|
599
|
+
// directory cache answers this for free in the common case; the chain is
|
|
600
|
+
// only consulted when the cache has never seen this wallet.
|
|
601
|
+
const cached = loadDirectory(keyDir, env).entries
|
|
602
|
+
.find(entry => entry.wallet.toLowerCase() === wallet.toLowerCase());
|
|
603
|
+
const onChain = cached != null
|
|
604
|
+
? []
|
|
605
|
+
: await readWorkerLocators(publicC, env, { wallet });
|
|
606
|
+
const advertised = cached?.locator ?? onChain[0]?.locator;
|
|
607
|
+
if (locator === '' || advertised === locator) {
|
|
608
|
+
registryLog('skip wallet=' + wallet + ' already pins ' + peerId);
|
|
609
|
+
log.info('worker registry already pins %s', peerId);
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
const tx = await advertiseWorker(walletClientFor(env, workerKey), env, locator);
|
|
613
|
+
registryLog('advertise wallet=' + wallet + ' tx=' + tx);
|
|
614
|
+
log.info('worker locator published for %s (tx %s)', peerId, tx);
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
registryLog('register wallet=' + wallet + ' ' + (existing == null ? 'new' : 'rebind ' + existing.peerId));
|
|
618
|
+
const tx = await registerWorkerWithLocator(walletClientFor(env, workerKey), env, {
|
|
619
|
+
peerIdDigest,
|
|
620
|
+
policyHash: policyHashOf(POLICY_VERSION),
|
|
621
|
+
locator,
|
|
622
|
+
});
|
|
623
|
+
registryLog('tx wallet=' + wallet + ' hash=' + tx);
|
|
624
|
+
log.info('worker registry bound %s to %s (tx %s)', peerId, wallet, tx);
|
|
625
|
+
}
|
|
626
|
+
catch (error) {
|
|
627
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
628
|
+
registryLog('error wallet=' + wallet + ' ' + message);
|
|
629
|
+
log.warn('worker registry registration failed (%s)', message);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/** Persisted catalog location inside the key directory. */
|
|
633
|
+
function catalogPath(keyDir) {
|
|
634
|
+
return join(keyDir, 'catalog.json');
|
|
635
|
+
}
|
|
636
|
+
/** Read <keyDir>/catalog.json; a missing or invalid file falls back to config.catalog. */
|
|
637
|
+
function loadCatalog(keyDir, fallback, log) {
|
|
638
|
+
const file = catalogPath(keyDir);
|
|
639
|
+
try {
|
|
640
|
+
const entries = normalizeCatalog(JSON.parse(readFileSync(file, 'utf8')));
|
|
641
|
+
if (entries != null)
|
|
642
|
+
return entries;
|
|
643
|
+
log.warn('catalog: %s is not a valid catalog — using config.catalog', file);
|
|
644
|
+
}
|
|
645
|
+
catch (error) {
|
|
646
|
+
if (error.code !== 'ENOENT') {
|
|
647
|
+
log.warn('catalog: cannot read %s (%s) — using config.catalog', file, error instanceof Error ? error.message : String(error));
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
return fallback;
|
|
651
|
+
}
|
|
652
|
+
/** Accept the persisted array or a POST-shaped {catalog: [...]} object. */
|
|
653
|
+
function normalizeCatalog(raw) {
|
|
654
|
+
const rows = Array.isArray(raw) ? raw : raw?.catalog;
|
|
655
|
+
if (!Array.isArray(rows))
|
|
656
|
+
return null;
|
|
657
|
+
const entries = [];
|
|
658
|
+
for (const row of rows) {
|
|
659
|
+
if (typeof row !== 'object' || row === null)
|
|
660
|
+
return null;
|
|
661
|
+
const model = row.model;
|
|
662
|
+
if (typeof model !== 'string' || model.trim() === '')
|
|
663
|
+
return null;
|
|
664
|
+
const rawPrice = row.pricePerHour;
|
|
665
|
+
const price = typeof rawPrice === 'number'
|
|
666
|
+
? String(Math.round(rawPrice))
|
|
667
|
+
: typeof rawPrice === 'string' && /^\d+$/.test(rawPrice.trim()) ? rawPrice.trim() : null;
|
|
668
|
+
if (price == null)
|
|
669
|
+
return null;
|
|
670
|
+
const entry = { model: model.trim(), pricePerHour: price };
|
|
671
|
+
const city = row.city;
|
|
672
|
+
if (typeof city === 'string')
|
|
673
|
+
entry.city = city;
|
|
674
|
+
const creditScore = row.creditScore;
|
|
675
|
+
if (typeof creditScore === 'number')
|
|
676
|
+
entry.creditScore = creditScore;
|
|
677
|
+
const sessionsServed = row.sessionsServed;
|
|
678
|
+
if (typeof sessionsServed === 'number')
|
|
679
|
+
entry.sessionsServed = sessionsServed;
|
|
680
|
+
const maxConcurrent = row.maxConcurrent;
|
|
681
|
+
if (typeof maxConcurrent === 'number')
|
|
682
|
+
entry.maxConcurrent = maxConcurrent;
|
|
683
|
+
entries.push(entry);
|
|
684
|
+
}
|
|
685
|
+
return entries;
|
|
686
|
+
}
|
|
687
|
+
/** POST /genkaku/catalog input (micro-USDC numbers) → persisted entries. */
|
|
688
|
+
function entriesFromInput(catalog) {
|
|
689
|
+
return catalog.map((row) => {
|
|
690
|
+
const entry = { model: row.model, pricePerHour: String(Math.round(row.pricePerHour)) };
|
|
691
|
+
if (typeof row.city === 'string')
|
|
692
|
+
entry.city = row.city;
|
|
693
|
+
if (typeof row.creditScore === 'number')
|
|
694
|
+
entry.creditScore = row.creditScore;
|
|
695
|
+
if (typeof row.sessionsServed === 'number')
|
|
696
|
+
entry.sessionsServed = row.sessionsServed;
|
|
697
|
+
if (typeof row.maxConcurrent === 'number')
|
|
698
|
+
entry.maxConcurrent = row.maxConcurrent;
|
|
699
|
+
return entry;
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
/** Best-effort catalog persistence; a write failure only logs. */
|
|
703
|
+
function persistCatalog(keyDir, entries, log) {
|
|
704
|
+
try {
|
|
705
|
+
mkdirSync(keyDir, { recursive: true });
|
|
706
|
+
writeFileSync(catalogPath(keyDir), JSON.stringify(entries, null, 2) + '\n');
|
|
707
|
+
}
|
|
708
|
+
catch (error) {
|
|
709
|
+
log.warn('catalog: persist failed (%s)', error instanceof Error ? error.message : String(error));
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
/** Catalog rows → the Offer shape createSidecar publishes (wallet added there). */
|
|
713
|
+
function catalogOf(workerName, entries) {
|
|
714
|
+
return entries.map(entry => ({
|
|
715
|
+
proto: 'offers/0',
|
|
716
|
+
workerName,
|
|
717
|
+
model: entry.model,
|
|
718
|
+
pricePerHour: BigInt(entry.pricePerHour),
|
|
719
|
+
city: entry.city,
|
|
720
|
+
creditScore: entry.creditScore ?? 50,
|
|
721
|
+
sessionsServed: entry.sessionsServed ?? 0,
|
|
722
|
+
maxConcurrent: entry.maxConcurrent,
|
|
723
|
+
}));
|
|
724
|
+
}
|
|
725
|
+
/** Best-effort local Ollama service; a failed spawn leaves an existing daemon alone. */
|
|
726
|
+
//# sourceMappingURL=runtime.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,62 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@genkaku/dsh-runtime",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "DeepSeek Harness adapter layer for the GenKaKu protocol runtime",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./package.json": "./package.json"
|
|
15
|
+
},
|
|
16
|
+
"files": [
|
|
17
|
+
"dist/**/*.js",
|
|
18
|
+
"dist/**/*.d.ts",
|
|
19
|
+
"dist/**/*.d.ts.map",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@deepseek-ai/cosmokit": "1.9.0",
|
|
25
|
+
"@deepseek-ai/schemastery": "^3.19.0",
|
|
26
|
+
"@genkaku/protocol": "0.1.1"
|
|
27
|
+
},
|
|
28
|
+
"peerDependencies": {
|
|
29
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
30
|
+
"@deepseek-ai/dsh-settings": "0.2.2-rc.1"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"@deepseek-ai/cordis": "^4.1.0",
|
|
34
|
+
"@deepseek-ai/dsh-settings": "0.2.2-rc.1",
|
|
35
|
+
"libp2p": "1.9.4",
|
|
36
|
+
"viem": "2.57.16"
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/mlaimail77/genkaku-plugin.git",
|
|
41
|
+
"directory": "packages/dsh-runtime"
|
|
42
|
+
},
|
|
43
|
+
"publishConfig": {
|
|
44
|
+
"access": "public"
|
|
45
|
+
},
|
|
46
|
+
"author": "GenKaKu Labs",
|
|
47
|
+
"homepage": "https://staging.genkaku.app",
|
|
48
|
+
"keywords": [
|
|
49
|
+
"deepseek-harness",
|
|
50
|
+
"dsh",
|
|
51
|
+
"dsh-plugin",
|
|
52
|
+
"p2p",
|
|
53
|
+
"libp2p",
|
|
54
|
+
"ollama",
|
|
55
|
+
"usdc",
|
|
56
|
+
"base"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsc -p tsconfig.json",
|
|
60
|
+
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
61
|
+
}
|
|
6
62
|
}
|
package/README.md
DELETED