= {
hermes: "Hermes",
kiro: "Kiro",
omp: "Oh My Pi",
- dsh: "DSH",
+ dsh: "DeepSeek",
};
/** Get the display name for an agent (e.g. "claude" → "Claude Code"). */
diff --git a/src/pages/__tests__/audit-utils.test.ts b/src/pages/__tests__/audit-utils.test.ts
index d3a446ec..6232e05b 100644
--- a/src/pages/__tests__/audit-utils.test.ts
+++ b/src/pages/__tests__/audit-utils.test.ts
@@ -166,4 +166,12 @@ describe("AUDIT_RULES", () => {
expect(valid.has(rule.severity)).toBe(true);
}
});
+
+ it("includes dsh-js-env-no-fallback scoped to MCP at Medium", () => {
+ const rule = AUDIT_RULES.find((r) => r.id === "dsh-js-env-no-fallback");
+ expect(rule).toBeDefined();
+ expect(rule?.kinds).toEqual(["mcp"]);
+ expect(rule?.severity).toBe("Medium");
+ expect(rule?.deduction).toBe(8);
+ });
});
diff --git a/src/pages/audit-utils.ts b/src/pages/audit-utils.ts
index ecf06a73..bf20edb1 100644
--- a/src/pages/audit-utils.ts
+++ b/src/pages/audit-utils.ts
@@ -2,6 +2,11 @@ import type { AuditFinding, ExtensionKind, Severity } from "@/lib/types";
type Kind = ExtensionKind;
+/** kebab-case rule id → camelCase i18n key (e.g. "prompt-injection" → "promptInjection") */
+export function ruleI18nKey(id: string): string {
+ return id.replace(/-(\w)/g, (_, c: string) => c.toUpperCase());
+}
+
export const AUDIT_RULES = [
{
id: "prompt-injection",
@@ -93,6 +98,15 @@ export const AUDIT_RULES = [
"Frontmatter uses a camelCase invocation key that DeepSeek Harness silently rejects, dropping the whole skill",
kinds: ["skill"] as Kind[],
},
+ {
+ id: "dsh-js-env-no-fallback",
+ label: "dsh !!js Env Without Fallback",
+ severity: "Medium" as Severity,
+ deduction: 8,
+ description:
+ 'A !!js config expression reads process.env without a ??/|| fallback — if the variable is unset the whole dsh boot fails at mount time; give the expression a default, e.g. process.env.X ?? "".',
+ kinds: ["mcp"] as Kind[],
+ },
{
id: "cli-credential-storage",
label: "CLI Credential Storage",
diff --git a/src/pages/audit.tsx b/src/pages/audit.tsx
index 9f33710a..a6224966 100644
--- a/src/pages/audit.tsx
+++ b/src/pages/audit.tsx
@@ -32,16 +32,12 @@ import {
AUDIT_RULES,
type GroupedResult,
maxSeverity,
+ ruleI18nKey,
rulesForKind,
severityBadgeClass,
severityIconColor,
} from "./audit-utils";
-/** kebab-case rule id → camelCase i18n key (e.g. "prompt-injection" → "promptInjection") */
-function ruleI18nKey(id: string): string {
- return id.replace(/-(\w)/g, (_, c: string) => c.toUpperCase());
-}
-
function IndeterminateBar({ className = "" }: { className?: string }) {
return (
{
});
});
+// ---------------------------------------------------------------------------
+// vendor baseline: built-in source group + hide toggle
+// ---------------------------------------------------------------------------
+
+describe("getCachedFiltered and the vendor baseline", () => {
+ // dsh ships ~130 plugin rows against ~20 from every other agent combined,
+ // spread over three bundles. Those three are one provenance from the user's
+ // side, and the whole set is what the hide toggle removes.
+ const byAgent = {
+ dsh: ["@deepseek-ai/dsh-base", "@deepseek-ai/dsh-web-app"],
+ };
+ const baseline: Extension = {
+ ...baseExt,
+ id: "b1",
+ kind: "plugin",
+ name: "timer",
+ pack: "@deepseek-ai/dsh-base",
+ };
+ const baseline2: Extension = {
+ ...baseExt,
+ id: "b2",
+ kind: "plugin",
+ name: "webserver",
+ pack: "@deepseek-ai/dsh-web-app",
+ };
+ const thirdParty: Extension = {
+ ...baseExt,
+ id: "t",
+ kind: "plugin",
+ name: "dsh-market",
+ pack: "dshmarket",
+ };
+ const unattributed: Extension = { ...baseExt, id: "u", pack: null };
+ const groups = buildGroups([baseline, baseline2, thirdParty, unattributed]);
+ const all: ScopeValue = { type: "all" };
+ const ids = (r: GroupedExtension[]) => r.map((g) => g.instances[0].id).sort();
+
+ it("collapses an agent's bundles into one selectable source", () => {
+ const result = getCachedFiltered(
+ groups,
+ null,
+ null,
+ `${BUILTIN_PACK_PREFIX}dsh`,
+ null,
+ "",
+ all,
+ byAgent,
+ );
+ expect(ids(result)).toEqual(["b1", "b2"]);
+ });
+
+ it("hides the whole baseline and keeps everything the user added", () => {
+ const result = getCachedFiltered(
+ groups,
+ null,
+ null,
+ null,
+ null,
+ "",
+ all,
+ byAgent,
+ true,
+ );
+ // An unattributed row is the user's too — a hand-written skill has no pack.
+ expect(ids(result)).toEqual(["t", "u"]);
+ });
+
+ it("shows the baseline by default", () => {
+ const result = getCachedFiltered(
+ groups,
+ null,
+ null,
+ null,
+ null,
+ "",
+ all,
+ byAgent,
+ );
+ expect(result).toHaveLength(4);
+ });
+
+ it("hiding wins over an explicit built-in selection", () => {
+ // The two contradict; going empty is honest, silently showing rows the
+ // user asked to hide is not.
+ const result = getCachedFiltered(
+ groups,
+ null,
+ null,
+ `${BUILTIN_PACK_PREFIX}dsh`,
+ null,
+ "",
+ all,
+ byAgent,
+ true,
+ );
+ expect(result).toEqual([]);
+ });
+
+ it("still matches a real pack exactly", () => {
+ const result = getCachedFiltered(
+ groups,
+ null,
+ null,
+ "dshmarket",
+ null,
+ "",
+ all,
+ byAgent,
+ );
+ expect(ids(result)).toEqual(["t"]);
+ });
+});
+
// ---------------------------------------------------------------------------
// getCachedFiltered with scope
// ---------------------------------------------------------------------------
diff --git a/src/stores/__tests__/extension-store.test.ts b/src/stores/__tests__/extension-store.test.ts
new file mode 100644
index 00000000..d9cc9626
--- /dev/null
+++ b/src/stores/__tests__/extension-store.test.ts
@@ -0,0 +1,85 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+import { api } from "@/lib/invoke";
+import type { Extension } from "@/lib/types";
+import { useExtensionStore } from "../extension-store";
+import { toast } from "../toast-store";
+
+vi.mock("@/lib/invoke");
+
+const dshPlugin: Extension = {
+ id: "p1",
+ kind: "plugin",
+ name: "timer",
+ description: "Plugin from profile web, bundle @deepseek-ai/dsh-base",
+ source: { origin: "agent", url: null, version: null, commit_hash: null },
+ agents: ["dsh"],
+ tags: [],
+ pack: "@deepseek-ai/dsh-base",
+ permissions: [],
+ enabled: true,
+ trust_score: null,
+ installed_at: "2026-08-01T00:00:00Z",
+ updated_at: "2026-08-01T00:00:00Z",
+ source_path: null,
+ cli_parent_id: null,
+ cli_meta: null,
+ install_meta: null,
+ scope: { type: "global" },
+};
+
+describe("extension-store confirmDelete", () => {
+ beforeEach(() => {
+ useExtensionStore.setState({ extensions: [], pendingDelete: null });
+ vi.resetAllMocks();
+ });
+
+ // Deletion is optimistic: the row leaves the list and the success toast
+ // fires five seconds BEFORE the request goes out. A refusal that only got
+ // logged left the UI asserting a deletion that never happened — the row
+ // stayed gone until a manual reload, and nothing told the user why.
+ it("puts the rows back and reports why when the backend refuses", async () => {
+ const errorToast = vi.spyOn(toast, "error").mockImplementation(() => {});
+ vi.mocked(api.deleteExtension).mockRejectedValue(
+ '{"kind":"Validation","message":"\'timer\' is a dsh plugin row"}',
+ );
+ // State right after the optimistic removal: gone from the list, parked
+ // in pendingDelete.
+ useExtensionStore.setState({
+ extensions: [],
+ pendingDelete: {
+ ids: new Set(["p1"]),
+ extensions: [dshPlugin],
+ timer: 0 as unknown as ReturnType,
+ },
+ });
+
+ await expect(
+ useExtensionStore.getState().confirmDelete(),
+ ).resolves.toBeUndefined();
+
+ expect(useExtensionStore.getState().extensions).toEqual([dshPlugin]);
+ expect(errorToast).toHaveBeenCalledTimes(1);
+ // The backend's reason has to reach the toast, not a generic failure.
+ expect(errorToast.mock.calls[0][0]).toContain("dsh plugin row");
+ // A failed delete must not leave a rescan running over a stale list.
+ expect(api.scanAndSync).not.toHaveBeenCalled();
+ });
+
+ it("does not restore anything when the delete succeeds", async () => {
+ vi.mocked(api.deleteExtension).mockResolvedValue(undefined);
+ vi.mocked(api.scanAndSync).mockResolvedValue(0);
+ vi.mocked(api.listExtensions).mockResolvedValue([]);
+ useExtensionStore.setState({
+ extensions: [],
+ pendingDelete: {
+ ids: new Set(["p1"]),
+ extensions: [dshPlugin],
+ timer: 0 as unknown as ReturnType,
+ },
+ });
+
+ await useExtensionStore.getState().confirmDelete();
+
+ expect(useExtensionStore.getState().extensions).toEqual([]);
+ });
+});
diff --git a/src/stores/extension-helpers.ts b/src/stores/extension-helpers.ts
index fe89d644..241da152 100644
--- a/src/stores/extension-helpers.ts
+++ b/src/stores/extension-helpers.ts
@@ -243,6 +243,12 @@ export function getCachedGroups(extensions: Extension[]): GroupedExtension[] {
return _cachedGroups;
}
+/** Synthetic `packFilter` value: one agent's whole shipped baseline, as a
+ * single "source". dsh ships its baseline across three bundles, and listing
+ * each as its own dropdown row is noise — they are one provenance from the
+ * user's side. Prefixed so it can never collide with a real pack name. */
+export const BUILTIN_PACK_PREFIX = "__hk_builtin__:";
+
export function getCachedFiltered(
groups: GroupedExtension[],
kindFilter: ExtensionKind | null,
@@ -251,6 +257,15 @@ export function getCachedFiltered(
tagFilter: string | null,
searchQuery: string,
scope: ScopeValue,
+ /** `agent name -> packs that ship with it`, from
+ * `capabilities.vendor_baseline_packs`. Empty for every agent whose
+ * baseline is compiled in and so never appears as an extension. */
+ vendorBaselineByAgent: Record = {},
+ /** Drop every shipped-baseline row. The list is about what the user
+ * configured, and an agent that ships its own internals as extensions
+ * buries that — dsh contributes ~130 rows against ~20 from every other
+ * agent combined. Off by default: the baseline stays visible unless asked. */
+ hideVendorBaseline = false,
): GroupedExtension[] {
// Memoize: skip recomputation if inputs haven't changed
const scopeKeyForCache =
@@ -259,7 +274,8 @@ export function getCachedFiltered(
: scope.type === "global"
? "global"
: `project:${scope.path}`;
- const key = `${groups.length}|${kindFilter}|${agentFilter}|${packFilter}|${tagFilter}|${searchQuery}|${scopeKeyForCache}`;
+ const shippedPacks = new Set(Object.values(vendorBaselineByAgent).flat());
+ const key = `${groups.length}|${kindFilter}|${agentFilter}|${packFilter}|${tagFilter}|${searchQuery}|${scopeKeyForCache}|${[...shippedPacks].join(",")}|${hideVendorBaseline}`;
if (key === _cachedFilterKey && groups === _cachedFilterGroupsRef) {
return _cachedFiltered;
}
@@ -275,7 +291,17 @@ export function getCachedFiltered(
agentsInScope(g, scope).includes(agentFilter),
);
}
- if (packFilter) {
+ // Applied before packFilter so "hide built-ins" and an explicit built-in
+ // source selection can't contradict each other on screen: the toggle wins
+ // and the list goes empty, which is the honest answer.
+ if (hideVendorBaseline && shippedPacks.size > 0) {
+ result = result.filter((g) => !g.pack || !shippedPacks.has(g.pack));
+ }
+ if (packFilter?.startsWith(BUILTIN_PACK_PREFIX)) {
+ const agent = packFilter.slice(BUILTIN_PACK_PREFIX.length);
+ const packs = new Set(vendorBaselineByAgent[agent] ?? []);
+ result = result.filter((g) => !!g.pack && packs.has(g.pack));
+ } else if (packFilter) {
result = result.filter((g) => g.pack === packFilter);
}
if (tagFilter) {
diff --git a/src/stores/extension-store.ts b/src/stores/extension-store.ts
index ea8607b9..3516e35e 100644
--- a/src/stores/extension-store.ts
+++ b/src/stores/extension-store.ts
@@ -1,4 +1,5 @@
import { create } from "zustand";
+import { parseError } from "@/lib/error-types";
import i18n from "@/lib/i18n";
import { api } from "@/lib/invoke";
import type {
@@ -9,6 +10,7 @@ import type {
NewRepoSkill,
UpdateStatus,
} from "@/lib/types";
+import { useAgentStore } from "./agent-store";
import {
expandGroupKeys,
findCliChildren,
@@ -20,6 +22,27 @@ import { toast } from "./toast-store";
export { buildGroups } from "./extension-helpers";
+/**
+ * Run a pending delete's backend calls, reporting the first failure instead of
+ * throwing.
+ *
+ * Deletion is optimistic: the rows leave the list and the success toast fires
+ * the moment the user confirms, five seconds BEFORE the request goes out. So a
+ * rejection that is merely logged (or, worse, dropped as an unhandled rejection
+ * from a timer callback) leaves the UI asserting a deletion that never
+ * happened — the caller must put the rows back and say why. A backend that
+ * refuses on purpose (a dsh plugin row, which names a package HarnessKit does
+ * not own) is the routine case, but so is any read-only or locked path.
+ */
+async function runPendingDeletes(ids: Iterable): Promise {
+ try {
+ await Promise.all([...ids].map((id) => api.deleteExtension(id)));
+ return null;
+ } catch (e) {
+ return e;
+ }
+}
+
const MIN_CHECK_UPDATES_VISIBLE_MS = 600;
interface PendingDelete {
@@ -44,6 +67,10 @@ interface ExtensionState {
allTags: string[];
tagFilter: string | null;
packFilter: string | null;
+ /** Hide rows whose pack ships with its agent. Off by default — the
+ * baseline is shown like any other extension unless the user asks. */
+ hideVendorBaseline: boolean;
+ setHideVendorBaseline: (hide: boolean) => void;
allPacks: string[];
pendingDelete: PendingDelete | null;
tableSorting: { id: string; desc: boolean }[];
@@ -109,6 +136,7 @@ export const useExtensionStore = create((set, get) => ({
allTags: [],
tagFilter: null,
packFilter: null,
+ hideVendorBaseline: false,
allPacks: [],
pendingDelete: null,
checkingUpdates: false,
@@ -193,6 +221,9 @@ export const useExtensionStore = create((set, get) => ({
setPackFilter(pack) {
set({ packFilter: pack });
},
+ setHideVendorBaseline(hide) {
+ set({ hideVendorBaseline: hide });
+ },
async fetchTags() {
const allTags = await api.getAllTags();
@@ -351,7 +382,17 @@ export const useExtensionStore = create((set, get) => ({
if (!pending) return;
clearTimeout(pending.timer);
set({ pendingDelete: null });
- await Promise.all([...pending.ids].map((id) => api.deleteExtension(id)));
+ const failure = await runPendingDeletes(pending.ids);
+ if (failure) {
+ set((s) => ({ extensions: [...s.extensions, ...pending.extensions] }));
+ toast.error(
+ i18n.t("extensions:detail.deleteFailedReason", {
+ name: pending.extensions[0]?.name ?? "",
+ msg: parseError(failure).message,
+ }),
+ );
+ return;
+ }
// Remove CLI binary only on full uninstall (CLI parent is in the set, not just children)
for (const ext of pending.extensions) {
if (
@@ -564,14 +605,26 @@ export const useExtensionStore = create((set, get) => ({
const prev = get().pendingDelete;
if (prev) {
clearTimeout(prev.timer);
- try {
- await Promise.all([...prev.ids].map((id) => api.deleteExtension(id)));
- } catch (e) {
- console.error("Failed to finalize previous deletion:", e);
+ const failure = await runPendingDeletes(prev.ids);
+ if (failure) {
+ // Same contract as confirmDelete: the earlier rows were removed
+ // optimistically too, so a refusal has to put them back.
+ set((s) => ({ extensions: [...s.extensions, ...prev.extensions] }));
+ toast.error(
+ i18n.t("extensions:detail.deleteFailedReason", {
+ name: prev.extensions[0]?.name ?? "",
+ msg: parseError(failure).message,
+ }),
+ );
}
}
const timer = setTimeout(() => {
- get().confirmDelete();
+ // confirmDelete reports its own failures; this guards the rescan tail.
+ get()
+ .confirmDelete()
+ .catch((e) => {
+ console.error("Failed to finalize deletion:", e);
+ });
}, 5000);
set({ pendingDelete: { ids, extensions: toDelete, timer } });
},
@@ -592,6 +645,20 @@ export const useExtensionStore = create((set, get) => ({
tagFilter,
searchQuery,
scope,
+ vendorBaselineByAgent(),
+ get().hideVendorBaseline,
);
},
}));
+
+/** `agent name -> packs that ship with it`, as the backend reports it. Read
+ * from the agent store rather than threaded through props so the source
+ * filter, the hide toggle, and the delete gating stay on one list. */
+export function vendorBaselineByAgent(): Record {
+ const out: Record = {};
+ for (const a of useAgentStore.getState().agents) {
+ const packs = a.capabilities?.vendor_baseline_packs ?? [];
+ if (packs.length > 0) out[a.name] = packs;
+ }
+ return out;
+}