@poolzin/pool-bot 2026.3.23 → 2026.3.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/CHANGELOG.md +57 -0
  2. package/dist/.buildstamp +1 -1
  3. package/dist/acp/policy.js +52 -0
  4. package/dist/agents/btw.js +280 -0
  5. package/dist/agents/fast-mode.js +24 -0
  6. package/dist/agents/live-model-errors.js +23 -0
  7. package/dist/agents/model-auth-env-vars.js +44 -0
  8. package/dist/agents/model-auth-markers.js +69 -0
  9. package/dist/agents/models-config.providers.discovery.js +180 -0
  10. package/dist/agents/models-config.providers.static.js +480 -0
  11. package/dist/auto-reply/reply/typing-policy.js +15 -0
  12. package/dist/build-info.json +3 -3
  13. package/dist/channels/account-snapshot-fields.js +176 -0
  14. package/dist/channels/draft-stream-controls.js +89 -0
  15. package/dist/channels/inbound-debounce-policy.js +28 -0
  16. package/dist/channels/typing-lifecycle.js +39 -0
  17. package/dist/cli/program/command-registry.js +52 -0
  18. package/dist/commands/agent-binding.js +123 -0
  19. package/dist/commands/agents.commands.bind.js +280 -0
  20. package/dist/commands/backup-shared.js +186 -0
  21. package/dist/commands/backup-verify.js +236 -0
  22. package/dist/commands/backup.js +166 -0
  23. package/dist/commands/channel-account-context.js +15 -0
  24. package/dist/commands/channel-account.js +190 -0
  25. package/dist/commands/gateway-install-token.js +117 -0
  26. package/dist/commands/oauth-tls-preflight.js +121 -0
  27. package/dist/commands/ollama-setup.js +402 -0
  28. package/dist/commands/self-hosted-provider-setup.js +207 -0
  29. package/dist/commands/session-store-targets.js +12 -0
  30. package/dist/commands/sessions-cleanup.js +97 -0
  31. package/dist/cron/heartbeat-policy.js +26 -0
  32. package/dist/gateway/hooks-mapping.js +46 -7
  33. package/dist/hooks/module-loader.js +28 -0
  34. package/dist/infra/agent-command-binding.js +144 -0
  35. package/dist/infra/backup.js +328 -0
  36. package/dist/infra/channel-account-context.js +173 -0
  37. package/dist/infra/session-cleanup.js +143 -0
  38. package/package.json +1 -1
@@ -0,0 +1,166 @@
1
+ /**
2
+ * Backup Command
3
+ *
4
+ * Create and restore backups of Pool Bot data.
5
+ */
6
+ import { createBackup, restoreBackup, listBackups, deleteBackup, getBackupDir, } from "../infra/backup.js";
7
+ function formatDuration(ms) {
8
+ if (ms < 1000)
9
+ return `${ms}ms`;
10
+ const seconds = Math.floor(ms / 1000);
11
+ if (seconds < 60)
12
+ return `${seconds}s`;
13
+ const minutes = Math.floor(seconds / 60);
14
+ const remainingSeconds = seconds % 60;
15
+ return `${minutes}m ${remainingSeconds}s`;
16
+ }
17
+ export function registerBackupCommand(program) {
18
+ const backupCmd = program.command("backup");
19
+ // Create backup
20
+ backupCmd
21
+ .command("create")
22
+ .description("Create a new backup")
23
+ .option("--no-sessions", "Exclude sessions from backup")
24
+ .option("--no-config", "Exclude config from backup")
25
+ .option("--no-credentials", "Exclude credentials from backup")
26
+ .option("--output <dir>", "Output directory for backup")
27
+ .action(async (options) => {
28
+ console.log("šŸŽ± Pool Bot - Creating backup...\n");
29
+ const result = await createBackup({
30
+ includeSessions: options.sessions !== false,
31
+ includeConfig: options.config !== false,
32
+ includeCredentials: options.credentials !== false,
33
+ destinationDir: options.output,
34
+ });
35
+ if (result.success) {
36
+ console.log("āœ… Backup created successfully!\n");
37
+ console.log(`šŸ“¦ Backup Path: ${result.backupPath}`);
38
+ console.log(`ā±ļø Duration: ${formatDuration(result.duration)}`);
39
+ console.log(`šŸ’¾ Total Size: ${(result.totalSize / 1024 / 1024).toFixed(2)} MB`);
40
+ console.log(`\nšŸ“Š Contents:`);
41
+ if (result.metadata.includesSessions) {
42
+ console.log(` - Sessions: ${result.sessionsBackedUp || 0} files`);
43
+ }
44
+ if (result.metadata.includesConfig) {
45
+ console.log(` - Config: ${result.configBackedUp ? "Yes" : "No"}`);
46
+ }
47
+ if (result.metadata.includesCredentials) {
48
+ console.log(` - Credentials: ${result.credentialsBackedUp ? "Yes" : "No"}`);
49
+ }
50
+ }
51
+ else {
52
+ console.error("āŒ Backup failed!\n");
53
+ console.error(`Error: ${result.error}`);
54
+ process.exit(1);
55
+ }
56
+ });
57
+ // Restore backup
58
+ backupCmd
59
+ .command("restore <backupPath>")
60
+ .description("Restore from a backup")
61
+ .option("--no-sessions", "Do not restore sessions")
62
+ .option("--no-config", "Do not restore config")
63
+ .option("--no-credentials", "Do not restore credentials")
64
+ .option("--dry-run", "Show what would be restored without actually restoring")
65
+ .action(async (backupPath, options) => {
66
+ console.log("šŸŽ± Pool Bot - Restoring backup...\n");
67
+ const result = await restoreBackup({
68
+ backupPath,
69
+ restoreSessions: options.sessions !== false,
70
+ restoreConfig: options.config !== false,
71
+ restoreCredentials: options.credentials !== false,
72
+ dryRun: options.dryRun,
73
+ });
74
+ if (result.success) {
75
+ if (options.dryRun) {
76
+ console.log("šŸ” Dry Run - No changes made\n");
77
+ console.log("šŸ“Š Would restore:");
78
+ if (result.sessionsRestored) {
79
+ console.log(` - Sessions: ${result.sessionsRestored} files`);
80
+ }
81
+ if (result.configRestored) {
82
+ console.log(` - Config: Yes`);
83
+ }
84
+ if (result.credentialsRestored) {
85
+ console.log(` - Credentials: Yes`);
86
+ }
87
+ }
88
+ else {
89
+ console.log("āœ… Backup restored successfully!\n");
90
+ console.log(`ā±ļø Duration: ${formatDuration(result.duration)}`);
91
+ console.log(`\nšŸ“Š Restored:`);
92
+ if (result.sessionsRestored) {
93
+ console.log(` - Sessions: ${result.sessionsRestored} files`);
94
+ }
95
+ if (result.configRestored) {
96
+ console.log(` - Config: Yes`);
97
+ }
98
+ if (result.credentialsRestored) {
99
+ console.log(` - Credentials: Yes`);
100
+ }
101
+ }
102
+ }
103
+ else {
104
+ console.error("āŒ Restore failed!\n");
105
+ console.error(`Error: ${result.error}`);
106
+ process.exit(1);
107
+ }
108
+ });
109
+ // List backups
110
+ backupCmd
111
+ .command("list")
112
+ .description("List available backups")
113
+ .option("--dir <dir>", "Backup directory", getBackupDir())
114
+ .action(async (options) => {
115
+ console.log("šŸŽ± Pool Bot - Available backups:\n");
116
+ const backups = await listBackups(options.dir);
117
+ if (backups.length === 0) {
118
+ console.log("No backups found.\n");
119
+ return;
120
+ }
121
+ console.log(`šŸ“‚ Backup Directory: ${options.dir}\n`);
122
+ console.log("ā”Œā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¬ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”");
123
+ console.log("│ # │ Created │ Sessions │ Size │ Path │");
124
+ console.log("ā”œā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¼ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤");
125
+ backups.forEach((backup, index) => {
126
+ const date = new Date(backup.createdAt).toLocaleString();
127
+ const sessions = backup.metadata.sessionCount || 0;
128
+ const size = (backup.size / 1024 / 1024).toFixed(2);
129
+ const pathDisplay = backup.path.length > 40 ? "..." + backup.path.slice(-37) : backup.path;
130
+ console.log(`│ ${String(index + 1).padEnd(3)} │ ${date.padEnd(20)} │ ${String(sessions).padEnd(12)} │ ${(size + " MB").padEnd(8)} │ ${pathDisplay.padEnd(7)} │`);
131
+ });
132
+ console.log("ā””ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”“ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜\n");
133
+ console.log(`Total: ${backups.length} backup(s)\n`);
134
+ });
135
+ // Delete backup
136
+ backupCmd
137
+ .command("delete <backupPath>")
138
+ .description("Delete a backup")
139
+ .option("--force", "Skip confirmation")
140
+ .action(async (backupPath, options) => {
141
+ if (!options.force) {
142
+ const readline = await import("node:readline");
143
+ const rl = readline.createInterface({
144
+ input: process.stdin,
145
+ output: process.stdout,
146
+ });
147
+ const answer = await new Promise((resolve) => {
148
+ rl.question(`āš ļø Are you sure you want to delete this backup?\n ${backupPath}\n\n Type 'yes' to confirm: `, resolve);
149
+ });
150
+ rl.close();
151
+ if (answer.toLowerCase() !== "yes") {
152
+ console.log("\nāŒ Deletion cancelled.\n");
153
+ return;
154
+ }
155
+ }
156
+ const success = await deleteBackup(backupPath);
157
+ if (success) {
158
+ console.log("āœ… Backup deleted successfully.\n");
159
+ }
160
+ else {
161
+ console.error("āŒ Failed to delete backup.\n");
162
+ process.exit(1);
163
+ }
164
+ });
165
+ return backupCmd;
166
+ }
@@ -0,0 +1,15 @@
1
+ import { resolveChannelDefaultAccountId } from "../channels/plugins/helpers.js";
2
+ export async function resolveDefaultChannelAccountContext(plugin, cfg) {
3
+ const accountIds = plugin.config.listAccountIds(cfg);
4
+ const defaultAccountId = resolveChannelDefaultAccountId({
5
+ plugin,
6
+ cfg,
7
+ accountIds,
8
+ });
9
+ const account = plugin.config.resolveAccount(cfg, defaultAccountId);
10
+ const enabled = plugin.config.isEnabled ? plugin.config.isEnabled(account, cfg) : true;
11
+ const configured = plugin.config.isConfigured
12
+ ? await plugin.config.isConfigured(account, cfg)
13
+ : true;
14
+ return { accountIds, defaultAccountId, account, enabled, configured };
15
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Channel Account Context CLI
3
+ *
4
+ * Manage multiple accounts per channel.
5
+ */
6
+ import { addChannelAccount, removeChannelAccount, getChannelAccount, getChannelAccounts, getActiveChannelAccount, setActiveChannelAccount, updateChannelAccount, getChannelAccountStats, } from "../infra/channel-account-context.js";
7
+ function formatAccount(account) {
8
+ const status = account.isActive ? "🟢 Active" : "⚪ Inactive";
9
+ const lastUsed = account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleString() : "Never";
10
+ return `${status} | ${account.accountName} | ${account.channelType} | Last: ${lastUsed}`;
11
+ }
12
+ export function registerChannelAccountCommand(program) {
13
+ const accountCmd = program
14
+ .command("channel-account")
15
+ .description("Manage channel accounts (multi-account support)");
16
+ // List accounts
17
+ accountCmd
18
+ .command("list [channelId]")
19
+ .description("List channel accounts")
20
+ .action(async (channelId) => {
21
+ console.log("šŸŽ± Pool Bot - Channel Accounts\n");
22
+ if (channelId) {
23
+ const accounts = await getChannelAccounts(channelId);
24
+ const active = await getActiveChannelAccount(channelId);
25
+ console.log(`Channel: ${channelId}`);
26
+ console.log(`Active Account: ${active ? active.accountName : "None"}\n`);
27
+ if (accounts.length === 0) {
28
+ console.log("No accounts found for this channel.\n");
29
+ return;
30
+ }
31
+ accounts.forEach((account) => {
32
+ console.log(formatAccount(account));
33
+ });
34
+ console.log(`\nTotal: ${accounts.length} account(s)\n`);
35
+ }
36
+ else {
37
+ const stats = await getChannelAccountStats();
38
+ console.log("šŸ“Š Account Statistics:");
39
+ console.log(` Total Accounts: ${stats.totalAccounts}`);
40
+ console.log(` Channels: ${stats.channelsWithAccounts}`);
41
+ console.log(` Active Accounts: ${stats.activeAccounts}`);
42
+ console.log("\nAccounts by Channel Type:");
43
+ Object.entries(stats.accountsByChannelType).forEach(([type, count]) => {
44
+ console.log(` ${type}: ${count}`);
45
+ });
46
+ console.log();
47
+ }
48
+ });
49
+ // Add account
50
+ accountCmd
51
+ .command("add <channelId> <channelType> <accountName>")
52
+ .description("Add a new channel account")
53
+ .option("--metadata <json>", "Additional metadata (JSON)")
54
+ .action(async (channelId, channelType, accountName, options) => {
55
+ console.log("šŸŽ± Pool Bot - Adding Channel Account\n");
56
+ let metadata;
57
+ if (options.metadata) {
58
+ try {
59
+ metadata = JSON.parse(options.metadata);
60
+ }
61
+ catch {
62
+ console.error("Error: Invalid JSON for metadata\n");
63
+ process.exit(1);
64
+ }
65
+ }
66
+ const account = await addChannelAccount({
67
+ channelId,
68
+ channelType,
69
+ accountName,
70
+ metadata,
71
+ });
72
+ console.log(`āœ… Account added successfully!\n`);
73
+ console.log(`Account ID: ${account.id}`);
74
+ console.log(`Channel: ${account.channelId}`);
75
+ console.log(`Type: ${account.channelType}`);
76
+ console.log(`Name: ${account.accountName}`);
77
+ console.log(`Status: ${account.isActive ? "Active" : "Inactive"}\n`);
78
+ console.log(`šŸ’” Use "poolbot channel-account switch ${channelId} ${account.id}" to activate\n`);
79
+ });
80
+ // Switch account
81
+ accountCmd
82
+ .command("switch <channelId> <accountId>")
83
+ .description("Switch active account for a channel")
84
+ .action(async (channelId, accountId) => {
85
+ console.log("šŸŽ± Pool Bot - Switching Account\n");
86
+ const success = await setActiveChannelAccount(channelId, accountId);
87
+ if (success) {
88
+ console.log(`āœ… Switched to account ${accountId}\n`);
89
+ }
90
+ else {
91
+ console.error(`āŒ Account not found: ${accountId}\n`);
92
+ process.exit(1);
93
+ }
94
+ });
95
+ // Remove account
96
+ accountCmd
97
+ .command("remove <channelId> <accountId>")
98
+ .description("Remove a channel account")
99
+ .action(async (channelId, accountId) => {
100
+ console.log("šŸŽ± Pool Bot - Removing Account\n");
101
+ const success = await removeChannelAccount(channelId, accountId);
102
+ if (success) {
103
+ console.log(`āœ… Account removed successfully\n`);
104
+ }
105
+ else {
106
+ console.error(`āŒ Account not found\n`);
107
+ process.exit(1);
108
+ }
109
+ });
110
+ // Show account
111
+ accountCmd
112
+ .command("show <channelId> <accountId>")
113
+ .description("Show account details")
114
+ .action(async (channelId, accountId) => {
115
+ console.log(`šŸŽ± Pool Bot - Account Details\n`);
116
+ const account = await getChannelAccount(channelId, accountId);
117
+ if (!account) {
118
+ console.error(`āŒ Account not found\n`);
119
+ process.exit(1);
120
+ }
121
+ console.log(`Account ID: ${account.id}`);
122
+ console.log(`Channel: ${account.channelId}`);
123
+ console.log(`Type: ${account.channelType}`);
124
+ console.log(`Name: ${account.accountName}`);
125
+ console.log(`Status: ${account.isActive ? "Active 🟢" : "Inactive ⚪"}`);
126
+ console.log(`Created: ${new Date(account.createdAt).toLocaleString()}`);
127
+ console.log(`Updated: ${new Date(account.updatedAt).toLocaleString()}`);
128
+ console.log(`Last Used: ${account.lastUsedAt ? new Date(account.lastUsedAt).toLocaleString() : "Never"}`);
129
+ if (account.metadata && Object.keys(account.metadata).length > 0) {
130
+ console.log("\nMetadata:");
131
+ Object.entries(account.metadata).forEach(([key, value]) => {
132
+ console.log(` ${key}: ${value}`);
133
+ });
134
+ }
135
+ console.log();
136
+ });
137
+ // Update account
138
+ accountCmd
139
+ .command("update <channelId> <accountId>")
140
+ .description("Update account details")
141
+ .option("--name <name>", "New account name")
142
+ .option("--metadata <json>", "New metadata (JSON, merges with existing)")
143
+ .action(async (channelId, accountId, options) => {
144
+ console.log("šŸŽ± Pool Bot - Updating Account\n");
145
+ let metadata;
146
+ if (options.metadata) {
147
+ try {
148
+ metadata = JSON.parse(options.metadata);
149
+ }
150
+ catch {
151
+ console.error("Error: Invalid JSON for metadata\n");
152
+ process.exit(1);
153
+ }
154
+ }
155
+ const account = await updateChannelAccount({
156
+ channelId,
157
+ accountId,
158
+ accountName: options.name,
159
+ metadata,
160
+ });
161
+ if (!account) {
162
+ console.error(`āŒ Account not found\n`);
163
+ process.exit(1);
164
+ }
165
+ console.log(`āœ… Account updated successfully\n`);
166
+ });
167
+ // Stats
168
+ accountCmd
169
+ .command("stats")
170
+ .description("Show account statistics")
171
+ .action(async () => {
172
+ console.log("šŸŽ± Pool Bot - Account Statistics\n");
173
+ const stats = await getChannelAccountStats();
174
+ console.log("šŸ“Š Statistics:");
175
+ console.log(` Total Accounts: ${stats.totalAccounts}`);
176
+ console.log(` Channels with Accounts: ${stats.channelsWithAccounts}`);
177
+ console.log(` Active Accounts: ${stats.activeAccounts}`);
178
+ console.log("\nAccounts by Channel Type:");
179
+ if (Object.keys(stats.accountsByChannelType).length === 0) {
180
+ console.log(" No accounts yet");
181
+ }
182
+ else {
183
+ Object.entries(stats.accountsByChannelType).forEach(([type, count]) => {
184
+ console.log(` ${type}: ${count}`);
185
+ });
186
+ }
187
+ console.log();
188
+ });
189
+ return accountCmd;
190
+ }
@@ -0,0 +1,117 @@
1
+ import { formatCliCommand } from "../cli/command-format.js";
2
+ import { readConfigFileSnapshot, writeConfigFile } from "../config/config.js";
3
+ import { resolveSecretInputRef } from "../config/types.secrets.js";
4
+ import { shouldRequireGatewayTokenForInstall } from "../gateway/auth-install-policy.js";
5
+ import { hasAmbiguousGatewayAuthModeConfig } from "../gateway/auth-mode-policy.js";
6
+ import { resolveGatewayAuth } from "../gateway/auth.js";
7
+ import { readGatewayTokenEnv } from "../gateway/credentials.js";
8
+ import { secretRefKey } from "../secrets/ref-contract.js";
9
+ import { resolveSecretRefValues } from "../secrets/resolve.js";
10
+ import { randomToken } from "./onboard-helpers.js";
11
+ function formatAmbiguousGatewayAuthModeReason() {
12
+ return [
13
+ "gateway.auth.token and gateway.auth.password are both configured while gateway.auth.mode is unset.",
14
+ `Set ${formatCliCommand("openclaw config set gateway.auth.mode token")} or ${formatCliCommand("openclaw config set gateway.auth.mode password")}.`,
15
+ ].join(" ");
16
+ }
17
+ export async function resolveGatewayInstallToken(options) {
18
+ const cfg = options.config;
19
+ const warnings = [];
20
+ const tokenRef = resolveSecretInputRef({
21
+ value: cfg.gateway?.auth?.token,
22
+ defaults: cfg.secrets?.defaults,
23
+ }).ref;
24
+ const tokenRefConfigured = Boolean(tokenRef);
25
+ const configToken = tokenRef || typeof cfg.gateway?.auth?.token !== "string"
26
+ ? undefined
27
+ : cfg.gateway.auth.token.trim() || undefined;
28
+ const explicitToken = options.explicitToken?.trim() || undefined;
29
+ const envToken = readGatewayTokenEnv(options.env);
30
+ if (hasAmbiguousGatewayAuthModeConfig(cfg)) {
31
+ return {
32
+ token: undefined,
33
+ tokenRefConfigured,
34
+ unavailableReason: formatAmbiguousGatewayAuthModeReason(),
35
+ warnings,
36
+ };
37
+ }
38
+ const resolvedAuth = resolveGatewayAuth({
39
+ authConfig: cfg.gateway?.auth,
40
+ tailscaleMode: cfg.gateway?.tailscale?.mode ?? "off",
41
+ });
42
+ const needsToken = shouldRequireGatewayTokenForInstall(cfg, options.env) && !resolvedAuth.allowTailscale;
43
+ let token = explicitToken || configToken || (tokenRef ? undefined : envToken);
44
+ let unavailableReason;
45
+ if (tokenRef && !token && needsToken) {
46
+ try {
47
+ const resolved = await resolveSecretRefValues([tokenRef], {
48
+ config: cfg,
49
+ env: options.env,
50
+ });
51
+ const value = resolved.get(secretRefKey(tokenRef));
52
+ if (typeof value !== "string" || value.trim().length === 0) {
53
+ throw new Error("gateway.auth.token resolved to an empty or non-string value.");
54
+ }
55
+ warnings.push("gateway.auth.token is SecretRef-managed; install will not persist a resolved token in service environment. Ensure the SecretRef is resolvable in the daemon runtime context.");
56
+ }
57
+ catch (err) {
58
+ unavailableReason = `gateway.auth.token SecretRef is configured but unresolved (${String(err)}).`;
59
+ }
60
+ }
61
+ const allowAutoGenerate = options.autoGenerateWhenMissing ?? false;
62
+ const persistGeneratedToken = options.persistGeneratedToken ?? false;
63
+ if (!token && needsToken && !tokenRef && allowAutoGenerate) {
64
+ token = randomToken();
65
+ warnings.push(persistGeneratedToken
66
+ ? "No gateway token found. Auto-generated one and saving to config."
67
+ : "No gateway token found. Auto-generated one for this run without saving to config.");
68
+ if (persistGeneratedToken) {
69
+ // Persist token in config so daemon and CLI share a stable credential source.
70
+ try {
71
+ const snapshot = await readConfigFileSnapshot();
72
+ if (snapshot.exists && !snapshot.valid) {
73
+ warnings.push("Warning: config file exists but is invalid; skipping token persistence.");
74
+ }
75
+ else {
76
+ const baseConfig = snapshot.exists ? snapshot.config : {};
77
+ const existingTokenRef = resolveSecretInputRef({
78
+ value: baseConfig.gateway?.auth?.token,
79
+ defaults: baseConfig.secrets?.defaults,
80
+ }).ref;
81
+ const baseConfigToken = existingTokenRef || typeof baseConfig.gateway?.auth?.token !== "string"
82
+ ? undefined
83
+ : baseConfig.gateway.auth.token.trim() || undefined;
84
+ if (!existingTokenRef && !baseConfigToken) {
85
+ await writeConfigFile({
86
+ ...baseConfig,
87
+ gateway: {
88
+ ...baseConfig.gateway,
89
+ auth: {
90
+ ...baseConfig.gateway?.auth,
91
+ mode: baseConfig.gateway?.auth?.mode ?? "token",
92
+ token,
93
+ },
94
+ },
95
+ });
96
+ }
97
+ else if (baseConfigToken) {
98
+ token = baseConfigToken;
99
+ }
100
+ else {
101
+ token = undefined;
102
+ warnings.push("Warning: gateway.auth.token is SecretRef-managed; skipping plaintext token persistence.");
103
+ }
104
+ }
105
+ }
106
+ catch (err) {
107
+ warnings.push(`Warning: could not persist token to config: ${String(err)}`);
108
+ }
109
+ }
110
+ }
111
+ return {
112
+ token,
113
+ tokenRefConfigured,
114
+ unavailableReason,
115
+ warnings,
116
+ };
117
+ }
@@ -0,0 +1,121 @@
1
+ import path from "node:path";
2
+ import { formatCliCommand } from "../cli/command-format.js";
3
+ import { note } from "../terminal/note.js";
4
+ const TLS_CERT_ERROR_CODES = new Set([
5
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
6
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
7
+ "CERT_HAS_EXPIRED",
8
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
9
+ "SELF_SIGNED_CERT_IN_CHAIN",
10
+ "ERR_TLS_CERT_ALTNAME_INVALID",
11
+ ]);
12
+ const TLS_CERT_ERROR_PATTERNS = [
13
+ /unable to get local issuer certificate/i,
14
+ /unable to verify the first certificate/i,
15
+ /self[- ]signed certificate/i,
16
+ /certificate has expired/i,
17
+ ];
18
+ const OPENAI_AUTH_PROBE_URL = "https://auth.openai.com/oauth/authorize?response_type=code&client_id=openclaw-preflight&redirect_uri=http%3A%2F%2Flocalhost%3A1455%2Fauth%2Fcallback&scope=openid+profile+email";
19
+ function asRecord(value) {
20
+ return value && typeof value === "object" ? value : null;
21
+ }
22
+ function extractFailure(error) {
23
+ const root = asRecord(error);
24
+ const rootCause = asRecord(root?.cause);
25
+ const code = typeof rootCause?.code === "string" ? rootCause.code : undefined;
26
+ const message = typeof rootCause?.message === "string"
27
+ ? rootCause.message
28
+ : typeof root?.message === "string"
29
+ ? root.message
30
+ : String(error);
31
+ const isTlsCertError = (code ? TLS_CERT_ERROR_CODES.has(code) : false) ||
32
+ TLS_CERT_ERROR_PATTERNS.some((pattern) => pattern.test(message));
33
+ return {
34
+ code,
35
+ message,
36
+ kind: isTlsCertError ? "tls-cert" : "network",
37
+ };
38
+ }
39
+ function resolveHomebrewPrefixFromExecPath(execPath) {
40
+ const marker = `${path.sep}Cellar${path.sep}`;
41
+ const idx = execPath.indexOf(marker);
42
+ if (idx > 0) {
43
+ return execPath.slice(0, idx);
44
+ }
45
+ const envPrefix = process.env.HOMEBREW_PREFIX?.trim();
46
+ return envPrefix ? envPrefix : null;
47
+ }
48
+ function resolveCertBundlePath() {
49
+ const prefix = resolveHomebrewPrefixFromExecPath(process.execPath);
50
+ if (!prefix) {
51
+ return null;
52
+ }
53
+ return path.join(prefix, "etc", "openssl@3", "cert.pem");
54
+ }
55
+ function hasOpenAICodexOAuthProfile(cfg) {
56
+ const profiles = cfg.auth?.profiles;
57
+ if (!profiles) {
58
+ return false;
59
+ }
60
+ return Object.values(profiles).some((profile) => profile.provider === "openai-codex" && profile.mode === "oauth");
61
+ }
62
+ function shouldRunOpenAIOAuthTlsPrerequisites(params) {
63
+ if (params.deep === true) {
64
+ return true;
65
+ }
66
+ return hasOpenAICodexOAuthProfile(params.cfg);
67
+ }
68
+ export async function runOpenAIOAuthTlsPreflight(options) {
69
+ const timeoutMs = options?.timeoutMs ?? 5000;
70
+ const fetchImpl = options?.fetchImpl ?? fetch;
71
+ try {
72
+ await fetchImpl(OPENAI_AUTH_PROBE_URL, {
73
+ method: "GET",
74
+ redirect: "manual",
75
+ signal: AbortSignal.timeout(timeoutMs),
76
+ });
77
+ return { ok: true };
78
+ }
79
+ catch (error) {
80
+ const failure = extractFailure(error);
81
+ return {
82
+ ok: false,
83
+ kind: failure.kind,
84
+ code: failure.code,
85
+ message: failure.message,
86
+ };
87
+ }
88
+ }
89
+ export function formatOpenAIOAuthTlsPreflightFix(result) {
90
+ if (result.kind !== "tls-cert") {
91
+ return [
92
+ "OpenAI OAuth prerequisites check failed due to a network error before the browser flow.",
93
+ `Cause: ${result.message}`,
94
+ "Verify DNS/firewall/proxy access to auth.openai.com and retry.",
95
+ ].join("\n");
96
+ }
97
+ const certBundlePath = resolveCertBundlePath();
98
+ const lines = [
99
+ "OpenAI OAuth prerequisites check failed: Node/OpenSSL cannot validate TLS certificates.",
100
+ `Cause: ${result.code ? `${result.code} (${result.message})` : result.message}`,
101
+ "",
102
+ "Fix (Homebrew Node/OpenSSL):",
103
+ `- ${formatCliCommand("brew postinstall ca-certificates")}`,
104
+ `- ${formatCliCommand("brew postinstall openssl@3")}`,
105
+ ];
106
+ if (certBundlePath) {
107
+ lines.push(`- Verify cert bundle exists: ${certBundlePath}`);
108
+ }
109
+ lines.push("- Retry the OAuth login flow.");
110
+ return lines.join("\n");
111
+ }
112
+ export async function noteOpenAIOAuthTlsPrerequisites(params) {
113
+ if (!shouldRunOpenAIOAuthTlsPrerequisites(params)) {
114
+ return;
115
+ }
116
+ const result = await runOpenAIOAuthTlsPreflight({ timeoutMs: 4000 });
117
+ if (result.ok || result.kind !== "tls-cert") {
118
+ return;
119
+ }
120
+ note(formatOpenAIOAuthTlsPreflightFix(result), "OAuth TLS prerequisites");
121
+ }