Skip to content

Commit 02fe8e7

Browse files
olaservoclaude
andcommitted
Add Allowed Origins UI for well-known publishers
Adds a panel parallel to "Allowed Orgs" so users can configure the well-known origin allowlist from the skill-config UI, matching how GitHub orgs are managed today. Server tools (skill-config-tool.ts): - skill-config-add-allowed-origin: accepts an origin or full URL, normalizes via new URL(input).origin, rejects http:// unless WELL_KNOWN_ALLOW_HTTP=1, persists via addWellKnownAllowedOrigin, and triggers onDirectoriesChanged() to re-sync any previously blocked publishers. - skill-config-remove-allowed-origin: exact-match removal. - All state-returning tools now include allowedOrigins alongside allowedOrgs / allowedUsers. - Skill-count computation in getDirectoriesWithCounts() now matches well-known directories by source.origin so the per-directory count is correct. UI (mcp-app.ts/.html/.css): - New "Well-Known Allowed Origins" section with add/remove buttons. - Add Origin modal + Confirm Remove Origin modal mirror the org ones. - ConfigState gains allowedOrigins; render(), updateState() handle it. - .remove-origin-btn shares CSS with .remove-org-btn. Without this, users had to edit ~/.skilljack/config.json or set WELL_KNOWN_ALLOWED_ORIGINS by hand. With it, the well-known consumer flow is fully manageable from the Claude Desktop UI. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 894b323 commit 02fe8e7

4 files changed

Lines changed: 399 additions & 2 deletions

File tree

src/skill-config-tool.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ import {
2828
getGitHubAllowedUsers,
2929
addGitHubAllowedOrg,
3030
removeGitHubAllowedOrg,
31+
getWellKnownAllowedOrigins,
32+
addWellKnownAllowedOrigin,
33+
removeWellKnownAllowedOrigin,
3134
getStaticModeFromConfig,
3235
setStaticModeInConfig,
3336
} from "./skill-config.js";
3437
import { SkillState } from "./skill-tool.js";
3538
import { isGitHubUrl, parseGitHubUrl } from "./github-config.js";
39+
import { isWellKnownUrl, parseWellKnownUrl, getWellKnownConfig } from "./well-known-config.js";
3640

3741
/**
3842
* Resource URI for the skill-config UI.
@@ -121,6 +125,21 @@ export function registerSkillConfigTool(
121125
} catch {
122126
// Invalid GitHub URL, count stays 0
123127
}
128+
} else if (isWellKnownUrl(dir.path)) {
129+
// For well-known directories, match by origin from skill source
130+
try {
131+
const spec = parseWellKnownUrl(dir.path, getWellKnownConfig().allowHttp);
132+
for (const skill of skillState.skillMap.values()) {
133+
if (
134+
skill.source.type === "well-known" &&
135+
skill.source.origin?.toLowerCase() === spec.origin.toLowerCase()
136+
) {
137+
count++;
138+
}
139+
}
140+
} catch {
141+
// Invalid well-known URL, count stays 0
142+
}
124143
} else {
125144
// For local directories, match by path prefix
126145
for (const skill of skillState.skillMap.values()) {
@@ -161,6 +180,7 @@ export function registerSkillConfigTool(
161180
staticMode: z.boolean(),
162181
allowedOrgs: z.array(z.string()),
163182
allowedUsers: z.array(z.string()),
183+
allowedOrigins: z.array(z.string()),
164184
},
165185
_meta: { ui: { resourceUri: RESOURCE_URI } },
166186
annotations: {
@@ -188,6 +208,7 @@ export function registerSkillConfigTool(
188208
staticMode: getStaticModeFromConfig(),
189209
allowedOrgs: getGitHubAllowedOrgs(),
190210
allowedUsers: getGitHubAllowedUsers(),
211+
allowedOrigins: getWellKnownAllowedOrigins(),
191212
},
192213
};
193214
}
@@ -251,6 +272,7 @@ export function registerSkillConfigTool(
251272
staticMode: getStaticModeFromConfig(),
252273
allowedOrgs: getGitHubAllowedOrgs(),
253274
allowedUsers: getGitHubAllowedUsers(),
275+
allowedOrigins: getWellKnownAllowedOrigins(),
254276
},
255277
};
256278
} catch (error) {
@@ -330,6 +352,7 @@ export function registerSkillConfigTool(
330352
staticMode: getStaticModeFromConfig(),
331353
allowedOrgs: getGitHubAllowedOrgs(),
332354
allowedUsers: getGitHubAllowedUsers(),
355+
allowedOrigins: getWellKnownAllowedOrigins(),
333356
},
334357
};
335358
} catch (error) {
@@ -403,6 +426,7 @@ export function registerSkillConfigTool(
403426
staticMode: getStaticModeFromConfig(),
404427
allowedOrgs: getGitHubAllowedOrgs(),
405428
allowedUsers: getGitHubAllowedUsers(),
429+
allowedOrigins: getWellKnownAllowedOrigins(),
406430
},
407431
};
408432
} catch (error) {
@@ -477,6 +501,7 @@ export function registerSkillConfigTool(
477501
staticMode: getStaticModeFromConfig(),
478502
allowedOrgs: getGitHubAllowedOrgs(),
479503
allowedUsers: getGitHubAllowedUsers(),
504+
allowedOrigins: getWellKnownAllowedOrigins(),
480505
},
481506
};
482507
} catch (error) {
@@ -499,6 +524,159 @@ export function registerSkillConfigTool(
499524
}
500525
);
501526

527+
// Add allowed well-known origin tool (UI-only)
528+
registerAppTool(
529+
server,
530+
"skill-config-add-allowed-origin",
531+
{
532+
title: "Add Allowed Well-Known Origin",
533+
description:
534+
"Add an HTTPS origin (e.g. https://example.com) to the allowed list for well-known publishers.",
535+
inputSchema: {
536+
origin: z
537+
.string()
538+
.describe(
539+
"Origin or full URL (e.g. https://example.com or https://example.com/.well-known/agent-skills/). Only the scheme + host[:port] is stored."
540+
),
541+
},
542+
outputSchema: {
543+
success: z.boolean(),
544+
allowedOrigins: z.array(z.string()),
545+
error: z.string().optional(),
546+
},
547+
_meta: {
548+
ui: {
549+
resourceUri: RESOURCE_URI,
550+
visibility: ["app"],
551+
},
552+
},
553+
annotations: {
554+
readOnlyHint: false,
555+
destructiveHint: false,
556+
idempotentHint: true,
557+
openWorldHint: false,
558+
},
559+
},
560+
async (args): Promise<CallToolResult> => {
561+
const { origin: rawInput } = args as { origin: string };
562+
563+
try {
564+
// Normalize: accept full URLs and reduce to origin only.
565+
let normalizedOrigin: string;
566+
try {
567+
normalizedOrigin = new URL(rawInput).origin;
568+
} catch {
569+
throw new Error(
570+
`Invalid origin: "${rawInput}". Use the form https://example.com[:port].`
571+
);
572+
}
573+
if (
574+
!normalizedOrigin.startsWith("https://") &&
575+
!(normalizedOrigin.startsWith("http://") && getWellKnownConfig().allowHttp)
576+
) {
577+
throw new Error(
578+
`Origin must use https:// (or http:// with WELL_KNOWN_ALLOW_HTTP=1 for local dev).`
579+
);
580+
}
581+
582+
addWellKnownAllowedOrigin(normalizedOrigin);
583+
await onDirectoriesChanged(); // Trigger well-known resync
584+
585+
const directories = getDirectoriesWithCounts();
586+
return {
587+
content: [
588+
{ type: "text", text: `Added allowed origin: ${normalizedOrigin}` },
589+
],
590+
structuredContent: {
591+
success: true,
592+
directories,
593+
activeSource: getConfigState().activeSource,
594+
isOverridden: getConfigState().isOverridden,
595+
staticMode: getStaticModeFromConfig(),
596+
allowedOrgs: getGitHubAllowedOrgs(),
597+
allowedUsers: getGitHubAllowedUsers(),
598+
allowedOrigins: getWellKnownAllowedOrigins(),
599+
},
600+
};
601+
} catch (error) {
602+
const message = error instanceof Error ? error.message : String(error);
603+
return {
604+
content: [{ type: "text", text: `Failed to add allowed origin: ${message}` }],
605+
structuredContent: {
606+
success: false,
607+
allowedOrigins: getWellKnownAllowedOrigins(),
608+
error: message,
609+
},
610+
isError: true,
611+
};
612+
}
613+
}
614+
);
615+
616+
// Remove allowed well-known origin tool (UI-only)
617+
registerAppTool(
618+
server,
619+
"skill-config-remove-allowed-origin",
620+
{
621+
title: "Remove Allowed Well-Known Origin",
622+
description: "Remove an HTTPS origin from the well-known allowed list.",
623+
inputSchema: {
624+
origin: z.string().describe("Origin to remove (exact match)"),
625+
},
626+
outputSchema: {
627+
success: z.boolean(),
628+
allowedOrigins: z.array(z.string()),
629+
error: z.string().optional(),
630+
},
631+
_meta: {
632+
ui: {
633+
resourceUri: RESOURCE_URI,
634+
visibility: ["app"],
635+
},
636+
},
637+
annotations: {
638+
readOnlyHint: false,
639+
destructiveHint: true,
640+
idempotentHint: true,
641+
openWorldHint: false,
642+
},
643+
},
644+
async (args): Promise<CallToolResult> => {
645+
const { origin } = args as { origin: string };
646+
647+
try {
648+
removeWellKnownAllowedOrigin(origin);
649+
await onDirectoriesChanged(); // Trigger well-known resync
650+
651+
const directories = getDirectoriesWithCounts();
652+
return {
653+
content: [{ type: "text", text: `Removed allowed origin: ${origin}` }],
654+
structuredContent: {
655+
success: true,
656+
directories,
657+
activeSource: getConfigState().activeSource,
658+
isOverridden: getConfigState().isOverridden,
659+
staticMode: getStaticModeFromConfig(),
660+
allowedOrgs: getGitHubAllowedOrgs(),
661+
allowedUsers: getGitHubAllowedUsers(),
662+
allowedOrigins: getWellKnownAllowedOrigins(),
663+
},
664+
};
665+
} catch (error) {
666+
const message = error instanceof Error ? error.message : String(error);
667+
return {
668+
content: [{ type: "text", text: `Failed to remove allowed origin: ${message}` }],
669+
structuredContent: {
670+
success: false,
671+
allowedOrigins: getWellKnownAllowedOrigins(),
672+
error: message,
673+
},
674+
isError: true,
675+
};
676+
}
677+
}
678+
);
679+
502680
// Set static mode tool (UI-only, hidden from model)
503681
registerAppTool(
504682
server,

src/ui/mcp-app.css

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,8 @@ h1 {
314314
font-family: 'Fira Code', 'Consolas', monospace;
315315
}
316316

317-
.remove-org-btn {
317+
.remove-org-btn,
318+
.remove-origin-btn {
318319
padding: 2px 8px;
319320
background: transparent;
320321
color: var(--text-secondary);
@@ -324,7 +325,8 @@ h1 {
324325
cursor: pointer;
325326
}
326327

327-
.remove-org-btn:hover {
328+
.remove-org-btn:hover,
329+
.remove-origin-btn:hover {
328330
background: var(--error);
329331
border-color: var(--error);
330332
color: white;

src/ui/mcp-app.html

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ <h2>GitHub Allowed Orgs</h2>
5050
<div class="empty-state small">No allowed orgs configured. GitHub repos are blocked until an org/user is added.</div>
5151
</div>
5252

53+
<!-- Well-Known Allowed Origins Section -->
54+
<div class="section-header">
55+
<div>
56+
<h2>Well-Known Allowed Origins</h2>
57+
<div class="section-hint">HTTPS origins allowed to publish skills via <code>/.well-known/agent-skills/</code></div>
58+
</div>
59+
<button class="add-btn add-origin-btn" id="add-origin-btn">
60+
<span>+</span> Add Origin
61+
</button>
62+
</div>
63+
64+
<div id="allowed-origins-list" class="allowed-list">
65+
<div class="empty-state small">No allowed origins configured. Well-known publishers are blocked until an origin is added.</div>
66+
</div>
67+
5368
<!-- Settings Section -->
5469
<div class="settings-section">
5570
<h2>Settings</h2>
@@ -86,6 +101,53 @@ <h2>Remove Allowed Org</h2>
86101
</div>
87102
</div>
88103

104+
<!-- Confirm Remove Origin Modal -->
105+
<div class="modal-overlay" id="confirm-remove-origin-modal">
106+
<div class="modal modal-small">
107+
<div class="modal-header">
108+
<h2>Remove Allowed Origin</h2>
109+
<button class="modal-close" id="confirm-remove-origin-close">&times;</button>
110+
</div>
111+
<div class="modal-body">
112+
<p>Remove this origin from the allowed list?</p>
113+
<p class="confirm-path" id="confirm-remove-origin-name"></p>
114+
</div>
115+
<div class="modal-footer">
116+
<button class="btn btn-secondary" id="confirm-remove-origin-cancel">Cancel</button>
117+
<button class="btn btn-danger" id="confirm-remove-origin-btn">Remove</button>
118+
</div>
119+
</div>
120+
</div>
121+
122+
<!-- Add Origin Modal -->
123+
<div class="modal-overlay" id="add-origin-modal">
124+
<div class="modal">
125+
<div class="modal-header">
126+
<h2>Add Allowed Well-Known Origin</h2>
127+
<button class="modal-close" data-modal="add-origin-modal">&times;</button>
128+
</div>
129+
<div class="modal-body">
130+
<div class="form-group">
131+
<label for="origin-name">Origin</label>
132+
<input
133+
type="text"
134+
id="origin-name"
135+
placeholder="https://example.com"
136+
>
137+
<div class="form-hint">
138+
HTTPS origin (scheme + host + optional port) of a well-known publisher.
139+
<br>You can paste a full URL — only the origin is stored.
140+
<br>Examples: <code>https://example.com</code>, <code>https://skills.acme.org:8443</code>
141+
</div>
142+
</div>
143+
</div>
144+
<div class="modal-footer">
145+
<button class="btn btn-secondary" data-modal="add-origin-modal">Cancel</button>
146+
<button class="btn btn-primary" id="add-origin-submit-btn">Add Origin</button>
147+
</div>
148+
</div>
149+
</div>
150+
89151
<!-- Add Org Modal -->
90152
<div class="modal-overlay" id="add-org-modal">
91153
<div class="modal">

0 commit comments

Comments
 (0)