feat(FN-1830): merge fusion/fn-1830
This commit is contained in:
293
packages/core/src/daemon-token.test.ts
Normal file
293
packages/core/src/daemon-token.test.ts
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||||
|
import { rm } from "node:fs/promises";
|
||||||
|
import { join } from "node:path";
|
||||||
|
import { mkdtempSync } from "node:fs";
|
||||||
|
import { tmpdir } from "node:os";
|
||||||
|
import { GlobalSettingsStore } from "./global-settings.js";
|
||||||
|
import {
|
||||||
|
DaemonTokenManager,
|
||||||
|
DAEMON_TOKEN_PREFIX,
|
||||||
|
DAEMON_TOKEN_HEX_LENGTH,
|
||||||
|
isDaemonTokenFormat,
|
||||||
|
} from "./daemon-token.js";
|
||||||
|
|
||||||
|
function makeTmpDir(): string {
|
||||||
|
return mkdtempSync(join(tmpdir(), "fn-daemon-token-test-"));
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("isDaemonTokenFormat", () => {
|
||||||
|
it("returns true for valid format", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_a1b2c3d4e5f6789012345678abcdef01")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns true for all lowercase hex", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_0123456789abcdef0123456789abcdef")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for missing prefix", () => {
|
||||||
|
expect(isDaemonTokenFormat("a1b2c3d4e5f6789012345678abcdef01")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for wrong prefix", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn__a1b2c3d4e5f6789012345678abcdef01")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for wrong length (too short)", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_a1b2c3d4e5f6789012345678abcdef0")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for wrong length (too long)", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_a1b2c3d4e5f6789012345678abcdef012")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for uppercase hex", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_A1B2C3D4E5F6789012345678ABCDEF01")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for mixed case hex", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_A1b2C3d4E5f6789012345678AbCdEf01")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for empty string", () => {
|
||||||
|
expect(isDaemonTokenFormat("")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for special characters", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_a1b2c3d4e5f6789012345678abcdef0!")).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for prefix only", () => {
|
||||||
|
expect(isDaemonTokenFormat("fn_")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DaemonTokenManager", () => {
|
||||||
|
let dir: string;
|
||||||
|
let store: GlobalSettingsStore;
|
||||||
|
let manager: DaemonTokenManager;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
dir = makeTmpDir();
|
||||||
|
store = new GlobalSettingsStore(dir);
|
||||||
|
manager = new DaemonTokenManager(store);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await rm(dir, { recursive: true, force: true });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("generateToken()", () => {
|
||||||
|
it("generates a token with correct format", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
expect(token).toMatch(/^fn_[0-9a-f]{32}$/);
|
||||||
|
expect(token.startsWith(DAEMON_TOKEN_PREFIX)).toBe(true);
|
||||||
|
expect(token.length).toBe(DAEMON_TOKEN_PREFIX.length + DAEMON_TOKEN_HEX_LENGTH);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores the token in settings", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
const settings = await store.getSettings();
|
||||||
|
|
||||||
|
expect(settings.daemonToken).toBe(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns the generated token", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
expect(typeof token).toBe("string");
|
||||||
|
expect(token.length).toBeGreaterThan(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("throws if token already exists", async () => {
|
||||||
|
await manager.generateToken();
|
||||||
|
|
||||||
|
await expect(manager.generateToken()).rejects.toThrow(
|
||||||
|
"Daemon token already exists. Use rotateToken() to replace it.",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generates unique tokens on each call", async () => {
|
||||||
|
// First, rotate to get an existing token
|
||||||
|
const firstToken = await manager.rotateToken();
|
||||||
|
|
||||||
|
// Rotate again to get a second token
|
||||||
|
const secondToken = await manager.rotateToken();
|
||||||
|
|
||||||
|
expect(firstToken).not.toBe(secondToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getToken()", () => {
|
||||||
|
it("returns undefined when no token", async () => {
|
||||||
|
const token = await manager.getToken();
|
||||||
|
|
||||||
|
expect(token).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns stored token after generation", async () => {
|
||||||
|
const generated = await manager.generateToken();
|
||||||
|
|
||||||
|
const retrieved = await manager.getToken();
|
||||||
|
|
||||||
|
expect(retrieved).toBe(generated);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns stored token after rotation", async () => {
|
||||||
|
await manager.rotateToken();
|
||||||
|
|
||||||
|
const retrieved = await manager.getToken();
|
||||||
|
|
||||||
|
expect(retrieved).toMatch(/^fn_[0-9a-f]{32}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("validateToken()", () => {
|
||||||
|
it("returns true for valid token", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
const isValid = await manager.validateToken(token);
|
||||||
|
|
||||||
|
expect(isValid).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for wrong token", async () => {
|
||||||
|
await manager.generateToken();
|
||||||
|
|
||||||
|
const isValid = await manager.validateToken(
|
||||||
|
"fn_00000000000000000000000000000001",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false when no token stored", async () => {
|
||||||
|
const isValid = await manager.validateToken(
|
||||||
|
"fn_a1b2c3d4e5f6789012345678abcdef01",
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for empty string", async () => {
|
||||||
|
await manager.generateToken();
|
||||||
|
|
||||||
|
const isValid = await manager.validateToken("");
|
||||||
|
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns false for wrong length token", async () => {
|
||||||
|
await manager.generateToken();
|
||||||
|
|
||||||
|
const isValid = await manager.validateToken(
|
||||||
|
"fn_a1b2c3d4e5f6789012345678abcdef0", // one char short
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(isValid).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("handles timing-safe comparison correctly", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
// Valid token should return true
|
||||||
|
expect(await manager.validateToken(token)).toBe(true);
|
||||||
|
|
||||||
|
// Invalid token should return false
|
||||||
|
expect(await manager.validateToken("fn_00000000000000000000000000000001")).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("rotateToken()", () => {
|
||||||
|
it("generates new token replacing old", async () => {
|
||||||
|
const oldToken = await manager.generateToken();
|
||||||
|
|
||||||
|
const newToken = await manager.rotateToken();
|
||||||
|
|
||||||
|
expect(newToken).not.toBe(oldToken);
|
||||||
|
expect(newToken).toMatch(/^fn_[0-9a-f]{32}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("returns different token each call", async () => {
|
||||||
|
const tokens = new Set<string>();
|
||||||
|
|
||||||
|
for (let i = 0; i < 5; i++) {
|
||||||
|
tokens.add(await manager.rotateToken());
|
||||||
|
}
|
||||||
|
|
||||||
|
// All tokens should be unique
|
||||||
|
expect(tokens.size).toBe(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("works when no existing token", async () => {
|
||||||
|
const token = await manager.rotateToken();
|
||||||
|
|
||||||
|
expect(token).toMatch(/^fn_[0-9a-f]{32}$/);
|
||||||
|
expect(await manager.getToken()).toBe(token);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("stores new token after rotation", async () => {
|
||||||
|
await manager.generateToken();
|
||||||
|
|
||||||
|
await manager.rotateToken();
|
||||||
|
|
||||||
|
const stored = await manager.getToken();
|
||||||
|
expect(stored).toMatch(/^fn_[0-9a-f]{32}$/);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("integration: full lifecycle", () => {
|
||||||
|
it("generate → validate → rotate → validate new → old token fails", async () => {
|
||||||
|
// Generate a token
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
// Validate the original token
|
||||||
|
expect(await manager.validateToken(token)).toBe(true);
|
||||||
|
|
||||||
|
// Rotate to get a new token
|
||||||
|
const newToken = await manager.rotateToken();
|
||||||
|
|
||||||
|
// Old token should no longer be valid
|
||||||
|
expect(await manager.validateToken(token)).toBe(false);
|
||||||
|
|
||||||
|
// New token should be valid
|
||||||
|
expect(await manager.validateToken(newToken)).toBe(true);
|
||||||
|
|
||||||
|
// New token should be different from old
|
||||||
|
expect(newToken).not.toBe(token);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("token format specifics", () => {
|
||||||
|
it("generated token has correct prefix", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
expect(token.startsWith(DAEMON_TOKEN_PREFIX)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generated token has exactly 32 lowercase hex chars", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
const hexPart = token.slice(DAEMON_TOKEN_PREFIX.length);
|
||||||
|
expect(hexPart).toMatch(/^[0-9a-f]{32}$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("generated token has correct total length", async () => {
|
||||||
|
const token = await manager.generateToken();
|
||||||
|
|
||||||
|
expect(token.length).toBe(DAEMON_TOKEN_PREFIX.length + DAEMON_TOKEN_HEX_LENGTH);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DAEMON_TOKEN_PREFIX constant", () => {
|
||||||
|
it("is fn_", () => {
|
||||||
|
expect(DAEMON_TOKEN_PREFIX).toBe("fn_");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("DAEMON_TOKEN_HEX_LENGTH constant", () => {
|
||||||
|
it("is 32", () => {
|
||||||
|
expect(DAEMON_TOKEN_HEX_LENGTH).toBe(32);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
123
packages/core/src/daemon-token.ts
Normal file
123
packages/core/src/daemon-token.ts
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
/**
|
||||||
|
* Daemon token management for fn daemon mode authentication.
|
||||||
|
*
|
||||||
|
* Daemon tokens are stored in global settings and used to authenticate
|
||||||
|
* CLI clients connecting to the daemon server.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { randomBytes, timingSafeEqual } from "node:crypto";
|
||||||
|
import { GlobalSettingsStore } from "./global-settings.js";
|
||||||
|
|
||||||
|
/** Prefix for daemon authentication tokens. */
|
||||||
|
export const DAEMON_TOKEN_PREFIX = "fn_";
|
||||||
|
|
||||||
|
/** Number of hex characters in the token body (16 bytes = 32 hex chars). */
|
||||||
|
export const DAEMON_TOKEN_HEX_LENGTH = 32;
|
||||||
|
|
||||||
|
/** Regular expression for validating daemon token format. */
|
||||||
|
const DAEMON_TOKEN_REGEX = /^fn_[0-9a-f]{32}$/;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a string matches the daemon token format (fn_<32 hex chars>).
|
||||||
|
*
|
||||||
|
* @param value - The string to validate
|
||||||
|
* @returns true if the string matches the expected format
|
||||||
|
*/
|
||||||
|
export function isDaemonTokenFormat(value: string): boolean {
|
||||||
|
return DAEMON_TOKEN_REGEX.test(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages daemon authentication token lifecycle: generation, storage, validation, and rotation.
|
||||||
|
*
|
||||||
|
* Tokens are stored in global settings alongside user preferences. This class
|
||||||
|
* provides a clean API for CLI and server components to manage daemon tokens
|
||||||
|
* without directly coupling to GlobalSettingsStore.
|
||||||
|
*/
|
||||||
|
export class DaemonTokenManager {
|
||||||
|
constructor(private readonly settingsStore: GlobalSettingsStore) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new daemon token and store it.
|
||||||
|
*
|
||||||
|
* @returns The generated token string (e.g., "fn_a1b2c3...")
|
||||||
|
* @throws Error if a token already exists. Use rotateToken() to replace.
|
||||||
|
*/
|
||||||
|
async generateToken(): Promise<string> {
|
||||||
|
const existing = await this.settingsStore.getSettings();
|
||||||
|
if (existing.daemonToken !== undefined) {
|
||||||
|
throw new Error("Daemon token already exists. Use rotateToken() to replace it.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = this.generateTokenValue();
|
||||||
|
await this.settingsStore.updateSettings({ daemonToken: token });
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the currently stored daemon token, if any.
|
||||||
|
*
|
||||||
|
* @returns The stored token or undefined if no token has been generated.
|
||||||
|
*/
|
||||||
|
async getToken(): Promise<string | undefined> {
|
||||||
|
const settings = await this.settingsStore.getSettings();
|
||||||
|
return settings.daemonToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a provided token matches the stored token.
|
||||||
|
*
|
||||||
|
* Uses constant-time comparison to prevent timing attacks.
|
||||||
|
*
|
||||||
|
* @param token - The token to validate
|
||||||
|
* @returns true if the token matches the stored token, false otherwise
|
||||||
|
*/
|
||||||
|
async validateToken(token: string): Promise<boolean> {
|
||||||
|
const stored = await this.getToken();
|
||||||
|
|
||||||
|
// No stored token means validation fails
|
||||||
|
if (stored === undefined) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fast path: check length first to avoid unnecessary crypto calls
|
||||||
|
if (token.length !== stored.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Constant-time comparison to prevent timing attacks
|
||||||
|
try {
|
||||||
|
const tokenBuffer = Buffer.from(token, "utf8");
|
||||||
|
const storedBuffer = Buffer.from(stored, "utf8");
|
||||||
|
return timingSafeEqual(tokenBuffer, storedBuffer);
|
||||||
|
} catch {
|
||||||
|
// Buffer lengths don't match (shouldn't happen if length check passes)
|
||||||
|
// or encoding issues - treat as mismatch
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a new token, replacing any existing token.
|
||||||
|
*
|
||||||
|
* This method is idempotent: it works whether or not a token currently exists.
|
||||||
|
*
|
||||||
|
* @returns The newly generated token string
|
||||||
|
*/
|
||||||
|
async rotateToken(): Promise<string> {
|
||||||
|
const token = this.generateTokenValue();
|
||||||
|
await this.settingsStore.updateSettings({ daemonToken: token });
|
||||||
|
return token;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a random token value without storing it.
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @returns A new token string in the format "fn_<32 hex chars>"
|
||||||
|
*/
|
||||||
|
private generateTokenValue(): string {
|
||||||
|
const hexChars = randomBytes(16).toString("hex");
|
||||||
|
return `${DAEMON_TOKEN_PREFIX}${hexChars}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, CheckoutConflictError } from "./types.js";
|
export { COLUMNS, COLUMN_LABELS, COLUMN_DESCRIPTIONS, VALID_TRANSITIONS, DEFAULT_SETTINGS, DEFAULT_GLOBAL_SETTINGS, DEFAULT_PROJECT_SETTINGS, GLOBAL_SETTINGS_KEYS, PROJECT_SETTINGS_KEYS, isGlobalSettingsKey, isProjectSettingsKey, THINKING_LEVELS, THEME_MODES, COLOR_THEMES, WORKFLOW_STEP_TEMPLATES, AGENT_PERMISSIONS, agentToConfigSnapshot, diffConfigSnapshots, CheckoutConflictError } from "./types.js";
|
||||||
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskCreateInput, TaskDetail, InboxTask, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
export type { Column, IssueInfo, IssueState, PrInfo, PrStatus, Task, TaskAttachment, TaskComment, TaskCommentInput, TaskDocument, TaskDocumentRevision, TaskDocumentCreateInput, TaskCreateInput, TaskDetail, InboxTask, AgentLogEntry, AgentLogType, AgentRole, BoardConfig, MergeDetails, MergeResult, Settings, GlobalSettings, ProjectSettings, SettingsScope, DaemonTokenSettings, TaskStep, StepStatus, TaskLogEntry, RunMutationContext, ActivityLogEntry, ActivityEventType, ThinkingLevel, ThemeMode, ColorTheme, PlanningQuestion, PlanningSummary, PlanningResponse, PlanningQuestionType, ArchivedTaskEntry, BatchStatusRequest, BatchStatusResponse, BatchStatusEntry, BatchStatusResult, ModelPreset, WorkflowStep, WorkflowStepMode, WorkflowStepPhase, WorkflowStepInput, WorkflowStepResult, WorkflowStepTemplate, Agent, OrgTreeNode, AgentState, AgentDetail, AgentCreateInput, AgentUpdateInput, AgentApiKey, AgentApiKeyCreateResult, AgentCapability, AgentPromptTemplate, AgentPromptsConfig, AgentPermission, TaskAssignSource, AgentAccessState, AgentHeartbeatConfig, AgentBudgetConfig, AgentBudgetStatus, InstructionsBundleConfig, MessageResponseMode, AgentHeartbeatEvent, AgentHeartbeatRun, BlockedStateSnapshot, HeartbeatInvocationSource, AgentTaskSession, AgentRating, AgentRatingSummary, AgentRatingInput, AgentConfigSnapshot, RevisionFieldDiff, AgentConfigRevision, AgentStats, ReflectionTrigger, ReflectionMetrics, AgentReflection, AgentPerformanceSummary, NtfyNotificationEvent, SteeringComment, ParticipantType, MessageType, Message, MessageCreateInput, MessageFilter, Mailbox, CheckoutLease, RunAuditDomain, RunAuditEvent, RunAuditEventInput, RunAuditEventFilter } from "./types.js";
|
||||||
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
export { AGENT_VALID_TRANSITIONS } from "./types.js";
|
||||||
export {
|
export {
|
||||||
BUILTIN_AGENT_PROMPTS,
|
BUILTIN_AGENT_PROMPTS,
|
||||||
@@ -46,6 +46,7 @@ export { Database, createDatabase, toJson, toJsonNullable, fromJson } from "./db
|
|||||||
export type { Statement } from "./db.js";
|
export type { Statement } from "./db.js";
|
||||||
export { detectLegacyData, migrateFromLegacy, getMigrationStatus } from "./db-migrate.js";
|
export { detectLegacyData, migrateFromLegacy, getMigrationStatus } from "./db-migrate.js";
|
||||||
export { GlobalSettingsStore, resolveGlobalDir } from "./global-settings.js";
|
export { GlobalSettingsStore, resolveGlobalDir } from "./global-settings.js";
|
||||||
|
export { DaemonTokenManager, DAEMON_TOKEN_PREFIX, DAEMON_TOKEN_HEX_LENGTH, isDaemonTokenFormat } from "./daemon-token.js";
|
||||||
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
|
export { canTransition, getValidTransitions, resolveDependencyOrder } from "./board.js";
|
||||||
export { getTaskMergeBlocker, getTaskCompletionBlocker, isTaskReadyForMerge } from "./task-merge.js";
|
export { getTaskMergeBlocker, getTaskCompletionBlocker, isTaskReadyForMerge } from "./task-merge.js";
|
||||||
export {
|
export {
|
||||||
|
|||||||
@@ -38,6 +38,10 @@ export const DEFAULT_GLOBAL_SETTINGS = {
|
|||||||
validatorGlobalModelId: undefined,
|
validatorGlobalModelId: undefined,
|
||||||
titleSummarizerGlobalProvider: undefined,
|
titleSummarizerGlobalProvider: undefined,
|
||||||
titleSummarizerGlobalModelId: undefined,
|
titleSummarizerGlobalModelId: undefined,
|
||||||
|
// Daemon mode settings
|
||||||
|
daemonToken: undefined,
|
||||||
|
daemonPort: 4040,
|
||||||
|
daemonHost: "0.0.0.0",
|
||||||
} satisfies CompleteSettings<GlobalSettings>;
|
} satisfies CompleteSettings<GlobalSettings>;
|
||||||
|
|
||||||
/** Default values for project-level settings. */
|
/** Default values for project-level settings. */
|
||||||
|
|||||||
@@ -825,6 +825,20 @@ export interface TaskCreateInput {
|
|||||||
/** Settings scope discriminator for UI and validation. */
|
/** Settings scope discriminator for UI and validation. */
|
||||||
export type SettingsScope = "global" | "project";
|
export type SettingsScope = "global" | "project";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Settings for daemon mode authentication token and server configuration.
|
||||||
|
* Stored in global settings alongside user preferences.
|
||||||
|
*/
|
||||||
|
export interface DaemonTokenSettings {
|
||||||
|
/** The daemon authentication token (format: fn_<32 hex chars>).
|
||||||
|
* Used for authenticating CLI clients to the daemon server. */
|
||||||
|
daemonToken?: string;
|
||||||
|
/** Port for daemon mode server binding. Default: 4040. */
|
||||||
|
daemonPort?: number;
|
||||||
|
/** Host for daemon mode server binding. Default: "0.0.0.0" (all interfaces). */
|
||||||
|
daemonHost?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Global (user-level) settings stored in `~/.pi/fusion/settings.json`.
|
* Global (user-level) settings stored in `~/.pi/fusion/settings.json`.
|
||||||
*
|
*
|
||||||
@@ -931,6 +945,13 @@ export interface GlobalSettings {
|
|||||||
/** Global baseline AI model ID for title summarization.
|
/** Global baseline AI model ID for title summarization.
|
||||||
* Must be set together with `titleSummarizerGlobalProvider`. */
|
* Must be set together with `titleSummarizerGlobalProvider`. */
|
||||||
titleSummarizerGlobalModelId?: string;
|
titleSummarizerGlobalModelId?: string;
|
||||||
|
/** The daemon authentication token (format: fn_<32 hex chars>).
|
||||||
|
* Used for authenticating CLI clients to the daemon server. */
|
||||||
|
daemonToken?: string;
|
||||||
|
/** Port for daemon mode server binding. Default: 4040. */
|
||||||
|
daemonPort?: number;
|
||||||
|
/** Host for daemon mode server binding. Default: "0.0.0.0" (all interfaces). */
|
||||||
|
daemonHost?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user