threadwire 0.1.11 → 0.1.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,192 @@
1
+ // @ts-nocheck
2
+
3
+ import {createHash} from "node:crypto"
4
+ import {isAbsolute, normalize, relative} from "node:path"
5
+
6
+ const UUID = /^[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/u
7
+ const VOLUME = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/u
8
+ const REVISION = /^[0-9a-f]{40}$/u
9
+ const DIGEST = /^sha256:[0-9a-f]{64}$/u
10
+ const CONTAINER_ID = /^[0-9a-f]{64}$/u
11
+ const POSITIVE_ID = /^[1-9]\d{0,9}$/u
12
+ const MAX_ID = 2_147_483_647
13
+ const MAX_MANIFEST_BYTES = 1_048_576
14
+ const MAX_CONTEXT_ENTRIES = 4_096
15
+ const MAX_CONTEXT_BYTES = 67_108_864
16
+
17
+ /**
18
+ * Parse the closed Threadwire binding transport schema. This function is the
19
+ * single authority for a binding: callers must not re-parse loose fields.
20
+ * @param {unknown} value
21
+ */
22
+ export function parseThreadwireBinding(value) {
23
+ if (!record(value) || !exactKeys(value, ["version", "taskId", "source", "context", "runtime", "leaseContainerId"])
24
+ || value.version !== 1 || !canonicalUuid(value.taskId) || !CONTAINER_ID.test(value.leaseContainerId)) failBinding()
25
+ const source = parseSource(value.source)
26
+ const context = parseContext(value.context)
27
+ const runtime = parseRuntime(value.runtime)
28
+ if (source.volume === context.volume || runtime.workdir !== "/workspace" && !within("/workspace", runtime.workdir)) failBinding()
29
+ return {
30
+ version: 1,
31
+ taskId: value.taskId,
32
+ source,
33
+ context,
34
+ runtime,
35
+ leaseContainerId: value.leaseContainerId
36
+ }
37
+ }
38
+
39
+ /**
40
+ * Parse the trusted launcher transport without retaining or reporting its raw
41
+ * text. A binding is control metadata, but still never belongs in a provider
42
+ * or worker environment.
43
+ * @param {unknown} value
44
+ */
45
+ export function parseTrustedThreadwireBinding(value) {
46
+ if (typeof value !== "string" || value.length === 0 || value.length > 32_768) failBinding()
47
+ try { return parseThreadwireBinding(JSON.parse(value)) } catch { failBinding() }
48
+ }
49
+
50
+ /** @param {ReturnType<typeof parseThreadwireBinding>} binding */
51
+ export function canonicalThreadwireBinding(binding) {
52
+ const parsed = parseThreadwireBinding(binding)
53
+ return JSON.stringify({
54
+ version: parsed.version,
55
+ taskId: parsed.taskId,
56
+ source: {
57
+ volume: parsed.source.volume, target: parsed.source.target,
58
+ readOnly: parsed.source.readOnly, revision: parsed.source.revision
59
+ },
60
+ context: {
61
+ volume: parsed.context.volume, target: parsed.context.target,
62
+ readOnly: parsed.context.readOnly,
63
+ digests: {manifest: parsed.context.digests.manifest, content: parsed.context.digests.content},
64
+ imageId: parsed.context.imageId
65
+ },
66
+ runtime: {uid: parsed.runtime.uid, gid: parsed.runtime.gid, workdir: parsed.runtime.workdir},
67
+ leaseContainerId: parsed.leaseContainerId
68
+ })
69
+ }
70
+
71
+ /** @param {ReturnType<typeof parseThreadwireBinding>} binding */
72
+ export function threadwireBindingDigest(binding) {
73
+ return `sha256:${createHash("sha256").update(canonicalThreadwireBinding(binding)).digest("hex")}`
74
+ }
75
+
76
+ /**
77
+ * A stable identity for state adoption. It intentionally excludes only the
78
+ * mutable source commit and recreatable lease identifier.
79
+ * @param {ReturnType<typeof parseThreadwireBinding>} binding
80
+ */
81
+ export function resumeTaskIdentity(binding) {
82
+ const parsed = parseThreadwireBinding(binding)
83
+ return JSON.stringify({
84
+ version: parsed.version,
85
+ taskId: parsed.taskId,
86
+ source: {volume: parsed.source.volume, target: parsed.source.target, readOnly: parsed.source.readOnly},
87
+ context: {
88
+ volume: parsed.context.volume, target: parsed.context.target,
89
+ digests: {manifest: parsed.context.digests.manifest, content: parsed.context.digests.content},
90
+ imageId: parsed.context.imageId
91
+ },
92
+ runtime: parsed.runtime
93
+ })
94
+ }
95
+
96
+ /** @param {Buffer | string} bytes */
97
+ export function parseContextManifest(bytes) {
98
+ const buffer = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes)
99
+ if (buffer.length === 0 || buffer.length > MAX_MANIFEST_BYTES) failManifest()
100
+ let value
101
+ try { value = JSON.parse(buffer.toString("utf8")) } catch { failManifest() }
102
+ if (!record(value) || !exactKeys(value, ["version", "taskId", "imageId", "content", "entries"])
103
+ || value.version !== 1 || !canonicalUuid(value.taskId) || !DIGEST.test(value.imageId) || !DIGEST.test(value.content)
104
+ || !Array.isArray(value.entries) || value.entries.length > MAX_CONTEXT_ENTRIES) failManifest()
105
+ let previous
106
+ let total = 0
107
+ const entries = value.entries.map((entry) => {
108
+ if (!record(entry) || !exactKeys(entry, ["path", "type", "mode", "bytes", "sha256"])
109
+ || !contextPath(entry.path) || entry.type !== "file" || !Number.isSafeInteger(entry.mode) || entry.mode < 0 || entry.mode > 0o777 || (entry.mode & 0o222) !== 0
110
+ || !Number.isSafeInteger(entry.bytes) || entry.bytes < 0 || entry.bytes > MAX_CONTEXT_BYTES || !DIGEST.test(entry.sha256)) failManifest()
111
+ if (previous !== undefined && Buffer.compare(Buffer.from(previous), Buffer.from(entry.path)) >= 0) failManifest()
112
+ previous = entry.path
113
+ total += entry.bytes
114
+ if (total > MAX_CONTEXT_BYTES) failManifest()
115
+ return {path: entry.path, type: "file", mode: entry.mode, bytes: entry.bytes, sha256: entry.sha256}
116
+ })
117
+ return {version: 1, taskId: value.taskId, imageId: value.imageId, content: value.content, entries}
118
+ }
119
+
120
+ /** @param {ReturnType<typeof parseContextManifest>} manifest */
121
+ export function canonicalContextInventory(manifest) {
122
+ const parsed = parseContextManifest(Buffer.from(JSON.stringify(manifest)))
123
+ return JSON.stringify(parsed.entries.map((entry) => ({
124
+ path: entry.path, type: entry.type, mode: entry.mode, bytes: entry.bytes, sha256: entry.sha256
125
+ })))
126
+ }
127
+
128
+ /** @param {ReturnType<typeof parseContextManifest>} manifest */
129
+ export function contextInventoryDigest(manifest) {
130
+ return `sha256:${createHash("sha256").update(canonicalContextInventory(manifest)).digest("hex")}`
131
+ }
132
+
133
+ /**
134
+ * Bind a parsed manifest to the exact task and immutable image of the active
135
+ * binding before any inventory is accepted.
136
+ * @param {ReturnType<typeof parseContextManifest>} manifest
137
+ * @param {unknown} taskId
138
+ * @param {unknown} imageId
139
+ */
140
+ export function assertContextManifestIdentity(manifest, taskId, imageId) {
141
+ if (!canonicalUuid(taskId) || typeof imageId !== "string" || !DIGEST.test(imageId)
142
+ || manifest.taskId !== taskId || manifest.imageId !== imageId) failManifest()
143
+ }
144
+
145
+ /** @param {unknown} value */
146
+ function parseSource(value) {
147
+ // readOnly selects the exact admitted mount mode: false is a writable
148
+ // implementation source, true an explicitly read-only review source.
149
+ if (!record(value) || !exactKeys(value, ["volume", "target", "readOnly", "revision"])
150
+ || !VOLUME.test(value.volume) || value.target !== "/workspace" || typeof value.readOnly !== "boolean" || !REVISION.test(value.revision)) failBinding()
151
+ return {volume: value.volume, target: value.target, readOnly: value.readOnly, revision: value.revision}
152
+ }
153
+
154
+ /** @param {unknown} value */
155
+ function parseContext(value) {
156
+ if (!record(value) || !exactKeys(value, ["volume", "target", "readOnly", "digests", "imageId"])
157
+ || !VOLUME.test(value.volume) || value.target !== "/context" || value.readOnly !== true || !DIGEST.test(value.imageId)
158
+ || !record(value.digests) || !exactKeys(value.digests, ["manifest", "content"])
159
+ || !DIGEST.test(value.digests.manifest) || !DIGEST.test(value.digests.content)) failBinding()
160
+ return {
161
+ volume: value.volume, target: value.target, readOnly: value.readOnly,
162
+ digests: {manifest: value.digests.manifest, content: value.digests.content}, imageId: value.imageId
163
+ }
164
+ }
165
+
166
+ /** @param {unknown} value */
167
+ function parseRuntime(value) {
168
+ if (!record(value) || !exactKeys(value, ["uid", "gid", "workdir"])
169
+ || !safeContainerId(value.uid) || !safeContainerId(value.gid) || typeof value.workdir !== "string"
170
+ || !isAbsolute(value.workdir) || normalize(value.workdir) !== value.workdir) failBinding()
171
+ return {uid: value.uid, gid: value.gid, workdir: value.workdir}
172
+ }
173
+
174
+ /** @param {unknown} value */
175
+ function safeContainerId(value) { return typeof value === "number" && Number.isSafeInteger(value) && POSITIVE_ID.test(String(value)) && value <= MAX_ID }
176
+ /** @param {unknown} value */
177
+ function canonicalUuid(value) { return typeof value === "string" && UUID.test(value) }
178
+ /** @param {string} root @param {string} path */
179
+ function within(root, path) { const path_ = relative(root, path); return path_ === "" || (!path_.startsWith("..") && !isAbsolute(path_)) }
180
+ /** @param {unknown} value */
181
+ function contextPath(value) {
182
+ return typeof value === "string" && value.length > 0 && value.length <= 1024 && !value.startsWith("/")
183
+ && !value.includes("\\") && value.split("/").every((part) => part !== "" && part !== "." && part !== "..")
184
+ }
185
+ /** @param {unknown} value @param {string[]} keys */
186
+ function exactKeys(value, keys) { return record(value) && Object.keys(value).length === keys.length && keys.every((key) => Object.hasOwn(value, key)) }
187
+ /** @param {unknown} value */
188
+ function record(value) { return typeof value === "object" && value !== null && !Array.isArray(value) }
189
+ /** Fail without reflecting untrusted binding input. */
190
+ function failBinding() { throw new Error("Threadwire binding is invalid") }
191
+ /** Fail without reflecting untrusted manifest input. */
192
+ function failManifest() { throw new Error("Threadwire context manifest is invalid") }
@@ -6,6 +6,7 @@ import {dirname, join, resolve, sep} from "node:path"
6
6
  import {fileURLToPath} from "node:url"
7
7
  import {promisify} from "node:util"
8
8
  import {PROVIDERS} from "./providers/index.js"
9
+ import {parseThreadwireBinding} from "./threadwire-binding.js"
9
10
 
10
11
  const execFile = promisify(nodeExecFile)
11
12
  const DEFAULT_PROFILES_PATH = join(dirname(fileURLToPath(import.meta.url)), "..", "threadwire.workspace-profiles.json")
@@ -14,9 +15,9 @@ const SOURCE_IDENTITY_PATTERN = /^[0-9a-f]{40}$|^[0-9a-f]{64}$/u
14
15
  const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u
15
16
 
16
17
  /**
17
- * @typedef {{repositoryRoot: string, cwd: string, providers: ("codex" | "claude" | "kimi" | "opencode")[]}} WorkspaceProfile
18
- * @typedef {{version: 1, defaultProfile: string, profiles: Record<string, WorkspaceProfile>}} WorkspaceProfilesConfig
19
- * @typedef {{profile: string, repositoryRoot: string, cwd: string, revision: string, sourceIdentity: string}} ResolvedWorkspaceProfile
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
20
21
  * @typedef {{repositoryRoot: string, revision: string, sourceIdentity: string}} GitProvenance
21
22
  * @typedef {{
22
23
  * readProfiles?: () => Promise<unknown>,
@@ -27,7 +28,7 @@ const PROFILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u
27
28
 
28
29
  /** @param {unknown} value @returns {WorkspaceProfilesConfig} */
29
30
  export function parseWorkspaceProfiles(value) {
30
- if (!isRecord(value) || value.version !== 1 || typeof value.defaultProfile !== "string" || !isRecord(value.profiles)) {
31
+ if (!isRecord(value) || (value.version !== 1 && value.version !== 2) || typeof value.defaultProfile !== "string" || !isRecord(value.profiles)) {
31
32
  throw new Error("Workspace profile configuration is invalid")
32
33
  }
33
34
  assertExactKeys(value, ["version", "defaultProfile", "profiles"])
@@ -35,24 +36,31 @@ export function parseWorkspaceProfiles(value) {
35
36
  const profiles = {}
36
37
  for (const [name, profile] of Object.entries(value.profiles)) {
37
38
  if (!PROFILE_NAME_PATTERN.test(name) || !isRecord(profile)) throw new Error("Workspace profile configuration is invalid")
38
- assertExactKeys(profile, ["repositoryRoot", "cwd", "providers"])
39
+ assertExactKeys(profile, value.version === 2 ? ["repositoryRoot", "cwd", "providers", "bindings"] : ["repositoryRoot", "cwd", "providers"])
39
40
  const repositoryRoot = absoluteNormalizedPath(profile.repositoryRoot)
40
41
  const cwd = absoluteAbsolutePath(profile.cwd)
41
42
  if (cwd !== resolve(cwd) || !pathWithin(repositoryRoot, resolve(cwd))) {
42
43
  throw new Error(`Workspace profile ${name} cwd must stay within its repository root`)
43
44
  }
44
45
  const providers = parseProviders(profile.providers)
45
- profiles[name] = {repositoryRoot, cwd: resolve(cwd), 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})}
46
54
  }
47
55
  if (!Object.hasOwn(profiles, value.defaultProfile)) {
48
56
  throw new Error("Workspace profile configuration is invalid")
49
57
  }
50
- return {version: 1, defaultProfile: value.defaultProfile, profiles}
58
+ return {version: value.version, defaultProfile: value.defaultProfile, profiles}
51
59
  }
52
60
 
53
61
  /**
54
62
  * Resolve a reviewed workspace profile to a validated in-container workspace.
55
- * @param {{provider: "codex" | "claude" | "kimi" | "opencode", profile?: string}} selection
63
+ * @param {{provider: "codex" | "claude" | "kimi" | "opencode", profile?: string, binding?: unknown}} selection
56
64
  * @param {WorkspaceProfileOperations} [operations]
57
65
  * @returns {Promise<ResolvedWorkspaceProfile>}
58
66
  */
@@ -65,6 +73,21 @@ export async function resolveWorkspaceProfile(selection, operations = {}) {
65
73
  if (!profile.providers.includes(selection.provider)) {
66
74
  throw new WorkspaceProviderMismatchError(profileName, selection.provider)
67
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
+ }
68
91
  const directoryExists = operations.directoryExists ?? defaultDirectoryExists
69
92
  if (!await directoryExists(profile.repositoryRoot) || !await directoryExists(profile.cwd)) {
70
93
  throw new Error(`Workspace profile ${profileName} workspace is unavailable`)
@@ -1,11 +1,12 @@
1
1
  {
2
- "version": 1,
2
+ "version": 2,
3
3
  "defaultProfile": "container-runtime",
4
4
  "profiles": {
5
5
  "container-runtime": {
6
6
  "repositoryRoot": "/workspace/threadwire",
7
7
  "cwd": "/workspace/threadwire",
8
- "providers": ["codex", "kimi"]
8
+ "providers": ["codex", "kimi"],
9
+ "bindings": {"kimi": "threadwire-v1"}
9
10
  }
10
11
  }
11
12
  }