@1claw/openclaw-plugin 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.
- package/LICENSE +21 -0
- package/README.md +112 -0
- package/openclaw.plugin.json +103 -0
- package/package.json +57 -0
- package/skills/1claw/SKILL.md +846 -0
- package/src/client.ts +379 -0
- package/src/commands/index.ts +18 -0
- package/src/commands/list.ts +33 -0
- package/src/commands/rotate.ts +39 -0
- package/src/commands/status.ts +46 -0
- package/src/config.ts +79 -0
- package/src/hooks/index.ts +24 -0
- package/src/hooks/secret-injection.ts +58 -0
- package/src/hooks/secret-redaction.ts +90 -0
- package/src/hooks/shroud-routing.ts +45 -0
- package/src/index.ts +83 -0
- package/src/security/index.ts +153 -0
- package/src/services/index.ts +17 -0
- package/src/services/key-rotation.ts +52 -0
- package/src/services/token-refresh.ts +33 -0
- package/src/tools/create-vault.ts +28 -0
- package/src/tools/delete-secret.ts +28 -0
- package/src/tools/describe-secret.ts +64 -0
- package/src/tools/get-env-bundle.ts +49 -0
- package/src/tools/get-secret.ts +46 -0
- package/src/tools/grant-access.ts +50 -0
- package/src/tools/index.ts +92 -0
- package/src/tools/list-secrets.ts +39 -0
- package/src/tools/list-vaults.ts +29 -0
- package/src/tools/put-secret.ts +44 -0
- package/src/tools/rotate-and-store.ts +25 -0
- package/src/tools/share-secret.ts +61 -0
- package/src/tools/simulate-transaction.ts +66 -0
- package/src/tools/submit-transaction.ts +69 -0
- package/src/types.ts +186 -0
package/src/client.ts
ADDED
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SecretMetadata,
|
|
3
|
+
SecretWithValue,
|
|
4
|
+
SecretListResponse,
|
|
5
|
+
VaultResponse,
|
|
6
|
+
VaultListResponse,
|
|
7
|
+
PolicyResponse,
|
|
8
|
+
ShareLinkResponse,
|
|
9
|
+
SimulationResponse,
|
|
10
|
+
BundleSimulationResponse,
|
|
11
|
+
TransactionResponse,
|
|
12
|
+
AgentProfile,
|
|
13
|
+
ApiErrorBody,
|
|
14
|
+
} from "./types.js";
|
|
15
|
+
|
|
16
|
+
export class OneClawApiError extends Error {
|
|
17
|
+
constructor(
|
|
18
|
+
public status: number,
|
|
19
|
+
public detail: string,
|
|
20
|
+
) {
|
|
21
|
+
super(detail);
|
|
22
|
+
this.name = "OneClawApiError";
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ClientConfig {
|
|
27
|
+
baseUrl: string;
|
|
28
|
+
token: string;
|
|
29
|
+
vaultId: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface AgentCredentials {
|
|
33
|
+
baseUrl: string;
|
|
34
|
+
agentId?: string;
|
|
35
|
+
apiKey: string;
|
|
36
|
+
vaultId?: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface AgentTokenResponse {
|
|
40
|
+
access_token: string;
|
|
41
|
+
expires_in: number;
|
|
42
|
+
agent_id?: string;
|
|
43
|
+
vault_ids?: string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function encodePath(path: string): string {
|
|
47
|
+
return path
|
|
48
|
+
.split("/")
|
|
49
|
+
.map((s) => encodeURIComponent(s))
|
|
50
|
+
.join("/");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const REFRESH_BUFFER_MS = 60_000;
|
|
54
|
+
|
|
55
|
+
export class OneClawClient {
|
|
56
|
+
private baseUrl: string;
|
|
57
|
+
private token: string;
|
|
58
|
+
private _vaultId: string;
|
|
59
|
+
private _resolvedAgentId?: string;
|
|
60
|
+
|
|
61
|
+
private agentCredentials?: { agentId?: string; apiKey: string };
|
|
62
|
+
private tokenExpiresAt = 0;
|
|
63
|
+
|
|
64
|
+
constructor(config: ClientConfig | AgentCredentials) {
|
|
65
|
+
this.baseUrl = config.baseUrl.replace(/\/$/, "");
|
|
66
|
+
this._vaultId = config.vaultId ?? "";
|
|
67
|
+
|
|
68
|
+
if ("apiKey" in config && !("token" in config)) {
|
|
69
|
+
this.agentCredentials = {
|
|
70
|
+
agentId: config.agentId,
|
|
71
|
+
apiKey: config.apiKey,
|
|
72
|
+
};
|
|
73
|
+
this.token = "";
|
|
74
|
+
} else {
|
|
75
|
+
this.token = (config as ClientConfig).token;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async ensureToken(): Promise<void> {
|
|
80
|
+
if (!this.agentCredentials) return;
|
|
81
|
+
if (this.token && Date.now() < this.tokenExpiresAt - REFRESH_BUFFER_MS)
|
|
82
|
+
return;
|
|
83
|
+
|
|
84
|
+
const body: Record<string, string> = {
|
|
85
|
+
api_key: this.agentCredentials.apiKey,
|
|
86
|
+
};
|
|
87
|
+
if (this.agentCredentials.agentId) {
|
|
88
|
+
body.agent_id = this.agentCredentials.agentId;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const res = await fetch(`${this.baseUrl}/v1/auth/agent-token`, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "Content-Type": "application/json" },
|
|
94
|
+
body: JSON.stringify(body),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
if (!res.ok) {
|
|
98
|
+
let detail = `HTTP ${res.status}`;
|
|
99
|
+
try {
|
|
100
|
+
const errBody = (await res.json()) as ApiErrorBody;
|
|
101
|
+
if (errBody.detail) detail = errBody.detail;
|
|
102
|
+
} catch {
|
|
103
|
+
/* use default */
|
|
104
|
+
}
|
|
105
|
+
throw new OneClawApiError(
|
|
106
|
+
res.status,
|
|
107
|
+
`Agent auth failed: ${detail}`,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const data = (await res.json()) as AgentTokenResponse;
|
|
112
|
+
this.token = data.access_token;
|
|
113
|
+
this.tokenExpiresAt = Date.now() + data.expires_in * 1000;
|
|
114
|
+
|
|
115
|
+
if (data.agent_id) {
|
|
116
|
+
this._resolvedAgentId = data.agent_id;
|
|
117
|
+
if (this.agentCredentials && !this.agentCredentials.agentId) {
|
|
118
|
+
this.agentCredentials.agentId = data.agent_id;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!this._vaultId && data.vault_ids && data.vault_ids.length === 1) {
|
|
123
|
+
this._vaultId = data.vault_ids[0];
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private async autoDiscoverVault(): Promise<void> {
|
|
128
|
+
const vaults = await this.listVaults();
|
|
129
|
+
if (vaults.vaults && vaults.vaults.length > 0) {
|
|
130
|
+
this._vaultId = vaults.vaults[0].id;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private async headers(): Promise<Record<string, string>> {
|
|
135
|
+
await this.ensureToken();
|
|
136
|
+
return {
|
|
137
|
+
Authorization: `Bearer ${this.token}`,
|
|
138
|
+
"Content-Type": "application/json",
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
private async resolveVaultUrl(suffix = ""): Promise<string> {
|
|
143
|
+
if (!this._vaultId) {
|
|
144
|
+
await this.autoDiscoverVault();
|
|
145
|
+
}
|
|
146
|
+
if (!this._vaultId) {
|
|
147
|
+
throw new OneClawApiError(
|
|
148
|
+
400,
|
|
149
|
+
"No vault configured. Set ONECLAW_VAULT_ID, bind the agent to a vault, or create a vault first.",
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return `${this.baseUrl}/v1/vaults/${this._vaultId}${suffix}`;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
private async request<T>(url: string, init?: RequestInit): Promise<T> {
|
|
156
|
+
const hdrs = await this.headers();
|
|
157
|
+
const res = await fetch(url, {
|
|
158
|
+
...init,
|
|
159
|
+
headers: { ...hdrs, ...(init?.headers as Record<string, string>) },
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
if (!res.ok) {
|
|
163
|
+
let detail = `HTTP ${res.status}`;
|
|
164
|
+
let errorType = "";
|
|
165
|
+
try {
|
|
166
|
+
const body = (await res.json()) as ApiErrorBody;
|
|
167
|
+
if (body.detail) detail = body.detail;
|
|
168
|
+
if (body.type) errorType = body.type;
|
|
169
|
+
} catch {
|
|
170
|
+
// use default detail
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (res.status === 402) {
|
|
174
|
+
throw new OneClawApiError(
|
|
175
|
+
402,
|
|
176
|
+
"Quota exhausted. Ask your human to upgrade the plan, add prepaid credits, or enable x402 micropayments at https://1claw.xyz/settings/billing",
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (
|
|
181
|
+
res.status === 403 &&
|
|
182
|
+
errorType === "resource_limit_exceeded"
|
|
183
|
+
) {
|
|
184
|
+
throw new OneClawApiError(
|
|
185
|
+
403,
|
|
186
|
+
`Resource limit reached: ${detail}. Ask your human to upgrade the plan at https://1claw.xyz/settings/billing`,
|
|
187
|
+
);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
throw new OneClawApiError(res.status, detail);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
if (res.status === 204) return undefined as T;
|
|
194
|
+
return res.json() as Promise<T>;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
get agentId(): string | undefined {
|
|
198
|
+
return this._resolvedAgentId ?? this.agentCredentials?.agentId;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
get vaultId(): string {
|
|
202
|
+
return this._vaultId;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
get tokenTtlMs(): number {
|
|
206
|
+
if (!this.tokenExpiresAt) return 0;
|
|
207
|
+
return Math.max(0, this.tokenExpiresAt - Date.now());
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
get isAuthenticated(): boolean {
|
|
211
|
+
return !!this.token;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// ── Secrets ──────────────────────────────────────────
|
|
215
|
+
|
|
216
|
+
async listSecrets(): Promise<SecretListResponse> {
|
|
217
|
+
return this.request<SecretListResponse>(
|
|
218
|
+
await this.resolveVaultUrl("/secrets"),
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
async getSecret(path: string): Promise<SecretWithValue> {
|
|
223
|
+
return this.request<SecretWithValue>(
|
|
224
|
+
await this.resolveVaultUrl(`/secrets/${encodePath(path)}`),
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async putSecret(
|
|
229
|
+
path: string,
|
|
230
|
+
body: {
|
|
231
|
+
value: string;
|
|
232
|
+
type: string;
|
|
233
|
+
metadata?: Record<string, unknown>;
|
|
234
|
+
expires_at?: string;
|
|
235
|
+
max_access_count?: number;
|
|
236
|
+
},
|
|
237
|
+
): Promise<SecretMetadata> {
|
|
238
|
+
return this.request<SecretMetadata>(
|
|
239
|
+
await this.resolveVaultUrl(`/secrets/${encodePath(path)}`),
|
|
240
|
+
{ method: "PUT", body: JSON.stringify(body) },
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async deleteSecret(path: string): Promise<void> {
|
|
245
|
+
await this.request<void>(
|
|
246
|
+
await this.resolveVaultUrl(`/secrets/${encodePath(path)}`),
|
|
247
|
+
{ method: "DELETE" },
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ── Vaults ───────────────────────────────────────────
|
|
252
|
+
|
|
253
|
+
async createVault(
|
|
254
|
+
name: string,
|
|
255
|
+
description?: string,
|
|
256
|
+
): Promise<VaultResponse> {
|
|
257
|
+
return this.request<VaultResponse>(`${this.baseUrl}/v1/vaults`, {
|
|
258
|
+
method: "POST",
|
|
259
|
+
body: JSON.stringify({ name, description: description ?? "" }),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
async listVaults(): Promise<VaultListResponse> {
|
|
264
|
+
return this.request<VaultListResponse>(`${this.baseUrl}/v1/vaults`);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// ── Policies ─────────────────────────────────────────
|
|
268
|
+
|
|
269
|
+
async createPolicy(
|
|
270
|
+
vaultId: string,
|
|
271
|
+
principalType: string,
|
|
272
|
+
principalId: string,
|
|
273
|
+
permissions: string[],
|
|
274
|
+
secretPathPattern = "**",
|
|
275
|
+
): Promise<PolicyResponse> {
|
|
276
|
+
return this.request<PolicyResponse>(
|
|
277
|
+
`${this.baseUrl}/v1/vaults/${vaultId}/policies`,
|
|
278
|
+
{
|
|
279
|
+
method: "POST",
|
|
280
|
+
body: JSON.stringify({
|
|
281
|
+
secret_path_pattern: secretPathPattern,
|
|
282
|
+
principal_type: principalType,
|
|
283
|
+
principal_id: principalId,
|
|
284
|
+
permissions,
|
|
285
|
+
}),
|
|
286
|
+
},
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// ── Sharing ──────────────────────────────────────────
|
|
291
|
+
|
|
292
|
+
async shareSecret(
|
|
293
|
+
secretId: string,
|
|
294
|
+
options: {
|
|
295
|
+
recipient_type: string;
|
|
296
|
+
email?: string;
|
|
297
|
+
recipient_id?: string;
|
|
298
|
+
expires_at: string;
|
|
299
|
+
max_access_count?: number;
|
|
300
|
+
},
|
|
301
|
+
): Promise<ShareLinkResponse> {
|
|
302
|
+
return this.request<ShareLinkResponse>(
|
|
303
|
+
`${this.baseUrl}/v1/secrets/${secretId}/share`,
|
|
304
|
+
{ method: "POST", body: JSON.stringify(options) },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ── Transactions ─────────────────────────────────────
|
|
309
|
+
|
|
310
|
+
async simulateTransaction(
|
|
311
|
+
agentId: string,
|
|
312
|
+
tx: {
|
|
313
|
+
to: string;
|
|
314
|
+
value: string;
|
|
315
|
+
chain: string;
|
|
316
|
+
data?: string;
|
|
317
|
+
signing_key_path?: string;
|
|
318
|
+
gas_limit?: number;
|
|
319
|
+
},
|
|
320
|
+
): Promise<SimulationResponse> {
|
|
321
|
+
return this.request<SimulationResponse>(
|
|
322
|
+
`${this.baseUrl}/v1/agents/${agentId}/transactions/simulate`,
|
|
323
|
+
{ method: "POST", body: JSON.stringify(tx) },
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async simulateBundle(
|
|
328
|
+
agentId: string,
|
|
329
|
+
transactions: Array<{
|
|
330
|
+
to: string;
|
|
331
|
+
value: string;
|
|
332
|
+
chain: string;
|
|
333
|
+
data?: string;
|
|
334
|
+
signing_key_path?: string;
|
|
335
|
+
gas_limit?: number;
|
|
336
|
+
}>,
|
|
337
|
+
): Promise<BundleSimulationResponse> {
|
|
338
|
+
return this.request<BundleSimulationResponse>(
|
|
339
|
+
`${this.baseUrl}/v1/agents/${agentId}/transactions/simulate-bundle`,
|
|
340
|
+
{ method: "POST", body: JSON.stringify({ transactions }) },
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
async submitTransaction(
|
|
345
|
+
agentId: string,
|
|
346
|
+
tx: {
|
|
347
|
+
to: string;
|
|
348
|
+
value: string;
|
|
349
|
+
chain: string;
|
|
350
|
+
data?: string;
|
|
351
|
+
signing_key_path?: string;
|
|
352
|
+
nonce?: number;
|
|
353
|
+
gas_price?: string;
|
|
354
|
+
gas_limit?: number;
|
|
355
|
+
max_fee_per_gas?: string;
|
|
356
|
+
max_priority_fee_per_gas?: string;
|
|
357
|
+
simulate_first?: boolean;
|
|
358
|
+
},
|
|
359
|
+
idempotencyKey?: string,
|
|
360
|
+
): Promise<TransactionResponse> {
|
|
361
|
+
const key = idempotencyKey ?? crypto.randomUUID();
|
|
362
|
+
return this.request<TransactionResponse>(
|
|
363
|
+
`${this.baseUrl}/v1/agents/${agentId}/transactions`,
|
|
364
|
+
{
|
|
365
|
+
method: "POST",
|
|
366
|
+
body: JSON.stringify(tx),
|
|
367
|
+
headers: { "Idempotency-Key": key },
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// ── Agent profile ────────────────────────────────────
|
|
373
|
+
|
|
374
|
+
async getAgentProfile(): Promise<AgentProfile> {
|
|
375
|
+
return this.request<AgentProfile>(
|
|
376
|
+
`${this.baseUrl}/v1/agents/me`,
|
|
377
|
+
);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
4
|
+
import { statusCommand } from "./status.js";
|
|
5
|
+
import { listCommand } from "./list.js";
|
|
6
|
+
import { rotateCommand } from "./rotate.js";
|
|
7
|
+
|
|
8
|
+
export function registerAllCommands(
|
|
9
|
+
api: PluginApi,
|
|
10
|
+
client: OneClawClient,
|
|
11
|
+
config: ResolvedConfig,
|
|
12
|
+
): void {
|
|
13
|
+
api.registerCommand(statusCommand(client, config));
|
|
14
|
+
api.registerCommand(listCommand(client));
|
|
15
|
+
api.registerCommand(rotateCommand(client));
|
|
16
|
+
|
|
17
|
+
api.logger.info("[1claw] Registered 3 slash commands (/oneclaw, /oneclaw-list, /oneclaw-rotate)");
|
|
18
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
|
|
3
|
+
export function listCommand(client: OneClawClient) {
|
|
4
|
+
return {
|
|
5
|
+
name: "oneclaw-list",
|
|
6
|
+
description: "List secret paths in the vault (metadata only, no values)",
|
|
7
|
+
acceptsArgs: true,
|
|
8
|
+
handler: async (ctx: { args?: string }) => {
|
|
9
|
+
try {
|
|
10
|
+
const data = await client.listSecrets();
|
|
11
|
+
let secrets = data.secrets;
|
|
12
|
+
const prefix = ctx.args?.trim();
|
|
13
|
+
|
|
14
|
+
if (prefix) {
|
|
15
|
+
secrets = secrets.filter((s) => s.path.startsWith(prefix));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (secrets.length === 0) {
|
|
19
|
+
return { text: prefix ? `No secrets matching prefix '${prefix}'.` : "No secrets in vault." };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const lines = secrets.map(
|
|
23
|
+
(s) => `${s.path} (${s.type}, v${s.version}${s.expires_at ? `, expires ${s.expires_at}` : ""})`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return { text: `${secrets.length} secret(s):\n${lines.join("\n")}` };
|
|
27
|
+
} catch (err) {
|
|
28
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
29
|
+
return { text: `Error listing secrets: ${msg}` };
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
|
|
3
|
+
export function rotateCommand(client: OneClawClient) {
|
|
4
|
+
return {
|
|
5
|
+
name: "oneclaw-rotate",
|
|
6
|
+
description: "Rotate a secret to a new value: /oneclaw-rotate <path> <new-value>",
|
|
7
|
+
acceptsArgs: true,
|
|
8
|
+
requireAuth: true,
|
|
9
|
+
handler: async (ctx: { args?: string }) => {
|
|
10
|
+
const args = ctx.args?.trim();
|
|
11
|
+
if (!args) {
|
|
12
|
+
return { text: "Usage: /oneclaw-rotate <path> <new-value>" };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const spaceIdx = args.indexOf(" ");
|
|
16
|
+
if (spaceIdx === -1) {
|
|
17
|
+
return { text: "Usage: /oneclaw-rotate <path> <new-value>\nBoth path and new value are required." };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const path = args.slice(0, spaceIdx);
|
|
21
|
+
const newValue = args.slice(spaceIdx + 1).trim();
|
|
22
|
+
|
|
23
|
+
if (!newValue) {
|
|
24
|
+
return { text: "Usage: /oneclaw-rotate <path> <new-value>\nNew value cannot be empty." };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await client.putSecret(path, {
|
|
29
|
+
value: newValue,
|
|
30
|
+
type: "api_key",
|
|
31
|
+
});
|
|
32
|
+
return { text: `Rotated '${path}' to version ${result.version}.` };
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
35
|
+
return { text: `Error rotating '${path}': ${msg}` };
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { ResolvedConfig } from "../config.js";
|
|
3
|
+
|
|
4
|
+
export function statusCommand(client: OneClawClient, config: ResolvedConfig) {
|
|
5
|
+
return {
|
|
6
|
+
name: "oneclaw",
|
|
7
|
+
description: "Show 1claw connection status, vault info, token TTL, and enabled features",
|
|
8
|
+
handler: async () => {
|
|
9
|
+
const lines: string[] = ["1Claw Status", "─".repeat(30)];
|
|
10
|
+
|
|
11
|
+
lines.push(`API: ${config.baseUrl}`);
|
|
12
|
+
lines.push(`Shroud: ${config.shroudUrl}`);
|
|
13
|
+
lines.push(`Authenticated: ${client.isAuthenticated ? "yes" : "no"}`);
|
|
14
|
+
|
|
15
|
+
if (client.agentId) {
|
|
16
|
+
lines.push(`Agent ID: ${client.agentId}`);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
if (client.vaultId) {
|
|
20
|
+
lines.push(`Vault ID: ${client.vaultId}`);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const ttlMin = Math.round(client.tokenTtlMs / 60_000);
|
|
24
|
+
lines.push(`Token TTL: ${ttlMin > 0 ? `${ttlMin} min` : "expired or not set"}`);
|
|
25
|
+
|
|
26
|
+
lines.push("", "Features:");
|
|
27
|
+
const features = config.features;
|
|
28
|
+
lines.push(` Tools: ${features.tools ? "on" : "off"}`);
|
|
29
|
+
lines.push(` Secret injection: ${features.secretInjection ? "on" : "off"}`);
|
|
30
|
+
lines.push(` Secret redaction: ${features.secretRedaction ? "on" : "off"}`);
|
|
31
|
+
lines.push(` Shroud routing: ${features.shroudRouting ? "on" : "off"}`);
|
|
32
|
+
lines.push(` Key rotation monitor: ${features.keyRotationMonitor ? "on" : "off"}`);
|
|
33
|
+
lines.push(` Slash commands: ${features.slashCommands ? "on" : "off"}`);
|
|
34
|
+
lines.push(` Security mode: ${config.securityMode}`);
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const vaults = await client.listVaults();
|
|
38
|
+
lines.push("", `Vaults accessible: ${vaults.vaults.length}`);
|
|
39
|
+
} catch {
|
|
40
|
+
lines.push("", "Vaults: unable to list (check credentials)");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { text: lines.join("\n") };
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export interface ResolvedFeatures {
|
|
2
|
+
tools: boolean;
|
|
3
|
+
secretInjection: boolean;
|
|
4
|
+
secretRedaction: boolean;
|
|
5
|
+
shroudRouting: boolean;
|
|
6
|
+
keyRotationMonitor: boolean;
|
|
7
|
+
slashCommands: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ResolvedConfig {
|
|
11
|
+
apiKey: string | undefined;
|
|
12
|
+
agentId: string | undefined;
|
|
13
|
+
vaultId: string | undefined;
|
|
14
|
+
baseUrl: string;
|
|
15
|
+
shroudUrl: string;
|
|
16
|
+
features: ResolvedFeatures;
|
|
17
|
+
securityMode: "block" | "surgical" | "log_only";
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
interface RawPluginConfig {
|
|
21
|
+
apiKey?: string;
|
|
22
|
+
agentId?: string;
|
|
23
|
+
vaultId?: string;
|
|
24
|
+
baseUrl?: string;
|
|
25
|
+
shroudUrl?: string;
|
|
26
|
+
features?: Partial<ResolvedFeatures>;
|
|
27
|
+
securityMode?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const DEFAULT_FEATURES: ResolvedFeatures = {
|
|
31
|
+
tools: true,
|
|
32
|
+
secretInjection: false,
|
|
33
|
+
secretRedaction: true,
|
|
34
|
+
shroudRouting: false,
|
|
35
|
+
keyRotationMonitor: false,
|
|
36
|
+
slashCommands: true,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
function resolveSecurityMode(
|
|
40
|
+
raw: string | undefined,
|
|
41
|
+
): "block" | "surgical" | "log_only" {
|
|
42
|
+
if (raw === "surgical" || raw === "log_only") return raw;
|
|
43
|
+
return "block";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function resolveConfig(pluginConfig?: RawPluginConfig): ResolvedConfig {
|
|
47
|
+
const raw = pluginConfig ?? {};
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
apiKey:
|
|
51
|
+
raw.apiKey ??
|
|
52
|
+
process.env.ONECLAW_AGENT_API_KEY ??
|
|
53
|
+
undefined,
|
|
54
|
+
agentId:
|
|
55
|
+
raw.agentId ??
|
|
56
|
+
process.env.ONECLAW_AGENT_ID ??
|
|
57
|
+
undefined,
|
|
58
|
+
vaultId:
|
|
59
|
+
raw.vaultId ??
|
|
60
|
+
process.env.ONECLAW_VAULT_ID ??
|
|
61
|
+
undefined,
|
|
62
|
+
baseUrl:
|
|
63
|
+
raw.baseUrl ??
|
|
64
|
+
process.env.ONECLAW_BASE_URL ??
|
|
65
|
+
"https://api.1claw.xyz",
|
|
66
|
+
shroudUrl:
|
|
67
|
+
raw.shroudUrl ??
|
|
68
|
+
process.env.ONECLAW_SHROUD_URL ??
|
|
69
|
+
"https://shroud.1claw.xyz",
|
|
70
|
+
features: {
|
|
71
|
+
...DEFAULT_FEATURES,
|
|
72
|
+
...(raw.features ?? {}),
|
|
73
|
+
},
|
|
74
|
+
securityMode: resolveSecurityMode(
|
|
75
|
+
raw.securityMode ??
|
|
76
|
+
process.env.ONECLAW_MCP_SANITIZATION_MODE,
|
|
77
|
+
),
|
|
78
|
+
};
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
4
|
+
import { registerSecretRedaction } from "./secret-redaction.js";
|
|
5
|
+
import { registerSecretInjection } from "./secret-injection.js";
|
|
6
|
+
import { registerShroudRouting } from "./shroud-routing.js";
|
|
7
|
+
|
|
8
|
+
export function registerAllHooks(
|
|
9
|
+
api: PluginApi,
|
|
10
|
+
client: OneClawClient,
|
|
11
|
+
config: ResolvedConfig,
|
|
12
|
+
): void {
|
|
13
|
+
if (config.features.secretRedaction) {
|
|
14
|
+
registerSecretRedaction(api, client);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (config.features.secretInjection) {
|
|
18
|
+
registerSecretInjection(api, client);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (config.features.shroudRouting) {
|
|
22
|
+
registerShroudRouting(api, client, config.shroudUrl);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const PLACEHOLDER_RE = /\{\{1claw:([^}]+)\}\}/g;
|
|
5
|
+
|
|
6
|
+
interface PromptBuildEvent {
|
|
7
|
+
messages?: Array<{ role: string; content: string }>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function registerSecretInjection(api: PluginApi, client: OneClawClient): void {
|
|
11
|
+
api.on(
|
|
12
|
+
"before_prompt_build",
|
|
13
|
+
async (event: unknown, _ctx: unknown) => {
|
|
14
|
+
const ev = event as PromptBuildEvent;
|
|
15
|
+
if (!ev.messages || ev.messages.length === 0) return {};
|
|
16
|
+
|
|
17
|
+
let injected = 0;
|
|
18
|
+
|
|
19
|
+
for (const msg of ev.messages) {
|
|
20
|
+
if (!msg.content) continue;
|
|
21
|
+
|
|
22
|
+
const matches = [...msg.content.matchAll(PLACEHOLDER_RE)];
|
|
23
|
+
if (matches.length === 0) continue;
|
|
24
|
+
|
|
25
|
+
for (const match of matches) {
|
|
26
|
+
const fullPlaceholder = match[0];
|
|
27
|
+
const secretPath = match[1];
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const secret = await client.getSecret(secretPath);
|
|
31
|
+
msg.content = msg.content.replace(fullPlaceholder, secret.value);
|
|
32
|
+
injected++;
|
|
33
|
+
} catch (err) {
|
|
34
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
35
|
+
api.logger.warn(
|
|
36
|
+
`[1claw/injection] Failed to resolve ${secretPath}: ${errMsg}`,
|
|
37
|
+
);
|
|
38
|
+
msg.content = msg.content.replace(
|
|
39
|
+
fullPlaceholder,
|
|
40
|
+
`[1CLAW_UNAVAILABLE:${secretPath}]`,
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (injected > 0) {
|
|
47
|
+
api.logger.info(
|
|
48
|
+
`[1claw/injection] Injected ${injected} secret(s) into conversation`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return {};
|
|
53
|
+
},
|
|
54
|
+
{ priority: 90 },
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
api.logger.info("[1claw] Secret injection hook registered");
|
|
58
|
+
}
|