@sentry/junior 0.4.1 → 0.6.0

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.
@@ -1,3 +1,7 @@
1
+ import {
2
+ discoverInstalledPluginPackageContent,
3
+ discoverProjectRoots
4
+ } from "./chunk-Z5E25LRN.js";
1
5
  import {
2
6
  logInfo,
3
7
  logWarn,
@@ -5,608 +9,231 @@ import {
5
9
  withSpan
6
10
  } from "./chunk-PY4AI2GZ.js";
7
11
  import {
8
- discoverInstalledPluginPackageContent,
9
- discoverProjectRoots
10
- } from "./chunk-Z5E25LRN.js";
12
+ parsePluginManifest
13
+ } from "./chunk-VW26MOSO.js";
11
14
 
12
- // src/chat/config.ts
13
- var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
14
- var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
15
- var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
16
- var TURN_TIMEOUT_BUFFER_SECONDS = 20;
17
- function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
18
- const value = Number.parseInt(rawValue ?? "", 10);
19
- if (Number.isNaN(value)) {
20
- return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
15
+ // src/chat/plugins/registry.ts
16
+ import { readFileSync, readdirSync, statSync } from "fs";
17
+ import path2 from "path";
18
+
19
+ // src/chat/home.ts
20
+ import fs from "fs";
21
+ import path from "path";
22
+ function homeDir() {
23
+ return resolveHomeDir();
24
+ }
25
+ function unique(values) {
26
+ return [...new Set(values)];
27
+ }
28
+ function pathExists(targetPath) {
29
+ try {
30
+ fs.accessSync(targetPath);
31
+ return true;
32
+ } catch {
33
+ return false;
21
34
  }
22
- return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
23
35
  }
24
- function resolveQueueCallbackMaxDurationSeconds() {
25
- const value = Number.parseInt(process.env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "", 10);
26
- if (Number.isNaN(value) || value <= 0) {
27
- return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
36
+ function hasAnyDataMarkers(appDir) {
37
+ return pathExists(path.join(appDir, "SOUL.md")) || pathExists(path.join(appDir, "ABOUT.md"));
38
+ }
39
+ function scoreAppCandidate(appDir) {
40
+ let score = 0;
41
+ if (pathExists(path.join(appDir, "SOUL.md"))) {
42
+ score += 4;
28
43
  }
29
- return value;
44
+ if (pathExists(path.join(appDir, "ABOUT.md"))) {
45
+ score += 2;
46
+ }
47
+ if (pathExists(path.join(appDir, "skills"))) {
48
+ score += 1;
49
+ }
50
+ if (pathExists(path.join(appDir, "plugins"))) {
51
+ score += 1;
52
+ }
53
+ return score;
30
54
  }
31
- function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
32
- const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
33
- return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
55
+ function resolveCandidateAppDirs(cwd, projectRoots) {
56
+ const roots = projectRoots ?? discoverProjectRoots(cwd);
57
+ const resolved = [];
58
+ const seen = /* @__PURE__ */ new Set();
59
+ for (const root of roots) {
60
+ const appDir = path.resolve(root, "app");
61
+ if (!pathExists(appDir)) {
62
+ continue;
63
+ }
64
+ if (seen.has(appDir)) {
65
+ continue;
66
+ }
67
+ seen.add(appDir);
68
+ resolved.push(appDir);
69
+ }
70
+ return resolved;
34
71
  }
35
- function buildBotConfig() {
36
- const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds();
37
- const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds);
38
- return {
39
- userName: process.env.JUNIOR_BOT_NAME ?? "junior",
40
- modelId: process.env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
41
- fastModelId: process.env.AI_FAST_MODEL ?? process.env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
42
- turnTimeoutMs: parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs)
43
- };
72
+ function resolveHomeDir(cwd = process.cwd(), options) {
73
+ const resolvedCwd = path.resolve(cwd);
74
+ const directApp = path.resolve(resolvedCwd, "app");
75
+ if (pathExists(directApp) && hasAnyDataMarkers(directApp)) {
76
+ return directApp;
77
+ }
78
+ const candidates = resolveCandidateAppDirs(
79
+ resolvedCwd,
80
+ options?.projectRoots
81
+ );
82
+ if (candidates.length === 0) {
83
+ return directApp;
84
+ }
85
+ candidates.sort((left, right) => {
86
+ const leftScore = scoreAppCandidate(left);
87
+ const rightScore = scoreAppCandidate(right);
88
+ if (leftScore !== rightScore) {
89
+ return rightScore - leftScore;
90
+ }
91
+ const leftDistance = path.relative(resolvedCwd, left).split(path.sep).length;
92
+ const rightDistance = path.relative(resolvedCwd, right).split(path.sep).length;
93
+ if (leftDistance !== rightDistance) {
94
+ return leftDistance - rightDistance;
95
+ }
96
+ return left.localeCompare(right);
97
+ });
98
+ return candidates[0];
44
99
  }
45
- var botConfig = buildBotConfig();
46
- function toOptionalTrimmed(value) {
47
- if (!value) {
48
- return void 0;
100
+ function resolveContentRoots(subdir) {
101
+ if (subdir === "data") {
102
+ return [homeDir()];
49
103
  }
50
- const trimmed = value.trim();
51
- return trimmed.length > 0 ? trimmed : void 0;
104
+ return [path.join(homeDir(), subdir)];
52
105
  }
53
- function getSlackBotToken() {
54
- return toOptionalTrimmed(process.env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(process.env.SLACK_BOT_USER_TOKEN);
106
+ function dataRoots() {
107
+ return unique(resolveContentRoots("data"));
55
108
  }
56
- function getSlackSigningSecret() {
57
- return toOptionalTrimmed(process.env.SLACK_SIGNING_SECRET);
109
+ function skillRoots() {
110
+ return unique(resolveContentRoots("skills"));
58
111
  }
59
- function getSlackClientId() {
60
- return toOptionalTrimmed(process.env.SLACK_CLIENT_ID);
112
+ function pluginRoots() {
113
+ return unique(resolveContentRoots("plugins"));
61
114
  }
62
- function getSlackClientSecret() {
63
- return toOptionalTrimmed(process.env.SLACK_CLIENT_SECRET);
115
+ function soulPathCandidates() {
116
+ const candidates = dataRoots().map((root) => path.join(root, "SOUL.md"));
117
+ return unique(candidates);
64
118
  }
65
- function hasRedisConfig() {
66
- return Boolean(process.env.REDIS_URL);
119
+ function aboutPathCandidates() {
120
+ const candidates = dataRoots().map((root) => path.join(root, "ABOUT.md"));
121
+ return unique(candidates);
67
122
  }
68
123
 
69
- // src/chat/state.ts
70
- import { createRedisState } from "@chat-adapter/state-redis";
71
- import { createMemoryState } from "@chat-adapter/state-memory";
72
- var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
73
- var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
74
- var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
75
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
76
- var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
77
- var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
78
- var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
79
- var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
80
- var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
81
- local key = KEYS[1]
82
- local nowMs = tonumber(ARGV[1])
83
- local ttlMs = tonumber(ARGV[2])
84
- local payload = ARGV[3]
85
- local current = redis.call("get", key)
86
-
87
- if not current then
88
- redis.call("set", key, payload, "PX", ttlMs)
89
- return 1
90
- end
91
-
92
- local ok, parsed = pcall(cjson.decode, current)
93
- if not ok or type(parsed) ~= "table" then
94
- return 0
95
- end
96
-
97
- local status = parsed["status"]
98
- if status == "failed" then
99
- redis.call("set", key, payload, "PX", ttlMs)
100
- return 3
101
- end
102
- if status ~= "processing" then
103
- return 0
104
- end
105
-
106
- local updatedAtMs = tonumber(parsed["updatedAtMs"])
107
- if not updatedAtMs then
108
- return 0
109
- end
110
-
111
- if updatedAtMs + ttlMs < nowMs then
112
- redis.call("set", key, payload, "PX", ttlMs)
113
- return 2
114
- end
115
-
116
- return 0
117
- `;
118
- var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
119
- local key = KEYS[1]
120
- local ownerToken = ARGV[1]
121
- local ttlMs = tonumber(ARGV[2])
122
- local payload = ARGV[3]
123
- local current = redis.call("get", key)
124
-
125
- if not current then
126
- return 0
127
- end
128
-
129
- local ok, parsed = pcall(cjson.decode, current)
130
- if not ok or type(parsed) ~= "table" then
131
- return 0
132
- end
133
-
134
- local currentOwner = parsed["ownerToken"]
135
- local status = parsed["status"]
136
- if currentOwner ~= ownerToken then
137
- return 0
138
- end
139
- if status ~= "processing" then
140
- return 0
141
- end
124
+ // src/chat/plugins/github-app-broker.ts
125
+ import { createPrivateKey, createSign, randomUUID } from "crypto";
142
126
 
143
- redis.call("set", key, payload, "PX", ttlMs)
144
- return 1
145
- `;
146
- function createQueuedStateAdapter(base) {
147
- const acquireLock = async (threadId, ttlMs) => {
148
- const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
149
- const lock = await base.acquireLock(threadId, effectiveTtlMs);
150
- return lock;
151
- };
152
- return {
153
- connect: () => base.connect(),
154
- disconnect: () => base.disconnect(),
155
- subscribe: (threadId) => base.subscribe(threadId),
156
- unsubscribe: (threadId) => base.unsubscribe(threadId),
157
- isSubscribed: (threadId) => base.isSubscribed(threadId),
158
- acquireLock,
159
- releaseLock: (lock) => base.releaseLock(lock),
160
- extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
161
- get: (key) => base.get(key),
162
- set: (key, value, ttlMs) => base.set(key, value, ttlMs),
163
- setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
164
- delete: (key) => base.delete(key)
165
- };
127
+ // src/chat/plugins/auth-token-placeholder.ts
128
+ var DEFAULT_PLACEHOLDERS = {
129
+ "oauth-bearer": "host_managed_credential",
130
+ "github-app": "ghp_host_managed_credential"
131
+ };
132
+ function resolveAuthTokenPlaceholder(credentials) {
133
+ return credentials.authTokenPlaceholder?.trim() || DEFAULT_PLACEHOLDERS[credentials.type];
166
134
  }
167
- function createStateAdapter() {
168
- if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
169
- _redisStateAdapter = void 0;
170
- return createQueuedStateAdapter(createMemoryState());
135
+
136
+ // src/chat/plugins/github-app-broker.ts
137
+ var MAX_LEASE_MS = 60 * 60 * 1e3;
138
+ function normalizeTargetScope(target) {
139
+ const owner = target?.owner?.trim().toLowerCase();
140
+ const repo = target?.repo?.trim().toLowerCase();
141
+ if (!owner || !repo) {
142
+ return "all";
171
143
  }
172
- if (!hasRedisConfig()) {
173
- throw new Error("REDIS_URL is required for durable Slack thread state");
174
- }
175
- const redisState = createRedisState({
176
- url: process.env.REDIS_URL
177
- });
178
- _redisStateAdapter = redisState;
179
- return createQueuedStateAdapter(redisState);
180
- }
181
- var _stateAdapter;
182
- var _redisStateAdapter;
183
- function getRedisStateAdapter() {
184
- if (!_redisStateAdapter) {
185
- getStateAdapter();
186
- }
187
- if (!_redisStateAdapter) {
188
- throw new Error("Redis state adapter is unavailable for this runtime");
189
- }
190
- return _redisStateAdapter;
144
+ return `${owner}/${repo}`;
191
145
  }
192
- function queueMessageKey(rawKey) {
193
- return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
146
+ function base64Url(input) {
147
+ return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
194
148
  }
195
- function parseQueueMessageState(value) {
196
- if (typeof value !== "string") {
197
- return void 0;
149
+ function normalizePrivateKey(raw) {
150
+ let normalized = raw.trim();
151
+ if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
152
+ normalized = normalized.slice(1, -1);
198
153
  }
199
- try {
200
- const parsed = JSON.parse(value);
201
- if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
202
- return void 0;
154
+ normalized = normalized.replace(/\r\n/g, "\n");
155
+ if (normalized.includes("\\n")) {
156
+ normalized = normalized.replace(/\\n/g, "\n");
157
+ }
158
+ if (!normalized.includes("-----BEGIN")) {
159
+ try {
160
+ const decoded = Buffer.from(normalized, "base64").toString("utf8").trim();
161
+ if (decoded.includes("-----BEGIN")) {
162
+ normalized = decoded;
163
+ }
164
+ } catch {
203
165
  }
204
- return {
205
- status: parsed.status,
206
- updatedAtMs: parsed.updatedAtMs,
207
- ...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
208
- ...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
209
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
210
- };
211
- } catch {
212
- return void 0;
213
166
  }
167
+ return normalized;
214
168
  }
215
- function agentTurnSessionKey(conversationId, sessionId) {
216
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
217
- }
218
- function isRecord(value) {
219
- return typeof value === "object" && value !== null;
220
- }
221
- function parseAgentTurnSessionCheckpoint(value) {
222
- if (typeof value !== "string") {
223
- return void 0;
169
+ function getPrivateKey(envName) {
170
+ const raw = process.env[envName];
171
+ if (!raw) {
172
+ throw new Error(`Missing ${envName}`);
224
173
  }
174
+ const normalized = normalizePrivateKey(raw);
175
+ let key;
225
176
  try {
226
- const parsed = JSON.parse(value);
227
- if (!isRecord(parsed)) {
228
- return void 0;
229
- }
230
- const status = parsed.state;
231
- if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
232
- return void 0;
233
- }
234
- const conversationId = parsed.conversationId;
235
- const sessionId = parsed.sessionId;
236
- const sliceId = parsed.sliceId;
237
- const checkpointVersion = parsed.checkpointVersion;
238
- const updatedAtMs = parsed.updatedAtMs;
239
- if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
240
- return void 0;
241
- }
242
- return {
243
- checkpointVersion,
244
- conversationId,
245
- sessionId,
246
- sliceId,
247
- state: status,
248
- updatedAtMs,
249
- piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
250
- ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
251
- ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
252
- };
177
+ key = createPrivateKey({ key: normalized, format: "pem" });
253
178
  } catch {
254
- return void 0;
255
- }
256
- }
257
- function getStateAdapter() {
258
- if (!_stateAdapter) {
259
- _stateAdapter = createStateAdapter();
260
- }
261
- return _stateAdapter;
262
- }
263
- async function disconnectStateAdapter() {
264
- if (!_stateAdapter) {
265
- return;
179
+ throw new Error(
180
+ `Invalid ${envName}: expected a PEM-encoded RSA private key (raw PEM, escaped newlines, or base64-encoded PEM)`
181
+ );
266
182
  }
267
- try {
268
- await _stateAdapter.disconnect();
269
- } finally {
270
- _stateAdapter = void 0;
271
- _redisStateAdapter = void 0;
183
+ if (key.asymmetricKeyType !== "rsa") {
184
+ throw new Error(
185
+ `Invalid ${envName}: GitHub App signing requires an RSA private key`
186
+ );
272
187
  }
188
+ return key;
273
189
  }
274
- async function claimQueueIngressDedup(rawKey, ttlMs) {
275
- await getStateAdapter().connect();
276
- const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
277
- const result = await getRedisStateAdapter().getClient().set(key, "1", {
278
- NX: true,
279
- PX: ttlMs
280
- });
281
- return result === "OK";
282
- }
283
- async function hasQueueIngressDedup(rawKey) {
284
- await getStateAdapter().connect();
285
- const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
286
- const value = await getRedisStateAdapter().getClient().get(key);
287
- return typeof value === "string" && value.length > 0;
288
- }
289
- async function getQueueMessageProcessingState(rawKey) {
290
- await getStateAdapter().connect();
291
- const state = await getStateAdapter().get(queueMessageKey(rawKey));
292
- return parseQueueMessageState(state);
190
+ function createAppJwt(appId, privateKeyEnv) {
191
+ const now = Math.floor(Date.now() / 1e3);
192
+ const header = { alg: "RS256", typ: "JWT" };
193
+ const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
194
+ const encodedHeader = base64Url(JSON.stringify(header));
195
+ const encodedPayload = base64Url(JSON.stringify(payload));
196
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
197
+ const signer = createSign("RSA-SHA256");
198
+ signer.update(signingInput);
199
+ signer.end();
200
+ const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
201
+ return `${signingInput}.${signature}`;
293
202
  }
294
- async function acquireQueueMessageProcessingOwnership(args) {
295
- await getStateAdapter().connect();
296
- const key = queueMessageKey(args.rawKey);
297
- const nowMs = Date.now();
298
- const payload = JSON.stringify({
299
- status: "processing",
300
- updatedAtMs: nowMs,
301
- ownerToken: args.ownerToken,
302
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
303
- });
304
- const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
305
- keys: [key],
306
- arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
203
+ async function githubRequest(apiBase, path3, params) {
204
+ const response = await fetch(`${apiBase}${path3}`, {
205
+ method: params.method ?? "GET",
206
+ headers: {
207
+ Accept: "application/vnd.github+json",
208
+ Authorization: `Bearer ${params.token}`,
209
+ "X-GitHub-Api-Version": "2022-11-28",
210
+ ...params.body ? { "Content-Type": "application/json" } : {}
211
+ },
212
+ ...params.body ? { body: JSON.stringify(params.body) } : {}
307
213
  });
308
- if (result === 1) {
309
- return "acquired";
310
- }
311
- if (result === 2) {
312
- return "reclaimed";
214
+ const text = await response.text();
215
+ let parsed = void 0;
216
+ if (text) {
217
+ try {
218
+ parsed = JSON.parse(text);
219
+ } catch {
220
+ parsed = void 0;
221
+ }
313
222
  }
314
- if (result === 3) {
315
- return "recovered";
223
+ if (!response.ok) {
224
+ const message = parsed && typeof parsed === "object" && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
225
+ throw new Error(message);
316
226
  }
317
- return "blocked";
318
- }
319
- async function refreshQueueMessageProcessingOwnership(args) {
320
- await getStateAdapter().connect();
321
- const nowMs = Date.now();
322
- const payload = JSON.stringify({
323
- status: "processing",
324
- updatedAtMs: nowMs,
325
- ownerToken: args.ownerToken,
326
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
327
- });
328
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
329
- keys: [queueMessageKey(args.rawKey)],
330
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
331
- });
332
- return result === 1;
333
- }
334
- async function completeQueueMessageProcessingOwnership(args) {
335
- await getStateAdapter().connect();
336
- const payload = JSON.stringify({
337
- status: "completed",
338
- updatedAtMs: Date.now(),
339
- ownerToken: args.ownerToken,
340
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
341
- });
342
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
343
- keys: [queueMessageKey(args.rawKey)],
344
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
345
- });
346
- return result === 1;
347
- }
348
- async function failQueueMessageProcessingOwnership(args) {
349
- await getStateAdapter().connect();
350
- const payload = JSON.stringify({
351
- status: "failed",
352
- updatedAtMs: Date.now(),
353
- ownerToken: args.ownerToken,
354
- errorMessage: args.errorMessage,
355
- ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
356
- });
357
- const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
358
- keys: [queueMessageKey(args.rawKey)],
359
- arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
360
- });
361
- return result === 1;
227
+ return parsed;
362
228
  }
363
- async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
364
- await getStateAdapter().connect();
365
- const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
366
- return parseAgentTurnSessionCheckpoint(value);
367
- }
368
- async function upsertAgentTurnSessionCheckpoint(args) {
369
- await getStateAdapter().connect();
370
- const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
371
- const checkpoint = {
372
- checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
373
- conversationId: args.conversationId,
374
- sessionId: args.sessionId,
375
- sliceId: args.sliceId,
376
- state: args.state,
377
- updatedAtMs: Date.now(),
378
- piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
379
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
380
- ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
381
- };
382
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
383
- await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
384
- return checkpoint;
385
- }
386
-
387
- // src/chat/sandbox/runtime-dependency-snapshots.ts
388
- import { createHash } from "crypto";
389
- import { Sandbox } from "@vercel/sandbox";
390
-
391
- // src/chat/plugins/registry.ts
392
- import { readFileSync, readdirSync, statSync } from "fs";
393
- import path2 from "path";
394
- import { parse as parseYaml } from "yaml";
395
-
396
- // src/chat/home.ts
397
- import fs from "fs";
398
- import path from "path";
399
- function homeDir() {
400
- return resolveHomeDir();
401
- }
402
- function unique(values) {
403
- return [...new Set(values)];
404
- }
405
- function pathExists(targetPath) {
406
- try {
407
- fs.accessSync(targetPath);
408
- return true;
409
- } catch {
410
- return false;
411
- }
412
- }
413
- function hasAnyDataMarkers(appDir) {
414
- return pathExists(path.join(appDir, "SOUL.md"));
415
- }
416
- function scoreAppCandidate(appDir) {
417
- let score = 0;
418
- if (pathExists(path.join(appDir, "SOUL.md"))) {
419
- score += 4;
420
- }
421
- if (pathExists(path.join(appDir, "ABOUT.md"))) {
422
- score += 2;
423
- }
424
- if (pathExists(path.join(appDir, "skills"))) {
425
- score += 1;
426
- }
427
- if (pathExists(path.join(appDir, "plugins"))) {
428
- score += 1;
429
- }
430
- return score;
431
- }
432
- function resolveCandidateAppDirs(cwd, projectRoots) {
433
- const roots = projectRoots ?? discoverProjectRoots(cwd);
434
- const resolved = [];
435
- const seen = /* @__PURE__ */ new Set();
436
- for (const root of roots) {
437
- const appDir = path.resolve(root, "app");
438
- if (!pathExists(appDir)) {
439
- continue;
440
- }
441
- if (seen.has(appDir)) {
442
- continue;
443
- }
444
- seen.add(appDir);
445
- resolved.push(appDir);
446
- }
447
- return resolved;
448
- }
449
- function resolveHomeDir(cwd = process.cwd(), options) {
450
- const resolvedCwd = path.resolve(cwd);
451
- const directApp = path.resolve(resolvedCwd, "app");
452
- if (pathExists(directApp) && hasAnyDataMarkers(directApp)) {
453
- return directApp;
454
- }
455
- const candidates = resolveCandidateAppDirs(resolvedCwd, options?.projectRoots);
456
- if (candidates.length === 0) {
457
- return directApp;
458
- }
459
- candidates.sort((left, right) => {
460
- const leftScore = scoreAppCandidate(left);
461
- const rightScore = scoreAppCandidate(right);
462
- if (leftScore !== rightScore) {
463
- return rightScore - leftScore;
464
- }
465
- const leftDistance = path.relative(resolvedCwd, left).split(path.sep).length;
466
- const rightDistance = path.relative(resolvedCwd, right).split(path.sep).length;
467
- if (leftDistance !== rightDistance) {
468
- return leftDistance - rightDistance;
469
- }
470
- return left.localeCompare(right);
471
- });
472
- return candidates[0];
473
- }
474
- function resolveContentRoots(subdir) {
475
- if (subdir === "data") {
476
- return [homeDir()];
477
- }
478
- if (subdir === "skills") {
479
- return [path.join(homeDir(), "skills"), path.resolve(process.cwd(), "skills")];
480
- }
481
- return [path.join(homeDir(), "plugins")];
482
- }
483
- function dataRoots() {
484
- return unique(resolveContentRoots("data"));
485
- }
486
- function skillRoots() {
487
- return unique(resolveContentRoots("skills"));
488
- }
489
- function pluginRoots() {
490
- return unique(resolveContentRoots("plugins"));
491
- }
492
- function soulPathCandidates() {
493
- const candidates = dataRoots().map((root) => path.join(root, "SOUL.md"));
494
- return unique(candidates);
495
- }
496
-
497
- // src/chat/plugins/github-app-broker.ts
498
- import { createPrivateKey, createSign, randomUUID } from "crypto";
499
-
500
- // src/chat/plugins/auth-token-placeholder.ts
501
- var DEFAULT_PLACEHOLDERS = {
502
- "oauth-bearer": "host_managed_credential",
503
- "github-app": "ghp_host_managed_credential"
504
- };
505
- function resolveAuthTokenPlaceholder(credentials) {
506
- return credentials.authTokenPlaceholder?.trim() || DEFAULT_PLACEHOLDERS[credentials.type];
507
- }
508
-
509
- // src/chat/plugins/github-app-broker.ts
510
- var MAX_LEASE_MS = 60 * 60 * 1e3;
511
- function normalizeTargetScope(target) {
512
- const owner = target?.owner?.trim().toLowerCase();
513
- const repo = target?.repo?.trim().toLowerCase();
514
- if (!owner || !repo) {
515
- return "all";
516
- }
517
- return `${owner}/${repo}`;
518
- }
519
- function base64Url(input) {
520
- return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
521
- }
522
- function normalizePrivateKey(raw) {
523
- let normalized = raw.trim();
524
- if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
525
- normalized = normalized.slice(1, -1);
526
- }
527
- normalized = normalized.replace(/\r\n/g, "\n");
528
- if (normalized.includes("\\n")) {
529
- normalized = normalized.replace(/\\n/g, "\n");
530
- }
531
- if (!normalized.includes("-----BEGIN")) {
532
- try {
533
- const decoded = Buffer.from(normalized, "base64").toString("utf8").trim();
534
- if (decoded.includes("-----BEGIN")) {
535
- normalized = decoded;
536
- }
537
- } catch {
538
- }
539
- }
540
- return normalized;
541
- }
542
- function getPrivateKey(envName) {
543
- const raw = process.env[envName];
544
- if (!raw) {
545
- throw new Error(`Missing ${envName}`);
546
- }
547
- const normalized = normalizePrivateKey(raw);
548
- let key;
549
- try {
550
- key = createPrivateKey({ key: normalized, format: "pem" });
551
- } catch {
552
- throw new Error(
553
- `Invalid ${envName}: expected a PEM-encoded RSA private key (raw PEM, escaped newlines, or base64-encoded PEM)`
554
- );
555
- }
556
- if (key.asymmetricKeyType !== "rsa") {
557
- throw new Error(
558
- `Invalid ${envName}: GitHub App signing requires an RSA private key`
559
- );
560
- }
561
- return key;
562
- }
563
- function createAppJwt(appId, privateKeyEnv) {
564
- const now = Math.floor(Date.now() / 1e3);
565
- const header = { alg: "RS256", typ: "JWT" };
566
- const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
567
- const encodedHeader = base64Url(JSON.stringify(header));
568
- const encodedPayload = base64Url(JSON.stringify(payload));
569
- const signingInput = `${encodedHeader}.${encodedPayload}`;
570
- const signer = createSign("RSA-SHA256");
571
- signer.update(signingInput);
572
- signer.end();
573
- const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
574
- return `${signingInput}.${signature}`;
575
- }
576
- async function githubRequest(apiBase, path3, params) {
577
- const response = await fetch(`${apiBase}${path3}`, {
578
- method: params.method ?? "GET",
579
- headers: {
580
- Accept: "application/vnd.github+json",
581
- Authorization: `Bearer ${params.token}`,
582
- "X-GitHub-Api-Version": "2022-11-28",
583
- ...params.body ? { "Content-Type": "application/json" } : {}
584
- },
585
- ...params.body ? { body: JSON.stringify(params.body) } : {}
586
- });
587
- const text = await response.text();
588
- let parsed = void 0;
589
- if (text) {
590
- try {
591
- parsed = JSON.parse(text);
592
- } catch {
593
- parsed = void 0;
594
- }
595
- }
596
- if (!response.ok) {
597
- const message = parsed && typeof parsed === "object" && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
598
- throw new Error(message);
599
- }
600
- return parsed;
601
- }
602
- function capabilityToPermissions(capability, pluginName) {
603
- if (capability === `${pluginName}.issues.read`) {
604
- return { issues: "read" };
605
- }
606
- if (capability === `${pluginName}.issues.write` || capability === `${pluginName}.issues.comment` || capability === `${pluginName}.labels.write`) {
607
- return { issues: "write" };
608
- }
609
- throw new Error(`Unsupported GitHub capability: ${capability}`);
229
+ function capabilityToPermissions(capability, pluginName) {
230
+ if (capability === `${pluginName}.issues.read`) {
231
+ return { issues: "read" };
232
+ }
233
+ if (capability === `${pluginName}.issues.write` || capability === `${pluginName}.issues.comment` || capability === `${pluginName}.labels.write`) {
234
+ return { issues: "write" };
235
+ }
236
+ throw new Error(`Unsupported GitHub capability: ${capability}`);
610
237
  }
611
238
  function createGitHubAppBroker(manifest, credentials) {
612
239
  const tokenCache = /* @__PURE__ */ new Map();
@@ -935,494 +562,23 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
935
562
  }
936
563
 
937
564
  // src/chat/plugins/registry.ts
938
- var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
939
- var SHORT_CAPABILITY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
940
- var SHORT_CONFIG_KEY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
941
- var AUTH_TOKEN_ENV_RE = /^[A-Z][A-Z0-9_]*$/;
942
- var API_DOMAIN_RE = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
943
- var RUNTIME_POSTINSTALL_CMD_RE = /^[A-Za-z0-9._/-]+$/;
944
- var RESERVED_AUTHORIZE_PARAM_KEYS = /* @__PURE__ */ new Set([
945
- "client_id",
946
- "scope",
947
- "state",
948
- "redirect_uri",
949
- "response_type"
950
- ]);
951
- var FORBIDDEN_API_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
952
- var FORBIDDEN_TOKEN_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
953
- function toRecord(value, errorMessage) {
954
- if (!value || typeof value !== "object" || Array.isArray(value)) {
955
- throw new Error(errorMessage);
565
+ var pluginDefinitions = [];
566
+ var capabilityToPlugin = /* @__PURE__ */ new Map();
567
+ var pluginConfigKeys = /* @__PURE__ */ new Set();
568
+ var pluginsByName = /* @__PURE__ */ new Map();
569
+ var packageSkillRoots = /* @__PURE__ */ new Set();
570
+ var pluginsLoaded = false;
571
+ function registerPluginManifest(raw, pluginDir) {
572
+ const manifest = parsePluginManifest(raw, pluginDir);
573
+ if (pluginsByName.has(manifest.name)) {
574
+ return;
956
575
  }
957
- return value;
958
- }
959
- function requireStringField(record, field, errorMessage) {
960
- const value = record[field];
961
- if (typeof value !== "string" || !value.trim()) {
962
- throw new Error(errorMessage);
963
- }
964
- return value.trim();
965
- }
966
- function requireEnvVarField(record, field, pluginName) {
967
- const value = requireStringField(
968
- record,
969
- field,
970
- `Plugin ${pluginName} ${field} must be a non-empty string`
971
- );
972
- if (!AUTH_TOKEN_ENV_RE.test(value)) {
973
- throw new Error(
974
- `Plugin ${pluginName} ${field} must be an uppercase env var name`
975
- );
976
- }
977
- return value;
978
- }
979
- function requireHttpsUrlField(record, field, pluginName) {
980
- const value = requireStringField(
981
- record,
982
- field,
983
- `Plugin ${pluginName} oauth.${field} must be a non-empty string`
984
- );
985
- let parsed;
986
- try {
987
- parsed = new URL(value);
988
- } catch {
989
- throw new Error(`Plugin ${pluginName} oauth.${field} must be a valid URL`);
990
- }
991
- if (parsed.protocol !== "https:") {
992
- throw new Error(`Plugin ${pluginName} oauth.${field} must use https`);
993
- }
994
- return value;
995
- }
996
- function normalizeApiDomain(rawDomain, name) {
997
- const domain = typeof rawDomain === "string" ? rawDomain.trim().toLowerCase() : "";
998
- if (!domain) {
999
- throw new Error(
1000
- `Plugin ${name} credentials.api-domains entries must be non-empty strings`
1001
- );
1002
- }
1003
- if (!API_DOMAIN_RE.test(domain)) {
1004
- throw new Error(
1005
- `Plugin ${name} credentials.api-domains entries must be valid domain names`
1006
- );
1007
- }
1008
- return domain;
1009
- }
1010
- function parseStringMap(data, errorLabel, options = {}) {
1011
- if (data === void 0) {
1012
- return void 0;
1013
- }
1014
- const record = toRecord(
1015
- data,
1016
- `${errorLabel} must be an object when provided`
1017
- );
1018
- const entries = Object.entries(record);
1019
- if (entries.length === 0) {
1020
- return void 0;
1021
- }
1022
- const result = {};
1023
- const seen = /* @__PURE__ */ new Set();
1024
- for (const [rawKey, rawValue] of entries) {
1025
- const key = rawKey.trim();
1026
- if (!key) {
1027
- throw new Error(`${errorLabel} keys must be non-empty strings`);
1028
- }
1029
- if (typeof rawValue !== "string" || !rawValue.trim()) {
1030
- throw new Error(`${errorLabel}.${key} must be a non-empty string`);
1031
- }
1032
- const normalizedKey = key.toLowerCase();
1033
- if (options.reservedKeys?.has(normalizedKey)) {
1034
- throw new Error(`${errorLabel}.${key} is reserved by the runtime`);
1035
- }
1036
- if (options.forbiddenKeys?.has(normalizedKey)) {
1037
- throw new Error(`${errorLabel}.${key} is not allowed`);
1038
- }
1039
- if (seen.has(normalizedKey)) {
1040
- throw new Error(`${errorLabel}.${key} is duplicated`);
1041
- }
1042
- seen.add(normalizedKey);
1043
- result[key] = rawValue.trim();
1044
- }
1045
- return Object.keys(result).length > 0 ? result : void 0;
1046
- }
1047
- function parseBaseCredentialFields(data, name) {
1048
- const rawDomains = data["api-domains"];
1049
- if (!Array.isArray(rawDomains) || rawDomains.length === 0) {
1050
- throw new Error(
1051
- `Plugin ${name} credentials.api-domains must be a non-empty array of strings`
1052
- );
1053
- }
1054
- const apiDomains = rawDomains.map(
1055
- (rawDomain) => normalizeApiDomain(rawDomain, name)
1056
- );
1057
- const apiHeaders = parseStringMap(
1058
- data["api-headers"],
1059
- `Plugin ${name} credentials.api-headers`,
1060
- { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
1061
- );
1062
- const authTokenEnv = requireEnvVarField(data, "auth-token-env", name);
1063
- const authTokenPlaceholderRaw = data["auth-token-placeholder"];
1064
- if (authTokenPlaceholderRaw !== void 0 && (typeof authTokenPlaceholderRaw !== "string" || !authTokenPlaceholderRaw.trim())) {
1065
- throw new Error(
1066
- `Plugin ${name} credentials.auth-token-placeholder must be a non-empty string when provided`
1067
- );
1068
- }
1069
- return {
1070
- apiDomains,
1071
- ...apiHeaders ? { apiHeaders } : {},
1072
- authTokenEnv,
1073
- ...typeof authTokenPlaceholderRaw === "string" ? { authTokenPlaceholder: authTokenPlaceholderRaw.trim() } : {}
1074
- };
1075
- }
1076
- function parseCredentials(data, name) {
1077
- const type = data.type;
1078
- if (type === "oauth-bearer") {
1079
- const base = parseBaseCredentialFields(data, name);
1080
- return { type: "oauth-bearer", ...base };
1081
- }
1082
- if (type === "github-app") {
1083
- const base = parseBaseCredentialFields(data, name);
1084
- const appIdEnv = requireEnvVarField(data, "app-id-env", name);
1085
- const privateKeyEnv = requireEnvVarField(data, "private-key-env", name);
1086
- const installationIdEnv = requireEnvVarField(
1087
- data,
1088
- "installation-id-env",
1089
- name
1090
- );
1091
- return {
1092
- type: "github-app",
1093
- ...base,
1094
- appIdEnv,
1095
- privateKeyEnv,
1096
- installationIdEnv
1097
- };
1098
- }
1099
- throw new Error(`Plugin ${name} has unsupported credentials.type: "${type}"`);
1100
- }
1101
- function parseRuntimeDependencies(data, name) {
1102
- if (data === void 0) {
1103
- return void 0;
1104
- }
1105
- if (!Array.isArray(data)) {
1106
- throw new Error(`Plugin ${name} runtime-dependencies must be an array`);
1107
- }
1108
- const parsed = [];
1109
- const seen = /* @__PURE__ */ new Set();
1110
- for (const entry of data) {
1111
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1112
- throw new Error(
1113
- `Plugin ${name} runtime-dependencies entries must be objects`
1114
- );
1115
- }
1116
- const record = entry;
1117
- const type = record.type;
1118
- const packageName = record.package;
1119
- const packageUrl = record.url;
1120
- const version = record.version;
1121
- const sha256 = record.sha256;
1122
- if (typeof type !== "string" || type !== "npm" && type !== "system") {
1123
- throw new Error(
1124
- `Plugin ${name} runtime dependency type must be "npm" or "system"`
1125
- );
1126
- }
1127
- const normalizedPackage = typeof packageName === "string" ? packageName.trim() : "";
1128
- const normalizedUrl = typeof packageUrl === "string" ? packageUrl.trim() : "";
1129
- if (type === "npm") {
1130
- if (!normalizedPackage) {
1131
- throw new Error(
1132
- `Plugin ${name} runtime dependency package must be a non-empty string`
1133
- );
1134
- }
1135
- if (packageUrl !== void 0 || sha256 !== void 0) {
1136
- throw new Error(
1137
- `Plugin ${name} npm runtime dependencies must only include package/version fields`
1138
- );
1139
- }
1140
- const normalizedVersion = typeof version === "string" ? version.trim() : "latest";
1141
- if (!normalizedVersion) {
1142
- throw new Error(
1143
- `Plugin ${name} runtime dependency version must be a non-empty string when provided`
1144
- );
1145
- }
1146
- const dedupeKey2 = `${type}:${normalizedPackage}:${normalizedVersion}`;
1147
- if (seen.has(dedupeKey2)) {
1148
- continue;
1149
- }
1150
- seen.add(dedupeKey2);
1151
- parsed.push({
1152
- type: "npm",
1153
- package: normalizedPackage,
1154
- version: normalizedVersion
1155
- });
1156
- continue;
1157
- }
1158
- if (version !== void 0) {
1159
- throw new Error(
1160
- `Plugin ${name} system runtime dependencies must not include a version`
1161
- );
1162
- }
1163
- if (normalizedPackage && normalizedUrl) {
1164
- throw new Error(
1165
- `Plugin ${name} system runtime dependencies must specify either package or url, not both`
1166
- );
1167
- }
1168
- if (!normalizedPackage && !normalizedUrl) {
1169
- throw new Error(
1170
- `Plugin ${name} system runtime dependencies must specify package or url`
1171
- );
1172
- }
1173
- if (normalizedPackage) {
1174
- if (sha256 !== void 0) {
1175
- throw new Error(
1176
- `Plugin ${name} system runtime dependency package entries must not include sha256`
1177
- );
1178
- }
1179
- const dedupeKey2 = `${type}:package:${normalizedPackage}`;
1180
- if (seen.has(dedupeKey2)) {
1181
- continue;
1182
- }
1183
- seen.add(dedupeKey2);
1184
- parsed.push({
1185
- type: "system",
1186
- package: normalizedPackage
1187
- });
1188
- continue;
1189
- }
1190
- if (!/^https:\/\//i.test(normalizedUrl)) {
1191
- throw new Error(
1192
- `Plugin ${name} system runtime dependency url must be an https URL`
1193
- );
1194
- }
1195
- const normalizedSha256 = typeof sha256 === "string" ? sha256.trim().toLowerCase() : "";
1196
- if (!/^[a-f0-9]{64}$/.test(normalizedSha256)) {
1197
- throw new Error(
1198
- `Plugin ${name} system runtime dependency url entries must include a valid sha256`
1199
- );
1200
- }
1201
- const dedupeKey = `${type}:url:${normalizedUrl}:${normalizedSha256}`;
1202
- if (seen.has(dedupeKey)) {
1203
- continue;
1204
- }
1205
- seen.add(dedupeKey);
1206
- parsed.push({
1207
- type: "system",
1208
- url: normalizedUrl,
1209
- sha256: normalizedSha256
1210
- });
1211
- }
1212
- return parsed.length > 0 ? parsed : void 0;
1213
- }
1214
- function parseRuntimePostinstall(data, name) {
1215
- if (data === void 0) {
1216
- return void 0;
1217
- }
1218
- if (!Array.isArray(data)) {
1219
- throw new Error(`Plugin ${name} runtime-postinstall must be an array`);
1220
- }
1221
- const parsed = [];
1222
- for (const entry of data) {
1223
- if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1224
- throw new Error(
1225
- `Plugin ${name} runtime-postinstall entries must be objects`
1226
- );
1227
- }
1228
- const record = entry;
1229
- const cmd = typeof record.cmd === "string" ? record.cmd.trim() : "";
1230
- if (!cmd) {
1231
- throw new Error(
1232
- `Plugin ${name} runtime-postinstall cmd must be a non-empty string`
1233
- );
1234
- }
1235
- if (!RUNTIME_POSTINSTALL_CMD_RE.test(cmd)) {
1236
- throw new Error(
1237
- `Plugin ${name} runtime-postinstall cmd must be a single executable token (letters, digits, ., _, /, -)`
1238
- );
1239
- }
1240
- const argsRaw = record.args;
1241
- if (argsRaw !== void 0 && (!Array.isArray(argsRaw) || !argsRaw.every((arg) => typeof arg === "string"))) {
1242
- throw new Error(
1243
- `Plugin ${name} runtime-postinstall args must be an array of strings when provided`
1244
- );
1245
- }
1246
- const sudoRaw = record.sudo;
1247
- if (sudoRaw !== void 0 && typeof sudoRaw !== "boolean") {
1248
- throw new Error(
1249
- `Plugin ${name} runtime-postinstall sudo must be a boolean when provided`
1250
- );
1251
- }
1252
- const normalizedArgs = Array.isArray(argsRaw) ? argsRaw.map((arg) => arg.trim()).filter((arg) => arg.length > 0) : void 0;
1253
- parsed.push({
1254
- cmd,
1255
- ...normalizedArgs && normalizedArgs.length > 0 ? { args: normalizedArgs } : {},
1256
- ...typeof sudoRaw === "boolean" ? { sudo: sudoRaw } : {}
1257
- });
1258
- }
1259
- return parsed.length > 0 ? parsed : void 0;
1260
- }
1261
- function parseManifest(raw, dir) {
1262
- const data = toRecord(
1263
- parseYaml(raw),
1264
- `Invalid plugin manifest in ${dir}: expected an object`
1265
- );
1266
- const rawName = data.name;
1267
- if (typeof rawName !== "string" || !PLUGIN_NAME_RE.test(rawName)) {
1268
- throw new Error(`Invalid plugin name in ${dir}: "${rawName}"`);
1269
- }
1270
- const name = rawName;
1271
- const rawDescription = data.description;
1272
- if (typeof rawDescription !== "string" || !rawDescription.trim()) {
1273
- throw new Error(`Invalid plugin description in ${dir}`);
1274
- }
1275
- const description = rawDescription;
1276
- const rawCapabilities = data.capabilities;
1277
- if (rawCapabilities !== void 0 && !Array.isArray(rawCapabilities)) {
1278
- throw new Error(
1279
- `Plugin ${name} capabilities must be an array when provided`
1280
- );
1281
- }
1282
- const capabilities = [];
1283
- for (const cap of rawCapabilities ?? []) {
1284
- if (typeof cap !== "string" || !SHORT_CAPABILITY_RE.test(cap)) {
1285
- throw new Error(`Invalid capability token "${cap}" in plugin ${name}`);
1286
- }
1287
- capabilities.push(`${name}.${cap}`);
1288
- }
1289
- const rawConfigKeys = data["config-keys"];
1290
- if (rawConfigKeys !== void 0 && !Array.isArray(rawConfigKeys)) {
1291
- throw new Error(
1292
- `Plugin ${name} config-keys must be an array when provided`
1293
- );
1294
- }
1295
- const configKeys = [];
1296
- for (const key of rawConfigKeys ?? []) {
1297
- if (typeof key !== "string" || !SHORT_CONFIG_KEY_RE.test(key)) {
1298
- throw new Error(`Invalid config key "${key}" in plugin ${name}`);
1299
- }
1300
- configKeys.push(`${name}.${key}`);
1301
- }
1302
- const credentialsRaw = data.credentials;
1303
- if (credentialsRaw !== void 0) {
1304
- toRecord(
1305
- credentialsRaw,
1306
- `Plugin ${name} credentials must be an object when provided`
1307
- );
1308
- }
1309
- const credentials = credentialsRaw ? parseCredentials(credentialsRaw, name) : void 0;
1310
- const runtimeDependencies = parseRuntimeDependencies(
1311
- data["runtime-dependencies"],
1312
- name
1313
- );
1314
- const runtimePostinstall = parseRuntimePostinstall(
1315
- data["runtime-postinstall"],
1316
- name
1317
- );
1318
- const manifest = {
1319
- name,
1320
- description,
1321
- capabilities,
1322
- configKeys,
1323
- ...credentials ? { credentials } : {},
1324
- ...runtimeDependencies ? { runtimeDependencies } : {},
1325
- ...runtimePostinstall ? { runtimePostinstall } : {}
1326
- };
1327
- const oauthRaw = data.oauth ? toRecord(data.oauth, `Plugin ${name} oauth must be an object`) : void 0;
1328
- if (oauthRaw) {
1329
- if (!credentials) {
1330
- throw new Error(`Plugin ${name} oauth requires credentials`);
1331
- }
1332
- if (credentials.type !== "oauth-bearer") {
1333
- throw new Error(
1334
- `Plugin ${name} oauth requires credentials.type "oauth-bearer"`
1335
- );
1336
- }
1337
- const authorizeParams = parseStringMap(
1338
- oauthRaw["authorize-params"],
1339
- `Plugin ${name} oauth.authorize-params`,
1340
- { reservedKeys: RESERVED_AUTHORIZE_PARAM_KEYS }
1341
- );
1342
- const tokenExtraHeaders = parseStringMap(
1343
- oauthRaw["token-extra-headers"],
1344
- `Plugin ${name} oauth.token-extra-headers`,
1345
- { forbiddenKeys: FORBIDDEN_TOKEN_HEADER_NAMES }
1346
- );
1347
- const tokenAuthMethodRaw = oauthRaw["token-auth-method"];
1348
- let tokenAuthMethod;
1349
- if (tokenAuthMethodRaw !== void 0) {
1350
- const parsedTokenAuthMethod = requireStringField(
1351
- oauthRaw,
1352
- "token-auth-method",
1353
- `Plugin ${name} oauth.token-auth-method must be a non-empty string`
1354
- );
1355
- if (parsedTokenAuthMethod !== "body" && parsedTokenAuthMethod !== "basic") {
1356
- throw new Error(
1357
- `Plugin ${name} oauth.token-auth-method must be "body" or "basic"`
1358
- );
1359
- }
1360
- tokenAuthMethod = parsedTokenAuthMethod;
1361
- }
1362
- manifest.oauth = {
1363
- clientIdEnv: requireEnvVarField(oauthRaw, "client-id-env", name),
1364
- clientSecretEnv: requireEnvVarField(oauthRaw, "client-secret-env", name),
1365
- authorizeEndpoint: requireHttpsUrlField(
1366
- oauthRaw,
1367
- "authorize-endpoint",
1368
- name
1369
- ),
1370
- tokenEndpoint: requireHttpsUrlField(oauthRaw, "token-endpoint", name),
1371
- ...oauthRaw.scope !== void 0 ? {
1372
- scope: requireStringField(
1373
- oauthRaw,
1374
- "scope",
1375
- `Plugin ${name} oauth.scope must be a non-empty string`
1376
- )
1377
- } : {},
1378
- ...authorizeParams ? { authorizeParams } : {},
1379
- ...tokenAuthMethod ? { tokenAuthMethod } : {},
1380
- ...tokenExtraHeaders ? { tokenExtraHeaders } : {}
1381
- };
1382
- }
1383
- const targetRaw = data.target ? toRecord(data.target, `Plugin ${name} target must be an object`) : void 0;
1384
- if (targetRaw) {
1385
- if (targetRaw.type !== "repo") {
1386
- throw new Error(`Plugin ${name} target.type must be "repo"`);
1387
- }
1388
- const rawConfigKey = targetRaw["config-key"];
1389
- if (typeof rawConfigKey !== "string" || !rawConfigKey.trim()) {
1390
- throw new Error(
1391
- `Plugin ${name} target.config-key must be a non-empty string`
1392
- );
1393
- }
1394
- if (!SHORT_CONFIG_KEY_RE.test(rawConfigKey)) {
1395
- throw new Error(
1396
- `Plugin ${name} target.config-key "${rawConfigKey}" is invalid`
1397
- );
1398
- }
1399
- const qualifiedKey = `${name}.${rawConfigKey}`;
1400
- if (!configKeys.includes(qualifiedKey)) {
1401
- throw new Error(
1402
- `Plugin ${name} target.config-key "${rawConfigKey}" must be listed in config-keys`
1403
- );
1404
- }
1405
- manifest.target = { type: "repo", configKey: qualifiedKey };
1406
- }
1407
- return manifest;
1408
- }
1409
- var pluginDefinitions = [];
1410
- var capabilityToPlugin = /* @__PURE__ */ new Map();
1411
- var pluginConfigKeys = /* @__PURE__ */ new Set();
1412
- var pluginsByName = /* @__PURE__ */ new Map();
1413
- var packageSkillRoots = /* @__PURE__ */ new Set();
1414
- var pluginsLoaded = false;
1415
- function registerPluginManifest(raw, pluginDir) {
1416
- const manifest = parseManifest(raw, pluginDir);
1417
- if (pluginsByName.has(manifest.name)) {
1418
- return;
1419
- }
1420
- for (const cap of manifest.capabilities) {
1421
- if (capabilityToPlugin.has(cap)) {
1422
- throw new Error(
1423
- `Duplicate capability "${cap}" in plugin "${manifest.name}"`
1424
- );
1425
- }
576
+ for (const cap of manifest.capabilities) {
577
+ if (capabilityToPlugin.has(cap)) {
578
+ throw new Error(
579
+ `Duplicate capability "${cap}" in plugin "${manifest.name}"`
580
+ );
581
+ }
1426
582
  }
1427
583
  const definition = {
1428
584
  manifest,
@@ -1525,119 +681,498 @@ function loadPlugins() {
1525
681
  function ensurePluginsLoaded() {
1526
682
  loadPlugins();
1527
683
  }
1528
- loadPlugins();
1529
- function getPluginCapabilityProviders() {
1530
- ensurePluginsLoaded();
1531
- return pluginDefinitions.map((plugin) => ({
1532
- provider: plugin.manifest.name,
1533
- capabilities: [...plugin.manifest.capabilities],
1534
- configKeys: [...plugin.manifest.configKeys],
1535
- ...plugin.manifest.target ? { target: { ...plugin.manifest.target } } : {}
1536
- }));
684
+ loadPlugins();
685
+ function getPluginCapabilityProviders() {
686
+ ensurePluginsLoaded();
687
+ return pluginDefinitions.map((plugin) => ({
688
+ provider: plugin.manifest.name,
689
+ capabilities: [...plugin.manifest.capabilities],
690
+ configKeys: [...plugin.manifest.configKeys],
691
+ ...plugin.manifest.target ? { target: { ...plugin.manifest.target } } : {}
692
+ }));
693
+ }
694
+ function getPluginProviders() {
695
+ ensurePluginsLoaded();
696
+ return [...pluginDefinitions];
697
+ }
698
+ function getPluginRuntimeDependencies() {
699
+ ensurePluginsLoaded();
700
+ const seen = /* @__PURE__ */ new Set();
701
+ const deps = [];
702
+ for (const plugin of pluginDefinitions) {
703
+ for (const dep of plugin.manifest.runtimeDependencies ?? []) {
704
+ const key = dep.type === "npm" ? `${dep.type}:${dep.package}:${dep.version}` : "package" in dep ? `${dep.type}:package:${dep.package}` : `${dep.type}:url:${dep.url}:${dep.sha256}`;
705
+ if (seen.has(key)) {
706
+ continue;
707
+ }
708
+ seen.add(key);
709
+ deps.push(dep);
710
+ }
711
+ }
712
+ return deps.sort((left, right) => {
713
+ if (left.type !== right.type) {
714
+ return left.type.localeCompare(right.type);
715
+ }
716
+ const leftIdentity = "package" in left ? `package:${left.package}` : `url:${left.url}:${left.sha256}`;
717
+ const rightIdentity = "package" in right ? `package:${right.package}` : `url:${right.url}:${right.sha256}`;
718
+ if (leftIdentity !== rightIdentity) {
719
+ return leftIdentity.localeCompare(rightIdentity);
720
+ }
721
+ if (left.type === "npm" && right.type === "npm") {
722
+ return left.version.localeCompare(right.version);
723
+ }
724
+ return 0;
725
+ });
726
+ }
727
+ function getPluginRuntimePostinstall() {
728
+ ensurePluginsLoaded();
729
+ const commands = [];
730
+ for (const plugin of pluginDefinitions) {
731
+ for (const command of plugin.manifest.runtimePostinstall ?? []) {
732
+ commands.push({
733
+ cmd: command.cmd,
734
+ ...command.args ? { args: [...command.args] } : {},
735
+ ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
736
+ });
737
+ }
738
+ }
739
+ return commands;
740
+ }
741
+ function getPluginOAuthConfig(provider) {
742
+ ensurePluginsLoaded();
743
+ const plugin = pluginsByName.get(provider);
744
+ if (!plugin?.manifest.oauth) return void 0;
745
+ const oauth = plugin.manifest.oauth;
746
+ return {
747
+ clientIdEnv: oauth.clientIdEnv,
748
+ clientSecretEnv: oauth.clientSecretEnv,
749
+ authorizeEndpoint: oauth.authorizeEndpoint,
750
+ tokenEndpoint: oauth.tokenEndpoint,
751
+ ...oauth.scope ? { scope: oauth.scope } : {},
752
+ ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
753
+ ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
754
+ ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
755
+ callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
756
+ };
757
+ }
758
+ function getPluginSkillRoots() {
759
+ ensurePluginsLoaded();
760
+ return [
761
+ .../* @__PURE__ */ new Set([
762
+ ...pluginDefinitions.map((plugin) => plugin.skillsDir),
763
+ ...packageSkillRoots
764
+ ])
765
+ ];
766
+ }
767
+ function isPluginProvider(provider) {
768
+ ensurePluginsLoaded();
769
+ return pluginsByName.has(provider);
770
+ }
771
+ function createPluginBroker(provider, deps) {
772
+ ensurePluginsLoaded();
773
+ const plugin = pluginsByName.get(provider);
774
+ if (!plugin) {
775
+ throw new Error(`Unknown plugin provider: "${provider}"`);
776
+ }
777
+ const { credentials, name } = plugin.manifest;
778
+ if (!credentials) {
779
+ throw new Error(`Provider "${name}" has no credentials configured`);
780
+ }
781
+ let broker;
782
+ if (credentials.type === "oauth-bearer") {
783
+ broker = createOAuthBearerBroker(plugin.manifest, credentials, deps);
784
+ } else if (credentials.type === "github-app") {
785
+ broker = createGitHubAppBroker(plugin.manifest, credentials);
786
+ } else {
787
+ throw new Error(`Unsupported credentials type for plugin "${name}"`);
788
+ }
789
+ setSpanAttributes({
790
+ "app.plugin.name": name,
791
+ "app.plugin.capabilities": plugin.manifest.capabilities,
792
+ "app.plugin.has_oauth": Boolean(plugin.manifest.oauth)
793
+ });
794
+ return broker;
795
+ }
796
+
797
+ // src/chat/config.ts
798
+ var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
799
+ var DEFAULT_AGENT_TURN_TIMEOUT_MS = 12 * 60 * 1e3;
800
+ var DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS = 800;
801
+ var TURN_TIMEOUT_BUFFER_SECONDS = 20;
802
+ function parseAgentTurnTimeoutMs(rawValue, maxTimeoutMs) {
803
+ const value = Number.parseInt(rawValue ?? "", 10);
804
+ if (Number.isNaN(value)) {
805
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(DEFAULT_AGENT_TURN_TIMEOUT_MS, maxTimeoutMs));
806
+ }
807
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, Math.min(value, maxTimeoutMs));
808
+ }
809
+ function resolveQueueCallbackMaxDurationSeconds() {
810
+ const value = Number.parseInt(process.env.QUEUE_CALLBACK_MAX_DURATION_SECONDS ?? "", 10);
811
+ if (Number.isNaN(value) || value <= 0) {
812
+ return DEFAULT_QUEUE_CALLBACK_MAX_DURATION_SECONDS;
813
+ }
814
+ return value;
815
+ }
816
+ function resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds) {
817
+ const budgetSeconds = queueCallbackMaxDurationSeconds - TURN_TIMEOUT_BUFFER_SECONDS;
818
+ return Math.max(MIN_AGENT_TURN_TIMEOUT_MS, budgetSeconds * 1e3);
819
+ }
820
+ function buildBotConfig() {
821
+ const queueCallbackMaxDurationSeconds = resolveQueueCallbackMaxDurationSeconds();
822
+ const maxTurnTimeoutMs = resolveMaxTurnTimeoutMs(queueCallbackMaxDurationSeconds);
823
+ return {
824
+ userName: process.env.JUNIOR_BOT_NAME ?? "junior",
825
+ modelId: process.env.AI_MODEL ?? "anthropic/claude-sonnet-4.6",
826
+ fastModelId: process.env.AI_FAST_MODEL ?? process.env.AI_MODEL ?? "anthropic/claude-haiku-4.5",
827
+ turnTimeoutMs: parseAgentTurnTimeoutMs(process.env.AGENT_TURN_TIMEOUT_MS, maxTurnTimeoutMs)
828
+ };
829
+ }
830
+ var botConfig = buildBotConfig();
831
+ function toOptionalTrimmed(value) {
832
+ if (!value) {
833
+ return void 0;
834
+ }
835
+ const trimmed = value.trim();
836
+ return trimmed.length > 0 ? trimmed : void 0;
837
+ }
838
+ function getSlackBotToken() {
839
+ return toOptionalTrimmed(process.env.SLACK_BOT_TOKEN) ?? toOptionalTrimmed(process.env.SLACK_BOT_USER_TOKEN);
840
+ }
841
+ function getSlackSigningSecret() {
842
+ return toOptionalTrimmed(process.env.SLACK_SIGNING_SECRET);
843
+ }
844
+ function getSlackClientId() {
845
+ return toOptionalTrimmed(process.env.SLACK_CLIENT_ID);
846
+ }
847
+ function getSlackClientSecret() {
848
+ return toOptionalTrimmed(process.env.SLACK_CLIENT_SECRET);
849
+ }
850
+ function hasRedisConfig() {
851
+ return Boolean(process.env.REDIS_URL);
852
+ }
853
+
854
+ // src/chat/state.ts
855
+ import { createRedisState } from "@chat-adapter/state-redis";
856
+ import { createMemoryState } from "@chat-adapter/state-memory";
857
+ var MIN_LOCK_TTL_MS = 1e3 * 60 * 5;
858
+ var QUEUE_INGRESS_DEDUP_PREFIX = "junior:queue_ingress";
859
+ var QUEUE_MESSAGE_PROCESSING_PREFIX = "junior:queue_message";
860
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
861
+ var QUEUE_MESSAGE_PROCESSING_TTL_MS = 30 * 60 * 1e3;
862
+ var QUEUE_MESSAGE_COMPLETED_TTL_MS = 7 * 24 * 60 * 60 * 1e3;
863
+ var QUEUE_MESSAGE_FAILED_TTL_MS = 6 * 60 * 60 * 1e3;
864
+ var AGENT_TURN_SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
865
+ var CLAIM_OR_RECLAIM_PROCESSING_SCRIPT = `
866
+ local key = KEYS[1]
867
+ local nowMs = tonumber(ARGV[1])
868
+ local ttlMs = tonumber(ARGV[2])
869
+ local payload = ARGV[3]
870
+ local current = redis.call("get", key)
871
+
872
+ if not current then
873
+ redis.call("set", key, payload, "PX", ttlMs)
874
+ return 1
875
+ end
876
+
877
+ local ok, parsed = pcall(cjson.decode, current)
878
+ if not ok or type(parsed) ~= "table" then
879
+ return 0
880
+ end
881
+
882
+ local status = parsed["status"]
883
+ if status == "failed" then
884
+ redis.call("set", key, payload, "PX", ttlMs)
885
+ return 3
886
+ end
887
+ if status ~= "processing" then
888
+ return 0
889
+ end
890
+
891
+ local updatedAtMs = tonumber(parsed["updatedAtMs"])
892
+ if not updatedAtMs then
893
+ return 0
894
+ end
895
+
896
+ if updatedAtMs + ttlMs < nowMs then
897
+ redis.call("set", key, payload, "PX", ttlMs)
898
+ return 2
899
+ end
900
+
901
+ return 0
902
+ `;
903
+ var UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT = `
904
+ local key = KEYS[1]
905
+ local ownerToken = ARGV[1]
906
+ local ttlMs = tonumber(ARGV[2])
907
+ local payload = ARGV[3]
908
+ local current = redis.call("get", key)
909
+
910
+ if not current then
911
+ return 0
912
+ end
913
+
914
+ local ok, parsed = pcall(cjson.decode, current)
915
+ if not ok or type(parsed) ~= "table" then
916
+ return 0
917
+ end
918
+
919
+ local currentOwner = parsed["ownerToken"]
920
+ local status = parsed["status"]
921
+ if currentOwner ~= ownerToken then
922
+ return 0
923
+ end
924
+ if status ~= "processing" then
925
+ return 0
926
+ end
927
+
928
+ redis.call("set", key, payload, "PX", ttlMs)
929
+ return 1
930
+ `;
931
+ function createQueuedStateAdapter(base) {
932
+ const acquireLock = async (threadId, ttlMs) => {
933
+ const effectiveTtlMs = Math.max(ttlMs, MIN_LOCK_TTL_MS);
934
+ const lock = await base.acquireLock(threadId, effectiveTtlMs);
935
+ return lock;
936
+ };
937
+ return {
938
+ connect: () => base.connect(),
939
+ disconnect: () => base.disconnect(),
940
+ subscribe: (threadId) => base.subscribe(threadId),
941
+ unsubscribe: (threadId) => base.unsubscribe(threadId),
942
+ isSubscribed: (threadId) => base.isSubscribed(threadId),
943
+ acquireLock,
944
+ releaseLock: (lock) => base.releaseLock(lock),
945
+ extendLock: (lock, ttlMs) => base.extendLock(lock, Math.max(ttlMs, MIN_LOCK_TTL_MS)),
946
+ get: (key) => base.get(key),
947
+ set: (key, value, ttlMs) => base.set(key, value, ttlMs),
948
+ setIfNotExists: (key, value, ttlMs) => base.setIfNotExists(key, value, ttlMs),
949
+ delete: (key) => base.delete(key)
950
+ };
951
+ }
952
+ function createStateAdapter() {
953
+ if (process.env.JUNIOR_STATE_ADAPTER?.trim().toLowerCase() === "memory") {
954
+ _redisStateAdapter = void 0;
955
+ return createQueuedStateAdapter(createMemoryState());
956
+ }
957
+ if (!hasRedisConfig()) {
958
+ throw new Error("REDIS_URL is required for durable Slack thread state");
959
+ }
960
+ const redisState = createRedisState({
961
+ url: process.env.REDIS_URL
962
+ });
963
+ _redisStateAdapter = redisState;
964
+ return createQueuedStateAdapter(redisState);
965
+ }
966
+ var _stateAdapter;
967
+ var _redisStateAdapter;
968
+ function getRedisStateAdapter() {
969
+ if (!_redisStateAdapter) {
970
+ getStateAdapter();
971
+ }
972
+ if (!_redisStateAdapter) {
973
+ throw new Error("Redis state adapter is unavailable for this runtime");
974
+ }
975
+ return _redisStateAdapter;
976
+ }
977
+ function queueMessageKey(rawKey) {
978
+ return `${QUEUE_MESSAGE_PROCESSING_PREFIX}:${rawKey}`;
979
+ }
980
+ function parseQueueMessageState(value) {
981
+ if (typeof value !== "string") {
982
+ return void 0;
983
+ }
984
+ try {
985
+ const parsed = JSON.parse(value);
986
+ if (!parsed || parsed.status !== "processing" && parsed.status !== "completed" && parsed.status !== "failed" || typeof parsed.updatedAtMs !== "number") {
987
+ return void 0;
988
+ }
989
+ return {
990
+ status: parsed.status,
991
+ updatedAtMs: parsed.updatedAtMs,
992
+ ...typeof parsed.ownerToken === "string" ? { ownerToken: parsed.ownerToken } : {},
993
+ ...typeof parsed.queueMessageId === "string" ? { queueMessageId: parsed.queueMessageId } : {},
994
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {}
995
+ };
996
+ } catch {
997
+ return void 0;
998
+ }
999
+ }
1000
+ function agentTurnSessionKey(conversationId, sessionId) {
1001
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
1537
1002
  }
1538
- function getPluginProviders() {
1539
- ensurePluginsLoaded();
1540
- return [...pluginDefinitions];
1003
+ function isRecord(value) {
1004
+ return typeof value === "object" && value !== null;
1541
1005
  }
1542
- function getPluginRuntimeDependencies() {
1543
- ensurePluginsLoaded();
1544
- const seen = /* @__PURE__ */ new Set();
1545
- const deps = [];
1546
- for (const plugin of pluginDefinitions) {
1547
- for (const dep of plugin.manifest.runtimeDependencies ?? []) {
1548
- const key = dep.type === "npm" ? `${dep.type}:${dep.package}:${dep.version}` : "package" in dep ? `${dep.type}:package:${dep.package}` : `${dep.type}:url:${dep.url}:${dep.sha256}`;
1549
- if (seen.has(key)) {
1550
- continue;
1551
- }
1552
- seen.add(key);
1553
- deps.push(dep);
1554
- }
1006
+ function parseAgentTurnSessionCheckpoint(value) {
1007
+ if (typeof value !== "string") {
1008
+ return void 0;
1555
1009
  }
1556
- return deps.sort((left, right) => {
1557
- if (left.type !== right.type) {
1558
- return left.type.localeCompare(right.type);
1010
+ try {
1011
+ const parsed = JSON.parse(value);
1012
+ if (!isRecord(parsed)) {
1013
+ return void 0;
1559
1014
  }
1560
- const leftIdentity = "package" in left ? `package:${left.package}` : `url:${left.url}:${left.sha256}`;
1561
- const rightIdentity = "package" in right ? `package:${right.package}` : `url:${right.url}:${right.sha256}`;
1562
- if (leftIdentity !== rightIdentity) {
1563
- return leftIdentity.localeCompare(rightIdentity);
1015
+ const status = parsed.state;
1016
+ if (status !== "running" && status !== "awaiting_resume" && status !== "completed" && status !== "failed") {
1017
+ return void 0;
1564
1018
  }
1565
- if (left.type === "npm" && right.type === "npm") {
1566
- return left.version.localeCompare(right.version);
1019
+ const conversationId = parsed.conversationId;
1020
+ const sessionId = parsed.sessionId;
1021
+ const sliceId = parsed.sliceId;
1022
+ const checkpointVersion = parsed.checkpointVersion;
1023
+ const updatedAtMs = parsed.updatedAtMs;
1024
+ if (typeof conversationId !== "string" || typeof sessionId !== "string" || typeof sliceId !== "number" || typeof checkpointVersion !== "number" || typeof updatedAtMs !== "number") {
1025
+ return void 0;
1567
1026
  }
1568
- return 0;
1569
- });
1027
+ return {
1028
+ checkpointVersion,
1029
+ conversationId,
1030
+ sessionId,
1031
+ sliceId,
1032
+ state: status,
1033
+ updatedAtMs,
1034
+ piMessages: Array.isArray(parsed.piMessages) ? parsed.piMessages : [],
1035
+ ...typeof parsed.errorMessage === "string" ? { errorMessage: parsed.errorMessage } : {},
1036
+ ...typeof parsed.resumedFromSliceId === "number" ? { resumedFromSliceId: parsed.resumedFromSliceId } : {}
1037
+ };
1038
+ } catch {
1039
+ return void 0;
1040
+ }
1570
1041
  }
1571
- function getPluginRuntimePostinstall() {
1572
- ensurePluginsLoaded();
1573
- const commands = [];
1574
- for (const plugin of pluginDefinitions) {
1575
- for (const command of plugin.manifest.runtimePostinstall ?? []) {
1576
- commands.push({
1577
- cmd: command.cmd,
1578
- ...command.args ? { args: [...command.args] } : {},
1579
- ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
1580
- });
1581
- }
1042
+ function getStateAdapter() {
1043
+ if (!_stateAdapter) {
1044
+ _stateAdapter = createStateAdapter();
1582
1045
  }
1583
- return commands;
1046
+ return _stateAdapter;
1584
1047
  }
1585
- function getPluginOAuthConfig(provider) {
1586
- ensurePluginsLoaded();
1587
- const plugin = pluginsByName.get(provider);
1588
- if (!plugin?.manifest.oauth) return void 0;
1589
- const oauth = plugin.manifest.oauth;
1590
- return {
1591
- clientIdEnv: oauth.clientIdEnv,
1592
- clientSecretEnv: oauth.clientSecretEnv,
1593
- authorizeEndpoint: oauth.authorizeEndpoint,
1594
- tokenEndpoint: oauth.tokenEndpoint,
1595
- ...oauth.scope ? { scope: oauth.scope } : {},
1596
- ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
1597
- ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
1598
- ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
1599
- callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
1600
- };
1048
+ async function disconnectStateAdapter() {
1049
+ if (!_stateAdapter) {
1050
+ return;
1051
+ }
1052
+ try {
1053
+ await _stateAdapter.disconnect();
1054
+ } finally {
1055
+ _stateAdapter = void 0;
1056
+ _redisStateAdapter = void 0;
1057
+ }
1601
1058
  }
1602
- function getPluginSkillRoots() {
1603
- ensurePluginsLoaded();
1604
- return [
1605
- .../* @__PURE__ */ new Set([
1606
- ...pluginDefinitions.map((plugin) => plugin.skillsDir),
1607
- ...packageSkillRoots
1608
- ])
1609
- ];
1059
+ async function claimQueueIngressDedup(rawKey, ttlMs) {
1060
+ await getStateAdapter().connect();
1061
+ const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
1062
+ const result = await getRedisStateAdapter().getClient().set(key, "1", {
1063
+ NX: true,
1064
+ PX: ttlMs
1065
+ });
1066
+ return result === "OK";
1610
1067
  }
1611
- function isPluginProvider(provider) {
1612
- ensurePluginsLoaded();
1613
- return pluginsByName.has(provider);
1068
+ async function hasQueueIngressDedup(rawKey) {
1069
+ await getStateAdapter().connect();
1070
+ const key = `${QUEUE_INGRESS_DEDUP_PREFIX}:${rawKey}`;
1071
+ const value = await getRedisStateAdapter().getClient().get(key);
1072
+ return typeof value === "string" && value.length > 0;
1614
1073
  }
1615
- function createPluginBroker(provider, deps) {
1616
- ensurePluginsLoaded();
1617
- const plugin = pluginsByName.get(provider);
1618
- if (!plugin) {
1619
- throw new Error(`Unknown plugin provider: "${provider}"`);
1074
+ async function getQueueMessageProcessingState(rawKey) {
1075
+ await getStateAdapter().connect();
1076
+ const state = await getStateAdapter().get(queueMessageKey(rawKey));
1077
+ return parseQueueMessageState(state);
1078
+ }
1079
+ async function acquireQueueMessageProcessingOwnership(args) {
1080
+ await getStateAdapter().connect();
1081
+ const key = queueMessageKey(args.rawKey);
1082
+ const nowMs = Date.now();
1083
+ const payload = JSON.stringify({
1084
+ status: "processing",
1085
+ updatedAtMs: nowMs,
1086
+ ownerToken: args.ownerToken,
1087
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1088
+ });
1089
+ const result = await getRedisStateAdapter().getClient().eval(CLAIM_OR_RECLAIM_PROCESSING_SCRIPT, {
1090
+ keys: [key],
1091
+ arguments: [String(nowMs), String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
1092
+ });
1093
+ if (result === 1) {
1094
+ return "acquired";
1620
1095
  }
1621
- const { credentials, name } = plugin.manifest;
1622
- if (!credentials) {
1623
- throw new Error(`Provider "${name}" has no credentials configured`);
1096
+ if (result === 2) {
1097
+ return "reclaimed";
1624
1098
  }
1625
- let broker;
1626
- if (credentials.type === "oauth-bearer") {
1627
- broker = createOAuthBearerBroker(plugin.manifest, credentials, deps);
1628
- } else if (credentials.type === "github-app") {
1629
- broker = createGitHubAppBroker(plugin.manifest, credentials);
1630
- } else {
1631
- throw new Error(`Unsupported credentials type for plugin "${name}"`);
1099
+ if (result === 3) {
1100
+ return "recovered";
1632
1101
  }
1633
- setSpanAttributes({
1634
- "app.plugin.name": name,
1635
- "app.plugin.capabilities": plugin.manifest.capabilities,
1636
- "app.plugin.has_oauth": Boolean(plugin.manifest.oauth)
1102
+ return "blocked";
1103
+ }
1104
+ async function refreshQueueMessageProcessingOwnership(args) {
1105
+ await getStateAdapter().connect();
1106
+ const nowMs = Date.now();
1107
+ const payload = JSON.stringify({
1108
+ status: "processing",
1109
+ updatedAtMs: nowMs,
1110
+ ownerToken: args.ownerToken,
1111
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1637
1112
  });
1638
- return broker;
1113
+ const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
1114
+ keys: [queueMessageKey(args.rawKey)],
1115
+ arguments: [args.ownerToken, String(QUEUE_MESSAGE_PROCESSING_TTL_MS), payload]
1116
+ });
1117
+ return result === 1;
1118
+ }
1119
+ async function completeQueueMessageProcessingOwnership(args) {
1120
+ await getStateAdapter().connect();
1121
+ const payload = JSON.stringify({
1122
+ status: "completed",
1123
+ updatedAtMs: Date.now(),
1124
+ ownerToken: args.ownerToken,
1125
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1126
+ });
1127
+ const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
1128
+ keys: [queueMessageKey(args.rawKey)],
1129
+ arguments: [args.ownerToken, String(QUEUE_MESSAGE_COMPLETED_TTL_MS), payload]
1130
+ });
1131
+ return result === 1;
1132
+ }
1133
+ async function failQueueMessageProcessingOwnership(args) {
1134
+ await getStateAdapter().connect();
1135
+ const payload = JSON.stringify({
1136
+ status: "failed",
1137
+ updatedAtMs: Date.now(),
1138
+ ownerToken: args.ownerToken,
1139
+ errorMessage: args.errorMessage,
1140
+ ...args.queueMessageId ? { queueMessageId: args.queueMessageId } : {}
1141
+ });
1142
+ const result = await getRedisStateAdapter().getClient().eval(UPDATE_PROCESSING_STATE_IF_OWNER_SCRIPT, {
1143
+ keys: [queueMessageKey(args.rawKey)],
1144
+ arguments: [args.ownerToken, String(QUEUE_MESSAGE_FAILED_TTL_MS), payload]
1145
+ });
1146
+ return result === 1;
1147
+ }
1148
+ async function getAgentTurnSessionCheckpoint(conversationId, sessionId) {
1149
+ await getStateAdapter().connect();
1150
+ const value = await getStateAdapter().get(agentTurnSessionKey(conversationId, sessionId));
1151
+ return parseAgentTurnSessionCheckpoint(value);
1152
+ }
1153
+ async function upsertAgentTurnSessionCheckpoint(args) {
1154
+ await getStateAdapter().connect();
1155
+ const existing = await getAgentTurnSessionCheckpoint(args.conversationId, args.sessionId);
1156
+ const checkpoint = {
1157
+ checkpointVersion: (existing?.checkpointVersion ?? 0) + 1,
1158
+ conversationId: args.conversationId,
1159
+ sessionId: args.sessionId,
1160
+ sliceId: args.sliceId,
1161
+ state: args.state,
1162
+ updatedAtMs: Date.now(),
1163
+ piMessages: Array.isArray(args.piMessages) ? args.piMessages : [],
1164
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1165
+ ...typeof args.resumedFromSliceId === "number" ? { resumedFromSliceId: args.resumedFromSliceId } : {}
1166
+ };
1167
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
1168
+ await getStateAdapter().set(agentTurnSessionKey(args.conversationId, args.sessionId), JSON.stringify(checkpoint), ttlMs);
1169
+ return checkpoint;
1639
1170
  }
1640
1171
 
1172
+ // src/chat/sandbox/runtime-dependency-snapshots.ts
1173
+ import { createHash } from "crypto";
1174
+ import { Sandbox } from "@vercel/sandbox";
1175
+
1641
1176
  // src/chat/sandbox/paths.ts
1642
1177
  function normalizeWorkspaceRoot(input) {
1643
1178
  const candidate = (input ?? "").trim();
@@ -2196,12 +1731,15 @@ export {
2196
1731
  homeDir,
2197
1732
  skillRoots,
2198
1733
  soulPathCandidates,
1734
+ aboutPathCandidates,
2199
1735
  resolveAuthTokenPlaceholder,
2200
1736
  CredentialUnavailableError,
2201
1737
  buildOAuthTokenRequest,
2202
1738
  parseOAuthTokenResponse,
2203
1739
  getPluginCapabilityProviders,
2204
1740
  getPluginProviders,
1741
+ getPluginRuntimeDependencies,
1742
+ getPluginRuntimePostinstall,
2205
1743
  getPluginOAuthConfig,
2206
1744
  getPluginSkillRoots,
2207
1745
  isPluginProvider,