@odla-ai/harness 0.10.3 → 0.11.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/README.md +24 -0
- package/dist/{chunk-TMBYA5JX.js → chunk-IXYKIPRA.js} +144 -29
- package/dist/chunk-IXYKIPRA.js.map +1 -0
- package/dist/code-runtime-cli.cjs +124 -18
- package/dist/code-runtime-cli.cjs.map +1 -1
- package/dist/code-runtime-cli.js +1 -1
- package/dist/node.cjs +145 -25
- package/dist/node.cjs.map +1 -1
- package/dist/node.d.cts +82 -1
- package/dist/node.d.ts +82 -1
- package/dist/node.js +15 -1
- package/dist/node.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-TMBYA5JX.js.map +0 -1
package/README.md
CHANGED
|
@@ -109,6 +109,30 @@ before approval:
|
|
|
109
109
|
}
|
|
110
110
|
~~~
|
|
111
111
|
|
|
112
|
+
A repository can replace that release-owned list with its own. Code reads
|
|
113
|
+
`odla.recipes.json` from the root of the trusted base (the default-branch
|
|
114
|
+
snapshot it staged, never the candidate's working tree) when a session starts:
|
|
115
|
+
|
|
116
|
+
~~~json
|
|
117
|
+
{
|
|
118
|
+
"version": 1,
|
|
119
|
+
"recipes": [
|
|
120
|
+
{ "id": "engine-tests", "command": ["node", "--test", "test/"] },
|
|
121
|
+
{ "id": "gates", "command": ["node", "scripts/gates.mjs"], "timeoutMs": 60000 }
|
|
122
|
+
]
|
|
123
|
+
}
|
|
124
|
+
~~~
|
|
125
|
+
|
|
126
|
+
The repository chooses each recipe's `id`, argv `command`, and optional
|
|
127
|
+
`timeoutMs` (default 120000, at most 900000, up to 16 recipes); the host
|
|
128
|
+
supplies the digest-pinned image and the resource limits, and a declaration that
|
|
129
|
+
names an `image` or any other field is refused. A malformed file fails the
|
|
130
|
+
session start with the fault named rather than falling back, and the session
|
|
131
|
+
thread's first system message says which list gates it and where that list came
|
|
132
|
+
from. Every declared recipe runs in the same fresh, networkless container as the
|
|
133
|
+
release list, so a repository whose tests need `node_modules` still cannot run
|
|
134
|
+
them here.
|
|
135
|
+
|
|
112
136
|
The runtime validates HTTPS, never places its credential in a request body,
|
|
113
137
|
never opens a listener, and rejects malformed or cross-host binding responses.
|
|
114
138
|
Theseus reads, patches, and runs only registered build recipes through the typed
|
|
@@ -1148,8 +1148,8 @@ var CodeRuntimeCheckpointManager = class {
|
|
|
1148
1148
|
trustedBaseDigest: active.trustedBaseDigest,
|
|
1149
1149
|
planningInputDigest: active.planningInputDigest,
|
|
1150
1150
|
conversationRefs: active.conversationRefs,
|
|
1151
|
-
fallbackPolicyDigest: this.options.fallbackPolicyDigest,
|
|
1152
|
-
recipes: this.options.recipes,
|
|
1151
|
+
fallbackPolicyDigest: active.buildPolicyDigest ?? this.options.fallbackPolicyDigest,
|
|
1152
|
+
recipes: active.recipes ?? this.options.recipes,
|
|
1153
1153
|
recipeExecutor: this.options.recipeExecutor,
|
|
1154
1154
|
review: (patch2, verification) => this.options.control.review(command.sessionId, { patch: patch2, verification })
|
|
1155
1155
|
});
|
|
@@ -1978,6 +1978,10 @@ import {
|
|
|
1978
1978
|
keepRecentExchanges,
|
|
1979
1979
|
runAgent
|
|
1980
1980
|
} from "@odla-ai/ai";
|
|
1981
|
+
function readOnlyNotice(readOnly, recipes) {
|
|
1982
|
+
if (!readOnly) return "";
|
|
1983
|
+
return recipes ? "\n\nThis session is read-only: inspect and report; repository mutation is intentionally unavailable. The registered proof recipes are available through odla_run_recipe and must be run before any verdict." : "\n\nThis session is read-only. Inspect and report; repository mutation and recipe execution are intentionally unavailable.";
|
|
1984
|
+
}
|
|
1981
1985
|
async function runCodeAgent(options) {
|
|
1982
1986
|
const toolCalls = [];
|
|
1983
1987
|
const surface = options.surface ?? "v1";
|
|
@@ -1987,6 +1991,7 @@ async function runCodeAgent(options) {
|
|
|
1987
1991
|
workspaceDir: options.workspaceDir,
|
|
1988
1992
|
surface,
|
|
1989
1993
|
...options.readOnly === void 0 ? {} : { readOnly: options.readOnly },
|
|
1994
|
+
...options.recipes === void 0 ? {} : { recipes: options.recipes },
|
|
1990
1995
|
...options.recipeIds ? { recipeIds: options.recipeIds } : {},
|
|
1991
1996
|
onToolCall: (call) => {
|
|
1992
1997
|
toolCalls.push(call);
|
|
@@ -1999,7 +2004,7 @@ async function runCodeAgent(options) {
|
|
|
1999
2004
|
{
|
|
2000
2005
|
name: "odla-code",
|
|
2001
2006
|
model: options.model,
|
|
2002
|
-
system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${options.readOnly
|
|
2007
|
+
system: options.system ?? `${SYSTEM_PROMPT_FOR[surface]}${readOnlyNotice(options.readOnly, options.recipes)}`,
|
|
2003
2008
|
skills: [skill, ...options.extraSkills ?? []],
|
|
2004
2009
|
maxSteps: options.maxSteps ?? 24,
|
|
2005
2010
|
maxTokens: options.maxTokens ?? 16384
|
|
@@ -2084,13 +2089,19 @@ function createCodeRuntimeSessionSkillLoader(control) {
|
|
|
2084
2089
|
...tool.acceptsTaint === void 0 ? {} : { acceptsTaint: tool.acceptsTaint },
|
|
2085
2090
|
handler: async (input, context) => {
|
|
2086
2091
|
if (!context.toolCallId) throw new TypeError("collaboration tool call identity is required");
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2092
|
+
try {
|
|
2093
|
+
return await execute2(command.sessionId, {
|
|
2094
|
+
commandId: command.commandId,
|
|
2095
|
+
toolCallId: context.toolCallId,
|
|
2096
|
+
skill: manifest.name,
|
|
2097
|
+
tool: tool.name,
|
|
2098
|
+
input
|
|
2099
|
+
}, context.signal);
|
|
2100
|
+
} catch (cause) {
|
|
2101
|
+
if (context.signal?.aborted) throw cause;
|
|
2102
|
+
const detail = (cause instanceof Error ? cause.message : String(cause)).slice(0, 500);
|
|
2103
|
+
return { content: `Tool "${tool.name}" failed: ${detail}`, isError: true };
|
|
2104
|
+
}
|
|
2094
2105
|
}
|
|
2095
2106
|
}))
|
|
2096
2107
|
}));
|
|
@@ -3145,6 +3156,110 @@ function assertBudget(budget) {
|
|
|
3145
3156
|
}
|
|
3146
3157
|
}
|
|
3147
3158
|
|
|
3159
|
+
// src/code-repository-recipes.ts
|
|
3160
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
3161
|
+
import { join as join6 } from "path";
|
|
3162
|
+
|
|
3163
|
+
// src/code-runtime-events.ts
|
|
3164
|
+
import { createHash as createHash3 } from "crypto";
|
|
3165
|
+
async function appendCodeRuntimeEvent(control, command, event, refs) {
|
|
3166
|
+
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
3167
|
+
refs.push(eventId);
|
|
3168
|
+
const attributed = { ...event, interactionId: command.commandId };
|
|
3169
|
+
const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
|
|
3170
|
+
await control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
3171
|
+
}
|
|
3172
|
+
var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
|
|
3173
|
+
var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
|
|
3174
|
+
|
|
3175
|
+
// src/code-repository-recipes.ts
|
|
3176
|
+
var REPOSITORY_RECIPES_FILE = "odla.recipes.json";
|
|
3177
|
+
var MAX_RECIPES = 16;
|
|
3178
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
3179
|
+
var MAX_TIMEOUT_MS = 15 * 6e4;
|
|
3180
|
+
var ID2 = /^[A-Za-z0-9._:-]{1,120}$/;
|
|
3181
|
+
var RECIPE_FIELDS = /* @__PURE__ */ new Set(["id", "command", "timeoutMs"]);
|
|
3182
|
+
var fault = (detail) => new TypeError(`${REPOSITORY_RECIPES_FILE} is malformed: ${detail}`);
|
|
3183
|
+
function parseRepositoryRecipes(text, envelope) {
|
|
3184
|
+
let parsed;
|
|
3185
|
+
try {
|
|
3186
|
+
parsed = JSON.parse(text);
|
|
3187
|
+
} catch {
|
|
3188
|
+
throw fault("not valid JSON");
|
|
3189
|
+
}
|
|
3190
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw fault("the document must be an object");
|
|
3191
|
+
const document = parsed;
|
|
3192
|
+
if (document.version !== 1) throw fault("version must be 1");
|
|
3193
|
+
const entries = document.recipes;
|
|
3194
|
+
if (!Array.isArray(entries) || entries.length < 1) throw fault("recipes must be a non-empty array");
|
|
3195
|
+
if (entries.length > MAX_RECIPES) throw fault(`at most ${MAX_RECIPES} recipes may be declared`);
|
|
3196
|
+
const recipes = [];
|
|
3197
|
+
const seen = /* @__PURE__ */ new Set();
|
|
3198
|
+
entries.forEach((entry, index) => {
|
|
3199
|
+
const label = `recipe ${index + 1}`;
|
|
3200
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) throw fault(`${label} must be an object`);
|
|
3201
|
+
const row = entry;
|
|
3202
|
+
const unknown = Object.keys(row).filter((key) => !RECIPE_FIELDS.has(key));
|
|
3203
|
+
if (unknown.length) throw fault(`${label} has unsupported field ${unknown[0]}; the host owns image and resource limits`);
|
|
3204
|
+
if (typeof row.id !== "string" || !ID2.test(row.id)) throw fault(`${label} needs an id of 1 to 120 letters, digits, . _ : or -`);
|
|
3205
|
+
if (seen.has(row.id)) throw fault(`${label} repeats id ${row.id}`);
|
|
3206
|
+
seen.add(row.id);
|
|
3207
|
+
if (!Array.isArray(row.command) || row.command.length < 1 || row.command.some((part) => typeof part !== "string" || !part)) {
|
|
3208
|
+
throw fault(`${label} (${row.id}) needs command as a non-empty array of non-empty strings`);
|
|
3209
|
+
}
|
|
3210
|
+
const timeoutMs = row.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
3211
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > MAX_TIMEOUT_MS) {
|
|
3212
|
+
throw fault(`${label} (${row.id}) timeoutMs must be an integer from 1 to ${MAX_TIMEOUT_MS}`);
|
|
3213
|
+
}
|
|
3214
|
+
const recipe2 = {
|
|
3215
|
+
id: row.id,
|
|
3216
|
+
command: [...row.command],
|
|
3217
|
+
timeoutMs,
|
|
3218
|
+
image: envelope.image,
|
|
3219
|
+
maxOutputBytes: envelope.maxOutputBytes,
|
|
3220
|
+
cpus: envelope.cpus,
|
|
3221
|
+
memory: envelope.memory,
|
|
3222
|
+
pids: envelope.pids
|
|
3223
|
+
};
|
|
3224
|
+
try {
|
|
3225
|
+
assertCodeBuildRecipe(recipe2);
|
|
3226
|
+
} catch (cause) {
|
|
3227
|
+
throw fault(`${label} (${row.id}) is not a runnable recipe: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
3228
|
+
}
|
|
3229
|
+
recipes.push(recipe2);
|
|
3230
|
+
});
|
|
3231
|
+
return recipes;
|
|
3232
|
+
}
|
|
3233
|
+
async function readRepositoryRecipes(baselineDir, envelope) {
|
|
3234
|
+
let text;
|
|
3235
|
+
try {
|
|
3236
|
+
text = await readFile6(join6(baselineDir, REPOSITORY_RECIPES_FILE), "utf8");
|
|
3237
|
+
} catch (cause) {
|
|
3238
|
+
if (cause.code === "ENOENT") return null;
|
|
3239
|
+
throw cause;
|
|
3240
|
+
}
|
|
3241
|
+
return parseRepositoryRecipes(text, envelope);
|
|
3242
|
+
}
|
|
3243
|
+
async function resolveCodeRecipes(baselineDir, release, envelope) {
|
|
3244
|
+
const declared = envelope ? await readRepositoryRecipes(baselineDir, envelope) : null;
|
|
3245
|
+
return declared ? { recipes: declared, source: "repository" } : { recipes: release, source: "release" };
|
|
3246
|
+
}
|
|
3247
|
+
function describeCodeRecipes(resolved) {
|
|
3248
|
+
const ids = resolved.recipes.map((recipe2) => recipe2.id).join(", ");
|
|
3249
|
+
return resolved.source === "repository" ? `Verification recipes: declared by the repository in ${REPOSITORY_RECIPES_FILE} \xB7 ${ids}` : `Verification recipes: release-owned (the repository declares none in ${REPOSITORY_RECIPES_FILE}) \xB7 ${ids}`;
|
|
3250
|
+
}
|
|
3251
|
+
async function sessionRecipesFor(workspace, options, resume) {
|
|
3252
|
+
const resolved = await resolveCodeRecipes(workspace.baselineDir, options.recipes, options.repositoryRecipes ?? null).catch(async (cause) => {
|
|
3253
|
+
await workspace.cleanup();
|
|
3254
|
+
throw cause;
|
|
3255
|
+
});
|
|
3256
|
+
return {
|
|
3257
|
+
...resolved,
|
|
3258
|
+
buildPolicyDigest: digestRuntimeValue(JSON.stringify(resolved.recipes)),
|
|
3259
|
+
note: !resume && resolved.source === "repository" ? describeCodeRecipes(resolved) : null
|
|
3260
|
+
};
|
|
3261
|
+
}
|
|
3262
|
+
|
|
3148
3263
|
// src/code-runtime-broker.ts
|
|
3149
3264
|
function createCodeRuntimeToolBroker(input, lease, role) {
|
|
3150
3265
|
const broker = createCodeToolBroker({
|
|
@@ -3154,16 +3269,17 @@ function createCodeRuntimeToolBroker(input, lease, role) {
|
|
|
3154
3269
|
readerId: `code-session:${lease.task.taskId}`,
|
|
3155
3270
|
readOnlyPrefixes: [".odla-references"]
|
|
3156
3271
|
});
|
|
3157
|
-
const
|
|
3272
|
+
const reviewTools = /* @__PURE__ */ new Set([
|
|
3158
3273
|
"sandbox.read",
|
|
3159
3274
|
"sandbox.list",
|
|
3160
3275
|
"sandbox.search",
|
|
3161
3276
|
"sandbox.overview",
|
|
3162
3277
|
"sandbox.where_is",
|
|
3163
3278
|
"sandbox.who_imports",
|
|
3164
|
-
"sandbox.who_touches"
|
|
3279
|
+
"sandbox.who_touches",
|
|
3280
|
+
"sandbox.run_recipe"
|
|
3165
3281
|
]);
|
|
3166
|
-
return role === "coding" ? broker : { execute: (context, request) =>
|
|
3282
|
+
return role === "coding" ? broker : { execute: (context, request) => reviewTools.has(request.tool) ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
|
|
3167
3283
|
}
|
|
3168
3284
|
|
|
3169
3285
|
// src/code-runtime-goal.ts
|
|
@@ -3325,18 +3441,6 @@ async function startGoalPursuit(input) {
|
|
|
3325
3441
|
return { status: run.met ? "completed" : "failed", finalText: "" };
|
|
3326
3442
|
}
|
|
3327
3443
|
|
|
3328
|
-
// src/code-runtime-events.ts
|
|
3329
|
-
import { createHash as createHash3 } from "crypto";
|
|
3330
|
-
async function appendCodeRuntimeEvent(control, command, event, refs) {
|
|
3331
|
-
const eventId = `${command.commandId.slice(0, 45)}:${refs.length + 1}`;
|
|
3332
|
-
refs.push(eventId);
|
|
3333
|
-
const attributed = { ...event, interactionId: command.commandId };
|
|
3334
|
-
const bounded = attributed.type === "message" ? { ...attributed, body: attributed.body.trim().slice(0, 2e4) || `${attributed.actor} event` } : attributed;
|
|
3335
|
-
await control.appendSessionEvent(command.sessionId, eventId, bounded);
|
|
3336
|
-
}
|
|
3337
|
-
var digestRuntimeValue = (value) => `sha256:${createHash3("sha256").update(value).digest("hex")}`;
|
|
3338
|
-
var runtimeErrorMessage = (value) => value instanceof Error ? value.message : String(value);
|
|
3339
|
-
|
|
3340
3444
|
// src/code-runtime-acknowledgement-gate.ts
|
|
3341
3445
|
function codeRuntimeAcknowledgementGate(signal) {
|
|
3342
3446
|
let settle;
|
|
@@ -3458,6 +3562,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3458
3562
|
control: this.options.control,
|
|
3459
3563
|
...this.options.localSource ? { localSource: this.options.localSource } : {}
|
|
3460
3564
|
});
|
|
3565
|
+
const resolvedRecipes = await sessionRecipesFor(workspace, this.options, resume);
|
|
3461
3566
|
const abort = new AbortController(), startGate = codeRuntimeAcknowledgementGate(abort.signal);
|
|
3462
3567
|
const conversationRefs = [];
|
|
3463
3568
|
const active = {
|
|
@@ -3466,6 +3571,8 @@ var TheseusRuntimeEngine = class {
|
|
|
3466
3571
|
conversationRefs,
|
|
3467
3572
|
acknowledged: false,
|
|
3468
3573
|
startGate,
|
|
3574
|
+
recipes: resolvedRecipes.recipes,
|
|
3575
|
+
buildPolicyDigest: resolvedRecipes.buildPolicyDigest,
|
|
3469
3576
|
role: metadata.role,
|
|
3470
3577
|
readOnly: metadata.readOnly,
|
|
3471
3578
|
title: metadata.title,
|
|
@@ -3483,6 +3590,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3483
3590
|
done: Promise.resolve(null)
|
|
3484
3591
|
};
|
|
3485
3592
|
this.#active.set(command.sessionId, active);
|
|
3593
|
+
if (resolvedRecipes.note) await this.#event(command, { type: "message", actor: "system", body: resolvedRecipes.note }, conversationRefs);
|
|
3486
3594
|
if (requestedLocal) {
|
|
3487
3595
|
await this.#event(command, {
|
|
3488
3596
|
type: "message",
|
|
@@ -3518,7 +3626,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3518
3626
|
const active = await this.#takeOver(command, "pursue requires an active Code session");
|
|
3519
3627
|
active.done = startGoalPursuit({
|
|
3520
3628
|
spec,
|
|
3521
|
-
recipes:
|
|
3629
|
+
recipes: active.recipes,
|
|
3522
3630
|
recipeExecutor: this.options.recipeExecutor ?? createContainerRecipeExecutor(this.options.engine),
|
|
3523
3631
|
workspace: active.workspace,
|
|
3524
3632
|
baseCommitSha: active.baseCommitSha,
|
|
@@ -3597,7 +3705,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3597
3705
|
async #runAttempt(command, metadata, active) {
|
|
3598
3706
|
const lease = fakeCodeLease(command, metadata);
|
|
3599
3707
|
const broker = this.#observed(command, active, createCodeRuntimeToolBroker({
|
|
3600
|
-
recipes:
|
|
3708
|
+
recipes: active.recipes,
|
|
3601
3709
|
engine: this.options.engine,
|
|
3602
3710
|
recipeAuthorization: this.options.recipeAuthorization
|
|
3603
3711
|
}, lease, metadata.role));
|
|
@@ -3623,7 +3731,7 @@ var TheseusRuntimeEngine = class {
|
|
|
3623
3731
|
// A review-role session is read-only and still runs the proof; a planner
|
|
3624
3732
|
// marked read-only by its payload runs nothing.
|
|
3625
3733
|
recipes: metadata.role === "review" || !metadata.readOnly,
|
|
3626
|
-
recipeIds:
|
|
3734
|
+
recipeIds: active.recipes.map((recipe2) => recipe2.id),
|
|
3627
3735
|
...extraSkills.length ? { extraSkills } : {}
|
|
3628
3736
|
});
|
|
3629
3737
|
const closing = result.finalText.trim();
|
|
@@ -3720,6 +3828,7 @@ export {
|
|
|
3720
3828
|
V3_SYSTEM_PROMPT,
|
|
3721
3829
|
SYSTEM_PROMPT_FOR,
|
|
3722
3830
|
codeSkill,
|
|
3831
|
+
readOnlyNotice,
|
|
3723
3832
|
runCodeAgent,
|
|
3724
3833
|
runCodeAgentAttempt,
|
|
3725
3834
|
createCodeRuntimeSessionSkillLoader,
|
|
@@ -3733,6 +3842,12 @@ export {
|
|
|
3733
3842
|
renderMemories,
|
|
3734
3843
|
hazardFromAttempt,
|
|
3735
3844
|
runGoal,
|
|
3845
|
+
REPOSITORY_RECIPES_FILE,
|
|
3846
|
+
parseRepositoryRecipes,
|
|
3847
|
+
readRepositoryRecipes,
|
|
3848
|
+
resolveCodeRecipes,
|
|
3849
|
+
describeCodeRecipes,
|
|
3850
|
+
sessionRecipesFor,
|
|
3736
3851
|
TheseusRuntimeEngine
|
|
3737
3852
|
};
|
|
3738
|
-
//# sourceMappingURL=chunk-
|
|
3853
|
+
//# sourceMappingURL=chunk-IXYKIPRA.js.map
|