@sema-agent/server 4.3.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/USAGE.md +5 -3
- package/dist/approval-hmac.js +1 -1
- package/dist/approval.d.ts +5 -37
- package/dist/approval.js +12 -112
- package/dist/auth-bridge.d.ts +0 -1
- package/dist/auth-bridge.js +0 -1
- package/dist/auth-keys.d.ts +1 -1
- package/dist/auth-keys.js +1 -1
- package/dist/bench/s1/run-firm.js +1 -1
- package/dist/boot/config-center.d.ts +1 -1
- package/dist/boot/config-center.js +3 -2
- package/dist/boot/reapers.d.ts +1 -2
- package/dist/boot/reapers.js +4 -5
- package/dist/boot/resolve-spec.d.ts +1 -3
- package/dist/boot/resolve-spec.js +21 -35
- package/dist/boot/stores.js +1 -1
- package/dist/brain.js +3 -1
- package/dist/capabilities/center-prompts.d.ts +2 -3
- package/dist/capabilities/center-prompts.js +2 -3
- package/dist/capabilities/sandbox-file-send.d.ts +0 -2
- package/dist/capabilities/sandbox-file-send.js +2 -5
- package/dist/capabilities/scenarios.d.ts +0 -9
- package/dist/capabilities/scenarios.js +1 -20
- package/dist/capabilities/send-user-file-tool.js +3 -3
- package/dist/config-center/apply-effective.js +8 -8
- package/dist/config-center/types.d.ts +10 -3
- package/dist/config-provider.js +6 -11
- package/dist/config-types.d.ts +12 -26
- package/dist/config.js +95 -68
- package/dist/fleet/fleet-bus.js +25 -33
- package/dist/fleet-client.js +1 -1
- package/dist/http/active-run-conflict.d.ts +2 -0
- package/dist/http/active-run-conflict.js +1 -0
- package/dist/http/routes/approvals-assistant.d.ts +2 -1
- package/dist/http/routes/approvals-assistant.js +10 -96
- package/dist/http/routes/capabilities.js +6 -5
- package/dist/http/routes/fleet.js +4 -1
- package/dist/http/routes/images.js +10 -4
- package/dist/http/routes/runs.js +34 -49
- package/dist/http/routes/session-sync.d.ts +1 -1
- package/dist/http/routes/session-sync.js +2 -1
- package/dist/http/routes/tasks.js +7 -7
- package/dist/http/routes/trace-usage.js +1 -1
- package/dist/http/server.d.ts +1 -6
- package/dist/http/server.js +15 -10
- package/dist/http/verify-rounds.d.ts +2 -2
- package/dist/http/wire-types.d.ts +1 -1
- package/dist/images/bake-validate.d.ts +0 -22
- package/dist/images/bake-validate.js +0 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +2 -5
- package/dist/main.js +5 -9
- package/dist/memory-scope.js +2 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +6 -4
- package/dist/orchestration/workflow-completion-inbox.js +6 -4
- package/dist/plugins/breaker-state-sql.d.ts +2 -3
- package/dist/plugins/image-index-sql.d.ts +0 -1
- package/dist/plugins/image-index-sql.js +0 -1
- package/dist/plugins/local-checkpoint-store.d.ts +1 -1
- package/dist/plugins/local-checkpoint-store.js +1 -1
- package/dist/plugins/memory-engine-pg.js +2 -1
- package/dist/plugins/memory-engine-tidb.js +1 -1
- package/dist/plugins/memory-engine-vector-util.d.ts +2 -3
- package/dist/plugins/memory-engine-vector-util.js +2 -3
- package/dist/plugins/memory-run-store.d.ts +1 -1
- package/dist/plugins/memory-session-policy-store.d.ts +1 -1
- package/dist/plugins/memory-session-policy-store.js +1 -1
- package/dist/plugins/memory-sync-store-pg.js +1 -1
- package/dist/plugins/pg-pool.js +8 -10
- package/dist/plugins/pg-rate-limiter.d.ts +1 -4
- package/dist/plugins/pg-rate-limiter.js +0 -1
- package/dist/plugins/roster-store-sql.d.ts +1 -3
- package/dist/plugins/roster-store-sql.js +3 -5
- package/dist/plugins/store-backend.d.ts +7 -17
- package/dist/plugins/store-backend.js +14 -30
- package/dist/plugins/tidb-pool.js +4 -28
- package/dist/plugins/workflow-journal-store-sql.d.ts +1 -1
- package/dist/plugins/workflow-journal-store-sql.js +1 -1
- package/dist/principal-jwt.js +2 -2
- package/dist/prompts-domain-validate.d.ts +4 -3
- package/dist/prompts-domain-validate.js +7 -8
- package/dist/router/route-orchestration.js +3 -3
- package/dist/run-local.js +5 -4
- package/dist/runs.js +8 -6
- package/dist/runtime-caps-resolver.d.ts +3 -2
- package/dist/runtime-caps-resolver.js +4 -4
- package/dist/runtime-governance.d.ts +4 -7
- package/dist/runtime-governance.js +12 -17
- package/dist/sealed-key.d.ts +1 -1
- package/dist/sealed-key.js +1 -1
- package/dist/security.d.ts +0 -5
- package/dist/security.js +8 -12
- package/dist/session-sync.d.ts +0 -2
- package/dist/session-sync.js +10 -17
- package/dist/spec-fields.js +6 -6
- package/dist/task-settings.js +7 -7
- package/dist/tool-approval.d.ts +3 -3
- package/dist/tool-approval.js +10 -10
- package/dist/trace/artifacts.d.ts +11 -0
- package/dist/trace/artifacts.js +8 -19
- package/dist/trace/ledger-sink.js +7 -1
- package/dist/trace/project.d.ts +17 -0
- package/dist/trace/project.js +55 -5
- package/package.json +2 -2
- package/dist/capabilities/oa-tools.d.ts +0 -15
- package/dist/capabilities/oa-tools.js +0 -54
- package/dist/plugins/approval-store-sql.d.ts +0 -116
- package/dist/plugins/approval-store-sql.js +0 -150
- package/dist/plugins/file-workflow-journal-store.d.ts +0 -12
- package/dist/plugins/file-workflow-journal-store.js +0 -12
- package/dist/plugins/pg-approval-store.d.ts +0 -9
- package/dist/plugins/pg-approval-store.js +0 -9
- package/dist/plugins/pg-breaker-state.d.ts +0 -8
- package/dist/plugins/pg-breaker-state.js +0 -8
- package/dist/plugins/pg-checkpoint-store.d.ts +0 -10
- package/dist/plugins/pg-checkpoint-store.js +0 -10
- package/dist/plugins/pg-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/pg-file-snapshot-store.js +0 -8
- package/dist/plugins/pg-image-bake.d.ts +0 -12
- package/dist/plugins/pg-image-bake.js +0 -11
- package/dist/plugins/pg-image-index.d.ts +0 -12
- package/dist/plugins/pg-image-index.js +0 -11
- package/dist/plugins/pg-outcome-ledger.d.ts +0 -12
- package/dist/plugins/pg-outcome-ledger.js +0 -11
- package/dist/plugins/pg-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/pg-resume-anchor-store.js +0 -7
- package/dist/plugins/pg-run-store.d.ts +0 -9
- package/dist/plugins/pg-run-store.js +0 -9
- package/dist/plugins/pg-session-policy-store.d.ts +0 -7
- package/dist/plugins/pg-session-policy-store.js +0 -7
- package/dist/plugins/pg-session-store.d.ts +0 -12
- package/dist/plugins/pg-session-store.js +0 -12
- package/dist/plugins/pg-tool-result-store.d.ts +0 -9
- package/dist/plugins/pg-tool-result-store.js +0 -9
- package/dist/plugins/pg-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-journal-store.js +0 -9
- package/dist/plugins/pg-workflow-run-store.d.ts +0 -9
- package/dist/plugins/pg-workflow-run-store.js +0 -9
- package/dist/plugins/tidb-approval-store.d.ts +0 -8
- package/dist/plugins/tidb-approval-store.js +0 -8
- package/dist/plugins/tidb-breaker-state.d.ts +0 -7
- package/dist/plugins/tidb-breaker-state.js +0 -7
- package/dist/plugins/tidb-checkpoint-store.d.ts +0 -9
- package/dist/plugins/tidb-checkpoint-store.js +0 -9
- package/dist/plugins/tidb-file-snapshot-store.d.ts +0 -8
- package/dist/plugins/tidb-file-snapshot-store.js +0 -8
- package/dist/plugins/tidb-image-bake.d.ts +0 -12
- package/dist/plugins/tidb-image-bake.js +0 -11
- package/dist/plugins/tidb-image-index.d.ts +0 -12
- package/dist/plugins/tidb-image-index.js +0 -11
- package/dist/plugins/tidb-outcome-ledger.d.ts +0 -12
- package/dist/plugins/tidb-outcome-ledger.js +0 -12
- package/dist/plugins/tidb-resume-anchor-store.d.ts +0 -7
- package/dist/plugins/tidb-resume-anchor-store.js +0 -7
- package/dist/plugins/tidb-run-store.d.ts +0 -10
- package/dist/plugins/tidb-run-store.js +0 -9
- package/dist/plugins/tidb-session-policy-store.d.ts +0 -7
- package/dist/plugins/tidb-session-policy-store.js +0 -7
- package/dist/plugins/tidb-tool-result-store.d.ts +0 -8
- package/dist/plugins/tidb-tool-result-store.js +0 -10
- package/dist/plugins/tidb-workflow-journal-store.d.ts +0 -9
- package/dist/plugins/tidb-workflow-journal-store.js +0 -9
- package/dist/plugins/tidb-workflow-run-store.d.ts +0 -10
- package/dist/plugins/tidb-workflow-run-store.js +0 -10
- package/dist/plugins/workflow-journal-limits.d.ts +0 -12
- package/dist/plugins/workflow-journal-limits.js +0 -12
package/dist/fleet/fleet-bus.js
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* replica-local). A cross-replica fleet roll-up is a fleet-token-gated trace-API concern (separate), not this.
|
|
22
22
|
*/
|
|
23
23
|
import { EventEmitter } from "node:events";
|
|
24
|
-
import { canonicalToolName } from "@sema-agent/core";
|
|
25
24
|
import { redactSecrets } from "../trace/redact.js";
|
|
26
25
|
/**
|
|
27
26
|
* The process-local fleet aggregation bus. Holds the current active set + fans out deltas to SSE subscribers.
|
|
@@ -231,7 +230,9 @@ export function fleetRunPublisher(bus, run) {
|
|
|
231
230
|
// `totalInputTokens`,旧核帧(尚未升级到 3.0.0、该键缺席)回落 `inputTokens`(防御性:本引擎恒 ≥3.0.0
|
|
232
231
|
// 时两键并存;异常帧宁少算不 NaN)。Display-only(leader fleet 行的 token 数);billing(costMicroUsd)+
|
|
233
232
|
// context/compaction 走 totalTokens,不受影响;child 行走 task_progress.usage.totalTokens(上面的早退臂)。
|
|
234
|
-
|
|
233
|
+
// [2354] 兼容面全清:旧核帧回落臂(?? u.inputTokens)已删——floor ^4.0.0 下 totalInputTokens 恒在;
|
|
234
|
+
// 键缺席=越契约帧,按 0 计(宁少算不借异语义键充数)。
|
|
235
|
+
return (u.totalInputTokens ?? 0) + (u.outputTokens ?? 0);
|
|
235
236
|
};
|
|
236
237
|
return {
|
|
237
238
|
onStart() {
|
|
@@ -283,7 +284,7 @@ export function fleetRunPublisher(bus, run) {
|
|
|
283
284
|
// PascalCase double-belt (core 1.176): canonicalize so a legacy snake CONTROL verb (e.g. a
|
|
284
285
|
// pre-1.176 emit / a stray report_blocked) renders as its CC name (ReportBlocked) and the shell's PascalCase
|
|
285
286
|
// suppression Set still hides it — never re-leaking a control verb as fleet-row activity.
|
|
286
|
-
bus.publishTask({ id: run.runId, description:
|
|
287
|
+
bus.publishTask({ id: run.runId, description: ev.toolName }); // RAW(5.0.0):live 名恒 canonical
|
|
287
288
|
}
|
|
288
289
|
},
|
|
289
290
|
onForwardEvent(ev) {
|
|
@@ -595,9 +596,10 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
595
596
|
...(inheritParent && tomb?.resolvedParentId !== undefined ? { resolvedParentId: tomb.resolvedParentId } : {}),
|
|
596
597
|
// session-scoped: registry owner IS the host sessionId (design/129 owner key); fork spawns also carry
|
|
597
598
|
// the child's uuid sessionId — that is the a*↔uuid cross-system anchor, NOT the host session, so it is not used here.
|
|
598
|
-
// [1522] LOW4
|
|
599
|
-
//
|
|
600
|
-
|
|
599
|
+
// [1522] LOW4 root-first。[2378]/[2380] RB-478 到货:三条 revive 发射腿全部无条件带
|
|
600
|
+
// `root ?? parent` 链 ⇒ `?? e.owner` 回落臂成纯死臂,删([2370] 补审时显式排除在全清宣称外
|
|
601
|
+
// 等的就是这一天)。rootSessionId 缺席=不打 host 标(over-filter 方向,durable drain 兜)。
|
|
602
|
+
...(e.sessionScoped && e.owner && e.rootSessionId ? { hostSessionId: e.rootSessionId } : {}),
|
|
601
603
|
// 🔴 scope DOMAIN mapping: core's registry scope is `ctx.principal ?? deployment scope` —
|
|
602
604
|
// UNDEFINED on a single-user no-principal deployment — while the service fleet domain keys the
|
|
603
605
|
// no-principal caller as "default" (`gatedPrincipal ?? "default"`, streamFleet's callerScope). Without
|
|
@@ -606,8 +608,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
606
608
|
// a principal exists, so the map is exact.
|
|
607
609
|
tenantScope: e.scope ?? "default",
|
|
608
610
|
sessionScoped: e.sessionScoped,
|
|
609
|
-
//
|
|
610
|
-
|
|
611
|
+
// [2354] 兼容面全清:旧 core 帧「回落收帧时刻」臂已删——floor ^4.0.0 下 startedAt 恒在;
|
|
612
|
+
// 缺席=诚实不发(行键 optional,消费端本就容忍缺席)。
|
|
613
|
+
...(e.startedAt !== undefined ? { spawnedAt: e.startedAt } : {}),
|
|
611
614
|
...(e.rootSessionId ? { rootSessionId: e.rootSessionId } : {}),
|
|
612
615
|
...(e.parentToolCallId ? { parentToolCallId: e.parentToolCallId } : {}), // P1-2(core 1.432,12 发射点全带)
|
|
613
616
|
...(e.sessionId ? { aliasUuid: e.sessionId } : {}), // codex R4(M-B):uuid↔a* 别名(fork spawn 带 forkedId)
|
|
@@ -617,15 +620,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
617
620
|
// core 帧的 parentSessionId(发起会话,1.350 声明字段——R2:无 runStore 的瞬态 sync 部署没有宿主
|
|
618
621
|
// fleet 行,行查 fallback 必空);②宿主行 sessionId 查表(同租户)兜底。
|
|
619
622
|
...(e.workflowRunId && !(e.sessionScoped && e.owner)
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
return { hostSessionId: e.rootSessionId };
|
|
624
|
-
if (e.parentSessionId)
|
|
625
|
-
return { hostSessionId: e.parentSessionId };
|
|
626
|
-
const host = e.parentTaskId ? bus.snapshot().tasks.find((r) => r.id === e.parentTaskId && r.scope === (e.scope ?? "default")) : undefined;
|
|
627
|
-
return host?.sessionId ? { hostSessionId: host.sessionId } : {};
|
|
628
|
-
})()
|
|
623
|
+
// [2378]/[2380] RB-478:wa* 三级回落(root→parentSessionId→行查表)收单级——core 三条发射腿
|
|
624
|
+
// 已无条件带 root 锚,后两级成死臂;缺席=不打标(over-filter,durable 兜),不再猜。
|
|
625
|
+
? (e.rootSessionId ? { hostSessionId: e.rootSessionId } : {})
|
|
629
626
|
: {}),
|
|
630
627
|
// [1493] cross-session isolation: a NON-session-scoped, non-wa* bg child (owner = the run taskId — the
|
|
631
628
|
// norm when backgroundScope!="session", i.e. every multi-tenant deployment, and any local path where the
|
|
@@ -654,18 +651,11 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
654
651
|
// top-level/nested discriminator) on the spawn event — asked as a core seam follow-up ([1493]).
|
|
655
652
|
// core 1.367 δ:[1493] fable5-R1 注里请的 root fix **到货**——spawn 帧显式带 rootSessionId
|
|
656
653
|
// (固定点语义,嵌套孙代恒=root shell 会话),信任源直用;alias 反查臂降为旧 core 帧回落。
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
for (const pm of meta.values()) {
|
|
663
|
-
if (pm.aliasUuid === e.parentSessionId && (pm.tenantScope ?? "default") === childScope) {
|
|
664
|
-
return pm.hostSessionId ? { hostSessionId: pm.hostSessionId } : {}; // nested: inherit root (or stay untagged → Part B)
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
return { hostSessionId: e.parentSessionId }; // top-level: parentSessionId IS the shell root session
|
|
668
|
-
})()
|
|
654
|
+
// [2354] 兼容面全清:alias 反查臂(旧 core 帧无 rootSessionId 时经 meta 反查 parentSessionId)已删——
|
|
655
|
+
// floor ^4.0.0 下 spawn 帧恒带 rootSessionId(core 1.367 δ 固定点语义);缺席=不打 host 标,
|
|
656
|
+
// 失败方向仍是 over-filtering(durable inbox/stream-open drain 兜),永不 cross-delivery。
|
|
657
|
+
...(!(e.sessionScoped && e.owner) && !e.workflowRunId && e.rootSessionId
|
|
658
|
+
? { hostSessionId: e.rootSessionId }
|
|
669
659
|
: {}),
|
|
670
660
|
};
|
|
671
661
|
remember(e.taskId, m);
|
|
@@ -719,9 +709,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
719
709
|
}
|
|
720
710
|
const tokens = sumUsage(e.usage);
|
|
721
711
|
const name = e.name ? redactSecrets(e.name) : undefined;
|
|
722
|
-
// [1364]③ K-7 换位:TYPE 列取 tick 帧的行级 `agentType`(core 1.350
|
|
723
|
-
//
|
|
724
|
-
const agentType = e.agentType ? redactSecrets(e.agentType) :
|
|
712
|
+
// [1364]③ K-7 换位:TYPE 列取 tick 帧的行级 `agentType`(core 1.350 定谳)。[2354] 兼容面全清:
|
|
713
|
+
// 「缺省回落 name」旧 core 兼容臂已删——floor ^4.0.0 下 agentType 恒在;缺席=诚实不发(行键 optional)。
|
|
714
|
+
const agentType = e.agentType ? redactSecrets(e.agentType) : undefined;
|
|
725
715
|
// The sink tick carries core's currentAction (its only attachment point today) —
|
|
726
716
|
// UNTRUSTED (tool argument head) → redact, same discipline as name. Additive; absent until a tool runs.
|
|
727
717
|
const currentAction = e.currentAction ? redactSecrets(e.currentAction) : undefined;
|
|
@@ -838,7 +828,9 @@ export function fleetBackgroundChildPublisher(bus, log) {
|
|
|
838
828
|
handleSpawn(e);
|
|
839
829
|
return;
|
|
840
830
|
}
|
|
841
|
-
|
|
831
|
+
// [2354] 补审 F2:lazy-mint 不再铸 spawnedAt: Date.now() 假锚(与 spawn 臂同口径——缺 launch 锚=
|
|
832
|
+
// 诚实缺席,行键 optional;tick/terminal 先到=restart/丢 spawn 边缘,收帧时刻不是 launch 时刻)。
|
|
833
|
+
const m = meta.get(e.taskId) ?? { sessionScoped: e.sessionScoped, tenantScope: e.scope ?? "default" }; // scope domain map holds on the lazily-minted path too([1338]④ 首见起表);F2(2026-07-30 复审):lazy-mint 曾硬编码 "default",scoped 部署下 claim 落错租户键 ⇒ 让位失效退回双生行
|
|
842
834
|
if (!meta.has(e.taskId)) {
|
|
843
835
|
remember(e.taskId, m); // tick/terminal without a seen spawn (restart/eviction edge) — row minted lazily
|
|
844
836
|
dlog("bg_child_event", { kind: `${e.kind}-lazy-mint`, taskId: e.taskId, sessionScoped: e.sessionScoped });
|
package/dist/fleet-client.js
CHANGED
|
@@ -190,7 +190,7 @@ export function startFleetClientFromEnv(cfg, opts) {
|
|
|
190
190
|
if (!cc?.baseUrl || !cc.token || !address)
|
|
191
191
|
return undefined;
|
|
192
192
|
if (!cc.worker) {
|
|
193
|
-
opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but
|
|
193
|
+
opts.logger?.warn("fleet_client_no_worker_scope", { note: "FLEET_ADVERTISE_ADDRESS set but SEMA_REGISTRY_WORKER missing — announce needs the worker (dispatch key); fleet client not started" });
|
|
194
194
|
return undefined;
|
|
195
195
|
}
|
|
196
196
|
return startFleetClient({ ...opts, center: { baseUrl: cc.baseUrl, token: cc.token, worker: cc.worker }, advertiseAddress: address });
|
|
@@ -38,6 +38,8 @@ export declare function resumeEntryForGate(kind: string, ids: {
|
|
|
38
38
|
*/
|
|
39
39
|
export interface ActiveRunConflictDoneResult {
|
|
40
40
|
status: "failed";
|
|
41
|
+
/** [2373]C-1:与 409 体同一机器码——流腿客户端凭它认拒绝,禁按 byte-frozen 文案匹配。 */
|
|
42
|
+
errorCode: "conflict.session_active_run";
|
|
41
43
|
errorMessage: string;
|
|
42
44
|
activeTaskId: string | null;
|
|
43
45
|
activeTaskStatus?: string;
|
|
@@ -36,6 +36,7 @@ export function resumeEntryForGate(kind, ids) {
|
|
|
36
36
|
export function toDoneFrameResult(body) {
|
|
37
37
|
return {
|
|
38
38
|
status: "failed",
|
|
39
|
+
errorCode: body.errorCode,
|
|
39
40
|
errorMessage: body.error,
|
|
40
41
|
activeTaskId: body.activeTaskId,
|
|
41
42
|
...(body.activeTaskStatus !== undefined ? { activeTaskStatus: body.activeTaskStatus } : {}),
|
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
* 路由(前缀 `/v1/approvals` 与 `/v1/assistant`):
|
|
5
5
|
* · durable 腿(接了 checkpointStore 时优先):list / stream(SSE 原生推送)/ decide|answer / preempt /
|
|
6
6
|
* resume / plan_review;
|
|
7
|
-
* · legacy 腿(
|
|
7
|
+
* · legacy 腿(approvalStore 轮询车道)已于 5.0.0 整体退役([2354]/[2356] A-5)——无 checkpointStore 时
|
|
8
|
+
* `/v1/approvals` 落 404(诚实缺席),不再有 trusted-header 决裁面。
|
|
8
9
|
*
|
|
9
10
|
* 所有会**烧模型**的续跑都经 `ctx.legs.*`(resumeCheckpoint / resumePreempted / resumePlanReview)走 resume
|
|
10
11
|
* 家族的同一条腿:lease admission、markResuming CAS、写 run log、发 fleet 帧都在腿里,本域只做入站鉴权/形状门。
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { principalFrom, decodeCheckpointScope } from "../../security.js";
|
|
2
|
+
import { verifyDirectDoorProof } from "../../principal-jwt.js";
|
|
3
|
+
import { MAX_APPROVAL_REASON_CHARS } from "../../approval-hmac.js";
|
|
3
4
|
import { redactedPreview } from "../../trace/redact.js";
|
|
4
5
|
import { fleetRunLabels } from "../../fleet/fleet-bus.js"; // [2069]④ §3 行展示名与 fleet 行同源(见用处的注)
|
|
5
6
|
import { sleep } from "../sse-log.js";
|
|
@@ -123,7 +124,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
123
124
|
sendError(res, 400, "request.route_unsupported", "DELETE needs /exemptions/:toolName");
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
|
-
const revoked = await deps.approvalExemptionStore.revoke(exSessionId,
|
|
127
|
+
const revoked = await deps.approvalExemptionStore.revoke(exSessionId, exToolName); // RAW(5.0.0):豁免键空间=CC canonical 名,live/存量行同名
|
|
127
128
|
sendJson(res, revoked ? 200 : 404, revoked ? { revoked: true } : { error: "not found", errorCode: "not_found.exemption" });
|
|
128
129
|
return;
|
|
129
130
|
}
|
|
@@ -445,7 +446,9 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
445
446
|
sendJson(res, out.status, out.body);
|
|
446
447
|
return;
|
|
447
448
|
}
|
|
448
|
-
|
|
449
|
+
// [2373]C-4(5.0.0):裸 `POST /v1/approvals/:id` 装饰臂删——/decide 是唯一决裁形(裸形曾与
|
|
450
|
+
// `GET /v1/approvals/:id` 撞 URL 空间,并把限流/计费/wire 注三处记账劈成两条)。
|
|
451
|
+
const m = /^\/v1\/approvals\/([^/]+)\/decide$/.exec(url);
|
|
449
452
|
if (m && req.method === "POST") {
|
|
450
453
|
if (rateLimited(req, res) || quotaExceeded(req, res))
|
|
451
454
|
return; // 🔴 复审 C2:lease admitted in driveResumeIntoRunLog (owner principal). resume hits TiDB — rate-limit (council)
|
|
@@ -622,7 +625,7 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
622
625
|
? async (overrideSessionId) => {
|
|
623
626
|
const grantSessionId = overrideSessionId ?? sessionId;
|
|
624
627
|
try {
|
|
625
|
-
const canonical =
|
|
628
|
+
const canonical = rememberToolName; // RAW(5.0.0):行 toolName 由 core 铸,恒 canonical
|
|
626
629
|
await deps.approvalExemptionStore.grant(grantSessionId, canonical, deciderPrincipal ?? null);
|
|
627
630
|
rememberApplied = true;
|
|
628
631
|
deps.logger?.info?.("approval_exemption_granted", { sessionId: grantSessionId, toolName: canonical, grantedBy: deciderPrincipal ?? null });
|
|
@@ -643,97 +646,8 @@ async function handleApprovalsAssistantBody(req, res, url, ctx, miss) {
|
|
|
643
646
|
sendError(res, 404, "not_found.route", "not found");
|
|
644
647
|
return;
|
|
645
648
|
}
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
const store = deps.approvalStore;
|
|
649
|
-
// 🔴 D-G belt (defense-in-depth): this LEGACY block authorizes /approvals via the trusted x-agent-principal
|
|
650
|
-
// header (no crypto). On a direct-door worker that path must NEVER be live — the boot invariant already forces
|
|
651
|
-
// DURABLE_APPROVAL (so checkpointStore owns /approvals above and we never reach here), but if a direct-door
|
|
652
|
-
// worker ever fell through to here, fail closed rather than expose the header-trusting path.
|
|
653
|
-
if (deps.config.directApprovalDoor) {
|
|
654
|
-
sendError(res, 503, "approval_door_unconfigured", "approvals are served by the direct door on this worker");
|
|
655
|
-
return;
|
|
656
|
-
}
|
|
657
|
-
if (req.method === "GET" && url === "/v1/approvals") {
|
|
658
|
-
const principal = principalFrom(req, deps.config);
|
|
659
|
-
if (isOperator(principal, deps.config.operatorPrincipals)) {
|
|
660
|
-
// operator: may filter by any ?owner (or list all). scope is owner-sourced, so the operator's ?owner
|
|
661
|
-
// filter IS the scope filter; an unfiltered operator list is cross-tenant BY DESIGN (operator queue).
|
|
662
|
-
const owner = new URL(req.url ?? "", "http://x").searchParams.get("owner") ?? undefined;
|
|
663
|
-
sendJson(res, 200, { pending: owner ? await store.listPending(owner, owner) : await store.listPendingAll() });
|
|
664
|
-
}
|
|
665
|
-
else {
|
|
666
|
-
// non-operator: only ITS OWN pending (ignore any ?owner spoof) — never others' tool-call args. scope =
|
|
667
|
-
// the caller's principal (owner-sourced), so the single-DB read guard isolates it at the SQL layer too.
|
|
668
|
-
sendJson(res, 200, { pending: principal ? await store.listPending(principal, principal) : [] });
|
|
669
|
-
}
|
|
670
|
-
return;
|
|
671
|
-
}
|
|
672
|
-
const m = /^\/v1\/approvals\/([^/]+)$/.exec(url);
|
|
673
|
-
if (m) {
|
|
674
|
-
const id = m[1];
|
|
675
|
-
if (req.method === "GET") {
|
|
676
|
-
// The row carries the full tool-call args (commands/paths/contents of a high-risk op). The list
|
|
677
|
-
// endpoint deliberately scopes non-operators to their OWN pending ("never others' tool-call args");
|
|
678
|
-
// a by-id GET must enforce the same boundary, else any token-holder who learns an id reads another
|
|
679
|
-
// tenant's args. Operators see all; everyone else only their own. 404 (not 403) to non-owners so
|
|
680
|
-
// existence isn't revealed — symmetric with runOwnerOk.
|
|
681
|
-
const principal = principalFrom(req, deps.config);
|
|
682
|
-
if (deps.config.requirePrincipal && !principal) {
|
|
683
|
-
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
const operator = isOperator(principal, deps.config.operatorPrincipals);
|
|
687
|
-
// Operators read across tenants (scope-agnostic getById); a non-operator reads ONLY its own scope so the
|
|
688
|
-
// single-DB read guard fires at the SQL layer (no cross-tenant args even if it learns an id).
|
|
689
|
-
const row = operator ? await store.getById(id) : principal != null ? await store.get(id, principal) : undefined;
|
|
690
|
-
if (!row || (!operator && row.owner !== null && row.owner !== principal)) {
|
|
691
|
-
sendError(res, 404, "not_found.approval", "approval not found");
|
|
692
|
-
return;
|
|
693
|
-
}
|
|
694
|
-
sendJson(res, 200, row);
|
|
695
|
-
return;
|
|
696
|
-
}
|
|
697
|
-
if (req.method === "POST") {
|
|
698
|
-
// Deciding (approve/deny) is OPERATOR-ONLY — a requester approving its own high-risk op would
|
|
699
|
-
// defeat the F4 gate. (Legacy/unset OPERATOR_PRINCIPALS → the service token is the boundary — but
|
|
700
|
-
// ONLY on a single-tenant deployment: under REQUIRE_PRINCIPAL the empty-list true-for-all would let
|
|
701
|
-
// ANY tenant decide ANOTHER tenant's approval [the durable /decide got
|
|
702
|
-
// this hardening earlier and this legacy leg never did] — multi-tenant demands an EXPLICIT
|
|
703
|
-
// operator list, same posture as the durable twin.)
|
|
704
|
-
const principal = principalFrom(req, deps.config);
|
|
705
|
-
const operatorOk = deps.config.requirePrincipal
|
|
706
|
-
? explicitOperatorOk(gatedPrincipal(req, deps.config), deps.config.operatorPrincipals)
|
|
707
|
-
: isOperator(principal, deps.config.operatorPrincipals);
|
|
708
|
-
if (!operatorOk) {
|
|
709
|
-
sendError(res, 403, "auth.operator_only", "only an operator may decide an approval (set OPERATOR_PRINCIPALS)");
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
const body = (await readJson(req));
|
|
713
|
-
const decision = body?.decision === "approve" ? "approved" : body?.decision === "deny" ? "denied" : undefined;
|
|
714
|
-
if (!decision) {
|
|
715
|
-
sendError(res, 400, "request.body_shape", "body must be { decision: 'approve' | 'deny', reason? }");
|
|
716
|
-
return;
|
|
717
|
-
}
|
|
718
|
-
// The operator decides ACROSS tenants by design (the F4 operator queue), so we bind the decide CAS to the
|
|
719
|
-
// row's OWN scope (read scope-agnostically first) rather than the operator's principal — the scope<=>?
|
|
720
|
-
// clause then becomes an exact, consistent guard instead of a cross-tenant bypass. Unknown id ⇒ 409.
|
|
721
|
-
const target = await store.getById(id);
|
|
722
|
-
if (!target) {
|
|
723
|
-
sendError(res, 409, "conflict.approval_settled", "approval already decided or not found");
|
|
724
|
-
return;
|
|
725
|
-
}
|
|
726
|
-
const applied = await store.decide(id, target.scope, decision, body.reason ?? null, principal ?? null);
|
|
727
|
-
if (applied)
|
|
728
|
-
sendJson(res, 200, { id, status: decision });
|
|
729
|
-
else
|
|
730
|
-
sendError(res, 409, "conflict.approval_settled", "approval already decided or not found");
|
|
731
|
-
return;
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
sendError(res, 404, "not_found.route", "not found");
|
|
735
|
-
return;
|
|
736
|
-
}
|
|
649
|
+
// [2354]/[2356] A-5(5.0.0):legacy F4 轮询腿(approvalStore list/by-id/decide,trusted-header 决裁)
|
|
650
|
+
// 整体退役。无 checkpointStore 的部署打 /v1/approvals 一律落到本域尾部的未匹配路径(404 诚实缺席)。
|
|
737
651
|
miss.fell = true;
|
|
738
652
|
}
|
|
739
653
|
/** Server-side poll cadence for the native-push approvals stream. The CLIENT subscribes ONCE and never polls;
|
|
@@ -40,8 +40,8 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
40
40
|
artifacts: Boolean(deps.runStore),
|
|
41
41
|
// TRUE when durable HITL can FIRE = checkpointStore present (DURABLE_APPROVAL=true): it mounts
|
|
42
42
|
// AskUserQuestion-suspend + the approval center + resumable checkpoints, independent of whether any
|
|
43
|
-
// TOOL is gated (APPROVAL_REQUIRE = the additive F4 layer).
|
|
44
|
-
//
|
|
43
|
+
// TOOL is gated (APPROVAL_REQUIRE = the additive F4 layer). ([2354] A-5:legacy 轮询门已退役,
|
|
44
|
+
// durable 是唯一门形——本谓词自然成为 approvals 面唯一的挂载条件。)
|
|
45
45
|
//
|
|
46
46
|
// 🔴 **辖域比名字宽**(2026-08-01,web [C33]③.3 踩出来的):这个谓词同时是 `/v1/assistant/*`
|
|
47
47
|
// 整条车道的挂载条件 —— `routes/approvals-assistant.ts` 的分支头逐字是
|
|
@@ -54,7 +54,8 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
54
54
|
approvals: Boolean(deps.checkpointStore),
|
|
55
55
|
sessions: Boolean(deps.sessionAudit),
|
|
56
56
|
// [1196] session 级 SSE 订阅面:与 /v1/sessions/:id/events 的 501 谓词同源(sessionWatch 只在
|
|
57
|
-
// sessionStorage 具备 getLeafId/ownerOf 时由 main.ts 构建)
|
|
57
|
+
// sessionStorage 具备 getLeafId/ownerOf 时由 main.ts 构建)。[2373]A-5:截至 2026-08-02 无消费端接线
|
|
58
|
+
// (web 未按此换轨)——本位是预备位;「web 据此换轨」是当年的预期,不是已核实的现状。
|
|
58
59
|
sessionEvents: Boolean(deps.sessionWatch && deps.sessionStorage?.getLeafId && deps.sessionStorage?.ownerOf),
|
|
59
60
|
// #13([1871] web asks,clay 排批 2026-07-27):fleet 面能力族——一个诚实对象或 false(无 fleetBus=
|
|
60
61
|
// 无 /v1/fleet/stream,路由 501 同谓词)。`resume:"snapshot"` 是刻意的语义广告而非搪塞:总线是
|
|
@@ -253,8 +254,8 @@ async function handleCapabilitiesBody(req, res, url, ctx, miss) {
|
|
|
253
254
|
// 缺席时 issue()/prepareDirectUpload() 一律 fail-loud,工具挂着也必失败)。"says yes ⟺ 调用真能
|
|
254
255
|
// 成功":只看 config.sendUserFile(签发面已配)会在工具缺席的形态(adb/local-docker lane、多租户
|
|
255
256
|
// host/ssh)假 yes(配置自查 agent 抓获 2026-07-14);只看 emitter 会在无公网端点时假 yes。
|
|
256
|
-
// s3PublicEndpoint
|
|
257
|
-
//
|
|
257
|
+
// s3PublicEndpoint=公网 base 的单一真源透出(server 透出,消费端不猜、不走 registry env 双源;
|
|
258
|
+
// 纯感知面)。[2373]A-5:截至 2026-08-02 无消费端读它——预备位,「web 渲染要用」是预期非现状。
|
|
258
259
|
sendUserFile: Boolean(deps.sendUserFile && deps.config.sendUserFile?.publicEndpoint),
|
|
259
260
|
s3PublicEndpoint: deps.config.sendUserFile?.publicEndpoint ?? null,
|
|
260
261
|
// SendUserFile 账本(多租户治理面):true ⇔ 签发面已配 + store backend 在(记账写点活 + GET
|
|
@@ -95,8 +95,11 @@ function streamFleet(req, res, bus, callerScope, callerSession = null, completio
|
|
|
95
95
|
}
|
|
96
96
|
return rest;
|
|
97
97
|
};
|
|
98
|
+
// [2373]C-12:data 里补 `type`(=event 名)双发——代理剥 `event:` 行/onmessage 忽略具名事件时,
|
|
99
|
+
// 按 data.type 分派的客户端在本流拿到的是 undefined(approvals 流注释里的真实约束,同理适用)。
|
|
100
|
+
// 单点注入:调用方已带 type 的帧(hook_notice/bg_notification)保持原值。
|
|
98
101
|
const send = (event, data) => { if (!res.writableEnded)
|
|
99
|
-
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`); };
|
|
102
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify({ type: event, ...data })}\n\n`); };
|
|
100
103
|
// connect-time snapshot (filtered + scope-stripped) — the visible rows become this connection's "seen" set.
|
|
101
104
|
const snap = bus.snapshot();
|
|
102
105
|
const tasks = snap.tasks.filter(visT);
|
|
@@ -23,6 +23,12 @@ export function coarseStatusForState(state) {
|
|
|
23
23
|
return "failed";
|
|
24
24
|
return "running";
|
|
25
25
|
}
|
|
26
|
+
/** [2373]C-7:202 受理体的**唯一铸点**——此前四份逐字构造散在 attach/竞态重读/新建/dryRun 四臂,
|
|
27
|
+
* 下一个字段必然只加到其中两处(:213/:217 与 :193/:206 已有分叉风险)。status/state 双发保留
|
|
28
|
+
* (status=state 的粗化像,省客户端抄映射表)。 */
|
|
29
|
+
function bakeAcceptedBody(b) {
|
|
30
|
+
return { bakeId: b.bakeId, eventsUrl: `/v1/images/bakes/${b.bakeId}/events`, status: b.status, state: b.state };
|
|
31
|
+
}
|
|
26
32
|
/** build.sh's structured exit code → the center band-composer UI's terminal `errorCode` (§P2.14 #2). null for an
|
|
27
33
|
* uncategorized failure (then `error` free-text + `exitCode` drive the UI). 10=dup-tag, 124=timeout, 507=disk. */
|
|
28
34
|
export function errorCodeForExit(exitCode, cancelled) {
|
|
@@ -168,7 +174,7 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
168
174
|
// (a) an IDENTICAL-argv in-flight bake ⇒ attach (idempotent re-submit, 202) — no second build.
|
|
169
175
|
const sameArgv = await bakes.findActiveByArgv(argv);
|
|
170
176
|
if (sameArgv) {
|
|
171
|
-
sendJson(res, 202,
|
|
177
|
+
sendJson(res, 202, bakeAcceptedBody(sameArgv));
|
|
172
178
|
return;
|
|
173
179
|
}
|
|
174
180
|
// (b) M4: ATOMIC admission — the busy-check and the create are ONE DB statement (`INSERT … SELECT …
|
|
@@ -181,18 +187,18 @@ async function handleImagesBody(req, res, url, ctx, miss) {
|
|
|
181
187
|
if (!r.created) {
|
|
182
188
|
const sameNow = await bakes.findActiveByArgv(argv);
|
|
183
189
|
if (sameNow) {
|
|
184
|
-
sendJson(res, 202,
|
|
190
|
+
sendJson(res, 202, bakeAcceptedBody(sameNow));
|
|
185
191
|
return;
|
|
186
192
|
}
|
|
187
193
|
const busy = await bakes.findActiveAny();
|
|
188
194
|
sendError(res, 409, "conflict.bake_in_progress", "a bake is already in progress", { ...(busy ? { activeBakeId: busy.bakeId, eventsUrl: `/v1/images/bakes/${busy.bakeId}/events` } : {}) });
|
|
189
195
|
return;
|
|
190
196
|
}
|
|
191
|
-
sendJson(res, 202,
|
|
197
|
+
sendJson(res, 202, bakeAcceptedBody(r.bake));
|
|
192
198
|
return;
|
|
193
199
|
}
|
|
194
200
|
const { bake } = await bakes.createBake(createInput);
|
|
195
|
-
sendJson(res, 202,
|
|
201
|
+
sendJson(res, 202, bakeAcceptedBody(bake));
|
|
196
202
|
return;
|
|
197
203
|
}
|
|
198
204
|
// GET /v1/images/bakes/:bakeId/events — resumable SSE (the live "baking…" view), run-trace-SSE-shaped (§P2.2).
|
package/dist/http/routes/runs.js
CHANGED
|
@@ -54,6 +54,29 @@ export const RUN_TASK_OUTPUT_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/output$
|
|
|
54
54
|
export const RUN_TASK_STOP_RE = /^\/v1\/runs\/([^/]+)\/tasks\/([^/]+)\/stop$/;
|
|
55
55
|
/** The task_run SSE — a thin wrapper over `streamSseLog` that PRESERVES the existing wire shape exactly
|
|
56
56
|
* (`data: {type, ...data}`, `event: failed` stale frame). Behavior-identical to the pre-refactor reader. */
|
|
57
|
+
/** [2373]C-5:subagent/task-handle 两读写面的**共享 authz 门**(principal→owner→session 三级,
|
|
58
|
+
* [1493] 模型)。此前两面逐字重写——[1493] 类修复要改两次的真风险。返回 run 行;`null`=已应答
|
|
59
|
+
* (401/404,honest 404 无存在性 oracle)。语义逐字保留:operator 双跳过(owner+session)、
|
|
60
|
+
* session-unbound run 仅 principal 门、fail-closed。 */
|
|
61
|
+
async function subagentReadGate(req, res, deps, runId) {
|
|
62
|
+
const principal = gatedPrincipal(req, deps.config);
|
|
63
|
+
if (deps.config.requirePrincipal && principal === undefined) {
|
|
64
|
+
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const trusted = explicitOperatorOk(principal, deps.config.operatorPrincipals);
|
|
68
|
+
const run = await deps.runStore.getRun(runId);
|
|
69
|
+
if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
|
|
70
|
+
sendError(res, 404, "not_found.run", "run not found");
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
74
|
+
if (!trusted && run.sessionId && callerSession !== run.sessionId) {
|
|
75
|
+
sendError(res, 404, "not_found.run", "run not found");
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
return { run, trusted };
|
|
79
|
+
}
|
|
57
80
|
async function streamRunEvents(req, res, runStore, taskId, staleMs) {
|
|
58
81
|
await streamSseLog(req, res, {
|
|
59
82
|
statusOf: async (id) => {
|
|
@@ -900,44 +923,18 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
900
923
|
sendError(res, 501, "capability.run_store_required", "subagent output reads require the run store");
|
|
901
924
|
return;
|
|
902
925
|
}
|
|
903
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
904
|
-
if (deps.config.requirePrincipal && principal === undefined) {
|
|
905
|
-
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
906
|
-
return;
|
|
907
|
-
}
|
|
908
926
|
const runId = safeDecode(subOutputMatch[1]);
|
|
909
927
|
const target = safeDecode(subOutputMatch[2]);
|
|
910
928
|
if (runId === null || target === null) {
|
|
911
929
|
sendError(res, 400, "request.path_malformed", "malformed subagent path (invalid percent-encoding)");
|
|
912
930
|
return;
|
|
913
931
|
}
|
|
914
|
-
const
|
|
915
|
-
|
|
916
|
-
if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
|
|
917
|
-
sendError(res, 404, "not_found.run", "run not found");
|
|
918
|
-
return;
|
|
919
|
-
}
|
|
920
|
-
// [1493] SESSION-scoped content read. principal answers "is this your data at all" (whose); session
|
|
921
|
-
// answers "which conversation" (which). This verb returns CONVERSATION CONTENT (a child's final body), so
|
|
922
|
-
// the principal owner-gate above is necessary but NOT sufficient: under a SHARED principal (single-user
|
|
923
|
-
// multi-shell — the default local posture, and any tenant that shares an identity) it passes for every
|
|
924
|
-
// co-principal shell, so a SIBLING session could read another conversation's child (clay's dual-shell
|
|
925
|
-
// repro). The run belongs to a conversation (run.sessionId); a non-operator caller must PROVE it owns that
|
|
926
|
-
// conversation by asserting the matching `?session=`. Threat model is ACCIDENTAL cross-read within one
|
|
927
|
-
// user's own shells (no adversary — the shell honestly reports its own session), so an honest
|
|
928
|
-
// caller-asserted session is correct AND sufficient here (no unguessable capability needed). fable5 R1
|
|
929
|
-
// (deliberate boundary, do not "harden" this into a capability): yes, a co-principal caller COULD forge the
|
|
930
|
-
// target's session — but co-principal means the SAME authz identity, i.e. the same user reading their own
|
|
931
|
-
// data through another window; session is a content-ROUTING axis inside one principal, not an authz wall.
|
|
932
|
-
// Deployments with genuinely different humans MUST give them distinct principals (requirePrincipal) — the
|
|
933
|
-
// principal owner-gate above is the security boundary, and it runs first. An
|
|
934
|
-
// operator/trace caller bypasses (as it does the owner-gate). A run with no sessionId is not
|
|
935
|
-
// conversation-bound → principal gate only. Fail-CLOSED: a session-bound run with no/mismatched assertion → 404.
|
|
936
|
-
const callerSession = new URL(req.url ?? "", "http://x").searchParams.get("session");
|
|
937
|
-
if (!trusted && run.sessionId && callerSession !== run.sessionId) {
|
|
938
|
-
sendError(res, 404, "not_found.run", "run not found");
|
|
932
|
+
const gated = await subagentReadGate(req, res, deps, runId); // [2373]C-5 共享门(principal→owner→session)
|
|
933
|
+
if (!gated)
|
|
939
934
|
return;
|
|
940
|
-
}
|
|
935
|
+
const { run } = gated;
|
|
936
|
+
// [1493] SESSION 边界的完整论证(threat model/为何 caller-asserted session 足够/为何不做 capability)
|
|
937
|
+
// 已随共享门挪至 subagentReadGate 注;本面语义逐字不变。
|
|
941
938
|
const out = await deps.subagentTaskOutput(target, { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) });
|
|
942
939
|
const details = out.details;
|
|
943
940
|
// codex R1 (type gate): pollTask is the GENERIC registry face — same-tuple b*/m*/w* handles (bash
|
|
@@ -1006,12 +1003,14 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1006
1003
|
return;
|
|
1007
1004
|
}
|
|
1008
1005
|
sseHeaders(res);
|
|
1006
|
+
// [2373]C-11(5.0.0 BREAKING,提货单点名):meta 帧的 run 实体键改 `taskId`——同 verb 家族的
|
|
1007
|
+
// output 面(:936/:1162)一直叫 taskId,meta 独叫 runId 是家族内第三键名;5.0.0 收一。
|
|
1009
1008
|
// meta 声明:live 帧 replica-local + replay 指引;status=探测快照(终态=不再有帧,发 meta 即收尾,
|
|
1010
1009
|
// cli 拿到「走 replay 面」的信号而不是空挂)。seq=stop-cycle 世代号(settle 铸;core 1.377 起
|
|
1011
1010
|
// **全域投影**——进程内 live 臂投当前复活周期号 [[1533] ask 兑现,[1548]①],durable 终态 serve
|
|
1012
1011
|
// 同键同义;老 core 只在 durable 臂在场,缺省容忍)。1=spawn 周期,≥2=复活周期(retained revive
|
|
1013
1012
|
// 或 tier-3 均 bump——codex 快审 #11 纠,勿窄读成仅 tier-3)。
|
|
1014
|
-
res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
|
|
1013
|
+
res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, taskId: runId, target, status: probeDetails.status ?? "running", ...(typeof probeDetails.seq === "number" ? { seq: probeDetails.seq } : {}), live: "replica-local", replayFace: "GET /v1/runs/:id/subagents/:handle/output" })}\n\n`);
|
|
1015
1014
|
// core 1.382([1561] 提货单③):"parked" 非活非终态(durably suspended pending approval)——跟
|
|
1016
1015
|
// running/pending 同归"还没完",tail 该继续等而不是收尾指去 replay 面(那是给真终态用的)。子代获批
|
|
1017
1016
|
// 恢复后会在**同一条**活流上继续产帧,提前收尾会让客户端误判"已经结束",错过恢复后的后续内容。
|
|
@@ -1068,31 +1067,17 @@ async function handleRunVerbsBody(req, res, url, ctx, miss) {
|
|
|
1068
1067
|
}
|
|
1069
1068
|
if (stopVerb && rateLimited(req, res))
|
|
1070
1069
|
return; // mutating, runs no model (parity with detach — rateLimited only, no quota/lease gate)
|
|
1071
|
-
const principal = gatedPrincipal(req, deps.config);
|
|
1072
|
-
if (deps.config.requirePrincipal && principal === undefined) {
|
|
1073
|
-
sendError(res, 401, "auth.principal_required", `missing principal header '${deps.config.principalHeader}'`);
|
|
1074
|
-
return;
|
|
1075
|
-
}
|
|
1076
1070
|
const runId = safeDecode(taskVerbMatch[1]);
|
|
1077
1071
|
const target = safeDecode(taskVerbMatch[2]);
|
|
1078
1072
|
if (runId === null || target === null) {
|
|
1079
1073
|
sendError(res, 400, "request.path_malformed", "malformed task path (invalid percent-encoding)");
|
|
1080
1074
|
return;
|
|
1081
1075
|
}
|
|
1082
|
-
const
|
|
1083
|
-
|
|
1084
|
-
if (!run || (!trusted && run.owner !== null && run.owner !== principal)) {
|
|
1085
|
-
sendError(res, 404, "not_found.run", "run not found");
|
|
1076
|
+
const gatedB = await subagentReadGate(req, res, deps, runId); // [2373]C-5 共享门([1493] 模型,论证见门注)
|
|
1077
|
+
if (!gatedB)
|
|
1086
1078
|
return;
|
|
1087
|
-
}
|
|
1088
|
-
// SESSION-scoped content/action face ([1493] model): enforced fail-closed — same boundary rationale as
|
|
1089
|
-
// subagentOutput (principal = whose data, session = which conversation; honest caller-asserted session is
|
|
1090
|
-
// correct AND sufficient inside one principal; operators bypass; a session-unbound run is principal-only).
|
|
1079
|
+
const { run } = gatedB;
|
|
1091
1080
|
const q = new URL(req.url ?? "", "http://x").searchParams;
|
|
1092
|
-
if (!trusted && run.sessionId && q.get("session") !== run.sessionId) {
|
|
1093
|
-
sendError(res, 404, "not_found.run", "run not found");
|
|
1094
|
-
return;
|
|
1095
|
-
}
|
|
1096
1081
|
const access = { owner: runId, scope: run.owner ?? "default", ...(run.sessionId ? { sessionId: run.sessionId } : {}) };
|
|
1097
1082
|
// ?filter= is REFUSED on BOTH verbs, not ignored (fail-loud 400; the face shipped unreleased, zero
|
|
1098
1083
|
// consumers — and codex R2: a GET-only refusal would leave the stop verb silently accepting it). The
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
11
11
|
import { type SessionRulesRecord } from "@sema-agent/core";
|
|
12
|
-
import { type StagingHandle } from "../../session-sync.js";
|
|
12
|
+
import { type StagingHandle } from "../../session-sync-kernel.js";
|
|
13
13
|
import type { RouteCtx } from "../route-ctx.js";
|
|
14
14
|
export type PendingStaging = {
|
|
15
15
|
handle: StagingHandle;
|
|
@@ -2,7 +2,8 @@ import { once } from "node:events";
|
|
|
2
2
|
import { randomBytes, createHash } from "node:crypto";
|
|
3
3
|
import { sessionLogDigest, sessionLogDigestsComparable, StreamingImportValidator, SessionError, SessionPolicyError } from "@sema-agent/core";
|
|
4
4
|
import { StringDecoder } from "node:string_decoder";
|
|
5
|
-
import { exportSessionManifest,
|
|
5
|
+
import { exportSessionManifest, fileSnapshotImportFace } from "../../session-sync.js";
|
|
6
|
+
import { classifySyncRelationshipByIds, SyncConflictError } from "../../session-sync-kernel.js";
|
|
6
7
|
import { isUuidShape } from "../../security.js";
|
|
7
8
|
import { sendJson, sendError, msg } from "../send.js";
|
|
8
9
|
import { gatedPrincipal, explicitOperatorOk } from "../principal-gate.js";
|
|
@@ -6,7 +6,7 @@ import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-t
|
|
|
6
6
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
|
|
7
7
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
8
8
|
import { redactSecrets } from "../../trace/redact.js";
|
|
9
|
-
import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, workspaceChangedEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
9
|
+
import { turnEndEventData, contextUsageEventData, toolStartEventData, toolEndEventData, taskProgressEventData, taskNotificationEventData, compactedEventData, diagnosticsEventData, brainStatusEventData, steeringInjectedEventData, compactionOutcomeEventData, workspaceChangedEventData, appendModelUsageDelta, attachModelUsage } from "../../trace/project.js";
|
|
10
10
|
import { cascadeConfig, runMeta } from "../run-meta.js";
|
|
11
11
|
import { scopedIdempotencyKey } from "../idempotency.js";
|
|
12
12
|
import { sendJson, sendError, sseHeaders } from "../send.js";
|
|
@@ -371,8 +371,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
371
371
|
// parentToolCallId-gated), so this sink is the ONLY path a tick reaches the top-run consumers.
|
|
372
372
|
// 1.78 (review HIGH): fan it out to the LIVE SSE too (shared whitelist+redact builder, carrying
|
|
373
373
|
// the E2 identity the shell binds concurrent ticks by) — previously it fed ONLY the fleet bus,
|
|
374
|
-
// so the conversation lane never saw a real tick and the in-loop task_progress branch below
|
|
375
|
-
// forward-compat
|
|
374
|
+
// so the conversation lane never saw a real tick and the in-loop task_progress branch below was
|
|
375
|
+
// forward-compat then — [2373]B-4:floor ^4.0.0 下它是热路径(腐注方向已翻转,勿按旧注当死支)。
|
|
376
|
+
// core swallow-guards a throwing sink.
|
|
376
377
|
const fwdInternals = {
|
|
377
378
|
onForwardEvent: (e) => {
|
|
378
379
|
fleetPub?.onForwardEvent(e);
|
|
@@ -652,10 +653,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
652
653
|
res.write(`data: ${JSON.stringify({ type: "workspace_changed", ...workspaceChangedEventData(ev) })}\n\n`);
|
|
653
654
|
}
|
|
654
655
|
else if (ev.type === "compaction_outcome") {
|
|
655
|
-
// core 2.5.0([2105]②
|
|
656
|
-
//
|
|
657
|
-
|
|
658
|
-
res.write(`data: ${JSON.stringify({ type: "compaction_outcome", outcome: e.outcome, trigger: e.trigger, ...(e.reason !== undefined ? { reason: redactSecrets(e.reason) } : {}), ...(e.eventId !== undefined ? { eventId: e.eventId } : {}), ...(e.parentToolCallId !== undefined ? { parentToolCallId: e.parentToolCallId } : {}) })}\n\n`);
|
|
656
|
+
// core 2.5.0([2105]②)压缩非 compacted 结局帧。[2373]C-2:内联挑键收进共享 builder,
|
|
657
|
+
// 与两条 durable 腿同源(它曾是唯一不走 builder 的内容帧=三方普查共同盲区)。
|
|
658
|
+
res.write(`data: ${JSON.stringify({ type: "compaction_outcome", ...compactionOutcomeEventData(ev) })}\n\n`);
|
|
659
659
|
}
|
|
660
660
|
else if (ev.type === "text_delta" || ev.type === "turn_end" || ev.type === "message_committed" || ev.type === "context_usage") {
|
|
661
661
|
// 裸 catch-all 白名单化收官(probe-wire-1 表征钉的根治半场):最后四个已知臂离开
|
|
@@ -242,7 +242,7 @@ async function streamTaskTrace(req, res, runStore, taskId, staleMs) {
|
|
|
242
242
|
sseHeaders(res);
|
|
243
243
|
// First event declares the stream contract: mode + a version (council — lets the relay detect a worker whose
|
|
244
244
|
// SSE shape changed, e.g. when P1 adds token-delta granularity). resumeFrom echoes the honored Last-Event-ID.
|
|
245
|
-
res.write(`event: meta\ndata: ${JSON.stringify({ version: 1, mode: "delta", resumeFrom: from })}\n\n`);
|
|
245
|
+
res.write(`event: meta\ndata: ${JSON.stringify({ type: "meta", version: 1, mode: "delta", resumeFrom: from })}\n\n`); // [2373]C-12:data.type 双发与内容帧一致
|
|
246
246
|
let closed = false;
|
|
247
247
|
req.on("close", () => {
|
|
248
248
|
closed = true;
|