oh-my-promptfoo 1.11.0 → 1.12.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
@@ -25,6 +25,12 @@ Set the agent credential through `delegate.env` in authored evals. Set
25
25
  require ORAS 1.x through `ALLAGENTS_ORAS_PATH`. A trusted runner must provide
26
26
  `ALLAGENTS_LOCAL_SOURCE_ROOT` before acquiring `type: local` sources.
27
27
 
28
+ Several `type: local` sources may name the same `destination` and merge into one
29
+ tree, so N plugins can stage their skills into a single `.agents/skills`. They
30
+ must agree on `permissions`, and no two of them may contribute the same relative
31
+ path; a collision names both sources and fails. Git and OCI sources keep
32
+ exclusive destinations, and nested destinations remain rejected.
33
+
28
34
  `llmAssert` batches named criteria into one judge request. A component passes
29
35
  when its judge verdict is not false, and `config.threshold` adds a numeric bar
30
36
  on top of that verdict when you state one — the same contract as Promptfoo's
package/dist/cli.js CHANGED
@@ -898,7 +898,7 @@ async function prepareProtectedCopy(source, path2, viewMode = "auto", caseIndex,
898
898
  init_esm_shims();
899
899
  import { isAbsolute as isAbsolute2, posix } from "path";
900
900
  var DEFAULT_LIMITS = {
901
- maxSources: 8,
901
+ maxSources: 64,
902
902
  maxDownloadBytes: 268435456,
903
903
  maxExtractedBytes: 536870912,
904
904
  timeoutMs: 12e4
@@ -2077,6 +2077,11 @@ import { isAbsolute as isAbsolute6, join as join8, relative as relative3, resolv
2077
2077
  import { Transform } from "stream";
2078
2078
  import { pipeline } from "stream/promises";
2079
2079
  var snapshots = /* @__PURE__ */ new WeakMap();
2080
+ function localEntries(source) {
2081
+ const entries = snapshots.get(source);
2082
+ if (!entries) throw new Error("Local source snapshot is not available to this provider");
2083
+ return entries;
2084
+ }
2080
2085
  var ordered = (a, b) => a < b ? -1 : a > b ? 1 : 0;
2081
2086
  var READ_FLAGS = constants5.O_RDONLY | constants5.O_NOFOLLOW | constants5.O_NONBLOCK;
2082
2087
  function sourceRoot(source, channels) {
@@ -2417,9 +2422,41 @@ async function logicalBytes(root) {
2417
2422
  }
2418
2423
  return total;
2419
2424
  }
2425
+ function assertMergeable(destination2, group) {
2426
+ const owners = /* @__PURE__ */ new Map();
2427
+ for (const source of group) {
2428
+ if (source.type !== "local")
2429
+ throw new Error(
2430
+ `Only local sources may share destination ${JSON.stringify(destination2)}; ${source.type} source ${JSON.stringify(source.repository)} cannot merge`
2431
+ );
2432
+ for (const entry of localEntries(source)) {
2433
+ const owner = owners.get(entry.path);
2434
+ if (!owner) {
2435
+ owners.set(entry.path, { path: source.path, entry });
2436
+ continue;
2437
+ }
2438
+ if (owner.entry.size !== void 0 || entry.size !== void 0)
2439
+ throw new Error(
2440
+ `Sources sharing destination ${JSON.stringify(destination2)} both provide ${JSON.stringify(entry.path)}: ${owner.path} and ${source.path}`
2441
+ );
2442
+ if (owner.entry.mode !== entry.mode)
2443
+ throw new Error(
2444
+ `Sources sharing destination ${JSON.stringify(destination2)} disagree on the mode of directory ${JSON.stringify(entry.path)}: ${owner.path} and ${source.path}`
2445
+ );
2446
+ }
2447
+ }
2448
+ }
2420
2449
  async function materializeSources(resolved, staging, limits, channels, signal, caseIndex) {
2421
2450
  signal?.throwIfAborted();
2422
2451
  if (resolved.length > limits.maxSources) throw new Error("Too many resolved sources");
2452
+ const groups = /* @__PURE__ */ new Map();
2453
+ for (const source of resolved) {
2454
+ const group = groups.get(source.destination);
2455
+ if (group) group.push(source);
2456
+ else groups.set(source.destination, [source]);
2457
+ }
2458
+ for (const [destination2, group] of groups)
2459
+ if (group.length > 1) assertMergeable(destination2, group);
2423
2460
  staging = resolve9(staging);
2424
2461
  const writer = await PhysicalWriter.create(
2425
2462
  staging,
@@ -2489,13 +2526,20 @@ function acquisitionIdentity(source) {
2489
2526
  materializerVersion: source.materializerVersion ?? 1
2490
2527
  };
2491
2528
  }
2492
- function protectedCheckoutKey(source, viewMode = "auto") {
2493
- const identity = canonicalJson(acquisitionIdentity(source));
2529
+ function sortedIdentities(sources) {
2530
+ return sources.map((source) => {
2531
+ const identity = acquisitionIdentity(source);
2532
+ return { destination: source.destination, identity, text: canonicalJson(identity) };
2533
+ }).sort(
2534
+ (a, b) => a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : a.text < b.text ? -1 : a.text > b.text ? 1 : 0
2535
+ ).map((entry) => entry.identity);
2536
+ }
2537
+ function protectedCheckoutKey(sources, viewMode = "auto") {
2538
+ const identity = sources.length === 1 ? canonicalJson(acquisitionIdentity(sources[0])) : canonicalJson({ schemaVersion: 1, merged: sortedIdentities(sources) });
2494
2539
  return hash(viewMode === "copy-only" ? `${identity}\0copy-only` : identity);
2495
2540
  }
2496
2541
  function manifestDigest(sources) {
2497
- const identities = [...sources].sort((a, b) => a.destination < b.destination ? -1 : a.destination > b.destination ? 1 : 0).map(acquisitionIdentity);
2498
- return `sha256:${hash(canonicalJson({ schemaVersion: 1, sources: identities }))}`;
2542
+ return `sha256:${hash(canonicalJson({ schemaVersion: 1, sources: sortedIdentities(sources) }))}`;
2499
2543
  }
2500
2544
  var MAX_INVENTORY_JSON_BYTES = 512 * 1024 ** 2;
2501
2545
  var LAST_USED_REFRESH_MS = 60 * 60 * 1e3;
@@ -2794,8 +2838,12 @@ var SeedCache = class {
2794
2838
  }
2795
2839
  });
2796
2840
  }
2797
- async protectedSource(record, source, signal, viewMode = "auto", caseIndex, sourceIndex, sourceCount) {
2798
- const key = protectedCheckoutKey(source, viewMode);
2841
+ /** `sources` is every source contributing to one destination, merged or alone. */
2842
+ async protectedSource(record, sources, signal, viewMode = "auto", caseIndex, sourceIndex, sourceCount) {
2843
+ const destination2 = sources[0]?.destination;
2844
+ if (destination2 === void 0 || sources.some((source) => source.destination !== destination2))
2845
+ throw new Error("Protected checkout requires sources sharing one destination");
2846
+ const key = protectedCheckoutKey(sources, viewMode);
2799
2847
  const path2 = join10(this.root, "checkouts", key, "tree");
2800
2848
  return this.lockAcquisition(
2801
2849
  join10(this.root, "locks", "admission"),
@@ -2818,7 +2866,7 @@ var SeedCache = class {
2818
2866
  );
2819
2867
  }
2820
2868
  } else {
2821
- const seedSource = join10(this.seedPath(record.digest), source.destination);
2869
+ const seedSource = join10(this.seedPath(record.digest), destination2);
2822
2870
  await measuredPhase(
2823
2871
  "checkout-admission",
2824
2872
  async () => this.admit(