@sema-agent/server 7.89.0 → 7.90.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.
@@ -0,0 +1,3 @@
1
+ /** `err` 的 fatal 日志文本:`String(err)` 过形状律一次。纯函数,零副作用。 */
2
+ export declare function buildFatalErrorText(err: unknown): string;
3
+ //# sourceMappingURL=fatal-exit.d.ts.map
@@ -0,0 +1,5 @@
1
+ import { redactSecrets } from "../trace/redact.js";
2
+ export function buildFatalErrorText(err) {
3
+ return redactSecrets(String(err));
4
+ }
5
+ //# sourceMappingURL=fatal-exit.js.map
@@ -303,6 +303,7 @@ export function createResolveSpec(ctx) {
303
303
  ...(retainBackgroundProcessesFromBody(body.retainBackgroundProcesses, config.requirePrincipal, (event, fields) => logger.warn(event, { ...fields, sessionId: auth?.sessionId ?? null })) === true
304
304
  ? { retainBackgroundProcesses: true }
305
305
  : {}),
306
+ ...(body.excludeAllTools === true ? { excludeAllTools: true } : {}),
306
307
  ...(() => { const v = toolNameListFromBody(body.excludeTools); return v ? { excludeTools: v } : {}; })(),
307
308
  ...(() => { const v = toolNameListFromBody(body.deferTools); return v ? { deferTools: v } : {}; })(),
308
309
  ...(() => { const v = promptProfileFromBody(body.promptProfile); return v ? { promptProfile: v } : {}; })(),
@@ -541,6 +542,7 @@ export function createResolveSpec(ctx) {
541
542
  declaredCapable: effectiveMemoryPersistenceCapable(config),
542
543
  hands: gated.hands,
543
544
  handsReadOnly: governed.handsReadOnly === true,
545
+ excludeAllTools: governed.excludeAllTools === true,
544
546
  excludeTools: governed.excludeTools,
545
547
  remoteExecutionEnv: config.remoteExec !== undefined,
546
548
  })) {
package/dist/brain.js CHANGED
@@ -1,22 +1,31 @@
1
1
  import { adjudicateModelRoute, createAnthropicBrain, createCircuitBreakerBrain, createDegradingBrain, createFailoverBrain, createOpenAIBrain, createRoutingBrain, hasAuthCarrier, resolveRouteCredential, routeRefusalText, } from "@sema-agent/core";
2
- import { normalizeBaseUrl } from "@sema-agent/core";
2
+ import { routeKeyOf } from "@sema-agent/core";
3
3
  import { buildDefaultResilience } from "./config-types.js";
4
4
  import { resolveModelApiKey } from "./key-resolver.js";
5
5
  import { catalogBrainLane } from "./model-provider.js";
6
- import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
6
+ import { buildDisplaySafeRefusal, buildDisplaySafeUrl } from "./trace/redact.js";
7
7
  function isOffDeploymentRoot(baseUrl, config) {
8
8
  if (!baseUrl)
9
9
  return false;
10
- const entry = normalizeBaseUrl(baseUrl);
10
+ const entry = routeKeyOf(baseUrl);
11
11
  if (entry === "")
12
12
  return false;
13
- return ![config.gatewayBaseUrl, ...config.gatewayFallbackUrls].some((u) => normalizeBaseUrl(u) === entry);
13
+ return ![config.gatewayBaseUrl, ...config.gatewayFallbackUrls].some((u) => routeKeyOf(u) === entry);
14
14
  }
15
15
  function degradeDeclaredRoot(fallbackBaseUrl, config) {
16
16
  const armed = Boolean(config.gatewayApiKey) && isOffDeploymentRoot(fallbackBaseUrl, config);
17
17
  return armed ? config.gatewayBaseUrl : (fallbackBaseUrl || config.gatewayBaseUrl);
18
18
  }
19
19
  export const FAST_FAIL_MAX_RETRIES = 2;
20
+ function constructRouteBrain(label, sourceUrls, construct) {
21
+ try {
22
+ return construct();
23
+ }
24
+ catch (e) {
25
+ const text = buildDisplaySafeRefusal(e instanceof Error ? e.message : String(e), sourceUrls.map((u) => u ?? ""));
26
+ throw new Error(`${label}: ${text}`, { cause: undefined });
27
+ }
28
+ }
20
29
  const DEFAULT_RESILIENCE = buildDefaultResilience();
21
30
  export function createBrain(config, deps = {}) {
22
31
  const { fetchImpl, breakerState } = deps;
@@ -34,7 +43,7 @@ export function createBrain(config, deps = {}) {
34
43
  : {};
35
44
  const routes = [config.gatewayBaseUrl, ...config.gatewayFallbackUrls];
36
45
  const openaiBrains = routes.map((baseUrl, i) => {
37
- const brain = createOpenAIBrain({
46
+ const brain = constructRouteBrain(i === 0 ? "gateway route" : `gateway fallback route[${i - 1}]`, [baseUrl], () => createOpenAIBrain({
38
47
  baseUrl,
39
48
  get apiKey() {
40
49
  return config.gatewayApiKey;
@@ -42,7 +51,7 @@ export function createBrain(config, deps = {}) {
42
51
  ...retryCap,
43
52
  fetchImpl,
44
53
  ...timeouts,
45
- });
54
+ }));
46
55
  const isLast = i === routes.length - 1;
47
56
  return r.circuitBreaker && !isLast
48
57
  ? createCircuitBreakerBrain(brain, {
@@ -54,18 +63,19 @@ export function createBrain(config, deps = {}) {
54
63
  : brain;
55
64
  });
56
65
  const gatewayBrain = openaiBrains.length > 1 ? createFailoverBrain(openaiBrains) : openaiBrains[0];
57
- const anthropicBrain = config.anthropic
58
- ? createAnthropicBrain({
59
- ...(config.anthropic.authToken
60
- ? { headers: { Authorization: `Bearer ${config.anthropic.authToken}` } }
61
- : { apiKey: config.anthropic.apiKey }),
62
- baseUrl: config.anthropic.baseUrl ?? "https://api.anthropic.com",
63
- version: config.anthropic.version,
64
- cacheBreakpoints: config.anthropic.cacheBreakpoints,
65
- ...(config.anthropic.maxRetries !== undefined ? { maxRetries: config.anthropic.maxRetries } : {}),
66
+ const anthropicCfg = config.anthropic;
67
+ const anthropicBrain = anthropicCfg
68
+ ? constructRouteBrain("anthropic route", [anthropicCfg.baseUrl], () => createAnthropicBrain({
69
+ ...(anthropicCfg.authToken
70
+ ? { headers: { Authorization: `Bearer ${anthropicCfg.authToken}` } }
71
+ : { apiKey: anthropicCfg.apiKey }),
72
+ baseUrl: anthropicCfg.baseUrl ?? "https://api.anthropic.com",
73
+ version: anthropicCfg.version,
74
+ cacheBreakpoints: anthropicCfg.cacheBreakpoints,
75
+ ...(anthropicCfg.maxRetries !== undefined ? { maxRetries: anthropicCfg.maxRetries } : {}),
66
76
  fetchImpl,
67
77
  ...timeouts,
68
- })
78
+ }))
69
79
  : undefined;
70
80
  let brain = anthropicBrain
71
81
  ? createRoutingBrain({ anthropic: anthropicBrain }, { fallback: gatewayBrain })
@@ -81,7 +91,7 @@ export function createBrain(config, deps = {}) {
81
91
  const fallback = fallbackLane === "anthropic" && anthropicBrain
82
92
  ?
83
93
  anthropicBrain
84
- : createOpenAIBrain({
94
+ : constructRouteBrain(`degrade target "${degradeTo}"`, [fallbackModel.baseUrl, config.gatewayBaseUrl], () => createOpenAIBrain({
85
95
  get baseUrl() {
86
96
  return degradeDeclaredRoot(fallbackModel.baseUrl, config);
87
97
  },
@@ -91,7 +101,7 @@ export function createBrain(config, deps = {}) {
91
101
  ...(config.gatewayMaxRetries !== undefined ? { maxRetries: config.gatewayMaxRetries } : {}),
92
102
  fetchImpl,
93
103
  ...timeouts,
94
- });
104
+ }));
95
105
  const degradeEntryModel = {
96
106
  ...fallbackModel,
97
107
  get headers() {
@@ -158,7 +168,10 @@ export function brainSummary(config) {
158
168
  const verdict = adjudicateModelRoute({ id: target.id, baseUrl: target.baseUrl }, resolveRouteCredential({ ...(config.gatewayApiKey ? { configApiKey: config.gatewayApiKey } : {}) }), { baseUrl: degradeDeclaredRoot(target.baseUrl, config) });
159
169
  return verdict.ok
160
170
  ? { reactiveDegradeRoutePairing: `ok:${verdict.posture}`, reactiveDegradeRouteRefusal: null }
161
- : { reactiveDegradeRoutePairing: `broken:${verdict.code}`, reactiveDegradeRouteRefusal: redactSecrets(routeRefusalText(verdict)) };
171
+ : {
172
+ reactiveDegradeRoutePairing: `broken:${verdict.code}`,
173
+ reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config)]),
174
+ };
162
175
  })()),
163
176
  };
164
177
  }
@@ -51,6 +51,12 @@ export interface MemoryFaceFacts {
51
51
  hands: ScenarioHands;
52
52
  /** 折完 governance+settings 后的最终 `handsReadOnly`(plan 模式 / autonomy 收窄)。手带只剩读腿。 */
53
53
  handsReadOnly: boolean;
54
+ /** 折完之后的最终 `TaskSpec.excludeAllTools`(S-493;core 7.8.0 [ref])。`true` = **整只**工具面卸空
55
+ * (core `prepareDeferClassify` 对它直接 `roster.unmountWhere(() => true)`)⇒ 手带在场也一条落盘路都不剩。
56
+ * 🔴 它与下面那张名单是**同一个语义面**(「哪些工具不在 roster 上」)的两种声明,判据合成一只
57
+ * {@link isUnmounted} —— 上游同版的姊妹谓词 `fullShellReachable` 也正是把 `spec.excludeAllTools !== true`
58
+ * 当一个合取项(`prepare-inherited-gate.js:174` 亲读),本仓与它同口径。 */
59
+ excludeAllTools: boolean;
54
60
  /** 折完之后的最终 `TaskSpec.excludeTools`(roster 真卸载,不是延迟披露)。**全部**已知落盘路都被排掉
55
61
  * ⇒ 手带在场也等于没有写通道 —— core 的 `rosterCanPersist` 也是按排除后的 roster 算的,这里跟上。 */
56
62
  excludeTools: readonly string[] | undefined;
@@ -71,7 +77,7 @@ export interface MemoryFaceFacts {
71
77
  * ① engine 在场 ⇒ **否**(引擎的座位,见文件头注);
72
78
  * ② 部署显式声明 ⇒ 声明说了算(`true` 撤回 / `false` 强制)—— operator 的部署事实压过我们的推断,
73
79
  * 与 core 对 `memoryPersistenceCapable` 的处置同向;
74
- * ③ 否则推断:有已知落盘路(手带在场且不是只读)⇒ 否;没有 ⇒ 是。
80
+ * ③ 否则推断:有已知落盘路(手带在场、不是只读、不在 remote 车道、且没被卸载决定卸光)⇒ 否;没有 ⇒ 是。
75
81
  */
76
82
  export declare function shouldDiscloseNoPersistentMemory(facts: MemoryFaceFacts): boolean;
77
83
  /**
@@ -2,11 +2,11 @@ import { NO_PERSISTENT_MEMORY_NOTICE } from "@sema-agent/core";
2
2
  import { hasConstitutionAnchors } from "../task-settings.js";
3
3
  export const KNOWN_PERSISTENCE_TOOL_NAMES = ["Write", "Edit", "NotebookEdit", "Bash"];
4
4
  export const NO_PERSISTENT_MEMORY_SECTION_ID = "sema/memory.no-persistent";
5
- function anyPersistenceToolSurvives(excludeTools) {
6
- if (excludeTools === undefined || excludeTools.length === 0)
7
- return true;
8
- const excluded = new Set(excludeTools);
9
- return KNOWN_PERSISTENCE_TOOL_NAMES.some((name) => !excluded.has(name));
5
+ function isUnmounted(facts, name) {
6
+ return facts.excludeAllTools || (facts.excludeTools?.includes(name) ?? false);
7
+ }
8
+ function anyPersistenceToolSurvives(facts) {
9
+ return KNOWN_PERSISTENCE_TOOL_NAMES.some((name) => !isUnmounted(facts, name));
10
10
  }
11
11
  export function shouldDiscloseNoPersistentMemory(facts) {
12
12
  if (facts.memoryFaceMounted)
@@ -17,7 +17,7 @@ export function shouldDiscloseNoPersistentMemory(facts) {
17
17
  return true;
18
18
  if (facts.remoteExecutionEnv)
19
19
  return true;
20
- return !anyPersistenceToolSurvives(facts.excludeTools);
20
+ return !anyPersistenceToolSurvives(facts);
21
21
  }
22
22
  const NOTICE_BLOCK = { id: NO_PERSISTENT_MEMORY_SECTION_ID, slot: "behavior", text: NO_PERSISTENT_MEMORY_NOTICE };
23
23
  function mountNotice(decls) {
@@ -20,6 +20,43 @@ import type { Logger } from "../observability/logger.js";
20
20
  * registerTeams` 那条消费腿——本文件旧注里「registry pushes more at runtime via GET /api/config/effective → teams」
21
21
  * 是 doc-rot。今天 `registerTeams` 只剩公共信任边界这一层意义(调用方=测试),注册表在生产上恒等于 BUILTIN_TEAMS;
22
22
  * 中心侧的多 agent 讨论配置面是 `collab` 域(0.7 继任者),投影成命名 workflow(capabilities/collab-workflows.ts)。
23
+ *
24
+ * 🔴 **成员重派阶梯收窄(core 7.24.0 [ref];本仓 7.90.0 提货批的读数说明,零改动)** —— 一次讨论**花的子 run
25
+ * 数掉一半**,上游实测:两成员一轮的讨论从 **6 个子 run 变 3 个**。原因:`runTurn` 此前对**每一个**没有
26
+ * 「completed 且有正文」落地的成员轮都重派一次(同目标、同提示词、同模型、同工具面),包括那些**结局上
27
+ * 不可能变**的终局;现在只对引擎能**肯定**认出是确定性的两种终局拒绝重派(`blocked` 因由 = 宿主
28
+ * `UserPromptSubmit` 的拒绝,照字面信;`failed` 且治理表判 TERMINAL 的码,读 `classifySubagentError`)。
29
+ *
30
+ * 🔴 **这不是「丢了重试」,读数变低也不是故障**(写在这里是因为它唯一会被误读的地方就是账单与子代行数):
31
+ * · 其余一切**照旧**保留第二次尝试 —— 那个分类器的 `logic`/`unknown` 桶是**else 臂**(它不认识的码全落
32
+ * 那里),把「永不重派」挂在 else 臂上会让**将来每一个新码**默默变成不可重派,所以那条判决留在表里。
33
+ * ⚠️ **本句在 7.90.0(core 7.24.2 / [ref])修了措辞**:上一版写的是「`stream_torn` 住在 `logic` 桶里
34
+ * (else 臂)…负控挑它最有判别力」—— core 本版给三个 in-band 终局输出码**各自开了行**
35
+ * (`stream_torn → network`(transient)/ `refusal → logic` / `length_empty → logic`),所以
36
+ * `stream_torn` **已经不在 else 臂上了**(本车亲跑差分实测:`classifySubagentError({code:"stream_torn"})`
37
+ * 从 7.24.1 的 `{errorKind:"logic", retryable:false}` 变 7.24.2 的 `{errorKind:"network", retryable:true}`
38
+ * —— 注意 `errorKind` 与 `retryable` **两个字段都翻了**,提货单只点了后者)。
39
+ * **结局没变**(`stream_torn` 照旧重派 ⇒ 仍 6):重派闸 `canDifferOnRetry` 判的是「`blocked` 因由 + 治理表
40
+ * 判 TERMINAL」,从不读桶名 —— 变的是**理由**(从「掉进 else 臂」变成「表里那一行说它 transient」)。
41
+ * ⇒ 拿 `stream_torn` 当 **else 臂**负控从本版起**没有判别力了**(它是一条有行的码);要挑 else 臂代表,
42
+ * 挑**未知码** / **无码的 `failed`** / **空正文的 `completed`** —— 那三形才真的落 else 臂。
43
+ * · `[unavailable: <status>/<code>]` 标记、讨论转录、预算记账的**形一个字节没变**。
44
+ * · **本仓没有任何按子 run 数计费 / 计量的座**(2026-09-19 车JG 亲扫:`billable` 是 `http/route-table.ts` 上
45
+ * 「这条路由算不算一次计费提交」的路由性质,与子 run 无关;`council_runs_total` 数的是**议会被调用了几次**,
46
+ * 不是子代;成员轮不建 run-store 行,它们是 TaskRegistry 上的 `a*` 句柄)。⇒ 本版在本仓**零改动**,
47
+ * 唯一会动的读数是 `GET /v1/runs/:id/subagents` 的**行数**与经 `stats` 透传的**用量/花费**(都变低),
48
+ * 两者都是上游事实的透传、本仓不据行数做任何裁决。
49
+ *
50
+ * 🔴 **判据按「前缀族」写,不按具体码字**(test 仓对 core 7.24.0 的实测②,本仓照此记账;将来真在本仓开
51
+ * 计量点时钉法同此):引擎读的是治理表对**码**的裁决,而裁决是按**前缀族**给的 —— 一个族里凭空合成的码
52
+ * (test 实测的 `usage.FAKE_prefix_only`)照样判 TERMINAL ⇒ **3**。所以正控/负控各挑**一个**代表就够,
53
+ * 逐字抄那张码表只会在上游加一行的那天变成谎:
54
+ * · **⇒ 3(不重派)**:`blocked` 因由,以及 7 个被治理表判 TERMINAL 的前缀族失败码(合成同前缀码同判);
55
+ * · **⇒ 仍 6(照旧两次)**:两个 transient 码 `memory.admission_required` / `config.compliance_required`,
56
+ * 以及 `stream_torn` / `refusal` / `length_empty` / 未知码 / 无码的 `failed` / 空正文的 `completed`。
57
+ * 🔴 **这六形自 core 7.24.2([ref])起不再同源**:前三形在分类器里**各有一行**
58
+ * (`stream_torn → network` transient / `refusal`、`length_empty` → `logic`),后三形才是真的 **else 臂**。
59
+ * 结局(仍 6)对六形都没变,但**负控要挑真 else 臂**:未知码 / 无码的 `failed` / 空正文的 `completed`。
23
60
  */
24
61
  type TeamFn = typeof runTeamDiscussion;
25
62
  export interface TeamMemberTemplate {
@@ -5,6 +5,7 @@ import { webSearchConfigFromEnv, WEB_SEARCH_PROVIDERS } from "./plugins/web-sear
5
5
  import { REMOTE_EXEC_PROVIDER_WORDS } from "./execution-lane-caps.js";
6
6
  import { CENTER_READ_FACE_LIVE_KEYS } from "./config-center/read-face.js";
7
7
  import { MODEL_PROVIDER_WORDS } from "./model-provider.js";
8
+ import { buildDisplaySafeUrl } from "./trace/redact.js";
8
9
  export const CATALOG_VALUE_TYPES = ["boolean", "number", "string", "enum", "csv", "json", "path", "url", "secret", "opaque"];
9
10
  export const CATALOG_TIMINGS = ["hot", "restart", "deploy", "inert"];
10
11
  export const CATALOG_DANGER_AXES = ["security", "billing", "data"];
@@ -55,7 +56,7 @@ export const CONFIG_CATALOG = [
55
56
  r("ALLOW_UNAUTHED_WRITES", "auth", "boolean", "无 service token 时仍接受任务提交(本地开发逃生口;缺省 fail-closed 拒提交)", { staticDefault: "false", danger: SEC, configKey: "allowUnauthedWrites" }),
56
57
  r("ANTHROPIC_API_KEY", "modelPlane", "secret", "Anthropic 协议路线的 x-api-key 凭据(与 AUTH_TOKEN 二选一,AUTH_TOKEN 优先;与 BASE_URL 同在时可推断 provider)", { danger: [...SEC, ...BILL] }),
57
58
  r("ANTHROPIC_AUTH_TOKEN", "modelPlane", "secret", "Anthropic 兼容生态的 Bearer 凭据(设了则不发 x-api-key)", { danger: [...SEC, ...BILL] }),
58
- r("ANTHROPIC_BASE_URL", "modelPlane", "url", "Anthropic 协议路线 base URL(与凭据同在且 MODEL_PROVIDER 未设 ⇒ 推断 provider=anthropic 并 boot notice;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo`;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`)"),
59
+ r("ANTHROPIC_BASE_URL", "modelPlane", "url", "Anthropic 协议路线 base URL(与凭据同在且 MODEL_PROVIDER 未设 ⇒ 推断 provider=anthropic 并 boot notice;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo`;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment` —— fragment 从不上线,后缀会落进被丢弃的那一半)"),
59
60
  r("ANTHROPIC_CACHE_BREAKPOINTS", "modelPlane", "boolean", "Anthropic 路线 prompt-cache breakpoints 开关", { staticDefault: "true" }),
60
61
  r("ANTHROPIC_MAX_RETRIES", "modelPlane", "number", "Anthropic 路线 SDK 每次调用重试上限", { derivedDefaultNote: "缺席 ⇒ 不传引擎,core/SDK 默认当家" }),
61
62
  r("ANTHROPIC_VERSION", "modelPlane", "string", "Anthropic API version 头(缺省由 SDK 定)"),
@@ -282,8 +283,8 @@ export const CONFIG_CATALOG = [
282
283
  r("MODEL_EXTRA_BODY", "modelPlane", "opaque", "生成参数直通 JSON(top_k/logit_bias 等;必须静态 —— 前缀缓存键序)", { danger: SEC }),
283
284
  r("MODEL_FIRST_TOKEN_TIMEOUT_MS", "modelPlane", "number", "首 token 超时(ms;0=关 —— 开流不吐字的唯一看门狗;自托管长 prefill 后端按此档留足)", { staticDefault: String(RESILIENCE_DEFAULTS.firstTokenTimeoutMs.value) }),
284
285
  r("MODEL_FREQUENCY_PENALTY", "modelPlane", "number", "frequency_penalty 直通(压过 EXTRA_BODY 同键)"),
285
- r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
286
- r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`)", { configKey: "gatewayFallbackUrls" }),
286
+ r("MODEL_GATEWAY_BASEURL", "modelPlane", "url", "openai 兼容网关 base URL(默认 localhost 占位 —— 公开分发不烤内网坐标;S-475:带 userinfo 拒启,机读码 `config.gateway_url_userinfo` —— WHATWG 禁止用带凭据的 URL 构造 fetch Request,凭据放 MODEL_API_KEY;S-483:解析不动/不透明形拒启,机读码 `config.model_route_url_invalid`;S-514:带 `#fragment` 拒启,机读码 `config.model_route_url_fragment`)", { staticDefault: "http://127.0.0.1:8000/v1", configKey: "gatewayBaseUrl", center: cModels("models[].baseUrl") }),
287
+ r("MODEL_GATEWAY_FALLBACK_URLS", "modelPlane", "url", "网关故障转移地址表(CSV;S-475:**逐项**判 userinfo,任一项带 ⇒ 拒启,机读码 `config.gateway_url_userinfo`;S-483:逐项判可解析性,机读码 `config.model_route_url_invalid`;S-514:**逐项**判 `#fragment`,机读码 `config.model_route_url_fragment`)", { configKey: "gatewayFallbackUrls" }),
287
288
  r("MODEL_ID", "modelPlane", "string", "主模型 id(3.0.0 起无出厂缺省,缺 ⇒ 拒启指路)", { configKey: "model", resolve: (c) => c.model.id, center: cModels("models[].id") }),
288
289
  r("MODEL_IDLE_TIMEOUT_MS", "modelPlane", "number", "流中静默看门狗(ms;0=关)", { staticDefault: String(RESILIENCE_DEFAULTS.idleTimeoutMs.value) }),
289
290
  r("MODEL_MAX_TOKENS", "modelPlane", "number", "主模型输出上限", { derivedDefaultNote: "缺席 ⇒ min(32000, contextWindow)", resolve: (c) => c.model.maxTokens ?? null, center: cModels("models[].maxTokens") }),
@@ -478,19 +479,6 @@ function primitiveConfigValue(config, key) {
478
479
  return v;
479
480
  return undefined;
480
481
  }
481
- const REDACTED_UNPARSEABLE_URL = "(redacted:unparseable-url)";
482
- function stripUrlUserinfo(v) {
483
- if (!URL.canParse(v))
484
- return REDACTED_UNPARSEABLE_URL;
485
- const u = new URL(v);
486
- u.username = "";
487
- u.password = "";
488
- if (u.search === "" && u.hash === "" && u.href === v)
489
- return v;
490
- u.search = "";
491
- u.hash = "";
492
- return u.href;
493
- }
494
482
  function sanitizeEnumValue(spec, v) {
495
483
  const words = spec.echoWords ?? spec.enumValues;
496
484
  if (v === null)
@@ -511,9 +499,9 @@ function sanitizeValue(spec, v) {
511
499
  const type = spec.type;
512
500
  if (type === "url") {
513
501
  if (typeof v === "string")
514
- return { value: v.includes(",") ? v.split(",").map((s) => stripUrlUserinfo(s.trim())).filter((s) => s.length > 0) : stripUrlUserinfo(v) };
502
+ return { value: v.includes(",") ? v.split(",").map((s) => buildDisplaySafeUrl(s.trim())).filter((s) => s.length > 0) : buildDisplaySafeUrl(v) };
515
503
  if (Array.isArray(v))
516
- return { value: v.map((s) => stripUrlUserinfo(s)) };
504
+ return { value: v.map((s) => buildDisplaySafeUrl(s)) };
517
505
  return { value: v };
518
506
  }
519
507
  if (type === "csv" && typeof v === "string")
package/dist/config.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { type Model, type PathRuleBases, type RuleBehavior } from "@sema-agent/core";
2
- import type { Autonomy } from "./runtime-governance.js";
2
+ import { type Autonomy } from "./runtime-governance.js";
3
3
  import type { ServiceConfig, ServiceConfigFlat, ServiceConfigGroups } from "./config-types.js";
4
4
  /** [ref] A1: the config TYPE face lives in the leaf module config-types.ts; re-exported here so
5
5
  * every existing `from "./config.js"` importer compiles unchanged (pure-type consumers should prefer
package/dist/config.js CHANGED
@@ -17,9 +17,10 @@ import { readFaceForPosture } from "./read-face-posture.js";
17
17
  import { THINKING_LEVELS, THINKING_FORMATS, buildModelCompat, isThinkingFormat, isThinkingTier } from "./model-compat.js";
18
18
  import {} from "./posture-source.js";
19
19
  import { CONTROL_AND_BIDI_STRIP_RE } from "./text-bidi.js";
20
+ import { AUTONOMY_MODES } from "./runtime-governance.js";
20
21
  import { modelProviderRefusal } from "./model-provider.js";
21
22
  import { modelRouteUrlRefusal } from "./model-route-url.js";
22
- import { buildDisplaySafeUrl } from "./trace/redact.js";
23
+ import { buildDisplaySafeUrl, redactSecrets } from "./trace/redact.js";
23
24
  export { RETENTION_MODES, MEMORY_CONSOLIDATION_MIN_INTERVAL_SEC } from "./config-types.js";
24
25
  function csv(name) {
25
26
  return (process.env[name] ?? "")
@@ -27,7 +28,6 @@ function csv(name) {
27
28
  .map((s) => s.trim())
28
29
  .filter(Boolean);
29
30
  }
30
- const AUTONOMY_MODES = ["read-only", "ask", "plan", "auto"];
31
31
  export function parseAutonomy(raw) {
32
32
  const v = raw?.trim();
33
33
  if (!v)
@@ -1972,9 +1972,9 @@ function parseA2aServe() {
1972
1972
  throw new Error(`A2A_SERVE_URL must not carry userinfo (credentials before "@") — it is published verbatim on the ANONYMOUSLY readable agent card, so those credentials would be handed to every discovery caller. ` +
1973
1973
  `Inbound authentication on this endpoint is header-shaped (a service credential), not URL-embedded; strip the userinfo and keep the secret in the caller's headers.`);
1974
1974
  }
1975
- const a2aDisplaySafe = buildDisplaySafeUrl(rawUrl);
1976
- if (a2aDisplaySafe !== parsed.href) {
1977
- throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${a2aDisplaySafe}) — this address is published VERBATIM on the ANONYMOUSLY readable agent card (the card's url is a functional address: peers fetch it, so it is never rewritten for display), ` +
1975
+ const a2aCredentialScan = redactSecrets(parsed.href);
1976
+ if (a2aCredentialScan !== parsed.href) {
1977
+ throw new Error(`A2A_SERVE_URL carries something the credential-redaction law recognizes (${a2aCredentialScan}) — this address is published VERBATIM on the ANONYMOUSLY readable agent card (the card's url is a functional address: peers fetch it, so it is never rewritten for display), ` +
1978
1978
  `so it must not carry credentials of ANY shape: no userinfo, no credential-named query parameter (api_key/token/…), no token-shaped path segment or parameter value. ` +
1979
1979
  `Keep A2A_SERVE_URL to scheme://host[:port][/path] and move the secret into the caller's headers.`);
1980
1980
  }
@@ -1,6 +1,6 @@
1
- import { adjudicateModelRoute, normalizeBaseUrl, resolveRouteCredential, routeRefusalText } from "@sema-agent/core";
1
+ import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
2
2
  import { matchCatalogModel } from "../model-select.js";
3
- import { redactSecrets } from "../trace/redact.js";
3
+ import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
4
4
  export function buildHookFailureText(raw) {
5
5
  if (raw === "")
6
6
  return raw;
@@ -53,9 +53,13 @@ export function createHookLlm(deps) {
53
53
  });
54
54
  const verdict = adjudicateModelRoute({ id: pick.catalogRef, baseUrl: pick.baseUrl }, credential, { ...(declaredRoot !== undefined ? { baseUrl: declaredRoot } : {}) });
55
55
  if (!verdict.ok) {
56
- return { ok: false, error: `hook model "${pick.catalogRef}" cannot be routed — ${routeRefusalText(verdict)}` };
56
+ return {
57
+ ok: false,
58
+ error: `hook model "${pick.catalogRef}" cannot be routed (${verdict.code}) — ` +
59
+ buildDisplaySafeRefusal(routeRefusalText(verdict), [pick.baseUrl, declaredRoot ?? ""]),
60
+ };
57
61
  }
58
- if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && normalizeBaseUrl(pick.baseUrl) !== normalizeBaseUrl(declaredRoot)) {
62
+ if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
59
63
  return {
60
64
  ok: false,
61
65
  error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
@@ -91,7 +95,7 @@ export function createHookLlm(deps) {
91
95
  const routed = judgeRoute(pick, perModelKey, config.anthropic?.authToken ?? config.anthropic?.apiKey, routeBase);
92
96
  if (!routed.ok)
93
97
  return routed;
94
- url = `${pick.baseUrl.replace(/\/+$/, "")}/v1/messages`;
98
+ url = joinRequestUrl(pick.baseUrl, "/v1/messages");
95
99
  headers = {
96
100
  "content-type": "application/json",
97
101
  "anthropic-version": config.anthropic?.version ?? "2023-06-01",
@@ -112,7 +116,7 @@ export function createHookLlm(deps) {
112
116
  if (!routed.ok)
113
117
  return routed;
114
118
  const hookKey = routed.credential.apiKey;
115
- url = `${pick.baseUrl.replace(/\/+$/, "")}/chat/completions`;
119
+ url = joinRequestUrl(pick.baseUrl, "/chat/completions");
116
120
  headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
117
121
  body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
118
122
  }
@@ -52,7 +52,9 @@ const BRAIN_TO_TURN_ERROR = {
52
52
  max_output_tokens: "max_output_tokens",
53
53
  };
54
54
  function toAssistantTurnError(errorKind) {
55
- return (errorKind !== undefined ? BRAIN_TO_TURN_ERROR[errorKind] : undefined) ?? "unknown";
55
+ if (errorKind === undefined)
56
+ return "unknown";
57
+ return Object.prototype.hasOwnProperty.call(BRAIN_TO_TURN_ERROR, errorKind) ? (BRAIN_TO_TURN_ERROR[errorKind] ?? "unknown") : "unknown";
56
58
  }
57
59
  export function parseHooksConfig(raw) {
58
60
  const parsed = HooksConfig.safeParse(raw);
@@ -1148,6 +1150,21 @@ export function createTaskHooks(config, ctx) {
1148
1150
  }
1149
1151
  return hooks;
1150
1152
  }
1153
+ const HOOK_SLOT_FOLD = {
1154
+ preToolUse: "custom",
1155
+ preToolUseObservational: "custom",
1156
+ postToolUse: "custom",
1157
+ postToolUseFailure: "custom",
1158
+ postToolBatch: "custom",
1159
+ userPromptSubmit: "custom",
1160
+ stop: "custom",
1161
+ preCompact: "custom",
1162
+ timeoutMs: "scalar",
1163
+ postCompact: "observe-void",
1164
+ stopFailure: "observe-void",
1165
+ permissionDenied: "observe-void",
1166
+ };
1167
+ const OBSERVE_VOID_HOOK_SLOTS = Object.keys(HOOK_SLOT_FOLD).filter((k) => HOOK_SLOT_FOLD[k] === "observe-void");
1151
1168
  export function composeHooks(deployment, task) {
1152
1169
  if (!deployment)
1153
1170
  return task;
@@ -1270,7 +1287,7 @@ export function composeHooks(deployment, task) {
1270
1287
  };
1271
1288
  };
1272
1289
  }
1273
- for (const slot of ["stopFailure", "postCompact", "permissionDenied"]) {
1290
+ for (const slot of OBSERVE_VOID_HOOK_SLOTS) {
1274
1291
  const ds = d[slot];
1275
1292
  const ts = t[slot];
1276
1293
  if (ds && ts) {
@@ -1,7 +1,7 @@
1
1
  import { isThinkingLevel, expandTiers } from "@sema-agent/core";
2
2
  import { isModelAllowlisted, matchCatalogModel } from "../model-select.js";
3
3
  import { HttpError } from "../security.js";
4
- import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
4
+ import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS, SETTINGS_PERMISSION_MODE_WORDS, isSettingsPermissionMode, parseTaskSettings, skillListingBudgetFractionOk, skillListingMaxDescOk, taskSettingsKeyIssue } from "../task-settings.js";
5
5
  import { taskBodyKeyIssue } from "../request-key-closure.js";
6
6
  import { singleMemberOffFieldIssue } from "../request-value-closure.js";
7
7
  import { selfOrchestrationDenial } from "../task-workflow.js";
@@ -430,6 +430,10 @@ export function createAdmission(ctx) {
430
430
  sendError(res, 400, "request.field_invalid", "retainBackgroundProcesses must be a boolean");
431
431
  return null;
432
432
  }
433
+ if (body.excludeAllTools !== undefined && typeof body.excludeAllTools !== "boolean") {
434
+ sendError(res, 400, "request.field_invalid", "excludeAllTools must be a boolean");
435
+ return null;
436
+ }
433
437
  for (const key of ["excludeTools", "deferTools"]) {
434
438
  const v = body[key];
435
439
  if (v !== undefined && (!Array.isArray(v) || v.some((n) => typeof n !== "string" || n.length === 0))) {
@@ -453,8 +457,8 @@ export function createAdmission(ctx) {
453
457
  }
454
458
  {
455
459
  const v = body.permissionMode;
456
- if (v !== undefined && v !== "default" && v !== "acceptEdits" && v !== "plan" && v !== "bypassPermissions" && v !== "auto") {
457
- sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
460
+ if (v !== undefined && !isSettingsPermissionMode(v)) {
461
+ sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
458
462
  return null;
459
463
  }
460
464
  }
@@ -464,8 +468,8 @@ export function createAdmission(ctx) {
464
468
  const p = s.permissions;
465
469
  if (p !== null && typeof p === "object" && !Array.isArray(p)) {
466
470
  const m = p.defaultMode;
467
- if (m !== undefined && m !== "default" && m !== "acceptEdits" && m !== "plan" && m !== "bypassPermissions" && m !== "auto") {
468
- sendError(res, 400, "request.field_invalid", 'settings.permissions.defaultMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
471
+ if (m !== undefined && !isSettingsPermissionMode(m)) {
472
+ sendError(res, 400, "request.field_invalid", `settings.permissions.defaultMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
469
473
  return null;
470
474
  }
471
475
  const d = p.disableAutoMode;
@@ -18,7 +18,7 @@ import { projectApprovalsStreamLiveCapability } from "../../capabilities/approva
18
18
  import { workflowModelAllowlistFor, selfOrchestrationDenial } from "../../task-workflow.js";
19
19
  import { centerEntitlementSourceWired } from "../../runtime-caps-resolver.js";
20
20
  import { judgeAutoModeArming } from "../../auto-mode-face.js";
21
- import { coercePermissionModeStrict } from "../../task-settings.js";
21
+ import { coercePermissionModeStrict, SETTINGS_PERMISSION_MODE_WORDS } from "../../task-settings.js";
22
22
  import { gatedPrincipal } from "../principal-gate.js";
23
23
  function routePairingOf(deps, name, m) {
24
24
  if (isSealedKeyPoison(deps.config.modelApiKeys?.[name]))
@@ -52,7 +52,7 @@ export async function handleCapabilities(req, res, match, ctx) {
52
52
  const requestedModeRaw = new URLSearchParams(match.search).get("permissionMode");
53
53
  const requestedMode = requestedModeRaw === null ? undefined : coercePermissionModeStrict(requestedModeRaw);
54
54
  if (requestedModeRaw !== null && requestedMode === undefined) {
55
- sendError(res, 400, "request.field_invalid", 'permissionMode must be one of "default" | "acceptEdits" | "plan" | "bypassPermissions" | "auto"');
55
+ sendError(res, 400, "request.field_invalid", `permissionMode must be one of ${SETTINGS_PERMISSION_MODE_WORDS}`);
56
56
  return;
57
57
  }
58
58
  const entitlementSource = centerEntitlementSourceWired(deps.config);
@@ -13,6 +13,7 @@ const SYNC_NDJSON_LINE_MAX_BYTES = 8 * 1024 * 1024;
13
13
  const SYNC_IMPORT_BATCH = 500;
14
14
  import { SHA256_HEX_RE } from "../../digest-form.js";
15
15
  import { assertNever } from "../route-ctx.js";
16
+ import { readSessionRulesRecords, describeSessionRulesRecordProblem } from "../../session-policy-wire.js";
16
17
  export function createSessionSyncLocal() {
17
18
  const pendingStagings = new Map();
18
19
  const importLeases = new Map();
@@ -215,10 +216,15 @@ export async function handleSessionSync(req, res, match, ctx) {
215
216
  sendError(res, 400, "request.field_invalid", "entryIds must be an array of strings");
216
217
  return;
217
218
  }
218
- if (!Array.isArray(body.policy) || !Array.isArray(body.anchors)) {
219
+ if (!Array.isArray(body.anchors)) {
219
220
  sendError(res, 400, "request.body_shape", "malformed import metadata");
220
221
  return;
221
222
  }
223
+ const policyRead = readSessionRulesRecords(body.policy);
224
+ if (policyRead.problem !== undefined) {
225
+ sendError(res, 400, "request.field_invalid", `policy: ${describeSessionRulesRecordProblem(policyRead.problem)}`);
226
+ return;
227
+ }
222
228
  let fileHistory = null;
223
229
  if (body.fileHistory !== undefined && body.fileHistory !== null) {
224
230
  const door = validateFileHistoryExport(body.fileHistory);
@@ -235,7 +241,7 @@ export async function handleSessionSync(req, res, match, ctx) {
235
241
  const resolution = body.resolution;
236
242
  const srcLogDigest = typeof body.logDigest === "string" && body.logDigest.length > 0 ? body.logDigest : undefined;
237
243
  const entryIds = body.entryIds;
238
- const policy = body.policy;
244
+ const policy = policyRead.records;
239
245
  const anchors = body.anchors;
240
246
  const dstIds = (await deps.sessionStorage.listEntryIds?.(sessionId)) ?? null;
241
247
  const relA = classifySyncRelationshipByIds(entryIds, dstIds);
@@ -5,6 +5,7 @@ import { windowMessages, truncateMessageBlobs } from "../../audit.js";
5
5
  import { redactSecrets } from "../../trace/redact.js";
6
6
  import { recordFailOpen } from "../../observability/fail-open.js";
7
7
  import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, MAX_SETTINGS_PERMISSION_RULES, MAX_SETTINGS_ENV_VARS, MAX_SETTINGS_ENV_KEY_CHARS, MAX_SETTINGS_ENV_VALUE_CHARS } from "../../task-settings.js";
8
+ import { readSessionPermissionRules, describeSessionPermissionRulesProblem } from "../../session-policy-wire.js";
8
9
  import { sendJson, sendError, sseHeaders } from "../send.js";
9
10
  import { gatedPrincipal, explicitOperatorOk, runOwnerReadable, sendPrincipalRefusal } from "../principal-gate.js";
10
11
  import { assertNever } from "../route-ctx.js";
@@ -741,25 +742,26 @@ export async function handleSessions(req, res, match, ctx) {
741
742
  sendError(res, 400, "request.invalid_json", "invalid JSON body");
742
743
  return;
743
744
  }
744
- if (typeof body.rules !== "object" || body.rules === null || Array.isArray(body.rules)) {
745
- sendError(res, 400, "request.body_shape", "rules must be a SessionPermissionRules object");
746
- return;
745
+ const read = readSessionPermissionRules(body.rules);
746
+ if (read.problem !== undefined) {
747
+ switch (read.problem.kind) {
748
+ case "not-an-object":
749
+ sendError(res, 400, "request.body_shape", describeSessionPermissionRulesProblem(read.problem));
750
+ return;
751
+ case "field-not-string-array":
752
+ sendError(res, 400, "request.field_invalid", describeSessionPermissionRulesProblem(read.problem));
753
+ return;
754
+ default: {
755
+ const unhandled = read.problem;
756
+ throw new Error(`unhandled SessionPermissionRules problem: ${JSON.stringify(unhandled)}`);
757
+ }
758
+ }
747
759
  }
748
760
  if (body.expectedRev !== undefined && (typeof body.expectedRev !== "number" || !Number.isInteger(body.expectedRev) || body.expectedRev < 0)) {
749
761
  sendError(res, 400, "request.field_invalid", "expectedRev must be a non-negative integer");
750
762
  return;
751
763
  }
752
- const raw = body.rules;
753
- const rules = {};
754
- for (const f of ["toolAllow", "toolDeny", "allowDirs", "commandAllow", "commandDeny"]) {
755
- if (raw[f] === undefined)
756
- continue;
757
- if (!Array.isArray(raw[f]) || !raw[f].every((x) => typeof x === "string")) {
758
- sendError(res, 400, "request.field_invalid", `${f} must be an array of strings`);
759
- return;
760
- }
761
- rules[f] = raw[f];
762
- }
764
+ const rules = read.rules;
763
765
  try {
764
766
  const stored = await store.putRules(sessionId, target, rules, { ...(typeof body.expectedRev === "number" ? { expectedRev: body.expectedRev } : {}), operator });
765
767
  sendJson(res, 200, { rules: stored });
@@ -264,6 +264,18 @@ export interface TaskRequestBody {
264
264
  * it takes the same single-user gate as backgroundShell/hooks (multi-tenant warns + ignores; advertised via
265
265
  * capabilities.retainBackgroundProcesses). Boolean fail-loud at submit; only `true` rides the spec. */
266
266
  retainBackgroundProcesses?: boolean;
267
+ /** S-493(core 7.8.0 [ref] `TaskSpec.excludeAllTools`,cli L-207 / 板 [ref]): **整只工具面卸载**
268
+ * —— 调用方工具、脚手架、手、MCP、skills 一个都不挂,模型这一 run 里没有任何工具可调、终局是纯文本。
269
+ *
270
+ * 🔴 **与 {@link excludeTools} 是两种语义,刻意不折叠**:`"*"` 是一个合法的调用方工具名,所以「卸全部」
271
+ * 不能写成一条通配规则(那会把「卸全部」与「卸一只叫 `*` 的工具」塌成一件事)。两键同带 ⇒ 各自生效
272
+ * (全卸之后名单里那几只本来也不在,不矛盾、不拒)。
273
+ * 只 literal `true` 上 spec(`false` / 缺席 = 名册照旧,不挂键保持 byte-compat —— 挂一个 `false` 上去
274
+ * 等于替引擎重申了一次它自己的缺省);非 boolean 由 HTTP 门 400 `request.field_invalid`,resume 重放
275
+ * defensive DROP。per-run 工具面**收窄**无跨租户面 ⇒ 无租户门(`excludeTools` 同姿势)。
276
+ * 探测:本键在不在受理面由**键闭集**自己回答 —— 没接线的 server 版本对它 400 `request.body_shape`
277
+ * 并逐字列名,所以**不加能力位**(一个键一条探测路,不长第二条)。 */
278
+ excludeAllTools?: boolean;
267
279
  /** [ref]② (core 1.314 工具面控制批): roster 真卸载清单(schema 不上 wire,manifest 收窄)。wire 工具名;
268
280
  * tighten-only(core 继承不变量 union 进子 spawn)。数组-of-string fail-loud at submit。 */
269
281
  excludeTools?: string[];
package/dist/main.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createLogger } from "./observability/logger.js";
2
+ import { buildFatalErrorText } from "./boot/fatal-exit.js";
2
3
  import { runStage01 } from "./boot/stage-01-config.js";
3
4
  import { runStage02 } from "./boot/stage-02-registry-pull.js";
4
5
  import { runStage03 } from "./boot/stage-03-store-backend.js";
@@ -22,7 +23,7 @@ async function main() {
22
23
  await runStage10(s09);
23
24
  }
24
25
  void main().catch((err) => {
25
- createLogger().error("fatal", { err: String(err) });
26
+ createLogger().error("fatal", { err: buildFatalErrorText(err) });
26
27
  process.exit(1);
27
28
  });
28
29
  //# sourceMappingURL=main.js.map