sentinelayer-cli 0.32.4 → 0.33.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 +75 -0
- package/package.json +1 -1
- package/src/commands/mcp.js +71 -0
- package/src/mcp/cli-command-tools.js +1 -0
- package/src/mcp/token-service.js +156 -0
- package/src/session/usage-report.js +35 -6
package/README.md
CHANGED
|
@@ -46,6 +46,27 @@ sl watch run-events --run-id <run-id>
|
|
|
46
46
|
|
|
47
47
|
Windows PowerShell note: `sl` is a built-in alias for `Set-Location`. Use `sentinelayer-cli` (or short alias `slc`) instead.
|
|
48
48
|
|
|
49
|
+
## MCP Live Entry Points And Incident Response
|
|
50
|
+
|
|
51
|
+
Live MCP entry points:
|
|
52
|
+
|
|
53
|
+
- Local stdio server: `sl mcp server run --path .`
|
|
54
|
+
- Hosted token mint: `sl mcp token mint --scope "sessions:read" --ttl-seconds 300 --json`
|
|
55
|
+
- Hosted resource smoke: `POST https://api.sentinelayer.com/mcp` with JSON-RPC `tools/list`
|
|
56
|
+
- CLI bridge registry: `sl mcp registry init-cli`; credential-bearing commands are blocked from bridge execution
|
|
57
|
+
|
|
58
|
+
First-debug path:
|
|
59
|
+
|
|
60
|
+
1. Run `sl auth status`.
|
|
61
|
+
2. For local MCP, run `sl mcp server validate --file .sentinelayer/mcp/server.<id>.json`.
|
|
62
|
+
3. For hosted MCP, mint a 300-second token and smoke `/mcp` with `tools/list`.
|
|
63
|
+
4. If a bearer or bridge output is exposed, set `SENTINELAYER_MCP_CLI_BRIDGE_DISABLED=1`, restart the MCP host, revoke the requesting CLI auth session with `sl auth revoke`, and follow [docs/mcp.md](docs/mcp.md#bearer-exposure-response).
|
|
64
|
+
|
|
65
|
+
Architecture map: local stdio runtime and incident response are in
|
|
66
|
+
[docs/mcp.md](docs/mcp.md); hosted OAuth/session-seat design is in
|
|
67
|
+
[docs/mcp-hosted-connector.md](docs/mcp-hosted-connector.md); session
|
|
68
|
+
coordination behavior is in [docs/sessions.md](docs/sessions.md).
|
|
69
|
+
|
|
49
70
|
## 60-second flow
|
|
50
71
|
|
|
51
72
|
1. Trigger:
|
|
@@ -662,6 +683,7 @@ The CLI now includes deterministic MCP registry commands:
|
|
|
662
683
|
- `sl mcp registry init-cli`
|
|
663
684
|
- `sl mcp registry validate --file <path>`
|
|
664
685
|
- `sl mcp registry validate-aidenid-adapter --file <path> [--registry-file <path>]`
|
|
686
|
+
- `sl mcp token mint --scope "sessions:read sessions:usage:read" --ttl-seconds 300 --json`
|
|
665
687
|
- `sl mcp server init --id <server-id> --registry-file <path>`
|
|
666
688
|
- `sl mcp server validate --file <path>`
|
|
667
689
|
- `sl mcp server run --path .`
|
|
@@ -669,9 +691,60 @@ The CLI now includes deterministic MCP registry commands:
|
|
|
669
691
|
|
|
670
692
|
Use `init-aidenid` to scaffold an Anthropic-compatible tool schema wrapper for AIdenID provisioning APIs, then customize transport/auth before runtime wiring.
|
|
671
693
|
Use `init-aidenid-adapter` to scaffold a deterministic AIdenID provisioning API contract (tool binding -> HTTP path/method -> response field mapping) and cross-check it against the registry with `validate-aidenid-adapter`.
|
|
694
|
+
Use `token mint` to request a short-lived hosted MCP bearer token from the Sentinelayer API. Text output hides the bearer value; `--json` is the explicit operator path that includes `accessToken`. The command is blocked from the generated CLI MCP bridge.
|
|
672
695
|
Use `init-session` plus `server run` for the local stdio Senti MCP server. It exposes session inbox, durable posts, actions/replies/reactions, file locks, and attention requests to local MCP clients. Hosted Claude-web/ChatGPT connectors need a separate HTTPS/OAuth service and are not shipped by the local stdio server. See [docs/mcp.md](docs/mcp.md).
|
|
673
696
|
Use `init-cli` to generate a bridge registry for every `sl` leaf command from Commander metadata. This exposes the full CLI surface as `sl.<command.path>` tool schemas, but execution still requires a bridge-capable MCP host with OAuth/session-seat policy and human approval enforcement. The hosted connector target contract is documented in [docs/mcp-hosted-connector.md](docs/mcp-hosted-connector.md).
|
|
674
697
|
|
|
698
|
+
MCP operator verification:
|
|
699
|
+
|
|
700
|
+
1. Local stdio demo: run `sl mcp registry init-session`, then
|
|
701
|
+
`sl mcp server init --id sentinelayer-session --registry-file .sentinelayer/mcp/tool-registry.session-tools.json`,
|
|
702
|
+
then configure the MCP host to execute `sl mcp server run --path .`.
|
|
703
|
+
2. Hosted token smoke: run
|
|
704
|
+
`sl mcp token mint --scope "sessions:read sessions:usage:read" --ttl-seconds 300 --json`,
|
|
705
|
+
place `accessToken` in a local secret variable, and call hosted `/mcp`
|
|
706
|
+
`tools/list` as shown in [docs/mcp.md](docs/mcp.md).
|
|
707
|
+
3. Kill switch: set `SENTINELAYER_MCP_CLI_BRIDGE_DISABLED=1` and restart the
|
|
708
|
+
MCP host if a CLI bridge or bearer credential is exposed.
|
|
709
|
+
|
|
710
|
+
Architecture references: local stdio runtime and incident response live in
|
|
711
|
+
[docs/mcp.md](docs/mcp.md); hosted OAuth/session-seat requirements live in
|
|
712
|
+
[docs/mcp-hosted-connector.md](docs/mcp-hosted-connector.md); Senti room
|
|
713
|
+
coordination behavior lives in [docs/sessions.md](docs/sessions.md).
|
|
714
|
+
|
|
715
|
+
### MCP Live Entry Point Map
|
|
716
|
+
|
|
717
|
+
- Local stdio server: `sl mcp server run --path .`, configured from
|
|
718
|
+
`.sentinelayer/mcp/server.<id>.json`.
|
|
719
|
+
- Hosted token mint: `sl mcp token mint --scope "sessions:read" --ttl-seconds 300 --json`,
|
|
720
|
+
backed by `POST /api/v1/auth/mcp-token`.
|
|
721
|
+
- Hosted resource smoke: `POST https://api.sentinelayer.com/mcp` with
|
|
722
|
+
`Authorization: Bearer $SENTINELAYER_MCP_TOKEN` and JSON-RPC `tools/list`.
|
|
723
|
+
- Generated CLI bridge registry: `sl mcp registry init-cli`; secret-bearing
|
|
724
|
+
commands such as `sl mcp token mint` are blocked from bridge execution.
|
|
725
|
+
|
|
726
|
+
Architecture decisions:
|
|
727
|
+
|
|
728
|
+
- Local stdio and hosted HTTPS MCP are intentionally separate. Browser-hosted
|
|
729
|
+
clients cannot spawn the local process, so hosted execution must enforce
|
|
730
|
+
OAuth, session-seat binding, and server-side authorization.
|
|
731
|
+
- Hosted bearer minting is API-owned. The CLI requests a token with existing
|
|
732
|
+
auth and never signs, verifies, or derives token claims locally.
|
|
733
|
+
- The generated CLI bridge is fail-closed for credential-bearing commands; use
|
|
734
|
+
direct operator execution for token minting and the documented kill switch for
|
|
735
|
+
containment.
|
|
736
|
+
|
|
737
|
+
### MCP Incident Response And Kill Switch
|
|
738
|
+
|
|
739
|
+
If a hosted MCP bearer or bridge command output is exposed:
|
|
740
|
+
|
|
741
|
+
1. Set `SENTINELAYER_MCP_CLI_BRIDGE_DISABLED=1`.
|
|
742
|
+
2. Restart the MCP host process so the generated CLI bridge cannot execute.
|
|
743
|
+
3. Run `sentinelayer-cli auth revoke` or revoke the requesting session from the
|
|
744
|
+
dashboard, then re-authenticate with `sentinelayer-cli auth login`.
|
|
745
|
+
4. Remove the exposed value from local logs/transcripts where possible.
|
|
746
|
+
5. Verify `/mcp` rejects the old bearer after expiry or revocation.
|
|
747
|
+
|
|
675
748
|
## Plugin governance foundation (Phase 5.2 slice)
|
|
676
749
|
|
|
677
750
|
The CLI now includes deterministic plugin/template/policy pack governance commands:
|
|
@@ -1035,6 +1108,7 @@ The CLI now supports a command tree, while keeping slash-command compatibility:
|
|
|
1035
1108
|
- `sentinelayer-cli daemon map scope|list|show` builds hybrid deterministic+semantic impact scopes with import-graph overlay for daemon work items
|
|
1036
1109
|
- `sentinelayer-cli daemon reliability run|status` and `daemon maintenance status|on|off` operate the midnight synthetic lane and maintenance billboard lifecycle
|
|
1037
1110
|
- `sentinelayer-cli mcp schema|registry|server|bridge ...` manages MCP registry schema, server configs, and VS Code bridge scaffolds
|
|
1111
|
+
- `sentinelayer-cli mcp token mint --scope "sessions:read" --ttl-seconds 300 --json` mints a short-lived hosted MCP bearer; text mode hides the secret, and generated CLI bridge execution blocks this command
|
|
1038
1112
|
- `sentinelayer-cli plugin init|validate|list|order` manages plugin/template/policy packs and deterministic load-order governance
|
|
1039
1113
|
- `sentinelayer-cli policy list|use <pack-id>` manages active policy pack selection (`community`, `strict`, `compliance-soc2`, `compliance-hipaa`, plugin packs)
|
|
1040
1114
|
- `sentinelayer-cli ai provision-email` scaffolds and optionally executes AIdenID identity provisioning requests
|
|
@@ -1067,4 +1141,5 @@ Roadmap:
|
|
|
1067
1141
|
- `GitHub CLI not installed`: install `gh` or run manual fallback.
|
|
1068
1142
|
- `Invalid repo format`: use exact `owner/repo`.
|
|
1069
1143
|
- `Missing token in workflow`: ensure `.github/workflows/omar-gate.yml` maps `sentinelayer_token: ${{ secrets.SENTINELAYER_TOKEN }}`.
|
|
1144
|
+
- `Hosted MCP bearer exposed`: set `SENTINELAYER_MCP_CLI_BRIDGE_DISABLED=1`, restart the MCP host, revoke/rotate the requesting CLI auth session with `sentinelayer-cli auth revoke`, and follow [docs/mcp.md](docs/mcp.md#bearer-exposure-response).
|
|
1070
1145
|
|
package/package.json
CHANGED
package/src/commands/mcp.js
CHANGED
|
@@ -6,6 +6,7 @@ import { ZodError } from "zod";
|
|
|
6
6
|
|
|
7
7
|
import fs from "node:fs/promises";
|
|
8
8
|
|
|
9
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, SentinelayerApiError } from "../auth/http.js";
|
|
9
10
|
import {
|
|
10
11
|
buildAidenIdProvisioningAdapterTemplate,
|
|
11
12
|
buildHostedSentiSessionConnectorContract,
|
|
@@ -27,6 +28,7 @@ import {
|
|
|
27
28
|
validateMcpToolRegistry,
|
|
28
29
|
writeJsonFile,
|
|
29
30
|
} from "../mcp/registry.js";
|
|
31
|
+
import { requestHostedMcpAccessToken } from "../mcp/token-service.js";
|
|
30
32
|
import { buildSentinelayerCliRegistryTemplate } from "../mcp/cli-registry.js";
|
|
31
33
|
import { runMcpStdioServer } from "../mcp/session-stdio-server.js";
|
|
32
34
|
import { resolveOutputRoot } from "../config/service.js";
|
|
@@ -56,6 +58,14 @@ function zodIssueSummary(error) {
|
|
|
56
58
|
.join("; ");
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
function formatApiError(error) {
|
|
62
|
+
if (!(error instanceof SentinelayerApiError)) {
|
|
63
|
+
return error instanceof Error ? error.message : String(error || "Unknown error");
|
|
64
|
+
}
|
|
65
|
+
const requestId = error.requestId ? ` request_id=${error.requestId}` : "";
|
|
66
|
+
return `${error.message} [${error.code}] status=${error.status}${requestId}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
59
69
|
export function registerMcpCommand(program) {
|
|
60
70
|
const mcp = program.command("mcp").description("Manage Sentinelayer MCP registry schemas and adapters");
|
|
61
71
|
|
|
@@ -113,6 +123,67 @@ export function registerMcpCommand(program) {
|
|
|
113
123
|
}
|
|
114
124
|
});
|
|
115
125
|
|
|
126
|
+
const token = mcp.command("token").description("Mint hosted MCP resource tokens");
|
|
127
|
+
|
|
128
|
+
token
|
|
129
|
+
.command("mint")
|
|
130
|
+
.description("Mint a short-lived bearer token for the hosted Sentinelayer MCP resource")
|
|
131
|
+
.option(
|
|
132
|
+
"--scope <scopes>",
|
|
133
|
+
"Space- or comma-separated MCP scopes (API default: sessions:read)"
|
|
134
|
+
)
|
|
135
|
+
.option("--ttl-seconds <seconds>", "Token lifetime in seconds (API default and maximum are server-configured)")
|
|
136
|
+
.option("--timeout-ms <ms>", "Sentinelayer API request timeout in milliseconds", String(DEFAULT_REQUEST_TIMEOUT_MS))
|
|
137
|
+
.option("--api-url <url>", "Override Sentinelayer API base URL")
|
|
138
|
+
.option("--no-auto-rotate", "Disable stored CLI token rotation before minting")
|
|
139
|
+
.option("--json", "Emit machine-readable output, including the minted access token")
|
|
140
|
+
.action(async (options, command) => {
|
|
141
|
+
const emitJson = shouldEmitJson(options, command);
|
|
142
|
+
let result;
|
|
143
|
+
try {
|
|
144
|
+
result = await requestHostedMcpAccessToken({
|
|
145
|
+
cwd: process.cwd(),
|
|
146
|
+
env: process.env,
|
|
147
|
+
explicitApiUrl: options.apiUrl,
|
|
148
|
+
autoRotate: options.autoRotate !== false,
|
|
149
|
+
scope: options.scope,
|
|
150
|
+
ttlSeconds: options.ttlSeconds,
|
|
151
|
+
timeoutMs: options.timeoutMs,
|
|
152
|
+
});
|
|
153
|
+
} catch (error) {
|
|
154
|
+
throw new Error(formatApiError(error));
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const payload = {
|
|
158
|
+
command: "mcp token mint",
|
|
159
|
+
apiUrl: result.apiUrl,
|
|
160
|
+
authSource: result.authSource,
|
|
161
|
+
rotated: result.rotated,
|
|
162
|
+
accessToken: result.accessToken,
|
|
163
|
+
tokenType: result.tokenType,
|
|
164
|
+
expiresIn: result.expiresIn,
|
|
165
|
+
expiresAt: result.expiresAt,
|
|
166
|
+
issuer: result.issuer,
|
|
167
|
+
audience: result.audience,
|
|
168
|
+
scope: result.scope,
|
|
169
|
+
};
|
|
170
|
+
if (emitJson) {
|
|
171
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
console.log(pc.green("Minted hosted MCP bearer credential"));
|
|
176
|
+
console.log(pc.gray(`API: ${payload.apiUrl}`));
|
|
177
|
+
console.log(`Expires in: ${payload.expiresIn}s`);
|
|
178
|
+
if (payload.expiresAt) {
|
|
179
|
+
console.log(`Expires at: ${payload.expiresAt}`);
|
|
180
|
+
}
|
|
181
|
+
console.log(`Issuer: ${payload.issuer}`);
|
|
182
|
+
console.log(`Audience: ${payload.audience}`);
|
|
183
|
+
console.log(`Scope: ${payload.scope}`);
|
|
184
|
+
console.log(pc.gray("Secret value hidden in text output; use --json when you need the bearer value."));
|
|
185
|
+
});
|
|
186
|
+
|
|
116
187
|
const schema = mcp.command("schema").description("Inspect or materialize MCP tool-registry schema");
|
|
117
188
|
schema
|
|
118
189
|
.command("show")
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import process from "node:process";
|
|
2
|
+
|
|
3
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, requestJsonMutation } from "../auth/http.js";
|
|
4
|
+
import {
|
|
5
|
+
DEFAULT_API_TOKEN_TTL_DAYS,
|
|
6
|
+
DEFAULT_TOKEN_ROTATE_THRESHOLD_DAYS,
|
|
7
|
+
resolveActiveAuthSession,
|
|
8
|
+
} from "../auth/service.js";
|
|
9
|
+
import { authLoginHint } from "../ui/command-hints.js";
|
|
10
|
+
|
|
11
|
+
function normalizePositiveNumber(rawValue, field, fallbackValue) {
|
|
12
|
+
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
|
|
13
|
+
if (fallbackValue !== undefined) {
|
|
14
|
+
return fallbackValue;
|
|
15
|
+
}
|
|
16
|
+
throw new Error(`${field} must be a positive number.`);
|
|
17
|
+
}
|
|
18
|
+
const normalized = Number(rawValue);
|
|
19
|
+
if (!Number.isFinite(normalized) || normalized <= 0) {
|
|
20
|
+
throw new Error(`${field} must be a positive number.`);
|
|
21
|
+
}
|
|
22
|
+
return normalized;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function normalizeOptionalPositiveInteger(rawValue, field) {
|
|
26
|
+
if (rawValue === undefined || rawValue === null || String(rawValue).trim() === "") {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
const normalized = Number(rawValue);
|
|
30
|
+
if (!Number.isFinite(normalized) || normalized <= 0 || !Number.isInteger(normalized)) {
|
|
31
|
+
throw new Error(`${field} must be a positive integer.`);
|
|
32
|
+
}
|
|
33
|
+
return normalized;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildApiPath(apiUrl, pathSuffix) {
|
|
37
|
+
const base = String(apiUrl || "").replace(/\/+$/, "");
|
|
38
|
+
const suffix = String(pathSuffix || "").replace(/^\/+/, "");
|
|
39
|
+
return `${base}/${suffix}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function toAuthHeader(token) {
|
|
43
|
+
return {
|
|
44
|
+
Authorization: `Bearer ${String(token || "").trim()}`,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function applyScope(body, { scope = "", scopes } = {}) {
|
|
49
|
+
const normalizedScope = String(scope || "").trim();
|
|
50
|
+
const normalizedScopes = Array.isArray(scopes)
|
|
51
|
+
? scopes.map((item) => String(item || "").trim()).filter(Boolean)
|
|
52
|
+
: String(scopes || "").trim();
|
|
53
|
+
if (normalizedScope && normalizedScopes && (!Array.isArray(normalizedScopes) || normalizedScopes.length > 0)) {
|
|
54
|
+
throw new Error("Use either scope or scopes, not both.");
|
|
55
|
+
}
|
|
56
|
+
if (normalizedScope) {
|
|
57
|
+
body.scope = normalizedScope;
|
|
58
|
+
} else if (Array.isArray(normalizedScopes) && normalizedScopes.length > 0) {
|
|
59
|
+
body.scopes = normalizedScopes;
|
|
60
|
+
} else if (typeof normalizedScopes === "string" && normalizedScopes) {
|
|
61
|
+
body.scope = normalizedScopes;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Request a short-lived hosted MCP bearer credential.
|
|
67
|
+
* The CLI authenticates with its existing Sentinelayer API token; the API
|
|
68
|
+
* performs all credential minting server-side and returns the secret in-memory.
|
|
69
|
+
*
|
|
70
|
+
* @param {{
|
|
71
|
+
* cwd?: string,
|
|
72
|
+
* env?: NodeJS.ProcessEnv,
|
|
73
|
+
* explicitApiUrl?: string,
|
|
74
|
+
* autoRotate?: boolean,
|
|
75
|
+
* rotateThresholdDays?: number,
|
|
76
|
+
* tokenLabel?: string,
|
|
77
|
+
* tokenTtlDays?: number,
|
|
78
|
+
* homeDir?: string,
|
|
79
|
+
* scope?: string,
|
|
80
|
+
* scopes?: string[] | string,
|
|
81
|
+
* ttlSeconds?: number | string | null,
|
|
82
|
+
* timeoutMs?: number | string | null
|
|
83
|
+
* }} [options]
|
|
84
|
+
* @returns {Promise<{
|
|
85
|
+
* apiUrl: string,
|
|
86
|
+
* authSource: string,
|
|
87
|
+
* rotated: boolean,
|
|
88
|
+
* accessToken: string,
|
|
89
|
+
* tokenType: string,
|
|
90
|
+
* expiresIn: number,
|
|
91
|
+
* expiresAt: string,
|
|
92
|
+
* issuer: string,
|
|
93
|
+
* audience: string,
|
|
94
|
+
* scope: string
|
|
95
|
+
* }>}
|
|
96
|
+
*/
|
|
97
|
+
export async function requestHostedMcpAccessToken({
|
|
98
|
+
cwd = process.cwd(),
|
|
99
|
+
env = process.env,
|
|
100
|
+
explicitApiUrl = "",
|
|
101
|
+
autoRotate = true,
|
|
102
|
+
rotateThresholdDays = DEFAULT_TOKEN_ROTATE_THRESHOLD_DAYS,
|
|
103
|
+
tokenLabel = "",
|
|
104
|
+
tokenTtlDays = DEFAULT_API_TOKEN_TTL_DAYS,
|
|
105
|
+
homeDir,
|
|
106
|
+
scope = "",
|
|
107
|
+
scopes,
|
|
108
|
+
ttlSeconds = null,
|
|
109
|
+
timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
110
|
+
} = {}) {
|
|
111
|
+
const session = await resolveActiveAuthSession({
|
|
112
|
+
cwd,
|
|
113
|
+
env,
|
|
114
|
+
explicitApiUrl,
|
|
115
|
+
autoRotate,
|
|
116
|
+
rotateThresholdDays,
|
|
117
|
+
tokenLabel,
|
|
118
|
+
tokenTtlDays,
|
|
119
|
+
homeDir,
|
|
120
|
+
});
|
|
121
|
+
if (!session || !session.token) {
|
|
122
|
+
throw new Error(`Not authenticated. Run \`${authLoginHint()}\` first.`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const body = {};
|
|
126
|
+
applyScope(body, { scope, scopes });
|
|
127
|
+
const normalizedTtlSeconds = normalizeOptionalPositiveInteger(ttlSeconds, "ttlSeconds");
|
|
128
|
+
if (normalizedTtlSeconds !== null) {
|
|
129
|
+
body.ttl_seconds = normalizedTtlSeconds;
|
|
130
|
+
}
|
|
131
|
+
const normalizedTimeoutMs = normalizePositiveNumber(timeoutMs, "timeoutMs", DEFAULT_REQUEST_TIMEOUT_MS);
|
|
132
|
+
|
|
133
|
+
const response = await requestJsonMutation(
|
|
134
|
+
buildApiPath(session.apiUrl, "/api/v1/auth/mcp-token"),
|
|
135
|
+
{
|
|
136
|
+
method: "POST",
|
|
137
|
+
operationName: "mcp-token-mint",
|
|
138
|
+
headers: toAuthHeader(session.token),
|
|
139
|
+
body,
|
|
140
|
+
timeoutMs: normalizedTimeoutMs,
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
return {
|
|
145
|
+
apiUrl: session.apiUrl,
|
|
146
|
+
authSource: session.source,
|
|
147
|
+
rotated: Boolean(session.rotated),
|
|
148
|
+
accessToken: String(response.access_token || ""),
|
|
149
|
+
tokenType: String(response.token_type || "Bearer"),
|
|
150
|
+
expiresIn: Number(response.expires_in || 0),
|
|
151
|
+
expiresAt: String(response.expires_at || ""),
|
|
152
|
+
issuer: String(response.issuer || ""),
|
|
153
|
+
audience: String(response.audience || ""),
|
|
154
|
+
scope: String(response.scope || ""),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
@@ -124,7 +124,7 @@ function hostedRollupToJson(rollup, fallbackLabel = "") {
|
|
|
124
124
|
const hasCustomerCost = Boolean(bag.hasCustomerCost ?? bag.has_customer_cost ?? bag.customerCostUsd != null ?? bag.customer_cost_usd != null);
|
|
125
125
|
return {
|
|
126
126
|
label: normalize(bag.label ?? bag.agentId ?? bag.agent_id ?? bag.action ?? fallbackLabel) || "unknown",
|
|
127
|
-
entries: nonNegativeInt(bag.entries ?? bag.count),
|
|
127
|
+
entries: nonNegativeInt(bag.entries ?? bag.entryCount ?? bag.entry_count ?? bag.count ?? bag.interactions),
|
|
128
128
|
inputTokens: nonNegativeInt(bag.inputTokens ?? bag.input_tokens),
|
|
129
129
|
outputTokens: nonNegativeInt(bag.outputTokens ?? bag.output_tokens),
|
|
130
130
|
totalTokens: nonNegativeInt(bag.totalTokens ?? bag.total_tokens),
|
|
@@ -249,11 +249,24 @@ export function buildSessionUsageReportFromLedgerPayload({
|
|
|
249
249
|
? embedded
|
|
250
250
|
: root;
|
|
251
251
|
const entries = sourceArray(source.entries).map(hostedEntryToJson);
|
|
252
|
-
const
|
|
253
|
-
const
|
|
252
|
+
const sourceTotals = object(source.totals);
|
|
253
|
+
const totals = hostedRollupToJson(sourceTotals, "session");
|
|
254
|
+
const priceBookVersions = sourceArray(
|
|
255
|
+
source.priceBooks ||
|
|
256
|
+
source.price_books ||
|
|
257
|
+
source.priceBookVersions ||
|
|
258
|
+
source.price_book_versions ||
|
|
259
|
+
sourceTotals.priceBooks ||
|
|
260
|
+
sourceTotals.price_books ||
|
|
261
|
+
sourceTotals.priceBookVersions ||
|
|
262
|
+
sourceTotals.price_book_versions,
|
|
263
|
+
)
|
|
254
264
|
.map(normalize)
|
|
255
265
|
.filter(Boolean)
|
|
256
266
|
.sort();
|
|
267
|
+
const resolvedPriceBookVersions = priceBookVersions.length
|
|
268
|
+
? priceBookVersions
|
|
269
|
+
: [...new Set(entries.map((entry) => normalize(entry.priceBookVersion)).filter(Boolean))].sort();
|
|
257
270
|
const limit = clampRecentLimit(recentLimit);
|
|
258
271
|
return {
|
|
259
272
|
schema: SESSION_USAGE_REPORT_SCHEMA,
|
|
@@ -268,12 +281,28 @@ export function buildSessionUsageReportFromLedgerPayload({
|
|
|
268
281
|
providerCostUsd: totals.providerCostUsd,
|
|
269
282
|
customerCostUsd: totals.customerCostUsd,
|
|
270
283
|
unpriced: totals.unpriced,
|
|
271
|
-
priceBookVersions,
|
|
284
|
+
priceBookVersions: resolvedPriceBookVersions,
|
|
272
285
|
},
|
|
273
|
-
perAgent: sourceArray(
|
|
286
|
+
perAgent: sourceArray(
|
|
287
|
+
source.agents ||
|
|
288
|
+
source.perAgent ||
|
|
289
|
+
source.per_agent ||
|
|
290
|
+
source.byAgent ||
|
|
291
|
+
source.by_agent ||
|
|
292
|
+
source.agentsById ||
|
|
293
|
+
source.agents_by_id,
|
|
294
|
+
)
|
|
274
295
|
.map((rollup) => hostedRollupToJson(rollup))
|
|
275
296
|
.sort((a, b) => b.providerCostUsd - a.providerCostUsd || b.totalTokens - a.totalTokens || a.label.localeCompare(b.label)),
|
|
276
|
-
perAction: sourceArray(
|
|
297
|
+
perAction: sourceArray(
|
|
298
|
+
source.actions ||
|
|
299
|
+
source.perAction ||
|
|
300
|
+
source.per_action ||
|
|
301
|
+
source.byAction ||
|
|
302
|
+
source.by_action ||
|
|
303
|
+
source.actionsById ||
|
|
304
|
+
source.actions_by_id,
|
|
305
|
+
)
|
|
277
306
|
.map((rollup) => hostedRollupToJson(rollup))
|
|
278
307
|
.sort((a, b) => b.providerCostUsd - a.providerCostUsd || b.totalTokens - a.totalTokens || a.label.localeCompare(b.label)),
|
|
279
308
|
recentEntries: recentReportEntries(entries, limit),
|