airlock-bot 0.2.36 → 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 +34 -7
- 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 +46 -8
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +367 -23
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/schema.d.ts +3542 -174
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +292 -63
- package/dist/config/schema.js.map +1 -1
- package/dist/configure-web/cli.d.ts +13 -0
- package/dist/configure-web/cli.d.ts.map +1 -1
- package/dist/configure-web/cli.js +570 -80
- 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 +8 -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 +13 -0
- package/dist/middleware/core/arg-policy.d.ts.map +1 -0
- package/dist/middleware/core/arg-policy.js +201 -0
- package/dist/middleware/core/arg-policy.js.map +1 -0
- 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 +4 -4
- package/extensions/openclaw/index.ts +9 -6
- package/extensions/openclaw/openclaw.plugin.json +5 -5
- package/package.json +3 -1
- package/schema.json +658 -8
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
1
2
|
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
|
|
3
|
+
import { isIP } from 'net';
|
|
2
4
|
import { parseArgs } from 'util';
|
|
3
5
|
import Fastify from 'fastify';
|
|
4
6
|
import { parse as parseYaml, stringify as stringifyYaml } from 'yaml';
|
|
@@ -7,8 +9,10 @@ import { buildAdapters } from '../backend/factory.js';
|
|
|
7
9
|
import { AuditConfig, GatewayConfig, McpServerConfig, ProviderConfig, getMcpConfigs, } from '../config/schema.js';
|
|
8
10
|
import { rememberAllow } from '../config/mutator.js';
|
|
9
11
|
import { validateConfig } from '../config/loader.js';
|
|
12
|
+
import { applyProfiles } from '../config/profiles.js';
|
|
10
13
|
import { ClientPool } from '../pool/pool.js';
|
|
11
14
|
import { checkSuspiciousPatterns } from '../registry/sanitizer.js';
|
|
15
|
+
import { bestSpecificity } from '../allowlist/pattern.js';
|
|
12
16
|
import { VERSION } from '../version.js';
|
|
13
17
|
const LATEST_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
14
18
|
let latestVersionCache = null;
|
|
@@ -22,6 +26,8 @@ Options:
|
|
|
22
26
|
-c, --config <path> Airlock config file (default: ./airlock.yaml)
|
|
23
27
|
-p, --port <port> Web UI port (default: 4177)
|
|
24
28
|
--host <host> Bind host (default: 127.0.0.1)
|
|
29
|
+
--insecure-remote-bind
|
|
30
|
+
Allow binding the unauthenticated UI beyond loopback
|
|
25
31
|
-h, --help Show this help
|
|
26
32
|
`;
|
|
27
33
|
const RUN_HELP = `
|
|
@@ -34,6 +40,8 @@ Options:
|
|
|
34
40
|
-c, --config <path> Airlock config file (default: ./airlock.yaml)
|
|
35
41
|
-p, --port <port> Web UI port (default: 4177)
|
|
36
42
|
--host <host> Bind host (default: 127.0.0.1)
|
|
43
|
+
--insecure-remote-bind
|
|
44
|
+
Allow binding the unauthenticated UI beyond loopback
|
|
37
45
|
-h, --help Show this help
|
|
38
46
|
`;
|
|
39
47
|
const DASHBOARD_HELP = `
|
|
@@ -46,7 +54,8 @@ Options:
|
|
|
46
54
|
-c, --config <path> Airlock config file (default: ./airlock.yaml)
|
|
47
55
|
-p, --port <port> Web UI port (default: 4177)
|
|
48
56
|
--host <host> Bind host (default: 127.0.0.1)
|
|
49
|
-
--
|
|
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)
|
|
50
59
|
--gateway-secret <secret> Gateway admin bearer token (default: AIRLOCK_GATEWAY_SECRET or AIRLOCK_API_SECRET)
|
|
51
60
|
-h, --help Show this help
|
|
52
61
|
`;
|
|
@@ -60,8 +69,9 @@ export async function runDashboard(argv) {
|
|
|
60
69
|
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
61
70
|
port: { type: 'string', short: 'p', default: '4177' },
|
|
62
71
|
host: { type: 'string', default: '127.0.0.1' },
|
|
63
|
-
'gateway-url': { type: 'string', default: 'http://127.0.0.1:
|
|
72
|
+
'gateway-url': { type: 'string', default: 'http://127.0.0.1:4113' },
|
|
64
73
|
'gateway-secret': { type: 'string' },
|
|
74
|
+
'insecure-remote-bind': { type: 'boolean', default: false },
|
|
65
75
|
help: { type: 'boolean', short: 'h', default: false },
|
|
66
76
|
},
|
|
67
77
|
allowPositionals: false,
|
|
@@ -73,11 +83,14 @@ export async function runDashboard(argv) {
|
|
|
73
83
|
const configPath = values.config ?? './airlock.yaml';
|
|
74
84
|
const port = Number(values.port ?? '4177');
|
|
75
85
|
const host = values.host ?? '127.0.0.1';
|
|
76
|
-
const gatewayUrl = values['gateway-url'] ?? 'http://127.0.0.1:
|
|
77
|
-
const gatewaySecret = values['gateway-secret'] ??
|
|
86
|
+
const gatewayUrl = values['gateway-url'] ?? 'http://127.0.0.1:4113';
|
|
87
|
+
const gatewaySecret = values['gateway-secret'] ??
|
|
88
|
+
process.env['AIRLOCK_GATEWAY_SECRET'] ??
|
|
89
|
+
process.env['AIRLOCK_API_SECRET'];
|
|
78
90
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
79
91
|
throw new Error(`Invalid --port: ${values.port}`);
|
|
80
92
|
}
|
|
93
|
+
assertSafeWebBindHost(host, values['insecure-remote-bind'] === true);
|
|
81
94
|
const app = createConfigureWebApp(configPath, {
|
|
82
95
|
remoteGateway: { url: gatewayUrl, secret: gatewaySecret },
|
|
83
96
|
});
|
|
@@ -93,6 +106,7 @@ export async function runConfigureWeb(argv, entrypoint = 'configure-web') {
|
|
|
93
106
|
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
94
107
|
port: { type: 'string', short: 'p', default: '4177' },
|
|
95
108
|
host: { type: 'string', default: '127.0.0.1' },
|
|
109
|
+
'insecure-remote-bind': { type: 'boolean', default: false },
|
|
96
110
|
help: { type: 'boolean', short: 'h', default: false },
|
|
97
111
|
},
|
|
98
112
|
allowPositionals: false,
|
|
@@ -107,11 +121,28 @@ export async function runConfigureWeb(argv, entrypoint = 'configure-web') {
|
|
|
107
121
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
108
122
|
throw new Error(`Invalid --port: ${values.port}`);
|
|
109
123
|
}
|
|
124
|
+
assertSafeWebBindHost(host, values['insecure-remote-bind'] === true);
|
|
110
125
|
const app = createConfigureWebApp(configPath);
|
|
111
126
|
await app.listen({ host, port });
|
|
112
127
|
console.log(`Airlock command center running at http://${host}:${port}`);
|
|
113
128
|
console.log(`Editing ${configPath}`);
|
|
114
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
|
+
}
|
|
115
146
|
export function createConfigureWebApp(configPath, options = {}) {
|
|
116
147
|
const app = Fastify({ logger: false });
|
|
117
148
|
app.get('/', async (_request, reply) => {
|
|
@@ -147,6 +178,18 @@ export function createConfigureWebApp(configPath, options = {}) {
|
|
|
147
178
|
};
|
|
148
179
|
}
|
|
149
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
|
+
});
|
|
150
193
|
app.get('/api/tools', async (_request, reply) => {
|
|
151
194
|
try {
|
|
152
195
|
if (options.remoteGateway) {
|
|
@@ -362,7 +405,7 @@ async function readRemoteCommandCenterStatus(configPath, remoteGateway) {
|
|
|
362
405
|
id,
|
|
363
406
|
type,
|
|
364
407
|
enabled,
|
|
365
|
-
status: enabled ? runtime ?? 'ok' : 'disabled',
|
|
408
|
+
status: enabled ? (runtime ?? 'ok') : 'disabled',
|
|
366
409
|
toolCount: 0,
|
|
367
410
|
toolFingerprint: '',
|
|
368
411
|
...(runtime === 'down' ? { error: 'Provider is down on the gateway' } : {}),
|
|
@@ -430,10 +473,11 @@ function registerRemoteGatewayRoutes(app, configPath, remoteGateway) {
|
|
|
430
473
|
const reader = response.body.getReader();
|
|
431
474
|
try {
|
|
432
475
|
while (true) {
|
|
433
|
-
const
|
|
434
|
-
if (done)
|
|
476
|
+
const result = await reader.read();
|
|
477
|
+
if (result.done)
|
|
435
478
|
break;
|
|
436
|
-
|
|
479
|
+
if (result.value)
|
|
480
|
+
reply.raw.write(Buffer.from(result.value));
|
|
437
481
|
}
|
|
438
482
|
}
|
|
439
483
|
catch {
|
|
@@ -477,7 +521,7 @@ async function rememberRemoteDecision(configPath, remoteGateway, code, remember,
|
|
|
477
521
|
return { status: 400, error: 'Invalid remember mode' };
|
|
478
522
|
}
|
|
479
523
|
const pending = await remoteGatewayJson(remoteGateway, '/hitl/pending');
|
|
480
|
-
const request = pending.find((entry) => entry.code === code
|
|
524
|
+
const request = pending.find((entry) => entry.code === code);
|
|
481
525
|
if (!request)
|
|
482
526
|
return { status: 404, error: 'No pending request found for code' };
|
|
483
527
|
const durationMs = typeof durationMsValue === 'string' && durationMsValue.trim()
|
|
@@ -504,7 +548,8 @@ async function rememberRemoteDecision(configPath, remoteGateway, code, remember,
|
|
|
504
548
|
}
|
|
505
549
|
}
|
|
506
550
|
async function forwardRemoteApproval(remoteGateway, action, code) {
|
|
507
|
-
const
|
|
551
|
+
const params = new URLSearchParams({ code });
|
|
552
|
+
const response = await remoteGatewayFetch(remoteGateway, `/${action}?${params.toString()}`, {
|
|
508
553
|
method: 'POST',
|
|
509
554
|
});
|
|
510
555
|
if (!response.ok)
|
|
@@ -564,6 +609,7 @@ export function saveRules(configPath, body) {
|
|
|
564
609
|
existing.deny = parseStringArray(body.deny, 'deny');
|
|
565
610
|
}
|
|
566
611
|
else {
|
|
612
|
+
existing.extends = parseStringArray(body.extends, 'extends');
|
|
567
613
|
existing.allow = parseStringArray(body.allow, 'allow');
|
|
568
614
|
existing.ask = parseStringArray(body.ask, 'ask');
|
|
569
615
|
existing.deny = parseStringArray(body.deny, 'deny');
|
|
@@ -584,15 +630,32 @@ export function createEntity(configPath, body) {
|
|
|
584
630
|
const section = asMutableRecord(doc[sectionName]);
|
|
585
631
|
if (section[id])
|
|
586
632
|
throw new Error(`${kind} "${id}" already exists`);
|
|
587
|
-
|
|
588
|
-
|
|
633
|
+
const baseId = parseOptionalId(body.baseId);
|
|
634
|
+
const base = baseId && section[baseId] ? structuredClone(section[baseId]) : undefined;
|
|
635
|
+
const createdToken = kind === 'agent' ? createAgentToken() : undefined;
|
|
636
|
+
if (kind === 'agent') {
|
|
637
|
+
const agent = asMutableRecord(base);
|
|
638
|
+
const profileIds = parseOptionalStringArray(body.profileIds, 'profileIds');
|
|
639
|
+
delete agent.token;
|
|
640
|
+
agent.extends = profileIds ?? stringArray(agent.extends);
|
|
641
|
+
agent.allow = stringArray(agent.allow);
|
|
642
|
+
agent.ask = stringArray(agent.ask);
|
|
643
|
+
agent.deny = stringArray(agent.deny);
|
|
644
|
+
agent.token = createdToken;
|
|
645
|
+
section[id] = agent;
|
|
646
|
+
}
|
|
647
|
+
else if (base) {
|
|
648
|
+
section[id] = structuredClone(base);
|
|
589
649
|
}
|
|
590
650
|
else {
|
|
591
|
-
section[id] = { allow: [], ask: [], deny: [] };
|
|
651
|
+
section[id] = { extends: [], allow: [], ask: [], deny: [] };
|
|
592
652
|
}
|
|
593
653
|
doc[sectionName] = section;
|
|
594
654
|
writeValidatedConfig(configPath, doc);
|
|
595
|
-
return
|
|
655
|
+
return {
|
|
656
|
+
...readState(configPath),
|
|
657
|
+
...(createdToken ? { createdToken: { agentId: id, token: createdToken } } : {}),
|
|
658
|
+
};
|
|
596
659
|
}
|
|
597
660
|
export function deleteEntity(configPath, kind, id) {
|
|
598
661
|
const parsedKind = parseKind(kind);
|
|
@@ -685,6 +748,44 @@ export function annotationTags(annotations, suspiciousPatterns = []) {
|
|
|
685
748
|
tags.push('injection');
|
|
686
749
|
return tags;
|
|
687
750
|
}
|
|
751
|
+
export function resolveEditableRuleDecision(rules, toolName) {
|
|
752
|
+
return resolveEditableRuleMatch(rules, toolName).decision;
|
|
753
|
+
}
|
|
754
|
+
export function resolveEditableRuleMatch(rules, toolName) {
|
|
755
|
+
const deny = bestRuleMatch(rules.deny, toolName);
|
|
756
|
+
const ask = bestRuleMatch(rules.ask, toolName);
|
|
757
|
+
const allow = bestRuleMatch(rules.allow, toolName);
|
|
758
|
+
const denySpec = deny?.specificity ?? -1;
|
|
759
|
+
const askSpec = ask?.specificity ?? -1;
|
|
760
|
+
const allowSpec = allow?.specificity ?? -1;
|
|
761
|
+
const best = Math.max(denySpec, askSpec, allowSpec);
|
|
762
|
+
if (best < 0)
|
|
763
|
+
return { decision: 'unset', source: 'none' };
|
|
764
|
+
if (denySpec === best)
|
|
765
|
+
return editableRuleMatch('deny', deny, toolName);
|
|
766
|
+
if (askSpec === best)
|
|
767
|
+
return editableRuleMatch('ask', ask, toolName);
|
|
768
|
+
return editableRuleMatch('allow', allow, toolName);
|
|
769
|
+
}
|
|
770
|
+
function editableRuleMatch(decision, match, toolName) {
|
|
771
|
+
if (!match)
|
|
772
|
+
return { decision, source: 'none' };
|
|
773
|
+
return {
|
|
774
|
+
decision,
|
|
775
|
+
source: match.pattern === toolName ? 'exact' : 'pattern',
|
|
776
|
+
pattern: match.pattern,
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
function bestRuleMatch(patterns, toolName) {
|
|
780
|
+
let best;
|
|
781
|
+
for (const pattern of patterns) {
|
|
782
|
+
const specificity = bestSpecificity([pattern], toolName);
|
|
783
|
+
if (specificity >= 0 && (!best || specificity > best.specificity)) {
|
|
784
|
+
best = { pattern, specificity };
|
|
785
|
+
}
|
|
786
|
+
}
|
|
787
|
+
return best;
|
|
788
|
+
}
|
|
688
789
|
export async function discoverTools(configPath) {
|
|
689
790
|
const raw = readConfigObject(configPath);
|
|
690
791
|
const parsed = GatewayConfig.parse(withoutDisabledProviders(raw));
|
|
@@ -820,6 +921,7 @@ function parseGatewayConfig(doc) {
|
|
|
820
921
|
],
|
|
821
922
|
};
|
|
822
923
|
}
|
|
924
|
+
applyProfiles(result.data);
|
|
823
925
|
return { config: result.data, diagnostics: validateConfig(result.data) };
|
|
824
926
|
}
|
|
825
927
|
catch (err) {
|
|
@@ -838,6 +940,7 @@ function toEditableAgents(input) {
|
|
|
838
940
|
allow: stringArray(agent.allow),
|
|
839
941
|
ask: stringArray(agent.ask),
|
|
840
942
|
deny: stringArray(agent.deny),
|
|
943
|
+
hasToken: typeof agent.token === 'string' && agent.token.trim().length > 0,
|
|
841
944
|
},
|
|
842
945
|
];
|
|
843
946
|
}));
|
|
@@ -868,6 +971,7 @@ function toEditableProfiles(input) {
|
|
|
868
971
|
return [
|
|
869
972
|
id,
|
|
870
973
|
{
|
|
974
|
+
extends: stringArray(profile.extends),
|
|
871
975
|
allow: stringArray(profile.allow),
|
|
872
976
|
ask: stringArray(profile.ask),
|
|
873
977
|
deny: stringArray(profile.deny),
|
|
@@ -895,6 +999,14 @@ function parseId(value) {
|
|
|
895
999
|
}
|
|
896
1000
|
return id;
|
|
897
1001
|
}
|
|
1002
|
+
function parseOptionalId(value) {
|
|
1003
|
+
if (value === undefined || value === null || value === '')
|
|
1004
|
+
return undefined;
|
|
1005
|
+
return parseId(value);
|
|
1006
|
+
}
|
|
1007
|
+
function createAgentToken() {
|
|
1008
|
+
return `airlock_agent_${randomBytes(32).toString('base64url')}`;
|
|
1009
|
+
}
|
|
898
1010
|
function parseBoolean(value) {
|
|
899
1011
|
if (typeof value === 'boolean')
|
|
900
1012
|
return value;
|
|
@@ -1236,6 +1348,17 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1236
1348
|
border-color: #f1d99a;
|
|
1237
1349
|
background: #fffaf0;
|
|
1238
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
|
+
}
|
|
1239
1362
|
.log-cell {
|
|
1240
1363
|
min-width: 0;
|
|
1241
1364
|
overflow-wrap: anywhere;
|
|
@@ -1479,6 +1602,25 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1479
1602
|
color: var(--red);
|
|
1480
1603
|
font-weight: 700;
|
|
1481
1604
|
}
|
|
1605
|
+
.rule-actions button.active.pattern-match {
|
|
1606
|
+
border-style: dashed;
|
|
1607
|
+
font-weight: 650;
|
|
1608
|
+
}
|
|
1609
|
+
.rule-actions button.active.pattern-match[data-decision="allow"] {
|
|
1610
|
+
border-color: #b8dfca;
|
|
1611
|
+
background: #f6fbf8;
|
|
1612
|
+
color: #4d7f63;
|
|
1613
|
+
}
|
|
1614
|
+
.rule-actions button.active.pattern-match[data-decision="ask"] {
|
|
1615
|
+
border-color: #f1d99a;
|
|
1616
|
+
background: #fffaf0;
|
|
1617
|
+
color: #8a6a19;
|
|
1618
|
+
}
|
|
1619
|
+
.rule-actions button.active.pattern-match[data-decision="deny"] {
|
|
1620
|
+
border-color: #f2c0bb;
|
|
1621
|
+
background: #fff8f7;
|
|
1622
|
+
color: #9b4a43;
|
|
1623
|
+
}
|
|
1482
1624
|
.pill {
|
|
1483
1625
|
display: inline-flex;
|
|
1484
1626
|
align-items: center;
|
|
@@ -1571,6 +1713,43 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1571
1713
|
overflow: auto;
|
|
1572
1714
|
padding: 14px;
|
|
1573
1715
|
}
|
|
1716
|
+
.form-grid {
|
|
1717
|
+
display: grid;
|
|
1718
|
+
gap: 12px;
|
|
1719
|
+
}
|
|
1720
|
+
.field {
|
|
1721
|
+
display: grid;
|
|
1722
|
+
gap: 6px;
|
|
1723
|
+
}
|
|
1724
|
+
.field label {
|
|
1725
|
+
color: var(--muted);
|
|
1726
|
+
font-size: 12px;
|
|
1727
|
+
font-weight: 700;
|
|
1728
|
+
text-transform: uppercase;
|
|
1729
|
+
letter-spacing: 0.08em;
|
|
1730
|
+
}
|
|
1731
|
+
.field input, .field select {
|
|
1732
|
+
min-height: 36px;
|
|
1733
|
+
min-width: 0;
|
|
1734
|
+
border: 1px solid var(--line);
|
|
1735
|
+
border-radius: 7px;
|
|
1736
|
+
padding: 0 10px;
|
|
1737
|
+
background: #fff;
|
|
1738
|
+
color: var(--ink);
|
|
1739
|
+
}
|
|
1740
|
+
.profile-select-list {
|
|
1741
|
+
display: grid;
|
|
1742
|
+
gap: 6px;
|
|
1743
|
+
padding: 8px;
|
|
1744
|
+
border: 1px solid var(--line);
|
|
1745
|
+
border-radius: 7px;
|
|
1746
|
+
background: #fbfcfe;
|
|
1747
|
+
}
|
|
1748
|
+
.token-value {
|
|
1749
|
+
width: 100%;
|
|
1750
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1751
|
+
font-size: 12px;
|
|
1752
|
+
}
|
|
1574
1753
|
.modal-title {
|
|
1575
1754
|
min-width: 0;
|
|
1576
1755
|
overflow-wrap: anywhere;
|
|
@@ -1725,13 +1904,27 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1725
1904
|
<div id="approvalModalFooter" class="modal-footer"></div>
|
|
1726
1905
|
</div>
|
|
1727
1906
|
</div>
|
|
1907
|
+
<div id="entityModal" class="modal-backdrop">
|
|
1908
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="entityModalTitle">
|
|
1909
|
+
<div class="modal-header">
|
|
1910
|
+
<div>
|
|
1911
|
+
<div id="entityModalTitle" class="modal-title"></div>
|
|
1912
|
+
<div id="entityModalMeta" class="subtle"></div>
|
|
1913
|
+
</div>
|
|
1914
|
+
<button id="entityModalClose" title="Close">Close</button>
|
|
1915
|
+
</div>
|
|
1916
|
+
<div id="entityModalBody" class="modal-body"></div>
|
|
1917
|
+
<div id="entityModalFooter" class="modal-footer"></div>
|
|
1918
|
+
</div>
|
|
1919
|
+
</div>
|
|
1728
1920
|
<script>
|
|
1729
1921
|
const state = {
|
|
1730
1922
|
config: null,
|
|
1731
1923
|
status: null,
|
|
1732
1924
|
audit: { dbPath: '', entries: [], pending: [] },
|
|
1733
|
-
|
|
1734
|
-
|
|
1925
|
+
activityEvents: [],
|
|
1926
|
+
livePendingById: {},
|
|
1927
|
+
currentApprovalId: '',
|
|
1735
1928
|
activityAgentId: '',
|
|
1736
1929
|
auditFilters: { agent: '', tool: '', since: '', limit: '50' },
|
|
1737
1930
|
tools: [],
|
|
@@ -1739,6 +1932,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1739
1932
|
errors: [],
|
|
1740
1933
|
activeKind: 'agent',
|
|
1741
1934
|
activeId: '',
|
|
1935
|
+
createKind: '',
|
|
1742
1936
|
drafts: {},
|
|
1743
1937
|
descriptionExpanded: {},
|
|
1744
1938
|
search: '',
|
|
@@ -1815,7 +2009,13 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1815
2009
|
for (const [key, value] of Object.entries(state.auditFilters)) {
|
|
1816
2010
|
if (String(value || '').trim()) params.set(key, String(value).trim());
|
|
1817
2011
|
}
|
|
1818
|
-
|
|
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();
|
|
1819
2019
|
render();
|
|
1820
2020
|
} catch (error) {
|
|
1821
2021
|
alert(error.message);
|
|
@@ -1902,26 +2102,41 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1902
2102
|
}
|
|
1903
2103
|
|
|
1904
2104
|
function pendingApprovals(agentId = '') {
|
|
1905
|
-
const
|
|
1906
|
-
for (const request of Object.values(state.livePending || {})) {
|
|
1907
|
-
const pending = normalizePending(request, true);
|
|
1908
|
-
if (pending.code && (!agentId || pending.agentId === agentId)) byCode.set(pending.code, pending);
|
|
1909
|
-
}
|
|
2105
|
+
const byId = new Map();
|
|
1910
2106
|
for (const request of state.audit.pending || []) {
|
|
1911
2107
|
const pending = normalizePending(request, false);
|
|
1912
|
-
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];
|
|
1913
2127
|
}
|
|
1914
|
-
return Array.from(byCode.values());
|
|
1915
2128
|
}
|
|
1916
2129
|
|
|
1917
2130
|
function normalizePending(request, live) {
|
|
1918
2131
|
const args = live ? request.args : parseArgs(request.args);
|
|
2132
|
+
const context = request.context || airlockContext(args);
|
|
1919
2133
|
return {
|
|
1920
2134
|
id: request.id || '',
|
|
1921
2135
|
code: request.code || '',
|
|
1922
2136
|
agentId: request.agentId || request.agent_id || '',
|
|
1923
2137
|
tool: request.tool || '',
|
|
1924
|
-
args,
|
|
2138
|
+
args: stripAirlockContext(args),
|
|
2139
|
+
context,
|
|
1925
2140
|
status: request.status || 'pending',
|
|
1926
2141
|
createdAt: request.createdAt || request.created_at || '',
|
|
1927
2142
|
timeoutMs: request.timeoutMs || 0,
|
|
@@ -1940,19 +2155,37 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1940
2155
|
}
|
|
1941
2156
|
}
|
|
1942
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
|
+
|
|
1943
2174
|
function renderActivity() {
|
|
1944
2175
|
el('activitySurface').hidden = state.activeKind !== 'activity';
|
|
1945
2176
|
renderActivityAgentFilter();
|
|
1946
2177
|
const agentId = state.auditFilters.agent || state.activityAgentId || '';
|
|
1947
2178
|
const pending = pendingApprovals(agentId);
|
|
1948
2179
|
const entries = state.audit.entries || [];
|
|
2180
|
+
const activity = activityEvents(agentId);
|
|
1949
2181
|
const rows = [
|
|
1950
2182
|
...pending.map(pendingRow),
|
|
2183
|
+
...activity.map(activityRow),
|
|
1951
2184
|
...entries.map(auditRow)
|
|
1952
2185
|
];
|
|
1953
2186
|
el('pendingBadge').textContent = 'pending ' + pending.length;
|
|
1954
2187
|
el('activityMeta').textContent = state.audit.dbPath
|
|
1955
|
-
? 'Audit log: ' + state.audit.dbPath + (agentId ? ' · agent ' + agentId : '')
|
|
2188
|
+
? 'Activity events ' + activity.length + ' · Audit log: ' + state.audit.dbPath + (agentId ? ' · agent ' + agentId : '')
|
|
1956
2189
|
: 'Audit log not loaded yet';
|
|
1957
2190
|
el('activityList').innerHTML = rows.length
|
|
1958
2191
|
? rows.join('')
|
|
@@ -1960,7 +2193,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1960
2193
|
document.querySelectorAll('[data-pending-row]').forEach((row) => {
|
|
1961
2194
|
row.addEventListener('click', (event) => {
|
|
1962
2195
|
if (event.target.closest('[data-approval-action]')) return;
|
|
1963
|
-
openApprovalModal(row.dataset.
|
|
2196
|
+
openApprovalModal(row.dataset.id);
|
|
1964
2197
|
});
|
|
1965
2198
|
});
|
|
1966
2199
|
document.querySelectorAll('[data-approval-action]').forEach((button) => {
|
|
@@ -1976,6 +2209,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1976
2209
|
});
|
|
1977
2210
|
}
|
|
1978
2211
|
|
|
2212
|
+
function activityEvents(agentId = '') {
|
|
2213
|
+
return (state.activityEvents || []).filter((event) => !agentId || event.agentId === agentId);
|
|
2214
|
+
}
|
|
2215
|
+
|
|
1979
2216
|
function renderActivityAgentFilter() {
|
|
1980
2217
|
const agents = Object.keys(state.config.agents);
|
|
1981
2218
|
if (!state.activityAgentId) state.activityAgentId = agents[0] || '';
|
|
@@ -1987,25 +2224,29 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1987
2224
|
}
|
|
1988
2225
|
|
|
1989
2226
|
function pendingRow(entry) {
|
|
1990
|
-
|
|
1991
|
-
|
|
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>' +
|
|
1992
2230
|
'<div class="log-cell">' + escapeHtml(entry.agentId) + '</div>' +
|
|
1993
|
-
'<div class="log-cell">' + escapeHtml(entry.tool) + '<br><span class="subtle">
|
|
1994
|
-
'<div class="log-cell"><span class="tag tag-warn">' + escapeHtml(entry.status) + '</span></div>' +
|
|
1995
|
-
'<pre class="log-args">' + escapeHtml(
|
|
1996
|
-
'<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>' +
|
|
1997
2235
|
'</div>';
|
|
1998
2236
|
}
|
|
1999
2237
|
|
|
2000
|
-
function approvalButtons(code) {
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
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>' : '') +
|
|
2004
2244
|
'<button class="deny" data-approval-action="deny" data-code="' + escapeHtml(code) + '">Deny</button>';
|
|
2005
2245
|
}
|
|
2006
2246
|
|
|
2007
2247
|
async function actApproval(action, code, remember, durationMs) {
|
|
2008
2248
|
if (!code) return;
|
|
2249
|
+
const pending = findPendingApproval(code);
|
|
2009
2250
|
const params = new URLSearchParams({ code });
|
|
2010
2251
|
if (remember) params.set('remember', remember);
|
|
2011
2252
|
if (durationMs) params.set('duration_ms', durationMs);
|
|
@@ -2020,7 +2261,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2020
2261
|
}
|
|
2021
2262
|
throw new Error(message);
|
|
2022
2263
|
}
|
|
2023
|
-
delete state.
|
|
2264
|
+
if (pending?.id) delete state.livePendingById[pending.id];
|
|
2024
2265
|
closeApprovalModal();
|
|
2025
2266
|
await Promise.all([refreshLogs(), refreshStatus()]);
|
|
2026
2267
|
render();
|
|
@@ -2028,21 +2269,26 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2028
2269
|
|
|
2029
2270
|
function findPendingApproval(code) {
|
|
2030
2271
|
const agentId = state.activeKind === 'activity' ? state.auditFilters.agent || '' : '';
|
|
2031
|
-
return pendingApprovals(agentId).find((entry) => entry.code === code);
|
|
2272
|
+
return pendingApprovals(agentId).find((entry) => entry.id === code || entry.code === code);
|
|
2032
2273
|
}
|
|
2033
2274
|
|
|
2034
|
-
function openApprovalModal(
|
|
2035
|
-
const pending = findPendingApproval(
|
|
2275
|
+
function openApprovalModal(id) {
|
|
2276
|
+
const pending = findPendingApproval(id);
|
|
2036
2277
|
if (!pending) return;
|
|
2037
|
-
state.
|
|
2038
|
-
|
|
2278
|
+
state.currentApprovalId = pending.id;
|
|
2279
|
+
const question = isUserQuestion(pending);
|
|
2280
|
+
el('approvalModalTitle').textContent = question ? questionText(pending) : pending.tool;
|
|
2039
2281
|
el('approvalModalMeta').textContent = 'agent ' + pending.agentId + ' · code ' + pending.code;
|
|
2040
2282
|
el('approvalModalBody').innerHTML =
|
|
2041
2283
|
'<div class="detail-label">Status</div><div>' + escapeHtml(pending.status) + '</div>' +
|
|
2042
2284
|
'<div class="detail-label">Created</div><div>' + escapeHtml(formatTime(pending.createdAt)) + '</div>' +
|
|
2043
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>' : '') +
|
|
2044
2290
|
'<div class="detail-label">Arguments</div><pre class="log-args">' + escapeHtml(prettyJson(pending.args)) + '</pre>';
|
|
2045
|
-
el('approvalModalFooter').innerHTML = approvalButtons(pending.code);
|
|
2291
|
+
el('approvalModalFooter').innerHTML = approvalButtons(pending.code, { remember: !question, approveLabel: question ? 'Confirm' : 'Approve' });
|
|
2046
2292
|
el('approvalModal').classList.add('open');
|
|
2047
2293
|
el('approvalModalFooter').querySelectorAll('[data-approval-action]').forEach((button) => {
|
|
2048
2294
|
button.addEventListener('click', () => {
|
|
@@ -2058,7 +2304,58 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2058
2304
|
|
|
2059
2305
|
function closeApprovalModal() {
|
|
2060
2306
|
el('approvalModal').classList.remove('open');
|
|
2061
|
-
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>';
|
|
2062
2359
|
}
|
|
2063
2360
|
|
|
2064
2361
|
function auditRow(entry) {
|
|
@@ -2208,7 +2505,8 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2208
2505
|
el('addProvider').style.display = state.activeKind === 'providers' ? 'inline-flex' : 'none';
|
|
2209
2506
|
el('deleteEntity').style.display = state.activeKind === 'activity' ? 'none' : el('deleteEntity').style.display;
|
|
2210
2507
|
el('deleteEntity').disabled = !state.activeId || state.activeKind === 'activity';
|
|
2211
|
-
el('profilePanel').style.display =
|
|
2508
|
+
el('profilePanel').style.display =
|
|
2509
|
+
state.activeKind === 'agent' || state.activeKind === 'profile' ? 'block' : 'none';
|
|
2212
2510
|
|
|
2213
2511
|
if (!draft) {
|
|
2214
2512
|
if (state.activeKind === 'providers') {
|
|
@@ -2236,12 +2534,18 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2236
2534
|
}
|
|
2237
2535
|
|
|
2238
2536
|
const denyCount = draft.deny.length;
|
|
2537
|
+
const tokenPill = state.activeKind === 'agent'
|
|
2538
|
+
? '<span class="pill ' + (state.config.agents[state.activeId]?.hasToken ? 'tag-good' : 'tag-danger') + '">' + (state.config.agents[state.activeId]?.hasToken ? 'token set' : 'token missing') + '</span>'
|
|
2539
|
+
: '';
|
|
2239
2540
|
el('summary').innerHTML =
|
|
2240
2541
|
'<span class="pill">allow ' + draft.allow.length + '</span>' +
|
|
2241
2542
|
'<span class="pill">ask ' + draft.ask.length + '</span>' +
|
|
2242
|
-
'<span class="pill">deny ' + denyCount + '</span>'
|
|
2543
|
+
'<span class="pill">deny ' + denyCount + '</span>' +
|
|
2544
|
+
tokenPill;
|
|
2243
2545
|
|
|
2244
|
-
const profiles = Object.keys(state.config.profiles)
|
|
2546
|
+
const profiles = Object.keys(state.config.profiles).filter(
|
|
2547
|
+
(id) => state.activeKind !== 'profile' || id !== state.activeId
|
|
2548
|
+
);
|
|
2245
2549
|
el('profileChecks').innerHTML = profiles.length
|
|
2246
2550
|
? profiles.map((id) => {
|
|
2247
2551
|
const checked = (draft.extends || []).includes(id) ? ' checked' : '';
|
|
@@ -2292,7 +2596,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2292
2596
|
|
|
2293
2597
|
const visible = filteredTools();
|
|
2294
2598
|
el('tools').innerHTML = visible.length
|
|
2295
|
-
? visible.map((tool) => toolRow(tool,
|
|
2599
|
+
? visible.map((tool) => toolRow(tool, getDecisionMatch(draft, tool.name))).join('')
|
|
2296
2600
|
: '<div class="empty">No tools match the current filters.</div>';
|
|
2297
2601
|
|
|
2298
2602
|
document.querySelectorAll('[data-rule]').forEach((button) => {
|
|
@@ -2321,7 +2625,8 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2321
2625
|
});
|
|
2322
2626
|
}
|
|
2323
2627
|
|
|
2324
|
-
function toolRow(tool,
|
|
2628
|
+
function toolRow(tool, match) {
|
|
2629
|
+
const decision = match.decision;
|
|
2325
2630
|
const tags = tool.tags && tool.tags.length ? tool.tags : ['untagged'];
|
|
2326
2631
|
const tagHtml = tags.map((tag) => '<span class="tag ' + tagClass(tag) + '">' + escapeHtml(tag) + '</span>').join('');
|
|
2327
2632
|
const recommended = '<span class="tag tag-recommended ' + recommendedClass(tool.recommended) + '">recommended ' + escapeHtml(tool.recommended) + '</span>';
|
|
@@ -2336,10 +2641,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2336
2641
|
'<div class="tool-name"><span class="provider">' + escapeHtml(tool.provider) + '</span><br>' + escapeHtml(tool.shortName || tool.name) + '</div>' +
|
|
2337
2642
|
'<div class="description"><div class="description-content' + descriptionClass + '">' + formatDescription(description) + '</div>' + toggle + '<div class="tag-list">' + tagHtml + recommended + '</div></div>' +
|
|
2338
2643
|
'<div class="rule-actions">' +
|
|
2339
|
-
ruleButton(tool.name, 'allow',
|
|
2340
|
-
ruleButton(tool.name, 'ask',
|
|
2341
|
-
ruleButton(tool.name, 'deny',
|
|
2342
|
-
ruleButton(tool.name, 'unset',
|
|
2644
|
+
ruleButton(tool.name, 'allow', match) +
|
|
2645
|
+
ruleButton(tool.name, 'ask', match) +
|
|
2646
|
+
ruleButton(tool.name, 'deny', match) +
|
|
2647
|
+
ruleButton(tool.name, 'unset', match) +
|
|
2343
2648
|
'</div>' +
|
|
2344
2649
|
'</div>';
|
|
2345
2650
|
}
|
|
@@ -2394,17 +2699,67 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2394
2699
|
return 'tag-good';
|
|
2395
2700
|
}
|
|
2396
2701
|
|
|
2397
|
-
function ruleButton(tool, decision,
|
|
2398
|
-
const active = decision ===
|
|
2702
|
+
function ruleButton(tool, decision, activeMatch) {
|
|
2703
|
+
const active = decision === activeMatch.decision ? ' active' : '';
|
|
2704
|
+
const patternMatch = active && activeMatch.source === 'pattern' ? ' pattern-match' : '';
|
|
2399
2705
|
const label = decision === 'unset' ? 'Clear' : decision;
|
|
2400
|
-
|
|
2706
|
+
const title = activeMatch.source === 'pattern' && decision === activeMatch.decision
|
|
2707
|
+
? decision + ' by ' + activeMatch.pattern + '. Click to add an exact rule.'
|
|
2708
|
+
: decision === activeMatch.decision && activeMatch.source === 'exact'
|
|
2709
|
+
? decision + ' by exact rule.'
|
|
2710
|
+
: decision === 'unset'
|
|
2711
|
+
? 'Clear exact rule for this tool.'
|
|
2712
|
+
: 'Set exact ' + decision + ' rule for this tool.';
|
|
2713
|
+
return '<button class="' + active + patternMatch + '" title="' + escapeHtml(title) + '" data-rule data-tool="' + escapeHtml(tool) + '" data-decision="' + decision + '">' + label + '</button>';
|
|
2401
2714
|
}
|
|
2402
2715
|
|
|
2403
2716
|
function getDecision(draft, tool) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2717
|
+
return getDecisionMatch(draft, tool).decision;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
function getDecisionMatch(draft, tool) {
|
|
2721
|
+
const deny = bestRuleMatch(draft.deny, tool);
|
|
2722
|
+
const ask = bestRuleMatch(draft.ask, tool);
|
|
2723
|
+
const allow = bestRuleMatch(draft.allow, tool);
|
|
2724
|
+
const denySpec = deny ? deny.specificity : -1;
|
|
2725
|
+
const askSpec = ask ? ask.specificity : -1;
|
|
2726
|
+
const allowSpec = allow ? allow.specificity : -1;
|
|
2727
|
+
const best = Math.max(denySpec, askSpec, allowSpec);
|
|
2728
|
+
if (best < 0) return { decision: 'unset', source: 'none' };
|
|
2729
|
+
if (denySpec === best) return decisionMatch('deny', deny, tool);
|
|
2730
|
+
if (askSpec === best) return decisionMatch('ask', ask, tool);
|
|
2731
|
+
return decisionMatch('allow', allow, tool);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
function decisionMatch(decision, match, tool) {
|
|
2735
|
+
if (!match) return { decision, source: 'none' };
|
|
2736
|
+
return {
|
|
2737
|
+
decision,
|
|
2738
|
+
source: match.pattern === tool ? 'exact' : 'pattern',
|
|
2739
|
+
pattern: match.pattern
|
|
2740
|
+
};
|
|
2741
|
+
}
|
|
2742
|
+
|
|
2743
|
+
function bestRuleMatch(patterns, tool) {
|
|
2744
|
+
let best = null;
|
|
2745
|
+
for (const pattern of patterns || []) {
|
|
2746
|
+
const score = ruleSpecificity(pattern, tool);
|
|
2747
|
+
if (score >= 0 && (!best || score > best.specificity)) best = { pattern, specificity: score };
|
|
2748
|
+
}
|
|
2749
|
+
return best;
|
|
2750
|
+
}
|
|
2751
|
+
|
|
2752
|
+
function ruleSpecificity(pattern, tool) {
|
|
2753
|
+
if (pattern === tool) return pattern.length + 1;
|
|
2754
|
+
if (pattern.endsWith('/*')) {
|
|
2755
|
+
const prefix = pattern.slice(0, -1);
|
|
2756
|
+
return tool.startsWith(prefix) && !tool.slice(prefix.length).includes('/') ? prefix.length : -1;
|
|
2757
|
+
}
|
|
2758
|
+
if (pattern.endsWith('*')) {
|
|
2759
|
+
const prefix = pattern.slice(0, -1);
|
|
2760
|
+
return tool.startsWith(prefix) ? prefix.length : -1;
|
|
2761
|
+
}
|
|
2762
|
+
return -1;
|
|
2408
2763
|
}
|
|
2409
2764
|
|
|
2410
2765
|
function setDecision(draft, tool, decision) {
|
|
@@ -2473,12 +2828,104 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2473
2828
|
render();
|
|
2474
2829
|
}
|
|
2475
2830
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2831
|
+
function openCreateModal(kind) {
|
|
2832
|
+
state.createKind = kind;
|
|
2833
|
+
renderCreateModal();
|
|
2834
|
+
el('entityModal').classList.add('open');
|
|
2835
|
+
setTimeout(() => el('entityId')?.focus(), 0);
|
|
2836
|
+
}
|
|
2837
|
+
|
|
2838
|
+
function closeEntityModal() {
|
|
2839
|
+
el('entityModal').classList.remove('open');
|
|
2840
|
+
state.createKind = '';
|
|
2841
|
+
}
|
|
2842
|
+
|
|
2843
|
+
function renderCreateModal() {
|
|
2844
|
+
const kind = state.createKind;
|
|
2845
|
+
const isAgent = kind === 'agent';
|
|
2846
|
+
const section = isAgent ? state.config.agents : state.config.profiles;
|
|
2847
|
+
const baseOptions = ['<option value="">Blank</option>'].concat(
|
|
2848
|
+
Object.keys(section).map((id) => '<option value="' + escapeHtml(id) + '">' + escapeHtml(id) + '</option>')
|
|
2849
|
+
).join('');
|
|
2850
|
+
el('entityModalTitle').textContent = isAgent ? 'New agent' : 'New profile';
|
|
2851
|
+
el('entityModalMeta').textContent = isAgent ? 'Creates a bearer token and permission policy' : 'Creates a reusable permission policy';
|
|
2852
|
+
el('entityModalBody').innerHTML =
|
|
2853
|
+
'<div class="form-grid">' +
|
|
2854
|
+
'<div class="field"><label for="entityId">Id</label><input id="entityId" autocomplete="off" placeholder="' + (isAgent ? 'claude-code' : 'readonly') + '"></div>' +
|
|
2855
|
+
'<div class="field"><label for="entityBase">Start from</label><select id="entityBase">' + baseOptions + '</select></div>' +
|
|
2856
|
+
(isAgent ? profilePicker() : '') +
|
|
2857
|
+
'</div>';
|
|
2858
|
+
el('entityModalFooter').innerHTML =
|
|
2859
|
+
'<button id="entityCreate" class="primary">Create</button>' +
|
|
2860
|
+
'<button id="entityCancel">Cancel</button>';
|
|
2861
|
+
el('entityCreate').addEventListener('click', () => submitCreateEntity().catch((error) => alert(error.message)));
|
|
2862
|
+
el('entityCancel').addEventListener('click', closeEntityModal);
|
|
2863
|
+
el('entityId').addEventListener('keydown', (event) => {
|
|
2864
|
+
if (event.key === 'Enter') submitCreateEntity().catch((error) => alert(error.message));
|
|
2865
|
+
});
|
|
2866
|
+
}
|
|
2867
|
+
|
|
2868
|
+
function profilePicker() {
|
|
2869
|
+
const profiles = Object.keys(state.config.profiles);
|
|
2870
|
+
if (profiles.length === 0) {
|
|
2871
|
+
return '<div class="field"><label>Profiles</label><div class="subtle">No profiles yet.</div></div>';
|
|
2872
|
+
}
|
|
2873
|
+
return '<div class="field"><label>Profiles</label><div class="profile-select-list">' +
|
|
2874
|
+
profiles.map((id) => '<label class="check"><input type="checkbox" data-create-profile="' + escapeHtml(id) + '"> ' + escapeHtml(id) + '</label>').join('') +
|
|
2875
|
+
'</div></div>';
|
|
2876
|
+
}
|
|
2877
|
+
|
|
2878
|
+
async function submitCreateEntity() {
|
|
2879
|
+
const kind = state.createKind;
|
|
2880
|
+
const id = el('entityId').value.trim();
|
|
2881
|
+
if (!id) throw new Error('Id is required');
|
|
2882
|
+
const profileIds = Array.from(document.querySelectorAll('[data-create-profile]:checked')).map((box) => box.dataset.createProfile);
|
|
2883
|
+
const body = {
|
|
2884
|
+
kind,
|
|
2885
|
+
id,
|
|
2886
|
+
baseId: el('entityBase').value,
|
|
2887
|
+
profileIds
|
|
2888
|
+
};
|
|
2889
|
+
const result = await api('/api/entities', { method: 'POST', body: JSON.stringify(body) });
|
|
2890
|
+
state.config = result;
|
|
2480
2891
|
state.drafts = {};
|
|
2481
|
-
setActive(kind, id
|
|
2892
|
+
setActive(kind, id);
|
|
2893
|
+
if (result.createdToken?.token) {
|
|
2894
|
+
showCreatedToken(result.createdToken.agentId, result.createdToken.token);
|
|
2895
|
+
} else {
|
|
2896
|
+
closeEntityModal();
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
|
|
2900
|
+
function showCreatedToken(agentId, token) {
|
|
2901
|
+
el('entityModalTitle').textContent = 'Agent token';
|
|
2902
|
+
el('entityModalMeta').textContent = agentId;
|
|
2903
|
+
el('entityModalBody').innerHTML =
|
|
2904
|
+
'<div class="form-grid">' +
|
|
2905
|
+
'<div class="field"><label for="createdToken">Bearer token</label><input id="createdToken" class="token-value" readonly value="' + escapeHtml(token) + '"></div>' +
|
|
2906
|
+
'<div class="field"><label>Header</label><input class="token-value" readonly value="Authorization: Bearer ' + escapeHtml(token) + '"></div>' +
|
|
2907
|
+
'</div>';
|
|
2908
|
+
el('entityModalFooter').innerHTML =
|
|
2909
|
+
'<button id="copyCreatedToken" class="primary">Copy Token</button>' +
|
|
2910
|
+
'<button id="copyCreatedHeader">Copy Header</button>' +
|
|
2911
|
+
'<button id="entityDone">Done</button>';
|
|
2912
|
+
el('copyCreatedToken').addEventListener('click', () => copyText(token));
|
|
2913
|
+
el('copyCreatedHeader').addEventListener('click', () => copyText('Authorization: Bearer ' + token));
|
|
2914
|
+
el('entityDone').addEventListener('click', closeEntityModal);
|
|
2915
|
+
el('createdToken').select();
|
|
2916
|
+
}
|
|
2917
|
+
|
|
2918
|
+
async function copyText(value) {
|
|
2919
|
+
if (navigator.clipboard?.writeText) {
|
|
2920
|
+
await navigator.clipboard.writeText(value);
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const textarea = document.createElement('textarea');
|
|
2924
|
+
textarea.value = value;
|
|
2925
|
+
document.body.appendChild(textarea);
|
|
2926
|
+
textarea.select();
|
|
2927
|
+
document.execCommand('copy');
|
|
2928
|
+
textarea.remove();
|
|
2482
2929
|
}
|
|
2483
2930
|
|
|
2484
2931
|
async function addProvider() {
|
|
@@ -2580,17 +3027,31 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2580
3027
|
events.onmessage = (event) => {
|
|
2581
3028
|
const message = JSON.parse(event.data);
|
|
2582
3029
|
if (message.type === 'new') {
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
3030
|
+
const request = message.request || {};
|
|
3031
|
+
if (request.id) {
|
|
3032
|
+
state.livePendingById[request.id] = request;
|
|
3033
|
+
notifyApproval(request);
|
|
3034
|
+
refreshLogs().catch(() => {});
|
|
3035
|
+
render();
|
|
3036
|
+
}
|
|
2587
3037
|
}
|
|
2588
3038
|
if (message.type === 'resolved') {
|
|
2589
|
-
|
|
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
|
+
}
|
|
2590
3046
|
refreshLogs().catch(() => {});
|
|
2591
3047
|
refreshStatus().catch(() => {});
|
|
2592
3048
|
render();
|
|
2593
3049
|
}
|
|
3050
|
+
if (message.type === 'activity') {
|
|
3051
|
+
upsertActivityEvent(message.event);
|
|
3052
|
+
notifyActivity(message.event);
|
|
3053
|
+
render();
|
|
3054
|
+
}
|
|
2594
3055
|
};
|
|
2595
3056
|
events.onerror = () => {
|
|
2596
3057
|
events.close();
|
|
@@ -2600,13 +3061,36 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2600
3061
|
function notifyApproval(request) {
|
|
2601
3062
|
if (!el('approvalNotifs').checked) return;
|
|
2602
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);
|
|
2603
3068
|
new Notification('Airlock: ' + request.tool, {
|
|
2604
|
-
body:
|
|
2605
|
-
tag: request.code,
|
|
3069
|
+
body: lines.join('\\n'),
|
|
3070
|
+
tag: request.id || request.code,
|
|
2606
3071
|
silent: !el('approvalSound').checked
|
|
2607
3072
|
});
|
|
2608
3073
|
}
|
|
2609
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,
|
|
3082
|
+
silent: !el('approvalSound').checked
|
|
3083
|
+
});
|
|
3084
|
+
}
|
|
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
|
+
|
|
2610
3094
|
async function checkVersion() {
|
|
2611
3095
|
try {
|
|
2612
3096
|
const current = await fetch('/version').then((response) => response.json());
|
|
@@ -2629,10 +3113,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2629
3113
|
return false;
|
|
2630
3114
|
}
|
|
2631
3115
|
|
|
2632
|
-
function
|
|
3116
|
+
function firstPendingApprovalId() {
|
|
2633
3117
|
const agentId = state.activeKind === 'activity' ? state.auditFilters.agent || '' : '';
|
|
2634
3118
|
const first = pendingApprovals(agentId)[0];
|
|
2635
|
-
return first ? first.
|
|
3119
|
+
return first ? first.id : '';
|
|
2636
3120
|
}
|
|
2637
3121
|
|
|
2638
3122
|
el('refreshTools').addEventListener('click', refreshTools);
|
|
@@ -2642,8 +3126,8 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2642
3126
|
el('manageProviders').addEventListener('click', () => setActive('providers'));
|
|
2643
3127
|
el('viewActivity').addEventListener('click', () => setActive('activity'));
|
|
2644
3128
|
el('addProvider').addEventListener('click', () => addProvider().catch((error) => alert(error.message)));
|
|
2645
|
-
el('addAgent').addEventListener('click', () =>
|
|
2646
|
-
el('addProfile').addEventListener('click', () =>
|
|
3129
|
+
el('addAgent').addEventListener('click', () => openCreateModal('agent'));
|
|
3130
|
+
el('addProfile').addEventListener('click', () => openCreateModal('profile'));
|
|
2647
3131
|
el('deleteEntity').addEventListener('click', () => deleteCurrent().catch((error) => alert(error.message)));
|
|
2648
3132
|
el('search').addEventListener('input', (event) => { state.search = event.target.value; renderTools(); });
|
|
2649
3133
|
el('providerFilter').addEventListener('change', (event) => { state.provider = event.target.value; renderTools(); });
|
|
@@ -2669,11 +3153,16 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2669
3153
|
el('resetAllRules').addEventListener('click', resetAllToCurrentConfig);
|
|
2670
3154
|
el('recommendedRules').addEventListener('click', setRecommended);
|
|
2671
3155
|
el('approvalModalClose').addEventListener('click', closeApprovalModal);
|
|
3156
|
+
el('entityModalClose').addEventListener('click', closeEntityModal);
|
|
2672
3157
|
el('approvalModal').addEventListener('click', (event) => {
|
|
2673
3158
|
if (event.target === el('approvalModal')) closeApprovalModal();
|
|
2674
3159
|
});
|
|
3160
|
+
el('entityModal').addEventListener('click', (event) => {
|
|
3161
|
+
if (event.target === el('entityModal')) closeEntityModal();
|
|
3162
|
+
});
|
|
2675
3163
|
document.addEventListener('keydown', (event) => {
|
|
2676
3164
|
if (event.key === 'Escape') {
|
|
3165
|
+
closeEntityModal();
|
|
2677
3166
|
closeApprovalModal();
|
|
2678
3167
|
return;
|
|
2679
3168
|
}
|
|
@@ -2682,9 +3171,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2682
3171
|
if (target && ['INPUT', 'TEXTAREA', 'SELECT'].includes(target.tagName)) return;
|
|
2683
3172
|
const key = event.key.toLowerCase();
|
|
2684
3173
|
if (key !== 'a' && key !== 'd') return;
|
|
2685
|
-
const
|
|
2686
|
-
|
|
2687
|
-
|
|
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));
|
|
2688
3178
|
});
|
|
2689
3179
|
|
|
2690
3180
|
initApprovalSettings();
|