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.
- package/README.md +29 -16
- 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 +166 -51
- 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 +160 -7
- package/src/mcp/registry.js +6 -5
- package/src/mcp/session-stdio-server.js +157 -8
- 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 +41 -10
- package/src/session/admission-auth.js +95 -16
- package/src/session/admission.js +91 -21
- package/src/session/checkpoints.js +11 -10
- package/src/session/control-events.js +2 -0
- 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/reactions.js +258 -0
- package/src/session/route-classes.js +107 -0
- package/src/session/sync.js +110 -66
- package/src/session/tickets.js +21 -19
- package/src/session/title-sync.js +4 -3
- package/src/telemetry/sync.js +8 -9
|
@@ -13,6 +13,19 @@ import { eventMatchesAgent } from "../session/listener.js";
|
|
|
13
13
|
import { isSessionControlEvent } from "../session/control-events.js";
|
|
14
14
|
import { buildObservations } from "../session/recall/observations.js";
|
|
15
15
|
import { createMemoryService } from "../engram/index.js";
|
|
16
|
+
import {
|
|
17
|
+
AdmissionCredentialRefused,
|
|
18
|
+
assertCanonicalAgentId,
|
|
19
|
+
canonicalAgentId,
|
|
20
|
+
withAgentAdmission,
|
|
21
|
+
} from "../session/admission-auth.js";
|
|
22
|
+
import { hasStoredAdmissionCredentials, storedAdmissionAgentsForSession } from "../session/admission.js";
|
|
23
|
+
import { sessionRouteClass } from "../session/route-classes.js";
|
|
24
|
+
import {
|
|
25
|
+
SESSION_REACTION_TYPES,
|
|
26
|
+
isSessionReactionType,
|
|
27
|
+
submitSessionReaction,
|
|
28
|
+
} from "../session/reactions.js";
|
|
16
29
|
import {
|
|
17
30
|
createSessionMessageAction,
|
|
18
31
|
listSessionMessageActions,
|
|
@@ -44,9 +57,12 @@ const SESSION_MESSAGE_ACTION_TYPES = new Set([
|
|
|
44
57
|
"reply",
|
|
45
58
|
"like",
|
|
46
59
|
"dislike",
|
|
60
|
+
"unlike",
|
|
61
|
+
"undislike",
|
|
47
62
|
"disregard",
|
|
48
63
|
"view",
|
|
49
64
|
]);
|
|
65
|
+
const SESSION_REACT_TYPES = Object.freeze(["ack", ...SESSION_REACTION_TYPES]);
|
|
50
66
|
const SESSION_MESSAGE_ACTION_ALIASES = new Map([["comment", "reply"]]);
|
|
51
67
|
|
|
52
68
|
function normalizeString(value) {
|
|
@@ -701,6 +717,25 @@ async function runSessionAction({
|
|
|
701
717
|
const normalizedActionType = normalizeSessionMessageActionType(actionType || input.actionType || input.action_type);
|
|
702
718
|
const target = requireSessionActionTarget(input);
|
|
703
719
|
const normalizedNote = truncateText(note || input.note || input.message || input.text || "").text;
|
|
720
|
+
if (isSessionReactionType(normalizedActionType)) {
|
|
721
|
+
// The same path as the native CLI: canonical fail-closed admission, a fresh
|
|
722
|
+
// operation key per new intent (an explicit idempotencyKey is a retry), and a
|
|
723
|
+
// truthful outcome. The raw agent id is passed so a non-canonical spelling is
|
|
724
|
+
// refused rather than silently aliased.
|
|
725
|
+
return submitSessionReaction({
|
|
726
|
+
surface: "mcp",
|
|
727
|
+
sessionId,
|
|
728
|
+
agentId: normalizeString(input.agentId || input.agent_id || input.agent),
|
|
729
|
+
reaction: normalizedActionType,
|
|
730
|
+
...target,
|
|
731
|
+
note: normalizedNote,
|
|
732
|
+
idempotencyKey: normalizeString(input.idempotencyKey || input.idempotency_key),
|
|
733
|
+
targetPath,
|
|
734
|
+
timeoutMs: normalizePositiveInteger(input.timeoutMs || input.timeout_ms, 15_000),
|
|
735
|
+
dryRun: Boolean(input.dryRun || input.dry_run),
|
|
736
|
+
createSessionMessageActionFn,
|
|
737
|
+
});
|
|
738
|
+
}
|
|
704
739
|
const idempotencyKey =
|
|
705
740
|
normalizeString(input.idempotencyKey || input.idempotency_key) ||
|
|
706
741
|
defaultActionIdempotencyKey({
|
|
@@ -858,6 +893,67 @@ async function persistSessionEvent({
|
|
|
858
893
|
};
|
|
859
894
|
}
|
|
860
895
|
|
|
896
|
+
function refuseOutsideAdmission(reason) {
|
|
897
|
+
throw new AdmissionCredentialRefused(
|
|
898
|
+
`This tool runs for an agent on this machine and needs an admission it can bind to: ${reason}. ` +
|
|
899
|
+
`It will not fall back to other credentials.`,
|
|
900
|
+
{ reason: "session_admission_not_bound" },
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* An agent context is a process that acts for an agent: SENTINELAYER_AGENT_ID is set, or
|
|
906
|
+
* this machine stores any agent admission credential (usable or not).
|
|
907
|
+
*/
|
|
908
|
+
export async function isAgentContext({ env = process.env } = {}) {
|
|
909
|
+
return Boolean(normalizeString(env.SENTINELAYER_AGENT_ID)) || (await hasStoredAdmissionCredentials());
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
/**
|
|
913
|
+
* The one credential policy for local MCP session tools, by the tool's class in
|
|
914
|
+
* SESSION_ROUTE_CLASSES (src/session/route-classes.js):
|
|
915
|
+
* - exempt-local tools, and every tool outside an agent context: run as before
|
|
916
|
+
* - owner tools: refused in an agent context
|
|
917
|
+
* - the session stores an admission on this machine (in any state): the tool binds to the
|
|
918
|
+
* admission of the agent it names (input agentId, else SENTINELAYER_AGENT_ID) and runs
|
|
919
|
+
* inside its scope; naming no agent, an agent holding none of the session's admissions,
|
|
920
|
+
* or an expired, malformed or mis-bound admission is refused before any request
|
|
921
|
+
* - the session stores none: legacy-data-plane tools run as before; control, owner and
|
|
922
|
+
* unclassified (including newly added) tools are refused before any request
|
|
923
|
+
*/
|
|
924
|
+
export async function runInSessionToolContext(name, input = {}, run, { env = process.env } = {}) {
|
|
925
|
+
const routeClass = sessionRouteClass(`mcp:${name}`);
|
|
926
|
+
if (routeClass === "exempt-local") return run(input);
|
|
927
|
+
if (!(await isAgentContext({ env }))) return run(input);
|
|
928
|
+
if (routeClass === "owner") refuseOutsideAdmission("owner actions are not available to agents");
|
|
929
|
+
const sessionId = normalizeString(input.sessionId || input.session_id || input.session);
|
|
930
|
+
if (!sessionId) refuseOutsideAdmission("no session id was given");
|
|
931
|
+
const stored = await storedAdmissionAgentsForSession(sessionId);
|
|
932
|
+
if (stored.length === 0) {
|
|
933
|
+
if (routeClass === "legacy-data-plane") return run(input);
|
|
934
|
+
refuseOutsideAdmission(`session ${sessionId} stores no admission on this machine and "${name}" needs one`);
|
|
935
|
+
}
|
|
936
|
+
const raw = normalizeString(input.agentId || input.agent_id || input.agent) || normalizeString(env.SENTINELAYER_AGENT_ID);
|
|
937
|
+
if (!raw) refuseOutsideAdmission("no agent id was given");
|
|
938
|
+
// One canonical identity for authorisation and execution (see admission-auth.js).
|
|
939
|
+
assertCanonicalAgentId(raw, "agentId");
|
|
940
|
+
const agentId = canonicalAgentId(raw);
|
|
941
|
+
if (!stored.includes(agentId)) {
|
|
942
|
+
refuseOutsideAdmission(`agent "${agentId}" holds none of the admissions stored for session ${sessionId}`);
|
|
943
|
+
}
|
|
944
|
+
// live: runs inside the admission scope; an expired, malformed or mis-bound one is refused there.
|
|
945
|
+
// The file was seen above, so one removed since then refuses too: never a fallback.
|
|
946
|
+
return withAgentAdmission(sessionId, agentId, () => run(input), { requireStored: true });
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
export function routeAgentToolsThroughAdmission(handlers, { env = process.env } = {}) {
|
|
950
|
+
for (const name of Object.keys(handlers)) {
|
|
951
|
+
const run = handlers[name];
|
|
952
|
+
handlers[name] = (input = {}) => runInSessionToolContext(name, input, run, { env });
|
|
953
|
+
}
|
|
954
|
+
return handlers;
|
|
955
|
+
}
|
|
956
|
+
|
|
861
957
|
export function createSessionMcpToolHandlers({
|
|
862
958
|
targetPath = process.cwd(),
|
|
863
959
|
pollSessionEventsFn = pollSessionEvents,
|
|
@@ -897,7 +993,7 @@ export function createSessionMcpToolHandlers({
|
|
|
897
993
|
verified: false,
|
|
898
994
|
};
|
|
899
995
|
};
|
|
900
|
-
|
|
996
|
+
const handlers = {
|
|
901
997
|
async poll_inbox(input = {}) {
|
|
902
998
|
const sessionId = requireSessionId(input);
|
|
903
999
|
const agentId = requireAgentId(input);
|
|
@@ -1105,8 +1201,8 @@ export function createSessionMcpToolHandlers({
|
|
|
1105
1201
|
|
|
1106
1202
|
async session_react(input = {}) {
|
|
1107
1203
|
const reaction = normalizeSessionMessageActionType(input.reaction || input.actionType || input.action_type);
|
|
1108
|
-
if (!
|
|
1109
|
-
throw new Error(
|
|
1204
|
+
if (!SESSION_REACT_TYPES.includes(reaction)) {
|
|
1205
|
+
throw new Error(`reaction must be one of: ${SESSION_REACT_TYPES.join(", ")}.`);
|
|
1110
1206
|
}
|
|
1111
1207
|
return runSessionAction({
|
|
1112
1208
|
input,
|
|
@@ -1267,6 +1363,7 @@ export function createSessionMcpToolHandlers({
|
|
|
1267
1363
|
});
|
|
1268
1364
|
},
|
|
1269
1365
|
};
|
|
1366
|
+
return routeAgentToolsThroughAdmission(handlers);
|
|
1270
1367
|
}
|
|
1271
1368
|
|
|
1272
1369
|
export const SESSION_MCP_TOOLS = Object.freeze([
|
|
@@ -1419,7 +1516,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1419
1516
|
name: "session_action",
|
|
1420
1517
|
title: "Record Senti Session Action",
|
|
1421
1518
|
description:
|
|
1422
|
-
"Record a low-noise message action such as ack, working_on, disregard, like, dislike, or reply; view advances the monotonic read cursor instead of appending an action.",
|
|
1519
|
+
"Record a low-noise message action such as ack, working_on, disregard, like, dislike, unlike, undislike, or reply; view advances the monotonic read cursor instead of appending an action.",
|
|
1423
1520
|
inputSchema: {
|
|
1424
1521
|
type: "object",
|
|
1425
1522
|
additionalProperties: false,
|
|
@@ -1442,7 +1539,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1442
1539
|
name: "session_react",
|
|
1443
1540
|
title: "React To Senti Message",
|
|
1444
1541
|
description:
|
|
1445
|
-
"Acknowledge or react to a target session event with ack, like, or dislike.",
|
|
1542
|
+
"Acknowledge or react to a target session event with ack, like, or dislike, or retract your own like/dislike with unlike/undislike. Reactions report an outcome and an operation key; resend that key as idempotencyKey only to retry the same intent.",
|
|
1446
1543
|
inputSchema: {
|
|
1447
1544
|
type: "object",
|
|
1448
1545
|
additionalProperties: false,
|
|
@@ -1450,7 +1547,7 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1450
1547
|
properties: {
|
|
1451
1548
|
sessionId: { type: "string", minLength: 1 },
|
|
1452
1549
|
agentId: { type: "string", minLength: 1 },
|
|
1453
|
-
reaction: { type: "string", enum: [
|
|
1550
|
+
reaction: { type: "string", enum: [...SESSION_REACT_TYPES] },
|
|
1454
1551
|
targetSequenceId: { type: "integer", minimum: 1 },
|
|
1455
1552
|
targetCursor: { type: "string" },
|
|
1456
1553
|
targetActionId: { type: "string" },
|
|
@@ -1555,13 +1652,14 @@ export const SESSION_MCP_TOOLS = Object.freeze([
|
|
|
1555
1652
|
name: "session_locks",
|
|
1556
1653
|
title: "List Senti File Locks",
|
|
1557
1654
|
description:
|
|
1558
|
-
"List active authoritative file leases for a session.",
|
|
1655
|
+
"List active authoritative file leases for a session. In an agent context it runs on the agent's admission (agentId, else SENTINELAYER_AGENT_ID).",
|
|
1559
1656
|
inputSchema: {
|
|
1560
1657
|
type: "object",
|
|
1561
1658
|
additionalProperties: false,
|
|
1562
1659
|
required: ["sessionId"],
|
|
1563
1660
|
properties: {
|
|
1564
1661
|
sessionId: { type: "string", minLength: 1 },
|
|
1662
|
+
agentId: { type: "string", minLength: 1 },
|
|
1565
1663
|
},
|
|
1566
1664
|
},
|
|
1567
1665
|
},
|
|
@@ -1642,6 +1740,45 @@ function jsonRpcError(id, code, message, data = undefined) {
|
|
|
1642
1740
|
});
|
|
1643
1741
|
}
|
|
1644
1742
|
|
|
1743
|
+
// The dispatcher enforces each tool's published policy before its handler runs. A tool that
|
|
1744
|
+
// requires human approval is refused: approval would have to be server-validated and bound to the
|
|
1745
|
+
// call's arguments, and no such approval exists yet. A runtime-blocked tool is refused too. Refused
|
|
1746
|
+
// tools are not advertised by tools/list, and a call to one gets a specific JSON-RPC error.
|
|
1747
|
+
export const MCP_HUMAN_APPROVAL_REQUIRED = Object.freeze({ code: -32001, reason: "human_approval_required" });
|
|
1748
|
+
export const MCP_TOOL_BLOCKED = Object.freeze({ code: -32002, reason: "tool_blocked" });
|
|
1749
|
+
|
|
1750
|
+
function toolRefusal(tool) {
|
|
1751
|
+
const security = isPlainObject(tool?.security) ? tool.security : {};
|
|
1752
|
+
if (security.requires_human_approval === true) return MCP_HUMAN_APPROVAL_REQUIRED.reason;
|
|
1753
|
+
if (tool?.metadata?.bridge === "cli-command" && security.requires_human_approval !== false) {
|
|
1754
|
+
return MCP_HUMAN_APPROVAL_REQUIRED.reason;
|
|
1755
|
+
}
|
|
1756
|
+
if (security.runtime_blocked === true || tool?.metadata?.blocked === true) return MCP_TOOL_BLOCKED.reason;
|
|
1757
|
+
return "";
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
function terminalCommandFor(tool, toolName) {
|
|
1761
|
+
const cliPath = Array.isArray(tool?.metadata?.cliPath) ? tool.metadata.cliPath : toolName.replace(/^sl\./, "").split(".");
|
|
1762
|
+
return `sl ${cliPath.join(" ")}`;
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1765
|
+
function refusalError(id, toolName, tool, reason) {
|
|
1766
|
+
if (reason === MCP_HUMAN_APPROVAL_REQUIRED.reason) {
|
|
1767
|
+
return jsonRpcError(
|
|
1768
|
+
id,
|
|
1769
|
+
MCP_HUMAN_APPROVAL_REQUIRED.code,
|
|
1770
|
+
`Human approval is required for ${toolName}. It is not available over the MCP bridge in this version; run \`${terminalCommandFor(tool, toolName)}\` in a terminal instead.`,
|
|
1771
|
+
{ reason, tool: toolName },
|
|
1772
|
+
);
|
|
1773
|
+
}
|
|
1774
|
+
return jsonRpcError(
|
|
1775
|
+
id,
|
|
1776
|
+
MCP_TOOL_BLOCKED.code,
|
|
1777
|
+
`${toolName} is not available over the MCP bridge; run \`${terminalCommandFor(tool, toolName)}\` in a terminal instead.`,
|
|
1778
|
+
{ reason, tool: toolName },
|
|
1779
|
+
);
|
|
1780
|
+
}
|
|
1781
|
+
|
|
1645
1782
|
export async function handleMcpJsonRpcMessage(
|
|
1646
1783
|
message,
|
|
1647
1784
|
{
|
|
@@ -1691,7 +1828,7 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1691
1828
|
if (method === "tools/list") {
|
|
1692
1829
|
if (isNotification) return null;
|
|
1693
1830
|
return jsonRpcSuccess(id, {
|
|
1694
|
-
tools: tools.map((tool) => toMcpTool(tool)),
|
|
1831
|
+
tools: tools.filter((tool) => !toolRefusal(tool)).map((tool) => toMcpTool(tool)),
|
|
1695
1832
|
});
|
|
1696
1833
|
}
|
|
1697
1834
|
|
|
@@ -1700,6 +1837,18 @@ export async function handleMcpJsonRpcMessage(
|
|
|
1700
1837
|
const toolName = normalizeString(message.params?.name);
|
|
1701
1838
|
const args = isPlainObject(message.params?.arguments) ? message.params.arguments : {};
|
|
1702
1839
|
const handler = handlers[toolName];
|
|
1840
|
+
const tool = (Array.isArray(tools) ? tools : []).find((candidate) => normalizeString(candidate?.name) === toolName);
|
|
1841
|
+
const refusal = typeof handler === "function" && tool ? toolRefusal(tool) : "";
|
|
1842
|
+
if (refusal) {
|
|
1843
|
+
return refusalError(id, toolName, tool, refusal);
|
|
1844
|
+
}
|
|
1845
|
+
if (typeof handler === "function" && !tool) {
|
|
1846
|
+
// a handler without a published definition has no policy to check: refused
|
|
1847
|
+
return jsonRpcSuccess(
|
|
1848
|
+
id,
|
|
1849
|
+
buildToolResult({ ok: false, reason: "unknown_tool", tool: toolName }, { isError: true }),
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1703
1852
|
if (typeof handler !== "function") {
|
|
1704
1853
|
return jsonRpcSuccess(
|
|
1705
1854
|
id,
|
package/src/mcp/smoke.js
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
CredentialDestinationRefused,
|
|
5
|
+
credentialedRequest,
|
|
6
|
+
isCredential,
|
|
7
|
+
userCredential,
|
|
8
|
+
} from "../auth/credential-destinations.js";
|
|
3
9
|
import { DEFAULT_REQUEST_TIMEOUT_MS } from "../auth/http.js";
|
|
4
10
|
import { requestHostedMcpAccessToken } from "./token-service.js";
|
|
5
11
|
|
|
@@ -75,7 +81,7 @@ function summarizeJsonRpcError(error, secrets = []) {
|
|
|
75
81
|
async function postJsonRpc({
|
|
76
82
|
fetchImpl,
|
|
77
83
|
url,
|
|
78
|
-
|
|
84
|
+
credential,
|
|
79
85
|
payload,
|
|
80
86
|
timeoutMs,
|
|
81
87
|
redactionSecrets = [],
|
|
@@ -83,15 +89,18 @@ async function postJsonRpc({
|
|
|
83
89
|
const controller = new AbortController();
|
|
84
90
|
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
85
91
|
try {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
92
|
+
// fetchImpl is the transport credentialedRequest calls, after its origin check.
|
|
93
|
+
const response = await credentialedRequest(
|
|
94
|
+
credential,
|
|
95
|
+
url,
|
|
96
|
+
{
|
|
97
|
+
method: "POST",
|
|
98
|
+
headers: { "Content-Type": "application/json" },
|
|
99
|
+
body: JSON.stringify(payload),
|
|
100
|
+
signal: controller.signal,
|
|
91
101
|
},
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
});
|
|
102
|
+
{ fetchImpl },
|
|
103
|
+
);
|
|
95
104
|
const text = await response.text().catch(() => "");
|
|
96
105
|
let json = null;
|
|
97
106
|
if (text) {
|
|
@@ -108,6 +117,7 @@ async function postJsonRpc({
|
|
|
108
117
|
detail: json ? "" : redactMcpSmokeText(text, redactionSecrets),
|
|
109
118
|
};
|
|
110
119
|
} catch (error) {
|
|
120
|
+
if (error instanceof CredentialDestinationRefused) throw error;
|
|
111
121
|
const aborted = Boolean(error) && error.name === "AbortError";
|
|
112
122
|
return {
|
|
113
123
|
reached: false,
|
|
@@ -172,6 +182,12 @@ export async function runHostedMcpSmoke({
|
|
|
172
182
|
if (!accessToken) {
|
|
173
183
|
throw new Error("Hosted MCP token mint succeeded but returned no access token.");
|
|
174
184
|
}
|
|
185
|
+
// The minted bearer as a credential: the mint's own hosted MCP credential, or the bare token bound
|
|
186
|
+
// to the configured API. Any other credential (an agent's admission, say) is not accepted here.
|
|
187
|
+
if (minted.credential !== undefined && !(isCredential(minted.credential) && minted.credential.source === "hosted_mcp")) {
|
|
188
|
+
throw new TypeError("The hosted MCP mint must return a hosted MCP credential.");
|
|
189
|
+
}
|
|
190
|
+
const credential = minted.credential || (await userCredential(accessToken, { env, homeDir, source: "hosted_mcp" }));
|
|
175
191
|
const redactionSecrets = [accessToken];
|
|
176
192
|
|
|
177
193
|
const mcpUrl = joinUrl(minted.apiUrl, "/mcp");
|
|
@@ -181,7 +197,7 @@ export async function runHostedMcpSmoke({
|
|
|
181
197
|
const toolsResponse = await postJsonRpc({
|
|
182
198
|
fetchImpl,
|
|
183
199
|
url: mcpUrl,
|
|
184
|
-
|
|
200
|
+
credential,
|
|
185
201
|
timeoutMs: normalizedTimeoutMs,
|
|
186
202
|
redactionSecrets,
|
|
187
203
|
payload: {
|
|
@@ -247,7 +263,7 @@ export async function runHostedMcpSmoke({
|
|
|
247
263
|
const eventsResponse = await postJsonRpc({
|
|
248
264
|
fetchImpl,
|
|
249
265
|
url: mcpUrl,
|
|
250
|
-
|
|
266
|
+
credential,
|
|
251
267
|
timeoutMs: normalizedTimeoutMs,
|
|
252
268
|
redactionSecrets,
|
|
253
269
|
payload: {
|
package/src/mcp/token-service.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import process from "node:process";
|
|
2
2
|
|
|
3
|
+
import { credentialFor, isAuthenticated, userCredential } from "../auth/credential-destinations.js";
|
|
3
4
|
import { DEFAULT_REQUEST_TIMEOUT_MS, requestJsonMutation } from "../auth/http.js";
|
|
4
5
|
import {
|
|
5
6
|
DEFAULT_API_TOKEN_TTL_DAYS,
|
|
@@ -39,12 +40,6 @@ function buildApiPath(apiUrl, pathSuffix) {
|
|
|
39
40
|
return `${base}/${suffix}`;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
function toAuthHeader(token) {
|
|
43
|
-
return {
|
|
44
|
-
Authorization: `Bearer ${String(token || "").trim()}`,
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
43
|
function applyScope(body, { scope = "", scopes } = {}) {
|
|
49
44
|
const normalizedScope = String(scope || "").trim();
|
|
50
45
|
const normalizedScopes = Array.isArray(scopes)
|
|
@@ -118,7 +113,7 @@ export async function requestHostedMcpAccessToken({
|
|
|
118
113
|
tokenTtlDays,
|
|
119
114
|
homeDir,
|
|
120
115
|
});
|
|
121
|
-
if (!session
|
|
116
|
+
if (!isAuthenticated(session)) {
|
|
122
117
|
throw new Error(`Not authenticated. Run \`${authLoginHint()}\` first.`);
|
|
123
118
|
}
|
|
124
119
|
|
|
@@ -135,17 +130,20 @@ export async function requestHostedMcpAccessToken({
|
|
|
135
130
|
{
|
|
136
131
|
method: "POST",
|
|
137
132
|
operationName: "mcp-token-mint",
|
|
138
|
-
|
|
133
|
+
credential: await credentialFor(session, { env, homeDir }),
|
|
139
134
|
body,
|
|
140
135
|
timeoutMs: normalizedTimeoutMs,
|
|
141
136
|
}
|
|
142
137
|
);
|
|
138
|
+
const accessToken = String(response.access_token || "");
|
|
143
139
|
|
|
144
140
|
return {
|
|
145
141
|
apiUrl: session.apiUrl,
|
|
146
142
|
authSource: session.source,
|
|
147
143
|
rotated: Boolean(session.rotated),
|
|
148
|
-
accessToken
|
|
144
|
+
accessToken,
|
|
145
|
+
// the minted bearer, bound to the configured API like the token that minted it
|
|
146
|
+
credential: await userCredential(accessToken, { env, homeDir, source: "hosted_mcp" }),
|
|
149
147
|
tokenType: String(response.token_type || "Bearer"),
|
|
150
148
|
expiresIn: Number(response.expires_in || 0),
|
|
151
149
|
expiresAt: String(response.expires_at || ""),
|
package/src/memory/retrieval.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fsp from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
|
|
4
|
+
import { credentialedRequest } from "../auth/credential-destinations.js";
|
|
5
|
+
|
|
4
6
|
function normalizeString(value) {
|
|
5
7
|
return String(value || "").trim();
|
|
6
8
|
}
|
|
@@ -506,6 +508,7 @@ export async function queryHybridRetriever({
|
|
|
506
508
|
provider = "local",
|
|
507
509
|
apiEndpoint = "",
|
|
508
510
|
apiKey = "",
|
|
511
|
+
credential = null,
|
|
509
512
|
fetchImpl = globalThis.fetch,
|
|
510
513
|
} = {}) {
|
|
511
514
|
const normalizedProvider = normalizeString(provider).toLowerCase() || "local";
|
|
@@ -534,12 +537,18 @@ export async function queryHybridRetriever({
|
|
|
534
537
|
}
|
|
535
538
|
|
|
536
539
|
try {
|
|
537
|
-
|
|
540
|
+
// The user's credential goes only to its own origin (credentialedRequest); a dedicated memory
|
|
541
|
+
// API key is that service's own key, sent to the endpoint configured for it.
|
|
542
|
+
const send = (init) =>
|
|
543
|
+
credential
|
|
544
|
+
? credentialedRequest(credential, String(apiEndpoint), init, { fetchImpl })
|
|
545
|
+
: fetchImpl(String(apiEndpoint), {
|
|
546
|
+
...init,
|
|
547
|
+
headers: { ...init.headers, ...(normalizeString(apiKey) ? { authorization: `Bearer ${apiKey}` } : {}) },
|
|
548
|
+
});
|
|
549
|
+
const response = await send({
|
|
538
550
|
method: "POST",
|
|
539
|
-
headers: {
|
|
540
|
-
"content-type": "application/json",
|
|
541
|
-
...(normalizeString(apiKey) ? { authorization: `Bearer ${apiKey}` } : {}),
|
|
542
|
-
},
|
|
551
|
+
headers: { "content-type": "application/json" },
|
|
543
552
|
body: JSON.stringify({
|
|
544
553
|
query: normalizeString(query),
|
|
545
554
|
limit: Math.max(1, Math.floor(normalizeNumber(limit, 12))),
|
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,
|