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.
- package/README.md +29 -16
- package/package.json +1 -1
- package/src/agents/jules/tools/aidenid-email.js +7 -9
- package/src/agents/jules/tools/frontend-analyze.js +15 -3
- package/src/agents/jules/tools/runtime-audit.js +51 -21
- package/src/ai/aidenid.js +5 -4
- package/src/ai/proxy.js +15 -14
- package/src/audit/orchestrator.js +8 -5
- package/src/auth/admission-scope.js +5 -3
- package/src/auth/credential-destinations.js +221 -0
- package/src/auth/gate.js +4 -4
- package/src/auth/http.js +22 -5
- package/src/auth/service.js +52 -56
- package/src/cli.js +14 -6
- package/src/commands/ai/provision-governance.js +7 -6
- package/src/commands/init.js +25 -16
- package/src/commands/scan.js +6 -4
- package/src/commands/session.js +166 -51
- package/src/commands/spec.js +2 -2
- package/src/commands/watch.js +4 -3
- package/src/config/service.js +37 -0
- package/src/daemon/pulse.js +2 -1
- package/src/daemon/watchdog.js +117 -68
- package/src/legacy-cli.js +119 -39
- package/src/mcp/bridge-inputs.js +223 -0
- package/src/mcp/cli-command-tools.js +160 -7
- package/src/mcp/registry.js +6 -5
- package/src/mcp/session-stdio-server.js +157 -8
- package/src/mcp/smoke.js +27 -11
- package/src/mcp/token-service.js +7 -9
- package/src/memory/retrieval.js +14 -5
- package/src/pocket/ring-owner.js +13 -8
- package/src/review/ai-review.js +2 -2
- package/src/review/dd-report-email-client.js +4 -5
- package/src/session/admission-access.js +41 -10
- package/src/session/admission-auth.js +95 -16
- package/src/session/admission.js +91 -21
- package/src/session/checkpoints.js +11 -10
- package/src/session/control-events.js +2 -0
- package/src/session/file-locks.js +19 -18
- package/src/session/invitations.js +26 -19
- package/src/session/live-source.js +13 -13
- package/src/session/reactions.js +258 -0
- package/src/session/route-classes.js +107 -0
- package/src/session/sync.js +110 -66
- package/src/session/tickets.js +21 -19
- package/src/session/title-sync.js +4 -3
- package/src/telemetry/sync.js +8 -9
package/src/session/sync.js
CHANGED
|
@@ -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
|
-
...
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
2860
|
+
if (!isAuthenticated(auth)) return { ok: false, reason: "not_authenticated" };
|
|
2827
2861
|
const base = `${resolveApiBaseUrl(auth)}/api/v1/sessions/${encodeURIComponent(sid)}`;
|
|
2828
|
-
const
|
|
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",
|
|
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({
|
|
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
|
|
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
|
-
|
|
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_${
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
3477
|
+
response = await credentialedRequest(
|
|
3478
|
+
await credentialFor(session),
|
|
3432
3479
|
endpoint,
|
|
3433
|
-
{
|
|
3434
|
-
|
|
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 {
|