@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
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import { OneClawApiError } from "../client.js";
|
|
4
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export function getSecretTool(client: OneClawClient): PluginTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "oneclaw_get_secret",
|
|
9
|
+
description:
|
|
10
|
+
"Fetch the decrypted value of a secret from the 1claw vault by its path. Use this immediately before making an API call that requires the credential. Do not store the value or include it in summaries.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
path: Type.String({
|
|
13
|
+
minLength: 1,
|
|
14
|
+
description: "Secret path, e.g. 'api-keys/stripe'",
|
|
15
|
+
}),
|
|
16
|
+
}),
|
|
17
|
+
optional: true,
|
|
18
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
19
|
+
const path = params.path as string;
|
|
20
|
+
try {
|
|
21
|
+
const secret = await client.getSecret(path);
|
|
22
|
+
return {
|
|
23
|
+
content: [{
|
|
24
|
+
type: "text",
|
|
25
|
+
text: JSON.stringify({
|
|
26
|
+
path: secret.path,
|
|
27
|
+
type: secret.type,
|
|
28
|
+
version: secret.version,
|
|
29
|
+
value: secret.value,
|
|
30
|
+
}),
|
|
31
|
+
}],
|
|
32
|
+
};
|
|
33
|
+
} catch (err) {
|
|
34
|
+
if (err instanceof OneClawApiError) {
|
|
35
|
+
if (err.status === 410) {
|
|
36
|
+
return { content: [{ type: "text", text: `Error: Secret at path '${path}' is expired or has exceeded its maximum access count.` }] };
|
|
37
|
+
}
|
|
38
|
+
if (err.status === 404) {
|
|
39
|
+
return { content: [{ type: "text", text: `Error: No secret found at path '${path}'.` }] };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw err;
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function grantAccessTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_grant_access",
|
|
8
|
+
description:
|
|
9
|
+
"Grant a user or agent access to one of your vaults. You can only grant access on vaults you created.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
vault_id: Type.String({ description: "UUID of the vault to share" }),
|
|
12
|
+
principal_type: Type.Union([Type.Literal("user"), Type.Literal("agent")], {
|
|
13
|
+
description: "Type of principal",
|
|
14
|
+
}),
|
|
15
|
+
principal_id: Type.String({ description: "UUID of the user or agent" }),
|
|
16
|
+
permissions: Type.Optional(
|
|
17
|
+
Type.Array(Type.String(), { description: "Permissions to grant (default: ['read'])" }),
|
|
18
|
+
),
|
|
19
|
+
secret_path_pattern: Type.Optional(
|
|
20
|
+
Type.String({ description: "Glob pattern for secret paths (default: '**')" }),
|
|
21
|
+
),
|
|
22
|
+
}),
|
|
23
|
+
optional: true,
|
|
24
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
25
|
+
const permissions = (params.permissions as string[]) ?? ["read"];
|
|
26
|
+
const pathPattern = (params.secret_path_pattern as string) ?? "**";
|
|
27
|
+
|
|
28
|
+
const policy = await client.createPolicy(
|
|
29
|
+
params.vault_id as string,
|
|
30
|
+
params.principal_type as string,
|
|
31
|
+
params.principal_id as string,
|
|
32
|
+
permissions,
|
|
33
|
+
pathPattern,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
content: [{
|
|
38
|
+
type: "text",
|
|
39
|
+
text:
|
|
40
|
+
`Access granted.\n` +
|
|
41
|
+
` Policy ID: ${policy.id}\n` +
|
|
42
|
+
` Vault: ${policy.vault_id}\n` +
|
|
43
|
+
` Granted to: ${policy.principal_type}:${policy.principal_id}\n` +
|
|
44
|
+
` Permissions: ${policy.permissions.join(", ")}\n` +
|
|
45
|
+
` Path pattern: ${policy.secret_path_pattern}`,
|
|
46
|
+
}],
|
|
47
|
+
};
|
|
48
|
+
},
|
|
49
|
+
};
|
|
50
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi, PluginTool, ToolResult } from "../types.js";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
4
|
+
import { inspectInput, inspectOutput } from "../security/index.js";
|
|
5
|
+
|
|
6
|
+
import { listSecretsTool } from "./list-secrets.js";
|
|
7
|
+
import { getSecretTool } from "./get-secret.js";
|
|
8
|
+
import { putSecretTool } from "./put-secret.js";
|
|
9
|
+
import { deleteSecretTool } from "./delete-secret.js";
|
|
10
|
+
import { describeSecretTool } from "./describe-secret.js";
|
|
11
|
+
import { rotateAndStoreTool } from "./rotate-and-store.js";
|
|
12
|
+
import { getEnvBundleTool } from "./get-env-bundle.js";
|
|
13
|
+
import { createVaultTool } from "./create-vault.js";
|
|
14
|
+
import { listVaultsTool } from "./list-vaults.js";
|
|
15
|
+
import { grantAccessTool } from "./grant-access.js";
|
|
16
|
+
import { shareSecretTool } from "./share-secret.js";
|
|
17
|
+
import { simulateTransactionTool } from "./simulate-transaction.js";
|
|
18
|
+
import { submitTransactionTool } from "./submit-transaction.js";
|
|
19
|
+
|
|
20
|
+
type ToolFactory = (client: OneClawClient) => PluginTool;
|
|
21
|
+
|
|
22
|
+
const ALL_TOOL_FACTORIES: ToolFactory[] = [
|
|
23
|
+
listSecretsTool,
|
|
24
|
+
getSecretTool,
|
|
25
|
+
putSecretTool,
|
|
26
|
+
deleteSecretTool,
|
|
27
|
+
describeSecretTool,
|
|
28
|
+
rotateAndStoreTool,
|
|
29
|
+
getEnvBundleTool,
|
|
30
|
+
createVaultTool,
|
|
31
|
+
listVaultsTool,
|
|
32
|
+
grantAccessTool,
|
|
33
|
+
shareSecretTool,
|
|
34
|
+
simulateTransactionTool,
|
|
35
|
+
submitTransactionTool,
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
function wrapWithSecurity(
|
|
39
|
+
tool: PluginTool,
|
|
40
|
+
api: PluginApi,
|
|
41
|
+
config: ResolvedConfig,
|
|
42
|
+
): PluginTool {
|
|
43
|
+
const originalExecute = tool.execute;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
...tool,
|
|
47
|
+
execute: async (id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
48
|
+
const inputCheck = inspectInput(tool.name, params, config.securityMode);
|
|
49
|
+
|
|
50
|
+
if (!inputCheck.passed) {
|
|
51
|
+
const threat = inputCheck.threats[0];
|
|
52
|
+
api.logger.warn(
|
|
53
|
+
`[1claw/security] Blocked ${tool.name}: ${threat?.type} (${threat?.pattern})`,
|
|
54
|
+
);
|
|
55
|
+
return {
|
|
56
|
+
content: [{ type: "text", text: `Security check failed: ${threat?.type} detected` }],
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (inputCheck.threats.length > 0) {
|
|
61
|
+
api.logger.info(
|
|
62
|
+
`[1claw/security] Warnings for ${tool.name}: ${inputCheck.threats.map((t) => t.pattern).join(", ")}`,
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const result = await originalExecute(id, params);
|
|
67
|
+
|
|
68
|
+
const outputText = result.content.map((c) => c.text).join("\n");
|
|
69
|
+
const outputCheck = inspectOutput(tool.name, outputText);
|
|
70
|
+
if (outputCheck.threats.length > 0) {
|
|
71
|
+
api.logger.info(
|
|
72
|
+
`[1claw/security] Output warnings for ${tool.name}: ${outputCheck.threats.map((t) => t.pattern).join(", ")}`,
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return result;
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function registerAllTools(
|
|
82
|
+
api: PluginApi,
|
|
83
|
+
client: OneClawClient,
|
|
84
|
+
config: ResolvedConfig,
|
|
85
|
+
): void {
|
|
86
|
+
for (const factory of ALL_TOOL_FACTORIES) {
|
|
87
|
+
const tool = factory(client);
|
|
88
|
+
const secured = wrapWithSecurity(tool, api, config);
|
|
89
|
+
api.registerTool(secured);
|
|
90
|
+
}
|
|
91
|
+
api.logger.info(`[1claw] Registered ${ALL_TOOL_FACTORIES.length} agent tools`);
|
|
92
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function listSecretsTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_list_secrets",
|
|
8
|
+
description:
|
|
9
|
+
"List all secrets stored in the 1claw vault. Returns paths, types, versions, and metadata — never secret values. Use this to discover what credentials are available before fetching one.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
prefix: Type.Optional(
|
|
12
|
+
Type.String({ description: "Path prefix to filter secrets (e.g. 'api-keys/')" }),
|
|
13
|
+
),
|
|
14
|
+
}),
|
|
15
|
+
optional: true,
|
|
16
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
17
|
+
const data = await client.listSecrets();
|
|
18
|
+
let secrets = data.secrets;
|
|
19
|
+
const prefix = params.prefix as string | undefined;
|
|
20
|
+
|
|
21
|
+
if (prefix) {
|
|
22
|
+
secrets = secrets.filter((s) => s.path.startsWith(prefix));
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (secrets.length === 0) {
|
|
26
|
+
return { content: [{ type: "text", text: "No secrets found in this vault." }] };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const lines = secrets.map(
|
|
30
|
+
(s) =>
|
|
31
|
+
`- ${s.path} (type: ${s.type}, version: ${s.version}, expires: ${s.expires_at ?? "never"})`,
|
|
32
|
+
);
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
content: [{ type: "text", text: `Found ${secrets.length} secret(s):\n${lines.join("\n")}` }],
|
|
36
|
+
};
|
|
37
|
+
},
|
|
38
|
+
};
|
|
39
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function listVaultsTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_list_vaults",
|
|
8
|
+
description:
|
|
9
|
+
"List all vaults accessible to you. Returns vault IDs, names, and creators. Use this to discover available vaults.",
|
|
10
|
+
parameters: Type.Object({}),
|
|
11
|
+
optional: true,
|
|
12
|
+
execute: async (): Promise<ToolResult> => {
|
|
13
|
+
const data = await client.listVaults();
|
|
14
|
+
const vaults = data.vaults;
|
|
15
|
+
|
|
16
|
+
if (vaults.length === 0) {
|
|
17
|
+
return { content: [{ type: "text", text: "No vaults found. Create one with oneclaw_create_vault." }] };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const lines = vaults.map(
|
|
21
|
+
(v) => `- ${v.name} (id: ${v.id}, created by: ${v.created_by_type}, ${v.created_at})`,
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
content: [{ type: "text", text: `Found ${vaults.length} vault(s):\n${lines.join("\n")}` }],
|
|
26
|
+
};
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function putSecretTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_put_secret",
|
|
8
|
+
description:
|
|
9
|
+
"Store a new secret or update an existing one in the 1claw vault. Each call creates a new version. Supports optional expiry and max access count.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
path: Type.String({ minLength: 1, description: "Secret path, e.g. 'api-keys/stripe'" }),
|
|
12
|
+
value: Type.String({ minLength: 1, description: "The secret value to store" }),
|
|
13
|
+
type: Type.Optional(
|
|
14
|
+
Type.Union([
|
|
15
|
+
Type.Literal("api_key"),
|
|
16
|
+
Type.Literal("password"),
|
|
17
|
+
Type.Literal("private_key"),
|
|
18
|
+
Type.Literal("certificate"),
|
|
19
|
+
Type.Literal("file"),
|
|
20
|
+
Type.Literal("note"),
|
|
21
|
+
Type.Literal("ssh_key"),
|
|
22
|
+
Type.Literal("env_bundle"),
|
|
23
|
+
], { description: "Secret type (default: api_key)" }),
|
|
24
|
+
),
|
|
25
|
+
metadata: Type.Optional(Type.Record(Type.String(), Type.Unknown(), { description: "Optional JSON metadata" })),
|
|
26
|
+
expires_at: Type.Optional(Type.String({ description: "Optional ISO 8601 expiry datetime" })),
|
|
27
|
+
max_access_count: Type.Optional(Type.Integer({ minimum: 1, description: "Max reads before auto-expiry" })),
|
|
28
|
+
}),
|
|
29
|
+
optional: true,
|
|
30
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
31
|
+
const result = await client.putSecret(params.path as string, {
|
|
32
|
+
value: params.value as string,
|
|
33
|
+
type: (params.type as string) ?? "api_key",
|
|
34
|
+
metadata: params.metadata as Record<string, unknown> | undefined,
|
|
35
|
+
expires_at: params.expires_at as string | undefined,
|
|
36
|
+
max_access_count: params.max_access_count as number | undefined,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
content: [{ type: "text", text: `Secret stored at '${params.path}' (version ${result.version}, type: ${result.type}).` }],
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function rotateAndStoreTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_rotate_and_store",
|
|
8
|
+
description:
|
|
9
|
+
"Store a new value for an existing secret (creating a new version). Use after regenerating an API key at a provider.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
path: Type.String({ minLength: 1, description: "Secret path to rotate" }),
|
|
12
|
+
value: Type.String({ minLength: 1, description: "The new secret value" }),
|
|
13
|
+
}),
|
|
14
|
+
optional: true,
|
|
15
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
16
|
+
const result = await client.putSecret(params.path as string, {
|
|
17
|
+
value: params.value as string,
|
|
18
|
+
type: "api_key",
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
content: [{ type: "text", text: `Rotated secret at '${params.path}'. New version: ${result.version}.` }],
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
4
|
+
|
|
5
|
+
export function shareSecretTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_share_secret",
|
|
8
|
+
description:
|
|
9
|
+
"Share a specific secret with a user, agent, or your creator (the human who registered you). " +
|
|
10
|
+
"Use recipient_type 'creator' to share back with the human — no recipient_id needed.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
secret_id: Type.String({ description: "UUID of the secret to share" }),
|
|
13
|
+
recipient_type: Type.Union([
|
|
14
|
+
Type.Literal("user"),
|
|
15
|
+
Type.Literal("agent"),
|
|
16
|
+
Type.Literal("anyone_with_link"),
|
|
17
|
+
Type.Literal("creator"),
|
|
18
|
+
], { description: "Recipient type" }),
|
|
19
|
+
recipient_id: Type.Optional(Type.String({ description: "UUID of recipient (required for user/agent)" })),
|
|
20
|
+
expires_at: Type.String({ description: "ISO-8601 expiry date" }),
|
|
21
|
+
max_access_count: Type.Optional(Type.Integer({ minimum: 1, description: "Max accesses (default: 5)" })),
|
|
22
|
+
}),
|
|
23
|
+
optional: true,
|
|
24
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
25
|
+
const recipientType = params.recipient_type as string;
|
|
26
|
+
const recipientId = params.recipient_id as string | undefined;
|
|
27
|
+
|
|
28
|
+
if ((recipientType === "user" || recipientType === "agent") && !recipientId) {
|
|
29
|
+
return {
|
|
30
|
+
content: [{ type: "text", text: `Error: recipient_id is required when sharing with a ${recipientType}.` }],
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const share = await client.shareSecret(params.secret_id as string, {
|
|
35
|
+
recipient_type: recipientType,
|
|
36
|
+
recipient_id: recipientId,
|
|
37
|
+
expires_at: params.expires_at as string,
|
|
38
|
+
max_access_count: (params.max_access_count as number) ?? 5,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
const recipientLabel =
|
|
42
|
+
recipientType === "creator"
|
|
43
|
+
? "your creator (the human who registered this agent)"
|
|
44
|
+
: `${recipientType}${recipientId ? ` (${recipientId})` : ""}`;
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
content: [{
|
|
48
|
+
type: "text",
|
|
49
|
+
text:
|
|
50
|
+
`Secret shared successfully.\n` +
|
|
51
|
+
` Share ID: ${share.id}\n` +
|
|
52
|
+
` Recipient: ${recipientLabel}\n` +
|
|
53
|
+
` Expires: ${share.expires_at}\n` +
|
|
54
|
+
` Max accesses: ${share.max_access_count}\n` +
|
|
55
|
+
` URL: ${share.share_url}\n\n` +
|
|
56
|
+
`The recipient must accept the share before they can access the secret.`,
|
|
57
|
+
}],
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import { OneClawApiError } from "../client.js";
|
|
4
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export function simulateTransactionTool(client: OneClawClient): PluginTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "oneclaw_simulate_transaction",
|
|
9
|
+
description:
|
|
10
|
+
"Simulate an EVM transaction via Tenderly without signing or broadcasting. Returns balance changes, gas estimates, and success/revert status.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
to: Type.String({ description: "Destination address (0x-prefixed)" }),
|
|
13
|
+
value: Type.String({ description: "Value in ETH (e.g. '0.01')" }),
|
|
14
|
+
chain: Type.String({ description: "Chain name or numeric chain ID" }),
|
|
15
|
+
data: Type.Optional(Type.String({ description: "Hex-encoded calldata" })),
|
|
16
|
+
signing_key_path: Type.Optional(Type.String({ description: "Vault path to signing key" })),
|
|
17
|
+
gas_limit: Type.Optional(Type.Integer({ description: "Gas limit (default: 21000)" })),
|
|
18
|
+
}),
|
|
19
|
+
optional: true,
|
|
20
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
21
|
+
const agentId = client.agentId;
|
|
22
|
+
if (!agentId) {
|
|
23
|
+
return { content: [{ type: "text", text: "Error: simulate_transaction requires agent authentication." }] };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const result = await client.simulateTransaction(agentId, {
|
|
28
|
+
to: params.to as string,
|
|
29
|
+
value: params.value as string,
|
|
30
|
+
chain: params.chain as string,
|
|
31
|
+
data: params.data as string | undefined,
|
|
32
|
+
signing_key_path: params.signing_key_path as string | undefined,
|
|
33
|
+
gas_limit: params.gas_limit as number | undefined,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const lines: string[] = [
|
|
37
|
+
`Simulation ${result.status.toUpperCase()}`,
|
|
38
|
+
`Gas used: ${result.gas_used}`,
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
if (result.gas_estimate_usd) lines.push(`Gas estimate: ${result.gas_estimate_usd}`);
|
|
42
|
+
|
|
43
|
+
if (result.balance_changes.length > 0) {
|
|
44
|
+
lines.push("", "Balance changes:");
|
|
45
|
+
for (const bc of result.balance_changes) {
|
|
46
|
+
const token = bc.token_symbol ?? bc.token ?? "ETH";
|
|
47
|
+
lines.push(` ${bc.address}: ${bc.change ?? "?"} ${token}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (result.error) lines.push("", `Error: ${result.error}`);
|
|
52
|
+
if (result.error_human_readable) lines.push(`Reason: ${result.error_human_readable}`);
|
|
53
|
+
if (result.tenderly_dashboard_url) lines.push("", `Tenderly: ${result.tenderly_dashboard_url}`);
|
|
54
|
+
|
|
55
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
if (err instanceof OneClawApiError) {
|
|
58
|
+
if (err.status === 400 || err.status === 403 || err.status === 422) {
|
|
59
|
+
return { content: [{ type: "text", text: `Error: ${err.detail}` }] };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox";
|
|
2
|
+
import type { OneClawClient } from "../client.js";
|
|
3
|
+
import { OneClawApiError } from "../client.js";
|
|
4
|
+
import type { PluginTool, ToolResult } from "../types.js";
|
|
5
|
+
|
|
6
|
+
export function submitTransactionTool(client: OneClawClient): PluginTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "oneclaw_submit_transaction",
|
|
9
|
+
description:
|
|
10
|
+
"Submit an EVM transaction to be signed by 1claw's Intents API and optionally broadcast. Supports legacy and EIP-1559 fee modes.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
to: Type.String({ description: "Destination address (0x-prefixed)" }),
|
|
13
|
+
value: Type.String({ description: "Value in ETH (e.g. '0.01')" }),
|
|
14
|
+
chain: Type.String({ description: "Chain name or numeric chain ID" }),
|
|
15
|
+
data: Type.Optional(Type.String({ description: "Hex-encoded calldata" })),
|
|
16
|
+
signing_key_path: Type.Optional(Type.String({ description: "Vault path to signing key" })),
|
|
17
|
+
nonce: Type.Optional(Type.Integer({ description: "Transaction nonce (auto-resolved if omitted)" })),
|
|
18
|
+
gas_price: Type.Optional(Type.String({ description: "Gas price in wei (legacy mode)" })),
|
|
19
|
+
gas_limit: Type.Optional(Type.Integer({ description: "Gas limit (default: 21000)" })),
|
|
20
|
+
max_fee_per_gas: Type.Optional(Type.String({ description: "EIP-1559 max fee per gas in wei" })),
|
|
21
|
+
max_priority_fee_per_gas: Type.Optional(Type.String({ description: "EIP-1559 max priority fee in wei" })),
|
|
22
|
+
simulate_first: Type.Optional(Type.Boolean({ description: "Run simulation before signing (default: true)" })),
|
|
23
|
+
}),
|
|
24
|
+
optional: true,
|
|
25
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
26
|
+
const agentId = client.agentId;
|
|
27
|
+
if (!agentId) {
|
|
28
|
+
return { content: [{ type: "text", text: "Error: submit_transaction requires agent authentication." }] };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await client.submitTransaction(agentId, {
|
|
33
|
+
to: params.to as string,
|
|
34
|
+
value: params.value as string,
|
|
35
|
+
chain: params.chain as string,
|
|
36
|
+
data: params.data as string | undefined,
|
|
37
|
+
signing_key_path: params.signing_key_path as string | undefined,
|
|
38
|
+
nonce: params.nonce as number | undefined,
|
|
39
|
+
gas_price: params.gas_price as string | undefined,
|
|
40
|
+
gas_limit: params.gas_limit as number | undefined,
|
|
41
|
+
max_fee_per_gas: params.max_fee_per_gas as string | undefined,
|
|
42
|
+
max_priority_fee_per_gas: params.max_priority_fee_per_gas as string | undefined,
|
|
43
|
+
simulate_first: (params.simulate_first as boolean) ?? true,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
const lines: string[] = [
|
|
47
|
+
`Transaction ${result.status.toUpperCase()}`,
|
|
48
|
+
`ID: ${result.id}`,
|
|
49
|
+
`Chain: ${result.chain} (${result.chain_id})`,
|
|
50
|
+
`To: ${result.to}`,
|
|
51
|
+
`Value: ${result.value_wei} wei`,
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
if (result.tx_hash) lines.push(`Tx hash: ${result.tx_hash}`);
|
|
55
|
+
if (result.simulation_id) lines.push(`Simulation: ${result.simulation_id} (${result.simulation_status})`);
|
|
56
|
+
if (result.error_message) lines.push(`Error: ${result.error_message}`);
|
|
57
|
+
|
|
58
|
+
return { content: [{ type: "text", text: lines.join("\n") }] };
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err instanceof OneClawApiError) {
|
|
61
|
+
if (err.status === 400 || err.status === 403 || err.status === 422) {
|
|
62
|
+
return { content: [{ type: "text", text: `Error: ${err.detail}` }] };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw err;
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|