threadwire 0.1.12 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,7 @@
1
1
  // @ts-check
2
2
 
3
3
  import {createHash, randomUUID, timingSafeEqual} from "node:crypto"
4
+ import {abortable} from "../absolute-deadline.js"
4
5
  import {evidenceTelegramDestination} from "../evidence-store.js"
5
6
  import {NoticeQueue} from "../notice-queue.js"
6
7
  import {createFetchTransport} from "../notifiers/fetch-transport.js"
@@ -10,7 +11,6 @@ import {kimiSessionEnvelopeId} from "../providers/kimi.js"
10
11
  import {Relay} from "../relay.js"
11
12
  import {runWorker} from "../run-worker.js"
12
13
  import {WorkerControl} from "../worker-control.js"
13
- import {resolveWorkspaceProfile} from "../workspace-profile.js"
14
14
  import {buildProviderEnvironment, collectEvidenceRedactions} from "./config.js"
15
15
  import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
16
16
 
@@ -27,12 +27,12 @@ import {parseCodeCommand, parseEvidenceCommand} from "./command.js"
27
27
  * Relay?: typeof Relay,
28
28
  * processNumber?: number,
29
29
  * providerEnvironment?: NodeJS.ProcessEnv,
30
+ * cwd?: string,
30
31
  * kimiBinding?: unknown,
31
- * workspaceProfileOperations?: import("../workspace-profile.js").WorkspaceProfileOperations,
32
- * activity?: Pick<import("../activity-log.js").ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
32
+ * activity?: Pick<import("../activity-log.js").ActivityLog, "recordStarted" | "recordSession" | "close">,
33
33
  * evidenceStore?: import("../evidence-store.js").EvidenceStore,
34
- * isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
35
- * kimiIsolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "run">,
34
+ * isolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "startRun">,
35
+ * kimiIsolatedRuntimeClient?: Pick<import("../isolated-runtime-client.js").IsolatedRuntimeClient, "preflight" | "startRun">,
36
36
  * onWorkerFailure?: (message: string) => void,
37
37
  * onWorkerSettled?: () => void
38
38
  * }} IngressDependencies
@@ -97,9 +97,6 @@ export function interpretUpdate(update, config) {
97
97
  */
98
98
  export async function dispatchWorker(job, config, dependencies = {}) {
99
99
  if ("type" in job) throw new Error("Worker dispatch requires a worker job")
100
- if (job.provider === "kimi" && dependencies.kimiIsolatedRuntimeClient === undefined) {
101
- throw new Error("Kimi isolated runtime is required")
102
- }
103
100
  if (job.provider === "codex" && config.requireCodexIsolation === true
104
101
  && dependencies.isolatedRuntimeClient === undefined) {
105
102
  throw new Error("Codex isolated runtime is required")
@@ -112,65 +109,74 @@ export async function dispatchWorker(job, config, dependencies = {}) {
112
109
  const NoticeQueueImpl = dependencies.NoticeQueue ?? NoticeQueue
113
110
  const RelayImpl = dependencies.Relay ?? Relay
114
111
  const processNumber = dependencies.processNumber ?? process.pid
115
- // Explicit only: never fall back to ambient process.env (avoids secret leakage).
116
- const providerEnvironment = buildProviderEnvironment(dependencies.providerEnvironment ?? {})
112
+ const cwd = dependencies.cwd ?? process.cwd()
117
113
  const kimiBinding = job.provider === "kimi" ? dependencies.kimiBinding : undefined
118
- const workspace = await resolveWorkspaceProfile(
119
- {provider: job.provider, ...(kimiBinding === undefined ? {} : {binding: kimiBinding})},
120
- dependencies.workspaceProfileOperations
121
- )
122
114
  const isolatedRuntimeClient = job.provider === "kimi"
123
115
  ? dependencies.kimiIsolatedRuntimeClient
124
116
  : job.provider === "codex" ? dependencies.isolatedRuntimeClient : undefined
125
- const earlyKimiPreflight = job.provider === "kimi"
117
+ // Explicit only: never fall back to ambient process.env (avoids secret leakage).
118
+ const providerEnvironment = buildProviderEnvironment(dependencies.providerEnvironment ?? {}, {
119
+ provider: job.provider,
120
+ isolated: isolatedRuntimeClient !== undefined
121
+ })
122
+ const earlyKimiPreflight = job.provider === "kimi" && isolatedRuntimeClient !== undefined
126
123
  ? await /** @type {NonNullable<typeof isolatedRuntimeClient>} */ (isolatedRuntimeClient).preflight({
127
124
  provider: "kimi",
128
- profile: workspace.profile,
125
+ profile: "explicit-isolation",
129
126
  binding: kimiBinding,
130
127
  providerArguments: []
131
128
  })
132
129
  : undefined
133
130
 
134
- const provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment)
135
- dependencies.activity?.recordWorkspace(
136
- workspace.profile,
137
- workspace.repositoryRoot,
138
- workspace.revision,
139
- workspace.sourceIdentity
140
- )
141
- const transport = createFetchTransportImpl(config.botToken, undefined, config.telegramRequestTimeoutMs)
142
- const sender = createTelegramSenderImpl(job.target, transport)
143
- const control = new WorkerControlImpl({
144
- sender,
145
- processNumber,
146
- toolMessages: config.toolMessages ?? false,
147
- NoticeQueueClass: NoticeQueueImpl,
148
- RelayClass: RelayImpl
149
- })
150
- const evidenceOwner = dependencies.evidenceStore?.createOwnerScope({
151
- destinationId: evidenceTelegramDestination(job.target, job.senderId),
152
- runId: randomUUID()
153
- })
154
- const evidence = evidenceOwner === undefined ? undefined : await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
155
- contentType: "text/plain; charset=utf-8",
156
- redactions: await collectEvidenceRedactions(dependencies.providerEnvironment ?? {})
157
- })
131
+ /** @type {ReturnType<typeof createProvider>} */
132
+ let provider
133
+ /** @type {ReturnType<typeof createTelegramSender>} */
134
+ let sender
135
+ /** @type {WorkerControl} */
136
+ let control
137
+ /** @type {Awaited<ReturnType<import("../evidence-store.js").EvidenceStore["createArtifact"]>> | undefined} */
138
+ let evidence
139
+ try {
140
+ provider = createProviderImpl(job.provider, [], job.prompt, undefined, providerEnvironment)
141
+ const transport = createFetchTransportImpl(config.botToken, undefined, config.telegramRequestTimeoutMs)
142
+ sender = createTelegramSenderImpl(job.target, transport)
143
+ control = new WorkerControlImpl({
144
+ sender,
145
+ processNumber,
146
+ toolMessages: config.toolMessages ?? false,
147
+ NoticeQueueClass: NoticeQueueImpl,
148
+ RelayClass: RelayImpl
149
+ })
150
+ const evidenceOwner = dependencies.evidenceStore?.createOwnerScope({
151
+ destinationId: evidenceTelegramDestination(job.target, job.senderId),
152
+ runId: randomUUID()
153
+ })
154
+ evidence = evidenceOwner === undefined ? undefined : await dependencies.evidenceStore?.createArtifact(evidenceOwner, {
155
+ contentType: "text/plain; charset=utf-8",
156
+ redactions: await collectEvidenceRedactions(dependencies.providerEnvironment ?? {})
157
+ })
158
+ } catch (error) {
159
+ earlyKimiPreflight?.deadline?.close()
160
+ throw error
161
+ }
158
162
  let evidenceTransferred = false
159
163
  try {
160
164
  await evidence?.append("prompt", `prompt\n${job.prompt}\nprovider-stream\n`)
161
165
 
162
166
  if (isolatedRuntimeClient !== undefined) {
163
167
  evidenceTransferred = true
168
+ let isolatedDeadline = earlyKimiPreflight?.deadline
164
169
  try {
165
170
  const preflight = earlyKimiPreflight ?? await isolatedRuntimeClient.preflight({
166
171
  provider: job.provider,
167
- profile: workspace.profile,
172
+ profile: "explicit-isolation",
168
173
  ...(job.provider === "kimi"
169
174
  ? {binding: kimiBinding}
170
- : {repositoryRoot: workspace.repositoryRoot, cwd: workspace.cwd}),
175
+ : {repositoryRoot: cwd, cwd}),
171
176
  providerArguments: []
172
177
  })
173
- const exitCode = await isolatedRuntimeClient.run({
178
+ isolatedDeadline = preflight.deadline
179
+ const isolatedRun = isolatedRuntimeClient.startRun({
174
180
  preflightId: preflight.preflightId,
175
181
  prompt: job.prompt,
176
182
  providerArguments: [],
@@ -185,6 +191,13 @@ export async function dispatchWorker(job, config, dependencies = {}) {
185
191
  onStderrChunk: (chunk) => evidence?.append("provider-stderr", chunk)
186
192
  })
187
193
  })
194
+ let exitCode
195
+ try {
196
+ exitCode = await abortable(isolatedRun.completion, preflight.deadline?.signal)
197
+ } catch (error) {
198
+ await isolatedRun.cancel(error instanceof Error ? error : new Error("Isolated runtime run failed"))
199
+ throw error
200
+ }
188
201
  if (exitCode !== 0) throw new Error(`Isolated ${job.provider === "kimi" ? "Kimi" : "Codex"} worker exited with status ${exitCode}`)
189
202
  await control.close()
190
203
  if (evidence !== undefined) {
@@ -197,6 +210,7 @@ export async function dispatchWorker(job, config, dependencies = {}) {
197
210
  await evidence?.abort()
198
211
  throw error
199
212
  } finally {
213
+ isolatedDeadline?.close()
200
214
  dependencies.onWorkerSettled?.()
201
215
  }
202
216
  }
@@ -241,9 +255,11 @@ export async function dispatchWorker(job, config, dependencies = {}) {
241
255
  const running = runWorkerImpl({
242
256
  executable: provider.executable,
243
257
  arguments: provider.arguments,
244
- cwd: workspace.cwd,
258
+ cwd,
245
259
  environment: providerEnvironment,
260
+ provider: provider.name,
246
261
  parse: provider.parse,
262
+ ...(provider.completion === undefined ? {} : {completion: provider.completion}),
247
263
  onEvent: async (event) => control.accept(event),
248
264
  onSpawn: (pid) => {
249
265
  if (pid !== undefined) dependencies.activity?.recordStarted(provider.name, pid)
@@ -262,11 +278,22 @@ export async function dispatchWorker(job, config, dependencies = {}) {
262
278
  // reported. Always notify settlement so concurrency slots are released.
263
279
  evidenceTransferred = true
264
280
  void (async () => {
281
+ /** @type {Error | undefined} */
282
+ let nativeKimiExitFailure
265
283
  try {
266
284
  try {
267
- await running
285
+ const exitCode = await running
286
+ if (job.provider === "kimi" && spawnGate.settled) {
287
+ nativeKimiExitFailure = exitCode === 0 ? undefined : new Error(`Kimi worker exited with status ${exitCode}`)
288
+ await control.accept({
289
+ type: "lifecycle",
290
+ phase: exitCode === 0 ? "completed" : "failed",
291
+ summary: exitCode === 0 ? "Kimi worker completed" : "Kimi worker failed"
292
+ })
293
+ if (nativeKimiExitFailure !== undefined) throw nativeKimiExitFailure
294
+ }
268
295
  } catch (error) {
269
- const failure = error instanceof Error ? error : new Error(safeFailureMessage(error))
296
+ const failure = nativeKimiExitFailure ?? (error instanceof Error ? error : new Error(safeFailureMessage(error)))
270
297
  if (spawnGate.settled) {
271
298
  reportBackgroundFailure(failure)
272
299
  } else {
@@ -316,7 +343,11 @@ export async function dispatchWorker(job, config, dependencies = {}) {
316
343
 
317
344
  await spawned
318
345
  } finally {
319
- if (!evidenceTransferred) await evidence?.abort()
346
+ try {
347
+ if (!evidenceTransferred) await evidence?.abort()
348
+ } finally {
349
+ earlyKimiPreflight?.deadline?.close()
350
+ }
320
351
  }
321
352
  }
322
353
 
@@ -3,7 +3,6 @@
3
3
  import {createConcurrencyLimiter} from "./concurrency.js"
4
4
  import {dispatchEvidenceRead, dispatchWorker, interpretUpdate, secretsEqual} from "./core.js"
5
5
  import {createUpdateGuard, parseUpdateId} from "./update-guard.js"
6
- import {WorkspaceProviderMismatchError} from "../workspace-profile.js"
7
6
 
8
7
  export const MAX_WEBHOOK_BODY_BYTES = 65_536
9
8
  export const WEBHOOK_PATH = "/webhook"
@@ -159,11 +158,6 @@ async function handleRequest(request, response, config, dependencies) {
159
158
  // Safety release for failures before the observer is installed (e.g. provider setup).
160
159
  // Idempotent with onWorkerSettled via slotHeld.
161
160
  releaseSlot()
162
- if (error instanceof WorkspaceProviderMismatchError) {
163
- dependencies.updateGuard.complete(updateId)
164
- send(response, 200)
165
- return
166
- }
167
161
  dependencies.updateGuard.release(updateId)
168
162
  const message = error instanceof Error ? error.message : "Worker launch failed"
169
163
  dependencies.onOperationalError?.(message)
@@ -17,7 +17,7 @@ import {parseTrustedThreadwireBinding} from "./threadwire-binding.js"
17
17
  * onOperationalError?: (message: string) => void,
18
18
  * onWorkerFailure?: (message: string) => void,
19
19
  * providerEnvironment?: NodeJS.ProcessEnv,
20
- * activity?: Pick<ActivityLog, "recordWorkspace" | "recordStarted" | "recordSession" | "close">,
20
+ * activity?: Pick<ActivityLog, "recordStarted" | "recordSession" | "close">,
21
21
  * evidenceStore?: EvidenceStore,
22
22
  * handlerDependencies?: import("./telegram-ingress/http.js").WebhookDependencies
23
23
  * }} [options]
@@ -25,14 +25,6 @@ import {parseTrustedThreadwireBinding} from "./threadwire-binding.js"
25
25
  */
26
26
  export async function startTelegramWebhook(options = {}) {
27
27
  const environment = await resolveIngressEnvironment(options.env ?? process.env)
28
- // Capture this trusted topology attestation before anything derives a
29
- // provider environment. The parsed object is passed only to Kimi preflight.
30
- // Codex-only Compose renders the variable as an empty string; a blank value
31
- // means "absent", while a malformed non-blank value still fails closed.
32
- const rawKimiBinding = environment.THREADWIRE_KIMI_TASK_BINDING
33
- const kimiBinding = rawKimiBinding === undefined || rawKimiBinding.trim().length === 0
34
- ? undefined
35
- : parseTrustedThreadwireBinding(rawKimiBinding)
36
28
  const config = parseIngressConfig(environment)
37
29
  const createServerImpl = options.createServerImpl ?? createServer
38
30
  const onOperationalError = options.onOperationalError ?? defaultOperationalError
@@ -42,7 +34,10 @@ export async function startTelegramWebhook(options = {}) {
42
34
  // Strip ingress/Telegram secrets before they enter the worker dependency chain.
43
35
  // Never pass ambient process.env through unfiltered.
44
36
  const providerEnvironment = buildProviderEnvironment(
45
- options.providerEnvironment ?? environment
37
+ options.providerEnvironment ?? environment,
38
+ // dispatchWorker applies the final provider/path-aware filter. Retain only
39
+ // the narrow native Kimi candidate configuration until it can do so.
40
+ {provider: "kimi"}
46
41
  )
47
42
  const isolatedConfigured = typeof environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN === "string"
48
43
  && environment.THREADWIRE_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
@@ -52,6 +47,12 @@ export async function startTelegramWebhook(options = {}) {
52
47
  && environment.THREADWIRE_KIMI_ISOLATED_RUNTIME_CONTROL_TOKEN.trim().length > 0
53
48
  const kimiIsolatedRuntimeClient = options.handlerDependencies?.kimiIsolatedRuntimeClient
54
49
  ?? (kimiIsolatedConfigured ? kimiIsolatedRuntimeClientFromEnvironment(environment) : undefined)
50
+ // A task binding belongs solely to an explicitly selected isolated Kimi
51
+ // supervisor. Native Kimi neither reads nor validates this topology input.
52
+ const rawKimiBinding = environment.THREADWIRE_KIMI_TASK_BINDING
53
+ const kimiBinding = kimiIsolatedRuntimeClient === undefined || rawKimiBinding === undefined || rawKimiBinding.trim().length === 0
54
+ ? undefined
55
+ : parseTrustedThreadwireBinding(rawKimiBinding)
55
56
  const activity = options.activity ?? options.handlerDependencies?.activity ?? new ActivityLog("/var/lib/threadwire/activity/threadwire.jsonl")
56
57
  const evidenceStore = options.evidenceStore ?? options.handlerDependencies?.evidenceStore ?? await EvidenceStore.open({
57
58
  root: config.evidenceRoot ?? "/var/lib/threadwire/evidence"
@@ -1,212 +0,0 @@
1
- // @ts-check
2
-
3
- import {execFile as nodeExecFile} from "node:child_process"
4
- import {readFile} from "node:fs/promises"
5
- import {dirname, join, resolve, sep} from "node:path"
6
- import {fileURLToPath} from "node:url"
7
- import {promisify} from "node:util"
8
- import {PROVIDERS} from "./providers/index.js"
9
- import {parseThreadwireBinding} from "./threadwire-binding.js"
10
-
11
- const execFile = promisify(nodeExecFile)
12
- const DEFAULT_PROFILES_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "threadwire.workspace-profiles.json")
13
- const REVISION_PATTERN = /^[0-9a-f]{40}$/u
14
- const SOURCE_IDENTITY_PATTERN = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/u
15
- const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u
16
-
17
- /**
18
- * @typedef {{repositoryRoot: string, cwd: string, providers: ("codex" | "claude" | "kimi" | "opencode")[], bindings?: {kimi: "threadwire-v1"}}} WorkspaceProfile
19
- * @typedef {{version: 1 | 2, defaultProfile: string, profiles: Record<string, WorkspaceProfile>}} WorkspaceProfilesConfig
20
- * @typedef {{profile: string, repositoryRoot: string, cwd: string, revision: string, sourceIdentity: string, binding?: unknown}} ResolvedWorkspaceProfile
21
- * @typedef {{repositoryRoot: string, revision: string, sourceIdentity: string}} GitProvenance
22
- * @typedef {{
23
- * readProfiles?: () => Promise<unknown>,
24
- * directoryExists?: (path: string) => Promise<boolean>,
25
- * resolveGitProvenance?: (cwd: string) => Promise<GitProvenance>
26
- * }} WorkspaceProfileOperations
27
- */
28
-
29
- /** @param {unknown} value @returns {WorkspaceProfilesConfig} */
30
- export function parseWorkspaceProfiles(value) {
31
- if (!isRecord(value) || (value.version !== 1 && value.version !== 2) || typeof value.defaultProfile !== "string" || !isRecord(value.profiles)) {
32
- throw new Error("Workspace profile configuration is invalid")
33
- }
34
- assertExactKeys(value, ["version", "defaultProfile", "profiles"])
35
- /** @type {Record<string, WorkspaceProfile>} */
36
- const profiles = {}
37
- for (const [name, profile] of Object.entries(value.profiles)) {
38
- if (!PROFILE_NAME_PATTERN.test(name) || !isRecord(profile)) throw new Error("Workspace profile configuration is invalid")
39
- assertExactKeys(profile, value.version === 2 ? ["repositoryRoot", "cwd", "providers", "bindings"] : ["repositoryRoot", "cwd", "providers"])
40
- const repositoryRoot = absoluteNormalizedPath(profile.repositoryRoot)
41
- const cwd = absoluteAbsolutePath(profile.cwd)
42
- if (cwd !== resolve(cwd) || !pathWithin(repositoryRoot, resolve(cwd))) {
43
- throw new Error(`Workspace profile ${name} cwd must stay within its repository root`)
44
- }
45
- const providers = parseProviders(profile.providers)
46
- let bindings
47
- if (value.version === 2 && Object.hasOwn(profile, "bindings")) {
48
- if (!isRecord(profile.bindings)) throw new Error("Workspace profile configuration is invalid")
49
- assertExactKeys(profile.bindings, ["kimi"])
50
- if (profile.bindings.kimi !== "threadwire-v1") throw new Error("Workspace profile configuration is invalid")
51
- bindings = /** @type {{kimi: "threadwire-v1"}} */ ({kimi: "threadwire-v1"})
52
- }
53
- profiles[name] = {repositoryRoot, cwd: resolve(cwd), providers, ...(bindings === undefined ? {} : {bindings})}
54
- }
55
- if (!Object.hasOwn(profiles, value.defaultProfile)) {
56
- throw new Error("Workspace profile configuration is invalid")
57
- }
58
- return {version: value.version, defaultProfile: value.defaultProfile, profiles}
59
- }
60
-
61
- /**
62
- * Resolve a reviewed workspace profile to a validated in-container workspace.
63
- * @param {{provider: "codex" | "claude" | "kimi" | "opencode", profile?: string, binding?: unknown}} selection
64
- * @param {WorkspaceProfileOperations} [operations]
65
- * @returns {Promise<ResolvedWorkspaceProfile>}
66
- */
67
- export async function resolveWorkspaceProfile(selection, operations = {}) {
68
- const raw = await (operations.readProfiles ?? readProfiles)()
69
- const config = parseWorkspaceProfiles(raw)
70
- const profileName = selection.profile ?? config.defaultProfile
71
- const profile = config.profiles[profileName]
72
- if (!profile) throw new Error("--workspace-profile must name a configured workspace profile")
73
- if (!profile.providers.includes(selection.provider)) {
74
- throw new WorkspaceProviderMismatchError(profileName, selection.provider)
75
- }
76
- if (selection.provider === "kimi") {
77
- if (config.version !== 2 || profile.bindings?.kimi !== "threadwire-v1" || selection.binding === undefined) {
78
- throw new Error("Kimi requires Threadwire binding schema v1")
79
- }
80
- let binding
81
- try { binding = parseThreadwireBinding(selection.binding) } catch { throw new Error("Kimi requires Threadwire binding schema v1") }
82
- return {
83
- profile: profileName,
84
- repositoryRoot: binding.source.target,
85
- cwd: binding.runtime.workdir,
86
- revision: binding.source.revision,
87
- sourceIdentity: binding.context.digests.content.slice("sha256:".length),
88
- binding
89
- }
90
- }
91
- const directoryExists = operations.directoryExists ?? defaultDirectoryExists
92
- if (!await directoryExists(profile.repositoryRoot) || !await directoryExists(profile.cwd)) {
93
- throw new Error(`Workspace profile ${profileName} workspace is unavailable`)
94
- }
95
- let provenance
96
- try {
97
- provenance = await (operations.resolveGitProvenance ?? defaultResolveGitProvenance)(profile.cwd)
98
- } catch {
99
- throw new Error(`Workspace profile ${profileName} workspace is unavailable`)
100
- }
101
- if (provenance.repositoryRoot !== profile.repositoryRoot) {
102
- throw new Error(`Workspace profile ${profileName} workspace is unavailable`)
103
- }
104
- return {
105
- profile: profileName,
106
- repositoryRoot: profile.repositoryRoot,
107
- cwd: profile.cwd,
108
- revision: provenance.revision,
109
- sourceIdentity: provenance.sourceIdentity
110
- }
111
- }
112
-
113
- export class WorkspaceProviderMismatchError extends Error {
114
- /** @param {string} profileName @param {string} provider */
115
- constructor(profileName, provider) {
116
- super(`Workspace profile ${profileName} does not allow provider ${provider}`)
117
- this.name = "WorkspaceProviderMismatchError"
118
- }
119
- }
120
-
121
- /** @returns {Promise<unknown>} */
122
- async function readProfiles() {
123
- return JSON.parse(await readFile(DEFAULT_PROFILES_PATH, "utf8"))
124
- }
125
-
126
- /** @param {string} path @returns {Promise<boolean>} */
127
- async function defaultDirectoryExists(path) {
128
- try {
129
- const metadata = await import("node:fs/promises").then(({stat}) => stat(path))
130
- return metadata.isDirectory()
131
- } catch {
132
- return false
133
- }
134
- }
135
-
136
- /** @param {string} cwd @returns {Promise<GitProvenance>} */
137
- async function defaultResolveGitProvenance(cwd) {
138
- const repositoryRoot = await gitText(cwd, ["rev-parse", "--show-toplevel"])
139
- const revision = await gitText(cwd, ["rev-parse", "HEAD"])
140
- const sourceIdentity = await readSourceIdentity(repositoryRoot, cwd)
141
- if (!REVISION_PATTERN.test(revision) || !SOURCE_IDENTITY_PATTERN.test(sourceIdentity)) throw new Error("invalid workspace provenance")
142
- return {repositoryRoot, revision, sourceIdentity}
143
- }
144
-
145
- /** @param {string} cwd @param {string[]} arguments_ */
146
- async function gitText(cwd, arguments_) {
147
- const {stdout} = await execFile("git", ["-C", cwd, ...arguments_], {encoding: "utf8"})
148
- return stdout.trim()
149
- }
150
-
151
- /** @param {string} repositoryRoot @param {string} cwd */
152
- async function readSourceIdentity(repositoryRoot, cwd) {
153
- try {
154
- return (await readFile(join(repositoryRoot, ".threadwire-source-identity"), "utf8")).trim()
155
- } catch {
156
- return gitText(cwd, ["rev-parse", "HEAD^{tree}"])
157
- }
158
- }
159
-
160
- /** @param {unknown} value */
161
- function parseProviders(value) {
162
- if (!Array.isArray(value) || value.length === 0) throw new Error("Workspace profile configuration is invalid")
163
- /** @type {("codex" | "claude" | "kimi" | "opencode")[]} */
164
- const providers = []
165
- for (const entry of value) {
166
- if (typeof entry !== "string") {
167
- throw new Error("Workspace profile configuration is invalid")
168
- }
169
- const provider = /** @type {(typeof PROVIDERS)[number]} */ (entry)
170
- if (!PROVIDERS.includes(provider) || providers.includes(provider)) {
171
- throw new Error(typeof entry === "string" ? `Workspace profile configuration contains unknown provider ${entry}` : "Workspace profile configuration is invalid")
172
- }
173
- providers.push(provider)
174
- }
175
- return providers
176
- }
177
-
178
- /** @param {unknown} value */
179
- function absoluteNormalizedPath(value) {
180
- if (typeof value !== "string") throw new Error("Workspace profile configuration is invalid")
181
- const normalized = resolve(value)
182
- if (!normalized.startsWith(sep) || value !== normalized) {
183
- throw new Error("Workspace profile configuration is invalid")
184
- }
185
- return normalized
186
- }
187
-
188
- /** @param {unknown} value */
189
- function absoluteAbsolutePath(value) {
190
- if (typeof value !== "string") throw new Error("Workspace profile configuration is invalid")
191
- const normalized = resolve(value)
192
- if (!normalized.startsWith(sep)) throw new Error("Workspace profile configuration is invalid")
193
- return value
194
- }
195
-
196
- /** @param {string} root @param {string} child */
197
- function pathWithin(root, child) {
198
- return child === root || child.startsWith(`${root}${sep}`)
199
- }
200
-
201
- /** @param {unknown} value @returns {value is Record<string, unknown>} */
202
- function isRecord(value) {
203
- return typeof value === "object" && value !== null && !Array.isArray(value)
204
- }
205
-
206
- /** @param {Record<string, unknown>} value @param {string[]} allowedKeys */
207
- function assertExactKeys(value, allowedKeys) {
208
- const allowed = new Set(allowedKeys)
209
- for (const key of Object.keys(value)) {
210
- if (!allowed.has(key)) throw new Error("Workspace profile configuration is invalid")
211
- }
212
- }
@@ -1,12 +0,0 @@
1
- {
2
- "version": 2,
3
- "defaultProfile": "container-runtime",
4
- "profiles": {
5
- "container-runtime": {
6
- "repositoryRoot": "/workspace/threadwire",
7
- "cwd": "/workspace/threadwire",
8
- "providers": ["codex", "kimi"],
9
- "bindings": {"kimi": "threadwire-v1"}
10
- }
11
- }
12
- }