@workos/quickstudy 0.11.0 → 0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@workos/quickstudy",
3
- "version": "0.11.0",
3
+ "version": "0.12.0",
4
4
  "description": "An open-source, company-agnostic eval harness for measuring whether coding agents can complete real integration tasks, across an agent x feature x framework x surface matrix.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -9,7 +9,7 @@ export const EMBEDDED_BUILD_INFO: {
9
9
  // (the `x-release-please-version` marker anchors the replacement). It is
10
10
  // restored as-is after a binary build (scripts/build-binary.ts), so the
11
11
  // committed source `--version` stays in lockstep with package.json.
12
- version: "0.11.0", // x-release-please-version
12
+ version: "0.12.0", // x-release-please-version
13
13
  revision: null,
14
14
  dirty: null,
15
15
  sourceHash: null,
@@ -8,10 +8,12 @@
8
8
  /**
9
9
  * Closed set of eval suites. `benchmark` and `regression` are the headline
10
10
  * suites: the leaderboard aggregates count both. `smoke` holds evals every
11
- * arm solves — run to catch breakage, never moving the headline. `other` is
12
- * anything else outside the headline.
11
+ * arm solves — run to catch breakage, never moving the headline. `discovery`
12
+ * holds vendor-neutral tasks whose outcome is categorical (which product was
13
+ * chosen) rather than pass/fail; recorded per attempt, never headline.
14
+ * `other` is anything else outside the headline.
13
15
  */
14
- export const EVAL_SUITES = ["benchmark", "regression", "smoke", "other"] as const;
16
+ export const EVAL_SUITES = ["benchmark", "regression", "smoke", "other", "discovery"] as const;
15
17
  export type EvalSuite = (typeof EVAL_SUITES)[number];
16
18
 
17
19
  /** The suites whose attempts count toward the leaderboard aggregates. */
@@ -53,6 +53,13 @@ export function resolvePairEnvironments(options: {
53
53
  images?: Record<string, RuntimeImageIdentity>;
54
54
  egressProxy?: boolean;
55
55
  adapterHosts?: Record<string, readonly string[]>;
56
+ /**
57
+ * Per experiment: the hosts of the agent's endpoint overrides. They join
58
+ * the allowlist only for pairs the proxy fronts, where the list is live and
59
+ * an IP-literal gateway is refused loudly by design; an open or unproxied
60
+ * pair never carries them, so such a gateway cannot stop the run.
61
+ */
62
+ adapterEndpointHosts?: Record<string, readonly string[]>;
56
63
  /** Per experiment: the agent's non-secret endpoint overrides (see PairEnvironment.endpoints). */
57
64
  adapterEndpoints?: Record<string, Record<string, string>>;
58
65
  budgets?: Partial<LifecycleBudgets>;
@@ -73,16 +80,23 @@ export function resolvePairEnvironments(options: {
73
80
  options.imageTags?.[pairKey(loaded.metadata.id, experiment.id)] ??
74
81
  runtime.image ??
75
82
  runtime.containerImage?.(loaded.metadata);
83
+ // With the flag, every container is proxied unless its runtime opts
84
+ // out with egress: "open". No runtime hosts means only the adapter's
85
+ // provider endpoints are permitted. An open pair still records the
86
+ // runtime and adapter hosts (inert) so identity holds if the run flag
87
+ // flips, but endpoint-override hosts join only a live allowlist: the
88
+ // proxy refuses IP literals, and an IP-literal gateway on an arm the
89
+ // proxy never fronts must not stop the run.
90
+ const proxied = runtime.kind === "container" && (options.egressProxy ?? false) && runtime.egress !== "open";
76
91
  pairs[pairKey(loaded.metadata.id, experiment.id)] = {
77
92
  ...(files ? { workspaceFiles: workspaceFilesIdentity(files) } : {}),
78
93
  image: tag ? (options.images?.[tag] ?? { tag, id: "unresolved", repoDigests: [] }) : null,
79
- // With the flag, every container is proxied. No runtime hosts means
80
- // only the adapter's provider endpoints are permitted.
81
94
  egress: {
82
- proxy: runtime.kind === "container" && (options.egressProxy ?? false),
95
+ proxy: proxied,
83
96
  allowlist: composeAllowlistFromHosts([
84
97
  ...(runtime.egressHosts?.(loaded.metadata) ?? []),
85
98
  ...(options.adapterHosts?.[experiment.id] ?? []),
99
+ ...(proxied ? (options.adapterEndpointHosts?.[experiment.id] ?? []) : []),
86
100
  ]),
87
101
  },
88
102
  webPolicy: runtime.webPolicy?.() ?? "native-web-blocked",
@@ -55,6 +55,15 @@ export interface Runtime extends RuntimeCapabilities {
55
55
  * declare the same fields. Source/config identity still changes per attempt.
56
56
  */
57
57
  treatmentPolicyFields?: Array<"egress" | "webPolicy" | "pathTreatment" | "mcp" | "promptPreamble" | "workspaceFiles">;
58
+ /**
59
+ * Whether this experiment's attempts go through the run's egress proxy.
60
+ * "open" skips the proxy sidecar and network for these attempts even when
61
+ * the run was started with --egress-proxy, so an arm that must reach the
62
+ * open internet can share a run with proxied arms. The choice is recorded
63
+ * in each pair environment (identity); exempt it in a comparison group with
64
+ * treatmentPolicyFields: ["egress"]. Container runtimes only.
65
+ */
66
+ egress?: "proxied" | "open";
58
67
  /** Attempt-local non-secret files, staged without replacing the normal adapter. */
59
68
  workspaceFiles?: WorkspaceFiles;
60
69
  /** Container image tag (container runtimes; alternative to `containerImage`). */
@@ -127,6 +136,11 @@ export function defineExperiment(experiment: Experiment): Experiment {
127
136
  if (experiment.comparisonGroup !== undefined && experiment.comparisonGroup.trim() === "") {
128
137
  throw new Error(`defineExperiment: experiment "${experiment.id}" comparisonGroup must not be empty`);
129
138
  }
139
+ if (kind === "host" && experiment.runtime.egress === "open") {
140
+ throw new Error(
141
+ `defineExperiment: experiment "${experiment.id}" runtime.egress "open" is only meaningful on container runtimes`,
142
+ );
143
+ }
130
144
  validateWorkspaceFilesRuntime(experiment.runtime);
131
145
  return deepFreeze(experiment);
132
146
  }
@@ -3,8 +3,8 @@
3
3
  * the same attempts and (banded) pairs as the rest of the report.
4
4
  *
5
5
  * Every number is restricted to headline-suite attempts (HEADLINE_SUITES:
6
- * benchmark and regression), so smoke and other suites never move the
7
- * headline row. Telemetry follows the report's rule: sums and means cover
6
+ * benchmark and regression), so the smoke, discovery and other suites never
7
+ * move the headline row. Telemetry follows the report's rule: sums and means cover
8
8
  * only attempts that reported a value, and are null (never 0) when none did.
9
9
  */
10
10
  import { isHeadlineSuite } from "../evals/types.ts";
@@ -471,20 +471,19 @@ export async function executeRunV3(options: ExecuteRunV3Options): Promise<RunSum
471
471
  if (scorer.identity) scorerIdentities[loaded.metadata.id] = scorer.identity;
472
472
  }
473
473
  // Each agent's endpoint overrides set on the host: the non-secret base
474
- // URLs join the pair environment (identity), their hosts join the egress
475
- // allowlist — but only under the proxy, where the allowlist is live and an
476
- // IP-literal gateway is refused loudly by design; without it the list is
477
- // inert and an IP-literal override must not stop the run.
474
+ // URLs join the pair environment (identity); their hosts reach the egress
475
+ // allowlist only for pairs the proxy fronts (pair resolution decides: the
476
+ // run flag on and the runtime not opted out), where the list is live and
477
+ // an IP-literal gateway is refused loudly by design. Elsewhere the list
478
+ // is inert and an IP-literal override must not stop the run.
478
479
  const overridesByExperiment = new Map([...containerAgents].map(([id, adapter]) => [id, collectOptionalHostEnv([adapter])] as const));
479
480
  const adapterEndpoints = Object.fromEntries([...overridesByExperiment].flatMap(([id, overrides]) => {
480
481
  const endpoints = Object.fromEntries(Object.entries(overrides).filter(([name]) => !isSecretEnvName(name)));
481
482
  return Object.keys(endpoints).length > 0 ? [[id, endpoints] as const] : [];
482
483
  }));
483
484
  const pairs = resolvePairEnvironments({ ...options, workspaceFilesByExperiment, images, imageTags: imageTagsByPair,
484
- adapterHosts: Object.fromEntries([...containerAgents].map(([id, adapter]) => [id, [
485
- ...(adapter.egressHosts ?? []),
486
- ...(options.egressProxy ? endpointHosts(overridesByExperiment.get(id) ?? {}) : []),
487
- ]])),
485
+ adapterHosts: Object.fromEntries([...containerAgents].map(([id, adapter]) => [id, [...(adapter.egressHosts ?? [])]])),
486
+ adapterEndpointHosts: Object.fromEntries([...overridesByExperiment].map(([id, overrides]) => [id, endpointHosts(overrides)])),
488
487
  adapterEndpoints,
489
488
  budgets: { ...options.budgets, agentMs: options.attemptTimeoutMs ?? options.budgets?.agentMs ?? attemptTimeoutMs },
490
489
  mcpHashes: Object.fromEntries([...declaredByExperiment].map(([id, servers]) => [id, hashString(canonicalJson(servers))])),