@thalovant/mcp-server 0.1.10 → 0.1.11
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/.env.example +16 -0
- package/CHANGELOG.md +11 -0
- package/README.md +15 -1
- package/dist/index.js +123 -14
- package/package.json +1 -1
package/.env.example
CHANGED
|
@@ -54,6 +54,22 @@ MCP_HTTP_AUTH_TOKEN=
|
|
|
54
54
|
# deployment where a model drives the tools with a long-lived token.
|
|
55
55
|
# THALOVANT_ENABLE_DESTRUCTIVE_TOOLS=false
|
|
56
56
|
|
|
57
|
+
# Non-catalog skill sources (e.g. thalovant_install_runtime_group_skill with
|
|
58
|
+
# sourceType "git" and an arbitrary sourceRef) can pull unvetted code into a
|
|
59
|
+
# production runtime. They are refused unless this is explicitly enabled. Leave
|
|
60
|
+
# it unset whenever a model drives the tools with a long-lived token.
|
|
61
|
+
# THALOVANT_ENABLE_GIT_SKILL_SOURCES=false
|
|
62
|
+
|
|
63
|
+
# Directory that thalovant_create_client_identity may write identity files into.
|
|
64
|
+
# savePath is confined to this directory; absolute paths outside it and ".."
|
|
65
|
+
# traversal are rejected. Defaults to <config-dir>/thalovant/identities.
|
|
66
|
+
# THALOVANT_MCP_IDENTITY_DIR=/var/lib/thalovant-mcp/identities
|
|
67
|
+
|
|
68
|
+
# Read-only mode: register ONLY tools annotated readOnlyHint:true, so a model
|
|
69
|
+
# cannot mutate control-plane or runtime state. Safer default for observe-only
|
|
70
|
+
# agents than hand-writing a denylist.
|
|
71
|
+
# THALOVANT_MCP_READONLY=false
|
|
72
|
+
|
|
57
73
|
# Optional policy and audit controls.
|
|
58
74
|
# MCP_TOOL_ALLOWLIST=thalovant_*
|
|
59
75
|
# MCP_TOOL_DENYLIST=thalovant_delete_memory_item
|
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
### Security
|
|
6
|
+
|
|
7
|
+
- `thalovant_install_runtime_group_skill` now refuses non-catalog install sources by default. A `sourceType` other than `catalog` (notably `git` with an arbitrary `sourceRef`) can pull unvetted code into a production runtime, and the control-plane validator is format-only with no host allowlist, so it is rejected before any control-plane request unless the operator sets `THALOVANT_ENABLE_GIT_SKILL_SOURCES` (accepting `1`/`true`/`yes`/`on`), mirroring the `THALOVANT_ENABLE_DESTRUCTIVE_TOOLS` gate. `sourceType` is trimmed and case-folded before it is both checked and forwarded, so a variant such as `" Catalog "` or `"CATALOG"` cannot pass the local gate yet reach the control plane — which special-cases only the exact string `"catalog"` — as an unrecognized non-catalog source. The tool is now annotated `destructiveHint: true`, and `thalovant_config_status` reports `gitSkillSourcesEnabled`.
|
|
8
|
+
- `thalovant_create_client_identity` now confines the optional `savePath` to a configurable identity directory (`THALOVANT_MCP_IDENTITY_DIR`, default `<config-dir>/thalovant/identities`). Absolute paths outside the directory and `..` traversal are rejected, and the destination is validated before the identity is created in the control plane, so a model can no longer drop a `0600` credential file into a git working tree or synced folder. `thalovant_config_status` reports `identityDir`.
|
|
9
|
+
- `thalovant_healthcheck` output is now passed through `redactSecrets`, matching every other tool output.
|
|
10
|
+
- `thalovant_get_analytics_overview` no longer advertises the `admin` and (admin-only) `ownerId` arguments to the model, so it no longer teaches an admin mode that only ever 403s for non-admin callers; only the plain overview call remains. Injected `admin`/`ownerId` values are stripped and never reach the control plane.
|
|
11
|
+
- Extended output redaction to additional secret-ish keys: `device_code`, `user_code`, `psk`, `cert`, and `jwt`.
|
|
12
|
+
- Added an opt-in read-only mode (`THALOVANT_MCP_READONLY`): only tools annotated `readOnlyHint: true` are registered, so an observe-only agent needs no hand-written denylist. `thalovant_config_status` reports `readOnly`.
|
|
13
|
+
|
|
3
14
|
## 0.1.10
|
|
4
15
|
|
|
5
16
|
- Added hub-provisioning and skill-discovery tools so an agent can discover skills and provision hubs conversationally, all delegating to `@thalovant/sdk` (now `^0.2.28`).
|
package/README.md
CHANGED
|
@@ -353,7 +353,7 @@ Destructive, **not registered unless explicitly enabled** (see [Destructive Tool
|
|
|
353
353
|
- `thalovant_delete_hub`
|
|
354
354
|
- `thalovant_delete_runtime_group`
|
|
355
355
|
|
|
356
|
-
Tool outputs redact credential-shaped fields. `thalovant_create_client_identity` does not return secret identity material; pass `savePath` when you want the full identity written to a local file with mode `0600`.
|
|
356
|
+
Tool outputs redact credential-shaped fields. `thalovant_create_client_identity` does not return secret identity material; pass `savePath` when you want the full identity written to a local file with mode `0600`. `savePath` is confined to the server's identity directory (`THALOVANT_MCP_IDENTITY_DIR`, default `<config-dir>/thalovant/identities`): pass a plain filename, since absolute paths outside that directory and `..` traversal are rejected, so a model cannot drop a credential file into a git working tree or synced folder. `thalovant_config_status` reports the active `identityDir`.
|
|
357
357
|
|
|
358
358
|
## Destructive Tools
|
|
359
359
|
|
|
@@ -379,6 +379,20 @@ with the trusted principal's credential file granting them back via `allowedTool
|
|
|
379
379
|
|
|
380
380
|
Deleting a hub still requires a current etag (`412` otherwise), and deleting a runtime group fails with `409` while it is the workspace default or still has hubs attached.
|
|
381
381
|
|
|
382
|
+
## Non-Catalog Skill Sources
|
|
383
|
+
|
|
384
|
+
`thalovant_install_runtime_group_skill` installs from the vetted marketplace catalog by default. Any other source — notably `sourceType: "git"` with an arbitrary `sourceRef` repository URL — pulls code the marketplace never reviewed straight into a production runtime, and the control-plane validator is format-only with no host allowlist. Because the tools are driven by a model holding a long-lived token, non-catalog sources are refused unless an operator opts in:
|
|
385
|
+
|
|
386
|
+
```bash
|
|
387
|
+
export THALOVANT_ENABLE_GIT_SKILL_SOURCES="true"
|
|
388
|
+
```
|
|
389
|
+
|
|
390
|
+
With the flag unset, a call with any `sourceType` other than `catalog` fails before any control-plane request is made. Accepted true values are `1`, `true`, `yes`, and `on`. `thalovant_config_status` reports the state as `gitSkillSourcesEnabled`. The tool is annotated `destructiveHint: true`.
|
|
391
|
+
|
|
392
|
+
## Read-Only Mode
|
|
393
|
+
|
|
394
|
+
Set `THALOVANT_MCP_READONLY=1` to register only tools annotated `readOnlyHint: true`. Write and destructive tools are then never registered and never appear in `tools/list`, so an operator can run an observe-only agent without hand-writing a denylist. Like the other registration-time gates it is read when a server instance is created; `thalovant_config_status` reports the state as `readOnly`.
|
|
395
|
+
|
|
382
396
|
## Development
|
|
383
397
|
|
|
384
398
|
```bash
|
package/dist/index.js
CHANGED
|
@@ -9,7 +9,8 @@ import { createHash, randomUUID, timingSafeEqual } from "node:crypto";
|
|
|
9
9
|
import { createServer as createHttpServer } from "node:http";
|
|
10
10
|
import { realpathSync } from "node:fs";
|
|
11
11
|
import { appendFile, readFile, writeFile } from "node:fs/promises";
|
|
12
|
-
import { dirname, resolve } from "node:path";
|
|
12
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
13
|
+
import { homedir } from "node:os";
|
|
13
14
|
import { mkdir } from "node:fs/promises";
|
|
14
15
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
15
16
|
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
@@ -42,9 +43,16 @@ const SECRET_KEYS = [
|
|
|
42
43
|
"privateKey",
|
|
43
44
|
"refresh_token",
|
|
44
45
|
"refreshToken",
|
|
46
|
+
"device_code",
|
|
47
|
+
"deviceCode",
|
|
48
|
+
"user_code",
|
|
49
|
+
"userCode",
|
|
45
50
|
"authToken",
|
|
46
51
|
"authorization",
|
|
47
52
|
"bearer",
|
|
53
|
+
"jwt",
|
|
54
|
+
"psk",
|
|
55
|
+
"cert",
|
|
48
56
|
"token",
|
|
49
57
|
"secret",
|
|
50
58
|
];
|
|
@@ -128,6 +136,38 @@ const DESTRUCTIVE_TOOLS = ["thalovant_delete_hub", "thalovant_delete_runtime_gro
|
|
|
128
136
|
function destructiveToolsEnabled() {
|
|
129
137
|
return parseBool(process.env.THALOVANT_ENABLE_DESTRUCTIVE_TOOLS, false);
|
|
130
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* Non-catalog skill sources are opt-in.
|
|
141
|
+
*
|
|
142
|
+
* thalovant_install_runtime_group_skill can install a skill from a source other
|
|
143
|
+
* than the vetted marketplace catalog — notably `sourceType: "git"` with an
|
|
144
|
+
* arbitrary `sourceRef` repository URL. The control-plane validator is
|
|
145
|
+
* format-only with no host allowlist, so any non-catalog source is effectively
|
|
146
|
+
* an arbitrary-code-into-the-runtime primitive. Because this server hands its
|
|
147
|
+
* tools to an LLM holding a long-lived token, that primitive is refused by
|
|
148
|
+
* default and only enabled when an operator opts in with this flag, mirroring
|
|
149
|
+
* the THALOVANT_ENABLE_DESTRUCTIVE_TOOLS gate. Installing from the catalog (the
|
|
150
|
+
* default source) is always allowed.
|
|
151
|
+
*/
|
|
152
|
+
function gitSkillSourcesEnabled() {
|
|
153
|
+
return parseBool(process.env.THALOVANT_ENABLE_GIT_SKILL_SOURCES, false);
|
|
154
|
+
}
|
|
155
|
+
/** The default install source is the vetted marketplace catalog. */
|
|
156
|
+
function isCatalogSource(sourceType) {
|
|
157
|
+
return !sourceType || sourceType.trim().toLowerCase() === "catalog";
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Read-only mode.
|
|
161
|
+
*
|
|
162
|
+
* When THALOVANT_MCP_READONLY is set, only tools annotated readOnlyHint: true
|
|
163
|
+
* are registered, so an operator can run a safe agent without hand-writing a
|
|
164
|
+
* tool denylist. Non-read-only tools are never registered and so never appear
|
|
165
|
+
* in tools/list. This complements, and is independent of, the per-principal
|
|
166
|
+
* allow/deny policy in authorizeTool().
|
|
167
|
+
*/
|
|
168
|
+
function readOnlyModeEnabled() {
|
|
169
|
+
return parseBool(process.env.THALOVANT_MCP_READONLY, false);
|
|
170
|
+
}
|
|
131
171
|
const capacityProfileSchema = z
|
|
132
172
|
.enum(["standard", "autoscaling"])
|
|
133
173
|
.describe('Hub capacity profile. Only "standard" (the API default, 1 replica) and "autoscaling" (2-32 replicas) are accepted. Omit the key entirely rather than sending null — an explicit null fails 422 INVALID_HUB_CAPACITY_PROFILE. "autoscaling" is plan-gated: 403 HUB_AUTOSCALING_NOT_INCLUDED when the plan has no autoscaling, 409 AUTOSCALING_HUB_LIMIT_REACHED when its autoscaling slots are already used.');
|
|
@@ -467,6 +507,13 @@ async function auditLog(event) {
|
|
|
467
507
|
}
|
|
468
508
|
}
|
|
469
509
|
function registerThalovantTool(server, name, config, handler) {
|
|
510
|
+
if (readOnlyModeEnabled()) {
|
|
511
|
+
const annotations = (config.annotations ?? {});
|
|
512
|
+
if (annotations.readOnlyHint !== true) {
|
|
513
|
+
// Read-only mode: do not register write/destructive tools at all.
|
|
514
|
+
return;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
470
517
|
server.registerTool(name, config, async (args, extra) => {
|
|
471
518
|
const principal = principalFromExtra(extra);
|
|
472
519
|
const start = Date.now();
|
|
@@ -619,8 +666,53 @@ function summarizeReply(reply) {
|
|
|
619
666
|
failureEvent: reply.failureEvent ? redactSecrets(reply.failureEvent.asObject()) : undefined,
|
|
620
667
|
};
|
|
621
668
|
}
|
|
622
|
-
|
|
623
|
-
|
|
669
|
+
/**
|
|
670
|
+
* Base directory that saved client-identity files are confined to.
|
|
671
|
+
*
|
|
672
|
+
* thalovant_create_client_identity takes an LLM-chosen savePath. The file is
|
|
673
|
+
* written 0600, but without a base directory the model controls WHERE the
|
|
674
|
+
* secret lands and could drop a credential file into a git working tree or a
|
|
675
|
+
* synced folder. Files are therefore confined to THALOVANT_MCP_IDENTITY_DIR,
|
|
676
|
+
* defaulting to an `identities` folder under the Thalovant SDK config dir
|
|
677
|
+
* ($XDG_CONFIG_HOME/thalovant, %APPDATA%/Thalovant on Windows, else
|
|
678
|
+
* ~/.config/thalovant), matching the SDK's own convention.
|
|
679
|
+
*/
|
|
680
|
+
function identityBaseDir() {
|
|
681
|
+
const override = process.env.THALOVANT_MCP_IDENTITY_DIR?.trim();
|
|
682
|
+
if (override)
|
|
683
|
+
return resolve(override);
|
|
684
|
+
if (process.env.XDG_CONFIG_HOME)
|
|
685
|
+
return join(process.env.XDG_CONFIG_HOME, "thalovant", "identities");
|
|
686
|
+
if (process.platform === "win32" && process.env.APPDATA)
|
|
687
|
+
return join(process.env.APPDATA, "Thalovant", "identities");
|
|
688
|
+
return join(homedir(), ".config", "thalovant", "identities");
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Resolve an LLM-supplied savePath against the identity base directory and
|
|
692
|
+
* refuse anything that escapes it. Relative paths resolve inside the base dir;
|
|
693
|
+
* absolute paths and `..` traversal that land outside it are rejected. The base
|
|
694
|
+
* dir is realpath'd so a symlinked base still validates correctly.
|
|
695
|
+
*/
|
|
696
|
+
function resolveIdentityPath(savePath) {
|
|
697
|
+
const baseDir = identityBaseDir();
|
|
698
|
+
let canonicalBase = resolve(baseDir);
|
|
699
|
+
try {
|
|
700
|
+
canonicalBase = realpathSync(canonicalBase);
|
|
701
|
+
}
|
|
702
|
+
catch {
|
|
703
|
+
// Base dir does not exist yet; writeIdentityFile creates it. Use the resolved
|
|
704
|
+
// (non-canonical) path for containment — nothing is symlinked yet.
|
|
705
|
+
}
|
|
706
|
+
const candidate = resolve(canonicalBase, savePath);
|
|
707
|
+
const rel = relative(canonicalBase, candidate);
|
|
708
|
+
if (rel === "" || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
|
|
709
|
+
throw new Error(`savePath must resolve to a location inside the identity directory (${canonicalBase}). ` +
|
|
710
|
+
`Absolute paths outside it and ".." traversal are rejected; pass a plain filename such as "my-hub.json". ` +
|
|
711
|
+
`Set THALOVANT_MCP_IDENTITY_DIR to change the identity directory.`);
|
|
712
|
+
}
|
|
713
|
+
return candidate;
|
|
714
|
+
}
|
|
715
|
+
async function writeIdentityFile(resolvedPath, identity) {
|
|
624
716
|
await mkdir(dirname(resolvedPath), { recursive: true, mode: 0o700 });
|
|
625
717
|
await writeFile(resolvedPath, `${stableStringify(identity.asObject(true))}\n`, { mode: 0o600 });
|
|
626
718
|
return resolvedPath;
|
|
@@ -1017,7 +1109,10 @@ export function createServer() {
|
|
|
1017
1109
|
defaultProtocol: "wss",
|
|
1018
1110
|
destructiveToolsEnabled: destructiveToolsEnabled(),
|
|
1019
1111
|
destructiveTools: destructiveToolsEnabled() ? [...DESTRUCTIVE_TOOLS] : [],
|
|
1020
|
-
|
|
1112
|
+
gitSkillSourcesEnabled: gitSkillSourcesEnabled(),
|
|
1113
|
+
readOnly: readOnlyModeEnabled(),
|
|
1114
|
+
identityDir: identityBaseDir(),
|
|
1115
|
+
secretHandling: "Secrets are redacted in tool output. Identity files are only written when savePath is provided, and only inside identityDir.",
|
|
1021
1116
|
}));
|
|
1022
1117
|
registerThalovantTool(server, "thalovant_list_public_hubs", {
|
|
1023
1118
|
title: "List Public Hubs",
|
|
@@ -1107,7 +1202,7 @@ export function createServer() {
|
|
|
1107
1202
|
.string()
|
|
1108
1203
|
.min(1)
|
|
1109
1204
|
.optional()
|
|
1110
|
-
.describe("Optional
|
|
1205
|
+
.describe("Optional filename for the full secret identity JSON, written 0600 inside the server's identity directory (see identityDir in thalovant_config_status). Pass a plain filename like \"my-hub.json\"; absolute paths outside the identity directory and \"..\" traversal are rejected."),
|
|
1111
1206
|
},
|
|
1112
1207
|
annotations: {
|
|
1113
1208
|
readOnlyHint: false,
|
|
@@ -1116,6 +1211,10 @@ export function createServer() {
|
|
|
1116
1211
|
openWorldHint: true,
|
|
1117
1212
|
},
|
|
1118
1213
|
}, async ({ hubId, name, siteId, ownerId, active, preferredProtocols, idempotencyKey, spec, savePath, ...auth }) => {
|
|
1214
|
+
// Validate the destination BEFORE creating a cloud identity, so a path
|
|
1215
|
+
// that escapes the identity directory is refused without leaving an
|
|
1216
|
+
// orphaned credential behind in the control plane.
|
|
1217
|
+
const targetIdentityPath = savePath ? resolveIdentityPath(savePath) : undefined;
|
|
1119
1218
|
const api = await createControlPlane(auth);
|
|
1120
1219
|
ensureAuthenticated(api);
|
|
1121
1220
|
const result = await api.createClientIdentity(hubId, {
|
|
@@ -1128,7 +1227,7 @@ export function createServer() {
|
|
|
1128
1227
|
spec,
|
|
1129
1228
|
});
|
|
1130
1229
|
const selectedEndpoint = api.requireRuntimeProtocol(result, preferredProtocols[0]);
|
|
1131
|
-
const savedIdentityPath =
|
|
1230
|
+
const savedIdentityPath = targetIdentityPath ? await writeIdentityFile(targetIdentityPath, result.identity) : undefined;
|
|
1132
1231
|
return jsonContent({
|
|
1133
1232
|
result: redactSecrets(result.asObject({ includeSecrets: false })),
|
|
1134
1233
|
selectedEndpoint,
|
|
@@ -1180,7 +1279,7 @@ export function createServer() {
|
|
|
1180
1279
|
const client = await createRuntimeClient(runtime);
|
|
1181
1280
|
try {
|
|
1182
1281
|
await client.connect(clampTimeout(timeoutMs));
|
|
1183
|
-
return jsonContent(client.healthcheck());
|
|
1282
|
+
return jsonContent(redactSecrets(client.healthcheck()));
|
|
1184
1283
|
}
|
|
1185
1284
|
finally {
|
|
1186
1285
|
await client.close();
|
|
@@ -1355,10 +1454,8 @@ export function createServer() {
|
|
|
1355
1454
|
description: "Read authenticated Thalovant analytics overview data.",
|
|
1356
1455
|
inputSchema: {
|
|
1357
1456
|
...controlPlaneSchema,
|
|
1358
|
-
admin: z.boolean().optional(),
|
|
1359
1457
|
range: z.string().min(1).optional(),
|
|
1360
1458
|
bucket: z.string().min(1).optional(),
|
|
1361
|
-
ownerId: z.string().min(1).optional(),
|
|
1362
1459
|
hubId: z.string().min(1).optional(),
|
|
1363
1460
|
clientId: z.string().min(1).optional(),
|
|
1364
1461
|
country: z.string().min(1).optional(),
|
|
@@ -1886,7 +1983,7 @@ export function createServer() {
|
|
|
1886
1983
|
});
|
|
1887
1984
|
registerThalovantTool(server, "thalovant_install_runtime_group_skill", {
|
|
1888
1985
|
title: "Install Runtime Group Skill",
|
|
1889
|
-
description: "Install (or re-install) a skill in a Thalovant runtime group. Installing a skill that is already present updates the existing entry rather than failing. Discover skillId with thalovant_list_marketplace_skills, then confirm installable/purchase_required with thalovant_list_runtime_group_marketplace before calling this. Requires the hubs:write scope and a paid plan (402 'API access requires a paid plan'); a paid marketplace skill ALSO needs marketplace access on the tenant plan, which fails with a second, distinct 402 about paid marketplace access.",
|
|
1986
|
+
description: "Install (or re-install) a skill in a Thalovant runtime group from the marketplace catalog. Installing a skill that is already present updates the existing entry rather than failing. Discover skillId with thalovant_list_marketplace_skills, then confirm installable/purchase_required with thalovant_list_runtime_group_marketplace before calling this. Requires the hubs:write scope and a paid plan (402 'API access requires a paid plan'); a paid marketplace skill ALSO needs marketplace access on the tenant plan, which fails with a second, distinct 402 about paid marketplace access. Only catalog sources are allowed by default: installing from a non-catalog source such as sourceType \"git\" runs code the marketplace never vetted and is refused unless the operator sets THALOVANT_ENABLE_GIT_SKILL_SOURCES.",
|
|
1890
1987
|
inputSchema: {
|
|
1891
1988
|
...controlPlaneSchema,
|
|
1892
1989
|
runtimeGroupId: z.string().min(1).describe("Runtime group UUID."),
|
|
@@ -1901,8 +1998,8 @@ export function createServer() {
|
|
|
1901
1998
|
.min(1)
|
|
1902
1999
|
.max(32)
|
|
1903
2000
|
.optional()
|
|
1904
|
-
.describe('Install source, 1-32 characters, defaulting to "catalog". The API accepts any string here rather than a fixed enum, but only "catalog" (requires the skill to exist in the marketplace catalog) and "git" (requires sourceRef) get special handling.'),
|
|
1905
|
-
sourceRef: z.string().min(1).max(255).optional().describe("Max 255 characters. Required for git installs — a git install without a valid repository URL fails 422."),
|
|
2001
|
+
.describe('Install source, 1-32 characters, defaulting to "catalog". The API accepts any string here rather than a fixed enum, but only "catalog" (requires the skill to exist in the marketplace catalog) and "git" (requires sourceRef) get special handling. The value is trimmed and lower-cased before it is both checked and forwarded, so "Catalog" and " catalog " are treated exactly as "catalog". Any value that is not "catalog" is refused unless the operator sets THALOVANT_ENABLE_GIT_SKILL_SOURCES, because a non-catalog source can pull unvetted code into the runtime.'),
|
|
2002
|
+
sourceRef: z.string().min(1).max(255).optional().describe("Max 255 characters. Required for git installs — a git install without a valid repository URL fails 422. Only usable when THALOVANT_ENABLE_GIT_SKILL_SOURCES is set."),
|
|
1906
2003
|
versionPin: z.string().min(1).max(64).optional().describe("Pin the skill to an exact version. Max 64 characters."),
|
|
1907
2004
|
active: z
|
|
1908
2005
|
.boolean()
|
|
@@ -1911,11 +2008,23 @@ export function createServer() {
|
|
|
1911
2008
|
},
|
|
1912
2009
|
annotations: {
|
|
1913
2010
|
readOnlyHint: false,
|
|
1914
|
-
destructiveHint:
|
|
2011
|
+
destructiveHint: true,
|
|
1915
2012
|
idempotentHint: true,
|
|
1916
2013
|
openWorldHint: true,
|
|
1917
2014
|
},
|
|
1918
|
-
}, async ({ runtimeGroupId, skillId, marketplaceSkillId, sourceType, sourceRef, versionPin, active, ...auth }) => {
|
|
2015
|
+
}, async ({ runtimeGroupId, skillId, marketplaceSkillId, sourceType: rawSourceType, sourceRef, versionPin, active, ...auth }) => {
|
|
2016
|
+
// Normalize once (trim + case-fold) and forward the SAME value the gate
|
|
2017
|
+
// validates. Otherwise a whitespace/case variant such as " Catalog " or
|
|
2018
|
+
// "CATALOG" passes the local catalog check while the control plane, which
|
|
2019
|
+
// special-cases only the exact string "catalog", treats it as a
|
|
2020
|
+
// non-catalog source — bypassing the gate for the case it exists to block.
|
|
2021
|
+
// A blank/omitted value defaults to the catalog source server-side.
|
|
2022
|
+
const trimmedSource = typeof rawSourceType === "string" ? rawSourceType.trim() : "";
|
|
2023
|
+
const sourceType = trimmedSource ? trimmedSource.toLowerCase() : undefined;
|
|
2024
|
+
if (!isCatalogSource(sourceType) && !gitSkillSourcesEnabled()) {
|
|
2025
|
+
throw new Error(`Refusing to install a skill from non-catalog source "${rawSourceType}". A non-catalog source (for example sourceType "git" with an arbitrary sourceRef) can pull unvetted code into the runtime and is disabled by default. ` +
|
|
2026
|
+
"Set THALOVANT_ENABLE_GIT_SKILL_SOURCES=1 to allow non-catalog skill sources, or install from the marketplace catalog by omitting sourceType (or setting it to \"catalog\").");
|
|
2027
|
+
}
|
|
1919
2028
|
const api = await createControlPlane(auth);
|
|
1920
2029
|
ensureAuthenticated(api);
|
|
1921
2030
|
const options = { marketplaceSkillId, sourceType, sourceRef, versionPin, active };
|