@dollhousemcp/mcp-server 2.0.25 → 2.0.26
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/CHANGELOG.md +4 -0
- package/dist/auto-dollhouse/portDiscovery.d.ts.map +1 -1
- package/dist/auto-dollhouse/portDiscovery.js +7 -4
- package/dist/di/Container.d.ts.map +1 -1
- package/dist/di/Container.js +4 -2
- package/dist/generated/version.d.ts +2 -2
- package/dist/generated/version.js +3 -3
- package/dist/handlers/mcp-aql/MCPAQLHandler.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/MCPAQLHandler.js +18 -1
- package/dist/handlers/mcp-aql/OperationRouter.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/OperationRouter.js +6 -1
- package/dist/handlers/mcp-aql/OperationSchema.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/OperationSchema.js +16 -1
- package/dist/handlers/mcp-aql/SchemaDispatcher.d.ts.map +1 -1
- package/dist/handlers/mcp-aql/SchemaDispatcher.js +2 -1
- package/dist/index.js +14 -2
- package/dist/server/tools/BuildInfoTools.d.ts +1 -0
- package/dist/server/tools/BuildInfoTools.d.ts.map +1 -1
- package/dist/server/tools/BuildInfoTools.js +2 -1
- package/dist/server/tools/MCPAQLTools.js +3 -1
- package/dist/services/ActivationStore.d.ts +8 -0
- package/dist/services/ActivationStore.d.ts.map +1 -1
- package/dist/services/ActivationStore.js +28 -29
- package/dist/services/BuildInfoService.d.ts +3 -0
- package/dist/services/BuildInfoService.d.ts.map +1 -1
- package/dist/services/BuildInfoService.js +18 -1
- package/dist/services/sessionIdentity.d.ts +24 -0
- package/dist/services/sessionIdentity.d.ts.map +1 -0
- package/dist/services/sessionIdentity.js +42 -0
- package/dist/utils/permissionAuthority.d.ts +38 -0
- package/dist/utils/permissionAuthority.d.ts.map +1 -0
- package/dist/utils/permissionAuthority.js +341 -0
- package/dist/utils/permissionHooks.d.ts.map +1 -1
- package/dist/utils/permissionHooks.js +10 -1
- package/dist/web/console/UnifiedConsole.d.ts +2 -0
- package/dist/web/console/UnifiedConsole.d.ts.map +1 -1
- package/dist/web/console/UnifiedConsole.js +3 -1
- package/dist/web/portDiscovery.d.ts +7 -0
- package/dist/web/portDiscovery.d.ts.map +1 -1
- package/dist/web/portDiscovery.js +35 -4
- package/dist/web/public/app.js +28 -4
- package/dist/web/public/index.html +2 -0
- package/dist/web/public/permissions.css +456 -0
- package/dist/web/public/permissions.js +553 -16
- package/dist/web/public/sessions.css +119 -0
- package/dist/web/public/sessions.js +95 -9
- package/dist/web/public/setup.js +56 -4
- package/dist/web/public/styles.css +21 -2
- package/dist/web/routes/permissionRoutes.d.ts +4 -1
- package/dist/web/routes/permissionRoutes.d.ts.map +1 -1
- package/dist/web/routes/permissionRoutes.js +118 -6
- package/dist/web/routes/setupRoutes.d.ts +18 -0
- package/dist/web/routes/setupRoutes.d.ts.map +1 -1
- package/dist/web/routes/setupRoutes.js +47 -26
- package/dist/web/server.d.ts +4 -0
- package/dist/web/server.d.ts.map +1 -1
- package/dist/web/server.js +19 -1
- package/package.json +4 -3
- package/scripts/pretooluse-dollhouse.sh +78 -12
- package/scripts/pretooluse-vscode.sh +6 -9
- package/scripts/pretooluse-windsurf.sh +6 -9
- package/server.json +2 -2
|
@@ -18,6 +18,34 @@
|
|
|
18
18
|
let latestAggregateData = null;
|
|
19
19
|
let latestSelectedData = null;
|
|
20
20
|
let latestPollRequestId = 0;
|
|
21
|
+
const AUTHORITY_MODE_REQUEST_STATES = {
|
|
22
|
+
idle: 'idle',
|
|
23
|
+
saving: 'saving',
|
|
24
|
+
};
|
|
25
|
+
const AUTHORITY_AUTHORITATIVE_HOSTS = new Set(['claude-code']);
|
|
26
|
+
// Authority modes are intentionally phrased in human terms in the UI:
|
|
27
|
+
// - off => host-controlled permissions
|
|
28
|
+
// - shared => both Dollhouse and the host participate
|
|
29
|
+
// - authoritative => Dollhouse is the source of truth
|
|
30
|
+
const authorityUiState = {
|
|
31
|
+
selectedHost: 'claude-code',
|
|
32
|
+
selectedMode: 'shared',
|
|
33
|
+
draftReason: '',
|
|
34
|
+
dirty: false,
|
|
35
|
+
requestState: AUTHORITY_MODE_REQUEST_STATES.idle,
|
|
36
|
+
feedback: '',
|
|
37
|
+
feedbackKind: 'info',
|
|
38
|
+
};
|
|
39
|
+
const AUTHORITY_HOST_META = {
|
|
40
|
+
'claude-code': { label: 'Claude Code', shortLabel: 'CC', tone: 'claude' },
|
|
41
|
+
'codex': { label: 'Codex', shortLabel: 'CX', tone: 'codex' },
|
|
42
|
+
'cursor': { label: 'Cursor', shortLabel: 'CU', tone: 'cursor' },
|
|
43
|
+
'vscode': { label: 'VS Code', shortLabel: 'VS', tone: 'vscode' },
|
|
44
|
+
'windsurf': { label: 'Windsurf', shortLabel: 'WS', tone: 'windsurf' },
|
|
45
|
+
'gemini': { label: 'Gemini CLI', shortLabel: 'GM', tone: 'gemini' },
|
|
46
|
+
'cline': { label: 'Cline', shortLabel: 'CL', tone: 'cline' },
|
|
47
|
+
'lmstudio': { label: 'LM Studio', shortLabel: 'LM', tone: 'lmstudio' },
|
|
48
|
+
};
|
|
21
49
|
|
|
22
50
|
async function fetchPermissionStatus(sessionId) {
|
|
23
51
|
const query = sessionId ? `?sessionId=${encodeURIComponent(sessionId)}` : '';
|
|
@@ -26,6 +54,17 @@
|
|
|
26
54
|
return res.json();
|
|
27
55
|
}
|
|
28
56
|
|
|
57
|
+
async function updatePermissionAuthority(payload) {
|
|
58
|
+
const res = await DollhouseAuth.apiFetch('/api/permissions/authority', {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'Content-Type': 'application/json' },
|
|
61
|
+
body: JSON.stringify(payload),
|
|
62
|
+
});
|
|
63
|
+
const data = await res.json().catch(function () { return {}; });
|
|
64
|
+
if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
|
|
65
|
+
return data;
|
|
66
|
+
}
|
|
67
|
+
|
|
29
68
|
// ── Public API ─────────────────────────────────────────────
|
|
30
69
|
|
|
31
70
|
window.DollhouseConsole = window.DollhouseConsole || {};
|
|
@@ -43,15 +82,31 @@
|
|
|
43
82
|
if (tabs) {
|
|
44
83
|
tabs.addEventListener('click', function (e) {
|
|
45
84
|
const btn = e.target.closest('.console-tab');
|
|
46
|
-
if (btn
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
85
|
+
if (!btn || btn.dataset.tab !== 'permissions') {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const dc = window.DollhouseConsole;
|
|
90
|
+
if (!dc || !dc.permissions) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!initialized) {
|
|
95
|
+
dc.permissions.init();
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (dc.permissions.refresh) {
|
|
100
|
+
dc.permissions.refresh();
|
|
52
101
|
}
|
|
53
102
|
});
|
|
54
103
|
}
|
|
104
|
+
|
|
105
|
+
window.addEventListener('dollhouse:policy-debug-visibility-changed', function () {
|
|
106
|
+
if (initialized) {
|
|
107
|
+
renderFromCache();
|
|
108
|
+
}
|
|
109
|
+
});
|
|
55
110
|
});
|
|
56
111
|
|
|
57
112
|
// ── Initialization ─────────────────────────────────────────
|
|
@@ -65,6 +120,7 @@
|
|
|
65
120
|
|
|
66
121
|
root.innerHTML = buildDashboardHTML();
|
|
67
122
|
attachCardToggles();
|
|
123
|
+
attachAuthorityControls();
|
|
68
124
|
poll(); // immediate first fetch
|
|
69
125
|
pollTimer = setInterval(poll, POLL_INTERVAL_MS);
|
|
70
126
|
}
|
|
@@ -87,13 +143,14 @@
|
|
|
87
143
|
return;
|
|
88
144
|
}
|
|
89
145
|
|
|
146
|
+
const visibleAggregateData = getVisibleAggregateData(aggregateData);
|
|
90
147
|
const currentSessionId = window.DollhouseSessions?.getFilterSessionId?.() || '';
|
|
91
|
-
const selectedData = deriveSelectedSessionData(
|
|
148
|
+
const selectedData = deriveSelectedSessionData(visibleAggregateData, currentSessionId);
|
|
92
149
|
|
|
93
150
|
latestAggregateData = aggregateData;
|
|
94
151
|
latestSelectedData = selectedData;
|
|
95
152
|
window.DollhouseSessions?.setPolicySessions?.(aggregateData.knownSessions || []);
|
|
96
|
-
render(
|
|
153
|
+
render(visibleAggregateData, selectedData);
|
|
97
154
|
} catch (err) {
|
|
98
155
|
renderError(err.message);
|
|
99
156
|
}
|
|
@@ -106,15 +163,16 @@
|
|
|
106
163
|
}
|
|
107
164
|
|
|
108
165
|
const sessionId = window.DollhouseSessions?.getFilterSessionId?.() || '';
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
166
|
+
const visibleAggregateData = getVisibleAggregateData(latestAggregateData);
|
|
167
|
+
latestSelectedData = deriveSelectedSessionData(visibleAggregateData, sessionId);
|
|
168
|
+
render(visibleAggregateData, latestSelectedData);
|
|
112
169
|
}
|
|
113
170
|
|
|
114
171
|
// ── Rendering ──────────────────────────────────────────────
|
|
115
172
|
|
|
116
173
|
function render(data, selectedData) {
|
|
117
174
|
renderStatusBar(data);
|
|
175
|
+
renderAuthorityMode(data);
|
|
118
176
|
renderSummaryStats(data);
|
|
119
177
|
renderAdvisory(data);
|
|
120
178
|
renderPolicySources(data, selectedData);
|
|
@@ -196,6 +254,119 @@
|
|
|
196
254
|
setText('perm-stat-asked', asked);
|
|
197
255
|
}
|
|
198
256
|
|
|
257
|
+
function renderAuthorityMode(data) {
|
|
258
|
+
const card = document.getElementById('perm-authority-card');
|
|
259
|
+
const saveButton = document.getElementById('perm-authority-save-btn');
|
|
260
|
+
const message = document.getElementById('perm-authority-message');
|
|
261
|
+
const currentHostList = document.getElementById('perm-authority-current-host-list');
|
|
262
|
+
const selectedHostHeading = document.getElementById('perm-authority-selected-host');
|
|
263
|
+
const reasonInput = document.getElementById('perm-authority-reason');
|
|
264
|
+
const note = document.getElementById('perm-authority-note');
|
|
265
|
+
const authoritativeNote = document.getElementById('perm-authority-authoritative-note');
|
|
266
|
+
const dirtyState = document.getElementById('perm-authority-dirty-state');
|
|
267
|
+
const saveCopy = document.getElementById('perm-authority-save-copy');
|
|
268
|
+
const saveShell = document.getElementById('perm-authority-save-shell');
|
|
269
|
+
|
|
270
|
+
if (!card || !saveButton || !message || !currentHostList || !selectedHostHeading || !reasonInput || !note || !authoritativeNote || !dirtyState || !saveCopy || !saveShell) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const supportedHosts = Array.isArray(data?.authoritySupportedHosts)
|
|
275
|
+
? data.authoritySupportedHosts
|
|
276
|
+
: ['claude-code'];
|
|
277
|
+
const authority = data?.authority || { defaultMode: 'shared', hosts: {} };
|
|
278
|
+
|
|
279
|
+
if (supportedHosts.length === 0) {
|
|
280
|
+
currentHostList.innerHTML = '<div class="perm-pattern-empty">No installed permission hosts detected yet.</div>';
|
|
281
|
+
selectedHostHeading.textContent = 'No installed hosts';
|
|
282
|
+
note.textContent = 'Install Dollhouse permission hooks for a host before changing permission authority mode here.';
|
|
283
|
+
authoritativeNote.hidden = true;
|
|
284
|
+
authoritativeNote.textContent = '';
|
|
285
|
+
dirtyState.hidden = true;
|
|
286
|
+
dirtyState.textContent = '';
|
|
287
|
+
message.hidden = true;
|
|
288
|
+
message.textContent = '';
|
|
289
|
+
message.dataset.kind = 'info';
|
|
290
|
+
saveCopy.textContent = 'Once a host is installed and configured, it will appear on the left for editing.';
|
|
291
|
+
saveShell.dataset.dirty = 'false';
|
|
292
|
+
saveShell.dataset.busy = 'false';
|
|
293
|
+
card.dataset.authorityDirty = 'false';
|
|
294
|
+
card.setAttribute('aria-busy', 'false');
|
|
295
|
+
reasonInput.value = authorityUiState.draftReason;
|
|
296
|
+
setAuthorityRadioState('perm-authority-mode-off', false, true);
|
|
297
|
+
setAuthorityRadioState('perm-authority-mode-shared', false, true);
|
|
298
|
+
setAuthorityRadioState('perm-authority-mode-authoritative', false, true);
|
|
299
|
+
saveButton.disabled = true;
|
|
300
|
+
saveButton.textContent = 'No Installed Hosts Yet';
|
|
301
|
+
saveButton.setAttribute('aria-busy', 'false');
|
|
302
|
+
card.hidden = false;
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
if (!supportedHosts.includes(authorityUiState.selectedHost)) {
|
|
307
|
+
authorityUiState.selectedHost = supportedHosts[0];
|
|
308
|
+
authorityUiState.dirty = false;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const serverMode = getAuthorityModeForHost(authority, authorityUiState.selectedHost);
|
|
312
|
+
if (!authorityUiState.dirty) {
|
|
313
|
+
authorityUiState.selectedMode = serverMode;
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
reasonInput.value = authorityUiState.draftReason;
|
|
317
|
+
|
|
318
|
+
const authoritativeSupported = AUTHORITY_AUTHORITATIVE_HOSTS.has(authorityUiState.selectedHost);
|
|
319
|
+
const desiredMode = authoritativeSupported ? authorityUiState.selectedMode : fallbackAuthorityMode(authorityUiState.selectedMode);
|
|
320
|
+
const dirty = desiredMode !== serverMode;
|
|
321
|
+
|
|
322
|
+
setAuthorityRadioState('perm-authority-mode-off', desiredMode === 'off', false);
|
|
323
|
+
setAuthorityRadioState('perm-authority-mode-shared', desiredMode === 'shared', false);
|
|
324
|
+
setAuthorityRadioState('perm-authority-mode-authoritative', desiredMode === 'authoritative', !authoritativeSupported);
|
|
325
|
+
|
|
326
|
+
currentHostList.innerHTML = renderAuthorityCurrentHostList(authority, supportedHosts, authorityUiState.selectedHost);
|
|
327
|
+
selectedHostHeading.textContent = formatAuthorityHost(authorityUiState.selectedHost);
|
|
328
|
+
note.textContent = 'Human-only control. AI can read authority mode but cannot change it through MCP.';
|
|
329
|
+
authoritativeNote.hidden = authoritativeSupported;
|
|
330
|
+
authoritativeNote.textContent = authoritativeSupported
|
|
331
|
+
? ''
|
|
332
|
+
: 'Claude Code only for now. Other hosts can use Host-Controlled or Shared Permissioning mode.';
|
|
333
|
+
dirtyState.hidden = !dirty;
|
|
334
|
+
dirtyState.textContent = dirty
|
|
335
|
+
? `Unsaved change: ${formatAuthorityHost(authorityUiState.selectedHost)} will move from ${formatAuthorityMode(serverMode)} to ${formatAuthorityMode(desiredMode)} after you save.`
|
|
336
|
+
: '';
|
|
337
|
+
saveCopy.textContent = buildAuthoritySaveCopy({
|
|
338
|
+
host: authorityUiState.selectedHost,
|
|
339
|
+
currentMode: serverMode,
|
|
340
|
+
desiredMode,
|
|
341
|
+
dirty,
|
|
342
|
+
saving: authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving,
|
|
343
|
+
});
|
|
344
|
+
saveShell.dataset.dirty = dirty ? 'true' : 'false';
|
|
345
|
+
saveShell.dataset.busy = authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving ? 'true' : 'false';
|
|
346
|
+
card.dataset.authorityDirty = dirty ? 'true' : 'false';
|
|
347
|
+
card.setAttribute('aria-busy', authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving ? 'true' : 'false');
|
|
348
|
+
|
|
349
|
+
if (authorityUiState.feedback) {
|
|
350
|
+
message.hidden = false;
|
|
351
|
+
message.textContent = authorityUiState.feedback;
|
|
352
|
+
message.dataset.kind = authorityUiState.feedbackKind;
|
|
353
|
+
} else {
|
|
354
|
+
message.hidden = true;
|
|
355
|
+
message.textContent = '';
|
|
356
|
+
message.dataset.kind = 'info';
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
saveButton.disabled = authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving || !dirty;
|
|
360
|
+
saveButton.textContent = authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving
|
|
361
|
+
? `Saving ${formatAuthorityMode(desiredMode)}...`
|
|
362
|
+
: dirty
|
|
363
|
+
? `Save ${formatAuthorityMode(desiredMode)} Mode for ${formatAuthorityHost(authorityUiState.selectedHost)}`
|
|
364
|
+
: `Saved for ${formatAuthorityHost(authorityUiState.selectedHost)}`;
|
|
365
|
+
saveButton.dataset.dirty = dirty ? 'true' : 'false';
|
|
366
|
+
saveButton.setAttribute('aria-busy', authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving ? 'true' : 'false');
|
|
367
|
+
card.hidden = false;
|
|
368
|
+
}
|
|
369
|
+
|
|
199
370
|
function renderAdvisory(data) {
|
|
200
371
|
const advisory = document.getElementById('perm-all-sessions-advisory');
|
|
201
372
|
if (!advisory) return;
|
|
@@ -500,6 +671,60 @@
|
|
|
500
671
|
};
|
|
501
672
|
}
|
|
502
673
|
|
|
674
|
+
/**
|
|
675
|
+
* Persisted policy state rows are primarily a debugging aid, so the
|
|
676
|
+
* dashboard treats them as opt-in and mirrors the explicit sessions UI flag.
|
|
677
|
+
*/
|
|
678
|
+
function shouldShowPersistedPolicyDebug() {
|
|
679
|
+
return window.DollhouseSessions?.isPolicyDebugVisible?.() === true;
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function getVisibleAggregateData(data) {
|
|
683
|
+
if (!data || shouldShowPersistedPolicyDebug()) {
|
|
684
|
+
return data;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const hiddenPolicySessions = Array.isArray(data.knownSessions) ? data.knownSessions : [];
|
|
688
|
+
if (hiddenPolicySessions.length === 0) {
|
|
689
|
+
return data;
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
const hiddenPolicySessionIds = new Set(
|
|
693
|
+
hiddenPolicySessions
|
|
694
|
+
.map(function (session) { return session && typeof session.sessionId === 'string' ? session.sessionId : ''; })
|
|
695
|
+
.filter(Boolean),
|
|
696
|
+
);
|
|
697
|
+
|
|
698
|
+
const filteredElements = ((data.elements || []).filter(function (element) {
|
|
699
|
+
const sessionIds = Array.isArray(element?.sessionIds) ? element.sessionIds.filter(function (sessionId) {
|
|
700
|
+
return typeof sessionId === 'string' && sessionId !== '';
|
|
701
|
+
}) : [];
|
|
702
|
+
if (sessionIds.length === 0) {
|
|
703
|
+
return true;
|
|
704
|
+
}
|
|
705
|
+
return sessionIds.some(function (sessionId) {
|
|
706
|
+
return !hiddenPolicySessionIds.has(sessionId);
|
|
707
|
+
});
|
|
708
|
+
}));
|
|
709
|
+
|
|
710
|
+
return {
|
|
711
|
+
...data,
|
|
712
|
+
activeElementCount: filteredElements.length,
|
|
713
|
+
hasAllowlist: filteredElements.some(function (element) {
|
|
714
|
+
return (Array.isArray(element.allowRules) && element.allowRules.length > 0)
|
|
715
|
+
|| (Array.isArray(element.allowPatterns) && element.allowPatterns.length > 0);
|
|
716
|
+
}),
|
|
717
|
+
elements: filteredElements,
|
|
718
|
+
knownSessions: [],
|
|
719
|
+
denyPatterns: flattenElementPatterns(filteredElements, 'denyPatterns'),
|
|
720
|
+
allowPatterns: flattenElementPatterns(filteredElements, 'allowPatterns'),
|
|
721
|
+
confirmPatterns: flattenElementPatterns(filteredElements, 'confirmPatterns'),
|
|
722
|
+
denyRules: flattenElementPatterns(filteredElements, 'denyRules'),
|
|
723
|
+
allowRules: flattenElementPatterns(filteredElements, 'allowRules'),
|
|
724
|
+
confirmRules: flattenElementPatterns(filteredElements, 'confirmRules'),
|
|
725
|
+
};
|
|
726
|
+
}
|
|
727
|
+
|
|
503
728
|
function flattenElementPatterns(elements, key) {
|
|
504
729
|
return elements.flatMap(function (element) {
|
|
505
730
|
return Array.isArray(element[key]) ? element[key] : [];
|
|
@@ -608,6 +833,80 @@
|
|
|
608
833
|
</div>
|
|
609
834
|
</div>
|
|
610
835
|
|
|
836
|
+
<div class="perm-card perm-card--full" data-collapsed="false" id="perm-authority-card">
|
|
837
|
+
<div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
|
|
838
|
+
<h3 class="perm-card-title">Permission Authority Mode</h3>
|
|
839
|
+
<span class="perm-card-toggle" aria-hidden="true">▾</span>
|
|
840
|
+
</div>
|
|
841
|
+
<div class="perm-card-body">
|
|
842
|
+
<div class="perm-selected-header perm-selected-header--compact">
|
|
843
|
+
<div>
|
|
844
|
+
<div class="perm-selected-title">Human-Only Permission Authority</div>
|
|
845
|
+
<div class="perm-selected-subtitle">Choose whether the host's native permission system or DollhouseMCP is the final authority for tool approvals.</div>
|
|
846
|
+
</div>
|
|
847
|
+
</div>
|
|
848
|
+
|
|
849
|
+
<div class="perm-selected-grid">
|
|
850
|
+
<div class="perm-selected-panel">
|
|
851
|
+
<h4 class="perm-selected-panel-title">Current Permission State</h4>
|
|
852
|
+
<div class="perm-authority-current-list" id="perm-authority-current-host-list"></div>
|
|
853
|
+
</div>
|
|
854
|
+
|
|
855
|
+
<div class="perm-selected-panel">
|
|
856
|
+
<h4 class="perm-selected-panel-title">Change Permission Mode</h4>
|
|
857
|
+
<div class="perm-authority-selected-host" id="perm-authority-selected-host">Claude Code</div>
|
|
858
|
+
<div class="perm-selected-subtitle">Choose how this host should handle permission decisions.</div>
|
|
859
|
+
|
|
860
|
+
<div class="perm-authority-options" role="radiogroup" aria-label="Authority mode">
|
|
861
|
+
<label class="perm-authority-option" id="perm-authority-option-off">
|
|
862
|
+
<span class="perm-authority-option-main">
|
|
863
|
+
<input type="radio" name="perm-authority-mode" id="perm-authority-mode-off" value="off">
|
|
864
|
+
<span class="perm-authority-option-copy">
|
|
865
|
+
<span class="perm-authority-option-title">Host-Controlled Permissions</span>
|
|
866
|
+
<span class="perm-authority-option-description">Dollhouse steps out of the way. The host's own permission system handles approvals by itself.</span>
|
|
867
|
+
</span>
|
|
868
|
+
</span>
|
|
869
|
+
</label>
|
|
870
|
+
<label class="perm-authority-option" id="perm-authority-option-shared">
|
|
871
|
+
<span class="perm-authority-option-main">
|
|
872
|
+
<input type="radio" name="perm-authority-mode" id="perm-authority-mode-shared" value="shared">
|
|
873
|
+
<span class="perm-authority-option-copy">
|
|
874
|
+
<span class="perm-authority-option-title">Shared Permissioning</span>
|
|
875
|
+
<span class="perm-authority-option-description">Dollhouse stays active, but the host permission system can still be more restrictive.</span>
|
|
876
|
+
</span>
|
|
877
|
+
</span>
|
|
878
|
+
</label>
|
|
879
|
+
<label class="perm-authority-option perm-authority-option--authoritative" id="perm-authority-option-authoritative">
|
|
880
|
+
<span class="perm-authority-option-main">
|
|
881
|
+
<input type="radio" name="perm-authority-mode" id="perm-authority-mode-authoritative" value="authoritative">
|
|
882
|
+
<span class="perm-authority-option-copy">
|
|
883
|
+
<span class="perm-authority-option-title-row">
|
|
884
|
+
<span class="perm-authority-option-title">Dollhouse-Controlled Permissions</span>
|
|
885
|
+
<span class="perm-authority-inline-note" id="perm-authority-authoritative-note" hidden></span>
|
|
886
|
+
</span>
|
|
887
|
+
<span class="perm-authority-option-description">Dollhouse becomes the permission authority. It syncs Dollhouse allow, ask, and deny rules into the host so Dollhouse decides conflicts instead of the host's own approval flow.</span>
|
|
888
|
+
</span>
|
|
889
|
+
</span>
|
|
890
|
+
</label>
|
|
891
|
+
</div>
|
|
892
|
+
|
|
893
|
+
<label class="perm-selected-subtitle perm-authority-field-label" for="perm-authority-reason">Reason (optional)</label>
|
|
894
|
+
<input id="perm-authority-reason" class="perm-authority-reason-input" type="text" maxlength="200" placeholder="Why are you changing the permission authority mode?">
|
|
895
|
+
|
|
896
|
+
<p class="perm-selected-subtitle perm-authority-human-note" id="perm-authority-note"></p>
|
|
897
|
+
<div class="perm-authority-save-shell" id="perm-authority-save-shell" data-dirty="false">
|
|
898
|
+
<div class="perm-authority-dirty-state" id="perm-authority-dirty-state" hidden></div>
|
|
899
|
+
<div class="perm-inline-warning" id="perm-authority-message" hidden></div>
|
|
900
|
+
<div class="perm-authority-actions">
|
|
901
|
+
<div class="perm-authority-save-copy" id="perm-authority-save-copy"></div>
|
|
902
|
+
<button type="button" class="perm-panel-action perm-authority-save-btn" id="perm-authority-save-btn">Save Authority Mode</button>
|
|
903
|
+
</div>
|
|
904
|
+
</div>
|
|
905
|
+
</div>
|
|
906
|
+
</div>
|
|
907
|
+
</div>
|
|
908
|
+
</div>
|
|
909
|
+
|
|
611
910
|
<!-- Selected Session Detail -->
|
|
612
911
|
<div class="perm-card perm-card--full" data-collapsed="false" id="perm-selected-card" hidden>
|
|
613
912
|
<div class="perm-card-header" role="button" tabindex="0" aria-expanded="true">
|
|
@@ -710,8 +1009,10 @@
|
|
|
710
1009
|
<span>Aggregate decision log across all sessions</span>
|
|
711
1010
|
<span id="perm-audit-modal-count">0 captured entries</span>
|
|
712
1011
|
</div>
|
|
713
|
-
<
|
|
714
|
-
|
|
1012
|
+
<div class="modal-header-actions">
|
|
1013
|
+
<button type="button" class="modal-action-btn" id="perm-audit-copy-btn">Copy Markdown</button>
|
|
1014
|
+
<button type="button" class="modal-close" id="perm-audit-modal-close" aria-label="Close audit view">✕</button>
|
|
1015
|
+
</div>
|
|
715
1016
|
</header>
|
|
716
1017
|
<div class="modal-body">
|
|
717
1018
|
<div class="perm-feed perm-feed--modal" id="perm-audit-modal-feed" role="log" aria-live="polite" aria-label="Full permission decision audit feed">
|
|
@@ -775,6 +1076,52 @@
|
|
|
775
1076
|
}
|
|
776
1077
|
}
|
|
777
1078
|
|
|
1079
|
+
function attachAuthorityControls() {
|
|
1080
|
+
const currentHostList = document.getElementById('perm-authority-current-host-list');
|
|
1081
|
+
const reasonInput = document.getElementById('perm-authority-reason');
|
|
1082
|
+
const saveButton = document.getElementById('perm-authority-save-btn');
|
|
1083
|
+
|
|
1084
|
+
if (currentHostList) {
|
|
1085
|
+
currentHostList.addEventListener('click', function (event) {
|
|
1086
|
+
const row = event.target.closest('.perm-authority-current-host[data-host]');
|
|
1087
|
+
if (!row) {
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
const host = row.getAttribute('data-host');
|
|
1091
|
+
if (!host || host === authorityUiState.selectedHost) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
authorityUiState.selectedHost = host;
|
|
1095
|
+
authorityUiState.feedback = '';
|
|
1096
|
+
authorityUiState.feedbackKind = 'info';
|
|
1097
|
+
authorityUiState.dirty = false;
|
|
1098
|
+
renderAuthorityMode(latestAggregateData);
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1102
|
+
document.querySelectorAll('input[name="perm-authority-mode"]').forEach(function (input) {
|
|
1103
|
+
input.addEventListener('change', function (event) {
|
|
1104
|
+
authorityUiState.selectedMode = event.target.value;
|
|
1105
|
+
authorityUiState.feedback = '';
|
|
1106
|
+
authorityUiState.feedbackKind = 'info';
|
|
1107
|
+
authorityUiState.dirty = true;
|
|
1108
|
+
renderAuthorityMode(latestAggregateData);
|
|
1109
|
+
});
|
|
1110
|
+
});
|
|
1111
|
+
|
|
1112
|
+
if (reasonInput) {
|
|
1113
|
+
reasonInput.addEventListener('input', function (event) {
|
|
1114
|
+
authorityUiState.draftReason = event.target.value;
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
if (saveButton) {
|
|
1119
|
+
saveButton.addEventListener('click', function () {
|
|
1120
|
+
saveAuthorityMode();
|
|
1121
|
+
});
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
|
|
778
1125
|
function openAuditModal() {
|
|
779
1126
|
const auditModal = document.getElementById('perm-audit-modal');
|
|
780
1127
|
if (!auditModal) return;
|
|
@@ -818,10 +1165,52 @@
|
|
|
818
1165
|
|
|
819
1166
|
async function copyTextToClipboard(text) {
|
|
820
1167
|
if (navigator.clipboard?.writeText) {
|
|
821
|
-
|
|
822
|
-
|
|
1168
|
+
try {
|
|
1169
|
+
await navigator.clipboard.writeText(text);
|
|
1170
|
+
return;
|
|
1171
|
+
} catch {
|
|
1172
|
+
// Fall through to the user-gesture fallback below. Some embedded browsers
|
|
1173
|
+
// expose the clipboard API but still reject writes in modal contexts.
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
copyTextWithSelectionFallback(text);
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
function copyTextWithSelectionFallback(text) {
|
|
1181
|
+
const textarea = document.createElement('textarea');
|
|
1182
|
+
let handled = false;
|
|
1183
|
+
|
|
1184
|
+
function handleCopy(event) {
|
|
1185
|
+
if (!event.clipboardData) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
event.clipboardData.setData('text/plain', text);
|
|
1189
|
+
event.preventDefault();
|
|
1190
|
+
handled = true;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
textarea.value = text;
|
|
1194
|
+
textarea.setAttribute('readonly', '');
|
|
1195
|
+
textarea.setAttribute('aria-hidden', 'true');
|
|
1196
|
+
textarea.style.position = 'fixed';
|
|
1197
|
+
textarea.style.top = '-9999px';
|
|
1198
|
+
textarea.style.left = '-9999px';
|
|
1199
|
+
textarea.style.opacity = '0';
|
|
1200
|
+
document.body.appendChild(textarea);
|
|
1201
|
+
document.addEventListener('copy', handleCopy, true);
|
|
1202
|
+
textarea.focus();
|
|
1203
|
+
textarea.select();
|
|
1204
|
+
textarea.setSelectionRange(0, textarea.value.length);
|
|
1205
|
+
|
|
1206
|
+
try {
|
|
1207
|
+
if (!document.execCommand || !document.execCommand('copy') || !handled) {
|
|
1208
|
+
throw new Error('Clipboard copy command unavailable');
|
|
1209
|
+
}
|
|
1210
|
+
} finally {
|
|
1211
|
+
document.removeEventListener('copy', handleCopy, true);
|
|
1212
|
+
textarea.remove();
|
|
823
1213
|
}
|
|
824
|
-
throw new Error('Clipboard API unavailable');
|
|
825
1214
|
}
|
|
826
1215
|
|
|
827
1216
|
function setText(id, value) {
|
|
@@ -848,6 +1237,154 @@
|
|
|
848
1237
|
return str.length > len ? str.slice(0, len) + '...' : str;
|
|
849
1238
|
}
|
|
850
1239
|
|
|
1240
|
+
function getAuthorityModeForHost(authority, host) {
|
|
1241
|
+
return authority?.hosts?.[host]?.mode || authority?.defaultMode || 'shared';
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function formatAuthorityHost(host) {
|
|
1245
|
+
const meta = AUTHORITY_HOST_META[String(host || '')];
|
|
1246
|
+
if (meta?.label) {
|
|
1247
|
+
return meta.label;
|
|
1248
|
+
}
|
|
1249
|
+
return String(host || '')
|
|
1250
|
+
.split('-')
|
|
1251
|
+
.map(function (part) { return part ? part.charAt(0).toUpperCase() + part.slice(1) : part; })
|
|
1252
|
+
.join(' ');
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function formatAuthorityMode(mode) {
|
|
1256
|
+
return mode === 'off'
|
|
1257
|
+
? 'Host-Controlled Permissions'
|
|
1258
|
+
: mode === 'authoritative'
|
|
1259
|
+
? 'Dollhouse-Controlled Permissions'
|
|
1260
|
+
: 'Shared Permissioning';
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function buildAuthorityExplanation(mode, authoritativeSupported) {
|
|
1264
|
+
if (mode === 'off') {
|
|
1265
|
+
return 'Dollhouse is not participating in approvals here. The host handles permissions on its own.';
|
|
1266
|
+
}
|
|
1267
|
+
if (mode === 'authoritative') {
|
|
1268
|
+
return 'Dollhouse is the source of truth for permissions here. The host follows Dollhouse-synced allow, ask, and deny rules, while user-authored entries outside Dollhouse-managed settings are still preserved.';
|
|
1269
|
+
}
|
|
1270
|
+
return authoritativeSupported
|
|
1271
|
+
? 'Dollhouse participates in permission checks, but the host can still be more restrictive.'
|
|
1272
|
+
: 'This host currently supports shared/advisory mode while authoritative settings sync is still being added.';
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
function fallbackAuthorityMode(mode) {
|
|
1276
|
+
return mode === 'authoritative' ? 'shared' : mode;
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function setAuthorityRadioState(id, checked, disabled) {
|
|
1280
|
+
const radio = document.getElementById(id);
|
|
1281
|
+
if (!radio) return;
|
|
1282
|
+
radio.checked = checked;
|
|
1283
|
+
radio.disabled = disabled;
|
|
1284
|
+
const option = radio.closest('.perm-authority-option');
|
|
1285
|
+
if (option) {
|
|
1286
|
+
option.dataset.checked = checked ? 'true' : 'false';
|
|
1287
|
+
option.dataset.disabled = disabled ? 'true' : 'false';
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function buildAuthoritySaveCopy(state) {
|
|
1292
|
+
if (state.saving) {
|
|
1293
|
+
return `Applying ${formatAuthorityMode(state.desiredMode)} mode for ${formatAuthorityHost(state.host)}...`;
|
|
1294
|
+
}
|
|
1295
|
+
if (state.dirty) {
|
|
1296
|
+
return `Review the change and save to apply ${formatAuthorityMode(state.desiredMode)} mode for ${formatAuthorityHost(state.host)}.`;
|
|
1297
|
+
}
|
|
1298
|
+
return `${formatAuthorityHost(state.host)} is currently saved in ${formatAuthorityMode(state.currentMode)} mode.`;
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
function renderAuthorityCurrentHostList(authority, supportedHosts, selectedHost) {
|
|
1302
|
+
const explicitHosts = Object.keys(authority?.hosts || {});
|
|
1303
|
+
const hostIds = Array.from(new Set(explicitHosts.concat(supportedHosts || [])));
|
|
1304
|
+
const orderedHosts = hostIds.sort(function (left, right) {
|
|
1305
|
+
return formatAuthorityHost(left).localeCompare(formatAuthorityHost(right));
|
|
1306
|
+
});
|
|
1307
|
+
|
|
1308
|
+
if (orderedHosts.length === 0) {
|
|
1309
|
+
return '<div class="perm-pattern-empty">No host authority settings saved yet.</div>';
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
return orderedHosts.map(function (host) {
|
|
1313
|
+
const mode = getAuthorityModeForHost(authority, host);
|
|
1314
|
+
const meta = AUTHORITY_HOST_META[host] || { shortLabel: formatAuthorityHost(host).slice(0, 2).toUpperCase(), tone: 'generic' };
|
|
1315
|
+
const selectedAttr = host === selectedHost ? 'true' : 'false';
|
|
1316
|
+
return `
|
|
1317
|
+
<button type="button" class="perm-authority-current-host" data-selected="${selectedAttr}" data-host="${esc(host)}" aria-pressed="${selectedAttr}">
|
|
1318
|
+
<span class="perm-authority-host-mark perm-authority-host-mark--${esc(meta.tone || 'generic')}" aria-hidden="true">${esc(meta.shortLabel || 'DH')}</span>
|
|
1319
|
+
<span class="perm-authority-current-host-copy">
|
|
1320
|
+
<span class="perm-authority-current-host-name">${esc(formatAuthorityHost(host))}</span>
|
|
1321
|
+
<span class="perm-authority-current-host-mode">${esc(formatAuthorityMode(mode))}</span>
|
|
1322
|
+
</span>
|
|
1323
|
+
</button>
|
|
1324
|
+
`;
|
|
1325
|
+
}).join('');
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
async function saveAuthorityMode() {
|
|
1329
|
+
if (!latestAggregateData || authorityUiState.requestState === AUTHORITY_MODE_REQUEST_STATES.saving) {
|
|
1330
|
+
return;
|
|
1331
|
+
}
|
|
1332
|
+
|
|
1333
|
+
const authority = latestAggregateData.authority || { defaultMode: 'shared', hosts: {} };
|
|
1334
|
+
const currentMode = getAuthorityModeForHost(authority, authorityUiState.selectedHost);
|
|
1335
|
+
const requestedMode = AUTHORITY_AUTHORITATIVE_HOSTS.has(authorityUiState.selectedHost)
|
|
1336
|
+
? authorityUiState.selectedMode
|
|
1337
|
+
: fallbackAuthorityMode(authorityUiState.selectedMode);
|
|
1338
|
+
|
|
1339
|
+
if (requestedMode === currentMode) {
|
|
1340
|
+
authorityUiState.feedback = 'No authority-mode change to save.';
|
|
1341
|
+
authorityUiState.feedbackKind = 'info';
|
|
1342
|
+
renderAuthorityMode(latestAggregateData);
|
|
1343
|
+
return;
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
const confirmMessage = [
|
|
1347
|
+
`Change ${formatAuthorityHost(authorityUiState.selectedHost)} from ${formatAuthorityMode(currentMode)} to ${formatAuthorityMode(requestedMode)}?`,
|
|
1348
|
+
'',
|
|
1349
|
+
requestedMode === 'authoritative'
|
|
1350
|
+
? 'Dollhouse will take a backup and write managed host permission settings.'
|
|
1351
|
+
: requestedMode === 'off'
|
|
1352
|
+
? 'Dollhouse hooks will no-op and the host permission system will become the only gate.'
|
|
1353
|
+
: 'Dollhouse will stay active, but the host will remain authoritative on conflicts.',
|
|
1354
|
+
].join('\n');
|
|
1355
|
+
|
|
1356
|
+
if (!window.confirm(confirmMessage)) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
authorityUiState.requestState = AUTHORITY_MODE_REQUEST_STATES.saving;
|
|
1361
|
+
authorityUiState.feedback = '';
|
|
1362
|
+
renderAuthorityMode(latestAggregateData);
|
|
1363
|
+
|
|
1364
|
+
try {
|
|
1365
|
+
const response = await updatePermissionAuthority({
|
|
1366
|
+
host: authorityUiState.selectedHost,
|
|
1367
|
+
mode: requestedMode,
|
|
1368
|
+
reason: authorityUiState.draftReason.trim() || undefined,
|
|
1369
|
+
});
|
|
1370
|
+
|
|
1371
|
+
latestAggregateData = {
|
|
1372
|
+
...latestAggregateData,
|
|
1373
|
+
authority: response.authority,
|
|
1374
|
+
};
|
|
1375
|
+
authorityUiState.selectedMode = requestedMode;
|
|
1376
|
+
authorityUiState.dirty = false;
|
|
1377
|
+
authorityUiState.feedback = `Saved ${formatAuthorityMode(requestedMode)} mode for ${formatAuthorityHost(authorityUiState.selectedHost)}.`;
|
|
1378
|
+
authorityUiState.feedbackKind = 'success';
|
|
1379
|
+
} catch (error) {
|
|
1380
|
+
authorityUiState.feedback = error instanceof Error ? error.message : 'Failed to update permission authority.';
|
|
1381
|
+
authorityUiState.feedbackKind = 'error';
|
|
1382
|
+
} finally {
|
|
1383
|
+
authorityUiState.requestState = AUTHORITY_MODE_REQUEST_STATES.idle;
|
|
1384
|
+
renderAuthorityMode(latestAggregateData);
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
851
1388
|
function dataAdvisoryPlaceholder() {
|
|
852
1389
|
return '<span id="perm-all-sessions-advisory" class="perm-inline-advisory" hidden></span>';
|
|
853
1390
|
}
|