@sema-agent/server 6.8.0 → 7.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/dist/boot/org-memory.d.ts +36 -0
- package/dist/boot/org-memory.js +76 -0
- package/dist/boot/resolve-spec.d.ts +6 -0
- package/dist/boot/resolve-spec.js +32 -6
- package/dist/boot/runner-deps.d.ts +6 -2
- package/dist/boot/runner-deps.js +11 -2
- package/dist/config-center/http-client.d.ts +1 -0
- package/dist/config-center/http-client.js +38 -14
- package/dist/config-types.d.ts +16 -1
- package/dist/config.js +18 -1
- package/dist/fleet/fleet-bus.d.ts +9 -0
- package/dist/fleet/fleet-bus.js +10 -0
- package/dist/http/routes/tasks.js +11 -3
- package/dist/http/server.js +8 -5
- package/dist/main.js +6 -1
- package/dist/memory-scope.d.ts +10 -1
- package/dist/memory-scope.js +22 -2
- package/dist/org-memory-admission.d.ts +96 -0
- package/dist/org-memory-admission.js +224 -0
- package/dist/question.d.ts +5 -0
- package/dist/question.js +7 -0
- package/dist/runs.d.ts +7 -0
- package/dist/runs.js +17 -3
- package/package.json +3 -3
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* design/170 件A(#148 件3③)——org 记忆准入的装配点:把 org-memory-admission 模块接上 core 5.13.0
|
|
3
|
+
* 的两个 seam(`RunnerDeps.memoryScopeAdmission` / `RunnerDeps.deploymentMemoryScopes`)。
|
|
4
|
+
*
|
|
5
|
+
* 目录源三态(单选,授权面不做双源合并):
|
|
6
|
+
* · center 在场(configCenter 且非 dryRun)⇒ 远程腿:per-principal caps 响应的 `orgMemory` 段
|
|
7
|
+
* (fetchPrincipalCaps 已 schema 读+透传;段键缺席=旧 center 能力握手 ⇒ 目录记 section_absent 瞬时)。
|
|
8
|
+
* **独立 fetch,不复用 caps 缓存条目**(C3:org 授权段自带判别联合缓存,两张表的失败域互不干扰
|
|
9
|
+
* ——caps 腿的 deny 缓存/304 复用机制一旦共享,org 面的 gen 拒退与退避语义就会被 caps 语义污染)。
|
|
10
|
+
* 流量记账:每 principal 每 grant TTL(60s)至多多一拍 caps 拉取,有界。
|
|
11
|
+
* · 无 center 而 `MEMORY_ORG_DIRECTORY_JSON` 在场 ⇒ 单机腿:operator 自证静态表(此处装载=启动期,
|
|
12
|
+
* 坏表 parseOrgDirectoryStatic 直接 throw 拒启动——config 层因 A4 分层不校验,见 config.ts 注)。
|
|
13
|
+
* 两者都在 ⇒ center 胜,env 表忽略 + warn(授权面双源合并=语义地雷,显式拒)。
|
|
14
|
+
* · 都缺 ⇒ resolver 缺席:core 对 request-origin org scope 铸 `memory.admission_required`(fail-closed),
|
|
15
|
+
* deployment-origin 走 `deploymentMemoryScopes` 自证通道不受影响(单用户零迁移,v4 §0 伤害①)。
|
|
16
|
+
*
|
|
17
|
+
* 能力探测(C12,fail-loud):多租户(requirePrincipal)部署的 projects 登记簿里挂了 org 键 ——这些
|
|
18
|
+
* 键在多租户下按 request-origin 盖章(N2 部署形态维)——而目录源缺席(含 configCenter dryRun 形)
|
|
19
|
+
* ⇒ 启动报错:该部署的每个 projectId 请求都会在 prepare 期被整拒,「audit 在跑但恒拒」不是可运行
|
|
20
|
+
* 状态,响亮拒启动比静默全拒服务诚实(clay 三裁 [2687]:直接 BREAKING,不留兼容臂)。
|
|
21
|
+
*/
|
|
22
|
+
import type { RunnerDeps } from "@sema-agent/core";
|
|
23
|
+
import type { ServiceConfig } from "../config.js";
|
|
24
|
+
import type { Logger } from "../observability/logger.js";
|
|
25
|
+
import type { Metrics } from "../observability/metrics.js";
|
|
26
|
+
export interface OrgMemoryAdmissionWiring {
|
|
27
|
+
memoryScopeAdmission: RunnerDeps["memoryScopeAdmission"];
|
|
28
|
+
deploymentMemoryScopes: RunnerDeps["deploymentMemoryScopes"];
|
|
29
|
+
}
|
|
30
|
+
/** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([2704] §1)。 */
|
|
31
|
+
export declare function createOrgMemoryAdmissionWiring(opts: {
|
|
32
|
+
config: ServiceConfig;
|
|
33
|
+
logger: Pick<Logger, "warn">;
|
|
34
|
+
metrics: Pick<Metrics, "inc">;
|
|
35
|
+
}): OrgMemoryAdmissionWiring;
|
|
36
|
+
//# sourceMappingURL=org-memory.d.ts.map
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { fetchPrincipalCaps } from "../config-center/http-client.js";
|
|
2
|
+
import { createMemoryScopeAdmission, createOrgMemoryDirectory, parseOrgDirectoryStatic } from "../org-memory-admission.js";
|
|
3
|
+
const ORG_PREFIX = "org:";
|
|
4
|
+
/** 部署自证 org scope 集(core 准入门的 deployment origin 词表):env 钉的 MEMORY_SCOPE(org 形时)
|
|
5
|
+
* + **单用户部署**的 projects 登记簿 org 键(N2:requirePrincipal !== true 的部署无租户边界,operator
|
|
6
|
+
* 登记簿条目归 deployment)。多租户下登记簿 org 键**不入**此集——它们由 resolve-spec 按 request 盖章,
|
|
7
|
+
* 走目录判决。 */
|
|
8
|
+
function collectDeploymentOrgScopes(config) {
|
|
9
|
+
const scopes = new Set();
|
|
10
|
+
if (config.memoryScope !== undefined && config.memoryScope.startsWith(ORG_PREFIX))
|
|
11
|
+
scopes.add(config.memoryScope);
|
|
12
|
+
if (config.requirePrincipal !== true) {
|
|
13
|
+
for (const project of Object.values(config.projects)) {
|
|
14
|
+
for (const s of project.defaultScopes ?? [])
|
|
15
|
+
if (s.startsWith(ORG_PREFIX))
|
|
16
|
+
scopes.add(s);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return [...scopes];
|
|
20
|
+
}
|
|
21
|
+
/** 依赖收窄到真实消费面(warn/inc 各一手)——接口隔离让测试用真形字面量,零铸形([2704] §1)。 */
|
|
22
|
+
export function createOrgMemoryAdmissionWiring(opts) {
|
|
23
|
+
const { config, logger, metrics } = opts;
|
|
24
|
+
const deploymentMemoryScopes = collectDeploymentOrgScopes(config);
|
|
25
|
+
const centerLeg = config.configCenter !== undefined && !config.configCenter.dryRun ? config.configCenter : undefined;
|
|
26
|
+
const staticJson = config.memoryOrgDirectoryJson;
|
|
27
|
+
if (centerLeg !== undefined && staticJson !== undefined) {
|
|
28
|
+
logger.warn("org_memory_directory_env_ignored", {
|
|
29
|
+
reason: "MEMORY_ORG_DIRECTORY_JSON is set but a config-center is wired — the center directory wins; the env table is IGNORED (authorization faces never merge two sources)",
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
const directory = centerLeg !== undefined
|
|
33
|
+
? createOrgMemoryDirectory({
|
|
34
|
+
fetchSection: async (principal) => {
|
|
35
|
+
// etag 恒缺席 ⇒ fetchPrincipalCaps 永不走 304 臂(返回 null 的唯一途径是条件请求),
|
|
36
|
+
// 这里的 null 防御臂只为类型收口;C8 回声核与 schema 校验都在 fetchPrincipalCaps 内。
|
|
37
|
+
const r = await fetchPrincipalCaps(centerLeg.baseUrl, centerLeg.token, principal, undefined, fetch, centerLeg.worker);
|
|
38
|
+
if (r === null)
|
|
39
|
+
throw new Error("config-center returned 304 to a non-conditional org-directory fetch");
|
|
40
|
+
return r.orgMemory;
|
|
41
|
+
},
|
|
42
|
+
grantTtlMs: config.memoryOrgGrantTtlMs,
|
|
43
|
+
unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
|
|
44
|
+
})
|
|
45
|
+
: staticJson !== undefined
|
|
46
|
+
? createOrgMemoryDirectory({
|
|
47
|
+
staticTable: parseOrgDirectoryStatic(staticJson), // 启动期 fail-loud(坏表拒启动)
|
|
48
|
+
grantTtlMs: config.memoryOrgGrantTtlMs,
|
|
49
|
+
unavailableBackoffMs: config.memoryOrgUnavailableBackoffMs,
|
|
50
|
+
})
|
|
51
|
+
: undefined;
|
|
52
|
+
// C12 能力探测:多租户 + 登记簿有 org 用面 + 目录源缺席 ⇒ 拒启动(见文件头注)。
|
|
53
|
+
if (directory === undefined && config.requirePrincipal === true) {
|
|
54
|
+
const orgProjects = Object.entries(config.projects)
|
|
55
|
+
.filter(([, p]) => (p.defaultScopes ?? []).some((s) => s.startsWith(ORG_PREFIX)))
|
|
56
|
+
.map(([id]) => id);
|
|
57
|
+
if (orgProjects.length > 0) {
|
|
58
|
+
throw new Error(`multi-tenant deployment has org: keys in projects[${orgProjects.join(", ")}].defaultScopes but NO org-memory directory source ` +
|
|
59
|
+
`(config-center absent/dry-run and MEMORY_ORG_DIRECTORY_JSON unset) — every request selecting these projects would be refused at prepare ` +
|
|
60
|
+
`(memory.admission_required, fail-closed). Wire a config-center or set MEMORY_ORG_DIRECTORY_JSON, or remove the org keys.`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const memoryScopeAdmission = directory !== undefined
|
|
64
|
+
? createMemoryScopeAdmission(directory, {
|
|
65
|
+
mode: config.memoryOrgAdmissionMode,
|
|
66
|
+
// §5 观测:outcome 闭集 metric + 审计日志线(per-principal 归因走日志,principal 不入 metric label)。
|
|
67
|
+
onOutcome: (outcome, details) => {
|
|
68
|
+
metrics.inc("memory_admission_total", { outcome });
|
|
69
|
+
if (outcome !== "ok" || details !== undefined)
|
|
70
|
+
logger.warn("memory_admission_outcome", { outcome, ...details });
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
: undefined;
|
|
74
|
+
return { memoryScopeAdmission, deploymentMemoryScopes };
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=org-memory.js.map
|
|
@@ -55,6 +55,12 @@ export interface ResolveSpecCtx {
|
|
|
55
55
|
root: string;
|
|
56
56
|
} | undefined;
|
|
57
57
|
durableEnabled: boolean;
|
|
58
|
+
/** #152 ([2703] 案二):活体 AskUserQuestion 面(QuestionCoordinator 的判决探针切面)。在场(=
|
|
59
|
+
* ASK_QUESTION_ENABLED)时 durable question 门按活流上下文分腿、spec 不再 stamp QUESTION_AWAITS_RESUME
|
|
60
|
+
* (否则 spec.onQuestion 恒遮蔽 RunnerDeps.onQuestion 的活人腿);缺席时行为与旧形逐字一致。 */
|
|
61
|
+
liveQuestionFace: {
|
|
62
|
+
hasLiveContext(): boolean;
|
|
63
|
+
} | undefined;
|
|
58
64
|
approvalExemptionStore: ReturnType<StoreBackend["approvalExemption"]> | undefined;
|
|
59
65
|
singleUserAutoAcceptBaseline: boolean;
|
|
60
66
|
checkpointStore: ReturnType<NonNullable<StoreBackend["checkpoint"]>> | undefined;
|
|
@@ -48,8 +48,27 @@ import { resolveRequestMcp } from "../task-mcp.js";
|
|
|
48
48
|
import { MAX_SETTINGS_OUTPUT_STYLE_CHARS, acceptAppendSystemPrompt, applyTaskSettings, coercePermissionMode, effectiveThinking, hasConstitutionAnchors, parseTaskSettings, providerDropsAppend, withPermissionMode } from "../task-settings.js";
|
|
49
49
|
import { enableForkFromBody, normalizeRetainSubagentSessions, selfOrchestrationFromBody } from "../task-workflow.js";
|
|
50
50
|
import { redactSecrets } from "../trace/redact.js";
|
|
51
|
+
/** #152([2703] 案二):durable 部署上的 AskUserQuestion 门。活体面(QuestionCoordinator)缺席 ⇒ 原形
|
|
52
|
+
* `createDurableQuestionPolicy()`(恒 ask ⇒ 恒 durable park)。在场 ⇒ **判决时**按活流上下文分腿:
|
|
53
|
+
* 活流腿(bg/SSE,coordinator.runWithContext 包裹,ALS 判)allow——工具执行落到 RunnerDeps.onQuestion
|
|
54
|
+
* 的 coordinator,问正在 tail 流的活人;无活流腿(sync /v1/tasks、verify/cascade)ask——durable park
|
|
55
|
+
* 原语义逐字保留(此时执行只会拿到 headless 空答,park 才是把问题送到人面前的那条腿)。
|
|
56
|
+
* 工具名字面量与 server.ts 的 pre-CAS 守卫同源("AskUserQuestion",core 未根导出常量)。 */
|
|
57
|
+
function durableQuestionPolicy(live) {
|
|
58
|
+
if (live === undefined)
|
|
59
|
+
return createDurableQuestionPolicy();
|
|
60
|
+
return {
|
|
61
|
+
check(req) {
|
|
62
|
+
if (req.toolName !== "AskUserQuestion")
|
|
63
|
+
return { action: "allow" };
|
|
64
|
+
return live.hasLiveContext()
|
|
65
|
+
? { action: "allow" }
|
|
66
|
+
: { action: "ask", message: "AskUserQuestion: awaiting a human answer (durable)" };
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
51
70
|
export function createResolveSpec(ctx) {
|
|
52
|
-
const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, } = ctx;
|
|
71
|
+
const { config, logger, metrics, localRoot, scenarios, principalCaps, centerRuntimeCapsResolver, getCenterPrompts, getKeyResolver, taskAttachmentStore, perSessionCwd, setSessionCwd, setSessionShellEnv, hookLlm, hookAgent, fleetBus, hookWakeBus, resumeAnchorStore, ownerAware, taskLimitCaps, taskTimeoutSec, selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore, singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage, sessionEnvSelection, liveQuestionFace, } = ctx;
|
|
53
72
|
// Deployment-owned mapping. Tools/prompt/skills come from the selected scenario (assembled once,
|
|
54
73
|
// bound per request); identity/session/policy stay server-side. Never taken from the request body.
|
|
55
74
|
// design/158 A8:从 createHttpServer 实参里提出来(原地占该字面量 1,093 行中的 967 行)——
|
|
@@ -677,7 +696,9 @@ export function createResolveSpec(ctx) {
|
|
|
677
696
|
// (`writeScope:null` — the engine materializes/reads but harvest commits nothing). Per-request (not a
|
|
678
697
|
// stored flag) → no new per-session state; the shell carries the toggle. RESUME re-runs resolveSpec
|
|
679
698
|
// from the persisted body, so a paused run stays paused across resume legs.
|
|
680
|
-
|
|
699
|
+
// 件A origin 盖章(#148 件3④):部署形态维随 requirePrincipal——多租户下登记簿种子(projectId
|
|
700
|
+
// 选定)按 request 盖章走 core 准入门,单用户一律 deployment(v4 §1 N2;零 org 键=整键缺席零迁移)。
|
|
701
|
+
memory: memoryEngine ? memorySpecForRequest(auth?.memoryScope, body.memoryWrite, s4DefaultScopes, { multiTenant: config.requirePrincipal === true }) : undefined,
|
|
681
702
|
// Scenario-provided capabilities (e.g. code-review = repo tools + reviewer subagents + prompt).
|
|
682
703
|
// RFC A2: the SelectEnvironment tool rides after the scenario's tools (spec.tools is ADDITIVE to core's
|
|
683
704
|
// built-in roster — prepare-task mounts first-party tools separately). Only when the image chain is live.
|
|
@@ -711,10 +732,11 @@ export function createResolveSpec(ctx) {
|
|
|
711
732
|
// operator with approval intent but no checkpointStore falls to `undefined` (core's UNGATED warning).
|
|
712
733
|
// Durable ask (TC-5.4, core 1.95): in durable mode an AskUserQuestion call suspends like an F4 gate —
|
|
713
734
|
// the question policy adjudicates it `ask`, the operator answers out-of-band, and the resume carries
|
|
714
|
-
// the QuestionAnswer (server.ts `body.answer` → onQuestion closure).
|
|
715
|
-
//
|
|
735
|
+
// the QuestionAnswer (server.ts `body.answer` → onQuestion closure). #152([2703] 案二): the question
|
|
736
|
+
// policy is LIVE-AWARE when the QuestionCoordinator is wired — a live-stream leg adjudicates `allow`
|
|
737
|
+
// (the live human answers over the stream) instead of unconditionally parking; see durableQuestionPolicy.
|
|
716
738
|
toolPolicy: durableEnabled
|
|
717
|
-
? combinePolicies(
|
|
739
|
+
? combinePolicies(durableQuestionPolicy(liveQuestionFace), createDurableAskPolicy({
|
|
718
740
|
requireApproval: config.approvalRequire, deny: config.approvalDeny, autoBudget: config.approvalAutoBudget, neverAuto: config.approvalNeverAuto,
|
|
719
741
|
// The probe key is the CONTINUED session (auth.sessionId — the same id that keys the
|
|
720
742
|
// durable checkpoint /decide route). A fresh session (no body.sessionId) has no exemptions by
|
|
@@ -761,7 +783,11 @@ export function createResolveSpec(ctx) {
|
|
|
761
783
|
isCascade: body.cascade === true,
|
|
762
784
|
scope: encodeCheckpointScope(auth?.principal),
|
|
763
785
|
})),
|
|
764
|
-
|
|
786
|
+
// #152([2703] 案二):sentinel 只在活体面缺席时 stamp。core 取 `spec.onQuestion ?? deps.onQuestion`
|
|
787
|
+
// (spec 位恒赢),无条件 stamp 会把 ASK_QUESTION_ENABLED 部署的活人腿永久遮蔽成 durable park。
|
|
788
|
+
// 活体面在场 ⇒ 键缺席,RunnerDeps.onQuestion 的 coordinator 生效;park 与否由上面的判决门分腿。
|
|
789
|
+
// (suspend 侧的「未答 approve」拒绝不靠 sentinel:server.ts decide 路由 pre-CAS 400 守着。)
|
|
790
|
+
...(liveQuestionFace === undefined ? { onQuestion: QUESTION_AWAITS_RESUME } : {}),
|
|
765
791
|
}
|
|
766
792
|
: {}),
|
|
767
793
|
};
|
|
@@ -49,6 +49,10 @@ export interface RunnerDepsCtx {
|
|
|
49
49
|
root: string;
|
|
50
50
|
} | undefined;
|
|
51
51
|
memorySyncRunner: MemorySyncRunner | undefined;
|
|
52
|
+
/** design/170 件A(#148 件3③):org 记忆准入两 seam 的装配产物(boot/org-memory.ts)。进**共享
|
|
53
|
+
* 基座**——主/sub 两 Runner 必须同源:委托子代的 prepare 同样跑准入(委托冻结的重判腿),漏挂
|
|
54
|
+
* subRunner=子代平面绕过目录判决。 */
|
|
55
|
+
orgMemoryAdmission: import("./org-memory.js").OrgMemoryAdmissionWiring;
|
|
52
56
|
toolResultStore: ToolResultStoreFull | undefined;
|
|
53
57
|
sessionPolicyStore: ReturnType<StoreBackend["sessionPolicy"]> | undefined;
|
|
54
58
|
runtimeCapsResolver: RunnerDeps["runtimeCapsResolver"];
|
|
@@ -68,7 +72,7 @@ export interface RunnerDepsCtx {
|
|
|
68
72
|
}
|
|
69
73
|
/** design/158 A10 留档发现②:main runner `RunnerDeps` 与 main.ts subRunner 字面量之间此前手工重复
|
|
70
74
|
* 的 ~15 个键,类型标注见 {@link createSharedRunnerDeps} 头注。 */
|
|
71
|
-
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore">;
|
|
75
|
+
export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "tiers" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "hooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindows" | "usageWindowStore" | "memoryScopeAdmission" | "deploymentMemoryScopes">;
|
|
72
76
|
/**
|
|
73
77
|
* design/158 A10 留档发现②(review 2026-07-29,[1543]§三族A 同源修补的延续):main runner 的
|
|
74
78
|
* `RunnerDeps` 字面量(下方 `createRunnerDeps`)与 `main.ts` 里 subRunner 的 `new Runner({...})`
|
|
@@ -97,7 +101,7 @@ export type SharedRunnerDeps = Pick<RunnerDeps, "brain" | "models" | "roles" | "
|
|
|
97
101
|
* 属性,比再抽一层共享基座更强的同源保证),不重复收纳进这里。
|
|
98
102
|
*/
|
|
99
103
|
/** 基座真实消费的窄面(Pick)——subRunner 调用点(main.ts)只需凑这 13 个字段,不必造全量 ctx。 */
|
|
100
|
-
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore">;
|
|
104
|
+
export type SharedRunnerDepsCtx = Pick<RunnerDepsCtx, "config" | "brain" | "pricing" | "tracer" | "promptSource" | "executionEnvFactory" | "lspManager" | "backgroundAgentStore" | "mailboxStore" | "rosterStore" | "deploymentHooks" | "toolResultStore" | "sessionPolicyStore" | "usageWindowStore" | "orgMemoryAdmission">;
|
|
101
105
|
export declare function createSharedRunnerDeps(ctx: SharedRunnerDepsCtx): SharedRunnerDeps;
|
|
102
106
|
export declare function createRunnerDeps(ctx: RunnerDepsCtx): RunnerDeps;
|
|
103
107
|
//# sourceMappingURL=runner-deps.d.ts.map
|
package/dist/boot/runner-deps.js
CHANGED
|
@@ -49,6 +49,12 @@ export function createSharedRunnerDeps(ctx) {
|
|
|
49
49
|
// E6: operator-tightened session tool rules — core folds them into the ToolPolicy FIRST (subtract-only) for tasks
|
|
50
50
|
// carrying a sessionId (a delegated subagent has none → inherits no rules). Opt-in: undefined ⇒ no rules read.
|
|
51
51
|
sessionPolicyStore: ctx.sessionPolicyStore ? ctx.sessionPolicyStore : undefined,
|
|
52
|
+
// design/170 件A:org 记忆准入两 seam(core 5.13.0)。共享基座=主/sub 同源(委托子代 prepare 的
|
|
53
|
+
// 委托冻结重判腿同样过目录);resolver 缺席=core 对 request-origin org 铸 admission_required
|
|
54
|
+
// fail-closed,deployment 自证集空=键缺席(诚实缺席>空数组——core 按 Set 语义消费,空集与缺席
|
|
55
|
+
// 同义,但缺席让 deps 字面量不携带死键)。
|
|
56
|
+
memoryScopeAdmission: ctx.orgMemoryAdmission.memoryScopeAdmission ? ctx.orgMemoryAdmission.memoryScopeAdmission : undefined,
|
|
57
|
+
deploymentMemoryScopes: ctx.orgMemoryAdmission.deploymentMemoryScopes && ctx.orgMemoryAdmission.deploymentMemoryScopes.length > 0 ? ctx.orgMemoryAdmission.deploymentMemoryScopes : undefined,
|
|
52
58
|
// design/166-T1:治理窗两键**成对**且进共享基座——主/sub/hook 三 runner 全覆盖(漏挂 subRunner=
|
|
53
59
|
// 子代用量不进窗,治理可被 subagent 刷穿;双记不存在:core stats.nested 是旁路侧栏不折入
|
|
54
60
|
// stats.tokens,governance.commit 是 per-任务 delta 制)。store 只在 config.usageWindows 配置时
|
|
@@ -203,8 +209,11 @@ export function createRunnerDeps(ctx) {
|
|
|
203
209
|
// E23: the live-only inbound-elicitation seam. core invokes it only for servers that opted in via
|
|
204
210
|
// McpServerSpec.elicitation (default OFF) AND only when this is wired — both must hold (doubly fail-closed).
|
|
205
211
|
onElicit: elicitation ? elicitation.elicit : undefined,
|
|
206
|
-
// §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here
|
|
207
|
-
//
|
|
212
|
+
// §4④: the live AskUserQuestion seam. core mounts the tool when onQuestion is present + routes each ask here.
|
|
213
|
+
// #152([2703] 案二): with the coordinator wired the durable leg NO LONGER stamps spec.onQuestion
|
|
214
|
+
// (QUESTION_AWAITS_RESUME) — park-vs-live is decided per-leg by resolve-spec's live-aware question policy
|
|
215
|
+
// (live-stream leg → this coordinator; no-live-context leg → durable park). The sentinel is stamped only
|
|
216
|
+
// when the coordinator is absent (ASK_QUESTION_ENABLED off).
|
|
208
217
|
onQuestion: question ? question.question : undefined,
|
|
209
218
|
// [816]/[820]②: the live tool-approval seam (core `resolveAsk` — `spec.onAsk ?? deps.onAsk`). ALS-routed like
|
|
210
219
|
// onQuestion: a leg wrapped by the coordinator's runWithContext reaches the human.
|
|
@@ -38,6 +38,7 @@ export declare function fetchPrincipalCaps(baseUrl: string, token: string, princ
|
|
|
38
38
|
scenario?: ScenarioRuling;
|
|
39
39
|
execution?: ExecutionRuling;
|
|
40
40
|
executionDrift?: string;
|
|
41
|
+
orgMemory?: unknown;
|
|
41
42
|
etag?: string;
|
|
42
43
|
} | null>;
|
|
43
44
|
/** An HTTP error from a config-center fetch that carries the response status so callers can branch on it
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* facade re-exports every symbol below unchanged).
|
|
6
6
|
*/
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import { readEffectiveWire } from "@sema-agent/registry-core";
|
|
8
|
+
import { readEffectiveWire, PrincipalCapsWire } from "@sema-agent/registry-core";
|
|
9
9
|
import { centerPromptsFromEffective } from "../prompts-domain-validate.js"; // #100 worker 腿 prompts 边界归一(中立叶)
|
|
10
10
|
/** GET the effective config (Bearer + ETag). null = 304 (unchanged). Throws on transport/HTTP error, and on a
|
|
11
11
|
* payload that is not an effective config at all(非对象 / version 非有限数 / gate 域坏形——[2281] 裁B)。
|
|
@@ -94,7 +94,22 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
|
|
|
94
94
|
}
|
|
95
95
|
if (!res.ok)
|
|
96
96
|
throw new Error(`config-center principal-caps HTTP ${res.status}`);
|
|
97
|
-
|
|
97
|
+
// 边界读=schema(clay 宪法 [2704] §2,同源锚=registry-core PrincipalCapsWire):治理面
|
|
98
|
+
// runtimeCaps/budget 严格+passthrough(值漂移整体拒 ⇒ 本函数 throw ⇒ resolver fail-close,
|
|
99
|
+
// 「outage 绝不静默 GRANT」同向);[565]/[580] fail-open 面与 orgMemory(C3 失败域隔离)在
|
|
100
|
+
// schema 里保持宽读,归一/校验仍在下方与目录腿(极性不因 schema 化翻转)。
|
|
101
|
+
const parsedBody = PrincipalCapsWire.safeParse(await res.json());
|
|
102
|
+
if (!parsedBody.success) {
|
|
103
|
+
throw new Error(`config-center principal-caps body failed schema validation: ${parsedBody.error.issues.map((i) => `${i.path.join(".")}: ${i.message}`).join("; ").slice(0, 600)}`);
|
|
104
|
+
}
|
|
105
|
+
const body = parsedBody.data;
|
|
106
|
+
// design/170 件A C8(响应 principal 回声核,串租户防线一行):center 两条 ?principal= 腿的 body 都
|
|
107
|
+
// 回带 principal——在场且与请求不符 = 中间层/LB 把别人的 caps 视图递了过来,弃用整响应(throw ⇒
|
|
108
|
+
// caps 腿 fail-close、org 目录腿记 unavailable)。键缺席 = 旧 center 形,按无证据容忍(回声核只拦
|
|
109
|
+
// 「有回声且对不上」的确定性错递,不把旧形升级成故障)。
|
|
110
|
+
if (body.principal !== undefined && body.principal !== principal) {
|
|
111
|
+
throw new Error(`config-center principal-caps response echoes principal ${JSON.stringify(body.principal)} but ${JSON.stringify(principal)} was requested — discarding the whole response (cross-tenant delivery defense)`);
|
|
112
|
+
}
|
|
98
113
|
return {
|
|
99
114
|
runtimeCaps: body.runtimeCaps ?? null,
|
|
100
115
|
configured: Boolean(body.configured),
|
|
@@ -120,28 +135,37 @@ export async function fetchPrincipalCaps(baseUrl, token, principal, etag, fetchI
|
|
|
120
135
|
// and a drifted `required` into silent non-enforcement. Now: drift ⇒ DROP the whole ruling (fail-open,
|
|
121
136
|
// gate passes) + `executionDrift` so the caller's audit line fires (the resolver routes it to onError).
|
|
122
137
|
...((() => {
|
|
123
|
-
|
|
124
|
-
|
|
138
|
+
// [2704] §1:unknown 面的窄化走运行时类型谓词(真检查),不走断言。
|
|
139
|
+
const isRecord = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
140
|
+
const exRaw = body.execution;
|
|
141
|
+
if (exRaw === undefined || exRaw === null)
|
|
125
142
|
return {};
|
|
126
|
-
if (
|
|
127
|
-
return { executionDrift: `execution
|
|
128
|
-
if (
|
|
129
|
-
return { executionDrift: `execution.
|
|
130
|
-
|
|
143
|
+
if (!isRecord(exRaw))
|
|
144
|
+
return { executionDrift: `execution is ${Array.isArray(exRaw) ? "array" : typeof exRaw}, expected object — ruling dropped (fail-open)` };
|
|
145
|
+
if (typeof exRaw.required !== "boolean")
|
|
146
|
+
return { executionDrift: `execution.required is ${typeof exRaw.required}, expected boolean — ruling dropped (fail-open)` };
|
|
147
|
+
if (!Array.isArray(exRaw.allowedLanes))
|
|
148
|
+
return { executionDrift: `execution.allowedLanes is ${typeof exRaw.allowedLanes}, expected string[] — ruling dropped (fail-open)` };
|
|
149
|
+
const execution = { required: exRaw.required, allowedLanes: exRaw.allowedLanes.filter((s) => typeof s === "string") };
|
|
131
150
|
// registry-core 0.10.1-0.10.2:execution.sessionMirror 宽读。observation-only 子面
|
|
132
151
|
// (server 非执法端,见 ExecutionRuling.sessionMirror 注),所以形状漂移只丢**本子面**并走 executionDrift
|
|
133
152
|
// 审计线(resolver 路由到 onError),**绝不**连带丢 lane ruling(那是真执法面,不能被观测子面拖垮)。
|
|
134
153
|
// required 缺省 false = registry-core zod 默认(便利态);engineUrl 必须非空 string。
|
|
135
|
-
if (
|
|
136
|
-
const sm =
|
|
137
|
-
|
|
138
|
-
if (smOk)
|
|
154
|
+
if (exRaw.sessionMirror !== undefined && exRaw.sessionMirror !== null) {
|
|
155
|
+
const sm = exRaw.sessionMirror;
|
|
156
|
+
if (isRecord(sm) && typeof sm.engineUrl === "string" && sm.engineUrl.length > 0 && (sm.required === undefined || typeof sm.required === "boolean")) {
|
|
139
157
|
execution.sessionMirror = { engineUrl: sm.engineUrl, required: sm.required === true };
|
|
140
|
-
|
|
158
|
+
}
|
|
159
|
+
else {
|
|
141
160
|
return { execution, executionDrift: "execution.sessionMirror malformed — mirror observation dropped (fail-open; lane ruling kept)" };
|
|
161
|
+
}
|
|
142
162
|
}
|
|
143
163
|
return { execution };
|
|
144
164
|
})()),
|
|
165
|
+
// design/170 件A:org 记忆解析段**原值透传**(键在场保在场、缺席保缺席——缺席=旧 center 能力握手,
|
|
166
|
+
// org 目录腿据此判 section_absent 瞬时;形状校验是 org-memory-admission 模块用 PrincipalOrgMemoryWire
|
|
167
|
+
// 的事,本腿保持薄透传不预判)。
|
|
168
|
+
...("orgMemory" in body ? { orgMemory: body.orgMemory } : {}),
|
|
145
169
|
etag: res.headers.get("etag") ?? undefined,
|
|
146
170
|
};
|
|
147
171
|
}
|
package/dist/config-types.d.ts
CHANGED
|
@@ -205,6 +205,21 @@ export interface ServiceConfigFlat {
|
|
|
205
205
|
maxPushEntries?: number;
|
|
206
206
|
maxPullEntries?: number;
|
|
207
207
|
};
|
|
208
|
+
/** design/170 件A(clay 三裁 [2687]):org 记忆准入模式。`enforce`(默认)=判决即结果;`audit`=判决
|
|
209
|
+
* 照算但纯观察零行为变化(不整拒、不窄化 writeScope,只记 would-deny/would-narrow)——运维诊断位,
|
|
210
|
+
* 非发布步骤(audit-first 灰度步按裁2 免除)。env `MEMORY_ORG_ADMISSION_MODE`(enumEnv 二值)。 */
|
|
211
|
+
memoryOrgAdmissionMode: "audit" | "enforce";
|
|
212
|
+
/** 件A 单机形目录:env `MEMORY_ORG_DIRECTORY_JSON` 原文(`Record<principal, Record<org:scope, {write?}>>`)。
|
|
213
|
+
* config 解析期即整段校验(parseOrgDirectoryStatic,fail-loud);此处存**原文**(config=纯数据,
|
|
214
|
+
* Map 在装配点重建)。与 config-center 目录腿互斥的裁决在装配点(两者都在=center 腿胜,env 表忽略
|
|
215
|
+
* 并 warn——授权面不做双源合并)。 */
|
|
216
|
+
memoryOrgDirectoryJson?: string;
|
|
217
|
+
/** 件A 目录 granted(含负结果)缓存 TTL(ms)。新任务/新 resume 腿的准入判决滞后上限=此值(「TTL 即
|
|
218
|
+
* 有界 LKG」);与同仓保护面 grant TTL 对齐默认 60s。env `MEMORY_ORG_GRANT_TTL_MS`(numEnvBounded)。 */
|
|
219
|
+
memoryOrgGrantTtlMs: number;
|
|
220
|
+
/** 件A 目录 unavailable 退避窗(ms)——取数失败的退避语义(bound center hammering),不是策略负结果。
|
|
221
|
+
* 默认 10s。env `MEMORY_ORG_UNAVAILABLE_BACKOFF_MS`(numEnvBounded)。 */
|
|
222
|
+
memoryOrgUnavailableBackoffMs: number;
|
|
208
223
|
/** design/48 v1b + design/61: opt-in remote execution backend (PEER adapters, chosen by REMOTE_EXEC). Unset
|
|
209
224
|
* (OA/review deployments) → in-process stub env, zero behavior change. Secrets (apiKey/privateKey) ONLY from env.
|
|
210
225
|
* - `e2b` → per-task E2B Firecracker VM (isolated, suspendable). The default code-agent worker.
|
|
@@ -893,7 +908,7 @@ export type ServiceModelPlaneConfig = Pick<ServiceConfigFlat, "gatewayBaseUrl" |
|
|
|
893
908
|
* 就是本组的门状态,故进组;`parseApprovalDomain` 的返回类型相应是 `Omit<…, "directDoorActive">`。 */
|
|
894
909
|
export type ServiceApprovalConfig = Pick<ServiceConfigFlat, "approvalRequire" | "approvalDeny" | "approvalTimeoutSec" | "approvalAutoBudget" | "approvalNeverAuto" | "approvalHmacKeys" | "durableApproval" | "directApprovalDoor" | "directDoorActive" | "resourceSuspend" | "resourceSuspendTtlSec" | "askQuestionEnabled" | "toolApprovalEnabled" | "mcpElicitation" | "sensitiveWritePatterns" | "manualModeShellGate">;
|
|
895
910
|
/** 组:memory(记忆面 + TOC 同步腿)。 */
|
|
896
|
-
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
911
|
+
export type ServiceMemoryConfig = Pick<ServiceConfigFlat, "memoryEngineEnabled" | "memoryEngineDir" | "memoryEngineRemoteLaneAllowed" | "memoryEngineBackend" | "memoryScope" | "memorySync" | "memoryOrgAdmissionMode" | "memoryOrgDirectoryJson" | "memoryOrgGrantTtlMs" | "memoryOrgUnavailableBackoffMs" | "projectMemoryEnabled" | "syncImportLeaseStaleSec">;
|
|
897
912
|
/** 组:auth(鉴权 / 身份 / 治理棒)。`commandPolicy` 只有 sema-registry 腿(无 env 标量形),故 env 解析
|
|
898
913
|
* 函数不产出它,但它与 `autonomy` 是同一根治理棒的两半,归本组。 */
|
|
899
914
|
export type ServiceAuthConfig = Pick<ServiceConfigFlat, "authToken" | "authTokens" | "allowUnauthedWrites" | "corsOrigins" | "principalHeader" | "requirePrincipal" | "autonomy" | "commandPolicy" | "operatorPrincipals" | "principalJwtPubkeys" | "principalJwtIss" | "principalJwtAud" | "principalJwtMaxTtlSec" | "bindHost" | "bindHostSource">;
|
package/dist/config.js
CHANGED
|
@@ -987,6 +987,18 @@ function parseMemoryDomain(ctx) {
|
|
|
987
987
|
// design/158 B4 ② 的 D 家族四枚里,唯一落在 ServiceConfig 上的就是这枚(其余三枚只在使用点懒读,
|
|
988
988
|
// 预登记留在 orchestration 域 —— 顺序与改前一致:本枚的弃用通知仍先于那三枚)。
|
|
989
989
|
const projectMemoryEnabled = boolEnvWithLegacyNegated("PROJECT_MEMORY_ENABLED", "PROJECT_MEMORY_DISABLED", true);
|
|
990
|
+
// design/170 件A(clay 三裁 [2687]):org 记忆准入四旋钮。模式=enumEnv 二值非布尔(N9),默认
|
|
991
|
+
// enforce(裁2:audit-first 灰度步免除,audit 降级为运维诊断位);TTL/退避窗=numEnvBounded 启动期
|
|
992
|
+
// 报错(env-numeric-fail-loud 门,F6:optFinitePositiveEnv 的静默回默认恰是要消灭的形)。单机目录
|
|
993
|
+
// env 表在这里就整段校验(fail-loud 越早越好——一张半坏的授权表比拒绝启动更危险),config 上只存
|
|
994
|
+
// 原文(纯数据;Map 在装配点重建)。
|
|
995
|
+
const memoryOrgAdmissionMode = enumEnv("MEMORY_ORG_ADMISSION_MODE", "enforce", ["audit", "enforce"]);
|
|
996
|
+
// 原文存 config(纯数据);整段 fail-loud 校验(parseOrgDirectoryStatic)在装配点——它传递值引
|
|
997
|
+
// security.js(assertPrincipalShape),而 base config 层禁值引 auth 模块(design/158 A4 分层),
|
|
998
|
+
// 校验不能放这里。装配点仍在启动期,坏表照样拒启动。
|
|
999
|
+
const memoryOrgDirectoryJson = process.env.MEMORY_ORG_DIRECTORY_JSON || undefined;
|
|
1000
|
+
const memoryOrgGrantTtlMs = numEnvBounded("MEMORY_ORG_GRANT_TTL_MS", "60000", 1000, 3_600_000);
|
|
1001
|
+
const memoryOrgUnavailableBackoffMs = numEnvBounded("MEMORY_ORG_UNAVAILABLE_BACKOFF_MS", "10000", 500, 600_000);
|
|
990
1002
|
return {
|
|
991
1003
|
memoryEngineEnabled,
|
|
992
1004
|
memoryEngineDir: process.env.MEMORY_ENGINE_DIR || undefined,
|
|
@@ -1000,6 +1012,10 @@ function parseMemoryDomain(ctx) {
|
|
|
1000
1012
|
memoryEngineBackend,
|
|
1001
1013
|
memoryScope, // hoisted above (the 142-S2.5-W1 sync-scope default consumes it)
|
|
1002
1014
|
...(memorySync ? { memorySync } : {}),
|
|
1015
|
+
memoryOrgAdmissionMode,
|
|
1016
|
+
...(memoryOrgDirectoryJson !== undefined ? { memoryOrgDirectoryJson } : {}),
|
|
1017
|
+
memoryOrgGrantTtlMs,
|
|
1018
|
+
memoryOrgUnavailableBackoffMs,
|
|
1003
1019
|
projectMemoryEnabled, // design/113 C4 opt-out, flipped positive in design/158 B4 (3.0.0 起 legacy PROJECT_MEMORY_DISABLED = fail-loud 墓碑)
|
|
1004
1020
|
syncImportLeaseStaleSec: Math.max(0, numEnv("SYNC_IMPORT_LEASE_STALE_SEC", "600")),
|
|
1005
1021
|
};
|
|
@@ -1362,7 +1378,8 @@ const APPROVAL_GROUP_KEYS = [
|
|
|
1362
1378
|
];
|
|
1363
1379
|
const MEMORY_GROUP_KEYS = [
|
|
1364
1380
|
"memoryEngineEnabled", "memoryEngineDir", "memoryEngineRemoteLaneAllowed", "memoryEngineBackend", "memoryScope",
|
|
1365
|
-
"memorySync", "
|
|
1381
|
+
"memorySync", "memoryOrgAdmissionMode", "memoryOrgDirectoryJson", "memoryOrgGrantTtlMs", "memoryOrgUnavailableBackoffMs",
|
|
1382
|
+
"projectMemoryEnabled", "syncImportLeaseStaleSec",
|
|
1366
1383
|
];
|
|
1367
1384
|
const AUTH_GROUP_KEYS = [
|
|
1368
1385
|
"authToken", "authTokens", "allowUnauthedWrites", "corsOrigins", "principalHeader", "requirePrincipal", "autonomy",
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
import type { TaskNotificationPayload } from "@sema-agent/core";
|
|
2
|
+
/** [2687-cli] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
|
|
3
|
+
* `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
|
|
4
|
+
* 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
|
|
5
|
+
* agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被
|
|
6
|
+
* 终态映射折成 `failed`。**两侧都正向枚举**(新 task_type/新 status 默认不铸行=安全方向)。
|
|
7
|
+
* 与 core `isDelegatedAgentTerminal`(task-notification.js,未从包根导出)语义同源——core 根导出后
|
|
8
|
+
* 换装 import 删本镜像(提货件已上板)。 */
|
|
9
|
+
export declare function isFleetAgentTerminalNotification(n: Pick<TaskNotificationPayload, "task_type" | "status">): boolean;
|
|
1
10
|
/** The fleet vocabulary the shell renders (MF-Fleet `FleetTask.status`). The service maps its run/workflow status
|
|
2
11
|
* onto this neutral set; `awaiting approval` = a needs-review/plan-approval park, `waiting` = a durable suspend.
|
|
3
12
|
*
|
package/dist/fleet/fleet-bus.js
CHANGED
|
@@ -22,6 +22,16 @@
|
|
|
22
22
|
*/
|
|
23
23
|
import { EventEmitter } from "node:events";
|
|
24
24
|
import { redactSecrets } from "../trace/redact.js";
|
|
25
|
+
/** [2687-cli] 幽灵行案的单源判别:一条 `task_notification` 只有在 **agent 族 × 终态** 时才允许打
|
|
26
|
+
* `onChildTerminal`(fleet「subagent 树」只渲 agent 子代)。`background_bash`/`monitor`/`external`
|
|
27
|
+
* 不属 agent fleet 树——它们此前每条都打,fleet-bus 的「无 tick 无 claim」臂给 b\* 与 m\* handle 合成
|
|
28
|
+
* agent 形瞬态终帧,壳终态留存池把这帧渲 60s = footer 幽灵子行;monitor 的 `status:"event"` 还被
|
|
29
|
+
* 终态映射折成 `failed`。**两侧都正向枚举**(新 task_type/新 status 默认不铸行=安全方向)。
|
|
30
|
+
* 与 core `isDelegatedAgentTerminal`(task-notification.js,未从包根导出)语义同源——core 根导出后
|
|
31
|
+
* 换装 import 删本镜像(提货件已上板)。 */
|
|
32
|
+
export function isFleetAgentTerminalNotification(n) {
|
|
33
|
+
return n.task_type === "background_agent" && (n.status === "completed" || n.status === "failed" || n.status === "killed" || n.status === "cancelled");
|
|
34
|
+
}
|
|
25
35
|
/**
|
|
26
36
|
* The process-local fleet aggregation bus. Holds the current active set + fans out deltas to SSE subscribers.
|
|
27
37
|
* Upserts MERGE (a partial delta patches the existing row), so a publisher can emit just the field that changed
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { runWithVerification, runCascade, uuidv7 } from "@sema-agent/core";
|
|
2
2
|
import { markChildrenStoppedByUserOnAbort, resumeAtHttpStatus, stripCheckpointToken, HEARTBEAT_MS } from "../../runs.js";
|
|
3
3
|
import { withPrincipal } from "../../observability/principal-context.js";
|
|
4
|
-
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals } from "../../fleet/fleet-bus.js";
|
|
4
|
+
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../../fleet/fleet-bus.js";
|
|
5
5
|
import { defaultSubagentTailBus, projectTailFrame } from "../../fleet/subagent-tail-bus.js";
|
|
6
6
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../../orchestration/workflow-completion-inbox.js";
|
|
7
7
|
import { createLedgerSink } from "../../trace/ledger-sink.js";
|
|
@@ -448,7 +448,9 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
448
448
|
defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
|
|
449
449
|
}
|
|
450
450
|
// id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
|
|
451
|
-
|
|
451
|
+
// [2687-cli] 幽灵行案:只对 agent 族终态打(isFleetAgentTerminalNotification 单源判别,
|
|
452
|
+
// 病灶链见其 doc 注);bash/monitor 的通知帧/park 面照走。孪生:runs.ts bg 腿、http/server.ts resume 腿。
|
|
453
|
+
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
452
454
|
// 🔴 对抗评审 2026-07-11(HIGH,「下一 turn 0 帧」最强根因):the teardown WINDOW — after the
|
|
453
455
|
// client disconnects (the user killed the turn → ac.abort → core reaps the bg child → THIS
|
|
454
456
|
// notification fires) but BEFORE the outer finally flips `syncLegLive=false` (several awaits sit
|
|
@@ -1075,7 +1077,13 @@ async function handleTasksBody(req, res, url, ctx, miss) {
|
|
|
1075
1077
|
if (resumeAtStatus) {
|
|
1076
1078
|
if (durableTaskId && deps.runStore)
|
|
1077
1079
|
await deps.runStore.setTerminal(durableTaskId, "failed", stripCheckpointToken(result), result.errorMessage ?? null);
|
|
1078
|
-
|
|
1080
|
+
// #148 件4: the transient org-memory refusal carries core's retry hint — forward it as
|
|
1081
|
+
// retryAfterSec (seconds, ceil; family shape = usage.window_exhausted). ONLY on the transient
|
|
1082
|
+
// code: a wait hint on a terminal 4xx would be a directional lie.
|
|
1083
|
+
const retryAfterSec = result.errorCode === "memory.admission_required" && result.retryAfterMs !== undefined
|
|
1084
|
+
? Math.max(1, Math.ceil(result.retryAfterMs / 1000))
|
|
1085
|
+
: undefined;
|
|
1086
|
+
return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed", ...(retryAfterSec !== undefined ? { retryAfterSec } : {}) } };
|
|
1079
1087
|
}
|
|
1080
1088
|
// Durable suspend on the sync path: PARK it as an async run + return a pollable taskId, NEVER the
|
|
1081
1089
|
// capability token. The submitter switches to GET /v1/runs/:id. Cost is recorded
|
package/dist/http/server.js
CHANGED
|
@@ -16,7 +16,7 @@ import { runInBackground, evictIfConflict, stripCheckpointToken, TurnAnchorCaptu
|
|
|
16
16
|
import { looksLikeJwt } from "@sema-agent/registry-core/api/auth-bridge";
|
|
17
17
|
import {} from "../orchestration/workflow-agent-steer.js";
|
|
18
18
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "../orchestration/workflow-completion-inbox.js";
|
|
19
|
-
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals } from "../fleet/fleet-bus.js";
|
|
19
|
+
import { fleetRunPublisher, fleetRunLabels, fleetRunResiduals, isFleetAgentTerminalNotification } from "../fleet/fleet-bus.js";
|
|
20
20
|
import { defaultSubagentTailBus, projectTailFrame } from "../fleet/subagent-tail-bus.js";
|
|
21
21
|
import {} from "../observability/rate-limit.js";
|
|
22
22
|
import { withPrincipal } from "../observability/principal-context.js";
|
|
@@ -1372,9 +1372,10 @@ export function createHttpServer(rawDeps) {
|
|
|
1372
1372
|
if (deps.config.remoteExec)
|
|
1373
1373
|
for (const name of HAND_TOOL_NAMES)
|
|
1374
1374
|
availableTools.add(name);
|
|
1375
|
-
// AskUserQuestion (durable ask, TC-5.4) is ALSO not in spec.tools — core mounts it from `onQuestion
|
|
1376
|
-
// the
|
|
1377
|
-
// this it 422s on resume exactly like a hand tool would. Gated on
|
|
1375
|
+
// AskUserQuestion (durable ask, TC-5.4) is ALSO not in spec.tools — core mounts it from `onQuestion`
|
|
1376
|
+
// (#152: the QUESTION_AWAITS_RESUME sentinel when no live coordinator is wired, else RunnerDeps.onQuestion's
|
|
1377
|
+
// coordinator serves the seat). Without this it 422s on resume exactly like a hand tool would. Gated on
|
|
1378
|
+
// checkpointStore = durable mode is active.
|
|
1378
1379
|
if (deps.checkpointStore)
|
|
1379
1380
|
availableTools.add("AskUserQuestion");
|
|
1380
1381
|
// Workflow (codex round-2 on [1245]/[1248]②): `run_workflow` is ALSO mounted at the runner, never in
|
|
@@ -1775,7 +1776,9 @@ export function createHttpServer(rawDeps) {
|
|
|
1775
1776
|
defaultSubagentTailBus.publish(n.task_id, { type: "task_settled", taskId: n.task_id, status: n.status, ...(typeof n.seq === "number" ? { seq: n.seq } : {}), ...(n.summary ? { summary: redactSecrets(n.summary) } : {}) });
|
|
1776
1777
|
}
|
|
1777
1778
|
// id-domain alias: flip by payload.sessionId (= the tick's uuid domain), fallback task_id.
|
|
1778
|
-
|
|
1779
|
+
// [2687-cli] 幽灵行案:只对 agent 族终态打(isFleetAgentTerminalNotification 单源判别,
|
|
1780
|
+
// 病灶链见其 doc 注);bash/monitor 的通知帧/park 面照走。孪生:runs.ts bg 腿、routes/tasks.ts sync 腿。
|
|
1781
|
+
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
1779
1782
|
const parked = !resumeLegLive && Boolean(deps.workflowCompletionInbox && sessionId);
|
|
1780
1783
|
// diagnosability (rationale in runs.ts twin).
|
|
1781
1784
|
deps.logger?.info?.("task_notification_observed", { route: "resume", taskId: n.task_id, taskType: n.task_type, status: n.status, hadFleetRow: hadRow, legLive: resumeLegLive, parkedDurable: parked });
|
package/dist/main.js
CHANGED
|
@@ -46,6 +46,7 @@ import { createWorkflowOrchestration } from "./boot/workflow-orchestration.js";
|
|
|
46
46
|
import { createLiveCoordinators } from "./boot/coordinators.js";
|
|
47
47
|
import { createRuntimeCaps } from "./boot/runtime-caps.js";
|
|
48
48
|
import { createRunnerDeps, createSharedRunnerDeps } from "./boot/runner-deps.js";
|
|
49
|
+
import { createOrgMemoryAdmissionWiring } from "./boot/org-memory.js";
|
|
49
50
|
import { createSessionFaces } from "./boot/session-faces.js";
|
|
50
51
|
import { createLeaderFace } from "./boot/leader.js";
|
|
51
52
|
import { installShutdownHandlers } from "./boot/shutdown.js";
|
|
@@ -183,13 +184,15 @@ async function main() {
|
|
|
183
184
|
const { elicitation, question, toolApproval, durableEnabled, sendUserFileEmitter, sendFileLedger, sendUserFileToolSpec } = createLiveCoordinators({ config, logger, backend, sendUserFileTaskEnvs });
|
|
184
185
|
// design/158 A10:per-principal caps 段搬到 src/boot/runtime-caps.ts(逐字)。
|
|
185
186
|
const { principalCaps, centerRuntimeCapsResolver, runtimeCapsResolver } = createRuntimeCaps({ config, logger });
|
|
187
|
+
// design/170 件A(#148 件3③):org 记忆准入装配(目录源三态选择+C12 能力探测,坏配置在此拒启动)。
|
|
188
|
+
const orgMemoryAdmission = createOrgMemoryAdmissionWiring({ config, logger, metrics });
|
|
186
189
|
// design/158 A10:RunnerDeps 装配段搬到 src/boot/runner-deps.ts(逐字;runStore 晚绑改取值,见该文件头注)。
|
|
187
190
|
const runnerDeps = createRunnerDeps({
|
|
188
191
|
config, logger, metrics, localRoot, promptSource: configCenter.promptSource, rosterStore, backgroundAgentStore, mailboxStore, usageWindowStore, brain,
|
|
189
192
|
pricing, tracer, outcomeSink, elicitation, question, toolApproval, sessionStore, memoryEngine,
|
|
190
193
|
memorySyncRunner, toolResultStore, sessionPolicyStore, runtimeCapsResolver, fileSnapshotStore,
|
|
191
194
|
executionEnvFactory, lspManager, fleetBus, deploymentHooks, workflowRunStore, workflowJournalStore,
|
|
192
|
-
workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion,
|
|
195
|
+
workflowAgentRegistry, workflowNotifyGate, workflowCompletionInbox, deliverWorkflowCompletion, orgMemoryAdmission,
|
|
193
196
|
getRunStore: () => runStore,
|
|
194
197
|
});
|
|
195
198
|
const runner = new Runner(runnerDeps);
|
|
@@ -330,6 +333,7 @@ async function main() {
|
|
|
330
333
|
toolResultStore,
|
|
331
334
|
sessionPolicyStore,
|
|
332
335
|
usageWindowStore,
|
|
336
|
+
orgMemoryAdmission,
|
|
333
337
|
}),
|
|
334
338
|
// ── 以下为 subRunner 差异键(不在共享基座;逐个有因)──────────────────────────────────────
|
|
335
339
|
sessionStore: subRunnerSessions, // 子代转录=私有短 TTL fork 路由店,生命周期异于宿主 durable 店
|
|
@@ -870,6 +874,7 @@ async function main() {
|
|
|
870
874
|
selectEnvTool, sendUserFileToolSpec, memoryEngine, durableEnabled, approvalExemptionStore,
|
|
871
875
|
singleUserAutoAcceptBaseline, checkpointStore, deploymentHooks, imageIndex, perTaskImage,
|
|
872
876
|
sessionEnvSelection,
|
|
877
|
+
liveQuestionFace: question, // #152:活体问答面在场 ⇒ durable question 门按活流分腿(见 ResolveSpecCtx 注)
|
|
873
878
|
});
|
|
874
879
|
const server = createHttpServer({ runner, config, resolveSpec, stores, coordinators, seams, observability, governance, deployment, knobs });
|
|
875
880
|
// D-D SLA-timer: wire the server's deny-sweep into the reaper holder declared above (the reaper is defined
|
package/dist/memory-scope.d.ts
CHANGED
|
@@ -58,9 +58,18 @@ export declare function memoryEngineBackendFor(config: ServiceConfig, fallbackRo
|
|
|
58
58
|
* (memory feature off for this run). Pure (testable in isolation) — main.ts's resolveSpec composes it with the
|
|
59
59
|
* engine-backend presence guard.
|
|
60
60
|
*/
|
|
61
|
-
export declare function memorySpecForRequest(scope: string | undefined, memoryWrite: boolean | undefined, defaultScopes?: string[]
|
|
61
|
+
export declare function memorySpecForRequest(scope: string | undefined, memoryWrite: boolean | undefined, defaultScopes?: string[],
|
|
62
|
+
/** design/170 件A(#148 件3④):origin 盖章的部署形态维(N2)。`multiTenant=true`(requirePrincipal)
|
|
63
|
+
* ⇒ 登记簿 defaultScopes 的 org 键按 **request** 盖章(条目由调用方 projectId 选定=caller 可影响的
|
|
64
|
+
* 选择器,core 准入门据此过目录判决);单用户 ⇒ 一律 deployment(operator 登记簿条目归 deployment,
|
|
65
|
+
* 否则单用户部署被自家规则整拒——v4 §0 伤害①的成立前提)。缺参=旧调用形,不盖章(整键缺席=
|
|
66
|
+
* core legacy 语义,零迁移)。 */
|
|
67
|
+
originPolicy?: {
|
|
68
|
+
multiTenant: boolean;
|
|
69
|
+
}): {
|
|
62
70
|
scopes: string[];
|
|
63
71
|
writeScope?: string | null;
|
|
64
72
|
scopeContract?: "v2";
|
|
73
|
+
scopeOrigins?: Record<string, "deployment" | "request">;
|
|
65
74
|
} | undefined;
|
|
66
75
|
//# sourceMappingURL=memory-scope.d.ts.map
|
package/dist/memory-scope.js
CHANGED
|
@@ -98,7 +98,13 @@ export function memoryEngineBackendFor(config, fallbackRoot) {
|
|
|
98
98
|
* (memory feature off for this run). Pure (testable in isolation) — main.ts's resolveSpec composes it with the
|
|
99
99
|
* engine-backend presence guard.
|
|
100
100
|
*/
|
|
101
|
-
export function memorySpecForRequest(scope, memoryWrite, defaultScopes
|
|
101
|
+
export function memorySpecForRequest(scope, memoryWrite, defaultScopes,
|
|
102
|
+
/** design/170 件A(#148 件3④):origin 盖章的部署形态维(N2)。`multiTenant=true`(requirePrincipal)
|
|
103
|
+
* ⇒ 登记簿 defaultScopes 的 org 键按 **request** 盖章(条目由调用方 projectId 选定=caller 可影响的
|
|
104
|
+
* 选择器,core 准入门据此过目录判决);单用户 ⇒ 一律 deployment(operator 登记簿条目归 deployment,
|
|
105
|
+
* 否则单用户部署被自家规则整拒——v4 §0 伤害①的成立前提)。缺参=旧调用形,不盖章(整键缺席=
|
|
106
|
+
* core legacy 语义,零迁移)。 */
|
|
107
|
+
originPolicy) {
|
|
102
108
|
if (!scope)
|
|
103
109
|
return undefined;
|
|
104
110
|
// 142-S1.5: a scope carrying a v2 typed prefix rides WITH the explicit contract marker — core validates
|
|
@@ -119,7 +125,21 @@ export function memorySpecForRequest(scope, memoryWrite, defaultScopes) {
|
|
|
119
125
|
// 最后一层就是派生 scope=正确,不钉保持最小形)。
|
|
120
126
|
const layered = extras.length > 0 ? { scopes: [scope, ...extras], writeScope: scope } : { scopes: [scope] };
|
|
121
127
|
const v2 = isV2(scope) || extras.some(isV2) ? { scopeContract: "v2" } : {};
|
|
128
|
+
// 件A origin 盖章(core 5.13.0 `memory.scopeOrigins` 两格语义):只盖 org 键(非 org 不入准入判决,
|
|
129
|
+
// v4 §1);派生/env 位的 `scope` org 形只可能来自 operator 显式 MEMORY_SCOPE(memoryScopeFor 多租户
|
|
130
|
+
// 恒铸 user: 形)⇒ deployment;登记簿种子按部署形态维分格(见参数注)。零 org 键 ⇒ 整键缺席
|
|
131
|
+
// (core legacy 零迁移;非 org 部署形状逐字节不变=additive 锁)。
|
|
132
|
+
const stamped = {};
|
|
133
|
+
if (originPolicy !== undefined) {
|
|
134
|
+
if (scope.startsWith("org:"))
|
|
135
|
+
stamped[scope] = "deployment";
|
|
136
|
+
for (const extra of extras) {
|
|
137
|
+
if (extra.startsWith("org:"))
|
|
138
|
+
stamped[extra] = originPolicy.multiTenant ? "request" : "deployment";
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const origins = Object.keys(stamped).length > 0 ? { scopeOrigins: stamped } : {};
|
|
122
142
|
// memoryWrite === false(MF-30 pause)wins over the layered writeScope pin — 只读语义在有种子时同样成立。
|
|
123
|
-
return memoryWrite === false ? { ...layered, writeScope: null, ...v2 } : { ...layered, ...v2 };
|
|
143
|
+
return memoryWrite === false ? { ...layered, writeScope: null, ...v2, ...origins } : { ...layered, ...v2, ...origins };
|
|
124
144
|
}
|
|
125
145
|
//# sourceMappingURL=memory-scope.js.map
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Org 记忆准入 resolver — server 侧对接 @sema-agent/core@5.13.0 `RunnerDeps.memoryScopeAdmission` seam。
|
|
3
|
+
* 授权事实来自「目录」抽象:远程腿 = config-center per-principal caps 响应里的 `orgMemory` 段(形状
|
|
4
|
+
* 同源锚 = registry-core `PrincipalOrgMemoryWire`,zod schema 直接 safeParse);单机腿 = env JSON 静态表
|
|
5
|
+
* (`MEMORY_ORG_DIRECTORY_JSON`,{@link parseOrgDirectoryStatic} 装载,启动期 fail-loud)。
|
|
6
|
+
*
|
|
7
|
+
* 判别联合 {@link OrgDirectoryLookup} 是本模块的核心裁定(设计 N3/C5):取数成功(granted,含零授权空表
|
|
8
|
+
* = 负结果)与瞬时不可用(unavailable)绝不塌成一格——granted 空表是 resolver 可以终局判决的「事实」
|
|
9
|
+
* (「这个 principal 没有这个 scope」),unavailable 必须让 core 铸瞬时码 memory.admission_required 重试,
|
|
10
|
+
* 而不是被误判成「没这个 scope」而永久拒绝。
|
|
11
|
+
*
|
|
12
|
+
* core 侧契约(RunnerDeps.memoryScopeAdmission,core types.d.ts):resolver 返回 {ok:false} ⇒ core 铸终局码
|
|
13
|
+
* memory.admission_denied;resolver throw 一个带 `retryAfterMs`(ms)属性的 Error ⇒ core 铸瞬时码
|
|
14
|
+
* memory.admission_required 并透传 retryAfterMs;{ok:true}.scopes 只能 ⊆ requested(多给 = resolver fault
|
|
15
|
+
* 会被 core 整拒)。core 只在「存在 request-origin org scope 且 principal 缺席」时自己先拒——本模块仍防御
|
|
16
|
+
* `principal === undefined` 这一支(不能信任「core 一定先拒过」这件事)。
|
|
17
|
+
*/
|
|
18
|
+
import type { MemoryScopeAdmission } from "@sema-agent/core";
|
|
19
|
+
/**
|
|
20
|
+
* 单机腿:装载 `MEMORY_ORG_DIRECTORY_JSON`(`Record<principal, Record<orgScope, {write?}>>`)。启动期调用,
|
|
21
|
+
* **fail-loud**——任何非法直接 throw 且消息点名坏在哪个键(响亮拒是正确方向:一张半坏的授权表比拒绝
|
|
22
|
+
* 启动更危险)。principal 键复用 {@link assertPrincipalShape}(拒保留哨兵/超长租户名,与所有其它
|
|
23
|
+
* principal 入口同一道闸);scope 键必须是 `org:` 前缀、值必须是仅含可选 `write: boolean` 的 plain object。
|
|
24
|
+
*/
|
|
25
|
+
export declare function parseOrgDirectoryStatic(json: string): Map<string, Record<string, {
|
|
26
|
+
write?: boolean;
|
|
27
|
+
}>>;
|
|
28
|
+
/**
|
|
29
|
+
* 目录条目的判别联合(设计裁定 N3/C5):`granted` 是取数成功——包括零授权空表(负结果,由 resolver
|
|
30
|
+
* 判终局拒);`unavailable` 是瞬时臂(取数本身失败/形状不可信/LB 陈旧副本),只在这一支才该让 core 重试。
|
|
31
|
+
*/
|
|
32
|
+
export type OrgDirectoryLookup = {
|
|
33
|
+
kind: "granted";
|
|
34
|
+
scopes: Readonly<Record<string, {
|
|
35
|
+
write?: boolean;
|
|
36
|
+
}>>;
|
|
37
|
+
} | {
|
|
38
|
+
kind: "unavailable";
|
|
39
|
+
reason: OrgDirectoryUnavailableReason;
|
|
40
|
+
retryAfterMs: number;
|
|
41
|
+
};
|
|
42
|
+
/** {@link createOrgMemoryDirectory} 的产物 —— 单一 per-principal 查表口。 */
|
|
43
|
+
export interface OrgMemoryDirectory {
|
|
44
|
+
lookup(principal: string): Promise<OrgDirectoryLookup>;
|
|
45
|
+
}
|
|
46
|
+
export interface OrgMemoryDirectoryOptions {
|
|
47
|
+
/** 远程腿:返回 caps 响应的 `orgMemory` 段原值(unknown,本模块用 `PrincipalOrgMemoryWire.safeParse` 校验)。
|
|
48
|
+
* 返回 undefined = 响应里键整体缺席(旧 center 能力握手)⇒ unavailable("section_absent")。
|
|
49
|
+
* throw = fetch 失败 ⇒ unavailable("fetch_failed")。
|
|
50
|
+
* 与 `staticTable` 互斥:二者恰好给一个(装配点保证;都给或都缺 = 构造期 throw,fail-loud)。 */
|
|
51
|
+
fetchSection?: (principal: string) => Promise<unknown>;
|
|
52
|
+
/** 单机腿:{@link parseOrgDirectoryStatic} 的产物。查表恒「取数成功」:缺 principal ⇒ granted 空表。 */
|
|
53
|
+
staticTable?: Map<string, Record<string, {
|
|
54
|
+
write?: boolean;
|
|
55
|
+
}>>;
|
|
56
|
+
/** granted(含负结果)缓存 TTL——ms。负结果与正授权同 TTL:未授权 principal 不该比授权 principal 打出
|
|
57
|
+
* 更多流量。默认值不在本模块(接线点决定,设计定 60_000)。 */
|
|
58
|
+
grantTtlMs: number;
|
|
59
|
+
/** 一次 unavailable 之后,同 principal 在这个窗口内的 lookup 直接返回 unavailable(不重新 fetch)——
|
|
60
|
+
* 防止对一个持续故障/未授权目标反复打 center。默认值不在本模块(接线点决定,设计定 10_000)。 */
|
|
61
|
+
unavailableBackoffMs: number;
|
|
62
|
+
/** 时间注入(测试用);默认 `Date.now`。 */
|
|
63
|
+
now?: () => number;
|
|
64
|
+
}
|
|
65
|
+
type OrgDirectoryUnavailableReason = "fetch_failed" | "section_absent" | "malformed" | "stale_generation";
|
|
66
|
+
/**
|
|
67
|
+
* 建目录:远程腿(fetchSection,per-principal TTL 缓存 + in-flight 去重 + 退避窗 + gen 高水位)或单机腿
|
|
68
|
+
* (staticTable,恒同步成功、无 TTL/退避语义——operator 自证配置面)。两腿互斥,装配点必须恰好给一个。
|
|
69
|
+
*/
|
|
70
|
+
export declare function createOrgMemoryDirectory(opts: OrgMemoryDirectoryOptions): OrgMemoryDirectory;
|
|
71
|
+
export interface MemoryScopeAdmissionOptions {
|
|
72
|
+
/** "audit": 判决照算但从不真拒——会拒的结果(终局 ok:false 或瞬时 throw)一律换成全额放行 + 观测记录
|
|
73
|
+
* would-deny;真通过的判决(含 write 收窄为 null)照常返回。"enforce": 判决即结果。 */
|
|
74
|
+
mode: "audit" | "enforce";
|
|
75
|
+
/** 观测 hook。outcome ∈ "ok" | "denied" | "principal_missing" | "directory_absent" | "directory_stale" |
|
|
76
|
+
* "directory_malformed"。audit 模式下的 would-deny 也走这里,`details.audited === true`。hook 本身绝不
|
|
77
|
+
* 允许打断准入判决——一律吞掉它可能抛出的异常。 */
|
|
78
|
+
onOutcome?: (outcome: string, details?: Record<string, unknown>) => void;
|
|
79
|
+
}
|
|
80
|
+
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
81
|
+
* memory.admission_required 终局码并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
82
|
+
export declare class OrgMemoryAdmissionRetryError extends Error {
|
|
83
|
+
readonly retryAfterMs: number;
|
|
84
|
+
constructor(message: string, retryAfterMs: number);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* `RunnerDeps.memoryScopeAdmission` 的 server 实装。deployment-origin 的 requested 项一律放行(operator
|
|
88
|
+
* 自证——v1 不做中央收窄部署声明);request-origin 项查 `directory.lookup(principal)`:granted 时逐项核
|
|
89
|
+
* 对是否在授权集里(任何一项缺席 = 整体终局拒绝,不做部分放行——reason 只点名调用方自己请求过的 scope
|
|
90
|
+
* 串,不逐 scope 展开「为什么」,避免变成成员探针);unavailable 时 throw 一个带 `retryAfterMs` 的
|
|
91
|
+
* {@link OrgMemoryAdmissionRetryError}。write 面独立收窄:deployment-origin 恒授,request-origin 仅当目录
|
|
92
|
+
* 条目 `write === true` 才授,否则收窄为 null(即使读侧整体放行)。
|
|
93
|
+
*/
|
|
94
|
+
export declare function createMemoryScopeAdmission(directory: OrgMemoryDirectory, opts: MemoryScopeAdmissionOptions): MemoryScopeAdmission;
|
|
95
|
+
export {};
|
|
96
|
+
//# sourceMappingURL=org-memory-admission.d.ts.map
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
import { PrincipalOrgMemoryWire } from "@sema-agent/registry-core";
|
|
2
|
+
import { assertPrincipalShape } from "./security.js";
|
|
3
|
+
const isPlainObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
4
|
+
const ORG_SCOPE_RE = /^org:\S+$/;
|
|
5
|
+
/**
|
|
6
|
+
* 单机腿:装载 `MEMORY_ORG_DIRECTORY_JSON`(`Record<principal, Record<orgScope, {write?}>>`)。启动期调用,
|
|
7
|
+
* **fail-loud**——任何非法直接 throw 且消息点名坏在哪个键(响亮拒是正确方向:一张半坏的授权表比拒绝
|
|
8
|
+
* 启动更危险)。principal 键复用 {@link assertPrincipalShape}(拒保留哨兵/超长租户名,与所有其它
|
|
9
|
+
* principal 入口同一道闸);scope 键必须是 `org:` 前缀、值必须是仅含可选 `write: boolean` 的 plain object。
|
|
10
|
+
*/
|
|
11
|
+
export function parseOrgDirectoryStatic(json) {
|
|
12
|
+
let parsed;
|
|
13
|
+
try {
|
|
14
|
+
parsed = JSON.parse(json);
|
|
15
|
+
}
|
|
16
|
+
catch (e) {
|
|
17
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON is not valid JSON: ${e.message}`);
|
|
18
|
+
}
|
|
19
|
+
if (!isPlainObject(parsed)) {
|
|
20
|
+
throw new Error("MEMORY_ORG_DIRECTORY_JSON must be a JSON object: Record<principal, Record<orgScope, {write?: boolean}>>");
|
|
21
|
+
}
|
|
22
|
+
const table = new Map();
|
|
23
|
+
for (const [principal, scopesRaw] of Object.entries(parsed)) {
|
|
24
|
+
assertPrincipalShape(principal); // reserved sentinel / oversized tenant name → throws (same gate as every other principal entry point)
|
|
25
|
+
if (!isPlainObject(scopesRaw)) {
|
|
26
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}] must be an object of org scopes`);
|
|
27
|
+
}
|
|
28
|
+
const scopes = {};
|
|
29
|
+
for (const [scope, entryRaw] of Object.entries(scopesRaw)) {
|
|
30
|
+
if (!ORG_SCOPE_RE.test(scope)) {
|
|
31
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}] has an invalid org scope key ${JSON.stringify(scope)} (must match /^org:\\S+$/)`);
|
|
32
|
+
}
|
|
33
|
+
if (!isPlainObject(entryRaw)) {
|
|
34
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}] must be an object`);
|
|
35
|
+
}
|
|
36
|
+
for (const key of Object.keys(entryRaw)) {
|
|
37
|
+
if (key !== "write") {
|
|
38
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}] has an unexpected key ${JSON.stringify(key)} (only "write" is allowed)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (entryRaw.write !== undefined && typeof entryRaw.write !== "boolean") {
|
|
42
|
+
throw new Error(`MEMORY_ORG_DIRECTORY_JSON[${JSON.stringify(principal)}][${JSON.stringify(scope)}].write must be a boolean`);
|
|
43
|
+
}
|
|
44
|
+
scopes[scope] = entryRaw.write !== undefined ? { write: entryRaw.write } : {};
|
|
45
|
+
}
|
|
46
|
+
table.set(principal, scopes);
|
|
47
|
+
}
|
|
48
|
+
return table;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* 建目录:远程腿(fetchSection,per-principal TTL 缓存 + in-flight 去重 + 退避窗 + gen 高水位)或单机腿
|
|
52
|
+
* (staticTable,恒同步成功、无 TTL/退避语义——operator 自证配置面)。两腿互斥,装配点必须恰好给一个。
|
|
53
|
+
*/
|
|
54
|
+
export function createOrgMemoryDirectory(opts) {
|
|
55
|
+
if ((opts.fetchSection !== undefined) === (opts.staticTable !== undefined)) {
|
|
56
|
+
throw new Error("createOrgMemoryDirectory requires exactly one of fetchSection or staticTable (both given or both missing)");
|
|
57
|
+
}
|
|
58
|
+
if (opts.staticTable !== undefined) {
|
|
59
|
+
const table = opts.staticTable;
|
|
60
|
+
return {
|
|
61
|
+
async lookup(principal) {
|
|
62
|
+
return { kind: "granted", scopes: table.get(principal) ?? {} };
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
if (opts.fetchSection === undefined) {
|
|
67
|
+
// Unreachable given the xor guard above (staticTable was undefined, so fetchSection must not be) — this
|
|
68
|
+
// check exists ONLY so the type checker narrows fetchSection to a function without a cast.
|
|
69
|
+
throw new Error("createOrgMemoryDirectory: fetchSection missing (invariant violated)");
|
|
70
|
+
}
|
|
71
|
+
const fetchSection = opts.fetchSection;
|
|
72
|
+
const grantTtlMs = opts.grantTtlMs;
|
|
73
|
+
const backoffMs = opts.unavailableBackoffMs;
|
|
74
|
+
const now = opts.now ?? (() => Date.now());
|
|
75
|
+
const states = new Map();
|
|
76
|
+
const inflight = new Map();
|
|
77
|
+
function markUnavailable(state, reason) {
|
|
78
|
+
state.unavailable = { until: now() + backoffMs, reason };
|
|
79
|
+
// C6: retryAfterMs never 0/negative — the window just opened, so its remaining time IS backoffMs.
|
|
80
|
+
return { kind: "unavailable", reason, retryAfterMs: Math.max(backoffMs, 1000) };
|
|
81
|
+
}
|
|
82
|
+
async function doFetch(principal, state) {
|
|
83
|
+
let raw;
|
|
84
|
+
try {
|
|
85
|
+
raw = await fetchSection(principal);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return markUnavailable(state, "fetch_failed");
|
|
89
|
+
}
|
|
90
|
+
if (raw === undefined)
|
|
91
|
+
return markUnavailable(state, "section_absent");
|
|
92
|
+
const parsed = PrincipalOrgMemoryWire.safeParse(raw);
|
|
93
|
+
if (!parsed.success) {
|
|
94
|
+
// malformed: does NOT touch state.grant — a bad shape must not refresh (or clear) an existing granted entry's
|
|
95
|
+
// timestamp; a still-fresh old grant keeps serving from cache on the NEXT lookup (checked before we ever get here).
|
|
96
|
+
return markUnavailable(state, "malformed");
|
|
97
|
+
}
|
|
98
|
+
if (state.highWaterGen !== undefined && parsed.data.gen < state.highWaterGen) {
|
|
99
|
+
// LB stale-replica defense: an older generation than one we've already seen is NEVER adopted.
|
|
100
|
+
return markUnavailable(state, "stale_generation");
|
|
101
|
+
}
|
|
102
|
+
state.highWaterGen = parsed.data.gen;
|
|
103
|
+
state.grant = { scopes: parsed.data.scopes, at: now() };
|
|
104
|
+
delete state.unavailable; // a real success clears any prior backoff window
|
|
105
|
+
return { kind: "granted", scopes: parsed.data.scopes };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
async lookup(principal) {
|
|
109
|
+
let state = states.get(principal);
|
|
110
|
+
if (!state) {
|
|
111
|
+
state = {};
|
|
112
|
+
states.set(principal, state);
|
|
113
|
+
}
|
|
114
|
+
const t = now();
|
|
115
|
+
// 1. Fresh grant (positive OR negative/empty result) always wins — no fetch, no backoff check.
|
|
116
|
+
if (state.grant && state.grant.at + grantTtlMs > t) {
|
|
117
|
+
return { kind: "granted", scopes: state.grant.scopes };
|
|
118
|
+
}
|
|
119
|
+
// 2. Inside an open backoff window → answer from the window WITHOUT re-fetching.
|
|
120
|
+
if (state.unavailable && state.unavailable.until > t) {
|
|
121
|
+
return { kind: "unavailable", reason: state.unavailable.reason, retryAfterMs: Math.max(state.unavailable.until - t, 1000) };
|
|
122
|
+
}
|
|
123
|
+
// 3. Grant expired (or never existed) and no active backoff → fetch, deduping concurrent callers.
|
|
124
|
+
const dup = inflight.get(principal);
|
|
125
|
+
if (dup)
|
|
126
|
+
return dup;
|
|
127
|
+
const p = doFetch(principal, state).finally(() => inflight.delete(principal));
|
|
128
|
+
inflight.set(principal, p);
|
|
129
|
+
return p;
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
/** core 侧「瞬时不可用,重试」契约的载体:一个带 `retryAfterMs`(ms)属性的真 Error 子类,core 据此铸
|
|
134
|
+
* memory.admission_required 终局码并透传该值。子类而非事后挂属性——避免任何宽松断言就能拿到正确类型。 */
|
|
135
|
+
export class OrgMemoryAdmissionRetryError extends Error {
|
|
136
|
+
retryAfterMs;
|
|
137
|
+
constructor(message, retryAfterMs) {
|
|
138
|
+
super(message);
|
|
139
|
+
this.retryAfterMs = retryAfterMs;
|
|
140
|
+
this.name = "OrgMemoryAdmissionRetryError";
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* `RunnerDeps.memoryScopeAdmission` 的 server 实装。deployment-origin 的 requested 项一律放行(operator
|
|
145
|
+
* 自证——v1 不做中央收窄部署声明);request-origin 项查 `directory.lookup(principal)`:granted 时逐项核
|
|
146
|
+
* 对是否在授权集里(任何一项缺席 = 整体终局拒绝,不做部分放行——reason 只点名调用方自己请求过的 scope
|
|
147
|
+
* 串,不逐 scope 展开「为什么」,避免变成成员探针);unavailable 时 throw 一个带 `retryAfterMs` 的
|
|
148
|
+
* {@link OrgMemoryAdmissionRetryError}。write 面独立收窄:deployment-origin 恒授,request-origin 仅当目录
|
|
149
|
+
* 条目 `write === true` 才授,否则收窄为 null(即使读侧整体放行)。
|
|
150
|
+
*/
|
|
151
|
+
export function createMemoryScopeAdmission(directory, opts) {
|
|
152
|
+
const emit = (outcome, details) => {
|
|
153
|
+
try {
|
|
154
|
+
opts.onOutcome?.(outcome, details);
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
/* an observability hook must never break admission */
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
return async ({ principal, requested, requestedWriteScope }) => {
|
|
161
|
+
const requestOriginRequested = requested.filter((r) => r.origin === "request");
|
|
162
|
+
const writeIsRequestOrigin = requestedWriteScope?.origin === "request";
|
|
163
|
+
const hasRequestOrigin = requestOriginRequested.length > 0 || writeIsRequestOrigin;
|
|
164
|
+
const fullScopes = requested.map((r) => r.scope);
|
|
165
|
+
const fullWriteScope = requestedWriteScope?.scope ?? null;
|
|
166
|
+
const admitFull = () => ({ ok: true, scopes: fullScopes, writeScope: fullWriteScope });
|
|
167
|
+
async function decide() {
|
|
168
|
+
// deployment-only requests (no request-origin item anywhere, including the write slot): nothing to
|
|
169
|
+
// look up — v1 does not centrally narrow operator-declared deployment scopes.
|
|
170
|
+
if (!hasRequestOrigin)
|
|
171
|
+
return { kind: "admit", verdict: admitFull() };
|
|
172
|
+
if (principal === undefined) {
|
|
173
|
+
// Defensive arm: core already refuses this shape itself when a request-origin org scope is present
|
|
174
|
+
// with no principal, but the resolver must not assume that guard always ran first.
|
|
175
|
+
return { kind: "deny", reason: "principal absent", outcome: "principal_missing" };
|
|
176
|
+
}
|
|
177
|
+
const lookup = await directory.lookup(principal);
|
|
178
|
+
if (lookup.kind === "unavailable") {
|
|
179
|
+
const outcome = lookup.reason === "stale_generation" ? "directory_stale" : lookup.reason === "malformed" ? "directory_malformed" : "directory_absent"; // fetch_failed | section_absent
|
|
180
|
+
return { kind: "unavailable", retryAfterMs: lookup.retryAfterMs, outcome, lookupReason: lookup.reason, details: { reason: lookup.reason } };
|
|
181
|
+
}
|
|
182
|
+
const deniedScopes = requestOriginRequested.filter((r) => !(r.scope in lookup.scopes)).map((r) => r.scope);
|
|
183
|
+
if (deniedScopes.length > 0) {
|
|
184
|
+
return {
|
|
185
|
+
kind: "deny",
|
|
186
|
+
reason: `org scope(s) not admitted for this principal: ${deniedScopes.join(", ")}`,
|
|
187
|
+
outcome: "denied",
|
|
188
|
+
details: { deniedScopes },
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
const writeScope = requestedWriteScope === null
|
|
192
|
+
? null
|
|
193
|
+
: requestedWriteScope.origin === "deployment"
|
|
194
|
+
? requestedWriteScope.scope
|
|
195
|
+
: lookup.scopes[requestedWriteScope.scope]?.write === true
|
|
196
|
+
? requestedWriteScope.scope
|
|
197
|
+
: null;
|
|
198
|
+
return { kind: "admit", verdict: { ok: true, scopes: fullScopes, writeScope } };
|
|
199
|
+
}
|
|
200
|
+
const decision = await decide();
|
|
201
|
+
if (decision.kind === "admit") {
|
|
202
|
+
// C10(audit=纯观察**零**行为变化):write 收窄也是行为变化(enforce 下 harvest 静默零提交)——
|
|
203
|
+
// audit 模式对「读面通过但 write 未显式授」的判决只记 would-narrow,返回未收窄的全额;
|
|
204
|
+
// enforce 照常收窄。ok:true 分支的 writeScope 恒为 fullWriteScope 或 null(decide 只做这两值)。
|
|
205
|
+
if (opts.mode === "audit" && decision.verdict.ok && decision.verdict.writeScope !== fullWriteScope) {
|
|
206
|
+
emit("ok", { audited: true, wouldNarrowWriteScope: true });
|
|
207
|
+
return admitFull();
|
|
208
|
+
}
|
|
209
|
+
emit("ok");
|
|
210
|
+
return decision.verdict;
|
|
211
|
+
}
|
|
212
|
+
if (opts.mode === "audit") {
|
|
213
|
+
// Zero behavior change: a would-deny (terminal or transient) is overridden to a full, UNNARROWED admit —
|
|
214
|
+
// the real decision is only ever recorded via onOutcome, never enforced.
|
|
215
|
+
emit(decision.outcome, { ...decision.details, audited: true, wouldDeny: true, ...(decision.kind === "unavailable" ? { retryAfterMs: decision.retryAfterMs } : { reason: decision.reason }) });
|
|
216
|
+
return admitFull();
|
|
217
|
+
}
|
|
218
|
+
emit(decision.outcome, decision.details);
|
|
219
|
+
if (decision.kind === "deny")
|
|
220
|
+
return { ok: false, reason: decision.reason };
|
|
221
|
+
throw new OrgMemoryAdmissionRetryError(`org memory directory unavailable (${decision.lookupReason})`, decision.retryAfterMs);
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
//# sourceMappingURL=org-memory-admission.js.map
|
package/dist/question.d.ts
CHANGED
|
@@ -67,6 +67,11 @@ export declare class QuestionCoordinator {
|
|
|
67
67
|
};
|
|
68
68
|
/** Test/observability hook: number of currently-parked questions. */
|
|
69
69
|
pendingCount(): number;
|
|
70
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条活流腿的
|
|
71
|
+
* per-run 上下文里(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade=false)。
|
|
72
|
+
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
73
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 */
|
|
74
|
+
hasLiveContext(): boolean;
|
|
70
75
|
private countersFor;
|
|
71
76
|
}
|
|
72
77
|
//# sourceMappingURL=question.d.ts.map
|
package/dist/question.js
CHANGED
|
@@ -192,6 +192,13 @@ export class QuestionCoordinator {
|
|
|
192
192
|
pendingCount() {
|
|
193
193
|
return this.pending.size;
|
|
194
194
|
}
|
|
195
|
+
/** #152 ([2703] 案二):durable 部署上的 AskUserQuestion 判决探针——本调用点是否处在某条活流腿的
|
|
196
|
+
* per-run 上下文里(runWithContext 包裹的 bg/SSE 腿=true;sync /v1/tasks、verify/cascade=false)。
|
|
197
|
+
* resolve-spec 的 durable question policy 用它在**判决时**分腿:有活流 ⇒ allow(问活人),无 ⇒
|
|
198
|
+
* ask(durable park)。ALS 让这个判断天然 per-leg,policy 组装期不必预知腿别。 */
|
|
199
|
+
hasLiveContext() {
|
|
200
|
+
return this.als.getStore() !== undefined;
|
|
201
|
+
}
|
|
195
202
|
countersFor(taskId) {
|
|
196
203
|
let rc = this.counters.get(taskId);
|
|
197
204
|
if (!rc) {
|
package/dist/runs.d.ts
CHANGED
|
@@ -106,6 +106,13 @@ export declare function isSessionConflictResult(result: {
|
|
|
106
106
|
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
107
107
|
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
108
108
|
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
109
|
+
*
|
|
110
|
+
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
111
|
+
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
112
|
+
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
113
|
+
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
114
|
+
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
115
|
+
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
109
116
|
*/
|
|
110
117
|
export declare function resumeAtHttpStatus(result: {
|
|
111
118
|
status: string;
|
package/dist/runs.js
CHANGED
|
@@ -3,7 +3,7 @@ import { withPrincipal } from "./observability/principal-context.js";
|
|
|
3
3
|
import { redactSecrets } from "./trace/redact.js";
|
|
4
4
|
import { taskNotificationEventData, appendModelUsageDelta, appendPromptManifest, attachModelUsage } from "./trace/project.js";
|
|
5
5
|
import { createLedgerSink } from "./trace/ledger-sink.js";
|
|
6
|
-
import { fleetRunResiduals } from "./fleet/fleet-bus.js";
|
|
6
|
+
import { fleetRunResiduals, isFleetAgentTerminalNotification } from "./fleet/fleet-bus.js";
|
|
7
7
|
import { defaultSubagentTailBus, projectTailFrame } from "./fleet/subagent-tail-bus.js";
|
|
8
8
|
import { emitPendingWorkflowCompletions, taskNotificationInboxEntry, taskNotificationStreamKey, NotifiedKeys } from "./orchestration/workflow-completion-inbox.js";
|
|
9
9
|
/**
|
|
@@ -148,6 +148,13 @@ export function isSessionConflictResult(result) {
|
|
|
148
148
|
* UNCHANGED for the shell). `rewind_snapshot.unresolvable` (rewindFiles + "before": no snapshot at/above the branch
|
|
149
149
|
* point) is core's third rejection with a DIFFERENT prefix — same caller-mistake shape (prepare-throw, nothing
|
|
150
150
|
* billed), map it to 422 explicitly so it doesn't fall through to a 200-with-failed body.
|
|
151
|
+
*
|
|
152
|
+
* design/170 件A(#148 件4): core 5.13.0's org-memory admission rides the SAME prepare-throw pipe
|
|
153
|
+
* (GOVERNANCE_CODES). `memory.admission_denied` is TERMINAL — the principal has no grant on that tenant
|
|
154
|
+
* plane → 403 (a retry cannot change the verdict). `memory.admission_required` is TRANSIENT fail-closed —
|
|
155
|
+
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
156
|
+
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
157
|
+
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
151
158
|
*/
|
|
152
159
|
export function resumeAtHttpStatus(result) {
|
|
153
160
|
if (result.status !== "failed")
|
|
@@ -156,6 +163,10 @@ export function resumeAtHttpStatus(result) {
|
|
|
156
163
|
return 404;
|
|
157
164
|
if (result.errorCode === "rewind_snapshot.unresolvable")
|
|
158
165
|
return 422;
|
|
166
|
+
if (result.errorCode === "memory.admission_denied")
|
|
167
|
+
return 403;
|
|
168
|
+
if (result.errorCode === "memory.admission_required")
|
|
169
|
+
return 503;
|
|
159
170
|
if (!(result.errorCode?.startsWith("resume_at.") ?? false))
|
|
160
171
|
return undefined;
|
|
161
172
|
return result.errorCode === "resume_at.not_found" ? 404 : 422;
|
|
@@ -575,8 +586,11 @@ promptManifests) {
|
|
|
575
586
|
}
|
|
576
587
|
// id-domain alias (core): fleet child rows key by the TICK's taskId = the child's sessionId
|
|
577
588
|
// (uuid domain); the notification's task_id is the a* domain — payload.sessionId IS the tick-domain
|
|
578
|
-
// alias, so flip by it (fallback task_id for pre-1.238 payloads
|
|
579
|
-
|
|
589
|
+
// alias, so flip by it (fallback task_id for pre-1.238 payloads).
|
|
590
|
+
// [2687-cli] 幽灵行案:onChildTerminal 只对 **agent 族终态**调用(isFleetAgentTerminalNotification
|
|
591
|
+
// 单源判别,理由与病灶链见其 doc 注)。通知帧/park 面照走,只掐 fleet 铸行。三消费点孪生同形
|
|
592
|
+
// (http/server.ts resume 腿、routes/tasks.ts sync 腿)。
|
|
593
|
+
const hadRow = isFleetAgentTerminalNotification(n) ? (fleetPublisher?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false) : false;
|
|
580
594
|
const parked = !legLive && Boolean(workflowCompletionInbox && spec.sessionId);
|
|
581
595
|
// diagnosability: one line per observed bg completion. hadFleetRow=false = the terminal frame was
|
|
582
596
|
// FLIP-THROUGH synthesized (the row was gone — parent settle removed it, or a bg BASH never ticked).
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sema-agent/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "7.0.0",
|
|
4
4
|
"description": "Sema Server — the server/API implementation layer for Sema, wiring core, registry, model providers, and cloud agent execution. Built on @sema-agent/core.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"build:binary:run-local:darwin-arm64": "bun build --compile --target=bun-darwin-arm64 src/run-local.ts --outfile dist/run-local-darwin-arm64"
|
|
55
55
|
},
|
|
56
56
|
"dependencies": {
|
|
57
|
-
"@sema-agent/core": "^5.
|
|
58
|
-
"@sema-agent/registry-core": "^0.
|
|
57
|
+
"@sema-agent/core": "^5.13.0",
|
|
58
|
+
"@sema-agent/registry-core": "^0.16.0",
|
|
59
59
|
"e2b": "^2.28.0",
|
|
60
60
|
"libsodium-wrappers": "^0.8.4",
|
|
61
61
|
"mysql2": "^3.22.4",
|