@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.
Files changed (42) hide show
  1. package/node_modules/@mycadet/ces-client/node_modules/@mycadet/service-contracts/package.json +1 -1
  2. package/node_modules/@mycadet/ces-client/package.json +1 -1
  3. package/node_modules/@mycadet/ces-client/src/__tests__/ces-client.test.ts +14 -14
  4. package/node_modules/@mycadet/gateway-client/node_modules/@mycadet/service-contracts/package.json +1 -1
  5. package/node_modules/@mycadet/service-contracts/package.json +1 -1
  6. package/openapi.yaml +2 -18
  7. package/package.json +2 -2
  8. package/src/__tests__/cloud-profile-guard.test.ts +21 -19
  9. package/src/__tests__/config-set-route.test.ts +12 -8
  10. package/src/__tests__/credential-health-service.test.ts +4 -2
  11. package/src/__tests__/get-skill-detail-audit.test.ts +134 -289
  12. package/src/__tests__/mcp-abort-signal.test.ts +0 -1
  13. package/src/__tests__/mcp-client-auth.test.ts +0 -1
  14. package/src/__tests__/mcp-config-secret-boundary.test.ts +44 -40
  15. package/src/__tests__/mcp-tool-annotations-risk.test.ts +0 -1
  16. package/src/__tests__/plugin-api-store-credential.test.ts +4 -1
  17. package/src/__tests__/skills-file-content-endpoint.test.ts +221 -535
  18. package/src/__tests__/skills-files-catalog-fallback.test.ts +258 -576
  19. package/src/api/events/show-cloud-login.ts +2 -2
  20. package/src/cli/commands/cloud/__tests__/callback-routes-list.test.ts +2 -2
  21. package/src/cli/commands/cloud/index.ts +1 -1
  22. package/src/cli/commands/config.ts +1 -1
  23. package/src/config/__tests__/substrate-twin-shadowing.test.ts +36 -26
  24. package/src/config/env-registry.ts +2 -9
  25. package/src/config/schemas/ingress.ts +30 -0
  26. package/src/config/schemas/llm.ts +0 -9
  27. package/src/config/seed-inference-profiles.ts +2 -2
  28. package/src/config/substrate-twin-shadowing.ts +12 -17
  29. package/src/config/sync-gated-profiles.ts +3 -4
  30. package/src/credential-execution/prompted-credential.ts +1 -10
  31. package/src/credential-health/credential-health-service.ts +8 -4
  32. package/src/daemon/credential-transcript-scrub.ts +11 -0
  33. package/src/plugins/defaults/memory/__tests__/memory-tier-boundary-guard.test.ts +5 -0
  34. package/src/providers/inference/backfill.ts +2 -2
  35. package/src/runtime/routes/__tests__/conversation-query-routes.test.ts +100 -57
  36. package/src/runtime/routes/cloud-routes.ts +5 -5
  37. package/src/runtime/routes/conversation-query-routes.ts +275 -397
  38. package/src/runtime/routes/secret-routes.ts +2 -17
  39. package/src/tools/credentials/store.ts +1 -10
  40. package/src/util/layout.ts +479 -0
  41. package/src/util/platform.ts +11 -467
  42. 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
- expect(contextWindow.futureField).toBe("keep-me");
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 survive completion of a touched entry", async () => {
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).toBe("keep-me");
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 = { llm: {} };
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: { callSites: { recall: { profile: "old", maxTokens: 200 } } },
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).toEqual({ provider: "elevenlabs" });
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 invents no stt block", async () => {
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
- expect(savedServices().stt).toBeUndefined();
1686
- expect(savedServices().tts).toEqual({ provider: "elevenlabs" });
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).toEqual({ provider: "elevenlabs" });
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 dropped too", async () => {
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
- await setRoute.handler({
2024
- body: { path: "acp.agents.mine.command", value: null },
2025
- });
2026
-
2027
- expect("command" in agentEntry(loadRawConfig(), "mine")).toBe(false);
2028
- const result = AssistantConfigSchema.safeParse(loadRawConfig());
2029
- expect(result.success).toBe(false);
2030
- if (result.success) {
2031
- return;
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 preserve untouched legacy values: %j`,
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
- expect(loadRawConfig()).toEqual({
2115
- ...rawConfigFixture,
2116
- maxStepsPerSession: 100,
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: 2 } },
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()).toEqual({
2134
- conversations: { autoArchive: { enabled: true, afterDays: 2 } },
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
- expect(loadRawConfig()).toEqual({
2204
- conversations: { autoArchive: { afterDays: null } },
2205
- notifications: { newMessageEnabled: null },
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()).toEqual({
2247
- ...rawConfigFixture,
2279
+ expect(loadRawConfig()).toMatchObject({
2280
+ maxStepsPerSession: 75,
2248
2281
  conversations: {
2249
2282
  skipAutoRetitling: true,
2250
- autoArchive: { enabled: true, afterDays: 30, futureOption: "keep" },
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
- expect(loadRawConfig()).toMatchObject({
2289
- conversations: { autoArchive: { futureOption: "keep" } },
2290
- notifications: { futureOption: "keep" },
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(loadRawConfig()).not.toHaveProperty(
2293
- "conversations.autoArchive.enabled",
2294
- );
2295
- expect(loadRawConfig()).not.toHaveProperty(
2296
- "notifications.newMessageEnabled",
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
- expect(loadRawConfig()).not.toHaveProperty("conversations.autoArchive");
2310
- expect(loadRawConfig()).not.toHaveProperty("notifications");
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
- expect(loadRawConfig()).toEqual({
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()).toEqual({ maxStepsPerSession: 75 });
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
- expect(loadRawConfig()).toHaveProperty(
2392
- "notifications.futureOption",
2393
- "keep",
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 { isCloudRemote } from "../../config/env-registry.js";
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: "show_platform_login" });
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 (isCloudRemote()) {
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: "platform_callback_routes_register",
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: ["platform"],
960
+ tags: ["cloud"],
961
961
  handler: handleCallbackRoutesRegister,
962
962
  requestBody: CallbackRouteRegisterRequestSchema,
963
963
  responseBody: CallbackRouteRegisterResponseSchema,