metergraph-cli 0.2.3 → 0.2.4
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 +78 -14
- package/assets/skill/SKILL.md +124 -31
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +31 -31
- package/assets/skills/metergraph-analyze/SKILL.md +161 -58
- package/assets/skills/metergraph-candidates/SKILL.md +50 -8
- package/assets/skills/metergraph-evals/SKILL.md +83 -16
- package/assets/skills/metergraph-investigate/SKILL.md +90 -34
- package/assets/skills/metergraph-iterate/SKILL.md +82 -12
- package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
- package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
- package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
- package/assets/skills/metergraph-report/SKILL.md +60 -15
- package/assets/skills/metergraph-workloads/SKILL.md +97 -13
- package/native/README.md +85 -0
- package/native/anchored-fs.c +243 -0
- package/native/bin/anchored-fs-darwin-arm64 +0 -0
- package/native/bin/anchored-fs-darwin-x64 +0 -0
- package/native/bin/anchored-fs-linux-arm64 +0 -0
- package/native/bin/anchored-fs-linux-x64 +0 -0
- package/native/build.mjs +55 -0
- package/native/manifest.json +9 -0
- package/native/targets.mjs +24 -0
- package/package.json +10 -3
- package/src/action-help.js +130 -0
- package/src/agent-key.js +21 -14
- package/src/anchored-fs.js +163 -0
- package/src/args.js +125 -17
- package/src/auth-binding.js +12 -7
- package/src/auth-callback.js +10 -2
- package/src/auth-login.js +111 -28
- package/src/auth-oauth.js +89 -45
- package/src/auth-session.js +19 -12
- package/src/auth-store.js +28 -7
- package/src/cli.js +52 -8
- package/src/constants.js +37 -9
- package/src/experiments-output.js +7 -6
- package/src/experiments.js +26 -17
- package/src/output.js +274 -79
- package/src/read-contract.js +42 -8
- package/src/read-output.js +66 -13
- package/src/read.js +90 -15
- package/src/repository-identity.js +2 -2
- package/src/setup-env-git.js +1 -1
- package/src/setup-env.js +16 -9
- package/src/setup-interactive.js +19 -4
- package/src/setup-refusal.js +2 -2
- package/src/setup-state.js +9 -4
- package/src/setup.js +15 -3
- package/src/skill-bundle.js +2 -2
- package/src/skill-trusted.js +56 -0
- package/src/skill.js +28 -9
- package/src/style.js +33 -7
- package/src/tools-output.js +73 -0
- package/src/tools.js +249 -0
- package/src/trace-contract.js +1 -1
- package/src/transport.js +21 -0
- package/src/verify-output.js +2 -2
- package/src/verify.js +5 -2
package/src/tools.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { AUTH_PATHS } from "./constants.js";
|
|
4
|
+
import { retryAfterOf } from "./read.js";
|
|
5
|
+
import { parseJsonObject } from "./http.js";
|
|
6
|
+
import { failureOf, send } from "./transport.js";
|
|
7
|
+
|
|
8
|
+
// tools list | call: any Agent Access tool the credential may use, through
|
|
9
|
+
// the service's MCP endpoint (POST /v1/agent/mcp), as an MCP client calls it.
|
|
10
|
+
// runRead (read.js) establishes and verifies the session first, exactly as
|
|
11
|
+
// for the read commands, so the same saved sign in or coding-agent key works
|
|
12
|
+
// here and the service decides which tools that credential sees.
|
|
13
|
+
//
|
|
14
|
+
// list sends tools/list. call sends tools/list, to learn whether the tool is
|
|
15
|
+
// offered to this credential and whether it writes or calls out, then one
|
|
16
|
+
// tools/call. A tool the service does not mark read-only, or marks as calling
|
|
17
|
+
// out (to a model provider, say), is called only with --yes, which an agent
|
|
18
|
+
// passes once the person has approved that exact call. Arguments come from
|
|
19
|
+
// --args or --args-file, never stdin. The result is printed as the service
|
|
20
|
+
// returned it, as JSON, after the caller checks it for known credentials.
|
|
21
|
+
|
|
22
|
+
export const TOOL_NAME = /^metergraph_[a-z0-9_]{1,100}$/;
|
|
23
|
+
// The service caps a tool result at 5 MiB; the envelope adds a little.
|
|
24
|
+
const TOOLS_MAX_BYTES = 6 * 1024 * 1024;
|
|
25
|
+
export const TOOL_ARGS_MAX_BYTES = 1024 * 1024;
|
|
26
|
+
const MAX_TOOLS = 500;
|
|
27
|
+
|
|
28
|
+
const stop = (outcome, reason, report = null) => ({ ok: false, outcome, reason, report });
|
|
29
|
+
|
|
30
|
+
// Resolves with { ok: true, value } or { ok: false, outcome, reason, report }.
|
|
31
|
+
export async function toolsRead(session, options, signal) {
|
|
32
|
+
const listed = await listTools(session, signal);
|
|
33
|
+
if (!listed.ok) return listed;
|
|
34
|
+
const request = options.tool;
|
|
35
|
+
if (options.action === "list") {
|
|
36
|
+
if (request.name === null) return { ok: true, value: { tools: listed.tools } };
|
|
37
|
+
const one = listed.tools.find((tool) => tool.name === request.name);
|
|
38
|
+
return one ? { ok: true, value: { tools: [one] } } : stop("capability_unavailable", "tool_unavailable");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tool = listed.tools.find((entry) => entry.name === request.name);
|
|
42
|
+
if (!tool) return stop("capability_unavailable", "tool_unavailable");
|
|
43
|
+
const args = options.toolArguments ?? readArguments(request);
|
|
44
|
+
if (!args.ok) return args;
|
|
45
|
+
if (tool.needs_confirmation && !request.yes) return stop("action_required", "confirmation_required");
|
|
46
|
+
|
|
47
|
+
const id = 2;
|
|
48
|
+
const response = await rpc(session, { jsonrpc: "2.0", id, method: "tools/call", params: { name: tool.name, arguments: args.value } }, signal);
|
|
49
|
+
// A write or a provider call that may have reached the service is never
|
|
50
|
+
// reported as a plain failure: it may have taken effect.
|
|
51
|
+
if (tool.needs_confirmation && response.kind === "error" && response.sent) {
|
|
52
|
+
return stop("unconfirmed", "tool_call_unconfirmed");
|
|
53
|
+
}
|
|
54
|
+
const answer = rpcAnswer(response, id);
|
|
55
|
+
if (!answer.ok) {
|
|
56
|
+
if (tool.needs_confirmation && response.kind === "response" && !["access_revoked", "forbidden", "rate_limited", "endpoint_unavailable"].includes(answer.reason)) return stop("unconfirmed", "tool_call_unconfirmed");
|
|
57
|
+
return answer;
|
|
58
|
+
}
|
|
59
|
+
if (answer.error !== undefined) {
|
|
60
|
+
if (tool.needs_confirmation && ![-32700, -32600, -32601, -32602].includes(answer.error.code)) return stop("unconfirmed", "tool_call_unconfirmed");
|
|
61
|
+
return rpcError(answer.error);
|
|
62
|
+
}
|
|
63
|
+
const result = answer.result;
|
|
64
|
+
if (!isObject(result) || (result.isError !== undefined && typeof result.isError !== "boolean")) {
|
|
65
|
+
return tool.needs_confirmation ? stop("unconfirmed", "tool_call_unconfirmed") : stop("verification_failed", "tool_response_invalid");
|
|
66
|
+
}
|
|
67
|
+
const document = isObject(result.structuredContent) ? result.structuredContent : result.content ?? null;
|
|
68
|
+
if (result.isError) {
|
|
69
|
+
const error = isObject(document?.error) ? document.error : {};
|
|
70
|
+
return stop("refused", "tool_error", {
|
|
71
|
+
tool_error: error,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
ok: true,
|
|
76
|
+
value: { tool: tool.name, read_only: tool.read_only, open_world: tool.open_world, result: document },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function listTools(session, signal) {
|
|
81
|
+
const tools = [];
|
|
82
|
+
const names = new Set();
|
|
83
|
+
const cursors = new Set();
|
|
84
|
+
let cursor;
|
|
85
|
+
for (let page = 0; page < MAX_TOOLS; page++) {
|
|
86
|
+
const id = 1;
|
|
87
|
+
const answer = rpcAnswer(await rpc(session, {
|
|
88
|
+
jsonrpc: "2.0", id, method: "tools/list", ...(cursor === undefined ? {} : { params: { cursor } }),
|
|
89
|
+
}, signal), id);
|
|
90
|
+
if (!answer.ok) return answer;
|
|
91
|
+
if (answer.error !== undefined) {
|
|
92
|
+
return answer.error.code === -32601 ? stop("unsupported", "tools_unavailable") : rpcError(answer.error);
|
|
93
|
+
}
|
|
94
|
+
const listed = answer.result?.tools;
|
|
95
|
+
if (!Array.isArray(listed) || tools.length + listed.length > MAX_TOOLS) return stop("verification_failed", "tool_response_invalid");
|
|
96
|
+
for (const entry of listed) {
|
|
97
|
+
if (!isObject(entry) || typeof entry.name !== "string" || !TOOL_NAME.test(entry.name) ||
|
|
98
|
+
names.has(entry.name) || !isObject(entry.inputSchema) || entry.inputSchema.type !== "object") {
|
|
99
|
+
return stop("verification_failed", "tool_response_invalid");
|
|
100
|
+
}
|
|
101
|
+
names.add(entry.name);
|
|
102
|
+
const hints = isObject(entry.annotations) ? entry.annotations : {};
|
|
103
|
+
// Without annotations (an older service), a tool counts as one that
|
|
104
|
+
// writes and calls out, so it is never called without --yes.
|
|
105
|
+
const readOnly = hints.readOnlyHint === true;
|
|
106
|
+
const openWorld = hints.openWorldHint !== false;
|
|
107
|
+
tools.push({
|
|
108
|
+
name: entry.name,
|
|
109
|
+
title: text(entry.title),
|
|
110
|
+
description: text(entry.description),
|
|
111
|
+
input_schema: entry.inputSchema,
|
|
112
|
+
read_only: readOnly,
|
|
113
|
+
open_world: openWorld,
|
|
114
|
+
needs_confirmation: !readOnly || openWorld,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const next = answer.result.nextCursor;
|
|
118
|
+
if (next === undefined) {
|
|
119
|
+
tools.sort((a, b) => a.name.localeCompare(b.name));
|
|
120
|
+
return { ok: true, tools };
|
|
121
|
+
}
|
|
122
|
+
if (typeof next !== "string" || next.length === 0 || cursors.has(next)) return stop("verification_failed", "tool_response_invalid");
|
|
123
|
+
cursors.add(next);
|
|
124
|
+
cursor = next;
|
|
125
|
+
}
|
|
126
|
+
return stop("verification_failed", "tool_response_invalid");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function rpcError(error) {
|
|
130
|
+
if ([-32700, -32600, -32601, -32602].includes(error.code)) {
|
|
131
|
+
return stop("refused", "tool_rejected", { tool_error: error });
|
|
132
|
+
}
|
|
133
|
+
return stop("unhealthy", "server_error");
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function rpc(session, message, signal) {
|
|
137
|
+
return send(session.origin, AUTH_PATHS.resource, {
|
|
138
|
+
method: "POST", signal, maxBytes: TOOLS_MAX_BYTES, bearer: session.accessToken, json: message,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// { ok: true, result } or { ok: true, error } for a JSON-RPC answer to this
|
|
143
|
+
// request, or a failure for anything else.
|
|
144
|
+
function rpcAnswer(response, id) {
|
|
145
|
+
if (response.kind === "response") {
|
|
146
|
+
if (response.status === 401) return stop("login_required", "access_revoked");
|
|
147
|
+
if (response.status === 403) return stop("permission_denied", "forbidden");
|
|
148
|
+
if (response.status === 429) return stop("rate_limited", "rate_limited", { retry_after_seconds: retryAfterOf(response) });
|
|
149
|
+
if (response.status === 404) return stop("unsupported", "endpoint_unavailable");
|
|
150
|
+
if (response.tooLarge) return stop("unsupported", "response_too_large");
|
|
151
|
+
}
|
|
152
|
+
const fault = failureOf(response);
|
|
153
|
+
if (fault !== null) return { ok: false, ...fault, report: null };
|
|
154
|
+
if (response.status !== 200) return stop("unsupported", "unexpected_status");
|
|
155
|
+
const message = parseJsonObject(response);
|
|
156
|
+
if (!boundedJson(message)) return stop("verification_failed", "tool_response_invalid");
|
|
157
|
+
if (message?.jsonrpc !== "2.0" || message.id !== id) return stop("verification_failed", "tool_response_invalid");
|
|
158
|
+
if (isObject(message.error)) return { ok: true, error: message.error };
|
|
159
|
+
return { ok: true, result: message.result };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// The call's arguments: a JSON object from --args or the file --args-file
|
|
163
|
+
// names, at most TOOL_ARGS_MAX_BYTES. No value from them is ever printed.
|
|
164
|
+
export function readArguments(request) {
|
|
165
|
+
let raw = request.args;
|
|
166
|
+
if (request.argsFile !== null) {
|
|
167
|
+
try {
|
|
168
|
+
const stats = fs.statSync(request.argsFile);
|
|
169
|
+
if (!stats.isFile()) return stop("invalid_input", "args_file_unreadable");
|
|
170
|
+
if (stats.size > TOOL_ARGS_MAX_BYTES) return stop("invalid_input", "args_too_large");
|
|
171
|
+
raw = fs.readFileSync(request.argsFile, "utf8");
|
|
172
|
+
} catch {
|
|
173
|
+
return stop("invalid_input", "args_file_unreadable");
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (raw === null) return { ok: true, value: {} };
|
|
177
|
+
if (Buffer.byteLength(raw) > TOOL_ARGS_MAX_BYTES) return stop("invalid_input", "args_too_large");
|
|
178
|
+
let value;
|
|
179
|
+
try {
|
|
180
|
+
value = JSON.parse(raw);
|
|
181
|
+
} catch {
|
|
182
|
+
return stop("invalid_input", "args_invalid");
|
|
183
|
+
}
|
|
184
|
+
return isObject(value) && boundedJson(value, true) ? { ok: true, value } : stop("invalid_input", "args_invalid");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// What an agent does next with a tools result, or undefined for the read
|
|
188
|
+
// commands' shared handling (keys, sign in, workspace, rate limits).
|
|
189
|
+
export function toolsNextAction(ctx, options) {
|
|
190
|
+
if (options.command !== "tools") return undefined;
|
|
191
|
+
const reason = ctx.reason;
|
|
192
|
+
if (reason === "confirmation_required") {
|
|
193
|
+
return {
|
|
194
|
+
kind: "confirm_tool_call",
|
|
195
|
+
message:
|
|
196
|
+
"This tool writes, starts work or calls a model provider. Ask the person to approve this exact call, " +
|
|
197
|
+
"then run the same command again with --yes. Nothing was sent.",
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
if (reason === "tool_unavailable") {
|
|
201
|
+
return {
|
|
202
|
+
kind: "list_tools",
|
|
203
|
+
message:
|
|
204
|
+
"This credential cannot use that tool here: the name is unknown, the tool needs more access (writes and " +
|
|
205
|
+
"starts need the Debug role and a workflow permission) or this deployment does not offer it. List the " +
|
|
206
|
+
"tools this credential can use.",
|
|
207
|
+
command: "metergraph tools list --json",
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (reason === "tool_error" || reason === "tool_rejected") {
|
|
211
|
+
return {
|
|
212
|
+
kind: "fix_tool_arguments",
|
|
213
|
+
message:
|
|
214
|
+
"The service refused the call; data.tool_error says why. Check the tool's input schema with " +
|
|
215
|
+
"\"metergraph tools list --name TOOL --json\", change the arguments and call it again.",
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
if (reason === "tool_call_unconfirmed") {
|
|
219
|
+
return {
|
|
220
|
+
kind: "check_before_retry",
|
|
221
|
+
message:
|
|
222
|
+
"The call reached the service, but its answer was not confirmed, so it may have taken effect. Check its " +
|
|
223
|
+
"effect with a read before calling it again.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
return undefined;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Service text kept for printing: a string, or null.
|
|
230
|
+
function text(value) {
|
|
231
|
+
return typeof value === "string" ? value : null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function isObject(value) {
|
|
235
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function boundedJson(value, numbers = false) {
|
|
239
|
+
const pending = [[value, 0]];
|
|
240
|
+
while (pending.length > 0) {
|
|
241
|
+
const [entry, depth] = pending.pop();
|
|
242
|
+
if (depth > 100) return false;
|
|
243
|
+
if (numbers && typeof entry === "number" && (!Number.isFinite(entry) || (Number.isInteger(entry) && !Number.isSafeInteger(entry)))) return false;
|
|
244
|
+
if (entry !== null && typeof entry === "object") {
|
|
245
|
+
for (const child of Object.values(entry)) pending.push([child, depth + 1]);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
}
|
package/src/trace-contract.js
CHANGED
|
@@ -66,7 +66,7 @@ export function traceReceipt(body, ctx, selected) {
|
|
|
66
66
|
if (page.truncated || page.next_cursor !== null || !e.complete) return fail("trace_evidence_incomplete");
|
|
67
67
|
const provenance = { workspace_id: ctx.workspaceId, deployment_profile: ctx.profile, generated_at: p.generated_at, source: p.source };
|
|
68
68
|
const base = {
|
|
69
|
-
origin: ctx.origin, workspace: { id: ctx.workspaceId }, deployment_profile: ctx.profile,
|
|
69
|
+
origin: ctx.origin, workspace: { id: ctx.workspaceId, ...(ctx.workspaceLabel ?? {}) }, deployment_profile: ctx.profile,
|
|
70
70
|
provenance, invocation: { trace_id: selected.traceId, request_id: selected.requestId, since: selected.since, until: selected.until, source: selected.source },
|
|
71
71
|
request_match: requestMatch, warnings, complete: true, content_included: false,
|
|
72
72
|
application_traffic_verified: false,
|
package/src/transport.js
CHANGED
|
@@ -62,6 +62,27 @@ export function send(
|
|
|
62
62
|
) {
|
|
63
63
|
const url = requestUrl(origin, pathname, query);
|
|
64
64
|
|
|
65
|
+
// A loopback port is not proof of the local bundle's identity. Until a
|
|
66
|
+
// verified Docker transport is selected, never put credentials, OAuth
|
|
67
|
+
// exchanges, or bootstrap state on the host's plaintext HTTP listener.
|
|
68
|
+
// Public pricing has an explicit opt-in. The credential-free reference
|
|
69
|
+
// GET API is loopback-only and retains its isolated agent (no proxy).
|
|
70
|
+
const publicPricing = useGlobalAgent && bearer === null && form === null && (
|
|
71
|
+
(pathname === "/v1/pricing/models" && method === "GET" && json === null) ||
|
|
72
|
+
(["/v1/pricing/lookup", "/v1/pricing/calculate"].includes(pathname) && method === "POST")
|
|
73
|
+
);
|
|
74
|
+
const referencePricing = method === "GET" && bearer === null && form === null && json === null &&
|
|
75
|
+
["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) &&
|
|
76
|
+
["/v1/pricing/lookup", "/v1/pricing/cost", "/v1/pricing/history", "/v1/pricing/market"].includes(pathname);
|
|
77
|
+
// These public probes report reachability only; their replies never
|
|
78
|
+
// authenticate the service or authorize a later credential exchange.
|
|
79
|
+
const connectionProbe = method === "GET" && bearer === null && form === null && json === null &&
|
|
80
|
+
!useGlobalAgent && ["localhost", "127.0.0.1", "[::1]"].includes(url.hostname) &&
|
|
81
|
+
["/healthz", "/v1/deployment"].includes(pathname);
|
|
82
|
+
if (url.protocol === "http:" && !publicPricing && !referencePricing && !connectionProbe) {
|
|
83
|
+
return Promise.resolve({ kind: "error", reason: "local_docker_transport_required", status: null, sent: false });
|
|
84
|
+
}
|
|
85
|
+
|
|
65
86
|
const headers = {
|
|
66
87
|
accept: "application/json",
|
|
67
88
|
"accept-encoding": "identity",
|
package/src/verify-output.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { PLAIN, fields, headline, paragraph, render, steps, toneFor } from "./style.js";
|
|
1
|
+
import { PLAIN, fields, headline, paragraph, render, steps, toneFor, workspaceShown } from "./style.js";
|
|
2
2
|
|
|
3
3
|
const MESSAGES = Object.freeze({
|
|
4
4
|
invalid_trace_identity: "Provide exactly one exact trace or request ID from your application run.",
|
|
@@ -27,7 +27,7 @@ export function verifyText(result, style = PLAIN) {
|
|
|
27
27
|
const lines = [headline(style, toneFor(result.outcome), title, result.outcome), ""];
|
|
28
28
|
if (data) {
|
|
29
29
|
lines.push(...fields(style, [
|
|
30
|
-
["Workspace", data.workspace
|
|
30
|
+
["Workspace", workspaceShown(data.workspace)],
|
|
31
31
|
["Deployment", data.deployment_profile],
|
|
32
32
|
["Trace ID", data.trace?.trace_id ?? null],
|
|
33
33
|
["Invocation source", data.invocation.source],
|
package/src/verify.js
CHANGED
|
@@ -3,7 +3,7 @@ import { verifiedSession } from "./auth-session.js";
|
|
|
3
3
|
import { Stop } from "./auth-store.js";
|
|
4
4
|
import { AUTH_PATHS, READ_MAX_BYTES } from "./constants.js";
|
|
5
5
|
import { parseJsonObject } from "./http.js";
|
|
6
|
-
import { capabilitySummary } from "./read-contract.js";
|
|
6
|
+
import { capabilitySummary, workspaceLabel } from "./read-contract.js";
|
|
7
7
|
import { deadline, failureOf, send, trapSignals } from "./transport.js";
|
|
8
8
|
import { traceReceipt, traceSelection } from "./trace-contract.js";
|
|
9
9
|
import { openTrace } from "./trace-open.js";
|
|
@@ -47,7 +47,10 @@ export async function runVerify(options, dependencies = {}) {
|
|
|
47
47
|
if (!sessionResult.ok) return end(sessionResult.outcome, sessionResult.reason);
|
|
48
48
|
known = sessionResult.knownCredentials;
|
|
49
49
|
const session = sessionResult.session;
|
|
50
|
-
const ctx = {
|
|
50
|
+
const ctx = {
|
|
51
|
+
origin: session.origin, workspaceId: session.workspaceId, profile: session.profile,
|
|
52
|
+
workspaceLabel: workspaceLabel(sessionResult.documents?.workspace?.workspace),
|
|
53
|
+
};
|
|
51
54
|
const capabilities = capabilitySummary(sessionResult.documents.capabilities, ctx, new Set());
|
|
52
55
|
if (!capabilities.ok) return end(capabilities.outcome, capabilities.reason);
|
|
53
56
|
if (capabilities.value.agent.trace_metadata?.available !== true) return end("capability_unavailable", "capability_unavailable");
|