sentinelayer-cli 0.43.0 → 0.45.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +29 -16
  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 +166 -51
  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 +160 -7
  27. package/src/mcp/registry.js +6 -5
  28. package/src/mcp/session-stdio-server.js +157 -8
  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 +41 -10
  36. package/src/session/admission-auth.js +95 -16
  37. package/src/session/admission.js +91 -21
  38. package/src/session/checkpoints.js +11 -10
  39. package/src/session/control-events.js +2 -0
  40. package/src/session/file-locks.js +19 -18
  41. package/src/session/invitations.js +26 -19
  42. package/src/session/live-source.js +13 -13
  43. package/src/session/reactions.js +258 -0
  44. package/src/session/route-classes.js +107 -0
  45. package/src/session/sync.js +110 -66
  46. package/src/session/tickets.js +21 -19
  47. package/src/session/title-sync.js +4 -3
  48. 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,258 @@
1
+ // reactions.js -- the ONE path every like / dislike / unlike / undislike takes, from
2
+ // the native CLI (`sl session react|action`) and the local MCP server
3
+ // (`session_react`, `session_action`) alike.
4
+ //
5
+ // Three guarantees. Separate copies of this logic once broke each of them:
6
+ //
7
+ // 1. Authority. The write runs inside withAgentAdmission for (session, agent): an
8
+ // admitted agent acts on ITS admission credential, a tombstoned (expired,
9
+ // malformed, mis-bound) admission is refused before any request, and nothing
10
+ // falls back to the human's broader token or legacy agent grants.
11
+ // 2. Intent. The API replays a reused idempotency key instead of acting on it
12
+ // (sentinelayer-api#914 reaction contract), so a key derived from
13
+ // (type, target, actor) makes like -> unlike -> like a silent no-op. Each NEW
14
+ // intent gets a fresh operation key. An explicit key is a retry of the intent
15
+ // that minted it, and is sent unchanged.
16
+ // 3. Truth. Every result names its outcome and carries its operation key, so an
17
+ // ambiguous failure (the request may have landed) can be retried as the SAME
18
+ // intent instead of becoming a new one that could retract a newer reaction.
19
+
20
+ import { randomBytes } from "node:crypto";
21
+ import process from "node:process";
22
+
23
+ import { assertCanonicalAgentId, canonicalAgentId, withAgentAdmission } from "./admission-auth.js";
24
+ import { createSessionMessageAction } from "./sync.js";
25
+
26
+ export const SESSION_REACTION_TYPES = Object.freeze(["like", "dislike", "unlike", "undislike"]);
27
+ const REACTION_TYPES = new Set(SESSION_REACTION_TYPES);
28
+ const UNDO_TARGETS = new Map([
29
+ ["unlike", "like"],
30
+ ["undislike", "dislike"],
31
+ ]);
32
+
33
+ // createSessionMessageAction failures that happen before any request leaves this machine.
34
+ const NOT_SENT_REASONS = new Set([
35
+ "invalid_input",
36
+ "remote_sync_disabled_env",
37
+ "circuit_breaker_open",
38
+ "no_session",
39
+ "not_authenticated",
40
+ ]);
41
+
42
+ /**
43
+ * Every outcome a reaction can have. `ok` is true only for the first three.
44
+ * applied -- this request recorded a new reaction row
45
+ * no_op -- an equal reaction was already active; nothing changed. With
46
+ * `collapsedActionId` the server retained this request as evidence.
47
+ * replayed -- an explicit (retry) key had already been recorded; the server
48
+ * returned that original result and did not re-check current state
49
+ * not_active -- an undo with nothing of this actor's active to retract (409)
50
+ * unsupported -- a server without reaction undo rejected the action value (422)
51
+ * refused -- any other definitive refusal; nothing was recorded
52
+ * not_sent -- the request never left this machine; nothing was recorded
53
+ * unknown -- the request may or may not have been recorded (timeout, lost
54
+ * response, 5xx): retry with the SAME operation key
55
+ */
56
+ export const SESSION_REACTION_OUTCOMES = Object.freeze([
57
+ "applied",
58
+ "no_op",
59
+ "replayed",
60
+ "not_active",
61
+ "unsupported",
62
+ "refused",
63
+ "not_sent",
64
+ "unknown",
65
+ ]);
66
+
67
+ function normalizeString(value) {
68
+ return String(value ?? "").trim();
69
+ }
70
+
71
+ export function isSessionReactionType(value) {
72
+ return REACTION_TYPES.has(normalizeString(value).toLowerCase());
73
+ }
74
+
75
+ /** The like/dislike an undo retracts, or "" for a non-undo. */
76
+ export function reactionUndoTarget(value) {
77
+ return UNDO_TARGETS.get(normalizeString(value).toLowerCase()) || "";
78
+ }
79
+
80
+ function targetKey({ targetSequenceId, targetCursor, targetActionId }) {
81
+ if (normalizeString(targetActionId)) return `action:${normalizeString(targetActionId)}`;
82
+ if (targetSequenceId) return `seq:${targetSequenceId}`;
83
+ return `cursor:${normalizeString(targetCursor)}`;
84
+ }
85
+
86
+ function targetLabel({ targetSequenceId, targetCursor, targetActionId }) {
87
+ if (normalizeString(targetActionId)) return `reply ${normalizeString(targetActionId)}`;
88
+ if (targetSequenceId) return `#${targetSequenceId}`;
89
+ return normalizeString(targetCursor) || "target";
90
+ }
91
+
92
+ /** A key for a NEW intent. Never derived from (type, target, actor) alone. */
93
+ export function newReactionOperationKey({ surface, reaction, agentId, ...target } = {}) {
94
+ return `${surface}:${reaction}:${targetKey(target)}:${agentId}:${randomBytes(8).toString("hex")}`;
95
+ }
96
+
97
+ /**
98
+ * The exact 422 a server without reaction undo (sentinelayer-api before #914) returns
99
+ * for this action: FastAPI's validation envelope around the route's action-type
100
+ * validator, a single entry echoing the value sent. Anything looser could relabel a
101
+ * different validation failure as "undo unsupported", so the match is exact.
102
+ */
103
+ export function isUndoUnsupportedValidationError(result = {}, actionType = "") {
104
+ if (result.status !== 422) return false;
105
+ const detail = result.error?.detail;
106
+ if (!Array.isArray(detail) || detail.length !== 1) return false;
107
+ const [entry] = detail;
108
+ return (
109
+ entry?.type === "value_error" &&
110
+ Array.isArray(entry.loc) &&
111
+ entry.loc.length === 2 &&
112
+ entry.loc[0] === "body" &&
113
+ entry.loc[1] === "actionType" &&
114
+ entry.msg === "Value error, unsupported message action type" &&
115
+ entry.input === actionType
116
+ );
117
+ }
118
+
119
+ function classify(reaction, result, retry) {
120
+ const status = Number.isInteger(result?.status) ? result.status : null;
121
+ if (result?.ok && result.action) {
122
+ if (normalizeString(result.collapsedActionId)) return "no_op";
123
+ if (result.duplicate) return retry ? "replayed" : "no_op";
124
+ return "applied";
125
+ }
126
+ if (status === null) {
127
+ return NOT_SENT_REASONS.has(normalizeString(result?.reason)) ? "not_sent" : "unknown";
128
+ }
129
+ if (status >= 500) return "unknown";
130
+ if (isUndoUnsupportedValidationError(result, reaction)) return "unsupported";
131
+ if (status === 409 && reactionUndoTarget(reaction) && result.error?.error?.code === "REACTION_NOT_ACTIVE") {
132
+ return "not_active";
133
+ }
134
+ if (status >= 400) return "refused";
135
+ return "unknown";
136
+ }
137
+
138
+ function describe(outcome, { reaction, agentId, target, operationKey, reason, errorCode, collapsedActionId }) {
139
+ const where = targetLabel(target);
140
+ switch (outcome) {
141
+ case "applied":
142
+ return `${agentId} ${reaction} recorded on ${where}.`;
143
+ case "no_op":
144
+ return (
145
+ `No change: ${agentId} already has an active ${reaction} on ${where}.` +
146
+ (collapsedActionId ? ` This request is retained as evidence ${collapsedActionId}.` : "")
147
+ );
148
+ case "replayed":
149
+ return (
150
+ `Replayed: operation ${operationKey} was already recorded. The server returned its ` +
151
+ `original result and did not re-check the current state.`
152
+ );
153
+ case "not_active":
154
+ return `Nothing to undo: ${agentId} has no active ${reactionUndoTarget(reaction)} on ${where}.`;
155
+ case "unsupported":
156
+ return `This server doesn't support undo yet (no '${reaction}' action). Nothing was changed.`;
157
+ case "refused":
158
+ return `The server refused this ${reaction} (${errorCode || reason}). Nothing was recorded.`;
159
+ case "not_sent":
160
+ return `Not sent (${reason}). Nothing was recorded.`;
161
+ default:
162
+ return (
163
+ `Outcome unknown (${reason || "no response"}): the server may have recorded this ${reaction}. ` +
164
+ `To retry this same intent, resend it with idempotency key ${operationKey}; ` +
165
+ `without that key it is a new intent.`
166
+ );
167
+ }
168
+ }
169
+
170
+ /**
171
+ * Submit one reaction. Throws only for invalid input or a refused admission (before
172
+ * any request); every server answer, network failure and timeout is an outcome.
173
+ */
174
+ export async function submitSessionReaction({
175
+ surface,
176
+ sessionId,
177
+ agentId,
178
+ reaction,
179
+ targetSequenceId = null,
180
+ targetCursor = "",
181
+ targetActionId = "",
182
+ note = "",
183
+ idempotencyKey = "",
184
+ targetPath = process.cwd(),
185
+ timeoutMs = 15_000,
186
+ dryRun = false,
187
+ createSessionMessageActionFn = createSessionMessageAction,
188
+ } = {}) {
189
+ const sid = normalizeString(sessionId);
190
+ const type = normalizeString(reaction).toLowerCase();
191
+ if (!sid) throw new Error("session id is required.");
192
+ if (!REACTION_TYPES.has(type)) {
193
+ throw new Error(`reaction must be one of: ${SESSION_REACTION_TYPES.join(", ")}.`);
194
+ }
195
+ // One canonical identity for authorisation and execution (see admission-auth.js).
196
+ assertCanonicalAgentId(agentId, "agentId");
197
+ const agent = canonicalAgentId(agentId);
198
+ if (!agent) throw new Error("agentId is required.");
199
+ const target = {
200
+ targetSequenceId: targetSequenceId || null,
201
+ targetCursor: normalizeString(targetCursor),
202
+ targetActionId: normalizeString(targetActionId),
203
+ };
204
+ if (!target.targetSequenceId && !target.targetCursor && !target.targetActionId) {
205
+ throw new Error("Provide a target sequence, cursor, or action id.");
206
+ }
207
+ const explicitKey = normalizeString(idempotencyKey);
208
+ const operationKey =
209
+ explicitKey || newReactionOperationKey({ surface, reaction: type, agentId: agent, ...target });
210
+ const base = {
211
+ sessionId: sid,
212
+ agentId: agent,
213
+ actionType: type,
214
+ ...target,
215
+ note: normalizeString(note),
216
+ operationKey,
217
+ idempotencyKey: operationKey,
218
+ retry: Boolean(explicitKey),
219
+ };
220
+ if (dryRun) return { ok: true, dryRun: true, outcome: "dry_run", ...base };
221
+
222
+ const result = await withAgentAdmission(sid, agent, () =>
223
+ createSessionMessageActionFn(sid, {
224
+ actionType: type,
225
+ targetPath,
226
+ ...target,
227
+ note: base.note,
228
+ metadata: { source: surface, agentId: agent },
229
+ idempotencyKey: operationKey,
230
+ timeoutMs,
231
+ }),
232
+ );
233
+ const outcome = classify(type, result, base.retry);
234
+ const collapsedActionId = normalizeString(result?.collapsedActionId) || null;
235
+ const status = Number.isInteger(result?.status) ? result.status : null;
236
+ const reason = normalizeString(result?.reason);
237
+ const errorCode = normalizeString(result?.error?.error?.code) || null;
238
+ return {
239
+ ok: outcome === "applied" || outcome === "no_op" || outcome === "replayed",
240
+ outcome,
241
+ ...base,
242
+ duplicate: Boolean(result?.duplicate),
243
+ collapsedActionId,
244
+ status,
245
+ reason,
246
+ errorCode,
247
+ message: describe(outcome, {
248
+ reaction: type,
249
+ agentId: agent,
250
+ target,
251
+ operationKey,
252
+ reason,
253
+ errorCode,
254
+ collapsedActionId,
255
+ }),
256
+ action: result?.ok && result.action ? result.action : null,
257
+ };
258
+ }
@@ -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
+ }