@runpod/mcp-server 4.7.0 → 4.9.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/README.md +9 -0
  3. package/dist/alp.cjs +319 -0
  4. package/dist/alp.cjs.map +1 -0
  5. package/dist/alp.d.cts +20 -0
  6. package/dist/alp.d.ts +20 -0
  7. package/dist/alp.mjs +25 -0
  8. package/dist/alp.mjs.map +1 -0
  9. package/dist/chunk-577IYCJT.mjs +53 -0
  10. package/dist/chunk-577IYCJT.mjs.map +1 -0
  11. package/dist/chunk-6IETGSON.mjs +135 -0
  12. package/dist/chunk-6IETGSON.mjs.map +1 -0
  13. package/dist/chunk-H3NZYQIB.mjs +236 -0
  14. package/dist/chunk-H3NZYQIB.mjs.map +1 -0
  15. package/dist/chunk-VKLWTK4O.mjs +10671 -0
  16. package/dist/chunk-VKLWTK4O.mjs.map +1 -0
  17. package/dist/hosted.cjs +11272 -0
  18. package/dist/hosted.cjs.map +1 -0
  19. package/dist/hosted.d.cts +58 -0
  20. package/dist/hosted.d.ts +58 -0
  21. package/dist/hosted.mjs +241 -0
  22. package/dist/hosted.mjs.map +1 -0
  23. package/dist/http-BaACCis6.d.cts +75 -0
  24. package/dist/http-BcFDW7LG.d.ts +75 -0
  25. package/dist/http.cjs +115 -8
  26. package/dist/http.cjs.map +1 -1
  27. package/dist/http.d.cts +5 -78
  28. package/dist/http.d.ts +5 -78
  29. package/dist/http.mjs +13 -10602
  30. package/dist/http.mjs.map +1 -1
  31. package/dist/sink-Cm8lp16D.d.cts +17 -0
  32. package/dist/sink-Cm8lp16D.d.ts +17 -0
  33. package/dist/specgen/profile.cjs +163 -0
  34. package/dist/specgen/profile.cjs.map +1 -0
  35. package/dist/specgen/profile.d.cts +22 -0
  36. package/dist/specgen/profile.d.ts +22 -0
  37. package/dist/specgen/profile.mjs +15 -0
  38. package/dist/specgen/profile.mjs.map +1 -0
  39. package/dist/stdio.cjs +115 -8
  40. package/dist/stdio.cjs.map +1 -1
  41. package/dist/stdio.mjs +115 -8
  42. package/dist/stdio.mjs.map +1 -1
  43. package/package.json +17 -2
package/CHANGELOG.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # @runpod/mcp-server
2
2
 
3
+ ## 4.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 6b0400d: Export the hosted-service surface as `@runpod/mcp-server/hosted` (`handleMcpRequest`, `getBaseUrl`, `handleAlpSubmit`, `handleAlpJournalRead`, `handleSearch`, `DIRECTORY_PATH`) and the ALP contract the Convex sink shares as `@runpod/mcp-server/alp` (`ALP_ROUTES`, `ONCE_PER_ACCOUNT_ROUTES`, `isOncePerAccountRoute`, `scrubSubmission` and friends), so an HTTP host and its Convex functions can build on the package instead of `src/`.
8
+
9
+ ## 4.8.0
10
+
11
+ ### Minor Changes
12
+
13
+ - 8f53070: Export the server profile module as `@runpod/mcp-server/profile` (`ServerProfile`, `DIRECTORY_PATH`, `DIRECTORY_TOOLS`, `profileForPath`, `directoryIncludes`, `directoryOmittedInputs`) so an HTTP host can resolve the directory profile from the package instead of reaching into `src/`.
14
+ - 4c51d2a: Resync the vendored v2 OpenAPI spec with production as of 2026-10-27 and regenerate the SDK schema and the tools. Adds `list-organization-users` and `get-organization-user`, documents the 413 response on list operations, and requires a non-empty mount `path`.
15
+
16
+ ### Patch Changes
17
+
18
+ - 2658966: Record the tool surface on each `runpod_mcp_tool_call` analytics event as `profile` (`directory` for `mcp.runpod.io/directory`, `default` otherwise), so directory traffic (the ChatGPT plugin and the Claude connector) can be told apart from custom connectors on `/`.
19
+ - f3efa2e: Avoid suggesting immediate retries when a timed-out or failed create may already have completed upstream.
20
+
3
21
  ## 4.7.0
4
22
 
5
23
  ### Minor Changes
package/README.md CHANGED
@@ -226,3 +226,12 @@ the new generated surface. Applications should connect to the server through
226
226
  MCP using the `runpod-mcp` CLI, or use `handleMcpRequest` from
227
227
  `@runpod/mcp-server/http` when embedding the HTTP handler. The HTTP entry point
228
228
  also no longer re-exports the legacy `registerTools` or `ToolContext`.
229
+
230
+ An HTTP host that serves the directory profile can import `DIRECTORY_PATH`
231
+ and `profileForPath` from `@runpod/mcp-server/profile`.
232
+
233
+ The hosted service's own surface (the MCP request handler, the ALP submit and
234
+ journal handlers and the knowledge search route) is `@runpod/mcp-server/hosted`.
235
+ The ALP contract the Convex sink shares (route names, the once-per-account
236
+ rule and the scrubber) is `@runpod/mcp-server/alp`, kept free of the HTTP
237
+ handler so a Convex function can bundle it.
package/dist/alp.cjs ADDED
@@ -0,0 +1,319 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/alp.ts
21
+ var alp_exports = {};
22
+ __export(alp_exports, {
23
+ ALP_REFERRAL_SOURCES: () => ALP_REFERRAL_SOURCES,
24
+ ALP_REFERRAL_USE_CASES: () => ALP_REFERRAL_USE_CASES,
25
+ ALP_ROUTES: () => ALP_ROUTES,
26
+ ALREADY_SUBMITTED_NOTE: () => ALREADY_SUBMITTED_NOTE,
27
+ ONCE_PER_ACCOUNT_ROUTES: () => ONCE_PER_ACCOUNT_ROUTES,
28
+ isOncePerAccountRoute: () => isOncePerAccountRoute,
29
+ isSinkUrl: () => isSinkUrl,
30
+ scrubSubmission: () => scrubSubmission,
31
+ sinkRouteUrl: () => sinkRouteUrl
32
+ });
33
+ module.exports = __toCommonJS(alp_exports);
34
+
35
+ // src/alp/sink.ts
36
+ var ALP_ROUTES = [
37
+ "feedback",
38
+ "journal",
39
+ "question",
40
+ "referral"
41
+ ];
42
+ var ONCE_PER_ACCOUNT_ROUTES = ["referral"];
43
+ function isOncePerAccountRoute(route) {
44
+ return ONCE_PER_ACCOUNT_ROUTES.includes(route);
45
+ }
46
+ var ALREADY_SUBMITTED_NOTE = "This account has already made this one-time submission; nothing new was stored. Do not call this again on this account \u2014 continue your task.";
47
+ var ALP_REFERRAL_SOURCES = [
48
+ "searched",
49
+ "user_named",
50
+ "recommended",
51
+ "other"
52
+ ];
53
+ var ALP_REFERRAL_USE_CASES = [
54
+ "inference",
55
+ "fine_tuning",
56
+ "training",
57
+ "image_video_generation",
58
+ "dev_environment",
59
+ "batch_processing",
60
+ "other"
61
+ ];
62
+ function isSinkUrl(value) {
63
+ try {
64
+ const url = new URL(value);
65
+ return url.protocol === "https:" && url.hostname.endsWith(".convex.site") && url.pathname === "/alp/submit";
66
+ } catch {
67
+ return false;
68
+ }
69
+ }
70
+ function sinkRouteUrl(submitUrl, path) {
71
+ if (!isSinkUrl(submitUrl)) return null;
72
+ const url = new URL(submitUrl);
73
+ url.pathname = path;
74
+ return url.toString();
75
+ }
76
+
77
+ // src/alp/scrub.ts
78
+ var SCRUB_VERSION = 5;
79
+ var MARKER = /^["']?\[redacted:[a-z_]+\]["']?$/;
80
+ var CREDENTIAL_PATTERNS = [
81
+ // Runpod API keys.
82
+ { name: "runpod_key", re: /\brpa_[A-Za-z0-9]{16,}\b/g },
83
+ // Bearer credentials pasted with their header.
84
+ { name: "bearer", re: /\bBearer\s+[A-Za-z0-9._~+/=-]{16,}/g },
85
+ // Common vendor key prefixes.
86
+ {
87
+ name: "vendor_key",
88
+ re: /\b(?:sk|pk|ghp|gho|phc|phx|xoxb|xoxp)[-_][A-Za-z0-9_-]{16,}\b/g
89
+ },
90
+ // Hugging Face access tokens.
91
+ { name: "hugging_face", re: /\bhf_[A-Za-z0-9]{30,}\b/g },
92
+ // AWS access key ids.
93
+ { name: "aws_key", re: /\bAKIA[0-9A-Z]{16}\b/g },
94
+ // Three-segment JWTs.
95
+ {
96
+ name: "jwt",
97
+ re: /\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/g
98
+ }
99
+ ];
100
+ function redactAnchoredCredentials(text) {
101
+ let redactions = 0;
102
+ let out = text;
103
+ for (const { name, re } of CREDENTIAL_PATTERNS) {
104
+ out = out.replace(re, () => {
105
+ redactions++;
106
+ return `[redacted:${name}]`;
107
+ });
108
+ }
109
+ return { text: out, redactions };
110
+ }
111
+ var AUTH_HEADER = /\b((?:proxy-)?authorization|x-api-key|x-auth-token|x-runpod-token)(\s*:\s*)((?:basic|bearer|token|digest|negotiate|ntlm|apikey)\s+[^\s|,]+|[^\s|,]+)/gi;
112
+ var COOKIE_HEADER = /\b(cookie|set-cookie)(\s*:\s*)([^\s;=,|]+=[^\s;,|]*(?:\s*;\s*[^\s;=,|]+(?:=[^\s;,|]*)?)*)/gi;
113
+ function redactHeaderValues(text) {
114
+ let redactions = 0;
115
+ const replace = (match, name, sep, value) => {
116
+ if (MARKER.test(value)) return match;
117
+ redactions++;
118
+ return `${name}${sep}[redacted:header]`;
119
+ };
120
+ const out = text.replace(AUTH_HEADER, replace).replace(COOKIE_HEADER, replace);
121
+ return { text: out, redactions };
122
+ }
123
+ function redactQuerySecrets(text) {
124
+ let redactions = 0;
125
+ const out = text.replace(
126
+ /([?&](?:api[_-]?key|access[_-]?token|token|key|secret)=)([^&#\s"'<>`]+)/gi,
127
+ (match, prefix, value) => {
128
+ if (MARKER.test(value)) return match;
129
+ redactions++;
130
+ return `${prefix}[redacted:query]`;
131
+ }
132
+ );
133
+ return { text: out, redactions };
134
+ }
135
+ var SENSITIVE_SEGMENTS = /* @__PURE__ */ new Set([
136
+ "password",
137
+ "passwd",
138
+ "secret",
139
+ "token",
140
+ "authorization",
141
+ "cookie",
142
+ "credential",
143
+ "credentials"
144
+ ]);
145
+ var KEY_QUALIFIERS = /* @__PURE__ */ new Set([
146
+ "api",
147
+ "access",
148
+ "private",
149
+ "signing",
150
+ "secret",
151
+ "client"
152
+ ]);
153
+ function splitKey(key) {
154
+ return key.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase().split(/[_\-.]+/).filter(Boolean);
155
+ }
156
+ function isSensitiveKey(key) {
157
+ const segments = splitKey(key);
158
+ if (segments.some((segment) => SENSITIVE_SEGMENTS.has(segment))) return true;
159
+ return segments.some(
160
+ (segment, i) => segment === "key" && i > 0 && KEY_QUALIFIERS.has(segments[i - 1]) || // No separator at all: `apikey`, `accesskey`.
161
+ segment.endsWith("key") && KEY_QUALIFIERS.has(segment.slice(0, -3))
162
+ );
163
+ }
164
+ var QUOTES = /* @__PURE__ */ new Set(['"', "'"]);
165
+ var VALUE_TERMINATORS = /* @__PURE__ */ new Set([",", ";", "{", "}", '"', "'"]);
166
+ var QUERY_TERMINATORS = /* @__PURE__ */ new Set(["&", "#"]);
167
+ function isAsciiLetter(ch) {
168
+ return ch >= "a" && ch <= "z" || ch >= "A" && ch <= "Z";
169
+ }
170
+ function isAsciiDigit(ch) {
171
+ return ch >= "0" && ch <= "9";
172
+ }
173
+ function isKeyStart(ch) {
174
+ return isAsciiLetter(ch) || ch === "_";
175
+ }
176
+ function isKeyBody(ch) {
177
+ return isAsciiLetter(ch) || isAsciiDigit(ch) || ch === "_" || ch === "." || ch === "-";
178
+ }
179
+ function isSpaceOrTab(ch) {
180
+ return ch === " " || ch === " ";
181
+ }
182
+ function isWhitespace(ch) {
183
+ return ch === " " || ch === " " || ch === "\n" || ch === "\r" || ch === "\f" || ch === "\v";
184
+ }
185
+ function readQuotedValue(text, start) {
186
+ const quote = text[start];
187
+ let i = start + 1;
188
+ while (i < text.length) {
189
+ const ch = text[i];
190
+ if (ch === "\\") {
191
+ i += 2;
192
+ continue;
193
+ }
194
+ if (ch === quote) return i + 1;
195
+ if (ch === "\n" || ch === "\r") return i;
196
+ i++;
197
+ }
198
+ return text.length;
199
+ }
200
+ function readUnquotedValue(text, start, inQueryString) {
201
+ let i = start;
202
+ while (i < text.length) {
203
+ const ch = text[i];
204
+ if (isWhitespace(ch) || VALUE_TERMINATORS.has(ch)) break;
205
+ if (inQueryString && QUERY_TERMINATORS.has(ch)) break;
206
+ i++;
207
+ }
208
+ return i;
209
+ }
210
+ function parseAssignment(text, at) {
211
+ const before = at > 0 ? text[at - 1] : "";
212
+ if (isKeyBody(before)) return null;
213
+ let i = at;
214
+ const openingQuote = QUOTES.has(text[i] ?? "") ? text[i] : "";
215
+ if (openingQuote) i++;
216
+ if (!isKeyStart(text[i] ?? "")) return null;
217
+ const keyStart = i;
218
+ while (i < text.length && isKeyBody(text[i])) i++;
219
+ const key = text.slice(keyStart, i);
220
+ if (QUOTES.has(text[i] ?? "")) i++;
221
+ while (i < text.length && isSpaceOrTab(text[i])) i++;
222
+ if (text[i] !== ":" && text[i] !== "=") return null;
223
+ i++;
224
+ while (i < text.length && isSpaceOrTab(text[i])) i++;
225
+ const inQueryString = before === "?" || before === "&";
226
+ const start = i;
227
+ let end;
228
+ let quote = "";
229
+ if (QUOTES.has(text[start] ?? "")) {
230
+ quote = text[start];
231
+ end = readQuotedValue(text, start);
232
+ } else {
233
+ end = readUnquotedValue(text, start, inQueryString);
234
+ }
235
+ if (end === start) return null;
236
+ return { key, raw: text.slice(start, end), quote, start, end };
237
+ }
238
+ function redactAssignments(text) {
239
+ let redactions = 0;
240
+ let out = "";
241
+ let copiedTo = 0;
242
+ let cursor = 0;
243
+ while (cursor < text.length) {
244
+ const found = parseAssignment(text, cursor);
245
+ if (!found) {
246
+ cursor++;
247
+ continue;
248
+ }
249
+ if (MARKER.test(found.raw)) {
250
+ cursor = found.end;
251
+ continue;
252
+ }
253
+ if (!isSensitiveKey(found.key)) {
254
+ cursor = found.start;
255
+ continue;
256
+ }
257
+ out += text.slice(copiedTo, found.start);
258
+ out += `${found.quote}[redacted:config]${found.quote}`;
259
+ copiedTo = found.end;
260
+ cursor = found.end;
261
+ redactions++;
262
+ }
263
+ out += text.slice(copiedTo);
264
+ return { text: out, redactions };
265
+ }
266
+ function scrub(text) {
267
+ let out = text;
268
+ let redactions = 0;
269
+ for (const stage of [
270
+ redactAnchoredCredentials,
271
+ redactHeaderValues,
272
+ redactQuerySecrets,
273
+ redactAssignments
274
+ ]) {
275
+ const result = stage(out);
276
+ out = result.text;
277
+ redactions += result.redactions;
278
+ }
279
+ return { text: out, redactions };
280
+ }
281
+ var SCRUBBED_FIELDS = [
282
+ "content",
283
+ "intention",
284
+ "modelType",
285
+ "severity",
286
+ "tool",
287
+ "workaround",
288
+ "trigger",
289
+ "source",
290
+ "useCase",
291
+ "harness",
292
+ "harnessSource",
293
+ "transport"
294
+ ];
295
+ function scrubSubmission(row) {
296
+ const clean = { ...row };
297
+ for (const field of SCRUBBED_FIELDS) {
298
+ const value = clean[field];
299
+ if (typeof value !== "string") continue;
300
+ const result = scrub(value);
301
+ clean[field] = result.text;
302
+ clean.redactions += result.redactions;
303
+ }
304
+ clean.scrubVersion = SCRUB_VERSION;
305
+ return clean;
306
+ }
307
+ // Annotate the CommonJS export names for ESM import in node:
308
+ 0 && (module.exports = {
309
+ ALP_REFERRAL_SOURCES,
310
+ ALP_REFERRAL_USE_CASES,
311
+ ALP_ROUTES,
312
+ ALREADY_SUBMITTED_NOTE,
313
+ ONCE_PER_ACCOUNT_ROUTES,
314
+ isOncePerAccountRoute,
315
+ isSinkUrl,
316
+ scrubSubmission,
317
+ sinkRouteUrl
318
+ });
319
+ //# sourceMappingURL=alp.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alp.ts","../src/alp/sink.ts","../src/alp/scrub.ts"],"sourcesContent":["// The ALP contract shared with the Convex sink: route names, the\n// once-per-account rule and the scrubber. Pure, so a Convex function can\n// bundle it without pulling the HTTP handler in.\nexport {\n ALP_ROUTES,\n ALP_REFERRAL_SOURCES,\n ALP_REFERRAL_USE_CASES,\n ALREADY_SUBMITTED_NOTE,\n ONCE_PER_ACCOUNT_ROUTES,\n isOncePerAccountRoute,\n isSinkUrl,\n sinkRouteUrl,\n} from './alp/sink.js';\nexport type {\n AlpReferralSource,\n AlpReferralUseCase,\n AlpRoute,\n OncePerAccountRoute,\n} from './alp/sink.js';\nexport { scrubSubmission } from './alp/scrub.js';\n","// The ALP constants and sink address that more than one side needs: the\n// hosted server, the Convex functions, and the tool definitions. This module\n// imports nothing, so anything can import it without a cycle; the ingest\n// handler cannot play that role because it imports src/http.ts.\n\n// Every ALP route. The Convex validators are built from this list.\nexport const ALP_ROUTES = [\n 'feedback',\n 'journal',\n 'question',\n 'referral',\n] as const;\nexport type AlpRoute = (typeof ALP_ROUTES)[number];\n\n// Routes that take ONE answer per account, ever (design doc, \"RT4\"). The\n// Convex mutation dedupes on this list and /alp/answered validates against it.\n// The next one (the agent NPS score, CON-1729) is a literal added here, in the\n// `oncePerAccountRoute` Convex validator, and in the route lists, plus the\n// tool itself and its own hasAnswered call.\nexport const ONCE_PER_ACCOUNT_ROUTES = ['referral'] as const;\nexport type OncePerAccountRoute = (typeof ONCE_PER_ACCOUNT_ROUTES)[number];\n\nexport function isOncePerAccountRoute(\n route: unknown\n): route is OncePerAccountRoute {\n return (ONCE_PER_ACCOUNT_ROUTES as readonly unknown[]).includes(route);\n}\n\n// What the agent reads when a once-per-account route already holds its\n// answer. The ingest sends it and the tool says it; one sentence, one place.\nexport const ALREADY_SUBMITTED_NOTE =\n 'This account has already made this one-time submission; nothing new was stored. Do not call this again on this account — continue your task.';\n\n// Referral route: why Runpod came up. Constrained like severity so it counts.\n// Lives here, not in ingest.ts, so the tool file can import it without\n// pulling the ingest handler (and its import of src/http.ts) into a cycle.\nexport const ALP_REFERRAL_SOURCES = [\n 'searched',\n 'user_named',\n 'recommended',\n 'other',\n] as const;\nexport type AlpReferralSource = (typeof ALP_REFERRAL_SOURCES)[number];\n\n// Referral route: the kind of work that brought the user to Runpod. A\n// category instead of the user's words, so nothing they typed is stored.\nexport const ALP_REFERRAL_USE_CASES = [\n 'inference',\n 'fine_tuning',\n 'training',\n 'image_video_generation',\n 'dev_environment',\n 'batch_processing',\n 'other',\n] as const;\nexport type AlpReferralUseCase = (typeof ALP_REFERRAL_USE_CASES)[number];\n\n/** The sink is a Convex HTTP action, always. Anything else is a typo. */\nexport function isSinkUrl(value: string): boolean {\n try {\n const url = new URL(value);\n return (\n url.protocol === 'https:' &&\n url.hostname.endsWith('.convex.site') &&\n url.pathname === '/alp/submit'\n );\n } catch {\n return false;\n }\n}\n\n/** A sibling route on the same deployment as the configured submit URL.\n * Derived, never configured, so no second variable can point elsewhere. */\nexport function sinkRouteUrl(submitUrl: string, path: string): string | null {\n if (!isSinkUrl(submitUrl)) return null;\n const url = new URL(submitUrl);\n url.pathname = path;\n return url.toString();\n}\n","// Best-effort secret redaction before forwarding to storage and again at the\n// Convex write boundary. This catches recognizable tokens and sensitive config\n// assignments; arbitrary unlabeled secrets cannot reliably be identified.\n//\n// Four stages, run in this order, each a pure function with its own table\n// of rules and its own tests:\n//\n// A. anchored credentials — shapes recognizable on their own (rpa_, JWT...)\n// B. sensitive headers — the WHOLE value of Authorization/Cookie/...\n// C. URL query secrets — api_key/token/key/secret parameters\n// D. config assignments — `key = value` / `key: value` by key name\n//\n// Specific before broad is the invariant. Several stage-A rules identify a\n// credential by the word in front of it (`Bearer <opaque>`), and stage D stops\n// an unquoted value at the first space or semicolon — so run in the other\n// order, C consumes \"Bearer\", destroys the anchor, and leaves the secret in\n// plaintext looking redacted. Stage B exists because header values are\n// DEFINED by containing spaces and semicolons (`Basic <cred>`, `a=1; b=2`),\n// which is exactly the shape the token-level matcher cannot hold whole.\n\nexport interface ScrubResult {\n text: string;\n /** How many redactions fired — a redaction rate is itself a metric. */\n redactions: number;\n}\n\n// Bump when any stage's rules change so stored rows record which pass they got.\nexport const SCRUB_VERSION = 5;\n\nconst MARKER = /^[\"']?\\[redacted:[a-z_]+\\][\"']?$/;\n\n// ---- Stage A: anchored credential shapes --------------------------------\n\nconst CREDENTIAL_PATTERNS: Array<{ name: string; re: RegExp }> = [\n // Runpod API keys.\n { name: 'runpod_key', re: /\\brpa_[A-Za-z0-9]{16,}\\b/g },\n // Bearer credentials pasted with their header.\n { name: 'bearer', re: /\\bBearer\\s+[A-Za-z0-9._~+/=-]{16,}/g },\n // Common vendor key prefixes.\n {\n name: 'vendor_key',\n re: /\\b(?:sk|pk|ghp|gho|phc|phx|xoxb|xoxp)[-_][A-Za-z0-9_-]{16,}\\b/g,\n },\n // Hugging Face access tokens.\n { name: 'hugging_face', re: /\\bhf_[A-Za-z0-9]{30,}\\b/g },\n // AWS access key ids.\n { name: 'aws_key', re: /\\bAKIA[0-9A-Z]{16}\\b/g },\n // Three-segment JWTs.\n {\n name: 'jwt',\n re: /\\beyJ[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\.[A-Za-z0-9_-]{8,}\\b/g,\n },\n];\n\nexport function redactAnchoredCredentials(text: string): ScrubResult {\n let redactions = 0;\n let out = text;\n for (const { name, re } of CREDENTIAL_PATTERNS) {\n out = out.replace(re, () => {\n redactions++;\n return `[redacted:${name}]`;\n });\n }\n return { text: out, redactions };\n}\n\n// ---- Stage B: sensitive headers, whole value ----------------------------\n\n// Headers are matched wherever they appear, not only at line start: pastes\n// arrive inline (\"… got 401 with Authorization: Basic xyz | cookie: a=1; b=2\")\n// and the first version, anchored ^…$, missed exactly that and fell through\n// to the token matcher — which redacted the word \"Basic\" and kept the\n// credential. So the VALUE is bounded by its own shape rather than by the end\n// of the line: an auth header is `<scheme> <token>` or a bare token; a cookie\n// header is a `k=v; k=v` chain. Header names are case-insensitive.\nconst AUTH_HEADER =\n /\\b((?:proxy-)?authorization|x-api-key|x-auth-token|x-runpod-token)(\\s*:\\s*)((?:basic|bearer|token|digest|negotiate|ntlm|apikey)\\s+[^\\s|,]+|[^\\s|,]+)/gi;\nconst COOKIE_HEADER =\n /\\b(cookie|set-cookie)(\\s*:\\s*)([^\\s;=,|]+=[^\\s;,|]*(?:\\s*;\\s*[^\\s;=,|]+(?:=[^\\s;,|]*)?)*)/gi;\n\nexport function redactHeaderValues(text: string): ScrubResult {\n let redactions = 0;\n const replace = (match: string, name: string, sep: string, value: string) => {\n // Stage A may already have replaced the value; keep its marker.\n if (MARKER.test(value)) return match;\n redactions++;\n return `${name}${sep}[redacted:header]`;\n };\n const out = text\n .replace(AUTH_HEADER, replace)\n .replace(COOKIE_HEADER, replace);\n return { text: out, redactions };\n}\n\n// ---- Stage C: URL query credentials -----------------------------------\n\n// URL query credentials stop at the next query field, fragment or text boundary.\n// Preserve parameter names and non-secret query fields for diagnostics.\nexport function redactQuerySecrets(text: string): ScrubResult {\n let redactions = 0;\n const out = text.replace(\n /([?&](?:api[_-]?key|access[_-]?token|token|key|secret)=)([^&#\\s\"'<>`]+)/gi,\n (match, prefix: string, value: string) => {\n if (MARKER.test(value)) return match;\n redactions++;\n return `${prefix}[redacted:query]`;\n }\n );\n return { text: out, redactions };\n}\n\n// ---- Stage D: config assignments, by key name ---------------------------\n\n// Keys are compared segment by segment, never by substring: a substring test\n// redacts `tokenizer: llama-3` because it contains \"token\". Split on\n// separators AND camelCase, so `databasePassword` and `clientSecret` become\n// segment lists rather than one opaque word.\nconst SENSITIVE_SEGMENTS = new Set([\n 'password',\n 'passwd',\n 'secret',\n 'token',\n 'authorization',\n 'cookie',\n 'credential',\n 'credentials',\n]);\n\n// `key` alone is too common to be sensitive (primary_key, cache_key). It\n// counts when the segment right before it qualifies it — anywhere in the\n// name, so SERVICE_API_KEY and STRIPE_ACCESS_KEY match, not only api_key.\nconst KEY_QUALIFIERS = new Set([\n 'api',\n 'access',\n 'private',\n 'signing',\n 'secret',\n 'client',\n]);\n\nexport function splitKey(key: string): string[] {\n return key\n .replace(/([a-z0-9])([A-Z])/g, '$1_$2')\n .toLowerCase()\n .split(/[_\\-.]+/)\n .filter(Boolean);\n}\n\nexport function isSensitiveKey(key: string): boolean {\n const segments = splitKey(key);\n if (segments.some((segment) => SENSITIVE_SEGMENTS.has(segment))) return true;\n return segments.some(\n (segment, i) =>\n (segment === 'key' && i > 0 && KEY_QUALIFIERS.has(segments[i - 1])) ||\n // No separator at all: `apikey`, `accesskey`.\n (segment.endsWith('key') && KEY_QUALIFIERS.has(segment.slice(0, -3)))\n );\n}\n\n// Covers JSON, YAML and shell env assignments, including quoted values with\n// spaces or escaped quotes. The field name is kept for diagnostic context.\n//\n// This was one regex with a lookbehind, two quoted-string alternatives, a\n// negated value class and a trailing lookahead. It was unreadable, and every\n// fix to one case broke another: excluding `&` from the value class so a URL's\n// second query parameter survived also truncated `password: p&ss#word123` at\n// the `&` and leaked the rest. Both behaviors were correct for their own\n// input and the character class had no way to tell the two apart, because the\n// distinction is context, not characters.\n//\n// So the scan is written out: a cursor, one small function per token, and the\n// value's terminator set chosen from where the key sits. Each rule below is a\n// named predicate that can be read and tested on its own.\n\nconst QUOTES = new Set(['\"', \"'\"]);\n\n// A value always ends at one of these: `{`/`}` and `,`/`;` close a JSON or\n// inline-YAML member, and a quote cannot appear inside an unquoted value.\nconst VALUE_TERMINATORS = new Set([',', ';', '{', '}', '\"', \"'\"]);\n\n// `&` and `#` end a value ONLY inside a URL query, where they begin the next\n// parameter and the fragment. In a config file they are ordinary password\n// characters — `p&ss#word` is one value, not three.\nconst QUERY_TERMINATORS = new Set(['&', '#']);\n\nfunction isAsciiLetter(ch: string): boolean {\n return (ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z');\n}\n\nfunction isAsciiDigit(ch: string): boolean {\n return ch >= '0' && ch <= '9';\n}\n\n/** A key begins with a letter or underscore, never a digit — so `10:30` in a\n * timestamp is not read as an assignment. */\nfunction isKeyStart(ch: string): boolean {\n return isAsciiLetter(ch) || ch === '_';\n}\n\n/** Inside a key name: adds digits and the three separators that `splitKey`\n * later splits on, so `aws.secretKey` and `DB_PASSWORD` arrive whole. */\nfunction isKeyBody(ch: string): boolean {\n return (\n isAsciiLetter(ch) ||\n isAsciiDigit(ch) ||\n ch === '_' ||\n ch === '.' ||\n ch === '-'\n );\n}\n\n/** Horizontal space only. A newline after the separator means the value is on\n * another line, so `db:\\n password: x` must not read `password` as db's\n * value — the bug that let nested YAML through entirely. */\nfunction isSpaceOrTab(ch: string): boolean {\n return ch === ' ' || ch === '\\t';\n}\n\nfunction isWhitespace(ch: string): boolean {\n return (\n ch === ' ' ||\n ch === '\\t' ||\n ch === '\\n' ||\n ch === '\\r' ||\n ch === '\\f' ||\n ch === '\\v'\n );\n}\n\ninterface Assignment {\n /** Bare key name: quotes and the separator stripped. */\n key: string;\n /** Raw value text as it appears, including its quotes when quoted. */\n raw: string;\n /** Quote character to reinstate around a replacement, or `''`. */\n quote: string;\n /** Half-open range of the value within the source text. */\n start: number;\n end: number;\n}\n\n/** Reads a quoted value, honoring backslash escapes. An unterminated quote is\n * a truncated paste, so it reads to the end of the line rather than giving\n * up: for a redactor, over-reading a secret is the safe direction. */\nfunction readQuotedValue(text: string, start: number): number {\n const quote = text[start];\n let i = start + 1;\n while (i < text.length) {\n const ch = text[i];\n if (ch === '\\\\') {\n i += 2;\n continue;\n }\n if (ch === quote) return i + 1;\n if (ch === '\\n' || ch === '\\r') return i;\n i++;\n }\n return text.length;\n}\n\n/** Reads an unquoted value up to the first terminator for its context. */\nfunction readUnquotedValue(\n text: string,\n start: number,\n inQueryString: boolean\n): number {\n let i = start;\n while (i < text.length) {\n const ch = text[i];\n if (isWhitespace(ch) || VALUE_TERMINATORS.has(ch)) break;\n if (inQueryString && QUERY_TERMINATORS.has(ch)) break;\n i++;\n }\n return i;\n}\n\n/** Parses `key: value` or `key = value` beginning at `at`, or null if there is\n * no assignment there. */\nfunction parseAssignment(text: string, at: number): Assignment | null {\n const before = at > 0 ? text[at - 1] : '';\n // Mid-identifier: `my_password` must not also match as key `password`.\n if (isKeyBody(before)) return null;\n\n let i = at;\n const openingQuote = QUOTES.has(text[i] ?? '') ? text[i] : '';\n if (openingQuote) i++;\n if (!isKeyStart(text[i] ?? '')) return null;\n\n const keyStart = i;\n while (i < text.length && isKeyBody(text[i])) i++;\n const key = text.slice(keyStart, i);\n\n // A closing quote is tolerated whether or not one opened: pasted snippets\n // arrive truncated, and `password\": x` should still redact.\n if (QUOTES.has(text[i] ?? '')) i++;\n\n while (i < text.length && isSpaceOrTab(text[i])) i++;\n if (text[i] !== ':' && text[i] !== '=') return null;\n i++;\n while (i < text.length && isSpaceOrTab(text[i])) i++;\n\n // `?api_key=` or `&token=` — this key sits in a URL query string.\n const inQueryString = before === '?' || before === '&';\n\n const start = i;\n let end: number;\n let quote = '';\n if (QUOTES.has(text[start] ?? '')) {\n quote = text[start];\n end = readQuotedValue(text, start);\n } else {\n end = readUnquotedValue(text, start, inQueryString);\n }\n // A parent key such as `db:` has no value of its own.\n if (end === start) return null;\n\n return { key, raw: text.slice(start, end), quote, start, end };\n}\n\nexport function redactAssignments(text: string): ScrubResult {\n let redactions = 0;\n let out = '';\n let copiedTo = 0;\n let cursor = 0;\n\n while (cursor < text.length) {\n const found = parseAssignment(text, cursor);\n if (!found) {\n cursor++;\n continue;\n }\n\n // Already handled by an earlier stage — keep its marker, do not count it\n // twice, and resume past it.\n if (MARKER.test(found.raw)) {\n cursor = found.end;\n continue;\n }\n\n if (!isSensitiveKey(found.key)) {\n // Resume INSIDE the value, not after it. A non-sensitive assignment can\n // contain one: `https://h/?password=x` parses as key `https` whose value\n // is the whole URL, and skipping it leaked the password outright.\n cursor = found.start;\n continue;\n }\n\n out += text.slice(copiedTo, found.start);\n out += `${found.quote}[redacted:config]${found.quote}`;\n copiedTo = found.end;\n cursor = found.end;\n redactions++;\n }\n\n out += text.slice(copiedTo);\n return { text: out, redactions };\n}\n\n// ---- Composition ---------------------------------------------------------\n\nexport function scrub(text: string): ScrubResult {\n let out = text;\n let redactions = 0;\n for (const stage of [\n redactAnchoredCredentials,\n redactHeaderValues,\n redactQuerySecrets,\n redactAssignments,\n ]) {\n const result = stage(out);\n out = result.text;\n redactions += result.redactions;\n }\n return { text: out, redactions };\n}\n\n// Only user-authored text is scrubbed; resolved identity and server timestamps\n// remain authoritative. Reapplying this at the sink is safe and idempotent.\nexport const SCRUBBED_FIELDS = [\n 'content',\n 'intention',\n 'modelType',\n 'severity',\n 'tool',\n 'workaround',\n 'trigger',\n 'source',\n 'useCase',\n 'harness',\n 'harnessSource',\n 'transport',\n] as const;\n\nexport function scrubSubmission<\n T extends {\n content: string;\n intention?: string;\n modelType?: string;\n severity?: string;\n tool?: string;\n workaround?: string;\n trigger?: string;\n source?: string;\n useCase?: string;\n harness?: string;\n harnessSource?: string;\n transport?: string;\n redactions: number;\n scrubVersion: number;\n },\n>(row: T): T {\n const clean = { ...row };\n // EVERY agent-writable string field belongs in this list. A field added to\n // the submission shape but not here is stored unscrubbed, which is how a\n // pasted credential reaches the table — the fields most likely to carry one\n // are exactly the free-text ones a new route adds. Guarded by a test that\n // walks the submission type, so the omission fails CI rather than shipping.\n for (const field of SCRUBBED_FIELDS) {\n const value = clean[field];\n if (typeof value !== 'string') continue;\n const result = scrub(value);\n clean[field] = result.text;\n clean.redactions += result.redactions;\n }\n clean.scrubVersion = SCRUB_VERSION;\n return clean;\n}\n"],"mappings":"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","names":[]}
package/dist/alp.d.cts ADDED
@@ -0,0 +1,20 @@
1
+ export { a as ALP_REFERRAL_SOURCES, b as ALP_REFERRAL_USE_CASES, A as ALP_ROUTES, c as ALREADY_SUBMITTED_NOTE, e as AlpReferralSource, f as AlpReferralUseCase, g as AlpRoute, O as ONCE_PER_ACCOUNT_ROUTES, h as OncePerAccountRoute, i as isOncePerAccountRoute, d as isSinkUrl, s as sinkRouteUrl } from './sink-Cm8lp16D.cjs';
2
+
3
+ declare function scrubSubmission<T extends {
4
+ content: string;
5
+ intention?: string;
6
+ modelType?: string;
7
+ severity?: string;
8
+ tool?: string;
9
+ workaround?: string;
10
+ trigger?: string;
11
+ source?: string;
12
+ useCase?: string;
13
+ harness?: string;
14
+ harnessSource?: string;
15
+ transport?: string;
16
+ redactions: number;
17
+ scrubVersion: number;
18
+ }>(row: T): T;
19
+
20
+ export { scrubSubmission };
package/dist/alp.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ export { a as ALP_REFERRAL_SOURCES, b as ALP_REFERRAL_USE_CASES, A as ALP_ROUTES, c as ALREADY_SUBMITTED_NOTE, e as AlpReferralSource, f as AlpReferralUseCase, g as AlpRoute, O as ONCE_PER_ACCOUNT_ROUTES, h as OncePerAccountRoute, i as isOncePerAccountRoute, d as isSinkUrl, s as sinkRouteUrl } from './sink-Cm8lp16D.js';
2
+
3
+ declare function scrubSubmission<T extends {
4
+ content: string;
5
+ intention?: string;
6
+ modelType?: string;
7
+ severity?: string;
8
+ tool?: string;
9
+ workaround?: string;
10
+ trigger?: string;
11
+ source?: string;
12
+ useCase?: string;
13
+ harness?: string;
14
+ harnessSource?: string;
15
+ transport?: string;
16
+ redactions: number;
17
+ scrubVersion: number;
18
+ }>(row: T): T;
19
+
20
+ export { scrubSubmission };
package/dist/alp.mjs ADDED
@@ -0,0 +1,25 @@
1
+ import {
2
+ scrubSubmission
3
+ } from "./chunk-H3NZYQIB.mjs";
4
+ import {
5
+ ALP_REFERRAL_SOURCES,
6
+ ALP_REFERRAL_USE_CASES,
7
+ ALP_ROUTES,
8
+ ALREADY_SUBMITTED_NOTE,
9
+ ONCE_PER_ACCOUNT_ROUTES,
10
+ isOncePerAccountRoute,
11
+ isSinkUrl,
12
+ sinkRouteUrl
13
+ } from "./chunk-577IYCJT.mjs";
14
+ export {
15
+ ALP_REFERRAL_SOURCES,
16
+ ALP_REFERRAL_USE_CASES,
17
+ ALP_ROUTES,
18
+ ALREADY_SUBMITTED_NOTE,
19
+ ONCE_PER_ACCOUNT_ROUTES,
20
+ isOncePerAccountRoute,
21
+ isSinkUrl,
22
+ scrubSubmission,
23
+ sinkRouteUrl
24
+ };
25
+ //# sourceMappingURL=alp.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
@@ -0,0 +1,53 @@
1
+ // src/alp/sink.ts
2
+ var ALP_ROUTES = [
3
+ "feedback",
4
+ "journal",
5
+ "question",
6
+ "referral"
7
+ ];
8
+ var ONCE_PER_ACCOUNT_ROUTES = ["referral"];
9
+ function isOncePerAccountRoute(route) {
10
+ return ONCE_PER_ACCOUNT_ROUTES.includes(route);
11
+ }
12
+ var ALREADY_SUBMITTED_NOTE = "This account has already made this one-time submission; nothing new was stored. Do not call this again on this account \u2014 continue your task.";
13
+ var ALP_REFERRAL_SOURCES = [
14
+ "searched",
15
+ "user_named",
16
+ "recommended",
17
+ "other"
18
+ ];
19
+ var ALP_REFERRAL_USE_CASES = [
20
+ "inference",
21
+ "fine_tuning",
22
+ "training",
23
+ "image_video_generation",
24
+ "dev_environment",
25
+ "batch_processing",
26
+ "other"
27
+ ];
28
+ function isSinkUrl(value) {
29
+ try {
30
+ const url = new URL(value);
31
+ return url.protocol === "https:" && url.hostname.endsWith(".convex.site") && url.pathname === "/alp/submit";
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ function sinkRouteUrl(submitUrl, path) {
37
+ if (!isSinkUrl(submitUrl)) return null;
38
+ const url = new URL(submitUrl);
39
+ url.pathname = path;
40
+ return url.toString();
41
+ }
42
+
43
+ export {
44
+ ALP_ROUTES,
45
+ ONCE_PER_ACCOUNT_ROUTES,
46
+ isOncePerAccountRoute,
47
+ ALREADY_SUBMITTED_NOTE,
48
+ ALP_REFERRAL_SOURCES,
49
+ ALP_REFERRAL_USE_CASES,
50
+ isSinkUrl,
51
+ sinkRouteUrl
52
+ };
53
+ //# sourceMappingURL=chunk-577IYCJT.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/alp/sink.ts"],"sourcesContent":["// The ALP constants and sink address that more than one side needs: the\n// hosted server, the Convex functions, and the tool definitions. This module\n// imports nothing, so anything can import it without a cycle; the ingest\n// handler cannot play that role because it imports src/http.ts.\n\n// Every ALP route. The Convex validators are built from this list.\nexport const ALP_ROUTES = [\n 'feedback',\n 'journal',\n 'question',\n 'referral',\n] as const;\nexport type AlpRoute = (typeof ALP_ROUTES)[number];\n\n// Routes that take ONE answer per account, ever (design doc, \"RT4\"). The\n// Convex mutation dedupes on this list and /alp/answered validates against it.\n// The next one (the agent NPS score, CON-1729) is a literal added here, in the\n// `oncePerAccountRoute` Convex validator, and in the route lists, plus the\n// tool itself and its own hasAnswered call.\nexport const ONCE_PER_ACCOUNT_ROUTES = ['referral'] as const;\nexport type OncePerAccountRoute = (typeof ONCE_PER_ACCOUNT_ROUTES)[number];\n\nexport function isOncePerAccountRoute(\n route: unknown\n): route is OncePerAccountRoute {\n return (ONCE_PER_ACCOUNT_ROUTES as readonly unknown[]).includes(route);\n}\n\n// What the agent reads when a once-per-account route already holds its\n// answer. The ingest sends it and the tool says it; one sentence, one place.\nexport const ALREADY_SUBMITTED_NOTE =\n 'This account has already made this one-time submission; nothing new was stored. Do not call this again on this account — continue your task.';\n\n// Referral route: why Runpod came up. Constrained like severity so it counts.\n// Lives here, not in ingest.ts, so the tool file can import it without\n// pulling the ingest handler (and its import of src/http.ts) into a cycle.\nexport const ALP_REFERRAL_SOURCES = [\n 'searched',\n 'user_named',\n 'recommended',\n 'other',\n] as const;\nexport type AlpReferralSource = (typeof ALP_REFERRAL_SOURCES)[number];\n\n// Referral route: the kind of work that brought the user to Runpod. A\n// category instead of the user's words, so nothing they typed is stored.\nexport const ALP_REFERRAL_USE_CASES = [\n 'inference',\n 'fine_tuning',\n 'training',\n 'image_video_generation',\n 'dev_environment',\n 'batch_processing',\n 'other',\n] as const;\nexport type AlpReferralUseCase = (typeof ALP_REFERRAL_USE_CASES)[number];\n\n/** The sink is a Convex HTTP action, always. Anything else is a typo. */\nexport function isSinkUrl(value: string): boolean {\n try {\n const url = new URL(value);\n return (\n url.protocol === 'https:' &&\n url.hostname.endsWith('.convex.site') &&\n url.pathname === '/alp/submit'\n );\n } catch {\n return false;\n }\n}\n\n/** A sibling route on the same deployment as the configured submit URL.\n * Derived, never configured, so no second variable can point elsewhere. */\nexport function sinkRouteUrl(submitUrl: string, path: string): string | null {\n if (!isSinkUrl(submitUrl)) return null;\n const url = new URL(submitUrl);\n url.pathname = path;\n return url.toString();\n}\n"],"mappings":";AAMO,IAAM,aAAa;AAAA,EACxB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAQO,IAAM,0BAA0B,CAAC,UAAU;AAG3C,SAAS,sBACd,OAC8B;AAC9B,SAAQ,wBAA+C,SAAS,KAAK;AACvE;AAIO,IAAM,yBACX;AAKK,IAAM,uBAAuB;AAAA,EAClC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAKO,IAAM,yBAAyB;AAAA,EACpC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAIO,SAAS,UAAU,OAAwB;AAChD,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK;AACzB,WACE,IAAI,aAAa,YACjB,IAAI,SAAS,SAAS,cAAc,KACpC,IAAI,aAAa;AAAA,EAErB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAIO,SAAS,aAAa,WAAmB,MAA6B;AAC3E,MAAI,CAAC,UAAU,SAAS,EAAG,QAAO;AAClC,QAAM,MAAM,IAAI,IAAI,SAAS;AAC7B,MAAI,WAAW;AACf,SAAO,IAAI,SAAS;AACtB;","names":[]}