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.
Files changed (243) hide show
  1. package/README.md +34 -7
  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 +46 -8
  48. package/dist/config/profiles.d.ts.map +1 -1
  49. package/dist/config/profiles.js +367 -23
  50. package/dist/config/profiles.js.map +1 -1
  51. package/dist/config/schema.d.ts +3542 -174
  52. package/dist/config/schema.d.ts.map +1 -1
  53. package/dist/config/schema.js +292 -63
  54. package/dist/config/schema.js.map +1 -1
  55. package/dist/configure-web/cli.d.ts +13 -0
  56. package/dist/configure-web/cli.d.ts.map +1 -1
  57. package/dist/configure-web/cli.js +570 -80
  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 +8 -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 +13 -0
  131. package/dist/middleware/core/arg-policy.d.ts.map +1 -0
  132. package/dist/middleware/core/arg-policy.js +201 -0
  133. package/dist/middleware/core/arg-policy.js.map +1 -0
  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 +4 -4
  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 +658 -8
@@ -1,19 +1,92 @@
1
1
  import { readFileSync } from 'fs';
2
+ import { isIP } from 'net';
2
3
  import { parse as parseYaml } from 'yaml';
3
- import { GatewayConfig, getBuiltinProviders } from './schema.js';
4
+ import { GatewayConfig, getBuiltinProviders, withEnvVarResolution } from './schema.js';
4
5
  import { applyProfiles } from './profiles.js';
5
6
  import { matches } from '../allowlist/pattern.js';
7
+ import { AIRLOCK_NON_ASK_TOOLS } from '../airlock/tools.js';
6
8
  import { childLogger } from '../util/logger.js';
7
9
  const log = childLogger('config');
8
- export function loadConfig(path) {
9
- const raw = readFileSync(path, 'utf-8');
10
- const parsed = parseYaml(raw);
11
- const result = GatewayConfig.safeParse(parsed);
10
+ export function loadConfigDetailed(path, options = {}) {
11
+ let raw;
12
+ try {
13
+ raw = readFileSync(path, 'utf-8');
14
+ }
15
+ catch (err) {
16
+ return {
17
+ diagnostics: [
18
+ {
19
+ level: 'error',
20
+ message: `Could not read config at ${path}: ${err instanceof Error ? err.message : String(err)}`,
21
+ },
22
+ ],
23
+ };
24
+ }
25
+ let parsed;
26
+ try {
27
+ parsed = parseYaml(raw);
28
+ }
29
+ catch (err) {
30
+ return {
31
+ diagnostics: [
32
+ {
33
+ level: 'error',
34
+ message: `Invalid YAML at ${path}: ${err instanceof Error ? err.message : String(err)}`,
35
+ },
36
+ ],
37
+ };
38
+ }
39
+ const diagnostics = findUnknownKeys(parsed);
40
+ const resolveEnv = options.resolveEnv ?? true;
41
+ let result;
42
+ try {
43
+ result = withEnvVarResolution(resolveEnv, () => GatewayConfig.safeParse(parsed));
44
+ }
45
+ catch (err) {
46
+ diagnostics.push({
47
+ level: 'error',
48
+ message: `Invalid config at ${path}:\n${err instanceof Error ? err.message : String(err)}`,
49
+ suggestion: resolveEnv
50
+ ? 'Set the referenced environment variable, or run config check with --no-resolve for structural validation without secrets.'
51
+ : undefined,
52
+ });
53
+ return { diagnostics: dedupeDiagnostics(diagnostics) };
54
+ }
12
55
  if (!result.success) {
13
- throw new Error(`Invalid config at ${path}:\n${result.error.toString()}`);
56
+ diagnostics.push({
57
+ level: 'error',
58
+ message: `Invalid config at ${path}:\n${result.error.toString()}`,
59
+ });
60
+ return { diagnostics: dedupeDiagnostics(diagnostics) };
61
+ }
62
+ const rawConfig = structuredClone(result.data);
63
+ diagnostics.push(...validateConfig(result.data));
64
+ diagnostics.push(...validateRawArgDimensionUsage(result.data));
65
+ diagnostics.push(...validateYamlScalarFootguns(result.data));
66
+ if (!diagnostics.some((diagnostic) => diagnostic.level === 'error')) {
67
+ try {
68
+ applyProfiles(result.data);
69
+ }
70
+ catch (err) {
71
+ diagnostics.push({
72
+ level: 'error',
73
+ message: err instanceof Error ? err.message : String(err),
74
+ });
75
+ }
76
+ }
77
+ if (!diagnostics.some((diagnostic) => diagnostic.level === 'error')) {
78
+ diagnostics.push(...validateConfig(result.data));
79
+ diagnostics.push(...validateEffectiveArgRestrictions(rawConfig, result.data));
14
80
  }
15
- const diagnostics = validateConfig(result.data);
16
- for (const d of diagnostics) {
81
+ return {
82
+ config: result.data,
83
+ rawConfig,
84
+ diagnostics: dedupeDiagnostics(diagnostics),
85
+ };
86
+ }
87
+ export function loadConfig(path, options = {}) {
88
+ const result = loadConfigDetailed(path, options);
89
+ for (const d of result.diagnostics) {
17
90
  const ctx = d.agent ? { agent: d.agent } : {};
18
91
  const msg = d.suggestion ? `${d.message}\n → ${d.suggestion}` : d.message;
19
92
  if (d.level === 'error') {
@@ -26,13 +99,15 @@ export function loadConfig(path) {
26
99
  log.info(ctx, msg);
27
100
  }
28
101
  }
29
- const errors = diagnostics.filter((d) => d.level === 'error');
102
+ const errors = result.diagnostics.filter((d) => d.level === 'error');
30
103
  if (errors.length > 0) {
31
104
  throw new Error(`Config validation failed with ${errors.length} error(s):\n` +
32
105
  errors.map((e) => ` - ${e.agent ? `[${e.agent}] ` : ''}${e.message}`).join('\n'));
33
106
  }
34
- applyProfiles(result.data);
35
- return result.data;
107
+ if (!result.config) {
108
+ throw new Error(`Config validation failed without a parsed config.`);
109
+ }
110
+ return result.config;
36
111
  }
37
112
  export function validateConfig(config) {
38
113
  const diagnostics = [];
@@ -41,15 +116,27 @@ export function validateConfig(config) {
41
116
  const profileNames = new Set(Object.keys(config.profiles));
42
117
  const sandboxPresetNames = new Set(Object.keys(config.sandbox_presets ?? {}));
43
118
  const serverHost = config.server.host;
44
- const loopbackHosts = new Set(['127.0.0.1', 'localhost', '::1']);
45
- const isLoopback = loopbackHosts.has(serverHost);
46
- const exposesNonMcpRoutes = config.server.expose_management_api ||
47
- config.server.expose_tools_api ||
48
- config.server.expose_hook_api;
119
+ const isLoopback = isLoopbackHost(serverHost);
120
+ const managementApiEnabled = config.server.management_api.enabled;
121
+ const managementApiSecret = config.server.management_api.api_secret;
49
122
  const agentsWithoutTokens = Object.entries(config.agents)
50
123
  .filter(([, agent]) => !agent.token)
51
124
  .map(([agentId]) => agentId);
52
- const requireAgentTokens = config.server.require_agent_tokens || !isLoopback;
125
+ const requireAgentTokens = config.server.require_agent_tokens || !isLoopback || managementApiEnabled;
126
+ if (config.server.expose_management_api !== undefined) {
127
+ diagnostics.push({
128
+ level: 'warn',
129
+ message: 'server.expose_management_api is deprecated.',
130
+ suggestion: 'Use server.management_api.enabled instead.',
131
+ });
132
+ }
133
+ if (config.server.expose_hook_api !== undefined) {
134
+ diagnostics.push({
135
+ level: 'warn',
136
+ message: 'server.expose_hook_api is deprecated.',
137
+ suggestion: 'Use server.management_api.expose_hook_api instead.',
138
+ });
139
+ }
53
140
  if (!isLoopback && !config.server.auth_required) {
54
141
  diagnostics.push({
55
142
  level: 'error',
@@ -61,7 +148,7 @@ export function validateConfig(config) {
61
148
  diagnostics.push({
62
149
  level: 'error',
63
150
  message: 'Per-agent tokens are required, but some agents have no token.',
64
- suggestion: `Add token to agents: ${agentsWithoutTokens.join(', ')}. For reverse-proxy exposure on loopback, set server.require_agent_tokens: true to keep this check enabled.`,
151
+ suggestion: `Add token to agents: ${agentsWithoutTokens.join(', ')}. Per-agent tokens are required for non-loopback data-plane binds, explicit server.require_agent_tokens, and split management API mode.`,
65
152
  });
66
153
  }
67
154
  if (config.server.auth_required) {
@@ -72,26 +159,69 @@ export function validateConfig(config) {
72
159
  suggestion: `Set server.api_secret or add token to agents: ${agentsWithoutTokens.join(', ')}.`,
73
160
  });
74
161
  }
75
- if (!config.server.api_secret && exposesNonMcpRoutes) {
162
+ if (!managementApiSecret && !config.server.api_secret && managementApiEnabled) {
76
163
  diagnostics.push({
77
164
  level: 'error',
78
- message: 'server.auth_required is true, but non-MCP APIs are exposed without server.api_secret.',
79
- suggestion: 'Set server.api_secret, or disable expose_management_api, expose_tools_api, and expose_hook_api.',
165
+ message: 'server.auth_required is true, but the control-plane management API is enabled without a credential.',
166
+ suggestion: 'Set server.management_api.api_secret, set server.api_secret as a temporary fallback, or disable server.management_api.enabled.',
80
167
  });
81
168
  }
82
169
  }
170
+ if (managementApiEnabled && !managementApiSecret && !config.server.api_secret) {
171
+ diagnostics.push({
172
+ level: 'error',
173
+ message: 'server.management_api.enabled requires server.management_api.api_secret or server.api_secret.',
174
+ suggestion: 'Set server.management_api.api_secret so control-plane requests require bearer-token auth.',
175
+ });
176
+ }
177
+ if (managementApiEnabled && !managementApiSecret && config.server.api_secret) {
178
+ diagnostics.push({
179
+ level: 'warn',
180
+ message: 'management_api is using server.api_secret; set server.management_api.api_secret to separate the control-plane secret from the data-plane fallback.',
181
+ suggestion: 'Generate a fresh management secret, set server.management_api.api_secret, and update management clients to use it.',
182
+ });
183
+ }
184
+ if (managementApiEnabled &&
185
+ managementApiSecret &&
186
+ config.server.api_secret &&
187
+ managementApiSecret === config.server.api_secret) {
188
+ diagnostics.push({
189
+ level: 'warn',
190
+ message: 'server.management_api.api_secret matches server.api_secret; rotate one secret to separate the control-plane credential from the data-plane fallback.',
191
+ suggestion: 'Use different resolved values for server.management_api.api_secret and server.api_secret.',
192
+ });
193
+ }
194
+ if (managementApiEnabled &&
195
+ !isLoopbackHost(config.server.management_api.host) &&
196
+ !config.server.management_api.insecure_remote_bind) {
197
+ diagnostics.push({
198
+ level: 'error',
199
+ message: 'server.management_api.host is non-loopback while insecure_remote_bind is false.',
200
+ suggestion: 'Keep server.management_api.host on 127.0.0.1/::1, or explicitly set server.management_api.insecure_remote_bind: true and restrict the control-plane port with network ACLs.',
201
+ });
202
+ }
203
+ if (managementApiEnabled && config.server.management_api.port === config.server.port) {
204
+ diagnostics.push({
205
+ level: 'error',
206
+ message: 'Control-plane and data-plane must not share a socket.',
207
+ suggestion: 'Set server.management_api.port to a different port than server.port so admin/audit/approval routes cannot co-host with agent MCP routes.',
208
+ });
209
+ }
83
210
  for (const [agentId, agent] of Object.entries(config.agents)) {
84
211
  // Check for unknown profile references
85
212
  for (const ref of agent.extends) {
86
213
  if (!profileNames.has(ref)) {
87
214
  diagnostics.push({
88
215
  level: 'error',
216
+ code: 'unknown-profile-ref',
89
217
  agent: agentId,
90
218
  message: `extends references unknown profile "${ref}".`,
91
219
  suggestion: `Add "${ref}" to your profiles block, or check for typos.`,
92
220
  });
93
221
  }
94
222
  }
223
+ diagnostics.push(...validateArgScopeRefs(config, agent.arg_scope, `agents.${agentId}.arg_scope`, agentId));
224
+ diagnostics.push(...validateArgPolicyRefs(config, agent.arg_policy, `agents.${agentId}.arg_policy`, agentId));
95
225
  for (const presetName of agent.sandbox.presets) {
96
226
  if (!sandboxPresetNames.has(presetName)) {
97
227
  diagnostics.push({
@@ -103,6 +233,7 @@ export function validateConfig(config) {
103
233
  }
104
234
  }
105
235
  for (const [toolName, override] of Object.entries(agent.tool_overrides)) {
236
+ diagnostics.push(...validateArgPolicyRefs(config, override.args ? { [toolName]: override.args } : undefined, `agents.${agentId}.tool_overrides.${toolName}.args`, agentId));
106
237
  for (const presetName of override.sandbox_presets ?? []) {
107
238
  if (!sandboxPresetNames.has(presetName)) {
108
239
  diagnostics.push({
@@ -117,6 +248,9 @@ export function validateConfig(config) {
117
248
  // Collect all referenced namespaces from allow/ask/deny
118
249
  const allPatterns = [...agent.allow, ...agent.ask, ...agent.deny];
119
250
  const referencedNamespaces = new Set();
251
+ const toolOverrideNamespaces = new Set(Object.keys(agent.tool_overrides)
252
+ .map((toolName) => toolName.split('/')[0])
253
+ .filter((namespace) => !!namespace));
120
254
  for (const pattern of allPatterns) {
121
255
  const ns = pattern.split('/')[0];
122
256
  if (ns)
@@ -124,7 +258,7 @@ export function validateConfig(config) {
124
258
  }
125
259
  // Check for unknown providers
126
260
  for (const ns of referencedNamespaces) {
127
- if (!providerNames.has(ns)) {
261
+ if (!providerNames.has(ns) && !toolOverrideNamespaces.has(ns)) {
128
262
  diagnostics.push({
129
263
  level: 'error',
130
264
  agent: agentId,
@@ -150,6 +284,15 @@ export function validateConfig(config) {
150
284
  }
151
285
  // Check for unreachable ask: ask pattern also matched by deny
152
286
  for (const askPattern of agent.ask) {
287
+ const forbiddenAirlockTool = AIRLOCK_NON_ASK_TOOLS.find((tool) => matches(askPattern, tool));
288
+ if (forbiddenAirlockTool) {
289
+ diagnostics.push({
290
+ level: 'error',
291
+ agent: agentId,
292
+ message: `"${askPattern}" puts ${forbiddenAirlockTool} behind ask, which is not allowed.`,
293
+ suggestion: 'Move Airlock human-attention tools to allow or deny. They cannot require approval without creating recursive approval.',
294
+ });
295
+ }
153
296
  for (const denyPattern of agent.deny) {
154
297
  if (matches(denyPattern, askPattern) ||
155
298
  matches(askPattern, denyPattern) ||
@@ -184,7 +327,7 @@ export function validateConfig(config) {
184
327
  });
185
328
  }
186
329
  // Check empty agent
187
- if (agent.allow.length === 0 && agent.ask.length === 0) {
330
+ if (agent.extends.length === 0 && agent.allow.length === 0 && agent.ask.length === 0) {
188
331
  diagnostics.push({
189
332
  level: 'info',
190
333
  agent: agentId,
@@ -201,6 +344,20 @@ export function validateConfig(config) {
201
344
  });
202
345
  }
203
346
  }
347
+ for (const [profileId, profile] of Object.entries(config.profiles)) {
348
+ for (const ref of profile.extends) {
349
+ if (!profileNames.has(ref)) {
350
+ diagnostics.push({
351
+ level: 'error',
352
+ code: 'unknown-profile-ref',
353
+ message: `Profile "${profileId}" extends unknown profile "${ref}".`,
354
+ suggestion: `Add "${ref}" to your profiles block, or check for typos.`,
355
+ });
356
+ }
357
+ }
358
+ diagnostics.push(...validateArgScopeRefs(config, profile.arg_scope, `profiles.${profileId}.arg_scope`));
359
+ diagnostics.push(...validateArgPolicyRefs(config, profile.arg_policy, `profiles.${profileId}.arg_policy`));
360
+ }
204
361
  // Validate CLI configs
205
362
  for (const [cliId, cli] of Object.entries(config.clis)) {
206
363
  for (const [cmdName, cmd] of Object.entries(cli.commands)) {
@@ -225,6 +382,625 @@ export function validateConfig(config) {
225
382
  }
226
383
  return diagnostics;
227
384
  }
385
+ function validateArgScopeRefs(config, argScope, location, agent) {
386
+ const diagnostics = [];
387
+ if (!argScope)
388
+ return diagnostics;
389
+ const providerNames = new Set(Object.keys(config.providers));
390
+ for (const [dimensionName, valueSetNames] of Object.entries(argScope)) {
391
+ const dimension = config.arg_dimensions[dimensionName];
392
+ if (!dimension) {
393
+ diagnostics.push({
394
+ level: 'error',
395
+ code: 'unknown-arg-dimension-ref',
396
+ agent,
397
+ message: `${location} references unknown arg_dimension "${dimensionName}".`,
398
+ suggestion: `Add "${dimensionName}" to arg_dimensions, or check for typos.`,
399
+ });
400
+ continue;
401
+ }
402
+ for (const valueSetName of valueSetNames) {
403
+ if (!config.value_sets[valueSetName]) {
404
+ diagnostics.push({
405
+ level: 'error',
406
+ code: 'unknown-value-set-ref',
407
+ agent,
408
+ message: `${location}.${dimensionName} references unknown value_set "${valueSetName}".`,
409
+ suggestion: `Add "${valueSetName}" to value_sets, or check for typos.`,
410
+ });
411
+ }
412
+ }
413
+ for (const toolName of Object.keys(dimension.bindings)) {
414
+ const providerName = toolName.split('/')[0];
415
+ if (providerName && !providerNames.has(providerName)) {
416
+ diagnostics.push({
417
+ level: 'warn',
418
+ agent,
419
+ message: `arg_dimensions.${dimensionName}.bindings references tool "${toolName}" from unknown provider "${providerName}".`,
420
+ suggestion: 'Declare the provider, remove the binding, or leave it only if this config fragment is merged with providers elsewhere.',
421
+ });
422
+ }
423
+ }
424
+ }
425
+ return diagnostics;
426
+ }
427
+ function validateArgPolicyRefs(config, argPolicy, location, agent) {
428
+ const diagnostics = [];
429
+ if (!argPolicy)
430
+ return diagnostics;
431
+ for (const [toolName, toolPolicy] of Object.entries(argPolicy)) {
432
+ for (const [argName, constraints] of Object.entries(toolPolicy)) {
433
+ for (const constraint of constraints) {
434
+ const valueSetName = constraint.in ?? constraint.glob_in ?? constraint.each_in;
435
+ if (valueSetName && !config.value_sets[valueSetName]) {
436
+ diagnostics.push({
437
+ level: 'error',
438
+ code: 'unknown-value-set-ref',
439
+ agent,
440
+ message: `${location}.${toolName}.${argName} references unknown value_set "${valueSetName}".`,
441
+ suggestion: `Add "${valueSetName}" to value_sets, or check for typos.`,
442
+ });
443
+ }
444
+ }
445
+ }
446
+ }
447
+ return diagnostics;
448
+ }
449
+ function validateRawArgDimensionUsage(config) {
450
+ const diagnostics = [];
451
+ const usedDimensions = new Set();
452
+ for (const [dimensionName, dimension] of Object.entries(config.arg_dimensions)) {
453
+ if (Object.keys(dimension.bindings).length === 0) {
454
+ diagnostics.push({
455
+ level: 'warn',
456
+ message: `arg_dimensions.${dimensionName}.bindings is empty.`,
457
+ suggestion: 'Add at least one tool-to-argument binding, or remove the dimension so arg_scope cannot resolve to a no-op.',
458
+ });
459
+ }
460
+ }
461
+ for (const agentName of Object.keys(config.agents)) {
462
+ for (const dimensionName of collectAgentArgScopeDimensions(config, agentName)) {
463
+ usedDimensions.add(dimensionName);
464
+ }
465
+ }
466
+ for (const dimensionName of Object.keys(config.arg_dimensions)) {
467
+ if (!usedDimensions.has(dimensionName)) {
468
+ diagnostics.push({
469
+ level: 'warn',
470
+ message: `arg_dimensions.${dimensionName} is not used by any scoped agent.`,
471
+ suggestion: 'Attach it through an agent arg_scope or through a profile that at least one agent extends.',
472
+ });
473
+ }
474
+ }
475
+ return diagnostics;
476
+ }
477
+ function validateEffectiveArgRestrictions(rawConfig, resolvedConfig) {
478
+ const diagnostics = [];
479
+ for (const [agentName, resolvedAgent] of Object.entries(resolvedConfig.agents)) {
480
+ const sources = collectAgentArgRestrictionSources(rawConfig, agentName);
481
+ if (sources.length === 0)
482
+ continue;
483
+ if (countArgPolicyConstraints(resolvedAgent.arg_policy) === 0) {
484
+ diagnostics.push({
485
+ level: 'error',
486
+ agent: agentName,
487
+ message: `Agent declares arg_scope/arg_policy via ${sources.join(', ')} but resolves to zero effective argument constraints.`,
488
+ suggestion: 'Check that arg_scope dimensions have bindings and referenced value_sets, or remove the no-op restriction.',
489
+ });
490
+ }
491
+ }
492
+ return diagnostics;
493
+ }
494
+ function validateYamlScalarFootguns(config) {
495
+ const diagnostics = [];
496
+ const stringMatchedValueSets = collectStringMatchedValueSets(config);
497
+ for (const [valueSetName, locations] of stringMatchedValueSets) {
498
+ const valueSet = config.value_sets[valueSetName];
499
+ if (!valueSet)
500
+ continue;
501
+ valueSet.values.forEach((value, index) => {
502
+ if (!isUnquotedStringFootgun(value))
503
+ return;
504
+ diagnostics.push({
505
+ level: 'warn',
506
+ message: `value_sets.${valueSetName}[${index}] value ${String(value)} looks like an unquoted string while used by ${locations.join(', ')}.`,
507
+ suggestion: `Quote it (${quoteSuggestion(value)}) if it should match string tool arguments.`,
508
+ });
509
+ });
510
+ }
511
+ for (const source of collectArgPolicySources(config)) {
512
+ for (const [toolName, toolPolicy] of Object.entries(source.policy ?? {})) {
513
+ for (const [argName, constraints] of Object.entries(toolPolicy)) {
514
+ for (const constraint of constraints) {
515
+ if (!usesStringNormalizer(constraint.normalize))
516
+ continue;
517
+ const values = constraint.allow ?? constraint.each_allow;
518
+ if (!values)
519
+ continue;
520
+ values.forEach((value, index) => {
521
+ if (!isUnquotedStringFootgun(value))
522
+ return;
523
+ diagnostics.push({
524
+ level: 'warn',
525
+ agent: source.agent,
526
+ message: `${source.location}.${toolName}.${argName}[${index}] value ${String(value)} looks like an unquoted string while used with normalize: ${constraint.normalize?.join(', ')}.`,
527
+ suggestion: `Quote it (${quoteSuggestion(value)}) so string normalization can apply before matching.`,
528
+ });
529
+ });
530
+ }
531
+ }
532
+ }
533
+ }
534
+ return diagnostics;
535
+ }
536
+ function collectAgentArgScopeDimensions(config, agentName) {
537
+ const agent = config.agents[agentName];
538
+ if (!agent)
539
+ return [];
540
+ const dimensions = new Set();
541
+ const visitedProfiles = new Set();
542
+ function visitProfile(profileName) {
543
+ if (visitedProfiles.has(profileName))
544
+ return;
545
+ visitedProfiles.add(profileName);
546
+ const profile = config.profiles[profileName];
547
+ if (!profile)
548
+ return;
549
+ for (const parentName of profile.extends)
550
+ visitProfile(parentName);
551
+ for (const dimensionName of Object.keys(profile.arg_scope ?? {}))
552
+ dimensions.add(dimensionName);
553
+ }
554
+ for (const profileName of agent.extends)
555
+ visitProfile(profileName);
556
+ for (const dimensionName of Object.keys(agent.arg_scope ?? {}))
557
+ dimensions.add(dimensionName);
558
+ return Array.from(dimensions);
559
+ }
560
+ function collectAgentArgRestrictionSources(config, agentName) {
561
+ const agent = config.agents[agentName];
562
+ if (!agent)
563
+ return [];
564
+ const sources = [];
565
+ const visitedProfiles = new Set();
566
+ function visitProfile(profileName) {
567
+ if (visitedProfiles.has(profileName))
568
+ return;
569
+ visitedProfiles.add(profileName);
570
+ const profile = config.profiles[profileName];
571
+ if (!profile)
572
+ return;
573
+ for (const parentName of profile.extends)
574
+ visitProfile(parentName);
575
+ if (profile.arg_scope)
576
+ sources.push(`profiles.${profileName}.arg_scope`);
577
+ if (profile.arg_policy)
578
+ sources.push(`profiles.${profileName}.arg_policy`);
579
+ }
580
+ for (const profileName of agent.extends)
581
+ visitProfile(profileName);
582
+ if (agent.arg_scope)
583
+ sources.push(`agents.${agentName}.arg_scope`);
584
+ if (agent.arg_policy)
585
+ sources.push(`agents.${agentName}.arg_policy`);
586
+ return sources;
587
+ }
588
+ function countArgPolicyConstraints(argPolicy) {
589
+ let count = 0;
590
+ for (const toolPolicy of Object.values(argPolicy ?? {})) {
591
+ for (const constraints of Object.values(toolPolicy)) {
592
+ count += constraints.length;
593
+ }
594
+ }
595
+ return count;
596
+ }
597
+ function collectStringMatchedValueSets(config) {
598
+ const valueSets = new Map();
599
+ function add(valueSetName, location) {
600
+ if (!valueSetName)
601
+ return;
602
+ const locations = valueSets.get(valueSetName) ?? [];
603
+ if (!locations.includes(location))
604
+ locations.push(location);
605
+ valueSets.set(valueSetName, locations);
606
+ }
607
+ for (const [agentName, agent] of Object.entries(config.agents)) {
608
+ collectArgScopeValueSets(config, agent.arg_scope, `agents.${agentName}.arg_scope`, add);
609
+ }
610
+ for (const [profileName, profile] of Object.entries(config.profiles)) {
611
+ collectArgScopeValueSets(config, profile.arg_scope, `profiles.${profileName}.arg_scope`, add);
612
+ }
613
+ for (const source of collectArgPolicySources(config)) {
614
+ for (const [toolName, toolPolicy] of Object.entries(source.policy ?? {})) {
615
+ for (const [argName, constraints] of Object.entries(toolPolicy)) {
616
+ for (const constraint of constraints) {
617
+ add(constraint.in, `${source.location}.${toolName}.${argName}.in`);
618
+ add(constraint.glob_in, `${source.location}.${toolName}.${argName}.glob_in`);
619
+ if (usesStringNormalizer(constraint.normalize)) {
620
+ add(constraint.each_in, `${source.location}.${toolName}.${argName}.each_in`);
621
+ }
622
+ }
623
+ }
624
+ }
625
+ }
626
+ return valueSets;
627
+ }
628
+ function collectArgScopeValueSets(config, argScope, location, add) {
629
+ for (const [dimensionName, valueSetNames] of Object.entries(argScope ?? {})) {
630
+ const dimension = config.arg_dimensions[dimensionName];
631
+ const stringMatched = dimension?.match === 'in' ||
632
+ dimension?.match === 'glob_in' ||
633
+ usesStringNormalizer(dimension?.normalize);
634
+ if (!stringMatched)
635
+ continue;
636
+ for (const valueSetName of valueSetNames) {
637
+ add(valueSetName, `${location}.${dimensionName}`);
638
+ }
639
+ }
640
+ }
641
+ function collectArgPolicySources(config) {
642
+ const sources = [];
643
+ for (const [agentName, agent] of Object.entries(config.agents)) {
644
+ sources.push({
645
+ location: `agents.${agentName}.arg_policy`,
646
+ agent: agentName,
647
+ policy: agent.arg_policy,
648
+ });
649
+ for (const [toolName, override] of Object.entries(agent.tool_overrides)) {
650
+ if (!override.args)
651
+ continue;
652
+ sources.push({
653
+ location: `agents.${agentName}.tool_overrides.${toolName}.args`,
654
+ agent: agentName,
655
+ policy: { [toolName]: override.args },
656
+ });
657
+ }
658
+ }
659
+ for (const [profileName, profile] of Object.entries(config.profiles)) {
660
+ sources.push({ location: `profiles.${profileName}.arg_policy`, policy: profile.arg_policy });
661
+ }
662
+ return sources;
663
+ }
664
+ function usesStringNormalizer(normalizers) {
665
+ return !!normalizers?.some((normalizer) => ['phone', 'email', 'lower', 'trim'].includes(normalizer));
666
+ }
667
+ function isUnquotedStringFootgun(value) {
668
+ return typeof value === 'number' || typeof value === 'boolean';
669
+ }
670
+ function quoteSuggestion(value) {
671
+ if (typeof value === 'number' && Number.isInteger(value) && value > 1_000_000_000) {
672
+ return `"+${value}"`;
673
+ }
674
+ return `"${String(value)}"`;
675
+ }
676
+ function dedupeDiagnostics(diagnostics) {
677
+ const seen = new Set();
678
+ const result = [];
679
+ for (const diagnostic of diagnostics) {
680
+ const key = `${diagnostic.level}\0${diagnostic.agent ?? ''}\0${diagnostic.message}`;
681
+ if (seen.has(key))
682
+ continue;
683
+ seen.add(key);
684
+ result.push(diagnostic);
685
+ }
686
+ return result;
687
+ }
688
+ function findUnknownKeys(value) {
689
+ const diagnostics = [];
690
+ checkObjectKeys(diagnostics, value, [], rootKeys);
691
+ return diagnostics;
692
+ }
693
+ const rootKeys = new Set([
694
+ 'providers',
695
+ 'value_sets',
696
+ 'arg_dimensions',
697
+ 'profiles',
698
+ 'sandbox_presets',
699
+ 'clis',
700
+ 'apis',
701
+ 'agents',
702
+ 'approvals',
703
+ 'security',
704
+ 'audit',
705
+ 'server',
706
+ 'lint',
707
+ ]);
708
+ const providerKeys = new Set([
709
+ 'type',
710
+ 'enabled',
711
+ 'command',
712
+ 'args',
713
+ 'env',
714
+ 'url',
715
+ 'headers',
716
+ 'oauth',
717
+ 'oauth_callback_port',
718
+ 'oauth_callback_url',
719
+ 'client_id',
720
+ 'client_secret',
721
+ ]);
722
+ const agentKeys = new Set([
723
+ 'token',
724
+ 'extends',
725
+ 'allow',
726
+ 'remember_allow',
727
+ 'ask',
728
+ 'deny',
729
+ 'tool_overrides',
730
+ 'arg_policy',
731
+ 'arg_scope',
732
+ 'exec',
733
+ 'http',
734
+ 'sandbox',
735
+ 'middleware',
736
+ ]);
737
+ const profileKeys = new Set(['extends', 'allow', 'ask', 'deny', 'arg_policy', 'arg_scope']);
738
+ const serverKeys = new Set([
739
+ 'port',
740
+ 'host',
741
+ 'api_secret',
742
+ 'auth_required',
743
+ 'require_agent_tokens',
744
+ 'allowed_origins',
745
+ 'expose_tools_api',
746
+ 'management_api',
747
+ 'expose_management_api',
748
+ 'expose_hook_api',
749
+ ]);
750
+ const managementApiKeys = new Set([
751
+ 'enabled',
752
+ 'api_secret',
753
+ 'host',
754
+ 'port',
755
+ 'insecure_remote_bind',
756
+ 'expose_hook_api',
757
+ ]);
758
+ const argDimensionKeys = new Set(['match', 'normalize', 'bindings']);
759
+ const valueSetKeys = new Set(['values', 'expose_values']);
760
+ const sandboxOverrideKeys = new Set(['filesystem', 'network']);
761
+ const sandboxKeys = new Set(['enabled', 'presets', 'filesystem', 'network', 'overrides']);
762
+ const filesystemKeys = new Set(['allow_write', 'deny_read', 'deny_write', 'allow_read']);
763
+ const networkKeys = new Set(['allowed_domains', 'denied_domains']);
764
+ const toolOverrideKeys = new Set(['description', 'alias_of', 'sandbox_presets', 'sandbox', 'args']);
765
+ const argConstraintKeys = new Set([
766
+ 'allow',
767
+ 'equals',
768
+ 'in',
769
+ 'glob_in',
770
+ 'each_in',
771
+ 'glob_allow',
772
+ 'each_allow',
773
+ 'normalize',
774
+ 'path',
775
+ 'required',
776
+ 'label',
777
+ 'value_set',
778
+ 'expose_values',
779
+ ]);
780
+ const agentExecKeys = new Set(['allow', 'ask', 'deny', 'env', 'default_timeout_ms']);
781
+ const agentHttpKeys = new Set(['domain_allowlist', 'max_response_bytes', 'timeout_ms']);
782
+ const rememberAllowKeys = new Set(['tool', 'expires_at']);
783
+ const approvalsKeys = new Set(['provider', 'timeout_ms', 'batch_window_ms']);
784
+ const approvalProviderKeys = new Set([
785
+ 'type',
786
+ 'bot_token',
787
+ 'chat_id',
788
+ 'gateway_url',
789
+ 'token',
790
+ 'session_key',
791
+ 'webhook_url',
792
+ 'url',
793
+ 'headers',
794
+ 'sound',
795
+ 'host',
796
+ 'port',
797
+ 'team_id',
798
+ 'key_id',
799
+ 'key_path',
800
+ 'bundle_id',
801
+ 'production',
802
+ 'interruption_level',
803
+ ]);
804
+ const securityKeys = new Set(['blocked_hosts', 'allowed_local']);
805
+ const auditKeys = new Set(['db_path', 'retention_days', 'redact_fields']);
806
+ const cliKeys = new Set(['discovered', 'shell', 'cwd', 'max_output_bytes', 'commands']);
807
+ const cliCommandKeys = new Set(['exec', 'description', 'params', 'cwd', 'timeout']);
808
+ const cliParamKeys = new Set(['type', 'flag', 'positional', 'required', 'default', 'description']);
809
+ const apiKeys = new Set([
810
+ 'spec',
811
+ 'base_url',
812
+ 'auth',
813
+ 'include',
814
+ 'exclude',
815
+ 'timeout_ms',
816
+ 'max_response_bytes',
817
+ ]);
818
+ const apiAuthKeys = new Set(['type', 'token', 'name', 'value']);
819
+ const middlewareKeys = new Set([
820
+ 'name',
821
+ 'enabled',
822
+ 'tools',
823
+ 'exclude',
824
+ 'max_requests',
825
+ 'window_ms',
826
+ 'per',
827
+ 'mode',
828
+ 'backend',
829
+ 'inference_url',
830
+ 'threshold',
831
+ 'max_lines',
832
+ 'max_chars',
833
+ 'model',
834
+ 'threshold_chars',
835
+ ]);
836
+ const lintKeys = new Set(['disable', 'severity']);
837
+ function checkObjectKeys(diagnostics, value, path, allowed) {
838
+ if (!isRecord(value))
839
+ return;
840
+ for (const key of Object.keys(value)) {
841
+ if (!allowed.has(key)) {
842
+ diagnostics.push({
843
+ level: 'error',
844
+ message: unknownKeyMessage(key, path),
845
+ suggestion: unknownKeySuggestion(key, path),
846
+ });
847
+ }
848
+ }
849
+ if (path.length === 0) {
850
+ checkRecordChildren(diagnostics, value.providers, ['providers'], providerKeys);
851
+ checkRecordChildren(diagnostics, value.value_sets, ['value_sets'], valueSetKeys);
852
+ checkRecordChildren(diagnostics, value.agents, ['agents'], agentKeys);
853
+ checkRecordChildren(diagnostics, value.profiles, ['profiles'], profileKeys);
854
+ checkRecordChildren(diagnostics, value.arg_dimensions, ['arg_dimensions'], argDimensionKeys);
855
+ checkRecordChildren(diagnostics, value.sandbox_presets, ['sandbox_presets'], sandboxOverrideKeys);
856
+ checkRecordChildren(diagnostics, value.clis, ['clis'], cliKeys);
857
+ checkRecordChildren(diagnostics, value.apis, ['apis'], apiKeys);
858
+ checkObjectKeys(diagnostics, value.approvals, ['approvals'], approvalsKeys);
859
+ checkObjectKeys(diagnostics, value.security, ['security'], securityKeys);
860
+ checkObjectKeys(diagnostics, value.audit, ['audit'], auditKeys);
861
+ checkObjectKeys(diagnostics, value.server, ['server'], serverKeys);
862
+ checkObjectKeys(diagnostics, value.lint, ['lint'], lintKeys);
863
+ }
864
+ else if (path[0] === 'agents' && path.length === 2) {
865
+ checkAgentChildren(diagnostics, value, path);
866
+ }
867
+ else if (path[0] === 'profiles' && path.length === 2) {
868
+ checkArgPolicyKeys(diagnostics, value.arg_policy, [...path, 'arg_policy']);
869
+ }
870
+ else if (path[0] === 'sandbox_presets' && path.length === 2) {
871
+ checkSandboxOverrideChildren(diagnostics, value, path);
872
+ }
873
+ else if (path[0] === 'clis' && path.length === 2) {
874
+ checkRecordChildren(diagnostics, value.commands, [...path, 'commands'], cliCommandKeys);
875
+ }
876
+ else if (path[0] === 'clis' && path[2] === 'commands' && path.length === 4) {
877
+ checkRecordChildren(diagnostics, value.params, [...path, 'params'], cliParamKeys);
878
+ }
879
+ else if (path[0] === 'apis' && path.length === 2) {
880
+ checkObjectKeys(diagnostics, value.auth, [...path, 'auth'], apiAuthKeys);
881
+ }
882
+ else if (path.join('.') === 'approvals') {
883
+ checkApprovalProviderKeys(diagnostics, value.provider, ['approvals', 'provider']);
884
+ }
885
+ else if (path.join('.') === 'server') {
886
+ checkObjectKeys(diagnostics, value.management_api, ['server', 'management_api'], managementApiKeys);
887
+ }
888
+ else if (path.at(-1) === 'sandbox' || path.join('.') === 'agents') {
889
+ checkSandboxChildren(diagnostics, value, path);
890
+ }
891
+ }
892
+ function checkAgentChildren(diagnostics, value, path) {
893
+ checkArrayChildren(diagnostics, value.remember_allow, [...path, 'remember_allow'], rememberAllowKeys);
894
+ checkRecordChildren(diagnostics, value.tool_overrides, [...path, 'tool_overrides'], toolOverrideKeys);
895
+ checkArgPolicyKeys(diagnostics, value.arg_policy, [...path, 'arg_policy']);
896
+ checkObjectKeys(diagnostics, value.exec, [...path, 'exec'], agentExecKeys);
897
+ checkObjectKeys(diagnostics, value.http, [...path, 'http'], agentHttpKeys);
898
+ checkObjectKeys(diagnostics, value.sandbox, [...path, 'sandbox'], sandboxKeys);
899
+ checkArrayChildren(diagnostics, value.middleware, [...path, 'middleware'], middlewareKeys);
900
+ }
901
+ function checkSandboxChildren(diagnostics, value, path) {
902
+ if (!isRecord(value))
903
+ return;
904
+ checkObjectKeys(diagnostics, value.filesystem, [...path, 'filesystem'], filesystemKeys);
905
+ checkObjectKeys(diagnostics, value.network, [...path, 'network'], networkKeys);
906
+ checkRecordChildren(diagnostics, value.overrides, [...path, 'overrides'], sandboxOverrideKeys);
907
+ }
908
+ function checkSandboxOverrideChildren(diagnostics, value, path) {
909
+ if (!isRecord(value))
910
+ return;
911
+ checkObjectKeys(diagnostics, value.filesystem, [...path, 'filesystem'], filesystemKeys);
912
+ checkObjectKeys(diagnostics, value.network, [...path, 'network'], networkKeys);
913
+ }
914
+ function checkToolOverrideChildren(diagnostics, value, path) {
915
+ checkSandboxOverrideChildren(diagnostics, value.sandbox, [...path, 'sandbox']);
916
+ checkArgPolicyKeys(diagnostics, value.args, [...path, 'args']);
917
+ }
918
+ function checkArgPolicyKeys(diagnostics, value, path) {
919
+ if (!isRecord(value))
920
+ return;
921
+ for (const [toolName, toolPolicy] of Object.entries(value)) {
922
+ if (!isRecord(toolPolicy))
923
+ continue;
924
+ for (const [argName, constraintOrList] of Object.entries(toolPolicy)) {
925
+ const constraints = Array.isArray(constraintOrList) ? constraintOrList : [constraintOrList];
926
+ constraints.forEach((constraint, index) => {
927
+ const constraintPath = [
928
+ ...path,
929
+ toolName,
930
+ argName,
931
+ ...(Array.isArray(constraintOrList) ? [String(index)] : []),
932
+ ];
933
+ checkObjectKeys(diagnostics, constraint, constraintPath, argConstraintKeys);
934
+ });
935
+ }
936
+ }
937
+ }
938
+ function checkApprovalProviderKeys(diagnostics, value, path) {
939
+ if (Array.isArray(value)) {
940
+ value.forEach((provider, index) => checkObjectKeys(diagnostics, provider, [...path, String(index)], approvalProviderKeys));
941
+ return;
942
+ }
943
+ checkObjectKeys(diagnostics, value, path, approvalProviderKeys);
944
+ }
945
+ function checkArrayChildren(diagnostics, value, path, allowed) {
946
+ if (!Array.isArray(value))
947
+ return;
948
+ value.forEach((child, index) => {
949
+ checkObjectKeys(diagnostics, child, [...path, String(index)], allowed);
950
+ });
951
+ }
952
+ function checkRecordChildren(diagnostics, value, path, allowed) {
953
+ if (!isRecord(value))
954
+ return;
955
+ for (const [name, child] of Object.entries(value)) {
956
+ if (isRecord(child)) {
957
+ checkObjectKeys(diagnostics, child, [...path, name], allowed);
958
+ if (path[0] === 'agents' && path[2] === 'tool_overrides') {
959
+ checkToolOverrideChildren(diagnostics, child, [...path, name]);
960
+ }
961
+ else if (path[0] === 'clis' && path[2] === 'commands') {
962
+ checkRecordChildren(diagnostics, child.params, [...path, name, 'params'], cliParamKeys);
963
+ }
964
+ else if (path[0] === 'sandbox_presets' || path.at(-1) === 'overrides') {
965
+ checkSandboxOverrideChildren(diagnostics, child, [...path, name]);
966
+ }
967
+ }
968
+ }
969
+ }
970
+ function unknownKeyMessage(key, path) {
971
+ if (path[0] === 'profiles' && path.length === 2) {
972
+ return `Unrecognized key "${key}" in profile "${path[1]}".`;
973
+ }
974
+ if (path[0] === 'agents' && path.length === 2) {
975
+ return `Unrecognized key "${key}" in agent "${path[1]}".`;
976
+ }
977
+ return `Unknown config key "${[...path, key].join('.')}".`;
978
+ }
979
+ function unknownKeySuggestion(key, path) {
980
+ if (key === 'scope' && (path[0] === 'agents' || path[0] === 'profiles')) {
981
+ return 'Did you mean "arg_scope"?';
982
+ }
983
+ if (key === 'mcps')
984
+ return 'Use "providers" to declare MCP servers and builtins.';
985
+ if (key === 'hitl')
986
+ return 'Use "ask" for agent tool routing or "approvals" for approval providers.';
987
+ return undefined;
988
+ }
989
+ function isRecord(value) {
990
+ return !!value && typeof value === 'object' && !Array.isArray(value);
991
+ }
992
+ function isLoopbackHost(host) {
993
+ const normalized = host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : host;
994
+ if (normalized === 'localhost')
995
+ return true;
996
+ const ipVersion = isIP(normalized);
997
+ if (ipVersion === 6)
998
+ return normalized === '::1';
999
+ if (ipVersion !== 4)
1000
+ return false;
1001
+ const firstOctet = Number(normalized.split('.')[0]);
1002
+ return firstOctet === 127;
1003
+ }
228
1004
  /** Check if two glob patterns could match the same tool name */
229
1005
  function patternsOverlap(a, b) {
230
1006
  // If either is a wildcard that covers the other's namespace