sentinelayer-cli 0.43.0 → 0.45.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.
Files changed (48) hide show
  1. package/README.md +29 -16
  2. package/package.json +1 -1
  3. package/src/agents/jules/tools/aidenid-email.js +7 -9
  4. package/src/agents/jules/tools/frontend-analyze.js +15 -3
  5. package/src/agents/jules/tools/runtime-audit.js +51 -21
  6. package/src/ai/aidenid.js +5 -4
  7. package/src/ai/proxy.js +15 -14
  8. package/src/audit/orchestrator.js +8 -5
  9. package/src/auth/admission-scope.js +5 -3
  10. package/src/auth/credential-destinations.js +221 -0
  11. package/src/auth/gate.js +4 -4
  12. package/src/auth/http.js +22 -5
  13. package/src/auth/service.js +52 -56
  14. package/src/cli.js +14 -6
  15. package/src/commands/ai/provision-governance.js +7 -6
  16. package/src/commands/init.js +25 -16
  17. package/src/commands/scan.js +6 -4
  18. package/src/commands/session.js +166 -51
  19. package/src/commands/spec.js +2 -2
  20. package/src/commands/watch.js +4 -3
  21. package/src/config/service.js +37 -0
  22. package/src/daemon/pulse.js +2 -1
  23. package/src/daemon/watchdog.js +117 -68
  24. package/src/legacy-cli.js +119 -39
  25. package/src/mcp/bridge-inputs.js +223 -0
  26. package/src/mcp/cli-command-tools.js +160 -7
  27. package/src/mcp/registry.js +6 -5
  28. package/src/mcp/session-stdio-server.js +157 -8
  29. package/src/mcp/smoke.js +27 -11
  30. package/src/mcp/token-service.js +7 -9
  31. package/src/memory/retrieval.js +14 -5
  32. package/src/pocket/ring-owner.js +13 -8
  33. package/src/review/ai-review.js +2 -2
  34. package/src/review/dd-report-email-client.js +4 -5
  35. package/src/session/admission-access.js +41 -10
  36. package/src/session/admission-auth.js +95 -16
  37. package/src/session/admission.js +91 -21
  38. package/src/session/checkpoints.js +11 -10
  39. package/src/session/control-events.js +2 -0
  40. package/src/session/file-locks.js +19 -18
  41. package/src/session/invitations.js +26 -19
  42. package/src/session/live-source.js +13 -13
  43. package/src/session/reactions.js +258 -0
  44. package/src/session/route-classes.js +107 -0
  45. package/src/session/sync.js +110 -66
  46. package/src/session/tickets.js +21 -19
  47. package/src/session/title-sync.js +4 -3
  48. package/src/telemetry/sync.js +8 -9
@@ -4,6 +4,7 @@ import path from "node:path";
4
4
  import process from "node:process";
5
5
  import { randomUUID } from "node:crypto";
6
6
 
7
+ import { credentialedRequest, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
7
8
  import { resolveActiveAuthSession } from "../auth/service.js";
8
9
  import { createAgentEvent } from "../events/schema.js";
9
10
  import { isSessionControlEvent } from "./control-events.js";
@@ -19,6 +20,8 @@ const DEFAULT_API_BASE_URL = "https://api.sentinelayer.com";
19
20
  const DEFAULT_SYNC_TIMEOUT_MS = 5_000;
20
21
  const MAX_CONSECUTIVE_FAILURES = 3;
21
22
  const CIRCUIT_RESET_MS = 60_000;
23
+ // Message-action statuses whose body the caller needs to explain the refusal.
24
+ const MESSAGE_ACTION_REFUSAL_STATUSES = Object.freeze([409, 422]);
22
25
  const SESSION_INGEST_LIMIT_PER_MINUTE = 500;
23
26
  const HUMAN_MESSAGE_LIMIT_PER_MINUTE = 10;
24
27
  const HUMAN_MESSAGE_MAX_LENGTH = 2_000;
@@ -374,6 +377,24 @@ function recordCircuitFailure(circuit, nowMs) {
374
377
  persistCircuitState();
375
378
  }
376
379
 
380
+ // 401 and 403 refuse one credential (an expired, revoked or narrower agent admission, or
381
+ // an identity the account has not granted). They say nothing about API health, so they
382
+ // never count toward either machine-wide breaker (inbound or outbound): one agent's
383
+ // refusals must not pause another agent's traffic. Network errors, timeouts and 5xx do.
384
+ function isCredentialRefusal(response) {
385
+ return Boolean(response) && (response.status === 401 || response.status === 403);
386
+ }
387
+
388
+ function recordOutboundFailure(response, nowMs) {
389
+ if (isCredentialRefusal(response)) return;
390
+ recordCircuitFailure(outboundCircuit, nowMs);
391
+ }
392
+
393
+ function recordInboundFailure(response, nowMs) {
394
+ if (isCredentialRefusal(response)) return;
395
+ recordCircuitFailure(inboundCircuit, nowMs);
396
+ }
397
+
377
398
  function recordCircuitSuccess(circuit) {
378
399
  if (!circuit) {
379
400
  return;
@@ -657,13 +678,21 @@ export async function fetchJsonWithFullTimeout(
657
678
  const controller = new AbortController();
658
679
  const startedAtMs = Date.now();
659
680
  let response = null;
681
+ // A credential (src/auth/credential-destinations.js) is sent only to its own origin, by
682
+ // credentialedRequest; fetchImpl is the transport that it calls.
683
+ const { credential = null, ...init } = options || {};
660
684
  const requestOptions = {
661
- ...options,
685
+ ...init,
662
686
  signal: combineAbortSignals([options?.signal, controller.signal]),
663
687
  };
688
+ const sent = credential
689
+ ? credentialedRequest(credential, url, requestOptions, {
690
+ fetchImpl: (target, request) => fetchImpl(target, request, resolvedTimeoutMs),
691
+ })
692
+ : fetchImpl(url, requestOptions, resolvedTimeoutMs);
664
693
 
665
694
  response = await withTimeout(
666
- fetchImpl(url, requestOptions, resolvedTimeoutMs),
695
+ sent,
667
696
  resolvedTimeoutMs,
668
697
  {
669
698
  reason: "request_timeout",
@@ -989,7 +1018,7 @@ export async function syncSessionEventToApi(
989
1018
  return { synced: false, reason: "no_session" };
990
1019
  }
991
1020
 
992
- if (!session || !session.token) {
1021
+ if (!isAuthenticated(session)) {
993
1022
  return { synced: false, reason: "not_authenticated" };
994
1023
  }
995
1024
 
@@ -1003,8 +1032,8 @@ export async function syncSessionEventToApi(
1003
1032
  method: "POST",
1004
1033
  headers: {
1005
1034
  "Content-Type": "application/json",
1006
- Authorization: `Bearer ${session.token}`,
1007
1035
  },
1036
+ credential: await credentialFor(session),
1008
1037
  body: requestBody,
1009
1038
  };
1010
1039
  const resolvedTimeoutMs = normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS);
@@ -1033,18 +1062,23 @@ export async function syncSessionEventToApi(
1033
1062
  if (response && response.status === 403) {
1034
1063
  const body = forbiddenBody;
1035
1064
  if (isIdentityForgeryBody(body)) {
1065
+ if (session.source === "session_admission") {
1066
+ // An admitted agent acts only on its admission: account-level agent grants
1067
+ // are neither created nor used for it.
1068
+ return { synced: false, reason: "api_403" };
1069
+ }
1036
1070
  const agentId = normalizeString(event?.agent?.id);
1037
1071
  if (!agentId || isReservedAgentIdForGrant(agentId)) {
1038
1072
  // Reserved or empty — server enforcement is intentional, no grant
1039
1073
  // attempt is meaningful. Treat as a normal 403 failure.
1040
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1074
+ recordOutboundFailure(response, normalizedNowMs);
1041
1075
  return { synced: false, reason: "api_403" };
1042
1076
  }
1043
1077
  if (autoGrantAttemptedAgentIds.has(agentId)) {
1044
1078
  // We already tried to grant this identity in a prior event in this
1045
1079
  // process and either it succeeded but the server still says no, or
1046
1080
  // the grant failed. Either way, don't loop — surface the 403.
1047
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1081
+ recordOutboundFailure(response, normalizedNowMs);
1048
1082
  return { synced: false, reason: "api_403" };
1049
1083
  }
1050
1084
  rememberAutoGrantAttempt(agentId);
@@ -1059,8 +1093,8 @@ export async function syncSessionEventToApi(
1059
1093
  method: "POST",
1060
1094
  headers: {
1061
1095
  "Content-Type": "application/json",
1062
- Authorization: `Bearer ${session.token}`,
1063
1096
  },
1097
+ credential: await credentialFor(session),
1064
1098
  body: JSON.stringify({
1065
1099
  agent_id: agentId,
1066
1100
  role: grantRole,
@@ -1081,7 +1115,7 @@ export async function syncSessionEventToApi(
1081
1115
  // Treat 409 (already granted) as success — idempotent on the server.
1082
1116
  const grantIdempotent = grantStatus === 409;
1083
1117
  if (!grantOk && !grantIdempotent) {
1084
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1118
+ recordOutboundFailure(grantResponse, normalizedNowMs);
1085
1119
  return {
1086
1120
  synced: false,
1087
1121
  reason: `grant_failed_${grantStatus || "no_response"}`,
@@ -1109,7 +1143,7 @@ export async function syncSessionEventToApi(
1109
1143
  autoGranted: true,
1110
1144
  };
1111
1145
  }
1112
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1146
+ recordOutboundFailure(retryResponse, normalizedNowMs);
1113
1147
  return {
1114
1148
  synced: false,
1115
1149
  reason: `api_${retryResponse ? retryResponse.status : "no_response"}`,
@@ -1117,7 +1151,7 @@ export async function syncSessionEventToApi(
1117
1151
  }
1118
1152
  }
1119
1153
 
1120
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1154
+ recordOutboundFailure(response, normalizedNowMs);
1121
1155
  return {
1122
1156
  synced: false,
1123
1157
  reason: `api_${response ? response.status : "no_response"}`,
@@ -1182,7 +1216,7 @@ async function syncSessionAuxPayload(
1182
1216
  } catch {
1183
1217
  return { synced: false, reason: "no_session" };
1184
1218
  }
1185
- if (!session || !session.token) {
1219
+ if (!isAuthenticated(session)) {
1186
1220
  return { synced: false, reason: "not_authenticated" };
1187
1221
  }
1188
1222
 
@@ -1195,8 +1229,8 @@ async function syncSessionAuxPayload(
1195
1229
  method: "POST",
1196
1230
  headers: {
1197
1231
  "Content-Type": "application/json",
1198
- Authorization: `Bearer ${session.token}`,
1199
1232
  },
1233
+ credential: await credentialFor(session),
1200
1234
  body: JSON.stringify({
1201
1235
  ...payload,
1202
1236
  source: "cli",
@@ -1207,7 +1241,7 @@ async function syncSessionAuxPayload(
1207
1241
  { readSuccessBody: false },
1208
1242
  );
1209
1243
  if (!response || !response.ok) {
1210
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1244
+ recordOutboundFailure(response, normalizedNowMs);
1211
1245
  return {
1212
1246
  synced: false,
1213
1247
  reason: `api_${response ? response.status : "no_response"}`,
@@ -1301,7 +1335,7 @@ export async function pollHumanMessages(
1301
1335
  cursor: normalizeString(since) || null,
1302
1336
  };
1303
1337
  }
1304
- if (!session || !session.token) {
1338
+ if (!isAuthenticated(session)) {
1305
1339
  return {
1306
1340
  ok: false,
1307
1341
  reason: "not_authenticated",
@@ -1325,14 +1359,14 @@ export async function pollHumanMessages(
1325
1359
  {
1326
1360
  method: "GET",
1327
1361
  headers: {
1328
- Authorization: `Bearer ${session.token}`,
1329
1362
  },
1363
+ credential: await credentialFor(session),
1330
1364
  },
1331
1365
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1332
1366
  fetchImpl,
1333
1367
  );
1334
1368
  if (!response || !response.ok) {
1335
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
1369
+ recordInboundFailure(response, normalizedNowMs);
1336
1370
  return {
1337
1371
  ok: false,
1338
1372
  reason: `api_${response ? response.status : "no_response"}`,
@@ -1468,7 +1502,7 @@ export async function pollSessionEvents(
1468
1502
  cursor: normalizeString(since) || null,
1469
1503
  };
1470
1504
  }
1471
- if (!session || !session.token) {
1505
+ if (!isAuthenticated(session)) {
1472
1506
  return {
1473
1507
  ok: false,
1474
1508
  reason: "not_authenticated",
@@ -1499,7 +1533,7 @@ export async function pollSessionEvents(
1499
1533
  endpoint,
1500
1534
  {
1501
1535
  method: "GET",
1502
- headers: { Authorization: `Bearer ${session.token}` },
1536
+ credential: await credentialFor(session),
1503
1537
  },
1504
1538
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1505
1539
  fetchImpl,
@@ -1517,7 +1551,7 @@ export async function pollSessionEvents(
1517
1551
  cursor: normalizedSince || null,
1518
1552
  };
1519
1553
  }
1520
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
1554
+ recordInboundFailure(response, normalizedNowMs);
1521
1555
  return {
1522
1556
  ok: false,
1523
1557
  reason: `api_${response ? response.status : "no_response"}`,
@@ -1636,7 +1670,7 @@ export async function fetchSessionPresence(
1636
1670
  present: [],
1637
1671
  };
1638
1672
  }
1639
- if (!session?.token) {
1673
+ if (!isAuthenticated(session)) {
1640
1674
  return {
1641
1675
  ok: false,
1642
1676
  reason: "not_authenticated",
@@ -1653,7 +1687,7 @@ export async function fetchSessionPresence(
1653
1687
  endpoint,
1654
1688
  {
1655
1689
  method: "GET",
1656
- headers: { Authorization: `Bearer ${session.token}` },
1690
+ credential: await credentialFor(session),
1657
1691
  },
1658
1692
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
1659
1693
  fetchImpl,
@@ -1679,7 +1713,7 @@ export async function fetchSessionPresence(
1679
1713
  present: [],
1680
1714
  };
1681
1715
  }
1682
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
1716
+ recordInboundFailure(response, normalizedNowMs);
1683
1717
  return {
1684
1718
  ok: false,
1685
1719
  reason: `api_${response ? response.status : "no_response"}`,
@@ -1788,7 +1822,7 @@ export async function renewSessionPresence(
1788
1822
  recorded: false,
1789
1823
  };
1790
1824
  }
1791
- if (!session?.token) {
1825
+ if (!isAuthenticated(session)) {
1792
1826
  return {
1793
1827
  ok: false,
1794
1828
  reason: "not_authenticated",
@@ -1817,8 +1851,8 @@ export async function renewSessionPresence(
1817
1851
  method: "PUT",
1818
1852
  headers: {
1819
1853
  "Content-Type": "application/json",
1820
- Authorization: `Bearer ${session.token}`,
1821
1854
  },
1855
+ credential: await credentialFor(session),
1822
1856
  body: JSON.stringify(body),
1823
1857
  signal,
1824
1858
  },
@@ -1843,7 +1877,7 @@ export async function renewSessionPresence(
1843
1877
  retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
1844
1878
  };
1845
1879
  }
1846
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1880
+ recordOutboundFailure(response, normalizedNowMs);
1847
1881
  return {
1848
1882
  ok: false,
1849
1883
  reason: `api_${response ? response.status : "no_response"}`,
@@ -1913,7 +1947,7 @@ export async function requestSessionListenerStop(
1913
1947
  } catch {
1914
1948
  return { ok: false, reason: "no_session", recorded: false };
1915
1949
  }
1916
- if (!session?.token) {
1950
+ if (!isAuthenticated(session)) {
1917
1951
  return { ok: false, reason: "not_authenticated", recorded: false };
1918
1952
  }
1919
1953
 
@@ -1933,9 +1967,9 @@ export async function requestSessionListenerStop(
1933
1967
  method: "POST",
1934
1968
  headers: {
1935
1969
  "Content-Type": "application/json",
1936
- Authorization: `Bearer ${session.token}`,
1937
1970
  "Idempotency-Key": normalizedIdempotencyKey,
1938
1971
  },
1972
+ credential: await credentialFor(session),
1939
1973
  body: JSON.stringify(body),
1940
1974
  },
1941
1975
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
@@ -1958,7 +1992,7 @@ export async function requestSessionListenerStop(
1958
1992
  retryAfterMs: retryAfterMsFromResponse(response, payload, normalizedNowMs),
1959
1993
  };
1960
1994
  }
1961
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
1995
+ recordOutboundFailure(response, normalizedNowMs);
1962
1996
  return {
1963
1997
  ok: false,
1964
1998
  reason: `api_${response ? response.status : "no_response"}`,
@@ -2041,7 +2075,7 @@ export async function updateSessionReadCursor(
2041
2075
  } catch {
2042
2076
  return { ok: false, reason: "no_session", updated: false };
2043
2077
  }
2044
- if (!session?.token) {
2078
+ if (!isAuthenticated(session)) {
2045
2079
  return { ok: false, reason: "not_authenticated", updated: false };
2046
2080
  }
2047
2081
 
@@ -2062,8 +2096,8 @@ export async function updateSessionReadCursor(
2062
2096
  method: "PUT",
2063
2097
  headers: {
2064
2098
  "Content-Type": "application/json",
2065
- Authorization: `Bearer ${session.token}`,
2066
2099
  },
2100
+ credential: await credentialFor(session),
2067
2101
  body: JSON.stringify(body),
2068
2102
  signal,
2069
2103
  },
@@ -2083,7 +2117,7 @@ export async function updateSessionReadCursor(
2083
2117
  updated: false,
2084
2118
  };
2085
2119
  }
2086
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
2120
+ recordOutboundFailure(response, normalizedNowMs);
2087
2121
  return {
2088
2122
  ok: false,
2089
2123
  reason: `api_${response ? response.status : "no_response"}`,
@@ -2175,7 +2209,7 @@ export async function streamSessionEvents(
2175
2209
  errorCount: 0,
2176
2210
  };
2177
2211
  }
2178
- if (!session || !session.token) {
2212
+ if (!isAuthenticated(session)) {
2179
2213
  return {
2180
2214
  ok: false,
2181
2215
  reason: "not_authenticated",
@@ -2225,18 +2259,18 @@ export async function streamSessionEvents(
2225
2259
 
2226
2260
  let response;
2227
2261
  try {
2228
- response = await fetchImpl(
2262
+ response = await credentialedRequest(
2263
+ await credentialFor(session),
2229
2264
  endpoint,
2230
2265
  {
2231
2266
  method: "GET",
2232
2267
  headers: {
2233
2268
  Accept: "text/event-stream",
2234
- Authorization: `Bearer ${session.token}`,
2235
2269
  ...(normalizedSince ? { "Last-Event-ID": normalizedSince } : {}),
2236
2270
  },
2237
2271
  signal: controller.signal,
2238
2272
  },
2239
- normalizedTimeoutMs
2273
+ { fetchImpl: (target, init) => fetchImpl(target, init, normalizedTimeoutMs) },
2240
2274
  );
2241
2275
  } catch (error) {
2242
2276
  clearTimeout(timeoutHandle);
@@ -2396,7 +2430,7 @@ export async function pollSessionEventsBefore(
2396
2430
  beforeSequence: null,
2397
2431
  };
2398
2432
  }
2399
- if (!session || !session.token) {
2433
+ if (!isAuthenticated(session)) {
2400
2434
  return {
2401
2435
  ok: false,
2402
2436
  reason: "not_authenticated",
@@ -2423,7 +2457,7 @@ export async function pollSessionEventsBefore(
2423
2457
  endpoint,
2424
2458
  {
2425
2459
  method: "GET",
2426
- headers: { Authorization: `Bearer ${session.token}` },
2460
+ credential: await credentialFor(session),
2427
2461
  },
2428
2462
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2429
2463
  fetchImpl,
@@ -2441,7 +2475,7 @@ export async function pollSessionEventsBefore(
2441
2475
  beforeSequence: Number.isFinite(normalizedBeforeSequence) ? normalizedBeforeSequence : null,
2442
2476
  };
2443
2477
  }
2444
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
2478
+ recordInboundFailure(response, normalizedNowMs);
2445
2479
  return {
2446
2480
  ok: false,
2447
2481
  reason: `api_${response ? response.status : "no_response"}`,
@@ -2519,7 +2553,7 @@ export async function listSessionMessageActions(
2519
2553
  } catch {
2520
2554
  return { ok: false, reason: "no_session", actions: [], count: 0, projection: null };
2521
2555
  }
2522
- if (!session || !session.token) {
2556
+ if (!isAuthenticated(session)) {
2523
2557
  return { ok: false, reason: "not_authenticated", actions: [], count: 0, projection: null };
2524
2558
  }
2525
2559
 
@@ -2544,13 +2578,13 @@ export async function listSessionMessageActions(
2544
2578
  endpoint,
2545
2579
  {
2546
2580
  method: "GET",
2547
- headers: { Authorization: `Bearer ${session.token}` },
2581
+ credential: await credentialFor(session),
2548
2582
  },
2549
2583
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2550
2584
  fetchImpl,
2551
2585
  );
2552
2586
  if (!response || !response.ok) {
2553
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
2587
+ recordInboundFailure(response, normalizedNowMs);
2554
2588
  return {
2555
2589
  ok: false,
2556
2590
  reason: `api_${response ? response.status : "no_response"}`,
@@ -2624,7 +2658,7 @@ export async function fetchSessionUsageLedger(
2624
2658
  } catch {
2625
2659
  return { ok: false, reason: "no_session", status: 0, payload: null };
2626
2660
  }
2627
- if (!session || !session.token) {
2661
+ if (!isAuthenticated(session)) {
2628
2662
  return { ok: false, reason: "not_authenticated", status: 0, payload: null };
2629
2663
  }
2630
2664
 
@@ -2641,13 +2675,13 @@ export async function fetchSessionUsageLedger(
2641
2675
  endpoint,
2642
2676
  {
2643
2677
  method: "GET",
2644
- headers: { Authorization: `Bearer ${session.token}` },
2678
+ credential: await credentialFor(session),
2645
2679
  },
2646
2680
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2647
2681
  fetchImpl,
2648
2682
  );
2649
2683
  if (!response || !response.ok) {
2650
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
2684
+ recordInboundFailure(response, normalizedNowMs);
2651
2685
  return {
2652
2686
  ok: false,
2653
2687
  reason: `api_${response ? response.status : "no_response"}`,
@@ -2823,9 +2857,9 @@ export async function editSessionMessage(sessionId, {
2823
2857
  let auth;
2824
2858
  try { auth = await resolveAuthSession({ cwd: targetPath, env: process.env, autoRotate: false }); }
2825
2859
  catch { return { ok: false, reason: "no_session" }; }
2826
- if (!auth?.token) return { ok: false, reason: "not_authenticated" };
2860
+ if (!isAuthenticated(auth)) return { ok: false, reason: "not_authenticated" };
2827
2861
  const base = `${resolveApiBaseUrl(auth)}/api/v1/sessions/${encodeURIComponent(sid)}`;
2828
- const headers = { Authorization: `Bearer ${auth.token}` };
2862
+ const credential = await credentialFor(auth);
2829
2863
  const failure = (response, payload) => {
2830
2864
  const code = normalizeString(payload?.detail?.code || payload?.error?.code || payload?.code);
2831
2865
  // Client/schema/ownership conflicts are terminal and must not poison unrelated writes.
@@ -2838,7 +2872,7 @@ export async function editSessionMessage(sessionId, {
2838
2872
  const resource = replyId ? `replies/${encodeURIComponent(replyId)}` : messageId
2839
2873
  ? `messages/${encodeURIComponent(messageId)}` : `messages/by-sequence/${sequence}`;
2840
2874
  const authorQuery = normalizeString(agentId) ? `?agentId=${encodeURIComponent(normalizeString(agentId))}` : "";
2841
- const { response, payload } = await fetchJsonWithFullTimeout(`${base}/${resource}${authorQuery}`, { method: "GET", headers, signal }, timeoutMs, fetchImpl, { readErrorBody: true });
2875
+ const { response, payload } = await fetchJsonWithFullTimeout(`${base}/${resource}${authorQuery}`, { method: "GET", credential, signal }, timeoutMs, fetchImpl, { readErrorBody: true });
2842
2876
  if (!response?.ok || payload?.ok === false) return failure(response, payload);
2843
2877
  const current = replyId ? payload?.reply : payload?.event;
2844
2878
  const fetchedRevision = Number(current?.messageRevision ?? 1);
@@ -2852,11 +2886,12 @@ export async function editSessionMessage(sessionId, {
2852
2886
  const { response, payload } = await fetchJsonWithFullTimeout(`${base}/${resource}`, {
2853
2887
  method: "PATCH",
2854
2888
  headers: {
2855
- ...createSessionMutationHeaders({ bearerToken: auth.token, sessionId: sid, routeId: replyId
2889
+ ...createSessionMutationHeaders({ credential, sessionId: sid, routeId: replyId
2856
2890
  ? "PATCH /api/v1/sessions/{session_id}/replies/{action_id}"
2857
2891
  : "PATCH /api/v1/sessions/{session_id}/messages/{message_id}", idempotencyKey: mutationKey }),
2858
2892
  "Content-Type": "application/json", "Idempotency-Key": mutationKey,
2859
2893
  },
2894
+ credential,
2860
2895
  body: JSON.stringify(body),
2861
2896
  signal,
2862
2897
  }, timeoutMs, fetchImpl, { readErrorBody: true });
@@ -2945,7 +2980,7 @@ export async function createSessionMessageAction(
2945
2980
  } catch {
2946
2981
  return { ok: false, reason: "no_session", action: null };
2947
2982
  }
2948
- if (!session || !session.token) {
2983
+ if (!isAuthenticated(session)) {
2949
2984
  return { ok: false, reason: "not_authenticated", action: null };
2950
2985
  }
2951
2986
 
@@ -2983,19 +3018,27 @@ export async function createSessionMessageAction(
2983
3018
  method: "POST",
2984
3019
  headers: {
2985
3020
  "Content-Type": "application/json",
2986
- Authorization: `Bearer ${session.token}`,
2987
3021
  },
3022
+ credential: await credentialFor(session),
2988
3023
  body: JSON.stringify(body),
2989
3024
  signal,
2990
3025
  },
2991
3026
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
2992
3027
  fetchImpl,
3028
+ { errorBodyStatuses: MESSAGE_ACTION_REFUSAL_STATUSES },
2993
3029
  );
2994
3030
  if (!response || !response.ok) {
2995
- recordCircuitFailure(outboundCircuit, normalizedNowMs);
3031
+ const status = response ? response.status : null;
3032
+ const refused = MESSAGE_ACTION_REFUSAL_STATUSES.includes(status);
3033
+ // A refusal (409 nothing-to-undo, 422 a server that predates the action) is an
3034
+ // answer about this request, not API health: it must not open the persisted
3035
+ // breaker that guards every other outbound write.
3036
+ if (!refused) recordOutboundFailure(response, normalizedNowMs);
2996
3037
  return {
2997
3038
  ok: false,
2998
- reason: `api_${response ? response.status : "no_response"}`,
3039
+ reason: `api_${status ?? "no_response"}`,
3040
+ status,
3041
+ error: refused && payload && typeof payload === "object" ? payload : null,
2999
3042
  action: null,
3000
3043
  };
3001
3044
  }
@@ -3004,6 +3047,9 @@ export async function createSessionMessageAction(
3004
3047
  ok: Boolean(payload?.ok ?? true),
3005
3048
  reason: "",
3006
3049
  duplicate: Boolean(payload?.duplicate),
3050
+ // A repeat reaction is a no-op whose key the server retains as a collapsed row
3051
+ // (sentinelayer-api#914): the id of that evidence row.
3052
+ collapsedActionId: normalizeString(payload?.collapsedActionId) || null,
3007
3053
  action: payload?.action && typeof payload.action === "object" ? payload.action : null,
3008
3054
  };
3009
3055
  } catch (error) {
@@ -3074,7 +3120,7 @@ export async function searchSessionEvents(
3074
3120
  nextBeforeSequence: null,
3075
3121
  };
3076
3122
  }
3077
- if (!session || !session.token) {
3123
+ if (!isAuthenticated(session)) {
3078
3124
  return {
3079
3125
  ok: false,
3080
3126
  reason: "not_authenticated",
@@ -3104,13 +3150,13 @@ export async function searchSessionEvents(
3104
3150
  endpoint,
3105
3151
  {
3106
3152
  method: "GET",
3107
- headers: { Authorization: `Bearer ${session.token}` },
3153
+ credential: await credentialFor(session),
3108
3154
  },
3109
3155
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
3110
3156
  fetchImpl,
3111
3157
  );
3112
3158
  if (!response || !response.ok) {
3113
- recordCircuitFailure(inboundCircuit, normalizedNowMs);
3159
+ recordInboundFailure(response, normalizedNowMs);
3114
3160
  return {
3115
3161
  ok: false,
3116
3162
  reason: `api_${response ? response.status : "no_response"}`,
@@ -3196,7 +3242,7 @@ export async function listSessionsFromApi({
3196
3242
  warnings: [],
3197
3243
  };
3198
3244
  }
3199
- if (!session || !session.token) {
3245
+ if (!isAuthenticated(session)) {
3200
3246
  return {
3201
3247
  ok: false,
3202
3248
  reason: "not_authenticated",
@@ -3232,7 +3278,7 @@ export async function listSessionsFromApi({
3232
3278
  endpoint,
3233
3279
  {
3234
3280
  method: "GET",
3235
- headers: { Authorization: `Bearer ${session.token}` },
3281
+ credential: await credentialFor(session),
3236
3282
  },
3237
3283
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
3238
3284
  fetchImpl,
@@ -3338,7 +3384,7 @@ export async function fetchSessionFromApi(
3338
3384
  } catch {
3339
3385
  return { ok: false, reason: "no_session", session: null };
3340
3386
  }
3341
- if (!session || !session.token) {
3387
+ if (!isAuthenticated(session)) {
3342
3388
  return { ok: false, reason: "not_authenticated", session: null, status: 401 };
3343
3389
  }
3344
3390
 
@@ -3352,7 +3398,7 @@ export async function fetchSessionFromApi(
3352
3398
  endpoint,
3353
3399
  {
3354
3400
  method: "GET",
3355
- headers: { Authorization: `Bearer ${session.token}` },
3401
+ credential: await credentialFor(session),
3356
3402
  },
3357
3403
  normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
3358
3404
  fetchImpl,
@@ -3417,7 +3463,7 @@ export async function probeSessionAccess(
3417
3463
  } catch {
3418
3464
  return { accessible: false, reason: "no_session" };
3419
3465
  }
3420
- if (!session || !session.token) {
3466
+ if (!isAuthenticated(session)) {
3421
3467
  return { accessible: false, reason: "not_authenticated" };
3422
3468
  }
3423
3469
 
@@ -3428,13 +3474,11 @@ export async function probeSessionAccess(
3428
3474
 
3429
3475
  let response;
3430
3476
  try {
3431
- response = await fetchImpl(
3477
+ response = await credentialedRequest(
3478
+ await credentialFor(session),
3432
3479
  endpoint,
3433
- {
3434
- method: "GET",
3435
- headers: { Authorization: `Bearer ${session.token}` },
3436
- },
3437
- normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS),
3480
+ { method: "GET" },
3481
+ { fetchImpl: (target, init) => fetchImpl(target, init, normalizePositiveInteger(timeoutMs, DEFAULT_SYNC_TIMEOUT_MS)) },
3438
3482
  );
3439
3483
  } catch (err) {
3440
3484
  return {