sentinelayer-cli 0.44.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 +8 -4
- 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 +33 -45
- 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 +157 -7
- package/src/mcp/registry.js +2 -1
- package/src/mcp/session-stdio-server.js +114 -35
- 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 +13 -10
- package/src/session/admission-auth.js +95 -16
- package/src/session/admission.js +44 -9
- package/src/session/checkpoints.js +11 -10
- 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/route-classes.js +107 -0
- package/src/session/sync.js +70 -56
- package/src/session/tickets.js +21 -19
- package/src/session/title-sync.js +4 -3
- package/src/telemetry/sync.js +8 -9
|
@@ -3,6 +3,7 @@ import fsp from "node:fs/promises";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import process from "node:process";
|
|
5
5
|
|
|
6
|
+
import { checkedTransport, credentialFor, credentialHmac, isAuthenticated, isCredential } from "../auth/credential-destinations.js";
|
|
6
7
|
import { requestJsonMutation } from "../auth/http.js";
|
|
7
8
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
8
9
|
import { renderCoordinationBulletList } from "./coordination-guidance.js";
|
|
@@ -30,6 +31,16 @@ export function canonicalSessionRef(value) {
|
|
|
30
31
|
return raw.toLowerCase();
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
function sessionMutationCsrfMessage({ sessionId, routeId, idempotencyKey } = {}) {
|
|
35
|
+
return [
|
|
36
|
+
"session-mutation-csrf:v1",
|
|
37
|
+
canonicalSessionRef(sessionId),
|
|
38
|
+
normalizeString(routeId),
|
|
39
|
+
normalizeString(idempotencyKey),
|
|
40
|
+
].join("\0");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// The API's CSRF proof for a given bearer: what the server (or a test fixture) checks against.
|
|
33
44
|
export function createSessionMutationCsrfToken({
|
|
34
45
|
bearerToken,
|
|
35
46
|
sessionId,
|
|
@@ -38,35 +49,29 @@ export function createSessionMutationCsrfToken({
|
|
|
38
49
|
} = {}) {
|
|
39
50
|
const secret = normalizeString(bearerToken);
|
|
40
51
|
if (!secret) return "";
|
|
41
|
-
|
|
42
|
-
"
|
|
43
|
-
|
|
44
|
-
normalizeString(routeId),
|
|
45
|
-
normalizeString(idempotencyKey),
|
|
46
|
-
].join("\0");
|
|
47
|
-
return createHmac("sha256", secret).update(message, "utf-8").digest("hex");
|
|
52
|
+
return createHmac("sha256", secret)
|
|
53
|
+
.update(sessionMutationCsrfMessage({ sessionId, routeId, idempotencyKey }), "utf-8")
|
|
54
|
+
.digest("hex");
|
|
48
55
|
}
|
|
49
56
|
|
|
57
|
+
// The session-mutation headers: origin and a CSRF proof derived from the credential the request
|
|
58
|
+
// is sent with. The credential itself is attached by credentialedRequest, never here.
|
|
50
59
|
export function createSessionMutationHeaders({
|
|
51
|
-
|
|
60
|
+
credential,
|
|
52
61
|
sessionId,
|
|
53
62
|
routeId,
|
|
54
63
|
idempotencyKey,
|
|
55
64
|
origin = SESSION_MUTATION_ORIGIN,
|
|
56
65
|
} = {}) {
|
|
57
|
-
const token = normalizeString(bearerToken);
|
|
58
66
|
const normalizedOrigin = normalizeString(origin) || SESSION_MUTATION_ORIGIN;
|
|
59
67
|
return {
|
|
60
|
-
Authorization: `Bearer ${token}`,
|
|
61
68
|
Origin: normalizedOrigin,
|
|
62
69
|
"Sec-Fetch-Site": "same-site",
|
|
63
70
|
"X-Sentinelayer-Session-Mutation": "session-mutation",
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
sessionId,
|
|
67
|
-
|
|
68
|
-
idempotencyKey,
|
|
69
|
-
}),
|
|
71
|
+
// keyed inside src/auth/credential-destinations.js; the token never leaves it
|
|
72
|
+
"X-CSRF-Token": isCredential(credential)
|
|
73
|
+
? credentialHmac(credential, sessionMutationCsrfMessage({ sessionId, routeId, idempotencyKey }))
|
|
74
|
+
: "",
|
|
70
75
|
};
|
|
71
76
|
}
|
|
72
77
|
|
|
@@ -106,10 +111,11 @@ export async function acceptSessionInvitation(
|
|
|
106
111
|
env: process.env,
|
|
107
112
|
autoRotate: false,
|
|
108
113
|
});
|
|
109
|
-
if (!auth
|
|
114
|
+
if (!isAuthenticated(auth) || !auth?.apiUrl) {
|
|
110
115
|
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
111
116
|
}
|
|
112
117
|
|
|
118
|
+
const credential = await credentialFor(auth);
|
|
113
119
|
const resolvedIdempotencyKey =
|
|
114
120
|
normalizeString(idempotencyKey) || createSessionMutationIdempotencyKey("session-invite-accept");
|
|
115
121
|
const apiUrl = normalizeString(auth.apiUrl).replace(/\/+$/, "");
|
|
@@ -121,14 +127,15 @@ export async function acceptSessionInvitation(
|
|
|
121
127
|
if (normalizedSeatKey) body.seatKey = normalizedSeatKey;
|
|
122
128
|
if (normalizedAgentId) body.agentId = normalizedAgentId;
|
|
123
129
|
|
|
124
|
-
const result = await requestMutation(
|
|
130
|
+
const result = await checkedTransport(requestMutation)(
|
|
125
131
|
`${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/invitations/accept`,
|
|
126
132
|
{
|
|
127
133
|
method: "POST",
|
|
128
134
|
operationName: "session.invitation_accept",
|
|
129
135
|
idempotencyKey: resolvedIdempotencyKey,
|
|
136
|
+
credential,
|
|
130
137
|
headers: createSessionMutationHeaders({
|
|
131
|
-
|
|
138
|
+
credential,
|
|
132
139
|
sessionId: normalizedSessionId,
|
|
133
140
|
routeId: SESSION_INVITATION_ACCEPT_ROUTE_ID,
|
|
134
141
|
idempotencyKey: resolvedIdempotencyKey,
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
import fs from "node:fs";
|
|
19
19
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
20
20
|
|
|
21
|
+
import { CredentialDestinationRefused, credentialedRequest } from "../auth/credential-destinations.js";
|
|
21
22
|
import { resolveSessionPaths } from "./paths.js";
|
|
22
23
|
import { readStream } from "./stream.js";
|
|
23
24
|
import {
|
|
@@ -155,12 +156,12 @@ export async function* watchLocalStream({
|
|
|
155
156
|
export async function* watchRemoteStream({
|
|
156
157
|
apiBaseUrl,
|
|
157
158
|
sessionId,
|
|
158
|
-
|
|
159
|
+
credential,
|
|
159
160
|
signal,
|
|
160
161
|
_sseFetch = fetch,
|
|
161
162
|
reconnectBackoffMs = DEFAULT_RECONNECT_BACKOFF_MS,
|
|
162
163
|
} = {}) {
|
|
163
|
-
if (!apiBaseUrl || !sessionId || !
|
|
164
|
+
if (!apiBaseUrl || !sessionId || !credential) return;
|
|
164
165
|
const endpoint = `${apiBaseUrl.replace(/\/+$/, "")}/api/v1/sessions/${encodeURIComponent(
|
|
165
166
|
sessionId,
|
|
166
167
|
)}/stream`;
|
|
@@ -182,17 +183,16 @@ export async function* watchRemoteStream({
|
|
|
182
183
|
? `${endpoint}?fromSequence=${encodeURIComponent(String(resumeSequence))}`
|
|
183
184
|
: endpoint;
|
|
184
185
|
try {
|
|
185
|
-
response = await
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
signal,
|
|
192
|
-
});
|
|
186
|
+
response = await credentialedRequest(
|
|
187
|
+
credential,
|
|
188
|
+
url,
|
|
189
|
+
{ method: "GET", headers: { Accept: "text/event-stream" }, signal },
|
|
190
|
+
{ fetchImpl: _sseFetch },
|
|
191
|
+
);
|
|
193
192
|
} catch (err) {
|
|
194
193
|
if (signal?.aborted) return;
|
|
195
194
|
yield { source: "sse", error: String(err?.message || err) };
|
|
195
|
+
if (err instanceof CredentialDestinationRefused) return; // final: no reconnect can change it
|
|
196
196
|
await sleep(backoff);
|
|
197
197
|
backoff = Math.min(backoff * 2, MAX_RECONNECT_BACKOFF_MS);
|
|
198
198
|
continue;
|
|
@@ -267,7 +267,7 @@ export async function* mergeLiveSources({
|
|
|
267
267
|
sessionId,
|
|
268
268
|
targetPath,
|
|
269
269
|
apiBaseUrl,
|
|
270
|
-
|
|
270
|
+
credential,
|
|
271
271
|
signal,
|
|
272
272
|
_localIterator,
|
|
273
273
|
_remoteIterator,
|
|
@@ -278,10 +278,10 @@ export async function* mergeLiveSources({
|
|
|
278
278
|
? _localIterator
|
|
279
279
|
: watchLocalStream({ sessionId, targetPath, signal });
|
|
280
280
|
const remoteIterable =
|
|
281
|
-
_remoteIterator || (apiBaseUrl &&
|
|
281
|
+
_remoteIterator || (apiBaseUrl && credential)
|
|
282
282
|
? _remoteIterator
|
|
283
283
|
? _remoteIterator
|
|
284
|
-
: watchRemoteStream({ apiBaseUrl, sessionId,
|
|
284
|
+
: watchRemoteStream({ apiBaseUrl, sessionId, credential, signal })
|
|
285
285
|
: null;
|
|
286
286
|
|
|
287
287
|
const seen = new Map();
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// route-classes.js -- the ONE classification of every local MCP session tool ("mcp:<tool>")
|
|
2
|
+
// and every `sl session` command the MCP CLI bridge can invoke ("cli:session <path>").
|
|
3
|
+
//
|
|
4
|
+
// In an agent context it decides what a route may do when the session it acts in stores NO
|
|
5
|
+
// admission on this machine (when the session stores one, every route binds to it or is
|
|
6
|
+
// refused; see runInSessionToolContext in src/mcp/session-stdio-server.js and
|
|
7
|
+
// assertDispatchMatchesScope in src/session/admission-auth.js):
|
|
8
|
+
//
|
|
9
|
+
// legacy-data-plane a compatibility list for rooms that don't require admission:
|
|
10
|
+
// posting, replying, reading/polling, message actions and reactions,
|
|
11
|
+
// attention requests. Runs as before; the server's admission mode
|
|
12
|
+
// decides whether a room accepts it.
|
|
13
|
+
// control leases, locks, guards, tickets, checkpoints, joining and leaving,
|
|
14
|
+
// listeners, room setup and everything else that is not plain
|
|
15
|
+
// data-plane traffic. Refused unless the session's admission binds it.
|
|
16
|
+
// owner room owner actions. Refused in an agent context.
|
|
17
|
+
// exempt-local sends no request (a local store or built-in constants only).
|
|
18
|
+
//
|
|
19
|
+
// A route missing from this table is treated as unclassified and refused like control. A
|
|
20
|
+
// unit test derives every MCP tool and every bridge-invokable command from the real handlers
|
|
21
|
+
// and command tree and fails when one is missing here.
|
|
22
|
+
|
|
23
|
+
export const SESSION_ROUTE_CLASSES = Object.freeze({
|
|
24
|
+
// ---- local MCP session tools
|
|
25
|
+
"mcp:poll_inbox": "legacy-data-plane",
|
|
26
|
+
"mcp:read_history": "legacy-data-plane",
|
|
27
|
+
"mcp:send_message": "legacy-data-plane",
|
|
28
|
+
"mcp:attention_request": "legacy-data-plane",
|
|
29
|
+
"mcp:session_action": "legacy-data-plane",
|
|
30
|
+
"mcp:session_react": "legacy-data-plane",
|
|
31
|
+
"mcp:session_reply": "legacy-data-plane",
|
|
32
|
+
"mcp:session_lock": "control",
|
|
33
|
+
"mcp:session_unlock": "control",
|
|
34
|
+
"mcp:session_locks": "control",
|
|
35
|
+
"mcp:memory.write": "exempt-local",
|
|
36
|
+
"mcp:memory.recall": "exempt-local",
|
|
37
|
+
"mcp:memory.summarize": "exempt-local",
|
|
38
|
+
|
|
39
|
+
// ---- `sl session` commands invokable through the MCP CLI bridge
|
|
40
|
+
"cli:session say": "legacy-data-plane",
|
|
41
|
+
"cli:session post-agent": "legacy-data-plane",
|
|
42
|
+
"cli:session observe": "legacy-data-plane",
|
|
43
|
+
"cli:session reply": "legacy-data-plane",
|
|
44
|
+
"cli:session comment": "legacy-data-plane",
|
|
45
|
+
"cli:session action": "legacy-data-plane",
|
|
46
|
+
"cli:session react": "legacy-data-plane",
|
|
47
|
+
"cli:session read": "legacy-data-plane",
|
|
48
|
+
"cli:session history": "legacy-data-plane",
|
|
49
|
+
"cli:session search": "legacy-data-plane",
|
|
50
|
+
"cli:session pins": "legacy-data-plane",
|
|
51
|
+
"cli:session lock": "control",
|
|
52
|
+
"cli:session unlock": "control",
|
|
53
|
+
"cli:session locks": "control",
|
|
54
|
+
"cli:session renew": "control",
|
|
55
|
+
"cli:session guard": "control",
|
|
56
|
+
"cli:session guard-hook": "control",
|
|
57
|
+
"cli:session guard-install": "control",
|
|
58
|
+
"cli:session guard-uninstall": "control",
|
|
59
|
+
"cli:session checkpoint create": "control",
|
|
60
|
+
"cli:session checkpoint generate": "control",
|
|
61
|
+
"cli:session checkpoint list": "control",
|
|
62
|
+
"cli:session checkpoint show": "control",
|
|
63
|
+
"cli:session ticket claim": "control",
|
|
64
|
+
"cli:session ticket events": "control",
|
|
65
|
+
"cli:session ticket list": "control",
|
|
66
|
+
"cli:session ticket release": "control",
|
|
67
|
+
"cli:session ticket renew": "control",
|
|
68
|
+
"cli:session ticket submit": "control",
|
|
69
|
+
"cli:session join": "control",
|
|
70
|
+
"cli:session leave": "control",
|
|
71
|
+
"cli:session stop-listener": "control",
|
|
72
|
+
"cli:session listeners": "control",
|
|
73
|
+
"cli:session set-title": "control",
|
|
74
|
+
"cli:session start": "control",
|
|
75
|
+
"cli:session ensure": "control",
|
|
76
|
+
"cli:session continue": "control",
|
|
77
|
+
"cli:session inject-guide": "control",
|
|
78
|
+
"cli:session setup-guides": "control",
|
|
79
|
+
"cli:session sync": "control",
|
|
80
|
+
"cli:session status": "control",
|
|
81
|
+
"cli:session list": "control",
|
|
82
|
+
"cli:session usage": "control",
|
|
83
|
+
"cli:session recall": "control",
|
|
84
|
+
"cli:session recap now": "control",
|
|
85
|
+
"cli:session edit": "control",
|
|
86
|
+
"cli:session wake codex": "control",
|
|
87
|
+
"cli:session wake codex-notify": "control",
|
|
88
|
+
"cli:session wake daemon": "control",
|
|
89
|
+
"cli:session access approve": "owner",
|
|
90
|
+
"cli:session access deny": "owner",
|
|
91
|
+
"cli:session access revoke": "owner",
|
|
92
|
+
"cli:session access mode": "owner",
|
|
93
|
+
"cli:session access request": "owner",
|
|
94
|
+
"cli:session access list": "owner",
|
|
95
|
+
"cli:session access status": "owner",
|
|
96
|
+
"cli:session actions": "exempt-local",
|
|
97
|
+
"cli:session templates": "exempt-local",
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
/** The compatibility list for rooms that don't require admission, as one named view of the table. */
|
|
101
|
+
export const LEGACY_DATA_PLANE_TOOLS = Object.freeze(
|
|
102
|
+
Object.keys(SESSION_ROUTE_CLASSES).filter((route) => SESSION_ROUTE_CLASSES[route] === "legacy-data-plane"),
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
export function sessionRouteClass(route) {
|
|
106
|
+
return SESSION_ROUTE_CLASSES[route] || "unclassified";
|
|
107
|
+
}
|