@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,90 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
5
|
+
|
|
6
|
+
interface SecretCache {
|
|
7
|
+
values: Map<string, string>;
|
|
8
|
+
refreshedAt: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
let cache: SecretCache | null = null;
|
|
12
|
+
|
|
13
|
+
async function refreshCache(client: OneClawClient): Promise<Map<string, string>> {
|
|
14
|
+
if (cache && Date.now() - cache.refreshedAt < CACHE_TTL_MS) {
|
|
15
|
+
return cache.values;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const values = new Map<string, string>();
|
|
19
|
+
try {
|
|
20
|
+
const listing = await client.listSecrets();
|
|
21
|
+
for (const secret of listing.secrets) {
|
|
22
|
+
try {
|
|
23
|
+
const full = await client.getSecret(secret.path);
|
|
24
|
+
if (full.value && full.value.length >= 8) {
|
|
25
|
+
values.set(secret.path, full.value);
|
|
26
|
+
}
|
|
27
|
+
} catch {
|
|
28
|
+
// Skip secrets we can't read (no policy, expired, etc.)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
} catch {
|
|
32
|
+
// Can't list secrets — return empty cache
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
cache = { values, refreshedAt: Date.now() };
|
|
36
|
+
return values;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function redactSecrets(text: string, secrets: Map<string, string>): { redacted: string; count: number } {
|
|
40
|
+
let redacted = text;
|
|
41
|
+
let count = 0;
|
|
42
|
+
|
|
43
|
+
for (const [path, value] of secrets) {
|
|
44
|
+
if (redacted.includes(value)) {
|
|
45
|
+
redacted = redacted.replaceAll(value, `[REDACTED:${path}]`);
|
|
46
|
+
count++;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return { redacted, count };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface PromptBuildEvent {
|
|
54
|
+
messages?: Array<{ role: string; content: string }>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function registerSecretRedaction(api: PluginApi, client: OneClawClient): void {
|
|
58
|
+
api.on(
|
|
59
|
+
"before_prompt_build",
|
|
60
|
+
async (event: unknown, _ctx: unknown) => {
|
|
61
|
+
const ev = event as PromptBuildEvent;
|
|
62
|
+
if (!ev.messages || ev.messages.length === 0) return {};
|
|
63
|
+
|
|
64
|
+
const secrets = await refreshCache(client);
|
|
65
|
+
if (secrets.size === 0) return {};
|
|
66
|
+
|
|
67
|
+
let totalRedacted = 0;
|
|
68
|
+
|
|
69
|
+
for (const msg of ev.messages) {
|
|
70
|
+
if (!msg.content) continue;
|
|
71
|
+
const { redacted, count } = redactSecrets(msg.content, secrets);
|
|
72
|
+
if (count > 0) {
|
|
73
|
+
msg.content = redacted;
|
|
74
|
+
totalRedacted += count;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (totalRedacted > 0) {
|
|
79
|
+
api.logger.warn(
|
|
80
|
+
`[1claw/redaction] Redacted ${totalRedacted} leaked secret value(s) from conversation`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return {};
|
|
85
|
+
},
|
|
86
|
+
{ priority: 100 },
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
api.logger.info("[1claw] Secret redaction hook registered");
|
|
90
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi, AgentProfile } from "../types.js";
|
|
3
|
+
|
|
4
|
+
let cachedProfile: AgentProfile | null = null;
|
|
5
|
+
let profileFetchedAt = 0;
|
|
6
|
+
const PROFILE_CACHE_TTL_MS = 10 * 60 * 1000; // 10 minutes
|
|
7
|
+
|
|
8
|
+
async function getAgentProfile(client: OneClawClient): Promise<AgentProfile | null> {
|
|
9
|
+
if (cachedProfile && Date.now() - profileFetchedAt < PROFILE_CACHE_TTL_MS) {
|
|
10
|
+
return cachedProfile;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
cachedProfile = await client.getAgentProfile();
|
|
15
|
+
profileFetchedAt = Date.now();
|
|
16
|
+
return cachedProfile;
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function registerShroudRouting(
|
|
23
|
+
api: PluginApi,
|
|
24
|
+
client: OneClawClient,
|
|
25
|
+
shroudUrl: string,
|
|
26
|
+
): void {
|
|
27
|
+
api.on(
|
|
28
|
+
"before_model_resolve",
|
|
29
|
+
async (_event: unknown, _ctx: unknown) => {
|
|
30
|
+
const profile = await getAgentProfile(client);
|
|
31
|
+
if (!profile?.shroud_enabled) return {};
|
|
32
|
+
|
|
33
|
+
api.logger.info(
|
|
34
|
+
`[1claw/shroud] Routing LLM traffic through Shroud at ${shroudUrl}`,
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
providerOverride: shroudUrl,
|
|
39
|
+
};
|
|
40
|
+
},
|
|
41
|
+
{ priority: 50 },
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
api.logger.info("[1claw] Shroud routing hook registered");
|
|
45
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { OneClawClient } from "./client.js";
|
|
2
|
+
import { resolveConfig } from "./config.js";
|
|
3
|
+
import { registerAllTools } from "./tools/index.js";
|
|
4
|
+
import { registerAllHooks } from "./hooks/index.js";
|
|
5
|
+
import { registerAllServices } from "./services/index.js";
|
|
6
|
+
import { registerAllCommands } from "./commands/index.js";
|
|
7
|
+
import type { PluginApi } from "./types.js";
|
|
8
|
+
|
|
9
|
+
interface FullConfig {
|
|
10
|
+
plugins?: {
|
|
11
|
+
entries?: Record<string, { enabled?: boolean; config?: Record<string, unknown> }>;
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function extractPluginConfig(api: PluginApi): Record<string, unknown> | undefined {
|
|
16
|
+
const full = api.config as FullConfig;
|
|
17
|
+
return full?.plugins?.entries?.["1claw"]?.config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function register(api: PluginApi): void {
|
|
21
|
+
const rawConfig = extractPluginConfig(api);
|
|
22
|
+
const config = resolveConfig(rawConfig as Parameters<typeof resolveConfig>[0]);
|
|
23
|
+
|
|
24
|
+
if (!config.apiKey) {
|
|
25
|
+
api.logger.warn(
|
|
26
|
+
"[1claw] No API key configured. Set plugins.entries.1claw.config.apiKey or ONECLAW_AGENT_API_KEY env var.",
|
|
27
|
+
);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const client = new OneClawClient({
|
|
32
|
+
baseUrl: config.baseUrl,
|
|
33
|
+
apiKey: config.apiKey,
|
|
34
|
+
agentId: config.agentId,
|
|
35
|
+
vaultId: config.vaultId,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
api.logger.info(
|
|
39
|
+
`[1claw] Initializing plugin (base: ${config.baseUrl}, agent: ${config.agentId ?? "auto-resolve"})`,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
if (config.features.tools) {
|
|
43
|
+
registerAllTools(api, client, config);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
registerAllHooks(api, client, config);
|
|
47
|
+
registerAllServices(api, client, config);
|
|
48
|
+
|
|
49
|
+
if (config.features.slashCommands) {
|
|
50
|
+
registerAllCommands(api, client, config);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
api.registerGatewayMethod("1claw.status", async ({ respond }: { respond: (ok: boolean, data: unknown) => void }) => {
|
|
54
|
+
try {
|
|
55
|
+
const vaults = await client.listVaults();
|
|
56
|
+
respond(true, {
|
|
57
|
+
authenticated: client.isAuthenticated,
|
|
58
|
+
agentId: client.agentId ?? null,
|
|
59
|
+
vaultId: client.vaultId || null,
|
|
60
|
+
tokenTtlMs: client.tokenTtlMs,
|
|
61
|
+
vaultCount: vaults.vaults.length,
|
|
62
|
+
features: config.features,
|
|
63
|
+
securityMode: config.securityMode,
|
|
64
|
+
});
|
|
65
|
+
} catch (err) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
respond(false, { error: msg });
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
api.logger.info("[1claw] Plugin initialized successfully");
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export default {
|
|
75
|
+
id: "1claw",
|
|
76
|
+
name: "1Claw Secrets Manager",
|
|
77
|
+
register,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
export { OneClawClient } from "./client.js";
|
|
81
|
+
export { resolveConfig } from "./config.js";
|
|
82
|
+
export type { ResolvedConfig, ResolvedFeatures } from "./config.js";
|
|
83
|
+
export type { PluginApi, PluginTool, ToolResult, CommandContext } from "./types.js";
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
export interface ThreatDetection {
|
|
2
|
+
type: string;
|
|
3
|
+
pattern: string;
|
|
4
|
+
location?: string;
|
|
5
|
+
severity: "low" | "medium" | "high" | "critical";
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export interface InspectionResult {
|
|
9
|
+
passed: boolean;
|
|
10
|
+
threats: ThreatDetection[];
|
|
11
|
+
sanitized?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const COMMAND_INJECTION_PATTERNS = [
|
|
15
|
+
{ name: "shell_chain", pattern: /(?:;|\||&&|\|\|)\s*(?:curl|wget|bash|sh|nc|python|perl|ruby|php|node)\b/i, severity: "critical" as const },
|
|
16
|
+
{ name: "command_substitution", pattern: /\$\([^)]+\)|`[^`]+`/, severity: "critical" as const },
|
|
17
|
+
{ name: "reverse_shell", pattern: /(?:bash\s+-i|nc\s+-[elp]|python\s+-c\s+['"]import\s+(?:socket|os))/i, severity: "critical" as const },
|
|
18
|
+
{ name: "path_traversal", pattern: /(?:\.\.\/){2,}/, severity: "high" as const },
|
|
19
|
+
{ name: "sensitive_paths", pattern: /(?:\/etc\/(?:passwd|shadow|sudoers)|\/proc\/self|~\/.ssh\/|\.env\b)/i, severity: "high" as const },
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
const ENCODING_PATTERNS = [
|
|
23
|
+
{ name: "base64_long", pattern: /(?:[A-Za-z0-9+/]{4}){8,}(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?/, severity: "medium" as const },
|
|
24
|
+
{ name: "hex_escape", pattern: /(?:\\\\x[0-9a-fA-F]{2}){3,}/, severity: "medium" as const },
|
|
25
|
+
{ name: "unicode_escape", pattern: /(?:\\\\u[0-9a-fA-F]{4}){2,}/, severity: "medium" as const },
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
const SOCIAL_ENGINEERING_PATTERNS = [
|
|
29
|
+
{ name: "urgency", pattern: /\b(?:urgent(?:ly)?|immediately|right\s+now|asap|emergency)\b/i, severity: "medium" as const },
|
|
30
|
+
{ name: "authority", pattern: /\b(?:i\s+am\s+(?:an?\s+)?(?:admin|administrator|manager|root|superuser))/i, severity: "high" as const },
|
|
31
|
+
{ name: "secrecy", pattern: /\b(?:don't\s+tell\s+(?:anyone|anybody)|keep\s+(?:this\s+)?secret)\b/i, severity: "high" as const },
|
|
32
|
+
{ name: "bypass", pattern: /\b(?:skip\s+(?:the\s+)?(?:verification|authentication|security)|bypass\s+(?:the\s+)?(?:check|security))\b/i, severity: "critical" as const },
|
|
33
|
+
{ name: "credential_request", pattern: /\b(?:(?:what\s+is|tell\s+me|give\s+me)\s+(?:your|the)\s+(?:password|api\s+key|secret|credentials?|token))\b/i, severity: "critical" as const },
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
const NETWORK_PATTERNS = [
|
|
37
|
+
{ name: "ngrok", pattern: /(?:ngrok\.io|ngrok\.app)/i, severity: "high" as const },
|
|
38
|
+
{ name: "pastebin", pattern: /pastebin\.com/i, severity: "high" as const },
|
|
39
|
+
{ name: "ip_url", pattern: /https?:\/\/\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/, severity: "medium" as const },
|
|
40
|
+
{ name: "data_exfil", pattern: /(?:curl|wget|nc)\s+(?:-[a-zA-Z]*\s+)*https?:\/\//i, severity: "critical" as const },
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const ZERO_WIDTH_CHARS = /[\u200B\u200C\u200D\u200E\u200F\u202A-\u202E\u2060-\u2064\u2066-\u2069\uFEFF]/g;
|
|
44
|
+
|
|
45
|
+
const CONFUSABLES: Record<string, string> = {
|
|
46
|
+
"а": "a", "А": "A", "с": "c", "С": "C", "е": "e", "Е": "E",
|
|
47
|
+
"о": "o", "О": "O", "р": "p", "Р": "P", "х": "x", "Х": "X",
|
|
48
|
+
"у": "y", "У": "Y", "і": "i", "І": "I", "Α": "A", "Β": "B",
|
|
49
|
+
"Ε": "E", "Η": "H", "Ι": "I", "Κ": "K", "Μ": "M", "Ν": "N",
|
|
50
|
+
"Ο": "O", "Ρ": "P", "Τ": "T", "Υ": "Y", "Χ": "X", "Ζ": "Z",
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const CONFUSABLE_REGEX = new RegExp(
|
|
54
|
+
`[${Object.keys(CONFUSABLES).join("")}]`,
|
|
55
|
+
"g",
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
export function normalizeUnicode(text: string): {
|
|
59
|
+
normalized: string;
|
|
60
|
+
modified: boolean;
|
|
61
|
+
} {
|
|
62
|
+
let modified = false;
|
|
63
|
+
|
|
64
|
+
let normalized = text.replace(ZERO_WIDTH_CHARS, () => {
|
|
65
|
+
modified = true;
|
|
66
|
+
return "";
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
normalized = normalized.replace(CONFUSABLE_REGEX, (char) => {
|
|
70
|
+
modified = true;
|
|
71
|
+
return CONFUSABLES[char] || char;
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return { normalized, modified };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function detectThreats(text: string): ThreatDetection[] {
|
|
78
|
+
const threats: ThreatDetection[] = [];
|
|
79
|
+
|
|
80
|
+
for (const { name, pattern, severity } of COMMAND_INJECTION_PATTERNS) {
|
|
81
|
+
const match = text.match(pattern);
|
|
82
|
+
if (match) {
|
|
83
|
+
threats.push({ type: "command_injection", pattern: name, location: match[0], severity });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
for (const { name, pattern, severity } of ENCODING_PATTERNS) {
|
|
88
|
+
const match = text.match(pattern);
|
|
89
|
+
if (match) {
|
|
90
|
+
threats.push({ type: "encoding_obfuscation", pattern: name, location: match[0].slice(0, 50), severity });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
for (const { name, pattern, severity } of SOCIAL_ENGINEERING_PATTERNS) {
|
|
95
|
+
const match = text.match(pattern);
|
|
96
|
+
if (match) {
|
|
97
|
+
threats.push({ type: "social_engineering", pattern: name, location: match[0], severity });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
for (const { name, pattern, severity } of NETWORK_PATTERNS) {
|
|
102
|
+
const match = text.match(pattern);
|
|
103
|
+
if (match) {
|
|
104
|
+
threats.push({ type: "network_threat", pattern: name, location: match[0], severity });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return threats;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export function inspectInput(
|
|
112
|
+
_toolName: string,
|
|
113
|
+
args: unknown,
|
|
114
|
+
mode: "block" | "surgical" | "log_only",
|
|
115
|
+
): InspectionResult {
|
|
116
|
+
const text = JSON.stringify(args);
|
|
117
|
+
const { normalized, modified } = normalizeUnicode(text);
|
|
118
|
+
const threats = detectThreats(normalized);
|
|
119
|
+
|
|
120
|
+
if (modified) {
|
|
121
|
+
threats.push({
|
|
122
|
+
type: "unicode_obfuscation",
|
|
123
|
+
pattern: "confusables_or_zero_width",
|
|
124
|
+
severity: "medium",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const hasCritical = threats.some((t) => t.severity === "critical");
|
|
129
|
+
const hasHigh = threats.some((t) => t.severity === "high");
|
|
130
|
+
|
|
131
|
+
if (mode === "block" && (hasCritical || hasHigh)) {
|
|
132
|
+
return { passed: false, threats };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (mode === "surgical" && modified) {
|
|
136
|
+
try {
|
|
137
|
+
const sanitizedArgs = JSON.parse(normalized);
|
|
138
|
+
return { passed: true, threats, sanitized: JSON.stringify(sanitizedArgs) };
|
|
139
|
+
} catch {
|
|
140
|
+
return { passed: true, threats };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return { passed: true, threats };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export function inspectOutput(
|
|
148
|
+
_toolName: string,
|
|
149
|
+
result: string,
|
|
150
|
+
): InspectionResult {
|
|
151
|
+
const threats = detectThreats(result);
|
|
152
|
+
return { passed: true, threats };
|
|
153
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
import type { ResolvedConfig } from "../config.js";
|
|
4
|
+
import { createTokenRefreshService } from "./token-refresh.js";
|
|
5
|
+
import { createKeyRotationService } from "./key-rotation.js";
|
|
6
|
+
|
|
7
|
+
export function registerAllServices(
|
|
8
|
+
api: PluginApi,
|
|
9
|
+
client: OneClawClient,
|
|
10
|
+
config: ResolvedConfig,
|
|
11
|
+
): void {
|
|
12
|
+
api.registerService(createTokenRefreshService(api, client));
|
|
13
|
+
|
|
14
|
+
if (config.features.keyRotationMonitor) {
|
|
15
|
+
api.registerService(createKeyRotationService(api, client));
|
|
16
|
+
}
|
|
17
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const POLL_INTERVAL_MS = 60 * 60 * 1000; // 1 hour
|
|
5
|
+
const WARNING_THRESHOLD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
6
|
+
|
|
7
|
+
export function createKeyRotationService(api: PluginApi, client: OneClawClient) {
|
|
8
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
9
|
+
|
|
10
|
+
async function checkExpiring(): Promise<void> {
|
|
11
|
+
try {
|
|
12
|
+
const data = await client.listSecrets();
|
|
13
|
+
const now = Date.now();
|
|
14
|
+
const expiring: string[] = [];
|
|
15
|
+
|
|
16
|
+
for (const secret of data.secrets) {
|
|
17
|
+
if (!secret.expires_at) continue;
|
|
18
|
+
const expiresAt = new Date(secret.expires_at).getTime();
|
|
19
|
+
if (expiresAt - now < WARNING_THRESHOLD_MS && expiresAt > now) {
|
|
20
|
+
const daysLeft = Math.ceil((expiresAt - now) / (24 * 60 * 60 * 1000));
|
|
21
|
+
expiring.push(`${secret.path} (expires in ${daysLeft}d)`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (expiring.length > 0) {
|
|
26
|
+
api.logger.warn(
|
|
27
|
+
`[1claw/rotation] ${expiring.length} secret(s) expiring within 7 days:\n ${expiring.join("\n ")}`,
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
32
|
+
api.logger.error(`[1claw/rotation] Check failed: ${msg}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
id: "1claw-key-rotation-monitor",
|
|
38
|
+
start: async () => {
|
|
39
|
+
await checkExpiring();
|
|
40
|
+
|
|
41
|
+
interval = setInterval(checkExpiring, POLL_INTERVAL_MS);
|
|
42
|
+
api.logger.info("[1claw] Key rotation monitor started");
|
|
43
|
+
},
|
|
44
|
+
stop: () => {
|
|
45
|
+
if (interval) {
|
|
46
|
+
clearInterval(interval);
|
|
47
|
+
interval = null;
|
|
48
|
+
}
|
|
49
|
+
api.logger.info("[1claw] Key rotation monitor stopped");
|
|
50
|
+
},
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { OneClawClient } from "../client.js";
|
|
2
|
+
import type { PluginApi } from "../types.js";
|
|
3
|
+
|
|
4
|
+
const REFRESH_INTERVAL_MS = 30 * 60 * 1000; // 30 minutes
|
|
5
|
+
|
|
6
|
+
export function createTokenRefreshService(api: PluginApi, client: OneClawClient) {
|
|
7
|
+
let interval: ReturnType<typeof setInterval> | null = null;
|
|
8
|
+
|
|
9
|
+
return {
|
|
10
|
+
id: "1claw-token-refresh",
|
|
11
|
+
start: () => {
|
|
12
|
+
interval = setInterval(async () => {
|
|
13
|
+
try {
|
|
14
|
+
await client.ensureToken();
|
|
15
|
+
const ttlMin = Math.round(client.tokenTtlMs / 60_000);
|
|
16
|
+
api.logger.info(`[1claw/token] Token refreshed, TTL: ${ttlMin}min`);
|
|
17
|
+
} catch (err) {
|
|
18
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
19
|
+
api.logger.error(`[1claw/token] Refresh failed: ${msg}`);
|
|
20
|
+
}
|
|
21
|
+
}, REFRESH_INTERVAL_MS);
|
|
22
|
+
|
|
23
|
+
api.logger.info("[1claw] Token refresh service started");
|
|
24
|
+
},
|
|
25
|
+
stop: () => {
|
|
26
|
+
if (interval) {
|
|
27
|
+
clearInterval(interval);
|
|
28
|
+
interval = null;
|
|
29
|
+
}
|
|
30
|
+
api.logger.info("[1claw] Token refresh service stopped");
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 createVaultTool(client: OneClawClient): PluginTool {
|
|
6
|
+
return {
|
|
7
|
+
name: "oneclaw_create_vault",
|
|
8
|
+
description:
|
|
9
|
+
"Create a new vault for organising secrets. The vault is owned by this agent and automatically shared with the human who registered you.",
|
|
10
|
+
parameters: Type.Object({
|
|
11
|
+
name: Type.String({ minLength: 1, maxLength: 255, description: "Vault name" }),
|
|
12
|
+
description: Type.Optional(Type.String({ description: "Short description" })),
|
|
13
|
+
}),
|
|
14
|
+
optional: true,
|
|
15
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
16
|
+
const vault = await client.createVault(
|
|
17
|
+
params.name as string,
|
|
18
|
+
params.description as string | undefined,
|
|
19
|
+
);
|
|
20
|
+
return {
|
|
21
|
+
content: [{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: `Vault created successfully.\n ID: ${vault.id}\n Name: ${vault.name}\n Owner: ${vault.created_by_type}:${vault.created_by}\n\nThe vault has been automatically shared with your creator.`,
|
|
24
|
+
}],
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
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 deleteSecretTool(client: OneClawClient): PluginTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "oneclaw_delete_secret",
|
|
9
|
+
description:
|
|
10
|
+
"Soft-delete a secret at the given path. All versions are marked deleted. This is reversible by an admin.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
path: Type.String({ minLength: 1, description: "Secret path to delete" }),
|
|
13
|
+
}),
|
|
14
|
+
optional: true,
|
|
15
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
16
|
+
const path = params.path as string;
|
|
17
|
+
try {
|
|
18
|
+
await client.deleteSecret(path);
|
|
19
|
+
return { content: [{ type: "text", text: `Secret at '${path}' has been soft-deleted.` }] };
|
|
20
|
+
} catch (err) {
|
|
21
|
+
if (err instanceof OneClawApiError && err.status === 404) {
|
|
22
|
+
return { content: [{ type: "text", text: `Error: No secret found at path '${path}'.` }] };
|
|
23
|
+
}
|
|
24
|
+
throw err;
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
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 describeSecretTool(client: OneClawClient): PluginTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "oneclaw_describe_secret",
|
|
9
|
+
description:
|
|
10
|
+
"Get metadata for a secret (type, version, expiry) without fetching its value. Use this to check if a secret exists or is still valid before fetching it.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
path: Type.String({ minLength: 1, description: "Secret path to describe" }),
|
|
13
|
+
}),
|
|
14
|
+
optional: true,
|
|
15
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
16
|
+
const path = params.path as string;
|
|
17
|
+
const data = await client.listSecrets();
|
|
18
|
+
const match = data.secrets.find((s) => s.path === path);
|
|
19
|
+
|
|
20
|
+
if (!match) {
|
|
21
|
+
try {
|
|
22
|
+
const secret = await client.getSecret(path);
|
|
23
|
+
return {
|
|
24
|
+
content: [{
|
|
25
|
+
type: "text",
|
|
26
|
+
text: JSON.stringify({
|
|
27
|
+
path: secret.path,
|
|
28
|
+
type: secret.type,
|
|
29
|
+
version: secret.version,
|
|
30
|
+
metadata: secret.metadata,
|
|
31
|
+
created_at: secret.created_at,
|
|
32
|
+
expires_at: secret.expires_at,
|
|
33
|
+
}, null, 2),
|
|
34
|
+
}],
|
|
35
|
+
};
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err instanceof OneClawApiError) {
|
|
38
|
+
if (err.status === 404) {
|
|
39
|
+
return { content: [{ type: "text", text: `Error: No secret found at path '${path}'.` }] };
|
|
40
|
+
}
|
|
41
|
+
if (err.status === 410) {
|
|
42
|
+
return { content: [{ type: "text", text: `Error: Secret at path '${path}' is expired or has exceeded its maximum access count.` }] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
content: [{
|
|
51
|
+
type: "text",
|
|
52
|
+
text: JSON.stringify({
|
|
53
|
+
path: match.path,
|
|
54
|
+
type: match.type,
|
|
55
|
+
version: match.version,
|
|
56
|
+
metadata: match.metadata,
|
|
57
|
+
created_at: match.created_at,
|
|
58
|
+
expires_at: match.expires_at,
|
|
59
|
+
}, null, 2),
|
|
60
|
+
}],
|
|
61
|
+
};
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
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 getEnvBundleTool(client: OneClawClient): PluginTool {
|
|
7
|
+
return {
|
|
8
|
+
name: "oneclaw_get_env_bundle",
|
|
9
|
+
description:
|
|
10
|
+
"Fetch a secret of type env_bundle, parse its KEY=VALUE lines, and return a structured JSON object. Useful for injecting environment variables into subprocesses.",
|
|
11
|
+
parameters: Type.Object({
|
|
12
|
+
path: Type.String({ minLength: 1, description: "Path to an env_bundle secret" }),
|
|
13
|
+
}),
|
|
14
|
+
optional: true,
|
|
15
|
+
execute: async (_id: unknown, params: Record<string, unknown>): Promise<ToolResult> => {
|
|
16
|
+
const path = params.path as string;
|
|
17
|
+
try {
|
|
18
|
+
const secret = await client.getSecret(path);
|
|
19
|
+
|
|
20
|
+
if (secret.type !== "env_bundle") {
|
|
21
|
+
return {
|
|
22
|
+
content: [{ type: "text", text: `Error: Secret at '${path}' is type '${secret.type}', not 'env_bundle'.` }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const env: Record<string, string> = {};
|
|
27
|
+
for (const line of secret.value.split("\n")) {
|
|
28
|
+
const trimmed = line.trim();
|
|
29
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
30
|
+
const eqIdx = trimmed.indexOf("=");
|
|
31
|
+
if (eqIdx === -1) continue;
|
|
32
|
+
env[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return { content: [{ type: "text", text: JSON.stringify(env, null, 2) }] };
|
|
36
|
+
} catch (err) {
|
|
37
|
+
if (err instanceof OneClawApiError) {
|
|
38
|
+
if (err.status === 410) {
|
|
39
|
+
return { content: [{ type: "text", text: `Error: Secret at path '${path}' is expired or has exceeded its maximum access count.` }] };
|
|
40
|
+
}
|
|
41
|
+
if (err.status === 404) {
|
|
42
|
+
return { content: [{ type: "text", text: `Error: No secret found at path '${path}'.` }] };
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
throw err;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|