sentinelayer-cli 0.30.12 → 0.30.14
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 +5 -0
- package/package.json +1 -1
- package/src/commands/mcp.js +34 -0
- package/src/commands/session.js +89 -19
- package/src/mcp/cli-command-tools.js +506 -0
- package/src/mcp/cli-registry.js +231 -0
- package/src/mcp/registry.js +260 -0
- package/src/mcp/session-stdio-server.js +436 -6
- package/src/review/persona-prompts.js +8 -0
- package/src/session/daemon-spawn.js +35 -15
- package/src/session/listener.js +44 -2
- package/src/session/rotating-log.js +181 -0
- package/src/session/sync.js +7 -1
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
4
4
|
|
|
5
5
|
import { createAgentEvent } from "../events/schema.js";
|
|
6
|
+
import {
|
|
7
|
+
buildCliCommandMcpTools,
|
|
8
|
+
createCliCommandMcpToolHandlers,
|
|
9
|
+
} from "./cli-command-tools.js";
|
|
6
10
|
import { appendToStream } from "../session/stream.js";
|
|
11
|
+
import { listFileLocks, lockFile, unlockFile } from "../session/file-locks.js";
|
|
7
12
|
import { eventMatchesAgent } from "../session/listener.js";
|
|
8
13
|
import { isSessionControlEvent } from "../session/control-events.js";
|
|
9
14
|
import {
|
|
15
|
+
createSessionMessageAction,
|
|
10
16
|
listSessionMessageActions,
|
|
11
17
|
pollSessionEvents,
|
|
12
18
|
pollSessionEventsBefore,
|
|
@@ -27,6 +33,16 @@ const SESSION_MCP_CONFIRM_TOTAL_TIMEOUT_MS = 10_000;
|
|
|
27
33
|
const SESSION_MCP_CONFIRM_REQUEST_TIMEOUT_MS = 2_000;
|
|
28
34
|
const JSON_RPC_VERSION = "2.0";
|
|
29
35
|
const CONTENT_LENGTH_PREFIX = "content-length:";
|
|
36
|
+
const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
37
|
+
"ack",
|
|
38
|
+
"working_on",
|
|
39
|
+
"reply",
|
|
40
|
+
"like",
|
|
41
|
+
"dislike",
|
|
42
|
+
"disregard",
|
|
43
|
+
"view",
|
|
44
|
+
]);
|
|
45
|
+
const SESSION_MESSAGE_ACTION_ALIASES = new Map([["comment", "reply"]]);
|
|
30
46
|
|
|
31
47
|
function normalizeString(value) {
|
|
32
48
|
return String(value == null ? "" : value).trim();
|
|
@@ -51,6 +67,17 @@ function normalizePositiveInteger(value, fallbackValue) {
|
|
|
51
67
|
return Math.floor(normalized);
|
|
52
68
|
}
|
|
53
69
|
|
|
70
|
+
function normalizeOptionalPositiveInteger(value, fieldName) {
|
|
71
|
+
if (value === undefined || value === null || String(value).trim() === "") {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
const normalized = Number(value);
|
|
75
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
76
|
+
throw new Error(`${fieldName} must be a positive integer.`);
|
|
77
|
+
}
|
|
78
|
+
return Math.floor(normalized);
|
|
79
|
+
}
|
|
80
|
+
|
|
54
81
|
function confirmationRemainingMs(deadlineMs, nowMs) {
|
|
55
82
|
const now = Number(nowMs()) || Date.now();
|
|
56
83
|
return Math.max(0, Math.floor(deadlineMs - now));
|
|
@@ -85,6 +112,22 @@ function normalizeRecipients(value) {
|
|
|
85
112
|
return recipients;
|
|
86
113
|
}
|
|
87
114
|
|
|
115
|
+
function normalizeFileList(value) {
|
|
116
|
+
const items = Array.isArray(value) ? value : String(value || "").split(/[\n,]+/g);
|
|
117
|
+
const seen = new Set();
|
|
118
|
+
const files = [];
|
|
119
|
+
for (const item of items) {
|
|
120
|
+
const file = normalizeString(item);
|
|
121
|
+
if (!file || seen.has(file)) continue;
|
|
122
|
+
seen.add(file);
|
|
123
|
+
files.push(file);
|
|
124
|
+
}
|
|
125
|
+
if (files.length === 0) {
|
|
126
|
+
throw new Error("files must include at least one path.");
|
|
127
|
+
}
|
|
128
|
+
return files;
|
|
129
|
+
}
|
|
130
|
+
|
|
88
131
|
function isPlainObject(value) {
|
|
89
132
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
90
133
|
}
|
|
@@ -99,6 +142,10 @@ function cleanObject(record = {}) {
|
|
|
99
142
|
return cleaned;
|
|
100
143
|
}
|
|
101
144
|
|
|
145
|
+
function shortSha256(value) {
|
|
146
|
+
return createHash("sha256").update(String(value || "")).digest("hex").slice(0, 32);
|
|
147
|
+
}
|
|
148
|
+
|
|
102
149
|
function eventAgentId(event = {}) {
|
|
103
150
|
return normalizeAgentId(event?.agent?.id || event?.agentId || event?.agent_id, "");
|
|
104
151
|
}
|
|
@@ -413,6 +460,51 @@ function requireAgentId(input = {}) {
|
|
|
413
460
|
return agentId;
|
|
414
461
|
}
|
|
415
462
|
|
|
463
|
+
function normalizeSessionMessageActionType(value) {
|
|
464
|
+
const raw = normalizeString(value).toLowerCase();
|
|
465
|
+
const normalized = SESSION_MESSAGE_ACTION_ALIASES.get(raw) || raw;
|
|
466
|
+
if (!SESSION_MESSAGE_ACTION_TYPES.has(normalized)) {
|
|
467
|
+
throw new Error(
|
|
468
|
+
`actionType must be one of: ${[...SESSION_MESSAGE_ACTION_TYPES].join(", ")}; aliases: comment=reply.`,
|
|
469
|
+
);
|
|
470
|
+
}
|
|
471
|
+
return normalized;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function requireSessionActionTarget(input = {}) {
|
|
475
|
+
const targetSequenceId = normalizeOptionalPositiveInteger(
|
|
476
|
+
input.targetSequenceId || input.target_sequence_id || input.sequenceId || input.sequence_id,
|
|
477
|
+
"targetSequenceId",
|
|
478
|
+
);
|
|
479
|
+
const targetCursor = normalizeString(input.targetCursor || input.target_cursor || input.cursor);
|
|
480
|
+
const targetActionId = normalizeString(input.targetActionId || input.target_action_id || input.actionId);
|
|
481
|
+
if (!targetSequenceId && !targetCursor && !targetActionId) {
|
|
482
|
+
throw new Error("Provide targetSequenceId, targetCursor, or targetActionId.");
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
targetSequenceId,
|
|
486
|
+
targetCursor,
|
|
487
|
+
targetActionId,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function defaultActionIdempotencyKey({
|
|
492
|
+
actionType,
|
|
493
|
+
targetSequenceId = null,
|
|
494
|
+
targetCursor = "",
|
|
495
|
+
targetActionId = "",
|
|
496
|
+
note = "",
|
|
497
|
+
agentId = "",
|
|
498
|
+
} = {}) {
|
|
499
|
+
const target = targetSequenceId
|
|
500
|
+
? `seq:${targetSequenceId}`
|
|
501
|
+
: targetActionId
|
|
502
|
+
? `action:${targetActionId}`
|
|
503
|
+
: `cursor:${normalizeString(targetCursor)}`;
|
|
504
|
+
const actor = normalizeAgentId(agentId, "unknown");
|
|
505
|
+
return `mcp:${normalizeString(actionType).toLowerCase()}:${target}:${actor}:${shortSha256(note)}`;
|
|
506
|
+
}
|
|
507
|
+
|
|
416
508
|
function buildAgentEnvelope(agentId, input = {}) {
|
|
417
509
|
return cleanObject({
|
|
418
510
|
id: agentId,
|
|
@@ -463,6 +555,67 @@ function buildSessionMessageEvent({
|
|
|
463
555
|
});
|
|
464
556
|
}
|
|
465
557
|
|
|
558
|
+
async function runSessionAction({
|
|
559
|
+
input = {},
|
|
560
|
+
actionType,
|
|
561
|
+
note = "",
|
|
562
|
+
targetPath,
|
|
563
|
+
createSessionMessageActionFn,
|
|
564
|
+
} = {}) {
|
|
565
|
+
const sessionId = requireSessionId(input);
|
|
566
|
+
const agentId = requireAgentId(input);
|
|
567
|
+
const normalizedActionType = normalizeSessionMessageActionType(actionType || input.actionType || input.action_type);
|
|
568
|
+
const target = requireSessionActionTarget(input);
|
|
569
|
+
const normalizedNote = truncateText(note || input.note || input.message || input.text || "").text;
|
|
570
|
+
const idempotencyKey =
|
|
571
|
+
normalizeString(input.idempotencyKey || input.idempotency_key) ||
|
|
572
|
+
defaultActionIdempotencyKey({
|
|
573
|
+
actionType: normalizedActionType,
|
|
574
|
+
...target,
|
|
575
|
+
note: normalizedNote,
|
|
576
|
+
agentId,
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
if (Boolean(input.dryRun || input.dry_run)) {
|
|
580
|
+
return {
|
|
581
|
+
ok: true,
|
|
582
|
+
dryRun: true,
|
|
583
|
+
sessionId,
|
|
584
|
+
agentId,
|
|
585
|
+
actionType: normalizedActionType,
|
|
586
|
+
...target,
|
|
587
|
+
note: normalizedNote,
|
|
588
|
+
idempotencyKey,
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const result = await createSessionMessageActionFn(sessionId, {
|
|
593
|
+
actionType: normalizedActionType,
|
|
594
|
+
targetPath,
|
|
595
|
+
...target,
|
|
596
|
+
note: normalizedNote,
|
|
597
|
+
metadata: {
|
|
598
|
+
source: "mcp",
|
|
599
|
+
agentId,
|
|
600
|
+
},
|
|
601
|
+
idempotencyKey,
|
|
602
|
+
timeoutMs: normalizePositiveInteger(input.timeoutMs || input.timeout_ms, 15_000),
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
return {
|
|
606
|
+
ok: Boolean(result?.ok),
|
|
607
|
+
reason: normalizeString(result?.reason),
|
|
608
|
+
duplicate: Boolean(result?.duplicate),
|
|
609
|
+
sessionId,
|
|
610
|
+
agentId,
|
|
611
|
+
actionType: normalizedActionType,
|
|
612
|
+
...target,
|
|
613
|
+
note: normalizedNote,
|
|
614
|
+
idempotencyKey,
|
|
615
|
+
action: result?.action || null,
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
|
|
466
619
|
async function persistSessionEvent({
|
|
467
620
|
sessionId,
|
|
468
621
|
event,
|
|
@@ -576,6 +729,10 @@ export function createSessionMcpToolHandlers({
|
|
|
576
729
|
pollSessionEventsFn = pollSessionEvents,
|
|
577
730
|
pollSessionEventsBeforeFn = pollSessionEventsBefore,
|
|
578
731
|
listSessionMessageActionsFn = listSessionMessageActions,
|
|
732
|
+
createSessionMessageActionFn = createSessionMessageAction,
|
|
733
|
+
lockFileFn = lockFile,
|
|
734
|
+
unlockFileFn = unlockFile,
|
|
735
|
+
listFileLocksFn = listFileLocks,
|
|
579
736
|
syncSessionEventToApiFn = syncSessionEventToApi,
|
|
580
737
|
appendToStreamFn = appendToStream,
|
|
581
738
|
sleepFn = sleep,
|
|
@@ -678,6 +835,123 @@ export function createSessionMcpToolHandlers({
|
|
|
678
835
|
});
|
|
679
836
|
},
|
|
680
837
|
|
|
838
|
+
async session_action(input = {}) {
|
|
839
|
+
return runSessionAction({
|
|
840
|
+
input,
|
|
841
|
+
targetPath,
|
|
842
|
+
createSessionMessageActionFn,
|
|
843
|
+
});
|
|
844
|
+
},
|
|
845
|
+
|
|
846
|
+
async session_react(input = {}) {
|
|
847
|
+
const reaction = normalizeSessionMessageActionType(input.reaction || input.actionType || input.action_type);
|
|
848
|
+
if (!["ack", "like", "dislike"].includes(reaction)) {
|
|
849
|
+
throw new Error("reaction must be one of: ack, like, dislike.");
|
|
850
|
+
}
|
|
851
|
+
return runSessionAction({
|
|
852
|
+
input,
|
|
853
|
+
actionType: reaction,
|
|
854
|
+
targetPath,
|
|
855
|
+
createSessionMessageActionFn,
|
|
856
|
+
});
|
|
857
|
+
},
|
|
858
|
+
|
|
859
|
+
async session_reply(input = {}) {
|
|
860
|
+
return runSessionAction({
|
|
861
|
+
input,
|
|
862
|
+
actionType: "reply",
|
|
863
|
+
note: input.message || input.text || input.note,
|
|
864
|
+
targetPath,
|
|
865
|
+
createSessionMessageActionFn,
|
|
866
|
+
});
|
|
867
|
+
},
|
|
868
|
+
|
|
869
|
+
async session_lock(input = {}) {
|
|
870
|
+
const sessionId = requireSessionId(input);
|
|
871
|
+
const agentId = requireAgentId(input);
|
|
872
|
+
const files = normalizeFileList(input.files || input.file || input.paths);
|
|
873
|
+
const intent = normalizeString(input.intent || input.reason);
|
|
874
|
+
const ttlSeconds = normalizePositiveInteger(input.ttlSeconds || input.ttl_seconds, 300);
|
|
875
|
+
const results = [];
|
|
876
|
+
for (const file of files) {
|
|
877
|
+
const result = await lockFileFn(sessionId, agentId, file, {
|
|
878
|
+
intent,
|
|
879
|
+
ttlSeconds,
|
|
880
|
+
targetPath,
|
|
881
|
+
syncRemote: input.syncRemote !== false && input.sync_remote !== false,
|
|
882
|
+
awaitRemoteSync: input.awaitRemoteSync !== false && input.await_remote_sync !== false,
|
|
883
|
+
});
|
|
884
|
+
results.push({
|
|
885
|
+
file: normalizeString(result?.file) || file,
|
|
886
|
+
locked: Boolean(result?.locked),
|
|
887
|
+
reason: normalizeString(result?.reason) || (result?.locked ? "locked" : "held_by_other_agent"),
|
|
888
|
+
heldBy: normalizeString(result?.heldBy) || null,
|
|
889
|
+
since: normalizeString(result?.since) || null,
|
|
890
|
+
lock: result?.lock || null,
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
const failed = results.filter((result) => !result.locked);
|
|
894
|
+
return {
|
|
895
|
+
ok: failed.length === 0,
|
|
896
|
+
reason: failed.length === 0 ? "" : "lock_conflict",
|
|
897
|
+
sessionId,
|
|
898
|
+
agentId,
|
|
899
|
+
lockedCount: results.length - failed.length,
|
|
900
|
+
failedCount: failed.length,
|
|
901
|
+
results,
|
|
902
|
+
};
|
|
903
|
+
},
|
|
904
|
+
|
|
905
|
+
async session_unlock(input = {}) {
|
|
906
|
+
const sessionId = requireSessionId(input);
|
|
907
|
+
const agentId = requireAgentId(input);
|
|
908
|
+
const files = normalizeFileList(input.files || input.file || input.paths);
|
|
909
|
+
const reason = normalizeString(input.reason || input.intent) || "manual_release";
|
|
910
|
+
const results = [];
|
|
911
|
+
for (const file of files) {
|
|
912
|
+
const result = await unlockFileFn(sessionId, agentId, file, {
|
|
913
|
+
reason,
|
|
914
|
+
force: false,
|
|
915
|
+
targetPath,
|
|
916
|
+
syncRemote: input.syncRemote !== false && input.sync_remote !== false,
|
|
917
|
+
awaitRemoteSync: input.awaitRemoteSync !== false && input.await_remote_sync !== false,
|
|
918
|
+
});
|
|
919
|
+
results.push({
|
|
920
|
+
file: normalizeString(result?.file) || file,
|
|
921
|
+
unlocked: Boolean(result?.unlocked),
|
|
922
|
+
reason: normalizeString(result?.reason) || (result?.unlocked ? "unlocked" : "not_locked"),
|
|
923
|
+
heldBy: normalizeString(result?.heldBy) || null,
|
|
924
|
+
since: normalizeString(result?.since) || null,
|
|
925
|
+
lock: result?.lock || null,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
const failed = results.filter((result) => !result.unlocked && result.reason !== "not_locked");
|
|
929
|
+
return {
|
|
930
|
+
ok: failed.length === 0,
|
|
931
|
+
reason: failed.length === 0 ? "" : "unlock_failed",
|
|
932
|
+
sessionId,
|
|
933
|
+
agentId,
|
|
934
|
+
unlockedCount: results.filter((result) => result.unlocked).length,
|
|
935
|
+
skippedCount: results.filter((result) => !result.unlocked && result.reason === "not_locked").length,
|
|
936
|
+
failedCount: failed.length,
|
|
937
|
+
results,
|
|
938
|
+
};
|
|
939
|
+
},
|
|
940
|
+
|
|
941
|
+
async session_locks(input = {}) {
|
|
942
|
+
const sessionId = requireSessionId(input);
|
|
943
|
+
const locks = await listFileLocksFn(sessionId, {
|
|
944
|
+
targetPath,
|
|
945
|
+
});
|
|
946
|
+
return {
|
|
947
|
+
ok: true,
|
|
948
|
+
reason: "",
|
|
949
|
+
sessionId,
|
|
950
|
+
lockCount: Array.isArray(locks) ? locks.length : 0,
|
|
951
|
+
locks: Array.isArray(locks) ? locks : [],
|
|
952
|
+
};
|
|
953
|
+
},
|
|
954
|
+
|
|
681
955
|
async attention_request(input = {}) {
|
|
682
956
|
const sessionId = requireSessionId(input);
|
|
683
957
|
const agentId = requireAgentId(input);
|
|
@@ -766,6 +1040,136 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
766
1040
|
},
|
|
767
1041
|
},
|
|
768
1042
|
},
|
|
1043
|
+
{
|
|
1044
|
+
name: "session_action",
|
|
1045
|
+
title: "Record Senti Session Action",
|
|
1046
|
+
description:
|
|
1047
|
+
"Record a low-noise message action such as ack, working_on, disregard, view, like, dislike, or reply against a target session event.",
|
|
1048
|
+
inputSchema: {
|
|
1049
|
+
type: "object",
|
|
1050
|
+
additionalProperties: false,
|
|
1051
|
+
required: ["sessionId", "agentId", "actionType"],
|
|
1052
|
+
properties: {
|
|
1053
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1054
|
+
agentId: { type: "string", minLength: 1 },
|
|
1055
|
+
actionType: { type: "string", enum: [...SESSION_MESSAGE_ACTION_TYPES] },
|
|
1056
|
+
targetSequenceId: { type: "integer", minimum: 1 },
|
|
1057
|
+
targetCursor: { type: "string" },
|
|
1058
|
+
targetActionId: { type: "string" },
|
|
1059
|
+
note: { type: "string", maxLength: MAX_MESSAGE_CHARS },
|
|
1060
|
+
idempotencyKey: { type: "string" },
|
|
1061
|
+
timeoutMs: { type: "integer", minimum: 1, default: 15000 },
|
|
1062
|
+
dryRun: { type: "boolean", default: false },
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: "session_react",
|
|
1068
|
+
title: "React To Senti Message",
|
|
1069
|
+
description:
|
|
1070
|
+
"Acknowledge or react to a target session event with ack, like, or dislike.",
|
|
1071
|
+
inputSchema: {
|
|
1072
|
+
type: "object",
|
|
1073
|
+
additionalProperties: false,
|
|
1074
|
+
required: ["sessionId", "agentId", "reaction"],
|
|
1075
|
+
properties: {
|
|
1076
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1077
|
+
agentId: { type: "string", minLength: 1 },
|
|
1078
|
+
reaction: { type: "string", enum: ["ack", "like", "dislike"] },
|
|
1079
|
+
targetSequenceId: { type: "integer", minimum: 1 },
|
|
1080
|
+
targetCursor: { type: "string" },
|
|
1081
|
+
targetActionId: { type: "string" },
|
|
1082
|
+
idempotencyKey: { type: "string" },
|
|
1083
|
+
timeoutMs: { type: "integer", minimum: 1, default: 15000 },
|
|
1084
|
+
dryRun: { type: "boolean", default: false },
|
|
1085
|
+
},
|
|
1086
|
+
},
|
|
1087
|
+
},
|
|
1088
|
+
{
|
|
1089
|
+
name: "session_reply",
|
|
1090
|
+
title: "Reply In Senti Thread",
|
|
1091
|
+
description:
|
|
1092
|
+
"Add a threaded reply/comment under a specific session event using the session action channel.",
|
|
1093
|
+
inputSchema: {
|
|
1094
|
+
type: "object",
|
|
1095
|
+
additionalProperties: false,
|
|
1096
|
+
required: ["sessionId", "agentId", "targetSequenceId", "message"],
|
|
1097
|
+
properties: {
|
|
1098
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1099
|
+
agentId: { type: "string", minLength: 1 },
|
|
1100
|
+
targetSequenceId: { type: "integer", minimum: 1 },
|
|
1101
|
+
targetCursor: { type: "string" },
|
|
1102
|
+
targetActionId: { type: "string" },
|
|
1103
|
+
message: { type: "string", minLength: 1, maxLength: MAX_MESSAGE_CHARS },
|
|
1104
|
+
idempotencyKey: { type: "string" },
|
|
1105
|
+
timeoutMs: { type: "integer", minimum: 1, default: 15000 },
|
|
1106
|
+
dryRun: { type: "boolean", default: false },
|
|
1107
|
+
},
|
|
1108
|
+
},
|
|
1109
|
+
},
|
|
1110
|
+
{
|
|
1111
|
+
name: "session_lock",
|
|
1112
|
+
title: "Lock Senti Files",
|
|
1113
|
+
description:
|
|
1114
|
+
"Claim session-scoped file locks before editing files, using the same fail-closed lock registry as the CLI.",
|
|
1115
|
+
inputSchema: {
|
|
1116
|
+
type: "object",
|
|
1117
|
+
additionalProperties: false,
|
|
1118
|
+
required: ["sessionId", "agentId", "files"],
|
|
1119
|
+
properties: {
|
|
1120
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1121
|
+
agentId: { type: "string", minLength: 1 },
|
|
1122
|
+
files: {
|
|
1123
|
+
anyOf: [
|
|
1124
|
+
{ type: "string" },
|
|
1125
|
+
{ type: "array", items: { type: "string" }, minItems: 1 },
|
|
1126
|
+
],
|
|
1127
|
+
},
|
|
1128
|
+
intent: { type: "string" },
|
|
1129
|
+
ttlSeconds: { type: "integer", minimum: 1, default: 300 },
|
|
1130
|
+
syncRemote: { type: "boolean", default: true },
|
|
1131
|
+
awaitRemoteSync: { type: "boolean", default: true },
|
|
1132
|
+
},
|
|
1133
|
+
},
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
name: "session_unlock",
|
|
1137
|
+
title: "Unlock Senti Files",
|
|
1138
|
+
description:
|
|
1139
|
+
"Release session-scoped file locks held by an agent.",
|
|
1140
|
+
inputSchema: {
|
|
1141
|
+
type: "object",
|
|
1142
|
+
additionalProperties: false,
|
|
1143
|
+
required: ["sessionId", "agentId", "files"],
|
|
1144
|
+
properties: {
|
|
1145
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1146
|
+
agentId: { type: "string", minLength: 1 },
|
|
1147
|
+
files: {
|
|
1148
|
+
anyOf: [
|
|
1149
|
+
{ type: "string" },
|
|
1150
|
+
{ type: "array", items: { type: "string" }, minItems: 1 },
|
|
1151
|
+
],
|
|
1152
|
+
},
|
|
1153
|
+
reason: { type: "string" },
|
|
1154
|
+
syncRemote: { type: "boolean", default: true },
|
|
1155
|
+
awaitRemoteSync: { type: "boolean", default: true },
|
|
1156
|
+
},
|
|
1157
|
+
},
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
name: "session_locks",
|
|
1161
|
+
title: "List Senti File Locks",
|
|
1162
|
+
description:
|
|
1163
|
+
"List active file locks for a session.",
|
|
1164
|
+
inputSchema: {
|
|
1165
|
+
type: "object",
|
|
1166
|
+
additionalProperties: false,
|
|
1167
|
+
required: ["sessionId"],
|
|
1168
|
+
properties: {
|
|
1169
|
+
sessionId: { type: "string", minLength: 1 },
|
|
1170
|
+
},
|
|
1171
|
+
},
|
|
1172
|
+
},
|
|
769
1173
|
{
|
|
770
1174
|
name: "attention_request",
|
|
771
1175
|
title: "Request Senti Attention",
|
|
@@ -798,12 +1202,15 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
798
1202
|
]);
|
|
799
1203
|
|
|
800
1204
|
function toMcpTool(tool) {
|
|
801
|
-
return {
|
|
1205
|
+
return cleanObject({
|
|
802
1206
|
name: tool.name,
|
|
803
1207
|
title: tool.title,
|
|
804
1208
|
description: tool.description,
|
|
805
1209
|
inputSchema: tool.inputSchema,
|
|
806
|
-
|
|
1210
|
+
annotations: tool.annotations,
|
|
1211
|
+
security: tool.security,
|
|
1212
|
+
metadata: tool.metadata,
|
|
1213
|
+
});
|
|
807
1214
|
}
|
|
808
1215
|
|
|
809
1216
|
function buildToolResult(payload, { isError = false } = {}) {
|
|
@@ -878,7 +1285,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
878
1285
|
version: "0.20.0",
|
|
879
1286
|
},
|
|
880
1287
|
instructions:
|
|
881
|
-
"Use
|
|
1288
|
+
"Use native Senti tools for coordination actions and generated sl.* bridge tools for the rest of SentinelLayer CLI. Poll first with poll_inbox, use session_action/session_react/session_reply/session_lock/session_unlock/session_locks for low-noise session work, use send_message for durable posts, and use generated command tools such as sl.auth.status or sl.review.scan for ordinary CLI commands.",
|
|
882
1289
|
});
|
|
883
1290
|
}
|
|
884
1291
|
|
|
@@ -932,6 +1339,24 @@ export async function handleMcpJsonRpcMessage(
|
|
|
932
1339
|
return isNotification ? null : jsonRpcError(id, -32601, "Method not found");
|
|
933
1340
|
}
|
|
934
1341
|
|
|
1342
|
+
export async function createSessionMcpRuntime({
|
|
1343
|
+
targetPath = process.cwd(),
|
|
1344
|
+
sessionHandlers = createSessionMcpToolHandlers({ targetPath }),
|
|
1345
|
+
sessionTools = SESSION_MCP_TOOLS,
|
|
1346
|
+
buildCliCommandMcpToolsFn = buildCliCommandMcpTools,
|
|
1347
|
+
createCliCommandMcpToolHandlersFn = createCliCommandMcpToolHandlers,
|
|
1348
|
+
} = {}) {
|
|
1349
|
+
const commandTools = await buildCliCommandMcpToolsFn();
|
|
1350
|
+
return {
|
|
1351
|
+
tools: [...sessionTools, ...commandTools],
|
|
1352
|
+
handlers: {
|
|
1353
|
+
...sessionHandlers,
|
|
1354
|
+
...createCliCommandMcpToolHandlersFn(commandTools, { targetPath }),
|
|
1355
|
+
},
|
|
1356
|
+
commandToolCount: commandTools.length,
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
|
|
935
1360
|
function findHeaderSeparator(buffer) {
|
|
936
1361
|
const crlf = buffer.indexOf("\r\n\r\n");
|
|
937
1362
|
if (crlf >= 0) {
|
|
@@ -1001,9 +1426,14 @@ export async function runMcpStdioServer({
|
|
|
1001
1426
|
stderr = process.stderr,
|
|
1002
1427
|
targetPath = process.cwd(),
|
|
1003
1428
|
framing = "newline",
|
|
1004
|
-
handlers =
|
|
1005
|
-
tools =
|
|
1429
|
+
handlers = null,
|
|
1430
|
+
tools = null,
|
|
1006
1431
|
} = {}) {
|
|
1432
|
+
if (!handlers || !tools) {
|
|
1433
|
+
const runtime = await createSessionMcpRuntime({ targetPath });
|
|
1434
|
+
handlers = handlers || runtime.handlers;
|
|
1435
|
+
tools = tools || runtime.tools;
|
|
1436
|
+
}
|
|
1007
1437
|
let buffer = Buffer.alloc(0);
|
|
1008
1438
|
let chain = Promise.resolve();
|
|
1009
1439
|
|
|
@@ -65,6 +65,14 @@ Focus areas:
|
|
|
65
65
|
- CORS misconfiguration allowing unauthorized origins
|
|
66
66
|
- Insecure deserialization and dynamic code execution (eval, Function)
|
|
67
67
|
|
|
68
|
+
MCP / agent-security (clearance-layer specific):
|
|
69
|
+
- Identity or tenant derived from a model/tool ARGUMENT instead of a validated token claim (confused-deputy: tool args are prompt-injectable — authorize on the verified token sub/claims, never a passed-in user_id/tenant)
|
|
70
|
+
- Hosted MCP server passing the inbound client token through to an upstream API instead of a token exchange (RFC 8693 on-behalf-of); token forwarded beyond its audience
|
|
71
|
+
- MCP resource server not validating token AUDIENCE (RFC 8707): accepts a bearer without checking aud == this server, or no PKCE on the OAuth client
|
|
72
|
+
- Agent sandbox / microVM handed a long-lived or user credential instead of a short-lived, narrowly-scoped, per-session token; secrets injected via env or baked into a VM image/snapshot
|
|
73
|
+
- Sandbox egress not default-denied, or the cloud metadata endpoint (169.254.169.254 / IMDS) reachable from guest code
|
|
74
|
+
- VM memory snapshot reused across sessions or tenants (entropy/RNG/credential bleed)
|
|
75
|
+
|
|
68
76
|
Evidence standard: Every finding MUST include file:line, exploit scenario, and remediation.
|
|
69
77
|
Do NOT report hypothetical issues without concrete code evidence.`,
|
|
70
78
|
},
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import fs from "node:fs";
|
|
3
2
|
import fsp from "node:fs/promises";
|
|
4
3
|
import path from "node:path";
|
|
5
4
|
import process from "node:process";
|
|
6
5
|
|
|
7
6
|
import { resolveSessionPaths } from "./paths.js";
|
|
7
|
+
import {
|
|
8
|
+
appendRotatingLogLine,
|
|
9
|
+
normalizeRotatingLogOptions,
|
|
10
|
+
} from "./rotating-log.js";
|
|
8
11
|
|
|
9
12
|
const PID_FILE_NAME = "senti-daemon.json";
|
|
10
13
|
const LOG_FILE_NAME = "senti-daemon.log";
|
|
@@ -132,6 +135,8 @@ export async function spawnDetachedSentiDaemon({
|
|
|
132
135
|
targetPath = process.cwd(),
|
|
133
136
|
cliPath = "",
|
|
134
137
|
env = process.env,
|
|
138
|
+
logMaxBytes = env.SENTINELAYER_SENTI_LOG_MAX_BYTES,
|
|
139
|
+
logMaxFiles = env.SENTINELAYER_SENTI_LOG_MAX_FILES,
|
|
135
140
|
} = {}) {
|
|
136
141
|
const normalizedSessionId = normalizeString(sessionId);
|
|
137
142
|
if (!normalizedSessionId) {
|
|
@@ -157,22 +162,45 @@ export async function spawnDetachedSentiDaemon({
|
|
|
157
162
|
}
|
|
158
163
|
|
|
159
164
|
const logPath = resolveDaemonLogPath(normalizedSessionId, { targetPath });
|
|
160
|
-
|
|
165
|
+
const logOptions = normalizeRotatingLogOptions({ maxBytes: logMaxBytes, maxFiles: logMaxFiles });
|
|
161
166
|
try {
|
|
162
|
-
|
|
163
|
-
|
|
167
|
+
appendRotatingLogLine(
|
|
168
|
+
logPath,
|
|
169
|
+
`${new Date().toISOString()} spawning Senti daemon for ${normalizedSessionId}`,
|
|
170
|
+
logOptions,
|
|
171
|
+
);
|
|
164
172
|
const child = spawn(
|
|
165
173
|
process.execPath,
|
|
166
|
-
[
|
|
174
|
+
[
|
|
175
|
+
entryPath,
|
|
176
|
+
"session",
|
|
177
|
+
"daemon",
|
|
178
|
+
normalizedSessionId,
|
|
179
|
+
"--path",
|
|
180
|
+
path.resolve(String(targetPath || ".")),
|
|
181
|
+
"--log-file",
|
|
182
|
+
logPath,
|
|
183
|
+
"--log-max-bytes",
|
|
184
|
+
String(logOptions.maxBytes),
|
|
185
|
+
"--log-max-files",
|
|
186
|
+
String(logOptions.maxFiles),
|
|
187
|
+
],
|
|
167
188
|
{
|
|
168
189
|
detached: true,
|
|
169
|
-
stdio: ["ignore",
|
|
190
|
+
stdio: ["ignore", "ignore", "ignore"],
|
|
170
191
|
windowsHide: true,
|
|
171
192
|
env,
|
|
172
193
|
}
|
|
173
194
|
);
|
|
174
195
|
child.unref();
|
|
175
|
-
return {
|
|
196
|
+
return {
|
|
197
|
+
spawned: true,
|
|
198
|
+
pid: child.pid ?? null,
|
|
199
|
+
reason: "spawned",
|
|
200
|
+
logPath,
|
|
201
|
+
logMaxBytes: logOptions.maxBytes,
|
|
202
|
+
logMaxFiles: logOptions.maxFiles,
|
|
203
|
+
};
|
|
176
204
|
} catch (error) {
|
|
177
205
|
return {
|
|
178
206
|
spawned: false,
|
|
@@ -180,13 +208,5 @@ export async function spawnDetachedSentiDaemon({
|
|
|
180
208
|
reason: `spawn_failed: ${normalizeString(error?.message) || "unknown"}`,
|
|
181
209
|
logPath,
|
|
182
210
|
};
|
|
183
|
-
} finally {
|
|
184
|
-
if (logFd != null) {
|
|
185
|
-
try {
|
|
186
|
-
fs.closeSync(logFd);
|
|
187
|
-
} catch {
|
|
188
|
-
// fd already closed
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
211
|
}
|
|
192
212
|
}
|