@sema-agent/settings-schema 14.4.0 → 14.5.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/dist/index.js CHANGED
@@ -5,6 +5,7 @@
5
5
  export * from "./types.js";
6
6
  export * from "./execution-lanes.js"; // 7.0.0 M-R2: the ONE lane / store vocabulary (EXECUTION_LANE_WORDS / EXECUTION_LANES / STORE_BACKENDS / SESSION_BACKENDS / MEMORY_ENGINE_BACKENDS) — pure data, browser-safe
7
7
  export * from "./url-shape.js"; // 11.0.0: THE URL shape judge (httpUrlFlaws / httpUrlRule / isHierarchicalUrl), pure/browser-safe
8
+ export * from "./url-credential.js"; // 14.5.0: THE "does this URL carry a credential" judge (findUrlCredential / findUrlUserinfo / URL_CREDENTIAL_REASONS / CREDENTIAL_PARAM_NAMES …), pure/browser-safe
8
9
  export * from "./default-model.js"; // 11.0.0: THE catalog-default resolver (roles.default → active tier ladder → fallback), pure/browser-safe
9
10
  export * from "./loopback.js"; // 11.0.0: the two「本机」predicates (auth relaxation — narrow / connection routing — wide), pure/browser-safe
10
11
  export * from "./hash.js";
package/dist/node.d.ts CHANGED
@@ -9,5 +9,6 @@ export * from "./file-edit.js";
9
9
  export * from "./bundle.js";
10
10
  export * from "./local-load.js";
11
11
  export * from "./scheduler-store-node.js";
12
- export { migrateConfigDirectory, type ConfigDirectoryDocument, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type MigrateConfigDirectoryOptions } from "./config-dir-migration.js";
12
+ export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
13
+ type ConfigDirectoryDocument, type ConfigDirectoryIo, type ConfigDirectoryMigration, type ConfigDirectoryRefusal, type ConfigDocumentsMigration, type MigrateConfigDirectoryOptions, type MigrateConfigDocumentsOptions, } from "./config-dir-migration.js";
13
14
  export { nodeConfigDirectoryIo } from "./config-dir-io.js";
package/dist/node.js CHANGED
@@ -10,5 +10,6 @@ export * from "./bundle.js";
10
10
  export * from "./local-load.js";
11
11
  export * from "./scheduler-store-node.js"; // R7 self-wake scheduler store — fs binding of the ./scheduler-store contract
12
12
  // 目录形的一次性迁移(读目录 → 迁 → 整域终判 → 原子写 → 删 retired 域文件)—— 语义只此一处,命令行是它的渲染器、引擎起服调同一只。显式列名:该模块的包内私有件(出门形/拼路径)不外泄。
13
- export { migrateConfigDirectory } from "./config-dir-migration.js";
13
+ export { migrateConfigDirectory, migrateConfigDocuments, // 14.5.0:同一段「迁 → 整域终判」的同步口(入参是已有的存量文档,无 io)—— 目录形调的就是它的内核
14
+ } from "./config-dir-migration.js";
14
15
  export { nodeConfigDirectoryIo } from "./config-dir-io.js"; // 上面那只函数的真 I/O 绑定:node:fs + 本包唯一的原子写器(config-document 模式规则)
@@ -33,6 +33,7 @@ import { outgoingPath, redactSecrets } from "./config-fns.js";
33
33
  import { LANE_ENV_OWNER, STORE_ENV_KEYS, isLaneEnvKey } from "./execution-lane-index.js";
34
34
  import { EXECUTION_LANE_WORDS, MEMORY_ENGINE_BACKENDS, STORE_BACKENDS } from "./execution-lanes.js";
35
35
  import { INTEGER } from "./integer.js";
36
+ import { isPlainJsonObject } from "./plain-json-object.js";
36
37
  import { RUNTIME_LIFT_TARGETS } from "./runtime-lift-targets.js";
37
38
  import { RETIRED_ROLE_WORDS } from "./retired-role-words.js";
38
39
  import { REMOTE_EXEC_DOCUMENT, RemoteExecSpec } from "./remote-exec.js";
@@ -197,7 +198,7 @@ export const ONE_TIME_MIGRATIONS = [
197
198
  * 重跑恒等:第二次跑零改动,只剩第一次就 `refused` 的那几张收据(那些坐标原样留着,读侧照旧拒)。
198
199
  */
199
200
  export function migrateStoredDocuments(docs, opts) {
200
- if (!isPlainObject(docs))
201
+ if (!isPlainJsonObject(docs))
201
202
  throw new TypeError("migrateStoredDocuments: `docs` must be a { domain: document } object");
202
203
  switch (opts.coverage) {
203
204
  case "scope":
@@ -229,7 +230,7 @@ export function migrateStoredDocuments(docs, opts) {
229
230
  * 不是对象 ⇒ 原样返回(判形是读者的事)。
230
231
  */
231
232
  export function migrateEffectiveSnapshot(effective) {
232
- if (!isPlainObject(effective))
233
+ if (!isPlainJsonObject(effective))
233
234
  return { effective, receipts: [] };
234
235
  const docs = {};
235
236
  for (const [k, v] of Object.entries(effective))
@@ -251,13 +252,7 @@ export function migrateEffectiveSnapshot(effective) {
251
252
  next[k] = v;
252
253
  return { effective: next, receipts: m.receipts };
253
254
  }
254
- function isPlainObject(v) {
255
- if (v === null || typeof v !== "object" || Array.isArray(v))
256
- return false;
257
- const proto = Object.getPrototypeOf(v);
258
- return proto === Object.prototype || proto === null;
259
- }
260
- const has = (o, k) => isPlainObject(o) && Object.prototype.hasOwnProperty.call(o, k);
255
+ const has = (o, k) => isPlainJsonObject(o) && Object.prototype.hasOwnProperty.call(o, k);
261
256
  const without = (o, k) => {
262
257
  const { [k]: _gone, ...rest } = o;
263
258
  return rest;
@@ -291,12 +286,12 @@ function stepRuntimeDomain(out, coverage, receipts) {
291
286
  if (raw === undefined)
292
287
  return;
293
288
  const refuse = (reason) => void receipts.push(refused("SS-M1", "runtime", [], reason));
294
- if (isPlainObject(raw) && Object.keys(raw).length === 0) {
289
+ if (isPlainJsonObject(raw) && Object.keys(raw).length === 0) {
295
290
  delete out.runtime;
296
291
  receipts.push(receipt("SS-M1", "runtime", [], "dropped-inert"));
297
292
  return;
298
293
  }
299
- if (!isPlainObject(raw))
294
+ if (!isPlainJsonObject(raw))
300
295
  return refuse("the stored runtime document is not a JSON object — the scope does not read today either; fix it by hand");
301
296
  if (coverage === "partial" && !RUNTIME_LIFT_TARGETS.every((target) => has(out, target))) {
302
297
  return refuse("a runtime document migrates only together with its scope's governance and limits documents (coverage \"scope\") — on its own there is no telling whether governance was ever set");
@@ -305,7 +300,7 @@ function stepRuntimeDomain(out, coverage, receipts) {
305
300
  if (!parsed.success)
306
301
  return refuse("the stored runtime document does not read under the 10.x runtime schema — the scope does not read today either; fix it by hand");
307
302
  const limitsRaw = out.limits;
308
- if (limitsRaw !== undefined && !isPlainObject(limitsRaw))
303
+ if (limitsRaw !== undefined && !isPlainJsonObject(limitsRaw))
309
304
  return refuse("the scope's limits document is not a JSON object — the scope does not read today either; fix limits first");
310
305
  const trio = GOVERNANCE_TRIO.filter((k) => has(raw, k) && raw[k] !== undefined);
311
306
  if (trio.length > 0) {
@@ -323,7 +318,7 @@ function stepRuntimeDomain(out, coverage, receipts) {
323
318
  const limitKeys = LIMIT_KEYS.filter((k) => parsed.data[k] !== undefined);
324
319
  if (limitKeys.length > 0) {
325
320
  // 迁移前的读法(消费方):`limits` 有这一键 ⇒ limits 赢;没有 ⇒ 读 runtime 上的值。
326
- const limits = isPlainObject(limitsRaw) ? { ...limitsRaw } : {};
321
+ const limits = isPlainJsonObject(limitsRaw) ? { ...limitsRaw } : {};
327
322
  let added = false;
328
323
  for (const k of limitKeys) {
329
324
  if (Object.prototype.hasOwnProperty.call(limits, k))
@@ -346,13 +341,13 @@ function stepRuntimeDomain(out, coverage, receipts) {
346
341
  // ── SS-M2:hosts 超界 ───────────────────────────────────────────────────────────────────────────────────
347
342
  function stepHostsBounds(out, receipts) {
348
343
  const doc = out.hosts;
349
- if (!isPlainObject(doc) || !Array.isArray(doc.hosts))
344
+ if (!isPlainJsonObject(doc) || !Array.isArray(doc.hosts))
350
345
  return;
351
346
  const rows = doc.hosts;
352
347
  let kept = [];
353
348
  const dropped = [];
354
349
  rows.forEach((row, index) => {
355
- const name = isPlainObject(row) ? row.name : undefined;
350
+ const name = isPlainJsonObject(row) ? row.name : undefined;
356
351
  if (typeof name === "string" && name.length > HOST_NAME_MAX)
357
352
  dropped.push(index);
358
353
  else
@@ -402,8 +397,8 @@ const UNCHANGED = { kind: "unchanged" };
402
397
  function migrateWorkerRow(raw) {
403
398
  const refuse = (field, reason) => ({ kind: "refused", field, reason });
404
399
  const enabled = raw.enabled !== false;
405
- const laneKeysOf = (rec) => (isPlainObject(rec) ? Object.keys(rec).filter(isLaneEnvKey) : []);
406
- const storeKeysOf = (rec) => (isPlainObject(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
400
+ const laneKeysOf = (rec) => (isPlainJsonObject(rec) ? Object.keys(rec).filter(isLaneEnvKey) : []);
401
+ const storeKeysOf = (rec) => (isPlainJsonObject(rec) ? STORE_ENV_KEYS.filter((k) => Object.hasOwn(rec, k)) : []);
407
402
  const envLaneKeys = laneKeysOf(raw.env);
408
403
  const secretLaneKeys = laneKeysOf(raw.secretEnv);
409
404
  const envStoreKeys = storeKeysOf(raw.env);
@@ -432,12 +427,12 @@ function migrateWorkerRow(raw) {
432
427
  const needLane = !hasExecution && (enabled || laneTouched);
433
428
  const needStore = !hasStore && (enabled || storeTouched);
434
429
  const w = structuredClone(raw);
435
- const runtime = isPlainObject(w.runtime) ? w.runtime : {};
430
+ const runtime = isPlainJsonObject(w.runtime) ? w.runtime : {};
436
431
  w.runtime = runtime;
437
- const env = isPlainObject(w.env) ? w.env : {};
438
- const secretEnv = isPlainObject(w.secretEnv) ? w.secretEnv : {};
439
- const coords = isPlainObject(w.coords) ? w.coords : {};
440
- const secretRefs = isPlainObject(w.secretRefs) ? w.secretRefs : {};
432
+ const env = isPlainJsonObject(w.env) ? w.env : {};
433
+ const secretEnv = isPlainJsonObject(w.secretEnv) ? w.secretEnv : {};
434
+ const coords = isPlainJsonObject(w.coords) ? w.coords : {};
435
+ const secretRefs = isPlainJsonObject(w.secretRefs) ? w.secretRefs : {};
441
436
  const dropped = [];
442
437
  const drop = (container, slot, key) => {
443
438
  delete container[key];
@@ -601,9 +596,9 @@ function migrateWorkerRow(raw) {
601
596
  }
602
597
  }
603
598
  // 被搬空的 env / secretEnv 整键去掉(它们是可选字段;留一个 `{}` 只是噪声)
604
- if (isPlainObject(w.env) && Object.keys(env).length === 0 && isPlainObject(raw.env) && Object.keys(raw.env).length > 0)
599
+ if (isPlainJsonObject(w.env) && Object.keys(env).length === 0 && isPlainJsonObject(raw.env) && Object.keys(raw.env).length > 0)
605
600
  delete w.env;
606
- if (isPlainObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && isPlainObject(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
601
+ if (isPlainJsonObject(w.secretEnv) && Object.keys(secretEnv).length === 0 && isPlainJsonObject(raw.secretEnv) && Object.keys(raw.secretEnv).length > 0)
607
602
  delete w.secretEnv;
608
603
  // 改写后的行仍读不进 7.x 形 ⇒ 不写回半成品:拒,原文留盘(读口 `readWorkersRows` 扣下它)。判据与拒句是读口那一只(`workerRowVerdict`)。
609
604
  const verdict = workerRowVerdict(w, "migrated");
@@ -613,12 +608,12 @@ function migrateWorkerRow(raw) {
613
608
  }
614
609
  function stepWorkers6x(out, receipts) {
615
610
  const doc = out.workers;
616
- if (!isPlainObject(doc) || !Array.isArray(doc.workers))
611
+ if (!isPlainJsonObject(doc) || !Array.isArray(doc.workers))
617
612
  return;
618
613
  const rows = doc.workers;
619
614
  let next;
620
615
  rows.forEach((row, index) => {
621
- if (!isPlainObject(row) || typeof row.name !== "string")
616
+ if (!isPlainJsonObject(row) || typeof row.name !== "string")
622
617
  return; // 认不出名字:整域读侧判
623
618
  const r = migrateWorkerRow(row);
624
619
  switch (r.kind) {
@@ -646,7 +641,7 @@ function stepWorkers6x(out, receipts) {
646
641
  // ── SS-M5:模型钥匙二选一 ──────────────────────────────────────────────────────────────────────────────
647
642
  function stepModelKeyOneOf(out, receipts) {
648
643
  const doc = out.models;
649
- if (!isPlainObject(doc) || !Array.isArray(doc.models))
644
+ if (!isPlainJsonObject(doc) || !Array.isArray(doc.models))
650
645
  return;
651
646
  const rows = doc.models;
652
647
  let next;
@@ -682,17 +677,17 @@ function rewriteAtMention(holder, document, at, receipts) {
682
677
  return { ...without(holder, "atModelAllowlist"), atMention };
683
678
  }
684
679
  function stepAtMention(out, receipts) {
685
- if (isPlainObject(out.models)) {
680
+ if (isPlainJsonObject(out.models)) {
686
681
  const next = rewriteAtMention(out.models, "models", [], receipts);
687
682
  if (next !== undefined)
688
683
  out.models = next;
689
684
  }
690
685
  const rosters = out.rosters;
691
- if (isPlainObject(rosters) && Array.isArray(rosters.rosters)) {
686
+ if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
692
687
  const rows = rosters.rosters;
693
688
  let next;
694
689
  rows.forEach((row, index) => {
695
- if (!isPlainObject(row))
690
+ if (!isPlainJsonObject(row))
696
691
  return;
697
692
  const r = rewriteAtMention(row, "rosters", ["rosters", index], receipts);
698
693
  if (r === undefined)
@@ -707,7 +702,7 @@ function stepAtMention(out, receipts) {
707
702
  // ── SS-M8:退役的角色词 ────────────────────────────────────────────────────────────────────────────────
708
703
  /** 一个 `roles` 图上的退役角色词键删掉(值不论 —— 没有角色读它);逐键一张 `dropped-inert` 收据。返回改后的图或 `undefined`(没改 / 不是对象:判形是读者的事)。 */
709
704
  function withoutRetiredRoles(roles, document, at, receipts) {
710
- if (!isPlainObject(roles))
705
+ if (!isPlainJsonObject(roles))
711
706
  return undefined;
712
707
  const present = RETIRED_ROLE_WORDS.filter((w) => Object.prototype.hasOwnProperty.call(roles, w));
713
708
  if (present.length === 0)
@@ -721,17 +716,17 @@ function withoutRetiredRoles(roles, document, at, receipts) {
721
716
  }
722
717
  function stepRetiredRoleWords(out, receipts) {
723
718
  const models = out.models;
724
- if (isPlainObject(models)) {
719
+ if (isPlainJsonObject(models)) {
725
720
  const roles = withoutRetiredRoles(models.roles, "models", [], receipts);
726
721
  if (roles !== undefined)
727
722
  out.models = { ...models, roles };
728
723
  }
729
724
  const rosters = out.rosters;
730
- if (isPlainObject(rosters) && Array.isArray(rosters.rosters)) {
725
+ if (isPlainJsonObject(rosters) && Array.isArray(rosters.rosters)) {
731
726
  const rows = rosters.rosters;
732
727
  let next;
733
728
  rows.forEach((row, index) => {
734
- if (!isPlainObject(row))
729
+ if (!isPlainJsonObject(row))
735
730
  return;
736
731
  const roles = withoutRetiredRoles(row.roles, "rosters", ["rosters", index], receipts);
737
732
  if (roles === undefined)
@@ -749,16 +744,16 @@ function stepRetiredRoleWords(out, receipts) {
749
744
  * 消失);异名 ⇒ 原 `roles.default` 作「被压过」删掉、`default` 搬入。`default` 不是非空串 ⇒ 拒(今天它让整个 models 域读不进,不替运维猜)。 */
750
745
  function stepModelsDefault(out, receipts) {
751
746
  const doc = out.models;
752
- if (!isPlainObject(doc) || !Object.prototype.hasOwnProperty.call(doc, "default"))
747
+ if (!isPlainJsonObject(doc) || !Object.prototype.hasOwnProperty.call(doc, "default"))
753
748
  return;
754
749
  const name = doc.default;
755
750
  if (typeof name !== "string" || name === "") {
756
751
  receipts.push(refused("SS-M9", "models", ["default"], "models.default is not a catalog name — it does not read today either; fix it by hand"));
757
752
  return;
758
753
  }
759
- const roles = isPlainObject(doc.roles) ? doc.roles : {};
754
+ const roles = isPlainJsonObject(doc.roles) ? doc.roles : {};
760
755
  const prior = roles.default;
761
- if (prior !== undefined && !(isPlainObject(prior) && prior.model === name))
756
+ if (prior !== undefined && !(isPlainJsonObject(prior) && prior.model === name))
762
757
  receipts.push(receipt("SS-M9", "models", ["roles", "default"], "dropped-shadowed"));
763
758
  receipts.push(moved("SS-M9", "models", ["default"], { document: "models", path: ["roles", "default"] }));
764
759
  out.models = { ...without(doc, "default"), roles: { ...roles, default: { model: name } } };
@@ -769,7 +764,7 @@ function stepModelsDefault(out, receipts) {
769
764
  * overlay,catalog 的 digest 是引擎铸的,替运维机械转就是第二个编译器 / 第二个铸点。`prompts` 不是对象 ⇒ 不动(判形是读者的事)。 */
770
765
  function stepPromptsAxes(out, receipts) {
771
766
  const doc = out.prompts;
772
- if (!isPlainObject(doc))
767
+ if (!isPlainJsonObject(doc))
773
768
  return;
774
769
  let next = doc;
775
770
  if (has(doc, "bindings") && doc.bindings !== undefined) {
@@ -820,7 +815,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
820
815
  return value.slice(0, hash);
821
816
  }
822
817
  if (s instanceof z.ZodDiscriminatedUnion) {
823
- if (!isPlainObject(value))
818
+ if (!isPlainJsonObject(value))
824
819
  return value;
825
820
  const disc = s._zod.def.discriminator;
826
821
  for (const option of s.options) {
@@ -842,7 +837,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
842
837
  return value;
843
838
  }
844
839
  if (s instanceof z.ZodObject) {
845
- if (!isPlainObject(value))
840
+ if (!isPlainJsonObject(value))
846
841
  return value;
847
842
  let next;
848
843
  for (const [k, fieldSchema] of Object.entries(s.shape)) {
@@ -866,7 +861,7 @@ function stripFragments(schema, value, path, onStrip, depth = 0) {
866
861
  return next ?? value;
867
862
  }
868
863
  if (s instanceof z.ZodRecord) {
869
- if (!isPlainObject(value))
864
+ if (!isPlainJsonObject(value))
870
865
  return value;
871
866
  let next;
872
867
  for (const [k, el] of Object.entries(value)) {
@@ -0,0 +1,6 @@
1
+ /**
2
+ * 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的谓词(三处读域 / 迁移 / config.d 的同一只;`hooks-config.ts` 的 `hookIsRecord`、`types.ts` 内联一份、`conformance/` 两只更弱的 `isPlainObject` 是既有副本,另票收)** —— 不是 `null`、不是数组、原型是 `Object.prototype` 或 `null`(JSON 解析出来的对象恒如此;
3
+ * `Date` / `Map` / 类实例 / 建在别的对象原型上的记录都不是)。读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判(`./one-time-migrations`)、
4
+ * `config.d` 文档「是不是 JSON 对象」的判(`./config-d-file`)读它,不各写一份。零 import。
5
+ */
6
+ export declare function isPlainJsonObject(v: unknown): v is Record<string, unknown>;
@@ -0,0 +1,11 @@
1
+ /**
2
+ * 包内私有(不进主桶、不进 exports 表):**「纯 JSON 对象」的谓词(三处读域 / 迁移 / config.d 的同一只;`hooks-config.ts` 的 `hookIsRecord`、`types.ts` 内联一份、`conformance/` 两只更弱的 `isPlainObject` 是既有副本,另票收)** —— 不是 `null`、不是数组、原型是 `Object.prototype` 或 `null`(JSON 解析出来的对象恒如此;
3
+ * `Date` / `Map` / 类实例 / 建在别的对象原型上的记录都不是)。读域文档的键集对比(`./config-fns`)、一次性迁移的文档形判(`./one-time-migrations`)、
4
+ * `config.d` 文档「是不是 JSON 对象」的判(`./config-d-file`)读它,不各写一份。零 import。
5
+ */
6
+ export function isPlainJsonObject(v) {
7
+ if (v === null || typeof v !== "object" || Array.isArray(v))
8
+ return false;
9
+ const proto = Object.getPrototypeOf(v);
10
+ return proto === Object.prototype || proto === null;
11
+ }
package/dist/types.d.ts CHANGED
@@ -57,6 +57,10 @@ export declare function urlQueryFragmentHasSecret(u: URL): boolean;
57
57
  * echoed; a fragment's also names its migration, {@link nonSecretUrlIssue}); `params` carries the sentence (`template` / `operands`,
58
58
  * no operands) and `flaw`, the rule word.
59
59
  *
60
+ * Scope of the secret test: it judges the VALUE's shape (a value that looks like a secret, whatever the parameter is called) — the sibling
61
+ * name-shaped judge `findUrlCredential` (`./url-credential`) asks the other question (a parameter whose NAME is on the credential-name table,
62
+ * whatever the value looks like). Neither contains the other: `?key=abc` passes here and is a hit there; `?x=sk-live-…` is the reverse.
63
+ *
60
64
  * This is the seat of every URL in a STORED document the one-time migration rewrites: the domain documents (models `baseUrl`, mcp `url`,
61
65
  * `sessionMirror.engineUrl`, workers `gitApiBaseUrl`) and `config.d/remote-exec.json` (k8s `apiUrl`, `s3Snapshot.endpoint`). A URL
62
66
  * that is not stored data — the web-admin credential envelope's `issuer`, a wire input — uses {@link NON_SECRET_URL_NO_MIGRATION}.
package/dist/types.js CHANGED
@@ -193,6 +193,10 @@ function createNonSecretUrl(reachedByMigration) {
193
193
  * echoed; a fragment's also names its migration, {@link nonSecretUrlIssue}); `params` carries the sentence (`template` / `operands`,
194
194
  * no operands) and `flaw`, the rule word.
195
195
  *
196
+ * Scope of the secret test: it judges the VALUE's shape (a value that looks like a secret, whatever the parameter is called) — the sibling
197
+ * name-shaped judge `findUrlCredential` (`./url-credential`) asks the other question (a parameter whose NAME is on the credential-name table,
198
+ * whatever the value looks like). Neither contains the other: `?key=abc` passes here and is a hit there; `?x=sk-live-…` is the reverse.
199
+ *
196
200
  * This is the seat of every URL in a STORED document the one-time migration rewrites: the domain documents (models `baseUrl`, mcp `url`,
197
201
  * `sessionMirror.engineUrl`, workers `gitApiBaseUrl`) and `config.d/remote-exec.json` (k8s `apiUrl`, `s3Snapshot.endpoint`). A URL
198
202
  * that is not stored data — the web-admin credential envelope's `issuer`, a wire input — uses {@link NON_SECRET_URL_NO_MIGRATION}.
@@ -0,0 +1,41 @@
1
+ /** 拒因词(闭集;判序 = 本数组序;词只在这一处写 —— {@link UrlCredentialReason} 与 {@link UrlCredential} 的成员都从它派生)。 */
2
+ export declare const URL_CREDENTIAL_REASONS: readonly ["url_userinfo", "url_credential_param"];
3
+ export type UrlCredentialReason = (typeof URL_CREDENTIAL_REASONS)[number];
4
+ /**
5
+ * 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)在这张表上的参数就是凭据。
6
+ * 加一个词 = 表里加一格(minor);判官与 {@link isCredentialParamName} 现算,不手抄第二份。
7
+ */
8
+ 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
+ export type CredentialParamName = (typeof CREDENTIAL_PARAM_NAMES)[number];
10
+ /**
11
+ * 「这个**已解码**的参数名是不是凭据名」。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字):整名在 {@link CREDENTIAL_PARAM_NAMES} 里(ASCII 大小写不敏感)才算。
12
+ */
13
+ export declare function isCredentialParamName(decodedName: string): boolean;
14
+ /** 每个拒因词带的细节(键集必须覆盖 {@link URL_CREDENTIAL_REASONS} 的每个词:数组里加一个词而这里没跟 ⇒ 下面的映射型编译红,{@link urlCredentialRule} 的穷举 switch 也红)。 */
15
+ interface UrlCredentialDetail {
16
+ readonly url_userinfo: unknown;
17
+ readonly url_credential_param: {
18
+ readonly at: "query" | "fragment";
19
+ readonly name: CredentialParamName;
20
+ };
21
+ }
22
+ /** 一处命中:`url_userinfo`,或 `url_credential_param` + 它在哪一段、表里的哪个词(小写;不是调用方的原字节)。成员从 {@link URL_CREDENTIAL_REASONS} 派生。 */
23
+ export type UrlCredential = {
24
+ readonly [R in UrlCredentialReason]: {
25
+ readonly reason: R;
26
+ } & UrlCredentialDetail[R];
27
+ }[UrlCredentialReason];
28
+ /**
29
+ * 判:这个值作为 URL 读出来(WHATWG),带不带 **userinfo**(判官的第一问,也是 {@link findUrlCredential} 的第一步)。只问这一件的座位 —— 运行时本就拒带 userinfo 的 URL、
30
+ * 而凭据名参数不由它拒的载体 —— 问这一只,不在座位里另写一份 userinfo 谓词。纯函数。
31
+ */
32
+ export declare function findUrlUserinfo(raw: string): Extract<UrlCredential, {
33
+ readonly reason: "url_userinfo";
34
+ }> | undefined;
35
+ /**
36
+ * 判:这个值作为 URL 读出来(WHATWG),带不带凭据。首个命中(userinfo → query → fragment);没有(含不透明 / 解析不了的值)⇒ `undefined`。纯函数。
37
+ */
38
+ export declare function findUrlCredential(raw: string): UrlCredential | undefined;
39
+ /** 一处命中的规则句(穷举;新拒因词不补 = 编译红)。只说规则与表词,不含值。 */
40
+ export declare function urlCredentialRule(c: UrlCredential): string;
41
+ export {};
@@ -0,0 +1,95 @@
1
+ /**
2
+ * **「这个 URL 的 userinfo 或 query / fragment 里,有没有名字在凭据名表上的参数」的名形判官**(14.5.0;lift 自 server `src/url-credential.ts`,规则、拒因词闭集与规则句逐字搬)。纯函数,零 import(除同包的 `./url-shape`),浏览器安全。
3
+ *
4
+ * ## 与 `NON_SECRET_URL`(`./types`)的关系:两个问题,互补,不是一个包含另一个
5
+ * 本判官按**名**判 —— 参数叫什么(名字整名在 {@link CREDENTIAL_PARAM_NAMES} 上就是凭据,不论值长什么样);`NON_SECRET_URL` 的 `urlQueryFragmentHasSecret` 按**值形**判密钥 —— 值长得像密钥
6
+ * (不论参数叫什么)。所以 `?key=abc` 本判官命中而 `NON_SECRET_URL` 放行(值不像密钥),`?x=sk-live-…` 反之;userinfo 两边都判。存量域文档位(`mcp` 的 `transport.url` 等)读 `NON_SECRET_URL`,
7
+ * 请求面等座位读本判官。把两个问题合成一只定义是另一件事,这里不做。
8
+ *
9
+ * ## 规则(一条):URL 里的凭据 = 日志脱敏器已经在用的那一个定义
10
+ * 一个 URL 里凭据住三个位置:
11
+ * · **userinfo**(`scheme://<这一段>@host`,只有用户名也算)—— 判由本包的 URL 形判官 {@link httpUrlFlaws}(`["userinfo"]`),本模块不留第二份谓词;
12
+ * · **query 参数**、**fragment 参数**(同一段文法)里名字整名在 {@link CREDENTIAL_PARAM_NAMES} 上的那一个 —— 名字按收方的读法读:WHATWG `URLSearchParams` 解一次码,
13
+ * ASCII 大小写不敏感(不做 Unicode 折叠),整名在表里才算(`db_password` / `tokens` / `keyword` 是另一个名字)。
14
+ * 答首个命中(判序 = 上面的序),拒因闭集两词 {@link URL_CREDENTIAL_REASONS};**值从不进答案**,参数名只回显表里的那个词(小写)。
15
+ * 不透明形 / 解析不了的值没有可读的部件 ⇒ 答「没有」—— 这一形收不收是**座位**自己的律,不在本判官里。
16
+ *
17
+ * ## 判官只判「有没有」;座位决定怎么用
18
+ * 本模块不决定哪个载体拒、哪个载体放:同一个键在不同载体上的律可以不同(例:新提交的请求面拒,续跑重放存量体的腿不拒 —— 拒会砖死升级前停驻的任务;
19
+ * 部署 env 与续跑重放只因 userinfo 拒,凭据名参数只在新提交门拒)。所以判官**不**接进任何一份读已落盘 / 重放字节的 schema 或读器 —— 那会把载体律焊死成
20
+ * 读器律;座位在自己的新提交门调 {@link findUrlCredential} / {@link findUrlUserinfo},拒句 / 拒体的形(HTTP 码、字段坐标、出路句)是座位自己的铸点。
21
+ *
22
+ * ## 名表的来源
23
+ * {@link CREDENTIAL_PARAM_NAMES} 是 server 日志脱敏器名表(锚强度表的键)的 23 词。脱敏器另有一列「自由文本里这个词的锚强度」,那一列是脱敏器的事,不在这里;
24
+ * 词表本身在这里成为契约面 —— 脱敏器与本判官读同一张词表(同一个定义)靠消费方把它的表键钉在本导出上(例:`Record<CredentialParamName, …>` 的穷尽映射 ⇒ 加 / 删词不同步即编译红)。
25
+ */
26
+ import { httpUrlFlaws, httpUrlRule, isHierarchicalUrl } from "./url-shape.js";
27
+ /** 拒因词(闭集;判序 = 本数组序;词只在这一处写 —— {@link UrlCredentialReason} 与 {@link UrlCredential} 的成员都从它派生)。 */
28
+ export const URL_CREDENTIAL_REASONS = Object.freeze(["url_userinfo", "url_credential_param"]);
29
+ /**
30
+ * 凭据参数名表(闭集,23 词,全部是小写 ASCII `[a-z][a-z0-9_-]*`)。query / fragment 里名字整名(ASCII 大小写不敏感)在这张表上的参数就是凭据。
31
+ * 加一个词 = 表里加一格(minor);判官与 {@link isCredentialParamName} 现算,不手抄第二份。
32
+ */
33
+ export const CREDENTIAL_PARAM_NAMES = Object.freeze([
34
+ "api_key", "apikey", "api-key", "key",
35
+ "token", "access_token", "refresh_token", "id_token",
36
+ "auth", "authorization", "proxy-authorization",
37
+ "password", "passwd", "pwd",
38
+ "secret", "client_secret", "private_key",
39
+ "signature", "sig",
40
+ "x-amz-signature", "x-amz-security-token", "x-amz-credential",
41
+ "sas",
42
+ ]);
43
+ /** 名表的查找形(`Set`,不拿普通对象当查表:`constructor` / `__proto__` 会读到原型链上的东西)。 */
44
+ const CREDENTIAL_PARAM_NAME_SET = new Set(CREDENTIAL_PARAM_NAMES);
45
+ /** ASCII 大小写折叠(只折 `A-Z`,不做 Unicode 折叠:`TOKEN` 不是 `token`)。 */
46
+ const asciiLower = (s) => s.replace(/[A-Z]/g, (c) => c.toLowerCase());
47
+ /**
48
+ * 「这个**已解码**的参数名是不是凭据名」。入参是收方读到的那一形(`URLSearchParams` 解过一次码的名字):整名在 {@link CREDENTIAL_PARAM_NAMES} 里(ASCII 大小写不敏感)才算。
49
+ */
50
+ export function isCredentialParamName(decodedName) {
51
+ return CREDENTIAL_PARAM_NAME_SET.has(asciiLower(decodedName));
52
+ }
53
+ /** 一段参数串(`?` / `#` 之后)里第一个凭据名参数的表词(小写);没有 ⇒ `undefined`。 */
54
+ function credentialParamIn(params) {
55
+ for (const name of new URLSearchParams(params).keys()) {
56
+ const word = asciiLower(name);
57
+ if (CREDENTIAL_PARAM_NAME_SET.has(word))
58
+ return word;
59
+ }
60
+ return undefined;
61
+ }
62
+ /**
63
+ * 判:这个值作为 URL 读出来(WHATWG),带不带 **userinfo**(判官的第一问,也是 {@link findUrlCredential} 的第一步)。只问这一件的座位 —— 运行时本就拒带 userinfo 的 URL、
64
+ * 而凭据名参数不由它拒的载体 —— 问这一只,不在座位里另写一份 userinfo 谓词。纯函数。
65
+ */
66
+ export function findUrlUserinfo(raw) {
67
+ return isHierarchicalUrl(raw) && httpUrlFlaws(raw, ["userinfo"]).includes("userinfo") ? { reason: "url_userinfo" } : undefined;
68
+ }
69
+ /**
70
+ * 判:这个值作为 URL 读出来(WHATWG),带不带凭据。首个命中(userinfo → query → fragment);没有(含不透明 / 解析不了的值)⇒ `undefined`。纯函数。
71
+ */
72
+ export function findUrlCredential(raw) {
73
+ if (!isHierarchicalUrl(raw))
74
+ return undefined;
75
+ const userinfo = findUrlUserinfo(raw);
76
+ if (userinfo !== undefined)
77
+ return userinfo;
78
+ const u = new URL(raw);
79
+ const inQuery = credentialParamIn(u.search);
80
+ if (inQuery !== undefined)
81
+ return { reason: "url_credential_param", at: "query", name: inQuery };
82
+ const inFragment = credentialParamIn(u.hash.slice(1));
83
+ if (inFragment !== undefined)
84
+ return { reason: "url_credential_param", at: "fragment", name: inFragment };
85
+ return undefined;
86
+ }
87
+ /** 一处命中的规则句(穷举;新拒因词不补 = 编译红)。只说规则与表词,不含值。 */
88
+ export function urlCredentialRule(c) {
89
+ switch (c.reason) {
90
+ case "url_userinfo":
91
+ return httpUrlRule("userinfo"); // 与形判官诸座同一句(不写第二句)
92
+ case "url_credential_param":
93
+ return `must not carry a credential parameter (the ${c.at} names "${c.name}", a name the service's log redactor treats as a credential)`;
94
+ }
95
+ }
@@ -4,9 +4,9 @@
4
4
  > 每次全量重算并逐名比对 —— 加 / 删 / 改形一个导出而不重生成本档 ⇒ 红。导出面的每次变动因此以本档的 diff 出现在评审里。
5
5
  > 形:`value` = 运行期拿得到的值;`type` = 只在类型层;`value+type` = 同名的值与类型(zod schema 与它推出的类型)。
6
6
 
7
- 子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1339 个(同名在多条子路径上各计一次)。
7
+ 子路径 24 条(资产出口 `./package.json` / `./fleet-paths.json` 不是模块,不列),导出名合计 1351 个(同名在多条子路径上各计一次)。
8
8
 
9
- ## `.`(662)
9
+ ## `.`(671)
10
10
 
11
11
  - `ABSOLUTE_HOST_PATH_MAX` — value
12
12
  - `ACCESS_TOKEN_TTL_SECONDS` — value
@@ -36,6 +36,7 @@
36
36
  - `COMMAND_DECISIONS` — value
37
37
  - `CONFIG_CHANGE_SOURCES` — value
38
38
  - `CONTROL_AND_BIDI_CHARS` — value
39
+ - `CREDENTIAL_PARAM_NAMES` — value
39
40
  - `CROSS_SESSION_INBOUND_UNSET` — value
40
41
  - `CROSS_SESSION_INBOUND_VALUES` — value
41
42
  - `CancelSource` — value+type
@@ -55,6 +56,7 @@
55
56
  - `ConfigChangeHookInput` — value
56
57
  - `ConfigPath` — type
57
58
  - `ConfigReader` — type
59
+ - `CredentialParamName` — type
58
60
  - `CrossSessionInboundSetting` — value
59
61
  - `CrossSessionInboundSettingValue` — type
60
62
  - `CrossSessionInboundValue` — type
@@ -479,10 +481,13 @@
479
481
  - `TokenScopeRefusal` — type
480
482
  - `UI_LANGUAGE_SETTING` — value
481
483
  - `UI_LANGUAGE_VALUES` — value
484
+ - `URL_CREDENTIAL_REASONS` — value
482
485
  - `USER_ROLES` — value
483
486
  - `UiLanguageSetting` — value
484
487
  - `UiLanguageSettings` — value+type
485
488
  - `UiLanguageValue` — type
489
+ - `UrlCredential` — type
490
+ - `UrlCredentialReason` — type
486
491
  - `UsageEntry` — value+type
487
492
  - `UsageReport` — value+type
488
493
  - `User` — type
@@ -548,6 +553,8 @@
548
553
  - `emptyEffective` — value
549
554
  - `extraBodyUnsafe` — value
550
555
  - `findSkillContent` — value
556
+ - `findUrlCredential` — value
557
+ - `findUrlUserinfo` — value
551
558
  - `fnv1a64` — value
552
559
  - `formatConfigPath` — value
553
560
  - `hasControlOrBidi` — value
@@ -563,6 +570,7 @@
563
570
  - `isAtLeast` — value
564
571
  - `isCanonicalGeneration` — value
565
572
  - `isContentOriginWord` — value
573
+ - `isCredentialParamName` — value
566
574
  - `isDisclosableIdentity` — value
567
575
  - `isDomainName` — value
568
576
  - `isHierarchicalUrl` — value
@@ -663,6 +671,7 @@
663
671
  - `toVerifiedIdentity` — value
664
672
  - `trustRank` — value
665
673
  - `urlAllowPatternMatches` — value
674
+ - `urlCredentialRule` — value
666
675
  - `urlDenyPatternMatches` — value
667
676
  - `urlQueryFragmentHasSecret` — value
668
677
  - `verifyScopedToken` — value
@@ -1060,17 +1069,19 @@
1060
1069
  - `resolveMemberAdmission` — value
1061
1070
  - `trustRank` — value
1062
1071
 
1063
- ## `./node`(36)
1072
+ ## `./node`(39)
1064
1073
 
1065
1074
  - `ConfigBundle` — type
1066
1075
  - `ConfigDirectoryDocument` — type
1067
1076
  - `ConfigDirectoryIo` — type
1068
1077
  - `ConfigDirectoryMigration` — type
1069
1078
  - `ConfigDirectoryRefusal` — type
1079
+ - `ConfigDocumentsMigration` — type
1070
1080
  - `DomainReadError` — type
1071
1081
  - `FileConfigStore` — value+type
1072
1082
  - `LocalConfig` — type
1073
1083
  - `MigrateConfigDirectoryOptions` — type
1084
+ - `MigrateConfigDocumentsOptions` — type
1074
1085
  - `NON_DOMAIN_CONFIG_FILES` — value
1075
1086
  - `NON_PORTABLE_DOMAIN_REASONS` — value
1076
1087
  - `PORTABLE_DOMAINS` — value
@@ -1088,6 +1099,7 @@
1088
1099
  - `loadRemoteExec` — value
1089
1100
  - `loadSchedulerStore` — value
1090
1101
  - `migrateConfigDirectory` — value
1102
+ - `migrateConfigDocuments` — value
1091
1103
  - `mutateSchedulerStore` — value
1092
1104
  - `nodeConfigDirectoryIo` — value
1093
1105
  - `readBundle` — value
@@ -234,6 +234,8 @@ server 直接拒启,不是静默降级。上一代 `SEMA_REGISTRY_*` 八名(`URL
234
234
  - **本地 `config.d` 的一次性迁移(引擎起服)**:调 `@sema-agent/settings-schema/node` 的 `migrateConfigDirectory(dir, { documents, io: nodeConfigDirectoryIo })`,`documents` 填你本地读腿会读的域名集
235
235
  (覆盖面外的文件不读不判不写不删;要迁 retired 的 `runtime` 域就得把 `governance` 与 `limits` 一并放进来 —— 迁出的旧值要写那两份,缺了 `runtime` 按 `partial` 拒迁、一字不写);按返回的 `kind` 穷举处置(`applied` / `noop` / `pending` / `refused` / `no-documents` / `unreadable`)。**别按同语义自写**读目录、迁、整域终判、原子写、删退役域文件那一串 ——
236
236
  命令行 `migrate` 与这只函数同一份实现;写盘用的就是本包那一只原子写器(契约文档新建 0o644、已有的沿用原模式、符号链接保留)。
237
+ 手里已有存量文档原文、不经目录的消费方(例:命令行壳按层读 `settings` 里的 models 域,要的是「引擎起服那一次迁移之后这份文档整域读不读得进」)调同一个内核的同步口 `migrateConfigDocuments(docs, { documents })`(14.5.0,`./node`):
238
+ 同一个闭集的无 io 形(`refused` ⇒ 引擎同样不写回、整域落缺省;**反向不成立** —— 引擎对整个目录一判,别的文档被拒也会让整目录不写,单层单域的 `noop` / `migrated` 不保证引擎那一次放行;`docs` 是壳的写手要的迁移后那一份)。一层一次调用;**别自己把 `migrateStoredDocuments` 与 `parseDomain` 按这个顺序再组装一遍**。
237
239
  - **会话网页链接**:要给用户一个 web-admin 里的会话链接,调 `webAdminSessionUrl(webBaseUrl, { worker, sessionId })`(`webBaseUrl` 取引擎 `/health` 自报的人用界面根 URL,
238
240
  `worker` 取引擎向 web-admin 登记用的 worker 名),得到 `<webBaseUrl>/chat?agent=<worker>&session=<sessionId>`;**别按地址形手拼** —— 路由属于 web-admin,模板与读回的
239
241
  `parseWebAdminSessionUrl(link)` 同在本包,路径与参数名只此一份。两位必填:只带 `session` 的链接会落到页面自己挑的 worker 上、可能找不到会话。
package/llms-full.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  llms-full.txt — 自包含全文 · 消费方(使用者)视角 · @sema-agent/settings-schema
3
- 重写日期: 2026-10-21 | 包版本: 14.4.0
3
+ 重写日期: 2026-10-23 | 包版本: 14.5.0
4
4
  生成方式: 无生成脚本——按当前树 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版。
5
5
  证据约定: 每条"它怎么工作"的断言附 (文件: 符号) 坐标,刻意不用行号(行号逐版腐烂;符号名由
6
6
  typecheck 与 test/package-surface.test.ts、test/docs-hygiene.test.ts 等常驻门钉住)。无证据推断显式标【推断】。
@@ -496,6 +496,18 @@ entitlement/execution/limits = `none`;`projects` = `salvage`,整域档位仍 `"t
496
496
  语义 = 命令行 `migrate` 的那一条(读目录 → 迁 → 整域终判 → 先写后删 → 原子写),命令行是它的渲染器;`documents` = 调用方读的域名集(覆盖面外不读不判不写不删;拼错的名字抛 `TypeError`;
497
497
  迁 retired 的 `runtime` 要把 `governance` 与 `limits` 一并放进覆盖面,否则 `runtime` 按 `partial` 拒迁、一字不写;迁移新建覆盖面外的文档 ⇒ 抛);
498
498
  `io: ConfigDirectoryIo`(`readDir` / `readFile` / `writeFileAtomic` / `removeFile`),真绑定 `nodeConfigDirectoryIo`(本包唯一的原子写器,`config-document` 模式规则;域文件是符号链接则写到真文件);写盘阶段的 I/O 错误原样抛出。
499
+ - 同步单口(14.5.0,`./node`):`migrateConfigDocuments(docs, { documents })` → `ConfigDocumentsMigration`(闭集 `kind`:`unreadable`〔覆盖面内某份文档不是 JSON 对象 ⇒ 不抛;`document` + `reason`〕· `no-documents` ·
500
+ `refused`〔同目录形的 `refused[]` + `why`,另带 `docs` = 迁移后的那一份〕· `noop`〔`docs` 每份都是入参的同一引用;没改的文档不终判〕· `migrated`〔`docs` 迁移后的覆盖面内文档全集 + `applied` + `changed` + `removed`〕)。
501
+ 与目录形同一个内核(读目录之后的「迁 → 整域终判」只此一处),`documents` 的律(覆盖面外不读不判不进结果、拼错的名字抛 `TypeError`、`runtime` 要把 `governance` / `limits` 放进覆盖面)逐字同;入参不被改,重跑 `docs` ⇒ `noop`;一层一次调用(层序 / 覆盖 / 托管锁的合并读法不在这里)。
502
+
503
+ --- 4.11b URL 内嵌凭据判官(主 barrel,14.5.0;src/url-credential.ts) ---
504
+ - `findUrlCredential(raw)`:URL 里的凭据 = 日志脱敏器已经在用的定义 —— userinfo(只有用户名也算;判由 `httpUrlFlaws(raw, ["userinfo"])`),或 query / fragment 里名字整名在 `CREDENTIAL_PARAM_NAMES`(23 词,小写 ASCII)上的参数
505
+ (名字按收方的读法读:`URLSearchParams` 解一次码,ASCII 大小写不敏感,不做 Unicode 折叠;复合名如 `db_password` 今天不是凭据名)。答首个命中(userinfo → query → fragment):`{ reason: "url_userinfo" }` 或
506
+ `{ reason: "url_credential_param", at: "query" | "fragment", name }`(`name` = 表里的词,小写);值从不进答案;不透明形 / 解析不了 / 非层级式 ⇒ `undefined`(这一形收不收是座位自己的律)。`findUrlUserinfo` = 判官的第一问;
507
+ `isCredentialParamName(decodedName)` = 名字判定;`urlCredentialRule(hit)` = 规则句(userinfo 与 `httpUrlRule("userinfo")` 同一句;参数句点名段与表词);`URL_CREDENTIAL_REASONS` = 拒因词闭集 `url_userinfo` / `url_credential_param`。
508
+ - 与 `NON_SECRET_URL` 是两个问题、互补(不是一个包含另一个):本判官按参数**名**判,`NON_SECRET_URL` 按**值形**判密钥(`?key=abc` 它放行、本判官命中;`?x=sk-live-…` 反之);存量域文档位读后者。
509
+ - 判官只判有没有,载体律留在座位:同一个键在新提交门拒、在续跑重放存量体的腿不拒、部署 env 只因 userinfo 拒 —— 所以它没有接进任何读已落盘 / 重放字节的 schema 或读器;拒体的形(HTTP 码、字段坐标、出路句)是座位自己的铸点。
510
+ 凭据名表另有一份在服务端日志脱敏器里(带自由文本锚强度一列):消费方把它的表键钉在 `CREDENTIAL_PARAM_NAMES` 上(`Record<CredentialParamName, …>` 穷尽映射),加 / 删词不同步即编译红。
499
511
 
500
512
  --- 4.12 迁移工具(主 barrel) ---
501
513
  - 0.5 → 0.7 的迁移工具(`./migrate` 出口:hosts grandfathering、worker.model 归并计划 / 执行 / diff 门、teams → collab)
package/llms.txt CHANGED
@@ -1,6 +1,6 @@
1
1
  <!--
2
2
  llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
3
- 重写日期: 2026-10-21 | 包版本: 14.4.0
3
+ 重写日期: 2026-10-23 | 包版本: 14.5.0
4
4
  生成方式: 无生成脚本——按当前树的 src/*.ts 导出面 + package.json + README/CHANGELOG 手写事实版;
5
5
  每条断言附证据坐标(文件:符号),刻意不用行号(行号随每版腐烂,符号名由 typecheck/test 钉住)。
6
6
  刷新触发: package.json 版号变更(test/docs-hygiene.test.ts D 组钉「包版本:」与 package.json 相等、
@@ -150,6 +150,8 @@ llms.txt — 消费方(使用者)视角索引 · @sema-agent/settings-schema
150
150
  (src/web-admin-credential.ts)
151
151
  - **目录形一次性迁移**(`./node`):`migrateConfigDirectory(dir, { documents, io, check? })` —— 命令行 `migrate` 背后的唯一实现(读目录 → 迁 → 整域终判 → 原子写 → 删退役域文件);`documents` = 调用方读的域名集(覆盖面外的文件不读不判不写不删),
152
152
  `io` 注入四个口,真绑定 `nodeConfigDirectoryIo`(node:fs + 唯一的原子写器);返回闭集 `unreadable` / `no-documents` / `refused` / `noop` / `pending` / `applied`。(src/config-dir-migration.ts · src/config-dir-io.ts)
153
+ - **同步单口**(`./node`,14.5.0):`migrateConfigDocuments(docs, { documents })` —— 手里已有存量文档原文(文档名 → 文档)时,「迁 → 整域终判」一次调用、无 io,与目录形同一个内核同一个闭集的无 io 形:`unreadable`(覆盖面内某份不是 JSON 对象 ⇒ 不抛,点名 `document`)/ `no-documents` / `refused`(`docs` 仍交回迁移后的那一份)/ `noop`(同一引用)/ `migrated`(`docs` + `applied` + `changed` + `removed`)。一层一次调用,层的合并不在这里。(src/config-dir-migration.ts)
154
+ - **URL 内嵌凭据判官**(包根,14.5.0):`findUrlCredential(raw)` —— userinfo,或 query / fragment 里名字整名在凭据名表 `CREDENTIAL_PARAM_NAMES`(23 词)上的参数 ⇒ `{ reason: "url_userinfo" }` / `{ reason: "url_credential_param", at, name }`,首个命中、值不进答案,不透明 / 解析不了 ⇒ `undefined`;`findUrlUserinfo`(第一问)· `isCredentialParamName` · `urlCredentialRule`(规则句)· `URL_CREDENTIAL_REASONS`(`url_userinfo` / `url_credential_param`)。判官只判有没有,载体律(新提交门拒 / 重放腿不拒 / 部署 env 只因 userinfo 拒)留在座位。(src/url-credential.ts)
153
155
  - **会话网页链接**:`webAdminSessionUrl(webBaseUrl, { worker, sessionId })` 铸 `<webBaseUrl>/chat?agent=<worker>&session=<sessionId>`(两位必填、逐位 `encodeURIComponent`、基址须是干净的绝对 http(s) URL 且尾斜杠折掉,
154
156
  坏输入抛、不铸半截链接)+ 配对解析 `parseWebAdminSessionUrl(link)`(绝对 / 相对串、`URL`、`URLSearchParams`;缺的那一位缺席)—— 路由与参数名只此一处,谁都别手拼。(src/web-admin-session-url.ts)
155
157
  - **按 scope 绑定的令牌**(`./api/pull-token`):形与铸法一条规则、令牌族是数据(`WORKER_PULL_TOKEN` = `wpt_` / `wpt2_` / `worker-pull`);