@runpod/mcp-server 4.9.1 → 4.10.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.
- package/CHANGELOG.md +14 -0
- 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-6IETGSON.mjs → chunk-FQJFKWUV.mjs} +32 -1
- package/dist/chunk-FQJFKWUV.mjs.map +1 -0
- package/dist/{chunk-N4OSRS4A.mjs → chunk-JTK6RETN.mjs} +1707 -151
- package/dist/chunk-JTK6RETN.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 +1757 -142
- package/dist/hosted.cjs.map +1 -1
- package/dist/hosted.mjs +16 -5
- package/dist/hosted.mjs.map +1 -1
- package/dist/http.cjs +1736 -149
- 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 +1736 -149
- package/dist/stdio.cjs.map +1 -1
- package/dist/stdio.mjs +1736 -149
- 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/dist/hosted.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
SCRUB_VERSION,
|
|
3
3
|
scrubSubmission
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-WWXM5EES.mjs";
|
|
5
5
|
import {
|
|
6
6
|
defaultCredentialChecker,
|
|
7
7
|
extractBearerToken,
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
handleMcpRequest,
|
|
11
11
|
parseSearchRequest,
|
|
12
12
|
searchKnowledge
|
|
13
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-JTK6RETN.mjs";
|
|
14
14
|
import {
|
|
15
15
|
ALP_REFERRAL_SOURCES,
|
|
16
16
|
ALP_REFERRAL_USE_CASES,
|
|
@@ -20,13 +20,14 @@ import {
|
|
|
20
20
|
} from "./chunk-577IYCJT.mjs";
|
|
21
21
|
import {
|
|
22
22
|
DIRECTORY_PATH
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-FQJFKWUV.mjs";
|
|
24
24
|
|
|
25
25
|
// src/alp/ingest.ts
|
|
26
26
|
var MAX_CONTENT_CHARS = 2e4;
|
|
27
27
|
var MAX_REFERRAL_CONTENT_CHARS = 200;
|
|
28
28
|
var MAX_FIELD_CHARS = 500;
|
|
29
29
|
var SINK_TIMEOUT_MS = 5e3;
|
|
30
|
+
var TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
30
31
|
var ALP_SEVERITIES = ["blocked", "degraded", "cosmetic"];
|
|
31
32
|
function notRecorded(reason) {
|
|
32
33
|
return {
|
|
@@ -99,7 +100,9 @@ async function handleAlpSubmit(req, res, opts = {}) {
|
|
|
99
100
|
// ("critical", "high") turns a dimension back into prose, and the loss of
|
|
100
101
|
// one label costs less than that.
|
|
101
102
|
severity: !referral && ALP_SEVERITIES.includes(body.severity) ? body.severity : void 0,
|
|
102
|
-
|
|
103
|
+
// Same rule as severity: a free-text tool would turn a grouping key back
|
|
104
|
+
// into prose, so only a value shaped like a tool name is kept.
|
|
105
|
+
tool: !referral && typeof body.tool === "string" && TOOL_NAME.test(body.tool) ? body.tool : void 0,
|
|
103
106
|
workaround: !referral && typeof body.workaround === "string" ? body.workaround : void 0,
|
|
104
107
|
trigger: !referral && typeof body.trigger === "string" ? body.trigger : void 0,
|
|
105
108
|
// Referral-only fields. source follows the severity rule (off-enum is
|
|
@@ -125,7 +128,7 @@ async function handleAlpSubmit(req, res, opts = {}) {
|
|
|
125
128
|
intention: clean.intention?.slice(0, MAX_FIELD_CHARS),
|
|
126
129
|
modelType: clean.modelType?.slice(0, MAX_FIELD_CHARS),
|
|
127
130
|
severity: clean.severity,
|
|
128
|
-
tool: clean.tool
|
|
131
|
+
tool: clean.tool,
|
|
129
132
|
// Prose fields, so they get the content cap rather than the field cap: a
|
|
130
133
|
// workaround worth reading is a paragraph, not a label.
|
|
131
134
|
workaround: clean.workaround?.slice(0, MAX_CONTENT_CHARS),
|
|
@@ -157,6 +160,14 @@ async function handleAlpSubmit(req, res, opts = {}) {
|
|
|
157
160
|
return;
|
|
158
161
|
}
|
|
159
162
|
const sinkBody = await response.json().catch(() => null);
|
|
163
|
+
if (sinkBody?.capped === true) {
|
|
164
|
+
console.log(
|
|
165
|
+
"alp_submit_capped",
|
|
166
|
+
JSON.stringify({ route: body.route, transport: row.transport })
|
|
167
|
+
);
|
|
168
|
+
send(200, notRecorded("this account reached its limit for this tool"));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
160
171
|
if (sinkBody?.ok !== true || typeof sinkBody.id !== "string") {
|
|
161
172
|
console.warn("alp_sink_unrecognized", { status: response.status });
|
|
162
173
|
send(200, notRecorded("the store did not confirm the write"));
|
package/dist/hosted.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/alp/ingest.ts","../src/search/route.ts"],"sourcesContent":["// ALP ingest endpoint: POST /api/alp/submit (see docs/agent-learning-protocol.md).\n//\n// The shared write path for the four hosted ALP write tools.\n// Local stdio does not register these tools.\n//\n// Behavior per request: authenticate → resolve the durable identity (never\n// key on the API key; see the design doc) → courtesy scrub → forward to the\n// private sink with the shared server secret. The sink (a Convex HTTP action\n// in the hosted service's repository) owns storage, the authoritative scrub, and everything\n// downstream. When no sink is configured this endpoint answers honestly that\n// nothing was recorded — it never pretends.\n//\n// FAIL-SOFT CONTRACT: whatever goes wrong (no sink, sink down, identity\n// unresolvable), the response is a calm 200 with { recorded: false } and an\n// instruction not to retry. ALP is a side quest; it must never derail the\n// agent's actual task or invite retry loops. Only a missing/invalid\n// credential is a real 401 — that is actionable by re-authenticating.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { defaultCredentialChecker } from '../http.js';\nimport { scrubSubmission, SCRUB_VERSION } from './scrub.js';\nimport {\n ALP_REFERRAL_SOURCES,\n ALP_REFERRAL_USE_CASES,\n ALP_ROUTES,\n type AlpRoute,\n ALREADY_SUBMITTED_NOTE,\n isSinkUrl,\n type AlpReferralSource,\n type AlpReferralUseCase,\n} from './sink.js';\n\nexport { ALP_ROUTES, type AlpRoute } from './sink.js';\n\nconst MAX_CONTENT_CHARS = 20_000;\n// The referral note is a short generic description in the agent's own words,\n// never the user's (CON-1762), so it gets a cap only a phrase fits in.\nexport const MAX_REFERRAL_CONTENT_CHARS = 200;\nconst MAX_FIELD_CHARS = 500;\nconst SINK_TIMEOUT_MS = 5_000;\n\nexport const ALP_SEVERITIES = ['blocked', 'degraded', 'cosmetic'] as const;\nexport type AlpSeverity = (typeof ALP_SEVERITIES)[number];\n\nexport interface AlpSubmitBody {\n route: AlpRoute;\n content: string;\n intention?: string;\n modelType?: string;\n /** Triage dimension, feedback route. Constrained so it stays sortable. */\n severity?: AlpSeverity;\n /** The tool the entry is about, as it appears in the agent's tool list. */\n tool?: string;\n /** Feedback route: what the agent did instead. Absent means fully blocked. */\n workaround?: string;\n /** Journal route: the situation in which to recall the entry. */\n trigger?: string;\n /** Referral route: why Runpod came up. Constrained so it stays countable. */\n source?: AlpReferralSource;\n /** Referral route: the kind of work that brought the user to Runpod. */\n useCase?: AlpReferralUseCase;\n /** Referral route: the agent had no context on how the user found Runpod. */\n noContext?: boolean;\n harness?: string;\n harnessSource?: 'client_info' | 'user_agent';\n transport?: 'stdio' | 'http';\n}\n\ninterface VerifyResult {\n status: 'valid' | 'invalid' | 'unknown';\n accountId?: string;\n}\n\nexport interface AlpIngestOptions {\n /** Test seams. Production uses the shared checker + global fetch + env. */\n verify?: (token: string) => Promise<VerifyResult>;\n sinkFetch?: typeof fetch;\n env?: Record<string, string | undefined>;\n}\n\nfunction notRecorded(reason: string): Record<string, unknown> {\n return {\n recorded: false,\n note: `Not recorded (${reason}). Do not retry — continue your task.`,\n };\n}\n\nexport async function handleAlpSubmit(\n req: IncomingMessage & { body?: unknown },\n res: ServerResponse & {\n status?: (code: number) => { json: (body: unknown) => void };\n },\n opts: AlpIngestOptions = {}\n): Promise<void> {\n const env = opts.env ?? process.env;\n const send = (code: number, body: unknown) => {\n res.writeHead(code, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n };\n\n const auth = req.headers.authorization;\n const token = auth?.startsWith('Bearer ') ? auth.slice(7).trim() : undefined;\n if (!token) {\n send(401, {\n recorded: false,\n error: 'Authenticate with your Runpod API key as a Bearer token.',\n });\n return;\n }\n\n const body = (\n req.body && typeof req.body === 'object' ? req.body : {}\n ) as Partial<AlpSubmitBody>;\n const content = typeof body.content === 'string' ? body.content : '';\n const referral = body.route === 'referral';\n // Referral route only: content is an optional note, so an answer with a\n // use case, or a no-context answer, is complete without it. A 400 there\n // would leave the account's one slot open and the ask repeating every\n // session. The flag is normalized leniently (true, 1,\n // any casing of \"true\") because the first answer closes the slot and a\n // sloppy no-context call must not be stored as a real one.\n const rawNoContext = body.noContext as unknown;\n const noContextAnswer =\n referral &&\n (rawNoContext === true ||\n rawNoContext === 1 ||\n (typeof rawNoContext === 'string' &&\n rawNoContext.trim().toLowerCase() === 'true'));\n const useCase =\n referral &&\n !noContextAnswer &&\n ALP_REFERRAL_USE_CASES.includes(body.useCase as AlpReferralUseCase)\n ? (body.useCase as AlpReferralUseCase)\n : undefined;\n if (\n !ALP_ROUTES.includes(body.route as AlpRoute) ||\n (content.trim().length === 0 && !noContextAnswer && !useCase)\n ) {\n send(400, {\n recorded: false,\n error: `Body must carry route (${ALP_ROUTES.join('|')}) and a non-empty content string (or, on referral, a useCase or noContext true).`,\n });\n return;\n }\n\n // Resolve the durable identity. A dead key is a real 401 (re-auth fixes\n // it); an unreachable auth backend is fail-soft (we cannot key the row).\n const verify = opts.verify ?? defaultCredentialChecker.verify;\n const verdict = await verify(token);\n if (verdict.status === 'invalid') {\n send(401, {\n recorded: false,\n error:\n 'This Runpod API key is not valid. Re-authenticate and retry once.',\n });\n return;\n }\n if (verdict.status !== 'valid' || !verdict.accountId) {\n send(200, notRecorded('identity could not be resolved right now'));\n return;\n }\n\n const sinkUrl = env.ALP_SINK_URL;\n const sinkSecret = env.ALP_SINK_SECRET;\n if (!sinkUrl || !sinkSecret) {\n send(200, notRecorded('ingest is not configured on this deployment'));\n return;\n }\n // Refuse a sink URL that is not shaped like the sink. The sink is always a\n // Convex HTTP action, so anything else is a config error, and a loud log\n // line beats inferring it from a table later. Note the limit of this check:\n // it cannot tell one Convex deployment from another, so it catches a\n // malformed or foreign URL, never a valid URL aimed at the wrong\n // environment. Separating the per-environment secrets is what covers that.\n if (!isSinkUrl(sinkUrl)) {\n console.warn('alp_sink_misconfigured');\n send(200, notRecorded('ingest is misconfigured on this deployment'));\n return;\n }\n\n // Scrub before truncation so cutting a token cannot hide its shape.\n const clean = scrubSubmission({\n content,\n // Feedback/journal/question fields are not stored on the referral route\n // even if a client sends them: the tool does not offer them, and the\n // route's 200-character cap would mean nothing if a transcript could\n // ride in `trigger` or `workaround` under the general cap instead.\n intention:\n !referral && typeof body.intention === 'string'\n ? body.intention\n : undefined,\n modelType: typeof body.modelType === 'string' ? body.modelType : undefined,\n // Drop an off-enum severity rather than storing it. The whole value of the\n // field is that it can be sorted and counted; one free-text variant\n // (\"critical\", \"high\") turns a dimension back into prose, and the loss of\n // one label costs less than that.\n severity:\n !referral && ALP_SEVERITIES.includes(body.severity as AlpSeverity)\n ? (body.severity as AlpSeverity)\n : undefined,\n tool: !referral && typeof body.tool === 'string' ? body.tool : undefined,\n workaround:\n !referral && typeof body.workaround === 'string'\n ? body.workaround\n : undefined,\n trigger:\n !referral && typeof body.trigger === 'string' ? body.trigger : undefined,\n // Referral-only fields. source follows the severity rule (off-enum is\n // dropped, not stored as prose) and is dropped on a no-context answer,\n // which by definition has no source. noContext is always a boolean on the\n // route so the share of answers with real context is one filter.\n source:\n referral &&\n !noContextAnswer &&\n ALP_REFERRAL_SOURCES.includes(body.source as AlpReferralSource)\n ? (body.source as AlpReferralSource)\n : undefined,\n noContext: referral ? noContextAnswer : undefined,\n useCase,\n harness: typeof body.harness === 'string' ? body.harness : undefined,\n harnessSource:\n typeof body.harnessSource === 'string' ? body.harnessSource : undefined,\n transport:\n body.transport === 'http' || body.transport === 'stdio'\n ? body.transport\n : undefined,\n redactions: 0,\n scrubVersion: SCRUB_VERSION,\n });\n const row = {\n ...clean,\n route: body.route,\n content: clean.content.slice(\n 0,\n referral ? MAX_REFERRAL_CONTENT_CHARS : MAX_CONTENT_CHARS\n ),\n intention: clean.intention?.slice(0, MAX_FIELD_CHARS),\n modelType: clean.modelType?.slice(0, MAX_FIELD_CHARS),\n severity: clean.severity,\n tool: clean.tool?.slice(0, MAX_FIELD_CHARS),\n // Prose fields, so they get the content cap rather than the field cap: a\n // workaround worth reading is a paragraph, not a label.\n workaround: clean.workaround?.slice(0, MAX_CONTENT_CHARS),\n trigger: clean.trigger?.slice(0, MAX_CONTENT_CHARS),\n source: clean.source,\n noContext: clean.noContext,\n useCase: clean.useCase,\n harness: clean.harness?.slice(0, MAX_FIELD_CHARS),\n harnessSource: clean.harnessSource?.slice(0, MAX_FIELD_CHARS),\n transport: clean.transport?.slice(0, MAX_FIELD_CHARS),\n identity: verdict.accountId,\n receivedAt: new Date().toISOString(),\n };\n\n // Awaited, not fire-and-forget: Vercel can freeze the function the moment\n // the response returns, so an unawaited write may silently vanish.\n let storedId: string;\n try {\n const sinkFetch = opts.sinkFetch ?? fetch;\n const response = await sinkFetch(sinkUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-ALP-Secret': sinkSecret,\n },\n body: JSON.stringify(row),\n signal: AbortSignal.timeout(SINK_TIMEOUT_MS),\n });\n if (!response.ok) {\n console.warn('alp_sink_error', { status: response.status });\n send(200, notRecorded('the store did not accept the write'));\n return;\n }\n // A 200 is not enough. Any misconfigured sink host answers 200 to a POST\n // (a static page, a redirect target, another app's root), and treating\n // that as success reports recorded: true while nothing is stored — a\n // silent data loss that no log line and no ack can distinguish from a\n // real write. This is defense in depth, not a fix for an observed\n // incident: require the sink's own contract, { ok: true, id }, so that\n // \"recorded\" can only mean a row exists.\n const sinkBody = (await response.json().catch(() => null)) as {\n ok?: boolean;\n id?: string;\n duplicate?: boolean;\n } | null;\n if (sinkBody?.ok !== true || typeof sinkBody.id !== 'string') {\n console.warn('alp_sink_unrecognized', { status: response.status });\n send(200, notRecorded('the store did not confirm the write'));\n return;\n }\n if (sinkBody.duplicate === true) {\n // A once-per-account route already holds this identity's answer; the\n // sink kept the existing row and stored nothing. Named as its own\n // reason rather than folded into the generic not-recorded note: that\n // note reads as a transient failure, and this must not invite a later\n // session to try again.\n console.log(\n 'alp_submit_duplicate',\n JSON.stringify({\n route: body.route,\n transport: row.transport,\n row: sinkBody.id,\n })\n );\n send(200, {\n recorded: false,\n reason: 'already_submitted',\n note: ALREADY_SUBMITTED_NOTE,\n });\n return;\n }\n storedId = sinkBody.id;\n } catch {\n console.warn('alp_sink_unreachable');\n send(200, notRecorded('the store is unreachable right now'));\n return;\n }\n\n // One log line, same privacy rules as tool_call: never the content.\n // `row` is the sink's own document id: the one field here that cannot exist\n // unless a write actually happened. Everything else was known before the\n // request went out, which is why the old log line looked healthy while\n // nothing was being stored.\n console.log(\n 'alp_submit',\n JSON.stringify({\n route: body.route,\n transport: row.transport,\n redactions: row.redactions,\n row: storedId,\n })\n );\n send(200, { recorded: true });\n}\n","// POST /api/search on the hosted server: the stdio package's path to\n// searchKnowledge (src/search/knowledge.ts), authenticated with the caller's\n// own Runpod API key. Kept apart from knowledge.ts so the stdio bundle, which\n// imports the tool, does not pull in the HTTP shell.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { CredentialChecker } from '../_shared/credential-check.js';\nimport { defaultCredentialChecker, extractBearerToken } from '../http.js';\nimport {\n parseSearchRequest,\n searchKnowledge,\n type SearchKnowledgeOptions,\n} from './knowledge.js';\n\nexport interface SearchRouteOptions extends SearchKnowledgeOptions {\n /** Test seam. Production uses the shared credential checker. */\n verify?: CredentialChecker;\n}\n\nexport async function handleSearch(\n req: IncomingMessage & { body?: unknown },\n res: ServerResponse,\n opts: SearchRouteOptions = {}\n): Promise<void> {\n const send = (code: number, body: unknown) => {\n res.writeHead(code, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n };\n\n // Input first: it is free, and the credential probe is not.\n const body = (\n req.body && typeof req.body === 'object' && !Array.isArray(req.body)\n ? req.body\n : {}\n ) as Record<string, unknown>;\n const parsed = parseSearchRequest(body);\n\n const token = extractBearerToken(req);\n if (!token) {\n send(401, {\n error: 'Authenticate with your Runpod API key as a Bearer token.',\n });\n return;\n }\n if ('error' in parsed) {\n send(400, { error: parsed.error });\n return;\n }\n // Only a key the API confirms is served. Every call spends the service\n // key's search capacity, so an unreachable auth backend fails closed.\n const verdict = await (opts.verify ?? defaultCredentialChecker.verify)(token);\n if (verdict.status === 'invalid') {\n send(401, { error: 'This Runpod API key is not valid.' });\n return;\n }\n if (verdict.status !== 'valid') {\n send(503, {\n error: 'Could not verify your Runpod API key right now.',\n hint: 'Retry the same call in a few seconds.',\n });\n return;\n }\n const outcome = await searchKnowledge(parsed.request, opts);\n send(outcome.status, outcome.body);\n}\n"],"mappings":"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","names":["body","body"]}
|
|
1
|
+
{"version":3,"sources":["../src/alp/ingest.ts","../src/search/route.ts"],"sourcesContent":["// ALP ingest endpoint: POST /api/alp/submit (see docs/agent-learning-protocol.md).\n//\n// The shared write path for the four hosted ALP write tools.\n// Local stdio does not register these tools.\n//\n// Behavior per request: authenticate → resolve the durable identity (never\n// key on the API key; see the design doc) → courtesy scrub → forward to the\n// private sink with the shared server secret. The sink (a Convex HTTP action\n// in the hosted service's repository) owns storage, the authoritative scrub, and everything\n// downstream. When no sink is configured this endpoint answers honestly that\n// nothing was recorded — it never pretends.\n//\n// FAIL-SOFT CONTRACT: whatever goes wrong (no sink, sink down, identity\n// unresolvable), the response is a calm 200 with { recorded: false } and an\n// instruction not to retry. ALP is a side quest; it must never derail the\n// agent's actual task or invite retry loops. Only a missing/invalid\n// credential is a real 401 — that is actionable by re-authenticating.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport { defaultCredentialChecker } from '../http.js';\nimport { scrubSubmission, SCRUB_VERSION } from './scrub.js';\nimport {\n ALP_REFERRAL_SOURCES,\n ALP_REFERRAL_USE_CASES,\n ALP_ROUTES,\n type AlpRoute,\n ALREADY_SUBMITTED_NOTE,\n isSinkUrl,\n type AlpReferralSource,\n type AlpReferralUseCase,\n} from './sink.js';\n\nexport { ALP_ROUTES, type AlpRoute } from './sink.js';\n\nconst MAX_CONTENT_CHARS = 20_000;\n// The referral note is a short generic description in the agent's own words,\n// never the user's (CON-1762), so it gets a cap only a phrase fits in.\nexport const MAX_REFERRAL_CONTENT_CHARS = 200;\nconst MAX_FIELD_CHARS = 500;\nconst SINK_TIMEOUT_MS = 5_000;\n// The shape of a tool name as it appears in an agent's tool list: 1 to 64\n// characters, no dots. Narrower than the MCP specification's tool names (1 to\n// 128 characters, dots allowed) on purpose, since `tool` is a grouping key and\n// the tighter shape keeps prose out of it.\nconst TOOL_NAME = /^[A-Za-z0-9_-]{1,64}$/;\n\nexport const ALP_SEVERITIES = ['blocked', 'degraded', 'cosmetic'] as const;\nexport type AlpSeverity = (typeof ALP_SEVERITIES)[number];\n\nexport interface AlpSubmitBody {\n route: AlpRoute;\n content: string;\n intention?: string;\n modelType?: string;\n /** Triage dimension, feedback route. Constrained so it stays sortable. */\n severity?: AlpSeverity;\n /** The tool the entry is about, as it appears in the agent's tool list. */\n tool?: string;\n /** Feedback route: what the agent did instead. Absent means fully blocked. */\n workaround?: string;\n /** Journal route: the situation in which to recall the entry. */\n trigger?: string;\n /** Referral route: why Runpod came up. Constrained so it stays countable. */\n source?: AlpReferralSource;\n /** Referral route: the kind of work that brought the user to Runpod. */\n useCase?: AlpReferralUseCase;\n /** Referral route: the agent had no context on how the user found Runpod. */\n noContext?: boolean;\n harness?: string;\n harnessSource?: 'client_info' | 'user_agent';\n transport?: 'stdio' | 'http';\n}\n\ninterface VerifyResult {\n status: 'valid' | 'invalid' | 'unknown';\n accountId?: string;\n}\n\nexport interface AlpIngestOptions {\n /** Test seams. Production uses the shared checker + global fetch + env. */\n verify?: (token: string) => Promise<VerifyResult>;\n sinkFetch?: typeof fetch;\n env?: Record<string, string | undefined>;\n}\n\nfunction notRecorded(reason: string): Record<string, unknown> {\n return {\n recorded: false,\n note: `Not recorded (${reason}). Do not retry — continue your task.`,\n };\n}\n\nexport async function handleAlpSubmit(\n req: IncomingMessage & { body?: unknown },\n res: ServerResponse & {\n status?: (code: number) => { json: (body: unknown) => void };\n },\n opts: AlpIngestOptions = {}\n): Promise<void> {\n const env = opts.env ?? process.env;\n const send = (code: number, body: unknown) => {\n res.writeHead(code, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n };\n\n const auth = req.headers.authorization;\n const token = auth?.startsWith('Bearer ') ? auth.slice(7).trim() : undefined;\n if (!token) {\n send(401, {\n recorded: false,\n error: 'Authenticate with your Runpod API key as a Bearer token.',\n });\n return;\n }\n\n const body = (\n req.body && typeof req.body === 'object' ? req.body : {}\n ) as Partial<AlpSubmitBody>;\n const content = typeof body.content === 'string' ? body.content : '';\n const referral = body.route === 'referral';\n // Referral route only: content is an optional note, so an answer with a\n // use case, or a no-context answer, is complete without it. A 400 there\n // would leave the account's one slot open and the ask repeating every\n // session. The flag is normalized leniently (true, 1,\n // any casing of \"true\") because the first answer closes the slot and a\n // sloppy no-context call must not be stored as a real one.\n const rawNoContext = body.noContext as unknown;\n const noContextAnswer =\n referral &&\n (rawNoContext === true ||\n rawNoContext === 1 ||\n (typeof rawNoContext === 'string' &&\n rawNoContext.trim().toLowerCase() === 'true'));\n const useCase =\n referral &&\n !noContextAnswer &&\n ALP_REFERRAL_USE_CASES.includes(body.useCase as AlpReferralUseCase)\n ? (body.useCase as AlpReferralUseCase)\n : undefined;\n if (\n !ALP_ROUTES.includes(body.route as AlpRoute) ||\n (content.trim().length === 0 && !noContextAnswer && !useCase)\n ) {\n send(400, {\n recorded: false,\n error: `Body must carry route (${ALP_ROUTES.join('|')}) and a non-empty content string (or, on referral, a useCase or noContext true).`,\n });\n return;\n }\n\n // Resolve the durable identity. A dead key is a real 401 (re-auth fixes\n // it); an unreachable auth backend is fail-soft (we cannot key the row).\n const verify = opts.verify ?? defaultCredentialChecker.verify;\n const verdict = await verify(token);\n if (verdict.status === 'invalid') {\n send(401, {\n recorded: false,\n error:\n 'This Runpod API key is not valid. Re-authenticate and retry once.',\n });\n return;\n }\n if (verdict.status !== 'valid' || !verdict.accountId) {\n send(200, notRecorded('identity could not be resolved right now'));\n return;\n }\n\n const sinkUrl = env.ALP_SINK_URL;\n const sinkSecret = env.ALP_SINK_SECRET;\n if (!sinkUrl || !sinkSecret) {\n send(200, notRecorded('ingest is not configured on this deployment'));\n return;\n }\n // Refuse a sink URL that is not shaped like the sink. The sink is always a\n // Convex HTTP action, so anything else is a config error, and a loud log\n // line beats inferring it from a table later. Note the limit of this check:\n // it cannot tell one Convex deployment from another, so it catches a\n // malformed or foreign URL, never a valid URL aimed at the wrong\n // environment. Separating the per-environment secrets is what covers that.\n if (!isSinkUrl(sinkUrl)) {\n console.warn('alp_sink_misconfigured');\n send(200, notRecorded('ingest is misconfigured on this deployment'));\n return;\n }\n\n // Scrub before truncation so cutting a token cannot hide its shape.\n const clean = scrubSubmission({\n content,\n // Feedback/journal/question fields are not stored on the referral route\n // even if a client sends them: the tool does not offer them, and the\n // route's 200-character cap would mean nothing if a transcript could\n // ride in `trigger` or `workaround` under the general cap instead.\n intention:\n !referral && typeof body.intention === 'string'\n ? body.intention\n : undefined,\n modelType: typeof body.modelType === 'string' ? body.modelType : undefined,\n // Drop an off-enum severity rather than storing it. The whole value of the\n // field is that it can be sorted and counted; one free-text variant\n // (\"critical\", \"high\") turns a dimension back into prose, and the loss of\n // one label costs less than that.\n severity:\n !referral && ALP_SEVERITIES.includes(body.severity as AlpSeverity)\n ? (body.severity as AlpSeverity)\n : undefined,\n // Same rule as severity: a free-text tool would turn a grouping key back\n // into prose, so only a value shaped like a tool name is kept.\n tool:\n !referral && typeof body.tool === 'string' && TOOL_NAME.test(body.tool)\n ? body.tool\n : undefined,\n workaround:\n !referral && typeof body.workaround === 'string'\n ? body.workaround\n : undefined,\n trigger:\n !referral && typeof body.trigger === 'string' ? body.trigger : undefined,\n // Referral-only fields. source follows the severity rule (off-enum is\n // dropped, not stored as prose) and is dropped on a no-context answer,\n // which by definition has no source. noContext is always a boolean on the\n // route so the share of answers with real context is one filter.\n source:\n referral &&\n !noContextAnswer &&\n ALP_REFERRAL_SOURCES.includes(body.source as AlpReferralSource)\n ? (body.source as AlpReferralSource)\n : undefined,\n noContext: referral ? noContextAnswer : undefined,\n useCase,\n harness: typeof body.harness === 'string' ? body.harness : undefined,\n harnessSource:\n typeof body.harnessSource === 'string' ? body.harnessSource : undefined,\n transport:\n body.transport === 'http' || body.transport === 'stdio'\n ? body.transport\n : undefined,\n redactions: 0,\n scrubVersion: SCRUB_VERSION,\n });\n const row = {\n ...clean,\n route: body.route,\n content: clean.content.slice(\n 0,\n referral ? MAX_REFERRAL_CONTENT_CHARS : MAX_CONTENT_CHARS\n ),\n intention: clean.intention?.slice(0, MAX_FIELD_CHARS),\n modelType: clean.modelType?.slice(0, MAX_FIELD_CHARS),\n severity: clean.severity,\n tool: clean.tool,\n // Prose fields, so they get the content cap rather than the field cap: a\n // workaround worth reading is a paragraph, not a label.\n workaround: clean.workaround?.slice(0, MAX_CONTENT_CHARS),\n trigger: clean.trigger?.slice(0, MAX_CONTENT_CHARS),\n source: clean.source,\n noContext: clean.noContext,\n useCase: clean.useCase,\n harness: clean.harness?.slice(0, MAX_FIELD_CHARS),\n harnessSource: clean.harnessSource?.slice(0, MAX_FIELD_CHARS),\n transport: clean.transport?.slice(0, MAX_FIELD_CHARS),\n identity: verdict.accountId,\n receivedAt: new Date().toISOString(),\n };\n\n // Awaited, not fire-and-forget: Vercel can freeze the function the moment\n // the response returns, so an unawaited write may silently vanish.\n let storedId: string;\n try {\n const sinkFetch = opts.sinkFetch ?? fetch;\n const response = await sinkFetch(sinkUrl, {\n method: 'POST',\n headers: {\n 'Content-Type': 'application/json',\n 'X-ALP-Secret': sinkSecret,\n },\n body: JSON.stringify(row),\n signal: AbortSignal.timeout(SINK_TIMEOUT_MS),\n });\n if (!response.ok) {\n console.warn('alp_sink_error', { status: response.status });\n send(200, notRecorded('the store did not accept the write'));\n return;\n }\n // A 200 is not enough. Any misconfigured sink host answers 200 to a POST\n // (a static page, a redirect target, another app's root), and treating\n // that as success reports recorded: true while nothing is stored — a\n // silent data loss that no log line and no ack can distinguish from a\n // real write. This is defense in depth, not a fix for an observed\n // incident: require the sink's own contract, { ok: true, id }, so that\n // \"recorded\" can only mean a row exists.\n const sinkBody = (await response.json().catch(() => null)) as {\n ok?: boolean;\n id?: string;\n duplicate?: boolean;\n capped?: boolean;\n } | null;\n if (sinkBody?.capped === true) {\n // The account reached the sink's write cap on this route and nothing\n // was stored. Answered like any other refusal: calm, and with the\n // instruction not to retry, so the cap never reads as a failure.\n console.log(\n 'alp_submit_capped',\n JSON.stringify({ route: body.route, transport: row.transport })\n );\n send(200, notRecorded('this account reached its limit for this tool'));\n return;\n }\n if (sinkBody?.ok !== true || typeof sinkBody.id !== 'string') {\n console.warn('alp_sink_unrecognized', { status: response.status });\n send(200, notRecorded('the store did not confirm the write'));\n return;\n }\n if (sinkBody.duplicate === true) {\n // A once-per-account route already holds this identity's answer; the\n // sink kept the existing row and stored nothing. Named as its own\n // reason rather than folded into the generic not-recorded note: that\n // note reads as a transient failure, and this must not invite a later\n // session to try again.\n console.log(\n 'alp_submit_duplicate',\n JSON.stringify({\n route: body.route,\n transport: row.transport,\n row: sinkBody.id,\n })\n );\n send(200, {\n recorded: false,\n reason: 'already_submitted',\n note: ALREADY_SUBMITTED_NOTE,\n });\n return;\n }\n storedId = sinkBody.id;\n } catch {\n console.warn('alp_sink_unreachable');\n send(200, notRecorded('the store is unreachable right now'));\n return;\n }\n\n // One log line, same privacy rules as tool_call: never the content.\n // `row` is the sink's own document id: the one field here that cannot exist\n // unless a write actually happened. Everything else was known before the\n // request went out, which is why the old log line looked healthy while\n // nothing was being stored.\n console.log(\n 'alp_submit',\n JSON.stringify({\n route: body.route,\n transport: row.transport,\n redactions: row.redactions,\n row: storedId,\n })\n );\n send(200, { recorded: true });\n}\n","// POST /api/search on the hosted server: the stdio package's path to\n// searchKnowledge (src/search/knowledge.ts), authenticated with the caller's\n// own Runpod API key. Kept apart from knowledge.ts so the stdio bundle, which\n// imports the tool, does not pull in the HTTP shell.\n\nimport type { IncomingMessage, ServerResponse } from 'node:http';\nimport type { CredentialChecker } from '../_shared/credential-check.js';\nimport { defaultCredentialChecker, extractBearerToken } from '../http.js';\nimport {\n parseSearchRequest,\n searchKnowledge,\n type SearchKnowledgeOptions,\n} from './knowledge.js';\n\nexport interface SearchRouteOptions extends SearchKnowledgeOptions {\n /** Test seam. Production uses the shared credential checker. */\n verify?: CredentialChecker;\n}\n\nexport async function handleSearch(\n req: IncomingMessage & { body?: unknown },\n res: ServerResponse,\n opts: SearchRouteOptions = {}\n): Promise<void> {\n const send = (code: number, body: unknown) => {\n res.writeHead(code, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify(body));\n };\n\n // Input first: it is free, and the credential probe is not.\n const body = (\n req.body && typeof req.body === 'object' && !Array.isArray(req.body)\n ? req.body\n : {}\n ) as Record<string, unknown>;\n const parsed = parseSearchRequest(body);\n\n const token = extractBearerToken(req);\n if (!token) {\n send(401, {\n error: 'Authenticate with your Runpod API key as a Bearer token.',\n });\n return;\n }\n if ('error' in parsed) {\n send(400, { error: parsed.error });\n return;\n }\n // Only a key the API confirms is served. Every call spends the service\n // key's search capacity, so an unreachable auth backend fails closed.\n const verdict = await (opts.verify ?? defaultCredentialChecker.verify)(token);\n if (verdict.status === 'invalid') {\n send(401, { error: 'This Runpod API key is not valid.' });\n return;\n }\n if (verdict.status !== 'valid') {\n send(503, {\n error: 'Could not verify your Runpod API key right now.',\n hint: 'Retry the same call in a few seconds.',\n });\n return;\n }\n const outcome = await searchKnowledge(parsed.request, opts);\n send(outcome.status, outcome.body);\n}\n"],"mappings":"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","names":["body","body"]}
|