@replayci/replay 0.1.14 → 0.1.16

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/dist/index.cjs CHANGED
@@ -1188,6 +1188,14 @@ function optionalString(value, path) {
1188
1188
  }
1189
1189
  return value;
1190
1190
  }
1191
+ function extractReplayTrace(capture) {
1192
+ const replay2 = capture.replay;
1193
+ if (!isRecord(replay2)) return void 0;
1194
+ const trace = replay2.trace;
1195
+ if (!isRecord(trace)) return void 0;
1196
+ if (!Array.isArray(trace.entries)) return void 0;
1197
+ return trace;
1198
+ }
1191
1199
  function parseCommonCapture(capture, index, modelId, schemaVersion) {
1192
1200
  const toolNames = requireStringArray(capture.tool_names, `captures[${index}].tool_names`);
1193
1201
  const primaryToolName = capture.primary_tool_name === void 0 ? toolNames[0] ?? null : nullableString(capture.primary_tool_name, `captures[${index}].primary_tool_name`);
@@ -1205,7 +1213,8 @@ function parseCommonCapture(capture, index, modelId, schemaVersion) {
1205
1213
  ...capture.validation !== void 0 ? { validation: validateValidation(capture.validation, `captures[${index}].validation`) } : {},
1206
1214
  ...capture.usage !== void 0 ? { usage: validateUsage(capture.usage, `captures[${index}].usage`) } : {},
1207
1215
  latency_ms: requireNonNegativeInt(capture.latency_ms, `captures[${index}].latency_ms`),
1208
- ...sdkSessionId !== void 0 ? { sdk_session_id: sdkSessionId } : {}
1216
+ ...sdkSessionId !== void 0 ? { sdk_session_id: sdkSessionId } : {},
1217
+ ...extractReplayTrace(capture) !== void 0 ? { replay_trace: extractReplayTrace(capture) } : {}
1209
1218
  };
1210
1219
  }
1211
1220
  function parseLegacyCapturedCall(capture, index) {
@@ -2320,7 +2329,6 @@ function normalizeInlineContract(input) {
2320
2329
  if (!tool) {
2321
2330
  throw new ReplayConfigurationError("Inline contract is missing required field: tool");
2322
2331
  }
2323
- const assertions = toRecord5(source.assertions);
2324
2332
  const expectTools = toStringArray(source.expect_tools);
2325
2333
  const expectedToolCalls = toExpectedToolCalls(source.expected_tool_calls);
2326
2334
  const contract = {
@@ -2328,28 +2336,38 @@ function normalizeInlineContract(input) {
2328
2336
  ...toString5(source.tool_schema_hash) ? { tool_schema_hash: toString5(source.tool_schema_hash) } : {},
2329
2337
  ...isSideEffect(source.side_effect) ? { side_effect: source.side_effect } : {},
2330
2338
  ...toString5(source.contract_file) ? { contract_file: toString5(source.contract_file) } : {},
2331
- timeouts: {
2332
- total_ms: toNonNegativeNumber(toRecord5(source.timeouts).total_ms, 0)
2333
- },
2334
- retries: {
2335
- max_attempts: Math.max(1, toNonNegativeNumber(toRecord5(source.retries).max_attempts, 1)),
2336
- retry_on: toStringArray(toRecord5(source.retries).retry_on)
2337
- },
2338
- rate_limits: {
2339
- on_429: {
2340
- respect_retry_after: toBoolean(toRecord5(toRecord5(source.rate_limits).on_429).respect_retry_after, false),
2341
- max_sleep_seconds: toNonNegativeNumber(
2342
- toRecord5(toRecord5(source.rate_limits).on_429).max_sleep_seconds,
2343
- 0
2344
- )
2339
+ ...source.timeouts != null ? {
2340
+ timeouts: { total_ms: toNonNegativeNumber(toRecord5(source.timeouts).total_ms, 0) }
2341
+ } : {},
2342
+ ...source.retries != null ? {
2343
+ retries: {
2344
+ max_attempts: Math.max(1, toNonNegativeNumber(toRecord5(source.retries).max_attempts, 1)),
2345
+ retry_on: toStringArray(toRecord5(source.retries).retry_on)
2345
2346
  }
2346
- },
2347
- assertions: {
2348
- input_invariants: toInvariantArray(assertions.input_invariants),
2349
- output_invariants: toInvariantArray(assertions.output_invariants)
2350
- },
2351
- golden_cases: Array.isArray(source.golden_cases) ? source.golden_cases : [],
2352
- allowed_errors: toStringArray(source.allowed_errors),
2347
+ } : {},
2348
+ ...source.rate_limits != null ? {
2349
+ rate_limits: {
2350
+ on_429: {
2351
+ respect_retry_after: toBoolean(toRecord5(toRecord5(source.rate_limits).on_429).respect_retry_after, false),
2352
+ max_sleep_seconds: toNonNegativeNumber(
2353
+ toRecord5(toRecord5(source.rate_limits).on_429).max_sleep_seconds,
2354
+ 0
2355
+ )
2356
+ }
2357
+ }
2358
+ } : {},
2359
+ ...source.assertions != null ? {
2360
+ assertions: {
2361
+ input_invariants: toInvariantArray(toRecord5(source.assertions).input_invariants),
2362
+ output_invariants: toInvariantArray(toRecord5(source.assertions).output_invariants)
2363
+ }
2364
+ } : {},
2365
+ ...source.golden_cases != null ? {
2366
+ golden_cases: Array.isArray(source.golden_cases) ? source.golden_cases : []
2367
+ } : {},
2368
+ ...source.allowed_errors != null ? {
2369
+ allowed_errors: toStringArray(source.allowed_errors)
2370
+ } : {},
2353
2371
  ...expectTools.length > 0 ? { expect_tools: expectTools } : {},
2354
2372
  ...toToolOrder(source.tool_order, expectTools.length > 0) ? {
2355
2373
  tool_order: toToolOrder(source.tool_order, expectTools.length > 0)
@@ -2375,8 +2393,9 @@ function normalizeInlineContract(input) {
2375
2393
  ...Array.isArray(source.schema_derived_exclude) ? { schema_derived_exclude: source.schema_derived_exclude } : {},
2376
2394
  ...Array.isArray(source.binds) ? { binds: source.binds } : {}
2377
2395
  };
2378
- validateSafeRegexes(contract);
2379
- return contract;
2396
+ const filled = { ...(0, import_contracts_core2.fillContractDefaults)(contract), ...contract.contract_file ? { contract_file: contract.contract_file } : {} };
2397
+ validateSafeRegexes(filled);
2398
+ return filled;
2380
2399
  }
2381
2400
  function validateContractSet(contracts) {
2382
2401
  const seenKeys = /* @__PURE__ */ new Set();
@@ -2395,11 +2414,11 @@ function validateSafeRegexes(contract) {
2395
2414
  const invariantGroups = [
2396
2415
  {
2397
2416
  label: "assertions.input_invariants",
2398
- invariants: contract.assertions.input_invariants
2417
+ invariants: contract.assertions?.input_invariants ?? []
2399
2418
  },
2400
2419
  {
2401
2420
  label: "assertions.output_invariants",
2402
- invariants: contract.assertions.output_invariants
2421
+ invariants: contract.assertions?.output_invariants ?? []
2403
2422
  }
2404
2423
  ];
2405
2424
  for (const [index, expectedToolCall] of (contract.expected_tool_calls ?? []).entries()) {
@@ -3002,7 +3021,7 @@ function evaluateExpectTools(contract, toolCalls) {
3002
3021
  function evaluateOutputInvariants(contract, normalizedResponse) {
3003
3022
  const invariantFailures = (0, import_contracts_core3.evaluateInvariants)(
3004
3023
  normalizedResponse,
3005
- contract.assertions.output_invariants,
3024
+ contract.assertions?.output_invariants ?? [],
3006
3025
  process.env
3007
3026
  );
3008
3027
  return invariantFailures.map(
@@ -3052,7 +3071,7 @@ function evaluateArgumentInvariants(contract, toolCalls) {
3052
3071
  return failures;
3053
3072
  }
3054
3073
  function mapInvariantFailure(contract, failure, normalizedResponse) {
3055
- const invariant = findMatchingInvariant(contract.assertions.output_invariants, failure);
3074
+ const invariant = findMatchingInvariant(contract.assertions?.output_invariants ?? [], failure);
3056
3075
  const lookup = (0, import_contracts_core3.getPathValue)(normalizedResponse, failure.path);
3057
3076
  return {
3058
3077
  path: failure.path,
@@ -4634,7 +4653,7 @@ function validateToolResultMessages(messages, contracts, provider) {
4634
4653
  for (const result of toolResults) {
4635
4654
  const contract = contractByTool.get(result.toolName);
4636
4655
  if (!contract) continue;
4637
- const outputInvariants = contract.assertions.output_invariants;
4656
+ const outputInvariants = contract.assertions?.output_invariants ?? [];
4638
4657
  if (outputInvariants.length === 0) continue;
4639
4658
  let parsed;
4640
4659
  try {
@@ -5284,6 +5303,32 @@ var RuntimeClient = class {
5284
5303
  stateVersion: h.state_version
5285
5304
  };
5286
5305
  }
5306
+ /**
5307
+ * Fetch governance plan for an agent.
5308
+ * Returns null on 404 (no plan exists).
5309
+ * @see zero-config-governance.md § GET /api/v1/governance/plan
5310
+ */
5311
+ async fetchGovernancePlan(agent, environment) {
5312
+ const env = environment ?? "development";
5313
+ try {
5314
+ const data = await this.get(
5315
+ `/api/v1/governance/plan?agent=${encodeURIComponent(agent)}&environment=${encodeURIComponent(env)}`
5316
+ );
5317
+ return {
5318
+ status: data.status,
5319
+ compiledSession: data.compiled_session,
5320
+ compiledHash: data.compiled_hash,
5321
+ observations: data.observations,
5322
+ confidence: data.confidence,
5323
+ version: data.version
5324
+ };
5325
+ } catch (err) {
5326
+ if (err instanceof RuntimeClientError && err.httpStatus === 404) {
5327
+ return null;
5328
+ }
5329
+ throw err;
5330
+ }
5331
+ }
5287
5332
  getHealth() {
5288
5333
  return {
5289
5334
  circuitOpen: this.now() < this.circuitOpenUntil,
@@ -5450,9 +5495,14 @@ function replay(client, opts = {}) {
5450
5495
  return createInactiveSession(client, sessionId, "Client already has an active observe() or replay() attachment");
5451
5496
  }
5452
5497
  let contracts;
5498
+ let zeroConfigMode = false;
5453
5499
  try {
5454
5500
  contracts = resolveContracts(opts);
5455
5501
  } catch (err) {
5502
+ const apiKeyForGov = resolveApiKey2(opts);
5503
+ if (apiKeyForGov && !opts.contracts && !opts.contractsDir) {
5504
+ return createGovernanceSession(client, sessionId, agent, provider, apiKeyForGov, opts, diagnostics);
5505
+ }
5456
5506
  const detail = err instanceof Error ? err.message : "Failed to load contracts";
5457
5507
  emitDiagnostic2(diagnostics, { type: "replay_compile_error", details: detail });
5458
5508
  return createBlockingInactiveSession(client, sessionId, detail);
@@ -5660,6 +5710,7 @@ function replay(client, opts = {}) {
5660
5710
  let manualFilter = null;
5661
5711
  const deferredReceipts = /* @__PURE__ */ new Map();
5662
5712
  let deferredPhase = null;
5713
+ const hasWrappedTools = opts.tools != null && Object.keys(opts.tools).length > 0;
5663
5714
  const contractLimits = resolveSessionLimits(contracts);
5664
5715
  const compiledLimits = compiledSession?.sessionLimits;
5665
5716
  const mergedLimits = { ...contractLimits ?? {}, ...compiledLimits ?? {} };
@@ -6619,9 +6670,29 @@ function replay(client, opts = {}) {
6619
6670
  }
6620
6671
  }
6621
6672
  }
6673
+ const compatHasPhaseTransition = !!(phaseResult?.legal && phaseResult.newPhase !== sessionState.currentPhase);
6674
+ const compatShouldDefer = hasWrappedTools && compatHasPhaseTransition;
6622
6675
  const prevVersion = sessionState.stateVersion;
6623
- sessionState = finalizeExecutedStep(sessionState, completedStep, contracts, compiledSession);
6676
+ sessionState = finalizeExecutedStep(
6677
+ sessionState,
6678
+ completedStep,
6679
+ contracts,
6680
+ compiledSession,
6681
+ compatShouldDefer ? { deferPhase: true } : void 0
6682
+ );
6624
6683
  syncStateToStore(prevVersion, sessionState);
6684
+ if (compatShouldDefer && compiledSession && phaseResult) {
6685
+ const advancingTools = /* @__PURE__ */ new Set();
6686
+ for (const tc of toolCalls) {
6687
+ const contract = compiledSession.perToolContracts.get(tc.name);
6688
+ if (contract?.transitions?.advances_to === phaseResult.newPhase) {
6689
+ advancingTools.add(tc.name);
6690
+ }
6691
+ }
6692
+ if (advancingTools.size > 0 && phaseResult.newPhase != null) {
6693
+ deferredPhase = { newPhase: phaseResult.newPhase, toolNames: advancingTools };
6694
+ }
6695
+ }
6625
6696
  }
6626
6697
  if (advisoryDecision.action === "block") {
6627
6698
  sessionState = recordDecisionOutcome(sessionState, "blocked");
@@ -6712,7 +6783,7 @@ function replay(client, opts = {}) {
6712
6783
  }
6713
6784
  }
6714
6785
  const hasPhaseTransition = phaseResult?.legal && phaseResult.newPhase !== sessionState.currentPhase;
6715
- const shouldDeferPhase = isActiveGovern && !attemptDegraded && hasPhaseTransition;
6786
+ const shouldDeferPhase = hasWrappedTools && !!hasPhaseTransition;
6716
6787
  const prevVersionAllow = sessionState.stateVersion;
6717
6788
  sessionState = finalizeExecutedStep(
6718
6789
  sessionState,
@@ -7355,7 +7426,7 @@ function validateResponse2(response, toolCalls, contracts, requestToolNames, unm
7355
7426
  }
7356
7427
  }
7357
7428
  for (const contract of matched) {
7358
- const outputInvariants = contract.assertions.output_invariants;
7429
+ const outputInvariants = contract.assertions?.output_invariants ?? [];
7359
7430
  if (outputInvariants.length > 0) {
7360
7431
  const normalizedResponse = buildNormalizedResponse(response, toolCalls);
7361
7432
  const result = (0, import_contracts_core7.evaluateInvariants)(normalizedResponse, outputInvariants, process.env);
@@ -7523,8 +7594,9 @@ function evaluateInputInvariants(request, contracts) {
7523
7594
  const requestToolSet = new Set(requestToolNames);
7524
7595
  for (const contract of contracts) {
7525
7596
  if (!requestToolSet.has(contract.tool)) continue;
7526
- if (contract.assertions.input_invariants.length === 0) continue;
7527
- const result = (0, import_contracts_core7.evaluateInvariants)(request, contract.assertions.input_invariants, process.env);
7597
+ const inputInvariants = contract.assertions?.input_invariants ?? [];
7598
+ if (inputInvariants.length === 0) continue;
7599
+ const result = (0, import_contracts_core7.evaluateInvariants)(request, inputInvariants, process.env);
7528
7600
  for (const failure of result) {
7529
7601
  failures.push({
7530
7602
  path: failure.path,
@@ -7899,6 +7971,126 @@ function createBlockingInactiveSession(client, sessionId, detail, configError) {
7899
7971
  handoff: () => Promise.resolve(null)
7900
7972
  };
7901
7973
  }
7974
+ function resolveGovernanceEnvironment(opts) {
7975
+ if (opts.environment) return opts.environment;
7976
+ const envVar = typeof process !== "undefined" ? process.env.REPLAYCI_ENVIRONMENT : void 0;
7977
+ if (envVar === "staging") return "staging";
7978
+ if (envVar === "production") return "production";
7979
+ if (envVar === "development") return "development";
7980
+ const nodeEnv = typeof process !== "undefined" ? process.env.NODE_ENV : void 0;
7981
+ if (nodeEnv === "production") return "production";
7982
+ return "development";
7983
+ }
7984
+ function governanceProtectionLevel(env) {
7985
+ switch (env) {
7986
+ case "production":
7987
+ return "govern";
7988
+ case "staging":
7989
+ return "protect";
7990
+ default:
7991
+ return "monitor";
7992
+ }
7993
+ }
7994
+ function createGovernanceSession(client, sessionId, agent, provider, apiKey, opts, diagnostics) {
7995
+ const environment = resolveGovernanceEnvironment(opts);
7996
+ const protLevel = governanceProtectionLevel(environment);
7997
+ const runtimeClient = new RuntimeClient({
7998
+ apiKey,
7999
+ apiUrl: opts.runtimeUrl
8000
+ });
8001
+ let governancePlan;
8002
+ let planFetchPromise = null;
8003
+ let planFetchDone = false;
8004
+ let planFetchError = null;
8005
+ planFetchPromise = runtimeClient.fetchGovernancePlan(agent, environment).then((result) => {
8006
+ governancePlan = result;
8007
+ planFetchDone = true;
8008
+ }).catch((err) => {
8009
+ planFetchDone = true;
8010
+ planFetchError = err instanceof Error ? err.message : String(err);
8011
+ governancePlan = null;
8012
+ });
8013
+ const captureBuffer = new CaptureBuffer({
8014
+ apiKey,
8015
+ endpoint: opts.runtimeUrl
8016
+ });
8017
+ registerBeforeExit(captureBuffer);
8018
+ const terminalInfo = resolveTerminal(client, provider);
8019
+ if (!terminalInfo) {
8020
+ emitDiagnostic2(diagnostics, { type: "replay_inactive", reason: "unsupported_client" });
8021
+ return createInactiveSession(client, sessionId, "Could not resolve terminal resource");
8022
+ }
8023
+ const { terminal, originalCreate } = terminalInfo;
8024
+ const patchedCreate = async function(...args) {
8025
+ if (!planFetchDone && planFetchPromise) {
8026
+ await planFetchPromise;
8027
+ }
8028
+ const hasApprovedPlan = governancePlan && (governancePlan.status === "approved" || governancePlan.status === "enforcing") && governancePlan.compiledSession;
8029
+ if (hasApprovedPlan) {
8030
+ }
8031
+ const result = await originalCreate.apply(this, args);
8032
+ try {
8033
+ const toolCalls = extractToolCalls(result, provider);
8034
+ const usage = extractUsage(result, provider);
8035
+ const requestArg = args[0] && typeof args[0] === "object" ? args[0] : {};
8036
+ captureBuffer.push({
8037
+ schema_version: CAPTURE_SCHEMA_VERSION_CURRENT,
8038
+ agent,
8039
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8040
+ provider,
8041
+ model_id: requestArg.model ?? "unknown",
8042
+ primary_tool_name: toolCalls[0]?.name ?? null,
8043
+ tool_names: toolCalls.map((tc) => tc.name),
8044
+ request: requestArg,
8045
+ response: result,
8046
+ usage,
8047
+ latency_ms: 0,
8048
+ sdk_session_id: sessionId
8049
+ });
8050
+ } catch {
8051
+ }
8052
+ return result;
8053
+ };
8054
+ terminal[terminalInfo.methodName] = patchedCreate;
8055
+ setReplayAttached(client);
8056
+ return {
8057
+ client,
8058
+ flush: () => captureBuffer.flush(),
8059
+ restore() {
8060
+ terminal[terminalInfo.methodName] = originalCreate;
8061
+ },
8062
+ kill() {
8063
+ },
8064
+ getHealth: () => ({
8065
+ status: "healthy",
8066
+ authorityState: "active",
8067
+ protectionLevel: protLevel,
8068
+ durability: "inactive",
8069
+ tier: "compat",
8070
+ compatEnforcement: "protective",
8071
+ cluster_detected: false,
8072
+ bypass_detected: false,
8073
+ totalSteps: 0,
8074
+ totalBlocks: 0,
8075
+ totalErrors: 0,
8076
+ killed: false,
8077
+ shadowEvaluations: 0
8078
+ }),
8079
+ getState: () => EMPTY_STATE_SNAPSHOT,
8080
+ getLastNarrowing: () => null,
8081
+ getLastShadowDelta: () => null,
8082
+ getLastTrace: () => null,
8083
+ narrow() {
8084
+ },
8085
+ widen() {
8086
+ },
8087
+ addLabel() {
8088
+ },
8089
+ tools: {},
8090
+ getWorkflowState: () => Promise.resolve(null),
8091
+ handoff: () => Promise.resolve(null)
8092
+ };
8093
+ }
7902
8094
  function toNarrowingSnapshot(result) {
7903
8095
  if (!result || result.removed.length === 0) return null;
7904
8096
  return {
package/dist/index.d.cts CHANGED
@@ -587,6 +587,15 @@ type ReplayOptions = {
587
587
  runtimeUrl?: string;
588
588
  captureLevel?: CapturePrivacyTier;
589
589
  diagnostics?: (event: ObserveDiagnosticEvent | ReplayDiagnosticEvent) => void;
590
+ /**
591
+ * Explicit environment for zero-config governance mode selection.
592
+ * - "development" → monitor mode (log, don't block)
593
+ * - "staging" → protect mode (warn, don't block)
594
+ * - "production" → govern mode (block violations)
595
+ * Falls back to NODE_ENV if not set.
596
+ * @see zero-config-governance.md § Environment promotion
597
+ */
598
+ environment?: "development" | "staging" | "production";
590
599
  };
591
600
  /**
592
601
  * Raw tool executor provided by the user in `replay()` options.
@@ -1305,6 +1314,14 @@ type HandoffOfferResult = {
1305
1314
  eventSeq: number;
1306
1315
  stateVersion: number;
1307
1316
  };
1317
+ type GovernancePlanResult = {
1318
+ status: string;
1319
+ compiledSession?: unknown;
1320
+ compiledHash?: string;
1321
+ observations?: number;
1322
+ confidence?: string;
1323
+ version?: number;
1324
+ };
1308
1325
  type RuntimeClientHealth = {
1309
1326
  circuitOpen: boolean;
1310
1327
  failureCount: number;
@@ -1336,6 +1353,12 @@ declare class RuntimeClient {
1336
1353
  getWorkflowState(workflowId: string): Promise<WorkflowStateResult>;
1337
1354
  /** v4: Offer a handoff from a session. */
1338
1355
  offerHandoff(input: HandoffOfferInput): Promise<HandoffOfferResult>;
1356
+ /**
1357
+ * Fetch governance plan for an agent.
1358
+ * Returns null on 404 (no plan exists).
1359
+ * @see zero-config-governance.md § GET /api/v1/governance/plan
1360
+ */
1361
+ fetchGovernancePlan(agent: string, environment?: string): Promise<GovernancePlanResult | null>;
1339
1362
  getHealth(): RuntimeClientHealth;
1340
1363
  isCircuitOpen(): boolean;
1341
1364
  private get;
package/dist/index.d.ts CHANGED
@@ -587,6 +587,15 @@ type ReplayOptions = {
587
587
  runtimeUrl?: string;
588
588
  captureLevel?: CapturePrivacyTier;
589
589
  diagnostics?: (event: ObserveDiagnosticEvent | ReplayDiagnosticEvent) => void;
590
+ /**
591
+ * Explicit environment for zero-config governance mode selection.
592
+ * - "development" → monitor mode (log, don't block)
593
+ * - "staging" → protect mode (warn, don't block)
594
+ * - "production" → govern mode (block violations)
595
+ * Falls back to NODE_ENV if not set.
596
+ * @see zero-config-governance.md § Environment promotion
597
+ */
598
+ environment?: "development" | "staging" | "production";
590
599
  };
591
600
  /**
592
601
  * Raw tool executor provided by the user in `replay()` options.
@@ -1305,6 +1314,14 @@ type HandoffOfferResult = {
1305
1314
  eventSeq: number;
1306
1315
  stateVersion: number;
1307
1316
  };
1317
+ type GovernancePlanResult = {
1318
+ status: string;
1319
+ compiledSession?: unknown;
1320
+ compiledHash?: string;
1321
+ observations?: number;
1322
+ confidence?: string;
1323
+ version?: number;
1324
+ };
1308
1325
  type RuntimeClientHealth = {
1309
1326
  circuitOpen: boolean;
1310
1327
  failureCount: number;
@@ -1336,6 +1353,12 @@ declare class RuntimeClient {
1336
1353
  getWorkflowState(workflowId: string): Promise<WorkflowStateResult>;
1337
1354
  /** v4: Offer a handoff from a session. */
1338
1355
  offerHandoff(input: HandoffOfferInput): Promise<HandoffOfferResult>;
1356
+ /**
1357
+ * Fetch governance plan for an agent.
1358
+ * Returns null on 404 (no plan exists).
1359
+ * @see zero-config-governance.md § GET /api/v1/governance/plan
1360
+ */
1361
+ fetchGovernancePlan(agent: string, environment?: string): Promise<GovernancePlanResult | null>;
1339
1362
  getHealth(): RuntimeClientHealth;
1340
1363
  isCircuitOpen(): boolean;
1341
1364
  private get;
package/dist/index.js CHANGED
@@ -1142,6 +1142,14 @@ function optionalString(value, path) {
1142
1142
  }
1143
1143
  return value;
1144
1144
  }
1145
+ function extractReplayTrace(capture) {
1146
+ const replay2 = capture.replay;
1147
+ if (!isRecord(replay2)) return void 0;
1148
+ const trace = replay2.trace;
1149
+ if (!isRecord(trace)) return void 0;
1150
+ if (!Array.isArray(trace.entries)) return void 0;
1151
+ return trace;
1152
+ }
1145
1153
  function parseCommonCapture(capture, index, modelId, schemaVersion) {
1146
1154
  const toolNames = requireStringArray(capture.tool_names, `captures[${index}].tool_names`);
1147
1155
  const primaryToolName = capture.primary_tool_name === void 0 ? toolNames[0] ?? null : nullableString(capture.primary_tool_name, `captures[${index}].primary_tool_name`);
@@ -1159,7 +1167,8 @@ function parseCommonCapture(capture, index, modelId, schemaVersion) {
1159
1167
  ...capture.validation !== void 0 ? { validation: validateValidation(capture.validation, `captures[${index}].validation`) } : {},
1160
1168
  ...capture.usage !== void 0 ? { usage: validateUsage(capture.usage, `captures[${index}].usage`) } : {},
1161
1169
  latency_ms: requireNonNegativeInt(capture.latency_ms, `captures[${index}].latency_ms`),
1162
- ...sdkSessionId !== void 0 ? { sdk_session_id: sdkSessionId } : {}
1170
+ ...sdkSessionId !== void 0 ? { sdk_session_id: sdkSessionId } : {},
1171
+ ...extractReplayTrace(capture) !== void 0 ? { replay_trace: extractReplayTrace(capture) } : {}
1163
1172
  };
1164
1173
  }
1165
1174
  function parseLegacyCapturedCall(capture, index) {
@@ -2173,6 +2182,7 @@ import {
2173
2182
 
2174
2183
  // src/contracts.ts
2175
2184
  import {
2185
+ fillContractDefaults,
2176
2186
  hashToolSchema,
2177
2187
  loadContractSync,
2178
2188
  normalizeToolArray as normalizeToolArray2
@@ -2300,7 +2310,6 @@ function normalizeInlineContract(input) {
2300
2310
  if (!tool) {
2301
2311
  throw new ReplayConfigurationError("Inline contract is missing required field: tool");
2302
2312
  }
2303
- const assertions = toRecord5(source.assertions);
2304
2313
  const expectTools = toStringArray(source.expect_tools);
2305
2314
  const expectedToolCalls = toExpectedToolCalls(source.expected_tool_calls);
2306
2315
  const contract = {
@@ -2308,28 +2317,38 @@ function normalizeInlineContract(input) {
2308
2317
  ...toString5(source.tool_schema_hash) ? { tool_schema_hash: toString5(source.tool_schema_hash) } : {},
2309
2318
  ...isSideEffect(source.side_effect) ? { side_effect: source.side_effect } : {},
2310
2319
  ...toString5(source.contract_file) ? { contract_file: toString5(source.contract_file) } : {},
2311
- timeouts: {
2312
- total_ms: toNonNegativeNumber(toRecord5(source.timeouts).total_ms, 0)
2313
- },
2314
- retries: {
2315
- max_attempts: Math.max(1, toNonNegativeNumber(toRecord5(source.retries).max_attempts, 1)),
2316
- retry_on: toStringArray(toRecord5(source.retries).retry_on)
2317
- },
2318
- rate_limits: {
2319
- on_429: {
2320
- respect_retry_after: toBoolean(toRecord5(toRecord5(source.rate_limits).on_429).respect_retry_after, false),
2321
- max_sleep_seconds: toNonNegativeNumber(
2322
- toRecord5(toRecord5(source.rate_limits).on_429).max_sleep_seconds,
2323
- 0
2324
- )
2320
+ ...source.timeouts != null ? {
2321
+ timeouts: { total_ms: toNonNegativeNumber(toRecord5(source.timeouts).total_ms, 0) }
2322
+ } : {},
2323
+ ...source.retries != null ? {
2324
+ retries: {
2325
+ max_attempts: Math.max(1, toNonNegativeNumber(toRecord5(source.retries).max_attempts, 1)),
2326
+ retry_on: toStringArray(toRecord5(source.retries).retry_on)
2325
2327
  }
2326
- },
2327
- assertions: {
2328
- input_invariants: toInvariantArray(assertions.input_invariants),
2329
- output_invariants: toInvariantArray(assertions.output_invariants)
2330
- },
2331
- golden_cases: Array.isArray(source.golden_cases) ? source.golden_cases : [],
2332
- allowed_errors: toStringArray(source.allowed_errors),
2328
+ } : {},
2329
+ ...source.rate_limits != null ? {
2330
+ rate_limits: {
2331
+ on_429: {
2332
+ respect_retry_after: toBoolean(toRecord5(toRecord5(source.rate_limits).on_429).respect_retry_after, false),
2333
+ max_sleep_seconds: toNonNegativeNumber(
2334
+ toRecord5(toRecord5(source.rate_limits).on_429).max_sleep_seconds,
2335
+ 0
2336
+ )
2337
+ }
2338
+ }
2339
+ } : {},
2340
+ ...source.assertions != null ? {
2341
+ assertions: {
2342
+ input_invariants: toInvariantArray(toRecord5(source.assertions).input_invariants),
2343
+ output_invariants: toInvariantArray(toRecord5(source.assertions).output_invariants)
2344
+ }
2345
+ } : {},
2346
+ ...source.golden_cases != null ? {
2347
+ golden_cases: Array.isArray(source.golden_cases) ? source.golden_cases : []
2348
+ } : {},
2349
+ ...source.allowed_errors != null ? {
2350
+ allowed_errors: toStringArray(source.allowed_errors)
2351
+ } : {},
2333
2352
  ...expectTools.length > 0 ? { expect_tools: expectTools } : {},
2334
2353
  ...toToolOrder(source.tool_order, expectTools.length > 0) ? {
2335
2354
  tool_order: toToolOrder(source.tool_order, expectTools.length > 0)
@@ -2355,8 +2374,9 @@ function normalizeInlineContract(input) {
2355
2374
  ...Array.isArray(source.schema_derived_exclude) ? { schema_derived_exclude: source.schema_derived_exclude } : {},
2356
2375
  ...Array.isArray(source.binds) ? { binds: source.binds } : {}
2357
2376
  };
2358
- validateSafeRegexes(contract);
2359
- return contract;
2377
+ const filled = { ...fillContractDefaults(contract), ...contract.contract_file ? { contract_file: contract.contract_file } : {} };
2378
+ validateSafeRegexes(filled);
2379
+ return filled;
2360
2380
  }
2361
2381
  function validateContractSet(contracts) {
2362
2382
  const seenKeys = /* @__PURE__ */ new Set();
@@ -2375,11 +2395,11 @@ function validateSafeRegexes(contract) {
2375
2395
  const invariantGroups = [
2376
2396
  {
2377
2397
  label: "assertions.input_invariants",
2378
- invariants: contract.assertions.input_invariants
2398
+ invariants: contract.assertions?.input_invariants ?? []
2379
2399
  },
2380
2400
  {
2381
2401
  label: "assertions.output_invariants",
2382
- invariants: contract.assertions.output_invariants
2402
+ invariants: contract.assertions?.output_invariants ?? []
2383
2403
  }
2384
2404
  ];
2385
2405
  for (const [index, expectedToolCall] of (contract.expected_tool_calls ?? []).entries()) {
@@ -2982,7 +3002,7 @@ function evaluateExpectTools(contract, toolCalls) {
2982
3002
  function evaluateOutputInvariants(contract, normalizedResponse) {
2983
3003
  const invariantFailures = evaluateInvariants(
2984
3004
  normalizedResponse,
2985
- contract.assertions.output_invariants,
3005
+ contract.assertions?.output_invariants ?? [],
2986
3006
  process.env
2987
3007
  );
2988
3008
  return invariantFailures.map(
@@ -3032,7 +3052,7 @@ function evaluateArgumentInvariants(contract, toolCalls) {
3032
3052
  return failures;
3033
3053
  }
3034
3054
  function mapInvariantFailure(contract, failure, normalizedResponse) {
3035
- const invariant = findMatchingInvariant(contract.assertions.output_invariants, failure);
3055
+ const invariant = findMatchingInvariant(contract.assertions?.output_invariants ?? [], failure);
3036
3056
  const lookup = getPathValue(normalizedResponse, failure.path);
3037
3057
  return {
3038
3058
  path: failure.path,
@@ -4625,7 +4645,7 @@ function validateToolResultMessages(messages, contracts, provider) {
4625
4645
  for (const result of toolResults) {
4626
4646
  const contract = contractByTool.get(result.toolName);
4627
4647
  if (!contract) continue;
4628
- const outputInvariants = contract.assertions.output_invariants;
4648
+ const outputInvariants = contract.assertions?.output_invariants ?? [];
4629
4649
  if (outputInvariants.length === 0) continue;
4630
4650
  let parsed;
4631
4651
  try {
@@ -5275,6 +5295,32 @@ var RuntimeClient = class {
5275
5295
  stateVersion: h.state_version
5276
5296
  };
5277
5297
  }
5298
+ /**
5299
+ * Fetch governance plan for an agent.
5300
+ * Returns null on 404 (no plan exists).
5301
+ * @see zero-config-governance.md § GET /api/v1/governance/plan
5302
+ */
5303
+ async fetchGovernancePlan(agent, environment) {
5304
+ const env = environment ?? "development";
5305
+ try {
5306
+ const data = await this.get(
5307
+ `/api/v1/governance/plan?agent=${encodeURIComponent(agent)}&environment=${encodeURIComponent(env)}`
5308
+ );
5309
+ return {
5310
+ status: data.status,
5311
+ compiledSession: data.compiled_session,
5312
+ compiledHash: data.compiled_hash,
5313
+ observations: data.observations,
5314
+ confidence: data.confidence,
5315
+ version: data.version
5316
+ };
5317
+ } catch (err) {
5318
+ if (err instanceof RuntimeClientError && err.httpStatus === 404) {
5319
+ return null;
5320
+ }
5321
+ throw err;
5322
+ }
5323
+ }
5278
5324
  getHealth() {
5279
5325
  return {
5280
5326
  circuitOpen: this.now() < this.circuitOpenUntil,
@@ -5441,9 +5487,14 @@ function replay(client, opts = {}) {
5441
5487
  return createInactiveSession(client, sessionId, "Client already has an active observe() or replay() attachment");
5442
5488
  }
5443
5489
  let contracts;
5490
+ let zeroConfigMode = false;
5444
5491
  try {
5445
5492
  contracts = resolveContracts(opts);
5446
5493
  } catch (err) {
5494
+ const apiKeyForGov = resolveApiKey2(opts);
5495
+ if (apiKeyForGov && !opts.contracts && !opts.contractsDir) {
5496
+ return createGovernanceSession(client, sessionId, agent, provider, apiKeyForGov, opts, diagnostics);
5497
+ }
5447
5498
  const detail = err instanceof Error ? err.message : "Failed to load contracts";
5448
5499
  emitDiagnostic2(diagnostics, { type: "replay_compile_error", details: detail });
5449
5500
  return createBlockingInactiveSession(client, sessionId, detail);
@@ -5651,6 +5702,7 @@ function replay(client, opts = {}) {
5651
5702
  let manualFilter = null;
5652
5703
  const deferredReceipts = /* @__PURE__ */ new Map();
5653
5704
  let deferredPhase = null;
5705
+ const hasWrappedTools = opts.tools != null && Object.keys(opts.tools).length > 0;
5654
5706
  const contractLimits = resolveSessionLimits(contracts);
5655
5707
  const compiledLimits = compiledSession?.sessionLimits;
5656
5708
  const mergedLimits = { ...contractLimits ?? {}, ...compiledLimits ?? {} };
@@ -6610,9 +6662,29 @@ function replay(client, opts = {}) {
6610
6662
  }
6611
6663
  }
6612
6664
  }
6665
+ const compatHasPhaseTransition = !!(phaseResult?.legal && phaseResult.newPhase !== sessionState.currentPhase);
6666
+ const compatShouldDefer = hasWrappedTools && compatHasPhaseTransition;
6613
6667
  const prevVersion = sessionState.stateVersion;
6614
- sessionState = finalizeExecutedStep(sessionState, completedStep, contracts, compiledSession);
6668
+ sessionState = finalizeExecutedStep(
6669
+ sessionState,
6670
+ completedStep,
6671
+ contracts,
6672
+ compiledSession,
6673
+ compatShouldDefer ? { deferPhase: true } : void 0
6674
+ );
6615
6675
  syncStateToStore(prevVersion, sessionState);
6676
+ if (compatShouldDefer && compiledSession && phaseResult) {
6677
+ const advancingTools = /* @__PURE__ */ new Set();
6678
+ for (const tc of toolCalls) {
6679
+ const contract = compiledSession.perToolContracts.get(tc.name);
6680
+ if (contract?.transitions?.advances_to === phaseResult.newPhase) {
6681
+ advancingTools.add(tc.name);
6682
+ }
6683
+ }
6684
+ if (advancingTools.size > 0 && phaseResult.newPhase != null) {
6685
+ deferredPhase = { newPhase: phaseResult.newPhase, toolNames: advancingTools };
6686
+ }
6687
+ }
6616
6688
  }
6617
6689
  if (advisoryDecision.action === "block") {
6618
6690
  sessionState = recordDecisionOutcome(sessionState, "blocked");
@@ -6703,7 +6775,7 @@ function replay(client, opts = {}) {
6703
6775
  }
6704
6776
  }
6705
6777
  const hasPhaseTransition = phaseResult?.legal && phaseResult.newPhase !== sessionState.currentPhase;
6706
- const shouldDeferPhase = isActiveGovern && !attemptDegraded && hasPhaseTransition;
6778
+ const shouldDeferPhase = hasWrappedTools && !!hasPhaseTransition;
6707
6779
  const prevVersionAllow = sessionState.stateVersion;
6708
6780
  sessionState = finalizeExecutedStep(
6709
6781
  sessionState,
@@ -7346,7 +7418,7 @@ function validateResponse2(response, toolCalls, contracts, requestToolNames, unm
7346
7418
  }
7347
7419
  }
7348
7420
  for (const contract of matched) {
7349
- const outputInvariants = contract.assertions.output_invariants;
7421
+ const outputInvariants = contract.assertions?.output_invariants ?? [];
7350
7422
  if (outputInvariants.length > 0) {
7351
7423
  const normalizedResponse = buildNormalizedResponse(response, toolCalls);
7352
7424
  const result = evaluateInvariants4(normalizedResponse, outputInvariants, process.env);
@@ -7514,8 +7586,9 @@ function evaluateInputInvariants(request, contracts) {
7514
7586
  const requestToolSet = new Set(requestToolNames);
7515
7587
  for (const contract of contracts) {
7516
7588
  if (!requestToolSet.has(contract.tool)) continue;
7517
- if (contract.assertions.input_invariants.length === 0) continue;
7518
- const result = evaluateInvariants4(request, contract.assertions.input_invariants, process.env);
7589
+ const inputInvariants = contract.assertions?.input_invariants ?? [];
7590
+ if (inputInvariants.length === 0) continue;
7591
+ const result = evaluateInvariants4(request, inputInvariants, process.env);
7519
7592
  for (const failure of result) {
7520
7593
  failures.push({
7521
7594
  path: failure.path,
@@ -7890,6 +7963,126 @@ function createBlockingInactiveSession(client, sessionId, detail, configError) {
7890
7963
  handoff: () => Promise.resolve(null)
7891
7964
  };
7892
7965
  }
7966
+ function resolveGovernanceEnvironment(opts) {
7967
+ if (opts.environment) return opts.environment;
7968
+ const envVar = typeof process !== "undefined" ? process.env.REPLAYCI_ENVIRONMENT : void 0;
7969
+ if (envVar === "staging") return "staging";
7970
+ if (envVar === "production") return "production";
7971
+ if (envVar === "development") return "development";
7972
+ const nodeEnv = typeof process !== "undefined" ? process.env.NODE_ENV : void 0;
7973
+ if (nodeEnv === "production") return "production";
7974
+ return "development";
7975
+ }
7976
+ function governanceProtectionLevel(env) {
7977
+ switch (env) {
7978
+ case "production":
7979
+ return "govern";
7980
+ case "staging":
7981
+ return "protect";
7982
+ default:
7983
+ return "monitor";
7984
+ }
7985
+ }
7986
+ function createGovernanceSession(client, sessionId, agent, provider, apiKey, opts, diagnostics) {
7987
+ const environment = resolveGovernanceEnvironment(opts);
7988
+ const protLevel = governanceProtectionLevel(environment);
7989
+ const runtimeClient = new RuntimeClient({
7990
+ apiKey,
7991
+ apiUrl: opts.runtimeUrl
7992
+ });
7993
+ let governancePlan;
7994
+ let planFetchPromise = null;
7995
+ let planFetchDone = false;
7996
+ let planFetchError = null;
7997
+ planFetchPromise = runtimeClient.fetchGovernancePlan(agent, environment).then((result) => {
7998
+ governancePlan = result;
7999
+ planFetchDone = true;
8000
+ }).catch((err) => {
8001
+ planFetchDone = true;
8002
+ planFetchError = err instanceof Error ? err.message : String(err);
8003
+ governancePlan = null;
8004
+ });
8005
+ const captureBuffer = new CaptureBuffer({
8006
+ apiKey,
8007
+ endpoint: opts.runtimeUrl
8008
+ });
8009
+ registerBeforeExit(captureBuffer);
8010
+ const terminalInfo = resolveTerminal(client, provider);
8011
+ if (!terminalInfo) {
8012
+ emitDiagnostic2(diagnostics, { type: "replay_inactive", reason: "unsupported_client" });
8013
+ return createInactiveSession(client, sessionId, "Could not resolve terminal resource");
8014
+ }
8015
+ const { terminal, originalCreate } = terminalInfo;
8016
+ const patchedCreate = async function(...args) {
8017
+ if (!planFetchDone && planFetchPromise) {
8018
+ await planFetchPromise;
8019
+ }
8020
+ const hasApprovedPlan = governancePlan && (governancePlan.status === "approved" || governancePlan.status === "enforcing") && governancePlan.compiledSession;
8021
+ if (hasApprovedPlan) {
8022
+ }
8023
+ const result = await originalCreate.apply(this, args);
8024
+ try {
8025
+ const toolCalls = extractToolCalls(result, provider);
8026
+ const usage = extractUsage(result, provider);
8027
+ const requestArg = args[0] && typeof args[0] === "object" ? args[0] : {};
8028
+ captureBuffer.push({
8029
+ schema_version: CAPTURE_SCHEMA_VERSION_CURRENT,
8030
+ agent,
8031
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
8032
+ provider,
8033
+ model_id: requestArg.model ?? "unknown",
8034
+ primary_tool_name: toolCalls[0]?.name ?? null,
8035
+ tool_names: toolCalls.map((tc) => tc.name),
8036
+ request: requestArg,
8037
+ response: result,
8038
+ usage,
8039
+ latency_ms: 0,
8040
+ sdk_session_id: sessionId
8041
+ });
8042
+ } catch {
8043
+ }
8044
+ return result;
8045
+ };
8046
+ terminal[terminalInfo.methodName] = patchedCreate;
8047
+ setReplayAttached(client);
8048
+ return {
8049
+ client,
8050
+ flush: () => captureBuffer.flush(),
8051
+ restore() {
8052
+ terminal[terminalInfo.methodName] = originalCreate;
8053
+ },
8054
+ kill() {
8055
+ },
8056
+ getHealth: () => ({
8057
+ status: "healthy",
8058
+ authorityState: "active",
8059
+ protectionLevel: protLevel,
8060
+ durability: "inactive",
8061
+ tier: "compat",
8062
+ compatEnforcement: "protective",
8063
+ cluster_detected: false,
8064
+ bypass_detected: false,
8065
+ totalSteps: 0,
8066
+ totalBlocks: 0,
8067
+ totalErrors: 0,
8068
+ killed: false,
8069
+ shadowEvaluations: 0
8070
+ }),
8071
+ getState: () => EMPTY_STATE_SNAPSHOT,
8072
+ getLastNarrowing: () => null,
8073
+ getLastShadowDelta: () => null,
8074
+ getLastTrace: () => null,
8075
+ narrow() {
8076
+ },
8077
+ widen() {
8078
+ },
8079
+ addLabel() {
8080
+ },
8081
+ tools: {},
8082
+ getWorkflowState: () => Promise.resolve(null),
8083
+ handoff: () => Promise.resolve(null)
8084
+ };
8085
+ }
7893
8086
  function toNarrowingSnapshot(result) {
7894
8087
  if (!result || result.removed.length === 0) return null;
7895
8088
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@replayci/replay",
3
- "version": "0.1.14",
3
+ "version": "0.1.16",
4
4
  "description": "ReplayCI SDK for deterministic tool-call validation and observation.",
5
5
  "license": "ISC",
6
6
  "author": "ReplayCI",