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