@sema-agent/server 6.7.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.
@@ -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
- const hadRow = fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false;
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
- return { status: resumeAtStatus, body: { errorCode: result.errorCode, error: result.errorMessage ?? "resume-at failed" } };
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
@@ -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`, which
1376
- // the durable path always sets (QUESTION_AWAITS_RESUME on suspend, the answer closure on resume). Without
1377
- // this it 422s on resume exactly like a hand tool would. Gated on checkpointStore = durable mode is active.
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
- const hadRow = fleetPub?.onChildTerminal(n.sessionId ?? n.task_id, n.status, n.task_id, n.toolUseId) ?? false;
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 });
@@ -1,6 +1,6 @@
1
1
  import { type Brain, type Model, type ModelRoles, type ModelPricing, type TaskSpec, type ExecutionEnvFactory, type RemoteExecutionEnv, type ToolResultStore, type SessionStore } from "@sema-agent/core";
2
2
  import type { CheckpointStoreFull } from "../plugins/store-backend.js";
3
- import { type LeaderResult } from "./leader.js";
3
+ import { type LeaderDeps, type LeaderResult } from "./leader.js";
4
4
  import type { LeaderRequestBody } from "./endpoint.js";
5
5
  export interface LeaderWireConfig {
6
6
  /** Static-env compat mode (E2B only): the leader provisions/owns each env. Required when `envFactory` unset. */
@@ -162,6 +162,16 @@ export declare function leaderLoopConfig(cfg: {
162
162
  repairRounds?: number;
163
163
  conflictRounds?: number;
164
164
  }, env?: NodeJS.ProcessEnv): LeaderLoopConfig;
165
+ export declare const LEADER_PUSH_NETWORK_TIMEOUT_MS = 120000;
166
+ export declare const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30000;
167
+ /** Coordinator(control plane,持有推送凭据)的 git push 闭包——从 `createLeaderRunner` 抽出为独立工厂,
168
+ * 仅依赖 durableRemote/targetRef/git 身份(不依赖 wire 内其余状态),因此可以脱离完整的
169
+ * plan→provision→fan-out→merge 管线单测(那条管线需要真 brain + E2B/k8s env)。行为与抽出前逐字相同,
170
+ * 唯一新增是六条 execFileSync 各自的 `timeout`。 */
171
+ export declare function createCoordinatorPush(durableRemote: string, targetRef: string | undefined, ident: {
172
+ name: string;
173
+ email: string;
174
+ }): LeaderDeps["push"];
165
175
  export declare function createLeaderRunner(cfg: LeaderWireConfig): (body: LeaderRequestBody) => Promise<LeaderResult>;
166
176
  export {};
167
177
  //# sourceMappingURL=wire.d.ts.map
@@ -177,6 +177,68 @@ export function leaderLoopConfig(cfg, env = process.env) {
177
177
  ...(env.LEADER_BUDGET_USD ? { replanBudgetUsd: parseNumOrFailNonNegative("LEADER_BUDGET_USD", env.LEADER_BUDGET_USD) } : {}),
178
178
  };
179
179
  }
180
+ // 鲁棒性批5 A4(2026-08-05):Coordinator 的六条 execFileSync(clone/config×2/am/ls-remote/push×2)此前零超时。
181
+ // `execFileSync` 是**同步**调用——它挂起的是整个 Node 事件循环,不只是这一次 leader 跑;`durableRemote`
182
+ // 是运维配的、可能半开/不可达的远端(DNS 黑洞、TCP SYN 丢、对端 git-upload-pack 卡死),一旦挂住,这一个
183
+ // 副本上**所有并发请求**一起冻结,直到操作系统层 TCP 超时(可能几十分钟)才会松绑。
184
+ // 上界两档:clone/push/ls-remote 是网络往返(仓可能较大,给 120s);config/am 是纯本地操作(不该合法挂起,
185
+ // 但损坏的 object store / 文件锁可能卡住 `git am`),给 30s——同 fleet-client.ts `FLEET_CLIENT_FETCH_TIMEOUT_MS`
186
+ // 判据的普适形(操作分类决定上界,不是一刀切)。
187
+ export const LEADER_PUSH_NETWORK_TIMEOUT_MS = 120_000;
188
+ export const LEADER_PUSH_LOCAL_TIMEOUT_MS = 30_000;
189
+ /** Coordinator(control plane,持有推送凭据)的 git push 闭包——从 `createLeaderRunner` 抽出为独立工厂,
190
+ * 仅依赖 durableRemote/targetRef/git 身份(不依赖 wire 内其余状态),因此可以脱离完整的
191
+ * plan→provision→fan-out→merge 管线单测(那条管线需要真 brain + E2B/k8s env)。行为与抽出前逐字相同,
192
+ * 唯一新增是六条 execFileSync 各自的 `timeout`。 */
193
+ export function createCoordinatorPush(durableRemote, targetRef, ident) {
194
+ return async (integratedPatch, baseSha) => {
195
+ // Coordinator (control plane, creds): clone durable remote, apply integrated series, force-with-lease push.
196
+ // 🔴 requires `git` on the service host PATH (deploy req).
197
+ const dir = mkdtempSync(join(tmpdir(), "leader-coord-"));
198
+ // Force C locale so the `raced` regex below matches git's (English) push-rejection messages regardless of
199
+ // the host locale — otherwise a translated message → regex miss → a real race mislabeled non-retryable (council #5).
200
+ const gitEnv = { ...process.env, LC_ALL: "C" };
201
+ try {
202
+ execFileSync("git", ["clone", "-q", durableRemote, dir], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
203
+ execFileSync("git", ["-C", dir, "config", "user.name", ident.name], { timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
204
+ execFileSync("git", ["-C", dir, "config", "user.email", ident.email], { timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
205
+ writeFileSync(join(dir, ".leader.patch"), integratedPatch);
206
+ execFileSync("git", ["-C", dir, "am", "--3way", ".leader.patch"], { env: gitEnv, timeout: LEADER_PUSH_LOCAL_TIMEOUT_MS });
207
+ rmSync(join(dir, ".leader.patch"));
208
+ const ref = targetRef ?? "refs/heads/main";
209
+ // CAS against the TARGET ref's ACTUAL current tip (not baseSha). The old `=ref:baseSha` lease assumed
210
+ // the target ref already pointed at baseSha — but a FRESH ref (or one at any other commit) is never at
211
+ // baseSha, so the lease rejected EVERY such push and mislabeled it "raced" (found live: auto1 reached
212
+ // push after a clean 6-worker fan-out + green gradle, and failed only here). ls-remote gives the ref's
213
+ // current oid (empty = it doesn't exist yet) → lease against THAT, which still detects a real concurrent
214
+ // merge race (the ref moved since we read it) but lets a normal create/update through.
215
+ // Does the target ref already exist? (exact full-ref match; --refs drops peeled annotated-tag lines.)
216
+ const ls = execFileSync("git", ["-C", dir, "ls-remote", "--refs", "origin", ref], { encoding: "utf8", env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
217
+ const exists = ls.split("\n").some((l) => l.split("\t")[1] === ref);
218
+ if (exists) {
219
+ // Existing ref → keep the original CAS against baseSha (detects a concurrent merge that moved the ref
220
+ // off the base this run started from — council#2). NOT against a just-read end-of-run tip: that would
221
+ // let leader B force-push over leader A's concurrent merge (Codex review #1). A ref that exists but
222
+ // isn't at baseSha → refuse (use a fresh per-run targetRef to avoid that — the proving-ground does).
223
+ execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:${baseSha}`, "origin", `HEAD:${ref}`], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
224
+ }
225
+ else {
226
+ // Absent → create with a "must not exist" lease (empty expected oid): a concurrent create races safely
227
+ // (rejected as stale info) instead of silently fast-forwarding. The original `:baseSha` lease wrongly
228
+ // rejected a fresh ref (it's never at baseSha) — that was auto1's only failure after a green build.
229
+ execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:`, "origin", `HEAD:${ref}`], { env: gitEnv, timeout: LEADER_PUSH_NETWORK_TIMEOUT_MS });
230
+ }
231
+ return { ok: true, ref };
232
+ }
233
+ catch (e) {
234
+ const msg = e instanceof Error ? e.message : String(e);
235
+ return { ok: false, raced: /stale info|force-with-lease|\[rejected\]|non-fast-forward/.test(msg), error: msg };
236
+ }
237
+ finally {
238
+ rmSync(dir, { recursive: true, force: true });
239
+ }
240
+ };
241
+ }
180
242
  export function createLeaderRunner(cfg) {
181
243
  const W = cfg.workspace ?? "/home/user";
182
244
  const repo = `${W}/repo`;
@@ -563,53 +625,7 @@ export function createLeaderRunner(cfg) {
563
625
  }
564
626
  return { env: asIntegrationEnv(env), destroy: () => env.destroy().then(() => { }), ...(mkRepair(env, repo, body.oracleFiles ?? []) ? { repair: mkRepair(env, repo, body.oracleFiles ?? []) } : {}), ...(mkConflictResolver(env, repo, body.oracleFiles ?? []) ? { conflictResolver: mkConflictResolver(env, repo, body.oracleFiles ?? []) } : {}) };
565
627
  };
566
- const push = async (integratedPatch, baseSha) => {
567
- // Coordinator (control plane, creds): clone durable remote, apply integrated series, force-with-lease push.
568
- // 🔴 requires `git` on the service host PATH (deploy req).
569
- const dir = mkdtempSync(join(tmpdir(), "leader-coord-"));
570
- // Force C locale so the `raced` regex below matches git's (English) push-rejection messages regardless of
571
- // the host locale — otherwise a translated message → regex miss → a real race mislabeled non-retryable (council #5).
572
- const gitEnv = { ...process.env, LC_ALL: "C" };
573
- try {
574
- execFileSync("git", ["clone", "-q", body.durableRemote, dir], { env: gitEnv });
575
- execFileSync("git", ["-C", dir, "config", "user.name", ident.name]);
576
- execFileSync("git", ["-C", dir, "config", "user.email", ident.email]);
577
- writeFileSync(join(dir, ".leader.patch"), integratedPatch);
578
- execFileSync("git", ["-C", dir, "am", "--3way", ".leader.patch"], { env: gitEnv });
579
- rmSync(join(dir, ".leader.patch"));
580
- const ref = body.targetRef ?? "refs/heads/main";
581
- // CAS against the TARGET ref's ACTUAL current tip (not baseSha). The old `=ref:baseSha` lease assumed
582
- // the target ref already pointed at baseSha — but a FRESH ref (or one at any other commit) is never at
583
- // baseSha, so the lease rejected EVERY such push and mislabeled it "raced" (found live: auto1 reached
584
- // push after a clean 6-worker fan-out + green gradle, and failed only here). ls-remote gives the ref's
585
- // current oid (empty = it doesn't exist yet) → lease against THAT, which still detects a real concurrent
586
- // merge race (the ref moved since we read it) but lets a normal create/update through.
587
- // Does the target ref already exist? (exact full-ref match; --refs drops peeled annotated-tag lines.)
588
- const ls = execFileSync("git", ["-C", dir, "ls-remote", "--refs", "origin", ref], { encoding: "utf8", env: gitEnv });
589
- const exists = ls.split("\n").some((l) => l.split("\t")[1] === ref);
590
- if (exists) {
591
- // Existing ref → keep the original CAS against baseSha (detects a concurrent merge that moved the ref
592
- // off the base this run started from — council#2). NOT against a just-read end-of-run tip: that would
593
- // let leader B force-push over leader A's concurrent merge (Codex review #1). A ref that exists but
594
- // isn't at baseSha → refuse (use a fresh per-run targetRef to avoid that — the proving-ground does).
595
- execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:${baseSha}`, "origin", `HEAD:${ref}`], { env: gitEnv });
596
- }
597
- else {
598
- // Absent → create with a "must not exist" lease (empty expected oid): a concurrent create races safely
599
- // (rejected as stale info) instead of silently fast-forwarding. The original `:baseSha` lease wrongly
600
- // rejected a fresh ref (it's never at baseSha) — that was auto1's only failure after a green build.
601
- execFileSync("git", ["-C", dir, "push", `--force-with-lease=${ref}:`, "origin", `HEAD:${ref}`], { env: gitEnv });
602
- }
603
- return { ok: true, ref };
604
- }
605
- catch (e) {
606
- const msg = e instanceof Error ? e.message : String(e);
607
- return { ok: false, raced: /stale info|force-with-lease|\[rejected\]|non-fast-forward/.test(msg), error: msg };
608
- }
609
- finally {
610
- rmSync(dir, { recursive: true, force: true });
611
- }
612
- };
628
+ const push = createCoordinatorPush(body.durableRemote, body.targetRef, ident);
613
629
  const deps = {
614
630
  plan,
615
631
  provisionWorker,
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
@@ -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
@@ -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