airlock-bot 0.2.37 → 0.2.38
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/README.md +26 -8
- package/dist/activity/stream.d.ts +29 -0
- package/dist/activity/stream.d.ts.map +1 -0
- package/dist/activity/stream.js +36 -0
- package/dist/activity/stream.js.map +1 -0
- package/dist/airlock/context.d.ts +12 -0
- package/dist/airlock/context.d.ts.map +1 -0
- package/dist/airlock/context.js +32 -0
- package/dist/airlock/context.js.map +1 -0
- package/dist/airlock/tools.d.ts +7 -0
- package/dist/airlock/tools.d.ts.map +1 -0
- package/dist/airlock/tools.js +13 -0
- package/dist/airlock/tools.js.map +1 -0
- package/dist/allowlist/engine.d.ts +12 -0
- package/dist/allowlist/engine.d.ts.map +1 -1
- package/dist/allowlist/engine.js +40 -15
- package/dist/allowlist/engine.js.map +1 -1
- package/dist/audit/api.d.ts +2 -0
- package/dist/audit/api.d.ts.map +1 -1
- package/dist/audit/api.js.map +1 -1
- package/dist/audit/db.d.ts +16 -0
- package/dist/audit/db.d.ts.map +1 -1
- package/dist/audit/db.js +73 -0
- package/dist/audit/db.js.map +1 -1
- package/dist/audit/logger.d.ts +8 -1
- package/dist/audit/logger.d.ts.map +1 -1
- package/dist/audit/logger.js +21 -0
- package/dist/audit/logger.js.map +1 -1
- package/dist/backend/airlock-adapter.d.ts +22 -0
- package/dist/backend/airlock-adapter.d.ts.map +1 -0
- package/dist/backend/airlock-adapter.js +136 -0
- package/dist/backend/airlock-adapter.js.map +1 -0
- package/dist/backend/factory.d.ts +5 -1
- package/dist/backend/factory.d.ts.map +1 -1
- package/dist/backend/factory.js +5 -5
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/mcp-adapter.d.ts.map +1 -1
- package/dist/backend/mcp-adapter.js +28 -4
- package/dist/backend/mcp-adapter.js.map +1 -1
- package/dist/backend/openapi/adapter.d.ts.map +1 -1
- package/dist/backend/openapi/adapter.js +33 -13
- package/dist/backend/openapi/adapter.js.map +1 -1
- package/dist/config/loader.d.ts +12 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +799 -23
- package/dist/config/loader.js.map +1 -1
- package/dist/config/profiles.d.ts +44 -3
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +314 -3
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/schema.d.ts +3300 -201
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +272 -71
- package/dist/config/schema.js.map +1 -1
- package/dist/configure-web/cli.d.ts +1 -0
- package/dist/configure-web/cli.d.ts.map +1 -1
- package/dist/configure-web/cli.js +238 -47
- package/dist/configure-web/cli.js.map +1 -1
- package/dist/gateway.d.ts +16 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +263 -57
- package/dist/gateway.js.map +1 -1
- package/dist/hitl/api.d.ts +2 -0
- package/dist/hitl/api.d.ts.map +1 -1
- package/dist/hitl/api.js +8 -0
- package/dist/hitl/api.js.map +1 -1
- package/dist/hitl/approval-dashboard.d.ts +5 -9
- package/dist/hitl/approval-dashboard.d.ts.map +1 -1
- package/dist/hitl/approval-dashboard.js +8 -55
- package/dist/hitl/approval-dashboard.js.map +1 -1
- package/dist/hitl/approval-stream.d.ts +34 -0
- package/dist/hitl/approval-stream.d.ts.map +1 -0
- package/dist/hitl/approval-stream.js +111 -0
- package/dist/hitl/approval-stream.js.map +1 -0
- package/dist/hitl/engine.d.ts +16 -7
- package/dist/hitl/engine.d.ts.map +1 -1
- package/dist/hitl/engine.js +109 -35
- package/dist/hitl/engine.js.map +1 -1
- package/dist/hitl/formatter.d.ts.map +1 -1
- package/dist/hitl/formatter.js +2 -0
- package/dist/hitl/formatter.js.map +1 -1
- package/dist/hitl/provider-factory.d.ts +8 -2
- package/dist/hitl/provider-factory.d.ts.map +1 -1
- package/dist/hitl/provider-factory.js +23 -2
- package/dist/hitl/provider-factory.js.map +1 -1
- package/dist/hitl/providers/composite.d.ts +9 -0
- package/dist/hitl/providers/composite.d.ts.map +1 -1
- package/dist/hitl/providers/composite.js +14 -0
- package/dist/hitl/providers/composite.js.map +1 -1
- package/dist/hitl/providers/dashboard.d.ts +12 -2
- package/dist/hitl/providers/dashboard.d.ts.map +1 -1
- package/dist/hitl/providers/dashboard.js +11 -5
- package/dist/hitl/providers/dashboard.js.map +1 -1
- package/dist/hitl/providers/ios.d.ts +23 -0
- package/dist/hitl/providers/ios.d.ts.map +1 -0
- package/dist/hitl/providers/ios.js +183 -0
- package/dist/hitl/providers/ios.js.map +1 -0
- package/dist/hitl/providers/macos.d.ts.map +1 -1
- package/dist/hitl/providers/macos.js +8 -4
- package/dist/hitl/providers/macos.js.map +1 -1
- package/dist/hitl/providers/openclaw.js +2 -2
- package/dist/hitl/providers/openclaw.js.map +1 -1
- package/dist/hitl/providers/stdio.js +2 -2
- package/dist/hitl/providers/stdio.js.map +1 -1
- package/dist/hitl/providers/telegram.js +2 -2
- package/dist/hitl/providers/telegram.js.map +1 -1
- package/dist/hitl/providers/tui.d.ts.map +1 -1
- package/dist/hitl/providers/tui.js +8 -2
- package/dist/hitl/providers/tui.js.map +1 -1
- package/dist/hitl/providers/types.d.ts +14 -0
- package/dist/hitl/providers/types.d.ts.map +1 -1
- package/dist/hook/api.d.ts +2 -0
- package/dist/hook/api.d.ts.map +1 -1
- package/dist/hook/api.js.map +1 -1
- package/dist/hook/normalizer.js +2 -2
- package/dist/hook/normalizer.js.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/introspect/cli.d.ts +31 -0
- package/dist/introspect/cli.d.ts.map +1 -0
- package/dist/introspect/cli.js +756 -0
- package/dist/introspect/cli.js.map +1 -0
- package/dist/middleware/chain-builder.d.ts +1 -1
- package/dist/middleware/chain-builder.d.ts.map +1 -1
- package/dist/middleware/chain-builder.js +6 -4
- package/dist/middleware/chain-builder.js.map +1 -1
- package/dist/middleware/core/allowlist.d.ts.map +1 -1
- package/dist/middleware/core/allowlist.js +14 -0
- package/dist/middleware/core/allowlist.js.map +1 -1
- package/dist/middleware/core/arg-policy.d.ts +2 -1
- package/dist/middleware/core/arg-policy.d.ts.map +1 -1
- package/dist/middleware/core/arg-policy.js +156 -46
- package/dist/middleware/core/arg-policy.js.map +1 -1
- package/dist/middleware/core/exec-policy.d.ts.map +1 -1
- package/dist/middleware/core/exec-policy.js +16 -1
- package/dist/middleware/core/exec-policy.js.map +1 -1
- package/dist/middleware/core/execute.d.ts.map +1 -1
- package/dist/middleware/core/execute.js +6 -2
- package/dist/middleware/core/execute.js.map +1 -1
- package/dist/middleware/core/hitl-gate.d.ts.map +1 -1
- package/dist/middleware/core/hitl-gate.js +15 -4
- package/dist/middleware/core/hitl-gate.js.map +1 -1
- package/dist/middleware/core/schema-validator.d.ts.map +1 -1
- package/dist/middleware/core/schema-validator.js +7 -3
- package/dist/middleware/core/schema-validator.js.map +1 -1
- package/dist/middleware/post/canary-token-injector.d.ts +4 -1
- package/dist/middleware/post/canary-token-injector.d.ts.map +1 -1
- package/dist/middleware/post/canary-token-injector.js +5 -1
- package/dist/middleware/post/canary-token-injector.js.map +1 -1
- package/dist/middleware/post/untrusted-envelope.d.ts.map +1 -1
- package/dist/middleware/post/untrusted-envelope.js +7 -1
- package/dist/middleware/post/untrusted-envelope.js.map +1 -1
- package/dist/mobile/api.d.ts +20 -0
- package/dist/mobile/api.d.ts.map +1 -0
- package/dist/mobile/api.js +255 -0
- package/dist/mobile/api.js.map +1 -0
- package/dist/mobile/apns.d.ts +68 -0
- package/dist/mobile/apns.d.ts.map +1 -0
- package/dist/mobile/apns.js +321 -0
- package/dist/mobile/apns.js.map +1 -0
- 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 +5 -2
- package/dist/pool/http-client.js.map +1 -1
- package/dist/pool/oauth-provider.d.ts +2 -0
- package/dist/pool/oauth-provider.d.ts.map +1 -1
- package/dist/pool/oauth-provider.js +23 -4
- package/dist/pool/oauth-provider.js.map +1 -1
- package/dist/pool/pool.d.ts +2 -1
- package/dist/pool/pool.d.ts.map +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/pool/sse-client.d.ts +3 -1
- package/dist/pool/sse-client.d.ts.map +1 -1
- package/dist/pool/sse-client.js +5 -2
- package/dist/pool/sse-client.js.map +1 -1
- package/dist/pool/stdio-client.d.ts +3 -1
- package/dist/pool/stdio-client.d.ts.map +1 -1
- package/dist/pool/stdio-client.js +5 -2
- package/dist/pool/stdio-client.js.map +1 -1
- package/dist/registry/airlock-policy.d.ts +3 -0
- package/dist/registry/airlock-policy.d.ts.map +1 -0
- package/dist/registry/airlock-policy.js +50 -0
- package/dist/registry/airlock-policy.js.map +1 -0
- package/dist/registry/registry.d.ts +2 -0
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +19 -13
- package/dist/registry/registry.js.map +1 -1
- package/dist/security/blocked-hosts.d.ts +2 -0
- package/dist/security/blocked-hosts.d.ts.map +1 -1
- package/dist/security/blocked-hosts.js +61 -7
- package/dist/security/blocked-hosts.js.map +1 -1
- package/dist/security/request.d.ts +2 -0
- package/dist/security/request.d.ts.map +1 -1
- package/dist/security/request.js +6 -2
- package/dist/security/request.js.map +1 -1
- package/dist/security/secrets.d.ts +12 -0
- package/dist/security/secrets.d.ts.map +1 -0
- package/dist/security/secrets.js +138 -0
- package/dist/security/secrets.js.map +1 -0
- package/dist/setup-openclaw/cli.js +1 -1
- package/dist/stdio-mode.d.ts.map +1 -1
- package/dist/stdio-mode.js +125 -24
- package/dist/stdio-mode.js.map +1 -1
- package/dist/tools/api.d.ts +4 -1
- package/dist/tools/api.d.ts.map +1 -1
- package/dist/tools/api.js +58 -7
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/exec.d.ts +2 -1
- package/dist/tools/exec.d.ts.map +1 -1
- package/dist/tools/exec.js +4 -3
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/http.d.ts.map +1 -1
- package/dist/tools/http.js +4 -7
- package/dist/tools/http.js.map +1 -1
- package/dist/transport/agent-server.d.ts +4 -0
- package/dist/transport/agent-server.d.ts.map +1 -1
- package/dist/transport/agent-server.js +23 -11
- package/dist/transport/agent-server.js.map +1 -1
- package/dist/transport/http-server.d.ts +2 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +10 -6
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/sse-server.d.ts +2 -0
- package/dist/transport/sse-server.d.ts.map +1 -1
- package/dist/transport/sse-server.js +10 -6
- package/dist/transport/sse-server.js.map +1 -1
- package/dist/transport/stdio-server.d.ts.map +1 -1
- package/dist/transport/stdio-server.js +4 -0
- package/dist/transport/stdio-server.js.map +1 -1
- package/dist/util/id.d.ts +1 -1
- package/dist/util/id.d.ts.map +1 -1
- package/dist/util/id.js +3 -3
- package/dist/util/id.js.map +1 -1
- package/examples/claude-code-setup.md +0 -2
- package/examples/docker-compose.yaml +4 -3
- package/examples/docker-gateway.yaml +8 -3
- package/examples/gateway.yaml +8 -5
- package/examples/profiles.yaml +0 -2
- package/extensions/openclaw/index.ts +9 -6
- package/extensions/openclaw/openclaw.plugin.json +5 -5
- package/package.json +3 -1
- package/schema.json +634 -28
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from 'crypto';
|
|
2
2
|
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
|
|
3
|
+
import { isIP } from 'net';
|
|
3
4
|
import { parseArgs } from 'util';
|
|
4
5
|
import Fastify from 'fastify';
|
|
5
6
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
@@ -25,6 +26,8 @@ Options:
|
|
|
25
26
|
-c, --config <path> Airlock config file (default: ./airlock.yaml)
|
|
26
27
|
-p, --port <port> Web UI port (default: 4177)
|
|
27
28
|
--host <host> Bind host (default: 127.0.0.1)
|
|
29
|
+
--insecure-remote-bind
|
|
30
|
+
Allow binding the unauthenticated UI beyond loopback
|
|
28
31
|
-h, --help Show this help
|
|
29
32
|
`;
|
|
30
33
|
const RUN_HELP = `
|
|
@@ -37,6 +40,8 @@ Options:
|
|
|
37
40
|
-c, --config <path> Airlock config file (default: ./airlock.yaml)
|
|
38
41
|
-p, --port <port> Web UI port (default: 4177)
|
|
39
42
|
--host <host> Bind host (default: 127.0.0.1)
|
|
43
|
+
--insecure-remote-bind
|
|
44
|
+
Allow binding the unauthenticated UI beyond loopback
|
|
40
45
|
-h, --help Show this help
|
|
41
46
|
`;
|
|
42
47
|
const DASHBOARD_HELP = `
|
|
@@ -49,7 +54,8 @@ Options:
|
|
|
49
54
|
-c, --config <path> Airlock config file (default: ./airlock.yaml)
|
|
50
55
|
-p, --port <port> Web UI port (default: 4177)
|
|
51
56
|
--host <host> Bind host (default: 127.0.0.1)
|
|
52
|
-
--
|
|
57
|
+
--insecure-remote-bind Allow binding the unauthenticated dashboard beyond loopback
|
|
58
|
+
--gateway-url <url> Gateway management API URL (default: http://127.0.0.1:4113)
|
|
53
59
|
--gateway-secret <secret> Gateway admin bearer token (default: AIRLOCK_GATEWAY_SECRET or AIRLOCK_API_SECRET)
|
|
54
60
|
-h, --help Show this help
|
|
55
61
|
`;
|
|
@@ -63,8 +69,9 @@ export async function runDashboard(argv) {
|
|
|
63
69
|
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
64
70
|
port: { type: 'string', short: 'p', default: '4177' },
|
|
65
71
|
host: { type: 'string', default: '127.0.0.1' },
|
|
66
|
-
'gateway-url': { type: 'string', default: 'http://127.0.0.1:
|
|
72
|
+
'gateway-url': { type: 'string', default: 'http://127.0.0.1:4113' },
|
|
67
73
|
'gateway-secret': { type: 'string' },
|
|
74
|
+
'insecure-remote-bind': { type: 'boolean', default: false },
|
|
68
75
|
help: { type: 'boolean', short: 'h', default: false },
|
|
69
76
|
},
|
|
70
77
|
allowPositionals: false,
|
|
@@ -76,13 +83,14 @@ export async function runDashboard(argv) {
|
|
|
76
83
|
const configPath = values.config ?? './airlock.yaml';
|
|
77
84
|
const port = Number(values.port ?? '4177');
|
|
78
85
|
const host = values.host ?? '127.0.0.1';
|
|
79
|
-
const gatewayUrl = values['gateway-url'] ?? 'http://127.0.0.1:
|
|
86
|
+
const gatewayUrl = values['gateway-url'] ?? 'http://127.0.0.1:4113';
|
|
80
87
|
const gatewaySecret = values['gateway-secret'] ??
|
|
81
88
|
process.env['AIRLOCK_GATEWAY_SECRET'] ??
|
|
82
89
|
process.env['AIRLOCK_API_SECRET'];
|
|
83
90
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
84
91
|
throw new Error(`Invalid --port: ${values.port}`);
|
|
85
92
|
}
|
|
93
|
+
assertSafeWebBindHost(host, values['insecure-remote-bind'] === true);
|
|
86
94
|
const app = createConfigureWebApp(configPath, {
|
|
87
95
|
remoteGateway: { url: gatewayUrl, secret: gatewaySecret },
|
|
88
96
|
});
|
|
@@ -98,6 +106,7 @@ export async function runConfigureWeb(argv, entrypoint = 'configure-web') {
|
|
|
98
106
|
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
99
107
|
port: { type: 'string', short: 'p', default: '4177' },
|
|
100
108
|
host: { type: 'string', default: '127.0.0.1' },
|
|
109
|
+
'insecure-remote-bind': { type: 'boolean', default: false },
|
|
101
110
|
help: { type: 'boolean', short: 'h', default: false },
|
|
102
111
|
},
|
|
103
112
|
allowPositionals: false,
|
|
@@ -112,11 +121,28 @@ export async function runConfigureWeb(argv, entrypoint = 'configure-web') {
|
|
|
112
121
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
113
122
|
throw new Error(`Invalid --port: ${values.port}`);
|
|
114
123
|
}
|
|
124
|
+
assertSafeWebBindHost(host, values['insecure-remote-bind'] === true);
|
|
115
125
|
const app = createConfigureWebApp(configPath);
|
|
116
126
|
await app.listen({ host, port });
|
|
117
127
|
console.log(`Airlock command center running at http://${host}:${port}`);
|
|
118
128
|
console.log(`Editing ${configPath}`);
|
|
119
129
|
}
|
|
130
|
+
export function assertSafeWebBindHost(host, insecureRemoteBind) {
|
|
131
|
+
if (insecureRemoteBind || isLoopbackBindHost(host))
|
|
132
|
+
return;
|
|
133
|
+
throw new Error('--host binds the unauthenticated Airlock web UI beyond loopback. Use --insecure-remote-bind only when the port is protected by a private network, tunnel, or authenticated reverse proxy.');
|
|
134
|
+
}
|
|
135
|
+
function isLoopbackBindHost(host) {
|
|
136
|
+
const normalized = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
|
|
137
|
+
if (normalized === 'localhost')
|
|
138
|
+
return true;
|
|
139
|
+
const ipVersion = isIP(normalized);
|
|
140
|
+
if (ipVersion === 6)
|
|
141
|
+
return normalized === '::1';
|
|
142
|
+
if (ipVersion !== 4)
|
|
143
|
+
return false;
|
|
144
|
+
return Number(normalized.split('.')[0]) === 127;
|
|
145
|
+
}
|
|
120
146
|
export function createConfigureWebApp(configPath, options = {}) {
|
|
121
147
|
const app = Fastify({ logger: false });
|
|
122
148
|
app.get('/', async (_request, reply) => {
|
|
@@ -152,6 +178,18 @@ export function createConfigureWebApp(configPath, options = {}) {
|
|
|
152
178
|
};
|
|
153
179
|
}
|
|
154
180
|
});
|
|
181
|
+
app.get('/api/activity', async (_request, reply) => {
|
|
182
|
+
try {
|
|
183
|
+
if (options.remoteGateway) {
|
|
184
|
+
return await remoteGatewayJson(options.remoteGateway, '/activity');
|
|
185
|
+
}
|
|
186
|
+
return { events: [] };
|
|
187
|
+
}
|
|
188
|
+
catch (err) {
|
|
189
|
+
reply.code(500);
|
|
190
|
+
return { events: [], error: err instanceof Error ? err.message : String(err) };
|
|
191
|
+
}
|
|
192
|
+
});
|
|
155
193
|
app.get('/api/tools', async (_request, reply) => {
|
|
156
194
|
try {
|
|
157
195
|
if (options.remoteGateway) {
|
|
@@ -483,7 +521,7 @@ async function rememberRemoteDecision(configPath, remoteGateway, code, remember,
|
|
|
483
521
|
return { status: 400, error: 'Invalid remember mode' };
|
|
484
522
|
}
|
|
485
523
|
const pending = await remoteGatewayJson(remoteGateway, '/hitl/pending');
|
|
486
|
-
const request = pending.find((entry) => entry.code === code
|
|
524
|
+
const request = pending.find((entry) => entry.code === code);
|
|
487
525
|
if (!request)
|
|
488
526
|
return { status: 404, error: 'No pending request found for code' };
|
|
489
527
|
const durationMs = typeof durationMsValue === 'string' && durationMsValue.trim()
|
|
@@ -1310,6 +1348,17 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1310
1348
|
border-color: #f1d99a;
|
|
1311
1349
|
background: #fffaf0;
|
|
1312
1350
|
}
|
|
1351
|
+
.log-row.question {
|
|
1352
|
+
border-color: #c8d7ef;
|
|
1353
|
+
background: #f3f7ff;
|
|
1354
|
+
}
|
|
1355
|
+
.log-row.notification {
|
|
1356
|
+
border-color: #c8d7ef;
|
|
1357
|
+
background: #f7fbff;
|
|
1358
|
+
}
|
|
1359
|
+
.log-row.log {
|
|
1360
|
+
background: #fbfcfe;
|
|
1361
|
+
}
|
|
1313
1362
|
.log-cell {
|
|
1314
1363
|
min-width: 0;
|
|
1315
1364
|
overflow-wrap: anywhere;
|
|
@@ -1873,8 +1922,9 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1873
1922
|
config: null,
|
|
1874
1923
|
status: null,
|
|
1875
1924
|
audit: { dbPath: '', entries: [], pending: [] },
|
|
1876
|
-
|
|
1877
|
-
|
|
1925
|
+
activityEvents: [],
|
|
1926
|
+
livePendingById: {},
|
|
1927
|
+
currentApprovalId: '',
|
|
1878
1928
|
activityAgentId: '',
|
|
1879
1929
|
auditFilters: { agent: '', tool: '', since: '', limit: '50' },
|
|
1880
1930
|
tools: [],
|
|
@@ -1959,7 +2009,13 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1959
2009
|
for (const [key, value] of Object.entries(state.auditFilters)) {
|
|
1960
2010
|
if (String(value || '').trim()) params.set(key, String(value).trim());
|
|
1961
2011
|
}
|
|
1962
|
-
|
|
2012
|
+
const [audit, activity] = await Promise.all([
|
|
2013
|
+
api('/api/logs' + (params.toString() ? '?' + params.toString() : '')),
|
|
2014
|
+
api('/api/activity')
|
|
2015
|
+
]);
|
|
2016
|
+
state.audit = audit;
|
|
2017
|
+
state.activityEvents = Array.isArray(activity.events) ? activity.events : [];
|
|
2018
|
+
reconcileLivePendingWithSnapshot();
|
|
1963
2019
|
render();
|
|
1964
2020
|
} catch (error) {
|
|
1965
2021
|
alert(error.message);
|
|
@@ -2046,26 +2102,41 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2046
2102
|
}
|
|
2047
2103
|
|
|
2048
2104
|
function pendingApprovals(agentId = '') {
|
|
2049
|
-
const
|
|
2050
|
-
for (const request of Object.values(state.livePending || {})) {
|
|
2051
|
-
const pending = normalizePending(request, true);
|
|
2052
|
-
if (pending.code && (!agentId || pending.agentId === agentId)) byCode.set(pending.code, pending);
|
|
2053
|
-
}
|
|
2105
|
+
const byId = new Map();
|
|
2054
2106
|
for (const request of state.audit.pending || []) {
|
|
2055
2107
|
const pending = normalizePending(request, false);
|
|
2056
|
-
if (pending.
|
|
2108
|
+
if (pending.id && (!agentId || pending.agentId === agentId)) byId.set(pending.id, pending);
|
|
2109
|
+
}
|
|
2110
|
+
for (const request of Object.values(state.livePendingById || {})) {
|
|
2111
|
+
const pending = normalizePending(request, true);
|
|
2112
|
+
if (pending.id && (!agentId || pending.agentId === agentId) && !byId.has(pending.id)) {
|
|
2113
|
+
byId.set(pending.id, pending);
|
|
2114
|
+
}
|
|
2115
|
+
}
|
|
2116
|
+
return Array.from(byId.values());
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
function reconcileLivePendingWithSnapshot() {
|
|
2120
|
+
const snapshotIds = new Set(
|
|
2121
|
+
(state.audit.pending || [])
|
|
2122
|
+
.map((request) => normalizePending(request, false).id)
|
|
2123
|
+
.filter(Boolean)
|
|
2124
|
+
);
|
|
2125
|
+
for (const id of Object.keys(state.livePendingById || {})) {
|
|
2126
|
+
if (!snapshotIds.has(id)) delete state.livePendingById[id];
|
|
2057
2127
|
}
|
|
2058
|
-
return Array.from(byCode.values());
|
|
2059
2128
|
}
|
|
2060
2129
|
|
|
2061
2130
|
function normalizePending(request, live) {
|
|
2062
2131
|
const args = live ? request.args : parseArgs(request.args);
|
|
2132
|
+
const context = request.context || airlockContext(args);
|
|
2063
2133
|
return {
|
|
2064
2134
|
id: request.id || '',
|
|
2065
2135
|
code: request.code || '',
|
|
2066
2136
|
agentId: request.agentId || request.agent_id || '',
|
|
2067
2137
|
tool: request.tool || '',
|
|
2068
|
-
args,
|
|
2138
|
+
args: stripAirlockContext(args),
|
|
2139
|
+
context,
|
|
2069
2140
|
status: request.status || 'pending',
|
|
2070
2141
|
createdAt: request.createdAt || request.created_at || '',
|
|
2071
2142
|
timeoutMs: request.timeoutMs || 0,
|
|
@@ -2084,19 +2155,37 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2084
2155
|
}
|
|
2085
2156
|
}
|
|
2086
2157
|
|
|
2158
|
+
function airlockContext(args) {
|
|
2159
|
+
const raw = args && typeof args === 'object' ? args._airlock : undefined;
|
|
2160
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return undefined;
|
|
2161
|
+
const context = {};
|
|
2162
|
+
if (typeof raw.reason === 'string' && raw.reason.trim()) context.reason = raw.reason.trim();
|
|
2163
|
+
if (typeof raw.note === 'string' && raw.note.trim()) context.note = raw.note.trim();
|
|
2164
|
+
return context.reason || context.note ? context : undefined;
|
|
2165
|
+
}
|
|
2166
|
+
|
|
2167
|
+
function stripAirlockContext(args) {
|
|
2168
|
+
if (!args || typeof args !== 'object' || Array.isArray(args) || !('_airlock' in args)) return args;
|
|
2169
|
+
const clean = { ...args };
|
|
2170
|
+
delete clean._airlock;
|
|
2171
|
+
return clean;
|
|
2172
|
+
}
|
|
2173
|
+
|
|
2087
2174
|
function renderActivity() {
|
|
2088
2175
|
el('activitySurface').hidden = state.activeKind !== 'activity';
|
|
2089
2176
|
renderActivityAgentFilter();
|
|
2090
2177
|
const agentId = state.auditFilters.agent || state.activityAgentId || '';
|
|
2091
2178
|
const pending = pendingApprovals(agentId);
|
|
2092
2179
|
const entries = state.audit.entries || [];
|
|
2180
|
+
const activity = activityEvents(agentId);
|
|
2093
2181
|
const rows = [
|
|
2094
2182
|
...pending.map(pendingRow),
|
|
2183
|
+
...activity.map(activityRow),
|
|
2095
2184
|
...entries.map(auditRow)
|
|
2096
2185
|
];
|
|
2097
2186
|
el('pendingBadge').textContent = 'pending ' + pending.length;
|
|
2098
2187
|
el('activityMeta').textContent = state.audit.dbPath
|
|
2099
|
-
? 'Audit log: ' + state.audit.dbPath + (agentId ? ' · agent ' + agentId : '')
|
|
2188
|
+
? 'Activity events ' + activity.length + ' · Audit log: ' + state.audit.dbPath + (agentId ? ' · agent ' + agentId : '')
|
|
2100
2189
|
: 'Audit log not loaded yet';
|
|
2101
2190
|
el('activityList').innerHTML = rows.length
|
|
2102
2191
|
? rows.join('')
|
|
@@ -2104,7 +2193,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2104
2193
|
document.querySelectorAll('[data-pending-row]').forEach((row) => {
|
|
2105
2194
|
row.addEventListener('click', (event) => {
|
|
2106
2195
|
if (event.target.closest('[data-approval-action]')) return;
|
|
2107
|
-
openApprovalModal(row.dataset.
|
|
2196
|
+
openApprovalModal(row.dataset.id);
|
|
2108
2197
|
});
|
|
2109
2198
|
});
|
|
2110
2199
|
document.querySelectorAll('[data-approval-action]').forEach((button) => {
|
|
@@ -2120,6 +2209,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2120
2209
|
});
|
|
2121
2210
|
}
|
|
2122
2211
|
|
|
2212
|
+
function activityEvents(agentId = '') {
|
|
2213
|
+
return (state.activityEvents || []).filter((event) => !agentId || event.agentId === agentId);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
2123
2216
|
function renderActivityAgentFilter() {
|
|
2124
2217
|
const agents = Object.keys(state.config.agents);
|
|
2125
2218
|
if (!state.activityAgentId) state.activityAgentId = agents[0] || '';
|
|
@@ -2131,25 +2224,29 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2131
2224
|
}
|
|
2132
2225
|
|
|
2133
2226
|
function pendingRow(entry) {
|
|
2134
|
-
|
|
2135
|
-
|
|
2227
|
+
const question = isUserQuestion(entry);
|
|
2228
|
+
return '<div class="log-row pending' + (question ? ' question' : '') + '" data-pending-row data-id="' + escapeHtml(entry.id) + '" data-code="' + escapeHtml(entry.code) + '">' +
|
|
2229
|
+
'<div class="log-cell"><strong>' + escapeHtml(question ? 'pending question' : 'pending approval') + '</strong><br>' + escapeHtml(formatTime(entry.createdAt)) + '</div>' +
|
|
2136
2230
|
'<div class="log-cell">' + escapeHtml(entry.agentId) + '</div>' +
|
|
2137
|
-
'<div class="log-cell">' + escapeHtml(entry.tool) + '<br><span class="subtle">
|
|
2138
|
-
'<div class="log-cell"><span class="tag tag-warn">' + escapeHtml(entry.status) + '</span></div>' +
|
|
2139
|
-
'<pre class="log-args">' + escapeHtml(
|
|
2140
|
-
'<div class="approval-actions">' + approvalButtons(entry.code) + '</div>' +
|
|
2231
|
+
'<div class="log-cell">' + escapeHtml(question ? questionText(entry) : entry.tool) + '<br><span class="subtle">' + escapeHtml(question ? entry.tool : 'code ' + entry.code) + '</span></div>' +
|
|
2232
|
+
'<div class="log-cell"><span class="tag tag-warn">' + escapeHtml(question ? 'question' : entry.status) + '</span></div>' +
|
|
2233
|
+
'<pre class="log-args">' + escapeHtml(question ? questionPreview(entry) : approvalPreview(entry)) + '</pre>' +
|
|
2234
|
+
'<div class="approval-actions">' + approvalButtons(entry.code, { remember: !question, approveLabel: question ? 'Confirm' : 'Approve' }) + '</div>' +
|
|
2141
2235
|
'</div>';
|
|
2142
2236
|
}
|
|
2143
2237
|
|
|
2144
|
-
function approvalButtons(code) {
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2238
|
+
function approvalButtons(code, options = {}) {
|
|
2239
|
+
const approveLabel = options.approveLabel || 'Approve';
|
|
2240
|
+
const remember = options.remember !== false;
|
|
2241
|
+
return '<button class="approve" data-approval-action="approve" data-code="' + escapeHtml(code) + '">' + escapeHtml(approveLabel) + '</button>' +
|
|
2242
|
+
(remember ? '<button class="approve" data-approval-action="approve" data-code="' + escapeHtml(code) + '" data-remember="temporary" data-duration="3600000">Allow 1h</button>' : '') +
|
|
2243
|
+
(remember ? '<button class="approve" data-approval-action="approve" data-code="' + escapeHtml(code) + '" data-remember="always">Always Allow</button>' : '') +
|
|
2148
2244
|
'<button class="deny" data-approval-action="deny" data-code="' + escapeHtml(code) + '">Deny</button>';
|
|
2149
2245
|
}
|
|
2150
2246
|
|
|
2151
2247
|
async function actApproval(action, code, remember, durationMs) {
|
|
2152
2248
|
if (!code) return;
|
|
2249
|
+
const pending = findPendingApproval(code);
|
|
2153
2250
|
const params = new URLSearchParams({ code });
|
|
2154
2251
|
if (remember) params.set('remember', remember);
|
|
2155
2252
|
if (durationMs) params.set('duration_ms', durationMs);
|
|
@@ -2164,7 +2261,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2164
2261
|
}
|
|
2165
2262
|
throw new Error(message);
|
|
2166
2263
|
}
|
|
2167
|
-
delete state.
|
|
2264
|
+
if (pending?.id) delete state.livePendingById[pending.id];
|
|
2168
2265
|
closeApprovalModal();
|
|
2169
2266
|
await Promise.all([refreshLogs(), refreshStatus()]);
|
|
2170
2267
|
render();
|
|
@@ -2172,21 +2269,26 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2172
2269
|
|
|
2173
2270
|
function findPendingApproval(code) {
|
|
2174
2271
|
const agentId = state.activeKind === 'activity' ? state.auditFilters.agent || '' : '';
|
|
2175
|
-
return pendingApprovals(agentId).find((entry) => entry.code === code);
|
|
2272
|
+
return pendingApprovals(agentId).find((entry) => entry.id === code || entry.code === code);
|
|
2176
2273
|
}
|
|
2177
2274
|
|
|
2178
|
-
function openApprovalModal(
|
|
2179
|
-
const pending = findPendingApproval(
|
|
2275
|
+
function openApprovalModal(id) {
|
|
2276
|
+
const pending = findPendingApproval(id);
|
|
2180
2277
|
if (!pending) return;
|
|
2181
|
-
state.
|
|
2182
|
-
|
|
2278
|
+
state.currentApprovalId = pending.id;
|
|
2279
|
+
const question = isUserQuestion(pending);
|
|
2280
|
+
el('approvalModalTitle').textContent = question ? questionText(pending) : pending.tool;
|
|
2183
2281
|
el('approvalModalMeta').textContent = 'agent ' + pending.agentId + ' · code ' + pending.code;
|
|
2184
2282
|
el('approvalModalBody').innerHTML =
|
|
2185
2283
|
'<div class="detail-label">Status</div><div>' + escapeHtml(pending.status) + '</div>' +
|
|
2186
2284
|
'<div class="detail-label">Created</div><div>' + escapeHtml(formatTime(pending.createdAt)) + '</div>' +
|
|
2187
2285
|
(pending.timeoutMs ? '<div class="detail-label">Timeout</div><div>' + Math.round(pending.timeoutMs / 1000) + 's</div>' : '') +
|
|
2286
|
+
(requestReason(pending) ? '<div class="detail-label">Request reason</div><div>' + escapeHtml(requestReason(pending)) + '</div>' : '') +
|
|
2287
|
+
(requestNote(pending) ? '<div class="detail-label">Request note</div><div>' + escapeHtml(requestNote(pending)) + '</div>' : '') +
|
|
2288
|
+
(question ? '<div class="detail-label">Question</div><div>' + escapeHtml(questionText(pending)) + '</div>' : '') +
|
|
2289
|
+
(question && questionContext(pending) ? '<div class="detail-label">Context</div><div>' + escapeHtml(questionContext(pending)) + '</div>' : '') +
|
|
2188
2290
|
'<div class="detail-label">Arguments</div><pre class="log-args">' + escapeHtml(prettyJson(pending.args)) + '</pre>';
|
|
2189
|
-
el('approvalModalFooter').innerHTML = approvalButtons(pending.code);
|
|
2291
|
+
el('approvalModalFooter').innerHTML = approvalButtons(pending.code, { remember: !question, approveLabel: question ? 'Confirm' : 'Approve' });
|
|
2190
2292
|
el('approvalModal').classList.add('open');
|
|
2191
2293
|
el('approvalModalFooter').querySelectorAll('[data-approval-action]').forEach((button) => {
|
|
2192
2294
|
button.addEventListener('click', () => {
|
|
@@ -2202,7 +2304,58 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2202
2304
|
|
|
2203
2305
|
function closeApprovalModal() {
|
|
2204
2306
|
el('approvalModal').classList.remove('open');
|
|
2205
|
-
state.
|
|
2307
|
+
state.currentApprovalId = '';
|
|
2308
|
+
}
|
|
2309
|
+
|
|
2310
|
+
function isUserQuestion(entry) {
|
|
2311
|
+
return entry.tool === 'airlock/ask_user';
|
|
2312
|
+
}
|
|
2313
|
+
|
|
2314
|
+
function questionText(entry) {
|
|
2315
|
+
const value = entry.args?.question ?? entry.args?.title ?? entry.args?.message;
|
|
2316
|
+
return typeof value === 'string' && value.trim() ? value.trim() : 'Question from agent';
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
function questionContext(entry) {
|
|
2320
|
+
const value = entry.args?.context ?? entry.args?.reason;
|
|
2321
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
2322
|
+
}
|
|
2323
|
+
|
|
2324
|
+
function questionPreview(entry) {
|
|
2325
|
+
const lines = ['question: ' + questionText(entry)];
|
|
2326
|
+
const context = questionContext(entry);
|
|
2327
|
+
if (context) lines.push('context: ' + context);
|
|
2328
|
+
return lines.join('\\n');
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
function requestReason(entry) {
|
|
2332
|
+
const value = entry.context?.reason;
|
|
2333
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
2334
|
+
}
|
|
2335
|
+
|
|
2336
|
+
function requestNote(entry) {
|
|
2337
|
+
const value = entry.context?.note;
|
|
2338
|
+
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
2339
|
+
}
|
|
2340
|
+
|
|
2341
|
+
function approvalPreview(entry) {
|
|
2342
|
+
const lines = [];
|
|
2343
|
+
const reason = requestReason(entry);
|
|
2344
|
+
const note = requestNote(entry);
|
|
2345
|
+
if (reason) lines.push('request reason: ' + reason);
|
|
2346
|
+
if (note) lines.push('request note: ' + note);
|
|
2347
|
+
lines.push(prettyJson(entry.args));
|
|
2348
|
+
return lines.join('\\n');
|
|
2349
|
+
}
|
|
2350
|
+
|
|
2351
|
+
function activityRow(event) {
|
|
2352
|
+
const tag = event.kind === 'notification' ? 'tag-warn' : resultTagClass(event.severity || 'info');
|
|
2353
|
+
return '<div class="log-row ' + escapeHtml(event.kind) + '">' +
|
|
2354
|
+
'<div class="log-cell"><strong>' + escapeHtml(event.kind) + '</strong><br>' + escapeHtml(formatTime(event.createdAt)) + '</div>' +
|
|
2355
|
+
'<div class="log-cell">' + escapeHtml(event.agentId || '') + '</div>' +
|
|
2356
|
+
'<div class="log-cell">' + escapeHtml(event.title || '') + '<br><span class="subtle">' + escapeHtml(event.body || '') + '</span></div>' +
|
|
2357
|
+
'<div class="log-cell"><span class="tag ' + tag + '">' + escapeHtml(event.severity || 'info') + '</span></div>' +
|
|
2358
|
+
'</div>';
|
|
2206
2359
|
}
|
|
2207
2360
|
|
|
2208
2361
|
function auditRow(entry) {
|
|
@@ -2874,17 +3027,31 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2874
3027
|
events.onmessage = (event) => {
|
|
2875
3028
|
const message = JSON.parse(event.data);
|
|
2876
3029
|
if (message.type === 'new') {
|
|
2877
|
-
|
|
2878
|
-
|
|
2879
|
-
|
|
2880
|
-
|
|
3030
|
+
const request = message.request || {};
|
|
3031
|
+
if (request.id) {
|
|
3032
|
+
state.livePendingById[request.id] = request;
|
|
3033
|
+
notifyApproval(request);
|
|
3034
|
+
refreshLogs().catch(() => {});
|
|
3035
|
+
render();
|
|
3036
|
+
}
|
|
2881
3037
|
}
|
|
2882
3038
|
if (message.type === 'resolved') {
|
|
2883
|
-
|
|
3039
|
+
if (message.id) {
|
|
3040
|
+
delete state.livePendingById[message.id];
|
|
3041
|
+
} else if (message.code) {
|
|
3042
|
+
for (const [id, request] of Object.entries(state.livePendingById || {})) {
|
|
3043
|
+
if (request && request.code === message.code) delete state.livePendingById[id];
|
|
3044
|
+
}
|
|
3045
|
+
}
|
|
2884
3046
|
refreshLogs().catch(() => {});
|
|
2885
3047
|
refreshStatus().catch(() => {});
|
|
2886
3048
|
render();
|
|
2887
3049
|
}
|
|
3050
|
+
if (message.type === 'activity') {
|
|
3051
|
+
upsertActivityEvent(message.event);
|
|
3052
|
+
notifyActivity(message.event);
|
|
3053
|
+
render();
|
|
3054
|
+
}
|
|
2888
3055
|
};
|
|
2889
3056
|
events.onerror = () => {
|
|
2890
3057
|
events.close();
|
|
@@ -2894,13 +3061,36 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2894
3061
|
function notifyApproval(request) {
|
|
2895
3062
|
if (!el('approvalNotifs').checked) return;
|
|
2896
3063
|
if (!('Notification' in window) || Notification.permission !== 'granted') return;
|
|
3064
|
+
const pending = normalizePending(request, true);
|
|
3065
|
+
const lines = ['agent: ' + pending.agentId, pending.code];
|
|
3066
|
+
const reason = requestReason(pending);
|
|
3067
|
+
if (reason) lines.push('request reason: ' + reason);
|
|
2897
3068
|
new Notification('Airlock: ' + request.tool, {
|
|
2898
|
-
body:
|
|
2899
|
-
tag: request.code,
|
|
3069
|
+
body: lines.join('\\n'),
|
|
3070
|
+
tag: request.id || request.code,
|
|
3071
|
+
silent: !el('approvalSound').checked
|
|
3072
|
+
});
|
|
3073
|
+
}
|
|
3074
|
+
|
|
3075
|
+
function notifyActivity(event) {
|
|
3076
|
+
if (!event || event.kind !== 'notification') return;
|
|
3077
|
+
if (!el('approvalNotifs').checked) return;
|
|
3078
|
+
if (!('Notification' in window) || Notification.permission !== 'granted') return;
|
|
3079
|
+
new Notification(event.title || 'Airlock notification', {
|
|
3080
|
+
body: event.body || '',
|
|
3081
|
+
tag: event.id,
|
|
2900
3082
|
silent: !el('approvalSound').checked
|
|
2901
3083
|
});
|
|
2902
3084
|
}
|
|
2903
3085
|
|
|
3086
|
+
function upsertActivityEvent(event) {
|
|
3087
|
+
if (!event || !event.id) return;
|
|
3088
|
+
state.activityEvents = [
|
|
3089
|
+
event,
|
|
3090
|
+
...(state.activityEvents || []).filter((existing) => existing.id !== event.id)
|
|
3091
|
+
].slice(0, 200);
|
|
3092
|
+
}
|
|
3093
|
+
|
|
2904
3094
|
async function checkVersion() {
|
|
2905
3095
|
try {
|
|
2906
3096
|
const current = await fetch('/version').then((response) => response.json());
|
|
@@ -2923,10 +3113,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2923
3113
|
return false;
|
|
2924
3114
|
}
|
|
2925
3115
|
|
|
2926
|
-
function
|
|
3116
|
+
function firstPendingApprovalId() {
|
|
2927
3117
|
const agentId = state.activeKind === 'activity' ? state.auditFilters.agent || '' : '';
|
|
2928
3118
|
const first = pendingApprovals(agentId)[0];
|
|
2929
|
-
return first ? first.
|
|
3119
|
+
return first ? first.id : '';
|
|
2930
3120
|
}
|
|
2931
3121
|
|
|
2932
3122
|
el('refreshTools').addEventListener('click', refreshTools);
|
|
@@ -2981,9 +3171,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2981
3171
|
if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return;
|
|
2982
3172
|
const key = event.key.toLowerCase();
|
|
2983
3173
|
if (key !== 'a' && key !== 'd') return;
|
|
2984
|
-
const
|
|
2985
|
-
|
|
2986
|
-
|
|
3174
|
+
const id = state.currentApprovalId || firstPendingApprovalId();
|
|
3175
|
+
const pending = findPendingApproval(id);
|
|
3176
|
+
if (!pending?.code) return;
|
|
3177
|
+
actApproval(key === 'a' ? 'approve' : 'deny', pending.code).catch((error) => alert(error.message));
|
|
2987
3178
|
});
|
|
2988
3179
|
|
|
2989
3180
|
initApprovalSettings();
|