threadwire 0.1.6 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +52 -0
- package/README.md +16 -5
- package/TELEGRAM-INGRESS.md +13 -3
- package/bin/isolated-runtime.js +5 -0
- package/bin/kimi-model-broker.js +5 -0
- package/bin/model-broker.js +5 -0
- package/docs/container-runtime.md +22 -0
- package/docs/isolated-provider-runtime.md +232 -0
- package/package.json +10 -3
- package/scripts/provider-shims/front-door.sh.template +18 -0
- package/scripts/verify-package.js +23 -1
- package/src/absolute-deadline.js +94 -0
- package/src/activity-log.js +3 -3
- package/src/cli.js +130 -25
- package/src/delegated-result-admission.js +1 -1
- package/src/docker-api.js +131 -0
- package/src/isolated-runtime-client.js +158 -0
- package/src/isolated-runtime.js +1117 -0
- package/src/isolated-state.js +470 -0
- package/src/isolated-worker.js +144 -0
- package/src/kimi-model-broker-policy.js +209 -0
- package/src/kimi-model-broker.js +325 -0
- package/src/kimi-oauth-store.js +267 -0
- package/src/model-broker-policy.js +139 -0
- package/src/model-broker.js +313 -0
- package/src/mount-policy.js +28 -0
- package/src/normalized-output.js +68 -0
- package/src/providers/index.js +8 -3
- package/src/providers/kimi.js +165 -0
- package/src/relay-write.js +44 -0
- package/src/telegram-ingress/command.js +4 -4
- package/src/telegram-ingress/config.js +18 -0
- package/src/telegram-ingress/core.js +65 -2
- package/src/telegram-ingress/http.js +2 -1
- package/src/telegram-webhook.js +11 -0
- package/src/workspace-profile.js +3 -3
- package/threadwire.workspace-profiles.json +1 -1
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
/* eslint-disable jsdoc/require-jsdoc */
|
|
3
|
+
|
|
4
|
+
import {deadlineAfter, abortable, readResponseCapped} from "./absolute-deadline.js"
|
|
5
|
+
|
|
6
|
+
const MAX_RESPONSE_BYTES = 2_097_152
|
|
7
|
+
const DEFAULT_TIMEOUT_MS = 3_660_000
|
|
8
|
+
const MAX_RAW_OUTPUT_BYTES = 1_048_576
|
|
9
|
+
|
|
10
|
+
export class IsolatedRuntimeClient {
|
|
11
|
+
/** @param {{url: string, controlToken: string, timeoutMs?: number | string, fetchImplementation?: typeof fetch}} options */
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.url = normalizedUrl(options.url)
|
|
14
|
+
this.controlToken = nonempty(options.controlToken, "isolated runtime control token")
|
|
15
|
+
this.fetch = options.fetchImplementation ?? fetch
|
|
16
|
+
this.timeoutMs = positiveDuration(options.timeoutMs, DEFAULT_TIMEOUT_MS)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @param {{provider: string, profile: string, repositoryRoot: string, cwd: string, providerArguments: string[], resumeSession?: string, signal?: AbortSignal, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}} request
|
|
21
|
+
* @returns {Promise<{preflightId: string, deadline?: import("./absolute-deadline.js").AbsoluteDeadline}>}
|
|
22
|
+
*/
|
|
23
|
+
async preflight(request) {
|
|
24
|
+
const deadline = request.deadline ?? deadlineAfter(this.timeoutMs, {signal: request.signal, timeoutMessage: "Isolated runtime launch timeout"})
|
|
25
|
+
const response = await this.request("/preflight", {...request, launchDeadlineAt: deadline.expiresAt}, deadline)
|
|
26
|
+
if (response.ok !== true || typeof response.preflightId !== "string") throw new Error("Isolated runtime preflight failed")
|
|
27
|
+
return {preflightId: response.preflightId, deadline}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* @param {{
|
|
32
|
+
* preflightId: string, prompt: string, providerArguments: string[],
|
|
33
|
+
* resumeSession?: string,
|
|
34
|
+
* signal?: AbortSignal,
|
|
35
|
+
* deadline?: import("./absolute-deadline.js").AbsoluteDeadline,
|
|
36
|
+
* onEvent: (event: import("./types.js").WorkerEvent) => void | Promise<void>,
|
|
37
|
+
* onRecord?: (record: unknown) => void | Promise<void>,
|
|
38
|
+
* onStdoutChunk?: (chunk: Buffer) => void | Promise<void>,
|
|
39
|
+
* onStderrChunk?: (chunk: Buffer) => void | Promise<void>
|
|
40
|
+
* }} options
|
|
41
|
+
*/
|
|
42
|
+
async run(options) {
|
|
43
|
+
const deadline = options.deadline ?? deadlineAfter(this.timeoutMs, {signal: options.signal, timeoutMessage: "Isolated runtime launch timeout"})
|
|
44
|
+
const response = await this.request("/run", {
|
|
45
|
+
preflightId: options.preflightId,
|
|
46
|
+
prompt: options.prompt,
|
|
47
|
+
providerArguments: options.providerArguments,
|
|
48
|
+
...(options.resumeSession === undefined ? {} : {resumeSession: options.resumeSession})
|
|
49
|
+
}, deadline)
|
|
50
|
+
if (!Array.isArray(response.records) || !Array.isArray(response.rawChunks) || !Number.isSafeInteger(response.exitCode)) throw new Error("Isolated runtime emitted an invalid response")
|
|
51
|
+
let rawBytes = 0
|
|
52
|
+
const decoded = response.rawChunks.map((item) => {
|
|
53
|
+
if (!isRecord(item) || Object.keys(item).sort().join(",") !== "channel,data"
|
|
54
|
+
|| (item.channel !== "stdout" && item.channel !== "stderr") || typeof item.data !== "string"
|
|
55
|
+
|| !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u.test(item.data)) {
|
|
56
|
+
throw new Error("Isolated runtime emitted an invalid response")
|
|
57
|
+
}
|
|
58
|
+
const data = Buffer.from(item.data, "base64")
|
|
59
|
+
rawBytes += data.length
|
|
60
|
+
if (rawBytes > MAX_RAW_OUTPUT_BYTES) throw new Error("Isolated runtime emitted an invalid response")
|
|
61
|
+
return {channel: item.channel, data}
|
|
62
|
+
})
|
|
63
|
+
for (const chunk of decoded) {
|
|
64
|
+
deadline.throwIfAborted()
|
|
65
|
+
const callback = chunk.channel === "stdout" ? options.onStdoutChunk : options.onStderrChunk
|
|
66
|
+
await abortable(callback?.(chunk.data), deadline.signal)
|
|
67
|
+
}
|
|
68
|
+
for (const record of response.records) {
|
|
69
|
+
deadline.throwIfAborted()
|
|
70
|
+
await abortable(options.onRecord?.(record), deadline.signal)
|
|
71
|
+
if (isWorkerEventEnvelope(record)) await abortable(options.onEvent(record.event), deadline.signal)
|
|
72
|
+
}
|
|
73
|
+
return /** @type {number} */ (response.exitCode)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @param {string} path @param {unknown} body */
|
|
77
|
+
async request(path, body, deadline) {
|
|
78
|
+
const signal = deadline.signal
|
|
79
|
+
const response = await abortable(this.fetch(`${this.url}${path}`, {
|
|
80
|
+
method: "POST",
|
|
81
|
+
redirect: "error",
|
|
82
|
+
headers: {
|
|
83
|
+
authorization: `Bearer ${this.controlToken}`,
|
|
84
|
+
"content-type": "application/json",
|
|
85
|
+
connection: "close",
|
|
86
|
+
"x-threadwire-launch-deadline": String(deadline.expiresAt)
|
|
87
|
+
},
|
|
88
|
+
body: JSON.stringify(body),
|
|
89
|
+
signal
|
|
90
|
+
}), signal)
|
|
91
|
+
let bytes
|
|
92
|
+
try {
|
|
93
|
+
bytes = await readResponseCapped(response, MAX_RESPONSE_BYTES, signal)
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error instanceof Error && error.message === "Response exceeded capacity") throw new Error("Isolated runtime response exceeded the configured capacity", {cause: error})
|
|
96
|
+
throw error
|
|
97
|
+
}
|
|
98
|
+
if (!response.ok) throw new Error(safeRemoteError(bytes))
|
|
99
|
+
try {
|
|
100
|
+
const value = JSON.parse(bytes.toString("utf8"))
|
|
101
|
+
if (!isRecord(value)) throw new Error()
|
|
102
|
+
return value
|
|
103
|
+
} catch {
|
|
104
|
+
throw new Error("Isolated runtime emitted an invalid response")
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function positiveDuration(value, fallback) {
|
|
110
|
+
if (value === undefined) return fallback
|
|
111
|
+
const number = Number(value)
|
|
112
|
+
if (!Number.isSafeInteger(number) || number < 1 || number > 604_800_000) throw new Error("Invalid isolated runtime client timeout")
|
|
113
|
+
return number
|
|
114
|
+
}
|
|
115
|
+
/** @param {NodeJS.ProcessEnv} environment */
|
|
116
|
+
export function isolatedRuntimeClientFromEnvironment(environment) {
|
|
117
|
+
return new IsolatedRuntimeClient({
|
|
118
|
+
url: nonempty(environment.THREADWIRE_ISOLATED_RUNTIME_URL, "THREADWIRE_ISOLATED_RUNTIME_URL"),
|
|
119
|
+
controlToken: nonempty(environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN"),
|
|
120
|
+
timeoutMs: environment.THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS
|
|
121
|
+
})
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** @param {NodeJS.ProcessEnv} environment */
|
|
125
|
+
export function kimiIsolatedRuntimeClientFromEnvironment(environment) {
|
|
126
|
+
return new IsolatedRuntimeClient({
|
|
127
|
+
url: nonempty(environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_URL, "THREADWIRE_KIMI_ISOLATED_RUNTIME_URL"),
|
|
128
|
+
controlToken: nonempty(environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN, "THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN"),
|
|
129
|
+
timeoutMs: environment.THREADWIRE_ISOLATED_RUNTIME_CLIENT_TIMEOUT_MS
|
|
130
|
+
})
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizedUrl(value) {
|
|
134
|
+
const url = new URL(value)
|
|
135
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") throw new Error("THREADWIRE_ISOLATED_RUNTIME_URL must use HTTP or HTTPS")
|
|
136
|
+
url.pathname = url.pathname.replace(/\/+$/u, "")
|
|
137
|
+
url.search = ""
|
|
138
|
+
url.hash = ""
|
|
139
|
+
return url.toString().replace(/\/$/u, "")
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function nonempty(value, name) {
|
|
143
|
+
if (typeof value !== "string" || value.trim().length === 0 || /[\r\n]/u.test(value)) throw new Error(`${name} is required`)
|
|
144
|
+
return value
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function safeRemoteError(bytes) {
|
|
148
|
+
const text = bytes.toString("utf8").trim()
|
|
149
|
+
return /^[\x20-\x7e]{1,200}$/u.test(text) ? text : "Isolated runtime request failed"
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isWorkerEventEnvelope(value) {
|
|
157
|
+
return isRecord(value) && value.type === "worker-event" && isRecord(value.event)
|
|
158
|
+
}
|