sentinelayer-cli 0.46.4 → 0.48.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 +63 -3
- package/package.json +12 -7
- package/skills/omar-gate-setup/SKILL.md +103 -0
- package/skills/senti-agent-identity/SKILL.md +80 -0
- package/skills/senti-judge/SKILL.md +139 -0
- package/skills/senti-room-notifications/SKILL.md +73 -0
- package/skills/senti-room-notifications/listen-wrapper.cjs +150 -0
- package/skills/senti-room-notifications/package.json +1 -0
- package/skills/senti-room-notifications/test-classifier.js +109 -0
- package/skills/senti-room-notifications/test-receipt.js +54 -0
- package/skills/sentinelayer-cli/SKILL.md +222 -0
- package/src/agents/devtestbot/runner.js +22 -11
- package/src/agents/devtestbot/tool.js +24 -11
- package/src/agents/jules/fix-cycle.js +7 -1
- package/src/agents/jules/tools/runtime-audit.js +17 -12
- package/src/agents/shared-tools/dispatch-core.js +10 -5
- package/src/ai/aidenid.js +4 -2
- package/src/ai/domain-target-store.js +11 -5
- package/src/ai/identity-store.js +11 -5
- package/src/ai/site-store.js +8 -4
- package/src/audit/orchestrator.js +21 -14
- package/src/audit/package.js +6 -4
- package/src/audit/persona-loop.js +7 -0
- package/src/audit/replay.js +4 -2
- package/src/auth/gate.js +46 -8
- package/src/auth/service.js +34 -15
- package/src/auth/session-store.js +105 -47
- package/src/board/evidence-resolvers.js +3 -4
- package/src/cli.js +23 -3
- package/src/commands/ai/identity-lifecycle.js +30 -26
- package/src/commands/ai/provision-governance.js +32 -24
- package/src/commands/ai/shared.js +6 -4
- package/src/commands/audit.js +14 -12
- package/src/commands/auth.js +24 -4
- package/src/commands/board.js +2 -2
- package/src/commands/chat.js +8 -6
- package/src/commands/judge.js +125 -0
- package/src/commands/legacy-args.js +1 -0
- package/src/commands/mcp.js +45 -52
- package/src/commands/omargate.js +2 -1
- package/src/commands/plugin.js +11 -3
- package/src/commands/review.js +9 -4
- package/src/commands/scan.js +7 -7
- package/src/commands/session.js +607 -57
- package/src/commands/skills.js +45 -0
- package/src/commands/watch.js +9 -7
- package/src/config/output-filesystem.js +156 -0
- package/src/config/schema.js +4 -2
- package/src/config/service.js +40 -4
- package/src/cost/history.js +15 -9
- package/src/daemon/artifact-lineage.js +15 -12
- package/src/daemon/assignment-ledger.js +31 -25
- package/src/daemon/ast-drift.js +7 -3
- package/src/daemon/budget-governor.js +26 -20
- package/src/daemon/error-worker.js +28 -22
- package/src/daemon/fix-cycle.js +5 -4
- package/src/daemon/hybrid-mapper.js +19 -16
- package/src/daemon/ingest-refresh.js +4 -0
- package/src/daemon/jira-lifecycle.js +21 -17
- package/src/daemon/operator-control.js +18 -15
- package/src/daemon/reliability-lane.js +18 -15
- package/src/daemon/scope-engine.js +12 -8
- package/src/daemon/watchdog.js +15 -12
- package/src/engram/build-state.js +24 -3
- package/src/engram/capability-index.js +367 -0
- package/src/engram/compile-graph.js +1063 -0
- package/src/engram/contracts/action-receipt.js +82 -0
- package/src/engram/contracts/capability-index.js +259 -0
- package/src/engram/contracts/capability.js +377 -0
- package/src/engram/contracts/content-hash.js +8 -0
- package/src/engram/contracts/evidence.js +258 -0
- package/src/engram/contracts/graph.js +594 -0
- package/src/engram/contracts/identity.js +8 -0
- package/src/engram/contracts/index.js +149 -0
- package/src/engram/contracts/media-asset.js +85 -0
- package/src/engram/contracts/money.js +249 -0
- package/src/engram/contracts/source-snapshot.js +57 -0
- package/src/engram/contracts/task-context-pack.js +114 -0
- package/src/engram/contracts/temporal-segment.js +72 -0
- package/src/engram/contracts/versions.js +96 -0
- package/src/engram/extract-html.js +692 -0
- package/src/engram/graph-store.js +877 -0
- package/src/engram/index.js +98 -14
- package/src/engram/ingest-site.js +132 -0
- package/src/engram/live-state.js +1180 -0
- package/src/engram/namespace.js +34 -1
- package/src/engram/retrieve-site.js +753 -0
- package/src/engram/safe-reader.js +597 -0
- package/src/engram/source-intake.js +251 -0
- package/src/engram/store.js +227 -9
- package/src/ingest/engine.js +21 -28
- package/src/judge/verdict.js +305 -0
- package/src/legacy-cli.js +96 -19
- package/src/mcp/bridge-inputs.js +28 -4
- package/src/mcp/cli-command-tools.js +70 -0
- package/src/mcp/registry.js +17 -13
- package/src/mcp/session-stdio-server.js +297 -19
- package/src/memory/blackboard.js +5 -4
- package/src/net/gh-api-path.js +35 -9
- package/src/net/test-egress-guard.js +16 -4
- package/src/plugin/manifest.js +12 -8
- package/src/review/ai-review.js +110 -10
- package/src/review/investor-dd-devtestbot.js +17 -6
- package/src/review/investor-dd-orchestrator.js +21 -13
- package/src/review/local-review.js +26 -16
- package/src/review/omargate-cache.js +5 -5
- package/src/review/omargate-orchestrator.js +89 -17
- package/src/review/replay.js +9 -4
- package/src/review/report.js +22 -13
- package/src/review/typesafe-severity.js +201 -0
- package/src/session/admission-auth.js +34 -7
- package/src/session/admission.js +322 -71
- package/src/session/agent-access.js +137 -0
- package/src/session/agent-registry.js +2 -1
- package/src/session/attachment-summary.js +77 -0
- package/src/session/edit-summary.js +19 -0
- package/src/session/invitations.js +65 -1
- package/src/session/mentions.js +17 -9
- package/src/session/message-edits.js +5 -4
- package/src/session/preview.js +10 -2
- package/src/session/recap.js +3 -2
- package/src/session/room-files.js +519 -0
- package/src/session/route-classes.js +22 -8
- package/src/session/setup-guides.js +4 -0
- package/src/session/standup.js +192 -0
- package/src/session/sync.js +229 -10
- package/src/session/tickets.js +41 -0
- package/src/session/transcript.js +2 -1
- package/src/session/wake/resolve-target.js +7 -3
- package/src/skills/listener-runtime.cjs +92 -0
- package/src/skills/service.js +207 -0
- package/src/swarm/factory.js +5 -5
- package/src/swarm/pentest.js +9 -9
- package/src/swarm/report.js +4 -4
- package/src/swarm/runtime.js +15 -12
- package/src/swarm/scenario-dsl.js +7 -5
- package/src/telemetry/ledger.js +16 -7
|
@@ -4,11 +4,24 @@ import { setTimeout as sleep } from "node:timers/promises";
|
|
|
4
4
|
|
|
5
5
|
import { createAgentEvent } from "../events/schema.js";
|
|
6
6
|
import {
|
|
7
|
+
approvedSelfProvisioningMcpCall,
|
|
7
8
|
buildCliCommandMcpTools,
|
|
8
9
|
createCliCommandMcpToolHandlers,
|
|
10
|
+
selfProvisioningMcpInputSchema,
|
|
9
11
|
} from "./cli-command-tools.js";
|
|
10
12
|
import { appendToStream, readStream } from "../session/stream.js";
|
|
13
|
+
import { formatAttachmentSummary, withAttachmentSummary } from "../session/attachment-summary.js";
|
|
14
|
+
import { formatEditSummary, withEditSummary } from "../session/edit-summary.js";
|
|
11
15
|
import { listFileLocks, lockFile, unlockFile } from "../session/file-locks.js";
|
|
16
|
+
import {
|
|
17
|
+
DEFAULT_ROOM_FILES_LIMIT,
|
|
18
|
+
DEFAULT_ROOM_FILES_MAX_PAGES,
|
|
19
|
+
MAX_ROOM_FILES_LIMIT,
|
|
20
|
+
MAX_ROOM_FILES_MAX_PAGES,
|
|
21
|
+
RoomFileError,
|
|
22
|
+
downloadRoomFile,
|
|
23
|
+
listRoomFiles,
|
|
24
|
+
} from "../session/room-files.js";
|
|
12
25
|
import { resolveSessionDir } from "../session/paths.js";
|
|
13
26
|
import { eventMatchesAgent } from "../session/listener.js";
|
|
14
27
|
import { isSessionControlEvent } from "../session/control-events.js";
|
|
@@ -18,6 +31,7 @@ import {
|
|
|
18
31
|
AdmissionCredentialRefused,
|
|
19
32
|
assertCanonicalAgentId,
|
|
20
33
|
canonicalAgentId,
|
|
34
|
+
currentAdmittedAgent,
|
|
21
35
|
withAgentAdmission,
|
|
22
36
|
} from "../session/admission-auth.js";
|
|
23
37
|
import { hasStoredAdmissionCredentials, storedAdmissionAgentsForSession } from "../session/admission.js";
|
|
@@ -32,14 +46,21 @@ import {
|
|
|
32
46
|
listSessionMessageActions,
|
|
33
47
|
pollSessionEvents,
|
|
34
48
|
pollSessionEventsBefore,
|
|
49
|
+
postJudgeVerdictToApi,
|
|
35
50
|
syncSessionEventToApi,
|
|
36
51
|
updateSessionReadCursor,
|
|
37
52
|
} from "../session/sync.js";
|
|
53
|
+
import { extractJudgeVerdictBlock, renderJudgeVerdictMessage, validateJudgeVerdict } from "../judge/verdict.js";
|
|
38
54
|
|
|
39
55
|
export const SESSION_MCP_SERVER_NAME = "sentinelayer-session-mcp";
|
|
40
56
|
export const SESSION_MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
41
57
|
|
|
42
58
|
const MAX_MESSAGE_CHARS = 16_000;
|
|
59
|
+
// A judge verdict carries a machine-readable block; cutting it would corrupt the verdict, so the
|
|
60
|
+
// judge tool posts whole messages up to this bound (below the server's message limit) or refuses.
|
|
61
|
+
const JUDGE_MESSAGE_MAX_CHARS = 100_000;
|
|
62
|
+
// A 404 with one of these codes is a real "not here"; any other 404 means the route is absent.
|
|
63
|
+
const JUDGE_NOT_FOUND_CODES = new Set(["SESSION_NOT_FOUND", "TARGET_NOT_FOUND"]);
|
|
43
64
|
const MAX_TOOL_LIMIT = 200;
|
|
44
65
|
const DEFAULT_TOOL_LIMIT = 50;
|
|
45
66
|
const SESSION_MCP_CONFIRM_ATTEMPTS = 3;
|
|
@@ -172,11 +193,17 @@ function eventAgentId(event = {}) {
|
|
|
172
193
|
return normalizeAgentId(event?.agent?.id || event?.agentId || event?.agent_id, "");
|
|
173
194
|
}
|
|
174
195
|
|
|
196
|
+
// A judge verdict is read whole by the coder: the numbered comments and the block are the point.
|
|
197
|
+
function carriesJudgeVerdict(payload) {
|
|
198
|
+
return isPlainObject(payload.judgeVerdict) || extractJudgeVerdictBlock(payload.message).status !== "none";
|
|
199
|
+
}
|
|
200
|
+
|
|
175
201
|
function safePayload(payload = {}) {
|
|
176
202
|
if (!isPlainObject(payload)) return {};
|
|
177
203
|
const next = { ...payload };
|
|
178
204
|
if (typeof next.message === "string") {
|
|
179
|
-
const
|
|
205
|
+
const limit = carriesJudgeVerdict(next) ? JUDGE_MESSAGE_MAX_CHARS : MAX_MESSAGE_CHARS;
|
|
206
|
+
const truncated = truncateText(next.message, limit);
|
|
180
207
|
next.message = truncated.text;
|
|
181
208
|
if (truncated.truncated) next.truncated = true;
|
|
182
209
|
}
|
|
@@ -200,6 +227,8 @@ function summarizeSessionEvent(event = {}) {
|
|
|
200
227
|
})
|
|
201
228
|
: undefined,
|
|
202
229
|
payload: safePayload(event.payload),
|
|
230
|
+
attachmentSummary: formatAttachmentSummary(event) || undefined,
|
|
231
|
+
editSummary: formatEditSummary(event) || undefined,
|
|
203
232
|
eventId: normalizeString(event.eventId),
|
|
204
233
|
idempotencyToken: normalizeString(event.idempotencyToken),
|
|
205
234
|
});
|
|
@@ -680,8 +709,9 @@ function buildSessionMessageEvent({
|
|
|
680
709
|
nowIso = new Date().toISOString(),
|
|
681
710
|
uuid = randomUUID,
|
|
682
711
|
extraPayload = {},
|
|
712
|
+
maxChars = MAX_MESSAGE_CHARS,
|
|
683
713
|
} = {}) {
|
|
684
|
-
const messageShape = truncateText(message);
|
|
714
|
+
const messageShape = truncateText(message, maxChars);
|
|
685
715
|
if (!messageShape.text) {
|
|
686
716
|
throw new Error("message is required.");
|
|
687
717
|
}
|
|
@@ -927,7 +957,7 @@ export async function isAgentContext({ env = process.env } = {}) {
|
|
|
927
957
|
*/
|
|
928
958
|
export async function runInSessionToolContext(name, input = {}, run, { env = process.env } = {}) {
|
|
929
959
|
const routeClass = sessionRouteClass(`mcp:${name}`);
|
|
930
|
-
if (routeClass === "exempt-local") return run(input);
|
|
960
|
+
if (routeClass === "exempt-local" || routeClass === "self-provisioning") return run(input);
|
|
931
961
|
if (!(await isAgentContext({ env }))) return run(input);
|
|
932
962
|
if (routeClass === "owner") refuseOutsideAdmission("owner actions are not available to agents");
|
|
933
963
|
const sessionId = normalizeString(input.sessionId || input.session_id || input.session);
|
|
@@ -968,7 +998,11 @@ export function createSessionMcpToolHandlers({
|
|
|
968
998
|
lockFileFn = lockFile,
|
|
969
999
|
unlockFileFn = unlockFile,
|
|
970
1000
|
listFileLocksFn = listFileLocks,
|
|
1001
|
+
listRoomFilesFn = listRoomFiles,
|
|
1002
|
+
downloadRoomFileFn = downloadRoomFile,
|
|
971
1003
|
syncSessionEventToApiFn = syncSessionEventToApi,
|
|
1004
|
+
postJudgeVerdictToApiFn = postJudgeVerdictToApi,
|
|
1005
|
+
currentAdmittedAgentFn = currentAdmittedAgent,
|
|
972
1006
|
appendToStreamFn = appendToStream,
|
|
973
1007
|
sleepFn = sleep,
|
|
974
1008
|
uuidFn = randomUUID,
|
|
@@ -1070,7 +1104,7 @@ export function createSessionMcpToolHandlers({
|
|
|
1070
1104
|
count: Array.isArray(actionResult.actions) ? actionResult.actions.length : 0,
|
|
1071
1105
|
}
|
|
1072
1106
|
: null,
|
|
1073
|
-
events:
|
|
1107
|
+
events: events.map((event) => includeRaw ? withEditSummary(withAttachmentSummary(event)) : summarizeSessionEvent(event)),
|
|
1074
1108
|
};
|
|
1075
1109
|
},
|
|
1076
1110
|
|
|
@@ -1163,7 +1197,7 @@ export function createSessionMcpToolHandlers({
|
|
|
1163
1197
|
count: Array.isArray(actionResult.actions) ? actionResult.actions.length : 0,
|
|
1164
1198
|
}
|
|
1165
1199
|
: null,
|
|
1166
|
-
events:
|
|
1200
|
+
events: events.map((event) => includeRaw ? withEditSummary(withAttachmentSummary(event)) : summarizeSessionEvent(event)),
|
|
1167
1201
|
};
|
|
1168
1202
|
},
|
|
1169
1203
|
|
|
@@ -1195,6 +1229,105 @@ export function createSessionMcpToolHandlers({
|
|
|
1195
1229
|
});
|
|
1196
1230
|
},
|
|
1197
1231
|
|
|
1232
|
+
async judge_verdict(input = {}) {
|
|
1233
|
+
const sessionId = requireSessionId(input, { targetPath });
|
|
1234
|
+
const agentId = requireAgentId(input);
|
|
1235
|
+
const via = normalizeString(input.via || "auto").toLowerCase();
|
|
1236
|
+
if (!["auto", "api", "room"].includes(via)) {
|
|
1237
|
+
throw new Error('via must be "auto", "api" or "room".');
|
|
1238
|
+
}
|
|
1239
|
+
const checked = validateJudgeVerdict(input.verdict);
|
|
1240
|
+
if (!checked.ok) {
|
|
1241
|
+
return { ok: false, reason: "invalid_verdict", errors: checked.errors, sessionId, agentId };
|
|
1242
|
+
}
|
|
1243
|
+
const verdict = checked.verdict;
|
|
1244
|
+
if (normalizeAgentId(verdict.coder, "") === agentId) {
|
|
1245
|
+
return { ok: false, reason: "self_judgement", sessionId, agentId, coder: verdict.coder };
|
|
1246
|
+
}
|
|
1247
|
+
const escalateTo = normalizeRecipients(input.escalateTo || input.escalate_to);
|
|
1248
|
+
// The API route mentions the room owner on escalation; naming other people needs the room message,
|
|
1249
|
+
// which carries their mentions. So an escalation to named handles never goes through the route.
|
|
1250
|
+
const namedEscalation = verdict.escalate && escalateTo.length > 0;
|
|
1251
|
+
if (namedEscalation && via === "api") {
|
|
1252
|
+
return { ok: false, reason: "escalate_to_needs_room_transport", sessionId, agentId, escalateTo };
|
|
1253
|
+
}
|
|
1254
|
+
// The route records whoever the credential is. Only this agent's own admission makes that the agent;
|
|
1255
|
+
// under the person's token the route would record the person. The room message is checked against the
|
|
1256
|
+
// agent's grant instead, so without the admission the verdict goes as a room message.
|
|
1257
|
+
const admitted = currentAdmittedAgentFn();
|
|
1258
|
+
const agentCredential = Boolean(admitted && admitted.sessionId === sessionId &&
|
|
1259
|
+
normalizeAgentId(admitted.agentId, "") === agentId);
|
|
1260
|
+
if (via === "api" && !agentCredential) {
|
|
1261
|
+
return { ok: false, reason: "api_needs_agent_credential", sessionId, agentId };
|
|
1262
|
+
}
|
|
1263
|
+
const message = renderJudgeVerdictMessage(verdict, { escalateTo });
|
|
1264
|
+
if (message.length > JUDGE_MESSAGE_MAX_CHARS) {
|
|
1265
|
+
return { ok: false, reason: "verdict_message_too_long", sessionId, agentId, limit: JUDGE_MESSAGE_MAX_CHARS };
|
|
1266
|
+
}
|
|
1267
|
+
const dryRun = Boolean(input.dryRun || input.dry_run);
|
|
1268
|
+
if (dryRun) {
|
|
1269
|
+
return { ok: true, dryRun: true, sessionId, agentId, via, verdict, message };
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
let api = null;
|
|
1273
|
+
if (via !== "room" && !namedEscalation && agentCredential) {
|
|
1274
|
+
api = await postJudgeVerdictToApiFn(sessionId, verdict, { targetPath });
|
|
1275
|
+
const apiSummary = { status: api?.status || 0, code: api?.code || "", reason: api?.reason || "" };
|
|
1276
|
+
if (api?.ok) {
|
|
1277
|
+
return { ok: true, transport: "api", sessionId, agentId, verdict, api: { ...apiSummary, body: api.body } };
|
|
1278
|
+
}
|
|
1279
|
+
// Fall back to the room post only when the route itself is absent (an older server, or
|
|
1280
|
+
// the judge loop switched off there): a refusal or a lost response is reported as is.
|
|
1281
|
+
const routeAbsent = api?.status === 404 && !JUDGE_NOT_FOUND_CODES.has(api.code);
|
|
1282
|
+
if (via === "api" || !routeAbsent) {
|
|
1283
|
+
return {
|
|
1284
|
+
ok: false,
|
|
1285
|
+
transport: "api",
|
|
1286
|
+
reason: apiSummary.code || apiSummary.reason || "api_failed",
|
|
1287
|
+
sessionId,
|
|
1288
|
+
agentId,
|
|
1289
|
+
api: { ...apiSummary, body: api?.body || null },
|
|
1290
|
+
};
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
const agent = buildAgentEnvelope(agentId, { ...input, role: normalizeString(input.role) || "reviewer" });
|
|
1295
|
+
const idempotencyKey = normalizeString(input.idempotencyKey || input.idempotency_key) ||
|
|
1296
|
+
`judge-${shortSha256(JSON.stringify([sessionId, agentId, verdict.target.type, verdict.target.ref, verdict.round]))}`;
|
|
1297
|
+
const event = buildSessionMessageEvent({
|
|
1298
|
+
event: "session_message",
|
|
1299
|
+
sessionId,
|
|
1300
|
+
agent,
|
|
1301
|
+
message,
|
|
1302
|
+
recipients: [normalizeAgentId(verdict.coder, verdict.coder)],
|
|
1303
|
+
idempotencyKey,
|
|
1304
|
+
nowIso: now(),
|
|
1305
|
+
uuid: uuidFn,
|
|
1306
|
+
maxChars: JUDGE_MESSAGE_MAX_CHARS,
|
|
1307
|
+
});
|
|
1308
|
+
const posted = await persistSessionEvent({
|
|
1309
|
+
sessionId,
|
|
1310
|
+
event,
|
|
1311
|
+
targetPath,
|
|
1312
|
+
dryRun: false,
|
|
1313
|
+
syncSessionEventToApiFn,
|
|
1314
|
+
pollSessionEventsBeforeFn,
|
|
1315
|
+
pollSessionEventsFn,
|
|
1316
|
+
appendToStreamFn,
|
|
1317
|
+
sleepFn,
|
|
1318
|
+
});
|
|
1319
|
+
return {
|
|
1320
|
+
...posted,
|
|
1321
|
+
transport: "room",
|
|
1322
|
+
...(api ? { fellBackFrom: "api_route_absent" } : {}),
|
|
1323
|
+
...(via === "auto" && namedEscalation ? { roomBecause: "escalate_to_named_handles" } : {}),
|
|
1324
|
+
...(via === "auto" && !namedEscalation && !agentCredential ? { roomBecause: "no_agent_credential" } : {}),
|
|
1325
|
+
sessionId,
|
|
1326
|
+
agentId,
|
|
1327
|
+
verdict,
|
|
1328
|
+
};
|
|
1329
|
+
},
|
|
1330
|
+
|
|
1198
1331
|
async session_action(input = {}) {
|
|
1199
1332
|
return runSessionAction({
|
|
1200
1333
|
input,
|
|
@@ -1307,6 +1440,57 @@ export function createSessionMcpToolHandlers({
|
|
|
1307
1440
|
};
|
|
1308
1441
|
},
|
|
1309
1442
|
|
|
1443
|
+
async session_files_list(input = {}) {
|
|
1444
|
+
const sessionId = requireSessionId(input, { targetPath });
|
|
1445
|
+
const result = await listRoomFilesFn(sessionId, {
|
|
1446
|
+
targetPath,
|
|
1447
|
+
limit: input.limit,
|
|
1448
|
+
maxPages: input.maxPages ?? input.max_pages,
|
|
1449
|
+
});
|
|
1450
|
+
return {
|
|
1451
|
+
ok: Boolean(result?.ok),
|
|
1452
|
+
reason: normalizeString(result?.reason),
|
|
1453
|
+
sessionId,
|
|
1454
|
+
count: Number(result?.count) || 0,
|
|
1455
|
+
complete: Boolean(result?.complete),
|
|
1456
|
+
pages: Number(result?.pages) || 0,
|
|
1457
|
+
scannedEvents: Number(result?.scannedEvents) || 0,
|
|
1458
|
+
files: Array.isArray(result?.files) ? result.files : [],
|
|
1459
|
+
};
|
|
1460
|
+
},
|
|
1461
|
+
|
|
1462
|
+
// Writes the file into this server's workspace and returns where; never the bytes.
|
|
1463
|
+
async session_file_get(input = {}) {
|
|
1464
|
+
const sessionId = requireSessionId(input, { targetPath });
|
|
1465
|
+
const attachmentId = normalizeString(input.attachmentId || input.attachment_id);
|
|
1466
|
+
try {
|
|
1467
|
+
const result = await downloadRoomFileFn(sessionId, attachmentId, {
|
|
1468
|
+
targetPath,
|
|
1469
|
+
out: normalizeString(input.out),
|
|
1470
|
+
force: Boolean(input.force),
|
|
1471
|
+
workspaceOnly: true,
|
|
1472
|
+
});
|
|
1473
|
+
return {
|
|
1474
|
+
ok: true,
|
|
1475
|
+
reason: "",
|
|
1476
|
+
sessionId,
|
|
1477
|
+
attachmentId,
|
|
1478
|
+
path: result.path,
|
|
1479
|
+
bytes: result.bytes,
|
|
1480
|
+
sha256: result.sha256,
|
|
1481
|
+
verified: Boolean(result.verified),
|
|
1482
|
+
overwritten: Boolean(result.overwritten),
|
|
1483
|
+
linkRefetched: Boolean(result.linkRefetched),
|
|
1484
|
+
attachment: result.attachment,
|
|
1485
|
+
};
|
|
1486
|
+
} catch (error) {
|
|
1487
|
+
if (error instanceof RoomFileError) {
|
|
1488
|
+
return { ok: false, reason: error.code, message: error.message, sessionId, attachmentId };
|
|
1489
|
+
}
|
|
1490
|
+
throw error;
|
|
1491
|
+
}
|
|
1492
|
+
},
|
|
1493
|
+
|
|
1310
1494
|
async attention_request(input = {}) {
|
|
1311
1495
|
const sessionId = requireSessionId(input, { targetPath });
|
|
1312
1496
|
const agentId = requireAgentId(input);
|
|
@@ -1516,6 +1700,34 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1516
1700
|
},
|
|
1517
1701
|
},
|
|
1518
1702
|
},
|
|
1703
|
+
{
|
|
1704
|
+
name: "judge_verdict",
|
|
1705
|
+
title: "Post Judge Verdict",
|
|
1706
|
+
description:
|
|
1707
|
+
"Send a judge-verdict back to the coding agent: ACCEPT, or CHANGES with numbered file:line comments (severity P0-P3, one-line fix). Validates the verdict first. When the agent acts under its own admission for the room and the server has the judge-verdicts route, the route records it; otherwise it posts a room message that mentions the coder and carries the verdict block (the server checks the agent's grant on that post). via=room always posts the message; dryRun returns the rendered message.",
|
|
1708
|
+
inputSchema: {
|
|
1709
|
+
type: "object",
|
|
1710
|
+
additionalProperties: false,
|
|
1711
|
+
required: ["sessionId", "agentId", "verdict"],
|
|
1712
|
+
properties: {
|
|
1713
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1714
|
+
agentId: { type: "string", minLength: 1 },
|
|
1715
|
+
verdict: { type: "object" },
|
|
1716
|
+
via: { type: "string", enum: ["auto", "api", "room"], default: "auto" },
|
|
1717
|
+
escalateTo: {
|
|
1718
|
+
anyOf: [
|
|
1719
|
+
{ type: "string" },
|
|
1720
|
+
{ type: "array", items: { type: "string" } },
|
|
1721
|
+
],
|
|
1722
|
+
},
|
|
1723
|
+
model: { type: "string" },
|
|
1724
|
+
role: { type: "string" },
|
|
1725
|
+
displayName: { type: "string" },
|
|
1726
|
+
idempotencyKey: { type: "string" },
|
|
1727
|
+
dryRun: { type: "boolean", default: false },
|
|
1728
|
+
},
|
|
1729
|
+
},
|
|
1730
|
+
},
|
|
1519
1731
|
{
|
|
1520
1732
|
name: "session_action",
|
|
1521
1733
|
title: "Record Senti Session Action",
|
|
@@ -1667,6 +1879,50 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1667
1879
|
},
|
|
1668
1880
|
},
|
|
1669
1881
|
},
|
|
1882
|
+
{
|
|
1883
|
+
name: "session_files_list",
|
|
1884
|
+
title: "List Senti Room Files",
|
|
1885
|
+
description:
|
|
1886
|
+
"List the files published in a session room, newest first: file name, size, content type, uploader, upload time, the message that published it, and the attachmentId that session_file_get downloads.",
|
|
1887
|
+
inputSchema: {
|
|
1888
|
+
type: "object",
|
|
1889
|
+
additionalProperties: false,
|
|
1890
|
+
required: ["sessionId"],
|
|
1891
|
+
properties: {
|
|
1892
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1893
|
+
agentId: { type: "string", minLength: 1 },
|
|
1894
|
+
limit: { type: "integer", minimum: 1, maximum: MAX_ROOM_FILES_LIMIT, default: DEFAULT_ROOM_FILES_LIMIT },
|
|
1895
|
+
maxPages: {
|
|
1896
|
+
type: "integer",
|
|
1897
|
+
minimum: 1,
|
|
1898
|
+
maximum: MAX_ROOM_FILES_MAX_PAGES,
|
|
1899
|
+
default: DEFAULT_ROOM_FILES_MAX_PAGES,
|
|
1900
|
+
description: "Most pages of room history to read, 200 events each.",
|
|
1901
|
+
},
|
|
1902
|
+
},
|
|
1903
|
+
},
|
|
1904
|
+
},
|
|
1905
|
+
{
|
|
1906
|
+
name: "session_file_get",
|
|
1907
|
+
title: "Download Senti Room File",
|
|
1908
|
+
description:
|
|
1909
|
+
"Download one published room file into the workspace: streamed to disk, checked against the server's sha256, and written atomically. Returns the saved path and the file's metadata, never its bytes. Refuses to overwrite an existing file unless force is true, and refuses an out path outside the workspace. workspaceOnly checks the destination folder when the call starts; it does not protect against another local process renaming or relinking folders inside the workspace during the download.",
|
|
1910
|
+
inputSchema: {
|
|
1911
|
+
type: "object",
|
|
1912
|
+
additionalProperties: false,
|
|
1913
|
+
required: ["sessionId", "attachmentId"],
|
|
1914
|
+
properties: {
|
|
1915
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1916
|
+
agentId: { type: "string", minLength: 1 },
|
|
1917
|
+
attachmentId: { type: "string", pattern: "^[A-Za-z0-9._:-]{8,120}$" },
|
|
1918
|
+
out: {
|
|
1919
|
+
type: "string",
|
|
1920
|
+
description: "File or directory inside the workspace; default: the server's file name in the workspace root.",
|
|
1921
|
+
},
|
|
1922
|
+
force: { type: "boolean", default: false },
|
|
1923
|
+
},
|
|
1924
|
+
},
|
|
1925
|
+
},
|
|
1670
1926
|
{
|
|
1671
1927
|
name: "attention_request",
|
|
1672
1928
|
title: "Request Senti Attention",
|
|
@@ -1698,15 +1954,26 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1698
1954
|
},
|
|
1699
1955
|
]);
|
|
1700
1956
|
|
|
1701
|
-
function toMcpTool(tool) {
|
|
1957
|
+
function toMcpTool(tool, { selfProvisioning = false } = {}) {
|
|
1958
|
+
const security = selfProvisioning
|
|
1959
|
+
? { ...tool.security, requires_human_approval: false, runtime_blocked: false }
|
|
1960
|
+
: tool.security;
|
|
1961
|
+
if (selfProvisioning) delete security.runtime_block_reason;
|
|
1962
|
+
const selfProvisioningDetail = tool.name === "sl.session.join"
|
|
1963
|
+
? "A goal requests room-owner admission approval; use wait:false to return the pending URL. Cancellation affects only this agent's pending request."
|
|
1964
|
+
: "It unregisters only this agent.";
|
|
1702
1965
|
return cleanObject({
|
|
1703
1966
|
name: tool.name,
|
|
1704
1967
|
title: tool.title,
|
|
1705
|
-
description:
|
|
1706
|
-
|
|
1968
|
+
description: selfProvisioning
|
|
1969
|
+
? `${tool.description} Only the host's caller agent may use this tool. ${selfProvisioningDetail}`
|
|
1970
|
+
: tool.description,
|
|
1971
|
+
inputSchema: selfProvisioning ? selfProvisioningMcpInputSchema(tool) : tool.inputSchema,
|
|
1707
1972
|
annotations: tool.annotations,
|
|
1708
|
-
security
|
|
1709
|
-
metadata:
|
|
1973
|
+
security,
|
|
1974
|
+
metadata: selfProvisioning
|
|
1975
|
+
? { ...tool.metadata, approvalRequired: false, approvalPolicy: "caller_bound_self_provisioning" }
|
|
1976
|
+
: tool.metadata,
|
|
1710
1977
|
});
|
|
1711
1978
|
}
|
|
1712
1979
|
|
|
@@ -1744,14 +2011,15 @@ function jsonRpcError(id, code, message, data = undefined) {
|
|
|
1744
2011
|
});
|
|
1745
2012
|
}
|
|
1746
2013
|
|
|
1747
|
-
// The dispatcher enforces each tool's published policy before its handler runs.
|
|
1748
|
-
//
|
|
1749
|
-
//
|
|
1750
|
-
// tools are not advertised by tools/list, and a call
|
|
2014
|
+
// The dispatcher enforces each tool's published policy before its handler runs. Ordinary CLI
|
|
2015
|
+
// tools still require human approval, which the bridge cannot validate. The two self-provisioning
|
|
2016
|
+
// commands have a narrowly validated caller-bound policy; every call checks its actual arguments.
|
|
2017
|
+
// Refused tools are not advertised by tools/list, and a call gets a specific JSON-RPC error.
|
|
1751
2018
|
export const MCP_HUMAN_APPROVAL_REQUIRED = Object.freeze({ code: -32001, reason: "human_approval_required" });
|
|
1752
2019
|
export const MCP_TOOL_BLOCKED = Object.freeze({ code: -32002, reason: "tool_blocked" });
|
|
1753
2020
|
|
|
1754
|
-
function toolRefusal(tool) {
|
|
2021
|
+
function toolRefusal(tool, input, { env = process.env, forListing = false } = {}) {
|
|
2022
|
+
if (approvedSelfProvisioningMcpCall(tool, input, { env, forListing })) return "";
|
|
1755
2023
|
const security = isPlainObject(tool?.security) ? tool.security : {};
|
|
1756
2024
|
if (security.requires_human_approval === true) return MCP_HUMAN_APPROVAL_REQUIRED.reason;
|
|
1757
2025
|
if (tool?.metadata?.bridge === "cli-command" && security.requires_human_approval !== false) {
|
|
@@ -1789,6 +2057,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1789
2057
|
targetPath = process.cwd(),
|
|
1790
2058
|
handlers = createSessionMcpToolHandlers({ targetPath }),
|
|
1791
2059
|
tools = SESSION_MCP_TOOLS,
|
|
2060
|
+
env = process.env,
|
|
1792
2061
|
} = {}
|
|
1793
2062
|
) {
|
|
1794
2063
|
if (!isPlainObject(message)) {
|
|
@@ -1821,7 +2090,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1821
2090
|
version: "0.20.0",
|
|
1822
2091
|
},
|
|
1823
2092
|
instructions:
|
|
1824
|
-
"Use native Senti tools for coordination actions and generated sl.* bridge tools for the rest of SentinelLayer CLI. Use poll_inbox for addressed wake-style delivery, read_history for recent/older transcript grounding, session_action/session_react/session_reply/session_lock/session_unlock/session_locks for low-noise session work, send_message for durable posts, and generated command tools such as sl.auth.status or sl.review.scan for ordinary CLI commands.",
|
|
2093
|
+
"Use native Senti tools for coordination actions and generated sl.* bridge tools for the rest of SentinelLayer CLI. Use poll_inbox for addressed wake-style delivery, read_history for recent/older transcript grounding, session_action/session_react/session_reply/session_lock/session_unlock/session_locks for low-noise session work, send_message for durable posts, judge_verdict to send a review verdict back to a coding agent, and generated command tools such as sl.auth.status or sl.review.scan for ordinary CLI commands.",
|
|
1825
2094
|
});
|
|
1826
2095
|
}
|
|
1827
2096
|
|
|
@@ -1832,7 +2101,10 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1832
2101
|
if (method === "tools/list") {
|
|
1833
2102
|
if (isNotification) return null;
|
|
1834
2103
|
return jsonRpcSuccess(id, {
|
|
1835
|
-
tools: tools.filter((tool) => !toolRefusal(tool
|
|
2104
|
+
tools: tools.filter((tool) => !toolRefusal(tool, undefined, { env, forListing: true }))
|
|
2105
|
+
.map((tool) => toMcpTool(tool, {
|
|
2106
|
+
selfProvisioning: approvedSelfProvisioningMcpCall(tool, undefined, { env, forListing: true }),
|
|
2107
|
+
})),
|
|
1836
2108
|
});
|
|
1837
2109
|
}
|
|
1838
2110
|
|
|
@@ -1842,7 +2114,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1842
2114
|
const args = isPlainObject(message.params?.arguments) ? message.params.arguments : {};
|
|
1843
2115
|
const handler = handlers[toolName];
|
|
1844
2116
|
const tool = (Array.isArray(tools) ? tools : []).find((candidate) => normalizeString(candidate?.name) === toolName);
|
|
1845
|
-
const refusal = typeof handler === "function" && tool ? toolRefusal(tool) : "";
|
|
2117
|
+
const refusal = typeof handler === "function" && tool ? toolRefusal(tool, args, { env }) : "";
|
|
1846
2118
|
if (refusal) {
|
|
1847
2119
|
return refusalError(id, toolName, tool, refusal);
|
|
1848
2120
|
}
|
|
@@ -1889,6 +2161,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1889
2161
|
|
|
1890
2162
|
export async function createSessionMcpRuntime({
|
|
1891
2163
|
targetPath = process.cwd(),
|
|
2164
|
+
env = process.env,
|
|
1892
2165
|
sessionHandlers = createSessionMcpToolHandlers({ targetPath }),
|
|
1893
2166
|
sessionTools = SESSION_MCP_TOOLS,
|
|
1894
2167
|
buildCliCommandMcpToolsFn = buildCliCommandMcpTools,
|
|
@@ -1899,9 +2172,14 @@ export async function createSessionMcpRuntime({
|
|
|
1899
2172
|
tools: [...sessionTools, ...commandTools],
|
|
1900
2173
|
handlers: {
|
|
1901
2174
|
...sessionHandlers,
|
|
1902
|
-
...createCliCommandMcpToolHandlersFn(commandTools, {
|
|
2175
|
+
...createCliCommandMcpToolHandlersFn(commandTools, {
|
|
2176
|
+
targetPath,
|
|
2177
|
+
env,
|
|
2178
|
+
approve: (tool, input) => approvedSelfProvisioningMcpCall(tool, input, { env }),
|
|
2179
|
+
}),
|
|
1903
2180
|
},
|
|
1904
2181
|
commandToolCount: commandTools.length,
|
|
2182
|
+
env,
|
|
1905
2183
|
};
|
|
1906
2184
|
}
|
|
1907
2185
|
|
package/src/memory/blackboard.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { requireOutputFileSystem } from "../config/output-filesystem.js";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
4
|
function normalizeString(value) {
|
|
@@ -274,13 +274,14 @@ export function summarizeBlackboard(blackboard) {
|
|
|
274
274
|
};
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
export async function writeBlackboardArtifact(blackboard, { outputRoot = "" } = {}) {
|
|
277
|
+
export async function writeBlackboardArtifact(blackboard, { outputRoot = "", outputFs } = {}) {
|
|
278
|
+
requireOutputFileSystem(outputFs);
|
|
278
279
|
if (!blackboard || !Array.isArray(blackboard.entries)) {
|
|
279
280
|
throw new Error("Invalid blackboard state.");
|
|
280
281
|
}
|
|
281
282
|
const resolvedOutputRoot = path.resolve(String(outputRoot || "."));
|
|
282
283
|
const memoryDirectory = path.join(resolvedOutputRoot, "memory");
|
|
283
|
-
await
|
|
284
|
+
await outputFs.mkdir(memoryDirectory, { recursive: true });
|
|
284
285
|
const runId = normalizeString(blackboard.runId) || "run";
|
|
285
286
|
const artifactPath = path.join(memoryDirectory, `blackboard-${runId}.json`);
|
|
286
287
|
const summary = summarizeBlackboard(blackboard);
|
|
@@ -293,7 +294,7 @@ export async function writeBlackboardArtifact(blackboard, { outputRoot = "" } =
|
|
|
293
294
|
entries: blackboard.entries,
|
|
294
295
|
queryEvents: blackboard.queryEvents,
|
|
295
296
|
};
|
|
296
|
-
await
|
|
297
|
+
await outputFs.writeFile(artifactPath, `${JSON.stringify(payload, null, 2)}\n`, "utf-8");
|
|
297
298
|
return {
|
|
298
299
|
artifactPath,
|
|
299
300
|
summary,
|
package/src/net/gh-api-path.js
CHANGED
|
@@ -4,11 +4,12 @@
|
|
|
4
4
|
// (repos/<owner>/<repo>/...) and passes --repo values that gh turns into paths of its own, so each
|
|
5
5
|
// value is checked for its role before any gh process starts:
|
|
6
6
|
//
|
|
7
|
-
// ghRepoSlug exactly "owner/repo". The owner starts with a letter or
|
|
8
|
-
// digits, "-" or "_" (Enterprise Managed User names end in
|
|
9
|
-
// 39 characters. The repository is letters, digits, ".",
|
|
10
|
-
// characters, and not "." or "..".
|
|
11
|
-
//
|
|
7
|
+
// ghRepoSlug exactly "owner/repo" for gh's --repo option. The owner starts with a letter or
|
|
8
|
+
// digit, then letters, digits, "-" or "_" (Enterprise Managed User names end in
|
|
9
|
+
// "_shortcode"), at most 39 characters. The repository is letters, digits, ".",
|
|
10
|
+
// "_" or "-", at most 100 characters, and not "." or "..".
|
|
11
|
+
// ghRepoApiPath the structural repos/<owner>/<repo> path, with owner and repo encoded separately.
|
|
12
|
+
// ghCommitSha 7 to 64 hexadecimal characters, encoded as one path segment (SHA-1 or SHA-256).
|
|
12
13
|
// ghRefSegment a git branch, tag or ref name under git's ref-name rules, not starting with "-",
|
|
13
14
|
// percent-encoded as ONE path segment. A branch name may contain "/", and git also
|
|
14
15
|
// allows "#", "%", "{" and other characters that mean something in a URL or to gh;
|
|
@@ -21,10 +22,18 @@
|
|
|
21
22
|
|
|
22
23
|
const OWNER = /^[A-Za-z0-9][A-Za-z0-9_-]{0,38}$/;
|
|
23
24
|
const REPO = /^[A-Za-z0-9._-]{1,100}$/;
|
|
24
|
-
const COMMIT_SHA = /^[0-9A-Fa-f]{7,
|
|
25
|
+
const COMMIT_SHA = /^[0-9A-Fa-f]{7,64}$/;
|
|
25
26
|
// Control characters, space, and the other characters git does not allow in a ref name.
|
|
26
27
|
const REF_FORBIDDEN = /[\u0000-\u0020\u007f-\u009f~^:?*[\\]/;
|
|
27
28
|
|
|
29
|
+
// encodeURIComponent intentionally leaves five RFC 3986 reserved characters alone.
|
|
30
|
+
// Encode those too so a validated value is always an opaque path segment.
|
|
31
|
+
function encodeGhPathSegment(value) {
|
|
32
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (character) =>
|
|
33
|
+
`%${character.charCodeAt(0).toString(16).toUpperCase()}`,
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
28
37
|
/** True when `value` is exactly "owner/repo" as GitHub names them. */
|
|
29
38
|
export function isGhRepoSlug(value) {
|
|
30
39
|
if (typeof value !== "string") return false;
|
|
@@ -51,12 +60,18 @@ export function ghRepoSlug(value, { label = "repo" } = {}) {
|
|
|
51
60
|
return value;
|
|
52
61
|
}
|
|
53
62
|
|
|
63
|
+
/** A `repos/<owner>/<repo>` API path with only its structural separators left raw. */
|
|
64
|
+
export function ghRepoApiPath(value, { label = "repo" } = {}) {
|
|
65
|
+
const [owner, repo] = ghRepoSlug(value, { label }).split("/");
|
|
66
|
+
return `repos/${encodeGhPathSegment(owner)}/${encodeGhPathSegment(repo)}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
/** `value` as a commit SHA path segment, or throws. */
|
|
55
70
|
export function ghCommitSha(value, { label = "sha" } = {}) {
|
|
56
71
|
if (typeof value !== "string" || !COMMIT_SHA.test(value)) {
|
|
57
|
-
throw new Error(`${label} must be a commit SHA of 7 to
|
|
72
|
+
throw new Error(`${label} must be a commit SHA of 7 to 64 hexadecimal characters.`);
|
|
58
73
|
}
|
|
59
|
-
return value;
|
|
74
|
+
return encodeGhPathSegment(value);
|
|
60
75
|
}
|
|
61
76
|
|
|
62
77
|
/** `value` (a git branch, tag or ref name) percent-encoded as one path segment, or throws. */
|
|
@@ -64,5 +79,16 @@ export function ghRefSegment(value, { label = "ref" } = {}) {
|
|
|
64
79
|
if (!isGitRefName(value)) {
|
|
65
80
|
throw new Error(`${label} must be a valid git branch, tag or ref name.`);
|
|
66
81
|
}
|
|
67
|
-
return
|
|
82
|
+
return encodeGhPathSegment(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** GitHub compare endpoint with a raw structural `...` between encoded base and head values. */
|
|
86
|
+
export function ghCompareApiPath({ repo, branch, sha }) {
|
|
87
|
+
const basehead = `${ghRefSegment(branch, { label: "branch" })}...${ghCommitSha(sha)}`;
|
|
88
|
+
return `${ghRepoApiPath(repo)}/compare/${basehead}`;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** GitHub check-runs endpoint with `ref` encoded as exactly one path segment. */
|
|
92
|
+
export function ghCheckRunsApiPath({ repo, ref }) {
|
|
93
|
+
return `${ghRepoApiPath(repo)}/commits/${ghRefSegment(ref)}/check-runs`;
|
|
68
94
|
}
|
|
@@ -103,8 +103,20 @@ export function installTestEgressGuard({ env = process.env, force = false } = {}
|
|
|
103
103
|
if (typeof nativeFetch !== "function" || typeof NativeRequest !== "function") return false;
|
|
104
104
|
if (nativeFetch[GUARD_MARKER]) return true; // idempotent
|
|
105
105
|
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
const requestOwners = new WeakMap();
|
|
107
|
+
async function dispatch(thisArg, request, ancestors = []) {
|
|
108
|
+
const response = await nativeFetch.call(thisArg, request);
|
|
109
|
+
// Undici follows cloned Requests through weak AbortController references. Keep
|
|
110
|
+
// the owners alive through body consumption, including after headers arrive.
|
|
111
|
+
const owners = [...ancestors, request];
|
|
112
|
+
requestOwners.set(response, owners);
|
|
113
|
+
if (response.body) requestOwners.set(response.body, owners);
|
|
114
|
+
return response;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function followChecked(thisArg, request, hops, ancestors = []) {
|
|
118
|
+
const owners = [...ancestors, request];
|
|
119
|
+
const response = await dispatch(thisArg, new NativeRequest(request, { redirect: "manual" }), owners);
|
|
108
120
|
const location = response.headers.get("location");
|
|
109
121
|
if (!REDIRECT_STATUSES.has(response.status) || location === null) return response;
|
|
110
122
|
let next;
|
|
@@ -134,7 +146,7 @@ export function installTestEgressGuard({ env = process.env, force = false } = {}
|
|
|
134
146
|
signal: request.signal,
|
|
135
147
|
redirect: "manual",
|
|
136
148
|
});
|
|
137
|
-
return followChecked(thisArg, nextRequest, hops + 1);
|
|
149
|
+
return followChecked(thisArg, nextRequest, hops + 1, owners);
|
|
138
150
|
}
|
|
139
151
|
|
|
140
152
|
const guarded = function guardedTestFetch(input, init) {
|
|
@@ -145,7 +157,7 @@ export function installTestEgressGuard({ env = process.env, force = false } = {}
|
|
|
145
157
|
return Promise.reject(error); // the same TypeError native fetch would raise
|
|
146
158
|
}
|
|
147
159
|
if (!isAllowedTestDestination(request.url)) return Promise.reject(block(request.url));
|
|
148
|
-
if (request.redirect !== "follow") return
|
|
160
|
+
if (request.redirect !== "follow") return dispatch(this, request);
|
|
149
161
|
return followChecked(this, request, 0);
|
|
150
162
|
};
|
|
151
163
|
Object.defineProperty(guarded, GUARD_MARKER, { value: true });
|