airlock-bot 0.2.37 → 0.2.38
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -8
- package/dist/activity/stream.d.ts +29 -0
- package/dist/activity/stream.d.ts.map +1 -0
- package/dist/activity/stream.js +36 -0
- package/dist/activity/stream.js.map +1 -0
- package/dist/airlock/context.d.ts +12 -0
- package/dist/airlock/context.d.ts.map +1 -0
- package/dist/airlock/context.js +32 -0
- package/dist/airlock/context.js.map +1 -0
- package/dist/airlock/tools.d.ts +7 -0
- package/dist/airlock/tools.d.ts.map +1 -0
- package/dist/airlock/tools.js +13 -0
- package/dist/airlock/tools.js.map +1 -0
- package/dist/allowlist/engine.d.ts +12 -0
- package/dist/allowlist/engine.d.ts.map +1 -1
- package/dist/allowlist/engine.js +40 -15
- package/dist/allowlist/engine.js.map +1 -1
- package/dist/audit/api.d.ts +2 -0
- package/dist/audit/api.d.ts.map +1 -1
- package/dist/audit/api.js.map +1 -1
- package/dist/audit/db.d.ts +16 -0
- package/dist/audit/db.d.ts.map +1 -1
- package/dist/audit/db.js +73 -0
- package/dist/audit/db.js.map +1 -1
- package/dist/audit/logger.d.ts +8 -1
- package/dist/audit/logger.d.ts.map +1 -1
- package/dist/audit/logger.js +21 -0
- package/dist/audit/logger.js.map +1 -1
- package/dist/backend/airlock-adapter.d.ts +22 -0
- package/dist/backend/airlock-adapter.d.ts.map +1 -0
- package/dist/backend/airlock-adapter.js +136 -0
- package/dist/backend/airlock-adapter.js.map +1 -0
- package/dist/backend/factory.d.ts +5 -1
- package/dist/backend/factory.d.ts.map +1 -1
- package/dist/backend/factory.js +5 -5
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/mcp-adapter.d.ts.map +1 -1
- package/dist/backend/mcp-adapter.js +28 -4
- package/dist/backend/mcp-adapter.js.map +1 -1
- package/dist/backend/openapi/adapter.d.ts.map +1 -1
- package/dist/backend/openapi/adapter.js +33 -13
- package/dist/backend/openapi/adapter.js.map +1 -1
- package/dist/config/loader.d.ts +12 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +799 -23
- package/dist/config/loader.js.map +1 -1
- package/dist/config/profiles.d.ts +44 -3
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +314 -3
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/schema.d.ts +3300 -201
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +272 -71
- package/dist/config/schema.js.map +1 -1
- package/dist/configure-web/cli.d.ts +1 -0
- package/dist/configure-web/cli.d.ts.map +1 -1
- package/dist/configure-web/cli.js +238 -47
- package/dist/configure-web/cli.js.map +1 -1
- package/dist/gateway.d.ts +16 -1
- package/dist/gateway.d.ts.map +1 -1
- package/dist/gateway.js +263 -57
- package/dist/gateway.js.map +1 -1
- package/dist/hitl/api.d.ts +2 -0
- package/dist/hitl/api.d.ts.map +1 -1
- package/dist/hitl/api.js +8 -0
- package/dist/hitl/api.js.map +1 -1
- package/dist/hitl/approval-dashboard.d.ts +5 -9
- package/dist/hitl/approval-dashboard.d.ts.map +1 -1
- package/dist/hitl/approval-dashboard.js +8 -55
- package/dist/hitl/approval-dashboard.js.map +1 -1
- package/dist/hitl/approval-stream.d.ts +34 -0
- package/dist/hitl/approval-stream.d.ts.map +1 -0
- package/dist/hitl/approval-stream.js +111 -0
- package/dist/hitl/approval-stream.js.map +1 -0
- package/dist/hitl/engine.d.ts +16 -7
- package/dist/hitl/engine.d.ts.map +1 -1
- package/dist/hitl/engine.js +109 -35
- package/dist/hitl/engine.js.map +1 -1
- package/dist/hitl/formatter.d.ts.map +1 -1
- package/dist/hitl/formatter.js +2 -0
- package/dist/hitl/formatter.js.map +1 -1
- package/dist/hitl/provider-factory.d.ts +8 -2
- package/dist/hitl/provider-factory.d.ts.map +1 -1
- package/dist/hitl/provider-factory.js +23 -2
- package/dist/hitl/provider-factory.js.map +1 -1
- package/dist/hitl/providers/composite.d.ts +9 -0
- package/dist/hitl/providers/composite.d.ts.map +1 -1
- package/dist/hitl/providers/composite.js +14 -0
- package/dist/hitl/providers/composite.js.map +1 -1
- package/dist/hitl/providers/dashboard.d.ts +12 -2
- package/dist/hitl/providers/dashboard.d.ts.map +1 -1
- package/dist/hitl/providers/dashboard.js +11 -5
- package/dist/hitl/providers/dashboard.js.map +1 -1
- package/dist/hitl/providers/ios.d.ts +23 -0
- package/dist/hitl/providers/ios.d.ts.map +1 -0
- package/dist/hitl/providers/ios.js +183 -0
- package/dist/hitl/providers/ios.js.map +1 -0
- package/dist/hitl/providers/macos.d.ts.map +1 -1
- package/dist/hitl/providers/macos.js +8 -4
- package/dist/hitl/providers/macos.js.map +1 -1
- package/dist/hitl/providers/openclaw.js +2 -2
- package/dist/hitl/providers/openclaw.js.map +1 -1
- package/dist/hitl/providers/stdio.js +2 -2
- package/dist/hitl/providers/stdio.js.map +1 -1
- package/dist/hitl/providers/telegram.js +2 -2
- package/dist/hitl/providers/telegram.js.map +1 -1
- package/dist/hitl/providers/tui.d.ts.map +1 -1
- package/dist/hitl/providers/tui.js +8 -2
- package/dist/hitl/providers/tui.js.map +1 -1
- package/dist/hitl/providers/types.d.ts +14 -0
- package/dist/hitl/providers/types.d.ts.map +1 -1
- package/dist/hook/api.d.ts +2 -0
- package/dist/hook/api.d.ts.map +1 -1
- package/dist/hook/api.js.map +1 -1
- package/dist/hook/normalizer.js +2 -2
- package/dist/hook/normalizer.js.map +1 -1
- package/dist/index.js +28 -1
- package/dist/index.js.map +1 -1
- package/dist/introspect/cli.d.ts +31 -0
- package/dist/introspect/cli.d.ts.map +1 -0
- package/dist/introspect/cli.js +756 -0
- package/dist/introspect/cli.js.map +1 -0
- package/dist/middleware/chain-builder.d.ts +1 -1
- package/dist/middleware/chain-builder.d.ts.map +1 -1
- package/dist/middleware/chain-builder.js +6 -4
- package/dist/middleware/chain-builder.js.map +1 -1
- package/dist/middleware/core/allowlist.d.ts.map +1 -1
- package/dist/middleware/core/allowlist.js +14 -0
- package/dist/middleware/core/allowlist.js.map +1 -1
- package/dist/middleware/core/arg-policy.d.ts +2 -1
- package/dist/middleware/core/arg-policy.d.ts.map +1 -1
- package/dist/middleware/core/arg-policy.js +156 -46
- package/dist/middleware/core/arg-policy.js.map +1 -1
- package/dist/middleware/core/exec-policy.d.ts.map +1 -1
- package/dist/middleware/core/exec-policy.js +16 -1
- package/dist/middleware/core/exec-policy.js.map +1 -1
- package/dist/middleware/core/execute.d.ts.map +1 -1
- package/dist/middleware/core/execute.js +6 -2
- package/dist/middleware/core/execute.js.map +1 -1
- package/dist/middleware/core/hitl-gate.d.ts.map +1 -1
- package/dist/middleware/core/hitl-gate.js +15 -4
- package/dist/middleware/core/hitl-gate.js.map +1 -1
- package/dist/middleware/core/schema-validator.d.ts.map +1 -1
- package/dist/middleware/core/schema-validator.js +7 -3
- package/dist/middleware/core/schema-validator.js.map +1 -1
- package/dist/middleware/post/canary-token-injector.d.ts +4 -1
- package/dist/middleware/post/canary-token-injector.d.ts.map +1 -1
- package/dist/middleware/post/canary-token-injector.js +5 -1
- package/dist/middleware/post/canary-token-injector.js.map +1 -1
- package/dist/middleware/post/untrusted-envelope.d.ts.map +1 -1
- package/dist/middleware/post/untrusted-envelope.js +7 -1
- package/dist/middleware/post/untrusted-envelope.js.map +1 -1
- package/dist/mobile/api.d.ts +20 -0
- package/dist/mobile/api.d.ts.map +1 -0
- package/dist/mobile/api.js +255 -0
- package/dist/mobile/api.js.map +1 -0
- package/dist/mobile/apns.d.ts +68 -0
- package/dist/mobile/apns.d.ts.map +1 -0
- package/dist/mobile/apns.js +321 -0
- package/dist/mobile/apns.js.map +1 -0
- package/dist/pool/http-client.d.ts +3 -1
- package/dist/pool/http-client.d.ts.map +1 -1
- package/dist/pool/http-client.js +5 -2
- package/dist/pool/http-client.js.map +1 -1
- package/dist/pool/oauth-provider.d.ts +2 -0
- package/dist/pool/oauth-provider.d.ts.map +1 -1
- package/dist/pool/oauth-provider.js +23 -4
- package/dist/pool/oauth-provider.js.map +1 -1
- package/dist/pool/pool.d.ts +2 -1
- package/dist/pool/pool.d.ts.map +1 -1
- package/dist/pool/pool.js +2 -2
- package/dist/pool/pool.js.map +1 -1
- package/dist/pool/sse-client.d.ts +3 -1
- package/dist/pool/sse-client.d.ts.map +1 -1
- package/dist/pool/sse-client.js +5 -2
- package/dist/pool/sse-client.js.map +1 -1
- package/dist/pool/stdio-client.d.ts +3 -1
- package/dist/pool/stdio-client.d.ts.map +1 -1
- package/dist/pool/stdio-client.js +5 -2
- package/dist/pool/stdio-client.js.map +1 -1
- package/dist/registry/airlock-policy.d.ts +3 -0
- package/dist/registry/airlock-policy.d.ts.map +1 -0
- package/dist/registry/airlock-policy.js +50 -0
- package/dist/registry/airlock-policy.js.map +1 -0
- package/dist/registry/registry.d.ts +2 -0
- package/dist/registry/registry.d.ts.map +1 -1
- package/dist/registry/registry.js +19 -13
- package/dist/registry/registry.js.map +1 -1
- package/dist/security/blocked-hosts.d.ts +2 -0
- package/dist/security/blocked-hosts.d.ts.map +1 -1
- package/dist/security/blocked-hosts.js +61 -7
- package/dist/security/blocked-hosts.js.map +1 -1
- package/dist/security/request.d.ts +2 -0
- package/dist/security/request.d.ts.map +1 -1
- package/dist/security/request.js +6 -2
- package/dist/security/request.js.map +1 -1
- package/dist/security/secrets.d.ts +12 -0
- package/dist/security/secrets.d.ts.map +1 -0
- package/dist/security/secrets.js +138 -0
- package/dist/security/secrets.js.map +1 -0
- package/dist/setup-openclaw/cli.js +1 -1
- package/dist/stdio-mode.d.ts.map +1 -1
- package/dist/stdio-mode.js +125 -24
- package/dist/stdio-mode.js.map +1 -1
- package/dist/tools/api.d.ts +4 -1
- package/dist/tools/api.d.ts.map +1 -1
- package/dist/tools/api.js +58 -7
- package/dist/tools/api.js.map +1 -1
- package/dist/tools/exec.d.ts +2 -1
- package/dist/tools/exec.d.ts.map +1 -1
- package/dist/tools/exec.js +4 -3
- package/dist/tools/exec.js.map +1 -1
- package/dist/tools/http.d.ts.map +1 -1
- package/dist/tools/http.js +4 -7
- package/dist/tools/http.js.map +1 -1
- package/dist/transport/agent-server.d.ts +4 -0
- package/dist/transport/agent-server.d.ts.map +1 -1
- package/dist/transport/agent-server.js +23 -11
- package/dist/transport/agent-server.js.map +1 -1
- package/dist/transport/http-server.d.ts +2 -0
- package/dist/transport/http-server.d.ts.map +1 -1
- package/dist/transport/http-server.js +10 -6
- package/dist/transport/http-server.js.map +1 -1
- package/dist/transport/sse-server.d.ts +2 -0
- package/dist/transport/sse-server.d.ts.map +1 -1
- package/dist/transport/sse-server.js +10 -6
- package/dist/transport/sse-server.js.map +1 -1
- package/dist/transport/stdio-server.d.ts.map +1 -1
- package/dist/transport/stdio-server.js +4 -0
- package/dist/transport/stdio-server.js.map +1 -1
- package/dist/util/id.d.ts +1 -1
- package/dist/util/id.d.ts.map +1 -1
- package/dist/util/id.js +3 -3
- package/dist/util/id.js.map +1 -1
- package/examples/claude-code-setup.md +0 -2
- package/examples/docker-compose.yaml +4 -3
- package/examples/docker-gateway.yaml +8 -3
- package/examples/gateway.yaml +8 -5
- package/examples/profiles.yaml +0 -2
- package/extensions/openclaw/index.ts +9 -6
- package/extensions/openclaw/openclaw.plugin.json +5 -5
- package/package.json +3 -1
- package/schema.json +634 -28
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { parseArgs } from 'util';
|
|
2
|
+
import { AllowlistEngine } from '../allowlist/engine.js';
|
|
3
|
+
import { matches } from '../allowlist/pattern.js';
|
|
4
|
+
import { loadConfigDetailed } from '../config/loader.js';
|
|
5
|
+
import { explainAgentPermissions, } from '../config/profiles.js';
|
|
6
|
+
import { LINT_RULE_IDS } from '../config/schema.js';
|
|
7
|
+
import { discoverTools } from '../configure-web/cli.js';
|
|
8
|
+
const DEFAULT_LINT_SEVERITY = {
|
|
9
|
+
'dead-deny': 'info',
|
|
10
|
+
'unused-profile': 'info',
|
|
11
|
+
'unused-value-set': 'info',
|
|
12
|
+
'unused-dimension': 'info',
|
|
13
|
+
'empty-agent': 'warn',
|
|
14
|
+
'missing-env-ref': 'warn',
|
|
15
|
+
'unresolvable-ref': 'warn',
|
|
16
|
+
};
|
|
17
|
+
const LINT_RULE_LABELS = {
|
|
18
|
+
'dead-deny': 'Deny pattern does not overlap any allow/ask pattern.',
|
|
19
|
+
'unused-profile': 'Profile is not referenced by any agent.',
|
|
20
|
+
'unused-value-set': 'Value set is not referenced by arg_scope or arg_policy.',
|
|
21
|
+
'unused-dimension': 'Argument dimension is not referenced by arg_scope.',
|
|
22
|
+
'empty-agent': 'Agent resolves to zero allow/ask tools.',
|
|
23
|
+
'missing-env-ref': 'Environment variable reference is unset.',
|
|
24
|
+
'unresolvable-ref': 'Extends, arg_scope, or value_set reference cannot be resolved.',
|
|
25
|
+
};
|
|
26
|
+
export async function runIntrospection(argv) {
|
|
27
|
+
const command = argv[0];
|
|
28
|
+
if (!command || command === '--help' || command === '-h') {
|
|
29
|
+
printHelp();
|
|
30
|
+
process.exit(command ? 0 : 1);
|
|
31
|
+
}
|
|
32
|
+
if (command === 'config') {
|
|
33
|
+
if (argv[1] !== 'check') {
|
|
34
|
+
console.error(`Unknown config command: "${argv[1] ?? ''}". Use "config check".`);
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
const result = checkConfig(argv.slice(2));
|
|
38
|
+
writeResult(result, result.exitCode);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
if (command === 'explain') {
|
|
42
|
+
const result = await explainCommand(argv.slice(1));
|
|
43
|
+
writeResult(result, result.exitCode);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (command === 'who-can') {
|
|
47
|
+
const result = whoCanCommand(argv.slice(1));
|
|
48
|
+
writeResult(result, result.exitCode);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
if (command === 'tools') {
|
|
52
|
+
const result = await toolsCommand(argv.slice(1));
|
|
53
|
+
writeResult(result, result.exitCode);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (command === 'lint') {
|
|
57
|
+
const result = lintCommand(argv.slice(1));
|
|
58
|
+
writeResult(result, result.exitCode);
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
console.error(`Unknown introspection command: "${command}".`);
|
|
62
|
+
printHelp();
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
export function checkConfig(argv) {
|
|
66
|
+
const { values } = parseArgs({
|
|
67
|
+
args: argv,
|
|
68
|
+
options: {
|
|
69
|
+
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
70
|
+
strict: { type: 'boolean', default: false },
|
|
71
|
+
'no-resolve': { type: 'boolean', default: false },
|
|
72
|
+
'fail-on': { type: 'string', default: 'error' },
|
|
73
|
+
json: { type: 'boolean', default: false },
|
|
74
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
75
|
+
},
|
|
76
|
+
allowPositionals: false,
|
|
77
|
+
});
|
|
78
|
+
if (values.help) {
|
|
79
|
+
return ok(configCheckHelp(), values.json);
|
|
80
|
+
}
|
|
81
|
+
const failOn = parseCheckFailLevel(values['fail-on']);
|
|
82
|
+
const loaded = loadConfigDetailed(values.config ?? './airlock.yaml', {
|
|
83
|
+
strict: values.strict,
|
|
84
|
+
resolveEnv: !values['no-resolve'],
|
|
85
|
+
});
|
|
86
|
+
const exitCode = shouldFail(loaded.diagnostics, failOn) ? 1 : 0;
|
|
87
|
+
const payload = {
|
|
88
|
+
ok: exitCode === 0,
|
|
89
|
+
configPath: values.config,
|
|
90
|
+
strict: values.strict,
|
|
91
|
+
resolveEnv: !values['no-resolve'],
|
|
92
|
+
failOn,
|
|
93
|
+
diagnostics: groupDiagnostics(loaded.diagnostics),
|
|
94
|
+
};
|
|
95
|
+
return {
|
|
96
|
+
json: values.json,
|
|
97
|
+
exitCode,
|
|
98
|
+
data: payload,
|
|
99
|
+
text: formatCheck(payload),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
async function explainCommand(argv) {
|
|
103
|
+
const { values, positionals } = parseArgs({
|
|
104
|
+
args: argv,
|
|
105
|
+
options: {
|
|
106
|
+
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
107
|
+
expand: { type: 'boolean', default: false },
|
|
108
|
+
json: { type: 'boolean', default: false },
|
|
109
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
110
|
+
},
|
|
111
|
+
allowPositionals: true,
|
|
112
|
+
});
|
|
113
|
+
if (values.help || positionals.length === 0) {
|
|
114
|
+
return {
|
|
115
|
+
json: values.json,
|
|
116
|
+
exitCode: positionals.length === 0 && !values.help ? 1 : 0,
|
|
117
|
+
data: explainHelp(),
|
|
118
|
+
text: explainHelp(),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const loaded = loadForCli(values.config ?? './airlock.yaml');
|
|
122
|
+
if ('result' in loaded)
|
|
123
|
+
return loaded.result(values.json);
|
|
124
|
+
const explanation = explainAgentPermissions(loaded.rawConfig, positionals[0]);
|
|
125
|
+
const engine = new AllowlistEngine(loaded.config.agents);
|
|
126
|
+
const expanded = values.expand
|
|
127
|
+
? await expandedDecisions(values.config ?? './airlock.yaml', explanation, engine)
|
|
128
|
+
: undefined;
|
|
129
|
+
const payload = {
|
|
130
|
+
agent: positionals[0],
|
|
131
|
+
explanation,
|
|
132
|
+
precedence: precedenceNotes(explanation),
|
|
133
|
+
expanded,
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
json: values.json,
|
|
137
|
+
exitCode: 0,
|
|
138
|
+
data: payload,
|
|
139
|
+
text: formatExplain(payload),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
function whoCanCommand(argv) {
|
|
143
|
+
const { values, positionals } = parseArgs({
|
|
144
|
+
args: argv,
|
|
145
|
+
options: {
|
|
146
|
+
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
147
|
+
level: { type: 'string', default: 'any' },
|
|
148
|
+
json: { type: 'boolean', default: false },
|
|
149
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
150
|
+
},
|
|
151
|
+
allowPositionals: true,
|
|
152
|
+
});
|
|
153
|
+
if (values.help || positionals.length === 0) {
|
|
154
|
+
return {
|
|
155
|
+
json: values.json,
|
|
156
|
+
exitCode: positionals.length === 0 && !values.help ? 1 : 0,
|
|
157
|
+
data: whoCanHelp(),
|
|
158
|
+
text: whoCanHelp(),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const level = parsePermissionLevel(values.level);
|
|
162
|
+
const loaded = loadForCli(values.config ?? './airlock.yaml');
|
|
163
|
+
if ('result' in loaded)
|
|
164
|
+
return loaded.result(values.json);
|
|
165
|
+
const query = positionals[0];
|
|
166
|
+
const agents = whoCan(loaded.config, query, level);
|
|
167
|
+
const payload = { query, level, agents };
|
|
168
|
+
return {
|
|
169
|
+
json: values.json,
|
|
170
|
+
exitCode: 0,
|
|
171
|
+
data: payload,
|
|
172
|
+
text: formatWhoCan(payload),
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function whoCan(config, query, level = 'any') {
|
|
176
|
+
const engine = new AllowlistEngine(config.agents);
|
|
177
|
+
return Object.keys(config.agents)
|
|
178
|
+
.map((agent) => ({ agent, ...engine.evaluateDetailed(agent, query) }))
|
|
179
|
+
.filter((entry) => level === 'any' ||
|
|
180
|
+
entry.decision === level ||
|
|
181
|
+
(level === 'deny' && entry.decision === 'default-deny'));
|
|
182
|
+
}
|
|
183
|
+
async function toolsCommand(argv) {
|
|
184
|
+
const { values } = parseArgs({
|
|
185
|
+
args: argv,
|
|
186
|
+
options: {
|
|
187
|
+
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
188
|
+
provider: { type: 'string' },
|
|
189
|
+
grep: { type: 'string' },
|
|
190
|
+
json: { type: 'boolean', default: false },
|
|
191
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
192
|
+
},
|
|
193
|
+
allowPositionals: false,
|
|
194
|
+
});
|
|
195
|
+
if (values.help)
|
|
196
|
+
return ok(toolsHelp(), values.json);
|
|
197
|
+
const grep = values.grep ? new RegExp(values.grep, 'i') : undefined;
|
|
198
|
+
const discovered = await discoverTools(values.config ?? './airlock.yaml');
|
|
199
|
+
const tools = discovered.tools
|
|
200
|
+
.filter((tool) => !values.provider || tool.provider === values.provider)
|
|
201
|
+
.filter((tool) => !grep || grep.test(tool.name) || grep.test(tool.description))
|
|
202
|
+
.map((tool) => ({
|
|
203
|
+
name: tool.name,
|
|
204
|
+
provider: tool.provider,
|
|
205
|
+
description: firstLine(tool.description),
|
|
206
|
+
}));
|
|
207
|
+
const payload = { tools, errors: discovered.errors };
|
|
208
|
+
return {
|
|
209
|
+
json: values.json,
|
|
210
|
+
exitCode: discovered.errors.length > 0 ? 1 : 0,
|
|
211
|
+
data: payload,
|
|
212
|
+
text: formatTools(payload),
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
export function lintCommand(argv) {
|
|
216
|
+
const { values } = parseArgs({
|
|
217
|
+
args: argv,
|
|
218
|
+
options: {
|
|
219
|
+
config: { type: 'string', short: 'c', default: './airlock.yaml' },
|
|
220
|
+
verbose: { type: 'boolean', default: false },
|
|
221
|
+
quiet: { type: 'boolean', default: false },
|
|
222
|
+
'fail-on': { type: 'string', default: 'warn' },
|
|
223
|
+
only: { type: 'string', multiple: true },
|
|
224
|
+
disable: { type: 'string', multiple: true },
|
|
225
|
+
rule: { type: 'string', multiple: true },
|
|
226
|
+
json: { type: 'boolean', default: false },
|
|
227
|
+
help: { type: 'boolean', short: 'h', default: false },
|
|
228
|
+
},
|
|
229
|
+
allowPositionals: false,
|
|
230
|
+
});
|
|
231
|
+
if (values.help)
|
|
232
|
+
return ok(lintHelp(), values.json);
|
|
233
|
+
const failOn = parseLintFailLevel(values['fail-on']);
|
|
234
|
+
const loaded = loadConfigDetailed(values.config ?? './airlock.yaml', { resolveEnv: false });
|
|
235
|
+
if (!loaded.config || !loaded.rawConfig) {
|
|
236
|
+
return {
|
|
237
|
+
json: values.json,
|
|
238
|
+
exitCode: 1,
|
|
239
|
+
data: { ok: false, diagnostics: groupDiagnostics(loaded.diagnostics) },
|
|
240
|
+
text: formatCheck({ ok: false, diagnostics: groupDiagnostics(loaded.diagnostics) }),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
const unresolvableDiagnostics = loaded.diagnostics.filter(isUnresolvableRefDiagnostic);
|
|
244
|
+
const fatalDiagnostics = loaded.diagnostics.filter((diagnostic) => diagnostic.level === 'error' && !isUnresolvableRefDiagnostic(diagnostic));
|
|
245
|
+
if (fatalDiagnostics.length > 0) {
|
|
246
|
+
return {
|
|
247
|
+
json: values.json,
|
|
248
|
+
exitCode: 1,
|
|
249
|
+
data: { ok: false, diagnostics: groupDiagnostics(fatalDiagnostics) },
|
|
250
|
+
text: formatCheck({ ok: false, diagnostics: groupDiagnostics(fatalDiagnostics) }),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
const controls = buildLintControls(loaded.rawConfig.lint, {
|
|
254
|
+
only: values.only,
|
|
255
|
+
disable: values.disable,
|
|
256
|
+
rule: values.rule,
|
|
257
|
+
});
|
|
258
|
+
const resolvedConfig = unresolvableDiagnostics.length === 0 ? loaded.config : undefined;
|
|
259
|
+
const findings = applyLintControls([
|
|
260
|
+
...unresolvableDiagnostics.map(lintFindingFromDiagnostic),
|
|
261
|
+
...lintConfig(loaded.rawConfig, resolvedConfig),
|
|
262
|
+
], controls);
|
|
263
|
+
const rules = groupLintFindings(findings);
|
|
264
|
+
const payload = { ok: !shouldFailLint(rules, failOn), failOn, rules };
|
|
265
|
+
return {
|
|
266
|
+
json: values.json,
|
|
267
|
+
exitCode: payload.ok ? 0 : 1,
|
|
268
|
+
data: values.json ? rules : payload,
|
|
269
|
+
text: formatLint(payload, { verbose: values.verbose, quiet: values.quiet }),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export function lintConfig(rawConfig, resolvedConfig) {
|
|
273
|
+
const findings = [];
|
|
274
|
+
const reachableProfiles = new Set();
|
|
275
|
+
const referencedValueSets = new Set();
|
|
276
|
+
const referencedDimensions = new Set();
|
|
277
|
+
function visitProfile(profileName) {
|
|
278
|
+
if (reachableProfiles.has(profileName))
|
|
279
|
+
return;
|
|
280
|
+
const profile = rawConfig.profiles[profileName];
|
|
281
|
+
if (!profile)
|
|
282
|
+
return;
|
|
283
|
+
reachableProfiles.add(profileName);
|
|
284
|
+
for (const parentName of profile.extends)
|
|
285
|
+
visitProfile(parentName);
|
|
286
|
+
}
|
|
287
|
+
for (const agent of Object.values(rawConfig.agents)) {
|
|
288
|
+
for (const profileName of agent.extends)
|
|
289
|
+
visitProfile(profileName);
|
|
290
|
+
collectArgRefs(agent.arg_scope, agent.arg_policy, referencedDimensions, referencedValueSets);
|
|
291
|
+
}
|
|
292
|
+
for (const profile of Object.values(rawConfig.profiles)) {
|
|
293
|
+
collectArgRefs(profile.arg_scope, profile.arg_policy, referencedDimensions, referencedValueSets);
|
|
294
|
+
}
|
|
295
|
+
for (const profileName of Object.keys(rawConfig.profiles)) {
|
|
296
|
+
if (!reachableProfiles.has(profileName)) {
|
|
297
|
+
findings.push({
|
|
298
|
+
rule: 'unused-profile',
|
|
299
|
+
severity: DEFAULT_LINT_SEVERITY['unused-profile'],
|
|
300
|
+
message: `profiles.${profileName} is not referenced by any agent.`,
|
|
301
|
+
example: profileName,
|
|
302
|
+
});
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
for (const valueSetName of Object.keys(rawConfig.value_sets)) {
|
|
306
|
+
if (!referencedValueSets.has(valueSetName)) {
|
|
307
|
+
findings.push({
|
|
308
|
+
rule: 'unused-value-set',
|
|
309
|
+
severity: DEFAULT_LINT_SEVERITY['unused-value-set'],
|
|
310
|
+
message: `value_sets.${valueSetName} is not referenced.`,
|
|
311
|
+
example: valueSetName,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
for (const dimensionName of Object.keys(rawConfig.arg_dimensions)) {
|
|
316
|
+
if (!referencedDimensions.has(dimensionName)) {
|
|
317
|
+
findings.push({
|
|
318
|
+
rule: 'unused-dimension',
|
|
319
|
+
severity: DEFAULT_LINT_SEVERITY['unused-dimension'],
|
|
320
|
+
message: `arg_dimensions.${dimensionName} is not referenced by any arg_scope.`,
|
|
321
|
+
example: dimensionName,
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const [agentName, agent] of Object.entries(resolvedConfig?.agents ?? {})) {
|
|
326
|
+
if (agent.allow.length === 0 && agent.ask.length === 0) {
|
|
327
|
+
findings.push({
|
|
328
|
+
rule: 'empty-agent',
|
|
329
|
+
severity: DEFAULT_LINT_SEVERITY['empty-agent'],
|
|
330
|
+
agent: agentName,
|
|
331
|
+
message: 'Agent has an empty effective allow/ask surface.',
|
|
332
|
+
example: `[${agentName}]`,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
for (const denyPattern of agent.deny) {
|
|
336
|
+
const overlapsGrant = [...agent.allow, ...agent.ask].some((pattern) => staticPatternsOverlap(pattern, denyPattern));
|
|
337
|
+
if (!overlapsGrant) {
|
|
338
|
+
findings.push({
|
|
339
|
+
rule: 'dead-deny',
|
|
340
|
+
severity: DEFAULT_LINT_SEVERITY['dead-deny'],
|
|
341
|
+
agent: agentName,
|
|
342
|
+
message: `deny pattern "${denyPattern}" does not overlap any allow or ask pattern.`,
|
|
343
|
+
example: `[${agentName}] ${denyPattern}`,
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
for (const envRef of missingEnvRefs(rawConfig)) {
|
|
349
|
+
findings.push({
|
|
350
|
+
rule: 'missing-env-ref',
|
|
351
|
+
severity: DEFAULT_LINT_SEVERITY['missing-env-ref'],
|
|
352
|
+
message: `Environment variable ${envRef} is referenced but not set.`,
|
|
353
|
+
example: `\${${envRef}}`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
return findings;
|
|
357
|
+
}
|
|
358
|
+
function collectArgRefs(argScope, argPolicy, dimensions, valueSets) {
|
|
359
|
+
for (const [dimensionName, valueSetNames] of Object.entries(argScope ?? {})) {
|
|
360
|
+
dimensions.add(dimensionName);
|
|
361
|
+
for (const valueSetName of valueSetNames)
|
|
362
|
+
valueSets.add(valueSetName);
|
|
363
|
+
}
|
|
364
|
+
for (const toolPolicy of Object.values(argPolicy ?? {})) {
|
|
365
|
+
for (const constraints of Object.values(toolPolicy)) {
|
|
366
|
+
for (const constraint of constraints) {
|
|
367
|
+
const valueSetName = constraint.in ?? constraint.glob_in ?? constraint.each_in;
|
|
368
|
+
if (valueSetName)
|
|
369
|
+
valueSets.add(valueSetName);
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function buildLintControls(config, values) {
|
|
375
|
+
const activeRules = new Set(LINT_RULE_IDS);
|
|
376
|
+
const severity = { ...DEFAULT_LINT_SEVERITY };
|
|
377
|
+
for (const rule of config.disable)
|
|
378
|
+
activeRules.delete(rule);
|
|
379
|
+
for (const [rule, level] of Object.entries(config.severity)) {
|
|
380
|
+
severity[rule] = level;
|
|
381
|
+
}
|
|
382
|
+
const only = parseRuleIdList(values.only, '--only');
|
|
383
|
+
if (only.length > 0) {
|
|
384
|
+
activeRules.clear();
|
|
385
|
+
for (const rule of only)
|
|
386
|
+
activeRules.add(rule);
|
|
387
|
+
}
|
|
388
|
+
for (const rule of parseRuleIdList(values.disable, '--disable')) {
|
|
389
|
+
activeRules.delete(rule);
|
|
390
|
+
}
|
|
391
|
+
for (const { rule, mode } of parseRuleOverrides(values.rule)) {
|
|
392
|
+
if (mode === 'off') {
|
|
393
|
+
activeRules.delete(rule);
|
|
394
|
+
}
|
|
395
|
+
else {
|
|
396
|
+
activeRules.add(rule);
|
|
397
|
+
severity[rule] = mode;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return { activeRules, severity };
|
|
401
|
+
}
|
|
402
|
+
function applyLintControls(findings, controls) {
|
|
403
|
+
return findings.flatMap((finding) => {
|
|
404
|
+
if (!controls.activeRules.has(finding.rule))
|
|
405
|
+
return [];
|
|
406
|
+
return [{ ...finding, severity: controls.severity[finding.rule] }];
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function parseRuleIdList(value, flag) {
|
|
410
|
+
return stringOptionValues(value)
|
|
411
|
+
.flatMap((entry) => entry.split(','))
|
|
412
|
+
.map((entry) => entry.trim())
|
|
413
|
+
.filter((entry) => entry.length > 0)
|
|
414
|
+
.map((entry) => parseLintRuleId(entry, flag));
|
|
415
|
+
}
|
|
416
|
+
function parseRuleOverrides(value) {
|
|
417
|
+
return stringOptionValues(value)
|
|
418
|
+
.flatMap((entry) => entry.split(','))
|
|
419
|
+
.map((entry) => entry.trim())
|
|
420
|
+
.filter((entry) => entry.length > 0)
|
|
421
|
+
.map((entry) => {
|
|
422
|
+
const [rawRule, rawMode, ...extra] = entry.split('=');
|
|
423
|
+
if (!rawRule || !rawMode || extra.length > 0) {
|
|
424
|
+
throw new Error('--rule must be formatted as <id>=off|info|warn|error.');
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
rule: parseLintRuleId(rawRule, '--rule'),
|
|
428
|
+
mode: parseLintRuleMode(rawMode),
|
|
429
|
+
};
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
function stringOptionValues(value) {
|
|
433
|
+
if (value === undefined)
|
|
434
|
+
return [];
|
|
435
|
+
return Array.isArray(value) ? value : [value];
|
|
436
|
+
}
|
|
437
|
+
function parseLintRuleId(value, flag) {
|
|
438
|
+
if (LINT_RULE_IDS.includes(value))
|
|
439
|
+
return value;
|
|
440
|
+
throw new Error(`${flag} references unknown lint rule "${value}". Valid rules: ${LINT_RULE_IDS.join(', ')}.`);
|
|
441
|
+
}
|
|
442
|
+
function parseLintRuleMode(value) {
|
|
443
|
+
if (value === 'off' || value === 'info' || value === 'warn' || value === 'error')
|
|
444
|
+
return value;
|
|
445
|
+
throw new Error('--rule severity must be off, info, warn, or error.');
|
|
446
|
+
}
|
|
447
|
+
function parseLintFailLevel(value) {
|
|
448
|
+
if (value === 'info' || value === 'warn' || value === 'error')
|
|
449
|
+
return value;
|
|
450
|
+
throw new Error('--fail-on must be info, warn, or error.');
|
|
451
|
+
}
|
|
452
|
+
function lintFindingFromDiagnostic(diagnostic) {
|
|
453
|
+
return {
|
|
454
|
+
rule: 'unresolvable-ref',
|
|
455
|
+
severity: DEFAULT_LINT_SEVERITY['unresolvable-ref'],
|
|
456
|
+
agent: diagnostic.agent,
|
|
457
|
+
message: diagnostic.message,
|
|
458
|
+
suggestion: diagnostic.suggestion,
|
|
459
|
+
example: diagnostic.agent ? `[${diagnostic.agent}] ${diagnostic.message}` : diagnostic.message,
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function isUnresolvableRefDiagnostic(diagnostic) {
|
|
463
|
+
return (diagnostic.level === 'error' &&
|
|
464
|
+
(diagnostic.code === 'unknown-profile-ref' ||
|
|
465
|
+
diagnostic.code === 'unknown-arg-dimension-ref' ||
|
|
466
|
+
diagnostic.code === 'unknown-value-set-ref'));
|
|
467
|
+
}
|
|
468
|
+
function groupLintFindings(findings) {
|
|
469
|
+
const byRule = new Map();
|
|
470
|
+
for (const finding of findings) {
|
|
471
|
+
const entries = byRule.get(finding.rule) ?? [];
|
|
472
|
+
entries.push(finding);
|
|
473
|
+
byRule.set(finding.rule, entries);
|
|
474
|
+
}
|
|
475
|
+
return Array.from(byRule.entries())
|
|
476
|
+
.map(([rule, entries]) => ({
|
|
477
|
+
rule,
|
|
478
|
+
severity: entries[0]?.severity ?? DEFAULT_LINT_SEVERITY[rule],
|
|
479
|
+
count: entries.length,
|
|
480
|
+
findings: entries,
|
|
481
|
+
}))
|
|
482
|
+
.sort((a, b) => {
|
|
483
|
+
const severityDelta = severityRank(b.severity) - severityRank(a.severity);
|
|
484
|
+
if (severityDelta !== 0)
|
|
485
|
+
return severityDelta;
|
|
486
|
+
return LINT_RULE_IDS.indexOf(a.rule) - LINT_RULE_IDS.indexOf(b.rule);
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
function loadForCli(path, options = {}) {
|
|
490
|
+
const loaded = loadConfigDetailed(path, options);
|
|
491
|
+
const errors = loaded.diagnostics.filter((diagnostic) => diagnostic.level === 'error');
|
|
492
|
+
if (errors.length > 0 || !loaded.config || !loaded.rawConfig) {
|
|
493
|
+
return {
|
|
494
|
+
result: (json) => ({
|
|
495
|
+
json,
|
|
496
|
+
exitCode: 1,
|
|
497
|
+
data: { ok: false, diagnostics: groupDiagnostics(loaded.diagnostics) },
|
|
498
|
+
text: formatCheck({ ok: false, diagnostics: groupDiagnostics(loaded.diagnostics) }),
|
|
499
|
+
}),
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
return { config: loaded.config, rawConfig: loaded.rawConfig, diagnostics: loaded.diagnostics };
|
|
503
|
+
}
|
|
504
|
+
function parseCheckFailLevel(value) {
|
|
505
|
+
if (value === 'warn' || value === 'error')
|
|
506
|
+
return value;
|
|
507
|
+
throw new Error('--fail-on must be "warn" or "error".');
|
|
508
|
+
}
|
|
509
|
+
function parsePermissionLevel(value) {
|
|
510
|
+
if (value === 'allow' || value === 'ask' || value === 'deny' || value === 'any')
|
|
511
|
+
return value;
|
|
512
|
+
throw new Error('--level must be allow, ask, deny, or any.');
|
|
513
|
+
}
|
|
514
|
+
function shouldFail(diagnostics, failOn) {
|
|
515
|
+
return diagnostics.some((diagnostic) => failOn === 'warn'
|
|
516
|
+
? diagnostic.level === 'warn' || diagnostic.level === 'error'
|
|
517
|
+
: diagnostic.level === 'error');
|
|
518
|
+
}
|
|
519
|
+
function shouldFailLint(groups, failOn) {
|
|
520
|
+
const threshold = severityRank(failOn);
|
|
521
|
+
return groups.some((group) => severityRank(group.severity) >= threshold);
|
|
522
|
+
}
|
|
523
|
+
function severityRank(severity) {
|
|
524
|
+
return severity === 'error' ? 2 : severity === 'warn' ? 1 : 0;
|
|
525
|
+
}
|
|
526
|
+
function groupDiagnostics(diagnostics) {
|
|
527
|
+
return {
|
|
528
|
+
error: diagnostics.filter((diagnostic) => diagnostic.level === 'error'),
|
|
529
|
+
warn: diagnostics.filter((diagnostic) => diagnostic.level === 'warn'),
|
|
530
|
+
info: diagnostics.filter((diagnostic) => diagnostic.level === 'info'),
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
function precedenceNotes(explanation) {
|
|
534
|
+
const notes = [];
|
|
535
|
+
for (const denied of explanation.permissions.deny) {
|
|
536
|
+
for (const allowed of [...explanation.permissions.allow, ...explanation.permissions.ask]) {
|
|
537
|
+
if (staticPatternsOverlap(denied.pattern, allowed.pattern)) {
|
|
538
|
+
notes.push(`${denied.pattern} denies overlap ${allowed.pattern}; deny wins when specificity ties or beats the grant.`);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return notes;
|
|
543
|
+
}
|
|
544
|
+
async function expandedDecisions(configPath, explanation, engine) {
|
|
545
|
+
const discovered = await discoverTools(configPath);
|
|
546
|
+
const allPatterns = [
|
|
547
|
+
...explanation.permissions.allow,
|
|
548
|
+
...explanation.permissions.ask,
|
|
549
|
+
...explanation.permissions.deny,
|
|
550
|
+
].map((entry) => entry.pattern);
|
|
551
|
+
const tools = discovered.tools
|
|
552
|
+
.filter((tool) => allPatterns.some((pattern) => matches(pattern, tool.name)))
|
|
553
|
+
.map((tool) => {
|
|
554
|
+
const decision = engine.evaluateDetailed(explanation.agent, tool.name);
|
|
555
|
+
return {
|
|
556
|
+
name: tool.name,
|
|
557
|
+
decision: decision.decision,
|
|
558
|
+
pattern: decision.match?.pattern,
|
|
559
|
+
};
|
|
560
|
+
});
|
|
561
|
+
return { tools, errors: discovered.errors };
|
|
562
|
+
}
|
|
563
|
+
function formatCheck(payload) {
|
|
564
|
+
const lines = [payload.ok ? 'Config OK' : 'Config has problems'];
|
|
565
|
+
for (const level of ['error', 'warn', 'info']) {
|
|
566
|
+
const diagnostics = payload.diagnostics[level];
|
|
567
|
+
if (diagnostics.length === 0)
|
|
568
|
+
continue;
|
|
569
|
+
lines.push(`${level.toUpperCase()} (${diagnostics.length})`);
|
|
570
|
+
for (const diagnostic of diagnostics) {
|
|
571
|
+
const agent = diagnostic.agent ? `[${diagnostic.agent}] ` : '';
|
|
572
|
+
lines.push(` - ${agent}${diagnostic.message}`);
|
|
573
|
+
if (diagnostic.suggestion)
|
|
574
|
+
lines.push(` ${diagnostic.suggestion}`);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
return lines.join('\n');
|
|
578
|
+
}
|
|
579
|
+
function formatLint(payload, options) {
|
|
580
|
+
const hiddenInfoCount = options.quiet
|
|
581
|
+
? payload.rules
|
|
582
|
+
.filter((group) => group.severity === 'info')
|
|
583
|
+
.reduce((total, group) => total + group.count, 0)
|
|
584
|
+
: 0;
|
|
585
|
+
const suffix = hiddenInfoCount > 0 ? ` (${hiddenInfoCount} info hidden)` : '';
|
|
586
|
+
const lines = [payload.ok ? `Lint OK${suffix}` : `Lint has problems${suffix}`];
|
|
587
|
+
let collapsedInfoCount = 0;
|
|
588
|
+
for (const group of payload.rules) {
|
|
589
|
+
if (options.quiet && group.severity === 'info')
|
|
590
|
+
continue;
|
|
591
|
+
if (group.severity === 'info' && !options.verbose) {
|
|
592
|
+
collapsedInfoCount += group.count;
|
|
593
|
+
lines.push(formatLintInfoSummary(group));
|
|
594
|
+
continue;
|
|
595
|
+
}
|
|
596
|
+
lines.push(`${group.rule}: ${group.count} (${group.severity})`);
|
|
597
|
+
for (const finding of group.findings) {
|
|
598
|
+
const agent = finding.agent ? `[${finding.agent}] ` : '';
|
|
599
|
+
lines.push(` - ${agent}${finding.message}`);
|
|
600
|
+
if (finding.suggestion)
|
|
601
|
+
lines.push(` ${finding.suggestion}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
if (collapsedInfoCount > 0) {
|
|
605
|
+
lines.push(`info collapsed; --verbose to list all; --quiet to hide; --fail-on info to gate.`);
|
|
606
|
+
}
|
|
607
|
+
return lines.join('\n');
|
|
608
|
+
}
|
|
609
|
+
function formatLintInfoSummary(group) {
|
|
610
|
+
const examples = group.findings.slice(0, 3).map((finding) => finding.example);
|
|
611
|
+
const more = group.count > examples.length ? ` ... +${group.count - examples.length} more` : '';
|
|
612
|
+
return `${group.rule}: ${group.count} (${group.severity}) - ${examples.join(', ')}${more}`;
|
|
613
|
+
}
|
|
614
|
+
function formatExplain(payload) {
|
|
615
|
+
const lines = [
|
|
616
|
+
`Agent: ${payload.agent}`,
|
|
617
|
+
'Extends:',
|
|
618
|
+
...formatTree(payload.explanation.extendsTree),
|
|
619
|
+
];
|
|
620
|
+
for (const level of ['allow', 'ask', 'deny']) {
|
|
621
|
+
lines.push(`${level.toUpperCase()}:`);
|
|
622
|
+
lines.push(...formatProvenance(payload.explanation.permissions[level]));
|
|
623
|
+
}
|
|
624
|
+
if (payload.explanation.argScope.length > 0) {
|
|
625
|
+
lines.push('ARG SCOPE:');
|
|
626
|
+
for (const scope of payload.explanation.argScope) {
|
|
627
|
+
lines.push(` ${scope.dimension}: ${scope.valueSets.map((set) => set.name).join(', ')}`);
|
|
628
|
+
for (const binding of scope.bindings) {
|
|
629
|
+
lines.push(` ${binding.tool}.${binding.arg}`);
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
if (payload.precedence.length > 0) {
|
|
634
|
+
lines.push('PRECEDENCE:');
|
|
635
|
+
lines.push(...payload.precedence.map((note) => ` - ${note}`));
|
|
636
|
+
}
|
|
637
|
+
if (payload.expanded) {
|
|
638
|
+
lines.push('EXPANDED TOOLS:');
|
|
639
|
+
lines.push(...payload.expanded.tools.map((tool) => ` - ${tool.name}: ${tool.decision}${tool.pattern ? ` via ${tool.pattern}` : ''}`));
|
|
640
|
+
for (const error of payload.expanded.errors)
|
|
641
|
+
lines.push(` ! ${error}`);
|
|
642
|
+
}
|
|
643
|
+
return lines.join('\n');
|
|
644
|
+
}
|
|
645
|
+
function formatProvenance(entries) {
|
|
646
|
+
if (entries.length === 0)
|
|
647
|
+
return [' (none)'];
|
|
648
|
+
return entries.map((entry) => ` - ${entry.pattern} (${entry.sources.map((source) => source.source).join(', ')})`);
|
|
649
|
+
}
|
|
650
|
+
function formatTree(nodes, indent = ' ') {
|
|
651
|
+
const lines = [];
|
|
652
|
+
for (const node of nodes) {
|
|
653
|
+
lines.push(`${indent}- ${node.kind}:${node.name}`);
|
|
654
|
+
lines.push(...formatTree(node.extends, `${indent} `));
|
|
655
|
+
}
|
|
656
|
+
return lines;
|
|
657
|
+
}
|
|
658
|
+
function formatWhoCan(payload) {
|
|
659
|
+
const lines = [`Tool: ${payload.query}`];
|
|
660
|
+
for (const agent of payload.agents) {
|
|
661
|
+
lines.push(` - ${agent.agent}: ${agent.decision}${agent.match ? ` via ${agent.match.pattern}` : ''}`);
|
|
662
|
+
}
|
|
663
|
+
if (payload.agents.length === 0)
|
|
664
|
+
lines.push(' (none)');
|
|
665
|
+
return lines.join('\n');
|
|
666
|
+
}
|
|
667
|
+
function formatTools(payload) {
|
|
668
|
+
const lines = payload.tools.map((tool) => `- ${tool.name}${tool.description ? `: ${tool.description}` : ''}`);
|
|
669
|
+
for (const error of payload.errors)
|
|
670
|
+
lines.push(`! ${error}`);
|
|
671
|
+
return lines.length > 0 ? lines.join('\n') : '(no tools)';
|
|
672
|
+
}
|
|
673
|
+
function writeResult(result, exitCode) {
|
|
674
|
+
if (result.json) {
|
|
675
|
+
console.log(JSON.stringify(result.data, null, 2));
|
|
676
|
+
}
|
|
677
|
+
else if (exitCode === 0) {
|
|
678
|
+
console.log(result.text);
|
|
679
|
+
}
|
|
680
|
+
else {
|
|
681
|
+
console.error(result.text);
|
|
682
|
+
}
|
|
683
|
+
process.exit(exitCode);
|
|
684
|
+
}
|
|
685
|
+
function ok(text, json = false) {
|
|
686
|
+
return { json, exitCode: 0, data: text, text };
|
|
687
|
+
}
|
|
688
|
+
function firstLine(value) {
|
|
689
|
+
return value.split(/\r?\n/)[0]?.trim() ?? '';
|
|
690
|
+
}
|
|
691
|
+
function staticPatternsOverlap(a, b) {
|
|
692
|
+
return (matches(a, b) ||
|
|
693
|
+
matches(b, a) ||
|
|
694
|
+
(a.split('/')[0] === b.split('/')[0] && (a.includes('*') || b.includes('*'))));
|
|
695
|
+
}
|
|
696
|
+
function missingEnvRefs(value) {
|
|
697
|
+
const refs = new Set();
|
|
698
|
+
walk(value);
|
|
699
|
+
return Array.from(refs).filter((name) => process.env[name] === undefined);
|
|
700
|
+
function walk(next) {
|
|
701
|
+
if (typeof next === 'string') {
|
|
702
|
+
for (const match of next.matchAll(/\$\{([^}]+)\}/g))
|
|
703
|
+
refs.add(match[1]);
|
|
704
|
+
}
|
|
705
|
+
else if (Array.isArray(next)) {
|
|
706
|
+
for (const item of next)
|
|
707
|
+
walk(item);
|
|
708
|
+
}
|
|
709
|
+
else if (next && typeof next === 'object') {
|
|
710
|
+
for (const item of Object.values(next))
|
|
711
|
+
walk(item);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
function printHelp() {
|
|
716
|
+
console.log(`airlock local introspection
|
|
717
|
+
|
|
718
|
+
Usage:
|
|
719
|
+
airlock config check [--config PATH] [--strict] [--no-resolve] [--fail-on warn|error] [--json]
|
|
720
|
+
airlock explain <agent> [--config PATH] [--expand] [--json]
|
|
721
|
+
airlock who-can <tool-or-glob> [--config PATH] [--level allow|ask|deny|any] [--json]
|
|
722
|
+
airlock tools [--provider NAME] [--grep REGEX] [--config PATH] [--json]
|
|
723
|
+
airlock lint [--config PATH] [--verbose] [--quiet] [--fail-on info|warn|error] [--only IDS] [--disable IDS] [--rule ID=LEVEL] [--json]`);
|
|
724
|
+
}
|
|
725
|
+
function configCheckHelp() {
|
|
726
|
+
return 'Run static config validation. --strict makes unknown keys errors. --no-resolve treats ${VAR} references as structurally satisfied. --fail-on controls the non-zero threshold.';
|
|
727
|
+
}
|
|
728
|
+
function explainHelp() {
|
|
729
|
+
return 'Explain one agent effective permissions, provenance, arg_scope, and precedence. --expand connects to providers.';
|
|
730
|
+
}
|
|
731
|
+
function whoCanHelp() {
|
|
732
|
+
return 'Reverse lookup effective permission decisions for a concrete tool name or glob-like pattern.';
|
|
733
|
+
}
|
|
734
|
+
function toolsHelp() {
|
|
735
|
+
return 'Enumerate the live provider tool surface. This command connects to configured providers.';
|
|
736
|
+
}
|
|
737
|
+
function lintHelp() {
|
|
738
|
+
const rules = LINT_RULE_IDS.map((rule) => ` ${rule} (${DEFAULT_LINT_SEVERITY[rule]}) - ${LINT_RULE_LABELS[rule]}`).join('\n');
|
|
739
|
+
return `Run static hygiene rules over a config without connecting to providers.
|
|
740
|
+
|
|
741
|
+
Rules:
|
|
742
|
+
${rules}
|
|
743
|
+
|
|
744
|
+
Default output prints warn/error findings in full and collapses each info rule to one summary line.
|
|
745
|
+
|
|
746
|
+
Options:
|
|
747
|
+
--verbose Expand info findings too
|
|
748
|
+
--quiet Hide info summaries and print warn/error findings only
|
|
749
|
+
--fail-on info|warn|error Non-zero threshold (default: warn)
|
|
750
|
+
--only ids Run only comma-separated rule ids
|
|
751
|
+
--disable ids Disable comma-separated rule ids for this invocation
|
|
752
|
+
--rule id=off|info|warn|error
|
|
753
|
+
Override a rule severity for this invocation
|
|
754
|
+
--json Print grouped machine-readable findings`;
|
|
755
|
+
}
|
|
756
|
+
//# sourceMappingURL=cli.js.map
|