@sema-agent/settings-schema 17.2.0 → 18.0.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 +166 -0
- package/README.md +41 -5
- package/conformance/VERSION +1 -1
- package/conformance/baseline.json +3 -0
- package/conformance/cases/c01-entry-salvage-then-typo/expected.json +4 -1
- package/conformance/cases/c02-hosts-clamp-stacked/expected.json +4 -1
- package/conformance/cases/c03-literal-dotted-top-key/expected.json +4 -1
- package/conformance/cases/c04-all-bad-collection-not-salvaged/expected.json +4 -1
- package/conformance/cases/c07-hooks-is-not-a-config-domain/expected.json +4 -1
- package/conformance/cases/c08-limits-unknown-carried-64/expected.json +4 -1
- package/conformance/cases/c09-limits-unknown-carried-65/expected.json +4 -1
- package/conformance/cases/c10-record-key-secret-redacted-twice/expected.json +4 -1
- package/conformance/cases/c11-unicode-unknown-keys/expected.json +4 -1
- package/conformance/cases/c12-prototype-name-top-keys/expected.json +4 -1
- package/conformance/cases/c13-models-field-null/expected.json +4 -1
- package/conformance/cases/c14-models-field-empty-array/expected.json +4 -1
- package/conformance/cases/c15-models-field-number/expected.json +4 -1
- package/conformance/cases/c16-deep-nesting-205/expected.json +3 -0
- package/conformance/cases/c17-empty-domain-file/expected.json +3 -0
- package/conformance/cases/c18-wire-envelope-not-an-object/expected.json +4 -1
- package/conformance/cases/c19-wire-envelope-no-version/expected.json +4 -1
- package/conformance/cases/c21-governance-empty-object-wins/expected.json +4 -1
- package/conformance/cases/c23-mixed-validity-models/expected.json +4 -1
- package/conformance/cases/c24-valid-entries-with-unknown-keys/expected.json +4 -1
- package/conformance/cases/c25-published-roundtrip-zero-warnings/expected.json +4 -1
- package/conformance/cases/c26-hosts-array-1024/expected.json +4 -1
- package/conformance/cases/c27-hosts-array-1025/expected.json +4 -1
- package/conformance/cases/c28-large-document-over-1mb/expected.json +4 -1
- package/conformance/cases/c29-salvage-then-domain-refine-fails/expected.json +4 -1
- package/conformance/cases/c30-version-triple-a-first/expected.json +4 -1
- package/conformance/cases/c31-version-triple-b-changed/expected.json +4 -1
- package/conformance/cases/c32-version-triple-a-restored/expected.json +4 -1
- package/conformance/cases/c33-limits-nested-carried-key/expected.json +4 -1
- package/conformance/cases/c34-skills-content-vs-manifest/expected.json +7 -1
- package/conformance/cases/c35-portable-domain-file-is-null/expected.json +3 -0
- package/conformance/cases/c36-skills-entry-without-content/expected.json +7 -1
- package/conformance/cases/c37-skills-field-null/expected.json +6 -0
- package/conformance/cases/c38-remote-exec-is-a-non-domain-file/expected.json +4 -1
- package/conformance/cases/c39-hidden-file-in-config-d/expected.json +4 -1
- package/conformance/cases/c40-s712-identity-only-what-reads-out/expected.json +4 -1
- package/conformance/cases/c41-s792-encoded-pem-identity/expected.json +4 -1
- package/conformance/cases/c42-s792-unknown-key-secret-redacted/expected.json +4 -1
- package/conformance/cases/c43-s792-form-and-deep-encoded-pem/expected.json +4 -1
- package/conformance/cases/c44-s792-utf8-escape-glued-token/expected.json +4 -1
- package/conformance/cases/c45-s792-identity-bounds-and-decode-depth/expected.json +4 -1
- package/conformance/cases/c46-s811-host-name-url-userinfo-withheld/expected.json +4 -1
- package/conformance/cases/c47-s811-roles-key-url-userinfo-withheld/expected.json +4 -1
- package/conformance/cases/c48-s935-model-headers-transported/expected.json +4 -1
- package/conformance/cases/c49-s935-model-headers-secret-entry-dropped/expected.json +4 -1
- package/conformance/cases/c50-s784-lane-host-legal/expected.json +4 -1
- package/conformance/cases/c51-s784-lane-e2b-legal/expected.json +4 -1
- package/conformance/cases/c52-s784-lane-k8s-legal/expected.json +4 -1
- package/conformance/cases/c53-s784-lane-ssh-legal/expected.json +4 -1
- package/conformance/cases/c54-s784-lane-adb-legal/expected.json +4 -1
- package/conformance/cases/c55-s784-lane-local-docker-legal/expected.json +4 -1
- package/conformance/cases/c56-s784-lane-device-legal/expected.json +4 -1
- package/conformance/cases/c57-s784-g1-enabled-no-lane/expected.json +4 -1
- package/conformance/cases/c58-s784-g2-multitenant-host/expected.json +4 -1
- package/conformance/cases/c59-s784-g3-k8s-missing-image/expected.json +4 -1
- package/conformance/cases/c60-s784-g3-e2b-missing-key/expected.json +4 -1
- package/conformance/cases/c61-s784-g3-other-lane-key/expected.json +4 -1
- package/conformance/cases/c62-s784-g7a-session-tidb/expected.json +4 -1
- package/conformance/cases/c63-s784-host-two-replicas/expected.json +4 -1
- package/conformance/cases/c64-s784-multitenant-local-store/expected.json +4 -1
- package/conformance/cases/c65-s784-pg-without-db/expected.json +4 -1
- package/conformance/cases/c66-s784-memory-backend-mismatch/expected.json +4 -1
- package/conformance/cases/c67-s784-retired-field-loud/expected.json +4 -1
- package/conformance/cases/c68-s784-device-local-store/expected.json +4 -1
- package/conformance/cases/c69-deep-encoded-token-past-every-reading/expected.json +4 -1
- package/conformance/cases/c70-rewritten-token-in-front-of-encoded-pem-header/expected.json +4 -1
- package/conformance/cases/c71-one-scanner-write-reads-display-forms/expected.json +4 -1
- package/conformance/cases/c72-dangling-reference-write-seat-refuses/expected.json +4 -1
- package/conformance/cases/c73-projects-extended-registration-reads/expected.json +4 -1
- package/conformance/cases/c74-projects-over-bound-refused/expected.json +4 -1
- package/conformance/cases/c75-projects-patch-remote-uniqueness/expected.json +4 -1
- package/conformance/cases/c76-projects-entry-salvage-nested/expected.json +4 -1
- package/conformance/cases/c77-prompts-center-artifact-carried/expected.json +4 -1
- package/conformance/cases/c78-prompts-empty-bindings-refused/expected.json +4 -1
- package/conformance/cases/c79-prompts-bad-worker-artifact-not-salvaged/expected.json +4 -1
- package/conformance/cases/c80-prompts-wire-v2-verbatim/expected.json +7 -1
- package/conformance/cases/c81-prompts-declaration-bindings-refused/expected.json +4 -1
- package/conformance/cases/c82-prompts-v1-catalog-refused/expected.json +4 -1
- package/conformance/cases/c83-prompts-bad-overlay-not-salvaged/expected.json +4 -1
- package/conformance/cases/c84-sandbox-git-block-carried-verbatim/expected.json +7 -1
- package/conformance/cases/c85-sandbox-git-refused-block-still-carried/expected.json +7 -1
- package/conformance/cases/c86-a2a-peers-legal/expected.json +135 -0
- package/conformance/cases/c86-a2a-peers-legal/input.json +32 -0
- package/conformance/cases/c86-a2a-peers-legal/meta.json +5 -0
- package/conformance/cases/c87-a2a-credential-refs-not-partially-salvaged/expected.json +123 -0
- package/conformance/cases/c87-a2a-credential-refs-not-partially-salvaged/input.json +39 -0
- package/conformance/cases/c87-a2a-credential-refs-not-partially-salvaged/meta.json +5 -0
- package/conformance/cases/cc01-mcp-toolfaces-non-object/expected.json +4 -1
- package/conformance/cases/cc02-mcp-source-key-is-transported/expected.json +4 -1
- package/conformance/cases/cc03-model-compat-unknown-subkey/expected.json +4 -1
- package/conformance/cases/cli01-model-name-nfc-vs-nfd/expected.json +4 -1
- package/conformance/cases/cli02-control-and-zero-width-chars/expected.json +4 -1
- package/conformance/cases/cli03-credential-pasted-into-unknown-key/expected.json +4 -1
- package/conformance/cases/cli04-env-value-with-newline/expected.json +4 -1
- package/conformance/cases/cli05-long-and-prefix-colliding-names/expected.json +4 -1
- package/conformance/cases/t01-absent-domain-key-not-null/expected.json +4 -1
- package/conformance/cases/t03-prototype-name-key-not-swallowed/expected.json +4 -1
- package/conformance/cases/t04-secret-literal-never-in-warning-value/expected.json +4 -1
- package/conformance/cases/z01-every-domain-empty-object/expected.json +4 -1
- package/conformance/cases/z02-every-domain-unknown-key/expected.json +4 -1
- package/dist/center-refs.d.ts +4 -3
- package/dist/center-refs.js +8 -6
- package/dist/cli.d.ts +1 -1
- package/dist/config-fns.d.ts +6 -1
- package/dist/config-fns.js +16 -2
- package/dist/execution-lanes.d.ts +158 -11
- package/dist/execution-lanes.js +76 -11
- package/dist/file-edit.d.ts +1 -1
- package/dist/file-edit.js +2 -1
- package/dist/mcp-request-entry.d.ts +11 -21
- package/dist/mcp-request-entry.js +21 -38
- package/dist/one-time-migrations.js +1 -1
- package/dist/portable-domains.d.ts +1 -1
- package/dist/portable-domains.js +1 -1
- package/dist/remote-exec.d.ts +5 -0
- package/dist/remote-exec.js +5 -0
- package/dist/reserved-member-name.d.ts +13 -0
- package/dist/reserved-member-name.js +14 -0
- package/dist/retired-envs.js +2 -0
- package/dist/secret-refs.d.ts +1 -1
- package/dist/secret-refs.js +5 -1
- package/dist/types.d.ts +70 -41
- package/dist/types.js +179 -32
- package/dist/url-credential.d.ts +12 -2
- package/dist/url-credential.js +164 -9
- package/dist/wire-name.d.ts +19 -0
- package/dist/wire-name.js +21 -0
- package/docs/API-SURFACE.md +22 -5
- package/docs/CONSUMER-INTEGRATION.md +55 -7
- package/llms-full.txt +36 -17
- package/llms.txt +9 -9
- package/package.json +1 -1
package/dist/types.js
CHANGED
|
@@ -22,21 +22,24 @@ import { MAX_EXTRA_BODY_DEPTH } from "./hash.js"; // reconcile the extraBody dep
|
|
|
22
22
|
import { DispatchPolicy, FLEET_GENERATION_ENV, FLEET_SCOPE_ENV } from "./fleet.js"; // WorkerSpec.fleet.dispatch (scheduling framework D3)
|
|
23
23
|
import { isLoopbackAuthHost } from "./loopback.js";
|
|
24
24
|
import { isJsonArray, isPlainJsonObject } from "./plain-json-object.js"; // THE "is this a JSON object" predicate + the total array test
|
|
25
|
-
import { appendOwn, guardedSafeParse, isAccessorDescriptor } from "./own-key-read.js"; // the one spelling of "is this descriptor an accessor" (its own fields only) + the one guarded zod verdict
|
|
25
|
+
import { appendOwn, guardedSafeParse, isAccessorDescriptor, ownEntriesOf } from "./own-key-read.js"; // the one spelling of "is this descriptor an accessor" (its own fields only) + the one guarded zod verdict
|
|
26
26
|
import { httpUrlFlaws, httpUrlRule } from "./url-shape.js"; // 11.0.0: THE URL shape judge // 11.0.0: the one narrow「本机」predicate (auth relaxation)
|
|
27
27
|
import { secretScanForms } from "./secret-scan.js"; // package-private normalization — the one set of forms both scanners read (not in the barrel)
|
|
28
28
|
import { WEB_ADMIN_ENV } from "./web-admin-env.js"; // the three connection keys the config renderer writes are reserved worker env
|
|
29
29
|
import { PREVIOUS_WEB_ADMIN_ENV } from "./previous-web-admin-env.js"; // the previous generation's names of those three (the renderer still writes them for engines that read them)
|
|
30
30
|
import { retiredEnvRefusal } from "./retired-envs.js"; // the worker env write gate asks the retired-env tombstone table too (one table, one refusal sentence)
|
|
31
31
|
import { Sha256Digest } from "./sha256.js"; // the one `sha256:<hex>` digest form
|
|
32
|
-
import { EXECUTION_LANES, EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS, isBlankCoordinate, retiredLaneWordRefusal } from "./execution-lanes.js"; // 7.0.0 M-R2: the lane / store vocabulary (one exported table)
|
|
32
|
+
import { EXECUTION_LANES, EXECUTION_LANE_WORDS, LANE_SET_RULE, MEMORY_ENGINE_BACKENDS, SESSION_BACKENDS, STORE_BACKENDS, isBlankCoordinate, laneSetFlaws, retiredLaneWordRefusal } from "./execution-lanes.js"; // 7.0.0 M-R2: the lane / store vocabulary (one exported table)
|
|
33
33
|
import { LANE_ENV_OWNER, envKeyMigration, laneEnvSeat, laneRowOf } from "./execution-lane-index.js"; // package-private indexes derived from that table (none of them is in the barrel)
|
|
34
34
|
import { migrationSignpost, retiredFormSentence, retiredKey } from "./retired-form.js"; // 11.0.0: the ONE retired-form sentence + key tombstone (package-private)
|
|
35
35
|
import { retiredRoleWordsRefusal } from "./retired-role-words.js"; // 12.0.0: the retired role-word tombstone (package-private; SS-M8)
|
|
36
36
|
import { buildQuotedIssue, fixed } from "./quoted-issue.js"; // THE minting point of custom issues (package-private)
|
|
37
|
+
import { unwrapSchema } from "./zod-unwrap.js"; // 18.0.0: ATOMIC_SALVAGE registers the schema the salvage walker reaches (wrappers peeled with the walker's own reader)
|
|
38
|
+
import { RESERVED_MEMBER_NAMES, isReservedMemberName } from "./reserved-member-name.js"; // 18.0.0: the one reserved-member-name table (the record reader and the center-refs resolver ask the same predicate)
|
|
37
39
|
import { AbsoluteHostPath } from "./host-path.js"; // the one host-path shape judge (a task's cwd and a project's folder / file entries)
|
|
38
40
|
import { RFC3339_DATE_TIME } from "./rfc3339.js"; // the package's one RFC 3339 date-time judge (package-private)
|
|
39
41
|
import { DEVICE_RELAY_RULE, deviceRelayFlaws } from "./device-relay.js"; // 16.0.0: the value grammar of the device lane's DEVICE_RELAY_URL (ws(s) URL or the literal none)
|
|
42
|
+
import { TOOL_WIRE_NAME_MAX_CHARS, isUsableWireName } from "./wire-name.js"; // 18.0.0: the one "usable wire name" judge (the a2a peer name; the request-plane MCP / A2A readers ask the same function)
|
|
40
43
|
// COARSE, best-effort literal-secret SHAPE detector (NOT exhaustive — the real boundary is the env-NAME schema
|
|
41
44
|
// on structured fields). Rejected in free-form value fields (worker `env`, model `extraBody`), URL components,
|
|
42
45
|
// AND as an env-NAME value (an AWS key id is a valid env-NAME charset yet is a secret VALUE, not a NAME). Case-
|
|
@@ -1476,7 +1479,11 @@ export const ExecutionPolicy = z.object({
|
|
|
1476
1479
|
/** Which execution lanes are allowed. OPEN value domain — z.string(), NEVER an enum: adding a lane
|
|
1477
1480
|
* must not be a BREAKING schema change (promised to the service). The closed set of lanes a worker can
|
|
1478
1481
|
* declare is `EXECUTION_LANE_WORDS` (`execution-lanes.ts`, 7.0.0) — read it there, not from a copy in this
|
|
1479
|
-
* comment. Unknown strings are consumer-defined.
|
|
1482
|
+
* comment. Unknown strings are consumer-defined.
|
|
1483
|
+
* 18.0.0 —— the declarable word for "no execution environment" is `none`; the engine's older internal name for that state, `in-process`,
|
|
1484
|
+
* was never a declarable lane. While engines that answer with the old name and engines that answer with `none` both read a policy, the policy
|
|
1485
|
+
* DATA lists both strings (`["in-process", "none", …]` — legal content of this open list, not a second reading in code); once every engine
|
|
1486
|
+
* reading it answers `none`, delete `"in-process"` from the data. */
|
|
1480
1487
|
allowedLanes: z.array(z.string().min(1)).prefault(["host"]),
|
|
1481
1488
|
/** Org 默认会话镜像目标(0.10.1,附B 两层)。ABSENT = INERT(web-admin 未管镜像,壳解析序里本腿为空)。
|
|
1482
1489
|
* 读损档随域(DOMAIN_READ_FALLBACK.execution="throw" 已覆盖:损坏绝不静默退默认放权)。 */
|
|
@@ -1796,6 +1803,8 @@ const WorkerSecretRefs = z.object({
|
|
|
1796
1803
|
* · **加** `DB_BACKEND` / `MEMORY_ENGINE` / `MEMORY_ENGINE_BACKEND` —— `runtime.store` 产出它们。
|
|
1797
1804
|
* · **删** `MEMORY_BACKEND`(引擎从不读的死名,渲染器不再产出)与 `TIDB_*` 族(引擎的墓碑名,见之拒启;结构字段不再产出它们)。
|
|
1798
1805
|
* MYSQL_* / PG_* 两族归 `runtime.store.db` + `secretRefs.dbPasswordEnv`。
|
|
1806
|
+
* 18.0.0 删 `REQUIRE_EXECUTION_LANE`:每个部署都点名车道之后它恒真,不再是旋钮 —— 引擎不读、渲染器不写,名字进墓碑表 `RETIRED_ENVS`(去向 `REMOTE_EXEC`);
|
|
1807
|
+
* 逃生口里写它 ⇒ 墓碑句(下一段的第二个问题)。迁移 SS-M4 识别它的键集(`LANE_WORD_KEYS`)不动。
|
|
1799
1808
|
*
|
|
1800
1809
|
* `env` / `secretEnv` 的写闸问**两张表**,各答一个问题,互相独立:
|
|
1801
1810
|
* · 本表答「规格作者可不可以自己设这个名」—— 不可以:它归某个结构字段,或归渲染器 / 编排器写,或与渲染器 / 编排器写的名互斥(拒句指路到那个写位);
|
|
@@ -1857,7 +1866,6 @@ const RESERVED_ENV_FIELD = Object.freeze({
|
|
|
1857
1866
|
[WEB_ADMIN_ENV.tokenFile]: WEB_ADMIN_OPERATOR_FILE_SEAT, [PREVIOUS_WEB_ADMIN_ENV.tokenFile]: WEB_ADMIN_OPERATOR_FILE_SEAT,
|
|
1858
1867
|
[WEB_ADMIN_ENV.assertionToken]: WEB_ADMIN_ASSERTION_SEAT.token,
|
|
1859
1868
|
[WEB_ADMIN_ENV.assertionTokenFile]: WEB_ADMIN_OPERATOR_FILE_SEAT,
|
|
1860
|
-
REQUIRE_EXECUTION_LANE: "runtime.execution.lanes (the renderer emits REQUIRE_EXECUTION_LANE=true for every declared lane set)",
|
|
1861
1869
|
...Object.fromEntries([...LANE_ENV_OWNER].map(([name, owner]) => [name, laneEnvSeat(name, owner)])),
|
|
1862
1870
|
});
|
|
1863
1871
|
/** 结构化 WorkerSpec 字段已经产出的 env 名 —— 通用 `env` / `secretEnv` 逃生口**不许**设它们(用专属字段)。由
|
|
@@ -1896,11 +1904,117 @@ const nonSecretLiteral = (minLength) => z
|
|
|
1896
1904
|
const COORDINATE_VALUE_GRAMMAR = new Map([
|
|
1897
1905
|
["DEVICE_RELAY_URL", { flaws: deviceRelayFlaws, rule: DEVICE_RELAY_RULE }],
|
|
1898
1906
|
]);
|
|
1899
|
-
/**
|
|
1900
|
-
const LANES_REQUIRED = `runtime.execution.lanes
|
|
1907
|
+
/** 车道集缺席 / 为空时的那一句(块在而缺 `lanes`、`lanes` 是空表,与 enabled 的 worker 根本没有车道块,同一句)。规则句取判官的 {@link LANE_SET_RULE}。 */
|
|
1908
|
+
const LANES_REQUIRED = `runtime.execution.lanes ${LANE_SET_RULE}`;
|
|
1901
1909
|
/**
|
|
1902
|
-
*
|
|
1903
|
-
*
|
|
1910
|
+
* 写闸对车道表的那一读:判官 {@link laneSetFlaws} 的每条拒因一条 issue(穷举 switch:判官加一种拒因而这里不补 ⇒ 编译红)。`path` 相对 `lanes`。
|
|
1911
|
+
* issue 的机读码与此前三只判据(枚举 / `.min(1)` / 去重)各自给的码逐一相同:词形坏(空白 / 不认识 / 退役)= `invalid_value`(`values` = 车道词表;退役词的句子是墓碑句),
|
|
1912
|
+
* 空表 = `too_small`(句子 = 车道集必填那一句),重复 = `custom`;独占车道与别的项同集(18.0.0)= `custom`,落在整张表上。句子都经铸点 `buildQuotedIssue`(写进表里的值是洞)。
|
|
1913
|
+
* 不是字符串的项也是「不是车道词」(`unknown`,同一个码,句子不回显它的值)。
|
|
1914
|
+
* 可续性:表里有不是车道词的项 ⇒ 不可续(`continue: false`)—— 列表连「车道」都不是,后面按车道表行判的(坐标归属 / 必需坐标 / 租户 / 单副本 / 店)都不跑,
|
|
1915
|
+
* 与此前枚举拒的读法同;空表 / 重复 / 不独占 ⇒ 可续(每一项都是车道词)。判官交回全部拒因,这里每条都出(一张表里坏词与重复同在 ⇒ 两条都报)。
|
|
1916
|
+
*/
|
|
1917
|
+
function addLaneSetIssue(ctx, lanes, flaw) {
|
|
1918
|
+
const values = [...EXECUTION_LANE_WORDS];
|
|
1919
|
+
switch (flaw.kind) {
|
|
1920
|
+
case "empty_set":
|
|
1921
|
+
return ctx.addIssue({ code: "too_small", origin: "array", minimum: 1, inclusive: true, input: lanes, ...buildQuotedIssue `${fixed(LANES_REQUIRED)}` });
|
|
1922
|
+
case "blank_entry":
|
|
1923
|
+
return ctx.addIssue({ code: "invalid_value", values, input: lanes[flaw.at], path: [flaw.at], continue: false, ...buildQuotedIssue `an entry of runtime.execution.lanes is empty or whitespace-only — runtime.execution.lanes ${fixed(LANE_SET_RULE)}` });
|
|
1924
|
+
case "unknown": {
|
|
1925
|
+
const { word } = flaw;
|
|
1926
|
+
return ctx.addIssue({
|
|
1927
|
+
code: "invalid_value",
|
|
1928
|
+
values,
|
|
1929
|
+
input: word,
|
|
1930
|
+
path: [flaw.at],
|
|
1931
|
+
continue: false,
|
|
1932
|
+
...(typeof word === "string"
|
|
1933
|
+
? buildQuotedIssue `"${word}" is not an execution lane — runtime.execution.lanes ${fixed(LANE_SET_RULE)}`
|
|
1934
|
+
: buildQuotedIssue `an entry of runtime.execution.lanes is not a string — runtime.execution.lanes ${fixed(LANE_SET_RULE)}`),
|
|
1935
|
+
});
|
|
1936
|
+
}
|
|
1937
|
+
case "retired": {
|
|
1938
|
+
const refusal = retiredLaneWordRefusal(flaw.word);
|
|
1939
|
+
if (refusal === undefined)
|
|
1940
|
+
throw new Error("addLaneSetIssue: a retired-lane flaw names a word the tombstone has no row for (package invariant broken)", { cause: flaw });
|
|
1941
|
+
// the tombstone sentence itself: its way out is given at the source, once
|
|
1942
|
+
return ctx.addIssue({ code: "invalid_value", values, input: flaw.word, path: [flaw.at], continue: false, ...buildQuotedIssue `${fixed(refusal)}` });
|
|
1943
|
+
}
|
|
1944
|
+
case "duplicate":
|
|
1945
|
+
return ctx.addIssue({ code: "custom", path: [flaw.at], ...buildQuotedIssue `lane "${flaw.lane}" is declared twice in runtime.execution.lanes — each lane appears once (the first one is the default)` });
|
|
1946
|
+
case "not_alone":
|
|
1947
|
+
return ctx.addIssue({
|
|
1948
|
+
code: "custom",
|
|
1949
|
+
...buildQuotedIssue `runtime.execution.lanes declares ${fixed(JSON.stringify(flaw.lane))} (no execution environment for the agent's tools) together with other entries — it stands alone: declare lanes: [${fixed(JSON.stringify(flaw.lane))}], or remove it`,
|
|
1950
|
+
});
|
|
1951
|
+
default: {
|
|
1952
|
+
const unhandled = flaw;
|
|
1953
|
+
throw new Error("addLaneSetIssue: unhandled lane-set flaw", { cause: unhandled });
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
}
|
|
1957
|
+
/**
|
|
1958
|
+
* 凭据引用集合(名 → 宿主 env-NAME)的**唯一铸法**:先按**自有属性名**判保留成员名(`isReservedMemberName`,一张表,记录读器与中心引用解析口问同一个谓词;
|
|
1959
|
+
* 经读内核 `ownEntriesOf` 的严归类读名:每个自有串键都读到名字,不调 getter),一条就使集合判不过、record 不再判;再交给 `record`。
|
|
1960
|
+
* zod 的 record 读键前先跳过 `__proto__`(不判它的值、输出里也没有它),只靠 record 判会把「带着一条被剥掉的引用」读成「没有那条引用」,条目带着其余引用活下来。
|
|
1961
|
+
* 只判朴素对象(形状问座位 `isPlainJsonObject`;别的形由 `record` 自己拒,拒句不变);成员名读不出(隐藏键 / 访问器 / 陷阱抛)⇒ 同样判坏,句子是内核那一句(fail-closed:守卫看不清名字就不担保)。
|
|
1962
|
+
* 每一只成员都由它铸,所以成员复用到哪个座位,这道判据就跟到哪个座位;{@link ATOMIC_SALVAGE} 登记的是剥掉包装之后的那只 `record`(抢救走查按它判身份)。
|
|
1963
|
+
*/
|
|
1964
|
+
function credentialRefs(record) {
|
|
1965
|
+
return z.preprocess((input, ctx) => {
|
|
1966
|
+
if (!isPlainJsonObject(input))
|
|
1967
|
+
return input;
|
|
1968
|
+
const members = ownEntriesOf(input);
|
|
1969
|
+
if (!members.ok) {
|
|
1970
|
+
ctx.addIssue({ code: "custom", input, ...buildQuotedIssue `${fixed(members.sentence)}` });
|
|
1971
|
+
return input;
|
|
1972
|
+
}
|
|
1973
|
+
for (const [name] of members.value)
|
|
1974
|
+
if (isReservedMemberName(name))
|
|
1975
|
+
ctx.addIssue({ code: "custom", input, ...buildQuotedIssue `a credential reference is named "${fixed(name)}" — a reserved member name (${fixed(RESERVED_MEMBER_NAMES.join(" | "))}) never names a reference; rename it` });
|
|
1976
|
+
return input;
|
|
1977
|
+
}, record);
|
|
1978
|
+
}
|
|
1979
|
+
/** 车道块的密钥引用(服务变量名 → 宿主 env-NAME)—— 一只具名 schema:它是凭据引用集合,在 {@link ATOMIC_SALVAGE} 里按身份登记。 */
|
|
1980
|
+
const LaneSecretRefs = credentialRefs(z.record(ENV_NAME, ENV_NAME));
|
|
1981
|
+
/** worker 的密钥 env 透传(服务变量名 → 宿主 env-NAME)—— 一只具名 schema:它是凭据引用集合,在 {@link ATOMIC_SALVAGE} 里按身份登记。 */
|
|
1982
|
+
const WorkerSecretEnv = credentialRefs(z.record(ENV_NAME, ENV_NAME).superRefine((rec, ctx) => {
|
|
1983
|
+
retiredEnvIssue(rec, ctx);
|
|
1984
|
+
for (const k of Object.keys(rec))
|
|
1985
|
+
if (RESERVED_WORKER_ENV.has(k))
|
|
1986
|
+
ctx.addIssue({ code: "custom", ...reservedKeyIssue("secretEnv", k) });
|
|
1987
|
+
}));
|
|
1988
|
+
/** 一项是不是可声明的车道词(只给下面的变换收窄类型用:判官放行之后每一项都是)。 */
|
|
1989
|
+
const isDeclarableLane = (v) => EXECUTION_LANE_WORDS.some((w) => w === v);
|
|
1990
|
+
/** 列表的一次快照(长度只读一次,空洞读成 `undefined`):判官判的与变换收窄的是同一份项,不对调用方的数组读第二遍。不是列表 ⇒ 原样(下面的判据拒)。 */
|
|
1991
|
+
const laneListSnapshot = (v) => (isJsonArray(v) ? Array.from({ length: v.length }, (_, i) => v[i]) : v);
|
|
1992
|
+
/**
|
|
1993
|
+
* `runtime.execution.lanes`:写闸只判「是一个列表」(缺 ⇒ 车道集必填那一句;不是列表 ⇒ `invalid_type`),每一项是不是车道词与整张表的合法性一起交给判官
|
|
1994
|
+
* {@link laneSetFlaws}(唯一一处)。不写成 `z.array(元素)`:元素 schema 先判 ⇒ 不是字符串的项得到另一个码、同表其余的项不判;元素写成什么都收的 schema,
|
|
1995
|
+
* 它又成了「本包不判形的解析透明键」—— 它不是,判官判它。有不可续的拒因时 zod 不跑变换;变换只把已判过的项收窄成车道词。
|
|
1996
|
+
*/
|
|
1997
|
+
const DeclaredLanes = z.preprocess(laneListSnapshot, z
|
|
1998
|
+
.unknown()
|
|
1999
|
+
.superRefine((value, ctx) => {
|
|
2000
|
+
if (!isJsonArray(value)) {
|
|
2001
|
+
ctx.addIssue({
|
|
2002
|
+
code: "invalid_type",
|
|
2003
|
+
expected: "array",
|
|
2004
|
+
input: value,
|
|
2005
|
+
continue: false,
|
|
2006
|
+
...(value === undefined ? buildQuotedIssue `${fixed(LANES_REQUIRED)}` : buildQuotedIssue `runtime.execution.lanes is not a list — runtime.execution.lanes ${fixed(LANE_SET_RULE)}`),
|
|
2007
|
+
});
|
|
2008
|
+
return;
|
|
2009
|
+
}
|
|
2010
|
+
for (const flaw of laneSetFlaws(value))
|
|
2011
|
+
addLaneSetIssue(ctx, value, flaw);
|
|
2012
|
+
})
|
|
2013
|
+
.transform((value) => (isJsonArray(value) ? value.filter(isDeclarableLane) : [])));
|
|
2014
|
+
/**
|
|
2015
|
+
* `runtime.execution` —— 这台 worker 的执行车道集(16.0.0;渲染:车道表 `lanes` → `REMOTE_EXEC` + coords / secretRefs 逐键;18.0.0 起不再渲总开关)。
|
|
2016
|
+
* - `lanes`:**有序、去重、非空**的车道表({@link EXECUTION_LANE_WORDS} 的词),首项 = 缺省;合不合法只问判官 {@link laneSetFlaws}。
|
|
2017
|
+
* 不给智能体的工具提供执行环境的 worker 写 `lanes: ["none"]`(18.0.0;独成一集)。
|
|
1904
2018
|
* - `coords`:声明的车道的**非密坐标**(服务变量名 → 字面值),每个键归表里的某一条车道(车道表 `requiredEnv ∪ optionalEnv`);每条声明的车道的必需键都在场且不是空白
|
|
1905
2019
|
* (空串或只含空白按缺 —— 唯一判官 `isBlankCoordinate`);有文法的键({@link COORDINATE_VALUE_GRAMMAR})值还要合它的文法。可选键的空串照收(例:`K8S_RUNTIME_CLASS=""` 在引擎里是「不用 Kata」)。
|
|
1906
2020
|
* - `secretRefs`:声明的车道的**密钥**(服务变量名 → 宿主 env-NAME),每个键归表里的某一条车道,每条声明的车道的必需键在场。
|
|
@@ -1910,21 +2024,13 @@ const LANES_REQUIRED = `runtime.execution.lanes must be a non-empty list — the
|
|
|
1910
2024
|
const ExecutionBlock = z
|
|
1911
2025
|
.object({
|
|
1912
2026
|
lane: retiredKey("runtime.execution.lane", { retiredIn: "16.0.0", now: "runtime.execution.lanes (an ordered lane set; the first one is the default)", migration: "SS-M12" }),
|
|
1913
|
-
lanes:
|
|
1914
|
-
.array(z.enum(EXECUTION_LANE_WORDS, { error: (iss) => retiredLaneWordRefusal(iss.input) }), { error: (iss) => (iss.input === undefined ? LANES_REQUIRED : undefined) }) // a retired lane word gets its way out
|
|
1915
|
-
.min(1, { error: LANES_REQUIRED }),
|
|
2027
|
+
lanes: DeclaredLanes,
|
|
1916
2028
|
coords: z.record(ENV_NAME, nonSecretLiteral(0)).prefault({}),
|
|
1917
|
-
secretRefs:
|
|
2029
|
+
secretRefs: LaneSecretRefs.prefault({}),
|
|
1918
2030
|
})
|
|
1919
2031
|
.superRefine((x, ctx) => {
|
|
1920
|
-
//
|
|
1921
|
-
const declared =
|
|
1922
|
-
x.lanes.forEach((lane, i) => {
|
|
1923
|
-
if (declared.includes(lane))
|
|
1924
|
-
ctx.addIssue({ code: "custom", path: ["lanes", i], ...buildQuotedIssue `lane "${lane}" is declared twice in runtime.execution.lanes — each lane appears once (the first one is the default)` });
|
|
1925
|
-
else
|
|
1926
|
-
declared.push(lane);
|
|
1927
|
-
});
|
|
2032
|
+
// 车道表本身合不合法只问判官(`lanes` 那一格,见 DeclaredLanes);这里按去重后的车道读表行(重复是可续的拒因,所以这里可能见到重复项)。
|
|
2033
|
+
const declared = x.lanes.filter((lane, i) => x.lanes.indexOf(lane) === i);
|
|
1928
2034
|
const rows = declared.map(laneRowOf);
|
|
1929
2035
|
const issue = (slot, key, q) => ctx.addIssue({ code: "custom", path: [slot, key], ...q });
|
|
1930
2036
|
const owned = (secret) => rows.flatMap((row) => (secret ? [...row.requiredSecretEnv, ...row.optionalSecretEnv] : [...row.requiredEnv, ...row.optionalEnv]));
|
|
@@ -2131,15 +2237,7 @@ const WorkerSpecObject = z.object({
|
|
|
2131
2237
|
* 故无法粘贴真值)。给 app 专属令牌用(如 `MY_APP_TOKEN` → 某 env-名)。在 reconciler 里于 `env` 之后解析,
|
|
2132
2238
|
* 故密钥永不被 `env` 同名键遮蔽。`RESERVED_WORKER_ENV` 里的键被拒(那些结构化 secret 走 `secretRefs`);墓碑表 `RETIRED_ENVS` 里的旧名被拒。
|
|
2133
2239
|
*/
|
|
2134
|
-
secretEnv:
|
|
2135
|
-
.record(ENV_NAME, ENV_NAME)
|
|
2136
|
-
.superRefine((rec, ctx) => {
|
|
2137
|
-
retiredEnvIssue(rec, ctx);
|
|
2138
|
-
for (const k of Object.keys(rec))
|
|
2139
|
-
if (RESERVED_WORKER_ENV.has(k))
|
|
2140
|
-
ctx.addIssue({ code: "custom", ...reservedKeyIssue("secretEnv", k) });
|
|
2141
|
-
})
|
|
2142
|
-
.optional(),
|
|
2240
|
+
secretEnv: WorkerSecretEnv.optional(),
|
|
2143
2241
|
/** Opaque recreate trigger (hash-INCLUDED): bump it to force the reconciler to recreate the worker — e.g. to
|
|
2144
2242
|
* re-project a secret VALUE that changed (a secret write doesn't alter the spec, so without this the running
|
|
2145
2243
|
* pod keeps the old value). Set by POST /api/config/workers/<name>/rotate. */
|
|
@@ -2180,7 +2278,7 @@ function workerSpecIssues(w, ctx) {
|
|
|
2180
2278
|
const { execution, store, requirePrincipal } = w.runtime;
|
|
2181
2279
|
// G1:enabled 的 worker 必须声明车道集与店(停用的不渲染,可缺)。
|
|
2182
2280
|
if (w.enabled && execution === undefined) {
|
|
2183
|
-
issue(["runtime", "execution", "lanes"], buildQuotedIssue `an enabled worker must declare its execution lanes (
|
|
2281
|
+
issue(["runtime", "execution", "lanes"], buildQuotedIssue `an enabled worker must declare its execution lanes (the lane set is the deployment's own statement — nothing reads a missing one as some lane) — ${fixed(LANES_REQUIRED)}`);
|
|
2184
2282
|
}
|
|
2185
2283
|
if (w.enabled && store === undefined) {
|
|
2186
2284
|
issue(["runtime", "store", "backend"], buildQuotedIssue `an enabled worker must declare its store — runtime.store.backend: ${fixed(STORE_BACKENDS.join(" | "))}`);
|
|
@@ -2459,7 +2557,7 @@ export const SkillsManifestSchema = z.object({
|
|
|
2459
2557
|
// env-NAME references (`envRefs`/`headerRefs`: target-key → an ENV_NAME in the SERVICE's env). The service's
|
|
2460
2558
|
// `resolveSpec` reads the real values from its own environment — this DB never holds a credential. Like
|
|
2461
2559
|
// skills, MCP is applied at boot/reconcile (not hot) and writing is gated to `publisher` + the publish gate.
|
|
2462
|
-
const McpEnvRefs = z.record(ENV_NAME, ENV_NAME); // target ENV key → ENV_NAME holding its value (both env-name-shaped)
|
|
2560
|
+
const McpEnvRefs = credentialRefs(z.record(ENV_NAME, ENV_NAME)); // target ENV key → ENV_NAME holding its value (both env-name-shaped)
|
|
2463
2561
|
// An HTTP header NAME: an RFC 7230 token that ALSO must not be a secret literal. A secret-SHAPED header name
|
|
2464
2562
|
// (a headerRefs key `sk-live-…`, or principalHeader `ghp_…`) is stored + shipped verbatim on /effective — the
|
|
2465
2563
|
// SAME invariant-① gap as worker.env / model.extraBody / mcp stdio command+args (credentials belong on the
|
|
@@ -2471,7 +2569,20 @@ const HTTP_HEADER_NAME = z
|
|
|
2471
2569
|
.regex(/^[A-Za-z0-9!#$%&'*+.^_`|~-]+$/, "must be an HTTP header NAME")
|
|
2472
2570
|
.refine((s) => !containsSecretToken(s), buildQuotedIssue `HTTP header name must not contain a secret literal — credentials go in the env-NAME value, never the header name`);
|
|
2473
2571
|
// Header NAME → ENV_NAME holding its value. Header names are tokens (RFC 7230) — not env-name-shaped.
|
|
2474
|
-
const McpHeaderRefs = z.record(HTTP_HEADER_NAME, ENV_NAME);
|
|
2572
|
+
const McpHeaderRefs = credentialRefs(z.record(HTTP_HEADER_NAME, ENV_NAME));
|
|
2573
|
+
/**
|
|
2574
|
+
* 18.0.0 —— **凭据引用集合不可部分抢救**(按 schema 身份的集合)。成员是「名 → 宿主 env-NAME」的凭据引用 Record:mcp 的 `envRefs` / `headerRefs`
|
|
2575
|
+
* (a2a 对端的 `headerRefs` 复用后者,因此同在)、车道块的 `secretRefs` 与 worker 的 `secretEnv`。逐条抢救(`salvageEntries`)走到成员**不下钻**、把它当一个值判:
|
|
2576
|
+
* 集合里任一键 / 值坏形 ⇒ 它所属的那一条条目整条 `entry-dropped`,兄弟条目保留。理由:丢掉一条坏引用不是「更窄」,而是让条目带着**一半凭据**活下来 ——
|
|
2577
|
+
* 拨一个远端服务 / 智能体时错误的失败方向(读方对「任一引用的 env 缺席」本来就整条跳过,同一个方向)。
|
|
2578
|
+
* 既有读数只变一处:直接对 `WorkersConfig` 调 `salvageEntries` 时,`secretEnv` 带坏引用的 worker 由「只丢那一条引用、worker 存活」变「整条丢」
|
|
2579
|
+
* (各域读数不变:workers 域不逐条抢救;mcp 的两只住在传输判别联合里,走查不进联合;车道块住在不是集合的 `runtime` 对象里)
|
|
2580
|
+
* —— 由 17.2.0 包的真读数逐行对拍钉住(`test/atomic-salvage-18.0.0.test.ts`,改了读数的那一行逐行登记)。别的集合字段(allowTools / scenarios / allowSkills …)照旧逐条抢救:丢坏元素只会更窄。
|
|
2581
|
+
* 成员读键按自有属性名判:`__proto__` / `constructor` / `prototype` 任一在场 ⇒ 集合坏 ⇒ 条目整条丢(见 `credentialRefs`;zod 的 record 读键前跳过 `__proto__`、不判它的值)。
|
|
2582
|
+
* 新的凭据引用集合必须复用这几只 schema 或由 `credentialRefs` 铸出并登记进这里(按身份判,换一只「形一样」的新 schema 不算成员)。
|
|
2583
|
+
* 登记的是剥掉包装之后的那只 `record`(与抢救走查同一只读法 `unwrapSchema`)。
|
|
2584
|
+
*/
|
|
2585
|
+
export const ATOMIC_SALVAGE = new Set([McpEnvRefs, McpHeaderRefs, LaneSecretRefs, WorkerSecretEnv].map(unwrapSchema));
|
|
2475
2586
|
export const McpTransport = z.discriminatedUnion("kind", [
|
|
2476
2587
|
z.object({
|
|
2477
2588
|
kind: z.literal("stdio"),
|
|
@@ -2609,6 +2720,41 @@ export const McpConfig = z
|
|
|
2609
2720
|
seen.add(s.name);
|
|
2610
2721
|
});
|
|
2611
2722
|
});
|
|
2723
|
+
// ── Domain: a2a (常驻出站 A2A 对端名册 —— 18.0.0) ─────────────────────────────────────────────────────────────
|
|
2724
|
+
// 一个 A2A 对端 = 一个 https 端点(单一传输,没有判别);形是 mcp http 条目的近亲,判官一律复用:URL = NON_SECRET_URL(存量文档座位,与 mcp `transport.url`
|
|
2725
|
+
// 同座;名形判官 `findUrlCredential` 是新提交门的事,不接进读已落盘字节的 schema),`headerRefs` = mcp 的头名 → env-NAME 引用集合(凭据引用,
|
|
2726
|
+
// 在 ATOMIC_SALVAGE 里:坏一项 ⇒ 整个对端丢),`principalHeader` = 头名判官,`name` = 线上名判官(对端名是线上工具名 `a2a__<名>__…` 的身份片)。
|
|
2727
|
+
// 秘密边界同 mcp:中心只存 env-NAME,值在读方自己的环境里。写权限 publisher(一个对端就是一组新挂进智能体的远端技能,同 mcp)。
|
|
2728
|
+
// 首版不收 `notes` / `rosters`(按 roster 定向)/ `toolAxes`(中心面不替调用方担保远端技能的副作用轴):三者都被剥并告警。
|
|
2729
|
+
/** 一个常驻出站 A2A 对端。 */
|
|
2730
|
+
export const A2aPeerSpec = z.object({
|
|
2731
|
+
/** 对端的身份:线上工具名 `a2a__<名>__…` 的命名空间段 —— 线上名判官({@link isUsableWireName}:非空、≤128、首尾无空白;不截断、不 trim)。 */
|
|
2732
|
+
name: z.string().refine(isUsableWireName, buildQuotedIssue `a2a peer name must be non-empty, at most ${fixed(String(TOOL_WIRE_NAME_MAX_CHARS))} characters, with no leading or trailing whitespace — it is the peer's identity in the wire tool names (a2a__<name>__…); it is never trimmed or truncated`),
|
|
2733
|
+
/** 对端的服务 URL(也是知名路径上的 agent card 相对它解析的源)。不带凭据(userinfo / 密钥形值)、不带 fragment。 */
|
|
2734
|
+
url: NON_SECRET_URL,
|
|
2735
|
+
/** agent card 的显式位置(对端不在知名路径上发布时);在场 ⇒ 只用它。同一只 URL 判官。 */
|
|
2736
|
+
cardUrl: NON_SECRET_URL.optional(),
|
|
2737
|
+
/** 头名 → 读方环境里的 env-NAME(例:`Authorization → KB_TOKEN`),**绝不是值**。任一引用坏形 ⇒ 整个对端读不进(不部分抢救);读方对任一缺席的 env 整个对端跳过。 */
|
|
2738
|
+
headerRefs: McpHeaderRefs.optional(),
|
|
2739
|
+
/** 读方把任务的已鉴权终端主体填进去的那个头名(不是引用 —— 值由读方逐请求填,模型碰不到)。 */
|
|
2740
|
+
principalHeader: HTTP_HEADER_NAME.optional(),
|
|
2741
|
+
/** 只挂这些技能 id(白名单);缺席或空 = 对端发布的全部。 */
|
|
2742
|
+
allowSkills: z.array(z.string().min(1)).optional(),
|
|
2743
|
+
/** 挂在哪些场景;空 = 全部(与 mcp / skills 同一条读方过滤)。 */
|
|
2744
|
+
scenarios: z.array(z.string().min(1)).prefault([]),
|
|
2745
|
+
enabled: z.boolean().default(true),
|
|
2746
|
+
});
|
|
2747
|
+
/** `a2a` 域:常驻出站 A2A 对端名册。空 / 缺席 = 不拨任何对端。对端名唯一(重名拒,同 mcp)。 */
|
|
2748
|
+
export const A2aConfig = z
|
|
2749
|
+
.object({ peers: z.array(A2aPeerSpec).prefault([]) })
|
|
2750
|
+
.superRefine((cfg, ctx) => {
|
|
2751
|
+
const seen = new Set();
|
|
2752
|
+
cfg.peers.forEach((p, i) => {
|
|
2753
|
+
if (seen.has(p.name))
|
|
2754
|
+
ctx.addIssue({ code: "custom", ...buildQuotedIssue `duplicate a2a peer "${p.name}"`, path: ["peers", i, "name"] });
|
|
2755
|
+
seen.add(p.name);
|
|
2756
|
+
});
|
|
2757
|
+
});
|
|
2612
2758
|
// ── Domain: plugins (plugin SOURCE REFERENCES — C2 plugins 上云, additive 0.5.0) ──────────────────────────
|
|
2613
2759
|
// The cloud stores WHERE a plugin comes from, NEVER its content (产品拍板): {name, source, version/sha pin}.
|
|
2614
2760
|
// The consuming shell materializes the plugin locally from the ref (git clone / marketplace install) — the
|
|
@@ -2863,6 +3009,7 @@ export const DOMAIN_SCHEMAS = {
|
|
|
2863
3009
|
projects: ProjectsConfig, // 0.10.0: 142-S3 project identity ledger + 无仓铸造权威 (scope=tenant 轴; NOT portable — 租户身份数据)
|
|
2864
3010
|
prompts: PromptsConfig, // 0.10.18 起;14.0.0 单轴:中心 artifact 的身份(`{ artifact?, workers? }`);13.x 两轴的键是墓碑(SS-M10)
|
|
2865
3011
|
limits: LimitsConfig, // 0.19.0: cost/限流族热更新发布位(消费方 #322 批1;本地腿闭集缺口的源头修复)
|
|
3012
|
+
a2a: A2aConfig, // 18.0.0: 常驻出站 A2A 对端名册(便携;本地腿 config.d/a2a.json 与中心腿 /effective.a2a 同一个声明座)
|
|
2866
3013
|
// integrations + access: DELETED in 0.6.0 (EXPERT-REDESIGN §9) — parseDomain/effective/路由随之 404。
|
|
2867
3014
|
};
|
|
2868
3015
|
// ── Effective-config PROVENANCE (the unified shape TOC `sema config --effective` and TOB `/effective?principal=`
|
package/dist/url-credential.d.ts
CHANGED
|
@@ -2,13 +2,23 @@
|
|
|
2
2
|
export declare const URL_CREDENTIAL_REASONS: readonly ["url_userinfo", "url_credential_param"];
|
|
3
3
|
export type UrlCredentialReason = (typeof URL_CREDENTIAL_REASONS)[number];
|
|
4
4
|
/**
|
|
5
|
-
* 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)
|
|
5
|
+
* 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)在这张表上的参数就是凭据;
|
|
6
|
+
* 18.0.0 起以其中的强档词收尾的复合名也是(`db_password` 等,判由 {@link credentialParamWordOf})。
|
|
6
7
|
* 加一个词 = 表里加一格(minor);判官与 {@link isCredentialParamName} 现算,不手抄第二份。
|
|
7
8
|
*/
|
|
8
9
|
export declare const CREDENTIAL_PARAM_NAMES: readonly ["api_key", "apikey", "api-key", "key", "token", "access_token", "refresh_token", "id_token", "auth", "authorization", "proxy-authorization", "password", "passwd", "pwd", "secret", "client_secret", "private_key", "signature", "sig", "x-amz-signature", "x-amz-security-token", "x-amz-credential", "sas"];
|
|
9
10
|
export type CredentialParamName = (typeof CREDENTIAL_PARAM_NAMES)[number];
|
|
10
11
|
/**
|
|
11
|
-
*
|
|
12
|
+
* 🔴 **「这个已解码的参数名是不是凭据名」的唯一答者**(18.0.0):是 ⇒ 名表里的那个词(小写);不是 ⇒ `undefined`。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字)。
|
|
13
|
+
* · 整名(ASCII 大小写不敏感)在 {@link CREDENTIAL_PARAM_NAMES} 里 ⇒ 那个词(整名优先);
|
|
14
|
+
* · 否则末段的尾部是强档词(`password` / `passwd` / `pwd` / `secret` / `apikey`),或名字末尾是尾串闭集的一条(`secret_key` / `secret_access_key` ⇒ `secret`;`api_key` ⇒ `api_key`;`access_token` ⇒ `access_token`;答词的律见 {@link COMPOUND_TAIL_PHRASES})⇒ 那个词。
|
|
15
|
+
* 切段、粘连与对齐的律见模块头「名字判定」。
|
|
16
|
+
* 返回的词是**表词**:消费方拿它去查自己挂在名表上的那一列(例:日志脱敏器的锚强度),不对整名再查一遍表。
|
|
17
|
+
*/
|
|
18
|
+
export declare function credentialParamWordOf(decodedName: string): CredentialParamName | undefined;
|
|
19
|
+
/**
|
|
20
|
+
* 「这个**已解码**的参数名是不是凭据名」:{@link credentialParamWordOf} 有词。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字);
|
|
21
|
+
* 整名在 {@link CREDENTIAL_PARAM_NAMES} 里、或末段 / 尾串收在强档词上(`db_password` / `PGPASSWORD` / `SECRET_KEY`)才算。
|
|
12
22
|
*/
|
|
13
23
|
export declare function isCredentialParamName(decodedName: string): boolean;
|
|
14
24
|
/** 每个拒因词带的细节(键集必须覆盖 {@link URL_CREDENTIAL_REASONS} 的每个词:数组里加一个词而这里没跟 ⇒ 下面的映射型编译红,{@link urlCredentialRule} 的穷举 switch 也红)。 */
|
package/dist/url-credential.js
CHANGED
|
@@ -2,16 +2,28 @@
|
|
|
2
2
|
* **「这个 URL 的 userinfo 或 query / fragment 里,有没有名字在凭据名表上的参数」的名形判官**(14.5.0;lift 自 server `src/url-credential.ts`,规则、拒因词闭集与规则句逐字搬)。纯函数,零 import(除同包的 `./url-shape`),浏览器安全。
|
|
3
3
|
*
|
|
4
4
|
* ## 与 `NON_SECRET_URL`(`./types`)的关系:两个问题,互补,不是一个包含另一个
|
|
5
|
-
* 本判官按**名**判 —— 参数叫什么(
|
|
5
|
+
* 本判官按**名**判 —— 参数叫什么(名字是凭据名 = 整名在 {@link CREDENTIAL_PARAM_NAMES} 上、或以表上的强档词收尾,见下「名字判定」;不论值长什么样);`NON_SECRET_URL` 的 `urlQueryFragmentHasSecret` 按**值形**判密钥 —— 值长得像密钥
|
|
6
6
|
* (不论参数叫什么)。所以 `?key=abc` 本判官命中而 `NON_SECRET_URL` 放行(值不像密钥),`?x=sk-live-…` 反之;userinfo 两边都判。存量域文档位(`mcp` 的 `transport.url` 等)读 `NON_SECRET_URL`,
|
|
7
7
|
* 请求面等座位读本判官。把两个问题合成一只定义是另一件事,这里不做。
|
|
8
8
|
*
|
|
9
9
|
* ## 规则(一条):URL 里的凭据 = 日志脱敏器已经在用的那一个定义
|
|
10
10
|
* 一个 URL 里凭据住三个位置:
|
|
11
11
|
* · **userinfo**(`scheme://<这一段>@host`,只有用户名也算)—— 判由本包的 URL 形判官 {@link httpUrlFlaws}(`["userinfo"]`),本模块不留第二份谓词;
|
|
12
|
-
* · **query 参数**、**fragment 参数**(同一段文法)
|
|
13
|
-
* ASCII 大小写不敏感(不做 Unicode 折叠)
|
|
14
|
-
* 答首个命中(判序 = 上面的序),拒因闭集两词 {@link URL_CREDENTIAL_REASONS};**值从不进答案**,参数名只回显表里的那个词(
|
|
12
|
+
* · **query 参数**、**fragment 参数**(同一段文法)里名字是凭据名的那一个(判由 {@link credentialParamWordOf},下一节)—— 名字按收方的读法读:WHATWG `URLSearchParams` 解一次码,
|
|
13
|
+
* ASCII 大小写不敏感(不做 Unicode 折叠);`tokens` / `keyword` / `password_hash` 不是凭据名。
|
|
14
|
+
* 答首个命中(判序 = 上面的序),拒因闭集两词 {@link URL_CREDENTIAL_REASONS};**值从不进答案**,参数名只回显表里的那个词(小写;复合名回显的是它收尾的那个词,不是调用方的名字字节)。
|
|
15
|
+
*
|
|
16
|
+
* ## 名字判定:整名,或末段(18.0.0)—— 答者一只 {@link credentialParamWordOf}
|
|
17
|
+
* 「这个已解码的名字是不是凭据名」只有一个答者,{@link isCredentialParamName} 与本判官的 query / fragment 臂都读它(不留第二张表、第二条规则):
|
|
18
|
+
* 1. **整名**(ASCII 大小写不敏感)在 {@link CREDENTIAL_PARAM_NAMES} 上 ⇒ 那个词(照旧;整名优先:`client_secret` 答 `client_secret`,不答它的末段 `secret`);
|
|
19
|
+
* 2. 否则名字按 `_` / `-` / `.` 与驼峰边界切段(空段不存在),**末段**的尾部是**强档词**(`password` / `passwd` / `pwd` / `secret` / `apikey`;列在 {@link COMPOUND_TAIL_STANCE})⇒ 那个词 ——
|
|
20
|
+
* 末段整段是它(`db_password` / `dbPassword` / `x-db-secret` / `user_pwd`),或它与前缀粘连(`PGPASSWORD`)都算;词**不跨**分隔符(`app_wd` 不是 `pwd`);
|
|
21
|
+
* 3. 尾串闭集 {@link COMPOUND_TAIL_PHRASES}(逐词对齐,分隔符与大小写不论):`secret key` · `secret access key` ⇒ `secret`(`SECRET_KEY` / `AWS_SECRET_ACCESS_KEY`);`api key` ⇒ `api_key`(`x-api-key` / `X_API_KEY` / `xApiKey`);
|
|
22
|
+
* `access token` ⇒ `access_token`(`github_access_token` / `oauth-access-token`)。答词的律只有一条:尾串用 `_` 连起来本身在整名表里 ⇒ 答那个整名词,否则答强段词。
|
|
23
|
+
* **只看末尾**:前缀 / 中段命中不算,词后还有别的字就不是(`password_hash` / `password_policy` / `secretName` / `pwd_dir` / `passwordless`);数字尾巴不剥(`password2` 的末段是 `password2`)。
|
|
24
|
+
* 名表里别的 18 个词(`token` / `key` / `refresh_token` / `private_key` …)在自由文本和路径里常是普通词、或本身带分隔符,作末尾时只认整名(上面三条的尾词与尾串除外)—— 裸 `access_key` 照整名表,不借尾串扩;
|
|
25
|
+
* 末段不是尾串的末词不命中(`api_key_count` / `token_count` / `api_key_id`)。
|
|
26
|
+
* 误报面(谁受伤):以强档词收尾、但值不是凭据的名字(`show_password=1` / `is_secret=true`)也会被判成凭据名:受理口拒这样的 URL、脱敏器遮它的值 —— 方向是多拒多遮(少洗的一侧是真钥明文)。
|
|
15
27
|
* 不透明形 / 解析不了的值没有可读的部件 ⇒ 答「没有」—— 这一形收不收是**座位**自己的律,不在本判官里。
|
|
16
28
|
*
|
|
17
29
|
* ## 判官只判「有没有」;座位决定怎么用
|
|
@@ -22,12 +34,15 @@
|
|
|
22
34
|
* ## 名表的来源
|
|
23
35
|
* {@link CREDENTIAL_PARAM_NAMES} 是 server 日志脱敏器名表(锚强度表的键)的 23 词。脱敏器另有一列「自由文本里这个词的锚强度」,那一列是脱敏器的事,不在这里;
|
|
24
36
|
* 词表本身在这里成为契约面 —— 脱敏器与本判官读同一张词表(同一个定义)靠消费方把它的表键钉在本导出上(例:`Record<CredentialParamName, …>` 的穷尽映射 ⇒ 加 / 删词不同步即编译红)。
|
|
37
|
+
* 18.0.0 起本模块同表多一列 {@link COMPOUND_TAIL_STANCE}「这个词作为更长名字的末尾时算不算」:同一个 `Record<CredentialParamName, …>` 穷尽映射,表里加一个词而这一列不表态 ⇒ 编译红。
|
|
38
|
+
* 消费方(脱敏器的三个名字读者、受理口判官)读的是 {@link credentialParamWordOf} 返回的词 —— 拿它去查自己那一列(例:锚强度),不再对整名查表。
|
|
25
39
|
*/
|
|
26
40
|
import { httpUrlFlaws, httpUrlRule, isHierarchicalUrl } from "./url-shape.js";
|
|
27
41
|
/** 拒因词(闭集;判序 = 本数组序;词只在这一处写 —— {@link UrlCredentialReason} 与 {@link UrlCredential} 的成员都从它派生)。 */
|
|
28
42
|
export const URL_CREDENTIAL_REASONS = Object.freeze(["url_userinfo", "url_credential_param"]);
|
|
29
43
|
/**
|
|
30
|
-
* 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)
|
|
44
|
+
* 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)在这张表上的参数就是凭据;
|
|
45
|
+
* 18.0.0 起以其中的强档词收尾的复合名也是(`db_password` 等,判由 {@link credentialParamWordOf})。
|
|
31
46
|
* 加一个词 = 表里加一格(minor);判官与 {@link isCredentialParamName} 现算,不手抄第二份。
|
|
32
47
|
*/
|
|
33
48
|
export const CREDENTIAL_PARAM_NAMES = Object.freeze([
|
|
@@ -44,17 +59,157 @@ export const CREDENTIAL_PARAM_NAMES = Object.freeze([
|
|
|
44
59
|
const CREDENTIAL_PARAM_NAME_SET = new Set(CREDENTIAL_PARAM_NAMES);
|
|
45
60
|
/** ASCII 大小写折叠(只折 `A-Z`,不做 Unicode 折叠:`TOKEN` 不是 `token`)。 */
|
|
46
61
|
const asciiLower = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
|
|
62
|
+
/** 名表的**唯一**查口:已折成小写的拼写是名表里的词 ⇒ 那个词,否则 `undefined`(整名臂与尾串答词的解析都经它,模块里不再有第二处读 `Set`)。 */
|
|
63
|
+
function tableWordOf(lowerSpelling) {
|
|
64
|
+
return CREDENTIAL_PARAM_NAME_SET.has(lowerSpelling) ? lowerSpelling : undefined;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* 🔴 名表的第二列:每个词作为更长名字的末尾时算不算(见 {@link CompoundTailStance})。键是 `Record<CredentialParamName, …>` 的**穷尽映射** ——
|
|
68
|
+
* 名表加一个词而这一列没给它表态 ⇒ 缺键,编译红;名表删一个词而这一列还留着 ⇒ 多余键,编译红。词表与这一列同住在本模块,消费方只读 {@link credentialParamWordOf} 的答案。
|
|
69
|
+
*/
|
|
70
|
+
const COMPOUND_TAIL_STANCE = {
|
|
71
|
+
api_key: "whole-name-only", apikey: "tail-word", "api-key": "whole-name-only", key: "whole-name-only",
|
|
72
|
+
token: "whole-name-only", access_token: "whole-name-only", refresh_token: "whole-name-only", id_token: "whole-name-only",
|
|
73
|
+
auth: "whole-name-only", authorization: "whole-name-only", "proxy-authorization": "whole-name-only",
|
|
74
|
+
password: "tail-word", passwd: "tail-word", pwd: "tail-word",
|
|
75
|
+
secret: "tail-word", client_secret: "whole-name-only", private_key: "whole-name-only",
|
|
76
|
+
signature: "whole-name-only", sig: "whole-name-only",
|
|
77
|
+
"x-amz-signature": "whole-name-only", "x-amz-security-token": "whole-name-only", "x-amz-credential": "whole-name-only",
|
|
78
|
+
sas: "whole-name-only",
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* 尾串闭集(多词):名表里没有、但名字末尾是这几个词的串就是凭据名。
|
|
82
|
+
*
|
|
83
|
+
* 🔴 **尾串答词只有一条律,不逐项特判**:尾串的词用 `_` 连起来,若**本身就是整名表里的词**,答那个整名词(与整名的答案同形,下游挂在名表上的列〔锚强度〕随之同形);
|
|
84
|
+
* 否则答它的**强段词**(`strong`,名表里态度为 `tail-word` 的词)。单词尾词是同一条律的退化形(一个词连起来就是它自己,它在表里)。解析在加载期做一次({@link COMPOUND_TAILS}):
|
|
85
|
+
* 既不在整名表里、又没写强段词的尾串 ⇒ 抛,不静默成死条目。
|
|
86
|
+
* · `secret key` · `secret access key`:`secret_key` / `secret_access_key` 不在整名表 ⇒ 答 `secret` —— `SECRET_KEY` · `AWS_SECRET_ACCESS_KEY` 的真钥住在这样的名字里,而末段只是 `KEY`(常用词);
|
|
87
|
+
* · `api key`:`api_key` 在整名表 ⇒ 答 `api_key` —— `x-api-key` · `X_API_KEY` · `xApiKey`(名表里 `api_key` 只认整名,带前缀的头名 / 参数名由这一条收);
|
|
88
|
+
* · `access token`:`access_token` 在整名表 ⇒ 答 `access_token` —— `github_access_token` · `oauth-access-token`。
|
|
89
|
+
* 闭集,不做通配:别的名表多段词(`refresh_token` · `private_key` …)与裸 `access_key` 作末尾照今天整名表;末段不是尾串的末词不命中(`api_key_count` · `token_count`)。
|
|
90
|
+
*/
|
|
91
|
+
const COMPOUND_TAIL_PHRASES = [
|
|
92
|
+
{ words: ["secret", "key"], strong: "secret" },
|
|
93
|
+
{ words: ["secret", "access", "key"], strong: "secret" },
|
|
94
|
+
{ words: ["api", "key"] },
|
|
95
|
+
{ words: ["access", "token"] },
|
|
96
|
+
];
|
|
97
|
+
/** 这一档是不是「作为末尾算数」(穷尽 `switch`)。 */
|
|
98
|
+
function endsLongerNames(stance) {
|
|
99
|
+
switch (stance) {
|
|
100
|
+
case "tail-word":
|
|
101
|
+
return true;
|
|
102
|
+
case "whole-name-only":
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* 全部尾串 = 名表里态度为 `tail-word` 的词(单词)+ 尾串闭集,答词按 {@link COMPOUND_TAIL_PHRASES} 头注的律解析。词必须是单段 `[a-z0-9]+`
|
|
108
|
+
* (切段切不出分隔符,带分隔符的词永远对不上 ⇒ 加载期抛,不静默成死条目)。
|
|
109
|
+
*/
|
|
110
|
+
const COMPOUND_TAILS = (() => {
|
|
111
|
+
const specs = [];
|
|
112
|
+
for (const w of CREDENTIAL_PARAM_NAMES) {
|
|
113
|
+
if (endsLongerNames(COMPOUND_TAIL_STANCE[w]))
|
|
114
|
+
specs.push({ words: [w] });
|
|
115
|
+
}
|
|
116
|
+
specs.push(...COMPOUND_TAIL_PHRASES);
|
|
117
|
+
return specs.map((spec) => {
|
|
118
|
+
for (const w of spec.words) {
|
|
119
|
+
if (!/^[a-z0-9]+$/.test(w))
|
|
120
|
+
throw new Error(`url-credential: compound-name tail word ${JSON.stringify(w)} must be one lowercase [a-z0-9] segment (names are cut at _ - . and camel boundaries)`);
|
|
121
|
+
}
|
|
122
|
+
const word = tableWordOf(spec.words.join("_")) ?? spec.strong;
|
|
123
|
+
if (word === undefined)
|
|
124
|
+
throw new Error(`url-credential: compound-name tail ${JSON.stringify(spec.words.join("_"))} is not a table word and names no strong word`);
|
|
125
|
+
return { words: spec.words, word };
|
|
126
|
+
});
|
|
127
|
+
})();
|
|
128
|
+
/** 切段只需要末尾这几段(最长的尾串有几个词就留几段)。 */
|
|
129
|
+
const TAIL_SEGMENTS_KEPT = Math.max(...COMPOUND_TAILS.map((t) => t.words.length));
|
|
130
|
+
/**
|
|
131
|
+
* 名字的末尾几段(小写,最多 `keep` 段,末段在最后)。切点:`_` `-` `.`(连续的、首尾的都只是分隔,空段不存在)与驼峰边界(小写字母 / 数字 → 大写字母,
|
|
132
|
+
* 大写串的最后一个大写字母若后面跟小写字母,它起新的一段:`HTTPPassword` → `HTTP` `Password`;`PGPASSWORD` 没有边界,是一段)。只折 ASCII 大小写,线性读,不用正则后顾断言(老浏览器加载期就抛)。
|
|
133
|
+
*/
|
|
134
|
+
function tailSegmentsOf(name, keep) {
|
|
135
|
+
const segs = [];
|
|
136
|
+
let start = 0;
|
|
137
|
+
const cut = (end) => {
|
|
138
|
+
if (end <= start)
|
|
139
|
+
return;
|
|
140
|
+
segs.push(name.slice(start, end));
|
|
141
|
+
if (segs.length > keep)
|
|
142
|
+
segs.shift();
|
|
143
|
+
};
|
|
144
|
+
const isUpper = (c) => c >= 65 && c <= 90;
|
|
145
|
+
const isLower = (c) => c >= 97 && c <= 122;
|
|
146
|
+
const isDigit = (c) => c >= 48 && c <= 57;
|
|
147
|
+
for (let i = 0; i < name.length; i++) {
|
|
148
|
+
const c = name.charCodeAt(i);
|
|
149
|
+
if (c === 95 || c === 45 || c === 46) {
|
|
150
|
+
cut(i);
|
|
151
|
+
start = i + 1;
|
|
152
|
+
}
|
|
153
|
+
else if (i > start && isUpper(c)) {
|
|
154
|
+
const prev = name.charCodeAt(i - 1);
|
|
155
|
+
if (isLower(prev) || isDigit(prev) || (isUpper(prev) && isLower(name.charCodeAt(i + 1)))) {
|
|
156
|
+
cut(i);
|
|
157
|
+
start = i;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
cut(name.length);
|
|
162
|
+
return segs.map(asciiLower);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* 这几段的末尾是不是这条尾串:从右往左逐词吃 —— 一段末尾能吃几个词就吃几个;吃完一段还剩词,这一段必须刚好吃光(切点落在词界上:词不跨分隔符,`app_wd` 不是 `pwd`);
|
|
166
|
+
* 词吃完时,最左那一段允许还剩前缀(`PGPASSWORD` 的 `PG`)。
|
|
167
|
+
*/
|
|
168
|
+
function endsWithTail(words, segs) {
|
|
169
|
+
let w = words.length;
|
|
170
|
+
let s = segs.length;
|
|
171
|
+
while (w > 0) {
|
|
172
|
+
if (s === 0)
|
|
173
|
+
return false;
|
|
174
|
+
let rest = segs[--s];
|
|
175
|
+
while (w > 0 && rest.endsWith(words[w - 1])) {
|
|
176
|
+
rest = rest.slice(0, rest.length - words[w - 1].length);
|
|
177
|
+
w--;
|
|
178
|
+
}
|
|
179
|
+
if (w > 0 && rest !== "")
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* 🔴 **「这个已解码的参数名是不是凭据名」的唯一答者**(18.0.0):是 ⇒ 名表里的那个词(小写);不是 ⇒ `undefined`。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字)。
|
|
186
|
+
* · 整名(ASCII 大小写不敏感)在 {@link CREDENTIAL_PARAM_NAMES} 里 ⇒ 那个词(整名优先);
|
|
187
|
+
* · 否则末段的尾部是强档词(`password` / `passwd` / `pwd` / `secret` / `apikey`),或名字末尾是尾串闭集的一条(`secret_key` / `secret_access_key` ⇒ `secret`;`api_key` ⇒ `api_key`;`access_token` ⇒ `access_token`;答词的律见 {@link COMPOUND_TAIL_PHRASES})⇒ 那个词。
|
|
188
|
+
* 切段、粘连与对齐的律见模块头「名字判定」。
|
|
189
|
+
* 返回的词是**表词**:消费方拿它去查自己挂在名表上的那一列(例:日志脱敏器的锚强度),不对整名再查一遍表。
|
|
190
|
+
*/
|
|
191
|
+
export function credentialParamWordOf(decodedName) {
|
|
192
|
+
const whole = tableWordOf(asciiLower(decodedName));
|
|
193
|
+
if (whole !== undefined)
|
|
194
|
+
return whole;
|
|
195
|
+
const segs = tailSegmentsOf(decodedName, TAIL_SEGMENTS_KEPT);
|
|
196
|
+
for (const tail of COMPOUND_TAILS)
|
|
197
|
+
if (endsWithTail(tail.words, segs))
|
|
198
|
+
return tail.word;
|
|
199
|
+
return undefined;
|
|
200
|
+
}
|
|
47
201
|
/**
|
|
48
|
-
*
|
|
202
|
+
* 「这个**已解码**的参数名是不是凭据名」:{@link credentialParamWordOf} 有词。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字);
|
|
203
|
+
* 整名在 {@link CREDENTIAL_PARAM_NAMES} 里、或末段 / 尾串收在强档词上(`db_password` / `PGPASSWORD` / `SECRET_KEY`)才算。
|
|
49
204
|
*/
|
|
50
205
|
export function isCredentialParamName(decodedName) {
|
|
51
|
-
return
|
|
206
|
+
return credentialParamWordOf(decodedName) !== undefined;
|
|
52
207
|
}
|
|
53
208
|
/** 一段参数串(`?` / `#` 之后)里第一个凭据名参数的表词(小写);没有 ⇒ `undefined`。 */
|
|
54
209
|
function credentialParamIn(params) {
|
|
55
210
|
for (const name of new URLSearchParams(params).keys()) {
|
|
56
|
-
const word =
|
|
57
|
-
if (
|
|
211
|
+
const word = credentialParamWordOf(name);
|
|
212
|
+
if (word !== undefined)
|
|
58
213
|
return word;
|
|
59
214
|
}
|
|
60
215
|
return undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有叶子模块(运行期零依赖,浏览器安全;不进 `package.json` 的 exports,主桶经 `./mcp-request-entry` 转出这里的两个公开名):
|
|
3
|
+
* **申报名能不能当线上名**的唯一判据 —— 一台 MCP server 或一个 A2A 对端的申报名会成为线上工具名的命名空间段(`mcp__<名>__…` / `a2a__<名>__…`)。
|
|
4
|
+
* 住在叶子里,是因为 `./types` 的域 schema(`a2a` 对端名)与请求面的读口(`./mcp-request-entry`)都要它,而后者反过来引 `./types` 的词表 ——
|
|
5
|
+
* 判据只有这一份函数体,两边 import 同一只。
|
|
6
|
+
*/
|
|
7
|
+
/** 引擎 `TOOL_WIRE_NAME_MAX_CHARS` 的镜像:一台 MCP server(或 A2A 对端)的申报名会成为线上工具名的命名空间段,引擎对超界名是**拒**(不截断)。 */
|
|
8
|
+
export declare const TOOL_WIRE_NAME_MAX_CHARS = 128;
|
|
9
|
+
/**
|
|
10
|
+
* 首尾带空白吗 —— 「空白」就是 `String.prototype.trim` 剥的那一组(ECMAScript 的 WhiteSpace + LineTerminator:空格 / 制表 / 换行 / NBSP / BOM / 全角空格 / 行段分隔符等)。
|
|
11
|
+
* {@link isUsableWireName} 与请求面的坏名摘要(`describeBadWireName`)共读这一个定义,判与说不会各按各的口径。
|
|
12
|
+
*/
|
|
13
|
+
export declare const hasEdgeWhitespace: (v: string) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* 这个申报名能用吗。严判:非串、空串、超 {@link TOOL_WIRE_NAME_MAX_CHARS}、**首尾带空白**一律不可用。不截断、不 trim、不规范化 —— 截短或剥掉一个名字就是**换一个身份**,
|
|
16
|
+
* 而被换掉身份的那台 server 上挂的 allowTools / toolFaces 收窄声明会一起对不上号。首尾空白 = 坏名(它不是另一个名字:名字是 `mcp__<server>__<tool>` 的身份片,
|
|
17
|
+
* 带着空白的一端无从与不带的那个对上号);内部空白(`a b`)照旧是合法名,本判官不扩判。
|
|
18
|
+
*/
|
|
19
|
+
export declare function isUsableWireName(v: unknown): v is string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 包内私有叶子模块(运行期零依赖,浏览器安全;不进 `package.json` 的 exports,主桶经 `./mcp-request-entry` 转出这里的两个公开名):
|
|
3
|
+
* **申报名能不能当线上名**的唯一判据 —— 一台 MCP server 或一个 A2A 对端的申报名会成为线上工具名的命名空间段(`mcp__<名>__…` / `a2a__<名>__…`)。
|
|
4
|
+
* 住在叶子里,是因为 `./types` 的域 schema(`a2a` 对端名)与请求面的读口(`./mcp-request-entry`)都要它,而后者反过来引 `./types` 的词表 ——
|
|
5
|
+
* 判据只有这一份函数体,两边 import 同一只。
|
|
6
|
+
*/
|
|
7
|
+
/** 引擎 `TOOL_WIRE_NAME_MAX_CHARS` 的镜像:一台 MCP server(或 A2A 对端)的申报名会成为线上工具名的命名空间段,引擎对超界名是**拒**(不截断)。 */
|
|
8
|
+
export const TOOL_WIRE_NAME_MAX_CHARS = 128;
|
|
9
|
+
/**
|
|
10
|
+
* 首尾带空白吗 —— 「空白」就是 `String.prototype.trim` 剥的那一组(ECMAScript 的 WhiteSpace + LineTerminator:空格 / 制表 / 换行 / NBSP / BOM / 全角空格 / 行段分隔符等)。
|
|
11
|
+
* {@link isUsableWireName} 与请求面的坏名摘要(`describeBadWireName`)共读这一个定义,判与说不会各按各的口径。
|
|
12
|
+
*/
|
|
13
|
+
export const hasEdgeWhitespace = (v) => v.trim() !== v;
|
|
14
|
+
/**
|
|
15
|
+
* 这个申报名能用吗。严判:非串、空串、超 {@link TOOL_WIRE_NAME_MAX_CHARS}、**首尾带空白**一律不可用。不截断、不 trim、不规范化 —— 截短或剥掉一个名字就是**换一个身份**,
|
|
16
|
+
* 而被换掉身份的那台 server 上挂的 allowTools / toolFaces 收窄声明会一起对不上号。首尾空白 = 坏名(它不是另一个名字:名字是 `mcp__<server>__<tool>` 的身份片,
|
|
17
|
+
* 带着空白的一端无从与不带的那个对上号);内部空白(`a b`)照旧是合法名,本判官不扩判。
|
|
18
|
+
*/
|
|
19
|
+
export function isUsableWireName(v) {
|
|
20
|
+
return typeof v === "string" && v.length > 0 && v.length <= TOOL_WIRE_NAME_MAX_CHARS && !hasEdgeWhitespace(v);
|
|
21
|
+
}
|