@mycadet/assistant 0.12.8 → 0.12.9
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/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/ces-client/package.json +1 -1
- package/node_modules/@mycadet/ces-client/src/__tests__/ces-client.test.ts +14 -14
- package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/node_modules/@mycadet/service-contracts/package.json +1 -1
- package/openapi.yaml +2 -18
- package/package.json +2 -2
- package/src/__tests__/cloud-profile-guard.test.ts +21 -19
- package/src/__tests__/config-set-route.test.ts +12 -8
- package/src/__tests__/credential-health-service.test.ts +4 -2
- package/src/__tests__/get-skill-detail-audit.test.ts +134 -289
- package/src/__tests__/mcp-abort-signal.test.ts +0 -1
- package/src/__tests__/mcp-client-auth.test.ts +0 -1
- package/src/__tests__/mcp-config-secret-boundary.test.ts +44 -40
- package/src/__tests__/mcp-tool-annotations-risk.test.ts +0 -1
- package/src/__tests__/plugin-api-store-credential.test.ts +4 -1
- package/src/__tests__/skills-file-content-endpoint.test.ts +221 -535
- package/src/__tests__/skills-files-catalog-fallback.test.ts +258 -576
- package/src/api/events/show-cloud-login.ts +2 -2
- package/src/cli/commands/cloud/__tests__/callback-routes-list.test.ts +2 -2
- package/src/cli/commands/cloud/index.ts +1 -1
- package/src/cli/commands/config.ts +1 -1
- package/src/config/__tests__/substrate-twin-shadowing.test.ts +36 -26
- package/src/config/env-registry.ts +2 -9
- package/src/config/schemas/ingress.ts +30 -0
- package/src/config/schemas/llm.ts +0 -9
- package/src/config/seed-inference-profiles.ts +2 -2
- package/src/config/substrate-twin-shadowing.ts +12 -17
- package/src/config/sync-gated-profiles.ts +3 -4
- package/src/credential-execution/prompted-credential.ts +1 -10
- package/src/credential-health/credential-health-service.ts +8 -4
- package/src/daemon/credential-transcript-scrub.ts +11 -0
- package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -0
- package/src/providers/inference/backfill.ts +2 -2
- package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +100 -57
- package/src/runtime/routes/cloud-routes.ts +5 -5
- package/src/runtime/routes/conversation-query-routes.ts +275 -397
- package/src/runtime/routes/secret-routes.ts +2 -17
- package/src/tools/credentials/store.ts +1 -10
- package/src/util/layout.ts +479 -0
- package/src/util/platform.ts +11 -467
- package/src/workspace/migrations/127-backfill-default-provider.ts +2 -2
|
@@ -1438,12 +1438,13 @@ describe("custom profile write normalization (complete overrides)", () => {
|
|
|
1438
1438
|
});
|
|
1439
1439
|
const saved = savedProfiles().partial;
|
|
1440
1440
|
const contextWindow = saved.contextWindow as Record<string, unknown>;
|
|
1441
|
-
|
|
1441
|
+
// Schema-canonical saves strip keys the schema doesn't know.
|
|
1442
|
+
expect(contextWindow.futureField).toBeUndefined();
|
|
1442
1443
|
expect(contextWindow.maxInputTokens).toBe(111);
|
|
1443
1444
|
expect(saved.provider).toBe("anthropic");
|
|
1444
1445
|
});
|
|
1445
1446
|
|
|
1446
|
-
test("unknown profile keys
|
|
1447
|
+
test("unknown profile keys are stripped by the schema parse", async () => {
|
|
1447
1448
|
(
|
|
1448
1449
|
rawConfigFixture.llm as { profiles: Record<string, unknown> }
|
|
1449
1450
|
).profiles.partial = {
|
|
@@ -1456,7 +1457,7 @@ describe("custom profile write normalization (complete overrides)", () => {
|
|
|
1456
1457
|
body: { path: "llm.profiles.partial.maxTokens", value: 999 },
|
|
1457
1458
|
});
|
|
1458
1459
|
const saved = savedProfiles().partial;
|
|
1459
|
-
expect(saved.futureField).
|
|
1460
|
+
expect(saved.futureField).toBeUndefined();
|
|
1460
1461
|
expect(saved.maxTokens).toBe(999);
|
|
1461
1462
|
expect(saved.provider).toBe("anthropic");
|
|
1462
1463
|
});
|
|
@@ -1534,7 +1535,24 @@ describe("call-site override writes stay sparse", () => {
|
|
|
1534
1535
|
).callSites ?? {};
|
|
1535
1536
|
|
|
1536
1537
|
beforeEach(() => {
|
|
1537
|
-
rawConfigFixture = {
|
|
1538
|
+
rawConfigFixture = {
|
|
1539
|
+
llm: {
|
|
1540
|
+
// The schema's superRefine rejects a call site that references an
|
|
1541
|
+
// undefined profile, so the fixtures define the ones they point at.
|
|
1542
|
+
profiles: {
|
|
1543
|
+
mine: {
|
|
1544
|
+
source: "user",
|
|
1545
|
+
provider: "anthropic",
|
|
1546
|
+
model: "claude-sonnet",
|
|
1547
|
+
},
|
|
1548
|
+
old: {
|
|
1549
|
+
source: "user",
|
|
1550
|
+
provider: "anthropic",
|
|
1551
|
+
model: "claude-sonnet",
|
|
1552
|
+
},
|
|
1553
|
+
},
|
|
1554
|
+
},
|
|
1555
|
+
};
|
|
1538
1556
|
seedRawConfig();
|
|
1539
1557
|
});
|
|
1540
1558
|
|
|
@@ -1578,7 +1596,21 @@ describe("call-site override writes stay sparse", () => {
|
|
|
1578
1596
|
|
|
1579
1597
|
test("an existing entry's own customization is preserved", async () => {
|
|
1580
1598
|
rawConfigFixture = {
|
|
1581
|
-
llm: {
|
|
1599
|
+
llm: {
|
|
1600
|
+
profiles: {
|
|
1601
|
+
mine: {
|
|
1602
|
+
source: "user",
|
|
1603
|
+
provider: "anthropic",
|
|
1604
|
+
model: "claude-sonnet",
|
|
1605
|
+
},
|
|
1606
|
+
old: {
|
|
1607
|
+
source: "user",
|
|
1608
|
+
provider: "anthropic",
|
|
1609
|
+
model: "claude-sonnet",
|
|
1610
|
+
},
|
|
1611
|
+
},
|
|
1612
|
+
callSites: { recall: { profile: "old", maxTokens: 200 } },
|
|
1613
|
+
},
|
|
1582
1614
|
};
|
|
1583
1615
|
seedRawConfig();
|
|
1584
1616
|
await configPatchRoute.handler({
|
|
@@ -1645,7 +1677,7 @@ describe("sparse services.stt patch provider seeding", () => {
|
|
|
1645
1677
|
expect(parsed.stt.language).toBe("hi");
|
|
1646
1678
|
expect(parsed.stt.provider).toBe(effectiveProvider);
|
|
1647
1679
|
// Unrelated tts settings survive: no services-section reset happened.
|
|
1648
|
-
expect(savedServices().tts).
|
|
1680
|
+
expect(savedServices().tts).toMatchObject({ provider: "elevenlabs" });
|
|
1649
1681
|
});
|
|
1650
1682
|
|
|
1651
1683
|
test("language patch onto a config that has a provider is a pure merge", async () => {
|
|
@@ -1677,13 +1709,17 @@ describe("sparse services.stt patch provider seeding", () => {
|
|
|
1677
1709
|
expect(stt.providers).toEqual({ deepgram: {} });
|
|
1678
1710
|
});
|
|
1679
1711
|
|
|
1680
|
-
test("a patch not touching stt
|
|
1712
|
+
test("a patch not touching stt leaves the materialized block intact", async () => {
|
|
1681
1713
|
await configPatchRoute.handler({
|
|
1682
1714
|
body: { heartbeat: { activeHoursStart: 9 } },
|
|
1683
1715
|
});
|
|
1684
1716
|
|
|
1685
|
-
|
|
1686
|
-
|
|
1717
|
+
// The merge base is the parsed config, whose stt block carries the
|
|
1718
|
+
// schema default — complete and provider-backed, so the LUM-2758
|
|
1719
|
+
// salvage family (a sparse invented block) cannot occur.
|
|
1720
|
+
expect(savedServices().stt).toMatchObject({ language: "multi" });
|
|
1721
|
+
expect(typeof savedServices().stt!.provider).toBe("string");
|
|
1722
|
+
expect(savedServices().tts).toMatchObject({ provider: "elevenlabs" });
|
|
1687
1723
|
});
|
|
1688
1724
|
|
|
1689
1725
|
test("a config_set language write onto a sparse config seeds the provider", async () => {
|
|
@@ -1702,7 +1738,7 @@ describe("sparse services.stt patch provider seeding", () => {
|
|
|
1702
1738
|
expect(stt.provider).toBe(effectiveProvider);
|
|
1703
1739
|
const parsed = getConfig().services;
|
|
1704
1740
|
expect(parsed.stt.language).toBe("hi");
|
|
1705
|
-
expect(savedServices().tts).
|
|
1741
|
+
expect(savedServices().tts).toMatchObject({ provider: "elevenlabs" });
|
|
1706
1742
|
});
|
|
1707
1743
|
|
|
1708
1744
|
test("a config_set onto a config with a provider leaves it untouched", async () => {
|
|
@@ -2014,26 +2050,21 @@ describe("config writes to a per-agent acp entry", () => {
|
|
|
2014
2050
|
expect(loadConfig().acp.agents.claude?.command).toBeUndefined();
|
|
2015
2051
|
});
|
|
2016
2052
|
|
|
2017
|
-
test("a nulled command on an id with no bundled profile is
|
|
2053
|
+
test("a nulled command on an id with no bundled profile is rejected", async () => {
|
|
2018
2054
|
rawConfigFixture = {
|
|
2019
2055
|
acp: { agents: { mine: { command: "my-acp", args: [] } } },
|
|
2020
2056
|
};
|
|
2021
2057
|
seedRawConfig();
|
|
2022
2058
|
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
}
|
|
2033
|
-
expect(result.error.issues.map((issue) => issue.path)).toEqual([
|
|
2034
|
-
["acp", "agents", "mine", "command"],
|
|
2035
|
-
]);
|
|
2036
|
-
expect(result.error.issues[0]?.message).toContain("acp.agents.mine");
|
|
2059
|
+
// The agents refinement requires a command for an id with no bundled
|
|
2060
|
+
// profile; the schema-validated write rejects instead of persisting a
|
|
2061
|
+
// config whose next load would salvage-drop the whole acp section.
|
|
2062
|
+
await expect(
|
|
2063
|
+
setRoute.handler({
|
|
2064
|
+
body: { path: "acp.agents.mine.command", value: null },
|
|
2065
|
+
}),
|
|
2066
|
+
).rejects.toMatchObject({ statusCode: 400 });
|
|
2067
|
+
expect(agentEntry(loadRawConfig(), "mine").command).toBe("my-acp");
|
|
2037
2068
|
});
|
|
2038
2069
|
|
|
2039
2070
|
test("a model set on a bare bundled entry survives the next load", async () => {
|
|
@@ -2103,7 +2134,7 @@ describe("persisted chat settings", () => {
|
|
|
2103
2134
|
{ notifications: { newMessageEnabled: "false" } },
|
|
2104
2135
|
{ notifications: { newMessageEnabled: null } },
|
|
2105
2136
|
])(
|
|
2106
|
-
`${writer} unrelated writes
|
|
2137
|
+
`${writer} unrelated writes succeed over malformed sections: %j`,
|
|
2107
2138
|
async (legacy) => {
|
|
2108
2139
|
rawConfigFixture = { ...legacy, maxStepsPerSession: 75 };
|
|
2109
2140
|
seedRawConfig();
|
|
@@ -2111,10 +2142,11 @@ describe("persisted chat settings", () => {
|
|
|
2111
2142
|
|
|
2112
2143
|
await writeSetting(writer, "maxStepsPerSession", 100);
|
|
2113
2144
|
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2145
|
+
// The loader's read-time recovery cleans malformed sections before
|
|
2146
|
+
// the merge, so no malformed value can block an unrelated write;
|
|
2147
|
+
// schema-canonical saves materialize defaults around what the user
|
|
2148
|
+
// set (the PATCH/SET writers heal the section, canonical rides).
|
|
2149
|
+
expect(loadRawConfig()).toMatchObject({ maxStepsPerSession: 100 });
|
|
2118
2150
|
expect(initializeProvidersCalls).toBe(1);
|
|
2119
2151
|
expect(clearEmbeddingBackendCacheCalls).toBe(1);
|
|
2120
2152
|
},
|
|
@@ -2122,7 +2154,7 @@ describe("persisted chat settings", () => {
|
|
|
2122
2154
|
|
|
2123
2155
|
test(`${writer} can repair one leaf without changing other legacy values`, async () => {
|
|
2124
2156
|
rawConfigFixture = {
|
|
2125
|
-
conversations: { autoArchive: { enabled: "true", afterDays:
|
|
2157
|
+
conversations: { autoArchive: { enabled: "true", afterDays: 14 } },
|
|
2126
2158
|
notifications: null,
|
|
2127
2159
|
};
|
|
2128
2160
|
seedRawConfig();
|
|
@@ -2130,9 +2162,8 @@ describe("persisted chat settings", () => {
|
|
|
2130
2162
|
|
|
2131
2163
|
await writeSetting(writer, "conversations.autoArchive.enabled", true);
|
|
2132
2164
|
|
|
2133
|
-
expect(loadRawConfig()).
|
|
2134
|
-
conversations: { autoArchive: { enabled: true, afterDays:
|
|
2135
|
-
notifications: null,
|
|
2165
|
+
expect(loadRawConfig()).toMatchObject({
|
|
2166
|
+
conversations: { autoArchive: { enabled: true, afterDays: 14 } },
|
|
2136
2167
|
});
|
|
2137
2168
|
});
|
|
2138
2169
|
|
|
@@ -2200,9 +2231,11 @@ describe("persisted chat settings", () => {
|
|
|
2200
2231
|
body: { conversations: { autoArchive: { enabled: null } } },
|
|
2201
2232
|
});
|
|
2202
2233
|
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2234
|
+
// Nulled leaves reset to the schema default on the canonical save (the
|
|
2235
|
+
// read-time recovery had already cleaned the stored nulls).
|
|
2236
|
+
expect(loadConfig().conversations.autoArchive).toEqual({
|
|
2237
|
+
enabled: false,
|
|
2238
|
+
afterDays: 7,
|
|
2206
2239
|
});
|
|
2207
2240
|
});
|
|
2208
2241
|
|
|
@@ -2243,12 +2276,13 @@ describe("persisted chat settings", () => {
|
|
|
2243
2276
|
notifications: { newMessageEnabled: false },
|
|
2244
2277
|
maxStepsPerSession: 75,
|
|
2245
2278
|
});
|
|
2246
|
-
expect(loadRawConfig()).
|
|
2247
|
-
|
|
2279
|
+
expect(loadRawConfig()).toMatchObject({
|
|
2280
|
+
maxStepsPerSession: 75,
|
|
2248
2281
|
conversations: {
|
|
2249
2282
|
skipAutoRetitling: true,
|
|
2250
|
-
autoArchive: { enabled: true, afterDays: 30
|
|
2283
|
+
autoArchive: { enabled: true, afterDays: 30 },
|
|
2251
2284
|
},
|
|
2285
|
+
notifications: { newMessageEnabled: false },
|
|
2252
2286
|
});
|
|
2253
2287
|
invalidateConfigCache();
|
|
2254
2288
|
expect(loadConfig().conversations.autoArchive).toEqual({
|
|
@@ -2285,16 +2319,17 @@ describe("persisted chat settings", () => {
|
|
|
2285
2319
|
},
|
|
2286
2320
|
notifications: { newMessageEnabled: true },
|
|
2287
2321
|
});
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2322
|
+
// The nulled leaves restore to the schema defaults on the canonical
|
|
2323
|
+
// save; the unknown sibling key (futureOption) doesn't persist.
|
|
2324
|
+
expect(loadConfig().conversations.autoArchive).toEqual({
|
|
2325
|
+
enabled: false,
|
|
2326
|
+
afterDays: 7,
|
|
2291
2327
|
});
|
|
2292
|
-
expect(
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
);
|
|
2328
|
+
expect(loadConfig().notifications.newMessageEnabled).toBe(true);
|
|
2329
|
+
expect(
|
|
2330
|
+
"futureOption" in
|
|
2331
|
+
(loadRawConfig().conversations as Record<string, unknown>),
|
|
2332
|
+
).toBe(false);
|
|
2298
2333
|
});
|
|
2299
2334
|
|
|
2300
2335
|
test.each([
|
|
@@ -2306,8 +2341,13 @@ describe("persisted chat settings", () => {
|
|
|
2306
2341
|
conversations: { autoArchive: { enabled: false, afterDays: 7 } },
|
|
2307
2342
|
notifications: { newMessageEnabled: true },
|
|
2308
2343
|
});
|
|
2309
|
-
|
|
2310
|
-
|
|
2344
|
+
// The nulled subtrees restore to the schema defaults on the canonical
|
|
2345
|
+
// save — the same values a fresh read would materialize.
|
|
2346
|
+
expect(loadConfig().conversations.autoArchive).toEqual({
|
|
2347
|
+
enabled: false,
|
|
2348
|
+
afterDays: 7,
|
|
2349
|
+
});
|
|
2350
|
+
expect(loadConfig().notifications.newMessageEnabled).toBe(true);
|
|
2311
2351
|
});
|
|
2312
2352
|
|
|
2313
2353
|
test.each([
|
|
@@ -2352,7 +2392,9 @@ describe("persisted chat settings", () => {
|
|
|
2352
2392
|
},
|
|
2353
2393
|
notifications: { newMessageEnabled: false },
|
|
2354
2394
|
});
|
|
2355
|
-
|
|
2395
|
+
// The valid legacy values ride; the malformed subtree heals to the
|
|
2396
|
+
// schema defaults on the canonical save.
|
|
2397
|
+
expect(loadRawConfig()).toMatchObject({
|
|
2356
2398
|
conversations: { skipAutoRetitling: true },
|
|
2357
2399
|
notifications: { newMessageEnabled: false },
|
|
2358
2400
|
maxStepsPerSession: 75,
|
|
@@ -2379,7 +2421,7 @@ describe("persisted chat settings", () => {
|
|
|
2379
2421
|
conversations: { autoArchive: { enabled: false, afterDays: 7 } },
|
|
2380
2422
|
notifications: { newMessageEnabled: true },
|
|
2381
2423
|
});
|
|
2382
|
-
expect(loadRawConfig()).
|
|
2424
|
+
expect(loadRawConfig()).toMatchObject({ maxStepsPerSession: 75 });
|
|
2383
2425
|
},
|
|
2384
2426
|
);
|
|
2385
2427
|
|
|
@@ -2388,10 +2430,11 @@ describe("persisted chat settings", () => {
|
|
|
2388
2430
|
body: { path: "notifications.newMessageEnabled", value: true },
|
|
2389
2431
|
});
|
|
2390
2432
|
expect(loadConfig().notifications.newMessageEnabled).toBe(true);
|
|
2391
|
-
|
|
2392
|
-
|
|
2393
|
-
"
|
|
2394
|
-
|
|
2433
|
+
// Schema-canonical saves strip keys the schema doesn't know.
|
|
2434
|
+
expect(
|
|
2435
|
+
"futureOption" in
|
|
2436
|
+
(loadRawConfig().notifications as Record<string, unknown>),
|
|
2437
|
+
).toBe(false);
|
|
2395
2438
|
});
|
|
2396
2439
|
|
|
2397
2440
|
test.each([
|
|
@@ -49,7 +49,7 @@ import {
|
|
|
49
49
|
setCloudOrganizationId,
|
|
50
50
|
setCloudUserId,
|
|
51
51
|
} from "../../config/env.js";
|
|
52
|
-
import {
|
|
52
|
+
import { getIsCloud } from "../../config/env-registry.js";
|
|
53
53
|
import {
|
|
54
54
|
registerCallbackRoute,
|
|
55
55
|
resolveCloudCallbackRegistrationContext,
|
|
@@ -352,7 +352,7 @@ async function handlePlatformConnect(
|
|
|
352
352
|
// The outcome travels in the response, where the CLI reads it and tells the
|
|
353
353
|
// user what to do. Nothing shipped consumes the signal below, so the
|
|
354
354
|
// response is the only path the answer reaches anyone by.
|
|
355
|
-
broadcastMessage({ type: "
|
|
355
|
+
broadcastMessage({ type: "show_cloud_login" });
|
|
356
356
|
|
|
357
357
|
return credentialRejected
|
|
358
358
|
? { showPlatformLogin: true, credentialRejected: true }
|
|
@@ -399,7 +399,7 @@ async function handleCloudDisconnect(
|
|
|
399
399
|
_args: RouteHandlerArgs,
|
|
400
400
|
): Promise<CloudDisconnectResponse> {
|
|
401
401
|
// Reject if running inside a cloud host
|
|
402
|
-
if (
|
|
402
|
+
if (getIsCloud()) {
|
|
403
403
|
throw new UnprocessableEntityError(
|
|
404
404
|
"Cannot disconnect from the cloud on a cloud-hosted assistant.",
|
|
405
405
|
);
|
|
@@ -947,7 +947,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
947
947
|
responseBody: CloudDisconnectResponseSchema,
|
|
948
948
|
},
|
|
949
949
|
{
|
|
950
|
-
operationId: "
|
|
950
|
+
operationId: "cloud_callback_routes_register",
|
|
951
951
|
endpoint: "platform/callback-routes/register",
|
|
952
952
|
method: "POST",
|
|
953
953
|
policy: {
|
|
@@ -957,7 +957,7 @@ export const ROUTES: RouteDefinition[] = [
|
|
|
957
957
|
summary: "Register a cloud callback route",
|
|
958
958
|
description:
|
|
959
959
|
"Registers a callback route with the cloud gateway for inbound provider webhooks.",
|
|
960
|
-
tags: ["
|
|
960
|
+
tags: ["cloud"],
|
|
961
961
|
handler: handleCallbackRoutesRegister,
|
|
962
962
|
requestBody: CallbackRouteRegisterRequestSchema,
|
|
963
963
|
responseBody: CallbackRouteRegisterResponseSchema,
|