@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `
|
|
2
|
+
* `show_cloud_login` SSE event.
|
|
3
3
|
*
|
|
4
4
|
* Emitted by the cloud-connect route when no cloud credentials are
|
|
5
5
|
* stored yet, signalling connected clients to surface the cloud login UI.
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
import { z } from "zod";
|
|
13
13
|
|
|
14
14
|
export const ShowCloudLoginEventSchema = z.object({
|
|
15
|
-
type: z.literal("
|
|
15
|
+
type: z.literal("show_cloud_login"),
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
export type ShowCloudLoginEvent = z.infer<typeof ShowCloudLoginEventSchema>;
|
|
@@ -66,7 +66,7 @@ describe("assistant cloud callback-routes list", () => {
|
|
|
66
66
|
expect((thrown as Error).message).toBe("exitFromIpcResult called");
|
|
67
67
|
});
|
|
68
68
|
|
|
69
|
-
test("callback-routes register calls
|
|
69
|
+
test("callback-routes register calls cloud_callback_routes_register", async () => {
|
|
70
70
|
ipc.response = {
|
|
71
71
|
ok: true,
|
|
72
72
|
result: {
|
|
@@ -87,7 +87,7 @@ describe("assistant cloud callback-routes list", () => {
|
|
|
87
87
|
"--json",
|
|
88
88
|
]);
|
|
89
89
|
|
|
90
|
-
expect(ipc.calls[0][0]).toBe("
|
|
90
|
+
expect(ipc.calls[0][0]).toBe("cloud_callback_routes_register");
|
|
91
91
|
expect((ipc.calls[0][1].body as Record<string, unknown>).path).toBe(
|
|
92
92
|
"webhooks/telegram",
|
|
93
93
|
);
|
|
@@ -260,7 +260,7 @@ export function registerCloudCommand(program: Command): void {
|
|
|
260
260
|
callbackUrl: string;
|
|
261
261
|
callbackPath: string;
|
|
262
262
|
type: string;
|
|
263
|
-
}>("
|
|
263
|
+
}>("cloud_callback_routes_register", {
|
|
264
264
|
body: { path: opts.path, type: opts.type },
|
|
265
265
|
});
|
|
266
266
|
if (!r.ok) {
|
|
@@ -153,7 +153,7 @@ export function registerConfigCommand(program: Command): void {
|
|
|
153
153
|
// the value above cannot be mistaken for the effective one.
|
|
154
154
|
const { describeShadowedConfigGet, findSubstrateShadowing } =
|
|
155
155
|
await import("../../config/substrate-twin-shadowing.js");
|
|
156
|
-
const shadowing = findSubstrateShadowing(
|
|
156
|
+
const shadowing = findSubstrateShadowing(key);
|
|
157
157
|
if (shadowing) {
|
|
158
158
|
log.warn(describeShadowedConfigGet(shadowing, key));
|
|
159
159
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { utimesSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { describe, expect, test } from "bun:test";
|
|
2
4
|
|
|
3
5
|
import { MemorySubstrateConfigSchema } from "../schemas/memory-substrate.js";
|
|
@@ -9,6 +11,18 @@ import {
|
|
|
9
11
|
SUBSTRATE_TWIN_BY_V2_KEY,
|
|
10
12
|
} from "../substrate-twin-shadowing.js";
|
|
11
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Write `raw` to the workspace config.json as the state the loader serves.
|
|
16
|
+
* A distinct mtime per write keeps the loader's file-signature cache from
|
|
17
|
+
* reading two consecutive seeds as identical.
|
|
18
|
+
*/
|
|
19
|
+
function seedRawConfig(raw: Record<string, unknown>): void {
|
|
20
|
+
const path = join(process.env.CADET_WORKSPACE_DIR!, "config.json");
|
|
21
|
+
writeFileSync(path, JSON.stringify(raw));
|
|
22
|
+
const stamp = new Date(Date.now() + Math.random());
|
|
23
|
+
utimesSync(path, stamp, stamp);
|
|
24
|
+
}
|
|
25
|
+
|
|
12
26
|
describe("substrate twin table", () => {
|
|
13
27
|
test("pairs exactly the memory.substrate schema's keys", () => {
|
|
14
28
|
// Drift guard: a substrate key with no entry here is a tunable whose
|
|
@@ -44,15 +58,15 @@ describe("substrate twin table", () => {
|
|
|
44
58
|
|
|
45
59
|
describe("findSubstrateShadowing", () => {
|
|
46
60
|
test("reports the winning substrate twin for a shadowed memory.v2 key", () => {
|
|
47
|
-
|
|
61
|
+
seedRawConfig({
|
|
48
62
|
memory: {
|
|
49
63
|
v2: { consolidation_interval_hours: 4 },
|
|
50
64
|
substrate: { consolidation_interval_hours: 876000 },
|
|
51
65
|
},
|
|
52
|
-
};
|
|
66
|
+
});
|
|
53
67
|
|
|
54
68
|
expect(
|
|
55
|
-
findSubstrateShadowing(
|
|
69
|
+
findSubstrateShadowing("memory.v2.consolidation_interval_hours"),
|
|
56
70
|
).toEqual({
|
|
57
71
|
substratePath: "memory.substrate.consolidation_interval_hours",
|
|
58
72
|
substrateValue: 876000,
|
|
@@ -61,29 +75,31 @@ describe("findSubstrateShadowing", () => {
|
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
test("maps the renamed spread keys to their substrate names", () => {
|
|
64
|
-
|
|
78
|
+
seedRawConfig({ memory: { v2: {}, substrate: { spread_k: 0.4 } } });
|
|
65
79
|
|
|
66
|
-
expect(findSubstrateShadowing(
|
|
80
|
+
expect(findSubstrateShadowing("memory.v2.k")).toEqual({
|
|
67
81
|
substratePath: "memory.substrate.spread_k",
|
|
68
82
|
substrateValue: 0.4,
|
|
69
83
|
alsoReadByV2Engine: true,
|
|
70
84
|
});
|
|
71
85
|
// hops has no twin set, so nothing shadows it.
|
|
72
|
-
expect(findSubstrateShadowing(
|
|
86
|
+
expect(findSubstrateShadowing("memory.v2.hops")).toBeUndefined();
|
|
73
87
|
});
|
|
74
88
|
|
|
75
89
|
test("treats an explicit null twin as set, matching resolveSubstrateTuning", () => {
|
|
76
90
|
// `orV2` falls back only on `undefined`, so a null substrate value wins —
|
|
77
|
-
// and it is exactly the value the consolidation freeze writes.
|
|
78
|
-
|
|
91
|
+
// and it is exactly the value the consolidation freeze writes. The field
|
|
92
|
+
// is nullable in the schema, so the null survives the loader's parse and
|
|
93
|
+
// stays present in the running config.
|
|
94
|
+
seedRawConfig({
|
|
79
95
|
memory: {
|
|
80
96
|
v2: { consolidation_max_buffer_lines: 100 },
|
|
81
97
|
substrate: { consolidation_max_buffer_lines: null },
|
|
82
98
|
},
|
|
83
|
-
};
|
|
99
|
+
});
|
|
84
100
|
|
|
85
101
|
expect(
|
|
86
|
-
findSubstrateShadowing(
|
|
102
|
+
findSubstrateShadowing("memory.v2.consolidation_max_buffer_lines"),
|
|
87
103
|
).toEqual({
|
|
88
104
|
substratePath: "memory.substrate.consolidation_max_buffer_lines",
|
|
89
105
|
substrateValue: null,
|
|
@@ -92,29 +108,23 @@ describe("findSubstrateShadowing", () => {
|
|
|
92
108
|
});
|
|
93
109
|
|
|
94
110
|
test("returns nothing for unshadowed paths", () => {
|
|
95
|
-
|
|
111
|
+
// No memory.substrate subtree at all.
|
|
112
|
+
seedRawConfig({ memory: { v2: { bm25_b: 0.6 } } });
|
|
113
|
+
expect(findSubstrateShadowing("memory.v2.bm25_b")).toBeUndefined();
|
|
114
|
+
|
|
115
|
+
seedRawConfig({
|
|
96
116
|
memory: {
|
|
97
117
|
v2: { bm25_b: 0.6, enabled: true },
|
|
98
118
|
substrate: { bm25_b: 0.3 },
|
|
99
119
|
},
|
|
100
|
-
};
|
|
101
|
-
|
|
102
|
-
// No memory.substrate subtree at all.
|
|
103
|
-
expect(
|
|
104
|
-
findSubstrateShadowing(
|
|
105
|
-
{ memory: { v2: { bm25_b: 0.6 } } },
|
|
106
|
-
"memory.v2.bm25_b",
|
|
107
|
-
),
|
|
108
|
-
).toBeUndefined();
|
|
120
|
+
});
|
|
109
121
|
// A v2 key with no substrate twin.
|
|
110
|
-
expect(findSubstrateShadowing(
|
|
122
|
+
expect(findSubstrateShadowing("memory.v2.enabled")).toBeUndefined();
|
|
111
123
|
// The substrate key itself is what wins — writing it is never shadowed.
|
|
112
|
-
expect(
|
|
113
|
-
findSubstrateShadowing(raw, "memory.substrate.bm25_b"),
|
|
114
|
-
).toBeUndefined();
|
|
124
|
+
expect(findSubstrateShadowing("memory.substrate.bm25_b")).toBeUndefined();
|
|
115
125
|
// Paths outside the memory namespace, and the wrong depth.
|
|
116
|
-
expect(findSubstrateShadowing(
|
|
117
|
-
expect(findSubstrateShadowing(
|
|
126
|
+
expect(findSubstrateShadowing("memory.v2")).toBeUndefined();
|
|
127
|
+
expect(findSubstrateShadowing("llm.v2.bm25_b")).toBeUndefined();
|
|
118
128
|
});
|
|
119
129
|
});
|
|
120
130
|
|
|
@@ -20,7 +20,7 @@ function str(name: string): string | undefined {
|
|
|
20
20
|
}
|
|
21
21
|
|
|
22
22
|
function flag(name: string): boolean {
|
|
23
|
-
const raw = str(name);
|
|
23
|
+
const raw = str(name)?.toLowerCase();
|
|
24
24
|
return raw === "true" || raw === "1";
|
|
25
25
|
}
|
|
26
26
|
|
|
@@ -65,14 +65,7 @@ export function getIsContainerized(): boolean {
|
|
|
65
65
|
* deployments provisioned before the rename keep working.
|
|
66
66
|
*/
|
|
67
67
|
export function getIsCloud(): boolean {
|
|
68
|
-
return flag("IS_CLOUD")
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Whether this assistant is running as a Cadet Cloud-managed remote instance.
|
|
73
|
-
*/
|
|
74
|
-
export function isCloudRemote(): boolean {
|
|
75
|
-
return getIsCloud();
|
|
68
|
+
return flag("IS_CLOUD");
|
|
76
69
|
}
|
|
77
70
|
|
|
78
71
|
/**
|
|
@@ -1,5 +1,23 @@
|
|
|
1
|
+
import {
|
|
2
|
+
INGRESS_ASSISTANT_ID_KEY,
|
|
3
|
+
INGRESS_LAST_TUNNEL_KEY,
|
|
4
|
+
INGRESS_PAIRING_TUNNEL_KEY,
|
|
5
|
+
TUNNEL_PROVIDERS,
|
|
6
|
+
} from "@mycadet/service-contracts/ingress";
|
|
1
7
|
import { z } from "zod";
|
|
2
8
|
|
|
9
|
+
/**
|
|
10
|
+
* A recorded tunnel under `ingress.lastTunnel` / `ingress.pairingTunnel`.
|
|
11
|
+
* The CLI writes these records (validated by `parseTunnelRecord` in
|
|
12
|
+
* service-contracts — the single source for the key names, the provider
|
|
13
|
+
* allowlist, and record validation); the schema carries them so schema-
|
|
14
|
+
* canonical config writes preserve live tunnel state instead of dropping it.
|
|
15
|
+
*/
|
|
16
|
+
const TunnelRecordSchema = z.object({
|
|
17
|
+
provider: z.enum(TUNNEL_PROVIDERS),
|
|
18
|
+
publicBaseUrl: z.string(),
|
|
19
|
+
});
|
|
20
|
+
|
|
3
21
|
function emptyOrAbsoluteHttpUrl(fieldPath: string) {
|
|
4
22
|
return z
|
|
5
23
|
.string({ error: `${fieldPath} must be a string` })
|
|
@@ -94,6 +112,18 @@ const IngressBaseSchema = z
|
|
|
94
112
|
rateLimit: IngressRateLimitConfigSchema.default(
|
|
95
113
|
IngressRateLimitConfigSchema.parse({}),
|
|
96
114
|
),
|
|
115
|
+
[INGRESS_ASSISTANT_ID_KEY]: z
|
|
116
|
+
.string()
|
|
117
|
+
.optional()
|
|
118
|
+
.describe("The assistant id the tunnel fronted (CLI-owned record)"),
|
|
119
|
+
[INGRESS_LAST_TUNNEL_KEY]: TunnelRecordSchema.optional(),
|
|
120
|
+
[INGRESS_PAIRING_TUNNEL_KEY]: TunnelRecordSchema.optional(),
|
|
121
|
+
publicBaseUrlManagedBy: z
|
|
122
|
+
.string()
|
|
123
|
+
.optional()
|
|
124
|
+
.describe(
|
|
125
|
+
'Who owns the current publicBaseUrl — "tunnel" when the tunnel machinery stamped it, anything else is user-managed (CLI/route-owned bookkeeping)',
|
|
126
|
+
),
|
|
97
127
|
})
|
|
98
128
|
.describe(
|
|
99
129
|
"Ingress HTTP server configuration — handles incoming webhooks, API requests, and channel events",
|
|
@@ -733,15 +733,6 @@ export const ProfileEntry = LLMConfigFragment.extend({
|
|
|
733
733
|
* not supported.
|
|
734
734
|
*/
|
|
735
735
|
fallbackProfile: z.string().min(1).optional().meta({ readOnly: true }),
|
|
736
|
-
/**
|
|
737
|
-
* Wire-only capability flags: stamped onto config-read responses by
|
|
738
|
-
* `enrichProfilesForWire` and stripped from incoming writes by
|
|
739
|
-
* `stripWireOnlyProfileKeys` (conversation-query-routes), so they never
|
|
740
|
-
* persist. Declared here so the wire contract (AssistantConfig → SDK
|
|
741
|
-
* types) describes the enriched profile shape clients consume.
|
|
742
|
-
*/
|
|
743
|
-
supportsVision: z.boolean().optional(),
|
|
744
|
-
invariant: z.boolean().optional(),
|
|
745
736
|
}).meta({ id: "ProfileEntry" });
|
|
746
737
|
export type ProfileEntry = z.infer<typeof ProfileEntry>;
|
|
747
738
|
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
MANAGED_PROFILE_NAMES,
|
|
15
15
|
MANAGED_PROFILE_TEMPLATES,
|
|
16
16
|
} from "./default-profile-catalog.js";
|
|
17
|
+
import { getIsCloud } from "./env-registry.js";
|
|
17
18
|
import { loadConfig, loadRawConfig, saveRawConfig } from "./loader.js";
|
|
18
19
|
import { isDispatchableProfile } from "./profile-dispatchability.js";
|
|
19
20
|
import {
|
|
@@ -67,8 +68,7 @@ export function seedInferenceProfiles(
|
|
|
67
68
|
const llm = config.llm;
|
|
68
69
|
const profiles = llm.profiles;
|
|
69
70
|
|
|
70
|
-
const isCloud =
|
|
71
|
-
process.env.IS_CLOUD === "true" || process.env.IS_CLOUD === "1";
|
|
71
|
+
const isCloud = getIsCloud();
|
|
72
72
|
|
|
73
73
|
// A hatch overlay that explicitly selected a cloud connection routes the
|
|
74
74
|
// default provider through cadet rather than the entered BYOK key.
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
// Memory substrate — twin-namespace shadowing detection
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
|
|
5
|
+
import { getConfig } from "./loader.js";
|
|
6
|
+
|
|
5
7
|
/** One `memory.v2` tunable's `memory.substrate` twin. */
|
|
6
8
|
interface SubstrateTwin {
|
|
7
9
|
/** The twin's `memory.substrate` key, e.g. `spread_k` for `k`. */
|
|
@@ -93,29 +95,22 @@ export type SubstrateShadowing = {
|
|
|
93
95
|
alsoReadByV2Engine: boolean;
|
|
94
96
|
};
|
|
95
97
|
|
|
96
|
-
function readPlainObject(value: unknown): Record<string, unknown> | undefined {
|
|
97
|
-
return value != null && typeof value === "object" && !Array.isArray(value)
|
|
98
|
-
? (value as Record<string, unknown>)
|
|
99
|
-
: undefined;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
98
|
/**
|
|
103
99
|
* The `memory.substrate` twin covering `path`, or `undefined` when `path` is
|
|
104
100
|
* not a shadowable `memory.v2` tunable or its twin is unset.
|
|
105
101
|
*
|
|
106
102
|
* `memory.substrate` is override-only and wins for substrate recall whenever
|
|
107
|
-
* its key is
|
|
108
|
-
* `resolveSubstrateTuning`
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
* remaining consumer, while for the three engine-read twins it still tunes v2
|
|
112
|
-
* injection.
|
|
103
|
+
* its key is set. Detection keys on presence in the RUNNING config — the same
|
|
104
|
+
* typed view `resolveSubstrateTuning` consults, so the check can never
|
|
105
|
+
* disagree with runtime precedence: a key absent from the parsed config is
|
|
106
|
+
* absent for the resolver too.
|
|
113
107
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
108
|
+
* What a set twin means for the `memory.v2` key depends on
|
|
109
|
+
* {@link SubstrateTwin.alsoReadByV2Engine}: for a substrate-only twin the
|
|
110
|
+
* `memory.v2` value has no remaining consumer, while for the three
|
|
111
|
+
* engine-read twins it still tunes v2 injection.
|
|
116
112
|
*/
|
|
117
113
|
export function findSubstrateShadowing(
|
|
118
|
-
raw: Record<string, unknown>,
|
|
119
114
|
path: string,
|
|
120
115
|
): SubstrateShadowing | undefined {
|
|
121
116
|
const segments = path.split(".");
|
|
@@ -130,8 +125,8 @@ export function findSubstrateShadowing(
|
|
|
130
125
|
if (twin === undefined) {
|
|
131
126
|
return undefined;
|
|
132
127
|
}
|
|
133
|
-
const substrate =
|
|
134
|
-
if (!
|
|
128
|
+
const substrate: Record<string, unknown> = getConfig().memory.substrate;
|
|
129
|
+
if (!(twin.substrateKey in substrate)) {
|
|
135
130
|
return undefined;
|
|
136
131
|
}
|
|
137
132
|
return {
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
AUTO_PROFILE_KEY,
|
|
13
13
|
OS_BETA_PROFILE_KEY,
|
|
14
14
|
} from "./default-profile-names.js";
|
|
15
|
+
import { getIsCloud } from "./env-registry.js";
|
|
15
16
|
import {
|
|
16
17
|
getConfigReadOnly,
|
|
17
18
|
invalidateConfigCache,
|
|
@@ -97,9 +98,7 @@ export function reconcileFlagGatedProfiles(): boolean {
|
|
|
97
98
|
: [];
|
|
98
99
|
llm.profileOrder = profileOrder;
|
|
99
100
|
|
|
100
|
-
const isCloud =
|
|
101
|
-
process.env.IS_CLOUD === "true" || process.env.IS_CLOUD === "1";
|
|
102
|
-
const isByokMode = !isCloud;
|
|
101
|
+
const isCloud = getIsCloud();
|
|
103
102
|
|
|
104
103
|
let changed = false;
|
|
105
104
|
for (const gated of GATED_PROFILES) {
|
|
@@ -123,7 +122,7 @@ export function reconcileFlagGatedProfiles(): boolean {
|
|
|
123
122
|
|
|
124
123
|
const order = llm.profileOrder as string[];
|
|
125
124
|
const profileChanged = enabled
|
|
126
|
-
? enableProfile(gated, profiles, order, previous,
|
|
125
|
+
? enableProfile(gated, profiles, order, previous, !isCloud)
|
|
127
126
|
: disableProfile(gated.key, llm, profiles, order, previous);
|
|
128
127
|
if (profileChanged) {
|
|
129
128
|
changed = true;
|
|
@@ -201,16 +201,7 @@ export async function persistPromptedCredential(args: {
|
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
203
|
|
|
204
|
-
|
|
205
|
-
// routinely used to RE-collect a secret the user already pasted into chat
|
|
206
|
-
// (the 06-credential-security rail promises that pasted message is
|
|
207
|
-
// scrubbed after storage). Run the scrub right after the store succeeds,
|
|
208
|
-
// best-effort and invisible to the caller, mirroring credentials_set.
|
|
209
|
-
const scrubbed = await scrubStoredCredentialFromTranscripts(value);
|
|
210
|
-
log.info(
|
|
211
|
-
{ service, field, ...scrubbed },
|
|
212
|
-
"Prompted credential stored; scrubbed value from recent transcripts",
|
|
213
|
-
);
|
|
204
|
+
await scrubStoredCredentialFromTranscripts(value, { service, field });
|
|
214
205
|
|
|
215
206
|
try {
|
|
216
207
|
upsertCredentialMetadata(service, field, {
|
|
@@ -668,10 +668,14 @@ export async function checkAssistantApiKey(): Promise<CredentialHealthResult | n
|
|
|
668
668
|
|
|
669
669
|
if (stored.value == null) {
|
|
670
670
|
// A workspace that was never connected to the cloud has no managed
|
|
671
|
-
// credential
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
671
|
+
// credential yet — missing, not unhealthy.
|
|
672
|
+
return {
|
|
673
|
+
...base,
|
|
674
|
+
status: "missing_token",
|
|
675
|
+
details:
|
|
676
|
+
"No Cadet Cloud credentials are stored yet. Log in to Cadet Cloud to set them up.",
|
|
677
|
+
canAutoRecover: true,
|
|
678
|
+
};
|
|
675
679
|
}
|
|
676
680
|
|
|
677
681
|
const { CadetCloudClient } = await import("../cloud/client.js");
|
|
@@ -86,8 +86,15 @@ export interface TranscriptScrubResult {
|
|
|
86
86
|
* Never throws — partial failures are logged (counts only, never content)
|
|
87
87
|
* and reflected in the best-effort result.
|
|
88
88
|
*/
|
|
89
|
+
export interface TranscriptScrubContext {
|
|
90
|
+
service?: string;
|
|
91
|
+
field?: string;
|
|
92
|
+
provider?: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
89
95
|
export async function scrubStoredCredentialFromTranscripts(
|
|
90
96
|
value: string,
|
|
97
|
+
context: TranscriptScrubContext = {},
|
|
91
98
|
): Promise<TranscriptScrubResult> {
|
|
92
99
|
const result: TranscriptScrubResult = {
|
|
93
100
|
dbMessagesScrubbed: 0,
|
|
@@ -120,6 +127,10 @@ export async function scrubStoredCredentialFromTranscripts(
|
|
|
120
127
|
"Credential transcript scrub failed",
|
|
121
128
|
);
|
|
122
129
|
}
|
|
130
|
+
log.info(
|
|
131
|
+
{ ...context, ...result },
|
|
132
|
+
"Credential stored; scrubbed value from recent transcripts",
|
|
133
|
+
);
|
|
123
134
|
return result;
|
|
124
135
|
}
|
|
125
136
|
|
|
@@ -209,8 +209,13 @@ interface TierKeyExemption {
|
|
|
209
209
|
* tuning when building the graph view.
|
|
210
210
|
* - `src/memory-v2-routes.ts` (v2) — the v2 tuning routes read and merge
|
|
211
211
|
* `memory.v2` config directly.
|
|
212
|
+
* - `config/substrate-twin-shadowing.ts` (substrate) — the operator-facing
|
|
213
|
+
* twin detector: it reads substrate PRESENCE so a `memory.v2` write can
|
|
214
|
+
* warn that the twin namespace wins. It never resolves a tunable itself —
|
|
215
|
+
* resolution stays in `substrate/tuning.ts`.
|
|
212
216
|
*/
|
|
213
217
|
const TIER_KEY_READ_ALLOWLIST: readonly TierKeyExemption[] = [
|
|
218
|
+
{ path: "src/config/substrate-twin-shadowing.ts", keys: ["substrate"] },
|
|
214
219
|
{ path: "src/config/loader.ts", keys: ["v3"] },
|
|
215
220
|
{ path: "src/config/memory-v3-gate.ts", keys: ["v2", "v3"] },
|
|
216
221
|
{
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
|
|
20
20
|
import { MANAGED_PROFILE_NAMES } from "../../config/default-profile-catalog.js";
|
|
21
|
+
import { getIsCloud } from "../../config/env-registry.js";
|
|
21
22
|
import { loadRawConfig, saveRawConfig } from "../../config/loader.js";
|
|
22
23
|
import type { DrizzleDb } from "../../persistence/db-connection.js";
|
|
23
24
|
import { credentialKey } from "../../security/credential-key.js";
|
|
@@ -73,8 +74,7 @@ function backfillConfigProfiles(db: DrizzleDb): void {
|
|
|
73
74
|
return;
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
const isCloud =
|
|
77
|
-
process.env.IS_CLOUD === "true" || process.env.IS_CLOUD === "1";
|
|
77
|
+
const isCloud = getIsCloud();
|
|
78
78
|
const globalMode = isCloud ? "managed" : "your-own";
|
|
79
79
|
|
|
80
80
|
let changed = false;
|