feat: require explicit opt-in for Claude Code use
This commit is contained in:
@@ -7,7 +7,7 @@ import {
|
||||
type TeachingAgent,
|
||||
} from "../agent/teacher-agent.ts";
|
||||
import { piEngineForSpec, type PiEngine } from "../agent/models.ts";
|
||||
import { connectionBackgroundAllowed, connectionScopeCurrent, isSubscriptionProvider } from "../agent/subscription-connections.ts";
|
||||
import { connectionBackgroundAllowed as subscriptionBackgroundAllowed, connectionScopeCurrent, isSubscriptionProvider } from "../agent/subscription-connections.ts";
|
||||
import { nativeSubscriptionError } from "../agent/subscription-stream.ts";
|
||||
import { chatGPTStreamError } from "../agent/chatgpt-stream.ts";
|
||||
import { SubscriptionRequestCycle, type SubscriptionDispatchLifecycle } from "../agent/subscription-request-cycle.ts";
|
||||
@@ -33,6 +33,7 @@ import { providerEventNote } from "../agent/provider-events.ts";
|
||||
import { effectiveSettings, stores } from "./stores.ts";
|
||||
import { searchAvailable, searchProvider } from "./search.ts";
|
||||
import { providerKeyPresent } from "./keys.ts";
|
||||
import { CLAUDE_CODE_PROVIDER } from "../core/contracts/claude-code.ts";
|
||||
import {
|
||||
extractionEnabled,
|
||||
graphToolsEnabled,
|
||||
@@ -80,6 +81,12 @@ import type {CallUsage} from "../core/contracts.ts";
|
||||
* for write-ahead persistence, turn liveness, and the turn services.
|
||||
*/
|
||||
|
||||
/** Fresh check only at new background-run admission; never cancel a run
|
||||
* already in progress when the opt-in is switched off. */
|
||||
function connectionBackgroundAllowed(provider: string): boolean {
|
||||
return subscriptionBackgroundAllowed(provider) && (provider !== CLAUDE_CODE_PROVIDER || providerKeyPresent(provider));
|
||||
}
|
||||
|
||||
function settlementErrorText(error: unknown): string {
|
||||
return error instanceof TurnPersistenceError ? error.message : "An error occurred.";
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getClaudeBinary } from "../agent/claude-code/binary.ts";
|
||||
import { settingsStore } from "../store/settings.ts";
|
||||
|
||||
/** Fresh admission state, not a process-wide cache. Existing runs retain
|
||||
* their engines; only new chat/job/extraction/compaction admissions consult it. */
|
||||
export function claudeCodeEnabled(): boolean {
|
||||
return settingsStore.readSettings()?.claudeCode?.enabled === true;
|
||||
}
|
||||
|
||||
/** Installation/opt-in availability only, never authenticated readiness. */
|
||||
export function claudeCodeAvailable(): boolean {
|
||||
return claudeCodeEnabled() && getClaudeBinary() !== null;
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
import { inspectClaudeInstallation } from "../agent/claude-code/installation.ts";
|
||||
import { initAuthEnv } from "./keys.ts";
|
||||
import { subscriptionRequestOrigin } from "./web-subscription-http.ts";
|
||||
import { claudeCodeEnabled } from "./claude-code-config.ts";
|
||||
|
||||
/** The Settings status seam: initialize provenance before any child env is copied. */
|
||||
export async function claudeCodeInstallationStatus() {
|
||||
@@ -13,6 +14,7 @@ export async function claudeCodeInstallationStatus() {
|
||||
export async function claudeCodeInstallationResponse(
|
||||
request: Request,
|
||||
inspect = claudeCodeInstallationStatus,
|
||||
enabled = claudeCodeEnabled,
|
||||
): Promise<Response> {
|
||||
const origin = subscriptionRequestOrigin(request);
|
||||
const supplied = request.headers.get("origin");
|
||||
@@ -23,5 +25,6 @@ export async function claudeCodeInstallationResponse(
|
||||
|| (supplied !== null && supplied !== origin)) {
|
||||
return Response.json({ error: "Claude Code checks require a same-origin LearnHarness request." }, { status: 403, headers });
|
||||
}
|
||||
if (!enabled()) return Response.json({ error: "Claude Code is disabled." }, { status: 403, headers });
|
||||
return Response.json(await inspect(), { headers });
|
||||
}
|
||||
|
||||
+4
-3
@@ -53,7 +53,7 @@ import {
|
||||
} from "../core/auth-keys.ts";
|
||||
import { authStore } from "../store/auth.ts";
|
||||
import { isSubscriptionProvider, subscriptionConnected } from "../agent/subscription-connections.ts";
|
||||
import { getClaudeBinary } from "../agent/claude-code/binary.ts";
|
||||
import { claudeCodeAvailable } from "./claude-code-config.ts";
|
||||
import { CLAUDE_CODE_PROVIDER } from "../agent/claude-code/constants.ts";
|
||||
|
||||
// Shared HMR-stable provenance lets external CLIs exclude file-only injections
|
||||
@@ -189,8 +189,9 @@ export function keyValue(id: string): string | undefined {
|
||||
* (gate by what would run). Providers absent from AUTH_PROVIDER_KEYS take
|
||||
* no key at all: nothing to gate, present by definition. */
|
||||
export function providerKeyPresent(provider: string): boolean {
|
||||
// Presence only, not authentication readiness. Launch performs the version gate.
|
||||
if (provider === CLAUDE_CODE_PROVIDER) return getClaudeBinary() !== null;
|
||||
// Explicit opt-in + installation only, never authentication readiness.
|
||||
// Consulted at admission, not inside already-running agent loops.
|
||||
if (provider === CLAUDE_CODE_PROVIDER) return claudeCodeAvailable();
|
||||
if (isSubscriptionProvider(provider)) return subscriptionConnected(provider);
|
||||
const info = AUTH_PROVIDER_KEYS.find((p) => p.id === provider);
|
||||
return info ? keyValue(info.envVar) !== undefined : true;
|
||||
|
||||
@@ -31,6 +31,7 @@ export async function refreshConnectionCatalog(provider?: string): Promise<void>
|
||||
]);
|
||||
}
|
||||
function connectionModel(provider: string, id: string): Model<Api> | undefined {
|
||||
if (provider === CLAUDE_CODE_PROVIDER && !claudeCodeAvailable()) return undefined;
|
||||
return provider === "chatgpt" ? chatGPTModel(id) : nativeSubscriptionId(provider) ? nativeSubscriptionModel(provider, id) : piModels().getModel(provider, id);
|
||||
}
|
||||
function connectionProviderIds(): string[] { return [...piModels().getProviders().map((p) => p.id), "chatgpt", ...NATIVE_SUBSCRIPTIONS.map((p) => p.logicalId)]; }
|
||||
@@ -48,7 +49,7 @@ import type {
|
||||
} from "../core/contracts/models.ts";
|
||||
import type { EffectiveModelSpec, EffectiveSlot } from "../core/settings-resolution.ts";
|
||||
import { AUTH_PROVIDER_KEYS } from "../core/auth-keys.ts";
|
||||
import { getClaudeBinary } from "../agent/claude-code/binary.ts";
|
||||
import { claudeCodeAvailable } from "./claude-code-config.ts";
|
||||
import { CLAUDE_CODE_PROVIDER } from "../agent/claude-code/constants.ts";
|
||||
import { BACKGROUND_MODEL_ROLES, MODEL_SLOTS, type BackgroundModelRole, type ModelSlotName, type ModelsSettings } from "../core/settings.ts";
|
||||
|
||||
@@ -143,7 +144,7 @@ export function catalogProviders(): CatalogProvider[] {
|
||||
keyPresent: keyValue(p.envVar) !== undefined,
|
||||
})).concat([
|
||||
{ id: "chatgpt", envVar: "", keyPresent: chatGPTConnected() },
|
||||
{ id: CLAUDE_CODE_PROVIDER, envVar: "", keyPresent: getClaudeBinary() !== null },
|
||||
{ id: CLAUDE_CODE_PROVIDER, envVar: "", keyPresent: claudeCodeAvailable() },
|
||||
...NATIVE_SUBSCRIPTIONS.map((p) => ({ id: p.logicalId, envVar: "", keyPresent: nativeSubscriptionConnected(p.providerId) })),
|
||||
]).sort((a, b) => a.id.localeCompare(b.id));
|
||||
}
|
||||
|
||||
@@ -91,6 +91,15 @@ export function parseGeneralSettingsBody(body: unknown): SettingsBodyResult<Gene
|
||||
return { ok: true, settings };
|
||||
}
|
||||
|
||||
/** claudeCode: explicit app-wide opt-in. Empty section resets to off. */
|
||||
export function parseClaudeCodeSettingsBody(body: unknown): SettingsBodyResult<{ enabled?: boolean }> {
|
||||
const raw = asRecord(body);
|
||||
if (!raw) return { ok: false, error: "Invalid JSON body" };
|
||||
if (raw.enabled === undefined) return { ok: true, settings: {} };
|
||||
if (typeof raw.enabled !== "boolean") return { ok: false, error: "enabled must be a boolean" };
|
||||
return { ok: true, settings: { enabled: raw.enabled } };
|
||||
}
|
||||
|
||||
/** agent.models: only the six known background roles; each override's
|
||||
* fields mirror ChatSettings (model pair + independent thinkingLevel). */
|
||||
function parseRoleOverride(value: unknown, role: string): { ok: true; override: RoleModelOverride } | { ok: false; error: string } {
|
||||
@@ -397,6 +406,7 @@ export const SETTINGS_SECTIONS = {
|
||||
compaction: parseCompactionSettingsBody,
|
||||
models: parseModelsSettingsBody,
|
||||
dictation: parseDictationSettingsBody,
|
||||
claudeCode: parseClaudeCodeSettingsBody,
|
||||
} as const;
|
||||
|
||||
export type SettingsSectionName = keyof typeof SETTINGS_SECTIONS;
|
||||
|
||||
Reference in New Issue
Block a user