airlock-bot 0.2.24 → 0.2.26
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/dist/config/schema.d.ts +26 -0
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +2 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +5 -0
- package/dist/gateway.js.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/pool/http-client.d.ts +3 -1
- package/dist/pool/http-client.d.ts.map +1 -1
- package/dist/pool/http-client.js +6 -2
- package/dist/pool/http-client.js.map +1 -1
- package/dist/pool/oauth-provider.d.ts +3 -1
- package/dist/pool/oauth-provider.d.ts.map +1 -1
- package/dist/pool/oauth-provider.js +17 -2
- package/dist/pool/oauth-provider.js.map +1 -1
- package/dist/pool/pool.d.ts.map +1 -1
- package/dist/pool/pool.js +1 -1
- package/dist/pool/pool.js.map +1 -1
- package/dist/setup-openclaw/cli.d.ts +9 -0
- package/dist/setup-openclaw/cli.d.ts.map +1 -0
- package/dist/setup-openclaw/cli.js +49 -0
- package/dist/setup-openclaw/cli.js.map +1 -0
- package/dist/tools/api.d.ts +8 -0
- package/dist/tools/api.d.ts.map +1 -0
- package/dist/tools/api.js +103 -0
- package/dist/tools/api.js.map +1 -0
- package/extensions/openclaw/index.ts +125 -0
- package/extensions/openclaw/openclaw.plugin.json +38 -0
- package/extensions/openclaw/package-lock.json +8548 -0
- package/extensions/openclaw/package.json +12 -0
- package/package.json +3 -1
- package/schema.json +6 -0
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `airlock setup openclaw` — one-command setup for the airlock-bridge plugin.
|
|
3
|
+
*
|
|
4
|
+
* Creates a symlink from ~/.openclaw/extensions/airlock-bridge to the bundled
|
|
5
|
+
* plugin in this package. The symlink means updates to airlock automatically
|
|
6
|
+
* update the plugin — no need to rerun this command after upgrading.
|
|
7
|
+
*/
|
|
8
|
+
import { mkdirSync, symlinkSync, existsSync, lstatSync, unlinkSync } from 'fs';
|
|
9
|
+
import { homedir } from 'os';
|
|
10
|
+
import { join, dirname } from 'path';
|
|
11
|
+
import { fileURLToPath } from 'url';
|
|
12
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
13
|
+
export function runSetupOpenclaw(_argv) {
|
|
14
|
+
// Locate the bundled plugin source relative to this file.
|
|
15
|
+
// In the source tree: src/setup-openclaw/cli.ts → ../../extensions/openclaw
|
|
16
|
+
// In the dist tree: dist/setup-openclaw/cli.js → ../../extensions/openclaw
|
|
17
|
+
const pluginSrc = join(__dirname, '..', '..', 'extensions', 'openclaw');
|
|
18
|
+
if (!existsSync(pluginSrc)) {
|
|
19
|
+
throw new Error(`Plugin source not found at ${pluginSrc}`);
|
|
20
|
+
}
|
|
21
|
+
const extensionsDir = join(homedir(), '.openclaw', 'extensions');
|
|
22
|
+
const dest = join(extensionsDir, 'airlock-bridge');
|
|
23
|
+
mkdirSync(extensionsDir, { recursive: true });
|
|
24
|
+
// Remove an existing symlink or warn about a real directory.
|
|
25
|
+
if (existsSync(dest) || lstatSync(dest, { throwIfNoEntry: false })) {
|
|
26
|
+
const stat = lstatSync(dest);
|
|
27
|
+
if (stat.isSymbolicLink()) {
|
|
28
|
+
unlinkSync(dest);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
throw new Error(`${dest} already exists and is not a symlink.\nRemove it manually to continue.`);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
symlinkSync(pluginSrc, dest, 'dir');
|
|
35
|
+
console.log(`✓ airlock-bridge → ${pluginSrc}
|
|
36
|
+
|
|
37
|
+
Restart the OpenClaw gateway to load it:
|
|
38
|
+
|
|
39
|
+
openclaw restart
|
|
40
|
+
|
|
41
|
+
The plugin connects to http://localhost:4111 as agent "openclaw" by default.
|
|
42
|
+
Override with environment variables if needed:
|
|
43
|
+
|
|
44
|
+
export AIRLOCK_URL=http://localhost:4111
|
|
45
|
+
export AIRLOCK_AGENT=openclaw
|
|
46
|
+
export AIRLOCK_SECRET=your-api-secret
|
|
47
|
+
`);
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../src/setup-openclaw/cli.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,IAAI,CAAC;AAC/E,OAAO,EAAE,OAAO,EAAE,MAAM,IAAI,CAAC;AAC7B,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,MAAM,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,KAAK,CAAC;AAEpC,MAAM,SAAS,GAAG,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAE1D,MAAM,UAAU,gBAAgB,CAAC,KAAe;IAC9C,0DAA0D;IAC1D,4EAA4E;IAC5E,6EAA6E;IAC7E,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,YAAY,EAAE,UAAU,CAAC,CAAC;IAExE,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,EAAE,CAAC;QAC3B,MAAM,IAAI,KAAK,CAAC,8BAA8B,SAAS,EAAE,CAAC,CAAC;IAC7D,CAAC;IAED,MAAM,aAAa,GAAG,IAAI,CAAC,OAAO,EAAE,EAAE,WAAW,EAAE,YAAY,CAAC,CAAC;IACjE,MAAM,IAAI,GAAG,IAAI,CAAC,aAAa,EAAE,gBAAgB,CAAC,CAAC;IAEnD,SAAS,CAAC,aAAa,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE9C,6DAA6D;IAC7D,IAAI,UAAU,CAAC,IAAI,CAAC,IAAI,SAAS,CAAC,IAAI,EAAE,EAAE,cAAc,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACnE,MAAM,IAAI,GAAG,SAAS,CAAC,IAAI,CAAC,CAAC;QAC7B,IAAI,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;YAC1B,UAAU,CAAC,IAAI,CAAC,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,MAAM,IAAI,KAAK,CACb,GAAG,IAAI,wEAAwE,CAChF,CAAC;QACJ,CAAC;IACH,CAAC;IAED,WAAW,CAAC,SAAS,EAAE,IAAI,EAAE,KAAK,CAAC,CAAC;IAEpC,OAAO,CAAC,GAAG,CAAC,sBAAsB,SAAS;;;;;;;;;;;;CAY5C,CAAC,CAAC;AACH,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyInstance } from 'fastify';
|
|
2
|
+
import type { AgentServerDeps } from '../transport/agent-server.js';
|
|
3
|
+
export interface ToolsApiOpts {
|
|
4
|
+
getDeps: (agentId: string) => AgentServerDeps | undefined;
|
|
5
|
+
secret?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function toolsApiPlugin(app: FastifyInstance, opts: ToolsApiOpts): Promise<void>;
|
|
8
|
+
//# sourceMappingURL=api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.d.ts","sourceRoot":"","sources":["../../src/tools/api.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAepE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,eAAe,GAAG,SAAS,CAAC;IAC1D,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAGD,wBAAsB,cAAc,CAAC,GAAG,EAAE,eAAe,EAAE,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC,CAkG5F"}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { timingSafeEqual } from 'crypto';
|
|
2
|
+
import { buildMiddlewareChain } from '../middleware/chain-builder.js';
|
|
3
|
+
import { generateId } from '../util/id.js';
|
|
4
|
+
import { childLogger } from '../util/logger.js';
|
|
5
|
+
const log = childLogger('tools-api');
|
|
6
|
+
function constantTimeEqual(a, b) {
|
|
7
|
+
const bufA = Buffer.from(a);
|
|
8
|
+
const bufB = Buffer.from(b);
|
|
9
|
+
if (bufA.length !== bufB.length)
|
|
10
|
+
return false;
|
|
11
|
+
return timingSafeEqual(bufA, bufB);
|
|
12
|
+
}
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/require-await
|
|
14
|
+
export async function toolsApiPlugin(app, opts) {
|
|
15
|
+
const { getDeps, secret } = opts;
|
|
16
|
+
app.addHook('preHandler', async (request, reply) => {
|
|
17
|
+
if (!secret)
|
|
18
|
+
return;
|
|
19
|
+
const auth = request.headers.authorization ?? '';
|
|
20
|
+
if (!constantTimeEqual(auth, `Bearer ${secret}`)) {
|
|
21
|
+
return reply.status(401).send({ error: 'Unauthorized' });
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
app.get('/agents/:agentId/tools', async (request, reply) => {
|
|
25
|
+
const { agentId } = request.params;
|
|
26
|
+
const deps = getDeps(agentId);
|
|
27
|
+
if (!deps) {
|
|
28
|
+
return reply.status(404).send({ error: `Unknown agent: ${agentId}` });
|
|
29
|
+
}
|
|
30
|
+
const tools = deps.registry.getFiltered(agentId);
|
|
31
|
+
return reply.send({ tools });
|
|
32
|
+
});
|
|
33
|
+
app.post('/agents/:agentId/tools/invoke', async (request, reply) => {
|
|
34
|
+
const { agentId } = request.params;
|
|
35
|
+
const body = request.body;
|
|
36
|
+
if (!body?.tool) {
|
|
37
|
+
return reply.status(400).send({ error: 'Missing required field: tool' });
|
|
38
|
+
}
|
|
39
|
+
const { tool, args = {} } = body;
|
|
40
|
+
const deps = getDeps(agentId);
|
|
41
|
+
if (!deps) {
|
|
42
|
+
return reply.status(404).send({ error: `Unknown agent: ${agentId}` });
|
|
43
|
+
}
|
|
44
|
+
const getConfig = deps.getAgentConfig ?? (() => deps.agentConfig);
|
|
45
|
+
const agentConfig = getConfig();
|
|
46
|
+
const chain = buildMiddlewareChain(agentConfig, {
|
|
47
|
+
registry: deps.registry,
|
|
48
|
+
allowlist: deps.allowlist,
|
|
49
|
+
hitlEngine: deps.hitlEngine,
|
|
50
|
+
hitlBatcher: deps.hitlBatcher,
|
|
51
|
+
auditLogger: deps.auditLogger,
|
|
52
|
+
securityConfig: deps.securityConfig ?? { blocked_hosts: [], allowed_local: [] },
|
|
53
|
+
});
|
|
54
|
+
const ctx = {
|
|
55
|
+
callId: generateId(),
|
|
56
|
+
agentId,
|
|
57
|
+
agentConfig,
|
|
58
|
+
toolName: tool,
|
|
59
|
+
args,
|
|
60
|
+
meta: {},
|
|
61
|
+
deps: {
|
|
62
|
+
registry: deps.registry,
|
|
63
|
+
allowlist: deps.allowlist,
|
|
64
|
+
hitlEngine: deps.hitlEngine,
|
|
65
|
+
hitlBatcher: deps.hitlBatcher,
|
|
66
|
+
auditLogger: deps.auditLogger,
|
|
67
|
+
securityConfig: deps.securityConfig ?? { blocked_hosts: [], allowed_local: [] },
|
|
68
|
+
},
|
|
69
|
+
startedAt: Date.now(),
|
|
70
|
+
};
|
|
71
|
+
try {
|
|
72
|
+
const response = await chain(ctx, () => {
|
|
73
|
+
throw new Error('Middleware chain did not terminate — missing execute middleware');
|
|
74
|
+
});
|
|
75
|
+
const duration_ms = Date.now() - ctx.startedAt;
|
|
76
|
+
// Middleware may return an MCP-style error response (e.g. HITL deny/timeout)
|
|
77
|
+
// with isError: true in the result — map that to success: false for the HTTP API.
|
|
78
|
+
const result = response.result;
|
|
79
|
+
if (result?.isError) {
|
|
80
|
+
return reply.send({
|
|
81
|
+
success: false,
|
|
82
|
+
error: response.text || 'Tool call failed',
|
|
83
|
+
metadata: { duration_ms },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
return reply.send({
|
|
87
|
+
success: true,
|
|
88
|
+
data: response.result,
|
|
89
|
+
metadata: { duration_ms, truncated: response.truncated ?? false },
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
const duration_ms = Date.now() - ctx.startedAt;
|
|
94
|
+
log.warn({ err, agentId, tool }, 'Tool invocation failed');
|
|
95
|
+
return reply.send({
|
|
96
|
+
success: false,
|
|
97
|
+
error: err instanceof Error ? err.message : String(err),
|
|
98
|
+
metadata: { duration_ms },
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
//# sourceMappingURL=api.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api.js","sourceRoot":"","sources":["../../src/tools/api.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Airlock Bridge — OpenClaw plugin
|
|
3
|
+
*
|
|
4
|
+
* Fetches the tool list from Airlock at startup and registers each one as a
|
|
5
|
+
* native OpenClaw tool. Every tool call is forwarded to Airlock, which applies
|
|
6
|
+
* the agent's allowlist, HITL gate, sandbox, and audit logging before executing.
|
|
7
|
+
*
|
|
8
|
+
* Configuration (plugin config or environment variable fallbacks):
|
|
9
|
+
* url / AIRLOCK_URL Base URL of the Airlock gateway (default: http://localhost:4111)
|
|
10
|
+
* agent / AIRLOCK_AGENT Agent profile to run as (default: openclaw)
|
|
11
|
+
* secret / AIRLOCK_SECRET Bearer token / api_secret (default: empty = no auth)
|
|
12
|
+
*
|
|
13
|
+
* Install:
|
|
14
|
+
* mkdir -p ~/.openclaw/extensions/airlock-bridge
|
|
15
|
+
* cp -r extensions/openclaw/* ~/.openclaw/extensions/airlock-bridge/
|
|
16
|
+
* cd ~/.openclaw/extensions/airlock-bridge && npm install --omit=dev
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { Type } from '@sinclair/typebox';
|
|
20
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
21
|
+
|
|
22
|
+
interface AirlockTool {
|
|
23
|
+
name: string;
|
|
24
|
+
description?: string;
|
|
25
|
+
inputSchema: Record<string, unknown>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface AirlockInvokeResult {
|
|
29
|
+
success: boolean;
|
|
30
|
+
data?: unknown;
|
|
31
|
+
error?: string;
|
|
32
|
+
metadata?: { duration_ms: number; truncated: boolean };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Convert an Airlock tool name (e.g. "github/list_prs") to a valid OpenClaw
|
|
36
|
+
// tool identifier (e.g. "airlock_github_list_prs").
|
|
37
|
+
function toOpenClawName(airlockName: string): string {
|
|
38
|
+
return 'airlock_' + airlockName.replace(/[^a-zA-Z0-9]/g, '_');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export default async function register(api: OpenClawPluginApi): Promise<void> {
|
|
42
|
+
const cfg = (api.pluginConfig ?? {}) as { url?: string; agent?: string; secret?: string };
|
|
43
|
+
|
|
44
|
+
const baseUrl = (process.env['AIRLOCK_URL'] ?? cfg.url ?? 'http://localhost:4111').replace(
|
|
45
|
+
/\/$/,
|
|
46
|
+
''
|
|
47
|
+
);
|
|
48
|
+
const agentId = process.env['AIRLOCK_AGENT'] ?? cfg.agent ?? 'openclaw';
|
|
49
|
+
const secret = process.env['AIRLOCK_SECRET'] ?? cfg.secret ?? '';
|
|
50
|
+
|
|
51
|
+
const authHeaders: Record<string, string> = secret ? { Authorization: `Bearer ${secret}` } : {};
|
|
52
|
+
|
|
53
|
+
// Fetch tools and register them once the gateway is ready.
|
|
54
|
+
api.on('gateway_start', async () => {
|
|
55
|
+
let tools: AirlockTool[];
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`${baseUrl}/agents/${agentId}/tools`, {
|
|
59
|
+
headers: authHeaders,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (!res.ok) {
|
|
63
|
+
api.logger.error(`[airlock-bridge] Failed to fetch tools: HTTP ${res.status}`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const body = (await res.json()) as { tools: AirlockTool[] };
|
|
68
|
+
tools = body.tools;
|
|
69
|
+
} catch (err) {
|
|
70
|
+
api.logger.error(`[airlock-bridge] Could not reach Airlock gateway: ${String(err)}`);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
api.logger.info(`[airlock-bridge] Registering ${tools.length} tools for agent "${agentId}"`);
|
|
75
|
+
|
|
76
|
+
for (const tool of tools) {
|
|
77
|
+
const openClawName = toOpenClawName(tool.name);
|
|
78
|
+
const airlockName = tool.name; // captured in closure
|
|
79
|
+
|
|
80
|
+
api.registerTool(
|
|
81
|
+
{
|
|
82
|
+
name: openClawName,
|
|
83
|
+
label: tool.name, // human-readable: preserves the original "namespace/tool" form
|
|
84
|
+
description: tool.description ?? openClawName,
|
|
85
|
+
// Wrap Airlock's JSON Schema as a typebox Unsafe type so OpenClaw's
|
|
86
|
+
// tool registry accepts it without modification.
|
|
87
|
+
parameters: Type.Unsafe<Record<string, unknown>>(tool.inputSchema),
|
|
88
|
+
|
|
89
|
+
async execute(_toolCallId: string, params: Record<string, unknown>) {
|
|
90
|
+
let result: AirlockInvokeResult;
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const res = await fetch(`${baseUrl}/agents/${agentId}/tools/invoke`, {
|
|
94
|
+
method: 'POST',
|
|
95
|
+
headers: { 'Content-Type': 'application/json', ...authHeaders },
|
|
96
|
+
body: JSON.stringify({ tool: airlockName, args: params }),
|
|
97
|
+
});
|
|
98
|
+
result = (await res.json()) as AirlockInvokeResult;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
return {
|
|
101
|
+
content: [{ type: 'text' as const, text: `Airlock unreachable: ${String(err)}` }],
|
|
102
|
+
details: {},
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (!result.success) {
|
|
107
|
+
return {
|
|
108
|
+
content: [
|
|
109
|
+
{ type: 'text' as const, text: `Error: ${result.error ?? 'unknown error'}` },
|
|
110
|
+
],
|
|
111
|
+
details: result.metadata ?? {},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
content: [{ type: 'text' as const, text: JSON.stringify(result.data ?? null) }],
|
|
117
|
+
details: result.metadata ?? {},
|
|
118
|
+
};
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{ optional: true }
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "airlock-bridge",
|
|
3
|
+
"name": "Airlock Bridge",
|
|
4
|
+
"description": "Routes tool calls through Airlock for permission enforcement and human-in-the-loop approval",
|
|
5
|
+
"configSchema": {
|
|
6
|
+
"type": "object",
|
|
7
|
+
"additionalProperties": false,
|
|
8
|
+
"properties": {
|
|
9
|
+
"url": {
|
|
10
|
+
"type": "string",
|
|
11
|
+
"description": "Airlock gateway base URL (overrides AIRLOCK_URL env var)"
|
|
12
|
+
},
|
|
13
|
+
"agent": {
|
|
14
|
+
"type": "string",
|
|
15
|
+
"description": "Airlock agent profile to run as (overrides AIRLOCK_AGENT env var)"
|
|
16
|
+
},
|
|
17
|
+
"secret": {
|
|
18
|
+
"type": "string",
|
|
19
|
+
"description": "Airlock API secret / Bearer token (overrides AIRLOCK_SECRET env var)"
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
"uiHints": {
|
|
24
|
+
"url": {
|
|
25
|
+
"label": "Gateway URL",
|
|
26
|
+
"placeholder": "http://localhost:4111"
|
|
27
|
+
},
|
|
28
|
+
"agent": {
|
|
29
|
+
"label": "Agent Profile",
|
|
30
|
+
"placeholder": "openclaw"
|
|
31
|
+
},
|
|
32
|
+
"secret": {
|
|
33
|
+
"label": "API Secret",
|
|
34
|
+
"sensitive": true,
|
|
35
|
+
"placeholder": "your-api-secret"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|