@zhivex-ai/harness 0.6.0 → 0.7.0
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 +53 -0
- package/README.md +78 -16
- package/ROADMAP.md +56 -18
- package/SECURITY.md +1 -1
- package/SUPPORT.md +2 -2
- package/dist/cli-stream.d.ts +167 -0
- package/dist/cli.d.ts +18 -27
- package/dist/cli.js +1865 -286
- package/dist/config.d.ts +8 -38
- package/dist/harness.d.ts +4 -1
- package/dist/hostile-repository-demo.js +4231 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +1422 -368
- package/dist/providers.d.ts +77 -0
- package/dist/routing.d.ts +16 -0
- package/dist/sessions.d.ts +122 -0
- package/dist/zhx.js +6910 -0
- package/docs/CLI.md +37 -2
- package/docs/DURABLE_OPERATIONS.md +31 -0
- package/docs/EXTENSIBILITY.md +33 -1
- package/docs/HOSTILE_REPOSITORY_DEMO.md +97 -0
- package/docs/LIVE_CERTIFICATION.md +34 -133
- package/docs/RELEASE.md +10 -25
- package/package.json +16 -4
package/dist/index.js
CHANGED
|
@@ -1,12 +1,359 @@
|
|
|
1
1
|
// @bun
|
|
2
2
|
// src/config.ts
|
|
3
|
-
import { createHash } from "crypto";
|
|
3
|
+
import { createHash as createHash2 } from "crypto";
|
|
4
4
|
import { realpathSync } from "fs";
|
|
5
5
|
import path from "path";
|
|
6
|
+
|
|
7
|
+
// src/providers.ts
|
|
8
|
+
import { createHash } from "crypto";
|
|
9
|
+
import { createGemini } from "@zhivex-ai/gemini";
|
|
6
10
|
import { createMeta } from "@zhivex-ai/meta";
|
|
7
11
|
import { createOpenAI } from "@zhivex-ai/openai";
|
|
8
12
|
import { createQwen } from "@zhivex-ai/qwen";
|
|
9
|
-
var PROVIDERS = ["meta", "qwen", "openai"];
|
|
13
|
+
var PROVIDERS = ["meta", "qwen", "openai", "gemini"];
|
|
14
|
+
var HARNESS_PROVIDER_CAPABILITIES = [
|
|
15
|
+
"streaming",
|
|
16
|
+
"tool-calling",
|
|
17
|
+
"approval-resume"
|
|
18
|
+
];
|
|
19
|
+
var QWEN_REGIONS = [
|
|
20
|
+
"singapore",
|
|
21
|
+
"beijing",
|
|
22
|
+
"hong-kong",
|
|
23
|
+
"tokyo",
|
|
24
|
+
"frankfurt",
|
|
25
|
+
"virginia"
|
|
26
|
+
];
|
|
27
|
+
var PROVIDER_ID = /^[a-z0-9][a-z0-9._-]{0,63}$/;
|
|
28
|
+
var ENVIRONMENT_VARIABLE = /^[A-Z_][A-Z0-9_]{0,127}$/;
|
|
29
|
+
var DIAGNOSTIC_KEY = /^[a-z][A-Za-z0-9]{0,63}$/;
|
|
30
|
+
var requiredText = (name, value) => {
|
|
31
|
+
const normalized = value.trim();
|
|
32
|
+
if (!normalized || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
33
|
+
throw new Error(`${name} must be a non-empty printable string.`);
|
|
34
|
+
}
|
|
35
|
+
return normalized;
|
|
36
|
+
};
|
|
37
|
+
var environmentVariable = (name, value) => {
|
|
38
|
+
if (!ENVIRONMENT_VARIABLE.test(value)) {
|
|
39
|
+
throw new Error(`${name} must be a valid environment variable name.`);
|
|
40
|
+
}
|
|
41
|
+
return value;
|
|
42
|
+
};
|
|
43
|
+
var diagnosticKey = (value) => {
|
|
44
|
+
if (!DIAGNOSTIC_KEY.test(value)) {
|
|
45
|
+
throw new Error(`Invalid provider diagnostic key: ${value}.`);
|
|
46
|
+
}
|
|
47
|
+
return value;
|
|
48
|
+
};
|
|
49
|
+
var normalizedDescriptor = (descriptor) => {
|
|
50
|
+
const id = descriptor.id.trim().toLowerCase();
|
|
51
|
+
if (descriptor.id !== id || !PROVIDER_ID.test(id)) {
|
|
52
|
+
throw new Error(`Invalid provider id: ${descriptor.id}. Use 1-64 lowercase letters, digits, dot, underscore, or hyphen.`);
|
|
53
|
+
}
|
|
54
|
+
if (!(descriptor.support === "certified" || descriptor.support === "provisional")) {
|
|
55
|
+
throw new Error(`Invalid support level for provider ${id}.`);
|
|
56
|
+
}
|
|
57
|
+
const credentialNames = [...new Set(descriptor.credentialNames.map((name) => environmentVariable(`Credential for provider ${id}`, name)))];
|
|
58
|
+
const capabilities = [...new Set(descriptor.capabilities)];
|
|
59
|
+
const invalidCapability = capabilities.find((capability) => !HARNESS_PROVIDER_CAPABILITIES.includes(capability));
|
|
60
|
+
if (invalidCapability) {
|
|
61
|
+
throw new Error(`Invalid capability for provider ${id}: ${invalidCapability}.`);
|
|
62
|
+
}
|
|
63
|
+
return Object.freeze({
|
|
64
|
+
id,
|
|
65
|
+
name: requiredText(`Name for provider ${id}`, descriptor.name),
|
|
66
|
+
defaultModel: requiredText(`Default model for provider ${id}`, descriptor.defaultModel),
|
|
67
|
+
credentialNames: Object.freeze(credentialNames),
|
|
68
|
+
capabilities: Object.freeze(capabilities),
|
|
69
|
+
support: descriptor.support
|
|
70
|
+
});
|
|
71
|
+
};
|
|
72
|
+
var normalizedDiagnostics = (provider, diagnostics) => {
|
|
73
|
+
const endpointEnvironmentVariable = diagnostics?.endpointEnvironmentVariable === undefined ? undefined : environmentVariable(`Endpoint diagnostic for provider ${provider}`, diagnostics.endpointEnvironmentVariable);
|
|
74
|
+
const seenKeys = new Set;
|
|
75
|
+
const presence = (diagnostics?.presence ?? []).map((entry) => {
|
|
76
|
+
const key = diagnosticKey(entry.key);
|
|
77
|
+
if (seenKeys.has(key)) {
|
|
78
|
+
throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
|
|
79
|
+
}
|
|
80
|
+
seenKeys.add(key);
|
|
81
|
+
return Object.freeze({
|
|
82
|
+
key,
|
|
83
|
+
environmentVariable: environmentVariable(`Presence diagnostic for provider ${provider}`, entry.environmentVariable)
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
const enums = (diagnostics?.enums ?? []).map((entry) => {
|
|
87
|
+
const key = diagnosticKey(entry.key);
|
|
88
|
+
if (seenKeys.has(key)) {
|
|
89
|
+
throw new Error(`Duplicate diagnostic key for provider ${provider}: ${key}.`);
|
|
90
|
+
}
|
|
91
|
+
seenKeys.add(key);
|
|
92
|
+
const allowedValues = [...new Set(entry.allowedValues.map((value) => requiredText(`Allowed diagnostic value for provider ${provider}`, value)))];
|
|
93
|
+
if (allowedValues.length === 0) {
|
|
94
|
+
throw new Error(`Enum diagnostic ${key} for provider ${provider} needs allowed values.`);
|
|
95
|
+
}
|
|
96
|
+
return Object.freeze({
|
|
97
|
+
key,
|
|
98
|
+
environmentVariable: environmentVariable(`Enum diagnostic for provider ${provider}`, entry.environmentVariable),
|
|
99
|
+
allowedValues: Object.freeze(allowedValues)
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
return Object.freeze({
|
|
103
|
+
...endpointEnvironmentVariable ? { endpointEnvironmentVariable } : {},
|
|
104
|
+
...presence.length > 0 ? { presence: Object.freeze(presence) } : {},
|
|
105
|
+
...enums.length > 0 ? { enums: Object.freeze(enums) } : {}
|
|
106
|
+
});
|
|
107
|
+
};
|
|
108
|
+
var endpointConfiguration = (value) => {
|
|
109
|
+
const configured = Boolean(value?.trim());
|
|
110
|
+
if (!configured) {
|
|
111
|
+
return {
|
|
112
|
+
customEndpoint: false,
|
|
113
|
+
endpointValid: true,
|
|
114
|
+
endpointSecure: true
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
try {
|
|
118
|
+
const endpoint = new URL(value.trim());
|
|
119
|
+
const supportedProtocol = endpoint.protocol === "https:" || endpoint.protocol === "http:";
|
|
120
|
+
return {
|
|
121
|
+
customEndpoint: true,
|
|
122
|
+
endpointValid: supportedProtocol && !endpoint.username && !endpoint.password && !endpoint.hash && !endpoint.search,
|
|
123
|
+
endpointSecure: endpoint.protocol === "https:"
|
|
124
|
+
};
|
|
125
|
+
} catch {
|
|
126
|
+
return {
|
|
127
|
+
customEndpoint: true,
|
|
128
|
+
endpointValid: false,
|
|
129
|
+
endpointSecure: false
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
var createCredentials = (env, provider, names) => Object.freeze({
|
|
134
|
+
names,
|
|
135
|
+
get: (name) => {
|
|
136
|
+
if (!names.includes(name)) {
|
|
137
|
+
throw new Error(`Credential ${name} is not declared for provider ${provider}.`);
|
|
138
|
+
}
|
|
139
|
+
return env[name]?.trim() || undefined;
|
|
140
|
+
},
|
|
141
|
+
first: () => {
|
|
142
|
+
for (const name of names) {
|
|
143
|
+
const value = env[name]?.trim();
|
|
144
|
+
if (value) {
|
|
145
|
+
return value;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
return;
|
|
149
|
+
},
|
|
150
|
+
require: () => {
|
|
151
|
+
for (const name of names) {
|
|
152
|
+
const value = env[name]?.trim();
|
|
153
|
+
if (value) {
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
throw new Error(names.length === 0 ? `Provider ${provider} does not declare a credential.` : `Missing credentials for ${provider}. Set ${names.join(" or ")} in the environment.`);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
var normalizeRegistration = (registration) => {
|
|
161
|
+
if (typeof registration.factory !== "function") {
|
|
162
|
+
throw new Error("Provider registration factory must be a function.");
|
|
163
|
+
}
|
|
164
|
+
const descriptor = normalizedDescriptor(registration.descriptor);
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
descriptor,
|
|
167
|
+
factory: registration.factory,
|
|
168
|
+
diagnostics: normalizedDiagnostics(descriptor.id, registration.diagnostics)
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
var providerConfiguration = (diagnostics, env) => {
|
|
172
|
+
const configuration = endpointConfiguration(diagnostics.endpointEnvironmentVariable ? env[diagnostics.endpointEnvironmentVariable] : undefined);
|
|
173
|
+
for (const entry of diagnostics.presence ?? []) {
|
|
174
|
+
configuration[`${entry.key}Configured`] = Boolean(env[entry.environmentVariable]?.trim());
|
|
175
|
+
}
|
|
176
|
+
for (const entry of diagnostics.enums ?? []) {
|
|
177
|
+
const value = env[entry.environmentVariable]?.trim();
|
|
178
|
+
configuration[`${entry.key}Configured`] = Boolean(value);
|
|
179
|
+
configuration[`${entry.key}Valid`] = !value || entry.allowedValues.includes(value);
|
|
180
|
+
}
|
|
181
|
+
return Object.freeze(configuration);
|
|
182
|
+
};
|
|
183
|
+
var parseWithRegistrations = (value, registrations) => {
|
|
184
|
+
const defaultProvider = registrations.has("openai") ? "openai" : registrations.keys().next().value;
|
|
185
|
+
const normalized = (value ?? defaultProvider ?? "").trim().toLowerCase();
|
|
186
|
+
if (registrations.has(normalized)) {
|
|
187
|
+
return normalized;
|
|
188
|
+
}
|
|
189
|
+
throw new Error(`Unknown provider: ${value}. Use ${[...registrations.keys()].join(", ")}.`);
|
|
190
|
+
};
|
|
191
|
+
var createProviderRegistry = (registrations) => {
|
|
192
|
+
if (registrations.length === 0) {
|
|
193
|
+
throw new Error("Provider registry must contain at least one provider.");
|
|
194
|
+
}
|
|
195
|
+
const entries = new Map;
|
|
196
|
+
for (const registration of registrations) {
|
|
197
|
+
const normalized = normalizeRegistration(registration);
|
|
198
|
+
if (entries.has(normalized.descriptor.id)) {
|
|
199
|
+
throw new Error(`Duplicate provider registration: ${normalized.descriptor.id}.`);
|
|
200
|
+
}
|
|
201
|
+
entries.set(normalized.descriptor.id, normalized);
|
|
202
|
+
}
|
|
203
|
+
const frozenRegistrations = Object.freeze([...entries.values()]);
|
|
204
|
+
const descriptors = Object.freeze(frozenRegistrations.map((entry) => entry.descriptor));
|
|
205
|
+
const registry = Object.freeze({
|
|
206
|
+
descriptors,
|
|
207
|
+
has: (provider) => entries.has(provider.trim().toLowerCase()),
|
|
208
|
+
parse: (value) => parseWithRegistrations(value, entries),
|
|
209
|
+
descriptor: (provider) => {
|
|
210
|
+
const registration = entries.get(provider);
|
|
211
|
+
if (!registration) {
|
|
212
|
+
throw new Error(`No descriptor is available for provider ${provider}.`);
|
|
213
|
+
}
|
|
214
|
+
return registration.descriptor;
|
|
215
|
+
},
|
|
216
|
+
createModel: (config, env = process.env) => {
|
|
217
|
+
const registration = entries.get(config.provider);
|
|
218
|
+
if (!registration) {
|
|
219
|
+
throw new Error(`No factory is available for provider ${config.provider}.`);
|
|
220
|
+
}
|
|
221
|
+
return registration.factory(Object.freeze({
|
|
222
|
+
provider: config.provider,
|
|
223
|
+
model: config.model,
|
|
224
|
+
env,
|
|
225
|
+
credentials: createCredentials(env, config.provider, registration.descriptor.credentialNames)
|
|
226
|
+
}));
|
|
227
|
+
},
|
|
228
|
+
availability: (env = process.env) => Object.freeze(frozenRegistrations.map((registration) => {
|
|
229
|
+
const credentials = Object.freeze(registration.descriptor.credentialNames.map((name) => Object.freeze({ name, present: Boolean(env[name]?.trim()) })));
|
|
230
|
+
return Object.freeze({
|
|
231
|
+
...registration.descriptor,
|
|
232
|
+
configured: credentials.length === 0 || credentials.some((credential) => credential.present),
|
|
233
|
+
credentials,
|
|
234
|
+
configuration: providerConfiguration(registration.diagnostics, env)
|
|
235
|
+
});
|
|
236
|
+
})),
|
|
237
|
+
transportFingerprint: (env = process.env) => {
|
|
238
|
+
const transport = frozenRegistrations.map((registration) => {
|
|
239
|
+
const names = [
|
|
240
|
+
registration.diagnostics.endpointEnvironmentVariable,
|
|
241
|
+
...(registration.diagnostics.presence ?? []).map((entry) => entry.environmentVariable),
|
|
242
|
+
...(registration.diagnostics.enums ?? []).map((entry) => entry.environmentVariable)
|
|
243
|
+
].filter((name) => Boolean(name));
|
|
244
|
+
return {
|
|
245
|
+
provider: registration.descriptor.id,
|
|
246
|
+
values: names.map((name) => [name, env[name]?.trim() ?? ""])
|
|
247
|
+
};
|
|
248
|
+
});
|
|
249
|
+
return `sha256:${createHash("sha256").update(JSON.stringify(transport)).digest("hex")}`;
|
|
250
|
+
},
|
|
251
|
+
extend: (additional) => createProviderRegistry([
|
|
252
|
+
...frozenRegistrations,
|
|
253
|
+
...additional
|
|
254
|
+
])
|
|
255
|
+
});
|
|
256
|
+
return registry;
|
|
257
|
+
};
|
|
258
|
+
var optionalQwenRegion = (value) => {
|
|
259
|
+
if (!value) {
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
if (QWEN_REGIONS.includes(value)) {
|
|
263
|
+
return value;
|
|
264
|
+
}
|
|
265
|
+
throw new Error(`Invalid QWEN_REGION: ${value}.`);
|
|
266
|
+
};
|
|
267
|
+
var BUILTIN_PROVIDER_REGISTRATIONS = Object.freeze([
|
|
268
|
+
{
|
|
269
|
+
descriptor: {
|
|
270
|
+
id: "meta",
|
|
271
|
+
name: "Meta Model API",
|
|
272
|
+
defaultModel: "muse-spark-1.2",
|
|
273
|
+
credentialNames: ["MODEL_API_KEY"],
|
|
274
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
275
|
+
support: "certified"
|
|
276
|
+
},
|
|
277
|
+
diagnostics: { endpointEnvironmentVariable: "META_BASE_URL" },
|
|
278
|
+
factory: ({ model, env, credentials }) => {
|
|
279
|
+
const baseURL = env.META_BASE_URL?.trim();
|
|
280
|
+
return createMeta({
|
|
281
|
+
apiKey: credentials.require(),
|
|
282
|
+
...baseURL ? { baseURL } : {}
|
|
283
|
+
})(model);
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
{
|
|
287
|
+
descriptor: {
|
|
288
|
+
id: "qwen",
|
|
289
|
+
name: "Qwen / Alibaba Cloud Model Studio",
|
|
290
|
+
defaultModel: "qwen3.8-max",
|
|
291
|
+
credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
292
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
293
|
+
support: "certified"
|
|
294
|
+
},
|
|
295
|
+
diagnostics: {
|
|
296
|
+
endpointEnvironmentVariable: "QWEN_BASE_URL",
|
|
297
|
+
presence: [{ key: "workspaceId", environmentVariable: "QWEN_WORKSPACE_ID" }],
|
|
298
|
+
enums: [{
|
|
299
|
+
key: "region",
|
|
300
|
+
environmentVariable: "QWEN_REGION",
|
|
301
|
+
allowedValues: QWEN_REGIONS
|
|
302
|
+
}]
|
|
303
|
+
},
|
|
304
|
+
factory: ({ model, env, credentials }) => {
|
|
305
|
+
const baseURL = env.QWEN_BASE_URL?.trim();
|
|
306
|
+
const workspaceId = env.QWEN_WORKSPACE_ID?.trim();
|
|
307
|
+
const region = optionalQwenRegion(env.QWEN_REGION?.trim());
|
|
308
|
+
return createQwen({
|
|
309
|
+
apiKey: credentials.require(),
|
|
310
|
+
...baseURL ? { baseURL } : {},
|
|
311
|
+
...workspaceId ? { workspaceId } : {},
|
|
312
|
+
...region ? { region } : {}
|
|
313
|
+
})(model);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
descriptor: {
|
|
318
|
+
id: "openai",
|
|
319
|
+
name: "OpenAI",
|
|
320
|
+
defaultModel: "gpt-5.4",
|
|
321
|
+
credentialNames: ["OPENAI_API_KEY"],
|
|
322
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
323
|
+
support: "certified"
|
|
324
|
+
},
|
|
325
|
+
diagnostics: { endpointEnvironmentVariable: "OPENAI_BASE_URL" },
|
|
326
|
+
factory: ({ model, env, credentials }) => {
|
|
327
|
+
const baseURL = env.OPENAI_BASE_URL?.trim();
|
|
328
|
+
return createOpenAI({
|
|
329
|
+
apiKey: credentials.require(),
|
|
330
|
+
...baseURL ? { baseURL } : {}
|
|
331
|
+
})(model);
|
|
332
|
+
}
|
|
333
|
+
},
|
|
334
|
+
{
|
|
335
|
+
descriptor: {
|
|
336
|
+
id: "gemini",
|
|
337
|
+
name: "Google Gemini",
|
|
338
|
+
defaultModel: "gemini-3.6-flash",
|
|
339
|
+
credentialNames: ["GEMINI_API_KEY", "GOOGLE_GENERATIVE_AI_API_KEY"],
|
|
340
|
+
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
341
|
+
support: "provisional"
|
|
342
|
+
},
|
|
343
|
+
diagnostics: { endpointEnvironmentVariable: "GEMINI_BASE_URL" },
|
|
344
|
+
factory: ({ model, env, credentials }) => {
|
|
345
|
+
const baseURL = env.GEMINI_BASE_URL?.trim();
|
|
346
|
+
return createGemini({
|
|
347
|
+
apiKey: credentials.require(),
|
|
348
|
+
...baseURL ? { baseURL } : {}
|
|
349
|
+
})(model);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
]);
|
|
353
|
+
var DEFAULT_PROVIDER_REGISTRY = createProviderRegistry(BUILTIN_PROVIDER_REGISTRATIONS);
|
|
354
|
+
var PROVIDER_DESCRIPTORS = DEFAULT_PROVIDER_REGISTRY.descriptors;
|
|
355
|
+
|
|
356
|
+
// src/config.ts
|
|
10
357
|
var DEFAULT_ALLOWED_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
11
358
|
var HARNESS_CONFIG_SCHEMA_VERSION = 4;
|
|
12
359
|
var HARNESS_STORE_BACKENDS = ["sqlite", "file"];
|
|
@@ -62,45 +409,6 @@ var DEFAULT_SUBAGENT_BUDGET = {
|
|
|
62
409
|
maxTotalTokens: 36000,
|
|
63
410
|
includeChildRuns: false
|
|
64
411
|
};
|
|
65
|
-
var HARNESS_PROVIDER_CAPABILITIES = [
|
|
66
|
-
"streaming",
|
|
67
|
-
"tool-calling",
|
|
68
|
-
"approval-resume"
|
|
69
|
-
];
|
|
70
|
-
var PROVIDER_DESCRIPTORS = [
|
|
71
|
-
{
|
|
72
|
-
id: "meta",
|
|
73
|
-
name: "Meta Model API",
|
|
74
|
-
defaultModel: "muse-spark-1.2",
|
|
75
|
-
credentialNames: ["MODEL_API_KEY"],
|
|
76
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
77
|
-
support: "certified"
|
|
78
|
-
},
|
|
79
|
-
{
|
|
80
|
-
id: "qwen",
|
|
81
|
-
name: "Qwen / Alibaba Cloud Model Studio",
|
|
82
|
-
defaultModel: "qwen3.8-max",
|
|
83
|
-
credentialNames: ["DASHSCOPE_API_KEY", "QWEN_API_KEY"],
|
|
84
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
85
|
-
support: "certified"
|
|
86
|
-
},
|
|
87
|
-
{
|
|
88
|
-
id: "openai",
|
|
89
|
-
name: "OpenAI",
|
|
90
|
-
defaultModel: "gpt-5.4",
|
|
91
|
-
credentialNames: ["OPENAI_API_KEY"],
|
|
92
|
-
capabilities: HARNESS_PROVIDER_CAPABILITIES,
|
|
93
|
-
support: "certified"
|
|
94
|
-
}
|
|
95
|
-
];
|
|
96
|
-
var QWEN_REGIONS = [
|
|
97
|
-
"singapore",
|
|
98
|
-
"beijing",
|
|
99
|
-
"hong-kong",
|
|
100
|
-
"tokyo",
|
|
101
|
-
"frankfurt",
|
|
102
|
-
"virginia"
|
|
103
|
-
];
|
|
104
412
|
var integerOption = (name, input, envValue, fallback, minimum, maximum) => {
|
|
105
413
|
const value = input ?? (envValue === undefined ? fallback : Number(envValue));
|
|
106
414
|
if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
|
|
@@ -129,7 +437,7 @@ var nonNegativeNumberOption = (name, input, envValue) => {
|
|
|
129
437
|
}
|
|
130
438
|
return value;
|
|
131
439
|
};
|
|
132
|
-
var defaultHarnessNamespace = (workspace) => `workspace-${
|
|
440
|
+
var defaultHarnessNamespace = (workspace) => `workspace-${createHash2("sha256").update(workspace).digest("hex").slice(0, 16)}`;
|
|
133
441
|
var canonicalStateDirectory = (requestedWorkspace, canonicalWorkspace, configured) => {
|
|
134
442
|
if (!configured) {
|
|
135
443
|
return path.join(canonicalWorkspace, ".zhivex-harness", "runs");
|
|
@@ -231,26 +539,14 @@ var canonicalWorkspaceFile = (requestedWorkspace, canonicalWorkspace, configured
|
|
|
231
539
|
const insideRequestedWorkspace = relative === "" || relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative);
|
|
232
540
|
return insideRequestedWorkspace ? path.resolve(canonicalWorkspace, relative) : requestedPath;
|
|
233
541
|
};
|
|
234
|
-
var parseProvider = (value) =>
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
return normalized;
|
|
238
|
-
}
|
|
239
|
-
throw new Error(`Unknown provider: ${value}. Use meta, qwen, or openai.`);
|
|
240
|
-
};
|
|
241
|
-
var providerDescriptor = (provider) => {
|
|
242
|
-
const descriptor = PROVIDER_DESCRIPTORS.find((entry) => entry.id === provider);
|
|
243
|
-
if (!descriptor) {
|
|
244
|
-
throw new Error(`No descriptor is available for provider ${provider}.`);
|
|
245
|
-
}
|
|
246
|
-
return descriptor;
|
|
247
|
-
};
|
|
248
|
-
var resolveHarnessConfig = (input = {}) => {
|
|
542
|
+
var parseProvider = (value, registry = DEFAULT_PROVIDER_REGISTRY) => registry.parse(value);
|
|
543
|
+
var providerDescriptor = (provider, registry = DEFAULT_PROVIDER_REGISTRY) => registry.descriptor(provider);
|
|
544
|
+
var resolveHarnessConfig = (input = {}, registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
249
545
|
if (input.schemaVersion !== undefined && input.schemaVersion !== HARNESS_CONFIG_SCHEMA_VERSION) {
|
|
250
546
|
throw new Error(`Unsupported config schema version: ${input.schemaVersion}. Expected ${HARNESS_CONFIG_SCHEMA_VERSION}.`);
|
|
251
547
|
}
|
|
252
|
-
const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER);
|
|
253
|
-
const descriptor = providerDescriptor(provider);
|
|
548
|
+
const provider = parseProvider(input.provider ?? process.env.ZHIVEX_HARNESS_PROVIDER, registry);
|
|
549
|
+
const descriptor = providerDescriptor(provider, registry);
|
|
254
550
|
const requestedWorkspace = path.resolve(input.workspace ?? process.cwd());
|
|
255
551
|
let workspace = requestedWorkspace;
|
|
256
552
|
try {
|
|
@@ -344,121 +640,896 @@ var resolveHarnessConfig = (input = {}) => {
|
|
|
344
640
|
...mcpConfigPath ? { mcpConfigPath } : {}
|
|
345
641
|
};
|
|
346
642
|
};
|
|
347
|
-
var
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
643
|
+
var createProviderModel = (config, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.createModel(config, env);
|
|
644
|
+
var providerAvailability = (env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => registry.availability(env);
|
|
645
|
+
// src/routing.ts
|
|
646
|
+
var routeError = (value) => new Error(`Invalid route: ${value}. Use <profile>=<provider>[:<model>].`);
|
|
647
|
+
var parseHarnessModelRoute = (value, registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
648
|
+
const separator = value.indexOf("=");
|
|
649
|
+
if (separator <= 0 || separator === value.length - 1) {
|
|
650
|
+
throw routeError(value);
|
|
651
|
+
}
|
|
652
|
+
const profile = value.slice(0, separator).trim().toLowerCase();
|
|
653
|
+
if (!HARNESS_SUBAGENT_PROFILES.includes(profile)) {
|
|
654
|
+
throw routeError(value);
|
|
655
|
+
}
|
|
656
|
+
const target = value.slice(separator + 1).trim();
|
|
657
|
+
const modelSeparator = target.indexOf(":");
|
|
658
|
+
const providerInput = (modelSeparator === -1 ? target : target.slice(0, modelSeparator)).trim();
|
|
659
|
+
const provider = registry.parse(providerInput);
|
|
660
|
+
const configuredModel = modelSeparator === -1 ? "" : target.slice(modelSeparator + 1).trim();
|
|
661
|
+
if (modelSeparator !== -1 && !configuredModel) {
|
|
662
|
+
throw routeError(value);
|
|
663
|
+
}
|
|
664
|
+
const model = configuredModel || registry.descriptor(provider).defaultModel;
|
|
665
|
+
if (/[\u0000-\u001f\u007f]/.test(model) || model.length > 256) {
|
|
666
|
+
throw routeError(value);
|
|
667
|
+
}
|
|
668
|
+
return Object.freeze({
|
|
669
|
+
profile,
|
|
670
|
+
provider,
|
|
671
|
+
model
|
|
672
|
+
});
|
|
673
|
+
};
|
|
674
|
+
var resolveHarnessModelRoutes = (values = [], registry = DEFAULT_PROVIDER_REGISTRY) => {
|
|
675
|
+
const routes = new Map;
|
|
676
|
+
for (const value of values) {
|
|
677
|
+
const route = parseHarnessModelRoute(value, registry);
|
|
678
|
+
if (routes.has(route.profile)) {
|
|
679
|
+
throw new Error(`Duplicate model route for ${route.profile}.`);
|
|
352
680
|
}
|
|
681
|
+
routes.set(route.profile, route);
|
|
353
682
|
}
|
|
354
|
-
|
|
683
|
+
return routes;
|
|
355
684
|
};
|
|
356
|
-
var
|
|
357
|
-
|
|
358
|
-
|
|
685
|
+
var createHarnessRouteModels = (routes, env = process.env, registry = DEFAULT_PROVIDER_REGISTRY) => Object.fromEntries([...routes].map(([profile, route]) => [
|
|
686
|
+
profile,
|
|
687
|
+
registry.createModel(route, env)
|
|
688
|
+
]));
|
|
689
|
+
var serializeHarnessModelRoutes = (routes) => Object.fromEntries([...routes].map(([profile, route]) => [
|
|
690
|
+
profile,
|
|
691
|
+
{ provider: route.provider, model: route.model }
|
|
692
|
+
]));
|
|
693
|
+
// src/cli-stream.ts
|
|
694
|
+
var CLI_JSON_SCHEMA_VERSION = 1;
|
|
695
|
+
var CLI_EVENT_SCHEMA_VERSION = 1;
|
|
696
|
+
var streamEventDocument = (event, sequence = 0) => {
|
|
697
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
698
|
+
throw new Error("JSONL event sequence must be a non-negative integer.");
|
|
699
|
+
}
|
|
700
|
+
const base = {
|
|
701
|
+
schemaVersion: CLI_EVENT_SCHEMA_VERSION,
|
|
702
|
+
kind: "run-event",
|
|
703
|
+
sequence,
|
|
704
|
+
type: event.type
|
|
705
|
+
};
|
|
706
|
+
switch (event.type) {
|
|
707
|
+
case "text-delta":
|
|
708
|
+
return { ...base, textDelta: event.textDelta };
|
|
709
|
+
case "tool-call":
|
|
710
|
+
return { ...base, toolCallId: event.toolCall.id, toolName: event.toolCall.name };
|
|
711
|
+
case "tool-result":
|
|
712
|
+
return {
|
|
713
|
+
...base,
|
|
714
|
+
toolCallId: event.toolResult.toolCallId,
|
|
715
|
+
toolName: event.toolResult.toolName,
|
|
716
|
+
isError: event.toolResult.isError
|
|
717
|
+
};
|
|
718
|
+
case "tool-approval-request":
|
|
719
|
+
case "agent-approval-request":
|
|
720
|
+
return {
|
|
721
|
+
...base,
|
|
722
|
+
approvalRequestId: event.approval.id,
|
|
723
|
+
approvalKind: event.approval.kind ?? "provider",
|
|
724
|
+
toolName: event.approval.name
|
|
725
|
+
};
|
|
726
|
+
case "agent-approval-resolved":
|
|
727
|
+
return {
|
|
728
|
+
...base,
|
|
729
|
+
approvalRequestId: event.approval.approvalRequestId,
|
|
730
|
+
approved: event.approval.approve
|
|
731
|
+
};
|
|
732
|
+
case "provider-data":
|
|
733
|
+
return { ...base, provider: event.provider };
|
|
734
|
+
case "image-generation":
|
|
735
|
+
return {
|
|
736
|
+
...base,
|
|
737
|
+
provider: event.provider,
|
|
738
|
+
partial: event.partial,
|
|
739
|
+
...event.id ? { id: event.id } : {},
|
|
740
|
+
...event.index !== undefined ? { index: event.index } : {}
|
|
741
|
+
};
|
|
742
|
+
case "finish":
|
|
743
|
+
return {
|
|
744
|
+
...base,
|
|
745
|
+
...event.finishReason ? { finishReason: event.finishReason } : {},
|
|
746
|
+
...event.usage ? { usage: event.usage } : {}
|
|
747
|
+
};
|
|
748
|
+
case "error":
|
|
749
|
+
return { ...base, error: "Provider stream failed." };
|
|
750
|
+
case "agent-run-start":
|
|
751
|
+
return { ...base, currentStep: event.currentStep, maxSteps: event.maxSteps };
|
|
752
|
+
case "agent-step-start":
|
|
753
|
+
return { ...base, stepIndex: event.stepIndex };
|
|
754
|
+
case "agent-step-finish":
|
|
755
|
+
return {
|
|
756
|
+
...base,
|
|
757
|
+
stepIndex: event.step.index,
|
|
758
|
+
status: event.step.status,
|
|
759
|
+
toolCalls: event.step.toolResults.length
|
|
760
|
+
};
|
|
761
|
+
case "agent-compaction":
|
|
762
|
+
return {
|
|
763
|
+
...base,
|
|
764
|
+
compactionId: event.compaction.id,
|
|
765
|
+
reasons: event.compaction.reasons,
|
|
766
|
+
messageCountBefore: event.compaction.messageCountBefore,
|
|
767
|
+
messageCountAfter: event.compaction.messageCountAfter
|
|
768
|
+
};
|
|
769
|
+
case "agent-run-finish":
|
|
770
|
+
return {
|
|
771
|
+
...base,
|
|
772
|
+
runId: event.state.runId,
|
|
773
|
+
status: event.status,
|
|
774
|
+
provider: event.state.provider,
|
|
775
|
+
model: event.state.modelId
|
|
776
|
+
};
|
|
359
777
|
}
|
|
360
|
-
|
|
361
|
-
|
|
778
|
+
};
|
|
779
|
+
var serializeStreamEvent = (event, sequence = 0) => JSON.stringify(streamEventDocument(event, sequence));
|
|
780
|
+
var streamResultDocument = (result, sequence = 0) => {
|
|
781
|
+
if (!Number.isSafeInteger(sequence) || sequence < 0) {
|
|
782
|
+
throw new Error("JSONL result sequence must be a non-negative integer.");
|
|
362
783
|
}
|
|
363
|
-
|
|
784
|
+
return {
|
|
785
|
+
schemaVersion: CLI_EVENT_SCHEMA_VERSION,
|
|
786
|
+
kind: "run-result",
|
|
787
|
+
sequence,
|
|
788
|
+
runId: result.runId,
|
|
789
|
+
status: result.status,
|
|
790
|
+
provider: result.provider,
|
|
791
|
+
model: result.model,
|
|
792
|
+
steps: result.steps,
|
|
793
|
+
toolCalls: result.toolCalls,
|
|
794
|
+
pendingApprovals: (result.pendingApprovals ?? []).map((approval) => ({
|
|
795
|
+
id: approval.id,
|
|
796
|
+
kind: approval.kind,
|
|
797
|
+
name: approval.name,
|
|
798
|
+
...approval.childRunId ? { childRunId: approval.childRunId } : {},
|
|
799
|
+
...approval.childAgentId ? { childAgentId: approval.childAgentId } : {}
|
|
800
|
+
})),
|
|
801
|
+
children: (result.children ?? []).map((child) => ({
|
|
802
|
+
runId: child.runId,
|
|
803
|
+
status: child.status
|
|
804
|
+
}))
|
|
805
|
+
};
|
|
364
806
|
};
|
|
365
|
-
var
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
807
|
+
var serializeStreamResult = (result, sequence = 0) => JSON.stringify(streamResultDocument(result, sequence));
|
|
808
|
+
// src/sessions.ts
|
|
809
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
810
|
+
import { chmod, lstat as lstat2, mkdir, open, realpath, stat } from "fs/promises";
|
|
811
|
+
import path3 from "path";
|
|
812
|
+
import { createRedactionPolicy } from "@zhivex-ai/agents";
|
|
813
|
+
import { Database } from "bun:sqlite";
|
|
814
|
+
|
|
815
|
+
// src/state-directory.ts
|
|
816
|
+
import { lstat } from "fs/promises";
|
|
817
|
+
import path2 from "path";
|
|
818
|
+
var SENSITIVE_STATE_SEGMENTS = new Set([".git", ".env", ".npmrc", "dist", "node_modules", "src"]);
|
|
819
|
+
var isInside = (root, candidate) => {
|
|
820
|
+
const relative = path2.relative(root, candidate);
|
|
821
|
+
return relative === "" || !relative.startsWith(`..${path2.sep}`) && relative !== ".." && !path2.isAbsolute(relative);
|
|
822
|
+
};
|
|
823
|
+
var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
824
|
+
if (stateDirectory === workspace || stateDirectory === path2.parse(stateDirectory).root) {
|
|
825
|
+
throw new Error("The state directory cannot be the workspace or filesystem root.");
|
|
826
|
+
}
|
|
827
|
+
const insideWorkspace = isInside(workspace, stateDirectory);
|
|
828
|
+
const relativeSegments = insideWorkspace ? path2.relative(workspace, stateDirectory).split(path2.sep).filter(Boolean) : stateDirectory.slice(path2.parse(stateDirectory).root.length).split(path2.sep).filter(Boolean);
|
|
829
|
+
const sensitiveSegment = insideWorkspace ? relativeSegments.find((segment) => SENSITIVE_STATE_SEGMENTS.has(segment)) : undefined;
|
|
830
|
+
if (sensitiveSegment) {
|
|
831
|
+
throw new Error(`The state directory is inside the protected workspace path: ${sensitiveSegment}.`);
|
|
832
|
+
}
|
|
833
|
+
if (!insideWorkspace) {
|
|
834
|
+
try {
|
|
835
|
+
const externalEntry = await lstat(stateDirectory);
|
|
836
|
+
if (externalEntry.isSymbolicLink()) {
|
|
837
|
+
throw new Error(`The state directory must not be a symbolic link: ${stateDirectory}.`);
|
|
838
|
+
}
|
|
839
|
+
if (!externalEntry.isDirectory()) {
|
|
840
|
+
throw new Error(`The state directory is not a directory: ${stateDirectory}.`);
|
|
841
|
+
}
|
|
842
|
+
} catch (error) {
|
|
843
|
+
if (error.code !== "ENOENT") {
|
|
844
|
+
throw error;
|
|
845
|
+
}
|
|
386
846
|
}
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
let current = workspace;
|
|
850
|
+
for (const segment of relativeSegments) {
|
|
851
|
+
current = path2.join(current, segment);
|
|
852
|
+
try {
|
|
853
|
+
const entry = await lstat(current);
|
|
854
|
+
if (entry.isSymbolicLink()) {
|
|
855
|
+
throw new Error(`The state directory must not resolve through a symbolic link: ${current}.`);
|
|
856
|
+
}
|
|
857
|
+
if (!entry.isDirectory()) {
|
|
858
|
+
throw new Error(`The state directory path contains a non-directory entry: ${current}.`);
|
|
859
|
+
}
|
|
860
|
+
} catch (error) {
|
|
861
|
+
if (error.code === "ENOENT") {
|
|
862
|
+
break;
|
|
863
|
+
}
|
|
864
|
+
throw error;
|
|
394
865
|
}
|
|
395
866
|
}
|
|
396
867
|
};
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
868
|
+
|
|
869
|
+
// src/sessions.ts
|
|
870
|
+
var HARNESS_SESSION_INDEX_FILE = "operations.sqlite";
|
|
871
|
+
var HARNESS_SESSION_SCHEMA_VERSION = 1;
|
|
872
|
+
var SESSION_RUN_STATUSES = [
|
|
873
|
+
"created",
|
|
874
|
+
"running",
|
|
875
|
+
"waiting_approval",
|
|
876
|
+
"cancel_requested",
|
|
877
|
+
"completed",
|
|
878
|
+
"failed",
|
|
879
|
+
"cancelled",
|
|
880
|
+
"timed_out"
|
|
881
|
+
];
|
|
882
|
+
var TERMINAL_SESSION_RUN_STATUSES = [
|
|
883
|
+
"completed",
|
|
884
|
+
"failed",
|
|
885
|
+
"cancelled",
|
|
886
|
+
"timed_out"
|
|
887
|
+
];
|
|
888
|
+
var DEFAULT_RETENTION_MS = 90 * 24 * 60 * 60 * 1000;
|
|
889
|
+
var DEFAULT_MAX_SESSIONS = 200;
|
|
890
|
+
var DEFAULT_MAX_RUNS = 1000;
|
|
891
|
+
var DEFAULT_MAX_METADATA_BYTES = 4096;
|
|
892
|
+
var DEFAULT_MAX_INDEX_BYTES = 16 * 1024 * 1024;
|
|
893
|
+
var MAX_LIST_LIMIT = 200;
|
|
894
|
+
var ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:@/+\-]{0,255}$/;
|
|
895
|
+
var TERMINAL_STATUSES = new Set(TERMINAL_SESSION_RUN_STATUSES);
|
|
896
|
+
var ACTIVE_STATUSES = new Set([
|
|
897
|
+
"created",
|
|
898
|
+
"running",
|
|
899
|
+
"waiting_approval",
|
|
900
|
+
"cancel_requested"
|
|
901
|
+
]);
|
|
902
|
+
var STATUS_TRANSITIONS = {
|
|
903
|
+
created: new Set(["created", "running", "waiting_approval", "completed", "failed", "cancelled", "timed_out"]),
|
|
904
|
+
running: new Set(["running", "waiting_approval", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
905
|
+
waiting_approval: new Set(["waiting_approval", "running", "cancel_requested", "completed", "failed", "cancelled", "timed_out"]),
|
|
906
|
+
cancel_requested: new Set(["cancel_requested", "cancelled", "failed", "timed_out"]),
|
|
907
|
+
completed: new Set(["completed"]),
|
|
908
|
+
failed: new Set(["failed"]),
|
|
909
|
+
cancelled: new Set(["cancelled"]),
|
|
910
|
+
timed_out: new Set(["timed_out"])
|
|
911
|
+
};
|
|
912
|
+
var metadataRedaction = createRedactionPolicy({
|
|
913
|
+
includeEmails: true,
|
|
914
|
+
rules: [
|
|
915
|
+
{ name: "private-key", pattern: /-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/g },
|
|
916
|
+
{ name: "common-provider-key", pattern: /\b(?:sk|ghp|github_pat|xox[baprs])[-_][A-Za-z0-9_-]{8,}\b/gi },
|
|
917
|
+
{ name: "url-credentials", pattern: /:\/\/[^\s/@:]+:[^\s/@]+@/g, replacement: "://[REDACTED]@" }
|
|
918
|
+
]
|
|
919
|
+
});
|
|
920
|
+
var byteLength = (value) => Buffer.byteLength(value, "utf8");
|
|
921
|
+
var boundedInteger = (name, value, fallback, minimum) => {
|
|
922
|
+
const resolved = value ?? fallback;
|
|
923
|
+
if (!Number.isSafeInteger(resolved) || resolved < minimum) {
|
|
924
|
+
throw new Error(`${name} must be an integer greater than or equal to ${minimum}.`);
|
|
405
925
|
}
|
|
926
|
+
return resolved;
|
|
927
|
+
};
|
|
928
|
+
var assertTimestamp = (name, value) => {
|
|
929
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
930
|
+
throw new Error(`${name} must be a non-negative integer timestamp.`);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
var assertIdentifier = (name, value) => {
|
|
934
|
+
if (!ID_PATTERN.test(value)) {
|
|
935
|
+
throw new Error(`${name} contains unsupported characters or exceeds 256 bytes.`);
|
|
936
|
+
}
|
|
937
|
+
if (metadataRedaction.redactText(value) !== value) {
|
|
938
|
+
throw new Error(`${name} resembles sensitive metadata and cannot be stored in the session index.`);
|
|
939
|
+
}
|
|
940
|
+
return value;
|
|
941
|
+
};
|
|
942
|
+
var assertScopeSegment = (name, value) => {
|
|
943
|
+
const normalized = value.trim();
|
|
944
|
+
if (!normalized || normalized.length > 128 || /[\u0000-\u001f\u007f]/.test(normalized)) {
|
|
945
|
+
throw new Error(`${name} must contain 1-128 printable characters.`);
|
|
946
|
+
}
|
|
947
|
+
return normalized;
|
|
948
|
+
};
|
|
949
|
+
var normalizeTitle = (value, maxMetadataBytes) => {
|
|
950
|
+
if (value === undefined || value === null) {
|
|
951
|
+
return value;
|
|
952
|
+
}
|
|
953
|
+
const normalized = metadataRedaction.redactText(value).replace(/[\u0000-\u001f\u007f]+/g, " ").trim();
|
|
954
|
+
if (!normalized) {
|
|
955
|
+
throw new Error("Session title must not be empty.");
|
|
956
|
+
}
|
|
957
|
+
if (byteLength(normalized) > maxMetadataBytes) {
|
|
958
|
+
throw new Error(`Session title exceeds the ${maxMetadataBytes}-byte metadata limit.`);
|
|
959
|
+
}
|
|
960
|
+
return normalized;
|
|
961
|
+
};
|
|
962
|
+
var normalizeScope = (scope) => {
|
|
963
|
+
const tenantId = assertScopeSegment("scope.tenantId", scope.tenantId);
|
|
964
|
+
const userId = scope.userId === undefined ? "" : assertScopeSegment("scope.userId", scope.userId);
|
|
965
|
+
const namespace = scope.namespace === undefined ? "" : assertScopeSegment("scope.namespace", scope.namespace);
|
|
966
|
+
return `${tenantId}\x00${userId}\x00${namespace}`;
|
|
967
|
+
};
|
|
968
|
+
var stableKey = (kind, value) => createHash3("sha256").update(kind).update("\x00").update(value).digest("hex");
|
|
969
|
+
var sessionId = () => `ses_${randomUUID()}`;
|
|
970
|
+
var turnId = () => `turn_${randomUUID()}`;
|
|
971
|
+
var isMissing = (error) => error.code === "ENOENT";
|
|
972
|
+
var ensurePrivateDatabase = async (workspace, stateDirectory) => {
|
|
973
|
+
const requestedStateDirectory = path3.resolve(stateDirectory);
|
|
974
|
+
await validateStateDirectory(workspace, requestedStateDirectory);
|
|
975
|
+
await mkdir(requestedStateDirectory, { recursive: true, mode: 448 });
|
|
976
|
+
const directoryEntry = await lstat2(requestedStateDirectory);
|
|
977
|
+
if (directoryEntry.isSymbolicLink() || !directoryEntry.isDirectory()) {
|
|
978
|
+
throw new Error(`The session state directory must be a real directory: ${requestedStateDirectory}.`);
|
|
979
|
+
}
|
|
980
|
+
await chmod(requestedStateDirectory, 448);
|
|
981
|
+
const canonicalStateDirectory2 = await realpath(requestedStateDirectory);
|
|
982
|
+
const databasePath = path3.join(canonicalStateDirectory2, HARNESS_SESSION_INDEX_FILE);
|
|
983
|
+
let databaseEntry;
|
|
406
984
|
try {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
}
|
|
985
|
+
databaseEntry = await lstat2(databasePath);
|
|
986
|
+
} catch (error) {
|
|
987
|
+
if (!isMissing(error)) {
|
|
988
|
+
throw error;
|
|
989
|
+
}
|
|
990
|
+
try {
|
|
991
|
+
const handle = await open(databasePath, "wx", 384);
|
|
992
|
+
await handle.close();
|
|
993
|
+
} catch (creationError) {
|
|
994
|
+
if (creationError.code !== "EEXIST") {
|
|
995
|
+
throw creationError;
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
databaseEntry = await lstat2(databasePath);
|
|
999
|
+
}
|
|
1000
|
+
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
1001
|
+
throw new Error(`The session SQLite path must be a real file: ${databasePath}.`);
|
|
420
1002
|
}
|
|
1003
|
+
await chmod(databasePath, 384);
|
|
1004
|
+
return { databasePath, databaseEntry };
|
|
421
1005
|
};
|
|
422
|
-
var
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
1006
|
+
var toRunReference = (row) => ({
|
|
1007
|
+
turnId: row.turn_id,
|
|
1008
|
+
...row.source_turn_id ? { sourceTurnId: row.source_turn_id } : {},
|
|
1009
|
+
sequence: row.ordinal,
|
|
1010
|
+
runId: row.run_id,
|
|
1011
|
+
provider: row.provider,
|
|
1012
|
+
model: row.model,
|
|
1013
|
+
...row.role ? { role: row.role } : {},
|
|
1014
|
+
status: row.status,
|
|
1015
|
+
createdAt: row.created_at,
|
|
1016
|
+
updatedAt: row.updated_at,
|
|
1017
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
1018
|
+
});
|
|
1019
|
+
var metadataSize = (input) => Object.values(input).reduce((total, value) => total + (typeof value === "string" ? byteLength(value) : 0), 0);
|
|
1020
|
+
var isTerminal = (status) => TERMINAL_STATUSES.has(status);
|
|
1021
|
+
var openCliSessionStore = async (options) => {
|
|
1022
|
+
const workspace = await realpath(path3.resolve(options.workspace));
|
|
1023
|
+
const workspaceEntry = await stat(workspace);
|
|
1024
|
+
if (!workspaceEntry.isDirectory()) {
|
|
1025
|
+
throw new Error(`The session workspace is not a directory: ${workspace}.`);
|
|
1026
|
+
}
|
|
1027
|
+
const scopeValue2 = normalizeScope(options.scope);
|
|
1028
|
+
const workspaceKey = stableKey("workspace", workspace);
|
|
1029
|
+
const scopeKey = stableKey("scope", scopeValue2);
|
|
1030
|
+
const retentionMs = boundedInteger("retentionMs", options.retentionMs, DEFAULT_RETENTION_MS, 0);
|
|
1031
|
+
const maxSessions = boundedInteger("maxSessionsPerWorkspace", options.maxSessionsPerWorkspace, DEFAULT_MAX_SESSIONS, 1);
|
|
1032
|
+
const maxRuns = boundedInteger("maxRunsPerSession", options.maxRunsPerSession, DEFAULT_MAX_RUNS, 1);
|
|
1033
|
+
const maxMetadataBytes = boundedInteger("maxMetadataBytes", options.maxMetadataBytes, DEFAULT_MAX_METADATA_BYTES, 32);
|
|
1034
|
+
const maxIndexBytes = boundedInteger("maxIndexBytes", options.maxIndexBytes, DEFAULT_MAX_INDEX_BYTES, 1024);
|
|
1035
|
+
const now = options.now ?? Date.now;
|
|
1036
|
+
const { databasePath, databaseEntry } = await ensurePrivateDatabase(workspace, options.stateDirectory);
|
|
1037
|
+
const database = new Database(databasePath, { create: false, strict: true });
|
|
1038
|
+
let closed = false;
|
|
1039
|
+
const assertOpen = () => {
|
|
1040
|
+
if (closed) {
|
|
1041
|
+
throw new Error("The CLI session store is closed.");
|
|
440
1042
|
}
|
|
441
1043
|
};
|
|
442
|
-
|
|
1044
|
+
const transaction = (operation) => {
|
|
1045
|
+
assertOpen();
|
|
1046
|
+
database.exec("BEGIN IMMEDIATE");
|
|
1047
|
+
try {
|
|
1048
|
+
const result = operation();
|
|
1049
|
+
const size = database.query(`
|
|
1050
|
+
SELECT
|
|
1051
|
+
COALESCE((SELECT SUM(metadata_bytes) FROM zhivex_cli_sessions WHERE workspace_key = ?1 AND scope_key = ?2), 0) +
|
|
1052
|
+
COALESCE((SELECT SUM(r.metadata_bytes) FROM zhivex_cli_session_runs r JOIN zhivex_cli_sessions s ON s.session_id = r.session_id WHERE s.workspace_key = ?1 AND s.scope_key = ?2), 0)
|
|
1053
|
+
AS size
|
|
1054
|
+
`).get(workspaceKey, scopeKey)?.size ?? 0;
|
|
1055
|
+
if (size > maxIndexBytes) {
|
|
1056
|
+
throw new Error(`Session index metadata exceeds the ${maxIndexBytes}-byte limit.`);
|
|
1057
|
+
}
|
|
1058
|
+
database.exec("COMMIT");
|
|
1059
|
+
return result;
|
|
1060
|
+
} catch (error) {
|
|
1061
|
+
try {
|
|
1062
|
+
database.exec("ROLLBACK");
|
|
1063
|
+
} catch {}
|
|
1064
|
+
throw error;
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
const ensureRevision = (row, expected) => {
|
|
1068
|
+
if (expected !== undefined && row.revision !== expected) {
|
|
1069
|
+
throw new Error(`Session ${row.session_id} changed concurrently (expected revision ${expected}, found ${row.revision}).`);
|
|
1070
|
+
}
|
|
1071
|
+
};
|
|
1072
|
+
const selectSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
1073
|
+
assertIdentifier("sessionId", candidateSessionId);
|
|
1074
|
+
return database.query(`
|
|
1075
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
1076
|
+
created_at, updated_at, archived_at, deleted_at
|
|
1077
|
+
FROM zhivex_cli_sessions
|
|
1078
|
+
WHERE session_id = ?1 AND workspace_key = ?2 AND scope_key = ?3
|
|
1079
|
+
${includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
1080
|
+
`).get(candidateSessionId, workspaceKey, scopeKey);
|
|
1081
|
+
};
|
|
1082
|
+
const requireSessionRow = (candidateSessionId, includeDeleted = false) => {
|
|
1083
|
+
const row = selectSessionRow(candidateSessionId, includeDeleted);
|
|
1084
|
+
if (!row) {
|
|
1085
|
+
throw new Error(`Session ${candidateSessionId} was not found in this workspace and scope.`);
|
|
1086
|
+
}
|
|
1087
|
+
return row;
|
|
1088
|
+
};
|
|
1089
|
+
const selectRuns = (candidateSessionId) => database.query(`
|
|
1090
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
1091
|
+
created_at, updated_at, completed_at
|
|
1092
|
+
FROM zhivex_cli_session_runs
|
|
1093
|
+
WHERE session_id = ?1
|
|
1094
|
+
ORDER BY ordinal ASC
|
|
1095
|
+
`).all(candidateSessionId).map(toRunReference);
|
|
1096
|
+
const nextActivitySequence = () => database.query(`
|
|
1097
|
+
SELECT COALESCE(MAX(activity_seq), 0) + 1 AS count
|
|
1098
|
+
FROM zhivex_cli_sessions
|
|
1099
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
1100
|
+
`).get(workspaceKey, scopeKey)?.count ?? 1;
|
|
1101
|
+
const sessionBase = (row) => ({
|
|
1102
|
+
schemaVersion: HARNESS_SESSION_SCHEMA_VERSION,
|
|
1103
|
+
sessionId: row.session_id,
|
|
1104
|
+
workspaceKey,
|
|
1105
|
+
scopeKey,
|
|
1106
|
+
...row.title ? { title: row.title } : {},
|
|
1107
|
+
...row.parent_session_id ? { parentSessionId: row.parent_session_id } : {},
|
|
1108
|
+
...row.forked_from_turn_id ? { forkedFromTurnId: row.forked_from_turn_id } : {},
|
|
1109
|
+
revision: row.revision,
|
|
1110
|
+
createdAt: row.created_at,
|
|
1111
|
+
updatedAt: row.updated_at,
|
|
1112
|
+
...row.archived_at === null ? {} : { archivedAt: row.archived_at },
|
|
1113
|
+
...row.deleted_at === null ? {} : { deletedAt: row.deleted_at }
|
|
1114
|
+
});
|
|
1115
|
+
const hydrate = (row) => ({
|
|
1116
|
+
...sessionBase(row),
|
|
1117
|
+
runs: selectRuns(row.session_id)
|
|
1118
|
+
});
|
|
1119
|
+
const latestRunRow = (candidateSessionId) => database.query(`
|
|
1120
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
1121
|
+
created_at, updated_at, completed_at
|
|
1122
|
+
FROM zhivex_cli_session_runs
|
|
1123
|
+
WHERE session_id = ?1
|
|
1124
|
+
ORDER BY ordinal DESC
|
|
1125
|
+
LIMIT 1
|
|
1126
|
+
`).get(candidateSessionId);
|
|
1127
|
+
const assertNoActiveRun = (candidateSessionId, action) => {
|
|
1128
|
+
const latest = latestRunRow(candidateSessionId);
|
|
1129
|
+
if (latest && ACTIVE_STATUSES.has(latest.status)) {
|
|
1130
|
+
throw new Error(`Cannot ${action} session ${candidateSessionId} while run ${latest.run_id} is ${latest.status}.`);
|
|
1131
|
+
}
|
|
1132
|
+
};
|
|
1133
|
+
const insertRun = (candidateSessionId, input, sequence, sourceTurnId) => {
|
|
1134
|
+
const runId = assertIdentifier("runId", input.runId);
|
|
1135
|
+
const provider = assertIdentifier("provider", input.provider);
|
|
1136
|
+
const model = assertIdentifier("model", input.model);
|
|
1137
|
+
const role = input.role === undefined ? null : assertIdentifier("role", input.role);
|
|
1138
|
+
const status = input.status ?? "created";
|
|
1139
|
+
if (!SESSION_RUN_STATUSES.includes(status)) {
|
|
1140
|
+
throw new Error(`Unsupported session run status: ${String(status)}.`);
|
|
1141
|
+
}
|
|
1142
|
+
const createdAt = input.createdAt ?? now();
|
|
1143
|
+
assertTimestamp("createdAt", createdAt);
|
|
1144
|
+
const entryBytes = metadataSize({ runId, provider, model, role });
|
|
1145
|
+
if (entryBytes > maxMetadataBytes) {
|
|
1146
|
+
throw new Error(`Run reference metadata exceeds the ${maxMetadataBytes}-byte limit.`);
|
|
1147
|
+
}
|
|
1148
|
+
const candidateTurnId = turnId();
|
|
1149
|
+
database.query(`
|
|
1150
|
+
INSERT INTO zhivex_cli_session_runs (
|
|
1151
|
+
turn_id, source_turn_id, session_id, ordinal, run_id, provider, model, role,
|
|
1152
|
+
status, created_at, updated_at, completed_at, metadata_bytes
|
|
1153
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, ?9, ?10, ?10, ?11, ?12)
|
|
1154
|
+
`).run(candidateTurnId, sourceTurnId ?? null, candidateSessionId, sequence, runId, provider, model, role, status, createdAt, isTerminal(status) ? createdAt : null, entryBytes);
|
|
1155
|
+
};
|
|
1156
|
+
try {
|
|
1157
|
+
const openedEntry = await lstat2(databasePath);
|
|
1158
|
+
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
1159
|
+
throw new Error(`The session SQLite path changed while it was being opened: ${databasePath}.`);
|
|
1160
|
+
}
|
|
1161
|
+
database.exec("PRAGMA journal_mode = WAL");
|
|
1162
|
+
database.exec("PRAGMA busy_timeout = 5000");
|
|
1163
|
+
database.exec("PRAGMA foreign_keys = ON");
|
|
1164
|
+
database.exec(`
|
|
1165
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_schema (
|
|
1166
|
+
singleton INTEGER PRIMARY KEY CHECK (singleton = 1),
|
|
1167
|
+
version INTEGER NOT NULL
|
|
1168
|
+
);
|
|
1169
|
+
INSERT OR IGNORE INTO zhivex_cli_session_schema (singleton, version)
|
|
1170
|
+
VALUES (1, ${HARNESS_SESSION_SCHEMA_VERSION});
|
|
1171
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_sessions (
|
|
1172
|
+
session_id TEXT PRIMARY KEY,
|
|
1173
|
+
workspace_key TEXT NOT NULL,
|
|
1174
|
+
scope_key TEXT NOT NULL,
|
|
1175
|
+
title TEXT,
|
|
1176
|
+
parent_session_id TEXT,
|
|
1177
|
+
forked_from_turn_id TEXT,
|
|
1178
|
+
revision INTEGER NOT NULL DEFAULT 0,
|
|
1179
|
+
activity_seq INTEGER NOT NULL,
|
|
1180
|
+
created_at INTEGER NOT NULL,
|
|
1181
|
+
updated_at INTEGER NOT NULL,
|
|
1182
|
+
archived_at INTEGER,
|
|
1183
|
+
deleted_at INTEGER,
|
|
1184
|
+
metadata_bytes INTEGER NOT NULL,
|
|
1185
|
+
CHECK (length(session_id) <= 260),
|
|
1186
|
+
CHECK (metadata_bytes >= 0)
|
|
1187
|
+
);
|
|
1188
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_sessions_latest
|
|
1189
|
+
ON zhivex_cli_sessions (workspace_key, scope_key, deleted_at, archived_at, activity_seq DESC);
|
|
1190
|
+
CREATE TABLE IF NOT EXISTS zhivex_cli_session_runs (
|
|
1191
|
+
turn_id TEXT PRIMARY KEY,
|
|
1192
|
+
source_turn_id TEXT,
|
|
1193
|
+
session_id TEXT NOT NULL REFERENCES zhivex_cli_sessions(session_id) ON DELETE CASCADE,
|
|
1194
|
+
ordinal INTEGER NOT NULL,
|
|
1195
|
+
run_id TEXT NOT NULL,
|
|
1196
|
+
provider TEXT NOT NULL,
|
|
1197
|
+
model TEXT NOT NULL,
|
|
1198
|
+
role TEXT,
|
|
1199
|
+
status TEXT NOT NULL CHECK (status IN ('created', 'running', 'waiting_approval', 'cancel_requested', 'completed', 'failed', 'cancelled', 'timed_out')),
|
|
1200
|
+
created_at INTEGER NOT NULL,
|
|
1201
|
+
updated_at INTEGER NOT NULL,
|
|
1202
|
+
completed_at INTEGER,
|
|
1203
|
+
metadata_bytes INTEGER NOT NULL,
|
|
1204
|
+
UNIQUE (session_id, ordinal),
|
|
1205
|
+
UNIQUE (session_id, run_id),
|
|
1206
|
+
CHECK (ordinal >= 0),
|
|
1207
|
+
CHECK (metadata_bytes >= 0)
|
|
1208
|
+
);
|
|
1209
|
+
CREATE INDEX IF NOT EXISTS zhivex_cli_session_runs_latest
|
|
1210
|
+
ON zhivex_cli_session_runs (session_id, ordinal DESC);
|
|
1211
|
+
`);
|
|
1212
|
+
const schemaVersion = database.query("SELECT version FROM zhivex_cli_session_schema WHERE singleton = 1").get()?.version;
|
|
1213
|
+
if (schemaVersion !== HARNESS_SESSION_SCHEMA_VERSION) {
|
|
1214
|
+
throw new Error(`Unsupported CLI session schema version: ${String(schemaVersion)}.`);
|
|
1215
|
+
}
|
|
1216
|
+
await chmod(databasePath, 384);
|
|
1217
|
+
} catch (error) {
|
|
1218
|
+
database.close(false);
|
|
1219
|
+
throw error;
|
|
1220
|
+
}
|
|
1221
|
+
const prune = async (input = {}) => transaction(() => {
|
|
1222
|
+
const before = input.before ?? Math.max(0, now() - retentionMs);
|
|
1223
|
+
assertTimestamp("before", before);
|
|
1224
|
+
const deletedSessions = database.query(`
|
|
1225
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
1226
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
1227
|
+
AND updated_at < ?3
|
|
1228
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
1229
|
+
`).get(workspaceKey, scopeKey, before)?.count ?? 0;
|
|
1230
|
+
database.query(`
|
|
1231
|
+
DELETE FROM zhivex_cli_sessions
|
|
1232
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
1233
|
+
AND updated_at < ?3
|
|
1234
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
1235
|
+
`).run(workspaceKey, scopeKey, before);
|
|
1236
|
+
return { before, deletedSessions };
|
|
1237
|
+
});
|
|
1238
|
+
const store = {
|
|
1239
|
+
databasePath,
|
|
1240
|
+
workspaceKey,
|
|
1241
|
+
scopeKey,
|
|
1242
|
+
async create(input = {}) {
|
|
1243
|
+
return transaction(() => {
|
|
1244
|
+
const cutoff = Math.max(0, now() - retentionMs);
|
|
1245
|
+
database.query(`
|
|
1246
|
+
DELETE FROM zhivex_cli_sessions
|
|
1247
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND updated_at < ?3
|
|
1248
|
+
AND (archived_at IS NOT NULL OR deleted_at IS NOT NULL)
|
|
1249
|
+
`).run(workspaceKey, scopeKey, cutoff);
|
|
1250
|
+
const count = database.query(`
|
|
1251
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
1252
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
1253
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
1254
|
+
if (count >= maxSessions) {
|
|
1255
|
+
throw new Error(`Session limit reached for this workspace and scope (${maxSessions}). Archive and prune an older session.`);
|
|
1256
|
+
}
|
|
1257
|
+
const createdAt = now();
|
|
1258
|
+
assertTimestamp("now", createdAt);
|
|
1259
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
1260
|
+
const candidateSessionId = sessionId();
|
|
1261
|
+
const activitySequence = nextActivitySequence();
|
|
1262
|
+
database.query(`
|
|
1263
|
+
INSERT INTO zhivex_cli_sessions (
|
|
1264
|
+
session_id, workspace_key, scope_key, title, revision, created_at,
|
|
1265
|
+
updated_at, activity_seq, metadata_bytes
|
|
1266
|
+
) VALUES (?1, ?2, ?3, ?4, 0, ?5, ?5, ?6, ?7)
|
|
1267
|
+
`).run(candidateSessionId, workspaceKey, scopeKey, title, createdAt, activitySequence, metadataSize({ title }));
|
|
1268
|
+
if (input.initialRun) {
|
|
1269
|
+
insertRun(candidateSessionId, input.initialRun, 0);
|
|
1270
|
+
}
|
|
1271
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
1272
|
+
});
|
|
1273
|
+
},
|
|
1274
|
+
async get(candidateSessionId, input = {}) {
|
|
1275
|
+
assertOpen();
|
|
1276
|
+
const row = selectSessionRow(candidateSessionId, input.includeDeleted ?? false);
|
|
1277
|
+
return row ? hydrate(row) : undefined;
|
|
1278
|
+
},
|
|
1279
|
+
async list(query = {}) {
|
|
1280
|
+
assertOpen();
|
|
1281
|
+
const limit = boundedInteger("limit", query.limit, 50, 1);
|
|
1282
|
+
if (limit > MAX_LIST_LIMIT) {
|
|
1283
|
+
throw new Error(`limit cannot exceed ${MAX_LIST_LIMIT}.`);
|
|
1284
|
+
}
|
|
1285
|
+
const rows = database.query(`
|
|
1286
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
1287
|
+
created_at, updated_at, archived_at, deleted_at
|
|
1288
|
+
FROM zhivex_cli_sessions
|
|
1289
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
1290
|
+
${query.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
1291
|
+
${query.includeDeleted ? "" : "AND deleted_at IS NULL"}
|
|
1292
|
+
ORDER BY activity_seq DESC
|
|
1293
|
+
LIMIT ?3
|
|
1294
|
+
`).all(workspaceKey, scopeKey, limit);
|
|
1295
|
+
return rows.map((row) => {
|
|
1296
|
+
const latest = latestRunRow(row.session_id);
|
|
1297
|
+
const runCount = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(row.session_id)?.count ?? 0;
|
|
1298
|
+
return {
|
|
1299
|
+
...sessionBase(row),
|
|
1300
|
+
runCount,
|
|
1301
|
+
...latest ? { latestRun: toRunReference(latest) } : {}
|
|
1302
|
+
};
|
|
1303
|
+
});
|
|
1304
|
+
},
|
|
1305
|
+
async latest(input = {}) {
|
|
1306
|
+
assertOpen();
|
|
1307
|
+
const row = database.query(`
|
|
1308
|
+
SELECT session_id, title, parent_session_id, forked_from_turn_id, revision, activity_seq,
|
|
1309
|
+
created_at, updated_at, archived_at, deleted_at
|
|
1310
|
+
FROM zhivex_cli_sessions
|
|
1311
|
+
WHERE workspace_key = ?1 AND scope_key = ?2 AND deleted_at IS NULL
|
|
1312
|
+
${input.includeArchived ? "" : "AND archived_at IS NULL"}
|
|
1313
|
+
ORDER BY activity_seq DESC
|
|
1314
|
+
LIMIT 1
|
|
1315
|
+
`).get(workspaceKey, scopeKey);
|
|
1316
|
+
return row ? hydrate(row) : undefined;
|
|
1317
|
+
},
|
|
1318
|
+
async update(candidateSessionId, input) {
|
|
1319
|
+
return transaction(() => {
|
|
1320
|
+
const row = requireSessionRow(candidateSessionId);
|
|
1321
|
+
ensureRevision(row, input.expectedRevision);
|
|
1322
|
+
if (input.archived === true) {
|
|
1323
|
+
assertNoActiveRun(candidateSessionId, "archive");
|
|
1324
|
+
}
|
|
1325
|
+
const title = input.title === undefined ? row.title : normalizeTitle(input.title, maxMetadataBytes) ?? null;
|
|
1326
|
+
const updatedAt = now();
|
|
1327
|
+
assertTimestamp("now", updatedAt);
|
|
1328
|
+
const archivedAt = input.archived === undefined ? row.archived_at : input.archived ? updatedAt : null;
|
|
1329
|
+
const activitySequence = nextActivitySequence();
|
|
1330
|
+
database.query(`
|
|
1331
|
+
UPDATE zhivex_cli_sessions
|
|
1332
|
+
SET title = ?1, archived_at = ?2, updated_at = ?3, revision = revision + 1,
|
|
1333
|
+
metadata_bytes = ?4, activity_seq = ?5
|
|
1334
|
+
WHERE session_id = ?6 AND workspace_key = ?7 AND scope_key = ?8 AND revision = ?9
|
|
1335
|
+
`).run(title, archivedAt, updatedAt, metadataSize({ title }), activitySequence, candidateSessionId, workspaceKey, scopeKey, row.revision);
|
|
1336
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
1337
|
+
});
|
|
1338
|
+
},
|
|
1339
|
+
async rename(candidateSessionId, title, input = {}) {
|
|
1340
|
+
return store.update(candidateSessionId, {
|
|
1341
|
+
title,
|
|
1342
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
1343
|
+
});
|
|
1344
|
+
},
|
|
1345
|
+
async appendRun(candidateSessionId, input, appendOptions = {}) {
|
|
1346
|
+
return transaction(() => {
|
|
1347
|
+
const row = requireSessionRow(candidateSessionId);
|
|
1348
|
+
ensureRevision(row, appendOptions.expectedRevision);
|
|
1349
|
+
if (row.archived_at !== null) {
|
|
1350
|
+
throw new Error(`Cannot append a run to archived session ${candidateSessionId}.`);
|
|
1351
|
+
}
|
|
1352
|
+
const existing = database.query(`
|
|
1353
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
1354
|
+
created_at, updated_at, completed_at
|
|
1355
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
1356
|
+
`).get(candidateSessionId, input.runId);
|
|
1357
|
+
if (existing) {
|
|
1358
|
+
const requestedStatus = input.status ?? "created";
|
|
1359
|
+
if (existing.provider === input.provider && existing.model === input.model && (existing.role ?? undefined) === input.role && existing.status === requestedStatus) {
|
|
1360
|
+
return hydrate(row);
|
|
1361
|
+
}
|
|
1362
|
+
throw new Error(`Run ${input.runId} is already immutably bound to ${existing.provider}/${existing.model} in this session.`);
|
|
1363
|
+
}
|
|
1364
|
+
assertNoActiveRun(candidateSessionId, "start another run in");
|
|
1365
|
+
const count = database.query("SELECT COUNT(*) AS count FROM zhivex_cli_session_runs WHERE session_id = ?1").get(candidateSessionId)?.count ?? 0;
|
|
1366
|
+
if (count >= maxRuns) {
|
|
1367
|
+
throw new Error(`Run-reference limit reached for session ${candidateSessionId} (${maxRuns}).`);
|
|
1368
|
+
}
|
|
1369
|
+
insertRun(candidateSessionId, input, count);
|
|
1370
|
+
const updatedAt = now();
|
|
1371
|
+
assertTimestamp("now", updatedAt);
|
|
1372
|
+
const activitySequence = nextActivitySequence();
|
|
1373
|
+
database.query(`
|
|
1374
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
1375
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
1376
|
+
`).run(updatedAt, activitySequence, candidateSessionId, row.revision);
|
|
1377
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
1378
|
+
});
|
|
1379
|
+
},
|
|
1380
|
+
async updateRun(candidateSessionId, runId, input) {
|
|
1381
|
+
return transaction(() => {
|
|
1382
|
+
const session = requireSessionRow(candidateSessionId);
|
|
1383
|
+
ensureRevision(session, input.expectedSessionRevision);
|
|
1384
|
+
assertIdentifier("runId", runId);
|
|
1385
|
+
const run = database.query(`
|
|
1386
|
+
SELECT turn_id, source_turn_id, ordinal, run_id, provider, model, role, status,
|
|
1387
|
+
created_at, updated_at, completed_at
|
|
1388
|
+
FROM zhivex_cli_session_runs WHERE session_id = ?1 AND run_id = ?2
|
|
1389
|
+
`).get(candidateSessionId, runId);
|
|
1390
|
+
if (!run) {
|
|
1391
|
+
throw new Error(`Run ${runId} is not linked to session ${candidateSessionId}.`);
|
|
1392
|
+
}
|
|
1393
|
+
if (!STATUS_TRANSITIONS[run.status].has(input.status)) {
|
|
1394
|
+
throw new Error(`Run ${runId} cannot transition from ${run.status} to ${input.status}.`);
|
|
1395
|
+
}
|
|
1396
|
+
const updatedAt = input.updatedAt ?? now();
|
|
1397
|
+
assertTimestamp("updatedAt", updatedAt);
|
|
1398
|
+
if (updatedAt < run.updated_at) {
|
|
1399
|
+
throw new Error(`Run ${runId} update timestamp cannot move backwards.`);
|
|
1400
|
+
}
|
|
1401
|
+
const completedAt = isTerminal(input.status) ? input.completedAt ?? updatedAt : null;
|
|
1402
|
+
if (completedAt !== null) {
|
|
1403
|
+
assertTimestamp("completedAt", completedAt);
|
|
1404
|
+
if (completedAt < run.created_at || completedAt > updatedAt) {
|
|
1405
|
+
throw new Error("completedAt must be between the run creation and update timestamps.");
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
database.query(`
|
|
1409
|
+
UPDATE zhivex_cli_session_runs
|
|
1410
|
+
SET status = ?1, updated_at = ?2, completed_at = ?3
|
|
1411
|
+
WHERE session_id = ?4 AND run_id = ?5
|
|
1412
|
+
`).run(input.status, updatedAt, completedAt, candidateSessionId, runId);
|
|
1413
|
+
const activitySequence = nextActivitySequence();
|
|
1414
|
+
database.query(`
|
|
1415
|
+
UPDATE zhivex_cli_sessions SET updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
1416
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
1417
|
+
`).run(updatedAt, activitySequence, candidateSessionId, session.revision);
|
|
1418
|
+
return hydrate(requireSessionRow(candidateSessionId));
|
|
1419
|
+
});
|
|
1420
|
+
},
|
|
1421
|
+
async fork(candidateSessionId, input = {}) {
|
|
1422
|
+
return transaction(() => {
|
|
1423
|
+
const source = requireSessionRow(candidateSessionId);
|
|
1424
|
+
ensureRevision(source, input.expectedRevision);
|
|
1425
|
+
const sourceRuns = selectRuns(candidateSessionId);
|
|
1426
|
+
let forkRuns = sourceRuns;
|
|
1427
|
+
let forkedFromTurnId;
|
|
1428
|
+
if (input.atTurnId) {
|
|
1429
|
+
const index = sourceRuns.findIndex((run) => run.turnId === input.atTurnId);
|
|
1430
|
+
if (index < 0) {
|
|
1431
|
+
throw new Error(`Turn ${input.atTurnId} is not part of session ${candidateSessionId}.`);
|
|
1432
|
+
}
|
|
1433
|
+
forkRuns = sourceRuns.slice(0, index + 1);
|
|
1434
|
+
forkedFromTurnId = input.atTurnId;
|
|
1435
|
+
} else if (sourceRuns.length > 0) {
|
|
1436
|
+
forkedFromTurnId = sourceRuns.at(-1).turnId;
|
|
1437
|
+
}
|
|
1438
|
+
const branchPoint = forkRuns.at(-1);
|
|
1439
|
+
if (branchPoint && !isTerminal(branchPoint.status)) {
|
|
1440
|
+
throw new Error(`Cannot fork from non-terminal run ${branchPoint.runId} (${branchPoint.status}).`);
|
|
1441
|
+
}
|
|
1442
|
+
if (forkRuns.length > maxRuns) {
|
|
1443
|
+
throw new Error(`Fork history exceeds the ${maxRuns}-run limit.`);
|
|
1444
|
+
}
|
|
1445
|
+
const count = database.query(`
|
|
1446
|
+
SELECT COUNT(*) AS count FROM zhivex_cli_sessions
|
|
1447
|
+
WHERE workspace_key = ?1 AND scope_key = ?2
|
|
1448
|
+
`).get(workspaceKey, scopeKey)?.count ?? 0;
|
|
1449
|
+
if (count >= maxSessions) {
|
|
1450
|
+
throw new Error(`Session limit reached for this workspace and scope (${maxSessions}).`);
|
|
1451
|
+
}
|
|
1452
|
+
const createdAt = now();
|
|
1453
|
+
assertTimestamp("now", createdAt);
|
|
1454
|
+
const title = normalizeTitle(input.title, maxMetadataBytes) ?? source.title;
|
|
1455
|
+
const forkSessionId = sessionId();
|
|
1456
|
+
const activitySequence = nextActivitySequence();
|
|
1457
|
+
database.query(`
|
|
1458
|
+
INSERT INTO zhivex_cli_sessions (
|
|
1459
|
+
session_id, workspace_key, scope_key, title, parent_session_id,
|
|
1460
|
+
forked_from_turn_id, revision, created_at, updated_at, activity_seq, metadata_bytes
|
|
1461
|
+
) VALUES (?1, ?2, ?3, ?4, ?5, ?6, 0, ?7, ?7, ?8, ?9)
|
|
1462
|
+
`).run(forkSessionId, workspaceKey, scopeKey, title, candidateSessionId, forkedFromTurnId ?? null, createdAt, activitySequence, metadataSize({ title }));
|
|
1463
|
+
for (const [index, run] of forkRuns.entries()) {
|
|
1464
|
+
insertRun(forkSessionId, {
|
|
1465
|
+
runId: run.runId,
|
|
1466
|
+
provider: run.provider,
|
|
1467
|
+
model: run.model,
|
|
1468
|
+
...run.role ? { role: run.role } : {},
|
|
1469
|
+
status: run.status,
|
|
1470
|
+
createdAt: run.createdAt
|
|
1471
|
+
}, index, run.turnId);
|
|
1472
|
+
database.query(`
|
|
1473
|
+
UPDATE zhivex_cli_session_runs
|
|
1474
|
+
SET updated_at = ?1, completed_at = ?2
|
|
1475
|
+
WHERE session_id = ?3 AND ordinal = ?4
|
|
1476
|
+
`).run(run.updatedAt, run.completedAt ?? null, forkSessionId, index);
|
|
1477
|
+
}
|
|
1478
|
+
return hydrate(requireSessionRow(forkSessionId));
|
|
1479
|
+
});
|
|
1480
|
+
},
|
|
1481
|
+
async archive(candidateSessionId, input = {}) {
|
|
1482
|
+
return store.update(candidateSessionId, {
|
|
1483
|
+
archived: true,
|
|
1484
|
+
...input.expectedRevision === undefined ? {} : { expectedRevision: input.expectedRevision }
|
|
1485
|
+
});
|
|
1486
|
+
},
|
|
1487
|
+
async delete(candidateSessionId, input = {}) {
|
|
1488
|
+
return transaction(() => {
|
|
1489
|
+
const row = requireSessionRow(candidateSessionId);
|
|
1490
|
+
ensureRevision(row, input.expectedRevision);
|
|
1491
|
+
assertNoActiveRun(candidateSessionId, "delete");
|
|
1492
|
+
const deletedAt = now();
|
|
1493
|
+
assertTimestamp("now", deletedAt);
|
|
1494
|
+
const activitySequence = nextActivitySequence();
|
|
1495
|
+
database.query(`
|
|
1496
|
+
UPDATE zhivex_cli_sessions
|
|
1497
|
+
SET deleted_at = ?1, updated_at = ?1, activity_seq = ?2, revision = revision + 1
|
|
1498
|
+
WHERE session_id = ?3 AND revision = ?4
|
|
1499
|
+
`).run(deletedAt, activitySequence, candidateSessionId, row.revision);
|
|
1500
|
+
return hydrate(requireSessionRow(candidateSessionId, true));
|
|
1501
|
+
});
|
|
1502
|
+
},
|
|
1503
|
+
prune,
|
|
1504
|
+
close() {
|
|
1505
|
+
if (!closed) {
|
|
1506
|
+
closed = true;
|
|
1507
|
+
database.close(false);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
};
|
|
1511
|
+
return store;
|
|
1512
|
+
};
|
|
1513
|
+
var openSessionStore = openCliSessionStore;
|
|
443
1514
|
// src/execution-environment.ts
|
|
444
|
-
import { createHash as
|
|
1515
|
+
import { createHash as createHash6, randomUUID as randomUUID3 } from "crypto";
|
|
445
1516
|
import {
|
|
446
|
-
chmod,
|
|
447
|
-
lstat as
|
|
448
|
-
mkdir as
|
|
1517
|
+
chmod as chmod2,
|
|
1518
|
+
lstat as lstat4,
|
|
1519
|
+
mkdir as mkdir3,
|
|
449
1520
|
readFile as readFile2,
|
|
450
1521
|
readdir as readdir2,
|
|
451
|
-
realpath as
|
|
1522
|
+
realpath as realpath3,
|
|
452
1523
|
rename as rename2,
|
|
453
1524
|
rm,
|
|
454
1525
|
writeFile
|
|
455
1526
|
} from "fs/promises";
|
|
456
|
-
import
|
|
1527
|
+
import path6 from "path";
|
|
457
1528
|
import { createAgentExecutionEnvironmentBinding } from "@zhivex-ai/agents/beta";
|
|
458
1529
|
|
|
459
1530
|
// src/edit-contracts.ts
|
|
460
|
-
import { createHash as
|
|
461
|
-
import
|
|
1531
|
+
import { createHash as createHash4 } from "crypto";
|
|
1532
|
+
import path4 from "path";
|
|
462
1533
|
import { z } from "zod";
|
|
463
1534
|
var EDIT_CONTRACT_SCHEMA_VERSION = 1;
|
|
464
1535
|
var EDIT_DIGEST_ALGORITHM = "sha256";
|
|
@@ -467,7 +1538,7 @@ var MAX_EDIT_FILE_BYTES = 1024 * 1024;
|
|
|
467
1538
|
var MAX_EDIT_PROPOSAL_BYTES = 4 * 1024 * 1024;
|
|
468
1539
|
var SHA256_DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/;
|
|
469
1540
|
var fileDigestSchema = z.string().regex(SHA256_DIGEST_PATTERN, "Digest must use the format sha256:<64 lowercase hexadecimal characters>.");
|
|
470
|
-
var workspaceFilePathSchema = z.string().min(1).max(1024).refine((value) => !value.includes("\x00"), "Path cannot contain a NUL byte.").refine((value) => !
|
|
1541
|
+
var workspaceFilePathSchema = z.string().min(1).max(1024).refine((value) => !value.includes("\x00"), "Path cannot contain a NUL byte.").refine((value) => !path4.isAbsolute(value) && !/^[A-Za-z]:[\\/]/.test(value), "Path must be relative.").refine((value) => {
|
|
471
1542
|
const segments = value.split(/[\\/]+/);
|
|
472
1543
|
return segments.every((segment) => segment !== "" && segment !== "." && segment !== "..");
|
|
473
1544
|
}, "Path must be normalized and cannot contain empty, dot, or parent segments.");
|
|
@@ -557,7 +1628,7 @@ var mutationAuditEntrySchema = z.strictObject({
|
|
|
557
1628
|
timestamp: z.iso.datetime(),
|
|
558
1629
|
quarantineId: z.string().min(1).max(200).optional()
|
|
559
1630
|
});
|
|
560
|
-
var digest = (value) => `sha256:${
|
|
1631
|
+
var digest = (value) => `sha256:${createHash4("sha256").update(value).digest("hex")}`;
|
|
561
1632
|
var canonicalProposalPayload = (changes) => ({
|
|
562
1633
|
schemaVersion: EDIT_CONTRACT_SCHEMA_VERSION,
|
|
563
1634
|
kind: "edit-proposal",
|
|
@@ -592,9 +1663,9 @@ var editContractDocument = (kind, result) => ({
|
|
|
592
1663
|
});
|
|
593
1664
|
|
|
594
1665
|
// src/workspace.ts
|
|
595
|
-
import { createHash as
|
|
596
|
-
import { link, lstat, mkdir, open, readFile, readdir, realpath, rename, stat, unlink } from "fs/promises";
|
|
597
|
-
import
|
|
1666
|
+
import { createHash as createHash5, randomUUID as randomUUID2 } from "crypto";
|
|
1667
|
+
import { link, lstat as lstat3, mkdir as mkdir2, open as open2, readFile, readdir, realpath as realpath2, rename, stat as stat2, unlink } from "fs/promises";
|
|
1668
|
+
import path5 from "path";
|
|
598
1669
|
var HARD_IGNORES = new Set([".git", ".next", ".turbo", ".zhivex-harness", "coverage", "dist", "node_modules"]);
|
|
599
1670
|
var IGNORE_FILES = [".gitignore", ".zhivex-harnessignore"];
|
|
600
1671
|
var DEFAULT_CHECKS = ["test", "typecheck", "lint", "build"];
|
|
@@ -605,12 +1676,12 @@ var isSensitiveName = (name) => {
|
|
|
605
1676
|
return normalized === ".env" || normalized.startsWith(".env.") && normalized !== ".env.example" || normalized === ".npmrc" || normalized === "id_rsa" || normalized === "id_ed25519" || normalized.endsWith(".key") || normalized.endsWith(".pem") || normalized.endsWith(".p12") || normalized.endsWith(".pfx");
|
|
606
1677
|
};
|
|
607
1678
|
var isHardIgnored = (relativePath) => relativePath.split("/").some((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
608
|
-
var
|
|
609
|
-
const relative =
|
|
610
|
-
return relative === "" || !relative.startsWith(`..${
|
|
1679
|
+
var isInside2 = (root, candidate) => {
|
|
1680
|
+
const relative = path5.relative(root, candidate);
|
|
1681
|
+
return relative === "" || !relative.startsWith(`..${path5.sep}`) && relative !== ".." && !path5.isAbsolute(relative);
|
|
611
1682
|
};
|
|
612
|
-
var wirePath = (value) => value.split(
|
|
613
|
-
var digestBytes = (value) => `sha256:${
|
|
1683
|
+
var wirePath = (value) => value.split(path5.sep).join("/");
|
|
1684
|
+
var digestBytes = (value) => `sha256:${createHash5("sha256").update(value).digest("hex")}`;
|
|
614
1685
|
var truncate = (value, maxLength = MAX_TOOL_OUTPUT) => value.length <= maxLength ? value : `${value.slice(0, maxLength)}
|
|
615
1686
|
\u2026 output truncated (${value.length - maxLength} characters omitted)`;
|
|
616
1687
|
var cursorEncode = (value) => Buffer.from(JSON.stringify(value)).toString("base64url");
|
|
@@ -716,18 +1787,18 @@ class Workspace {
|
|
|
716
1787
|
this.root = root;
|
|
717
1788
|
}
|
|
718
1789
|
static async open(root) {
|
|
719
|
-
const resolved = await
|
|
720
|
-
if (!(await
|
|
1790
|
+
const resolved = await realpath2(path5.resolve(root));
|
|
1791
|
+
if (!(await stat2(resolved)).isDirectory())
|
|
721
1792
|
throw new Error(`The workspace is not a directory: ${root}`);
|
|
722
1793
|
return new Workspace(resolved);
|
|
723
1794
|
}
|
|
724
1795
|
lexicalPath(relativePath) {
|
|
725
1796
|
if (!relativePath || relativePath.includes("\x00"))
|
|
726
1797
|
throw new Error("The path must be a valid relative path.");
|
|
727
|
-
const candidate =
|
|
728
|
-
if (!
|
|
1798
|
+
const candidate = path5.resolve(this.root, relativePath);
|
|
1799
|
+
if (!isInside2(this.root, candidate))
|
|
729
1800
|
throw new Error(`The path escapes the workspace: ${relativePath}`);
|
|
730
|
-
const relative = wirePath(
|
|
1801
|
+
const relative = wirePath(path5.relative(this.root, candidate));
|
|
731
1802
|
if (relative && isHardIgnored(relative)) {
|
|
732
1803
|
const blocked = relative.split("/").find((segment) => HARD_IGNORES.has(segment.toLocaleLowerCase()) || isSensitiveName(segment));
|
|
733
1804
|
throw new Error(`The path is protected by the harness policy: ${blocked}`);
|
|
@@ -738,13 +1809,13 @@ class Workspace {
|
|
|
738
1809
|
const candidate = this.lexicalPath(relativePath);
|
|
739
1810
|
if (candidate === this.root && options.requireFile)
|
|
740
1811
|
throw new Error("The workspace root is not a regular file.");
|
|
741
|
-
const segments =
|
|
1812
|
+
const segments = path5.relative(this.root, candidate).split(path5.sep).filter(Boolean);
|
|
742
1813
|
let current = this.root;
|
|
743
1814
|
for (let index = 0;index < segments.length; index += 1) {
|
|
744
|
-
current =
|
|
1815
|
+
current = path5.join(current, segments[index]);
|
|
745
1816
|
let entry;
|
|
746
1817
|
try {
|
|
747
|
-
entry = await
|
|
1818
|
+
entry = await lstat3(current);
|
|
748
1819
|
} catch (error) {
|
|
749
1820
|
if (options.allowMissing && error.code === "ENOENT") {
|
|
750
1821
|
return { path: candidate, exists: false };
|
|
@@ -764,18 +1835,18 @@ class Workspace {
|
|
|
764
1835
|
}
|
|
765
1836
|
async readStableFile(relativePath, allowBinary = true) {
|
|
766
1837
|
const safe = await this.safePath(relativePath, { requireFile: true });
|
|
767
|
-
const before = await
|
|
1838
|
+
const before = await lstat3(safe.path);
|
|
768
1839
|
if (before.size > MAX_FILE_BYTES)
|
|
769
1840
|
throw new Error(`The file exceeds the ${MAX_FILE_BYTES}-byte limit.`);
|
|
770
1841
|
const contents = await readFile(safe.path);
|
|
771
|
-
const after = await
|
|
1842
|
+
const after = await lstat3(safe.path);
|
|
772
1843
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
773
1844
|
throw new Error(`The file changed while it was being read: ${relativePath}`);
|
|
774
1845
|
}
|
|
775
1846
|
if (!allowBinary && contents.includes(0))
|
|
776
1847
|
throw new Error("The file appears to be binary and cannot be read as text.");
|
|
777
1848
|
return {
|
|
778
|
-
path: wirePath(
|
|
1849
|
+
path: wirePath(path5.relative(this.root, safe.path)),
|
|
779
1850
|
absolutePath: safe.path,
|
|
780
1851
|
contents,
|
|
781
1852
|
digest: digestBytes(contents),
|
|
@@ -786,8 +1857,8 @@ class Workspace {
|
|
|
786
1857
|
const rules = [...inherited];
|
|
787
1858
|
for (const filename of IGNORE_FILES) {
|
|
788
1859
|
try {
|
|
789
|
-
const ignorePath =
|
|
790
|
-
const entry = await
|
|
1860
|
+
const ignorePath = path5.join(directory, filename);
|
|
1861
|
+
const entry = await lstat3(ignorePath);
|
|
791
1862
|
if (entry.isFile() && entry.size <= MAX_FILE_BYTES) {
|
|
792
1863
|
rules.push(...parseIgnoreRules(await readFile(ignorePath, "utf8"), base));
|
|
793
1864
|
}
|
|
@@ -807,11 +1878,11 @@ class Workspace {
|
|
|
807
1878
|
const relative = base ? `${base}/${entry.name}` : entry.name;
|
|
808
1879
|
if (isHardIgnored(relative) || entry.isSymbolicLink() || isIgnoredByRules(relative, entry.isDirectory(), rules))
|
|
809
1880
|
continue;
|
|
810
|
-
const absolute =
|
|
1881
|
+
const absolute = path5.join(directory, entry.name);
|
|
811
1882
|
if (entry.isDirectory())
|
|
812
1883
|
await walk(absolute, relative, rules);
|
|
813
1884
|
else if (entry.isFile())
|
|
814
|
-
files.push({ path: relative, size: (await
|
|
1885
|
+
files.push({ path: relative, size: (await lstat3(absolute)).size });
|
|
815
1886
|
}
|
|
816
1887
|
};
|
|
817
1888
|
await walk(this.root, "", []);
|
|
@@ -819,9 +1890,9 @@ class Workspace {
|
|
|
819
1890
|
}
|
|
820
1891
|
async listFiles(relativePath = ".", options = {}) {
|
|
821
1892
|
const start = await this.safePath(relativePath);
|
|
822
|
-
if (!(await
|
|
1893
|
+
if (!(await lstat3(start.path)).isDirectory())
|
|
823
1894
|
throw new Error("list_files requires a directory.");
|
|
824
|
-
const requestPath = wirePath(
|
|
1895
|
+
const requestPath = wirePath(path5.relative(this.root, start.path)) || ".";
|
|
825
1896
|
const input = typeof options === "number" ? { limit: options } : options;
|
|
826
1897
|
const limit = input.limit ?? 200;
|
|
827
1898
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 5000)
|
|
@@ -883,9 +1954,9 @@ class Workspace {
|
|
|
883
1954
|
if (!query || query.length > 200)
|
|
884
1955
|
throw new Error("The search query must be between 1 and 200 characters.");
|
|
885
1956
|
const start = await this.safePath(relativePath);
|
|
886
|
-
if (!(await
|
|
1957
|
+
if (!(await lstat3(start.path)).isDirectory())
|
|
887
1958
|
throw new Error("search_files requires a directory.");
|
|
888
|
-
const requestPath = wirePath(
|
|
1959
|
+
const requestPath = wirePath(path5.relative(this.root, start.path)) || ".";
|
|
889
1960
|
const limit = options.limit ?? options.maxMatches ?? 100;
|
|
890
1961
|
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 500)
|
|
891
1962
|
throw new Error("limit must be between 1 and 500.");
|
|
@@ -940,7 +2011,7 @@ class Workspace {
|
|
|
940
2011
|
};
|
|
941
2012
|
}
|
|
942
2013
|
audit(entry) {
|
|
943
|
-
const record = { id:
|
|
2014
|
+
const record = { id: randomUUID2(), timestamp: new Date().toISOString(), ...entry };
|
|
944
2015
|
this.auditEntries.push(record);
|
|
945
2016
|
return record;
|
|
946
2017
|
}
|
|
@@ -948,16 +2019,16 @@ class Workspace {
|
|
|
948
2019
|
return this.auditEntries.map((entry) => ({ ...entry }));
|
|
949
2020
|
}
|
|
950
2021
|
async secureWorkspaceDirectory(directory, mode = 493) {
|
|
951
|
-
const resolvedInput =
|
|
952
|
-
if (!
|
|
2022
|
+
const resolvedInput = path5.resolve(directory);
|
|
2023
|
+
if (!isInside2(this.root, resolvedInput)) {
|
|
953
2024
|
throw new Error("A mutation directory escapes the workspace.");
|
|
954
2025
|
}
|
|
955
|
-
const segments =
|
|
2026
|
+
const segments = path5.relative(this.root, resolvedInput).split(path5.sep).filter(Boolean);
|
|
956
2027
|
let current = this.root;
|
|
957
2028
|
for (const segment of segments) {
|
|
958
|
-
current =
|
|
2029
|
+
current = path5.join(current, segment);
|
|
959
2030
|
try {
|
|
960
|
-
const entry = await
|
|
2031
|
+
const entry = await lstat3(current);
|
|
961
2032
|
if (entry.isSymbolicLink())
|
|
962
2033
|
throw new Error("A mutation directory must not contain a symbolic link.");
|
|
963
2034
|
if (!entry.isDirectory())
|
|
@@ -966,26 +2037,26 @@ class Workspace {
|
|
|
966
2037
|
if (error.code !== "ENOENT")
|
|
967
2038
|
throw error;
|
|
968
2039
|
try {
|
|
969
|
-
await
|
|
2040
|
+
await mkdir2(current, { mode });
|
|
970
2041
|
} catch (mkdirError) {
|
|
971
2042
|
if (mkdirError.code !== "EEXIST")
|
|
972
2043
|
throw mkdirError;
|
|
973
|
-
const raced = await
|
|
2044
|
+
const raced = await lstat3(current);
|
|
974
2045
|
if (raced.isSymbolicLink() || !raced.isDirectory()) {
|
|
975
2046
|
throw new Error("A mutation directory changed during creation.");
|
|
976
2047
|
}
|
|
977
2048
|
}
|
|
978
2049
|
}
|
|
979
2050
|
}
|
|
980
|
-
const canonical = await
|
|
981
|
-
if (!
|
|
2051
|
+
const canonical = await realpath2(current);
|
|
2052
|
+
if (!isInside2(this.root, canonical))
|
|
982
2053
|
throw new Error("A mutation directory resolves outside the workspace.");
|
|
983
2054
|
return canonical;
|
|
984
2055
|
}
|
|
985
2056
|
async stageFile(target, contents, mode) {
|
|
986
|
-
const directory = await this.secureWorkspaceDirectory(
|
|
987
|
-
const temporary =
|
|
988
|
-
const handle = await
|
|
2057
|
+
const directory = await this.secureWorkspaceDirectory(path5.dirname(target));
|
|
2058
|
+
const temporary = path5.join(directory, `.${path5.basename(target)}.zhivex-${randomUUID2()}.tmp`);
|
|
2059
|
+
const handle = await open2(temporary, "wx", mode);
|
|
989
2060
|
try {
|
|
990
2061
|
await handle.writeFile(contents);
|
|
991
2062
|
await handle.chmod(mode);
|
|
@@ -1142,7 +2213,7 @@ class Workspace {
|
|
|
1142
2213
|
const destination = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1143
2214
|
if (destination.exists)
|
|
1144
2215
|
throw new Error(`Move destination already exists: ${parsed.destination}.`);
|
|
1145
|
-
await this.secureWorkspaceDirectory(
|
|
2216
|
+
await this.secureWorkspaceDirectory(path5.dirname(destination.path));
|
|
1146
2217
|
const destinationRecheck = await this.safePath(parsed.destination, { allowMissing: true });
|
|
1147
2218
|
if (destinationRecheck.exists)
|
|
1148
2219
|
throw new Error(`Move destination already exists: ${parsed.destination}.`);
|
|
@@ -1169,9 +2240,9 @@ class Workspace {
|
|
|
1169
2240
|
const segments = [".zhivex-harness", "quarantine"];
|
|
1170
2241
|
let current = this.root;
|
|
1171
2242
|
for (const segment of segments) {
|
|
1172
|
-
current =
|
|
2243
|
+
current = path5.join(current, segment);
|
|
1173
2244
|
try {
|
|
1174
|
-
const entry = await
|
|
2245
|
+
const entry = await lstat3(current);
|
|
1175
2246
|
if (entry.isSymbolicLink()) {
|
|
1176
2247
|
throw new Error("Harness quarantine must not resolve through a symbolic link.");
|
|
1177
2248
|
}
|
|
@@ -1182,37 +2253,37 @@ class Workspace {
|
|
|
1182
2253
|
if (error.code !== "ENOENT" || !create)
|
|
1183
2254
|
throw error;
|
|
1184
2255
|
try {
|
|
1185
|
-
await
|
|
2256
|
+
await mkdir2(current, { mode: 448 });
|
|
1186
2257
|
} catch (mkdirError) {
|
|
1187
2258
|
if (mkdirError.code !== "EEXIST")
|
|
1188
2259
|
throw mkdirError;
|
|
1189
|
-
const racedEntry = await
|
|
2260
|
+
const racedEntry = await lstat3(current);
|
|
1190
2261
|
if (racedEntry.isSymbolicLink() || !racedEntry.isDirectory()) {
|
|
1191
2262
|
throw new Error("Harness quarantine path changed during creation.");
|
|
1192
2263
|
}
|
|
1193
2264
|
}
|
|
1194
2265
|
}
|
|
1195
2266
|
}
|
|
1196
|
-
const resolved = await
|
|
1197
|
-
if (!
|
|
2267
|
+
const resolved = await realpath2(current);
|
|
2268
|
+
if (!isInside2(this.root, resolved))
|
|
1198
2269
|
throw new Error("Harness quarantine resolves outside the workspace.");
|
|
1199
2270
|
return resolved;
|
|
1200
2271
|
}
|
|
1201
2272
|
async writeManifest(manifest) {
|
|
1202
2273
|
const directory = await this.secureQuarantineDirectory(true);
|
|
1203
|
-
const target =
|
|
2274
|
+
const target = path5.join(directory, `${manifest.quarantineId}.json`);
|
|
1204
2275
|
const temporary = await this.stageFile(target, `${JSON.stringify(manifest, null, 2)}
|
|
1205
2276
|
`, 384);
|
|
1206
2277
|
await rename(temporary, target);
|
|
1207
2278
|
}
|
|
1208
2279
|
async readInternalRegularFile(target, label, maxBytes) {
|
|
1209
|
-
const before = await
|
|
2280
|
+
const before = await lstat3(target);
|
|
1210
2281
|
if (before.isSymbolicLink() || !before.isFile())
|
|
1211
2282
|
throw new Error(`${label} must be a regular non-symlink file.`);
|
|
1212
2283
|
if (before.size > maxBytes)
|
|
1213
2284
|
throw new Error(`${label} exceeds its size limit.`);
|
|
1214
2285
|
const contents = await readFile(target);
|
|
1215
|
-
const after = await
|
|
2286
|
+
const after = await lstat3(target);
|
|
1216
2287
|
if (after.isSymbolicLink() || !after.isFile() || before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
1217
2288
|
throw new Error(`${label} changed while it was being read.`);
|
|
1218
2289
|
}
|
|
@@ -1223,9 +2294,9 @@ class Workspace {
|
|
|
1223
2294
|
const source = await this.readStableFile(parsed.path);
|
|
1224
2295
|
if (source.digest !== parsed.expectedDigest)
|
|
1225
2296
|
throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
|
|
1226
|
-
const quarantineId = `${Date.now()}-${
|
|
2297
|
+
const quarantineId = `${Date.now()}-${randomUUID2()}`;
|
|
1227
2298
|
const directory = await this.secureQuarantineDirectory(true);
|
|
1228
|
-
const dataPath =
|
|
2299
|
+
const dataPath = path5.join(directory, `${quarantineId}.data`);
|
|
1229
2300
|
if ((await this.readStableFile(parsed.path)).digest !== parsed.expectedDigest)
|
|
1230
2301
|
throw new Error(`Stale quarantine rejected for ${parsed.path}.`);
|
|
1231
2302
|
await link(source.absolutePath, dataPath);
|
|
@@ -1247,7 +2318,7 @@ class Workspace {
|
|
|
1247
2318
|
await unlink(source.absolutePath);
|
|
1248
2319
|
} catch (error) {
|
|
1249
2320
|
await unlink(dataPath).catch(() => {});
|
|
1250
|
-
await unlink(
|
|
2321
|
+
await unlink(path5.join(directory, `${quarantineId}.json`)).catch(() => {});
|
|
1251
2322
|
throw error;
|
|
1252
2323
|
}
|
|
1253
2324
|
const audit = this.audit({ operation: "quarantine", path: parsed.path, beforeDigest: source.digest, quarantineId });
|
|
@@ -1258,7 +2329,7 @@ class Workspace {
|
|
|
1258
2329
|
const directory = await this.secureQuarantineDirectory(false);
|
|
1259
2330
|
let manifest;
|
|
1260
2331
|
try {
|
|
1261
|
-
const manifestFile = await this.readInternalRegularFile(
|
|
2332
|
+
const manifestFile = await this.readInternalRegularFile(path5.join(directory, `${parsed.quarantineId}.json`), "Quarantine manifest", 64 * 1024);
|
|
1262
2333
|
manifest = JSON.parse(manifestFile.contents.toString("utf8"));
|
|
1263
2334
|
} catch {
|
|
1264
2335
|
throw new Error(`Quarantine entry was not found: ${parsed.quarantineId}.`);
|
|
@@ -1266,7 +2337,7 @@ class Workspace {
|
|
|
1266
2337
|
if (manifest.schemaVersion !== 1 || manifest.quarantineId !== parsed.quarantineId || manifest.status !== "quarantined") {
|
|
1267
2338
|
throw new Error(`Quarantine entry is not restorable: ${parsed.quarantineId}.`);
|
|
1268
2339
|
}
|
|
1269
|
-
const dataPath =
|
|
2340
|
+
const dataPath = path5.join(directory, `${parsed.quarantineId}.data`);
|
|
1270
2341
|
const dataFile = await this.readInternalRegularFile(dataPath, "Quarantine payload", MAX_FILE_BYTES);
|
|
1271
2342
|
const digest2 = digestBytes(dataFile.contents);
|
|
1272
2343
|
if (digest2 !== manifest.digest || parsed.expectedDigest && parsed.expectedDigest !== digest2) {
|
|
@@ -1276,16 +2347,16 @@ class Workspace {
|
|
|
1276
2347
|
const destination = await this.safePath(destinationPath, { allowMissing: true });
|
|
1277
2348
|
if (destination.exists)
|
|
1278
2349
|
throw new Error(`Restore destination already exists: ${destinationPath}.`);
|
|
1279
|
-
await this.secureWorkspaceDirectory(
|
|
2350
|
+
await this.secureWorkspaceDirectory(path5.dirname(destination.path));
|
|
1280
2351
|
const destinationRecheck = await this.safePath(destinationPath, { allowMissing: true });
|
|
1281
2352
|
if (destinationRecheck.exists)
|
|
1282
2353
|
throw new Error(`Restore destination already exists: ${destinationPath}.`);
|
|
1283
|
-
const dataRecheck = await
|
|
2354
|
+
const dataRecheck = await lstat3(dataPath);
|
|
1284
2355
|
if (dataRecheck.isSymbolicLink() || !dataRecheck.isFile() || dataRecheck.dev !== dataFile.stat.dev || dataRecheck.ino !== dataFile.stat.ino || dataRecheck.size !== dataFile.stat.size || dataRecheck.mtimeMs !== dataFile.stat.mtimeMs) {
|
|
1285
2356
|
throw new Error("Quarantine payload changed before restore commit.");
|
|
1286
2357
|
}
|
|
1287
2358
|
await link(dataPath, destination.path);
|
|
1288
|
-
const linkedDestination = await
|
|
2359
|
+
const linkedDestination = await lstat3(destination.path);
|
|
1289
2360
|
if (linkedDestination.isSymbolicLink() || !linkedDestination.isFile() || linkedDestination.dev !== dataFile.stat.dev || linkedDestination.ino !== dataFile.stat.ino || digestBytes(await readFile(destination.path)) !== digest2) {
|
|
1290
2361
|
await unlink(destination.path).catch(() => {});
|
|
1291
2362
|
throw new Error("Restore destination did not match the quarantined payload.");
|
|
@@ -1354,7 +2425,7 @@ class Workspace {
|
|
|
1354
2425
|
var HARNESS_EXECUTION_ARTIFACT_SCHEMA_VERSION = 1;
|
|
1355
2426
|
var HARNESS_OCI_LABEL = "com.zhivex.harness.execution";
|
|
1356
2427
|
var HARNESS_OCI_LABEL_VALUE = "v1";
|
|
1357
|
-
var
|
|
2428
|
+
var TERMINAL_STATUSES2 = new Set([
|
|
1358
2429
|
"completed",
|
|
1359
2430
|
"failed",
|
|
1360
2431
|
"cancelled",
|
|
@@ -1377,22 +2448,22 @@ var BUILT_IN_TOOL_NAMES = new Set([
|
|
|
1377
2448
|
"apply_environment_patch",
|
|
1378
2449
|
"environment_status"
|
|
1379
2450
|
]);
|
|
1380
|
-
var digest2 = (value) => `sha256:${
|
|
1381
|
-
var runHash = (runId) =>
|
|
1382
|
-
var
|
|
1383
|
-
const relative =
|
|
1384
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
2451
|
+
var digest2 = (value) => `sha256:${createHash6("sha256").update(value).digest("hex")}`;
|
|
2452
|
+
var runHash = (runId) => createHash6("sha256").update(runId).digest("hex").slice(0, 24);
|
|
2453
|
+
var isInside3 = (root, candidate) => {
|
|
2454
|
+
const relative = path6.relative(root, candidate);
|
|
2455
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path6.sep}`) && !path6.isAbsolute(relative);
|
|
1385
2456
|
};
|
|
1386
2457
|
var privateDirectory = async (directory) => {
|
|
1387
|
-
await
|
|
1388
|
-
const entry = await
|
|
2458
|
+
await mkdir3(directory, { recursive: true, mode: 448 });
|
|
2459
|
+
const entry = await lstat4(directory);
|
|
1389
2460
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
1390
2461
|
throw new Error(`Execution artifact directory must be a real directory: ${directory}.`);
|
|
1391
2462
|
}
|
|
1392
|
-
await
|
|
2463
|
+
await chmod2(directory, 448);
|
|
1393
2464
|
};
|
|
1394
2465
|
var atomicJson = async (target, value) => {
|
|
1395
|
-
const temporary = `${target}.${
|
|
2466
|
+
const temporary = `${target}.${randomUUID3()}.tmp`;
|
|
1396
2467
|
await writeFile(temporary, `${JSON.stringify(value, null, 2)}
|
|
1397
2468
|
`, { encoding: "utf8", mode: 384 });
|
|
1398
2469
|
await rename2(temporary, target);
|
|
@@ -1494,8 +2565,8 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
1494
2565
|
const visit = async (directory, relativeDirectory = "") => {
|
|
1495
2566
|
for (const name of await readdir2(directory)) {
|
|
1496
2567
|
const relative = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
1497
|
-
const absolute =
|
|
1498
|
-
if (!
|
|
2568
|
+
const absolute = path6.join(directory, name);
|
|
2569
|
+
if (!isInside3(stagedRoot, absolute) || relative.length > 1024) {
|
|
1499
2570
|
throw new Error(`OCI workspace export contains an invalid path: ${relative}.`);
|
|
1500
2571
|
}
|
|
1501
2572
|
entries += 1;
|
|
@@ -1506,7 +2577,7 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
1506
2577
|
await rm(absolute, { recursive: true, force: true });
|
|
1507
2578
|
continue;
|
|
1508
2579
|
}
|
|
1509
|
-
const entry = await
|
|
2580
|
+
const entry = await lstat4(absolute);
|
|
1510
2581
|
if (entry.isSymbolicLink()) {
|
|
1511
2582
|
throw new Error(`OCI workspace export contains a symbolic link: ${relative}.`);
|
|
1512
2583
|
}
|
|
@@ -1522,10 +2593,10 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
1522
2593
|
throw new Error(`OCI workspace export exceeds the ${maxWorkspaceBytes}-byte workspace limit.`);
|
|
1523
2594
|
}
|
|
1524
2595
|
if (entry.size > maxFileWriteBytes) {
|
|
1525
|
-
const previous =
|
|
2596
|
+
const previous = path6.join(previousRoot, ...relative.split("/"));
|
|
1526
2597
|
let unchanged = false;
|
|
1527
2598
|
try {
|
|
1528
|
-
const previousEntry = await
|
|
2599
|
+
const previousEntry = await lstat4(previous);
|
|
1529
2600
|
unchanged = previousEntry.isFile() && !previousEntry.isSymbolicLink() && previousEntry.size === entry.size && digest2(await readFile2(previous)) === digest2(await readFile2(absolute));
|
|
1530
2601
|
} catch (error) {
|
|
1531
2602
|
if (error.code !== "ENOENT")
|
|
@@ -1540,7 +2611,7 @@ var validateSynchronizedSnapshot = async (stagedRoot, previousRoot, maxWorkspace
|
|
|
1540
2611
|
await visit(stagedRoot);
|
|
1541
2612
|
};
|
|
1542
2613
|
var replaceSnapshot = async (snapshotRoot, stagedRoot) => {
|
|
1543
|
-
const backupRoot = `${snapshotRoot}.${
|
|
2614
|
+
const backupRoot = `${snapshotRoot}.${randomUUID3()}.previous`;
|
|
1544
2615
|
await rename2(snapshotRoot, backupRoot);
|
|
1545
2616
|
try {
|
|
1546
2617
|
await rename2(stagedRoot, snapshotRoot);
|
|
@@ -1609,7 +2680,7 @@ ${result.stdout}`)) {
|
|
|
1609
2680
|
}
|
|
1610
2681
|
}
|
|
1611
2682
|
async run(request) {
|
|
1612
|
-
const resourceName = `zhivex-harness-${runHash(request.runId)}-${
|
|
2683
|
+
const resourceName = `zhivex-harness-${runHash(request.runId)}-${randomUUID3().slice(0, 8)}`;
|
|
1613
2684
|
const name = `${resourceName}-job`;
|
|
1614
2685
|
const exportName = `${resourceName}-export`;
|
|
1615
2686
|
const volumeName = `${resourceName}-workspace`;
|
|
@@ -1731,7 +2802,7 @@ ${result.stdout}`)) {
|
|
|
1731
2802
|
await forcedRemoval;
|
|
1732
2803
|
} else if (result.exitCode === 0) {
|
|
1733
2804
|
await this.cli(["pause", name]);
|
|
1734
|
-
const stagedRoot =
|
|
2805
|
+
const stagedRoot = path6.join(path6.dirname(request.snapshotRoot), `.workspace-sync-${randomUUID3()}`);
|
|
1735
2806
|
try {
|
|
1736
2807
|
await privateDirectory(stagedRoot);
|
|
1737
2808
|
try {
|
|
@@ -1865,14 +2936,14 @@ var collectSnapshotFiles = async (workspace) => {
|
|
|
1865
2936
|
do {
|
|
1866
2937
|
const page = await workspace.listFiles(".", { limit: 500, ...cursor ? { cursor } : {} });
|
|
1867
2938
|
for (const file of page.files) {
|
|
1868
|
-
const absolute =
|
|
1869
|
-
if (!
|
|
2939
|
+
const absolute = path6.join(workspace.root, ...file.path.split("/"));
|
|
2940
|
+
if (!isInside3(workspace.root, absolute))
|
|
1870
2941
|
throw new Error(`Snapshot file escaped its workspace: ${file.path}.`);
|
|
1871
|
-
const before = await
|
|
2942
|
+
const before = await lstat4(absolute);
|
|
1872
2943
|
if (before.isSymbolicLink() || !before.isFile())
|
|
1873
2944
|
throw new Error(`Snapshot source is not a regular file: ${file.path}.`);
|
|
1874
2945
|
const contents = await readFile2(absolute);
|
|
1875
|
-
const after = await
|
|
2946
|
+
const after = await lstat4(absolute);
|
|
1876
2947
|
if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size || before.mtimeMs !== after.mtimeMs) {
|
|
1877
2948
|
throw new Error(`Snapshot source changed while reading: ${file.path}.`);
|
|
1878
2949
|
}
|
|
@@ -1896,10 +2967,10 @@ var copyWorkspaceSnapshot = async (source, baseRoot, snapshotRoot, maxWorkspaceB
|
|
|
1896
2967
|
}
|
|
1897
2968
|
for (const file of files.values()) {
|
|
1898
2969
|
for (const root of [baseRoot, snapshotRoot]) {
|
|
1899
|
-
const target =
|
|
1900
|
-
await
|
|
2970
|
+
const target = path6.join(root, ...file.path.split("/"));
|
|
2971
|
+
await mkdir3(path6.dirname(target), { recursive: true, mode: 448 });
|
|
1901
2972
|
await writeFile(target, file.contents, { mode: file.mode });
|
|
1902
|
-
await
|
|
2973
|
+
await chmod2(target, file.mode);
|
|
1903
2974
|
}
|
|
1904
2975
|
}
|
|
1905
2976
|
return { files: files.size, bytes: totalBytes };
|
|
@@ -2110,21 +3181,21 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
2110
3181
|
}
|
|
2111
3182
|
};
|
|
2112
3183
|
const binding = createAgentExecutionEnvironmentBinding(manifest);
|
|
2113
|
-
const environmentRoot =
|
|
3184
|
+
const environmentRoot = path6.join(options.stateDirectory, "environments");
|
|
2114
3185
|
return {
|
|
2115
3186
|
manifest,
|
|
2116
3187
|
image,
|
|
2117
3188
|
runtime,
|
|
2118
3189
|
async acquire(request) {
|
|
2119
3190
|
await privateDirectory(environmentRoot);
|
|
2120
|
-
const directory =
|
|
3191
|
+
const directory = path6.join(environmentRoot, runHash(request.runId));
|
|
2121
3192
|
await privateDirectory(directory);
|
|
2122
|
-
const metadataPath =
|
|
2123
|
-
const baseRoot =
|
|
2124
|
-
const snapshotRoot =
|
|
3193
|
+
const metadataPath = path6.join(directory, "environment.json");
|
|
3194
|
+
const baseRoot = path6.join(directory, "base");
|
|
3195
|
+
const snapshotRoot = path6.join(directory, "workspace");
|
|
2125
3196
|
let metadata;
|
|
2126
3197
|
try {
|
|
2127
|
-
const entry = await
|
|
3198
|
+
const entry = await lstat4(metadataPath);
|
|
2128
3199
|
if (entry.isSymbolicLink() || !entry.isFile())
|
|
2129
3200
|
throw new Error("Execution metadata must be a regular file.");
|
|
2130
3201
|
metadata = JSON.parse(await readFile2(metadataPath, "utf8"));
|
|
@@ -2153,14 +3224,14 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
2153
3224
|
await atomicJson(metadataPath, metadata);
|
|
2154
3225
|
const workspace = await Workspace.open(snapshotRoot);
|
|
2155
3226
|
const base = await Workspace.open(baseRoot);
|
|
2156
|
-
const dependencyPath =
|
|
3227
|
+
const dependencyPath = path6.join(options.workspace.root, "node_modules");
|
|
2157
3228
|
let dependencyRoot;
|
|
2158
3229
|
try {
|
|
2159
|
-
const entry = await
|
|
3230
|
+
const entry = await lstat4(dependencyPath);
|
|
2160
3231
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
2161
3232
|
throw new Error("node_modules must be a real directory before it can be mounted read-only.");
|
|
2162
3233
|
}
|
|
2163
|
-
dependencyRoot = await
|
|
3234
|
+
dependencyRoot = await realpath3(dependencyPath);
|
|
2164
3235
|
} catch (error) {
|
|
2165
3236
|
if (error.code !== "ENOENT") {
|
|
2166
3237
|
throw error;
|
|
@@ -2261,7 +3332,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
2261
3332
|
}
|
|
2262
3333
|
let manifestDocument;
|
|
2263
3334
|
try {
|
|
2264
|
-
manifestDocument = JSON.parse(await readFile2(
|
|
3335
|
+
manifestDocument = JSON.parse(await readFile2(path6.join(snapshotRoot, "package.json"), "utf8"));
|
|
2265
3336
|
} catch (error) {
|
|
2266
3337
|
if (error.code === "ENOENT") {
|
|
2267
3338
|
throw new Error("The environment snapshot does not contain package.json.");
|
|
@@ -2299,7 +3370,7 @@ var createHarnessOciExecutionEnvironment = async (options) => {
|
|
|
2299
3370
|
};
|
|
2300
3371
|
};
|
|
2301
3372
|
var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
|
|
2302
|
-
const root =
|
|
3373
|
+
const root = path6.join(stateDirectory, "environments");
|
|
2303
3374
|
const result = { scanned: 0, deleted: 0, skipped: 0 };
|
|
2304
3375
|
let entries;
|
|
2305
3376
|
try {
|
|
@@ -2315,20 +3386,20 @@ var cleanupHarnessExecutionArtifacts = async (stateDirectory, before) => {
|
|
|
2315
3386
|
continue;
|
|
2316
3387
|
}
|
|
2317
3388
|
result.scanned += 1;
|
|
2318
|
-
const directory =
|
|
2319
|
-
const resolved = await
|
|
2320
|
-
if (!
|
|
3389
|
+
const directory = path6.join(root, entry.name);
|
|
3390
|
+
const resolved = await realpath3(directory);
|
|
3391
|
+
if (!isInside3(root, resolved)) {
|
|
2321
3392
|
result.skipped += 1;
|
|
2322
3393
|
continue;
|
|
2323
3394
|
}
|
|
2324
3395
|
try {
|
|
2325
|
-
const metadataPath =
|
|
2326
|
-
const metadataEntry = await
|
|
3396
|
+
const metadataPath = path6.join(directory, "environment.json");
|
|
3397
|
+
const metadataEntry = await lstat4(metadataPath);
|
|
2327
3398
|
if (metadataEntry.isSymbolicLink() || !metadataEntry.isFile())
|
|
2328
3399
|
throw new Error("unsafe metadata");
|
|
2329
3400
|
const metadata = JSON.parse(await readFile2(metadataPath, "utf8"));
|
|
2330
3401
|
const releasedAt = metadata.releasedAt ? Date.parse(metadata.releasedAt) : Number.NaN;
|
|
2331
|
-
if (!metadata.status || !
|
|
3402
|
+
if (!metadata.status || !TERMINAL_STATUSES2.has(metadata.status) || !Number.isFinite(releasedAt) || releasedAt >= before) {
|
|
2332
3403
|
result.skipped += 1;
|
|
2333
3404
|
continue;
|
|
2334
3405
|
}
|
|
@@ -2398,8 +3469,8 @@ var selectHarnessModel = (candidates, required) => {
|
|
|
2398
3469
|
};
|
|
2399
3470
|
};
|
|
2400
3471
|
// src/mcp.ts
|
|
2401
|
-
import { lstat as
|
|
2402
|
-
import
|
|
3472
|
+
import { lstat as lstat5, readFile as readFile3, realpath as realpath4 } from "fs/promises";
|
|
3473
|
+
import path7 from "path";
|
|
2403
3474
|
import {
|
|
2404
3475
|
createMcpToolSet,
|
|
2405
3476
|
serializeJsonValue
|
|
@@ -2449,8 +3520,8 @@ var configurationSchema = z2.object({
|
|
|
2449
3520
|
servers: z2.array(serverSchema).max(20)
|
|
2450
3521
|
}).strict();
|
|
2451
3522
|
var isInsidePath = (root, candidate) => {
|
|
2452
|
-
const relative =
|
|
2453
|
-
return relative === "" || relative !== ".." && !relative.startsWith(`..${
|
|
3523
|
+
const relative = path7.relative(root, candidate);
|
|
3524
|
+
return relative === "" || relative !== ".." && !relative.startsWith(`..${path7.sep}`) && !path7.isAbsolute(relative);
|
|
2454
3525
|
};
|
|
2455
3526
|
var validatedHttpUrl = (value, serverName) => {
|
|
2456
3527
|
let url;
|
|
@@ -2534,7 +3605,7 @@ var normalizeHarnessMcpConfiguration = (value) => {
|
|
|
2534
3605
|
return { schemaVersion: HARNESS_MCP_CONFIG_SCHEMA_VERSION, servers };
|
|
2535
3606
|
};
|
|
2536
3607
|
var loadHarnessMcpConfiguration = async (workspace, configPath) => {
|
|
2537
|
-
const entry = await
|
|
3608
|
+
const entry = await lstat5(configPath);
|
|
2538
3609
|
if (entry.isSymbolicLink() || !entry.isFile()) {
|
|
2539
3610
|
throw new Error(`MCP configuration must be a regular non-symlink file: ${configPath}.`);
|
|
2540
3611
|
}
|
|
@@ -2542,8 +3613,8 @@ var loadHarnessMcpConfiguration = async (workspace, configPath) => {
|
|
|
2542
3613
|
throw new Error("MCP configuration cannot exceed 1 MiB.");
|
|
2543
3614
|
}
|
|
2544
3615
|
const [canonicalWorkspace, canonicalConfig] = await Promise.all([
|
|
2545
|
-
|
|
2546
|
-
|
|
3616
|
+
realpath4(workspace),
|
|
3617
|
+
realpath4(configPath)
|
|
2547
3618
|
]);
|
|
2548
3619
|
if (!isInsidePath(canonicalWorkspace, canonicalConfig)) {
|
|
2549
3620
|
throw new Error("MCP configuration must remain inside the canonical workspace.");
|
|
@@ -2649,7 +3720,7 @@ var createHttpMcpClient = (server, env = process.env, fetchImplementation = fetc
|
|
|
2649
3720
|
throw new Error(`MCP server ${server.name} is not configured for HTTP transport.`);
|
|
2650
3721
|
}
|
|
2651
3722
|
const staticHeaders = resolveHeaders(server, env);
|
|
2652
|
-
let
|
|
3723
|
+
let sessionId2;
|
|
2653
3724
|
let requestId = 0;
|
|
2654
3725
|
let initialization;
|
|
2655
3726
|
const post = async (payload, options = {}, expectResponse = true) => {
|
|
@@ -2661,7 +3732,7 @@ var createHttpMcpClient = (server, env = process.env, fetchImplementation = fetc
|
|
|
2661
3732
|
"content-type": "application/json",
|
|
2662
3733
|
accept: "application/json, text/event-stream",
|
|
2663
3734
|
"mcp-protocol-version": "2025-06-18",
|
|
2664
|
-
...
|
|
3735
|
+
...sessionId2 ? { "mcp-session-id": sessionId2 } : {}
|
|
2665
3736
|
},
|
|
2666
3737
|
body: JSON.stringify(payload),
|
|
2667
3738
|
...options.abortSignal ? { signal: options.abortSignal } : {}
|
|
@@ -2670,7 +3741,7 @@ var createHttpMcpClient = (server, env = process.env, fetchImplementation = fetc
|
|
|
2670
3741
|
await response.body?.cancel();
|
|
2671
3742
|
throw new Error(`MCP server ${server.name} returned HTTP ${response.status}.`);
|
|
2672
3743
|
}
|
|
2673
|
-
|
|
3744
|
+
sessionId2 = response.headers.get("mcp-session-id") ?? sessionId2;
|
|
2674
3745
|
if (!expectResponse) {
|
|
2675
3746
|
await response.body?.cancel();
|
|
2676
3747
|
return;
|
|
@@ -2788,7 +3859,7 @@ var createHarnessMcpTools = async (configuration, options = {}) => {
|
|
|
2788
3859
|
};
|
|
2789
3860
|
var mcpConfigurationFingerprintInput = (configuration) => serializeJsonValue(configuration);
|
|
2790
3861
|
// src/orchestration.ts
|
|
2791
|
-
import { createHash as
|
|
3862
|
+
import { createHash as createHash7, randomUUID as randomUUID4 } from "crypto";
|
|
2792
3863
|
import {
|
|
2793
3864
|
applySafetyPolicyToAgent,
|
|
2794
3865
|
createBudgetGuard,
|
|
@@ -2835,7 +3906,7 @@ var childHarnessBinding = (parent, profile, model, toolNames, config) => ({
|
|
|
2835
3906
|
schemaVersion: 1,
|
|
2836
3907
|
id: `${parent.id}-${profile}`,
|
|
2837
3908
|
version: parent.version,
|
|
2838
|
-
fingerprint: `sha256:${
|
|
3909
|
+
fingerprint: `sha256:${createHash7("sha256").update(JSON.stringify({
|
|
2839
3910
|
parent: parent.fingerprint,
|
|
2840
3911
|
profile,
|
|
2841
3912
|
model: {
|
|
@@ -2935,7 +4006,7 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2935
4006
|
}
|
|
2936
4007
|
return { name: profile, agent };
|
|
2937
4008
|
});
|
|
2938
|
-
const groupId = input.groupId ?? `review_${
|
|
4009
|
+
const groupId = input.groupId ?? `review_${randomUUID4()}`;
|
|
2939
4010
|
const result = await runAgentGroup(members, {
|
|
2940
4011
|
prompt: input.prompt,
|
|
2941
4012
|
...input.scope ? { scope: input.scope } : {},
|
|
@@ -2958,13 +4029,13 @@ var runHarnessReviewGroup = async (runtime, input, profiles = ["explorer", "revi
|
|
|
2958
4029
|
};
|
|
2959
4030
|
};
|
|
2960
4031
|
// src/harness.ts
|
|
2961
|
-
import { createHash as
|
|
4032
|
+
import { createHash as createHash8 } from "crypto";
|
|
2962
4033
|
import {
|
|
2963
4034
|
Agent,
|
|
2964
4035
|
applySafetyPolicyToAgent as applySafetyPolicyToAgent2,
|
|
2965
4036
|
createBudgetGuard as createBudgetGuard2,
|
|
2966
4037
|
createProductionSafetyPolicy as createProductionSafetyPolicy2,
|
|
2967
|
-
createRedactionPolicy as
|
|
4038
|
+
createRedactionPolicy as createRedactionPolicy3,
|
|
2968
4039
|
getAgentBudgetStatus as getAgentBudgetStatus2,
|
|
2969
4040
|
tool
|
|
2970
4041
|
} from "@zhivex-ai/agents";
|
|
@@ -2976,12 +4047,12 @@ import { serializeJsonValue as serializeJsonValue2 } from "@zhivex-ai/core";
|
|
|
2976
4047
|
import { z as z3 } from "zod";
|
|
2977
4048
|
|
|
2978
4049
|
// src/operations.ts
|
|
2979
|
-
import { chmod as
|
|
2980
|
-
import
|
|
4050
|
+
import { chmod as chmod3, lstat as lstat6, mkdir as mkdir4, open as open3 } from "fs/promises";
|
|
4051
|
+
import path8 from "path";
|
|
2981
4052
|
import {
|
|
2982
4053
|
cancelAgentRun,
|
|
2983
4054
|
cancelAgentRunTree,
|
|
2984
|
-
createRedactionPolicy,
|
|
4055
|
+
createRedactionPolicy as createRedactionPolicy2,
|
|
2985
4056
|
getAgentBudgetStatus
|
|
2986
4057
|
} from "@zhivex-ai/agents";
|
|
2987
4058
|
import {
|
|
@@ -2994,63 +4065,7 @@ import {
|
|
|
2994
4065
|
createSqliteAgentRunStore
|
|
2995
4066
|
} from "@zhivex-ai/agents/ops";
|
|
2996
4067
|
import { createAgentRunLedger } from "@zhivex-ai/agents/control-plane";
|
|
2997
|
-
import { Database } from "bun:sqlite";
|
|
2998
|
-
|
|
2999
|
-
// src/state-directory.ts
|
|
3000
|
-
import { lstat as lstat4 } from "fs/promises";
|
|
3001
|
-
import path6 from "path";
|
|
3002
|
-
var SENSITIVE_STATE_SEGMENTS = new Set([".git", ".env", ".npmrc", "dist", "node_modules", "src"]);
|
|
3003
|
-
var isInside3 = (root, candidate) => {
|
|
3004
|
-
const relative = path6.relative(root, candidate);
|
|
3005
|
-
return relative === "" || !relative.startsWith(`..${path6.sep}`) && relative !== ".." && !path6.isAbsolute(relative);
|
|
3006
|
-
};
|
|
3007
|
-
var validateStateDirectory = async (workspace, stateDirectory) => {
|
|
3008
|
-
if (stateDirectory === workspace || stateDirectory === path6.parse(stateDirectory).root) {
|
|
3009
|
-
throw new Error("The state directory cannot be the workspace or filesystem root.");
|
|
3010
|
-
}
|
|
3011
|
-
const insideWorkspace = isInside3(workspace, stateDirectory);
|
|
3012
|
-
const relativeSegments = insideWorkspace ? path6.relative(workspace, stateDirectory).split(path6.sep).filter(Boolean) : stateDirectory.slice(path6.parse(stateDirectory).root.length).split(path6.sep).filter(Boolean);
|
|
3013
|
-
const sensitiveSegment = insideWorkspace ? relativeSegments.find((segment) => SENSITIVE_STATE_SEGMENTS.has(segment)) : undefined;
|
|
3014
|
-
if (sensitiveSegment) {
|
|
3015
|
-
throw new Error(`The state directory is inside the protected workspace path: ${sensitiveSegment}.`);
|
|
3016
|
-
}
|
|
3017
|
-
if (!insideWorkspace) {
|
|
3018
|
-
try {
|
|
3019
|
-
const externalEntry = await lstat4(stateDirectory);
|
|
3020
|
-
if (externalEntry.isSymbolicLink()) {
|
|
3021
|
-
throw new Error(`The state directory must not be a symbolic link: ${stateDirectory}.`);
|
|
3022
|
-
}
|
|
3023
|
-
if (!externalEntry.isDirectory()) {
|
|
3024
|
-
throw new Error(`The state directory is not a directory: ${stateDirectory}.`);
|
|
3025
|
-
}
|
|
3026
|
-
} catch (error) {
|
|
3027
|
-
if (error.code !== "ENOENT") {
|
|
3028
|
-
throw error;
|
|
3029
|
-
}
|
|
3030
|
-
}
|
|
3031
|
-
return;
|
|
3032
|
-
}
|
|
3033
|
-
let current = workspace;
|
|
3034
|
-
for (const segment of relativeSegments) {
|
|
3035
|
-
current = path6.join(current, segment);
|
|
3036
|
-
try {
|
|
3037
|
-
const entry = await lstat4(current);
|
|
3038
|
-
if (entry.isSymbolicLink()) {
|
|
3039
|
-
throw new Error(`The state directory must not resolve through a symbolic link: ${current}.`);
|
|
3040
|
-
}
|
|
3041
|
-
if (!entry.isDirectory()) {
|
|
3042
|
-
throw new Error(`The state directory path contains a non-directory entry: ${current}.`);
|
|
3043
|
-
}
|
|
3044
|
-
} catch (error) {
|
|
3045
|
-
if (error.code === "ENOENT") {
|
|
3046
|
-
break;
|
|
3047
|
-
}
|
|
3048
|
-
throw error;
|
|
3049
|
-
}
|
|
3050
|
-
}
|
|
3051
|
-
};
|
|
3052
|
-
|
|
3053
|
-
// src/operations.ts
|
|
4068
|
+
import { Database as Database2 } from "bun:sqlite";
|
|
3054
4069
|
var HARNESS_SQLITE_FILE = "operations.sqlite";
|
|
3055
4070
|
var HARNESS_OPERATIONS_SCHEMA_VERSION = 1;
|
|
3056
4071
|
var TERMINAL_RUN_STATUSES = ["completed", "failed", "cancelled", "timed_out"];
|
|
@@ -3098,7 +4113,7 @@ var inspectHarnessRun = async (store, config, runId) => {
|
|
|
3098
4113
|
if (!state) {
|
|
3099
4114
|
throw new Error(`Run ${runId} was not found in ${config.stateDirectory}.`);
|
|
3100
4115
|
}
|
|
3101
|
-
const redaction =
|
|
4116
|
+
const redaction = createRedactionPolicy2({ includeEmails: true });
|
|
3102
4117
|
const traceOptions = createProductionTraceOptions({
|
|
3103
4118
|
includeMessages: false,
|
|
3104
4119
|
includeToolInputs: false,
|
|
@@ -3220,12 +4235,12 @@ var sqliteAdapter = (database) => ({
|
|
|
3220
4235
|
}
|
|
3221
4236
|
});
|
|
3222
4237
|
var ensurePrivateStateDirectory = async (directory) => {
|
|
3223
|
-
await
|
|
3224
|
-
const entry = await
|
|
4238
|
+
await mkdir4(directory, { recursive: true, mode: 448 });
|
|
4239
|
+
const entry = await lstat6(directory);
|
|
3225
4240
|
if (entry.isSymbolicLink() || !entry.isDirectory()) {
|
|
3226
4241
|
throw new Error(`The state directory must be a real directory: ${directory}.`);
|
|
3227
4242
|
}
|
|
3228
|
-
await
|
|
4243
|
+
await chmod3(directory, 448);
|
|
3229
4244
|
};
|
|
3230
4245
|
var migrateJournalEntry = (entry, state, config) => ({
|
|
3231
4246
|
...entry,
|
|
@@ -3277,35 +4292,35 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
3277
4292
|
backend: "file",
|
|
3278
4293
|
store,
|
|
3279
4294
|
memory: createFileAgentMemoryStore({
|
|
3280
|
-
directory:
|
|
4295
|
+
directory: path8.join(config.stateDirectory, "memory"),
|
|
3281
4296
|
scope: config.scope
|
|
3282
4297
|
}),
|
|
3283
4298
|
migration,
|
|
3284
4299
|
close() {}
|
|
3285
4300
|
};
|
|
3286
4301
|
}
|
|
3287
|
-
const databasePath =
|
|
4302
|
+
const databasePath = path8.join(config.stateDirectory, HARNESS_SQLITE_FILE);
|
|
3288
4303
|
let databaseEntry;
|
|
3289
4304
|
try {
|
|
3290
|
-
databaseEntry = await
|
|
4305
|
+
databaseEntry = await lstat6(databasePath);
|
|
3291
4306
|
} catch (error) {
|
|
3292
4307
|
if (error.code !== "ENOENT") {
|
|
3293
4308
|
throw error;
|
|
3294
4309
|
}
|
|
3295
4310
|
try {
|
|
3296
|
-
const handle = await
|
|
4311
|
+
const handle = await open3(databasePath, "wx", 384);
|
|
3297
4312
|
await handle.close();
|
|
3298
4313
|
} catch (creationError) {
|
|
3299
4314
|
if (creationError.code !== "EEXIST") {
|
|
3300
4315
|
throw creationError;
|
|
3301
4316
|
}
|
|
3302
4317
|
}
|
|
3303
|
-
databaseEntry = await
|
|
4318
|
+
databaseEntry = await lstat6(databasePath);
|
|
3304
4319
|
}
|
|
3305
4320
|
if (databaseEntry.isSymbolicLink() || !databaseEntry.isFile()) {
|
|
3306
4321
|
throw new Error(`The SQLite state path must be a real file: ${databasePath}.`);
|
|
3307
4322
|
}
|
|
3308
|
-
const database = new
|
|
4323
|
+
const database = new Database2(databasePath, { create: false, strict: true });
|
|
3309
4324
|
let closed = false;
|
|
3310
4325
|
const closeDatabase = () => {
|
|
3311
4326
|
if (!closed) {
|
|
@@ -3314,14 +4329,14 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
3314
4329
|
}
|
|
3315
4330
|
};
|
|
3316
4331
|
try {
|
|
3317
|
-
const openedEntry = await
|
|
4332
|
+
const openedEntry = await lstat6(databasePath);
|
|
3318
4333
|
if (openedEntry.isSymbolicLink() || !openedEntry.isFile() || openedEntry.dev !== databaseEntry.dev || openedEntry.ino !== databaseEntry.ino) {
|
|
3319
4334
|
throw new Error(`The SQLite state path changed while it was being opened: ${databasePath}.`);
|
|
3320
4335
|
}
|
|
3321
4336
|
database.exec("PRAGMA journal_mode = WAL");
|
|
3322
4337
|
database.exec("PRAGMA busy_timeout = 5000");
|
|
3323
4338
|
database.exec("PRAGMA foreign_keys = ON");
|
|
3324
|
-
await
|
|
4339
|
+
await chmod3(databasePath, 384);
|
|
3325
4340
|
const databaseLike = sqliteAdapter(database);
|
|
3326
4341
|
const store = createSqliteAgentRunStore({ db: databaseLike, scope: config.scope });
|
|
3327
4342
|
const memory = createSqliteAgentMemoryStore({ db: databaseLike, scope: config.scope });
|
|
@@ -3343,11 +4358,11 @@ var openHarnessPersistence = async (config, options = {}) => {
|
|
|
3343
4358
|
// src/harness.ts
|
|
3344
4359
|
var APPROVAL_VERSION = "2026-08-17-v5";
|
|
3345
4360
|
var TOOL_CONTRACT_VERSION = "enforced-execution-v1";
|
|
3346
|
-
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, executionEnvironment) => ({
|
|
4361
|
+
var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, providerTransportFingerprint, executionEnvironment) => ({
|
|
3347
4362
|
schemaVersion: 1,
|
|
3348
4363
|
id: "zhivex-harness",
|
|
3349
4364
|
version: HARNESS_VERSION,
|
|
3350
|
-
fingerprint: `sha256:${
|
|
4365
|
+
fingerprint: `sha256:${createHash8("sha256").update(JSON.stringify({
|
|
3351
4366
|
configSchemaVersion: HARNESS_CONFIG_SCHEMA_VERSION,
|
|
3352
4367
|
approvalVersion: APPROVAL_VERSION,
|
|
3353
4368
|
toolContractVersion: TOOL_CONTRACT_VERSION,
|
|
@@ -3355,6 +4370,7 @@ var createHarnessBinding = (config, mcpConfiguration, model, subagentModels, exe
|
|
|
3355
4370
|
provider: config.provider,
|
|
3356
4371
|
model: config.model,
|
|
3357
4372
|
runtimeModel: inspectHarnessModelCapabilities(model),
|
|
4373
|
+
providerTransportFingerprint,
|
|
3358
4374
|
subagentModels: Object.fromEntries(config.orchestration.profiles.map((profile) => [
|
|
3359
4375
|
profile,
|
|
3360
4376
|
inspectHarnessModelCapabilities(subagentModels?.[profile] ?? model)
|
|
@@ -3426,7 +4442,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3426
4442
|
cursor: z3.string().min(1).max(2000).optional()
|
|
3427
4443
|
}),
|
|
3428
4444
|
metadata: readOnlyMetadata,
|
|
3429
|
-
execute: async ({ path:
|
|
4445
|
+
execute: async ({ path: path9, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).listFiles(path9, {
|
|
3430
4446
|
limit,
|
|
3431
4447
|
...cursor ? { cursor } : {}
|
|
3432
4448
|
}))
|
|
@@ -3440,7 +4456,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3440
4456
|
endLine: z3.number().int().min(1).optional()
|
|
3441
4457
|
}),
|
|
3442
4458
|
metadata: readOnlyMetadata,
|
|
3443
|
-
execute: async ({ path:
|
|
4459
|
+
execute: async ({ path: path9, startLine, endLine }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).readFile(path9, startLine, endLine))
|
|
3444
4460
|
}),
|
|
3445
4461
|
search_files: tool({
|
|
3446
4462
|
name: "search_files",
|
|
@@ -3453,7 +4469,7 @@ var createWorkspaceTools = (workspace, allowedChecks) => ({
|
|
|
3453
4469
|
cursor: z3.string().min(1).max(2000).optional()
|
|
3454
4470
|
}),
|
|
3455
4471
|
metadata: readOnlyMetadata,
|
|
3456
|
-
execute: async ({ query, path:
|
|
4472
|
+
execute: async ({ query, path: path9, caseSensitive, limit, cursor }, context) => serializeJsonValue2(await (harnessExecutionSession(context)?.workspace ?? workspace).searchFiles(query, path9, {
|
|
3457
4473
|
caseSensitive,
|
|
3458
4474
|
limit,
|
|
3459
4475
|
...cursor ? { cursor } : {}
|
|
@@ -3577,32 +4593,49 @@ var createExecutionEnvironmentTools = (workspace) => ({
|
|
|
3577
4593
|
})
|
|
3578
4594
|
});
|
|
3579
4595
|
var estimateMessageTokens = (messages) => Math.max(1, Math.ceil(JSON.stringify(messages).length / 4));
|
|
3580
|
-
var
|
|
3581
|
-
const redaction =
|
|
3582
|
-
|
|
3583
|
-
const
|
|
3584
|
-
|
|
3585
|
-
|
|
3586
|
-
|
|
3587
|
-
|
|
3588
|
-
|
|
3589
|
-
|
|
3590
|
-
|
|
3591
|
-
|
|
3592
|
-
|
|
3593
|
-
|
|
3594
|
-
|
|
3595
|
-
|
|
3596
|
-
|
|
3597
|
-
|
|
3598
|
-
return typeof part.type === "string" ? part.type : "part";
|
|
3599
|
-
});
|
|
3600
|
-
return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
|
|
4596
|
+
var summarizeHarnessMessages = (messages) => {
|
|
4597
|
+
const redaction = createRedactionPolicy3({ includeEmails: true });
|
|
4598
|
+
const summaries = messages.map((message, index) => {
|
|
4599
|
+
const record = message;
|
|
4600
|
+
const parts = (record.parts ?? []).map((part) => {
|
|
4601
|
+
if (part.type === "text" && typeof part.text === "string") {
|
|
4602
|
+
const text = redaction.redactText(part.text).replace(/\bBearer\s+\S+/gi, "Bearer [REDACTED]").replace(/\b([A-Z][A-Z0-9_]*(?:KEY|TOKEN|SECRET|PASSWORD)|api[_-]?key|access[_-]?token|password)\s*([=:])\s*(?:"[^"]*"|'[^']*'|\S+)/gi, "$1$2[REDACTED]").replace(/\b(?:sk|ghp|gho|github_pat)-[A-Za-z0-9_-]{8,}\b/g, "[REDACTED]").replace(/\s+/g, " ").trim();
|
|
4603
|
+
return text.length > 160 ? `${text.slice(0, 160)}\u2026` : text;
|
|
4604
|
+
}
|
|
4605
|
+
if (part.type === "tool-call") {
|
|
4606
|
+
const call = part.toolCall;
|
|
4607
|
+
return `tool-call:${typeof call?.name === "string" ? call.name : "unknown"}`;
|
|
4608
|
+
}
|
|
4609
|
+
if (part.type === "tool-result") {
|
|
4610
|
+
const result = part.toolResult;
|
|
4611
|
+
return `tool-result:${typeof result?.toolName === "string" ? result.toolName : "unknown"}`;
|
|
4612
|
+
}
|
|
4613
|
+
return typeof part.type === "string" ? part.type : "part";
|
|
3601
4614
|
});
|
|
3602
|
-
|
|
4615
|
+
return `${index + 1}. ${record.role ?? "message"}: ${parts.join(" | ")}`;
|
|
4616
|
+
});
|
|
4617
|
+
const summary = summaries.join(`
|
|
3603
4618
|
`);
|
|
4619
|
+
return summary.length > 4000 ? `${summary.slice(0, 4000)}\u2026` : summary;
|
|
4620
|
+
};
|
|
4621
|
+
var compactHarnessMessages = (messages) => {
|
|
4622
|
+
if (messages.length === 0)
|
|
4623
|
+
return [];
|
|
4624
|
+
return [
|
|
4625
|
+
{
|
|
4626
|
+
role: "user",
|
|
4627
|
+
parts: [{
|
|
4628
|
+
type: "text",
|
|
4629
|
+
text: `[Compacted conversation context]
|
|
4630
|
+
${summarizeHarnessMessages(messages)}`
|
|
4631
|
+
}]
|
|
4632
|
+
}
|
|
4633
|
+
];
|
|
4634
|
+
};
|
|
4635
|
+
var createHarnessCompactor = () => {
|
|
4636
|
+
return async ({ messages }) => {
|
|
3604
4637
|
return {
|
|
3605
|
-
summary:
|
|
4638
|
+
summary: summarizeHarnessMessages(messages),
|
|
3606
4639
|
metadata: {
|
|
3607
4640
|
strategy: "deterministic-redacted-transcript",
|
|
3608
4641
|
sourceMessages: messages.length
|
|
@@ -3665,7 +4698,7 @@ var createProviderCompatibleBudget = (config) => {
|
|
|
3665
4698
|
};
|
|
3666
4699
|
};
|
|
3667
4700
|
var createHarness = async (options = {}) => {
|
|
3668
|
-
const config = resolveHarnessConfig(options);
|
|
4701
|
+
const config = resolveHarnessConfig(options, options.providerRegistry);
|
|
3669
4702
|
const workspace = await Workspace.open(config.workspace);
|
|
3670
4703
|
await validateStateDirectory(config.workspace, config.stateDirectory);
|
|
3671
4704
|
const executionEnvironment = config.execution.backend === "oci" ? await createHarnessOciExecutionEnvironment({
|
|
@@ -3674,7 +4707,7 @@ var createHarness = async (options = {}) => {
|
|
|
3674
4707
|
stateDirectory: config.stateDirectory,
|
|
3675
4708
|
...options.ociRuntimeAdapter ? { runtime: options.ociRuntimeAdapter } : {}
|
|
3676
4709
|
}) : undefined;
|
|
3677
|
-
const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env);
|
|
4710
|
+
const model = options.modelInstance ?? createProviderModel(config, options.env ?? process.env, options.providerRegistry);
|
|
3678
4711
|
const capabilityRequirements = [...new Set([
|
|
3679
4712
|
...config.requiredCapabilities,
|
|
3680
4713
|
...config.orchestration.profiles.length > 0 || config.mcpConfigPath || options.mcpConfiguration ? ["tools", "streaming"] : []
|
|
@@ -3716,7 +4749,7 @@ var createHarness = async (options = {}) => {
|
|
|
3716
4749
|
await options.onTelemetryEvent?.(event);
|
|
3717
4750
|
};
|
|
3718
4751
|
const costGuardrails = createCostGuardrails(config);
|
|
3719
|
-
const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, executionEnvironment);
|
|
4752
|
+
const binding = createHarnessBinding(config, mcpConfiguration, model, options.subagentModels, (options.providerRegistry ?? DEFAULT_PROVIDER_REGISTRY).transportFingerprint(options.env ?? process.env), executionEnvironment);
|
|
3720
4753
|
const subagentRuntime = createHarnessSubagents({
|
|
3721
4754
|
config,
|
|
3722
4755
|
parentBinding: binding,
|
|
@@ -3847,16 +4880,25 @@ export {
|
|
|
3847
4880
|
workspaceFilePathSchema,
|
|
3848
4881
|
validateStateDirectory,
|
|
3849
4882
|
validateEditProposal,
|
|
4883
|
+
streamResultDocument,
|
|
4884
|
+
streamEventDocument,
|
|
4885
|
+
serializeStreamResult,
|
|
4886
|
+
serializeStreamEvent,
|
|
4887
|
+
serializeHarnessModelRoutes,
|
|
3850
4888
|
selectHarnessModel,
|
|
3851
4889
|
runHarnessReviewGroup,
|
|
3852
4890
|
runHarness,
|
|
3853
4891
|
restoreFileInputSchema,
|
|
4892
|
+
resolveHarnessModelRoutes,
|
|
3854
4893
|
resolveHarnessConfig,
|
|
3855
4894
|
quarantineFileInputSchema,
|
|
3856
4895
|
providerDescriptor,
|
|
3857
4896
|
providerAvailability,
|
|
3858
4897
|
parseProvider,
|
|
4898
|
+
parseHarnessModelRoute,
|
|
4899
|
+
openSessionStore,
|
|
3859
4900
|
openHarnessPersistence,
|
|
4901
|
+
openCliSessionStore,
|
|
3860
4902
|
normalizeHarnessMcpConfiguration,
|
|
3861
4903
|
mutationOperationSchema,
|
|
3862
4904
|
mutationAuditEntrySchema,
|
|
@@ -3869,6 +4911,7 @@ export {
|
|
|
3869
4911
|
harnessExecutionSession,
|
|
3870
4912
|
fileDigestSchema,
|
|
3871
4913
|
executionFingerprintInput,
|
|
4914
|
+
estimateMessageTokens,
|
|
3872
4915
|
editProposalSchema,
|
|
3873
4916
|
editProposalInputSchema,
|
|
3874
4917
|
editProposalChangeSchema,
|
|
@@ -3877,14 +4920,17 @@ export {
|
|
|
3877
4920
|
editChangeSchema,
|
|
3878
4921
|
describeOciCommand,
|
|
3879
4922
|
defaultHarnessNamespace,
|
|
4923
|
+
createProviderRegistry,
|
|
3880
4924
|
createProviderModel,
|
|
3881
4925
|
createHttpMcpClient,
|
|
3882
4926
|
createHarnessSubagents,
|
|
4927
|
+
createHarnessRouteModels,
|
|
3883
4928
|
createHarnessOciExecutionEnvironment,
|
|
3884
4929
|
createHarnessMcpTools,
|
|
3885
4930
|
createHarness,
|
|
3886
4931
|
createExecutionEnvironmentTools,
|
|
3887
4932
|
createEditProposal,
|
|
4933
|
+
compactHarnessMessages,
|
|
3888
4934
|
cleanupHarnessRuns,
|
|
3889
4935
|
cleanupHarnessExecutionArtifacts,
|
|
3890
4936
|
cancelHarnessRun,
|
|
@@ -3892,7 +4938,9 @@ export {
|
|
|
3892
4938
|
applyEditProposalInputSchema,
|
|
3893
4939
|
appendUserMessage,
|
|
3894
4940
|
Workspace,
|
|
4941
|
+
TERMINAL_SESSION_RUN_STATUSES,
|
|
3895
4942
|
TERMINAL_RUN_STATUSES,
|
|
4943
|
+
SESSION_RUN_STATUSES,
|
|
3896
4944
|
PROVIDER_DESCRIPTORS,
|
|
3897
4945
|
PROVIDERS,
|
|
3898
4946
|
MAX_EDIT_PROPOSAL_BYTES,
|
|
@@ -3902,6 +4950,8 @@ export {
|
|
|
3902
4950
|
HARNESS_SUBAGENT_PROFILE_DESCRIPTORS,
|
|
3903
4951
|
HARNESS_SUBAGENT_PROFILES,
|
|
3904
4952
|
HARNESS_SQLITE_FILE,
|
|
4953
|
+
HARNESS_SESSION_SCHEMA_VERSION,
|
|
4954
|
+
HARNESS_SESSION_INDEX_FILE,
|
|
3905
4955
|
HARNESS_REQUIRED_CAPABILITIES,
|
|
3906
4956
|
HARNESS_OPERATIONS_SCHEMA_VERSION,
|
|
3907
4957
|
HARNESS_OCI_RUNTIMES,
|
|
@@ -3917,8 +4967,12 @@ export {
|
|
|
3917
4967
|
EDIT_DIGEST_ALGORITHM,
|
|
3918
4968
|
EDIT_CONTRACT_SCHEMA_VERSION,
|
|
3919
4969
|
DEFAULT_SUBAGENT_BUDGET,
|
|
4970
|
+
DEFAULT_PROVIDER_REGISTRY,
|
|
3920
4971
|
DEFAULT_OCI_EXECUTION,
|
|
3921
4972
|
DEFAULT_ALLOWED_CHECKS,
|
|
3922
4973
|
CliOciRuntimeAdapter,
|
|
3923
|
-
|
|
4974
|
+
CLI_JSON_SCHEMA_VERSION,
|
|
4975
|
+
CLI_EVENT_SCHEMA_VERSION,
|
|
4976
|
+
BUN_ENGINE_RANGE,
|
|
4977
|
+
BUILTIN_PROVIDER_REGISTRATIONS
|
|
3924
4978
|
};
|