@sema-agent/server 3.21.0 → 3.22.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/approval-hmac.d.ts +9 -9
- package/dist/approval-hmac.js +0 -31
- package/dist/approval.js +8 -1
- package/dist/boot/config-center.d.ts +3 -2
- package/dist/boot/resolve-spec.js +4 -4
- package/dist/boot/session-faces.js +3 -2
- package/dist/boot/workflow-orchestration.js +1 -1
- package/dist/budget.d.ts +2 -2
- package/dist/budget.js +11 -6
- package/dist/hooks/hook-llm.d.ts +2 -2
- package/dist/hooks/hook-llm.js +1 -1
- package/dist/http/principal-gate.d.ts +7 -3
- package/dist/http/principal-gate.js +7 -5
- package/dist/http/route-ctx.d.ts +34 -25
- package/dist/http/routes/approvals-assistant.js +5 -5
- package/dist/http/routes/images.js +8 -8
- package/dist/http/server.d.ts +2 -2
- package/dist/http/server.js +2 -2
- package/dist/key-resolver.d.ts +7 -1
- package/dist/key-resolver.js +0 -23
- package/dist/leader/wire.d.ts +19 -1
- package/dist/leader/wire.js +48 -18
- package/dist/main.js +2 -2
- package/dist/parked-decide.js +4 -1
- package/dist/plugins/file-run-store.js +5 -5
- package/dist/plugins/host-platform.d.ts +11 -24
- package/dist/plugins/host-platform.js +14 -0
- package/dist/plugins/memory-engine-tidb.js +16 -0
- package/dist/plugins/memory-run-store.js +5 -5
- package/dist/plugins/remote-env-adb.js +12 -5
- package/dist/plugins/remote-env-local-docker.js +3 -7
- package/dist/plugins/remote-env-ssh.js +17 -2
- package/dist/plugins/run-store-sql.js +12 -12
- package/dist/plugins/store-backend.d.ts +1 -1
- package/dist/plugins/store-backend.js +2 -2
- package/dist/plugins/tool-result-store-sql.d.ts +7 -1
- package/dist/plugins/tool-result-store-sql.js +9 -8
- package/dist/plugins/workflow-run-store-sql.d.ts +11 -6
- package/dist/plugins/workflow-run-store-sql.js +18 -8
- package/dist/session-sync.js +6 -3
- package/package.json +1 -1
package/dist/approval-hmac.d.ts
CHANGED
|
@@ -30,13 +30,19 @@ import type { ApprovalHmacKey } from "./auth-keys.js";
|
|
|
30
30
|
* 之所以到今天才把规则写死:此前四个字段全是**受限字母表**(uuid / 调用 id / 十六进制 / 枚举),转义怎么
|
|
31
31
|
* 写都一样、规则不写也测不出来;`reason` 是第一个进入签名载荷的自由文本,转义从"无所谓"变成"承重"。
|
|
32
32
|
*/
|
|
33
|
-
|
|
33
|
+
/** The canonical 5-field envelope both {@link approvalHmacMessage} (the signer's/verifier's shared byte-rule) and
|
|
34
|
+
* {@link verifyApprovalHmac} take — ONE named type so the two signatures cannot silently drift into two
|
|
35
|
+
* structurally-similar-but-not-identical inline shapes (drift there would only surface at RUNTIME as a MAC
|
|
36
|
+
* mismatch, never at compile time — see the file-header canonical-message note for why each field's presence
|
|
37
|
+
* is load-bearing). */
|
|
38
|
+
export interface ApprovalEnvelope {
|
|
34
39
|
sessionId: string;
|
|
35
40
|
boundCallId?: string | null;
|
|
36
41
|
boundInputHash?: string | null;
|
|
37
42
|
decision: string;
|
|
38
43
|
reason?: string | null;
|
|
39
|
-
}
|
|
44
|
+
}
|
|
45
|
+
export declare function approvalHmacMessage(env: ApprovalEnvelope): string;
|
|
40
46
|
/** 签名载荷里 `reason` 的字符上限。**拒**而不是截 —— 截断在这条路径上**结构上不可行**:服务端截过的字节
|
|
41
47
|
* 与签名方签的字节必然不同,MAC 恒不匹配,"截断"只会把一个可诊断的 413 变成一个费解的 401。
|
|
42
48
|
* 取 4096 与本仓既有的人写文本上限同档(`MAX_ELICIT_MESSAGE_CHARS` / `MAX_AGENT_TEXT_CHARS`)。 */
|
|
@@ -47,11 +53,5 @@ export declare const MAX_APPROVAL_REASON_CHARS = 4096;
|
|
|
47
53
|
* signed with a key being rotated out still verifies during the overlap). `timingSafeEqual` avoids a timing
|
|
48
54
|
* oracle. An empty key-set ⇒ false (caller decides whether absence = skip-because-inactive or fail-closed).
|
|
49
55
|
*/
|
|
50
|
-
export declare function verifyApprovalHmac(env:
|
|
51
|
-
sessionId: string;
|
|
52
|
-
boundCallId?: string | null;
|
|
53
|
-
boundInputHash?: string | null;
|
|
54
|
-
decision: string;
|
|
55
|
-
reason?: string | null;
|
|
56
|
-
}, mac: string, kid: string | undefined, keys: ApprovalHmacKey[]): boolean;
|
|
56
|
+
export declare function verifyApprovalHmac(env: ApprovalEnvelope, mac: string, kid: string | undefined, keys: ApprovalHmacKey[]): boolean;
|
|
57
57
|
//# sourceMappingURL=approval-hmac.d.ts.map
|
package/dist/approval-hmac.js
CHANGED
|
@@ -25,37 +25,6 @@
|
|
|
25
25
|
* already-resolved token → the resolve CAS fails already_resolved), so no separate replay cache is needed.
|
|
26
26
|
*/
|
|
27
27
|
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
28
|
-
/**
|
|
29
|
-
* The canonical message an approval MAC signs — a fixed-order, JSON-escaped tuple so no field value (an id, a
|
|
30
|
-
* hash, a free-text note) can ambiguate the boundary with the next (same discipline as the approvals-stream diff
|
|
31
|
-
* key). 🔴 Bound to CLIENT-VISIBLE values only: `sessionId` (in the /decide URL) + `boundCallId`/`boundInputHash`
|
|
32
|
-
* (surfaced by listPending) + `reason` (the caller's own body). It must NOT include the checkpointToken — that is
|
|
33
|
-
* the unexposed resume credential the client can never see (it resumes by sessionId), so a checkpointToken-bound
|
|
34
|
-
* message would be uncomputable by the signer.
|
|
35
|
-
*
|
|
36
|
-
* ── `reason` 入签(三方裁 (a):cli [1797] / core [1801]§四 / server;clay「干净切」令 [1802])─────────────
|
|
37
|
-
* `reason` 是随决定落账的自由文本(审计面的一部分)。它此前**在信封之外** ⇒ 能改线上字节的一方(反代 /
|
|
38
|
-
* 被劫持的客户端 —— 正是 HMAC 要防的威胁模型)可以在操作员批准同一个动作的那次决定里,把落账理由换成
|
|
39
|
-
* 别的:动作没变,**审计记录变了**。三方裁定纳入签名载荷,不留 `reasonUnsigned` 宽限代际(未上生产)。
|
|
40
|
-
*
|
|
41
|
-
* ── 🔴 字节构造规则(权威定义;跨仓签名器照此实现,docs/ASSISTANT-WIRE-CONTRACT.md §6.2 同源)──────────
|
|
42
|
-
* 被 HMAC 的是下面这个字符串的 **UTF-8 字节**:
|
|
43
|
-
* 1. **恒 5 元素、定序**的紧凑 JSON 数组(元素间**无空格**):
|
|
44
|
-
* `[sessionId, boundCallId|null, boundInputHash|null, decision, reason|null]`
|
|
45
|
-
* —— 恒定长度是刻意的:签名器里少一个"有 reason 才追加"的条件分支,就少一类跨实现漂移。
|
|
46
|
-
* 2. 缺席 ⇒ `null`;**`""`(空串)是与 `null` 不同的被签值**(否则"把理由抹成空"不改变 MAC)。
|
|
47
|
-
* 3. 字符串转义 = RFC 8259 最小集:`"`→`\"`、`\`→`\\`、U+0000–U+001F → `\n`/`\t`/`\uXXXX`。
|
|
48
|
-
* 4. **非 ASCII 原样输出 UTF-8,不转 `\uXXXX`**。⚠️ Python 的 `json.dumps` 缺省 `ensure_ascii=True` 会踩
|
|
49
|
-
* —— 必须 `json.dumps(t, ensure_ascii=False, separators=(",", ":"))`。
|
|
50
|
-
* 5. **`<` `>` `&` 不转义**。⚠️ Go 的 `json.Marshal` 缺省 HTML 转义会踩 —— 必须
|
|
51
|
-
* `enc := json.NewEncoder(w); enc.SetEscapeHTML(false)`(并去掉它追加的换行)。
|
|
52
|
-
* 6. 孤代理项按 ES2019 well-formed 语义转成 `\udXXX`(JS 原生行为,其它语言按同规则)。
|
|
53
|
-
* 7. MAC = HMAC-SHA256(上述字节),**小写十六进制**上 wire。
|
|
54
|
-
* 逐字节黄金向量(含中文/引号/换行/制表/`<`/`&`/emoji)钉在 `test/approval-hmac.test.ts`,签名器对拍用。
|
|
55
|
-
*
|
|
56
|
-
* 之所以到今天才把规则写死:此前四个字段全是**受限字母表**(uuid / 调用 id / 十六进制 / 枚举),转义怎么
|
|
57
|
-
* 写都一样、规则不写也测不出来;`reason` 是第一个进入签名载荷的自由文本,转义从"无所谓"变成"承重"。
|
|
58
|
-
*/
|
|
59
28
|
export function approvalHmacMessage(env) {
|
|
60
29
|
return JSON.stringify([env.sessionId, env.boundCallId ?? null, env.boundInputHash ?? null, env.decision, env.reason ?? null]);
|
|
61
30
|
}
|
package/dist/approval.js
CHANGED
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
* dropped at the task deadline instead of holding the worker — design/06 F4 self-protection.
|
|
9
9
|
*/
|
|
10
10
|
import { createApprovalPolicy, canonicalToolName, uuidv7 } from "@sema-agent/core";
|
|
11
|
+
import { createLogger } from "./observability/logger.js";
|
|
12
|
+
const logger = createLogger();
|
|
11
13
|
/**
|
|
12
14
|
* 运维**是否表达了门意图** —— boot 的单用户 allow-all 基线只在"零门意图"时才允许铺开,
|
|
13
15
|
* 所以这个谓词漏一格 = 那一格的意图被 allow-all 静默吞掉。
|
|
@@ -170,7 +172,12 @@ export async function waitForDecision(store, id, scope, signal, pollMs) {
|
|
|
170
172
|
let consecutiveErrors = 0;
|
|
171
173
|
for (;;) {
|
|
172
174
|
if (signal?.aborted) {
|
|
173
|
-
|
|
175
|
+
// C1(failloud):最小对症=失败留痕,不必重试——这是最后一次尝试把行标 expired,任务本身已经在
|
|
176
|
+
// abort 路径上收尾,重试没有下一个时机。裸吞会让一次真实的店写故障(TiDB blip)在观测面上
|
|
177
|
+
// 与"成功标了 expired"毫无区别;补 error 留痕,行为(deny)不变。
|
|
178
|
+
await store.decide(id, scope, "expired", "task aborted before decision", null).catch((e) => {
|
|
179
|
+
logger.error("approval_expire_write_failed", { id, scope, err: e instanceof Error ? e.message : String(e) });
|
|
180
|
+
});
|
|
174
181
|
return false;
|
|
175
182
|
}
|
|
176
183
|
let s;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
import { CenterPromptSource
|
|
1
|
+
import { CenterPromptSource } from "@sema-agent/core";
|
|
2
2
|
import { buildPricing } from "../budget.js";
|
|
3
3
|
import { type PromptsDomainFaces } from "../capabilities/center-prompts.js";
|
|
4
4
|
import { type Scenario, type ScenarioDeps, type ScenarioDetail } from "../capabilities/scenarios.js";
|
|
5
5
|
import type { LoadedSkill } from "../capabilities/skills.js";
|
|
6
6
|
import { type ServiceConfig } from "../config.js";
|
|
7
|
+
import { type ModelKeyRef } from "../key-resolver.js";
|
|
7
8
|
import type { Logger } from "../observability/logger.js";
|
|
8
9
|
import type { Metrics } from "../observability/metrics.js";
|
|
9
10
|
import { type RestartSignal } from "../config-center/facade.js";
|
|
@@ -42,7 +43,7 @@ export interface ConfigCenterRuntime {
|
|
|
42
43
|
pricing: ReturnType<typeof buildPricing>;
|
|
43
44
|
}): void;
|
|
44
45
|
/** 晚绑取值:refresh 热应用会整个换引用(hook LLM / resolveSpec 每次现取)。 */
|
|
45
|
-
getKeyResolver(): ((model:
|
|
46
|
+
getKeyResolver(): ((model: ModelKeyRef) => Promise<{
|
|
46
47
|
apiKey: string;
|
|
47
48
|
} | undefined>) | undefined;
|
|
48
49
|
/** 晚绑取值:center 提示词面(热采用,新任务边界生效)。 */
|
|
@@ -30,7 +30,7 @@ import { applyLongtailDefer } from "../capabilities/tool-defer.js";
|
|
|
30
30
|
import { acceptShellScratchpadDir, buildEnvFacts, egressForRemoteExec, ensureScratchpadDir, resumeFactsForLane } from "../env-facts.js";
|
|
31
31
|
import { FleetEventBus } from "../fleet/fleet-bus.js";
|
|
32
32
|
import { composeHooks, createTaskHooks } from "../hooks/hook-runner.js";
|
|
33
|
-
import {
|
|
33
|
+
import { explicitOperatorOk } from "../http/server.js";
|
|
34
34
|
import { matchCatalogModel, modelSupportsImages, resolveTaskModel } from "../model-select.js";
|
|
35
35
|
import { PerTaskImageRegistry, resolveSandboxImageRef } from "../per-task-image.js";
|
|
36
36
|
import { isRemoteScratchpadLane, remoteScratchpadDirFor } from "../plugins/remote-scratchpad.js";
|
|
@@ -533,7 +533,7 @@ export function createResolveSpec(ctx) {
|
|
|
533
533
|
// design/131 (core 1.246): per-task resilience INTENT flags. allowDegrade/allowFailover are
|
|
534
534
|
// caller-facing (a bench/eval run wants true failure shapes); bypassBreaker is operator-only (normalizer
|
|
535
535
|
// drops it for non-operators — it punches through a SHARED breaker). All-absent = byte-compat.
|
|
536
|
-
resilience: normalizeResilience(body.resilience,
|
|
536
|
+
resilience: normalizeResilience(body.resilience, explicitOperatorOk(auth?.principal, config.operatorPrincipals)),
|
|
537
537
|
// design/132 (core 1.249): one-shot end-game verification nudge ("re-run the final artifact through
|
|
538
538
|
// its real entrypoint before finishing"). OPT-IN by core's own judgment (default ON lost the evidence case:
|
|
539
539
|
// +1 turn on every interactive write task) — the AUTONOMY caller declares it (harness/scheduler lanes).
|
|
@@ -962,11 +962,11 @@ export function createResolveSpec(ctx) {
|
|
|
962
962
|
profile: taskImageProfile,
|
|
963
963
|
...(capsNeeded && capsNeeded.length > 0 ? { capabilitiesNeeded: capsNeeded } : {}),
|
|
964
964
|
// principal comes from the auth channel (trusted header / verified JWT), NEVER the body — so a caller
|
|
965
|
-
// cannot widen its own visibility.
|
|
965
|
+
// cannot widen its own visibility. explicitOperatorOk (NOT isOperator): an empty OPERATOR_PRINCIPALS must
|
|
966
966
|
// yield operator=false here — isOperator([],p)=true-for-all would let any caller resolve tenant-scoped
|
|
967
967
|
// images on an operator-less deployment (adversarial-review HIGH-1; the bake/direct-door boot guards do
|
|
968
968
|
// NOT cover per-task image selection).
|
|
969
|
-
viewer: { operator:
|
|
969
|
+
viewer: { operator: explicitOperatorOk(auth.principal, config.operatorPrincipals), tenantId: auth.principal ?? null },
|
|
970
970
|
index: imageIndex,
|
|
971
971
|
});
|
|
972
972
|
if (!resolved.ok)
|
|
@@ -63,8 +63,9 @@ export function createSessionFaces(ctx) {
|
|
|
63
63
|
})
|
|
64
64
|
: undefined;
|
|
65
65
|
// S2 fast path([1208]③ 兑现):同副本 append 落点(tidb/pg persist post-commit)经 leaf-bus 直推
|
|
66
|
-
// notifyLocal——单副本部署零延迟;owner
|
|
67
|
-
// (
|
|
66
|
+
// notifyLocal——单副本部署零延迟;owner 随 bus 传(commit-time row owner,session-leaf-bus 头注契约:
|
|
67
|
+
// "Owner IS carried"),notifyLocal 据此做 generation 围栏(旧 A 世代不会假匹配新租户的叶子)。
|
|
68
|
+
// local lane 无写钩,探针道照旧。
|
|
68
69
|
setLeafAdvanceListener(sessionWatchRegistry ? (sid, leaf, owner, seq) => sessionWatchRegistry.notifyLocal(sid, leaf, owner, seq) : undefined);
|
|
69
70
|
const purgeSession = ownerAware.deleteSession
|
|
70
71
|
? async (sessionId, owner) => {
|
|
@@ -33,7 +33,7 @@ export function createWorkflowOrchestration(ctx) {
|
|
|
33
33
|
// present — a failover-landed session sees the run's history + its pending completion push on ANY replica.
|
|
34
34
|
// Explicit WORKFLOW_RUN_STORE=file/memory still wins (single-box File posture unchanged: local backend has
|
|
35
35
|
// no workflowRun() so auto falls to File there).
|
|
36
|
-
const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.() : undefined;
|
|
36
|
+
const sqlWorkflowRunStore = config.workflowRunStoreBackend === "auto" ? backend?.workflowRun?.((msg, meta) => logger.warn(msg, meta)) : undefined; // C5: oversize-slim 留痕接 logger(completionInbox :82 同款)
|
|
37
37
|
const baseWorkflowRunStore = config.selfOrchestrationEnabled
|
|
38
38
|
? config.workflowRunStoreBackend === "memory"
|
|
39
39
|
? new InMemoryWorkflowRunStore()
|
package/dist/budget.d.ts
CHANGED
|
@@ -83,7 +83,7 @@ export { type PromptManifestRecord, promptManifestRecordOf, configAssembledRecor
|
|
|
83
83
|
* fires for EVERY call (top-level + async + council sub-tasks) → `model_cost_micro_usd_total` is the authoritative
|
|
84
84
|
* spend, and (when a {@link ModelUsageTracker} is supplied) the per-task `modelUsage` echo's source.
|
|
85
85
|
*/
|
|
86
|
-
export declare function createTracer(metrics: Pick<Metrics, "inc" | "observe">, costQuota?: QuotaTracker, modelUsage?: ModelUsageTracker, fleetUsage?: FleetUsageAccumulator, fleetLease?: FleetLeaseManager, quotaWeightFor?: (model: string) => number, promptManifests?: PromptManifestTracker): TracerHook;
|
|
86
|
+
export declare function createTracer(metrics: Pick<Metrics, "inc" | "observe">, costQuota?: QuotaTracker, modelUsage?: ModelUsageTracker, fleetUsage?: Pick<FleetUsageAccumulator, "record">, fleetLease?: Pick<FleetLeaseManager, "recordSpend">, quotaWeightFor?: (model: string) => number, promptManifests?: PromptManifestTracker): TracerHook;
|
|
87
87
|
/**
|
|
88
88
|
* [1469] side-query 记账 seam(codex R3 high:sideQuery 不走 core tracer——brain.stream 直调不发
|
|
89
89
|
* brain.call——createTracer 的四路 sink(costQuota/model_cost 指标/fleetUsage 批报/fleetLease 本地扣减)
|
|
@@ -106,7 +106,7 @@ export declare function cacheFamilyOfMirror(model: {
|
|
|
106
106
|
promptCacheFamily?: string;
|
|
107
107
|
};
|
|
108
108
|
} | undefined): "input-includes-cached" | "input-excludes-cached" | undefined;
|
|
109
|
-
export declare function createSideQueryAccountant(metrics: Pick<Metrics, "inc">, costQuota?: QuotaTracker, fleetUsage?: FleetUsageAccumulator, fleetLease?: FleetLeaseManager, quotaWeightFor?: (model: string) => number): (principal: string | undefined, r: {
|
|
109
|
+
export declare function createSideQueryAccountant(metrics: Pick<Metrics, "inc">, costQuota?: QuotaTracker, fleetUsage?: Pick<FleetUsageAccumulator, "record">, fleetLease?: Pick<FleetLeaseManager, "recordSpend">, quotaWeightFor?: (model: string) => number): (principal: string | undefined, r: {
|
|
110
110
|
model: string;
|
|
111
111
|
family?: "input-includes-cached" | "input-excludes-cached";
|
|
112
112
|
usage?: {
|
package/dist/budget.js
CHANGED
|
@@ -320,7 +320,9 @@ export function cacheFamilyOfMirror(model) {
|
|
|
320
320
|
return "input-includes-cached";
|
|
321
321
|
return model.api === "anthropic-messages" || model.api === "bedrock-converse-stream" ? "input-excludes-cached" : "input-includes-cached";
|
|
322
322
|
}
|
|
323
|
-
export function createSideQueryAccountant(metrics, costQuota,
|
|
323
|
+
export function createSideQueryAccountant(metrics, costQuota,
|
|
324
|
+
// 依赖面收窄到真消费的方法(§X 同 ModelKeyRef 一式):全形对象照传(结构超集),测试桩零铸型。
|
|
325
|
+
fleetUsage, fleetLease, quotaWeightFor) {
|
|
324
326
|
return (principal, r) => {
|
|
325
327
|
const input = typeof r.usage?.input === "number" ? r.usage.input : 0;
|
|
326
328
|
const output = typeof r.usage?.output === "number" ? r.usage.output : 0;
|
|
@@ -331,8 +333,11 @@ export function createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetL
|
|
|
331
333
|
// 大幅少收(lease 可被 cache-heavy side query 绕穿)。family 未知(catalog 查不到)走**保守臂**
|
|
332
334
|
// (含 cache——OpenAI 族此臂多收,方向与 cost 残差一致:宁多勿少,记档同一条)。
|
|
333
335
|
const promptTokens = r.family === "input-includes-cached" ? input : input + cacheRead + cacheWrite;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
+
// RB-368/#59 同判据(createTracer 上方注释同款纪律):`usage.cost.total` 整键缺席 = 未知(unpriced 部署的
|
|
337
|
+
// side query 真形),不是「花了 $0」——折 0 就是把「不知道」编造成「免费」。已知(哪怕显式 0)才带键。
|
|
338
|
+
const costTotal = typeof r.usage?.cost?.total === "number" ? r.usage.cost.total : undefined;
|
|
339
|
+
const micro = costTotal !== undefined ? Math.round(costTotal * 1_000_000) : undefined;
|
|
340
|
+
if (micro !== undefined && micro > 0) {
|
|
336
341
|
metrics.inc("model_cost_micro_usd_total", { model: r.model }, micro);
|
|
337
342
|
if (principal)
|
|
338
343
|
costQuota?.add(principal, micro);
|
|
@@ -340,9 +345,9 @@ export function createSideQueryAccountant(metrics, costQuota, fleetUsage, fleetL
|
|
|
340
345
|
const qwRaw = quotaWeightFor?.(r.model);
|
|
341
346
|
const qw = typeof qwRaw === "number" && Number.isFinite(qwRaw) && qwRaw > 0 ? qwRaw : 1;
|
|
342
347
|
const weightedTokens = Math.round((promptTokens + output) * qw);
|
|
343
|
-
if (weightedTokens > 0 || micro > 0) {
|
|
344
|
-
fleetUsage?.record(principal, r.model, "", { inputTokens: promptTokens, outputTokens: output, costMicroUsd: micro, weightedTokens, quotaWeightAtUse: qw }); // taskId ""(无 run 语义;accumulator 对 "" 不计 runsSeen);inputTokens=归一化总输入含 cache(fleet-bus 同口径)
|
|
345
|
-
fleetLease?.recordSpend(principal, { weightedTokens, costMicroUsd: micro });
|
|
348
|
+
if (weightedTokens > 0 || (micro !== undefined && micro > 0)) {
|
|
349
|
+
fleetUsage?.record(principal, r.model, "", { inputTokens: promptTokens, outputTokens: output, ...(micro !== undefined ? { costMicroUsd: micro } : {}), weightedTokens, quotaWeightAtUse: qw }); // taskId ""(无 run 语义;accumulator 对 "" 不计 runsSeen);inputTokens=归一化总输入含 cache(fleet-bus 同口径);RB-368 透传缺席
|
|
350
|
+
fleetLease?.recordSpend(principal, { weightedTokens, ...(micro !== undefined ? { costMicroUsd: micro } : {}) }); // RB-368 透传缺席
|
|
346
351
|
}
|
|
347
352
|
};
|
|
348
353
|
}
|
package/dist/hooks/hook-llm.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { ModelKeyRef } from "../key-resolver.js";
|
|
2
2
|
import type { ServiceConfig } from "../config-types.js";
|
|
3
3
|
import type { HookLlmCall } from "./hook-runner.js";
|
|
4
4
|
/**
|
|
@@ -32,7 +32,7 @@ export interface HookLlm {
|
|
|
32
32
|
export declare function createHookLlm(deps: {
|
|
33
33
|
config: ServiceConfig;
|
|
34
34
|
/** 与 brain 同一 hot-refreshed per-model key 面(main.ts 热应用会整个换引用,故取函数不取值)。 */
|
|
35
|
-
getKeyResolver: () => ((model:
|
|
35
|
+
getKeyResolver: () => ((model: ModelKeyRef) => Promise<{
|
|
36
36
|
apiKey: string;
|
|
37
37
|
} | undefined>) | undefined;
|
|
38
38
|
metrics: {
|
package/dist/hooks/hook-llm.js
CHANGED
|
@@ -74,7 +74,7 @@ export function createHookLlm(deps) {
|
|
|
74
74
|
let perModelKey;
|
|
75
75
|
if (keyResolver) {
|
|
76
76
|
try {
|
|
77
|
-
perModelKey = (await keyResolver({ name: pick.catalogRef }))?.apiKey;
|
|
77
|
+
perModelKey = (await keyResolver({ name: pick.catalogRef }))?.apiKey; // ModelKeyRef: legal mint, no cast (B1)
|
|
78
78
|
}
|
|
79
79
|
catch (e) {
|
|
80
80
|
return { ok: false, error: `per-model key resolution for "${pick.catalogRef}" failed — refusing to fall back to the shared route credential (${String(e)})` };
|
|
@@ -14,7 +14,10 @@ export declare function bearerPresentedButUnverified(req: IncomingMessage, confi
|
|
|
14
14
|
export declare function gatedPrincipal(req: IncomingMessage, config: ServiceConfig): string | undefined;
|
|
15
15
|
/** The EXPLICIT operator gate (IMAGE-API-DESIGN.md §P2.4a MUST-FIX) — NOT the bare `isOperator` (whose empty-list
|
|
16
16
|
* "true-for-all" is a world-writable RCE door the instant OPERATOR_PRINCIPALS is empty). Requires a non-empty
|
|
17
|
-
* operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
|
|
17
|
+
* operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
|
|
18
|
+
* 🔴 SINGLE mint point (E1): a byte-identical twin `explicitOperator` used to live below (image-visibility lane)
|
|
19
|
+
* and two more hand-inlined copies sat in approvals-assistant — a future hardening of this judgment would have
|
|
20
|
+
* missed them. All call sites now consume THIS function; do not re-introduce a sibling. */
|
|
18
21
|
export declare function explicitOperatorOk(principal: string | undefined, operatorPrincipals: readonly string[]): boolean;
|
|
19
22
|
/** May `principal` act as an OPERATOR on the F4 approval queue? Empty `operatorPrincipals` = legacy
|
|
20
23
|
* behavior (the shared service token IS the operator boundary → everyone authenticated is an operator).
|
|
@@ -24,6 +27,7 @@ export declare function isOperator(principal: string | undefined, operatorPrinci
|
|
|
24
27
|
* yields `false` (no one is an operator). Use this where empty-operators-means-all would be a SECURITY hole rather
|
|
25
28
|
* than a back-compat convenience: image VISIBILITY scoping (a tenant-scoped image must not leak to every caller
|
|
26
29
|
* just because OPERATOR_PRINCIPALS is unset). Adversarial-review HIGH: `isOperator([], p)=true` made
|
|
27
|
-
* `latestPublished(..., {operator:true})` bypass tenant visibility for any caller on an operator-less deployment.
|
|
28
|
-
|
|
30
|
+
* `latestPublished(..., {operator:true})` bypass tenant visibility for any caller on an operator-less deployment.
|
|
31
|
+
* (The byte-identical `explicitOperator` twin that used to live here was folded into `explicitOperatorOk` above —
|
|
32
|
+
* one judgment, one mint point.) */
|
|
29
33
|
//# sourceMappingURL=principal-gate.d.ts.map
|
|
@@ -61,7 +61,10 @@ export function gatedPrincipal(req, config) {
|
|
|
61
61
|
}
|
|
62
62
|
/** The EXPLICIT operator gate (IMAGE-API-DESIGN.md §P2.4a MUST-FIX) — NOT the bare `isOperator` (whose empty-list
|
|
63
63
|
* "true-for-all" is a world-writable RCE door the instant OPERATOR_PRINCIPALS is empty). Requires a non-empty
|
|
64
|
-
* operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
|
|
64
|
+
* operator set AND a present principal IN it. The same form the preempt/resume endpoints already use.
|
|
65
|
+
* 🔴 SINGLE mint point (E1): a byte-identical twin `explicitOperator` used to live below (image-visibility lane)
|
|
66
|
+
* and two more hand-inlined copies sat in approvals-assistant — a future hardening of this judgment would have
|
|
67
|
+
* missed them. All call sites now consume THIS function; do not re-introduce a sibling. */
|
|
65
68
|
export function explicitOperatorOk(principal, operatorPrincipals) {
|
|
66
69
|
return operatorPrincipals.length > 0 && principal !== undefined && operatorPrincipals.includes(principal);
|
|
67
70
|
}
|
|
@@ -77,8 +80,7 @@ export function isOperator(principal, operatorPrincipals) {
|
|
|
77
80
|
* yields `false` (no one is an operator). Use this where empty-operators-means-all would be a SECURITY hole rather
|
|
78
81
|
* than a back-compat convenience: image VISIBILITY scoping (a tenant-scoped image must not leak to every caller
|
|
79
82
|
* just because OPERATOR_PRINCIPALS is unset). Adversarial-review HIGH: `isOperator([], p)=true` made
|
|
80
|
-
* `latestPublished(..., {operator:true})` bypass tenant visibility for any caller on an operator-less deployment.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
83
|
+
* `latestPublished(..., {operator:true})` bypass tenant visibility for any caller on an operator-less deployment.
|
|
84
|
+
* (The byte-identical `explicitOperator` twin that used to live here was folded into `explicitOperatorOk` above —
|
|
85
|
+
* one judgment, one mint point.) */
|
|
84
86
|
//# sourceMappingURL=principal-gate.js.map
|
package/dist/http/route-ctx.d.ts
CHANGED
|
@@ -75,6 +75,38 @@ export interface RouteRequestState {
|
|
|
75
75
|
/** 凭证派生的调用方系统身份(`source`)。handle() 的鉴权门是**唯一**赋值点,域模块只读。 */
|
|
76
76
|
source: string | null;
|
|
77
77
|
}
|
|
78
|
+
/** B4①(命名化):`prepareSpec` 的返回形(tasks.ts/runs.ts 两域共同消费的属性面)——原为 6 成员内联匿名对象。
|
|
79
|
+
* 命名后好处不只是过 B4 门:两个消费域现在都能对同一个符号做 `import type` 标注,而不是各自重推断结构。 */
|
|
80
|
+
export interface PreparedTaskSubmission {
|
|
81
|
+
spec: TaskSpec;
|
|
82
|
+
auth?: RequestAuth;
|
|
83
|
+
verify?: {
|
|
84
|
+
maxRounds: number;
|
|
85
|
+
costCeilingMicroUsd?: number;
|
|
86
|
+
};
|
|
87
|
+
cascade?: boolean;
|
|
88
|
+
jobId?: string;
|
|
89
|
+
body: TaskRequestBody;
|
|
90
|
+
}
|
|
91
|
+
/** B4①(命名化):`driveResumeIntoRunLog` 的入参形(resume 家族共用)——原为 9 成员内联匿名对象。 */
|
|
92
|
+
export interface DriveResumeArgs {
|
|
93
|
+
token: CheckpointToken;
|
|
94
|
+
sessionId: string;
|
|
95
|
+
principal: string | undefined;
|
|
96
|
+
fleetScope: string;
|
|
97
|
+
taskConfig: Omit<TaskSpec, "objective" | "sessionId">;
|
|
98
|
+
resumeObjective: string;
|
|
99
|
+
outcome: ResumeOutcome;
|
|
100
|
+
verifyRounds: {
|
|
101
|
+
maxRounds: number;
|
|
102
|
+
costCeilingMicroUsd?: number;
|
|
103
|
+
} | undefined;
|
|
104
|
+
/** codex M2: side-effects that must land AFTER the markResuming CAS is WON (decide accepted, sibling races
|
|
105
|
+
* lost — never fires on the 409/404 paths) and BEFORE the model leg drives (so the resumed leg's own next
|
|
106
|
+
* ask sees them — the decide leg's exemption grant). Contract: must not throw (callers swallow internally);
|
|
107
|
+
* awaited so the ordering guarantee is real, and a store write here is ms-scale vs the model leg. */
|
|
108
|
+
onResumeCommitted?: () => Promise<void>;
|
|
109
|
+
}
|
|
78
110
|
/** 提交/续跑「腿」= 跨域共享的**有状态**长流程(不像 {@link RouteHelpers} 那样只依赖 deps/config:它们要写
|
|
79
111
|
* run log、发 fleet 帧、走 markResuming CAS、驱动模型)。A9 上批的停点就在这里——tasks/runs 两域共用
|
|
80
112
|
* `prepareSpec`,approvals/assistant/notify-wake 三域共用 resume 家族,把它们跟着任一域搬都会造出
|
|
@@ -82,17 +114,7 @@ export interface RouteRequestState {
|
|
|
82
114
|
* 装进这一格,域模块经 `ctx.legs.*` 调用。类型化 ⇒ 腿的形漂了是编译红,不是运行时静默 404。 */
|
|
83
115
|
export interface RouteLegs {
|
|
84
116
|
/** POST /v1/tasks · /v1/tasks/stream · /v1/runs 的共同前段:读体 → 校验 → resolveSpec。null = 已应答(400/…)。 */
|
|
85
|
-
prepareSpec(req: IncomingMessage, res: ServerResponse): Promise<
|
|
86
|
-
spec: TaskSpec;
|
|
87
|
-
auth?: RequestAuth;
|
|
88
|
-
verify?: {
|
|
89
|
-
maxRounds: number;
|
|
90
|
-
costCeilingMicroUsd?: number;
|
|
91
|
-
};
|
|
92
|
-
cascade?: boolean;
|
|
93
|
-
jobId?: string;
|
|
94
|
-
body: TaskRequestBody;
|
|
95
|
-
} | null>;
|
|
117
|
+
prepareSpec(req: IncomingMessage, res: ServerResponse): Promise<PreparedTaskSubmission | null>;
|
|
96
118
|
/** 同步腿的终局记账(计费/配额/指标),tasks 域用。 */
|
|
97
119
|
finalizeTaskResult(result: TaskResult, principal: string | undefined, objective: string, sessionId: string | undefined): void;
|
|
98
120
|
/** approvals 决策腿:session → pending checkpoint → markResuming CAS → 驱动续跑。 */
|
|
@@ -101,20 +123,7 @@ export interface RouteLegs {
|
|
|
101
123
|
body: object;
|
|
102
124
|
}>;
|
|
103
125
|
/** 全 resume 家族的共同下半场(lease admission + 写 run log + fleet 发布)。 */
|
|
104
|
-
driveResumeIntoRunLog(args: {
|
|
105
|
-
token: CheckpointToken;
|
|
106
|
-
sessionId: string;
|
|
107
|
-
principal: string | undefined;
|
|
108
|
-
fleetScope: string;
|
|
109
|
-
taskConfig: Omit<TaskSpec, "objective" | "sessionId">;
|
|
110
|
-
resumeObjective: string;
|
|
111
|
-
outcome: ResumeOutcome;
|
|
112
|
-
verifyRounds: {
|
|
113
|
-
maxRounds: number;
|
|
114
|
-
costCeilingMicroUsd?: number;
|
|
115
|
-
} | undefined;
|
|
116
|
-
onResumeCommitted?: () => Promise<void>;
|
|
117
|
-
}): Promise<{
|
|
126
|
+
driveResumeIntoRunLog(args: DriveResumeArgs): Promise<{
|
|
118
127
|
status: number;
|
|
119
128
|
body: object;
|
|
120
129
|
}>;
|
|
@@ -92,7 +92,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
92
92
|
return;
|
|
93
93
|
}
|
|
94
94
|
const operators = deps.config.operatorPrincipals;
|
|
95
|
-
const isExplicitOperator = operators
|
|
95
|
+
const isExplicitOperator = explicitOperatorOk(principal, operators); // E1: single mint point (principal-gate)
|
|
96
96
|
if (!isExplicitOperator) {
|
|
97
97
|
// 🔴 fail-CLOSED owner gate (both reviewers, 2026-07-13): mirror the sibling owner-gated routes
|
|
98
98
|
// (session policy/delete) — ownerOf REQUIRED (absent ⇒ 501, never open), a store error PROPAGATES
|
|
@@ -292,7 +292,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
292
292
|
// + the cancel/decide precedent). NOT the bare isOperator (its empty-list "true-for-all" would let any
|
|
293
293
|
// caller preempt anyone — same trap the /decide owner-gate avoids).
|
|
294
294
|
const operators = deps.config.operatorPrincipals;
|
|
295
|
-
const explicitOperator = operators
|
|
295
|
+
const explicitOperator = explicitOperatorOk(principal, operators); // E1: single mint point (principal-gate)
|
|
296
296
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
297
297
|
sendError(res, 404, "not_found.run", "task not found");
|
|
298
298
|
return;
|
|
@@ -349,7 +349,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
349
349
|
return;
|
|
350
350
|
}
|
|
351
351
|
const operators = deps.config.operatorPrincipals;
|
|
352
|
-
const explicitOperator = operators
|
|
352
|
+
const explicitOperator = explicitOperatorOk(principal, operators); // E1: single mint point (principal-gate)
|
|
353
353
|
if (!explicitOperator && run.owner !== null && run.owner !== principal) {
|
|
354
354
|
sendError(res, 404, "not_found.run", "task not found");
|
|
355
355
|
return;
|
|
@@ -436,7 +436,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
436
436
|
deciderPrincipal = proof.principal;
|
|
437
437
|
}
|
|
438
438
|
const operators = deps.config.operatorPrincipals;
|
|
439
|
-
const explicitOperator = operators
|
|
439
|
+
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators); // E1: single mint point (principal-gate)
|
|
440
440
|
if (!explicitOperator && run.owner !== null && run.owner !== deciderPrincipal) {
|
|
441
441
|
sendError(res, 404, "not_found.run", "task not found");
|
|
442
442
|
return;
|
|
@@ -579,7 +579,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
579
579
|
// credential sessionId+boundInputHash (from a log/screenshot/ticket). A non-owner-non-operator gets 404
|
|
580
580
|
// (no existence oracle — parity with runOwnerOk + the cancel P0 precedent), never 403.
|
|
581
581
|
const operators = deps.config.operatorPrincipals;
|
|
582
|
-
const explicitOperator = operators
|
|
582
|
+
const explicitOperator = explicitOperatorOk(deciderPrincipal, operators); // E1: single mint point (principal-gate)
|
|
583
583
|
if (!explicitOperator) {
|
|
584
584
|
const cpScope = await cs.peekPendingScope(sessionId);
|
|
585
585
|
// "_" = the anonymous-submit scope SENTINEL (main.ts writes auth?.principal ?? "_" at suspend) — treat it
|
|
@@ -5,7 +5,7 @@ import { manifestToIndexEntry } from "../../images/manifest.js";
|
|
|
5
5
|
import { validateBake, buildBakeArgv } from "../../images/bake-validate.js";
|
|
6
6
|
import { streamSseLog } from "../sse-log.js";
|
|
7
7
|
import { sendJson, sendError } from "../send.js";
|
|
8
|
-
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk
|
|
8
|
+
import { headerStr, safeEqual, gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
9
9
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
10
10
|
export function createImagesLocal(deps) {
|
|
11
11
|
// Per-principal bake-submission rate guard (IMAGE-API-DESIGN.md §P2.4c — fills the register DoS gap the images
|
|
@@ -86,7 +86,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
86
86
|
// Identity resolved EXACTLY as P1 (direct-door JWT-without-cnf.bnd, else principalFrom).
|
|
87
87
|
const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
|
|
88
88
|
const operators = deps.config.operatorPrincipals;
|
|
89
|
-
const
|
|
89
|
+
const isExplicitOperator = explicitOperatorOk(principal, operators);
|
|
90
90
|
const cfg = deps.config.imageBakes;
|
|
91
91
|
// 🔴 H1: the bake-runner identity is DERIVED from its bearer CREDENTIAL (the token-derived `source`,
|
|
92
92
|
// resolved above from SERVICE_AUTH_TOKENS), NOT a self-asserted x-agent-principal/JWT the runner never
|
|
@@ -96,7 +96,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
96
96
|
const isRunner = source !== undefined && source !== null && source === cfg.runnerPrincipal;
|
|
97
97
|
// POST /v1/images/bakes — operator-only submit. EXPLICIT operator gate (NOT the bare isOperator; §P2.4a).
|
|
98
98
|
if (req.method === "POST" && url === "/v1/images/bakes") {
|
|
99
|
-
if (!
|
|
99
|
+
if (!isExplicitOperator) {
|
|
100
100
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
@@ -198,7 +198,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
198
198
|
// GET /v1/images/bakes/:bakeId/events — resumable SSE (the live "baking…" view), run-trace-SSE-shaped (§P2.2).
|
|
199
199
|
const evMatch = req.method === "GET" ? BAKE_EVENTS_RE.exec(url) : null;
|
|
200
200
|
if (evMatch) {
|
|
201
|
-
if (!
|
|
201
|
+
if (!isExplicitOperator) {
|
|
202
202
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
203
203
|
return;
|
|
204
204
|
}
|
|
@@ -214,7 +214,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
214
214
|
// POST /v1/images/bakes/:bakeId/cancel — cooperative cancel (durable flag; the runner kills the pgid, §P2.11).
|
|
215
215
|
const cancelMatch = req.method === "POST" ? BAKE_CANCEL_RE.exec(url) : null;
|
|
216
216
|
if (cancelMatch) {
|
|
217
|
-
if (!
|
|
217
|
+
if (!isExplicitOperator) {
|
|
218
218
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
219
219
|
return;
|
|
220
220
|
}
|
|
@@ -337,7 +337,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
337
337
|
// the `/events|/cancel|/claim|/ingest` sub-actions (two-segment) win first.
|
|
338
338
|
const idMatch = req.method === "GET" ? BAKE_ID_RE.exec(url) : null;
|
|
339
339
|
if (idMatch) {
|
|
340
|
-
if (!
|
|
340
|
+
if (!isExplicitOperator) {
|
|
341
341
|
sendError(res, 403, "auth.operator_only", "operator only");
|
|
342
342
|
return;
|
|
343
343
|
}
|
|
@@ -362,10 +362,10 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
362
362
|
if (deps.imageIndex && url.startsWith("/v1/images")) {
|
|
363
363
|
const idx = deps.imageIndex;
|
|
364
364
|
const principal = gatedPrincipal(req, deps.config); // direct-door safe — identity-only proof; single source of truth (see gatedPrincipal)
|
|
365
|
-
//
|
|
365
|
+
// explicitOperatorOk (NOT isOperator): for image VISIBILITY, an empty OPERATOR_PRINCIPALS must NOT make every
|
|
366
366
|
// caller an operator (that would leak tenant-scoped images). Adversarial-review HIGH — same fix as the P0.5
|
|
367
367
|
// per-task resolve in main.ts. (Approval operator gates below keep isOperator's legacy empty=all boundary.)
|
|
368
|
-
const operator =
|
|
368
|
+
const operator = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
369
369
|
const viewer = { operator, tenantId: principal ?? null };
|
|
370
370
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
371
371
|
// GET /v1/images — the paginated catalog. ?profile=&status=&capability=&latest=true&limit=&cursor=
|
package/dist/http/server.d.ts
CHANGED
|
@@ -31,8 +31,8 @@ import { cascadeConfig } from "./run-meta.js";
|
|
|
31
31
|
export { cascadeConfig };
|
|
32
32
|
import { coarseStatusForState, errorCodeForExit } from "./routes/images.js";
|
|
33
33
|
export { coarseStatusForState, errorCodeForExit };
|
|
34
|
-
import { explicitOperatorOk, isOperator
|
|
35
|
-
export { explicitOperatorOk, isOperator
|
|
34
|
+
import { explicitOperatorOk, isOperator } from "./principal-gate.js";
|
|
35
|
+
export { explicitOperatorOk, isOperator };
|
|
36
36
|
/** Per-request authorization context resolved before building the spec (identity, owned session). */
|
|
37
37
|
export interface RequestAuth {
|
|
38
38
|
principal?: string;
|
package/dist/http/server.js
CHANGED
|
@@ -48,8 +48,8 @@ export { coarseStatusForState, errorCodeForExit };
|
|
|
48
48
|
// design/158 A9:装配器缝——发送器/身份门下沉到 http/ 叶子模块,routes/* 与 server.ts 共用同一实现
|
|
49
49
|
// (routes/* 绝不可值 import server.ts:那条边会闭合运行时环,见 test/module-cycle-gate.test.ts)。
|
|
50
50
|
import { sendJson, sendError, httpErrorCode, msg } from "./send.js";
|
|
51
|
-
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator
|
|
52
|
-
export { explicitOperatorOk, isOperator
|
|
51
|
+
import { authorized, systemFor, gatedPrincipal, explicitOperatorOk, isOperator } from "./principal-gate.js";
|
|
52
|
+
export { explicitOperatorOk, isOperator };
|
|
53
53
|
/** 分组入参 → 平铺视图(createHttpServer 的第一件事)。一组都没给 ⇒ 原样返回(存量调用零开销、零形变)。 */
|
|
54
54
|
export function flattenServiceDeps(deps) {
|
|
55
55
|
const { stores, coordinators, seams, observability, governance, deployment, knobs, ...flat } = deps;
|
package/dist/key-resolver.d.ts
CHANGED
|
@@ -23,7 +23,13 @@ import { type SealedKeyPoison } from "./sealed-key.js";
|
|
|
23
23
|
* construction in applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is
|
|
24
24
|
* belt-and-suspenders. 🔴 Plaintext values are live secrets: memory-only, never log them.
|
|
25
25
|
*/
|
|
26
|
-
|
|
26
|
+
/** The ONE field the per-model key plane keys on. The resolver's parameter is deliberately this minimal
|
|
27
|
+
* named ref (not the full core `Model`): full models are structural supersets and pass unchanged, while
|
|
28
|
+
* callers that never materialize a full Model (hook-llm resolves keys for roster entries by catalog name)
|
|
29
|
+
* can mint `{ name }` legally instead of forcing it through an `as never` escape hatch (B1). Contravariance
|
|
30
|
+
* keeps the resolver assignable wherever `(model: Model) => …` is expected. */
|
|
31
|
+
export type ModelKeyRef = Pick<Model, "name">;
|
|
32
|
+
export declare function createKeyResolver(modelApiKeyEnv: Record<string, string>, env?: NodeJS.ProcessEnv, modelApiKeys?: Record<string, string | SealedKeyPoison>): ((model: ModelKeyRef) => Promise<{
|
|
27
33
|
apiKey: string;
|
|
28
34
|
} | undefined>) | undefined;
|
|
29
35
|
/**
|
package/dist/key-resolver.js
CHANGED
|
@@ -1,27 +1,4 @@
|
|
|
1
1
|
import { isSealedKeyPoison, SealedKeyPoisonedError } from "./sealed-key.js";
|
|
2
|
-
/**
|
|
3
|
-
* Build a per-model API-key resolver for `TaskSpec.getApiKeyAndHeaders` from the catalog's
|
|
4
|
-
* name → env-var-NAME map (sema-registry `apiKeyEnv`). core threads this per brain call — and per cascade
|
|
5
|
-
* rung — so each model/rung authenticates with its OWN upstream account/key.
|
|
6
|
-
*
|
|
7
|
-
* Additive by construction: a model absent from the map — or whose env var is unset — returns
|
|
8
|
-
* `undefined`, so core falls back to the brain's construction-time gateway key (today's behavior, no
|
|
9
|
-
* regression). When no per-model keys are configured at all this returns `undefined`, leaving the spec
|
|
10
|
-
* field unset so core's default path is byte-for-byte unchanged.
|
|
11
|
-
*
|
|
12
|
-
* Only the api KEY is per-model; the base URL stays brain-owned (design/15) — core's seam carries
|
|
13
|
-
* `{ apiKey, headers? }`, not a baseUrl. Same-account-rotation could round-robin inside this function.
|
|
14
|
-
*
|
|
15
|
-
* `modelApiKeys` (sealed-box custody) carries per-model entries unsealed from `ModelEntry.sealedApiKey` —
|
|
16
|
-
* resolved FIRST (sealed outranks the env-NAME reference, the registry-core mutual-exclusion ruling). Each
|
|
17
|
-
* entry is either the PLAINTEXT key (successful unseal) or a `SealedKeyPoison` marker (unseal FAILED at
|
|
18
|
-
* apply time): a poisoned model THROWS `SealedKeyPoisonedError` on every resolve — task-level fail-loud —
|
|
19
|
-
* because returning `undefined` here means "use the gateway key", and a broken sealed key must NEVER
|
|
20
|
-
* silently burn the shared gateway account (the core invariant of the poison-pill fix; a poisoned model
|
|
21
|
-
* skips its env reference too, same mutual-exclusion as a healthy sealed key). The maps are disjoint by
|
|
22
|
-
* construction in applyEffective (a sealed model never lands in modelApiKeyEnv); the ordering here is
|
|
23
|
-
* belt-and-suspenders. 🔴 Plaintext values are live secrets: memory-only, never log them.
|
|
24
|
-
*/
|
|
25
2
|
export function createKeyResolver(modelApiKeyEnv, env = process.env, modelApiKeys = {}) {
|
|
26
3
|
if (Object.keys(modelApiKeyEnv).length === 0 && Object.keys(modelApiKeys).length === 0)
|
|
27
4
|
return undefined;
|
package/dist/leader/wire.d.ts
CHANGED
|
@@ -115,7 +115,6 @@ export declare function stageWorkerEnv(env: WireEnv, opts: {
|
|
|
115
115
|
uploadScriptPath: string;
|
|
116
116
|
uploadScript: string;
|
|
117
117
|
}): Promise<void>;
|
|
118
|
-
/** Build the `runLeader(body)` the HTTP endpoint invokes. Constructs real per-task deps + runs the leader. */
|
|
119
118
|
/**
|
|
120
119
|
* Per-leader-run resource bounds derived from env (2026-06-14). Exported for unit tests. The worker sandbox
|
|
121
120
|
* lifetime (BUG2), the worker's diff-upload presigned-URL TTL (BUG1), and the worker spec's triple bound
|
|
@@ -140,6 +139,25 @@ export declare function leaderResourceConfig(env?: NodeJS.ProcessEnv): {
|
|
|
140
139
|
maxSuspends: number;
|
|
141
140
|
};
|
|
142
141
|
};
|
|
142
|
+
/** Repair/conflict/replan-loop knobs derived from env + optional `LeaderWireConfig` fallbacks (`repairRounds`/
|
|
143
|
+
* `conflictRounds` land in cfg when the caller wires them programmatically instead of via env). Split out of
|
|
144
|
+
* `createLeaderRunner`'s closure so it's a plain, unit-testable data constructor. */
|
|
145
|
+
export interface LeaderLoopConfig {
|
|
146
|
+
repairRounds: number;
|
|
147
|
+
repairBudgetUsd: number;
|
|
148
|
+
conflictRounds: number;
|
|
149
|
+
repairLoopOn: boolean;
|
|
150
|
+
measureGatesOn: boolean;
|
|
151
|
+
repairLoopAttempts: number;
|
|
152
|
+
oracleFlakyK: number;
|
|
153
|
+
/** replan-lite (design/68 §6) fan-out spend cap from LEADER_BUDGET_USD. Absent = replan-lite's own default
|
|
154
|
+
* (unset env stays absent — existing contract untouched; see leaderLoopConfig doc below). */
|
|
155
|
+
replanBudgetUsd?: number;
|
|
156
|
+
}
|
|
157
|
+
export declare function leaderLoopConfig(cfg: {
|
|
158
|
+
repairRounds?: number;
|
|
159
|
+
conflictRounds?: number;
|
|
160
|
+
}, env?: NodeJS.ProcessEnv): LeaderLoopConfig;
|
|
143
161
|
export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
|
|
144
162
|
export {};
|
|
145
163
|
//# sourceMappingURL=wire.d.ts.map
|