@sema-agent/server 7.88.2 → 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.
- package/dist/boot/fatal-exit.d.ts +3 -0
- package/dist/boot/fatal-exit.js +5 -0
- package/dist/boot/resolve-spec.js +2 -0
- package/dist/boot/stage-06-runners.js +7 -3
- package/dist/brain.js +32 -19
- package/dist/capabilities/center-plugins.d.ts +10 -0
- package/dist/capabilities/center-plugins.js +21 -5
- package/dist/capabilities/memory-notice.d.ts +7 -1
- package/dist/capabilities/memory-notice.js +6 -6
- package/dist/capabilities/team.d.ts +37 -0
- package/dist/config-catalog.js +7 -19
- package/dist/config-center/apply-effective.js +1 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.js +11 -5
- package/dist/hooks/hook-llm.d.ts +25 -0
- package/dist/hooks/hook-llm.js +27 -9
- package/dist/hooks/hook-runner.js +21 -4
- package/dist/http/admission.js +9 -5
- package/dist/http/routes/capabilities.js +2 -2
- package/dist/http/routes/session-sync.js +10 -3
- package/dist/http/routes/sessions.js +16 -14
- package/dist/http/wire-types.d.ts +12 -0
- package/dist/main.js +2 -1
- package/dist/model-route-url.d.ts +27 -3
- package/dist/model-route-url.js +28 -14
- package/dist/plugins/memory-embedder.d.ts +0 -6
- package/dist/plugins/memory-embedder.js +10 -16
- package/dist/plugins/remote-env-adb.d.ts +13 -0
- package/dist/plugins/remote-env-adb.js +42 -9
- package/dist/plugins/remote-env-device.d.ts +39 -3
- package/dist/plugins/remote-env-device.js +107 -31
- package/dist/request-key-closure.js +2 -1
- package/dist/runtime-governance.d.ts +16 -7
- package/dist/runtime-governance.js +27 -7
- package/dist/session-policy-wire.d.ts +103 -0
- package/dist/session-policy-wire.js +66 -0
- package/dist/task-settings.d.ts +12 -2
- package/dist/task-settings.js +7 -5
- package/dist/trace/core-keyset-guard.d.ts +2 -2
- package/dist/trace/project.d.ts +7 -0
- package/dist/trace/project.js +1 -0
- package/dist/trace/redact.d.ts +110 -5
- package/dist/trace/redact.js +71 -12
- package/package.json +2 -2
|
@@ -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
|
})) {
|
|
@@ -7,7 +7,7 @@ import { redactSecrets } from "../trace/redact.js";
|
|
|
7
7
|
import { hasServiceAuth } from "../http/server.js";
|
|
8
8
|
import { workflowModelAllowlistFor } from "../task-workflow.js";
|
|
9
9
|
import { backendOwnsControlPlane } from "../memory-operator-faces.js";
|
|
10
|
-
import { createHookLlm } from "../hooks/hook-llm.js";
|
|
10
|
+
import { createHookLlm, buildHookFailureText } from "../hooks/hook-llm.js";
|
|
11
11
|
import { assertRetentionLaneWirable } from "./retention-lane.js";
|
|
12
12
|
import { assertConsolidationDriverWirable, buildConsolidationValveAudit, createMemoryConsolidationFaces, resolveConsolidationDriverSeat } from "./memory-consolidation.js";
|
|
13
13
|
import { createPerModelAuthSeat } from "./side-query-lane.js";
|
|
@@ -132,7 +132,7 @@ export function runStage06(ctx) {
|
|
|
132
132
|
const runnerTierFrozen = Object.keys(config.tiers).length > 0;
|
|
133
133
|
const { hookModelFor, hookLlm } = createHookLlm({ config, getKeyResolver: () => configCenter.getKeyResolver(), metrics });
|
|
134
134
|
const hookAgentRunner = new Runner({ ...runnerDeps, sessionStore: new TtlSessionStore({ defaultTtlDays: 1 / 24 }) });
|
|
135
|
-
const
|
|
135
|
+
const agentHookCall = async ({ prompt, model, timeoutMs, system }) => {
|
|
136
136
|
const pick = hookModelFor(model);
|
|
137
137
|
if (!pick.ok)
|
|
138
138
|
return pick;
|
|
@@ -150,7 +150,7 @@ export function runStage06(ctx) {
|
|
|
150
150
|
metrics.inc("hook_llm_calls_total", { type: "agent" });
|
|
151
151
|
if (result.terminal.kind !== "completed") {
|
|
152
152
|
const plane = terminalProjection(result.terminal);
|
|
153
|
-
return { ok: false, error: `hook agent ${plane.status}: ${
|
|
153
|
+
return { ok: false, error: `hook agent ${plane.status}: ${plane.errorMessage ?? ""}` };
|
|
154
154
|
}
|
|
155
155
|
return { ok: true, text: result.result ?? "" };
|
|
156
156
|
}
|
|
@@ -158,6 +158,10 @@ export function runStage06(ctx) {
|
|
|
158
158
|
return { ok: false, error: String(e) };
|
|
159
159
|
}
|
|
160
160
|
};
|
|
161
|
+
const hookAgent = async (opts) => {
|
|
162
|
+
const out = await agentHookCall(opts);
|
|
163
|
+
return out.ok ? out : { ...out, error: clipHookErr(buildHookFailureText(out.error)) };
|
|
164
|
+
};
|
|
161
165
|
const clipHookErr = (s) => (s ?? "").slice(0, 300);
|
|
162
166
|
const titleCapable = sessionStore;
|
|
163
167
|
const sessionTitler = config.sessionAutoTitle && titleCapable.setTitleIfNull && titleCapable.probeTitle
|
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 {
|
|
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 {
|
|
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 =
|
|
10
|
+
const entry = routeKeyOf(baseUrl);
|
|
11
11
|
if (entry === "")
|
|
12
12
|
return false;
|
|
13
|
-
return ![config.gatewayBaseUrl, ...config.gatewayFallbackUrls].some((u) =>
|
|
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
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
: {
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
: {
|
|
171
|
+
: {
|
|
172
|
+
reactiveDegradeRoutePairing: `broken:${verdict.code}`,
|
|
173
|
+
reactiveDegradeRouteRefusal: buildDisplaySafeRefusal(routeRefusalText(verdict), [target.baseUrl ?? "", degradeDeclaredRoot(target.baseUrl, config)]),
|
|
174
|
+
};
|
|
162
175
|
})()),
|
|
163
176
|
};
|
|
164
177
|
}
|
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import type { Logger } from "../observability/logger.js";
|
|
2
2
|
import { type LoadedSkill } from "./skills.js";
|
|
3
|
+
/**
|
|
4
|
+
* 声明面的 `source.url` 长度上限(S-483)—— **界在声明关口,不在每个出口各切一刀**。
|
|
5
|
+
*
|
|
6
|
+
* 由来:出口上原有一刀 `.slice(0, 80)`,而它做了两件坏事(① 把 display-safe 铸点的 `«redacted…»` 记号截成
|
|
7
|
+
* 半只;② 与同一条 URL 在别处的处置成了两把尺)。删掉它之后,这个 **center 下发**值在日志行与
|
|
8
|
+
* `failures[]` 回执上就再没有界 —— 于是界回到它该在的地方:**声明被接收的那一刻**(与 hook 条目的 url
|
|
9
|
+
* 同形 —— `parseHooksConfig` 用 `MAX_HOOK_COMMAND_CHARS` 在解析期就拒)。2048 = URL 的事实上限
|
|
10
|
+
* (同一个数在 `http/routes/sessions.ts` 的对端 endpoint 读面上也在用);超界的 git clone URL 不存在真部署。
|
|
11
|
+
*/
|
|
12
|
+
export declare const MAX_PLUGIN_SOURCE_URL_CHARS = 2048;
|
|
3
13
|
/** 缺省 clone host 白名单(契约条款 1;国内镜像走本名单加 https 镜像域,不开 http 口)。 */
|
|
4
14
|
export declare const DEFAULT_PLUGIN_ALLOW_HOSTS: readonly string[];
|
|
5
15
|
/** wire 上的插件声明(registry-core PluginSpec 的宽读形——本模块只消费不裁决,未知键忽略)。 */
|
|
@@ -3,8 +3,17 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import { promises as fsp, realpathSync, existsSync, readFileSync, lstatSync, statSync } from "node:fs";
|
|
4
4
|
import { join, resolve, isAbsolute, sep } from "node:path";
|
|
5
5
|
import { assertConfined, loadSkills } from "./skills.js";
|
|
6
|
+
import { buildDisplaySafeUrl, redactSecrets } from "../trace/redact.js";
|
|
6
7
|
const execFileP = promisify(execFile);
|
|
8
|
+
export const MAX_PLUGIN_SOURCE_URL_CHARS = 2048;
|
|
7
9
|
export const DEFAULT_PLUGIN_ALLOW_HOSTS = ["github.com"];
|
|
10
|
+
function wireTypeWord(v) {
|
|
11
|
+
if (v === null)
|
|
12
|
+
return "null";
|
|
13
|
+
if (Array.isArray(v))
|
|
14
|
+
return "array";
|
|
15
|
+
return typeof v;
|
|
16
|
+
}
|
|
8
17
|
export function validatePluginDecl(decl, allowHosts) {
|
|
9
18
|
const name = typeof decl.name === "string" && decl.name !== "" ? decl.name : "(unnamed)";
|
|
10
19
|
if (decl.enabled === false)
|
|
@@ -16,8 +25,14 @@ export function validatePluginDecl(decl, allowHosts) {
|
|
|
16
25
|
if (kind !== "git")
|
|
17
26
|
return { ok: false, name, reason: `unsupported source.kind ${String(kind)}` };
|
|
18
27
|
const url = decl.source?.url;
|
|
19
|
-
if (typeof url !== "string"
|
|
20
|
-
return { ok: false, name, reason: `plugin source must be
|
|
28
|
+
if (typeof url !== "string") {
|
|
29
|
+
return { ok: false, name, reason: `plugin source.url must be a string (got ${wireTypeWord(url)})` };
|
|
30
|
+
}
|
|
31
|
+
if (url.length > MAX_PLUGIN_SOURCE_URL_CHARS) {
|
|
32
|
+
return { ok: false, name, reason: `plugin source url exceeds ${MAX_PLUGIN_SOURCE_URL_CHARS} chars (got ${url.length}) — a clone URL that long is a declaration error, and it would ride every log line and failure receipt verbatim` };
|
|
33
|
+
}
|
|
34
|
+
if (!/^https:\/\//i.test(url)) {
|
|
35
|
+
return { ok: false, name, reason: `plugin source must be an https:// URL (contract: https-only; got ${buildDisplaySafeUrl(url)})` };
|
|
21
36
|
}
|
|
22
37
|
let host;
|
|
23
38
|
try {
|
|
@@ -140,13 +155,14 @@ export async function applyCenterPlugins(baseline, eff, opts) {
|
|
|
140
155
|
}
|
|
141
156
|
catch (err) {
|
|
142
157
|
await fsp.rm(staging, { recursive: true, force: true }).catch(() => undefined);
|
|
158
|
+
const safeErr = redactSecrets(err instanceof Error ? err.message : String(err));
|
|
143
159
|
if (existsSync(liveDir)) {
|
|
144
|
-
opts.logger?.warn("center_plugin_fetch_failed_lkg", { plugin: v.name, error:
|
|
160
|
+
opts.logger?.warn("center_plugin_fetch_failed_lkg", { plugin: v.name, error: safeErr });
|
|
145
161
|
out.lkgUsed.push(v.name);
|
|
146
162
|
}
|
|
147
163
|
else {
|
|
148
|
-
opts.logger?.warn("center_plugin_fetch_failed", { plugin: v.name, error:
|
|
149
|
-
out.failures.push({ plugin: v.name, reason: `fetch failed: ${
|
|
164
|
+
opts.logger?.warn("center_plugin_fetch_failed", { plugin: v.name, error: safeErr });
|
|
165
|
+
out.failures.push({ plugin: v.name, reason: `fetch failed: ${safeErr}` });
|
|
150
166
|
continue;
|
|
151
167
|
}
|
|
152
168
|
}
|
|
@@ -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
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return KNOWN_PERSISTENCE_TOOL_NAMES.some((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
|
|
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 {
|
package/dist/config-catalog.js
CHANGED
|
@@ -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`)"),
|
|
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 定)"),
|
|
@@ -72,7 +73,7 @@ export const CONFIG_CATALOG = [
|
|
|
72
73
|
r("ATTACHMENT_ORPHAN_GRACE_MS", "limitsHttp", "number", "附件孤儿对象 GC 宽限(ms;0=关本腿,坏值回默认+warn)", { staticDefault: "3600000", danger: DATA, center: cLimits("attachmentOrphanGraceMs") }),
|
|
73
74
|
r("ATTACHMENT_UNBOUND_TTL_MS", "limitsHttp", "number", "未绑定附件的 TTL(ms)", { staticDefault: String(24 * 3600 * 1000), danger: DATA, configKey: "attachmentUnboundTtlMs", center: cLimits("attachmentUnboundTtlMs") }),
|
|
74
75
|
r("AUTH_BRIDGE_AUDIENCE", "auth", "string", "registry SSO-JWT 验签期望 aud", { danger: SEC }),
|
|
75
|
-
r("AUTH_BRIDGE_ISSUER", "auth", "
|
|
76
|
+
r("AUTH_BRIDGE_ISSUER", "auth", "url", "registry SSO-JWT issuer(auth bridge 的开关之一)", { danger: SEC, derivedDefaultNote: "缺席 ⇒ 用 configCenter.baseUrl(配了 center 即默认开桥)" }),
|
|
76
77
|
r("AUTH_BRIDGE_JWKS_URL", "auth", "url", "registry SSO-JWT 的 JWKS 地址(缺省从 issuer 派生)", { danger: SEC }),
|
|
77
78
|
r("AUTONOMY", "auth", "enum", "运维 autonomy 模式,tighten-only 叠到每个 TaskSpec(read-only/plan ⇒ 只读手,ask ⇒ 恒 shellGate);center 腿热且 non-sticky", { danger: SEC, enumValues: ["read-only", "ask", "plan", "auto"], configKey: "autonomy", center: cGov("autonomy") }),
|
|
78
79
|
r("AUTO_MODE_CLASSIFIER_FALLBACK", "approval", "enum", "auto 分类器席答不出裁决时的梯子(main=主模型代判 / none=只留分类器那一级,整条拒)", { enumValues: ["main", "none"], danger: SEC, derivedDefaultNote: "未设/空串 ⇒ 不铸键,跟随引擎缺省 main(core 7.18.0 起;空串按未设,与其余词表旋钮同判);none = 7.77.0 及以前的行为逐字;词表外的值启动期拒启。权限轴放宽,升级须知见 DEPLOY-PREREQS", configKey: "autoModeClassifierFallback" }),
|
|
@@ -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)", { 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`)", { 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) =>
|
|
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) =>
|
|
504
|
+
return { value: v.map((s) => buildDisplaySafeUrl(s)) };
|
|
517
505
|
return { value: v };
|
|
518
506
|
}
|
|
519
507
|
if (type === "csv" && typeof v === "string")
|
|
@@ -203,7 +203,7 @@ function stageEffective(config, eff, logger, opts) {
|
|
|
203
203
|
if (ok)
|
|
204
204
|
logger?.warn("sema_registry_model_base_url", { model: m.name, baseUrl: buildDisplaySafeUrl(rawBaseUrl), hint: "registry-directed gateway override — this model's key authenticates AGAINST THIS URL" });
|
|
205
205
|
else {
|
|
206
|
-
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl))
|
|
206
|
+
logger?.warn("sema_registry_model_base_url_rejected", { model: m.name, baseUrl: buildDisplaySafeUrl(String(rawBaseUrl)), hint: "only http(s) URLs are honored — falling back to the boot-env gateway" });
|
|
207
207
|
safeUrl = undefined;
|
|
208
208
|
}
|
|
209
209
|
}
|
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
|
|
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)
|
|
@@ -1334,12 +1334,12 @@ function parseMemoryEmbedder(ctx) {
|
|
|
1334
1334
|
throw new Error(`MEMORY_EMBEDDER_* is half-configured — missing ${missing.join(", ")}. The memory vector plane needs all three of MEMORY_EMBEDDER_ENDPOINT, MEMORY_EMBEDDER_MODEL, MEMORY_EMBEDDER_DIM; refusing to start rather than silently running lexical-only retrieval while the deployment believes vectors are on (#228).`);
|
|
1335
1335
|
}
|
|
1336
1336
|
if (!URL.canParse(endpoint)) {
|
|
1337
|
-
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" is not a valid URL (expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint)`);
|
|
1337
|
+
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${buildDisplaySafeUrl(endpoint)}" is not a valid URL (expected e.g. https://embeddings.example.com/v1 — the OpenAI-compatible base, or the full .../embeddings endpoint)`);
|
|
1338
1338
|
}
|
|
1339
1339
|
const parsedEndpoint = new URL(endpoint);
|
|
1340
1340
|
const proto = parsedEndpoint.protocol;
|
|
1341
1341
|
if (proto !== "http:" && proto !== "https:") {
|
|
1342
|
-
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${endpoint}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
|
|
1342
|
+
throw new Error(`MEMORY_EMBEDDER_ENDPOINT="${buildDisplaySafeUrl(endpoint)}" must be http(s) (got "${proto}") — the embedder leg is an HTTP POST to an OpenAI-compatible /embeddings face`);
|
|
1343
1343
|
}
|
|
1344
1344
|
if (parsedEndpoint.username !== "" || parsedEndpoint.password !== "") {
|
|
1345
1345
|
throw new Error("MEMORY_EMBEDDER_ENDPOINT must not carry credentials in the URL (user:password@host) — URL userinfo leaks into transport errors, logs and memory conflict reports. Use MEMORY_EMBEDDER_API_KEY (sent as a Bearer header) instead.");
|
|
@@ -1966,12 +1966,18 @@ function parseA2aServe() {
|
|
|
1966
1966
|
parsed = undefined;
|
|
1967
1967
|
}
|
|
1968
1968
|
if (parsed === undefined || (parsed.protocol !== "http:" && parsed.protocol !== "https:")) {
|
|
1969
|
-
throw new Error(`A2A_SERVE_URL must be a parseable http(s) URL (e.g. "https://agent.example.com/v1/a2a"); got ${JSON.stringify(rawUrl)}`);
|
|
1969
|
+
throw new Error(`A2A_SERVE_URL must be a parseable http(s) URL (e.g. "https://agent.example.com/v1/a2a"); got ${JSON.stringify(buildDisplaySafeUrl(rawUrl))}`);
|
|
1970
1970
|
}
|
|
1971
1971
|
if (parsed.username !== "" || parsed.password !== "") {
|
|
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 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
|
+
`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
|
+
`Keep A2A_SERVE_URL to scheme://host[:port][/path] and move the secret into the caller's headers.`);
|
|
1980
|
+
}
|
|
1975
1981
|
return {
|
|
1976
1982
|
name: env("A2A_SERVE_NAME", "sema-server"),
|
|
1977
1983
|
...(process.env.A2A_SERVE_DESCRIPTION ? { description: process.env.A2A_SERVE_DESCRIPTION } : {}),
|
package/dist/hooks/hook-llm.d.ts
CHANGED
|
@@ -24,6 +24,31 @@ export type HookModelPick = {
|
|
|
24
24
|
ok: false;
|
|
25
25
|
error: string;
|
|
26
26
|
};
|
|
27
|
+
/**
|
|
28
|
+
* 🔴 **hook 失败展示文本的唯一铸点**(S-494;`build*` = 返回纯数据的一只纯函数)。
|
|
29
|
+
*
|
|
30
|
+
* 病(codex 对 7.88.1/7.88.2 的补审抓出,存量泄漏,本车红先复现):这一族失败文本是**拼**出来的 ——
|
|
31
|
+
* 料有四种:core 的路由拒绝文案(`routeRefusalText` 的 `fixHint` 逐字带 `config.baseUrl` 的值)、
|
|
32
|
+
* 原生 `fetch` 的**构造期异常正文**(WHATWG:`…includes credentials: <整条 URL>`,口令逐字)、
|
|
33
|
+
* 它的 `cause`、per-model key 解析错。改前只有**两个 URL 位**走了 display-safe 铸点 —— 那是名单制:
|
|
34
|
+
* 组装料换一种(异常正文、cause)就漏,而它的出口是 `hook_runner` 的 `hook_llm_failed` warn 与
|
|
35
|
+
* `noteHookFailure` 的 stderr 位。⇒ 把尺放到**出口**:整段组装完了过**一次**律,规则从「N 个 URL 位」
|
|
36
|
+
* 减到「一只出口」(那两个 display-safe 调用随之删)。
|
|
37
|
+
*
|
|
38
|
+
* 用的是自由文本那一条律({@link redactSecrets})—— `buildDisplaySafeUrl` 是它加两道 URL 专属闸的
|
|
39
|
+
* **已知是一条 URL** 的那个特例,而这里的料是自由文本(一段异常正文里可能一个 URL 都没有,也可能有三个)。
|
|
40
|
+
* 同一张名表、同一批记号,不是第二把尺。
|
|
41
|
+
*
|
|
42
|
+
* 🔴 **脱敏器抛 ⇒ 响亮扣下**(不回原串、不 fail-open):文案形与 7.88.1 的
|
|
43
|
+
* `fail-open.ts` `renderDetail` 逐字同族(`[… withheld: redactor threw <Name>]`)—— 运维丢掉的是细节、
|
|
44
|
+
* 不是「这次失败了」这件事,而一个字节的原串都不会漏出去。
|
|
45
|
+
*
|
|
46
|
+
* 🚧 **已知射程边界(登记,另车修在源头)**:自由文本律对**非规范拼法**的 URL
|
|
47
|
+
* (`https:alice:pw@h/v1` 省了 `//`、authority 里夹 tab)认不出结构 ⇒ 那两形若出现在料里(目录下发的
|
|
48
|
+
* `models[].baseUrl` 今天原样存,`apply-effective.ts` 只校验 `protocol`)仍会原样。修在源头 =
|
|
49
|
+
* 目录 baseUrl 落库前归一成 `new URL(raw).href`(S-483 收车档未闭环项),不是在这里再加一把尺。
|
|
50
|
+
*/
|
|
51
|
+
export declare function buildHookFailureText(raw: string): string;
|
|
27
52
|
/** The live object `createHookLlm` produces — model-pick resolution + the direct completion call, both capturing the hot-refreshed keyResolver/fetchImpl in closure. */
|
|
28
53
|
export interface HookLlm {
|
|
29
54
|
hookModelFor: (requested?: string) => HookModelPick;
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -1,6 +1,16 @@
|
|
|
1
|
-
import { adjudicateModelRoute,
|
|
1
|
+
import { adjudicateModelRoute, joinRequestUrl, resolveRouteCredential, routeKeyOf, routeRefusalText } from "@sema-agent/core";
|
|
2
2
|
import { matchCatalogModel } from "../model-select.js";
|
|
3
|
-
import {
|
|
3
|
+
import { buildDisplaySafeRefusal, redactSecrets } from "../trace/redact.js";
|
|
4
|
+
export function buildHookFailureText(raw) {
|
|
5
|
+
if (raw === "")
|
|
6
|
+
return raw;
|
|
7
|
+
try {
|
|
8
|
+
return redactSecrets(raw);
|
|
9
|
+
}
|
|
10
|
+
catch (err) {
|
|
11
|
+
return `[hook failure text withheld: redactor threw ${err instanceof Error ? err.name : typeof err}]`;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
4
14
|
export function createHookLlm(deps) {
|
|
5
15
|
const { config, metrics } = deps;
|
|
6
16
|
const anthropicRouteBaseUrl = () => config.anthropic ? config.anthropic.baseUrl || "https://api.anthropic.com" : undefined;
|
|
@@ -43,12 +53,16 @@ export function createHookLlm(deps) {
|
|
|
43
53
|
});
|
|
44
54
|
const verdict = adjudicateModelRoute({ id: pick.catalogRef, baseUrl: pick.baseUrl }, credential, { ...(declaredRoot !== undefined ? { baseUrl: declaredRoot } : {}) });
|
|
45
55
|
if (!verdict.ok) {
|
|
46
|
-
return {
|
|
56
|
+
return {
|
|
57
|
+
ok: false,
|
|
58
|
+
error: `hook model "${pick.catalogRef}" cannot be routed (${verdict.code}) — ` +
|
|
59
|
+
buildDisplaySafeRefusal(routeRefusalText(verdict), [pick.baseUrl, declaredRoot ?? ""]),
|
|
60
|
+
};
|
|
47
61
|
}
|
|
48
|
-
if ((verdict.posture === "unpinned" || verdict.posture === "keyless") &&
|
|
62
|
+
if ((verdict.posture === "unpinned" || verdict.posture === "keyless") && routeKeyOf(pick.baseUrl) !== routeKeyOf(declaredRoot)) {
|
|
49
63
|
return {
|
|
50
64
|
ok: false,
|
|
51
|
-
error: `hook model "${pick.catalogRef}" points at ${
|
|
65
|
+
error: `hook model "${pick.catalogRef}" points at ${pick.baseUrl}, which this deployment did not declare as its own endpoint, ` +
|
|
52
66
|
`and no per-model credential resolved — refusing to send the prompt there (it embeds user content; an unauthenticated ` +
|
|
53
67
|
`request still hands that content to whoever operates that URL)`,
|
|
54
68
|
};
|
|
@@ -56,7 +70,7 @@ export function createHookLlm(deps) {
|
|
|
56
70
|
return { ok: true, credential };
|
|
57
71
|
};
|
|
58
72
|
const fetchImpl = deps.fetchImpl ?? fetch;
|
|
59
|
-
const
|
|
73
|
+
const callLlm = async ({ prompt, model, timeoutMs, system, maxOutputTokens }) => {
|
|
60
74
|
const pick = hookModelFor(model);
|
|
61
75
|
if (!pick.ok)
|
|
62
76
|
return pick;
|
|
@@ -81,7 +95,7 @@ export function createHookLlm(deps) {
|
|
|
81
95
|
const routed = judgeRoute(pick, perModelKey, config.anthropic?.authToken ?? config.anthropic?.apiKey, routeBase);
|
|
82
96
|
if (!routed.ok)
|
|
83
97
|
return routed;
|
|
84
|
-
url =
|
|
98
|
+
url = joinRequestUrl(pick.baseUrl, "/v1/messages");
|
|
85
99
|
headers = {
|
|
86
100
|
"content-type": "application/json",
|
|
87
101
|
"anthropic-version": config.anthropic?.version ?? "2023-06-01",
|
|
@@ -102,7 +116,7 @@ export function createHookLlm(deps) {
|
|
|
102
116
|
if (!routed.ok)
|
|
103
117
|
return routed;
|
|
104
118
|
const hookKey = routed.credential.apiKey;
|
|
105
|
-
url =
|
|
119
|
+
url = joinRequestUrl(pick.baseUrl, "/chat/completions");
|
|
106
120
|
headers = { "content-type": "application/json", ...(hookKey ? { authorization: `Bearer ${hookKey}` } : {}) };
|
|
107
121
|
body = JSON.stringify({ model: pick.providerModelId, ...(maxOutputTokens !== undefined ? { max_tokens: maxOutputTokens } : {}), messages: [...(system ? [{ role: "system", content: system }] : []), { role: "user", content: prompt }], stream: false });
|
|
108
122
|
}
|
|
@@ -126,13 +140,17 @@ export function createHookLlm(deps) {
|
|
|
126
140
|
const cause = e?.cause;
|
|
127
141
|
return {
|
|
128
142
|
ok: false,
|
|
129
|
-
error: ac.signal.aborted ? "hook llm timeout" : `hook llm request to ${
|
|
143
|
+
error: ac.signal.aborted ? "hook llm timeout" : `hook llm request to ${pick.baseUrl} failed: ${String(e)}${cause ? ` (${String(cause)})` : ""}`,
|
|
130
144
|
};
|
|
131
145
|
}
|
|
132
146
|
finally {
|
|
133
147
|
clearTimeout(t);
|
|
134
148
|
}
|
|
135
149
|
};
|
|
150
|
+
const hookLlm = async (opts) => {
|
|
151
|
+
const out = await callLlm(opts);
|
|
152
|
+
return out.ok ? out : { ...out, error: buildHookFailureText(out.error) };
|
|
153
|
+
};
|
|
136
154
|
return { hookModelFor, hookLlm };
|
|
137
155
|
}
|
|
138
156
|
//# sourceMappingURL=hook-llm.js.map
|