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.
Files changed (243) hide show
  1. package/README.md +26 -8
  2. package/dist/activity/stream.d.ts +29 -0
  3. package/dist/activity/stream.d.ts.map +1 -0
  4. package/dist/activity/stream.js +36 -0
  5. package/dist/activity/stream.js.map +1 -0
  6. package/dist/airlock/context.d.ts +12 -0
  7. package/dist/airlock/context.d.ts.map +1 -0
  8. package/dist/airlock/context.js +32 -0
  9. package/dist/airlock/context.js.map +1 -0
  10. package/dist/airlock/tools.d.ts +7 -0
  11. package/dist/airlock/tools.d.ts.map +1 -0
  12. package/dist/airlock/tools.js +13 -0
  13. package/dist/airlock/tools.js.map +1 -0
  14. package/dist/allowlist/engine.d.ts +12 -0
  15. package/dist/allowlist/engine.d.ts.map +1 -1
  16. package/dist/allowlist/engine.js +40 -15
  17. package/dist/allowlist/engine.js.map +1 -1
  18. package/dist/audit/api.d.ts +2 -0
  19. package/dist/audit/api.d.ts.map +1 -1
  20. package/dist/audit/api.js.map +1 -1
  21. package/dist/audit/db.d.ts +16 -0
  22. package/dist/audit/db.d.ts.map +1 -1
  23. package/dist/audit/db.js +73 -0
  24. package/dist/audit/db.js.map +1 -1
  25. package/dist/audit/logger.d.ts +8 -1
  26. package/dist/audit/logger.d.ts.map +1 -1
  27. package/dist/audit/logger.js +21 -0
  28. package/dist/audit/logger.js.map +1 -1
  29. package/dist/backend/airlock-adapter.d.ts +22 -0
  30. package/dist/backend/airlock-adapter.d.ts.map +1 -0
  31. package/dist/backend/airlock-adapter.js +136 -0
  32. package/dist/backend/airlock-adapter.js.map +1 -0
  33. package/dist/backend/factory.d.ts +5 -1
  34. package/dist/backend/factory.d.ts.map +1 -1
  35. package/dist/backend/factory.js +5 -5
  36. package/dist/backend/factory.js.map +1 -1
  37. package/dist/backend/mcp-adapter.d.ts.map +1 -1
  38. package/dist/backend/mcp-adapter.js +28 -4
  39. package/dist/backend/mcp-adapter.js.map +1 -1
  40. package/dist/backend/openapi/adapter.d.ts.map +1 -1
  41. package/dist/backend/openapi/adapter.js +33 -13
  42. package/dist/backend/openapi/adapter.js.map +1 -1
  43. package/dist/config/loader.d.ts +12 -1
  44. package/dist/config/loader.d.ts.map +1 -1
  45. package/dist/config/loader.js +799 -23
  46. package/dist/config/loader.js.map +1 -1
  47. package/dist/config/profiles.d.ts +44 -3
  48. package/dist/config/profiles.d.ts.map +1 -1
  49. package/dist/config/profiles.js +314 -3
  50. package/dist/config/profiles.js.map +1 -1
  51. package/dist/config/schema.d.ts +3300 -201
  52. package/dist/config/schema.d.ts.map +1 -1
  53. package/dist/config/schema.js +272 -71
  54. package/dist/config/schema.js.map +1 -1
  55. package/dist/configure-web/cli.d.ts +1 -0
  56. package/dist/configure-web/cli.d.ts.map +1 -1
  57. package/dist/configure-web/cli.js +238 -47
  58. package/dist/configure-web/cli.js.map +1 -1
  59. package/dist/gateway.d.ts +16 -1
  60. package/dist/gateway.d.ts.map +1 -1
  61. package/dist/gateway.js +263 -57
  62. package/dist/gateway.js.map +1 -1
  63. package/dist/hitl/api.d.ts +2 -0
  64. package/dist/hitl/api.d.ts.map +1 -1
  65. package/dist/hitl/api.js +8 -0
  66. package/dist/hitl/api.js.map +1 -1
  67. package/dist/hitl/approval-dashboard.d.ts +5 -9
  68. package/dist/hitl/approval-dashboard.d.ts.map +1 -1
  69. package/dist/hitl/approval-dashboard.js +8 -55
  70. package/dist/hitl/approval-dashboard.js.map +1 -1
  71. package/dist/hitl/approval-stream.d.ts +34 -0
  72. package/dist/hitl/approval-stream.d.ts.map +1 -0
  73. package/dist/hitl/approval-stream.js +111 -0
  74. package/dist/hitl/approval-stream.js.map +1 -0
  75. package/dist/hitl/engine.d.ts +16 -7
  76. package/dist/hitl/engine.d.ts.map +1 -1
  77. package/dist/hitl/engine.js +109 -35
  78. package/dist/hitl/engine.js.map +1 -1
  79. package/dist/hitl/formatter.d.ts.map +1 -1
  80. package/dist/hitl/formatter.js +2 -0
  81. package/dist/hitl/formatter.js.map +1 -1
  82. package/dist/hitl/provider-factory.d.ts +8 -2
  83. package/dist/hitl/provider-factory.d.ts.map +1 -1
  84. package/dist/hitl/provider-factory.js +23 -2
  85. package/dist/hitl/provider-factory.js.map +1 -1
  86. package/dist/hitl/providers/composite.d.ts +9 -0
  87. package/dist/hitl/providers/composite.d.ts.map +1 -1
  88. package/dist/hitl/providers/composite.js +14 -0
  89. package/dist/hitl/providers/composite.js.map +1 -1
  90. package/dist/hitl/providers/dashboard.d.ts +12 -2
  91. package/dist/hitl/providers/dashboard.d.ts.map +1 -1
  92. package/dist/hitl/providers/dashboard.js +11 -5
  93. package/dist/hitl/providers/dashboard.js.map +1 -1
  94. package/dist/hitl/providers/ios.d.ts +23 -0
  95. package/dist/hitl/providers/ios.d.ts.map +1 -0
  96. package/dist/hitl/providers/ios.js +183 -0
  97. package/dist/hitl/providers/ios.js.map +1 -0
  98. package/dist/hitl/providers/macos.d.ts.map +1 -1
  99. package/dist/hitl/providers/macos.js +8 -4
  100. package/dist/hitl/providers/macos.js.map +1 -1
  101. package/dist/hitl/providers/openclaw.js +2 -2
  102. package/dist/hitl/providers/openclaw.js.map +1 -1
  103. package/dist/hitl/providers/stdio.js +2 -2
  104. package/dist/hitl/providers/stdio.js.map +1 -1
  105. package/dist/hitl/providers/telegram.js +2 -2
  106. package/dist/hitl/providers/telegram.js.map +1 -1
  107. package/dist/hitl/providers/tui.d.ts.map +1 -1
  108. package/dist/hitl/providers/tui.js +8 -2
  109. package/dist/hitl/providers/tui.js.map +1 -1
  110. package/dist/hitl/providers/types.d.ts +14 -0
  111. package/dist/hitl/providers/types.d.ts.map +1 -1
  112. package/dist/hook/api.d.ts +2 -0
  113. package/dist/hook/api.d.ts.map +1 -1
  114. package/dist/hook/api.js.map +1 -1
  115. package/dist/hook/normalizer.js +2 -2
  116. package/dist/hook/normalizer.js.map +1 -1
  117. package/dist/index.js +28 -1
  118. package/dist/index.js.map +1 -1
  119. package/dist/introspect/cli.d.ts +31 -0
  120. package/dist/introspect/cli.d.ts.map +1 -0
  121. package/dist/introspect/cli.js +756 -0
  122. package/dist/introspect/cli.js.map +1 -0
  123. package/dist/middleware/chain-builder.d.ts +1 -1
  124. package/dist/middleware/chain-builder.d.ts.map +1 -1
  125. package/dist/middleware/chain-builder.js +6 -4
  126. package/dist/middleware/chain-builder.js.map +1 -1
  127. package/dist/middleware/core/allowlist.d.ts.map +1 -1
  128. package/dist/middleware/core/allowlist.js +14 -0
  129. package/dist/middleware/core/allowlist.js.map +1 -1
  130. package/dist/middleware/core/arg-policy.d.ts +2 -1
  131. package/dist/middleware/core/arg-policy.d.ts.map +1 -1
  132. package/dist/middleware/core/arg-policy.js +156 -46
  133. package/dist/middleware/core/arg-policy.js.map +1 -1
  134. package/dist/middleware/core/exec-policy.d.ts.map +1 -1
  135. package/dist/middleware/core/exec-policy.js +16 -1
  136. package/dist/middleware/core/exec-policy.js.map +1 -1
  137. package/dist/middleware/core/execute.d.ts.map +1 -1
  138. package/dist/middleware/core/execute.js +6 -2
  139. package/dist/middleware/core/execute.js.map +1 -1
  140. package/dist/middleware/core/hitl-gate.d.ts.map +1 -1
  141. package/dist/middleware/core/hitl-gate.js +15 -4
  142. package/dist/middleware/core/hitl-gate.js.map +1 -1
  143. package/dist/middleware/core/schema-validator.d.ts.map +1 -1
  144. package/dist/middleware/core/schema-validator.js +7 -3
  145. package/dist/middleware/core/schema-validator.js.map +1 -1
  146. package/dist/middleware/post/canary-token-injector.d.ts +4 -1
  147. package/dist/middleware/post/canary-token-injector.d.ts.map +1 -1
  148. package/dist/middleware/post/canary-token-injector.js +5 -1
  149. package/dist/middleware/post/canary-token-injector.js.map +1 -1
  150. package/dist/middleware/post/untrusted-envelope.d.ts.map +1 -1
  151. package/dist/middleware/post/untrusted-envelope.js +7 -1
  152. package/dist/middleware/post/untrusted-envelope.js.map +1 -1
  153. package/dist/mobile/api.d.ts +20 -0
  154. package/dist/mobile/api.d.ts.map +1 -0
  155. package/dist/mobile/api.js +255 -0
  156. package/dist/mobile/api.js.map +1 -0
  157. package/dist/mobile/apns.d.ts +68 -0
  158. package/dist/mobile/apns.d.ts.map +1 -0
  159. package/dist/mobile/apns.js +321 -0
  160. package/dist/mobile/apns.js.map +1 -0
  161. package/dist/pool/http-client.d.ts +3 -1
  162. package/dist/pool/http-client.d.ts.map +1 -1
  163. package/dist/pool/http-client.js +5 -2
  164. package/dist/pool/http-client.js.map +1 -1
  165. package/dist/pool/oauth-provider.d.ts +2 -0
  166. package/dist/pool/oauth-provider.d.ts.map +1 -1
  167. package/dist/pool/oauth-provider.js +23 -4
  168. package/dist/pool/oauth-provider.js.map +1 -1
  169. package/dist/pool/pool.d.ts +2 -1
  170. package/dist/pool/pool.d.ts.map +1 -1
  171. package/dist/pool/pool.js +2 -2
  172. package/dist/pool/pool.js.map +1 -1
  173. package/dist/pool/sse-client.d.ts +3 -1
  174. package/dist/pool/sse-client.d.ts.map +1 -1
  175. package/dist/pool/sse-client.js +5 -2
  176. package/dist/pool/sse-client.js.map +1 -1
  177. package/dist/pool/stdio-client.d.ts +3 -1
  178. package/dist/pool/stdio-client.d.ts.map +1 -1
  179. package/dist/pool/stdio-client.js +5 -2
  180. package/dist/pool/stdio-client.js.map +1 -1
  181. package/dist/registry/airlock-policy.d.ts +3 -0
  182. package/dist/registry/airlock-policy.d.ts.map +1 -0
  183. package/dist/registry/airlock-policy.js +50 -0
  184. package/dist/registry/airlock-policy.js.map +1 -0
  185. package/dist/registry/registry.d.ts +2 -0
  186. package/dist/registry/registry.d.ts.map +1 -1
  187. package/dist/registry/registry.js +19 -13
  188. package/dist/registry/registry.js.map +1 -1
  189. package/dist/security/blocked-hosts.d.ts +2 -0
  190. package/dist/security/blocked-hosts.d.ts.map +1 -1
  191. package/dist/security/blocked-hosts.js +61 -7
  192. package/dist/security/blocked-hosts.js.map +1 -1
  193. package/dist/security/request.d.ts +2 -0
  194. package/dist/security/request.d.ts.map +1 -1
  195. package/dist/security/request.js +6 -2
  196. package/dist/security/request.js.map +1 -1
  197. package/dist/security/secrets.d.ts +12 -0
  198. package/dist/security/secrets.d.ts.map +1 -0
  199. package/dist/security/secrets.js +138 -0
  200. package/dist/security/secrets.js.map +1 -0
  201. package/dist/setup-openclaw/cli.js +1 -1
  202. package/dist/stdio-mode.d.ts.map +1 -1
  203. package/dist/stdio-mode.js +125 -24
  204. package/dist/stdio-mode.js.map +1 -1
  205. package/dist/tools/api.d.ts +4 -1
  206. package/dist/tools/api.d.ts.map +1 -1
  207. package/dist/tools/api.js +58 -7
  208. package/dist/tools/api.js.map +1 -1
  209. package/dist/tools/exec.d.ts +2 -1
  210. package/dist/tools/exec.d.ts.map +1 -1
  211. package/dist/tools/exec.js +4 -3
  212. package/dist/tools/exec.js.map +1 -1
  213. package/dist/tools/http.d.ts.map +1 -1
  214. package/dist/tools/http.js +4 -7
  215. package/dist/tools/http.js.map +1 -1
  216. package/dist/transport/agent-server.d.ts +4 -0
  217. package/dist/transport/agent-server.d.ts.map +1 -1
  218. package/dist/transport/agent-server.js +23 -11
  219. package/dist/transport/agent-server.js.map +1 -1
  220. package/dist/transport/http-server.d.ts +2 -0
  221. package/dist/transport/http-server.d.ts.map +1 -1
  222. package/dist/transport/http-server.js +10 -6
  223. package/dist/transport/http-server.js.map +1 -1
  224. package/dist/transport/sse-server.d.ts +2 -0
  225. package/dist/transport/sse-server.d.ts.map +1 -1
  226. package/dist/transport/sse-server.js +10 -6
  227. package/dist/transport/sse-server.js.map +1 -1
  228. package/dist/transport/stdio-server.d.ts.map +1 -1
  229. package/dist/transport/stdio-server.js +4 -0
  230. package/dist/transport/stdio-server.js.map +1 -1
  231. package/dist/util/id.d.ts +1 -1
  232. package/dist/util/id.d.ts.map +1 -1
  233. package/dist/util/id.js +3 -3
  234. package/dist/util/id.js.map +1 -1
  235. package/examples/claude-code-setup.md +0 -2
  236. package/examples/docker-compose.yaml +4 -3
  237. package/examples/docker-gateway.yaml +8 -3
  238. package/examples/gateway.yaml +8 -5
  239. package/examples/profiles.yaml +0 -2
  240. package/extensions/openclaw/index.ts +9 -6
  241. package/extensions/openclaw/openclaw.plugin.json +5 -5
  242. package/package.json +3 -1
  243. 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
- --gateway-url <url> Gateway URL (default: http://127.0.0.1:4111)
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:4111' },
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:4111';
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 || entry.id === 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
- livePending: {},
1877
- currentApprovalCode: '',
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
- state.audit = await api('/api/logs' + (params.toString() ? '?' + params.toString() : ''));
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 byCode = new Map();
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.code && (!agentId || pending.agentId === agentId) && !byCode.has(pending.code)) byCode.set(pending.code, 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.code);
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
- return '<div class="log-row pending" data-pending-row data-code="' + escapeHtml(entry.code) + '">' +
2135
- '<div class="log-cell"><strong>pending approval</strong><br>' + escapeHtml(formatTime(entry.createdAt)) + '</div>' +
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">code ' + escapeHtml(entry.code) + '</span></div>' +
2138
- '<div class="log-cell"><span class="tag tag-warn">' + escapeHtml(entry.status) + '</span></div>' +
2139
- '<pre class="log-args">' + escapeHtml(prettyJson(entry.args)) + '</pre>' +
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
- return '<button class="approve" data-approval-action="approve" data-code="' + escapeHtml(code) + '">Approve</button>' +
2146
- '<button class="approve" data-approval-action="approve" data-code="' + escapeHtml(code) + '" data-remember="temporary" data-duration="3600000">Allow 1h</button>' +
2147
- '<button class="approve" data-approval-action="approve" data-code="' + escapeHtml(code) + '" data-remember="always">Always Allow</button>' +
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.livePending[code];
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(code) {
2179
- const pending = findPendingApproval(code);
2275
+ function openApprovalModal(id) {
2276
+ const pending = findPendingApproval(id);
2180
2277
  if (!pending) return;
2181
- state.currentApprovalCode = code;
2182
- el('approvalModalTitle').textContent = pending.tool;
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.currentApprovalCode = '';
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
- state.livePending[message.request.code] = message.request;
2878
- notifyApproval(message.request);
2879
- refreshLogs().catch(() => {});
2880
- render();
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
- delete state.livePending[message.code];
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: 'agent: ' + request.agentId + '\\n' + request.code,
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 firstPendingCode() {
3116
+ function firstPendingApprovalId() {
2927
3117
  const agentId = state.activeKind === 'activity' ? state.auditFilters.agent || '' : '';
2928
3118
  const first = pendingApprovals(agentId)[0];
2929
- return first ? first.code : '';
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 code = state.currentApprovalCode || firstPendingCode();
2985
- if (!code) return;
2986
- actApproval(key === 'a' ? 'approve' : 'deny', code).catch((error) => alert(error.message));
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();