@simonsbs/keylore 1.0.0-rc4 → 1.0.0-rc6
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 +5 -1
- package/README.md +27 -15
- package/bin/keylore-http.js +10 -2
- package/data/catalog.json +4 -0
- package/dist/app.js +2 -2
- package/dist/config.js +11 -7
- package/dist/domain/types.js +60 -46
- package/dist/http/admin-ui.js +1068 -371
- package/dist/http/server.js +173 -0
- package/dist/http-service.js +167 -0
- package/dist/mcp/create-server.js +4 -4
- package/dist/repositories/credential-repository.js +37 -7
- package/dist/repositories/pg-credential-repository.js +50 -11
- package/dist/services/backup-service.js +10 -4
- package/dist/services/core-mode-service.js +17 -1
- package/dist/storage/database.js +85 -2
- package/dist/storage/in-memory-database.js +10 -1
- package/dist/storage/migrations.js +2 -2
- package/migrations/009_v1_context_split.sql +30 -0
- package/package.json +2 -1
package/.env.example
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
|
|
1
|
+
KEYLORE_DATABASE_MODE=local
|
|
2
|
+
KEYLORE_LOCAL_DATABASE_FILE=keylore.db
|
|
3
|
+
# Optional advanced override:
|
|
4
|
+
# KEYLORE_DATABASE_MODE=postgres
|
|
5
|
+
# KEYLORE_DATABASE_URL=postgresql://keylore:keylore@127.0.0.1:5432/keylore
|
|
2
6
|
KEYLORE_DATABASE_POOL_MAX=10
|
|
3
7
|
KEYLORE_AUTH_CLIENTS_FILE=auth-clients.json
|
|
4
8
|
KEYLORE_LOCAL_SECRETS_FILE=local-secrets.enc.json
|
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ This repository is incubating privately today, but it is structured to be publis
|
|
|
22
22
|
- MCP server for `stdio` and Streamable HTTP
|
|
23
23
|
- metadata-only catalogue search and retrieval tools
|
|
24
24
|
- default-deny policy engine with domain and operation constraints
|
|
25
|
-
-
|
|
25
|
+
- file-backed local persistence by default with startup migrations, plus explicit PostgreSQL support for advanced deployments
|
|
26
26
|
- OAuth-style client credentials token issuance for remote HTTP and MCP access
|
|
27
27
|
- PKCE-bound `authorization_code` plus rotating `refresh_token` support for interactive public or confidential clients
|
|
28
28
|
- protected-resource metadata for REST and MCP surfaces
|
|
@@ -64,13 +64,13 @@ This repository is incubating privately today, but it is structured to be publis
|
|
|
64
64
|
|
|
65
65
|
## What is intentionally deferred
|
|
66
66
|
|
|
67
|
-
The full `KeyLore.md` specification is broader than a sane `v1.0.0-
|
|
67
|
+
The full `KeyLore.md` specification is broader than a sane `v1.0.0-rc6` delivery. The main remaining work before `v1.0.0` is:
|
|
68
68
|
|
|
69
69
|
- public release polish and final operator documentation cleanup
|
|
70
70
|
|
|
71
71
|
Those items are tracked in [docs/roadmap.md](/home/simon/keylore/docs/roadmap.md) and mapped back to the spec in [docs/keylore-spec-map.md](/home/simon/keylore/docs/keylore-spec-map.md).
|
|
72
72
|
|
|
73
|
-
The active post-`v1.0.0-
|
|
73
|
+
The active post-`v1.0.0-rc6` refocus is documented in [docs/core-mode-plan.md](/home/simon/keylore/docs/core-mode-plan.md): make the default user journey "add secret, add context, connect MCP, use it" and push broader operator features behind an advanced path.
|
|
74
74
|
|
|
75
75
|
The handoff from local core mode to advanced self-hosted mode is documented in [docs/production-handoff.md](/home/simon/keylore/docs/production-handoff.md).
|
|
76
76
|
|
|
@@ -88,14 +88,16 @@ npm install
|
|
|
88
88
|
npm run quickstart
|
|
89
89
|
```
|
|
90
90
|
|
|
91
|
+
That starts KeyLore in the background. Use `keylore-http status`, `keylore-http stop`, and `keylore-http restart` to manage it later. Use `keylore-http run` only when you intentionally want the server attached to the terminal for debugging.
|
|
92
|
+
|
|
91
93
|
For a clean Linux VM install from npm instead of cloning the repo:
|
|
92
94
|
|
|
93
95
|
```bash
|
|
94
96
|
npm install -g @simonsbs/keylore@next
|
|
95
|
-
keylore-http
|
|
97
|
+
keylore-http start
|
|
96
98
|
```
|
|
97
99
|
|
|
98
|
-
That starts KeyLore from the packaged migrations and seed data
|
|
100
|
+
That starts KeyLore from the packaged migrations and seed data with no Docker or external PostgreSQL required. Writable state defaults to `~/.keylore`.
|
|
99
101
|
|
|
100
102
|
To simulate a brand-new user install on the same machine without reusing your normal checkout or shell environment:
|
|
101
103
|
|
|
@@ -105,8 +107,8 @@ npm run ops:fresh-user-env
|
|
|
105
107
|
|
|
106
108
|
That launches an isolated disposable user, a fresh clone, and a separate KeyLore UI port for onboarding and MCP testing. By default it clones from the current local repo source so it also works while the repo is private.
|
|
107
109
|
|
|
108
|
-
This
|
|
109
|
-
If KeyLore is already running locally
|
|
110
|
+
This boots KeyLore at `http://127.0.0.1:8787` with a local embedded database and encrypted local secret store.
|
|
111
|
+
If KeyLore is already running locally, the command reuses the existing background instance instead of failing.
|
|
110
112
|
|
|
111
113
|
3. Open KeyLore in your browser:
|
|
112
114
|
|
|
@@ -116,28 +118,36 @@ KeyLore now redirects `/` to `/admin` and automatically opens a local operator s
|
|
|
116
118
|
|
|
117
119
|
If that local session bootstrap fails for any reason, use `Start working locally` or the manual sign-in form shown on the page.
|
|
118
120
|
|
|
119
|
-
4. In `
|
|
121
|
+
4. In `Quick start`, follow the short path: add token, test token, then connect your AI tool.
|
|
122
|
+
|
|
123
|
+
5. In `Your tokens`, click `Add token`, choose the closest template for the token you are adding, such as `GitHub read-only`, `GitHub write-capable`, `npm read-only`, or `Internal service token`, then fill in:
|
|
120
124
|
- `Name shown in KeyLore`
|
|
121
125
|
- `Token key`
|
|
122
126
|
- `Paste token`
|
|
123
127
|
- `Where can it be used?`
|
|
128
|
+
- `Explain this token for people`
|
|
124
129
|
- `Tell the AI when to use this token`
|
|
125
130
|
|
|
126
|
-
That stores the raw token outside the searchable catalogue and keeps only the
|
|
131
|
+
That stores the raw token outside the searchable catalogue and keeps only the metadata record in the credential catalogue.
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
6. Review `Writing help` and `What the AI will see` in the form to confirm the record is specific, useful, and secret-free. `LLM context` is the primary retrieval hint for agents. `User context` explains the human purpose of the token. `Token key` is the unique identifier for the token; if KeyLore says a token already exists, change that field and save again. Open `Advanced token settings` only if you need to change storage mode, risk level, service name, tags, or write access.
|
|
129
134
|
|
|
130
|
-
|
|
135
|
+
7. In `Saved tokens`, everything is now listed together in one place. Example records are marked as examples, and they can be edited or deleted from the same list.
|
|
131
136
|
|
|
132
|
-
|
|
137
|
+
8. In `Test credential`, choose `Token to check`, set the `URL to call with this token`, and run the check.
|
|
133
138
|
|
|
134
139
|
The check makes a real brokered `http.get` call with that token and URL. Success means the token, the target domain, and KeyLore policy all allowed the request.
|
|
135
140
|
|
|
136
|
-
|
|
141
|
+
9. In `Connect your AI tool`, pick the tool tab you want:
|
|
142
|
+
- `Codex`: copy the snippet or click `Apply to my Codex settings` to merge it into `~/.codex/config.toml`, then restart Codex
|
|
143
|
+
- `Gemini CLI`: copy the snippet or click `Apply to my Gemini settings` to merge it into `~/.gemini/settings.json`, then restart Gemini
|
|
144
|
+
- `Claude CLI`: copy the command or click `Apply to my Claude settings` to register KeyLore, then restart Claude
|
|
145
|
+
|
|
146
|
+
Use the built-in `First prompt to try` example after restarting the client. If you want remote HTTP MCP instead, open `Remote or advanced connection options` and mint an `/mcp` token there.
|
|
137
147
|
|
|
138
148
|
Everything beyond that now sits behind `Show advanced controls` in the UI, so a first-run user can ignore tenants, OAuth client administration, approvals, backups, audit, and system internals entirely.
|
|
139
149
|
|
|
140
|
-
After creation, use `
|
|
150
|
+
After creation, use `Edit token` from the saved-token list if you need to refine the metadata or replace the stored value for a locally stored token. Token creation and editing both happen in a popup now, while the main page stays focused on the token list, testing, and MCP connection.
|
|
141
151
|
|
|
142
152
|
When that local path stops being enough, use [docs/production-handoff.md](/home/simon/keylore/docs/production-handoff.md) to decide when to switch to external secret backends, real OAuth clients, approvals, and tenant-separated self-hosting.
|
|
143
153
|
|
|
@@ -164,12 +174,14 @@ KEYLORE_SANDBOX_COMMAND_ALLOWLIST=/usr/bin/env,node
|
|
|
164
174
|
|
|
165
175
|
## Advanced local usage
|
|
166
176
|
|
|
167
|
-
|
|
177
|
+
If you want production-style external persistence locally, start PostgreSQL first:
|
|
168
178
|
|
|
169
179
|
```bash
|
|
170
180
|
npm run db:up
|
|
171
181
|
```
|
|
172
182
|
|
|
183
|
+
Then either set `KEYLORE_DATABASE_MODE=postgres` and `KEYLORE_DATABASE_URL=...` in `.env`, or export them for one run.
|
|
184
|
+
|
|
173
185
|
Start the HTTP server directly:
|
|
174
186
|
|
|
175
187
|
```bash
|
package/bin/keylore-http.js
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
|
|
5
|
+
const binDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
const runtimeEntryPath = path.resolve(binDir, "../dist/index.js");
|
|
7
|
+
const serviceManagerPath = path.resolve(binDir, "../dist/http-service.js");
|
|
8
|
+
|
|
9
|
+
const { runHttpServiceCommand } = await import(serviceManagerPath);
|
|
10
|
+
const exitCode = await runHttpServiceCommand(process.argv.slice(2), runtimeEntryPath);
|
|
11
|
+
process.exit(exitCode);
|
package/data/catalog.json
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
"expiresAt": null,
|
|
14
14
|
"rotationPolicy": "Rotate every 90 days",
|
|
15
15
|
"lastValidatedAt": "2026-03-13T00:00:00.000Z",
|
|
16
|
+
"userContext": "Shared example credential for GitHub metadata lookups in local demos. Keep it read-only and prefer a real user-owned token when one exists.",
|
|
17
|
+
"llmContext": "Fallback GitHub read-only demo credential. Use for repository metadata, issue lookup, release inspection, and rate-limit reads only when no better user-owned GitHub token is available. Never use for write operations.",
|
|
16
18
|
"selectionNotes": "Use for repository metadata, issue lookup, and release inspection. Never use for write operations.",
|
|
17
19
|
"binding": {
|
|
18
20
|
"adapter": "env",
|
|
@@ -37,6 +39,8 @@
|
|
|
37
39
|
"expiresAt": null,
|
|
38
40
|
"rotationPolicy": "Rotate every 90 days",
|
|
39
41
|
"lastValidatedAt": "2026-03-13T00:00:00.000Z",
|
|
42
|
+
"userContext": "Shared example credential for npm package metadata inspection in local demos. Keep it read-only and do not use it for publish workflows.",
|
|
43
|
+
"llmContext": "Fallback npm read-only demo credential. Use for package metadata inspection, dependency lookup, and registry read operations only. Never use for publish or package mutation workflows.",
|
|
40
44
|
"selectionNotes": "Use for package metadata inspection only. Publishing is intentionally excluded from the default demo policy.",
|
|
41
45
|
"binding": {
|
|
42
46
|
"adapter": "env",
|
package/dist/app.js
CHANGED
|
@@ -38,13 +38,13 @@ import { TenantService } from "./services/tenant-service.js";
|
|
|
38
38
|
import { TraceExportService } from "./services/trace-export-service.js";
|
|
39
39
|
import { TraceService } from "./services/trace-service.js";
|
|
40
40
|
import { bootstrapFromFiles } from "./storage/bootstrap.js";
|
|
41
|
-
import {
|
|
41
|
+
import { createSqlDatabase } from "./storage/database.js";
|
|
42
42
|
import { runMigrations } from "./storage/migrations.js";
|
|
43
43
|
import { SandboxRunner } from "./runtime/sandbox-runner.js";
|
|
44
44
|
export async function createKeyLoreApp() {
|
|
45
45
|
const config = loadConfig();
|
|
46
46
|
const logger = pino({ name: config.appName, level: config.logLevel });
|
|
47
|
-
const database =
|
|
47
|
+
const database = await createSqlDatabase(config);
|
|
48
48
|
const telemetry = new TelemetryService();
|
|
49
49
|
const traces = new TraceService(config.traceCaptureEnabled, config.traceRecentSpanLimit);
|
|
50
50
|
const traceExports = new TraceExportService(config.traceExportUrl, config.traceExportAuthHeader, config.traceExportBatchSize, config.traceExportIntervalMs, config.traceExportTimeoutMs, telemetry);
|
package/dist/config.js
CHANGED
|
@@ -3,7 +3,6 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import * as z from "zod/v4";
|
|
6
|
-
const LOCAL_DATABASE_URL = "postgresql://keylore:keylore@127.0.0.1:5432/keylore";
|
|
7
6
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
7
|
const LOCAL_ADMIN_CLIENT_ID = "keylore-admin-local";
|
|
9
8
|
const LOCAL_ADMIN_CLIENT_SECRET = "keylore-local-admin";
|
|
@@ -73,9 +72,6 @@ function hydrateEnvironment(cwd) {
|
|
|
73
72
|
...fileEnv,
|
|
74
73
|
...process.env,
|
|
75
74
|
};
|
|
76
|
-
if (isMissing(effectiveEnv.KEYLORE_DATABASE_URL)) {
|
|
77
|
-
effectiveEnv.KEYLORE_DATABASE_URL = LOCAL_DATABASE_URL;
|
|
78
|
-
}
|
|
79
75
|
const environment = effectiveEnv.KEYLORE_ENVIRONMENT?.trim() || "development";
|
|
80
76
|
const httpHost = effectiveEnv.KEYLORE_HTTP_HOST?.trim() || "127.0.0.1";
|
|
81
77
|
const bootstrapFromFiles = effectiveEnv.KEYLORE_BOOTSTRAP_FROM_FILES !== "false";
|
|
@@ -113,14 +109,16 @@ function resolveDefaultDataDir(cwd) {
|
|
|
113
109
|
return path.join(os.homedir(), ".keylore");
|
|
114
110
|
}
|
|
115
111
|
const envSchema = z.object({
|
|
112
|
+
KEYLORE_DATABASE_MODE: z.enum(["local", "postgres"]).optional(),
|
|
116
113
|
KEYLORE_DATA_DIR: z.string().optional(),
|
|
117
114
|
KEYLORE_CATALOG_FILE: z.string().optional(),
|
|
118
115
|
KEYLORE_POLICY_FILE: z.string().optional(),
|
|
119
116
|
KEYLORE_AUTH_CLIENTS_FILE: z.string().optional(),
|
|
120
117
|
KEYLORE_MIGRATIONS_DIR: z.string().optional(),
|
|
118
|
+
KEYLORE_LOCAL_DATABASE_FILE: z.string().optional(),
|
|
121
119
|
KEYLORE_LOCAL_SECRETS_FILE: z.string().optional(),
|
|
122
120
|
KEYLORE_LOCAL_SECRETS_KEY_FILE: z.string().optional(),
|
|
123
|
-
KEYLORE_DATABASE_URL:
|
|
121
|
+
KEYLORE_DATABASE_URL: optionalString,
|
|
124
122
|
KEYLORE_DATABASE_POOL_MAX: z.coerce.number().int().min(1).max(100).default(10),
|
|
125
123
|
KEYLORE_HTTP_HOST: z.string().default("127.0.0.1"),
|
|
126
124
|
KEYLORE_HTTP_PORT: z.coerce.number().int().min(1).max(65535).default(8787),
|
|
@@ -196,22 +194,28 @@ export function loadConfig(cwd = process.cwd()) {
|
|
|
196
194
|
const env = envSchema.parse(hydrated.env);
|
|
197
195
|
const runtimeRoot = resolveRuntimeRoot(cwd);
|
|
198
196
|
const dataDir = path.resolve(env.KEYLORE_DATA_DIR ?? resolveDefaultDataDir(cwd));
|
|
197
|
+
const databaseMode = env.KEYLORE_DATABASE_MODE ?? (env.KEYLORE_DATABASE_URL ? "postgres" : "local");
|
|
199
198
|
const publicBaseUrl = env.KEYLORE_PUBLIC_BASE_URL ?? `http://${env.KEYLORE_HTTP_HOST}:${env.KEYLORE_HTTP_PORT}`;
|
|
200
199
|
const oauthIssuerUrl = env.KEYLORE_OAUTH_ISSUER_URL ?? `${publicBaseUrl}/oauth`;
|
|
201
200
|
const localQuickstartEnabled = env.KEYLORE_ENVIRONMENT !== "production" &&
|
|
202
201
|
env.KEYLORE_BOOTSTRAP_FROM_FILES &&
|
|
203
202
|
isLoopbackHost(env.KEYLORE_HTTP_HOST);
|
|
203
|
+
if (databaseMode === "postgres" && isMissing(env.KEYLORE_DATABASE_URL)) {
|
|
204
|
+
throw new Error("KEYLORE_DATABASE_URL is required when KEYLORE_DATABASE_MODE=postgres.");
|
|
205
|
+
}
|
|
204
206
|
return {
|
|
205
207
|
appName: "keylore",
|
|
206
|
-
version: "1.0.0-
|
|
208
|
+
version: "1.0.0-rc6",
|
|
207
209
|
dataDir,
|
|
208
210
|
bootstrapCatalogPath: path.resolve(runtimeRoot, "data", env.KEYLORE_CATALOG_FILE ?? "catalog.json"),
|
|
209
211
|
bootstrapPolicyPath: path.resolve(runtimeRoot, "data", env.KEYLORE_POLICY_FILE ?? "policies.json"),
|
|
210
212
|
bootstrapAuthClientsPath: path.resolve(runtimeRoot, "data", env.KEYLORE_AUTH_CLIENTS_FILE ?? "auth-clients.json"),
|
|
211
213
|
migrationsDir: path.resolve(runtimeRoot, env.KEYLORE_MIGRATIONS_DIR ?? "migrations"),
|
|
214
|
+
databaseMode,
|
|
215
|
+
localDatabasePath: path.resolve(dataDir, env.KEYLORE_LOCAL_DATABASE_FILE ?? "keylore.db"),
|
|
212
216
|
localSecretsFilePath: path.resolve(dataDir, env.KEYLORE_LOCAL_SECRETS_FILE ?? "local-secrets.enc.json"),
|
|
213
217
|
localSecretsKeyPath: path.resolve(dataDir, env.KEYLORE_LOCAL_SECRETS_KEY_FILE ?? "local-secrets.key"),
|
|
214
|
-
databaseUrl: env.KEYLORE_DATABASE_URL,
|
|
218
|
+
databaseUrl: env.KEYLORE_DATABASE_URL || undefined,
|
|
215
219
|
databasePoolMax: env.KEYLORE_DATABASE_POOL_MAX,
|
|
216
220
|
httpHost: env.KEYLORE_HTTP_HOST,
|
|
217
221
|
httpPort: env.KEYLORE_HTTP_PORT,
|
package/dist/domain/types.js
CHANGED
|
@@ -113,6 +113,8 @@ export const credentialRecordSchema = z.object({
|
|
|
113
113
|
rotationPolicy: z.string().min(1),
|
|
114
114
|
lastValidatedAt: z.string().datetime().nullable(),
|
|
115
115
|
selectionNotes: z.string().min(1),
|
|
116
|
+
userContext: z.string().min(1).optional(),
|
|
117
|
+
llmContext: z.string().min(1).optional(),
|
|
116
118
|
binding: credentialBindingSchema,
|
|
117
119
|
tags: z.array(z.string().min(1)).default([]),
|
|
118
120
|
status: credentialStatusSchema.default("active"),
|
|
@@ -210,6 +212,52 @@ export const catalogSearchInputSchema = z.object({
|
|
|
210
212
|
export const createCredentialInputSchema = credentialRecordSchema;
|
|
211
213
|
const secretLikeSelectionNotesPattern = /(gh[pousr]_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|sk-[A-Za-z0-9_-]+|AKIA[0-9A-Z]{16})/;
|
|
212
214
|
const vagueSelectionNotesPattern = /^(use when needed|general use|general purpose|for api|api token|token for api|default token|main token)$/i;
|
|
215
|
+
function normalizedLlmContext(value) {
|
|
216
|
+
return (value.llmContext ?? value.selectionNotes ?? "").trim();
|
|
217
|
+
}
|
|
218
|
+
function normalizedUserContext(value) {
|
|
219
|
+
return (value.userContext ?? normalizedLlmContext(value)).trim();
|
|
220
|
+
}
|
|
221
|
+
function validateLlmContext(llmContext, ctx, path) {
|
|
222
|
+
if (!llmContext) {
|
|
223
|
+
ctx.addIssue({
|
|
224
|
+
code: z.ZodIssueCode.custom,
|
|
225
|
+
path,
|
|
226
|
+
message: "LLM context is required. Explain when the agent should use this credential.",
|
|
227
|
+
});
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (llmContext.length < 16) {
|
|
231
|
+
ctx.addIssue({
|
|
232
|
+
code: z.ZodIssueCode.custom,
|
|
233
|
+
path,
|
|
234
|
+
message: "LLM context must explain when the agent should use this credential in more detail.",
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
if (vagueSelectionNotesPattern.test(llmContext)) {
|
|
238
|
+
ctx.addIssue({
|
|
239
|
+
code: z.ZodIssueCode.custom,
|
|
240
|
+
path,
|
|
241
|
+
message: "LLM context is too vague. Describe the target service, intended use, and what the agent should avoid.",
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (secretLikeSelectionNotesPattern.test(llmContext)) {
|
|
245
|
+
ctx.addIssue({
|
|
246
|
+
code: z.ZodIssueCode.custom,
|
|
247
|
+
path,
|
|
248
|
+
message: "LLM context must not contain token-like secret material.",
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function validateUserContext(userContext, ctx, path) {
|
|
253
|
+
if (!userContext) {
|
|
254
|
+
ctx.addIssue({
|
|
255
|
+
code: z.ZodIssueCode.custom,
|
|
256
|
+
path,
|
|
257
|
+
message: "User context is required. Describe the human purpose of this credential.",
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
}
|
|
213
261
|
export const coreCredentialCreateInputSchema = z
|
|
214
262
|
.object({
|
|
215
263
|
credentialId: z.string().min(1),
|
|
@@ -221,7 +269,9 @@ export const coreCredentialCreateInputSchema = z
|
|
|
221
269
|
sensitivity: sensitivitySchema.default("high"),
|
|
222
270
|
allowedDomains: z.array(z.string().min(1)).min(1),
|
|
223
271
|
permittedOperations: z.array(operationSchema).min(1).default(["http.get"]),
|
|
224
|
-
selectionNotes: z.string().min(1),
|
|
272
|
+
selectionNotes: z.string().min(1).optional(),
|
|
273
|
+
userContext: z.string().min(1).optional(),
|
|
274
|
+
llmContext: z.string().min(1).optional(),
|
|
225
275
|
rotationPolicy: z.string().default("Managed locally"),
|
|
226
276
|
tags: z.array(z.string().min(1)).default([]),
|
|
227
277
|
status: credentialStatusSchema.default("active"),
|
|
@@ -251,28 +301,8 @@ export const coreCredentialCreateInputSchema = z
|
|
|
251
301
|
message: "Bearer credentials should include a non-empty header prefix.",
|
|
252
302
|
});
|
|
253
303
|
}
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
ctx.addIssue({
|
|
257
|
-
code: z.ZodIssueCode.custom,
|
|
258
|
-
path: ["selectionNotes"],
|
|
259
|
-
message: "Selection notes must explain when the agent should use this credential in more detail.",
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
if (vagueSelectionNotesPattern.test(selectionNotes)) {
|
|
263
|
-
ctx.addIssue({
|
|
264
|
-
code: z.ZodIssueCode.custom,
|
|
265
|
-
path: ["selectionNotes"],
|
|
266
|
-
message: "Selection notes are too vague. Describe the target service, intended use, and what the agent should avoid.",
|
|
267
|
-
});
|
|
268
|
-
}
|
|
269
|
-
if (secretLikeSelectionNotesPattern.test(selectionNotes)) {
|
|
270
|
-
ctx.addIssue({
|
|
271
|
-
code: z.ZodIssueCode.custom,
|
|
272
|
-
path: ["selectionNotes"],
|
|
273
|
-
message: "Selection notes must not contain token-like secret material.",
|
|
274
|
-
});
|
|
275
|
-
}
|
|
304
|
+
validateLlmContext(normalizedLlmContext(value), ctx, ["llmContext"]);
|
|
305
|
+
validateUserContext(normalizedUserContext(value), ctx, ["userContext"]);
|
|
276
306
|
if (value.permittedOperations.includes("http.post") && value.scopeTier === "read_only") {
|
|
277
307
|
ctx.addIssue({
|
|
278
308
|
code: z.ZodIssueCode.custom,
|
|
@@ -293,6 +323,8 @@ export const coreCredentialContextUpdateInputSchema = z
|
|
|
293
323
|
allowedDomains: z.array(z.string().min(1)).min(1).optional(),
|
|
294
324
|
permittedOperations: z.array(operationSchema).min(1).optional(),
|
|
295
325
|
selectionNotes: z.string().min(1).optional(),
|
|
326
|
+
userContext: z.string().min(1).optional(),
|
|
327
|
+
llmContext: z.string().min(1).optional(),
|
|
296
328
|
tags: z.array(z.string().min(1)).optional(),
|
|
297
329
|
status: credentialStatusSchema.optional(),
|
|
298
330
|
})
|
|
@@ -300,29 +332,11 @@ export const coreCredentialContextUpdateInputSchema = z
|
|
|
300
332
|
message: "At least one context field must be provided.",
|
|
301
333
|
})
|
|
302
334
|
.superRefine((value, ctx) => {
|
|
303
|
-
if (value.selectionNotes !== undefined
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
path: ["selectionNotes"],
|
|
309
|
-
message: "Selection notes must explain when the agent should use this credential in more detail.",
|
|
310
|
-
});
|
|
311
|
-
}
|
|
312
|
-
if (vagueSelectionNotesPattern.test(selectionNotes)) {
|
|
313
|
-
ctx.addIssue({
|
|
314
|
-
code: z.ZodIssueCode.custom,
|
|
315
|
-
path: ["selectionNotes"],
|
|
316
|
-
message: "Selection notes are too vague. Describe the target service, intended use, and what the agent should avoid.",
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
|
-
if (secretLikeSelectionNotesPattern.test(selectionNotes)) {
|
|
320
|
-
ctx.addIssue({
|
|
321
|
-
code: z.ZodIssueCode.custom,
|
|
322
|
-
path: ["selectionNotes"],
|
|
323
|
-
message: "Selection notes must not contain token-like secret material.",
|
|
324
|
-
});
|
|
325
|
-
}
|
|
335
|
+
if (value.selectionNotes !== undefined ||
|
|
336
|
+
value.llmContext !== undefined ||
|
|
337
|
+
value.userContext !== undefined) {
|
|
338
|
+
validateLlmContext(normalizedLlmContext(value), ctx, ["llmContext"]);
|
|
339
|
+
validateUserContext(normalizedUserContext(value), ctx, ["userContext"]);
|
|
326
340
|
}
|
|
327
341
|
const scopeTier = value.scopeTier;
|
|
328
342
|
const permittedOperations = value.permittedOperations;
|