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.
Files changed (46) hide show
  1. package/README.md +8 -4
  2. package/package.json +1 -1
  3. package/src/agents/jules/tools/aidenid-email.js +7 -9
  4. package/src/agents/jules/tools/frontend-analyze.js +15 -3
  5. package/src/agents/jules/tools/runtime-audit.js +51 -21
  6. package/src/ai/aidenid.js +5 -4
  7. package/src/ai/proxy.js +15 -14
  8. package/src/audit/orchestrator.js +8 -5
  9. package/src/auth/admission-scope.js +5 -3
  10. package/src/auth/credential-destinations.js +221 -0
  11. package/src/auth/gate.js +4 -4
  12. package/src/auth/http.js +22 -5
  13. package/src/auth/service.js +52 -56
  14. package/src/cli.js +14 -6
  15. package/src/commands/ai/provision-governance.js +7 -6
  16. package/src/commands/init.js +25 -16
  17. package/src/commands/scan.js +6 -4
  18. package/src/commands/session.js +33 -45
  19. package/src/commands/spec.js +2 -2
  20. package/src/commands/watch.js +4 -3
  21. package/src/config/service.js +37 -0
  22. package/src/daemon/pulse.js +2 -1
  23. package/src/daemon/watchdog.js +117 -68
  24. package/src/legacy-cli.js +119 -39
  25. package/src/mcp/bridge-inputs.js +223 -0
  26. package/src/mcp/cli-command-tools.js +157 -7
  27. package/src/mcp/registry.js +2 -1
  28. package/src/mcp/session-stdio-server.js +114 -35
  29. package/src/mcp/smoke.js +27 -11
  30. package/src/mcp/token-service.js +7 -9
  31. package/src/memory/retrieval.js +14 -5
  32. package/src/pocket/ring-owner.js +13 -8
  33. package/src/review/ai-review.js +2 -2
  34. package/src/review/dd-report-email-client.js +4 -5
  35. package/src/session/admission-access.js +13 -10
  36. package/src/session/admission-auth.js +95 -16
  37. package/src/session/admission.js +44 -9
  38. package/src/session/checkpoints.js +11 -10
  39. package/src/session/file-locks.js +19 -18
  40. package/src/session/invitations.js +26 -19
  41. package/src/session/live-source.js +13 -13
  42. package/src/session/route-classes.js +107 -0
  43. package/src/session/sync.js +70 -56
  44. package/src/session/tickets.js +21 -19
  45. package/src/session/title-sync.js +4 -3
  46. 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
- const message = [
42
- "session-mutation-csrf:v1",
43
- canonicalSessionRef(sessionId),
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
- bearerToken,
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
- "X-CSRF-Token": createSessionMutationCsrfToken({
65
- bearerToken: token,
66
- sessionId,
67
- routeId,
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?.token || !auth?.apiUrl) {
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
- bearerToken: auth.token,
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
- token,
159
+ credential,
159
160
  signal,
160
161
  _sseFetch = fetch,
161
162
  reconnectBackoffMs = DEFAULT_RECONNECT_BACKOFF_MS,
162
163
  } = {}) {
163
- if (!apiBaseUrl || !sessionId || !token) return;
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 _sseFetch(url, {
186
- method: "GET",
187
- headers: {
188
- Authorization: `Bearer ${token}`,
189
- Accept: "text/event-stream",
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
- token,
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 && token)
281
+ _remoteIterator || (apiBaseUrl && credential)
282
282
  ? _remoteIterator
283
283
  ? _remoteIterator
284
- : watchRemoteStream({ apiBaseUrl, sessionId, token, signal })
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
+ }