@odla-ai/cli 0.15.0 → 0.16.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 CHANGED
@@ -29,8 +29,8 @@ For a project you keep working in, install it as a dev dependency so the
29
29
  shorter `npx odla-ai` form works and the version is pinned by your lockfile:
30
30
 
31
31
  ```bash
32
- npm view @odla-ai/cli@0.15.0 version
33
- npm i -D --save-exact @odla-ai/cli@0.15.0
32
+ npm view @odla-ai/cli@0.16.0 version
33
+ npm i -D --save-exact @odla-ai/cli@0.16.0
34
34
  ```
35
35
 
36
36
  ## Prerequisites
package/dist/bin.cjs CHANGED
@@ -2540,7 +2540,7 @@ async function stageWorkspacePair(baselineSource, workspaceSource, options = {})
2540
2540
  }
2541
2541
  }
2542
2542
 
2543
- // ../harness/dist/chunk-QEW6NNAO.js
2543
+ // ../harness/dist/chunk-4EM6NMET.js
2544
2544
  var import_crypto = require("crypto");
2545
2545
  var import_promises5 = require("fs/promises");
2546
2546
  var import_path5 = require("path");
@@ -2877,7 +2877,7 @@ function validateSnapshot(snapshot, limits) {
2877
2877
  }
2878
2878
  }
2879
2879
 
2880
- // ../harness/dist/chunk-QEW6NNAO.js
2880
+ // ../harness/dist/chunk-4EM6NMET.js
2881
2881
  var import_child_process4 = require("child_process");
2882
2882
  var import_promises6 = require("fs/promises");
2883
2883
  var import_path6 = require("path");
@@ -3170,7 +3170,7 @@ function looksLikeDestination(value) {
3170
3170
  return /^(?:[a-z][a-z0-9+.-]*:\/\/|\/|\\\\)/i.test(text) || /^[\w.-]+\.[a-z]{2,}(?:[/:]|$)/i.test(text);
3171
3171
  }
3172
3172
 
3173
- // ../harness/dist/chunk-QEW6NNAO.js
3173
+ // ../harness/dist/chunk-4EM6NMET.js
3174
3174
  var import_crypto4 = require("crypto");
3175
3175
  async function digestStagedWorkspace(root, limits) {
3176
3176
  const files = [];
@@ -3418,10 +3418,28 @@ async function parseSource(value) {
3418
3418
  if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("source file");
3419
3419
  return { path: file.path, content: file.content };
3420
3420
  });
3421
+ const referencesValue = snapshot.references === void 0 ? [] : snapshot.references;
3422
+ if (!Array.isArray(referencesValue) || referencesValue.length > 5) throw invalid("reference sources");
3423
+ const aliases = /* @__PURE__ */ new Set();
3424
+ const references = [];
3425
+ for (const item of referencesValue) {
3426
+ const reference = record3(item);
3427
+ if (!reference || typeof reference.alias !== "string" || !/^[a-z][a-z0-9-]{0,39}$/.test(reference.alias) || aliases.has(reference.alias) || reference.alias === "primary" || typeof reference.repository !== "string" || typeof reference.commitSha !== "string" || typeof reference.treeDigest !== "string" || !Array.isArray(reference.files)) throw invalid("reference source");
3428
+ aliases.add(reference.alias);
3429
+ const referenceFiles = reference.files.map((entry) => {
3430
+ const file = record3(entry);
3431
+ if (!file || typeof file.path !== "string" || typeof file.content !== "string") throw invalid("reference source file");
3432
+ return { path: file.path, content: file.content };
3433
+ });
3434
+ const source2 = { repository: reference.repository, commitSha: reference.commitSha, files: referenceFiles };
3435
+ const referenceDigest = await digestCodeRepositorySnapshot(source2, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
3436
+ if (referenceDigest !== reference.treeDigest) throw invalid("reference source digest");
3437
+ references.push({ alias: reference.alias, ...source2, treeDigest: referenceDigest });
3438
+ }
3421
3439
  const source = { repository: snapshot.repository, commitSha: snapshot.commitSha, files };
3422
3440
  const digest = await digestCodeRepositorySnapshot(source, { maximumFiles: 1e4, maximumBytes: 16 * 1024 * 1024 });
3423
3441
  if (digest !== snapshot.treeDigest) throw invalid("source digest");
3424
- return { ...source, treeDigest: digest };
3442
+ return { ...source, treeDigest: digest, ...references.length ? { references } : {} };
3425
3443
  }
3426
3444
  function parseReview(value) {
3427
3445
  const review = record3(record3(value)?.review);
@@ -3952,7 +3970,7 @@ var CodeRuntimeCheckpointManager = class {
3952
3970
  await this.options.event(command, {
3953
3971
  type: "message",
3954
3972
  actor: "system",
3955
- body: `Candidate ${candidate.candidateId} is ready for exact owner publication approval`
3973
+ body: command.payload.sourceSet ? `Candidate ${candidate.candidateId} was verified and delivered to the session PR branch` : `Candidate ${candidate.candidateId} is ready for legacy owner publication approval`
3956
3974
  }, pending.refs);
3957
3975
  this.#pending.delete(command.commandId);
3958
3976
  return true;
@@ -3979,12 +3997,51 @@ async function materializeCodeRuntimeSource(snapshot, tempRoot = (0, import_os3.
3979
3997
  await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
3980
3998
  await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 420 });
3981
3999
  }
4000
+ for (const reference of snapshot.references ?? []) {
4001
+ validateAlias(reference.alias);
4002
+ if (!reference.files.length || reference.files.length > 1e4) throw new TypeError("Code reference file count is invalid");
4003
+ for (const file of reference.files) {
4004
+ validatePath(file.path);
4005
+ const path = `.odla-references/${reference.alias}/${file.path}`;
4006
+ if (seen.has(path)) throw new TypeError("Code reference repeats a path");
4007
+ seen.add(path);
4008
+ bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
4009
+ if (bytes > 80 * 1024 * 1024) throw new TypeError("Code source set exceeds its byte bound");
4010
+ const target = (0, import_path8.resolve)(sourceDir, path);
4011
+ if (!target.startsWith(`${(0, import_path8.resolve)(sourceDir)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
4012
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
4013
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
4014
+ }
4015
+ }
3982
4016
  return { sourceDir, cleanup: () => (0, import_promises8.rm)(root, { recursive: true, force: true }) };
3983
4017
  } catch (cause) {
3984
4018
  await (0, import_promises8.rm)(root, { recursive: true, force: true });
3985
4019
  throw cause;
3986
4020
  }
3987
4021
  }
4022
+ function validateAlias(alias) {
4023
+ if (!/^[a-z][a-z0-9-]{0,39}$/.test(alias) || alias === "primary") {
4024
+ throw new TypeError("Code reference alias is invalid");
4025
+ }
4026
+ }
4027
+ async function attachCodeRuntimeReferences(workspace, references) {
4028
+ let bytes = 0;
4029
+ for (const reference of references) {
4030
+ validateAlias(reference.alias);
4031
+ for (const file of reference.files) {
4032
+ validatePath(file.path);
4033
+ const path = `.odla-references/${reference.alias}/${file.path}`;
4034
+ bytes += Buffer.byteLength(path) + Buffer.byteLength(file.content);
4035
+ if (bytes > 64 * 1024 * 1024) throw new TypeError("Code reference set exceeds its byte bound");
4036
+ for (const root of [workspace.baselineDir, workspace.workspaceDir]) {
4037
+ const target = (0, import_path8.resolve)(root, path);
4038
+ if (!target.startsWith(`${(0, import_path8.resolve)(root)}${import_path8.sep}`)) throw new TypeError("Code reference path escapes its root");
4039
+ await (0, import_promises8.mkdir)((0, import_path8.dirname)(target), { recursive: true });
4040
+ await (0, import_promises8.writeFile)(target, file.content, { flag: "wx", mode: 292 });
4041
+ }
4042
+ }
4043
+ }
4044
+ }
3988
4045
  function validatePath(path) {
3989
4046
  const parts = path.split("/");
3990
4047
  if (!path || path.startsWith("/") || path.includes("\\") || path.includes("\0") || parts.some((part) => !part || part === "." || part === ".." || RESERVED2.has(part) || SECRET2.test(part))) {
@@ -4183,6 +4240,9 @@ async function patch(context, request, options, policy) {
4183
4240
  exactKeys(request.input, ["patch"]);
4184
4241
  const value = stringField(request.input, "patch");
4185
4242
  const paths = validateCodePatch(value, options.maxPatchBytes ?? 256 * 1024);
4243
+ if (paths.some((path) => options.readOnlyPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)))) {
4244
+ throw new TypeError("patch targets a read-only reference source");
4245
+ }
4186
4246
  const allowed = await policy.patch(policyContext(context, request, options, { patch: value }));
4187
4247
  if (!allowed) return response(request, false, "tool denied by CaMeL policy");
4188
4248
  await applyCodePatch(context.workspaceDir, value, paths);
@@ -4268,6 +4328,9 @@ function validateOptions(options) {
4268
4328
  throw new TypeError("Code tool broker requires a reader and unique registered recipes");
4269
4329
  }
4270
4330
  for (const recipe2 of options.recipes) assertCodeBuildRecipe(recipe2);
4331
+ if (options.readOnlyPrefixes?.some((prefix) => !/^[A-Za-z0-9_.-]+$/.test(prefix) || prefix === "." || prefix === "..")) {
4332
+ throw new TypeError("Code tool broker read-only prefix is invalid");
4333
+ }
4271
4334
  }
4272
4335
  function exactKeys(input, allowed) {
4273
4336
  if (Object.keys(input).some((key) => !allowed.includes(key))) throw new TypeError("tool input contains an unsupported field");
@@ -4375,7 +4438,8 @@ function createCodeRuntimeToolBroker(input, lease, role) {
4375
4438
  recipes: input.recipes,
4376
4439
  recipeExecutor: createContainerRecipeExecutor(input.engine),
4377
4440
  recipeAuthorization: input.recipeAuthorization ?? "registered_recipe",
4378
- readerId: `code-session:${lease.task.taskId}`
4441
+ readerId: `code-session:${lease.task.taskId}`,
4442
+ readOnlyPrefixes: [".odla-references"]
4379
4443
  });
4380
4444
  return role === "coding" ? broker : { execute: (context, request) => request.tool === "sandbox.read" ? broker.execute(context, request) : Promise.resolve({ requestId: request.requestId, ok: false, content: "review sessions are read-only" }) };
4381
4445
  }
@@ -4453,6 +4517,14 @@ var CodePiRuntimeEngine = class {
4453
4517
  resume
4454
4518
  });
4455
4519
  ({ workspace, sourceDigest, trustedBaseDigest: localTrustedBaseDigest } = prepared);
4520
+ if (command.payload.sourceSet) {
4521
+ const selected = await this.options.control.source(command.sessionId);
4522
+ if (selected.repository !== metadata2.repository || selected.commitSha !== metadata2.baseCommitSha || selected.treeDigest !== metadata2.sourceTreeDigest) {
4523
+ await workspace.cleanup();
4524
+ throw new TypeError("Code local source does not match the selected GitHub primary source");
4525
+ }
4526
+ await attachCodeRuntimeReferences(workspace, selected.references ?? []);
4527
+ }
4456
4528
  } else {
4457
4529
  const source = await this.options.control.source(command.sessionId);
4458
4530
  const materialized = await materializeCodeRuntimeSource(source);
@@ -4480,8 +4552,8 @@ var CodePiRuntimeEngine = class {
4480
4552
  repository: metadata2.repository,
4481
4553
  sourceTreeDigest: metadata2.sourceTreeDigest,
4482
4554
  trustedBaseDigest: requestedLocal ? localTrustedBaseDigest : await digestStagedWorkspace(workspace.baselineDir, {
4483
- maxFiles: 1e4,
4484
- maxBytes: 16 * 1024 * 1024
4555
+ maxFiles: 2e4,
4556
+ maxBytes: 512 * 1024 * 1024
4485
4557
  }),
4486
4558
  planningInputDigest: metadata2.planningInputDigest ?? digestRuntimeValue(
4487
4559
  JSON.stringify({ attestation: metadata2.attestationDigest, prompt: metadata2.prompt, tree: sourceDigest })