@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/README.md +35 -0
- package/dist/hono.cjs +445 -11
- package/dist/hono.cjs.map +1 -1
- package/dist/hono.d.cts +2 -2
- package/dist/hono.d.ts +2 -2
- package/dist/hono.js +445 -11
- package/dist/hono.js.map +1 -1
- package/dist/index.cjs +2185 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +1699 -180
- package/dist/index.d.ts +1699 -180
- package/dist/index.js +2159 -13
- package/dist/index.js.map +1 -1
- package/dist/{protect-DiRVfVLq.d.cts → protect-C0t0fP1y.d.cts} +449 -2
- package/dist/{protect-DiRVfVLq.d.ts → protect-C0t0fP1y.d.ts} +449 -2
- package/package.json +6 -1
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-
|
|
3
|
-
export { P as Permit, b as ProtectRequest } from './protect-
|
|
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-
|
|
3
|
-
export { P as Permit, b as ProtectRequest } from './protect-
|
|
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
|
-
|
|
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
|
-
|
|
1888
|
-
|
|
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
|
-
|
|
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
|
|