@alfe.ai/openclaw-secrets 0.2.23 → 0.2.25

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