@alfe.ai/openclaw-secrets 0.1.0

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.
@@ -0,0 +1,4 @@
1
+
2
+ > @alfe.ai/openclaw-secrets@0.1.0 build /home/runner/work/alfe/alfe/packages/openclaw-secrets
3
+ > tsc
4
+
@@ -0,0 +1,39 @@
1
+ import type { EncryptedEnvelopeV1, SecretScope } from "@alfe.ai/agent-api-client";
2
+ /**
3
+ * Narrow interface capturing just the two KMS-proxy methods the crypto
4
+ * helpers need. `AgentApiClient` satisfies this; tests can supply a
5
+ * hand-rolled stub without mocking the whole client surface.
6
+ */
7
+ export interface SecretsKmsProxy {
8
+ generateSecretDataKey(args: {
9
+ scope: SecretScope;
10
+ scopeId: string;
11
+ secretId: string;
12
+ }): Promise<{
13
+ plaintextKey: string;
14
+ dataKeyCiphertext: string;
15
+ }>;
16
+ decryptSecretDataKey(args: {
17
+ scope: SecretScope;
18
+ scopeId: string;
19
+ secretId: string;
20
+ dataKeyCiphertext: string;
21
+ }): Promise<{
22
+ plaintextKey: string;
23
+ }>;
24
+ }
25
+ export declare function encryptSecretValue(args: {
26
+ client: SecretsKmsProxy;
27
+ scope: SecretScope;
28
+ scopeId: string;
29
+ secretId: string;
30
+ plaintext: string;
31
+ }): Promise<EncryptedEnvelopeV1>;
32
+ export declare function decryptSecretEnvelope(args: {
33
+ client: SecretsKmsProxy;
34
+ scope: SecretScope;
35
+ scopeId: string;
36
+ secretId: string;
37
+ envelope: EncryptedEnvelopeV1;
38
+ }): Promise<string>;
39
+ //# sourceMappingURL=crypto.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.d.ts","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"AAeA,OAAO,KAAK,EAAE,mBAAmB,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAElF;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,qBAAqB,CAAC,IAAI,EAAE;QAC1B,KAAK,EAAE,WAAW,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;KAClB,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAC;QAAC,iBAAiB,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAEjE,oBAAoB,CAAC,IAAI,EAAE;QACzB,KAAK,EAAE,WAAW,CAAC;QACnB,OAAO,EAAE,MAAM,CAAC;QAChB,QAAQ,EAAE,MAAM,CAAC;QACjB,iBAAiB,EAAE,MAAM,CAAC;KAC3B,GAAG,OAAO,CAAC;QAAE,YAAY,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvC;AAcD,wBAAsB,kBAAkB,CAAC,IAAI,EAAE;IAC7C,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB,GAAG,OAAO,CAAC,mBAAmB,CAAC,CAwB/B;AAED,wBAAsB,qBAAqB,CAAC,IAAI,EAAE;IAChD,MAAM,EAAE,eAAe,CAAC;IACxB,KAAK,EAAE,WAAW,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,mBAAmB,CAAC;CAC/B,GAAG,OAAO,CAAC,MAAM,CAAC,CAqBlB"}
package/dist/crypto.js ADDED
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Agent-side AES-256-GCM with data keys issued by the Alfe secrets service.
3
+ *
4
+ * Trust model:
5
+ * - The agent NEVER holds a KMS key directly.
6
+ * - For each encrypt/decrypt, the agent fetches a one-shot AES data key from
7
+ * the secrets service over authenticated TLS.
8
+ * - The plaintext key is held as a `Buffer` ONLY — never as a JS string.
9
+ * (Strings are immutable and cannot be zeroed; Buffers can be `.fill(0)`'d.)
10
+ * - After each operation the Buffer is zeroed.
11
+ *
12
+ * Envelope format is `EncryptedEnvelopeV1` as defined in `@alfe/types` and
13
+ * persisted by services/secrets.
14
+ */
15
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
16
+ const AES_256_KEY_BYTES = 32;
17
+ const GCM_IV_BYTES = 12;
18
+ function decodeKeyToBuffer(base64) {
19
+ const buf = Buffer.from(base64, "base64");
20
+ if (buf.length !== AES_256_KEY_BYTES) {
21
+ buf.fill(0);
22
+ throw new Error(`Unexpected data key length: ${String(buf.length)} bytes`);
23
+ }
24
+ return buf;
25
+ }
26
+ export async function encryptSecretValue(args) {
27
+ const { client, scope, scopeId, secretId, plaintext } = args;
28
+ const { plaintextKey, dataKeyCiphertext } = await client.generateSecretDataKey({
29
+ scope,
30
+ scopeId,
31
+ secretId,
32
+ });
33
+ const keyBuf = decodeKeyToBuffer(plaintextKey);
34
+ try {
35
+ const iv = randomBytes(GCM_IV_BYTES);
36
+ const cipher = createCipheriv("aes-256-gcm", keyBuf, iv);
37
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
38
+ const authTag = cipher.getAuthTag();
39
+ return {
40
+ version: 1,
41
+ iv: iv.toString("base64"),
42
+ ciphertext: ciphertext.toString("base64"),
43
+ authTag: authTag.toString("base64"),
44
+ dataKeyCiphertext,
45
+ };
46
+ }
47
+ finally {
48
+ keyBuf.fill(0);
49
+ }
50
+ }
51
+ export async function decryptSecretEnvelope(args) {
52
+ const { client, scope, scopeId, secretId, envelope } = args;
53
+ const { plaintextKey } = await client.decryptSecretDataKey({
54
+ scope,
55
+ scopeId,
56
+ secretId,
57
+ dataKeyCiphertext: envelope.dataKeyCiphertext,
58
+ });
59
+ const keyBuf = decodeKeyToBuffer(plaintextKey);
60
+ try {
61
+ const iv = Buffer.from(envelope.iv, "base64");
62
+ const ciphertext = Buffer.from(envelope.ciphertext, "base64");
63
+ const authTag = Buffer.from(envelope.authTag, "base64");
64
+ const decipher = createDecipheriv("aes-256-gcm", keyBuf, iv);
65
+ decipher.setAuthTag(authTag);
66
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
67
+ return decrypted.toString("utf8");
68
+ }
69
+ finally {
70
+ keyBuf.fill(0);
71
+ }
72
+ }
73
+ //# sourceMappingURL=crypto.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"crypto.js","sourceRoot":"","sources":["../src/crypto.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,47 @@
1
+ interface PluginLogger {
2
+ info: (msg: string, ctx?: Record<string, unknown>) => void;
3
+ debug: (msg: string, ctx?: Record<string, unknown>) => void;
4
+ warn: (msg: string, ctx?: Record<string, unknown>) => void;
5
+ error: (msg: string, ctx?: Record<string, unknown>) => void;
6
+ }
7
+ interface ToolContext {
8
+ agentId?: string;
9
+ sessionKey?: string;
10
+ sessionId?: string;
11
+ messageChannel?: string;
12
+ }
13
+ interface Tool {
14
+ name: string;
15
+ label: string;
16
+ description: string;
17
+ parameters: Record<string, unknown>;
18
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
19
+ }
20
+ interface PluginApi {
21
+ pluginConfig?: Record<string, unknown>;
22
+ config: Record<string, unknown>;
23
+ logger: PluginLogger;
24
+ registerTool: (factory: (ctx: ToolContext) => Tool, opts?: {
25
+ names?: string[];
26
+ }) => void;
27
+ registerHook?: (events: string | string[], handler: (...args: unknown[]) => unknown, opts?: {
28
+ name?: string;
29
+ description?: string;
30
+ }) => void;
31
+ on?: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: {
32
+ priority?: number;
33
+ }) => void;
34
+ registerMemoryPromptSection?: (builder: (params: {
35
+ availableTools: Set<string>;
36
+ }) => string[]) => void;
37
+ }
38
+ declare const _default: {
39
+ id: string;
40
+ name: string;
41
+ description: string;
42
+ version: string;
43
+ kind: "secrets";
44
+ register(api: PluginApi): void;
45
+ };
46
+ export default _default;
47
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AA4BA,UAAU,YAAY;IACpB,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC3D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC5D,IAAI,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;IAC3D,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;CAC7D;AAED,UAAU,WAAW;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,UAAU,IAAI;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACpC,OAAO,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;CACpF;AAED,UAAU,SAAS;IACjB,YAAY,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACvC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAChC,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,WAAW,KAAK,IAAI,EAAE,IAAI,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,KAAK,IAAI,CAAC;IACzF,YAAY,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7I,EAAE,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,OAAO,EAAE,IAAI,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IACxG,2BAA2B,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,MAAM,EAAE;QAAE,cAAc,EAAE,GAAG,CAAC,MAAM,CAAC,CAAA;KAAE,KAAK,MAAM,EAAE,KAAK,IAAI,CAAC;CACxG;;;;;;;kBAuDe,SAAS,GAAG,IAAI;;AAPhC,wBAyPE"}
package/dist/index.js ADDED
@@ -0,0 +1,309 @@
1
+ /**
2
+ * openclaw-secrets — OpenClaw plugin for per-scope encrypted secret storage.
3
+ *
4
+ * Trust model:
5
+ * - Secret values are encrypted and decrypted ON THIS AGENT using AES-256-GCM.
6
+ * - Data keys are issued per-secret by the Alfe secrets service; plaintext keys
7
+ * are held as Buffers only, used once, and zeroed immediately.
8
+ * - The backend never sees plaintext secret values (the dashboard can set values
9
+ * through a server-side encrypt path but cannot read them back).
10
+ *
11
+ * All HTTP traffic goes through `@alfe.ai/agent-api-client` — the single
12
+ * canonical agent-side HTTP client — so routes, auth, and response unwrapping
13
+ * stay in lockstep with every other agent endpoint.
14
+ *
15
+ * Tools:
16
+ * - secret_set — create a new secret (mints UUID, encrypts, uploads envelope)
17
+ * - secret_get — fetch + decrypt plaintext by secretId
18
+ * - secret_get_by_name — list then get (convenience)
19
+ * - secret_list — list metadata in a scope (never plaintext)
20
+ * - secret_list_scopes — enumerate scopes the agent can access
21
+ * - secret_delete — delete a secret
22
+ * - secret_rotate — re-encrypt with a fresh data key
23
+ */
24
+ import { randomUUID } from "node:crypto";
25
+ import { AgentApiClient } from "@alfe.ai/agent-api-client";
26
+ import { decryptSecretEnvelope, encryptSecretValue } from "./crypto.js";
27
+ const VALID_SCOPES = ["org", "team", "project", "agent"];
28
+ const NAME_REGEX = /^[a-zA-Z0-9_./-]{1,128}$/;
29
+ 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;
30
+ function resolveConfig(raw) {
31
+ // Match @alfe.ai/agent-api-client's config shape. agentId and tenantId are
32
+ // resolved server-side from the bearer token, so we intentionally don't
33
+ // accept them here — reading caller-supplied identity would be a footgun.
34
+ const apiUrl = typeof raw?.apiUrl === "string" ? raw.apiUrl : (process.env.ALFE_API_URL ?? "");
35
+ const agentApiKey = typeof raw?.agentApiKey === "string" ? raw.agentApiKey : "";
36
+ if (!apiUrl)
37
+ throw new Error("openclaw-secrets: apiUrl (or ALFE_API_URL) is required");
38
+ if (!agentApiKey)
39
+ throw new Error("openclaw-secrets: agentApiKey is required");
40
+ return { apiUrl, agentApiKey };
41
+ }
42
+ function parseScope(raw, field = "scope") {
43
+ if (typeof raw !== "string" || !VALID_SCOPES.includes(raw)) {
44
+ throw new Error(`${field} must be one of ${VALID_SCOPES.join(", ")}`);
45
+ }
46
+ return raw;
47
+ }
48
+ function parseString(raw, field, max = 256) {
49
+ if (typeof raw !== "string" || raw.length === 0) {
50
+ throw new Error(`${field} is required`);
51
+ }
52
+ if (raw.length > max) {
53
+ throw new Error(`${field} must be ${String(max)} characters or fewer`);
54
+ }
55
+ return raw;
56
+ }
57
+ function parseSecretName(raw) {
58
+ const s = parseString(raw, "name", 128);
59
+ if (!NAME_REGEX.test(s)) {
60
+ throw new Error("name must match ^[a-zA-Z0-9_./-]{1,128}$");
61
+ }
62
+ return s;
63
+ }
64
+ function parseSecretId(raw) {
65
+ const s = parseString(raw, "secretId", 64);
66
+ if (!UUID_V4.test(s))
67
+ throw new Error("secretId must be a UUID v4");
68
+ return s;
69
+ }
70
+ export default {
71
+ id: "secrets",
72
+ name: "Secrets",
73
+ description: "Per-scope encrypted secret storage — agent-side AES-256-GCM with KMS-issued data keys.",
74
+ version: "0.1.0",
75
+ kind: "secrets",
76
+ register(api) {
77
+ const config = resolveConfig(api.pluginConfig);
78
+ const client = new AgentApiClient({ apiUrl: config.apiUrl, apiKey: config.agentApiKey });
79
+ const logger = api.logger;
80
+ if (api.registerMemoryPromptSection) {
81
+ api.registerMemoryPromptSection(({ availableTools }) => {
82
+ const lines = ["## Secrets"];
83
+ if (availableTools.has("secret_list_scopes")) {
84
+ lines.push("Use secret_list_scopes to discover which scopes (org/team/project/agent) you can manage secrets in.");
85
+ }
86
+ if (availableTools.has("secret_list")) {
87
+ lines.push("Use secret_list to see secret names in a scope. secret_list NEVER returns plaintext.");
88
+ }
89
+ if (availableTools.has("secret_get")) {
90
+ lines.push("Use secret_get to retrieve a plaintext secret value. The output is SENSITIVE — do not echo it to the user or log it.");
91
+ }
92
+ if (availableTools.has("secret_set")) {
93
+ lines.push("Use secret_set to create or update a secret. Prefer scoping to 'agent' for personal secrets.");
94
+ }
95
+ return lines;
96
+ });
97
+ }
98
+ // ─── secret_list_scopes ─────────────────────────────────────
99
+ api.registerTool(() => ({
100
+ name: "secret_list_scopes",
101
+ label: "List Secret Scopes",
102
+ description: "List scopes (org/team/project/agent) the agent can manage secrets in. Use this before guessing scopeIds.",
103
+ parameters: { type: "object", properties: {} },
104
+ execute: async () => {
105
+ const scopes = await client.listSecretScopes();
106
+ return { scopes };
107
+ },
108
+ }), { names: ["secret_list_scopes"] });
109
+ // ─── secret_list ────────────────────────────────────────────
110
+ api.registerTool(() => ({
111
+ name: "secret_list",
112
+ label: "List Secrets",
113
+ description: "List secret metadata (never plaintext) in a given scope.",
114
+ parameters: {
115
+ type: "object",
116
+ properties: {
117
+ scope: { type: "string", enum: VALID_SCOPES, description: "Scope level" },
118
+ scopeId: { type: "string", description: "Scope identifier (tenantId / teamId / projectId / agentId)" },
119
+ },
120
+ required: ["scope", "scopeId"],
121
+ },
122
+ execute: async (_id, params) => {
123
+ const scope = parseScope(params.scope);
124
+ const scopeId = parseString(params.scopeId, "scopeId");
125
+ const secrets = await client.listSecrets({ scope, scopeId });
126
+ return { secrets };
127
+ },
128
+ }), { names: ["secret_list"] });
129
+ // ─── secret_set ─────────────────────────────────────────────
130
+ api.registerTool(() => ({
131
+ name: "secret_set",
132
+ label: "Set Secret",
133
+ description: "Create a new secret. Encrypts the value locally and uploads only the envelope.",
134
+ parameters: {
135
+ type: "object",
136
+ properties: {
137
+ scope: { type: "string", enum: VALID_SCOPES, description: "Scope level" },
138
+ scopeId: { type: "string", description: "Scope identifier" },
139
+ name: { type: "string", description: "Human-readable secret name (unique within scope)" },
140
+ value: { type: "string", description: "Secret plaintext value (encrypted before upload)" },
141
+ description: { type: "string", description: "Optional description" },
142
+ tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
143
+ },
144
+ required: ["scope", "scopeId", "name", "value"],
145
+ },
146
+ execute: async (_id, params) => {
147
+ const scope = parseScope(params.scope);
148
+ const scopeId = parseString(params.scopeId, "scopeId");
149
+ const name = parseSecretName(params.name);
150
+ const value = parseString(params.value, "value", 65_536);
151
+ const description = typeof params.description === "string" ? params.description : undefined;
152
+ const tags = Array.isArray(params.tags)
153
+ ? params.tags.filter((t) => typeof t === "string")
154
+ : undefined;
155
+ const secretId = randomUUID();
156
+ const envelope = await encryptSecretValue({
157
+ client,
158
+ scope,
159
+ scopeId,
160
+ secretId,
161
+ plaintext: value,
162
+ });
163
+ await client.putSecretEnvelope({
164
+ scope,
165
+ scopeId,
166
+ secretId,
167
+ secretName: name,
168
+ envelope,
169
+ description,
170
+ tags,
171
+ });
172
+ logger.info("Secret created", { scope, scopeId, secretId });
173
+ return { secretId };
174
+ },
175
+ }), { names: ["secret_set"] });
176
+ // ─── secret_get ─────────────────────────────────────────────
177
+ api.registerTool(() => ({
178
+ name: "secret_get",
179
+ label: "Get Secret",
180
+ description: "Fetch and decrypt a secret by secretId. The return value is SENSITIVE; do not echo or log it.",
181
+ parameters: {
182
+ type: "object",
183
+ properties: {
184
+ scope: { type: "string", enum: VALID_SCOPES },
185
+ scopeId: { type: "string" },
186
+ secretId: { type: "string", description: "UUID v4 of the secret" },
187
+ },
188
+ required: ["scope", "scopeId", "secretId"],
189
+ },
190
+ execute: async (_id, params) => {
191
+ const scope = parseScope(params.scope);
192
+ const scopeId = parseString(params.scopeId, "scopeId");
193
+ const secretId = parseSecretId(params.secretId);
194
+ const row = await client.getSecretEnvelope({ scope, scopeId, secretId });
195
+ const plaintext = await decryptSecretEnvelope({
196
+ client,
197
+ scope,
198
+ scopeId,
199
+ secretId,
200
+ envelope: row.envelope,
201
+ });
202
+ return {
203
+ secretId,
204
+ secretName: row.secretName,
205
+ value: plaintext,
206
+ };
207
+ },
208
+ }), { names: ["secret_get"] });
209
+ // ─── secret_get_by_name ─────────────────────────────────────
210
+ api.registerTool(() => ({
211
+ name: "secret_get_by_name",
212
+ label: "Get Secret By Name",
213
+ description: "Fetch a secret by its human-readable name. Convenience wrapper around secret_list + secret_get.",
214
+ parameters: {
215
+ type: "object",
216
+ properties: {
217
+ scope: { type: "string", enum: VALID_SCOPES },
218
+ scopeId: { type: "string" },
219
+ name: { type: "string" },
220
+ },
221
+ required: ["scope", "scopeId", "name"],
222
+ },
223
+ execute: async (_id, params) => {
224
+ const scope = parseScope(params.scope);
225
+ const scopeId = parseString(params.scopeId, "scopeId");
226
+ const name = parseSecretName(params.name);
227
+ const secrets = await client.listSecrets({ scope, scopeId });
228
+ const match = secrets.find((s) => s.secretName === name);
229
+ if (!match)
230
+ return { found: false };
231
+ const row = await client.getSecretEnvelope({ scope, scopeId, secretId: match.secretId });
232
+ const plaintext = await decryptSecretEnvelope({
233
+ client,
234
+ scope,
235
+ scopeId,
236
+ secretId: match.secretId,
237
+ envelope: row.envelope,
238
+ });
239
+ return { found: true, secretId: match.secretId, secretName: match.secretName, value: plaintext };
240
+ },
241
+ }), { names: ["secret_get_by_name"] });
242
+ // ─── secret_delete ──────────────────────────────────────────
243
+ api.registerTool(() => ({
244
+ name: "secret_delete",
245
+ label: "Delete Secret",
246
+ description: "Delete a secret by secretId.",
247
+ parameters: {
248
+ type: "object",
249
+ properties: {
250
+ scope: { type: "string", enum: VALID_SCOPES },
251
+ scopeId: { type: "string" },
252
+ secretId: { type: "string" },
253
+ },
254
+ required: ["scope", "scopeId", "secretId"],
255
+ },
256
+ execute: async (_id, params) => {
257
+ const scope = parseScope(params.scope);
258
+ const scopeId = parseString(params.scopeId, "scopeId");
259
+ const secretId = parseSecretId(params.secretId);
260
+ await client.deleteSecret({ scope, scopeId, secretId });
261
+ logger.info("Secret deleted", { scope, scopeId, secretId });
262
+ return { deleted: true };
263
+ },
264
+ }), { names: ["secret_delete"] });
265
+ // ─── secret_rotate ──────────────────────────────────────────
266
+ api.registerTool(() => ({
267
+ name: "secret_rotate",
268
+ label: "Rotate Secret",
269
+ description: "Replace an existing secret's value with a new one. Issues a fresh data key and re-encrypts.",
270
+ parameters: {
271
+ type: "object",
272
+ properties: {
273
+ scope: { type: "string", enum: VALID_SCOPES },
274
+ scopeId: { type: "string" },
275
+ secretId: { type: "string" },
276
+ newValue: { type: "string", description: "The new plaintext value" },
277
+ },
278
+ required: ["scope", "scopeId", "secretId", "newValue"],
279
+ },
280
+ execute: async (_id, params) => {
281
+ const scope = parseScope(params.scope);
282
+ const scopeId = parseString(params.scopeId, "scopeId");
283
+ const secretId = parseSecretId(params.secretId);
284
+ const newValue = parseString(params.newValue, "newValue", 65_536);
285
+ const existing = await client.getSecretEnvelope({ scope, scopeId, secretId });
286
+ const envelope = await encryptSecretValue({
287
+ client,
288
+ scope,
289
+ scopeId,
290
+ secretId,
291
+ plaintext: newValue,
292
+ });
293
+ await client.putSecretEnvelope({
294
+ scope,
295
+ scopeId,
296
+ secretId,
297
+ secretName: existing.secretName,
298
+ envelope,
299
+ description: existing.description,
300
+ tags: existing.tags,
301
+ });
302
+ logger.info("Secret rotated", { scope, scopeId, secretId });
303
+ return { secretId, rotated: true };
304
+ },
305
+ }), { names: ["secret_rotate"] });
306
+ logger.info("openclaw-secrets plugin registered", { apiUrl: config.apiUrl });
307
+ },
308
+ };
309
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Plugin-local types. Shared contract types (SecretScope, EncryptedEnvelopeV1,
3
+ * SecretMetadata, etc.) live in `@alfe/types` and are re-exported by
4
+ * `@alfe.ai/agent-api-client`; import from there so we have one canonical
5
+ * source of truth across every agent HTTP surface.
6
+ */
7
+ export interface SecretsConfig {
8
+ /**
9
+ * Base URL of the Alfe API host — the same value `@alfe.ai/agent-api-client`
10
+ * expects (normally read from `ALFE_API_URL`), e.g. `https://api.alfe.ai`.
11
+ * The client prepends `/agent/secrets/*` automatically (the `/agent` segment
12
+ * is the agent-gateway api-mapping key; `/secrets` is the service
13
+ * pathPrefix). Do NOT include either segment in `apiUrl`.
14
+ */
15
+ apiUrl: string;
16
+ agentApiKey: string;
17
+ }
18
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,WAAW,aAAa;IAC5B;;;;;;OAMG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,WAAW,EAAE,MAAM,CAAC;CACrB"}
package/dist/types.js ADDED
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Plugin-local types. Shared contract types (SecretScope, EncryptedEnvelopeV1,
3
+ * SecretMetadata, etc.) live in `@alfe/types` and are re-exported by
4
+ * `@alfe.ai/agent-api-client`; import from there so we have one canonical
5
+ * source of truth across every agent HTTP surface.
6
+ */
7
+ export {};
8
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;;GAKG"}
@@ -0,0 +1,22 @@
1
+ {
2
+ "id": "secrets",
3
+ "kind": "secrets",
4
+ "name": "Secrets",
5
+ "description": "Per-scope (org/team/project/agent) encrypted secret store. Agents encrypt/decrypt locally with AES-256-GCM; KMS data keys are issued by the Alfe secrets service.",
6
+ "version": "0.1.0",
7
+ "entry": "./dist/index.js",
8
+ "configSchema": {
9
+ "type": "object",
10
+ "required": ["apiUrl", "agentApiKey"],
11
+ "properties": {
12
+ "apiUrl": {
13
+ "type": "string",
14
+ "description": "Base URL of the Alfe API host (same as ALFE_API_URL used by @alfe.ai/agent-api-client, e.g. https://api.alfe.ai). The client prepends /agent/secrets/* automatically — pass the host root, not the agent subpath."
15
+ },
16
+ "agentApiKey": {
17
+ "type": "string",
18
+ "description": "Agent API key (Bearer). agentId and tenantId are resolved server-side from this token — do NOT pass them in config."
19
+ }
20
+ }
21
+ }
22
+ }
package/package.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "name": "@alfe.ai/openclaw-secrets",
3
+ "version": "0.1.0",
4
+ "description": "OpenClaw plugin — per-scope (org/team/project/agent) encrypted secret store. Agents encrypt/decrypt locally with AES-256-GCM; KMS data keys are fetched via the Alfe secrets service.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "license": "UNLICENSED",
9
+ "dependencies": {
10
+ "@alfe.ai/agent-api-client": "0.0.7"
11
+ },
12
+ "devDependencies": {
13
+ "vitest": "^4.0.18"
14
+ },
15
+ "scripts": {
16
+ "build": "tsc",
17
+ "typecheck": "tsc --noEmit",
18
+ "lint": "eslint .",
19
+ "test": "vitest run --passWithNoTests"
20
+ }
21
+ }
@@ -0,0 +1,187 @@
1
+ /**
2
+ * Crypto roundtrip + tamper-fail tests for the openclaw-secrets plugin.
3
+ *
4
+ * We stub the narrow `SecretsKmsProxy` interface (the two KMS-proxy methods
5
+ * `generateSecretDataKey` / `decryptSecretDataKey`) so we don't need a real
6
+ * `AgentApiClient` or KMS during tests:
7
+ * - generateSecretDataKey returns a deterministic (random-but-stable) AES-256 key.
8
+ * - decryptSecretDataKey returns the same key for the same ciphertext handle.
9
+ *
10
+ * This lets us verify:
11
+ * - happy-path encrypt → decrypt roundtrip preserves bytes,
12
+ * - flipping any byte in the ciphertext / iv / authTag causes decrypt to fail,
13
+ * - a mismatched data key (simulating KMS context mismatch) causes decrypt to fail,
14
+ * - the Buffer passed to createCipheriv is zeroed after the call returns.
15
+ *
16
+ * We do NOT test the actual KMS proxy here — that is integration-tested
17
+ * against the real services/secrets.
18
+ */
19
+ import { describe, it, expect, vi } from "vitest";
20
+ import { randomBytes } from "node:crypto";
21
+ import { encryptSecretValue, decryptSecretEnvelope, type SecretsKmsProxy } from "../crypto.js";
22
+ import type { EncryptedEnvelopeV1 } from "@alfe.ai/agent-api-client";
23
+
24
+ function makeFakeProxy(options: {
25
+ /**
26
+ * If set, `decryptSecretDataKey` returns a DIFFERENT key than the one that
27
+ * was used to encrypt, simulating an encryption-context mismatch or a wrong
28
+ * data-key ciphertext.
29
+ */
30
+ tamperDecryptKey?: boolean;
31
+ } = {}): SecretsKmsProxy {
32
+ const keysByHandle = new Map<string, Buffer>();
33
+ let counter = 0;
34
+
35
+ return {
36
+ generateSecretDataKey: vi.fn(async () => {
37
+ const key = randomBytes(32);
38
+ const handle = `handle-${String(++counter)}`;
39
+ keysByHandle.set(handle, key);
40
+ return {
41
+ plaintextKey: key.toString("base64"),
42
+ dataKeyCiphertext: handle,
43
+ };
44
+ }),
45
+
46
+ decryptSecretDataKey: vi.fn(async ({ dataKeyCiphertext }) => {
47
+ const key = keysByHandle.get(dataKeyCiphertext);
48
+ if (!key) throw new Error(`Unknown handle: ${dataKeyCiphertext}`);
49
+ if (options.tamperDecryptKey) {
50
+ return { plaintextKey: randomBytes(32).toString("base64") };
51
+ }
52
+ return { plaintextKey: key.toString("base64") };
53
+ }),
54
+ };
55
+ }
56
+
57
+ const baseArgs = {
58
+ scope: "agent" as const,
59
+ scopeId: "agent-1",
60
+ secretId: "00000000-0000-4000-8000-000000000000",
61
+ };
62
+
63
+ describe("crypto", () => {
64
+ it("roundtrip: encrypt then decrypt returns the original plaintext", async () => {
65
+ const client = makeFakeProxy();
66
+ const plaintext = "sk-live-super-secret-\u2764\ufe0f";
67
+
68
+ const envelope = await encryptSecretValue({
69
+ client,
70
+ ...baseArgs,
71
+ plaintext,
72
+ });
73
+
74
+ expect(envelope.version).toBe(1);
75
+ expect(envelope.iv).toBeTruthy();
76
+ expect(envelope.ciphertext).toBeTruthy();
77
+ expect(envelope.authTag).toBeTruthy();
78
+ expect(envelope.dataKeyCiphertext).toBeTruthy();
79
+
80
+ const recovered = await decryptSecretEnvelope({
81
+ client,
82
+ ...baseArgs,
83
+ envelope,
84
+ });
85
+
86
+ expect(recovered).toBe(plaintext);
87
+ });
88
+
89
+ it("encrypt produces different ciphertext each call (fresh IV + data key)", async () => {
90
+ const client = makeFakeProxy();
91
+ const [a, b] = await Promise.all([
92
+ encryptSecretValue({ client, ...baseArgs, plaintext: "same" }),
93
+ encryptSecretValue({ client, ...baseArgs, plaintext: "same" }),
94
+ ]);
95
+ expect(a.ciphertext).not.toBe(b.ciphertext);
96
+ expect(a.iv).not.toBe(b.iv);
97
+ expect(a.dataKeyCiphertext).not.toBe(b.dataKeyCiphertext);
98
+ });
99
+
100
+ it("rejects a ciphertext with a flipped byte (GCM authTag fails)", async () => {
101
+ const client = makeFakeProxy();
102
+ const envelope = await encryptSecretValue({
103
+ client,
104
+ ...baseArgs,
105
+ plaintext: "hello",
106
+ });
107
+ const ct = Buffer.from(envelope.ciphertext, "base64");
108
+ ct[0] = ct[0] ^ 0x01;
109
+ const tampered: EncryptedEnvelopeV1 = {
110
+ ...envelope,
111
+ ciphertext: ct.toString("base64"),
112
+ };
113
+
114
+ await expect(
115
+ decryptSecretEnvelope({ client, ...baseArgs, envelope: tampered }),
116
+ ).rejects.toThrow();
117
+ });
118
+
119
+ it("rejects a flipped authTag byte", async () => {
120
+ const client = makeFakeProxy();
121
+ const envelope = await encryptSecretValue({
122
+ client,
123
+ ...baseArgs,
124
+ plaintext: "hello",
125
+ });
126
+ const tag = Buffer.from(envelope.authTag, "base64");
127
+ tag[0] = tag[0] ^ 0x80;
128
+
129
+ await expect(
130
+ decryptSecretEnvelope({
131
+ client,
132
+ ...baseArgs,
133
+ envelope: { ...envelope, authTag: tag.toString("base64") },
134
+ }),
135
+ ).rejects.toThrow();
136
+ });
137
+
138
+ it("rejects a flipped IV byte", async () => {
139
+ const client = makeFakeProxy();
140
+ const envelope = await encryptSecretValue({
141
+ client,
142
+ ...baseArgs,
143
+ plaintext: "hello",
144
+ });
145
+ const iv = Buffer.from(envelope.iv, "base64");
146
+ iv[0] = iv[0] ^ 0x11;
147
+
148
+ await expect(
149
+ decryptSecretEnvelope({
150
+ client,
151
+ ...baseArgs,
152
+ envelope: { ...envelope, iv: iv.toString("base64") },
153
+ }),
154
+ ).rejects.toThrow();
155
+ });
156
+
157
+ it("rejects a decrypt that returns a different data key (encryption-context mismatch)", async () => {
158
+ const client = makeFakeProxy({ tamperDecryptKey: true });
159
+ const envelope = await encryptSecretValue({
160
+ client,
161
+ ...baseArgs,
162
+ plaintext: "hello",
163
+ });
164
+
165
+ await expect(
166
+ decryptSecretEnvelope({ client, ...baseArgs, envelope }),
167
+ ).rejects.toThrow();
168
+ });
169
+
170
+ it("rejects a bogus data-key length from the proxy (32 bytes required)", async () => {
171
+ const badClient: SecretsKmsProxy = {
172
+ generateSecretDataKey: vi.fn(async () => ({
173
+ plaintextKey: Buffer.alloc(16).toString("base64"),
174
+ dataKeyCiphertext: "h",
175
+ })),
176
+ decryptSecretDataKey: vi.fn(),
177
+ };
178
+
179
+ await expect(
180
+ encryptSecretValue({
181
+ client: badClient,
182
+ ...baseArgs,
183
+ plaintext: "hello",
184
+ }),
185
+ ).rejects.toThrow(/data key length/);
186
+ });
187
+ });
package/src/crypto.ts ADDED
@@ -0,0 +1,109 @@
1
+ /**
2
+ * Agent-side AES-256-GCM with data keys issued by the Alfe secrets service.
3
+ *
4
+ * Trust model:
5
+ * - The agent NEVER holds a KMS key directly.
6
+ * - For each encrypt/decrypt, the agent fetches a one-shot AES data key from
7
+ * the secrets service over authenticated TLS.
8
+ * - The plaintext key is held as a `Buffer` ONLY — never as a JS string.
9
+ * (Strings are immutable and cannot be zeroed; Buffers can be `.fill(0)`'d.)
10
+ * - After each operation the Buffer is zeroed.
11
+ *
12
+ * Envelope format is `EncryptedEnvelopeV1` as defined in `@alfe/types` and
13
+ * persisted by services/secrets.
14
+ */
15
+ import { createCipheriv, createDecipheriv, randomBytes } from "node:crypto";
16
+ import type { EncryptedEnvelopeV1, SecretScope } from "@alfe.ai/agent-api-client";
17
+
18
+ /**
19
+ * Narrow interface capturing just the two KMS-proxy methods the crypto
20
+ * helpers need. `AgentApiClient` satisfies this; tests can supply a
21
+ * hand-rolled stub without mocking the whole client surface.
22
+ */
23
+ export interface SecretsKmsProxy {
24
+ generateSecretDataKey(args: {
25
+ scope: SecretScope;
26
+ scopeId: string;
27
+ secretId: string;
28
+ }): Promise<{ plaintextKey: string; dataKeyCiphertext: string }>;
29
+
30
+ decryptSecretDataKey(args: {
31
+ scope: SecretScope;
32
+ scopeId: string;
33
+ secretId: string;
34
+ dataKeyCiphertext: string;
35
+ }): Promise<{ plaintextKey: string }>;
36
+ }
37
+
38
+ const AES_256_KEY_BYTES = 32;
39
+ const GCM_IV_BYTES = 12;
40
+
41
+ function decodeKeyToBuffer(base64: string): Buffer {
42
+ const buf = Buffer.from(base64, "base64");
43
+ if (buf.length !== AES_256_KEY_BYTES) {
44
+ buf.fill(0);
45
+ throw new Error(`Unexpected data key length: ${String(buf.length)} bytes`);
46
+ }
47
+ return buf;
48
+ }
49
+
50
+ export async function encryptSecretValue(args: {
51
+ client: SecretsKmsProxy;
52
+ scope: SecretScope;
53
+ scopeId: string;
54
+ secretId: string;
55
+ plaintext: string;
56
+ }): Promise<EncryptedEnvelopeV1> {
57
+ const { client, scope, scopeId, secretId, plaintext } = args;
58
+ const { plaintextKey, dataKeyCiphertext } = await client.generateSecretDataKey({
59
+ scope,
60
+ scopeId,
61
+ secretId,
62
+ });
63
+
64
+ const keyBuf = decodeKeyToBuffer(plaintextKey);
65
+ try {
66
+ const iv = randomBytes(GCM_IV_BYTES);
67
+ const cipher = createCipheriv("aes-256-gcm", keyBuf, iv);
68
+ const ciphertext = Buffer.concat([cipher.update(plaintext, "utf8"), cipher.final()]);
69
+ const authTag = cipher.getAuthTag();
70
+ return {
71
+ version: 1,
72
+ iv: iv.toString("base64"),
73
+ ciphertext: ciphertext.toString("base64"),
74
+ authTag: authTag.toString("base64"),
75
+ dataKeyCiphertext,
76
+ };
77
+ } finally {
78
+ keyBuf.fill(0);
79
+ }
80
+ }
81
+
82
+ export async function decryptSecretEnvelope(args: {
83
+ client: SecretsKmsProxy;
84
+ scope: SecretScope;
85
+ scopeId: string;
86
+ secretId: string;
87
+ envelope: EncryptedEnvelopeV1;
88
+ }): Promise<string> {
89
+ const { client, scope, scopeId, secretId, envelope } = args;
90
+ const { plaintextKey } = await client.decryptSecretDataKey({
91
+ scope,
92
+ scopeId,
93
+ secretId,
94
+ dataKeyCiphertext: envelope.dataKeyCiphertext,
95
+ });
96
+
97
+ const keyBuf = decodeKeyToBuffer(plaintextKey);
98
+ try {
99
+ const iv = Buffer.from(envelope.iv, "base64");
100
+ const ciphertext = Buffer.from(envelope.ciphertext, "base64");
101
+ const authTag = Buffer.from(envelope.authTag, "base64");
102
+ const decipher = createDecipheriv("aes-256-gcm", keyBuf, iv);
103
+ decipher.setAuthTag(authTag);
104
+ const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
105
+ return decrypted.toString("utf8");
106
+ } finally {
107
+ keyBuf.fill(0);
108
+ }
109
+ }
package/src/index.ts ADDED
@@ -0,0 +1,356 @@
1
+ /**
2
+ * openclaw-secrets — OpenClaw plugin for per-scope encrypted secret storage.
3
+ *
4
+ * Trust model:
5
+ * - Secret values are encrypted and decrypted ON THIS AGENT using AES-256-GCM.
6
+ * - Data keys are issued per-secret by the Alfe secrets service; plaintext keys
7
+ * are held as Buffers only, used once, and zeroed immediately.
8
+ * - The backend never sees plaintext secret values (the dashboard can set values
9
+ * through a server-side encrypt path but cannot read them back).
10
+ *
11
+ * All HTTP traffic goes through `@alfe.ai/agent-api-client` — the single
12
+ * canonical agent-side HTTP client — so routes, auth, and response unwrapping
13
+ * stay in lockstep with every other agent endpoint.
14
+ *
15
+ * Tools:
16
+ * - secret_set — create a new secret (mints UUID, encrypts, uploads envelope)
17
+ * - secret_get — fetch + decrypt plaintext by secretId
18
+ * - secret_get_by_name — list then get (convenience)
19
+ * - secret_list — list metadata in a scope (never plaintext)
20
+ * - secret_list_scopes — enumerate scopes the agent can access
21
+ * - secret_delete — delete a secret
22
+ * - secret_rotate — re-encrypt with a fresh data key
23
+ */
24
+ import { randomUUID } from "node:crypto";
25
+ import { AgentApiClient, type SecretScope } from "@alfe.ai/agent-api-client";
26
+ import { decryptSecretEnvelope, encryptSecretValue } from "./crypto.js";
27
+ import type { SecretsConfig } from "./types.js";
28
+
29
+ interface PluginLogger {
30
+ info: (msg: string, ctx?: Record<string, unknown>) => void;
31
+ debug: (msg: string, ctx?: Record<string, unknown>) => void;
32
+ warn: (msg: string, ctx?: Record<string, unknown>) => void;
33
+ error: (msg: string, ctx?: Record<string, unknown>) => void;
34
+ }
35
+
36
+ interface ToolContext {
37
+ agentId?: string;
38
+ sessionKey?: string;
39
+ sessionId?: string;
40
+ messageChannel?: string;
41
+ }
42
+
43
+ interface Tool {
44
+ name: string;
45
+ label: string;
46
+ description: string;
47
+ parameters: Record<string, unknown>;
48
+ execute: (toolCallId: string, params: Record<string, unknown>) => Promise<unknown>;
49
+ }
50
+
51
+ interface PluginApi {
52
+ pluginConfig?: Record<string, unknown>;
53
+ config: Record<string, unknown>;
54
+ logger: PluginLogger;
55
+ registerTool: (factory: (ctx: ToolContext) => Tool, opts?: { names?: string[] }) => void;
56
+ registerHook?: (events: string | string[], handler: (...args: unknown[]) => unknown, opts?: { name?: string; description?: string }) => void;
57
+ on?: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
58
+ registerMemoryPromptSection?: (builder: (params: { availableTools: Set<string> }) => string[]) => void;
59
+ }
60
+
61
+ const VALID_SCOPES: SecretScope[] = ["org", "team", "project", "agent"];
62
+ const NAME_REGEX = /^[a-zA-Z0-9_./-]{1,128}$/;
63
+ 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;
64
+
65
+ function resolveConfig(raw?: Record<string, unknown>): SecretsConfig {
66
+ // Match @alfe.ai/agent-api-client's config shape. agentId and tenantId are
67
+ // resolved server-side from the bearer token, so we intentionally don't
68
+ // accept them here — reading caller-supplied identity would be a footgun.
69
+ const apiUrl = typeof raw?.apiUrl === "string" ? raw.apiUrl : (process.env.ALFE_API_URL ?? "");
70
+ const agentApiKey = typeof raw?.agentApiKey === "string" ? raw.agentApiKey : "";
71
+ if (!apiUrl) throw new Error("openclaw-secrets: apiUrl (or ALFE_API_URL) is required");
72
+ if (!agentApiKey) throw new Error("openclaw-secrets: agentApiKey is required");
73
+ return { apiUrl, agentApiKey };
74
+ }
75
+
76
+ function parseScope(raw: unknown, field = "scope"): SecretScope {
77
+ if (typeof raw !== "string" || !VALID_SCOPES.includes(raw as SecretScope)) {
78
+ throw new Error(`${field} must be one of ${VALID_SCOPES.join(", ")}`);
79
+ }
80
+ return raw as SecretScope;
81
+ }
82
+
83
+ function parseString(raw: unknown, field: string, max = 256): string {
84
+ if (typeof raw !== "string" || raw.length === 0) {
85
+ throw new Error(`${field} is required`);
86
+ }
87
+ if (raw.length > max) {
88
+ throw new Error(`${field} must be ${String(max)} characters or fewer`);
89
+ }
90
+ return raw;
91
+ }
92
+
93
+ function parseSecretName(raw: unknown): string {
94
+ const s = parseString(raw, "name", 128);
95
+ if (!NAME_REGEX.test(s)) {
96
+ throw new Error("name must match ^[a-zA-Z0-9_./-]{1,128}$");
97
+ }
98
+ return s;
99
+ }
100
+
101
+ function parseSecretId(raw: unknown): string {
102
+ const s = parseString(raw, "secretId", 64);
103
+ if (!UUID_V4.test(s)) throw new Error("secretId must be a UUID v4");
104
+ return s;
105
+ }
106
+
107
+ export default {
108
+ id: "secrets",
109
+ name: "Secrets",
110
+ description: "Per-scope encrypted secret storage — agent-side AES-256-GCM with KMS-issued data keys.",
111
+ version: "0.1.0",
112
+ kind: "secrets" as const,
113
+
114
+ register(api: PluginApi): void {
115
+ const config = resolveConfig(api.pluginConfig);
116
+ const client = new AgentApiClient({ apiUrl: config.apiUrl, apiKey: config.agentApiKey });
117
+ const logger = api.logger;
118
+
119
+ if (api.registerMemoryPromptSection) {
120
+ api.registerMemoryPromptSection(({ availableTools }) => {
121
+ const lines: string[] = ["## Secrets"];
122
+ if (availableTools.has("secret_list_scopes")) {
123
+ lines.push("Use secret_list_scopes to discover which scopes (org/team/project/agent) you can manage secrets in.");
124
+ }
125
+ if (availableTools.has("secret_list")) {
126
+ lines.push("Use secret_list to see secret names in a scope. secret_list NEVER returns plaintext.");
127
+ }
128
+ if (availableTools.has("secret_get")) {
129
+ lines.push("Use secret_get to retrieve a plaintext secret value. The output is SENSITIVE — do not echo it to the user or log it.");
130
+ }
131
+ if (availableTools.has("secret_set")) {
132
+ lines.push("Use secret_set to create or update a secret. Prefer scoping to 'agent' for personal secrets.");
133
+ }
134
+ return lines;
135
+ });
136
+ }
137
+
138
+ // ─── secret_list_scopes ─────────────────────────────────────
139
+ api.registerTool(() => ({
140
+ name: "secret_list_scopes",
141
+ label: "List Secret Scopes",
142
+ description: "List scopes (org/team/project/agent) the agent can manage secrets in. Use this before guessing scopeIds.",
143
+ parameters: { type: "object", properties: {} },
144
+ execute: async () => {
145
+ const scopes = await client.listSecretScopes();
146
+ return { scopes };
147
+ },
148
+ }), { names: ["secret_list_scopes"] });
149
+
150
+ // ─── secret_list ────────────────────────────────────────────
151
+ api.registerTool(() => ({
152
+ name: "secret_list",
153
+ label: "List Secrets",
154
+ description: "List secret metadata (never plaintext) in a given scope.",
155
+ parameters: {
156
+ type: "object",
157
+ properties: {
158
+ scope: { type: "string", enum: VALID_SCOPES, description: "Scope level" },
159
+ scopeId: { type: "string", description: "Scope identifier (tenantId / teamId / projectId / agentId)" },
160
+ },
161
+ required: ["scope", "scopeId"],
162
+ },
163
+ execute: async (_id, params) => {
164
+ const scope = parseScope(params.scope);
165
+ const scopeId = parseString(params.scopeId, "scopeId");
166
+ const secrets = await client.listSecrets({ scope, scopeId });
167
+ return { secrets };
168
+ },
169
+ }), { names: ["secret_list"] });
170
+
171
+ // ─── secret_set ─────────────────────────────────────────────
172
+ api.registerTool(() => ({
173
+ name: "secret_set",
174
+ label: "Set Secret",
175
+ description: "Create a new secret. Encrypts the value locally and uploads only the envelope.",
176
+ parameters: {
177
+ type: "object",
178
+ properties: {
179
+ scope: { type: "string", enum: VALID_SCOPES, description: "Scope level" },
180
+ scopeId: { type: "string", description: "Scope identifier" },
181
+ name: { type: "string", description: "Human-readable secret name (unique within scope)" },
182
+ value: { type: "string", description: "Secret plaintext value (encrypted before upload)" },
183
+ description: { type: "string", description: "Optional description" },
184
+ tags: { type: "array", items: { type: "string" }, description: "Optional tags" },
185
+ },
186
+ required: ["scope", "scopeId", "name", "value"],
187
+ },
188
+ execute: async (_id, params) => {
189
+ const scope = parseScope(params.scope);
190
+ const scopeId = parseString(params.scopeId, "scopeId");
191
+ const name = parseSecretName(params.name);
192
+ const value = parseString(params.value, "value", 65_536);
193
+ const description = typeof params.description === "string" ? params.description : undefined;
194
+ const tags = Array.isArray(params.tags)
195
+ ? params.tags.filter((t): t is string => typeof t === "string")
196
+ : undefined;
197
+ const secretId = randomUUID();
198
+
199
+ const envelope = await encryptSecretValue({
200
+ client,
201
+ scope,
202
+ scopeId,
203
+ secretId,
204
+ plaintext: value,
205
+ });
206
+ await client.putSecretEnvelope({
207
+ scope,
208
+ scopeId,
209
+ secretId,
210
+ secretName: name,
211
+ envelope,
212
+ description,
213
+ tags,
214
+ });
215
+ logger.info("Secret created", { scope, scopeId, secretId });
216
+ return { secretId };
217
+ },
218
+ }), { names: ["secret_set"] });
219
+
220
+ // ─── secret_get ─────────────────────────────────────────────
221
+ api.registerTool(() => ({
222
+ name: "secret_get",
223
+ label: "Get Secret",
224
+ description: "Fetch and decrypt a secret by secretId. The return value is SENSITIVE; do not echo or log it.",
225
+ parameters: {
226
+ type: "object",
227
+ properties: {
228
+ scope: { type: "string", enum: VALID_SCOPES },
229
+ scopeId: { type: "string" },
230
+ secretId: { type: "string", description: "UUID v4 of the secret" },
231
+ },
232
+ required: ["scope", "scopeId", "secretId"],
233
+ },
234
+ execute: async (_id, params) => {
235
+ const scope = parseScope(params.scope);
236
+ const scopeId = parseString(params.scopeId, "scopeId");
237
+ const secretId = parseSecretId(params.secretId);
238
+ const row = await client.getSecretEnvelope({ scope, scopeId, secretId });
239
+ const plaintext = await decryptSecretEnvelope({
240
+ client,
241
+ scope,
242
+ scopeId,
243
+ secretId,
244
+ envelope: row.envelope,
245
+ });
246
+ return {
247
+ secretId,
248
+ secretName: row.secretName,
249
+ value: plaintext,
250
+ };
251
+ },
252
+ }), { names: ["secret_get"] });
253
+
254
+ // ─── secret_get_by_name ─────────────────────────────────────
255
+ api.registerTool(() => ({
256
+ name: "secret_get_by_name",
257
+ label: "Get Secret By Name",
258
+ description: "Fetch a secret by its human-readable name. Convenience wrapper around secret_list + secret_get.",
259
+ parameters: {
260
+ type: "object",
261
+ properties: {
262
+ scope: { type: "string", enum: VALID_SCOPES },
263
+ scopeId: { type: "string" },
264
+ name: { type: "string" },
265
+ },
266
+ required: ["scope", "scopeId", "name"],
267
+ },
268
+ execute: async (_id, params) => {
269
+ const scope = parseScope(params.scope);
270
+ const scopeId = parseString(params.scopeId, "scopeId");
271
+ const name = parseSecretName(params.name);
272
+ const secrets = await client.listSecrets({ scope, scopeId });
273
+ const match = secrets.find((s) => s.secretName === name);
274
+ if (!match) return { found: false };
275
+ const row = await client.getSecretEnvelope({ scope, scopeId, secretId: match.secretId });
276
+ const plaintext = await decryptSecretEnvelope({
277
+ client,
278
+ scope,
279
+ scopeId,
280
+ secretId: match.secretId,
281
+ envelope: row.envelope,
282
+ });
283
+ return { found: true, secretId: match.secretId, secretName: match.secretName, value: plaintext };
284
+ },
285
+ }), { names: ["secret_get_by_name"] });
286
+
287
+ // ─── secret_delete ──────────────────────────────────────────
288
+ api.registerTool(() => ({
289
+ name: "secret_delete",
290
+ label: "Delete Secret",
291
+ description: "Delete a secret by secretId.",
292
+ parameters: {
293
+ type: "object",
294
+ properties: {
295
+ scope: { type: "string", enum: VALID_SCOPES },
296
+ scopeId: { type: "string" },
297
+ secretId: { type: "string" },
298
+ },
299
+ required: ["scope", "scopeId", "secretId"],
300
+ },
301
+ execute: async (_id, params) => {
302
+ const scope = parseScope(params.scope);
303
+ const scopeId = parseString(params.scopeId, "scopeId");
304
+ const secretId = parseSecretId(params.secretId);
305
+ await client.deleteSecret({ scope, scopeId, secretId });
306
+ logger.info("Secret deleted", { scope, scopeId, secretId });
307
+ return { deleted: true };
308
+ },
309
+ }), { names: ["secret_delete"] });
310
+
311
+ // ─── secret_rotate ──────────────────────────────────────────
312
+ api.registerTool(() => ({
313
+ name: "secret_rotate",
314
+ label: "Rotate Secret",
315
+ description: "Replace an existing secret's value with a new one. Issues a fresh data key and re-encrypts.",
316
+ parameters: {
317
+ type: "object",
318
+ properties: {
319
+ scope: { type: "string", enum: VALID_SCOPES },
320
+ scopeId: { type: "string" },
321
+ secretId: { type: "string" },
322
+ newValue: { type: "string", description: "The new plaintext value" },
323
+ },
324
+ required: ["scope", "scopeId", "secretId", "newValue"],
325
+ },
326
+ execute: async (_id, params) => {
327
+ const scope = parseScope(params.scope);
328
+ const scopeId = parseString(params.scopeId, "scopeId");
329
+ const secretId = parseSecretId(params.secretId);
330
+ const newValue = parseString(params.newValue, "newValue", 65_536);
331
+
332
+ const existing = await client.getSecretEnvelope({ scope, scopeId, secretId });
333
+ const envelope = await encryptSecretValue({
334
+ client,
335
+ scope,
336
+ scopeId,
337
+ secretId,
338
+ plaintext: newValue,
339
+ });
340
+ await client.putSecretEnvelope({
341
+ scope,
342
+ scopeId,
343
+ secretId,
344
+ secretName: existing.secretName,
345
+ envelope,
346
+ description: existing.description,
347
+ tags: existing.tags,
348
+ });
349
+ logger.info("Secret rotated", { scope, scopeId, secretId });
350
+ return { secretId, rotated: true };
351
+ },
352
+ }), { names: ["secret_rotate"] });
353
+
354
+ logger.info("openclaw-secrets plugin registered", { apiUrl: config.apiUrl });
355
+ },
356
+ };
package/src/types.ts ADDED
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Plugin-local types. Shared contract types (SecretScope, EncryptedEnvelopeV1,
3
+ * SecretMetadata, etc.) live in `@alfe/types` and are re-exported by
4
+ * `@alfe.ai/agent-api-client`; import from there so we have one canonical
5
+ * source of truth across every agent HTTP surface.
6
+ */
7
+
8
+ export interface SecretsConfig {
9
+ /**
10
+ * Base URL of the Alfe API host — the same value `@alfe.ai/agent-api-client`
11
+ * expects (normally read from `ALFE_API_URL`), e.g. `https://api.alfe.ai`.
12
+ * The client prepends `/agent/secrets/*` automatically (the `/agent` segment
13
+ * is the agent-gateway api-mapping key; `/secrets` is the service
14
+ * pathPrefix). Do NOT include either segment in `apiUrl`.
15
+ */
16
+ apiUrl: string;
17
+ agentApiKey: string;
18
+ }
package/sst-env.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /* This file is auto-generated by SST. Do not edit. */
2
+ /* tslint:disable */
3
+ /* eslint-disable */
4
+ /* deno-fmt-ignore-file */
5
+ /* biome-ignore-all lint: auto-generated */
6
+
7
+ /// <reference path="../../sst-env.d.ts" />
8
+
9
+ import "sst"
10
+ export {}
package/tsconfig.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "lib": ["ES2022"],
7
+ "outDir": "dist",
8
+ "rootDir": "src",
9
+ "strict": true,
10
+ "esModuleInterop": true,
11
+ "skipLibCheck": true,
12
+ "forceConsistentCasingInFileNames": true,
13
+ "resolveJsonModule": true,
14
+ "declaration": true,
15
+ "declarationMap": true,
16
+ "sourceMap": true
17
+ },
18
+ "include": ["src/**/*"],
19
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts", "src/**/__tests__/**"]
20
+ }
@@ -0,0 +1,9 @@
1
+ import { defineConfig } from "vitest/config";
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ include: ["src/**/*.test.ts"],
6
+ exclude: ["dist/**", "node_modules/**"],
7
+ testTimeout: 5_000,
8
+ },
9
+ });