airlock-bot 0.2.36 → 0.2.37
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 +10 -1
- package/dist/config/loader.d.ts.map +1 -1
- package/dist/config/loader.js +1 -1
- package/dist/config/loader.js.map +1 -1
- package/dist/config/profiles.d.ts +3 -6
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +52 -19
- package/dist/config/profiles.js.map +1 -1
- package/dist/config/schema.d.ts +277 -8
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +28 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/configure-web/cli.d.ts +12 -0
- package/dist/configure-web/cli.d.ts.map +1 -1
- package/dist/configure-web/cli.js +332 -33
- package/dist/configure-web/cli.js.map +1 -1
- 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 +4 -2
- package/dist/middleware/chain-builder.js.map +1 -1
- package/dist/middleware/core/arg-policy.d.ts +12 -0
- package/dist/middleware/core/arg-policy.d.ts.map +1 -0
- package/dist/middleware/core/arg-policy.js +91 -0
- package/dist/middleware/core/arg-policy.js.map +1 -0
- package/examples/profiles.yaml +4 -2
- package/package.json +1 -1
- package/schema.json +44 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { randomBytes } from 'crypto';
|
|
1
2
|
import { existsSync, readFileSync, writeFileSync, copyFileSync } from 'fs';
|
|
2
3
|
import { parseArgs } from 'util';
|
|
3
4
|
import Fastify from 'fastify';
|
|
@@ -7,8 +8,10 @@ import { buildAdapters } from '../backend/factory.js';
|
|
|
7
8
|
import { AuditConfig, GatewayConfig, McpServerConfig, ProviderConfig, getMcpConfigs, } from '../config/schema.js';
|
|
8
9
|
import { rememberAllow } from '../config/mutator.js';
|
|
9
10
|
import { validateConfig } from '../config/loader.js';
|
|
11
|
+
import { applyProfiles } from '../config/profiles.js';
|
|
10
12
|
import { ClientPool } from '../pool/pool.js';
|
|
11
13
|
import { checkSuspiciousPatterns } from '../registry/sanitizer.js';
|
|
14
|
+
import { bestSpecificity } from '../allowlist/pattern.js';
|
|
12
15
|
import { VERSION } from '../version.js';
|
|
13
16
|
const LATEST_VERSION_CACHE_TTL_MS = 60 * 60 * 1000;
|
|
14
17
|
let latestVersionCache = null;
|
|
@@ -74,7 +77,9 @@ export async function runDashboard(argv) {
|
|
|
74
77
|
const port = Number(values.port ?? '4177');
|
|
75
78
|
const host = values.host ?? '127.0.0.1';
|
|
76
79
|
const gatewayUrl = values['gateway-url'] ?? 'http://127.0.0.1:4111';
|
|
77
|
-
const gatewaySecret = values['gateway-secret'] ??
|
|
80
|
+
const gatewaySecret = values['gateway-secret'] ??
|
|
81
|
+
process.env['AIRLOCK_GATEWAY_SECRET'] ??
|
|
82
|
+
process.env['AIRLOCK_API_SECRET'];
|
|
78
83
|
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
79
84
|
throw new Error(`Invalid --port: ${values.port}`);
|
|
80
85
|
}
|
|
@@ -362,7 +367,7 @@ async function readRemoteCommandCenterStatus(configPath, remoteGateway) {
|
|
|
362
367
|
id,
|
|
363
368
|
type,
|
|
364
369
|
enabled,
|
|
365
|
-
status: enabled ? runtime ?? 'ok' : 'disabled',
|
|
370
|
+
status: enabled ? (runtime ?? 'ok') : 'disabled',
|
|
366
371
|
toolCount: 0,
|
|
367
372
|
toolFingerprint: '',
|
|
368
373
|
...(runtime === 'down' ? { error: 'Provider is down on the gateway' } : {}),
|
|
@@ -430,10 +435,11 @@ function registerRemoteGatewayRoutes(app, configPath, remoteGateway) {
|
|
|
430
435
|
const reader = response.body.getReader();
|
|
431
436
|
try {
|
|
432
437
|
while (true) {
|
|
433
|
-
const
|
|
434
|
-
if (done)
|
|
438
|
+
const result = await reader.read();
|
|
439
|
+
if (result.done)
|
|
435
440
|
break;
|
|
436
|
-
|
|
441
|
+
if (result.value)
|
|
442
|
+
reply.raw.write(Buffer.from(result.value));
|
|
437
443
|
}
|
|
438
444
|
}
|
|
439
445
|
catch {
|
|
@@ -504,7 +510,8 @@ async function rememberRemoteDecision(configPath, remoteGateway, code, remember,
|
|
|
504
510
|
}
|
|
505
511
|
}
|
|
506
512
|
async function forwardRemoteApproval(remoteGateway, action, code) {
|
|
507
|
-
const
|
|
513
|
+
const params = new URLSearchParams({ code });
|
|
514
|
+
const response = await remoteGatewayFetch(remoteGateway, `/${action}?${params.toString()}`, {
|
|
508
515
|
method: 'POST',
|
|
509
516
|
});
|
|
510
517
|
if (!response.ok)
|
|
@@ -564,6 +571,7 @@ export function saveRules(configPath, body) {
|
|
|
564
571
|
existing.deny = parseStringArray(body.deny, 'deny');
|
|
565
572
|
}
|
|
566
573
|
else {
|
|
574
|
+
existing.extends = parseStringArray(body.extends, 'extends');
|
|
567
575
|
existing.allow = parseStringArray(body.allow, 'allow');
|
|
568
576
|
existing.ask = parseStringArray(body.ask, 'ask');
|
|
569
577
|
existing.deny = parseStringArray(body.deny, 'deny');
|
|
@@ -584,15 +592,32 @@ export function createEntity(configPath, body) {
|
|
|
584
592
|
const section = asMutableRecord(doc[sectionName]);
|
|
585
593
|
if (section[id])
|
|
586
594
|
throw new Error(`${kind} "${id}" already exists`);
|
|
587
|
-
|
|
588
|
-
|
|
595
|
+
const baseId = parseOptionalId(body.baseId);
|
|
596
|
+
const base = baseId && section[baseId] ? structuredClone(section[baseId]) : undefined;
|
|
597
|
+
const createdToken = kind === 'agent' ? createAgentToken() : undefined;
|
|
598
|
+
if (kind === 'agent') {
|
|
599
|
+
const agent = asMutableRecord(base);
|
|
600
|
+
const profileIds = parseOptionalStringArray(body.profileIds, 'profileIds');
|
|
601
|
+
delete agent.token;
|
|
602
|
+
agent.extends = profileIds ?? stringArray(agent.extends);
|
|
603
|
+
agent.allow = stringArray(agent.allow);
|
|
604
|
+
agent.ask = stringArray(agent.ask);
|
|
605
|
+
agent.deny = stringArray(agent.deny);
|
|
606
|
+
agent.token = createdToken;
|
|
607
|
+
section[id] = agent;
|
|
608
|
+
}
|
|
609
|
+
else if (base) {
|
|
610
|
+
section[id] = structuredClone(base);
|
|
589
611
|
}
|
|
590
612
|
else {
|
|
591
|
-
section[id] = { allow: [], ask: [], deny: [] };
|
|
613
|
+
section[id] = { extends: [], allow: [], ask: [], deny: [] };
|
|
592
614
|
}
|
|
593
615
|
doc[sectionName] = section;
|
|
594
616
|
writeValidatedConfig(configPath, doc);
|
|
595
|
-
return
|
|
617
|
+
return {
|
|
618
|
+
...readState(configPath),
|
|
619
|
+
...(createdToken ? { createdToken: { agentId: id, token: createdToken } } : {}),
|
|
620
|
+
};
|
|
596
621
|
}
|
|
597
622
|
export function deleteEntity(configPath, kind, id) {
|
|
598
623
|
const parsedKind = parseKind(kind);
|
|
@@ -685,6 +710,44 @@ export function annotationTags(annotations, suspiciousPatterns = []) {
|
|
|
685
710
|
tags.push('injection');
|
|
686
711
|
return tags;
|
|
687
712
|
}
|
|
713
|
+
export function resolveEditableRuleDecision(rules, toolName) {
|
|
714
|
+
return resolveEditableRuleMatch(rules, toolName).decision;
|
|
715
|
+
}
|
|
716
|
+
export function resolveEditableRuleMatch(rules, toolName) {
|
|
717
|
+
const deny = bestRuleMatch(rules.deny, toolName);
|
|
718
|
+
const ask = bestRuleMatch(rules.ask, toolName);
|
|
719
|
+
const allow = bestRuleMatch(rules.allow, toolName);
|
|
720
|
+
const denySpec = deny?.specificity ?? -1;
|
|
721
|
+
const askSpec = ask?.specificity ?? -1;
|
|
722
|
+
const allowSpec = allow?.specificity ?? -1;
|
|
723
|
+
const best = Math.max(denySpec, askSpec, allowSpec);
|
|
724
|
+
if (best < 0)
|
|
725
|
+
return { decision: 'unset', source: 'none' };
|
|
726
|
+
if (denySpec === best)
|
|
727
|
+
return editableRuleMatch('deny', deny, toolName);
|
|
728
|
+
if (askSpec === best)
|
|
729
|
+
return editableRuleMatch('ask', ask, toolName);
|
|
730
|
+
return editableRuleMatch('allow', allow, toolName);
|
|
731
|
+
}
|
|
732
|
+
function editableRuleMatch(decision, match, toolName) {
|
|
733
|
+
if (!match)
|
|
734
|
+
return { decision, source: 'none' };
|
|
735
|
+
return {
|
|
736
|
+
decision,
|
|
737
|
+
source: match.pattern === toolName ? 'exact' : 'pattern',
|
|
738
|
+
pattern: match.pattern,
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function bestRuleMatch(patterns, toolName) {
|
|
742
|
+
let best;
|
|
743
|
+
for (const pattern of patterns) {
|
|
744
|
+
const specificity = bestSpecificity([pattern], toolName);
|
|
745
|
+
if (specificity >= 0 && (!best || specificity > best.specificity)) {
|
|
746
|
+
best = { pattern, specificity };
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
return best;
|
|
750
|
+
}
|
|
688
751
|
export async function discoverTools(configPath) {
|
|
689
752
|
const raw = readConfigObject(configPath);
|
|
690
753
|
const parsed = GatewayConfig.parse(withoutDisabledProviders(raw));
|
|
@@ -820,6 +883,7 @@ function parseGatewayConfig(doc) {
|
|
|
820
883
|
],
|
|
821
884
|
};
|
|
822
885
|
}
|
|
886
|
+
applyProfiles(result.data);
|
|
823
887
|
return { config: result.data, diagnostics: validateConfig(result.data) };
|
|
824
888
|
}
|
|
825
889
|
catch (err) {
|
|
@@ -838,6 +902,7 @@ function toEditableAgents(input) {
|
|
|
838
902
|
allow: stringArray(agent.allow),
|
|
839
903
|
ask: stringArray(agent.ask),
|
|
840
904
|
deny: stringArray(agent.deny),
|
|
905
|
+
hasToken: typeof agent.token === 'string' && agent.token.trim().length > 0,
|
|
841
906
|
},
|
|
842
907
|
];
|
|
843
908
|
}));
|
|
@@ -868,6 +933,7 @@ function toEditableProfiles(input) {
|
|
|
868
933
|
return [
|
|
869
934
|
id,
|
|
870
935
|
{
|
|
936
|
+
extends: stringArray(profile.extends),
|
|
871
937
|
allow: stringArray(profile.allow),
|
|
872
938
|
ask: stringArray(profile.ask),
|
|
873
939
|
deny: stringArray(profile.deny),
|
|
@@ -895,6 +961,14 @@ function parseId(value) {
|
|
|
895
961
|
}
|
|
896
962
|
return id;
|
|
897
963
|
}
|
|
964
|
+
function parseOptionalId(value) {
|
|
965
|
+
if (value === undefined || value === null || value === '')
|
|
966
|
+
return undefined;
|
|
967
|
+
return parseId(value);
|
|
968
|
+
}
|
|
969
|
+
function createAgentToken() {
|
|
970
|
+
return `airlock_agent_${randomBytes(32).toString('base64url')}`;
|
|
971
|
+
}
|
|
898
972
|
function parseBoolean(value) {
|
|
899
973
|
if (typeof value === 'boolean')
|
|
900
974
|
return value;
|
|
@@ -1479,6 +1553,25 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1479
1553
|
color: var(--red);
|
|
1480
1554
|
font-weight: 700;
|
|
1481
1555
|
}
|
|
1556
|
+
.rule-actions button.active.pattern-match {
|
|
1557
|
+
border-style: dashed;
|
|
1558
|
+
font-weight: 650;
|
|
1559
|
+
}
|
|
1560
|
+
.rule-actions button.active.pattern-match[data-decision="allow"] {
|
|
1561
|
+
border-color: #b8dfca;
|
|
1562
|
+
background: #f6fbf8;
|
|
1563
|
+
color: #4d7f63;
|
|
1564
|
+
}
|
|
1565
|
+
.rule-actions button.active.pattern-match[data-decision="ask"] {
|
|
1566
|
+
border-color: #f1d99a;
|
|
1567
|
+
background: #fffaf0;
|
|
1568
|
+
color: #8a6a19;
|
|
1569
|
+
}
|
|
1570
|
+
.rule-actions button.active.pattern-match[data-decision="deny"] {
|
|
1571
|
+
border-color: #f2c0bb;
|
|
1572
|
+
background: #fff8f7;
|
|
1573
|
+
color: #9b4a43;
|
|
1574
|
+
}
|
|
1482
1575
|
.pill {
|
|
1483
1576
|
display: inline-flex;
|
|
1484
1577
|
align-items: center;
|
|
@@ -1571,6 +1664,43 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1571
1664
|
overflow: auto;
|
|
1572
1665
|
padding: 14px;
|
|
1573
1666
|
}
|
|
1667
|
+
.form-grid {
|
|
1668
|
+
display: grid;
|
|
1669
|
+
gap: 12px;
|
|
1670
|
+
}
|
|
1671
|
+
.field {
|
|
1672
|
+
display: grid;
|
|
1673
|
+
gap: 6px;
|
|
1674
|
+
}
|
|
1675
|
+
.field label {
|
|
1676
|
+
color: var(--muted);
|
|
1677
|
+
font-size: 12px;
|
|
1678
|
+
font-weight: 700;
|
|
1679
|
+
text-transform: uppercase;
|
|
1680
|
+
letter-spacing: 0.08em;
|
|
1681
|
+
}
|
|
1682
|
+
.field input, .field select {
|
|
1683
|
+
min-height: 36px;
|
|
1684
|
+
min-width: 0;
|
|
1685
|
+
border: 1px solid var(--line);
|
|
1686
|
+
border-radius: 7px;
|
|
1687
|
+
padding: 0 10px;
|
|
1688
|
+
background: #fff;
|
|
1689
|
+
color: var(--ink);
|
|
1690
|
+
}
|
|
1691
|
+
.profile-select-list {
|
|
1692
|
+
display: grid;
|
|
1693
|
+
gap: 6px;
|
|
1694
|
+
padding: 8px;
|
|
1695
|
+
border: 1px solid var(--line);
|
|
1696
|
+
border-radius: 7px;
|
|
1697
|
+
background: #fbfcfe;
|
|
1698
|
+
}
|
|
1699
|
+
.token-value {
|
|
1700
|
+
width: 100%;
|
|
1701
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
|
1702
|
+
font-size: 12px;
|
|
1703
|
+
}
|
|
1574
1704
|
.modal-title {
|
|
1575
1705
|
min-width: 0;
|
|
1576
1706
|
overflow-wrap: anywhere;
|
|
@@ -1725,6 +1855,19 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1725
1855
|
<div id="approvalModalFooter" class="modal-footer"></div>
|
|
1726
1856
|
</div>
|
|
1727
1857
|
</div>
|
|
1858
|
+
<div id="entityModal" class="modal-backdrop">
|
|
1859
|
+
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="entityModalTitle">
|
|
1860
|
+
<div class="modal-header">
|
|
1861
|
+
<div>
|
|
1862
|
+
<div id="entityModalTitle" class="modal-title"></div>
|
|
1863
|
+
<div id="entityModalMeta" class="subtle"></div>
|
|
1864
|
+
</div>
|
|
1865
|
+
<button id="entityModalClose" title="Close">Close</button>
|
|
1866
|
+
</div>
|
|
1867
|
+
<div id="entityModalBody" class="modal-body"></div>
|
|
1868
|
+
<div id="entityModalFooter" class="modal-footer"></div>
|
|
1869
|
+
</div>
|
|
1870
|
+
</div>
|
|
1728
1871
|
<script>
|
|
1729
1872
|
const state = {
|
|
1730
1873
|
config: null,
|
|
@@ -1739,6 +1882,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
1739
1882
|
errors: [],
|
|
1740
1883
|
activeKind: 'agent',
|
|
1741
1884
|
activeId: '',
|
|
1885
|
+
createKind: '',
|
|
1742
1886
|
drafts: {},
|
|
1743
1887
|
descriptionExpanded: {},
|
|
1744
1888
|
search: '',
|
|
@@ -2208,7 +2352,8 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2208
2352
|
el('addProvider').style.display = state.activeKind === 'providers' ? 'inline-flex' : 'none';
|
|
2209
2353
|
el('deleteEntity').style.display = state.activeKind === 'activity' ? 'none' : el('deleteEntity').style.display;
|
|
2210
2354
|
el('deleteEntity').disabled = !state.activeId || state.activeKind === 'activity';
|
|
2211
|
-
el('profilePanel').style.display =
|
|
2355
|
+
el('profilePanel').style.display =
|
|
2356
|
+
state.activeKind === 'agent' || state.activeKind === 'profile' ? 'block' : 'none';
|
|
2212
2357
|
|
|
2213
2358
|
if (!draft) {
|
|
2214
2359
|
if (state.activeKind === 'providers') {
|
|
@@ -2236,12 +2381,18 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2236
2381
|
}
|
|
2237
2382
|
|
|
2238
2383
|
const denyCount = draft.deny.length;
|
|
2384
|
+
const tokenPill = state.activeKind === 'agent'
|
|
2385
|
+
? '<span class="pill ' + (state.config.agents[state.activeId]?.hasToken ? 'tag-good' : 'tag-danger') + '">' + (state.config.agents[state.activeId]?.hasToken ? 'token set' : 'token missing') + '</span>'
|
|
2386
|
+
: '';
|
|
2239
2387
|
el('summary').innerHTML =
|
|
2240
2388
|
'<span class="pill">allow ' + draft.allow.length + '</span>' +
|
|
2241
2389
|
'<span class="pill">ask ' + draft.ask.length + '</span>' +
|
|
2242
|
-
'<span class="pill">deny ' + denyCount + '</span>'
|
|
2390
|
+
'<span class="pill">deny ' + denyCount + '</span>' +
|
|
2391
|
+
tokenPill;
|
|
2243
2392
|
|
|
2244
|
-
const profiles = Object.keys(state.config.profiles)
|
|
2393
|
+
const profiles = Object.keys(state.config.profiles).filter(
|
|
2394
|
+
(id) => state.activeKind !== 'profile' || id !== state.activeId
|
|
2395
|
+
);
|
|
2245
2396
|
el('profileChecks').innerHTML = profiles.length
|
|
2246
2397
|
? profiles.map((id) => {
|
|
2247
2398
|
const checked = (draft.extends || []).includes(id) ? ' checked' : '';
|
|
@@ -2292,7 +2443,7 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2292
2443
|
|
|
2293
2444
|
const visible = filteredTools();
|
|
2294
2445
|
el('tools').innerHTML = visible.length
|
|
2295
|
-
? visible.map((tool) => toolRow(tool,
|
|
2446
|
+
? visible.map((tool) => toolRow(tool, getDecisionMatch(draft, tool.name))).join('')
|
|
2296
2447
|
: '<div class="empty">No tools match the current filters.</div>';
|
|
2297
2448
|
|
|
2298
2449
|
document.querySelectorAll('[data-rule]').forEach((button) => {
|
|
@@ -2321,7 +2472,8 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2321
2472
|
});
|
|
2322
2473
|
}
|
|
2323
2474
|
|
|
2324
|
-
function toolRow(tool,
|
|
2475
|
+
function toolRow(tool, match) {
|
|
2476
|
+
const decision = match.decision;
|
|
2325
2477
|
const tags = tool.tags && tool.tags.length ? tool.tags : ['untagged'];
|
|
2326
2478
|
const tagHtml = tags.map((tag) => '<span class="tag ' + tagClass(tag) + '">' + escapeHtml(tag) + '</span>').join('');
|
|
2327
2479
|
const recommended = '<span class="tag tag-recommended ' + recommendedClass(tool.recommended) + '">recommended ' + escapeHtml(tool.recommended) + '</span>';
|
|
@@ -2336,10 +2488,10 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2336
2488
|
'<div class="tool-name"><span class="provider">' + escapeHtml(tool.provider) + '</span><br>' + escapeHtml(tool.shortName || tool.name) + '</div>' +
|
|
2337
2489
|
'<div class="description"><div class="description-content' + descriptionClass + '">' + formatDescription(description) + '</div>' + toggle + '<div class="tag-list">' + tagHtml + recommended + '</div></div>' +
|
|
2338
2490
|
'<div class="rule-actions">' +
|
|
2339
|
-
ruleButton(tool.name, 'allow',
|
|
2340
|
-
ruleButton(tool.name, 'ask',
|
|
2341
|
-
ruleButton(tool.name, 'deny',
|
|
2342
|
-
ruleButton(tool.name, 'unset',
|
|
2491
|
+
ruleButton(tool.name, 'allow', match) +
|
|
2492
|
+
ruleButton(tool.name, 'ask', match) +
|
|
2493
|
+
ruleButton(tool.name, 'deny', match) +
|
|
2494
|
+
ruleButton(tool.name, 'unset', match) +
|
|
2343
2495
|
'</div>' +
|
|
2344
2496
|
'</div>';
|
|
2345
2497
|
}
|
|
@@ -2394,17 +2546,67 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2394
2546
|
return 'tag-good';
|
|
2395
2547
|
}
|
|
2396
2548
|
|
|
2397
|
-
function ruleButton(tool, decision,
|
|
2398
|
-
const active = decision ===
|
|
2549
|
+
function ruleButton(tool, decision, activeMatch) {
|
|
2550
|
+
const active = decision === activeMatch.decision ? ' active' : '';
|
|
2551
|
+
const patternMatch = active && activeMatch.source === 'pattern' ? ' pattern-match' : '';
|
|
2399
2552
|
const label = decision === 'unset' ? 'Clear' : decision;
|
|
2400
|
-
|
|
2553
|
+
const title = activeMatch.source === 'pattern' && decision === activeMatch.decision
|
|
2554
|
+
? decision + ' by ' + activeMatch.pattern + '. Click to add an exact rule.'
|
|
2555
|
+
: decision === activeMatch.decision && activeMatch.source === 'exact'
|
|
2556
|
+
? decision + ' by exact rule.'
|
|
2557
|
+
: decision === 'unset'
|
|
2558
|
+
? 'Clear exact rule for this tool.'
|
|
2559
|
+
: 'Set exact ' + decision + ' rule for this tool.';
|
|
2560
|
+
return '<button class="' + active + patternMatch + '" title="' + escapeHtml(title) + '" data-rule data-tool="' + escapeHtml(tool) + '" data-decision="' + decision + '">' + label + '</button>';
|
|
2401
2561
|
}
|
|
2402
2562
|
|
|
2403
2563
|
function getDecision(draft, tool) {
|
|
2404
|
-
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
|
|
2564
|
+
return getDecisionMatch(draft, tool).decision;
|
|
2565
|
+
}
|
|
2566
|
+
|
|
2567
|
+
function getDecisionMatch(draft, tool) {
|
|
2568
|
+
const deny = bestRuleMatch(draft.deny, tool);
|
|
2569
|
+
const ask = bestRuleMatch(draft.ask, tool);
|
|
2570
|
+
const allow = bestRuleMatch(draft.allow, tool);
|
|
2571
|
+
const denySpec = deny ? deny.specificity : -1;
|
|
2572
|
+
const askSpec = ask ? ask.specificity : -1;
|
|
2573
|
+
const allowSpec = allow ? allow.specificity : -1;
|
|
2574
|
+
const best = Math.max(denySpec, askSpec, allowSpec);
|
|
2575
|
+
if (best < 0) return { decision: 'unset', source: 'none' };
|
|
2576
|
+
if (denySpec === best) return decisionMatch('deny', deny, tool);
|
|
2577
|
+
if (askSpec === best) return decisionMatch('ask', ask, tool);
|
|
2578
|
+
return decisionMatch('allow', allow, tool);
|
|
2579
|
+
}
|
|
2580
|
+
|
|
2581
|
+
function decisionMatch(decision, match, tool) {
|
|
2582
|
+
if (!match) return { decision, source: 'none' };
|
|
2583
|
+
return {
|
|
2584
|
+
decision,
|
|
2585
|
+
source: match.pattern === tool ? 'exact' : 'pattern',
|
|
2586
|
+
pattern: match.pattern
|
|
2587
|
+
};
|
|
2588
|
+
}
|
|
2589
|
+
|
|
2590
|
+
function bestRuleMatch(patterns, tool) {
|
|
2591
|
+
let best = null;
|
|
2592
|
+
for (const pattern of patterns || []) {
|
|
2593
|
+
const score = ruleSpecificity(pattern, tool);
|
|
2594
|
+
if (score >= 0 && (!best || score > best.specificity)) best = { pattern, specificity: score };
|
|
2595
|
+
}
|
|
2596
|
+
return best;
|
|
2597
|
+
}
|
|
2598
|
+
|
|
2599
|
+
function ruleSpecificity(pattern, tool) {
|
|
2600
|
+
if (pattern === tool) return pattern.length + 1;
|
|
2601
|
+
if (pattern.endsWith('/*')) {
|
|
2602
|
+
const prefix = pattern.slice(0, -1);
|
|
2603
|
+
return tool.startsWith(prefix) && !tool.slice(prefix.length).includes('/') ? prefix.length : -1;
|
|
2604
|
+
}
|
|
2605
|
+
if (pattern.endsWith('*')) {
|
|
2606
|
+
const prefix = pattern.slice(0, -1);
|
|
2607
|
+
return tool.startsWith(prefix) ? prefix.length : -1;
|
|
2608
|
+
}
|
|
2609
|
+
return -1;
|
|
2408
2610
|
}
|
|
2409
2611
|
|
|
2410
2612
|
function setDecision(draft, tool, decision) {
|
|
@@ -2473,12 +2675,104 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2473
2675
|
render();
|
|
2474
2676
|
}
|
|
2475
2677
|
|
|
2476
|
-
|
|
2477
|
-
|
|
2478
|
-
|
|
2479
|
-
|
|
2678
|
+
function openCreateModal(kind) {
|
|
2679
|
+
state.createKind = kind;
|
|
2680
|
+
renderCreateModal();
|
|
2681
|
+
el('entityModal').classList.add('open');
|
|
2682
|
+
setTimeout(() => el('entityId')?.focus(), 0);
|
|
2683
|
+
}
|
|
2684
|
+
|
|
2685
|
+
function closeEntityModal() {
|
|
2686
|
+
el('entityModal').classList.remove('open');
|
|
2687
|
+
state.createKind = '';
|
|
2688
|
+
}
|
|
2689
|
+
|
|
2690
|
+
function renderCreateModal() {
|
|
2691
|
+
const kind = state.createKind;
|
|
2692
|
+
const isAgent = kind === 'agent';
|
|
2693
|
+
const section = isAgent ? state.config.agents : state.config.profiles;
|
|
2694
|
+
const baseOptions = ['<option value="">Blank</option>'].concat(
|
|
2695
|
+
Object.keys(section).map((id) => '<option value="' + escapeHtml(id) + '">' + escapeHtml(id) + '</option>')
|
|
2696
|
+
).join('');
|
|
2697
|
+
el('entityModalTitle').textContent = isAgent ? 'New agent' : 'New profile';
|
|
2698
|
+
el('entityModalMeta').textContent = isAgent ? 'Creates a bearer token and permission policy' : 'Creates a reusable permission policy';
|
|
2699
|
+
el('entityModalBody').innerHTML =
|
|
2700
|
+
'<div class="form-grid">' +
|
|
2701
|
+
'<div class="field"><label for="entityId">Id</label><input id="entityId" autocomplete="off" placeholder="' + (isAgent ? 'claude-code' : 'readonly') + '"></div>' +
|
|
2702
|
+
'<div class="field"><label for="entityBase">Start from</label><select id="entityBase">' + baseOptions + '</select></div>' +
|
|
2703
|
+
(isAgent ? profilePicker() : '') +
|
|
2704
|
+
'</div>';
|
|
2705
|
+
el('entityModalFooter').innerHTML =
|
|
2706
|
+
'<button id="entityCreate" class="primary">Create</button>' +
|
|
2707
|
+
'<button id="entityCancel">Cancel</button>';
|
|
2708
|
+
el('entityCreate').addEventListener('click', () => submitCreateEntity().catch((error) => alert(error.message)));
|
|
2709
|
+
el('entityCancel').addEventListener('click', closeEntityModal);
|
|
2710
|
+
el('entityId').addEventListener('keydown', (event) => {
|
|
2711
|
+
if (event.key === 'Enter') submitCreateEntity().catch((error) => alert(error.message));
|
|
2712
|
+
});
|
|
2713
|
+
}
|
|
2714
|
+
|
|
2715
|
+
function profilePicker() {
|
|
2716
|
+
const profiles = Object.keys(state.config.profiles);
|
|
2717
|
+
if (profiles.length === 0) {
|
|
2718
|
+
return '<div class="field"><label>Profiles</label><div class="subtle">No profiles yet.</div></div>';
|
|
2719
|
+
}
|
|
2720
|
+
return '<div class="field"><label>Profiles</label><div class="profile-select-list">' +
|
|
2721
|
+
profiles.map((id) => '<label class="check"><input type="checkbox" data-create-profile="' + escapeHtml(id) + '"> ' + escapeHtml(id) + '</label>').join('') +
|
|
2722
|
+
'</div></div>';
|
|
2723
|
+
}
|
|
2724
|
+
|
|
2725
|
+
async function submitCreateEntity() {
|
|
2726
|
+
const kind = state.createKind;
|
|
2727
|
+
const id = el('entityId').value.trim();
|
|
2728
|
+
if (!id) throw new Error('Id is required');
|
|
2729
|
+
const profileIds = Array.from(document.querySelectorAll('[data-create-profile]:checked')).map((box) => box.dataset.createProfile);
|
|
2730
|
+
const body = {
|
|
2731
|
+
kind,
|
|
2732
|
+
id,
|
|
2733
|
+
baseId: el('entityBase').value,
|
|
2734
|
+
profileIds
|
|
2735
|
+
};
|
|
2736
|
+
const result = await api('/api/entities', { method: 'POST', body: JSON.stringify(body) });
|
|
2737
|
+
state.config = result;
|
|
2480
2738
|
state.drafts = {};
|
|
2481
|
-
setActive(kind, id
|
|
2739
|
+
setActive(kind, id);
|
|
2740
|
+
if (result.createdToken?.token) {
|
|
2741
|
+
showCreatedToken(result.createdToken.agentId, result.createdToken.token);
|
|
2742
|
+
} else {
|
|
2743
|
+
closeEntityModal();
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
function showCreatedToken(agentId, token) {
|
|
2748
|
+
el('entityModalTitle').textContent = 'Agent token';
|
|
2749
|
+
el('entityModalMeta').textContent = agentId;
|
|
2750
|
+
el('entityModalBody').innerHTML =
|
|
2751
|
+
'<div class="form-grid">' +
|
|
2752
|
+
'<div class="field"><label for="createdToken">Bearer token</label><input id="createdToken" class="token-value" readonly value="' + escapeHtml(token) + '"></div>' +
|
|
2753
|
+
'<div class="field"><label>Header</label><input class="token-value" readonly value="Authorization: Bearer ' + escapeHtml(token) + '"></div>' +
|
|
2754
|
+
'</div>';
|
|
2755
|
+
el('entityModalFooter').innerHTML =
|
|
2756
|
+
'<button id="copyCreatedToken" class="primary">Copy Token</button>' +
|
|
2757
|
+
'<button id="copyCreatedHeader">Copy Header</button>' +
|
|
2758
|
+
'<button id="entityDone">Done</button>';
|
|
2759
|
+
el('copyCreatedToken').addEventListener('click', () => copyText(token));
|
|
2760
|
+
el('copyCreatedHeader').addEventListener('click', () => copyText('Authorization: Bearer ' + token));
|
|
2761
|
+
el('entityDone').addEventListener('click', closeEntityModal);
|
|
2762
|
+
el('createdToken').select();
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
async function copyText(value) {
|
|
2766
|
+
if (navigator.clipboard?.writeText) {
|
|
2767
|
+
await navigator.clipboard.writeText(value);
|
|
2768
|
+
return;
|
|
2769
|
+
}
|
|
2770
|
+
const textarea = document.createElement('textarea');
|
|
2771
|
+
textarea.value = value;
|
|
2772
|
+
document.body.appendChild(textarea);
|
|
2773
|
+
textarea.select();
|
|
2774
|
+
document.execCommand('copy');
|
|
2775
|
+
textarea.remove();
|
|
2482
2776
|
}
|
|
2483
2777
|
|
|
2484
2778
|
async function addProvider() {
|
|
@@ -2642,8 +2936,8 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2642
2936
|
el('manageProviders').addEventListener('click', () => setActive('providers'));
|
|
2643
2937
|
el('viewActivity').addEventListener('click', () => setActive('activity'));
|
|
2644
2938
|
el('addProvider').addEventListener('click', () => addProvider().catch((error) => alert(error.message)));
|
|
2645
|
-
el('addAgent').addEventListener('click', () =>
|
|
2646
|
-
el('addProfile').addEventListener('click', () =>
|
|
2939
|
+
el('addAgent').addEventListener('click', () => openCreateModal('agent'));
|
|
2940
|
+
el('addProfile').addEventListener('click', () => openCreateModal('profile'));
|
|
2647
2941
|
el('deleteEntity').addEventListener('click', () => deleteCurrent().catch((error) => alert(error.message)));
|
|
2648
2942
|
el('search').addEventListener('input', (event) => { state.search = event.target.value; renderTools(); });
|
|
2649
2943
|
el('providerFilter').addEventListener('change', (event) => { state.provider = event.target.value; renderTools(); });
|
|
@@ -2669,11 +2963,16 @@ const INDEX_HTML = `<!doctype html>
|
|
|
2669
2963
|
el('resetAllRules').addEventListener('click', resetAllToCurrentConfig);
|
|
2670
2964
|
el('recommendedRules').addEventListener('click', setRecommended);
|
|
2671
2965
|
el('approvalModalClose').addEventListener('click', closeApprovalModal);
|
|
2966
|
+
el('entityModalClose').addEventListener('click', closeEntityModal);
|
|
2672
2967
|
el('approvalModal').addEventListener('click', (event) => {
|
|
2673
2968
|
if (event.target === el('approvalModal')) closeApprovalModal();
|
|
2674
2969
|
});
|
|
2970
|
+
el('entityModal').addEventListener('click', (event) => {
|
|
2971
|
+
if (event.target === el('entityModal')) closeEntityModal();
|
|
2972
|
+
});
|
|
2675
2973
|
document.addEventListener('keydown', (event) => {
|
|
2676
2974
|
if (event.key === 'Escape') {
|
|
2975
|
+
closeEntityModal();
|
|
2677
2976
|
closeApprovalModal();
|
|
2678
2977
|
return;
|
|
2679
2978
|
}
|