@atlasent/sdk 2.5.0 → 2.10.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/dist/hono.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, ErrorHandler, MiddlewareHandler } from 'hono';
2
- import { A as AtlaSentDeniedError, a as AtlaSentError } from './protect-DiRVfVLq.cjs';
3
- export { P as Permit, b as ProtectRequest } from './protect-DiRVfVLq.cjs';
2
+ import { A as AtlaSentDeniedError, a as AtlaSentError } from './protect-C0t0fP1y.cjs';
3
+ export { P as Permit, b as ProtectRequest } from './protect-C0t0fP1y.cjs';
4
4
 
5
5
  /**
6
6
  * Hono middleware for AtlaSent execution-time authorization.
package/dist/hono.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Context, ErrorHandler, MiddlewareHandler } from 'hono';
2
- import { A as AtlaSentDeniedError, a as AtlaSentError } from './protect-DiRVfVLq.js';
3
- export { P as Permit, b as ProtectRequest } from './protect-DiRVfVLq.js';
2
+ import { A as AtlaSentDeniedError, a as AtlaSentError } from './protect-C0t0fP1y.js';
3
+ export { P as Permit, b as ProtectRequest } from './protect-C0t0fP1y.js';
4
4
 
5
5
  /**
6
6
  * Hono middleware for AtlaSent execution-time authorization.
package/dist/hono.js CHANGED
@@ -134,9 +134,8 @@ function normalizeEvaluateRequest(input) {
134
134
  action_type: legacy.action,
135
135
  actor_id: legacy.agent
136
136
  };
137
- if (legacy.context !== void 0) {
138
- normalized.context = legacy.context;
139
- }
137
+ if (legacy.context !== void 0) normalized.context = legacy.context;
138
+ if (legacy.explain !== void 0) normalized.explain = legacy.explain;
140
139
  return normalized;
141
140
  }
142
141
  return input;
@@ -308,6 +307,7 @@ var AtlaSentClient = class {
308
307
  actor_id: normalized.actor_id,
309
308
  context: normalized.context ?? {}
310
309
  };
310
+ if (normalized.explain !== void 0) body.explain = normalized.explain;
311
311
  const { body: wire, rateLimit } = await this.post(
312
312
  "/v1-evaluate",
313
313
  body
@@ -344,9 +344,211 @@ var AtlaSentClient = class {
344
344
  reason,
345
345
  auditHash: wire.audit_hash ?? "",
346
346
  timestamp: wire.timestamp ?? "",
347
+ rateLimit,
348
+ ...wire.risk_envelope && {
349
+ riskEnvelope: {
350
+ weightedScore: wire.risk_envelope.weighted_score,
351
+ engineDecision: wire.risk_envelope.engine_decision,
352
+ envelopeDecision: wire.risk_envelope.envelope_decision,
353
+ promoted: wire.risk_envelope.promoted,
354
+ hardBlocks: wire.risk_envelope.hard_blocks ?? [],
355
+ ...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
356
+ }
357
+ }
358
+ };
359
+ }
360
+ /**
361
+ * Batch evaluate — send up to 100 decisions in a single round-trip.
362
+ *
363
+ * Wraps `POST /v1-evaluate-batch`. The server evaluates each item
364
+ * against the active policy bundle and returns results in the same
365
+ * order as the input. One rate-limit token is consumed for the
366
+ * whole batch, and one audit-chain entry lists every included
367
+ * decision id.
368
+ *
369
+ * A per-item policy `deny` is **not** thrown — it appears as
370
+ * `item.decision === "deny"` in the returned items. A whole-batch
371
+ * network error, 4xx, or 5xx throws {@link AtlaSentError}.
372
+ *
373
+ * Requires the `v2_batch` tenant feature flag to be enabled on the
374
+ * org (returns 404 when off). Requires scope `evaluate:write`.
375
+ *
376
+ * @param requests - 1–100 evaluate items.
377
+ * @param batchId - Optional caller-supplied UUID for idempotency.
378
+ * A retried call with the same `batchId` and identical items
379
+ * returns the cached response within 24 h (`replayed: true`).
380
+ */
381
+ async evaluateBatch(requests, batchId) {
382
+ if (!Array.isArray(requests) || requests.length === 0) {
383
+ throw new AtlaSentError(
384
+ "evaluateBatch: requests must be a non-empty array",
385
+ { code: "bad_request" }
386
+ );
387
+ }
388
+ if (requests.length > 100) {
389
+ throw new AtlaSentError(
390
+ `evaluateBatch: requests.length ${requests.length} exceeds the 100-item cap`,
391
+ { code: "bad_request" }
392
+ );
393
+ }
394
+ const wireItems = requests.map((r) => ({
395
+ action_type: r.action,
396
+ actor_id: r.agent,
397
+ context: r.context ?? {}
398
+ }));
399
+ const wireBody = { items: wireItems };
400
+ if (batchId) wireBody.batch_id = batchId;
401
+ const { body: wire, rateLimit } = await this.post(
402
+ "/v1-evaluate-batch",
403
+ wireBody
404
+ );
405
+ const items = (wire.items ?? []).map(
406
+ (item) => {
407
+ const rawDecision = typeof item.decision === "string" ? item.decision.toLowerCase() : void 0;
408
+ const decision = rawDecision === "allow" || rawDecision === "deny" || rawDecision === "hold" || rawDecision === "escalate" ? rawDecision : void 0;
409
+ return {
410
+ index: item.index,
411
+ ...decision !== void 0 ? { decision } : {},
412
+ ...item.decision_id ? { decisionId: item.decision_id } : {},
413
+ ...item.permit_token != null ? { permitToken: item.permit_token } : {},
414
+ ...item.reason != null ? { reason: item.reason } : {},
415
+ ...item.audit_entry_hash ? { auditHash: item.audit_entry_hash } : {},
416
+ ...item.timestamp ? { timestamp: item.timestamp } : {},
417
+ ...item.error ? { error: item.error } : {},
418
+ ...item.message ? { message: item.message } : {}
419
+ };
420
+ }
421
+ );
422
+ return {
423
+ batchId: wire.batch_id,
424
+ items,
425
+ partial: wire.partial ?? false,
426
+ ...wire.replayed ? { replayed: wire.replayed } : {},
347
427
  rateLimit
348
428
  };
349
429
  }
430
+ /**
431
+ * Subscribe to a live stream of decisions for this org.
432
+ *
433
+ * Wraps `GET /v1-decisions-stream`. The server emits one SSE frame
434
+ * per audit event and sends a heartbeat every 15 s. The session
435
+ * auto-closes after `maxSeconds` (default 30 min); reconnect with
436
+ * the last received `event.id` to resume without replaying history.
437
+ *
438
+ * ```ts
439
+ * const controller = new AbortController();
440
+ * for await (const event of client.subscribeDecisions({ signal: controller.signal })) {
441
+ * if (event.type === "heartbeat") continue;
442
+ * console.log(event.type, event.decision, event.actorId);
443
+ * if (event.type === "session_end") break; // reconnect
444
+ * }
445
+ * ```
446
+ *
447
+ * Requires scope `audit:read`. Requires the `v2_decisions_stream`
448
+ * tenant feature flag (returns 404 when off).
449
+ */
450
+ async *subscribeDecisions(opts = {}) {
451
+ const url = new URL(`${this.baseUrl}/v1-decisions-stream`);
452
+ if (opts.types?.length) url.searchParams.set("types", opts.types.join(","));
453
+ if (opts.actorId) url.searchParams.set("actor_id", opts.actorId);
454
+ if (opts.maxSeconds !== void 0) url.searchParams.set("max_seconds", String(opts.maxSeconds));
455
+ const headers = {
456
+ Accept: "text/event-stream",
457
+ Authorization: `Bearer ${this.apiKey}`,
458
+ "User-Agent": this.userAgent,
459
+ // ADR-025: declare the wire-protocol version we were built
460
+ // against. Runtime serves this version's response shape; older
461
+ // versions outside the compatibility window get 426.
462
+ "X-AtlaSent-Protocol-Version": "1"
463
+ };
464
+ if (opts.lastEventId) headers["Last-Event-ID"] = opts.lastEventId;
465
+ let response;
466
+ try {
467
+ response = await this.fetchImpl(url.toString(), {
468
+ method: "GET",
469
+ headers,
470
+ ...opts.signal ? { signal: opts.signal } : {}
471
+ });
472
+ } catch (err) {
473
+ if (err instanceof Error && err.name === "AbortError") return;
474
+ throw new AtlaSentError(
475
+ `Failed to connect to decisions stream: ${err instanceof Error ? err.message : String(err)}`,
476
+ { code: "network" }
477
+ );
478
+ }
479
+ if (!response.ok) {
480
+ const code = response.status === 401 ? "invalid_api_key" : "server_error";
481
+ throw new AtlaSentError(
482
+ `Decisions stream returned ${response.status}`,
483
+ { code, status: response.status }
484
+ );
485
+ }
486
+ if (!response.body) {
487
+ throw new AtlaSentError("Decisions stream response has no body", { code: "bad_response" });
488
+ }
489
+ const reader = response.body.getReader();
490
+ const decoder = new TextDecoder("utf-8");
491
+ let buf = "";
492
+ try {
493
+ while (true) {
494
+ let chunk;
495
+ try {
496
+ chunk = await reader.read();
497
+ } catch (err) {
498
+ if (err instanceof Error && err.name === "AbortError") return;
499
+ throw new AtlaSentError(
500
+ `Decisions stream read error: ${err instanceof Error ? err.message : String(err)}`,
501
+ { code: "network" }
502
+ );
503
+ }
504
+ if (chunk.done) break;
505
+ buf += decoder.decode(chunk.value, { stream: true });
506
+ const rawBlocks = buf.split("\n\n");
507
+ buf = rawBlocks.pop() ?? "";
508
+ for (const block of rawBlocks) {
509
+ if (!block.trim()) continue;
510
+ if (block.trimStart().startsWith(":")) {
511
+ yield { type: "heartbeat" };
512
+ continue;
513
+ }
514
+ let id;
515
+ let eventType = "audit_event";
516
+ let dataLine = "";
517
+ for (const line of block.split("\n")) {
518
+ if (line.startsWith("id:")) id = line.slice(3).trim();
519
+ else if (line.startsWith("event:")) eventType = line.slice(6).trim();
520
+ else if (line.startsWith("data:")) dataLine = line.slice(5).trim();
521
+ }
522
+ if (!dataLine) continue;
523
+ let parsed;
524
+ try {
525
+ parsed = JSON.parse(dataLine);
526
+ } catch {
527
+ continue;
528
+ }
529
+ if (eventType === "session_end") {
530
+ yield { ...id !== void 0 ? { id } : {}, type: "session_end", payload: parsed };
531
+ return;
532
+ }
533
+ const decision = typeof parsed.decision === "string" ? parsed.decision.toLowerCase() : void 0;
534
+ yield {
535
+ ...id !== void 0 ? { id } : {},
536
+ type: eventType,
537
+ ...decision ? { decision } : {},
538
+ ...typeof parsed.actor_id === "string" ? { actorId: parsed.actor_id } : {},
539
+ ...typeof parsed.resource_type === "string" ? { resourceType: parsed.resource_type } : {},
540
+ ...typeof parsed.resource_id === "string" ? { resourceId: parsed.resource_id } : {},
541
+ ...parsed.payload && typeof parsed.payload === "object" ? { payload: parsed.payload } : {},
542
+ ...typeof parsed.hash === "string" ? { hash: parsed.hash } : {},
543
+ ...typeof parsed.previous_hash === "string" ? { previousHash: parsed.previous_hash } : {},
544
+ ...typeof parsed.occurred_at === "string" ? { occurredAt: parsed.occurred_at } : {}
545
+ };
546
+ }
547
+ }
548
+ } finally {
549
+ reader.releaseLock();
550
+ }
551
+ }
350
552
  /**
351
553
  * Pre-flight evaluation that always returns the constraint trace.
352
554
  *
@@ -413,7 +615,17 @@ var AtlaSentClient = class {
413
615
  reason,
414
616
  auditHash: wire.audit_hash ?? "",
415
617
  timestamp: wire.timestamp ?? "",
416
- rateLimit
618
+ rateLimit,
619
+ ...wire.risk_envelope && {
620
+ riskEnvelope: {
621
+ weightedScore: wire.risk_envelope.weighted_score,
622
+ engineDecision: wire.risk_envelope.engine_decision,
623
+ envelopeDecision: wire.risk_envelope.envelope_decision,
624
+ promoted: wire.risk_envelope.promoted,
625
+ hardBlocks: wire.risk_envelope.hard_blocks ?? [],
626
+ ...wire.risk_envelope.factors && { factors: wire.risk_envelope.factors }
627
+ }
628
+ }
417
629
  };
418
630
  let constraintTrace = null;
419
631
  if (wire.constraint_trace !== void 0 && wire.constraint_trace !== null && typeof wire.constraint_trace === "object") {
@@ -462,6 +674,7 @@ var AtlaSentClient = class {
462
674
  outcome: wire.outcome ?? "",
463
675
  permitHash: wire.permit_hash ?? "",
464
676
  timestamp: wire.timestamp ?? "",
677
+ expiresAt: wire.expires_at ?? null,
465
678
  rateLimit
466
679
  };
467
680
  }
@@ -783,6 +996,151 @@ var AtlaSentClient = class {
783
996
  }
784
997
  return { ...wire, rateLimit };
785
998
  }
999
+ /**
1000
+ * Re-evaluate a recorded decision against its originally-pinned policy
1001
+ * bundle and engine version, and report whether the result agrees with
1002
+ * what was recorded.
1003
+ *
1004
+ * Wraps `POST /v1-decisions-replay/:id/replay`. **Side-effect-free** — no
1005
+ * audit chain row is written and no permit is issued (per ADR-016).
1006
+ * Useful for compliance review, regression testing of bundle changes,
1007
+ * and post-incident investigation.
1008
+ *
1009
+ * Outcomes encoded in the response:
1010
+ * - `variance: "NONE"` — replay agrees with the original decision.
1011
+ * - `variance: "DECISION_CHANGED"` — same envelope, same bundle, different
1012
+ * decision. Almost always indicates non-determinism in a rule
1013
+ * (e.g. wall-clock comparison) and warrants investigation.
1014
+ * - `variance: "ENVELOPE_DRIFT"` — the recorded request envelope no longer
1015
+ * hashes to the recorded value. The replay short-circuits without
1016
+ * running the engine; `replay_decision` is absent. Treat as evidence
1017
+ * of substrate tamper or a recorder bug.
1018
+ *
1019
+ * Server-side 409 responses (replay refused because the engine version
1020
+ * does not accept replay, or because no bundle was pinned) surface as
1021
+ * `AtlaSentError` with `code: "replay_not_eligible"` — callers should
1022
+ * treat them as expected for old / un-pinned decisions, not as bugs.
1023
+ *
1024
+ * Requires the `evaluate:write` API key scope.
1025
+ *
1026
+ * @param decisionId The UUID of the recorded decision to replay.
1027
+ * Matches `execution_evaluations.request_id`.
1028
+ *
1029
+ * @example
1030
+ * ```ts
1031
+ * const result = await client.replayDecision("dec_abc123");
1032
+ * if (result.variance === "DECISION_CHANGED") {
1033
+ * console.warn(
1034
+ * `Decision ${result.decision_id} changed on replay: ` +
1035
+ * `${result.original_decision} → ${result.replay_decision}`,
1036
+ * );
1037
+ * }
1038
+ * ```
1039
+ */
1040
+ async replayDecision(decisionId) {
1041
+ if (typeof decisionId !== "string" || decisionId.length === 0) {
1042
+ throw new AtlaSentError("decisionId is required", {
1043
+ code: "bad_request"
1044
+ });
1045
+ }
1046
+ const path = `/v1-decisions-replay/${encodeURIComponent(decisionId)}/replay`;
1047
+ const { body: wire, rateLimit } = await this.post(
1048
+ path,
1049
+ {}
1050
+ );
1051
+ if (typeof wire.decision_id !== "string" || typeof wire.original_decision !== "string" || typeof wire.engine_version_kind !== "string" || typeof wire.accepts_replay !== "boolean" || typeof wire.variance !== "string" || typeof wire.envelope_verification !== "string" || typeof wire.replayed_at !== "string") {
1052
+ throw new AtlaSentError(
1053
+ "Malformed response from /v1-decisions-replay/:id/replay: missing required fields",
1054
+ { code: "bad_response" }
1055
+ );
1056
+ }
1057
+ return { ...wire, rateLimit };
1058
+ }
1059
+ /**
1060
+ * ADR-015 Phase C — SDK-canonical replay runtime.
1061
+ *
1062
+ * Re-evaluates a recorded decision against its originally-pinned policy
1063
+ * bundle and engine version via `POST /v1/decisions/:id/replay`.
1064
+ * Side-effect-free server-side: no audit chain row is written and no
1065
+ * permit is issued (ADR-016 `mode: "replay"` sentinel).
1066
+ *
1067
+ * Differences from {@link replayDecision} (the 2.7.0 raw-wire surface):
1068
+ *
1069
+ * | | `replayDecision()` | `replay()` |
1070
+ * | --- | --- | --- |
1071
+ * | Path | `/v1-decisions-replay/:id/replay` | `/v1/decisions/:id/replay` |
1072
+ * | Variance | raw wire (`DECISION_CHANGED`) | SDK-canonical (`POLICY_DRIFT`) |
1073
+ * | 409 handling | throws `AtlaSentError` | returns `ENGINE_DRIFT` / `BUNDLE_MISSING` |
1074
+ * | Input shape | `decisionId: string` | `{ evaluationId }` |
1075
+ *
1076
+ * **Never throws on `409 replay_not_eligible`** — instead returns a
1077
+ * `ReplayResponse` with `varianceKind: "ENGINE_DRIFT"` (engine retired
1078
+ * beyond archival window) or `"BUNDLE_MISSING"` (no bundle pinned on
1079
+ * the original evaluation). Callers can always `switch` on
1080
+ * `result.varianceKind` without a try/catch.
1081
+ *
1082
+ * Fix-forward note: this method was originally landed in PR #275 but
1083
+ * dropped from the squash merge. The TS types (`ReplayResponse`,
1084
+ * `ReplayRequest`) and CHANGELOG made it through; the method itself
1085
+ * did not. Restored here to match the Python {@link
1086
+ * AtlaSentClient}.replay() that landed in atlasent-sdk@2.6.0 (Python).
1087
+ */
1088
+ async replay(input) {
1089
+ if (!input || typeof input.evaluationId !== "string" || input.evaluationId.length === 0) {
1090
+ throw new AtlaSentError("evaluationId is required", {
1091
+ code: "bad_request"
1092
+ });
1093
+ }
1094
+ const path = `/v1/decisions/${encodeURIComponent(input.evaluationId)}/replay`;
1095
+ let wire;
1096
+ let rateLimit;
1097
+ try {
1098
+ const result = await this.post(path, {});
1099
+ wire = result.body;
1100
+ rateLimit = result.rateLimit;
1101
+ } catch (err) {
1102
+ if (err instanceof AtlaSentError && err.status === 409) {
1103
+ const msg = (err.message ?? "").toLowerCase();
1104
+ const varianceKind2 = msg.includes("bundle") ? "BUNDLE_MISSING" : "ENGINE_DRIFT";
1105
+ return {
1106
+ decisionId: input.evaluationId,
1107
+ varianceKind: varianceKind2,
1108
+ originalDecision: "deny",
1109
+ acceptsReplay: false,
1110
+ replayedAt: (/* @__PURE__ */ new Date()).toISOString(),
1111
+ rateLimit: null
1112
+ };
1113
+ }
1114
+ throw err;
1115
+ }
1116
+ const VARIANCE_MAP = {
1117
+ NONE: "NONE",
1118
+ DECISION_CHANGED: "POLICY_DRIFT",
1119
+ ENVELOPE_DRIFT: "ENVELOPE_DRIFT",
1120
+ CHAIN_TAMPER: "CHAIN_TAMPER",
1121
+ BUNDLE_MISSING: "BUNDLE_MISSING",
1122
+ ENGINE_DRIFT: "ENGINE_DRIFT"
1123
+ };
1124
+ const rawVariance = typeof wire.variance === "string" ? wire.variance : "";
1125
+ const varianceKind = VARIANCE_MAP[rawVariance] ?? "NONE";
1126
+ const replayDec = typeof wire.replay_decision === "string" ? wire.replay_decision.toLowerCase() : void 0;
1127
+ const originalDec = typeof wire.original_decision === "string" ? wire.original_decision.toLowerCase() : "deny";
1128
+ const response = {
1129
+ decisionId: typeof wire.decision_id === "string" ? wire.decision_id : input.evaluationId,
1130
+ varianceKind,
1131
+ originalDecision: originalDec,
1132
+ acceptsReplay: typeof wire.accepts_replay === "boolean" ? wire.accepts_replay : true,
1133
+ replayedAt: typeof wire.replayed_at === "string" ? wire.replayed_at : (/* @__PURE__ */ new Date()).toISOString(),
1134
+ rateLimit
1135
+ };
1136
+ if (typeof wire.original_deny_code === "string") response.originalDenyCode = wire.original_deny_code;
1137
+ if (replayDec !== void 0) response.replayedDecision = replayDec;
1138
+ if (typeof wire.replay_deny_code === "string") response.replayedDenyCode = wire.replay_deny_code;
1139
+ if (typeof wire.engine_version === "string") response.engineVersion = wire.engine_version;
1140
+ if (typeof wire.engine_version_kind === "string") response.engineVersionKind = wire.engine_version_kind;
1141
+ if (typeof wire.envelope_verification === "string") response.envelopeVerification = wire.envelope_verification;
1142
+ return response;
1143
+ }
786
1144
  /**
787
1145
  * Open a streaming evaluation session against `POST /v1-evaluate-stream`.
788
1146
  *
@@ -831,6 +1189,8 @@ var AtlaSentClient = class {
831
1189
  "Content-Type": "application/json",
832
1190
  Authorization: `Bearer ${this.apiKey}`,
833
1191
  "User-Agent": this.userAgent,
1192
+ // ADR-025: wire-protocol version declared on every request.
1193
+ "X-AtlaSent-Protocol-Version": "1",
834
1194
  "X-Request-ID": requestId
835
1195
  };
836
1196
  if (lastEventId !== void 0) {
@@ -918,7 +1278,9 @@ var AtlaSentClient = class {
918
1278
  Accept: "application/json",
919
1279
  Authorization: `Bearer ${this.apiKey}`,
920
1280
  "User-Agent": this.userAgent,
921
- "X-Request-ID": requestId
1281
+ "X-Request-ID": requestId,
1282
+ // ADR-025: wire-protocol version declared on every request.
1283
+ "X-AtlaSent-Protocol-Version": "1"
922
1284
  };
923
1285
  if (method === "POST") headers["Content-Type"] = "application/json";
924
1286
  const bodyStr = method === "POST" ? JSON.stringify(body) : void 0;
@@ -1534,6 +1896,69 @@ var AtlaSentClient = class {
1534
1896
  );
1535
1897
  return body;
1536
1898
  }
1899
+ // ── Constrained governance agents (read surface) ──────────────────────────
1900
+ //
1901
+ // Three GETs onto the v1-governance-agents edge function. Doctrine:
1902
+ // findings produced by these endpoints are advisory signal, never
1903
+ // authority. There is no `runGovernanceAgent` method on this client —
1904
+ // invocation belongs in CI (atlasent-action `governance-agents` mode),
1905
+ // not in application code.
1906
+ /**
1907
+ * List the advisory governance-agent registry for the calling org.
1908
+ *
1909
+ * Calls `GET /v1/governance/agents`. The registry is reference data
1910
+ * seeded at runtime-DB migration time; every row has
1911
+ * `authority_class = "advisory"` and `can_authorize = false` —
1912
+ * structural invariants enforced by the schema, not policy.
1913
+ */
1914
+ async listGovernanceAgents() {
1915
+ const { body } = await this.get(
1916
+ "/v1/governance/agents"
1917
+ );
1918
+ return [...body.agents ?? []];
1919
+ }
1920
+ /**
1921
+ * List advisory findings emitted against one governed change.
1922
+ *
1923
+ * Calls `GET /v1/governance/findings?change_id=…[&agent_slug=…]`.
1924
+ * Returns the typed-finding rows in `created_at DESC` order, including
1925
+ * `routed_gate_id` when the finding→gate trigger linked them. Findings
1926
+ * with `can_authorize === false` (always) are advisory; rendering them
1927
+ * never satisfies a gate.
1928
+ */
1929
+ async listGovernanceFindings(query) {
1930
+ if (!query?.change_id) {
1931
+ throw new AtlaSentError("change_id is required", { code: "bad_request" });
1932
+ }
1933
+ const params = new URLSearchParams({ change_id: query.change_id });
1934
+ if (query.agent_slug) params.set("agent_slug", query.agent_slug);
1935
+ const { body } = await this.get(
1936
+ "/v1/governance/findings",
1937
+ params
1938
+ );
1939
+ return [...body.findings ?? []];
1940
+ }
1941
+ /**
1942
+ * List agent run records against one governed change.
1943
+ *
1944
+ * Calls `GET /v1/governance/evaluations?change_id=…[&agent_slug=…]`.
1945
+ * Returns every persisted evaluation, including `failed` / `timeout`
1946
+ * runs and `completed` runs with zero findings — the latter is the
1947
+ * positive signal "the agent ran and found nothing", which the UI
1948
+ * surfaces as `clear`.
1949
+ */
1950
+ async listGovernanceEvaluations(query) {
1951
+ if (!query?.change_id) {
1952
+ throw new AtlaSentError("change_id is required", { code: "bad_request" });
1953
+ }
1954
+ const params = new URLSearchParams({ change_id: query.change_id });
1955
+ if (query.agent_slug) params.set("agent_slug", query.agent_slug);
1956
+ const { body } = await this.get(
1957
+ "/v1/governance/evaluations",
1958
+ params
1959
+ );
1960
+ return [...body.evaluations ?? []];
1961
+ }
1537
1962
  };
1538
1963
  function parseRateLimitHeaders(headers) {
1539
1964
  const rawLimit = headers.get("x-ratelimit-limit");
@@ -1835,6 +2260,7 @@ function getClient() {
1835
2260
  sharedClient = new AtlaSentClient(options);
1836
2261
  return sharedClient;
1837
2262
  }
2263
+ var ACTION_TYPE_RE = /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*)+$/;
1838
2264
  function wireDecisionToDenied(serverDecision) {
1839
2265
  const lower = serverDecision.toLowerCase();
1840
2266
  if (lower === "hold" || lower === "escalate") return lower;
@@ -1873,6 +2299,12 @@ async function computeExecutionHash(payload) {
1873
2299
  }
1874
2300
  }
1875
2301
  async function protect(request) {
2302
+ if (!ACTION_TYPE_RE.test(request.action)) {
2303
+ throw new AtlaSentError(
2304
+ `action must be in dot-notation format (e.g. "production.deploy"). Got: ${JSON.stringify(request.action)}`,
2305
+ { code: "bad_request" }
2306
+ );
2307
+ }
1876
2308
  const client = getClient();
1877
2309
  const evaluation = await client.evaluate(request);
1878
2310
  if (evaluation.decision !== "allow") {
@@ -1883,12 +2315,13 @@ async function protect(request) {
1883
2315
  auditHash: evaluation.auditHash
1884
2316
  });
1885
2317
  }
1886
- const environment = request.context?.environment ?? (() => {
1887
- console.warn(
1888
- "[atlasent] environment not set on evaluate request \u2014 defaulting to 'production'. Set context.environment explicitly to suppress."
2318
+ const environment = request.context?.environment;
2319
+ if (!environment) {
2320
+ throw new AtlaSentError(
2321
+ 'context.environment is required. Pass the environment where this action executes (e.g. "production", "staging").',
2322
+ { code: "bad_request" }
1889
2323
  );
1890
- return "production";
1891
- })();
2324
+ }
1892
2325
  const evaluatePayload = {
1893
2326
  action_type: request.action,
1894
2327
  actor_id: request.agent,
@@ -1919,7 +2352,8 @@ async function protect(request) {
1919
2352
  permitHash: verification.permitHash,
1920
2353
  auditHash: evaluation.auditHash,
1921
2354
  reason: evaluation.reason,
1922
- timestamp: verification.timestamp
2355
+ timestamp: verification.timestamp,
2356
+ permitExpiresAt: verification.expiresAt ?? null
1923
2357
  };
1924
2358
  }
1925
2359