@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.
- package/dist/bake-runner/main.js +11 -0
- package/dist/boot/leader.d.ts +11 -0
- package/dist/boot/leader.js +22 -0
- 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 +31 -6
- package/dist/fleet/fleet-bus.d.ts +9 -0
- package/dist/fleet/fleet-bus.js +10 -0
- package/dist/http/routes/session-sync.js +4 -0
- package/dist/http/routes/tasks.js +11 -3
- package/dist/http/server.js +8 -5
- package/dist/leader/wire.d.ts +11 -1
- package/dist/leader/wire.js +63 -47
- 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 +20 -4
- package/dist/spec-fields.js +4 -3
- package/package.json +3 -3
|
@@ -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/leader/wire.d.ts
CHANGED
|
@@ -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
|
package/dist/leader/wire.js
CHANGED
|
@@ -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 =
|
|
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
|
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
|