Fix hook trust state transitions

This commit is contained in:
murashit 2026-07-13 20:54:40 +09:00
parent 08cd3f624e
commit 3db509a98a
4 changed files with 33 additions and 38 deletions

View file

@ -95,15 +95,15 @@ export async function listHookCatalog(client: AppServerRequestClient, cwd: strin
export async function trustHookItem(client: AppServerRequestClient, hook: HookItem): Promise<void> {
const operation = appServerHookOperationFromHookItem(hook);
await writeHookState(client, operation.key, {
enabled: true,
trusted_hash: operation.currentHash,
});
}
export async function setHookItemEnabled(client: AppServerRequestClient, hook: HookItem, enabled: boolean): Promise<void> {
if (hook.isManaged) throw new Error("Managed hooks cannot be enabled or disabled here.");
const operation = appServerHookOperationFromHookItem(hook);
const state: HookConfigState = operation.trustStatus === "trusted" ? { enabled, trusted_hash: operation.currentHash } : { enabled };
await writeHookState(client, operation.key, state);
if (operation.trustStatus !== "trusted") throw new Error("Trust the current hook definition before enabling it.");
await writeHookState(client, operation.key, { enabled });
}
type HookConfigState = Record<string, string | boolean | null>;

View file

@ -44,27 +44,34 @@ function Hooks({ state }: { state: HookSectionState }): UiNode {
function HookRow({ hook, state }: { hook: HookItem; state: HookSectionState }): UiNode {
const canTrust = !hook.isManaged && (hook.trustStatus === "untrusted" || hook.trustStatus === "modified");
const canToggle = !hook.isManaged && hook.trustStatus === "trusted";
const hookName = firstNonEmptyString(hook.statusMessage, hook.command, hook.matcher, hook.eventName);
const executionStatus =
hook.trustStatus === "trusted" || hook.trustStatus === "managed" ? (hook.enabled ? "enabled" : "disabled") : "inactive";
return (
<SettingRow
className="codex-panel-settings__dynamic-row codex-panel-settings__hook-row"
name={hookName}
desc={`${hook.eventName} · ${hook.matcher ?? "(no matcher)"} · ${hook.trustStatus} · ${hook.enabled ? "enabled" : "disabled"}`}
desc={`${hook.eventName} · ${hook.matcher ?? "(no matcher)"} · ${hook.trustStatus} · ${executionStatus}`}
>
<ObsidianButton
text="Trust"
disabled={state.loading || !canTrust}
onClick={() => {
state.onTrust(hook);
}}
/>
<ObsidianButton
text={hook.enabled ? "Disable" : "Enable"}
disabled={state.loading || hook.isManaged}
onClick={() => {
state.onToggleEnabled(hook, !hook.enabled);
}}
/>
{canTrust ? (
<ObsidianButton
text="Trust"
disabled={state.loading}
onClick={() => {
state.onTrust(hook);
}}
/>
) : null}
{canToggle ? (
<ObsidianButton
text={hook.enabled ? "Disable" : "Enable"}
disabled={state.loading}
onClick={() => {
state.onToggleEnabled(hook, !hook.enabled);
}}
/>
) : null}
</SettingRow>
);
}

View file

@ -180,7 +180,6 @@ describe("app-server catalog adapters", () => {
keyPath: "hooks.state",
value: {
"hook-trust": {
enabled: true,
trusted_hash: "newhash",
},
},
@ -191,7 +190,7 @@ describe("app-server catalog adapters", () => {
});
});
it("preserves trusted hashes when toggling already trusted hooks", async () => {
it("updates hook enablement without rewriting independently managed trust state", async () => {
const client = {
request: vi.fn().mockResolvedValue({}),
} as unknown as AppServerRequestClient;
@ -207,7 +206,6 @@ describe("app-server catalog adapters", () => {
value: {
"hook-enabled": {
enabled: false,
trusted_hash: "trustedhash",
},
},
mergeStrategy: "upsert",
@ -217,7 +215,7 @@ describe("app-server catalog adapters", () => {
});
});
it("does not write trusted hashes when toggling untrusted hook revisions", async () => {
it("rejects enablement changes for untrusted hook revisions", async () => {
const client = {
request: vi.fn().mockResolvedValue({}),
} as unknown as AppServerRequestClient;
@ -226,22 +224,8 @@ describe("app-server catalog adapters", () => {
])[0];
if (!hook) throw new Error("Expected mapped hook");
await setHookItemEnabled(client, hook, true);
expect(client.request).toHaveBeenCalledWith("config/batchWrite", {
edits: [
{
keyPath: "hooks.state",
value: {
"hook-modified": {
enabled: true,
},
},
mergeStrategy: "upsert",
},
],
reloadUserConfig: true,
});
await expect(setHookItemEnabled(client, hook, true)).rejects.toThrow("Trust the current hook definition before enabling it.");
expect(client.request).not.toHaveBeenCalled();
});
});

View file

@ -854,7 +854,11 @@ describe("settings tab", () => {
expect(tab.containerEl.querySelectorAll(".codex-panel-settings__hook-list .codex-panel-settings__hook-row")).toHaveLength(1);
expect(tab.containerEl.querySelectorAll(".codex-panel-settings__archived-list .codex-panel-settings__archived-row")).toHaveLength(1);
expect(tab.containerEl.querySelector(".codex-panel-settings__hook-list")?.textContent).not.toContain("abc123");
expect(tab.containerEl.querySelector(".codex-panel-settings__hook-list")?.textContent).toContain("untrusted · inactive");
expect(tab.containerEl.querySelector(".codex-panel-settings__archived-list")?.textContent).toContain("Archived thread");
expect(buttonTexts(tab)).toContain("Trust");
expect(buttonTexts(tab)).not.toContain("Enable");
expect(buttonTexts(tab)).not.toContain("Disable");
expect(buttonLabels(tab)).toContain("Restore thread");
expect(buttonLabels(tab)).toContain("Delete thread");
});