@sema-agent/core 7.23.4 → 7.24.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 +133 -0
- package/dist/agents/list-agents-tool.d.ts +8 -1
- package/dist/agents/list-agents-tool.js +26 -3
- package/dist/agents/observer.js +5 -8
- package/dist/agents/subagent.js +2 -2
- package/dist/agents/team.js +9 -0
- package/dist/brain/anthropic.js +3 -2
- package/dist/brain/circuit-breaker.js +3 -3
- package/dist/brain/degrading.js +11 -14
- package/dist/brain/errors.d.ts +35 -18
- package/dist/brain/errors.js +49 -31
- package/dist/brain/failover.js +2 -3
- package/dist/brain/open-responses.js +6 -5
- package/dist/brain/openai.js +6 -5
- package/dist/brain/route-adjudicator.d.ts +50 -15
- package/dist/brain/route-adjudicator.js +112 -20
- package/dist/brain/route-conformance.js +9 -0
- package/dist/brain/stream-engine.js +18 -10
- package/dist/core/arg-summary.js +17 -6
- package/dist/core/auto-compaction.d.ts +42 -1
- package/dist/core/auto-compaction.js +21 -1
- package/dist/core/auto-mode.js +2 -8
- package/dist/core/checkpoint-store.d.ts +4 -6
- package/dist/core/checkpoint-store.js +2 -2
- package/dist/core/exec-gate.js +3 -5
- package/dist/core/file-history-store.js +11 -10
- package/dist/core/hooks.d.ts +7 -0
- package/dist/core/mcp.js +7 -5
- package/dist/core/memory-engine/distiller.d.ts +7 -4
- package/dist/core/memory-engine/distiller.js +11 -3
- package/dist/core/memory.js +1 -0
- package/dist/core/permission-rule-model.js +8 -5
- package/dist/core/runner/assemble-result.js +12 -19
- package/dist/core/runner/contracts.d.ts +1 -0
- package/dist/core/runner/prepare-delegation-surface.d.ts +3 -0
- package/dist/core/runner/prepare-delegation-surface.js +1 -0
- package/dist/core/runner/prepare-file-history.js +19 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -1
- package/dist/core/runner/prepare-hands-readface.js +4 -0
- package/dist/core/runner/prepare-task.js +2 -2
- package/dist/core/runner/run-leg.js +2 -13
- package/dist/core/runner/run-recovery-lanes.js +4 -20
- package/dist/core/runner/run-settle-and-teardown.js +2 -16
- package/dist/core/runner/run-turn-boundary.js +4 -20
- package/dist/core/runner/runtask.js +3 -5
- package/dist/core/runner/stream-settle-backstop.js +5 -5
- package/dist/core/runner/tool-end-body.js +5 -4
- package/dist/core/runner/tool-output-projection.js +3 -2
- package/dist/core/runner/turn-attachments.d.ts +0 -20
- package/dist/core/runner/turn-attachments.js +41 -36
- package/dist/core/session-store.js +2 -4
- package/dist/core/session.d.ts +4 -1
- package/dist/core/session.js +2 -1
- package/dist/core/task-event.d.ts +19 -9
- package/dist/core/task-registry-shared.js +1 -0
- package/dist/core/thrown-value.d.ts +52 -11
- package/dist/core/thrown-value.js +35 -1
- package/dist/core/tool-catalog-entries.js +1 -1
- package/dist/core/tool-errors.d.ts +6 -0
- package/dist/core/tool-errors.js +6 -14
- package/dist/core/tool-model-gate.js +3 -15
- package/dist/core/tools.js +15 -16
- package/dist/core/untrusted-text.d.ts +6 -0
- package/dist/core/untrusted-text.js +1 -0
- package/dist/core/workflow-run-store-contract.js +13 -0
- package/dist/core/workflow-run-store.d.ts +15 -0
- package/dist/core/workflow-run-store.js +1 -0
- package/dist/engine/llm/types.d.ts +4 -4
- package/dist/engine/loop/agent-loop.js +10 -24
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow.js +44 -23
- package/dist/stores/file/file-history-store.js +12 -11
- package/dist/tools/fs/bash-program-position.d.ts +23 -0
- package/dist/tools/fs/bash-program-position.js +11 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +30 -7
- package/dist/tools/fs/bash-readonly-classifier.js +31 -10
- package/dist/tools/fs/fs-bash.d.ts +30 -1
- package/dist/tools/fs/fs-bash.js +123 -54
- package/dist/tools/fs/read-deny.d.ts +14 -0
- package/dist/tools/fs/read-deny.js +5 -0
- package/dist/tools/fs/safety.d.ts +1 -1
- package/dist/tools/fs/safety.js +2 -1
- package/dist/tools/monitor.d.ts +5 -0
- package/dist/tools/monitor.js +7 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +7 -3
- package/wire-changes/core@7.23.5.json +133 -0
- package/wire-changes/core@7.24.0.json +249 -0
package/dist/brain/errors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { thrownText } from "../core/thrown-value.js";
|
|
1
|
+
import { isErrorLike, readMember, thrownText } from "../core/thrown-value.js";
|
|
2
2
|
export const ZERO_USAGE = {
|
|
3
3
|
input: 0,
|
|
4
4
|
output: 0,
|
|
@@ -82,16 +82,12 @@ const STALE_CONNECTION_CODES = new Set([
|
|
|
82
82
|
"StreamSuspended",
|
|
83
83
|
]);
|
|
84
84
|
function hasStaleConnectionCode(e) {
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
cur = cur instanceof Error ? cur.cause : undefined;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
catch {
|
|
85
|
+
let cur = e;
|
|
86
|
+
for (let depth = 0; depth < 5 && cur !== undefined && cur !== null; depth++) {
|
|
87
|
+
const code = readMember(cur, "code");
|
|
88
|
+
if (typeof code === "string" && STALE_CONNECTION_CODES.has(code))
|
|
89
|
+
return true;
|
|
90
|
+
cur = isErrorLike(cur) ? readMember(cur, "cause") : undefined;
|
|
95
91
|
}
|
|
96
92
|
return false;
|
|
97
93
|
}
|
|
@@ -104,25 +100,26 @@ function walkConnectFailure(node, budget) {
|
|
|
104
100
|
if (node === undefined || node === null)
|
|
105
101
|
return "silent";
|
|
106
102
|
let verdict = "silent";
|
|
107
|
-
const code = node
|
|
103
|
+
const code = readMember(node, "code");
|
|
108
104
|
if (typeof code === "string") {
|
|
109
105
|
if (!DETERMINISTIC_CONNECT_CODES.has(code))
|
|
110
106
|
return "indeterminate";
|
|
111
107
|
verdict = "deterministic";
|
|
112
108
|
}
|
|
113
|
-
if (!(node
|
|
109
|
+
if (!isErrorLike(node))
|
|
114
110
|
return verdict;
|
|
115
|
-
const members = node
|
|
116
|
-
|
|
117
|
-
|
|
111
|
+
const members = readMember(node, "errors");
|
|
112
|
+
const legCount = readMember(members, "length");
|
|
113
|
+
if (members !== undefined) {
|
|
114
|
+
if (typeof legCount !== "number" || !Number.isInteger(legCount) || legCount <= 0)
|
|
118
115
|
return "indeterminate";
|
|
119
|
-
for (
|
|
120
|
-
if (walkConnectFailure(
|
|
116
|
+
for (let i = 0; i < legCount; i++) {
|
|
117
|
+
if (walkConnectFailure(readMember(members, String(i)), budget) !== "deterministic")
|
|
121
118
|
return "indeterminate";
|
|
122
119
|
}
|
|
123
120
|
verdict = "deterministic";
|
|
124
121
|
}
|
|
125
|
-
const cause = node
|
|
122
|
+
const cause = readMember(node, "cause");
|
|
126
123
|
if (cause !== undefined && cause !== null) {
|
|
127
124
|
const below = walkConnectFailure(cause, budget);
|
|
128
125
|
if (below === "indeterminate")
|
|
@@ -135,21 +132,42 @@ function walkConnectFailure(node, budget) {
|
|
|
135
132
|
export function classifyConnectFailure(e) {
|
|
136
133
|
return walkConnectFailure(e, { left: CONNECT_GRAPH_NODE_BUDGET }) === "deterministic" ? "deterministic" : "indeterminate";
|
|
137
134
|
}
|
|
135
|
+
const FETCH_TRANSPORT_SHELLS = new Set(["fetch failed", "terminated"]);
|
|
136
|
+
const REQUEST_ARGUMENT_CODES = new Set(["ERR_INVALID_URL"]);
|
|
137
|
+
export function classifyFetchThrow(e) {
|
|
138
|
+
if (readMember(e, "name") !== "TypeError")
|
|
139
|
+
return "transport";
|
|
140
|
+
const message = readMember(e, "message");
|
|
141
|
+
if (typeof message === "string" && FETCH_TRANSPORT_SHELLS.has(message))
|
|
142
|
+
return "transport";
|
|
143
|
+
const cause = readMember(e, "cause");
|
|
144
|
+
if (cause === undefined || cause === null)
|
|
145
|
+
return "request_unbuildable";
|
|
146
|
+
const code = readMember(cause, "code");
|
|
147
|
+
return typeof code === "string" && REQUEST_ARGUMENT_CODES.has(code) ? "request_unbuildable" : "transport";
|
|
148
|
+
}
|
|
149
|
+
export function abortLikeName(err) {
|
|
150
|
+
if (!isErrorLike(err))
|
|
151
|
+
return undefined;
|
|
152
|
+
const name = readMember(err, "name");
|
|
153
|
+
return name === "AbortError" || name === "TimeoutError" ? name : undefined;
|
|
154
|
+
}
|
|
155
|
+
export function brainErrorCodeOf(e) {
|
|
156
|
+
const code = readMember(e, "code");
|
|
157
|
+
return typeof code === "string" && BRAIN_ERROR_CODES.includes(code) ? code : undefined;
|
|
158
|
+
}
|
|
138
159
|
export function describeNetworkError(e) {
|
|
139
160
|
const top = thrownText(e);
|
|
140
161
|
const parts = [];
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
catch {
|
|
162
|
+
let cur = isErrorLike(e) ? readMember(e, "cause") : undefined;
|
|
163
|
+
for (let depth = 0; depth < 4 && cur !== undefined && cur !== null; depth++) {
|
|
164
|
+
const rawCode = readMember(cur, "code");
|
|
165
|
+
const code = typeof rawCode === "string" ? rawCode : undefined;
|
|
166
|
+
const msg = thrownText(cur);
|
|
167
|
+
const piece = code !== undefined && !msg.includes(code) ? `${code}: ${msg}` : msg;
|
|
168
|
+
if (piece.length > 0 && piece !== top && !parts.includes(piece))
|
|
169
|
+
parts.push(piece);
|
|
170
|
+
cur = isErrorLike(cur) ? readMember(cur, "cause") : undefined;
|
|
153
171
|
}
|
|
154
172
|
const body = parts.length > 0 ? `${top} (${parts.join(" ← ")})` : top;
|
|
155
173
|
return hasStaleConnectionCode(e) ? `${body} — possibly a stale connection; a fresh one may succeed` : body;
|
package/dist/brain/failover.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
|
-
import { extractErrorCode } from "./errors.js";
|
|
2
|
+
import { abortLikeName, extractErrorCode } from "./errors.js";
|
|
3
3
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
4
4
|
import { passthroughStream } from "./degrading.js";
|
|
5
5
|
import { thrownText } from "../core/thrown-value.js";
|
|
@@ -65,8 +65,7 @@ export function createFailoverBrain(brains) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
catch (e) {
|
|
68
|
-
const aborted = options?.signal?.aborted === true ||
|
|
69
|
-
(e instanceof Error && (e.name === "AbortError" || e.name === "TimeoutError"));
|
|
68
|
+
const aborted = options?.signal?.aborted === true || abortLikeName(e) !== undefined;
|
|
70
69
|
const errorMsg = {
|
|
71
70
|
role: "assistant",
|
|
72
71
|
content: [{ type: "text", text: "" }],
|
|
@@ -6,7 +6,7 @@ import { mintFallbackToolCallId } from "./tool-call-id.js";
|
|
|
6
6
|
import { emitBrainTelemetry } from "./status-sink.js";
|
|
7
7
|
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
8
|
import { OUTPUT_CAP_KEYS, RESPONSES_RESERVED, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
|
|
9
|
-
import { adjudicateModelRoute, applyRouteCredentialHeaders, createBrainRouteJudge, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
9
|
+
import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
10
10
|
import { declaredOffSpelling, mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
11
11
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
12
12
|
const DEGENERATE_POLL_CHARS = 64;
|
|
@@ -217,6 +217,7 @@ function mapTerminalStopReason(incompleteReason, hadToolCalls) {
|
|
|
217
217
|
return "stop";
|
|
218
218
|
}
|
|
219
219
|
export function createOpenResponsesBrain(config = {}) {
|
|
220
|
+
assertServableRequestRoot(config.baseUrl, "config.baseUrl");
|
|
220
221
|
const doFetch = config.fetchImpl ?? fetch;
|
|
221
222
|
const stream = (model, context, options) => {
|
|
222
223
|
let sentMaxTokens;
|
|
@@ -232,6 +233,10 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
232
233
|
httpLabel: "responses",
|
|
233
234
|
stallTimeouts: options?.stallTimeouts,
|
|
234
235
|
buildRequest: (overrides) => {
|
|
236
|
+
const root = resolveRequestRoot(model.baseUrl, config.baseUrl);
|
|
237
|
+
if (!root) {
|
|
238
|
+
throw new Error("createOpenResponsesBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
239
|
+
}
|
|
235
240
|
const routeCredential = resolveRouteCredential({
|
|
236
241
|
optionsApiKey: options?.apiKey,
|
|
237
242
|
optionsHeaders: options?.headers,
|
|
@@ -243,10 +248,6 @@ export function createOpenResponsesBrain(config = {}) {
|
|
|
243
248
|
if (!routeVerdict.ok)
|
|
244
249
|
throw new BrainError("invalid_request", routeRefusalText(routeVerdict));
|
|
245
250
|
const apiKey = routeCredential.apiKey;
|
|
246
|
-
const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
|
|
247
|
-
if (!root) {
|
|
248
|
-
throw new Error("createOpenResponsesBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
249
|
-
}
|
|
250
251
|
if (options?.stop !== undefined && options.stop.length > 0) {
|
|
251
252
|
throw new Error("createOpenResponsesBrain: options.stop is not expressible in the Open Responses request (the wire form has no stop-sequence field); " +
|
|
252
253
|
"remove it, or serve this model through the chat-completions lane, which does support it");
|
package/dist/brain/openai.js
CHANGED
|
@@ -7,7 +7,7 @@ import { emitBrainTelemetry } from "./status-sink.js";
|
|
|
7
7
|
import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
|
|
8
8
|
import { OPENAI_RESERVED, OUTPUT_CAP_KEYS, applyExtraBody, effectiveOutputCap, lockHeader, mergeHeaders } from "./request-params.js";
|
|
9
9
|
import { BrainError } from "./errors.js";
|
|
10
|
-
import { adjudicateModelRoute, applyRouteCredentialHeaders, createBrainRouteJudge, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
10
|
+
import { adjudicateModelRoute, applyRouteCredentialHeaders, assertServableRequestRoot, createBrainRouteJudge, resolveRequestRoot, resolveRouteCredential, routeRefusalText } from "./route-adjudicator.js";
|
|
11
11
|
import { declaredOffSpelling, mintEffortWireValue, reasoningRequestCarried } from "./reasoning.js";
|
|
12
12
|
import { runStreamingBrain } from "./stream-engine.js";
|
|
13
13
|
function closeToolCallAccum(acc) {
|
|
@@ -281,6 +281,7 @@ export function inferMaxTokensField(modelId) {
|
|
|
281
281
|
return "max_tokens";
|
|
282
282
|
}
|
|
283
283
|
export function createOpenAIBrain(config = {}) {
|
|
284
|
+
assertServableRequestRoot(config.baseUrl, "config.baseUrl");
|
|
284
285
|
const doFetch = config.fetchImpl ?? fetch;
|
|
285
286
|
const stream = (model, context, options) => {
|
|
286
287
|
let sentMaxTokens;
|
|
@@ -295,6 +296,10 @@ export function createOpenAIBrain(config = {}) {
|
|
|
295
296
|
httpLabel: "gateway",
|
|
296
297
|
stallTimeouts: options?.stallTimeouts,
|
|
297
298
|
buildRequest: (overrides) => {
|
|
299
|
+
const root = resolveRequestRoot(model.baseUrl, config.baseUrl);
|
|
300
|
+
if (!root) {
|
|
301
|
+
throw new Error("createOpenAIBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
302
|
+
}
|
|
298
303
|
const routeCredential = resolveRouteCredential({
|
|
299
304
|
optionsApiKey: options?.apiKey,
|
|
300
305
|
optionsHeaders: options?.headers,
|
|
@@ -306,10 +311,6 @@ export function createOpenAIBrain(config = {}) {
|
|
|
306
311
|
if (!routeVerdict.ok)
|
|
307
312
|
throw new BrainError("invalid_request", routeRefusalText(routeVerdict));
|
|
308
313
|
const apiKey = routeCredential.apiKey;
|
|
309
|
-
const root = (model.baseUrl || config.baseUrl || "").replace(/\/+$/, "");
|
|
310
|
-
if (!root) {
|
|
311
|
-
throw new Error("createOpenAIBrain: no baseUrl configured (set config.baseUrl or model.baseUrl)");
|
|
312
|
-
}
|
|
313
314
|
const body = {
|
|
314
315
|
model: model.id,
|
|
315
316
|
messages: toOpenAIMessages(context, config.replayThinking, model),
|
|
@@ -27,29 +27,64 @@
|
|
|
27
27
|
* deployment root — a distinct endpoint with no credential route is a configuration hole, and a
|
|
28
28
|
* loud refusal with a fix hint beats an unauthenticated request to a third-party host.
|
|
29
29
|
*
|
|
30
|
-
* Fail-closed direction: "no credential leak" outranks "silently works"
|
|
31
|
-
*
|
|
32
|
-
* and every refusal names the entry, both URL halves, and a one-line fix.
|
|
30
|
+
* Fail-closed direction: "no credential leak" outranks "silently works", and every refusal names the
|
|
31
|
+
* entry, both URL halves, and a one-line fix.
|
|
33
32
|
*
|
|
34
|
-
* Bad-value loudness (garbage / contradiction / absence):
|
|
33
|
+
* Bad-value loudness (garbage / contradiction / absence / misplacement):
|
|
35
34
|
* - garbage — non-string URL/credential inputs throw a `TypeError` here (a programmer error must
|
|
36
35
|
* not be laundered into a polite verdict); whitespace-only URLs normalize to "" (undeclared).
|
|
37
36
|
* - contradiction — the torn pair refuses with `route.credential_mismatch` + both halves named.
|
|
38
37
|
* - absence — `route.credential_missing` with the fix hint, exactly where absence is a hole.
|
|
38
|
+
* - misplacement — a credential in the URL, or a non-http(s) root: {@link assertServableRequestRoot}.
|
|
39
39
|
*/
|
|
40
40
|
import type { Model } from "../internal/llm.js";
|
|
41
41
|
import type { RouteAdjudication, RouteCredential, RoutePairingConfig, RoutePairingPosture } from "../internal/llm.js";
|
|
42
42
|
import type { Brain, EngineNotice } from "../core/types.js";
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
export declare function
|
|
43
|
+
/** A base URL's ROUTE KEY — the value the pairing law COMPARES, folded to WIRE equivalence: the
|
|
44
|
+
* halves the wire itself treats as case-insensitive (scheme and host, RFC 9110) fold, the scheme's
|
|
45
|
+
* default port elides, and PATH and QUERY ride byte-preserved (a query selects the endpoint as
|
|
46
|
+
* surely as a path does; folding either would compare two different endpoints equal — the widening
|
|
47
|
+
* this module exists to stop). An unparseable value keeps its raw trimmed spelling: fail-closed raw
|
|
48
|
+
* comparison. Non-string input throws — see the module note on garbage.
|
|
49
|
+
*
|
|
50
|
+
* **Never render this.** Byte-preserving on the query means `?api_key=<secret>` rides it verbatim;
|
|
51
|
+
* {@link displayUrlOf} is the one spelling allowed into text. */
|
|
52
|
+
export declare function routeKeyOf(url: string | undefined): string;
|
|
53
|
+
/** A base URL's DISPLAY spelling — the only rendering of a request root allowed into text a human,
|
|
54
|
+
* a log or a consumer sees. Composed from the PARSE (never edited out of raw bytes): userinfo
|
|
55
|
+
* drops, EVERY query value becomes {@link REDACTED_VALUE}, and a query NAME rides only where it
|
|
56
|
+
* really is a name.
|
|
57
|
+
* - no secret-name list: a list has a blind spot at the name nobody listed.
|
|
58
|
+
* - names ride because dropping the query WHOLE renders two roots differing only in query as one
|
|
59
|
+
* string — exactly the pairs the law refuses, so its refusal would read "targets X … paired
|
|
60
|
+
* with X". {@link queryDifferenceNote} closes the case where the names match too.
|
|
61
|
+
* - a BARE token (`?<token>`) renders as one placeholder with NO name: `URLSearchParams` calls it a
|
|
62
|
+
* name with an empty value, and that reading put the payload itself on screen. "Every value is
|
|
63
|
+
* withheld" protects only what IS a value, so the raw segment decides what rides.
|
|
64
|
+
* Answers `""` for an undeclared root, like its key twin. */
|
|
65
|
+
export declare function displayUrlOf(url: string | undefined): string;
|
|
66
|
+
/**
|
|
67
|
+
* The bad-value refusal every base-URL seat owes: **a request root must be an absolute http(s) URL, and
|
|
68
|
+
* it may not carry the credential itself.**
|
|
69
|
+
*
|
|
70
|
+
* Both halves fail the same way if they are not caught here — as a TRANSPORT failure, indistinguishable
|
|
71
|
+
* from an outage, retried for the whole backoff ladder. Measured: `file:`/`ftp:` reject with
|
|
72
|
+
* `TypeError("fetch failed")` over a causeless "unknown scheme", the exact shell a refused connection uses.
|
|
73
|
+
* Stripping userinfo instead of refusing would send an UNAUTHENTICATED request to the host the operator
|
|
74
|
+
* thought they had authenticated to. Refused where the value ARRIVES — construction for `config.baseUrl`,
|
|
75
|
+
* request build for `Model.baseUrl`. An undeclared root is not this rule's question; the PAIRING law keeps
|
|
76
|
+
* its tolerance for an unparseable spelling ({@link routeKeyOf}). Answers the value it judged.
|
|
77
|
+
*/
|
|
78
|
+
export declare function assertServableRequestRoot(url: string | undefined, seat: string): string | undefined;
|
|
79
|
+
/** Remove the userinfo from every authority in free text, WHOLE — the general redactor keeps a short or
|
|
80
|
+
* unprefixed username, and `TOKEN@host` / `TOKEN:x-oauth-basic@host` are the canonical PAT spellings. */
|
|
81
|
+
export declare function stripUrlCredentials(text: string): string;
|
|
82
|
+
/** The request ROOT a brain targets — `Model.baseUrl` else `config.baseUrl` else the lane's default,
|
|
83
|
+
* trailing slashes stripped — with {@link assertServableRequestRoot} applied to both declared
|
|
84
|
+
* halves on the way. The three brains each carried a copy of this coalesce-and-strip line, so a rule
|
|
85
|
+
* about base URLs had three homes; now it has one. Answers `""` when nothing is declared and the lane
|
|
86
|
+
* has no default — whether that is a refusal is the caller's. */
|
|
87
|
+
export declare function resolveRequestRoot(modelBaseUrl: string | undefined, configBaseUrl: string | undefined, laneDefault?: string): string;
|
|
53
88
|
/** Whether a header bag carries an auth-bearing header (any case). */
|
|
54
89
|
export declare function hasAuthCarrier(headers: Record<string, string> | undefined): boolean;
|
|
55
90
|
/**
|
|
@@ -168,7 +203,7 @@ export declare function adjudicateDerivedRoute(args: {
|
|
|
168
203
|
}): Promise<RouteAdjudication | undefined>;
|
|
169
204
|
/**
|
|
170
205
|
* Whether two resolved models denote the SAME route for pre-flight purposes: identical wire id AND
|
|
171
|
-
* identical
|
|
206
|
+
* identical entry ROUTE KEY. `Model.id` alone is NOT route identity — two catalog entries can
|
|
172
207
|
* legitimately share a wire id (the same model served from two endpoints), and a same-id guard
|
|
173
208
|
* would silently skip the pre-flight for exactly the split the pairing law exists to judge.
|
|
174
209
|
*/
|
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
import { AUTH_CARRIER_NAMES } from "./request-params.js";
|
|
2
|
-
|
|
2
|
+
import { BrainError } from "./errors.js";
|
|
3
|
+
const REDACTED_VALUE = "«redacted»";
|
|
4
|
+
const REDACTED_CREDENTIALS = "[redacted-credentials]";
|
|
5
|
+
function servableParse(raw) {
|
|
6
|
+
let u;
|
|
7
|
+
try {
|
|
8
|
+
u = new URL(raw);
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
return u.protocol === "http:" || u.protocol === "https:" ? u : undefined;
|
|
14
|
+
}
|
|
15
|
+
function composableParse(raw) {
|
|
16
|
+
let u;
|
|
17
|
+
try {
|
|
18
|
+
u = new URL(raw);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return u.host !== "" || u.pathname.startsWith("/") ? u : undefined;
|
|
24
|
+
}
|
|
25
|
+
export function routeKeyOf(url) {
|
|
3
26
|
if (url === undefined)
|
|
4
27
|
return "";
|
|
5
28
|
if (typeof url !== "string") {
|
|
@@ -8,13 +31,78 @@ export function normalizeBaseUrl(url) {
|
|
|
8
31
|
const raw = url.trim().replace(/\/+$/, "");
|
|
9
32
|
if (raw === "")
|
|
10
33
|
return "";
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
return `${u.protocol}//${u.host}${u.pathname}${u.search}`.replace(/\/+$/, "");
|
|
14
|
-
}
|
|
15
|
-
catch {
|
|
34
|
+
const u = servableParse(raw);
|
|
35
|
+
if (u === undefined)
|
|
16
36
|
return raw;
|
|
37
|
+
return `${u.protocol}//${u.host}${u.pathname}${u.search}`.replace(/\/+$/, "");
|
|
38
|
+
}
|
|
39
|
+
function querySegments(search) {
|
|
40
|
+
if (search === "" || search === "?")
|
|
41
|
+
return [];
|
|
42
|
+
return search.replace(/^\?/, "").split("&").map((raw) => {
|
|
43
|
+
const eq = raw.indexOf("=");
|
|
44
|
+
return eq === -1 ? { raw } : { raw, name: raw.slice(0, eq), value: raw.slice(eq + 1) };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
export function displayUrlOf(url) {
|
|
48
|
+
const key = routeKeyOf(url);
|
|
49
|
+
if (key === "")
|
|
50
|
+
return "";
|
|
51
|
+
const u = composableParse(key);
|
|
52
|
+
if (u === undefined) {
|
|
53
|
+
const cut = key.search(/[?#]/);
|
|
54
|
+
const head = cut === -1 ? key : key.slice(0, cut);
|
|
55
|
+
const at = head.lastIndexOf("@");
|
|
56
|
+
const shown = at === -1 ? head : `${REDACTED_VALUE}@${head.slice(at + 1)}`;
|
|
57
|
+
return cut === -1 ? shown : `${shown}${key[cut]}${REDACTED_VALUE}`;
|
|
17
58
|
}
|
|
59
|
+
const segs = querySegments(u.search);
|
|
60
|
+
const query = segs.length === 0 ? "" : `?${segs.map((sg) => (sg.raw === "" ? "" : sg.name === undefined ? REDACTED_VALUE : `${sg.name}=${REDACTED_VALUE}`)).join("&")}`;
|
|
61
|
+
return `${u.protocol}//${u.host}${u.pathname}${query}`.replace(/\/+$/, "");
|
|
62
|
+
}
|
|
63
|
+
function queryDifferenceNote(requestKey, declaredKey) {
|
|
64
|
+
if (displayUrlOf(requestKey) !== displayUrlOf(declaredKey))
|
|
65
|
+
return "";
|
|
66
|
+
const names = new Set();
|
|
67
|
+
const ua = composableParse(requestKey);
|
|
68
|
+
const ub = composableParse(declaredKey);
|
|
69
|
+
if (ua !== undefined && ub !== undefined) {
|
|
70
|
+
const a = querySegments(ua.search);
|
|
71
|
+
const b = querySegments(ub.search);
|
|
72
|
+
const valuesOf = (segs, n) => segs.filter((sg) => sg.name === n).map((sg) => sg.value).join("\u0000");
|
|
73
|
+
for (const n of new Set([...a, ...b].flatMap((sg) => (sg.name === undefined ? [] : [sg.name])))) {
|
|
74
|
+
if (valuesOf(a, n) !== valuesOf(b, n))
|
|
75
|
+
names.add(n);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return names.size > 0
|
|
79
|
+
? ` (same rendering — the query values differ under: ${[...names].sort().join(", ")})`
|
|
80
|
+
: ` (same rendering — the difference is in a part this line withholds or folds)`;
|
|
81
|
+
}
|
|
82
|
+
export function assertServableRequestRoot(url, seat) {
|
|
83
|
+
if (typeof url !== "string" || url.trim() === "")
|
|
84
|
+
return undefined;
|
|
85
|
+
const trimmed = url.trim();
|
|
86
|
+
const parsed = URL.canParse(trimmed) ? new URL(trimmed) : undefined;
|
|
87
|
+
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
88
|
+
const named = parsed === undefined ? "it did not parse as a URL" : displayUrlOf(trimmed);
|
|
89
|
+
throw new BrainError("invalid_request", `${seat} is not a servable request root (${named}) — it must be an ABSOLUTE http(s) URL. Another scheme, ` +
|
|
90
|
+
`or a spelling that is not a URL at all, fails at the transport in a shape indistinguishable from an ` +
|
|
91
|
+
`outage, so it would be retried for the whole backoff ladder instead of refused here.`);
|
|
92
|
+
}
|
|
93
|
+
if (parsed.username === "" && parsed.password === "")
|
|
94
|
+
return trimmed;
|
|
95
|
+
throw new BrainError("invalid_request", `${seat} carries credentials in its authority (${parsed.protocol}//${REDACTED_CREDENTIALS}@${parsed.host}${parsed.pathname}) — ` +
|
|
96
|
+
`a request cannot be built from a URL with userinfo, so every call through this brain would fail: put the ` +
|
|
97
|
+
`credential on the apiKey knob or an auth header and leave the URL bare`);
|
|
98
|
+
}
|
|
99
|
+
export function stripUrlCredentials(text) {
|
|
100
|
+
return text.replace(/([A-Za-z][A-Za-z0-9+.\-]*:\/\/)[^/\s"'@]*@/g, `$1${REDACTED_CREDENTIALS}@`);
|
|
101
|
+
}
|
|
102
|
+
export function resolveRequestRoot(modelBaseUrl, configBaseUrl, laneDefault = "") {
|
|
103
|
+
const entry = assertServableRequestRoot(modelBaseUrl, "Model.baseUrl");
|
|
104
|
+
const declared = assertServableRequestRoot(configBaseUrl, "config.baseUrl");
|
|
105
|
+
return (entry ?? declared ?? laneDefault).replace(/\/+$/, "");
|
|
18
106
|
}
|
|
19
107
|
export function hasAuthCarrier(headers) {
|
|
20
108
|
if (!headers)
|
|
@@ -50,9 +138,12 @@ export function resolveRouteCredential(input) {
|
|
|
50
138
|
return { source: "absent" };
|
|
51
139
|
}
|
|
52
140
|
export function adjudicateModelRoute(model, credential, config) {
|
|
53
|
-
const entry =
|
|
54
|
-
const declared =
|
|
141
|
+
const entry = routeKeyOf(model.baseUrl);
|
|
142
|
+
const declared = routeKeyOf(config.baseUrl);
|
|
55
143
|
const request = entry || declared;
|
|
144
|
+
const entryShown = displayUrlOf(entry);
|
|
145
|
+
const declaredShown = displayUrlOf(declared);
|
|
146
|
+
const requestShown = displayUrlOf(request);
|
|
56
147
|
if (credential.source === "per-model")
|
|
57
148
|
return { ok: true, posture: "per-model" };
|
|
58
149
|
if (credential.source === "deployment-config") {
|
|
@@ -65,12 +156,13 @@ export function adjudicateModelRoute(model, credential, config) {
|
|
|
65
156
|
code: "route.credential_mismatch",
|
|
66
157
|
detail: {
|
|
67
158
|
modelId: model.id,
|
|
68
|
-
entryBaseUrl:
|
|
69
|
-
requestBaseUrl:
|
|
70
|
-
configBaseUrl:
|
|
71
|
-
fixHint: `model "${model.id}" targets ${
|
|
159
|
+
entryBaseUrl: entryShown,
|
|
160
|
+
requestBaseUrl: requestShown,
|
|
161
|
+
configBaseUrl: declaredShown,
|
|
162
|
+
fixHint: `model "${model.id}" targets ${requestShown} but the deployment credential is paired with ${declaredShown}` +
|
|
163
|
+
`${queryDifferenceNote(request, declared)}: ` +
|
|
72
164
|
`give the model its own credential (a per-model key via getApiKeyAndHeaders, or an auth header on Model.headers), ` +
|
|
73
|
-
`or serve it through a brain whose config.baseUrl is
|
|
165
|
+
`or serve it through a brain whose config.baseUrl is the model's own root`,
|
|
74
166
|
},
|
|
75
167
|
};
|
|
76
168
|
}
|
|
@@ -81,12 +173,12 @@ export function adjudicateModelRoute(model, credential, config) {
|
|
|
81
173
|
code: "route.credential_missing",
|
|
82
174
|
detail: {
|
|
83
175
|
modelId: model.id,
|
|
84
|
-
entryBaseUrl:
|
|
85
|
-
requestBaseUrl:
|
|
86
|
-
configBaseUrl:
|
|
87
|
-
fixHint: `model "${model.id}" targets ${
|
|
88
|
-
`(the deployment credential root is ${declared}
|
|
89
|
-
`(getApiKeyAndHeaders, or an auth header on Model.headers), or serve it through a brain constructed for
|
|
176
|
+
entryBaseUrl: entryShown,
|
|
177
|
+
requestBaseUrl: requestShown,
|
|
178
|
+
configBaseUrl: declaredShown,
|
|
179
|
+
fixHint: `model "${model.id}" targets ${requestShown} but no credential is configured for that endpoint ` +
|
|
180
|
+
`(the deployment credential root is ${declaredShown})${queryDifferenceNote(request, declared)}: supply a per-model credential ` +
|
|
181
|
+
`(getApiKeyAndHeaders, or an auth header on Model.headers), or serve it through a brain constructed for that root`,
|
|
90
182
|
},
|
|
91
183
|
};
|
|
92
184
|
}
|
|
@@ -166,7 +258,7 @@ export async function adjudicateDerivedRoute(args) {
|
|
|
166
258
|
}
|
|
167
259
|
}
|
|
168
260
|
export function sameRouteIdentity(a, b) {
|
|
169
|
-
return a.id === b.id &&
|
|
261
|
+
return a.id === b.id && routeKeyOf(a.baseUrl) === routeKeyOf(b.baseUrl);
|
|
170
262
|
}
|
|
171
263
|
export function fallbackToPrimaryNotice(args) {
|
|
172
264
|
return {
|
|
@@ -73,6 +73,15 @@ export const ROUTE_ADJUDICATION_CONFORMANCE_CORPUS = [
|
|
|
73
73
|
expect: { ok: false, code: "route.credential_mismatch" },
|
|
74
74
|
leg: "named",
|
|
75
75
|
},
|
|
76
|
+
{
|
|
77
|
+
name: "deployment-mismatch-query-differs",
|
|
78
|
+
intent: "two roots differing only in a QUERY parameter are two routes — the deployment credential is not paired with the second",
|
|
79
|
+
model: { id: "m-tenant", baseUrl: "https://gw.example.com/v1?tenant=b-corp" },
|
|
80
|
+
credential: { source: "deployment-config", apiKey: "k-boot" },
|
|
81
|
+
config: { baseUrl: "https://gw.example.com/v1?tenant=a-corp" },
|
|
82
|
+
expect: { ok: false, code: "route.credential_mismatch" },
|
|
83
|
+
leg: "named",
|
|
84
|
+
},
|
|
76
85
|
{
|
|
77
86
|
name: "deployment-mismatch-derived-leg",
|
|
78
87
|
intent: "the same torn pair on a DERIVED model: same verdict; the engine disposes of it as fallback-to-primary + notice",
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { createAssistantMessageEventStream, } from "../internal/llm.js";
|
|
2
2
|
import { STREAM_TORN_WARNING } from "./stream-shared.js";
|
|
3
3
|
import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
|
|
4
|
-
import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
4
|
+
import { abortLikeName, BrainError, brainErrorCodeOf, classifyConnectFailure, classifyFetchThrow, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
|
|
5
|
+
import { stripUrlCredentials } from "./route-adjudicator.js";
|
|
5
6
|
import { classifyInputTooLong } from "./input-too-long.js";
|
|
6
7
|
import { FAST_MAX_BACKOFF_MS, providerWaitHint, retryBackoffMs } from "./retry.js";
|
|
7
8
|
import { bindBrainStatusEmitter, emitBrainStatus, emitBrainTelemetry } from "./status-sink.js";
|
|
8
9
|
import { createConnectController, resolveStallTimeoutMs } from "./timeout.js";
|
|
9
|
-
import { thrownText } from "../core/thrown-value.js";
|
|
10
|
+
import { readMember, thrownText } from "../core/thrown-value.js";
|
|
11
|
+
import { redactSecrets } from "../core/untrusted-egress.js";
|
|
10
12
|
const DEFAULT_MAX_RETRIES = 10;
|
|
11
13
|
const MAX_RETRIES_ENV_CEILING = 15;
|
|
12
14
|
const MAX_RETRIES_ENV = "SEMA_MAX_RETRIES";
|
|
@@ -215,7 +217,7 @@ function emptyAssistant(model) {
|
|
|
215
217
|
};
|
|
216
218
|
}
|
|
217
219
|
function isAbortError(err) {
|
|
218
|
-
return
|
|
220
|
+
return abortLikeName(err) !== undefined;
|
|
219
221
|
}
|
|
220
222
|
function sleep(ms, signal) {
|
|
221
223
|
return new Promise((resolve) => {
|
|
@@ -272,15 +274,17 @@ export function runStreamingBrain(args) {
|
|
|
272
274
|
const errorMsg = emptyAssistant(model);
|
|
273
275
|
errorMsg.stopReason = aborted ? "aborted" : "error";
|
|
274
276
|
errorMsg.errorMessage = thrownText(err);
|
|
275
|
-
if (!aborted && err
|
|
276
|
-
if (err
|
|
277
|
+
if (!aborted && brainErrorCodeOf(err) !== undefined) {
|
|
278
|
+
if (readMember(err, "inputTooLong") === true)
|
|
277
279
|
errorMsg.errorKind = "input_too_long";
|
|
278
|
-
else if (err
|
|
280
|
+
else if (readMember(err, "inputTooLongRuledOut") === true)
|
|
279
281
|
errorMsg.inputTooLongRuledOut = true;
|
|
280
|
-
if (err
|
|
282
|
+
if (readMember(err, "apiError") === true) {
|
|
281
283
|
errorMsg.isApiErrorMessage = true;
|
|
282
|
-
|
|
283
|
-
|
|
284
|
+
const status = readMember(err, "status");
|
|
285
|
+
const statusNum = typeof status === "number" ? status : undefined;
|
|
286
|
+
if (namesTheFailure(statusNum))
|
|
287
|
+
errorMsg.apiErrorStatus = statusNum;
|
|
284
288
|
if (requestIdSeen !== undefined)
|
|
285
289
|
errorMsg.requestId = requestIdSeen;
|
|
286
290
|
}
|
|
@@ -338,6 +342,10 @@ export function runStreamingBrain(args) {
|
|
|
338
342
|
cc.dispose();
|
|
339
343
|
throw e;
|
|
340
344
|
}
|
|
345
|
+
else if (classifyFetchThrow(e) === "request_unbuildable") {
|
|
346
|
+
cc.dispose();
|
|
347
|
+
throw new BrainError("invalid_request", `${httpLabel} request could not be built: ${redactSecrets(stripUrlCredentials(thrownText(e)))}`);
|
|
348
|
+
}
|
|
341
349
|
else {
|
|
342
350
|
netErr = e;
|
|
343
351
|
}
|
|
@@ -575,7 +583,7 @@ export function runStreamingBrain(args) {
|
|
|
575
583
|
return false;
|
|
576
584
|
}
|
|
577
585
|
catch (e) {
|
|
578
|
-
if (
|
|
586
|
+
if (brainErrorCodeOf(e) !== "server" || !parser.snapshot().hasSubstantiveText)
|
|
579
587
|
throw e;
|
|
580
588
|
return true;
|
|
581
589
|
}
|
package/dist/core/arg-summary.js
CHANGED
|
@@ -196,14 +196,15 @@ export function runRedactionPasses(input, passes, report) {
|
|
|
196
196
|
return result;
|
|
197
197
|
}
|
|
198
198
|
export const SECRET_PASSES = [
|
|
199
|
-
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur])[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
|
|
199
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])(?:sk|pk|rk|gh[opsur]|github_pat)[-_][A-Za-z0-9_-]{8,}/g, replace: "[redacted]" },
|
|
200
200
|
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AIza[A-Za-z0-9_-]{20,}/g, replace: "[redacted]" },
|
|
201
201
|
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])xox[baprs]-[A-Za-z0-9-]{10,}/g, replace: "[redacted]" },
|
|
202
202
|
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])AKIA[0-9A-Z]{12,}/g, replace: "[redacted]" },
|
|
203
203
|
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])hf_[A-Za-z0-9]{20,}/g, replace: "[redacted]" },
|
|
204
204
|
{ kind: "jwt", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])eyJ[A-Za-z0-9._-]{20,}/g, replace: "[redacted]" },
|
|
205
|
-
{ kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-
|
|
205
|
+
{ kind: "private-key-block", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /-----BEGIN[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----[\s\S]*?(?:-----END[ A-Z0-9]*PRIVATE KEY(?: BLOCK)?-----|$)/g, replace: "[redacted]" },
|
|
206
206
|
{ kind: "bearer-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(?<![A-Za-z0-9])Bearer\s+[A-Za-z0-9._~+/-]{8,}=*/gi, replace: "[redacted]" },
|
|
207
|
+
{ kind: "prefixed-token", confidence: "high", marker: "[redacted]", source: "arg-summary", re: /(AccountKey=)[A-Za-z0-9+/]{40,}={0,2}/g, replace: "$1[redacted]" },
|
|
207
208
|
{
|
|
208
209
|
kind: "keyword-value",
|
|
209
210
|
confidence: "medium",
|
|
@@ -247,12 +248,22 @@ export function primaryActivityArg(args) {
|
|
|
247
248
|
val = basename(path);
|
|
248
249
|
}
|
|
249
250
|
else if (url !== undefined) {
|
|
250
|
-
try {
|
|
251
|
-
|
|
252
|
-
val = `${u.origin}${u.pathname}`;
|
|
251
|
+
const parsed = (() => { try {
|
|
252
|
+
return new URL(url);
|
|
253
253
|
}
|
|
254
254
|
catch {
|
|
255
|
-
|
|
255
|
+
return undefined;
|
|
256
|
+
} })();
|
|
257
|
+
if (parsed !== undefined && parsed.host !== "") {
|
|
258
|
+
val = `${parsed.origin}${parsed.pathname}`;
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
const head = url.split(/[?#]/, 1)[0] ?? url;
|
|
262
|
+
const sep = head.indexOf("://");
|
|
263
|
+
const scheme = sep > 0 && /^[a-z][a-z0-9+.-]*$/i.test(head.slice(0, sep)) ? head.slice(0, sep + 3) : "";
|
|
264
|
+
const rest = head.slice(scheme.length);
|
|
265
|
+
const at = rest.lastIndexOf("@");
|
|
266
|
+
val = scheme + (at === -1 ? rest : rest.slice(at + 1));
|
|
256
267
|
}
|
|
257
268
|
}
|
|
258
269
|
else {
|