@runpod/mcp-server 4.9.1 → 4.10.1
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.
- package/CHANGELOG.md +35 -0
- package/README.md +2 -11
- package/dist/alp.cjs +21 -2
- package/dist/alp.cjs.map +1 -1
- package/dist/alp.d.cts +2 -1
- package/dist/alp.d.ts +2 -1
- package/dist/alp.mjs +3 -1
- package/dist/{chunk-N4OSRS4A.mjs → chunk-AKX5ZMCL.mjs} +1931 -221
- package/dist/chunk-AKX5ZMCL.mjs.map +1 -0
- package/dist/{chunk-6IETGSON.mjs → chunk-FQJFKWUV.mjs} +32 -1
- package/dist/chunk-FQJFKWUV.mjs.map +1 -0
- package/dist/{chunk-H3NZYQIB.mjs → chunk-WWXM5EES.mjs} +20 -3
- package/dist/chunk-WWXM5EES.mjs.map +1 -0
- package/dist/hosted.cjs +1986 -217
- package/dist/hosted.cjs.map +1 -1
- package/dist/hosted.mjs +16 -5
- package/dist/hosted.mjs.map +1 -1
- package/dist/http.cjs +1960 -219
- package/dist/http.cjs.map +1 -1
- package/dist/http.mjs +2 -2
- package/dist/specgen/profile.cjs +31 -0
- package/dist/specgen/profile.cjs.map +1 -1
- package/dist/specgen/profile.mjs +1 -1
- package/dist/stdio.cjs +1960 -219
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1960 -219
- package/dist/stdio.mjs.map +1 -1
- package/package.json +4 -3
- package/dist/chunk-6IETGSON.mjs.map +0 -1
- package/dist/chunk-H3NZYQIB.mjs.map +0 -1
- package/dist/chunk-N4OSRS4A.mjs.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# @runpod/mcp-server
|
|
2
2
|
|
|
3
|
+
## 4.10.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 12d34ee: Treat a `null` optional argument as omitted on the hand-written tools too.
|
|
8
|
+
Strict-schema clients such as Codex and opencode send every property, and the
|
|
9
|
+
fix for generated tools did not reach the curated ones: `get-job-status` and
|
|
10
|
+
`runsync-endpoint` rejected `wait: null` with a 400, `set-endpoint-gpus`
|
|
11
|
+
rejected every null GPU field (`gpuIds`, `gpuCount`, `minCudaVersion` and the
|
|
12
|
+
rest) so it could not be called at all, and `run-endpoint` and
|
|
13
|
+
`runsync-endpoint` forwarded `webhook: null`, `policy: null` and
|
|
14
|
+
`s3Config: null` into the job body. A null required argument is still reported
|
|
15
|
+
as missing.
|
|
16
|
+
- 568328f: `stream-pod-logs` and `stream-worker-logs` no longer fail with "did not respond before the request deadline" when the log stream's headers take longer than `maxWaitMs`. Headers now get their own 10-second budget, and `maxWaitMs` measures how long the open stream is read. Snapshots are also capped at 32,000 serialized characters so they fit within MCP clients' tool-output limits: a capped snapshot keeps its leading lines, cuts only between timestamps where it can, clips oversized lines (measured after JSON escaping), and sets `truncated: true`. The tool descriptions now say that `since` is exclusive and replays container lines only.
|
|
17
|
+
- 1cb0082: Record why a hosted tool call failed. Failed calls now carry `error_class` (`capacity`, `balance`, `conflicting_args`, `validation`, `auth`, `permission`, `not_found`, `rate_limited`, `timeout`, `server` or `other`) on the anonymous analytics event, from a fixed list and never the upstream message. A 400 for "no instances currently available" now gets the capacity hint, the same as "no longer any instances available".
|
|
18
|
+
- 27fe395: Update `@runpod/plugin-knowledge` from 1.9.0 to 2.0.0, so `list-guides` and `read-guide` serve the latest plugin guides.
|
|
19
|
+
|
|
20
|
+
Plugin-knowledge 2.0.0 renames the journey guides with a `runpod-` prefix (`pod-doctor` is now `runpod-pod-doctor`). The server instructions name the new ids, and `read-guide` and `runpod://guides/` resource reads still accept the old ids through the bundle's alias map.
|
|
21
|
+
|
|
22
|
+
Update run: https://github.com/runpod/runpod-mcp/actions/runs/42194967234
|
|
23
|
+
|
|
24
|
+
## 4.10.0
|
|
25
|
+
|
|
26
|
+
### Minor Changes
|
|
27
|
+
|
|
28
|
+
- ba8ae08: Redact email addresses in ALP submissions, non-ASCII ones included, as `[redacted:email]`, counted in `redactions`. Runpod Pod SSH logins (`@ssh.runpod.io`) and the `git@` user of an SSH remote are kept. `SCRUB_VERSION` is now 6 and is exported from `@runpod/mcp-server/alp`. The ingest endpoint keeps `tool` only when it is shaped like a tool name (`^[A-Za-z0-9_-]{1,64}$`) and drops anything else, as it does an off-enum `severity`, and answers a write the sink refused at its cap (`capped: true`) with `recorded: false` and the do-not-retry note.
|
|
29
|
+
- 80d0d1f: Pin `@runpod/plugin-knowledge` to an exact version, checked before each release for npm provenance built in `runpod/runpod-plugins-official`, and leave the concept rules listed in `KNOWLEDGE_HIDDEN_RULES` (comma-separated rule ids) out of `lookup-concept` and `search-concepts`, citations included, so the hosted server can withdraw a rule before the knowledge release that deletes it ships. An id the bundle does not hold is logged at startup.
|
|
30
|
+
- ba8ae08: Resync the vendored v2 OpenAPI spec with production as of 2026-11-05 and regenerate the SDK schema and the tools. Adds 31 tools: organization groups, roles, invites and member updates and removal, cost centers, and endpoint cold-start, request and worker metrics. A pod's mounts now take up to 4 network volumes and 1 global volume. The `filters` query parameter on the pod, cluster, endpoint and network volume list routes is left out until the client can send JSON query parameters.
|
|
31
|
+
|
|
32
|
+
### Patch Changes
|
|
33
|
+
|
|
34
|
+
- 017cdc3: Update `@runpod/plugin-knowledge` from 1.8.0 to 1.9.0, so `list-guides` and `read-guide` serve the latest plugin guides.
|
|
35
|
+
|
|
36
|
+
Update run: https://github.com/runpod/runpod-mcp/actions/runs/41610964582
|
|
37
|
+
|
|
3
38
|
## 4.9.1
|
|
4
39
|
|
|
5
40
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ Without `--client` and without a terminal, the installer exits with code 2 and c
|
|
|
36
36
|
|
|
37
37
|
### Or install the whole Runpod plugin
|
|
38
38
|
|
|
39
|
-
For the full agent setup, the [official plugin marketplace](https://github.com/runpod/runpod-plugins-official) bundles this server with a router skill
|
|
39
|
+
For the full agent setup, the [official plugin marketplace](https://github.com/runpod/runpod-plugins-official) bundles this server with a router skill and task skills such as `runpod-mcp`, `runpodctl`, `runpod-flash`, `runpod-usage`, and `runpod-companion-clis`:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
42
|
npx skills add runpod/runpod-plugins-official
|
|
@@ -102,7 +102,7 @@ You can also [run the server locally](#run-locally-with-npx). The local server c
|
|
|
102
102
|
|
|
103
103
|
### Usage analytics
|
|
104
104
|
|
|
105
|
-
The hosted server records one anonymous event per tool call (tool name, status, duration, transport) and per guide resource read (guide URI, or `unknown` on a miss), plus one event when it starts blocking a 404 loop (tool name and block length). It never records your API key or any resource data, and drops tool arguments except on the guide and concept tools (`list-guides`, `read-guide`, `lookup-concept`, `search-concepts`), where it keeps the guide id, concept name or search words, plus the kind of guide read. Your identity is an irreversible salted hash. To opt out, send the header `X-Runpod-Analytics: off` — e.g. append `--header "X-Runpod-Analytics: off"` to the `claude mcp add` command. Running locally, nothing is ever sent.
|
|
105
|
+
The hosted server records one anonymous event per tool call (tool name, status, duration, transport, and for a failed call a failure category from a fixed list such as `capacity` or `validation`, never the error text) and per guide resource read (guide URI, or `unknown` on a miss), plus one event when it starts blocking a 404 loop (tool name and block length). It never records your API key or any resource data, and drops tool arguments except on the guide and concept tools (`list-guides`, `read-guide`, `lookup-concept`, `search-concepts`), where it keeps the guide id, concept name or search words, plus the kind of guide read. Your identity is an irreversible salted hash. To opt out, send the header `X-Runpod-Analytics: off` — e.g. append `--header "X-Runpod-Analytics: off"` to the `claude mcp add` command. Running locally, nothing is ever sent.
|
|
106
106
|
|
|
107
107
|
## Run locally with `npx`
|
|
108
108
|
|
|
@@ -167,15 +167,6 @@ Endpoints are image-based — pass an image and a GPU pool (a `pool` value from
|
|
|
167
167
|
|
|
168
168
|
See [`docs/configuration.md`](docs/configuration.md) for host overrides, private image pull (registry credentials vs ECR delegation), and large-output handling.
|
|
169
169
|
|
|
170
|
-
## Upgrading from 3.x
|
|
171
|
-
|
|
172
|
-
Eight tools follow their spec operationIds (e.g.
|
|
173
|
-
`create-container-registry-auth` → `create-registry`, `get-billing` →
|
|
174
|
-
`list-billing`) and `start-pod`/`stop-pod`/`restart-pod` fold into
|
|
175
|
-
`pod-action` with an `action` argument. The full old→new map is
|
|
176
|
-
[`specgen/old-mcp-tools.yaml`](specgen/old-mcp-tools.yaml). The server is
|
|
177
|
-
v2-only; `RUNPOD_REST_VERSION` and the v1 fallback are retired.
|
|
178
|
-
|
|
179
170
|
## Security
|
|
180
171
|
|
|
181
172
|
This server acts with the full permissions of the supplied API key.
|
package/dist/alp.cjs
CHANGED
|
@@ -25,6 +25,7 @@ __export(alp_exports, {
|
|
|
25
25
|
ALP_ROUTES: () => ALP_ROUTES,
|
|
26
26
|
ALREADY_SUBMITTED_NOTE: () => ALREADY_SUBMITTED_NOTE,
|
|
27
27
|
ONCE_PER_ACCOUNT_ROUTES: () => ONCE_PER_ACCOUNT_ROUTES,
|
|
28
|
+
SCRUB_VERSION: () => SCRUB_VERSION,
|
|
28
29
|
isOncePerAccountRoute: () => isOncePerAccountRoute,
|
|
29
30
|
isSinkUrl: () => isSinkUrl,
|
|
30
31
|
scrubSubmission: () => scrubSubmission,
|
|
@@ -75,7 +76,7 @@ function sinkRouteUrl(submitUrl, path) {
|
|
|
75
76
|
}
|
|
76
77
|
|
|
77
78
|
// src/alp/scrub.ts
|
|
78
|
-
var SCRUB_VERSION =
|
|
79
|
+
var SCRUB_VERSION = 6;
|
|
79
80
|
var MARKER = /^["']?\[redacted:[a-z_]+\]["']?$/;
|
|
80
81
|
var CREDENTIAL_PATTERNS = [
|
|
81
82
|
// Runpod API keys.
|
|
@@ -263,6 +264,22 @@ function redactAssignments(text) {
|
|
|
263
264
|
out += text.slice(copiedTo);
|
|
264
265
|
return { text: out, redactions };
|
|
265
266
|
}
|
|
267
|
+
var EMAIL = /(?<![\p{L}\p{M}\p{N}_.%+-])([\p{L}\p{M}\p{N}_.%+-]{1,64})@((?:[\p{L}\p{M}\p{N}-]{1,63}\.){1,8}[\p{L}\p{M}]{2,63})(?![\p{L}\p{M}\p{N}])/gu;
|
|
268
|
+
var RUNPOD_SSH_HOST = "ssh.runpod.io";
|
|
269
|
+
function redactEmails(text) {
|
|
270
|
+
let redactions = 0;
|
|
271
|
+
const out = text.replace(
|
|
272
|
+
EMAIL,
|
|
273
|
+
(address, local, domain, offset) => {
|
|
274
|
+
const next = text[offset + address.length];
|
|
275
|
+
if (domain.toLowerCase() === RUNPOD_SSH_HOST || local === "git" && (next === ":" || next === "/"))
|
|
276
|
+
return address;
|
|
277
|
+
redactions++;
|
|
278
|
+
return "[redacted:email]";
|
|
279
|
+
}
|
|
280
|
+
);
|
|
281
|
+
return { text: out, redactions };
|
|
282
|
+
}
|
|
266
283
|
function scrub(text) {
|
|
267
284
|
let out = text;
|
|
268
285
|
let redactions = 0;
|
|
@@ -270,7 +287,8 @@ function scrub(text) {
|
|
|
270
287
|
redactAnchoredCredentials,
|
|
271
288
|
redactHeaderValues,
|
|
272
289
|
redactQuerySecrets,
|
|
273
|
-
redactAssignments
|
|
290
|
+
redactAssignments,
|
|
291
|
+
redactEmails
|
|
274
292
|
]) {
|
|
275
293
|
const result = stage(out);
|
|
276
294
|
out = result.text;
|
|
@@ -311,6 +329,7 @@ function scrubSubmission(row) {
|
|
|
311
329
|
ALP_ROUTES,
|
|
312
330
|
ALREADY_SUBMITTED_NOTE,
|
|
313
331
|
ONCE_PER_ACCOUNT_ROUTES,
|
|
332
|
+
SCRUB_VERSION,
|
|
314
333
|
isOncePerAccountRoute,
|
|
315
334
|
isSinkUrl,
|
|
316
335
|
scrubSubmission,
|
package/dist/alp.cjs.map
CHANGED
|
@@ -1 +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":[]}
|
|
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 with its version. Pure, so a Convex\n// function can 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 { SCRUB_VERSION, 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, sensitive config\n// assignments and email addresses; arbitrary unlabeled secrets cannot reliably\n// be identified.\n//\n// Five 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// E. email addresses — personal data, not a credential\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 = 6;\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// ---- Stage E: email addresses -------------------------------------------\n\n// Letters, marks and digits of any script, so `josé@example.com` and\n// `用户@例子.example.com` are caught too. Quantifiers carry the address limits\n// (64-character local part, 63-character labels), and the local part must\n// start a run of address characters, so each run is tried once and a run of\n// more than 64 characters before the `@` is not an address. Together they\n// keep a long run with no `@` from costing quadratic time on a long\n// submission. A dotted version such as `pkg@1.2.3` is not matched: the\n// top-level label must be letters.\nconst EMAIL =\n /(?<![\\p{L}\\p{M}\\p{N}_.%+-])([\\p{L}\\p{M}\\p{N}_.%+-]{1,64})@((?:[\\p{L}\\p{M}\\p{N}-]{1,63}\\.){1,8}[\\p{L}\\p{M}]{2,63})(?![\\p{L}\\p{M}\\p{N}])/gu;\n\n// Two logins are kept: they name a machine, not a person, and the address is\n// what tells a reader where a command ran. They are Runpod's Pod SSH proxy\n// (`<podId>-<hash>@ssh.runpod.io`) and the git user of an SSH remote\n// (`git@github.com:org/repo.git`, `ssh://git@github.com/org/repo.git`).\nconst RUNPOD_SSH_HOST = 'ssh.runpod.io';\n\nexport function redactEmails(text: string): ScrubResult {\n let redactions = 0;\n const out = text.replace(\n EMAIL,\n (address: string, local: string, domain: string, offset: number) => {\n const next = text[offset + address.length];\n if (\n domain.toLowerCase() === RUNPOD_SSH_HOST ||\n (local === 'git' && (next === ':' || next === '/'))\n )\n return address;\n redactions++;\n return '[redacted:email]';\n }\n );\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 redactEmails,\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
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
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
2
|
|
|
3
|
+
declare const SCRUB_VERSION = 6;
|
|
3
4
|
declare function scrubSubmission<T extends {
|
|
4
5
|
content: string;
|
|
5
6
|
intention?: string;
|
|
@@ -17,4 +18,4 @@ declare function scrubSubmission<T extends {
|
|
|
17
18
|
scrubVersion: number;
|
|
18
19
|
}>(row: T): T;
|
|
19
20
|
|
|
20
|
-
export { scrubSubmission };
|
|
21
|
+
export { SCRUB_VERSION, scrubSubmission };
|
package/dist/alp.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
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
2
|
|
|
3
|
+
declare const SCRUB_VERSION = 6;
|
|
3
4
|
declare function scrubSubmission<T extends {
|
|
4
5
|
content: string;
|
|
5
6
|
intention?: string;
|
|
@@ -17,4 +18,4 @@ declare function scrubSubmission<T extends {
|
|
|
17
18
|
scrubVersion: number;
|
|
18
19
|
}>(row: T): T;
|
|
19
20
|
|
|
20
|
-
export { scrubSubmission };
|
|
21
|
+
export { SCRUB_VERSION, scrubSubmission };
|
package/dist/alp.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
|
+
SCRUB_VERSION,
|
|
2
3
|
scrubSubmission
|
|
3
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WWXM5EES.mjs";
|
|
4
5
|
import {
|
|
5
6
|
ALP_REFERRAL_SOURCES,
|
|
6
7
|
ALP_REFERRAL_USE_CASES,
|
|
@@ -17,6 +18,7 @@ export {
|
|
|
17
18
|
ALP_ROUTES,
|
|
18
19
|
ALREADY_SUBMITTED_NOTE,
|
|
19
20
|
ONCE_PER_ACCOUNT_ROUTES,
|
|
21
|
+
SCRUB_VERSION,
|
|
20
22
|
isOncePerAccountRoute,
|
|
21
23
|
isSinkUrl,
|
|
22
24
|
scrubSubmission,
|