CLI: add secrets configuration, doctor check, and path resolver extraction
Add secrets section to onboard, configure, and doctor commands. Doctor validates local encrypted provider key file and can auto-repair missing keys. Extract shared path resolution into path-resolver module used by database and log checks. Show secrets env vars in `paperclip env`. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,19 +1,22 @@
|
||||
import * as p from "@clack/prompts";
|
||||
import pc from "picocolors";
|
||||
import { readConfig, writeConfig, configExists } from "../config/store.js";
|
||||
import { readConfig, writeConfig, configExists, resolveConfigPath } from "../config/store.js";
|
||||
import type { PaperclipConfig } from "../config/schema.js";
|
||||
import { ensureLocalSecretsKeyFile } from "../config/secrets-key.js";
|
||||
import { promptDatabase } from "../prompts/database.js";
|
||||
import { promptLlm } from "../prompts/llm.js";
|
||||
import { promptLogging } from "../prompts/logging.js";
|
||||
import { defaultSecretsConfig, promptSecrets } from "../prompts/secrets.js";
|
||||
import { promptServer } from "../prompts/server.js";
|
||||
|
||||
type Section = "llm" | "database" | "logging" | "server";
|
||||
type Section = "llm" | "database" | "logging" | "server" | "secrets";
|
||||
|
||||
const SECTION_LABELS: Record<Section, string> = {
|
||||
llm: "LLM Provider",
|
||||
database: "Database",
|
||||
logging: "Logging",
|
||||
server: "Server",
|
||||
secrets: "Secrets",
|
||||
};
|
||||
|
||||
function defaultConfig(): PaperclipConfig {
|
||||
@@ -36,6 +39,7 @@ function defaultConfig(): PaperclipConfig {
|
||||
port: 3100,
|
||||
serveUi: true,
|
||||
},
|
||||
secrets: defaultSecretsConfig(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -44,6 +48,7 @@ export async function configure(opts: {
|
||||
section?: string;
|
||||
}): Promise<void> {
|
||||
p.intro(pc.bgCyan(pc.black(" paperclip configure ")));
|
||||
const configPath = resolveConfigPath(opts.config);
|
||||
|
||||
if (!configExists(opts.config)) {
|
||||
p.log.error("No config file found. Run `paperclip onboard` first.");
|
||||
@@ -112,6 +117,21 @@ export async function configure(opts: {
|
||||
case "server":
|
||||
config.server = await promptServer();
|
||||
break;
|
||||
case "secrets":
|
||||
config.secrets = await promptSecrets(config.secrets);
|
||||
{
|
||||
const keyResult = ensureLocalSecretsKeyFile(config, configPath);
|
||||
if (keyResult.status === "created") {
|
||||
p.log.success(`Created local secrets key file at ${pc.dim(keyResult.path)}`);
|
||||
} else if (keyResult.status === "existing") {
|
||||
p.log.message(pc.dim(`Using existing local secrets key file at ${keyResult.path}`));
|
||||
} else if (keyResult.status === "skipped_provider") {
|
||||
p.log.message(pc.dim("Skipping local key file management for non-local provider"));
|
||||
} else {
|
||||
p.log.message(pc.dim("Skipping local key file management because PAPERCLIP_SECRETS_MASTER_KEY is set"));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
config.$meta.updatedAt = new Date().toISOString();
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
llmCheck,
|
||||
logCheck,
|
||||
portCheck,
|
||||
secretsCheck,
|
||||
type CheckResult,
|
||||
} from "../checks/index.js";
|
||||
|
||||
@@ -61,24 +62,30 @@ export async function doctor(opts: {
|
||||
printResult(jwtResult);
|
||||
await maybeRepair(jwtResult, opts);
|
||||
|
||||
// 3. Database check
|
||||
// 3. Secrets adapter check
|
||||
const secretsResult = secretsCheck(config, configPath);
|
||||
results.push(secretsResult);
|
||||
printResult(secretsResult);
|
||||
await maybeRepair(secretsResult, opts);
|
||||
|
||||
// 4. Database check
|
||||
const dbResult = await databaseCheck(config, configPath);
|
||||
results.push(dbResult);
|
||||
printResult(dbResult);
|
||||
await maybeRepair(dbResult, opts);
|
||||
|
||||
// 4. LLM check
|
||||
// 5. LLM check
|
||||
const llmResult = await llmCheck(config);
|
||||
results.push(llmResult);
|
||||
printResult(llmResult);
|
||||
|
||||
// 5. Log directory check
|
||||
// 6. Log directory check
|
||||
const logResult = logCheck(config, configPath);
|
||||
results.push(logResult);
|
||||
printResult(logResult);
|
||||
await maybeRepair(logResult, opts);
|
||||
|
||||
// 6. Port check
|
||||
// 7. Port check
|
||||
const portResult = await portCheck(config);
|
||||
results.push(portResult);
|
||||
printResult(portResult);
|
||||
|
||||
@@ -22,6 +22,8 @@ const DEFAULT_AGENT_JWT_TTL_SECONDS = "172800";
|
||||
const DEFAULT_AGENT_JWT_ISSUER = "paperclip";
|
||||
const DEFAULT_AGENT_JWT_AUDIENCE = "paperclip-api";
|
||||
const DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS = "30000";
|
||||
const DEFAULT_SECRETS_PROVIDER = "local_encrypted";
|
||||
const DEFAULT_SECRETS_KEY_FILE_PATH = "./data/secrets/master.key";
|
||||
|
||||
export async function envCommand(opts: { config?: string }): Promise<void> {
|
||||
p.intro(pc.bgCyan(pc.black(" paperclip env ")));
|
||||
@@ -108,6 +110,17 @@ function collectDeploymentEnvRows(config: PaperclipConfig | null, configPath: st
|
||||
|
||||
const heartbeatInterval = process.env.HEARTBEAT_SCHEDULER_INTERVAL_MS ?? DEFAULT_HEARTBEAT_SCHEDULER_INTERVAL_MS;
|
||||
const heartbeatEnabled = process.env.HEARTBEAT_SCHEDULER_ENABLED ?? "true";
|
||||
const secretsProvider =
|
||||
process.env.PAPERCLIP_SECRETS_PROVIDER ??
|
||||
config?.secrets?.provider ??
|
||||
DEFAULT_SECRETS_PROVIDER;
|
||||
const secretsStrictMode =
|
||||
process.env.PAPERCLIP_SECRETS_STRICT_MODE ??
|
||||
String(config?.secrets?.strictMode ?? false);
|
||||
const secretsKeyFilePath =
|
||||
process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE ??
|
||||
config?.secrets?.localEncrypted?.keyFilePath ??
|
||||
DEFAULT_SECRETS_KEY_FILE_PATH;
|
||||
|
||||
const rows: EnvVarRow[] = [
|
||||
{
|
||||
@@ -176,6 +189,39 @@ function collectDeploymentEnvRows(config: PaperclipConfig | null, configPath: st
|
||||
required: false,
|
||||
note: "Set to `false` to disable timer scheduling",
|
||||
},
|
||||
{
|
||||
key: "PAPERCLIP_SECRETS_PROVIDER",
|
||||
value: secretsProvider,
|
||||
source: process.env.PAPERCLIP_SECRETS_PROVIDER
|
||||
? "env"
|
||||
: config?.secrets?.provider
|
||||
? "config"
|
||||
: "default",
|
||||
required: false,
|
||||
note: "Default provider for new secrets",
|
||||
},
|
||||
{
|
||||
key: "PAPERCLIP_SECRETS_STRICT_MODE",
|
||||
value: secretsStrictMode,
|
||||
source: process.env.PAPERCLIP_SECRETS_STRICT_MODE
|
||||
? "env"
|
||||
: config?.secrets?.strictMode !== undefined
|
||||
? "config"
|
||||
: "default",
|
||||
required: false,
|
||||
note: "Require secret refs for sensitive env keys",
|
||||
},
|
||||
{
|
||||
key: "PAPERCLIP_SECRETS_MASTER_KEY_FILE",
|
||||
value: secretsKeyFilePath,
|
||||
source: process.env.PAPERCLIP_SECRETS_MASTER_KEY_FILE
|
||||
? "env"
|
||||
: config?.secrets?.localEncrypted?.keyFilePath
|
||||
? "config"
|
||||
: "default",
|
||||
required: false,
|
||||
note: "Path to local encrypted secrets key file",
|
||||
},
|
||||
];
|
||||
|
||||
const defaultConfigPath = resolveConfigPath();
|
||||
|
||||
@@ -6,6 +6,7 @@ import { ensureAgentJwtSecret, resolveAgentJwtEnvFile } from "../config/env.js";
|
||||
import { promptDatabase } from "../prompts/database.js";
|
||||
import { promptLlm } from "../prompts/llm.js";
|
||||
import { promptLogging } from "../prompts/logging.js";
|
||||
import { defaultSecretsConfig } from "../prompts/secrets.js";
|
||||
import { promptServer } from "../prompts/server.js";
|
||||
|
||||
export async function onboard(opts: { config?: string }): Promise<void> {
|
||||
@@ -98,6 +99,15 @@ export async function onboard(opts: { config?: string }): Promise<void> {
|
||||
p.log.step(pc.bold("Server"));
|
||||
const server = await promptServer();
|
||||
|
||||
// Secrets
|
||||
p.log.step(pc.bold("Secrets"));
|
||||
const secrets = defaultSecretsConfig();
|
||||
p.log.message(
|
||||
pc.dim(
|
||||
`Using defaults: provider=${secrets.provider}, strictMode=${secrets.strictMode}, keyFile=${secrets.localEncrypted.keyFilePath}`,
|
||||
),
|
||||
);
|
||||
|
||||
const jwtSecret = ensureAgentJwtSecret();
|
||||
const envFilePath = resolveAgentJwtEnvFile();
|
||||
if (jwtSecret.created) {
|
||||
@@ -119,6 +129,7 @@ export async function onboard(opts: { config?: string }): Promise<void> {
|
||||
database,
|
||||
logging,
|
||||
server,
|
||||
secrets,
|
||||
};
|
||||
|
||||
writeConfig(config, opts.config);
|
||||
@@ -129,6 +140,7 @@ export async function onboard(opts: { config?: string }): Promise<void> {
|
||||
llm ? `LLM: ${llm.provider}` : "LLM: not configured",
|
||||
`Logging: ${logging.mode} → ${logging.logDir}`,
|
||||
`Server: port ${server.port}`,
|
||||
`Secrets: ${secrets.provider} (strict mode ${secrets.strictMode ? "on" : "off"})`,
|
||||
`Agent auth: PAPERCLIP_AGENT_JWT_SECRET configured`,
|
||||
].join("\n"),
|
||||
"Configuration saved",
|
||||
|
||||
Reference in New Issue
Block a user