thepopebot 1.2.75-beta.6 → 1.2.75-beta.7

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.
@@ -70,9 +70,9 @@ export async function register() {
70
70
  const { startClusterRuntime } = await import('../lib/cluster/runtime.js');
71
71
  startClusterRuntime();
72
72
 
73
- // Start internal maintenance cron (cleanup expired agent job keys, etc.)
74
- const { startMaintenanceCron } = await import('../lib/maintenance.js');
75
- startMaintenanceCron();
73
+ // Maintenance cron disabled agent job key cleanup will be redesigned
74
+ // const { startMaintenanceCron } = await import('../lib/maintenance.js');
75
+ // startMaintenanceCron();
76
76
 
77
77
  console.log('thepopebot initialized');
78
78
  }
package/lib/ai/tools.js CHANGED
@@ -54,9 +54,6 @@ const agentChatCodingTool = tool(
54
54
  const codingAgent = getConfig('CODING_AGENT') || 'claude-code';
55
55
  const containerName = `${codingAgent}-headless-${randomUUID().slice(0, 8)}`;
56
56
 
57
- const { createAgentJobApiKey } = await import('../db/api-keys.js');
58
- const { key: agentJobToken } = createAgentJobApiKey(randomUUID());
59
-
60
57
  const { runHeadlessContainer, tailContainerLogs, waitForContainer, removeContainer } = await import('../tools/docker.js');
61
58
  await runHeadlessContainer({
62
59
  containerName,
@@ -67,7 +64,6 @@ const agentChatCodingTool = tool(
67
64
  taskPrompt: prompt,
68
65
  mode,
69
66
  injectSecrets: true,
70
- agentJobToken,
71
67
  });
72
68
 
73
69
  const streamCallback = runtime.configurable.streamCallback;
@@ -138,12 +138,6 @@ export async function ensureCodeWorkspaceContainer(id) {
138
138
  const chat = getChatByWorkspaceId(id);
139
139
  const injectSecrets = chat?.chatMode === 'agent';
140
140
 
141
- let agentJobToken;
142
- if (injectSecrets) {
143
- const { createAgentJobApiKey } = await import('../db/api-keys.js');
144
- ({ key: agentJobToken } = createAgentJobApiKey(id));
145
- }
146
-
147
141
  try {
148
142
  const { inspectContainer, startContainer, removeContainer, runInteractiveContainer } =
149
143
  await import('../tools/docker.js');
@@ -159,7 +153,6 @@ export async function ensureCodeWorkspaceContainer(id) {
159
153
  featureBranch: workspace.featureBranch,
160
154
  workspaceId: id,
161
155
  injectSecrets,
162
- agentJobToken,
163
156
  });
164
157
  return { status: 'created' };
165
158
  }
@@ -188,7 +181,6 @@ export async function ensureCodeWorkspaceContainer(id) {
188
181
  featureBranch: workspace.featureBranch,
189
182
  workspaceId: id,
190
183
  injectSecrets,
191
- agentJobToken,
192
184
  });
193
185
  return { status: 'created' };
194
186
  } catch (err) {
@@ -217,12 +209,6 @@ export async function startInteractiveMode(id) {
217
209
  const chat = getChatByWorkspaceId(id);
218
210
  const injectSecrets = chat?.chatMode === 'agent';
219
211
 
220
- let agentJobToken;
221
- if (injectSecrets) {
222
- const { createAgentJobApiKey } = await import('../db/api-keys.js');
223
- ({ key: agentJobToken } = createAgentJobApiKey(id));
224
- }
225
-
226
212
  try {
227
213
  const { getConfig } = await import('../config.js');
228
214
  const agent = getConfig('CODING_AGENT') || 'claude-code';
@@ -237,7 +223,6 @@ export async function startInteractiveMode(id) {
237
223
  featureBranch: workspace.featureBranch,
238
224
  workspaceId: id,
239
225
  injectSecrets,
240
- agentJobToken,
241
226
  });
242
227
 
243
228
  updateContainerName(id, containerName);
@@ -5,9 +5,6 @@ import { settings } from './schema.js';
5
5
 
6
6
  const KEY_PREFIX = 'tpb_';
7
7
 
8
- // In-memory cache: array of { id, keyHash } or null (not loaded)
9
- let _cache = null;
10
-
11
8
  /**
12
9
  * Generate a new API key: tpb_ + 64 hex chars (32 random bytes).
13
10
  * @returns {string}
@@ -25,44 +22,6 @@ export function hashApiKey(key) {
25
22
  return createHash('sha256').update(key).digest('hex');
26
23
  }
27
24
 
28
- /**
29
- * Lazy-load all API key hashes into the in-memory cache.
30
- */
31
- function _ensureCache() {
32
- if (_cache !== null) return _cache;
33
-
34
- const db = getDb();
35
- const ONE_HOUR = 60 * 60 * 1000;
36
- const cutoff = Date.now() - ONE_HOUR;
37
-
38
- const rows = db
39
- .select()
40
- .from(settings)
41
- .where(eq(settings.type, 'api_key'))
42
- .all();
43
-
44
- const jobKeyRows = db
45
- .select()
46
- .from(settings)
47
- .where(eq(settings.type, 'agent_job_api_key'))
48
- .all()
49
- .filter(r => r.lastUsedAt !== null ? r.lastUsedAt > cutoff : r.createdAt > cutoff);
50
-
51
- _cache = [...rows, ...jobKeyRows].map((row) => {
52
- const parsed = JSON.parse(row.value);
53
- return { id: row.id, keyHash: parsed.key_hash, type: row.type };
54
- });
55
-
56
- return _cache;
57
- }
58
-
59
- /**
60
- * Clear the in-memory cache (call after create/delete).
61
- */
62
- export function invalidateApiKeyCache() {
63
- _cache = null;
64
- }
65
-
66
25
  /**
67
26
  * Create a new named API key.
68
27
  * @param {string} name - Human-readable name for the key
@@ -89,7 +48,6 @@ export function createApiKeyRecord(name, createdBy) {
89
48
  };
90
49
 
91
50
  db.insert(settings).values(record).run();
92
- invalidateApiKeyCache();
93
51
 
94
52
  return {
95
53
  key,
@@ -143,7 +101,6 @@ export function getApiKey() {
143
101
  export function deleteApiKeyById(id) {
144
102
  const db = getDb();
145
103
  db.delete(settings).where(eq(settings.id, id)).run();
146
- invalidateApiKeyCache();
147
104
  }
148
105
 
149
106
  /**
@@ -152,11 +109,11 @@ export function deleteApiKeyById(id) {
152
109
  export function deleteApiKey() {
153
110
  const db = getDb();
154
111
  db.delete(settings).where(eq(settings.type, 'api_key')).run();
155
- invalidateApiKeyCache();
156
112
  }
157
113
 
158
114
  /**
159
- * Verify a raw API key against all cached hashes.
115
+ * Verify a raw API key against stored hashes.
116
+ * Queries the database directly on each call (SQLite is in-process, no caching needed).
160
117
  * @param {string} rawKey - Raw API key from request header
161
118
  * @returns {object|null} Record if valid, null otherwise
162
119
  */
@@ -164,27 +121,32 @@ export function verifyApiKey(rawKey) {
164
121
  if (!rawKey || !rawKey.startsWith(KEY_PREFIX)) return null;
165
122
 
166
123
  const keyHash = hashApiKey(rawKey);
167
- const cached = _ensureCache();
124
+ const db = getDb();
168
125
 
169
- if (!cached || cached.length === 0) return null;
126
+ const rows = [
127
+ ...db.select().from(settings).where(eq(settings.type, 'api_key')).all(),
128
+ ...db.select().from(settings).where(eq(settings.type, 'agent_job_api_key')).all(),
129
+ ];
130
+
131
+ if (rows.length === 0) return null;
170
132
 
171
133
  const b = Buffer.from(keyHash, 'hex');
172
134
 
173
- for (const entry of cached) {
174
- const a = Buffer.from(entry.keyHash, 'hex');
135
+ for (const row of rows) {
136
+ const parsed = JSON.parse(row.value);
137
+ const a = Buffer.from(parsed.key_hash, 'hex');
175
138
  if (a.length === b.length && timingSafeEqual(a, b)) {
176
- // Update last_used_at column directly (non-blocking)
139
+ // Update last_used_at
177
140
  try {
178
- const db = getDb();
179
141
  const now = Date.now();
180
142
  db.update(settings)
181
143
  .set({ lastUsedAt: now, updatedAt: now })
182
- .where(eq(settings.id, entry.id))
144
+ .where(eq(settings.id, row.id))
183
145
  .run();
184
- } catch {
185
- // Non-fatal: last_used_at is informational
146
+ } catch (err) {
147
+ console.error('[api-keys] Failed to update last_used_at:', err.message);
186
148
  }
187
- return entry;
149
+ return { id: row.id, keyHash: parsed.key_hash, type: row.type };
188
150
  }
189
151
  }
190
152
 
@@ -193,8 +155,8 @@ export function verifyApiKey(rawKey) {
193
155
 
194
156
  /**
195
157
  * Create a per-job API key for an agent job container.
196
- * Stored as type 'agent_job_api_key'. Valid for 1 hour since last use.
197
- * @param {string} jobId - Agent job ID (stored as key column for traceability)
158
+ * Stored as type 'agent_job_api_key'.
159
+ * @param {string} jobId - Agent job or workspace ID (stored as key column for traceability)
198
160
  * @returns {{ key: string }} The raw API key to inject into the container
199
161
  */
200
162
  export function createAgentJobApiKey(jobId) {
@@ -210,7 +172,6 @@ export function createAgentJobApiKey(jobId) {
210
172
  createdAt: now,
211
173
  updatedAt: now,
212
174
  }).run();
213
- invalidateApiKeyCache();
214
175
  return { key };
215
176
  }
216
177
 
@@ -2,7 +2,6 @@ import cron from 'node-cron';
2
2
  import { eq } from 'drizzle-orm';
3
3
  import { getDb } from './db/index.js';
4
4
  import { settings } from './db/schema.js';
5
- import { invalidateApiKeyCache } from './db/api-keys.js';
6
5
 
7
6
  const ONE_HOUR = 60 * 60 * 1000;
8
7
 
@@ -22,7 +21,6 @@ function cleanExpiredAgentJobKeys() {
22
21
  for (const id of expiredIds) {
23
22
  db.delete(settings).where(eq(settings.id, id)).run();
24
23
  }
25
- invalidateApiKeyCache();
26
24
  console.log(`[maintenance] Deleted ${expiredIds.length} expired agent job key(s)`);
27
25
  } else {
28
26
  console.log(`[maintenance] No expired agent job keys (${rows.length} active)`);
@@ -3,8 +3,6 @@ import { z } from 'zod';
3
3
  import { githubApi } from './github.js';
4
4
  import { createModel } from '../ai/model.js';
5
5
  import { getConfig } from '../config.js';
6
- import { createAgentJobApiKey } from '../db/api-keys.js';
7
-
8
6
  /**
9
7
  * Generate a short descriptive title for an agent job using the LLM.
10
8
  * Uses structured output to avoid thinking-token leaks with extended-thinking models.
@@ -93,7 +91,6 @@ async function createAgentJob(agentJobDescription, options = {}) {
93
91
 
94
92
  // 6. Launch Docker container locally (fire-and-forget with async cleanup)
95
93
  const repoSlug = `${GH_OWNER}/${GH_REPO}`;
96
- const { key: agentJobToken } = createAgentJobApiKey(agentJobId);
97
94
  launchAgentJobContainer({
98
95
  agentJobId,
99
96
  repo: repoSlug,
@@ -102,7 +99,6 @@ async function createAgentJob(agentJobDescription, options = {}) {
102
99
  description: agentJobDescription,
103
100
  codingAgent: options.agentBackend,
104
101
  llmModel: options.llmModel,
105
- agentJobToken,
106
102
  }).catch(err => {
107
103
  console.error(`[agent-job] Failed to launch container for ${agentJobId}:`, err.message);
108
104
  });
@@ -196,7 +196,7 @@ async function runContainer({ containerName, image, env = [], workingDir, hostCo
196
196
  * @param {boolean} [options.injectSecrets] - Inject agent job secrets into container env
197
197
  * @returns {Promise<{containerId: string, containerName: string}>}
198
198
  */
199
- async function runInteractiveContainer({ containerName, repo, branch, codingAgent, featureBranch, workspaceId, injectSecrets, agentJobToken, continueSession = true }) {
199
+ async function runInteractiveContainer({ containerName, repo, branch, codingAgent, featureBranch, workspaceId, injectSecrets, continueSession = true }) {
200
200
  const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
201
201
  const version = process.env.THEPOPEBOT_VERSION;
202
202
  const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
@@ -234,11 +234,12 @@ async function runInteractiveContainer({ containerName, repo, branch, codingAgen
234
234
  if (jobSecrets.length > 0) {
235
235
  env.push(`AGENT_JOB_SECRETS=${JSON.stringify(Object.fromEntries(jobSecrets.map(s => [s.key, s.value])))}`);
236
236
  }
237
- if (agentJobToken) {
238
- env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
239
- const appUrl = getConfig('APP_URL');
240
- if (appUrl) env.push(`APP_URL=${appUrl}`);
241
- }
237
+ // Create per-container API key for agent-secrets access
238
+ const { createAgentJobApiKey } = await import('../db/api-keys.js');
239
+ const { key: agentJobToken } = createAgentJobApiKey(workspaceId || containerName);
240
+ env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
241
+ const appUrl = getConfig('APP_URL');
242
+ if (appUrl) env.push(`APP_URL=${appUrl}`);
242
243
  }
243
244
 
244
245
  const hostConfig = {};
@@ -399,7 +400,7 @@ function buildAgentAuthEnv(agent) {
399
400
  * @param {boolean} [options.injectSecrets] - Inject agent job secrets into container env
400
401
  * @returns {Promise<{containerId: string, containerName: string}>}
401
402
  */
402
- async function runHeadlessContainer({ containerName, repo, branch, featureBranch, workspaceId, taskPrompt, mode = 'plan', codingAgent, systemPrompt, continueSession = true, injectSecrets, agentJobToken }) {
403
+ async function runHeadlessContainer({ containerName, repo, branch, featureBranch, workspaceId, taskPrompt, mode = 'plan', codingAgent, systemPrompt, continueSession = true, injectSecrets }) {
403
404
  const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
404
405
  const version = process.env.THEPOPEBOT_VERSION;
405
406
  const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
@@ -446,11 +447,12 @@ async function runHeadlessContainer({ containerName, repo, branch, featureBranch
446
447
  if (jobSecrets.length > 0) {
447
448
  env.push(`AGENT_JOB_SECRETS=${JSON.stringify(Object.fromEntries(jobSecrets.map(s => [s.key, s.value])))}`);
448
449
  }
449
- if (agentJobToken) {
450
- env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
451
- const appUrl = getConfig('APP_URL');
452
- if (appUrl) env.push(`APP_URL=${appUrl}`);
453
- }
450
+ // Create per-container API key for agent-secrets access
451
+ const { createAgentJobApiKey } = await import('../db/api-keys.js');
452
+ const { key: agentJobToken } = createAgentJobApiKey(workspaceId || containerName);
453
+ env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
454
+ const appUrl = getConfig('APP_URL');
455
+ if (appUrl) env.push(`APP_URL=${appUrl}`);
454
456
  }
455
457
 
456
458
  const hostConfig = {};
@@ -861,7 +863,7 @@ async function removeVolume(name) {
861
863
  * @param {string} [options.llmModel] - Model override
862
864
  * @returns {Promise<{containerId: string, containerName: string, volumeName: string}>}
863
865
  */
864
- async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel, agentJobToken }) {
866
+ async function runAgentJobContainer({ agentJobId, repo, branch, title, description, codingAgent, llmModel }) {
865
867
  const agent = codingAgent || getConfig('CODING_AGENT') || 'claude-code';
866
868
  const version = process.env.THEPOPEBOT_VERSION;
867
869
  const image = `stephengpope/thepopebot:coding-agent-${agent}-${version}`;
@@ -899,8 +901,10 @@ async function runAgentJobContainer({ agentJobId, repo, branch, title, descripti
899
901
  const appUrl = getConfig('APP_URL');
900
902
  if (appUrl) env.push(`APP_URL=${appUrl}`);
901
903
 
902
- // Inject per-job API token for agent-secrets skill
903
- if (agentJobToken) env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
904
+ // Create per-container API key for agent-secrets access
905
+ const { createAgentJobApiKey } = await import('../db/api-keys.js');
906
+ const { key: agentJobToken } = createAgentJobApiKey(agentJobId);
907
+ env.push(`AGENT_JOB_TOKEN=${agentJobToken}`);
904
908
 
905
909
  // Inject agent job secrets (plain secrets as env vars; oauth types are null — agent must fetch via get)
906
910
  const { getAllAgentJobSecrets } = await import('../db/config.js');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "thepopebot",
3
- "version": "1.2.75-beta.6",
3
+ "version": "1.2.75-beta.7",
4
4
  "type": "module",
5
5
  "description": "Create autonomous AI agents with a two-layer architecture: Next.js Event Handler + Docker Agent.",
6
6
  "bin": {
@@ -7,14 +7,14 @@ description: List, get, or update agent secrets. Use get for OAuth credentials (
7
7
 
8
8
  ```bash
9
9
  # List available secrets (null = must fetch, plain = already in env)
10
- node skills/agent-job-secrets/agent-job-secrets.js
10
+ node skills/agent-job-secrets/agent-job-secrets.mjs
11
11
 
12
12
  # Get a secret value (OAuth credentials are auto-refreshed)
13
- node skills/agent-job-secrets/agent-job-secrets.js get MY_CREDENTIALS
13
+ node skills/agent-job-secrets/agent-job-secrets.mjs get MY_CREDENTIALS
14
14
 
15
15
  # Set/update a secret (plain string or piped value)
16
- node skills/agent-job-secrets/agent-job-secrets.js set MY_KEY "value"
17
- echo "$UPDATED_CREDENTIALS" | node skills/agent-job-secrets/agent-job-secrets.js set MY_KEY
16
+ node skills/agent-job-secrets/agent-job-secrets.mjs set MY_KEY "value"
17
+ echo "$UPDATED_CREDENTIALS" | node skills/agent-job-secrets/agent-job-secrets.mjs set MY_KEY
18
18
  ```
19
19
 
20
20
  ## Notes
@@ -18,9 +18,8 @@ if (!cmd || cmd === 'list') {
18
18
  console.log('Available secrets:');
19
19
  keys.forEach(k => {
20
20
  const fetchRequired = secrets[k] === null;
21
- console.log(` - ${k}${fetchRequired ? ' (fetch required: use get)' : ''}`);
21
+ console.log(` - ${k}${fetchRequired ? ' (use agent-job-secrets skill to fetch)' : ''}`);
22
22
  });
23
- console.log('\nTo get a value: agent-job-secrets.js get KEY_NAME');
24
23
  }
25
24
  process.exit(0);
26
25
  }
@@ -31,33 +30,43 @@ if (!apiKey) { console.error('AGENT_JOB_TOKEN not available'); process.exit(1);
31
30
  if (!appUrl) { console.error('APP_URL not available'); process.exit(1); }
32
31
 
33
32
  if (cmd === 'get') {
34
- if (!key) { console.error('Usage: agent-job-secrets.js get KEY_NAME'); process.exit(1); }
35
- const res = await fetch(`${appUrl}/api/get-agent-job-secret?key=${encodeURIComponent(key)}`, {
33
+ if (!key) { console.error('Usage: agent-job-secrets get KEY_NAME'); process.exit(1); }
34
+ const url = `${appUrl}/api/get-agent-job-secret?key=${encodeURIComponent(key)}`;
35
+ const res = await fetch(url, {
36
36
  headers: { 'x-api-key': apiKey },
37
37
  });
38
+ if (!res.ok) {
39
+ const body = await res.text();
40
+ console.error(`GET ${url} → ${res.status} ${body}`);
41
+ process.exit(1);
42
+ }
38
43
  const json = await res.json();
39
- if (!res.ok || json.error) { console.error('Failed:', json.error || res.status); process.exit(1); }
40
44
  console.log(json.value);
41
45
  process.exit(0);
42
46
  }
43
47
 
44
48
  if (cmd === 'set') {
45
49
  if (!key) {
46
- console.error('Usage: agent-job-secrets.js set KEY_NAME [value]');
47
- console.error(' echo "value" | agent-job-secrets.js set KEY_NAME');
50
+ console.error('Usage: agent-job-secrets set KEY_NAME [value]');
51
+ console.error(' echo "value" | agent-job-secrets set KEY_NAME');
48
52
  process.exit(1);
49
53
  }
50
54
  let value = inlineValue;
51
55
  if (value === undefined) {
52
56
  value = readFileSync('/dev/stdin', 'utf8').trim();
53
57
  }
54
- const res = await fetch(`${appUrl}/api/set-agent-job-secret`, {
58
+ const url = `${appUrl}/api/set-agent-job-secret`;
59
+ const res = await fetch(url, {
55
60
  method: 'POST',
56
61
  headers: { 'Content-Type': 'application/json', 'x-api-key': apiKey },
57
62
  body: JSON.stringify({ key, value }),
58
63
  });
64
+ if (!res.ok) {
65
+ const body = await res.text();
66
+ console.error(`POST ${url} → ${res.status} ${body}`);
67
+ process.exit(1);
68
+ }
59
69
  const json = await res.json();
60
- if (!res.ok || json.error) { console.error('Failed:', json.error || res.status); process.exit(1); }
61
70
  console.log(`Secret "${key}" updated.`);
62
71
  process.exit(0);
63
72
  }