@alfe.ai/openclaw-secrets 0.1.9 → 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/.turbo/turbo-build.log +1 -1
- package/CHANGELOG.md +16 -0
- package/dist/crypto.d.ts +4 -0
- package/dist/crypto.d.ts.map +1 -1
- package/dist/crypto.js +4 -2
- package/dist/crypto.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +375 -117
- package/dist/index.js.map +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -2
- package/src/crypto.ts +8 -2
- package/src/index.ts +386 -118
package/src/index.ts
CHANGED
|
@@ -2,27 +2,39 @@
|
|
|
2
2
|
* openclaw-secrets — OpenClaw plugin for per-scope encrypted secret storage.
|
|
3
3
|
*
|
|
4
4
|
* Trust model:
|
|
5
|
-
* - Secret
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* -
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
5
|
+
* - Secret VALUES (encrypted fields) are encrypted and decrypted ON THIS
|
|
6
|
+
* AGENT using AES-256-GCM with KMS-issued data keys. The backend never
|
|
7
|
+
* sees plaintext for encrypted fields.
|
|
8
|
+
* - Plaintext fields ride the wire as-is (they're not secret material —
|
|
9
|
+
* things like email, URL, username; the suspicious-key denylist on the
|
|
10
|
+
* server rejects obvious mistakes).
|
|
11
|
+
* - Per-field encryption: each encrypted field has its own data key,
|
|
12
|
+
* bound by KMS encryption context to (tenantId, scope, scopeId,
|
|
13
|
+
* secretId, fieldKey). Cross-field swap attacks fail at decrypt.
|
|
14
14
|
*
|
|
15
15
|
* Tools:
|
|
16
|
-
* -
|
|
17
|
-
* -
|
|
18
|
-
* -
|
|
19
|
-
* -
|
|
20
|
-
* -
|
|
21
|
-
* -
|
|
22
|
-
* -
|
|
16
|
+
* - secret_list_scopes — enumerate scopes the agent can access
|
|
17
|
+
* - secret_list — list secret metadata in a scope (with optional filters)
|
|
18
|
+
* - secret_set — create a secret with one or more fields
|
|
19
|
+
* - secret_get — legacy wrapper: returns the `value` field's plaintext as a string
|
|
20
|
+
* - secret_get_full — full aggregate with all fields (plaintext + decrypted encrypted)
|
|
21
|
+
* - secret_get_field — fetch and decrypt one field by key
|
|
22
|
+
* - secret_set_field — add OR rotate one field
|
|
23
|
+
* - secret_remove_field — remove one field row
|
|
24
|
+
* - secret_update_metadata — name/description/tags/category PATCH
|
|
25
|
+
* - secret_search — facet GSI search by category/tag/fieldKey
|
|
26
|
+
* - secret_get_history — bounded changelog read (metadata only, never values)
|
|
27
|
+
* - secret_delete — delete a secret
|
|
23
28
|
*/
|
|
24
29
|
import { randomUUID } from "node:crypto";
|
|
25
|
-
import {
|
|
30
|
+
import {
|
|
31
|
+
AgentApiClient,
|
|
32
|
+
type SecretScope,
|
|
33
|
+
type SecretCategory,
|
|
34
|
+
type FieldFormat,
|
|
35
|
+
type FieldSensitivity,
|
|
36
|
+
type Field,
|
|
37
|
+
} from "@alfe.ai/agent-api-client";
|
|
26
38
|
import { resolveConfig as resolveAlfeConfig } from "@alfe.ai/config";
|
|
27
39
|
import { decryptSecretEnvelope, encryptSecretValue } from "./crypto.js";
|
|
28
40
|
import { createRequire } from 'node:module';
|
|
@@ -62,10 +74,16 @@ interface PluginApi {
|
|
|
62
74
|
}
|
|
63
75
|
|
|
64
76
|
const VALID_SCOPES: SecretScope[] = ["org", "team", "project", "agent"];
|
|
77
|
+
const VALID_CATEGORIES: SecretCategory[] = [
|
|
78
|
+
"login", "api_key", "database", "ssh_key", "certificate",
|
|
79
|
+
"secure_note", "credit_card", "identity", "wifi", "other",
|
|
80
|
+
];
|
|
81
|
+
const VALID_FORMATS: FieldFormat[] = ["text", "email", "url", "phone", "date", "number", "json"];
|
|
82
|
+
const VALID_SENSITIVITIES: FieldSensitivity[] = ["plaintext", "encrypted"];
|
|
65
83
|
const NAME_REGEX = /^[a-zA-Z0-9_./-]{1,128}$/;
|
|
84
|
+
const FIELD_KEY_REGEX = /^[a-zA-Z0-9_./-]{1,64}$/;
|
|
66
85
|
const UUID_V4 = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
67
86
|
|
|
68
|
-
|
|
69
87
|
function parseScope(raw: unknown, field = "scope"): SecretScope {
|
|
70
88
|
if (typeof raw !== "string" || !VALID_SCOPES.includes(raw as SecretScope)) {
|
|
71
89
|
throw new Error(`${field} must be one of ${VALID_SCOPES.join(", ")}`);
|
|
@@ -91,16 +109,51 @@ function parseSecretName(raw: unknown): string {
|
|
|
91
109
|
return s;
|
|
92
110
|
}
|
|
93
111
|
|
|
112
|
+
function parseFieldKey(raw: unknown): string {
|
|
113
|
+
const s = parseString(raw, "key", 64);
|
|
114
|
+
if (!FIELD_KEY_REGEX.test(s)) {
|
|
115
|
+
throw new Error("field key must match ^[a-zA-Z0-9_./-]{1,64}$");
|
|
116
|
+
}
|
|
117
|
+
return s;
|
|
118
|
+
}
|
|
119
|
+
|
|
94
120
|
function parseSecretId(raw: unknown): string {
|
|
95
121
|
const s = parseString(raw, "secretId", 64);
|
|
96
122
|
if (!UUID_V4.test(s)) throw new Error("secretId must be a UUID v4");
|
|
97
123
|
return s;
|
|
98
124
|
}
|
|
99
125
|
|
|
126
|
+
function parseFields(raw: unknown): Field[] {
|
|
127
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
128
|
+
throw new Error("fields must be a non-empty array");
|
|
129
|
+
}
|
|
130
|
+
if (raw.length > 32) throw new Error("fields cannot exceed 32 entries");
|
|
131
|
+
return raw.map((f, i) => {
|
|
132
|
+
if (typeof f !== "object" || f === null) throw new Error(`fields[${String(i)}] must be an object`);
|
|
133
|
+
const obj = f as Record<string, unknown>;
|
|
134
|
+
const key = parseFieldKey(obj.key);
|
|
135
|
+
const sensitivity = (() => {
|
|
136
|
+
if (typeof obj.sensitivity !== "string" || !VALID_SENSITIVITIES.includes(obj.sensitivity as FieldSensitivity)) {
|
|
137
|
+
throw new Error(`fields[${String(i)}].sensitivity must be 'plaintext' or 'encrypted'`);
|
|
138
|
+
}
|
|
139
|
+
return obj.sensitivity as FieldSensitivity;
|
|
140
|
+
})();
|
|
141
|
+
const format = (() => {
|
|
142
|
+
if (obj.format === undefined) return undefined;
|
|
143
|
+
if (typeof obj.format !== "string" || !VALID_FORMATS.includes(obj.format as FieldFormat)) {
|
|
144
|
+
throw new Error(`fields[${String(i)}].format must be one of ${VALID_FORMATS.join(", ")}`);
|
|
145
|
+
}
|
|
146
|
+
return obj.format as FieldFormat;
|
|
147
|
+
})();
|
|
148
|
+
if (typeof obj.value !== "string") throw new Error(`fields[${String(i)}].value must be a string`);
|
|
149
|
+
return { key, sensitivity, format, value: obj.value };
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
|
|
100
153
|
export default {
|
|
101
154
|
id: "secrets",
|
|
102
155
|
name: "Secrets",
|
|
103
|
-
description: "Per-scope encrypted secret storage
|
|
156
|
+
description: "Per-scope encrypted secret storage with multi-field aggregates, per-field rotation, and audit history.",
|
|
104
157
|
version: pkg.version,
|
|
105
158
|
kind: "secrets" as const,
|
|
106
159
|
|
|
@@ -113,16 +166,22 @@ export default {
|
|
|
113
166
|
api.registerMemoryPromptSection(({ availableTools }) => {
|
|
114
167
|
const lines: string[] = ["## Secrets"];
|
|
115
168
|
if (availableTools.has("secret_list_scopes")) {
|
|
116
|
-
lines.push("Use secret_list_scopes to discover
|
|
169
|
+
lines.push("Use secret_list_scopes to discover scopes (org/team/project/agent) you can manage secrets in.");
|
|
117
170
|
}
|
|
118
171
|
if (availableTools.has("secret_list")) {
|
|
119
|
-
lines.push("Use secret_list to see secret
|
|
172
|
+
lines.push("Use secret_list to see secret metadata in a scope (filters: category, tag, fieldKey). Never returns values.");
|
|
120
173
|
}
|
|
121
|
-
if (availableTools.has("
|
|
122
|
-
lines.push("Use
|
|
174
|
+
if (availableTools.has("secret_get_full")) {
|
|
175
|
+
lines.push("Use secret_get_full to fetch a secret with all fields decrypted. Output is SENSITIVE — do not echo or log.");
|
|
176
|
+
}
|
|
177
|
+
if (availableTools.has("secret_get_field")) {
|
|
178
|
+
lines.push("Use secret_get_field to fetch one specific field's value (cheaper than secret_get_full when you only need one).");
|
|
123
179
|
}
|
|
124
180
|
if (availableTools.has("secret_set")) {
|
|
125
|
-
lines.push("Use secret_set to create
|
|
181
|
+
lines.push("Use secret_set to create a secret with one or more fields. Each field has sensitivity (plaintext/encrypted).");
|
|
182
|
+
}
|
|
183
|
+
if (availableTools.has("secret_set_field")) {
|
|
184
|
+
lines.push("Use secret_set_field to add a new field OR rotate an existing one — the same call handles both.");
|
|
126
185
|
}
|
|
127
186
|
return lines;
|
|
128
187
|
});
|
|
@@ -132,7 +191,7 @@ export default {
|
|
|
132
191
|
api.registerTool(() => ({
|
|
133
192
|
name: "secret_list_scopes",
|
|
134
193
|
label: "List Secret Scopes",
|
|
135
|
-
description: "List scopes (org/team/project/agent) the agent can manage secrets in.
|
|
194
|
+
description: "List scopes (org/team/project/agent) the agent can manage secrets in.",
|
|
136
195
|
parameters: { type: "object", properties: {} },
|
|
137
196
|
execute: async () => {
|
|
138
197
|
const scopes = await client.listSecretScopes();
|
|
@@ -144,19 +203,25 @@ export default {
|
|
|
144
203
|
api.registerTool(() => ({
|
|
145
204
|
name: "secret_list",
|
|
146
205
|
label: "List Secrets",
|
|
147
|
-
description: "List secret metadata
|
|
206
|
+
description: "List secret metadata in a scope. Optional category/tag/fieldKey filters route through the byFacet GSI.",
|
|
148
207
|
parameters: {
|
|
149
208
|
type: "object",
|
|
150
209
|
properties: {
|
|
151
|
-
scope: { type: "string", enum: VALID_SCOPES
|
|
152
|
-
scopeId: { type: "string"
|
|
210
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
211
|
+
scopeId: { type: "string" },
|
|
212
|
+
category: { type: "string", enum: VALID_CATEGORIES },
|
|
213
|
+
tag: { type: "string" },
|
|
214
|
+
fieldKey: { type: "string" },
|
|
153
215
|
},
|
|
154
216
|
required: ["scope", "scopeId"],
|
|
155
217
|
},
|
|
156
218
|
execute: async (_id, params) => {
|
|
157
219
|
const scope = parseScope(params.scope);
|
|
158
220
|
const scopeId = parseString(params.scopeId, "scopeId");
|
|
159
|
-
const
|
|
221
|
+
const category = typeof params.category === "string" ? params.category as SecretCategory : undefined;
|
|
222
|
+
const tag = typeof params.tag === "string" ? params.tag : undefined;
|
|
223
|
+
const fieldKey = typeof params.fieldKey === "string" ? params.fieldKey : undefined;
|
|
224
|
+
const secrets = await client.listSecrets({ scope, scopeId, category, tag, fieldKey });
|
|
160
225
|
return { secrets };
|
|
161
226
|
},
|
|
162
227
|
}), { names: ["secret_list"] });
|
|
@@ -165,62 +230,94 @@ export default {
|
|
|
165
230
|
api.registerTool(() => ({
|
|
166
231
|
name: "secret_set",
|
|
167
232
|
label: "Set Secret",
|
|
168
|
-
description: "Create a new secret
|
|
233
|
+
description: "Create a new secret with one or more fields. Encrypted fields are sealed locally before upload.",
|
|
169
234
|
parameters: {
|
|
170
235
|
type: "object",
|
|
171
236
|
properties: {
|
|
172
|
-
scope: { type: "string", enum: VALID_SCOPES
|
|
173
|
-
scopeId: { type: "string"
|
|
237
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
238
|
+
scopeId: { type: "string" },
|
|
174
239
|
name: { type: "string", description: "Human-readable secret name (unique within scope)" },
|
|
175
|
-
|
|
176
|
-
description: { type: "string"
|
|
177
|
-
tags: { type: "array", items: { type: "string" }
|
|
240
|
+
category: { type: "string", enum: VALID_CATEGORIES, description: "Defaults to 'other'" },
|
|
241
|
+
description: { type: "string" },
|
|
242
|
+
tags: { type: "array", items: { type: "string" } },
|
|
243
|
+
fields: {
|
|
244
|
+
type: "array",
|
|
245
|
+
description: "Fields to include. Each: { key, sensitivity (plaintext|encrypted), format?, value }",
|
|
246
|
+
items: {
|
|
247
|
+
type: "object",
|
|
248
|
+
properties: {
|
|
249
|
+
key: { type: "string" },
|
|
250
|
+
sensitivity: { type: "string", enum: VALID_SENSITIVITIES },
|
|
251
|
+
format: { type: "string", enum: VALID_FORMATS },
|
|
252
|
+
value: { type: "string" },
|
|
253
|
+
},
|
|
254
|
+
required: ["key", "sensitivity", "value"],
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
// Backwards-compat shorthand: { value: "..." } synthesises one
|
|
258
|
+
// encrypted field with key="value".
|
|
259
|
+
value: { type: "string", description: "Shorthand for fields=[{ key: 'value', sensitivity: 'encrypted', value }]" },
|
|
260
|
+
reason: { type: "string" },
|
|
178
261
|
},
|
|
179
|
-
required: ["scope", "scopeId", "name"
|
|
262
|
+
required: ["scope", "scopeId", "name"],
|
|
180
263
|
},
|
|
181
264
|
execute: async (_id, params) => {
|
|
182
265
|
const scope = parseScope(params.scope);
|
|
183
266
|
const scopeId = parseString(params.scopeId, "scopeId");
|
|
184
267
|
const name = parseSecretName(params.name);
|
|
185
|
-
const
|
|
268
|
+
const category = typeof params.category === "string"
|
|
269
|
+
? params.category as SecretCategory
|
|
270
|
+
: undefined;
|
|
186
271
|
const description = typeof params.description === "string" ? params.description : undefined;
|
|
187
272
|
const tags = Array.isArray(params.tags)
|
|
188
273
|
? params.tags.filter((t): t is string => typeof t === "string")
|
|
189
274
|
: undefined;
|
|
275
|
+
const reason = typeof params.reason === "string" ? params.reason : undefined;
|
|
276
|
+
|
|
277
|
+
let fields: Field[];
|
|
278
|
+
if (Array.isArray(params.fields)) {
|
|
279
|
+
fields = parseFields(params.fields);
|
|
280
|
+
} else if (typeof params.value === "string") {
|
|
281
|
+
fields = [{ key: "value", sensitivity: "encrypted", value: params.value }];
|
|
282
|
+
} else {
|
|
283
|
+
throw new Error("either `fields` or `value` is required");
|
|
284
|
+
}
|
|
285
|
+
|
|
190
286
|
const secretId = randomUUID();
|
|
191
287
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
288
|
+
// Pre-seal each encrypted field with its own data key.
|
|
289
|
+
const sealed = await Promise.all(fields.map(async (f) => {
|
|
290
|
+
if (f.sensitivity === "encrypted") {
|
|
291
|
+
const envelope = await encryptSecretValue({
|
|
292
|
+
client, scope, scopeId, secretId, fieldKey: f.key, plaintext: f.value,
|
|
293
|
+
});
|
|
294
|
+
return { ...f, value: undefined, envelope };
|
|
295
|
+
}
|
|
296
|
+
return f;
|
|
297
|
+
}));
|
|
298
|
+
|
|
299
|
+
await client.createSecret({
|
|
300
|
+
scope, scopeId, secretId,
|
|
203
301
|
secretName: name,
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
tags,
|
|
302
|
+
category, description, tags, reason,
|
|
303
|
+
fields: sealed,
|
|
207
304
|
});
|
|
208
|
-
logger.info("Secret created", { scope, scopeId, secretId });
|
|
305
|
+
logger.info("Secret created", { scope, scopeId, secretId, fieldCount: fields.length });
|
|
209
306
|
return { secretId };
|
|
210
307
|
},
|
|
211
308
|
}), { names: ["secret_set"] });
|
|
212
309
|
|
|
213
|
-
// ─── secret_get
|
|
310
|
+
// ─── secret_get (legacy: returns `value` field plaintext as a string) ──
|
|
214
311
|
api.registerTool(() => ({
|
|
215
312
|
name: "secret_get",
|
|
216
|
-
label: "Get Secret",
|
|
217
|
-
description: "Fetch and decrypt
|
|
313
|
+
label: "Get Secret (Legacy)",
|
|
314
|
+
description: "Fetch and decrypt the `value`-keyed field. The output is SENSITIVE; do not echo or log.",
|
|
218
315
|
parameters: {
|
|
219
316
|
type: "object",
|
|
220
317
|
properties: {
|
|
221
318
|
scope: { type: "string", enum: VALID_SCOPES },
|
|
222
319
|
scopeId: { type: "string" },
|
|
223
|
-
secretId: { type: "string"
|
|
320
|
+
secretId: { type: "string" },
|
|
224
321
|
},
|
|
225
322
|
required: ["scope", "scopeId", "secretId"],
|
|
226
323
|
},
|
|
@@ -228,121 +325,292 @@ export default {
|
|
|
228
325
|
const scope = parseScope(params.scope);
|
|
229
326
|
const scopeId = parseString(params.scopeId, "scopeId");
|
|
230
327
|
const secretId = parseSecretId(params.secretId);
|
|
231
|
-
const
|
|
328
|
+
const field = await client.getSecretField({ scope, scopeId, secretId, fieldKey: "value" });
|
|
329
|
+
if (field.sensitivity === "plaintext") {
|
|
330
|
+
return { secretId, value: field.value };
|
|
331
|
+
}
|
|
332
|
+
if (!field.envelope) throw new Error("encrypted field has no envelope");
|
|
232
333
|
const plaintext = await decryptSecretEnvelope({
|
|
233
|
-
client,
|
|
234
|
-
scope,
|
|
235
|
-
scopeId,
|
|
236
|
-
secretId,
|
|
237
|
-
envelope: row.envelope,
|
|
334
|
+
client, scope, scopeId, secretId, fieldKey: "value", envelope: field.envelope,
|
|
238
335
|
});
|
|
336
|
+
return { secretId, value: plaintext };
|
|
337
|
+
},
|
|
338
|
+
}), { names: ["secret_get"] });
|
|
339
|
+
|
|
340
|
+
// ─── secret_get_full ────────────────────────────────────────
|
|
341
|
+
api.registerTool(() => ({
|
|
342
|
+
name: "secret_get_full",
|
|
343
|
+
label: "Get Secret (All Fields)",
|
|
344
|
+
description: "Fetch the secret aggregate with all fields decrypted. Output is SENSITIVE.",
|
|
345
|
+
parameters: {
|
|
346
|
+
type: "object",
|
|
347
|
+
properties: {
|
|
348
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
349
|
+
scopeId: { type: "string" },
|
|
350
|
+
secretId: { type: "string" },
|
|
351
|
+
},
|
|
352
|
+
required: ["scope", "scopeId", "secretId"],
|
|
353
|
+
},
|
|
354
|
+
execute: async (_id, params) => {
|
|
355
|
+
const scope = parseScope(params.scope);
|
|
356
|
+
const scopeId = parseString(params.scopeId, "scopeId");
|
|
357
|
+
const secretId = parseSecretId(params.secretId);
|
|
358
|
+
const { aggregate, envelopes } = await client.getSecret({ scope, scopeId, secretId });
|
|
359
|
+
|
|
360
|
+
const decryptedFields = await Promise.all(aggregate.fields.map(async (f) => {
|
|
361
|
+
if (f.sensitivity === "plaintext") {
|
|
362
|
+
return { key: f.key, sensitivity: f.sensitivity, format: f.format, value: f.value };
|
|
363
|
+
}
|
|
364
|
+
const envelope = envelopes.find((e) => e.key === f.key)?.envelope;
|
|
365
|
+
if (!envelope) return { key: f.key, sensitivity: f.sensitivity, format: f.format, error: "envelope missing" };
|
|
366
|
+
const plaintext = await decryptSecretEnvelope({
|
|
367
|
+
client, scope, scopeId, secretId, fieldKey: f.key, envelope,
|
|
368
|
+
});
|
|
369
|
+
return { key: f.key, sensitivity: f.sensitivity, format: f.format, value: plaintext };
|
|
370
|
+
}));
|
|
371
|
+
|
|
239
372
|
return {
|
|
240
373
|
secretId,
|
|
241
|
-
secretName:
|
|
242
|
-
|
|
374
|
+
secretName: aggregate.secretName,
|
|
375
|
+
description: aggregate.description,
|
|
376
|
+
tags: aggregate.tags,
|
|
377
|
+
category: aggregate.category,
|
|
378
|
+
fields: decryptedFields,
|
|
243
379
|
};
|
|
244
380
|
},
|
|
245
|
-
}), { names: ["
|
|
381
|
+
}), { names: ["secret_get_full"] });
|
|
246
382
|
|
|
247
|
-
// ───
|
|
383
|
+
// ─── secret_get_field ───────────────────────────────────────
|
|
248
384
|
api.registerTool(() => ({
|
|
249
|
-
name: "
|
|
250
|
-
label: "Get Secret
|
|
251
|
-
description: "Fetch
|
|
385
|
+
name: "secret_get_field",
|
|
386
|
+
label: "Get Secret Field",
|
|
387
|
+
description: "Fetch and decrypt one field by key. Cheaper than secret_get_full when you only need one piece.",
|
|
252
388
|
parameters: {
|
|
253
389
|
type: "object",
|
|
254
390
|
properties: {
|
|
255
391
|
scope: { type: "string", enum: VALID_SCOPES },
|
|
256
392
|
scopeId: { type: "string" },
|
|
257
|
-
|
|
393
|
+
secretId: { type: "string" },
|
|
394
|
+
fieldKey: { type: "string" },
|
|
258
395
|
},
|
|
259
|
-
required: ["scope", "scopeId", "
|
|
396
|
+
required: ["scope", "scopeId", "secretId", "fieldKey"],
|
|
260
397
|
},
|
|
261
398
|
execute: async (_id, params) => {
|
|
262
399
|
const scope = parseScope(params.scope);
|
|
263
400
|
const scopeId = parseString(params.scopeId, "scopeId");
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
268
|
-
|
|
401
|
+
const secretId = parseSecretId(params.secretId);
|
|
402
|
+
const fieldKey = parseFieldKey(params.fieldKey);
|
|
403
|
+
const field = await client.getSecretField({ scope, scopeId, secretId, fieldKey });
|
|
404
|
+
if (field.sensitivity === "plaintext") {
|
|
405
|
+
return { key: field.key, sensitivity: field.sensitivity, format: field.format, value: field.value };
|
|
406
|
+
}
|
|
407
|
+
if (!field.envelope) throw new Error("encrypted field has no envelope");
|
|
269
408
|
const plaintext = await decryptSecretEnvelope({
|
|
270
|
-
client,
|
|
271
|
-
scope,
|
|
272
|
-
scopeId,
|
|
273
|
-
secretId: match.secretId,
|
|
274
|
-
envelope: row.envelope,
|
|
409
|
+
client, scope, scopeId, secretId, fieldKey, envelope: field.envelope,
|
|
275
410
|
});
|
|
276
|
-
return {
|
|
411
|
+
return { key: field.key, sensitivity: field.sensitivity, format: field.format, value: plaintext };
|
|
277
412
|
},
|
|
278
|
-
}), { names: ["
|
|
413
|
+
}), { names: ["secret_get_field"] });
|
|
279
414
|
|
|
280
|
-
// ───
|
|
415
|
+
// ─── secret_set_field ───────────────────────────────────────
|
|
281
416
|
api.registerTool(() => ({
|
|
282
|
-
name: "
|
|
283
|
-
label: "
|
|
284
|
-
description: "
|
|
417
|
+
name: "secret_set_field",
|
|
418
|
+
label: "Set Secret Field",
|
|
419
|
+
description: "Add OR rotate one field. Same key as existing = rotate; new key = add. Encrypted fields are sealed locally.",
|
|
285
420
|
parameters: {
|
|
286
421
|
type: "object",
|
|
287
422
|
properties: {
|
|
288
423
|
scope: { type: "string", enum: VALID_SCOPES },
|
|
289
424
|
scopeId: { type: "string" },
|
|
290
425
|
secretId: { type: "string" },
|
|
426
|
+
fieldKey: { type: "string" },
|
|
427
|
+
sensitivity: { type: "string", enum: VALID_SENSITIVITIES },
|
|
428
|
+
format: { type: "string", enum: VALID_FORMATS },
|
|
429
|
+
value: { type: "string" },
|
|
430
|
+
reason: { type: "string" },
|
|
291
431
|
},
|
|
292
|
-
required: ["scope", "scopeId", "secretId"],
|
|
432
|
+
required: ["scope", "scopeId", "secretId", "fieldKey", "sensitivity", "value"],
|
|
293
433
|
},
|
|
294
434
|
execute: async (_id, params) => {
|
|
295
435
|
const scope = parseScope(params.scope);
|
|
296
436
|
const scopeId = parseString(params.scopeId, "scopeId");
|
|
297
437
|
const secretId = parseSecretId(params.secretId);
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
438
|
+
const fieldKey = parseFieldKey(params.fieldKey);
|
|
439
|
+
const sensitivity = (() => {
|
|
440
|
+
if (typeof params.sensitivity !== "string" || !VALID_SENSITIVITIES.includes(params.sensitivity as FieldSensitivity)) {
|
|
441
|
+
throw new Error(`sensitivity must be one of ${VALID_SENSITIVITIES.join(", ")}`);
|
|
442
|
+
}
|
|
443
|
+
return params.sensitivity as FieldSensitivity;
|
|
444
|
+
})();
|
|
445
|
+
const format = (() => {
|
|
446
|
+
if (params.format === undefined) return undefined;
|
|
447
|
+
if (typeof params.format !== "string" || !VALID_FORMATS.includes(params.format as FieldFormat)) {
|
|
448
|
+
throw new Error(`format must be one of ${VALID_FORMATS.join(", ")}`);
|
|
449
|
+
}
|
|
450
|
+
return params.format as FieldFormat;
|
|
451
|
+
})();
|
|
452
|
+
const value = parseString(params.value, "value", 16384);
|
|
453
|
+
const reason = typeof params.reason === "string" ? params.reason : undefined;
|
|
454
|
+
|
|
455
|
+
if (sensitivity === "encrypted") {
|
|
456
|
+
const envelope = await encryptSecretValue({
|
|
457
|
+
client, scope, scopeId, secretId, fieldKey, plaintext: value,
|
|
458
|
+
});
|
|
459
|
+
const result = await client.setSecretField({
|
|
460
|
+
scope, scopeId, secretId, fieldKey,
|
|
461
|
+
sensitivity, format, envelope, reason,
|
|
462
|
+
});
|
|
463
|
+
logger.info("Encrypted field upserted", { scope, scopeId, secretId, fieldKey, rotated: result.rotated });
|
|
464
|
+
return result;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const result = await client.setSecretField({
|
|
468
|
+
scope, scopeId, secretId, fieldKey,
|
|
469
|
+
sensitivity, format, value, reason,
|
|
470
|
+
});
|
|
471
|
+
logger.info("Plaintext field upserted", { scope, scopeId, secretId, fieldKey, rotated: result.rotated });
|
|
472
|
+
return result;
|
|
301
473
|
},
|
|
302
|
-
}), { names: ["
|
|
474
|
+
}), { names: ["secret_set_field"] });
|
|
303
475
|
|
|
304
|
-
// ───
|
|
476
|
+
// ─── secret_remove_field ────────────────────────────────────
|
|
305
477
|
api.registerTool(() => ({
|
|
306
|
-
name: "
|
|
307
|
-
label: "
|
|
308
|
-
description: "
|
|
478
|
+
name: "secret_remove_field",
|
|
479
|
+
label: "Remove Secret Field",
|
|
480
|
+
description: "Remove one field from a secret. Other fields untouched.",
|
|
309
481
|
parameters: {
|
|
310
482
|
type: "object",
|
|
311
483
|
properties: {
|
|
312
484
|
scope: { type: "string", enum: VALID_SCOPES },
|
|
313
485
|
scopeId: { type: "string" },
|
|
314
486
|
secretId: { type: "string" },
|
|
315
|
-
|
|
487
|
+
fieldKey: { type: "string" },
|
|
316
488
|
},
|
|
317
|
-
required: ["scope", "scopeId", "secretId", "
|
|
489
|
+
required: ["scope", "scopeId", "secretId", "fieldKey"],
|
|
318
490
|
},
|
|
319
491
|
execute: async (_id, params) => {
|
|
320
492
|
const scope = parseScope(params.scope);
|
|
321
493
|
const scopeId = parseString(params.scopeId, "scopeId");
|
|
322
494
|
const secretId = parseSecretId(params.secretId);
|
|
323
|
-
const
|
|
495
|
+
const fieldKey = parseFieldKey(params.fieldKey);
|
|
496
|
+
await client.removeSecretField({ scope, scopeId, secretId, fieldKey });
|
|
497
|
+
logger.info("Field removed", { scope, scopeId, secretId, fieldKey });
|
|
498
|
+
return { deleted: true };
|
|
499
|
+
},
|
|
500
|
+
}), { names: ["secret_remove_field"] });
|
|
324
501
|
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
502
|
+
// ─── secret_update_metadata ─────────────────────────────────
|
|
503
|
+
api.registerTool(() => ({
|
|
504
|
+
name: "secret_update_metadata",
|
|
505
|
+
label: "Update Secret Metadata",
|
|
506
|
+
description: "Patch secret-level metadata (name, description, tags, category). Does not touch fields.",
|
|
507
|
+
parameters: {
|
|
508
|
+
type: "object",
|
|
509
|
+
properties: {
|
|
510
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
511
|
+
scopeId: { type: "string" },
|
|
512
|
+
secretId: { type: "string" },
|
|
513
|
+
secretName: { type: "string" },
|
|
514
|
+
description: { type: "string" },
|
|
515
|
+
tags: { type: "array", items: { type: "string" } },
|
|
516
|
+
category: { type: "string", enum: VALID_CATEGORIES },
|
|
517
|
+
reason: { type: "string" },
|
|
518
|
+
},
|
|
519
|
+
required: ["scope", "scopeId", "secretId"],
|
|
520
|
+
},
|
|
521
|
+
execute: async (_id, params) => {
|
|
522
|
+
const scope = parseScope(params.scope);
|
|
523
|
+
const scopeId = parseString(params.scopeId, "scopeId");
|
|
524
|
+
const secretId = parseSecretId(params.secretId);
|
|
525
|
+
const secretName = typeof params.secretName === "string" ? parseSecretName(params.secretName) : undefined;
|
|
526
|
+
const description = typeof params.description === "string" ? params.description : undefined;
|
|
527
|
+
const tags = Array.isArray(params.tags)
|
|
528
|
+
? params.tags.filter((t): t is string => typeof t === "string")
|
|
529
|
+
: undefined;
|
|
530
|
+
const category = typeof params.category === "string" ? params.category as SecretCategory : undefined;
|
|
531
|
+
const reason = typeof params.reason === "string" ? params.reason : undefined;
|
|
532
|
+
return client.updateSecretMetadata({
|
|
533
|
+
scope, scopeId, secretId, secretName, description, tags, category, reason,
|
|
341
534
|
});
|
|
342
|
-
logger.info("Secret rotated", { scope, scopeId, secretId });
|
|
343
|
-
return { secretId, rotated: true };
|
|
344
535
|
},
|
|
345
|
-
}), { names: ["
|
|
536
|
+
}), { names: ["secret_update_metadata"] });
|
|
537
|
+
|
|
538
|
+
// ─── secret_search ──────────────────────────────────────────
|
|
539
|
+
api.registerTool(() => ({
|
|
540
|
+
name: "secret_search",
|
|
541
|
+
label: "Search Secrets",
|
|
542
|
+
description: "Find secrets by category, tag, or field key via the byFacet GSI.",
|
|
543
|
+
parameters: {
|
|
544
|
+
type: "object",
|
|
545
|
+
properties: {
|
|
546
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
547
|
+
scopeId: { type: "string" },
|
|
548
|
+
category: { type: "string", enum: VALID_CATEGORIES },
|
|
549
|
+
tag: { type: "string" },
|
|
550
|
+
fieldKey: { type: "string" },
|
|
551
|
+
},
|
|
552
|
+
required: ["scope", "scopeId"],
|
|
553
|
+
},
|
|
554
|
+
execute: async (_id, params) => {
|
|
555
|
+
const scope = parseScope(params.scope);
|
|
556
|
+
const scopeId = parseString(params.scopeId, "scopeId");
|
|
557
|
+
const category = typeof params.category === "string" ? params.category as SecretCategory : undefined;
|
|
558
|
+
const tag = typeof params.tag === "string" ? params.tag : undefined;
|
|
559
|
+
const fieldKey = typeof params.fieldKey === "string" ? params.fieldKey : undefined;
|
|
560
|
+
const secrets = await client.listSecrets({ scope, scopeId, category, tag, fieldKey });
|
|
561
|
+
return { secrets };
|
|
562
|
+
},
|
|
563
|
+
}), { names: ["secret_search"] });
|
|
564
|
+
|
|
565
|
+
// ─── secret_get_history ─────────────────────────────────────
|
|
566
|
+
api.registerTool(() => ({
|
|
567
|
+
name: "secret_get_history",
|
|
568
|
+
label: "Get Secret History",
|
|
569
|
+
description: "Bounded changelog read — metadata-only audit entries (no field values).",
|
|
570
|
+
parameters: {
|
|
571
|
+
type: "object",
|
|
572
|
+
properties: {
|
|
573
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
574
|
+
scopeId: { type: "string" },
|
|
575
|
+
secretId: { type: "string" },
|
|
576
|
+
limit: { type: "number" },
|
|
577
|
+
cursor: { type: "string" },
|
|
578
|
+
},
|
|
579
|
+
required: ["scope", "scopeId", "secretId"],
|
|
580
|
+
},
|
|
581
|
+
execute: async (_id, params) => {
|
|
582
|
+
const scope = parseScope(params.scope);
|
|
583
|
+
const scopeId = parseString(params.scopeId, "scopeId");
|
|
584
|
+
const secretId = parseSecretId(params.secretId);
|
|
585
|
+
const limit = typeof params.limit === "number" ? params.limit : undefined;
|
|
586
|
+
const cursor = typeof params.cursor === "string" ? params.cursor : undefined;
|
|
587
|
+
return client.getSecretHistory({ scope, scopeId, secretId, limit, cursor });
|
|
588
|
+
},
|
|
589
|
+
}), { names: ["secret_get_history"] });
|
|
590
|
+
|
|
591
|
+
// ─── secret_delete ──────────────────────────────────────────
|
|
592
|
+
api.registerTool(() => ({
|
|
593
|
+
name: "secret_delete",
|
|
594
|
+
label: "Delete Secret",
|
|
595
|
+
description: "Delete a secret and all its fields, tags, and changelog rows.",
|
|
596
|
+
parameters: {
|
|
597
|
+
type: "object",
|
|
598
|
+
properties: {
|
|
599
|
+
scope: { type: "string", enum: VALID_SCOPES },
|
|
600
|
+
scopeId: { type: "string" },
|
|
601
|
+
secretId: { type: "string" },
|
|
602
|
+
},
|
|
603
|
+
required: ["scope", "scopeId", "secretId"],
|
|
604
|
+
},
|
|
605
|
+
execute: async (_id, params) => {
|
|
606
|
+
const scope = parseScope(params.scope);
|
|
607
|
+
const scopeId = parseString(params.scopeId, "scopeId");
|
|
608
|
+
const secretId = parseSecretId(params.secretId);
|
|
609
|
+
await client.deleteSecret({ scope, scopeId, secretId });
|
|
610
|
+
logger.info("Secret deleted", { scope, scopeId, secretId });
|
|
611
|
+
return { deleted: true };
|
|
612
|
+
},
|
|
613
|
+
}), { names: ["secret_delete"] });
|
|
346
614
|
|
|
347
615
|
logger.info("openclaw-secrets plugin registered");
|
|
348
616
|
},
|