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
package/src/pocket/ring-owner.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
+
import { checkedTransport, credentialFor, gatewayCredential, isAuthenticated } from "../auth/credential-destinations.js";
|
|
3
4
|
import { requestJsonMutation } from "../auth/http.js";
|
|
4
5
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
5
6
|
|
|
@@ -16,14 +17,12 @@ function normalizeString(value) {
|
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* Resolve the pocket-gateway base URL — DISTINCT from the senti apiUrl (do NOT reuse resolveApiUrl; that is the API host).
|
|
19
|
-
* Precedence: env SENTI_POCKET_URL ->
|
|
20
|
+
* Precedence: env SENTI_POCKET_URL -> a config value if the caller passed one. Absent -> "". The user's token is only
|
|
21
|
+
* sent to the SENTI_POCKET_URL origin (src/auth/credential-destinations.js).
|
|
20
22
|
* A present value MUST be an http(s) URL (fail-closed on a bogus scheme). Trailing slashes trimmed.
|
|
21
23
|
*/
|
|
22
24
|
export function resolvePocketGatewayUrl({ env = process.env, configUrl = "" } = {}) {
|
|
23
|
-
const raw =
|
|
24
|
-
normalizeString(env.SENTI_POCKET_URL) ||
|
|
25
|
-
normalizeString(env.POCKET_GATEWAY_URL) ||
|
|
26
|
-
normalizeString(configUrl);
|
|
25
|
+
const raw = normalizeString(env.SENTI_POCKET_URL) || normalizeString(configUrl);
|
|
27
26
|
if (!raw) return "";
|
|
28
27
|
if (!/^https?:\/\//i.test(raw)) throw new Error("pocket gateway URL must be an http(s) URL");
|
|
29
28
|
return raw.replace(/\/+$/, "");
|
|
@@ -74,9 +73,15 @@ export async function ringOwner(
|
|
|
74
73
|
}
|
|
75
74
|
|
|
76
75
|
const auth = await resolveAuthSession({ cwd, env, autoRotate: false });
|
|
77
|
-
if (!auth
|
|
76
|
+
if (!isAuthenticated(auth)) {
|
|
78
77
|
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
79
78
|
}
|
|
79
|
+
// The user's token, bound to the gateway SENTI_POCKET_URL names; a --gateway-url on any other
|
|
80
|
+
// origin is refused when the request is sent.
|
|
81
|
+
const credential = await gatewayCredential(await credentialFor(auth, { env }), { env });
|
|
82
|
+
if (!credential) {
|
|
83
|
+
throw new Error("pocket gateway URL not configured. Set SENTI_POCKET_URL to the pocket-gateway base URL.");
|
|
84
|
+
}
|
|
80
85
|
|
|
81
86
|
const context = { sessionId: sid };
|
|
82
87
|
if (normalizeString(whatWeNeed)) context.whatWeNeed = normalizeString(whatWeNeed);
|
|
@@ -88,10 +93,10 @@ export async function ringOwner(
|
|
|
88
93
|
|
|
89
94
|
// requestJsonMutation auto-derives a transport Idempotency-Key from operationName (retry-safe at the wire); the
|
|
90
95
|
// gateway's own ring-level dedupe (PR-B4) keys on body.idempotencyKey OR a content hash, so an identical retry rings once.
|
|
91
|
-
return requestMutation(`${gw}/dial/ring-owner`, {
|
|
96
|
+
return checkedTransport(requestMutation)(`${gw}/dial/ring-owner`, {
|
|
92
97
|
method: "POST",
|
|
93
98
|
operationName: "pocket.ring_owner",
|
|
94
|
-
|
|
99
|
+
credential,
|
|
95
100
|
body,
|
|
96
101
|
});
|
|
97
102
|
}
|
package/src/review/ai-review.js
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
import { computeProviderCost } from "../billing/price-book.js";
|
|
11
11
|
import { buildBillingRunId, buildCallIdempotencyKey, sanitizeBillingMetadata } from "../billing/ledger-entry.js";
|
|
12
12
|
import { recordSessionUsage } from "../billing/session-usage.js";
|
|
13
|
-
import { loadConfig } from "../config/service.js";
|
|
13
|
+
import { loadConfig, userConfigValues } from "../config/service.js";
|
|
14
14
|
import { evaluateBudget } from "../cost/budget.js";
|
|
15
15
|
import { appendCostEntry, summarizeCostHistory } from "../cost/history.js";
|
|
16
16
|
import { estimateTokens } from "../cost/tokenizer.js";
|
|
@@ -534,7 +534,7 @@ export async function runAiReviewLayer({
|
|
|
534
534
|
configModel: config.resolved.defaultModelId || DEFAULT_REVIEW_AI_MODEL,
|
|
535
535
|
});
|
|
536
536
|
const explicitApiKey = normalizeString(apiKey);
|
|
537
|
-
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, config
|
|
537
|
+
const configuredApiKey = resolveConfiguredApiKey(resolvedProvider, userConfigValues(config));
|
|
538
538
|
|
|
539
539
|
const prompt = buildAiReviewPrompt({
|
|
540
540
|
targetPath: normalizedTargetPath,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
|
|
3
|
+
import { checkedTransport, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
3
4
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
4
5
|
import { requestJson } from "../auth/http.js";
|
|
5
6
|
|
|
@@ -97,7 +98,7 @@ export async function sendDdReportEmail({
|
|
|
97
98
|
});
|
|
98
99
|
}
|
|
99
100
|
|
|
100
|
-
if (!session
|
|
101
|
+
if (!isAuthenticated(session)) {
|
|
101
102
|
return errorResult({
|
|
102
103
|
runId: normalizedRunId,
|
|
103
104
|
to: normalizedTo,
|
|
@@ -116,11 +117,9 @@ export async function sendDdReportEmail({
|
|
|
116
117
|
});
|
|
117
118
|
|
|
118
119
|
try {
|
|
119
|
-
const response = await requestJsonImpl(endpoint, {
|
|
120
|
+
const response = await checkedTransport(requestJsonImpl)(endpoint, {
|
|
120
121
|
method: "POST",
|
|
121
|
-
|
|
122
|
-
Authorization: `Bearer ${session.token}`,
|
|
123
|
-
},
|
|
122
|
+
credential: await credentialFor(session),
|
|
124
123
|
idempotencyKey,
|
|
125
124
|
body: { to: normalizedTo },
|
|
126
125
|
timeoutMs,
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
import process from "node:process";
|
|
10
10
|
|
|
11
|
+
import { checkedTransport, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
11
12
|
import { requestJson, requestJsonMutation } from "../auth/http.js";
|
|
12
13
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
13
14
|
import { hasStoredAdmissionCredentials } from "./admission.js";
|
|
@@ -74,11 +75,11 @@ async function authContext(targetPath, resolveAuthSession) {
|
|
|
74
75
|
env: process.env,
|
|
75
76
|
autoRotate: false,
|
|
76
77
|
});
|
|
77
|
-
if (!auth
|
|
78
|
+
if (!isAuthenticated(auth) || !auth?.apiUrl) {
|
|
78
79
|
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
79
80
|
}
|
|
80
81
|
return {
|
|
81
|
-
|
|
82
|
+
credential: await credentialFor(auth),
|
|
82
83
|
apiUrl: normalizeString(auth.apiUrl).replace(/\/+$/, ""),
|
|
83
84
|
};
|
|
84
85
|
}
|
|
@@ -109,12 +110,13 @@ async function mutateAdmission(
|
|
|
109
110
|
) {
|
|
110
111
|
const key = boundedIdempotencyKey(idempotencyKey, operationName);
|
|
111
112
|
const auth = await authContext(targetPath, resolveAuthSession);
|
|
112
|
-
const result = await requestMutation(admissionsUrl(auth.apiUrl, sessionId, suffix), {
|
|
113
|
+
const result = await checkedTransport(requestMutation)(admissionsUrl(auth.apiUrl, sessionId, suffix), {
|
|
113
114
|
method: "POST",
|
|
114
115
|
operationName,
|
|
115
116
|
idempotencyKey: key,
|
|
117
|
+
credential: auth.credential,
|
|
116
118
|
headers: createSessionMutationHeaders({
|
|
117
|
-
|
|
119
|
+
credential: auth.credential,
|
|
118
120
|
sessionId,
|
|
119
121
|
routeId,
|
|
120
122
|
idempotencyKey: key,
|
|
@@ -143,9 +145,9 @@ export async function listSessionAdmissions(
|
|
|
143
145
|
}
|
|
144
146
|
const auth = await authContext(targetPath, resolveAuthSession);
|
|
145
147
|
const query = normalizedStatus ? `?status=${encodeURIComponent(normalizedStatus)}` : "";
|
|
146
|
-
return requestRead(`${admissionsUrl(auth.apiUrl, sid)}${query}`, {
|
|
148
|
+
return checkedTransport(requestRead)(`${admissionsUrl(auth.apiUrl, sid)}${query}`, {
|
|
147
149
|
method: "GET",
|
|
148
|
-
|
|
150
|
+
credential: auth.credential,
|
|
149
151
|
});
|
|
150
152
|
}
|
|
151
153
|
|
|
@@ -161,9 +163,9 @@ export async function getSessionAdmission(
|
|
|
161
163
|
const sid = required(sessionId, "session id");
|
|
162
164
|
const aid = required(admissionId, "admission id");
|
|
163
165
|
const auth = await authContext(targetPath, resolveAuthSession);
|
|
164
|
-
return requestRead(admissionsUrl(auth.apiUrl, sid, `/${encodeURIComponent(aid)}`), {
|
|
166
|
+
return checkedTransport(requestRead)(admissionsUrl(auth.apiUrl, sid, `/${encodeURIComponent(aid)}`), {
|
|
165
167
|
method: "GET",
|
|
166
|
-
|
|
168
|
+
credential: auth.credential,
|
|
167
169
|
});
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -264,14 +266,15 @@ export async function setSessionAdmissionMode(
|
|
|
264
266
|
await assertOwnerAccessContext();
|
|
265
267
|
const key = boundedIdempotencyKey(idempotencyKey, "session.admission_mode");
|
|
266
268
|
const auth = await authContext(targetPath, resolveAuthSession);
|
|
267
|
-
const result = await requestMutation(
|
|
269
|
+
const result = await checkedTransport(requestMutation)(
|
|
268
270
|
`${auth.apiUrl}/api/v1/sessions/${encodeURIComponent(sid)}/admission-mode`,
|
|
269
271
|
{
|
|
270
272
|
method: "POST",
|
|
271
273
|
operationName: "session.admission_mode",
|
|
272
274
|
idempotencyKey: key,
|
|
275
|
+
credential: auth.credential,
|
|
273
276
|
headers: createSessionMutationHeaders({
|
|
274
|
-
|
|
277
|
+
credential: auth.credential,
|
|
275
278
|
sessionId: sid,
|
|
276
279
|
routeId: ROUTES.mode,
|
|
277
280
|
idempotencyKey: key,
|
|
@@ -25,7 +25,12 @@ import path from "node:path";
|
|
|
25
25
|
import process from "node:process";
|
|
26
26
|
|
|
27
27
|
import { admittedAgentScope } from "../auth/admission-scope.js";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
hasStoredAdmissionCredentials,
|
|
30
|
+
readAdmissionCredentialState,
|
|
31
|
+
storedAdmissionAgentsForSession,
|
|
32
|
+
} from "./admission.js";
|
|
33
|
+
import { sessionRouteClass } from "./route-classes.js";
|
|
29
34
|
|
|
30
35
|
export class AdmissionCredentialRefused extends Error {
|
|
31
36
|
constructor(message, { reason, sessionId, agentId } = {}) {
|
|
@@ -238,26 +243,89 @@ export async function resolveAgentAdmissionTarget(args = [], { env = process.env
|
|
|
238
243
|
return null;
|
|
239
244
|
}
|
|
240
245
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
* disagree with execution. A mismatch refuses before the action runs.
|
|
247
|
-
*/
|
|
248
|
-
export async function assertDispatchMatchesScope(actionCommand, { env = process.env, homeDir } = {}) {
|
|
246
|
+
// What Commander parsed for the action about to run: its `session` command path, its
|
|
247
|
+
// options (and which of them were given on the command line, not defaulted, such as read's
|
|
248
|
+
// `--agent cli-user`), and its operands. The preAction guard reads only these, the same values
|
|
249
|
+
// the action itself uses, so no reading of the raw argv can disagree with execution.
|
|
250
|
+
function parsedSessionAction(actionCommand) {
|
|
249
251
|
const names = [];
|
|
250
252
|
for (let cmd = actionCommand; cmd && cmd.parent; cmd = cmd.parent) names.unshift(cmd.name());
|
|
251
|
-
if (String(names[0] || "").toLowerCase() !== "session") return;
|
|
252
|
-
const command = names.slice(1).join(" ").toLowerCase();
|
|
253
|
-
const principal = SESSION_AGENT_PRINCIPAL[command];
|
|
253
|
+
if (String(names[0] || "").toLowerCase() !== "session") return null;
|
|
254
254
|
const opts = actionCommand.opts();
|
|
255
255
|
const given = (key) =>
|
|
256
256
|
actionCommand.getOptionValueSource?.(key) === "cli" ? String(opts[key] ?? "").trim() : "";
|
|
257
|
+
const processed = Array.isArray(actionCommand.processedArgs) ? actionCommand.processedArgs : [];
|
|
258
|
+
return {
|
|
259
|
+
route: `cli:${names.join(" ").toLowerCase()}`,
|
|
260
|
+
command: names.slice(1).join(" ").toLowerCase(),
|
|
261
|
+
opts,
|
|
262
|
+
given,
|
|
263
|
+
first: String(processed[0] ?? "").trim(),
|
|
264
|
+
operands: processed.flat().map((value) => String(value ?? "").trim()).filter(Boolean),
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Through the MCP CLI bridge (which marks its child processes with SENTINELAYER_MCP_BRIDGE=1)
|
|
269
|
+
// in an agent context (SENTINELAYER_AGENT_ID is set, or this machine stores any agent
|
|
270
|
+
// admission), by the command's class in SESSION_ROUTE_CLASSES:
|
|
271
|
+
// - exempt-local: runs as before
|
|
272
|
+
// - owner: refused
|
|
273
|
+
// - a session the command will use (its --session or --id value, given or defaulted, or any
|
|
274
|
+
// operand) stores an admission, in any state: the action must be running in that
|
|
275
|
+
// session's admission scope, or it is refused before it runs
|
|
276
|
+
// - no session it uses stores one: legacy-data-plane runs as before; control and
|
|
277
|
+
// unclassified commands are refused
|
|
278
|
+
async function assertBridgedRoute(action, { env, homeDir }) {
|
|
279
|
+
if (env.SENTINELAYER_MCP_BRIDGE !== "1") return;
|
|
280
|
+
const agentContext =
|
|
281
|
+
Boolean(String(env.SENTINELAYER_AGENT_ID || "").trim()) || (await hasStoredAdmissionCredentials({ homeDir }));
|
|
282
|
+
if (!agentContext) return;
|
|
283
|
+
const routeClass = sessionRouteClass(action.route);
|
|
284
|
+
const refuse = (why) => {
|
|
285
|
+
throw new AdmissionCredentialRefused(
|
|
286
|
+
`\`sl ${action.route.slice("cli:".length)}\` runs for an agent here and needs an admission it can bind to: ` +
|
|
287
|
+
`${why}. It will not fall back to other credentials.`,
|
|
288
|
+
{ reason: "bridged_route_refused" },
|
|
289
|
+
);
|
|
290
|
+
};
|
|
291
|
+
if (routeClass === "exempt-local") return;
|
|
292
|
+
if (routeClass === "owner") refuse("owner actions are not available to agents");
|
|
293
|
+
const named = [action.opts.session, action.opts.id, ...action.operands]
|
|
294
|
+
.map((value) => String(value ?? "").trim())
|
|
295
|
+
.filter(Boolean);
|
|
296
|
+
const admitted = [];
|
|
297
|
+
for (const sessionId of named) {
|
|
298
|
+
if ((await storedAdmissionAgentsForSession(sessionId, { homeDir })).length > 0) admitted.push(sessionId);
|
|
299
|
+
}
|
|
300
|
+
if (admitted.length > 0) {
|
|
301
|
+
const bound = String(currentAdmittedAgent()?.sessionId || "").toLowerCase();
|
|
302
|
+
if (!bound || !admitted.some((sid) => sid.toLowerCase() === bound)) {
|
|
303
|
+
refuse(`session ${admitted[0]} stores an admission and this command's agent is not bound to it`);
|
|
304
|
+
}
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
if (routeClass === "legacy-data-plane") return;
|
|
308
|
+
refuse(routeClass === "control" ? "it is a control operation" : "it is not classified for agents");
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* preAction guard, run before ANY action, on Commander's parsed values only:
|
|
313
|
+
* 1. a bridged command in an agent context binds to its session's admission, runs as a
|
|
314
|
+
* legacy data-plane route, or is refused (assertBridgedRoute above);
|
|
315
|
+
* 2. the identity Commander resolved for the action must be the one whose admission scope
|
|
316
|
+
* (if any) the dispatch was wrapped in.
|
|
317
|
+
* Either refusal happens before the action runs, so before any request.
|
|
318
|
+
*/
|
|
319
|
+
export async function assertDispatchMatchesScope(actionCommand, { env = process.env, homeDir } = {}) {
|
|
320
|
+
const action = parsedSessionAction(actionCommand);
|
|
321
|
+
if (!action) return;
|
|
322
|
+
await assertBridgedRoute(action, { env, homeDir });
|
|
323
|
+
const { command, given } = action;
|
|
324
|
+
const principal = SESSION_AGENT_PRINCIPAL[command];
|
|
257
325
|
let actual = null;
|
|
258
326
|
if (principal === "actor" || (!principal && given("agent"))) {
|
|
259
|
-
const sessionId = given("session") || given("id") ||
|
|
260
|
-
const targetPath = path.resolve(process.cwd(), String(opts.path || "."));
|
|
327
|
+
const sessionId = given("session") || given("id") || action.first;
|
|
328
|
+
const targetPath = path.resolve(process.cwd(), String(action.opts.path || "."));
|
|
261
329
|
const agentId =
|
|
262
330
|
given("agent") ||
|
|
263
331
|
String(env.SENTINELAYER_AGENT_ID || "").trim() ||
|
|
@@ -279,9 +347,20 @@ export async function assertDispatchMatchesScope(actionCommand, { env = process.
|
|
|
279
347
|
}
|
|
280
348
|
|
|
281
349
|
/** Run `fn` with the right credential for (sessionId, agentId). See the module header. */
|
|
282
|
-
export async function withAgentAdmission(sessionId, agentId, fn, { homeDir, now } = {}) {
|
|
350
|
+
export async function withAgentAdmission(sessionId, agentId, fn, { homeDir, now, requireStored = false } = {}) {
|
|
283
351
|
const held = await readAdmissionCredentialState(sessionId, agentId, { homeDir, now });
|
|
284
|
-
if (held.state === "none")
|
|
352
|
+
if (held.state === "none") {
|
|
353
|
+
// A caller that already decided this must bind (it saw a stored admission) never runs
|
|
354
|
+
// without one: a credential file that vanished in between is a refusal, not absence.
|
|
355
|
+
if (requireStored) {
|
|
356
|
+
throw new AdmissionCredentialRefused(
|
|
357
|
+
`Agent "${agentId}"'s stored admission for session ${sessionId} is no longer present. ` +
|
|
358
|
+
`It will not fall back to your own credentials.`,
|
|
359
|
+
{ reason: "admission_removed", sessionId, agentId },
|
|
360
|
+
);
|
|
361
|
+
}
|
|
362
|
+
return fn({ admitted: false });
|
|
363
|
+
}
|
|
285
364
|
if (held.state !== "live") {
|
|
286
365
|
const why = REFUSAL_HINT[held.reason] || `its stored admission is unusable (${held.reason})`;
|
|
287
366
|
throw new AdmissionCredentialRefused(
|
package/src/session/admission.js
CHANGED
|
@@ -30,6 +30,7 @@ import path from "node:path";
|
|
|
30
30
|
import process from "node:process";
|
|
31
31
|
import { setTimeout as sleepMs } from "node:timers/promises";
|
|
32
32
|
|
|
33
|
+
import { checkedTransport, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
33
34
|
import { requestJson, requestJsonMutation } from "../auth/http.js";
|
|
34
35
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
35
36
|
import { canonicalize } from "../engram/canonical.js";
|
|
@@ -271,6 +272,39 @@ export async function hasStoredAdmissionCredentials({ homeDir } = {}) {
|
|
|
271
272
|
return false;
|
|
272
273
|
}
|
|
273
274
|
|
|
275
|
+
/**
|
|
276
|
+
* The agents for which this machine stores an admission credential file for this
|
|
277
|
+
* session, usable or not (`<home>/.sentinelayer/agents/<agent>/admissions/<session>.json`).
|
|
278
|
+
* An unreadable agents directory is reported as a stored entry, never as absence.
|
|
279
|
+
*/
|
|
280
|
+
export async function storedAdmissionAgentsForSession(sessionId, { homeDir } = {}) {
|
|
281
|
+
let sessionFile;
|
|
282
|
+
try {
|
|
283
|
+
sessionFile = `${safeSegment(sessionId, "sessionId")}.json`;
|
|
284
|
+
} catch {
|
|
285
|
+
return []; // not a storable session id, so nothing can be stored for it
|
|
286
|
+
}
|
|
287
|
+
const agentsDir = path.join(sentinelayerHome(homeDir), "agents");
|
|
288
|
+
let agents;
|
|
289
|
+
try {
|
|
290
|
+
agents = await fsp.readdir(agentsDir, { withFileTypes: true });
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error && error.code === "ENOENT") return [];
|
|
293
|
+
return ["(unreadable)"];
|
|
294
|
+
}
|
|
295
|
+
const found = [];
|
|
296
|
+
for (const agent of agents) {
|
|
297
|
+
if (!agent.isDirectory()) continue;
|
|
298
|
+
try {
|
|
299
|
+
await fsp.access(path.join(agentsDir, agent.name, "admissions", sessionFile));
|
|
300
|
+
found.push(agent.name.replace(/%3a/g, ":"));
|
|
301
|
+
} catch (error) {
|
|
302
|
+
if (!(error && error.code === "ENOENT")) found.push(agent.name.replace(/%3a/g, ":"));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return found;
|
|
306
|
+
}
|
|
307
|
+
|
|
274
308
|
/**
|
|
275
309
|
* What this machine holds for (session, agent):
|
|
276
310
|
* { state: "none" } -- never admitted here: the legacy path applies
|
|
@@ -333,10 +367,10 @@ function requestFingerprint({ apiUrl, agentId, publicKey, goal, actions, ttlSeco
|
|
|
333
367
|
|
|
334
368
|
async function authContext({ targetPath, resolveAuthSession }) {
|
|
335
369
|
const auth = await resolveAuthSession({ cwd: targetPath, env: process.env, autoRotate: false });
|
|
336
|
-
if (!auth
|
|
370
|
+
if (!isAuthenticated(auth) || !auth?.apiUrl) {
|
|
337
371
|
throw new Error("Not authenticated. Run `sl auth login` first.");
|
|
338
372
|
}
|
|
339
|
-
return {
|
|
373
|
+
return { credential: await credentialFor(auth), apiUrl: normalizeString(auth.apiUrl).replace(/\/+$/, "") };
|
|
340
374
|
}
|
|
341
375
|
|
|
342
376
|
function admissionUrl(apiUrl, sessionId, suffix = "") {
|
|
@@ -345,12 +379,13 @@ function admissionUrl(apiUrl, sessionId, suffix = "") {
|
|
|
345
379
|
|
|
346
380
|
async function mutate(auth, sessionId, routeId, url, body, { requestMutation, operationName, origin }) {
|
|
347
381
|
const idempotencyKey = createSessionMutationIdempotencyKey(operationName);
|
|
348
|
-
return requestMutation(url, {
|
|
382
|
+
return checkedTransport(requestMutation)(url, {
|
|
349
383
|
method: "POST",
|
|
350
384
|
operationName,
|
|
351
385
|
idempotencyKey,
|
|
386
|
+
credential: auth.credential,
|
|
352
387
|
headers: createSessionMutationHeaders({
|
|
353
|
-
|
|
388
|
+
credential: auth.credential,
|
|
354
389
|
sessionId,
|
|
355
390
|
routeId,
|
|
356
391
|
idempotencyKey,
|
|
@@ -495,9 +530,9 @@ export async function runAdmissionJoin(
|
|
|
495
530
|
const deadline = now() + Math.max(0, waitTimeoutMs);
|
|
496
531
|
let polled;
|
|
497
532
|
for (;;) {
|
|
498
|
-
polled = await requestRead(admissionUrl(auth.apiUrl, sid, `/${encodeURIComponent(state.admissionId)}`), {
|
|
533
|
+
polled = await checkedTransport(requestRead)(admissionUrl(auth.apiUrl, sid, `/${encodeURIComponent(state.admissionId)}`), {
|
|
499
534
|
method: "GET",
|
|
500
|
-
|
|
535
|
+
credential: auth.credential,
|
|
501
536
|
});
|
|
502
537
|
state.status = polled.status;
|
|
503
538
|
// Identity readiness is a server capability: a server that gates the claim on
|
|
@@ -635,13 +670,13 @@ export async function fetchOwnAdmissionReceipt(
|
|
|
635
670
|
) {
|
|
636
671
|
const sid = normalizeString(sessionId);
|
|
637
672
|
const auth = await resolveAuthSession({ cwd: targetPath, env: process.env, autoRotate: false });
|
|
638
|
-
if (!auth
|
|
673
|
+
if (!isAuthenticated(auth) || auth.source !== "session_admission") {
|
|
639
674
|
throw new Error("The admission receipt must be fetched with the admission credential.");
|
|
640
675
|
}
|
|
641
676
|
const apiUrl = normalizeString(auth.apiUrl).replace(/\/+$/, "");
|
|
642
|
-
return requestRead(admissionUrl(apiUrl, sid, "/self"), {
|
|
677
|
+
return checkedTransport(requestRead)(admissionUrl(apiUrl, sid, "/self"), {
|
|
643
678
|
method: "GET",
|
|
644
|
-
|
|
679
|
+
credential: await credentialFor(auth),
|
|
645
680
|
});
|
|
646
681
|
}
|
|
647
682
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import crypto from "node:crypto";
|
|
2
2
|
import process from "node:process";
|
|
3
3
|
|
|
4
|
+
import { checkedTransport, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
4
5
|
import { SentinelayerApiError, requestJson, requestJsonMutation } from "../auth/http.js";
|
|
5
6
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
6
7
|
import { pollSessionEventsBefore } from "./sync.js";
|
|
@@ -419,12 +420,12 @@ async function resolveCheckpointApi({
|
|
|
419
420
|
env: process.env,
|
|
420
421
|
autoRotate: false,
|
|
421
422
|
});
|
|
422
|
-
if (!auth
|
|
423
|
+
if (!isAuthenticated(auth)) {
|
|
423
424
|
throw new Error("Sentinelayer auth is required. Run `sl auth login` first.");
|
|
424
425
|
}
|
|
425
426
|
return {
|
|
426
427
|
apiUrl: normalizeApiUrl(auth.apiUrl),
|
|
427
|
-
|
|
428
|
+
credential: await credentialFor(auth),
|
|
428
429
|
};
|
|
429
430
|
}
|
|
430
431
|
|
|
@@ -438,11 +439,11 @@ export async function listSessionCheckpoints(sessionId, {
|
|
|
438
439
|
if (!normalizedSessionId) {
|
|
439
440
|
throw new Error("session id is required.");
|
|
440
441
|
}
|
|
441
|
-
const { apiUrl,
|
|
442
|
+
const { apiUrl, credential } = await resolveCheckpointApi({ targetPath, resolveAuthSession });
|
|
442
443
|
const params = new URLSearchParams({ limit: String(normalizeLimit(limit)) });
|
|
443
444
|
const response = await request(
|
|
444
445
|
`${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/checkpoints?${params.toString()}`,
|
|
445
|
-
{ method: "GET",
|
|
446
|
+
{ method: "GET", credential },
|
|
446
447
|
);
|
|
447
448
|
const checkpoints = Array.isArray(response?.checkpoints) ? response.checkpoints : [];
|
|
448
449
|
return {
|
|
@@ -519,12 +520,12 @@ export async function createSessionCheckpoint(sessionId, options = {}) {
|
|
|
519
520
|
requestMutation = requestJsonMutation,
|
|
520
521
|
} = options;
|
|
521
522
|
const { body, idempotencyKey } = buildManualCheckpointPayload(normalizedSessionId, options);
|
|
522
|
-
const { apiUrl,
|
|
523
|
-
const response = await requestMutation(
|
|
523
|
+
const { apiUrl, credential } = await resolveCheckpointApi({ targetPath, resolveAuthSession });
|
|
524
|
+
const response = await checkedTransport(requestMutation)(
|
|
524
525
|
`${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/checkpoints`,
|
|
525
526
|
{
|
|
526
527
|
operationName: "session-checkpoint-create",
|
|
527
|
-
|
|
528
|
+
credential,
|
|
528
529
|
body,
|
|
529
530
|
idempotencyKey,
|
|
530
531
|
},
|
|
@@ -546,12 +547,12 @@ export async function generateSessionCheckpoint(sessionId, options = {}) {
|
|
|
546
547
|
requestMutation = requestJsonMutation,
|
|
547
548
|
} = options;
|
|
548
549
|
const { body, idempotencyKey } = buildGenerateCheckpointPayload(normalizedSessionId, options);
|
|
549
|
-
const { apiUrl,
|
|
550
|
-
const response = await requestMutation(
|
|
550
|
+
const { apiUrl, credential } = await resolveCheckpointApi({ targetPath, resolveAuthSession });
|
|
551
|
+
const response = await checkedTransport(requestMutation)(
|
|
551
552
|
`${apiUrl}/api/v1/sessions/${encodeURIComponent(normalizedSessionId)}/checkpoints/generate`,
|
|
552
553
|
{
|
|
553
554
|
operationName: "session-checkpoint-generate",
|
|
554
|
-
|
|
555
|
+
credential,
|
|
555
556
|
body,
|
|
556
557
|
idempotencyKey,
|
|
557
558
|
},
|
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
requestJson,
|
|
10
10
|
requestJsonMutation,
|
|
11
11
|
} from "../auth/http.js";
|
|
12
|
+
import { checkedTransport, credentialFor, isAuthenticated } from "../auth/credential-destinations.js";
|
|
12
13
|
import { resolveActiveAuthSession } from "../auth/service.js";
|
|
13
14
|
import { resolveSessionPaths } from "./paths.js";
|
|
14
15
|
|
|
@@ -477,14 +478,14 @@ async function resolveLeaseApi({
|
|
|
477
478
|
env: process.env,
|
|
478
479
|
autoRotate: false,
|
|
479
480
|
});
|
|
480
|
-
if (!auth
|
|
481
|
+
if (!isAuthenticated(auth) || !auth?.apiUrl) {
|
|
481
482
|
throw new Error(
|
|
482
483
|
"Authoritative session file leases require Sentinelayer auth. Run `sl auth login` first.",
|
|
483
484
|
);
|
|
484
485
|
}
|
|
485
486
|
return {
|
|
486
487
|
apiUrl: normalizeApiUrl(auth.apiUrl),
|
|
487
|
-
|
|
488
|
+
credential: await credentialFor(auth),
|
|
488
489
|
};
|
|
489
490
|
}
|
|
490
491
|
|
|
@@ -505,10 +506,10 @@ async function listRemoteLeases(
|
|
|
505
506
|
} = {},
|
|
506
507
|
) {
|
|
507
508
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
508
|
-
const { apiUrl,
|
|
509
|
+
const { apiUrl, credential } = await resolveLeaseApi({ targetPath, resolveAuthSession });
|
|
509
510
|
const response = await request(leaseCollectionUrl(apiUrl, normalizedSessionId), {
|
|
510
511
|
method: "GET",
|
|
511
|
-
|
|
512
|
+
credential,
|
|
512
513
|
});
|
|
513
514
|
if (response?.authoritative !== true || !Array.isArray(response?.leases)) {
|
|
514
515
|
throw new Error("File-lease authority returned an invalid list response; refusing local fallback.");
|
|
@@ -529,13 +530,13 @@ async function guardRemoteClaims(
|
|
|
529
530
|
) {
|
|
530
531
|
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
531
532
|
const normalizedHolderId = normalizeAgentId(holderId);
|
|
532
|
-
const { apiUrl,
|
|
533
|
-
const response = await requestMutation(
|
|
533
|
+
const { apiUrl, credential } = await resolveLeaseApi({ targetPath, resolveAuthSession });
|
|
534
|
+
const response = await checkedTransport(requestMutation)(
|
|
534
535
|
`${leaseCollectionUrl(apiUrl, normalizedSessionId)}/guard`,
|
|
535
536
|
{
|
|
536
537
|
method: "POST",
|
|
537
538
|
operationName: "session-file-lease-guard",
|
|
538
|
-
|
|
539
|
+
credential,
|
|
539
540
|
body: {
|
|
540
541
|
holderId: normalizedHolderId,
|
|
541
542
|
claims: paths.map((file, index) => ({
|
|
@@ -712,15 +713,15 @@ export async function lockFile(
|
|
|
712
713
|
}
|
|
713
714
|
|
|
714
715
|
const leaseToken = exactClaim?.leaseToken || newCapabilityToken();
|
|
715
|
-
const { apiUrl,
|
|
716
|
+
const { apiUrl, credential } = await resolveLeaseApi({ targetPath, resolveAuthSession });
|
|
716
717
|
let response;
|
|
717
718
|
try {
|
|
718
|
-
response = await requestMutation(
|
|
719
|
+
response = await checkedTransport(requestMutation)(
|
|
719
720
|
leaseCollectionUrl(apiUrl, normalizedSessionId),
|
|
720
721
|
{
|
|
721
722
|
method: "POST",
|
|
722
723
|
operationName: "session-file-lease-acquire",
|
|
723
|
-
|
|
724
|
+
credential,
|
|
724
725
|
body: {
|
|
725
726
|
path: normalizedFilePath,
|
|
726
727
|
holderId: normalizedAgentId,
|
|
@@ -792,12 +793,12 @@ export async function lockFile(
|
|
|
792
793
|
} catch (cacheError) {
|
|
793
794
|
let compensated = false;
|
|
794
795
|
try {
|
|
795
|
-
const release = await requestMutation(
|
|
796
|
+
const release = await checkedTransport(requestMutation)(
|
|
796
797
|
leaseMemberUrl(apiUrl, normalizedSessionId, lease.leaseId, "release"),
|
|
797
798
|
{
|
|
798
799
|
method: "POST",
|
|
799
800
|
operationName: "session-file-lease-acquire-compensation",
|
|
800
|
-
|
|
801
|
+
credential,
|
|
801
802
|
body: {
|
|
802
803
|
holderId: normalizedAgentId,
|
|
803
804
|
leaseToken,
|
|
@@ -845,15 +846,15 @@ async function releaseCapabilityClaim(
|
|
|
845
846
|
requestMutation,
|
|
846
847
|
},
|
|
847
848
|
) {
|
|
848
|
-
const { apiUrl,
|
|
849
|
+
const { apiUrl, credential } = await resolveLeaseApi({ targetPath, resolveAuthSession });
|
|
849
850
|
let response;
|
|
850
851
|
try {
|
|
851
|
-
response = await requestMutation(
|
|
852
|
+
response = await checkedTransport(requestMutation)(
|
|
852
853
|
leaseMemberUrl(apiUrl, sessionId, claim.leaseId, "release"),
|
|
853
854
|
{
|
|
854
855
|
method: "POST",
|
|
855
856
|
operationName: "session-file-lease-release",
|
|
856
|
-
|
|
857
|
+
credential,
|
|
857
858
|
body: {
|
|
858
859
|
holderId: agentId,
|
|
859
860
|
leaseToken: claim.leaseToken,
|
|
@@ -1007,13 +1008,13 @@ export async function renewFileLease(
|
|
|
1007
1008
|
};
|
|
1008
1009
|
}
|
|
1009
1010
|
|
|
1010
|
-
const { apiUrl,
|
|
1011
|
-
const response = await requestMutation(
|
|
1011
|
+
const { apiUrl, credential } = await resolveLeaseApi({ targetPath, resolveAuthSession });
|
|
1012
|
+
const response = await checkedTransport(requestMutation)(
|
|
1012
1013
|
leaseMemberUrl(apiUrl, normalizedSessionId, claim.leaseId, "renew"),
|
|
1013
1014
|
{
|
|
1014
1015
|
method: "POST",
|
|
1015
1016
|
operationName: "session-file-lease-renew",
|
|
1016
|
-
|
|
1017
|
+
credential,
|
|
1017
1018
|
body: {
|
|
1018
1019
|
holderId: normalizedAgentId,
|
|
1019
1020
|
leaseToken: claim.leaseToken,
|